diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx
index 63c09a67c..6c454052d 100644
--- a/apps/edr-freight-web/backoffice/src/App.tsx
+++ b/apps/edr-freight-web/backoffice/src/App.tsx
@@ -52,6 +52,7 @@ import TrainScheduleV2DetailPage from "./pages/trainScheduling/TrainScheduleV2De
import TrainScheduleTrackPage from "./pages/trainScheduling/TrainScheduleTrackPage";
import TrainSchedulingGlobalRulesPage from "./pages/trainScheduling/TrainSchedulingGlobalRulesPage";
import FleetResourcePage from "./pages/fleet/FleetResourcePage";
+import LastMilePage from "./pages/operations/LastMilePage";
import { getCategorySidebarChildren } from "./pages/ruleEngine/config/resources";
import { FREIGHT_PERMS, hasPermission as hasFreightPermission } from "./lib/permissions";
import { RequirePermission } from "./components/auth/RequirePermission";
@@ -113,6 +114,12 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
icon: ,
permission: FREIGHT_PERMS.trainScheduling.view,
},
+ {
+ label: "Last Mile",
+ href: "/dashboard/operations/last-mile",
+ icon: ,
+ permission: FREIGHT_PERMS.trainScheduling.view,
+ },
],
},
{
@@ -408,6 +415,14 @@ const App = () => {
}
/>
+
+
+
+ }
+ />
= [
subtitle: "View booking payment transactions",
},
},
+ {
+ prefix: "/dashboard/operations/last-mile",
+ meta: {
+ title: "Last Mile",
+ subtitle: "Assign vehicles to final-leg deliveries",
+ },
+ },
{
prefix: "/dashboard/operations/train-scheduling-v2/",
meta: {
diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx
new file mode 100644
index 000000000..e879685be
--- /dev/null
+++ b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx
@@ -0,0 +1,295 @@
+import { useMemo, useState } from "react";
+import { Truck } from "lucide-react";
+import type { ColumnDef } from "@edr/ui-common";
+import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common";
+import {
+ Badge,
+ Box,
+ Button,
+ Card,
+ Group,
+ Modal,
+ Select,
+ Stack,
+ Text,
+ TextInput,
+} from "@mantine/core";
+
+import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
+import { useToast } from "@/hooks/use-toast";
+
+type LastMileStatus = "UNASSIGNED" | "ASSIGNED";
+
+interface LastMileJob {
+ id: string;
+ bookingRef: string;
+ customer: string;
+ destination: string;
+ cargo: string;
+ status: LastMileStatus;
+ assignedVehicle: string | null;
+}
+
+// Placeholder data — replace with a real last-mile service once the API exists.
+const PLACEHOLDER_JOBS: LastMileJob[] = [
+ {
+ id: "1",
+ bookingRef: "BK-10241",
+ customer: "Awash Trading PLC",
+ destination: "Bole Sub-city, Addis Ababa",
+ cargo: "20ft container · Electronics",
+ status: "UNASSIGNED",
+ assignedVehicle: null,
+ },
+ {
+ id: "2",
+ bookingRef: "BK-10238",
+ customer: "Dire Logistics",
+ destination: "Industry Zone, Dire Dawa",
+ cargo: "Bulk · 18t Cement",
+ status: "ASSIGNED",
+ assignedVehicle: "Isuzu FVR (3-AA-45821)",
+ },
+ {
+ id: "3",
+ bookingRef: "BK-10233",
+ customer: "Horizon Imports",
+ destination: "Kality Terminal, Addis Ababa",
+ cargo: "40ft container · Machinery",
+ status: "UNASSIGNED",
+ assignedVehicle: null,
+ },
+];
+
+// Placeholder vehicle options — replace with the vehicles service.
+const VEHICLE_OPTIONS = [
+ { value: "isuzu-fvr-45821", label: "Isuzu FVR (3-AA-45821)" },
+ { value: "sino-howo-12044", label: "Sinotruk Howo (3-AA-12044)" },
+ { value: "mercedes-actros-90113", label: "Mercedes Actros (3-AA-90113)" },
+];
+
+const LastMilePage = () => {
+ const { toast } = useToast();
+ const { pagination, setPagination } = usePagination({ pageSize: 10 });
+
+ const [jobs, setJobs] = useState(PLACEHOLDER_JOBS);
+ const [search, setSearch] = useState("");
+
+ const [assignOpen, setAssignOpen] = useState(false);
+ const [activeJobId, setActiveJobId] = useState(null);
+ const [vehicleValue, setVehicleValue] = useState(null);
+
+ const filteredJobs = useMemo(() => {
+ const term = search.trim().toLowerCase();
+ if (!term) return jobs;
+ return jobs.filter((job) =>
+ [job.bookingRef, job.customer, job.destination, job.cargo]
+ .join(" ")
+ .toLowerCase()
+ .includes(term),
+ );
+ }, [jobs, search]);
+
+ const pageCount = Math.max(1, Math.ceil(filteredJobs.length / pagination.pageSize));
+ const pagedJobs = useMemo(() => {
+ const start = pagination.pageIndex * pagination.pageSize;
+ return filteredJobs.slice(start, start + pagination.pageSize);
+ }, [filteredJobs, pagination.pageIndex, pagination.pageSize]);
+
+ const openAssign = (jobId: string | null) => {
+ setActiveJobId(jobId);
+ setVehicleValue(null);
+ setAssignOpen(true);
+ };
+
+ const closeAssign = () => {
+ setAssignOpen(false);
+ setActiveJobId(null);
+ setVehicleValue(null);
+ };
+
+ const handleAssign = () => {
+ const jobId = activeJobId ?? filteredJobs.find((job) => job.status === "UNASSIGNED")?.id;
+ if (!jobId || !vehicleValue) {
+ toast({
+ title: "Select a vehicle",
+ description: "Choose a vehicle to assign to this delivery.",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ const vehicleLabel =
+ VEHICLE_OPTIONS.find((option) => option.value === vehicleValue)?.label ?? vehicleValue;
+
+ setJobs((current) =>
+ current.map((job) =>
+ job.id === jobId
+ ? { ...job, status: "ASSIGNED", assignedVehicle: vehicleLabel }
+ : job,
+ ),
+ );
+
+ toast({ title: "Vehicle assigned", description: vehicleLabel });
+ closeAssign();
+ };
+
+ const columns = useMemo((): ColumnDef[] => {
+ const headerClassName = ruleEngineTable.headerCell;
+ const cellClassName = ruleEngineTable.bodyCell;
+ return [
+ {
+ id: "bookingRef",
+ header: "Booking",
+ meta: { headerClassName, cellClassName },
+ cell: ({ row }) => (
+
+ {row.original.bookingRef}
+
+ ),
+ },
+ {
+ id: "customer",
+ header: "Customer",
+ meta: { headerClassName, cellClassName },
+ cell: ({ row }) => row.original.customer,
+ },
+ {
+ id: "destination",
+ header: "Destination",
+ meta: { headerClassName, cellClassName },
+ cell: ({ row }) => row.original.destination,
+ },
+ {
+ id: "cargo",
+ header: "Cargo",
+ meta: { headerClassName, cellClassName },
+ cell: ({ row }) => row.original.cargo,
+ },
+ {
+ id: "vehicle",
+ header: "Vehicle",
+ meta: { headerClassName, cellClassName },
+ cell: ({ row }) =>
+ row.original.assignedVehicle ?? —,
+ },
+ {
+ id: "status",
+ header: "Status",
+ meta: { headerClassName, cellClassName },
+ cell: ({ row }) => (
+
+ {row.original.status === "ASSIGNED" ? "Assigned" : "Unassigned"}
+
+ ),
+ },
+ {
+ id: "actions",
+ header: "Actions",
+ meta: { headerClassName, cellClassName: `${cellClassName} whitespace-nowrap` },
+ cell: ({ row }) => (
+
+ }
+ onClick={() => openAssign(row.original.id)}
+ >
+ {row.original.status === "ASSIGNED" ? "Reassign" : "Assign"}
+
+
+ ),
+ },
+ ];
+ }, []);
+
+ const tableStatus = "success" as const;
+
+ return (
+
+
+
+
+
+ setSearch(e.currentTarget.value)}
+ w={260}
+ />
+ }
+ onClick={() => openAssign(null)}
+ >
+ Assign Mile
+
+
+
+
+ (
+
+ )}
+ />
+
+
+
+ Assign Vehicle}
+ radius="lg"
+ centered
+ >
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default LastMilePage;