fix: 线下签署登记标题英文转中文+完成时间时区+签署记录角标

1. documentTitle 中 contractType 枚举值转中文
   (LABOR→劳务协议、FIXED→固定期限劳动合同等)
2. 签署日期解析改为本地时区构造(new Date(y,m-1,d,12)),
   避免 new Date('2026-08-16') 被解析为 UTC 00:00 导致
   本地 +8 显示为 08:00:00
3. 签署记录 Tab 增加数字角标显示总记录数

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 12:26:13 +08:00
parent 72a6eab3bd
commit 5a9d440339
9 changed files with 485 additions and 3 deletions
+17 -2
View File
@@ -214,7 +214,10 @@ router.post('/sign-date', async (req: AuthRequest, res: Response, next: NextFunc
if (contract.signMethod === 'ELECTRONIC') {
return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: '电子签合同的签署日期由电签系统自动回写,不可手动修改' } })
}
const parsedDate = new Date(signDate)
// 解析日期字符串(YYYY-MM-DD),手动构造本地中午时间避免时区偏移
const dateStr = String(signDate).slice(0, 10)
const [y, m, d] = dateStr.split('-').map(Number)
const parsedDate = new Date(y, (m || 1) - 1, d || 1, 12, 0, 0, 0)
if (isNaN(parsedDate.getTime())) {
return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: '签署日期格式无效' } })
}
@@ -229,6 +232,18 @@ router.post('/sign-date', async (req: AuthRequest, res: Response, next: NextFunc
select: { id: true },
})
if (!existingRecord) {
// 合同类型枚举转中文
const CONTRACT_TYPE_LABEL: Record<string, string> = {
FIXED: '固定期限劳动合同',
UNFIXED: '无固定期限劳动合同',
UNSIGNED: '未签合同',
LABOR: '劳务协议',
INTERNSHIP: '实习协议',
DISPATCH: '劳务派遣合同',
OUTSOURCING: '外包合同',
PARTTIME: '非全日制合同',
}
const contractLabel = CONTRACT_TYPE_LABEL[contract.contractType] || '合同'
await prisma.eSignRecord.create({
data: {
orgId: req.user!.orgId,
@@ -236,7 +251,7 @@ router.post('/sign-date', async (req: AuthRequest, res: Response, next: NextFunc
employeeId: contract.employeeId,
scene: 'CONTRACT',
signMethod: 'PAPER',
documentTitle: `${contract.contractType}合同线下签署登记`,
documentTitle: `${contractLabel}线下签署登记`,
status: 'COMPLETED',
completedAt: parsedDate,
signedAt: parsedDate,
+76
View File
@@ -11,6 +11,7 @@ import {
prePayrollCheck,
} from '../services/payroll.service'
import { isInProbation } from '../services/contract.service'
import { getBonusByMonthAndEmployeeIds } from '../services/commission-bonus.service'
// RFC 5987 编码中文文件名
function contentDisposition(filename: string): string {
@@ -548,6 +549,81 @@ router.put('/batches/:batchId/entries/:employeeId', async (req: AuthRequest, res
}
})
// 获取提成奖金:按批次月份从 CommissionBonus 表拉取,填充到 entries.bonus
router.post('/batches/:batchId/fetch-bonus', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { batchId } = req.params
const orgId = req.user!.orgId
const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, orgId } })
if (!batch) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
if (batch.status === 'ARCHIVED') return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: '已归档批次不可操作' } })
// 获取批次所有条目
const entries = await prisma.batchEntry.findMany({
where: { batchId },
select: { id: true, employeeId: true, bonus: true },
})
if (entries.length === 0) {
return res.json({ success: true, data: { filled: 0, totalAmount: 0, message: '批次无员工条目' } })
}
// 按批次月份查询提成奖金
const bonusMap = await getBonusByMonthAndEmployeeIds(orgId, batch.month, entries.map((e) => e.employeeId))
let filled = 0
let totalAmount = 0
for (const entry of entries) {
const bonus = bonusMap.get(entry.employeeId)
if (bonus) {
await prisma.batchEntry.update({
where: { id: entry.id },
data: { bonus: bonus.amount },
})
filled++
totalAmount += bonus.amount
}
}
// 重算批次汇总
const allEntries = await prisma.batchEntry.findMany({ where: { batchId } })
const totals = allEntries.reduce((acc, e) => ({
totalPay: acc.totalPay + e.baseSalary + e.overtimePay + e.allowance + e.bonus - e.deduction,
totalNetPay: acc.totalNetPay + e.netPay,
totalSocialOrg: acc.totalSocialOrg + e.socialOrg,
totalSocialEmp: acc.totalSocialEmp + e.socialEmp,
totalHousingOrg: acc.totalHousingOrg + e.housingOrg,
totalHousingEmp: acc.totalHousingEmp + e.housingEmp,
totalTax: acc.totalTax + e.tax,
}), { totalPay: 0, totalNetPay: 0, totalSocialOrg: 0, totalSocialEmp: 0, totalHousingOrg: 0, totalHousingEmp: 0, totalTax: 0 })
await prisma.payrollBatch.update({
where: { id: batchId },
data: {
totalPay: Math.round(totals.totalPay * 100) / 100,
totalNetPay: Math.round(totals.totalNetPay * 100) / 100,
totalSocialOrg: Math.round(totals.totalSocialOrg * 100) / 100,
totalSocialEmp: Math.round(totals.totalSocialEmp * 100) / 100,
totalHousingOrg: Math.round(totals.totalHousingOrg * 100) / 100,
totalHousingEmp: Math.round(totals.totalHousingEmp * 100) / 100,
totalTax: Math.round(totals.totalTax * 100) / 100,
},
})
res.json({
success: true,
data: {
filled,
totalAmount: Math.round(totalAmount * 100) / 100,
message: filled > 0 ? `已填充 ${filled} 人提成奖金,合计 ¥${Math.round(totalAmount * 100) / 100}` : `${batch.month} 无提成奖金数据`,
},
})
} catch (err) {
next(err)
}
})
// 获取条目个税计算明细
router.get('/batches/:batchId/entries/:employeeId/tax-detail', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {