feat: 薪资表格增加最低工资保护和递延扣款的可视化提示

1. 实发列:最低工资保护触发时显示橙色 + ★标记 + tooltip
2. 新增「递延扣款」列:显示递延金额(橙色)或补扣金额(蓝色)
3. 风险列:最低工资保护触发时显示橙色警告图标 + tooltip
4. 工作流步骤从4步改为3步(编辑薪资→归档锁定→发布工资条)

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
selfrelease
2026-08-16 14:30:08 +08:00
parent 2875965113
commit 904c9c6369
2 changed files with 39 additions and 15 deletions
+14 -12
View File
@@ -470,10 +470,10 @@ POST /batches/:batchId/unarchive
---
## 十四、4 步工作流
## 十四、3 步工作流
```
① 编辑薪资 → ② 核对汇总 归档锁定 → 发布工资条
① 编辑薪资(含核对) 归档锁定 → 发布工资条
```
### 步骤 1:编辑薪资(DRAFT 状态)
@@ -487,24 +487,26 @@ POST /batches/:batchId/unarchive
- **添加/删除人员**:动态调整批次人员
- **查看个税明细**:点击个税金额查看累计预扣计算过程
质量门禁(草稿状态自动检查):
- 实发为负的员工 → 红色警告
- 全零记录 → 黄色警告
### 步骤 2:核对汇总
批次列表展示汇总数据:
- 应发合计、社保合计、公积金合计、个税合计、实发合计
核对(编辑页面内直接展示,非独立步骤):
- 批次汇总卡片:应发合计、社保合计、公积金合计、个税合计、实发合计
- 质量门禁自动检查:实发为负、全零记录、最低工资、递延扣款等
- 可导出 **薪资汇总表** / **薪资明细表**CSV
### 步骤 3:归档锁定(ARCHIVED
质量门禁(草稿状态自动检查):
- 实发为负的员工 → 红色警告
- 实发低于最低工资 → 红色警告
- 最低工资保护已触发(递延扣款)→ 黄色警告
- 本月补扣上月递延 → 黄色警告
- 全零记录 → 黄色警告
### 步骤 2:归档锁定(ARCHIVED
**归档时后端自动执行**
1. **重算所有条目**:调用 `calcBatchEntry` 重新计算社保公积金和个税
2. **更新批次汇总**:重算 totalPay/totalNetPay/各项合计
3. **标记为 ARCHIVED**:设置 `archivedAt`,不可再编辑
### 步骤 4:发布工资条
### 步骤 3:发布工资条
- 调用 `POST /batches/:id/publish` → 更新 `Payslip.publishStatus = PUBLISHED`
- 或调用 `POST /batches/:id/schedule` → 设置 `publishStatus = SCHEDULED` + `scheduledAt`
+25 -3
View File
@@ -948,8 +948,7 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
<Card className="p-4">
<Stepper
steps={[
{ key: 'edit', title: '编辑薪资', description: '填写/导入工资数据', status: isArchived ? 'complete' : 'current' },
{ key: 'review', title: '核对汇总', description: '检查应发/个税/实发', status: isArchived ? 'complete' : 'pending' },
{ key: 'edit', title: '编辑薪资', description: '填写/导入/核对工资数据', status: isArchived ? 'complete' : 'current' },
{ key: 'archive', title: '归档锁定', description: '归档后不可编辑', status: isArchived ? 'complete' : 'pending' },
{ key: 'publish', title: '发布工资条', description: '员工端可见', status: batch.payslipPublished ? 'complete' : 'pending' },
] as Step[]}
@@ -1086,6 +1085,7 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
<th className="py-2 px-2 text-right">()</th>
<th className="py-2 px-2 text-right text-gray-500"></th>
<th className="py-2 px-2 text-right text-gray-500"></th>
<th className="py-2 px-2 text-right text-gray-500"></th>
<th className="py-2 px-2"></th>
{!isArchived && <th className="py-2 px-2"></th>}
</tr>
@@ -1110,12 +1110,34 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
{renderCell(entry, 'socialEmp')}
{renderCell(entry, 'housingEmp')}
<td className="py-2 px-2 text-right text-gray-500 cursor-pointer hover:text-primary hover:underline" onClick={() => handleTaxDetailClick(entry.employeeId, entry.employee.name)} title="点击查看个税计算明细">{fmt(entry.tax)}</td>
<td className="py-2 px-2 text-right font-bold text-safe">{fmt(entry.netPay)}</td>
{/* 实发:最低工资保护触发时高亮显示 */}
<td className={`py-2 px-2 text-right font-bold ${(entry.minWageApplied || 0) > 0 ? 'text-amber-600' : 'text-safe'}`} title={(entry.minWageApplied || 0) > 0 ? `最低工资保护:补齐 ¥${entry.minWageApplied},递延扣款 ¥${(entry.deferredSocialEmp || 0) + (entry.deferredHousingEmp || 0) + (entry.deferredMinWage || 0)} 将在次月补扣` : ''}>
{fmt(entry.netPay)}
{(entry.minWageApplied || 0) > 0 && <span className="text-xs text-amber-500 ml-1"></span>}
</td>
{/* 递延扣款明细 */}
<td className="py-2 px-2 text-right text-xs">
{(() => {
const deferred = (entry.deferredSocialEmp || 0) + (entry.deferredHousingEmp || 0) + (entry.deferredMinWage || 0)
const prevDeferred = (entry.prevDeferredSocialEmp || 0) + (entry.prevDeferredHousingEmp || 0) + (entry.prevDeferredMinWage || 0)
if (deferred > 0) {
return <span className="text-amber-600 cursor-help" title={`递延到次月:社保 ¥${entry.deferredSocialEmp || 0} + 公积金 ¥${entry.deferredHousingEmp || 0} + 最低工资补齐 ¥${entry.deferredMinWage || 0}`}> ¥{fmt(deferred)}</span>
}
if (prevDeferred > 0) {
return <span className="text-blue-600 cursor-help" title={`本月补扣上月递延:社保 ¥${entry.prevDeferredSocialEmp || 0} + 公积金 ¥${entry.prevDeferredHousingEmp || 0} + 最低工资补齐 ¥${entry.prevDeferredMinWage || 0}`}> ¥{fmt(prevDeferred)}</span>
}
return <span className="text-gray-300"></span>
})()}
</td>
<td className="py-2 px-2">
{entry.riskWarnings && entry.riskWarnings.length > 0 ? (
<span className="text-danger cursor-help" title={entry.riskWarnings.join('\n')}>
<AlertTriangle className="w-4 h-4" />
</span>
) : (entry.minWageApplied || 0) > 0 ? (
<span className="text-amber-500 cursor-help" title={`最低工资保护已触发:实发补齐到 ¥${entry.minWage},递延 ¥${(entry.deferredSocialEmp || 0) + (entry.deferredHousingEmp || 0) + (entry.deferredMinWage || 0)} 次月补扣`}>
<AlertTriangle className="w-4 h-4" />
</span>
) : (
<span className="text-gray-300"></span>
)}