last mile

This commit is contained in:
natib21
2026-06-20 13:05:00 +00:00
parent 4e926c0d39
commit b3f3faad72

View File

@@ -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 }) => (
<Stack gap="sm">
<Group justify="space-between">
<Text fw={600}>{job.bookingRef}</Text>
<Badge
color={job.status === "ASSIGNED" ? "green" : "orange"}
variant="light"
size="sm"
>
{job.status === "ASSIGNED" ? "Assigned" : "Unassigned"}
</Badge>
<Group gap="xs">
<Badge
color={DELIVERY_STATUS_META[job.deliveryStatus].color}
variant="light"
size="sm"
>
{DELIVERY_STATUS_META[job.deliveryStatus].label}
</Badge>
<Badge
color={job.status === "ASSIGNED" ? "green" : "orange"}
variant="light"
size="sm"
>
{job.status === "ASSIGNED" ? "Assigned" : "Unassigned"}
</Badge>
</Group>
</Group>
<SimpleGrid cols={2} spacing="sm">
<InfoRow label="Customer" value={job.customer} />
@@ -141,6 +179,7 @@ const LastMilePage = () => {
const [jobs, setJobs] = useState<LastMileJob[]>(PLACEHOLDER_JOBS);
const [search, setSearch] = useState("");
const [statusFilter, setStatusFilter] = useState<StatusFilter>("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 ?? <Text c="dimmed"></Text>,
},
{
id: "status",
id: "deliveryStatus",
header: "Status",
meta: { headerClassName, cellClassName },
cell: ({ row }) => {
const meta = DELIVERY_STATUS_META[row.original.deliveryStatus];
return (
<Badge color={meta.color} variant="light" size="sm">
{meta.label}
</Badge>
);
},
},
{
id: "status",
header: "Assignment",
meta: { headerClassName, cellClassName },
cell: ({ row }) => (
<Badge
color={row.original.status === "ASSIGNED" ? "green" : "orange"}
@@ -329,12 +395,38 @@ const LastMilePage = () => {
<Stack gap={0}>
<Box px="md" pt="md" pb="sm" w="100%">
<Group justify="space-between" wrap="wrap">
<TextInput
placeholder="Search deliveries…"
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
w={260}
/>
<Group gap="sm" wrap="wrap">
<TextInput
placeholder="Search deliveries…"
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
w={260}
/>
<Menu position="bottom-start" width={200} withinPortal>
<Menu.Target>
<Button variant="default" leftSection={<Filter size={16} />}>
{FILTER_OPTIONS.find((o) => o.value === statusFilter)?.label ??
"All statuses"}
</Button>
</Menu.Target>
<Menu.Dropdown>
{FILTER_OPTIONS.map((option) => (
<Menu.Item
key={option.value}
rightSection={
statusFilter === option.value ? <Check size={14} /> : null
}
onClick={() => {
setStatusFilter(option.value);
setPagination((p) => ({ ...p, pageIndex: 0 }));
}}
>
{option.label}
</Menu.Item>
))}
</Menu.Dropdown>
</Menu>
</Group>
<Button
leftSection={<Truck size={16} />}
onClick={() => openAssign(null)}