/* ============================================================================= * M02_MasterTemplete_Labor_Pick.ts * [로직 더하기] 창 — 구분 → 상세구분 → 찾기로 사용자가 로직을 하나씩 고름(자동 추천 없음) · * 탭 단가산출 · 단가승격 · 사전조합(50-3 ④) — 서고 탭은 층 단추(위 층만 · 짝 `M02_Labor_Ref.readable`) + * 그 층 목록 · 고르면 참조 {층 · 주인 · 키} · 프로젝트 · 카드엔 서고 탭 없음 * ========================================================================== */ import { createButton, createInputField, createSelectField, el, showToast, } from "@ui/ui_template_elements"; 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 searchField = createInputField({ type: "search", placeholder: tc("Pick_Search") }); const list = el("div", { className: "m01-logic__pick-list" }); const hint = el("p", { className: "m01-logic__muted", text: tc("Pick_Hint") }); const close = (): void => backdrop.remove(); const find = (): void => { const q = searchField.input.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)), ); hint.textContent = found.length ? `${found.length} / ${all.length}` : tc("Pick_None"); list.replaceChildren( ...found.slice(0, 100).map((x) => { const row = el("button", { className: "m01-logic__pick-row m01-logic__pick-row--button", attrs: { type: "button", "data-pick": x.키 }, children: [ el("div", { className: "m01-logic__stack", children: [ el("strong", { text: x.이름 }), el("span", { className: "m01-logic__muted", text: `${x.키} · ${x.구분} ${x.상세구분}`.trim(), }), ], }), el("span", { text: `${x.결과단위}${x.blocked ? ` · ${tc("Pick_Blocked")}` : ""}` }), ], }); row.addEventListener("click", () => { close(); done(x); }); return row; }), ); }; 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; }); /* 서고 탭(50-3 ④) — 탭 · 층 단추 · 목록 */ const filters = el("div", { className: "m01-logic__filters", children: [ sub.root, detail.root, searchField.root, createButton({ label: tc("Pick_Find"), variant: "filled", onClick: find }), ], }); const tabs = el("div", { className: "ui-btn-group", attrs: { "data-pick-tabs": "" } }); const layerBar = el("div", { className: "ui-btn-group", attrs: { "data-pick-layers": "" } }); let tab: "logic" | "promote" | "precombo" = "logic"; let scopes: Scope[] = []; let scope: Scope | null = null; let seq = 0; const libList = async (): Promise => { if (!lib || !scope || tab === "logic") return; const mine = ++seq; const at = scope; list.replaceChildren(); hint.textContent = tc("Loading"); let rows: LibraryRow[] = []; try { rows = await fetchLibrary(at, tab); } catch (error) { if (mine === seq) hint.textContent = error instanceof Error ? error.message : tc("Load_Failed"); return; } if (mine !== seq) return; hint.textContent = rows.length ? `${rows.length}` : tc("Pick_None"); list.replaceChildren( ...rows.map((x) => { const row = el("button", { className: "m01-logic__pick-row m01-logic__pick-row--button", attrs: { type: "button", "data-pick": x.이름 }, children: [ el("div", { className: "m01-logic__stack", children: [ el("strong", { text: x.제목 || x.이름 }), el("span", { className: "m01-logic__muted", text: x.이름 }), ], }), el("span", { text: x.단위 ?? "" }), ], }); row.addEventListener("click", () => { close(); lib.pick({ ...at, 키: x.이름 }, x.제목 || x.이름); }); return row; }), ); }; 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 libList(); }, }), ), ); const drawTabs = (): void => { tabs.replaceChildren( ...(["logic", "promote", "precombo"] as const).map((k) => createButton({ label: tc( k === "logic" ? "Kind_Logic" : k === "promote" ? "Kind_Promote" : "Lib_Precombo", ), variant: k === tab ? "filled" : "ghost", onClick: () => { tab = k; drawTabs(); filters.style.display = k === "logic" ? "" : "none"; // 공용 꼴 display 가 hidden 을 이김 layerBar.style.display = k === "logic" ? "none" : ""; if (k === "logic") { list.replaceChildren(); hint.textContent = tc("Pick_Hint"); } else void libList(); }, }), ), ); }; 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; drawTabs(); drawLayers(); }); 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 }), ], }), tabs, filters, layerBar, hint, list, ], }); 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); searchField.input.addEventListener("keydown", (ev) => ev.key === "Enter" && find()); searchField.input.focus(); void Promise.all([fetchLogicSubs(), fetchLogics()]) .then(([s, l]) => { subs.push(...s); all = l; sub.setOptions(opts(s.map((x) => x.name)), ""); }) .catch((error) => showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error"), ); }