832 lines
26 KiB
TypeScript
832 lines
26 KiB
TypeScript
import { ROUTES, type RoutePath } 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";
|
|
import {
|
|
createButton,
|
|
createCard,
|
|
createInputField,
|
|
createLineChart,
|
|
createTag,
|
|
hideLoadingOverlay,
|
|
showLoadingOverlay,
|
|
showToast,
|
|
} from "@ui/ui_template_elements";
|
|
import {
|
|
addCompanyMember,
|
|
changePassword,
|
|
createCompany,
|
|
fetchAllCompanies,
|
|
fetchAllJoinRequests,
|
|
fetchAllProjects,
|
|
fetchAllUsers,
|
|
fetchAuditLogs,
|
|
fetchCompanyJoinRequests,
|
|
fetchCompanyMembers,
|
|
fetchCompanyProjects,
|
|
fetchDashboardMe,
|
|
fetchSystemResources,
|
|
fetchUserCompany,
|
|
fetchUserProjects,
|
|
joinCompany,
|
|
processJoinRequest,
|
|
removeCompanyMember,
|
|
searchCompanies,
|
|
updateUserProfile,
|
|
type AuditLog,
|
|
type CompanyInfo,
|
|
type DashboardUser,
|
|
type JoinRequest,
|
|
type Member,
|
|
type ProjectItem,
|
|
type ResourceData,
|
|
} from "./B01_Dashboard_Api_Fetch";
|
|
import "./B01_Dashboard_UI_Style.css";
|
|
|
|
const PASSWORD_MIN_LENGTH = 8;
|
|
|
|
function L(key: keyof typeof ui_locales): string {
|
|
return ui_locales[key][currentLanguageIndex];
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
export async function renderB01Dashboard(root: HTMLElement): Promise<void> {
|
|
showLoadingOverlay();
|
|
try {
|
|
const user = await fetchDashboardMe();
|
|
const state: DashboardState = {
|
|
user,
|
|
userProjects: [],
|
|
companyProjects: [],
|
|
allProjects: [],
|
|
company: null,
|
|
members: [],
|
|
joinRequests: [],
|
|
allCompanies: [],
|
|
allUsers: [],
|
|
allJoinRequests: [],
|
|
auditLogs: [],
|
|
resources: null,
|
|
};
|
|
await loadRoleData(state);
|
|
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<void> {
|
|
if (state.user.role === "SYSTEM_ADMIN") {
|
|
const [companies, users, requests, logs, resources, projects] = await Promise.all([
|
|
fetchAllCompanies(),
|
|
fetchAllUsers(),
|
|
fetchAllJoinRequests(),
|
|
fetchAuditLogs(),
|
|
fetchSystemResources(7),
|
|
fetchAllProjects(),
|
|
]);
|
|
state.allCompanies = companies;
|
|
state.allUsers = users;
|
|
state.allJoinRequests = requests;
|
|
state.auditLogs = logs;
|
|
state.resources = resources;
|
|
state.allProjects = projects;
|
|
return;
|
|
}
|
|
const [projects, company] = await Promise.all([fetchUserProjects(), fetchUserCompany()]);
|
|
state.userProjects = projects;
|
|
state.company = company;
|
|
if (state.user.role === "ADMIN") {
|
|
const [members, requests, companyProjects] = await Promise.all([
|
|
fetchCompanyMembers(),
|
|
fetchCompanyJoinRequests(),
|
|
fetchCompanyProjects(),
|
|
]);
|
|
state.members = members;
|
|
state.joinRequests = requests;
|
|
state.companyProjects = companyProjects;
|
|
}
|
|
}
|
|
|
|
function buildPage(state: DashboardState): HTMLElement {
|
|
const page = document.createElement("div");
|
|
page.className = "b01-dashboard";
|
|
page.append(buildHeader(state.user));
|
|
|
|
const grid = document.createElement("div");
|
|
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, [
|
|
createButton({
|
|
label: L("B01_Dashboard_NewProject"),
|
|
onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER),
|
|
}),
|
|
]),
|
|
section(L("B01_Dashboard_Members"), memberTable(state.members), true, [
|
|
createButton({
|
|
label: L("B01_Dashboard_AddMember"),
|
|
variant: "ghost",
|
|
onClick: () => openAddMemberModal(),
|
|
}),
|
|
]),
|
|
section(L("B01_Dashboard_JoinRequests"), joinRequestTable(state.joinRequests, false), true),
|
|
section(L("B01_Dashboard_Company"), buildCompanyPanel(state), true),
|
|
);
|
|
} else {
|
|
// 일반 사용자 (USER)
|
|
// 순서: 프로젝트, 회사관리(회사패널), 기본정보(프로필), 보안
|
|
grid.append(
|
|
section(L("B01_Dashboard_Projects"), projectTable(state.userProjects), true, [
|
|
createButton({
|
|
label: L("B01_Dashboard_NewProject"),
|
|
onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER),
|
|
}),
|
|
]),
|
|
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()),
|
|
);
|
|
page.append(grid);
|
|
return page;
|
|
}
|
|
|
|
function buildHeader(user: DashboardUser): HTMLElement {
|
|
const header = document.createElement("header");
|
|
header.className = "b01-dashboard__header";
|
|
const text = document.createElement("div");
|
|
const title = document.createElement("h1");
|
|
title.className = "b01-dashboard__title";
|
|
title.textContent = L("B01_Dashboard_Title");
|
|
const subtitle = document.createElement("p");
|
|
subtitle.className = "b01-dashboard__subtitle";
|
|
subtitle.textContent = L("B01_Dashboard_Subtitle");
|
|
text.append(title, subtitle);
|
|
const tag = createTag(roleLabel(user.role), user.role === "SYSTEM_ADMIN" ? "accent" : "neutral");
|
|
tag.classList.add("b01-dashboard__role");
|
|
header.append(text, tag);
|
|
return header;
|
|
}
|
|
|
|
function roleLabel(role: DashboardUser["role"]): string {
|
|
if (role === "SYSTEM_ADMIN") return L("B01_Dashboard_Role_SystemAdmin");
|
|
if (role === "ADMIN") return L("B01_Dashboard_Role_Admin");
|
|
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");
|
|
empty.className = "b01-dashboard__empty";
|
|
empty.textContent = L("Common_Status_Empty");
|
|
return empty;
|
|
}
|
|
const wrap = document.createElement("div");
|
|
wrap.className = "b01-dashboard__table-wrap";
|
|
const tableEl = document.createElement("table");
|
|
tableEl.className = "b01-dashboard__table";
|
|
const thead = document.createElement("thead");
|
|
const headRow = document.createElement("tr");
|
|
for (const header of headers) {
|
|
const th = document.createElement("th");
|
|
th.textContent = header;
|
|
headRow.append(th);
|
|
}
|
|
thead.append(headRow);
|
|
const tbody = document.createElement("tbody");
|
|
for (const row of rows) {
|
|
const tr = document.createElement("tr");
|
|
for (const cell of row) {
|
|
const td = document.createElement("td");
|
|
td.append(cell);
|
|
tr.append(td);
|
|
}
|
|
tbody.append(tr);
|
|
}
|
|
tableEl.append(thead, tbody);
|
|
wrap.append(tableEl);
|
|
return wrap;
|
|
}
|
|
|
|
function text(value: unknown): HTMLElement {
|
|
const span = document.createElement("span");
|
|
span.textContent = value == null || value === "" ? "-" : String(value);
|
|
return span;
|
|
}
|
|
|
|
function projectTable(projects: ProjectItem[]): HTMLElement {
|
|
return table(
|
|
[
|
|
L("B01_Dashboard_Table_Project"),
|
|
L("B01_Dashboard_Table_Region"),
|
|
L("B01_Dashboard_Table_Progress"),
|
|
L("B01_Dashboard_Table_Workflow"),
|
|
L("B01_Dashboard_Table_Updated"),
|
|
],
|
|
projects.map((project) => [
|
|
text(project.name),
|
|
text(project.region),
|
|
text(`${project.progress_percent}%`),
|
|
workflow(project.workflow_stage),
|
|
text(formatDate(project.updated_at)),
|
|
]),
|
|
);
|
|
}
|
|
|
|
function workflow(activeStage: number): HTMLElement {
|
|
const routes: RoutePath[] = [
|
|
ROUTES.B03_FILE_INPUT,
|
|
ROUTES.B04_WF1_SURFACE,
|
|
ROUTES.B05_WF2_ROUTE,
|
|
ROUTES.B06_WF3_PROFILE_CROSS,
|
|
ROUTES.B07_WF4_DESIGN_DETAIL,
|
|
ROUTES.B08_WF5_QUANTITY,
|
|
ROUTES.B09_WF6_ESTIMATION,
|
|
];
|
|
const box = document.createElement("div");
|
|
box.className = "b01-dashboard__workflow";
|
|
routes.forEach((route, index) => {
|
|
const button = document.createElement("button");
|
|
button.type = "button";
|
|
button.className = "b01-dashboard__step";
|
|
button.textContent = `B${String(index + 3).padStart(2, "0")}`;
|
|
const enabled = index + 1 <= Math.max(activeStage, 1);
|
|
button.disabled = !enabled;
|
|
if (enabled) {
|
|
button.classList.add("is-enabled");
|
|
button.addEventListener("click", () => navigateTo(route));
|
|
}
|
|
box.append(button);
|
|
});
|
|
return box;
|
|
}
|
|
|
|
function buildCompanyPanel(state: DashboardState): HTMLElement {
|
|
const wrap = document.createElement("div");
|
|
wrap.className = "b01-dashboard__actions";
|
|
if (!state.company) {
|
|
wrap.append(
|
|
createTag(L("B01_Dashboard_NoCompany"), "warning"),
|
|
createButton({
|
|
label: L("B01_Dashboard_CreateCompany"),
|
|
onClick: () => openCreateCompanyModal(),
|
|
}),
|
|
createButton({
|
|
label: L("B01_Dashboard_FindCompany"),
|
|
variant: "ghost",
|
|
onClick: () => openFindCompanyModal(),
|
|
}),
|
|
);
|
|
return wrap;
|
|
}
|
|
wrap.append(
|
|
createTag(`${state.company.name} (${state.user.status})`, "success"),
|
|
text(`${L("B01_Dashboard_Metric_ActiveUsers")}: ${state.company.user_count ?? 0}`),
|
|
text(`${L("B01_Dashboard_Projects")}: ${state.company.project_count ?? 0}`),
|
|
);
|
|
return wrap;
|
|
}
|
|
|
|
function memberTable(members: Member[]): HTMLElement {
|
|
return table(
|
|
[
|
|
L("B01_Dashboard_Table_Email"),
|
|
L("B01_Dashboard_Table_Name"),
|
|
L("B01_Dashboard_Table_Position"),
|
|
L("B01_Dashboard_Table_Department"),
|
|
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),
|
|
}),
|
|
]),
|
|
);
|
|
}
|
|
|
|
function joinRequestTable(requests: JoinRequest[], systemMode: boolean): HTMLElement {
|
|
return table(
|
|
[
|
|
L("B01_Dashboard_Table_Email"),
|
|
...(systemMode ? [L("B01_Dashboard_Table_Company")] : []),
|
|
L("B01_Dashboard_Table_Requested"),
|
|
L("B01_Dashboard_Table_Status"),
|
|
L("B01_Dashboard_Table_Action"),
|
|
],
|
|
requests.map((request) => [
|
|
text(request.user_email),
|
|
...(systemMode ? [text(request.company_name)] : []),
|
|
text(formatDate(request.requested_at)),
|
|
text(request.status),
|
|
actionPair(
|
|
() => onB01_JoinRequest_Process_Click(request.id, "APPROVE", systemMode),
|
|
() => onB01_JoinRequest_Process_Click(request.id, "REJECT", systemMode),
|
|
),
|
|
]),
|
|
);
|
|
}
|
|
|
|
function actionPair(approve: () => void, reject: () => void): HTMLElement {
|
|
const row = document.createElement("div");
|
|
row.className = "b01-dashboard__actions";
|
|
row.append(
|
|
createButton({ label: L("B01_Dashboard_Approve"), variant: "ghost", onClick: approve }),
|
|
createButton({ label: L("B01_Dashboard_Reject"), variant: "danger", onClick: reject }),
|
|
);
|
|
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(
|
|
[
|
|
L("B01_Dashboard_Table_Company"),
|
|
L("B01_Dashboard_Field_BusinessNumber"),
|
|
L("B01_Dashboard_Table_Status"),
|
|
],
|
|
companies.map((company) => [
|
|
text(company.name),
|
|
text(company.business_registration_number),
|
|
text(company.business_status),
|
|
]),
|
|
);
|
|
}
|
|
|
|
function userTable(users: DashboardUser[]): HTMLElement {
|
|
return table(
|
|
[
|
|
L("B01_Dashboard_Table_Email"),
|
|
L("B01_Dashboard_Table_Name"),
|
|
L("B01_Dashboard_Table_Role"),
|
|
L("B01_Dashboard_Table_Status"),
|
|
],
|
|
users.map((user) => [text(user.email), text(user.name), text(user.role), text(user.status)]),
|
|
);
|
|
}
|
|
|
|
function auditLogTable(logs: AuditLog[]): HTMLElement {
|
|
return table(
|
|
[
|
|
L("B01_Dashboard_Table_Email"),
|
|
L("B01_Dashboard_Table_Action"),
|
|
L("B01_Dashboard_Table_Updated"),
|
|
],
|
|
logs.map((log) => [text(log.email), text(log.action), text(formatDate(log.timestamp))]),
|
|
);
|
|
}
|
|
|
|
function buildProfileForm(user: DashboardUser): HTMLElement {
|
|
const name = createInputField({
|
|
label: L("B01_Account_Field_Name"),
|
|
value: user.name,
|
|
required: true,
|
|
});
|
|
const position = createInputField({
|
|
label: L("B01_Dashboard_Table_Position"),
|
|
value: user.position ?? "",
|
|
});
|
|
const department = createInputField({
|
|
label: L("B01_Dashboard_Table_Department"),
|
|
value: user.department ?? "",
|
|
});
|
|
const phone = createInputField({ label: L("B01_Account_Field_Phone"), value: user.phone ?? "" });
|
|
const grid = document.createElement("div");
|
|
grid.className = "b01-dashboard__form-grid";
|
|
grid.append(name.root, position.root, department.root, phone.root);
|
|
const save = createButton({
|
|
label: L("B01_Dashboard_SaveProfile"),
|
|
onClick: async function onB01_Profile_Save_Click() {
|
|
name.setError();
|
|
if (isBlank(name.input.value)) {
|
|
name.setError(L("Common_Msg_RequiredField"));
|
|
return;
|
|
}
|
|
await runRequest(() =>
|
|
updateUserProfile({
|
|
name: name.input.value.trim(),
|
|
position: position.input.value.trim() || null,
|
|
department: department.input.value.trim() || null,
|
|
phone: phone.input.value.trim() || null,
|
|
}),
|
|
);
|
|
},
|
|
});
|
|
const wrap = document.createElement("div");
|
|
wrap.append(grid, save);
|
|
return wrap;
|
|
}
|
|
|
|
function buildSecurityForm(): HTMLElement {
|
|
const currentPassword = createInputField({
|
|
label: L("B01_Account_Field_CurrentPw"),
|
|
type: "password",
|
|
required: true,
|
|
});
|
|
const newPassword = createInputField({
|
|
label: L("B01_Account_Field_NewPw"),
|
|
type: "password",
|
|
required: true,
|
|
});
|
|
const confirmPassword = createInputField({
|
|
label: L("B01_Account_Field_ConfirmPw"),
|
|
type: "password",
|
|
required: true,
|
|
});
|
|
const grid = document.createElement("div");
|
|
grid.className = "b01-dashboard__form-grid";
|
|
grid.append(currentPassword.root, newPassword.root, confirmPassword.root);
|
|
const save = createButton({
|
|
label: L("B01_Account_Save_Password"),
|
|
variant: "ghost",
|
|
onClick: async function onB01_Password_Save_Click() {
|
|
currentPassword.setError();
|
|
newPassword.setError();
|
|
confirmPassword.setError();
|
|
|
|
const currentValue = currentPassword.input.value;
|
|
const nextValue = newPassword.input.value;
|
|
const confirmValue = confirmPassword.input.value;
|
|
if (isBlank(currentValue) || isBlank(nextValue) || isBlank(confirmValue)) {
|
|
currentPassword.setError(isBlank(currentValue) ? L("Common_Msg_RequiredField") : undefined);
|
|
newPassword.setError(isBlank(nextValue) ? L("Common_Msg_RequiredField") : undefined);
|
|
confirmPassword.setError(isBlank(confirmValue) ? L("Common_Msg_RequiredField") : undefined);
|
|
return;
|
|
}
|
|
if (nextValue.length < PASSWORD_MIN_LENGTH) {
|
|
newPassword.setError(L("B01_Account_Error_PwLength"));
|
|
return;
|
|
}
|
|
if (nextValue !== confirmValue) {
|
|
confirmPassword.setError(L("B01_Account_Error_PwMismatch"));
|
|
return;
|
|
}
|
|
await runRequest(() =>
|
|
changePassword({
|
|
current_password: currentValue,
|
|
new_password: nextValue,
|
|
new_password_confirm: confirmValue,
|
|
logout_all: false,
|
|
}),
|
|
);
|
|
currentPassword.input.value = "";
|
|
newPassword.input.value = "";
|
|
confirmPassword.input.value = "";
|
|
},
|
|
});
|
|
const wrap = document.createElement("div");
|
|
wrap.append(grid, save);
|
|
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",
|
|
systemMode: boolean,
|
|
): Promise<void> {
|
|
await runRequest(() =>
|
|
systemMode && action === "APPROVE"
|
|
? import("./B01_Dashboard_Api_Fetch").then((api) => api.systemApproveJoinRequest(requestId))
|
|
: processJoinRequest(requestId, action),
|
|
);
|
|
window.dispatchEvent(new HashChangeEvent("hashchange"));
|
|
}
|
|
|
|
async function runRequest(action: () => Promise<unknown>): Promise<void> {
|
|
showLoadingOverlay();
|
|
try {
|
|
await action();
|
|
showToast(L("B01_Dashboard_Saved"), "success");
|
|
} catch (error) {
|
|
showToast(error instanceof Error ? error.message : L("B01_Dashboard_RequestFailed"), "error");
|
|
} finally {
|
|
hideLoadingOverlay();
|
|
}
|
|
}
|
|
|
|
function formatDate(value?: string | null): string {
|
|
return value ? value.slice(0, 10) : "-";
|
|
}
|
|
|
|
function percent(value?: number | null): string {
|
|
return value == null ? "-" : `${Math.round(value)}%`;
|
|
}
|