init: AI HR Compliance Assistant
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
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 }
|
||||
}
|
||||
Reference in New Issue
Block a user