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:
+14
-12
@@ -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`
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user