init: AI HR Compliance Assistant
This commit is contained in:
@@ -0,0 +1,604 @@
|
||||
import { useState, useMemo, useRef } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Calculator, AlertCircle, Info, Save, Check, Upload, Zap, Bell } from 'lucide-react'
|
||||
import api from '../lib/api'
|
||||
import Card from '../components/ui/Card'
|
||||
import Button from '../components/ui/Button'
|
||||
import { Input, Label, Select } from '../components/ui/Input'
|
||||
|
||||
type Tab = 'overtime' | 'social' | 'payslip'
|
||||
|
||||
export default function Money() {
|
||||
const [tab, setTab] = useState<Tab>('overtime')
|
||||
|
||||
const tabs: { key: Tab; label: string }[] = [
|
||||
{ key: 'overtime', label: '加班费计算' },
|
||||
{ key: 'social', label: '社保公积金' },
|
||||
{ key: 'payslip', label: '工资条管理' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-lg font-semibold">薪税计算</h1>
|
||||
|
||||
<div className="flex gap-1 border-b">
|
||||
{tabs.map((t) => (
|
||||
<button
|
||||
key={t.key}
|
||||
onClick={() => setTab(t.key)}
|
||||
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
||||
tab === t.key ? 'border-primary text-primary' : 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{tab === 'overtime' && <OvertimeCalculator />}
|
||||
{tab === 'social' && <SocialInsuranceCalculator />}
|
||||
{tab === 'payslip' && <PayslipManager />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function OvertimeCalculator() {
|
||||
const queryClient = useQueryClient()
|
||||
const [monthlyWage, setMonthlyWage] = useState(8000)
|
||||
const [weekdayHours, setWeekdayHours] = useState(0)
|
||||
const [weekendHours, setWeekendHours] = useState(0)
|
||||
const [holidayHours, setHolidayHours] = useState(0)
|
||||
const [selectedEmployee, setSelectedEmployee] = useState('')
|
||||
const [month, setMonth] = useState(new Date().toISOString().slice(0, 7))
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const { data: employees } = useQuery<{ items: { id: string; name: string; department: string; monthlySalary: number }[] }>({
|
||||
queryKey: ['employees-for-overtime'],
|
||||
queryFn: async () => {
|
||||
const res = await api.get('/employees', { params: { pageSize: 100 } }) as any
|
||||
return res.data
|
||||
},
|
||||
})
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: (data: any) => api.post('/payroll/overtime', data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['overtime-records'] })
|
||||
alert('加班费记录已保存')
|
||||
},
|
||||
})
|
||||
|
||||
const batchImportMutation = useMutation({
|
||||
mutationFn: (data: any[]) => api.post('/payroll/overtime/batch', data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['overtime-records'] })
|
||||
alert('批量导入成功')
|
||||
},
|
||||
})
|
||||
|
||||
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
const text = event.target?.result as string
|
||||
const lines = text.split('\n').filter(l => l.trim())
|
||||
const items: any[] = []
|
||||
const empList = employees?.items || []
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const cols = lines[i].split(',').map(c => c.trim())
|
||||
const empName = cols[0]
|
||||
const emp = empList.find(e => e.name === empName)
|
||||
if (!emp) continue
|
||||
const salary = Number(emp.monthlySalary) || 8000
|
||||
items.push({
|
||||
employeeId: emp.id,
|
||||
month: cols[4] || month,
|
||||
monthlyWage: salary,
|
||||
weekdayHours: Number(cols[1]) || 0,
|
||||
weekendHours: Number(cols[2]) || 0,
|
||||
holidayHours: Number(cols[3]) || 0,
|
||||
})
|
||||
}
|
||||
if (items.length > 0) {
|
||||
batchImportMutation.mutate(items)
|
||||
} else {
|
||||
alert('未匹配到员工,请确保CSV第一列为员工姓名')
|
||||
}
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
const result = useMemo(() => {
|
||||
const hourlyWage = monthlyWage / 21.75 / 8
|
||||
const weekdayPay = hourlyWage * 1.5 * weekdayHours
|
||||
const weekendPay = hourlyWage * 2.0 * weekendHours
|
||||
const holidayPay = hourlyWage * 3.0 * holidayHours
|
||||
const total = weekdayPay + weekendPay + holidayPay
|
||||
const totalHours = weekdayHours + weekendHours + holidayHours
|
||||
return { hourlyWage, weekdayPay, weekendPay, holidayPay, total, totalHours }
|
||||
}, [monthlyWage, weekdayHours, weekendHours, holidayHours])
|
||||
|
||||
return (
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<Card>
|
||||
<h2 className="font-medium mb-4">填写信息</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>关联员工(选填,自动填入月工资)</Label>
|
||||
<Select value={selectedEmployee} onChange={(e) => {
|
||||
setSelectedEmployee(e.target.value)
|
||||
const emp = employees?.items.find((x) => x.id === e.target.value)
|
||||
if (emp) {
|
||||
const salary = Number(emp.monthlySalary) || 8000
|
||||
setMonthlyWage(salary)
|
||||
}
|
||||
}}>
|
||||
<option value="">不关联员工</option>
|
||||
{employees?.items.map((emp) => (
|
||||
<option key={emp.id} value={emp.id}>{emp.name} - {emp.department}</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>月份</Label>
|
||||
<Input type="month" value={month} onChange={(e) => setMonth(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>月工资(元)</Label>
|
||||
<Input type="number" value={monthlyWage} onChange={(e) => setMonthlyWage(Number(e.target.value) || 0)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>工作日加班(小时)</Label>
|
||||
<Input type="number" value={weekdayHours} onChange={(e) => setWeekdayHours(Number(e.target.value) || 0)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>休息日加班(小时)</Label>
|
||||
<Input type="number" value={weekendHours} onChange={(e) => setWeekendHours(Number(e.target.value) || 0)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>法定节假日加班(小时)</Label>
|
||||
<Input type="number" value={holidayHours} onChange={(e) => setHolidayHours(Number(e.target.value) || 0)} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<h2 className="font-medium mb-4 flex items-center gap-2"><Calculator className="w-5 h-5" />计算结果</h2>
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm text-gray-500">小时工资:<span className="text-gray-900 font-medium">¥{result.hourlyWage.toFixed(2)}</span></div>
|
||||
<div className="space-y-2">
|
||||
<ResultRow label={`工作日 ${weekdayHours}h × 1.5`} value={result.weekdayPay} />
|
||||
<ResultRow label={`休息日 ${weekendHours}h × 2.0`} value={result.weekendPay} />
|
||||
<ResultRow label={`节假日 ${holidayHours}h × 3.0`} value={result.holidayPay} />
|
||||
</div>
|
||||
<div className="border-t pt-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">合计</span>
|
||||
<span className="text-xl font-bold text-primary">¥{result.total.toLocaleString(undefined, { maximumFractionDigits: 2 })}</span>
|
||||
</div>
|
||||
</div>
|
||||
{result.totalHours > 36 && (
|
||||
<div className="flex items-center gap-2 px-3 py-2 rounded-md bg-yellow-50 text-yellow-700 text-sm">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
月加班{result.totalHours}小时,超过36小时上限
|
||||
</div>
|
||||
)}
|
||||
{result.totalHours > 0 && result.totalHours <= 36 && (
|
||||
<div className="text-sm text-safe">月加班{result.totalHours}小时,未超36小时上限 ✅</div>
|
||||
)}
|
||||
{selectedEmployee && (
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => saveMutation.mutate({
|
||||
employeeId: selectedEmployee,
|
||||
month,
|
||||
monthlyWage,
|
||||
weekdayHours,
|
||||
weekendHours,
|
||||
holidayHours,
|
||||
})}
|
||||
disabled={saveMutation.isPending}
|
||||
>
|
||||
<Save className="w-4 h-4 mr-1" />
|
||||
{saveMutation.isPending ? '保存中...' : '保存加班费记录'}
|
||||
</Button>
|
||||
)}
|
||||
<div className="border-t pt-3">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".csv"
|
||||
className="hidden"
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={batchImportMutation.isPending}
|
||||
>
|
||||
<Upload className="w-4 h-4 mr-1" />
|
||||
{batchImportMutation.isPending ? '导入中...' : '批量导入加班数据(CSV)'}
|
||||
</Button>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
CSV格式:姓名,工作日加班,休息日加班,节假日加班,月份
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ResultRow({ label, value }: { label: string; value: number }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600">{label}</span>
|
||||
<span className="font-medium">¥{value.toLocaleString(undefined, { maximumFractionDigits: 2 })}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PayslipManager() {
|
||||
const queryClient = useQueryClient()
|
||||
const [month, setMonth] = useState(new Date().toISOString().slice(0, 7))
|
||||
const [showCreate, setShowCreate] = useState(false)
|
||||
const [createForm, setCreateForm] = useState({
|
||||
employeeId: '',
|
||||
baseSalary: 8000,
|
||||
allowance: 0,
|
||||
deduction: 0,
|
||||
})
|
||||
|
||||
const { data: employees } = useQuery<{ items: { id: string; name: string; department: string }[] }>({
|
||||
queryKey: ['employees-for-payslip'],
|
||||
queryFn: async () => {
|
||||
const res = await api.get('/employees', { params: { pageSize: 100 } }) as any
|
||||
return res.data
|
||||
},
|
||||
})
|
||||
|
||||
const { data: payslips, isLoading } = useQuery<any[]>({
|
||||
queryKey: ['payslips', month],
|
||||
queryFn: async () => {
|
||||
const res = await api.get('/payroll/payslip', { params: { month } }) as any
|
||||
return res.data
|
||||
},
|
||||
})
|
||||
|
||||
const generateMutation = useMutation({
|
||||
mutationFn: (data: any) => api.post('/payroll/payslip/generate', data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['payslips'] })
|
||||
setShowCreate(false)
|
||||
},
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: string) => api.delete(`/payroll/payslip/${id}`),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['payslips'] }),
|
||||
})
|
||||
|
||||
const batchGenerateMutation = useMutation({
|
||||
mutationFn: (data: any) => api.post('/payroll/payslip/batch-generate', data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['payslips'] })
|
||||
alert('批量生成完成')
|
||||
},
|
||||
})
|
||||
|
||||
const confirmedCount = payslips?.filter((p: any) => p.confirmedAt).length || 0
|
||||
const unconfirmedCount = payslips ? payslips.length - confirmedCount : 0
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<Input type="month" value={month} onChange={(e) => setMonth(e.target.value)} className="w-40" />
|
||||
{payslips && payslips.length > 0 && (
|
||||
<div className="flex gap-2 text-sm">
|
||||
<span className="px-2 py-0.5 rounded bg-gray-100 text-gray-600">共 {payslips.length} 条</span>
|
||||
<span className="px-2 py-0.5 rounded bg-green-50 text-safe">已确认 {confirmedCount}</span>
|
||||
<span className="px-2 py-0.5 rounded bg-amber-50 text-warning">未确认 {unconfirmedCount}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={() => setShowCreate(!showCreate)}>生成工资条</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => batchGenerateMutation.mutate({ month })}
|
||||
disabled={batchGenerateMutation.isPending}
|
||||
>
|
||||
<Zap className="w-4 h-4 mr-1" />
|
||||
{batchGenerateMutation.isPending ? '生成中...' : '一键全员生成'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showCreate && (
|
||||
<Card>
|
||||
<h2 className="font-medium mb-4">生成工资条</h2>
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>选择员工</Label>
|
||||
<Select value={createForm.employeeId} onChange={(e) => setCreateForm({ ...createForm, employeeId: e.target.value })}>
|
||||
<option value="">请选择</option>
|
||||
{employees?.items.map((emp) => (
|
||||
<option key={emp.id} value={emp.id}>{emp.name} - {emp.department}</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>基本工资</Label>
|
||||
<Input type="number" value={createForm.baseSalary} onChange={(e) => setCreateForm({ ...createForm, baseSalary: Number(e.target.value) || 0 })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>津贴</Label>
|
||||
<Input type="number" value={createForm.allowance} onChange={(e) => setCreateForm({ ...createForm, allowance: Number(e.target.value) || 0 })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>扣款</Label>
|
||||
<Input type="number" value={createForm.deduction} onChange={(e) => setCreateForm({ ...createForm, deduction: Number(e.target.value) || 0 })} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-2">
|
||||
<Button
|
||||
onClick={() => generateMutation.mutate({
|
||||
employeeId: createForm.employeeId,
|
||||
month,
|
||||
baseSalary: createForm.baseSalary,
|
||||
allowance: createForm.allowance,
|
||||
deduction: createForm.deduction,
|
||||
})}
|
||||
disabled={!createForm.employeeId || generateMutation.isPending}
|
||||
>
|
||||
{generateMutation.isPending ? '生成中...' : '确认生成(自动关联加班费)'}
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={() => setShowCreate(false)}>取消</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8 text-gray-400">加载中...</div>
|
||||
) : !payslips || payslips.length === 0 ? (
|
||||
<Card><div className="text-center py-8 text-gray-400">该月份暂无工资条记录</div></Card>
|
||||
) : (
|
||||
<Card>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-gray-500">
|
||||
<th className="py-2">员工</th>
|
||||
<th className="py-2">部门</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>
|
||||
<th className="py-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{payslips.map((p: any) => (
|
||||
<tr key={p.id} className="border-b last:border-0">
|
||||
<td className="py-2">{p.employee?.name}</td>
|
||||
<td className="py-2 text-gray-500">{p.employee?.department}</td>
|
||||
<td className="py-2 text-right">¥{p.baseSalary.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</td>
|
||||
<td className="py-2 text-right">¥{p.overtimePay.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</td>
|
||||
<td className="py-2 text-right">¥{p.allowance.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</td>
|
||||
<td className="py-2 text-right text-danger">{p.deduction > 0 ? '-¥' + p.deduction.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : '¥0'}</td>
|
||||
<td className="py-2 text-right font-bold">¥{p.totalPay.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</td>
|
||||
<td className="py-2 text-center">
|
||||
{p.confirmedAt ? (
|
||||
<span className="inline-flex items-center gap-1 text-safe text-xs">
|
||||
<Check className="w-3 h-3" />已确认
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-warning text-xs">未确认</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2">
|
||||
<button
|
||||
onClick={() => deleteMutation.mutate(p.id)}
|
||||
className="text-xs text-gray-400 hover:text-danger"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SocialInsuranceCalculator() {
|
||||
const queryClient = useQueryClient()
|
||||
const [base, setBase] = useState(8000)
|
||||
const [showConfig, setShowConfig] = useState(false)
|
||||
const [configForm, setConfigForm] = useState<any>({})
|
||||
|
||||
const { data: config } = useQuery<any>({
|
||||
queryKey: ['social-config'],
|
||||
queryFn: async () => {
|
||||
const res = await api.get('/social/config') as any
|
||||
return res.data
|
||||
},
|
||||
})
|
||||
|
||||
const { data: result, mutate: calcMutate, isPending } = useMutation<any>({
|
||||
mutationFn: async () => {
|
||||
const res = await api.post('/social/calculate', { base }) as any
|
||||
return res.data
|
||||
},
|
||||
})
|
||||
|
||||
const updateConfigMutation = useMutation({
|
||||
mutationFn: (data: any) => api.put('/social/config', data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['social-config'] }),
|
||||
})
|
||||
|
||||
useMemo(() => {
|
||||
if (config) setConfigForm(config)
|
||||
}, [config])
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="font-medium">社保公积金计算</h2>
|
||||
<Button variant="secondary" size="sm" onClick={() => setShowConfig(!showConfig)}>
|
||||
{showConfig ? '收起配置' : '配置比例'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{showConfig && (
|
||||
<Card>
|
||||
<h3 className="font-medium mb-4 text-sm">社保比例配置({config?.city || '北京'})</h3>
|
||||
<div className="grid md:grid-cols-3 gap-3 text-sm">
|
||||
<div>
|
||||
<Label>城市</Label>
|
||||
<Input value={configForm.city || ''} onChange={(e) => setConfigForm({ ...configForm, city: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>养老(企业%)</Label>
|
||||
<Input type="number" value={configForm.pensionOrg || 0} onChange={(e) => setConfigForm({ ...configForm, pensionOrg: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>养老(个人%)</Label>
|
||||
<Input type="number" value={configForm.pensionEmp || 0} onChange={(e) => setConfigForm({ ...configForm, pensionEmp: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>医疗(企业%)</Label>
|
||||
<Input type="number" value={configForm.medicalOrg || 0} onChange={(e) => setConfigForm({ ...configForm, medicalOrg: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>医疗(个人%)</Label>
|
||||
<Input type="number" value={configForm.medicalEmp || 0} onChange={(e) => setConfigForm({ ...configForm, medicalEmp: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>失业(企业%)</Label>
|
||||
<Input type="number" value={configForm.unemploymentOrg || 0} onChange={(e) => setConfigForm({ ...configForm, unemploymentOrg: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>失业(个人%)</Label>
|
||||
<Input type="number" value={configForm.unemploymentEmp || 0} onChange={(e) => setConfigForm({ ...configForm, unemploymentEmp: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>工伤(企业%)</Label>
|
||||
<Input type="number" value={configForm.injuryOrg || 0} onChange={(e) => setConfigForm({ ...configForm, injuryOrg: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>生育(企业%)</Label>
|
||||
<Input type="number" value={configForm.maternityOrg || 0} onChange={(e) => setConfigForm({ ...configForm, maternityOrg: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>公积金(企业%)</Label>
|
||||
<Input type="number" value={configForm.housingOrg || 0} onChange={(e) => setConfigForm({ ...configForm, housingOrg: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>公积金(个人%)</Label>
|
||||
<Input type="number" value={configForm.housingEmp || 0} onChange={(e) => setConfigForm({ ...configForm, housingEmp: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>缴费基数下限</Label>
|
||||
<Input type="number" value={configForm.baseMin || 0} onChange={(e) => setConfigForm({ ...configForm, baseMin: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>缴费基数上限</Label>
|
||||
<Input type="number" value={configForm.baseMax || 0} onChange={(e) => setConfigForm({ ...configForm, baseMax: Number(e.target.value) })} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<Button size="sm" onClick={() => updateConfigMutation.mutate(configForm)} disabled={updateConfigMutation.isPending}>
|
||||
{updateConfigMutation.isPending ? '保存中...' : '保存配置'}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<Card>
|
||||
<h2 className="font-medium mb-4">填写信息</h2>
|
||||
<div className="space-y-4">
|
||||
<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} | 基数范围 {config.baseMin}~{config.baseMax}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<h2 className="font-medium mb-4 flex items-center gap-2"><Calculator className="w-5 h-5" />计算结果</h2>
|
||||
{result ? (
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm text-gray-500">
|
||||
缴费基数:<span className="text-gray-900 font-medium">¥{result.actualBase.toLocaleString()}</span>
|
||||
{result.capped && <span className="text-warning ml-2">(已封顶)</span>}
|
||||
{result.floored && <span className="text-warning ml-2">(已保底)</span>}
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<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">¥{item.orgAmount.toFixed(2)}</td>
|
||||
<td className="py-1.5 text-right">¥{item.empAmount.toFixed(2)}</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">¥{result.totalOrg.toFixed(2)}</td>
|
||||
<td className="py-2 text-right text-warning">¥{result.totalEmp.toFixed(2)}</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-xl font-bold text-primary">¥{result.total.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
企业承担 ¥{result.totalOrg.toFixed(2)} + 个人承担 ¥{result.totalEmp.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-400 text-sm">点击「开始计算」查看结果</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user