- UI_Main: 종류 labor 갈래 한 곳(mountLaborDetail · 「면적산출에서 잇기」 = 물은 뒤 그 열 면적산출) · 번호 칸 일위대가번 - Api_Fetch: Kind 에 labor - 새 Labor_View.ts(createLaborView — 면적산출 오른쪽 패널 · 일위대가 카드용 · 담은 로직 + 자재목록 · show · setQty · onRow) - Labor_Detail: 면적산출 연결 칸 → 「수량 F14 = …」 · 수량만 바뀌면 Labor_Calc.settle 로 서버 없이 · 손 입력 숫자는 수로 - Labor_Api: calc 답 모양은 Labor_Calc.ts 것을 씀 · normalizeLabor · areaLinks - 자재 표 머리 「단위」 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7
247 lines
8.6 KiB
TypeScript
247 lines
8.6 KiB
TypeScript
/* =============================================================================
|
|
* 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<string>();
|
|
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));
|
|
},
|
|
});
|
|
}
|