import { useState, useEffect } from 'react' 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 } 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 confirm = useConfirm() const [tab, setTab] = useState<'monthly' | 'social' | 'housing' | 'deduction'>('monthly') const [city, setCity] = useState('北京') const [base, setBase] = useState(8000) const [deductionMonth, setDeductionMonth] = useState(new Date().toISOString().slice(0, 7)) 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 [monthlyProcessed, setMonthlyProcessed] = useState(false) const [processStatus, setProcessStatus] = useState<{ social: any; housing: any } | null>(null) 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, medicalBaseMin: 0, medicalBaseMax: 0, extraInsurances: [], }) 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', }) // 已办理月份列表(进入月度办理Tab时自动加载) const { data: processedList, refetch: refetchProcessedList } = useQuery({ queryKey: ['monthly-process-list'], queryFn: async () => { const res = await api.get('/social/monthly-process/list') as any return res.data }, enabled: tab === 'monthly', }) // 进入月度办理Tab时自动查询当前月状态 useEffect(() => { if (tab === 'monthly') { api.get('/social/monthly-process/status', { params: { month: monthlyMonth } }).then((res: any) => { setProcessStatus(res.data) }).catch(() => {}) refetchProcessedList() } }, [tab]) const { mutateAsync: fetchMonthlyChanges, isPending: monthlyLoading, data: monthlyChanges } = useMutation({ mutationFn: 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, } }, }) const handleMonthlyProcess = async () => { try { await fetchMonthlyChanges() setMonthlyProcessed(true) // 查询该月办理状态 const statusRes = await api.get('/social/monthly-process/status', { params: { month: monthlyMonth } }) as any setProcessStatus(statusRes.data) } catch { toast.error('获取月度办理数据失败') } } const completeProcessMutation = useMutation({ mutationFn: async (type: 'SOCIAL' | 'HOUSING') => { const snapshot = type === 'SOCIAL' ? monthlyChanges.social : monthlyChanges.housing const activeSnapshot = type === 'SOCIAL' ? monthlyChanges.socialActive : monthlyChanges.housingActive const res = await api.post('/social/monthly-process/complete', { month: monthlyMonth, type, snapshot: { changes: snapshot, active: activeSnapshot }, }) as any return res.data }, onSuccess: (data: any, type: 'SOCIAL' | 'HOUSING') => { setProcessStatus((prev: any) => ({ ...prev, [type === 'SOCIAL' ? 'social' : 'housing']: data })) refetchProcessedList() queryClient.invalidateQueries({ queryKey: ['roster-profile'] }) toast.success(`${type === 'SOCIAL' ? '社保' : '公积金'}月度办理已完成并保存`) }, onError: () => { toast.error('保存办理记录失败') }, }) 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'] }) queryClient.invalidateQueries({ queryKey: ['roster-profile'] }) queryClient.invalidateQueries({ queryKey: ['roster'] }) 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'] }) queryClient.invalidateQueries({ queryKey: ['roster-profile'] }) queryClient.invalidateQueries({ queryKey: ['roster'] }) 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) => { const d = i.detail if (type === 'social') { return [i.name, i.department, i.base, d?.totalOrg || '', d?.totalEmp || '', d?.total || '', i.startMonth, i.endMonth || '', i.changeType] } return [i.name, i.department, i.base, d?.orgAmount || '', d?.empAmount || '', d?.total || '', 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 切换 + 城市选择 */}
{(['monthly', 'social', 'housing', 'deduction'] as const).map((t) => ( ))} {(tab === 'social' || tab === 'housing') && (
setCity(e.target.value)} placeholder="输入或选择城市" /> {cities.map((c) =>
)}
{/* ========== 社保 / 公积金 Tab ========== */} {(tab === 'social' || tab === 'housing') && ( (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.medicalBaseMin > 0 && (
医保基数下限¥{fmt(activeConfig.medicalBaseMin)}
)} {activeConfig.medicalBaseMax > 0 && (
医保基数上限¥{fmt(activeConfig.medicalBaseMax)}
)}
养老(企业/个人){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, medicalBaseMin: Number(e.target.value) })} />

填 0 时使用统一基数下限

activeSetNewVersion({ ...activeNewVersion, medicalBaseMax: Number(e.target.value) })} />

填 0 时使用统一基数上限

)} {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) })} />
)} {!isHousing && (
附加险种(大病险/长护险等)
{(activeNewVersion.extraInsurances || []).map((ins: any, idx: number) => (
{ const arr = [...(activeNewVersion.extraInsurances || [])]; arr[idx] = { ...ins, name: e.target.value }; activeSetNewVersion({ ...activeNewVersion, extraInsurances: arr }) }} />
{ins.baseType === 'fixed' ? ( <>
{ const arr = [...(activeNewVersion.extraInsurances || [])]; arr[idx] = { ...ins, fixedAmount: Number(e.target.value) }; activeSetNewVersion({ ...activeNewVersion, extraInsurances: arr }) }} />
{ const arr = [...(activeNewVersion.extraInsurances || [])]; arr[idx] = { ...ins, empFixedAmount: Number(e.target.value) }; activeSetNewVersion({ ...activeNewVersion, extraInsurances: arr }) }} />
) : ( <>
{ const arr = [...(activeNewVersion.extraInsurances || [])]; arr[idx] = { ...ins, orgRate: Number(e.target.value) }; activeSetNewVersion({ ...activeNewVersion, extraInsurances: arr }) }} />
{ const arr = [...(activeNewVersion.extraInsurances || [])]; arr[idx] = { ...ins, empRate: Number(e.target.value) }; activeSetNewVersion({ ...activeNewVersion, extraInsurances: arr }) }} />
)}
))}
)}
)} {/* 试算工具 */} {(tab === 'social' || tab === 'housing') && (

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

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

计算结果

{(() => { const r = isHousing ? housingResult : result if (!r) return
点击「开始计算」查看结果
if (isHousing) { return (
缴费基数:¥{fmt(r.actualBase)} {r.capped && (已封顶)} {r.floored && (已保底)} {r.configVersion && | 配置版本:{r.configVersion}}
企业缴纳 ¥{fmt(r.housingOrg)}
个人缴纳 ¥{fmt(r.housingEmp)}
总费用 ¥{fmt(r.total)}
企业承担 ¥{fmt(r.housingOrg)} + 个人承担 ¥{fmt(r.housingEmp)}
) } 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); setMonthlyProcessed(false); setProcessStatus(null) }} className="!w-32" /> {monthlyProcessed && monthlyChanges && ( <> )}
{/* 办理状态总览:近12个月时间线 */} {processedList && (() => { const now = new Date() const months: string[] = [] for (let i = 5; i >= 0; i--) { const d = new Date(now.getFullYear(), now.getMonth() - i, 1) months.push(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`) } const socialMonths = new Set(processedList.filter((r: any) => r.type === 'SOCIAL').map((r: any) => r.month)) const housingMonths = new Set(processedList.filter((r: any) => r.type === 'HOUSING').map((r: any) => r.month)) const currentMonth = monthlyMonth return (
办理状态总览(近6个月)
{months.map((m) => { const sDone = socialMonths.has(m) const hDone = housingMonths.has(m) const isCurrent = m === currentMonth const allDone = sDone && hDone const partial = (sDone || hDone) && !allDone return ( ) })}
{(() => { const sDone = socialMonths.has(currentMonth) const hDone = housingMonths.has(currentMonth) if (sDone && hDone) return
{currentMonth} 社保和公积金均已办理完成
if (sDone || hDone) return
{currentMonth} {sDone ? '公积金' : '社保'}尚未办理完成
return
{currentMonth} 社保和公积金均未办理
})()}
) })()} {/* 办理完成按钮区 */} {monthlyProcessed && monthlyChanges && (
{processStatus?.social && ( 社保已办理 {new Date(processStatus.social.processedAt).toLocaleString('zh-CN')} )} {processStatus?.housing && ( 公积金已办理 {new Date(processStatus.housing.processedAt).toLocaleString('zh-CN')} )}
)}
展示当月社保/公积金新增(入职/重新入职)、减少(离职/解聘)及正常在保人员列表,用于经办机构申报。
{(() => { if (!monthlyProcessed) { return
选择月份后点击「获取」按钮,获取当月增减员及在保人员列表
} if (monthlyLoading) return
加载中...
if (!monthlyChanges) return
无数据
const sAdd = monthlyChanges.social?.additions || [] const sSub = monthlyChanges.social?.reductions || [] const sNormal = monthlyChanges.socialActive?.items || [] const hAdd = monthlyChanges.housing?.additions || [] const hSub = monthlyChanges.housing?.reductions || [] 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} 无办理记录
} const sConfigs = monthlyChanges.social?.configs || {} const hConfigs = monthlyChanges.housing?.configs || {} // 收集所有涉及的城市 const allCities = [...new Set([ ...sAdd.map((i: any) => i.city), ...sSub.map((i: any) => i.city), ...sNormal.map((i: any) => i.city), ...hAdd.map((i: any) => i.city), ...hSub.map((i: any) => i.city), ...hNormal.map((i: any) => i.city), ])].filter(Boolean).sort() const renderSocialTable = (city: string) => { const add = sAdd.filter((i: any) => i.city === city) const sub = sSub.filter((i: any) => i.city === city) const normal = sNormal.filter((i: any) => i.city === city) if (add.length === 0 && sub.length === 0 && normal.length === 0) return null const cfg = sConfigs[city] return (
{add.map((i: any) => )} {sub.map((i: any) => )} {normal.map((i: any) => )} {(add.length > 0 || normal.length > 0) && ( )}
姓名 部门 类型 基数 企业部分 个人部分 合计 起止年月
社保小计 ¥{fmt([...add, ...normal].reduce((s: number, i: any) => s + (i.detail?.totalOrg || 0), 0))} ¥{fmt([...add, ...normal].reduce((s: number, i: any) => s + (i.detail?.totalEmp || 0), 0))} ¥{fmt([...add, ...normal].reduce((s: number, i: any) => s + (i.detail?.total || 0), 0))}
{cfg &&
配置版本:{cfg.effectiveFrom} | 基数范围 ¥{fmt(cfg.baseMin)}~¥{fmt(cfg.baseMax)}
}
) } const renderHousingTable = (city: string) => { const add = hAdd.filter((i: any) => i.city === city) const sub = hSub.filter((i: any) => i.city === city) const normal = hNormal.filter((i: any) => i.city === city) if (add.length === 0 && sub.length === 0 && normal.length === 0) return null const cfg = hConfigs[city] return (
{add.map((i: any) => )} {sub.map((i: any) => )} {normal.map((i: any) => )} {(add.length > 0 || normal.length > 0) && ( )}
姓名 部门 类型 基数 企业部分 个人部分 合计 起止年月
公积金小计 ¥{fmt([...add, ...normal].reduce((s: number, i: any) => s + (i.detail?.orgAmount || 0), 0))} ¥{fmt([...add, ...normal].reduce((s: number, i: any) => s + (i.detail?.empAmount || 0), 0))} ¥{fmt([...add, ...normal].reduce((s: number, i: any) => s + (i.detail?.total || 0), 0))}
{cfg &&
配置版本:{cfg.effectiveFrom} | 企业 {cfg.housingOrg}% / 个人 {cfg.housingEmp}%
}
) } return (
{allCities.map((city) => { const sTable = renderSocialTable(city) const hTable = renderHousingTable(city) if (!sTable && !hTable) return null return (

{city} 向{city}社保/公积金经办机构申报

{sTable &&

社保

{sTable}
} {hTable &&

公积金

{hTable}
}
) })}
) })()}
)} {/* ========== 专项附加扣除 Tab ========== */} {tab === 'deduction' && ( )}

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

) } /** 月度办理社保行组件(可展开查看各险种明细) */ function MonthlyRow({ item: i, type }: { item: any; type: 'add' | 'sub' | 'normal' }) { const [expanded, setExpanded] = useState(false) const typeLabel = type === 'add' ? (i.changeType === 'CITY_CHANGE' ? '新增(城市变更)' : '新增') : type === 'sub' ? (i.changeType === 'CITY_CHANGE' ? '减员(城市变更)' : '减员') : '正常' const typeClass = type === 'add' ? 'bg-green-50 text-safe' : type === 'sub' ? 'bg-red-50 text-danger' : 'bg-gray-100 text-gray-500' const d = i.detail return ( <> setExpanded(!expanded)}> {i.name} {d && {expanded ? '▾' : '▸'}} {i.department} {typeLabel} ¥{fmt(i.base)} {d ? `¥${fmt(d.totalOrg)}` : '-'} {d ? `¥${fmt(d.totalEmp)}` : '-'} {d ? `¥${fmt(d.total)}` : '-'} {type === 'add' ? `${i.startMonth} →` : type === 'sub' ? `→ ${i.endMonth}` : `${i.startMonth} ~ ${i.endMonth || '在保'}`} {expanded && d && ( {d.items.map((item: any) => ( ))}
险种 企业比例 个人比例 企业缴纳 个人缴纳
{item.name} {item.orgRate}% {item.empRate > 0 ? `${item.empRate}%` : '-'} ¥{fmt(item.orgAmount)} {item.empAmount > 0 ? `¥${fmt(item.empAmount)}` : '-'}
)} ) } /** 月度办理公积金行组件 */ function MonthlyHousingRow({ item: i, type }: { item: any; type: 'add' | 'sub' | 'normal' }) { const typeLabel = type === 'add' ? (i.changeType === 'CITY_CHANGE' ? '新增(城市变更)' : '新增') : type === 'sub' ? (i.changeType === 'CITY_CHANGE' ? '减员(城市变更)' : '减员') : '正常' const typeClass = type === 'add' ? 'bg-green-50 text-safe' : type === 'sub' ? 'bg-red-50 text-danger' : 'bg-gray-100 text-gray-500' const d = i.detail return ( {i.name} {i.department} {typeLabel} ¥{fmt(i.base)} {d ? `¥${fmt(d.orgAmount)}` : '-'} {d ? `¥${fmt(d.empAmount)}` : '-'} {d ? `¥${fmt(d.total)}` : '-'} {type === 'add' ? `${i.startMonth} →` : type === 'sub' ? `→ ${i.endMonth}` : `${i.startMonth} ~ ${i.endMonth || '在保'}`} ) } /** 专项附加扣除按月录入组件 */ function SpecialDeductionTab({ month, setMonth }: { month: string; setMonth: (m: string) => void }) { const queryClient = useQueryClient() const [editing, setEditing] = useState(null) const [editForm, setEditForm] = useState(null) // 查询当月所有员工的专项附加扣除 const { data: records = [], isLoading } = useQuery({ queryKey: ['special-deduction', month], queryFn: async () => { const res = await api.get('/social/special-deduction/batch', { params: { month } }) as any return res.data }, }) // 查询所有员工列表(用于添加未录入的员工) const { data: employees = [] } = useQuery({ queryKey: ['employees-for-deduction'], queryFn: async () => { const res = await api.get('/roster', { params: { pageSize: 999 } }) as any return res.data?.items || res.data || [] }, }) // 查询上月专项附加扣除数据(用于「复制上月」功能) const prevMonth = (() => { const [y, m] = month.split('-').map(Number) const d = new Date(y, m - 2, 1) return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}` })() const { data: prevRecords = [] } = useQuery({ queryKey: ['special-deduction', prevMonth], queryFn: async () => { const res = await api.get('/social/special-deduction/batch', { params: { month: prevMonth } }) as any return res.data || [] }, }) const prevRecordMap = new Map(prevRecords.map((r: any) => [r.employeeId, r])) const saveMutation = useMutation({ mutationFn: (data: any) => api.post('/social/special-deduction', { ...data, month }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['special-deduction'] }) setEditing(null) setEditForm(null) }, }) const recordMap = new Map(records.map((r: any) => [r.employeeId, r])) const unrecorded = employees.filter((e: any) => !recordMap.has(e.id)) const startEdit = (empId: string, existing?: any) => { setEditing(empId) setEditForm(existing ? { children: existing.children, elderly: existing.elderly, housing: existing.housing, education: existing.education, infant: existing.infant, remark: existing.remark, } : { children: 0, elderly: 0, housing: 0, education: 0, infant: 0, remark: '' }) } /** 复制上月数据到编辑表单 */ const copyFromPrevMonth = (empId: string) => { const prev = prevRecordMap.get(empId) if (prev) { setEditForm({ children: prev.children || 0, elderly: prev.elderly || 0, housing: prev.housing || 0, education: prev.education || 0, infant: prev.infant || 0, remark: prev.remark || '', }) } else { toast.info(`${prevMonth} 无该员工的扣除记录`) } } const calcTotal = (f: any) => (f.children || 0) + (f.elderly || 0) + (f.housing || 0) + (f.education || 0) + (f.infant || 0) return (

专项附加扣除 — {month}

setMonth(e.target.value)} className="!w-32" />
{isLoading ? (
加载中...
) : (
{/* 已录入列表 */} {records.length > 0 && (
{records.map((r: any) => ( {editing === r.employeeId ? ( <> ) : ( <> )} ))}
姓名 部门 子女教育 赡养老人 住房 继续教育 婴幼儿照护 合计 备注
{r.employee?.name} {r.employee?.department} setEditForm({ ...editForm, children: Number(e.target.value) })} /> setEditForm({ ...editForm, elderly: Number(e.target.value) })} /> setEditForm({ ...editForm, housing: Number(e.target.value) })} /> setEditForm({ ...editForm, education: Number(e.target.value) })} /> setEditForm({ ...editForm, infant: Number(e.target.value) })} /> ¥{fmt(calcTotal(editForm))} setEditForm({ ...editForm, remark: e.target.value })} />
{r.employee?.name} {r.employee?.department} {r.children > 0 ? `¥${fmt(r.children)}` : '-'} {r.elderly > 0 ? `¥${fmt(r.elderly)}` : '-'} {r.housing > 0 ? `¥${fmt(r.housing)}` : '-'} {r.education > 0 ? `¥${fmt(r.education)}` : '-'} {r.infant > 0 ? `¥${fmt(r.infant)}` : '-'} ¥{fmt(r.amount)} {r.remark || '-'}
)} {/* 未录入员工 */} {unrecorded.length > 0 && (

未录入员工({unrecorded.length}人)

{unrecorded.map((e: any) => ( ))}
)} {/* 新增/编辑表单 */} {editing && !recordMap.has(editing) && (

新增专项附加扣除 — {employees.find((e: any) => e.id === editing)?.name}

setEditForm({ ...editForm, children: Number(e.target.value) })} />
setEditForm({ ...editForm, elderly: Number(e.target.value) })} />
setEditForm({ ...editForm, housing: Number(e.target.value) })} />
setEditForm({ ...editForm, education: Number(e.target.value) })} />
setEditForm({ ...editForm, infant: Number(e.target.value) })} />
setEditForm({ ...editForm, remark: e.target.value })} />
合计:¥{fmt(calcTotal(editForm))}
)} {records.length === 0 && unrecorded.length === 0 && (
暂无员工数据
)}
)}
) }