'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(null) const isDrawing = useRef(false) const scaleRef = useRef(1) const getPos = (e: React.MouseEvent | React.TouchEvent) => { 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 | React.TouchEvent) => { 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 | React.TouchEvent) => { 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(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 ( {/* NEW: smooth redirect overlay */} {redirectTo && (
Redirecting…
Please wait
)}
{/* Animated background (same as dashboard) */}
{/* Soft gradient blobs */}
{/* Subtle radial highlight */}

Sign Personal Participation Contract

Please review the contract details and sign electronically.

{/* Contract Meta + Preview */}

Contract Information

  • Contract ID: PERS-2024-001
  • Version: 1.2 (valid from 01.11.2024)
  • Jurisdiction: EU / Germany
  • Language: EN (binding)

Note

Your electronic signature is legally binding. Please ensure all details are correct.

Contract Preview
{(['contract','gdpr'] as const).map((tab) => ( ))}
{previewLoading || previewState[activeTab].loading ? (
Loading preview…
) : previewState[activeTab].error ? (
{previewState[activeTab].error}
) : previewState[activeTab].html ? (