164 lines
5.2 KiB
TypeScript
164 lines
5.2 KiB
TypeScript
import prisma from '../lib/prisma'
|
|
import { decrypt } from '../lib/crypto'
|
|
import { RiskAssessment } from '@prisma/client'
|
|
|
|
function daysBetween(a: Date, b: Date): number {
|
|
return Math.floor((a.getTime() - b.getTime()) / (1000 * 60 * 60 * 24))
|
|
}
|
|
|
|
export function getChecklistForReason(reason: string): { key: string; label: string }[] {
|
|
switch (reason) {
|
|
case 'NEGOTIATED':
|
|
return [
|
|
{ key: 'compensation_paid', label: '是否已支付经济补偿金' },
|
|
{ key: 'agreement_signed', label: '是否签署协商解除协议' },
|
|
{ key: 'final_pay_ready', label: '是否结清最后工资' },
|
|
]
|
|
case 'FAULT':
|
|
return [
|
|
{ key: 'has_rules', label: '是否有规章制度依据' },
|
|
{ key: 'has_evidence', label: '是否有违纪证据' },
|
|
{ key: 'notify_union', label: '是否事先通知工会' },
|
|
{ key: 'written_notice', label: '是否出具书面解除通知' },
|
|
]
|
|
case 'NONFAULT':
|
|
return [
|
|
{ key: 'medical_period_end', label: '医疗期是否已届满' },
|
|
{ key: 'training_given', label: '是否经过培训或调岗' },
|
|
{ key: 'compensation_paid', label: '是否支付经济补偿金' },
|
|
{ key: 'advance_notice', label: '是否提前30天通知或支付代通知金' },
|
|
]
|
|
case 'LAYOFF':
|
|
return [
|
|
{ key: 'advance_notice_30', label: '是否提前30天向工会或全体职工说明' },
|
|
{ key: 'listen_opinions', label: '是否听取工会或职工意见' },
|
|
{ key: 'report_labor_dept', label: '是否向劳动行政部门报告' },
|
|
{ key: 'compensation_paid', label: '是否支付经济补偿金' },
|
|
]
|
|
case 'EXPIRED':
|
|
return [
|
|
{ key: 'compensation_paid', label: '是否支付经济补偿金(如需)' },
|
|
{ key: 'written_notice', label: '是否提前通知员工不续签' },
|
|
]
|
|
default:
|
|
return []
|
|
}
|
|
}
|
|
|
|
export function assessRisk(employee: any, reason: string): { level: RiskAssessment; warnings: string[] } {
|
|
const warnings: string[] = []
|
|
|
|
if (employee.isPregnant) {
|
|
warnings.push('该员工在孕期/哺乳期,法律禁止以非过错理由解除')
|
|
}
|
|
if (employee.isWorkInjured) {
|
|
warnings.push('工伤期间不得解除劳动合同')
|
|
}
|
|
if (employee.isInMedicalPeriod && reason !== 'FAULT') {
|
|
warnings.push('医疗期内不得解除劳动合同(非过错理由)')
|
|
}
|
|
|
|
let level: RiskAssessment = 'SAFE'
|
|
if (warnings.length > 0) {
|
|
level = 'DANGER'
|
|
}
|
|
|
|
return { level, warnings }
|
|
}
|
|
|
|
export async function createTermination(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 { level } = assessRisk(employee, data.reason)
|
|
|
|
const record = await prisma.terminationRecord.create({
|
|
data: {
|
|
orgId,
|
|
employeeId: data.employeeId,
|
|
reason: data.reason,
|
|
terminationDate: new Date(data.terminationDate),
|
|
compensation: data.compensation || 0,
|
|
riskLevel: level,
|
|
checklist: data.checklist || {},
|
|
remark: data.remark,
|
|
createdBy: userId,
|
|
},
|
|
})
|
|
|
|
await prisma.employee.update({
|
|
where: { id: data.employeeId },
|
|
data: { status: 'RESIGNED' },
|
|
})
|
|
|
|
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
|
|
|
|
const [total, records] = await Promise.all([
|
|
prisma.terminationRecord.count({ where: { orgId } }),
|
|
prisma.terminationRecord.findMany({
|
|
where: { orgId },
|
|
include: { employee: true },
|
|
orderBy: { createdAt: 'desc' },
|
|
skip,
|
|
take: pageSize,
|
|
}),
|
|
])
|
|
|
|
return {
|
|
items: records.map((r) => ({
|
|
id: r.id,
|
|
employeeName: r.employee.name,
|
|
department: r.employee.department,
|
|
reason: r.reason,
|
|
terminationDate: r.terminationDate.toISOString().slice(0, 10),
|
|
compensation: r.compensation,
|
|
riskLevel: r.riskLevel,
|
|
createdAt: r.createdAt.toISOString().slice(0, 10),
|
|
})),
|
|
total,
|
|
page,
|
|
pageSize,
|
|
totalPages: Math.ceil(total / pageSize),
|
|
}
|
|
}
|
|
|
|
export function calculateCompensation(hireDate: Date, leaveDate: Date, monthlyWage: number, socialAvgWage: number = 0): {
|
|
years: number
|
|
remainingMonths: number
|
|
compMonths: number
|
|
totalPay: number
|
|
capped: boolean
|
|
} {
|
|
const totalMonths = (leaveDate.getFullYear() - hireDate.getFullYear()) * 12 + (leaveDate.getMonth() - hireDate.getMonth())
|
|
const years = Math.floor(totalMonths / 12)
|
|
const remainingMonths = totalMonths % 12
|
|
|
|
let compMonths: number
|
|
if (remainingMonths >= 6) compMonths = years + 1
|
|
else if (remainingMonths > 0) compMonths = years + 0.5
|
|
else compMonths = years
|
|
|
|
if (compMonths <= 0) compMonths = 0.5
|
|
|
|
let wage = monthlyWage
|
|
let capped = false
|
|
if (socialAvgWage > 0 && monthlyWage > socialAvgWage * 3) {
|
|
wage = socialAvgWage * 3
|
|
compMonths = Math.min(compMonths, 12)
|
|
capped = true
|
|
}
|
|
|
|
return { years, remainingMonths, compMonths, totalPay: wage * compMonths, capped }
|
|
}
|