Files
Aislo/M01_MasterData/M01_MasterData_UI_Rows.ts
T

154 lines
4.6 KiB
TypeScript

/* =============================================================================
* M01_MasterData_UI_Rows.ts
* 요소 파일(인력·재료·기계·환율·요율) 표 — 한 줄 = 요소 · 이름 찾기 · 쪽 나눔 · 눌러 고침(노랑)
* ========================================================================== */
import { createButton, el, showToast } from "@ui/ui_template_elements";
import { t as L } from "@ui/ui_template_locale";
import { fetchRows, type Row, type RowsPage } from "./M01_MasterData_Api_Fetch";
import {
addRow,
editRow,
onDraftChange,
peek,
removeAdd,
same,
setAdd,
toggleDelete,
} from "./M01_MasterData_Draft";
import {
buildCell,
buildPager,
coerce,
flatten,
isScalar,
show,
withCell,
} from "./M01_MasterData_UI_Cells";
const SIZE = 50;
const FALLBACK: Row = { 열쇠: "", 이름: "", 규격: "", 단위: "", : null, 출처: "" };
/** 새 줄 바탕 — 본 줄과 같은 칸 · 글 칸은 "" · 수·값 칸은 null. */
function blank(sample: Row): Row {
const out: Row = {};
for (const [k, v] of Object.entries(sample)) {
if (v && typeof v === "object" && !Array.isArray(v)) out[k] = blank(v as Row);
else out[k] = typeof v === "string" && k !== "값" ? "" : null;
}
return out;
}
/** 돌려받은 함수 = 이 화면을 걷을 때 부를 해제. */
export function renderRows(host: HTMLElement, file: string, q: string): () => void {
let page = 1;
let data: RowsPage | null = null;
const paint = (): void => {
if (!data) return;
const d = data;
const draft = peek(file);
const adds = draft?.adds ?? [];
const cols: string[] = [];
for (const row of [...adds, ...d.rows]) {
for (const k of Object.keys(flatten(row))) if (!cols.includes(k)) cols.push(k);
}
const head = el("tr", { children: [el("th"), ...cols.map((c) => el("th", { text: c }))] });
const body = el("tbody");
adds.forEach((row, i) => {
const flat = flatten(row);
const tr = el("tr", { className: "is-new" });
tr.append(actionCell(L("M01_RowRemove"), () => removeAdd(file, i)));
for (const c of cols) {
tr.append(
buildCell(show(flat[c]), {
changed: true,
onEdit: (text) => setAdd(file, i, withCell(row, c, coerce(text, flat[c]))),
}),
);
}
body.append(tr);
});
for (const row of d.rows) {
const key = String(row["열쇠"]);
const cur = draft?.edits[key] ?? row;
const deleted = draft?.deletes.includes(key) ?? false;
const orig = flatten(row);
const now = flatten(cur);
const tr = el("tr");
tr.classList.toggle("is-deleted", deleted);
tr.append(
actionCell(deleted ? L("M01_RowRestore") : L("M01_RowDelete"), () =>
toggleDelete(file, d.version, key),
),
);
for (const c of cols) {
const editable = !deleted && isScalar(orig[c]);
tr.append(
buildCell(show(now[c]), {
changed: !same(now[c], orig[c]),
onEdit: editable
? (text) =>
editRow(file, d.version, key, row, withCell(cur, c, coerce(text, orig[c])))
: undefined,
}),
);
}
body.append(tr);
}
const add = createButton({ label: L("M01_RowAdd"), variant: "ghost" });
add.addEventListener("click", () => addRow(file, d.version, blank(d.rows[0] ?? FALLBACK)));
const empty =
d.rows.length || adds.length
? []
: [el("p", { className: "m01-master__empty", text: L("M01_NoRows") })];
host.replaceChildren(
el("div", {
className: "m01-master__bar",
children: [add, buildPager(d.total, SIZE, d.page, go)],
}),
el("div", {
className: "m01-master__grid-wrap",
children: [
el("table", {
className: "m01-master__grid",
children: [el("thead", { children: [head] }), body],
}),
],
}),
...empty,
);
};
const go = (next: number): void => {
page = next;
void load();
};
const load = async (): Promise<void> => {
try {
data = await fetchRows(file, page, SIZE, q);
paint();
} catch (error) {
showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error");
}
};
const off = onDraftChange(paint);
void load();
return off;
}
function actionCell(label: string, onClick: () => void): HTMLTableCellElement {
const button = el("button", {
className: "m01-master__row-btn",
text: label,
attrs: { type: "button" },
});
button.addEventListener("click", onClick);
return el("td", { children: [button] });
}