feat: 完成全部11项优化需求 + 模板必填项标注 + 归档重算个税

- 高优先级: 花名册导入模板必填项标注、性别自动识别、导入结果反馈、证据链Excel导出、身份证搜索修复、试用期区分与转正提醒、薪税批次流程优化
- 中优先级: 专项附加扣除批量导入、文本模板库完善(Word下载/复制/使用说明)、用工体检评分标准说明、考勤页面导入入口
- 所有导入模板表头标注必填项(*后缀)并含示例行
- 导入逻辑统一改用getField兼容*后缀列名
- 批次归档时强制重算所有条目个税和社保,解决多未归档批次并存时累计计算不准问题
- 更新需求梳理文档
This commit is contained in:
freedakgmail
2026-07-29 19:08:45 +08:00
parent 7c24ebe3d9
commit 0372cbe243
14 changed files with 1275 additions and 171 deletions
+93 -9
View File
@@ -62,6 +62,89 @@ export default function Money() {
// ========== 发薪批次管理 ==========
function CustomEmployeeSelector({ selectedIds, onChange }: { selectedIds: string[]; onChange: (ids: string[]) => void }) {
const [search, setSearch] = useState('')
const [filterDept, setFilterDept] = useState('')
const { data: employees } = useQuery<any>({
queryKey: ['roster-for-batch', search, filterDept],
queryFn: async () => {
const params: any = { pageSize: 999 }
if (search) params.search = search
if (filterDept) params.department = filterDept
params.status = 'ACTIVE'
const res = await api.get('/roster', { params }) as any
return res.data || []
},
})
const { data: deptList } = useQuery<string[]>({
queryKey: ['roster-departments'],
queryFn: async () => {
const res = await api.get('/roster/departments') as any
return res.data || []
},
})
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 (
<div className="border rounded-md p-3 space-y-2">
<div className="flex items-center justify-between">
<Label></Label>
<span className="text-xs text-gray-500"> {selectedIds.length} </span>
</div>
<div className="flex gap-2">
<Input placeholder="搜索姓名" value={search} onChange={(e) => setSearch(e.target.value)} className="!w-40" />
<select value={filterDept} onChange={(e) => setFilterDept(e.target.value)} className="h-9 rounded-md border border-gray-200 bg-white px-3 text-sm">
<option value=""></option>
{deptList?.map((d: string) => <option key={d} value={d}>{d}</option>)}
</select>
{employees && employees.length > 0 && (
<button onClick={toggleAll} className="text-xs text-primary hover:underline whitespace-nowrap">
{employees.every((e: any) => selectedIds.includes(e.id)) ? '取消全选' : '全选当前'}
</button>
)}
</div>
<div className="max-h-48 overflow-y-auto border rounded">
{employees && employees.length > 0 ? (
<table className="w-full text-xs">
<tbody>
{employees.map((e: any) => (
<tr key={e.id} className="border-b last:border-0 hover:bg-gray-50 cursor-pointer" onClick={() => toggle(e.id)}>
<td className="px-2 py-1.5 w-8">
<input type="checkbox" checked={selectedIds.includes(e.id)} onChange={() => toggle(e.id)} />
</td>
<td className="px-2 py-1.5 font-medium">{e.name}</td>
<td className="px-2 py-1.5 text-gray-500">{e.department}</td>
<td className="px-2 py-1.5 text-gray-400 text-right">¥{fmt(e.monthlySalary)}</td>
</tr>
))}
</tbody>
</table>
) : (
<div className="py-4 text-center text-gray-400 text-xs"></div>
)}
</div>
</div>
)
}
function BatchManager() {
const queryClient = useQueryClient()
const confirm = useConfirm()
@@ -73,8 +156,9 @@ function BatchManager() {
const [selectedBatchId, setSelectedBatchId] = useState<string | null>(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'>('copy_last')
const [createMode, setCreateMode] = useState<'copy_last' | 'blank_employees' | 'blank_all' | 'copy_batch' | 'custom'>('copy_last')
const [sourceBatchId, setSourceBatchId] = useState<string>('')
const [selectedEmployeeIds, setSelectedEmployeeIds] = useState<string[]>([])
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
@@ -206,11 +290,6 @@ function BatchManager() {
)}
<div className="flex-1" />
<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" />
@@ -233,11 +312,12 @@ function BatchManager() {
</div>
<div>
<Label></Label>
<Select value={createMode} onChange={(e) => { setCreateMode(e.target.value as any); setSourceBatchId('') }}>
<Select value={createMode} onChange={(e) => { setCreateMode(e.target.value as any); setSourceBatchId(''); setSelectedEmployeeIds([]) }}>
<option value="copy_last"></option>
<option value="blank_employees">0</option>
<option value="blank_all"></option>
<option value="copy_batch"></option>
<option value="custom"></option>
</Select>
</div>
</div>
@@ -255,16 +335,20 @@ function BatchManager() {
)}
</div>
)}
{createMode === 'custom' && (
<CustomEmployeeSelector selectedIds={selectedEmployeeIds} onChange={setSelectedEmployeeIds} />
)}
<div className="text-xs text-gray-500 space-y-0.5">
{createMode === 'copy_last' && <p>//</p>}
{createMode === 'blank_employees' && <p>0</p>}
{createMode === 'blank_all' && <p></p>}
{createMode === 'copy_batch' && <p></p>}
{createMode === 'custom' && <p></p>}
</div>
<div className="flex gap-2">
<Button
onClick={() => createMutation.mutate({ month, type: createType, mode: createMode, sourceBatchId: sourceBatchId || undefined })}
disabled={createMutation.isPending || (createMode === 'copy_batch' && !sourceBatchId)}
onClick={() => createMutation.mutate({ month, type: createType, mode: createMode, sourceBatchId: sourceBatchId || undefined, employeeIds: createMode === 'custom' ? selectedEmployeeIds : undefined })}
disabled={createMutation.isPending || (createMode === 'copy_batch' && !sourceBatchId) || (createMode === 'custom' && selectedEmployeeIds.length === 0)}
>
{createMutation.isPending ? '创建中...' : '确认创建'}
</Button>