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 }) => ( + + + + ), + }, + ]; + }, []); + + const tableStatus = "success" as const; + + return ( + + + + + + setSearch(e.currentTarget.value)} + w={260} + /> + + + + + ( + + )} + /> + + + + Assign Vehicle} + radius="lg" + centered + > + +