feat: 完成全部11项优化需求 + 模板必填项标注 + 归档重算个税
- 高优先级: 花名册导入模板必填项标注、性别自动识别、导入结果反馈、证据链Excel导出、身份证搜索修复、试用期区分与转正提醒、薪税批次流程优化 - 中优先级: 专项附加扣除批量导入、文本模板库完善(Word下载/复制/使用说明)、用工体检评分标准说明、考勤页面导入入口 - 所有导入模板表头标注必填项(*后缀)并含示例行 - 导入逻辑统一改用getField兼容*后缀列名 - 批次归档时强制重算所有条目个税和社保,解决多未归档批次并存时累计计算不准问题 - 更新需求梳理文档
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { useConfirm } from '../hooks/useConfirm'
|
||||
import { Calculator, Info, Check, Settings as SettingsIcon, Plus, History, Download, AlertCircle, Clock, MapPin, Sparkles } from 'lucide-react'
|
||||
import { Calculator, Info, Check, Settings as SettingsIcon, Plus, History, Download, AlertCircle, Clock, MapPin, Sparkles, Upload, X } 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 } from '../components/ui/Input'
|
||||
@@ -1219,6 +1220,11 @@ function SpecialDeductionTab({ month, setMonth }: { month: string; setMonth: (m:
|
||||
const queryClient = useQueryClient()
|
||||
const [editing, setEditing] = useState<string | null>(null)
|
||||
const [editForm, setEditForm] = useState<any>(null)
|
||||
const [showImport, setShowImport] = useState(false)
|
||||
const [importFile, setImportFile] = useState<File | null>(null)
|
||||
const [importResult, setImportResult] = useState<any>(null)
|
||||
const [importing, setImporting] = useState(false)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// 查询当月所有员工的专项附加扣除
|
||||
const { data: records = [], isLoading } = useQuery<any[]>({
|
||||
@@ -1326,6 +1332,13 @@ function SpecialDeductionTab({ month, setMonth }: { month: string; setMonth: (m:
|
||||
>
|
||||
{batchCopyMutation.isPending ? '复制中...' : `复制上月(${prevMonth})`}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => setShowImport(true)}
|
||||
>
|
||||
<Upload className="w-3.5 h-3.5 mr-1" />批量导入
|
||||
</Button>
|
||||
<Input type="month" value={month} onChange={(e) => setMonth(e.target.value)} className="!w-32" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -1439,6 +1452,91 @@ function SpecialDeductionTab({ month, setMonth }: { month: string; setMonth: (m:
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 批量导入弹窗 */}
|
||||
{showImport && (
|
||||
<div className="fixed inset-0 bg-black/30 flex items-center justify-center z-50 p-4" onClick={() => setShowImport(false)}>
|
||||
<Card className="max-w-lg w-full" >
|
||||
<div onClick={(e) => e.stopPropagation()} className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-sm font-medium">批量导入专项附加扣除 — {month}</h2>
|
||||
<button onClick={() => { setShowImport(false); setImportFile(null); setImportResult(null) }} className="text-gray-400 hover:text-gray-600"><X className="w-4 h-4" /></button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={async () => {
|
||||
try {
|
||||
const token = useAuthStore.getState().accessToken
|
||||
const baseURL = import.meta.env.DEV ? 'http://localhost:3000/api/v1' : '/api/v1'
|
||||
const res = await fetch(`${baseURL}/import/special-deduction/template`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
})
|
||||
const blob = await res.blob()
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = '专项附加扣除导入模板.xlsx'
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
} catch { toast.error('下载模板失败') }
|
||||
}}>
|
||||
<Download className="w-3.5 h-3.5 mr-1" />下载模板
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="border-2 border-dashed border-gray-200 rounded-lg p-6 text-center">
|
||||
<input ref={fileInputRef} type="file" accept=".xlsx,.xls" className="hidden" id="special-deduction-import-file" onChange={(e) => { setImportFile(e.target.files?.[0] || null); setImportResult(null) }} />
|
||||
<label htmlFor="special-deduction-import-file" className="cursor-pointer text-xs text-primary hover:underline">
|
||||
{importFile ? importFile.name : '点击选择 Excel 文件'}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{importResult && (
|
||||
<div className="px-3 py-2 rounded-md bg-green-50 text-green-700 text-xs space-y-1">
|
||||
<div className="font-medium">导入完成</div>
|
||||
<div>成功 {importResult.updated} 条,跳过 {importResult.skipped} 条,共 {importResult.total} 条</div>
|
||||
{importResult.errors?.length > 0 && (
|
||||
<div className="mt-1 pt-1 border-t border-green-200">
|
||||
{importResult.errors.slice(0, 5).map((e: string, i: number) => <div key={i} className="text-amber-600">{e}</div>)}
|
||||
{importResult.errors.length > 5 && <div className="text-amber-600">...还有 {importResult.errors.length - 5} 条</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="secondary" size="sm" onClick={() => { setShowImport(false); setImportFile(null); setImportResult(null) }}>取消</Button>
|
||||
<Button size="sm" onClick={async () => {
|
||||
if (!importFile) return toast.error('请选择文件')
|
||||
setImporting(true)
|
||||
setImportResult(null)
|
||||
try {
|
||||
const token = useAuthStore.getState().accessToken
|
||||
const formData = new FormData()
|
||||
formData.append('file', importFile)
|
||||
formData.append('month', month)
|
||||
const res = await fetch('/api/v1/import/special-deduction', {
|
||||
method: 'POST',
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
body: formData,
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!data.success) { toast.error(data.error?.message || '导入失败') }
|
||||
else {
|
||||
setImportResult(data.data)
|
||||
queryClient.invalidateQueries({ queryKey: ['special-deduction'] })
|
||||
toast.success(`导入完成:成功 ${data.data.updated} 条`)
|
||||
}
|
||||
} catch (e: any) { toast.error(e?.message || '导入失败') }
|
||||
finally { setImporting(false) }
|
||||
}} disabled={!importFile || importing}>{importing ? '导入中...' : '开始导入'}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user