240 lines
8.3 KiB
TypeScript
240 lines
8.3 KiB
TypeScript
'use client';
|
|
|
|
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 { getBooking } from '@/lib/api';
|
|
import { Link } from '@/i18n/navigation';
|
|
import { Badge, type BadgeProps } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
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<string, NonNullable<BadgeProps['variant']>> = {
|
|
draft: 'neutral',
|
|
pending: 'warning',
|
|
confirmed: 'info',
|
|
in_transit: 'info',
|
|
delivered: 'success',
|
|
cancelled: 'danger',
|
|
};
|
|
|
|
export default function BookingDetailPage() {
|
|
const t = useTranslations('dashboard.bookingDetail');
|
|
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 statusLabel = (status: string) => {
|
|
try {
|
|
return t(`status.${status}` as any);
|
|
} catch {
|
|
return status;
|
|
}
|
|
};
|
|
|
|
// 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 (isLoading) return <PageSpinner label={tCommon('loading')} />;
|
|
|
|
if (!booking) {
|
|
return (
|
|
<EmptyState
|
|
icon={Package}
|
|
title={t('notFound')}
|
|
action={
|
|
<Button asChild variant="outline">
|
|
<Link href="/dashboard/bookings">
|
|
<ArrowLeft />
|
|
{t('back')}
|
|
</Link>
|
|
</Button>
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<PageHeader
|
|
title={booking.bookingNumber}
|
|
description={t('createdOn', {
|
|
date: new Date(booking.createdAt).toLocaleDateString(dateLocale),
|
|
})}
|
|
actions={
|
|
<>
|
|
<Badge variant={STATUS_VARIANT[booking.status] ?? 'neutral'}>
|
|
{statusLabel(booking.status)}
|
|
</Badge>
|
|
<Button variant="outline" onClick={downloadPDF}>
|
|
<Download />
|
|
{t('downloadPdf')}
|
|
</Button>
|
|
</>
|
|
}
|
|
/>
|
|
|
|
<div className="grid gap-4 lg:grid-cols-3">
|
|
<div className="flex flex-col gap-4 lg:col-span-2">
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle>{t('cargo.title')}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<DescriptionList
|
|
columns={1}
|
|
items={[
|
|
{ label: t('cargo.description'), value: booking.cargoDescription },
|
|
...(booking.specialInstructions
|
|
? [
|
|
{
|
|
label: t('cargo.specialInstructions'),
|
|
value: booking.specialInstructions,
|
|
},
|
|
]
|
|
: []),
|
|
]}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle>
|
|
{t('containers.title', { count: booking.containers?.length || 0 })}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{booking.containers && booking.containers.length > 0 ? (
|
|
<ul className="flex flex-col gap-3">
|
|
{booking.containers.map((container, index) => (
|
|
<li
|
|
key={container.id || index}
|
|
className="rounded-md border border-border p-4"
|
|
>
|
|
<DescriptionList
|
|
items={[
|
|
{ label: t('containers.type'), value: container.type },
|
|
...(container.containerNumber
|
|
? [{ label: t('containers.number'), value: container.containerNumber }]
|
|
: []),
|
|
...(container.sealNumber
|
|
? [{ label: t('containers.seal'), value: container.sealNumber }]
|
|
: []),
|
|
...(container.vgm
|
|
? [{ label: t('containers.vgm'), value: container.vgm }]
|
|
: []),
|
|
]}
|
|
/>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
) : (
|
|
<EmptyState size="sm" icon={Package} title={t('containers.title', { count: 0 })} />
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle>{t('shipper.title')}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<DescriptionList
|
|
columns={1}
|
|
items={[
|
|
{ label: t('shipper.name'), value: booking.shipper.name, emphasis: true },
|
|
{ label: t('shipper.contact'), value: booking.shipper.contactName },
|
|
{ label: t('shipper.email'), value: booking.shipper.contactEmail },
|
|
{ label: t('shipper.phone'), value: booking.shipper.contactPhone },
|
|
]}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle>{t('consignee.title')}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<DescriptionList
|
|
columns={1}
|
|
items={[
|
|
{ label: t('consignee.name'), value: booking.consignee.name, emphasis: true },
|
|
{ label: t('consignee.contact'), value: booking.consignee.contactName },
|
|
{ label: t('consignee.email'), value: booking.consignee.contactEmail },
|
|
{ label: t('consignee.phone'), value: booking.consignee.contactPhone },
|
|
]}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-4">
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle>{t('timeline.title')}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ol className="relative flex flex-col gap-4">
|
|
<li className="flex gap-3">
|
|
<span
|
|
aria-hidden="true"
|
|
className="mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-full bg-brand-blue/10"
|
|
>
|
|
<FileText className="size-3 text-brand-blue" />
|
|
</span>
|
|
<div className="min-w-0">
|
|
<p className="text-body-sm font-medium text-brand-navy">
|
|
{t('timeline.created')}
|
|
</p>
|
|
<p className="mt-0.5 text-body-xs text-neutral-500">
|
|
{new Date(booking.createdAt).toLocaleString(dateLocale)}
|
|
</p>
|
|
</div>
|
|
</li>
|
|
</ol>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle>{t('info.title')}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<DescriptionList
|
|
columns={1}
|
|
items={[
|
|
{ label: t('info.bookingId'), value: booking.id },
|
|
{
|
|
label: t('info.lastUpdated'),
|
|
value: new Date(booking.updatedAt).toLocaleString(dateLocale),
|
|
},
|
|
]}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|