feat: 完成全部11项优化需求 + 模板必填项标注 + 归档重算个税
- 高优先级: 花名册导入模板必填项标注、性别自动识别、导入结果反馈、证据链Excel导出、身份证搜索修复、试用期区分与转正提醒、薪税批次流程优化 - 中优先级: 专项附加扣除批量导入、文本模板库完善(Word下载/复制/使用说明)、用工体检评分标准说明、考勤页面导入入口 - 所有导入模板表头标注必填项(*后缀)并含示例行 - 导入逻辑统一改用getField兼容*后缀列名 - 批次归档时强制重算所有条目个税和社保,解决多未归档批次并存时累计计算不准问题 - 更新需求梳理文档
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user