import { ROUTES } from "@config/config_frontend"; import { attachCollapsible } from "@ui/ui_template_collapsible"; import { createButton, createTag, hideLoadingOverlay, showLoadingOverlay, showToast, } from "@ui/ui_template_elements"; import { section } from "@ui/ui_template_general_blocks"; import { createGeneralLayout } from "@ui/ui_template_general_layout"; import { navigateTo } from "../A00_Common/router"; import { fetchAllCompanies, fetchAllUsers, fetchAuditLogs, fetchCompanyJoinRequests, fetchCompanyMembers, fetchCompanyProjects, fetchDashboardMe, fetchSystemResources, fetchUserCompany, fetchUserProjects, type AuditLog, type CompanyInfo, type DashboardUser, type JoinRequest, type Member, type ProjectItem, type ResourceData, } from "./B01_Dashboard_Api_Fetch"; import { auditLogTable, userTable } from "./B01_Dashboard_UI_Admin"; import { L, roleLabel } from "./B01_Dashboard_UI_Common"; import { buildCompanyPanel, companyTable, joinRequestTable, memberTable, } from "./B01_Dashboard_UI_Company"; import { openAddMemberModal, openCreateCompanyModal } from "./B01_Dashboard_UI_Modals"; import { buildProfileForm, buildSecurityForm } from "./B01_Dashboard_UI_Profile"; import { openCompanyPicker } from "./B01_Dashboard_UI_CompanyPick"; import { projectTable } from "./B01_Dashboard_UI_Projects"; import { buildResourcePanel } from "./B01_Dashboard_UI_Resources"; import { buildTempUploadSection } from "./B01_Dashboard_UI_TempUpload"; import "./B01_Dashboard_UI_Style.css"; export interface DashboardState { user: DashboardUser; userProjects: ProjectItem[]; companyProjects: ProjectItem[]; allProjects: ProjectItem[]; company: CompanyInfo | null; members: Member[]; joinRequests: JoinRequest[]; allCompanies: CompanyInfo[]; allUsers: DashboardUser[]; allJoinRequests: JoinRequest[]; auditLogs: AuditLog[]; resources: ResourceData | null; /** 시스템 관리자가 고른 회사(29-1) — 프로젝트 · 사용자 · 가입 요청 · 회사 관리가 이 회사 것 */ adminCompanyId: number | null; } /** 고른 회사 id 자리 — M02 도 같은 키를 읽음(이름 바꾸지 않음) */ const ADMIN_COMPANY_KEY = "aislo.adminCompany"; /** 고른 회사 — 저장값이 없거나 없는 회사면 자기 회사 */ function pickAdminCompany(user: DashboardUser, companies: CompanyInfo[]): number | null { const saved = Number(sessionStorage.getItem(ADMIN_COMPANY_KEY)); if (companies.some((c) => c.id === saved)) return saved; return user.company_id ?? companies[0]?.id ?? null; } export async function renderB01Dashboard(root: HTMLElement): Promise { showLoadingOverlay(); try { const user = await fetchDashboardMe(); const state: DashboardState = { user, userProjects: [], companyProjects: [], allProjects: [], company: null, members: [], joinRequests: [], allCompanies: [], allUsers: [], allJoinRequests: [], auditLogs: [], resources: null, adminCompanyId: null, }; await loadRoleData(state); root.innerHTML = ""; root.append(buildPage(state)); } catch (error) { showToast(error instanceof Error ? error.message : L("B01_Dashboard_LoadFailed"), "error"); } finally { hideLoadingOverlay(); } } async function loadRoleData(state: DashboardState): Promise { if (state.user.role === "SYSTEM_ADMIN") { // 회사 목록을 먼저 — 고른 회사를 가린 뒤 그 회사 것만 읽음(29-1) state.allCompanies = await fetchAllCompanies().catch(() => []); const companyId = pickAdminCompany(state.user, state.allCompanies); state.adminCompanyId = companyId; const results = await Promise.allSettled([ fetchAllUsers(companyId), fetchCompanyJoinRequests(companyId), fetchAuditLogs(), fetchSystemResources(7), fetchCompanyProjects(companyId), ]); state.allUsers = results[0].status === "fulfilled" ? results[0].value : []; state.allJoinRequests = results[1].status === "fulfilled" ? results[1].value : []; state.auditLogs = results[2].status === "fulfilled" ? results[2].value : []; state.resources = results[3].status === "fulfilled" ? results[3].value : null; state.allProjects = results[4].status === "fulfilled" ? results[4].value : []; results.forEach((res, idx) => { if (res.status === "rejected") { console.error(`System Admin API failed at index ${idx}:`, res.reason); } }); return; } const [projects, company] = await Promise.all([ fetchUserProjects().catch(() => []), fetchUserCompany().catch(() => null), ]); state.userProjects = projects; state.company = company; if (state.user.role === "ADMIN") { const [members, requests, companyProjects] = await Promise.all([ fetchCompanyMembers().catch(() => []), fetchCompanyJoinRequests().catch(() => []), fetchCompanyProjects().catch(() => []), ]); state.members = members; state.joinRequests = requests; state.companyProjects = companyProjects; } } /** 설정 카드 공용 양식 — 제목 + 한 줄 설명 · 누르면 이동 */ function settingCard(title: string, desc: string, go: () => void): HTMLElement { const card = document.createElement("button"); card.type = "button"; card.className = "b01-dashboard__setting-card"; const head = document.createElement("strong"); head.className = "b01-dashboard__setting-title"; head.textContent = title; const sub = document.createElement("span"); sub.className = "b01-dashboard__setting-desc"; sub.textContent = desc; card.append(head, sub); card.addEventListener("click", go); return card; } /** 시스템 설정 — 역할로 카드 숨김 · M02 카드는 `?open=&root=` 로 진입(27-1d) */ function buildSystemSettingsPanel(user: DashboardUser, companyId: number | null): HTMLElement { const wrap = document.createElement("div"); wrap.className = "b01-dashboard__setting-grid"; // 시스템 관리자는 고른 회사를 해시에 실음(29-1) const company: Record = user.role === "SYSTEM_ADMIN" && companyId ? { company: String(companyId) } : {}; const m02 = (title: string, open: string, root: string, desc: string): HTMLElement => settingCard(title, desc, () => navigateTo(ROUTES.M02_MASTER_TEMPLATE, { open, root, ...company }), ); if (user.role === "SYSTEM_ADMIN") { const desc = L("B01_Dashboard_PermMasterWrite"); wrap.append( settingCard(L("B01_Dashboard_MasterElements"), L("B01_Dashboard_PermSourceRead"), () => navigateTo(ROUTES.M01_MASTER_DATA), ), m02(L("B01_Dashboard_CardUnitCombo"), "일위대가", "master", desc), m02(L("B01_Dashboard_CardStructCombo"), "구조물조합", "master", desc), m02(L("B01_Dashboard_CardStructOption"), "옵션", "master", desc), m02(L("B01_Dashboard_CardDrawing"), "도면", "master", desc), ); return wrap; } // 회사 수정 = 회사 관리자 · 회사 마스터(M02 accessFor 와 같은 규칙) const companyAdmin = user.role === "ADMIN" || user.is_master; const root = companyAdmin ? "company" : "personal"; const desc = [ L("B01_Dashboard_PermMasterRead"), L(companyAdmin ? "B01_Dashboard_PermCompanyWrite" : "B01_Dashboard_PermCompanyRead"), L("B01_Dashboard_PermPersonalWrite"), ].join(" · "); wrap.append( m02(L("B01_Dashboard_CardUnitCombo"), "일위대가", root, desc), m02(L("B01_Dashboard_CardStructCombo"), "구조물조합", root, desc), ); return wrap; } /** 회사 알약(시스템 관리자만) — 컨테이너 제목 바로 옆 · 누르면 조회 후 고르기 · 고르면 저장 뒤 다시 읽음 */ function withCompanyPill(card: HTMLElement, state: DashboardState): HTMLElement { const name = state.allCompanies.find((c) => c.id === state.adminCompanyId)?.name ?? "-"; const pill = document.createElement("button"); pill.type = "button"; pill.className = "ui-tag ui-tag--accent b01-dashboard__company-pill"; pill.title = L("B01_Dashboard_PickCompany"); pill.textContent = `${name} ▾`; pill.addEventListener("click", (event) => { // 제목 줄 누름 = 접힘 — 알약 누름은 올려 보내지 않음 event.stopPropagation(); openCompanyPicker(state.allCompanies, state.adminCompanyId, (company) => { sessionStorage.setItem(ADMIN_COMPANY_KEY, String(company.id)); window.dispatchEvent(new HashChangeEvent("hashchange")); }); }); card.querySelector(".b01-dashboard__section-header > .ui-card__title")?.after(pill); return card; } /** 시스템 설정 컨테이너 — 모든 역할 · 한 줄 통째 · 제목 눌러 통째로 접힘 */ function buildSystemSettingsSection(state: DashboardState): HTMLElement { const { user } = state; const panel = buildSystemSettingsPanel(user, state.adminCompanyId); const card = section(L("B01_Dashboard_SystemSettings"), panel, true); if (user.role === "SYSTEM_ADMIN") withCompanyPill(card, state); card.querySelector(".ui-card__body")?.classList.add("ui-collapsible"); card.querySelector(".b01-dashboard__section-header")?.classList.add("ui-collapsible__title"); attachCollapsible(card); return card; } function buildPage(state: DashboardState): HTMLElement { // 제목·여백은 공용 템플릿을 따른다 (2026-09-06 사용자 지시) — B02 등 다른 화면과 같은 모양. const page = document.createElement("div"); const grid = document.createElement("div"); grid.className = "b01-dashboard__grid"; if (state.user.role === "SYSTEM_ADMIN") { // 시스템 설정은 리소스 현황 아래(29-1) · 회사 알약이 붙은 컨테이너는 고른 회사 것 const picked = state.allCompanies.filter((c) => c.id === state.adminCompanyId); grid.append( section(L("B01_Dashboard_Resources"), buildResourcePanel(state.resources), true), buildSystemSettingsSection(state), withCompanyPill( section(L("B01_Dashboard_Projects"), projectTable(state.allProjects, state.user), true, [ createButton({ label: "+", onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER) }), ]), state, ), buildTempUploadSection(), withCompanyPill( section(L("B01_Dashboard_Users"), userTable(state.allUsers, state.user), true, [ createButton({ label: "+", onClick: () => openAddMemberModal(undefined, state.adminCompanyId), }), ]), state, ), withCompanyPill( section( L("B01_Dashboard_JoinRequests"), joinRequestTable(state.allJoinRequests, true), true, ), state, ), withCompanyPill( section(L("B01_Dashboard_Companies"), companyTable(picked, state.user), true, [ createButton({ label: "+", onClick: () => openCreateCompanyModal() }), ]), state, ), ); } else if (state.user.role === "ADMIN") { // 시스템 설정은 맨 위 한 줄(27-3a) grid.append( buildSystemSettingsSection(state), section(L("B01_Dashboard_Projects"), projectTable(state.companyProjects, state.user), true, [ createButton({ label: "+", onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER) }), ]), buildTempUploadSection(), section(L("B01_Dashboard_Members"), memberTable(state.members, state.user), true, [ createButton({ label: "+", onClick: () => openAddMemberModal() }), ]), section(L("B01_Dashboard_JoinRequests"), joinRequestTable(state.joinRequests, false), true), section(L("B01_Dashboard_Company"), buildCompanyPanel(state), true), ); } else { grid.append( buildSystemSettingsSection(state), section(L("B01_Dashboard_Projects"), projectTable(state.userProjects, state.user), true, [ createButton({ label: "+", onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER) }), ]), // 임시 보관함 — 프로젝트를 만들기 전에 자료를 올려 두는 곳이라 프로젝트 목록 // 바로 아래에 둔다(2026-08-08 사용자 지시). buildTempUploadSection(), section(L("B01_Dashboard_Company"), buildCompanyPanel(state), true), ); } grid.append( section(L("B01_Dashboard_Profile"), buildProfileForm(state.user)), section(L("B01_Account_Section_Security"), buildSecurityForm()), ); // 시스템 로그는 맨 아래 (2026-09-06 사용자 지시). if (state.user.role === "SYSTEM_ADMIN") { grid.append(section(L("B01_Dashboard_AuditLogs"), auditLogTable(state.auditLogs), true)); } page.append(grid); const layout = createGeneralLayout({ pageClass: "b01-dashboard", title: L("B01_Dashboard_Title"), subtitle: L("B01_Dashboard_Subtitle"), content: page, }); // 역할 배지는 제목 줄 오른쪽에 둔다(자리는 CSS 격자가 잡는다). const tag = createTag( roleLabel(state.user.role), state.user.role === "SYSTEM_ADMIN" ? "accent" : "neutral", ); tag.classList.add("b01-dashboard__role"); layout.root.querySelector(".ui-general-layout__header")?.append(tag); return layout.root; }