PC6 完善 PC 工作台与审批信息架构
这个提交包含在:
+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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
在新工单中引用
屏蔽一个用户