diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage.tsx index 8b153515d..6a4f13227 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage.tsx @@ -1,4 +1,5 @@ import { useNavigate, useParams } from "react-router-dom"; +import { useQuery } from "@tanstack/react-query"; import { Calendar, MapPin, @@ -22,10 +23,12 @@ import { CreditCard, FileSignature, PackageCheck, + LoaderCircle, } from "lucide-react"; import Breadcrumbs from "@/components/Breadcrumbs"; -import { getBookingById } from "./bookings.mock"; +import { api } from "@/services/api"; +import type { Freight } from "@edr/types"; import { Card, CardHeader, @@ -37,45 +40,67 @@ import { } from "@edr/ui-common"; import { cn } from "@/lib/utils"; -// Grouping the 15 granular statuses into 6 logical progress stages for the UI tracker const PROGRESS_STAGES = [ - { label: "Request", icon: FileText, statuses: ["DRAFT", "RFQ_SUBMITTED"] }, - { label: "Quotation", icon: ClipboardCheck, statuses: ["QUOTATION_SENT", "QUOTATION_APPROVED", "QUOTATION_REJECTED"] }, - { label: "Approval", icon: ShieldCheck, statuses: ["PENDING_APPROVAL", "APPROVED"] }, - { label: "Execution", icon: FileSignature, statuses: ["SIGNED_CUSTOMER", "FULLY_EXECUTED", "PAID"] }, - { label: "In Transit", icon: Train, statuses: ["IN_TRANSIT", "PENDING_CONSOLIDATION", "CONSOLIDATED"] }, - { label: "Complete", icon: PackageCheck, statuses: ["COMPLETED"] }, + { label: "Request", icon: FileText, statuses: ["DRAFT"] }, + { label: "Approval", icon: ClipboardCheck, statuses: ["CONFIRMED"] }, + { label: "In Transit", icon: Train, statuses: ["IN_TRANSIT"] }, + { label: "Complete", icon: PackageCheck, statuses: ["DELIVERED"] }, ]; const STATUS_MAP: Record = { 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 }, - QUOTATION_SENT: { title: "Quotation Received", description: "EDR has sent a formal quotation for your review.", color: "text-sky-600", stage: 1 }, - QUOTATION_APPROVED: { title: "Quotation Approved", description: "You have accepted the quotation terms.", color: "text-emerald-600", stage: 1 }, - 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 }, + CONFIRMED: { title: "Booking Confirmed", description: "Booking has been confirmed and approved.", color: "text-emerald-600", stage: 1 }, + IN_TRANSIT: { title: "Cargo Moving", description: "Shipment is currently moving through the rail network.", color: "text-sky-600", stage: 2 }, + DELIVERED: { title: "Service Complete", description: "Cargo delivered and service successfully terminated.", color: "text-emerald-600", stage: 3 }, CANCELLED: { title: "Cancelled", description: "This booking process has been terminated.", color: "text-red-600", stage: -1 }, }; export default function BookingDetailPage() { const { id } = useParams<{ id: string }>(); 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 ( +
+
+ +

Loading booking details…

+
+
+ ); + } + + if (isError) { + return ( +
+ +
+ +
+

+ Failed to load booking +

+

+ {error instanceof Error ? error.message : "An unexpected error occurred."} +

+
+
+ ); + } if (!booking) { return (
- +

Booking not found @@ -85,16 +110,17 @@ export default function BookingDetailPage() { ); } - // Normalize status to upper case for mapping - const normalizedStatus = (booking.status === "In Transit" ? "IN_TRANSIT" : booking.status === "Pending" ? "RFQ_SUBMITTED" : booking.status.toUpperCase()) as keyof typeof STATUS_MAP; + const normalizedStatus = booking.status as keyof typeof STATUS_MAP; const statusConfig = STATUS_MAP[normalizedStatus] || STATUS_MAP.DRAFT; const currentStageIndex = statusConfig.stage; + const containerCount = booking.containers?.reduce((sum, c) => sum + c.qty, 0) ?? 0; + const containerType = booking.containers?.[0]?.type ?? null; + return (
- {/* Breadcrumbs Restored */} - {/* Compact Header Card */}
@@ -117,11 +142,9 @@ export default function BookingDetailPage() {
- {booking.customer} - - {booking.requestedDate} + {booking.scheduledDate ?? booking.createdAt}
@@ -129,7 +152,6 @@ export default function BookingDetailPage() { - {/* Granular Status Lifecycle */} @@ -140,7 +162,6 @@ export default function BookingDetailPage() {
- {/* Progress Line */}
- {normalizedStatus !== "CANCELLED" && normalizedStatus !== "COMPLETED" && ( + {normalizedStatus !== "CANCELLED" && normalizedStatus !== "DELIVERED" && (

Est. Waiting

@@ -200,7 +221,6 @@ export default function BookingDetailPage() {
- {/* Route & Core Service Card */} @@ -232,14 +252,13 @@ export default function BookingDetailPage() {
- } label="Service" value="Rail & Forwarding" /> - } label="Return" value="With Return" /> - } label="Customs" value="Enabled" /> + } label="Service" value={booking.serviceType === "RAIL_AND_FORWARDING" ? "Rail & Forwarding" : "Rail Only"} /> + } label="Return" value={booking.equipmentReturn === "WITH_RETURN" ? "With Return" : "Without Return"} /> + } label="Trade" value={booking.tradeDirection === "IMPORT" ? "Import" : "Export"} />
- {/* Mile Services Card */} @@ -252,18 +271,19 @@ export default function BookingDetailPage() {

First Mile

- +

Last Mile

-

Not requested

+

+ {booking.lastMileEnabled && booking.lastMileDeliveryAddress ? booking.lastMileDeliveryAddress : "Not requested"} +

- {/* Cargo Specifications Card */} @@ -273,40 +293,44 @@ export default function BookingDetailPage() {
- } label="Category" value={booking.cargoType} /> - } label="Weight" value={`${booking.weightTons} Tons`} /> - } label="Shipping Line" value="MSC" /> + } label="Freight Type" value={booking.freightType === "BULK" ? "Bulk" : "Break Bulk"} /> + } label="Weight (VGM)" value={`${booking.cargoTotalWeightVgm} Tons`} /> + } label="Currency" value={booking.paymentCurrency} />
- - -
-

Load Details

-
- - - - - - - - - - - - - - - -
DescriptionUnitValue
Main Equipment20FT Container4 Units
-
-
+ {booking.containers && booking.containers.length > 0 && ( + <> + +
+

Load Details

+
+ + + + + + + + + + {booking.containers.map((c, i) => ( + + + + + + ))} + +
TypeQuantityVGM (Tons)
{c.type}{c.qty} Units{c.vgm}t
+
+
+ + )}
- {/* Contract Card */} @@ -315,40 +339,48 @@ export default function BookingDetailPage() { - - + +
- Hazardous: No + Hazardous: {booking.isHazardous ? "Yes" : "No"} - Refrigerated: No + Refrigerated: {booking.isRefrigerated ? "Yes" : "No"}
- {/* Notes Card */} Additional Info -
-

Description

-

"{booking.cargoDescription}"

-
- -
-

Instructions

-
-

- - {booking.specialInstructions} -

-
-
+ {booking.freightSubtype && ( +
+

Cargo Description

+

"{booking.freightSubtype}"

+
+ )} + {booking.financialTerms && ( + <> + +
+

Financial Terms

+
+

+ + {booking.financialTerms} +

+
+
+ + )} + {!booking.freightSubtype && !booking.financialTerms && ( +

No additional information provided.

+ )}
@@ -396,7 +428,7 @@ function InfoItem({ {icon &&
{icon}
}

{label}

-

{value || "—"}

+

{value ?? "—"}

); @@ -405,20 +437,10 @@ function InfoItem({ function StatusBadge({ status }: { status: string }) { const statusColors: Record = { DRAFT: "bg-slate-50 text-slate-700 border-slate-200", - RFQ_SUBMITTED: "bg-amber-50 text-amber-700 border-amber-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", + CONFIRMED: "bg-emerald-50 text-emerald-700 border-emerald-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", - PENDING_CONSOLIDATION: "bg-amber-50 text-amber-700 border-amber-200", - CONSOLIDATED: "bg-indigo-50 text-indigo-700 border-indigo-200", }; return ( diff --git a/apps/edr-freight-web/portal/src/pages/bookings/MyBookings.tsx b/apps/edr-freight-web/portal/src/pages/bookings/MyBookings.tsx index 9d8501df0..960d9c0f2 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/MyBookings.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/MyBookings.tsx @@ -1,5 +1,6 @@ import { useMemo, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; +import { useQuery } from "@tanstack/react-query"; import { ArrowRight, Clock, @@ -9,13 +10,11 @@ import { Package, Plus, Search, - Trash2, Truck, } from "lucide-react"; -import DeleteBookingDialog from "./DeleteBookingDialog"; -import { getMyBookings } from "@/lib/currentCustomer"; -import { deleteBooking, type Booking, type BookingStatus } from "./bookings.mock"; +import { api } from "@/services/api"; +import type { Freight } from "@edr/types"; import { DataTable, DataTableFooter, @@ -32,32 +31,30 @@ import { DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, - DropdownMenuSeparator, } from "@edr/ui-common"; export default function MyBookings() { const navigate = useNavigate(); const { pagination, setPagination } = usePagination({ pageSize: 10 }); const [searchTerm, setSearchTerm] = useState(""); - const [myBookings, setMyBookings] = useState(() => getMyBookings()); - const handleDeleteConfirm = (id: number) => { - deleteBooking(id); - setMyBookings(getMyBookings()); - }; + const { data, isLoading, isError } = useQuery( + api.bookings.list.queryOptions(), + ); + + const bookings = data?.items ?? []; const filteredData = useMemo(() => { - return myBookings.filter((b) => { + return bookings.filter((b) => { const term = searchTerm.toLowerCase(); return ( b.reference.toLowerCase().includes(term) || b.originStation.toLowerCase().includes(term) || b.destinationStation.toLowerCase().includes(term) || - b.cargoDescription.toLowerCase().includes(term) || b.status.toLowerCase().includes(term) ); }); - }, [myBookings, searchTerm]); + }, [bookings, searchTerm]); const total = filteredData.length; 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 activeCount = useMemo(() => { - return myBookings.filter( - (b) => b.status === "Confirmed" || b.status === "In Transit", + return bookings.filter( + (b) => b.status === "CONFIRMED" || b.status === "IN_TRANSIT", ).length; - }, [myBookings]); + }, [bookings]); const pendingCount = useMemo(() => { - return myBookings.filter((b) => b.status === "Pending").length; - }, [myBookings]); + return bookings.filter((b) => b.status === "DRAFT").length; + }, [bookings]); - const columns: ColumnDef[] = [ + const columns: ColumnDef[] = [ { accessorKey: "reference", header: "Reference", @@ -89,7 +86,7 @@ export default function MyBookings() {

{booking.reference}

-

{booking.requestedDate}

+

{booking.scheduledDate ?? booking.createdAt}

); @@ -111,22 +108,24 @@ export default function MyBookings() { header: "Cargo", cell: ({ row }) => { const b = row.original; + const containerCount = b.containers?.reduce((sum, c) => sum + c.qty, 0) ?? 0; + const containerType = b.containers?.[0]?.type ?? null; return (
-

{b.cargoType}

+

{b.freightType === "BULK" ? "Bulk" : "Break Bulk"}

- {b.containerCount > 0 ? `${b.containerCount} × ${b.containerType} · ` : ""}{b.weightTons}t + {containerType && containerCount > 0 ? `${containerCount} × ${containerType} · ` : ""}{b.cargoTotalWeightVgm}t

); }, }, { - accessorKey: "transportMode", + id: "transportMode", header: "Transport", cell: ({ row }) => ( - {row.original.transportMode} + {row.original.serviceType === "RAIL_AND_FORWARDING" ? "Rail & Forwarding" : "Rail"} ), }, @@ -158,19 +157,6 @@ export default function MyBookings() { View - - handleDeleteConfirm(booking.id)} - > - e.preventDefault()} - variant="destructive" - > - - Delete - -
@@ -179,10 +165,11 @@ export default function MyBookings() { }, ]; + const dataTableStatus = isLoading ? "loading" : isError ? "error" : "success"; + return (
- {/* Header Section Card */}

@@ -214,14 +201,13 @@ export default function MyBookings() {

- {/* Stat Cards */}

Total Bookings

- {myBookings.length} + {bookings.length}

@@ -259,7 +245,6 @@ export default function MyBookings() {
- {/* Data Table */}
@@ -276,7 +261,7 @@ export default function MyBookings() { - {total === 0 ? ( + {total === 0 && dataTableStatus === "success" ? (

No bookings found

@@ -288,8 +273,8 @@ export default function MyBookings() { navigate(`/bookings/${(row as Booking).id}`)} + status={dataTableStatus} + onRowClick={(row) => navigate(`/bookings/${(row as Freight.IBooking).id}`)} pagination={{ pageIndex: pagination.pageIndex, pageSize: pagination.pageSize, @@ -311,20 +296,20 @@ export default function MyBookings() { ); } -function StatusBadge({ status }: { status: BookingStatus }) { - const styles: Record = { - Pending: "bg-amber-100 text-amber-700", - Confirmed: "bg-sky-100 text-sky-700", - "In Transit": "bg-indigo-100 text-indigo-700", - Delivered: "bg-emerald-100 text-emerald-700", - Cancelled: "bg-red-100 text-red-700", +function StatusBadge({ status }: { status: string }) { + const styles: Record = { + DRAFT: "bg-amber-100 text-amber-700", + CONFIRMED: "bg-sky-100 text-sky-700", + IN_TRANSIT: "bg-indigo-100 text-indigo-700", + DELIVERED: "bg-emerald-100 text-emerald-700", + CANCELLED: "bg-red-100 text-red-700", }; return ( - {status} + {status.replace(/_/g, ' ')} ); } diff --git a/apps/edr-freight-web/portal/src/services/bookings.service.ts b/apps/edr-freight-web/portal/src/services/bookings.service.ts index dd4c9f0dd..6ac93a742 100644 --- a/apps/edr-freight-web/portal/src/services/bookings.service.ts +++ b/apps/edr-freight-web/portal/src/services/bookings.service.ts @@ -6,22 +6,22 @@ export type CreateBookingPayload = Freight.CreateBookingDto; export const bookingsService = { list: async (): Promise> => { - const { data } = await client.get("/bookings"); + const { data } = await client.get("/api/bookings"); return data.data; }, get: async (id: string): Promise => { - const { data } = await client.get(`/bookings/${id}`); + const { data } = await client.get(`/api/bookings/${id}`); return data.data; }, create: async (payload: CreateBookingPayload): Promise => { const { data } = await client.post("/api/bookings", payload); - return data.data; + return data.data.booking; }, getReferenceData: async (): Promise => { const { data } = await client.get("/api/bookings/reference-data"); return data.data; }, remove: async (id: string): Promise => { - await client.delete(`/bookings/${id}`); + await client.delete(`/api/bookings/${id}`); }, };