'use client'; import { useEffect, useState, useRef } from 'react'; import { useParams } from 'next/navigation'; import Image from 'next/image'; import { FileText, Download, Loader2, XCircle, Package, Ship, Clock, AlertCircle, ArrowRight, Lock, Eye, EyeOff, } from 'lucide-react'; interface Document { id: string; type: string; fileName: string; mimeType: string; size: number; downloadUrl: string; } interface BookingSummary { id: string; bookingNumber?: string; carrierName: string; origin: string; destination: string; routeDescription: string; volumeCBM: number; weightKG: number; palletCount: number; price: number; currency: string; transitDays: number; containerType: string; acceptedAt: string; } interface CarrierDocumentsData { booking: BookingSummary; documents: Document[]; } interface AccessRequirements { requiresPassword: boolean; bookingNumber?: string; status: string; } const documentTypeLabels: Record = { BILL_OF_LADING: 'Connaissement', PACKING_LIST: 'Liste de colisage', COMMERCIAL_INVOICE: 'Facture commerciale', CERTIFICATE_OF_ORIGIN: "Certificat d'origine", OTHER: 'Autre document', }; const formatFileSize = (bytes: number): string => { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }; const getFileIcon = (mimeType: string) => { if (mimeType.includes('pdf')) return '📄'; if (mimeType.includes('image')) return '🖼️'; if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) return '📊'; if (mimeType.includes('word') || mimeType.includes('document')) return '📝'; return '📎'; }; export default function CarrierDocumentsPage() { const params = useParams(); const token = params.token as string; const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [data, setData] = useState(null); const [downloading, setDownloading] = useState(null); // Password protection state const [requirements, setRequirements] = useState(null); const [password, setPassword] = useState(''); const [showPassword, setShowPassword] = useState(false); const [passwordError, setPasswordError] = useState(null); const [verifying, setVerifying] = useState(false); const hasCalledApi = useRef(false); const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'; // Check access requirements first const checkRequirements = async () => { if (!token) { setError('Lien invalide'); setLoading(false); return; } try { const response = await fetch( `${apiUrl}/api/v1/csv-booking-actions/documents/${token}/requirements`, { method: 'GET', headers: { 'Content-Type': 'application/json', }, } ); if (!response.ok) { let errorData; try { errorData = await response.json(); } catch { errorData = { message: `Erreur HTTP ${response.status}` }; } const errorMessage = errorData.message || 'Erreur lors du chargement'; if (errorMessage.includes('introuvable') || errorMessage.includes('not found')) { throw new Error('Réservation introuvable. Vérifiez que le lien est correct.'); } throw new Error(errorMessage); } const reqData: AccessRequirements = await response.json(); setRequirements(reqData); // If booking is not accepted yet if (reqData.status !== 'ACCEPTED') { setError( "Cette réservation n'a pas encore été acceptée. Les documents seront disponibles après l'acceptation." ); setLoading(false); return; } // If no password required, fetch documents directly if (!reqData.requiresPassword) { await fetchDocumentsWithoutPassword(); } else { setLoading(false); } } catch (err) { console.error('Error checking requirements:', err); setError(err instanceof Error ? err.message : 'Erreur lors du chargement'); setLoading(false); } }; // Fetch documents without password (legacy bookings) const fetchDocumentsWithoutPassword = async () => { try { const response = await fetch(`${apiUrl}/api/v1/csv-booking-actions/documents/${token}`, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); if (!response.ok) { let errorData; try { errorData = await response.json(); } catch { errorData = { message: `Erreur HTTP ${response.status}` }; } const errorMessage = errorData.message || 'Erreur lors du chargement des documents'; if ( errorMessage.includes('pas encore été acceptée') || errorMessage.includes('not accepted') ) { throw new Error( "Cette réservation n'a pas encore été acceptée. Les documents seront disponibles après l'acceptation." ); } else if (errorMessage.includes('introuvable') || errorMessage.includes('not found')) { throw new Error('Réservation introuvable. Vérifiez que le lien est correct.'); } else if (errorMessage.includes('Mot de passe requis') || errorMessage.includes('required')) { // Password is now required, show the form setRequirements({ requiresPassword: true, status: 'ACCEPTED' }); setLoading(false); return; } throw new Error(errorMessage); } const responseData = await response.json(); setData(responseData); setLoading(false); } catch (err) { console.error('Error fetching documents:', err); setError(err instanceof Error ? err.message : 'Erreur lors du chargement'); setLoading(false); } }; // Fetch documents with password const fetchDocumentsWithPassword = async (pwd: string) => { setVerifying(true); setPasswordError(null); try { const response = await fetch(`${apiUrl}/api/v1/csv-booking-actions/documents/${token}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ password: pwd }), }); if (!response.ok) { let errorData; try { errorData = await response.json(); } catch { errorData = { message: `Erreur HTTP ${response.status}` }; } const errorMessage = errorData.message || 'Erreur lors de la vérification'; if ( response.status === 401 || errorMessage.includes('incorrect') || errorMessage.includes('invalid') ) { setPasswordError('Mot de passe incorrect. Vérifiez votre email pour retrouver le mot de passe.'); setVerifying(false); return; } throw new Error(errorMessage); } const responseData = await response.json(); setData(responseData); setVerifying(false); } catch (err) { console.error('Error verifying password:', err); setPasswordError(err instanceof Error ? err.message : 'Erreur lors de la vérification'); setVerifying(false); } }; useEffect(() => { if (hasCalledApi.current) return; hasCalledApi.current = true; checkRequirements(); }, [token]); const handlePasswordSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!password.trim()) { setPasswordError('Veuillez entrer le mot de passe'); return; } fetchDocumentsWithPassword(password.trim()); }; const handleDownload = async (doc: Document) => { setDownloading(doc.id); try { // The downloadUrl is already a signed URL, open it directly window.open(doc.downloadUrl, '_blank'); } catch (err) { console.error('Error downloading document:', err); alert('Erreur lors du téléchargement. Veuillez réessayer.'); } finally { // Small delay to show loading state setTimeout(() => setDownloading(null), 500); } }; const handleRefresh = () => { setLoading(true); setError(null); setData(null); setRequirements(null); setPassword(''); setPasswordError(null); hasCalledApi.current = false; checkRequirements(); }; // Loading state if (loading) { return (

Chargement...

Veuillez patienter

); } // Error state if (error) { return (

Erreur

{error}

); } // Password form state if (requirements?.requiresPassword && !data) { return (

Accès sécurisé

Cette page est protégée. Entrez le mot de passe reçu par email pour accéder aux documents.

{requirements.bookingNumber && (

Réservation: {requirements.bookingNumber}

)}
setPassword(e.target.value.toUpperCase())} placeholder="Ex: A3B7K9" className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-turquoise focus:border-brand-turquoise text-center text-xl tracking-widest font-mono uppercase" autoComplete="off" autoFocus />
{passwordError && (

{passwordError}

)}

Où trouver le mot de passe ?
Le mot de passe vous a été envoyé dans l'email de confirmation de la réservation. Il correspond aux 6 derniers caractères du numéro de devis.

); } if (!data) return null; const { booking, documents } = data; return (
{/* Header */}
Xpeditis
{/* Booking Summary Card */}
{booking.origin} {booking.destination}
{booking.bookingNumber && (

N° {booking.bookingNumber}

)}

Volume

{booking.volumeCBM} CBM

Poids

{booking.weightKG} kg

Transit

{booking.transitDays} jours

Type

{booking.containerType}

Transporteur:{' '} {booking.carrierName} Ref:{' '} {booking.bookingNumber || booking.id.substring(0, 8).toUpperCase()}
{/* Documents Section */}

Documents ({documents.length})

{documents.length === 0 ? (

Aucun document disponible pour le moment.

Les documents apparaîtront ici une fois ajoutés.

) : (
{documents.map(doc => (
{getFileIcon(doc.mimeType)}

{doc.fileName}

{documentTypeLabels[doc.type] || doc.type} {formatFileSize(doc.size)}
))}
)}
{/* Info */}

Cette page affiche toujours les documents les plus récents de la réservation.

{/* Footer */}
); }