'use client' import { useState, useEffect, useRef } 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 { accessToken } = useAuthStore() const { refreshStatus } = useUserStatus() 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 } }) const [previewsReady, setPreviewsReady] = useState(false) 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) { await res.text().catch(() => null) throw new Error('No contract available at this moment, please contact us.') } const html = await res.text() setPreviewState((prev) => ({ ...prev, [contractType]: { loading: false, html, error: null } })) } catch (e: unknown) { console.error('PersonalSignContractPage.loadPreview error:', e) setPreviewState((prev) => ({ ...prev, [contractType]: { loading: false, html: null, error: 'No contract available at this moment, please contact us.' } })) } } // 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]) useEffect(() => { const doneLoading = !previewState.contract.loading && !previewState.gdpr.loading && !previewLoading const anyAvailable = !!previewState.contract.html || !!previewState.gdpr.html const blockingMsg = 'Temporarily unable to sign contracts. No active documents are available at this moment.' if (doneLoading) { setPreviewsReady(true) } if (doneLoading) { if (anyAvailable) { setError((prev) => (prev === blockingMsg ? '' : prev)) } else { setError(blockingMsg) } // If one preview is missing, default to showing the available one if (!previewState.contract.html && previewState.gdpr.html) { setActiveTab('gdpr') } else if (previewState.contract.html && !previewState.gdpr.html) { setActiveTab('contract') } } }, [previewState, previewLoading]) const valid = () => { const contractAvailable = !!previewState.contract.html const gdprAvailable = !!previewState.gdpr.html // Only require acknowledgements for documents that actually exist const contractChecked = contractAvailable ? agreeContract : true const dataChecked = gdprAvailable ? agreeData : true const signatureChecked = confirmSignature const signatureDrawn = !!signatureDataUrl const anyPreview = contractAvailable || gdprAvailable return contractChecked && dataChecked && signatureChecked && signatureDrawn && anyPreview } const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!valid()) { // Detailed error message to help debug const issues: string[] = [] const contractAvailable = !!previewState.contract.html const gdprAvailable = !!previewState.gdpr.html if (!contractAvailable && !gdprAvailable) { const msg = 'Temporarily unable to sign contracts. No active documents are available at this moment.' setError(msg) showToast({ variant: 'error', title: 'No documents available', message: msg, }) return } if (contractAvailable && !agreeContract) issues.push('Contract read and understood') if (gdprAvailable && !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(() => { // Check if we came from tutorial const urlParams = new URLSearchParams(window.location.search) const fromTutorial = urlParams.get('tutorial') === 'true' if (fromTutorial) { router.push('/quickaction-dashboard?tutorial=true') } else { router.push('/quickaction-dashboard') } }, 2000) } catch (error: unknown) { console.error('Contract signing error:', error) const msg = error instanceof Error ? (error.message || 'Signature failed. Please try again.') : 'Signature failed. Please try again.' setError(msg) showToast({ variant: 'error', title: 'Signature failed', message: msg, }) } finally { setSubmitting(false) } } return (
{/* 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 */}

Document Information

{(['contract','gdpr'] as const).map((tab) => ( ))}
{(() => { const meta = activeTab === 'contract' ? { id: 'PERS-2025-001', title: 'VERTRIEBSPARTNER / BUSINESSPARTNER / AFFILIATE - VERTRAG', version: 'idF 21.05.2025', jurisdiction: 'EU / Austria (Graz)', language: 'DE (binding)', issuer: 'Profit Planet GmbH', address: 'Liebenauer Hauptstraße 82c, A-8041 Graz' } : { id: 'PERS-GDPR-2025-001', title: 'SUB-AUFTRAGSVERARBEITUNGS-VERTRAG', version: 'Art. 28 Abs. 3 DSGVO', jurisdiction: 'EU / Austria (Graz)', language: 'DE (binding)', issuer: 'Profit Planet GmbH', address: 'Liebenauer Hauptstraße 82c, A-8041 Graz' } return (
  • Document: {meta.title}
  • ID: {meta.id}
  • Version / Basis: {meta.version}
  • Jurisdiction: {meta.jurisdiction}
  • Language: {meta.language}
  • Issuer: {meta.issuer}
  • Address: {meta.address}
) })()}

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 ? (