mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 03:05:42 +00:00
feat(bookings): Integrate booking list and detail pages with API
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
Calendar,
|
Calendar,
|
||||||
MapPin,
|
MapPin,
|
||||||
@@ -22,10 +23,12 @@ import {
|
|||||||
CreditCard,
|
CreditCard,
|
||||||
FileSignature,
|
FileSignature,
|
||||||
PackageCheck,
|
PackageCheck,
|
||||||
|
LoaderCircle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import Breadcrumbs from "@/components/Breadcrumbs";
|
import Breadcrumbs from "@/components/Breadcrumbs";
|
||||||
import { getBookingById } from "./bookings.mock";
|
import { api } from "@/services/api";
|
||||||
|
import type { Freight } from "@edr/types";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
@@ -37,45 +40,67 @@ import {
|
|||||||
} from "@edr/ui-common";
|
} from "@edr/ui-common";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
// Grouping the 15 granular statuses into 6 logical progress stages for the UI tracker
|
|
||||||
const PROGRESS_STAGES = [
|
const PROGRESS_STAGES = [
|
||||||
{ label: "Request", icon: FileText, statuses: ["DRAFT", "RFQ_SUBMITTED"] },
|
{ label: "Request", icon: FileText, statuses: ["DRAFT"] },
|
||||||
{ label: "Quotation", icon: ClipboardCheck, statuses: ["QUOTATION_SENT", "QUOTATION_APPROVED", "QUOTATION_REJECTED"] },
|
{ label: "Approval", icon: ClipboardCheck, statuses: ["CONFIRMED"] },
|
||||||
{ label: "Approval", icon: ShieldCheck, statuses: ["PENDING_APPROVAL", "APPROVED"] },
|
{ label: "In Transit", icon: Train, statuses: ["IN_TRANSIT"] },
|
||||||
{ label: "Execution", icon: FileSignature, statuses: ["SIGNED_CUSTOMER", "FULLY_EXECUTED", "PAID"] },
|
{ label: "Complete", icon: PackageCheck, statuses: ["DELIVERED"] },
|
||||||
{ label: "In Transit", icon: Train, statuses: ["IN_TRANSIT", "PENDING_CONSOLIDATION", "CONSOLIDATED"] },
|
|
||||||
{ label: "Complete", icon: PackageCheck, statuses: ["COMPLETED"] },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const STATUS_MAP: Record<string, { title: string; description: string; color: string; stage: number }> = {
|
const STATUS_MAP: Record<string, { title: string; description: string; color: string; stage: number }> = {
|
||||||
DRAFT: { title: "Drafting Request", description: "Booking is being prepared and has not been submitted.", color: "text-slate-500", stage: 0 },
|
DRAFT: { title: "Drafting Request", description: "Booking is being prepared and has not been submitted.", color: "text-slate-500", stage: 0 },
|
||||||
RFQ_SUBMITTED: { title: "RFQ Submitted", description: "Request for Quotation has been sent to the operations team.", color: "text-amber-600", stage: 0 },
|
CONFIRMED: { title: "Booking Confirmed", description: "Booking has been confirmed and approved.", color: "text-emerald-600", stage: 1 },
|
||||||
QUOTATION_SENT: { title: "Quotation Received", description: "EDR has sent a formal quotation for your review.", color: "text-sky-600", stage: 1 },
|
IN_TRANSIT: { title: "Cargo Moving", description: "Shipment is currently moving through the rail network.", color: "text-sky-600", stage: 2 },
|
||||||
QUOTATION_APPROVED: { title: "Quotation Approved", description: "You have accepted the quotation terms.", color: "text-emerald-600", stage: 1 },
|
DELIVERED: { title: "Service Complete", description: "Cargo delivered and service successfully terminated.", color: "text-emerald-600", stage: 3 },
|
||||||
QUOTATION_REJECTED: { title: "Quotation Rejected", description: "The quotation was not accepted.", color: "text-red-600", stage: 1 },
|
|
||||||
PENDING_APPROVAL: { title: "Internal Approval", description: "Booking is undergoing final administrative review.", color: "text-amber-600", stage: 2 },
|
|
||||||
APPROVED: { title: "Booking Approved", description: "Request is fully approved and ready for execution.", color: "text-emerald-600", stage: 2 },
|
|
||||||
SIGNED_CUSTOMER: { title: "Customer Signed", description: "Contract has been signed by the customer.", color: "text-sky-600", stage: 3 },
|
|
||||||
FULLY_EXECUTED: { title: "Contract Executed", description: "All parties have signed. Operational setup in progress.", color: "text-indigo-600", stage: 3 },
|
|
||||||
PAID: { title: "Payment Received", description: "Initial payments confirmed. Cargo ready for dispatch.", color: "text-emerald-600", stage: 3 },
|
|
||||||
IN_TRANSIT: { title: "Cargo Moving", description: "Shipment is currently moving through the rail network.", color: "text-sky-600", stage: 4 },
|
|
||||||
PENDING_CONSOLIDATION: { title: "Consolidation Node", description: "Cargo is waiting to be consolidated with other shipments.", color: "text-amber-500", stage: 4 },
|
|
||||||
CONSOLIDATED: { title: "Load Consolidated", description: "Cargo has been successfully merged into a larger shipment.", color: "text-indigo-500", stage: 4 },
|
|
||||||
COMPLETED: { title: "Service Complete", description: "Cargo delivered and service successfully terminated.", color: "text-emerald-600", stage: 5 },
|
|
||||||
CANCELLED: { title: "Cancelled", description: "This booking process has been terminated.", color: "text-red-600", stage: -1 },
|
CANCELLED: { title: "Cancelled", description: "This booking process has been terminated.", color: "text-red-600", stage: -1 },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function BookingDetailPage() {
|
export default function BookingDetailPage() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const booking = id ? getBookingById(id) : undefined;
|
|
||||||
|
const { data: booking, isLoading, isError, error } = useQuery(
|
||||||
|
api.bookings.get.queryOptions({
|
||||||
|
input: { id: id! },
|
||||||
|
enabled: !!id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto flex items-center justify-center p-12">
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<LoaderCircle className="size-8 animate-spin text-primary" />
|
||||||
|
<p className="text-sm text-muted-foreground">Loading booking details…</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6">
|
||||||
|
<Card className="flex flex-col items-center p-12 text-center">
|
||||||
|
<div className="flex size-16 items-center justify-center rounded-full bg-red-50 text-red-400">
|
||||||
|
<AlertTriangle className="size-8" />
|
||||||
|
</div>
|
||||||
|
<h1 className="mt-4 text-2xl font-bold text-slate-900">
|
||||||
|
Failed to load booking
|
||||||
|
</h1>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
|
{error instanceof Error ? error.message : "An unexpected error occurred."}
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!booking) {
|
if (!booking) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-6">
|
<div className="container mx-auto p-6">
|
||||||
<Card className="flex flex-col items-center p-12 text-center">
|
<Card className="flex flex-col items-center p-12 text-center">
|
||||||
<div className="flex size-16 items-center justify-center rounded-full bg-slate-100 text-slate-400">
|
<div className="flex size-16 items-center justify-center rounded-full bg-slate-100 text-slate-400">
|
||||||
<Package className="size-8" />
|
<Package className="size-8" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="mt-4 text-2xl font-bold text-slate-900">
|
<h1 className="mt-4 text-2xl font-bold text-slate-900">
|
||||||
Booking not found
|
Booking not found
|
||||||
@@ -85,16 +110,17 @@ export default function BookingDetailPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize status to upper case for mapping
|
const normalizedStatus = booking.status as keyof typeof STATUS_MAP;
|
||||||
const normalizedStatus = (booking.status === "In Transit" ? "IN_TRANSIT" : booking.status === "Pending" ? "RFQ_SUBMITTED" : booking.status.toUpperCase()) as keyof typeof STATUS_MAP;
|
|
||||||
const statusConfig = STATUS_MAP[normalizedStatus] || STATUS_MAP.DRAFT;
|
const statusConfig = STATUS_MAP[normalizedStatus] || STATUS_MAP.DRAFT;
|
||||||
const currentStageIndex = statusConfig.stage;
|
const currentStageIndex = statusConfig.stage;
|
||||||
|
|
||||||
|
const containerCount = booking.containers?.reduce((sum, c) => sum + c.qty, 0) ?? 0;
|
||||||
|
const containerType = booking.containers?.[0]?.type ?? null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto max-w-7xl px-4 py-8">
|
<div className="container mx-auto max-w-7xl px-4 py-8">
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
|
|
||||||
{/* Breadcrumbs Restored */}
|
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
items={[
|
items={[
|
||||||
{ label: "Bookings", href: "/bookings" },
|
{ label: "Bookings", href: "/bookings" },
|
||||||
@@ -102,7 +128,6 @@ export default function BookingDetailPage() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Compact Header Card */}
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center gap-6">
|
<div className="flex items-center gap-6">
|
||||||
@@ -117,11 +142,9 @@ export default function BookingDetailPage() {
|
|||||||
<StatusBadge status={normalizedStatus} />
|
<StatusBadge status={normalizedStatus} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
||||||
<span className="font-semibold">{booking.customer}</span>
|
|
||||||
<Separator orientation="vertical" className="h-3" />
|
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Calendar className="size-3" />
|
<Calendar className="size-3" />
|
||||||
{booking.requestedDate}
|
{booking.scheduledDate ?? booking.createdAt}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,7 +152,6 @@ export default function BookingDetailPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Granular Status Lifecycle */}
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-base">
|
<CardTitle className="flex items-center gap-2 text-base">
|
||||||
@@ -140,7 +162,6 @@ export default function BookingDetailPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-8">
|
<CardContent className="flex flex-col gap-8">
|
||||||
<div className="relative flex w-full justify-between px-2">
|
<div className="relative flex w-full justify-between px-2">
|
||||||
{/* Progress Line */}
|
|
||||||
<div className="absolute top-4 left-0 h-0.5 w-full bg-muted">
|
<div className="absolute top-4 left-0 h-0.5 w-full bg-muted">
|
||||||
<div
|
<div
|
||||||
className="h-full bg-primary transition-all duration-500"
|
className="h-full bg-primary transition-all duration-500"
|
||||||
@@ -185,7 +206,7 @@ export default function BookingDetailPage() {
|
|||||||
{statusConfig.description}
|
{statusConfig.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{normalizedStatus !== "CANCELLED" && normalizedStatus !== "COMPLETED" && (
|
{normalizedStatus !== "CANCELLED" && normalizedStatus !== "DELIVERED" && (
|
||||||
<div className="ml-auto flex items-center gap-4 border-l border-border pl-6">
|
<div className="ml-auto flex items-center gap-4 border-l border-border pl-6">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<p className="text-[9px] font-bold uppercase text-muted-foreground">Est. Waiting</p>
|
<p className="text-[9px] font-bold uppercase text-muted-foreground">Est. Waiting</p>
|
||||||
@@ -200,7 +221,6 @@ export default function BookingDetailPage() {
|
|||||||
|
|
||||||
<div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
|
<div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
|
||||||
<div className="flex flex-col gap-8 lg:col-span-2">
|
<div className="flex flex-col gap-8 lg:col-span-2">
|
||||||
{/* Route & Core Service Card */}
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-base">
|
<CardTitle className="flex items-center gap-2 text-base">
|
||||||
@@ -232,14 +252,13 @@ export default function BookingDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||||
<InfoItem icon={<Layers />} label="Service" value="Rail & Forwarding" />
|
<InfoItem icon={<Layers />} label="Service" value={booking.serviceType === "RAIL_AND_FORWARDING" ? "Rail & Forwarding" : "Rail Only"} />
|
||||||
<InfoItem icon={<ShieldCheck />} label="Return" value="With Return" />
|
<InfoItem icon={<ShieldCheck />} label="Return" value={booking.equipmentReturn === "WITH_RETURN" ? "With Return" : "Without Return"} />
|
||||||
<InfoItem icon={<FileText />} label="Customs" value="Enabled" />
|
<InfoItem icon={<FileText />} label="Trade" value={booking.tradeDirection === "IMPORT" ? "Import" : "Export"} />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Mile Services Card */}
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-base">
|
<CardTitle className="flex items-center gap-2 text-base">
|
||||||
@@ -252,18 +271,19 @@ export default function BookingDetailPage() {
|
|||||||
<h3 className="border-l-4 border-primary pl-3 text-xs font-bold uppercase tracking-wide text-foreground">
|
<h3 className="border-l-4 border-primary pl-3 text-xs font-bold uppercase tracking-wide text-foreground">
|
||||||
First Mile
|
First Mile
|
||||||
</h3>
|
</h3>
|
||||||
<InfoItem label="Address" value="Inside Addis Ababa Yard, Gate 2" />
|
<InfoItem label="Address" value={booking.firstMileEnabled && booking.firstMilePickupAddress ? booking.firstMilePickupAddress : "Not requested"} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<h3 className="border-l-4 border-primary pl-3 text-xs font-bold uppercase tracking-wide text-foreground">
|
<h3 className="border-l-4 border-primary pl-3 text-xs font-bold uppercase tracking-wide text-foreground">
|
||||||
Last Mile
|
Last Mile
|
||||||
</h3>
|
</h3>
|
||||||
<p className="pl-4 text-xs text-muted-foreground italic">Not requested</p>
|
<p className="pl-4 text-xs text-muted-foreground italic">
|
||||||
|
{booking.lastMileEnabled && booking.lastMileDeliveryAddress ? booking.lastMileDeliveryAddress : "Not requested"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Cargo Specifications Card */}
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-base">
|
<CardTitle className="flex items-center gap-2 text-base">
|
||||||
@@ -273,40 +293,44 @@ export default function BookingDetailPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-6">
|
<CardContent className="flex flex-col gap-6">
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||||
<InfoItem icon={<Package />} label="Category" value={booking.cargoType} />
|
<InfoItem icon={<Package />} label="Freight Type" value={booking.freightType === "BULK" ? "Bulk" : "Break Bulk"} />
|
||||||
<InfoItem icon={<Weight />} label="Weight" value={`${booking.weightTons} Tons`} />
|
<InfoItem icon={<Weight />} label="Weight (VGM)" value={`${booking.cargoTotalWeightVgm} Tons`} />
|
||||||
<InfoItem icon={<Ship />} label="Shipping Line" value="MSC" />
|
<InfoItem icon={<Ship />} label="Currency" value={booking.paymentCurrency} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator />
|
{booking.containers && booking.containers.length > 0 && (
|
||||||
|
<>
|
||||||
<div className="flex flex-col gap-3">
|
<Separator />
|
||||||
<h3 className="text-xs font-bold uppercase tracking-wide text-foreground">Load Details</h3>
|
<div className="flex flex-col gap-3">
|
||||||
<div className="rounded-lg border overflow-hidden">
|
<h3 className="text-xs font-bold uppercase tracking-wide text-foreground">Load Details</h3>
|
||||||
<table className="w-full text-left text-xs">
|
<div className="rounded-lg border overflow-hidden">
|
||||||
<thead className="bg-muted text-muted-foreground">
|
<table className="w-full text-left text-xs">
|
||||||
<tr>
|
<thead className="bg-muted text-muted-foreground">
|
||||||
<th className="px-3 py-2 font-semibold">Description</th>
|
<tr>
|
||||||
<th className="px-3 py-2 font-semibold text-center">Unit</th>
|
<th className="px-3 py-2 font-semibold">Type</th>
|
||||||
<th className="px-3 py-2 font-semibold text-right">Value</th>
|
<th className="px-3 py-2 font-semibold text-center">Quantity</th>
|
||||||
</tr>
|
<th className="px-3 py-2 font-semibold text-right">VGM (Tons)</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody className="divide-y">
|
</thead>
|
||||||
<tr>
|
<tbody className="divide-y">
|
||||||
<td className="px-3 py-2 font-medium">Main Equipment</td>
|
{booking.containers.map((c, i) => (
|
||||||
<td className="px-3 py-2 text-center">20FT Container</td>
|
<tr key={i}>
|
||||||
<td className="px-3 py-2 text-right">4 Units</td>
|
<td className="px-3 py-2 font-medium">{c.type}</td>
|
||||||
</tr>
|
<td className="px-3 py-2 text-center">{c.qty} Units</td>
|
||||||
</tbody>
|
<td className="px-3 py-2 text-right">{c.vgm}t</td>
|
||||||
</table>
|
</tr>
|
||||||
</div>
|
))}
|
||||||
</div>
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
{/* Contract Card */}
|
|
||||||
<Card className="border-primary/20 bg-primary/[0.02]">
|
<Card className="border-primary/20 bg-primary/[0.02]">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-base">
|
<CardTitle className="flex items-center gap-2 text-base">
|
||||||
@@ -315,40 +339,48 @@ export default function BookingDetailPage() {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
<InfoItem label="Type" value="Renewal" />
|
<InfoItem label="Type" value={booking.contractType === "RENEWAL" ? "Renewal" : "New"} />
|
||||||
<InfoItem label="Ref" value="EDR-2024-88123" />
|
<InfoItem label="Customer ID" value={booking.customerId} />
|
||||||
<Separator />
|
<Separator />
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<Badge variant="outline" className="bg-background text-[9px]">
|
<Badge variant="outline" className="bg-background text-[9px]">
|
||||||
Hazardous: No
|
Hazardous: {booking.isHazardous ? "Yes" : "No"}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="outline" className="bg-background text-[9px]">
|
<Badge variant="outline" className="bg-background text-[9px]">
|
||||||
Refrigerated: No
|
Refrigerated: {booking.isRefrigerated ? "Yes" : "No"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Notes Card */}
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-base">Additional Info</CardTitle>
|
<CardTitle className="text-base">Additional Info</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-1">
|
{booking.freightSubtype && (
|
||||||
<p className="text-[9px] font-bold uppercase tracking-tight text-muted-foreground">Description</p>
|
<div className="flex flex-col gap-1">
|
||||||
<p className="text-xs text-foreground leading-relaxed italic">"{booking.cargoDescription}"</p>
|
<p className="text-[9px] font-bold uppercase tracking-tight text-muted-foreground">Cargo Description</p>
|
||||||
</div>
|
<p className="text-xs text-foreground leading-relaxed italic">"{booking.freightSubtype}"</p>
|
||||||
<Separator />
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
)}
|
||||||
<p className="text-[9px] font-bold uppercase tracking-tight text-muted-foreground">Instructions</p>
|
{booking.financialTerms && (
|
||||||
<div className="rounded-lg bg-amber-50/50 border border-amber-100 p-2">
|
<>
|
||||||
<p className="text-xs text-amber-900 flex gap-2">
|
<Separator />
|
||||||
<StickyNote className="size-3 shrink-0 mt-0.5 text-amber-500" />
|
<div className="flex flex-col gap-1">
|
||||||
{booking.specialInstructions}
|
<p className="text-[9px] font-bold uppercase tracking-tight text-muted-foreground">Financial Terms</p>
|
||||||
</p>
|
<div className="rounded-lg bg-amber-50/50 border border-amber-100 p-2">
|
||||||
</div>
|
<p className="text-xs text-amber-900 flex gap-2">
|
||||||
</div>
|
<StickyNote className="size-3 shrink-0 mt-0.5 text-amber-500" />
|
||||||
|
{booking.financialTerms}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!booking.freightSubtype && !booking.financialTerms && (
|
||||||
|
<p className="text-xs text-muted-foreground italic">No additional information provided.</p>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -396,7 +428,7 @@ function InfoItem({
|
|||||||
{icon && <div className="mt-0.5 text-muted-foreground [&_svg]:size-3.5">{icon}</div>}
|
{icon && <div className="mt-0.5 text-muted-foreground [&_svg]:size-3.5">{icon}</div>}
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<p className="text-[9px] font-bold uppercase tracking-tight text-muted-foreground">{label}</p>
|
<p className="text-[9px] font-bold uppercase tracking-tight text-muted-foreground">{label}</p>
|
||||||
<p className="text-xs font-bold text-foreground">{value || "—"}</p>
|
<p className="text-xs font-bold text-foreground">{value ?? "—"}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -405,20 +437,10 @@ function InfoItem({
|
|||||||
function StatusBadge({ status }: { status: string }) {
|
function StatusBadge({ status }: { status: string }) {
|
||||||
const statusColors: Record<string, string> = {
|
const statusColors: Record<string, string> = {
|
||||||
DRAFT: "bg-slate-50 text-slate-700 border-slate-200",
|
DRAFT: "bg-slate-50 text-slate-700 border-slate-200",
|
||||||
RFQ_SUBMITTED: "bg-amber-50 text-amber-700 border-amber-200",
|
CONFIRMED: "bg-emerald-50 text-emerald-700 border-emerald-200",
|
||||||
QUOTATION_SENT: "bg-sky-50 text-sky-700 border-sky-200",
|
|
||||||
QUOTATION_APPROVED: "bg-emerald-50 text-emerald-700 border-emerald-200",
|
|
||||||
QUOTATION_REJECTED: "bg-red-50 text-red-700 border-red-200",
|
|
||||||
PENDING_APPROVAL: "bg-amber-50 text-amber-700 border-amber-200",
|
|
||||||
APPROVED: "bg-emerald-50 text-emerald-700 border-emerald-200",
|
|
||||||
SIGNED_CUSTOMER: "bg-sky-50 text-sky-700 border-sky-200",
|
|
||||||
FULLY_EXECUTED: "bg-indigo-50 text-indigo-700 border-indigo-200",
|
|
||||||
PAID: "bg-emerald-50 text-emerald-700 border-emerald-200",
|
|
||||||
IN_TRANSIT: "bg-sky-50 text-sky-700 border-sky-200",
|
IN_TRANSIT: "bg-sky-50 text-sky-700 border-sky-200",
|
||||||
COMPLETED: "bg-indigo-50 text-indigo-700 border-indigo-200",
|
DELIVERED: "bg-indigo-50 text-indigo-700 border-indigo-200",
|
||||||
CANCELLED: "bg-red-50 text-red-700 border-red-200",
|
CANCELLED: "bg-red-50 text-red-700 border-red-200",
|
||||||
PENDING_CONSOLIDATION: "bg-amber-50 text-amber-700 border-amber-200",
|
|
||||||
CONSOLIDATED: "bg-indigo-50 text-indigo-700 border-indigo-200",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
Clock,
|
Clock,
|
||||||
@@ -9,13 +10,11 @@ import {
|
|||||||
Package,
|
Package,
|
||||||
Plus,
|
Plus,
|
||||||
Search,
|
Search,
|
||||||
Trash2,
|
|
||||||
Truck,
|
Truck,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import DeleteBookingDialog from "./DeleteBookingDialog";
|
import { api } from "@/services/api";
|
||||||
import { getMyBookings } from "@/lib/currentCustomer";
|
import type { Freight } from "@edr/types";
|
||||||
import { deleteBooking, type Booking, type BookingStatus } from "./bookings.mock";
|
|
||||||
import {
|
import {
|
||||||
DataTable,
|
DataTable,
|
||||||
DataTableFooter,
|
DataTableFooter,
|
||||||
@@ -32,32 +31,30 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuSeparator,
|
|
||||||
} from "@edr/ui-common";
|
} from "@edr/ui-common";
|
||||||
|
|
||||||
export default function MyBookings() {
|
export default function MyBookings() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { pagination, setPagination } = usePagination({ pageSize: 10 });
|
const { pagination, setPagination } = usePagination({ pageSize: 10 });
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [myBookings, setMyBookings] = useState(() => getMyBookings());
|
|
||||||
|
|
||||||
const handleDeleteConfirm = (id: number) => {
|
const { data, isLoading, isError } = useQuery(
|
||||||
deleteBooking(id);
|
api.bookings.list.queryOptions(),
|
||||||
setMyBookings(getMyBookings());
|
);
|
||||||
};
|
|
||||||
|
const bookings = data?.items ?? [];
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
return myBookings.filter((b) => {
|
return bookings.filter((b) => {
|
||||||
const term = searchTerm.toLowerCase();
|
const term = searchTerm.toLowerCase();
|
||||||
return (
|
return (
|
||||||
b.reference.toLowerCase().includes(term) ||
|
b.reference.toLowerCase().includes(term) ||
|
||||||
b.originStation.toLowerCase().includes(term) ||
|
b.originStation.toLowerCase().includes(term) ||
|
||||||
b.destinationStation.toLowerCase().includes(term) ||
|
b.destinationStation.toLowerCase().includes(term) ||
|
||||||
b.cargoDescription.toLowerCase().includes(term) ||
|
|
||||||
b.status.toLowerCase().includes(term)
|
b.status.toLowerCase().includes(term)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}, [myBookings, searchTerm]);
|
}, [bookings, searchTerm]);
|
||||||
|
|
||||||
const total = filteredData.length;
|
const total = filteredData.length;
|
||||||
const pageCount = Math.ceil(total / pagination.pageSize);
|
const pageCount = Math.ceil(total / pagination.pageSize);
|
||||||
@@ -67,16 +64,16 @@ export default function MyBookings() {
|
|||||||
const paginatedData = useMemo(() => filteredData.slice(start, end), [filteredData, start, end]);
|
const paginatedData = useMemo(() => filteredData.slice(start, end), [filteredData, start, end]);
|
||||||
|
|
||||||
const activeCount = useMemo(() => {
|
const activeCount = useMemo(() => {
|
||||||
return myBookings.filter(
|
return bookings.filter(
|
||||||
(b) => b.status === "Confirmed" || b.status === "In Transit",
|
(b) => b.status === "CONFIRMED" || b.status === "IN_TRANSIT",
|
||||||
).length;
|
).length;
|
||||||
}, [myBookings]);
|
}, [bookings]);
|
||||||
|
|
||||||
const pendingCount = useMemo(() => {
|
const pendingCount = useMemo(() => {
|
||||||
return myBookings.filter((b) => b.status === "Pending").length;
|
return bookings.filter((b) => b.status === "DRAFT").length;
|
||||||
}, [myBookings]);
|
}, [bookings]);
|
||||||
|
|
||||||
const columns: ColumnDef<Booking>[] = [
|
const columns: ColumnDef<Freight.IBooking>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "reference",
|
accessorKey: "reference",
|
||||||
header: "Reference",
|
header: "Reference",
|
||||||
@@ -89,7 +86,7 @@ export default function MyBookings() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-slate-900">{booking.reference}</p>
|
<p className="font-medium text-slate-900">{booking.reference}</p>
|
||||||
<p className="text-sm text-slate-500">{booking.requestedDate}</p>
|
<p className="text-sm text-slate-500">{booking.scheduledDate ?? booking.createdAt}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -111,22 +108,24 @@ export default function MyBookings() {
|
|||||||
header: "Cargo",
|
header: "Cargo",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const b = row.original;
|
const b = row.original;
|
||||||
|
const containerCount = b.containers?.reduce((sum, c) => sum + c.qty, 0) ?? 0;
|
||||||
|
const containerType = b.containers?.[0]?.type ?? null;
|
||||||
return (
|
return (
|
||||||
<div className="text-sm text-slate-700">
|
<div className="text-sm text-slate-700">
|
||||||
<p>{b.cargoType}</p>
|
<p>{b.freightType === "BULK" ? "Bulk" : "Break Bulk"}</p>
|
||||||
<p className="text-xs text-slate-500">
|
<p className="text-xs text-slate-500">
|
||||||
{b.containerCount > 0 ? `${b.containerCount} × ${b.containerType} · ` : ""}{b.weightTons}t
|
{containerType && containerCount > 0 ? `${containerCount} × ${containerType} · ` : ""}{b.cargoTotalWeightVgm}t
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "transportMode",
|
id: "transportMode",
|
||||||
header: "Transport",
|
header: "Transport",
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span className="text-sm text-slate-700">
|
<span className="text-sm text-slate-700">
|
||||||
{row.original.transportMode}
|
{row.original.serviceType === "RAIL_AND_FORWARDING" ? "Rail & Forwarding" : "Rail"}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -158,19 +157,6 @@ export default function MyBookings() {
|
|||||||
<Eye />
|
<Eye />
|
||||||
View
|
View
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DeleteBookingDialog
|
|
||||||
bookingReference={booking.reference}
|
|
||||||
onConfirm={() => handleDeleteConfirm(booking.id)}
|
|
||||||
>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onSelect={(e: Event) => e.preventDefault()}
|
|
||||||
variant="destructive"
|
|
||||||
>
|
|
||||||
<Trash2 />
|
|
||||||
Delete
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DeleteBookingDialog>
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
@@ -179,10 +165,11 @@ export default function MyBookings() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const dataTableStatus = isLoading ? "loading" : isError ? "error" : "success";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen p-6">
|
<div className="min-h-screen p-6">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header Section Card */}
|
|
||||||
<Card className="p-6 flex-row justify-between">
|
<Card className="p-6 flex-row justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight text-slate-900">
|
<h1 className="text-3xl font-bold tracking-tight text-slate-900">
|
||||||
@@ -214,14 +201,13 @@ export default function MyBookings() {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Stat Cards */}
|
|
||||||
<div className="grid gap-4 md:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-3">
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="flex items-center justify-between">
|
<CardContent className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-slate-500">Total Bookings</p>
|
<p className="text-sm text-slate-500">Total Bookings</p>
|
||||||
<h3 className="mt-2 text-3xl font-bold text-slate-900">
|
<h3 className="mt-2 text-3xl font-bold text-slate-900">
|
||||||
{myBookings.length}
|
{bookings.length}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-primary/10 text-primary">
|
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-primary/10 text-primary">
|
||||||
@@ -259,7 +245,6 @@ export default function MyBookings() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Data Table */}
|
|
||||||
<Card className="gap-0">
|
<Card className="gap-0">
|
||||||
<CardHeader className="flex flex-row items-center justify-between border-b">
|
<CardHeader className="flex flex-row items-center justify-between border-b">
|
||||||
<div>
|
<div>
|
||||||
@@ -276,7 +261,7 @@ export default function MyBookings() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="px-0">
|
<CardContent className="px-0">
|
||||||
{total === 0 ? (
|
{total === 0 && dataTableStatus === "success" ? (
|
||||||
<div className="flex flex-col items-center justify-center py-12 px-6 text-center">
|
<div className="flex flex-col items-center justify-center py-12 px-6 text-center">
|
||||||
<Package className="h-12 w-12 text-slate-300 mb-4" />
|
<Package className="h-12 w-12 text-slate-300 mb-4" />
|
||||||
<h3 className="text-sm font-semibold text-slate-900">No bookings found</h3>
|
<h3 className="text-sm font-semibold text-slate-900">No bookings found</h3>
|
||||||
@@ -288,8 +273,8 @@ export default function MyBookings() {
|
|||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={paginatedData}
|
data={paginatedData}
|
||||||
status="success"
|
status={dataTableStatus}
|
||||||
onRowClick={(row) => navigate(`/bookings/${(row as Booking).id}`)}
|
onRowClick={(row) => navigate(`/bookings/${(row as Freight.IBooking).id}`)}
|
||||||
pagination={{
|
pagination={{
|
||||||
pageIndex: pagination.pageIndex,
|
pageIndex: pagination.pageIndex,
|
||||||
pageSize: pagination.pageSize,
|
pageSize: pagination.pageSize,
|
||||||
@@ -311,20 +296,20 @@ export default function MyBookings() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusBadge({ status }: { status: BookingStatus }) {
|
function StatusBadge({ status }: { status: string }) {
|
||||||
const styles: Record<BookingStatus, string> = {
|
const styles: Record<string, string> = {
|
||||||
Pending: "bg-amber-100 text-amber-700",
|
DRAFT: "bg-amber-100 text-amber-700",
|
||||||
Confirmed: "bg-sky-100 text-sky-700",
|
CONFIRMED: "bg-sky-100 text-sky-700",
|
||||||
"In Transit": "bg-indigo-100 text-indigo-700",
|
IN_TRANSIT: "bg-indigo-100 text-indigo-700",
|
||||||
Delivered: "bg-emerald-100 text-emerald-700",
|
DELIVERED: "bg-emerald-100 text-emerald-700",
|
||||||
Cancelled: "bg-red-100 text-red-700",
|
CANCELLED: "bg-red-100 text-red-700",
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={`inline-flex rounded-full px-3 py-1 text-xs font-medium ${styles[status]}`}
|
className={`inline-flex rounded-full px-3 py-1 text-xs font-medium ${styles[status] ?? "bg-slate-100 text-slate-700"}`}
|
||||||
>
|
>
|
||||||
{status}
|
{status.replace(/_/g, ' ')}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,22 +6,22 @@ export type CreateBookingPayload = Freight.CreateBookingDto;
|
|||||||
|
|
||||||
export const bookingsService = {
|
export const bookingsService = {
|
||||||
list: async (): Promise<PaginatedResponse<Freight.IBooking>> => {
|
list: async (): Promise<PaginatedResponse<Freight.IBooking>> => {
|
||||||
const { data } = await client.get("/bookings");
|
const { data } = await client.get("/api/bookings");
|
||||||
return data.data;
|
return data.data;
|
||||||
},
|
},
|
||||||
get: async (id: string): Promise<Freight.IBooking> => {
|
get: async (id: string): Promise<Freight.IBooking> => {
|
||||||
const { data } = await client.get(`/bookings/${id}`);
|
const { data } = await client.get(`/api/bookings/${id}`);
|
||||||
return data.data;
|
return data.data;
|
||||||
},
|
},
|
||||||
create: async (payload: CreateBookingPayload): Promise<Freight.IBooking> => {
|
create: async (payload: CreateBookingPayload): Promise<Freight.IBooking> => {
|
||||||
const { data } = await client.post("/api/bookings", payload);
|
const { data } = await client.post("/api/bookings", payload);
|
||||||
return data.data;
|
return data.data.booking;
|
||||||
},
|
},
|
||||||
getReferenceData: async (): Promise<Freight.BookingReferenceData> => {
|
getReferenceData: async (): Promise<Freight.BookingReferenceData> => {
|
||||||
const { data } = await client.get("/api/bookings/reference-data");
|
const { data } = await client.get("/api/bookings/reference-data");
|
||||||
return data.data;
|
return data.data;
|
||||||
},
|
},
|
||||||
remove: async (id: string): Promise<void> => {
|
remove: async (id: string): Promise<void> => {
|
||||||
await client.delete(`/bookings/${id}`);
|
await client.delete(`/api/bookings/${id}`);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user