Files
TurboHR/frontend/src/components/ui/SignMethodChoice.tsx
T
selfrelease 3da61d5a09 feat: 花名册操作联动电子签署 + 修复重复创建
## 修复
- POST /contracts 不再自动创建签署记录(由前端根据signMethod决定),避免与ContractInfo重复创建

## 新增 SignMethodChoice 通用组件
- 花名册操作成功后弹窗选择签署方式:电子签/线下手签/稍后处理
- 电子签:自动创建ESignRecord(PENDING),员工在员工端签署
- 线下手签:跳转签署页面,预填员工/场景/标题,上传扫描件登记
- 稍后处理:跳过,可后续在签署页面手动发起

## 花名册4项操作联动签署方式选择
- 重新入职:操作成功后弹窗(scene=CONTRACT,劳动合同)
- 合同续签:操作成功后弹窗(scene=CONTRACT,续签劳动合同)
- 薪酬变更:操作成功后弹窗(scene=POLICY,薪酬调整确认书)
- 调岗调动:操作成功后弹窗(scene=POLICY,调岗确认书)

## ESign页面支持URL参数
- 读取 ?action=paper-sign 自动打开线下手签登记Modal
- 预填employeeId/scene/documentTitle

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-15 16:45:53 +08:00

167 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 签署方式选择弹窗
* 花名册操作(重新入职/合同续签/薪酬变更/调岗调动)成功后,
* 弹窗让HR选择是走电子签署还是线下手签。
*
* - 电子签:自动创建电子签署记录(status=PENDING),员工在员工端签署
* - 线下手签:跳转到电子签署页面的线下手签登记
* - 跳过:不创建签署记录
*/
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { toast } from 'sonner'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { PenTool, FileCheck, SkipForward, Shield } from 'lucide-react'
import { esignApi } from '../../lib/api-services'
import Modal from './Modal'
import Button from './Button'
interface SignMethodChoiceProps {
open: boolean
onClose: () => void
/** 员工ID */
employeeId: string
/** 员工姓名(用于显示) */
employeeName: string
/** 签署场景 */
scene: 'CONTRACT' | 'RESIGNATION' | 'POLICY' | 'PAYSLIP' | 'ONBOARDING'
/** 文件标题 */
documentTitle: string
/** 关联合同ID(可选) */
contractId?: string
/** 备注 */
remark?: string
/** 操作名称(如"重新入职"、"合同续签" */
actionName: string
}
const SCENE_LABELS: Record<string, string> = {
CONTRACT: '劳动合同',
RESIGNATION: '离职协议',
POLICY: '规章制度',
PAYSLIP: '工资条',
ONBOARDING: '入职文件',
}
export default function SignMethodChoice({
open,
onClose,
employeeId,
employeeName,
scene,
documentTitle,
contractId,
remark,
actionName,
}: SignMethodChoiceProps) {
const navigate = useNavigate()
const queryClient = useQueryClient()
const [creating, setCreating] = useState(false)
const createEsignMutation = useMutation({
mutationFn: (data: any) => esignApi.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['esign-records'] })
toast.success('电子签署已发起,员工可在员工端查看并签署', {
action: { label: '查看签署', onClick: () => navigate('/esign') },
})
onClose()
},
onError: (err: any) => toast.error(err?.response?.data?.error?.message || '创建失败'),
})
/** 选择电子签署 */
const handleEsign = () => {
setCreating(true)
createEsignMutation.mutate({
employeeId,
contractId,
scene,
documentTitle,
remark: remark || `${actionName}时自动发起`,
})
setCreating(false)
}
/** 选择线下手签 → 跳转到签署页面 */
const handlePaperSign = () => {
onClose()
// 通过 URL 参数传递信息,签署页面读取后自动打开线下手签登记
const params = new URLSearchParams({
action: 'paper-sign',
employeeId,
scene,
documentTitle,
})
if (contractId) params.set('contractId', contractId)
navigate(`/esign?${params.toString()}`)
}
/** 跳过 */
const handleSkip = () => {
toast.info('已跳过签署,可稍后在「电子签署」页面手动发起')
onClose()
}
return (
<Modal open={open} onClose={onClose} title={`${actionName}成功 — 选择签署方式`} size="sm">
<div className="space-y-4">
<div className="text-sm text-gray-600 bg-gray-50 rounded-md p-3">
<div className="font-medium text-gray-700">{employeeName} · {SCENE_LABELS[scene] || scene}</div>
<div className="text-xs text-gray-400 mt-1">{documentTitle}</div>
</div>
<div className="text-xs text-gray-500 flex items-start gap-1.5">
<Shield className="w-3.5 h-3.5 mt-0.5 shrink-0 text-primary" />
<div>{SCENE_LABELS[scene] || '文件'}</div>
</div>
<div className="space-y-2">
{/* 电子签署 */}
<button
onClick={handleEsign}
disabled={creating}
className="w-full flex items-center gap-3 p-3 border-2 border-blue-200 bg-blue-50/50 rounded-lg hover:border-blue-400 hover:bg-blue-50 transition text-left disabled:opacity-50"
>
<div className="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center shrink-0">
<PenTool className="w-5 h-5 text-blue-600" />
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-gray-800"></div>
<div className="text-xs text-gray-500 mt-0.5"></div>
</div>
</button>
{/* 线下手签 */}
<button
onClick={handlePaperSign}
className="w-full flex items-center gap-3 p-3 border-2 border-orange-200 bg-orange-50/50 rounded-lg hover:border-orange-400 hover:bg-orange-50 transition text-left"
>
<div className="w-10 h-10 rounded-lg bg-orange-100 flex items-center justify-center shrink-0">
<FileCheck className="w-5 h-5 text-orange-600" />
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-gray-800">线</div>
<div className="text-xs text-gray-500 mt-0.5"></div>
</div>
</button>
{/* 跳过 */}
<button
onClick={handleSkip}
className="w-full flex items-center gap-3 p-3 border-2 border-gray-200 bg-gray-50/50 rounded-lg hover:border-gray-300 hover:bg-gray-50 transition text-left"
>
<div className="w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center shrink-0">
<SkipForward className="w-5 h-5 text-gray-500" />
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-gray-600"></div>
<div className="text-xs text-gray-400 mt-0.5"></div>
</div>
</button>
</div>
</div>
</Modal>
)
}