feat: 员工特殊状态台账(三期/工伤/医疗期)完整实现
This commit is contained in:
@@ -60,6 +60,7 @@ import calendarRoutes from './routes/calendar.routes'
|
||||
import platformRoutes from './routes/platform.routes'
|
||||
import workProcessRoutes from './routes/work-process.routes'
|
||||
import enterpriseTemplateRoutes from './routes/enterprise-template.routes'
|
||||
import specialStatusRoutes from './routes/special-status.routes'
|
||||
app.use('/api/v1/auth', authRoutes)
|
||||
app.use('/api/v1/dashboard', dashboardRoutes)
|
||||
app.use('/api/v1/employees', employeeRoutes)
|
||||
@@ -84,6 +85,7 @@ app.use('/api/v1/calendar', calendarRoutes)
|
||||
app.use('/api/v1/platform', platformRoutes)
|
||||
app.use('/api/v1/work-processes', workProcessRoutes)
|
||||
app.use('/api/v1/enterprise-templates', enterpriseTemplateRoutes)
|
||||
app.use('/api/v1/special-statuses', specialStatusRoutes)
|
||||
|
||||
app.use(errorHandler)
|
||||
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* 员工特殊状态台账路由
|
||||
* 管理三期/工伤/医疗期等特殊状态的 CRUD 和提醒
|
||||
*/
|
||||
import { Router, Response, NextFunction } from 'express'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import prisma from '../lib/prisma'
|
||||
import {
|
||||
createSpecialStatus,
|
||||
updateSpecialStatus,
|
||||
deleteSpecialStatus,
|
||||
getPendingReminders,
|
||||
STATUS_TYPES,
|
||||
getAlertLevel,
|
||||
} from '../services/special-status.service'
|
||||
|
||||
const router = Router()
|
||||
|
||||
/**
|
||||
* 获取特殊状态列表(分页 + 筛选)
|
||||
*/
|
||||
router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const orgId = req.user!.orgId
|
||||
const page = parseInt(req.query.page as string) || 1
|
||||
const pageSize = parseInt(req.query.pageSize as string) || 20
|
||||
const type = (req.query.type as string) || ''
|
||||
const status = (req.query.status as string) || ''
|
||||
const search = (req.query.search as string) || ''
|
||||
|
||||
const where: any = { orgId }
|
||||
if (type) where.type = type
|
||||
if (status) where.status = status
|
||||
if (search) {
|
||||
where.employee = {
|
||||
OR: [
|
||||
{ name: { contains: search, mode: 'insensitive' } },
|
||||
{ phone: { contains: search } },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
const [list, total] = await Promise.all([
|
||||
(prisma as any).employeeSpecialStatus.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
employee: {
|
||||
select: { id: true, name: true, department: true, phone: true, gender: true, status: true },
|
||||
},
|
||||
},
|
||||
}),
|
||||
(prisma as any).employeeSpecialStatus.count({ where }),
|
||||
])
|
||||
|
||||
// 附加预警级别
|
||||
const listWithAlert = list.map((item: any) => ({
|
||||
...item,
|
||||
alertLevel: getAlertLevel(item.reminderDate, item.status),
|
||||
}))
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { list: listWithAlert, total, page, pageSize },
|
||||
})
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取单条详情
|
||||
*/
|
||||
router.get('/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const record = await (prisma as any).employeeSpecialStatus.findFirst({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId },
|
||||
include: {
|
||||
employee: {
|
||||
select: { id: true, name: true, department: true, phone: true, gender: true, hireDate: true },
|
||||
},
|
||||
},
|
||||
})
|
||||
if (!record) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } })
|
||||
}
|
||||
res.json({ success: true, data: record })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 创建特殊状态
|
||||
*/
|
||||
router.post('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const record = await createSpecialStatus(req.user!.orgId, req.user!.id, req.body)
|
||||
res.json({ success: true, data: record })
|
||||
} catch (err: any) {
|
||||
if (err.code) {
|
||||
return res.status(400).json({ success: false, error: { code: err.code, message: err.message } })
|
||||
}
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 更新特殊状态
|
||||
*/
|
||||
router.put('/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const record = await updateSpecialStatus(req.user!.orgId, req.user!.id, req.params.id, req.body)
|
||||
res.json({ success: true, data: record })
|
||||
} catch (err: any) {
|
||||
if (err.code) {
|
||||
return res.status(400).json({ success: false, error: { code: err.code, message: err.message } })
|
||||
}
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 删除特殊状态
|
||||
*/
|
||||
router.delete('/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await deleteSpecialStatus(req.user!.orgId, req.params.id)
|
||||
res.json({ success: true, data: { message: '已删除' } })
|
||||
} catch (err: any) {
|
||||
if (err.code) {
|
||||
return res.status(400).json({ success: false, error: { code: err.code, message: err.message } })
|
||||
}
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取待提醒列表
|
||||
*/
|
||||
router.get('/reminders/pending', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const reminders = await getPendingReminders(req.user!.orgId)
|
||||
res.json({ success: true, data: reminders })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取统计概览
|
||||
*/
|
||||
router.get('/stats/overview', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const orgId = req.user!.orgId
|
||||
const [total, active, pending, byType] = await Promise.all([
|
||||
(prisma as any).employeeSpecialStatus.count({ where: { orgId } }),
|
||||
(prisma as any).employeeSpecialStatus.count({ where: { orgId, status: 'ACTIVE' } }),
|
||||
(prisma as any).employeeSpecialStatus.count({ where: { orgId, status: 'PENDING' } }),
|
||||
(prisma as any).employeeSpecialStatus.groupBy({
|
||||
by: ['type'],
|
||||
where: { orgId, status: 'ACTIVE' },
|
||||
_count: true,
|
||||
}),
|
||||
])
|
||||
|
||||
// 预警统计
|
||||
const now = new Date()
|
||||
const soon7 = new Date()
|
||||
soon7.setDate(soon7.getDate() + 7)
|
||||
const soon30 = new Date()
|
||||
soon30.setDate(soon30.getDate() + 30)
|
||||
|
||||
const [redCount, yellowCount] = await Promise.all([
|
||||
(prisma as any).employeeSpecialStatus.count({
|
||||
where: { orgId, status: 'ACTIVE', reminderDate: { lte: soon7 } },
|
||||
}),
|
||||
(prisma as any).employeeSpecialStatus.count({
|
||||
where: { orgId, status: 'ACTIVE', reminderDate: { gt: soon7, lte: soon30 } },
|
||||
}),
|
||||
])
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
total,
|
||||
active,
|
||||
pending,
|
||||
redAlert: redCount,
|
||||
yellowAlert: yellowCount,
|
||||
byType: byType.map((t: any) => ({ type: t.type, label: STATUS_TYPES[t.type]?.label || t.type, count: t._count })),
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -305,6 +305,80 @@ export async function detectTerminationRisks(orgId: string) {
|
||||
return risks
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测特殊状态相关风险(三期/工伤/医疗期员工合同到期不得终止)
|
||||
*/
|
||||
async function detectSpecialStatusRisks(orgId: string) {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
// 查找所有进行中的特殊状态记录
|
||||
const specialStatuses = await (prisma as any).employeeSpecialStatus.findMany({
|
||||
where: { orgId, status: 'ACTIVE' },
|
||||
include: {
|
||||
employee: {
|
||||
select: { id: true, name: true, status: true, contracts: { orderBy: { createdAt: 'desc' }, take: 1 } },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const risks: { employeeId: string; type: RiskType; level: RiskLevel; title: string; description: string; actionUrl: string }[] = []
|
||||
|
||||
for (const ss of specialStatuses) {
|
||||
const emp = ss.employee
|
||||
if (!emp || emp.status !== 'ACTIVE') continue
|
||||
const latestContract = emp.contracts[0]
|
||||
if (!latestContract || !latestContract.endDate) continue
|
||||
|
||||
const daysToExpire = daysBetween(latestContract.endDate, today)
|
||||
const typeLabel = ss.type === 'PREGNANCY' ? '三期' : ss.type === 'WORK_INJURY' ? '工伤' : ss.type === 'MEDICAL_PERIOD' ? '医疗期' : '特殊状态'
|
||||
|
||||
// 合同即将到期但处于特殊状态期间,不得终止
|
||||
if (daysToExpire >= 0 && daysToExpire <= 60) {
|
||||
risks.push({
|
||||
employeeId: emp.id,
|
||||
type: 'CONTRACT' as RiskType,
|
||||
level: 'HIGH' as RiskLevel,
|
||||
title: `${emp.name}处于${typeLabel}期间,合同${daysToExpire}天后到期,依法不得终止`,
|
||||
description: `${typeLabel}员工在合同到期时,用人单位不得依照《劳动合同法》第四十条、第四十一条终止合同,需顺延至相应情形消失。`,
|
||||
actionUrl: `/special-status?type=${ss.type}`,
|
||||
})
|
||||
}
|
||||
|
||||
// 工伤认定超期提醒(受伤30天内未认定)
|
||||
if (ss.type === 'WORK_INJURY' && ss.injuryDate && !ss.certificationDate) {
|
||||
const daysSinceInjury = daysBetween(today, new Date(ss.injuryDate))
|
||||
if (daysSinceInjury > 30) {
|
||||
risks.push({
|
||||
employeeId: emp.id,
|
||||
type: 'CONTRACT' as RiskType,
|
||||
level: 'HIGH' as RiskLevel,
|
||||
title: `${emp.name}工伤已${daysSinceInjury}天未完成认定,超30天限期`,
|
||||
description: `用人单位应自事故伤害发生之日起30日内提出工伤认定申请,逾期将影响工伤待遇。`,
|
||||
actionUrl: `/special-status?type=WORK_INJURY`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 医疗期即将到期
|
||||
if (ss.type === 'MEDICAL_PERIOD' && ss.medicalPeriodEnd) {
|
||||
const daysToMedicalEnd = daysBetween(new Date(ss.medicalPeriodEnd), today)
|
||||
if (daysToMedicalEnd >= 0 && daysToMedicalEnd <= 30) {
|
||||
risks.push({
|
||||
employeeId: emp.id,
|
||||
type: 'CONTRACT' as RiskType,
|
||||
level: daysToMedicalEnd <= 7 ? 'HIGH' as RiskLevel : 'MEDIUM' as RiskLevel,
|
||||
title: `${emp.name}医疗期${daysToMedicalEnd}天后到期,需提前处理`,
|
||||
description: `医疗期届满后,员工仍需治疗的,用人单位应按规定处理。医疗期满不能从事原工作的,可依法解除合同但需支付经济补偿。`,
|
||||
actionUrl: `/special-status?type=MEDICAL_PERIOD`,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return risks
|
||||
}
|
||||
|
||||
export async function detectMonthlyTasks(orgId: string) {
|
||||
const setting = await prisma.notificationSetting.findUnique({ where: { orgId } })
|
||||
if (!setting) return []
|
||||
@@ -372,9 +446,10 @@ export async function runRiskDetection(orgId: string) {
|
||||
const terminationRisks = await detectTerminationRisks(orgId)
|
||||
const onboardingRisks = await detectOnboardingRisks(orgId)
|
||||
const monthlyTasks = await detectMonthlyTasks(orgId)
|
||||
const specialStatusRisks = await detectSpecialStatusRisks(orgId)
|
||||
|
||||
// 获取所有相关员工数据用于风险量化
|
||||
const allEmployeeIds = [...contractRisks, ...terminationRisks, ...onboardingRisks]
|
||||
const allEmployeeIds = [...contractRisks, ...terminationRisks, ...onboardingRisks, ...specialStatusRisks]
|
||||
.map(r => r.employeeId)
|
||||
.filter(Boolean) as string[]
|
||||
const employees = allEmployeeIds.length > 0
|
||||
@@ -383,7 +458,7 @@ export async function runRiskDetection(orgId: string) {
|
||||
const empMap = new Map(employees.map(e => [e.id, e]))
|
||||
|
||||
// 月度任务用 monthlyKeys 去重,其他任务用 existingKeys 去重
|
||||
const nonMonthlyRisks = [...contractRisks, ...terminationRisks, ...onboardingRisks]
|
||||
const nonMonthlyRisks = [...contractRisks, ...terminationRisks, ...onboardingRisks, ...specialStatusRisks]
|
||||
const toCreate = [
|
||||
...nonMonthlyRisks.filter((r) => !existingKeys.has(`${r.employeeId}:${r.type}:${r.actionUrl}`)),
|
||||
...monthlyTasks.filter((r) => !monthlyKeys.has(`${r.employeeId}:${r.title}`)),
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* 员工特殊状态台账服务
|
||||
* 管理三期(孕期/产期/哺乳期)、工伤、医疗期等特殊状态的跟踪和提醒
|
||||
*/
|
||||
import prisma from '../lib/prisma'
|
||||
|
||||
/** 特殊状态类型 */
|
||||
export const STATUS_TYPES: Record<string, { label: string; color: string }> = {
|
||||
PREGNANCY: { label: '三期', color: 'pink' },
|
||||
WORK_INJURY: { label: '工伤', color: 'red' },
|
||||
MEDICAL_PERIOD: { label: '医疗期', color: 'orange' },
|
||||
OTHER: { label: '其他', color: 'gray' },
|
||||
}
|
||||
|
||||
/** 状态文本映射 */
|
||||
export const STATUS_LABELS: Record<string, string> = {
|
||||
ACTIVE: '进行中',
|
||||
PENDING: '待处理',
|
||||
RESOLVED: '已结束',
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预警级别(基于提醒日期)
|
||||
* - red: 已过期或7天内到期
|
||||
* - yellow: 30天内到期
|
||||
* - green: 正常
|
||||
*/
|
||||
export function getAlertLevel(reminderDate: Date | null, status: string): 'red' | 'yellow' | 'green' {
|
||||
if (status === 'RESOLVED') return 'green'
|
||||
if (!reminderDate) return 'green'
|
||||
const now = new Date()
|
||||
const days = Math.floor((reminderDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
|
||||
if (days <= 7) return 'red'
|
||||
if (days <= 30) return 'yellow'
|
||||
return 'green'
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据三期数据自动计算关键日期
|
||||
* - 产假: 产前15天 + 产后75天(共98天,难产+15天)
|
||||
* - 哺乳期: 至孩子满1周岁
|
||||
*/
|
||||
export function calculatePregnancyDates(expectedDueDate: Date): {
|
||||
maternityLeaveStart: Date
|
||||
maternityLeaveEnd: Date
|
||||
nursingEndDate: Date
|
||||
} {
|
||||
// 产假开始: 预产期前15天
|
||||
const maternityLeaveStart = new Date(expectedDueDate)
|
||||
maternityLeaveStart.setDate(maternityLeaveStart.getDate() - 15)
|
||||
|
||||
// 产假结束: 预产期后75天(共98天)
|
||||
const maternityLeaveEnd = new Date(expectedDueDate)
|
||||
maternityLeaveEnd.setDate(maternityLeaveEnd.getDate() + 75)
|
||||
|
||||
// 哺乳期截止: 孩子满1周岁
|
||||
const nursingEndDate = new Date(expectedDueDate)
|
||||
nursingEndDate.setFullYear(nursingEndDate.getFullYear() + 1)
|
||||
|
||||
return { maternityLeaveStart, maternityLeaveEnd, nursingEndDate }
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据工龄计算医疗期月数
|
||||
* - 实际工作年限 < 5年: 本单位 < 3年 → 3个月, ≥ 3年 → 6个月
|
||||
* - 实际工作年限 ≥ 5年: 本单位 < 3年 → 6个月, 3~5年 → 9个月, 5~10年 → 12个月, ≥10年 → 24个月
|
||||
*/
|
||||
export function calculateMedicalMonths(totalWorkYears: number, companyYears: number): number {
|
||||
if (totalWorkYears < 5) {
|
||||
return companyYears < 3 ? 3 : 6
|
||||
}
|
||||
if (companyYears < 3) return 6
|
||||
if (companyYears < 5) return 9
|
||||
if (companyYears < 10) return 12
|
||||
return 24
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加操作时间线条目
|
||||
*/
|
||||
function addTimelineEntry(existing: any[], action: string, by: string): any[] {
|
||||
const entry = { time: new Date().toISOString(), action, by }
|
||||
return [...(existing || []), entry]
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建特殊状态记录
|
||||
*/
|
||||
export async function createSpecialStatus(orgId: string, userId: string, data: any) {
|
||||
const employee = await prisma.employee.findFirst({ where: { id: data.employeeId, orgId } })
|
||||
if (!employee) {
|
||||
throw { code: 'NOT_FOUND', message: '员工不存在' }
|
||||
}
|
||||
|
||||
// 三期自动计算
|
||||
let pregnancyData: any = {}
|
||||
if (data.type === 'PREGNANCY' && data.expectedDueDate) {
|
||||
const dates = calculatePregnancyDates(new Date(data.expectedDueDate))
|
||||
pregnancyData = dates
|
||||
}
|
||||
|
||||
// 医疗期自动计算截止日
|
||||
let medicalData: any = {}
|
||||
if (data.type === 'MEDICAL_PERIOD' && data.startDate && data.medicalMonths) {
|
||||
const end = new Date(data.startDate)
|
||||
end.setMonth(end.getMonth() + data.medicalMonths)
|
||||
medicalData.medicalPeriodEnd = end
|
||||
}
|
||||
|
||||
const record = await (prisma as any).employeeSpecialStatus.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: data.employeeId,
|
||||
type: data.type,
|
||||
status: data.status || 'ACTIVE',
|
||||
startDate: data.startDate ? new Date(data.startDate) : null,
|
||||
endDate: data.endDate ? new Date(data.endDate) : null,
|
||||
expectedDueDate: data.expectedDueDate ? new Date(data.expectedDueDate) : null,
|
||||
maternityLeaveStart: pregnancyData.maternityLeaveStart || null,
|
||||
maternityLeaveEnd: pregnancyData.maternityLeaveEnd || null,
|
||||
nursingEndDate: pregnancyData.nursingEndDate || null,
|
||||
injuryDate: data.injuryDate ? new Date(data.injuryDate) : null,
|
||||
injuryDescription: data.injuryDescription || null,
|
||||
certificationDate: data.certificationDate ? new Date(data.certificationDate) : null,
|
||||
certificationNo: data.certificationNo || null,
|
||||
disabilityLevel: data.disabilityLevel || null,
|
||||
assessmentDate: data.assessmentDate ? new Date(data.assessmentDate) : null,
|
||||
medicalMonths: data.medicalMonths || null,
|
||||
medicalPeriodEnd: medicalData.medicalPeriodEnd || null,
|
||||
description: data.description || null,
|
||||
attachments: data.attachments || null,
|
||||
reminderDate: data.reminderDate ? new Date(data.reminderDate) : null,
|
||||
timeline: addTimelineEntry([], '创建记录', userId),
|
||||
createdBy: userId,
|
||||
},
|
||||
})
|
||||
|
||||
// 同步更新 Employee 的布尔字段
|
||||
await syncEmployeeFlags(employee.id, data.type, true)
|
||||
|
||||
return record
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新特殊状态记录
|
||||
*/
|
||||
export async function updateSpecialStatus(orgId: string, userId: string, id: string, data: any) {
|
||||
const existing = await (prisma as any).employeeSpecialStatus.findFirst({
|
||||
where: { id, orgId },
|
||||
})
|
||||
if (!existing) {
|
||||
throw { code: 'NOT_FOUND', message: '记录不存在' }
|
||||
}
|
||||
|
||||
const updateData: any = {}
|
||||
const fields = [
|
||||
'type', 'status', 'startDate', 'endDate', 'actualEndDate',
|
||||
'expectedDueDate', 'maternityLeaveStart', 'maternityLeaveEnd', 'nursingEndDate',
|
||||
'injuryDate', 'injuryDescription', 'certificationDate', 'certificationNo',
|
||||
'disabilityLevel', 'assessmentDate', 'medicalMonths', 'medicalPeriodEnd',
|
||||
'description', 'attachments', 'reminderDate',
|
||||
]
|
||||
for (const f of fields) {
|
||||
if (data[f] !== undefined) {
|
||||
updateData[f] = data[f] === null ? null : (data[f] instanceof Date || f === 'injuryDescription' || f === 'certificationNo' || f === 'description' || f === 'type' || f === 'status' || f === 'attachments' ? data[f] : new Date(data[f]))
|
||||
}
|
||||
}
|
||||
|
||||
// 三期自动计算
|
||||
if (data.type === 'PREGNANCY' && data.expectedDueDate) {
|
||||
const dates = calculatePregnancyDates(new Date(data.expectedDueDate))
|
||||
updateData.maternityLeaveStart = dates.maternityLeaveStart
|
||||
updateData.maternityLeaveEnd = dates.maternityLeaveEnd
|
||||
updateData.nursingEndDate = dates.nursingEndDate
|
||||
}
|
||||
|
||||
// 医疗期自动计算截止日
|
||||
if (data.type === 'MEDICAL_PERIOD' && data.startDate && data.medicalMonths) {
|
||||
const end = new Date(data.startDate)
|
||||
end.setMonth(end.getMonth() + data.medicalMonths)
|
||||
updateData.medicalPeriodEnd = end
|
||||
}
|
||||
|
||||
// 记录时间线
|
||||
const actions: string[] = []
|
||||
if (data.status && data.status !== existing.status) {
|
||||
actions.push(`状态变更为: ${STATUS_LABELS[data.status] || data.status}`)
|
||||
}
|
||||
if (data.certificationDate && !existing.certificationDate) {
|
||||
actions.push('工伤认定完成')
|
||||
}
|
||||
if (data.assessmentDate && !existing.assessmentDate) {
|
||||
actions.push('伤残鉴定完成')
|
||||
}
|
||||
if (data.actualEndDate && !existing.actualEndDate) {
|
||||
actions.push('状态结束')
|
||||
}
|
||||
if (actions.length === 0) {
|
||||
actions.push('更新记录')
|
||||
}
|
||||
updateData.timeline = addTimelineEntry(existing.timeline, actions.join('; '), userId)
|
||||
|
||||
const record = await (prisma as any).employeeSpecialStatus.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
})
|
||||
|
||||
// 如果状态变为 RESOLVED,同步 Employee 布尔字段
|
||||
if (data.status === 'RESOLVED' && existing.status !== 'RESOLVED') {
|
||||
await syncEmployeeFlags(existing.employeeId, existing.type, false)
|
||||
}
|
||||
|
||||
return record
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除特殊状态记录
|
||||
*/
|
||||
export async function deleteSpecialStatus(orgId: string, id: string) {
|
||||
const existing = await (prisma as any).employeeSpecialStatus.findFirst({
|
||||
where: { id, orgId },
|
||||
})
|
||||
if (!existing) {
|
||||
throw { code: 'NOT_FOUND', message: '记录不存在' }
|
||||
}
|
||||
|
||||
await (prisma as any).employeeSpecialStatus.delete({ where: { id } })
|
||||
|
||||
// 同步 Employee 布尔字段
|
||||
if (existing.status === 'ACTIVE') {
|
||||
await syncEmployeeFlags(existing.employeeId, existing.type, false)
|
||||
}
|
||||
|
||||
return { id }
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步 Employee 表的布尔标记字段
|
||||
*/
|
||||
async function syncEmployeeFlags(employeeId: string, type: string, active: boolean) {
|
||||
const updateData: any = {}
|
||||
if (type === 'PREGNANCY') updateData.isPregnant = active
|
||||
if (type === 'MEDICAL_PERIOD') updateData.isInMedicalPeriod = active
|
||||
if (type === 'WORK_INJURY') updateData.isWorkInjured = active
|
||||
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
await prisma.employee.update({ where: { id: employeeId }, data: updateData })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取需要提醒的特殊状态列表
|
||||
*/
|
||||
export async function getPendingReminders(orgId: string) {
|
||||
const now = new Date()
|
||||
const soon = new Date()
|
||||
soon.setDate(soon.getDate() + 7)
|
||||
|
||||
return (prisma as any).employeeSpecialStatus.findMany({
|
||||
where: {
|
||||
orgId,
|
||||
status: 'ACTIVE',
|
||||
reminderDate: { gte: now, lte: soon },
|
||||
},
|
||||
include: {
|
||||
employee: { select: { id: true, name: true, department: true, phone: true } },
|
||||
},
|
||||
orderBy: { reminderDate: 'asc' },
|
||||
})
|
||||
}
|
||||
@@ -220,6 +220,21 @@ export async function createTermination(orgId: string, userId: string, data: any
|
||||
throw { code: 'NOT_FOUND', message: '员工不存在' }
|
||||
}
|
||||
|
||||
// 特殊状态拦截:三期/工伤/医疗期员工不得违法终止
|
||||
const activeSpecialStatus = await (prisma as any).employeeSpecialStatus.findFirst({
|
||||
where: { orgId, employeeId: data.employeeId, status: 'ACTIVE' },
|
||||
})
|
||||
if (activeSpecialStatus) {
|
||||
const typeLabel = activeSpecialStatus.type === 'PREGNANCY' ? '三期(孕期/产期/哺乳期)'
|
||||
: activeSpecialStatus.type === 'WORK_INJURY' ? '工伤'
|
||||
: activeSpecialStatus.type === 'MEDICAL_PERIOD' ? '医疗期'
|
||||
: '特殊状态'
|
||||
throw {
|
||||
code: 'SPECIAL_STATUS_BLOCK',
|
||||
message: `该员工处于${typeLabel}期间,依法不得终止劳动合同。如需操作,请先在特殊状态台账中结束该状态记录。`,
|
||||
}
|
||||
}
|
||||
|
||||
const { level } = assessRisk(employee, data.reason)
|
||||
|
||||
return createTerminationRecord(
|
||||
|
||||
Reference in New Issue
Block a user