Files
TurboHR/backend/src/routes/approval.routes.ts
T
selfrelease e1b5ae9aab feat: 20260815 系统优化 - 全部31项问题修复(P0×6+P1×14+P2×9+P3×2)
P0紧急修复(6项):
- 草稿保存完整恢复所有字段(含socialAvgWage)
- 补偿金批次从compensationBreakdown读取
- 违法解除风险确认UI
- 合同结束日期前后校验(前后端双保险)

P1高优先级(14项):
- 离职日期联动社保/公积金截止月(15号规则)
- 合规检查+工作交接改为软阻断(生成待办)
- 补偿月数(N/N+1/2N/自定义)+计算基数(近12月/合同/自定义)
- 解聘并入花名册操作栏(类型选择跳转向导)
- 合同续签开始日期自动推导(原合同结束日+1天)
- 年龄合规筛查(童工阻断/未成年工/退休警告)
- 编辑入职日期后状态联动(待入职↔在职)
- 转正移植到花名册操作栏+薪资回写
- 男职工无法选择三期

P2体验优化(9项):
- "劳动合同"调整为"用工关系"
- 费用结算新增剩余年假折算(300%日工资)
- 身份证号全域改为"证件号码"(前后端18个文件)
- 手机号查重
- 开具证明+合同续签移植到花名册操作栏
- 批量转正+批量开具证明
- 去掉用工办理模块

P3规划(2项):
- 组织架构+审批流(Department/Position/ApprovalFlow/ApprovalInstance)
- 客服工作台(Ticket/ChatSession+SUPPORT角色)

新增模型: Department/Position/ApprovalFlow/ApprovalInstance/Ticket/TicketMessage/ChatSession/ChatMessage
新增字段: Employee.departmentId/supervisorId
新增角色: SUPPORT

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-15 12:37:27 +08:00

108 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 审批流路由
* 提供审批流配置和实例管理
*/
import { Router } from 'express'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import prisma from '../lib/prisma'
import { z } from 'zod'
import { processApproval, cancelApproval } from '../services/approval.service'
const router = Router()
const flowSchema = z.object({
type: z.string(), // LEAVE / TERMINATION / SALARY_CHANGE / OTHER
name: z.string().min(1),
enabled: z.boolean().default(true),
steps: z.array(z.object({
step: z.number().int().min(1).max(3),
approverType: z.enum(['SUPERVISOR', 'DEPT_HEAD', 'PERSON']),
approverId: z.string().optional(),
name: z.string(),
})).min(1, '至少一个审批步骤').max(3, '最多三个审批步骤'),
})
/** 获取审批流配置列表 */
router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const flows = await prisma.approvalFlow.findMany({
where: { orgId: req.user!.orgId! },
orderBy: { createdAt: 'asc' },
})
res.json({ success: true, data: flows })
} catch (err) {
next(err)
}
})
/** 创建/更新审批流配置(upsert by type */
router.post('/', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const data = flowSchema.parse(req.body)
const existing = await prisma.approvalFlow.findFirst({
where: { orgId: req.user!.orgId!, type: data.type },
})
let flow
if (existing) {
flow = await prisma.approvalFlow.update({
where: { id: existing.id },
data: { ...data, createdBy: req.user!.id },
})
} else {
flow = await prisma.approvalFlow.create({
data: {
...data,
orgId: req.user!.orgId!,
createdBy: req.user!.id,
},
})
}
res.json({ success: true, data: flow })
} catch (err) {
next(err)
}
})
/** 获取待我审批的实例 */
router.get('/pending', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const instances = await prisma.approvalInstance.findMany({
where: { orgId: req.user!.orgId!, status: 'PENDING' },
orderBy: { createdAt: 'desc' },
include: { employee: { select: { id: true, name: true, department: true } } },
})
res.json({ success: true, data: instances })
} catch (err) {
next(err)
}
})
/** 处理审批 */
router.post('/:id/process', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const { id } = req.params
const { result, comment } = req.body as { result: 'APPROVED' | 'REJECTED'; comment?: string }
if (!result || !['APPROVED', 'REJECTED'].includes(result)) {
throw { code: 'VALIDATION_ERROR', message: 'result 必须为 APPROVED 或 REJECTED' }
}
const approverName = req.user!.id || '审批人'
const outcome = await processApproval(req.user!.orgId!, id, req.user!.id, approverName, result, comment)
res.json({ success: true, data: outcome })
} catch (err) {
next(err)
}
})
/** 取消审批 */
router.post('/:id/cancel', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const { id } = req.params
await cancelApproval(req.user!.orgId!, id)
res.json({ success: true })
} catch (err) {
next(err)
}
})
export default router