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 { auditLog } from '../middleware/auditLog'
|
||||
import { createEvidence } from '../services/evidence.service'
|
||||
import { autoCreateEsignRecord } from '../services/esign.service'
|
||||
import prisma from '../lib/prisma'
|
||||
import { sha256, decrypt } from '../lib/crypto'
|
||||
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'] }],
|
||||
createdBy: req.user!.id,
|
||||
}).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) {
|
||||
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 { RiskAssessment, TerminationReason } from '@prisma/client'
|
||||
import { autoCreateEsignRecord } from './esign.service'
|
||||
|
||||
function dateToMonth(date: Date): string {
|
||||
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 }
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
|
||||
@@ -155,14 +155,22 @@ export default function Roster() {
|
||||
}, [employeeParam, employeeIdParam, debouncedSearch, isLoading, employees, selectedId, setSearchParams])
|
||||
|
||||
const addMutation = useMutation({
|
||||
mutationFn: (data: any) => employeeApi.create(data),
|
||||
onSuccess: () => {
|
||||
mutationFn: async (data: any) => {
|
||||
const res = await employeeApi.create(data)
|
||||
// 如果返回了 esignRecord,说明自动创建了电子签署
|
||||
return res
|
||||
},
|
||||
onSuccess: (res: any) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['roster'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['roster-profile'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['esign-records'] })
|
||||
localStorage.removeItem('add-employee-draft')
|
||||
setShowAddModal(false)
|
||||
toast.success('员工已添加')
|
||||
const esignCreated = res?.data?.esignRecord
|
||||
toast.success(esignCreated ? '员工已添加,劳动合同电子签署已自动发起' : '员工已添加', {
|
||||
action: esignCreated ? { label: '查看签署', onClick: () => navigate('/esign') } : undefined,
|
||||
})
|
||||
},
|
||||
onError: (err: any) => toastError(err, '创建失败'),
|
||||
})
|
||||
@@ -181,7 +189,8 @@ export default function Roster() {
|
||||
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['termination-drafts'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['roster-profile'] })
|
||||
toast.success('已创建离职草稿,请前往「解聘补偿」页面完成流程', {
|
||||
queryClient.invalidateQueries({ queryKey: ['esign-records'] })
|
||||
toast.success('已创建离职草稿,离职协议电子签署已自动发起', {
|
||||
action: { label: '前往处理', onClick: () => navigate('/termination') },
|
||||
})
|
||||
setShowResignModal(false)
|
||||
|
||||
Reference in New Issue
Block a user