Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4uKaV4ZBhsJN4GHG7yRj4
293 lines
10 KiB
TypeScript
293 lines
10 KiB
TypeScript
/* =============================================================================
|
|
* 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<typeof tc>[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 ㎡」(칸 = 그 알약을 이은 면적산출 칸) ·
|
|
* 이은 칸이 비었으면 「수량 A1 = 빈 칸」(수량 없음 · 서버와 같음) · 아니면 빈 글 */
|
|
export function qtyText(
|
|
line: { 수량?: number | null; 수량출처?: string; 알약?: string | null } | undefined,
|
|
unit: string,
|
|
cells: ReadonlyMap<string, string> = new Map(),
|
|
): string {
|
|
if (!line) return "";
|
|
if (line.수량 === null || line.수량 === undefined) {
|
|
const at = line.알약 ? cells.get(line.알약) : undefined;
|
|
return at ? `${tc("Labor_Qty")} ${at} = ${tc("In_QtyEmpty")}` : "";
|
|
}
|
|
const v = `${num(line.수량)}${unit ? ` ${unit}` : ""}`;
|
|
if (line.수량출처 === "손") return `${tc("Labor_Qty")} ${tc("In_QtyHand")} ${v}`;
|
|
const cell = (line.알약 && cells.get(line.알약)) || tc("In_QtyCell");
|
|
return `${tc("Labor_Qty")} ${cell} = ${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<string, ItemLogic>;
|
|
/** calc 답 — null = 계산 중 · 못 셈은 `fail` */
|
|
calc: LaborCalc | null;
|
|
fail?: string | null;
|
|
/** 알약 키 → 이은 면적산출 칸(「B6」) */
|
|
cells?: ReadonlyMap<string, string>;
|
|
}
|
|
|
|
/** 담은 로직 표 — 보기 갈래는 `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?.분모 ?? "", data.cells);
|
|
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<LaborCost> | 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;
|
|
cells?: ReadonlyMap<string, string>;
|
|
}
|
|
|
|
/** 자재목록 표 — 보기 갈래는 `on` 없이 */
|
|
export function matTable(data: MatTableData, on?: Omit<RowHandlers, "move">): 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: qtyText(line, mat.단위 ?? "", data.cells) },
|
|
}),
|
|
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("Labor_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") });
|
|
}
|