import { Router, Response, NextFunction } from 'express' import { authMiddleware, AuthRequest } from '../middleware/auth' import prisma from '../lib/prisma' import { parsePagination } from '../lib/pagination' import { executeWorkProcess, generateDocument, PROCESS_TYPES, PROCESS_STATUS } from '../services/work-process.service' import { createWorkProcessSchema, updateWorkProcessSchema } from '../schemas/work-process.schema' const router = Router() // 各流程类型的必填字段映射 const REQUIRED_FIELDS: Record = { HIRE: ['name', 'department', 'hireDate', 'phone', 'idCardNumber'], ONBOARD: ['employeeId', 'hireDate'], CUSTOM_CONTRACT: ['employeeId', 'contractStartDate'], INFO_SUBMIT: ['employeeId'], CONFIRM: ['employeeId', 'confirmDate'], CHANGE: ['contractId', 'newEndDate'], RENEW: ['employeeId', 'newStartDate'], SUSPEND: ['contractId', 'suspendDate'], INCOME_CERT: ['employeeName', 'idCardNumber'], TERMINATE: ['employeeId', 'terminateDate', 'reason'], RESCIND: ['employeeId', 'rescindDate', 'reason'], LEAVING_CERT: ['employeeName', 'idCardNumber', 'leaveDate'], FLEXIBLE: ['name', 'idCardNumber', 'agreementStartDate'], } // 必填字段中文标签映射 const FIELD_LABELS: Record = { name: '员工姓名', department: '部门', hireDate: '入职日期', phone: '手机号', idCardNumber: '证件号码', employeeId: '员工ID', contractId: '合同ID', contractStartDate: '合同开始日期', confirmDate: '转正日期', newEndDate: '新到期日期', newStartDate: '新合同开始日期', suspendDate: '中止日期', employeeName: '员工姓名', terminateDate: '终止日期', rescindDate: '解除日期', reason: '原因', leaveDate: '离职日期', agreementStartDate: '协议开始日期', } /** 日期字段对配置:各流程类型的开始/结束日期字段对 */ const DATE_RANGE_FIELDS: Record> = { HIRE: [{ start: 'contractStartDate', end: 'contractEndDate', startLabel: '合同开始日期', endLabel: '合同结束日期' }], CUSTOM_CONTRACT: [{ start: 'contractStartDate', end: 'contractEndDate', startLabel: '合同开始日期', endLabel: '合同结束日期' }], RENEW: [{ start: 'newStartDate', end: 'newEndDate', startLabel: '新合同开始日期', endLabel: '新合同结束日期' }], FLEXIBLE: [{ start: 'agreementStartDate', end: 'agreementEndDate', startLabel: '协议开始日期', endLabel: '协议结束日期' }], } /** 校验日期前后关系:结束日期不能早于开始日期 */ function validateWorkProcessDateRange(type: string, formData: Record): string | null { const pairs = DATE_RANGE_FIELDS[type] if (!pairs) return null for (const pair of pairs) { const start = formData[pair.start] const end = formData[pair.end] if (start && end && new Date(end) < new Date(start)) { return `${pair.endLabel}不能早于${pair.startLabel}` } } return null } // 创建办理(含草稿) router.post('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => { try { const data = createWorkProcessSchema.parse(req.body) const { type, title, employeeId, formData, status, remark } = data // 后端日期前后关系校验(双保险) const dateError = validateWorkProcessDateRange(type, formData || {}) if (dateError) { return res.status(400).json({ success: false, error: { code: 'INVALID_DATE_RANGE', message: dateError } }) } const process = await (prisma as any).workProcess.create({ data: { orgId: req.user!.orgId, type, title: title || PROCESS_TYPES[type].label, employeeId: employeeId || null, formData: formData || {}, status, remark: remark || null, createdBy: req.user!.id, }, }) res.json({ success: true, data: process }) } catch (err) { next(err) } }) // 列表查询 router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => { try { const { type, status } = req.query const { page, pageSize } = parsePagination(req.query) const where: any = { orgId: req.user!.orgId } if (type) where.type = type if (status) where.status = status const total = await (prisma as any).workProcess.count({ where }) const items = await (prisma as any).workProcess.findMany({ where, include: { employee: { select: { id: true, name: true, department: true } } }, orderBy: { createdAt: 'desc' }, skip: (page - 1) * pageSize, take: pageSize, }) res.json({ success: true, data: { items, total, page, pageSize } }) } catch (err) { next(err) } }) // 详情 router.get('/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => { try { const process = await (prisma as any).workProcess.findFirst({ where: { id: req.params.id, orgId: req.user!.orgId }, include: { employee: { select: { id: true, name: true, department: true, status: true } } }, }) if (!process) { return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '办理记录不存在' } }) } res.json({ success: true, data: process }) } catch (err) { next(err) } }) // 更新草稿 router.patch('/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => { try { const existing = await (prisma as any).workProcess.findFirst({ where: { id: req.params.id, orgId: req.user!.orgId }, }) if (!existing) { return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '办理记录不存在' } }) } if (existing.status !== 'DRAFT') { return res.status(400).json({ success: false, error: { code: 'NOT_DRAFT', message: '仅草稿状态可编辑' } }) } const { title, employeeId, formData, remark } = updateWorkProcessSchema.parse(req.body) const updated = await (prisma as any).workProcess.update({ where: { id: req.params.id }, data: { ...(title !== undefined && { title }), ...(employeeId !== undefined && { employeeId: employeeId || null }), ...(formData !== undefined && { formData }), ...(remark !== undefined && { remark }), }, }) res.json({ success: true, data: updated }) } catch (err) { next(err) } }) // 提交办理 router.post('/:id/submit', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => { try { const process = await (prisma as any).workProcess.findFirst({ where: { id: req.params.id, orgId: req.user!.orgId }, }) if (!process) { return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '办理记录不存在' } }) } if (process.status !== 'DRAFT') { return res.status(400).json({ success: false, error: { code: 'NOT_DRAFT', message: '仅草稿状态可提交' } }) } // 后端必填字段校验 const required = REQUIRED_FIELDS[process.type] || [] const fd = process.formData || {} const missing = required.filter((key) => !fd[key] || String(fd[key]).trim() === '') if (missing.length > 0) { const labels = missing.map((k) => FIELD_LABELS[k] || k).join('、') return res.status(400).json({ success: false, error: { code: 'MISSING_REQUIRED', message: `请填写必填项:${labels}` } }) } // 执行业务联动 let execResult: any = {} try { execResult = await executeWorkProcess(process.id, process.type, process.formData, req.user!.orgId, req.user!.id) } catch (execErr: any) { return res.status(400).json({ success: false, error: { code: 'EXEC_FAILED', message: `执行失败:${execErr?.message || '未知错误'}` } }) } // 生成文书 const org = await prisma.organization.findUnique({ where: { id: req.user!.orgId } }) const doc = await generateDocument(process.type, process.formData, org?.name || '') const documents = doc.content ? [doc] : [] const updated = await (prisma as any).workProcess.update({ where: { id: process.id }, data: { status: 'COMPLETED', documents: documents.length > 0 ? documents : null, ...(execResult.employeeId && !process.employeeId && { employeeId: execResult.employeeId }), }, }) res.json({ success: true, data: updated }) } catch (err) { next(err) } }) // 审批通过 router.post('/:id/approve', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => { try { const process = await (prisma as any).workProcess.findFirst({ where: { id: req.params.id, orgId: req.user!.orgId }, }) if (!process) { return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '办理记录不存在' } }) } if (process.status !== 'PENDING_APPROVAL') { return res.status(400).json({ success: false, error: { code: 'NOT_PENDING', message: '仅待审批状态可审批' } }) } let execResult: any = {} try { execResult = await executeWorkProcess(process.id, process.type, process.formData, req.user!.orgId, req.user!.id) } catch (execErr: any) { return res.status(400).json({ success: false, error: { code: 'EXEC_FAILED', message: `执行失败:${execErr?.message || '未知错误'}` } }) } const org = await prisma.organization.findUnique({ where: { id: req.user!.orgId } }) const doc = await generateDocument(process.type, process.formData, org?.name || '') const documents = doc.content ? [doc] : [] const updated = await (prisma as any).workProcess.update({ where: { id: process.id }, data: { status: 'COMPLETED', approverId: req.user!.id, approvedAt: new Date(), documents: documents.length > 0 ? documents : null, ...(execResult.employeeId && !process.employeeId && { employeeId: execResult.employeeId }), }, }) // 入职审批通过且开启了入职文件电子签,创建电子签记录 if (execResult.employeeId && process.type === 'ONBOARDING') { const orgSettings = await prisma.organization.findUnique({ where: { id: req.user!.orgId }, select: { esignOnboardingEnabled: true } }) if (orgSettings?.esignOnboardingEnabled) { await prisma.eSignRecord.create({ data: { orgId: req.user!.orgId, employeeId: execResult.employeeId, scene: 'ONBOARDING', documentTitle: '入职文件签署', status: 'PENDING', initiatedBy: req.user!.id, createdBy: req.user!.id, remark: '入职审批通过后自动发起', expiredAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), }, }) } } // 离职证明/收入证明审批通过后,如果有关联员工,创建电子签记录 if (execResult.employeeId && (process.type === 'LEAVING_CERT' || process.type === 'INCOME_CERT')) { const docTitle = process.type === 'LEAVING_CERT' ? '离职证明签署' : '收入证明签署' await prisma.eSignRecord.create({ data: { orgId: req.user!.orgId, employeeId: execResult.employeeId, scene: process.type === 'LEAVING_CERT' ? 'RESIGNATION' : 'OTHER', documentTitle: docTitle, status: 'PENDING', initiatedBy: req.user!.id, createdBy: req.user!.id, remark: '文书审批通过后自动发起', expiredAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), }, }).catch(() => {}) } res.json({ success: true, data: updated }) } catch (err) { next(err) } }) // 驳回 router.post('/:id/reject', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => { try { const process = await (prisma as any).workProcess.findFirst({ where: { id: req.params.id, orgId: req.user!.orgId }, }) if (!process) { return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '办理记录不存在' } }) } if (process.status !== 'PENDING_APPROVAL') { return res.status(400).json({ success: false, error: { code: 'NOT_PENDING', message: '仅待审批状态可驳回' } }) } const updated = await (prisma as any).workProcess.update({ where: { id: process.id }, data: { status: 'REJECTED', approverId: req.user!.id, approvedAt: new Date(), remark: req.body.reason || '驳回', }, }) res.json({ success: true, data: updated }) } catch (err) { next(err) } }) // 撤销 router.post('/:id/cancel', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => { try { const process = await (prisma as any).workProcess.findFirst({ where: { id: req.params.id, orgId: req.user!.orgId }, }) if (!process) { return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '办理记录不存在' } }) } if (['COMPLETED', 'CANCELLED'].includes(process.status)) { return res.status(400).json({ success: false, error: { code: 'INVALID_STATUS', message: '已完成或已撤销的记录不可撤销' } }) } const updated = await (prisma as any).workProcess.update({ where: { id: process.id }, data: { status: 'CANCELLED' }, }) res.json({ success: true, data: updated }) } catch (err) { next(err) } }) // 删除草稿 router.delete('/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => { try { const process = await (prisma as any).workProcess.findFirst({ where: { id: req.params.id, orgId: req.user!.orgId }, }) if (!process) { return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '办理记录不存在' } }) } if (process.status !== 'DRAFT') { return res.status(400).json({ success: false, error: { code: 'NOT_DRAFT', message: '仅草稿状态可删除' } }) } await (prisma as any).workProcess.delete({ where: { id: process.id } }) res.json({ success: true, data: { message: '已删除' } }) } catch (err) { next(err) } }) // 预览文书 router.get('/:id/preview', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => { try { const process = await (prisma as any).workProcess.findFirst({ where: { id: req.params.id, orgId: req.user!.orgId }, }) if (!process) { return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '办理记录不存在' } }) } const org = await prisma.organization.findUnique({ where: { id: req.user!.orgId } }) const doc = await generateDocument(process.type, process.formData, org?.name || '') res.json({ success: true, data: doc }) } catch (err) { next(err) } }) // 获取流程类型列表 router.get('/meta/types', authMiddleware, (_req: AuthRequest, res: Response) => { res.json({ success: true, data: PROCESS_TYPES }) }) // 获取状态列表 router.get('/meta/statuses', authMiddleware, (_req: AuthRequest, res: Response) => { res.json({ success: true, data: PROCESS_STATUS }) }) export default router