mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
last mile
This commit is contained in:
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user