feat: 城市变更功能完善及跨页面缓存刷新修复
- 城市变更使用CITY_CHANGE类型替代ADJUST,月度办理显示减员/新增(城市变更) - 在保人员查询排除本月已关闭记录(gte→gt)和本月新增记录(lte→lt) - 社保/公积金减员查询包含CITY_CHANGE类型 - 缴纳记录表格添加城市列显示 - 后端profile API从快照提取city字段 - 修复旧快照缺失city字段的数据 - 修复旧记录changeType为CITY_CHANGE,endMonth与新记录startMonth一致 - 城市变更必填原因,写入备注和审计日志 - 员工详情页添加变更历史Tab - 移除薪酬社保Tab下重复的参保城市变更子Tab - 全局修复跨页面mutation缓存刷新:调薪/调部门/离职/重新入职/批量续签/批量解聘/社保公积金调基/月度办理/撤销解聘均刷新roster-profile
This commit is contained in:
@@ -51,6 +51,7 @@ export default function Contracts() {
|
||||
queryClient.invalidateQueries({ queryKey: ['employees'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['roster'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['roster-profile'] })
|
||||
setShowAddModal(false)
|
||||
},
|
||||
})
|
||||
|
||||
+1012
-159
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
import { useState } from 'react'
|
||||
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 } from 'lucide-react'
|
||||
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'
|
||||
@@ -14,7 +14,7 @@ const fmt = (n: number) => (n || 0).toLocaleString('zh-CN', { minimumFractionDig
|
||||
export default function SocialInsurance() {
|
||||
const queryClient = useQueryClient()
|
||||
const confirm = useConfirm()
|
||||
const [tab, setTab] = useState<'social' | 'housing' | 'monthly'>('social')
|
||||
const [tab, setTab] = useState<'monthly' | 'social' | 'housing'>('monthly')
|
||||
const [city, setCity] = useState<string>('北京')
|
||||
const [base, setBase] = useState(8000)
|
||||
const [showNewVersion, setShowNewVersion] = useState(false)
|
||||
@@ -24,6 +24,8 @@ export default function SocialInsurance() {
|
||||
const [editItems, setEditItems] = useState<Record<string, number>>({})
|
||||
const [editingId, setEditingId] = useState<string | null>(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<any>({
|
||||
effectiveFrom: new Date().toISOString().slice(0, 7),
|
||||
city: '北京',
|
||||
@@ -83,9 +85,28 @@ export default function SocialInsurance() {
|
||||
enabled: showVersions && tab === 'housing',
|
||||
})
|
||||
|
||||
const { data: monthlyChanges } = useQuery<any>({
|
||||
queryKey: ['monthly-changes', monthlyMonth],
|
||||
// 已办理月份列表(进入月度办理Tab时自动加载)
|
||||
const { data: processedList, refetch: refetchProcessedList } = useQuery<any[]>({
|
||||
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<any>({
|
||||
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,
|
||||
@@ -99,7 +120,40 @@ export default function SocialInsurance() {
|
||||
housingActive: housingActiveRes.data,
|
||||
}
|
||||
},
|
||||
enabled: tab === 'monthly',
|
||||
})
|
||||
|
||||
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<any>({
|
||||
@@ -164,6 +218,8 @@ export default function SocialInsurance() {
|
||||
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({})
|
||||
@@ -178,6 +234,8 @@ export default function SocialInsurance() {
|
||||
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({})
|
||||
@@ -207,11 +265,15 @@ export default function SocialInsurance() {
|
||||
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 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)
|
||||
@@ -258,31 +320,33 @@ export default function SocialInsurance() {
|
||||
|
||||
{/* Tab 切换 + 城市选择 */}
|
||||
<div className="flex items-center gap-4 border-b">
|
||||
{(['social', 'housing', 'monthly'] as const).map((t) => (
|
||||
{(['monthly', 'social', 'housing'] 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); setShowVersions(false); setShowNewVersion(false); setShowAdjust(false); setAdjustData(null); setEditItems({}); setEditingId(null) }}
|
||||
onClick={() => { setTab(t); setShowVersions(false); setShowNewVersion(false); setShowAdjust(false); setAdjustData(null); setEditItems({}); setEditingId(null); setMonthlyProcessed(false); setProcessStatus(null) }}
|
||||
>
|
||||
{t === 'social' ? '社保' : t === 'housing' ? '公积金' : '月度办理'}
|
||||
{t === 'monthly' ? '月度办理' : t === 'social' ? '社保' : '公积金'}
|
||||
</button>
|
||||
))}
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
<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={city}
|
||||
onChange={(e) => setCity(e.target.value)}
|
||||
>
|
||||
{cities.length > 0 ? (
|
||||
cities.map((c) => <option key={c} value={c}>{c}</option>)
|
||||
) : (
|
||||
<option value="北京">北京</option>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
{tab !== 'monthly' && (
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
<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={city}
|
||||
onChange={(e) => setCity(e.target.value)}
|
||||
>
|
||||
{cities.length > 0 ? (
|
||||
cities.map((c) => <option key={c} value={c}>{c}</option>)
|
||||
) : (
|
||||
<option value="北京">北京</option>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ========== 社保 / 公积金 Tab ========== */}
|
||||
@@ -622,131 +686,218 @@ export default function SocialInsurance() {
|
||||
<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)} className="!w-32" />
|
||||
<Button variant="secondary" size="sm" onClick={() => monthlyChanges && handleExportCSV('social', monthlyChanges.social)}>
|
||||
<Download className="w-3.5 h-3.5 mr-1" />导出社保
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" onClick={() => monthlyChanges && handleExportCSV('housing', monthlyChanges.housing)}>
|
||||
<Download className="w-3.5 h-3.5 mr-1" />导出公积金
|
||||
<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>
|
||||
)}
|
||||
<div className="bg-blue-50 text-blue-700 text-sm px-3 py-2 rounded-md mb-3">
|
||||
展示当月社保/公积金新增(入职/重新入职)、减少(离职/解聘)及正常在保人员列表,用于经办机构申报。
|
||||
</div>
|
||||
{(() => {
|
||||
if (!monthlyChanges) return <div className="text-center py-4 text-gray-400 text-sm">加载中...</div>
|
||||
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?.subtractions || []
|
||||
const sSub = monthlyChanges.social?.reductions || []
|
||||
const sNormal = monthlyChanges.socialActive?.items || []
|
||||
const hAdd = monthlyChanges.housing?.additions || []
|
||||
const hSub = monthlyChanges.housing?.subtractions || []
|
||||
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" />)}
|
||||
{sub.map((i: any) => <MonthlyRow key={`ss-${city}-${i.employeeId}`} item={i} type="sub" />)}
|
||||
{normal.map((i: any) => <MonthlyRow key={`sn-${city}-${i.employeeId}`} item={i} type="normal" />)}
|
||||
</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" />)}
|
||||
{sub.map((i: any) => <MonthlyHousingRow key={`hs-${city}-${i.employeeId}`} item={i} type="sub" />)}
|
||||
{normal.map((i: any) => <MonthlyHousingRow key={`hn-${city}-${i.employeeId}`} item={i} type="normal" />)}
|
||||
</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">
|
||||
{/* 社保 */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">社保</h3>
|
||||
<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-left">开始年月</th>
|
||||
<th className="py-2 text-left">截止年月</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sAdd.map((i: any) => (
|
||||
<tr key={`sa-${i.employeeId}`} className="border-b last:border-0">
|
||||
<td className="py-1.5">{i.name}</td>
|
||||
<td className="py-1.5 text-gray-500">{i.department}</td>
|
||||
<td className="py-1.5"><span className="px-2 py-0.5 rounded bg-green-50 text-safe">新增</span></td>
|
||||
<td className="py-1.5 text-right">¥{fmt(i.base)}</td>
|
||||
<td className="py-1.5">{i.startMonth}</td>
|
||||
<td className="py-1.5 text-gray-400">—</td>
|
||||
</tr>
|
||||
))}
|
||||
{sSub.map((i: any) => (
|
||||
<tr key={`ss-${i.employeeId}`} className="border-b last:border-0">
|
||||
<td className="py-1.5">{i.name}</td>
|
||||
<td className="py-1.5 text-gray-500">{i.department}</td>
|
||||
<td className="py-1.5"><span className="px-2 py-0.5 rounded bg-red-50 text-danger">减少</span></td>
|
||||
<td className="py-1.5 text-right">¥{fmt(i.base)}</td>
|
||||
<td className="py-1.5 text-gray-400">—</td>
|
||||
<td className="py-1.5">{i.endMonth}</td>
|
||||
</tr>
|
||||
))}
|
||||
{sNormal.map((i: any) => (
|
||||
<tr key={`sn-${i.employeeId}`} className="border-b last:border-0">
|
||||
<td className="py-1.5">{i.name}</td>
|
||||
<td className="py-1.5 text-gray-500">{i.department}</td>
|
||||
<td className="py-1.5"><span className="px-2 py-0.5 rounded bg-gray-100 text-gray-500">正常</span></td>
|
||||
<td className="py-1.5 text-right">¥{fmt(i.base)}</td>
|
||||
<td className="py-1.5 text-gray-400">{i.startMonth}</td>
|
||||
<td className="py-1.5 text-gray-400">{i.endMonth || '在保'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{/* 公积金 */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">公积金</h3>
|
||||
<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-left">开始年月</th>
|
||||
<th className="py-2 text-left">截止年月</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{hAdd.map((i: any) => (
|
||||
<tr key={`ha-${i.employeeId}`} className="border-b last:border-0">
|
||||
<td className="py-1.5">{i.name}</td>
|
||||
<td className="py-1.5 text-gray-500">{i.department}</td>
|
||||
<td className="py-1.5"><span className="px-2 py-0.5 rounded bg-green-50 text-safe">新增</span></td>
|
||||
<td className="py-1.5 text-right">¥{fmt(i.base)}</td>
|
||||
<td className="py-1.5">{i.startMonth}</td>
|
||||
<td className="py-1.5 text-gray-400">—</td>
|
||||
</tr>
|
||||
))}
|
||||
{hSub.map((i: any) => (
|
||||
<tr key={`hs-${i.employeeId}`} className="border-b last:border-0">
|
||||
<td className="py-1.5">{i.name}</td>
|
||||
<td className="py-1.5 text-gray-500">{i.department}</td>
|
||||
<td className="py-1.5"><span className="px-2 py-0.5 rounded bg-red-50 text-danger">减少</span></td>
|
||||
<td className="py-1.5 text-right">¥{fmt(i.base)}</td>
|
||||
<td className="py-1.5 text-gray-400">—</td>
|
||||
<td className="py-1.5">{i.endMonth}</td>
|
||||
</tr>
|
||||
))}
|
||||
{hNormal.map((i: any) => (
|
||||
<tr key={`hn-${i.employeeId}`} className="border-b last:border-0">
|
||||
<td className="py-1.5">{i.name}</td>
|
||||
<td className="py-1.5 text-gray-500">{i.department}</td>
|
||||
<td className="py-1.5"><span className="px-2 py-0.5 rounded bg-gray-100 text-gray-500">正常</span></td>
|
||||
<td className="py-1.5 text-right">¥{fmt(i.base)}</td>
|
||||
<td className="py-1.5 text-gray-400">{i.startMonth}</td>
|
||||
<td className="py-1.5 text-gray-400">{i.endMonth || '在保'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{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>
|
||||
)
|
||||
})()}
|
||||
@@ -760,3 +911,72 @@ export default function SocialInsurance() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** 月度办理社保行组件(可展开查看各险种明细) */
|
||||
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 (
|
||||
<>
|
||||
<tr className="border-b last:border-0 hover:bg-gray-50 cursor-pointer" onClick={() => setExpanded(!expanded)}>
|
||||
<td className="py-1.5">{i.name} {d && <span className="text-gray-300 text-xs">{expanded ? '▾' : '▸'}</span>}</td>
|
||||
<td className="py-1.5 text-gray-500">{i.department}</td>
|
||||
<td className="py-1.5"><span className={`px-2 py-0.5 rounded text-xs ${typeClass}`}>{typeLabel}</span></td>
|
||||
<td className="py-1.5 text-right">¥{fmt(i.base)}</td>
|
||||
<td className="py-1.5 text-right text-danger">{d ? `¥${fmt(d.totalOrg)}` : '-'}</td>
|
||||
<td className="py-1.5 text-right text-warning">{d ? `¥${fmt(d.totalEmp)}` : '-'}</td>
|
||||
<td className="py-1.5 text-right font-medium text-primary">{d ? `¥${fmt(d.total)}` : '-'}</td>
|
||||
<td className="py-1.5 text-gray-400 text-xs">{type === 'add' ? `${i.startMonth} →` : type === 'sub' ? `→ ${i.endMonth}` : `${i.startMonth} ~ ${i.endMonth || '在保'}`}</td>
|
||||
</tr>
|
||||
{expanded && d && (
|
||||
<tr className="bg-gray-50/50">
|
||||
<td colSpan={8} className="py-2 px-8">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="border-b text-gray-400">
|
||||
<th className="py-1 text-left">险种</th>
|
||||
<th className="py-1 text-right">企业比例</th>
|
||||
<th className="py-1 text-right">个人比例</th>
|
||||
<th className="py-1 text-right">企业缴纳</th>
|
||||
<th className="py-1 text-right">个人缴纳</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{d.items.map((item: any) => (
|
||||
<tr key={item.name} className="border-b last:border-0">
|
||||
<td className="py-1">{item.name}</td>
|
||||
<td className="py-1 text-right text-gray-500">{item.orgRate}%</td>
|
||||
<td className="py-1 text-right text-gray-500">{item.empRate > 0 ? `${item.empRate}%` : '-'}</td>
|
||||
<td className="py-1 text-right">¥{fmt(item.orgAmount)}</td>
|
||||
<td className="py-1 text-right">{item.empAmount > 0 ? `¥${fmt(item.empAmount)}` : '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/** 月度办理公积金行组件 */
|
||||
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 (
|
||||
<tr className="border-b last:border-0 hover:bg-gray-50">
|
||||
<td className="py-1.5">{i.name}</td>
|
||||
<td className="py-1.5 text-gray-500">{i.department}</td>
|
||||
<td className="py-1.5"><span className={`px-2 py-0.5 rounded text-xs ${typeClass}`}>{typeLabel}</span></td>
|
||||
<td className="py-1.5 text-right">¥{fmt(i.base)}</td>
|
||||
<td className="py-1.5 text-right text-danger">{d ? `¥${fmt(d.orgAmount)}` : '-'}</td>
|
||||
<td className="py-1.5 text-right text-warning">{d ? `¥${fmt(d.empAmount)}` : '-'}</td>
|
||||
<td className="py-1.5 text-right font-medium text-primary">{d ? `¥${fmt(d.total)}` : '-'}</td>
|
||||
<td className="py-1.5 text-gray-400 text-xs">{type === 'add' ? `${i.startMonth} →` : type === 'sub' ? `→ ${i.endMonth}` : `${i.startMonth} ~ ${i.endMonth || '在保'}`}</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -329,6 +329,7 @@ export default function Termination() {
|
||||
queryClient.invalidateQueries({ queryKey: ['termination-drafts'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['roster'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['roster-profile'] })
|
||||
setView('list')
|
||||
},
|
||||
onError: () => toast.error('撤销失败'),
|
||||
|
||||
Reference in New Issue
Block a user