- 판정 = 자체 로직 편집 · 시험 값 고침 · M02 줄 덮음 · 비목 박스 소계 모두 수식 보기로 됨(살릴 것 없음) - M01 상세: 숨은 단가산출 상세 덩이 · 자체 로직 시트 편집기 · 시험 고침 · 셈 구동(createDrive) · 보기 이름표(View.ts) · data-view CSS 걷음 - M02 불러오기 모달 머리 [항목 추가] · [율 줄 추가] 단추와 그 창(Rate.ts · openSangeunLinePicker) 걷음 - 글 정리 = Info_Own 새 뜻 · 죽은 글 키 · No_Key · 항목 추가 글 걷음 - 낡은 시험 고침 = 722(시험 고침 · 덮기 셈) · 799-2 · 804-2 · 804-6 · 804-7 · 806-1 · 717-2b · 717-4 · 721 · 728-2 — 걷은 것이 없음 확인으로 - 남김 = M02 수식 문서 없는 줄의 단가산출 상세 몸(cover · buildHo) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
143 lines
5.5 KiB
TypeScript
143 lines
5.5 KiB
TypeScript
/* =============================================================================
|
|
* M01_MasterData_UI_LogicLab_Cover.ts
|
|
* 단가산출 상세 줄 「그 자리 고침」(PLAN 722-2 · 내역 16 · 17-2) — 줄 모달 안 수량 · 단가 칸.
|
|
* 고침 꼴 = M02 단가산출 덮개 칸과 같음(`qty` · `labor` · `material` · `expense` — `M02_Sangeun_Doc._covered` 규칙 중 품 · 단가)
|
|
* M02(724) = 고침 칸의 값을 고침 고리(`HoEdit.set`)로 받아 덮개로 보관 — M01 시험 고침은 840-1 에 걷음
|
|
* 서버 셈 = 줄 `단가`(손 단가) · 로직 줄 `값묶음`(노 · 재 · 경) · `수량` 글 — `master_pick._priced` · `bundled` 가 읽음
|
|
* ========================================================================== */
|
|
|
|
import { createButton, createInputField, el } from "@ui/ui_template_elements";
|
|
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
|
import { createText } from "@ui/ui_template_text";
|
|
|
|
/* ── 순수 셈 ── */
|
|
/** 줄 하나 고침 — 품(수량) · 비목 단가(로직 줄은 셋 · 그 밖은 그 줄 비목 하나) */
|
|
export interface LineCover {
|
|
qty?: number;
|
|
labor?: number;
|
|
material?: number;
|
|
expense?: number;
|
|
}
|
|
export const COVER_PARTS = { labor: "노무비", material: "재료비", expense: "경비" } as const;
|
|
export type CoverPart = keyof typeof COVER_PARTS;
|
|
|
|
/* ── 순수 셈 끝 ── */
|
|
|
|
const TEXT = {
|
|
Head: ["그 자리 고침 (시험 값)", "Edit here (trial value)"],
|
|
Note: [
|
|
"정본 · 마스터 값(노임 · 자재)은 안 바뀜 · 로직을 다시 열면 사라짐",
|
|
"Source and master prices stay unchanged · cleared when reopened",
|
|
],
|
|
NoteM02: [
|
|
"고친 값은 이 단가산출의 변경 항목으로 저장 — M01 은 안 바뀜",
|
|
"Saved as this item's override — M01 stays unchanged",
|
|
],
|
|
Qty: ["수량(품)", "Quantity"],
|
|
Price: ["단가", "Unit price"],
|
|
Apply: ["적용", "Apply"],
|
|
Undo: ["되돌림", "Undo"],
|
|
Bad: ["0 이상 수만", "Numbers ≥ 0 only"],
|
|
} as const satisfies Record<string, readonly [string, string]>;
|
|
export const tc = (k: keyof typeof TEXT): string =>
|
|
TEXT[k][currentLanguageIndex as 0 | 1] ?? TEXT[k][0];
|
|
|
|
export interface CoverFormOptions {
|
|
/** 지금 값(덮기 전 셈 값) — 칸 처음 값 */
|
|
qty: number | null;
|
|
/** 단가 칸 — 로직 줄 = 셋(1 단위 비목 값) · 그 밖 = 그 줄 비목 하나 */
|
|
prices: Partial<Record<CoverPart, number | null>>;
|
|
/** 지금 든 고침(있으면 칸 처음 값 · [되돌림]) */
|
|
cover?: LineCover;
|
|
/** 「m02」 = 덮개 보관 안내 */
|
|
scope?: "m01" | "m02";
|
|
onApply: (cover: LineCover | null) => void;
|
|
/** 모달 머리 단추 자리(닫기 왼쪽 · 748-1) — 주면 [적용] 이 거기로 */
|
|
head?: HTMLElement;
|
|
}
|
|
|
|
const num = (raw: string): number | null => {
|
|
const v = Number(raw.replaceAll(",", "").trim());
|
|
return raw.trim() !== "" && Number.isFinite(v) && v >= 0 ? v : null;
|
|
};
|
|
|
|
/** 고침 칸 — 수량 · 단가(들) · [적용] [되돌림] · 바뀐 칸만 고침으로 넘김 */
|
|
export function coverForm(o: CoverFormOptions): HTMLElement {
|
|
const parts = Object.keys(o.prices) as CoverPart[];
|
|
const field = (label: string, raw: number | null, mine?: number) => {
|
|
const now = raw === null ? null : Number(raw.toPrecision(10)); // 칸 글 = 유효 10 자리 · 손대지 않으면 고침 아님
|
|
const box = createInputField({
|
|
label,
|
|
value: String(mine ?? now ?? ""),
|
|
compact: true,
|
|
type: "text",
|
|
});
|
|
box.input.inputMode = "decimal";
|
|
return { box, now };
|
|
};
|
|
const qty = field(tc("Qty"), o.qty, o.cover?.qty);
|
|
const prices = parts.map((k) => ({
|
|
k,
|
|
...field(parts.length > 1 ? COVER_PARTS[k] : tc("Price"), o.prices[k] ?? null, o.cover?.[k]),
|
|
}));
|
|
const error = createText("", "error");
|
|
error.hidden = true;
|
|
const apply = (): void => {
|
|
const next: LineCover = {};
|
|
let bad = false;
|
|
const take = (f: { box: { input: HTMLInputElement }; now: number | null }, was?: number) => {
|
|
const v = num(f.box.input.value);
|
|
if (v === null)
|
|
return f.box.input.value.trim() === "" ? undefined : ((bad = true), undefined);
|
|
return v !== f.now || was !== undefined ? v : undefined; // 셈 값 그대로면 고침 아님
|
|
};
|
|
const q = take(qty, o.cover?.qty);
|
|
if (q !== undefined) next.qty = q;
|
|
for (const p of prices) {
|
|
const v = take(p, o.cover?.[p.k]);
|
|
if (v !== undefined) next[p.k] = v;
|
|
}
|
|
error.textContent = tc("Bad");
|
|
error.hidden = !bad;
|
|
if (!bad) o.onApply(Object.keys(next).length ? next : null);
|
|
};
|
|
const applyButton = createButton({
|
|
label: tc("Apply"),
|
|
variant: "filled",
|
|
size: 32,
|
|
onClick: apply,
|
|
});
|
|
o.head?.append(applyButton);
|
|
const actions = el("div", {
|
|
className: "m01lab-cover__actions",
|
|
children: [
|
|
...(o.head ? [] : [applyButton]),
|
|
...(o.cover
|
|
? [
|
|
createButton({
|
|
label: tc("Undo"),
|
|
variant: "ghost",
|
|
size: 32,
|
|
onClick: () => o.onApply(null),
|
|
}),
|
|
]
|
|
: []),
|
|
],
|
|
});
|
|
actions.hidden = !actions.childElementCount;
|
|
return el("section", {
|
|
className: "m01lab-cover",
|
|
attrs: { "data-cover-form": "" },
|
|
children: [
|
|
el("strong", { text: tc("Head") }),
|
|
createText(tc(o.scope === "m02" ? "NoteM02" : "Note"), "hint"),
|
|
el("div", {
|
|
className: "m01lab-cover__fields",
|
|
children: [qty.box.root, ...prices.map((p) => p.box.root)],
|
|
}),
|
|
error,
|
|
actions,
|
|
],
|
|
});
|
|
}
|