Files
Aislo/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts
T

291 lines
9.6 KiB
TypeScript

/* =============================================================================
* M01_MasterData_UI_LogicLab_Detail.ts
* 「로직 개선 시험」 상세 화면 — 컨테이너 넷을 위에서 아래로
* ① 기본정보(정본은 읽기 전용 · 비고만 고침) ② 일위대가 호표(인력·자재·경비·그 밖 묶음 + 소계)
* ③ 텍스트 수식(자리 — `_LogicLab_Formula.ts`) ④ 시험 계산(입력값 + 결과값 — `Logic_Calc` 그대로)
* 호표 줄을 누르면 그 줄이 쓰는 자료 모달(`_LogicLab_Modal.ts`)
* ========================================================================== */
import { el } from "@ui/ui_template_elements";
import type {
CalcLine,
ElementBrief,
HoLine,
LogicRow,
NamedFormula,
} from "./M01_MasterData_UI_Logic_Api";
import { formatNumber, qtyKind } from "./M01_MasterData_UI_Logic_Edit";
import { tx } from "./M01_MasterData_UI_Logic_Text";
import { buildFormula } from "./M01_MasterData_UI_LogicLab_Formula";
import { openMaterialModal } from "./M01_MasterData_UI_LogicLab_Modal";
import { tl, type LabTextKey } from "./M01_MasterData_UI_LogicLab_Text";
import "./M01_MasterData_UI_LogicLab_Style.css";
export interface DetailContext {
row: LogicRow;
file: string;
reasons: string[];
prices: Record<string, ElementBrief | null>;
/** 마지막 시험 계산의 줄 — 호표 차례 뒤에 덧줄 */
lines: CalcLine[] | null;
values: Record<string, string>;
onChange: () => void;
/** 시험 계산 칸(`buildCalc` 가 채움) — 넷째 컨테이너 안에 놓음 */
calcHost: HTMLElement;
}
type Group = "labor" | "material" | "cost" | "other";
const GROUPS: Group[] = ["labor", "material", "cost", "other"];
const OF_KIND: Record<string, Group> = { 인력: "labor", 재료: "material", 기계: "cost" };
const OF_COST: Record<string, Group> = { 노무비: "labor", 재료비: "material", 경비: "cost" };
/** 줄 종류가 셋 중 어디에도 안 들어가면 「그 밖」 — 다른 로직을 부르는 줄은 비목으로 */
const groupOfHo = (item: HoLine): Group =>
OF_KIND[item.종류] ?? (item.종류 === "로직" ? OF_COST[item.비목 ?? ""] : undefined) ?? "other";
interface Entry {
group: Group;
name: string;
spec: string;
unit: string;
qty: string;
qtyTag: string;
price: string;
amount: number | null;
/** 덧줄이면 true */
extra: boolean;
open: () => void;
}
const groupName = (group: Group): string => tl(`G_${group}` as LabTextKey);
const section = (title: string, body: HTMLElement[], hint = ""): HTMLElement =>
el("section", {
className: "m01-logic__section m01lab__box",
children: [
el("div", {
className: "m01-logic__section-head",
children: [
el("h3", { text: title }),
...(hint ? [el("span", { className: "m01-logic__muted", text: hint })] : []),
],
}),
...body,
],
});
function infoBox(ctx: DetailContext): HTMLElement {
const row = ctx.row;
const cell = (label: string, value: string): HTMLElement =>
el("div", {
className: "m01lab__cell",
children: [
el("span", { className: "m01-logic__muted", text: label }),
el("span", { text: value || "—" }),
],
});
const note = el("textarea", { className: "m01-logic__input m01-logic__note" });
note.value = row.비고 ?? "";
note.addEventListener("input", () => {
if (note.value.trim()) row.비고 = note.value;
else delete row.비고;
ctx.onChange();
});
const reasons = ctx.reasons.length
? [
el("div", {
className: "m01-logic__reasons",
children: [
el("strong", { text: tx("Head_Blocked") }),
...ctx.reasons.map((r) => el("div", { text: r })),
],
}),
]
: [];
return section(tl("Info_Title"), [
el("p", { className: "m01-logic__muted", text: tl("Info_ReadOnly") }),
...reasons,
el("div", {
className: "m01lab__info",
children: [
cell(tx("Head_File"), ctx.file),
cell(tx("Head_Key"), row.),
cell(tx("Head_Number"), row.원문번호),
cell(tx("Head_Name"), row.이름),
cell(tx("Head_Unit"), row.결과단위),
cell(tx("Head_Source"), row.출처),
cell(tx("Head_Owner"), row.소유 ?? ""),
el("label", {
className: "m01lab__cell m01lab__cell--wide",
children: [el("span", { className: "m01-logic__muted", text: tx("Head_Note") }), note],
}),
],
}),
]);
}
function entries(ctx: DetailContext): Entry[] {
const middles: NamedFormula[] = ctx.row.중간 ?? [];
const ho = ctx.row.호표 ?? [];
const out: Entry[] = ho.map((item, i) => {
const line = ctx.lines?.[i];
const brief = ctx.prices[item.요소];
const price = line ? line.단가 : brief?.값;
return {
group: groupOfHo(item),
name: item.이름 ?? item.요소,
spec: item.규격 ?? brief?.규격 ?? "",
unit: item.단위 ?? "",
qty: item.수량,
qtyTag: qtyKind(item.수량),
price: price === undefined || price === null ? "" : formatNumber(price),
amount: line ? line.금액 : null,
extra: false,
open: () =>
openMaterialModal({
title: item.이름 ?? item.요소,
expr: item.수량,
element: item.요소,
prices: ctx.prices,
middles,
values: ctx.values,
}),
};
});
(ctx.row.덧줄 ?? []).forEach((extra, i) => {
const line = ctx.lines?.[ho.length + i];
out.push({
group: OF_COST[extra.비목 ?? ""] ?? "other",
name: extra.이름,
spec: "",
unit: "",
qty: extra.식,
qtyTag: tl("Extra"),
price: "",
amount: line ? line.금액 : null,
extra: true,
open: () =>
openMaterialModal({
title: extra.이름,
expr: extra.식,
prices: ctx.prices,
middles,
values: ctx.values,
}),
});
});
return out;
}
const cellOf = (text: string, className = ""): HTMLElement => el("td", { className, text });
function lineRow(e: Entry): HTMLElement {
const tr = el("tr", {
className: `m01lab__line${e.extra ? " m01lab__line--extra" : ""}`,
attrs: { tabindex: "0", "data-line": e.name },
children: [
el("td", {
children: [
el("div", { text: e.name }),
...(e.spec ? [el("div", { className: "m01-logic__muted", text: e.spec })] : []),
],
}),
cellOf(e.unit),
el("td", {
children: [
el("span", { className: "m01-logic__tag", text: e.qtyTag }),
el("span", { className: "m01lab__qty", text: ` ${e.qty}` }),
],
}),
cellOf(e.price, "m01-logic__money"),
cellOf(e.amount === null ? "" : formatNumber(e.amount), "m01-logic__money"),
],
});
tr.addEventListener("click", e.open);
tr.addEventListener("keydown", (ev) => ev.key === "Enter" && e.open());
return tr;
}
function groupTable(group: Group, mine: Entry[], calculated: boolean): HTMLElement[] {
const sub = mine.reduce((s, e) => s + (e.amount ?? 0), 0);
const empty = el("tr", {
children: [
el("td", {
attrs: { colspan: "5" },
className: "m01-logic__muted",
text: tl("Empty_Group"),
}),
],
});
const subtotal = el("tr", {
className: "m01lab__subtotal",
attrs: { "data-group": group, "data-subtotal": calculated ? String(sub) : "" },
children: [
el("td", { attrs: { colspan: "4" }, text: `${groupName(group)} ${tl("Subtotal")}` }),
cellOf(calculated ? formatNumber(sub) : "—", "m01-logic__money"),
],
});
const headers = [
tl("Col_Name"),
tl("Col_Unit"),
tl("Col_Qty"),
tl("Col_Price"),
tl("Col_Amount"),
];
return [
el("h4", { className: "m01lab__group", text: groupName(group) }),
el("div", {
className: "m01-logic__scroll",
children: [
el("table", {
className: "m01-logic__grid m01lab__ho",
children: [
el("thead", {
children: [el("tr", { children: headers.map((h) => el("th", { text: h })) })],
}),
el("tbody", { children: [...(mine.length ? mine.map(lineRow) : [empty]), subtotal] }),
],
}),
],
}),
];
}
function hoBox(ctx: DetailContext): HTMLElement {
const money = !("결과" in ctx.row) && (ctx.row.결과단위 ?? "").startsWith("원");
if (!money) {
return section(tl("Ho_Title"), [
el("p", { className: "m01-logic__muted", text: tl("Not_Money") }),
el("pre", { className: "m01lab__expr", text: ctx.row.결과?.식 ?? "" }),
]);
}
const all = entries(ctx);
const calculated = ctx.lines !== null;
const blocks = GROUPS.flatMap((group) => {
const mine = all.filter((e) => e.group === group);
return group === "other" && !mine.length ? [] : groupTable(group, mine, calculated); // 그 밖은 있을 때만
});
const total = all.reduce((s, e) => s + (e.amount ?? 0), 0);
blocks.push(
el("p", {
className: "m01lab__total",
attrs: { "data-sum": calculated ? String(total) : "" },
text: `${tl("Total")} ${calculated ? formatNumber(total) : "—"}`,
}),
);
const hint = calculated ? tl("Click_Hint") : `${tl("Click_Hint")} · ${tl("Calc_First")}`;
return section(tl("Ho_Title"), blocks, hint);
}
/** 상세 화면 넷을 `host` 에 쌓음 */
export function buildLabDetail(host: HTMLElement, ctx: DetailContext): void {
const formula = el("div", { className: "m01lab__formula" });
buildFormula(formula, { row: ctx.row, lines: ctx.lines });
host.replaceChildren(
infoBox(ctx),
hoBox(ctx),
section(tl("Formula_Title"), [formula]),
section(tl("Calc_Title"), [ctx.calcHost]),
);
}