From b3f3faad72c0a1884714c95a0bf8680dc767cd34 Mon Sep 17 00:00:00 2001 From: natib21 Date: Sat, 20 Jun 2026 13:05:00 +0000 Subject: [PATCH] last mile --- .../src/pages/operations/LastMilePage.tsx | 134 +++++++++++++++--- 1 file changed, 113 insertions(+), 21 deletions(-) diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx index 075d73705..08166e5b8 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx @@ -1,5 +1,5 @@ import { useMemo, useState } from "react"; -import { Eye, MoreHorizontal, RefreshCw, Truck } from "lucide-react"; +import { Check, Eye, Filter, MoreHorizontal, RefreshCw, Truck } from "lucide-react"; import type { ColumnDef } from "@edr/ui-common"; import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common"; import { @@ -23,6 +23,7 @@ import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles"; import { useToast } from "@/hooks/use-toast"; type LastMileStatus = "UNASSIGNED" | "ASSIGNED"; +type DeliveryStatus = "PAYMENT_PENDING" | "READY_TO_TRANSIT" | "DELIVERED"; interface LastMileJob { id: string; @@ -31,6 +32,7 @@ interface LastMileJob { destination: string; cargo: string; status: LastMileStatus; + deliveryStatus: DeliveryStatus; assignedVehicle: string | null; // Booking info shown in the Assign / View Detail modals. serviceType: string; @@ -41,6 +43,30 @@ interface LastMileJob { requestedDate: string; } +const DELIVERY_STATUS_META: Record< + DeliveryStatus, + { label: string; color: string } +> = { + PAYMENT_PENDING: { label: "Payment Pending", color: "yellow" }, + READY_TO_TRANSIT: { label: "Ready to Transit", color: "blue" }, + DELIVERED: { label: "Delivered", color: "green" }, +}; + +// Single filter covering both the delivery lifecycle and assignment state. +type StatusFilter = + | "ALL" + | DeliveryStatus + | LastMileStatus; + +const FILTER_OPTIONS: { value: StatusFilter; label: string }[] = [ + { value: "ALL", label: "All statuses" }, + { value: "PAYMENT_PENDING", label: "Payment Pending" }, + { value: "READY_TO_TRANSIT", label: "Ready to Transit" }, + { value: "DELIVERED", label: "Delivered" }, + { value: "ASSIGNED", label: "Assigned" }, + { value: "UNASSIGNED", label: "Unassigned" }, +]; + // Placeholder data — replace with a real last-mile service once the API exists. const PLACEHOLDER_JOBS: LastMileJob[] = [ { @@ -50,6 +76,7 @@ const PLACEHOLDER_JOBS: LastMileJob[] = [ destination: "Bole Sub-city, Addis Ababa", cargo: "20ft container · Electronics", status: "UNASSIGNED", + deliveryStatus: "PAYMENT_PENDING", assignedVehicle: null, serviceType: "Door-to-door (Last Mile)", weight: "12.4 t", @@ -65,6 +92,7 @@ const PLACEHOLDER_JOBS: LastMileJob[] = [ destination: "Industry Zone, Dire Dawa", cargo: "Bulk · 18t Cement", status: "ASSIGNED", + deliveryStatus: "READY_TO_TRANSIT", assignedVehicle: "Isuzu FVR (3-AA-45821)", serviceType: "Terminal-to-door (Last Mile)", weight: "18.0 t", @@ -80,6 +108,7 @@ const PLACEHOLDER_JOBS: LastMileJob[] = [ destination: "Kality Terminal, Addis Ababa", cargo: "40ft container · Machinery", status: "UNASSIGNED", + deliveryStatus: "DELIVERED", assignedVehicle: null, serviceType: "Door-to-door (Last Mile)", weight: "24.7 t", @@ -111,13 +140,22 @@ const BookingInfo = ({ job }: { job: LastMileJob }) => ( {job.bookingRef} - - {job.status === "ASSIGNED" ? "Assigned" : "Unassigned"} - + + + {DELIVERY_STATUS_META[job.deliveryStatus].label} + + + {job.status === "ASSIGNED" ? "Assigned" : "Unassigned"} + + @@ -141,6 +179,7 @@ const LastMilePage = () => { const [jobs, setJobs] = useState(PLACEHOLDER_JOBS); const [search, setSearch] = useState(""); + const [statusFilter, setStatusFilter] = useState("ALL"); const [assignOpen, setAssignOpen] = useState(false); const [detailOpen, setDetailOpen] = useState(false); @@ -152,16 +191,30 @@ const LastMilePage = () => { [jobs, activeJobId], ); + const matchesStatusFilter = (job: LastMileJob) => { + switch (statusFilter) { + case "ALL": + return true; + case "ASSIGNED": + case "UNASSIGNED": + return job.status === statusFilter; + default: + return job.deliveryStatus === statusFilter; + } + }; + const filteredJobs = useMemo(() => { const term = search.trim().toLowerCase(); - if (!term) return jobs; - return jobs.filter((job) => - [job.bookingRef, job.customer, job.destination, job.cargo] + return jobs.filter((job) => { + if (!matchesStatusFilter(job)) return false; + if (!term) return true; + return [job.bookingRef, job.customer, job.destination, job.cargo] .join(" ") .toLowerCase() - .includes(term), - ); - }, [jobs, search]); + .includes(term); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [jobs, search, statusFilter]); const pageCount = Math.max(1, Math.ceil(filteredJobs.length / pagination.pageSize)); const pagedJobs = useMemo(() => { @@ -258,9 +311,22 @@ const LastMilePage = () => { row.original.assignedVehicle ?? , }, { - id: "status", + id: "deliveryStatus", header: "Status", meta: { headerClassName, cellClassName }, + cell: ({ row }) => { + const meta = DELIVERY_STATUS_META[row.original.deliveryStatus]; + return ( + + {meta.label} + + ); + }, + }, + { + id: "status", + header: "Assignment", + meta: { headerClassName, cellClassName }, cell: ({ row }) => ( { - setSearch(e.currentTarget.value)} - w={260} - /> + + setSearch(e.currentTarget.value)} + w={260} + /> + + + + + + {FILTER_OPTIONS.map((option) => ( + : null + } + onClick={() => { + setStatusFilter(option.value); + setPagination((p) => ({ ...p, pageIndex: 0 })); + }} + > + {option.label} + + ))} + + +