Files
TurboHR/backend/src/routes/termination.routes.ts
T
freedakgmail a2e9ba55c2 feat: 20260809 系统优化 - 全部28项问题修复(P0×6+P1×16+P2×6)
P0: 福利批量参保/离职证明下载防乱码/考勤模板合并Sheet/补卡修改/附件在线查看删除
P1: 分页pageSize修复/离职导出筛选/撤回删除草稿/加班费自动计算/考勤加班汇总/证据链异常详情/制度催办/模板导入Word/社保封顶保底/校验字段提示/职务字段/社保费用明细/弹窗防误关/身份证查重/证明员工下拉/培训批量
P2: 离职流程去重/社保基数覆盖输入/薪税入口改名/添加员工引导/绩效模板清理
2026-08-09 11:59:02 +08:00

364 lines
15 KiB
TypeScript

import { Router } from 'express'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import { auditLog } from '../middleware/auditLog'
import { terminationChecklistSchema, resignationSchema, batchTerminatePreviewSchema, batchTerminateSchema, createTerminationDraftSchema, updateTerminationDraftSchema } from '../schemas/termination.schema'
import { createTermination, createResignation, revokeTermination, getTerminations, getChecklistForReason, assessRisk, batchTerminatePreview, batchTerminate, createDraft, updateDraft, submitForApproval, approveTermination, rejectTermination, executeTermination, cancelTermination, getDrafts, getTerminationDetail, getDefaultHandoverItems, validateTerminationStep } from '../services/termination.service'
import prisma from '../lib/prisma'
import { createEvidence } from '../services/evidence.service'
const router = Router()
router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const page = parseInt(req.query.page as string) || 1
const pageSize = Math.min(parseInt(req.query.pageSize as string) || 20, 200)
const result = await getTerminations(req.user!.orgId, page, pageSize)
res.json({ success: true, data: result })
} catch (err) {
next(err)
}
})
router.get('/checklist/:reason', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const employeeId = req.query.employeeId as string
let employee: any = undefined
if (employeeId) {
const emp = await prisma.employee.findFirst({
where: { id: employeeId, orgId: req.user!.orgId },
include: {
trainingRecords: true,
},
})
if (emp) {
employee = {
isInMedicalPeriod: emp.isInMedicalPeriod,
trainingRecords: emp.trainingRecords,
}
}
}
const checklist = getChecklistForReason(req.params.reason, employee)
res.json({ success: true, data: checklist })
} catch (err) {
next(err)
}
})
router.get('/assess/:employeeId', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const employee = await prisma.employee.findFirst({ where: { id: req.params.employeeId, orgId: req.user!.orgId } })
if (!employee) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '员工不存在' } })
}
const assessment = assessRisk(employee, req.query.reason as string || '')
res.json({ success: true, data: assessment })
} catch (err) {
next(err)
}
})
router.post('/', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const data = terminationChecklistSchema.parse(req.body)
const result = await createTermination(req.user!.orgId, req.user!.id, data)
await auditLog(req, 'TERMINATE', 'EMPLOYEE', data.employeeId, { reason: data.reason })
await createEvidence({
orgId: req.user!.orgId,
category: 'TERMINATION',
refId: result.id,
employeeId: data.employeeId,
events: [{ action: '解聘流程启动', timestamp: new Date().toISOString(), ip: req.ip, userAgent: req.headers['user-agent'] }],
createdBy: req.user!.id,
}).catch(() => {})
res.json({ success: true, data: result })
} catch (err) {
next(err)
}
})
router.post('/resignation', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const { employeeId, terminationDate, resignationReason, remark } = resignationSchema.parse(req.body)
const result = await createResignation(req.user!.orgId, req.user!.id, { employeeId, terminationDate, resignationReason, remark })
await auditLog(req, 'RESIGN', 'EMPLOYEE', employeeId, { resignationReason })
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 } })
}
next(err)
}
})
router.delete('/:id/revoke', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const result = await revokeTermination(req.user!.orgId, req.params.id)
const termRecord = await prisma.terminationRecord.findFirst({ where: { id: req.params.id }, select: { employeeId: true, reason: true, type: true } })
const emp = termRecord ? await prisma.employee.findFirst({ where: { id: termRecord.employeeId }, select: { name: true } }) : null
await auditLog(req, 'REVOKE_TERMINATION', 'TERMINATION_RECORD', req.params.id, {
employeeName: emp?.name || '',
reason: termRecord?.reason || '',
type: termRecord?.type || '',
})
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 === 'NOT_FOUND') {
return res.status(404).json({ success: false, error: { code: err.code, message: err.message } })
}
next(err)
}
})
// 批量解聘预检
router.post('/batch/preview', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const { items } = batchTerminatePreviewSchema.parse(req.body)
const results = await batchTerminatePreview(req.user!.orgId, items)
res.json({ success: true, data: { total: results.length, warnings: results.filter(r => r.warnings.length > 0).length, results } })
} catch (err) {
next(err)
}
})
// 批量解聘执行
router.post('/batch', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const { items } = batchTerminateSchema.parse(req.body)
const result = await batchTerminate(req.user!.orgId, req.user!.id, items)
for (const id of result.success) {
await auditLog(req, 'TERMINATE', 'EMPLOYEE', id, { batch: true })
}
res.json({ success: true, data: result })
} catch (err) {
next(err)
}
})
// ============================================================
// 解聘流程状态机 API
// ============================================================
// 获取草稿/流程列表
router.get('/drafts', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const status = req.query.status as string | undefined
const search = req.query.search as string | undefined
const department = req.query.department as string | undefined
const page = parseInt(req.query.page as string) || 1
const pageSize = Math.min(parseInt(req.query.pageSize as string) || 20, 200)
const result = await getDrafts(req.user!.orgId, status, search, department, page, pageSize)
res.json({ success: true, data: result })
} catch (err) {
next(err)
}
})
// 获取单条记录详情
router.get('/detail/:id', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const result = await getTerminationDetail(req.user!.orgId, req.params.id)
res.json({ success: true, data: result })
} catch (err: any) {
if (err?.code === 'NOT_FOUND') {
return res.status(404).json({ success: false, error: { code: err.code, message: err.message } })
}
next(err)
}
})
// 获取默认工作交接清单模板
router.get('/handover-template', authMiddleware, async (req: AuthRequest, res) => {
res.json({ success: true, data: getDefaultHandoverItems() })
})
// 创建草稿
router.post('/draft', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const data = createTerminationDraftSchema.parse(req.body)
const result = await createDraft(req.user!.orgId, req.user!.id, data)
const emp = await prisma.employee.findFirst({ where: { id: data.employeeId }, select: { name: true, department: true } })
await auditLog(req, 'CREATE_DRAFT', 'TERMINATION_RECORD', result.id, {
employeeName: emp?.name || '',
department: emp?.department || '',
reason: data.reason || '',
type: data.type || 'TERMINATION',
terminationDate: data.terminationDate || '',
compensation: data.compensation || 0,
})
res.json({ success: true, data: result })
} catch (err: any) {
if (err?.code === 'NOT_FOUND') {
return res.status(404).json({ success: false, error: { code: err.code, message: err.message } })
}
next(err)
}
})
// 更新草稿
router.put('/draft/:id', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const data = updateTerminationDraftSchema.parse(req.body)
const result = await updateDraft(req.user!.orgId, req.params.id, req.user!.id, data)
res.json({ success: true, data: result })
} catch (err: any) {
if (err?.code === 'CONFLICT' || err?.code === 'NOT_FOUND') {
return res.status(err.code === 'NOT_FOUND' ? 404 : 409).json({ success: false, error: { code: err.code, message: err.message } })
}
next(err)
}
})
// 提交审批
router.post('/draft/:id/submit', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const result = await submitForApproval(req.user!.orgId, req.params.id, req.user!.id)
const termRecord = await prisma.terminationRecord.findFirst({ where: { id: req.params.id }, select: { employeeId: true, reason: true, type: true } })
const emp = termRecord ? await prisma.employee.findFirst({ where: { id: termRecord.employeeId }, select: { name: true } }) : null
await auditLog(req, 'SUBMIT_TERMINATION', 'TERMINATION_RECORD', req.params.id, {
employeeName: emp?.name || '',
reason: termRecord?.reason || '',
type: termRecord?.type || '',
})
res.json({ success: true, data: result })
} catch (err: any) {
if (err?.code === 'CONFLICT' || err?.code === 'NOT_FOUND') {
return res.status(err.code === 'NOT_FOUND' ? 404 : 409).json({ success: false, error: { code: err.code, message: err.message } })
}
next(err)
}
})
// 审批通过
router.post('/draft/:id/approve', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const { comment } = req.body
const result = await approveTermination(req.user!.orgId, req.params.id, req.user!.id, comment || '')
const termRecord = await prisma.terminationRecord.findFirst({ where: { id: req.params.id }, select: { employeeId: true, reason: true } })
const emp = termRecord ? await prisma.employee.findFirst({ where: { id: termRecord.employeeId }, select: { name: true } }) : null
await auditLog(req, 'APPROVE_TERMINATION', 'TERMINATION_RECORD', req.params.id, {
employeeName: emp?.name || '',
reason: termRecord?.reason || '',
comment: comment || '',
})
res.json({ success: true, data: result })
} catch (err: any) {
if (err?.code === 'CONFLICT' || err?.code === 'NOT_FOUND') {
return res.status(err.code === 'NOT_FOUND' ? 404 : 409).json({ success: false, error: { code: err.code, message: err.message } })
}
next(err)
}
})
// 审批驳回
router.post('/draft/:id/reject', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const { comment } = req.body
const result = await rejectTermination(req.user!.orgId, req.params.id, req.user!.id, comment || '')
const termRecord = await prisma.terminationRecord.findFirst({ where: { id: req.params.id }, select: { employeeId: true, reason: true } })
const emp = termRecord ? await prisma.employee.findFirst({ where: { id: termRecord.employeeId }, select: { name: true } }) : null
await auditLog(req, 'REJECT_TERMINATION', 'TERMINATION_RECORD', req.params.id, {
employeeName: emp?.name || '',
reason: termRecord?.reason || '',
comment: comment || '',
})
res.json({ success: true, data: result })
} catch (err: any) {
if (err?.code === 'CONFLICT' || err?.code === 'NOT_FOUND') {
return res.status(err.code === 'NOT_FOUND' ? 404 : 409).json({ success: false, error: { code: err.code, message: err.message } })
}
next(err)
}
})
// 执行解聘
router.post('/draft/:id/execute', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const result = await executeTermination(req.user!.orgId, req.params.id, req.user!.id)
const termRecord = await prisma.terminationRecord.findFirst({ where: { id: req.params.id }, select: { employeeId: true, reason: true, type: true, terminationDate: true, compensation: true } })
const emp = termRecord ? await prisma.employee.findFirst({ where: { id: termRecord.employeeId }, select: { name: true, department: true } }) : null
await auditLog(req, 'EXECUTE_TERMINATION', 'TERMINATION_RECORD', req.params.id, {
employeeName: emp?.name || '',
department: emp?.department || '',
reason: termRecord?.reason || '',
type: termRecord?.type || '',
terminationDate: termRecord?.terminationDate?.toISOString().slice(0, 10) || '',
compensation: termRecord?.compensation || 0,
})
await createEvidence({
orgId: req.user!.orgId,
category: 'TERMINATION',
refId: req.params.id,
employeeId: (result as any)?.employeeId,
events: [{ action: '解聘执行', timestamp: new Date().toISOString(), ip: req.ip, userAgent: req.headers['user-agent'] }],
createdBy: req.user!.id,
}).catch(() => {})
res.json({ success: true, data: result })
} catch (err: any) {
if (err?.code === 'CONFLICT' || err?.code === 'NOT_FOUND') {
return res.status(err.code === 'NOT_FOUND' ? 404 : 409).json({ success: false, error: { code: err.code, message: err.message } })
}
next(err)
}
})
// 撤销
router.post('/draft/:id/cancel', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const result = await cancelTermination(req.user!.orgId, req.params.id, req.user!.id)
const termRecord = await prisma.terminationRecord.findFirst({ where: { id: req.params.id }, select: { employeeId: true, reason: true, type: true } })
const emp = termRecord ? await prisma.employee.findFirst({ where: { id: termRecord.employeeId }, select: { name: true } }) : null
await auditLog(req, 'CANCEL_TERMINATION', 'TERMINATION_RECORD', req.params.id, {
employeeName: emp?.name || '',
reason: termRecord?.reason || '',
type: termRecord?.type || '',
})
res.json({ success: true, data: result })
} catch (err: any) {
if (err?.code === 'CONFLICT' || err?.code === 'NOT_FOUND') {
return res.status(err.code === 'NOT_FOUND' ? 404 : 409).json({ success: false, error: { code: err.code, message: err.message } })
}
next(err)
}
})
// 步骤前置校验
router.get('/draft/:id/validate-step', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const step = parseInt(req.query.step as string) || 1
const result = await validateTerminationStep(req.user!.orgId, req.params.id, step)
res.json({ success: true, data: result })
} catch (err: any) {
if (err?.code === 'NOT_FOUND') {
return res.status(404).json({ success: false, error: { code: err.code, message: err.message } })
}
next(err)
}
})
// 删除草稿(仅允许 DRAFT 和 CANCELLED 状态)
router.delete('/draft/:id', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const record = await prisma.terminationRecord.findFirst({
where: { id: req.params.id, orgId: req.user!.orgId },
})
if (!record) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } })
}
if (record.status !== 'DRAFT' && record.status !== 'CANCELLED') {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '仅草稿或已撤销的记录可以删除' } })
}
await prisma.terminationRecord.delete({ where: { id: req.params.id } })
await auditLog(req, 'DELETE_DRAFT', 'TERMINATION_RECORD', req.params.id, { employeeId: record.employeeId })
res.json({ success: true })
} catch (err) {
next(err)
}
})
export default router