/* ============================================================================= * M02_MasterTemplete_Labor_Input.ts * 일위대가 입력 모달(39-3) — 담은 로직 줄을 누르면 그 로직 입력 전부(빈 칸 = 자동값) · 메모 · 수량 * (면적산출 이은 칸 · 값 보임 / 손 입력 / 「면적산출에서 잇기」). 자재 줄은 수량만(`openQtyModal`). * [확인] = 줄 값만 바꿈(화면 캐시) · 저장은 상세 [저장]. 값 칸 규칙 = `M02_Labor_Values`(한 벌). * ========================================================================== */ import { createButton, el, showToast } from "@ui/ui_template_elements"; import { openModal } from "@ui/ui_template_modal"; import type { ItemInput, ItemLogic } from "./M02_MasterTemplete_Api_Fetch"; import type { LaborQty } from "./M02_MasterTemplete_Labor_Api"; import { tc } from "./M02_MasterTemplete_Labor_Text"; import { createValueControl } from "./M02_Labor_Values"; import { keepKnown, valueHint, type LaborValues } from "./M02_Labor_Values_Model"; import "./M02_Labor_Values.css"; import "./M02_MasterTemplete_Labor.css"; const fill = (text: string, value: string): string => text.replace("{value}", value); export interface QtySeat { /** 지금 수량 */ qty: LaborQty; /** 면적산출 이은 칸 글(「B6 = 12.5 ㎡」) — 없으면 null */ linked: string | null; /** 손 입력 단위 표기(분모 · 자재 단위) */ unit: string; } interface QtyPart { root: HTMLElement; /** 확인 때 값 — 손 입력이 수가 아니면 undefined(막음) */ read: () => LaborQty | undefined; } /** 수량 칸 — 「면적산출 칸」 / 「손 입력」 · 손이 칸보다 앞섬 */ function qtyPart(seat: QtySeat, readOnly: boolean, goArea: (() => void) | null): QtyPart { const name = `m02-labin-qty-${Math.random().toString(36).slice(2)}`; const radio = (value: "cell" | "hand"): HTMLInputElement => { const input = el("input", { attrs: { type: "radio", name, value } }) as HTMLInputElement; input.disabled = readOnly; return input; }; const cell = radio("cell"); const hand = radio("hand"); const number = el("input", { className: "ui-input m02-inputval__control", attrs: { type: "number", step: "any", "data-labor-qty": "hand" }, }) as HTMLInputElement; number.readOnly = readOnly; if (seat.qty) number.value = String(seat.qty.손); (seat.qty ? hand : cell).checked = true; number.disabled = !hand.checked; for (const r of [cell, hand]) r.addEventListener("change", () => { number.disabled = !hand.checked; if (hand.checked) number.focus(); }); const go = goArea ? createButton({ label: tc("In_QtyGoArea"), variant: "ghost", size: "sm", onClick: goArea }) : null; const line = (input: HTMLInputElement, label: string, ...rest: HTMLElement[]): HTMLElement => el("label", { className: "m02-labin__qty-line", children: [input, el("span", { text: label }), ...rest], }); const root = el("section", { className: "m02-labin__qty", children: [ el("h4", { className: "m02-inputval__logic-name", text: tc("In_Qty") }), line( cell, tc("In_QtyCell"), el("small", { className: "m02-labin__muted", text: seat.linked ? fill(tc("In_QtyLinked"), seat.linked) : tc("In_QtyNoLink"), }), ...(go ? [go] : []), ), line(hand, tc("In_QtyHand"), number, el("small", { text: seat.unit })), ], }); return { root, read: () => { if (!hand.checked) return null; const v = Number(number.value); return number.value.trim() !== "" && Number.isFinite(v) ? { 손: v } : undefined; }, }; } function footer(readOnly: boolean, ok: () => void, close: () => void): HTMLElement { return el("div", { className: "m02-labin__foot", children: readOnly ? [createButton({ label: tc("In_Close"), variant: "ghost", onClick: close })] : [ createButton({ label: tc("In_Cancel"), variant: "ghost", onClick: close }), createButton({ label: tc("In_Ok"), variant: "filled", onClick: ok }), ], }); } export interface LaborInputResult { 입력값: LaborValues; 메모: string | null; 수량: LaborQty; } export interface LaborInputOptions { /** 「GF000219 메쌓기」 */ title: string; /** 로직 입력 목록(없으면 = 못 읽음 · 넣을 값 없이 수량만) */ logic: ItemLogic | null; values: LaborValues; memo: string | null; seat: QtySeat; readOnly: boolean; /** 「면적산출에서 잇기」 — 모달을 닫고 면적산출 상세로(없으면 단추 없음) */ goArea: (() => void) | null; done: (next: LaborInputResult) => void; } /** 담은 로직 줄 입력 모달 */ export function openLaborInput(opt: LaborInputOptions): void { const inputs: ItemInput[] = opt.logic?.입력 ?? []; const { kept, lost } = keepKnown(inputs, opt.values); const draft: LaborValues = { ...kept }; const bad = new Set(); openModal({ title: fill(tc("In_Title"), opt.title), closeLabel: tc("In_Close"), dialogClass: "m02-modal m02-labin", mount: (body, close) => { const rows = inputs.map((input) => { const field = createValueControl(input, draft[input.이름] ?? null, (value, isBad) => { if (isBad) bad.add(input.이름); else bad.delete(input.이름); if (value === null) delete draft[input.이름]; // 고르기 · 범위 없는 칸도 수로 읽히면 수로 둠(M01 넣을 값과 같게) else draft[input.이름] = typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value)) ? Number(value) : value; }); field.control.disabled = opt.readOnly; field.control.setAttribute("data-labor-input", input.이름); const hint = valueHint(input); return el("div", { className: "m02-inputval__row", children: [ el("span", { className: "m02-inputval__name", text: input.단위 ? `${input.이름} (${input.단위})` : input.이름, attrs: { title: [input.설명, hint].filter(Boolean).join(" · ") }, }), el("div", { className: "m02-inputval__cell", children: [field.control, field.error] }), ], }); }); const memo = el("input", { className: "ui-input m02-inputval__control", attrs: { type: "text", maxlength: "200", "data-labor-memo": "" }, }) as HTMLInputElement; memo.value = opt.memo ?? ""; memo.readOnly = opt.readOnly; const qty = qtyPart( opt.seat, opt.readOnly, opt.goArea && (() => { close(); opt.goArea?.(); }), ); const ok = (): void => { if (bad.size) return showToast(tc("In_Bad"), "error"); const 수량 = qty.read(); if (수량 === undefined) return showToast(tc("In_QtyNeed"), "error"); opt.done({ 입력값: { ...draft }, 메모: memo.value.trim() || null, 수량 }); close(); }; body.append( el("div", { className: "m02-inputval", children: [ ...(lost.length ? [ el("p", { className: "m02-labin__lost", text: fill(tc("Labor_Lost"), lost.join(", ")), }), ] : []), ...(rows.length ? rows : [el("p", { className: "m02-labin__muted", text: tc("In_None") })]), el("div", { className: "m02-inputval__row", children: [ el("span", { className: "m02-inputval__name", text: tc("In_Memo") }), memo, ], }), qty.root, ], }), footer(opt.readOnly, ok, close), ); }, }); } /** 자재 줄 수량 모달 */ export function openQtyModal( title: string, seat: QtySeat, readOnly: boolean, goArea: (() => void) | null, done: (qty: LaborQty) => void, ): void { openModal({ title, closeLabel: tc("In_Close"), dialogClass: "m02-modal m02-labin", mount: (body, close) => { const qty = qtyPart( seat, readOnly, goArea && (() => { close(); goArea(); }), ); const ok = (): void => { const 수량 = qty.read(); if (수량 === undefined) return showToast(tc("In_QtyNeed"), "error"); done(수량); close(); }; body.append(qty.root, footer(readOnly, ok, close)); }, }); }