PC6 完善 PC 工作台与审批信息架构

这个提交包含在:
2026-05-12 02:34:15 +08:00
父节点 e7bf57380e
当前提交 aec660a8fe
共修改 13 个文件,包含 518 行新增和 74 行删除
+79 -9
查看文件
@@ -10,6 +10,30 @@ const { currentDateString, parseDateOnlyString } = require("../utils/date");
const PORTAL_RECENT_ACTIVITY_LIMIT = 15;
const PORTAL_RECENT_ACTIVITY_SOURCE_TAKE = 24;
const BUSINESS_APPROVE_PERMISSIONS = [
"expense.approve",
"leave.approve",
"procurement.approve",
"travel.approve",
];
const BUSINESS_SUBMIT_PERMISSIONS = [
"expense.create",
"expense.submit",
"leave.create",
"leave.submit",
"procurement.create",
"procurement.submit",
"travel.create",
"travel.submit",
];
const SYSTEM_MANAGE_PERMISSIONS = [
"system.users",
"system.settings",
"system.audit.view",
"notification.channel.manage",
];
const FINANCE_ROLE_CODES = new Set(["finance", "finance_mgr"]);
const BUSINESS_APPROVER_ROLE_CODES = new Set(["manager", "ceo", "finance", "finance_mgr", "hr"]);
async function buildTaskVisibilityFilters(tx, request) {
const roleFilters = (request.employee.roleCodes ?? []).length
@@ -18,6 +42,30 @@ async function buildTaskVisibilityFilters(tx, request) {
return [{ assignee_id: request.employee.id }, ...roleFilters];
}
function hasAnyPermission(fastify, employee, permissionCodes) {
return permissionCodes.some((permissionCode) => fastify.hasPermission(employee, permissionCode));
}
function buildRoleCapabilities(fastify, employee) {
const roleCodes = employee.roleCodes ?? [];
const isAdmin = roleCodes.includes("admin") || fastify.hasPermission(employee, "*");
const hasBusinessApproverRole = roleCodes.some((roleCode) => BUSINESS_APPROVER_ROLE_CODES.has(roleCode));
const canUseFinance =
roleCodes.some((roleCode) => FINANCE_ROLE_CODES.has(roleCode)) &&
hasAnyPermission(fastify, employee, ["payment.batch.manage", "risk.review.manage", "expense.finance.pay"]);
return {
canSubmitApplications: hasAnyPermission(fastify, employee, BUSINESS_SUBMIT_PERMISSIONS),
canApproveTasks: hasBusinessApproverRole && hasAnyPermission(fastify, employee, BUSINESS_APPROVE_PERMISSIONS),
canUseFinance,
canManageSystem: isAdmin || hasAnyPermission(fastify, employee, SYSTEM_MANAGE_PERMISSIONS),
canViewTeam:
hasBusinessApproverRole ||
hasAnyPermission(fastify, employee, ["report.dashboard.view", "system.users", "project.manage"]),
isAdminOnly: isAdmin && !hasBusinessApproverRole,
};
}
async function ensureFeatureEnabled(fastify, request) {
const gate = await evaluateFeatureFlag({
prisma: fastify.prisma,
@@ -410,6 +458,7 @@ async function portalRoutes(fastify) {
const today = currentDateString(fastify.config.appTimezone);
const currentWeekStart = resolveWeekStart(today);
const assigneeFilters = await buildTaskVisibilityFilters(fastify.prisma, request);
const roleCapabilities = buildRoleCapabilities(fastify, request.employee);
const isManagerView =
fastify.hasPermission(request.employee, "system.users") ||
fastify.hasPermission(request.employee, "report.dashboard.view") ||
@@ -447,6 +496,8 @@ async function portalRoutes(fastify) {
openProjectRiskCount,
projectFollowUpDueTodayCount,
closurePendingProjectCount,
pendingFinanceBatchCount,
failedFinanceItemCount,
recentActivities,
teamStatusBoard,
] = await Promise.all([
@@ -454,7 +505,7 @@ async function portalRoutes(fastify) {
fastify.prisma.reimbursementForm.count({ where: { OR: [{ owner_id: request.employee.id }, { submitter_id: request.employee.id }] } }),
fastify.prisma.leaveRequest.count({ where: { owner_id: request.employee.id } }),
fastify.prisma.assetCard.count({ where: { assignee_employee_id: request.employee.id } }),
fastify.hasPermission(request.employee, "payment.batch.manage") || fastify.hasPermission(request.employee, "risk.review.manage")
roleCapabilities.canUseFinance
? fastify.prisma.entityRiskFlag.count({ where: { status: "open" } })
: Promise.resolve(0),
featureEnabled.announcements
@@ -556,6 +607,14 @@ async function portalRoutes(fastify) {
select: { project_id: true },
}).then((items) => items.length)
: Promise.resolve(0),
roleCapabilities.canUseFinance
? fastify.prisma.paymentBatch.count({
where: { status: { in: ["draft", "reviewed", "failed"] } },
})
: Promise.resolve(0),
roleCapabilities.canUseFinance
? fastify.prisma.paymentBatchItem.count({ where: { status: "failed" } })
: Promise.resolve(0),
buildRecentActivities(fastify.prisma),
buildTeamStatusBoard(fastify.prisma, today),
]);
@@ -619,13 +678,15 @@ async function portalRoutes(fastify) {
href: "/people",
severity: (assetReturnPendingCount ?? 0) > 0 ? "warning" : "normal",
});
cards.push({
key: "pending_approvals",
title: "审批待办",
value: pendingTaskCount,
href: "/workflow/tasks",
severity: pendingTaskCount > 0 ? "warning" : "normal",
});
if (roleCapabilities.canApproveTasks) {
cards.push({
key: "pending_approvals",
title: "审批待办",
value: pendingTaskCount,
href: "/workflow/tasks",
severity: pendingTaskCount > 0 ? "warning" : "normal",
});
}
if (featureEnabled.projectDomain) {
cards.push({ key: "project_milestones", title: "逾期里程碑", value: overdueProjectMilestoneCount, href: "/projects", severity: overdueProjectMilestoneCount > 0 ? "warning" : "normal" });
cards.push({ key: "project_risks", title: "项目风险", value: openProjectRiskCount, href: "/projects", severity: openProjectRiskCount > 0 ? "high" : "normal" });
@@ -660,7 +721,7 @@ async function portalRoutes(fastify) {
}
}
if (featureEnabled.paymentBatch && riskCount > 0) {
if (featureEnabled.paymentBatch && roleCapabilities.canUseFinance && riskCount > 0) {
cards.push({ key: "risk_flags", title: "待复核风险", value: riskCount, href: "/ap-operations", severity: "high" });
}
@@ -747,6 +808,15 @@ async function portalRoutes(fastify) {
href: "/people",
}
: null,
roleCapabilities,
financeSummary: roleCapabilities.canUseFinance
? {
pendingBatchCount: pendingFinanceBatchCount,
failedItemCount: failedFinanceItemCount,
riskCount,
href: "/ap-operations",
}
: null,
adoptionSummary,
recentActivities,
teamStatusBoard,
+6
查看文件
@@ -51,6 +51,7 @@ const BATCH_REJECT_BODY_SCHEMA = z.object({
const TASK_LIST_QUERY_SCHEMA = z.object({
status: z.enum(["pending", "completed", "rejected", "cancelled", "skipped"]).optional(),
entityType: z.enum(["reimbursement", "leave", "purchase_request", "travel_request"]).optional(),
page: z.coerce.number().int().min(1).optional(),
pageSize: z.coerce.number().int().min(1).max(100).optional(),
});
@@ -250,6 +251,7 @@ function parseTaskListQuery(query) {
return {
status: parsed.data.status ?? "pending",
entityType: parsed.data.entityType ?? null,
page: parsed.data.page ?? 1,
pageSize: parsed.data.pageSize ?? 20,
};
@@ -1543,6 +1545,9 @@ async function workflowRoutes(fastify) {
const where = {
status: query.status,
OR: assigneeFilters,
...(query.entityType
? { workflow_instance: { is: { entity_type: query.entityType } } }
: {}),
};
const [tasks, total] = await Promise.all([
@@ -2663,5 +2668,6 @@ module.exports.__testables = {
buildWorkflowEntityMeta,
handlePurchaseApprovalErpSync,
listAllowedActionsForTask,
parseTaskListQuery,
resolveWorkflowOutcomeNotificationType,
};