/* ============================================================================= * M02_MasterTemplete_Hopyo_QtyLinks.ts * 가져온 식 소요량 · 계수 이음 화면(PLAN 797-2 · 요구 655-3) — `Hopyo_ImportedPanel` 한 줄의 [이음 보기] 가 엶. * · 줄마다 「소요량 후보 N · 사람 확인」(0 = 「표 칸 없음」) + 후보 칸(표 · 칸 · 값 · 단위 · 맥락) [고르기] · [버리기] * · 고른 것 = 그 줄에 M01 표 칸 참조 기록(층 · 표 · 줄 · 칸) — 기록만(금액 그대로) · 값이 다르면 서버가 거절(까닭 토스트) * · M01 칸 값이 그새 바뀌었으면 「M01 값 바뀜」 표시 · 유일해도 자동 고름 없음 * 부품 = 모달 · 표 · 배지 · 단추 · 글 · 띠(날 단추 · 입력 없음) · 서버 길은 부르는 쪽(`Hopyo_Imported`). * ========================================================================== */ import { createBadge } from "@ui/ui_template_badge"; import { createButton } from "@ui/ui_template_button"; import { el } from "@ui/ui_template_elements"; import { currentLanguageIndex } from "@ui/ui_template_locale"; import { openModal } from "@ui/ui_template_modal"; import { createTable } from "@ui/ui_template_table"; import { createText } from "@ui/ui_template_text"; import { createToolbar } from "@ui/ui_template_toolbar"; const TEXT = { Title: ["소요량 · 계수 이음", "Quantity / coefficient links"], Head: [ "엑셀 수와 같은 값의 M01 표 칸만 후보 — 고르면 기록만 남고 금액은 그대로", "Only M01 cells with the same value are candidates — picking only records the link", ], Line: ["줄", "Line"], Num: ["수", "Value"], State: ["상태", "State"], Cells: ["후보 칸", "Candidate cells"], Cand: ["소요량 후보 {n} · 사람 확인", "{n} candidates · confirm"], Empty: ["표 칸 없음", "No table cell"], Unique: ["후보 유일", "One candidate"], Many: ["후보 여럿", "Several candidates"], Picked: ["고름", "Picked"], Dropped: ["버림", "Dropped"], Pick: ["고르기", "Pick"], Drop: ["버리기", "Drop"], Undo: ["되돌리기", "Undo"], Free: ["풀기", "Release"], Changed: ["M01 값 바뀜 {v}", "M01 value changed {v}"], More: ["외 {n}", "+{n} more"], Close: ["닫기", "Close"], None: ["가져온 식에 소요량 · 계수 수가 없음", "No quantity / coefficient values"], Ctx: ["공종", "Work"], } as const satisfies Record; const tx = (k: keyof typeof TEXT): string => TEXT[k][currentLanguageIndex as 0 | 1] ?? TEXT[k][0]; export interface QtyCell { 표: string; 줄: number; 칸: string; 값: number; 단위: string; 꼴: "eq" | "pct" | "kgt"; 맥락: string; 이름?: string; 번호?: string; 갈래: "소요량" | "계수"; } export interface QtyPicked { 표: string; 줄: number; 칸: string; 값: number; 지금?: number | null; 같음?: boolean; } export interface QtyRow { 자리: string; 줄: string; 단위: string; 수: number; 후보수: number; 후보: QtyCell[]; 고른: QtyPicked | null; 상태: "후보" | "없음" | "고름" | "버림"; } export interface QtyTally { 자리: number; 줄: number; 유일: number; 여럿: number; 없음: number; 고름: number; 버림: number; } export interface QtyView { 줄: QtyRow[]; 집계: QtyTally; } export interface QtyApi { view: () => Promise; pick: (spot: string, cell: QtyCell) => Promise; drop: (spot: string, undo: boolean) => Promise; } const muted = (s: string): HTMLElement => createText(s, "muted", 14); /** 집계 배지 띠(패널 줄 · 모달 머리 공용) */ export function tallyBadges(t: QtyTally): HTMLElement[] { return [ createBadge(`${tx("Unique")} ${t.유일}`, t.유일 ? "warning" : "neutral"), createBadge(`${tx("Many")} ${t.여럿}`, t.여럿 ? "warning" : "neutral"), createBadge(`${tx("Empty")} ${t.없음}`, "neutral"), createBadge(`${tx("Picked")} ${t.고름}`, t.고름 ? "success" : "neutral"), ]; } function cellRow(c: QtyCell, spot: string, locked: boolean, go: () => void): HTMLElement { const pick = createButton({ label: tx("Pick"), size: 24, variant: "ghost", disabled: locked, onClick: go }); // prettier-ignore pick.dataset.qtyPick = `${spot}|${c.표}|${c.줄}|${c.칸}`; return el("div", { className: "ui-toolbar m02-judge__wrap", attrs: { "data-qty-cand": c.표 }, children: [ createBadge(`${c.표} ${c.이름 ?? ""}`.trim(), c.갈래 === "소요량" ? "accent" : "info"), muted([c.번호, `${c.칸} ${c.값}${c.단위 ? ` ${c.단위}` : ""}`].filter(Boolean).join(" · ")), createBadge(c.맥락 === "공종" ? tx("Ctx") : c.맥락, "neutral"), pick, ], }); } /** 모달 — 자리마다 후보 · 고름 · 버림 */ export function openQtyLinks(api: QtyApi, readOnly: boolean): void { const root = el("div", { attrs: { "data-qty-links": "" } }); let busy = false; const act = (job: () => Promise): void => { if (busy) return; busy = true; void job() .catch(() => {}) .finally(() => { busy = false; void draw(); }); }; function state(r: QtyRow): HTMLElement { if (r.상태 === "고름" && r.고른) { const bad = r.고른.같음 === false; return el("div", { className: "ui-toolbar m02-judge__wrap", children: [ createBadge(`${tx("Picked")} ${r.고른.표} ${r.고른.칸} ${r.고른.값}`, "success"), ...(bad ? [createBadge(tx("Changed").replace("{v}", String(r.고른.지금 ?? "—")), "warning")] : []), // prettier-ignore ], }); } if (r.상태 === "버림") return createBadge(tx("Dropped"), "neutral"); if (r.상태 === "없음") return createBadge(tx("Empty"), "neutral"); return createBadge(tx("Cand").replace("{n}", String(r.후보수)), "warning"); } function cells(r: QtyRow): HTMLElement { const locked = readOnly || busy; const list = r.상태 === "후보" || r.상태 === "고름" ? r.후보 : []; const more = r.후보수 - r.후보.length; return el("div", { children: [ ...list.map((c) => cellRow(c, r.자리, locked, () => act(() => api.pick(r.자리, c)))), ...(more > 0 && list.length ? [muted(tx("More").replace("{n}", String(more)))] : []), ], }); } function side(r: QtyRow): HTMLElement | string { if (r.상태 === "없음") return ""; const undo = r.상태 !== "후보"; // 버림 → 되돌리기 · 고름 → 풀기(후보로) · 후보 → 버리기 const label = r.상태 === "버림" ? tx("Undo") : r.상태 === "고름" ? tx("Free") : tx("Drop"); const b = createButton({ label, size: 24, variant: "ghost", disabled: readOnly || busy, onClick: () => act(() => api.drop(r.자리, undo)) }); // prettier-ignore b.dataset.qtyDrop = r.자리; return b; } async function draw(): Promise { const got = await api.view().catch(() => null); if (!got) return; const rank = { 고름: 0, 후보: 1, 버림: 2, 없음: 3 }; const sorted = [...got.줄].sort((a, b) => rank[a.상태] - rank[b.상태]); // 사람이 볼 것 먼저(안정 정렬) const rows = sorted.map((r) => [`${r.줄}`, `${r.수}${r.단위 ? ` ${r.단위}` : ""}`, state(r), cells(r), side(r)]); // prettier-ignore const heads = [tx("Line"), tx("Num"), tx("State"), tx("Cells"), ""]; const table = createTable(heads.map((h, i) => ({ label: h, ...(i === 1 ? { align: "end" as const } : {}), ...(i === 4 ? { width: "84px" } : {}) })), rows, { density: "data" }); // prettier-ignore table.dataset.qtyTable = String(got.줄.length); root.replaceChildren( createToolbar({ items: [muted(tx("Head")), ...tallyBadges(got.집계)] }), got.줄.length ? table : createText(tx("None"), "muted", 14), ); } openModal({ title: tx("Title"), closeLabel: tx("Close"), wide: true, dialogClass: "m02-modal m02-hopyo__qty", mount: (body) => body.append(root), }); void draw(); }