feat: 账户关联根部门 + 员工新增自动带出账户

1. 账户管理:新建/编辑时可勾选关联 level=0 根部门(公司/分公司/子公司)
2. 后端新增 API:
   - PUT /social/accounts/:id/departments 批量关联根部门
   - GET /social/accounts/:id/departments 查询已关联部门
   - GET /social/department-account/:departmentId 按部门带出适用账户+标准
3. 部门更新 API 支持 socialAccountId/housingAccountId 字段
4. 员工新增表单:选定部门后自动带出社保公积金账户,可手动调整
5. 参保记录创建时写入 accountId

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
selfrelease
2026-08-16 13:59:06 +08:00
parent 63b6c9dcc7
commit 014c94e482
7 changed files with 799 additions and 10 deletions
+73 -5
View File
@@ -1,7 +1,7 @@
import { useState, useEffect } from "react"
import { useQuery } from "@tanstack/react-query"
import { toast } from "sonner"
import { rosterApi, socialInsuranceApi, employeeApi } from '../../lib/api-services'
import { rosterApi, socialInsuranceApi, employeeApi, socialAccountApi } from '../../lib/api-services'
import api from '../../lib/api'
import Button from "../../components/ui/Button"
import { Input, Label, Select } from "../../components/ui/Input"
@@ -710,7 +710,7 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
if (saved) return JSON.parse(saved)
} catch {}
return {
name: '', department: '', position: '', hireDate: todayStr, monthlySalary: '',
name: '', department: '', departmentId: '', position: '', hireDate: todayStr, monthlySalary: '',
idCardNumber: '', gender: '男' as '男' | '女', femaleWorkerType: '' as '' | 'CADRE' | 'WORKER', phone: '',
city: '北京', education: '',
contractType: 'FIXED' as 'FIXED' | 'UNFIXED' | 'UNSIGNED',
@@ -735,6 +735,36 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
const hireMonth = form.hireDate ? form.hireDate.slice(0, 7) : ''
// 选定部门后自动带出社保公积金账户
const [autoAccounts, setAutoAccounts] = useState<{ socialAccount: any; housingAccount: any; socialStandard: any; housingStandard: any } | null>(null)
const [manualSocialAccountId, setManualSocialAccountId] = useState<string>('')
const [manualHousingAccountId, setManualHousingAccountId] = useState<string>('')
// 部门变化时查询适用账户
useEffect(() => {
if (form.departmentId) {
socialAccountApi.departmentAccount(form.departmentId).then((res: any) => {
setAutoAccounts(res)
setManualSocialAccountId(res.socialAccount?.id || '')
setManualHousingAccountId(res.housingAccount?.id || '')
}).catch(() => {})
} else {
setAutoAccounts(null)
}
}, [form.departmentId])
// 加载所有账户列表(供手动调整)
const { data: allSocialAccounts = [] } = useQuery<any[]>({
queryKey: ['social-accounts', 'SOCIAL'],
queryFn: () => socialAccountApi.list('SOCIAL'),
enabled: !!form.departmentId,
})
const { data: allHousingAccounts = [] } = useQuery<any[]>({
queryKey: ['social-accounts', 'HOUSING'],
queryFn: () => socialAccountApi.list('HOUSING'),
enabled: !!form.departmentId,
})
// 入职日期变更 → 同步合同开始日期 + 重算结束日期
const handleHireDateChange = (hireDate: string) => {
if (form.contractType === 'FIXED' && form.contractYears > 0 && hireDate) {
@@ -913,7 +943,7 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
return
}
const data: any = {
name: form.name, department: form.department,
name: form.name, department: form.department, departmentId: form.departmentId || undefined,
position: form.position || undefined,
hireDate: new Date(form.hireDate).toISOString(),
monthlySalary: form.monthlySalary, gender: form.gender,
@@ -925,6 +955,8 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
socialInsStartMonth: form.socialInsStartMonth || undefined,
housingFundBase: form.housingFundBase ? parseFloat(form.housingFundBase) : undefined,
housingFundStartMonth: form.housingFundStartMonth || undefined,
socialAccountId: manualSocialAccountId || undefined,
housingAccountId: manualHousingAccountId || undefined,
}
if (form.contractType !== 'UNSIGNED' && form.startDate) {
data.contract = {
@@ -964,7 +996,10 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
{/* 基本信息 */}
<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><Select value={form.department} onChange={(e) => setForm({ ...form, department: e.target.value })}><option value=""></option>{deptOptions.map(d => <option key={d.id} value={d.label}>{' '.repeat(d.level)}{d.label}</option>)}</Select></div>
<div><Label> *</Label><Select value={form.departmentId} onChange={(e) => {
const opt = deptOptions.find(d => d.id === e.target.value)
setForm({ ...form, departmentId: e.target.value, department: opt?.label || '' })
}}><option value=""></option>{deptOptions.map(d => <option key={d.id} value={d.id}>{' '.repeat(d.level)}{d.label}</option>)}</Select></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 && (
@@ -1037,9 +1072,42 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
{isNoSocialContract ? (
<span className="text-xs text-amber-600">/</span>
) : (
<span className="text-xs text-gray-500"></span>
<span className="text-xs text-gray-500"></span>
)}
</div>
{/* 账户选择(选部门后自动带出,可手动调整) */}
{!isNoSocialContract && (
<div className="grid grid-cols-2 gap-4 mb-3">
<div>
<Label></Label>
<Select value={manualSocialAccountId} onChange={(e) => setManualSocialAccountId(e.target.value)} disabled={!form.departmentId}>
<option value="">{form.departmentId ? '未选择' : '请先选择部门'}</option>
{allSocialAccounts.map((a: any) => (
<option key={a.id} value={a.id}>{a.name}{a.city}</option>
))}
</Select>
{autoAccounts?.socialStandard && manualSocialAccountId === autoAccounts.socialAccount?.id && (
<div className="text-xs text-gray-400 mt-1">
{autoAccounts.socialStandard.baseMin}~{autoAccounts.socialStandard.baseMax} {autoAccounts.socialStandard.pensionOrg}%/{autoAccounts.socialStandard.pensionEmp}%
</div>
)}
</div>
<div>
<Label></Label>
<Select value={manualHousingAccountId} onChange={(e) => setManualHousingAccountId(e.target.value)} disabled={!form.departmentId}>
<option value="">{form.departmentId ? '未选择' : '请先选择部门'}</option>
{allHousingAccounts.map((a: any) => (
<option key={a.id} value={a.id}>{a.name}{a.city}</option>
))}
</Select>
{autoAccounts?.housingStandard && manualHousingAccountId === autoAccounts.housingAccount?.id && (
<div className="text-xs text-gray-400 mt-1">
{autoAccounts.housingStandard.baseMin}~{autoAccounts.housingStandard.baseMax} {autoAccounts.housingStandard.housingOrg}%/{autoAccounts.housingStandard.housingEmp}%
</div>
)}
</div>
</div>
)}
<div className={`grid grid-cols-4 gap-4 ${isNoSocialContract ? 'opacity-50' : ''}`}>
<div>
<Label></Label>