diff --git a/apps/frontend/app/[locale]/dashboard/bookings/page.tsx b/apps/frontend/app/[locale]/dashboard/bookings/page.tsx index 40b86b8..4cdfd08 100644 --- a/apps/frontend/app/[locale]/dashboard/bookings/page.tsx +++ b/apps/frontend/app/[locale]/dashboard/bookings/page.tsx @@ -5,9 +5,19 @@ import { useQuery } from '@tanstack/react-query'; import { useSearchParams } from 'next/navigation'; import { useLocale, useTranslations } from 'next-intl'; import type { ColumnDef } from '@tanstack/react-table'; -import { CreditCard, Eye, MoreVertical, Package, Pencil, Plus, Sparkles, X } from 'lucide-react'; +import { + CreditCard, + Eye, + MoreVertical, + Package, + Pencil, + Plus, + Sparkles, + Trash2, + X, +} from 'lucide-react'; -import { listCsvBookings } from '@/lib/api'; +import { deleteCsvBooking, listCsvBookings } from '@/lib/api'; import { Link } from '@/i18n/navigation'; import { PageHeader } from '@/components/ui/PageHeader'; import { Button } from '@/components/ui/button'; @@ -33,6 +43,8 @@ import { BookingStatusBadge, useBookingStatusLabel, } from '@/components/bookings/BookingStatusBadge'; +import { useConfirm } from '@/components/ui/use-confirm'; +import { useToast } from '@/components/ui/toast'; import ExportButton from '@/components/ExportButton'; import { useReservationQuota } from '@/hooks/useReservationQuota'; import { useUrlState } from '@/hooks/use-url-state'; @@ -53,6 +65,8 @@ export default function BookingsListPage() { const searchParams = useSearchParams(); const url = useUrlState(); const statusLabel = useBookingStatusLabel(); + const confirm = useConfirm(); + const { toast } = useToast(); // L'etat de vue vit dans l'URL : la liste est partageable et survit au // rechargement, ce qui n'etait pas le cas avec un etat purement local. @@ -80,6 +94,31 @@ export default function BookingsListPage() { const quota = useReservationQuota(); + /** + * Supprime une reservation impayee, apres confirmation. + * + * L'action est destructrice et sans annulation : la confirmation nomme la + * reservation concernee plutot que de demander « etes-vous sur ? ». + */ + const remove = async (b: any) => { + const reference = b.bookingNumber || b.quoteNumber || `${b.origin} → ${b.destination}`; + const confirmed = await confirm({ + title: t('deleteConfirm.title'), + description: t('deleteConfirm.body', { reference }), + confirmLabel: t('actions.delete'), + destructive: true, + }); + if (!confirmed) return; + + try { + await deleteCsvBooking(b.id); + toast.success(t('deleteConfirm.done')); + await refetch(); + } catch { + toast.error(t('deleteConfirm.failed')); + } + }; + // « Modifier » rouvre le parcours existant (recherche → transporteur → // paiement) pre-rempli, afin qu'un changement redonne lieu a une cotation. const buildEditUrl = (b: any) => { @@ -291,6 +330,16 @@ export default function BookingsListPage() { {t('actions.pay')} + {/* Suppression reservee aux reservations impayees : une fois + la commission reglee, la reservation est partie chez le + transporteur et ne peut plus qu'etre annulee. */} + remove(b)} + className="text-destructive focus:text-destructive" + > + + {t('actions.delete')} + )} @@ -409,10 +458,7 @@ export default function BookingsListPage() { }} filters={ <> - url.set('by', value)}> @@ -468,8 +514,7 @@ export default function BookingsListPage() { empty={{ icon: Package, title: t('empty.title'), - description: - searchTerm || statusFilter ? t('empty.hasFilters') : t('empty.noBookings'), + description: searchTerm || statusFilter ? t('empty.hasFilters') : t('empty.noBookings'), action: newReservationAction, }} caption={t('title')} diff --git a/apps/frontend/messages/en.json b/apps/frontend/messages/en.json index 692fea1..1df4c55 100644 --- a/apps/frontend/messages/en.json +++ b/apps/frontend/messages/en.json @@ -532,7 +532,14 @@ "actions": { "view": "View", "pay": "Pay", - "edit": "Edit" + "edit": "Edit", + "delete": "Delete" + }, + "deleteConfirm": { + "title": "Delete this booking?", + "body": "{reference} will be permanently deleted. This cannot be undone.", + "done": "Booking deleted", + "failed": "The booking could not be deleted" }, "detail": { "title": "Booking", diff --git a/apps/frontend/messages/fr.json b/apps/frontend/messages/fr.json index b1a5545..ba2b85c 100644 --- a/apps/frontend/messages/fr.json +++ b/apps/frontend/messages/fr.json @@ -532,7 +532,14 @@ "actions": { "view": "Voir", "pay": "Payer", - "edit": "Modifier" + "edit": "Modifier", + "delete": "Supprimer" + }, + "deleteConfirm": { + "title": "Supprimer cette réservation ?", + "body": "{reference} sera définitivement supprimée. Cette action est irréversible.", + "done": "Réservation supprimée", + "failed": "La réservation n’a pas pu être supprimée" }, "detail": { "title": "Réservation", diff --git a/apps/frontend/src/lib/api/bookings.ts b/apps/frontend/src/lib/api/bookings.ts index 3ca3dca..471c151 100644 --- a/apps/frontend/src/lib/api/bookings.ts +++ b/apps/frontend/src/lib/api/bookings.ts @@ -4,7 +4,7 @@ * Endpoints for managing container bookings */ -import { get, post, patch, upload } from './client'; +import { get, post, patch, del, upload } from './client'; import type { CreateBookingRequest, BookingResponse, @@ -39,6 +39,8 @@ export interface UpdateBookingStatusRequest { export interface CsvBookingResponse { id: string; bookingId: string; + /** Référence affichée, ex. « XPD-2026-R9KE8U ». */ + bookingNumber?: string; carrierName: string; carrierEmail: string; origin: string; @@ -51,7 +53,13 @@ export interface CsvBookingResponse { primaryCurrency: string; transitDays: number; containerType: string; - status: 'PENDING_PAYMENT' | 'PENDING_BANK_TRANSFER' | 'PENDING' | 'ACCEPTED' | 'REJECTED' | 'CANCELLED'; + status: + | 'PENDING_PAYMENT' + | 'PENDING_BANK_TRANSFER' + | 'PENDING' + | 'ACCEPTED' + | 'REJECTED' + | 'CANCELLED'; documents: Array<{ type: string; fileName: string; @@ -67,6 +75,19 @@ export interface CsvBookingResponse { commissionRate?: number; commissionAmountEur?: number; options?: Record; + + // Champs renvoyes par l'API mais absents de ce type : la page de detail les + // affichait sans que TypeScript puisse les verifier. + requestedAt?: string; + respondedAt?: string; + rejectionReason?: string; + routeDescription?: string; + isExpired?: boolean; + price?: number; + freightTotal?: number; + freightCurrency?: string; + fobTotal?: number; + fobCurrency?: string; } export interface CommissionPaymentResponse { @@ -279,6 +300,15 @@ export async function cancelCsvBooking(id: string): Promise { return patch(`/api/v1/csv-bookings/${id}/cancel`, {}); } +/** + * Supprime définitivement une réservation dont la commission n'a pas été payée. + * Une réservation payée est partie chez le transporteur : elle s'annule. + * DELETE /api/v1/csv-bookings/:id + */ +export async function deleteCsvBooking(id: string): Promise { + return del(`/api/v1/csv-bookings/${id}`); +} + /** * Update cargo details of a booking awaiting payment * PATCH /api/v1/csv-bookings/:id/details diff --git a/apps/frontend/src/lib/api/index.ts b/apps/frontend/src/lib/api/index.ts index 595c792..3ff3ab0 100644 --- a/apps/frontend/src/lib/api/index.ts +++ b/apps/frontend/src/lib/api/index.ts @@ -54,6 +54,7 @@ export { listCsvBookings, getCsvBookingStats, cancelCsvBooking, + deleteCsvBooking, acceptCsvBooking, rejectCsvBooking, type CsvBookingResponse,