/* ============================================================================= * M02_MasterTemplete_TreePanel_Roots.ts * 라이브러리 한 화면의 뿌리 셋(마스터 · 회사 · 개인 · PLAN 27-1b · c) — 나무 종류와 무관한 공용 부품 * (구조물 라이브러리 · 일위대가 라이브러리가 함께 씀). 고른 뿌리(`active`) 아래에 부르는 쪽 나무(`panel`)를 붙임. * 시스템 관리자 「회사」 → 회사 목록 → (회사 공용 · 그 회사 사람) * 회사 관리자 「개인」 → (내 것 · 같은 회사 사람) * 그 밖 마스터 · 회사 · 개인 한 줄씩 * 목록은 펼칠 때 불러옴(`fetchOwners`) · 다른 뿌리를 누르면 `go`(부르는 쪽이 해시로 다시 엶) · * 읽기만 뿌리는 줄 끝에 「읽기」(권한 = `accessFor` · 서버 `M02_Layer_Access` 와 짝). * ========================================================================== */ import { el } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { createTreeRow } from "@ui/ui_template_tree"; import type { SessionUser } from "../A06_Login/A06_Login_Api_Fetch"; import { fetchOwners, type LayerOwner } from "./M02_MasterTemplete_Api_Fetch"; import { accessFor, buildNeedCompany, layerOfRoot, ownOwner, type Root, } from "./M02_MasterTemplete_Layer"; export interface RootsOptions { user: SessionUser | null; /** 지금 고른 뿌리 — `owner` null = 자기 층 */ active: { root: Root; owner: string | null }; /** 고른 뿌리 아래에 붙일 나무 */ panel: HTMLElement; /** 다른 뿌리를 누름 — `owner` null = 자기 층 */ go: (root: Root, owner: string | null) => void; } const hint = (text: string): HTMLElement => el("p", { className: "m02-tree__hint", text }); const why = (error: unknown): string => (error instanceof Error ? error.message : ""); export function createLibraryRoots(opt: RootsOptions): HTMLElement { const { user, active } = opt; const isSystem = user?.role === "SYSTEM_ADMIN"; const companyAdmin = isSystem || user?.role === "ADMIN" || !!user?.is_master; const ownCompany = ownOwner("company", user); const activeCompany = active.owner?.split("/")[0] ?? ownCompany; /** 층 하나 — 누르면 그 뿌리로 · 고른 뿌리면 아래에 나무(누르면 접기/펴기) */ function leaf(label: string, root: Root, owner: string | null, depth: number): HTMLElement[] { const current = root === active.root && owner === active.owner; const readOnly = !accessFor(layerOfRoot(root), owner, user).canWrite; const text = readOnly ? `${label} · ${L("M02_RootReadOnly")}` : label; const wrap = el("div", { className: "m02-roots__panel", children: current ? [opt.panel] : [] }); wrap.style.paddingLeft = `calc(${depth + 1} * var(--spacing-16))`; const row = createTreeRow( { id: `${root}|${owner}`, label: text, count: null }, "", depth, () => { if (!current) return opt.go(root, owner); wrap.hidden = !wrap.hidden; }, ); row.classList.toggle("is-active", current); return current ? [row, wrap] : [row]; } /** 묶음 — 펼칠 때 한 번 불러옴(`load`) · `open` = 고른 뿌리가 안에 있어 처음부터 펼침 */ function group( label: string, depth: number, open: boolean, load: () => Promise, ): HTMLElement { const kids = el("div", { className: "ui-tree__kids" }); kids.hidden = !open; let loaded = false; const fill = async (): Promise => { loaded = true; kids.replaceChildren(hint(L("M02_PickerLoading"))); try { const nodes = await load(); kids.replaceChildren(...(nodes.length ? nodes : [hint(L("M02_RootNone"))])); } catch (error) { loaded = false; kids.replaceChildren(hint(L("M02_PickerLoadFailed").replace("{value}", why(error)))); } }; const row = createTreeRow({ id: label, label, count: null }, open ? "▾" : "▸", depth, () => { kids.hidden = !kids.hidden; const caret = row.querySelector(".ui-tree__caret"); if (caret) caret.textContent = kids.hidden ? "▸" : "▾"; if (!kids.hidden && !loaded) void fill(); }); if (open) void fill(); return el("div", { className: "ui-tree__branch", children: [row, kids] }); } /** 목록에 없는 고른 층(해시로 바로 들어옴 · 탈퇴한 사람 등)도 줄을 세움 — 나무를 붙일 자리 */ const withActive = (list: LayerOwner[], owner: string | null, fits: boolean): LayerOwner[] => owner && fits && !list.some((one) => one.owner === owner) ? [...list, { owner, name: owner }] : list; /** 그 회사 사람들의 개인 층(본인 뺌) */ const people = async (company: string | null, depth: number): Promise => { const fits = active.root === "personal" && activeCompany === (company ?? ownCompany); return withActive(await fetchOwners("personal", company), active.owner, fits).flatMap((one) => leaf(one.name, "personal", one.owner, depth), ); }; const master = leaf(L("M02_PickerSystem"), "master", null, 0); if (!ownCompany && !isSystem) { return el("div", { className: "m02-roots", children: [...master, buildNeedCompany()] }); } // 시스템 관리자 — 회사 목록 → 회사 공용 · 그 회사 사람 const companies = (): HTMLElement => group(L("M02_PickerCompany"), 0, active.root === "company" || !!active.owner, async () => withActive(await fetchOwners("company"), activeCompany, !!active.owner).map((one) => { const mine = one.owner === ownCompany; const here = !!active.owner || active.root === "company"; return group(one.name, 1, here && activeCompany === one.owner, async () => [ ...leaf(L("M02_RootShared"), "company", mine ? null : one.owner, 2), ...(await people(one.owner, 2)), ]); }), ); const company = isSystem ? [companies()] : leaf(L("M02_PickerCompany"), "company", null, 0); // 회사 관리자 — 개인 → 내 것 · 같은 회사 사람 · 시스템 관리자는 남의 개인을 회사 아래에서 봄 const personal = !ownCompany ? [] : companyAdmin && !isSystem ? [ group(L("M02_PickerPersonal"), 0, active.root === "personal", async () => [ ...leaf(L("M02_RootMine"), "personal", null, 1), ...(await people(null, 1)), ]), ] : leaf(L("M02_PickerPersonal"), "personal", null, 0); return el("div", { className: "m02-roots", children: [...master, ...company, ...personal] }); }