feat: 实现20260730优化方案全部功能
- AI文件审查:.docx上传提取文本,支持多种文档类型 - 用工办理工作流:WorkProcess页面+后端API,支持入职/续签/终止等流程 - 企业自建文本库:Templates页面Tab切换,企业模板CRUD+渲染+下载Word - 考勤发布:Attendance发布/取消发布按钮,员工端MyAttendance页面 - 工资条发布:Money发布/定时发送按钮+弹窗,portal端publishStatus过滤 - 合同到期弹窗:Dashboard合同到期预警可点击打开弹窗,支持续签/终止操作 - Prisma schema新增WorkProcess/EnterpriseTemplate/AttendancePublish模型 - 前后端编译验证全部通过
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
import prisma from '../lib/prisma'
|
||||
import { encrypt } from '../lib/crypto'
|
||||
import crypto from 'crypto'
|
||||
|
||||
// 13类流程定义
|
||||
export const PROCESS_TYPES: Record<string, { label: string; description: string; icon: string }> = {
|
||||
HIRE: { label: '员工录用', description: '录用新员工并起草劳动合同', icon: 'user-plus' },
|
||||
ONBOARD: { label: '员工入职', description: '办理员工入职手续', icon: 'log-in' },
|
||||
CUSTOM_CONTRACT: { label: '自定义合同签署', description: '自定义合同内容并签署', icon: 'file-signature' },
|
||||
INFO_SUBMIT: { label: '员工信息提交', description: '提交员工信息变更', icon: 'edit' },
|
||||
CONFIRM: { label: '员工转正', description: '试用期员工转正', icon: 'check-circle' },
|
||||
CHANGE: { label: '合同变更', description: '变更合同内容', icon: 'refresh-cw' },
|
||||
RENEW: { label: '合同续签', description: '到期合同续签', icon: 'repeat' },
|
||||
SUSPEND: { label: '合同中止', description: '中止履行合同', icon: 'pause' },
|
||||
INCOME_CERT: { label: '开具收入证明', description: '为员工开具收入证明', icon: 'file-text' },
|
||||
TERMINATE: { label: '合同终止', description: '合同到期终止', icon: 'x-circle' },
|
||||
RESCIND: { label: '合同解除', description: '协商或单方解除合同', icon: 'user-x' },
|
||||
LEAVING_CERT: { label: '开具离职证明', description: '为离职员工开具证明', icon: 'file-minus' },
|
||||
FLEXIBLE: { label: '灵活用工', description: '灵活用工协议签署', icon: 'briefcase' },
|
||||
}
|
||||
|
||||
export const PROCESS_STATUS: Record<string, { label: string; color: string }> = {
|
||||
DRAFT: { label: '草稿', color: 'bg-gray-100 text-gray-600' },
|
||||
PENDING_APPROVAL: { label: '待审批', color: 'bg-amber-50 text-amber-700' },
|
||||
APPROVED: { label: '已审批', color: 'bg-blue-50 text-blue-700' },
|
||||
REJECTED: { label: '已驳回', color: 'bg-red-50 text-red-700' },
|
||||
EXECUTING: { label: '执行中', color: 'bg-purple-50 text-purple-700' },
|
||||
COMPLETED: { label: '已完成', color: 'bg-green-50 text-safe' },
|
||||
CANCELLED: { label: '已撤销', color: 'bg-gray-100 text-gray-400' },
|
||||
}
|
||||
|
||||
// 提交后业务联动
|
||||
export async function executeWorkProcess(processId: string, type: string, formData: any, orgId: string, userId: string) {
|
||||
switch (type) {
|
||||
case 'HIRE': {
|
||||
// 创建员工 + 合同
|
||||
const { name, department, hireDate, monthlySalary, phone, idCardNumber, gender, contractStartDate, contractEndDate, contractType = 'FIXED' } = formData
|
||||
const idCardHash = idCardNumber ? crypto.createHash('sha256').update(idCardNumber).digest('hex') : null
|
||||
const employee = await prisma.employee.create({
|
||||
data: {
|
||||
orgId,
|
||||
name,
|
||||
department: department || '未分配',
|
||||
hireDate: new Date(hireDate),
|
||||
monthlySalary: encrypt(String(monthlySalary || 0)),
|
||||
phone: phone || null,
|
||||
idCardNumber: idCardNumber ? encrypt(idCardNumber) : null,
|
||||
idCardHash,
|
||||
gender: gender || null,
|
||||
createdBy: userId,
|
||||
},
|
||||
})
|
||||
if (contractStartDate) {
|
||||
await prisma.laborContract.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: employee.id,
|
||||
startDate: new Date(contractStartDate),
|
||||
endDate: contractEndDate ? new Date(contractEndDate) : null,
|
||||
contractType: contractType as any,
|
||||
signMethod: 'PAPER',
|
||||
contractYears: 3,
|
||||
createdBy: userId,
|
||||
},
|
||||
})
|
||||
}
|
||||
return { employeeId: employee.id }
|
||||
}
|
||||
case 'ONBOARD': {
|
||||
const { employeeId, hireDate } = formData
|
||||
if (employeeId) {
|
||||
await prisma.employee.update({
|
||||
where: { id: employeeId },
|
||||
data: { hireDate: new Date(hireDate), status: 'ACTIVE' },
|
||||
})
|
||||
}
|
||||
return { employeeId }
|
||||
}
|
||||
case 'CONFIRM': {
|
||||
const { employeeId, confirmDate, regularSalary } = formData
|
||||
if (employeeId) {
|
||||
if (regularSalary) {
|
||||
await prisma.employee.update({
|
||||
where: { id: employeeId },
|
||||
data: { monthlySalary: encrypt(String(regularSalary)) },
|
||||
})
|
||||
}
|
||||
}
|
||||
return { employeeId }
|
||||
}
|
||||
case 'RENEW': {
|
||||
const { employeeId, oldContractId, newStartDate, newEndDate, newSalary, contractType = 'FIXED', contractYears = 3 } = formData
|
||||
if (oldContractId) {
|
||||
const oldEndDate = new Date(newStartDate)
|
||||
oldEndDate.setDate(oldEndDate.getDate() - 1)
|
||||
await prisma.laborContract.update({
|
||||
where: { id: oldContractId },
|
||||
data: { endDate: oldEndDate },
|
||||
})
|
||||
}
|
||||
if (employeeId) {
|
||||
const oldContract = oldContractId ? await prisma.laborContract.findUnique({ where: { id: oldContractId } }) : null
|
||||
const contract = await prisma.laborContract.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId,
|
||||
signDate: new Date(),
|
||||
startDate: new Date(newStartDate),
|
||||
endDate: newEndDate ? new Date(newEndDate) : null,
|
||||
contractType: contractType as any,
|
||||
signMethod: 'PAPER',
|
||||
contractYears: Number(contractYears) || 3,
|
||||
renewalCount: (oldContract?.renewalCount || 0) + 1,
|
||||
createdBy: userId,
|
||||
},
|
||||
})
|
||||
if (newSalary) {
|
||||
await prisma.employee.update({
|
||||
where: { id: employeeId },
|
||||
data: { monthlySalary: encrypt(String(newSalary)) },
|
||||
})
|
||||
}
|
||||
return { employeeId, newContractId: contract.id }
|
||||
}
|
||||
return { employeeId }
|
||||
}
|
||||
case 'TERMINATE': {
|
||||
const { employeeId, contractId, terminateDate } = formData
|
||||
if (contractId) {
|
||||
await prisma.laborContract.update({
|
||||
where: { id: contractId },
|
||||
data: { endDate: new Date(terminateDate) },
|
||||
})
|
||||
}
|
||||
if (employeeId) {
|
||||
await prisma.employee.update({
|
||||
where: { id: employeeId },
|
||||
data: { status: 'RESIGNED' },
|
||||
})
|
||||
}
|
||||
return { employeeId }
|
||||
}
|
||||
case 'RESCIND': {
|
||||
const { employeeId, contractId, rescindDate } = formData
|
||||
if (contractId) {
|
||||
await prisma.laborContract.update({
|
||||
where: { id: contractId },
|
||||
data: { endDate: new Date(rescindDate) },
|
||||
})
|
||||
}
|
||||
if (employeeId) {
|
||||
await prisma.employee.update({
|
||||
where: { id: employeeId },
|
||||
data: { status: 'RESIGNED' },
|
||||
})
|
||||
}
|
||||
return { employeeId }
|
||||
}
|
||||
case 'CHANGE': {
|
||||
const { contractId, newEndDate } = formData
|
||||
if (contractId && newEndDate) {
|
||||
await prisma.laborContract.update({
|
||||
where: { id: contractId },
|
||||
data: { endDate: new Date(newEndDate) },
|
||||
})
|
||||
}
|
||||
return { contractId }
|
||||
}
|
||||
case 'SUSPEND': {
|
||||
const { contractId, suspendDate } = formData
|
||||
if (contractId && suspendDate) {
|
||||
await prisma.laborContract.update({
|
||||
where: { id: contractId },
|
||||
data: { endDate: new Date(suspendDate) },
|
||||
})
|
||||
}
|
||||
return { contractId }
|
||||
}
|
||||
case 'CUSTOM_CONTRACT': {
|
||||
const { employeeId, contractStartDate, contractEndDate, contractType = 'FIXED', signMethod = 'PAPER', contractYears = 3 } = formData
|
||||
if (employeeId) {
|
||||
const contract = await prisma.laborContract.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId,
|
||||
signDate: new Date(),
|
||||
startDate: new Date(contractStartDate),
|
||||
endDate: contractEndDate ? new Date(contractEndDate) : null,
|
||||
contractType: contractType as any,
|
||||
signMethod: signMethod as any,
|
||||
contractYears: Number(contractYears) || 3,
|
||||
createdBy: userId,
|
||||
},
|
||||
})
|
||||
return { employeeId, newContractId: contract.id }
|
||||
}
|
||||
return {}
|
||||
}
|
||||
case 'FLEXIBLE': {
|
||||
const { name, phone, idCardNumber, department, agreementStartDate, agreementEndDate, payMethod } = formData
|
||||
const idCardHash = idCardNumber ? crypto.createHash('sha256').update(idCardNumber).digest('hex') : null
|
||||
const employee = await prisma.employee.create({
|
||||
data: {
|
||||
orgId,
|
||||
name,
|
||||
department: department || '灵活用工',
|
||||
hireDate: new Date(agreementStartDate),
|
||||
monthlySalary: encrypt('0'),
|
||||
phone: phone || null,
|
||||
idCardNumber: idCardNumber ? encrypt(idCardNumber) : null,
|
||||
idCardHash,
|
||||
createdBy: userId,
|
||||
},
|
||||
})
|
||||
const contract = await prisma.laborContract.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: employee.id,
|
||||
signDate: new Date(),
|
||||
startDate: new Date(agreementStartDate),
|
||||
endDate: agreementEndDate ? new Date(agreementEndDate) : null,
|
||||
contractType: 'LABOR',
|
||||
signMethod: 'PAPER',
|
||||
contractYears: 1,
|
||||
createdBy: userId,
|
||||
},
|
||||
})
|
||||
return { employeeId: employee.id, newContractId: contract.id }
|
||||
}
|
||||
case 'INFO_SUBMIT': {
|
||||
const { employeeId, ...updateFields } = formData
|
||||
if (employeeId) {
|
||||
const allowedFields: Record<string, any> = {}
|
||||
if (updateFields.department) allowedFields.department = updateFields.department
|
||||
if (updateFields.phone) allowedFields.phone = updateFields.phone
|
||||
if (updateFields.address) allowedFields.address = updateFields.address
|
||||
if (updateFields.emergencyContact) allowedFields.emergencyContact = updateFields.emergencyContact
|
||||
if (updateFields.emergencyPhone) allowedFields.emergencyPhone = updateFields.emergencyPhone
|
||||
if (updateFields.bankAccount) allowedFields.bankAccount = encrypt(updateFields.bankAccount)
|
||||
if (updateFields.bankName) allowedFields.bankName = updateFields.bankName
|
||||
if (Object.keys(allowedFields).length > 0) {
|
||||
await prisma.employee.update({ where: { id: employeeId }, data: allowedFields })
|
||||
}
|
||||
}
|
||||
return { employeeId }
|
||||
}
|
||||
case 'INCOME_CERT':
|
||||
case 'LEAVING_CERT': {
|
||||
// 这两类只生成文书,不改变业务数据
|
||||
return {}
|
||||
}
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
// 生成文书预览
|
||||
export function generateDocument(type: string, formData: any, orgName: string): { name: string; content: string } {
|
||||
const templates: Record<string, (data: any, org: string) => string> = {
|
||||
INCOME_CERT: (data, org) => `收入证明
|
||||
|
||||
兹证明 ${data.employeeName || '___'}(身份证号:${data.idCardNumber || '___'})系我单位员工,自 ${data.hireDate || '___'} 起在我单位工作,现任 ${data.position || '___'} 职务。
|
||||
|
||||
该员工近一年平均月收入为人民币 ${data.monthlyIncome || '___'} 元(税前)。
|
||||
|
||||
本证明仅用于 ${data.purpose || '___'},不作其他用途。
|
||||
|
||||
特此证明。
|
||||
|
||||
${org}
|
||||
${new Date().toLocaleDateString('zh-CN')}`,
|
||||
LEAVING_CERT: (data, org) => `离职证明
|
||||
|
||||
兹证明 ${data.employeeName || '___'}(身份证号:${data.idCardNumber || '___'})自 ${data.hireDate || '___'} 至 ${data.leaveDate || '___'} 在我单位工作,最后职务为 ${data.position || '___'}。
|
||||
|
||||
该员工已于 ${data.leaveDate || '___'} 与我单位解除劳动关系,双方已办妥交接手续。
|
||||
|
||||
特此证明。
|
||||
|
||||
${org}
|
||||
${new Date().toLocaleDateString('zh-CN')}`,
|
||||
}
|
||||
const generator = templates[type]
|
||||
if (!generator) return { name: '', content: '' }
|
||||
return { name: `${PROCESS_TYPES[type]?.label || '文书'}.doc`, content: generator(formData, orgName) }
|
||||
}
|
||||
Reference in New Issue
Block a user