+
+
+
+
+
+
+
+
+
+
+
+ {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 (
+
+
+
+
+ {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 (
+
+
+
+
+ {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"