/* ============================================================================= * M02_MasterTemplete_Hopyo_SkLines.ts * 마법사 단가산출 블록 펼침 = 줄 표(PLAN 778-5 D-5 · 요구 623 · 624) — 서버 줄 판정(D-3) + 캐시 고름 겹침. * · 칸 = 묶음 · 이름 · 규격 · 매개값 · 상태 칩(이어짐 · 고를 것 · 못 이음 · 밖 · 뺌 — 엑셀 보기 칠과 한 규칙 * `SheetMap.unitTone`) + 꼬리표(「기본 — 건설업 작업반장」 · 「내가 고름」) · 방법 글 · 우리 금액 · 엑셀 금액 · 차(참고) * · 고름(밖 줄 빼고) = 서버 후보 단추 · [찾기](`Hopyo_Lost.lineFind` = 찾아 넣기 창) · 율 = 기준 고르기 · * 하위 · 값 = 뺌만 · [뺌] + 까닭 · [고름 지움] — 고름은 부르는 쪽 캐시(`onPick`) * · [보기] = 엑셀 보기 단가산출 상세 그 행(779-4 뛰기) * 금액 · 판정은 서버 — 고른 줄 금액은 [확인한 것 옮기기] 뒤 서버가 다시 셈(그 전 「—」). * ========================================================================== */ import { createBadge, type BadgeTone } from "@ui/ui_template_badge"; import { createButton, createInputField, createSelectField, el } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { createTable, type Cell } from "@ui/ui_template_table"; import { formatNumber, formatPrice } from "../common_util/common_util_money"; import type { HopyoScope } from "./M02_MasterTemplete_Hopyo_Api"; import { lineFind } from "./M02_MasterTemplete_Hopyo_Lost"; import { unitTone, type Tone } from "./logic/M02_MasterTemplete_Hopyo_SheetMap"; import { BASES, GROUP, candPick, candText, dropOnly, lineView, pickable, type SkBlock, type SkLine, type SkPick, } from "./logic/M02_MasterTemplete_Hopyo_SkPick"; const BADGE: Record = { success: "success", warning: "warning", info: "info", danger: "danger", muted: "neutral", }; const FIND = { L: "노임", X: "기계", M: "자재" } as const; const won = (v: number | null | undefined): string => v === null || v === undefined ? "—" : formatPrice(v); export interface SkLinesOptions { scope: HopyoScope; block: SkBlock; picks: Record; /** 고름 넣기 · 지움(null) */ onPick: (차례: number, p: SkPick | null) => void; /** 엑셀 보기 그 행 */ onSheet?: (행: number) => void; } /** 상태 칩 + 꼬리표 */ function stateCell(line: SkLine, pick: SkPick | undefined): HTMLElement { const v = lineView(line, pick); const diff = !v.mine && line.차 !== null && line.차 !== undefined && Math.abs(line.차) >= 1; const tone = v.상태 === "뺌" ? "muted" : unitTone(v.상태, diff); const chip = createBadge(v.상태, BADGE[tone]); chip.dataset.skState = v.상태; const tags = v.꼬리표.map((t) => { const tag = createBadge(t, t === "내가 고름" ? "accent" : "neutral"); tag.dataset.skTag = t; return tag; }); return el("div", { className: "ui-toolbar", children: [chip, ...tags] }); } /** 고름 칸 — 후보 · 찾기 · 율 기준 · 뺌 + 까닭 · 지움 */ function pickCell(opt: SkLinesOptions, line: SkLine, pick: SkPick | undefined): HTMLElement { const box = el("div", { className: "ui-toolbar", attrs: { "data-sk-pick": String(line.차례) } }); if (!pickable(line)) return box; const put = (p: SkPick | null): void => opt.onPick(line.차례, p); if (!dropOnly(line)) { for (const c of line.후보.slice(0, 3)) { const p = candPick(line.묶음, c); if (!p) continue; const b = createButton({ label: candText(c), size: 24, variant: "ghost", onClick: () => put(p) }); // prettier-ignore b.dataset.skCand = String(line.차례); b.title = c.키 ?? c.자재 ?? c.글 ?? ""; box.append(b); } const kind = FIND[line.묶음 as keyof typeof FIND]; if (kind) box.append(lineFind({ scope: opt.scope, 갈래: kind, 이름: line.이름 ?? "", 규격: line.규격, done: put })); // prettier-ignore if (line.묶음 === "R") { const base = createSelectField({ options: [{ value: "", text: L("M02_SkBasePick") }].concat( BASES.map((b) => ({ value: b, text: b })), ), compact: true, size: 24, ariaLabel: L("M02_SkBasePick"), onChange: (v) => v && put({ 기준: v }), }); box.append(base.root); } } const why = createInputField({ compact: true, size: 24, placeholder: L("M02_SkDropWhy") }); why.input.dataset.skWhy = String(line.차례); const drop = createButton({ label: L("M02_SkDrop"), size: 24, variant: "ghost", onClick: () => put({ 뺌: why.input.value.trim() || L("M02_SkDropDefault") }), }); drop.dataset.skDrop = String(line.차례); box.append(why.root, drop); if (pick) { const clear = createButton({ label: L("M02_SkClear"), size: 24, variant: "link", onClick: () => put(null) }); // prettier-ignore clear.dataset.skClear = String(line.차례); box.append(clear); } return box; } /** 블록 줄 표 */ export function skLineTable(opt: SkLinesOptions): HTMLElement { const rows: Cell[][] = opt.block.줄.map((line) => { const pick = opt.picks[String(line.차례)]; const v = lineView(line, pick); const param = Object.entries(line.매개값) .map(([k, n]) => `${k} ${formatNumber(n)}`) .join(" · "); const how = [line.방법, v.글].filter(Boolean).join(" · "); const sheet = line.행 !== null && opt.onSheet ? createButton({ label: L("M02_SkSheet"), size: 24, variant: "link", onClick: () => opt.onSheet!(line.행!) }) // prettier-ignore : ""; if (sheet) sheet.dataset.skSheet = String(line.행); return [ GROUP[line.묶음] ?? line.묶음, line.이름 ?? "", line.규격, param, stateCell(line, pick), how, v.mine ? "—" : won(line.우리금액), won(line.엑셀금액), v.mine || line.차 === null || line.차 === undefined ? "—" : won(line.차), pickCell(opt, line, pick), sheet, ]; }); const table = createTable( [ { label: L("M02_SkColGroup"), width: "48px" }, { label: L("M02_SangeunColName") }, { label: L("M02_LeftSpec") }, { label: L("M02_SkColParam") }, { label: L("M02_SkColState") }, { label: L("M02_SkColHow") }, { label: L("M02_SkColOurs"), align: "end" }, { label: L("M02_SkColExcel"), align: "end" }, { label: L("M02_SkColDiff"), align: "end" }, { label: L("M02_SkColPick") }, { label: L("M02_SkSheet"), width: "52px" }, ], rows, { density: "data", empty: L("M02_SkNoLines") }, ); table.dataset.skLines = String(opt.block.번); return table; }