feat: 员工特殊状态台账(三期/工伤/医疗期)完整实现

This commit is contained in:
selfrelease
2026-07-30 15:04:34 +08:00
parent 5a5ce7186b
commit 826c8fda65
11 changed files with 1275 additions and 6 deletions
@@ -0,0 +1,45 @@
-- 员工特殊状态台账表(三期/工伤/医疗期等)
CREATE TABLE IF NOT EXISTS "EmployeeSpecialStatus" (
"id" TEXT NOT NULL,
"orgId" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'ACTIVE',
"startDate" TIMESTAMP(3),
"endDate" TIMESTAMP(3),
"actualEndDate" TIMESTAMP(3),
"expectedDueDate" TIMESTAMP(3),
"maternityLeaveStart" TIMESTAMP(3),
"maternityLeaveEnd" TIMESTAMP(3),
"nursingEndDate" TIMESTAMP(3),
"injuryDate" TIMESTAMP(3),
"injuryDescription" TEXT,
"certificationDate" TIMESTAMP(3),
"certificationNo" TEXT,
"disabilityLevel" INTEGER,
"assessmentDate" TIMESTAMP(3),
"medicalMonths" INTEGER,
"medicalPeriodEnd" TIMESTAMP(3),
"description" TEXT,
"attachments" JSONB,
"timeline" JSONB,
"reminderDate" TIMESTAMP(3),
"createdBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "EmployeeSpecialStatus_pkey" PRIMARY KEY ("id")
);
CREATE INDEX IF NOT EXISTS "EmployeeSpecialStatus_orgId_status_idx" ON "EmployeeSpecialStatus"("orgId", "status");
CREATE INDEX IF NOT EXISTS "EmployeeSpecialStatus_orgId_type_idx" ON "EmployeeSpecialStatus"("orgId", "type");
CREATE INDEX IF NOT EXISTS "EmployeeSpecialStatus_employeeId_idx" ON "EmployeeSpecialStatus"("employeeId");
-- 外键约束
ALTER TABLE "EmployeeSpecialStatus"
ADD CONSTRAINT "EmployeeSpecialStatus_orgId_fkey"
FOREIGN KEY ("orgId") REFERENCES "Organization"("id") ON DELETE CASCADE;
ALTER TABLE "EmployeeSpecialStatus"
ADD CONSTRAINT "EmployeeSpecialStatus_employeeId_fkey"
FOREIGN KEY ("employeeId") REFERENCES "Employee"("id") ON DELETE CASCADE;
+48
View File
@@ -256,6 +256,7 @@ model Employee {
leaveRecords LeaveRecord[]
calendarEvents CalendarEvent[]
workProcesses WorkProcess[]
specialStatuses EmployeeSpecialStatus[]
@@unique([orgId, idCardHash])
}
@@ -1235,3 +1236,50 @@ model AttendancePublish {
@@unique([orgId, month])
@@index([orgId, month])
}
// ========== 员工特殊状态台账(三期/工伤/医疗期等) ==========
model EmployeeSpecialStatus {
id String @id @default(cuid())
orgId String
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
employeeId String
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
type String // PREGNANCY(三期) | WORK_INJURY(工伤) | MEDICAL_PERIOD(医疗期) | OTHER
status String @default("ACTIVE") // ACTIVE(进行中) | RESOLVED(已结束) | PENDING(待处理)
// 通用时间节点
startDate DateTime? // 开始日期
endDate DateTime? // 预计结束日期
actualEndDate DateTime? // 实际结束日期
// 三期专用
expectedDueDate DateTime? // 预产期
maternityLeaveStart DateTime? // 产假开始
maternityLeaveEnd DateTime? // 产假结束
nursingEndDate DateTime? // 哺乳期截止
// 工伤专用
injuryDate DateTime? // 受伤日期
injuryDescription String? // 伤情描述
certificationDate DateTime? // 工伤认定日期
certificationNo String? // 认定书编号
disabilityLevel Int? // 伤残等级(1-10级)
assessmentDate DateTime? // 鉴定日期
// 医疗期专用
medicalMonths Int? // 医疗期月数
medicalPeriodEnd DateTime? // 医疗期截止日
// 通用
description String? // 备注
attachments Json? // 附件列表 [{name, url}]
timeline Json? // 操作时间线 [{time, action, by}]
reminderDate DateTime? // 提醒日期
createdBy String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([orgId, status])
@@index([orgId, type])
@@index([employeeId])
}
+2
View File
@@ -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)
+201
View File
@@ -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
+77 -2
View File
@@ -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(
+3 -3
View File
@@ -14,9 +14,9 @@
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"ignoreDeprecations": "6.0"
},
"include": ["src/**/*", "prisma/**/*"],
"exclude": ["node_modules", "dist"],
"ignoreDeprecations": "6.0"
"exclude": ["node_modules", "dist"]
}