/* ============================================================================= * B09_Estimation_UI_Detail.ts * 호표 본표(일위대가표·단가산출근거·시간당 중기) 한 장 + **들어가기 자취** (PLAN 12장 1차) * * - ⭐ STmate 를 익숙하게 만드는 동작 = 타고 들어가기(브레인 판정). 내역 줄 → 제 N 호표 → * 호표 안 줄(일위대가·단산·중기) → … 을 누를 때마다 자취가 쌓이고, 자취 글을 누르면 그 자리로 되돌아감. * - 표 모양은 실무 `일위대가표`·`단가산출근거` 시트: 명칭·규격·수량·단위 · 합계/노무/재료/경비(단가·금액) · 비고. * - ⚠ 합계 줄은 서버 값(호표 성분 소계 원 미만 절사) — 여기서 안 더함. * ========================================================================== */ import type { B09TabContext } from "./B09_Estimation_UI_Shell_Types"; import { L, el, hint, linkButton, moneyCells, numberCell, quantity, sheetHead, sheetTable, } from "./B09_Estimation_UI_Sheet"; import { loadDetail, type DetailDto, type DetailRowDto } from "./B09_Estimation_UI_Store"; import { addRowForm, qFormulaInput, quantityInput, rowControls, } from "./B09_Estimation_UI_DetailEdit"; interface Crumb { tab: string; code: string; label: string; } const crumbs: Crumb[] = []; let drilling = false; /** 본표 안 줄을 눌러 들어갈 때 — 자취를 이어 쌓음. 목록·내역에서 들어가면 자취가 새로 시작. */ export function drill(ctx: B09TabContext, tab: string, code: string): void { drilling = true; ctx.open(tab, code); } function visit(tab: string, code: string, label: string): void { if (!drilling) crumbs.length = 0; drilling = false; const at = crumbs.findIndex((crumb) => crumb.tab === tab && crumb.code === code); if (at >= 0) crumbs.length = at; crumbs.push({ tab, code, label }); } function trail(ctx: B09TabContext): HTMLElement { const box = el("div", "b09s-trail"); crumbs.forEach((crumb, index) => { if (index > 0) box.append(el("span", "b09s-hint", "›")); if (index === crumbs.length - 1) { box.append(el("span", "b09s-title", crumb.label)); return; } box.append(linkButton(crumb.label, () => drill(ctx, crumb.tab, crumb.code))); }); return box; } /** 줄이 가리키는 곳 — 단산(D)은 단가산출근거 탭, 시간당 중기(X)는 중기 탭, 일위대가는 일위대가 탭. */ function targetTab(row: DetailRowDto): string | null { if (!row.drillable || !row.ref_code) return null; if (row.kind === "price_basis") return "price_basis"; return row.kind === "machine_hourly" ? "machine" : "unit_price"; } /** 편집 칸을 켰는가 — 탭을 오가도 남음(본표 한 장 단위가 아니라 화면 단위). */ let editing = false; interface EditHooks { projectId: string; reload: () => void; } function detailRow(row: DetailRowDto, ctx: B09TabContext, hooks: EditHooks | null): HTMLElement { const tr = el( "tr", row.edit?.removed ? "is-removed" : row.edit?.user || row.edit?.q_formula ? "is-user" : "", ); const edit = row.edit; const quantityCell = hooks && edit && editing && row.unit !== "%" ? el("td") : numberCell(quantity(row.quantity)); if (hooks && edit && editing && row.unit !== "%") { quantityCell.append(quantityInput(hooks.projectId, edit, row.quantity, hooks.reload)); } // 비율 줄(잡품·공구손료·제잡비)은 단가 칸에 **밑수**가 옴 — 「21 % × 주연료비 6,365」 꼴(실무 표). const unit: [string, string, string, string] = [ row.unit_total ?? "", row.unit_labor ?? "", row.unit_material ?? "", row.unit_expense ?? "", ]; tr.append( el("td", "", row.name), el("td", "", row.spec), quantityCell, el("td", "", row.unit), ...moneyCells(unit, [row.total, row.labor, row.material, row.expense]), ); const note = el("td", "b09s-note"); if (hooks && edit && (editing || edit.user)) { note.append(rowControls(hooks.projectId, edit, hooks.reload, editing)); } if (hooks && edit?.output && (editing || edit.q_formula)) { note.append(qFormulaInput(hooks.projectId, edit, hooks.reload)); } const source = row.source_label || row.source || ""; if (source) note.append(el("span", "b09s-hint", `[${source}] `)); if (row.note) note.append(el("span", "b09s-formula", row.note)); tr.append(note); const tab = targetTab(row); if (tab && row.ref_code) { const code = row.ref_code; tr.classList.add("is-clickable"); tr.title = L("B09_Sheet_Drill"); tr.addEventListener("click", () => drill(ctx, tab, code)); } return tr; } function drawDetail( ctx: B09TabContext, box: HTMLElement, detail: DetailDto, label: string, reload: () => void, ): void { const title = el( "div", "b09s-title b09s-inline", `${label} ${detail.name}${detail.spec ? ` · ${detail.spec}` : ""}`, ); if (detail.unit) title.append(el("span", "b09s-hint", ` (${detail.unit})`)); const hooks = detail.editable && ctx.projectId ? { projectId: ctx.projectId, reload } : null; if (hooks) { // 2차 편집(PLAN 12장) — 구성행 수정 · Q 식 편집. 프로젝트 단위로 저장, 금액은 서버가 다시 셈. const toggle = el( "button", "b09s-undo", editing ? L("B09_Sheet_EditDone") : L("B09_Sheet_Edit"), ); toggle.type = "button"; toggle.addEventListener("click", () => { editing = !editing; reload(); }); title.append(toggle); } const { wrap, tbody } = sheetTable( sheetHead([ L("B09_Sheet_Col_Name"), L("B09_Sheet_Col_Spec"), L("B09_Sheet_Col_Quantity"), L("B09_Sheet_Col_Unit"), ]), ); for (const row of detail.rows) tbody.append(detailRow(row, ctx, hooks)); const sum = el("tr", "is-sum"); sum.append(el("td", "", L("B09_Sheet_Sum")), el("td"), el("td"), el("td")); sum.append( ...moneyCells(null, [detail.total, detail.labor, detail.material, detail.expense]), el("td"), ); tbody.append(sum); box.append(title, wrap); if (hooks && editing && detail.next_add_key) { box.append(addRowForm(hooks.projectId, detail.code, detail.next_add_key, reload)); } if (detail.unattached_note) box.append(hint(detail.unattached_note.replace(/\*\*/g, ""), true)); if (detail.known_gap_note) box.append(hint(detail.known_gap_note, true)); } /** 본표 한 장을 `box` 에 — 자취를 쌓고 서버 본표를 받아 그림. */ export function renderDetail( ctx: B09TabContext, box: HTMLElement, tab: string, code: string, label: string, ): void { if (!ctx.projectId) return; visit(tab, code, label); const projectId = ctx.projectId; const load = (): void => { box.replaceChildren(trail(ctx), hint(L("B09_Sheet_Loading"))); loadDetail(projectId, code) .then((detail) => { box.replaceChildren(trail(ctx)); drawDetail(ctx, box, detail, label, load); }) .catch((error: Error) => { box.replaceChildren( trail(ctx), hint(`${L("B09_Sheet_LoadFailed")} ${error.message}`, true), ); }); }; load(); }