import { useState, useMemo, useEffect } from 'react' import { usePageSize } from '../hooks/usePageSize' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' import { AlertTriangle, Check, ChevronRight, ChevronLeft, Shield, Info, Calculator, FileText, Printer, Trash2, List, Download, Plus, Edit, Send, CheckCircle, XCircle, Play, Ban, CheckCheck } from 'lucide-react' import { Stepper } from '../components/ui/Stepper' import { InlineAlert } from '../components/ui/InlineAlert' import PageGuide from '../components/ui/PageGuide' import jsPDF from 'jspdf' import { rosterApi, terminationApi } from '../lib/api-services' import { useAuthStore } from '../store/authStore' import Card from '../components/ui/Card' import Button from '../components/ui/Button' import { Input, Label, Select } from '../components/ui/Input' import EmptyState from '../components/ui/EmptyState' import Pagination from '../components/ui/Pagination' import { useConfirm } from '../hooks/useConfirm' // 金额格式化:保留两位小数 + 千分位 const fmt = (n: number) => (n || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) const REASONS = [ { value: 'NEGOTIATED', label: '协商解除(双方同意分开了)', legalBasis: '《劳动合同法》第36条', legalDetail: '用人单位与劳动者协商一致,可以解除劳动合同。需支付经济补偿(N),工作满1年支付1个月工资。', steps: [ '第一步:与员工进行协商沟通,达成解除意向', '第二步:签订《协商解除劳动合同协议书》,明确解除日期、补偿金额、支付方式', '第三步:按协议约定支付经济补偿金', '第四步:出具《解除劳动合同证明书》', '第五步:办理工作交接、社保减员、档案转移', ], }, { value: 'FAULT', label: '员工犯错被辞退(严重违纪/失职等)', legalBasis: '《劳动合同法》第39条', legalDetail: '劳动者有下列情形之一的,用人单位可以解除劳动合同:(1)在试用期间被证明不符合录用条件的;(2)严重违反用人单位的规章制度的;(3)严重失职,营私舞弊,给用人单位造成重大损害的;(4)同时与其他用人单位建立劳动关系,对完成本单位的工作任务造成严重影响,或经提出拒不改正的;(5)以欺诈、胁迫等手段致使劳动合同无效的;(6)被依法追究刑事责任的。无需支付经济补偿。', steps: [ '第一步:收集并固化证据(违纪事实、制度依据、证人证言等)', '第二步:确认规章制度经过民主程序制定并已向员工公示(签收记录)', '第三步:将解除理由通知工会,工会提出意见后书面回复工会', '第四步:向员工送达《解除劳动合同通知书》,注明解除依据和事实', '第五步:办理工作交接、社保减员、档案转移', '⚠ 注意:证据不足或制度未公示可能导致违法解除,建议咨询律师', ], }, { value: 'NONFAULT', label: '员工没犯错但干不了(生病/不胜任等)', legalBasis: '《劳动合同法》第40条', legalDetail: '有下列情形之一的,用人单位提前三十日以书面形式通知劳动者本人或者额外支付一个月工资后,可以解除劳动合同:(1)劳动者患病或非因工负伤,在规定的医疗期满后不能从事原工作,也不能从事由用人单位另行安排的工作的;(2)劳动者不能胜任工作,经过培训或者调整工作岗位,仍不能胜任工作的。需支付经济补偿(N)+代通知金(1个月)。', steps: [ '第一步(医疗期满):确认医疗期已满,安排劳动能力鉴定', '第一步(不胜任):进行绩效考核,确认不胜任事实', '第二步(医疗期满):另行安排合适岗位,员工仍不能胜任', '第二步(不胜任):进行培训或调岗,再次考核仍不胜任', '第三步:提前30天书面通知员工,或额外支付1个月代通知金', '第四步:支付经济补偿金(N)', '第五步:出具解除证明,办理交接、社保减员', ], }, { value: 'LAYOFF', label: '公司裁员(经营困难/技术调整等)', legalBasis: '《劳动合同法》第41条', legalDetail: '有下列情形之一,需要裁减人员二十人以上或不足二十人但占企业职工总数百分之十以上的,用人单位提前三十日向工会或全体职工说明情况,听取意见后,向劳动行政部门报告,可以裁减人员:(1)依照企业破产法规定进行重整的;(2)生产经营发生严重困难的;(3)企业转产、重大技术革新或经营方式调整,经变更劳动合同仍需裁减人员的。需支付经济补偿(N)。', steps: [ '第一步:确认符合法定裁员情形,准备相关证明材料', '第二步:提前30天向工会或全体职工说明情况(书面会议记录)', '第三步:听取工会或职工意见,形成意见处理方案', '第四步:向当地劳动行政部门报告裁员方案', '第五步:确定裁员名单(优先留用法定保护人员)', '第六步:向员工送达解除通知,支付经济补偿金(N)', '第七步:办理交接、社保减员、档案转移', ], }, { value: 'EXPIRED', label: '合同到期不续签', legalBasis: '《劳动合同法》第44条、第46条', legalDetail: '劳动合同期满,劳动合同终止。除用人单位维持或提高劳动合同约定条件续订劳动合同,劳动者不同意续订的情形外,用人单位应当向劳动者支付经济补偿(N)。连续订立两次固定期限劳动合同后续订的,应当订立无固定期限劳动合同。', steps: [ '第一步:确认合同到期日期,提前评估是否续签', '第二步:如不续签,提前通知员工(建议提前30天)', '第三步:确认是否属于法定应续签无固定期限合同的情形', '第四步:支付经济补偿金(N),工作满1年支付1个月工资', '第五步:出具《终止劳动合同证明书》', '第六步:办理交接、社保减员、档案转移', ], }, { value: 'ILLEGAL', label: '违法解除(赔偿金×2)', legalBasis: '《劳动合同法》第87条', legalDetail: '用人单位违反本法规定解除或终止劳动合同的,应当依照本法第47条规定的经济补偿标准的二倍向劳动者支付赔偿金(2N)。', steps: [ '第一步:确认违法解除事实(未走法定程序、证据不足等)', '第二步:与员工协商赔偿金额,尽量达成一致', '第三步:如员工要求继续履行合同,应恢复劳动关系', '第四步:如无法恢复,支付2倍经济补偿作为赔偿金', '第五步:办理交接、社保减员、档案转移', '⚠ 建议:违法解除风险极高,操作前务必咨询专业律师', ], }, { value: 'RESIGNATION', label: '员工主动离职', legalBasis: '《劳动合同法》第37条', legalDetail: '劳动者提前三十日以书面形式通知用人单位,可以解除劳动合同。劳动者在试用期内提前三日通知用人单位,可以解除劳动合同。无需支付经济补偿。', steps: [ '第一步:接收员工书面辞职信(确认日期和签字)', '第二步:确认提前30天通知(试用期3天)', '第三步:安排工作交接计划', '第四步:结算未发放工资、未休年假补偿等', '第五步:出具《解除劳动合同证明书》', '第六步:办理社保减员、档案转移', ], }, ] const STEPS = ['选择员工', '解聘方式', '合规检查', '费用结算', '工作交接', '确认提交'] const STATUS_LABELS: Record = { DRAFT: { label: '草稿', color: 'bg-gray-100 text-gray-600' }, PENDING_APPROVAL: { label: '待审批', color: 'bg-amber-50 text-amber-700' }, APPROVED: { label: '已审批', color: 'bg-blue-50 text-blue-700' }, REJECTED: { label: '已驳回', color: 'bg-red-50 text-red-700' }, EXECUTING: { label: '执行中', color: 'bg-purple-50 text-purple-700' }, COMPLETED: { label: '已完成', color: 'bg-green-50 text-safe' }, CANCELLED: { label: '已撤销', color: 'bg-gray-100 text-gray-400' }, } const DEFAULT_HANDOVER_ITEMS = [ { key: 'work_handover', label: '工作交接完成', done: false, remark: '' }, { key: 'equipment_return', label: '办公设备归还', done: false, remark: '' }, { key: 'access_revoke', label: '系统权限收回', done: false, remark: '' }, { key: 'docs_signed', label: '离职文件签署', done: false, remark: '' }, { key: 'finance_settled', label: '财务结算完成', done: false, remark: '' }, { key: 'contract_return', label: '劳动合同收回', done: false, remark: '' }, ] interface EmployeeProfile { id: string name: string department: string status: string hireDate: string monthlySalary: number isPregnant: boolean isInMedicalPeriod: boolean isWorkInjured: boolean contracts: any[] disciplinaryRecords: any[] attendanceRecords: any[] performanceRecords: any[] trainingRecords: any[] } export default function Termination() { const queryClient = useQueryClient() const confirm = useConfirm() const [view, setView] = useState<'list' | 'wizard' | 'detail'>('list') const [draftId, setDraftId] = useState(null) const [step, setStep] = useState(0) const [reason, setReason] = useState('') const [employeeId, setEmployeeId] = useState('') const [terminationDate, setTerminationDate] = useState('') const [socialInsEndMonth, setSocialInsEndMonth] = useState('') const [housingFundEndMonth, setHousingFundEndMonth] = useState('') const [checklist, setChecklist] = useState>({}) const [acknowledgeRisk, setAcknowledgeRisk] = useState(false) const [socialAvgWage, setSocialAvgWage] = useState(0) const [, setCompBreakdown] = useState(null) const [compAdjustments, setCompAdjustments] = useState>([]) const [handoverItems, setHandoverItems] = useState(DEFAULT_HANDOVER_ITEMS) const [checklistOverrides, setChecklistOverrides] = useState>({}) const [editingCompField, setEditingCompField] = useState(null) const [editCompValue, setEditCompValue] = useState(0) const [editCompReason, setEditCompReason] = useState('') const [approvalComment, setApprovalComment] = useState('') const [savedItems, setSavedItems] = useState>([]) // 是否展开对比 const [showCompare, setShowCompare] = useState(false) const [filterStatus, setFilterStatus] = useState('') const [filterDepartment, setFilterDepartment] = useState('') const [searchTerm, setSearchTerm] = useState('') const draftPageSize = usePageSize() const [draftPage, setDraftPage] = useState(1) const { data: employees } = useQuery({ queryKey: ['roster-for-termination'], queryFn: async () => { const res = await rosterApi.list({ pageSize: 999 }) return res.data }, }) const selectedEmployee = employees?.find((e) => e.id === employeeId) const { data: departmentList } = useQuery({ queryKey: ['roster-departments'], queryFn: () => rosterApi.departments(), }) const { data: profile } = useQuery({ queryKey: ['employee-profile', employeeId], queryFn: async () => { return await rosterApi.profile(employeeId) }, enabled: !!employeeId, }) // 根据员工数据生成解聘建议 const suggestions = useMemo(() => { if (!profile) return [] const list: { reason: string; label: string; why: string }[] = [] // 有违纪记录 → 建议过错解除 if (profile.disciplinaryRecords?.length > 0) { const severe = profile.disciplinaryRecords.filter((d) => d.action === 'TERMINATION' || d.type === 'INSUBORDINATION' || d.type === 'MISCONDUCT') if (severe.length > 0) { list.push({ reason: 'FAULT', label: '过错解除', why: `有${severe.length}条严重违纪记录,可依据规章制度解除` }) } else { list.push({ reason: 'FAULT', label: '过错解除', why: `有${profile.disciplinaryRecords.length}条违纪记录,可考虑过错解除` }) } } // 绩效不佳 → 建议非过错解除 const badPerf = profile.performanceRecords?.filter((p) => p.result === 'NEED_IMPROVE' || p.result === 'UNQUALIFIED') if (badPerf?.length > 0) { const hasTraining = profile.trainingRecords?.length > 0 list.push({ reason: 'NONFAULT', label: '非过错解除', why: hasTraining ? `有${badPerf.length}次绩效不佳且已培训/调岗,可按不胜任解除` : `有${badPerf.length}次绩效不佳,需先培训或调岗后才能按不胜任解除`, }) } // 合同到期 → 建议不续签 const latestContract = profile.contracts?.[0] if (latestContract?.endDate) { const daysToExpire = Math.floor((new Date(latestContract.endDate).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24)) if (daysToExpire <= 30 && daysToExpire >= -90) { list.push({ reason: 'EXPIRED', label: '合同到期不续签', why: `合同将于${latestContract.endDate.slice(0, 10)}到期,可选择不续签` }) } } // 未签合同 → 提示双倍工资风险 if (!latestContract?.signDate || latestContract?.contractType === 'UNSIGNED') { const days = Math.floor((new Date().getTime() - new Date(profile.hireDate).getTime()) / (1000 * 60 * 60 * 24)) if (days > 30) { list.push({ reason: 'NEGOTIATED', label: '协商解除', why: `未签合同已${days}天,协商解除可同时解决双倍工资问题` }) } } // 孕期/哺乳期/工伤 → 风险提示 if (profile.isPregnant) list.push({ reason: '', label: '⚠️ 孕期禁止解除', why: '该员工在孕期/哺乳期,法律禁止以非过错理由解除' }) if (profile.isWorkInjured) list.push({ reason: '', label: '⚠️ 工伤期间禁止解除', why: '工伤期间不得解除劳动合同' }) if (profile.isInMedicalPeriod) list.push({ reason: '', label: '⚠️ 医疗期保护', why: '医疗期内不得以非过错理由解除' }) // 默认推荐协商解除 if (list.length === 0 || !list.some((s) => s.reason !== '')) { list.push({ reason: 'NEGOTIATED', label: '协商解除', why: '无特殊风险因素,推荐协商解除,成本最低、风险最小' }) } return list }, [profile]) const { data: checklistItems } = useQuery<{ key: string; label: string; autoChecked?: boolean | null; autoSource?: string; suggestion?: string; suggestionType?: string }[]>({ queryKey: ['checklist', reason, employeeId], queryFn: async () => { return await terminationApi.checklist(reason, employeeId) }, enabled: !!reason && !!employeeId && step >= 2, }) // checklist 加载后自动预填系统判断结果 useEffect(() => { if (checklistItems) { const prefilled: Record = {} checklistItems.forEach((item) => { if (item.autoChecked === true) prefilled[item.key] = true else if (item.autoChecked === false) prefilled[item.key] = false }) setChecklist(prefilled) } }, [checklistItems]) const { data: riskAssessment } = useQuery<{ level: string; warnings: string[] }>({ queryKey: ['assess', employeeId, reason], queryFn: async () => { return await terminationApi.assess(employeeId, reason) }, enabled: !!employeeId && !!reason && step >= 1, }) const saveMutation = useMutation({ mutationFn: (data: any) => terminationApi.create(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['dashboard'] }) queryClient.invalidateQueries({ queryKey: ['employees'] }) queryClient.invalidateQueries({ queryKey: ['roster'] }) queryClient.invalidateQueries({ queryKey: ['roster-profile'] }) queryClient.invalidateQueries({ queryKey: ['evidence-chain'] }) setStep(5) }, }) // 草稿列表 const { data: draftsData } = useQuery({ queryKey: ['termination-drafts', filterStatus, filterDepartment, searchTerm, draftPage, draftPageSize], queryFn: async () => { const params: any = { page: draftPage, pageSize: draftPageSize } if (filterStatus) params.status = filterStatus if (filterDepartment) params.department = filterDepartment if (searchTerm) params.search = searchTerm return await terminationApi.drafts(params) }, enabled: view === 'list', }) const drafts = draftsData?.items || [] const draftsTotal = draftsData?.total || 0 // 草稿详情 const { data: draftDetail } = useQuery({ queryKey: ['termination-detail', draftId], queryFn: async () => { return await terminationApi.detail(draftId!) }, enabled: !!draftId && view === 'detail', }) // 保存草稿 const saveDraftMutation = useMutation({ mutationFn: (data: any) => draftId ? terminationApi.updateDraft(draftId!, data) : terminationApi.createDraft(data), onSuccess: (res: any) => { const newId = draftId || res?.data?.id setDraftId(newId) toast.success('草稿已保存') queryClient.invalidateQueries({ queryKey: ['termination-drafts'] }) }, onError: () => toast.error('保存失败'), }) // 提交审批 const submitMutation = useMutation({ mutationFn: () => terminationApi.submit(draftId!), onSuccess: () => { toast.success('已提交审批') queryClient.invalidateQueries({ queryKey: ['termination-drafts'] }) setView('list') resetWizard() }, onError: () => toast.error('提交失败'), }) // 审批通过 const approveMutation = useMutation({ mutationFn: (comment: string) => terminationApi.approve(draftId!, comment), onSuccess: () => { toast.success('审批通过') queryClient.invalidateQueries({ queryKey: ['termination-drafts'] }) setView('list') }, onError: () => toast.error('操作失败'), }) // 审批驳回 const rejectMutation = useMutation({ mutationFn: (comment: string) => terminationApi.reject(draftId!, comment), onSuccess: () => { toast.success('已驳回') queryClient.invalidateQueries({ queryKey: ['termination-drafts'] }) setView('list') }, onError: () => toast.error('操作失败'), }) // 执行解聘 const executeMutation = useMutation({ mutationFn: () => terminationApi.execute(draftId!), onSuccess: () => { toast.success('解聘已执行') queryClient.invalidateQueries({ queryKey: ['termination-drafts'] }) queryClient.invalidateQueries({ queryKey: ['dashboard'] }) queryClient.invalidateQueries({ queryKey: ['roster'] }) queryClient.invalidateQueries({ queryKey: ['roster-profile'] }) queryClient.invalidateQueries({ queryKey: ['evidence-chain'] }) setView('list') resetWizard() }, onError: () => toast.error('执行失败'), }) // 撤销 const cancelMutation = useMutation({ mutationFn: () => terminationApi.cancel(draftId!), onSuccess: () => { toast.success('已撤销') queryClient.invalidateQueries({ queryKey: ['termination-drafts'] }) queryClient.invalidateQueries({ queryKey: ['roster'] }) queryClient.invalidateQueries({ queryKey: ['dashboard'] }) queryClient.invalidateQueries({ queryKey: ['roster-profile'] }) setView('list') }, onError: () => toast.error('撤销失败'), }) const { data: evidenceChain } = useQuery({ queryKey: ['evidence-chain', employeeId], queryFn: async () => { return await rosterApi.evidenceChain(employeeId) }, enabled: !!employeeId && step === 5 && saveMutation.isSuccess, }) const reasonLabel = REASONS.find((r) => r.value === reason)?.label || '' const reasonLegalBasis = REASONS.find((r) => r.value === reason)?.legalBasis || '' const costResult = useMemo(() => { if (!selectedEmployee || !terminationDate) return null const hire = new Date(selectedEmployee.hireDate) const leave = new Date(terminationDate) const totalMonths = (leave.getFullYear() - hire.getFullYear()) * 12 + (leave.getMonth() - hire.getMonth()) const years = Math.floor(totalMonths / 12) const remainingMonths = totalMonths % 12 let compMonths: number if (remainingMonths >= 6) compMonths = years + 1 else if (remainingMonths > 0) compMonths = years + 0.5 else compMonths = years if (compMonths <= 0) compMonths = 0.5 const wage = selectedEmployee.monthlySalary || 0 let capped = false let cappedWage = wage let cappedMonths = compMonths if (socialAvgWage > 0 && wage > socialAvgWage * 3) { cappedWage = socialAvgWage * 3 cappedMonths = Math.min(compMonths, 12) capped = true } const reasonMap: Record = { NEGOTIATED: { multiplier: 1, notice: false }, FAULT: { multiplier: 0, notice: false }, NONFAULT: { multiplier: 1, notice: true }, LAYOFF: { multiplier: 1, notice: false }, EXPIRED: { multiplier: 1, notice: false }, ILLEGAL: { multiplier: 2, notice: false }, } const r = reasonMap[reason] || { multiplier: 1, notice: false } const basePay = cappedWage * cappedMonths const severancePay = basePay * r.multiplier const noticePay = r.notice ? cappedWage : 0 const totalSeverance = severancePay + noticePay // 双倍工资计算(未签合同) const contract = selectedEmployee.latestContract const hasContract = contract && contract.signDate && contract.contractType !== 'UNSIGNED' let doublePay = 0 let doubleMonths = 0 let doubleStartDate = '' let doubleEndDate = '' if (!hasContract) { const startDate = new Date(hire) startDate.setMonth(startDate.getMonth() + 1) startDate.setDate(startDate.getDate() + 1) let endDate = new Date(hire) endDate.setFullYear(endDate.getFullYear() + 1) if (leave < endDate) endDate = leave doubleMonths = Math.min( Math.floor((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24 * 30.44)), 11, ) doubleMonths = Math.max(doubleMonths, 0) doublePay = wage * doubleMonths doubleStartDate = startDate.toISOString().slice(0, 10) doubleEndDate = endDate.toISOString().slice(0, 10) } return { years, remainingMonths, compMonths, wage, cappedWage, cappedMonths, capped, basePay, severancePay, noticePay, totalSeverance, doublePay, doubleMonths, doubleStartDate, doubleEndDate, hasContract, noComp: r.multiplier === 0, isIllegal: r.multiplier === 2, grandTotal: totalSeverance + doublePay, } }, [selectedEmployee, terminationDate, socialAvgWage, reason]) // 计算调整后的实际补偿金(系统预估 + 手动调整差额) const adjustedTotal = useMemo(() => { if (!costResult) return 0 let total = costResult.grandTotal compAdjustments.forEach(adj => { total += (adj.to - adj.from) }) return total }, [costResult, compAdjustments]) const canProceed = () => { if (step === 0) return !!employeeId && (!!draftId || !selectedEmployee?.hasTermination || selectedEmployee?.latestTerminationStatus === 'CANCELLED' || selectedEmployee?.latestTerminationStatus === 'COMPLETED') if (step === 1) return !!reason && !!terminationDate && (!riskAssessment?.warnings.length || acknowledgeRisk) // step 2: 合规检查 — required 项必须勾选 if (step === 2) { if (!checklistItems) return true const requiredItems = checklistItems.filter(item => item.suggestionType === 'required') if (requiredItems.length === 0) return true return requiredItems.every(item => checklist[item.key] === true) } // step 3: 费用结算 — 有补偿金时需确认 if (step === 3) return true // step 4: 工作交接 — 关键项需完成 if (step === 4) { const keyItems = handoverItems.filter(item => item.key === 'work_handover' || item.key === 'equipment_return' || item.key === 'access_revoke') if (keyItems.length === 0) return true return keyItems.every(item => item.done) } return false } /** 保存草稿(任意步骤可调用) */ const handleSaveDraft = () => { const breakdown = costResult ? { severance: costResult.severancePay, noticePay: costResult.noticePay, doublePay: costResult.doublePay, other: 0, total: adjustedTotal, systemTotal: costResult.grandTotal, adjustments: compAdjustments, } : null saveDraftMutation.mutate({ employeeId, reason, type: 'TERMINATION', terminationDate: terminationDate ? new Date(terminationDate).toISOString() : new Date().toISOString(), socialInsEndMonth: socialInsEndMonth || terminationDate.slice(0, 7), housingFundEndMonth: housingFundEndMonth || terminationDate.slice(0, 7), compensation: adjustedTotal || 0, checklist, currentStep: step, compensationBreakdown: breakdown, handoverItems, checklistOverrides, remark: '', }) } /** 编辑已有草稿 */ const handleEditDraft = (item: any) => { setDraftId(item.id) setEmployeeId(item.employeeId) setReason(item.reason) setTerminationDate(item.terminationDate) setStep(item.currentStep || 0) setView('wizard') } /** 查看详情 */ const handleViewDetail = (id: string) => { setDraftId(id) setView('detail') } /** 新建解聘 */ const handleNewTermination = () => { resetWizard() setView('wizard') } /** 补偿金分项调整 */ const handleCompAdjust = (field: string, fromVal: number, toVal: number, reason: string) => { if (!reason.trim()) { toast.error('请填写调整原因') return } setCompAdjustments(prev => [...prev, { field, from: fromVal, to: toVal, reason }]) setCompBreakdown((prev: any) => ({ ...prev, [field]: toVal, total: (prev?.total || 0) - fromVal + toVal })) setEditingCompField(null) setEditCompValue(0) setEditCompReason('') toast.success('已调整') } // 保存成功后追加正式版本(isSimulated=false) useEffect(() => { if (saveMutation.isSuccess && costResult && selectedEmployee) { setSavedItems((prev) => { // 该员工的最新正式版本 const sameEmp = prev.filter(item => item.employeeId === employeeId && !item.isSimulated) const maxVer = sameEmp.reduce((max, item) => Math.max(max, item.version), 0) const newVer = maxVer + 1 // 替换该员工的旧正式版本(如有),追加新版本 const filtered = prev.filter(item => !(item.employeeId === employeeId && !item.isSimulated)) return [ ...filtered, { id: `saved-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, employeeId, name: selectedEmployee.name, department: selectedEmployee.department, reason, reasonLabel, terminationDate, severancePay: costResult.severancePay, noticePay: costResult.noticePay, doublePay: costResult.doublePay, grandTotal: costResult.grandTotal, years: costResult.years, remainingMonths: costResult.remainingMonths, compMonths: costResult.compMonths, version: newVer, isSimulated: false, createdAt: new Date().toISOString().slice(0, 19).replace('T', ' '), }, ] }) } }, [saveMutation.isSuccess]) const resetWizard = () => { setStep(0) setReason('') setEmployeeId('') setTerminationDate('') setSocialInsEndMonth('') setHousingFundEndMonth('') setChecklist({}) setAcknowledgeRisk(false) setDraftId(null) setCompBreakdown(null) setCompAdjustments([]) setHandoverItems(DEFAULT_HANDOVER_ITEMS) setChecklistOverrides({}) setEditingCompField(null) setApprovalComment('') } const handleReset = resetWizard const totalSeverance = savedItems.reduce((sum, item) => sum + item.severancePay, 0) const totalNotice = savedItems.reduce((sum, item) => sum + item.noticePay, 0) const totalDouble = savedItems.reduce((sum, item) => sum + item.doublePay, 0) const totalGrand = savedItems.reduce((sum, item) => sum + item.grandTotal, 0) return (

解聘补偿

规范处理离职、解聘审批及补偿核算流程

{view === 'list' && ( )} {view !== 'list' && ( )}
{/* 草稿列表视图 */} {view === 'list' && ( <> 解聘补偿页面用于规范处理离职、解聘审批及补偿核算。流程:①新建解聘草稿,选择员工和解聘原因 → ②系统自动计算经济补偿金 → ③合规检查(合同状态、医疗期、孕期等) → ④确认补偿方案 → ⑤生成解聘协议等法律文书。支持协商解除、过错解除、非过错解除、经济性裁员等场景。关联:完成后可下载离职证明;社保截止请前往「社保公积金」处理;档案状态更新请前往「花名册」确认。
setSearchTerm(e.target.value)} className="!w-48" /> {(searchTerm || filterStatus || filterDepartment) && ( )}
{(!drafts || drafts.length === 0) ? ( } title="暂无解聘记录" description="点击「新建解聘」开始创建" /> ) : (
{drafts.map((item: any) => ( ))}
员工 部门 解聘原因 解聘日期 补偿金 风险 状态 更新时间 操作
{item.employeeName} {item.department} {REASONS.find(r => r.value === item.reason)?.label || item.reason} {item.terminationDate} ¥{fmt(item.compensation)} {item.riskLevel === 'HIGH' ? '高' : item.riskLevel === 'MEDIUM' ? '中' : '低'} {STATUS_LABELS[item.status]?.label || item.status} {item.updatedAt}
{(item.status === 'DRAFT' || item.status === 'REJECTED') && ( )} {item.status === 'DRAFT' && ( )} {item.status === 'PENDING_APPROVAL' && ( <> )} {item.status === 'APPROVED' && ( )} {item.status !== 'COMPLETED' && item.status !== 'CANCELLED' && ( )}
)} setDraftPage(1)} />
)} {/* 详情视图 */} {view === 'detail' && draftDetail && (

{draftDetail.employeeName}

{STATUS_LABELS[draftDetail.status]?.label || draftDetail.status}
部门:{draftDetail.department}
解聘原因:{REASONS.find(r => r.value === draftDetail.reason)?.label || draftDetail.reason}
解聘日期:{draftDetail.terminationDate}
补偿金:¥{fmt(draftDetail.compensation)}
社保截止:{draftDetail.socialInsEndMonth || '-'}
公积金截止:{draftDetail.housingFundEndMonth || '-'}
风险等级:{draftDetail.riskLevel}
创建时间:{draftDetail.createdAt}
{/* 补偿金分项 */} {draftDetail.compensationBreakdown && (
补偿金明细
经济补偿金¥{fmt(draftDetail.compensationBreakdown.severance)}
代通知金¥{fmt(draftDetail.compensationBreakdown.noticePay)}
双倍工资¥{fmt(draftDetail.compensationBreakdown.doublePay)}
其他¥{fmt(draftDetail.compensationBreakdown.other)}
合计¥{fmt(draftDetail.compensationBreakdown.total)}
{draftDetail.compensationBreakdown.adjustments?.length > 0 && (
调整记录:
{draftDetail.compensationBreakdown.adjustments.map((adj: any, i: number) => (
{adj.field}: ¥{fmt(adj.from)} → ¥{fmt(adj.to)}({adj.reason})
))}
)}
)} {/* 工作交接清单 */} {draftDetail.handoverItems && (
工作交接清单
{draftDetail.handoverItems.map((item: any, i: number) => (
{item.done ? '✓' : '○'} {item.label} {item.remark && ({item.remark})}
))}
)} {/* 审批信息 */} {draftDetail.approvalComment && (
审批意见
{draftDetail.approvalComment}
{draftDetail.approvedAt &&
审批时间:{draftDetail.approvedAt}
}
)} {/* 审批操作 */} {draftDetail.status === 'PENDING_APPROVAL' && (
setApprovalComment(e.target.value)} placeholder="填写审批意见..." />
)} {/* 执行操作 */} {draftDetail.status === 'APPROVED' && (
)} {/* 撤销操作 */} {draftDetail.status !== 'COMPLETED' && draftDetail.status !== 'CANCELLED' && (
)} {/* 下载离职证明 & 交接清单 */} {draftDetail.status === 'COMPLETED' && (
{draftDetail.handoverItems && draftDetail.handoverItems.length > 0 && ( )}
)}
)} {/* 向导视图 */} {view === 'wizard' && ( <>
{/* 左侧:向导 */}
{/* 步骤条 */} ({ key: String(i), title, status: i < step ? 'complete' : i === step ? 'current' : 'pending', }))} onStepClick={(key) => { const i = parseInt(key); if (i <= step) setStep(i) }} className="mb-4" />
Step {step + 1}/6:{STEPS[step]}
{/* Step 1: 选择员工 */} {step === 0 && (
{selectedEmployee && (
{selectedEmployee.name}({selectedEmployee.department})
入职日期:{selectedEmployee.hireDate?.toString().slice(0, 10)}
月工资:¥{fmt(selectedEmployee.monthlySalary)}
{selectedEmployee.hasTermination && selectedEmployee.latestTerminationStatus !== 'CANCELLED' && selectedEmployee.latestTerminationStatus !== 'COMPLETED' && (
⚠️ 该员工已有离职/解聘记录,如需再次解聘请先办理重新雇佣
)} {selectedEmployee.latestContract ? (
合同状态:{selectedEmployee.latestContract.contractType === 'UNSIGNED' ? '未签订' : `签订于 ${selectedEmployee.latestContract.signDate?.slice(0, 10) || '未知'}`}
) : (
⚠️ 无合同记录
)} {selectedEmployee.counts && (
{selectedEmployee.counts.disciplinaryRecords > 0 && ( 违纪记录:{selectedEmployee.counts.disciplinaryRecords}条 )} {selectedEmployee.counts.performanceRecords > 0 && ( 绩效记录:{selectedEmployee.counts.performanceRecords}条 )} {selectedEmployee.counts.attendanceRecords > 0 && ( 考勤记录:{selectedEmployee.counts.attendanceRecords}条 )}
)}
)} {profile && suggestions.length > 0 && (
📋 解聘方式建议
{suggestions.map((s, i) => (
{s.label}
{s.why}
))}
)} {employeeId && !profile && (
加载员工档案中...
)}
)} {/* Step 2: 解聘方式 */} {step === 1 && (
{suggestions.length > 0 && (
💡 系统建议
{suggestions.filter((s) => s.reason).map((s, i) => (
{s.label}:{s.why}
))}
)}
{REASONS.map((r) => { const suggested = suggestions.find((s) => s.reason === r.value) return ( ) })}
{/* 法律条款详情 + 操作步骤 */} {reason && (() => { const r = REASONS.find((x) => x.value === reason) if (!r) return null return (
法律依据:{r.legalBasis}

{r.legalDetail}

📋 规范操作流程
    {r.steps.map((s, i) => (
  1. {s}
  2. ))}
) })()}
setTerminationDate(e.target.value)} />
{/* 实时费用预览 - 当有足够数据时在解聘方式下方显示 */} {step === 1 && costResult && (
实时费用预览
工作年限:{costResult.years}年{costResult.remainingMonths}个月 · 月工资:¥{fmt(costResult.wage)}
{costResult.capped && (
⚠️ 工资超社平3倍,按三倍封顶且最多12个月
)} {costResult.noComp ? (
员工过错解除,无需支付经济补偿金
) : (
{costResult.isIllegal ? '违法解除赔偿金(×2)' : '经济补偿金'} ({costResult.cappedMonths}个月 × ¥{fmt(costResult.cappedWage)}) ¥{fmt(costResult.severancePay)}
)} {costResult.noticePay > 0 && (
代通知金 ¥{fmt(costResult.noticePay)}
)} {costResult.isIllegal && (
违法解除赔偿金 = 经济补偿金 × 2(《劳动合同法》第87条)
)} {!costResult.hasContract && costResult.doubleMonths > 0 && (
未签合同双倍工资({costResult.doubleMonths}个月) ¥{fmt(costResult.doublePay)}
)}
预估合计 ¥{fmt(costResult.grandTotal)}
)}
默认与解聘日期同月,可手动修改
setSocialInsEndMonth(e.target.value)} />
setHousingFundEndMonth(e.target.value)} />
{terminationDate && ((socialInsEndMonth && socialInsEndMonth !== terminationDate.slice(0, 7)) || (housingFundEndMonth && housingFundEndMonth !== terminationDate.slice(0, 7))) && (
截止缴费年月与解聘日期不在同月,请确认是否为多缴/少缴月份。
)}
{/* 禁止解聘检查 */} {riskAssessment && riskAssessment.warnings.length > 0 && (
{riskAssessment.warnings.map((w, i) => ( {w} ))}
)}
)} {/* Step 3: 合规检查 */} {step === 2 && (
系统已根据员工记录自动预填部分检查项,您可逐项调整后继续。 {checklistItems?.map((item) => { const checked = checklist[item.key] || false const isAutoChecked = item.autoChecked !== null && item.autoChecked !== undefined const suggestionColor = item.suggestionType === 'warning' ? 'bg-amber-50 text-amber-700' : item.suggestionType === 'required' ? 'bg-red-50 text-red-700' : 'bg-gray-50 text-gray-600' return (
{item.autoSource && (
{item.autoSource}
)} {item.suggestion && (
{item.suggestion}
)}
) })}
)} {/* Step 4: 费用结算 */} {step === 3 && (
setSocialAvgWage(Number(e.target.value) || 0)} placeholder="用于三倍封顶计算" />
{costResult && (
{/* 员工概况 */}
{selectedEmployee?.name}({selectedEmployee?.department})
工作年限:{costResult.years}年{costResult.remainingMonths}个月
月工资:¥{fmt(costResult.wage)}/月
{costResult.capped && (
⚠️ 工资超过社平3倍,已按三倍封顶且最多补偿12个月
)}
{/* 经济补偿金 / 赔偿金 */} {costResult.noComp ? (
员工过错解除,无需支付经济补偿金
) : (
{costResult.isIllegal ? '违法解除赔偿金' : '经济补偿金'}
补偿月数:{costResult.cappedMonths}个月
计算基数:¥{fmt(costResult.cappedWage)}/月
{costResult.isIllegal && (
经济补偿金 ¥{fmt(costResult.basePay)}
)}
{costResult.isIllegal ? '赔偿金(×2)' : '补偿金'} ¥{fmt(costResult.severancePay)}
{costResult.noticePay > 0 && (
代通知金 ¥{fmt(costResult.noticePay)}
)} {costResult.noticePay > 0 && (
含代通知金 ¥{fmt(costResult.noticePay)}
)} {costResult.isIllegal && (
违法解除劳动合同,按经济补偿金的2倍支付赔偿金(《劳动合同法》第87条)
)}
)} {/* 双倍工资(未签合同自动触发) */} {!costResult.hasContract && costResult.doubleMonths > 0 && (
未签劳动合同双倍工资
双倍工资起算:{costResult.doubleStartDate}
双倍工资截止:{costResult.doubleEndDate}
赔偿月数:{costResult.doubleMonths}个月
双倍工资赔偿 ¥{fmt(costResult.doublePay)}
({costResult.doubleMonths}个月 × ¥{fmt(costResult.wage)})
入职1个月未签合同,从第2个月起需付双倍工资,最多11个月
)} {/* 合计 */}
合计应付 ¥{fmt(costResult.grandTotal)}
{/* 补偿金分项手动调整 */}
手动调整补偿金分项
点击各分项可手动修改金额,需填写调整原因
{[ { field: 'severance', label: '经济补偿金/赔偿金', value: costResult.severancePay }, { field: 'noticePay', label: '代通知金', value: costResult.noticePay }, { field: 'doublePay', label: '未签合同双倍工资', value: costResult.doublePay }, { field: 'other', label: '其他费用', value: 0 }, ].map((item) => (
{item.label} {editingCompField === item.field ? (
setEditCompValue(Number(e.target.value) || 0)} className="w-24 text-xs" placeholder="新金额" /> setEditCompReason(e.target.value)} className="w-32 text-xs" placeholder="调整原因" />
) : (
¥{fmt(compAdjustments.find(a => a.field === item.field)?.to ?? item.value)}
)}
))} {compAdjustments.length > 0 && (
调整记录:
{compAdjustments.map((adj, i) => (
{adj.field}: ¥{fmt(adj.from)} → ¥{fmt(adj.to)}({adj.reason})
))}
)}
{!costResult.noComp && (
满1年补1个月,满6个月不满1年按1年算,不满6个月补半个月
)}
)}
)} {/* Step 5: 工作交接 */} {step === 4 && (
请逐项确认工作交接事项,可添加备注说明。勾选完成后进入下一步。
{handoverItems.map((item, i) => (
setHandoverItems(prev => prev.map((p, idx) => idx === i ? { ...p, remark: e.target.value } : p))} placeholder="备注说明(选填)" className="text-xs" />
))}
已完成 {handoverItems.filter(i => i.done).length}/{handoverItems.length} 项 {handoverItems.every(i => i.done) && ( 全部完成 )}
)} {/* Step 6: 确认提交 */} {step === 5 && (
{/* 汇总信息 */}
{selectedEmployee?.name}({selectedEmployee?.department})
解聘原因:{reasonLabel}
解聘日期:{terminationDate}
社保截止:{socialInsEndMonth || terminationDate.slice(0, 7)}
公积金截止:{housingFundEndMonth || terminationDate.slice(0, 7)}
{costResult && ( <> {compAdjustments.length > 0 ? ( <>
系统预估补偿金:¥{fmt(costResult.grandTotal)}
{compAdjustments.map((adj, i) => (
手动调整 - {adj.field}:¥{fmt(adj.from)} → ¥{fmt(adj.to)}({adj.reason})
))}
实际补偿金合计:¥{fmt(adjustedTotal)}
) : (
补偿金合计:¥{fmt(costResult.grandTotal)}
)} )}
工作交接:{handoverItems.filter(i => i.done).length}/{handoverItems.length} 项完成
{/* 风险提示 */} {riskAssessment && riskAssessment.warnings.length > 0 && (
{riskAssessment.warnings.map((w, i) => (
{w}
))}
)} {/* 操作按钮 */}
选择操作:
保存草稿:可稍后继续编辑 · 提交审批:需审批人确认后执行 · 直接执行:跳过审批立即生效
)} {/* Step 6: 解聘材料(执行完成后展示) */} {step === 5 && saveMutation.isSuccess && (
{saveMutation.isError ? (
保存失败
{(saveMutation as any).error?.response?.data?.error?.message || '请稍后重试'}
) : saveMutation.isPending ? (
保存中...
) : (
{/* 成功提示 */}
解聘记录已保存,以下为完整解聘材料
{/* 打印按钮 */}
{/* 1. 解聘通知书 */}

解除劳动合同通知书

{selectedEmployee?.name} 先生/女士:

您于 {selectedEmployee?.hireDate?.toString().slice(0, 10)} 入职我公司{selectedEmployee?.department}部门。 因 {reasonLabel} 原因,公司决定于 {terminationDate} 起解除与您的劳动合同。

解除依据:{reasonLegalBasis}

{costResult && !costResult.noComp && (

经济补偿金:补偿月数 {costResult.cappedMonths} 个月,计算基数 ¥{fmt(costResult.cappedWage)}/月, 应付金额 ¥{fmt(costResult.severancePay)} {costResult.noticePay > 0 && `(含代通知金 ¥${fmt(costResult.noticePay)})`} 。

)} {costResult && costResult.noComp && (

因员工过错解除,无需支付经济补偿金。

)} {costResult && !costResult.hasContract && costResult.doubleMonths > 0 && (

未签订劳动合同双倍工资:{costResult.doubleMonths}个月,合计 ¥{fmt(costResult.doublePay)}

)} {costResult && (

合计应付金额:¥{fmt(costResult.grandTotal)}

)}

请于解除日期前办理工作交接手续,结清相关费用。

公司(盖章)

{new Date().toISOString().slice(0, 10)}

{/* 2. 费用结算明细 */} {costResult && (

费用结算明细

工作年限{costResult.years}年{costResult.remainingMonths}个月
月工资¥{fmt(costResult.wage)}/月
{costResult.capped &&
⚠️ 工资超过社平3倍,已按三倍封顶且最多补偿12个月
} {!costResult.noComp && ( <>
补偿月数{costResult.cappedMonths}个月
计算基数¥{fmt(costResult.cappedWage)}/月
{costResult.isIllegal ? '违法解除赔偿金(×2)' : '经济补偿金'}¥{fmt(costResult.severancePay)}
{costResult.noticePay > 0 &&
代通知金¥{fmt(costResult.noticePay)}
} )} {!costResult.hasContract && costResult.doubleMonths > 0 && (
未签合同双倍工资({costResult.doubleMonths}个月)¥{fmt(costResult.doublePay)}
)}
合计应付¥{fmt(costResult.grandTotal)}
)} {/* 3. 合规检查清单 */}

合规检查清单

{checklistItems?.map((item) => (
{checklist[item.key] ? '✓' : '✗'} {item.label}
))} {riskAssessment && riskAssessment.warnings.length > 0 && (
{riskAssessment.warnings.map((w, i) => (
{w}
))}
)}
{/* 4. 仲裁证据链 */}

仲裁证据链

{evidenceChain ? ( <>
共 {evidenceChain.summary?.total || 0} 条证据, 已签确认 {evidenceChain.summary?.signed || 0} 条, 未签 {evidenceChain.summary?.unsigned || 0} 条
{(() => { const grouped = (evidenceChain.evidence || []).reduce((acc: Record, e: any) => { (acc[e.category] = acc[e.category] || []).push(e) return acc }, {}) return Object.entries(grouped).map(([category, items]) => (
{category as string}
{(items as any[]).map((e: any, i: number) => (
{e.title} {e.acknowledged === true && ✓已签} {e.acknowledged === false && ✗未签}
{e.description}
))}
)) })()} ) : (
加载证据链中...
)}
)}
)} {/* 导航按钮 */} {step < 5 && (
{step < 4 ? (
{!canProceed() && step === 2 && checklistItems?.some(item => item.suggestionType === 'required' && !checklist[item.key]) && ( 请先勾选所有必检项 )} {!canProceed() && step === 4 && handoverItems.some(item => (item.key === 'work_handover' || item.key === 'equipment_return' || item.key === 'access_revoke') && !item.done) && ( 请先完成工作交接、设备归还、权限收回 )}
) : step === 4 ? (
) : null}
)}
{/* 右侧:暂存列表 */}

已计算列表

{savedItems.length > 0 && ( )} {savedItems.length > 0 && ( <> )}
{savedItems.length === 0 && !showCompare ? (
计算完一人后
暂存结果将显示在此
) : showCompare ? (
{savedItems.map((item, i) => (
{item.name}
{item.department}
{item.reasonLabel} {item.years}年{item.remainingMonths}月
{item.severancePay > 0 && (
补偿金¥{fmt(item.severancePay)}
)} {item.noticePay > 0 && (
代通知金¥{fmt(item.noticePay)}
)} {item.doublePay > 0 && (
双倍工资¥{fmt(item.doublePay)}
)}
合计¥{fmt(item.grandTotal)}
))} {/* 合计 */}
合计({savedItems.length}人)
{totalSeverance > 0 && (
补偿金¥{fmt(totalSeverance)}
)} {totalNotice > 0 && (
代通知金¥{fmt(totalNotice)}
)} {totalDouble > 0 && (
双倍工资¥{fmt(totalDouble)}
)}
总计 ¥{fmt(totalGrand)}
) : (
{savedItems.map((item, i) => (
{item.name}
{item.department}
{item.reasonLabel} {item.years}年{item.remainingMonths}月
{item.severancePay > 0 && (
补偿金¥{fmt(item.severancePay)}
)} {item.noticePay > 0 && (
代通知金¥{fmt(item.noticePay)}
)} {item.doublePay > 0 && (
双倍工资¥{fmt(item.doublePay)}
)}
合计¥{fmt(item.grandTotal)}
))} {/* 合计 */}
合计({savedItems.length}人)
{totalSeverance > 0 && (
补偿金¥{fmt(totalSeverance)}
)} {totalNotice > 0 && (
代通知金¥{fmt(totalNotice)}
)} {totalDouble > 0 && (
双倍工资¥{fmt(totalDouble)}
)}
总计 ¥{fmt(totalGrand)}
)}
)}
) }