diff --git a/backend/src/services/risk.service.ts b/backend/src/services/risk.service.ts index dae8f27..1522f3a 100644 --- a/backend/src/services/risk.service.ts +++ b/backend/src/services/risk.service.ts @@ -447,9 +447,16 @@ export async function runRiskDetection(orgId: string) { // 按 employeeId:type 归并,不依赖 actionUrl(actionUrl 可能因天数变化而不同) const existingRisks = await prisma.riskItem.findMany({ where: { orgId, status: { in: ['PENDING', 'RESOLVED', 'IGNORED'] } }, - select: { employeeId: true, type: true }, + select: { id: true, employeeId: true, type: true, title: true, status: true }, }) const existingKeys = new Set(existingRisks.map((r: typeof existingRisks[number]) => `${r.employeeId}:${r.type}`)) + // PENDING 风险的 employeeId:type → record 映射,用于更新标题 + const pendingRiskMap = new Map() + for (const r of existingRisks) { + if (r.status === 'PENDING' && r.employeeId) { + pendingRiskMap.set(`${r.employeeId}:${r.type}`, r) + } + } // 当月任务去重:检查所有状态(含 RESOLVED/IGNORED),避免已完成的当月任务被重新创建 const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}` @@ -477,10 +484,41 @@ export async function runRiskDetection(orgId: string) { // 月度任务用 monthlyKeys 去重,其他任务用 existingKeys 去重 const nonMonthlyRisks = [...contractRisks, ...terminationRisks, ...onboardingRisks, ...specialStatusRisks] const toCreate = [ - ...nonMonthlyRisks.filter((r) => !existingKeys.has(`${r.employeeId}:${r.type}:${r.actionUrl}`)), + ...nonMonthlyRisks.filter((r) => !existingKeys.has(`${r.employeeId}:${r.type}`)), ...monthlyTasks.filter((r) => !monthlyKeys.has(`${r.employeeId}:${r.title}`)), ] + // 更新已存在的 PENDING 风险:标题/量化信息可能因天数变化而变化 + const toUpdate: { id: string; title: string; description: string; estimatedLoss: number; lossRange: [number, number]; deadline?: Date }[] = [] + for (const r of nonMonthlyRisks) { + const key = `${r.employeeId}:${r.type}` + const existing = pendingRiskMap.get(key) + if (existing && existing.title !== r.title) { + const emp = r.employeeId ? empMap.get(r.employeeId) : null + const cost = estimateRiskCost(r, emp) + toUpdate.push({ + id: existing.id, + title: r.title, + description: r.description, + estimatedLoss: cost.estimatedLoss, + lossRange: cost.lossRange, + deadline: cost.deadline, + }) + } + } + for (const u of toUpdate) { + await prisma.riskItem.update({ + where: { id: u.id }, + data: { + title: u.title, + description: u.description, + estimatedLoss: u.estimatedLoss, + lossRange: u.lossRange, + deadline: u.deadline, + }, + }) + } + if (toCreate.length > 0) { // 为每个风险计算量化信息 const createData = toCreate.map((r) => { @@ -527,9 +565,8 @@ export async function getDashboardData(orgId: string) { prisma.riskItem.count({ where: { orgId, status: 'PENDING' } }), prisma.riskItem.findMany({ where: { orgId, status: 'PENDING' }, - include: { employee: true }, - orderBy: [{ level: 'asc' }, { createdAt: 'desc' }], - take: 10, + include: { employee: { select: { id: true, name: true, department: true, idCardHash: true } } }, + orderBy: [{ level: 'asc' }, { estimatedLoss: 'desc' }], }), prisma.riskItem.findMany({ where: { orgId, status: 'RESOLVED' }, @@ -759,9 +796,42 @@ export async function getDashboardData(orgId: string) { lossRange: r.lossRange as [number, number] | null, deadline: r.deadline?.toISOString().slice(0, 10) || null, daysUntilDeadline, + employeeName: r.employee?.name || null, + employeeDepartment: r.employee?.department || null, + employeeIdCardHash: r.employee?.idCardHash || null, } }) + // 按员工 + 风险类别去重:同一员工同一类别只保留 estimatedLoss 最大的一条 + function getTodoRiskCategoryKey(title: string): string { + if (title.includes('未签合同') && title.includes('无固定期限')) return 'CONTRACT_UNSIGNED_OVER_YEAR' + if (title.includes('未签合同') && title.includes('个月')) return 'CONTRACT_UNSIGNED' + if (title.includes('到期') && title.includes('未续签')) return 'CONTRACT_EXPIRED' + if (title.includes('即将到期')) return 'CONTRACT_EXPIRING' + if (title.includes('试用期')) return 'CONTRACT_PROBATION' + if (title.includes('入职手续未完成')) return 'ONBOARDING_INCOMPLETE' + if (title.includes('孕期') || title.includes('哺乳期')) return 'SPECIAL_PREGNANCY' + if (title.includes('工伤')) return 'SPECIAL_INJURY' + if (title.includes('医疗期')) return 'SPECIAL_MEDICAL' + if (title.includes('社保')) return 'MONTHLY_SOCIAL' + if (title.includes('公积金')) return 'MONTHLY_HOUSING' + if (title.includes('工资')) return 'MONTHLY_PAYROLL' + if (title.includes('个税')) return 'MONTHLY_TAX' + return title.replace(/\d+/g, '').trim() + } + + const dedupedTodoMap = new Map() + for (const t of todosWithCost) { + const personKey = t.employeeIdCardHash || t.employeeName || t.id + const catKey = getTodoRiskCategoryKey(t.title || '') + const dedupKey = `${personKey}:${catKey}` + const existing = dedupedTodoMap.get(dedupKey) + if (!existing || t.estimatedLoss > existing.estimatedLoss) { + dedupedTodoMap.set(dedupKey, t) + } + } + const dedupedTodos = Array.from(dedupedTodoMap.values()) + // 按优先级排序:URGENT > HIGH > MEDIUM > LOW const priorityOrder = { URGENT: 0, HIGH: 1, MEDIUM: 2, LOW: 3 } todosWithCost.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]) @@ -783,9 +853,11 @@ export async function getDashboardData(orgId: string) { })) // 紧急风险横幅:最高优先级且预估损失>0 - const urgentRisk = todosWithCost.find((t) => t.priority === 'URGENT' && t.estimatedLoss > 0) || null + const urgentRisk = dedupedTodos.find((t) => t.priority === 'URGENT' && t.estimatedLoss > 0) || null - const todos = todosWithCost + // 按优先级排序去重后的 todos + dedupedTodos.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]) + const todos = dedupedTodos const resolvedTodos = resolvedItems.map((r: typeof resolvedItems[number]) => ({ id: r.id, diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 2082a13..58b8b20 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -978,8 +978,14 @@ export default function Dashboard() {
-
- {todo.title} +
+ {todo.employeeName && ( + {todo.employeeName} + )} + {todo.employeeDepartment && ( + {todo.employeeDepartment} + )} + {todo.title} {todo.priority && priorityConfig[todo.priority] && ( {priorityConfig[todo.priority].label} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 5ef5585..448bdeb 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -108,6 +108,8 @@ export interface DashboardData { lossRange: [number, number] | null deadline: string | null daysUntilDeadline: number | null + employeeName: string | null + employeeDepartment: string | null }[] resolvedTodos: { id: string