diff --git a/backend/src/routes/employee.routes.ts b/backend/src/routes/employee.routes.ts index a9d639f..2b7eb97 100644 --- a/backend/src/routes/employee.routes.ts +++ b/backend/src/routes/employee.routes.ts @@ -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) } diff --git a/backend/src/services/esign.service.ts b/backend/src/services/esign.service.ts new file mode 100644 index 0000000..69deb4b --- /dev/null +++ b/backend/src/services/esign.service.ts @@ -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 = { + 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 +}): Promise { + 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 = { + 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 + } +} diff --git a/backend/src/services/termination.service.ts b/backend/src/services/termination.service.ts index 965d2f0..b5508eb 100644 --- a/backend/src/services/termination.service.ts +++ b/backend/src/services/termination.service.ts @@ -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 } } diff --git a/frontend/src/pages/Roster.tsx b/frontend/src/pages/Roster.tsx index 6b0c740..05a263d 100644 --- a/frontend/src/pages/Roster.tsx +++ b/frontend/src/pages/Roster.tsx @@ -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)