/* ============================================================================= * M02_MasterTemplete_Layer.ts * M02 한 화면의 뿌리 셋(마스터 · 회사 · 개인 · PLAN 27-1) — 진입 해시 * `#/m02-master-template?open=구조물조합|일위대가|옵션|도면&root=master|company|personal&owner=…` * (옛 `?layer=` 도 root 로 읽음) · 고칠 권한(`accessFor` — 서버 `M02_Layer_Access` 와 짝 · 27-1c) · * 회사 없음 안내 · 회사용 읽기만일 때 [개인용으로 복사] 막대. * ========================================================================== */ import { ROUTES } from "@config/config_frontend"; import { createButton, el, showToast } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { currentQuery, navigateTo } from "../A00_Common/router"; import type { SessionUser } from "../A06_Login/A06_Login_Api_Fetch"; import { copyItemToPersonal, fetchStructureCombos } from "./M02_MasterTemplete_Api_Fetch"; export type ManageLayer = "system" | "company" | "personal"; /** 해시 · 화면의 뿌리 이름 — 서버 층 이름은 `layerOfRoot` */ export type Root = "master" | "company" | "personal"; export type OpenTarget = "구조물조합" | "일위대가" | "옵션" | "도면"; export interface Entry { root: Root; /** 남의 층 주인(회사 `{C}` · 개인 `{C}/{U}`) — 자기 층 = null */ owner: string | null; open: OpenTarget; } const OPENS: OpenTarget[] = ["구조물조합", "일위대가", "옵션", "도면"]; export const layerOfRoot = (root: Root): ManageLayer => (root === "master" ? "system" : root); /** 주소 인자에서 진입 — 모르는 값 · 없음 = 마스터 · 구조물 조합 */ export function entryFromHash(): Entry { const query = currentQuery(); const raw = query.get("root") ?? query.get("layer"); const root: Root = raw === "company" || raw === "personal" ? raw : "master"; const open = OPENS.find((one) => one === query.get("open")) ?? "구조물조합"; return { root, owner: root === "master" ? null : query.get("owner") || null, open }; } /** 진입 해시 인자 — `navigateTo(ROUTES.M02_MASTER_TEMPLATE, entryQuery(…))` */ export const entryQuery = (entry: Entry): Record => ({ open: entry.open, root: entry.root, ...(entry.owner ? { owner: entry.owner } : {}), }); /** 자기 층 주인 글 — 회사 없으면 null */ export const ownOwner = (layer: ManageLayer, user: SessionUser | null): string | null => !user?.company_id || layer === "system" ? null : layer === "company" ? String(user.company_id) : `${user.company_id}/${user.id}`; export interface LayerAccess { /** 이 층을 고칠 수 있나 — 서버도 같은 규칙으로 막음 */ canWrite: boolean; /** 회사 · 개인 자기 층인데 회사가 없음 — 「회사부터 등록」 안내 */ needCompany: boolean; } /** 서버 `M02_Layer_Access.grant` 와 같은 규칙 — 남의 층(`owner`)은 늘 읽기만(읽을 수 있나는 서버가 가름) */ export function accessFor( layer: ManageLayer, owner: string | null, user: SessionUser | null, ): LayerAccess { if (layer === "system") return { canWrite: user?.role === "SYSTEM_ADMIN", needCompany: false }; const own = ownOwner(layer, user); if (owner && owner !== own) return { canWrite: false, needCompany: false }; if (!own) return { canWrite: false, needCompany: true }; const companyAdmin = user?.role === "ADMIN" || user?.role === "SYSTEM_ADMIN" || !!user?.is_master; return { canWrite: layer === "personal" || companyAdmin, needCompany: false }; } /** 회사 없음 — 개인 · 회사 층은 회사 폴더 아래라 회사부터 */ export function buildNeedCompany(): HTMLElement { const go = createButton({ label: L("M02_NeedCompanyGo"), variant: "filled" }); go.addEventListener("click", () => navigateTo(ROUTES.B01_ACCOUNT)); return el("div", { className: "m02-layer-need", children: [el("p", { text: L("M02_NeedCompany") }), go], }); } export interface ReadOnlyBar { root: HTMLElement; /** 집계표에서 고른 열 id — 없으면 null */ setColumn: (column: string | null) => void; } /** 회사용을 읽기만 볼 때 — 안내 + [개인용으로 복사](고른 열 하나를 내 개인 층으로) */ export function buildReadOnlyBar(user: SessionUser | null): ReadOnlyBar { let column: string | null = null; const copy = createButton({ label: L("M02_CopyToPersonal"), variant: "filled", size: "sm" }); copy.disabled = true; copy.addEventListener("click", async () => { if (!column) return showToast(L("M02_CopyPickFirst"), "error"); copy.disabled = true; try { const key = (await fetchStructureCombos()).줄.find((row) => row.열 === column)?.키; if (!key) throw new Error(L("M02_CopyPickFirst")); await copyItemToPersonal({ fromLayer: "company", fromOwner: user?.company_id == null ? null : String(user.company_id), key, }); showToast(L("M02_CopyDone"), "success"); } catch (error) { const why = error instanceof Error ? error.message : ""; showToast(L("M02_CopyFailed").replace("{value}", why), "error"); } finally { copy.disabled = !column; } }); return { root: el("div", { className: "m02-layer-bar", children: [el("span", { text: L("M02_ReadOnlyCompany") }), copy], }), setColumn: (id) => { column = id; copy.disabled = !id; }, }; }