/* ============================================================================= * M01_MasterData_UI_Alias.ts * M01 별칭 장부(PLAN 718-3 · 요구서 내역12 4장 23 · 4-1 확정 6) — 본 칸 · 시스템 관리자만. * · 「모인 후보」 = 모든 회사 · 개인 층 별칭 장부(일위대가 맞춤) + 공사 내역 맞춤을 서버가 모음 * (`/api/m01/alias-ledger` · 718-2) · 이름 · 짝 · 회사 수 · 사람 수(누군지 안 보임) · 충돌 · * [승인] = 마스터 장부에 넣음(모든 사용자 제안에 쓰임) · [반려] = 다시 안 보임 · 둘 다 확인 창 * · 「마스터 장부」 = M02 별칭 장부 부품(`buildHopyoLedger` · 마스터 층) 그대로 + 마스터 공사 내역 맞춤 줄 [지움] * · 자동 반영 없음 — 승인한 것만 모두에게 · 승인 · 반려 · 지움은 누를 때 바로 서버(확인 창 뒤) * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; import { createBadge } from "@ui/ui_template_badge"; import { askConfirm, createButton, el, showToast } from "@ui/ui_template_elements"; import { loadingError, showLoading } from "@ui/ui_template_loading"; import { createPanelHead } from "@ui/ui_template_panel_head"; import { createTable, type Column } from "@ui/ui_template_table"; import { createTabs } from "@ui/ui_template_tabs"; import "@ui/ui_template_general_layout.css"; // 마스터 장부 부품(M02 Hopyo_Ledger) 표 꼴 — M01 엔 안 실려 있음 import type { LedgerPair } from "../M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api"; import { buildHopyoLedger, KIND_TEXT, LEDGER_HINT, pairText, } from "../M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger"; export const ALIAS_TITLE = "별칭 장부"; const KIND_NAME: Record = { 맞춤: "일위대가", 내역맞춤: "공사 내역" }; interface Origin { 품명: string; 규격: string; 단위: string; } export interface AliasCandidate { 종류: "맞춤" | "내역맞춤"; 열쇠: string; 서명: string; 원래: Origin; 갈래?: string; 짝?: LedgerPair[]; 확정?: string; 이름?: string; 회사: number; 사람: number; 때: string | null; 충돌: boolean; 마스터: "다름" | null; } interface MasterBill { 열쇠: string; 원래: Origin; 확정: string; 이름?: string; } export interface AliasBoard { 후보: AliasCandidate[]; 모은곳: { 회사: number; 사람: number }; 반려: number; 마스터내역?: MasterBill[]; } async function call(path: string, body?: unknown): Promise { const res = await fetch(`${API_BASE_URL}/m01/alias-ledger${path}`, { method: body === undefined ? "GET" : "POST", credentials: "include", headers: { "Content-Type": "application/json" }, ...(body === undefined ? {} : { body: JSON.stringify(body) }), }); const got = (await res.json()) as T & { detail?: unknown }; if (!res.ok) throw new Error(String(got.detail ?? `HTTP ${res.status}`)); return got; } /** 짝 글 — 일위대가 = 갈래 · 우리 로직 · 공사 내역 = 우리 공종 이름 */ export const candidateText = (c: AliasCandidate): string => c.종류 === "내역맞춤" ? `${c.이름 ?? ""} (${c.확정 ?? ""})` : c.갈래 === "짝" ? (c.짝 ?? []).map(pairText).join(" + ") : (KIND_TEXT[c.갈래 ?? ""] ?? c.갈래 ?? ""); const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); const pick = (c: AliasCandidate) => ({ 종류: c.종류, 열쇠: c.열쇠, 서명: c.서명 }); const CANDIDATE_COLS: Column[] = [ { label: "종류", width: "84px" }, { label: "엑셀 품명" }, { label: "규격" }, { label: "단위", width: "64px" }, { label: "짝" }, { label: "회사", width: "56px", align: "end" }, { label: "사람", width: "56px", align: "end" }, { label: "충돌", width: "96px" }, { label: "", width: "136px" }, ]; const BILL_COLS: Column[] = [ { label: "엑셀 품명" }, { label: "규격" }, { label: "단위", width: "64px" }, { label: "우리 공종" }, { label: "", width: "72px" }, ]; export function buildAliasLedger(): HTMLElement { const root = el("div", { className: "m01-alias", attrs: { "data-alias-ledger": "" } }); const body = el("div", { className: "m01-alias__body" }); let view = "candidates"; async function act(c: AliasCandidate, ok: boolean): Promise { const name = `${c.원래.품명} ${c.원래.규격}`.trim(); const yes = await askConfirm({ title: ok ? "마스터 장부에 승인" : "후보 반려", message: ok ? `「${name}」 → ${candidateText(c)}\n마스터 장부에 넣으면 모든 사용자 제안에 쓰임(각자 장부가 먼저).` : `「${name}」 → ${candidateText(c)}\n반려하면 같은 짝은 후보에 다시 안 보임.`, okLabel: ok ? "승인" : "반려", danger: !ok, }); if (!yes) return; try { const got = await call(ok ? "/approve" : "/reject", pick(c)); showToast(ok ? "마스터 장부에 넣음" : "반려함", "success"); paintCandidates(got); } catch (error) { showToast(`${ok ? "승인" : "반려"} 못 함 — ${why(error)}`, "error"); } } function candidateRow(c: AliasCandidate): (string | Node)[] { const clash = c.마스터 === "다름" ? "마스터와 다름" : c.충돌 ? "다른 짝 있음" : ""; return [ KIND_NAME[c.종류] ?? c.종류, c.원래.품명, c.원래.규격, c.원래.단위, candidateText(c), String(c.회사), String(c.사람), clash ? createBadge(clash, "warning") : "", el("div", { className: "ui-toolbar", children: [ createButton({ label: "승인", variant: "filled", size: "sm", onClick: () => void act(c, true), }), createButton({ label: "반려", variant: "ghost", size: "sm", onClick: () => void act(c, false), }), ], }), ]; } function paintCandidates(board: AliasBoard): void { const head = createPanelHead({ title: "모인 후보", sub: `${LEDGER_HINT} · 회사 ${board.모은곳.회사} · 사람 ${board.모은곳.사람}의 장부에서 모음 · 반려 ${board.반려} · 승인한 것만 모두에게 · 누가 적었는지는 안 보임`, }); body.replaceChildren( head.root, createTable(CANDIDATE_COLS, board.후보.map(candidateRow), { density: "data", empty: "모인 후보 없음", }), ); } async function forget(b: MasterBill): Promise { const yes = await askConfirm({ title: "마스터 공사 내역 맞춤 지움", message: `「${b.원래.품명} ${b.원래.규격}」 → ${b.이름 ?? b.확정}\n지우면 모든 사용자 제안에서 빠짐.`, okLabel: "지움", danger: true, }); if (!yes) return; try { const got = await call<{ 마스터내역: MasterBill[] }>("/master-bill/forget", { 열쇠: b.열쇠 }); showToast("지움", "success"); paintMaster(got.마스터내역); } catch (error) { showToast(`지우지 못 함 — ${why(error)}`, "error"); } } function paintMaster(bill: MasterBill[]): void { const rows = bill.map((b) => [ b.원래.품명, b.원래.규격, b.원래.단위, `${b.이름 ?? ""} (${b.확정})`, createButton({ label: "지움", variant: "danger", size: "sm", onClick: () => void forget(b) }), ]); body.replaceChildren( buildHopyoLedger({ scope: { layer: "system", owner: null }, canWrite: true }), createPanelHead({ title: "마스터 공사 내역 맞춤", sub: "B09 내역 줄 ↔ 우리 공종(각자 기억이 먼저)", }).root, createTable(BILL_COLS, rows, { density: "data", empty: "마스터 공사 내역 맞춤 없음" }), ); } async function load(): Promise { const stop = showLoading(body); try { const got = await call(""); if (view === "candidates") paintCandidates(got); else paintMaster(got.마스터내역 ?? []); } catch (error) { body.replaceChildren(loadingError(why(error), () => void load())); } finally { stop(); } } const tabs = createTabs({ tabs: [ { key: "candidates", label: "모인 후보" }, { key: "master", label: "마스터 장부" }, ], value: view, onChange: (key) => { view = key; void load(); }, }); root.append(tabs.root, body); // 제목 = 본 칸 머리(ALIAS_TITLE · 왼쪽 판이 넘김) void load(); return root; }