init: AI HR Compliance Assistant

This commit is contained in:
freedakgmail
2026-07-23 12:34:43 +08:00
commit 820579e98d
81 changed files with 19327 additions and 0 deletions
+709
View File
@@ -0,0 +1,709 @@
import { useState, useMemo } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { AlertTriangle, Check, ChevronRight, ChevronLeft, Shield, Info, Calculator, FileText, Printer } from 'lucide-react'
import api from '../lib/api'
import Card from '../components/ui/Card'
import Button from '../components/ui/Button'
import { Input, Label, Select } from '../components/ui/Input'
import Signal from '../components/ui/Signal'
const REASONS = [
{ value: 'NEGOTIATED', label: '协商解除(双方同意分开了)', legalBasis: '《劳动合同法》第36条' },
{ value: 'FAULT', label: '员工犯错被辞退(严重违纪/失职等)', legalBasis: '《劳动合同法》第39条' },
{ value: 'NONFAULT', label: '员工没犯错但干不了(生病/不胜任等)', legalBasis: '《劳动合同法》第40条' },
{ value: 'LAYOFF', label: '公司裁员(经营困难/技术调整等)', legalBasis: '《劳动合同法》第41条' },
{ value: 'EXPIRED', label: '合同到期不续签', legalBasis: '《劳动合同法》第44条、第46条' },
{ value: 'ILLEGAL', label: '违法解除(赔偿金×2', legalBasis: '《劳动合同法》第87条' },
]
const STEPS = ['选择员工', '解聘方式', '合规检查', '费用结算', '确认完成']
interface RosterEmployee {
id: string
name: string
department: string
status: string
hireDate: string
monthlySalary: number
latestContract: any
counts: any
}
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 [step, setStep] = useState(0)
const [reason, setReason] = useState('')
const [employeeId, setEmployeeId] = useState('')
const [terminationDate, setTerminationDate] = useState('')
const [checklist, setChecklist] = useState<Record<string, boolean>>({})
const [acknowledgeRisk, setAcknowledgeRisk] = useState(false)
const [socialAvgWage, setSocialAvgWage] = useState(0)
const { data: employees } = useQuery<RosterEmployee[]>({
queryKey: ['roster-for-termination'],
queryFn: async () => {
const res = await api.get('/roster') as any
return res.data
},
})
const selectedEmployee = employees?.find((e) => e.id === employeeId)
const { data: profile } = useQuery<EmployeeProfile>({
queryKey: ['employee-profile', employeeId],
queryFn: async () => {
const res = await api.get(`/roster/${employeeId}/profile`) as any
return res.data
},
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 }[]>({
queryKey: ['checklist', reason],
queryFn: async () => {
const res = await api.get(`/termination/checklist/${reason}`) as any
return res.data
},
enabled: !!reason && step >= 2,
})
const { data: riskAssessment } = useQuery<{ level: string; warnings: string[] }>({
queryKey: ['assess', employeeId, reason],
queryFn: async () => {
const res = await api.get(`/termination/assess/${employeeId}`, { params: { reason } }) as any
return res.data
},
enabled: !!employeeId && !!reason && step >= 1,
})
const saveMutation = useMutation({
mutationFn: (data: any) => api.post('/termination', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
queryClient.invalidateQueries({ queryKey: ['employees'] })
setStep(4)
},
})
const { data: evidenceChain } = useQuery({
queryKey: ['evidence-chain', employeeId],
queryFn: async () => {
const res = await api.get(`/roster/${employeeId}/evidence-chain`) as any
return res.data
},
enabled: !!employeeId && step === 4 && 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<string, { multiplier: number; notice: boolean }> = {
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 canProceed = () => {
if (step === 0) return !!employeeId
if (step === 1) return !!reason && !!terminationDate && (!riskAssessment?.warnings.length || acknowledgeRisk)
if (step === 2) return true
if (step === 3) return true
return false
}
const handleSave = () => {
saveMutation.mutate({
employeeId,
reason,
terminationDate: new Date(terminationDate).toISOString(),
compensation: costResult?.totalSeverance || 0,
checklist,
remark: '',
})
}
const handleReset = () => {
setStep(0)
setReason('')
setEmployeeId('')
setTerminationDate('')
setChecklist({})
setAcknowledgeRisk(false)
}
return (
<div className="space-y-4">
<h1 className="text-lg font-semibold"></h1>
{/* 进度条 */}
<div className="flex items-center gap-1">
{STEPS.map((s, i) => (
<div key={i} className="flex items-center">
<div className={`w-2.5 h-2.5 rounded-full ${i <= step ? 'bg-primary' : 'bg-gray-300'}`} />
{i < STEPS.length - 1 && <div className={`w-8 h-0.5 ${i < step ? 'bg-primary' : 'bg-gray-300'}`} />}
</div>
))}
</div>
<Card>
<div className="mb-2 text-sm text-gray-500">Step {step + 1}/5{STEPS[step]}</div>
{/* Step 1: 选择员工 */}
{step === 0 && (
<div className="space-y-4">
<div>
<Label></Label>
<Select value={employeeId} onChange={(e) => setEmployeeId(e.target.value)}>
<option value=""></option>
{employees?.map((emp) => (
<option key={emp.id} value={emp.id}>{emp.name} - {emp.department}</option>
))}
</Select>
</div>
{selectedEmployee && (
<div className="text-sm text-gray-600 bg-gray-50 p-3 rounded-md space-y-1">
<div className="font-medium">{selectedEmployee.name}{selectedEmployee.department}</div>
<div>{selectedEmployee.hireDate?.toString().slice(0, 10)}</div>
<div>¥{selectedEmployee.monthlySalary.toLocaleString()}</div>
{selectedEmployee.latestContract ? (
<div>{selectedEmployee.latestContract.contractType === 'UNSIGNED' ? '未签订' : `签订于 ${selectedEmployee.latestContract.signDate?.slice(0, 10) || '未知'}`}</div>
) : (
<div className="text-warning"> </div>
)}
{selectedEmployee.counts && (
<div className="flex gap-3 flex-wrap mt-2">
{selectedEmployee.counts.disciplinaryRecords > 0 && (
<span className="text-danger">{selectedEmployee.counts.disciplinaryRecords}</span>
)}
{selectedEmployee.counts.performanceRecords > 0 && (
<span>{selectedEmployee.counts.performanceRecords}</span>
)}
{selectedEmployee.counts.attendanceRecords > 0 && (
<span>{selectedEmployee.counts.attendanceRecords}</span>
)}
</div>
)}
</div>
)}
{profile && suggestions.length > 0 && (
<div className="space-y-2">
<div className="text-sm font-medium">📋 </div>
{suggestions.map((s, i) => (
<div
key={i}
className={`px-3 py-2 rounded-md text-sm ${s.reason === '' ? 'bg-red-50 text-red-700' : 'bg-blue-50 text-blue-700'}`}
>
<div className="font-medium">{s.label}</div>
<div className="text-xs mt-0.5">{s.why}</div>
</div>
))}
</div>
)}
{employeeId && !profile && (
<div className="text-sm text-gray-400">...</div>
)}
</div>
)}
{/* Step 2: 解聘方式 */}
{step === 1 && (
<div className="space-y-4">
{suggestions.length > 0 && (
<div className="bg-blue-50 rounded-md p-3 space-y-1">
<div className="text-sm font-medium text-blue-700">💡 </div>
{suggestions.filter((s) => s.reason).map((s, i) => (
<div key={i} className="text-xs text-blue-600">
{s.label}{s.why}
</div>
))}
</div>
)}
<div className="space-y-2">
{REASONS.map((r) => {
const suggested = suggestions.find((s) => s.reason === r.value)
return (
<label
key={r.value}
className={`flex items-start gap-3 p-3 rounded-md border cursor-pointer hover:bg-gray-50 ${suggested ? 'border-primary bg-primary/5' : ''}`}
>
<input type="radio" name="reason" value={r.value} checked={reason === r.value} onChange={(e) => setReason(e.target.value)} className="mt-0.5" />
<div className="flex-1">
<div className="text-sm flex items-center gap-2">
{r.label}
{suggested && <span className="text-xs text-primary font-medium"></span>}
</div>
{suggested && (
<div className="text-xs text-gray-500 mt-0.5">{suggested.why}</div>
)}
</div>
</label>
)
})}
</div>
<div>
<Label></Label>
<Input type="date" value={terminationDate} onChange={(e) => setTerminationDate(e.target.value)} />
</div>
{/* 禁止解聘检查 */}
{riskAssessment && riskAssessment.warnings.length > 0 && (
<div className="space-y-2">
{riskAssessment.warnings.map((w, i) => (
<div key={i} className="flex items-center gap-2 px-3 py-2 rounded-md bg-red-50 text-red-700 text-sm">
<AlertTriangle className="w-4 h-4 shrink-0" />
{w}
</div>
))}
<label className="flex items-center gap-2 text-sm px-3 py-2 rounded-md bg-yellow-50 text-yellow-800">
<input type="checkbox" checked={acknowledgeRisk} onChange={(e) => setAcknowledgeRisk(e.target.checked)} />
</label>
</div>
)}
</div>
)}
{/* Step 3: 合规检查 */}
{step === 2 && (
<div className="space-y-3">
{checklistItems?.map((item) => (
<label key={item.key} className="flex items-center gap-3 p-3 rounded-md border cursor-pointer hover:bg-gray-50">
<input
type="checkbox"
checked={checklist[item.key] || false}
onChange={(e) => setChecklist({ ...checklist, [item.key]: e.target.checked })}
/>
<span className="text-sm">{item.label}</span>
</label>
))}
</div>
)}
{/* Step 4: 费用结算 */}
{step === 3 && (
<div className="space-y-4">
<div>
<Label></Label>
<Input type="number" value={socialAvgWage} onChange={(e) => setSocialAvgWage(Number(e.target.value) || 0)} placeholder="用于三倍封顶计算" />
</div>
{costResult && (
<div className="space-y-4">
{/* 员工概况 */}
<div className="text-sm text-gray-600 bg-gray-50 p-3 rounded-md space-y-1">
<div className="font-medium">{selectedEmployee?.name}{selectedEmployee?.department}</div>
<div>{costResult.years}{costResult.remainingMonths}</div>
<div>¥{costResult.wage.toLocaleString()}/</div>
{costResult.capped && (
<div className="text-warning"> 312</div>
)}
</div>
{/* 经济补偿金 / 赔偿金 */}
{costResult.noComp ? (
<div className="px-3 py-2 rounded-md bg-gray-50 text-gray-700 text-sm">
</div>
) : (
<div className="border rounded-md p-4 space-y-2">
<div className="font-medium flex items-center gap-2">
<Calculator className="w-4 h-4" />
{costResult.isIllegal ? '违法解除赔偿金' : '经济补偿金'}
</div>
<div className="text-sm text-gray-500">{costResult.cappedMonths}</div>
<div className="text-sm text-gray-500">¥{costResult.cappedWage.toLocaleString()}/</div>
{costResult.isIllegal && (
<div className="flex items-center justify-between text-sm">
<span className="text-gray-500"></span>
<span>¥{costResult.basePay.toLocaleString(undefined, { maximumFractionDigits: 2 })}</span>
</div>
)}
<div className="flex items-center justify-between">
<span className="font-medium">{costResult.isIllegal ? '赔偿金(×2' : '补偿金'}</span>
<span className={`text-lg font-bold ${costResult.isIllegal ? 'text-danger' : 'text-primary'}`}>
¥{costResult.severancePay.toLocaleString(undefined, { maximumFractionDigits: 2 })}
</span>
</div>
{costResult.noticePay > 0 && (
<div className="flex items-center justify-between text-sm">
<span className="text-gray-500"></span>
<span>¥{costResult.noticePay.toLocaleString()}</span>
</div>
)}
{costResult.noticePay > 0 && (
<div className="text-xs text-gray-400"> ¥{costResult.noticePay.toLocaleString()}</div>
)}
{costResult.isIllegal && (
<div className="flex items-start gap-2 px-3 py-2 rounded-md bg-red-50 text-red-700 text-xs">
<Info className="w-3 h-3 mt-0.5 shrink-0" />
<span>287</span>
</div>
)}
</div>
)}
{/* 双倍工资(未签合同自动触发) */}
{!costResult.hasContract && costResult.doubleMonths > 0 && (
<div className="border border-warning rounded-md p-4 space-y-2">
<div className="font-medium flex items-center gap-2 text-warning">
<AlertTriangle className="w-4 h-4" />
</div>
<div className="text-sm text-gray-500">{costResult.doubleStartDate}</div>
<div className="text-sm text-gray-500">{costResult.doubleEndDate}</div>
<div className="text-sm text-gray-500">{costResult.doubleMonths}</div>
<div className="flex items-center justify-between">
<span className="font-medium"></span>
<span className="text-lg font-bold text-warning">¥{costResult.doublePay.toLocaleString()}</span>
</div>
<div className="text-xs text-gray-400">{costResult.doubleMonths} × ¥{costResult.wage.toLocaleString()}</div>
<div className="flex items-start gap-2 px-3 py-2 rounded-md bg-yellow-50 text-yellow-800 text-xs">
<Info className="w-3 h-3 mt-0.5 shrink-0" />
<span>1211</span>
</div>
</div>
)}
{/* 合计 */}
<div className="border-t pt-3">
<div className="flex items-center justify-between">
<span className="font-medium"></span>
<span className="text-xl font-bold text-danger">¥{costResult.grandTotal.toLocaleString(undefined, { maximumFractionDigits: 2 })}</span>
</div>
</div>
{!costResult.noComp && (
<div className="flex items-start gap-2 px-3 py-2 rounded-md bg-blue-50 text-blue-700 text-sm">
<Info className="w-4 h-4 mt-0.5 shrink-0" />
<span>116116</span>
</div>
)}
</div>
)}
</div>
)}
{/* Step 5: 解聘材料 */}
{step === 4 && (
<div className="space-y-4">
{saveMutation.isError ? (
<div className="text-center py-8">
<AlertTriangle className="w-12 h-12 text-danger mx-auto" />
<div className="text-danger font-medium mt-2"></div>
<div className="text-sm text-gray-500">{(saveMutation.error as any)?.response?.data?.error?.message || '请稍后重试'}</div>
<Button onClick={() => setStep(3)} className="mt-4"></Button>
</div>
) : saveMutation.isPending ? (
<div className="text-center py-8 text-gray-400">...</div>
) : (
<div className="space-y-4">
{/* 成功提示 */}
<div className="flex items-center gap-2 text-safe">
<Check className="w-5 h-5" />
<span className="font-medium"></span>
</div>
{/* 打印按钮 */}
<div className="flex justify-end gap-2">
<Button variant="secondary" size="sm" onClick={() => window.print()}>
<Printer className="w-4 h-4 mr-1" />
</Button>
<Button variant="secondary" size="sm" onClick={handleReset}>
</Button>
</div>
{/* 1. 解聘通知书 */}
<div className="border rounded-lg p-6 space-y-4 print:shadow-none">
<div className="text-center">
<h2 className="text-lg font-bold"></h2>
</div>
<div className="text-sm text-gray-700 space-y-3">
<p><strong>{selectedEmployee?.name}</strong> /</p>
<p>
<strong>{selectedEmployee?.hireDate?.toString().slice(0, 10)}</strong> {selectedEmployee?.department}
<strong>{reasonLabel}</strong> <strong>{terminationDate}</strong>
</p>
<p>
{reasonLegalBasis}
</p>
{costResult && !costResult.noComp && (
<p>
<strong>{costResult.cappedMonths}</strong> <strong>¥{costResult.cappedWage.toLocaleString()}/</strong>
<strong>¥{costResult.severancePay.toLocaleString(undefined, { maximumFractionDigits: 2 })}</strong>
{costResult.noticePay > 0 && `(含代通知金 ¥${costResult.noticePay.toLocaleString()}`}
</p>
)}
{costResult && costResult.noComp && (
<p></p>
)}
{costResult && !costResult.hasContract && costResult.doubleMonths > 0 && (
<p>
{costResult.doubleMonths} <strong>¥{costResult.doublePay.toLocaleString()}</strong>
</p>
)}
{costResult && (
<p><strong>¥{costResult.grandTotal.toLocaleString(undefined, { maximumFractionDigits: 2 })}</strong></p>
)}
<p></p>
<div className="text-right mt-6 space-y-1">
<p></p>
<p className="text-gray-400">{new Date().toISOString().slice(0, 10)}</p>
</div>
</div>
</div>
{/* 2. 费用结算明细 */}
{costResult && (
<div className="border rounded-lg p-4 space-y-2">
<h3 className="font-medium flex items-center gap-2"><Calculator className="w-4 h-4" /></h3>
<div className="text-sm space-y-1">
<div className="flex justify-between"><span></span><span>{costResult.years}{costResult.remainingMonths}</span></div>
<div className="flex justify-between"><span></span><span>¥{costResult.wage.toLocaleString()}/</span></div>
{costResult.capped && <div className="text-warning"> 312</div>}
{!costResult.noComp && (
<>
<div className="flex justify-between"><span></span><span>{costResult.cappedMonths}</span></div>
<div className="flex justify-between"><span></span><span>¥{costResult.cappedWage.toLocaleString()}/</span></div>
<div className="flex justify-between font-medium"><span>{costResult.isIllegal ? '违法解除赔偿金(×2' : '经济补偿金'}</span><span>¥{costResult.severancePay.toLocaleString(undefined, { maximumFractionDigits: 2 })}</span></div>
{costResult.noticePay > 0 && <div className="flex justify-between"><span></span><span>¥{costResult.noticePay.toLocaleString()}</span></div>}
</>
)}
{!costResult.hasContract && costResult.doubleMonths > 0 && (
<div className="flex justify-between text-warning"><span>{costResult.doubleMonths}</span><span>¥{costResult.doublePay.toLocaleString()}</span></div>
)}
<div className="flex justify-between border-t pt-2 font-bold text-danger"><span></span><span>¥{costResult.grandTotal.toLocaleString(undefined, { maximumFractionDigits: 2 })}</span></div>
</div>
</div>
)}
{/* 3. 合规检查清单 */}
<div className="border rounded-lg p-4 space-y-2">
<h3 className="font-medium flex items-center gap-2"><Shield className="w-4 h-4" /></h3>
<div className="text-sm space-y-1">
{checklistItems?.map((item) => (
<div key={item.key} className="flex items-center gap-2">
<span className={checklist[item.key] ? 'text-safe' : 'text-danger'}>
{checklist[item.key] ? '✓' : '✗'}
</span>
<span className={checklist[item.key] ? '' : 'text-gray-500'}>{item.label}</span>
</div>
))}
{riskAssessment && riskAssessment.warnings.length > 0 && (
<div className="mt-2 space-y-1">
{riskAssessment.warnings.map((w, i) => (
<div key={i} className="flex items-center gap-2 text-danger">
<AlertTriangle className="w-3 h-3" />{w}
</div>
))}
</div>
)}
</div>
</div>
{/* 4. 仲裁证据链 */}
<div className="border rounded-lg p-4 space-y-3">
<h3 className="font-medium flex items-center gap-2"><FileText className="w-4 h-4" /></h3>
{evidenceChain ? (
<>
<div className="text-xs text-gray-500">
{evidenceChain.summary?.total || 0}
{evidenceChain.summary?.signed || 0}
{evidenceChain.summary?.unsigned || 0}
</div>
{(() => {
const grouped = (evidenceChain.evidence || []).reduce((acc: Record<string, any[]>, e: any) => {
(acc[e.category] = acc[e.category] || []).push(e)
return acc
}, {})
return Object.entries(grouped).map(([category, items]) => (
<div key={category} className="space-y-1">
<div className="text-sm font-medium text-gray-700">{category as string}</div>
{(items as any[]).map((e: any, i: number) => (
<div key={i} className="text-xs text-gray-600 pl-4 border-l-2 border-gray-200 ml-1">
<div className="flex items-center gap-2">
<span>{e.title}</span>
{e.acknowledged === true && <span className="text-safe"></span>}
{e.acknowledged === false && <span className="text-danger"></span>}
</div>
<div className="text-gray-400">{e.description}</div>
</div>
))}
</div>
))
})()}
</>
) : (
<div className="text-sm text-gray-400">...</div>
)}
</div>
</div>
)}
</div>
)}
{/* 导航按钮 */}
{step < 4 && (
<div className="flex justify-between mt-6">
<Button
variant="secondary"
onClick={() => setStep(Math.max(0, step - 1))}
disabled={step === 0}
>
<ChevronLeft className="w-4 h-4 mr-1" />
</Button>
{step < 3 ? (
<Button onClick={() => setStep(step + 1)} disabled={!canProceed()}>
<ChevronRight className="w-4 h-4 ml-1" />
</Button>
) : (
<Button onClick={handleSave} disabled={saveMutation.isPending}>
<Shield className="w-4 h-4 mr-1" />
</Button>
)}
</div>
)}
</Card>
</div>
)
}