feat: 已离职/解聘员工可重新入职

1. 后端新增 rehireEmployee 函数和 POST /employees/:id/rehire 接口
2. 校验:仅已离职员工可重新入职,新入职日期须晚于上次离职日期
3. 复用员工已有基本信息,只需填写新入职日期和劳动合同
4. 前端花名册已离职员工显示「重新入职」按钮+弹窗(RehireModal)
This commit is contained in:
freedakgmail
2026-07-23 18:11:01 +08:00
parent 2896ba77aa
commit 1eeca663a0
3 changed files with 252 additions and 1 deletions
+17
View File
@@ -11,6 +11,7 @@ import {
getEmployees,
getEmployeeDetail,
createEmployee,
rehireEmployee,
updateEmployee,
deleteEmployee,
batchRenew,
@@ -64,6 +65,22 @@ router.put('/:id', authMiddleware, async (req: AuthRequest, res, next) => {
}
})
router.post('/:id/rehire', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const result = await rehireEmployee(req.user!.orgId, req.user!.id, req.params.id, req.body)
await auditLog(req, 'REHIRE', 'EMPLOYEE', req.params.id, { hireDate: req.body.hireDate })
res.json({ success: true, data: result })
} catch (err: any) {
if (err?.code === 'CONFLICT') {
return res.status(409).json({ success: false, error: { code: err.code, message: err.message } })
}
if (err?.code === 'VALIDATION_ERROR') {
return res.status(400).json({ success: false, error: { code: err.code, message: err.message } })
}
next(err)
}
})
router.delete('/:id', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const result = await deleteEmployee(req.user!.orgId, req.params.id)
+66
View File
@@ -203,6 +203,72 @@ export async function createEmployee(orgId: string, userId: string, data: any) {
return { id: employee.id }
}
// 重新入职:复用已有员工基本信息,更新入职日期和状态,可选创建新合同
export async function rehireEmployee(orgId: string, userId: string, id: string, data: any) {
const employee = await prisma.employee.findFirst({
where: { id, orgId },
include: { terminations: { orderBy: { terminationDate: 'desc' }, take: 1 } },
})
if (!employee) {
throw { code: 'NOT_FOUND', message: '员工不存在' }
}
const today = new Date()
today.setHours(0, 0, 0, 0)
const isResigned = employee.terminations.some((t) => t.terminationDate <= today)
if (!isResigned) {
throw { code: 'CONFLICT', message: '该员工当前在职,无需重新入职' }
}
const newHireDate = new Date(data.hireDate)
const latestTerm = employee.terminations[0]
if (latestTerm && newHireDate <= latestTerm.terminationDate) {
throw { code: 'VALIDATION_ERROR', message: '新入职日期必须晚于上次离职/解聘日期' }
}
await prisma.employee.update({
where: { id },
data: {
hireDate: newHireDate,
status: 'ACTIVE',
isPregnant: data.isPregnant || false,
isInMedicalPeriod: data.isInMedicalPeriod || false,
isWorkInjured: data.isWorkInjured || false,
},
})
if (data.contract && data.contract.contractType !== 'UNSIGNED') {
const contractMonths = data.contract.endDate
? Math.ceil(daysBetween(new Date(data.contract.endDate), new Date(data.contract.startDate)) / 30.44)
: data.contract.contractYears * 12
const probationCheck = validateProbation(contractMonths, data.contract.probationMonths)
if (!probationCheck.valid) {
throw { code: 'VALIDATION_ERROR', message: probationCheck.message }
}
await prisma.laborContract.create({
data: {
orgId,
employeeId: id,
signDate: data.contract.signDate ? new Date(data.contract.signDate) : null,
startDate: new Date(data.contract.startDate),
endDate: data.contract.endDate ? new Date(data.contract.endDate) : null,
contractType: data.contract.contractType,
signMethod: data.contract.signMethod || 'PAPER',
contractYears: data.contract.contractYears || 3,
probationMonths: data.contract.probationMonths || 0,
probationSalary: data.contract.probationSalary || 0,
createdBy: userId,
},
})
}
await runRiskDetection(orgId)
return { id }
}
export async function updateEmployee(orgId: string, id: string, data: any) {
const employee = await prisma.employee.findFirst({ where: { id, orgId } })
if (!employee) {