260707_4_대시보드 완료
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { ROUTES, type RoutePath } from "@config/config_frontend";
|
||||
import { ROUTES } from "@config/config_frontend";
|
||||
import { isBlank } from "@util/common_util_validate";
|
||||
import { navigateTo } from "../A00_Common/router";
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
@@ -6,16 +6,13 @@ import {
|
||||
createButton,
|
||||
createCard,
|
||||
createInputField,
|
||||
createLineChart,
|
||||
createTag,
|
||||
hideLoadingOverlay,
|
||||
showLoadingOverlay,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import {
|
||||
addCompanyMember,
|
||||
changePassword,
|
||||
createCompany,
|
||||
fetchAllCompanies,
|
||||
fetchAllJoinRequests,
|
||||
fetchAllProjects,
|
||||
@@ -28,10 +25,7 @@ import {
|
||||
fetchSystemResources,
|
||||
fetchUserCompany,
|
||||
fetchUserProjects,
|
||||
joinCompany,
|
||||
processJoinRequest,
|
||||
removeCompanyMember,
|
||||
searchCompanies,
|
||||
updateUserProfile,
|
||||
type AuditLog,
|
||||
type CompanyInfo,
|
||||
@@ -41,6 +35,25 @@ import {
|
||||
type ProjectItem,
|
||||
type ResourceData,
|
||||
} from "./B01_Dashboard_Api_Fetch";
|
||||
import { buildResourcePanel } from "./B01_Dashboard_UI_Resources";
|
||||
import {
|
||||
canEditProject,
|
||||
canDeleteProject,
|
||||
canChangeRole,
|
||||
canDeleteUser,
|
||||
canManageAutomation,
|
||||
} from "./B01_Dashboard_UI_Helper";
|
||||
import {
|
||||
openEditProjectModal,
|
||||
openDeleteProjectModal,
|
||||
openEditUserModal,
|
||||
openChangeRoleModal,
|
||||
openDeleteUserModal,
|
||||
openAutomationModal,
|
||||
openCreateCompanyModal,
|
||||
openFindCompanyModal,
|
||||
openAddMemberModal,
|
||||
} from "./B01_Dashboard_UI_Modals";
|
||||
import "./B01_Dashboard_UI_Style.css";
|
||||
|
||||
const PASSWORD_MIN_LENGTH = 8;
|
||||
@@ -83,6 +96,7 @@ export async function renderB01Dashboard(root: HTMLElement): Promise<void> {
|
||||
resources: null,
|
||||
};
|
||||
await loadRoleData(state);
|
||||
root.innerHTML = "";
|
||||
root.append(buildPage(state));
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : L("B01_Dashboard_LoadFailed"), "error");
|
||||
@@ -93,7 +107,7 @@ export async function renderB01Dashboard(root: HTMLElement): Promise<void> {
|
||||
|
||||
async function loadRoleData(state: DashboardState): Promise<void> {
|
||||
if (state.user.role === "SYSTEM_ADMIN") {
|
||||
const [companies, users, requests, logs, resources, projects] = await Promise.all([
|
||||
const results = await Promise.allSettled([
|
||||
fetchAllCompanies(),
|
||||
fetchAllUsers(),
|
||||
fetchAllJoinRequests(),
|
||||
@@ -101,22 +115,31 @@ async function loadRoleData(state: DashboardState): Promise<void> {
|
||||
fetchSystemResources(7),
|
||||
fetchAllProjects(),
|
||||
]);
|
||||
state.allCompanies = companies;
|
||||
state.allUsers = users;
|
||||
state.allJoinRequests = requests;
|
||||
state.auditLogs = logs;
|
||||
state.resources = resources;
|
||||
state.allProjects = projects;
|
||||
state.allCompanies = results[0].status === "fulfilled" ? results[0].value : [];
|
||||
state.allUsers = results[1].status === "fulfilled" ? results[1].value : [];
|
||||
state.allJoinRequests = results[2].status === "fulfilled" ? results[2].value : [];
|
||||
state.auditLogs = results[3].status === "fulfilled" ? results[3].value : [];
|
||||
state.resources = results[4].status === "fulfilled" ? results[4].value : null;
|
||||
state.allProjects = results[5].status === "fulfilled" ? results[5].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(), fetchUserCompany()]);
|
||||
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(),
|
||||
fetchCompanyJoinRequests(),
|
||||
fetchCompanyProjects(),
|
||||
fetchCompanyMembers().catch(() => []),
|
||||
fetchCompanyJoinRequests().catch(() => []),
|
||||
fetchCompanyProjects().catch(() => []),
|
||||
]);
|
||||
state.members = members;
|
||||
state.joinRequests = requests;
|
||||
@@ -124,6 +147,40 @@ async function loadRoleData(state: DashboardState): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function buildSectionHeader(titleText: string, actionButtons: HTMLElement[] = []): HTMLElement {
|
||||
const header = document.createElement("div");
|
||||
header.className = "b01-dashboard__section-header";
|
||||
header.style.display = "flex";
|
||||
header.style.justifyContent = "space-between";
|
||||
header.style.alignItems = "center";
|
||||
header.style.marginBottom = "var(--spacing-16)";
|
||||
|
||||
const title = document.createElement("h3");
|
||||
title.className = "ui-card__title";
|
||||
title.style.margin = "0";
|
||||
title.textContent = titleText;
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "b01-dashboard__actions";
|
||||
actions.append(...actionButtons);
|
||||
|
||||
header.append(title, actions);
|
||||
return header;
|
||||
}
|
||||
|
||||
function section(
|
||||
title: string,
|
||||
body: HTMLElement,
|
||||
wide = false,
|
||||
actions: HTMLElement[] = [],
|
||||
): HTMLElement {
|
||||
const header = buildSectionHeader(title, actions);
|
||||
const card = createCard({ body: [header, body], raised: true });
|
||||
card.classList.add("b01-dashboard__section");
|
||||
if (wide) card.classList.add("b01-dashboard__section--wide");
|
||||
return card;
|
||||
}
|
||||
|
||||
function buildPage(state: DashboardState): HTMLElement {
|
||||
const page = document.createElement("div");
|
||||
page.className = "b01-dashboard";
|
||||
@@ -133,28 +190,40 @@ function buildPage(state: DashboardState): HTMLElement {
|
||||
grid.className = "b01-dashboard__grid";
|
||||
|
||||
if (state.user.role === "SYSTEM_ADMIN") {
|
||||
// 순서: 리소스 현황(최상단), 프로젝트, 사용자 관리(사용자), 가입요청(1행 전체), 회사관리(회사목록), 시스템 로그(감사로그), 기본정보(프로필), 보안
|
||||
grid.append(
|
||||
section(L("B01_Dashboard_Resources"), buildResourcePanel(state.resources), true),
|
||||
section(L("B01_Dashboard_Projects"), projectTable(state.allProjects), true),
|
||||
section(L("B01_Dashboard_Users"), userTable(state.allUsers), true),
|
||||
section(L("B01_Dashboard_JoinRequests"), joinRequestTable(state.allJoinRequests, true), true),
|
||||
section(L("B01_Dashboard_Companies"), companyTable(state.allCompanies), true),
|
||||
section(L("B01_Dashboard_AuditLogs"), auditLogTable(state.auditLogs), true),
|
||||
);
|
||||
} else if (state.user.role === "ADMIN") {
|
||||
// 순서: 프로젝트, 사용자 관리(멤버), 가입요청(1행 전체), 회사관리(회사패널), 기본정보(프로필), 보안
|
||||
grid.append(
|
||||
section(L("B01_Dashboard_Projects"), projectTable(state.companyProjects), true, [
|
||||
section(L("B01_Dashboard_Projects"), projectTable(state.allProjects, state.user), true, [
|
||||
createButton({
|
||||
label: L("B01_Dashboard_NewProject"),
|
||||
label: "+",
|
||||
onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER),
|
||||
}),
|
||||
]),
|
||||
section(L("B01_Dashboard_Members"), memberTable(state.members), true, [
|
||||
section(L("B01_Dashboard_Users"), userTable(state.allUsers, state.user), true, [
|
||||
createButton({
|
||||
label: L("B01_Dashboard_AddMember"),
|
||||
variant: "ghost",
|
||||
label: "+",
|
||||
onClick: () => openAddMemberModal(),
|
||||
}),
|
||||
]),
|
||||
section(L("B01_Dashboard_JoinRequests"), joinRequestTable(state.allJoinRequests, true), true),
|
||||
section(L("B01_Dashboard_Companies"), companyTable(state.allCompanies), true, [
|
||||
createButton({
|
||||
label: "+",
|
||||
onClick: () => openCreateCompanyModal(),
|
||||
}),
|
||||
]),
|
||||
section(L("B01_Dashboard_AuditLogs"), auditLogTable(state.auditLogs), true),
|
||||
);
|
||||
} else if (state.user.role === "ADMIN") {
|
||||
grid.append(
|
||||
section(L("B01_Dashboard_Projects"), projectTable(state.companyProjects, state.user), true, [
|
||||
createButton({
|
||||
label: "+",
|
||||
onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER),
|
||||
}),
|
||||
]),
|
||||
section(L("B01_Dashboard_Members"), memberTable(state.members, state.user), true, [
|
||||
createButton({
|
||||
label: "+",
|
||||
onClick: () => openAddMemberModal(),
|
||||
}),
|
||||
]),
|
||||
@@ -162,12 +231,10 @@ function buildPage(state: DashboardState): HTMLElement {
|
||||
section(L("B01_Dashboard_Company"), buildCompanyPanel(state), true),
|
||||
);
|
||||
} else {
|
||||
// 일반 사용자 (USER)
|
||||
// 순서: 프로젝트, 회사관리(회사패널), 기본정보(프로필), 보안
|
||||
grid.append(
|
||||
section(L("B01_Dashboard_Projects"), projectTable(state.userProjects), true, [
|
||||
section(L("B01_Dashboard_Projects"), projectTable(state.userProjects, state.user), true, [
|
||||
createButton({
|
||||
label: L("B01_Dashboard_NewProject"),
|
||||
label: "+",
|
||||
onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER),
|
||||
}),
|
||||
]),
|
||||
@@ -175,7 +242,6 @@ function buildPage(state: DashboardState): HTMLElement {
|
||||
);
|
||||
}
|
||||
|
||||
// 기본정보 (프로필) & 보안 순서
|
||||
grid.append(
|
||||
section(L("B01_Dashboard_Profile"), buildProfileForm(state.user)),
|
||||
section(L("B01_Account_Section_Security"), buildSecurityForm()),
|
||||
@@ -207,22 +273,6 @@ function roleLabel(role: DashboardUser["role"]): string {
|
||||
return L("B01_Dashboard_Role_User");
|
||||
}
|
||||
|
||||
function section(
|
||||
title: string,
|
||||
body: HTMLElement,
|
||||
wide = false,
|
||||
actions: HTMLElement[] = [],
|
||||
): HTMLElement {
|
||||
const actionRow = document.createElement("div");
|
||||
actionRow.className = "b01-dashboard__actions";
|
||||
actionRow.append(...actions);
|
||||
const cardBody = actions.length ? [actionRow, body] : [body];
|
||||
const card = createCard({ title, body: cardBody, raised: true });
|
||||
card.classList.add("b01-dashboard__section");
|
||||
if (wide) card.classList.add("b01-dashboard__section--wide");
|
||||
return card;
|
||||
}
|
||||
|
||||
function table(headers: string[], rows: HTMLElement[][]): HTMLElement {
|
||||
if (!rows.length) {
|
||||
const empty = document.createElement("p");
|
||||
@@ -263,7 +313,7 @@ function text(value: unknown): HTMLElement {
|
||||
return span;
|
||||
}
|
||||
|
||||
function projectTable(projects: ProjectItem[]): HTMLElement {
|
||||
function projectTable(projects: ProjectItem[], currentUser: DashboardUser): HTMLElement {
|
||||
return table(
|
||||
[
|
||||
L("B01_Dashboard_Table_Project"),
|
||||
@@ -271,14 +321,49 @@ function projectTable(projects: ProjectItem[]): HTMLElement {
|
||||
L("B01_Dashboard_Table_Progress"),
|
||||
L("B01_Dashboard_Table_Workflow"),
|
||||
L("B01_Dashboard_Table_Updated"),
|
||||
L("B01_Dashboard_Table_Action"),
|
||||
],
|
||||
projects.map((project) => [
|
||||
text(project.name),
|
||||
text(project.region),
|
||||
text(`${project.progress_percent}%`),
|
||||
workflow(project.workflow_stage),
|
||||
text(formatDate(project.updated_at)),
|
||||
]),
|
||||
projects.map((project) => {
|
||||
const actCell = document.createElement("div");
|
||||
actCell.className = "b01-dashboard__actions";
|
||||
|
||||
if (canEditProject(currentUser, project)) {
|
||||
actCell.append(
|
||||
createButton({
|
||||
label: L("Common_Btn_Edit"),
|
||||
variant: "ghost",
|
||||
onClick: () => openEditProjectModal(currentUser, project),
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (canDeleteProject(currentUser)) {
|
||||
actCell.append(
|
||||
createButton({
|
||||
label: L("Common_Btn_Delete"),
|
||||
variant: "danger",
|
||||
onClick: () => openDeleteProjectModal(project),
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (canManageAutomation(currentUser, project)) {
|
||||
actCell.append(
|
||||
createButton({
|
||||
label: L("B01_Dashboard_AutomationLogic"),
|
||||
variant: "ghost",
|
||||
onClick: () => openAutomationModal(project),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
text(project.name),
|
||||
text(project.region),
|
||||
text(`${project.progress_percent}%`),
|
||||
workflow(project.workflow_stage),
|
||||
text(formatDate(project.updated_at)),
|
||||
actCell,
|
||||
];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -336,7 +421,7 @@ function buildCompanyPanel(state: DashboardState): HTMLElement {
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function memberTable(members: Member[]): HTMLElement {
|
||||
function memberTable(members: Member[], currentUser: DashboardUser): HTMLElement {
|
||||
return table(
|
||||
[
|
||||
L("B01_Dashboard_Table_Email"),
|
||||
@@ -346,18 +431,47 @@ function memberTable(members: Member[]): HTMLElement {
|
||||
L("B01_Dashboard_Table_Role"),
|
||||
L("B01_Dashboard_Table_Action"),
|
||||
],
|
||||
members.map((member) => [
|
||||
text(member.email),
|
||||
text(member.name),
|
||||
text(member.position),
|
||||
text(member.department),
|
||||
text(member.role),
|
||||
createButton({
|
||||
label: L("B01_Dashboard_RemoveMember"),
|
||||
variant: "ghost",
|
||||
onClick: () => onB01_Member_Remove_Click(member.id),
|
||||
}),
|
||||
]),
|
||||
members.map((member) => {
|
||||
const actionsEl = document.createElement("div");
|
||||
actionsEl.className = "b01-dashboard__actions";
|
||||
|
||||
actionsEl.append(
|
||||
createButton({
|
||||
label: L("Common_Btn_Edit"),
|
||||
variant: "ghost",
|
||||
onClick: () => openEditUserModal(currentUser, member),
|
||||
}),
|
||||
);
|
||||
|
||||
if (canChangeRole(currentUser, member)) {
|
||||
actionsEl.append(
|
||||
createButton({
|
||||
label: L("B01_Dashboard_ChangeRole"),
|
||||
variant: "ghost",
|
||||
onClick: () => openChangeRoleModal(member),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (canDeleteUser(currentUser, member)) {
|
||||
actionsEl.append(
|
||||
createButton({
|
||||
label: L("B01_Dashboard_RemoveMember"),
|
||||
variant: "danger",
|
||||
onClick: () => openDeleteUserModal(member),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
text(member.email),
|
||||
text(member.name),
|
||||
text(member.position),
|
||||
text(member.department),
|
||||
text(member.role),
|
||||
actionsEl,
|
||||
];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -393,169 +507,6 @@ function actionPair(approve: () => void, reject: () => void): HTMLElement {
|
||||
return row;
|
||||
}
|
||||
|
||||
function buildResourcePanel(resources: ResourceData | null): HTMLElement {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "b01-dashboard__resource-panel";
|
||||
wrap.append(buildResourceMetrics(resources), buildResourceChart(resources));
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function buildResourceMetrics(resources: ResourceData | null): HTMLElement {
|
||||
const values = resources?.current;
|
||||
const grid = document.createElement("div");
|
||||
grid.className = "b01-dashboard__metric-grid";
|
||||
const items = [
|
||||
[L("B01_Dashboard_Metric_Cpu"), percent(values?.cpu_usage_percent)],
|
||||
[L("B01_Dashboard_Metric_Memory"), percent(values?.memory_usage_percent)],
|
||||
[L("B01_Dashboard_Metric_Disk"), percent(values?.disk_usage_percent)],
|
||||
[L("B01_Dashboard_Metric_ActiveUsers"), values?.active_user_count ?? 0],
|
||||
[L("B01_Dashboard_Metric_ActiveProjects"), values?.active_project_count ?? 0],
|
||||
];
|
||||
for (const [label, value] of items) {
|
||||
const box = document.createElement("div");
|
||||
box.className = "b01-dashboard__metric";
|
||||
const labelEl = document.createElement("span");
|
||||
labelEl.className = "b01-dashboard__metric-label";
|
||||
labelEl.textContent = String(label);
|
||||
const valueEl = document.createElement("span");
|
||||
valueEl.className = "b01-dashboard__metric-value";
|
||||
valueEl.textContent = String(value);
|
||||
box.append(labelEl, valueEl);
|
||||
grid.append(box);
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
|
||||
function formatChartTime(iso: string | null | undefined): string {
|
||||
if (!iso) return "";
|
||||
try {
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return "";
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
return `${pad(d.getMonth() + 1)}/${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function buildResourceChart(resources: ResourceData | null): HTMLElement {
|
||||
const box = document.createElement("div");
|
||||
box.className = "b01-dashboard__chart";
|
||||
|
||||
const caption = document.createElement("p");
|
||||
caption.className = "b01-dashboard__chart-caption";
|
||||
caption.textContent = L("B01_Dashboard_Resources_ChartCaption");
|
||||
box.append(caption);
|
||||
|
||||
const chartWrap = document.createElement("div");
|
||||
chartWrap.className = "b01-dashboard__chart-content";
|
||||
box.append(chartWrap);
|
||||
|
||||
const history = resources?.history ?? [];
|
||||
|
||||
// 7일간의 고정 타임라인 생성 (4시간 간격, 총 43개 포인트)
|
||||
const points: { timestamp: Date; cpu: number | null; mem: number | null; disk: number | null }[] =
|
||||
[];
|
||||
const now = new Date();
|
||||
|
||||
// 12시간 단위(정오 또는 자정)로 올림 정렬하여 기준점 계산
|
||||
const baseDate = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
now.getHours() >= 12 ? 24 : 12,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
|
||||
for (let i = 0; i <= 42; i++) {
|
||||
// 7일 전부터 현재 정렬 시점까지 4시간 단위 버킷 생성
|
||||
const time = new Date(baseDate.getTime() - (42 - i) * 4 * 60 * 60 * 1000);
|
||||
points.push({ timestamp: time, cpu: null, mem: null, disk: null });
|
||||
}
|
||||
|
||||
// DB에서 불러온 계측 이력 매핑
|
||||
history.forEach((h) => {
|
||||
if (!h.timestamp) return;
|
||||
// DB에서 넘어오는 ISO 문자열을 UTC 기준으로 정확히 매핑하기 위해 Z를 보완하여 파싱
|
||||
const tsStr = h.timestamp.endsWith("Z") ? h.timestamp : h.timestamp + "Z";
|
||||
const hTime = new Date(tsStr);
|
||||
|
||||
// 가장 가까운 4시간 단위 버킷 매핑 (±2시간 내)
|
||||
let closestIdx = -1;
|
||||
let minDiff = Infinity;
|
||||
|
||||
points.forEach((p, idx) => {
|
||||
const diff = Math.abs(p.timestamp.getTime() - hTime.getTime());
|
||||
if (diff < minDiff && diff <= 2 * 60 * 60 * 1000) {
|
||||
minDiff = diff;
|
||||
closestIdx = idx;
|
||||
}
|
||||
});
|
||||
|
||||
if (closestIdx !== -1) {
|
||||
const p = points[closestIdx];
|
||||
p.cpu = h.cpu_usage_percent;
|
||||
p.mem = h.memory_usage_percent;
|
||||
p.disk = h.disk_usage_percent;
|
||||
}
|
||||
});
|
||||
|
||||
// xLabels 생성 (Midnight: MM/DD, 그 외는 빈칸)
|
||||
const xLabels = points.map((p) => {
|
||||
const hours = p.timestamp.getHours();
|
||||
if (hours === 0) {
|
||||
return `${p.timestamp.getMonth() + 1}/${p.timestamp.getDate()}`;
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
let resizeTimeout: number | null = null;
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const width = Math.floor(entry.contentRect.width);
|
||||
if (width <= 0) continue;
|
||||
|
||||
if (resizeTimeout) {
|
||||
window.cancelAnimationFrame(resizeTimeout);
|
||||
}
|
||||
|
||||
resizeTimeout = window.requestAnimationFrame(() => {
|
||||
chartWrap.innerHTML = "";
|
||||
const chart = createLineChart({
|
||||
ariaLabel: L("B01_Dashboard_Resources_ChartAria"),
|
||||
xLabels,
|
||||
width: width,
|
||||
height: 140,
|
||||
series: [
|
||||
{
|
||||
name: L("B01_Dashboard_Metric_Cpu"),
|
||||
colorIndex: 0,
|
||||
values: points.map((p) => p.cpu),
|
||||
},
|
||||
{
|
||||
name: L("B01_Dashboard_Metric_Memory"),
|
||||
colorIndex: 1,
|
||||
values: points.map((p) => p.mem),
|
||||
},
|
||||
{
|
||||
name: L("B01_Dashboard_Metric_Disk"),
|
||||
colorIndex: 2,
|
||||
values: points.map((p) => p.disk),
|
||||
},
|
||||
],
|
||||
});
|
||||
chartWrap.append(chart);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(box);
|
||||
|
||||
return box;
|
||||
}
|
||||
|
||||
function companyTable(companies: CompanyInfo[]): HTMLElement {
|
||||
return table(
|
||||
[
|
||||
@@ -571,15 +522,39 @@ function companyTable(companies: CompanyInfo[]): HTMLElement {
|
||||
);
|
||||
}
|
||||
|
||||
function userTable(users: DashboardUser[]): HTMLElement {
|
||||
function userTable(users: DashboardUser[], currentUser: DashboardUser): HTMLElement {
|
||||
return table(
|
||||
[
|
||||
L("B01_Dashboard_Table_Email"),
|
||||
L("B01_Dashboard_Table_Name"),
|
||||
L("B01_Dashboard_Table_Role"),
|
||||
L("B01_Dashboard_Table_Status"),
|
||||
L("B01_Dashboard_Table_Action"),
|
||||
],
|
||||
users.map((user) => [text(user.email), text(user.name), text(user.role), text(user.status)]),
|
||||
users.map((user) => {
|
||||
const actionsEl = document.createElement("div");
|
||||
actionsEl.className = "b01-dashboard__actions";
|
||||
|
||||
actionsEl.append(
|
||||
createButton({
|
||||
label: L("Common_Btn_Edit"),
|
||||
variant: "ghost",
|
||||
onClick: () => openEditUserModal(currentUser, user),
|
||||
}),
|
||||
);
|
||||
|
||||
if (canChangeRole(currentUser, user)) {
|
||||
actionsEl.append(
|
||||
createButton({
|
||||
label: L("B01_Dashboard_ChangeRole"),
|
||||
variant: "ghost",
|
||||
onClick: () => openChangeRoleModal(user),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return [text(user.email), text(user.name), text(user.role), text(user.status), actionsEl];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -697,106 +672,6 @@ function buildSecurityForm(): HTMLElement {
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function openCreateCompanyModal(): void {
|
||||
const name = createInputField({ label: L("B01_Dashboard_Table_Company"), required: true });
|
||||
const number = createInputField({
|
||||
label: L("B01_Dashboard_Field_BusinessNumber"),
|
||||
required: true,
|
||||
});
|
||||
const address = createInputField({ label: L("B01_Dashboard_Field_Address") });
|
||||
const owner = createInputField({ label: L("B01_Dashboard_Field_Owner") });
|
||||
openModal(
|
||||
L("B01_Dashboard_Modal_CreateCompany"),
|
||||
[name.root, number.root, address.root, owner.root],
|
||||
async () => {
|
||||
if (isBlank(name.input.value) || isBlank(number.input.value)) return;
|
||||
await runRequest(() =>
|
||||
createCompany({
|
||||
name: name.input.value.trim(),
|
||||
business_registration_number: number.input.value.trim(),
|
||||
business_address: address.input.value.trim() || null,
|
||||
business_owner: owner.input.value.trim() || null,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function openFindCompanyModal(): void {
|
||||
const query = createInputField({ label: L("B01_Dashboard_Field_Search"), required: true });
|
||||
const results = document.createElement("div");
|
||||
results.className = "b01-dashboard__actions";
|
||||
const search = createButton({
|
||||
label: L("Common_Btn_Search"),
|
||||
variant: "ghost",
|
||||
onClick: async function onB01_Company_Search_Click() {
|
||||
results.innerHTML = "";
|
||||
const companies = await searchCompanies(query.input.value.trim());
|
||||
for (const company of companies) {
|
||||
results.append(
|
||||
createButton({
|
||||
label: `${company.name} ${L("B01_Dashboard_JoinCompany")}`,
|
||||
variant: "ghost",
|
||||
onClick: () => runRequest(() => joinCompany(company.id)),
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
openModal(
|
||||
L("B01_Dashboard_Modal_FindCompany"),
|
||||
[query.root, search, results],
|
||||
async () => undefined,
|
||||
);
|
||||
}
|
||||
|
||||
function openAddMemberModal(): void {
|
||||
const email = createInputField({
|
||||
label: L("B01_Dashboard_Field_MemberEmail"),
|
||||
type: "email",
|
||||
required: true,
|
||||
});
|
||||
openModal(L("B01_Dashboard_Modal_AddMember"), [email.root], async () => {
|
||||
if (isBlank(email.input.value)) return;
|
||||
await runRequest(() => addCompanyMember(email.input.value.trim()));
|
||||
});
|
||||
}
|
||||
|
||||
function openModal(title: string, body: HTMLElement[], onConfirm: () => Promise<void>): void {
|
||||
const modal = document.createElement("div");
|
||||
modal.className = "b01-dashboard__modal";
|
||||
const panel = document.createElement("div");
|
||||
panel.className = "b01-dashboard__modal-panel";
|
||||
const heading = document.createElement("h3");
|
||||
heading.className = "b01-dashboard__modal-title";
|
||||
heading.textContent = title;
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "b01-dashboard__actions";
|
||||
actions.append(
|
||||
createButton({
|
||||
label: L("Common_Btn_Cancel"),
|
||||
variant: "ghost",
|
||||
onClick: () => modal.remove(),
|
||||
}),
|
||||
createButton({
|
||||
label: L("Common_Btn_Confirm"),
|
||||
onClick: async () => {
|
||||
await onConfirm();
|
||||
modal.remove();
|
||||
window.dispatchEvent(new HashChangeEvent("hashchange"));
|
||||
},
|
||||
}),
|
||||
);
|
||||
panel.append(heading, ...body, actions);
|
||||
modal.append(panel);
|
||||
document.body.append(modal);
|
||||
}
|
||||
|
||||
async function onB01_Member_Remove_Click(userId: number): Promise<void> {
|
||||
await runRequest(() => removeCompanyMember(userId));
|
||||
window.dispatchEvent(new HashChangeEvent("hashchange"));
|
||||
}
|
||||
|
||||
async function onB01_JoinRequest_Process_Click(
|
||||
requestId: number,
|
||||
action: "APPROVE" | "REJECT",
|
||||
@@ -825,7 +700,3 @@ async function runRequest(action: () => Promise<unknown>): Promise<void> {
|
||||
function formatDate(value?: string | null): string {
|
||||
return value ? value.slice(0, 10) : "-";
|
||||
}
|
||||
|
||||
function percent(value?: number | null): string {
|
||||
return value == null ? "-" : `${Math.round(value)}%`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user