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:
freedakgmail
2026-07-30 10:21:22 +08:00
parent 38b8849332
commit 42e0c650a4
24 changed files with 3639 additions and 35 deletions
@@ -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) }
}