Files
Aislo/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts
T

214 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* M01_MasterData_UI_LogicLab_Modal.ts
* 호표 줄 자료 모달 — 그 줄이 쓰는 자료를 보임
* 표(소요량·계수)는 표 그대로 + 지금 넣은 값으로 걸린 줄 강조 · 단일 값(단가)은 값과 출처
* 표 읽기 = `/elements` → `/table`(`Logic_Note.loadTable`) · 단가 = 로직 화면이 이미 받은 `prices`
* ========================================================================== */
import { createButton, el } from "@ui/ui_template_elements";
import type { ElementBrief, NamedFormula } from "./M01_MasterData_UI_Logic_Api";
import { formatNumber } from "./M01_MasterData_UI_Logic_Edit";
import {
explain,
loadTable,
matchRow,
relatedFinds,
type RelatedFind,
type TableRow,
} from "./M01_MasterData_UI_Logic_Note";
import { jumpToMaster, type MasterJump } from "./M01_MasterData_UI_Side";
import { pillBar, type Pill } from "./M01_MasterData_UI_LogicLab_Pill";
import { tl } from "./M01_MasterData_UI_LogicLab_Text";
import "./M01_MasterData_UI_LogicLab_Pill.css";
export interface ModalTarget {
title: string;
/** 수량 식(호표 줄) · 덧줄은 그 식 */
expr: string;
/** 호표 줄의 단가 요소 키 — 덧줄은 없음 */
element?: string;
prices: Record<string, ElementBrief | null>;
middles: NamedFormula[];
/** 시험 계산 칸에 넣은 값 — 걸린 줄을 맞히는 데 씀 */
values: Record<string, string>;
}
function tableView(
find: RelatedFind,
table: TableRow,
values: Record<string, string>,
): HTMLElement {
// 표 줄의 빈 칸은 「무엇이든」 — 서버 찾기()와 같게 그 칸은 조건에서 뺌
const hit =
(table. ?? []).find((line) =>
matchRow(
{ ...table, : [line] },
find.find.conds.filter(
([col]) => line[col] !== undefined && line[col] !== null && line[col] !== "",
),
{ values, middle: {} },
),
) ?? null;
const cols = [...Object.keys(table.조건 ?? {}), ...Object.keys(table.값칸 ?? {})];
const rows = (table. ?? []).map((line) =>
el("tr", {
className: hit === line ? "m01lab__hit" : "",
attrs: hit === line ? { "data-hit": "1" } : {},
children: cols.map((c) => el("td", { text: formatNumber(line[c]) })),
}),
);
const use = table.용도
? [table.용도.공종, (table.용도.대상 ?? []).join("·")].filter(Boolean).join(" · ")
: "";
return el("div", {
className: "m01lab__data",
attrs: { "data-table": table. },
children: [
el("strong", { text: `${table.원문번호 ?? ""} ${table.이름 ?? table.} · ${table.}` }),
el("p", {
className: "m01-logic__muted",
text: [`${tl("Modal_Source")} ${table.출처 ?? ""}`, use].filter(Boolean).join(" · "),
}),
el("p", {
className: "m01-logic__muted",
text: `${find.source}${find.find.col} · ${hit ? tl("Modal_Hit") : tl("Modal_NoHit")}`,
}),
goButton({ kind: "table", key: table.키 }),
el("div", {
className: "m01-logic__scroll",
children: [
el("table", {
className: "m01-logic__grid m01lab__table",
children: [
el("thead", {
children: [el("tr", { children: cols.map((c) => el("th", { text: c })) })],
}),
el("tbody", { children: rows }),
],
}),
],
}),
],
});
}
const groupOf = (ref: string): string =>
ref.startsWith("LB") ? "인력" : ref.startsWith("M") ? "재료" : ref.startsWith("EQ") ? "기계" : "";
function goButton(target: MasterJump): HTMLElement {
return createButton({
label: tl("Pill_Go"),
variant: "ghost",
onClick: () => jumpToMaster(target),
});
}
function priceView(ref: string, brief: ElementBrief): HTMLElement {
const cols = Object.entries(brief.값칸 ?? {});
return el("div", {
className: "m01lab__data",
attrs: { "data-price": ref },
children: [
el("strong", { text: `${brief.이름 ?? ref} · ${ref}` }),
el("dl", {
className: "m01-logic__sums",
children: [
...(brief.규격
? [el("dt", { text: tl("Modal_Spec") }), el("dd", { text: brief.규격 })]
: []),
el("dt", { text: tl("Modal_Value") }),
el("dd", { className: "m01-logic__money", text: formatNumber(brief.) }),
...(brief.file
? [el("dt", { text: tl("Modal_Source") }), el("dd", { text: brief.file })]
: []),
...cols.flatMap(([k, v]) => [el("dt", { text: k }), el("dd", { text: String(v) })]),
],
}),
goButton({ kind: "element", group: groupOf(ref), ref: brief.ref ?? ref, file: brief.file }),
],
});
}
/** 모달을 엶 */
export function openMaterialModal(target: ModalTarget): void {
const body = el("div", { className: "m01lab__modal-body" });
const close = (): void => backdrop.remove();
const dialog = el("div", {
className: "m01-logic__pick m01lab__modal",
attrs: { role: "dialog", "aria-label": target.title },
children: [
el("div", {
className: "m01lab__modal-top",
children: [
el("h3", { text: target.title }),
createButton({ label: tl("Modal_Close"), variant: "ghost", onClick: close }),
],
}),
body,
],
});
const backdrop = el("div", { className: "m01-logic__backdrop", children: [dialog] });
backdrop.addEventListener("click", (ev) => ev.target === backdrop && close());
backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && close());
const finds = relatedFinds(target.expr, target.middles);
const base = target.element?.split(".")[0] ?? "";
const price = target.element ? (target.prices[target.element] ?? target.prices[base]) : undefined;
const logicRef = target.element?.startsWith("로직(") ? target.element : "";
const pills: Pill[] = [];
if (price && target.element) {
pills.push({
label: `${groupOf(base)} ${price.이름 ?? base}`.trim(),
view: () => priceView(target.element as string, price),
});
} else if (target.element && !logicRef) {
pills.push({
label: `${groupOf(base)} ${target.title}`.trim(),
view: () =>
el("p", {
className: "m01-logic__muted",
text: `${target.element}${tl(target.element?.includes("{") ? "Pill_NoPick" : "Pill_NoValue")}`,
}),
});
}
if (logicRef) {
pills.push({
label: `${tl("Pill_Logic")} ${logicRef.slice(3).split(",")[0].replace(/\)$/, "")}`,
view: () => el("p", { text: `${tl("Modal_Logic")} · ${logicRef}` }),
});
}
for (const f of finds) {
pills.push({
label: `${tl("Pill_Table")} ${f.find.table}`,
view: async () => {
const table = await loadTable(f.find.table);
return table
? tableView(f, table, target.values)
: el("p", { className: "m01-logic__bad", text: `${f.find.table} ?` });
},
});
}
const unit = price ? `${tl("Modal_UnitPrice")} ${formatNumber(price.)} × ` : "";
const parts: HTMLElement[] = [
el("p", { className: "m01-logic__muted", text: tl("Modal_Sum") }),
el("p", {
className: "m01lab__sum",
text: `${target.title} = ${unit}${explain(target.expr)}`,
}),
...(pills.length
? [el("p", { className: "m01-logic__muted", text: tl("Pill_Hint") }), pillBar(pills)]
: [el("p", { className: "m01-logic__muted", text: tl("Modal_NoData") })]),
el("details", {
className: "m01lab__raw",
children: [
el("summary", { text: tl("Modal_Qty") }),
el("pre", { className: "m01lab__expr", text: target.expr }),
],
}),
];
body.replaceChildren(...parts);
document.body.append(backdrop);
dialog.tabIndex = -1;
dialog.focus();
}