/* ============================================================================= * M02_MasterTemplete_UI_Page.ts * 마스터 템플릿 · 구조물 관리 화면 — 좌측 도킹 패널(양식 목록 · 층 단추 · 고른 층의 나무) / * 우측 편집 칸(표·도면 부품 · 「구조물 조합」이면 위쪽에 나무 찾기 칸 · PLAN 29-2) * * 한 화면에 층 셋(마스터 · 회사 · 개인 · PLAN 27-1) — 고른 층 = 진입 해시 `?open=&root=&owner=&company=` * (`M02_MasterTemplete_Layer.ts`) · 층 단추 · 「사람 고르기」를 누르면 해시를 바꿔 이 화면을 다시 세움. * 화면은 로그인만 확인 — 고치기는 층별 권한일 때만 켬(서버도 막음 · `accessFor`). * ========================================================================== */ import "@ui/ui_template_workflow_layout.css"; import { ROUTES } from "@config/config_frontend"; import { el } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { createWorkflowOverlays } from "@ui/ui_template_overlay"; import { navigateTo, type MountedPage } from "../A00_Common/router"; import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch"; import { fetchOwners, setActiveLayer } from "./M02_MasterTemplete_Api_Fetch"; import { accessFor, buildReadOnlyBar, entryFromHash, entryQuery, layerOfRoot, ownOwner, viewCompany, type OpenTarget, type Root, } from "./M02_MasterTemplete_Layer"; import { buildLaborLibrary, buildLaborSection } from "./M02_MasterTemplete_Labor"; import { STRUCTURE_TABLE, pickCombo } from "./M02_MasterTemplete_Structure"; import { createTreePanel } from "./M02_MasterTemplete_TreePanel"; import { companyOwner, createLayerBar, createPeopleTree, seesOthers, } from "./M02_MasterTemplete_TreePanel_Roots"; import { canEdit, createMain, newItemButton, type Selection } from "./M02_MasterTemplete_UI_Main"; import { buildSide } from "./M02_MasterTemplete_UI_Side"; import "./M02_MasterTemplete_UI_Style.css"; export async function renderM02MasterTemplate(root: HTMLElement): Promise { const user = await fetchSessionUser().catch(() => null); const entry = entryFromHash(); const company = viewCompany(entry, user); // 볼 회사(시스템 관리자는 고른 회사) let layer = layerOfRoot(entry.root); // 회사 층 주인 = 볼 회사 · 자기 층 주인을 적어도 자기 층 · 회사 없는 사람의 회사 · 개인 = 마스터(단추 자리에 안내) let owner = entry.owner ?? (entry.root === "company" ? companyOwner(company, user) : null); if (owner === ownOwner(layer, user)) owner = null; // 시스템 관리자가 남의 회사 「개인」 — 그 회사 관리자처럼 사람 나무(PLAN 29-6) · 사람을 안 골랐으면 첫 사람 · // 사람이 없으면 마스터 const people = entry.root === "personal" && seesOthers(company, user); if (people && !owner) { owner = (await fetchOwners("personal", company).catch(() => []))[0]?.owner ?? null; entry.owner = owner; if (!owner) [entry.root, layer] = ["master", "system"]; } if (accessFor(layer, owner, user).needCompany) { [entry.root, layer, owner] = ["master", "system", null]; } setActiveLayer(layer, owner); const isAdmin = accessFor(layer, owner, user).canWrite; // 이 층을 고칠 수 있나(하위 부품의 「관리자」) // 자기 회사용을 읽기만 볼 때 — 안내 + [개인용으로 복사] const readOnlyBar = layer === "company" && !owner && !isAdmin ? buildReadOnlyBar(user) : null; // 고른 층의 나무 — 고르기 모달과 같은 폴더 나무 · 좌측 「구조물 조합」 줄 아래 // 「〇〇로 복사」(30-2) — 이 사람이 쓸 수 있는 자기 층(지금 보는 층은 뺌) 맨 위로 바로 const copyTo = (["master", "company", "personal"] as Root[]).flatMap((one) => { const to = layerOfRoot(one); if (!accessFor(to, null, user).canWrite || (to === layer && !owner)) return []; const key = one === "master" ? "Master" : one === "company" ? "Company" : "Personal"; return [{ layer: to, owner: null, label: L(`M02_TreeCopyTo${key}`) }]; }); const tree = createTreePanel({ layer, owner, copyTo, pickCombo: () => pickCombo(L("M02_TreePickCombo")), // 항목 추가 창 일위대가(31-2) canEdit: canEdit(layer, isAdmin), onPick: (item) => void main.openTable(layer, item.열), settle: () => main.settle(), onChanged: (colId) => main.openTable(layer, colId, true), }); // 일위대가 화면이 떠 있나 — 층을 바꿔도 일위대가로 다시 엶 · // 해시 `open` 도 맞춤(새로 고침 · 카드 진입과 같게) · 옵션 · 도면은 옛 규칙(구조물 조합으로) let view: OpenTarget = entry.open; const mark = (open: OpenTarget): void => { view = open; const query = new URLSearchParams(entryQuery({ ...entry, open })); history.replaceState(history.state, "", `#/${ROUTES.M02_MASTER_TEMPLATE}?${query}`); }; const go = async (next: Root, nextOwner: string | null, open: OpenTarget): Promise => { // 같은 주소 안 이동은 머리 가드가 안 물음 — 저장 안 한 고침은 여기서 if (!(await main.confirmLeave())) return; navigateTo( ROUTES.M02_MASTER_TEMPLATE, entryQuery({ ...entry, open, root: next, owner: nextOwner }), ); }; // 층 단추 셋(PLAN 29-2) — 「일위대가 라이브러리」 · 「구조물 조합」 줄 아래 한 벌씩 // 단추 옆 「+」 = 그 나무 맨 위 더하기(뿌리 줄을 갈음 · 29-6) // 켜진 단추를 다시 누르면 그 층 목록 화면(30-4 · 구조물 = 집계표 · 일위대가 = 조합 목록) const layerBar = (open: OpenTarget, add: HTMLElement, reselect: () => void): HTMLElement => createLayerBar({ user, active: { root: entry.root, owner }, company, go: (next, nextOwner) => void go(next, nextOwner, open), add, reselect, }); // 남의 회사 「개인」 — 고른 사람 줄 아래에 그 사람 나무(사람 줄 → 라이브러리 · 29-6) const peopleTree = (open: OpenTarget, panel: HTMLElement): HTMLElement => people && owner && company ? createPeopleTree({ company, active: owner, panel, go: (next) => void go("personal", next, open), }) : panel; const main = createMain( isAdmin, () => void Promise.all([side.refresh(), tree.refresh()]), () => { side.paint(); // 일위대가 화면에서 양식으로 옮김(일위대가 · 옵션 화면은 current 없음) if (main.current() && view === "일위대가") mark("구조물조합"); }, { top: readOnlyBar?.root, search: tree.search, // 「새 항목」(30-5) — 제목 줄 오른쪽 끝 · 쓸 수 있는 층만 · 나무 「항목 추가」와 같은 창 ...(canEdit(layer, isAdmin) ? { newItem: newItemButton(() => tree.newItem()) } : {}), onColumn: (column) => { readOnlyBar?.setColumn(column); tree.select(column); }, }, ); // 첫 화면 · 닫은 뒤(양식 지움 · 자재 조합 상세 닫음) = 구조물집계표 — 빈 화면 없음 const home: Selection = { layer, projectId: null, kind: "table", name: STRUCTURE_TABLE, }; const side = buildSide({ layer, isAdmin, onOpen: (sel) => void main.open(sel ?? home), getOpen: main.current, confirmLeave: main.confirmLeave, showView: main.showView, tree: [ layerBar("구조물조합", tree.add, () => void main.openTable(layer)), peopleTree("구조물조합", tree.root), ], labor: ({ onOpen }) => buildLaborSection({ show: main.showView, confirmLeave: main.confirmLeave, onOpen: () => { onOpen(); mark("일위대가"); }, bar: (add, back) => layerBar("일위대가", add, back), wrap: (panel) => peopleTree("일위대가", panel), make: (reveal) => buildLaborLibrary({ scope: { layer, owner }, canEdit: isAdmin, reveal, copyTo }), }), }); const layout = el("div", { className: "ui-workflow-layout m02-master" }); const overlays = createWorkflowOverlays({ title: L("M02_Title"), optionsContent: el("div", { className: "m02-master__side", children: [side.root] }), showProjectName: false, onOptionsOpenChange: (isOpen) => layout.classList.toggle("is-options-open", isOpen), }); layout.append( el("div", { className: "ui-workflow-layout__body", children: [el("main", { className: "ui-workflow-layout__main", children: [main.root] })], }), overlays.root, ); root.innerHTML = ""; root.append(layout); void main.open(home); // 진입 해시 `open` — 옵션 · 도면 컨테이너는 마스터에만 · 일위대가는 세 층 · // 옵션 · 일위대가 화면이 뒤에 열리면 편집 칸 차례(`seq`)가 늦게 온 집계표를 버림 if (entry.open !== "구조물조합") side.reveal(entry.open); void tree.refresh(); // 이 화면을 떠나면 층 길을 시스템 · 자기 층으로 되돌림(다른 화면의 항목 고르기가 남의 층을 안 부르게) return { covers: () => false, show: () => {}, dispose: () => setActiveLayer("system") }; }