Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts
T

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"),
);
}