diff --git a/apps/edr-freight-web/portal/package.json b/apps/edr-freight-web/portal/package.json index 3cd617e0a..b9b33d35d 100644 --- a/apps/edr-freight-web/portal/package.json +++ b/apps/edr-freight-web/portal/package.json @@ -20,6 +20,7 @@ "axios": "^1.7.7", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^3.6.0", "lucide-react": "^1.14.0", "radix-ui": "^1.4.3", "react": "19.2.6", 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..fc725c368 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"; @@ -57,10 +59,11 @@ import { cn } from "@/lib/utils"; import useAuth from "@/hooks/useAuth"; const PROGRESS_STAGES = [ - { 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"] }, + { label: "Request", icon: FileText, statuses: ["DRAFT", "CHANGES_REQUESTED"] }, + { label: "Submitted", icon: ClipboardCheck, statuses: ["SUBMITTED", "PENDING_APPROVAL"] }, + { label: "Approved", icon: ShieldCheck, statuses: ["APPROVED_PENDING_SIGNATURE", "APPROVED", "CONTRACT_READY", "SIGNED_CUSTOMER", "FULLY_EXECUTED"] }, + { label: "In Transit", icon: Train, statuses: ["PNR_GENERATED", "PAYMENT_VERIFICATION_IN_PROGRESS", "PAID", "IN_TRANSIT", "PENDING_CONSOLIDATION", "CONSOLIDATED"] }, + { label: "Complete", icon: PackageCheck, statuses: ["COMPLETED", "DELIVERED"] }, ]; const STATUS_MAP: Record< @@ -70,31 +73,115 @@ 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", + CHANGES_REQUESTED: { + title: "Changes Requested", + description: "Staff has requested changes. Please review and resubmit.", + color: "text-amber-600", + stage: 0, + }, + SUBMITTED: { + title: "Submitted", + description: "Your booking has been submitted for review.", + color: "text-primary", stage: 1, }, + PENDING_APPROVAL: { + title: "Pending Approval", + description: "Booking is in the approval process.", + color: "text-primary", + stage: 1, + }, + APPROVED_PENDING_SIGNATURE: { + title: "Awaiting Signature", + description: "Approved — pending contract signature.", + color: "text-primary", + stage: 2, + }, + APPROVED: { + title: "Approved", + description: "Booking has been fully approved.", + color: "text-primary", + stage: 2, + }, + CONTRACT_READY: { + title: "Contract Ready", + description: "Contract is available for review and signature.", + color: "text-primary", + stage: 2, + }, + SIGNED_CUSTOMER: { + title: "Customer Signed", + description: "Customer has signed. Awaiting staff signature.", + color: "text-primary", + stage: 2, + }, + FULLY_EXECUTED: { + title: "Fully Executed", + description: "Contract has been fully signed and executed.", + color: "text-primary", + stage: 2, + }, + PNR_GENERATED: { + title: "PNR Generated", + description: "Payment reference number generated.", + color: "text-primary", + stage: 3, + }, + PAYMENT_VERIFICATION_IN_PROGRESS: { + title: "Payment Verification", + description: "Payment is being verified.", + color: "text-primary", + stage: 3, + }, + PAID: { + title: "Paid", + description: "Payment has been confirmed.", + color: "text-primary", + stage: 3, + }, IN_TRANSIT: { title: "Cargo Moving", description: "Shipment is currently moving through the rail network.", - color: "text-sky-600", - stage: 2, + color: "text-primary", + stage: 3, + }, + PENDING_CONSOLIDATION: { + title: "Pending Consolidation", + description: "Awaiting consolidation partner.", + color: "text-primary", + stage: 3, + }, + CONSOLIDATED: { + title: "Consolidated", + description: "Cargo has been consolidated with partner shipment.", + color: "text-primary", + stage: 3, + }, + COMPLETED: { + title: "Service Complete", + description: "Cargo delivered and service successfully terminated.", + color: "text-primary", + stage: 4, }, DELIVERED: { title: "Service Complete", description: "Cargo delivered and service successfully terminated.", - color: "text-emerald-600", - stage: 3, + color: "text-primary", + stage: 4, + }, + REJECTED: { + title: "Rejected", + description: "This booking request has been rejected.", + color: "text-destructive", + stage: -1, }, CANCELLED: { title: "Cancelled", description: "This booking process has been terminated.", - color: "text-red-600", + color: "text-destructive", stage: -1, }, }; @@ -145,10 +232,10 @@ export default function BookingDetailPage() { return ( - + - + Failed to load booking @@ -165,10 +252,10 @@ export default function BookingDetailPage() { return ( - + - + Booking not found @@ -176,7 +263,7 @@ export default function BookingDetailPage() { ); } - if (booking.status === "DRAFT") { + if (booking.status === "DRAFT" || booking.status === "CHANGES_REQUESTED") { return ( ); @@ -196,15 +283,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 +311,7 @@ function DraftBookingView({ api.bookings.uploadDocuments.call({ id: booking.id, files }), onSuccess: () => { setSelectedFiles({}); + setDocError(""); onBookingUpdated(); }, }); @@ -258,8 +353,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 +386,7 @@ function DraftBookingView({ - + @@ -290,22 +394,49 @@ function DraftBookingView({ {booking.reference} - + - Complete the steps below to submit your booking request. + {booking.status === "CHANGES_REQUESTED" + ? "Staff has requested changes. Please review, update, and resubmit." + : "Complete the steps below to submit your booking request."} + + + {submitMutation.isPending ? ( + + ) : ( + + )} + {submitMutation.isPending + ? "Submitting..." + : "Confirm Booking Request"} + + {booking.status === "CHANGES_REQUESTED" && booking.latestChangeRequestNote && ( + + + + Changes Requested by Staff + {booking.latestChangeRequestNote} + + + )} + {pricingQuery.isError && ( - - + + Pricing failed - + {pricingQuery.error instanceof Error ? pricingQuery.error.message : "An unexpected error occurred."} @@ -315,11 +446,11 @@ function DraftBookingView({ )} {uploadMutation.isError && ( - - + + Document upload failed - + {uploadMutation.error instanceof Error ? uploadMutation.error.message : "An unexpected error occurred."} @@ -329,11 +460,11 @@ function DraftBookingView({ )} {submitMutation.isError && ( - - + + Submission failed - + {submitMutation.error instanceof Error ? submitMutation.error.message : "An unexpected error occurred."} @@ -343,11 +474,11 @@ function DraftBookingView({ )} {cancelMutation.isError && ( - - + + Cancel failed - + {cancelMutation.error instanceof Error ? cancelMutation.error.message : "An unexpected error occurred."} @@ -358,7 +489,7 @@ function DraftBookingView({ @@ -441,22 +572,12 @@ function DraftBookingView({ ))} )} - - - pricingQuery.refetch()} - > - Re-calculate pricing - - ) : null} - + @@ -468,6 +589,12 @@ function DraftBookingView({ + {docError && ( + + + {docError} + + )} @@ -479,7 +606,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 +714,7 @@ function DraftBookingView({ : "Select files to upload"} {uploadMutation.isSuccess && ( - + Documents uploaded successfully @@ -597,7 +741,11 @@ function DraftBookingView({ - + Cancel Booking @@ -647,41 +795,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 +833,10 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) { - {booking.scheduledDate ?? booking.createdAt} + {format( + new Date(booking.scheduledDate ?? booking.createdAt), + "MMM d, yyyy HH:mm", + )} @@ -728,8 +844,9 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) { - {(booking.status === "CONFIRMED" || - booking.status === "IN_TRANSIT") && ( + {(booking.status === "CONTRACT_READY" || + booking.status === "SIGNED_CUSTOMER" || + booking.status === "FULLY_EXECUTED") && ( @@ -801,7 +918,7 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) { @@ -815,7 +932,7 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) { {normalizedStatus === "CANCELLED" ? ( - + ) : ( )} @@ -837,7 +954,7 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) { normalizedStatus !== "DELIVERED" && ( - + Est. Waiting @@ -864,7 +981,11 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) { } /> @@ -874,14 +995,18 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) { Rail } /> @@ -1038,10 +1163,10 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) { - + Hazardous: {booking.isHazardous ? "Yes" : "No"} - + Refrigerated: {booking.isRefrigerated ? "Yes" : "No"} @@ -1055,7 +1180,7 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) { {booking.freightSubtype && ( - + Cargo Description @@ -1067,7 +1192,7 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) { <> - + Financial Terms @@ -1108,7 +1233,7 @@ function RouteEndpoint({ {icon && {icon}} - + {label} {station} @@ -1134,7 +1259,7 @@ function InfoItem({ )} - + {label} {value ?? "—"} @@ -1145,18 +1270,33 @@ 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", + CHANGES_REQUESTED: "bg-amber-50 text-amber-700 border-amber-200", + SUBMITTED: "bg-primary/10 text-primary border-primary/20", + PENDING_APPROVAL: "bg-primary/10 text-primary border-primary/20", + APPROVED_PENDING_SIGNATURE: "bg-primary/10 text-primary border-primary/20", + APPROVED: "bg-primary/10 text-primary border-primary/20", + CONTRACT_READY: "bg-primary/10 text-primary border-primary/20", + SIGNED_CUSTOMER: "bg-primary/10 text-primary border-primary/20", + FULLY_EXECUTED: "bg-primary/10 text-primary border-primary/20", + PNR_GENERATED: "bg-primary/10 text-primary border-primary/20", + PAYMENT_VERIFICATION_IN_PROGRESS: "bg-primary/10 text-primary border-primary/20", + PAID: "bg-primary/10 text-primary border-primary/20", + CONFIRMED: "bg-primary/10 text-primary border-primary/20", + IN_TRANSIT: "bg-primary/10 text-primary border-primary/20", + PENDING_CONSOLIDATION: "bg-primary/10 text-primary border-primary/20", + CONSOLIDATED: "bg-primary/10 text-primary border-primary/20", + COMPLETED: "bg-muted text-foreground border-border", + DELIVERED: "bg-muted text-foreground border-border", + REJECTED: "bg-destructive/10 text-destructive border-destructive/20", + 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..7e20712d3 100644 --- a/packages/types/src/freight/index.ts +++ b/packages/types/src/freight/index.ts @@ -34,10 +34,24 @@ export enum FreightType { export enum BookingStatus { Draft = "DRAFT", - Confirmed = "CONFIRMED", + Submitted = "SUBMITTED", + ChangesRequested = "CHANGES_REQUESTED", + PendingApproval = "PENDING_APPROVAL", + ApprovedPendingSignature = "APPROVED_PENDING_SIGNATURE", + Approved = "APPROVED", + ContractReady = "CONTRACT_READY", + SignedCustomer = "SIGNED_CUSTOMER", + FullyExecuted = "FULLY_EXECUTED", + PnrGenerated = "PNR_GENERATED", + PaymentVerificationInProgress = "PAYMENT_VERIFICATION_IN_PROGRESS", + Paid = "PAID", InTransit = "IN_TRANSIT", + Completed = "COMPLETED", Delivered = "DELIVERED", + Rejected = "REJECTED", Cancelled = "CANCELLED", + PendingConsolidation = "PENDING_CONSOLIDATION", + Consolidated = "CONSOLIDATED", } export enum ConsignmentStatus { @@ -158,6 +172,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 +199,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 +230,20 @@ 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; + }>; + + latestChangeRequestNote?: string | null; + contractSummary?: string | null; } export interface IInvoice extends BaseEntity { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dbb202c1e..feb0162dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -299,6 +299,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + date-fns: + specifier: ^3.6.0 + version: 3.6.0 lucide-react: specifier: ^1.14.0 version: 1.16.0(react@19.2.6)
+
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 +252,10 @@ export default function BookingDetailPage() { return (
- Complete the steps below to submit your booking request. + {booking.status === "CHANGES_REQUESTED" + ? "Staff has requested changes. Please review, update, and resubmit." + : "Complete the steps below to submit your booking request."}
Changes Requested by Staff
{booking.latestChangeRequestNote}
Pricing failed
{pricingQuery.error instanceof Error ? pricingQuery.error.message : "An unexpected error occurred."} @@ -315,11 +446,11 @@ function DraftBookingView({ )} {uploadMutation.isError && ( -
Document upload failed
{uploadMutation.error instanceof Error ? uploadMutation.error.message : "An unexpected error occurred."} @@ -329,11 +460,11 @@ function DraftBookingView({ )} {submitMutation.isError && ( -
Submission failed
{submitMutation.error instanceof Error ? submitMutation.error.message : "An unexpected error occurred."} @@ -343,11 +474,11 @@ function DraftBookingView({ )} {cancelMutation.isError && ( -
Cancel failed
{cancelMutation.error instanceof Error ? cancelMutation.error.message : "An unexpected error occurred."} @@ -358,7 +489,7 @@ function DraftBookingView({ @@ -441,22 +572,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 +714,7 @@ function DraftBookingView({ : "Select files to upload"} {uploadMutation.isSuccess && ( - + Documents uploaded successfully @@ -597,7 +741,11 @@ function DraftBookingView({ - + Cancel Booking @@ -647,41 +795,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 +833,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 +981,11 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
Cargo Description
@@ -1067,7 +1192,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."}