/* ============================================================================= * M02_MasterTemplete_Labor_Pick.ts * [로직 더하기] 창 — 한 목록에 구분(단가산출 · 단가승격 · 일위조합) 칸 · 치는 대로 걸러짐(찾기 단추 없음) · * 단가승격 · 일위조합 줄은 층 단추(위 층만 · 짝 `M02_Labor_Ref.readable`) 층의 서고 · 고르면 참조 {층 · 주인 · 키} · * 프로젝트 · 카드엔 서고 줄 없음 * ========================================================================== */ import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements"; import { createFindList } from "@ui/ui_template_find_list"; import { t as L } from "@ui/ui_template_locale"; import type { LogicSummary, SubBrief } from "../M01_MasterData/M01_MasterData_UI_Logic_Api"; import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch"; import type { Layer } from "./M02_MasterTemplete_Api_Fetch"; import { fetchLibLogics as fetchLogics, fetchLibLogicSubs as fetchLogicSubs, fetchLibrary, type LibraryRow, } from "./M02_MasterTemplete_Labor_Api"; import type { LaborRef } from "./M02_MasterTemplete_Labor_Calc"; import { ownOwner } from "./M02_MasterTemplete_Layer"; import { tc } from "./M02_MasterTemplete_Labor_Text"; import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css"; // 창 겉틀 import "./M02_MasterTemplete_Labor.css"; const opts = (values: string[]): { value: string; text: string }[] => [L("M01_All"), ...values].map((v, i) => ({ value: i ? v : "", text: v })); type Scope = Omit; /** 서고 탭 — 일위대가가 있는 층 · 주인(null = 자기 층) · 고르면 참조 */ export interface LibraryPick { layer: Layer; owner: string | null; pick: (ref: LaborRef, name: string) => void; } /** 고를 수 있는 층 — 위 층만(마스터 → 마스터 · 회사 → 그 회사 · 마스터 · 개인 → 그 개인 · 그 회사 · 마스터) */ async function upper(layer: Layer, owner: string | null): Promise { if (layer === "project") return []; const own = layer === "system" ? null : (owner ?? ownOwner(layer, await fetchSessionUser().catch(() => null))); const out: Scope[] = [{ 층: "system", 주인: null }]; if (own) out.push({ 층: "company", 주인: own.split("/")[0] }); if (own && layer === "personal") out.push({ 층: "personal", 주인: own }); return out; } const LAYER_TEXT = { system: "Lib_System", company: "Lib_Company", personal: "Lib_Personal", } as const; export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: LibraryPick): void { const subs: SubBrief[] = []; let all: LogicSummary[] = []; const sub = createSelectField({ options: opts([]), compact: true, label: tc("Sub") }); const detail = createSelectField({ options: opts([]), compact: true, label: tc("Detail") }); detail.select.disabled = true; const close = (): void => backdrop.remove(); /* 한 목록 — 구분(단가산출 · 단가승격 · 일위조합) 칸 · 층 단추는 서고 줄(승격 · 조합)의 층 */ const layerBar = el("div", { className: "ui-btn-group", attrs: { "data-pick-layers": "" } }); let scopes: Scope[] = []; let scope: Scope | null = null; let libRows: { kind: "Kind_Promote" | "Kind_Combo"; row: LibraryRow }[] = []; let loading = false; let seq = 0; /* 공용 찾기 목록 — 구분 · 상세구분 2열 · 이름 왼쪽 맞춤 · ↑ ↓ Enter */ const finder = createFindList({ placeholder: tc("Pick_Search"), filters: [sub.root, detail.root], extra: [layerBar], }); const find = (): void => { const q = finder.search.value.trim().toLowerCase(); const found = all.filter( (x) => (!sub.select.value || x.구분 === sub.select.value) && (!detail.select.value || x.상세구분 === detail.select.value) && (!q || `${x.키} ${x.이름}`.toLowerCase().includes(q)), ); const libFound = sub.select.value ? [] : libRows.filter((x) => !q || `${x.row.이름} ${x.row.제목 ?? ""}`.toLowerCase().includes(q)); const total = all.length + libRows.length; const shown = found.length + libFound.length; finder.hint.textContent = loading ? tc("Loading") : shown ? `${shown} / ${total}` : tc("Pick_None"); finder.setRows([ ...found.slice(0, 100).map((x) => ({ key: x.키, tag: tc("Kind_Logic"), name: x.이름, sub: `${x.키} · ${x.구분} ${x.상세구분}`.trim(), unit: `${x.결과단위}${x.blocked ? ` · ${tc("Pick_Blocked")}` : ""}`, onPick: () => { close(); done(x); }, })), ...libFound.map((x) => { const at = scope as Scope; return { key: x.row.이름, tag: tc(x.kind), name: x.row.제목 || x.row.이름, sub: x.row.이름, unit: x.row.단위 ?? "", onPick: () => { close(); lib?.pick({ ...at, 키: x.row.이름 }, x.row.제목 || x.row.이름); }, }; }), ]); }; const loadLib = async (): Promise => { if (!lib || !scope) return; const mine = ++seq; const at = scope; loading = true; find(); try { const [promote, combo] = await Promise.all([ fetchLibrary(at, "promote"), fetchLibrary(at, "precombo"), ]); if (mine !== seq) return; libRows = [ ...promote.map((row) => ({ kind: "Kind_Promote" as const, row })), ...combo.map((row) => ({ kind: "Kind_Combo" as const, row })), ]; } catch (error) { if (mine !== seq) return; libRows = []; showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error"); } loading = false; find(); }; const drawLayers = (): void => layerBar.replaceChildren( ...scopes.map((x) => createButton({ label: tc(LAYER_TEXT[x.층 as keyof typeof LAYER_TEXT]), variant: x === scope ? "filled" : "ghost", onClick: () => { scope = x; drawLayers(); void loadLib(); }, }), ), ); sub.select.addEventListener("change", () => { const d = subs.find((s) => s.name === sub.select.value)?.details ?? []; detail.setOptions(opts(d), ""); detail.select.disabled = d.length === 0; find(); }); detail.select.addEventListener("change", find); finder.search.addEventListener("input", find); layerBar.style.display = "none"; if (lib) void upper(lib.layer, lib.owner).then((got) => { scopes = got; scope = got[got.length - 1] ?? null; // 기본 = 가장 가까운 층 if (!got.length) return; layerBar.style.display = ""; drawLayers(); void loadLib(); }); const dialog = el("div", { className: "m01-logic__pick m01-combo", attrs: { role: "dialog", "aria-label": tc("Pick_Title") }, children: [ el("div", { className: "m01-logic__section-head", children: [ el("h3", { text: tc("Pick_Title") }), createButton({ label: tc("Pick_Close"), variant: "ghost", onClick: close }), ], }), finder.root, ], }); 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()); document.body.append(backdrop); finder.search.focus(); void Promise.all([fetchLogicSubs(), fetchLogics()]) .then(([s, l]) => { subs.push(...s); all = l; find(); sub.setOptions(opts(s.map((x) => x.name)), ""); }) .catch((error) => showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error"), ); }