Le statut PENDING_PAYMENT ne designe plus un paiement en attente mais un devis : une reservation construite dont les frais de booking ne sont pas encore regles. L'enum backend devient QUOTE, converti en place par migration. Les deux onglets se partagent desormais la meme liste : - « Reservations » n'affiche que PENDING, ACCEPTED et REJECTED ; - « Historique des devis » regroupe QUOTE, PENDING_BANK_TRANSFER et CANCELLED, avec les actions propres a un devis (modifier, payer, supprimer). La table, l'export et les filtres sont extraits dans BookingListView pour eviter de dupliquer la page entre les deux vues. Tableau de bord : - l'alerte « Paiement en attente » disparait des points a traiter, les devis non regles ne bloquant aucun envoi en cours ; - « Sans reponse du transporteur » devient « En attente du transporteur ». Migration verifiee en transaction annulee sur la base de dev : les lignes PENDING_PAYMENT deviennent QUOTE, l'enum et le defaut de colonne suivent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NZJ5aowZJVAcyPZw5TiNEm
165 lines
4.7 KiB
TypeScript
165 lines
4.7 KiB
TypeScript
'use client';
|
|
|
|
import { useLocale, useTranslations } from 'next-intl';
|
|
import { CreditCard, Pencil } from 'lucide-react';
|
|
import { Link } from '@/i18n/navigation';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Callout } from '@/components/ui/callout';
|
|
import { BookingStatusBadge } from './BookingStatusBadge';
|
|
|
|
/**
|
|
* Detail d'une reservation.
|
|
*
|
|
* Extrait de la page de liste, ou il etait ecrit en ligne avec une superposition
|
|
* maison sans focus trap ni fermeture au clavier.
|
|
*/
|
|
interface BookingDetailDialogProps {
|
|
booking: any | null;
|
|
onOpenChange: (open: boolean) => void;
|
|
buildEditUrl: (booking: any) => string;
|
|
/** Namespace de la vue appelante, pour que le titre suive l'onglet. */
|
|
namespace?: string;
|
|
}
|
|
|
|
export function BookingDetailDialog({
|
|
booking,
|
|
onOpenChange,
|
|
buildEditUrl,
|
|
namespace = 'dashboard.bookingsList',
|
|
}: BookingDetailDialogProps) {
|
|
const t = useTranslations(namespace as any);
|
|
const locale = useLocale();
|
|
const dateLocale = locale === 'fr' ? 'fr-FR' : 'en-US';
|
|
|
|
if (!booking) return null;
|
|
|
|
const longDate = (value?: string) =>
|
|
value
|
|
? new Date(value).toLocaleDateString(dateLocale, {
|
|
day: '2-digit',
|
|
month: 'long',
|
|
year: 'numeric',
|
|
})
|
|
: 'N/A';
|
|
|
|
const reference = booking.bookingId
|
|
? `#${booking.bookingId}`
|
|
: `#${booking.id?.slice(0, 8).toUpperCase()}`;
|
|
|
|
return (
|
|
<Dialog open={Boolean(booking)} onOpenChange={onOpenChange}>
|
|
<DialogContent size="lg">
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{t('detail.title')} {reference}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="flex flex-col gap-4">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<BookingStatusBadge status={booking.status} />
|
|
{booking.carrierName && (
|
|
<span className="text-body-sm text-neutral-500">{booking.carrierName}</span>
|
|
)}
|
|
</div>
|
|
|
|
<dl className="grid gap-4 sm:grid-cols-2">
|
|
<Field label={t('detail.route')}>
|
|
{booking.origin || booking.originCity || 'N/A'}
|
|
{' → '}
|
|
{booking.destination || booking.destinationCity || 'N/A'}
|
|
</Field>
|
|
|
|
<Field label={t('detail.date')}>
|
|
{longDate(booking.createdAt || booking.requestedAt)}
|
|
</Field>
|
|
|
|
<Field label={t('detail.cargo')}>
|
|
{booking.palletCount
|
|
? t('units.palletsCount', { count: booking.palletCount })
|
|
: '—'}
|
|
</Field>
|
|
|
|
<Field label={t('detail.weight')}>
|
|
{booking.weightKG ? t('units.kg', { value: booking.weightKG }) : 'N/A'}
|
|
{booking.volumeCBM ? ` / ${t('units.cbm', { value: booking.volumeCBM })}` : ''}
|
|
</Field>
|
|
|
|
{booking.requestedPickupDate && (
|
|
<Field label={t('detail.pickupDate')}>{longDate(booking.requestedPickupDate)}</Field>
|
|
)}
|
|
|
|
{(booking.priceEUR || booking.priceUSD) && (
|
|
<Field label={t('detail.price')} emphasis>
|
|
{booking.priceEUR ? `${booking.priceEUR} €` : `${booking.priceUSD} $`}
|
|
</Field>
|
|
)}
|
|
</dl>
|
|
|
|
{booking.status === 'QUOTE' && (
|
|
<Callout variant="warning">{t('detail.quoteNotice')}</Callout>
|
|
)}
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
{t('detail.close')}
|
|
</Button>
|
|
|
|
{booking.status === 'QUOTE' && (
|
|
<>
|
|
<Button asChild variant="outline">
|
|
<Link href={buildEditUrl(booking)} onClick={() => onOpenChange(false)}>
|
|
<Pencil />
|
|
{t('actions.edit')}
|
|
</Link>
|
|
</Button>
|
|
<Button asChild>
|
|
<Link
|
|
href={`/dashboard/booking/${booking.id}/pay`}
|
|
onClick={() => onOpenChange(false)}
|
|
>
|
|
<CreditCard />
|
|
{t('detail.payNow')}
|
|
</Link>
|
|
</Button>
|
|
</>
|
|
)}
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
function Field({
|
|
label,
|
|
children,
|
|
emphasis = false,
|
|
}: {
|
|
label: string;
|
|
children: React.ReactNode;
|
|
emphasis?: boolean;
|
|
}) {
|
|
return (
|
|
<div className="min-w-0">
|
|
<dt className="text-label uppercase text-neutral-400">{label}</dt>
|
|
<dd
|
|
className={
|
|
emphasis
|
|
? 'mt-1 text-body-sm font-semibold text-brand-navy'
|
|
: 'mt-1 text-body-sm text-neutral-700'
|
|
}
|
|
>
|
|
{children}
|
|
</dd>
|
|
</div>
|
|
);
|
|
}
|