/* ============================================================================= * B09_Estimation_UI_Tabs_Bill.ts * B09 설계내역서 탭(PLAN 145-3 · 184-3) — 실무 엑셀 꼴 칸(공종 · 명칭 · 규격 · 수량 · 단위 · 합계 · 노무 · 재료 · 경비 · 비고). * * 값 = `B09_Estimation_Calc_Bill`(조작 중 브라우저 · [저장] 때 서버 정본) * 초기값 = 기본 로직이 B08 수량으로 짠 줄 · 모든 줄 고침 · 더함 · 지움 · 옮김 자유(184 · `…_UI_Tabs_Bill_Edit`) * 고칠 칸 = 항목 줄 명칭 · 규격 · 단위 · 수량 · 묶음 줄 이름 · 조달수수료 율(%) — 자동 줄은 덮개 * 고친 칸 = 칸 강조(셈 줄 `auto_{칸}` 있음) · 풍선 = 자동 값 * 줄 누름 = 단가를 낸 탭 줄(자재 → 재료비목록표 · 구조물 → 일위대가목록표) * 레벨 = 줄 `level`(셈이 냄) → class is-lvN · 글자 크기 · 첫 칸 들여쓰기(CSS) * 「연결 ▾」 칸 = 항목 줄(자재 · 조달수수료 빼고) · 누름 = 우클릭 [연결] 과 같은 메뉴 * ========================================================================== */ import type { DataRow } from "@ui/ui_template_data_table"; import type { Row } from "./B09_Estimation_Calc_Base"; import { money, parseNumber, qty, text, type TabBuild, type TabId } from "./B09_Estimation_UI_Tabs"; import { billMenu, editCell, linkable, openLinkMenu, type BillTable, } from "./B09_Estimation_UI_Tabs_Bill_Edit"; const TEXT_KEYS = ["name", "spec", "unit"]; export const buildBill: TabBuild = (ctx): BillTable => { const { sheets, user, setUser } = ctx; const bill: Row = sheets.bill || { rows: [], totals: null }; // 손질 · 메뉴 = 셈 줄 그대로(raw) · 표 = 번호 붙인 이름(no = 레벨 1 · 2 묶음 차례) const raw: DataRow[] = bill.rows; const rows: DataRow[] = raw.map((r: Row) => ({ ...r, name: r.no ? `${r.no} ${r.name}` : r.name, connect: linkable(r) ? `${!r.auto && r.note ? r.note : r.ref ? "연결됨" : "연결"} ▾` : "", })); const direct: Row = bill.totals?.direct || {}; const pair = (key: string, label: string) => [ money(`price_${key}`, "단가", [label]), money(key, "금액", [label]), ]; /** 고친 칸 — 셈 줄에 자동 값(auto_칸)이 있음 · 연결 칸 = auto_ref */ const edited = (r: DataRow | undefined, key: string): boolean => !!r && `auto_${key === "connect" ? "ref" : key}` in r; return { columns: [ text("name", "공종 · 명칭", 220), text("spec", "규격", 160), qty("qty", "수량", 3), text("unit", "단위", 56), ...pair("sum", "합계"), ...pair("labor", "노무비"), ...pair("material", "재료비"), ...pair("expense", "경비"), text("connect", "연결", 96), text("note", "비고", 200), ], rows, total: { name: "순공사비 계(관급 제외)", ...direct }, frozen: 1, hint: "기본 로직이 짠 내역 = 초기값 · 모든 줄 고침 · 더함 · 지움 · 옮김 자유(우클릭 · 고친 칸 = 색 칸 · 풍선 = 자동 값) · " + "금액 = ROUNDDOWN(단가 × 수량, 0) 비목마다 · 묶음 금액 = 아래 항목 합 · 4 자재대 = 대비표 「별도」 · 관급자재대 = 「관급」 표시 자재(관급자재대 밖으로 못 옮김) · " + "조달수수료 율(%)은 수량 칸 · 「연결 ▾」 = 일위대가 · 단가산출 연결(새 산근 단가 = [저장] 뒤) · 처음으로 = 왼쪽 [내역 초기화] · " + `빈 줄 ${direct.blank ?? 0} — 계는 아는 금액만 · 원가계산서는 빈 줄이 없을 때만 받음`, editable: (i, key) => { const r = rows[i]; if (!r) return false; if (r.key === "fee") return key === "qty"; if (r.kind !== "item") return key === "name"; return key === "qty" || TEXT_KEYS.includes(key); }, onEdit: (i, key, value) => { const r = rows[i]; if (!r) return; // 번호 = 셈이 붙임 — 고친 이름 앞 번호는 뗌 const head = `${r.no} `; if (key === "name" && r.no && value.startsWith(head)) value = value.slice(head.length); if (key !== "qty") return editCell(ctx, raw, i, key, value.trim()); const v = parseNumber(value); if (v === undefined) return; if (r.key !== "fee") return editCell(ctx, raw, i, key, v); setUser({ ...user, estimate: { ...(user.estimate || {}), procurement_fee_pct: v } }); }, cellClass: (i, key) => (edited(rows[i], key) ? "is-edited" : null), tip: (i, key) => { const r = rows[i]; if (!r || !edited(r, key)) return null; const auto = key === "connect" ? (r.auto_ref as Row | null)?.key : r[`auto_${key}`]; return `자동 값 ${auto ?? "빈칸"} — 우클릭 [자동값으로 되돌리기]`; }, rowClass: (i) => { const r = rows[i]; if (!r) return null; const title = r.kind === "title" ? "is-title" : r.kind === "group" ? "is-group" : ""; return [`is-lv${r.level ?? 1}`, title, r.auto ? "" : "is-hand"].filter((c) => c).join(" "); }, menu: (i) => billMenu(ctx, raw, i), onCell: (i, key) => { if (key === "connect") openLinkMenu(ctx, raw, i); }, link: (i, key) => { if (key === "connect") return null; // 「연결 ▾」 = 메뉴(이동 안 함) const ref = rows[i]?.ref as Row | null | undefined; return ref?.key ? { tab: ref.tab as TabId, key: String(ref.key) } : null; }, }; };