import { useState } from 'react' import { toast } from 'sonner' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Calculator, Info, Check, Settings as SettingsIcon, Plus, History, Download } from 'lucide-react' import api from '../lib/api' import Card from '../components/ui/Card' import Button from '../components/ui/Button' import { Input, Label } from '../components/ui/Input' // 金额格式化:保留两位小数 + 千分位 const fmt = (n: number) => (n || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) export default function SocialInsurance() { const queryClient = useQueryClient() const [tab, setTab] = useState<'social' | 'housing' | 'monthly'>('social') const [city, setCity] = useState('北京') const [base, setBase] = useState(8000) const [showNewVersion, setShowNewVersion] = useState(false) const [showVersions, setShowVersions] = useState(false) const [showAdjust, setShowAdjust] = useState(false) const [adjustData, setAdjustData] = useState(null) const [editItems, setEditItems] = useState>({}) const [editingId, setEditingId] = useState(null) const [monthlyMonth, setMonthlyMonth] = useState(new Date().toISOString().slice(0, 7)) const [newVersion, setNewVersion] = useState({ effectiveFrom: new Date().toISOString().slice(0, 7), city: '北京', pensionOrg: 16, pensionEmp: 8, medicalOrg: 9.8, medicalEmp: 2, unemploymentOrg: 0.5, unemploymentEmp: 0.5, injuryOrg: 0.2, maternityOrg: 0.8, baseMin: 6326, baseMax: 33891, }) const [newHousingVersion, setNewHousingVersion] = useState({ effectiveFrom: new Date().toISOString().slice(0, 7), city: '北京', housingOrg: 12, housingEmp: 12, baseMin: 6326, baseMax: 33891, }) // 获取城市列表 const { data: cities = [] } = useQuery({ queryKey: ['social-config-cities'], queryFn: async () => { const res = await api.get('/social/config/cities') as any return res.data }, }) const { data: config, isLoading: configLoading } = useQuery({ queryKey: ['social-config', city], queryFn: async () => { const res = await api.get('/social/config', { params: { city } }) as any return res.data }, }) const { data: housingConfig, isLoading: housingLoading } = useQuery({ queryKey: ['housing-config', city], queryFn: async () => { const res = await api.get('/social/housing-config', { params: { city } }) as any return res.data }, }) const { data: versions } = useQuery({ queryKey: ['social-config-versions', city], queryFn: async () => { const res = await api.get('/social/config/versions', { params: { city } }) as any return res.data }, enabled: showVersions && tab === 'social', }) const { data: housingVersions } = useQuery({ queryKey: ['housing-config-versions'], queryFn: async () => { const res = await api.get('/social/housing-config/versions') as any return res.data }, enabled: showVersions && tab === 'housing', }) const { data: monthlyChanges } = useQuery({ queryKey: ['monthly-changes', monthlyMonth], queryFn: async () => { const [socialRes, housingRes, socialActiveRes, housingActiveRes] = await Promise.all([ api.get('/social/monthly-changes', { params: { month: monthlyMonth } }) as any, api.get('/social/housing/monthly-changes', { params: { month: monthlyMonth } }) as any, api.get('/social/active-declaration', { params: { month: monthlyMonth } }) as any, api.get('/social/housing/active-declaration', { params: { month: monthlyMonth } }) as any, ]) return { social: socialRes.data, housing: housingRes.data, socialActive: socialActiveRes.data, housingActive: housingActiveRes.data, } }, enabled: tab === 'monthly', }) const { data: result, mutate: calcMutate, isPending } = useMutation({ mutationFn: async () => { const res = await api.post('/social/calculate', { base }) as any return res.data }, }) const { data: housingResult, mutate: calcHousingMutate, isPending: housingCalcPending } = useMutation({ mutationFn: async () => { const res = await api.post('/social/housing-calculate', { base }) as any return res.data }, }) const createVersionMutation = useMutation({ mutationFn: (data: any) => api.post('/social/config/versions', data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['social-config'] }) queryClient.invalidateQueries({ queryKey: ['social-config-versions'] }) setShowNewVersion(false) toast.success('新版本已创建,旧版本已自动归档') }, }) const createHousingVersionMutation = useMutation({ mutationFn: (data: any) => api.post('/social/housing-config/versions', data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['housing-config'] }) queryClient.invalidateQueries({ queryKey: ['housing-config-versions'] }) setShowNewVersion(false) toast.success('公积金新版本已创建,旧版本已自动归档') }, }) const previewAdjustMutation = useMutation({ mutationFn: async () => { const res = await api.get(`/social/config/${config?.id}/adjust-preview`) as any return res.data }, onSuccess: (data) => { setAdjustData(data) setShowAdjust(true) }, }) const previewHousingAdjustMutation = useMutation({ mutationFn: async () => { const res = await api.get(`/social/housing-config/${housingConfig?.id}/adjust-preview`) as any return res.data }, onSuccess: (data) => { setAdjustData(data) setShowAdjust(true) }, }) const applyAdjustMutation = useMutation({ mutationFn: (data: { items: { employeeId: string; newBase: number }[] }) => api.post(`/social/config/${config?.id}/adjust-apply`, data), onSuccess: (res: any) => { queryClient.invalidateQueries({ queryKey: ['social-config'] }) queryClient.invalidateQueries({ queryKey: ['social-config-versions'] }) setShowAdjust(false) setAdjustData(null) setEditItems({}) setEditingId(null) toast.success(`调整完成,共调整 ${res.data?.adjusted || 0} 名员工的社保基数`) }, }) const applyHousingAdjustMutation = useMutation({ mutationFn: (data: { items: { employeeId: string; newBase: number }[] }) => api.post(`/social/housing-config/${housingConfig?.id}/adjust-apply`, data), onSuccess: (res: any) => { queryClient.invalidateQueries({ queryKey: ['housing-config'] }) queryClient.invalidateQueries({ queryKey: ['housing-config-versions'] }) setShowAdjust(false) setAdjustData(null) setEditItems({}) setEditingId(null) toast.success(`调整完成,共调整 ${res.data?.adjusted || 0} 名员工的公积金基数`) }, }) const resetAdjustMutation = useMutation({ mutationFn: () => api.post(`/social/config/${config?.id}/reset-adjustment`, { city }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['social-config', city] }) queryClient.invalidateQueries({ queryKey: ['social-config-versions', city] }) toast.success('社保基数调整已重置,可以重新调整') }, }) const resetHousingAdjustMutation = useMutation({ mutationFn: () => api.post(`/social/housing-config/${housingConfig?.id}/reset-adjustment`, { city }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['housing-config', city] }) queryClient.invalidateQueries({ queryKey: ['housing-config-versions', city] }) toast.success('公积金基数调整已重置,可以重新调整') }, }) const handleExportCSV = (type: 'social' | 'housing', data: any) => { if (!data?.items?.length) return const headers = type === 'social' ? ['姓名', '部门', '社保基数', '开始年月', '截止年月', '变更类型'] : ['姓名', '部门', '公积金基数', '开始年月', '截止年月', '变更类型'] const rows = data.items.map((i: any) => [ i.name, i.department, i.base, i.startMonth, i.endMonth || '', i.changeType ]) const csv = [headers, ...rows].map(r => r.join(',')).join('\n') const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `${type === 'social' ? '社保' : '公积金'}_${data.month || monthlyMonth}.csv` a.click() URL.revokeObjectURL(url) } const isHousing = tab === 'housing' const activeConfig = isHousing ? housingConfig : config const activeVersions = isHousing ? housingVersions : versions const activePreviewMut = isHousing ? previewHousingAdjustMutation : previewAdjustMutation const activeApplyMut = isHousing ? applyHousingAdjustMutation : applyAdjustMutation const activeCreateMut = isHousing ? createHousingVersionMutation : createVersionMutation const activeNewVersion = isHousing ? newHousingVersion : newVersion const activeSetNewVersion = isHousing ? setNewHousingVersion : setNewVersion return (

社保公积金

{tab !== 'monthly' && ( <> )}
{/* Tab 切换 + 城市选择 */}
{(['social', 'housing', 'monthly'] as const).map((t) => ( ))}
{/* ========== 社保 / 公积金 Tab ========== */} {tab !== 'monthly' && ( (isHousing ? housingLoading : configLoading) ? (
加载中...
) : activeConfig ? (
当前生效 生效月份:{activeConfig.effectiveFrom} · {activeConfig.city} {activeConfig.adjustmentDone && ( 已调整员工基数 )}
{activeConfig.adjustmentDone && ( )}
{isHousing ? (
缴费基数下限¥{fmt(activeConfig.baseMin)}
缴费基数上限¥{fmt(activeConfig.baseMax)}
公积金(企业){activeConfig.housingOrg}%
公积金(个人){activeConfig.housingEmp}%
) : (
缴费基数下限¥{fmt(activeConfig.baseMin)}
缴费基数上限¥{fmt(activeConfig.baseMax)}
养老(企业/个人){activeConfig.pensionOrg}% / {activeConfig.pensionEmp}%
医疗(企业/个人){activeConfig.medicalOrg}% / {activeConfig.medicalEmp}%
失业(企业/个人){activeConfig.unemploymentOrg}% / {activeConfig.unemploymentEmp}%
工伤(企业){activeConfig.injuryOrg}%
生育(企业){activeConfig.maternityOrg}%
)}
) : (
该城市暂无{isHousing ? '公积金' : '社保'}配置,请点击「新建版本」创建
) )} {/* 调整预览 */} {tab !== 'monthly' && showAdjust && adjustData && (

员工{isHousing ? '公积金' : '社保'}基数调整

按当前版本基数上下限(¥{fmt(adjustData.baseMin)} ~ ¥{fmt(adjustData.baseMax)})调整全部在职员工{isHousing ? '公积金' : '社保'}缴费基数。 建议基数=上年月均工资按上下限裁剪。您可逐行修改,也可点击「采用建议值」或「保持原基数」。确认后保存,此操作只能执行一次。
共 {adjustData.total} 名员工
{adjustData.items.map((item: any) => { const edit = editItems[item.employeeId] const newBase = edit ?? item.suggestedBase const changed = newBase !== item.oldBase return ( ) })}
姓名 部门 上年月均 {isHousing ? '公积金' : '社保'}基数(当前) {isHousing ? '公积金' : '社保'}基数(建议) {isHousing ? '公积金' : '社保'}基数(新)
{item.name} {item.department} ¥{fmt(item.avgSalary)} ¥{fmt(item.oldBase)} ¥{fmt(item.suggestedBase)} {editingId === item.employeeId ? ( setEditItems({ ...editItems, [item.employeeId]: Number(e.target.value) || 0 })} onBlur={() => setEditingId(null)} autoFocus /> ) : ( setEditingId(item.employeeId)}> ¥{fmt(newBase)} )} {changed && }
)} {/* 版本历史 */} {tab !== 'monthly' && showVersions && (

{isHousing ? '公积金' : '社保'}版本历史

{!activeVersions || activeVersions.length === 0 ? (
暂无版本记录
) : (
{isHousing ? ( ) : ( <> )} {activeVersions.map((v: any) => ( {isHousing ? ( ) : ( <> )} ))}
生效月份 失效月份 城市 基数下限 基数上限公积金%养老% 医疗%状态
{v.effectiveFrom} {v.effectiveTo || '—'} {v.city} ¥{fmt(v.baseMin)} ¥{fmt(v.baseMax)}{v.housingOrg}/{v.housingEmp}{v.pensionOrg}/{v.pensionEmp} {v.medicalOrg}/{v.medicalEmp} {v.isCurrent ? 当前 : 历史}
)}
)} {/* 新建版本 */} {tab !== 'monthly' && showNewVersion && (

新建{isHousing ? '公积金' : '社保'}配置版本

新版本生效后,当前版本将自动归档。发薪批次计算时按批次月份匹配对应版本的配置。通常每年7月调基时新建版本。
activeSetNewVersion({ ...activeNewVersion, effectiveFrom: e.target.value })} />
activeSetNewVersion({ ...activeNewVersion, city: e.target.value })} />
activeSetNewVersion({ ...activeNewVersion, baseMin: Number(e.target.value) })} />
activeSetNewVersion({ ...activeNewVersion, baseMax: Number(e.target.value) })} />
{isHousing ? (
activeSetNewVersion({ ...activeNewVersion, housingOrg: Number(e.target.value) })} />
activeSetNewVersion({ ...activeNewVersion, housingEmp: Number(e.target.value) })} />
) : (
activeSetNewVersion({ ...activeNewVersion, pensionOrg: Number(e.target.value) })} />
activeSetNewVersion({ ...activeNewVersion, pensionEmp: Number(e.target.value) })} />
activeSetNewVersion({ ...activeNewVersion, medicalOrg: Number(e.target.value) })} />
activeSetNewVersion({ ...activeNewVersion, medicalEmp: Number(e.target.value) })} />
activeSetNewVersion({ ...activeNewVersion, unemploymentOrg: Number(e.target.value) })} />
activeSetNewVersion({ ...activeNewVersion, unemploymentEmp: Number(e.target.value) })} />
activeSetNewVersion({ ...activeNewVersion, injuryOrg: Number(e.target.value) })} />
activeSetNewVersion({ ...activeNewVersion, maternityOrg: Number(e.target.value) })} />
)}
)} {/* 试算工具 */} {tab !== 'monthly' && (

{isHousing ? '公积金' : '社保'}试算

setBase(Number(e.target.value) || 0)} />
{activeConfig && (
当前配置:{activeConfig.city} | 基数范围 {fmt(activeConfig.baseMin)}~{fmt(activeConfig.baseMax)}
)}

计算结果

{(() => { const r = isHousing ? housingResult : result if (!r) return
点击「开始计算」查看结果
return (
缴费基数:¥{fmt(r.actualBase)} {r.capped && (已封顶)} {r.floored && (已保底)} {r.configVersion && | 配置版本:{r.configVersion}}
{r.items.map((item: any) => ( ))}
险种 企业% 个人% 企业缴纳 个人缴纳
{item.name} {item.orgRate}% {item.empRate}% ¥{fmt(item.orgAmount)} ¥{fmt(item.empAmount)}
合计 ¥{fmt(r.totalOrg)} ¥{fmt(r.totalEmp)}
总费用 ¥{fmt(r.total)}
企业承担 ¥{fmt(r.totalOrg)} + 个人承担 ¥{fmt(r.totalEmp)}
) })()}
)} {/* ========== 月度办理 Tab ========== */} {tab === 'monthly' && (

月度办理

setMonthlyMonth(e.target.value)} className="!w-32" />
展示当月社保/公积金新增(入职/重新入职)、减少(离职/解聘)及正常在保人员列表,用于经办机构申报。
{(() => { if (!monthlyChanges) return
加载中...
const sAdd = monthlyChanges.social?.additions || [] const sSub = monthlyChanges.social?.subtractions || [] const sNormal = monthlyChanges.socialActive?.items || [] const hAdd = monthlyChanges.housing?.additions || [] const hSub = monthlyChanges.housing?.subtractions || [] const hNormal = monthlyChanges.housingActive?.items || [] if (sAdd.length === 0 && sSub.length === 0 && hAdd.length === 0 && hSub.length === 0 && sNormal.length === 0 && hNormal.length === 0) { return
{monthlyMonth} 无办理记录
} return (
{/* 社保 */}

社保

{sAdd.map((i: any) => ( ))} {sSub.map((i: any) => ( ))} {sNormal.map((i: any) => ( ))}
姓名 部门 类型 基数 开始年月 截止年月
{i.name} {i.department} 新增 ¥{fmt(i.base)} {i.startMonth}
{i.name} {i.department} 减少 ¥{fmt(i.base)} {i.endMonth}
{i.name} {i.department} 正常 ¥{fmt(i.base)} {i.startMonth} {i.endMonth || '在保'}
{/* 公积金 */}

公积金

{hAdd.map((i: any) => ( ))} {hSub.map((i: any) => ( ))} {hNormal.map((i: any) => ( ))}
姓名 部门 类型 基数 开始年月 截止年月
{i.name} {i.department} 新增 ¥{fmt(i.base)} {i.startMonth}
{i.name} {i.department} 减少 ¥{fmt(i.base)} {i.endMonth}
{i.name} {i.department} 正常 ¥{fmt(i.base)} {i.startMonth} {i.endMonth || '在保'}
) })()}
)}

社保/公积金基数按上年度月均工资核定,每人不同,在员工基本信息中设置。比例和基数上下限按版本管理,通常每年7月调整。 发薪批次计算时按批次月份自动匹配对应版本配置。

) }