refactor: 社保公积金从薪税tab移至顶层导航,放在薪税和解聘之间
- 新建 SocialInsurance.tsx 独立页面 - App.tsx 添加 /social 路由 - TopNav/MobileTabBar 添加社保公积金导航项 - Money.tsx 移除 social tab 和 SocialInsuranceCalculator 函数
This commit is contained in:
@@ -9,6 +9,7 @@ import ForgotPassword from './pages/auth/ForgotPassword'
|
||||
import Dashboard from './pages/Dashboard'
|
||||
import Contracts from './pages/Contracts'
|
||||
import Money from './pages/Money'
|
||||
import SocialInsurance from './pages/SocialInsurance'
|
||||
import Roster from './pages/Roster'
|
||||
import Termination from './pages/Termination'
|
||||
import AIAssistant from './pages/AIAssistant'
|
||||
@@ -65,6 +66,7 @@ export default function App() {
|
||||
<Route path="/" element={<ProtectedRoute><AdminLayout><Dashboard /></AdminLayout></ProtectedRoute>} />
|
||||
<Route path="/roster" element={<ProtectedRoute><AdminLayout><Roster /></AdminLayout></ProtectedRoute>} />
|
||||
<Route path="/money" element={<ProtectedRoute><AdminLayout><Money /></AdminLayout></ProtectedRoute>} />
|
||||
<Route path="/social" element={<ProtectedRoute><AdminLayout><SocialInsurance /></AdminLayout></ProtectedRoute>} />
|
||||
<Route path="/termination" element={<ProtectedRoute><AdminLayout><Termination /></AdminLayout></ProtectedRoute>} />
|
||||
<Route path="/ai-assistant" element={<ProtectedRoute><AdminLayout><AIAssistant /></AdminLayout></ProtectedRoute>} />
|
||||
<Route path="/settings" element={<ProtectedRoute><AdminLayout><Settings /></AdminLayout></ProtectedRoute>} />
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { Home, FileText, Users, Calculator, UserX, Bot } from 'lucide-react'
|
||||
import { Home, FileText, Users, Calculator, UserX, Bot, Shield } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
const tabs = [
|
||||
{ path: '/', label: '总览', icon: Home },
|
||||
{ path: '/roster', label: '花名册', icon: Users },
|
||||
{ path: '/money', label: '薪税', icon: Calculator },
|
||||
{ path: '/social', label: '社保', icon: Shield },
|
||||
{ path: '/termination', label: '解聘', icon: UserX },
|
||||
{ path: '/ai-assistant', label: 'AI', icon: Bot },
|
||||
]
|
||||
|
||||
@@ -8,6 +8,7 @@ const tabs = [
|
||||
{ path: '/', label: '总览' },
|
||||
{ path: '/roster', label: '花名册' },
|
||||
{ path: '/money', label: '薪税' },
|
||||
{ path: '/social', label: '社保公积金' },
|
||||
{ path: '/termination', label: '解聘' },
|
||||
{ path: '/ai-assistant', label: 'AI顾问' },
|
||||
]
|
||||
|
||||
@@ -11,7 +11,7 @@ import Pagination from '../components/ui/Pagination'
|
||||
// 金额格式化:保留两位小数 + 千分位
|
||||
const fmt = (n: number) => (n || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
|
||||
type Tab = 'batch' | 'template' | 'overtime' | 'social' | 'payslip'
|
||||
type Tab = 'batch' | 'template' | 'overtime' | 'payslip'
|
||||
|
||||
export default function Money() {
|
||||
const [tab, setTab] = useState<Tab>('batch')
|
||||
@@ -20,7 +20,6 @@ export default function Money() {
|
||||
{ key: 'batch', label: '发薪批次' },
|
||||
{ key: 'template', label: '薪酬模版' },
|
||||
{ key: 'overtime', label: '加班费计算' },
|
||||
{ key: 'social', label: '社保公积金' },
|
||||
{ key: 'payslip', label: '工资条管理' },
|
||||
]
|
||||
|
||||
@@ -45,7 +44,6 @@ export default function Money() {
|
||||
{tab === 'batch' && <BatchManager />}
|
||||
{tab === 'template' && <TemplateManager />}
|
||||
{tab === 'overtime' && <OvertimeCalculator />}
|
||||
{tab === 'social' && <SocialInsuranceCalculator />}
|
||||
{tab === 'payslip' && <PayslipManager />}
|
||||
</div>
|
||||
)
|
||||
@@ -1155,495 +1153,3 @@ function PayslipManager() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SocialInsuranceCalculator() {
|
||||
const queryClient = useQueryClient()
|
||||
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, { socialBase: number; housingBase: number }>>({})
|
||||
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,
|
||||
housingOrg: 12, housingEmp: 12,
|
||||
baseMin: 6326, baseMax: 33891,
|
||||
})
|
||||
|
||||
const { data: config } = useQuery<any>({
|
||||
queryKey: ['social-config'],
|
||||
queryFn: async () => {
|
||||
const res = await api.get('/social/config') as any
|
||||
return res.data
|
||||
},
|
||||
})
|
||||
|
||||
const { data: versions } = useQuery<any[]>({
|
||||
queryKey: ['social-config-versions'],
|
||||
queryFn: async () => {
|
||||
const res = await api.get('/social/config/versions') as any
|
||||
return res.data
|
||||
},
|
||||
enabled: showVersions,
|
||||
})
|
||||
|
||||
const { data: result, mutate: calcMutate, isPending } = useMutation<any>({
|
||||
mutationFn: async () => {
|
||||
const res = await api.post('/social/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)
|
||||
alert('新版本已创建,旧版本已自动归档')
|
||||
},
|
||||
})
|
||||
|
||||
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 applyAdjustMutation = useMutation({
|
||||
mutationFn: (data: { items: { employeeId: string; newSocialBase: number; newHousingBase: 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({})
|
||||
alert(`调整完成,共调整 ${res.data?.adjusted || 0} 名员工的社保/公积金基数`)
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xs font-medium">社保公积金管理</h2>
|
||||
<div className="flex gap-2">
|
||||
<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>
|
||||
|
||||
{/* 当前生效版本信息 */}
|
||||
{config && (
|
||||
<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">生效月份:{config.effectiveFrom}</span>
|
||||
<span className="text-xs text-gray-500">· {config.city}</span>
|
||||
{config.adjustmentDone && (
|
||||
<span className="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-400">已调整员工基数</span>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => previewAdjustMutation.mutate()}
|
||||
disabled={config.adjustmentDone || previewAdjustMutation.isPending}
|
||||
>
|
||||
<SettingsIcon className="w-4 h-4 mr-1" />
|
||||
{config.adjustmentDone ? '已调整' : previewAdjustMutation.isPending ? '加载中...' : '调整员工基数'}
|
||||
</Button>
|
||||
</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(config.baseMin)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b pb-1.5">
|
||||
<span className="text-gray-500">缴费基数上限</span>
|
||||
<span className="font-medium">¥{fmt(config.baseMax)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b pb-1.5">
|
||||
<span className="text-gray-500">养老(企业/个人)</span>
|
||||
<span className="font-medium">{config.pensionOrg}% / {config.pensionEmp}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b pb-1.5">
|
||||
<span className="text-gray-500">医疗(企业/个人)</span>
|
||||
<span className="font-medium">{config.medicalOrg}% / {config.medicalEmp}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b pb-1.5">
|
||||
<span className="text-gray-500">失业(企业/个人)</span>
|
||||
<span className="font-medium">{config.unemploymentOrg}% / {config.unemploymentEmp}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b pb-1.5">
|
||||
<span className="text-gray-500">工伤(企业)</span>
|
||||
<span className="font-medium">{config.injuryOrg}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b pb-1.5">
|
||||
<span className="text-gray-500">生育(企业)</span>
|
||||
<span className="font-medium">{config.maternityOrg}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b pb-1.5">
|
||||
<span className="text-gray-500">公积金(企业/个人)</span>
|
||||
<span className="font-medium">{config.housingOrg}% / {config.housingEmp}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 调整预览 */}
|
||||
{showAdjust && adjustData && (
|
||||
<Card>
|
||||
<h3 className="text-xs font-medium mb-3 flex items-center gap-2">
|
||||
<SettingsIcon className="w-4 h-4" />员工基数调整
|
||||
</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)})调整全部在职员工社保/公积金缴费基数。
|
||||
建议基数=上年月均工资按上下限裁剪。您可逐行修改,也可点击「采用建议值」或「保持原基数」。确认后保存,此操作只能执行一次。
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Button variant="secondary" size="sm" onClick={() => {
|
||||
const newEdits: Record<string, { socialBase: number; housingBase: number }> = {}
|
||||
adjustData.items.forEach((i: any) => {
|
||||
newEdits[i.employeeId] = { socialBase: i.suggestedSocialBase, housingBase: i.suggestedHousingBase }
|
||||
})
|
||||
setEditItems(newEdits)
|
||||
}}>
|
||||
<Check className="w-3.5 h-3.5 mr-1" />全部采用建议值
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" onClick={() => {
|
||||
const newEdits: Record<string, { socialBase: number; housingBase: number }> = {}
|
||||
adjustData.items.forEach((i: any) => {
|
||||
newEdits[i.employeeId] = { socialBase: i.oldSocialBase, housingBase: i.oldHousingBase }
|
||||
})
|
||||
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">社保基数(当前)</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-right">公积金基数(新)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{adjustData.items.map((item: any) => {
|
||||
const edit = editItems[item.employeeId]
|
||||
const socialBase = edit?.socialBase ?? item.suggestedSocialBase
|
||||
const housingBase = edit?.housingBase ?? item.suggestedHousingBase
|
||||
const socialChanged = socialBase !== item.oldSocialBase
|
||||
const housingChanged = housingBase !== item.oldHousingBase
|
||||
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.oldSocialBase)}</td>
|
||||
<td className="py-1.5 text-right text-gray-500">¥{fmt(item.suggestedSocialBase)}</td>
|
||||
<td className="py-1.5 text-right">
|
||||
<Input
|
||||
type="number"
|
||||
className="w-24 text-right text-xs"
|
||||
value={socialBase}
|
||||
onChange={(e) => setEditItems({
|
||||
...editItems,
|
||||
[item.employeeId]: {
|
||||
socialBase: Number(e.target.value) || 0,
|
||||
housingBase: edit?.housingBase ?? item.suggestedHousingBase,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{socialChanged && <span className="text-warning ml-1">●</span>}
|
||||
</td>
|
||||
<td className="py-1.5 text-right text-gray-400">¥{fmt(item.oldHousingBase)}</td>
|
||||
<td className="py-1.5 text-right text-gray-500">¥{fmt(item.suggestedHousingBase)}</td>
|
||||
<td className="py-1.5 text-right">
|
||||
<Input
|
||||
type="number"
|
||||
className="w-24 text-right text-xs"
|
||||
value={housingBase}
|
||||
onChange={(e) => setEditItems({
|
||||
...editItems,
|
||||
[item.employeeId]: {
|
||||
socialBase: edit?.socialBase ?? item.suggestedSocialBase,
|
||||
housingBase: Number(e.target.value) || 0,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{housingChanged && <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) => {
|
||||
const edit = editItems[i.employeeId]
|
||||
return {
|
||||
employeeId: i.employeeId,
|
||||
newSocialBase: edit?.socialBase ?? i.suggestedSocialBase,
|
||||
newHousingBase: edit?.housingBase ?? i.suggestedHousingBase,
|
||||
}
|
||||
})
|
||||
applyAdjustMutation.mutate({ items })
|
||||
}}
|
||||
disabled={applyAdjustMutation.isPending}
|
||||
>
|
||||
{applyAdjustMutation.isPending ? '保存中...' : '确认保存'}
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={() => { setShowAdjust(false); setAdjustData(null); setEditItems({}) }}>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 版本历史 */}
|
||||
{showVersions && (
|
||||
<Card>
|
||||
<h3 className="text-xs font-medium mb-3 flex items-center gap-2"><History className="w-4 h-4" />版本历史</h3>
|
||||
{!versions || versions.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>
|
||||
<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>
|
||||
{versions.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>
|
||||
<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-right text-gray-500">{v.housingOrg}/{v.housingEmp}</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>
|
||||
)}
|
||||
|
||||
{/* 新建版本 */}
|
||||
{showNewVersion && (
|
||||
<Card>
|
||||
<h3 className="text-xs font-medium mb-3 flex items-center gap-2"><Plus className="w-4 h-4" />新建配置版本</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={newVersion.effectiveFrom} onChange={(e) => setNewVersion({ ...newVersion, effectiveFrom: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>城市</Label>
|
||||
<Input value={newVersion.city} onChange={(e) => setNewVersion({ ...newVersion, city: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>缴费基数下限</Label>
|
||||
<Input type="number" value={newVersion.baseMin} onChange={(e) => setNewVersion({ ...newVersion, baseMin: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>缴费基数上限</Label>
|
||||
<Input type="number" value={newVersion.baseMax} onChange={(e) => setNewVersion({ ...newVersion, baseMax: 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={newVersion.pensionOrg} onChange={(e) => setNewVersion({ ...newVersion, pensionOrg: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>养老(个人%)</Label>
|
||||
<Input type="number" step="0.1" value={newVersion.pensionEmp} onChange={(e) => setNewVersion({ ...newVersion, pensionEmp: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>医疗(企业%)</Label>
|
||||
<Input type="number" step="0.1" value={newVersion.medicalOrg} onChange={(e) => setNewVersion({ ...newVersion, medicalOrg: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>医疗(个人%)</Label>
|
||||
<Input type="number" step="0.1" value={newVersion.medicalEmp} onChange={(e) => setNewVersion({ ...newVersion, medicalEmp: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>失业(企业%)</Label>
|
||||
<Input type="number" step="0.1" value={newVersion.unemploymentOrg} onChange={(e) => setNewVersion({ ...newVersion, unemploymentOrg: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>失业(个人%)</Label>
|
||||
<Input type="number" step="0.1" value={newVersion.unemploymentEmp} onChange={(e) => setNewVersion({ ...newVersion, unemploymentEmp: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>工伤(企业%)</Label>
|
||||
<Input type="number" step="0.1" value={newVersion.injuryOrg} onChange={(e) => setNewVersion({ ...newVersion, injuryOrg: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>生育(企业%)</Label>
|
||||
<Input type="number" step="0.1" value={newVersion.maternityOrg} onChange={(e) => setNewVersion({ ...newVersion, maternityOrg: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>公积金(企业%)</Label>
|
||||
<Input type="number" step="0.1" value={newVersion.housingOrg} onChange={(e) => setNewVersion({ ...newVersion, housingOrg: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>公积金(个人%)</Label>
|
||||
<Input type="number" step="0.1" value={newVersion.housingEmp} onChange={(e) => setNewVersion({ ...newVersion, housingEmp: Number(e.target.value) })} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={() => createVersionMutation.mutate(newVersion)} disabled={createVersionMutation.isPending}>
|
||||
{createVersionMutation.isPending ? '保存中...' : '创建版本'}
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={() => setShowNewVersion(false)}>取消</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 试算工具 */}
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<Card>
|
||||
<h2 className="text-xs font-medium mb-3">试算工具</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={() => calcMutate()} disabled={isPending}>
|
||||
<Calculator className="w-4 h-4 mr-1" />
|
||||
{isPending ? '计算中...' : '开始计算'}
|
||||
</Button>
|
||||
{config && (
|
||||
<div className="text-xs text-gray-400">
|
||||
当前配置:{config.city} | 基数范围 {fmt(config.baseMin)}~{fmt(config.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>
|
||||
{result ? (
|
||||
<div className="space-y-3">
|
||||
<div className="text-xs text-gray-500">
|
||||
缴费基数:<span className="text-gray-900 font-medium">¥{fmt(result.actualBase)}</span>
|
||||
{result.capped && <span className="text-warning ml-2">(已封顶)</span>}
|
||||
{result.floored && <span className="text-warning ml-2">(已保底)</span>}
|
||||
{result.configVersion && <span className="text-gray-400 ml-2">| 配置版本:{result.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>
|
||||
{result.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(result.totalOrg)}</td>
|
||||
<td className="py-2 text-right text-warning">¥{fmt(result.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(result.total)}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
企业承担 ¥{fmt(result.totalOrg)} + 个人承担 ¥{fmt(result.totalEmp)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-400 text-xs">点击「开始计算」查看结果</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-gray-400">
|
||||
社保/公积金基数按上年度月均工资核定,每人不同,在员工基本信息中设置。比例和基数上下限按版本管理,通常每年7月调整。
|
||||
发薪批次计算时按批次月份自动匹配对应版本配置。
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,502 @@
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Calculator, Info, Check, Settings as SettingsIcon, Plus, History } 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 [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, { socialBase: number; housingBase: number }>>({})
|
||||
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,
|
||||
housingOrg: 12, housingEmp: 12,
|
||||
baseMin: 6326, baseMax: 33891,
|
||||
})
|
||||
|
||||
const { data: config } = useQuery<any>({
|
||||
queryKey: ['social-config'],
|
||||
queryFn: async () => {
|
||||
const res = await api.get('/social/config') as any
|
||||
return res.data
|
||||
},
|
||||
})
|
||||
|
||||
const { data: versions } = useQuery<any[]>({
|
||||
queryKey: ['social-config-versions'],
|
||||
queryFn: async () => {
|
||||
const res = await api.get('/social/config/versions') as any
|
||||
return res.data
|
||||
},
|
||||
enabled: showVersions,
|
||||
})
|
||||
|
||||
const { data: result, mutate: calcMutate, isPending } = useMutation<any>({
|
||||
mutationFn: async () => {
|
||||
const res = await api.post('/social/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)
|
||||
alert('新版本已创建,旧版本已自动归档')
|
||||
},
|
||||
})
|
||||
|
||||
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 applyAdjustMutation = useMutation({
|
||||
mutationFn: (data: { items: { employeeId: string; newSocialBase: number; newHousingBase: 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({})
|
||||
alert(`调整完成,共调整 ${res.data?.adjusted || 0} 名员工的社保/公积金基数`)
|
||||
},
|
||||
})
|
||||
|
||||
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">
|
||||
<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>
|
||||
|
||||
{/* 当前生效版本信息 */}
|
||||
{config && (
|
||||
<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">生效月份:{config.effectiveFrom}</span>
|
||||
<span className="text-xs text-gray-500">· {config.city}</span>
|
||||
{config.adjustmentDone && (
|
||||
<span className="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-400">已调整员工基数</span>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => previewAdjustMutation.mutate()}
|
||||
disabled={config.adjustmentDone || previewAdjustMutation.isPending}
|
||||
>
|
||||
<SettingsIcon className="w-4 h-4 mr-1" />
|
||||
{config.adjustmentDone ? '已调整' : previewAdjustMutation.isPending ? '加载中...' : '调整员工基数'}
|
||||
</Button>
|
||||
</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(config.baseMin)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b pb-1.5">
|
||||
<span className="text-gray-500">缴费基数上限</span>
|
||||
<span className="font-medium">¥{fmt(config.baseMax)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b pb-1.5">
|
||||
<span className="text-gray-500">养老(企业/个人)</span>
|
||||
<span className="font-medium">{config.pensionOrg}% / {config.pensionEmp}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b pb-1.5">
|
||||
<span className="text-gray-500">医疗(企业/个人)</span>
|
||||
<span className="font-medium">{config.medicalOrg}% / {config.medicalEmp}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b pb-1.5">
|
||||
<span className="text-gray-500">失业(企业/个人)</span>
|
||||
<span className="font-medium">{config.unemploymentOrg}% / {config.unemploymentEmp}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b pb-1.5">
|
||||
<span className="text-gray-500">工伤(企业)</span>
|
||||
<span className="font-medium">{config.injuryOrg}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b pb-1.5">
|
||||
<span className="text-gray-500">生育(企业)</span>
|
||||
<span className="font-medium">{config.maternityOrg}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b pb-1.5">
|
||||
<span className="text-gray-500">公积金(企业/个人)</span>
|
||||
<span className="font-medium">{config.housingOrg}% / {config.housingEmp}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 调整预览 */}
|
||||
{showAdjust && adjustData && (
|
||||
<Card>
|
||||
<h3 className="text-xs font-medium mb-3 flex items-center gap-2">
|
||||
<SettingsIcon className="w-4 h-4" />员工基数调整
|
||||
</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)})调整全部在职员工社保/公积金缴费基数。
|
||||
建议基数=上年月均工资按上下限裁剪。您可逐行修改,也可点击「采用建议值」或「保持原基数」。确认后保存,此操作只能执行一次。
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Button variant="secondary" size="sm" onClick={() => {
|
||||
const newEdits: Record<string, { socialBase: number; housingBase: number }> = {}
|
||||
adjustData.items.forEach((i: any) => {
|
||||
newEdits[i.employeeId] = { socialBase: i.suggestedSocialBase, housingBase: i.suggestedHousingBase }
|
||||
})
|
||||
setEditItems(newEdits)
|
||||
}}>
|
||||
<Check className="w-3.5 h-3.5 mr-1" />全部采用建议值
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" onClick={() => {
|
||||
const newEdits: Record<string, { socialBase: number; housingBase: number }> = {}
|
||||
adjustData.items.forEach((i: any) => {
|
||||
newEdits[i.employeeId] = { socialBase: i.oldSocialBase, housingBase: i.oldHousingBase }
|
||||
})
|
||||
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">社保基数(当前)</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-right">公积金基数(新)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{adjustData.items.map((item: any) => {
|
||||
const edit = editItems[item.employeeId]
|
||||
const socialBase = edit?.socialBase ?? item.suggestedSocialBase
|
||||
const housingBase = edit?.housingBase ?? item.suggestedHousingBase
|
||||
const socialChanged = socialBase !== item.oldSocialBase
|
||||
const housingChanged = housingBase !== item.oldHousingBase
|
||||
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.oldSocialBase)}</td>
|
||||
<td className="py-1.5 text-right text-gray-500">¥{fmt(item.suggestedSocialBase)}</td>
|
||||
<td className="py-1.5 text-right">
|
||||
<Input
|
||||
type="number"
|
||||
className="w-24 text-right text-xs"
|
||||
value={socialBase}
|
||||
onChange={(e) => setEditItems({
|
||||
...editItems,
|
||||
[item.employeeId]: {
|
||||
socialBase: Number(e.target.value) || 0,
|
||||
housingBase: edit?.housingBase ?? item.suggestedHousingBase,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{socialChanged && <span className="text-warning ml-1">●</span>}
|
||||
</td>
|
||||
<td className="py-1.5 text-right text-gray-400">¥{fmt(item.oldHousingBase)}</td>
|
||||
<td className="py-1.5 text-right text-gray-500">¥{fmt(item.suggestedHousingBase)}</td>
|
||||
<td className="py-1.5 text-right">
|
||||
<Input
|
||||
type="number"
|
||||
className="w-24 text-right text-xs"
|
||||
value={housingBase}
|
||||
onChange={(e) => setEditItems({
|
||||
...editItems,
|
||||
[item.employeeId]: {
|
||||
socialBase: edit?.socialBase ?? item.suggestedSocialBase,
|
||||
housingBase: Number(e.target.value) || 0,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{housingChanged && <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) => {
|
||||
const edit = editItems[i.employeeId]
|
||||
return {
|
||||
employeeId: i.employeeId,
|
||||
newSocialBase: edit?.socialBase ?? i.suggestedSocialBase,
|
||||
newHousingBase: edit?.housingBase ?? i.suggestedHousingBase,
|
||||
}
|
||||
})
|
||||
applyAdjustMutation.mutate({ items })
|
||||
}}
|
||||
disabled={applyAdjustMutation.isPending}
|
||||
>
|
||||
{applyAdjustMutation.isPending ? '保存中...' : '确认保存'}
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={() => { setShowAdjust(false); setAdjustData(null); setEditItems({}) }}>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 版本历史 */}
|
||||
{showVersions && (
|
||||
<Card>
|
||||
<h3 className="text-xs font-medium mb-3 flex items-center gap-2"><History className="w-4 h-4" />版本历史</h3>
|
||||
{!versions || versions.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>
|
||||
<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>
|
||||
{versions.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>
|
||||
<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-right text-gray-500">{v.housingOrg}/{v.housingEmp}</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>
|
||||
)}
|
||||
|
||||
{/* 新建版本 */}
|
||||
{showNewVersion && (
|
||||
<Card>
|
||||
<h3 className="text-xs font-medium mb-3 flex items-center gap-2"><Plus className="w-4 h-4" />新建配置版本</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={newVersion.effectiveFrom} onChange={(e) => setNewVersion({ ...newVersion, effectiveFrom: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>城市</Label>
|
||||
<Input value={newVersion.city} onChange={(e) => setNewVersion({ ...newVersion, city: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>缴费基数下限</Label>
|
||||
<Input type="number" value={newVersion.baseMin} onChange={(e) => setNewVersion({ ...newVersion, baseMin: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>缴费基数上限</Label>
|
||||
<Input type="number" value={newVersion.baseMax} onChange={(e) => setNewVersion({ ...newVersion, baseMax: 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={newVersion.pensionOrg} onChange={(e) => setNewVersion({ ...newVersion, pensionOrg: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>养老(个人%)</Label>
|
||||
<Input type="number" step="0.1" value={newVersion.pensionEmp} onChange={(e) => setNewVersion({ ...newVersion, pensionEmp: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>医疗(企业%)</Label>
|
||||
<Input type="number" step="0.1" value={newVersion.medicalOrg} onChange={(e) => setNewVersion({ ...newVersion, medicalOrg: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>医疗(个人%)</Label>
|
||||
<Input type="number" step="0.1" value={newVersion.medicalEmp} onChange={(e) => setNewVersion({ ...newVersion, medicalEmp: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>失业(企业%)</Label>
|
||||
<Input type="number" step="0.1" value={newVersion.unemploymentOrg} onChange={(e) => setNewVersion({ ...newVersion, unemploymentOrg: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>失业(个人%)</Label>
|
||||
<Input type="number" step="0.1" value={newVersion.unemploymentEmp} onChange={(e) => setNewVersion({ ...newVersion, unemploymentEmp: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>工伤(企业%)</Label>
|
||||
<Input type="number" step="0.1" value={newVersion.injuryOrg} onChange={(e) => setNewVersion({ ...newVersion, injuryOrg: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>生育(企业%)</Label>
|
||||
<Input type="number" step="0.1" value={newVersion.maternityOrg} onChange={(e) => setNewVersion({ ...newVersion, maternityOrg: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>公积金(企业%)</Label>
|
||||
<Input type="number" step="0.1" value={newVersion.housingOrg} onChange={(e) => setNewVersion({ ...newVersion, housingOrg: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>公积金(个人%)</Label>
|
||||
<Input type="number" step="0.1" value={newVersion.housingEmp} onChange={(e) => setNewVersion({ ...newVersion, housingEmp: Number(e.target.value) })} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={() => createVersionMutation.mutate(newVersion)} disabled={createVersionMutation.isPending}>
|
||||
{createVersionMutation.isPending ? '保存中...' : '创建版本'}
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={() => setShowNewVersion(false)}>取消</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 试算工具 */}
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<Card>
|
||||
<h2 className="text-xs font-medium mb-3">试算工具</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={() => calcMutate()} disabled={isPending}>
|
||||
<Calculator className="w-4 h-4 mr-1" />
|
||||
{isPending ? '计算中...' : '开始计算'}
|
||||
</Button>
|
||||
{config && (
|
||||
<div className="text-xs text-gray-400">
|
||||
当前配置:{config.city} | 基数范围 {fmt(config.baseMin)}~{fmt(config.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>
|
||||
{result ? (
|
||||
<div className="space-y-3">
|
||||
<div className="text-xs text-gray-500">
|
||||
缴费基数:<span className="text-gray-900 font-medium">¥{fmt(result.actualBase)}</span>
|
||||
{result.capped && <span className="text-warning ml-2">(已封顶)</span>}
|
||||
{result.floored && <span className="text-warning ml-2">(已保底)</span>}
|
||||
{result.configVersion && <span className="text-gray-400 ml-2">| 配置版本:{result.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>
|
||||
{result.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(result.totalOrg)}</td>
|
||||
<td className="py-2 text-right text-warning">¥{fmt(result.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(result.total)}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
企业承担 ¥{fmt(result.totalOrg)} + 个人承担 ¥{fmt(result.totalEmp)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-400 text-xs">点击「开始计算」查看结果</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-gray-400">
|
||||
社保/公积金基数按上年度月均工资核定,每人不同,在员工基本信息中设置。比例和基数上下限按版本管理,通常每年7月调整。
|
||||
发薪批次计算时按批次月份自动匹配对应版本配置。
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user