Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QND7Co43B24zK2P937rdxt
176 lines
6.7 KiB
TypeScript
176 lines
6.7 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete_Labor_Input.ts
|
|
* 일위대가 입력 모달(39-3) — 단가산출 로직 줄을 누르면 그 로직 입력 전부(빈 칸 = 자동값) · 메모.
|
|
* 수량은 이 모달에 없음(43-2) — 수량산출 칸 누름 = `Labor_QtyModal`(면적산출 시트를 모달 안에).
|
|
* [확인] = 줄 값만 바꿈(화면 캐시) · 저장은 상세 [저장]. 값 칸 규칙 = `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 { 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 LaborInputResult {
|
|
입력값: LaborValues;
|
|
메모: string | null;
|
|
/** 켜쌓기 적용율 %(85-2 · 대상 로직만 · 빈 칸 · 100 = null) */
|
|
켜쌓기율: number | null;
|
|
}
|
|
|
|
/** 켜쌓기율 칸 글 → 값 — 빈 글 · 100 = null · 0 초과 200 이하 · 소수 한 자리 밖 = NaN */
|
|
export function layerRate(raw: string): number | null {
|
|
if (raw.trim() === "") return null;
|
|
const v = Number(raw);
|
|
const ok =
|
|
Number.isFinite(v) && v > 0 && v <= 200 && Math.abs(v * 10 - Math.round(v * 10)) < 1e-9;
|
|
return !ok ? NaN : v === 100 ? null : v;
|
|
}
|
|
|
|
export interface LaborInputOptions {
|
|
/** 「GF000219 메쌓기」 */
|
|
title: string;
|
|
/** 로직 입력 목록(없으면 = 못 읽음 · 메모만) */
|
|
logic: ItemLogic | null;
|
|
values: LaborValues;
|
|
memo: string | null;
|
|
/** 켜쌓기율(85-2) — 로직이 대상일 때만 칸 */
|
|
rate?: number | null;
|
|
readOnly: boolean;
|
|
done: (next: LaborInputResult) => void;
|
|
/** 머리 한 줄 알림(51-14 · 서고 원본 고침) */
|
|
notice?: string;
|
|
/** 확인 단추 글(기본 「확인」) */
|
|
okLabel?: string;
|
|
}
|
|
|
|
/** 담은 로직 줄 입력 모달 */
|
|
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<string>();
|
|
openModal({
|
|
title: fill(tc("In_Title"), opt.title),
|
|
closeLabel: tc("In_Close"),
|
|
dialogClass: "m02-modal m02-labin",
|
|
mount: (body, close, head) => {
|
|
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 rate = opt.logic?.켜쌓기
|
|
? (el("input", {
|
|
className: "ui-input m02-inputval__control",
|
|
attrs: {
|
|
type: "number",
|
|
min: "0.1",
|
|
max: "200",
|
|
step: "0.1",
|
|
placeholder: "100",
|
|
"data-labor-layer": "",
|
|
},
|
|
}) as HTMLInputElement)
|
|
: null;
|
|
if (rate) {
|
|
rate.value = opt.rate == null ? "" : String(opt.rate);
|
|
rate.readOnly = opt.readOnly;
|
|
}
|
|
const ok = (): void => {
|
|
const layer = rate ? layerRate(rate.value) : null;
|
|
if (bad.size || Number.isNaN(layer)) return showToast(tc("In_Bad"), "error");
|
|
opt.done({ 입력값: { ...draft }, 메모: memo.value.trim() || null, 켜쌓기율: layer });
|
|
close();
|
|
};
|
|
body.append(
|
|
el("div", {
|
|
className: "m02-inputval",
|
|
children: [
|
|
...(opt.notice
|
|
? [
|
|
el("p", {
|
|
className: "m02-labin__muted",
|
|
text: opt.notice,
|
|
attrs: { "data-labor-notice": "" },
|
|
}),
|
|
]
|
|
: []),
|
|
...(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") })]),
|
|
...(rate
|
|
? [
|
|
el("div", {
|
|
className: "m02-inputval__row",
|
|
children: [
|
|
el("span", {
|
|
className: "m02-inputval__name",
|
|
text: tc("In_Layer"),
|
|
attrs: { title: tc("In_LayerHint") },
|
|
}),
|
|
rate,
|
|
],
|
|
}),
|
|
]
|
|
: []),
|
|
el("div", {
|
|
className: "m02-inputval__row",
|
|
children: [
|
|
el("span", { className: "m02-inputval__name", text: tc("In_Memo") }),
|
|
memo,
|
|
],
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
// [확인] = 머리 [닫기] 왼쪽 · 아래 단추 없음(머리 [닫기] = 취소 · 147-3) · 읽기만 = [닫기]만
|
|
if (!opt.readOnly)
|
|
head.append(
|
|
createButton({ label: opt.okLabel ?? tc("In_Ok"), variant: "filled", onClick: ok }),
|
|
);
|
|
},
|
|
});
|
|
}
|