Files
TurboHR/frontend/src/pages/SocialInsurance.tsx
T
selfrelease 0df8aa77d9 feat: AIHR 智能人力资源管理系统初始提交
- 员工花名册管理(加密存储、导入导出)
- 薪酬管理(发薪批次、薪酬模版、加班费计算、工资条)
- 社保公积金(多城市配置、版本管理、基数调整)
- 解聘管理(6步流程、证据链、工作交接)
- AI 助手(合同审查、风险预测、RAG 知识库)
- Dashboard 仪表盘
- 设置与通知
2026-07-24 13:53:11 +08:00

755 lines
41 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react'
import { toast } from 'sonner'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Calculator, Info, Check, Settings as SettingsIcon, Plus, History, Download } from 'lucide-react'
import api from '../lib/api'
import Card from '../components/ui/Card'
import Button from '../components/ui/Button'
import { Input, Label } from '../components/ui/Input'
// 金额格式化:保留两位小数 + 千分位
const fmt = (n: number) => (n || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
export default function SocialInsurance() {
const queryClient = useQueryClient()
const [tab, setTab] = useState<'social' | 'housing' | 'monthly'>('social')
const [city, setCity] = useState<string>('北京')
const [base, setBase] = useState(8000)
const [showNewVersion, setShowNewVersion] = useState(false)
const [showVersions, setShowVersions] = useState(false)
const [showAdjust, setShowAdjust] = useState(false)
const [adjustData, setAdjustData] = useState<any>(null)
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 [newVersion, setNewVersion] = useState<any>({
effectiveFrom: new Date().toISOString().slice(0, 7),
city: '北京',
pensionOrg: 16, pensionEmp: 8,
medicalOrg: 9.8, medicalEmp: 2,
unemploymentOrg: 0.5, unemploymentEmp: 0.5,
injuryOrg: 0.2, maternityOrg: 0.8,
baseMin: 6326, baseMax: 33891,
})
const [newHousingVersion, setNewHousingVersion] = useState<any>({
effectiveFrom: new Date().toISOString().slice(0, 7),
city: '北京',
housingOrg: 12, housingEmp: 12,
baseMin: 6326, baseMax: 33891,
})
// 获取城市列表
const { data: cities = [] } = useQuery<string[]>({
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<any>({
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<any>({
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<any[]>({
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<any[]>({
queryKey: ['housing-config-versions'],
queryFn: async () => {
const res = await api.get('/social/housing-config/versions') as any
return res.data
},
enabled: showVersions && tab === 'housing',
})
const { data: monthlyChanges } = useQuery<any>({
queryKey: ['monthly-changes', monthlyMonth],
queryFn: async () => {
const [socialRes, housingRes, socialActiveRes, housingActiveRes] = await Promise.all([
api.get('/social/monthly-changes', { params: { month: monthlyMonth } }) as any,
api.get('/social/housing/monthly-changes', { params: { month: monthlyMonth } }) as any,
api.get('/social/active-declaration', { params: { month: monthlyMonth } }) as any,
api.get('/social/housing/active-declaration', { params: { month: monthlyMonth } }) as any,
])
return {
social: socialRes.data,
housing: housingRes.data,
socialActive: socialActiveRes.data,
housingActive: housingActiveRes.data,
}
},
enabled: tab === 'monthly',
})
const { data: result, mutate: calcMutate, isPending } = useMutation<any>({
mutationFn: async () => {
const res = await api.post('/social/calculate', { base }) as any
return res.data
},
})
const { data: housingResult, mutate: calcHousingMutate, isPending: housingCalcPending } = useMutation<any>({
mutationFn: async () => {
const res = await api.post('/social/housing-calculate', { base }) as any
return res.data
},
})
const createVersionMutation = useMutation({
mutationFn: (data: any) => api.post('/social/config/versions', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['social-config'] })
queryClient.invalidateQueries({ queryKey: ['social-config-versions'] })
setShowNewVersion(false)
toast.success('新版本已创建,旧版本已自动归档')
},
})
const createHousingVersionMutation = useMutation({
mutationFn: (data: any) => api.post('/social/housing-config/versions', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['housing-config'] })
queryClient.invalidateQueries({ queryKey: ['housing-config-versions'] })
setShowNewVersion(false)
toast.success('公积金新版本已创建,旧版本已自动归档')
},
})
const previewAdjustMutation = useMutation({
mutationFn: async () => {
const res = await api.get(`/social/config/${config?.id}/adjust-preview`) as any
return res.data
},
onSuccess: (data) => {
setAdjustData(data)
setShowAdjust(true)
},
})
const previewHousingAdjustMutation = useMutation({
mutationFn: async () => {
const res = await api.get(`/social/housing-config/${housingConfig?.id}/adjust-preview`) as any
return res.data
},
onSuccess: (data) => {
setAdjustData(data)
setShowAdjust(true)
},
})
const applyAdjustMutation = useMutation({
mutationFn: (data: { items: { employeeId: string; newBase: number }[] }) =>
api.post(`/social/config/${config?.id}/adjust-apply`, data),
onSuccess: (res: any) => {
queryClient.invalidateQueries({ queryKey: ['social-config'] })
queryClient.invalidateQueries({ queryKey: ['social-config-versions'] })
setShowAdjust(false)
setAdjustData(null)
setEditItems({})
setEditingId(null)
toast.success(`调整完成,共调整 ${res.data?.adjusted || 0} 名员工的社保基数`)
},
})
const applyHousingAdjustMutation = useMutation({
mutationFn: (data: { items: { employeeId: string; newBase: number }[] }) =>
api.post(`/social/housing-config/${housingConfig?.id}/adjust-apply`, data),
onSuccess: (res: any) => {
queryClient.invalidateQueries({ queryKey: ['housing-config'] })
queryClient.invalidateQueries({ queryKey: ['housing-config-versions'] })
setShowAdjust(false)
setAdjustData(null)
setEditItems({})
setEditingId(null)
toast.success(`调整完成,共调整 ${res.data?.adjusted || 0} 名员工的公积金基数`)
},
})
const resetAdjustMutation = useMutation({
mutationFn: () => api.post(`/social/config/${config?.id}/reset-adjustment`, { city }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['social-config', city] })
queryClient.invalidateQueries({ queryKey: ['social-config-versions', city] })
toast.success('社保基数调整已重置,可以重新调整')
},
})
const resetHousingAdjustMutation = useMutation({
mutationFn: () => api.post(`/social/housing-config/${housingConfig?.id}/reset-adjustment`, { city }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['housing-config', city] })
queryClient.invalidateQueries({ queryKey: ['housing-config-versions', city] })
toast.success('公积金基数调整已重置,可以重新调整')
},
})
const handleExportCSV = (type: 'social' | 'housing', data: any) => {
if (!data?.items?.length) return
const headers = type === 'social'
? ['姓名', '部门', '社保基数', '开始年月', '截止年月', '变更类型']
: ['姓名', '部门', '公积金基数', '开始年月', '截止年月', '变更类型']
const rows = data.items.map((i: any) => [
i.name, i.department, i.base, i.startMonth, i.endMonth || '', i.changeType
])
const csv = [headers, ...rows].map(r => r.join(',')).join('\n')
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${type === 'social' ? '社保' : '公积金'}_${data.month || monthlyMonth}.csv`
a.click()
URL.revokeObjectURL(url)
}
const isHousing = tab === 'housing'
const activeConfig = isHousing ? housingConfig : config
const activeVersions = isHousing ? housingVersions : versions
const activePreviewMut = isHousing ? previewHousingAdjustMutation : previewAdjustMutation
const activeApplyMut = isHousing ? applyHousingAdjustMutation : applyAdjustMutation
const activeCreateMut = isHousing ? createHousingVersionMutation : createVersionMutation
const activeNewVersion = isHousing ? newHousingVersion : newVersion
const activeSetNewVersion = isHousing ? setNewHousingVersion : setNewVersion
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h1 className="text-xs font-medium"></h1>
<div className="flex gap-2">
{tab !== 'monthly' && (
<>
<Button variant="secondary" size="sm" onClick={() => setShowVersions(!showVersions)}>
<History className="w-4 h-4 mr-1" />
{showVersions ? '收起历史' : '版本历史'}
</Button>
<Button size="sm" onClick={() => setShowNewVersion(!showNewVersion)}>
<Plus className="w-4 h-4 mr-1" />
</Button>
</>
)}
</div>
</div>
{/* Tab 切换 + 城市选择 */}
<div className="flex items-center gap-4 border-b">
{(['social', 'housing', 'monthly'] as const).map((t) => (
<button
key={t}
className={`px-4 py-2 text-xs 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) }}
>
{t === 'social' ? '社保' : t === 'housing' ? '公积金' : '月度办理'}
</button>
))}
<div className="flex items-center gap-2 ml-auto">
<label className="text-xs text-gray-500">:</label>
<select
className="text-xs border rounded px-2 py-1.5"
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 ========== */}
{tab !== 'monthly' && (
(isHousing ? housingLoading : configLoading) ? (
<Card><div className="text-center py-8 text-gray-500">...</div></Card>
) : activeConfig ? (
<Card>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<span className="px-2 py-0.5 rounded text-xs bg-green-50 text-safe"></span>
<span className="text-xs text-gray-500">{activeConfig.effectiveFrom}</span>
<span className="text-xs text-gray-500">· {activeConfig.city}</span>
{activeConfig.adjustmentDone && (
<span className="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-500"></span>
)}
</div>
<div className="flex items-center gap-2">
{activeConfig.adjustmentDone && (
<Button
variant="secondary"
size="sm"
onClick={() => {
if (window.confirm(`确定要重置${isHousing ? '公积金' : '社保'}基数调整吗?重置后可重新调整。`)) {
isHousing ? resetHousingAdjustMutation.mutate() : resetAdjustMutation.mutate()
}
}}
disabled={isHousing ? resetHousingAdjustMutation.isPending : resetAdjustMutation.isPending}
>
<SettingsIcon className="w-4 h-4 mr-1" />
{isHousing ? resetHousingAdjustMutation.isPending ? '重置中...' : '重置调整' : resetAdjustMutation.isPending ? '重置中...' : '重置调整'}
</Button>
)}
<Button
variant="secondary"
size="sm"
onClick={() => activePreviewMut.mutate()}
disabled={activeConfig.adjustmentDone || activePreviewMut.isPending}
>
<SettingsIcon className="w-4 h-4 mr-1" />
{activeConfig.adjustmentDone ? '已调整' : activePreviewMut.isPending ? '加载中...' : `调整员工${isHousing ? '公积金' : '社保'}基数`}
</Button>
</div>
</div>
{isHousing ? (
<div className="grid md:grid-cols-4 gap-3 text-xs">
<div className="flex justify-between border-b pb-1.5"><span className="text-gray-500"></span><span className="font-medium">¥{fmt(activeConfig.baseMin)}</span></div>
<div className="flex justify-between border-b pb-1.5"><span className="text-gray-500"></span><span className="font-medium">¥{fmt(activeConfig.baseMax)}</span></div>
<div className="flex justify-between border-b pb-1.5"><span className="text-gray-500">()</span><span className="font-medium">{activeConfig.housingOrg}%</span></div>
<div className="flex justify-between border-b pb-1.5"><span className="text-gray-500">()</span><span className="font-medium">{activeConfig.housingEmp}%</span></div>
</div>
) : (
<div className="grid md:grid-cols-4 gap-3 text-xs">
<div className="flex justify-between border-b pb-1.5"><span className="text-gray-500"></span><span className="font-medium">¥{fmt(activeConfig.baseMin)}</span></div>
<div className="flex justify-between border-b pb-1.5"><span className="text-gray-500"></span><span className="font-medium">¥{fmt(activeConfig.baseMax)}</span></div>
<div className="flex justify-between border-b pb-1.5"><span className="text-gray-500">(/)</span><span className="font-medium">{activeConfig.pensionOrg}% / {activeConfig.pensionEmp}%</span></div>
<div className="flex justify-between border-b pb-1.5"><span className="text-gray-500">(/)</span><span className="font-medium">{activeConfig.medicalOrg}% / {activeConfig.medicalEmp}%</span></div>
<div className="flex justify-between border-b pb-1.5"><span className="text-gray-500">(/)</span><span className="font-medium">{activeConfig.unemploymentOrg}% / {activeConfig.unemploymentEmp}%</span></div>
<div className="flex justify-between border-b pb-1.5"><span className="text-gray-500">()</span><span className="font-medium">{activeConfig.injuryOrg}%</span></div>
<div className="flex justify-between border-b pb-1.5"><span className="text-gray-500">()</span><span className="font-medium">{activeConfig.maternityOrg}%</span></div>
</div>
)}
</Card>
) : (
<Card><div className="text-center py-8 text-gray-500">{isHousing ? '公积金' : '社保'}</div></Card>
)
)}
{/* 调整预览 */}
{tab !== 'monthly' && showAdjust && adjustData && (
<Card>
<h3 className="text-xs font-medium mb-3 flex items-center gap-2">
<SettingsIcon className="w-4 h-4" />{isHousing ? '公积金' : '社保'}
</h3>
<div className="bg-blue-50 text-blue-700 text-xs px-3 py-2 rounded-md flex items-start gap-2 mb-3">
<Info className="w-4 h-4 mt-0.5 shrink-0" />
<div>
¥{fmt(adjustData.baseMin)} ~ ¥{fmt(adjustData.baseMax)}{isHousing ? '公积金' : '社保'}
=
</div>
</div>
<div className="flex items-center gap-2 mb-3">
<Button variant="secondary" size="sm" onClick={() => {
const newEdits: Record<string, number> = {}
adjustData.items.forEach((i: any) => { newEdits[i.employeeId] = i.suggestedBase })
setEditItems(newEdits)
}}>
<Check className="w-3.5 h-3.5 mr-1" />
</Button>
<Button variant="secondary" size="sm" onClick={() => {
const newEdits: Record<string, number> = {}
adjustData.items.forEach((i: any) => { newEdits[i.employeeId] = i.oldBase })
setEditItems(newEdits)
}}>
</Button>
<span className="text-xs text-gray-400"> {adjustData.total} </span>
</div>
<div className="overflow-x-auto mb-4">
<table className="w-full text-xs">
<thead>
<tr className="border-b text-gray-500">
<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">{isHousing ? '公积金' : '社保'}()</th>
<th className="py-2 text-right">{isHousing ? '公积金' : '社保'}()</th>
<th className="py-2 text-right">{isHousing ? '公积金' : '社保'}()</th>
</tr>
</thead>
<tbody>
{adjustData.items.map((item: any) => {
const edit = editItems[item.employeeId]
const newBase = edit ?? item.suggestedBase
const changed = newBase !== item.oldBase
return (
<tr key={item.employeeId} className="border-b last:border-0">
<td className="py-1.5">{item.name}</td>
<td className="py-1.5 text-gray-500">{item.department}</td>
<td className="py-1.5 text-right text-gray-400">¥{fmt(item.avgSalary)}</td>
<td className="py-1.5 text-right text-gray-400">¥{fmt(item.oldBase)}</td>
<td className="py-1.5 text-right text-gray-500">¥{fmt(item.suggestedBase)}</td>
<td className="py-1.5 text-right">
{editingId === item.employeeId ? (
<Input type="number" step="0.01" min="0" className="!w-28 text-right text-xs" value={newBase}
onChange={(e) => setEditItems({ ...editItems, [item.employeeId]: Number(e.target.value) || 0 })}
onBlur={() => setEditingId(null)}
autoFocus />
) : (
<span className="cursor-text inline-block !w-28 text-right"
onClick={() => setEditingId(item.employeeId)}>
¥{fmt(newBase)}
</span>
)}
{changed && <span className="text-warning ml-1"></span>}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
<div className="flex gap-2">
<Button onClick={() => {
const items = adjustData.items.map((i: any) => ({ employeeId: i.employeeId, newBase: editItems[i.employeeId] ?? i.suggestedBase }))
activeApplyMut.mutate({ items })
}} disabled={activeApplyMut.isPending}>
{activeApplyMut.isPending ? '保存中...' : '确认保存'}
</Button>
<Button variant="secondary" onClick={() => { setShowAdjust(false); setAdjustData(null); setEditItems({}); setEditingId(null) }}></Button>
</div>
</Card>
)}
{/* 版本历史 */}
{tab !== 'monthly' && showVersions && (
<Card>
<h3 className="text-xs font-medium mb-3 flex items-center gap-2"><History className="w-4 h-4" />{isHousing ? '公积金' : '社保'}</h3>
{!activeVersions || activeVersions.length === 0 ? (
<div className="text-center py-4 text-gray-400 text-xs"></div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="border-b 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>
{isHousing ? (
<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-center"></th>
</tr>
</thead>
<tbody>
{activeVersions.map((v: any) => (
<tr key={v.id} className="border-b last:border-0 hover:bg-gray-50">
<td className="py-2">{v.effectiveFrom}</td>
<td className="py-2 text-gray-400">{v.effectiveTo || '—'}</td>
<td className="py-2">{v.city}</td>
<td className="py-2 text-right">¥{fmt(v.baseMin)}</td>
<td className="py-2 text-right">¥{fmt(v.baseMax)}</td>
{isHousing ? (
<td className="py-2 text-right text-gray-500">{v.housingOrg}/{v.housingEmp}</td>
) : (
<>
<td className="py-2 text-right text-gray-500">{v.pensionOrg}/{v.pensionEmp}</td>
<td className="py-2 text-right text-gray-500">{v.medicalOrg}/{v.medicalEmp}</td>
</>
)}
<td className="py-2 text-center">
{v.isCurrent ? <span className="px-2 py-0.5 rounded bg-green-50 text-safe"></span> : <span className="px-2 py-0.5 rounded bg-gray-100 text-gray-400"></span>}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</Card>
)}
{/* 新建版本 */}
{tab !== 'monthly' && showNewVersion && (
<Card>
<h3 className="text-xs font-medium mb-3 flex items-center gap-2"><Plus className="w-4 h-4" />{isHousing ? '公积金' : '社保'}</h3>
<div className="space-y-3">
<div className="bg-blue-50 text-blue-700 text-xs px-3 py-2 rounded-md flex items-start gap-2">
<Info className="w-4 h-4 mt-0.5 shrink-0" />
<div>7</div>
</div>
<div className="grid md:grid-cols-3 gap-3">
<div><Label></Label><Input type="month" value={activeNewVersion.effectiveFrom} onChange={(e) => activeSetNewVersion({ ...activeNewVersion, effectiveFrom: e.target.value })} /></div>
<div><Label></Label><Input value={activeNewVersion.city} onChange={(e) => activeSetNewVersion({ ...activeNewVersion, city: e.target.value })} /></div>
<div><Label></Label><Input type="number" value={activeNewVersion.baseMin} onChange={(e) => activeSetNewVersion({ ...activeNewVersion, baseMin: Number(e.target.value) })} /></div>
<div><Label></Label><Input type="number" value={activeNewVersion.baseMax} onChange={(e) => activeSetNewVersion({ ...activeNewVersion, baseMax: Number(e.target.value) })} /></div>
</div>
{isHousing ? (
<div className="grid md:grid-cols-2 gap-3">
<div><Label>(%)</Label><Input type="number" step="0.1" value={activeNewVersion.housingOrg} onChange={(e) => activeSetNewVersion({ ...activeNewVersion, housingOrg: Number(e.target.value) })} /></div>
<div><Label>(%)</Label><Input type="number" step="0.1" value={activeNewVersion.housingEmp} onChange={(e) => activeSetNewVersion({ ...activeNewVersion, housingEmp: Number(e.target.value) })} /></div>
</div>
) : (
<div className="grid md:grid-cols-4 gap-3">
<div><Label>(%)</Label><Input type="number" step="0.1" value={activeNewVersion.pensionOrg} onChange={(e) => activeSetNewVersion({ ...activeNewVersion, pensionOrg: Number(e.target.value) })} /></div>
<div><Label>(%)</Label><Input type="number" step="0.1" value={activeNewVersion.pensionEmp} onChange={(e) => activeSetNewVersion({ ...activeNewVersion, pensionEmp: Number(e.target.value) })} /></div>
<div><Label>(%)</Label><Input type="number" step="0.1" value={activeNewVersion.medicalOrg} onChange={(e) => activeSetNewVersion({ ...activeNewVersion, medicalOrg: Number(e.target.value) })} /></div>
<div><Label>(%)</Label><Input type="number" step="0.1" value={activeNewVersion.medicalEmp} onChange={(e) => activeSetNewVersion({ ...activeNewVersion, medicalEmp: Number(e.target.value) })} /></div>
<div><Label>(%)</Label><Input type="number" step="0.1" value={activeNewVersion.unemploymentOrg} onChange={(e) => activeSetNewVersion({ ...activeNewVersion, unemploymentOrg: Number(e.target.value) })} /></div>
<div><Label>(%)</Label><Input type="number" step="0.1" value={activeNewVersion.unemploymentEmp} onChange={(e) => activeSetNewVersion({ ...activeNewVersion, unemploymentEmp: Number(e.target.value) })} /></div>
<div><Label>(%)</Label><Input type="number" step="0.1" value={activeNewVersion.injuryOrg} onChange={(e) => activeSetNewVersion({ ...activeNewVersion, injuryOrg: Number(e.target.value) })} /></div>
<div><Label>(%)</Label><Input type="number" step="0.1" value={activeNewVersion.maternityOrg} onChange={(e) => activeSetNewVersion({ ...activeNewVersion, maternityOrg: Number(e.target.value) })} /></div>
</div>
)}
<div className="flex gap-2">
<Button onClick={() => activeCreateMut.mutate(activeNewVersion)} disabled={activeCreateMut.isPending}>
{activeCreateMut.isPending ? '保存中...' : '创建版本'}
</Button>
<Button variant="secondary" onClick={() => setShowNewVersion(false)}></Button>
</div>
</div>
</Card>
)}
{/* 试算工具 */}
{tab !== 'monthly' && (
<div className="grid md:grid-cols-2 gap-4">
<Card>
<h2 className="text-xs font-medium mb-3">{isHousing ? '公积金' : '社保'}</h2>
<div className="space-y-3">
<div>
<Label></Label>
<Input type="number" value={base} onChange={(e) => setBase(Number(e.target.value) || 0)} />
</div>
<Button onClick={() => isHousing ? calcHousingMutate() : calcMutate()} disabled={isHousing ? housingCalcPending : isPending}>
<Calculator className="w-4 h-4 mr-1" />
{(isHousing ? housingCalcPending : isPending) ? '计算中...' : '开始计算'}
</Button>
{activeConfig && (
<div className="text-xs text-gray-400">
{activeConfig.city} | {fmt(activeConfig.baseMin)}~{fmt(activeConfig.baseMax)}
</div>
)}
</div>
</Card>
<Card>
<h2 className="text-xs font-medium mb-3 flex items-center gap-2"><Calculator className="w-4 h-4" /></h2>
{(() => {
const r = isHousing ? housingResult : result
if (!r) return <div className="text-gray-400 text-xs"></div>
return (
<div className="space-y-3">
<div className="text-xs text-gray-500">
<span className="text-gray-900 font-medium">¥{fmt(r.actualBase)}</span>
{r.capped && <span className="text-warning ml-2"></span>}
{r.floored && <span className="text-warning ml-2"></span>}
{r.configVersion && <span className="text-gray-400 ml-2">| {r.configVersion}</span>}
</div>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="border-b text-left text-gray-500">
<th className="py-1.5"></th>
<th className="py-1.5 text-right">%</th>
<th className="py-1.5 text-right">%</th>
<th className="py-1.5 text-right"></th>
<th className="py-1.5 text-right"></th>
</tr>
</thead>
<tbody>
{r.items.map((item: any) => (
<tr key={item.name} className="border-b last:border-0">
<td className="py-1.5">{item.name}</td>
<td className="py-1.5 text-right text-gray-500">{item.orgRate}%</td>
<td className="py-1.5 text-right text-gray-500">{item.empRate}%</td>
<td className="py-1.5 text-right">¥{fmt(item.orgAmount)}</td>
<td className="py-1.5 text-right">¥{fmt(item.empAmount)}</td>
</tr>
))}
</tbody>
<tfoot>
<tr className="border-t-2 font-bold">
<td className="py-2" colSpan={3}></td>
<td className="py-2 text-right text-danger">¥{fmt(r.totalOrg)}</td>
<td className="py-2 text-right text-warning">¥{fmt(r.totalEmp)}</td>
</tr>
</tfoot>
</table>
</div>
<div className="border-t pt-3">
<div className="flex items-center justify-between">
<span className="font-medium"></span>
<span className="text-lg font-bold text-primary">¥{fmt(r.total)}</span>
</div>
<div className="text-xs text-gray-400 mt-1">
¥{fmt(r.totalOrg)} + ¥{fmt(r.totalEmp)}
</div>
</div>
</div>
)
})()}
</Card>
</div>
)}
{/* ========== 月度办理 Tab ========== */}
{tab === 'monthly' && (
<Card>
<div className="flex items-center justify-between mb-3">
<h2 className="text-xs 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" />
</Button>
</div>
</div>
<div className="bg-blue-50 text-blue-700 text-xs px-3 py-2 rounded-md mb-3">
///
</div>
{(() => {
if (!monthlyChanges) return <div className="text-center py-4 text-gray-400 text-xs">...</div>
const sAdd = monthlyChanges.social?.additions || []
const sSub = monthlyChanges.social?.subtractions || []
const sNormal = monthlyChanges.socialActive?.items || []
const hAdd = monthlyChanges.housing?.additions || []
const hSub = monthlyChanges.housing?.subtractions || []
const hNormal = monthlyChanges.housingActive?.items || []
if (sAdd.length === 0 && sSub.length === 0 && hAdd.length === 0 && hSub.length === 0 && sNormal.length === 0 && hNormal.length === 0) {
return <div className="text-center py-4 text-gray-400 text-xs">{monthlyMonth} </div>
}
return (
<div className="space-y-4">
{/* 社保 */}
<div>
<h3 className="text-xs font-medium mb-2"></h3>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="border-b 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-xs font-medium mb-2"></h3>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="border-b 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>
</div>
)
})()}
</Card>
)}
<p className="text-xs text-gray-400">
/7
</p>
</div>
)
}