/** * 费率管理页面 — 地域费率套维护(与评估引擎对齐)。 * * 专业设计: * - 以「地域费率套」为单位维护(一套 = 五险单位费率 + 公积金 + 增值税 + 附加税) * - 展示引擎内置默认费率(全国/上海/北京/广东)作为对照基准 * - 与默认对比:偏离默认值的项高亮 * - 复核流程:编辑即重置待复核,复核后驱动评估盈利测算 * - 实时显示社保单位合计、用工成本加载估算 */ import { useCallback, useEffect, useMemo, useState } from 'react'; import { colorVar, FONT_FAMILY, RADIUS, space, typographyStyle } from '../design-system/components/styles.js'; import { Card, Icon } from '../design-system/index.js'; import { fetchEngineDefaults, fetchRegionRates, saveRegionRate, reviewRegionRate, deleteRegionRate, fetchMinWages, saveMinWage, deleteMinWageApi, type RegionRates, type RegionRateRecord, type MinWageItem, } from '../api/client.js'; import { useAuthStore } from '../stores/authStore.js'; /** 费率项定义(路径 + 标签 + 分组 + 单位/个人说明)。 */ const RATE_FIELDS: ReadonlyArray<{ path: string; label: string; group: string; hint?: string }> = [ { path: 'socialInsurance.pension', label: '养老保险(单位)', group: '社会保险(单位部分)', hint: '通常 14%~16%' }, { path: 'socialInsurance.medical', label: '医疗保险(单位)', group: '社会保险(单位部分)', hint: '通常 4.5%~10%' }, { path: 'socialInsurance.unemployment', label: '失业保险(单位)', group: '社会保险(单位部分)', hint: '通常 0.5%~0.8%' }, { path: 'socialInsurance.injury', label: '工伤保险(单位)', group: '社会保险(单位部分)', hint: '按行业风险 0.16%~1.9%' }, { path: 'socialInsurance.maternity', label: '生育保险(单位)', group: '社会保险(单位部分)', hint: '与医疗并轨地区填 0' }, { path: 'housingFund', label: '住房公积金(单位)', group: '公积金', hint: '5%~12%,按属地' }, { path: 'vatGeneralRate', label: '增值税(一般计税)', group: '税率', hint: '现代服务业 6%' }, { path: 'vatSimplifiedRate', label: '增值税(简易/差额)', group: '税率', hint: '劳务派遣差额 5%' }, { path: 'surchargeRate', label: '附加税费(占增值税)', group: '税率', hint: '城建+教育附加,约 12%' }, ]; const REGIONS = ['全国默认', '上海', '北京', '广东', '深圳', '江苏', '浙江', '河北', '四川', '重庆', '湖北', '天津']; function getPath(obj: RegionRates, path: string): number { const parts = path.split('.'); let cur: unknown = obj; for (const p of parts) cur = (cur as Record)?.[p]; return typeof cur === 'number' ? cur : 0; } function setPath(obj: RegionRates, path: string, value: number): RegionRates { const clone: RegionRates = JSON.parse(JSON.stringify(obj)); const parts = path.split('.'); let cur: Record = clone as unknown as Record; for (let i = 0; i < parts.length - 1; i += 1) cur = cur[parts[i]!] as Record; cur[parts[parts.length - 1]!] = value; return clone; } function emptyRates(regionName: string): RegionRates { return { regionName, socialInsurance: { pension: 0, medical: 0, unemployment: 0, injury: 0, maternity: 0 }, housingFund: 0, vatGeneralRate: 0.06, vatSimplifiedRate: 0.05, surchargeRate: 0.12, }; } function socialTotal(r: RegionRates): number { const s = r.socialInsurance; return s.pension + s.medical + s.unemployment + s.injury + s.maternity; } /** 全成本加载系数估算(应发=1,加社保+公积金)。 */ function loadingFactor(r: RegionRates): number { return 1 + socialTotal(r) + r.housingFund; } export function RateManagement(): JSX.Element { const { user } = useAuthStore(); const [defaults, setDefaults] = useState<{ national: RegionRates; regions: Record } | null>(null); const [records, setRecords] = useState([]); const [loading, setLoading] = useState(true); const [editRegion, setEditRegion] = useState(''); const [editRates, setEditRates] = useState(null); const load = useCallback(() => { setLoading(true); Promise.all([fetchEngineDefaults(), fetchRegionRates()]) .then(([d, r]) => { setDefaults(d); setRecords(r); }) .finally(() => setLoading(false)); }, []); useEffect(() => { load(); }, [load]); // 当前编辑地域的"引擎默认"基准(用于对比高亮) const baseline = useMemo(() => { if (defaults === null || editRegion === '') return null; const key = ['上海', '北京', '广东'].find((k) => editRegion.includes(k)); return key ? (defaults.regions[key] ?? defaults.national) : defaults.national; }, [defaults, editRegion]); function startEdit(region: string): void { const existing = records.find((r) => r.region === region); if (existing) { setEditRates(existing.rates); } else { // 以引擎默认为初始值 const key = ['上海', '北京', '广东'].find((k) => region.includes(k)); const init = defaults ? (key ? (defaults.regions[key] ?? defaults.national) : defaults.national) : emptyRates(region); setEditRates({ ...JSON.parse(JSON.stringify(init)), regionName: region }); } setEditRegion(region); } async function handleSave(): Promise { if (editRates === null || editRegion === '') return; await saveRegionRate(editRegion, { ...editRates, regionName: editRegion }, user?.username); setEditRegion(''); setEditRates(null); load(); } const inputStyle: React.CSSProperties = { padding: `${space(1)}px ${space(2)}px`, border: `1px solid ${colorVar('color.border.default')}`, borderRadius: `${RADIUS.sm}px`, fontFamily: FONT_FAMILY, ...typographyStyle('body'), backgroundColor: colorVar('color.bg.canvas'), color: colorVar('color.text.primary'), width: 90, textAlign: 'right', }; const groups = [...new Set(RATE_FIELDS.map((f) => f.group))]; return (

费率管理

以「地域费率套」维护社保、公积金与税率,与评估引擎对齐。维护并复核后,该地域评估将自动采用此费率(覆盖引擎内置默认)。所有费率为行业近似值,须经财务复核。

{loading ?

加载中…

: ( <> {/* 最低工资标准(驱动"低于最低工资"红线) */}
{/* 已维护的地域费率套 */} {records.length === 0 ? (

尚未维护任何地域费率套。系统当前使用引擎内置默认费率。点击下方地域按钮开始维护。

) : (
{['地域', '社保单位合计', '公积金', '增值税(一般)', '附加税', '加载系数', '复核状态', '更新', '操作'].map((h) => ( ))} {records.map((rec) => ( ))}
{h}
{rec.region} {(socialTotal(rec.rates) * 100).toFixed(2)}% {(rec.rates.housingFund * 100).toFixed(1)}% {(rec.rates.vatGeneralRate * 100).toFixed(1)}% {(rec.rates.surchargeRate * 100).toFixed(0)}% {loadingFactor(rec.rates).toFixed(3)}× {rec.reviewed ? 已生效 : 待复核} {rec.updatedBy ?? '—'}
{!rec.reviewed && }
)}
新增/维护地域: {REGIONS.filter((r) => !records.some((rec) => rec.region === r)).map((r) => ( ))}
{/* 编辑表单 */} {editRates !== null && (

填写各项费率(小数,如 0.16 表示 16%)。橙色表示偏离引擎默认值。保存后需复核方生效。

{groups.map((g) => (
{g}
{RATE_FIELDS.filter((f) => f.group === g).map((f) => { const val = getPath(editRates, f.path); const baseVal = baseline ? getPath(baseline, f.path) : val; const deviated = Math.abs(val - baseVal) > 1e-6; return (
setEditRates((r) => r ? setPath(r, f.path, Number(e.target.value) || 0) : r)} inputMode="decimal" /> {(val * 100).toFixed(2)}%
{f.hint}{deviated ? ` · 默认 ${(baseVal * 100).toFixed(2)}%` : ''}
); })}
))} {/* 汇总 */}
)} {/* 引擎内置默认费率(只读对照) */} {defaults !== null && (

未维护费率套的地域评估时采用以下默认值。

{['地域', '养老', '医疗', '失业', '工伤', '生育', '公积金', '增值税', '附加税', '加载系数'].map((h) => ( ))} {[defaults.national, ...Object.values(defaults.regions).filter((r) => r.regionName !== '全国(默认)')].map((r) => ( ))}
{h}
{r.regionName} {(r.socialInsurance.pension * 100).toFixed(1)}% {(r.socialInsurance.medical * 100).toFixed(1)}% {(r.socialInsurance.unemployment * 100).toFixed(2)}% {(r.socialInsurance.injury * 100).toFixed(2)}% {(r.socialInsurance.maternity * 100).toFixed(2)}% {(r.housingFund * 100).toFixed(1)}% {(r.vatGeneralRate * 100).toFixed(1)}% {(r.surchargeRate * 100).toFixed(0)}% {loadingFactor(r).toFixed(3)}×
)} )}
); } const tdStyle: React.CSSProperties = { padding: `${space(2)}px`, borderBottom: '1px solid var(--color-border-default)', fontSize: '14px' }; const defTd: React.CSSProperties = { padding: `${space(1)}px ${space(2)}px`, borderBottom: '1px solid var(--color-border-default)', fontSize: '13px', whiteSpace: 'nowrap' }; function linkBtn(color: string): React.CSSProperties { return { cursor: 'pointer', border: 'none', background: 'none', color, fontWeight: 600, fontSize: '12px' }; } function Summary({ label, value, highlight, small }: { label: string; value: string; highlight?: boolean; small?: boolean }): JSX.Element { return (
{label}
{value}
); } /** 最低工资标准维护面板:驱动"低于最低工资"红线自动比对。 */ function MinWagePanel(): JSX.Element { const [items, setItems] = useState([]); const [region, setRegion] = useState(''); const [wage, setWage] = useState(''); const reload = useCallback(() => { fetchMinWages().then(setItems).catch(() => setItems([])); }, []); useEffect(() => { reload(); }, [reload]); const inputStyle: React.CSSProperties = { padding: `${space(1)}px ${space(2)}px`, border: `1px solid ${colorVar('color.border.default')}`, borderRadius: `${RADIUS.md}px`, fontFamily: FONT_FAMILY, ...typographyStyle('caption'), backgroundColor: colorVar('color.bg.canvas'), color: colorVar('color.text.primary'), }; async function handleSave(): Promise { const w = Number(wage); if (region.trim() === '' || !Number.isFinite(w) || w <= 0) return; await saveMinWage(region.trim(), w); setRegion(''); setWage(''); reload(); } return (

评估时按岗位应发工资与所在地域最低工资比对,低于标准的岗位数将驱动「低于最低工资」红线。近似默认值须经 HR/财务按当地官方标准复核。

setRegion(e.target.value)} placeholder="地域(如 北京)" /> setWage(e.target.value)} placeholder="月最低工资(元)" inputMode="decimal" />
{items.map((m) => (
{m.region}
{m.monthlyWage.toLocaleString('zh-CN')} 元
))}
); }