import { useState } from 'react' import { usePageSize } from '../hooks/usePageSize' import { toast } from 'sonner' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Link } from 'react-router-dom' import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, RadialBarChart, RadialBar, PolarAngleAxis } from 'recharts' import { Users, AlertTriangle, CheckSquare, DollarSign, ArrowRight, RefreshCw, FileText, Calendar, TrendingUp, Briefcase, Calculator, Wallet, Building2, Receipt, Check, X, Clock, LayoutDashboard, ListTodo, ShieldAlert, UserPlus, AlertCircle, Download, ChevronRight, ShieldCheck, Lightbulb, BookOpen, Sparkles, Repeat, XCircle, Settings } from 'lucide-react' import { dashboardApi, rosterApi, workProcessApi } from '../lib/api-services' import { useAuthStore } from '../store/authStore' import Card from '../components/ui/Card' import Button from '../components/ui/Button' import EmptyState from '../components/ui/EmptyState' import Pagination from '../components/ui/Pagination' import type { DashboardData } from '../types' import TurnoverStats from './dashboard/TurnoverStats' import PageGuide from '../components/ui/PageGuide' import PerformanceStats from './dashboard/PerformanceStats' function fmt(n: number) { return `¥${(n || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` } const TODO_ICON_CONFIG: Record = { CONTRACT: { icon: FileText, color: 'text-blue-600', bg: 'bg-blue-50' }, SALARY: { icon: DollarSign, color: 'text-amber-600', bg: 'bg-amber-50' }, TERMINATION: { icon: ShieldAlert, color: 'text-red-600', bg: 'bg-red-50' }, MONTHLY: { icon: Calendar, color: 'text-purple-600', bg: 'bg-purple-50' }, ONBOARDING: { icon: UserPlus, color: 'text-cyan-600', bg: 'bg-cyan-50' }, RETIREMENT: { icon: Clock, color: 'text-orange-600', bg: 'bg-orange-50' }, } function TodoIcon({ type }: { type: string; level: string }) { const config = TODO_ICON_CONFIG[type] || TODO_ICON_CONFIG.MONTHLY const Icon = config.icon return (
) } export default function Dashboard() { const todoPageSize = usePageSize() const [todoPage, setTodoPage] = useState(1) const queryClient = useQueryClient() const [activeTab, setActiveTab] = useState<'overview' | 'risk' | 'task' | 'cost' | 'workforce'>('overview') const [selectedIds, setSelectedIds] = useState>(new Set()) const [drillDownType, setDrillDownType] = useState(null) const [showExpiringModal, setShowExpiringModal] = useState(false) const [dismissedExpiring, setDismissedExpiring] = useState(false) const [showTabSettings, setShowTabSettings] = useState(false) const [tabVisibility, setTabVisibility] = useState>(() => { try { const saved = localStorage.getItem('dashboard-tab-visibility') if (saved) return JSON.parse(saved) } catch {} return { overview: true, risk: true, task: true, cost: true, workforce: true } }) const toggleTabVisibility = (key: string) => { setTabVisibility(prev => { const next = { ...prev, [key]: !prev[key] } localStorage.setItem('dashboard-tab-visibility', JSON.stringify(next)) return next }) } const [settingsTab, setSettingsTab] = useState('overview') const [sectionVisibility, setSectionVisibility] = useState>(() => { try { const saved = localStorage.getItem('dashboard-section-visibility') if (saved) return JSON.parse(saved) } catch {} return {} }) const toggleSectionVisibility = (key: string) => { setSectionVisibility(prev => { const next = { ...prev, [key]: !prev[key] } localStorage.setItem('dashboard-section-visibility', JSON.stringify(next)) return next }) } const isSectionVisible = (key: string) => sectionVisibility[key] !== false const tabSections: Record = { overview: [ { key: 'overview_stats', label: '统计卡片' }, { key: 'overview_compliance', label: '合规评分' }, { key: 'overview_ai', label: 'AI 建议' }, { key: 'overview_expiring', label: '合同到期预警' }, { key: 'overview_nav', label: '快捷导航' }, ], risk: [ { key: 'risk_distribution', label: '风险分布饼图' }, { key: 'risk_todos', label: '待办列表' }, { key: 'risk_resolved', label: '已办事项' }, ], task: [ { key: 'task_todos', label: '待办列表' }, { key: 'task_resolved', label: '已办事项' }, ], cost: [ { key: 'cost_metrics', label: '成本指标卡片' }, { key: 'cost_overview', label: '月度/年度成本' }, { key: 'cost_analysis', label: '成本分析' }, { key: 'cost_payroll', label: '薪税明细' }, ], workforce: [ { key: 'workforce_activities', label: '本月工作动态' }, { key: 'workforce_turnover', label: '入离职/绩效统计' }, { key: 'workforce_distribution', label: '员工分布' }, ], } const { data, isLoading, refetch, isFetching } = useQuery({ queryKey: ['dashboard'], queryFn: () => dashboardApi.data(), }) const { data: expiringContracts } = useQuery({ queryKey: ['expiring-contracts'], queryFn: () => rosterApi.expiringContracts(), }) const currentMonth = new Date().toISOString().slice(0, 7) const { data: costAnalysis } = useQuery({ queryKey: ['cost-analysis', currentMonth], queryFn: () => dashboardApi.costAnalysis(currentMonth), }) const { data: complianceScore } = useQuery({ queryKey: ['compliance-score'], queryFn: () => dashboardApi.healthCheck(), }) const { data: workforceStats } = useQuery({ queryKey: ['workforce-stats'], queryFn: () => dashboardApi.workforceStats(), }) const resolveMutation = useMutation({ mutationFn: (id: string) => dashboardApi.resolveTodo(id), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['dashboard'] }), }) const ignoreMutation = useMutation({ mutationFn: (id: string) => dashboardApi.ignoreTodo(id), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['dashboard'] }), }) const batchResolveMutation = useMutation({ mutationFn: (ids: string[]) => dashboardApi.batchResolveTodos(ids), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['dashboard'] }) setSelectedIds(new Set()) }, }) const batchIgnoreMutation = useMutation({ mutationFn: (ids: string[]) => dashboardApi.batchIgnoreTodos(ids), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['dashboard'] }) setSelectedIds(new Set()) }, }) const handleExportPayroll = async () => { try { const month = payroll?.month || new Date().toISOString().slice(0, 7) const token = useAuthStore.getState().accessToken const baseURL = import.meta.env.DEV ? 'http://localhost:3000/api/v1' : '/api/v1' const res = await fetch(`${baseURL}/export/payroll?month=${month}`, { headers: { Authorization: `Bearer ${token}` } }) if (!res.ok) throw new Error('导出失败') const blob = await res.blob() const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `薪税汇总-${month}.xlsx` a.click() URL.revokeObjectURL(url) } catch { toast.error('导出失败') } } const toggleSelect = (id: string) => { setSelectedIds(prev => { const next = new Set(prev) if (next.has(id)) next.delete(id) else next.add(id) return next }) } const toggleSelectAll = (ids: string[]) => { setSelectedIds(prev => { const allSelected = ids.every(id => prev.has(id)) const next = new Set(prev) if (allSelected) ids.forEach(id => next.delete(id)) else ids.forEach(id => next.add(id)) return next }) } const riskTodos = data?.todos?.filter((t) => t.type === 'CONTRACT' || t.type === 'TERMINATION' || t.type === 'ONBOARDING' || t.type === 'RETIREMENT') || [] const taskTodos = data?.todos?.filter((t) => t.type === 'MONTHLY' || t.type === 'SALARY') || [] const filteredTodos = activeTab === 'risk' ? riskTodos : taskTodos if (isLoading) { return
加载中...
} if (!data) return null const payroll = data.payrollSummary const activities = data.monthlyActivities const yearCost = data.yearCostSummary const stats = [ { label: '在管员工', value: data.stats.employeeCount, icon: Users, color: 'text-primary' }, { label: '高风险', value: data.stats.highRiskCount, icon: AlertTriangle, color: 'text-danger' }, { label: '待办事项', value: data.stats.todoCount, icon: CheckSquare, color: 'text-warning' }, { label: '工资条', value: payroll?.payslipCount ?? 0, icon: Receipt, color: 'text-safe' }, ] // 当月人力成本 const monthCostItems = [ { label: '工资总额', value: payroll?.totalPay ?? 0, color: 'text-primary' }, { label: '企业社保', value: payroll?.socialOrg ?? 0, color: 'text-blue-600' }, { label: '企业公积金', value: payroll?.housingOrg ?? 0, color: 'text-purple-600' }, { label: '加班费', value: payroll?.overtimePay ?? 0, color: 'text-safe' }, { label: '补偿金', value: payroll?.severancePay ?? 0, color: 'text-orange-600' }, ] const monthTotalCost = payroll?.orgTotalCost ?? 0 // 年度累计人力成本 const yearCostItems = [ { label: '工资总额', value: yearCost?.totalPay ?? 0, color: 'text-primary' }, { label: '企业社保', value: yearCost?.socialOrg ?? 0, color: 'text-blue-600' }, { label: '企业公积金', value: yearCost?.housingOrg ?? 0, color: 'text-purple-600' }, { label: '加班费', value: yearCost?.overtimePay ?? 0, color: 'text-safe' }, { label: '补偿金', value: yearCost?.severancePay ?? 0, color: 'text-orange-600' }, ] const yearTotalCost = yearCost?.orgTotalCost ?? 0 const activityItems = [ { label: '新签合同', value: activities?.newContracts ?? 0, icon: FileText, color: 'text-primary' }, { label: '解聘人数', value: activities?.terminations ?? 0, icon: Users, color: 'text-danger' }, { label: '违纪处理', value: activities?.disciplinaryActions ?? 0, icon: AlertTriangle, color: 'text-warning' }, { label: '考勤记录', value: activities?.attendanceRecords ?? 0, icon: Calendar, color: 'text-gray-600' }, { label: '加班时长', value: `${activities?.overtimeHours ?? 0}h`, icon: TrendingUp, color: 'text-safe' }, { label: '加班费', value: fmt(activities?.overtimePay ?? 0), icon: DollarSign, color: 'text-safe' }, ] const payrollItems = [ { label: '基本工资', value: payroll?.baseSalary ?? 0, icon: Wallet, color: 'text-gray-700' }, { label: '加班费', value: payroll?.overtimePay ?? 0, icon: TrendingUp, color: 'text-gray-700' }, { label: '津贴补贴', value: payroll?.allowance ?? 0, icon: Wallet, color: 'text-gray-700' }, { label: '扣款', value: -(payroll?.deduction ?? 0), icon: Wallet, color: 'text-danger' }, ] const deductionItems = [ { label: '个人社保', value: -(payroll?.socialEmp ?? 0) }, { label: '个人公积金', value: -(payroll?.housingEmp ?? 0) }, { label: '个人所得税', value: -(payroll?.estimatedTax ?? 0) }, ] const tabs = [ { key: 'overview' as const, label: '概览', icon: LayoutDashboard, badge: data.stats.todoCount }, { key: 'risk' as const, label: '风险提醒', icon: AlertTriangle, badge: riskTodos.length }, { key: 'task' as const, label: '月度任务', icon: ListTodo, badge: taskTodos.length }, { key: 'cost' as const, label: '人力成本', icon: Wallet, badge: 0 }, { key: 'workforce' as const, label: '人员分析', icon: Users, badge: 0 }, ] const priorityConfig: Record = { URGENT: { label: '紧急', color: 'text-red-700', bg: 'bg-red-100' }, HIGH: { label: '高', color: 'text-orange-700', bg: 'bg-orange-100' }, MEDIUM: { label: '中', color: 'text-amber-700', bg: 'bg-amber-100' }, LOW: { label: '低', color: 'text-gray-600', bg: 'bg-gray-100' }, } return (
工作台展示企业人力全局数据和待办事项,包括在管员工数、当月人力成本、合同风险、审批待办等。可切换概览、人力成本、风险、任务等视图。点击各统计卡片可快速跳转到对应详情页。

工作台

{data.greeting} · {payroll?.month} 月度总览

{showTabSettings && ( <>
setShowTabSettings(false)} />
工作台设置
{tabs.map(tab => { const TabIcon = tab.icon const visible = tabVisibility[tab.key] !== false const sections = tabSections[tab.key] || [] const expanded = settingsTab === tab.key return (
{visible && sections.length > 0 && ( )}
{expanded && visible && sections.length > 0 && (
{sections.map(section => ( ))}
)}
) })}
)}
{/* Tab 导航 */}
{tabs.filter(tab => tabVisibility[tab.key] !== false).map((tab) => { const Icon = tab.icon return ( ) })}
{/* 概览 Tab */} {activeTab === 'overview' && (
概览页展示企业人力全局数据:在管员工数、待办事项、当月人力成本及年度累计成本。可点击各统计卡片快速跳转到对应详情页。右上角可自定义显示/隐藏模块。 {/* 统计卡片 */} {isSectionVisible('overview_stats') && (
{stats.map((stat) => { const Icon = stat.icon return (
{stat.value}
{stat.label}
) })}
)} {/* 合规健康度评分 + AI 建议卡片流 */} {isSectionVisible('overview_compliance') && isSectionVisible('overview_ai') && complianceScore && (
{/* 评分环 + 维度 + 合同到期预警 */}
{/* SVG 环形评分 */}
{complianceScore?.overallScore ?? complianceScore?.totalScore ?? 0} {complianceScore?.levelLabel ?? ''}
{/* 维度评分 */}
{complianceScore?.dimensions?.map((dim: any) => ( = 85 ? 'text-safe' : dim.score >= 60 ? 'text-warning' : 'text-danger'}`}>{dim.score} {dim.name} {dim.todoCount > 0 && {dim.todoCount}待办} ))}
{/* 合同到期预警内联 */} {isSectionVisible('overview_expiring') && expiringContracts && expiringContracts.length > 0 && !dismissedExpiring && (
setShowExpiringModal(true)} > 合同到期预警 {expiringContracts.slice(0, 3).map((c: any, i: number) => ( {i > 0 && '、'} {c.employeeName} ({c.daysLeft}天) ))} {expiringContracts.length > 3 && 等{expiringContracts.length}人}
)}
{/* AI 建议卡片流 */} {complianceScore?.suggestions && complianceScore.suggestions.length > 0 && (
AI 建议
{complianceScore.suggestions.map((s: any, i: number) => { const suggestionConfig: Record = { danger: { icon: AlertTriangle, bg: 'bg-red-50', border: 'border-red-200', iconColor: 'text-red-600', label: '高风险', labelBg: 'bg-red-100 text-red-700' }, warning: { icon: AlertCircle, bg: 'bg-amber-50', border: 'border-amber-200', iconColor: 'text-amber-600', label: '中风险', labelBg: 'bg-amber-100 text-amber-700' }, value: { icon: ShieldCheck, bg: 'bg-green-50', border: 'border-green-200', iconColor: 'text-safe', label: '价值', labelBg: 'bg-green-100 text-safe' }, knowledge: { icon: BookOpen, bg: 'bg-purple-50', border: 'border-purple-200', iconColor: 'text-purple-600', label: '知识', labelBg: 'bg-purple-100 text-purple-700' }, } const config = suggestionConfig[s.type] || { icon: Lightbulb, bg: 'bg-gray-50', border: 'border-gray-200', iconColor: 'text-gray-600', label: '', labelBg: '' } const Icon = config.icon return (
{config.label} {s.title} {s.estimatedLoss && s.estimatedLoss > 0 && ( 预估损失 {fmt(s.estimatedLoss)} )}

{s.description}

{s.actionLabel}
) })}
)}
)} {/* 快捷导航 */} {isSectionVisible('overview_nav') && (
)}
)} {/* 人力成本 Tab */} {activeTab === 'cost' && (
人力成本页展示当月及年度累计的工资、社保、公积金、加班费、补偿金等各项成本明细。可导出薪税汇总表,点击月份卡片可查看历史趋势。 {/* 成本专属指标卡片 */} {isSectionVisible('cost_metrics') && (
{fmt(monthTotalCost)}
本月总成本
{fmt(yearTotalCost)}
年度累计
{fmt(costAnalysis?.current?.perCapita || 0)}
人均成本
{payroll?.payslipCount ?? 0}
工资条数
)} {/* 人力成本概览:当月 + 年度累计 */} {isSectionVisible('cost_overview') && (

当月人力成本

{payroll?.month}
{monthCostItems.map((item) => (
{item.label} {fmt(item.value)}
))}
企业总成本 {fmt(monthTotalCost)}

年度累计成本

{yearCost?.year}年
{yearCostItems.map((item) => (
{item.label} {fmt(item.value)}
))}
年度企业总成本 {fmt(yearTotalCost)}
)} {/* 人力成本分析 */} {isSectionVisible('cost_analysis') && (

人力成本分析

{currentMonth}
{costAnalysis ? (
0 ? (costAnalysis.monthOnMonth?.change >= 0 ? 'bg-red-50' : 'bg-green-50') : 'bg-gray-50'}`}>
环比上月
{costAnalysis.monthOnMonth?.prevTotal > 0 ? ( <>
= 0 ? 'text-red-600' : 'text-green-600'}`}> {costAnalysis.monthOnMonth?.change >= 0 ? '+' : ''}{costAnalysis.monthOnMonth?.changePercent?.toFixed(1)}%
{costAnalysis.monthOnMonth?.change >= 0 ? '↑' : '↓'} {fmt(Math.abs(costAnalysis.monthOnMonth?.change || 0))}
) : (
暂无数据
)}
0 ? (costAnalysis.yearOnYear?.change >= 0 ? 'bg-red-50' : 'bg-green-50') : 'bg-gray-50'}`}>
同比去年
{costAnalysis.yearOnYear?.lastYearTotal > 0 ? ( <>
= 0 ? 'text-red-600' : 'text-green-600'}`}> {costAnalysis.yearOnYear?.change >= 0 ? '+' : ''}{costAnalysis.yearOnYear?.changePercent?.toFixed(1)}%
{costAnalysis.yearOnYear?.change >= 0 ? '↑' : '↓'} {fmt(Math.abs(costAnalysis.yearOnYear?.change || 0))}
) : (
暂无数据
)}
本月人均成本 {fmt(costAnalysis.current?.perCapita || 0)}
{costAnalysis.factors && costAnalysis.factors.length > 0 && (
成本变化归因
{costAnalysis.factors.map((f: any, i: number) => (
= 0 ? 'bg-red-500' : 'bg-green-500'}`} /> {f.description}
))}
)} {costAnalysis.departmentCost && costAnalysis.departmentCost.length > 0 && (
部门成本拆分
{costAnalysis.departmentCost.map((d: any, i: number) => { const maxCost = costAnalysis.departmentCost[0].totalCost || 1 return (
{d.department}({d.headcount}人) {fmt(d.totalCost)}
工资 {fmt(d.totalPay)} 社保 {fmt(d.socialOrg)} 公积金 {fmt(d.housingOrg)} 人均 {fmt(d.perCapita)}
) })}
)}
) : (
暂无成本分析数据
)} )} {/* 薪税明细 */} {isSectionVisible('cost_payroll') && (

本月薪税费用总览

查看明细
{payroll && payroll.payslipCount > 0 ? (
工资构成
{payrollItems.map((item) => { const Icon = item.icon return (
{item.label}
{fmt(item.value)}
) })}
应发合计 {fmt(payroll.totalPay)}
扣减项
{deductionItems.map((item) => (
{item.label} {fmt(item.value)}
))}
员工实发工资 {fmt(payroll.empNetPay)}
企业用工成本
企业社保 {fmt(payroll.socialOrg)}
企业公积金 {fmt(payroll.housingOrg)}
工资总额 {fmt(payroll.totalPay)}
{payroll.severancePay > 0 && (
经济补偿金 {fmt(payroll.severancePay)}
)}
企业总成本 {fmt(payroll.orgTotalCost)}
工资条确认: 已确认 {payroll.confirmedPayslips} 未确认 {payroll.unconfirmedPayslips} 共 {payroll.payslipCount} 条
) : ( )}
)}
)} {/* 人员分析 Tab */} {activeTab === 'workforce' && (
人员分析页展示本月入离职动态、员工分布及绩效统计。可按部门筛选查看人员构成,洞察团队变化趋势。 {/* 本月工作动态 */} {isSectionVisible('workforce_activities') && (

本月工作动态

{activities?.month}
{activityItems.map((item) => { const Icon = item.icon return (
{item.value}
{item.label}
) })}
)} {/* 入离职统计 + 绩效统计 */} {isSectionVisible('workforce_turnover') && (
)} {/* 员工分布统计 */} {isSectionVisible('workforce_distribution') && workforceStats && workforceStats.total > 0 && (
{/* 性别分布 */}

性别分布

{workforceStats.gender.map((_: any, i: number) => )}
{workforceStats.gender.map((g: any, i: number) => ( {g.name} {g.value} ))}
{/* 年龄段分布 */}

年龄段分布

a.value > 0)} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={55} innerRadius={30}> {workforceStats.age.filter((a: any) => a.value > 0).map((_: any, i: number) => )}
{workforceStats.age.filter((a: any) => a.value > 0).map((a: any, i: number) => ( {a.name} {a.value} ))}
{/* 学历分布 */}

学历分布

{workforceStats.education.map((_: any, i: number) => )}
{workforceStats.education.map((e: any, i: number) => ( {e.name} {e.value} ))}
{/* 司龄分布 */}

司龄分布

t.value > 0)} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={55} innerRadius={30}> {workforceStats.tenure.filter((t: any) => t.value > 0).map((_: any, i: number) => )}
{workforceStats.tenure.filter((t: any) => t.value > 0).map((t: any, i: number) => ( {t.name} {t.value} ))}
)}
)} {/* 风险提醒 Tab */} {(activeTab === 'risk' || activeTab === 'task') && (
{activeTab === 'risk' ? '风险提醒页汇总合同到期、未签合同、离职风险、退休预警等高风险待办事项。可批量处理或逐项解决,处理完成后自动归档至已办事项。' : '月度任务页汇总社保办理、发薪批次等周期性待办事项。可批量处理或逐项解决,处理完成后自动归档至已办事项。'} {/* 风险分布饼图(仅风险提醒Tab) */} {activeTab === 'risk' && isSectionVisible('risk_distribution') && (

风险分布

d.value > 0)} dataKey="value" nameKey="name" cx="50%" cy="50%" innerRadius={30} outerRadius={55} paddingAngle={2} > {[ { name: '合同风险', value: data.riskDistribution.contract, color: '#4F46E5' }, { name: '薪资风险', value: data.riskDistribution.salary, color: '#F59E0B' }, { name: '解聘风险', value: data.riskDistribution.termination, color: '#EF4444' }, ].filter(d => d.value > 0).map((entry, i) => ( ))} `${v} 项`} />
{drillDownType && (
{drillDownType === 'CONTRACT' ? '合同' : drillDownType === 'SALARY' ? '薪资' : '解聘'}风险明细
{(data.topRisks || []).filter(r => r.type === drillDownType).length > 0 ? ( (data.topRisks || []).filter(r => r.type === drillDownType).map((r) => (
{r.title} {r.priority && priorityConfig[r.priority] && ( {priorityConfig[r.priority].label} )}
{r.employeeName && {r.employeeName}} {r.estimatedLoss > 0 && 损失 {fmt(r.estimatedLoss)}} {r.daysUntilDeadline !== null && r.daysUntilDeadline <= 7 && ( {r.daysUntilDeadline <= 0 ? '已逾期' : `${r.daysUntilDeadline}天`} )}
)) ) : (
暂无高风险项
)}
)}
)} {/* 待办列表 */} {isSectionVisible(activeTab === 'risk' ? 'risk_todos' : 'task_todos') && (

{activeTab === 'risk' ? '风险提醒' : '月度任务'}

{filteredTodos.length} 项
{filteredTodos.length === 0 ? ( ) : ( <> {/* 批量操作栏 */}
{selectedIds.size > 0 && ( <> 已选 {selectedIds.size} 项 )}
{filteredTodos.slice((todoPage - 1) * todoPageSize, todoPage * todoPageSize).map((todo) => { const hasValidUrl = todo.actionUrl && todo.actionUrl !== '/' && todo.actionUrl !== '' return (
toggleSelect(todo.id)} className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary" /> {hasValidUrl ? (
{todo.employeeName && ( {todo.employeeName} )} {todo.employeeDepartment && ( {todo.employeeDepartment} )} {todo.title} {todo.priority && priorityConfig[todo.priority] && ( {priorityConfig[todo.priority].label} )} {todo.estimatedLoss > 0 && ( 损失 {fmt(todo.estimatedLoss)} )} {todo.daysUntilDeadline !== null && todo.daysUntilDeadline <= 3 && ( {todo.daysUntilDeadline <= 0 ? '已逾期' : `${todo.daysUntilDeadline}天`} )}
{todo.description}
) : (
{todo.employeeName && ( {todo.employeeName} )} {todo.employeeDepartment && ( {todo.employeeDepartment} )} {todo.title} {todo.priority && priorityConfig[todo.priority] && ( {priorityConfig[todo.priority].label} )} {todo.estimatedLoss > 0 && ( 损失 {fmt(todo.estimatedLoss)} )} {todo.daysUntilDeadline !== null && todo.daysUntilDeadline <= 3 && ( {todo.daysUntilDeadline <= 0 ? '已逾期' : `${todo.daysUntilDeadline}天`} )}
{todo.description}
)}
{hasValidUrl && ( 立刻办理 → )}
)})}
setTodoPage(1)} /> )}
)} {/* 已办事项 */} {isSectionVisible(activeTab === 'risk' ? 'risk_resolved' : 'task_resolved') && (() => { const riskTypes = ['CONTRACT', 'TERMINATION', 'ONBOARDING', 'RETIREMENT'] const taskTypes = ['MONTHLY', 'SALARY'] const resolvedFiltered = (data.resolvedTodos || []).filter(t => activeTab === 'risk' ? riskTypes.includes(t.type) : taskTypes.includes(t.type) ) if (resolvedFiltered.length === 0) return null return (

已办事项

{resolvedFiltered.length} 项
{resolvedFiltered.map((todo) => (
{todo.title} {todo.description}
{todo.resolvedAt ? new Date(todo.resolvedAt).toLocaleDateString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : ''}
))}
) })()}
)} {/* 合同到期处理弹窗 */} {showExpiringModal && expiringContracts && (
setShowExpiringModal(false)}>
e.stopPropagation()}>

合同到期处理

({expiringContracts.length}人)
{expiringContracts.map((c: any) => (
{c.employeeName}
{c.department} · 到期日:{c.endDate ? new Date(c.endDate).toLocaleDateString('zh-CN') : '未知'} 剩余 {c.daysLeft} 天
))}
setShowExpiringModal(false)} className="text-xs text-primary hover:underline" > 查看全部合同 →
)}
) }