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:
@@ -547,6 +547,15 @@ export const socialAccountApi = {
|
||||
/** 按员工获取适用账户 */
|
||||
employeeAccount: (employeeId: string) =>
|
||||
get(`/social/employee-account/${employeeId}`).then(unwrap<any>()),
|
||||
/** 获取账户已关联的根部门 */
|
||||
accountDepartments: (id: string) =>
|
||||
get(`/social/accounts/${id}/departments`).then(unwrap<any[]>()),
|
||||
/** 账户关联根部门(批量) */
|
||||
linkDepartments: (id: string, departmentIds: string[]) =>
|
||||
put(`/social/accounts/${id}/departments`, { departmentIds }).then(unwrap<any>()),
|
||||
/** 按部门获取适用账户(选部门时自动带出) */
|
||||
departmentAccount: (departmentId: string) =>
|
||||
get(`/social/department-account/${departmentId}`).then(unwrap<any>()),
|
||||
}
|
||||
|
||||
export const socialInsuranceApi = {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { toast } from 'sonner'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Building2, Users, CreditCard, Plus, Bell, Download, Upload, FileSpreadsheet, Clock, CheckCircle, AlertCircle, ClipboardList, LayoutGrid, PenTool, HeartPulse, Trash2, Shield, Edit2 } from 'lucide-react'
|
||||
import { settingsApi, notificationsApi, socialAccountApi } from '../lib/api-services'
|
||||
import api from '../lib/api'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
import { getPageSize, setPageSize as setGlobalPageSize } from '../lib/pageSize'
|
||||
import Card from '../components/ui/Card'
|
||||
@@ -1710,11 +1711,25 @@ function SocialAccountSettings() {
|
||||
type={accountType}
|
||||
account={editAccount}
|
||||
onClose={() => { setShowForm(false); setEditAccount(null) }}
|
||||
onSubmit={(data) => {
|
||||
onSubmit={async (data, departmentIds) => {
|
||||
if (editAccount) {
|
||||
updateMutation.mutate({ id: editAccount.id, data })
|
||||
if (departmentIds) {
|
||||
await socialAccountApi.linkDepartments(editAccount.id, departmentIds)
|
||||
toast.success('账户已更新,部门关联已同步')
|
||||
}
|
||||
} else {
|
||||
createMutation.mutate({ ...data, type: accountType })
|
||||
createMutation.mutate(
|
||||
{ ...data, type: accountType },
|
||||
{
|
||||
onSuccess: async (created: any) => {
|
||||
if (departmentIds && departmentIds.length > 0) {
|
||||
await socialAccountApi.linkDepartments(created.id, departmentIds)
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ['social-accounts'] })
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
}}
|
||||
saving={createMutation.isPending || updateMutation.isPending}
|
||||
@@ -1728,7 +1743,7 @@ function AccountFormModal({ type, account, onClose, onSubmit, saving }: {
|
||||
type: string
|
||||
account: any
|
||||
onClose: () => void
|
||||
onSubmit: (data: any) => void
|
||||
onSubmit: (data: any, departmentIds?: string[]) => void
|
||||
saving: boolean
|
||||
}) {
|
||||
const [name, setName] = useState(account?.name || '')
|
||||
@@ -1741,6 +1756,27 @@ function AccountFormModal({ type, account, onClose, onSubmit, saving }: {
|
||||
const [accountTypeVal, setAccountTypeVal] = useState(account?.accountType || 'BASIC')
|
||||
const [isDefault, setIsDefault] = useState(account?.isDefault || false)
|
||||
const [remark, setRemark] = useState(account?.remark || '')
|
||||
const [selectedDeptIds, setSelectedDeptIds] = useState<string[]>([])
|
||||
|
||||
// 加载 level=0 根部门列表
|
||||
const { data: rootDepartments = [] } = useQuery<any[]>({
|
||||
queryKey: ['departments'],
|
||||
queryFn: () => api.get('/departments').then(r => r.data),
|
||||
})
|
||||
const rootDepts = rootDepartments.filter((d: any) => d.level === 0)
|
||||
|
||||
// 编辑时加载已关联部门
|
||||
useEffect(() => {
|
||||
if (account?.id) {
|
||||
socialAccountApi.accountDepartments(account.id).then((depts: any[]) => {
|
||||
setSelectedDeptIds(depts.map((d: any) => d.id))
|
||||
}).catch(() => {})
|
||||
}
|
||||
}, [account?.id])
|
||||
|
||||
const toggleDept = (id: string) => {
|
||||
setSelectedDeptIds(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id])
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open onClose={onClose} title={account ? '编辑账户' : '新建账户'} size="md">
|
||||
@@ -1788,6 +1824,30 @@ function AccountFormModal({ type, account, onClose, onSubmit, saving }: {
|
||||
<Label>备注</Label>
|
||||
<Input value={remark} onChange={(e) => setRemark(e.target.value)} />
|
||||
</div>
|
||||
|
||||
{/* 关联根部门 */}
|
||||
<div>
|
||||
<Label>关联根部门(公司/分公司/子公司)</Label>
|
||||
<p className="text-xs text-gray-400 mb-2">勾选后,这些根部门下的员工将自动继承此账户。未勾选则使用公司默认账户。</p>
|
||||
{rootDepts.length === 0 ? (
|
||||
<div className="text-xs text-gray-400">暂无根部门</div>
|
||||
) : (
|
||||
<div className="space-y-1 max-h-40 overflow-y-auto border rounded-md p-2">
|
||||
{rootDepts.map((d: any) => (
|
||||
<label key={d.id} className="flex items-center gap-2 text-sm py-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedDeptIds.includes(d.id)}
|
||||
onChange={() => toggleDept(d.id)}
|
||||
/>
|
||||
<span>{d.name}</span>
|
||||
{d._count?.employees > 0 && <span className="text-xs text-gray-400">({d._count.employees}人)</span>}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input type="checkbox" checked={isDefault} onChange={(e) => setIsDefault(e.target.checked)} />
|
||||
设为默认账户(同类型下仅一个默认)
|
||||
@@ -1796,7 +1856,7 @@ function AccountFormModal({ type, account, onClose, onSubmit, saving }: {
|
||||
<Button variant="secondary" onClick={onClose}>取消</Button>
|
||||
<Button
|
||||
disabled={!name || !city || saving}
|
||||
onClick={() => onSubmit({ name, city, accountNo, orgName, orgCode, bankName, bankAccount, accountType: accountTypeVal, isDefault, remark })}
|
||||
onClick={() => onSubmit({ name, city, accountNo, orgName, orgCode, bankName, bankAccount, accountType: accountTypeVal, isDefault, remark }, selectedDeptIds)}
|
||||
>
|
||||
{saving ? '保存中...' : '保存'}
|
||||
</Button>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user