feat: add signature drawing functionality to contract pages

This commit is contained in:
seaznCode 2026-01-14 16:58:20 +01:00
parent a88efc3e9f
commit e32a9f69cd
2 changed files with 264 additions and 114 deletions

View File

@ -1,25 +1,26 @@
'use client' 'use client'
import { useState, useEffect } from 'react' import { useState, useEffect, useRef } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import PageLayout from '../../../components/PageLayout' import PageLayout from '../../../components/PageLayout'
import useAuthStore from '../../../store/authStore' import useAuthStore from '../../../store/authStore'
import { useUserStatus } from '../../../hooks/useUserStatus' import { useUserStatus } from '../../../hooks/useUserStatus'
import { API_BASE_URL } from '../../../utils/api' import { API_BASE_URL } from '../../../utils/api'
import { useToast } from '../../../components/toast/toastComponent' // NEW import { useToast } from '../../../components/toast/toastComponent'
export default function CompanySignContractPage() { export default function CompanySignContractPage() {
const router = useRouter() const router = useRouter()
const { accessToken } = useAuthStore() const { accessToken } = useAuthStore()
const { refreshStatus } = useUserStatus() const { refreshStatus } = useUserStatus()
const { showToast } = useToast() // NEW const { showToast } = useToast()
const [companyName, setCompanyName] = useState('') const [companyName, setCompanyName] = useState('')
const [repName, setRepName] = useState('') const [repName, setRepName] = useState('')
const [repTitle, setRepTitle] = useState('') const [repTitle, setRepTitle] = useState('')
const [location, setLocation] = useState('') const [location, setLocation] = useState('')
const [date, setDate] = useState('')
const [note, setNote] = useState('') const [note, setNote] = useState('')
const [date, setDate] = useState('')
const [signatureDataUrl, setSignatureDataUrl] = useState('')
const [agreeContract, setAgreeContract] = useState(false) const [agreeContract, setAgreeContract] = useState(false)
const [agreeData, setAgreeData] = useState(false) const [agreeData, setAgreeData] = useState(false)
const [confirmSignature, setConfirmSignature] = useState(false) const [confirmSignature, setConfirmSignature] = useState(false)
@ -31,9 +32,91 @@ export default function CompanySignContractPage() {
const [previewError, setPreviewError] = useState<string | null>(null) const [previewError, setPreviewError] = useState<string | null>(null)
useEffect(() => { useEffect(() => {
setDate(new Date().toISOString().slice(0,10)) setDate(new Date().toISOString().slice(0, 10))
}, []) }, [])
const canvasRef = useRef<HTMLCanvasElement | null>(null)
const isDrawing = useRef(false)
const scaleRef = useRef(1)
const getPos = (e: React.MouseEvent<HTMLCanvasElement> | React.TouchEvent<HTMLCanvasElement>) => {
const canvas = canvasRef.current
if (!canvas) return { x: 0, y: 0 }
const rect = canvas.getBoundingClientRect()
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY
const x = (clientX - rect.left) * scaleRef.current
const y = (clientY - rect.top) * scaleRef.current
return { x, y }
}
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const resize = () => {
const rect = canvas.getBoundingClientRect()
const dpr = window.devicePixelRatio || 1
canvas.width = rect.width * dpr
canvas.height = rect.height * dpr
const ctx = canvas.getContext('2d')
if (ctx) {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
ctx.lineWidth = 2
ctx.lineCap = 'round'
ctx.strokeStyle = '#0f172a'
}
scaleRef.current = dpr
}
resize()
window.addEventListener('resize', resize)
return () => window.removeEventListener('resize', resize)
}, [])
const startDrawing = (e: React.MouseEvent<HTMLCanvasElement> | React.TouchEvent<HTMLCanvasElement>) => {
e.preventDefault()
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
ctx.lineWidth = 2
ctx.lineCap = 'round'
ctx.strokeStyle = '#0f172a'
const { x, y } = getPos(e)
ctx.beginPath()
ctx.moveTo(x, y)
isDrawing.current = true
}
const draw = (e: React.MouseEvent<HTMLCanvasElement> | React.TouchEvent<HTMLCanvasElement>) => {
e.preventDefault()
if (!isDrawing.current) return
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
const { x, y } = getPos(e)
ctx.lineTo(x, y)
ctx.stroke()
}
const endDrawing = () => {
if (!isDrawing.current) return
isDrawing.current = false
const canvas = canvasRef.current
if (!canvas) return
const dataUrl = canvas.toDataURL('image/png')
setSignatureDataUrl(dataUrl)
}
const clearSignature = () => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
ctx.clearRect(0, 0, canvas.width, canvas.height)
setSignatureDataUrl('')
}
// Load latest contract preview for company user // Load latest contract preview for company user
useEffect(() => { useEffect(() => {
const loadPreview = async () => { const loadPreview = async () => {
@ -41,10 +124,10 @@ export default function CompanySignContractPage() {
setPreviewLoading(true) setPreviewLoading(true)
setPreviewError(null) setPreviewError(null)
try { try {
const res = await fetch(`${API_BASE_URL}/api/contracts/preview/latest`, { const res = await fetch(`${API_BASE_URL}/api/contracts/preview/latest?contract_type=company`, {
method: 'GET', method: 'GET',
headers: { Authorization: `Bearer ${accessToken}` }, headers: { Authorization: `Bearer ${accessToken}` },
credentials: 'include' credentials: 'include'
}) })
if (!res.ok) { if (!res.ok) {
const text = await res.text().catch(() => '') const text = await res.text().catch(() => '')
@ -64,15 +147,16 @@ export default function CompanySignContractPage() {
}, [accessToken]) }, [accessToken])
const valid = () => { const valid = () => {
const companyValid = companyName.trim().length >= 3 // Min 3 characters for company name const companyValid = companyName.trim().length >= 3
const repNameValid = repName.trim().length >= 3 // Min 3 characters for representative name const repNameValid = repName.trim().length >= 3
const repTitleValid = repTitle.trim().length >= 2 // Min 2 characters for title const repTitleValid = repTitle.trim().length >= 2
const locationValid = location.trim().length >= 2 // Min 2 characters for location const locationValid = location.trim().length >= 2
const contractChecked = agreeContract const contractChecked = agreeContract
const dataChecked = agreeData const dataChecked = agreeData
const signatureChecked = confirmSignature const signatureChecked = confirmSignature
const signatureDrawn = !!signatureDataUrl
return companyValid && repNameValid && repTitleValid && locationValid && contractChecked && dataChecked && signatureChecked return companyValid && repNameValid && repTitleValid && locationValid && contractChecked && dataChecked && signatureChecked && signatureDrawn
} }
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
@ -87,6 +171,7 @@ export default function CompanySignContractPage() {
if (!agreeContract) issues.push('Contract read and understood') if (!agreeContract) issues.push('Contract read and understood')
if (!agreeData) issues.push('Privacy policy accepted') if (!agreeData) issues.push('Privacy policy accepted')
if (!confirmSignature) issues.push('Electronic signature confirmed') if (!confirmSignature) issues.push('Electronic signature confirmed')
if (!signatureDataUrl) issues.push('Signature captured on pad')
const msg = `Please complete: ${issues.join(', ')}` const msg = `Please complete: ${issues.join(', ')}`
setError(msg) setError(msg)
@ -117,9 +202,9 @@ export default function CompanySignContractPage() {
companyName: companyName.trim(), companyName: companyName.trim(),
representativeName: repName.trim(), representativeName: repName.trim(),
representativeTitle: repTitle.trim(), representativeTitle: repTitle.trim(),
location: location.trim(),
date, date,
note: note.trim() || null, location: location.trim(),
note: note.trim(),
contractType: 'company', contractType: 'company',
confirmations: { confirmations: {
agreeContract, agreeContract,
@ -131,12 +216,11 @@ export default function CompanySignContractPage() {
// Create FormData for the existing backend endpoint // Create FormData for the existing backend endpoint
const formData = new FormData() const formData = new FormData()
formData.append('contractData', JSON.stringify(contractData)) formData.append('contractData', JSON.stringify(contractData))
// Create a dummy PDF file since the backend expects one (electronic signature) if (signatureDataUrl) {
const dummyPdfContent = '%PDF-1.4\n1 0 obj\n<<\n/Type /Catalog\n/Pages 2 0 R\n>>\nendobj\n2 0 obj\n<<\n/Type /Pages\n/Kids [3 0 R]\n/Count 1\n>>\nendobj\n3 0 obj\n<<\n/Type /Page\n/Parent 2 0 R\n/MediaBox [0 0 612 792]\n/Contents 4 0 R\n>>\nendobj\n4 0 obj\n<<\n/Length 44\n>>\nstream\nBT\n/F1 12 Tf\n100 700 Td\n(Electronic Signature) Tj\nET\nendstream\nendobj\nxref\n0 5\n0000000000 65535 f \n0000000010 00000 n \n0000000079 00000 n \n0000000136 00000 n \n0000000225 00000 n \ntrailer\n<<\n/Size 5\n/Root 1 0 R\n>>\nstartxref\n319\n%%EOF' formData.append('signatureImage', signatureDataUrl)
const dummyFile = new Blob([dummyPdfContent], { type: 'application/pdf' }) }
formData.append('contract', dummyFile, 'electronic_signature.pdf')
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/upload/contract/company`, { const response = await fetch(`${API_BASE_URL}/api/upload/contract/company`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Authorization': `Bearer ${accessToken}` 'Authorization': `Bearer ${accessToken}`
@ -190,13 +274,10 @@ export default function CompanySignContractPage() {
return ( return (
<PageLayout> <PageLayout>
<div className="relative min-h-screen overflow-hidden bg-slate-50"> <div className="relative min-h-screen overflow-hidden bg-slate-50">
{/* Animated background (same as dashboard) */}
<div className="pointer-events-none absolute inset-0 z-0"> <div className="pointer-events-none absolute inset-0 z-0">
{/* Soft gradient blobs */}
<div className="absolute -top-32 -left-20 h-80 w-80 rounded-full bg-blue-400/25 blur-3xl animate-pulse" /> <div className="absolute -top-32 -left-20 h-80 w-80 rounded-full bg-blue-400/25 blur-3xl animate-pulse" />
<div className="absolute top-1/4 -right-24 h-96 w-96 rounded-full bg-emerald-400/20 blur-3xl animate-pulse" /> <div className="absolute top-1/4 -right-24 h-96 w-96 rounded-full bg-emerald-400/20 blur-3xl animate-pulse" />
<div className="absolute bottom-[-6rem] left-1/4 h-96 w-96 rounded-full bg-indigo-400/20 blur-3xl animate-pulse" /> <div className="absolute bottom-[-6rem] left-1/4 h-96 w-96 rounded-full bg-indigo-400/20 blur-3xl animate-pulse" />
{/* Subtle radial highlight */}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.14),transparent_60%)]" /> <div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.14),transparent_60%)]" />
</div> </div>
@ -213,7 +294,6 @@ export default function CompanySignContractPage() {
Please review the contract details and sign on behalf of the company. Please review the contract details and sign on behalf of the company.
</p> </p>
{/* Meta + Preview */}
<section className="grid gap-8 lg:grid-cols-2 mb-10"> <section className="grid gap-8 lg:grid-cols-2 mb-10">
<div className="space-y-4"> <div className="space-y-4">
<div className="rounded-lg border border-gray-200 p-5 bg-gray-50"> <div className="rounded-lg border border-gray-200 p-5 bg-gray-50">
@ -257,7 +337,7 @@ export default function CompanySignContractPage() {
setPreviewLoading(true) setPreviewLoading(true)
setPreviewError(null) setPreviewError(null)
try { try {
const res = await fetch(`${API_BASE_URL}/api/contracts/preview/latest`, { const res = await fetch(`${API_BASE_URL}/api/contracts/preview/latest?contract_type=company`, {
method: 'GET', method: 'GET',
headers: { Authorization: `Bearer ${accessToken}` }, headers: { Authorization: `Bearer ${accessToken}` },
credentials: 'include' credentials: 'include'
@ -296,14 +376,11 @@ export default function CompanySignContractPage() {
<hr className="my-10 border-gray-200" /> <hr className="my-10 border-gray-200" />
{/* Company Signature Fields */}
<section> <section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-5">Company & Representative</h2> <h2 className="text-sm font-semibold text-[#0F2460] mb-5">Company & Representative</h2>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<div className="sm:col-span-2 lg:col-span-2"> <div className="sm:col-span-2 lg:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">Company Name *</label>
Company Name *
</label>
<input <input
value={companyName} value={companyName}
onChange={e => { setCompanyName(e.target.value); setError('') }} onChange={e => { setCompanyName(e.target.value); setError('') }}
@ -313,9 +390,7 @@ export default function CompanySignContractPage() {
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">Date *</label>
Date *
</label>
<input <input
type="date" type="date"
value={date} value={date}
@ -325,9 +400,7 @@ export default function CompanySignContractPage() {
/> />
</div> </div>
<div> <div>
<label className="block text_sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">Location *</label>
Location *
</label>
<input <input
value={location} value={location}
onChange={e => { setLocation(e.target.value); setError('') }} onChange={e => { setLocation(e.target.value); setError('') }}
@ -337,9 +410,7 @@ export default function CompanySignContractPage() {
/> />
</div> </div>
<div className="sm:col-span-2 lg:col-span-1"> <div className="sm:col-span-2 lg:col-span-1">
<label className="block text_sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">Representative Name *</label>
Representative Name *
</label>
<input <input
value={repName} value={repName}
onChange={e => { setRepName(e.target.value); setError('') }} onChange={e => { setRepName(e.target.value); setError('') }}
@ -349,9 +420,7 @@ export default function CompanySignContractPage() {
/> />
</div> </div>
<div className="sm:col-span-2 lg:col-span-2"> <div className="sm:col-span-2 lg:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">Representative Position / Title *</label>
Representative Position / Title *
</label>
<input <input
value={repTitle} value={repTitle}
onChange={e => { setRepTitle(e.target.value); setError('') }} onChange={e => { setRepTitle(e.target.value); setError('') }}
@ -361,9 +430,7 @@ export default function CompanySignContractPage() {
/> />
</div> </div>
<div className="sm:col-span-2 lg:col-span-3"> <div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">Note (optional)</label>
Note (optional)
</label>
<input <input
value={note} value={note}
onChange={e => setNote(e.target.value)} onChange={e => setNote(e.target.value)}
@ -372,11 +439,40 @@ export default function CompanySignContractPage() {
/> />
</div> </div>
</div> </div>
<div className="mt-8">
<label className="block text-sm font-medium text-gray-700 mb-2">Draw Signature *</label>
<div className="rounded-lg border border-gray-200 bg-white p-4">
<canvas
ref={canvasRef}
width={800}
height={220}
className="w-full h-48 rounded-md border border-gray-200 bg-white shadow-inner touch-none"
onMouseDown={startDrawing}
onMouseMove={draw}
onMouseUp={endDrawing}
onMouseLeave={endDrawing}
onTouchStart={startDrawing}
onTouchMove={draw}
onTouchEnd={endDrawing}
/>
<div className="mt-3 flex items-center gap-3 text-xs text-gray-600">
<button
type="button"
onClick={clearSignature}
className="inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-50"
>
Clear
</button>
<span className="text-gray-500">Use mouse or touch to sign. A signature is required.</span>
{signatureDataUrl && <span className="text-green-600 font-medium">Captured</span>}
</div>
</div>
</div>
</section> </section>
<hr className="my-10 border-gray-200" /> <hr className="my-10 border-gray-200" />
{/* Confirmations */}
<section className="space-y-5"> <section className="space-y-5">
<h2 className="text-sm font-semibold text-[#0F2460]">Confirmations</h2> <h2 className="text-sm font-semibold text-[#0F2460]">Confirmations</h2>
<label className="flex items-start gap-3 text-sm text-gray-700"> <label className="flex items-start gap-3 text-sm text-gray-700">

View File

@ -1,6 +1,6 @@
'use client' 'use client'
import { useState, useEffect } from 'react' import { useState, useEffect, useRef } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import PageLayout from '../../../components/PageLayout' import PageLayout from '../../../components/PageLayout'
import useAuthStore from '../../../store/authStore' import useAuthStore from '../../../store/authStore'
@ -14,10 +14,8 @@ export default function PersonalSignContractPage() {
const { refreshStatus } = useUserStatus() const { refreshStatus } = useUserStatus()
const { showToast } = useToast() const { showToast } = useToast()
const [fullName, setFullName] = useState('')
const [location, setLocation] = useState('')
const [date, setDate] = useState('') const [date, setDate] = useState('')
const [note, setNote] = useState('') const [signatureDataUrl, setSignatureDataUrl] = useState('')
const [agreeContract, setAgreeContract] = useState(false) const [agreeContract, setAgreeContract] = useState(false)
const [agreeData, setAgreeData] = useState(false) const [agreeData, setAgreeData] = useState(false)
const [confirmSignature, setConfirmSignature] = useState(false) const [confirmSignature, setConfirmSignature] = useState(false)
@ -38,6 +36,89 @@ export default function PersonalSignContractPage() {
setDate(new Date().toISOString().slice(0, 10)) setDate(new Date().toISOString().slice(0, 10))
}, []) }, [])
const canvasRef = useRef<HTMLCanvasElement | null>(null)
const isDrawing = useRef(false)
const scaleRef = useRef(1)
const getPos = (e: React.MouseEvent<HTMLCanvasElement> | React.TouchEvent<HTMLCanvasElement>) => {
const canvas = canvasRef.current
if (!canvas) return { x: 0, y: 0 }
const rect = canvas.getBoundingClientRect()
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY
const x = (clientX - rect.left) * scaleRef.current
const y = (clientY - rect.top) * scaleRef.current
return { x, y }
}
// Size canvas to devicePixelRatio to avoid offset between cursor and strokes
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const resize = () => {
const rect = canvas.getBoundingClientRect()
const dpr = window.devicePixelRatio || 1
canvas.width = rect.width * dpr
canvas.height = rect.height * dpr
const ctx = canvas.getContext('2d')
if (ctx) {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
ctx.lineWidth = 2
ctx.lineCap = 'round'
ctx.strokeStyle = '#0f172a'
}
scaleRef.current = dpr
}
resize()
window.addEventListener('resize', resize)
return () => window.removeEventListener('resize', resize)
}, [])
const startDrawing = (e: React.MouseEvent<HTMLCanvasElement> | React.TouchEvent<HTMLCanvasElement>) => {
e.preventDefault()
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
ctx.lineWidth = 2
ctx.lineCap = 'round'
ctx.strokeStyle = '#0f172a'
const { x, y } = getPos(e)
ctx.beginPath()
ctx.moveTo(x, y)
isDrawing.current = true
}
const draw = (e: React.MouseEvent<HTMLCanvasElement> | React.TouchEvent<HTMLCanvasElement>) => {
e.preventDefault()
if (!isDrawing.current) return
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
const { x, y } = getPos(e)
ctx.lineTo(x, y)
ctx.stroke()
}
const endDrawing = () => {
if (!isDrawing.current) return
isDrawing.current = false
const canvas = canvasRef.current
if (!canvas) return
const dataUrl = canvas.toDataURL('image/png')
setSignatureDataUrl(dataUrl)
}
const clearSignature = () => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
ctx.clearRect(0, 0, canvas.width, canvas.height)
setSignatureDataUrl('')
}
const loadPreview = async (contractType: 'contract' | 'gdpr') => { const loadPreview = async (contractType: 'contract' | 'gdpr') => {
if (!accessToken) return if (!accessToken) return
setPreviewState((prev) => ({ setPreviewState((prev) => ({
@ -80,13 +161,12 @@ export default function PersonalSignContractPage() {
}, [accessToken]) }, [accessToken])
const valid = () => { const valid = () => {
const nameValid = fullName.trim().length >= 3 // Min 3 characters for name
const locationValid = location.trim().length >= 2 // Min 2 characters for location
const contractChecked = agreeContract const contractChecked = agreeContract
const dataChecked = agreeData const dataChecked = agreeData
const signatureChecked = confirmSignature const signatureChecked = confirmSignature
const signatureDrawn = !!signatureDataUrl
return nameValid && locationValid && contractChecked && dataChecked && signatureChecked return contractChecked && dataChecked && signatureChecked && signatureDrawn
} }
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
@ -94,11 +174,10 @@ export default function PersonalSignContractPage() {
if (!valid()) { if (!valid()) {
// Detailed error message to help debug // Detailed error message to help debug
const issues: string[] = [] const issues: string[] = []
if (fullName.trim().length < 3) issues.push('Full name (min 3 characters)')
if (location.trim().length < 2) issues.push('Location (min 2 characters)')
if (!agreeContract) issues.push('Contract read and understood') if (!agreeContract) issues.push('Contract read and understood')
if (!agreeData) issues.push('Privacy policy accepted') if (!agreeData) issues.push('Privacy policy accepted')
if (!confirmSignature) issues.push('Electronic signature confirmed') if (!confirmSignature) issues.push('Electronic signature confirmed')
if (!signatureDataUrl) issues.push('Signature captured on pad')
const msg = `Please complete: ${issues.join(', ')}` const msg = `Please complete: ${issues.join(', ')}`
setError(msg) setError(msg)
@ -126,21 +205,16 @@ export default function PersonalSignContractPage() {
try { try {
const contractData = { const contractData = {
fullName: fullName.trim(),
location: location.trim(),
date, date,
note: note.trim() || null, contractType: 'personal'
contractType: 'personal',
confirmations: {
agreeContract,
agreeData,
confirmSignature
}
} }
// Create FormData for the backend endpoint (no dummy PDF needed; server generates from templates) // Create FormData for the backend endpoint (no dummy PDF needed; server generates from templates)
const formData = new FormData() const formData = new FormData()
formData.append('contractData', JSON.stringify(contractData)) formData.append('contractData', JSON.stringify(contractData))
if (signatureDataUrl) {
formData.append('signatureImage', signatureDataUrl)
}
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/upload/contract/personal`, { const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/upload/contract/personal`, {
method: 'POST', method: 'POST',
@ -288,59 +362,39 @@ export default function PersonalSignContractPage() {
<hr className="my-10 border-gray-200" /> <hr className="my-10 border-gray-200" />
{/* Signature Fields */} {/* Signature Pad */}
<section> <section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-5">Signature Details</h2> <h2 className="text-sm font-semibold text-[#0F2460] mb-5">Signature</h2>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<div className="sm:col-span-2 lg:col-span-2"> <div className="">
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-2">
Full Name (Signature) * Draw Signature *
</label> </label>
<input <div className="rounded-lg border border-gray-200 bg-white p-4">
value={fullName} <canvas
onChange={e => { setFullName(e.target.value); setError('') }} ref={canvasRef}
placeholder="Vor- und Nachname" width={800}
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent" height={220}
required className="w-full h-48 rounded-md border border-gray-200 bg-white shadow-inner touch-none"
/> onMouseDown={startDrawing}
<p className="mt-1 text-xs text-gray-500"> onMouseMove={draw}
Must match your official ID. onMouseUp={endDrawing}
</p> onMouseLeave={endDrawing}
</div> onTouchStart={startDrawing}
<div> onTouchMove={draw}
<label className="block text-sm font-medium text-gray-700 mb-1"> onTouchEnd={endDrawing}
Date *
</label>
<input
type="date"
value={date}
onChange={e => setDate(e.target.value)}
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Location *
</label>
<input
value={location}
onChange={e => { setLocation(e.target.value); setError('') }}
placeholder="z.B. Berlin"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
/>
</div>
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Note (optional)
</label>
<input
value={note}
onChange={e => setNote(e.target.value)}
placeholder="Optionale zusätzliche Bemerkung"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/> />
<div className="mt-3 flex items-center gap-3 text-xs text-gray-600">
<button
type="button"
onClick={clearSignature}
className="inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-50"
>
Clear
</button>
<span className="text-gray-500">Use mouse or touch to sign. A signature is required.</span>
{signatureDataUrl && <span className="text-green-600 font-medium">Captured</span>}
</div>
</div> </div>
</div> </div>
</section> </section>