import { useState, useEffect } from 'react' import { useSearchParams } from 'react-router-dom' import { toast } from 'sonner' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Calculator, Check, Plus, Download, AlertCircle, Clock } from 'lucide-react' import { InlineAlert } from '../components/ui/InlineAlert' import PageGuide from '../components/ui/PageGuide' import { socialInsuranceApi, socialAccountApi } from '../lib/api-services' import Card from '../components/ui/Card' import Button from '../components/ui/Button' import { Input } from '../components/ui/Input' import { MonthlyRow, MonthlyHousingRow } from './social-insurance/MonthlyRows' import SpecialDeductionTab from './social-insurance/SpecialDeductionTab' import EmployeeEnrollmentTab from './social-insurance/EmployeeEnrollmentTab' import { AccountCard } from './social-insurance/AccountCard' import { AccountFormModal } from './social-insurance/AccountFormModal' // 金额格式化:保留两位小数 + 千分位 const fmt = (n: number) => (n || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) export default function SocialInsurance() { const queryClient = useQueryClient() const [searchParams] = useSearchParams() const initialTab = (searchParams.get('tab') as 'monthly' | 'social' | 'housing' | 'deduction' | 'enrollment') || 'monthly' const [tab, setTab] = useState<'monthly' | 'social' | 'housing' | 'deduction' | 'enrollment'>(initialTab) const [deductionMonth, setDeductionMonth] = useState(new Date().toISOString().slice(0, 7)) 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) // 账户管理相关 state const [showAccountForm, setShowAccountForm] = useState(false) const [editAccount, setEditAccount] = useState(null) // 获取账户列表(按当前 tab 类型) const accountType = tab === 'housing' ? 'HOUSING' : 'SOCIAL' const { data: accounts = [] } = useQuery({ queryKey: ['social-accounts', accountType], queryFn: () => socialAccountApi.list(accountType), }) // 账户 CRUD mutations const createAccountMutation = useMutation({ mutationFn: (data: any) => socialAccountApi.create(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['social-accounts'] }) setShowAccountForm(false) toast.success('账户已创建') }, onError: (err: any) => toast.error(err?.response?.data?.error?.message || '创建失败'), }) const updateAccountMutation = useMutation({ mutationFn: ({ id, data }: { id: string; data: any }) => socialAccountApi.update(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['social-accounts'] }) setEditAccount(null) setShowAccountForm(false) toast.success('已更新') }, onError: (err: any) => toast.error(err?.response?.data?.error?.message || '更新失败'), }) const deleteAccountMutation = useMutation({ mutationFn: (id: string) => socialAccountApi.remove(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['social-accounts'] }) toast.success('已删除') }, onError: (err: any) => toast.error(err?.response?.data?.error?.message || '删除失败'), }) // 已办理月份列表(进入月度办理Tab时自动加载) const { data: processedList, refetch: refetchProcessedList } = useQuery({ queryKey: ['monthly-process-list'], queryFn: async () => { return await socialInsuranceApi.monthlyProcessList() }, enabled: tab === 'monthly', }) // 进入月度办理Tab时自动查询当前月状态 useEffect(() => { if (tab === 'monthly') { socialInsuranceApi.monthlyProcessStatus(monthlyMonth).then((res: any) => { setProcessStatus(res) }).catch(() => {}) refetchProcessedList() } }, [tab]) const { mutateAsync: fetchMonthlyChanges, isPending: monthlyLoading, data: monthlyChanges } = useMutation({ mutationFn: async () => { const [socialRes, housingRes, socialActiveRes, housingActiveRes] = await Promise.all([ socialInsuranceApi.monthlyChanges(monthlyMonth) as any, socialInsuranceApi.housingMonthlyChanges(monthlyMonth) as any, socialInsuranceApi.activeDeclaration(monthlyMonth) as any, socialInsuranceApi.housingActiveDeclaration(monthlyMonth) as any, ]) return { social: socialRes, housing: housingRes, socialActive: socialActiveRes, housingActive: housingActiveRes, } }, }) const handleMonthlyProcess = async () => { try { await fetchMonthlyChanges() setMonthlyProcessed(true) // 查询该月办理状态 const statusRes = await socialInsuranceApi.monthlyProcessStatus(monthlyMonth) as any setProcessStatus(statusRes) } 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 socialInsuranceApi.completeMonthlyProcess({ 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 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) } return (
社保公积金管理,维护社保、公积金缴费基数和比例,按月办理增减员。流程:①设置缴费基数和比例 → ②按月获取增减员名单 → ③完成办理。关联:社保基数影响薪税管理的工资计算;离职员工的社保截止请在离职管理中确认。

社保公积金

维护社保、公积金、商业保险缴费基数、版本及月度记录

{/* Tab 切换 */}
{(['monthly', 'social', 'housing', 'enrollment', 'deduction'] as const).map((t) => ( ))}
{/* ========== 社保 / 公积金 Tab:账户卡片列表 ========== */} {(tab === 'social' || tab === 'housing') && (
{/* 新建对应类型账户按钮 */}
{accounts.length === 0 ? (
暂无{tab === 'housing' ? '公积金' : '社保'}账户,点击上方「新建{tab === 'housing' ? '公积金' : '社保'}账户」创建
) : ( accounts.map((a: any) => ( { setEditAccount(acc); setShowAccountForm(true) }} onDelete={(acc) => deleteAccountMutation.mutate(acc.id)} /> )) )} {/* 账户新建/编辑弹窗 */} {showAccountForm && ( { setShowAccountForm(false); setEditAccount(null) }} onSubmit={async (data, departmentIds) => { if (editAccount) { updateAccountMutation.mutate({ id: editAccount.id, data }) if (departmentIds) { await socialAccountApi.linkDepartments(editAccount.id, departmentIds) toast.success('账户已更新,部门关联已同步') } } else { createAccountMutation.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={createAccountMutation.isPending || updateAccountMutation.isPending} /> )}
)} {/* ========== 月度办理 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 === 'enrollment' && ( )} {/* ========== 专项附加扣除 Tab ========== */} {tab === 'deduction' && ( )}
) }