import { useState, useRef } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Users, FileText, AlertTriangle, Calendar, GraduationCap, TrendingUp, Scale, X, Plus, Paperclip, Trash2, Printer, Calculator, Shield, Info, Check } 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 Modal from '../components/ui/Modal' import Signal from '../components/ui/Signal' type DetailTab = 'basic' | 'contract' | 'payslip' | 'overtime' | 'disciplinary' | 'attendance' | 'training' | 'performance' | 'termination' | 'attachment' | 'evidence' export default function Roster() { const queryClient = useQueryClient() const [selectedId, setSelectedId] = useState(null) const [search, setSearch] = useState('') const [showAddModal, setShowAddModal] = useState(false) const { data: employees, isLoading } = useQuery({ queryKey: ['roster'], queryFn: async () => { const res = await api.get('/roster') as any return res.data }, }) const addMutation = useMutation({ mutationFn: (data: any) => api.post('/employees', data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster'] }) queryClient.invalidateQueries({ queryKey: ['dashboard'] }) setShowAddModal(false) }, }) const filtered = employees?.filter((e: any) => !search || e.name.includes(search) || e.department.includes(search) ) || [] if (selectedId) { return setSelectedId(null)} /> } return (

花名册

setSearch(e.target.value)} className="w-48" />
{isLoading ? (
加载中...
) : filtered.length === 0 ? (
暂无员工
) : (
{filtered.map((e: any) => ( setSelectedId(e.id)} > ))}
姓名 部门 状态 入职日期 月薪 合同状态 违纪 考勤 培训 绩效 工资条
{e.name} {e.department} {e.status === 'ACTIVE' ? '在职' : '离职'} {e.hireDate?.toString().slice(0, 10)} ¥{e.monthlySalary.toLocaleString()} {e.counts?.disciplinaryRecords ? ( {e.counts.disciplinaryRecords} ) : 0} {e.counts?.attendanceRecords || 0} {e.counts?.trainingRecords || 0} {e.counts?.performanceRecords || 0} {e.counts?.payslips || 0}
)} {showAddModal && ( setShowAddModal(false)} onSubmit={(data) => addMutation.mutate(data)} loading={addMutation.isPending} error={addMutation.error as any} /> )}
) } function EmployeeProfile({ employeeId, onBack }: { employeeId: string; onBack: () => void }) { const [tab, setTab] = useState('basic') const [showEvidence, setShowEvidence] = useState(false) const { data: profile, isLoading } = useQuery({ queryKey: ['roster-profile', employeeId], queryFn: async () => { const res = await api.get(`/roster/${employeeId}/profile`) as any return res.data }, }) const tabs: { key: DetailTab; label: string; icon: any }[] = [ { key: 'basic', label: '基本信息', icon: Users }, { key: 'contract', label: '劳动合同', icon: FileText }, { key: 'payslip', label: '工资条', icon: FileText }, { key: 'overtime', label: '加班记录', icon: Calendar }, { key: 'disciplinary', label: '违纪记录', icon: AlertTriangle }, { key: 'attendance', label: '考勤记录', icon: Calendar }, { key: 'training', label: '培训签收', icon: GraduationCap }, { key: 'performance', label: '绩效考核', icon: TrendingUp }, { key: 'termination', label: '解聘记录', icon: FileText }, { key: 'attachment', label: '附件管理', icon: Paperclip }, { key: 'evidence', label: '仲裁证据链', icon: Scale }, ] if (isLoading) return
加载中...
if (!profile) return
员工不存在
return (

{profile.name} - 完整档案

{profile.status === 'ACTIVE' ? '在职' : '离职'}
{tabs.map((t) => { const Icon = t.icon return ( ) })}
{tab === 'basic' && } {tab === 'contract' && } {tab === 'payslip' && } {tab === 'overtime' && } {tab === 'disciplinary' && } {tab === 'attendance' && } {tab === 'training' && } {tab === 'performance' && } {tab === 'termination' && } {tab === 'attachment' && } {tab === 'evidence' && }
) } function BasicInfo({ profile }: { profile: any }) { const fields = [ { label: '姓名', value: profile.name }, { label: '部门', value: profile.department }, { label: '性别', value: profile.gender || '未填写' }, { label: '手机号', value: profile.phone || '未填写' }, { label: '入职日期', value: profile.hireDate?.toString().slice(0, 10) }, { label: '月工资', value: `¥${profile.monthlySalary.toLocaleString()}` }, { label: '紧急联系人', value: profile.emergencyContact || '未填写' }, { label: '紧急联系电话', value: profile.emergencyPhone || '未填写' }, { label: '住址', value: profile.address || '未填写' }, { label: '开户行', value: profile.bankName || '未填写' }, { label: '银行账号', value: profile.bankAccount || '未填写' }, { label: '状态', value: profile.status === 'ACTIVE' ? '在职' : '离职' }, ] const special = [ { label: '孕期', value: profile.isPregnant }, { label: '医疗期', value: profile.isInMedicalPeriod }, { label: '工伤', value: profile.isWorkInjured }, ] return (
{fields.map((f) => (
{f.label} {f.value}
))}
{special.map((s) => ( {s.label}:{s.value ? '是' : '否'} ))}
) } function ContractInfo({ employeeId, contracts }: { employeeId: string; contracts: any[] }) { const queryClient = useQueryClient() const [showForm, setShowForm] = useState(false) const [form, setForm] = useState({ contractType: 'FIXED', signDate: '', startDate: '', endDate: '', contractYears: 3, probationMonths: 0, probationSalary: 0 }) const addContractMutation = useMutation({ mutationFn: (data: any) => api.post('/employees/contracts', { ...data, employeeId }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }); setShowForm(false) }, }) const typeMap: Record = { FIXED: '固定期限', UNFIXED: '无固定期限', UNSIGNED: '未签订' } return (

劳动合同({contracts?.length || 0}份)

{showForm && (
setForm({ ...form, signDate: e.target.value })} />
setForm({ ...form, startDate: e.target.value })} />
{form.contractType === 'FIXED' && (
setForm({ ...form, endDate: e.target.value })} />
)} {form.contractType === 'FIXED' && ( <>
setForm({ ...form, probationMonths: parseInt(e.target.value) || 0 })} min={0} max={6} />
setForm({ ...form, probationSalary: parseInt(e.target.value) || 0 })} />
)}
)} {!contracts?.length ? (
暂无合同记录
) : contracts.map((c) => (
合同类型{typeMap[c.contractType] || c.contractType}
签订日期{c.signDate ? c.signDate.toString().slice(0, 10) : '未签订'}
合同开始{c.startDate?.toString().slice(0, 10)}
合同结束{c.endDate ? c.endDate.toString().slice(0, 10) : '无固定期限'}
合同期限{c.contractYears}年
试用期{c.probationMonths}个月(¥{c.probationSalary})
签订方式{c.signMethod === 'PAPER' ? '纸质' : '电子'}
续签次数{c.renewalCount}
))}
) } function AddEmployeeModal({ onClose, onSubmit, loading, error }: { onClose: () => void onSubmit: (data: any) => void loading: boolean error: any }) { const [form, setForm] = useState({ name: '', department: '', hireDate: '', monthlySalary: '', gender: '男' as '男' | '女', phone: '', isPregnant: false, isInMedicalPeriod: false, isWorkInjured: false, contractType: 'FIXED' as 'FIXED' | 'UNFIXED' | 'UNSIGNED', signDate: '', startDate: '', endDate: '', contractYears: 3, probationMonths: 0, probationSalary: 0, }) const handleSubmit = () => { const data: any = { name: form.name, department: form.department, hireDate: new Date(form.hireDate).toISOString(), monthlySalary: form.monthlySalary, gender: form.gender, phone: form.phone || undefined, isPregnant: form.isPregnant, isInMedicalPeriod: form.isInMedicalPeriod, isWorkInjured: form.isWorkInjured, } if (form.contractType !== 'UNSIGNED' && form.startDate) { data.contract = { signDate: form.signDate ? new Date(form.signDate).toISOString() : null, startDate: new Date(form.startDate).toISOString(), endDate: form.endDate ? new Date(form.endDate).toISOString() : null, contractType: form.contractType, contractYears: form.contractYears, probationMonths: form.probationMonths, probationSalary: form.probationSalary, } } onSubmit(data) } return (
{error && (
{error.response?.data?.error?.message || '操作失败'}
)}
setForm({ ...form, name: e.target.value })} placeholder="员工姓名" />
setForm({ ...form, department: e.target.value })} placeholder="如:技术部" />
setForm({ ...form, hireDate: e.target.value })} />
setForm({ ...form, monthlySalary: e.target.value })} placeholder="元" />
setForm({ ...form, phone: e.target.value })} placeholder="选填" maxLength={11} />
{form.contractType !== 'UNSIGNED' && (
setForm({ ...form, signDate: e.target.value })} />
setForm({ ...form, startDate: e.target.value })} />
{form.contractType === 'FIXED' && (
setForm({ ...form, endDate: e.target.value })} />
setForm({ ...form, probationMonths: parseInt(e.target.value) || 0 })} min={0} max={6} />
setForm({ ...form, probationSalary: parseInt(e.target.value) || 0 })} />
)}
)}
) } function AttachmentInfo({ employeeId, attachments }: { employeeId: string; attachments: any[] }) { const queryClient = useQueryClient() const fileInputRef = useRef(null) const [fileType, setFileType] = useState<'ID_CARD' | 'BANK_CARD' | 'CONTRACT_SCAN' | 'EDUCATION' | 'OTHER'>('ID_CARD') const addAttachmentMutation = useMutation({ mutationFn: (data: any) => api.post('/attachments', data), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['roster-profile', employeeId] }), }) const deleteAttachmentMutation = useMutation({ mutationFn: (id: string) => api.delete(`/attachments/${id}`), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['roster-profile', employeeId] }), }) const handleFileUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (!file) return const reader = new FileReader() reader.onload = (event) => { const fileUrl = event.target?.result as string addAttachmentMutation.mutate({ employeeId, fileName: file.name, fileType, fileUrl, fileSize: file.size }) } reader.readAsDataURL(file) } const fileTypeLabels: Record = { ID_CARD: '身份证', BANK_CARD: '银行卡', CONTRACT_SCAN: '合同扫描件', EDUCATION: '学历证书', OTHER: '其他' } return (

附件管理({attachments?.length || 0}个)

{attachments?.length ? (
{attachments.map((att) => (
{att.fileName}
{fileTypeLabels[att.fileType] || att.fileType} · {new Date(att.createdAt).toLocaleDateString('zh-CN')}
))}
) :
暂无附件
}
) } function PayslipInfo({ payslips }: { payslips: any[] }) { if (!payslips?.length) return
暂无工资条记录
return ( {payslips.map((p) => ( ))}
月份 基本工资 加班费 津贴 扣款 应发合计 确认状态
{p.month} ¥{p.baseSalary.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ¥{p.overtimePay.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ¥{p.allowance.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} {p.deduction > 0 ? '-¥' : '¥'}{p.deduction.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ¥{p.totalPay.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} {p.confirmedAt ? 已确认 : 未确认}
) } function OvertimeInfo({ records }: { records: any[] }) { if (!records?.length) return
暂无加班记录
return ( {records.map((o) => ( ))}
月份 工作日(h) 休息日(h) 节假日(h) 加班费
{o.month} {o.weekdayHours} {o.weekendHours} {o.holidayHours} ¥{o.totalPay.toLocaleString(undefined, { maximumFractionDigits: 2 })}
) } function TerminationInfo({ employeeId, profile, records }: { employeeId: string; profile: any; records: any[] }) { const [printRecord, setPrintRecord] = useState(null) const reasonMap: Record = { NEGOTIATED: '协商解除', FAULT: '员工过错', NONFAULT: '非过错解除', LAYOFF: '经济性裁员', EXPIRED: '合同到期', ILLEGAL: '违法解除', } const legalBasisMap: Record = { NEGOTIATED: '《劳动合同法》第36条', FAULT: '《劳动合同法》第39条', NONFAULT: '《劳动合同法》第40条', LAYOFF: '《劳动合同法》第41条', EXPIRED: '《劳动合同法》第44条、第46条', ILLEGAL: '《劳动合同法》第87条', } 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: !!printRecord, }) if (!records?.length) return
暂无解聘记录
if (printRecord) { return (
{/* 1. 解聘通知书 */}

解除劳动合同通知书

{profile.name} 先生/女士:

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

解除依据:{legalBasisMap[printRecord.reason] || ''}

经济补偿金:¥{printRecord.compensation.toLocaleString(undefined, { maximumFractionDigits: 2 })}

{printRecord.remark &&

备注:{printRecord.remark}

}

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

公司(盖章)

{printRecord.terminationDate?.toString().slice(0, 10)}

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

费用结算明细

员工{profile.name}({profile.department})
入职日期{profile.hireDate?.toString().slice(0, 10)}
月工资¥{profile.monthlySalary.toLocaleString()}/月
解聘日期{printRecord.terminationDate?.toString().slice(0, 10)}
解聘原因{reasonMap[printRecord.reason] || printRecord.reason}
经济补偿金¥{printRecord.compensation.toLocaleString(undefined, { maximumFractionDigits: 2 })}
{/* 3. 合规检查清单 */} {printRecord.checklist && Object.keys(printRecord.checklist).length > 0 && (

合规检查清单

{Object.entries(printRecord.checklist).map(([key, passed]: [string, any]) => (
{passed ? '✓' : '✗'} {key}
))}
)} {/* 4. 风险评估 */} {printRecord.riskLevel && printRecord.riskLevel !== 'SAFE' && (

风险评估

风险等级:{printRecord.riskLevel === 'DANGER' ? '高风险' : '注意'}
)} {/* 5. 仲裁证据链 */}

仲裁证据链

{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}
{(items as any[]).map((e: any, i: number) => (
{e.title} {e.acknowledged === true && ✓已签} {e.acknowledged === false && ✗未签}
{e.description}
))}
)) })()} ) : (
加载证据链中...
)}
) } return (
{records.map((t) => (
解聘日期{t.terminationDate?.toString().slice(0, 10)}
解聘原因{reasonMap[t.reason] || t.reason}
经济补偿金¥{t.compensation.toLocaleString()}
风险等级{t.riskLevel === 'SAFE' ? '安全' : t.riskLevel === 'WARNING' ? '注意' : '高风险'}
{t.remark &&
备注:{t.remark}
}
))}
) } // ========== 违纪记录管理 ========== function DisciplinaryInfo({ employeeId, records }: { employeeId: string; records: any[] }) { const queryClient = useQueryClient() const [showForm, setShowForm] = useState(false) const [form, setForm] = useState({ violationDate: '', violationType: 'LATE', description: '', severity: 'WARNING', action: 'ORAL_WARNING', actionDetail: '', employeeAck: false, ackDate: '', ackMethod: 'SIGN', witness: '' }) const createMutation = useMutation({ mutationFn: (data: any) => api.post(`/roster/${employeeId}/disciplinary`, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }); setShowForm(false) }, }) const deleteMutation = useMutation({ mutationFn: (id: string) => api.delete(`/roster/${employeeId}/disciplinary/${id}`), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['roster-profile'] }), }) const typeMap: Record = { LATE: '迟到', ABSENT: '旷工', INSUBORDINATION: '不服从管理', MISCONDUCT: '违纪', VIOLATE_POLICY: '违反规章制度', OTHER: '其他' } const actionMap: Record = { ORAL_WARNING: '口头警告', WRITTEN_WARNING: '书面警告', DEDUCTION: '扣款', DEMOTION: '降职', TERMINATION: '解除劳动合同' } const severityMap: Record = { WARNING: '警告', SERIOUS: '严重', SEVERE: '重度' } return (

违纪记录({records?.length || 0}条)

{showForm && (
setForm({ ...form, violationDate: e.target.value })} />
setForm({ ...form, description: e.target.value })} placeholder="详细描述违纪事实" />
setForm({ ...form, actionDetail: e.target.value })} placeholder="扣款金额/降职说明等" />
setForm({ ...form, witness: e.target.value })} />
setForm({ ...form, employeeAck: e.target.checked })} />
{form.employeeAck &&
setForm({ ...form, ackDate: e.target.value })} />
}
)} {records?.length === 0 ? (
暂无违纪记录
) : records?.map((r) => (
{r.violationDate?.toString().slice(0, 10)} {typeMap[r.violationType] || r.violationType} {severityMap[r.severity] || r.severity}
{r.description}
处理:{actionMap[r.action] || r.action}{r.actionDetail ? `(${r.actionDetail})` : ''}
{r.employeeAck ? ✓ 员工已签字({r.ackDate?.toString().slice(0, 10)}) : ⚠ 员工未签字} {r.witness && 见证人:{r.witness}}
))}
) } // ========== 考勤记录管理 ========== function AttendanceInfo({ employeeId, records }: { employeeId: string; records: any[] }) { const queryClient = useQueryClient() const [showForm, setShowForm] = useState(false) const [form, setForm] = useState({ date: '', checkInTime: '', checkOutTime: '', status: 'NORMAL', lateMinutes: 0, earlyMinutes: 0, workHours: 8, overtimeHours: 0, remark: '' }) const createMutation = useMutation({ mutationFn: (data: any) => api.post(`/roster/${employeeId}/attendance`, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }); setShowForm(false) }, }) const deleteMutation = useMutation({ mutationFn: (id: string) => api.delete(`/roster/${employeeId}/attendance/${id}`), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['roster-profile'] }), }) const statusMap: Record = { NORMAL: '正常', LATE: '迟到', EARLY_LEAVE: '早退', ABSENT: '旷工', LEAVE: '请假', BUSINESS_TRIP: '出差' } const statusColor: Record = { NORMAL: 'bg-green-50 text-safe', LATE: 'bg-amber-50 text-warning', EARLY_LEAVE: 'bg-amber-50 text-warning', ABSENT: 'bg-red-50 text-danger', LEAVE: 'bg-blue-50 text-blue-600', BUSINESS_TRIP: 'bg-blue-50 text-blue-600' } return (

考勤记录({records?.length || 0}条)

{showForm && (
setForm({ ...form, date: e.target.value })} />
setForm({ ...form, checkInTime: e.target.value })} />
setForm({ ...form, checkOutTime: e.target.value })} />
setForm({ ...form, lateMinutes: Number(e.target.value) })} />
setForm({ ...form, earlyMinutes: Number(e.target.value) })} />
setForm({ ...form, workHours: Number(e.target.value) })} />
setForm({ ...form, overtimeHours: Number(e.target.value) })} />
setForm({ ...form, remark: e.target.value })} />
)} {records?.length === 0 ? (
暂无考勤记录
) : ( {records?.map((a) => ( ))}
日期 签到 签退 状态 工时 加班
{a.date?.toString().slice(0, 10)} {a.checkInTime || '-'} {a.checkOutTime || '-'} {statusMap[a.status] || a.status} {a.workHours}h {a.overtimeHours > 0 ? `${a.overtimeHours}h` : '-'}
)}
) } // ========== 培训签收记录管理 ========== function TrainingInfo({ employeeId, records }: { employeeId: string; records: any[] }) { const queryClient = useQueryClient() const [showForm, setShowForm] = useState(false) const [form, setForm] = useState({ trainingDate: '', topic: '', content: '', trainer: '', duration: 1, ackStatus: 'PENDING', ackDate: '', remark: '' }) const createMutation = useMutation({ mutationFn: (data: any) => api.post(`/roster/${employeeId}/training`, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }); setShowForm(false) }, }) const deleteMutation = useMutation({ mutationFn: (id: string) => api.delete(`/roster/${employeeId}/training/${id}`), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['roster-profile'] }), }) const ackMap: Record = { PENDING: '待签收', SIGNED: '已签收', REFUSED: '拒绝签收' } const ackColor: Record = { PENDING: 'bg-amber-50 text-warning', SIGNED: 'bg-green-50 text-safe', REFUSED: 'bg-red-50 text-danger' } return (

培训签收记录({records?.length || 0}条)

{showForm && (
setForm({ ...form, trainingDate: e.target.value })} />
setForm({ ...form, topic: e.target.value })} placeholder="如《员工手册》培训" />
setForm({ ...form, content: e.target.value })} />
setForm({ ...form, trainer: e.target.value })} />
setForm({ ...form, duration: Number(e.target.value) })} />
{form.ackStatus === 'SIGNED' &&
setForm({ ...form, ackDate: e.target.value })} />
}
setForm({ ...form, remark: e.target.value })} />
)} {records?.length === 0 ? (
暂无培训签收记录
) : records?.map((r) => (
{r.trainingDate?.toString().slice(0, 10)} {r.topic} {ackMap[r.ackStatus] || r.ackStatus}
{r.content &&
{r.content}
}
时长:{r.duration}h {r.trainer && 培训人:{r.trainer}} {r.ackDate && 签收日期:{r.ackDate.toString().slice(0, 10)}}
))}
) } // ========== 绩效记录管理 ========== function PerformanceInfo({ employeeId, records }: { employeeId: string; records: any[] }) { const queryClient = useQueryClient() const [showForm, setShowForm] = useState(false) const [form, setForm] = useState({ period: '', score: 80, grade: 'B', result: 'QUALIFIED', summary: '', improvementPlan: '', employeeAck: false, ackDate: '', reviewer: '' }) const createMutation = useMutation({ mutationFn: (data: any) => api.post(`/roster/${employeeId}/performance`, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }); setShowForm(false) }, }) const deleteMutation = useMutation({ mutationFn: (id: string) => api.delete(`/roster/${employeeId}/performance/${id}`), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['roster-profile'] }), }) const resultMap: Record = { EXCELLENT: '优秀', QUALIFIED: '合格', NEED_IMPROVE: '需改进', UNQUALIFIED: '不胜任' } const resultColor: Record = { EXCELLENT: 'bg-green-50 text-safe', QUALIFIED: 'bg-blue-50 text-blue-600', NEED_IMPROVE: 'bg-amber-50 text-warning', UNQUALIFIED: 'bg-red-50 text-danger' } return (

绩效考核记录({records?.length || 0}条)

{showForm && (
setForm({ ...form, period: e.target.value })} placeholder="如 2026-07 或 2026-Q3" />
setForm({ ...form, score: Number(e.target.value) })} />
setForm({ ...form, summary: e.target.value })} />
setForm({ ...form, improvementPlan: e.target.value })} placeholder="如:调岗至XX岗位,培训XX技能" />
setForm({ ...form, reviewer: e.target.value })} />
setForm({ ...form, employeeAck: e.target.checked })} />
{form.employeeAck &&
setForm({ ...form, ackDate: e.target.value })} />
}
)} {records?.length === 0 ? (
暂无绩效记录
) : records?.map((r) => (
{r.period} 得分:{r.score}({r.grade}) {resultMap[r.result] || r.result}
{r.summary &&
{r.summary}
} {r.improvementPlan &&
改进计划:{r.improvementPlan}
}
{r.employeeAck ? ✓ 员工已签字({r.ackDate?.toString().slice(0, 10)}) : ⚠ 员工未签字} {r.reviewer && 考核人:{r.reviewer}}
))}
) } // ========== 仲裁证据链 ========== function EvidenceChain({ employeeId }: { employeeId: string }) { const { data, isLoading } = useQuery({ queryKey: ['evidence-chain', employeeId], queryFn: async () => { const res = await api.get(`/roster/${employeeId}/evidence-chain`) as any return res.data }, }) if (isLoading) return
生成证据链中...
if (!data) return
无数据
const categoryColor: Record = { '劳动关系': 'bg-blue-50 text-blue-700 border-blue-200', '薪酬发放': 'bg-green-50 text-green-700 border-green-200', '考勤记录': 'bg-amber-50 text-amber-700 border-amber-200', '违纪处理': 'bg-red-50 text-red-700 border-red-200', '培训签收': 'bg-purple-50 text-purple-700 border-purple-200', '绩效考核': 'bg-indigo-50 text-indigo-700 border-indigo-200', '解聘记录': 'bg-gray-100 text-gray-700 border-gray-300', } const handleExport = () => { const text = generateEvidenceText(data) const blob = new Blob([text], { type: 'text/plain;charset=utf-8' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `仲裁证据链_${data.employee.name}_${new Date().toISOString().slice(0, 10)}.txt` a.click() URL.revokeObjectURL(url) } return (

仲裁证据链

{data.employee.name} · {data.employee.department} · 入职{data.employee.hireDate}
证据总数
{data.summary.total}
已签字
{data.summary.signed}
未签字
{data.summary.unsigned}
{data.evidence.map((e: any, i: number) => (
{e.category}
{e.title} {e.date} {e.acknowledged === true && ✓ 已签字} {e.acknowledged === false && ⚠ 未签字}
{e.description}
))}
) } function generateEvidenceText(data: any): string { const lines: string[] = [] lines.push('========================================') lines.push(' 劳动仲裁证据链') lines.push('========================================') lines.push('') lines.push(`员工姓名:${data.employee.name}`) lines.push(`部门:${data.employee.department}`) lines.push(`入职日期:${data.employee.hireDate}`) lines.push(`状态:${data.employee.status === 'ACTIVE' ? '在职' : '离职'}`) lines.push('') lines.push(`证据总数:${data.summary.total} 条`) lines.push(`已签字:${data.summary.signed} 条`) lines.push(`未签字:${data.summary.unsigned} 条`) lines.push('') lines.push('----------------------------------------') lines.push('') let currentCategory = '' data.evidence.forEach((e: any, i: number) => { if (e.category !== currentCategory) { currentCategory = e.category lines.push(`【${currentCategory}】`) lines.push('') } lines.push(`${i + 1}. ${e.title}(${e.date})`) lines.push(` ${e.description}`) if (e.acknowledged === true) lines.push(' [已签字确认]') if (e.acknowledged === false) lines.push(' [未签字]') lines.push('') }) lines.push('----------------------------------------') lines.push(`导出时间:${new Date().toLocaleString('zh-CN')}`) return lines.join('\n') }