/* ============================================================================= * M02_MasterTemplete_Labor_Spec.ts * 일위대가 규격 표(PLAN 704-4) — 704-3 격자에 규격 열(H=1.5 | 2.0 …)을 폄 · 고칠 때 격자 위 규격 줄. * 문서 `규격` = {이름, 열, 기본, 수량: {줄 id: {열: 수}}}(짝 `M02_Labor_Spec.py`) · 화면 캐시만(저장은 [저장]) * 규격 줄 = 규격 이름 · 열마다 이름 칸 · [기본] · [×] · [+ 열](새 열 수량 = 지금 기본 열 값 그대로) * 격자 수량 칸(열마다) 고침 = 그 줄 · 그 열 수량(빈 글 = 공통 수량을 따름) * 열마다 수량 · 1 단위 합계 = 짝 셈 `settle(…, 열)`(서버 안 부름) · 금액 칸 = 기본 열 * 쓰는 사본이 있는 열 지움 · 이름 바꿈은 [저장] 때 서버가 막음(409 까닭 그대로) * ========================================================================== */ import { createButton, createInputField, el } from "@ui/ui_template_elements"; import type { DataTableOptions } from "@ui/ui_template_data_table"; import { hopyoGrid, hopyoSpecKey, type HopyoBlock } from "@ui/ui_template_hopyo_grid"; import { createToolbar } from "@ui/ui_template_toolbar"; import { createText } from "@ui/ui_template_text"; import type { LaborCalc, LaborDoc } from "./M02_MasterTemplete_Labor_Api"; import { givenOf, lineId, settle, specColumn } from "./M02_MasterTemplete_Labor_Calc"; /** 고칠 때 — 규격이 바뀌면 `true`(수량만 · 기준 그대로 · 짝 셈으로 바로) */ export type SpecEdit = (qtyOnly: boolean) => void; const TEXT = { none: "규격 열 없음 — [+ 열] = H 같은 규격마다 줄 수량만 다른 열", add: "+ 열", name: "규격 이름", base: "기본", remove: "열 지움", first: "1", next: "새 열", }; /** 줄 id 차례 — 격자 줄 차례(로직 줄 → 자재 줄)와 같음 */ const lineIds = (doc: LaborDoc): string[] => [ ...doc.담은로직.map((r) => lineId(r) ?? ""), ...doc.자재.map((m) => m.코드), ]; /** 열마다 셈 → 블록 `specs` · 줄 `qtys`(규격 없음 = 그대로) */ export function withSpecs(block: HopyoBlock, doc: LaborDoc, calc: LaborCalc | null): HopyoBlock { const spec = doc.규격; if (!spec?.열.length) return block; const per = calc ? spec.열.map((c) => settle(calc, doc, givenOf(calc), c)) : []; const ids = lineIds(doc); const logics = doc.담은로직.length; const qtyOf = (p: LaborCalc, n: number): number | null => (n < logics ? p.줄.find((l) => lineId(l) === ids[n])?.수량 : p.자재.find((m) => m.코드 === ids[n])?.수량) ?? null; return { ...block, specs: { name: spec.이름, cols: spec.열, pick: specColumn(doc), totals: per.map((p) => p.반영합?.계 ?? null), }, lines: block.lines.map((ln, n) => ({ ...ln, qtys: per.map((p) => qtyOf(p, n)) })), }; } /** 격자 수량 칸 고침(열마다) — 공용 표 옵션 덧 */ export function specCells( block: HopyoBlock, doc: LaborDoc, edit: SpecEdit, ): Partial { const spec = doc.규격; if (!spec?.열.length) return {}; const g = hopyoGrid([block]); const ids = lineIds(doc); const col = (key: string): string | undefined => spec.열.find((_, n) => hopyoSpecKey(n) === key); return { editable: (i, key) => g.at.has(i) && col(key) !== undefined, onEdit: (i, key, text) => { const id = ids[g.at.get(i)?.[1] ?? -1]; const name = col(key); if (id === undefined || name === undefined) return; const v = Number(text.replace(/,/g, "").trim()); const row = { ...(spec.수량[id] ?? {}) }; if (text.trim() === "" || !Number.isFinite(v)) delete row[name]; else row[name] = v; if (Object.keys(row).length) spec.수량[id] = row; else delete spec.수량[id]; edit(true); }, }; } /** 새 열 이름 — 겹치면 뒤에 번호 */ const fresh = (cols: string[], want: string): string => { let out = want; for (let n = 2; cols.includes(out); n++) out = `${want} ${n}`; return out; }; /** [+ 열] — 규격이 없으면 지금 수량 열(1) + 새 열 · 새 열 수량 = 기본 열 손 값 그대로 */ function addColumn(doc: LaborDoc, calc: LaborCalc | null): void { const spec = (doc.규격 ??= { 이름: "H", 열: [TEXT.first], 기본: TEXT.first, 수량: {} }); const col = fresh(spec.열, TEXT.next); const base = specColumn(doc) ?? spec.열[0]; spec.열.push(col); const ids = lineIds(doc); const logics = doc.담은로직.length; ids.forEach((id, n) => { const own = spec.수량[id]?.[base]; const line = n < logics ? calc?.줄.find((l) => lineId(l) === id) : calc?.자재.find((m) => m.코드 === id); const v = own ?? (line?.수량출처 === "손" ? line.수량 : null); if (v != null) spec.수량[id] = { ...(spec.수량[id] ?? {}), [col]: v }; }); } function renameColumn(doc: LaborDoc, from: string, to: string): boolean { const spec = doc.규격; const name = to.trim(); if (!spec || !name || name.length > 20 || name === from || spec.열.includes(name)) return false; spec.열 = spec.열.map((c) => (c === from ? name : c)); if (spec.기본 === from) spec.기본 = name; for (const row of Object.values(spec.수량)) if (from in row) { row[name] = row[from]; delete row[from]; } return true; } function removeColumn(doc: LaborDoc, col: string): void { const spec = doc.규격; if (!spec) return; spec.열 = spec.열.filter((c) => c !== col); for (const [id, row] of Object.entries(spec.수량)) { delete row[col]; if (!Object.keys(row).length) delete spec.수량[id]; } if (!spec.열.length) doc.규격 = null; // 마지막 열 = 규격 없음(옛 문서 꼴) else if (spec.기본 === col) spec.기본 = spec.열[0]; } /** 격자 위 규격 줄(고칠 때만) */ export function specBar(doc: LaborDoc, calc: LaborCalc | null, edit: SpecEdit): HTMLElement { const add = createButton({ label: TEXT.add, variant: "ghost", onClick: () => { addColumn(doc, calc); edit(true); }, }); const spec = doc.규격; if (!spec?.열.length) return createToolbar({ items: [createText(TEXT.none, "muted"), add], label: TEXT.name }); const name = createInputField({ value: spec.이름, compact: true, placeholder: TEXT.name }); name.input.title = TEXT.name; name.root.style.width = "6em"; // 짧은 글 칸 — 열 이름 칸도 같은 폭(카드 한 줄에) name.input.addEventListener("change", () => { const v = name.input.value.trim(); if (v && v.length <= 20) spec.이름 = v; edit(true); }); const cols = spec.열.flatMap((col) => { const box = createInputField({ value: col, compact: true }); box.input.setAttribute("data-spec-col", col); box.root.style.width = "6em"; box.input.addEventListener("change", () => { renameColumn(doc, col, box.input.value); edit(true); }); const base = createButton({ label: TEXT.base, variant: col === spec.기본 ? "filled" : "ghost", size: "sm", onClick: () => { spec.기본 = col; edit(true); }, }); const drop = createButton({ label: "×", variant: "ghost", size: "sm", onClick: () => { removeColumn(doc, col); edit(true); }, }); drop.title = TEXT.remove; return [el("span", { children: [box.root] }), base, drop, "divider" as const]; }); return createToolbar({ items: [name.root, "divider", ...cols, add], label: TEXT.name }); }