/* ============================================================================= * M01_MasterData_UI_Combo.ts * 일위대가 조합 화면 — 왼쪽 컨테이너(구분 · 상세구분 거름) / 오른쪽 목록 · 상세 * 상세 = 이름·구분·단위·비고 + 담은 로직 표(차례 · 키 · 이름 · 결과 단위 · 메모 · 순서) + 미리 보기 * 저장·지우기는 서버 조합 마스터(`일위대가조합.json` · 키 UA) — 길은 `Combo_Api` 한 곳 * ========================================================================== */ import { createButton, createInputField, el, showConfirmDialog, showToast, } from "@ui/ui_template_elements"; import { fetchLogics, type LogicSummary } from "./M01_MasterData_UI_Logic_Api"; import { deleteCombo, fetchCombo, fetchCombos, saveCombo, type Combo, type ComboSummary, } from "./M01_MasterData_UI_Combo_Api"; import { openLogicPicker } from "./M01_MasterData_UI_Combo_Pick"; import { buildPreview } from "./M01_MasterData_UI_Combo_Preview"; import { tc } from "./M01_MasterData_UI_Combo_Text"; import type { SideHandle } from "./M01_MasterData_UI_Side"; import "./M01_MasterData_UI_Logic_Style.css"; import "./M01_MasterData_UI_LogicLab_Style.css"; const FILTER_ID = "조합|"; const blankCombo = (): Combo => ({ 키: "", 이름: "", 구분: "", 상세구분: "", 단위: "", 비고: "", 로직: [], }); export interface ComboHandle { /** 컨테이너를 펼칠 때 — 목록을 새로 받아 그림 */ show: () => Promise; } export function mountM01Combo( host: HTMLElement, side: SideHandle, onOpenLogic: (key: string) => void, ): ComboHandle { let items: ComboSummary[] = []; let logics = new Map(); let pick = { sub: "", detail: "" }; const filterHost = el("div"); side.comboHost.replaceChildren(filterHost); const view = el("div", { className: "m01-logic__editor" }); host.replaceChildren(view); const load = async (): Promise => { try { const [combos, all] = await Promise.all([fetchCombos(), fetchLogics()]); items = combos; logics = new Map(all.map((x) => [x.키, x])); } catch (error) { showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error"); } drawFilter(); drawList(); }; /* --- 왼쪽 거름 --- */ const drawFilter = (): void => { const subs = [...new Set(items.map((x) => x.구분).filter(Boolean))].map((name) => ({ name, book: null, details: [...new Set(items.filter((x) => x.구분 === name).map((x) => x.상세구분))].filter( Boolean, ), })); filterHost.replaceChildren( side.filter({ id: FILTER_ID, store: "m01.filter.조합", subs, total: items.length, subLabel: tc("Sub"), detailLabel: tc("Detail"), onPick: (sub, detail) => { pick = { sub, detail }; drawList(); }, }), ); }; /* --- 목록 --- */ const toolbar = (...more: HTMLElement[]): HTMLElement => el("div", { className: "m01-logic__section-head", children: [ createButton({ label: tc("New"), variant: "filled", onClick: () => draw(blankCombo()) }), ...more, ], }); const drawList = (): void => { const shown = items.filter( (x) => (!pick.sub || x.구분 === pick.sub) && (!pick.detail || x.상세구분 === pick.detail), ); const heads = [ tc("Head_Key"), tc("Head_Name"), tc("Sub"), tc("Detail"), tc("Head_Unit"), tc("Head_Count"), ]; const rows = shown.map((x) => { const tr = el("tr", { className: "m01-master__table-row", attrs: { "data-combo-key": x.키 }, children: [x.키, x.이름, x.구분, x.상세구분, x.단위, String(x.로직수)].map((v) => el("td", { text: v }), ), }); tr.addEventListener("click", () => void open(x.키)); return tr; }); view.replaceChildren( toolbar(), shown.length ? el("div", { className: "m01-master__grid-wrap", children: [ el("table", { className: "m01-master__grid", children: [ el("thead", { children: [el("tr", { children: heads.map((t) => el("th", { text: t })) })], }), el("tbody", { children: rows }), ], }), ], }) : el("p", { className: "m01-logic__muted", text: tc("Empty") }), ); }; const open = async (key: string): Promise => { try { draw(await fetchCombo(key)); } catch (error) { showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error"); } }; /* --- 상세 --- */ const draw = (combo: Combo): void => { const field = (label: string, name: "이름" | "구분" | "상세구분" | "단위" | "비고") => { const f = createInputField({ type: "text", label }); f.input.value = combo[name]; f.input.setAttribute("data-combo-field", name); f.input.addEventListener("input", () => (combo[name] = f.input.value)); return f.root; }; const preview = buildPreview(() => combo.로직); const table = el("div"); const drawTable = (): void => { const move = (i: number, d: number): void => { const j = i + d; if (j < 0 || j >= combo.로직.length) return; [combo.로직[i], combo.로직[j]] = [combo.로직[j], combo.로직[i]]; drawTable(); }; const trs = combo.로직.map((row, i) => { const info = logics.get(row.키); const memo = createInputField({ type: "text" }); memo.input.value = row.메모; memo.input.addEventListener("input", () => (row.메모 = memo.input.value)); const name = el("button", { className: "ui-btn ui-btn--ghost", text: info?.이름 ?? row.키, attrs: { type: "button", title: tc("Open") }, }); name.addEventListener("click", () => onOpenLogic(row.키)); const btns = el("div", { className: "m01-logic__chips", children: [ createButton({ label: tc("Up"), variant: "ghost", onClick: () => move(i, -1) }), createButton({ label: tc("Down"), variant: "ghost", onClick: () => move(i, 1) }), createButton({ label: tc("Remove"), variant: "ghost", onClick: () => { combo.로직.splice(i, 1); drawTable(); }, }), ], }); return el("tr", { attrs: { "data-combo-logic": row.키 }, children: [ el("td", { text: String(i + 1) }), el("td", { text: row.키 }), el("td", { children: [name] }), el("td", { text: info?.결과단위 ?? "" }), el("td", { children: [memo.root] }), el("td", { children: [btns] }), ], }); }); const heads = [ tc("Col_Order"), tc("Col_Key"), tc("Col_Name"), tc("Col_Unit"), tc("Col_Memo"), tc("Col_Move"), ]; table.replaceChildren( el("table", { className: "m01-master__grid", children: [ el("thead", { children: [el("tr", { children: heads.map((t) => el("th", { text: t })) })], }), el("tbody", { children: trs }), ], }), ...(trs.length ? [] : [el("p", { className: "m01-logic__muted", text: tc("NoLogic") })]), ); void preview.redraw(); }; const add = createButton({ label: tc("AddLogic"), variant: "filled", onClick: () => openLogicPicker((x) => { if (!logics.has(x.키)) logics.set(x.키, x); combo.로직.push({ 키: x.키, 메모: "", 입력: {} }); drawTable(); }), }); const save = createButton({ label: tc("Save"), variant: "filled", onClick: async () => { if (!combo.이름.trim()) return showToast(tc("NeedName"), "error"); try { const saved = await saveCombo(combo); showToast(tc("Saved"), "success"); await load(); draw(saved); } catch (error) { showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error"); } }, }); const remove = createButton({ label: tc("Delete"), variant: "ghost", onClick: async () => { if (!combo.키 || !(await showConfirmDialog(tc("DeleteAsk")))) return; try { await deleteCombo(combo.키); showToast(tc("Deleted"), "success"); await load(); } catch (error) { showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error"); } }, }); const back = createButton({ label: "←", variant: "ghost", onClick: () => drawList() }); view.replaceChildren( el("div", { className: "m01-logic__section-head", children: [back, el("h3", { text: combo.키 || tc("New") }), remove, save], }), el("section", { className: "m01lab__box m01-logic__section", children: [ el("div", { className: "m01lab__info", children: [ field(tc("Head_Name"), "이름"), field(tc("Sub"), "구분"), field(tc("Detail"), "상세구분"), field(tc("Head_Unit"), "단위"), field(tc("Head_Note"), "비고"), ], }), ], }), el("section", { className: "m01lab__box m01-logic__section", children: [ el("div", { className: "m01-logic__section-head", children: [el("h3", { text: tc("Logics_Title") }), add], }), table, ], }), preview.root, ); drawTable(); }; return { show: load }; }