- 공용 격자 — 첫 호표 specs 면 수량 칸을 열마다(머리 「H 열」 · 기본 표시) · 합계 줄 그 칸 = 열마다 1 단위 합계 - M02 일위대가 카드 · 상세 — 격자 위 규격 줄(이름 · 열 이름 · 기본 · 열 지움 · [+ 열]) · 격자 수량 칸 열마다 고침(짝 셈 바로) - 새 파일 M02_MasterTemplete_Labor_Spec.ts — 상세(700 줄)는 한 줄만 바꿈 - 고르기 창 — 규격 열 둘 이상이면 열 단추(열마다 합계) · [적용] = spec 붙은 참조 · 이름 뒤 「(H=열)」 · 열 하나 = 단추 없음 - 시험 704-3 격자 시험 두 줄 고침(laborGrid 넷째 인자 · Labor_Spec 이어 붙임) · 704-4 화면 자료 시험 둘 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
190 lines
7.3 KiB
TypeScript
190 lines
7.3 KiB
TypeScript
/* =============================================================================
|
||
* 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<DataTableOptions> {
|
||
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.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.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 });
|
||
}
|