feat: 社保公积金账户化重构
新增 SocialAccount(账户)+ SocialYearStandard(年度标准)两层实体, 替代原 SocialInsuranceConfig/HousingFundConfig 按城市管理的方式。 - DB: 新增 SocialAccount、SocialYearStandard 表,Department 加账户关联 - 迁移: 旧 Config 表数据迁移到 Account + YearStandard - 后端: 新增账户 CRUD + 年度标准 API,薪资计算适配 accountId - 前端: 设置页新增账户管理 Tab,组织架构提示 level=0 可关联账户 - 前端: SocialInsurance.tsx 城市选择器改为账户选择器 - 兼容: 旧 Config 表保留,薪资计算回退旧表 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -3,10 +3,10 @@ import { useSearchParams } from 'react-router-dom'
|
||||
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, Sparkles } from 'lucide-react'
|
||||
import { InlineAlert } from '../components/ui/InlineAlert'
|
||||
import PageGuide from '../components/ui/PageGuide'
|
||||
import { socialInsuranceApi } from '../lib/api-services'
|
||||
import { socialInsuranceApi, socialAccountApi } from '../lib/api-services'
|
||||
import Card from '../components/ui/Card'
|
||||
import Button from '../components/ui/Button'
|
||||
import { Input, Label } from '../components/ui/Input'
|
||||
@@ -24,6 +24,7 @@ export default function SocialInsurance() {
|
||||
const initialTab = (searchParams.get('tab') as 'monthly' | 'social' | 'housing' | 'deduction' | 'enrollment') || 'monthly'
|
||||
const [tab, setTab] = useState<'monthly' | 'social' | 'housing' | 'deduction' | 'enrollment'>(initialTab)
|
||||
const [city, setCity] = useState<string>('北京')
|
||||
const [selectedAccountId, setSelectedAccountId] = useState<string>('')
|
||||
const [showAddCity, setShowAddCity] = useState(false)
|
||||
const [newCityName, setNewCityName] = useState('')
|
||||
const [base, setBase] = useState(8000)
|
||||
@@ -56,14 +57,26 @@ export default function SocialInsurance() {
|
||||
baseMin: 6326, baseMax: 33891,
|
||||
})
|
||||
|
||||
// 获取城市列表
|
||||
const { data: cities = [] } = useQuery<string[]>({
|
||||
queryKey: ['social-config-cities'],
|
||||
queryFn: async () => {
|
||||
return await socialInsuranceApi.cities()
|
||||
},
|
||||
// 获取账户列表(按当前 tab 类型)
|
||||
const accountType = tab === 'housing' ? 'HOUSING' : 'SOCIAL'
|
||||
const { data: accounts = [] } = useQuery<any[]>({
|
||||
queryKey: ['social-accounts', accountType],
|
||||
queryFn: () => socialAccountApi.list(accountType),
|
||||
})
|
||||
|
||||
// 选中账户变化时同步 city
|
||||
useEffect(() => {
|
||||
if (selectedAccountId) {
|
||||
const acc = accounts.find((a: any) => a.id === selectedAccountId)
|
||||
if (acc) setCity(acc.city)
|
||||
} else if (accounts.length > 0) {
|
||||
// 默认选第一个(或默认账户)
|
||||
const def = accounts.find((a: any) => a.isDefault) || accounts[0]
|
||||
setSelectedAccountId(def.id)
|
||||
setCity(def.city)
|
||||
}
|
||||
}, [accounts, selectedAccountId])
|
||||
|
||||
const { data: config, isLoading: configLoading } = useQuery<any>({
|
||||
queryKey: ['social-config', city],
|
||||
queryFn: async () => {
|
||||
@@ -185,10 +198,16 @@ export default function SocialInsurance() {
|
||||
})
|
||||
|
||||
const createVersionMutation = useMutation({
|
||||
mutationFn: (data: any) => socialInsuranceApi.createConfigVersion(data),
|
||||
mutationFn: (data: any) => {
|
||||
if (selectedAccountId) {
|
||||
return socialAccountApi.createStandard(selectedAccountId, data)
|
||||
}
|
||||
return socialInsuranceApi.createConfigVersion(data)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['social-config'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['social-config-versions'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['social-accounts'] })
|
||||
setShowNewVersion(false)
|
||||
toast.success('新版本已创建,旧版本已自动归档')
|
||||
},
|
||||
@@ -199,10 +218,16 @@ export default function SocialInsurance() {
|
||||
})
|
||||
|
||||
const createHousingVersionMutation = useMutation({
|
||||
mutationFn: (data: any) => socialInsuranceApi.createHousingConfigVersion(data),
|
||||
mutationFn: (data: any) => {
|
||||
if (selectedAccountId) {
|
||||
return socialAccountApi.createStandard(selectedAccountId, data)
|
||||
}
|
||||
return socialInsuranceApi.createHousingConfigVersion(data)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['housing-config'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['housing-config-versions'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['social-accounts'] })
|
||||
setShowNewVersion(false)
|
||||
toast.success('公积金新版本已创建,旧版本已自动归档')
|
||||
},
|
||||
@@ -394,53 +419,23 @@ export default function SocialInsurance() {
|
||||
))}
|
||||
{(tab === 'social' || tab === 'housing') && (
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
<label className="text-sm text-gray-500">城市:</label>
|
||||
{showAddCity ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
className="h-9 w-24 rounded-md border border-gray-200 bg-white px-3 text-sm text-gray-700 shadow-sm outline-none transition focus:border-primary focus:ring-2 focus:ring-primary/10"
|
||||
value={newCityName}
|
||||
onChange={(e) => setNewCityName(e.target.value)}
|
||||
placeholder="城市名"
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && newCityName.trim()) {
|
||||
setCity(newCityName.trim())
|
||||
setNewCityName('')
|
||||
setShowAddCity(false)
|
||||
queryClient.invalidateQueries({ queryKey: ['social-config-cities'] })
|
||||
}
|
||||
if (e.key === 'Escape') { setShowAddCity(false); setNewCityName('') }
|
||||
}}
|
||||
/>
|
||||
<button className="h-9 px-2 text-xs text-primary" onClick={() => {
|
||||
if (newCityName.trim()) {
|
||||
setCity(newCityName.trim())
|
||||
setNewCityName('')
|
||||
setShowAddCity(false)
|
||||
queryClient.invalidateQueries({ queryKey: ['social-config-cities'] })
|
||||
}
|
||||
}}>确定</button>
|
||||
<button className="h-9 px-2 text-xs text-gray-400" onClick={() => { setShowAddCity(false); setNewCityName('') }}>取消</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<select
|
||||
className="h-9 rounded-md border border-gray-200 bg-white px-3 text-sm text-gray-700 shadow-sm outline-none transition focus:border-primary focus:ring-2 focus:ring-primary/10"
|
||||
value={city}
|
||||
onChange={(e) => setCity(e.target.value)}
|
||||
>
|
||||
{cities.map((c) => <option key={c} value={c}>{c}</option>)}
|
||||
</select>
|
||||
<button
|
||||
className="h-9 w-9 flex items-center justify-center rounded-md border border-gray-200 bg-white text-gray-400 hover:text-primary hover:border-primary transition"
|
||||
title="添加新城市"
|
||||
onClick={() => setShowAddCity(true)}
|
||||
>
|
||||
<MapPin className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<label className="text-sm text-gray-500">账户:</label>
|
||||
<select
|
||||
className="h-9 rounded-md border border-gray-200 bg-white px-3 text-sm text-gray-700 shadow-sm outline-none transition focus:border-primary focus:ring-2 focus:ring-primary/10"
|
||||
value={selectedAccountId}
|
||||
onChange={(e) => {
|
||||
const acc = accounts.find((a: any) => a.id === e.target.value)
|
||||
setSelectedAccountId(e.target.value)
|
||||
if (acc) setCity(acc.city)
|
||||
}}
|
||||
>
|
||||
{accounts.length === 0 && <option value="">暂无账户,请到设置中创建</option>}
|
||||
{accounts.map((a: any) => (
|
||||
<option key={a.id} value={a.id}>
|
||||
{a.name}({a.city}){a.isDefault ? ' · 默认' : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -708,9 +703,8 @@ export default function SocialInsurance() {
|
||||
<div className="grid md:grid-cols-3 gap-3">
|
||||
<div><Label>生效月份</Label><Input type="month" value={activeNewVersion.effectiveFrom} onChange={(e) => activeSetNewVersion({ ...activeNewVersion, effectiveFrom: e.target.value })} /></div>
|
||||
<div><Label>城市</Label>
|
||||
<select className="w-full h-9 rounded-md border border-input bg-background px-3 text-sm" value={activeNewVersion.city} onChange={(e) => activeSetNewVersion({ ...activeNewVersion, city: e.target.value })}>
|
||||
{[...new Set([city, ...cities])].map((c) => <option key={c} value={c}>{c}</option>)}
|
||||
</select>
|
||||
<Input value={city} disabled className="bg-gray-50" />
|
||||
<p className="text-xs text-gray-400 mt-1">从所选账户继承</p>
|
||||
</div>
|
||||
<div><Label>缴费基数下限</Label><Input type="number" value={activeNewVersion.baseMin} onChange={(e) => activeSetNewVersion({ ...activeNewVersion, baseMin: Number(e.target.value) })} /></div>
|
||||
<div><Label>缴费基数上限</Label><Input type="number" value={activeNewVersion.baseMax} onChange={(e) => activeSetNewVersion({ ...activeNewVersion, baseMax: Number(e.target.value) })} /></div>
|
||||
|
||||
Reference in New Issue
Block a user