feat: Phase 1-3 优化全部完成
Phase 1 紧急修复(8项): - 社保城市选择改为可输入 - 社保上下限拆分(三险/医保独立基数) - 公积金试算结果展示修复 - 花名册合同保存修复(日期ISO格式) - 薪酬批次创建失败修复(城市过滤+错误处理) - 证据链查看修复 - 个税计算修复(blank_employees读取基本工资) - 加班费倍率读取配置 Phase 2 功能完善(3项): - 批量导入per-row异常捕获+导入按钮 - 单人发薪UI入口优化 - 解除协议模板补充(员工提出离职版) Phase 3 后期规划(4项): - 工资表导入功能(POST /import/payroll + 前端入口) - 大病险/长护险附加险种(extraInsurances JSON + 计算适配) - 专项附加扣除按月录入(SpecialDeductionRecord模型 + 前端Tab) - 预置河北省社保政策(seed数据)
This commit is contained in:
@@ -4,6 +4,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { useConfirm } from '../hooks/useConfirm'
|
||||
import { Calculator, AlertCircle, Info, Check, Upload, Layers, Settings as SettingsIcon, Archive, Plus, Trash2, AlertTriangle, Download, FileText, X, ChevronLeft, Wallet, LayoutTemplate, Clock, Receipt, Users, TrendingDown, TrendingUp, BadgeCheck } from 'lucide-react'
|
||||
import api from '../lib/api'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
import Card from '../components/ui/Card'
|
||||
import Button from '../components/ui/Button'
|
||||
import { Input, Label, Select } from '../components/ui/Input'
|
||||
@@ -374,6 +375,8 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const [showAddEmployee, setShowAddEmployee] = useState(false)
|
||||
const payrollFileRef = useRef<HTMLInputElement>(null)
|
||||
const [payrollImportResult, setPayrollImportResult] = useState<any>(null)
|
||||
|
||||
const { data: batch, isLoading } = useQuery<any>({
|
||||
queryKey: ['batch-detail', batchId],
|
||||
@@ -423,6 +426,33 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
||||
},
|
||||
})
|
||||
|
||||
const importPayrollMutation = useMutation({
|
||||
mutationFn: async (file: File) => {
|
||||
const token = useAuthStore.getState().accessToken
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('batchId', batchId)
|
||||
const res = await fetch('/api/v1/import/payroll', {
|
||||
method: 'POST',
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
body: formData,
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!data.success) throw new Error(data.message || '导入失败')
|
||||
return data.data
|
||||
},
|
||||
onSuccess: (data: any) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['batch-detail'] })
|
||||
setPayrollImportResult(data)
|
||||
if (data.updated > 0) {
|
||||
toast.success(`成功更新 ${data.updated} 条工资记录`)
|
||||
} else {
|
||||
toast.info('未更新任何记录')
|
||||
}
|
||||
},
|
||||
onError: () => toast.error('工资表导入失败'),
|
||||
})
|
||||
|
||||
const deleteBatchMutation = useMutation({
|
||||
mutationFn: () => api.delete(`/payroll2/batches/${batchId}`),
|
||||
onSuccess: () => {
|
||||
@@ -546,6 +576,33 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
||||
<Button variant="secondary" size="sm" onClick={() => setShowAddEmployee(!showAddEmployee)}>
|
||||
<Plus className="w-4 h-4 mr-1" />添加人员
|
||||
</Button>
|
||||
<input
|
||||
ref={payrollFileRef}
|
||||
type="file"
|
||||
accept=".xlsx,.xls"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (file) importPayrollMutation.mutate(file)
|
||||
e.target.value = ''
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => payrollFileRef.current?.click()}
|
||||
disabled={importPayrollMutation.isPending}
|
||||
>
|
||||
<Upload className="w-4 h-4 mr-1" />
|
||||
{importPayrollMutation.isPending ? '导入中...' : '导入工资表'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => window.open('/api/v1/import/payroll-template', '_blank')}
|
||||
>
|
||||
<Download className="w-4 h-4 mr-1" />下载模板
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
@@ -636,6 +693,29 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
||||
<AddEmployeeToBatch batchId={batchId} onClose={() => setShowAddEmployee(false)} />
|
||||
)}
|
||||
|
||||
{/* 工资表导入结果 */}
|
||||
{payrollImportResult && (
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-xs font-medium">工资表导入结果</h3>
|
||||
<button onClick={() => setPayrollImportResult(null)} className="text-gray-500"><X className="w-4 h-4" /></button>
|
||||
</div>
|
||||
<div className="text-sm space-y-1">
|
||||
<div className="text-gray-600">总计 {payrollImportResult.total} 行,成功更新 {payrollImportResult.updated} 条</div>
|
||||
{payrollImportResult.errors?.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="text-warning text-xs font-medium">错误详情({payrollImportResult.errors.length}条):</div>
|
||||
<ul className="mt-1 space-y-0.5 text-xs text-danger max-h-40 overflow-y-auto">
|
||||
{payrollImportResult.errors.map((err: string, i: number) => (
|
||||
<li key={i}>{err}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 提示 */}
|
||||
{!isArchived && (
|
||||
<div className="text-xs text-gray-500 flex items-center gap-1">
|
||||
|
||||
Reference in New Issue
Block a user