- B01_Dashboard_UI_CalcSites: 머리 줄 + 계산 줄(번호 · 이름) · 잠긴 줄 까닭 글 · 머리 · 줄 체크 서로 따라감 - 시험: test_309_2 두 파일 줄 수 · 켤 수 있는 줄 · 머리 따라 줄 꺼짐 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
156 lines
5.7 KiB
TypeScript
156 lines
5.7 KiB
TypeScript
/* =============================================================================
|
|
* B01_Dashboard_UI_CalcSites.ts
|
|
* 「계산 자리」 창(PLAN 504-2 · 504-3 · 요구 309-2 · 312) — 시스템 관리자만(카드 = `settingLinks` · 쓰기 = 서버
|
|
* `require_system_admin`). 묶음 8 머리 + 그 아래 계산 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<typeof L>[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<string, CalcSiteValue>,
|
|
changed: () => void,
|
|
): HTMLElement[][] {
|
|
const server = (): HTMLInputElement => check(group.fixed !== "browser", true);
|
|
const why = K(`Why_${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 })]),
|
|
];
|
|
}
|
|
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 }),
|
|
]),
|
|
];
|
|
}
|
|
|
|
const calcName = (no: string): HTMLElement =>
|
|
el("span", { className: "b01-dashboard__calc-name", text: `${no} · ${LABELS.get(no) ?? ""}` });
|
|
|
|
function grid(
|
|
groups: CalcSiteGroup[],
|
|
draft: Record<string, CalcSiteValue>,
|
|
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<number>();
|
|
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<void> {
|
|
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<string, CalcSiteValue> =>
|
|
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<boolean> => {
|
|
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),
|
|
);
|
|
},
|
|
});
|
|
}
|