- 새 M01_MasterData_UI_Alias — 모인 후보 / 마스터 장부 탭 · 승인 · 반려 · 지움 = 확인 창 뒤 서버(718-2 길) · 마스터 장부 = M02 별칭 장부 부품 그대로(마스터 층) + 마스터 공사 내역 맞춤 - M01 왼쪽 판 = 구조물 옵션 정의 아래 「별칭 장부」 칸(본 칸 = 옵션 정의와 같은 자리) · 주소 ?open=별칭(M01 페이지 한 줄) - 대시보드 설계 기본값 마스터 데이터 묶음에 「별칭 장부」 줄(m_settings_nav 한 줄 · 화면 글 키 하나) - 시험 test_b01_settings_cards · test_197_1 = 줄 하나 늘어난 자리 · 별칭 칸 대조로 고침(낡은 줄 번호) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
238 lines
8.6 KiB
TypeScript
238 lines
8.6 KiB
TypeScript
/* =============================================================================
|
|
* M01_MasterData_UI_Alias.ts
|
|
* M01 별칭 장부(PLAN 718-3 · 요구서 내역12 4장 23 · 4-1 확정 6) — 본 칸 · 시스템 관리자만.
|
|
* · 「모인 후보」 = 모든 회사 · 개인 층 별칭 장부(일위대가 맞춤) + 공사 내역 맞춤을 서버가 모음
|
|
* (`/api/m01/alias-ledger` · 718-2) · 이름 · 짝 · 회사 수 · 사람 수(누군지 안 보임) · 충돌 ·
|
|
* [승인] = 마스터 장부에 넣음(모든 사용자 제안에 쓰임) · [반려] = 다시 안 보임 · 둘 다 확인 창
|
|
* · 「마스터 장부」 = M02 별칭 장부 부품(`buildHopyoLedger` · 마스터 층) 그대로 + 마스터 공사 내역 맞춤 줄 [지움]
|
|
* · 자동 반영 없음 — 승인한 것만 모두에게 · 승인 · 반려 · 지움은 누를 때 바로 서버(확인 창 뒤)
|
|
* ========================================================================== */
|
|
|
|
import { API_BASE_URL } from "@config/config_frontend";
|
|
import { createBadge } from "@ui/ui_template_badge";
|
|
import { askConfirm, createButton, el, showToast } from "@ui/ui_template_elements";
|
|
import { loadingError, showLoading } from "@ui/ui_template_loading";
|
|
import { createPanelHead } from "@ui/ui_template_panel_head";
|
|
import { createTable, type Column } from "@ui/ui_template_table";
|
|
import { createTabs } from "@ui/ui_template_tabs";
|
|
import "@ui/ui_template_general_layout.css"; // 마스터 장부 부품(M02 Hopyo_Ledger) 표 꼴 — M01 엔 안 실려 있음
|
|
import type { LedgerPair } from "../M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api";
|
|
import {
|
|
buildHopyoLedger,
|
|
KIND_TEXT,
|
|
LEDGER_HINT,
|
|
pairText,
|
|
} from "../M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger";
|
|
|
|
export const ALIAS_TITLE = "별칭 장부";
|
|
const KIND_NAME: Record<string, string> = { 맞춤: "일위대가", 내역맞춤: "공사 내역" };
|
|
|
|
interface Origin {
|
|
품명: string;
|
|
규격: string;
|
|
단위: string;
|
|
}
|
|
export interface AliasCandidate {
|
|
종류: "맞춤" | "내역맞춤";
|
|
열쇠: string;
|
|
서명: string;
|
|
원래: Origin;
|
|
갈래?: string;
|
|
짝?: LedgerPair[];
|
|
확정?: string;
|
|
이름?: string;
|
|
회사: number;
|
|
사람: number;
|
|
때: string | null;
|
|
충돌: boolean;
|
|
마스터: "다름" | null;
|
|
}
|
|
interface MasterBill {
|
|
열쇠: string;
|
|
원래: Origin;
|
|
확정: string;
|
|
이름?: string;
|
|
}
|
|
export interface AliasBoard {
|
|
후보: AliasCandidate[];
|
|
모은곳: { 회사: number; 사람: number };
|
|
반려: number;
|
|
마스터내역?: MasterBill[];
|
|
}
|
|
|
|
async function call<T>(path: string, body?: unknown): Promise<T> {
|
|
const res = await fetch(`${API_BASE_URL}/m01/alias-ledger${path}`, {
|
|
method: body === undefined ? "GET" : "POST",
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
});
|
|
const got = (await res.json()) as T & { detail?: unknown };
|
|
if (!res.ok) throw new Error(String(got.detail ?? `HTTP ${res.status}`));
|
|
return got;
|
|
}
|
|
|
|
/** 짝 글 — 일위대가 = 갈래 · 우리 로직 · 공사 내역 = 우리 공종 이름 */
|
|
export const candidateText = (c: AliasCandidate): string =>
|
|
c.종류 === "내역맞춤"
|
|
? `${c.이름 ?? ""} (${c.확정 ?? ""})`
|
|
: c.갈래 === "짝"
|
|
? (c.짝 ?? []).map(pairText).join(" + ")
|
|
: (KIND_TEXT[c.갈래 ?? ""] ?? c.갈래 ?? "");
|
|
|
|
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
|
const pick = (c: AliasCandidate) => ({ 종류: c.종류, 열쇠: c.열쇠, 서명: c.서명 });
|
|
|
|
const CANDIDATE_COLS: Column[] = [
|
|
{ label: "종류", width: "84px" },
|
|
{ label: "엑셀 품명" },
|
|
{ label: "규격" },
|
|
{ label: "단위", width: "64px" },
|
|
{ label: "짝" },
|
|
{ label: "회사", width: "56px", align: "end" },
|
|
{ label: "사람", width: "56px", align: "end" },
|
|
{ label: "충돌", width: "96px" },
|
|
{ label: "", width: "136px" },
|
|
];
|
|
const BILL_COLS: Column[] = [
|
|
{ label: "엑셀 품명" },
|
|
{ label: "규격" },
|
|
{ label: "단위", width: "64px" },
|
|
{ label: "우리 공종" },
|
|
{ label: "", width: "72px" },
|
|
];
|
|
|
|
export function buildAliasLedger(): HTMLElement {
|
|
const root = el("div", { className: "m01-alias", attrs: { "data-alias-ledger": "" } });
|
|
const body = el("div", { className: "m01-alias__body" });
|
|
let view = "candidates";
|
|
|
|
async function act(c: AliasCandidate, ok: boolean): Promise<void> {
|
|
const name = `${c.원래.품명} ${c.원래.규격}`.trim();
|
|
const yes = await askConfirm({
|
|
title: ok ? "마스터 장부에 승인" : "후보 반려",
|
|
message: ok
|
|
? `「${name}」 → ${candidateText(c)}\n마스터 장부에 넣으면 모든 사용자 제안에 쓰임(각자 장부가 먼저).`
|
|
: `「${name}」 → ${candidateText(c)}\n반려하면 같은 짝은 후보에 다시 안 보임.`,
|
|
okLabel: ok ? "승인" : "반려",
|
|
danger: !ok,
|
|
});
|
|
if (!yes) return;
|
|
try {
|
|
const got = await call<AliasBoard>(ok ? "/approve" : "/reject", pick(c));
|
|
showToast(ok ? "마스터 장부에 넣음" : "반려함", "success");
|
|
paintCandidates(got);
|
|
} catch (error) {
|
|
showToast(`${ok ? "승인" : "반려"} 못 함 — ${why(error)}`, "error");
|
|
}
|
|
}
|
|
|
|
function candidateRow(c: AliasCandidate): (string | Node)[] {
|
|
const clash = c.마스터 === "다름" ? "마스터와 다름" : c.충돌 ? "다른 짝 있음" : "";
|
|
return [
|
|
KIND_NAME[c.종류] ?? c.종류,
|
|
c.원래.품명,
|
|
c.원래.규격,
|
|
c.원래.단위,
|
|
candidateText(c),
|
|
String(c.회사),
|
|
String(c.사람),
|
|
clash ? createBadge(clash, "warning") : "",
|
|
el("div", {
|
|
className: "ui-toolbar",
|
|
children: [
|
|
createButton({
|
|
label: "승인",
|
|
variant: "filled",
|
|
size: "sm",
|
|
onClick: () => void act(c, true),
|
|
}),
|
|
createButton({
|
|
label: "반려",
|
|
variant: "ghost",
|
|
size: "sm",
|
|
onClick: () => void act(c, false),
|
|
}),
|
|
],
|
|
}),
|
|
];
|
|
}
|
|
|
|
function paintCandidates(board: AliasBoard): void {
|
|
const head = createPanelHead({
|
|
title: "모인 후보",
|
|
sub: `${LEDGER_HINT} · 회사 ${board.모은곳.회사} · 사람 ${board.모은곳.사람}의 장부에서 모음 · 반려 ${board.반려} · 승인한 것만 모두에게 · 누가 적었는지는 안 보임`,
|
|
});
|
|
body.replaceChildren(
|
|
head.root,
|
|
createTable(CANDIDATE_COLS, board.후보.map(candidateRow), {
|
|
density: "data",
|
|
empty: "모인 후보 없음",
|
|
}),
|
|
);
|
|
}
|
|
|
|
async function forget(b: MasterBill): Promise<void> {
|
|
const yes = await askConfirm({
|
|
title: "마스터 공사 내역 맞춤 지움",
|
|
message: `「${b.원래.품명} ${b.원래.규격}」 → ${b.이름 ?? b.확정}\n지우면 모든 사용자 제안에서 빠짐.`,
|
|
okLabel: "지움",
|
|
danger: true,
|
|
});
|
|
if (!yes) return;
|
|
try {
|
|
const got = await call<{ 마스터내역: MasterBill[] }>("/master-bill/forget", { 열쇠: b.열쇠 });
|
|
showToast("지움", "success");
|
|
paintMaster(got.마스터내역);
|
|
} catch (error) {
|
|
showToast(`지우지 못 함 — ${why(error)}`, "error");
|
|
}
|
|
}
|
|
|
|
function paintMaster(bill: MasterBill[]): void {
|
|
const rows = bill.map((b) => [
|
|
b.원래.품명,
|
|
b.원래.규격,
|
|
b.원래.단위,
|
|
`${b.이름 ?? ""} (${b.확정})`,
|
|
createButton({ label: "지움", variant: "danger", size: "sm", onClick: () => void forget(b) }),
|
|
]);
|
|
body.replaceChildren(
|
|
buildHopyoLedger({ scope: { layer: "system", owner: null }, canWrite: true }),
|
|
createPanelHead({
|
|
title: "마스터 공사 내역 맞춤",
|
|
sub: "B09 내역 줄 ↔ 우리 공종(각자 기억이 먼저)",
|
|
}).root,
|
|
createTable(BILL_COLS, rows, { density: "data", empty: "마스터 공사 내역 맞춤 없음" }),
|
|
);
|
|
}
|
|
|
|
async function load(): Promise<void> {
|
|
const stop = showLoading(body);
|
|
try {
|
|
const got = await call<AliasBoard>("");
|
|
if (view === "candidates") paintCandidates(got);
|
|
else paintMaster(got.마스터내역 ?? []);
|
|
} catch (error) {
|
|
body.replaceChildren(loadingError(why(error), () => void load()));
|
|
} finally {
|
|
stop();
|
|
}
|
|
}
|
|
|
|
const tabs = createTabs({
|
|
tabs: [
|
|
{ key: "candidates", label: "모인 후보" },
|
|
{ key: "master", label: "마스터 장부" },
|
|
],
|
|
value: view,
|
|
onChange: (key) => {
|
|
view = key;
|
|
void load();
|
|
},
|
|
});
|
|
root.append(tabs.root, body); // 제목 = 본 칸 머리(ALIAS_TITLE · 왼쪽 판이 넘김)
|
|
void load();
|
|
return root;
|
|
}
|