/* ============================================================================= * B08_Quantity_UI_SummaryGrid.ts * 토공집계표·운반거리 그리드 (PLAN 8-11·8-3). * * 토공집계표 열은 거창 실무 시트 그대로 — 구분·공종·규격·단위·계·비고. * 비고에는 **설계자가 정한 값만** 남는다(반영률을 바꿨을 때·비율 합이 100 이 아닐 때). * 기본값 그대로면 비워 둔다 — 안내가 매번 뜨면 잡음이 된다. * * ⚠ 무대(소운반 20m)는 집계에는 오르되 **내역 줄이 아니다**(품셈 1-2-7). 그 줄에 * 「내역 제외」를 붙여 화면에서도 보이게 한다 — 규칙이 코드에만 있으면 잊힌다. * ========================================================================== */ import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale"; function L(key: keyof typeof ui_locales): string { return ui_locales[key][currentLanguageIndex]; } export interface SummaryRow { group: string; item: string; spec: string; unit: string; amount: number; note: string; in_bill: boolean; } export interface SummaryTable { columns: string[]; rows: SummaryRow[]; rock_classes: string[]; } export interface HaulRow { equipment: string; ground: string; volume_m3: number; average_distance_m: number; legs: number; in_bill: boolean; } export interface HaulTable { rows: HaulRow[]; legs: { equipment: string; ground: string; volume_m3: number; distance_m: number; from_m: number; to_m: number; }[]; bill_row_count: number; } /** 운반수단 표기 — 서버 키를 실무 시트 문구로. */ const HAUL_LABELS: Record = { free_haul: "무대(종방향유용토)", dozer: "도자운반", dump_truck: "덤프운반", }; function num(value: number | undefined, digits: number): string { if (value === undefined || value === null || Number.isNaN(value) || value === 0) return ""; return value.toLocaleString("ko-KR", { minimumFractionDigits: digits, maximumFractionDigits: digits, }); } function textCell(text: string, className?: string): HTMLTableCellElement { const td = document.createElement("td"); td.textContent = text; if (className) td.className = className; return td; } /** 토공집계표 — 실무 시트와 같은 여섯 열. */ export function renderSummaryGrid(table: SummaryTable): HTMLElement { const wrap = document.createElement("div"); wrap.className = "b08-grid"; const scroller = document.createElement("div"); scroller.className = "b08-grid__scroll"; const element = document.createElement("table"); element.className = "b08-grid__table b08-grid__table--summary"; const head = document.createElement("thead"); const headRow = document.createElement("tr"); for (const label of table.columns) { const th = document.createElement("th"); th.textContent = label; headRow.append(th); } head.append(headRow); const body = document.createElement("tbody"); let lastGroup = ""; for (const row of table.rows) { const tr = document.createElement("tr"); // 같은 구분이 이어지면 한 번만 적는다 — 실무 시트가 그렇게 병합해 둔다. tr.append(textCell(row.group === lastGroup ? "" : row.group, "b08-grid__station")); lastGroup = row.group; tr.append(textCell(row.item)); tr.append(textCell(row.spec)); tr.append(textCell(row.unit, "b08-grid__unit")); tr.append(textCell(num(row.amount, row.unit === "㎥" ? 2 : 1))); const note = textCell(row.note, "b08-grid__note"); if (!row.in_bill) { const tag = document.createElement("span"); tag.className = "b08-grid__tag"; tag.textContent = L("B08_Quantity_Haul_Excluded"); note.prepend(tag); } tr.append(note); body.append(tr); } element.append(head, body); scroller.append(element); wrap.append(scroller); return wrap; } /** 운반거리 — 내역 줄(가중평균)과 근거 줄을 나눠 보인다. */ export function renderHaulGrid(table: HaulTable, available: boolean): HTMLElement { const wrap = document.createElement("div"); wrap.className = "b08-grid"; if (!available || !table.rows.length) { const message = document.createElement("p"); message.className = "b08-quantity__message"; message.textContent = L("B08_Quantity_Haul_Missing"); wrap.append(message); return wrap; } const caption = document.createElement("p"); caption.className = "b08-grid__caption"; caption.textContent = `내역 줄 ${table.bill_row_count}개 · 근거 구간 ${table.legs.length}개 · 토량 가중평균`; wrap.append(caption); const scroller = document.createElement("div"); scroller.className = "b08-grid__scroll"; const element = document.createElement("table"); element.className = "b08-grid__table b08-grid__table--summary"; const head = document.createElement("thead"); const headRow = document.createElement("tr"); for (const label of [ "운반수단", "지반유형", "토량(㎥)", "평균운반거리(m)", "근거 구간", "비고", ]) { const th = document.createElement("th"); th.textContent = label; headRow.append(th); } head.append(headRow); const body = document.createElement("tbody"); for (const row of table.rows) { const tr = document.createElement("tr"); tr.append(textCell(HAUL_LABELS[row.equipment] ?? row.equipment, "b08-grid__station")); tr.append(textCell(row.ground)); tr.append(textCell(num(row.volume_m3, 2))); tr.append(textCell(num(row.average_distance_m, 2))); tr.append(textCell(String(row.legs))); const note = textCell("", "b08-grid__note"); if (!row.in_bill) { const tag = document.createElement("span"); tag.className = "b08-grid__tag"; tag.textContent = L("B08_Quantity_Haul_Excluded"); note.append(tag); // 왜 빠지는지 같이 적는다 — 「제외」만 있으면 빠뜨린 것으로 오해된다. note.append(document.createTextNode(" 품셈 1-2-7 소운반 20m 이내는 품에 포함")); } tr.append(note); body.append(tr); } element.append(head, body); scroller.append(element); wrap.append(scroller); return wrap; } export interface PreparationRow { group: string; item: string; unit: string; amount: number | null; status: string; reason: string; reference_amount?: number; work_item_code: string | null; } export interface PreparationTable { columns: string[]; rows: PreparationRow[]; pending_count: number; row_count: number; } /** 준비공·사방공 — **못 서는 줄도 보인다.** * * 빈 표를 내면 「빠뜨린 것」과 「원래 없는 것」이 구별되지 않는다. 그래서 값이 없는 줄도 * 상태와 사유를 달아 그대로 세운다. */ export function renderPreparationGrid(table: PreparationTable): HTMLElement { const wrap = document.createElement("div"); wrap.className = "b08-grid"; const caption = document.createElement("p"); caption.className = "b08-grid__caption"; caption.textContent = `${table.row_count}줄 · 값을 낼 근거가 아직 없는 줄 ${table.pending_count}개`; wrap.append(caption); const scroller = document.createElement("div"); scroller.className = "b08-grid__scroll"; const element = document.createElement("table"); element.className = "b08-grid__table b08-grid__table--summary"; const head = document.createElement("thead"); const headRow = document.createElement("tr"); for (const label of table.columns) { const th = document.createElement("th"); th.textContent = label; headRow.append(th); } head.append(headRow); const body = document.createElement("tbody"); let lastGroup = ""; for (const row of table.rows) { const tr = document.createElement("tr"); tr.append(textCell(row.group === lastGroup ? "" : row.group, "b08-grid__station")); lastGroup = row.group; tr.append(textCell(row.item)); tr.append(textCell(row.unit, "b08-grid__unit")); // 값이 없으면 빈칸이 아니라 「-」 — 빈칸이면 0 으로 오해된다. tr.append(textCell(row.amount === null ? "-" : num(row.amount, 2))); tr.append(textCell(row.status)); const note = textCell(row.reason.replace(/\*\*/g, ""), "b08-grid__note"); if (row.reference_amount) { note.append(document.createTextNode(` (참고 면적 ${num(row.reference_amount, 1)})`)); } tr.append(note); body.append(tr); } element.append(head, body); scroller.append(element); wrap.append(scroller); return wrap; }