feat: AIHR 智能人力资源管理系统初始提交
- 员工花名册管理(加密存储、导入导出) - 薪酬管理(发薪批次、薪酬模版、加班费计算、工资条) - 社保公积金(多城市配置、版本管理、基数调整) - 解聘管理(6步流程、证据链、工作交接) - AI 助手(合同审查、风险预测、RAG 知识库) - Dashboard 仪表盘 - 设置与通知
This commit is contained in:
@@ -0,0 +1,611 @@
|
||||
import prisma from '../lib/prisma'
|
||||
import { encrypt, decrypt, sha256 } 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))
|
||||
}
|
||||
|
||||
function dateToMonth(date: Date): string {
|
||||
const y = date.getFullYear()
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||
return `${y}-${m}`
|
||||
}
|
||||
|
||||
function prevMonth(month: string): string {
|
||||
const [y, m] = month.split('-').map(Number)
|
||||
const d = new Date(y, m - 2, 1)
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
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()
|
||||
const typeLabel = contract.contractType === 'FIXED' ? '固定期限' : contract.contractType === 'UNFIXED' ? '无固定期限' : ''
|
||||
|
||||
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: `${typeLabel}·已到期未续签`, riskLevel: 'high' }
|
||||
} else if (daysToExpire <= 30) {
|
||||
return { status: 'expiring', statusText: `${typeLabel}·即将到期(${daysToExpire}天)`, riskLevel: 'medium' }
|
||||
}
|
||||
return { status: 'active', statusText: `${typeLabel}·正常`, 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 org = await prisma.organization.findUnique({ where: { id: orgId } })
|
||||
if (org && org.maxEmployees > 0) {
|
||||
const activeCount = await prisma.employee.count({ where: { orgId, status: 'ACTIVE' } })
|
||||
if (activeCount >= org.maxEmployees) {
|
||||
throw { code: 'PLAN_LIMIT', message: `当前套餐人数上限为 ${org.maxEmployees} 人,已达上限,请升级套餐` }
|
||||
}
|
||||
}
|
||||
|
||||
const hireDate = new Date(data.hireDate)
|
||||
const hireMonth = dateToMonth(hireDate)
|
||||
const salaryNum = Number(data.monthlySalary) || 0
|
||||
const socialInsBase = data.socialInsBase != null ? Number(data.socialInsBase) : salaryNum
|
||||
const housingFundBase = data.housingFundBase != null ? Number(data.housingFundBase) : salaryNum
|
||||
const socialInsStartMonth = data.socialInsStartMonth || hireMonth
|
||||
const housingFundStartMonth = data.housingFundStartMonth || hireMonth
|
||||
|
||||
const employee = await prisma.employee.create({
|
||||
data: {
|
||||
orgId,
|
||||
name: data.name,
|
||||
department: data.department,
|
||||
hireDate,
|
||||
monthlySalary: encrypt(data.monthlySalary),
|
||||
gender: data.gender,
|
||||
phone: data.phone,
|
||||
idCardNumber: data.idCardNumber ? encrypt(data.idCardNumber) : null,
|
||||
idCardHash: data.idCardNumber ? sha256(data.idCardNumber) : null,
|
||||
isPregnant: data.isPregnant || false,
|
||||
isInMedicalPeriod: data.isInMedicalPeriod || false,
|
||||
isWorkInjured: data.isWorkInjured || false,
|
||||
socialInsBase,
|
||||
housingFundBase,
|
||||
socialInsStartMonth,
|
||||
housingFundStartMonth,
|
||||
createdBy: userId,
|
||||
city: data.city || '北京',
|
||||
},
|
||||
})
|
||||
|
||||
// 创建社保缴费记录
|
||||
await prisma.employeeSocialInsRecord.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: employee.id,
|
||||
startMonth: socialInsStartMonth,
|
||||
endMonth: null,
|
||||
base: socialInsBase,
|
||||
changeType: 'ONBOARDING',
|
||||
createdBy: userId,
|
||||
city: data.city || '北京',
|
||||
},
|
||||
})
|
||||
|
||||
// 创建公积金缴费记录
|
||||
await prisma.employeeHousingFundRecord.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: employee.id,
|
||||
startMonth: housingFundStartMonth,
|
||||
endMonth: null,
|
||||
base: housingFundBase,
|
||||
changeType: 'ONBOARDING',
|
||||
createdBy: userId,
|
||||
city: data.city || '北京',
|
||||
},
|
||||
})
|
||||
|
||||
// 创建初始薪资变更记录
|
||||
await prisma.salaryChangeRecord.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: employee.id,
|
||||
oldSalary: 0,
|
||||
newSalary: salaryNum,
|
||||
effectiveDate: hireDate,
|
||||
effectiveMonth: hireMonth,
|
||||
endMonth: null,
|
||||
changeType: 'ONBOARDING',
|
||||
createdBy: userId,
|
||||
},
|
||||
})
|
||||
|
||||
// 创建初始部门记录
|
||||
await prisma.employeeDepartmentRecord.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: employee.id,
|
||||
oldDepartment: '',
|
||||
newDepartment: data.department,
|
||||
effectiveMonth: hireMonth,
|
||||
endMonth: null,
|
||||
changeType: 'ONBOARDING',
|
||||
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 rehireEmployee(orgId: string, userId: string, id: string, data: any) {
|
||||
const employee = await prisma.employee.findFirst({
|
||||
where: { id, orgId },
|
||||
include: { terminations: { orderBy: { terminationDate: 'desc' }, take: 1 } },
|
||||
})
|
||||
if (!employee) {
|
||||
throw { code: 'NOT_FOUND', message: '员工不存在' }
|
||||
}
|
||||
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
const isResigned = employee.terminations.some((t) => t.terminationDate <= today)
|
||||
if (!isResigned) {
|
||||
throw { code: 'CONFLICT', message: '该员工当前在职,无需重新入职' }
|
||||
}
|
||||
|
||||
const newHireDate = new Date(data.hireDate)
|
||||
const latestTerm = employee.terminations[0]
|
||||
if (latestTerm && newHireDate <= latestTerm.terminationDate) {
|
||||
throw { code: 'VALIDATION_ERROR', message: '新入职日期必须晚于上次离职/解聘日期' }
|
||||
}
|
||||
|
||||
const newHireMonth = dateToMonth(newHireDate)
|
||||
const salaryNum = Number(decrypt(employee.monthlySalary)) || 0
|
||||
const socialInsBase = data.socialInsBase != null ? Number(data.socialInsBase) : salaryNum
|
||||
const housingFundBase = data.housingFundBase != null ? Number(data.housingFundBase) : salaryNum
|
||||
const socialInsStartMonth = data.socialInsStartMonth || newHireMonth
|
||||
const housingFundStartMonth = data.housingFundStartMonth || newHireMonth
|
||||
const prevHireMonth = prevMonth(newHireMonth)
|
||||
|
||||
// 关闭旧社保缴费记录
|
||||
await prisma.employeeSocialInsRecord.updateMany({
|
||||
where: { employeeId: id, endMonth: null },
|
||||
data: { endMonth: prevHireMonth },
|
||||
})
|
||||
|
||||
// 关闭旧公积金缴费记录
|
||||
await prisma.employeeHousingFundRecord.updateMany({
|
||||
where: { employeeId: id, endMonth: null },
|
||||
data: { endMonth: prevHireMonth },
|
||||
})
|
||||
|
||||
// 关闭旧薪资记录
|
||||
await prisma.salaryChangeRecord.updateMany({
|
||||
where: { employeeId: id, endMonth: null },
|
||||
data: { endMonth: prevHireMonth },
|
||||
})
|
||||
|
||||
// 关闭旧部门记录
|
||||
await prisma.employeeDepartmentRecord.updateMany({
|
||||
where: { employeeId: id, endMonth: null },
|
||||
data: { endMonth: prevHireMonth },
|
||||
})
|
||||
|
||||
await prisma.employee.update({
|
||||
where: { id },
|
||||
data: {
|
||||
hireDate: newHireDate,
|
||||
status: 'ACTIVE',
|
||||
department: data.department || employee.department,
|
||||
isPregnant: false,
|
||||
isInMedicalPeriod: false,
|
||||
isWorkInjured: false,
|
||||
socialInsBase,
|
||||
housingFundBase,
|
||||
socialInsStartMonth,
|
||||
socialInsEndMonth: null,
|
||||
housingFundStartMonth,
|
||||
housingFundEndMonth: null,
|
||||
city: data.city || employee.city || '北京',
|
||||
},
|
||||
})
|
||||
|
||||
// 创建新社保缴费记录
|
||||
await prisma.employeeSocialInsRecord.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: id,
|
||||
startMonth: socialInsStartMonth,
|
||||
endMonth: null,
|
||||
base: socialInsBase,
|
||||
changeType: 'REHIRE',
|
||||
createdBy: userId,
|
||||
city: data.city || employee.city || '北京',
|
||||
},
|
||||
})
|
||||
|
||||
// 创建新公积金缴费记录
|
||||
await prisma.employeeHousingFundRecord.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: id,
|
||||
startMonth: housingFundStartMonth,
|
||||
endMonth: null,
|
||||
base: housingFundBase,
|
||||
changeType: 'REHIRE',
|
||||
createdBy: userId,
|
||||
city: data.city || employee.city || '北京',
|
||||
},
|
||||
})
|
||||
|
||||
// 创建新薪资记录
|
||||
await prisma.salaryChangeRecord.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: id,
|
||||
oldSalary: salaryNum,
|
||||
newSalary: salaryNum,
|
||||
effectiveDate: newHireDate,
|
||||
effectiveMonth: newHireMonth,
|
||||
endMonth: null,
|
||||
changeType: 'REHIRE',
|
||||
createdBy: userId,
|
||||
},
|
||||
})
|
||||
|
||||
// 创建新部门记录
|
||||
await prisma.employeeDepartmentRecord.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: id,
|
||||
oldDepartment: employee.department,
|
||||
newDepartment: data.department || employee.department,
|
||||
effectiveMonth: newHireMonth,
|
||||
endMonth: null,
|
||||
changeType: 'REHIRE',
|
||||
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: 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 }
|
||||
}
|
||||
|
||||
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) {
|
||||
const oldSalary = Number(decrypt(employee.monthlySalary)) || 0
|
||||
const newSalary = Number(data.monthlySalary) || 0
|
||||
updateData.monthlySalary = encrypt(data.monthlySalary)
|
||||
// 记录薪资变更
|
||||
if (oldSalary !== newSalary) {
|
||||
const now = new Date()
|
||||
const nowMonth = dateToMonth(now)
|
||||
// 关闭之前有效记录
|
||||
await prisma.salaryChangeRecord.updateMany({
|
||||
where: { employeeId: id, endMonth: null },
|
||||
data: { endMonth: prevMonth(nowMonth) },
|
||||
})
|
||||
await prisma.salaryChangeRecord.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: id,
|
||||
oldSalary,
|
||||
newSalary,
|
||||
effectiveDate: now,
|
||||
effectiveMonth: nowMonth,
|
||||
endMonth: null,
|
||||
changeType: 'SALARY_CHANGE',
|
||||
reason: data.salaryChangeReason || '手动调整',
|
||||
createdBy: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
if (data.gender !== undefined) updateData.gender = data.gender
|
||||
if (data.phone !== undefined) updateData.phone = data.phone
|
||||
if (data.bankName !== undefined) updateData.bankName = data.bankName
|
||||
if (data.bankAccount !== undefined) updateData.bankAccount = encrypt(data.bankAccount)
|
||||
if (data.emergencyContact !== undefined) updateData.emergencyContact = data.emergencyContact
|
||||
if (data.emergencyPhone !== undefined) updateData.emergencyPhone = data.emergencyPhone
|
||||
if (data.address !== undefined) updateData.address = data.address
|
||||
if (data.isPregnant !== undefined) updateData.isPregnant = data.isPregnant
|
||||
if (data.isInMedicalPeriod !== undefined) updateData.isInMedicalPeriod = data.isInMedicalPeriod
|
||||
if (data.isWorkInjured !== undefined) updateData.isWorkInjured = data.isWorkInjured
|
||||
if (data.socialInsBase !== undefined) updateData.socialInsBase = data.socialInsBase
|
||||
if (data.housingFundBase !== undefined) updateData.housingFundBase = data.housingFundBase
|
||||
if (data.specialDeduction !== undefined) updateData.specialDeduction = data.specialDeduction
|
||||
if (data.city !== undefined) updateData.city = data.city
|
||||
|
||||
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,
|
||||
attachmentName: data.attachmentUrl ? '合同扫描件' : null,
|
||||
attachmentUrl: data.attachmentUrl || null,
|
||||
electronicContractNo: data.electronicContractNo || null,
|
||||
electronicContractUrl: data.electronicContractUrl || null,
|
||||
createdBy: userId,
|
||||
},
|
||||
})
|
||||
|
||||
await runRiskDetection(orgId)
|
||||
|
||||
return { id: contract.id }
|
||||
}
|
||||
Reference in New Issue
Block a user