260717_0
This commit is contained in:
@@ -11,6 +11,7 @@ import aiomysql
|
||||
import psutil
|
||||
|
||||
from config.config_db import get_db_pool
|
||||
from config.config_system import EMAIL_REVERIFY_DAYS
|
||||
|
||||
|
||||
def _role(value: str | None) -> str:
|
||||
@@ -50,10 +51,11 @@ async def get_dashboard_me(user_id: int) -> dict[str, Any] | None:
|
||||
await cursor.execute(
|
||||
"""SELECT u.id, u.email, u.name, u.position, u.department, u.phone,
|
||||
u.company_id, u.role, u.is_master, u.status, u.last_login,
|
||||
u.auth_expires_at, c.name AS company_name
|
||||
DATE_ADD(u.last_email_verified_at, INTERVAL %s DAY) AS auth_expires_at,
|
||||
c.name AS company_name
|
||||
FROM users u LEFT JOIN companies c ON c.id = u.company_id
|
||||
WHERE u.id = %s AND u.deleted_at IS NULL""",
|
||||
(user_id,),
|
||||
(EMAIL_REVERIFY_DAYS, user_id),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
if row:
|
||||
@@ -327,8 +329,7 @@ async def create_company(user_id: int, data: dict[str, Any]) -> dict[str, Any]:
|
||||
await cursor.execute(
|
||||
"""UPDATE users
|
||||
SET company_id = %s, role = 'ADMIN', is_master = TRUE,
|
||||
status = 'ACTIVE',
|
||||
auth_expires_at = DATE_ADD(CURRENT_TIMESTAMP, INTERVAL 3 MONTH)
|
||||
status = 'ACTIVE'
|
||||
WHERE id = %s""",
|
||||
(company_id, user_id),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import type { AuditLog, DashboardUser } from "./B01_Dashboard_Api_Fetch";
|
||||
import { canChangeRole } from "./B01_Dashboard_UI_Helper";
|
||||
import { openChangeRoleModal, openEditUserModal } from "./B01_Dashboard_UI_Modals";
|
||||
import { formatDate, L, table, text } from "./B01_Dashboard_UI_Common";
|
||||
|
||||
export function userTable(users: DashboardUser[], currentUser: DashboardUser): 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_Account_Field_Phone"),
|
||||
L("B01_Dashboard_Table_Role"),
|
||||
L("B01_Dashboard_Table_Status"),
|
||||
L("B01_Dashboard_Table_Action"),
|
||||
],
|
||||
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.position),
|
||||
text(user.department),
|
||||
text(user.phone),
|
||||
text(user.role),
|
||||
text(user.status),
|
||||
actionsEl,
|
||||
];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export 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))]),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import {
|
||||
createButton,
|
||||
createCard,
|
||||
hideLoadingOverlay,
|
||||
showLoadingOverlay,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import type { DashboardUser } from "./B01_Dashboard_Api_Fetch";
|
||||
|
||||
export function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
}
|
||||
|
||||
export 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;
|
||||
}
|
||||
|
||||
export 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;
|
||||
}
|
||||
|
||||
export 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");
|
||||
}
|
||||
|
||||
export 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;
|
||||
}
|
||||
|
||||
export function text(value: unknown): HTMLElement {
|
||||
const span = document.createElement("span");
|
||||
span.textContent = value == null || value === "" ? "-" : String(value);
|
||||
return span;
|
||||
}
|
||||
|
||||
export 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;
|
||||
}
|
||||
|
||||
export 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();
|
||||
}
|
||||
}
|
||||
|
||||
export function formatDate(value?: string | null): string {
|
||||
return value ? value.slice(0, 10) : "-";
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { createButton, createTag } from "@ui/ui_template_elements";
|
||||
import {
|
||||
processJoinRequest,
|
||||
type CompanyInfo,
|
||||
type DashboardUser,
|
||||
type JoinRequest,
|
||||
type Member,
|
||||
} from "./B01_Dashboard_Api_Fetch";
|
||||
import { canChangeRole, canDeleteUser } from "./B01_Dashboard_UI_Helper";
|
||||
import {
|
||||
openChangeRoleModal,
|
||||
openCreateCompanyModal,
|
||||
openDeleteUserModal,
|
||||
openEditUserModal,
|
||||
openFindCompanyModal,
|
||||
} from "./B01_Dashboard_UI_Modals";
|
||||
import type { DashboardState } from "./B01_Dashboard_UI_Page";
|
||||
import { actionPair, formatDate, L, runRequest, table, text } from "./B01_Dashboard_UI_Common";
|
||||
|
||||
export 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;
|
||||
}
|
||||
|
||||
export function memberTable(members: Member[], currentUser: DashboardUser): 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) => {
|
||||
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,
|
||||
];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export 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),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
export 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),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
export 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"));
|
||||
}
|
||||
@@ -1,19 +1,13 @@
|
||||
import { CURRENT_PROJECT_ID_KEY, ROUTES, type RoutePath } from "@config/config_frontend";
|
||||
import { isBlank } from "@util/common_util_validate";
|
||||
import { navigateTo } from "../A00_Common/router";
|
||||
import { workflowSteps } from "../A00_Common/b_page_scaffold";
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { ROUTES } from "@config/config_frontend";
|
||||
import {
|
||||
createButton,
|
||||
createCard,
|
||||
createInputField,
|
||||
createTag,
|
||||
hideLoadingOverlay,
|
||||
showLoadingOverlay,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { navigateTo } from "../A00_Common/router";
|
||||
import {
|
||||
changePassword,
|
||||
fetchAllCompanies,
|
||||
fetchAllJoinRequests,
|
||||
fetchAllProjects,
|
||||
@@ -26,8 +20,6 @@ import {
|
||||
fetchSystemResources,
|
||||
fetchUserCompany,
|
||||
fetchUserProjects,
|
||||
processJoinRequest,
|
||||
updateUserProfile,
|
||||
type AuditLog,
|
||||
type CompanyInfo,
|
||||
type DashboardUser,
|
||||
@@ -36,34 +28,21 @@ import {
|
||||
type ProjectItem,
|
||||
type ResourceData,
|
||||
} from "./B01_Dashboard_Api_Fetch";
|
||||
import { auditLogTable, userTable } from "./B01_Dashboard_UI_Admin";
|
||||
import { L, roleLabel, section } 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 { projectTable } from "./B01_Dashboard_UI_Projects";
|
||||
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;
|
||||
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
}
|
||||
|
||||
interface DashboardState {
|
||||
export interface DashboardState {
|
||||
user: DashboardUser;
|
||||
userProjects: ProjectItem[];
|
||||
companyProjects: ProjectItem[];
|
||||
@@ -148,40 +127,6 @@ 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";
|
||||
@@ -194,39 +139,24 @@ function buildPage(state: DashboardState): HTMLElement {
|
||||
grid.append(
|
||||
section(L("B01_Dashboard_Resources"), buildResourcePanel(state.resources), true),
|
||||
section(L("B01_Dashboard_Projects"), projectTable(state.allProjects, state.user), true, [
|
||||
createButton({
|
||||
label: "+",
|
||||
onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER),
|
||||
}),
|
||||
createButton({ label: "+", onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER) }),
|
||||
]),
|
||||
section(L("B01_Dashboard_Users"), userTable(state.allUsers, state.user), true, [
|
||||
createButton({
|
||||
label: "+",
|
||||
onClick: () => openAddMemberModal(),
|
||||
}),
|
||||
createButton({ 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(),
|
||||
}),
|
||||
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),
|
||||
}),
|
||||
createButton({ label: "+", onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER) }),
|
||||
]),
|
||||
section(L("B01_Dashboard_Members"), memberTable(state.members, state.user), true, [
|
||||
createButton({
|
||||
label: "+",
|
||||
onClick: () => openAddMemberModal(),
|
||||
}),
|
||||
createButton({ label: "+", onClick: () => openAddMemberModal() }),
|
||||
]),
|
||||
section(L("B01_Dashboard_JoinRequests"), joinRequestTable(state.joinRequests, false), true),
|
||||
section(L("B01_Dashboard_Company"), buildCompanyPanel(state), true),
|
||||
@@ -234,10 +164,7 @@ function buildPage(state: DashboardState): HTMLElement {
|
||||
} else {
|
||||
grid.append(
|
||||
section(L("B01_Dashboard_Projects"), projectTable(state.userProjects, state.user), true, [
|
||||
createButton({
|
||||
label: "+",
|
||||
onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER),
|
||||
}),
|
||||
createButton({ label: "+", onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER) }),
|
||||
]),
|
||||
section(L("B01_Dashboard_Company"), buildCompanyPanel(state), true),
|
||||
);
|
||||
@@ -267,473 +194,3 @@ function buildHeader(user: DashboardUser): HTMLElement {
|
||||
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 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[], currentUser: DashboardUser): 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"),
|
||||
L("B01_Dashboard_Table_Action"),
|
||||
],
|
||||
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),
|
||||
text(formatDate(project.updated_at)),
|
||||
actCell,
|
||||
];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function workflow(project: ProjectItem): 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";
|
||||
|
||||
const stages = project.workflow_state?.stages;
|
||||
const stepLabels = workflowSteps();
|
||||
|
||||
routes.forEach((route, index) => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "b01-dashboard__step";
|
||||
button.textContent = stepLabels[index] ?? `B${String(index + 3).padStart(2, "0")}`;
|
||||
|
||||
// 스텝바는 항상 자유롭게 이동 가능(게이팅하지 않음).
|
||||
// 단계 완료/무효화 판정은 각 페이지의 액션(업로드·분석 실행 등) 시 백엔드가
|
||||
// 계산·DB 갱신하며, 여기서는 그 결과를 색상/툴팁으로 표시만 한다.
|
||||
button.classList.add("is-enabled");
|
||||
button.addEventListener("click", () => {
|
||||
localStorage.setItem(CURRENT_PROJECT_ID_KEY, project.id);
|
||||
navigateTo(route);
|
||||
});
|
||||
|
||||
if (stages && stages[index]) {
|
||||
const state = stages[index].state;
|
||||
button.classList.add(`state-${state.toLowerCase()}`);
|
||||
if (state === "STALE") {
|
||||
button.title = "Stale (하위 단계 변경으로 무효화됨)";
|
||||
} else if (state === "FAILED") {
|
||||
button.title = "Failed (실패)";
|
||||
} else if (state === "COMPLETE") {
|
||||
button.title = "Complete (완료)";
|
||||
} else if (state === "IN_PROGRESS") {
|
||||
button.title = "In Progress (진행 중)";
|
||||
} else {
|
||||
button.title = "Not Started (미실행)";
|
||||
}
|
||||
}
|
||||
|
||||
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[], currentUser: DashboardUser): 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) => {
|
||||
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,
|
||||
];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
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 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[], currentUser: DashboardUser): 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_Account_Field_Phone"),
|
||||
L("B01_Dashboard_Table_Role"),
|
||||
L("B01_Dashboard_Table_Status"),
|
||||
L("B01_Dashboard_Table_Action"),
|
||||
],
|
||||
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.position),
|
||||
text(user.department),
|
||||
text(user.phone),
|
||||
text(user.role),
|
||||
text(user.status),
|
||||
actionsEl,
|
||||
];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) : "-";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { isBlank } from "@util/common_util_validate";
|
||||
import { createButton, createInputField } from "@ui/ui_template_elements";
|
||||
import { changePassword, updateUserProfile, type DashboardUser } from "./B01_Dashboard_Api_Fetch";
|
||||
import { L, runRequest } from "./B01_Dashboard_UI_Common";
|
||||
|
||||
const PASSWORD_MIN_LENGTH = 8;
|
||||
|
||||
export 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;
|
||||
}
|
||||
|
||||
export 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;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { createStepBar } from "@ui/ui_template_workflow_layout";
|
||||
import { workflowSteps } from "../A00_Common/b_page_scaffold";
|
||||
import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../A00_Common/b_workflow_nav";
|
||||
import type { DashboardUser, ProjectItem } from "./B01_Dashboard_Api_Fetch";
|
||||
import { canDeleteProject, canEditProject, canManageAutomation } from "./B01_Dashboard_UI_Helper";
|
||||
import {
|
||||
openAutomationModal,
|
||||
openDeleteProjectModal,
|
||||
openEditProjectModal,
|
||||
} from "./B01_Dashboard_UI_Modals";
|
||||
import { formatDate, L, table, text } from "./B01_Dashboard_UI_Common";
|
||||
|
||||
export function projectTable(projects: ProjectItem[], currentUser: DashboardUser): 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"),
|
||||
L("B01_Dashboard_Table_Action"),
|
||||
],
|
||||
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),
|
||||
text(formatDate(project.updated_at)),
|
||||
actCell,
|
||||
];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function workflow(project: ProjectItem): HTMLElement {
|
||||
return createStepBar(
|
||||
workflowSteps(),
|
||||
project.workflow_state?.current_stage ?? project.workflow_stage,
|
||||
{
|
||||
stages: project.workflow_state?.stages,
|
||||
currentStage: project.workflow_state?.current_stage,
|
||||
routes: WORKFLOW_STEP_ROUTES,
|
||||
compact: true,
|
||||
onStepClick: (stepIndex) => goToWorkflowStage(project.id, WORKFLOW_STEP_ROUTES[stepIndex]),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -81,30 +81,6 @@
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
.b01-dashboard__workflow {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.b01-dashboard__step {
|
||||
height: 28px;
|
||||
padding: 0 var(--spacing-12);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-pills);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-caption);
|
||||
white-space: nowrap;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.b01-dashboard__step.is-enabled {
|
||||
background: var(--color-mist-violet);
|
||||
color: var(--color-accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.b01-dashboard__metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
|
||||
Reference in New Issue
Block a user