- Remplacement de DocuSeal par solution souveraine Odentas Sign - Système d'authentification OTP pour signataires (bcryptjs + JWT) - 8 routes API: send-otp, verify-otp, sign, pdf-url, positions, status, webhook, signers - Interface moderne avec canvas de signature et animations (framer-motion, confetti) - Système de templates pour auto-détection des positions de signature (CDDU, RG, avenants) - PDF viewer avec @react-pdf-viewer (compatible Next.js) - Stockage S3: source/, signatures/, evidence/, signed/, certs/ - Tables Supabase: sign_requests, signers, sign_positions, sign_events, sign_assets - Evidence bundle automatique (JSON metadata + timestamps) - Templates emails: OTP et completion - Scripts Lambda prêts: pades-sign (KMS seal) et tsaStamp (RFC3161) - Mode test détecté automatiquement (emails whitelist) - Tests complets avec PDF CDDU réel (2 signataires)
247 lines
9.3 KiB
TypeScript
247 lines
9.3 KiB
TypeScript
'use client';
|
|
|
|
import { motion } from 'framer-motion';
|
|
import { CheckCircle, Download, Clock, Users, ArrowRight, Sparkles } from 'lucide-react';
|
|
import confetti from 'canvas-confetti';
|
|
import { useEffect } from 'react';
|
|
|
|
interface CompletionScreenProps {
|
|
signerName: string;
|
|
documentTitle: string;
|
|
documentRef: string;
|
|
signedAt: string | null;
|
|
progress: {
|
|
total: number;
|
|
signed: number;
|
|
percentage: number;
|
|
};
|
|
}
|
|
|
|
export default function CompletionScreen({
|
|
signerName,
|
|
documentTitle,
|
|
documentRef,
|
|
signedAt,
|
|
progress,
|
|
}: CompletionScreenProps) {
|
|
const isFullyCompleted = progress.signed === progress.total;
|
|
|
|
useEffect(() => {
|
|
// Launch confetti on mount
|
|
const duration = 2000;
|
|
const animationEnd = Date.now() + duration;
|
|
|
|
const randomInRange = (min: number, max: number) => {
|
|
return Math.random() * (max - min) + min;
|
|
};
|
|
|
|
const interval = setInterval(() => {
|
|
const timeLeft = animationEnd - Date.now();
|
|
|
|
if (timeLeft <= 0) {
|
|
clearInterval(interval);
|
|
return;
|
|
}
|
|
|
|
const particleCount = 50 * (timeLeft / duration);
|
|
|
|
confetti({
|
|
particleCount,
|
|
startVelocity: 30,
|
|
spread: 360,
|
|
origin: {
|
|
x: randomInRange(0.1, 0.9),
|
|
y: Math.random() - 0.2,
|
|
},
|
|
colors: ['#6366f1', '#8b5cf6', '#ec4899', '#f59e0b'],
|
|
});
|
|
}, 250);
|
|
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.95 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
transition={{ type: 'spring' }}
|
|
className="max-w-2xl mx-auto"
|
|
>
|
|
<div className="bg-white rounded-2xl shadow-xl overflow-hidden">
|
|
{/* Success header */}
|
|
<div className="bg-gradient-to-br from-green-500 via-emerald-500 to-teal-500 px-8 py-12 text-white text-center relative overflow-hidden">
|
|
<motion.div
|
|
initial={{ scale: 0 }}
|
|
animate={{ scale: 1 }}
|
|
transition={{ type: 'spring', delay: 0.2 }}
|
|
className="relative z-10"
|
|
>
|
|
<div className="w-24 h-24 bg-white/20 backdrop-blur-sm rounded-full flex items-center justify-center mx-auto mb-6">
|
|
<CheckCircle className="w-14 h-14" />
|
|
</div>
|
|
<h2 className="text-3xl font-bold mb-2">Signature enregistrée !</h2>
|
|
<p className="text-green-50 text-lg">
|
|
Merci {signerName.split(' ')[0]} 🎉
|
|
</p>
|
|
</motion.div>
|
|
|
|
{/* Animated sparkles */}
|
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
|
{[...Array(6)].map((_, i) => (
|
|
<motion.div
|
|
key={i}
|
|
initial={{ opacity: 0, y: 100, rotate: 0 }}
|
|
animate={{ opacity: [0, 1, 0], y: -100, rotate: 360 }}
|
|
transition={{
|
|
duration: 3,
|
|
delay: i * 0.3,
|
|
repeat: Infinity,
|
|
repeatDelay: 2,
|
|
}}
|
|
className="absolute"
|
|
style={{
|
|
left: `${20 + i * 12}%`,
|
|
top: '50%',
|
|
}}
|
|
>
|
|
<Sparkles className="w-6 h-6 text-yellow-300" />
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-8">
|
|
{/* Document info */}
|
|
<div className="bg-slate-50 rounded-xl p-6 mb-6">
|
|
<h3 className="text-sm font-medium text-slate-600 mb-3">Détails du document</h3>
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between items-start">
|
|
<span className="text-sm text-slate-600">Titre</span>
|
|
<span className="text-sm font-medium text-slate-900 text-right">{documentTitle}</span>
|
|
</div>
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-sm text-slate-600">Référence</span>
|
|
<span className="text-sm font-mono font-medium text-slate-900">{documentRef}</span>
|
|
</div>
|
|
{signedAt && (
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-sm text-slate-600">Date de signature</span>
|
|
<span className="text-sm font-medium text-slate-900">
|
|
{new Date(signedAt).toLocaleDateString('fr-FR', {
|
|
day: 'numeric',
|
|
month: 'long',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
})}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Progress indicator */}
|
|
<div className="bg-gradient-to-br from-indigo-50 to-purple-50 rounded-xl p-6 mb-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex items-center gap-2">
|
|
<Users className="w-5 h-5 text-indigo-600" />
|
|
<span className="font-semibold text-slate-900">Progression des signatures</span>
|
|
</div>
|
|
<span className="text-2xl font-bold text-indigo-600">
|
|
{progress.signed}/{progress.total}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Progress bar */}
|
|
<div className="w-full bg-white rounded-full h-3 overflow-hidden">
|
|
<motion.div
|
|
initial={{ width: 0 }}
|
|
animate={{ width: `${progress.percentage}%` }}
|
|
transition={{ duration: 1, ease: 'easeOut' }}
|
|
className="h-full bg-gradient-to-r from-indigo-500 to-purple-500 rounded-full"
|
|
/>
|
|
</div>
|
|
|
|
<p className="text-sm text-slate-600 mt-3">
|
|
{isFullyCompleted ? (
|
|
<span className="flex items-center gap-2 text-green-600 font-medium">
|
|
<CheckCircle className="w-4 h-4" />
|
|
Tous les signataires ont signé !
|
|
</span>
|
|
) : (
|
|
<>
|
|
{progress.total - progress.signed} signataire{progress.total - progress.signed > 1 ? 's' : ''} restant{progress.total - progress.signed > 1 ? 's' : ''}
|
|
</>
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Status message */}
|
|
{isFullyCompleted ? (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.3 }}
|
|
className="bg-green-50 border border-green-200 rounded-xl p-6 mb-6"
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<Clock className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
|
|
<div className="text-sm">
|
|
<p className="font-medium text-green-900 mb-1">Document finalisé</p>
|
|
<p className="text-green-700">
|
|
Le document est en cours de scellement cryptographique et d'horodatage. Vous recevrez une copie signée par email d'ici quelques instants.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
) : (
|
|
<div className="bg-blue-50 border border-blue-200 rounded-xl p-6 mb-6">
|
|
<div className="flex items-start gap-3">
|
|
<Clock className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
|
|
<div className="text-sm">
|
|
<p className="font-medium text-blue-900 mb-1">En attente des autres signatures</p>
|
|
<p className="text-blue-700">
|
|
Nous vous informerons par email dès que tous les signataires auront validé le document.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Actions */}
|
|
<div className="space-y-3">
|
|
{/* Download button (disabled for now) */}
|
|
<button
|
|
disabled
|
|
className="w-full px-6 py-4 bg-slate-100 text-slate-400 rounded-xl font-semibold cursor-not-allowed flex items-center justify-center gap-2"
|
|
>
|
|
<Download className="w-5 h-5" />
|
|
Télécharger le document signé
|
|
<span className="text-xs">(disponible après finalisation)</span>
|
|
</button>
|
|
|
|
{/* Close button */}
|
|
<button
|
|
onClick={() => window.location.href = '/'}
|
|
className="w-full px-6 py-4 bg-white border-2 border-slate-200 text-slate-700 rounded-xl font-semibold hover:bg-slate-50 transition-colors flex items-center justify-center gap-2"
|
|
>
|
|
Retour à l'accueil
|
|
<ArrowRight className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Security footer */}
|
|
<div className="mt-8 pt-6 border-t border-slate-200">
|
|
<div className="text-center text-sm text-slate-600">
|
|
<p className="font-medium text-slate-900 mb-2">🔒 Sécurité et conformité</p>
|
|
<p className="leading-relaxed">
|
|
Votre signature a été horodatée de manière sécurisée et sera archivée pendant 10 ans conformément à la réglementation eIDAS. Un certificat de preuve est automatiquement généré.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
}
|