/* ============================================================================= * B08_Quantity_UI_Milestone.ts * B08 메인 탭 「거리이정표」(PLAN 244-1 · 244-4) — 하위 탭 줄 [거리이정표] + 경로 탭(거래처마다 · 이름 = 거래처 이름). * 경로 탭은 다이어그램 [+] 등록 · 항목 정보 모달 [삭제]로만 생기고 없어짐(하위 탭 줄 [추가] · [삭제] 없음). * * [거리이정표] = 표(항목 · 거래처 이름 · 목적지 ▾ · 도로 구분 · 계 · 비고) — 열(244-13) = 기준점 있음 = * 2차포장 · 1차포장 (· 포장) · 비포장 + 계 포장 · 비포장 · 합계 / 없음 = 포장 · 비포장 · 합계 * 표 합 = `…_Milestone_Calc` TS 짝(조작 즉시) · [저장] 때 서버가 파이썬 짝으로 다시 셈 * 입력 = 더블클릭(243) · 거래처 이름을 고치면 그 경로 이름(하위 탭)도 * 위쪽 다이어그램 = `…_UI_Milestone_Diagram`(상자 누름 = 모달 · [+] = 항목 추가 모달 · 팬 · 줌) * 경로 탭 = `…_UI_Milestone_Route`(길찾기 지도 그림만) · 데이터 · [저장] = `…_UI_Milestone_State` * 단가연결(251-4) = 항목 칸 앞 램프(초록 = 연결 · 노랑 = 없음 · 호버 = 무엇) · 줄 우클릭 [단가연결] · * [단가연결 풀기] — 창 = 연결 없음 → 단가연결 목록 → 옵션 값 창(276 · 옛 길) · 연결 있음 → * 「운반 단가」 모달(253-3 · B09 운반공 줄과 같음 · 그 행 거리) · 경로 `ref` · B09 운반공 줄이 받음 * 표 열 · 줄 짜기 = `B08_Quantity_Milestone_Table`(화면 없음 · 인쇄 미리보기도 씀 · 302-3) * ========================================================================== */ 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 { attachHoverInfo } from "@ui/ui_template_hover_info"; import type { Row } from "../B09_Estimation/B09_Estimation_Calc_Base"; import { openHaulModal } from "../B09_Estimation/B09_Estimation_UI_Haul_Modal"; import { openPriceLink } from "../B09_Estimation/B09_Estimation_UI_Tabs_Bill_Link"; import type { MilestoneRoute } from "./B08_Quantity_Milestone_Calc"; import { DEST_TEXT, milestoneColumns, milestoneTable } from "./B08_Quantity_Milestone_Table"; import { createDiagram } from "./B08_Quantity_UI_Milestone_Diagram"; import { liveMilestone, loadMilestone, onMilestoneChange, putMilestone, siteInfo, } from "./B08_Quantity_UI_Milestone_State"; import { editRoute, pickAtCell, routeView, trackCells, type MilestoneView, } from "./B08_Quantity_UI_Milestone_Route"; /** 단가연결 램프 칸 class(`B08_Quantity_UI_Milestone.css`) */ const LAMP = "b08-milestone__lamp"; /** 램프 호버 글 — 연결된 것 */ export function refText(ref: Record | null | undefined): string { if (!ref) return "단가연결 없음 — 줄 우클릭 [단가연결]"; const what = ref.tab === "logic" ? `단가산출 ${String(ref.code ?? "")} ${String(ref.name ?? "")}` : `일위대가 ${String(ref.name ?? ref.key ?? "")}`; return `단가연결 — ${what.trim()}`; } /** 새 경로(거래처) — 캐시에 넣고 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: "", to: "site", dir: null, legs: [{ road: null, km: null }], // 도로 구분 = 사용자 고름(244-7) 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; L1: number; L2: number }> = []; const routeAt = (i: number) => liveMilestone(projectId).routes.find((r) => r.id === keys[i]?.route); const makeTable = (columns: DataColumn[]) => { const t = createDataTable({ columns, rows: [], caption: "거리이정표", empty: "", editOnDouble: true, cellClass: (i, key) => (key !== "item" ? null : routeAt(i)?.ref ? `${LAMP} is-linked` : LAMP), 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(t.root); t.root.dataset.b08Milestone = "table"; attachHoverInfo( t.root, (cell) => refText(routeAt(Number(cell.closest("tr")?.dataset.i))?.ref), `td.${LAMP}`, ); t.root.addEventListener("contextmenu", (event) => { const tr = (event.target as Element).closest("tr[data-i]"); const route = tr ? routeAt(Number(tr.dataset.i)) : undefined; if (!route) return; event.preventDefault(); const link = (ref: Row | null, from?: Row): void => editRoute(projectId, route.id, (_, r) => { // 호버 글 이름(사전조합 · 구조물 조합 = 들여온 대가 이름) — B09 는 tab · key 만 읽음 r.ref = ref && { ...ref, name: ref.name ?? from?.name ?? "" }; }); const at = keys[Number(tr?.dataset.i)]; pickAtCell([ [ "단가연결", () => route.ref ? openHaulModal({ ref: route.ref as Row, dist: at && { L1: at.L1, L2: at.L2 }, name: route.rows[at?.index ?? 0]?.item || route.name || "운반", apply: link, }) : void openPriceLink({ apply: link }), ], ...(route.ref ? [["단가연결 풀기", () => link(null)] as [string, () => void]] : []), ]); }); return t; }; let mode = "false:false"; let table = makeTable(milestoneColumns(false, false)); const none = el("p", { className: "ui-general-block__empty", text: "경로가 없습니다 — 다이어그램 공사지 · 기준점 [+]로 항목을 넣으세요.", }); const refresh = (): void => { const data = liveMilestone(projectId); const { options, rows, mode: next } = milestoneTable(data, siteInfo()?.mid_default_m ?? null); // 기준점 생김 · 없앰 = 열 바뀜 → 표 새로 if (next !== mode) { mode = next; const fresh = makeTable(options.columns); table.root.replaceWith(fresh.root); table = fresh; } keys = rows.map(({ route, index, paved, unpaved }) => ({ route, index, L1: paved, L2: unpaved, })); none.hidden = rows.length > 0; table.setRows(options.rows.map((r) => ({ ...r, dest: `${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 ui-subtabs 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() || r.rows[0]?.item || `거래처 ${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"), ); }