'use client'; import { useState, useEffect, useCallback, useRef } from 'react'; import { useTranslations, useLocale } from 'next-intl'; import { listCsvBookings, CsvBookingResponse } from '@/lib/api/bookings'; import { FileText, Image as ImageIcon, FileEdit, FileSpreadsheet, Paperclip } from 'lucide-react'; import type { ReactNode } from 'react'; import ExportButton from '@/components/ExportButton'; import { PageHeader } from '@/components/ui/PageHeader'; interface Document { id: string; fileName: string; filePath: string; type: string; mimeType: string; size: number; uploadedAt?: Date; name?: string; url?: string; } interface DocumentWithBooking extends Document { bookingId: string; quoteNumber: string; route: string; status: string; carrierName: string; fileType?: string; } export default function UserDocumentsPage() { const t = useTranslations('dashboard.userDocuments'); const locale = useLocale(); const [bookings, setBookings] = useState([]); const [documents, setDocuments] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [searchTerm, setSearchTerm] = useState(''); const [filterStatus, setFilterStatus] = useState('all'); const [filterQuoteNumber, setFilterQuoteNumber] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(10); const [showAddModal, setShowAddModal] = useState(false); const [selectedBookingId, setSelectedBookingId] = useState(null); const [uploadingFiles, setUploadingFiles] = useState(false); const fileInputRef = useRef(null); const [showReplaceModal, setShowReplaceModal] = useState(false); const [documentToReplace, setDocumentToReplace] = useState(null); const [replacingFile, setReplacingFile] = useState(false); const replaceFileInputRef = useRef(null); const [openDropdownId, setOpenDropdownId] = useState(null); const getQuoteNumber = (booking: CsvBookingResponse): string => { return `#${booking.bookingId || booking.id.slice(0, 8).toUpperCase()}`; }; const getFileType = (fileName: string): string => { const ext = fileName.split('.').pop()?.toLowerCase() || ''; const typeMap: Record = { pdf: 'PDF', doc: 'Word', docx: 'Word', xls: 'Excel', xlsx: 'Excel', jpg: 'Image', jpeg: 'Image', png: 'Image', gif: 'Image', txt: 'Text', csv: 'CSV', }; return typeMap[ext] || ext.toUpperCase(); }; const fetchBookingsAndDocuments = useCallback(async () => { try { setLoading(true); const response = await listCsvBookings({ page: 1, limit: 1000 }); const allBookings = response.bookings || []; setBookings(allBookings); const allDocuments: DocumentWithBooking[] = []; allBookings.forEach((booking: CsvBookingResponse) => { if (booking.documents && booking.documents.length > 0) { booking.documents.forEach((doc: any, index: number) => { const actualFileName = doc.fileName || doc.name || 'document'; const actualFilePath = doc.filePath || doc.url || ''; const actualMimeType = doc.mimeType || doc.type || ''; let fileType = ''; if (actualMimeType.includes('/')) { const parts = actualMimeType.split('/'); fileType = getFileType(parts[1]); } else { fileType = getFileType(actualFileName); } allDocuments.push({ id: doc.id || `${booking.id}-doc-${index}`, fileName: actualFileName, filePath: actualFilePath, type: doc.type || '', mimeType: actualMimeType, size: doc.size || 0, uploadedAt: doc.uploadedAt, bookingId: booking.id, quoteNumber: getQuoteNumber(booking), route: `${booking.origin || 'N/A'} → ${booking.destination || 'N/A'}`, status: booking.status, carrierName: booking.carrierName || 'N/A', fileType: fileType, }); }); } }); setDocuments(allDocuments); setError(null); } catch (err: any) { setError(err.message || t('error')); } finally { setLoading(false); } }, [t]); useEffect(() => { fetchBookingsAndDocuments(); }, [fetchBookingsAndDocuments]); const filteredDocuments = documents.filter(doc => { const matchesSearch = searchTerm === '' || (doc.fileName && doc.fileName.toLowerCase().includes(searchTerm.toLowerCase())) || (doc.type && doc.type.toLowerCase().includes(searchTerm.toLowerCase())) || doc.route.toLowerCase().includes(searchTerm.toLowerCase()) || doc.carrierName.toLowerCase().includes(searchTerm.toLowerCase()); const matchesStatus = filterStatus === 'all' || doc.status === filterStatus; const matchesQuote = filterQuoteNumber === '' || doc.quoteNumber.toLowerCase().includes(filterQuoteNumber.toLowerCase()); return matchesSearch && matchesStatus && matchesQuote; }); const totalPages = Math.ceil(filteredDocuments.length / itemsPerPage); const startIndex = (currentPage - 1) * itemsPerPage; const endIndex = startIndex + itemsPerPage; const paginatedDocuments = filteredDocuments.slice(startIndex, endIndex); useEffect(() => { setCurrentPage(1); }, [searchTerm, filterStatus, filterQuoteNumber]); const getDocumentIcon = (type: string): ReactNode => { const typeLower = type.toLowerCase(); const cls = "h-6 w-6"; const iconMap: Record = { 'application/pdf': , 'image/jpeg': , 'image/png': , 'image/jpg': , pdf: , jpeg: , jpg: , png: , gif: , image: , word: , doc: , docx: , excel: , xls: , xlsx: , csv: , text: , txt: , }; return iconMap[typeLower] || ; }; const getStatusColor = (status: string) => { const colors: Record = { PENDING: 'bg-yellow-100 text-yellow-800', ACCEPTED: 'bg-green-100 text-green-800', REJECTED: 'bg-red-100 text-red-800', CANCELLED: 'bg-gray-100 text-gray-800', }; return colors[status] || 'bg-gray-100 text-gray-800'; }; const getStatusLabel = (status: string) => { const key = status as 'PENDING' | 'ACCEPTED' | 'REJECTED' | 'CANCELLED'; return t(`statuses.${key}` as any) || status; }; const handleDownload = async (url: string, fileName: string) => { try { const link = document.createElement('a'); link.href = url; link.download = fileName; link.target = '_blank'; link.setAttribute('download', fileName); document.body.appendChild(link); link.click(); document.body.removeChild(link); setTimeout(async () => { try { const response = await fetch(url, { mode: 'cors', credentials: 'include' }); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const blob = await response.blob(); const blobUrl = window.URL.createObjectURL(blob); const link2 = document.createElement('a'); link2.href = blobUrl; link2.download = fileName; document.body.appendChild(link2); link2.click(); document.body.removeChild(link2); window.URL.revokeObjectURL(blobUrl); } catch (fetchError) { console.error('Fetch download failed:', fetchError); } }, 100); } catch (err) { console.error('Error downloading file:', err); alert(`${t('downloadError')}: ${err instanceof Error ? err.message : ''}`); } }; const bookingsAvailableForDocuments = bookings.filter( b => b.status === 'PENDING' || b.status === 'ACCEPTED' ); const handleAddDocumentClick = () => { setShowAddModal(true); }; const handleCloseModal = () => { setShowAddModal(false); setSelectedBookingId(null); if (fileInputRef.current) fileInputRef.current.value = ''; }; const handleFileUpload = async () => { if (!selectedBookingId || !fileInputRef.current?.files?.length) { alert(t('addDocument.noBookingError')); return; } setUploadingFiles(true); try { const formData = new FormData(); const files = fileInputRef.current.files; for (let i = 0; i < files.length; i++) { formData.append('documents', files[i]); } const token = localStorage.getItem('access_token'); const response = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/csv-bookings/${selectedBookingId}/documents`, { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: formData } ); if (!response.ok) throw new Error(t('addDocument.errorMessage')); alert(t('addDocument.successMessage')); handleCloseModal(); fetchBookingsAndDocuments(); } catch (err) { console.error('Error uploading documents:', err); alert(`${t('addDocument.errorMessage')}: ${err instanceof Error ? err.message : ''}`); } finally { setUploadingFiles(false); } }; const toggleDropdown = (docId: string) => { setOpenDropdownId(openDropdownId === docId ? null : docId); }; useEffect(() => { const handleClickOutside = () => setOpenDropdownId(null); if (openDropdownId) { document.addEventListener('click', handleClickOutside); return () => document.removeEventListener('click', handleClickOutside); } }, [openDropdownId]); const handleReplaceClick = (doc: DocumentWithBooking) => { setOpenDropdownId(null); setDocumentToReplace(doc); setShowReplaceModal(true); }; const handleCloseReplaceModal = () => { setShowReplaceModal(false); setDocumentToReplace(null); if (replaceFileInputRef.current) replaceFileInputRef.current.value = ''; }; const handleReplaceDocument = async () => { if (!documentToReplace || !replaceFileInputRef.current?.files?.length) { alert(t('replaceDocument.noFileError')); return; } setReplacingFile(true); try { const formData = new FormData(); formData.append('document', replaceFileInputRef.current.files[0]); const token = localStorage.getItem('access_token'); const response = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/csv-bookings/${documentToReplace.bookingId}/documents/${documentToReplace.id}`, { method: 'PATCH', headers: { Authorization: `Bearer ${token}` }, body: formData } ); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.message || t('replaceDocument.errorMessage')); } alert(t('replaceDocument.successMessage')); handleCloseReplaceModal(); fetchBookingsAndDocuments(); } catch (err) { console.error('Error replacing document:', err); alert(`${t('replaceDocument.errorMessage')}: ${err instanceof Error ? err.message : ''}`); } finally { setReplacingFile(false); } }; if (loading) { return (

{t('loading')}

); } return (
getStatusLabel(v) }, { key: 'uploadedAt', label: t('export.uploadedAt'), format: (v) => v ? new Date(v).toLocaleDateString(locale) : '' }, ]} /> } /> {/* Stats */}
{t('stats.total')}
{documents.length}
{t('stats.withDocuments')}
{bookings.filter(b => b.documents && b.documents.length > 0).length}
{t('stats.filtered')}
{filteredDocuments.length}
{/* Filters */}
setSearchTerm(e.target.value)} className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none" />
setFilterQuoteNumber(e.target.value)} className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none" />
{/* Error Message */} {error && (
{error}
)} {/* Documents Table */}
{paginatedDocuments.length === 0 ? ( ) : ( paginatedDocuments.map((doc, index) => ( )) )}
{t('table.documentName')} {t('table.type')} {t('table.quoteNumber')} {t('table.route')} {t('table.carrier')} {t('table.status')} {t('table.actions')}
{documents.length === 0 ? t('empty.noDocuments') : t('empty.noMatch')}
{doc.fileName}
{getDocumentIcon(doc.fileType || doc.type)}
{doc.fileType || doc.type}
{doc.quoteNumber}
{doc.route}
{doc.carrierName}
{getStatusLabel(doc.status)}
{openDropdownId === `${doc.bookingId}-${doc.id}` && (
e.stopPropagation()} >
)}
{/* Pagination Controls */} {filteredDocuments.length > 0 && (

{t('pagination.showing', { from: startIndex + 1, to: Math.min(endIndex, filteredDocuments.length), total: filteredDocuments.length, })}

)}
{/* Add Document Modal */} {showAddModal && (

{t('addDocument.modalTitle')}

{t('addDocument.acceptedFormats')}

)} {/* Replace Document Modal */} {showReplaceModal && documentToReplace && (

{t('replaceDocument.modalTitle')}

{t('replaceDocument.currentDocument')}

{documentToReplace.fileName}

{t('replaceDocument.booking')}: {documentToReplace.quoteNumber} - {documentToReplace.route}

{t('replaceDocument.acceptedFormats')}

)}
); }