feat(P25-1/P25-5/P26-1/P26-3/P24-1/P29-4): 落地内部OA够用版后端与试运行能力
关联 Issue: P25-1, P25-2, P25-3, P25-4, P25-5, P26-1, P26-3, P24-1, P24-2, P29-1, P29-2, P29-3, P29-4
这个提交包含在:
@@ -0,0 +1,115 @@
|
||||
const { AppError } = require("../utils/AppError");
|
||||
const { sendData } = require("../utils/reply");
|
||||
const { FEATURE_FLAG_KEYS, evaluateFeatureFlag } = require("../services/featureFlags");
|
||||
|
||||
async function buildTaskVisibilityFilters(tx, request) {
|
||||
const roleFilters = (request.employee.roleCodes ?? []).length
|
||||
? [{ assignee_role: { in: request.employee.roleCodes } }]
|
||||
: [];
|
||||
return [{ assignee_id: request.employee.id }, ...roleFilters];
|
||||
}
|
||||
|
||||
async function ensureFeatureEnabled(fastify, request) {
|
||||
const gate = await evaluateFeatureFlag({
|
||||
prisma: fastify.prisma,
|
||||
config: fastify.config,
|
||||
flagKey: FEATURE_FLAG_KEYS.portalHome,
|
||||
departmentId: request.employee.department_id ?? null,
|
||||
strictDepartment: false,
|
||||
});
|
||||
if (!gate.enabled) {
|
||||
throw new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode: 409,
|
||||
details: { featureFlag: "portal_home_v1_disabled", reason: gate.reason },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function buildQuickLinks(request) {
|
||||
const items = [];
|
||||
if (request.employee.permissionCodes.includes("expense.view_own") || request.employee.permissionCodes.includes("expense.view_all")) {
|
||||
items.push({ key: "expense", title: "我的报销", href: "/reimbursements" });
|
||||
}
|
||||
if (request.employee.permissionCodes.includes("procurement.view_own") || request.employee.permissionCodes.includes("procurement.view_all")) {
|
||||
items.push({ key: "procurement", title: "采购申请", href: "/purchase-requests" });
|
||||
}
|
||||
if (request.employee.permissionCodes.includes("travel.view_own") || request.employee.permissionCodes.includes("travel.view_all")) {
|
||||
items.push({ key: "travel", title: "差旅申请", href: "/travel-requests" });
|
||||
}
|
||||
if (request.employee.permissionCodes.includes("budget.view") || request.employee.permissionCodes.includes("budget.manage")) {
|
||||
items.push({ key: "budget", title: "预算管理", href: "/budgets" });
|
||||
}
|
||||
if (request.employee.permissionCodes.includes("payment.batch.manage")) {
|
||||
items.push({ key: "payment-batches", title: "支付批次", href: "/payment-batches" });
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
async function portalRoutes(fastify) {
|
||||
fastify.get("/portal/home", { preHandler: fastify.authenticate }, async (request, reply) => {
|
||||
await ensureFeatureEnabled(fastify, request);
|
||||
|
||||
const assigneeFilters = await buildTaskVisibilityFilters(fastify.prisma, request);
|
||||
const now = new Date();
|
||||
|
||||
const [pendingTaskCount, reimbursementsCount, leaveCount, purchaseCount, travelCount, riskCount, announcementRows, paymentBatchCount] = await Promise.all([
|
||||
fastify.prisma.workflowTask.count({ where: { status: "pending", OR: assigneeFilters } }),
|
||||
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.purchaseRequest.count({ where: { applicant_id: request.employee.id } }),
|
||||
fastify.prisma.travelRequest.count({ where: { OR: [{ applicant_employee_id: request.employee.id }, { submitter_id: request.employee.id }] } }),
|
||||
(fastify.hasPermission(request.employee, "payment.batch.manage") || fastify.hasPermission(request.employee, "risk.review.manage"))
|
||||
? fastify.prisma.entityRiskFlag.count({ where: { status: "open" } })
|
||||
: Promise.resolve(0),
|
||||
fastify.prisma.portalAnnouncement.findMany({
|
||||
where: {
|
||||
enabled: true,
|
||||
OR: [{ starts_at: null }, { starts_at: { lte: now } }],
|
||||
AND: [{ OR: [{ ends_at: null }, { ends_at: { gte: now } }] }],
|
||||
},
|
||||
orderBy: [{ priority: "asc" }, { created_at: "desc" }],
|
||||
take: 6,
|
||||
}),
|
||||
fastify.hasPermission(request.employee, "payment.batch.manage")
|
||||
? fastify.prisma.paymentBatch.count({ where: { status: { in: ["draft", "reviewed", "failed"] } } })
|
||||
: Promise.resolve(0),
|
||||
]);
|
||||
|
||||
const cards = [
|
||||
{ key: "pending_tasks", title: "我的待办", value: pendingTaskCount, href: "/workflow/tasks", severity: pendingTaskCount > 0 ? "warning" : "normal" },
|
||||
{ key: "my_reimbursements", title: "报销单", value: reimbursementsCount, href: "/reimbursements", severity: "normal" },
|
||||
{ key: "my_purchases", title: "采购单", value: purchaseCount, href: "/purchase-requests", severity: "normal" },
|
||||
{ key: "my_travel", title: "差旅单", value: travelCount, href: "/travel-requests", severity: "normal" },
|
||||
];
|
||||
if (paymentBatchCount > 0) {
|
||||
cards.push({ key: "payment_batches", title: "待处理支付批次", value: paymentBatchCount, href: "/payment-batches", severity: "warning" });
|
||||
}
|
||||
if (riskCount > 0) {
|
||||
cards.push({ key: "risk_flags", title: "待复核风险", value: riskCount, href: "/ap-operations", severity: "high" });
|
||||
}
|
||||
|
||||
return sendData(reply, request, {
|
||||
greeting: { name: request.employee.name ?? request.employee.id },
|
||||
cards,
|
||||
pendingTasks: { count: pendingTaskCount, href: "/workflow/tasks" },
|
||||
myApplications: [
|
||||
{ domain: "reimbursement", count: reimbursementsCount, href: "/reimbursements" },
|
||||
{ domain: "leave", count: leaveCount, href: "/leave-requests" },
|
||||
{ domain: "purchase_request", count: purchaseCount, href: "/purchase-requests" },
|
||||
{ domain: "travel_request", count: travelCount, href: "/travel-requests" },
|
||||
],
|
||||
riskSummary: riskCount > 0 ? { openCount: riskCount, href: "/ap-operations" } : null,
|
||||
announcements: announcementRows.map((item) => ({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
summary: item.summary,
|
||||
href: item.link_url ?? null,
|
||||
})),
|
||||
quickLinks: buildQuickLinks(request),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = portalRoutes;
|
||||
在新工单中引用
屏蔽一个用户