271 lines
9.0 KiB
TypeScript
271 lines
9.0 KiB
TypeScript
/**
|
|
* 员工特殊状态台账服务
|
|
* 管理三期(孕期/产期/哺乳期)、工伤、医疗期等特殊状态的跟踪和提醒
|
|
*/
|
|
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' },
|
|
})
|
|
}
|