import { useState, useRef } from 'react' import { usePageSize } from '../../hooks/usePageSize' import { toast } from 'sonner' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useConfirm } from '../../hooks/useConfirm' import { Calculator, AlertCircle, Info, Upload, Layers, Settings as SettingsIcon, Archive, Plus, Trash2, AlertTriangle, Download, FileText, X, ChevronLeft, Clock, Users, TrendingDown, TrendingUp, BadgeCheck } from 'lucide-react' import { Stepper, type Step } from '../../components/ui/Stepper' import { InlineAlert } from '../../components/ui/InlineAlert' import PageGuide from '../../components/ui/PageGuide' import { payrollApi, rosterApi, employeeApi } from '../../lib/api-services' 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' import EmptyState from '../../components/ui/EmptyState' import Pagination from '../../components/ui/Pagination' // 金额格式化:保留两位小数 + 千分位 const fmt = (n: number) => (n || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) // ========== 发薪批次管理 ========== function CustomEmployeeSelector({ selectedIds, onChange }: { selectedIds: string[]; onChange: (ids: string[]) => void }) { const [search, setSearch] = useState('') const [filterDept, setFilterDept] = useState('') const { data: employees } = useQuery({ queryKey: ['roster-for-batch', search, filterDept], queryFn: async () => { const res = await rosterApi.list({ pageSize: 999, search, department: filterDept, status: 'ACTIVE' }) return res.data || [] }, }) const { data: deptList } = useQuery({ queryKey: ['roster-departments'], queryFn: () => rosterApi.departments(), }) const toggle = (id: string) => { if (selectedIds.includes(id)) { onChange(selectedIds.filter(x => x !== id)) } else { onChange([...selectedIds, id]) } } const toggleAll = () => { if (employees && employees.every((e: any) => selectedIds.includes(e.id))) { onChange(selectedIds.filter(id => !employees.some((e: any) => e.id === id))) } else { const newIds = new Set([...selectedIds, ...(employees?.map((e: any) => e.id) || [])]) onChange(Array.from(newIds)) } } return (
已选 {selectedIds.length} 人
setSearch(e.target.value)} className="!w-40" /> {employees && employees.length > 0 && ( )}
{employees && employees.length > 0 ? ( {employees.map((e: any) => ( toggle(e.id)}> ))}
toggle(e.id)} /> {e.name} {e.department} ¥{fmt(e.monthlySalary)}
) : (
暂无员工
)}
) } export function BatchManager() { const queryClient = useQueryClient() const confirm = useConfirm() const [month, setMonth] = useState(new Date().toISOString().slice(0, 7)) const [monthFrom, setMonthFrom] = useState('') const [monthTo, setMonthTo] = useState('') const [dateFrom, setDateFrom] = useState('') const [dateTo, setDateTo] = useState('') const [filterStatus, setFilterStatus] = useState('') const [filterType, setFilterType] = useState('') const [selectedBatchId, setSelectedBatchId] = useState(null) const [showCreateModal, setShowCreateModal] = useState(false) const [createType, setCreateType] = useState<'REGULAR' | 'TERMINATION' | 'BONUS' | 'SEVERANCE'>('REGULAR') const [createMode, setCreateMode] = useState<'copy_last' | 'blank_employees' | 'blank_all' | 'copy_batch' | 'custom'>('copy_last') const [sourceBatchId, setSourceBatchId] = useState('') const [selectedEmployeeIds, setSelectedEmployeeIds] = useState([]) const pageSize = usePageSize() const [page, setPage] = useState(1) const { data: checkResult } = useQuery({ queryKey: ['batch-check', month], queryFn: async () => { return await payrollApi.batchCheck(month) }, }) const { data: batches, isLoading } = useQuery({ queryKey: ['batches', month, monthFrom, monthTo, dateFrom, dateTo, filterStatus, filterType], queryFn: async () => { const params: any = {} if (month && !monthFrom && !monthTo && !dateFrom && !dateTo) params.month = month if (monthFrom) params.monthFrom = monthFrom if (monthTo) params.monthTo = monthTo if (dateFrom) params.dateFrom = dateFrom if (dateTo) params.dateTo = dateTo if (filterStatus) params.status = filterStatus if (filterType) params.type = filterType return await payrollApi.batches(params) }, }) const { data: archivedBatches } = useQuery({ queryKey: ['archived-batches'], queryFn: async () => { return await payrollApi.archivedBatches() }, enabled: createMode === 'copy_batch', }) const createMutation = useMutation({ mutationFn: (data: any) => payrollApi.createBatch(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['batches'] }) queryClient.invalidateQueries({ queryKey: ['batch-check'] }) setShowCreateModal(false) toast.success('发薪批次已创建') }, onError: (err: any) => toast.error(err?.response?.data?.error?.message || '创建失败'), }) const deleteBatchMutation = useMutation({ mutationFn: (batchId: string) => payrollApi.removeBatch(batchId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['batches'] }) queryClient.invalidateQueries({ queryKey: ['batch-check'] }) toast.success('批次已删除') }, onError: () => toast.error('删除失败'), }) const renameBatchMutation = useMutation({ mutationFn: ({ batchId, name }: { batchId: string; name: string }) => payrollApi.renameBatch(batchId, name), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['batches'] }) toast.success('批次名称已更新') }, onError: () => toast.error('重命名失败'), }) const unarchiveBatchMutation = useMutation({ mutationFn: (batchId: string) => payrollApi.unarchiveBatch(batchId), 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(null) const [renameValue, setRenameValue] = useState('') if (selectedBatchId) { return { queryClient.invalidateQueries({ queryKey: ['batches'] }) queryClient.invalidateQueries({ queryKey: ['batch-check'] }) setSelectedBatchId(null) }} /> } return (
发薪批次用于按月生成工资数据。流程:①选择月份和发薪类型(常规/解聘/奖金/补偿)→ ②选择员工范围或复制上月 → ③系统自动计算社保、公积金、个税 → ④确认并发布工资条。支持批量编辑、导出Excel。 {/* 重复发薪提醒 */} {checkResult?.hasArchivedBatch && (
本月已有 {checkResult.archivedCount} 个已归档批次{checkResult.payslipsPublished ? ',工资条已发布' : ''}
)}
{ setMonthFrom(e.target.value); setMonth('') }} className="!w-36" />
{ setMonthTo(e.target.value); setMonth('') }} className="!w-36" />
{!monthFrom && !monthTo && !dateFrom && !dateTo && ( setMonth(e.target.value)} className="!w-36 shrink-0" placeholder="单月" /> )}
创建日期 { setDateFrom(e.target.value); setMonth('') }} className="!w-36" placeholder="起始" /> ~ { setDateTo(e.target.value); setMonth('') }} className="!w-36" placeholder="截止" />
{(monthFrom || monthTo || dateFrom || dateTo || filterStatus || filterType) && ( )} {batches && batches.length > 0 && ( {batches.length} 个批次 )}
{showCreateModal && (

创建发薪批次

{createMode === 'copy_batch' && (
{!archivedBatches?.length && (

暂无可复制的已归档批次

)}
)} {createMode === 'custom' && ( )}
{createMode === 'copy_last' &&

拉入在职员工,从上月工资条复制基本工资/津贴/扣款,自动计算社保个税

} {createMode === 'blank_employees' &&

拉入在职员工,所有金额为0,需手动填写

} {createMode === 'blank_all' &&

创建空批次,不拉入员工,后续手动添加人员和填写数据

} {createMode === 'copy_batch' &&

从指定的已归档批次复制员工和薪资数据

} {createMode === 'custom' &&

按部门筛选、搜索、勾选指定员工创建批次。适合为部分人员单独发薪或离职结算

}
)} {isLoading ? (
加载中...
) : !batches || batches.length === 0 ? ( ) : (
{batches.slice((page - 1) * pageSize, page * pageSize).map((batch: any) => ( setSelectedBatchId(batch.id)} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') setSelectedBatchId(batch.id) }}> ))}
批次名称 类型 人数 应发合计 社保合计 公积金合计 个税合计 实发合计 创建时间 状态 操作
{renamingId === batch.id ? ( setRenameValue(e.target.value)} onClick={(e) => e.stopPropagation()} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault() if (renameValue.trim() && renameValue !== batch.name) { renameBatchMutation.mutate({ batchId: batch.id, name: renameValue.trim() }) } setRenamingId(null) } if (e.key === 'Escape') setRenamingId(null) }} onBlur={() => { if (renameValue.trim() && renameValue !== batch.name) { renameBatchMutation.mutate({ batchId: batch.id, name: renameValue.trim() }) } setRenamingId(null) }} className="text-sm border-b border-primary bg-transparent px-1 py-0.5 focus:outline-none" /> ) : ( {batch.name} )}
{batch.type === 'REGULAR' ? '常规发薪' : batch.type === 'BONUS' ? '年终奖/奖金' : batch.type === 'TERMINATION' ? '离职结算' : batch.type === 'SEVERANCE' ? '补偿金' : batch.type} {batch.employeeCount} ¥{fmt(batch.totalPay)} ¥{fmt((batch.totalSocialOrg || 0) + (batch.totalSocialEmp || 0))} ¥{fmt((batch.totalHousingOrg || 0) + (batch.totalHousingEmp || 0))} ¥{fmt(batch.totalTax)} ¥{fmt(batch.totalNetPay)} {batch.createdAt ? new Date(batch.createdAt).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '-'} {batch.status === 'ARCHIVED' ? ( 已归档 ) : ( 草稿 )}
e.stopPropagation()}> {batch.status !== 'ARCHIVED' && ( <> )} {batch.status === 'ARCHIVED' && ( )}
setPage(1)} />
)}
) } function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void }) { const queryClient = useQueryClient() const confirm = useConfirm() const [editCell, setEditCell] = useState<{ employeeId: string; field: string } | null>(null) const [editValue, setEditValue] = useState('') const pageSize = usePageSize() const [page, setPage] = useState(1) const [showAddEmployee, setShowAddEmployee] = useState(false) const payrollFileRef = useRef(null) const [payrollImportResult, setPayrollImportResult] = useState(null) const [taxDetailFor, setTaxDetailFor] = useState<{ employeeId: string; name: string } | null>(null) const [taxDetailData, setTaxDetailData] = useState(null) const [taxDetailLoading, setTaxDetailLoading] = useState(false) const { data: batch, isLoading } = useQuery({ queryKey: ['batch-detail', batchId], queryFn: async () => { return await payrollApi.batchDetail(batchId) }, }) const updateEntryMutation = useMutation({ mutationFn: ({ employeeId, data }: { employeeId: string; data: any }) => payrollApi.updateBatchEntry(batchId, employeeId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['batch-detail'] }) queryClient.invalidateQueries({ queryKey: ['batches'] }) }, onError: (err: any) => { toast.error(err?.response?.data?.error?.message || '保存失败,请重试') }, }) const removeEmployeeMutation = useMutation({ mutationFn: (employeeId: string) => payrollApi.removeBatchEmployee(batchId, employeeId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['batch-detail'] }) queryClient.invalidateQueries({ queryKey: ['batches'] }) }, }) const archiveMutation = useMutation({ mutationFn: () => payrollApi.archiveBatch(batchId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['batch-detail'] }) queryClient.invalidateQueries({ queryKey: ['batches'] }) queryClient.invalidateQueries({ queryKey: ['batch-check'] }) queryClient.invalidateQueries({ queryKey: ['dashboard'] }) toast.success('批次已归档。归档后批次锁定不可编辑。可前往「工资条管理」生成工资条。') }, }) const unarchiveMutation = useMutation({ mutationFn: () => payrollApi.unarchiveBatch(batchId), 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 publishPayslipMutation = useMutation({ mutationFn: () => payrollApi.publishPayslip(batchId), onSuccess: (res: any) => { toast.success(`已发布 ${res.data?.published || 0} 条工资条`) queryClient.invalidateQueries({ queryKey: ['batch-detail'] }) }, onError: (err: any) => toast.error(err?.response?.data?.error?.message || '发布失败'), }) const [showScheduleModal, setShowScheduleModal] = useState(false) const [scheduleDate, setScheduleDate] = useState('') const schedulePayslipMutation = useMutation({ mutationFn: () => payrollApi.schedulePayslip(batchId, scheduleDate), onSuccess: (res: any) => { toast.success(`已设定定时发送 ${res.data?.scheduled || 0} 条工资条`) setShowScheduleModal(false) queryClient.invalidateQueries({ queryKey: ['batch-detail'] }) }, onError: (err: any) => toast.error(err?.response?.data?.error?.message || '设定失败'), }) const importOvertimeMutation = useMutation({ mutationFn: () => payrollApi.importOvertimeToBatch(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} 条加班费记录`) } else { toast.info(res.data?.message || '没有可导入的加班记录') } }, onError: () => { toast.error('导入失败,请重试') }, }) 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'] }) queryClient.invalidateQueries({ queryKey: ['batches'] }) setPayrollImportResult(data) if (data.updated > 0) { toast.success(`成功更新 ${data.updated} 条工资记录`) } else { toast.info('未更新任何记录') } }, onError: () => toast.error('工资表导入失败'), }) const deleteBatchMutation = useMutation({ mutationFn: () => payrollApi.removeBatch(batchId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['batches'] }) queryClient.invalidateQueries({ queryKey: ['batch-check'] }) onBack() }, }) if (isLoading) return
加载中...
if (!batch) return
批次不存在
const isArchived = batch.status === 'ARCHIVED' const isBonus = batch.type === 'BONUS' const handleTaxDetailClick = async (employeeId: string, name: string) => { setTaxDetailFor({ employeeId, name }) setTaxDetailData(null) setTaxDetailLoading(true) try { const data = await payrollApi.taxDetail(batchId, employeeId) setTaxDetailData(data) } catch { toast.error('获取个税明细失败') } finally { setTaxDetailLoading(false) } } // 可编辑的输入项字段 const editableFields = isBonus ? ['bonus'] : ['baseSalary', 'overtimePay', 'allowance', 'deduction', 'bonus', 'socialEmp', 'housingEmp', 'socialOrg', 'housingOrg'] // 点击单元格进入编辑 const startEdit = (employeeId: string, field: string, currentValue: number) => { if (isArchived || !editableFields.includes(field)) return setEditCell({ employeeId, field }) setEditValue(currentValue.toFixed(2)) } // 保存编辑 const saveEdit = () => { if (!editCell) return const { employeeId, field } = editCell const numValue = Number(editValue) || 0 const entry = batch.entries.find((e: any) => e.employeeId === employeeId) if (!entry) { setEditCell(null); return } const data: any = {} // 输入项字段一起提交 const inputFields = isBonus ? ['bonus'] : ['baseSalary', 'overtimePay', 'allowance', 'deduction', 'bonus'] inputFields.forEach(f => { data[f] = f === field ? numValue : entry[f] }) // 社保公积金字段:编辑哪个提交哪个 const socialFields = ['socialEmp', 'housingEmp', 'socialOrg', 'housingOrg'] if (socialFields.includes(field)) { data[field] = numValue } updateEntryMutation.mutate({ employeeId, data }) setEditCell(null) } // 失焦保存 const handleBlur = () => saveEdit() // Enter 保存,Esc 取消 const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); saveEdit() } if (e.key === 'Escape') setEditCell(null) } // 渲染可编辑单元格 const renderCell = (entry: any, field: string, className: string = '') => { const isEditing = editCell?.employeeId === entry.employeeId && editCell?.field === field const canEdit = !isArchived && editableFields.includes(field) const value = entry[field] || 0 const displayValue = field === 'deduction' && value > 0 ? '-' + fmt(value) : fmt(value) if (isEditing) { return ( setEditValue(e.target.value)} onBlur={handleBlur} onKeyDown={handleKeyDown} /> ) } return ( canEdit && startEdit(entry.employeeId, field, value)} > {canEdit ? ( 0 ? 'text-danger' : ''}`}> {displayValue} ) : field === 'deduction' && value > 0 ? ( {displayValue} ) : ( displayValue )} ) } return (

{batch.name}

{isArchived ? ( 已归档 ) : ( 草稿 )}
{!isArchived && (
{ const file = e.target.files?.[0] if (file) importPayrollMutation.mutate(file) e.target.value = '' }} />
)} {isArchived && (
)}
{/* 发薪工作流步骤条 */} {/* 质量门禁 — 草稿状态下检查异常 */} {!isArchived && batch.entries.length > 0 && ( <> {batch.entries.some((e: any) => e.totalPay > 0 && e.netPay <= 0) && ( 请检查社保/公积金基数是否过大,导致实发工资 ≤ 0 的记录需修正后再归档。 )} {batch.entries.some((e: any) => e.baseSalary === 0 && e.bonus === 0 && e.totalPay === 0) && ( 部分员工所有金额为 0,请确认是否需要填写或移除这些人员。 )} {batch.entries.some((e: any) => e.riskWarnings && e.riskWarnings.length > 0) && ( 部分员工有薪资风险提示(如社保基数偏低/偏高),请在「风险」列查看详情。 )} )} {/* 批次汇总 */}
{batch.employeeCount}
人数
¥{fmt(batch.totalPay)}
应发合计
¥{fmt(batch.totalTax)}
个税合计
¥{fmt(batch.totalNetPay)}
实发合计
{/* 添加人员 */} {showAddEmployee && !isArchived && ( setShowAddEmployee(false)} /> )} {/* 工资表导入结果 */} {payrollImportResult && (

工资表导入结果

总计 {payrollImportResult.total} 行,成功更新 {payrollImportResult.updated} 条
{payrollImportResult.errors?.length > 0 && (
错误详情({payrollImportResult.errors.length}条):
    {payrollImportResult.errors.map((err: string, i: number) => (
  • {err}
  • ))}
)}
)} {/* 提示 */} {!isArchived && (
带下划线的单元格可直接点击编辑,失焦自动保存。社保/公积金可手动覆盖,个税和实发自动计算。
)} {/* 人员表格 */}
{isBonus && } {!isBonus && } {!isArchived && } {batch.entries.slice((page - 1) * pageSize, page * pageSize).map((entry: any) => ( {renderCell(entry, 'baseSalary')} {renderCell(entry, 'overtimePay')} {renderCell(entry, 'allowance')} {renderCell(entry, 'bonus')} {renderCell(entry, 'deduction')} {/* 计算项 - 灰显 */} {renderCell(entry, 'socialEmp')} {renderCell(entry, 'housingEmp')} {!isArchived && ( )} ))}
员工 基本工资 加班费 津贴奖金奖金扣款 应发 社保(个人) 公积金(个人) 个税 实发 风险操作
{entry.employee.name}
{entry.employee.department}
{entry.employee.status === 'RESIGNED' && ( 已离职 )}
{fmt(entry.totalPay)} handleTaxDetailClick(entry.employeeId, entry.employee.name)} title="点击查看个税计算明细">{fmt(entry.tax)} {fmt(entry.netPay)} {entry.riskWarnings && entry.riskWarnings.length > 0 ? ( ) : ( )}
{batch.entries.length > 0 && ( setPage(1)} /> )}
{/* 定时发送弹窗 */} {showScheduleModal && (
setShowScheduleModal(false)}>
e.stopPropagation()}>

定时发送工资条

设定发送时间后,工资条将在指定时间自动发布到员工端

setScheduleDate(e.target.value)} />
)} {/* 个税计算明细弹窗 */} {taxDetailFor && (
setTaxDetailFor(null)}>
e.stopPropagation()}>

个税计算明细 — {taxDetailFor.name}

{taxDetailLoading ? (
计算中...
) : taxDetailData ? (
计税方式:{taxDetailData.method}
{taxDetailData.method === '累计预扣法' ? (
当年累计收入¥{fmt(taxDetailData.ytdIncome)}
累计减除费用(5000×{taxDetailData.month}月)-¥{fmt(taxDetailData.deductionAmount)}
累计个人社保-¥{fmt(taxDetailData.ytdSocialEmp)}
累计个人公积金-¥{fmt(taxDetailData.ytdHousingEmp)}
累计专项附加扣除-¥{fmt(taxDetailData.ytdSpecialDeduction)}
累计应纳税所得额¥{fmt(taxDetailData.ytdTaxableIncome)}
累计已预扣税额¥{fmt(taxDetailData.ytdTaxDeducted)}
当月应预扣税额¥{fmt(taxDetailData.currentMonthTax)}
) : (
年终奖金额¥{fmt(taxDetailData.bonus)}
应纳税额¥{fmt(taxDetailData.tax)}
)} {taxDetailData.archivedCount === 0 && taxDetailData.method === '累计预扣法' && (
注:本年度暂无已归档批次,个税按当月收入全额计算。归档历史批次后累计预扣将自动生效。
)} {/* 社保公积金:系统计算值 vs 实际值对比 */} {(taxDetailData.systemSocialEmp !== undefined || taxDetailData.systemHousingEmp !== undefined) && (
社保公积金对比
{taxDetailData.systemSocialEmp !== undefined && ( )} {taxDetailData.systemHousingEmp !== undefined && ( )} {taxDetailData.systemSocialOrg !== undefined && ( )} {taxDetailData.systemHousingOrg !== undefined && ( )}
项目 系统计算值 实际缴纳值 差异
个人社保 ¥{fmt(taxDetailData.systemSocialEmp)} ¥{fmt(taxDetailData.actualSocialEmp)} 0.01 ? 'text-amber-600 font-medium' : 'text-gray-300'}`}>{Math.abs((taxDetailData.actualSocialEmp || 0) - (taxDetailData.systemSocialEmp || 0)) > 0.01 ? (taxDetailData.actualSocialEmp > taxDetailData.systemSocialEmp ? '+' : '') + '¥' + fmt((taxDetailData.actualSocialEmp || 0) - (taxDetailData.systemSocialEmp || 0)) : '—'}
个人公积金 ¥{fmt(taxDetailData.systemHousingEmp)} ¥{fmt(taxDetailData.actualHousingEmp)} 0.01 ? 'text-amber-600 font-medium' : 'text-gray-300'}`}>{Math.abs((taxDetailData.actualHousingEmp || 0) - (taxDetailData.systemHousingEmp || 0)) > 0.01 ? (taxDetailData.actualHousingEmp > taxDetailData.systemHousingEmp ? '+' : '') + '¥' + fmt((taxDetailData.actualHousingEmp || 0) - (taxDetailData.systemHousingEmp || 0)) : '—'}
单位社保 ¥{fmt(taxDetailData.systemSocialOrg)} ¥{fmt(taxDetailData.actualSocialOrg)} 0.01 ? 'text-amber-600 font-medium' : 'text-gray-300'}`}>{Math.abs((taxDetailData.actualSocialOrg || 0) - (taxDetailData.systemSocialOrg || 0)) > 0.01 ? (taxDetailData.actualSocialOrg > taxDetailData.systemSocialOrg ? '+' : '') + '¥' + fmt((taxDetailData.actualSocialOrg || 0) - (taxDetailData.systemSocialOrg || 0)) : '—'}
单位公积金 ¥{fmt(taxDetailData.systemHousingOrg)} ¥{fmt(taxDetailData.actualHousingOrg)} 0.01 ? 'text-amber-600 font-medium' : 'text-gray-300'}`}>{Math.abs((taxDetailData.actualHousingOrg || 0) - (taxDetailData.systemHousingOrg || 0)) > 0.01 ? (taxDetailData.actualHousingOrg > taxDetailData.systemHousingOrg ? '+' : '') + '¥' + fmt((taxDetailData.actualHousingOrg || 0) - (taxDetailData.systemHousingOrg || 0)) : '—'}
系统计算值按参保地政策比例自动计算,实际缴纳值可手动调整以匹配社保局核定金额
)}
) : (
无数据
)}
)}
) } function AddEmployeeToBatch({ batchId, onClose }: { batchId: string; onClose: () => void }) { const queryClient = useQueryClient() const [selected, setSelected] = useState([]) const { data: employees } = useQuery({ queryKey: ['employees-for-batch'], queryFn: async () => { return await employeeApi.paged({ pageSize: 100 }) }, }) const addMutation = useMutation({ mutationFn: (employeeIds: string[]) => payrollApi.addBatchEmployees(batchId, employeeIds), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['batch-detail'] }) queryClient.invalidateQueries({ queryKey: ['batches'] }) onClose() }, }) return (

添加人员到批次

{employees?.items?.map((emp: any) => ( ))}
) }