From d89104f49ef747103f045dbfaaa0519c14a4c8e0 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 7 Sep 2026 21:40:56 +0200 Subject: [PATCH] fix(ui): la page de detail lit csv-bookings, la table bookings n existe pas Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C --- .../[locale]/dashboard/bookings/[id]/page.tsx | 367 ++++++++++-------- 1 file changed, 198 insertions(+), 169 deletions(-) diff --git a/apps/frontend/app/[locale]/dashboard/bookings/[id]/page.tsx b/apps/frontend/app/[locale]/dashboard/bookings/[id]/page.tsx index 8b94282..717f55f 100644 --- a/apps/frontend/app/[locale]/dashboard/bookings/[id]/page.tsx +++ b/apps/frontend/app/[locale]/dashboard/bookings/[id]/page.tsx @@ -3,61 +3,68 @@ import { useQuery } from '@tanstack/react-query'; import { useParams } from 'next/navigation'; import { useLocale, useTranslations } from 'next-intl'; -import { ArrowLeft, Download, FileText, Package } from 'lucide-react'; +import { ArrowLeft, ArrowRight, CreditCard, FileText, Package, Pencil } from 'lucide-react'; -import { getBooking } from '@/lib/api'; +import { getCsvBooking } from '@/lib/api'; import { Link } from '@/i18n/navigation'; -import { Badge, type BadgeProps } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; +import { Callout } from '@/components/ui/callout'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { PageHeader } from '@/components/ui/PageHeader'; -import { EmptyState, PageSpinner } from '@/components/ui/states'; -import { useToast } from '@/components/ui/toast'; -import { DescriptionList } from '@/components/data/description-list'; - -const STATUS_VARIANT: Record> = { - draft: 'neutral', - pending: 'warning', - confirmed: 'info', - in_transit: 'info', - delivered: 'success', - cancelled: 'danger', -}; +import { EmptyState, ErrorState, PageSpinner } from '@/components/ui/states'; +import { BookingStatusBadge } from '@/components/bookings/BookingStatusBadge'; +/** + * Detail d'une reservation. + * + * La page interrogeait `GET /bookings/:id` et affichait un modele — expediteur, + * destinataire, conteneurs — qui n'existe pas dans ce produit : la table + * `bookings` n'a jamais ete creee, les reservations vivent dans `csv_bookings`. + * Elle repondait donc 500 et tournait indefiniment. C'est ce qui donnait + * l'impression qu'un clic sur une notification « ne faisait rien » : la + * navigation avait bien lieu, la destination ne s'affichait jamais. + * + * Elle lit desormais `GET /csv-bookings/:id`, la meme source que la liste. + */ export default function BookingDetailPage() { const t = useTranslations('dashboard.bookingDetail'); + const tList = useTranslations('dashboard.bookingsList'); const tCommon = useTranslations('common'); const locale = useLocale(); const dateLocale = locale === 'fr' ? 'fr-FR' : 'en-US'; const params = useParams(); const bookingId = params.id as string; - const { toast } = useToast(); - const { data: booking, isLoading } = useQuery({ - queryKey: ['booking', bookingId], - queryFn: () => getBooking(bookingId), - enabled: !!bookingId, + const { + data: booking, + isPending, + isError, + error, + refetch, + } = useQuery({ + queryKey: ['csv-booking', bookingId], + queryFn: () => getCsvBooking(bookingId), + enabled: Boolean(bookingId), + // Une reservation introuvable ne le deviendra pas en reessayant : trois + // tentatives ne faisaient que prolonger le tourniquet. + retry: (failureCount, err) => + (err as { statusCode?: number })?.statusCode === 404 ? false : failureCount < 1, }); - const statusLabel = (status: string) => { - try { - return t(`status.${status}` as any); - } catch { - return status; - } - }; + const dateTime = (value?: string) => + value + ? new Date(value).toLocaleString(dateLocale, { dateStyle: 'long', timeStyle: 'short' }) + : '—'; - // Le telechargement PDF n'est pas encore branche cote back : on le signale - // par une notification plutot que par une alerte bloquante du navigateur. - const downloadPDF = () => toast.info(t('pdfNotImplemented')); + if (isPending) return ; - if (isLoading) return ; - - if (!booking) { - return ( + if (isError) { + const missing = (error as { statusCode?: number })?.statusCode === 404; + return missing ? ( @@ -67,173 +74,195 @@ export default function BookingDetailPage() { } /> + ) : ( + refetch()} + /> ); } + const unpaid = booking.status === 'PENDING_PAYMENT'; + const price = booking.priceEUR + ? `${booking.priceEUR} €` + : booking.priceUSD + ? `${booking.priceUSD} $` + : null; + return ( <> - - {statusLabel(booking.status)} - - + + {unpaid && ( + + )} } /> + {unpaid && ( + + {tList('detail.pendingPaymentNotice')} + + )} + + {booking.status === 'REJECTED' && booking.rejectionReason && ( + + {booking.rejectionReason} + + )} +
+ {/* La route est ce qu'un transitaire regarde en premier : elle occupe + la tete de page, pas une ligne de tableau parmi d'autres. */} - - {t('cargo.title')} - - - - - - - - - - {t('containers.title', { count: booking.containers?.length || 0 })} - - - - {booking.containers && booking.containers.length > 0 ? ( -
    - {booking.containers.map((container, index) => ( -
  • - -
  • - ))} -
- ) : ( - + +
+ {booking.origin} + + + {booking.destination} +
+ {booking.carrierName && ( +

+ {t('carrier', { carrier: booking.carrierName })} +

)}
-
- - - {t('shipper.title')} - - - - - + + + {tList('detail.cargo')} + + +
+ + {booking.palletCount + ? tList('units.palletsCount', { count: booking.palletCount }) + : '—'} + + + {booking.volumeCBM ? tList('units.cbm', { value: booking.volumeCBM }) : '—'} + + + {booking.weightKG ? tList('units.kg', { value: booking.weightKG }) : '—'} + +
+ {booking.notes && ( +

+ {booking.notes} +

+ )} +
+
- - - {t('consignee.title')} - - - - - -
+ + + {t('documents', { count: booking.documents?.length ?? 0 })} + + + {booking.documents?.length ? ( +
    + {booking.documents.map((document, index) => ( +
  • +
  • + ))} +
+ ) : ( + + )} +
+
- - - {t('timeline.title')} - - -
    -
  1. - -
    -

    - {t('timeline.created')} -

    -

    - {new Date(booking.createdAt).toLocaleString(dateLocale)} -

    -
    -
  2. -
-
-
+ {price && ( + + + {tList('detail.price')} + + +

{price}

+
+ {booking.freightTotal ? ( + + {booking.freightTotal} {booking.freightCurrency} + + ) : null} + {booking.fobTotal ? ( + + {booking.fobTotal} {booking.fobCurrency} + + ) : null} + {booking.commissionAmountEur ? ( + {booking.commissionAmountEur} € + ) : null} +
+
+
+ )} - {t('info.title')} + {t('timeline')} - +
+ {dateTime(booking.requestedAt)} + {booking.respondedAt && ( + {dateTime(booking.respondedAt)} + )} + {booking.id} +
+ +
); } + +function Field({ label, children }: { label: string; children: React.ReactNode }) { + return ( +
+
{label}
+
{children}
+
+ ); +}