- 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)
171 lines
5 KiB
TypeScript
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>
|
|
);
|
|
}
|