import { useState, useRef } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Plus, Search, Paperclip, Trash2, X, FileText } 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 EmptyState from '../components/ui/EmptyState' interface EmployeeItem { id: string name: string department: string hireDate: string status: string contractStatus: string contractStatusText: string riskLevel: 'high' | 'medium' | 'low' | 'safe' isPregnant: boolean isInMedicalPeriod: boolean isWorkInjured: boolean } interface EmployeeListResponse { items: EmployeeItem[] total: number page: number pageSize: number totalPages: number } export default function Contracts() { const queryClient = useQueryClient() const [search, setSearch] = useState('') const [page, setPage] = useState(1) const [showAddModal, setShowAddModal] = useState(false) const [selectedEmpId, setSelectedEmpId] = useState(null) const { data, isLoading } = useQuery({ queryKey: ['employees', search, page], queryFn: async () => { const res = await api.get('/employees', { params: { search, page, pageSize: 20 } }) as any return res.data }, }) const addMutation = useMutation({ mutationFn: (data: any) => api.post('/employees', data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['employees'] }) queryClient.invalidateQueries({ queryKey: ['dashboard'] }) setShowAddModal(false) }, }) return (

合同管理

集中管理劳动合同、签署状态及到期风险

{/* 搜索栏 */}
{ setSearch(e.target.value); setPage(1) }} className="pl-9" />
{/* 员工列表 */} {isLoading ? (
加载中...
) : !data || data.items.length === 0 ? ( setShowAddModal(true)} /> ) : ( <>
{data.items.map((emp) => ( setSelectedEmpId(emp.id)}> ))}
姓名 部门 入职日期 合同状态 特殊状态
{emp.name} {emp.department} {emp.hireDate} {(() => { const tagStyles: Record = { expired: 'bg-red-50 text-danger', unsigned_over_year: 'bg-red-50 text-danger', unsigned_over_30: 'bg-red-50 text-danger', unsigned: 'bg-yellow-50 text-yellow-700', expiring: 'bg-yellow-50 text-yellow-700', active: 'bg-green-50 text-safe', unfixed: 'bg-blue-50 text-blue-700', } const style = tagStyles[emp.contractStatus] || 'bg-gray-100 text-gray-600' return {emp.contractStatusText} })()}
{emp.isPregnant && 孕期} {emp.isInMedicalPeriod && 医疗期} {emp.isWorkInjured && 工伤}
{/* 分页 */} {data.totalPages > 1 && (
{page} / {data.totalPages}
)} )}
{/* 添加员工 Modal */} setShowAddModal(false)} onSubmit={(data) => addMutation.mutate(data)} loading={addMutation.isPending} error={addMutation.error as any} /> {/* 员工详情抽屉 */} {selectedEmpId && ( setSelectedEmpId(null)} /> )}
) } function AddEmployeeModal({ open, onClose, onSubmit, loading, error }: { open: boolean 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 EmployeeDetailDrawer({ employeeId, onClose }: { employeeId: string; onClose: () => void }) { const queryClient = useQueryClient() const fileInputRef = useRef(null) const [fileType, setFileType] = useState<'ID_CARD' | 'BANK_CARD' | 'CONTRACT_SCAN' | 'EDUCATION' | 'OTHER'>('ID_CARD') const { data: employee } = useQuery({ queryKey: ['employee-detail', employeeId], queryFn: async () => { const res = await api.get(`/employees/${employeeId}`) as any return res.data }, }) const { data: attachments } = useQuery({ queryKey: ['employee-attachments', employeeId], queryFn: async () => { const res = await api.get(`/attachments/${employeeId}`) as any return res.data }, }) const addAttachmentMutation = useMutation({ mutationFn: (data: any) => api.post('/attachments', data), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['employee-attachments', employeeId] }), }) const deleteAttachmentMutation = useMutation({ mutationFn: (id: string) => api.delete(`/attachments/${id}`), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['employee-attachments', 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: '其他', } const emp = employee?.data || employee return (
{emp && ( <>

{emp.name}

{emp.department}
入职日期:{emp.hireDate?.slice(0, 10)}
性别:{emp.gender || '-'}
手机:{emp.phone || '-'}
状态:{emp.status === 'ACTIVE' ? '在职' : '离职'}
{(emp.isPregnant || emp.isInMedicalPeriod || emp.isWorkInjured) && (
{emp.isPregnant && 孕期} {emp.isInMedicalPeriod && 医疗期} {emp.isWorkInjured && 工伤}
)}
{emp.contracts && emp.contracts.length > 0 && (

合同信息

{emp.contracts.map((c: any) => (
{(() => { const typeLabel = c.contractType === 'FIXED' ? '固定期限' : c.contractType === 'UNFIXED' ? '无固定期限' : '未签' const typeStyle = c.contractType === 'UNSIGNED' ? 'bg-red-50 text-danger' : 'bg-blue-50 text-blue-700' return {typeLabel} })()}
{c.startDate?.slice(0, 10)} ~ {c.endDate?.slice(0, 10) || '无固定期限'}
))}
)} )}

附件管理

{attachments && attachments.length > 0 ? (
{attachments.map((att: any) => (
{att.fileName}
{fileTypeLabels[att.fileType] || att.fileType} · {new Date(att.createdAt).toLocaleDateString('zh-CN')}
))}
) : (
暂无附件
)}
) }