diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx index 3bfc004ea..b7a68555e 100644 --- a/apps/edr-freight-web/backoffice/src/App.tsx +++ b/apps/edr-freight-web/backoffice/src/App.tsx @@ -1,7 +1,7 @@ import { Navigate, Outlet, Route, Routes, useLocation, useNavigate } from "react-router-dom"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { DashboardLayout, type SidebarItem } from "@edr/ui-common"; -import { LayoutDashboard, Network, Paperclip, Settings } from "lucide-react"; +import { FileText, LayoutDashboard, Network, Paperclip, Settings } from "lucide-react"; import { useAuth } from "./auth/useAuth"; import LoginPage from "./pages/auth/LoginPage"; import OverviewPage from "./pages/dashboard/OverviewPage"; @@ -16,6 +16,8 @@ import DropdownSettingsPage from "./pages/dropdown_settings/DropdownSettingsPage import DemoUser1Page from "./pages/dashboard/demo/DemoUser1Page"; import DemoUser2Page from "./pages/dashboard/demo/DemoUser2Page"; import { RuleEnginePage } from "./pages/ruleEngine/RuleEngine"; +import BookingRequestsPage from "./pages/bookings/BookingRequestsPage"; +import BookingRequestDetailPage from "./pages/bookings/BookingRequestDetailPage"; const queryClient = new QueryClient({ defaultOptions: { @@ -33,6 +35,11 @@ const baseSidebarItems: SidebarItem[] = [ href: "/dashboard/overview", icon: , }, + { + label: "Booking Requests", + href: "/dashboard/booking-requests", + icon: , + }, { label: "User management", href: "/dashboard/user-management", @@ -159,6 +166,9 @@ const App = () => { }> } /> + } /> + } /> + } /> } /> } /> diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestDetailPage.tsx new file mode 100644 index 000000000..86b4f483e --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestDetailPage.tsx @@ -0,0 +1,734 @@ +import { useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { + AlertCircle, + AlertTriangle, + Anchor, + ArrowLeft, + ArrowRight, + Calendar, + Check, + CheckCircle2, + Clock, + FileSignature, + FileText, + History, + Info, + MapPin, + Package, + ShieldCheck, + Ship, + StickyNote, + Train, + Truck, + Weight, + X, +} from "lucide-react"; + +import Breadcrumbs from "@/components/ui/Breadcrumbs"; +import { cn } from "@/lib/utils"; +import { + getBookingRequestById, + getBookingRequests, + saveBookingRequestsToStorage, + updateBookingRequestStatus, + BOOKING_STATUSES, + type BookingRequest, +} from "./booking-requests.mock"; +import { + Badge, + Button, + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + Separator, +} from "@edr/ui-common"; + +const STATUS_STYLES: Record = { + DRAFT: { + label: "Draft", + color: "bg-slate-100 text-slate-700 border-slate-300", + }, + RFQ_SUBMITTED: { + label: "RFQ Submitted", + color: "bg-amber-50 text-amber-700 border-amber-200", + }, + QUOTATION_SENT: { + label: "Quotation Sent", + color: "bg-sky-50 text-sky-700 border-sky-200", + }, + QUOTATION_APPROVED: { + label: "Quotation Approved", + color: "bg-emerald-50 text-emerald-700 border-emerald-200", + }, + QUOTATION_REJECTED: { + label: "Quotation Rejected", + color: "bg-red-50 text-red-700 border-red-200", + }, + PENDING_APPROVAL: { + label: "Pending Approval", + color: "bg-amber-50 text-amber-700 border-amber-200", + }, + APPROVED: { + label: "Approved", + color: "bg-emerald-50 text-emerald-700 border-emerald-200", + }, + SIGNED_CUSTOMER: { + label: "Customer Signed", + color: "bg-sky-50 text-sky-700 border-sky-200", + }, + FULLY_EXECUTED: { + label: "Fully Executed", + color: "bg-indigo-50 text-indigo-700 border-indigo-200", + }, + PAID: { + label: "Paid", + color: "bg-emerald-50 text-emerald-700 border-emerald-200", + }, + IN_TRANSIT: { + label: "In Transit", + color: "bg-sky-50 text-sky-700 border-sky-200", + }, + COMPLETED: { + label: "Completed", + color: "bg-indigo-50 text-indigo-700 border-indigo-200", + }, + CANCELLED: { + label: "Cancelled", + color: "bg-red-50 text-red-700 border-red-200", + }, + PENDING_CONSOLIDATION: { + label: "Pending Consolidation", + color: "bg-amber-50 text-amber-700 border-amber-200", + }, + CONSOLIDATED: { + label: "Consolidated", + color: "bg-indigo-50 text-indigo-700 border-indigo-200", + }, +}; + +const PROGRESS_STAGES = [ + { label: "Request", icon: FileText, statuses: ["DRAFT", "RFQ_SUBMITTED"] }, + { + label: "Quotation", + icon: ShieldCheck, + statuses: ["QUOTATION_SENT", "QUOTATION_APPROVED", "QUOTATION_REJECTED"], + }, + { + label: "Approval", + icon: FileSignature, + statuses: ["PENDING_APPROVAL", "APPROVED"], + }, + { + label: "Execution", + icon: CheckCircle2, + statuses: ["SIGNED_CUSTOMER", "FULLY_EXECUTED", "PAID"], + }, + { + label: "In Transit", + icon: Train, + statuses: ["IN_TRANSIT", "PENDING_CONSOLIDATION", "CONSOLIDATED"], + }, + { label: "Complete", icon: Check, statuses: ["COMPLETED"] }, +]; + +const STATUS_CONFIG: Record< + string, + { title: string; description: string; color: string; stage: number } +> = { + DRAFT: { + title: "Draft", + description: "Booking is being prepared.", + color: "text-slate-500", + stage: 0, + }, + RFQ_SUBMITTED: { + title: "RFQ Submitted", + description: "Customer has submitted a request for quotation.", + color: "text-amber-600", + stage: 0, + }, + QUOTATION_SENT: { + title: "Quotation Sent", + description: "A formal quotation has been sent to the customer.", + color: "text-sky-600", + stage: 1, + }, + QUOTATION_APPROVED: { + title: "Quotation Approved", + description: "Customer approved the quotation.", + color: "text-emerald-600", + stage: 1, + }, + QUOTATION_REJECTED: { + title: "Quotation Rejected", + description: "Customer rejected the quotation.", + color: "text-red-600", + stage: 1, + }, + PENDING_APPROVAL: { + title: "Pending Approval", + description: "Booking requires your approval to proceed.", + color: "text-amber-600", + stage: 2, + }, + APPROVED: { + title: "Approved", + description: "Booking has been approved by all parties.", + color: "text-emerald-600", + stage: 2, + }, + SIGNED_CUSTOMER: { + title: "Customer Signed", + description: "Customer has signed the contract.", + color: "text-sky-600", + stage: 3, + }, + FULLY_EXECUTED: { + title: "Fully Executed", + description: "All parties have signed.", + color: "text-indigo-600", + stage: 3, + }, + PAID: { + title: "Paid", + description: "Payment received.", + color: "text-emerald-600", + stage: 3, + }, + IN_TRANSIT: { + title: "In Transit", + description: "Cargo is moving through the rail network.", + color: "text-sky-600", + stage: 4, + }, + PENDING_CONSOLIDATION: { + title: "Pending Consolidation", + description: "Cargo awaiting consolidation.", + color: "text-amber-500", + stage: 4, + }, + CONSOLIDATED: { + title: "Consolidated", + description: "Cargo merged into larger shipment.", + color: "text-indigo-500", + stage: 4, + }, + COMPLETED: { + title: "Completed", + description: "Service completed successfully.", + color: "text-emerald-600", + stage: 5, + }, + CANCELLED: { + title: "Cancelled", + description: "Booking terminated.", + color: "text-red-600", + stage: -1, + }, +}; + +export default function BookingRequestDetailPage() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const [booking, setBooking] = useState( + id ? getBookingRequestById(id) : undefined, + ); + + if (!booking) { + return ( +
+ +
+ +
+

+ Booking not found +

+ +
+
+ ); + } + + const statusConfig = STATUS_CONFIG[booking.status] ?? STATUS_CONFIG.DRAFT; + const currentStage = statusConfig.stage; + + const canApprove = ["PENDING_APPROVAL", "RFQ_SUBMITTED"].includes( + booking.status, + ); + const canReject = !["COMPLETED", "CANCELLED", "QUOTATION_REJECTED"].includes( + booking.status, + ); + + function handleApprove() { + if (!booking) return; + const nextStatus = + booking.status === "RFQ_SUBMITTED" + ? ("QUOTATION_SENT" as const) + : ("APPROVED" as const); + updateBookingRequestStatus(booking.id, nextStatus); + setBooking(getBookingRequestById(booking.id)); + } + + function handleReject() { + if (!booking) return; + updateBookingRequestStatus(booking.id, "CANCELLED"); + setBooking(getBookingRequestById(booking.id)); + } + + return ( +
+
+ + +
+ + +
+
+ +
+
+
+

+ {booking.reference} +

+ + +
+
+ {booking.customer} + + + + Requested {booking.scheduledDate} + + + + + {new Date(booking.createdAt).toLocaleDateString()} + +
+
+
+
+
+ +
+ {canReject && ( + + )} + {canApprove && ( + + )} +
+
+ + + + + + Status Lifecycle + + + Track the booking from request to completion + + + +
+
+
= 0 + ? `${(currentStage / (PROGRESS_STAGES.length - 1)) * 100}%` + : "0%", + }} + /> +
+ {PROGRESS_STAGES.map((stage, idx) => { + const isCompleted = idx < currentStage; + const isActive = idx === currentStage; + return ( +
+
+ {isCompleted ? ( + + ) : ( + + )} +
+ + {stage.label} + +
+ ); + })} +
+ +
+
+ {booking.status === "CANCELLED" ? ( + + ) : ( + + )} +
+
+

+ {statusConfig.title} +

+

+ {statusConfig.description} +

+
+
+ + + +
+
+ + + + + Route & Service + + + +
+ } + /> +
+
+ + +
+ + {booking.serviceType.replace(/_/g, " ")} + +
+ } + /> +
+ +
+ } + label="Trade Direction" + value={booking.tradeDirection} + /> + } + label="Return" + value={ + booking.serviceType === "RAIL_AND_FORWARDING" + ? "With Return" + : "Without Return" + } + /> + {booking.shippingLine && ( + } + label="Shipping Line" + value={booking.shippingLine} + /> + )} +
+
+
+ + {(booking.firstMilePickupAddress || + booking.lastMileDeliveryAddress) && ( + + + + + Mile Services + + + + {booking.firstMilePickupAddress && ( +
+

+ First Mile +

+ +
+ )} + {booking.lastMileDeliveryAddress && ( +
+

+ Last Mile +

+ +
+ )} +
+
+ )} + + + + + + Cargo Specifications + + + +
+ } + label="Type" + value={booking.cargoType} + /> + } + label="Total Weight" + value={`${booking.cargoTotalWeightVgm} Tons`} + /> + {booking.shippingLine && ( + } + label="Shipping Line" + value={booking.shippingLine} + /> + )} +
+ +
+ + Hazardous: {booking.isHazardous ? "Yes" : "No"} + + {booking.pnrCode && ( + + PNR: {booking.pnrCode} + + )} +
+
+
+
+ +
+ + + + + Contract Info + + + + + + + + + + + + + + + {canApprove && ( + + + + + Approval Required + + + This booking is waiting for your review. + + + + + + + + )} +
+
+
+
+ ); +} + +function StatusBadge({ status }: { status: string }) { + const style = STATUS_STYLES[status] ?? { + label: status, + color: "bg-muted text-muted-foreground border-border", + }; + return ( + + {style.label} + + ); +} + +function PriorityBadge({ score }: { score: number }) { + if (score >= 3) { + return ( + + Urgent + + ); + } + if (score === 2) { + return ( + + High + + ); + } + return ( + + Normal + + ); +} + +function RouteEndpoint({ + label, + station, + icon, +}: { + label: string; + station: string; + icon: React.ReactNode; +}) { + return ( +
+
+
{icon}
+
+
+

+ {label} +

+

{station}

+
+
+ ); +} + +function InfoItem({ + icon, + label, + value, +}: { + icon?: React.ReactNode; + label: string; + value?: string | number | null; +}) { + return ( +
+ {icon && ( +
+ {icon} +
+ )} +
+

+ {label} +

+

{value ?? "—"}

+
+
+ ); +} diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestsPage.tsx new file mode 100644 index 000000000..d59658131 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestsPage.tsx @@ -0,0 +1,479 @@ +import { useMemo, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { + AlertCircle, + ArrowRight, + Calendar, + Clock, + Eye, + FileText, + Filter, + MoreHorizontal, + Package, + Search, + ShieldCheck, + Train, + User, +} from "lucide-react"; + +import Breadcrumbs from "@/components/ui/Breadcrumbs"; +import { cn } from "@/lib/utils"; +import { + getBookingRequests, + BOOKING_STATUSES, + type BookingRequest, +} from "./booking-requests.mock"; +import { + DataTable, + DataTableFooter, + type ColumnDef, + usePagination, + Badge, + Button, + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + Input, + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + Separator, +} from "@edr/ui-common"; + +const STATUS_STYLES: Record = { + DRAFT: { + label: "Draft", + color: "bg-slate-100 text-slate-700 border-slate-300", + }, + RFQ_SUBMITTED: { + label: "RFQ Submitted", + color: "bg-amber-50 text-amber-700 border-amber-200", + }, + QUOTATION_SENT: { + label: "Quotation Sent", + color: "bg-sky-50 text-sky-700 border-sky-200", + }, + QUOTATION_APPROVED: { + label: "Quotation Approved", + color: "bg-emerald-50 text-emerald-700 border-emerald-200", + }, + QUOTATION_REJECTED: { + label: "Quotation Rejected", + color: "bg-red-50 text-red-700 border-red-200", + }, + PENDING_APPROVAL: { + label: "Pending Approval", + color: "bg-amber-50 text-amber-700 border-amber-200", + }, + APPROVED: { + label: "Approved", + color: "bg-emerald-50 text-emerald-700 border-emerald-200", + }, + SIGNED_CUSTOMER: { + label: "Customer Signed", + color: "bg-sky-50 text-sky-700 border-sky-200", + }, + FULLY_EXECUTED: { + label: "Fully Executed", + color: "bg-indigo-50 text-indigo-700 border-indigo-200", + }, + PAID: { + label: "Paid", + color: "bg-emerald-50 text-emerald-700 border-emerald-200", + }, + IN_TRANSIT: { + label: "In Transit", + color: "bg-sky-50 text-sky-700 border-sky-200", + }, + COMPLETED: { + label: "Completed", + color: "bg-indigo-50 text-indigo-700 border-indigo-200", + }, + CANCELLED: { + label: "Cancelled", + color: "bg-red-50 text-red-700 border-red-200", + }, + PENDING_CONSOLIDATION: { + label: "Pending Consolidation", + color: "bg-amber-50 text-amber-700 border-amber-200", + }, + CONSOLIDATED: { + label: "Consolidated", + color: "bg-indigo-50 text-indigo-700 border-indigo-200", + }, +}; + +function StatusBadge({ status }: { status: string }) { + const style = STATUS_STYLES[status] ?? { + label: status, + color: "bg-muted text-muted-foreground border-border", + }; + return ( + + {style.label} + + ); +} + +function PriorityBadge({ score }: { score: number }) { + if (score >= 3) { + return ( + + Urgent + + ); + } + if (score === 2) { + return ( + + High + + ); + } + return ( + + Normal + + ); +} + +export default function BookingRequestsPage() { + const navigate = useNavigate(); + const { pagination, setPagination } = usePagination({ pageSize: 10 }); + const [query, setQuery] = useState(""); + const [statusFilter, setStatusFilter] = useState(null); + + const bookingRequests = useMemo(() => getBookingRequests(), []); + + const filtered = useMemo(() => { + const q = query.trim().toLowerCase(); + return bookingRequests.filter((b) => { + if ( + q && + !b.reference.toLowerCase().includes(q) && + !b.customer.toLowerCase().includes(q) + ) { + return false; + } + if (statusFilter && b.status !== statusFilter) { + return false; + } + return true; + }); + }, [bookingRequests, query, statusFilter]); + + const total = filtered.length; + const pageCount = Math.max(1, Math.ceil(total / pagination.pageSize)); + const start = pagination.pageIndex * pagination.pageSize; + const end = Math.min(start + pagination.pageSize, total); + + const paginatedData = useMemo( + () => filtered.slice(start, end), + [start, end, filtered], + ); + + const pendingCount = bookingRequests.filter( + (b) => b.status === "PENDING_APPROVAL" || b.status === "RFQ_SUBMITTED", + ).length; + const activeCount = bookingRequests.filter( + (b) => !["COMPLETED", "CANCELLED"].includes(b.status), + ).length; + const urgentCount = bookingRequests.filter( + (b) => b.priorityScore >= 3, + ).length; + + const columns: ColumnDef[] = [ + { + id: "booking", + header: "Booking", + cell: ({ row }) => { + const b = row.original; + return ( +
+
+ +
+
+

{b.reference}

+

+ + {b.customer} +

+
+
+ ); + }, + }, + { + id: "route", + header: "Route", + cell: ({ row }) => { + const b = row.original; + return ( +
+
+ {b.originYard} + + {b.destinationYard} +
+ + {b.tradeDirection} + +
+ ); + }, + }, + { + id: "status", + header: "Status", + cell: ({ row }) => , + }, + { + id: "service", + header: "Service", + cell: ({ row }) => { + const b = row.original; + return ( +
+ + {b.serviceType.replace(/_/g, " ")} + + + + {b.scheduledDate} + +
+ ); + }, + }, + { + id: "cargo", + header: "Cargo", + cell: ({ row }) => { + const b = row.original; + return ( +
+ + {b.cargoType} + + + {b.cargoTotalWeightVgm}T + +
+ ); + }, + }, + { + id: "priority", + header: "Priority", + cell: ({ row }) => , + }, + { + id: "amount", + header: "Amount", + cell: ({ row }) => { + const b = row.original; + return ( + + {b.paymentCurrency} {b.totalAmount.toLocaleString()} + + ); + }, + }, + { + id: "actions", + size: 40, + cell: ({ row }) => { + const b = row.original; + return ( +
e.stopPropagation()} + > + + + + + + + navigate(`/dashboard/booking-requests/${b.id}`) + } + > + + View Details + + + + navigate(`/dashboard/booking-requests/${b.id}`) + } + > + + Review + + + +
+ ); + }, + }, + ]; + + return ( +
+
+ + + +
+

+ Booking Requests +

+

+ Review, approve, or reject customer booking requests across the + freight network. +

+
+ +
+
+ + { + setQuery(e.target.value); + setPagination({ + pageIndex: 0, + pageSize: pagination.pageSize, + }); + }} + placeholder="Search reference or customer..." + className="pl-8!" + /> +
+
+
+ +
+ } + /> + } + /> + } /> + } /> +
+ + + +
+ All Booking Requests + + {total} request{total !== 1 ? "s" : ""} found + +
+ +
+ {statusFilter && ( + + )} + + + + + + {BOOKING_STATUSES.map((s) => ( + setStatusFilter(s)} + > + {STATUS_STYLES[s]?.label ?? s} + + ))} + + +
+
+ + + + navigate(`/dashboard/booking-requests/${row.id}`) + } + pagination={{ + pageIndex: pagination.pageIndex, + pageSize: pagination.pageSize, + pageCount, + totalCount: total, + }} + tableOptions={{ + state: { pagination }, + onPaginationChange: setPagination, + }} + containerClassName="border-b shadow-none" + footer={DataTableFooter} + /> + +
+
+
+ ); +} + +function StatCard({ + label, + value, + icon, +}: { + label: string; + value: number; + icon: React.ReactNode; +}) { + return ( + + +
+

{label}

+

{value}

+
+
+ {icon} +
+
+
+ ); +} diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/booking-requests.mock.ts b/apps/edr-freight-web/backoffice/src/pages/bookings/booking-requests.mock.ts new file mode 100644 index 000000000..a5d13cbc0 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/booking-requests.mock.ts @@ -0,0 +1,148 @@ +export interface BookingRequest { + id: string; + reference: string; + customer: string; + status: (typeof BOOKING_STATUSES)[number]; + scheduledDate: string; + totalAmount: number; + paymentStatus: string; + contractType: string; + serviceType: string; + tradeDirection: string; + originYard: string; + destinationYard: string; + cargoType: string; + cargoTotalWeightVgm: number; + isHazardous: boolean; + paymentCurrency: string; + priorityScore: number; + firstMilePickupAddress: string | null; + lastMileDeliveryAddress: string | null; + shippingLine: string | null; + pnrCode: string | null; + createdBy: string; + createdAt: string; + updatedAt: string; +} + +export const BOOKING_STATUSES = [ + "DRAFT", + "RFQ_SUBMITTED", + "QUOTATION_SENT", + "QUOTATION_APPROVED", + "QUOTATION_REJECTED", + "PENDING_APPROVAL", + "APPROVED", + "SIGNED_CUSTOMER", + "FULLY_EXECUTED", + "PAID", + "IN_TRANSIT", + "COMPLETED", + "CANCELLED", + "PENDING_CONSOLIDATION", + "CONSOLIDATED", +] as const; + +const customers = [ + "Ethio Cargo Logistics", + "Djibouti Shipping PLC", + "Horn of Africa Traders", + "Addis Freight Forwarders", + "Red Sea Maritime Services", + "Dire Dawa Imports Ltd", + "Awash Agro Industry", + "Mieso Mineral Exports", +]; + +const yards = [ + "Addis Ababa Dry Port", + "Mojo Inland Container Depot", + "Dire Dawa Freight Station", + "Djibouti Port Terminal", + "Adama Logistics Hub", + "Awash Cargo Center", +]; + +const serviceTypes = ["RAIL", "RAIL_AND_FORWARDING"]; +const tradeDirections = ["EXPORT", "IMPORT", "DOMESTIC"]; +const cargoTypes = ["Containerized", "Bulk", "Liquid", "Refrigerated", "Hazardous"]; +const shippingLines = ["MSC", "CMA CGM", "Maersk", "COSCO", "Hapag-Lloyd", null]; + +function pick(arr: T[], index: number): T { + return arr[index % arr.length]; +} + +function randDate(daysAgo: number): string { + const d = new Date(2026, 4, 28 - daysAgo); + return d.toISOString(); +} + +const now = Date.now(); + +const INITIAL_REQUESTS: BookingRequest[] = Array.from({ length: 25 }, (_, i) => { + const statusIndex = i % BOOKING_STATUSES.length; + const status = BOOKING_STATUSES[statusIndex]; + const customer = pick(customers, i); + + return { + id: String(i + 1), + reference: `EDR-BK-${String(2026001 + i).slice(-6)}`, + customer, + status, + scheduledDate: new Date(2026, 5, 1 + (i % 28)).toISOString().slice(0, 10), + totalAmount: 1500 + i * 320 + (i % 7) * 100, + paymentStatus: status === "PAID" || status === "COMPLETED" ? "PAID" : status === "CANCELLED" ? "REFUNDED" : "PENDING", + contractType: i % 5 === 0 ? "RENEWAL" : "NEW", + serviceType: pick(serviceTypes, i), + tradeDirection: pick(tradeDirections, i), + originYard: pick(yards, i), + destinationYard: pick(yards, i + 3), + cargoType: pick(cargoTypes, i), + cargoTotalWeightVgm: 10 + ((i * 7) % 90), + isHazardous: i % 7 === 0, + paymentCurrency: "USD", + priorityScore: i % 4 === 0 ? 3 : i % 3 === 0 ? 2 : 1, + firstMilePickupAddress: i % 3 === 0 ? "Bole Industrial Zone, Addis Ababa" : null, + lastMileDeliveryAddress: i % 4 === 0 ? "Port Boulevard, Djibouti City" : null, + shippingLine: pick(shippingLines, i), + pnrCode: i % 6 === 0 ? `PNR-${202600 + i}` : null, + createdBy: customer, + createdAt: randDate(30 - i), + updatedAt: randDate(2), + }; +}); + +export function saveBookingRequestsToStorage(data: BookingRequest[]) { + if (typeof window !== "undefined" && window.localStorage) { + localStorage.setItem("edr_backoffice_booking_requests", JSON.stringify(data)); + } +} + +export function getBookingRequestById(id: string): BookingRequest | undefined { + const requests = getBookingRequests(); + return requests.find((r) => r.id === id); +} + +export function updateBookingRequestStatus(id: string, newStatus: (typeof BOOKING_STATUSES)[number]) { + const requests = getBookingRequests(); + const idx = requests.findIndex((r) => r.id === id); + if (idx === -1) return; + requests[idx] = { ...requests[idx], status: newStatus, updatedAt: new Date().toISOString() }; + saveBookingRequestsToStorage(requests); +} + +export function getBookingRequests(): BookingRequest[] { + if (typeof window === "undefined" || !window.localStorage) { + return INITIAL_REQUESTS; + } + const data = localStorage.getItem("edr_backoffice_booking_requests"); + if (!data) { + localStorage.setItem("edr_backoffice_booking_requests", JSON.stringify(INITIAL_REQUESTS)); + return INITIAL_REQUESTS; + } + try { + return JSON.parse(data); + } catch { + return INITIAL_REQUESTS; + } +} diff --git a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx index 5be286d76..4a1c53fef 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx @@ -12,15 +12,12 @@ import { LoaderCircle, } from "lucide-react"; import { Button } from "@edr/ui-common"; -import type { Freight } from "@edr/types"; -import Breadcrumbs from "@/components/Breadcrumbs"; import { api } from "@/services/api"; import type { CreateBookingPayload } from "@/services/bookings.service"; import { BookingFormInputValues, STEPS, bookingFormSchema, - calcWagons, getRouteDirection, initialBookingFormValues, stepFields, @@ -135,14 +132,14 @@ export default function NewBookingPage() { return ""; }; - const cargoTypeId = - data.cargoType === "container" - ? findContainerCargoTypeId() - : (findCargoTypeId( - data.freightType === "bulk" - ? data.bulkCommodity - : data.breakBulkType, - ) ?? ""); + const cargoTypeId = cargoTree[0].id; + // data.cargoType === "container" + // ? findContainerCargoTypeId() + // : (findCargoTypeId( + // data.freightType === "bulk" + // ? data.bulkCommodity + // : data.breakBulkType, + // ) ?? ""); const cargoFreeText = data.cargoType === "container"