Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PAdA5ThqmtVSsbzjusk1cJ
219 lines
7.6 KiB
TypeScript
219 lines
7.6 KiB
TypeScript
/* =============================================================================
|
|
* M01_MasterData_UI_LogicLab_New_Find.ts
|
|
* 새 로직 만들기의 고르기 창 둘
|
|
* ① 마스터 요소 — 종류(인력·재료·기계) → 구분 → 상세구분 → 이름 찾기 → 한 줄 고르기
|
|
* ② 표 찾기 — 표(소요량·계수)와 값 칸을 고른 뒤 표의 조건 칸마다 식을 적음
|
|
* ========================================================================== */
|
|
|
|
import {
|
|
createButton,
|
|
createInputField,
|
|
createSelectField,
|
|
el,
|
|
showToast,
|
|
} from "@ui/ui_template_elements";
|
|
import {
|
|
fetchFiles,
|
|
fetchRows,
|
|
fetchSubsAll,
|
|
type FileInfo,
|
|
type SubInfo,
|
|
} from "./M01_MasterData_Api_Fetch";
|
|
import type { DraftFind } from "./M01_MasterData_UI_Logic_Api";
|
|
import { formatNumber } from "./M01_MasterData_UI_Logic_Edit";
|
|
import { loadTable } from "./M01_MasterData_UI_Logic_Note";
|
|
import { openPicker } from "./M01_MasterData_UI_Logic_Pick";
|
|
import { tn } from "./M01_MasterData_UI_LogicLab_New_Text";
|
|
|
|
export interface PickedElement {
|
|
ref: string;
|
|
/** 이름 + 규격 */
|
|
name: string;
|
|
/** 수량에 쓰는 단위(「원/일」 → 「일」) */
|
|
unit: string;
|
|
group: string;
|
|
}
|
|
|
|
const GROUPS = ["인력", "재료", "기계"];
|
|
const SHOWN = 50;
|
|
|
|
/** 창 뼈대 — 제목 · 닫기 · 몸통 */
|
|
function shell(title: string, body: HTMLElement[]): { close: () => void } {
|
|
const close = (): void => backdrop.remove();
|
|
const dialog = el("div", {
|
|
className: "m01-logic__pick",
|
|
attrs: { role: "dialog", "aria-label": title },
|
|
children: [
|
|
el("div", {
|
|
className: "m01-logic__section-head",
|
|
children: [
|
|
el("h3", { text: title }),
|
|
createButton({ label: tn("Close"), variant: "ghost", onClick: close }),
|
|
],
|
|
}),
|
|
...body,
|
|
],
|
|
});
|
|
const backdrop = el("div", { className: "m01-logic__backdrop", children: [dialog] });
|
|
backdrop.addEventListener("click", (ev) => ev.target === backdrop && close());
|
|
backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && close());
|
|
document.body.append(backdrop);
|
|
return { close };
|
|
}
|
|
|
|
/** 그룹의 파일 — 재료는 자재품목(구분·상세구분이 있는 것) */
|
|
async function fileOf(group: string): Promise<FileInfo | undefined> {
|
|
const files = await fetchFiles(group);
|
|
return group === "재료" ? (files.find((f) => f.file.includes("자재품목")) ?? files[0]) : files[0];
|
|
}
|
|
|
|
const options = (values: string[]): { value: string; text: string }[] => [
|
|
{ value: "", text: tn("All") },
|
|
...values.map((v) => ({ value: v, text: v })),
|
|
];
|
|
|
|
export function openElementPick(done: (picked: PickedElement) => void): void {
|
|
let file: FileInfo | undefined;
|
|
let subs: SubInfo[] = [];
|
|
const groupBox = createSelectField({
|
|
options: GROUPS.map((g) => ({ value: g, text: g })),
|
|
value: GROUPS[0],
|
|
compact: true,
|
|
label: tn("Group"),
|
|
});
|
|
const subBox = createSelectField({
|
|
options: options([]),
|
|
value: "",
|
|
compact: true,
|
|
label: tn("Sub"),
|
|
});
|
|
const detailBox = createSelectField({
|
|
options: options([]),
|
|
value: "",
|
|
compact: true,
|
|
label: tn("Detail"),
|
|
});
|
|
const searchField = createInputField({ type: "search", placeholder: tn("Search") });
|
|
const count = el("span", { className: "m01-logic__muted" });
|
|
const list = el("div", { className: "m01-logic__pick-list" });
|
|
const win = shell(tn("K_master"), [
|
|
el("div", {
|
|
className: "m01-logic__pick-bar",
|
|
children: [groupBox.root, subBox.root, detailBox.root, searchField.root, count],
|
|
}),
|
|
list,
|
|
]);
|
|
|
|
let seq = 0;
|
|
const load = async (): Promise<void> => {
|
|
if (!file) return;
|
|
const mine = ++seq;
|
|
try {
|
|
const got = await fetchRows(
|
|
file.file,
|
|
1,
|
|
SHOWN,
|
|
searchField.input.value.trim(),
|
|
false,
|
|
subBox.select.value,
|
|
detailBox.select.value,
|
|
);
|
|
if (mine !== seq) return;
|
|
count.textContent = tn("Count", { n: got.total, m: Math.min(got.total, SHOWN) });
|
|
list.replaceChildren(
|
|
...got.rows.map((row) => {
|
|
const name = `${row["이름"] ?? ""} ${row["규격"] ?? ""}`.trim();
|
|
const unitText = String(row["단위"] ?? "");
|
|
const unit = groupBox.select.value === "인력" ? "인" : (unitText.split("/").pop() ?? "");
|
|
const button = el("button", {
|
|
className: "m01-logic__pick-row m01-logic__pick-row--button",
|
|
attrs: { type: "button", "data-ref": String(row["키"]) },
|
|
children: [
|
|
el("div", {
|
|
className: "m01-logic__stack",
|
|
children: [
|
|
el("strong", { text: name }),
|
|
el("span", { className: "m01-logic__muted", text: String(row["키"]) }),
|
|
],
|
|
}),
|
|
el("span", {
|
|
className: "m01-logic__money",
|
|
text: `${formatNumber(row["값"])} ${unitText}`.trim(),
|
|
}),
|
|
],
|
|
});
|
|
button.addEventListener("click", () => {
|
|
win.close();
|
|
done({ ref: String(row["키"]), name, unit, group: groupBox.select.value });
|
|
});
|
|
return button;
|
|
}),
|
|
);
|
|
} catch (error) {
|
|
showToast(error instanceof Error ? error.message : tn("Failed"), "error");
|
|
}
|
|
};
|
|
|
|
const changeGroup = async (): Promise<void> => {
|
|
file = await fileOf(groupBox.select.value);
|
|
subs = file ? (await fetchSubsAll({ file: file.file })).subs : [];
|
|
subBox.setOptions(options(subs.map((s) => s.name)), "");
|
|
detailBox.setOptions(options([]), "");
|
|
detailBox.select.disabled = true;
|
|
await load();
|
|
};
|
|
groupBox.select.addEventListener("change", () => void changeGroup());
|
|
subBox.select.addEventListener("change", () => {
|
|
const details = subs.find((s) => s.name === subBox.select.value)?.details ?? [];
|
|
detailBox.setOptions(options(details), "");
|
|
detailBox.select.disabled = details.length === 0;
|
|
void load();
|
|
});
|
|
detailBox.select.addEventListener("change", () => void load());
|
|
let timer = 0;
|
|
searchField.input.addEventListener("input", () => {
|
|
window.clearTimeout(timer);
|
|
timer = window.setTimeout(() => void load(), 250);
|
|
});
|
|
void changeGroup();
|
|
}
|
|
|
|
/** 표 찾기 — 표·값 칸을 고른 뒤 조건 칸마다 식을 받음 · 표의 조건 칸이 없으면 바로 끝 */
|
|
export function openTablePick(done: (find: DraftFind, label: string) => void): void {
|
|
openPicker("table", "소요량", (item, column) => {
|
|
void (async () => {
|
|
const table = await loadTable(item.ref);
|
|
const conds = Object.keys(table?.조건 ?? {});
|
|
const label = `${item.이름 ?? item.ref} · ${item.ref} → ${column ?? ""}`;
|
|
const find = { 표: item.ref, 값칸: column ?? "", 조건: {} as Record<string, string> };
|
|
if (!conds.length) {
|
|
done(find, label);
|
|
return;
|
|
}
|
|
const inputs = conds.map((c) => {
|
|
const box = createInputField({ type: "text", value: c });
|
|
box.input.setAttribute("data-cond", c);
|
|
return { c, box };
|
|
});
|
|
const win = shell(tn("TableTitle"), [
|
|
el("p", { className: "m01-logic__muted", text: label }),
|
|
el("p", { text: tn("Cond") }),
|
|
...inputs.map(({ c, box }) =>
|
|
el("label", {
|
|
className: "m01-logic__field",
|
|
children: [el("span", { text: c }), box.root],
|
|
}),
|
|
),
|
|
createButton({
|
|
label: tn("Ok"),
|
|
onClick: () => {
|
|
for (const { c, box } of inputs) find.조건[c] = box.input.value.trim() || c;
|
|
win.close();
|
|
done(find, label);
|
|
},
|
|
}),
|
|
]);
|
|
})();
|
|
});
|
|
}
|