/* ============================================================================= * M02_MasterTemplete_Library_List.ts * 일위대가 조합 서고 목록표(51-4) — 좌측 항목(단가산출 승격 · 일위대가 사전조합)을 누르면 상세 칸에 뜸. * 표 = 고름 · 키 · 구분 · 상세구분(51-12) · 이름 · 단위 · 기준 금액(서버 1 단위 계) · 고친 날 · 줄 누름 = 그 문서 상세. * 머리 [생성] · [복사](고른 줄) · [삭제](고른 줄 · 쓰는 곳 있으면 서버 409 까닭 그대로). * 꼴 = M01 단가산출 로직 목록 — 찾기 줄(`buildHeadFilters`) · 쪽(`buildPager`) · 표(`m01-master__grid`) 를 M01 에서 import. * ========================================================================== */ import { createButton, createInputField, el, showConfirmDialog, showToast, } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { openModal } from "@ui/ui_template_modal"; import { buildPager } from "../M01_MasterData/M01_MasterData_UI_Cells"; import { buildHeadFilters } from "../M01_MasterData/M01_MasterData_UI_Head"; import "../M01_MasterData/M01_MasterData_UI_Style.css"; import { deleteLayerTemplate, deleteTemplate, getActiveOwner, listTemplates, newLibrary, readTemplate, saveTemplate, type Layer, type LibraryBody, type TemplateInfo, } from "./M02_MasterTemplete_Api_Fetch"; import { fetchLibLogicSubs } from "./M02_MasterTemplete_Labor_Api"; import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick"; import { tc } from "./M02_MasterTemplete_Labor_Text"; import type { PrecomboKind } from "./M02_MasterTemplete_UI_Side_Precombo"; /** 목록 줄 — 서버가 문서 `이름`(제목) · 단위 · 1 단위 기준 계를 실어 보냄 */ type Row = TemplateInfo & { 제목?: string | null; 단위?: string | null; 기준금액?: number | null; 구분?: string | null; 상세구분?: string | null; }; export interface LibraryListOptions { layer: Layer; kind: PrecomboKind; canEdit: boolean; /** 줄 누름 · 생성 · 복사 뒤 — 그 문서 상세 */ open: (key: string, label: string) => void; } const SIZE = 30; const why = (error: unknown): string => (error instanceof Error ? error.message : ""); const money = (v: number | null | undefined): string => v == null ? "" : v.toLocaleString("ko-KR", { maximumFractionDigits: 2 }); export const libraryLabel = (row: Row): string => (row.제목 ? `${row.이름} ${row.제목}` : row.이름); /** `tools` = 상세 머리 단추 자리(페이지 slot) · 돌려준 `destroy` 는 안 씀(칸째 비움) */ export function mountLibraryList( host: HTMLElement, tools: HTMLElement, opt: LibraryListOptions, ): { getDoc: () => null; destroy: () => void } { let rows: Row[] = []; let picked: string | null = null; let page = 1; const searchField = createInputField({ type: "search", placeholder: tc("Search") }); const body = el("tbody"); let pager = buildPager(0, SIZE, 1, () => {}); const btnNew = createButton({ label: tc("Lib_Create"), variant: "filled" }); const btnCopy = createButton({ label: tc("Lib_Copy"), variant: "ghost" }); const btnDel = createButton({ label: tc("Lib_Delete"), variant: "danger" }); const paintButtons = (): void => { btnCopy.disabled = btnDel.disabled = !rows.some((r) => r.이름 === picked); }; const draw = (): void => { const q = searchField.input.value.trim().toLowerCase(); const shown = rows.filter((r) => !q || libraryLabel(r).toLowerCase().includes(q)); page = Math.min(page, Math.max(1, Math.ceil(shown.length / SIZE))); const next = buildPager(shown.length, SIZE, page, (p) => { page = p; draw(); }); pager.replaceWith(next); pager = next; body.replaceChildren( ...(shown.length ? shown.slice((page - 1) * SIZE, page * SIZE).map((row) => { const radio = el("input", { attrs: { type: "radio", name: `m02-lib-${opt.kind}`, "aria-label": row.이름 }, }) as HTMLInputElement; radio.checked = row.이름 === picked; const pick = el("td", { className: "m02-lib__pick", children: [radio] }); pick.addEventListener("click", (event) => { event.stopPropagation(); // 고름 칸은 열지 않음 picked = row.이름; radio.checked = true; paintButtons(); }); const tr = el("tr", { className: "m01-master__table-row", attrs: { "data-lib-key": row.이름 }, children: [ pick, ...[ row.이름, row.구분 ?? "", row.상세구분 ?? "", row.제목 ?? "", row.단위 ?? "", money(row.기준금액), row.수정일, ].map((v) => el("td", { text: v, attrs: { title: v } })), ], }); tr.addEventListener("click", () => opt.open(row.이름, libraryLabel(row))); return tr; }) : [ el("tr", { children: [el("td", { text: L("M02_NoTemplates"), attrs: { colspan: "8" } })], }), ]), ); paintButtons(); }; const load = async (): Promise => { try { rows = (await listTemplates(opt.layer, null)).filter((r) => r.종류 === opt.kind) as Row[]; } catch (error) { rows = []; showToast(L("M02_LoadFailed").replace("{value}", why(error)), "error"); } draw(); }; const nameDialog = (title: string, start: string, onOk: (name: string) => Promise) => { const name = createInputField({ label: L("M02_Name"), placeholder: L("M02_Name") }); name.input.value = start; openModal({ title, closeLabel: L("M02_Close"), dialogClass: "m02-modal", mount: (box, close) => { const ok = createButton({ label: L("M02_Create"), variant: "filled" }); ok.addEventListener("click", async () => { const text = name.input.value.trim(); if (!text) return showToast(L("M02_NameNeeded"), "error"); ok.disabled = true; // 연달아 눌러도 한 번만 try { await onOk(text); close(); } catch (error) { showToast(L("M02_ActionFailed").replace("{value}", why(error)), "error"); } finally { ok.disabled = false; } }); name.input.addEventListener("keydown", (event) => { if (event.key === "Enter" && !event.isComposing) ok.click(); }); box.append(name.root, ok); }, }); }; /** 새로 — 서버가 그 층 다음 키로 씀 → 그 문서 상세 */ const made = (key: string, label: string): void => { showToast(L("M02_Created"), "success"); opt.open(key, label); }; const create = async (body: LibraryBody, label: string): Promise => made((await newLibrary(opt.layer, opt.kind, body)).이름, label); /** 단가승격 = 단가산출 로직 고르기 → 이름 · 단위 · 구분 · 연결은 로직 것(서버가 채움) · * 사전조합 = 이름 + 구분 · 상세구분 처음 값 = 로직 구분 목록 첫 줄(51-12 · 상세에서 바꿈) · 빈 묶음 */ btnNew.addEventListener("click", () => { if (opt.kind === "promote") return openLogicPicker((x) => { const doc = { 이름: x.이름, 단위: x.결과단위 || null, 담은로직: [{ 로직: x.키, 입력값: {} }], }; void create({ 문서: doc }, x.이름).catch((error) => showToast(L("M02_ActionFailed").replace("{value}", why(error)), "error"), ); }); nameDialog(L("M02_NewTitle"), "", async (name) => { const first = (await fetchLibLogicSubs())[0]; const 문서 = { 이름: name, 구분: first?.name ?? null, 상세구분: first?.details[0] ?? null, 단위: null, 담은로직: [], }; await create({ 문서 }, name); }); }); /** 복사 = 같은 층 새 키(서버 · 본뜬 = 원래 키) → 고른 이름으로 고쳐 씀 */ btnCopy.addEventListener("click", () => { const from = rows.find((r) => r.이름 === picked); if (!from) return; nameDialog(L("M02_CopyTitle"), `${from.제목 ?? from.이름} (2)`, async (name) => { const got = await newLibrary(opt.layer, opt.kind, { 본뜬: { 층: opt.layer, 주인: getActiveOwner(), 키: from.이름 }, }); const doc = await readTemplate(opt.layer, opt.kind, got.이름, null); await saveTemplate(opt.layer, opt.kind, got.이름, null, got.판, { ...(doc.문서 as object), 이름: name, }); made(got.이름, name); }); }); btnDel.addEventListener("click", async () => { await load(); // 판을 최신으로 const info = rows.find((r) => r.이름 === picked); if (!info) return; const label = libraryLabel(info); if (!(await showConfirmDialog(L("M02_DeleteConfirm").replace("{value}", label)))) return; try { if (opt.layer === "system") await deleteTemplate(opt.kind, info.이름, info.판); else await deleteLayerTemplate(opt.layer, opt.kind, info.이름, info.판); showToast(L("M02_Deleted"), "success"); picked = null; await load(); } catch (error) { // 쓰는 곳이 있으면 서버가 막음 — 까닭(쓰는 곳 이름) 그대로 showToast(why(error) || L("M02_ActionFailed").replace("{value}", ""), "error"); } }); searchField.input.addEventListener("input", () => { page = 1; draw(); }); const heads = [ "", "Lib_Key", "Sub", "Detail", "Lib_Name", "Lib_Unit", "Lib_Base", "Lib_Date", ] as const; host.replaceChildren( buildHeadFilters([searchField.root, pager]), el("div", { className: "m01-master__grid-wrap", attrs: { "data-lib-list": opt.kind }, children: [ el("table", { className: "m01-master__grid", children: [ el("thead", { children: [ el("tr", { children: heads.map((h) => el("th", { text: h ? tc(h) : "" })) }), ], }), body, ], }), ], }), ); if (opt.canEdit) tools.replaceChildren( el("span", { className: "m02-main__tools", children: [btnNew, btnCopy, btnDel] }), ); void load(); return { getDoc: () => null, destroy: () => undefined }; }