diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 8e8136c..d78c5f8 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -42,6 +42,7 @@ enum RiskType { SALARY TERMINATION MONTHLY + ONBOARDING } enum RiskLevel { diff --git a/backend/src/routes/roster.routes.ts b/backend/src/routes/roster.routes.ts index 9050255..4291fa7 100644 --- a/backend/src/routes/roster.routes.ts +++ b/backend/src/routes/roster.routes.ts @@ -58,11 +58,13 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => { contractType: 'UNSIGNED', hireDate: e.hireDate, }) + const isResigned = e.terminations.some((t) => t.terminationDate <= today) + const isPreHire = !isResigned && e.hireDate > today return { id: e.id, name: e.name, department: e.department, - status: e.terminations.some((t) => t.terminationDate <= today) ? 'RESIGNED' : 'ACTIVE', + status: isResigned ? 'RESIGNED' : (isPreHire ? 'PRE_HIRE' : 'ACTIVE'), hasTermination: e.terminations.length > 0, latestTerminationDate: e.terminations[0]?.terminationDate || null, latestTerminationType: e.terminations[0]?.type || null, diff --git a/backend/src/services/risk.service.ts b/backend/src/services/risk.service.ts index 573470c..b6628cb 100644 --- a/backend/src/services/risk.service.ts +++ b/backend/src/services/risk.service.ts @@ -7,8 +7,10 @@ function daysBetween(a: Date, b: Date): number { } export async function detectContractRisks(orgId: string) { + const today = new Date() + today.setHours(0, 0, 0, 0) const employees = await prisma.employee.findMany({ - where: { orgId, status: 'ACTIVE' }, + where: { orgId, status: 'ACTIVE', hireDate: { lte: today } }, include: { contracts: { orderBy: { createdAt: 'desc' } } }, }) @@ -98,6 +100,43 @@ export async function detectContractRisks(orgId: string) { return risks } +// 预入职检查:入职日期已到但未签合同 → 待办 +export async function detectOnboardingRisks(orgId: string) { + const today = new Date() + today.setHours(0, 0, 0, 0) + const employees = await prisma.employee.findMany({ + where: { orgId, status: 'ACTIVE', hireDate: { lte: today } }, + include: { + contracts: { orderBy: { createdAt: 'desc' }, take: 1 }, + terminations: { where: { terminationDate: { lte: today } }, take: 1 }, + }, + }) + + const risks: { employeeId: string; type: RiskType; level: RiskLevel; title: string; description: string; actionUrl: string }[] = [] + + for (const emp of employees) { + // 已离职的跳过 + if (emp.terminations.length > 0) continue + + const latestContract = emp.contracts[0] + const hasSignedContract = latestContract && latestContract.contractType !== 'UNSIGNED' + + if (!hasSignedContract) { + const daysSinceHire = daysBetween(today, emp.hireDate) + risks.push({ + employeeId: emp.id, + type: 'ONBOARDING', + level: daysSinceHire > 30 ? 'HIGH' : 'MEDIUM', + title: `${emp.name}入职手续未完成${daysSinceHire > 30 ? `(已超${daysSinceHire}天)` : ''}`, + description: `入职日期 ${emp.hireDate.toISOString().slice(0, 10)},尚未签订劳动合同,请尽快完成入职手续。`, + actionUrl: `/roster?employee=${encodeURIComponent(emp.name)}`, + }) + } + } + + return risks +} + export async function detectTerminationRisks(orgId: string) { const employees = await prisma.employee.findMany({ where: { orgId, status: 'ACTIVE' }, @@ -206,10 +245,11 @@ export async function runRiskDetection(orgId: string) { const contractRisks = await detectContractRisks(orgId) const terminationRisks = await detectTerminationRisks(orgId) + const onboardingRisks = await detectOnboardingRisks(orgId) const monthlyTasks = await detectMonthlyTasks(orgId) // 月度任务用 monthlyKeys 去重,其他任务用 existingKeys 去重 - const nonMonthlyRisks = [...contractRisks, ...terminationRisks] + const nonMonthlyRisks = [...contractRisks, ...terminationRisks, ...onboardingRisks] const toCreate = [ ...nonMonthlyRisks.filter((r) => !existingKeys.has(`${r.employeeId}:${r.title}`)), ...monthlyTasks.filter((r) => !monthlyKeys.has(`${r.employeeId}:${r.title}`)), diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 25620cd..01aea71 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Link } from 'react-router-dom' -import { Users, AlertTriangle, CheckSquare, DollarSign, ArrowRight, RefreshCw, FileText, Calendar, TrendingUp, Briefcase, Calculator, Wallet, Building2, Receipt, Check, X, Clock, LayoutDashboard, ListTodo, ShieldAlert } from 'lucide-react' +import { Users, AlertTriangle, CheckSquare, DollarSign, ArrowRight, RefreshCw, FileText, Calendar, TrendingUp, Briefcase, Calculator, Wallet, Building2, Receipt, Check, X, Clock, LayoutDashboard, ListTodo, ShieldAlert, UserPlus } from 'lucide-react' import api from '../lib/api' import Card from '../components/ui/Card' import Button from '../components/ui/Button' @@ -18,6 +18,7 @@ const TODO_ICON_CONFIG: Record queryClient.invalidateQueries({ queryKey: ['dashboard'] }), }) - const riskTodos = data?.todos.filter((t) => t.type === 'CONTRACT' || t.type === 'TERMINATION') || [] + const riskTodos = data?.todos.filter((t) => t.type === 'CONTRACT' || t.type === 'TERMINATION' || t.type === 'ONBOARDING') || [] const taskTodos = data?.todos.filter((t) => t.type === 'MONTHLY' || t.type === 'SALARY') || [] const filteredTodos = activeTab === 'risk' ? riskTodos : taskTodos diff --git a/frontend/src/pages/Roster.tsx b/frontend/src/pages/Roster.tsx index e07a677..aba42ea 100644 --- a/frontend/src/pages/Roster.tsx +++ b/frontend/src/pages/Roster.tsx @@ -133,8 +133,12 @@ export default function Roster() { {e.name} {e.department} - - {e.status === 'ACTIVE' ? '在职' : '离职'} + + {e.status === 'ACTIVE' ? '在职' : e.status === 'PRE_HIRE' ? '预入职' : '离职'} {e.hireDate?.toString().slice(0, 10)} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index f2a8efc..bfc0f57 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -85,7 +85,7 @@ export interface DashboardData { } todos: { id: string - type: 'CONTRACT' | 'SALARY' | 'TERMINATION' | 'MONTHLY' + type: 'CONTRACT' | 'SALARY' | 'TERMINATION' | 'MONTHLY' | 'ONBOARDING' level: 'high' | 'medium' | 'low' title: string description: string @@ -93,7 +93,7 @@ export interface DashboardData { }[] resolvedTodos: { id: string - type: 'CONTRACT' | 'SALARY' | 'TERMINATION' | 'MONTHLY' + type: 'CONTRACT' | 'SALARY' | 'TERMINATION' | 'MONTHLY' | 'ONBOARDING' level: 'high' | 'medium' | 'low' title: string description: string