feat: 电子签署集成到合同管理和离职流程

- 合同管理:选择电子签署时保存后自动创建ESignRecord并关联合同ID
- 离职管理:工作交接清单「离职文件签署」步骤增加「发起电子签署」按钮
- 电子签署页面:列表增加来源标识(劳动合同/离职协议)
- 合同卡片:电子签署合同显示提示而非手动填写编号/链接
This commit is contained in:
freedakgmail
2026-08-05 07:34:01 +08:00
parent cce0c936bd
commit b9240ffe9b
3 changed files with 64 additions and 21 deletions
+6
View File
@@ -149,6 +149,12 @@ export default function ESign() {
<div className="flex items-center gap-1.5">
<FileText className="w-3.5 h-3.5 text-gray-400 shrink-0" />
<span className="font-medium truncate max-w-[200px]">{r.documentTitle}</span>
{r.contractId && (
<span className="px-1.5 py-0.5 rounded text-xs bg-blue-50 text-blue-600 border border-blue-200 shrink-0"></span>
)}
{!r.contractId && r.documentTitle?.includes('离职') && (
<span className="px-1.5 py-0.5 rounded text-xs bg-orange-50 text-orange-600 border border-orange-200 shrink-0"></span>
)}
</div>
{r.remark && <div className="text-xs text-gray-400 mt-0.5">{r.remark}</div>}
</td>
+23 -2
View File
@@ -2,12 +2,12 @@ import { useState, useMemo, useEffect } from 'react'
import { usePageSize } from '../hooks/usePageSize'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { AlertTriangle, Check, ChevronRight, ChevronLeft, Shield, Info, Calculator, FileText, Printer, Trash2, List, Download, Plus, Edit, Send, CheckCircle, XCircle, Play, Ban, CheckCheck } from 'lucide-react'
import { AlertTriangle, Check, ChevronRight, ChevronLeft, Shield, Info, Calculator, FileText, Printer, Trash2, List, Download, Plus, Edit, Send, CheckCircle, XCircle, Play, Ban, CheckCheck, PenTool } from 'lucide-react'
import { Stepper } from '../components/ui/Stepper'
import { InlineAlert } from '../components/ui/InlineAlert'
import PageGuide from '../components/ui/PageGuide'
import jsPDF from 'jspdf'
import { rosterApi, terminationApi } from '../lib/api-services'
import { rosterApi, terminationApi, esignApi } from '../lib/api-services'
import { useAuthStore } from '../store/authStore'
import Card from '../components/ui/Card'
import Button from '../components/ui/Button'
@@ -1549,6 +1549,27 @@ export default function Termination() {
placeholder="备注说明(选填)"
className="text-xs"
/>
{item.key === 'docs_signed' && employeeId && (
<Button
size="sm"
variant="secondary"
onClick={async () => {
try {
await esignApi.create({
employeeId,
documentTitle: '离职协议',
remark: '离职流程中发起',
})
queryClient.invalidateQueries({ queryKey: ['esign-records'] })
toast.success('离职协议电子签署已发起')
} catch {
toast.error('发起签署失败')
}
}}
>
<PenTool className="w-3.5 h-3.5 mr-1" />
</Button>
)}
</div>
))}
<div className="flex items-center gap-2 text-xs text-gray-500">
+35 -19
View File
@@ -1,12 +1,12 @@
import { useState, useRef } from "react"
import { toast } from "sonner"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { employeeApi } from '../../lib/api-services'
import { employeeApi, esignApi } from '../../lib/api-services'
import { useConfirm } from '../../hooks/useConfirm'
import Card from "../../components/ui/Card"
import Button from "../../components/ui/Button"
import { Input, Label, Select } from "../../components/ui/Input"
import { FileText, AlertTriangle, X, Paperclip, Trash2, Info, Download } from "lucide-react"
import { FileText, AlertTriangle, X, Paperclip, Trash2, Info, Download, PenTool } from "lucide-react"
export default function ContractInfo({ employeeId, contracts, hireDate }: { employeeId: string; contracts: any[]; hireDate: string }) {
const queryClient = useQueryClient()
@@ -17,8 +17,27 @@ export default function ContractInfo({ employeeId, contracts, hireDate }: { empl
const [previewUrl, setPreviewUrl] = useState<string | null>(null)
const addContractMutation = useMutation({
mutationFn: (data: any) => employeeApi.addContract({ ...data, employeeId }),
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }); setShowForm(false) },
mutationFn: async (data: any) => {
const res = await employeeApi.addContract({ ...data, employeeId }) as any
const contractId = res?.data?.id
if (data.signMethod === 'ELECTRONIC' && contractId) {
try {
await esignApi.create({
contractId,
employeeId,
documentTitle: `${data.contractType === 'UNFIXED' ? '无固定期限' : '固定期限'}劳动合同`,
remark: '合同创建时自动发起',
})
toast.success('合同已保存,电子签署记录已创建')
} catch {
toast.success('合同已保存,电子签署记录创建失败(可稍后手动发起)')
}
} else {
toast.success('合同已保存')
}
return res
},
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }); queryClient.invalidateQueries({ queryKey: ['esign-records'] }); setShowForm(false) },
})
const deleteContractMutation = useMutation({
@@ -193,10 +212,12 @@ export default function ContractInfo({ employeeId, contracts, hireDate }: { empl
</div>
)}
{form.signMethod === 'ELECTRONIC' && (
<>
<div><Label> *</Label><Input value={form.electronicContractNo} onChange={(e) => setForm({ ...form, electronicContractNo: e.target.value })} placeholder="如 E-2026-001" /></div>
<div><Label> *</Label><Input value={form.electronicContractUrl} onChange={(e) => setForm({ ...form, electronicContractUrl: e.target.value })} placeholder="https://..." /></div>
</>
<div className="md:col-span-2">
<div className="px-3 py-2 rounded-md bg-blue-50 text-blue-700 text-xs flex items-start gap-2">
<PenTool className="w-4 h-4 mt-0.5 shrink-0" />
<span></span>
</div>
</div>
)}
<div className="md:col-span-2 flex gap-2">
<Button onClick={() => {
@@ -267,17 +288,12 @@ export default function ContractInfo({ employeeId, contracts, hireDate }: { empl
</div>
)}
{c.signMethod === 'ELECTRONIC' && (
<>
{c.electronicContractNo && <div className="flex justify-between"><span className="text-gray-500"></span><span className="font-medium">{c.electronicContractNo}</span></div>}
{c.electronicContractUrl && (
<div className="flex justify-between md:col-span-3">
<span className="text-gray-500"></span>
<a href={c.electronicContractUrl} target="_blank" rel="noopener noreferrer" className="text-primary hover:underline flex items-center gap-1">
<FileText className="w-3 h-3" />
</a>
</div>
)}
</>
<div className="flex justify-between md:col-span-3">
<span className="text-gray-500"></span>
<span className="text-primary flex items-center gap-1 text-xs">
<PenTool className="w-3 h-3" />
</span>
</div>
)}
</div>
<button