feat: 20260809 系统优化 - 全部28项问题修复(P0×6+P1×16+P2×6)

P0: 福利批量参保/离职证明下载防乱码/考勤模板合并Sheet/补卡修改/附件在线查看删除
P1: 分页pageSize修复/离职导出筛选/撤回删除草稿/加班费自动计算/考勤加班汇总/证据链异常详情/制度催办/模板导入Word/社保封顶保底/校验字段提示/职务字段/社保费用明细/弹窗防误关/身份证查重/证明员工下拉/培训批量
P2: 离职流程去重/社保基数覆盖输入/薪税入口改名/添加员工引导/绩效模板清理
This commit is contained in:
freedakgmail
2026-08-09 11:59:02 +08:00
parent c355a7d208
commit a2e9ba55c2
43 changed files with 2913 additions and 324 deletions
+77 -19
View File
@@ -1,6 +1,6 @@
import { useState } from "react"
import { useState, useEffect } from "react"
import { useQuery } from "@tanstack/react-query"
import { rosterApi, socialInsuranceApi } from '../../lib/api-services'
import { rosterApi, socialInsuranceApi, employeeApi } from '../../lib/api-services'
import Button from "../../components/ui/Button"
import { Input, Label, Select } from "../../components/ui/Input"
import Modal from "../../components/ui/Modal"
@@ -395,7 +395,7 @@ export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
<div className="grid grid-cols-4 gap-3">
<div>
<Label></Label>
<Input type="number" value={form.socialInsBase || employee?.monthlySalary || ''} onChange={(e) => setForm({ ...form, socialInsBase: e.target.value })} placeholder="默认为月工资" />
<Input type="number" value={form.socialInsBase} onChange={(e) => setForm({ ...form, socialInsBase: e.target.value })} onFocus={(e) => e.target.select()} placeholder={employee?.monthlySalary || '默认为月工资'} />
</div>
<div>
<Label></Label>
@@ -403,7 +403,7 @@ export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
</div>
<div>
<Label></Label>
<Input type="number" value={form.housingFundBase || employee?.monthlySalary || ''} onChange={(e) => setForm({ ...form, housingFundBase: e.target.value })} placeholder="默认为月工资" />
<Input type="number" value={form.housingFundBase} onChange={(e) => setForm({ ...form, housingFundBase: e.target.value })} onFocus={(e) => e.target.select()} placeholder={employee?.monthlySalary || '默认为月工资'} />
</div>
<div>
<Label></Label>
@@ -510,17 +510,35 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
d.setDate(d.getDate() - 1)
return d.toISOString().slice(0, 10)
})()
const [form, setForm] = useState({
name: '', department: '', hireDate: todayStr, monthlySalary: '',
idCardNumber: '', gender: '男' as '男' | '女', femaleWorkerType: '' as '' | 'CADRE' | 'WORKER', phone: '',
city: '北京', education: '',
contractType: 'FIXED' as 'FIXED' | 'UNFIXED' | 'UNSIGNED',
signDate: '', startDate: todayStr, endDate: defaultEndDate,
contractYears: 3, probationMonths: 0, probationSalary: 0,
socialInsBase: '', socialInsStartMonth: '',
housingFundBase: '', housingFundStartMonth: '',
const [form, setForm] = useState(() => {
try {
const saved = localStorage.getItem('add-employee-draft')
if (saved) return JSON.parse(saved)
} catch {}
return {
name: '', department: '', position: '', hireDate: todayStr, monthlySalary: '',
idCardNumber: '', gender: '男' as '男' | '女', femaleWorkerType: '' as '' | 'CADRE' | 'WORKER', phone: '',
city: '北京', education: '',
contractType: 'FIXED' as 'FIXED' | 'UNFIXED' | 'UNSIGNED',
signDate: '', startDate: todayStr, endDate: defaultEndDate,
contractYears: 3, probationMonths: 0, probationSalary: 0,
socialInsBase: '', socialInsStartMonth: '',
housingFundBase: '', housingFundStartMonth: '',
}
})
// 持久化草稿到 localStorage,防止录入数据丢失
useEffect(() => {
try {
const isDirty = !!(form.name || form.department || form.idCardNumber || form.monthlySalary || form.phone)
if (isDirty) {
localStorage.setItem('add-employee-draft', JSON.stringify(form))
} else {
localStorage.removeItem('add-employee-draft')
}
} catch {}
}, [form])
const hireMonth = form.hireDate ? form.hireDate.slice(0, 7) : ''
// 入职日期变更 → 同步合同开始日期 + 重算结束日期
@@ -536,7 +554,8 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
}
}
// 根据身份证号自动计算性别(第17位:奇数=男,偶数=女)
// 根据身份证号自动计算性别(第17位:奇数=男,偶数=女)+ 查重
const [idCardDuplicate, setIdCardDuplicate] = useState<{ exists: boolean; employee?: any } | null>(null)
const handleIdCardChange = (idCard: string) => {
let gender = form.gender
if (idCard.length >= 17) {
@@ -544,6 +563,12 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
if (!isNaN(digit)) gender = digit % 2 === 1 ? '男' : '女'
}
setForm({ ...form, idCardNumber: idCard, gender })
setIdCardDuplicate(null)
if (idCard.length === 18) {
employeeApi.checkIdCard(idCard).then((data: { exists: boolean; employee?: any }) => {
setIdCardDuplicate(data)
}).catch(() => {})
}
}
// 计算合同月数
@@ -610,6 +635,7 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
const handleSubmit = () => {
const data: any = {
name: form.name, department: form.department,
position: form.position || undefined,
hireDate: new Date(form.hireDate).toISOString(),
monthlySalary: form.monthlySalary, gender: form.gender,
femaleWorkerType: form.gender === '女' && form.femaleWorkerType ? form.femaleWorkerType : undefined,
@@ -642,18 +668,29 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
useUnsavedChanges(isDirty)
return (
<Modal open onClose={onClose} title="添加员工" size="xl">
<Modal open onClose={onClose} title="添加员工" size="xl" closeOnOverlayClick={false}>
<div className="space-y-4">
{error && (
<div className="px-3 py-2 rounded-md bg-red-50 text-red-700 text-sm">
{error.response?.data?.error?.message || '操作失败'}
{error.response?.data?.error?.details?.length > 0
? error.response.data.error.details.map((d: any, i: number) => (
<div key={i}> {d.path}: {d.message}</div>
))
: (error.response?.data?.error?.message || '操作失败')}
</div>
)}
{/* 基本信息 */}
<div className="grid grid-cols-4 gap-4">
<div><Label> *</Label><Input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder="员工姓名" /></div>
<div><Label> *</Label><Input value={form.department} onChange={(e) => setForm({ ...form, department: e.target.value })} placeholder="如:技术部" /></div>
<div><Label>/</Label><Input value={form.position} onChange={(e) => setForm({ ...form, position: e.target.value })} placeholder="如:前端工程师" /></div>
<div><Label> *</Label><Input value={form.idCardNumber} onChange={(e) => handleIdCardChange(e.target.value)} placeholder="18位" maxLength={18} /></div>
{idCardDuplicate?.exists && (
<div className="col-span-4 px-3 py-2 rounded-md bg-amber-50 text-amber-700 text-xs flex items-center gap-2">
<AlertTriangle className="w-4 h-4 shrink-0" />
<span>{idCardDuplicate.employee?.name}{idCardDuplicate.employee?.department}</span>
</div>
)}
<div><Label></Label><div className="text-sm text-gray-600 py-2">{form.idCardNumber.length >= 17 ? form.gender : '自动识别'}</div></div>
{form.gender === '女' && (
<div><Label></Label><Select value={form.femaleWorkerType} onChange={(e) => setForm({ ...form, femaleWorkerType: e.target.value as '' | 'CADRE' | 'WORKER' })}><option value=""></option><option value="CADRE">/</option><option value="WORKER">/</option></Select></div>
@@ -665,7 +702,28 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
<div><Label></Label><Input value={form.phone} onChange={(e) => setForm({ ...form, phone: e.target.value })} placeholder="选填" maxLength={11} /></div>
</div>
<div className="grid grid-cols-4 gap-4">
<div><Label></Label><Select value={form.city} onChange={(e) => setForm({ ...form, city: e.target.value })}>{cities.map((c) => <option key={c} value={c}>{c}</option>)}</Select></div>
<div><Label></Label><Select value={form.city} onChange={async (e) => {
const city = e.target.value
setForm({ ...form, city })
const salary = Number(form.socialInsBase === '' ? form.monthlySalary : form.socialInsBase) || 0
const hfBase = Number(form.housingFundBase === '' ? form.monthlySalary : form.housingFundBase) || 0
if (salary > 0) {
try {
const res = await socialInsuranceApi.calculate(salary, city)
if (res?.capped || res?.floored) {
setForm((prev: any) => ({ ...prev, socialInsBase: String(res.actualBase) }))
}
} catch {}
}
if (hfBase > 0) {
try {
const res = await socialInsuranceApi.housingCalculate(hfBase, city)
if (res?.capped || res?.floored) {
setForm((prev: any) => ({ ...prev, housingFundBase: String(res.actualBase) }))
}
} catch {}
}
}}>{cities.map((c) => <option key={c} value={c}>{c}</option>)}</Select></div>
<div><Label></Label><Select value={form.education} onChange={(e) => setForm({ ...form, education: e.target.value })}><option value=""></option><option value="博士"></option><option value="硕士"></option><option value="本科"></option><option value="大专"></option><option value="高中"></option><option value="其他"></option></Select></div>
</div>
{/* 社保公积金 */}
@@ -678,7 +736,7 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
<div className="grid grid-cols-4 gap-4">
<div>
<Label></Label>
<Input type="number" value={form.socialInsBase || form.monthlySalary} onChange={(e) => setForm({ ...form, socialInsBase: e.target.value })} placeholder="默认为月工资" />
<Input type="number" value={form.socialInsBase} onChange={(e) => setForm({ ...form, socialInsBase: e.target.value })} onFocus={(e) => e.target.select()} placeholder={form.monthlySalary || '默认为月工资'} />
</div>
<div>
<Label></Label>
@@ -686,7 +744,7 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
</div>
<div>
<Label></Label>
<Input type="number" value={form.housingFundBase || form.monthlySalary} onChange={(e) => setForm({ ...form, housingFundBase: e.target.value })} placeholder="默认为月工资" />
<Input type="number" value={form.housingFundBase} onChange={(e) => setForm({ ...form, housingFundBase: e.target.value })} onFocus={(e) => e.target.select()} placeholder={form.monthlySalary || '默认为月工资'} />
</div>
<div>
<Label></Label>