Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4uKaV4ZBhsJN4GHG7yRj4
236 lines
8.7 KiB
TypeScript
236 lines
8.7 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete_Labor_Pick.ts
|
|
* [로직 더하기] 창 — 한 목록에 구분(단가산출 · 단가승격 · 일위조합) 칸 · 치는 대로 걸러짐(찾기 단추 없음) ·
|
|
* 단가승격 · 일위조합 줄은 층 단추(위 층만 · 짝 `M02_Labor_Ref.readable`) 층의 서고 · 고르면 참조 {층 · 주인 · 키} ·
|
|
* 프로젝트 · 카드엔 서고 줄 없음
|
|
* ========================================================================== */
|
|
|
|
import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements";
|
|
import { createFindList } from "@ui/ui_template_find_list";
|
|
import { t as L } from "@ui/ui_template_locale";
|
|
import type { LogicSummary, SubBrief } from "../M01_MasterData/M01_MasterData_UI_Logic_Api";
|
|
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
|
|
import type { Layer } from "./M02_MasterTemplete_Api_Fetch";
|
|
import {
|
|
fetchLibLogics as fetchLogics,
|
|
fetchLibLogicSubs as fetchLogicSubs,
|
|
fetchLibrary,
|
|
type LibraryRow,
|
|
} from "./M02_MasterTemplete_Labor_Api";
|
|
import type { LaborRef } from "./M02_MasterTemplete_Labor_Calc";
|
|
import { ownOwner } from "./M02_MasterTemplete_Layer";
|
|
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
|
import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css"; // 창 겉틀
|
|
import "./M02_MasterTemplete_Labor.css";
|
|
|
|
const opts = (values: string[]): { value: string; text: string }[] =>
|
|
[L("M01_All"), ...values].map((v, i) => ({ value: i ? v : "", text: v }));
|
|
|
|
type Scope = Omit<LaborRef, "키">;
|
|
/** 서고 탭 — 일위대가가 있는 층 · 주인(null = 자기 층) · 고르면 참조 */
|
|
export interface LibraryPick {
|
|
layer: Layer;
|
|
owner: string | null;
|
|
pick: (ref: LaborRef, name: string) => void;
|
|
}
|
|
|
|
/** 고를 수 있는 층 — 위 층만(마스터 → 마스터 · 회사 → 그 회사 · 마스터 · 개인 → 그 개인 · 그 회사 · 마스터) */
|
|
async function upper(layer: Layer, owner: string | null): Promise<Scope[]> {
|
|
if (layer === "project") return [];
|
|
const own =
|
|
layer === "system"
|
|
? null
|
|
: (owner ?? ownOwner(layer, await fetchSessionUser().catch(() => null)));
|
|
const out: Scope[] = [{ 층: "system", 주인: null }];
|
|
if (own) out.push({ 층: "company", 주인: own.split("/")[0] });
|
|
if (own && layer === "personal") out.push({ 층: "personal", 주인: own });
|
|
return out;
|
|
}
|
|
const LAYER_TEXT = {
|
|
system: "Lib_System",
|
|
company: "Lib_Company",
|
|
personal: "Lib_Personal",
|
|
} as const;
|
|
|
|
export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: LibraryPick): void {
|
|
const subs: SubBrief[] = [];
|
|
let all: LogicSummary[] = [];
|
|
const sub = createSelectField({ options: opts([]), compact: true, label: tc("Sub") });
|
|
const detail = createSelectField({ options: opts([]), compact: true, label: tc("Detail") });
|
|
detail.select.disabled = true;
|
|
const close = (): void => backdrop.remove();
|
|
|
|
/* 한 목록 — 구분(단가산출 · 단가승격 · 일위조합) 칸 · 층 단추는 서고 줄(승격 · 조합)의 층 */
|
|
const layerBar = el("div", { className: "ui-btn-group", attrs: { "data-pick-layers": "" } });
|
|
let scopes: Scope[] = [];
|
|
let scope: Scope | null = null;
|
|
let libRows: { kind: "Kind_Promote" | "Kind_Combo"; row: LibraryRow }[] = [];
|
|
let loading = false;
|
|
let seq = 0;
|
|
/* 공용 찾기 목록 — 구분 · 상세구분 2열 · 이름 왼쪽 맞춤 · ↑ ↓ Enter */
|
|
const finder = createFindList({
|
|
placeholder: tc("Pick_Search"),
|
|
filters: [sub.root, detail.root],
|
|
extra: [layerBar],
|
|
});
|
|
|
|
const find = (): void => {
|
|
const q = finder.search.value.trim().toLowerCase();
|
|
const found = all.filter(
|
|
(x) =>
|
|
(!sub.select.value || x.구분 === sub.select.value) &&
|
|
(!detail.select.value || x.상세구분 === detail.select.value) &&
|
|
(!q || `${x.키} ${x.이름}`.toLowerCase().includes(q)),
|
|
);
|
|
const libFound = libRows.filter(
|
|
(x) =>
|
|
(!sub.select.value || x.row.구분 === sub.select.value) &&
|
|
(!detail.select.value || x.row.상세구분 === detail.select.value) &&
|
|
(!q || `${x.row.이름} ${x.row.제목 ?? ""}`.toLowerCase().includes(q)),
|
|
);
|
|
const total = all.length + libRows.length;
|
|
const shown = found.length + libFound.length;
|
|
finder.hint.textContent = loading
|
|
? tc("Loading")
|
|
: shown
|
|
? `${shown} / ${total}`
|
|
: tc("Pick_None");
|
|
finder.setRows([
|
|
...found.slice(0, 100).map((x) => ({
|
|
key: x.키,
|
|
tag: tc("Kind_Logic"),
|
|
name: x.이름,
|
|
sub: `${x.키} · ${x.구분} ${x.상세구분}`.trim(),
|
|
unit: `${x.결과단위}${x.blocked ? ` · ${tc("Pick_Blocked")}` : ""}`,
|
|
onPick: () => {
|
|
close();
|
|
done(x);
|
|
},
|
|
})),
|
|
...libFound.map((x) => {
|
|
const at = scope as Scope;
|
|
return {
|
|
key: x.row.이름,
|
|
tag: tc(x.kind),
|
|
name: x.row.제목 || x.row.이름,
|
|
sub: [x.row.이름, `${x.row.구분 ?? ""} ${x.row.상세구분 ?? ""}`.trim()]
|
|
.filter(Boolean)
|
|
.join(" · "),
|
|
unit: x.row.단위 ?? "",
|
|
onPick: () => {
|
|
close();
|
|
lib?.pick({ ...at, 키: x.row.이름 }, x.row.제목 || x.row.이름);
|
|
},
|
|
};
|
|
}),
|
|
]);
|
|
};
|
|
const loadLib = async (): Promise<void> => {
|
|
if (!lib || !scope) return;
|
|
const mine = ++seq;
|
|
const at = scope;
|
|
loading = true;
|
|
find();
|
|
try {
|
|
const [promote, combo] = await Promise.all([
|
|
fetchLibrary(at, "promote"),
|
|
fetchLibrary(at, "precombo"),
|
|
]);
|
|
if (mine !== seq) return;
|
|
libRows = [
|
|
...promote.map((row) => ({ kind: "Kind_Promote" as const, row })),
|
|
...combo.map((row) => ({ kind: "Kind_Combo" as const, row })),
|
|
];
|
|
} catch (error) {
|
|
if (mine !== seq) return;
|
|
libRows = [];
|
|
showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error");
|
|
}
|
|
loading = false;
|
|
drawSubs();
|
|
find();
|
|
};
|
|
const drawLayers = (): void =>
|
|
layerBar.replaceChildren(
|
|
...scopes.map((x) =>
|
|
createButton({
|
|
label: tc(LAYER_TEXT[x.층 as keyof typeof LAYER_TEXT]),
|
|
variant: x === scope ? "filled" : "ghost",
|
|
onClick: () => {
|
|
scope = x;
|
|
drawLayers();
|
|
void loadLib();
|
|
},
|
|
}),
|
|
),
|
|
);
|
|
/** 구분 → 상세구분 — 로직 구분에 서고 줄(승격 · 조합) 것을 더함 · 같은 값은 한 칸 */
|
|
const subMap = (): Map<string, Set<string>> => {
|
|
const m = new Map(subs.map((s) => [s.name, new Set(s.details)]));
|
|
for (const { row } of libRows) {
|
|
if (!row.구분) continue;
|
|
if (!m.has(row.구분)) m.set(row.구분, new Set());
|
|
if (row.상세구분) m.get(row.구분)!.add(row.상세구분);
|
|
}
|
|
return m;
|
|
};
|
|
const drawDetails = (keep: string): void => {
|
|
const d = [...(subMap().get(sub.select.value) ?? [])];
|
|
detail.setOptions(opts(d), d.includes(keep) ? keep : "");
|
|
detail.select.disabled = d.length === 0;
|
|
};
|
|
const drawSubs = (): void => {
|
|
const keep = detail.select.value;
|
|
sub.setOptions(opts([...subMap().keys()]), sub.select.value);
|
|
drawDetails(keep);
|
|
};
|
|
sub.select.addEventListener("change", () => {
|
|
drawDetails("");
|
|
find();
|
|
});
|
|
detail.select.addEventListener("change", find);
|
|
finder.search.addEventListener("input", find);
|
|
|
|
layerBar.style.display = "none";
|
|
if (lib)
|
|
void upper(lib.layer, lib.owner).then((got) => {
|
|
scopes = got;
|
|
scope = got[got.length - 1] ?? null; // 기본 = 가장 가까운 층
|
|
if (!got.length) return;
|
|
layerBar.style.display = "";
|
|
drawLayers();
|
|
void loadLib();
|
|
});
|
|
|
|
const dialog = el("div", {
|
|
className: "m01-logic__pick m01-combo",
|
|
attrs: { role: "dialog", "aria-label": tc("Pick_Title") },
|
|
children: [
|
|
el("div", {
|
|
className: "m01-logic__section-head",
|
|
children: [
|
|
el("h3", { text: tc("Pick_Title") }),
|
|
createButton({ label: tc("Pick_Close"), variant: "ghost", onClick: close }),
|
|
],
|
|
}),
|
|
finder.root,
|
|
],
|
|
});
|
|
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);
|
|
finder.search.focus();
|
|
|
|
void Promise.all([fetchLogicSubs(), fetchLogics()])
|
|
.then(([s, l]) => {
|
|
subs.push(...s);
|
|
all = l;
|
|
drawSubs();
|
|
find();
|
|
})
|
|
.catch((error) =>
|
|
showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error"),
|
|
);
|
|
}
|