espace-paie-odentas/app/signer/[requestId]/[signerId]/components/CompletionScreen.tsx
odentas b790faf12c feat: Implémentation complète du système Odentas Sign
- 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)
2025-10-27 19:03:07 +01:00

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 é 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>
);
}