feat: 花名册操作自动创建电子签署流程
## 新增 esign.service.ts - autoCreateEsignRecord 公共方法,供其他业务模块调用 - 自动渲染模板文件内容 + 创建证据链 ## 自动触发签署 - 增加员工(创建合同时):自动创建劳动合同电子签署记录(scene=CONTRACT) - 主动离职(createDraft type=RESIGNATION):自动创建离职协议签署(scene=RESIGNATION) - 公司解聘(executeTermination type=TERMINATION):执行完成后自动创建离职协议签署 ## 前端提示 - 添加员工成功后提示"劳动合同电子签署已自动发起",可跳转签署页面 - 主动离职成功后提示"离职协议电子签署已自动发起" Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { Router } from 'express'
|
|||||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||||
import { auditLog } from '../middleware/auditLog'
|
import { auditLog } from '../middleware/auditLog'
|
||||||
import { createEvidence } from '../services/evidence.service'
|
import { createEvidence } from '../services/evidence.service'
|
||||||
|
import { autoCreateEsignRecord } from '../services/esign.service'
|
||||||
import prisma from '../lib/prisma'
|
import prisma from '../lib/prisma'
|
||||||
import { sha256, decrypt } from '../lib/crypto'
|
import { sha256, decrypt } from '../lib/crypto'
|
||||||
import {
|
import {
|
||||||
@@ -310,7 +311,19 @@ router.post('/contracts', authMiddleware, async (req: AuthRequest, res, next) =>
|
|||||||
events: [{ action: '合同签订', timestamp: new Date().toISOString(), ip: req.ip, userAgent: req.headers['user-agent'] }],
|
events: [{ action: '合同签订', timestamp: new Date().toISOString(), ip: req.ip, userAgent: req.headers['user-agent'] }],
|
||||||
createdBy: req.user!.id,
|
createdBy: req.user!.id,
|
||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
res.json({ success: true, data: result })
|
|
||||||
|
// 自动创建劳动合同电子签署记录
|
||||||
|
const esignRecord = await autoCreateEsignRecord({
|
||||||
|
orgId: req.user!.orgId,
|
||||||
|
employeeId: data.employeeId,
|
||||||
|
scene: 'CONTRACT',
|
||||||
|
documentTitle: `${emp?.name || ''}的劳动合同`,
|
||||||
|
contractId: result.id,
|
||||||
|
remark: '新增员工合同时自动发起',
|
||||||
|
createdBy: req.user!.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
res.json({ success: true, data: result, esignRecord })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(err)
|
next(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* 电子签署服务
|
||||||
|
* 提供自动创建签署记录的公共方法,供其他业务模块调用
|
||||||
|
*/
|
||||||
|
import prisma from '../lib/prisma'
|
||||||
|
import { createEvidence } from './evidence.service'
|
||||||
|
import { renderTemplate, getTemplateById } from './template.service'
|
||||||
|
|
||||||
|
/** 场景与模板ID的映射 */
|
||||||
|
const SCENE_TEMPLATE_MAP: Record<string, string | null> = {
|
||||||
|
CONTRACT: 'tpl_fixed_term_contract',
|
||||||
|
RESIGNATION: 'tpl_termination_agreement',
|
||||||
|
POLICY: null,
|
||||||
|
PAYSLIP: null,
|
||||||
|
ONBOARDING: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动创建电子签署记录
|
||||||
|
* @param params.orgId 组织ID
|
||||||
|
* @param params.employeeId 员工ID
|
||||||
|
* @param params.scene 签署场景
|
||||||
|
* @param params.documentTitle 文件标题
|
||||||
|
* @param params.contractId 关联合同ID(可选)
|
||||||
|
* @param params.remark 备注(可选)
|
||||||
|
* @param params.createdBy 创建人用户ID
|
||||||
|
* @param params.templateVars 模板变量(可选)
|
||||||
|
* @returns 创建的 ESignRecord 或 null(创建失败时)
|
||||||
|
*/
|
||||||
|
export async function autoCreateEsignRecord(params: {
|
||||||
|
orgId: string
|
||||||
|
employeeId: string
|
||||||
|
scene: string
|
||||||
|
documentTitle: string
|
||||||
|
contractId?: string
|
||||||
|
remark?: string
|
||||||
|
createdBy: string
|
||||||
|
templateVars?: Record<string, string>
|
||||||
|
}): Promise<any | null> {
|
||||||
|
try {
|
||||||
|
const { orgId, employeeId, scene, documentTitle, contractId, remark, createdBy, templateVars } = params
|
||||||
|
|
||||||
|
// 获取员工信息
|
||||||
|
const employee = await prisma.employee.findFirst({
|
||||||
|
where: { id: employeeId, orgId },
|
||||||
|
select: { id: true, name: true, phone: true, idCardNumber: true, position: true, monthlySalary: true, department: true },
|
||||||
|
})
|
||||||
|
if (!employee) return null
|
||||||
|
|
||||||
|
// 自动渲染文件内容
|
||||||
|
let documentContent = ''
|
||||||
|
const templateId = SCENE_TEMPLATE_MAP[scene]
|
||||||
|
if (templateId) {
|
||||||
|
const template = getTemplateById(templateId)
|
||||||
|
if (template) {
|
||||||
|
const org = await prisma.organization.findUnique({ where: { id: orgId }, select: { name: true } })
|
||||||
|
const vars: Record<string, string> = {
|
||||||
|
companyName: org?.name || '',
|
||||||
|
employeeName: employee.name || '',
|
||||||
|
idCard: employee.idCardNumber || '',
|
||||||
|
position: employee.position || '',
|
||||||
|
monthlySalary: String(employee.monthlySalary || ''),
|
||||||
|
department: employee.department || '',
|
||||||
|
...templateVars,
|
||||||
|
}
|
||||||
|
documentContent = renderTemplate(templateId, vars) || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建签署记录
|
||||||
|
const record = await prisma.eSignRecord.create({
|
||||||
|
data: {
|
||||||
|
orgId,
|
||||||
|
contractId: contractId || null,
|
||||||
|
employeeId,
|
||||||
|
scene,
|
||||||
|
documentTitle,
|
||||||
|
documentContent: documentContent || null,
|
||||||
|
status: 'PENDING',
|
||||||
|
initiatedBy: createdBy,
|
||||||
|
createdBy,
|
||||||
|
remark: remark || null,
|
||||||
|
expiredAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 创建证据链
|
||||||
|
await createEvidence({
|
||||||
|
orgId,
|
||||||
|
category: 'CONTRACT_SIGN',
|
||||||
|
refId: record.id,
|
||||||
|
employeeId,
|
||||||
|
events: [{
|
||||||
|
action: `自动发起电子签署:${documentTitle}`,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
location: `场景:${scene},触发:系统自动`,
|
||||||
|
}],
|
||||||
|
createdBy,
|
||||||
|
}).catch(() => {})
|
||||||
|
|
||||||
|
return record
|
||||||
|
} catch (err) {
|
||||||
|
// 自动创建失败不阻断主流程
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import prisma from '../lib/prisma'
|
import prisma from '../lib/prisma'
|
||||||
import { RiskAssessment, TerminationReason } from '@prisma/client'
|
import { RiskAssessment, TerminationReason } from '@prisma/client'
|
||||||
|
import { autoCreateEsignRecord } from './esign.service'
|
||||||
|
|
||||||
function dateToMonth(date: Date): string {
|
function dateToMonth(date: Date): string {
|
||||||
const y = date.getFullYear()
|
const y = date.getFullYear()
|
||||||
@@ -594,6 +595,18 @@ export async function createDraft(orgId: string, userId: string, data: any) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 主动离职时自动创建离职协议电子签署记录
|
||||||
|
if (data.type === 'RESIGNATION') {
|
||||||
|
await autoCreateEsignRecord({
|
||||||
|
orgId,
|
||||||
|
employeeId: data.employeeId,
|
||||||
|
scene: 'RESIGNATION',
|
||||||
|
documentTitle: `${employee.name}的离职协议`,
|
||||||
|
remark: '员工主动离职时自动发起',
|
||||||
|
createdBy: userId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return { id: record.id }
|
return { id: record.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -749,6 +762,19 @@ export async function executeTermination(orgId: string, recordId: string, userId
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 公司解聘执行完成后自动创建离职协议电子签署记录
|
||||||
|
if (record.type === 'TERMINATION') {
|
||||||
|
const employee = await prisma.employee.findFirst({ where: { id: record.employeeId }, select: { name: true } })
|
||||||
|
await autoCreateEsignRecord({
|
||||||
|
orgId,
|
||||||
|
employeeId: record.employeeId,
|
||||||
|
scene: 'RESIGNATION',
|
||||||
|
documentTitle: `${employee?.name || '员工'}的解除劳动合同协议`,
|
||||||
|
remark: '公司解聘执行完成时自动发起',
|
||||||
|
createdBy: userId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return { id: recordId }
|
return { id: recordId }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -155,14 +155,22 @@ export default function Roster() {
|
|||||||
}, [employeeParam, employeeIdParam, debouncedSearch, isLoading, employees, selectedId, setSearchParams])
|
}, [employeeParam, employeeIdParam, debouncedSearch, isLoading, employees, selectedId, setSearchParams])
|
||||||
|
|
||||||
const addMutation = useMutation({
|
const addMutation = useMutation({
|
||||||
mutationFn: (data: any) => employeeApi.create(data),
|
mutationFn: async (data: any) => {
|
||||||
onSuccess: () => {
|
const res = await employeeApi.create(data)
|
||||||
|
// 如果返回了 esignRecord,说明自动创建了电子签署
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
onSuccess: (res: any) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['roster'] })
|
queryClient.invalidateQueries({ queryKey: ['roster'] })
|
||||||
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
|
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
|
||||||
queryClient.invalidateQueries({ queryKey: ['roster-profile'] })
|
queryClient.invalidateQueries({ queryKey: ['roster-profile'] })
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['esign-records'] })
|
||||||
localStorage.removeItem('add-employee-draft')
|
localStorage.removeItem('add-employee-draft')
|
||||||
setShowAddModal(false)
|
setShowAddModal(false)
|
||||||
toast.success('员工已添加')
|
const esignCreated = res?.data?.esignRecord
|
||||||
|
toast.success(esignCreated ? '员工已添加,劳动合同电子签署已自动发起' : '员工已添加', {
|
||||||
|
action: esignCreated ? { label: '查看签署', onClick: () => navigate('/esign') } : undefined,
|
||||||
|
})
|
||||||
},
|
},
|
||||||
onError: (err: any) => toastError(err, '创建失败'),
|
onError: (err: any) => toastError(err, '创建失败'),
|
||||||
})
|
})
|
||||||
@@ -181,7 +189,8 @@ export default function Roster() {
|
|||||||
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
|
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
|
||||||
queryClient.invalidateQueries({ queryKey: ['termination-drafts'] })
|
queryClient.invalidateQueries({ queryKey: ['termination-drafts'] })
|
||||||
queryClient.invalidateQueries({ queryKey: ['roster-profile'] })
|
queryClient.invalidateQueries({ queryKey: ['roster-profile'] })
|
||||||
toast.success('已创建离职草稿,请前往「解聘补偿」页面完成流程', {
|
queryClient.invalidateQueries({ queryKey: ['esign-records'] })
|
||||||
|
toast.success('已创建离职草稿,离职协议电子签署已自动发起', {
|
||||||
action: { label: '前往处理', onClick: () => navigate('/termination') },
|
action: { label: '前往处理', onClick: () => navigate('/termination') },
|
||||||
})
|
})
|
||||||
setShowResignModal(false)
|
setShowResignModal(false)
|
||||||
|
|||||||
Reference in New Issue
Block a user