feat: 社保AI建议、合同附件多文件上传预览、删除合同、扩展上传格式

This commit is contained in:
freedakgmail
2026-07-27 23:45:22 +08:00
parent 71d7ab2de5
commit 5e22a82163
21 changed files with 1246 additions and 178 deletions
+78 -8
View File
@@ -117,7 +117,7 @@ function BatchManager() {
setShowCreateModal(false)
toast.success('发薪批次已创建')
},
onError: () => toast.error('创建失败'),
onError: (err: any) => toast.error(err?.response?.data?.error?.message || '创建失败'),
})
const deleteBatchMutation = useMutation({
@@ -140,11 +140,26 @@ function BatchManager() {
onError: () => toast.error('重命名失败'),
})
const unarchiveBatchMutation = useMutation({
mutationFn: (batchId: string) => api.post(`/payroll2/batches/${batchId}/unarchive`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['batches'] })
queryClient.invalidateQueries({ queryKey: ['batch-check'] })
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
toast.success('已取消归档,批次恢复为草稿状态')
},
onError: (err: any) => toast.error(err?.response?.data?.error?.message || '取消归档失败'),
})
const [renamingId, setRenamingId] = useState<string | null>(null)
const [renameValue, setRenameValue] = useState('')
if (selectedBatchId) {
return <BatchDetail batchId={selectedBatchId} onBack={() => setSelectedBatchId(null)} />
return <BatchDetail batchId={selectedBatchId} onBack={() => {
queryClient.invalidateQueries({ queryKey: ['batches'] })
queryClient.invalidateQueries({ queryKey: ['batch-check'] })
setSelectedBatchId(null)
}} />
}
return (
@@ -190,7 +205,14 @@ function BatchManager() {
<span className="text-xs text-gray-500 whitespace-nowrap shrink-0">{batches.length} </span>
)}
<div className="flex-1" />
<Button onClick={() => { setCreateType('REGULAR'); setShowCreateModal(true) }} className="shrink-0">
<Button onClick={() => {
const hasDraft = batches?.some((b: any) => b.status === 'DRAFT' && b.month === month)
if (hasDraft) {
toast.error('当月存在未归档的批次,请先归档后再创建新批次')
return
}
setCreateType('REGULAR'); setShowCreateModal(true)
}} className="shrink-0">
<Plus className="w-4 h-4 mr-1" />
</Button>
</div>
@@ -353,6 +375,21 @@ function BatchManager() {
</button>
</>
)}
{batch.status === 'ARCHIVED' && (
<button
onClick={async () => {
if (await confirm({ title: '取消归档', message: `确认取消归档「${batch.name}」?取消后批次恢复为草稿状态,对应的工资条和个税累计将被撤销。`, variant: 'primary' })) {
unarchiveBatchMutation.mutate(batch.id)
}
}}
disabled={unarchiveBatchMutation.isPending}
className="p-1 rounded hover:bg-amber-50 text-gray-500 hover:text-warning transition-colors"
aria-label="取消归档"
title="取消归档"
>
<Archive className="w-3.5 h-3.5" />
</button>
)}
</div>
</td>
</tr>
@@ -391,12 +428,16 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
api.put(`/payroll2/batches/${batchId}/entries/${employeeId}`, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['batch-detail'] })
queryClient.invalidateQueries({ queryKey: ['batches'] })
},
})
const removeEmployeeMutation = useMutation({
mutationFn: (employeeId: string) => api.delete(`/payroll2/batches/${batchId}/employees/${employeeId}`),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['batch-detail'] }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['batch-detail'] })
queryClient.invalidateQueries({ queryKey: ['batches'] })
},
})
const archiveMutation = useMutation({
@@ -410,10 +451,23 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
},
})
const unarchiveMutation = useMutation({
mutationFn: () => api.post(`/payroll2/batches/${batchId}/unarchive`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['batch-detail'] })
queryClient.invalidateQueries({ queryKey: ['batches'] })
queryClient.invalidateQueries({ queryKey: ['batch-check'] })
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
toast.success('已取消归档,批次恢复为草稿状态')
},
onError: (err: any) => toast.error(err?.response?.data?.error?.message || '取消归档失败'),
})
const importOvertimeMutation = useMutation({
mutationFn: () => api.post(`/payroll/overtime/import-to-batch/${batchId}`),
onSuccess: (res: any) => {
queryClient.invalidateQueries({ queryKey: ['batch-detail'] })
queryClient.invalidateQueries({ queryKey: ['batches'] })
queryClient.invalidateQueries({ queryKey: ['overtime-records'] })
if (res.data?.imported > 0) {
toast.success(`成功导入 ${res.data.imported} 条加班费记录`)
@@ -443,6 +497,7 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
},
onSuccess: (data: any) => {
queryClient.invalidateQueries({ queryKey: ['batch-detail'] })
queryClient.invalidateQueries({ queryKey: ['batches'] })
setPayrollImportResult(data)
if (data.updated > 0) {
toast.success(`成功更新 ${data.updated} 条工资记录`)
@@ -640,11 +695,25 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
</div>
)}
{isArchived && (
<a href={`/api/v1/payroll2/batches/${batchId}/export?format=csv`} download>
<Button variant="secondary" size="sm">
<Download className="w-4 h-4 mr-1" />
<div className="flex gap-2">
<a href={`/api/v1/payroll2/batches/${batchId}/export?format=csv`} download>
<Button variant="secondary" size="sm">
<Download className="w-4 h-4 mr-1" />
</Button>
</a>
<Button
variant="secondary"
size="sm"
onClick={async () => {
if (await confirm({ title: '取消归档', message: '确认取消归档?取消后批次恢复为草稿状态,对应的工资条和个税累计将被撤销。', variant: 'primary' })) {
unarchiveMutation.mutate()
}
}}
disabled={unarchiveMutation.isPending}
>
{unarchiveMutation.isPending ? '取消中...' : '取消归档'}
</Button>
</a>
</div>
)}
</div>
@@ -816,6 +885,7 @@ function AddEmployeeToBatch({ batchId, onClose }: { batchId: string; onClose: ()
mutationFn: (employeeIds: string[]) => api.post(`/payroll2/batches/${batchId}/employees`, { employeeIds }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['batch-detail'] })
queryClient.invalidateQueries({ queryKey: ['batches'] })
onClose()
},
})