diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx index 6cb111bb7..b6b17e71f 100644 --- a/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx @@ -1,5 +1,7 @@ import { useMemo, useState } from "react"; -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; +import { format } from "date-fns"; +import { useQuery } from "@tanstack/react-query"; import { ArrowRight, Building2, @@ -7,6 +9,7 @@ import { Clock, DollarSign, Eye, + LoaderCircle, Mail, MapPin, Package, @@ -20,14 +23,12 @@ import { import { getCurrentCustomer, - getMyBookings, getMyInvoices, getMyShipments, } from "@/lib/currentCustomer"; import { formatCurrency } from "@/pages/billing/invoices.mock"; import type { ShipmentStatus } from "@/pages/tracking/shipments.mock"; import type { InvoiceStatus } from "@/pages/billing/invoices.mock"; -import type { BookingStatus } from "@/pages/bookings/bookings.mock"; import { Button, Card, @@ -36,16 +37,36 @@ import { CardTitle, CardDescription, } from "@edr/ui-common"; +import { api } from "@/services/api"; + +const ACTIVE_STATUSES = [ + "DRAFT", + "SUBMITTED", + "PENDING_APPROVAL", + "IN_TRANSIT", +]; export default function MyPortalPage() { const me = useMemo(() => getCurrentCustomer(), []); - const myBookings = useMemo(() => getMyBookings(), []); const myShipments = useMemo(() => getMyShipments(), []); const myInvoices = useMemo(() => getMyInvoices(), []); - const activeBookings = myBookings.filter( - (b) => b.status === "Confirmed" || b.status === "In Transit", + const navigate = useNavigate(); + + const bookingsQuery = useQuery( + api.bookings.list.queryOptions({ + input: { sortBy: "createdAt", sortOrder: "DESC" }, + }), ); + + const myBookings = useMemo( + () => + (bookingsQuery.data?.items ?? []).filter((b) => + ACTIVE_STATUSES.includes(b.status), + ), + [bookingsQuery.data], + ); + const activeShipments = myShipments.filter((s) => s.status === "In Transit"); const outstandingInvoices = myInvoices.filter( (inv) => inv.status === "Sent" || inv.status === "Overdue", @@ -58,7 +79,7 @@ export default function MyPortalPage() { .filter((inv) => inv.status === "Paid" && inv.currency === "USD") .reduce((sum, inv) => sum + inv.amount, 0); - const recentBookings = [...myBookings].slice(0, 5); + const recentBookings = myBookings.slice(0, 5); const recentInvoices = [...myInvoices].slice(0, 4); return ( @@ -117,7 +138,7 @@ export default function MyPortalPage() { New Booking @@ -156,7 +177,7 @@ export default function MyPortalPage() { {activeShipments.length === 0 ? ( - + No shipments currently in transit. ) : ( @@ -164,27 +185,27 @@ export default function MyPortalPage() { {activeShipments.slice(0, 4).map((shipment) => ( - + {shipment.reference} - + {shipment.originStation} {shipment.destinationStation} - + {shipment.currentLocation} ETA {shipment.eta} - + {recentBookings.length === 0 ? ( - + You haven't booked any freight yet. ) : ( - + Reference Route Cargo + Date Status - - Action - {recentBookings.map((booking) => ( navigate(`/bookings/${booking.id}`)} > - + {booking.reference} - - {booking.originStation} → {booking.destinationStation} + + {booking.originYard?.label ?? booking.originYard?.code ?? "—"} → {booking.destinationYard?.label ?? booking.destinationYard?.code ?? "—"} - - {booking.cargoType} + + {booking.freightType === "CONTAINER" ? "Container" : booking.freightType} + + + {format(new Date(booking.createdAt), "MMM d, yyyy HH:mm")} - - - - - ))} @@ -289,7 +303,7 @@ export default function MyPortalPage() { {recentInvoices.length === 0 ? ( - + No invoices yet. ) : ( @@ -297,16 +311,16 @@ export default function MyPortalPage() { {recentInvoices.map((invoice) => ( - + {formatCurrency(invoice.amount, invoice.currency)} - + Due {invoice.dueDate} @@ -334,8 +348,8 @@ function ProfileRow({ {icon} - {label} - {value} + {label} + {value} ); @@ -343,9 +357,9 @@ function ProfileRow({ function ShipmentBadge({ status }: { status: ShipmentStatus }) { const styles: Record = { - "In Transit": "bg-indigo-100 text-indigo-700", - Delivered: "bg-emerald-100 text-emerald-700", - Delayed: "bg-red-100 text-red-700", + "In Transit": "bg-muted text-foreground", + Delivered: "bg-primary/10 text-primary", + Delayed: "bg-destructive/10 text-destructive", }; return ( = { - 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 BookingBadge({ status }: { status: string }) { + const styles: Record = { + DRAFT: "bg-amber-100 text-amber-700", + SUBMITTED: "bg-primary/10 text-primary", + PENDING_APPROVAL: "bg-muted text-foreground", + IN_TRANSIT: "bg-muted text-foreground", + COMPLETED: "bg-primary/10 text-primary", + CANCELLED: "bg-destructive/10 text-destructive", + REJECTED: "bg-destructive/10 text-destructive", }; return ( - {status} + {status.replace(/_/g, " ")} ); } function InvoiceBadge({ status }: { status: InvoiceStatus }) { const styles: Record = { - Draft: "bg-slate-100 text-slate-600", - Sent: "bg-sky-100 text-sky-700", - Paid: "bg-emerald-100 text-emerald-700", - Overdue: "bg-red-100 text-red-700", + Draft: "bg-muted text-muted-foreground", + Sent: "bg-primary/10 text-primary", + Paid: "bg-primary/10 text-primary", + Overdue: "bg-destructive/10 text-destructive", Cancelled: "bg-amber-100 text-amber-700", }; return ( 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 07243cdec..f7364af47 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,4 @@ -import { useRef, useState } from "react"; +import { useMemo, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { @@ -32,6 +32,8 @@ import { FileUp, } from "lucide-react"; +import { format } from "date-fns"; + import Breadcrumbs from "@/components/Breadcrumbs"; import { api } from "@/services/api"; import type { Freight } from "@edr/types"; @@ -70,31 +72,31 @@ const STATUS_MAP: Record< DRAFT: { title: "Drafting Request", description: "Booking is being prepared and has not been submitted.", - color: "text-slate-500", + color: "text-muted-foreground", stage: 0, }, CONFIRMED: { title: "Booking Confirmed", description: "Booking has been confirmed and approved.", - color: "text-emerald-600", + color: "text-primary", stage: 1, }, IN_TRANSIT: { title: "Cargo Moving", description: "Shipment is currently moving through the rail network.", - color: "text-sky-600", + color: "text-primary", stage: 2, }, DELIVERED: { title: "Service Complete", description: "Cargo delivered and service successfully terminated.", - color: "text-emerald-600", + color: "text-primary", stage: 3, }, CANCELLED: { title: "Cancelled", description: "This booking process has been terminated.", - color: "text-red-600", + color: "text-destructive", stage: -1, }, }; @@ -145,10 +147,10 @@ export default function BookingDetailPage() { return ( - + - + Failed to load booking @@ -165,10 +167,10 @@ export default function BookingDetailPage() { return ( - + - + Booking not found @@ -196,15 +198,22 @@ function DraftBookingView({ const queryClient = useQueryClient(); const { customer } = useAuth(); const fileInputRefs = useRef>({}); + const documentsRef = useRef(null); const [selectedFiles, setSelectedFiles] = useState< Record >({}); const [cancelDialogOpen, setCancelDialogOpen] = useState(false); const [cancelReason, setCancelReason] = useState(""); + const [docError, setDocError] = useState(""); const anyFileSelected = Object.values(selectedFiles).some(Boolean); + const uploadedCodes = useMemo( + () => new Set(booking.files?.map((f) => f.code) ?? []), + [booking.files], + ); + const pricingQuery = useQuery( api.bookings.generatePrice.queryOptions({ input: { id: booking.id }, @@ -217,6 +226,7 @@ function DraftBookingView({ api.bookings.uploadDocuments.call({ id: booking.id, files }), onSuccess: () => { setSelectedFiles({}); + setDocError(""); onBookingUpdated(); }, }); @@ -258,8 +268,17 @@ function DraftBookingView({ cancelMutation.mutate(reason); } - const canConfirm = - pricingQuery.isSuccess && !uploadMutation.isPending && !submitMutation.isPending; + function handleSubmitRequest() { + const missing = REQUIRED_DOC_FIELDS.filter( + (doc) => !uploadedCodes.has(doc.key), + ); + if (missing.length > 0) { + setDocError("Please upload all required documents before submitting."); + documentsRef.current?.scrollIntoView({ behavior: "smooth" }); + return; + } + submitMutation.mutate(); + } const companyName = (customer as any)?.company?.name ?? "—"; const companyTin = (customer as any)?.company?.tin ?? "—"; @@ -282,7 +301,7 @@ function DraftBookingView({ - + @@ -296,16 +315,31 @@ function DraftBookingView({ Complete the steps below to submit your booking request. + + + {submitMutation.isPending ? ( + + ) : ( + + )} + {submitMutation.isPending + ? "Submitting..." + : "Confirm Booking Request"} + {pricingQuery.isError && ( - - + + Pricing failed - + {pricingQuery.error instanceof Error ? pricingQuery.error.message : "An unexpected error occurred."} @@ -315,11 +349,11 @@ function DraftBookingView({ )} {uploadMutation.isError && ( - - + + Document upload failed - + {uploadMutation.error instanceof Error ? uploadMutation.error.message : "An unexpected error occurred."} @@ -329,11 +363,11 @@ function DraftBookingView({ )} {submitMutation.isError && ( - - + + Submission failed - + {submitMutation.error instanceof Error ? submitMutation.error.message : "An unexpected error occurred."} @@ -343,11 +377,11 @@ function DraftBookingView({ )} {cancelMutation.isError && ( - - + + Cancel failed - + {cancelMutation.error instanceof Error ? cancelMutation.error.message : "An unexpected error occurred."} @@ -358,7 +392,7 @@ function DraftBookingView({ @@ -441,22 +475,12 @@ function DraftBookingView({ ))} )} - - - pricingQuery.refetch()} - > - Re-calculate pricing - - ) : null} - + @@ -468,6 +492,12 @@ function DraftBookingView({ + {docError && ( + + + {docError} + + )} @@ -479,7 +509,7 @@ function DraftBookingView({ - + To update your company information, go to{" "} - {REQUIRED_DOC_FIELDS.map((doc) => ( - - - {doc.label} - - - { - fileInputRefs.current[doc.key] = el; - }} - type="file" - accept=".pdf,.jpg,.jpeg,.png" - className="hidden" - onChange={(e) => { - handleFileSelect( - doc.key, - e.target.files?.[0] ?? null, - ); - }} - /> - { + const isUploaded = uploadedCodes.has(doc.key); + return ( + + + {isUploaded && ( + )} - onClick={() => fileInputRefs.current[doc.key]?.click()} - > - - {selectedFiles[doc.key] - ? selectedFiles[doc.key]!.name - : "Choose file"} - - {selectedFiles[doc.key] && ( - handleFileSelect(doc.key, null)} - > - - - )} + {doc.label} + + + {isUploaded ? ( + + + Uploaded + + ) : ( + <> + { + fileInputRefs.current[doc.key] = el; + }} + type="file" + accept=".pdf,.jpg,.jpeg,.png" + className="hidden" + onChange={(e) => { + handleFileSelect( + doc.key, + e.target.files?.[0] ?? null, + ); + }} + /> + + fileInputRefs.current[doc.key]?.click() + } + > + + {selectedFiles[doc.key] + ? selectedFiles[doc.key]!.name + : "Choose file"} + + {selectedFiles[doc.key] && ( + handleFileSelect(doc.key, null)} + > + + + )} + > + )} + - - ))} + ); + })} @@ -570,7 +617,7 @@ function DraftBookingView({ : "Select files to upload"} {uploadMutation.isSuccess && ( - + Documents uploaded successfully @@ -597,7 +644,11 @@ function DraftBookingView({ - + Cancel Booking @@ -647,41 +698,6 @@ function DraftBookingView({ - - - - {!canConfirm && ( - - {pricingQuery.isLoading - ? "Calculating price…" - : pricingQuery.isError - ? "Price calculation failed." - : "Upload documents before confirming."} - - )} - navigate("/bookings")} - > - Back to Bookings - - submitMutation.mutate()} - disabled={!canConfirm} - > - {submitMutation.isPending ? ( - - ) : ( - - )} - {submitMutation.isPending - ? "Submitting..." - : "Confirm Booking Request"} - - - ); @@ -720,7 +736,10 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) { - {booking.scheduledDate ?? booking.createdAt} + {format( + new Date(booking.scheduledDate ?? booking.createdAt), + "MMM d, yyyy HH:mm", + )} @@ -801,7 +820,7 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) { @@ -815,7 +834,7 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) { {normalizedStatus === "CANCELLED" ? ( - + ) : ( )} @@ -837,7 +856,7 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) { normalizedStatus !== "DELIVERED" && ( - + Est. Waiting @@ -864,7 +883,11 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) { } /> @@ -874,14 +897,18 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) { Rail } /> @@ -1038,10 +1065,10 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) { - + Hazardous: {booking.isHazardous ? "Yes" : "No"} - + Refrigerated: {booking.isRefrigerated ? "Yes" : "No"} @@ -1055,7 +1082,7 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) { {booking.freightSubtype && ( - + Cargo Description @@ -1067,7 +1094,7 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) { <> - + Financial Terms @@ -1108,7 +1135,7 @@ function RouteEndpoint({ {icon && {icon}} - + {label} {station} @@ -1134,7 +1161,7 @@ function InfoItem({ )} - + {label} {value ?? "—"} @@ -1145,18 +1172,18 @@ function InfoItem({ function StatusBadge({ status }: { status: string }) { const statusColors: Record = { - DRAFT: "bg-slate-50 text-slate-700 border-slate-200", - CONFIRMED: "bg-emerald-50 text-emerald-700 border-emerald-200", - IN_TRANSIT: "bg-sky-50 text-sky-700 border-sky-200", - DELIVERED: "bg-indigo-50 text-indigo-700 border-indigo-200", - CANCELLED: "bg-red-50 text-red-700 border-red-200", + DRAFT: "bg-muted text-muted-foreground border-border", + CONFIRMED: "bg-primary/10 text-primary border-primary/20", + IN_TRANSIT: "bg-primary/10 text-primary border-primary/20", + DELIVERED: "bg-muted text-foreground border-border", + CANCELLED: "bg-destructive/10 text-destructive border-destructive/20", }; 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 960d9c0f2..ac0b3661f 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/MyBookings.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/MyBookings.tsx @@ -49,8 +49,8 @@ export default function MyBookings() { const term = searchTerm.toLowerCase(); return ( b.reference.toLowerCase().includes(term) || - b.originStation.toLowerCase().includes(term) || - b.destinationStation.toLowerCase().includes(term) || + (b.originYard?.label ?? b.originYard?.code ?? "").toLowerCase().includes(term) || + (b.destinationYard?.label ?? b.destinationYard?.code ?? "").toLowerCase().includes(term) || b.status.toLowerCase().includes(term) ); }); @@ -85,8 +85,8 @@ export default function MyBookings() { - {booking.reference} - {booking.scheduledDate ?? booking.createdAt} + {booking.reference} + {booking.scheduledDate ?? booking.createdAt} ); @@ -96,10 +96,10 @@ export default function MyBookings() { id: "route", header: "Route", cell: ({ row }) => ( - - {row.original.originStation} - - {row.original.destinationStation} + + {row.original.originYard?.label ?? row.original.originYard?.code ?? "—"} + + {row.original.destinationYard?.label ?? row.original.destinationYard?.code ?? "—"} ), }, @@ -111,9 +111,9 @@ export default function MyBookings() { const containerCount = b.containers?.reduce((sum, c) => sum + c.qty, 0) ?? 0; const containerType = b.containers?.[0]?.type ?? null; return ( - + {b.freightType === "BULK" ? "Bulk" : "Break Bulk"} - + {containerType && containerCount > 0 ? `${containerCount} × ${containerType} · ` : ""}{b.cargoTotalWeightVgm}t @@ -124,7 +124,7 @@ export default function MyBookings() { id: "transportMode", header: "Transport", cell: ({ row }) => ( - + {row.original.serviceType === "RAIL_AND_FORWARDING" ? "Rail & Forwarding" : "Rail"} ), @@ -172,10 +172,10 @@ export default function MyBookings() { - + My Bookings - + View and manage your freight booking requests. @@ -205,8 +205,8 @@ export default function MyBookings() { - Total Bookings - + Total Bookings + {bookings.length} @@ -219,8 +219,8 @@ export default function MyBookings() { - Active Bookings - + Active Bookings + {activeCount} @@ -233,8 +233,8 @@ export default function MyBookings() { - Pending Approval - + Pending Approval + {pendingCount} @@ -263,9 +263,9 @@ export default function MyBookings() { {total === 0 && dataTableStatus === "success" ? ( - - No bookings found - + + No bookings found + {searchTerm ? "No bookings match your current search filter." : "You haven't requested any bookings yet."} @@ -299,15 +299,15 @@ export default function MyBookings() { 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", + CONFIRMED: "bg-primary/10 text-primary", + IN_TRANSIT: "bg-muted text-foreground", + DELIVERED: "bg-primary/10 text-primary", + CANCELLED: "bg-destructive/10 text-destructive", }; return ( {status.replace(/_/g, ' ')} diff --git a/apps/edr-freight-web/portal/src/services/api.ts b/apps/edr-freight-web/portal/src/services/api.ts index 847fac2bd..9b306b8b1 100644 --- a/apps/edr-freight-web/portal/src/services/api.ts +++ b/apps/edr-freight-web/portal/src/services/api.ts @@ -10,6 +10,7 @@ import type { } from "@/types/fileUploadSettings"; import { bookingsService, + BookingListFilter, CreateBookingPayload, GeneratePriceResponse, } from "./bookings.service"; @@ -115,7 +116,7 @@ export const api = { }, bookings: { - list: endpoint>( + list: endpoint>( "bookings", "list", bookingsService.list, 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 c3ea58ad9..95a3e5740 100644 --- a/apps/edr-freight-web/portal/src/services/bookings.service.ts +++ b/apps/edr-freight-web/portal/src/services/bookings.service.ts @@ -47,9 +47,19 @@ export interface SignContractPayload { consentText?: string; } +export interface BookingListFilter { + status?: string; + page?: number; + pageSize?: number; + sortBy?: string; + sortOrder?: "ASC" | "DESC"; +} + export const bookingsService = { - list: async (): Promise> => { - const { data } = await client.get("/api/bookings"); + list: async ( + filter: BookingListFilter | void = {}, + ): Promise> => { + const { data } = await client.get("/api/bookings", { params: filter }); return data.data; }, get: async (id: string): Promise => { diff --git a/packages/types/src/freight/index.ts b/packages/types/src/freight/index.ts index c6a627de2..ee46f4d76 100644 --- a/packages/types/src/freight/index.ts +++ b/packages/types/src/freight/index.ts @@ -158,6 +158,14 @@ export interface IConsignment extends BaseEntity { destinationStation: string; } +export interface IYard extends BaseEntity { + code: string; + label: string; + country: string; + isActive: boolean; + displayOrder: number; +} + export interface IBooking extends BaseEntity { reference: string; customerId: string; @@ -177,8 +185,8 @@ export interface IBooking extends BaseEntity { lastMileDeliveryAddress?: string | null; equipmentReturn: "WITH_RETURN" | "WITHOUT_RETURN"; - originStation: string; - destinationStation: string; + originYard?: IYard | null; + destinationYard?: IYard | null; cargoTotalWeightVgm: number; freightType: FreightType; @@ -208,7 +216,17 @@ export interface IBooking extends BaseEntity { signedByCeoId?: string | null; signedByCeoAt?: string | null; - files?: Array<{ id: string; name: string; url: string; mimeType: string }>; + files?: Array<{ + id: string; + code: string; + name: string; + url: string; + mimeType: string; + size: number; + resourceId: string; + resource: string; + signedUrl?: string | null; + }>; } export interface IInvoice extends BaseEntity {
+
No shipments currently in transit.
{shipment.originStation} {shipment.destinationStation}
You haven't booked any freight yet.
No invoices yet.
{formatCurrency(invoice.amount, invoice.currency)}
Due {invoice.dueDate}
{label}
{value}
@@ -165,10 +167,10 @@ export default function BookingDetailPage() { return (
Pricing failed
{pricingQuery.error instanceof Error ? pricingQuery.error.message : "An unexpected error occurred."} @@ -315,11 +349,11 @@ function DraftBookingView({ )} {uploadMutation.isError && ( -
Document upload failed
{uploadMutation.error instanceof Error ? uploadMutation.error.message : "An unexpected error occurred."} @@ -329,11 +363,11 @@ function DraftBookingView({ )} {submitMutation.isError && ( -
Submission failed
{submitMutation.error instanceof Error ? submitMutation.error.message : "An unexpected error occurred."} @@ -343,11 +377,11 @@ function DraftBookingView({ )} {cancelMutation.isError && ( -
Cancel failed
{cancelMutation.error instanceof Error ? cancelMutation.error.message : "An unexpected error occurred."} @@ -358,7 +392,7 @@ function DraftBookingView({ @@ -441,22 +475,12 @@ function DraftBookingView({ ))}
{docError}
To update your company information, go to{" "} - {REQUIRED_DOC_FIELDS.map((doc) => ( - - - {doc.label} - - - { - fileInputRefs.current[doc.key] = el; - }} - type="file" - accept=".pdf,.jpg,.jpeg,.png" - className="hidden" - onChange={(e) => { - handleFileSelect( - doc.key, - e.target.files?.[0] ?? null, - ); - }} - /> - { + const isUploaded = uploadedCodes.has(doc.key); + return ( + + + {isUploaded && ( + )} - onClick={() => fileInputRefs.current[doc.key]?.click()} - > - - {selectedFiles[doc.key] - ? selectedFiles[doc.key]!.name - : "Choose file"} - - {selectedFiles[doc.key] && ( - handleFileSelect(doc.key, null)} - > - - - )} + {doc.label} + + + {isUploaded ? ( + + + Uploaded + + ) : ( + <> + { + fileInputRefs.current[doc.key] = el; + }} + type="file" + accept=".pdf,.jpg,.jpeg,.png" + className="hidden" + onChange={(e) => { + handleFileSelect( + doc.key, + e.target.files?.[0] ?? null, + ); + }} + /> + + fileInputRefs.current[doc.key]?.click() + } + > + + {selectedFiles[doc.key] + ? selectedFiles[doc.key]!.name + : "Choose file"} + + {selectedFiles[doc.key] && ( + handleFileSelect(doc.key, null)} + > + + + )} + > + )} + - - ))} + ); + })} @@ -570,7 +617,7 @@ function DraftBookingView({ : "Select files to upload"} {uploadMutation.isSuccess && ( - + Documents uploaded successfully @@ -597,7 +644,11 @@ function DraftBookingView({ - + Cancel Booking @@ -647,41 +698,6 @@ function DraftBookingView({ - - - - {!canConfirm && ( - - {pricingQuery.isLoading - ? "Calculating price…" - : pricingQuery.isError - ? "Price calculation failed." - : "Upload documents before confirming."} - - )} - navigate("/bookings")} - > - Back to Bookings - - submitMutation.mutate()} - disabled={!canConfirm} - > - {submitMutation.isPending ? ( - - ) : ( - - )} - {submitMutation.isPending - ? "Submitting..." - : "Confirm Booking Request"} - - - ); @@ -720,7 +736,10 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) { - {booking.scheduledDate ?? booking.createdAt} + {format( + new Date(booking.scheduledDate ?? booking.createdAt), + "MMM d, yyyy HH:mm", + )}
Documents uploaded successfully
- {pricingQuery.isLoading - ? "Calculating price…" - : pricingQuery.isError - ? "Price calculation failed." - : "Upload documents before confirming."} -
Est. Waiting
@@ -864,7 +883,11 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
Cargo Description
@@ -1067,7 +1094,7 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) { <>
Financial Terms
{station}
{value ?? "—"}
{booking.reference}
{booking.scheduledDate ?? booking.createdAt}
{b.freightType === "BULK" ? "Bulk" : "Break Bulk"}
{containerType && containerCount > 0 ? `${containerCount} × ${containerType} · ` : ""}{b.cargoTotalWeightVgm}t
View and manage your freight booking requests.
Total Bookings
Active Bookings
Pending Approval
+ +
{searchTerm ? "No bookings match your current search filter." : "You haven't requested any bookings yet."}