diff --git a/apps/backend/src/application/controllers/admin.controller.ts b/apps/backend/src/application/controllers/admin.controller.ts index d3c314f..745afff 100644 --- a/apps/backend/src/application/controllers/admin.controller.ts +++ b/apps/backend/src/application/controllers/admin.controller.ts @@ -860,4 +860,55 @@ export class AdminController { total: organization.documents.length, }; } + + /** + * Delete a document from a CSV booking (admin only) + * Bypasses ownership and status restrictions + */ + @Delete('bookings/:bookingId/documents/:documentId') + @HttpCode(HttpStatus.OK) + @ApiOperation({ + summary: 'Delete document from CSV booking (Admin only)', + description: 'Remove a document from a booking, bypassing ownership and status restrictions.', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Document deleted successfully', + }) + async deleteDocument( + @Param('bookingId', ParseUUIDPipe) bookingId: string, + @Param('documentId', ParseUUIDPipe) documentId: string, + @CurrentUser() user: UserPayload + ): Promise<{ success: boolean; message: string }> { + this.logger.log(`[ADMIN: ${user.email}] Deleting document ${documentId} from booking ${bookingId}`); + + const booking = await this.csvBookingRepository.findById(bookingId); + if (!booking) { + throw new NotFoundException(`Booking ${bookingId} not found`); + } + + const documentIndex = booking.documents.findIndex(doc => doc.id === documentId); + if (documentIndex === -1) { + throw new NotFoundException(`Document ${documentId} not found`); + } + + const updatedDocuments = booking.documents.filter(doc => doc.id !== documentId); + + 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(`[ADMIN] Document ${documentId} deleted from booking ${bookingId}`); + return { success: true, message: 'Document deleted successfully' }; + } } diff --git a/apps/backend/src/application/controllers/admin/csv-rates.controller.ts b/apps/backend/src/application/controllers/admin/csv-rates.controller.ts index fe1c5c6..f7df27c 100644 --- a/apps/backend/src/application/controllers/admin/csv-rates.controller.ts +++ b/apps/backend/src/application/controllers/admin/csv-rates.controller.ts @@ -489,6 +489,7 @@ export class CsvRatesAdminController { size: fileSize, uploadedAt: config.uploadedAt.toISOString(), rowCount: config.rowCount, + companyEmail: config.metadata?.companyEmail ?? null, }; }); diff --git a/apps/frontend/app/dashboard/admin/bookings/page.tsx b/apps/frontend/app/dashboard/admin/bookings/page.tsx index 39ea9b6..485aa40 100644 --- a/apps/frontend/app/dashboard/admin/bookings/page.tsx +++ b/apps/frontend/app/dashboard/admin/bookings/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState, useEffect } from 'react'; -import { getAllBookings, validateBankTransfer } from '@/lib/api/admin'; +import { getAllBookings, validateBankTransfer, deleteAdminBooking } from '@/lib/api/admin'; interface Booking { id: string; @@ -32,11 +32,29 @@ export default function AdminBookingsPage() { const [filterStatus, setFilterStatus] = useState('all'); const [searchTerm, setSearchTerm] = useState(''); const [validatingId, setValidatingId] = useState(null); + const [deletingId, setDeletingId] = useState(null); + const [openMenuId, setOpenMenuId] = useState(null); + const [menuPosition, setMenuPosition] = useState<{ top: number; left: number } | null>(null); + const [selectedBooking, setSelectedBooking] = useState(null); + const [showDetailsModal, setShowDetailsModal] = useState(false); useEffect(() => { fetchBookings(); }, []); + const handleDeleteBooking = async (bookingId: string) => { + if (!window.confirm('Supprimer définitivement cette réservation ?')) return; + setDeletingId(bookingId); + try { + await deleteAdminBooking(bookingId); + setBookings(prev => prev.filter(b => b.id !== bookingId)); + } catch (err: any) { + setError(err.message || 'Erreur lors de la suppression'); + } finally { + setDeletingId(null); + } + }; + const handleValidateTransfer = async (bookingId: string) => { if (!window.confirm('Confirmer la réception du virement et activer ce booking ?')) return; setValidatingId(bookingId); @@ -286,15 +304,23 @@ export default function AdminBookingsPage() { {/* Actions */} - {booking.status.toUpperCase() === 'PENDING_BANK_TRANSFER' && ( - - )} + )) @@ -303,6 +329,220 @@ export default function AdminBookingsPage() { + {/* Actions Dropdown Menu */} + {openMenuId && menuPosition && ( + <> +
{ setOpenMenuId(null); setMenuPosition(null); }} + /> +
+
+ + {(() => { + const booking = bookings.find(b => b.id === openMenuId); + return booking?.status.toUpperCase() === 'PENDING_BANK_TRANSFER' ? ( + + ) : null; + })()} + +
+
+ + )} + + {/* Details Modal */} + {showDetailsModal && selectedBooking && ( +
+
+
+

Détails de la réservation

+ +
+ +
+
+
+ +
+ {selectedBooking.bookingNumber || getShortId(selectedBooking)} +
+
+
+ + + {getStatusLabel(selectedBooking.status)} + +
+
+ +
+

Route

+
+
+ +
{selectedBooking.origin || '—'}
+
+
+ +
{selectedBooking.destination || '—'}
+
+
+
+ +
+

Cargo & Transporteur

+
+
+ +
{selectedBooking.carrierName || '—'}
+
+
+ +
{selectedBooking.containerType}
+
+ {selectedBooking.palletCount != null && ( +
+ +
{selectedBooking.palletCount}
+
+ )} + {selectedBooking.weightKG != null && ( +
+ +
{selectedBooking.weightKG.toLocaleString()} kg
+
+ )} + {selectedBooking.volumeCBM != null && ( +
+ +
{selectedBooking.volumeCBM} CBM
+
+ )} +
+
+ + {(selectedBooking.priceEUR != null || selectedBooking.priceUSD != null) && ( +
+

Prix

+
+ {selectedBooking.priceEUR != null && ( +
+ +
{selectedBooking.priceEUR.toLocaleString()} €
+
+ )} + {selectedBooking.priceUSD != null && ( +
+ +
{selectedBooking.priceUSD.toLocaleString()} $
+
+ )} +
+
+ )} + +
+

Dates

+
+
+ +
+ {new Date(selectedBooking.requestedAt || selectedBooking.createdAt || '').toLocaleString('fr-FR')} +
+
+ {selectedBooking.updatedAt && ( +
+ +
{new Date(selectedBooking.updatedAt).toLocaleString('fr-FR')}
+
+ )} +
+
+ + {selectedBooking.status.toUpperCase() === 'PENDING_BANK_TRANSFER' && ( +
+ +
+ )} +
+ +
+ +
+
+
+ )}
); } diff --git a/apps/frontend/app/dashboard/admin/csv-rates/page.tsx b/apps/frontend/app/dashboard/admin/csv-rates/page.tsx index aeb9e42..3f385f2 100644 --- a/apps/frontend/app/dashboard/admin/csv-rates/page.tsx +++ b/apps/frontend/app/dashboard/admin/csv-rates/page.tsx @@ -81,20 +81,22 @@ export default function AdminCsvRatesPage() { {/* Configurations Table */} - -
- Configurations CSV actives - - Liste de toutes les compagnies avec fichiers CSV configurés - + +
+
+ Configurations CSV actives + + Liste de toutes les compagnies avec fichiers CSV configurés + +
+
-
{error && ( @@ -120,6 +122,7 @@ export default function AdminCsvRatesPage() { Taille Lignes Date d'upload + Email Actions @@ -142,6 +145,11 @@ export default function AdminCsvRatesPage() { {new Date(file.uploadedAt).toLocaleDateString('fr-FR')}
+ +
+ {file.companyEmail ?? '—'} +
+
@@ -586,6 +611,60 @@ export default function AdminDocumentsPage() { )} + {/* Actions Dropdown Menu */} + {openMenuId && menuPosition && ( + <> +
{ setOpenMenuId(null); setMenuPosition(null); }} + /> +
+
+ {(() => { + const [bookingId, documentId] = openMenuId.split('::'); + const doc = documents.find(d => d.bookingId === bookingId && d.id === documentId); + if (!doc) return null; + return ( + <> + + + + ); + })()} +
+
+ + )}
); } diff --git a/apps/frontend/src/lib/api/admin.ts b/apps/frontend/src/lib/api/admin.ts index 78ce939..ce227a4 100644 --- a/apps/frontend/src/lib/api/admin.ts +++ b/apps/frontend/src/lib/api/admin.ts @@ -144,6 +144,26 @@ export async function validateBankTransfer(bookingId: string): Promise(`/api/v1/admin/bookings/${bookingId}/validate-transfer`, {}); } +/** + * Delete a booking (admin only) + * DELETE /api/v1/admin/bookings/:id + * Permanently deletes a booking from the database + * Requires: ADMIN role + */ +export async function deleteAdminBooking(bookingId: string): Promise { + return del(`/api/v1/admin/bookings/${bookingId}`); +} + +/** + * Delete a document from a booking (admin only) + * DELETE /api/v1/admin/bookings/:bookingId/documents/:documentId + * Bypasses ownership and status restrictions + * Requires: ADMIN role + */ +export async function deleteAdminDocument(bookingId: string, documentId: string): Promise { + return del(`/api/v1/admin/bookings/${bookingId}/documents/${documentId}`); +} + // ==================== DOCUMENTS ==================== /** diff --git a/apps/frontend/src/lib/api/admin/csv-rates.ts b/apps/frontend/src/lib/api/admin/csv-rates.ts index 86dc11e..71796ec 100644 --- a/apps/frontend/src/lib/api/admin/csv-rates.ts +++ b/apps/frontend/src/lib/api/admin/csv-rates.ts @@ -16,6 +16,7 @@ export interface CsvFileInfo { size: number; uploadedAt: string; rowCount?: number; + companyEmail?: string | null; } export interface CsvFileListResponse {