init: AI HR Compliance Assistant

This commit is contained in:
freedakgmail
2026-07-23 12:34:43 +08:00
commit 820579e98d
81 changed files with 19327 additions and 0 deletions
+314
View File
@@ -0,0 +1,314 @@
import prisma from '../lib/prisma'
import { encrypt, decrypt } from '../lib/crypto'
import { runRiskDetection } from './risk.service'
function daysBetween(a: Date, b: Date): number {
return Math.floor((a.getTime() - b.getTime()) / (1000 * 60 * 60 * 24))
}
export function getContractStatus(contract: {
signDate: Date | null
startDate: Date
endDate: Date | null
contractType: string
hireDate: Date
}): { status: string; statusText: string; riskLevel: 'high' | 'medium' | 'low' | 'safe' } {
const today = new Date()
if (!contract.signDate || contract.contractType === 'UNSIGNED') {
const days = daysBetween(today, contract.hireDate)
if (days > 365) {
return { status: 'unsigned_over_year', statusText: '已视为无固定期限', riskLevel: 'high' }
} else if (days > 30) {
return { status: 'unsigned_over_30', statusText: `未签合同(${days}天)`, riskLevel: 'high' }
}
return { status: 'unsigned', statusText: `未签合同(${days}天)`, riskLevel: 'medium' }
}
if (contract.endDate) {
const daysToExpire = daysBetween(contract.endDate, today)
if (daysToExpire < 0) {
return { status: 'expired', statusText: '已到期未续签', riskLevel: 'high' }
} else if (daysToExpire <= 30) {
return { status: 'expiring', statusText: `即将到期(${daysToExpire}天)`, riskLevel: 'medium' }
}
return { status: 'active', statusText: '正常', riskLevel: 'safe' }
}
return { status: 'unfixed', statusText: '无固定期限', riskLevel: 'safe' }
}
export function validateProbation(contractMonths: number, probationMonths: number): { valid: boolean; max: number; message?: string } {
let max = 0
if (contractMonths >= 36) max = 6
else if (contractMonths >= 12) max = 2
else if (contractMonths >= 3) max = 1
if (probationMonths > max) {
return {
valid: false,
max,
message: `${contractMonths}个月合同试用期最多${max}个月,当前${probationMonths}个月不合法`,
}
}
return { valid: true, max }
}
export async function getEmployees(orgId: string, params: { page?: number; pageSize?: number; search?: string; department?: string }) {
const page = params.page || 1
const pageSize = params.pageSize || 20
const skip = (page - 1) * pageSize
const where: any = { orgId, status: 'ACTIVE' }
if (params.search) {
where.OR = [
{ name: { contains: params.search } },
{ phone: { contains: params.search } },
]
}
if (params.department) {
where.department = params.department
}
const [total, employees] = await Promise.all([
prisma.employee.count({ where }),
prisma.employee.findMany({
where,
include: {
contracts: { orderBy: { createdAt: 'desc' }, take: 1 },
},
orderBy: { createdAt: 'desc' },
skip,
take: pageSize,
}),
])
const items = employees.map((emp) => {
const latestContract = emp.contracts[0]
const contractInfo = latestContract
? getContractStatus({
signDate: latestContract.signDate,
startDate: latestContract.startDate,
endDate: latestContract.endDate,
contractType: latestContract.contractType,
hireDate: emp.hireDate,
})
: getContractStatus({
signDate: null,
startDate: emp.hireDate,
endDate: null,
contractType: 'UNSIGNED',
hireDate: emp.hireDate,
})
let decryptedSalary = 0
try {
decryptedSalary = Number(decrypt(emp.monthlySalary)) || 0
} catch {
decryptedSalary = Number(emp.monthlySalary) || 0
}
return {
id: emp.id,
name: emp.name,
department: emp.department,
hireDate: emp.hireDate.toISOString().slice(0, 10),
status: emp.status,
monthlySalary: decryptedSalary,
contractStatus: contractInfo.status,
contractStatusText: contractInfo.statusText,
riskLevel: contractInfo.riskLevel,
isPregnant: emp.isPregnant,
isInMedicalPeriod: emp.isInMedicalPeriod,
isWorkInjured: emp.isWorkInjured,
}
})
return { items, total, page, pageSize, totalPages: Math.ceil(total / pageSize) }
}
export async function getEmployeeDetail(orgId: string, id: string) {
const employee = await prisma.employee.findFirst({
where: { id, orgId },
include: {
contracts: { orderBy: { createdAt: 'desc' } },
riskItems: { where: { status: 'PENDING' }, orderBy: { level: 'asc' } },
},
})
if (!employee) {
throw { code: 'NOT_FOUND', message: '员工不存在' }
}
let decryptedSalary = 0
try {
decryptedSalary = Number(decrypt(employee.monthlySalary)) || 0
} catch {
decryptedSalary = Number(employee.monthlySalary) || 0
}
return {
...employee,
monthlySalary: decryptedSalary,
}
}
export async function createEmployee(orgId: string, userId: string, data: any) {
const employee = await prisma.employee.create({
data: {
orgId,
name: data.name,
department: data.department,
hireDate: new Date(data.hireDate),
monthlySalary: encrypt(data.monthlySalary),
gender: data.gender,
phone: data.phone,
isPregnant: data.isPregnant || false,
isInMedicalPeriod: data.isInMedicalPeriod || false,
isWorkInjured: data.isWorkInjured || false,
createdBy: userId,
},
})
if (data.contract && data.contract.contractType !== 'UNSIGNED') {
const contractMonths = data.contract.endDate
? Math.ceil(daysBetween(new Date(data.contract.endDate), new Date(data.contract.startDate)) / 30.44)
: data.contract.contractYears * 12
const probationCheck = validateProbation(contractMonths, data.contract.probationMonths)
if (!probationCheck.valid) {
throw { code: 'VALIDATION_ERROR', message: probationCheck.message }
}
await prisma.laborContract.create({
data: {
orgId,
employeeId: employee.id,
signDate: data.contract.signDate ? new Date(data.contract.signDate) : null,
startDate: new Date(data.contract.startDate),
endDate: data.contract.endDate ? new Date(data.contract.endDate) : null,
contractType: data.contract.contractType,
signMethod: data.contract.signMethod || 'PAPER',
contractYears: data.contract.contractYears || 3,
probationMonths: data.contract.probationMonths || 0,
probationSalary: data.contract.probationSalary || 0,
createdBy: userId,
},
})
}
await runRiskDetection(orgId)
return { id: employee.id }
}
export async function updateEmployee(orgId: string, id: string, data: any) {
const employee = await prisma.employee.findFirst({ where: { id, orgId } })
if (!employee) {
throw { code: 'NOT_FOUND', message: '员工不存在' }
}
const updateData: any = {}
if (data.name !== undefined) updateData.name = data.name
if (data.department !== undefined) updateData.department = data.department
if (data.hireDate !== undefined) updateData.hireDate = new Date(data.hireDate)
if (data.monthlySalary !== undefined) updateData.monthlySalary = encrypt(data.monthlySalary)
if (data.gender !== undefined) updateData.gender = data.gender
if (data.phone !== undefined) updateData.phone = data.phone
if (data.isPregnant !== undefined) updateData.isPregnant = data.isPregnant
if (data.isInMedicalPeriod !== undefined) updateData.isInMedicalPeriod = data.isInMedicalPeriod
if (data.isWorkInjured !== undefined) updateData.isWorkInjured = data.isWorkInjured
await prisma.employee.update({ where: { id }, data: updateData })
await runRiskDetection(orgId)
return { id }
}
export async function deleteEmployee(orgId: string, id: string) {
const employee = await prisma.employee.findFirst({ where: { id, orgId } })
if (!employee) {
throw { code: 'NOT_FOUND', message: '员工不存在' }
}
await prisma.employee.update({ where: { id }, data: { status: 'RESIGNED' } })
await prisma.riskItem.updateMany({
where: { employeeId: id, status: 'PENDING' },
data: { status: 'RESOLVED', resolvedAt: new Date() },
})
return { id }
}
export async function batchRenew(orgId: string, userId: string, contractIds: string[], years: number) {
const contracts = await prisma.laborContract.findMany({
where: { id: { in: contractIds }, orgId },
})
if (contracts.length === 0) {
throw { code: 'NOT_FOUND', message: '未找到符合条件的合同' }
}
for (const contract of contracts) {
const newStartDate = contract.endDate || new Date()
const newEndDate = new Date(newStartDate)
newEndDate.setFullYear(newEndDate.getFullYear() + years)
await prisma.laborContract.create({
data: {
orgId,
employeeId: contract.employeeId,
signDate: new Date(),
startDate: newStartDate,
endDate: newEndDate,
contractType: contract.contractType,
signMethod: contract.signMethod,
contractYears: years,
probationMonths: 0,
probationSalary: 0,
renewalCount: contract.renewalCount + 1,
createdBy: userId,
},
})
}
await runRiskDetection(orgId)
return { renewed: contracts.length }
}
export async function addContract(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 contractMonths = data.endDate
? Math.ceil(daysBetween(new Date(data.endDate), new Date(data.startDate)) / 30.44)
: data.contractYears * 12
const probationCheck = validateProbation(contractMonths, data.probationMonths)
if (!probationCheck.valid) {
throw { code: 'VALIDATION_ERROR', message: probationCheck.message }
}
const contract = await prisma.laborContract.create({
data: {
orgId,
employeeId: data.employeeId,
signDate: data.signDate ? new Date(data.signDate) : null,
startDate: new Date(data.startDate),
endDate: data.endDate ? new Date(data.endDate) : null,
contractType: data.contractType,
signMethod: data.signMethod || 'PAPER',
contractYears: data.contractYears || 3,
probationMonths: data.probationMonths || 0,
probationSalary: data.probationSalary || 0,
createdBy: userId,
},
})
await runRiskDetection(orgId)
return { id: contract.id }
}