/* ============================================================================= * B08_Quantity_UI_Milestone.ts * B08 메인 탭 「거리이정표」(PLAN 244-1 · 244-4) — 하위 탭 줄 [거리이정표] + 경로 탭(업체마다 · 이름 = 업체 이름). * 경로 탭은 다이어그램 [+] 등록 · 항목 정보 모달 [삭제]로만 생기고 없어짐(하위 탭 줄 [추가] · [삭제] 없음). * * [거리이정표] = 표(항목 · 업체 이름 · 목적지 ▾ · 2차포장 · 1차포장 · 비포장 · 포장 · 비포장 · 합계 · 비고) * 표 합 = `…_Milestone_Calc` TS 짝(조작 즉시) · [저장] 때 서버가 파이썬 짝으로 다시 셈 * 입력 = 더블클릭(243) · 업체 이름을 고치면 그 경로 이름(하위 탭)도 * 위쪽 다이어그램 = `…_UI_Milestone_Diagram`(상자 누름 = 모달 · [+] = 항목 추가 모달 · 팬 · 줌) * 경로 탭 = `…_UI_Milestone_Route`(길찾기 지도 그림만) · 데이터 · [저장] = `…_UI_Milestone_State` * ========================================================================== */ import "./B08_Quantity_UI_Milestone.css"; import { createDataTable, type DataColumn } from "@ui/ui_template_data_table"; import { createButton, el, showToast } from "@ui/ui_template_elements"; import { computeMilestone, type MilestoneRoute } from "./B08_Quantity_Milestone_Calc"; import { createDiagram } from "./B08_Quantity_UI_Milestone_Diagram"; import { liveMilestone, loadMilestone, onMilestoneChange, putMilestone, siteInfo, } from "./B08_Quantity_UI_Milestone_State"; import { DEST_TEXT, editRoute, pickAtCell, routeView, trackCells, type MilestoneView, } from "./B08_Quantity_UI_Milestone_Route"; const KM = { kind: "number", decimals: 3, trim: true, width: 72 } as const; const COLUMNS: DataColumn[] = [ { key: "item", label: "항목", width: 120 }, { key: "company", label: "업체 이름", width: 120 }, { key: "dest", label: "목적지", width: 72 }, { key: "s2", label: "2차포장", group: ["도로 구분 (km)"], ...KM }, { key: "s1", label: "1차포장", group: ["도로 구분 (km)"], ...KM }, { key: "un", label: "비포장", group: ["도로 구분 (km)"], ...KM }, { key: "paved", label: "포장", group: ["계 (km)"], ...KM }, { key: "unpaved", label: "비포장", group: ["계 (km)"], ...KM }, { key: "total", label: "합계", group: ["계 (km)"], ...KM }, { key: "note", label: "비고", width: 160 }, ]; /** 새 경로(업체) — 캐시에 넣고 id 를 돌려줌 */ export function addRoute(projectId: string, init: Partial = {}): string { const data = liveMilestone(projectId); const id = `r${Date.now().toString(36)}${Math.floor(Math.random() * 1296).toString(36)}`; data.routes.push({ id, name: `업체 ${data.routes.length + 1}`, address: "", via_base: true, dir: null, legs: [{ road: "2차포장", km: null }], rows: [{ item: "", dest: "start", note: "" }], ...init, }); putMilestone(projectId, data); return id; } /** [거리이정표] 하위 탭 — 위 다이어그램 + 표 */ function tableView(projectId: string): MilestoneView { const diagram = createDiagram(projectId, (init) => addRoute(projectId, init)); let keys: Array<{ route: string; index: number }> = []; const table = createDataTable({ columns: COLUMNS, rows: [], caption: "거리이정표", empty: "", editOnDouble: true, editable: (_i, key) => key === "item" || key === "company" || key === "note", onEdit: (i, key, text) => { const at = keys[i]; if (!at) return; editRoute(projectId, at.route, (_, r) => { if (key === "company") r.name = text; else if (r.rows[at.index]) r.rows[at.index][key as "item" | "note"] = text; }); }, onCellClick: (i, key) => { const at = keys[i]; if (key !== "dest" || !at) return; pickAtCell( (["start", "mid"] as const).map((dest) => [ DEST_TEXT[dest], () => editRoute(projectId, at.route, (_, r) => { if (r.rows[at.index]) r.rows[at.index].dest = dest; }), ]), ); }, }); trackCells(table.root); table.root.dataset.b08Milestone = "table"; const none = el("p", { className: "ui-general-block__empty", text: "경로가 없습니다 — 다이어그램 공사지 · 기준점 [+]로 항목을 넣으세요.", }); const refresh = (): void => { const rows = computeMilestone(liveMilestone(projectId), siteInfo()?.mid_default_m ?? null); keys = rows.map(({ route, index }) => ({ route, index })); none.hidden = rows.length > 0; table.setRows(rows.map((r) => ({ ...r, dest: `${DEST_TEXT[r.dest]} ▾` }))); diagram.refresh(); }; refresh(); return { root: el("div", { className: "b08-milestone", children: [diagram.root, none, table.root] }), refresh, }; } export function renderMilestone(host: HTMLElement, projectId: string): void { const tabs = el("div", { className: "b08-quantity__tabs b08-structure__tabs", attrs: { role: "tablist" }, }); const view = el("div"); let current = "table"; let body: MilestoneView | null = null; const drawTabs = (): void => { const routes = liveMilestone(projectId).routes; const tab = (id: string, label: string): HTMLButtonElement => { const button = createButton({ label, variant: id === current ? "filled" : "ghost", size: "sm", onClick: () => id !== current && open(id), }); button.setAttribute("role", "tab"); button.setAttribute("aria-selected", String(id === current)); button.dataset.b08MilestoneTab = id; return button; }; tabs.replaceChildren( tab("table", "거리이정표"), ...routes.map((r, i) => tab(r.id, r.name.trim() || `업체 ${i + 1}`)), ); }; function open(id: string): void { current = id; drawTabs(); body = id === "table" ? tableView(projectId) : routeView(projectId, id); view.replaceChildren(body.root); } host.replaceChildren( tabs, el("p", { className: "ui-general-block__empty", text: "거리이정표를 불러오는 중…" }), ); loadMilestone(projectId).then( () => { host.replaceChildren(tabs, view); open("table"); onMilestoneChange(host, () => { // 지운 경로 탭이면 [거리이정표] 로 · 아니면 탭 이름 · 몸만 다시 셈(입력 칸은 그대로) if (current !== "table" && !liveMilestone(projectId).routes.some((r) => r.id === current)) return open("table"); drawTabs(); body?.refresh(); }); }, (error: unknown) => showToast(`거리이정표를 불러오지 못했습니다 — ${String(error)}`, "error"), ); }