'use client'; import { useEffect, useState, useRef } from 'react'; import { useParams } from 'next/navigation'; import { useTranslations } from 'next-intl'; 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 DOCUMENT_TYPE_KEYS = [ 'BILL_OF_LADING', 'PACKING_LIST', 'COMMERCIAL_INVOICE', 'CERTIFICATE_OF_ORIGIN', 'OTHER', ] as const; 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 t = useTranslations('carrierPortal.documents'); 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'; const getDocumentTypeLabel = (type: string): string => { if ((DOCUMENT_TYPE_KEYS as readonly string[]).includes(type)) { return t(`documentTypes.${type}` as any); } return type; }; // Check access requirements first const checkRequirements = async () => { if (!token) { setError(t('linkInvalid')); 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: `HTTP ${response.status}` }; } const errorMessage = errorData.message || t('loadError'); if (errorMessage.includes('introuvable') || errorMessage.includes('not found')) { throw new Error(t('bookingNotFound')); } throw new Error(errorMessage); } const reqData: AccessRequirements = await response.json(); setRequirements(reqData); if (reqData.status !== 'ACCEPTED') { setError(t('notAcceptedYet')); setLoading(false); return; } if (!reqData.requiresPassword) { await fetchDocumentsWithoutPassword(); } else { setLoading(false); } } catch (err) { console.error('Error checking requirements:', err); setError(err instanceof Error ? err.message : t('loadError')); 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: `HTTP ${response.status}` }; } const errorMessage = errorData.message || t('loadDocsError'); if ( errorMessage.includes('pas encore été acceptée') || errorMessage.includes('not accepted') ) { throw new Error(t('notAcceptedYet')); } else if (errorMessage.includes('introuvable') || errorMessage.includes('not found')) { throw new Error(t('bookingNotFound')); } else if ( errorMessage.includes('Mot de passe requis') || errorMessage.includes('required') ) { 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 : t('loadError')); 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: `HTTP ${response.status}` }; } const errorMessage = errorData.message || t('verifyError'); if ( response.status === 401 || errorMessage.includes('incorrect') || errorMessage.includes('invalid') ) { setPasswordError(t('passwordIncorrect')); 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 : t('verifyError')); setVerifying(false); } }; useEffect(() => { if (hasCalledApi.current) return; hasCalledApi.current = true; checkRequirements(); }, [token]); const handlePasswordSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!password.trim()) { setPasswordError(t('passwordMissing')); return; } fetchDocumentsWithPassword(password.trim()); }; const handleDownload = async (doc: Document) => { setDownloading(doc.id); try { window.open(doc.downloadUrl, '_blank'); } catch (err) { console.error('Error downloading document:', err); alert(t('downloadError')); } finally { 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 (

{t('loading')}

{t('loadingHint')}

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

{t('errorTitle')}

{error}

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

{t('password.title')}

{t('password.intro')}

{requirements.bookingNumber && (

{t('password.bookingLabel')}{' '} {requirements.bookingNumber}

)}
setPassword(e.target.value.toUpperCase())} placeholder={t('password.passwordPlaceholder')} 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}

)}

{t('password.helpTitle')}
{t('password.helpBody')}

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

{t('summary.bookingNumberPrefix')} {booking.bookingNumber}

)}

{t('summary.volume')}

{booking.volumeCBM} CBM

{t('summary.weight')}

{booking.weightKG} kg

{t('summary.transit')}

{t('summary.transitDays', { count: booking.transitDays })}

{t('summary.type')}

{booking.containerType}

{t('summary.carrierLabel')}{' '} {booking.carrierName} {t('summary.refLabel')}{' '} {booking.bookingNumber || booking.id.substring(0, 8).toUpperCase()}
{/* Documents Section */}

{t('list.heading', { count: documents.length })}

{documents.length === 0 ? (

{t('list.empty')}

{t('list.emptyHint')}

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

{doc.fileName}

{getDocumentTypeLabel(doc.type)} {formatFileSize(doc.size)}
))}
)}
{/* Info */}

{t('footerNote')}

{/* Footer */}
); }