import { useState, useRef } from "react" import { toast } from "sonner" import { useMutation, useQueryClient } from "@tanstack/react-query" import { employeeApi, esignApi } from '../../lib/api-services' import { useConfirm } from '../../hooks/useConfirm' import Card from "../../components/ui/Card" import Button from "../../components/ui/Button" import { Input, Label, Select } from "../../components/ui/Input" import { FileText, AlertTriangle, X, Paperclip, Trash2, Info, Download, PenTool } from "lucide-react" export default function ContractInfo({ employeeId, contracts, hireDate }: { employeeId: string; contracts: any[]; hireDate: string }) { const queryClient = useQueryClient() const confirm = useConfirm() const [showForm, setShowForm] = useState(false) const [form, setForm] = useState({ contractType: 'FIXED', signDate: '', startDate: '', endDate: '', contractYears: 3, probationMonths: 0, probationSalary: 0, signMethod: 'PAPER' as 'PAPER' | 'ELECTRONIC', attachmentUrl: '', attachments: [] as { name: string; url: string }[], electronicContractNo: '', electronicContractUrl: '' }) const contractFileRef = useRef(null) const [previewUrl, setPreviewUrl] = useState(null) const addContractMutation = useMutation({ mutationFn: async (data: any) => { const res = await employeeApi.addContract({ ...data, employeeId }) as any const contractId = res?.data?.id if (data.signMethod === 'ELECTRONIC' && contractId) { try { await esignApi.create({ contractId, employeeId, documentTitle: `${data.contractType === 'UNFIXED' ? '无固定期限' : '固定期限'}劳动合同`, remark: '合同创建时自动发起', scene: 'CONTRACT', }) toast.success('合同已保存,电子签署记录已创建') } catch { toast.success('合同已保存,电子签署记录创建失败(可稍后手动发起)') } } else { toast.success('合同已保存') } return res }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }); queryClient.invalidateQueries({ queryKey: ['esign-records'] }); setShowForm(false) }, }) const deleteContractMutation = useMutation({ mutationFn: (contractId: string) => employeeApi.removeContract(contractId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }); toast.success('合同已删除') }, }) const handleContractFileUpload = (e: React.ChangeEvent) => { const files = e.target.files if (!files || files.length === 0) return const allowedExts = ['.pdf', '.jpg', '.jpeg', '.png', '.heic', '.gif', '.bmp', '.webp', '.doc', '.docx', '.xls', '.xlsx', '.tiff', '.tif'] const maxSize = 10 * 1024 * 1024 for (const file of Array.from(files)) { const ext = file.name.toLowerCase().substring(file.name.lastIndexOf('.')) if (!allowedExts.includes(ext)) { toast.error(`不支持的文件格式: ${file.name}`) continue } if (file.size > maxSize) { toast.error(`文件过大: ${file.name}(最大 10MB)`) continue } const reader = new FileReader() reader.onload = (event) => { setForm(prev => ({ ...prev, attachments: [...prev.attachments, { name: file.name, url: event.target?.result as string }], })) } reader.readAsDataURL(file) } // 清空 input 以便重复选择同一文件 e.target.value = '' } const typeMap: Record = { FIXED: '固定期限', UNFIXED: '无固定期限', UNSIGNED: '未签订' } // 按劳动合同法自动判断续签和合同类型建议 const contractAdvice = (() => { if (!contracts?.length) return null const fixedContracts = contracts.filter((c: any) => c.contractType === 'FIXED') const latestContract = contracts[0] const isRenewal = !!latestContract?.endDate const renewalCount = (latestContract?.renewalCount || 0) // 连续订立二次固定期限劳动合同,第三次应订立无固定期限 const shouldUnfixed = fixedContracts.length >= 2 // 连续工作满十年 const yearsSinceHire = hireDate ? (Date.now() - new Date(hireDate).getTime()) / (365.25 * 24 * 60 * 60 * 1000) : 0 const shouldUnfixedByTenure = yearsSinceHire >= 10 if (shouldUnfixed || shouldUnfixedByTenure) { return { isRenewal, renewalCount: isRenewal ? renewalCount + 1 : renewalCount, suggestedType: 'UNFIXED', reason: shouldUnfixed ? `已连续签订${fixedContracts.length}次固定期限合同,按《劳动合同法》第十四条应订立无固定期限合同` : `连续工作满${Math.floor(yearsSinceHire)}年,按《劳动合同法》第十四条应订立无固定期限合同`, } } if (isRenewal) { return { isRenewal: true, renewalCount: renewalCount + 1, suggestedType: 'FIXED', reason: `本次为第${renewalCount + 1}次续签`, } } return null })() const handleShowForm = () => { if (contractAdvice?.suggestedType) { setForm({ ...form, contractType: contractAdvice.suggestedType, signDate: new Date().toISOString().slice(0, 10), startDate: contractAdvice.isRenewal && contracts[0]?.endDate ? contracts[0].endDate.toString().slice(0, 10) : new Date().toISOString().slice(0, 10), endDate: '', probationMonths: 0, probationSalary: 0, signMethod: 'PAPER', attachmentUrl: '', attachments: [], electronicContractNo: '', electronicContractUrl: '', }) } setShowForm(!showForm) } return (

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

{contractAdvice && !showForm && (
{contractAdvice.reason},建议选择「{contractAdvice.suggestedType === 'UNFIXED' ? '无固定期限' : '固定期限'}」
)} {showForm && ( {contractAdvice && (
{contractAdvice.reason}
)}
setForm({ ...form, signDate: e.target.value })} />
setForm({ ...form, startDate: e.target.value })} />
{form.contractType === 'FIXED' && (
setForm({ ...form, endDate: e.target.value })} />
)} {form.contractType === 'FIXED' && !contractAdvice?.isRenewal && ( <>
setForm({ ...form, probationMonths: parseInt(e.target.value) || 0 })} min={0} max={6} />
setForm({ ...form, probationSalary: parseInt(e.target.value) || 0 })} />
)}
{form.signMethod === 'PAPER' && (
{form.attachments.length > 0 && {form.attachments.length} 个文件}
{form.attachments.length > 0 && (
{form.attachments.map((att, idx) => (
))}
)}

支持 PDF、JPG、PNG、HEIC、GIF、BMP、WebP、TIFF、Word、Excel 等格式,每个文件最大 10MB

)} {form.signMethod === 'ELECTRONIC' && (
保存合同后将自动创建电子签署记录,对接易签宝后系统会自动生成签署链接并发送给员工。当前为框架预留阶段,签署记录创建后可在「电子签署」页面查看。
)}
)} {!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}
{c.signMethod === 'PAPER' && (
合同附件 {(() => { let atts: { name: string; url: string }[] = [] try { const parsed = JSON.parse(c.attachmentUrl) atts = Array.isArray(parsed) ? parsed : [{ name: '附件', url: c.attachmentUrl }] } catch { if (c.attachmentUrl) { const mime = c.attachmentUrl.match(/data:(.*?);/)?.[1] || '' const ext = mime.split('/')[1] || '文件' atts = [{ name: `附件.${ext}`, url: c.attachmentUrl }] } } if (atts.length === 0) return 未上传 return (
{atts.map((att, idx) => (
))}
) })()}
)} {c.signMethod === 'ELECTRONIC' && (
签署方式 电子签署(记录已创建,请在「电子签署」页面查看进度)
)}
))} {/* 附件预览弹窗 */} {previewUrl && (() => { // 将 data URL 转为 blob URL,解决浏览器阻止 iframe 加载 data URL 的问题 const dataToBlobUrl = (dataUrl: string) => { try { const arr = dataUrl.split(',') const mime = arr[0].match(/:(.*?);/)?.[1] || 'application/octet-stream' const bstr = atob(arr[1]) const u8 = new Uint8Array(bstr.length) for (let i = 0; i < bstr.length; i++) u8[i] = bstr.charCodeAt(i) return URL.createObjectURL(new Blob([u8], { type: mime })) } catch { return dataUrl } } const blobUrl = previewUrl.startsWith('data:') ? dataToBlobUrl(previewUrl) : previewUrl const mime = previewUrl.startsWith('data:') ? previewUrl.match(/data:(.*?);/)?.[1] || '' : '' const isImage = mime.startsWith('image/') const isPdf = mime === 'application/pdf' return (
{ if (blobUrl !== previewUrl) URL.revokeObjectURL(blobUrl); setPreviewUrl(null) }}>
e.stopPropagation()}>
附件预览
下载
{isImage ? ( 附件预览 ) : isPdf ? ( ) : (

此文件格式不支持在线预览

点击下载查看
)}
) })()}
) }