Files
Aislo/B09_Estimation/B09_Estimation_UI_Tabs_Base.ts
T

320 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* B09_Estimation_UI_Tabs_Base.ts
* B09 바탕 단가 탭 일곱(PLAN 144-2~4) — 노무비 · 경비목록표 · 환율및기초자료 · 자재단가대비표 ·
* 재료비목록표 · 중기사용료 · 중기목록표(실무 엑셀 꼴 · 값 = `B09_Estimation_Calc_Base`).
* 재료비목록표 = 엑셀 꼴 + 수량(B08 합계) · 총액 열(166-3)
*
* 고칠 칸 = 자재단가대비표 적용 단가(빈칸으로 지우면 최저 자동) · 관급 / 별도 칸 누름 = 표시 바꿈
* · 견적 단가 · 면 손입력 · 적용 「출처」 칸 누름 = 고를 출처 돌림(값 있는 출처만 · 견적이 높아도)
* · 「견적서」 칸 누름 = 파일 모달(`B09_Estimation_UI_Tabs_Base_Quote`)(166-4)
* 이동 = 재료비목록표 줄 → 대비표 · 중기목록표 줄 → 중기사용료 블록 · 블록 줄 → 경비 · 기초 · 대비표
* ========================================================================== */
import type { DataRow } from "@ui/ui_template_data_table";
import { SOURCES, type Row } from "./B09_Estimation_Calc_Base";
import { openQuotes } from "./B09_Estimation_UI_Tabs_Base_Quote";
import {
money,
parseNumber,
qty,
text,
type TabBuild,
type TabId,
type TabTable,
} from "./B09_Estimation_UI_Tabs";
const LINK_HINT = "줄을 누르면 단가를 낸 탭 · 줄로 이동";
/** 단가 줄 표(노무비 · 경비목록표) — 호표 · 명칭 · 규격 · 단위 · 단가 · 비고(출처) */
function priceList(rows: Row[], unitPrice: string, hint: string): TabTable {
return {
columns: [
qty("no", "호표", 0),
text("name", "명칭", 200),
text("spec", "규격", 140),
text("unit", "단위", 56),
money("price", unitPrice),
text("source", "비고", 220),
],
rows: rows.map((r) => ({ ...r })),
frozen: 2,
hint,
};
}
export const buildLabor: TabBuild = ({ sheets, payload }) =>
priceList(
sheets.labor || [],
"단가(원/인)",
`노임 = 굳힌 단가(${madeText(payload)}) · M01 인력 · 바꾸려면 [단가 갱신]`,
);
export const buildExpense: TabBuild = ({ sheets, payload }) =>
priceList(
sheets.expense || [],
"기준가(천원)",
`기계 손료 기준가 = 굳힌 단가(${madeText(payload)}) · M01 기계 가격 · 바꾸려면 [단가 갱신]`,
);
function madeText(payload: Row): string {
return payload.book?.made ? `${String(payload.book.made).slice(0, 10)} 굳힘` : "아직 안 굳힘";
}
export const buildBasis: TabBuild = ({ sheets }): TabTable => {
const basis: Row = sheets.basis || { fx: [], drivers: [] };
const rows: DataRow[] = [
...basis.fx.map((f: Row) => ({
part: "1. 환율",
key: f.key,
name: f.name,
spec: f.date,
unit: f.unit,
day: null,
factor: "",
value: f.value,
})),
...basis.drivers.map((d: Row) => ({
part: "2. 인건비",
key: d.key,
name: d.name,
spec: `직종 ${d.key}`,
unit: "원/시간",
day: d.day,
factor: d.factor,
value: d.hour,
labor_key: d.labor_key,
})),
];
return {
columns: [
text("part", "구분", 90),
text("name", "명칭", 160),
text("spec", "규격 · 기준일", 120),
text("unit", "단위", 80),
money("day", "일당(원)"),
text("factor", "시간 환산", 160),
money("value", "값", undefined, 2),
],
rows,
frozen: 2,
hint: "운전사 시간 노임 = ROUNDDOWN(일당 × 1/8 × 16/12 × 25/20, 0)(M01 CC000336 · 건협기술-2230) · 환율 = M01 한국은행 · 인건비 줄 누름 = 노무비목록표",
link: (i) => {
const row = rows[i];
return row?.labor_key ? { tab: "labor", key: String(row.labor_key) } : null;
},
};
};
const SUPPLY_NEXT: Record<string, string | null> = { "": "관급", 관급: "별도", 별도: null };
/** 고를 출처 돌림 — 자동 → 값 있는 출처 차례 → 자동 */
function nextPick(r: Row): string | null {
const have = SOURCES.filter((s) => (r.prices[s] ?? 0) > 0);
return have[have.indexOf(r.pick) + 1] ?? null;
}
export const buildCompare: TabBuild = ({ sheets, user, setUser }): TabTable => {
const rows: Row[] = sheets.compare || [];
const out: DataRow[] = rows.map((r) => {
const files: string[] = (user.materials || {})[r.key]?.files || [];
const row: DataRow = {
no: r.no,
key: r.key,
name: r.name,
spec: r.spec,
unit: r.unit,
lowest: r.lowest,
applied: r.applied,
applied_from: r.pick ? `${r.pick} 고름` : (r.applied_from ?? ""),
supply: r.supply ?? "",
files: files.length ? `${files.length} 파일` : "첨부",
};
for (const s of SOURCES) {
row[`p:${s}`] = r.prices[s];
row[`g:${s}`] = r.pages[s] ?? "";
}
return row;
});
const mine = (key: string): Row => ({ ...((user.materials || {})[key] || {}) });
const put = (key: string, cell: Row): void => {
const materials = { ...(user.materials || {}) };
if (Object.keys(cell).length) materials[key] = cell;
else delete materials[key];
setUser({ ...user, materials });
};
return {
columns: [
qty("no", "번호", 0),
text("name", "품명", 180),
text("spec", "규격", 140),
text("unit", "단위", 56),
...SOURCES.flatMap((s) => [money(`p:${s}`, "단가", [s]), text(`g:${s}`, "면", 64, [s])]),
money("lowest", "최저"),
money("applied", "적용 단가", ["적용"]),
text("applied_from", "출처", 80, ["적용"]),
text("supply", "관급 · 별도", 80),
text("files", "견적서", 72),
],
rows: out,
frozen: 2,
hint: "적용 단가 = 출처 중 최저(0 · 빈칸 뺌 · 견적 포함) 자동 · 칸에 넣으면 사용자 값 · 지우면 다시 최저 · 「출처」 칸 누름 = 고를 출처 돌림(값 있는 출처 · 견적이 높아도 고름) · 견적 단가 · 면 = 손입력 · 「관급 · 별도」 칸 누름 = 없음 → 관급 → 별도 · 「견적서」 칸 누름 = 파일 올리기 · 보기",
editable: (_i, key) => key === "applied" || key === "p:견적" || key === "g:견적",
onEdit: (i, key, value) => {
const row = out[i];
if (!row) return;
const cell = mine(String(row.key));
if (key === "g:견적") {
if (value.trim()) cell.quote_page = value.trim();
else delete cell.quote_page;
return put(String(row.key), cell);
}
const v = parseNumber(value);
if (v === undefined) return;
const field = key === "applied" ? "applied" : key === "p:견적" ? "quote" : null;
if (!field) return;
if (v === null) delete cell[field];
else cell[field] = v;
if (field === "applied") delete cell.pick; // 손 수 = 고른 출처를 풂
put(String(row.key), cell);
},
onCell: (i, key) => {
const row = out[i];
const r = rows[i];
if (!row || !r) return;
const cell = mine(String(row.key));
if (key === "files") {
const label = `${r.name} ${r.spec}`.trim();
return openQuotes(r.key, label, (names) => {
const now = mine(r.key);
if (JSON.stringify(now.files || []) === JSON.stringify(names)) return;
if (names.length) now.files = names;
else delete now.files;
put(r.key, now);
});
}
if (key === "applied_from") {
const pick = nextPick(r);
if (pick) cell.pick = pick;
else delete cell.pick;
return put(String(row.key), cell);
}
if (key !== "supply") return;
const next = SUPPLY_NEXT[String(row.supply ?? "")] ?? null;
if (next) cell.supply = next;
else delete cell.supply;
put(String(row.key), cell);
},
};
};
export const buildMaterial: TabBuild = ({ sheets }): TabTable => {
const rows: DataRow[] = (sheets.material || []).map((r: Row) => ({
...r,
supply: r.supply ?? "",
}));
return {
columns: [
qty("no", "호표", 0),
text("name", "명칭", 200),
text("spec", "규격", 160),
text("unit", "단위", 56),
qty("qty", "수량", 3),
money("price", "단가"),
money("amount", "총액"),
text("supply", "관급 · 별도", 80),
text("origin", "비고", 90),
],
rows,
total: rows.length
? { name: "계", amount: rows.reduce((sum, r) => sum + (Number(r.amount) || 0), 0) }
: null,
frozen: 2,
hint: `수량 = B08 총괄자재수량집계표 합계 열(할증 포함 · 사본 자원 줄은 빈칸) · 단가 = 자재단가대비표 적용 단가 · 총액 = ROUND(수량 × 단가, 0) · 계 = 빈칸 뺀 합 · ${LINK_HINT}`,
link: (i) => (rows[i] ? { tab: "compare", key: String(rows[i].key) } : null),
};
};
export const buildMachine: TabBuild = ({ sheets }): TabTable => {
const rows: DataRow[] = (sheets.machine || []).map((r: Row) => ({ ...r }));
return {
columns: [
qty("no", "호표", 0),
text("name", "명칭", 200),
text("spec", "규격", 120),
text("unit", "단위", 56),
money("sum", "합계"),
money("labor", "노무비"),
money("material", "재료비"),
money("expense", "경비"),
],
rows,
frozen: 2,
hint: `시간당 = 중기사용료 블록 합계 · ${LINK_HINT}`,
link: (i) => (rows[i] ? { tab: "machine_blocks", key: String(rows[i].key) } : null),
};
};
const KIND_TAB: Record<string, TabId> = {
expense: "expense",
labor: "basis",
material: "compare",
};
export const buildMachineBlocks: TabBuild = ({ sheets }): TabTable => {
const rows: DataRow[] = [];
const links: Array<{ tab: TabId; key: string } | null> = [];
(sheets.machine_blocks || []).forEach((b: Row, n: number) => {
rows.push({ key: b.key, name: `제 ${n + 1} 호표 ${b.name}`, spec: b.spec, unit: b.unit });
links.push(null);
for (const ln of b.lines) {
const at = (part: string): number | null => (ln.kind === part ? ln.price : null);
const amt = (part: string): number | null => (ln.kind === part ? ln.amount : null);
rows.push({
key: `${b.key}|${ln.name}`,
name: ` ${ln.name}`,
spec: ln.spec,
qty: ln.qty,
unit: ln.unit,
sum_price: ln.price,
sum: ln.amount,
labor_price: at("labor"),
labor: amt("labor"),
material_price: at("material"),
material: amt("material"),
expense_price: at("expense"),
expense: amt("expense"),
});
links.push(ln.ref?.key ? { tab: KIND_TAB[ln.kind] ?? "compare", key: ln.ref.key } : null);
}
rows.push({
key: `${b.key}|합계`,
name: " 합 계",
sum: b.sum,
labor: b.labor,
material: b.material,
expense: b.expense,
});
links.push(null);
});
const pair = (key: string, label: string) => [
money(`${key}_price`, "단가", [label], 1),
money(key, "금액", [label], 1),
];
return {
columns: [
text("name", "명칭", 220),
text("spec", "규격", 120),
qty("qty", "수량"),
text("unit", "단위", 56),
...pair("sum", "합계"),
...pair("labor", "노무비"),
...pair("material", "재료비"),
...pair("expense", "경비"),
],
rows,
frozen: 1,
hint: "손료 = 기준가(천원) × 손료계수 ÷ 10,000 · 운전사 = 인 × 시간 노임 · 연료 = ℓ/hr × 적용 단가 · 잡품 = 연료 금액 × % · 줄 ROUNDDOWN 1 · 비목 합 ROUNDDOWN 0 · 줄 누름 = 단가를 낸 탭",
link: (i) => links[i] ?? null,
};
};