/* ============================================================================= * M02_MasterTemplete_Labor_Rows.ts * 일위대가 담은 로직 표 · 자재목록 표(39-3) — 상세 · 보기 갈래(면적산출 오른쪽 패널 · 카드)가 같이 씀. * 담은 로직 = 차례 · 로직(키 · 이름 + 작은 글 「지역 서울 · 규격 25mm · 수량 B6 = 12.5㎡」 · 「빈 값 n」 붉게) · * 결과 단위 · 금액 칸 넷(노무비 · 재료비 · 경비 · 합계 · 칸마다 위 기준 작게 / 아래 반영) · 맨 아래 합계 줄. * 자재목록 = 코드 · 이름 · 규격 · 단위 · 수량 · 기준 단가 · 반영 금액. 금액은 calc 답(없으면 「…」). * 줄 누름 · 순서 단추는 `on` 이 있을 때만(보기 갈래 = 없음). * ========================================================================== */ import { createButton, el } from "@ui/ui_template_elements"; import { formatMoney } from "../M01_MasterData/M01_MasterData_UI_Logic_Money"; import type { ItemLogic } from "./M02_MasterTemplete_Api_Fetch"; import type { LaborCalc, LaborCalcLine, LaborCalcMat, LaborCost, LaborMat, LaborRow, } from "./M02_MasterTemplete_Labor_Api"; import { tc } from "./M02_MasterTemplete_Labor_Text"; import "./M02_MasterTemplete_Labor.css"; import { valueLine } from "./M02_Labor_Values_Model"; const COSTS = ["노무비", "재료비", "경비", "계"] as const; const HEAD: Record<(typeof COSTS)[number], Parameters[0]> = { 노무비: "Prev_Labor", 재료비: "Prev_Material", 경비: "Prev_Expense", 계: "Prev_Sum", }; const num = (v: number): string => Number(v.toPrecision(12)).toLocaleString("ko-KR", { maximumFractionDigits: 6 }); /** 수량 글 — 「수량 손 3 개소」 · 「수량 B6 = 12.5 ㎡」 · 없으면 빈 글 */ export function qtyText( line: { 수량: number | null; 수량출처: string | null } | undefined, unit: string, ): string { if (!line || line.수량 === null || line.수량 === undefined) return ""; const v = `${num(line.수량)}${unit ? ` ${unit}` : ""}`; if (!line.수량출처 || line.수량출처 === "손") return `${tc("Labor_Qty")} ${tc("In_QtyHand")} ${v}`; return `${tc("Labor_Qty")} ${line.수량출처} = ${v}`; } /** 금액 칸 하나 — 위 기준(작게) · 아래 반영 */ function moneyCell( base: number | null | undefined, applied: number | null | undefined, wait: boolean, ): HTMLElement { const show = (v: number | null | undefined): string => wait ? "…" : v === null || v === undefined ? "—" : formatMoney(v); return el("td", { className: "m02-labrow__money", children: [ el("small", { className: "m02-labrow__base", text: show(base), attrs: { title: tc("Labor_Base") }, }), el("span", { className: "m02-labrow__applied", text: show(applied), attrs: { title: tc("Labor_Applied") }, }), ], }); } const table = (heads: string[], rows: HTMLElement[], foot: HTMLElement[]): HTMLElement => el("div", { className: "ui-general-block__table-wrap", children: [ el("table", { className: "ui-general-block__table m02-labrow", children: [ el("thead", { children: [el("tr", { children: heads.map((t) => el("th", { text: t })) })], }), el("tbody", { children: rows }), ...(foot.length ? [el("tfoot", { children: foot })] : []), ], }), ], }); /** 줄 누름 · 순서 · 빼기 — 순서 · 빼기가 없으면 그 칸 없음 */ export interface RowHandlers { open: (i: number) => void; move?: (i: number, d: number) => void; remove?: (i: number) => void; } export interface LogicTableData { rows: LaborRow[]; logics: Map; /** calc 답 — null = 계산 중 · 못 셈은 `fail` */ calc: LaborCalc | null; fail?: string | null; } /** 담은 로직 표 — 보기 갈래는 `on` 없이 */ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement { const wait = !data.calc && !data.fail; const { move, remove } = on ?? {}; const tools = !!(move && remove); const lineOf = (key: string): LaborCalcLine | undefined => data.calc?.줄.find((l) => l.로직 === key); const trs = data.rows.map((row, i) => { const logic = data.logics.get(row.로직); const line = lineOf(row.로직); const unit = logic?.결과단위 ?? ""; const values = valueLine(logic?.입력 ?? [], row.입력값); const qty = qtyText(line, line?.분모 ?? ""); const note = [values.text, qty].filter(Boolean).join(" · "); const why = line?.까닭 ?? (data.fail && i === 0 ? data.fail : null); const small = (label: string, run: () => void): HTMLElement => createButton({ label, variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); // 줄 누름(입력 모달)과 따로 run(); }, }); const tr = el("tr", { className: on ? "m02-labrow__row is-open" : "m02-labrow__row", attrs: { "data-labor-logic": row.로직 }, children: [ el("td", { text: String(i + 1) }), el("td", { className: "m02-labrow__name", children: [ el("span", { className: "m02-labrow__key", text: row.로직 }), el("strong", { text: logic?.이름 ?? row.로직 }), ...(note ? [el("small", { className: "m02-labrow__note", text: note })] : []), ...(values.empty ? [ el("small", { className: "m02-labrow__empty", text: tc("Labor_Empty").replace("{n}", String(values.empty)), }), ] : []), ...(row.메모 ? [el("small", { className: "m02-labrow__note", text: row.메모 })] : []), ...(why ? [ el("small", { className: "m02-labrow__empty", text: `${tc("Labor_Calc_Fail")}: ${why}`, }), ] : []), ], }), el("td", { text: unit }), ...COSTS.map((c) => moneyCell(line?.기준?.[c], line?.반영?.[c], wait)), ...(move && remove ? [ el("td", { className: "m02-labrow__move", children: [ small(tc("Up"), () => move(i, -1)), small(tc("Down"), () => move(i, 1)), small(tc("Remove"), () => remove(i)), ], }), ] : []), ], }); if (on) tr.addEventListener("click", () => on.open(i)); return tr; }); const sum = (part: Partial | undefined, c: (typeof COSTS)[number]): number | null => part?.[c] ?? null; const foot = data.rows.length ? [ el("tr", { className: "m02-labrow__sum", children: [ el("td", { text: "" }), el("td", { text: tc("Prev_Sum") }), el("td", { text: "" }), ...COSTS.map((c) => moneyCell(sum(data.calc?.기준합, c), sum(data.calc?.반영합, c), wait), ), ...(tools ? [el("td", { text: "" })] : []), ], }), ] : []; const heads = [ tc("Col_Order"), tc("Col_Name"), tc("Col_Unit"), ...COSTS.map((c) => tc(HEAD[c])), ...(tools ? [tc("Col_Move")] : []), ]; return data.rows.length ? table(heads, trs, foot) : el("p", { className: "m02-labrow__muted", text: tc("NoLogic") }); } export interface MatTableData { mats: LaborMat[]; calc: LaborCalc | null; fail?: string | null; } /** 자재목록 표 — 보기 갈래는 `on` 없이 */ export function matTable(data: MatTableData, on?: Omit): HTMLElement { const wait = !data.calc && !data.fail; const remove = on?.remove; const show = (v: number | null | undefined): string => wait ? "…" : v === null || v === undefined ? "—" : formatMoney(v); const trs = data.mats.map((mat, i) => { const line: LaborCalcMat | undefined = data.calc?.자재.find((m) => m.코드 === mat.코드); const qty = line?.수량 === null || line?.수량 === undefined ? "" : num(line.수량); const tr = el("tr", { className: on ? "m02-labrow__row is-open" : "m02-labrow__row", attrs: { "data-labor-mat": mat.코드 }, children: [ el("td", { text: mat.코드 }), el("td", { className: "m02-labrow__name", children: [ el("strong", { text: mat.이름 ?? "" }), ...(line?.까닭 ? [el("small", { className: "m02-labrow__empty", text: line.까닭 })] : []), ], }), el("td", { text: mat.규격 ?? "" }), el("td", { text: mat.단위 ?? "" }), el("td", { className: "m02-labrow__num", text: qty, attrs: { title: line?.수량출처 ?? "" }, }), el("td", { className: "m02-labrow__num", text: show(line?.기준단가) }), el("td", { className: "m02-labrow__num", text: show(line?.반영) }), ...(remove ? [ el("td", { className: "m02-labrow__move", children: [ createButton({ label: tc("Remove"), variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); remove(i); }, }), ], }), ] : []), ], }); if (on) tr.addEventListener("click", () => on.open(i)); return tr; }); const heads = [ tc("Labor_Code"), tc("Col_Name"), tc("Labor_Spec"), tc("Col_Unit"), tc("Labor_Qty"), tc("Labor_BasePrice"), tc("Labor_AppliedSum"), ...(remove ? [tc("Col_Move")] : []), ]; return data.mats.length ? table(heads, trs, []) : el("p", { className: "m02-labrow__muted", text: tc("Labor_NoMat") }); }