diff --git a/apps/backend/src/application/controllers/bookings.controller.ts b/apps/backend/src/application/controllers/bookings.controller.ts index c2279b3..386c25d 100644 --- a/apps/backend/src/application/controllers/bookings.controller.ts +++ b/apps/backend/src/application/controllers/bookings.controller.ts @@ -372,14 +372,20 @@ export class BookingsController { const endIndex = startIndex + pageSize; const paginatedBookings = filteredBookings.slice(startIndex, endIndex); - // Fetch rate quotes for all bookings - const bookingsWithQuotes = await Promise.all( + // Fetch rate quotes for all bookings (filter out those with missing rate quotes) + const bookingsWithQuotesRaw = await Promise.all( paginatedBookings.map(async (booking: any) => { const rateQuote = await this.rateQuoteRepository.findById(booking.rateQuoteId); - return { booking, rateQuote: rateQuote! }; + return { booking, rateQuote }; }) ); + // Filter out bookings with missing rate quotes to avoid null pointer errors + const bookingsWithQuotes = bookingsWithQuotesRaw.filter( + (item): item is { booking: any; rateQuote: NonNullable } => + item.rateQuote !== null && item.rateQuote !== undefined + ); + // Convert to DTOs const bookingDtos = BookingMapper.toListItemDtoArray(bookingsWithQuotes); @@ -440,14 +446,21 @@ export class BookingsController { ); // Map ORM entities to domain and fetch rate quotes - const bookingsWithQuotes = await Promise.all( + const bookingsWithQuotesRaw = await Promise.all( bookingOrms.map(async bookingOrm => { const booking = await this.bookingRepository.findById(bookingOrm.id); const rateQuote = await this.rateQuoteRepository.findById(bookingOrm.rateQuoteId); - return { booking: booking!, rateQuote: rateQuote! }; + return { booking, rateQuote }; }) ); + // Filter out bookings or rate quotes that are null + const bookingsWithQuotes = bookingsWithQuotesRaw.filter( + (item): item is { booking: NonNullable; rateQuote: NonNullable } => + item.booking !== null && item.booking !== undefined && + item.rateQuote !== null && item.rateQuote !== undefined + ); + // Convert to DTOs const bookingDtos = bookingsWithQuotes.map(({ booking, rateQuote }) => BookingMapper.toDto(booking, rateQuote) @@ -487,8 +500,10 @@ export class BookingsController { // Apply filters bookings = this.applyFilters(bookings, filter); - // Sort bookings - bookings = this.sortBookings(bookings, filter.sortBy!, filter.sortOrder!); + // Sort bookings (use defaults if not provided) + const sortBy = filter.sortBy || 'createdAt'; + const sortOrder = filter.sortOrder || 'desc'; + bookings = this.sortBookings(bookings, sortBy, sortOrder); // Total count before pagination const total = bookings.length; @@ -498,14 +513,20 @@ export class BookingsController { const endIndex = startIndex + (filter.pageSize || 20); const paginatedBookings = bookings.slice(startIndex, endIndex); - // Fetch rate quotes - const bookingsWithQuotes = await Promise.all( + // Fetch rate quotes (filter out those with missing rate quotes) + const bookingsWithQuotesRaw = await Promise.all( paginatedBookings.map(async booking => { const rateQuote = await this.rateQuoteRepository.findById(booking.rateQuoteId); - return { booking, rateQuote: rateQuote! }; + return { booking, rateQuote }; }) ); + // Filter out bookings with missing rate quotes to avoid null pointer errors + const bookingsWithQuotes = bookingsWithQuotesRaw.filter( + (item): item is { booking: any; rateQuote: NonNullable } => + item.rateQuote !== null && item.rateQuote !== undefined + ); + // Convert to DTOs const bookingDtos = BookingMapper.toListItemDtoArray(bookingsWithQuotes); @@ -562,14 +583,20 @@ export class BookingsController { bookings = this.applyFilters(bookings, filter); } - // Fetch rate quotes - const bookingsWithQuotes = await Promise.all( + // Fetch rate quotes (filter out those with missing rate quotes) + const bookingsWithQuotesRaw = await Promise.all( bookings.map(async booking => { const rateQuote = await this.rateQuoteRepository.findById(booking.rateQuoteId); - return { booking, rateQuote: rateQuote! }; + return { booking, rateQuote }; }) ); + // Filter out bookings with missing rate quotes to avoid null pointer errors + const bookingsWithQuotes = bookingsWithQuotesRaw.filter( + (item): item is { booking: any; rateQuote: NonNullable } => + item.rateQuote !== null && item.rateQuote !== undefined + ); + // Generate export file const exportResult = await this.exportService.exportBookings( bookingsWithQuotes, diff --git a/apps/backend/src/application/controllers/csv-bookings.controller.ts b/apps/backend/src/application/controllers/csv-bookings.controller.ts index 9b085cd..26e3bed 100644 --- a/apps/backend/src/application/controllers/csv-bookings.controller.ts +++ b/apps/backend/src/application/controllers/csv-bookings.controller.ts @@ -3,6 +3,7 @@ import { Post, Get, Patch, + Delete, Body, Param, Query, @@ -386,4 +387,98 @@ export class CsvBookingsController { const organizationId = req.user.organizationId; return await this.csvBookingService.getOrganizationStats(organizationId); } + + /** + * Add documents to an existing booking + * + * POST /api/v1/csv-bookings/:id/documents + */ + @Post(':id/documents') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @UseInterceptors(FilesInterceptor('documents', 10)) + @ApiConsumes('multipart/form-data') + @ApiOperation({ + summary: 'Add documents to an existing booking', + description: + 'Upload additional documents to a pending booking. Only the booking owner can add documents.', + }) + @ApiParam({ name: 'id', description: 'Booking ID (UUID)' }) + @ApiBody({ + schema: { + type: 'object', + properties: { + documents: { + type: 'array', + items: { type: 'string', format: 'binary' }, + description: 'Documents to add (max 10 files)', + }, + }, + }, + }) + @ApiResponse({ + status: 200, + description: 'Documents added successfully', + schema: { + type: 'object', + properties: { + success: { type: 'boolean', example: true }, + message: { type: 'string', example: 'Documents added successfully' }, + documentsAdded: { type: 'number', example: 2 }, + }, + }, + }) + @ApiResponse({ status: 400, description: 'Invalid request or booking cannot be modified' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 404, description: 'Booking not found' }) + async addDocuments( + @Param('id') id: string, + @UploadedFiles() files: Express.Multer.File[], + @Request() req: any + ) { + if (!files || files.length === 0) { + throw new BadRequestException('At least one document is required'); + } + + const userId = req.user.id; + return await this.csvBookingService.addDocuments(id, files, userId); + } + + /** + * Delete a document from a booking + * + * DELETE /api/v1/csv-bookings/:bookingId/documents/:documentId + */ + @Delete(':bookingId/documents/:documentId') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOperation({ + summary: 'Delete a document from a booking', + description: + 'Remove a document from a pending booking. Only the booking owner can delete documents.', + }) + @ApiParam({ name: 'bookingId', description: 'Booking ID (UUID)' }) + @ApiParam({ name: 'documentId', description: 'Document ID (UUID)' }) + @ApiResponse({ + status: 200, + description: 'Document deleted successfully', + schema: { + type: 'object', + properties: { + success: { type: 'boolean', example: true }, + message: { type: 'string', example: 'Document deleted successfully' }, + }, + }, + }) + @ApiResponse({ status: 400, description: 'Booking cannot be modified (not pending)' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 404, description: 'Booking or document not found' }) + async deleteDocument( + @Param('bookingId') bookingId: string, + @Param('documentId') documentId: string, + @Request() req: any + ) { + const userId = req.user.id; + return await this.csvBookingService.deleteDocument(bookingId, documentId, userId); + } } diff --git a/apps/backend/src/application/services/csv-booking.service.ts b/apps/backend/src/application/services/csv-booking.service.ts index 4e4e21b..ee87fbd 100644 --- a/apps/backend/src/application/services/csv-booking.service.ts +++ b/apps/backend/src/application/services/csv-booking.service.ts @@ -454,6 +454,147 @@ export class CsvBookingService { } } + /** + * Add documents to an existing booking + */ + async addDocuments( + bookingId: string, + files: Express.Multer.File[], + userId: string + ): Promise<{ success: boolean; message: string; documentsAdded: number }> { + this.logger.log(`Adding ${files.length} documents to booking ${bookingId}`); + + const booking = await this.csvBookingRepository.findById(bookingId); + + if (!booking) { + throw new NotFoundException(`Booking with ID ${bookingId} not found`); + } + + // Verify user owns this booking + if (booking.userId !== userId) { + throw new NotFoundException(`Booking with ID ${bookingId} not found`); + } + + // Verify booking is still pending + if (booking.status !== CsvBookingStatus.PENDING) { + throw new BadRequestException('Cannot add documents to a booking that is not pending'); + } + + // Upload new documents + const newDocuments = await this.uploadDocuments(files, bookingId); + + // Add documents to booking + const updatedDocuments = [...booking.documents, ...newDocuments]; + + // Update booking in database using ORM repository directly + const ormBooking = await this.csvBookingRepository['repository'].findOne({ + where: { id: bookingId }, + }); + + if (ormBooking) { + ormBooking.documents = updatedDocuments.map(doc => ({ + id: doc.id, + type: doc.type, + fileName: doc.fileName, + filePath: doc.filePath, + mimeType: doc.mimeType, + size: doc.size, + uploadedAt: doc.uploadedAt, + })); + await this.csvBookingRepository['repository'].save(ormBooking); + } + + this.logger.log(`Added ${newDocuments.length} documents to booking ${bookingId}`); + + return { + success: true, + message: 'Documents added successfully', + documentsAdded: newDocuments.length, + }; + } + + /** + * Delete a document from a booking + */ + async deleteDocument( + bookingId: string, + documentId: string, + userId: string + ): Promise<{ success: boolean; message: string }> { + this.logger.log(`Deleting document ${documentId} from booking ${bookingId}`); + + const booking = await this.csvBookingRepository.findById(bookingId); + + if (!booking) { + throw new NotFoundException(`Booking with ID ${bookingId} not found`); + } + + // Verify user owns this booking + if (booking.userId !== userId) { + throw new NotFoundException(`Booking with ID ${bookingId} not found`); + } + + // Verify booking is still pending + if (booking.status !== CsvBookingStatus.PENDING) { + throw new BadRequestException('Cannot delete documents from a booking that is not pending'); + } + + // Find the document + const documentIndex = booking.documents.findIndex(doc => doc.id === documentId); + + if (documentIndex === -1) { + throw new NotFoundException(`Document with ID ${documentId} not found`); + } + + // Ensure at least one document remains + if (booking.documents.length <= 1) { + throw new BadRequestException( + 'Cannot delete the last document. At least one document is required.' + ); + } + + // Get the document to delete (for potential S3 cleanup - currently kept for audit) + const _documentToDelete = booking.documents[documentIndex]; + + // Remove document from array + const updatedDocuments = booking.documents.filter(doc => doc.id !== documentId); + + // Update booking in database using ORM repository directly + const ormBooking = await this.csvBookingRepository['repository'].findOne({ + where: { id: bookingId }, + }); + + if (ormBooking) { + ormBooking.documents = updatedDocuments.map(doc => ({ + id: doc.id, + type: doc.type, + fileName: doc.fileName, + filePath: doc.filePath, + mimeType: doc.mimeType, + size: doc.size, + uploadedAt: doc.uploadedAt, + })); + await this.csvBookingRepository['repository'].save(ormBooking); + } + + // Optionally delete from S3 (commented out for safety - keep files for audit) + // try { + // await this.storageAdapter.delete({ + // bucket: 'xpeditis-documents', + // key: documentToDelete.filePath, + // }); + // } catch (error) { + // this.logger.warn(`Failed to delete file from S3: ${documentToDelete.filePath}`); + // } + + this.logger.log(`Deleted document ${documentId} from booking ${bookingId}`); + + return { + success: true, + message: 'Document deleted successfully', + }; + } + /** * Infer document type from filename */ diff --git a/apps/frontend/app/dashboard/documents/page.tsx b/apps/frontend/app/dashboard/documents/page.tsx new file mode 100644 index 0000000..7e913e6 --- /dev/null +++ b/apps/frontend/app/dashboard/documents/page.tsx @@ -0,0 +1,792 @@ +'use client'; + +import { useState, useEffect, useCallback, useRef } from 'react'; +import { listCsvBookings, CsvBookingResponse } from '@/lib/api/bookings'; + +interface Document { + id: string; + fileName: string; + filePath: string; + type: string; + mimeType: string; + size: number; + uploadedAt?: Date; + // Legacy fields for compatibility + 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 [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); + + // Modal state for adding documents + const [showAddModal, setShowAddModal] = useState(false); + const [selectedBookingId, setSelectedBookingId] = useState(null); + const [uploadingFiles, setUploadingFiles] = useState(false); + const fileInputRef = useRef(null); + + // Helper function to get formatted quote number + const getQuoteNumber = (booking: CsvBookingResponse): string => { + return `#${booking.bookingId || booking.id.slice(0, 8).toUpperCase()}`; + }; + + // Get file extension and type + 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); + // Fetch all user's bookings (paginated, get all pages) + const response = await listCsvBookings({ page: 1, limit: 1000 }); + const allBookings = response.bookings || []; + setBookings(allBookings); + + // Extract all documents from all bookings + const allDocuments: DocumentWithBooking[] = []; + + allBookings.forEach((booking: CsvBookingResponse) => { + if (booking.documents && booking.documents.length > 0) { + booking.documents.forEach((doc: any, index: number) => { + // Use the correct field names from the backend + const actualFileName = doc.fileName || doc.name || 'document'; + const actualFilePath = doc.filePath || doc.url || ''; + const actualMimeType = doc.mimeType || doc.type || ''; + + // Extract clean file type from mimeType or fileName + 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 || 'Erreur lors du chargement des documents'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchBookingsAndDocuments(); + }, [fetchBookingsAndDocuments]); + + // Filter documents + 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; + }); + + // Pagination + const totalPages = Math.ceil(filteredDocuments.length / itemsPerPage); + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const paginatedDocuments = filteredDocuments.slice(startIndex, endIndex); + + // Reset to page 1 when filters change + useEffect(() => { + setCurrentPage(1); + }, [searchTerm, filterStatus, filterQuoteNumber]); + + const getDocumentIcon = (type: string) => { + const typeLower = type.toLowerCase(); + const icons: 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 icons[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 labels: Record = { + PENDING: 'En attente', + ACCEPTED: 'Accepté', + REJECTED: 'Refusé', + CANCELLED: 'Annulé', + }; + return labels[status] || status; + }; + + const handleDownload = async (url: string, fileName: string) => { + try { + // Try direct download first + 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); + + // If direct download doesn't work, try fetch with blob + 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 (error) { + console.error('Error downloading file:', error); + alert( + `Erreur lors du téléchargement du document: ${error instanceof Error ? error.message : 'Erreur inconnue'}` + ); + } + }; + + // Get unique bookings for add document modal + const bookingsWithPendingStatus = bookings.filter(b => b.status === 'PENDING'); + + 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('Veuillez sélectionner une réservation et au moins un fichier'); + 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('Erreur lors de l\'ajout des documents'); + } + + alert('Documents ajoutés avec succès!'); + handleCloseModal(); + fetchBookingsAndDocuments(); // Refresh the list + } catch (error) { + console.error('Error uploading documents:', error); + alert( + `Erreur lors de l'ajout des documents: ${error instanceof Error ? error.message : 'Erreur inconnue'}` + ); + } finally { + setUploadingFiles(false); + } + }; + + const handleDeleteDocument = async (bookingId: string, documentId: string, fileName: string) => { + if (!confirm(`Êtes-vous sûr de vouloir supprimer le document "${fileName}" ?`)) { + return; + } + + try { + const token = localStorage.getItem('access_token'); + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/csv-bookings/${bookingId}/documents/${documentId}`, + { + method: 'DELETE', + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + + if (!response.ok) { + throw new Error('Erreur lors de la suppression du document'); + } + + alert('Document supprimé avec succès!'); + fetchBookingsAndDocuments(); // Refresh the list + } catch (error) { + console.error('Error deleting document:', error); + alert( + `Erreur lors de la suppression: ${error instanceof Error ? error.message : 'Erreur inconnue'}` + ); + } + }; + + if (loading) { + return ( +
+
+
+

Chargement des documents...

+
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+

Mes Documents

+

+ Gérez tous les documents de vos réservations +

+
+ +
+ + {/* Stats */} +
+
+
Total Documents
+
{documents.length}
+
+
+
Réservations avec Documents
+
+ {bookings.filter(b => b.documents && b.documents.length > 0).length} +
+
+
+
Documents Filtrés
+
{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) => ( + + + + + + + + + + )) + )} + +
+ Nom du Document + + Type + + N° de Devis + + Route + + Transporteur + + Statut + + Actions +
+ {documents.length === 0 + ? 'Aucun document trouvé. Ajoutez des documents à vos réservations.' + : 'Aucun document ne correspond aux filtres sélectionnés.'} +
+
{doc.fileName}
+
+
+ + {getDocumentIcon(doc.fileType || doc.type)} + +
{doc.fileType || doc.type}
+
+
+
{doc.quoteNumber}
+
+
{doc.route}
+
+
{doc.carrierName}
+
+ + {getStatusLabel(doc.status)} + + +
+ + {doc.status === 'PENDING' && ( + + )} +
+
+ + {/* Pagination Controls */} + {filteredDocuments.length > 0 && ( +
+
+ + +
+
+
+

+ Affichage de {startIndex + 1} à{' '} + + {Math.min(endIndex, filteredDocuments.length)} + {' '} + sur {filteredDocuments.length} résultats +

+
+
+
+ + +
+ +
+
+
+ )} +
+ + {/* Add Document Modal */} + {showAddModal && ( +
+
+ {/* Background overlay */} +
+ + {/* Modal panel */} +
+
+
+
+ + + +
+
+

+ Ajouter un document +

+
+
+ + +
+
+ + +

+ Formats acceptés: PDF, Word, Excel, Images (max 10 fichiers) +

+
+
+
+
+
+
+ + +
+
+
+
+ )} +
+ ); +} diff --git a/apps/frontend/app/dashboard/layout.tsx b/apps/frontend/app/dashboard/layout.tsx index 50872ae..97653c0 100644 --- a/apps/frontend/app/dashboard/layout.tsx +++ b/apps/frontend/app/dashboard/layout.tsx @@ -24,6 +24,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod const navigation = [ { name: 'Dashboard', href: '/dashboard', icon: '📊' }, { name: 'Bookings', href: '/dashboard/bookings', icon: '📦' }, + { name: 'Documents', href: '/dashboard/documents', icon: '📄' }, { name: 'My Profile', href: '/dashboard/profile', icon: '👤' }, { name: 'Organization', href: '/dashboard/settings/organization', icon: '🏢' }, // ADMIN and MANAGER only navigation items