54d138cc73
按钮从页面顶部移到Tab内容区顶部,文案改为"新建社保账户"/"新建公积金账户", 明确表示新建的是当前Tab对应类型的账户。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
498 lines
26 KiB
TypeScript
498 lines
26 KiB
TypeScript
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<any>(null)
|
||
|
||
// 获取账户列表(按当前 tab 类型)
|
||
const accountType = tab === 'housing' ? 'HOUSING' : 'SOCIAL'
|
||
const { data: accounts = [] } = useQuery<any[]>({
|
||
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<any[]>({
|
||
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<any>({
|
||
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 (
|
||
<div className="space-y-3">
|
||
<PageGuide>
|
||
社保公积金管理,维护社保、公积金缴费基数和比例,按月办理增减员。流程:①设置缴费基数和比例 → ②按月获取增减员名单 → ③完成办理。关联:社保基数影响薪税管理的工资计算;离职员工的社保截止请在离职管理中确认。
|
||
</PageGuide>
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<Calculator className="h-5 w-5 text-primary" />
|
||
<div>
|
||
<h1 className="text-base font-semibold">社保公积金</h1>
|
||
<p className="mt-1 text-sm text-gray-500">维护社保、公积金、商业保险缴费基数、版本及月度记录</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Tab 切换 */}
|
||
<div className="flex items-center gap-4 border-b">
|
||
{(['monthly', 'social', 'housing', 'enrollment', 'deduction'] as const).map((t) => (
|
||
<button
|
||
key={t}
|
||
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
||
tab === t ? 'border-primary text-primary' : 'border-transparent text-gray-500 hover:text-gray-700'
|
||
}`}
|
||
onClick={() => { setTab(t); setMonthlyProcessed(false); setProcessStatus(null) }}
|
||
>
|
||
{t === 'monthly' ? '月度办理' : t === 'social' ? '社保' : t === 'housing' ? '公积金' : t === 'enrollment' ? '员工参保' : '专项附加扣除'}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* ========== 社保 / 公积金 Tab:账户卡片列表 ========== */}
|
||
{(tab === 'social' || tab === 'housing') && (
|
||
<div className="space-y-3">
|
||
{/* 新建对应类型账户按钮 */}
|
||
<div className="flex justify-end">
|
||
<Button size="sm" onClick={() => { setEditAccount(null); setShowAccountForm(true) }}>
|
||
<Plus className="w-4 h-4 mr-1" />新建{tab === 'housing' ? '公积金' : '社保'}账户
|
||
</Button>
|
||
</div>
|
||
{accounts.length === 0 ? (
|
||
<Card>
|
||
<div className="text-center py-8 text-gray-500">
|
||
暂无{tab === 'housing' ? '公积金' : '社保'}账户,点击上方「新建{tab === 'housing' ? '公积金' : '社保'}账户」创建
|
||
</div>
|
||
</Card>
|
||
) : (
|
||
accounts.map((a: any) => (
|
||
<AccountCard
|
||
key={a.id}
|
||
account={a}
|
||
isHousing={tab === 'housing'}
|
||
onEdit={(acc) => { setEditAccount(acc); setShowAccountForm(true) }}
|
||
onDelete={(acc) => deleteAccountMutation.mutate(acc.id)}
|
||
/>
|
||
))
|
||
)}
|
||
|
||
{/* 账户新建/编辑弹窗 */}
|
||
{showAccountForm && (
|
||
<AccountFormModal
|
||
type={accountType}
|
||
account={editAccount}
|
||
onClose={() => { 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}
|
||
/>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* ========== 月度办理 Tab ========== */}
|
||
{tab === 'monthly' && (
|
||
<div className="space-y-3">
|
||
<PageGuide>
|
||
月度办理用于按月获取社保增减员名单并完成办理。流程:①选择月份点击「获取」→ ②系统自动汇总新增、减员、正常缴费人员 → ③确认无误后点击「完成办理」保存记录。办理完成后自动更新员工社保状态。关联:社保基数影响「薪税管理」的工资计算;离职员工的社保截止请在「离职管理」中确认。
|
||
</PageGuide>
|
||
<Card>
|
||
<div className="flex items-center justify-between mb-3">
|
||
<h2 className="text-sm font-medium">月度办理</h2>
|
||
<div className="flex items-center gap-2">
|
||
<Input type="month" value={monthlyMonth} onChange={(e) => { setMonthlyMonth(e.target.value); setMonthlyProcessed(false); setProcessStatus(null) }} className="!w-32" />
|
||
<Button size="sm" onClick={handleMonthlyProcess} disabled={monthlyLoading}>
|
||
{monthlyLoading ? '获取中...' : '获取'}
|
||
</Button>
|
||
{monthlyProcessed && monthlyChanges && (
|
||
<>
|
||
<Button variant="secondary" size="sm" onClick={() => handleExportCSV('social', monthlyChanges.social)}>
|
||
<Download className="w-3.5 h-3.5 mr-1" />导出社保
|
||
</Button>
|
||
<Button variant="secondary" size="sm" onClick={() => handleExportCSV('housing', monthlyChanges.housing)}>
|
||
<Download className="w-3.5 h-3.5 mr-1" />导出公积金
|
||
</Button>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
{/* 办理状态总览:近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 (
|
||
<div className="mb-3 p-3 bg-gray-50 rounded-md">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<Clock className="w-3.5 h-3.5 text-gray-400" />
|
||
<span className="text-xs font-medium text-gray-600">办理状态总览(近6个月)</span>
|
||
</div>
|
||
<div className="flex gap-2 flex-wrap">
|
||
{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 (
|
||
<button
|
||
key={m}
|
||
onClick={() => { setMonthlyMonth(m); setMonthlyProcessed(false); setProcessStatus(null) }}
|
||
className={`px-3 py-1.5 rounded-md text-xs border transition-all ${isCurrent ? 'ring-2 ring-primary/20 border-primary' : 'border-gray-200'} ${allDone ? 'bg-green-50' : partial ? 'bg-amber-50' : 'bg-white hover:bg-gray-100'}`}
|
||
>
|
||
<div className="font-medium">{m}</div>
|
||
<div className="flex gap-1 mt-0.5">
|
||
<span className={`px-1 rounded text-[10px] ${sDone ? 'bg-green-100 text-safe' : 'bg-gray-100 text-gray-400'}`}>社保{sDone ? '✓' : '×'}</span>
|
||
<span className={`px-1 rounded text-[10px] ${hDone ? 'bg-green-100 text-safe' : 'bg-gray-100 text-gray-400'}`}>公积金{hDone ? '✓' : '×'}</span>
|
||
</div>
|
||
</button>
|
||
)
|
||
})}
|
||
</div>
|
||
{(() => {
|
||
const sDone = socialMonths.has(currentMonth)
|
||
const hDone = housingMonths.has(currentMonth)
|
||
if (sDone && hDone) return <div className="mt-2 text-xs text-safe flex items-center gap-1"><Check className="w-3.5 h-3.5" />{currentMonth} 社保和公积金均已办理完成</div>
|
||
if (sDone || hDone) return <div className="mt-2 text-xs text-amber-600 flex items-center gap-1"><AlertCircle className="w-3.5 h-3.5" />{currentMonth} {sDone ? '公积金' : '社保'}尚未办理完成</div>
|
||
return <div className="mt-2 text-xs text-gray-500 flex items-center gap-1"><AlertCircle className="w-3.5 h-3.5" />{currentMonth} 社保和公积金均未办理</div>
|
||
})()}
|
||
</div>
|
||
)
|
||
})()}
|
||
{/* 办理完成按钮区 */}
|
||
{monthlyProcessed && monthlyChanges && (
|
||
<div className="flex items-center gap-3 mb-3 pb-3 border-b">
|
||
<Button size="sm" onClick={() => completeProcessMutation.mutate('SOCIAL')} disabled={completeProcessMutation.isPending}>
|
||
{processStatus?.social ? '重新办理完成(社保)' : '办理完成(社保)'}
|
||
</Button>
|
||
{processStatus?.social && (
|
||
<span className="text-xs text-safe flex items-center gap-1">
|
||
<Check className="w-3.5 h-3.5" />社保已办理 {new Date(processStatus.social.processedAt).toLocaleString('zh-CN')}
|
||
</span>
|
||
)}
|
||
<Button size="sm" onClick={() => completeProcessMutation.mutate('HOUSING')} disabled={completeProcessMutation.isPending}>
|
||
{processStatus?.housing ? '重新办理完成(公积金)' : '办理完成(公积金)'}
|
||
</Button>
|
||
{processStatus?.housing && (
|
||
<span className="text-xs text-safe flex items-center gap-1">
|
||
<Check className="w-3.5 h-3.5" />公积金已办理 {new Date(processStatus.housing.processedAt).toLocaleString('zh-CN')}
|
||
</span>
|
||
)}
|
||
</div>
|
||
)}
|
||
<InlineAlert type="info" className="mb-3">
|
||
展示当月社保/公积金新增(入职/重新入职)、减少(离职/解聘)及正常在保人员列表,用于经办机构申报。
|
||
</InlineAlert>
|
||
{(() => {
|
||
if (!monthlyProcessed) {
|
||
return <div className="text-center py-8 text-gray-400 text-sm">选择月份后点击「获取」按钮,获取当月增减员及在保人员列表</div>
|
||
}
|
||
if (monthlyLoading) return <div className="text-center py-4 text-gray-400 text-sm">加载中...</div>
|
||
if (!monthlyChanges) return <div className="text-center py-4 text-gray-400 text-sm">无数据</div>
|
||
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 <div className="text-center py-4 text-gray-400 text-sm">{monthlyMonth} 无办理记录</div>
|
||
}
|
||
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 (
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-sm">
|
||
<thead>
|
||
<tr className="border-b text-xs text-gray-500">
|
||
<th className="py-2 text-left">姓名</th>
|
||
<th className="py-2 text-left">部门</th>
|
||
<th className="py-2 text-left">类型</th>
|
||
<th className="py-2 text-right">基数</th>
|
||
<th className="py-2 text-right">企业部分</th>
|
||
<th className="py-2 text-right">个人部分</th>
|
||
<th className="py-2 text-right">合计</th>
|
||
<th className="py-2 text-left">起止年月</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{add.map((i: any) => <MonthlyRow key={`sa-${city}-${i.employeeId}`} item={i} type="add" onCorrected={handleMonthlyProcess} />)}
|
||
{sub.map((i: any) => <MonthlyRow key={`ss-${city}-${i.employeeId}`} item={i} type="sub" onCorrected={handleMonthlyProcess} />)}
|
||
{normal.map((i: any) => <MonthlyRow key={`sn-${city}-${i.employeeId}`} item={i} type="normal" onCorrected={handleMonthlyProcess} />)}
|
||
</tbody>
|
||
{(add.length > 0 || normal.length > 0) && (
|
||
<tfoot>
|
||
<tr className="border-t-2 bg-gray-50 font-medium">
|
||
<td className="py-2" colSpan={4}>社保小计</td>
|
||
<td className="py-2 text-right text-danger">¥{fmt([...add, ...normal].reduce((s: number, i: any) => s + (i.detail?.totalOrg || 0), 0))}</td>
|
||
<td className="py-2 text-right text-warning">¥{fmt([...add, ...normal].reduce((s: number, i: any) => s + (i.detail?.totalEmp || 0), 0))}</td>
|
||
<td className="py-2 text-right font-bold text-primary">¥{fmt([...add, ...normal].reduce((s: number, i: any) => s + (i.detail?.total || 0), 0))}</td>
|
||
<td></td>
|
||
</tr>
|
||
</tfoot>
|
||
)}
|
||
</table>
|
||
{cfg && <div className="text-xs text-gray-400 mt-1">配置版本:{cfg.effectiveFrom} | 基数范围 ¥{fmt(cfg.baseMin)}~¥{fmt(cfg.baseMax)}</div>}
|
||
</div>
|
||
)
|
||
}
|
||
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 (
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-sm">
|
||
<thead>
|
||
<tr className="border-b text-xs text-gray-500">
|
||
<th className="py-2 text-left">姓名</th>
|
||
<th className="py-2 text-left">部门</th>
|
||
<th className="py-2 text-left">类型</th>
|
||
<th className="py-2 text-right">基数</th>
|
||
<th className="py-2 text-right">企业部分</th>
|
||
<th className="py-2 text-right">个人部分</th>
|
||
<th className="py-2 text-right">合计</th>
|
||
<th className="py-2 text-left">起止年月</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{add.map((i: any) => <MonthlyHousingRow key={`ha-${city}-${i.employeeId}`} item={i} type="add" onCorrected={handleMonthlyProcess} />)}
|
||
{sub.map((i: any) => <MonthlyHousingRow key={`hs-${city}-${i.employeeId}`} item={i} type="sub" onCorrected={handleMonthlyProcess} />)}
|
||
{normal.map((i: any) => <MonthlyHousingRow key={`hn-${city}-${i.employeeId}`} item={i} type="normal" onCorrected={handleMonthlyProcess} />)}
|
||
</tbody>
|
||
{(add.length > 0 || normal.length > 0) && (
|
||
<tfoot>
|
||
<tr className="border-t-2 bg-gray-50 font-medium">
|
||
<td className="py-2" colSpan={4}>公积金小计</td>
|
||
<td className="py-2 text-right text-danger">¥{fmt([...add, ...normal].reduce((s: number, i: any) => s + (i.detail?.orgAmount || 0), 0))}</td>
|
||
<td className="py-2 text-right text-warning">¥{fmt([...add, ...normal].reduce((s: number, i: any) => s + (i.detail?.empAmount || 0), 0))}</td>
|
||
<td className="py-2 text-right font-bold text-primary">¥{fmt([...add, ...normal].reduce((s: number, i: any) => s + (i.detail?.total || 0), 0))}</td>
|
||
<td></td>
|
||
</tr>
|
||
</tfoot>
|
||
)}
|
||
</table>
|
||
{cfg && <div className="text-xs text-gray-400 mt-1">配置版本:{cfg.effectiveFrom} | 企业 {cfg.housingOrg}% / 个人 {cfg.housingEmp}%</div>}
|
||
</div>
|
||
)
|
||
}
|
||
return (
|
||
<div className="space-y-4">
|
||
{allCities.map((city) => {
|
||
const sTable = renderSocialTable(city)
|
||
const hTable = renderHousingTable(city)
|
||
if (!sTable && !hTable) return null
|
||
return (
|
||
<div key={city} className="border rounded-lg p-3">
|
||
<h3 className="text-sm font-medium mb-3 flex items-center gap-2">
|
||
<span className="px-2 py-0.5 rounded bg-indigo-50 text-indigo-600 text-xs">{city}</span>
|
||
<span className="text-gray-400 text-xs">向{city}社保/公积金经办机构申报</span>
|
||
</h3>
|
||
{sTable && <div className="mb-3"><h4 className="text-xs font-medium text-gray-600 mb-1">社保</h4>{sTable}</div>}
|
||
{hTable && <div><h4 className="text-xs font-medium text-gray-600 mb-1">公积金</h4>{hTable}</div>}
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
)
|
||
})()}
|
||
</Card>
|
||
</div>
|
||
)}
|
||
|
||
{/* ========== 员工参保信息 Tab ========== */}
|
||
{tab === 'enrollment' && (
|
||
<EmployeeEnrollmentTab />
|
||
)}
|
||
|
||
{/* ========== 专项附加扣除 Tab ========== */}
|
||
{tab === 'deduction' && (
|
||
<SpecialDeductionTab month={deductionMonth} setMonth={setDeductionMonth} />
|
||
)}
|
||
|
||
</div>
|
||
)
|
||
}
|
||
|