feat: 社保公积金版本管理 + 员工基数调整 + 加班费计算优化 + UI组件改进
- SocialInsuranceConfig 版本化(effectiveFrom/effectiveTo/isCurrent/adjustmentDone) - 社保配置版本管理接口(列表/新建/按月获取/当前版本) - 员工基数调整:预览全部在职员工、上年月均工资计算建议基数、可编辑表格、确认后批量保存 - payroll.service 按批次月份匹配对应版本社保配置 - risk.service / seed.ts 同步更新 - 前端社保tab重构为版本管理+调整+试算 - 加班费计算三步流程、CSV导入、批次导入 - UI组件、Dashboard、合同、薪酬等页面优化
This commit is contained in:
@@ -275,8 +275,8 @@ export default function Termination() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-lg font-semibold">解聘助手</h1>
|
||||
<div className="space-y-3">
|
||||
<h1 className="text-xs font-medium">解聘助手</h1>
|
||||
|
||||
{/* 进度条 */}
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -289,11 +289,11 @@ export default function Termination() {
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<div className="mb-2 text-sm text-gray-500">Step {step + 1}/5:{STEPS[step]}</div>
|
||||
<div className="mb-2 text-xs text-gray-500">Step {step + 1}/5:{STEPS[step]}</div>
|
||||
|
||||
{/* Step 1: 选择员工 */}
|
||||
{step === 0 && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label>选择员工</Label>
|
||||
<Select value={employeeId} onChange={(e) => setEmployeeId(e.target.value)}>
|
||||
@@ -304,7 +304,7 @@ export default function Termination() {
|
||||
</Select>
|
||||
</div>
|
||||
{selectedEmployee && (
|
||||
<div className="text-sm text-gray-600 bg-gray-50 p-3 rounded-md space-y-1">
|
||||
<div className="text-xs text-gray-600 bg-gray-50 p-3 rounded-md space-y-1">
|
||||
<div className="font-medium">{selectedEmployee.name}({selectedEmployee.department})</div>
|
||||
<div>入职日期:{selectedEmployee.hireDate?.toString().slice(0, 10)}</div>
|
||||
<div>月工资:¥{fmt(selectedEmployee.monthlySalary)}</div>
|
||||
@@ -330,11 +330,11 @@ export default function Termination() {
|
||||
)}
|
||||
{profile && suggestions.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">📋 解聘方式建议</div>
|
||||
<div className="text-xs font-medium">📋 解聘方式建议</div>
|
||||
{suggestions.map((s, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`px-3 py-2 rounded-md text-sm ${s.reason === '' ? 'bg-red-50 text-red-700' : 'bg-blue-50 text-blue-700'}`}
|
||||
className={`px-3 py-2 rounded-md text-xs ${s.reason === '' ? 'bg-red-50 text-red-700' : 'bg-blue-50 text-blue-700'}`}
|
||||
>
|
||||
<div className="font-medium">{s.label}</div>
|
||||
<div className="text-xs mt-0.5">{s.why}</div>
|
||||
@@ -343,17 +343,17 @@ export default function Termination() {
|
||||
</div>
|
||||
)}
|
||||
{employeeId && !profile && (
|
||||
<div className="text-sm text-gray-400">加载员工档案中...</div>
|
||||
<div className="text-xs text-gray-400">加载员工档案中...</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: 解聘方式 */}
|
||||
{step === 1 && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
{suggestions.length > 0 && (
|
||||
<div className="bg-blue-50 rounded-md p-3 space-y-1">
|
||||
<div className="text-sm font-medium text-blue-700">💡 系统建议</div>
|
||||
<div className="text-xs font-medium text-blue-700">💡 系统建议</div>
|
||||
{suggestions.filter((s) => s.reason).map((s, i) => (
|
||||
<div key={i} className="text-xs text-blue-600">
|
||||
{s.label}:{s.why}
|
||||
@@ -371,7 +371,7 @@ export default function Termination() {
|
||||
>
|
||||
<input type="radio" name="reason" value={r.value} checked={reason === r.value} onChange={(e) => setReason(e.target.value)} className="mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<div className="text-sm flex items-center gap-2">
|
||||
<div className="text-xs flex items-center gap-2">
|
||||
{r.label}
|
||||
{suggested && <span className="text-xs text-primary font-medium">推荐</span>}
|
||||
</div>
|
||||
@@ -392,12 +392,12 @@ export default function Termination() {
|
||||
{riskAssessment && riskAssessment.warnings.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{riskAssessment.warnings.map((w, i) => (
|
||||
<div key={i} className="flex items-center gap-2 px-3 py-2 rounded-md bg-red-50 text-red-700 text-sm">
|
||||
<div key={i} className="flex items-center gap-2 px-3 py-2 rounded-md bg-red-50 text-red-700 text-xs">
|
||||
<AlertTriangle className="w-4 h-4 shrink-0" />
|
||||
{w}
|
||||
</div>
|
||||
))}
|
||||
<label className="flex items-center gap-2 text-sm px-3 py-2 rounded-md bg-yellow-50 text-yellow-800">
|
||||
<label className="flex items-center gap-2 text-xs px-3 py-2 rounded-md bg-yellow-50 text-yellow-800">
|
||||
<input type="checkbox" checked={acknowledgeRisk} onChange={(e) => setAcknowledgeRisk(e.target.checked)} />
|
||||
我已了解风险,继续操作
|
||||
</label>
|
||||
@@ -416,7 +416,7 @@ export default function Termination() {
|
||||
checked={checklist[item.key] || false}
|
||||
onChange={(e) => setChecklist({ ...checklist, [item.key]: e.target.checked })}
|
||||
/>
|
||||
<span className="text-sm">{item.label}</span>
|
||||
<span className="text-xs">{item.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
@@ -424,15 +424,15 @@ export default function Termination() {
|
||||
|
||||
{/* Step 4: 费用结算 */}
|
||||
{step === 3 && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label>当地社平工资(选填)</Label>
|
||||
<Input type="number" value={socialAvgWage} onChange={(e) => setSocialAvgWage(Number(e.target.value) || 0)} placeholder="用于三倍封顶计算" />
|
||||
</div>
|
||||
{costResult && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
{/* 员工概况 */}
|
||||
<div className="text-sm text-gray-600 bg-gray-50 p-3 rounded-md space-y-1">
|
||||
<div className="text-xs text-gray-600 bg-gray-50 p-3 rounded-md space-y-1">
|
||||
<div className="font-medium">{selectedEmployee?.name}({selectedEmployee?.department})</div>
|
||||
<div>工作年限:{costResult.years}年{costResult.remainingMonths}个月</div>
|
||||
<div>月工资:¥{fmt(costResult.wage)}/月</div>
|
||||
@@ -443,7 +443,7 @@ export default function Termination() {
|
||||
|
||||
{/* 经济补偿金 / 赔偿金 */}
|
||||
{costResult.noComp ? (
|
||||
<div className="px-3 py-2 rounded-md bg-gray-50 text-gray-700 text-sm">
|
||||
<div className="px-3 py-2 rounded-md bg-gray-50 text-gray-700 text-xs">
|
||||
员工过错解除,无需支付经济补偿金
|
||||
</div>
|
||||
) : (
|
||||
@@ -452,22 +452,22 @@ export default function Termination() {
|
||||
<Calculator className="w-4 h-4" />
|
||||
{costResult.isIllegal ? '违法解除赔偿金' : '经济补偿金'}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">补偿月数:{costResult.cappedMonths}个月</div>
|
||||
<div className="text-sm text-gray-500">计算基数:¥{fmt(costResult.cappedWage)}/月</div>
|
||||
<div className="text-xs text-gray-500">补偿月数:{costResult.cappedMonths}个月</div>
|
||||
<div className="text-xs text-gray-500">计算基数:¥{fmt(costResult.cappedWage)}/月</div>
|
||||
{costResult.isIllegal && (
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-500">经济补偿金</span>
|
||||
<span>¥{fmt(costResult.basePay)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">{costResult.isIllegal ? '赔偿金(×2)' : '补偿金'}</span>
|
||||
<span className={`text-lg font-bold ${costResult.isIllegal ? 'text-danger' : 'text-primary'}`}>
|
||||
<span className={`text-base font-bold ${costResult.isIllegal ? 'text-danger' : 'text-primary'}`}>
|
||||
¥{fmt(costResult.severancePay)}
|
||||
</span>
|
||||
</div>
|
||||
{costResult.noticePay > 0 && (
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-500">代通知金</span>
|
||||
<span>¥{fmt(costResult.noticePay)}</span>
|
||||
</div>
|
||||
@@ -491,12 +491,12 @@ export default function Termination() {
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
未签劳动合同双倍工资
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">双倍工资起算:{costResult.doubleStartDate}</div>
|
||||
<div className="text-sm text-gray-500">双倍工资截止:{costResult.doubleEndDate}</div>
|
||||
<div className="text-sm text-gray-500">赔偿月数:{costResult.doubleMonths}个月</div>
|
||||
<div className="text-xs text-gray-500">双倍工资起算:{costResult.doubleStartDate}</div>
|
||||
<div className="text-xs text-gray-500">双倍工资截止:{costResult.doubleEndDate}</div>
|
||||
<div className="text-xs text-gray-500">赔偿月数:{costResult.doubleMonths}个月</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">双倍工资赔偿</span>
|
||||
<span className="text-lg font-bold text-warning">¥{fmt(costResult.doublePay)}</span>
|
||||
<span className="text-base font-bold text-warning">¥{fmt(costResult.doublePay)}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">({costResult.doubleMonths}个月 × ¥{fmt(costResult.wage)})</div>
|
||||
<div className="flex items-start gap-2 px-3 py-2 rounded-md bg-yellow-50 text-yellow-800 text-xs">
|
||||
@@ -510,12 +510,12 @@ export default function Termination() {
|
||||
<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-danger">¥{fmt(costResult.grandTotal)}</span>
|
||||
<span className="text-lg font-bold text-danger">¥{fmt(costResult.grandTotal)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!costResult.noComp && (
|
||||
<div className="flex items-start gap-2 px-3 py-2 rounded-md bg-blue-50 text-blue-700 text-sm">
|
||||
<div className="flex items-start gap-2 px-3 py-2 rounded-md bg-blue-50 text-blue-700 text-xs">
|
||||
<Info className="w-4 h-4 mt-0.5 shrink-0" />
|
||||
<span>满1年补1个月,满6个月不满1年按1年算,不满6个月补半个月</span>
|
||||
</div>
|
||||
@@ -527,18 +527,18 @@ export default function Termination() {
|
||||
|
||||
{/* Step 5: 解聘材料 */}
|
||||
{step === 4 && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
{saveMutation.isError ? (
|
||||
<div className="text-center py-8">
|
||||
<AlertTriangle className="w-12 h-12 text-danger mx-auto" />
|
||||
<div className="text-danger font-medium mt-2">保存失败</div>
|
||||
<div className="text-sm text-gray-500">{(saveMutation.error as any)?.response?.data?.error?.message || '请稍后重试'}</div>
|
||||
<div className="text-xs text-gray-500">{(saveMutation.error as any)?.response?.data?.error?.message || '请稍后重试'}</div>
|
||||
<Button onClick={() => setStep(3)} className="mt-4">返回修改</Button>
|
||||
</div>
|
||||
) : saveMutation.isPending ? (
|
||||
<div className="text-center py-8 text-gray-400">保存中...</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
{/* 成功提示 */}
|
||||
<div className="flex items-center gap-2 text-safe">
|
||||
<Check className="w-5 h-5" />
|
||||
@@ -556,11 +556,11 @@ export default function Termination() {
|
||||
</div>
|
||||
|
||||
{/* 1. 解聘通知书 */}
|
||||
<div className="border rounded-lg p-6 space-y-4 print:shadow-none">
|
||||
<div className="border rounded-lg p-6 space-y-3 print:shadow-none">
|
||||
<div className="text-center">
|
||||
<h2 className="text-lg font-bold">解除劳动合同通知书</h2>
|
||||
<h2 className="text-base font-bold">解除劳动合同通知书</h2>
|
||||
</div>
|
||||
<div className="text-sm text-gray-700 space-y-3">
|
||||
<div className="text-xs text-gray-700 space-y-3">
|
||||
<p><strong>{selectedEmployee?.name}</strong> 先生/女士:</p>
|
||||
<p>
|
||||
您于 <strong>{selectedEmployee?.hireDate?.toString().slice(0, 10)}</strong> 入职我公司{selectedEmployee?.department}部门。
|
||||
@@ -600,7 +600,7 @@ export default function Termination() {
|
||||
{costResult && (
|
||||
<div className="border rounded-lg p-4 space-y-2">
|
||||
<h3 className="font-medium flex items-center gap-2"><Calculator className="w-4 h-4" />费用结算明细</h3>
|
||||
<div className="text-sm space-y-1">
|
||||
<div className="text-xs space-y-1">
|
||||
<div className="flex justify-between"><span>工作年限</span><span>{costResult.years}年{costResult.remainingMonths}个月</span></div>
|
||||
<div className="flex justify-between"><span>月工资</span><span>¥{fmt(costResult.wage)}/月</span></div>
|
||||
{costResult.capped && <div className="text-warning">⚠️ 工资超过社平3倍,已按三倍封顶且最多补偿12个月</div>}
|
||||
@@ -623,7 +623,7 @@ export default function Termination() {
|
||||
{/* 3. 合规检查清单 */}
|
||||
<div className="border rounded-lg p-4 space-y-2">
|
||||
<h3 className="font-medium flex items-center gap-2"><Shield className="w-4 h-4" />合规检查清单</h3>
|
||||
<div className="text-sm space-y-1">
|
||||
<div className="text-xs space-y-1">
|
||||
{checklistItems?.map((item) => (
|
||||
<div key={item.key} className="flex items-center gap-2">
|
||||
<span className={checklist[item.key] ? 'text-safe' : 'text-danger'}>
|
||||
@@ -661,7 +661,7 @@ export default function Termination() {
|
||||
}, {})
|
||||
return Object.entries(grouped).map(([category, items]) => (
|
||||
<div key={category} className="space-y-1">
|
||||
<div className="text-sm font-medium text-gray-700">{category as string}</div>
|
||||
<div className="text-xs font-medium text-gray-700">{category as string}</div>
|
||||
{(items as any[]).map((e: any, i: number) => (
|
||||
<div key={i} className="text-xs text-gray-600 pl-4 border-l-2 border-gray-200 ml-1">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -677,7 +677,7 @@ export default function Termination() {
|
||||
})()}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-sm text-gray-400">加载证据链中...</div>
|
||||
<div className="text-xs text-gray-400">加载证据链中...</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user