feat: 商业保险/员工福利独立页面+易签宝电子签署框架

- 商业保险:从社公商保中拆出为独立页面,侧边栏新增「福利保障」分组
- 员工福利:新建完整模块(方案管理+批量参保+员工汇总),Prisma模型+后端路由+前端页面
- 电子签署:搭建易签宝对接框架(ESignRecord模型+创建/查询/取消/回调接口+前端签署管理页面)
- 侧边栏新增「福利保障」分组:商业保险、员工福利、电子签署
- Prisma schema 新增6个模型:CommercialInsurancePlan/Enrollment, EmployeeBenefitPlan/Enrollment, ESignRecord
This commit is contained in:
freedakgmail
2026-08-04 23:08:49 +08:00
parent 5604d02de9
commit c328172e7d
12 changed files with 1348 additions and 9 deletions
+173
View File
@@ -0,0 +1,173 @@
import { Router, Response, NextFunction } from 'express'
import { z } from 'zod'
import prisma from '../lib/prisma'
import { authMiddleware, AuthRequest } from '../middleware/auth'
const router = Router()
router.use(authMiddleware)
const planSchema = z.object({
name: z.string().min(1),
category: z.string(),
amount: z.number().default(0),
frequency: z.string().default('MONTHLY'),
taxDeductible: z.boolean().default(false),
description: z.string().optional(),
})
const BENEFIT_CATEGORIES: Record<string, string> = {
TRANSPORT: '交通补贴',
MEAL: '餐补',
HOUSING: '住房补贴',
COMMUNICATION: '通讯补贴',
HEALTH_CHECK: '体检',
HOLIDAY: '节日福利',
BIRTHDAY: '生日福利',
OTHER: '其他',
}
// 福利方案列表
router.get('/plans', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const plans = await prisma.employeeBenefitPlan.findMany({
where: { orgId: req.user!.orgId },
orderBy: { createdAt: 'desc' },
include: { _count: { select: { enrollments: { where: { status: 'ACTIVE' } } } } },
})
res.json({ success: true, data: plans })
} catch (err) { next(err) }
})
// 创建福利方案
router.post('/plans', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const data = planSchema.parse(req.body)
const plan = await prisma.employeeBenefitPlan.create({
data: { ...data, orgId: req.user!.orgId, createdBy: req.user!.id },
})
res.json({ success: true, data: plan })
} catch (err) { next(err) }
})
// 更新福利方案
router.put('/plans/:planId', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const data = planSchema.partial().parse(req.body)
const plan = await prisma.employeeBenefitPlan.update({
where: { id: req.params.planId, orgId: req.user!.orgId },
data,
})
res.json({ success: true, data: plan })
} catch (err) { next(err) }
})
// 删除福利方案
router.delete('/plans/:planId', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
await prisma.employeeBenefitPlan.delete({
where: { id: req.params.planId, orgId: req.user!.orgId },
})
res.json({ success: true })
} catch (err) { next(err) }
})
// 方案参保人员
router.get('/plans/:planId/enrollments', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const enrollments = await prisma.employeeBenefitEnrollment.findMany({
where: { orgId: req.user!.orgId, planId: req.params.planId },
include: { employee: { select: { id: true, name: true, department: true } } },
orderBy: { createdAt: 'desc' },
})
const data = enrollments.map((e: any) => ({
id: e.id,
employeeId: e.employeeId,
name: e.employee.name,
department: e.employee.department,
effectiveFrom: e.effectiveFrom,
effectiveTo: e.effectiveTo,
amount: e.amount,
status: e.status,
}))
res.json({ success: true, data })
} catch (err) { next(err) }
})
// 批量参保
router.post('/plans/:planId/enroll', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { employeeIds, effectiveFrom } = req.body as { employeeIds: string[]; effectiveFrom: string }
const plan = await prisma.employeeBenefitPlan.findFirst({ where: { id: req.params.planId, orgId: req.user!.orgId } })
if (!plan) return res.status(404).json({ success: false, error: { message: '方案不存在' } })
const existing = await prisma.employeeBenefitEnrollment.findMany({
where: { planId: plan.id, employeeId: { in: employeeIds }, status: 'ACTIVE' },
select: { employeeId: true },
})
const existingIds = new Set(existing.map((e: any) => e.employeeId))
const newIds = employeeIds.filter((id) => !existingIds.has(id))
if (newIds.length > 0) {
await prisma.employeeBenefitEnrollment.createMany({
data: newIds.map((empId) => ({
orgId: req.user!.orgId,
planId: plan.id,
employeeId: empId,
effectiveFrom: effectiveFrom || new Date().toISOString().slice(0, 7),
})),
})
}
res.json({ success: true, data: { enrolled: newIds.length, skipped: existingIds.size } })
} catch (err) { next(err) }
})
// 退福利
router.post('/enrollments/:enrollmentId/terminate', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { effectiveTo } = req.body as { effectiveTo: string }
const enrollment = await prisma.employeeBenefitEnrollment.update({
where: { id: req.params.enrollmentId, orgId: req.user!.orgId },
data: { status: 'TERMINATED', effectiveTo: effectiveTo || new Date().toISOString().slice(0, 7) },
})
res.json({ success: true, data: enrollment })
} catch (err) { next(err) }
})
// 员工福利汇总(按员工维度)
router.get('/employee-summary', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const enrollments = await prisma.employeeBenefitEnrollment.findMany({
where: { orgId: req.user!.orgId, status: 'ACTIVE' },
include: {
employee: { select: { id: true, name: true, department: true } },
plan: { select: { id: true, name: true, category: true, amount: true } },
},
})
const summary: Record<string, any> = {}
for (const e of enrollments) {
if (!summary[e.employeeId]) {
summary[e.employeeId] = {
employeeId: e.employeeId,
name: e.employee.name,
department: e.employee.department,
benefits: [],
totalMonthly: 0,
}
}
const amount = e.amount ?? e.plan.amount
summary[e.employeeId].benefits.push({
planId: e.planId,
planName: e.plan.name,
category: e.plan.category,
categoryLabel: BENEFIT_CATEGORIES[e.plan.category] || e.plan.category,
amount,
})
if (e.plan.frequency === 'MONTHLY') {
summary[e.employeeId].totalMonthly += amount
}
}
res.json({ success: true, data: Object.values(summary) })
} catch (err) { next(err) }
})
export default router