mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 22:58:17 +00:00
last mile
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo, useState } from "react";
|
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 type { ColumnDef } from "@edr/ui-common";
|
||||||
import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common";
|
import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common";
|
||||||
import {
|
import {
|
||||||
@@ -23,6 +23,7 @@ import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
|
|||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
type LastMileStatus = "UNASSIGNED" | "ASSIGNED";
|
type LastMileStatus = "UNASSIGNED" | "ASSIGNED";
|
||||||
|
type DeliveryStatus = "PAYMENT_PENDING" | "READY_TO_TRANSIT" | "DELIVERED";
|
||||||
|
|
||||||
interface LastMileJob {
|
interface LastMileJob {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -31,6 +32,7 @@ interface LastMileJob {
|
|||||||
destination: string;
|
destination: string;
|
||||||
cargo: string;
|
cargo: string;
|
||||||
status: LastMileStatus;
|
status: LastMileStatus;
|
||||||
|
deliveryStatus: DeliveryStatus;
|
||||||
assignedVehicle: string | null;
|
assignedVehicle: string | null;
|
||||||
// Booking info shown in the Assign / View Detail modals.
|
// Booking info shown in the Assign / View Detail modals.
|
||||||
serviceType: string;
|
serviceType: string;
|
||||||
@@ -41,6 +43,30 @@ interface LastMileJob {
|
|||||||
requestedDate: string;
|
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.
|
// Placeholder data — replace with a real last-mile service once the API exists.
|
||||||
const PLACEHOLDER_JOBS: LastMileJob[] = [
|
const PLACEHOLDER_JOBS: LastMileJob[] = [
|
||||||
{
|
{
|
||||||
@@ -50,6 +76,7 @@ const PLACEHOLDER_JOBS: LastMileJob[] = [
|
|||||||
destination: "Bole Sub-city, Addis Ababa",
|
destination: "Bole Sub-city, Addis Ababa",
|
||||||
cargo: "20ft container · Electronics",
|
cargo: "20ft container · Electronics",
|
||||||
status: "UNASSIGNED",
|
status: "UNASSIGNED",
|
||||||
|
deliveryStatus: "PAYMENT_PENDING",
|
||||||
assignedVehicle: null,
|
assignedVehicle: null,
|
||||||
serviceType: "Door-to-door (Last Mile)",
|
serviceType: "Door-to-door (Last Mile)",
|
||||||
weight: "12.4 t",
|
weight: "12.4 t",
|
||||||
@@ -65,6 +92,7 @@ const PLACEHOLDER_JOBS: LastMileJob[] = [
|
|||||||
destination: "Industry Zone, Dire Dawa",
|
destination: "Industry Zone, Dire Dawa",
|
||||||
cargo: "Bulk · 18t Cement",
|
cargo: "Bulk · 18t Cement",
|
||||||
status: "ASSIGNED",
|
status: "ASSIGNED",
|
||||||
|
deliveryStatus: "READY_TO_TRANSIT",
|
||||||
assignedVehicle: "Isuzu FVR (3-AA-45821)",
|
assignedVehicle: "Isuzu FVR (3-AA-45821)",
|
||||||
serviceType: "Terminal-to-door (Last Mile)",
|
serviceType: "Terminal-to-door (Last Mile)",
|
||||||
weight: "18.0 t",
|
weight: "18.0 t",
|
||||||
@@ -80,6 +108,7 @@ const PLACEHOLDER_JOBS: LastMileJob[] = [
|
|||||||
destination: "Kality Terminal, Addis Ababa",
|
destination: "Kality Terminal, Addis Ababa",
|
||||||
cargo: "40ft container · Machinery",
|
cargo: "40ft container · Machinery",
|
||||||
status: "UNASSIGNED",
|
status: "UNASSIGNED",
|
||||||
|
deliveryStatus: "DELIVERED",
|
||||||
assignedVehicle: null,
|
assignedVehicle: null,
|
||||||
serviceType: "Door-to-door (Last Mile)",
|
serviceType: "Door-to-door (Last Mile)",
|
||||||
weight: "24.7 t",
|
weight: "24.7 t",
|
||||||
@@ -111,13 +140,22 @@ const BookingInfo = ({ job }: { job: LastMileJob }) => (
|
|||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<Text fw={600}>{job.bookingRef}</Text>
|
<Text fw={600}>{job.bookingRef}</Text>
|
||||||
<Badge
|
<Group gap="xs">
|
||||||
color={job.status === "ASSIGNED" ? "green" : "orange"}
|
<Badge
|
||||||
variant="light"
|
color={DELIVERY_STATUS_META[job.deliveryStatus].color}
|
||||||
size="sm"
|
variant="light"
|
||||||
>
|
size="sm"
|
||||||
{job.status === "ASSIGNED" ? "Assigned" : "Unassigned"}
|
>
|
||||||
</Badge>
|
{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>
|
</Group>
|
||||||
<SimpleGrid cols={2} spacing="sm">
|
<SimpleGrid cols={2} spacing="sm">
|
||||||
<InfoRow label="Customer" value={job.customer} />
|
<InfoRow label="Customer" value={job.customer} />
|
||||||
@@ -141,6 +179,7 @@ const LastMilePage = () => {
|
|||||||
|
|
||||||
const [jobs, setJobs] = useState<LastMileJob[]>(PLACEHOLDER_JOBS);
|
const [jobs, setJobs] = useState<LastMileJob[]>(PLACEHOLDER_JOBS);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
const [statusFilter, setStatusFilter] = useState<StatusFilter>("ALL");
|
||||||
|
|
||||||
const [assignOpen, setAssignOpen] = useState(false);
|
const [assignOpen, setAssignOpen] = useState(false);
|
||||||
const [detailOpen, setDetailOpen] = useState(false);
|
const [detailOpen, setDetailOpen] = useState(false);
|
||||||
@@ -152,16 +191,30 @@ const LastMilePage = () => {
|
|||||||
[jobs, activeJobId],
|
[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 filteredJobs = useMemo(() => {
|
||||||
const term = search.trim().toLowerCase();
|
const term = search.trim().toLowerCase();
|
||||||
if (!term) return jobs;
|
return jobs.filter((job) => {
|
||||||
return jobs.filter((job) =>
|
if (!matchesStatusFilter(job)) return false;
|
||||||
[job.bookingRef, job.customer, job.destination, job.cargo]
|
if (!term) return true;
|
||||||
|
return [job.bookingRef, job.customer, job.destination, job.cargo]
|
||||||
.join(" ")
|
.join(" ")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(term),
|
.includes(term);
|
||||||
);
|
});
|
||||||
}, [jobs, search]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [jobs, search, statusFilter]);
|
||||||
|
|
||||||
const pageCount = Math.max(1, Math.ceil(filteredJobs.length / pagination.pageSize));
|
const pageCount = Math.max(1, Math.ceil(filteredJobs.length / pagination.pageSize));
|
||||||
const pagedJobs = useMemo(() => {
|
const pagedJobs = useMemo(() => {
|
||||||
@@ -258,9 +311,22 @@ const LastMilePage = () => {
|
|||||||
row.original.assignedVehicle ?? <Text c="dimmed">—</Text>,
|
row.original.assignedVehicle ?? <Text c="dimmed">—</Text>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "status",
|
id: "deliveryStatus",
|
||||||
header: "Status",
|
header: "Status",
|
||||||
meta: { headerClassName, cellClassName },
|
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 }) => (
|
cell: ({ row }) => (
|
||||||
<Badge
|
<Badge
|
||||||
color={row.original.status === "ASSIGNED" ? "green" : "orange"}
|
color={row.original.status === "ASSIGNED" ? "green" : "orange"}
|
||||||
@@ -329,12 +395,38 @@ const LastMilePage = () => {
|
|||||||
<Stack gap={0}>
|
<Stack gap={0}>
|
||||||
<Box px="md" pt="md" pb="sm" w="100%">
|
<Box px="md" pt="md" pb="sm" w="100%">
|
||||||
<Group justify="space-between" wrap="wrap">
|
<Group justify="space-between" wrap="wrap">
|
||||||
<TextInput
|
<Group gap="sm" wrap="wrap">
|
||||||
placeholder="Search deliveries…"
|
<TextInput
|
||||||
value={search}
|
placeholder="Search deliveries…"
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
value={search}
|
||||||
w={260}
|
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
|
<Button
|
||||||
leftSection={<Truck size={16} />}
|
leftSection={<Truck size={16} />}
|
||||||
onClick={() => openAssign(null)}
|
onClick={() => openAssign(null)}
|
||||||
|
|||||||
Reference in New Issue
Block a user