- 레벨 고쳐 생긴 폴더 줄도 처음 폴더 줄과 같은 꼴(is-title · 크기 = 레벨) - 들여쓰기 한 단 22px · 끌기 좌우 한 단 = 들여쓰기 폭 - 끌기 미리 보기 = 레벨 시작 점 · 「○○ 안 · 레벨 N」 · 들어갈 폴더 줄 테두리 · 끄는 줄 흐림 - 데이터 점검 [업데이트] 위 틈 = 카드 왼쪽 여유 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LLCk2y5xE8YGVgjbTntWsY
125 lines
6.0 KiB
TypeScript
125 lines
6.0 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`(셈이 냄) → 공용 표 level 옵션(공종 · 명칭 칸 들여쓰기 · 201-1) · 다루기 = `…_Bill_Level`
|
|
* 「연결 ▾」 칸 = 항목 줄(자재 · 조달수수료 빼고) · 누름 = 우클릭 [연결] 과 같은 메뉴
|
|
* 접히지 않는 나무(235-1) = 폴더(묶음 · 자식 있는 줄) = 이름만 · 수량 · 단가 빈칸 · 금액 = 자손 합 수식(읽기 전용) ·
|
|
* 파일(항목) = 이름 · 규격 · 단위 · 수량 + 연결 · 안내선 · 아이콘 = 공용 표 level.tree · 고칠 칸 = 더블클릭 입력
|
|
* ========================================================================== */
|
|
|
|
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,
|
|
dropMove,
|
|
editCell,
|
|
linkable,
|
|
openLinkMenu,
|
|
type BillTable,
|
|
} from "./B09_Estimation_UI_Tabs_Bill_Edit";
|
|
|
|
const TEXT_KEYS = ["name", "spec", "unit"];
|
|
/** 폴더 줄 수식 칸 — 자손 합(셈) */
|
|
const SUM_KEYS = ["sum", "labor", "material", "expense"];
|
|
|
|
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:
|
|
"줄을 우클릭하면 추가 · 지움 · 옮김 · 「연결 ▾」로 일위대가 · 단가산출 연결" +
|
|
(direct.blank
|
|
? ` · 빈 줄 ${direct.blank} — 계는 아는 금액만 · 원가계산서는 빈 줄이 없을 때만 받음`
|
|
: ""),
|
|
editOnDouble: true,
|
|
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 && r.kind !== "item" && SUM_KEYS.includes(key)) return "수식 = 아래 항목 합(읽기 전용)";
|
|
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;
|
|
// 240 폴더 줄 = 처음 · 레벨 고쳐 생긴 것 모두 같은 줄 꼴(글자 크기 = 레벨 · 공용 is-lvN)
|
|
const title = r.kind === "item" ? "" : "is-title";
|
|
return [title, r.auto ? "" : "is-hand"].filter((c) => c).join(" ") || null;
|
|
},
|
|
// 228-1 레벨 = 셈 나무 깊이(들여 · 내어쓰기 · 끌기 = 다시 셈 · 다시 그림) · 글꼴 = 레벨 · 묶음(빈 공종도)
|
|
level: {
|
|
key: "name",
|
|
of: (i) => rows[i]?.level as number | undefined,
|
|
group: (i) => rows[i]?.kind !== "item",
|
|
tree: true,
|
|
},
|
|
menu: (i) => billMenu(ctx, raw, i),
|
|
drop: (i, parent, before) => dropMove(ctx, raw, i, parent, before),
|
|
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;
|
|
},
|
|
};
|
|
};
|