feat: 待签合同支持登记线下签署日期
- 线下手签合同:展开后可点击「登记签署日期」选择日期并保存 保存后合同从待签列表移除(signDate 已填写) - 电子签合同:签署日期由电签系统自动回写,不可手动修改 - 后端新增 POST /esign/sign-date 接口 电子签合同拒绝手动修改签署日期 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -191,6 +191,42 @@ router.get('/pending', async (req: AuthRequest, res: Response, next: NextFunctio
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
/**
|
||||
* 登记线下合同签署日期
|
||||
* POST /esign/sign-date body: { contractId, signDate }
|
||||
*
|
||||
* 适用于纸质合同(signMethod=PAPER)未登记签署日期的情况。
|
||||
* 电子签合同的签署日期由电签系统回写,不通过此接口修改。
|
||||
*/
|
||||
router.post('/sign-date', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { contractId, signDate } = req.body
|
||||
if (!contractId || !signDate) {
|
||||
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少 contractId 或 signDate' } })
|
||||
}
|
||||
const contract = await prisma.laborContract.findFirst({
|
||||
where: { id: contractId, orgId: req.user!.orgId },
|
||||
select: { id: true, signMethod: true, employee: { select: { name: true } } },
|
||||
})
|
||||
if (!contract) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '合同不存在' } })
|
||||
}
|
||||
if (contract.signMethod === 'ELECTRONIC') {
|
||||
return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: '电子签合同的签署日期由电签系统自动回写,不可手动修改' } })
|
||||
}
|
||||
const parsedDate = new Date(signDate)
|
||||
if (isNaN(parsedDate.getTime())) {
|
||||
return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: '签署日期格式无效' } })
|
||||
}
|
||||
await prisma.laborContract.update({
|
||||
where: { id: contractId },
|
||||
data: { signDate: parsedDate },
|
||||
})
|
||||
await auditLog(req, 'SIGN_DATE', 'CONTRACT', contractId, { employeeName: contract.employee.name, signDate: parsedDate.toISOString() })
|
||||
res.json({ success: true, data: { message: '签署日期已登记', signDate: parsedDate.toISOString() } })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
/**
|
||||
* 催办 — 生成员工一次性自动登录链接(指向员工端签署页)
|
||||
* POST /esign/remind body: { employeeId }
|
||||
|
||||
@@ -3,6 +3,7 @@ import prisma from '../lib/prisma'
|
||||
import { decrypt } from '../lib/crypto'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { z } from 'zod'
|
||||
import { isInProbation } from '../services/contract.service'
|
||||
|
||||
const router = Router()
|
||||
router.use(authMiddleware)
|
||||
@@ -377,8 +378,11 @@ router.post('/payslip/batch-generate', async (req: AuthRequest, res: Response, n
|
||||
const deduction = deductions[emp.id] || 0
|
||||
|
||||
let baseSalary = 0
|
||||
if (emp.contracts[0]?.probationSalary && new Date(emp.contracts[0].startDate) > new Date(Date.now() - 365 * 24 * 60 * 60 * 1000)) {
|
||||
baseSalary = emp.contracts[0].probationSalary
|
||||
// 按月份判定是否仍在试用期
|
||||
const monthEnd = new Date(`${month}-28T23:59:59`)
|
||||
const latestContract = emp.contracts[0]
|
||||
if (isInProbation(latestContract, monthEnd) && latestContract?.probationSalary > 0) {
|
||||
baseSalary = latestContract.probationSalary
|
||||
} else if (emp.monthlySalary) {
|
||||
try {
|
||||
baseSalary = Number(decrypt(emp.monthlySalary)) || 0
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
generatePayslipFromBatches,
|
||||
prePayrollCheck,
|
||||
} from '../services/payroll.service'
|
||||
import { isInProbation } from '../services/contract.service'
|
||||
|
||||
// RFC 5987 编码中文文件名
|
||||
function contentDisposition(filename: string): string {
|
||||
@@ -352,8 +353,12 @@ router.post('/batches', async (req: AuthRequest, res: Response, next: NextFuncti
|
||||
where: { employeeId_month: { employeeId: emp.id, month } },
|
||||
})
|
||||
|
||||
if (emp.contracts?.[0]?.probationSalary && new Date(emp.contracts[0].startDate) > new Date(Date.now() - 365 * 24 * 60 * 60 * 1000)) {
|
||||
baseSalary = emp.contracts[0].probationSalary
|
||||
// 按批次月份判定是否仍在试用期(试用期结束日 = 合同开始日 + 试用期月数)
|
||||
// 试用期且 probationSalary > 0 → 用试用期工资;否则用转正工资
|
||||
const batchMonthEnd = new Date(`${month}-28T23:59:59`) // 月末近似
|
||||
const latestContract = emp.contracts?.[0]
|
||||
if (isInProbation(latestContract, batchMonthEnd) && latestContract.probationSalary > 0) {
|
||||
baseSalary = latestContract.probationSalary
|
||||
} else if (emp.monthlySalary) {
|
||||
try { baseSalary = Number(decrypt(emp.monthlySalary)) || 0 } catch { baseSalary = Number(emp.monthlySalary) || 0 }
|
||||
}
|
||||
@@ -607,8 +612,11 @@ router.post('/batches/:batchId/employees', async (req: AuthRequest, res: Respons
|
||||
if (!emp) continue
|
||||
|
||||
let baseSalary = 0
|
||||
if (emp.contracts?.[0]?.probationSalary && new Date(emp.contracts[0].startDate) > new Date(Date.now() - 365 * 24 * 60 * 60 * 1000)) {
|
||||
baseSalary = emp.contracts[0].probationSalary
|
||||
// 按批次月份判定是否仍在试用期
|
||||
const batchMonthEnd = new Date(`${batch.month}-28T23:59:59`)
|
||||
const latestContract = emp.contracts?.[0]
|
||||
if (isInProbation(latestContract, batchMonthEnd) && latestContract.probationSalary > 0) {
|
||||
baseSalary = latestContract.probationSalary
|
||||
} else if (emp.monthlySalary) {
|
||||
try { baseSalary = Number(decrypt(emp.monthlySalary)) || 0 } catch { baseSalary = Number(emp.monthlySalary) || 0 }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user