From 9950d96fd629fd6b9747da07a99a8b0319c47aa6 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 7 Jun 2026 16:49:12 +0200 Subject: [PATCH] fix: correct 5 reported bugs across booking and document management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SIRET/SIREN admin: controller applied siren but skipped siret — add missing updateSiret() call - Document upload on validated booking: backend blocked PENDING_BANK_TRANSFER; frontend filter excluded it — both now allow all non-terminal statuses - Accent encoding in filenames: Multer stores originalname as latin1; re-decode to utf8 before storing and displaying - Drag & drop in document modals: replace bare input with styled dropzone supporting click and native drag-and-drop in both Add and Replace modals - Resume existing booking: add Actions column with View button and booking detail modal to the bookings list page Co-Authored-By: Claude Sonnet 4.6 --- .../controllers/organizations.controller.ts | 4 + .../services/csv-booking.service.ts | 11 +- .../app/[locale]/dashboard/bookings/page.tsx | 157 +++++++++++++++++- .../app/[locale]/dashboard/documents/page.tsx | 129 +++++++++++--- apps/frontend/messages/en.json | 22 ++- apps/frontend/messages/fr.json | 22 ++- apps/frontend/src/lib/api/bookings.ts | 2 +- 7 files changed, 313 insertions(+), 34 deletions(-) diff --git a/apps/backend/src/application/controllers/organizations.controller.ts b/apps/backend/src/application/controllers/organizations.controller.ts index 821ca37..d9ec42f 100644 --- a/apps/backend/src/application/controllers/organizations.controller.ts +++ b/apps/backend/src/application/controllers/organizations.controller.ts @@ -252,6 +252,10 @@ export class OrganizationsController { organization.updateSiren(dto.siren); } + if (dto.siret) { + organization.updateSiret(dto.siret); + } + if (dto.eori) { organization.updateEori(dto.eori); } diff --git a/apps/backend/src/application/services/csv-booking.service.ts b/apps/backend/src/application/services/csv-booking.service.ts index 0de8541..8ef2668 100644 --- a/apps/backend/src/application/services/csv-booking.service.ts +++ b/apps/backend/src/application/services/csv-booking.service.ts @@ -989,7 +989,9 @@ export class CsvBookingService { for (const file of files) { const documentId = uuidv4(); - const fileKey = `csv-bookings/${bookingId}/${documentId}-${file.originalname}`; + // Multer decodes originalname as latin1; re-encode to utf8 to preserve accented characters + const fileName = Buffer.from(file.originalname, 'latin1').toString('utf8'); + const fileKey = `csv-bookings/${bookingId}/${documentId}-${fileName}`; // Upload to S3 const uploadResult = await this.storageAdapter.upload({ @@ -1000,12 +1002,12 @@ export class CsvBookingService { }); // Determine document type from filename or default to OTHER - const documentType = this.inferDocumentType(file.originalname); + const documentType = this.inferDocumentType(fileName); const document = new CsvBookingDocumentImpl( documentId, documentType, - file.originalname, + fileName, uploadResult.url, file.mimetype, file.size, @@ -1064,9 +1066,10 @@ export class CsvBookingService { throw new NotFoundException(`Booking with ID ${bookingId} not found`); } - // Allow adding documents to PENDING_PAYMENT, PENDING, or ACCEPTED bookings + // Allow adding documents to PENDING_PAYMENT, PENDING_BANK_TRANSFER, PENDING, or ACCEPTED bookings if ( booking.status !== CsvBookingStatus.PENDING_PAYMENT && + booking.status !== CsvBookingStatus.PENDING_BANK_TRANSFER && booking.status !== CsvBookingStatus.PENDING && booking.status !== CsvBookingStatus.ACCEPTED ) { diff --git a/apps/frontend/app/[locale]/dashboard/bookings/page.tsx b/apps/frontend/app/[locale]/dashboard/bookings/page.tsx index 4da6923..025f56a 100644 --- a/apps/frontend/app/[locale]/dashboard/bookings/page.tsx +++ b/apps/frontend/app/[locale]/dashboard/bookings/page.tsx @@ -4,7 +4,7 @@ import { useState, useEffect } from 'react'; import { useQuery } from '@tanstack/react-query'; import { listCsvBookings } from '@/lib/api'; import { Link } from '@/i18n/navigation'; -import { Plus, Clock } from 'lucide-react'; +import { Plus, Clock, Eye, X } from 'lucide-react'; import ExportButton from '@/components/ExportButton'; import { useSearchParams } from 'next/navigation'; import { PageHeader } from '@/components/ui/PageHeader'; @@ -22,6 +22,7 @@ export default function BookingsListPage() { const [statusFilter, setStatusFilter] = useState(''); const [page, setPage] = useState(1); const [showTransferBanner, setShowTransferBanner] = useState(false); + const [selectedBooking, setSelectedBooking] = useState(null); const ITEMS_PER_PAGE = 20; useEffect(() => { @@ -341,12 +342,21 @@ export default function BookingsListPage() { -
- {booking.type === 'csv' - ? t('mobile.ref', { - id: booking.bookingId || booking.id.slice(0, 8).toUpperCase(), - }) - : t('mobile.booking', { number: booking.bookingNumber || '-' })} +
+
+ {booking.type === 'csv' + ? t('mobile.ref', { + id: booking.bookingId || booking.id.slice(0, 8).toUpperCase(), + }) + : t('mobile.booking', { number: booking.bookingNumber || '-' })} +
+
))} @@ -377,6 +387,9 @@ export default function BookingsListPage() { {t('columns.bookingNumber')} + + {t('columns.actions')} + @@ -451,6 +464,15 @@ export default function BookingsListPage() { {booking.bookingNumber || '-'} + + + ))} @@ -601,6 +623,127 @@ export default function BookingsListPage() { )} + + {/* Booking Detail Modal */} + {selectedBooking && ( +
+
+
setSelectedBooking(null)} + /> +
+
+

+ {t('detail.title')} + {selectedBooking.bookingId + ? ` #${selectedBooking.bookingId}` + : ` #${selectedBooking.id?.slice(0, 8).toUpperCase()}`} +

+ +
+
+
+ + {getStatusLabel(selectedBooking.status)} + + {selectedBooking.carrierName && ( + {selectedBooking.carrierName} + )} +
+
+
+

{t('detail.route')}

+

+ {selectedBooking.origin || selectedBooking.originCity || 'N/A'} + {' → '} + {selectedBooking.destination || selectedBooking.destinationCity || 'N/A'} +

+
+
+

{t('detail.date')}

+

+ {selectedBooking.createdAt || selectedBooking.requestedAt + ? new Date( + selectedBooking.createdAt || selectedBooking.requestedAt + ).toLocaleDateString(dateLocale, { + day: '2-digit', + month: 'long', + year: 'numeric', + }) + : 'N/A'} +

+
+
+

{t('detail.cargo')}

+

+ {selectedBooking.palletCount + ? t('units.palletsCount', { count: selectedBooking.palletCount }) + : '-'} +

+
+
+

{t('detail.weight')}

+

+ {selectedBooking.weightKG ? t('units.kg', { value: selectedBooking.weightKG }) : 'N/A'} + {selectedBooking.volumeCBM ? ` / ${t('units.cbm', { value: selectedBooking.volumeCBM })}` : ''} +

+
+ {selectedBooking.requestedPickupDate && ( +
+

{t('detail.pickupDate')}

+

+ {new Date(selectedBooking.requestedPickupDate).toLocaleDateString(dateLocale, { + day: '2-digit', + month: 'long', + year: 'numeric', + })} +

+
+ )} + {(selectedBooking.priceEUR || selectedBooking.priceUSD) && ( +
+

{t('detail.price')}

+

+ {selectedBooking.priceEUR + ? `${selectedBooking.priceEUR} €` + : `${selectedBooking.priceUSD} $`} +

+
+ )} +
+ {selectedBooking.status === 'PENDING_PAYMENT' && ( +
+

+ {t('detail.pendingPaymentNotice')} +

+ + + {t('detail.newBooking')} + +
+ )} +
+
+ +
+
+
+
+ )}
); } diff --git a/apps/frontend/app/[locale]/dashboard/documents/page.tsx b/apps/frontend/app/[locale]/dashboard/documents/page.tsx index f5dc209..bff453a 100644 --- a/apps/frontend/app/[locale]/dashboard/documents/page.tsx +++ b/apps/frontend/app/[locale]/dashboard/documents/page.tsx @@ -45,11 +45,13 @@ export default function UserDocumentsPage() { const [showAddModal, setShowAddModal] = useState(false); const [selectedBookingId, setSelectedBookingId] = useState(null); const [uploadingFiles, setUploadingFiles] = useState(false); + const [isDraggingAdd, setIsDraggingAdd] = useState(false); const fileInputRef = useRef(null); const [showReplaceModal, setShowReplaceModal] = useState(false); const [documentToReplace, setDocumentToReplace] = useState(null); const [replacingFile, setReplacingFile] = useState(false); + const [isDraggingReplace, setIsDraggingReplace] = useState(false); const replaceFileInputRef = useRef(null); const [openDropdownId, setOpenDropdownId] = useState(null); @@ -235,7 +237,11 @@ export default function UserDocumentsPage() { }; const bookingsAvailableForDocuments = bookings.filter( - b => b.status === 'PENDING' || b.status === 'ACCEPTED' + b => + b.status === 'PENDING' || + b.status === 'ACCEPTED' || + b.status === 'PENDING_PAYMENT' || + b.status === 'PENDING_BANK_TRANSFER' ); const handleAddDocumentClick = () => { @@ -775,16 +781,59 @@ export default function UserDocumentsPage() { - -

- {t('addDocument.acceptedFormats')} -

+
{ + e.preventDefault(); + setIsDraggingAdd(true); + }} + onDragLeave={() => setIsDraggingAdd(false)} + onDrop={e => { + e.preventDefault(); + setIsDraggingAdd(false); + if (fileInputRef.current && e.dataTransfer.files.length > 0) { + const dt = new DataTransfer(); + Array.from(e.dataTransfer.files).forEach(f => dt.items.add(f)); + fileInputRef.current.files = dt.files; + } + }} + onClick={() => fileInputRef.current?.click()} + > + + + +

+ {isDraggingAdd + ? t('addDocument.dropHere') + : t('addDocument.dragOrClick')} +

+

{t('addDocument.acceptedFormats')}

+ { + // Force re-render to show selected file names + setIsDraggingAdd(false); + }} + /> +
@@ -884,15 +933,55 @@ export default function UserDocumentsPage() { - -

- {t('replaceDocument.acceptedFormats')} -

+
{ + e.preventDefault(); + setIsDraggingReplace(true); + }} + onDragLeave={() => setIsDraggingReplace(false)} + onDrop={e => { + e.preventDefault(); + setIsDraggingReplace(false); + if (replaceFileInputRef.current && e.dataTransfer.files.length > 0) { + const dt = new DataTransfer(); + dt.items.add(e.dataTransfer.files[0]); + replaceFileInputRef.current.files = dt.files; + } + }} + onClick={() => replaceFileInputRef.current?.click()} + > + + + +

+ {isDraggingReplace + ? t('replaceDocument.dropHere') + : t('replaceDocument.dragOrClick')} +

+

{t('replaceDocument.acceptedFormats')}

+ setIsDraggingReplace(false)} + /> +
diff --git a/apps/frontend/messages/en.json b/apps/frontend/messages/en.json index a847fca..e111a3d 100644 --- a/apps/frontend/messages/en.json +++ b/apps/frontend/messages/en.json @@ -426,7 +426,23 @@ "status": "Status", "date": "Date", "quoteNumber": "Quote No.", - "bookingNumber": "Booking No." + "bookingNumber": "Booking No.", + "actions": "Actions" + }, + "actions": { + "view": "View" + }, + "detail": { + "title": "Booking", + "route": "Route", + "date": "Creation date", + "cargo": "Cargo", + "weight": "Weight / Volume", + "pickupDate": "Requested pickup date", + "price": "Price", + "pendingPaymentNotice": "This booking is awaiting payment. Contact an administrator or create a new booking.", + "newBooking": "New booking", + "close": "Close" }, "mobile": { "pallets": "Pallets", @@ -1472,6 +1488,8 @@ "selectBookingPlaceholder": "-- Choose a booking --", "filesToAdd": "Files to add", "acceptedFormats": "Accepted formats: PDF, Word, Excel, Images (max 10 files)", + "dragOrClick": "Drag & drop your files here or click to select", + "dropHere": "Drop your files here", "uploading": "Uploading...", "add": "Add", "cancel": "Cancel", @@ -1485,6 +1503,8 @@ "booking": "Booking", "newFile": "New file", "acceptedFormats": "Accepted formats: PDF, Word, Excel, Images", + "dragOrClick": "Drag & drop your file here or click to select", + "dropHere": "Drop your file here", "replacing": "Replacing...", "replace": "Replace", "cancel": "Cancel", diff --git a/apps/frontend/messages/fr.json b/apps/frontend/messages/fr.json index de6060e..df451e3 100644 --- a/apps/frontend/messages/fr.json +++ b/apps/frontend/messages/fr.json @@ -426,7 +426,23 @@ "status": "Statut", "date": "Date", "quoteNumber": "N° Devis", - "bookingNumber": "N° Booking" + "bookingNumber": "N° Booking", + "actions": "Actions" + }, + "actions": { + "view": "Voir" + }, + "detail": { + "title": "Réservation", + "route": "Route", + "date": "Date de création", + "cargo": "Marchandise", + "weight": "Poids / Volume", + "pickupDate": "Date d'enlèvement souhaitée", + "price": "Prix", + "pendingPaymentNotice": "Cette réservation est en attente de paiement. Contactez l'administrateur ou créez une nouvelle réservation.", + "newBooking": "Nouvelle réservation", + "close": "Fermer" }, "mobile": { "pallets": "Palettes", @@ -1472,6 +1488,8 @@ "selectBookingPlaceholder": "-- Choisir une réservation --", "filesToAdd": "Fichiers à ajouter", "acceptedFormats": "Formats acceptés: PDF, Word, Excel, Images (max 10 fichiers)", + "dragOrClick": "Glissez-déposez vos fichiers ici ou cliquez pour sélectionner", + "dropHere": "Déposez vos fichiers ici", "uploading": "Envoi en cours...", "add": "Ajouter", "cancel": "Annuler", @@ -1485,6 +1503,8 @@ "booking": "Réservation", "newFile": "Nouveau fichier", "acceptedFormats": "Formats acceptés: PDF, Word, Excel, Images", + "dragOrClick": "Glissez-déposez votre fichier ici ou cliquez pour sélectionner", + "dropHere": "Déposez votre fichier ici", "replacing": "Remplacement en cours...", "replace": "Remplacer", "cancel": "Annuler", diff --git a/apps/frontend/src/lib/api/bookings.ts b/apps/frontend/src/lib/api/bookings.ts index 763bbf6..3018eee 100644 --- a/apps/frontend/src/lib/api/bookings.ts +++ b/apps/frontend/src/lib/api/bookings.ts @@ -51,7 +51,7 @@ export interface CsvBookingResponse { primaryCurrency: string; transitDays: number; containerType: string; - status: 'PENDING_PAYMENT' | 'PENDING' | 'ACCEPTED' | 'REJECTED' | 'CANCELLED'; + status: 'PENDING_PAYMENT' | 'PENDING_BANK_TRANSFER' | 'PENDING' | 'ACCEPTED' | 'REJECTED' | 'CANCELLED'; documents: Array<{ type: string; fileName: string;