- 표 둘 = 표 부품(m01-master__grid = 일반표 · m01-logic__grid = 데이터표) · 페이지 CSS 는 폭 · 머리 고정 · 칸 상태만 - 손 모달 다섯(조달 찾기 · 요소 고르기 · 표 바꾸기 · 복사 시트 · 표 고르기) = 모달 부품 openModal - 알림 띠 · 막힘 까닭 = 띠 알림 · 태그 = 배지 · 알약 · × · 삭제 · 로직 키 = 단추 부품 · muted 세 벌 = 보조 글 · 빈 줄 = 빈 상태 - M02 가 빌린 M01 클래스(표 · muted · 고르기 창)도 같은 부품으로 · 죽은 CSS 걷음 · 지킴 시험 test_553_5_m01_m02 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
118 lines
4.6 KiB
TypeScript
118 lines
4.6 KiB
TypeScript
/* =============================================================================
|
|
* M01_MasterData_UI_Pick.ts
|
|
* 고르기 모달 — 가격 연결(자재품목) · 준용 직종을 이름·규격으로 찾아 고르면 ref 를 돌려줌
|
|
* ========================================================================== */
|
|
|
|
import { createButton, createInputField, el, showToast } from "@ui/ui_template_elements";
|
|
import { createEmpty } from "@ui/ui_template_empty";
|
|
import { openModal } from "@ui/ui_template_modal";
|
|
import { createText } from "@ui/ui_template_text";
|
|
import { t as L } from "@ui/ui_template_locale";
|
|
import { fetchPick, type PickItem, type PickKind } from "./M01_MasterData_Api_Fetch";
|
|
import { show } from "./M01_MasterData_UI_Cells";
|
|
|
|
/** 지역이 갈리는 재료 — 이름·규격만 적어 두고 지역은 계산 때 입력. */
|
|
export interface Cond {
|
|
이름: string;
|
|
규격: string;
|
|
지역: "입력";
|
|
}
|
|
|
|
export interface PickOptions {
|
|
kind: PickKind;
|
|
title: string;
|
|
/** 지금 연결 글 — 비면 「연결 끊기」 꺼짐. */
|
|
current: string;
|
|
/** 처음 찾을 글 — 그 재료 이름·규격 (없는 결과면 첫 낱말만으로 다시). */
|
|
seed: string;
|
|
/** 고르면 ref · 연결 끊기 = null. */
|
|
onPick: (ref: string | null, name?: string, value?: unknown) => void;
|
|
/** 있으면 「후보 조건으로 연결」 칸이 뜸. */
|
|
cond?: { 이름: string; 규격: string; onCond: (c: Cond) => void };
|
|
}
|
|
|
|
const button = (label: string): HTMLButtonElement => createButton({ label, variant: "ghost" });
|
|
|
|
const field = (value: string, placeholder: string): ReturnType<typeof createInputField> =>
|
|
createInputField({ type: "search", placeholder, value });
|
|
|
|
export function openPickModal(opt: PickOptions): void {
|
|
const searchField = field(opt.seed, L("M01_ProcureSearch"));
|
|
const search = searchField.input;
|
|
const list = el("div", { className: "m01-procure__list" });
|
|
const cut = button(L("M01_ProcureCut"));
|
|
cut.disabled = !opt.current;
|
|
cut.addEventListener("click", () => {
|
|
opt.onPick(null);
|
|
close();
|
|
});
|
|
|
|
const condRow: HTMLElement[] = [];
|
|
if (opt.cond) {
|
|
const { onCond } = opt.cond;
|
|
const name = field(opt.cond.이름, L("M01_PriceCondName")).input;
|
|
const spec = field(opt.cond.규격, L("M01_PriceCondSpec")).input;
|
|
const go = button(L("M01_PriceCond"));
|
|
go.addEventListener("click", () => {
|
|
if (!name.value.trim()) return;
|
|
onCond({ 이름: name.value.trim(), 규격: spec.value.trim(), 지역: "입력" });
|
|
close();
|
|
});
|
|
condRow.push(el("div", { className: "m01-procure__cond", children: [name, spec, go] }));
|
|
}
|
|
|
|
// 모달 부품 — 제목줄 [닫기] · 바깥 누르기 · Esc · 위 고정(공용 꼴) · [연결 끊기] 는 머리 단추 자리
|
|
const { close } = openModal({
|
|
title: opt.title,
|
|
closeLabel: L("M01_ProcureClose"),
|
|
width: "40rem",
|
|
dialogClass: "m01-procure__box",
|
|
actions: [cut],
|
|
mount: (body) => body.append(createText(opt.current), searchField.root, list, ...condRow),
|
|
});
|
|
|
|
const paint = (items: PickItem[], total: number): void => {
|
|
list.replaceChildren(
|
|
...(items.length
|
|
? items.map((it) => {
|
|
const tag = it.관급 ? ` · ${L("M01_Govt")}` : "";
|
|
const b = el("button", {
|
|
className: "m01-procure__item ui-list-row ui-sidebar-row",
|
|
attrs: { type: "button", "data-size": "36" },
|
|
children: [
|
|
el("strong", { text: it.이름 }),
|
|
el("span", { text: `${it.규격} · ${it.단위} · ${show(it.값)}${tag}` }),
|
|
el("span", { className: "ui-text", text: it.ref }),
|
|
],
|
|
});
|
|
b.addEventListener("click", () => {
|
|
opt.onPick(it.ref, `${it.이름} ${it.규격}`.trim(), it.값);
|
|
close();
|
|
});
|
|
return b;
|
|
})
|
|
: [createEmpty({ text: L("M01_NoRows"), small: true })]),
|
|
...(total > items.length ? [createText(`${items.length} / ${total}`)] : []),
|
|
);
|
|
};
|
|
let timer: number | undefined;
|
|
const run = async (fallback = false): Promise<void> => {
|
|
try {
|
|
const q = search.value.trim();
|
|
const r = await fetchPick(opt.kind, q);
|
|
if (!r.total && fallback && q.includes(" ")) {
|
|
search.value = q.split(/\s+/)[0];
|
|
return run();
|
|
}
|
|
paint(r.items, r.total);
|
|
} catch (error) {
|
|
showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error");
|
|
}
|
|
};
|
|
search.addEventListener("input", () => {
|
|
window.clearTimeout(timer);
|
|
timer = window.setTimeout(() => void run(), 300);
|
|
});
|
|
void run(true);
|
|
}
|