- 해시 ?tab= 탭 줄(B08 단추 꼴) · 고른 탭 자료만 받음 · 설정 카드는 탭 줄 위 · 회사 고르기 알약은 탭 줄 오른쪽 하나 - 회사 없음 진입 두 곳(router · M02 Layer)은 ?tab=company · 회사 관리자 프로젝트 목록 한 번만 받음 - 개요 숫자 카드(단계별 · 실패 · 재확인 · 가입 대기 · 보관함 · 리소스) · 프로젝트 표 칸 보강 · 찾기 · 거르기 · 쪽 넘김 · 진행 현황 - 회사 정보 칸(사업자번호 · 대표 · 상태) · 팀원 읽기 · 자산 표 · 내 계정 마지막 로그인 · 시스템 리소스 7·30·90일 · 활성 추이 · 전체 회사 · 사용자 · 로그 쪽 · 고객 문의 - 시험 test_b01_dashboard_tabs 신규 · test_b01_settings_cards 고침 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7
135 lines
5.2 KiB
TypeScript
135 lines
5.2 KiB
TypeScript
/* =============================================================================
|
|
* 「시스템」 탭(92장 · 시스템 관리자만) — 리소스(7 · 30 · 90일) · 활성 추이 · 전체 회사 ·
|
|
* 전체 사용자 · 시스템 로그(쪽 넘김) · 고객 문의. 모두 이미 있는 API.
|
|
* ========================================================================== */
|
|
|
|
import { createButton, createTag, showToast, type TagVariant } from "@ui/ui_template_elements";
|
|
import { section, text } from "@ui/ui_template_general_blocks";
|
|
import {
|
|
fetchAuditLogPage,
|
|
fetchSystemResources,
|
|
type AuditLog,
|
|
type ResourceData,
|
|
type SupportRequest,
|
|
} from "./B01_Dashboard_Api_Fetch";
|
|
import { allUserTable, auditLogTable } from "./B01_Dashboard_UI_Admin";
|
|
import { companyTable } from "./B01_Dashboard_UI_Company";
|
|
import { formatDate, formatTime, L, stackedCell } from "./B01_Dashboard_UI_Common";
|
|
import type { DashboardState } from "./B01_Dashboard_UI_Page";
|
|
import { buildPager, createPagedTable } from "./B01_Dashboard_UI_Paging";
|
|
import { buildActivityChart, buildResourcePanel } from "./B01_Dashboard_UI_Resources";
|
|
|
|
/** 리소스 기간(일) — 백엔드 최대 90 */
|
|
export const RESOURCE_DAYS = [7, 30, 90] as const;
|
|
/** 시스템 로그 한 쪽 줄 수 */
|
|
export const AUDIT_PAGE_SIZE = 20;
|
|
|
|
const INQUIRY_TAG: Record<SupportRequest["status"], TagVariant> = {
|
|
NEW: "warning",
|
|
REVIEWED: "accent",
|
|
RESOLVED: "success",
|
|
DELETED: "neutral",
|
|
};
|
|
|
|
/** 기간 고르기 + 리소스 현황 + 활성 추이 — 기간을 바꾸면 이 두 컨테이너만 새로 받음 */
|
|
function buildResourceSections(initial: ResourceData | null): HTMLElement[] {
|
|
const bar = document.createElement("div");
|
|
bar.className = "b01-dashboard__period";
|
|
const panelHost = document.createElement("div");
|
|
const trendHost = document.createElement("div");
|
|
let days: number = RESOURCE_DAYS[0];
|
|
let latest = 0;
|
|
|
|
const draw = (data: ResourceData | null): void => {
|
|
panelHost.replaceChildren(buildResourcePanel(data, days));
|
|
trendHost.replaceChildren(buildActivityChart(data, days));
|
|
};
|
|
const drawBar = (): void => {
|
|
bar.replaceChildren(
|
|
...RESOURCE_DAYS.map((value) => {
|
|
const button = createButton({
|
|
label: L("B01_Sy_Days").replace("{days}", String(value)),
|
|
variant: value === days ? "filled" : "ghost",
|
|
onClick: () => void pick(value),
|
|
});
|
|
button.setAttribute("aria-pressed", String(value === days));
|
|
button.dataset.b01Days = String(value);
|
|
return button;
|
|
}),
|
|
);
|
|
};
|
|
const pick = async (value: number): Promise<void> => {
|
|
if (value === days) return;
|
|
days = value;
|
|
drawBar();
|
|
const mine = ++latest;
|
|
try {
|
|
const data = await fetchSystemResources(value);
|
|
if (mine === latest) draw(data);
|
|
} catch (error) {
|
|
if (mine === latest) showToast(error instanceof Error ? error.message : "", "error");
|
|
}
|
|
};
|
|
drawBar();
|
|
draw(initial);
|
|
const resource = section(L("B01_Dashboard_Resources"), panelHost, true);
|
|
resource.querySelector(".ui-card__body")?.prepend(bar);
|
|
return [resource, section(L("B01_Sy_ActiveTrend"), trendHost, true)];
|
|
}
|
|
|
|
/** 시스템 로그 — 서버가 쪽으로 나눠 줌(전체 건수 포함) */
|
|
function buildAuditSection(first: { total: number; items: AuditLog[] }): HTMLElement {
|
|
const host = document.createElement("div");
|
|
const draw = (page: number, data: { total: number; items: AuditLog[] }): void => {
|
|
const pages = Math.max(1, Math.ceil(data.total / AUDIT_PAGE_SIZE));
|
|
host.replaceChildren(
|
|
auditLogTable(data.items),
|
|
buildPager(page, pages, data.total, (next) => {
|
|
fetchAuditLogPage(next, AUDIT_PAGE_SIZE)
|
|
.then((result) => draw(next, result))
|
|
.catch((error) => showToast(error instanceof Error ? error.message : "", "error"));
|
|
}),
|
|
);
|
|
};
|
|
draw(1, first);
|
|
return section(L("B01_Dashboard_AuditLogs"), host, true);
|
|
}
|
|
|
|
function inquiryTable(requests: SupportRequest[]): HTMLElement {
|
|
return createPagedTable(
|
|
[
|
|
L("B01_Sy_Received"),
|
|
L("B01_Sy_Sender"),
|
|
L("B01_Dashboard_Table_Company"),
|
|
L("B01_Sy_Subject"),
|
|
L("B01_Pj_Message"),
|
|
L("B01_Dashboard_Table_Status"),
|
|
],
|
|
(request: SupportRequest) => {
|
|
const message = document.createElement("span");
|
|
message.className = "b01-dashboard__ellipsis";
|
|
message.textContent = request.message;
|
|
message.title = request.message;
|
|
return [
|
|
stackedCell(formatDate(request.created_at), formatTime(request.created_at)),
|
|
text([request.user_name, request.user_email].filter(Boolean).join(" · ")),
|
|
text(request.company_name),
|
|
text(request.subject),
|
|
message,
|
|
createTag(L(`B01_Sy_Inquiry_${request.status}`), INQUIRY_TAG[request.status]),
|
|
];
|
|
},
|
|
requests,
|
|
).root;
|
|
}
|
|
|
|
export function buildSystemTab(state: DashboardState): HTMLElement[] {
|
|
return [
|
|
...buildResourceSections(state.resources),
|
|
section(L("B01_Sy_AllCompanies"), companyTable(state.allCompanies, state.user), true),
|
|
section(L("B01_Sy_AllUsers"), allUserTable(state.systemUsers), true),
|
|
buildAuditSection({ total: state.auditTotal, items: state.auditLogs }),
|
|
section(L("B01_Sy_Inquiries"), inquiryTable(state.supportRequests), true),
|
|
];
|
|
}
|