feat: 社保公积金独立配置+版本化缴费记录+月度增减员+补偿金批次
- Schema: 拆分社保/公积金配置,新增EmployeeSocialInsRecord/EmployeeHousingFundRecord/DepartmentRecord模型,扩展SalaryChangeRecord,增加SEVERANCE批次类型 - 后端: createEmployee/rehireEmployee接收社保公积金字段并创建缴费记录版本;createTermination/createResignation接收截止年月并关闭缴费记录;调薪/调部门API+版本记录;月度增减员API;公积金独立CRUD/计算/调基;SEVERANCE批次calcBatchEntry - 前端: AddEmployeeModal/RehireModal增加社保公积金输入;ResignModal/Termination增加截止年月+日期不一致提醒;花名册增加调薪/调部门弹窗;SocialInsurance.tsx Tab拆分(社保/公积金/月度增减员)+CSV导出;Money.tsx增加补偿金批次类型 - 修复: seed.ts移除housingOrg/housingEmp;risk.service.ts从HousingFundConfig获取公积金费率
This commit is contained in:
+292
-10
@@ -23,6 +23,10 @@ export default function Roster() {
|
||||
const [resignEmployee, setResignEmployee] = useState<any>(null)
|
||||
const [showRehireModal, setShowRehireModal] = useState(false)
|
||||
const [rehireEmployee, setRehireEmployee] = useState<any>(null)
|
||||
const [showSalaryModal, setShowSalaryModal] = useState(false)
|
||||
const [salaryEmployee, setSalaryEmployee] = useState<any>(null)
|
||||
const [showDeptModal, setShowDeptModal] = useState(false)
|
||||
const [deptEmployee, setDeptEmployee] = useState<any>(null)
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
|
||||
@@ -71,6 +75,26 @@ export default function Roster() {
|
||||
},
|
||||
})
|
||||
|
||||
const salaryChangeMutation = useMutation({
|
||||
mutationFn: (data: any) => api.post(`/roster/${salaryEmployee?.id}/salary-change`, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['roster'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
|
||||
setShowSalaryModal(false)
|
||||
setSalaryEmployee(null)
|
||||
},
|
||||
})
|
||||
|
||||
const deptChangeMutation = useMutation({
|
||||
mutationFn: (data: any) => api.post(`/roster/${deptEmployee?.id}/department-change`, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['roster'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
|
||||
setShowDeptModal(false)
|
||||
setDeptEmployee(null)
|
||||
},
|
||||
})
|
||||
|
||||
const filtered = employees?.filter((e: any) =>
|
||||
!search || e.name.includes(search) || e.department.includes(search)
|
||||
) || []
|
||||
@@ -179,16 +203,38 @@ export default function Roster() {
|
||||
<td className="py-2 px-3 text-center text-gray-500">{e.counts?.payslips || 0}</td>
|
||||
<td className="py-2 px-3 text-center">
|
||||
{e.status === 'ACTIVE' && !e.hasTermination && (
|
||||
<button
|
||||
className="text-xs text-gray-500 hover:text-danger flex items-center gap-0.5"
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation()
|
||||
setResignEmployee(e)
|
||||
setShowResignModal(true)
|
||||
}}
|
||||
>
|
||||
<UserX className="w-3.5 h-3.5" />离职
|
||||
</button>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<button
|
||||
className="text-xs text-gray-500 hover:text-primary"
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation()
|
||||
setSalaryEmployee(e)
|
||||
setShowSalaryModal(true)
|
||||
}}
|
||||
>
|
||||
调薪
|
||||
</button>
|
||||
<button
|
||||
className="text-xs text-gray-500 hover:text-primary"
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation()
|
||||
setDeptEmployee(e)
|
||||
setShowDeptModal(true)
|
||||
}}
|
||||
>
|
||||
调部门
|
||||
</button>
|
||||
<button
|
||||
className="text-xs text-gray-500 hover:text-danger flex items-center gap-0.5"
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation()
|
||||
setResignEmployee(e)
|
||||
setShowResignModal(true)
|
||||
}}
|
||||
>
|
||||
<UserX className="w-3.5 h-3.5" />离职
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{e.hasTermination && e.status === 'ACTIVE' && (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
@@ -257,6 +303,26 @@ export default function Roster() {
|
||||
error={rehireMutation.error as any}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showSalaryModal && salaryEmployee && (
|
||||
<SalaryChangeModal
|
||||
employee={salaryEmployee}
|
||||
onClose={() => { setShowSalaryModal(false); setSalaryEmployee(null) }}
|
||||
onSubmit={(data) => salaryChangeMutation.mutate(data)}
|
||||
loading={salaryChangeMutation.isPending}
|
||||
error={salaryChangeMutation.error as any}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showDeptModal && deptEmployee && (
|
||||
<DeptChangeModal
|
||||
employee={deptEmployee}
|
||||
onClose={() => { setShowDeptModal(false); setDeptEmployee(null) }}
|
||||
onSubmit={(data) => deptChangeMutation.mutate(data)}
|
||||
loading={deptChangeMutation.isPending}
|
||||
error={deptChangeMutation.error as any}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -729,6 +795,136 @@ function ContractInfo({ employeeId, contracts, hireDate }: { employeeId: string;
|
||||
)
|
||||
}
|
||||
|
||||
function SalaryChangeModal({ employee, onClose, onSubmit, loading, error }: {
|
||||
employee: any
|
||||
onClose: () => void
|
||||
onSubmit: (data: any) => void
|
||||
loading: boolean
|
||||
error: any
|
||||
}) {
|
||||
const todayStr = new Date().toISOString().slice(0, 10)
|
||||
const [form, setForm] = useState({
|
||||
newSalary: '',
|
||||
effectiveDate: todayStr,
|
||||
reason: '',
|
||||
})
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit({
|
||||
newSalary: parseFloat(form.newSalary),
|
||||
effectiveDate: new Date(form.effectiveDate).toISOString(),
|
||||
reason: form.reason || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
const canSubmit = form.newSalary && parseFloat(form.newSalary) > 0 && form.effectiveDate
|
||||
|
||||
return (
|
||||
<Modal open onClose={onClose} title={`调薪 - ${employee.name}`}>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label>员工</Label>
|
||||
<div className="text-xs text-gray-600 py-1.5">{employee.name} - {employee.department}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>当前月薪</Label>
|
||||
<div className="text-xs text-gray-600 py-1.5">¥{fmt(employee.monthlySalary)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label>新月薪 *</Label>
|
||||
<Input type="number" value={form.newSalary} onChange={(e) => setForm({ ...form, newSalary: e.target.value })} placeholder="元" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>生效日期 *</Label>
|
||||
<Input type="date" value={form.effectiveDate} onChange={(e) => setForm({ ...form, effectiveDate: e.target.value })} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>调薪原因(选填)</Label>
|
||||
<Input value={form.reason} onChange={(e) => setForm({ ...form, reason: e.target.value })} placeholder="如:年度调薪、晋升加薪" />
|
||||
</div>
|
||||
{error && (
|
||||
<div className="text-xs text-danger">
|
||||
{(error as any)?.response?.data?.error?.message || '操作失败,请重试'}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button variant="secondary" onClick={onClose}>取消</Button>
|
||||
<Button onClick={handleSubmit} disabled={loading || !canSubmit}>{loading ? '保存中...' : '确认调薪'}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
function DeptChangeModal({ employee, onClose, onSubmit, loading, error }: {
|
||||
employee: any
|
||||
onClose: () => void
|
||||
onSubmit: (data: any) => void
|
||||
loading: boolean
|
||||
error: any
|
||||
}) {
|
||||
const todayStr = new Date().toISOString().slice(0, 10)
|
||||
const [form, setForm] = useState({
|
||||
newDepartment: employee.department || '',
|
||||
effectiveDate: todayStr,
|
||||
reason: '',
|
||||
})
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit({
|
||||
newDepartment: form.newDepartment,
|
||||
effectiveDate: new Date(form.effectiveDate).toISOString(),
|
||||
reason: form.reason || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
const canSubmit = form.newDepartment && form.effectiveDate && form.newDepartment !== employee.department
|
||||
|
||||
return (
|
||||
<Modal open onClose={onClose} title={`调部门 - ${employee.name}`}>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label>员工</Label>
|
||||
<div className="text-xs text-gray-600 py-1.5">{employee.name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>当前部门</Label>
|
||||
<div className="text-xs text-gray-600 py-1.5">{employee.department}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label>新部门 *</Label>
|
||||
<Input value={form.newDepartment} onChange={(e) => setForm({ ...form, newDepartment: e.target.value })} placeholder="如:市场部" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>生效日期 *</Label>
|
||||
<Input type="date" value={form.effectiveDate} onChange={(e) => setForm({ ...form, effectiveDate: e.target.value })} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>调部门原因(选填)</Label>
|
||||
<Input value={form.reason} onChange={(e) => setForm({ ...form, reason: e.target.value })} placeholder="如:组织架构调整" />
|
||||
</div>
|
||||
{error && (
|
||||
<div className="text-xs text-danger">
|
||||
{(error as any)?.response?.data?.error?.message || '操作失败,请重试'}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button variant="secondary" onClick={onClose}>取消</Button>
|
||||
<Button onClick={handleSubmit} disabled={loading || !canSubmit}>{loading ? '保存中...' : '确认调部门'}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
function ResignModal({ employee, onClose, onSubmit, loading, error }: {
|
||||
employee: any
|
||||
onClose: () => void
|
||||
@@ -740,8 +936,12 @@ function ResignModal({ employee, onClose, onSubmit, loading, error }: {
|
||||
terminationDate: new Date().toISOString().slice(0, 10),
|
||||
resignationReason: '个人原因',
|
||||
remark: '',
|
||||
socialInsEndMonth: '',
|
||||
housingFundEndMonth: '',
|
||||
})
|
||||
|
||||
const terminationMonth = form.terminationDate ? form.terminationDate.slice(0, 7) : ''
|
||||
|
||||
const reasons = ['个人原因', '职业发展', '薪资不满意', '家庭原因', '身体原因', '其他']
|
||||
|
||||
const handleSubmit = () => {
|
||||
@@ -750,6 +950,8 @@ function ResignModal({ employee, onClose, onSubmit, loading, error }: {
|
||||
terminationDate: new Date(form.terminationDate).toISOString(),
|
||||
resignationReason: form.resignationReason,
|
||||
remark: form.remark || undefined,
|
||||
socialInsEndMonth: form.socialInsEndMonth || terminationMonth,
|
||||
housingFundEndMonth: form.housingFundEndMonth || terminationMonth,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -777,6 +979,26 @@ function ResignModal({ employee, onClose, onSubmit, loading, error }: {
|
||||
{reasons.map((r) => <option key={r} value={r}>{r}</option>)}
|
||||
</Select>
|
||||
</div>
|
||||
<div className="border-t pt-3">
|
||||
<Label>社保公积金截止缴费年月</Label>
|
||||
<div className="text-xs text-gray-400 mb-2">默认与离职日期同月,可手动修改</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label>社保截止年月</Label>
|
||||
<Input type="month" value={form.socialInsEndMonth || terminationMonth} onChange={(e) => setForm({ ...form, socialInsEndMonth: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>公积金截止年月</Label>
|
||||
<Input type="month" value={form.housingFundEndMonth || terminationMonth} onChange={(e) => setForm({ ...form, housingFundEndMonth: e.target.value })} />
|
||||
</div>
|
||||
</div>
|
||||
{((form.socialInsEndMonth && form.socialInsEndMonth !== terminationMonth) || (form.housingFundEndMonth && form.housingFundEndMonth !== terminationMonth)) && (
|
||||
<div className="flex items-center gap-2 px-3 py-2 rounded-md bg-amber-50 text-warning text-xs mt-2">
|
||||
<AlertTriangle className="w-4 h-4 shrink-0" />
|
||||
截止缴费年月与离职日期不在同月,请确认是否为多缴/少缴月份。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label>备注(选填)</Label>
|
||||
<Input value={form.remark} onChange={(e) => setForm({ ...form, remark: e.target.value })} placeholder="补充说明" />
|
||||
@@ -821,7 +1043,10 @@ function RehireModal({ employee, onClose, onSubmit, loading, error }: {
|
||||
contractYears: 3,
|
||||
probationMonths: 0,
|
||||
probationSalary: 0,
|
||||
socialInsBase: '', socialInsStartMonth: '',
|
||||
housingFundBase: '', housingFundStartMonth: '',
|
||||
})
|
||||
const hireMonth = form.hireDate ? form.hireDate.slice(0, 7) : ''
|
||||
|
||||
// 计算合同月数
|
||||
const contractMonths = (() => {
|
||||
@@ -914,6 +1139,10 @@ function RehireModal({ employee, onClose, onSubmit, loading, error }: {
|
||||
const data: any = {
|
||||
hireDate: new Date(form.hireDate).toISOString(),
|
||||
department: form.department,
|
||||
socialInsBase: form.socialInsBase ? parseFloat(form.socialInsBase) : undefined,
|
||||
socialInsStartMonth: form.socialInsStartMonth || undefined,
|
||||
housingFundBase: form.housingFundBase ? parseFloat(form.housingFundBase) : undefined,
|
||||
housingFundStartMonth: form.housingFundStartMonth || undefined,
|
||||
}
|
||||
if (form.contractType !== 'UNSIGNED' && form.startDate) {
|
||||
data.contract = {
|
||||
@@ -953,6 +1182,28 @@ function RehireModal({ employee, onClose, onSubmit, loading, error }: {
|
||||
<Label>新入职日期 *</Label>
|
||||
<Input type="date" value={form.hireDate} onChange={(e) => handleHireDateChange(e.target.value)} />
|
||||
</div>
|
||||
<div className="border-t pt-3">
|
||||
<Label>社保公积金</Label>
|
||||
<div className="text-xs text-gray-400 mb-2">默认与月工资一致,可手动修改</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label>社保缴费基数</Label>
|
||||
<Input type="number" value={form.socialInsBase || employee?.monthlySalary || ''} onChange={(e) => setForm({ ...form, socialInsBase: e.target.value })} placeholder="默认为月工资" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>社保开始年月</Label>
|
||||
<Input type="month" value={form.socialInsStartMonth || hireMonth} onChange={(e) => setForm({ ...form, socialInsStartMonth: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>公积金缴费基数</Label>
|
||||
<Input type="number" value={form.housingFundBase || employee?.monthlySalary || ''} onChange={(e) => setForm({ ...form, housingFundBase: e.target.value })} placeholder="默认为月工资" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>公积金开始年月</Label>
|
||||
<Input type="month" value={form.housingFundStartMonth || hireMonth} onChange={(e) => setForm({ ...form, housingFundStartMonth: e.target.value })} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t pt-3">
|
||||
<Label>合同类型</Label>
|
||||
<Select value={form.contractType} onChange={(e) => setForm({ ...form, contractType: e.target.value as any, endDate: e.target.value === 'UNFIXED' ? '' : form.endDate })}>
|
||||
@@ -1039,8 +1290,13 @@ function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
|
||||
contractType: 'FIXED' as 'FIXED' | 'UNFIXED' | 'UNSIGNED',
|
||||
signDate: '', startDate: todayStr, endDate: defaultEndDate,
|
||||
contractYears: 3, probationMonths: 0, probationSalary: 0,
|
||||
socialInsBase: '', socialInsStartMonth: '',
|
||||
housingFundBase: '', housingFundStartMonth: '',
|
||||
})
|
||||
|
||||
const hireMonth = form.hireDate ? form.hireDate.slice(0, 7) : ''
|
||||
const salaryNum = parseFloat(form.monthlySalary) || 0
|
||||
|
||||
// 入职日期变更 → 同步合同开始日期 + 重算结束日期
|
||||
const handleHireDateChange = (hireDate: string) => {
|
||||
if (form.contractType === 'FIXED' && form.contractYears > 0 && hireDate) {
|
||||
@@ -1145,6 +1401,10 @@ function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
|
||||
monthlySalary: form.monthlySalary, gender: form.gender,
|
||||
idCardNumber: form.idCardNumber || undefined,
|
||||
phone: form.phone || undefined,
|
||||
socialInsBase: form.socialInsBase ? parseFloat(form.socialInsBase) : undefined,
|
||||
socialInsStartMonth: form.socialInsStartMonth || undefined,
|
||||
housingFundBase: form.housingFundBase ? parseFloat(form.housingFundBase) : undefined,
|
||||
housingFundStartMonth: form.housingFundStartMonth || undefined,
|
||||
}
|
||||
if (form.contractType !== 'UNSIGNED' && form.startDate) {
|
||||
data.contract = {
|
||||
@@ -1186,6 +1446,28 @@ function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div><Label>手机号</Label><Input value={form.phone} onChange={(e) => setForm({ ...form, phone: e.target.value })} placeholder="选填" maxLength={11} /></div>
|
||||
</div>
|
||||
<div className="border-t pt-3">
|
||||
<Label>社保公积金</Label>
|
||||
<div className="text-xs text-gray-400 mb-2">默认与月工资一致,可手动修改</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label>社保缴费基数</Label>
|
||||
<Input type="number" value={form.socialInsBase || form.monthlySalary} onChange={(e) => setForm({ ...form, socialInsBase: e.target.value })} placeholder="默认为月工资" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>社保开始年月</Label>
|
||||
<Input type="month" value={form.socialInsStartMonth || hireMonth} onChange={(e) => setForm({ ...form, socialInsStartMonth: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>公积金缴费基数</Label>
|
||||
<Input type="number" value={form.housingFundBase || form.monthlySalary} onChange={(e) => setForm({ ...form, housingFundBase: e.target.value })} placeholder="默认为月工资" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>公积金开始年月</Label>
|
||||
<Input type="month" value={form.housingFundStartMonth || hireMonth} onChange={(e) => setForm({ ...form, housingFundStartMonth: e.target.value })} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t pt-3">
|
||||
<Label>合同类型</Label>
|
||||
<Select value={form.contractType} onChange={(e) => setForm({ ...form, contractType: e.target.value as any, endDate: e.target.value === 'UNFIXED' ? '' : form.endDate })}>
|
||||
|
||||
Reference in New Issue
Block a user