Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Layer.ts
T

219 lines
8.6 KiB
TypeScript

/* =============================================================================
* M02_MasterTemplete_Layer.ts
* M02 한 화면의 뿌리 셋(마스터 · 회사 · 개인 · PLAN 27-1) — 진입 해시
* `#/m02-master-template?open=구조물조합|일위대가|옵션|도면&root=master|company|personal&owner=…&company=…`
* (옛 `?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;
/** 시스템 관리자가 보는 회사(PLAN 29 · 없으면 sessionStorage `aislo.adminCompany` · 자기 회사) */
company: string | null;
}
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")) ?? "구조물조합";
const owner = root === "master" ? null : query.get("owner") || null;
return { root, owner, open, company: query.get("company") || null };
}
/** 진입 해시 인자 — `navigateTo(ROUTES.M02_MASTER_TEMPLATE, entryQuery(…))` */
export const entryQuery = (entry: Entry): Record<string, string> => ({
open: entry.open,
root: entry.root,
...(entry.owner ? { owner: entry.owner } : {}),
...(entry.company ? { company: entry.company } : {}),
});
/** 볼 회사 — 시스템 관리자 = 해시 `company` · sessionStorage `aislo.adminCompany`(대시보드가 넣음) ·
* 자기 회사 차례 · 그 밖 = 자기 회사 · 없으면 null */
export function viewCompany(entry: Entry, user: SessionUser | null): string | null {
const own = user?.company_id ? String(user.company_id) : null;
if (user?.role !== "SYSTEM_ADMIN") return own;
let saved: string | null = null;
try {
saved = sessionStorage.getItem("aislo.adminCompany");
} catch {
saved = null;
}
const picked = entry.company ?? saved;
return picked && /^\d+$/.test(picked) ? picked : own;
}
/** 자기 층 주인 글 — 회사 없으면 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 };
}
/** 회사 C 를 맡음 — 시스템 관리자 · 또는 C 의 회사 관리자(서버 `M02_Layer_Access.manages` 짝 · 39-6) */
export function manages(company: string | null, user: SessionUser | null): boolean {
if (!company || !user) return false;
if (user.role === "SYSTEM_ADMIN") return true;
const companyAdmin = user.role === "ADMIN" || !!user.is_master;
return companyAdmin && company === ownOwner("company", user);
}
/** 가져올 곳 탭 하나 — `people` = 다른 사람(주인은 부르는 쪽이 `fetchOwners` 로 고름) */
export interface Source {
id: "master" | "company" | "personal" | "people" | "practice";
layer: ManageLayer;
/** 층 주인 — 자기 층 · 마스터 · 실무문서 · 다른 사람 = null */
owner: string | null;
label: string;
}
/** 복사 생성 · 복사해 가져오기의 탭(39-6 · 답 2) — 읽을 수 있는 곳 + 기본 탭.
* 기본 = 시스템 관리자 마스터 · 그 밖 내 개인(회사 없으면 마스터) · `practice` = 일위대가 고를 때만 */
export function readableSources(
user: SessionUser | null,
company: string | null,
opt: { practice?: boolean } = {},
): { tabs: Source[]; first: Source["id"] } {
const isSystem = user?.role === "SYSTEM_ADMIN";
const own = ownOwner("personal", user);
const tabs: Source[] = [
{ id: "master", layer: "system", owner: null, label: L("M02_PickerSystem") },
...(company
? [
{
id: "company" as const,
layer: "company" as const,
owner: company === ownOwner("company", user) ? null : company,
label: L("M02_PickerCompany"),
},
]
: []),
...(own
? [
{
id: "personal" as const,
layer: "personal" as const,
owner: null,
label: L("M02_PickerPersonal"),
},
]
: []),
...(manages(company, user)
? [
{
id: "people" as const,
layer: "personal" as const,
owner: null,
label: L("M02_SourcePeople"),
},
]
: []),
...(opt.practice && isSystem
? [
{
id: "practice" as const,
layer: "system" as const,
owner: null,
label: L("M02_SourcePractice"),
},
]
: []),
];
return { tabs, first: isSystem || !own ? "master" : "personal" };
}
/** 회사 없음 — 개인 · 회사 층은 회사 폴더 아래라 회사부터 */
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;
},
};
}