profit-planet-frontend/src/app/quickaction-dashboard/register-sign-contract/personal/page.tsx
2026-01-14 18:15:48 +01:00

505 lines
21 KiB
TypeScript

'use client'
import { useState, useEffect, useRef, useCallback } from 'react'
import { useRouter } from 'next/navigation'
import PageLayout from '../../../components/PageLayout'
import useAuthStore from '../../../store/authStore'
import { useUserStatus } from '../../../hooks/useUserStatus'
import { API_BASE_URL } from '../../../utils/api'
import { useToast } from '../../../components/toast/toastComponent'
export default function PersonalSignContractPage() {
const router = useRouter()
const user = useAuthStore(s => s.user) // NEW
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
const { accessToken } = useAuthStore()
const { userStatus, loading: statusLoading, refreshStatus } = useUserStatus() // CHANGED
const { showToast } = useToast()
const [date, setDate] = useState('')
const [signatureDataUrl, setSignatureDataUrl] = useState('')
const [agreeContract, setAgreeContract] = useState(false)
const [agreeData, setAgreeData] = useState(false)
const [confirmSignature, setConfirmSignature] = useState(false)
const [submitting, setSubmitting] = useState(false)
const [success, setSuccess] = useState(false)
const [error, setError] = useState('')
const [previewLoading, setPreviewLoading] = useState(false)
const [activeTab, setActiveTab] = useState<'contract' | 'gdpr'>('contract')
const [previewState, setPreviewState] = useState<{
contract: { loading: boolean; html: string | null; error: string | null }
gdpr: { loading: boolean; html: string | null; error: string | null }
}>({
contract: { loading: false, html: null, error: null },
gdpr: { loading: false, html: null, error: null }
})
useEffect(() => {
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') => {
if (!accessToken) return
setPreviewState((prev) => ({
...prev,
[contractType]: { ...prev[contractType], loading: true, error: null }
}))
try {
const qs = contractType ? `?contract_type=${contractType}` : ''
const res = await fetch(`${API_BASE_URL}/api/contracts/preview/latest${qs}`, {
method: 'GET',
headers: { Authorization: `Bearer ${accessToken}` },
credentials: 'include'
})
if (!res.ok) {
const text = await res.text().catch(() => '')
throw new Error(text || 'Failed to load contract preview')
}
const html = await res.text()
setPreviewState((prev) => ({
...prev,
[contractType]: { loading: false, html, error: null }
}))
} catch (e: any) {
console.error('PersonalSignContractPage.loadPreview error:', e)
setPreviewState((prev) => ({
...prev,
[contractType]: { loading: false, html: null, error: e?.message || 'Failed to load contract preview' }
}))
}
}
// Load latest contract and GDPR previews for personal user
useEffect(() => {
if (!accessToken) return
setPreviewLoading(true)
Promise.all([
loadPreview('contract'),
loadPreview('gdpr')
]).finally(() => setPreviewLoading(false))
}, [accessToken])
// NEW: hard block if step already done OR all steps done
const [redirectTo, setRedirectTo] = useState<string | null>(null)
const redirectOnceRef = useRef(false)
const smoothReplace = useCallback((to: string) => {
if (redirectOnceRef.current) return
redirectOnceRef.current = true
setRedirectTo(to)
window.setTimeout(() => router.replace(to), 200)
}, [router])
// NEW: must be logged in
useEffect(() => {
if (!isAuthReady) return
if (!user || !accessToken) smoothReplace('/login')
}, [isAuthReady, user, accessToken, smoothReplace])
useEffect(() => {
if (statusLoading || !userStatus) return
const allDone =
!!userStatus.email_verified &&
!!userStatus.documents_uploaded &&
!!userStatus.profile_completed &&
!!userStatus.contract_signed
if (allDone) {
smoothReplace('/dashboard') // CHANGED
} else if (userStatus.contract_signed) {
smoothReplace('/quickaction-dashboard') // CHANGED
}
}, [statusLoading, userStatus, smoothReplace])
const valid = () => {
const contractChecked = agreeContract
const dataChecked = agreeData
const signatureChecked = confirmSignature
const signatureDrawn = !!signatureDataUrl
return contractChecked && dataChecked && signatureChecked && signatureDrawn
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!valid()) {
// Detailed error message to help debug
const issues: string[] = []
if (!agreeContract) issues.push('Contract read and understood')
if (!agreeData) issues.push('Privacy policy accepted')
if (!confirmSignature) issues.push('Electronic signature confirmed')
if (!signatureDataUrl) issues.push('Signature captured on pad')
const msg = `Please complete: ${issues.join(', ')}`
setError(msg)
showToast({
variant: 'error',
title: 'Missing information',
message: msg,
})
return
}
if (!accessToken) {
const msg = 'Not authenticated. Please log in again.'
setError(msg)
showToast({
variant: 'error',
title: 'Authentication error',
message: msg,
})
return
}
setError('')
setSubmitting(true)
try {
const contractData = {
date,
contractType: 'personal'
}
// Create FormData for the backend endpoint (no dummy PDF needed; server generates from templates)
const formData = new FormData()
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`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`
// Don't set Content-Type, let browser set it for FormData
},
body: formData
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: 'Contract signing failed' }))
throw new Error(errorData.message || 'Contract signing failed')
}
setSuccess(true)
showToast({
variant: 'success',
title: 'Contract signed',
message: 'Your personal contract has been signed successfully.',
})
// Refresh user status to update contract signed state
await refreshStatus()
// Redirect to main dashboard after short delay
setTimeout(() => {
router.push('/dashboard')
}, 2000)
} catch (error: any) {
console.error('Contract signing error:', error)
const msg = error.message || 'Signature failed. Please try again.'
setError(msg)
showToast({
variant: 'error',
title: 'Signature failed',
message: msg,
})
} finally {
setSubmitting(false)
}
}
return (
<PageLayout>
{/* NEW: smooth redirect overlay */}
{redirectTo && (
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-white/70 backdrop-blur-sm transition-opacity duration-200 opacity-100">
<div className="rounded-xl bg-white px-5 py-4 shadow ring-1 ring-black/5">
<div className="text-sm font-medium text-gray-900">Redirecting</div>
<div className="mt-1 text-xs text-gray-600">Please wait</div>
</div>
</div>
)}
<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">
{/* 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-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" />
{/* Subtle radial highlight */}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.14),transparent_60%)]" />
</div>
<main className="relative z-10 flex flex-col flex-1 w-full px-5 sm:px-8 lg:px-12 py-12">
<form
onSubmit={handleSubmit}
className="relative max-w-5xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10"
>
<div className="px-6 py-8 sm:px-10 lg:px-14">
<h1 className="text-center text-2xl sm:text-3xl font-semibold text-[#0F172A] mb-2">
Sign Personal Participation Contract
</h1>
<p className="text-center text-sm text-gray-600 mb-8">
Please review the contract details and sign electronically.
</p>
{/* Contract Meta + Preview */}
<section className="grid gap-8 lg:grid-cols-2 mb-10">
<div className="space-y-4">
<div className="rounded-lg border border-gray-200 p-5 bg-gray-50">
<h2 className="text-sm font-semibold text-gray-800 mb-3">Contract Information</h2>
<ul className="space-y-2 text-xs sm:text-sm text-gray-600">
<li><span className="font-medium text-gray-700">Contract ID:</span> PERS-2024-001</li>
<li><span className="font-medium text-gray-700">Version:</span> 1.2 (valid from 01.11.2024)</li>
<li><span className="font-medium text-gray-700">Jurisdiction:</span> EU / Germany</li>
<li><span className="font-medium text-gray-700">Language:</span> EN (binding)</li>
</ul>
</div>
<div className="rounded-lg border border-indigo-100 bg-indigo-50/60 p-5">
<h3 className="text-sm font-semibold text-indigo-900 mb-2">Note</h3>
<p className="text-xs sm:text-sm text-indigo-800 leading-relaxed">
Your electronic signature is legally binding. Please ensure all details are correct.
</p>
</div>
</div>
<div>
<div className="rounded-lg border border-gray-200 bg-white relative overflow-hidden">
<div className="flex items-center justify-between p-3 border-b border-gray-200 bg-gray-50">
<div className="flex items-center gap-2 text-sm font-semibold text-gray-900">
<span>Contract Preview</span>
<div className="flex items-center gap-1 rounded-full border border-gray-200 bg-white px-1">
{(['contract','gdpr'] as const).map((tab) => (
<button
key={tab}
type="button"
onClick={() => setActiveTab(tab)}
className={`px-2.5 py-1 text-xs rounded-full transition ${activeTab === tab ? 'bg-indigo-600 text-white shadow' : 'text-gray-700 hover:bg-gray-100'}`}
>
{tab === 'contract' ? 'Contract' : 'GDPR'}
</button>
))}
</div>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={async () => {
const current = previewState[activeTab];
if (!current?.html) return
const blob = new Blob([current.html], { type: 'text/html' })
const url = URL.createObjectURL(blob)
window.open(url, '_blank', 'noopener,noreferrer')
}}
disabled={!previewState[activeTab]?.html}
className="inline-flex items-center rounded-md bg-gray-100 hover:bg-gray-200 text-gray-900 px-2.5 py-1.5 text-xs disabled:opacity-60"
>
Open in new tab
</button>
</div>
</div>
{previewLoading || previewState[activeTab].loading ? (
<div className="h-72 flex items-center justify-center text-xs text-gray-500">Loading preview</div>
) : previewState[activeTab].error ? (
<div className="h-72 flex items-center justify-center text-xs text-red-600 px-4 text-center">{previewState[activeTab].error}</div>
) : previewState[activeTab].html ? (
<iframe title={`Contract Preview ${activeTab}`} className="w-full h-72" srcDoc={previewState[activeTab].html || ''} />
) : (
<div className="h-72 flex items-center justify-center text-xs text-gray-500">No preview available.</div>
)}
</div>
</div>
</section>
<hr className="my-10 border-gray-200" />
{/* Signature Pad */}
<section>
<h2 className="text-sm font-semibold text-[#0F2460] mb-5">Signature</h2>
<div className="">
<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>
<hr className="my-10 border-gray-200" />
{/* Confirmations */}
<section className="space-y-5">
<h2 className="text-sm font-semibold text-[#0F2460]">Confirmations</h2>
<label className="flex items-start gap-3 text-sm text-gray-700">
<input
type="checkbox"
checked={agreeContract}
onChange={e => setAgreeContract(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span>I confirm that I have read and understood the contract in full.</span>
</label>
<label className="flex items-start gap-3 text-sm text-gray-700">
<input
type="checkbox"
checked={agreeData}
onChange={e => setAgreeData(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span>I consent to the processing of my personal data in accordance with the privacy policy.</span>
</label>
<label className="flex items-start gap-3 text-sm text-gray-700">
<input
type="checkbox"
checked={confirmSignature}
onChange={e => setConfirmSignature(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span>I confirm this electronic signature is legally binding and equivalent to a handwritten signature.</span>
</label>
</section>
{error && (
<div className="mt-8 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
{error}
</div>
)}
{success && (
<div className="mt-8 rounded-md border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
Contract signed successfully. Redirecting shortly
</div>
)}
<div className="mt-10 flex items-center justify-between">
<button
type="button"
onClick={() => router.push('/quickaction-dashboard')}
className="inline-flex items-center rounded-md border border-gray-300 px-4 py-2 text-sm font-semibold text-gray-700 bg-white hover:bg-gray-50"
>
Back to Dashboard
</button>
<button
type="submit"
disabled={submitting || success}
className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-8 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
>
{submitting ? 'Signing…' : success ? 'Signed' : 'Sign Now'}
</button>
</div>
</div>
</form>
</main>
</div>
</PageLayout>
)
}