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