feat: 分页组件、Dashboard待办图标、归档与工资条解耦、总览数据优化

- 新增公用 Pagination 组件,Roster/Money/Dashboard 列表加分页
- Dashboard 待办按类型显示不同图标(合同/薪资/解聘/月度)
- 待办分为「风险提醒」「月度任务」两个顶层 tab
- 归档与工资条生成解耦:归档只锁定批次,工资条单独生成
- 工资条管理新增「从批次汇总生成」按钮
- Dashboard 总览优先从已归档批次 BatchEntry 汇总数据
- 新增工资条生成待办提醒,生成后自动标记完成
- 修复高风险统计只含 CONTRACT/TERMINATION 类型
- 修复月度任务去重逻辑覆盖 SALARY 类型
This commit is contained in:
freedakgmail
2026-07-23 13:42:28 +08:00
parent 820579e98d
commit 2a09d31ccc
20 changed files with 2300 additions and 235 deletions
+24 -21
View File
@@ -7,6 +7,9 @@ import Button from '../components/ui/Button'
import { Input, Label, Select } from '../components/ui/Input'
import Signal from '../components/ui/Signal'
// 金额格式化:保留两位小数 + 千分位
const fmt = (n: number) => (n || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
const REASONS = [
{ value: 'NEGOTIATED', label: '协商解除(双方同意分开了)', legalBasis: '《劳动合同法》第36条' },
{ value: 'FAULT', label: '员工犯错被辞退(严重违纪/失职等)', legalBasis: '《劳动合同法》第39条' },
@@ -304,7 +307,7 @@ export default function Termination() {
<div className="text-sm 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>¥{selectedEmployee.monthlySalary.toLocaleString()}</div>
<div>¥{fmt(selectedEmployee.monthlySalary)}</div>
{selectedEmployee.latestContract ? (
<div>{selectedEmployee.latestContract.contractType === 'UNSIGNED' ? '未签订' : `签订于 ${selectedEmployee.latestContract.signDate?.slice(0, 10) || '未知'}`}</div>
) : (
@@ -432,7 +435,7 @@ export default function Termination() {
<div className="text-sm 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>¥{costResult.wage.toLocaleString()}/</div>
<div>¥{fmt(costResult.wage)}/</div>
{costResult.capped && (
<div className="text-warning"> 312</div>
)}
@@ -450,27 +453,27 @@ export default function Termination() {
{costResult.isIllegal ? '违法解除赔偿金' : '经济补偿金'}
</div>
<div className="text-sm text-gray-500">{costResult.cappedMonths}</div>
<div className="text-sm text-gray-500">¥{costResult.cappedWage.toLocaleString()}/</div>
<div className="text-sm text-gray-500">¥{fmt(costResult.cappedWage)}/</div>
{costResult.isIllegal && (
<div className="flex items-center justify-between text-sm">
<span className="text-gray-500"></span>
<span>¥{costResult.basePay.toLocaleString(undefined, { maximumFractionDigits: 2 })}</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'}`}>
¥{costResult.severancePay.toLocaleString(undefined, { maximumFractionDigits: 2 })}
¥{fmt(costResult.severancePay)}
</span>
</div>
{costResult.noticePay > 0 && (
<div className="flex items-center justify-between text-sm">
<span className="text-gray-500"></span>
<span>¥{costResult.noticePay.toLocaleString()}</span>
<span>¥{fmt(costResult.noticePay)}</span>
</div>
)}
{costResult.noticePay > 0 && (
<div className="text-xs text-gray-400"> ¥{costResult.noticePay.toLocaleString()}</div>
<div className="text-xs text-gray-400"> ¥{fmt(costResult.noticePay)}</div>
)}
{costResult.isIllegal && (
<div className="flex items-start gap-2 px-3 py-2 rounded-md bg-red-50 text-red-700 text-xs">
@@ -493,9 +496,9 @@ export default function Termination() {
<div className="text-sm 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">¥{costResult.doublePay.toLocaleString()}</span>
<span className="text-lg font-bold text-warning">¥{fmt(costResult.doublePay)}</span>
</div>
<div className="text-xs text-gray-400">{costResult.doubleMonths} × ¥{costResult.wage.toLocaleString()}</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">
<Info className="w-3 h-3 mt-0.5 shrink-0" />
<span>1211</span>
@@ -507,7 +510,7 @@ 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">¥{costResult.grandTotal.toLocaleString(undefined, { maximumFractionDigits: 2 })}</span>
<span className="text-xl font-bold text-danger">¥{fmt(costResult.grandTotal)}</span>
</div>
</div>
@@ -568,9 +571,9 @@ export default function Termination() {
</p>
{costResult && !costResult.noComp && (
<p>
<strong>{costResult.cappedMonths}</strong> <strong>¥{costResult.cappedWage.toLocaleString()}/</strong>
<strong>¥{costResult.severancePay.toLocaleString(undefined, { maximumFractionDigits: 2 })}</strong>
{costResult.noticePay > 0 && `(含代通知金 ¥${costResult.noticePay.toLocaleString()}`}
<strong>{costResult.cappedMonths}</strong> <strong>¥{fmt(costResult.cappedWage)}/</strong>
<strong>¥{fmt(costResult.severancePay)}</strong>
{costResult.noticePay > 0 && `(含代通知金 ¥${fmt(costResult.noticePay)}`}
</p>
)}
@@ -579,11 +582,11 @@ export default function Termination() {
)}
{costResult && !costResult.hasContract && costResult.doubleMonths > 0 && (
<p>
{costResult.doubleMonths} <strong>¥{costResult.doublePay.toLocaleString()}</strong>
{costResult.doubleMonths} <strong>¥{fmt(costResult.doublePay)}</strong>
</p>
)}
{costResult && (
<p><strong>¥{costResult.grandTotal.toLocaleString(undefined, { maximumFractionDigits: 2 })}</strong></p>
<p><strong>¥{fmt(costResult.grandTotal)}</strong></p>
)}
<p></p>
<div className="text-right mt-6 space-y-1">
@@ -599,20 +602,20 @@ export default function Termination() {
<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="flex justify-between"><span></span><span>{costResult.years}{costResult.remainingMonths}</span></div>
<div className="flex justify-between"><span></span><span>¥{costResult.wage.toLocaleString()}/</span></div>
<div className="flex justify-between"><span></span><span>¥{fmt(costResult.wage)}/</span></div>
{costResult.capped && <div className="text-warning"> 312</div>}
{!costResult.noComp && (
<>
<div className="flex justify-between"><span></span><span>{costResult.cappedMonths}</span></div>
<div className="flex justify-between"><span></span><span>¥{costResult.cappedWage.toLocaleString()}/</span></div>
<div className="flex justify-between font-medium"><span>{costResult.isIllegal ? '违法解除赔偿金(×2' : '经济补偿金'}</span><span>¥{costResult.severancePay.toLocaleString(undefined, { maximumFractionDigits: 2 })}</span></div>
{costResult.noticePay > 0 && <div className="flex justify-between"><span></span><span>¥{costResult.noticePay.toLocaleString()}</span></div>}
<div className="flex justify-between"><span></span><span>¥{fmt(costResult.cappedWage)}/</span></div>
<div className="flex justify-between font-medium"><span>{costResult.isIllegal ? '违法解除赔偿金(×2' : '经济补偿金'}</span><span>¥{fmt(costResult.severancePay)}</span></div>
{costResult.noticePay > 0 && <div className="flex justify-between"><span></span><span>¥{fmt(costResult.noticePay)}</span></div>}
</>
)}
{!costResult.hasContract && costResult.doubleMonths > 0 && (
<div className="flex justify-between text-warning"><span>{costResult.doubleMonths}</span><span>¥{costResult.doublePay.toLocaleString()}</span></div>
<div className="flex justify-between text-warning"><span>{costResult.doubleMonths}</span><span>¥{fmt(costResult.doublePay)}</span></div>
)}
<div className="flex justify-between border-t pt-2 font-bold text-danger"><span></span><span>¥{costResult.grandTotal.toLocaleString(undefined, { maximumFractionDigits: 2 })}</span></div>
<div className="flex justify-between border-t pt-2 font-bold text-danger"><span></span><span>¥{fmt(costResult.grandTotal)}</span></div>
</div>
</div>
)}