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:
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user