/* ============================================================================= * M02_MasterTemplete_Hopyo_WizardSk.ts * 일위대가 불러오기 마법사 ② 단가산출 단계(PLAN 729-3 · 요구서 내역19 1장 7 · 2장 3) — * · [단가산출 만들기] = 서버 `sangeun`(산근 블록마다 매칭 → 같으면 기초 · 다르면 SK = 기초 + 엑셀 차이 · 맞는 기초 없음 = 고름 대기) * · 만든 줄 표 = 블록 · 이름 · 키 · 갈래(기초 · SK 차이 있음) · 이미 만든 블록은 「그대로」 수만 * · 고름 대기 표 = 블록마다 후보(판정 · 글) 고르기 칸 → [고른 기초로 만들기](바탕 = 고른 기초 · 엑셀 품 견줘 SK) · * 단위 성격 다른 후보(729-4 `계수`) = 계수 칸(가 1 = 나 몇) 함께 → `환산 {계수}` * · 못옮김(729-4) = 만든 줄마다 수 · [잇기] = 못옮김 잇기 창(`Hopyo_Lost`) · 만든 줄은 이 칸이 모아 둠(누를 때마다 더함) · * 이미 만든 SK 의 남은 못옮김(729-5 `남은못옮김`)도 같은 표 * · 같은 내용(733-1) = 갈래 칸 꼬리표 「같은 것 씀」(이 층 같은 SK 를 씀) · 「위 층에 같은 것 — 연결 권함」(회사 · 마스터 · 알림만) · * 수는 요약 글에 * · 별칭 제안 = 이 층 별칭 장부에 남음 · 모두에게 쓰이려면 시스템 관리자가 M01 › 별칭 장부에서 승인(링크 · 716 길 그대로) * 계산 · 판정은 서버 — 화면은 보이기 · 고름만 · 자동 저장 없음(누를 때만 서버). * ========================================================================== */ import { ROUTES } from "@config/config_frontend"; import { createBadge } from "@ui/ui_template_badge"; import { createButton, createInputField, createSelectField, el, showToast, } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { createTable } from "@ui/ui_template_table"; import { createText } from "@ui/ui_template_text"; import { makeSangeun, type HopyoScope, type SangeunBuilt, type SangeunPick, } from "./M02_MasterTemplete_Hopyo_Api"; import { openLost } from "./M02_MasterTemplete_Hopyo_Lost"; type Made = SangeunBuilt["만듦"][number]; const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); /** 고름 대기 후보 글 — 「이름 [단위] · 판정 · 글」 */ export const candLabel = (c: { 이름: string; 단위: string; 판정: string; 글: string }): string => `${c.이름} [${c.단위}] · ${c.판정}${c.글 && c.글 !== c.판정 ? ` · ${c.글}` : ""}`; /** 만든 갈래 꼬리표 — 기초 = 초록 · SK = 차이 있음 */ const kindBadge = (kind: string): HTMLElement => kind === "SK" ? createBadge(L("M02_WizSkDiff"), "accent") : createBadge(L("M02_WizSkBase"), "success"); /** ② 단가산출 단계 칸 — `root` 를 마법사 본 칸에 붙임 */ export function sangeunStep(scope: HopyoScope, key: string): { root: HTMLElement } { const out = el("div", { className: "m02-hopyo", attrs: { "data-wiz-sk": "" } }); const make = createButton({ label: L("M02_WizSkMake"), variant: "filled", onClick: () => void run(), }); make.title = L("M02_WizSkMakeTip"); make.dataset.hopyoSangeunMake = ""; const root = el("div", { className: "m02-hopyo", children: [el("div", { className: "ui-toolbar", children: [make] }), out], }); const made = new Map(); // 이 칸에서 만든 줄(누를 때마다 더함) const reused = new Set(); // 733-1 같은 것 씀 블록 const above = new Map(); // 733-1 위 층 같은 것 「층 키」 async function run(picks?: SangeunPick): Promise { make.disabled = true; try { const got = await makeSangeun(scope, key, picks); draw(got); showToast(summary(got), got.고름대기.length ? "warning" : "success"); } catch (error) { showToast(why(error), "error"); } finally { make.disabled = false; } } const summary = (got: SangeunBuilt): string => L("M02_WizSkSummary") .replace("{b}", String(got.블록)) .replace("{base}", String(got.기초)) .replace("{sk}", String(got.SK)) .replace("{keep}", String(got.그대로)) .replace("{wait}", String(got.고름대기.length)) + (got.같은것씀?.length || got.위층같음?.length ? L("M02_WizSkSameSum") .replace("{same}", String(got.같은것씀?.length ?? 0)) .replace("{above}", String(new Set(got.위층같음?.map((x) => x.번)).size)) : ""); /** 갈래 칸 — 갈래 + 같은 것 씀 · 위 층 같은 것 꼬리표 */ const kindCell = (x: Made): HTMLElement => { const tags: HTMLElement[] = [kindBadge(x.갈래)]; if (reused.has(x.번)) { const tag = createBadge(L("M02_WizSkSame"), "info"); tag.title = L("M02_WizSkSameTip"); tag.dataset.wizSame = String(x.번); tags.push(tag); } const up = above.get(x.번); if (up?.length) { const tag = createBadge(L("M02_WizSkAbove"), "warning"); tag.title = up.join(" · "); tag.dataset.wizAbove = String(x.번); tags.push(tag); } return el("div", { className: "ui-toolbar", children: tags }); }; /** 못옮김 칸 — 수 + [잇기](남은 것 있을 때) */ const lostCell = (x: Made, got: SangeunBuilt): HTMLElement => { const n = x.못옮김?.length ?? 0; if (!n) return el("span", { text: "0" }); const go = createButton({ label: `${L("M02_LostOpen")} (${n})`, size: 24, onClick: () => openLost({ scope, key, 번: x.번, sk: x.키, 이름: x.이름, lost: x.못옮김 ?? [], done: (next) => draw({ ...got, 만듦: next.만듦, 별칭: got.별칭 }) }), // prettier-ignore }); go.dataset.wizLost = String(x.번); return go; }; function draw(got: SangeunBuilt): void { for (const x of got.남은못옮김 ?? []) if (!made.has(x.번)) made.set(x.번, { ...x, 갈래: "SK" }); for (const x of got.만듦) made.set(x.번, x); for (const x of got.같은것씀 ?? []) reused.add(x.번); for (const x of got.위층같음 ?? []) { const at = x.층 === "system" ? L("M02_PickerSystem") : L("M02_PickerCompany"); above.set(x.번, [...(above.get(x.번) ?? []), `${at} ${x.키}`]); } const table = createTable( [ { label: L("M02_WizSkBlock"), width: "80px" }, { label: L("M02_SangeunColName") }, { label: L("M02_WizSkKey"), width: "110px" }, { label: L("M02_WizSkKind"), width: "260px" }, { label: L("M02_LostCol"), width: "120px" }, ], [...made.values()] .sort((a, b) => a.번 - b.번) .map((x) => [`단산${x.번}`, x.이름, x.키, kindCell(x), lostCell(x, got)]), { density: "data", empty: L("M02_WizSkNoneMade") }, ); const selects = new Map(); const coefs = new Map(); const wait = createTable( [ { label: L("M02_WizSkBlock"), width: "80px" }, { label: L("M02_SangeunColName") }, { label: L("M02_SangeunColUnit"), width: "64px" }, { label: L("M02_LeftWhy") }, { label: L("M02_LeftBase"), width: "40%" }, ], got.고름대기.map((x) => { const pick = createSelectField({ options: [{ value: "", text: L("M02_WizSkNoPick") }].concat( x.후보.map((c) => ({ value: c.로직, text: candLabel(c) })), ), compact: true, size: 24, ariaLabel: `${L("M02_LeftBase")} 단산${x.번}`, }); pick.select.dataset.wizWait = String(x.번); selects.set(x.번, pick.select); const coef = createInputField({ type: "number", min: 0, compact: true, size: 24, placeholder: L("M02_WizSkCoef") }); // prettier-ignore coef.input.dataset.wizCoef = String(x.번); const need = (): void => { const c = x.후보.find((y) => y.로직 === pick.select.value)?.계수; coef.root.hidden = !c; coef.input.title = c ? `${c.가} 1 = ${c.나} ?` : ""; }; pick.select.addEventListener("change", need); need(); coefs.set(x.번, coef.input); return [`단산${x.번}`, x.이름, x.단위 ?? "", x.까닭, el("div", { className: "ui-toolbar", children: [pick.root, coef.root] })]; // prettier-ignore }), { density: "data", empty: L("M02_WizSkNoWait") }, ); const again = createButton({ label: L("M02_WizSkRemake"), variant: "filled", size: 28, onClick: () => { const picks: SangeunPick = {}; for (const [no, s] of selects) { if (!s.value) continue; const c = Number(coefs.get(no)?.value); const need = got.고름대기.find((x) => x.번 === no)?.후보.find((y) => y.로직 === s.value)?.계수; // prettier-ignore if (need && !(c > 0)) return showToast(L("M02_WizSkCoefNeed").replace("{n}", String(no)), "warning"); // prettier-ignore picks[no] = { 로직: s.value, ...(need ? { 환산: { 계수: c } } : {}) }; } if (!Object.keys(picks).length) return showToast(L("M02_WizSkPickFirst"), "warning"); void run(picks); }, }); again.dataset.wizRemake = ""; const ledger = createButton({ label: L("M02_WizSkLedger"), variant: "link", onClick: () => void window.open(`#/${ROUTES.M01_MASTER_DATA}`, "_blank"), }); out.replaceChildren( createText(summary(got), "muted", 14), table, ...(got.고름대기.length ? [ createText(L("M02_WizSkWaitHint"), "hint", 14), wait, el("div", { className: "ui-toolbar", children: [again] }), ] : []), el("div", { className: "ui-toolbar", children: [ createText(L("M02_WizSkAlias").replace("{n}", String(got.별칭 ?? 0)), "muted", 14), ledger, ], }), ); } return { root }; }