espace-paie-odentas/app/signer/[requestId]/[signerId]/components/PDFImageViewer.tsx
odentas 59749d481b feat: Migration Cloudinary vers Poppler pour conversion PDF→JPEG
- Remplacer Cloudinary (US) par solution 100% AWS eu-west-3
- Lambda odentas-sign-pdf-converter avec pdftoppm
- Lambda Layer poppler-utils v5 avec dépendances complètes
- Trigger S3 ObjectCreated pour conversion automatique
- Support multi-pages validé (PDF 3 pages)
- Stockage images dans S3 odentas-docs
- PDFImageViewer pour affichage images converties
- Conformité RGPD garantie (données EU uniquement)
2025-10-28 10:22:45 +01:00

171 lines
5 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { Loader2, AlertCircle } from 'lucide-react';
interface SignPosition {
page: number;
x: number;
y: number;
width: number;
height: number;
role: string;
}
interface PDFImageViewerProps {
pdfUrl: string;
positions: SignPosition[];
currentSignerRole: string;
requestId: string;
sessionToken: string;
}
interface PageImage {
pageNumber: number;
imageUrl: string;
width: number;
height: number;
}
export default function PDFImageViewer({
pdfUrl,
positions,
currentSignerRole,
requestId,
sessionToken,
}: PDFImageViewerProps) {
const [pageImages, setPageImages] = useState<PageImage[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function convertPdfToImages() {
try {
setIsLoading(true);
setError(null);
console.log('[PDFImageViewer] Début conversion', {
requestId,
pdfUrl,
hasSessionToken: !!sessionToken,
sessionTokenLength: sessionToken?.length,
sessionTokenPreview: sessionToken?.substring(0, 20) + '...',
});
// Appel API pour convertir le PDF en images
const response = await fetch(`/api/odentas-sign/requests/${requestId}/pdf-to-images`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${sessionToken}`,
},
body: JSON.stringify({
pdfUrl,
}),
});
if (!response.ok) {
const data = await response.json();
console.error('[PDFImageViewer] Erreur response:', {
status: response.status,
statusText: response.statusText,
data,
});
throw new Error(data.error || 'Erreur lors de la conversion du PDF');
}
const data = await response.json();
setPageImages(data.pages || []);
} catch (err) {
console.error('[PDFImageViewer] Erreur:', err);
setError(err instanceof Error ? err.message : 'Erreur de chargement');
} finally {
setIsLoading(false);
}
}
if (pdfUrl) {
convertPdfToImages();
}
}, [pdfUrl, requestId, sessionToken]);
if (isLoading) {
return (
<div className="h-full bg-slate-50 rounded-xl flex flex-col items-center justify-center">
<Loader2 className="w-12 h-12 text-indigo-600 animate-spin mb-4" />
<p className="text-slate-600 font-medium">Conversion du PDF en cours...</p>
</div>
);
}
if (error) {
return (
<div className="h-full bg-red-50 rounded-xl flex flex-col items-center justify-center p-6">
<AlertCircle className="w-12 h-12 text-red-500 mb-4" />
<p className="text-red-700 font-medium mb-2">Erreur de chargement</p>
<p className="text-red-600 text-sm text-center">{error}</p>
</div>
);
}
if (pageImages.length === 0) {
return (
<div className="h-full bg-slate-50 rounded-xl flex flex-col items-center justify-center">
<p className="text-slate-500">Aucune page à afficher</p>
</div>
);
}
return (
<div className="h-full overflow-y-auto space-y-4">
{pageImages.map((page) => {
// Filtrer les positions de signature pour cette page et ce rôle
const pagePositions = positions.filter(
(pos) => pos.page === page.pageNumber && pos.role === currentSignerRole
);
return (
<div
key={page.pageNumber}
className="relative bg-white border border-slate-200 rounded-lg overflow-hidden"
style={{
aspectRatio: `${page.width} / ${page.height}`,
}}
>
{/* Image de la page PDF */}
<img
src={page.imageUrl}
alt={`Page ${page.pageNumber}`}
className="w-full h-full object-contain"
/>
{/* Zones de signature superposées */}
{pagePositions.map((pos, idx) => (
<div
key={idx}
className="absolute border-2 border-dashed border-indigo-500 bg-indigo-100/30 pointer-events-none"
style={{
left: `${pos.x * 100}%`,
top: `${pos.y * 100}%`,
width: `${pos.width * 100}%`,
height: `${pos.height * 100}%`,
}}
>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-xs font-semibold text-indigo-700 bg-white/80 px-2 py-1 rounded">
Signez ici
</span>
</div>
</div>
))}
{/* Numéro de page */}
<div className="absolute bottom-2 right-2 bg-slate-900/70 text-white text-xs px-2 py-1 rounded">
Page {page.pageNumber}
</div>
</div>
);
})}
</div>
);
}