311 lines
13 KiB
TypeScript
311 lines
13 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { Shield, Check, Loader2, Lock, FileCheck, Info } from 'lucide-react';
|
|
import OTPVerification from '@/app/signer/[requestId]/[signerId]/components/OTPVerification';
|
|
import SignatureCapture from '@/app/signer/[requestId]/[signerId]/components/SignatureCapture';
|
|
import CompletionScreen from '@/app/signer/[requestId]/[signerId]/components/CompletionScreen';
|
|
import ProgressBar from '@/app/signer/[requestId]/[signerId]/components/ProgressBar';
|
|
|
|
type SignerStatus = {
|
|
id: string;
|
|
role: string;
|
|
name: string;
|
|
email: string;
|
|
has_signed: boolean;
|
|
};
|
|
|
|
type RequestInfo = {
|
|
id: string;
|
|
ref: string;
|
|
title: string;
|
|
status: string;
|
|
progress: {
|
|
total: number;
|
|
signed: number;
|
|
percentage: number;
|
|
};
|
|
};
|
|
|
|
export default function SignerPage({
|
|
params,
|
|
}: {
|
|
params: { requestId: string; signerId: string };
|
|
}) {
|
|
const router = useRouter();
|
|
const [currentStep, setCurrentStep] = useState<'loading' | 'otp' | 'signature' | 'completed'>('loading');
|
|
const [sessionToken, setSessionToken] = useState<string | null>(null);
|
|
const [signerInfo, setSignerInfo] = useState<SignerStatus | null>(null);
|
|
const [requestInfo, setRequestInfo] = useState<RequestInfo | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Charger les infos du signataire au démarrage
|
|
useEffect(() => {
|
|
loadSignerStatus();
|
|
}, []);
|
|
|
|
async function loadSignerStatus() {
|
|
try {
|
|
const response = await fetch(
|
|
`/api/odentas-sign/signers/${params.signerId}/status`
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Impossible de charger les informations');
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.signer.has_signed) {
|
|
// Déjà signé
|
|
setSignerInfo(data.signer);
|
|
setRequestInfo(data.request);
|
|
setCurrentStep('completed');
|
|
} else {
|
|
// Pas encore signé, commencer par l'OTP
|
|
setSignerInfo(data.signer);
|
|
setRequestInfo(data.request);
|
|
setCurrentStep('otp');
|
|
}
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Erreur inconnue');
|
|
setCurrentStep('otp'); // Fallback vers OTP
|
|
}
|
|
}
|
|
|
|
function handleOTPVerified(token: string, signer: any, request: any) {
|
|
setSessionToken(token);
|
|
setSignerInfo(signer);
|
|
setRequestInfo(request);
|
|
setCurrentStep('signature');
|
|
}
|
|
|
|
async function handleSignatureCompleted() {
|
|
// Recharger le statut pour obtenir les infos à jour
|
|
await loadSignerStatus();
|
|
setCurrentStep('completed');
|
|
}
|
|
|
|
if (error && !signerInfo) {
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 flex items-center justify-center p-4">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="bg-white rounded-2xl shadow-xl p-8 max-w-md w-full"
|
|
>
|
|
<div className="text-center">
|
|
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<Shield className="w-8 h-8 text-red-600" />
|
|
</div>
|
|
<h2 className="text-2xl font-bold text-slate-900 mb-2">
|
|
Erreur de chargement
|
|
</h2>
|
|
<p className="text-slate-600 mb-6">{error}</p>
|
|
<button
|
|
onClick={() => router.push('/')}
|
|
className="px-6 py-3 bg-slate-900 text-white rounded-xl font-medium hover:bg-slate-800 transition-colors"
|
|
>
|
|
Retour à l'accueil
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (currentStep === 'loading') {
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 flex items-center justify-center">
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
className="text-center"
|
|
>
|
|
<Loader2 className="w-12 h-12 text-indigo-600 animate-spin mx-auto mb-4" />
|
|
<p className="text-slate-600 font-medium">Chargement...</p>
|
|
</motion.div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-white">
|
|
{/* Header épuré style Espace Paie */}
|
|
<header className="bg-white border-b border-slate-200">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div className="flex items-center justify-between h-16">
|
|
<div className="flex items-center gap-3">
|
|
<Shield className="w-6 h-6 text-indigo-600" />
|
|
<div className="h-8 w-px bg-slate-200" />
|
|
<div>
|
|
<h1 className="text-base font-semibold text-slate-900">Signature Électronique</h1>
|
|
</div>
|
|
</div>
|
|
|
|
{requestInfo && (
|
|
<div className="flex items-center gap-2 text-sm text-slate-600">
|
|
<span className="hidden sm:inline">Réf.</span>
|
|
<span className="font-mono font-medium text-slate-900">{requestInfo.ref}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Layout 2 colonnes */}
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
{/* Colonne principale - Formulaire de signature */}
|
|
<div className="lg:col-span-2">
|
|
{/* Progress steps */}
|
|
{currentStep !== 'completed' && (
|
|
<div className="mb-6">
|
|
<div className="flex items-center gap-4">
|
|
<div className={`flex items-center gap-2 ${currentStep === 'otp' ? 'text-indigo-600' : 'text-slate-400'}`}>
|
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold ${
|
|
currentStep === 'otp' ? 'bg-indigo-600 text-white' : 'bg-slate-200 text-slate-600'
|
|
}`}>
|
|
1
|
|
</div>
|
|
<span className="text-sm font-medium">Vérification</span>
|
|
</div>
|
|
<div className="flex-1 h-px bg-slate-200" />
|
|
<div className={`flex items-center gap-2 ${currentStep === 'signature' ? 'text-indigo-600' : 'text-slate-400'}`}>
|
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold ${
|
|
currentStep === 'signature' ? 'bg-indigo-600 text-white' : 'bg-slate-200 text-slate-600'
|
|
}`}>
|
|
2
|
|
</div>
|
|
<span className="text-sm font-medium">Signature</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Contenu principal avec transitions */}
|
|
<AnimatePresence mode="wait">
|
|
{currentStep === 'otp' && signerInfo && (
|
|
<OTPVerification
|
|
key="otp"
|
|
signerId={params.signerId}
|
|
signerName={signerInfo.name}
|
|
signerEmail={signerInfo.email}
|
|
documentTitle={requestInfo?.title || ''}
|
|
onVerified={handleOTPVerified}
|
|
/>
|
|
)}
|
|
|
|
{currentStep === 'signature' && signerInfo && sessionToken && requestInfo && (
|
|
<SignatureCapture
|
|
key="signature"
|
|
signerId={params.signerId}
|
|
requestId={params.requestId}
|
|
signerName={signerInfo.name}
|
|
signerRole={signerInfo.role}
|
|
documentTitle={requestInfo.title}
|
|
sessionToken={sessionToken}
|
|
onCompleted={handleSignatureCompleted}
|
|
/>
|
|
)}
|
|
|
|
{currentStep === 'completed' && signerInfo && requestInfo && (
|
|
<CompletionScreen
|
|
key="completed"
|
|
signerName={signerInfo.name}
|
|
documentTitle={requestInfo.title}
|
|
documentRef={requestInfo.ref}
|
|
signedAt={signerInfo.has_signed ? new Date().toISOString() : null}
|
|
progress={requestInfo.progress}
|
|
/>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
|
|
{/* Sidebar informative */}
|
|
<div className="lg:col-span-1">
|
|
<div className="sticky top-24 space-y-6">
|
|
{/* Carte sécurité */}
|
|
<div className="bg-slate-50 rounded-lg border border-slate-200 p-6">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<div className="w-10 h-10 rounded-lg bg-indigo-600 flex items-center justify-center">
|
|
<Lock className="w-5 h-5 text-white" />
|
|
</div>
|
|
<h3 className="font-semibold text-slate-900">Signature Sécurisée</h3>
|
|
</div>
|
|
<div className="space-y-4 text-sm text-slate-600">
|
|
<div className="flex items-start gap-3">
|
|
<Check className="w-4 h-4 text-green-600 flex-shrink-0 mt-0.5" />
|
|
<p>Conforme au règlement <strong className="text-slate-900">eIDAS</strong></p>
|
|
</div>
|
|
<div className="flex items-start gap-3">
|
|
<Check className="w-4 h-4 text-green-600 flex-shrink-0 mt-0.5" />
|
|
<p>Norme <strong className="text-slate-900">PAdES-BASELINE-B</strong> (ETSI)</p>
|
|
</div>
|
|
<div className="flex items-start gap-3">
|
|
<Check className="w-4 h-4 text-green-600 flex-shrink-0 mt-0.5" />
|
|
<p>Archivage sécurisé <strong className="text-slate-900">10 ans</strong></p>
|
|
</div>
|
|
<div className="flex items-start gap-3">
|
|
<Check className="w-4 h-4 text-green-600 flex-shrink-0 mt-0.5" />
|
|
<p>Chiffrement <strong className="text-slate-900">AES-256</strong></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Carte informations */}
|
|
<div className="bg-slate-50 rounded-lg border border-slate-200 p-6">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<div className="w-10 h-10 rounded-lg bg-slate-200 flex items-center justify-center">
|
|
<Info className="w-5 h-5 text-slate-600" />
|
|
</div>
|
|
<h3 className="font-semibold text-slate-900">Comment ça marche ?</h3>
|
|
</div>
|
|
<ol className="space-y-3 text-sm text-slate-600">
|
|
<li className="flex gap-3">
|
|
<span className="flex-shrink-0 w-5 h-5 rounded-full bg-indigo-100 text-indigo-600 flex items-center justify-center text-xs font-bold">1</span>
|
|
<span>Vous recevez un code par email</span>
|
|
</li>
|
|
<li className="flex gap-3">
|
|
<span className="flex-shrink-0 w-5 h-5 rounded-full bg-indigo-100 text-indigo-600 flex items-center justify-center text-xs font-bold">2</span>
|
|
<span>Vous saisissez votre signature</span>
|
|
</li>
|
|
<li className="flex gap-3">
|
|
<span className="flex-shrink-0 w-5 h-5 rounded-full bg-indigo-100 text-indigo-600 flex items-center justify-center text-xs font-bold">3</span>
|
|
<span>Le document est scellé électroniquement</span>
|
|
</li>
|
|
<li className="flex gap-3">
|
|
<span className="flex-shrink-0 w-5 h-5 rounded-full bg-indigo-100 text-indigo-600 flex items-center justify-center text-xs font-bold">4</span>
|
|
<span>Vous recevez une copie signée</span>
|
|
</li>
|
|
</ol>
|
|
</div>
|
|
|
|
{/* Support */}
|
|
<div className="text-center text-sm text-slate-600">
|
|
<p className="mb-2">Besoin d'aide ?</p>
|
|
<a
|
|
href="mailto:support@odentas.fr"
|
|
className="inline-flex items-center gap-2 text-indigo-600 hover:text-indigo-700 font-medium"
|
|
>
|
|
Contactez le support
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer minimaliste */}
|
|
<footer className="mt-16 border-t border-slate-200 bg-slate-50">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
|
<div className="text-center text-xs text-slate-500">
|
|
<p>© 2025 Odentas Media SAS - Tous droits réservés</p>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
);
|
|
}
|