Files
Aislo/E03_StabSection/E03_StabSection_UI_List.ts

156 lines
5.7 KiB
TypeScript

/* =============================================================================
* E03_StabSection_UI_List.ts
* E03 좌측 측점 목록(PLAN 532-3) — 측점 · 깎기/쌓기 · 조건별 최소 안전율 · 판정(아이콘 + 글) ·
* 대표 단면 별(자동 = 채움 · 사용자 켬/끔 = 밑줄 · 누르면 바꿈) · 판정 면(원호 · 비원호 + 면 확인 · 모형 끝 면 —
* 567-1 · 좁아서 맨 끝 칸). 정렬 · 거름은 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 { stationLabel } from "../common_util/common_util_station";
import { e03Table } from "./E03_StabSection_UI_Table";
import {
basisOf,
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;
/** 측점 간격(m) — 측점 글 「No.+거리」 의 분모 */
interval: number;
/** 사용자 켬/끔 — 서버 값(별 누름 = 바로 저장) */
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") },
{ label: t("E03_Col_Basis"), title: t("E03_Basis_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: stationLabel(row.chainage_m, draw.interval, 1),
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,
basisBadges(row, draw.condition),
];
});
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;
}
/** 판정 면 칸 — 원호 · 비원호 + 「면 확인」 · 「모형 끝 면」(그 조건 기준) */
export function basisBadges(row: StationRow, condition: Condition): HTMLElement {
const box = el("span", { className: "e03-basis" });
const b = basisOf(row, condition);
if (b.method) {
const kind = createBadge(
t(b.circular ? "E03_Basis_Circular" : "E03_Basis_NonCircular"),
b.circular ? "neutral" : "info",
{ size: 18 },
);
kind.title = t(`E03_Method_${b.method}` as LocaleKey);
kind.dataset.role = "basis-kind";
box.append(kind);
}
if (b.surfaceCheck) {
const flag = createBadge(t("E03_SurfaceCheck_Short"), "warning", { size: 18 });
flag.title = `${t("E03_SurfaceCheck")} — ${t("E03_SurfaceCheck_Hint")}`;
flag.dataset.role = "basis-check";
box.append(flag);
}
if (b.edge) {
const flag = createBadge(t("E03_EdgeSurface_Short"), "warning", { size: 18 });
flag.title = `${t("E03_EdgeSurface")} — ${t("E03_EdgeSurface_Hint")}`;
flag.dataset.role = "basis-edge";
box.append(flag);
}
return box;
}
function hookEmpty(text: string): HTMLElement {
const empty = createEmpty({ text, small: true });
empty.classList.add("e03-empty");
return empty;
}