120 lines
4.3 KiB
TypeScript
120 lines
4.3 KiB
TypeScript
/* =============================================================================
|
|
* E03_StabSection_UI_List.ts
|
|
* E03 좌측 측점 목록(PLAN 532-3) — 측점 · 깎기/쌓기 · 조건별 최소 안전율 · 판정(아이콘 + 글) ·
|
|
* 대표 단면 별(자동 = 채움 · 사용자 켬/끔 = 밑줄 · 누르면 바꿈). 정렬 · 거름은 logic `listRows`.
|
|
* ========================================================================== */
|
|
|
|
import { createBadge, type BadgeTone } from "@ui/ui_template_badge";
|
|
import { createIconButton } from "@ui/ui_template_button";
|
|
import { el } from "@ui/ui_template_elements";
|
|
import { createEmpty } from "@ui/ui_template_empty";
|
|
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
|
import { e03Table } from "./E03_StabSection_UI_Table";
|
|
import {
|
|
listRows,
|
|
roleText,
|
|
verdictOf,
|
|
type Condition,
|
|
type SortMode,
|
|
type StationRow,
|
|
} from "./logic/E03_StabSection_Logic";
|
|
|
|
const fmt = (v: number | null | undefined): string =>
|
|
v === null || v === undefined || !Number.isFinite(v) ? "-" : v.toFixed(2);
|
|
|
|
export interface ListDraw {
|
|
rows: StationRow[];
|
|
conditions: Condition[];
|
|
condition: Condition;
|
|
sort: SortMode;
|
|
ngOnly: boolean;
|
|
active: string | null;
|
|
/** 사용자 켬/끔 — 저장 전 캐시 포함 · 없으면 서버 값 */
|
|
representative: (row: StationRow) => { on: boolean; user: boolean | null };
|
|
editable: boolean;
|
|
}
|
|
|
|
const VERDICT_TONE: Record<string, BadgeTone> = { ok: "success", ng: "danger", none: "neutral" };
|
|
|
|
export function buildList(
|
|
draw: ListDraw,
|
|
onSelect: (key: string) => void,
|
|
onToggle: (key: string) => void,
|
|
): HTMLElement {
|
|
const box = el("div", { className: "e03-list" });
|
|
const rows = listRows(draw.rows, draw.condition, draw.sort, draw.ngOnly);
|
|
const columns = [
|
|
{ label: t("E03_Col_Station") },
|
|
{ label: t("E03_Col_Role") },
|
|
...draw.conditions.map((c) => ({ label: t(`E03_Cond_${c}` as LocaleKey), num: true })),
|
|
{ label: t("E03_Verdict") },
|
|
{ label: t("E03_Col_Rep"), title: t("E03_Rep_Hint") },
|
|
];
|
|
const cells = rows.map((row) => {
|
|
const v = verdictOf(row, draw.condition);
|
|
const rep = draw.representative(row);
|
|
const title =
|
|
[
|
|
...(row.representative?.auto ?? []).map((r) => t(`E03_Rep_${r}` as LocaleKey)),
|
|
rep.user !== null ? t("E03_Rep_User") : "",
|
|
]
|
|
.filter(Boolean)
|
|
.join(" · ") || t("E03_Rep_Hint");
|
|
const star = createIconButton({
|
|
glyph: rep.on ? "★" : "☆",
|
|
title,
|
|
size: 20,
|
|
pressed: rep.on,
|
|
disabled: !draw.editable,
|
|
onClick: (ev) => {
|
|
ev.stopPropagation();
|
|
onToggle(row.key);
|
|
},
|
|
});
|
|
star.classList.add("e03-star");
|
|
star.classList.toggle("is-on", rep.on);
|
|
star.classList.toggle("is-user", rep.user !== null);
|
|
const verdict = createBadge(
|
|
row.error ? t("E03_Error") : v === "none" ? "-" : t(`E03_Verdict_${v}` as LocaleKey),
|
|
VERDICT_TONE[v],
|
|
{ kind: "lamp" },
|
|
);
|
|
verdict.classList.add("e03-verdict", `e03-verdict--${v}`);
|
|
const station = el("span", {
|
|
text: row.label?.replace(/^STA\./, "").replace(/\.000$/, "") ?? row.key,
|
|
children: row.walls
|
|
? [el("span", { className: "e03-wall-tag", text: "▦", attrs: { title: t("E03_Walls") } })]
|
|
: [],
|
|
});
|
|
return [
|
|
station,
|
|
row.sides.length ? roleText(row, { cut: t("E03_CutShort"), fill: t("E03_FillShort") }) : "-",
|
|
...draw.conditions.map((c) => fmt(row.min[c])),
|
|
verdict,
|
|
star,
|
|
];
|
|
});
|
|
const list = e03Table(columns, cells, "station-list");
|
|
list.table.tHead?.rows[0].cells
|
|
.item(2 + draw.conditions.indexOf(draw.condition))
|
|
?.setAttribute("aria-sort", "other");
|
|
list.rows.forEach((tr, i) => {
|
|
const row = rows[i];
|
|
tr.dataset.key = row.key;
|
|
tr.dataset.verdict = verdictOf(row, draw.condition);
|
|
tr.classList.toggle("is-active", row.key === draw.active);
|
|
if (row.error) tr.title = row.error;
|
|
tr.addEventListener("click", () => onSelect(row.key));
|
|
});
|
|
box.append(list.root);
|
|
if (!rows.length) box.append(hookEmpty(t("E03_ListEmpty")));
|
|
queueMicrotask(() => box.querySelector("tr.is-active")?.scrollIntoView({ block: "nearest" }));
|
|
return box;
|
|
}
|
|
|
|
function hookEmpty(text: string): HTMLElement {
|
|
const empty = createEmpty({ text, small: true });
|
|
empty.classList.add("e03-empty");
|
|
return empty;
|
|
}
|