/* ============================================================================= * B01_Dashboard_UI_CalcSites.ts * 「계산 자리」 창(PLAN 504-2 · 504-3 · 요구 309-2 · 312) — 시스템 관리자만(카드 = `settingLinks` · 쓰기 = 서버 * `require_system_admin`). 묶음 12 머리 + 그 아래 계산 45 줄(번호 · 이름 = 등록부 `label`) · 칸 둘(「조작 중 * 브라우저 셈」 · 「[저장] 서버 정본」 늘 켬 · 잠금). 묶음 머리 체크 = 그 묶음 전부. * 바꿀 수 있는 줄 = 서버 판 `groups[].switchable`(판 열쇠 = 묶음) · 나머지는 잠긴 체크 + 까닭 글. * 고침은 창 안 사본에만 · [저장] 때만 바뀐 줄을 PUT(선례 `app_settings.ts`). * ========================================================================== */ import { createButton, el, showToast } from "@ui/ui_template_elements"; import { table } from "@ui/ui_template_general_blocks"; import { askLeave } from "@ui/ui_template_leave_dialog"; import { openModal } from "@ui/ui_template_modal"; import { fetchCalcSites, saveCalcSites, type CalcSiteGroup, type CalcSiteValue, type CalcSitesBoard, } from "../A00_Common/design/design_calc_sites"; import { CALC_REGISTRY } from "../A00_Common/design/logic/design_calc_registry"; import { L } from "./B01_Dashboard_UI_Common"; type LocaleKey = Parameters[0]; const K = (key: string): string => L(`B01_CalcSites_${key}` as LocaleKey); function check(checked: boolean, locked: boolean): HTMLInputElement { const input = el("input", { attrs: { type: "checkbox" } }); input.checked = checked; input.disabled = locked; return input; } const LABELS = new Map(CALC_REGISTRY.map((entry) => [entry.no, entry.label])); /** 묶음 머리 줄 + 계산 한 줄씩. 바꿀 수 있는 묶음은 머리 · 줄 체크가 같은 값(서로 따라감). */ function groupRows( group: CalcSiteGroup, draft: Record, changed: () => void, ): HTMLElement[][] { const server = (): HTMLInputElement => check(group.fixed !== "browser", true); const why = (no: string): string => K(`Why_${group.whys?.[no] ?? group.why ?? group.key}`); const head = el("span", { children: [el("strong", { text: K(group.key) }), ` (${group.nos.length})`], }); if (!group.switchable) { const browser = (): HTMLInputElement => check(group.fixed !== "server", true); return [ [head, browser(), server(), el("span")], ...group.nos.map((no) => [calcName(no), browser(), server(), el("span", { text: why(no) })]), ]; } const boxes = [group.key, ...group.nos].map((id, index) => { const box = check(draft[group.key] !== "server", false); if (index === 0) box.dataset.calcSite = group.key; else box.dataset.calcNo = id; return box; }); for (const box of boxes) { box.addEventListener("change", () => { draft[group.key] = box.checked ? "both" : "server"; for (const other of boxes) other.checked = box.checked; changed(); }); } return [ [head, boxes[0], server(), el("span")], ...group.nos.map((no, index) => [ calcName(no), boxes[index + 1], server(), el("span", { text: why(no) }), ]), ]; } const calcName = (no: string): HTMLElement => el("span", { className: "b01-dashboard__calc-name", text: `${no} · ${LABELS.get(no) ?? ""}` }); function grid( groups: CalcSiteGroup[], draft: Record, changed: () => void, ): HTMLElement { const rows = groups.flatMap((group) => groupRows(group, draft, changed)); const wrap = table([K("Col_Group"), K("Col_Browser"), K("Col_Server"), K("Col_Why")], rows, 14); const heads = new Set(); let at = 0; for (const group of groups) { heads.add(at); at += 1 + group.nos.length; } wrap.querySelectorAll("tbody tr").forEach((row, index) => { if (heads.has(index)) row.classList.add("b01-dashboard__calc-group"); }); return wrap; } /** 시스템 설정 「계산 자리」 카드 — 판을 받아 창을 엶. */ export async function openCalcSitesDialog(): Promise { let board: CalcSitesBoard; try { board = await fetchCalcSites(); } catch (error) { showToast(error instanceof Error ? error.message : K("LoadFailed"), "error"); return; } const draft = { ...board.sites }; const dirty = (): Record => Object.fromEntries(Object.entries(draft).filter(([key, value]) => board.sites[key] !== value)); const count = el("p", { className: "b01-dashboard__calc-count" }); const recount = (): void => { const n = Object.values(draft).filter((value) => value === "server").length; count.textContent = K("ServerCount").replace("{value}", String(n)); }; recount(); const save = async (): Promise => { const changes = dirty(); if (!Object.keys(changes).length) return true; try { board = await saveCalcSites(changes); } catch (error) { showToast(error instanceof Error ? error.message : String(error), "error"); return false; } showToast(K("Saved"), "success"); return true; }; const saveButton = createButton({ label: L("Common_Btn_Save"), variant: "filled", onClick: () => void save().then((ok) => ok && modal.close()), }); const modal = openModal({ title: K("Title"), closeLabel: L("Common_Btn_Close"), dialogClass: "b01-dashboard__calc-sites", wide: true, actions: [saveButton], beforeClose: async () => { if (!Object.keys(dirty()).length) return true; const choice = await askLeave(K("Leave")); return choice === "discard" || (choice === "save" && (await save())); }, mount: (body) => { body.append( count, el("p", { className: "b01-dashboard__calc-note", text: K("NextOpen") }), grid(board.groups, draft, recount), ); }, }); }