From fa7aa01c481324e4fe15299a16838e386376ce7d Mon Sep 17 00:00:00 2001 From: freedakgmail Date: Thu, 23 Jul 2026 16:48:10 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=A4=BE=E4=BF=9D=E5=85=AC?= =?UTF-8?q?=E7=A7=AF=E9=87=91=E4=BB=8E=E8=96=AA=E7=A8=8Etab=E7=A7=BB?= =?UTF-8?q?=E8=87=B3=E9=A1=B6=E5=B1=82=E5=AF=BC=E8=88=AA=EF=BC=8C=E6=94=BE?= =?UTF-8?q?=E5=9C=A8=E8=96=AA=E7=A8=8E=E5=92=8C=E8=A7=A3=E8=81=98=E4=B9=8B?= =?UTF-8?q?=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新建 SocialInsurance.tsx 独立页面 - App.tsx 添加 /social 路由 - TopNav/MobileTabBar 添加社保公积金导航项 - Money.tsx 移除 social tab 和 SocialInsuranceCalculator 函数 --- frontend/src/App.tsx | 2 + .../src/components/layout/MobileTabBar.tsx | 3 +- frontend/src/components/layout/TopNav.tsx | 1 + frontend/src/pages/Money.tsx | 496 +---------------- frontend/src/pages/SocialInsurance.tsx | 502 ++++++++++++++++++ 5 files changed, 508 insertions(+), 496 deletions(-) create mode 100644 frontend/src/pages/SocialInsurance.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3c15059..94952b8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/layout/MobileTabBar.tsx b/frontend/src/components/layout/MobileTabBar.tsx index 58a9fa7..c7e5f80 100644 --- a/frontend/src/components/layout/MobileTabBar.tsx +++ b/frontend/src/components/layout/MobileTabBar.tsx @@ -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 }, ] diff --git a/frontend/src/components/layout/TopNav.tsx b/frontend/src/components/layout/TopNav.tsx index 1b36c36..498d6c0 100644 --- a/frontend/src/components/layout/TopNav.tsx +++ b/frontend/src/components/layout/TopNav.tsx @@ -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顾问' }, ] diff --git a/frontend/src/pages/Money.tsx b/frontend/src/pages/Money.tsx index 72c39c9..9cdc06d 100644 --- a/frontend/src/pages/Money.tsx +++ b/frontend/src/pages/Money.tsx @@ -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('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' && } {tab === 'template' && } {tab === 'overtime' && } - {tab === 'social' && } {tab === 'payslip' && } ) @@ -1155,495 +1153,3 @@ function PayslipManager() { ) } - -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(null) - const [editItems, setEditItems] = useState>({}) - const [newVersion, setNewVersion] = useState({ - 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({ - queryKey: ['social-config'], - queryFn: async () => { - const res = await api.get('/social/config') as any - return res.data - }, - }) - - const { data: versions } = useQuery({ - 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({ - 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 ( -
-
-

社保公积金管理

-
- - -
-
- - {/* 当前生效版本信息 */} - {config && ( - -
-
- 当前生效 - 生效月份:{config.effectiveFrom} - · {config.city} - {config.adjustmentDone && ( - 已调整员工基数 - )} -
- -
-
-
- 缴费基数下限 - ¥{fmt(config.baseMin)} -
-
- 缴费基数上限 - ¥{fmt(config.baseMax)} -
-
- 养老(企业/个人) - {config.pensionOrg}% / {config.pensionEmp}% -
-
- 医疗(企业/个人) - {config.medicalOrg}% / {config.medicalEmp}% -
-
- 失业(企业/个人) - {config.unemploymentOrg}% / {config.unemploymentEmp}% -
-
- 工伤(企业) - {config.injuryOrg}% -
-
- 生育(企业) - {config.maternityOrg}% -
-
- 公积金(企业/个人) - {config.housingOrg}% / {config.housingEmp}% -
-
-
- )} - - {/* 调整预览 */} - {showAdjust && adjustData && ( - -

- 员工基数调整 -

-
- -
- 按当前版本基数上下限(¥{fmt(adjustData.baseMin)} ~ ¥{fmt(adjustData.baseMax)})调整全部在职员工社保/公积金缴费基数。 - 建议基数=上年月均工资按上下限裁剪。您可逐行修改,也可点击「采用建议值」或「保持原基数」。确认后保存,此操作只能执行一次。 -
-
-
- - - 共 {adjustData.total} 名员工 -
-
- - - - - - - - - - - - - - - - {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 ( - - - - - - - - - - - - ) - })} - -
姓名部门上年月均社保基数(当前)社保基数(建议)社保基数(新)公积金基数(当前)公积金基数(建议)公积金基数(新)
{item.name}{item.department}¥{fmt(item.avgSalary)}¥{fmt(item.oldSocialBase)}¥{fmt(item.suggestedSocialBase)} - setEditItems({ - ...editItems, - [item.employeeId]: { - socialBase: Number(e.target.value) || 0, - housingBase: edit?.housingBase ?? item.suggestedHousingBase, - }, - })} - /> - {socialChanged && } - ¥{fmt(item.oldHousingBase)}¥{fmt(item.suggestedHousingBase)} - setEditItems({ - ...editItems, - [item.employeeId]: { - socialBase: edit?.socialBase ?? item.suggestedSocialBase, - housingBase: Number(e.target.value) || 0, - }, - })} - /> - {housingChanged && } -
-
-
- - -
-
- )} - - {/* 版本历史 */} - {showVersions && ( - -

版本历史

- {!versions || versions.length === 0 ? ( -
暂无版本记录
- ) : ( -
- - - - - - - - - - - - - - - - {versions.map((v: any) => ( - - - - - - - - - - - - ))} - -
生效月份失效月份城市基数下限基数上限养老%医疗%公积金%状态
{v.effectiveFrom}{v.effectiveTo || '—'}{v.city}¥{fmt(v.baseMin)}¥{fmt(v.baseMax)}{v.pensionOrg}/{v.pensionEmp}{v.medicalOrg}/{v.medicalEmp}{v.housingOrg}/{v.housingEmp} - {v.isCurrent - ? 当前 - : 历史 - } -
-
- )} -
- )} - - {/* 新建版本 */} - {showNewVersion && ( - -

新建配置版本

-
-
- -
- 新版本生效后,当前版本将自动归档。发薪批次计算时按批次月份匹配对应版本的配置。 - 通常每年7月社保调基时新建版本。 -
-
-
-
- - setNewVersion({ ...newVersion, effectiveFrom: e.target.value })} /> -
-
- - setNewVersion({ ...newVersion, city: e.target.value })} /> -
-
- - setNewVersion({ ...newVersion, baseMin: Number(e.target.value) })} /> -
-
- - setNewVersion({ ...newVersion, baseMax: Number(e.target.value) })} /> -
-
-
-
- - setNewVersion({ ...newVersion, pensionOrg: Number(e.target.value) })} /> -
-
- - setNewVersion({ ...newVersion, pensionEmp: Number(e.target.value) })} /> -
-
- - setNewVersion({ ...newVersion, medicalOrg: Number(e.target.value) })} /> -
-
- - setNewVersion({ ...newVersion, medicalEmp: Number(e.target.value) })} /> -
-
- - setNewVersion({ ...newVersion, unemploymentOrg: Number(e.target.value) })} /> -
-
- - setNewVersion({ ...newVersion, unemploymentEmp: Number(e.target.value) })} /> -
-
- - setNewVersion({ ...newVersion, injuryOrg: Number(e.target.value) })} /> -
-
- - setNewVersion({ ...newVersion, maternityOrg: Number(e.target.value) })} /> -
-
- - setNewVersion({ ...newVersion, housingOrg: Number(e.target.value) })} /> -
-
- - setNewVersion({ ...newVersion, housingEmp: Number(e.target.value) })} /> -
-
-
- - -
-
-
- )} - - {/* 试算工具 */} -
- -

试算工具

-
-
- - setBase(Number(e.target.value) || 0)} /> -
- - {config && ( -
- 当前配置:{config.city} | 基数范围 {fmt(config.baseMin)}~{fmt(config.baseMax)} -
- )} -
-
- - -

计算结果

- {result ? ( -
-
- 缴费基数:¥{fmt(result.actualBase)} - {result.capped && (已封顶)} - {result.floored && (已保底)} - {result.configVersion && | 配置版本:{result.configVersion}} -
-
- - - - - - - - - - - - {result.items.map((item: any) => ( - - - - - - - - ))} - - - - - - - - -
险种企业%个人%企业缴纳个人缴纳
{item.name}{item.orgRate}%{item.empRate}%¥{fmt(item.orgAmount)}¥{fmt(item.empAmount)}
合计¥{fmt(result.totalOrg)}¥{fmt(result.totalEmp)}
-
-
-
- 总费用 - ¥{fmt(result.total)} -
-
- 企业承担 ¥{fmt(result.totalOrg)} + 个人承担 ¥{fmt(result.totalEmp)} -
-
-
- ) : ( -
点击「开始计算」查看结果
- )} -
-
- -

- 社保/公积金基数按上年度月均工资核定,每人不同,在员工基本信息中设置。比例和基数上下限按版本管理,通常每年7月调整。 - 发薪批次计算时按批次月份自动匹配对应版本配置。 -

-
- ) -} diff --git a/frontend/src/pages/SocialInsurance.tsx b/frontend/src/pages/SocialInsurance.tsx new file mode 100644 index 0000000..f761703 --- /dev/null +++ b/frontend/src/pages/SocialInsurance.tsx @@ -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(null) + const [editItems, setEditItems] = useState>({}) + const [newVersion, setNewVersion] = useState({ + 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({ + queryKey: ['social-config'], + queryFn: async () => { + const res = await api.get('/social/config') as any + return res.data + }, + }) + + const { data: versions } = useQuery({ + 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({ + 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 ( +
+
+

社保公积金

+
+ + +
+
+ + {/* 当前生效版本信息 */} + {config && ( + +
+
+ 当前生效 + 生效月份:{config.effectiveFrom} + · {config.city} + {config.adjustmentDone && ( + 已调整员工基数 + )} +
+ +
+
+
+ 缴费基数下限 + ¥{fmt(config.baseMin)} +
+
+ 缴费基数上限 + ¥{fmt(config.baseMax)} +
+
+ 养老(企业/个人) + {config.pensionOrg}% / {config.pensionEmp}% +
+
+ 医疗(企业/个人) + {config.medicalOrg}% / {config.medicalEmp}% +
+
+ 失业(企业/个人) + {config.unemploymentOrg}% / {config.unemploymentEmp}% +
+
+ 工伤(企业) + {config.injuryOrg}% +
+
+ 生育(企业) + {config.maternityOrg}% +
+
+ 公积金(企业/个人) + {config.housingOrg}% / {config.housingEmp}% +
+
+
+ )} + + {/* 调整预览 */} + {showAdjust && adjustData && ( + +

+ 员工基数调整 +

+
+ +
+ 按当前版本基数上下限(¥{fmt(adjustData.baseMin)} ~ ¥{fmt(adjustData.baseMax)})调整全部在职员工社保/公积金缴费基数。 + 建议基数=上年月均工资按上下限裁剪。您可逐行修改,也可点击「采用建议值」或「保持原基数」。确认后保存,此操作只能执行一次。 +
+
+
+ + + 共 {adjustData.total} 名员工 +
+
+ + + + + + + + + + + + + + + + {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 ( + + + + + + + + + + + + ) + })} + +
姓名部门上年月均社保基数(当前)社保基数(建议)社保基数(新)公积金基数(当前)公积金基数(建议)公积金基数(新)
{item.name}{item.department}¥{fmt(item.avgSalary)}¥{fmt(item.oldSocialBase)}¥{fmt(item.suggestedSocialBase)} + setEditItems({ + ...editItems, + [item.employeeId]: { + socialBase: Number(e.target.value) || 0, + housingBase: edit?.housingBase ?? item.suggestedHousingBase, + }, + })} + /> + {socialChanged && } + ¥{fmt(item.oldHousingBase)}¥{fmt(item.suggestedHousingBase)} + setEditItems({ + ...editItems, + [item.employeeId]: { + socialBase: edit?.socialBase ?? item.suggestedSocialBase, + housingBase: Number(e.target.value) || 0, + }, + })} + /> + {housingChanged && } +
+
+
+ + +
+
+ )} + + {/* 版本历史 */} + {showVersions && ( + +

版本历史

+ {!versions || versions.length === 0 ? ( +
暂无版本记录
+ ) : ( +
+ + + + + + + + + + + + + + + + {versions.map((v: any) => ( + + + + + + + + + + + + ))} + +
生效月份失效月份城市基数下限基数上限养老%医疗%公积金%状态
{v.effectiveFrom}{v.effectiveTo || '—'}{v.city}¥{fmt(v.baseMin)}¥{fmt(v.baseMax)}{v.pensionOrg}/{v.pensionEmp}{v.medicalOrg}/{v.medicalEmp}{v.housingOrg}/{v.housingEmp} + {v.isCurrent + ? 当前 + : 历史 + } +
+
+ )} +
+ )} + + {/* 新建版本 */} + {showNewVersion && ( + +

新建配置版本

+
+
+ +
+ 新版本生效后,当前版本将自动归档。发薪批次计算时按批次月份匹配对应版本的配置。 + 通常每年7月社保调基时新建版本。 +
+
+
+
+ + setNewVersion({ ...newVersion, effectiveFrom: e.target.value })} /> +
+
+ + setNewVersion({ ...newVersion, city: e.target.value })} /> +
+
+ + setNewVersion({ ...newVersion, baseMin: Number(e.target.value) })} /> +
+
+ + setNewVersion({ ...newVersion, baseMax: Number(e.target.value) })} /> +
+
+
+
+ + setNewVersion({ ...newVersion, pensionOrg: Number(e.target.value) })} /> +
+
+ + setNewVersion({ ...newVersion, pensionEmp: Number(e.target.value) })} /> +
+
+ + setNewVersion({ ...newVersion, medicalOrg: Number(e.target.value) })} /> +
+
+ + setNewVersion({ ...newVersion, medicalEmp: Number(e.target.value) })} /> +
+
+ + setNewVersion({ ...newVersion, unemploymentOrg: Number(e.target.value) })} /> +
+
+ + setNewVersion({ ...newVersion, unemploymentEmp: Number(e.target.value) })} /> +
+
+ + setNewVersion({ ...newVersion, injuryOrg: Number(e.target.value) })} /> +
+
+ + setNewVersion({ ...newVersion, maternityOrg: Number(e.target.value) })} /> +
+
+ + setNewVersion({ ...newVersion, housingOrg: Number(e.target.value) })} /> +
+
+ + setNewVersion({ ...newVersion, housingEmp: Number(e.target.value) })} /> +
+
+
+ + +
+
+
+ )} + + {/* 试算工具 */} +
+ +

试算工具

+
+
+ + setBase(Number(e.target.value) || 0)} /> +
+ + {config && ( +
+ 当前配置:{config.city} | 基数范围 {fmt(config.baseMin)}~{fmt(config.baseMax)} +
+ )} +
+
+ + +

计算结果

+ {result ? ( +
+
+ 缴费基数:¥{fmt(result.actualBase)} + {result.capped && (已封顶)} + {result.floored && (已保底)} + {result.configVersion && | 配置版本:{result.configVersion}} +
+
+ + + + + + + + + + + + {result.items.map((item: any) => ( + + + + + + + + ))} + + + + + + + + +
险种企业%个人%企业缴纳个人缴纳
{item.name}{item.orgRate}%{item.empRate}%¥{fmt(item.orgAmount)}¥{fmt(item.empAmount)}
合计¥{fmt(result.totalOrg)}¥{fmt(result.totalEmp)}
+
+
+
+ 总费用 + ¥{fmt(result.total)} +
+
+ 企业承担 ¥{fmt(result.totalOrg)} + 个人承担 ¥{fmt(result.totalEmp)} +
+
+
+ ) : ( +
点击「开始计算」查看结果
+ )} +
+
+ +

+ 社保/公积金基数按上年度月均工资核定,每人不同,在员工基本信息中设置。比例和基数上下限按版本管理,通常每年7月调整。 + 发薪批次计算时按批次月份自动匹配对应版本配置。 +

+
+ ) +}