86e5526a83
- 问题1/3: 绩效考核/培训记录员工姓名可点击跳转员工详情页 - 问题2: 离职证明模板支持自定义+员工端下载 - 问题4(P0): 修复工资填写后数据归零问题 - 问题5: 社保添加员工参保信息列表 - 问题6(P0): 商业保险支持为员工参保 - 问题7(P0): 员工福利支持为员工添加福利 - 问题8: 规章制度支持导入Word文档 - 问题9: 文本模板下载Word增加HTML格式 - 问题10: 模板下载变量替换修复(排除token参数) - 问题11(P0): 电子签署发起时员工下拉框有选项 - 问题12: 新增绩效记录添加考评人选项 - 问题13: 违纪记录添加处罚执行细节 - 问题14: 特殊员工列表添加查看详情按钮和姓名链接 - 问题15: 员工福利汇总正确显示参保人员 - 问题16(P0): 证据链验证修复(递归排序key+自动修复历史哈希)
174 lines
6.0 KiB
TypeScript
174 lines
6.0 KiB
TypeScript
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, frequency: 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
|