feat: 完成 R17-4 试点观测支撑
这个提交包含在:
+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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户