feat: Phase 1-3 优化全部完成

Phase 1 紧急修复(8项):
- 社保城市选择改为可输入
- 社保上下限拆分(三险/医保独立基数)
- 公积金试算结果展示修复
- 花名册合同保存修复(日期ISO格式)
- 薪酬批次创建失败修复(城市过滤+错误处理)
- 证据链查看修复
- 个税计算修复(blank_employees读取基本工资)
- 加班费倍率读取配置

Phase 2 功能完善(3项):
- 批量导入per-row异常捕获+导入按钮
- 单人发薪UI入口优化
- 解除协议模板补充(员工提出离职版)

Phase 3 后期规划(4项):
- 工资表导入功能(POST /import/payroll + 前端入口)
- 大病险/长护险附加险种(extraInsurances JSON + 计算适配)
- 专项附加扣除按月录入(SpecialDeductionRecord模型 + 前端Tab)
- 预置河北省社保政策(seed数据)
This commit is contained in:
selfrelease
2026-07-27 18:55:08 +08:00
parent 034fcc4111
commit 255af519d2
16 changed files with 1464 additions and 77 deletions
+15 -2
View File
@@ -297,6 +297,7 @@ router.post('/batches', async (req: AuthRequest, res: Response, next: NextFuncti
// 创建批次条目
const entries: any[] = []
const failedEmployees: { employeeId: string; name: string; error: string }[] = []
for (const emp of employees) {
let baseSalary = 0
let overtimePay = 0
@@ -334,6 +335,10 @@ router.post('/batches', async (req: AuthRequest, res: Response, next: NextFuncti
deduction = prevPayslip?.deduction || 0
}
// blank_employees 和 blank_all: 所有金额默认 0
// blank_employees 模式下尝试从员工记录获取基本工资
if (mode === 'blank_employees' && emp.monthlySalary) {
try { baseSalary = Number(decrypt(emp.monthlySalary)) || 0 } catch { baseSalary = Number(emp.monthlySalary) || 0 }
}
// 判断同月是否已有归档的常规批次(用于决定是否跳过社保)
const hasArchivedRegularBatch = await prisma.payrollBatch.count({
@@ -343,7 +348,15 @@ router.post('/batches', async (req: AuthRequest, res: Response, next: NextFuncti
// 计算社保、个税等
// 同月已有归档常规批次时,新批次跳过社保(避免重复扣缴),但用户可手动编辑覆盖
const skipSocial = type !== 'BONUS' && type !== 'SEVERANCE' && hasArchivedRegularBatch > 0
const calcResult = await calcBatchEntry(orgId, emp.id, month, { baseSalary, overtimePay, allowance, deduction, bonus }, type, { skipSocial })
let calcResult: any
try {
calcResult = await calcBatchEntry(orgId, emp.id, month, { baseSalary, overtimePay, allowance, deduction, bonus }, type, { skipSocial })
} catch (calcErr: any) {
// 单个员工计算失败不阻塞整个批次,记录错误并使用零值
failedEmployees.push({ employeeId: emp.id, name: emp.name, error: calcErr?.message || '计算失败' })
calcResult = { socialEmp: 0, socialOrg: 0, housingEmp: 0, housingOrg: 0, tax: 0, totalPay: baseSalary + overtimePay + allowance + bonus - deduction, netPay: baseSalary + overtimePay + allowance + bonus - deduction }
}
// 风险提示
const riskWarnings = await getPayrollRiskWarnings(orgId, emp.id)
@@ -396,7 +409,7 @@ router.post('/batches', async (req: AuthRequest, res: Response, next: NextFuncti
include: { entries: { include: { employee: { select: { id: true, name: true, department: true, status: true } } } } },
})
res.json({ success: true, data: updatedBatch })
res.json({ success: true, data: updatedBatch, failedEmployees: failedEmployees.length > 0 ? failedEmployees : undefined })
} catch (err) {
next(err)
}