feat: 完成 R17-4 试点观测支撑

这个提交包含在:
2026-03-21 22:04:30 +08:00
父节点 736c803d19
当前提交 5e9ef9cab2
共修改 9 个文件,包含 169 行新增和 12 行删除
+65 -3
查看文件
@@ -146,6 +146,26 @@ function buildQuickLinks({ request, featureEnabled }) {
return items.slice(0, 8);
}
function buildAdoptionHints({ activeEmployeeCount, login7dCount, weeklySummary, teamPresenceSummary, offboardingCount, assetReturnPendingCount }) {
const hints = [];
if (login7dCount < activeEmployeeCount) {
hints.push(`近 7 天仍有 ${Math.max(0, activeEmployeeCount - login7dCount)} 人未登录系统`);
}
if ((weeklySummary?.missingCount ?? 0) > 0) {
hints.push(`本周仍有 ${weeklySummary.missingCount} 人未提交周报`);
}
if ((teamPresenceSummary?.unspecified ?? 0) > 0) {
hints.push(`今日仍有 ${teamPresenceSummary.unspecified} 人未更新团队状态`);
}
if ((offboardingCount ?? 0) > 0 || (assetReturnPendingCount ?? 0) > 0) {
hints.push(`仍有 ${offboardingCount ?? 0} 条离职流程和 ${assetReturnPendingCount ?? 0} 项资产交接待收口`);
}
if (!hints.length) {
hints.push("近 7 天登录、周报、团队状态与离职交接均满足试点观测口径");
}
return hints;
}
async function portalRoutes(fastify) {
fastify.get("/portal/home", { preHandler: fastify.authenticate }, async (request, reply) => {
await ensureFeatureEnabled(fastify, request);
@@ -180,6 +200,8 @@ async function portalRoutes(fastify) {
onboardingCount,
offboardingCount,
assetReturnPendingCount,
activeEmployeeCount,
login7dCount,
] = 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 }] } }),
@@ -225,6 +247,19 @@ async function portalRoutes(fastify) {
featureEnabled.employeeLifecycle && isManagerView
? fastify.prisma.assetCard.count({ where: { assignee: { is: { employment_status: "offboarding" } } } })
: Promise.resolve(0),
isManagerView ? fastify.prisma.employee.count({ where: { status: "active" } }) : Promise.resolve(0),
isManagerView
? fastify.prisma.authSession.findMany({
where: {
revoked_at: null,
expires_at: { gt: new Date() },
last_seen_at: { gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) },
employee: { status: "active" },
},
select: { employee_id: true },
distinct: ["employee_id"],
}).then((rows) => rows.length)
: Promise.resolve(0),
]);
const todayPresence = featureEnabled.presenceBoard
@@ -236,15 +271,14 @@ async function portalRoutes(fastify) {
const teamWeeklySummary = featureEnabled.weeklyReport && isManagerView
? await (async () => {
const [employeeCount, submittedCount, needsHelpCount] = await Promise.all([
fastify.prisma.employee.count({ where: { status: "active" } }),
const [submittedCount, needsHelpCount] = await Promise.all([
fastify.prisma.weeklyReport.count({ where: { week_start: parseDateOnlyString(currentWeekStart), status: "submitted" } }),
fastify.prisma.weeklyReport.count({ where: { week_start: parseDateOnlyString(currentWeekStart), needs_help: true } }),
]);
return {
weekStart: currentWeekStart,
submittedCount,
missingCount: Math.max(0, employeeCount - submittedCount),
missingCount: Math.max(0, activeEmployeeCount - submittedCount),
needsHelpCount,
};
})()
@@ -325,6 +359,33 @@ async function portalRoutes(fastify) {
cards.push({ key: "risk_flags", title: "待复核风险", value: riskCount, href: "/ap-operations", severity: "high" });
}
const adoptionSummary = isManagerView
? (() => {
const presenceUpdatedCount = teamPresenceSummary ? teamPresenceSummary.total - teamPresenceSummary.unspecified : 0;
const weeklySubmittedCount = teamWeeklySummary?.submittedCount ?? 0;
return {
activeEmployeeCount,
login7dCount,
weeklySubmittedCount,
weeklyMissingCount: teamWeeklySummary?.missingCount ?? 0,
presenceUpdatedCount,
presenceMissingCount: teamPresenceSummary?.unspecified ?? 0,
announcementCount: announcementRows.length,
login7dRate: activeEmployeeCount > 0 ? Number(((login7dCount / activeEmployeeCount) * 100).toFixed(1)) : 0,
weeklySubmissionRate: activeEmployeeCount > 0 ? Number(((weeklySubmittedCount / activeEmployeeCount) * 100).toFixed(1)) : 0,
presenceCoverageRate: activeEmployeeCount > 0 ? Number(((presenceUpdatedCount / activeEmployeeCount) * 100).toFixed(1)) : 0,
hints: buildAdoptionHints({
activeEmployeeCount,
login7dCount,
weeklySummary: teamWeeklySummary,
teamPresenceSummary,
offboardingCount,
assetReturnPendingCount,
}),
};
})()
: null;
return sendData(reply, request, {
workspace,
greeting: { name: request.employee.name ?? request.employee.id },
@@ -370,6 +431,7 @@ async function portalRoutes(fastify) {
href: "/people",
}
: null,
adoptionSummary,
});
});
}