Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SE9MxeXTu2uaDZ55BJowAY
109 lines
5.4 KiB
TypeScript
109 lines
5.4 KiB
TypeScript
/* =============================================================================
|
||
* 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;
|
||
},
|
||
};
|
||
};
|