feat: 离职/解聘完整功能
1. 已有离职/解聘记录的员工不能再次解聘(后端校验+前端提示) 2. 员工主动离职功能:花名册列表增加「离职」操作按钮和弹窗 3. 离职也按离职日期动态判断在职/离职状态,支持提前办理 4. 员工档案「解聘记录」tab改为「离职/解聘记录」,区分类型显示 5. Schema: TerminationRecord 增加 type、resignationReason 字段 6. 后端新增 POST /termination/resignation 接口 7. 花名册列表返回 hasTermination 标记
This commit is contained in:
@@ -83,6 +83,7 @@ enum TerminationReason {
|
||||
NONFAULT
|
||||
LAYOFF
|
||||
EXPIRED
|
||||
RESIGNATION
|
||||
}
|
||||
|
||||
enum RiskAssessment {
|
||||
@@ -249,8 +250,10 @@ model TerminationRecord {
|
||||
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
||||
employeeId String
|
||||
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
|
||||
type String @default("TERMINATION") // TERMINATION=公司解聘, RESIGNATION=员工主动离职
|
||||
reason TerminationReason
|
||||
terminationDate DateTime
|
||||
resignationReason String? // 主动离职原因(type=RESIGNATION时使用)
|
||||
compensation Float @default(0)
|
||||
riskLevel RiskAssessment @default(SAFE)
|
||||
checklist Json
|
||||
|
||||
@@ -63,6 +63,8 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
name: e.name,
|
||||
department: e.department,
|
||||
status: e.terminations.some((t) => t.terminationDate <= today) ? 'RESIGNED' : 'ACTIVE',
|
||||
hasTermination: e.terminations.length > 0,
|
||||
latestTerminationDate: e.terminations[0]?.terminationDate || null,
|
||||
hireDate: e.hireDate,
|
||||
gender: e.gender,
|
||||
phone: e.phone,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Router } from 'express'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { auditLog } from '../middleware/auditLog'
|
||||
import { terminationChecklistSchema } from '../schemas/termination.schema'
|
||||
import { createTermination, getTerminations, getChecklistForReason, assessRisk, calculateCompensation } from '../services/termination.service'
|
||||
import { createTermination, createResignation, getTerminations, getChecklistForReason, assessRisk, calculateCompensation } from '../services/termination.service'
|
||||
import prisma from '../lib/prisma'
|
||||
import { decrypt } from '../lib/crypto'
|
||||
|
||||
@@ -70,4 +70,21 @@ router.post('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/resignation', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { employeeId, terminationDate, resignationReason, remark } = req.body
|
||||
if (!employeeId || !terminationDate) {
|
||||
return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: '缺少必填字段' } })
|
||||
}
|
||||
const result = await createResignation(req.user!.orgId, req.user!.id, { employeeId, terminationDate, resignationReason, remark })
|
||||
await auditLog(req, 'RESIGN', 'EMPLOYEE', employeeId, { resignationReason })
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err: any) {
|
||||
if (err?.code === 'CONFLICT') {
|
||||
return res.status(409).json({ success: false, error: { code: err.code, message: err.message } })
|
||||
}
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -141,12 +141,22 @@ export async function createTermination(orgId: string, userId: string, data: any
|
||||
throw { code: 'NOT_FOUND', message: '员工不存在' }
|
||||
}
|
||||
|
||||
// 校验:已有离职/解聘记录且未重新雇佣则不允许再次解聘
|
||||
const latestTerm = await prisma.terminationRecord.findFirst({
|
||||
where: { employeeId: data.employeeId },
|
||||
orderBy: { terminationDate: 'desc' },
|
||||
})
|
||||
if (latestTerm && latestTerm.terminationDate >= employee.hireDate) {
|
||||
throw { code: 'CONFLICT', message: '该员工已有离职/解聘记录,如需再次解聘请先办理重新雇佣' }
|
||||
}
|
||||
|
||||
const { level } = assessRisk(employee, data.reason)
|
||||
|
||||
const record = await prisma.terminationRecord.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: data.employeeId,
|
||||
type: 'TERMINATION',
|
||||
reason: data.reason,
|
||||
terminationDate: new Date(data.terminationDate),
|
||||
compensation: data.compensation || 0,
|
||||
@@ -176,6 +186,57 @@ export async function createTermination(orgId: string, userId: string, data: any
|
||||
return { id: record.id }
|
||||
}
|
||||
|
||||
// 员工主动离职
|
||||
export async function createResignation(orgId: string, userId: string, data: any) {
|
||||
const employee = await prisma.employee.findFirst({ where: { id: data.employeeId, orgId } })
|
||||
if (!employee) {
|
||||
throw { code: 'NOT_FOUND', message: '员工不存在' }
|
||||
}
|
||||
|
||||
// 校验:已有离职/解聘记录且未重新雇佣则不允许再次离职
|
||||
const latestTerm = await prisma.terminationRecord.findFirst({
|
||||
where: { employeeId: data.employeeId },
|
||||
orderBy: { terminationDate: 'desc' },
|
||||
})
|
||||
if (latestTerm && latestTerm.terminationDate >= employee.hireDate) {
|
||||
throw { code: 'CONFLICT', message: '该员工已有离职/解聘记录,如需再次办理请先重新雇佣' }
|
||||
}
|
||||
|
||||
const record = await prisma.terminationRecord.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: data.employeeId,
|
||||
type: 'RESIGNATION',
|
||||
reason: 'RESIGNATION',
|
||||
terminationDate: new Date(data.terminationDate),
|
||||
resignationReason: data.resignationReason || null,
|
||||
compensation: 0,
|
||||
riskLevel: 'SAFE',
|
||||
checklist: {},
|
||||
remark: data.remark || null,
|
||||
createdBy: userId,
|
||||
},
|
||||
})
|
||||
|
||||
// 根据离职日期判断在职/离职状态
|
||||
const termDate = new Date(data.terminationDate)
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
const isResigned = termDate <= today
|
||||
|
||||
await prisma.employee.update({
|
||||
where: { id: data.employeeId },
|
||||
data: { status: isResigned ? 'RESIGNED' : 'ACTIVE' },
|
||||
})
|
||||
|
||||
await prisma.riskItem.updateMany({
|
||||
where: { employeeId: data.employeeId, status: 'PENDING' },
|
||||
data: { status: 'RESOLVED', resolvedAt: new Date() },
|
||||
})
|
||||
|
||||
return { id: record.id }
|
||||
}
|
||||
|
||||
export async function getTerminations(orgId: string, page: number, pageSize: number) {
|
||||
const skip = (page - 1) * pageSize
|
||||
|
||||
@@ -195,10 +256,13 @@ export async function getTerminations(orgId: string, page: number, pageSize: num
|
||||
id: r.id,
|
||||
employeeName: r.employee.name,
|
||||
department: r.employee.department,
|
||||
type: r.type,
|
||||
reason: r.reason,
|
||||
resignationReason: r.resignationReason,
|
||||
terminationDate: r.terminationDate.toISOString().slice(0, 10),
|
||||
compensation: r.compensation,
|
||||
riskLevel: r.riskLevel,
|
||||
remark: r.remark,
|
||||
createdAt: r.createdAt.toISOString().slice(0, 10),
|
||||
})),
|
||||
total,
|
||||
|
||||
Reference in New Issue
Block a user