292 行
14 KiB
JavaScript
292 行
14 KiB
JavaScript
const { FEATURE_FLAG_KEYS, evaluateFeatureFlag } = require("./featureFlags");
|
|
|
|
const WORKSPACE_PROFILES = Object.freeze({
|
|
smallTeamInternal: "small_team_internal",
|
|
});
|
|
|
|
const MODULE_VISIBILITY_MODES = Object.freeze(["main", "admin", "hidden"]);
|
|
const BUSINESS_APPROVE_PERMISSIONS = Object.freeze([
|
|
"expense.approve",
|
|
"leave.approve",
|
|
"attendance.approve",
|
|
"procurement.approve",
|
|
"travel.approve",
|
|
]);
|
|
const BUSINESS_APPROVER_ROLE_CODES = new Set(["manager", "ceo", "finance", "finance_mgr", "hr"]);
|
|
const FINANCE_ROLE_CODES = new Set(["finance", "finance_mgr"]);
|
|
const FINANCE_PERMISSION_CODES = Object.freeze(["payment.batch.manage", "risk.review.manage", "expense.finance.pay"]);
|
|
const FINANCE_NAVIGATION_MODULES = new Set(["paymentBatches", "apOperations", "invoicePool"]);
|
|
|
|
const NAVIGATION_REGISTRY = Object.freeze({
|
|
portal: { key: "portal", label: "Portal", icon: "home_app_logo", href: "/portal", anyOf: [] },
|
|
team: { key: "team", label: "团队", icon: "groups", href: "/team", anyOf: [], featureFlagKey: FEATURE_FLAG_KEYS.presenceBoard },
|
|
leaveRequests: { key: "leaveRequests", label: "请假", icon: "event_available", href: "/leave-requests", anyOf: ["leave.view_own", "leave.view_all", "leave.manage"] },
|
|
attendance: { key: "attendance", label: "考勤", icon: "schedule", href: "/attendance", anyOf: [], featureFlagKey: FEATURE_FLAG_KEYS.attendanceUnified },
|
|
reimbursements: { key: "reimbursements", label: "报销", icon: "request_quote", href: "/reimbursements", anyOf: ["expense.view_own", "expense.view_all"] },
|
|
assets: { key: "assets", label: "资产", icon: "inventory_2", href: "/assets", anyOf: ["asset.view_own", "asset.view_all", "asset.manage", "asset.transfer", "asset.inventory"] },
|
|
weeklyReports: { key: "weeklyReports", label: "周报", icon: "article", href: "/weekly-reports", anyOf: [], featureFlagKey: FEATURE_FLAG_KEYS.weeklyReport },
|
|
todos: { key: "todos", label: "待办", icon: "check_circle", href: "/todos", anyOf: [], featureFlagKey: FEATURE_FLAG_KEYS.todoBoard },
|
|
announcements: { key: "announcements", label: "公告", icon: "campaign", href: "/announcements", anyOf: [], featureFlagKey: FEATURE_FLAG_KEYS.announcements },
|
|
projects: { key: "projects", label: "项目", icon: "workspaces", href: "/projects", anyOf: ["project.view_own", "project.view_all", "project.manage"], featureFlagKey: FEATURE_FLAG_KEYS.projectDomain },
|
|
|
|
workspace: { key: "workspace", label: "工作区设置", icon: "tune", href: "/system/workspace", anyOf: ["system.settings"] },
|
|
people: { key: "people", label: "人员管理", icon: "group", href: "/people", anyOf: ["system.users", "report.dashboard.view"], featureFlagKey: FEATURE_FLAG_KEYS.employeeLifecycle },
|
|
organization: { key: "organization", label: "组织架构", icon: "account_tree", href: "/organization", anyOf: ["system.users", "report.dashboard.view"], featureFlagKey: FEATURE_FLAG_KEYS.employeeLifecycle },
|
|
contractsRegistry: { key: "contractsRegistry", label: "合同台账", icon: "description", href: "/contracts-registry", anyOf: ["project.manage", "project.view_all", "system.settings"], featureFlagKey: FEATURE_FLAG_KEYS.projectLinking },
|
|
workflowDefinitions: { key: "workflowDefinitions", label: "流程定义", icon: "schema", href: "/system/workflow-definitions", anyOf: ["system.settings"], featureFlagKey: FEATURE_FLAG_KEYS.workflowDefinitionAdmin },
|
|
systemSettings: { key: "systemSettings", label: "系统设置", icon: "settings", href: "/system/settings", anyOf: ["system.settings", "system.users", "masterdata.manage", "notification.channel.manage", "integration.manage"] },
|
|
featureFlags: { key: "featureFlags", label: "功能开关", icon: "toggle_on", href: "/system/feature-flags", anyOf: ["system.settings"] },
|
|
auditLogs: { key: "auditLogs", label: "审计日志", icon: "manage_search", href: "/audit-logs", anyOf: ["system.audit.view"] },
|
|
|
|
approvalRules: { key: "approvalRules", label: "审批规则", icon: "rule", href: "/system/approval-rules", anyOf: ["system.settings"] },
|
|
userManagement: { key: "userManagement", label: "用户管理", icon: "badge", href: "/system/users", anyOf: ["system.users"] },
|
|
departments: { key: "departments", label: "部门管理", icon: "account_tree", href: "/system/departments", anyOf: ["system.users"] },
|
|
masterData: { key: "masterData", label: "主数据", icon: "dataset", href: "/system/master-data", anyOf: ["masterdata.view", "masterdata.manage"] },
|
|
notificationChannels: { key: "notificationChannels", label: "通知渠道", icon: "notifications_active", href: "/system/notification-channels", anyOf: ["notification.channel.manage"] },
|
|
workflowTasks: { key: "workflowTasks", label: "审批待办", icon: "fact_check", href: "/workflow/tasks", anyOf: ["expense.approve", "leave.approve", "attendance.approve", "procurement.approve", "travel.approve"] },
|
|
dashboard: { key: "dashboard", label: "仪表盘", icon: "dashboard", href: "/dashboard", anyOf: ["report.dashboard.view"] },
|
|
purchaseRequests: { key: "purchaseRequests", label: "采购申请", icon: "shopping_cart", href: "/purchase-requests", anyOf: ["procurement.create", "procurement.view_own", "procurement.view_all", "procurement.manage"] },
|
|
travelRequests: { key: "travelRequests", label: "差旅申请", icon: "flight", href: "/travel-requests", anyOf: ["travel.view_all", "travel.manage", "travel.payment.manage"] },
|
|
budgets: { key: "budgets", label: "预算管理", icon: "account_balance", href: "/budgets", anyOf: ["budget.view", "budget.manage"] },
|
|
paymentBatches: { key: "paymentBatches", label: "支付批次", icon: "payments", href: "/payment-batches", anyOf: ["payment.batch.manage", "risk.review.manage"], featureFlagKey: FEATURE_FLAG_KEYS.paymentBatch },
|
|
apOperations: { key: "apOperations", label: "AP 运营", icon: "account_balance_wallet", href: "/ap-operations", anyOf: ["payment.batch.manage", "risk.review.manage"], featureFlagKey: FEATURE_FLAG_KEYS.paymentBatch },
|
|
invoicePool: { key: "invoicePool", label: "票据中心", icon: "receipt_long", href: "/invoice-pool", anyOf: ["invoice.manage"] },
|
|
inventoryChecks: { key: "inventoryChecks", label: "资产盘点", icon: "inventory", href: "/inventory-checks", anyOf: ["asset.inventory"] },
|
|
cutover: { key: "cutover", label: "切换控制台", icon: "swap_calls", href: "/system/cutover", anyOf: ["system.settings"], featureFlagKey: FEATURE_FLAG_KEYS.cutoverConsole },
|
|
erpIntegrations: { key: "erpIntegrations", label: "ERP 集成", icon: "hub", href: "/system/integrations/erp", anyOf: ["integration.manage"] },
|
|
profile: { key: "profile", label: "个人设置", icon: "manage_accounts", href: "/settings/profile", anyOf: [] },
|
|
});
|
|
|
|
const DEFAULT_MODULE_VISIBILITY = Object.freeze({
|
|
portal: "main",
|
|
team: "main",
|
|
leaveRequests: "main",
|
|
attendance: "hidden",
|
|
reimbursements: "main",
|
|
assets: "main",
|
|
weeklyReports: "main",
|
|
todos: "main",
|
|
announcements: "main",
|
|
projects: "main",
|
|
|
|
workspace: "admin",
|
|
people: "admin",
|
|
organization: "admin",
|
|
contractsRegistry: "admin",
|
|
workflowDefinitions: "admin",
|
|
systemSettings: "admin",
|
|
featureFlags: "admin",
|
|
auditLogs: "admin",
|
|
|
|
approvalRules: "admin",
|
|
userManagement: "admin",
|
|
departments: "admin",
|
|
masterData: "admin",
|
|
notificationChannels: "admin",
|
|
workflowTasks: "admin",
|
|
dashboard: "hidden",
|
|
purchaseRequests: "admin",
|
|
travelRequests: "admin",
|
|
budgets: "admin",
|
|
paymentBatches: "admin",
|
|
apOperations: "admin",
|
|
invoicePool: "admin",
|
|
inventoryChecks: "admin",
|
|
cutover: "admin",
|
|
erpIntegrations: "admin",
|
|
profile: "hidden",
|
|
});
|
|
|
|
function normalizeModuleVisibilityJson(input) {
|
|
const result = { ...DEFAULT_MODULE_VISIBILITY };
|
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
return result;
|
|
}
|
|
|
|
for (const [key, rawValue] of Object.entries(input)) {
|
|
if (!(key in DEFAULT_MODULE_VISIBILITY)) {
|
|
continue;
|
|
}
|
|
const value = String(rawValue ?? "").trim();
|
|
if (MODULE_VISIBILITY_MODES.includes(value)) {
|
|
result[key] = value;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function buildDefaultWorkspaceSetting() {
|
|
return {
|
|
id: null,
|
|
persisted: false,
|
|
profile: WORKSPACE_PROFILES.smallTeamInternal,
|
|
orgMode: "single_company",
|
|
primaryEntryMode: "password",
|
|
notificationStrategy: "in_app",
|
|
devicePriority: "desktop",
|
|
moduleVisibility: { ...DEFAULT_MODULE_VISIBILITY },
|
|
createdAt: null,
|
|
updatedAt: null,
|
|
updatedBy: null,
|
|
};
|
|
}
|
|
|
|
function mapWorkspaceSetting(record) {
|
|
if (!record) {
|
|
return buildDefaultWorkspaceSetting();
|
|
}
|
|
|
|
return {
|
|
id: record.id,
|
|
persisted: true,
|
|
profile: record.profile,
|
|
orgMode: record.org_mode,
|
|
primaryEntryMode: record.primary_entry_mode,
|
|
notificationStrategy: record.notification_strategy,
|
|
devicePriority: record.device_priority,
|
|
moduleVisibility: normalizeModuleVisibilityJson(record.module_visibility_json),
|
|
createdAt: record.created_at?.toISOString?.() ?? null,
|
|
updatedAt: record.updated_at?.toISOString?.() ?? null,
|
|
updatedBy: record.updated_by ?? null,
|
|
};
|
|
}
|
|
|
|
async function getWorkspaceSettingRecord(prisma) {
|
|
return prisma.workspaceSetting.findFirst({ orderBy: { created_at: "asc" } });
|
|
}
|
|
|
|
async function getWorkspaceSetting(prisma) {
|
|
const record = await getWorkspaceSettingRecord(prisma);
|
|
return mapWorkspaceSetting(record);
|
|
}
|
|
|
|
async function upsertWorkspaceSetting(prisma, actorId, input = {}) {
|
|
const current = await getWorkspaceSettingRecord(prisma);
|
|
const data = {
|
|
profile: WORKSPACE_PROFILES.smallTeamInternal,
|
|
org_mode: "single_company",
|
|
primary_entry_mode: "password",
|
|
notification_strategy: "in_app",
|
|
device_priority: "desktop",
|
|
module_visibility_json: normalizeModuleVisibilityJson(input.moduleVisibility ?? input.module_visibility_json ?? current?.module_visibility_json ?? null),
|
|
updated_by: actorId ?? null,
|
|
};
|
|
|
|
const record = current
|
|
? await prisma.workspaceSetting.update({ where: { id: current.id }, data })
|
|
: await prisma.workspaceSetting.create({ data });
|
|
|
|
return mapWorkspaceSetting(record);
|
|
}
|
|
|
|
function hasAnyPermission(hasPermission, employee, anyOf = []) {
|
|
if (!anyOf.length) {
|
|
return true;
|
|
}
|
|
return anyOf.some((permissionCode) => hasPermission(employee, permissionCode));
|
|
}
|
|
|
|
function hasBusinessApproverRole(hasPermission, employee) {
|
|
const roleCodes = employee?.roleCodes ?? [];
|
|
return (
|
|
roleCodes.some((roleCode) => BUSINESS_APPROVER_ROLE_CODES.has(roleCode)) &&
|
|
hasAnyPermission(hasPermission, employee, BUSINESS_APPROVE_PERMISSIONS)
|
|
);
|
|
}
|
|
|
|
function hasFinanceRole(hasPermission, employee) {
|
|
const roleCodes = employee?.roleCodes ?? [];
|
|
return (
|
|
roleCodes.some((roleCode) => FINANCE_ROLE_CODES.has(roleCode)) &&
|
|
hasAnyPermission(hasPermission, employee, FINANCE_PERMISSION_CODES)
|
|
);
|
|
}
|
|
|
|
async function resolveModuleGate({ prisma, config, employee, moduleKey, registryItem }) {
|
|
if (!registryItem?.featureFlagKey) {
|
|
return { enabled: true, reason: "no_feature_gate" };
|
|
}
|
|
|
|
return evaluateFeatureFlag({
|
|
prisma,
|
|
config,
|
|
flagKey: registryItem.featureFlagKey,
|
|
departmentId: employee?.department_id ?? null,
|
|
strictDepartment: false,
|
|
});
|
|
}
|
|
|
|
async function getSystemNavigation({ prisma, config, employee, hasPermission }) {
|
|
const workspace = await getWorkspaceSetting(prisma);
|
|
const sections = { main: [], admin: [], backup: [] };
|
|
|
|
for (const [moduleKey, registryItem] of Object.entries(NAVIGATION_REGISTRY)) {
|
|
const placement = workspace.moduleVisibility[moduleKey] ?? DEFAULT_MODULE_VISIBILITY[moduleKey] ?? "hidden";
|
|
if (placement === "hidden") {
|
|
continue;
|
|
}
|
|
|
|
if (!hasAnyPermission(hasPermission, employee, registryItem.anyOf)) {
|
|
continue;
|
|
}
|
|
|
|
if (moduleKey === "workflowTasks" && !hasBusinessApproverRole(hasPermission, employee)) {
|
|
continue;
|
|
}
|
|
|
|
if (FINANCE_NAVIGATION_MODULES.has(moduleKey) && !hasFinanceRole(hasPermission, employee)) {
|
|
continue;
|
|
}
|
|
|
|
const gate = await resolveModuleGate({ prisma, config, employee, moduleKey, registryItem });
|
|
if (!gate.enabled) {
|
|
continue;
|
|
}
|
|
|
|
const entry = {
|
|
key: registryItem.key,
|
|
label: registryItem.label,
|
|
icon: registryItem.icon,
|
|
href: registryItem.href,
|
|
placement,
|
|
permissionCodes: registryItem.anyOf,
|
|
featureFlagKey: registryItem.featureFlagKey ?? null,
|
|
};
|
|
|
|
if (["purchaseRequests", "travelRequests", "budgets", "paymentBatches", "apOperations", "invoicePool", "inventoryChecks", "cutover", "erpIntegrations", "approvalRules", "userManagement", "departments", "masterData", "notificationChannels", "workflowTasks", "dashboard"].includes(moduleKey)) {
|
|
sections.backup.push(entry);
|
|
continue;
|
|
}
|
|
|
|
if (placement === "admin") {
|
|
sections.admin.push(entry);
|
|
} else {
|
|
sections.main.push(entry);
|
|
}
|
|
}
|
|
|
|
return {
|
|
workspace,
|
|
sections,
|
|
recommendedRolePresets: [
|
|
{ key: "owner", label: "老板", roleCodes: ["ceo"], description: "查看团队状态、审批汇总与关键管理入口。" },
|
|
{ key: "admin", label: "管理员", roleCodes: ["admin"], description: "管理工作区、人员、流程、开关与审计。" },
|
|
{ key: "employee", label: "员工", roleCodes: ["employee"], description: "日常使用 Portal、请假、报销、周报、待办。" },
|
|
{ key: "manager", label: "主管", roleCodes: ["manager"], description: "处理审批、查看团队周报与在岗状态。" },
|
|
],
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
WORKSPACE_PROFILES,
|
|
NAVIGATION_REGISTRY,
|
|
DEFAULT_MODULE_VISIBILITY,
|
|
normalizeModuleVisibilityJson,
|
|
buildDefaultWorkspaceSetting,
|
|
mapWorkspaceSetting,
|
|
getWorkspaceSetting,
|
|
upsertWorkspaceSetting,
|
|
getSystemNavigation,
|
|
};
|