import { useState } from 'react' 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, TrendingDown, ShieldCheck, Lightbulb, BookOpen, Sparkles, Repeat, XCircle, Loader2 } from 'lucide-react' import api from '../lib/api' 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' 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' }, } 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 [todoPage, setTodoPage] = useState(1) const [todoPageSize, setTodoPageSize] = useState(10) const queryClient = useQueryClient() const [activeTab, setActiveTab] = useState<'overview' | 'payroll' | 'risk' | 'task'>('overview') const [selectedIds, setSelectedIds] = useState>(new Set()) const [drillDownType, setDrillDownType] = useState(null) const [showExpiringModal, setShowExpiringModal] = useState(false) const [dismissedExpiring, setDismissedExpiring] = useState(false) const { data, isLoading, refetch, isFetching } = useQuery({ queryKey: ['dashboard'], queryFn: async () => { const res = await api.get('/dashboard') as any return res.data }, }) const { data: expiringContracts } = useQuery({ queryKey: ['expiring-contracts'], queryFn: async () => { const res = await api.get('/roster/contracts/expiring') as any return res.data }, }) const currentMonth = new Date().toISOString().slice(0, 7) const { data: costAnalysis } = useQuery({ queryKey: ['cost-analysis', currentMonth], queryFn: async () => { const res = await api.get(`/dashboard/cost-analysis?month=${currentMonth}`) as any return res.data }, }) const { data: complianceScore } = useQuery({ queryKey: ['compliance-score'], queryFn: async () => { const res = await api.get('/dashboard/compliance-score') as any return res.data }, }) const { data: workforceStats } = useQuery({ queryKey: ['workforce-stats'], queryFn: async () => { const res = await api.get('/dashboard/workforce-stats') as any return res.data }, }) const resolveMutation = useMutation({ mutationFn: (id: string) => api.patch(`/dashboard/todos/${id}/resolve`), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['dashboard'] }), }) const ignoreMutation = useMutation({ mutationFn: (id: string) => api.patch(`/dashboard/todos/${id}/ignore`), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['dashboard'] }), }) const batchResolveMutation = useMutation({ mutationFn: (ids: string[]) => api.patch('/dashboard/todos/batch-resolve', { ids }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['dashboard'] }) setSelectedIds(new Set()) }, }) const batchIgnoreMutation = useMutation({ mutationFn: (ids: string[]) => api.patch('/dashboard/todos/batch-ignore', { 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') || [] 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 }, ] 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} 月度总览

{/* Tab 导航 */}
{tabs.map((tab) => { const Icon = tab.icon return ( ) })}
{/* 概览 Tab */} {activeTab === 'overview' && (
{/* 合规健康度评分 + AI 建议卡片流 */} {complianceScore && (
{/* 评分环 + 5 维度 */}
{/* SVG 环形评分 */}
{complianceScore.overallScore} {complianceScore.levelLabel}
{/* 5 维度评分 */}
{complianceScore.dimensions.map((dim: any) => (
= 85 ? 'bg-safe' : dim.score >= 60 ? 'bg-warning' : 'bg-danger'}`} /> {dim.name}
{dim.todoCount > 0 && {dim.todoCount}待办} = 85 ? 'text-safe' : dim.score >= 60 ? 'text-warning' : 'text-danger'}`}>{dim.score}
))}
{/* AI 建议卡片流 */} {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}
) })}
)}
)} {/* 统计卡片 */}
{stats.map((stat) => { const Icon = stat.icon return (
{stat.value}
{stat.label}
) })}
{/* 人力成本概览:当月 + 年度累计 */}
{/* 当月人力成本 */}

当月人力成本

{payroll?.month}
{monthCostItems.map((item) => (
{item.label} {fmt(item.value)}
))}
企业总成本 {fmt(monthTotalCost)}
{/* 年度累计人力成本 */}

年度累计成本

{yearCost?.year}年
{yearCostItems.map((item) => (
{item.label} {fmt(item.value)}
))}
年度企业总成本 {fmt(yearTotalCost)}
{/* 合同到期预警 */} {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}人}
)} {/* 本月工作动态 + 风险分布 左右两列 */}
{/* 本月工作动态 */}

本月工作动态

{activities?.month}
{activityItems.map((item) => { const Icon = item.icon return (
{item.value}
{item.label}
) })}
{/* 风险分布 */}

风险分布

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}天`} )}
)) ) : (
暂无高风险项
)}
)}
{/* 人力成本分析 */}

人力成本分析

{currentMonth}
{costAnalysis ? (
= 0 ? 'bg-red-50' : 'bg-green-50'}`}>
环比上月
= 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 ? 'bg-red-50' : 'bg-green-50'}`}>
同比去年
= 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)}
) })}
)}
) : (
暂无成本分析数据
)}
)} {/* 薪税 Tab */} {activeTab === '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} 条
) : ( )}
)} {/* 员工分布统计 */} {activeTab === 'overview' && 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' ? '风险提醒' : '月度任务'}

{filteredTodos.length} 项
{filteredTodos.length === 0 ? ( ) : ( <> {/* 批量操作栏 */}
{selectedIds.size > 0 && ( <> 已选 {selectedIds.size} 项 )}
{ setTodoPageSize(s); setTodoPage(1) }} />
{filteredTodos.slice((todoPage - 1) * todoPageSize, todoPage * todoPageSize).map((todo) => (
toggleSelect(todo.id)} className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary" />
{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}
))}
)}
{/* 已办事项 */} {data.resolvedTodos && data.resolvedTodos.length > 0 && (

已办事项

{data.resolvedTodos.length} 项
{data.resolvedTodos.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" > 查看全部合同 →
)}
) }