mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 05:18:11 +00:00
last mile
This commit is contained in:
@@ -1,15 +1,19 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Truck } from "lucide-react";
|
import { Eye, 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 {
|
||||||
|
ActionIcon,
|
||||||
Badge,
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
Divider,
|
||||||
Group,
|
Group,
|
||||||
|
Menu,
|
||||||
Modal,
|
Modal,
|
||||||
Select,
|
Select,
|
||||||
|
SimpleGrid,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
@@ -28,6 +32,13 @@ interface LastMileJob {
|
|||||||
cargo: string;
|
cargo: string;
|
||||||
status: LastMileStatus;
|
status: LastMileStatus;
|
||||||
assignedVehicle: string | null;
|
assignedVehicle: string | null;
|
||||||
|
// Booking info shown in the Assign / View Detail modals.
|
||||||
|
serviceType: string;
|
||||||
|
weight: string;
|
||||||
|
originYard: string;
|
||||||
|
contactName: string;
|
||||||
|
contactPhone: string;
|
||||||
|
requestedDate: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -40,6 +51,12 @@ const PLACEHOLDER_JOBS: LastMileJob[] = [
|
|||||||
cargo: "20ft container · Electronics",
|
cargo: "20ft container · Electronics",
|
||||||
status: "UNASSIGNED",
|
status: "UNASSIGNED",
|
||||||
assignedVehicle: null,
|
assignedVehicle: null,
|
||||||
|
serviceType: "Door-to-door (Last Mile)",
|
||||||
|
weight: "12.4 t",
|
||||||
|
originYard: "Indode Dry Port",
|
||||||
|
contactName: "Selam Bekele",
|
||||||
|
contactPhone: "+251 911 234 567",
|
||||||
|
requestedDate: "2026-06-22",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "2",
|
id: "2",
|
||||||
@@ -49,6 +66,12 @@ const PLACEHOLDER_JOBS: LastMileJob[] = [
|
|||||||
cargo: "Bulk · 18t Cement",
|
cargo: "Bulk · 18t Cement",
|
||||||
status: "ASSIGNED",
|
status: "ASSIGNED",
|
||||||
assignedVehicle: "Isuzu FVR (3-AA-45821)",
|
assignedVehicle: "Isuzu FVR (3-AA-45821)",
|
||||||
|
serviceType: "Terminal-to-door (Last Mile)",
|
||||||
|
weight: "18.0 t",
|
||||||
|
originYard: "Dire Dawa Terminal",
|
||||||
|
contactName: "Yonas Tadesse",
|
||||||
|
contactPhone: "+251 912 887 010",
|
||||||
|
requestedDate: "2026-06-21",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "3",
|
id: "3",
|
||||||
@@ -58,6 +81,12 @@ const PLACEHOLDER_JOBS: LastMileJob[] = [
|
|||||||
cargo: "40ft container · Machinery",
|
cargo: "40ft container · Machinery",
|
||||||
status: "UNASSIGNED",
|
status: "UNASSIGNED",
|
||||||
assignedVehicle: null,
|
assignedVehicle: null,
|
||||||
|
serviceType: "Door-to-door (Last Mile)",
|
||||||
|
weight: "24.7 t",
|
||||||
|
originYard: "Mojo Dry Port",
|
||||||
|
contactName: "Hanna Girma",
|
||||||
|
contactPhone: "+251 913 445 221",
|
||||||
|
requestedDate: "2026-06-23",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -68,6 +97,44 @@ const VEHICLE_OPTIONS = [
|
|||||||
{ value: "mercedes-actros-90113", label: "Mercedes Actros (3-AA-90113)" },
|
{ value: "mercedes-actros-90113", label: "Mercedes Actros (3-AA-90113)" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const InfoRow = ({ label, value }: { label: string; value: string }) => (
|
||||||
|
<Stack gap={2}>
|
||||||
|
<Text size="xs" c="dimmed" tt="uppercase" fw={600}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm">{value}</Text>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
|
const BookingInfo = ({ job }: { job: LastMileJob }) => (
|
||||||
|
<Card radius="md" padding="md" withBorder bg="var(--mantine-color-gray-0)">
|
||||||
|
<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>
|
||||||
|
<SimpleGrid cols={2} spacing="sm">
|
||||||
|
<InfoRow label="Customer" value={job.customer} />
|
||||||
|
<InfoRow label="Service type" value={job.serviceType} />
|
||||||
|
<InfoRow label="Origin yard" value={job.originYard} />
|
||||||
|
<InfoRow label="Destination" value={job.destination} />
|
||||||
|
<InfoRow label="Cargo" value={job.cargo} />
|
||||||
|
<InfoRow label="Weight" value={job.weight} />
|
||||||
|
<InfoRow label="Contact" value={job.contactName} />
|
||||||
|
<InfoRow label="Phone" value={job.contactPhone} />
|
||||||
|
<InfoRow label="Requested date" value={job.requestedDate} />
|
||||||
|
<InfoRow label="Assigned vehicle" value={job.assignedVehicle ?? "—"} />
|
||||||
|
</SimpleGrid>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
const LastMilePage = () => {
|
const LastMilePage = () => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { pagination, setPagination } = usePagination({ pageSize: 10 });
|
const { pagination, setPagination } = usePagination({ pageSize: 10 });
|
||||||
@@ -76,9 +143,15 @@ const LastMilePage = () => {
|
|||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
const [assignOpen, setAssignOpen] = useState(false);
|
const [assignOpen, setAssignOpen] = useState(false);
|
||||||
|
const [detailOpen, setDetailOpen] = useState(false);
|
||||||
const [activeJobId, setActiveJobId] = useState<string | null>(null);
|
const [activeJobId, setActiveJobId] = useState<string | null>(null);
|
||||||
const [vehicleValue, setVehicleValue] = useState<string | null>(null);
|
const [vehicleValue, setVehicleValue] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const activeJob = useMemo(
|
||||||
|
() => jobs.find((job) => job.id === activeJobId) ?? null,
|
||||||
|
[jobs, activeJobId],
|
||||||
|
);
|
||||||
|
|
||||||
const filteredJobs = useMemo(() => {
|
const filteredJobs = useMemo(() => {
|
||||||
const term = search.trim().toLowerCase();
|
const term = search.trim().toLowerCase();
|
||||||
if (!term) return jobs;
|
if (!term) return jobs;
|
||||||
@@ -97,17 +170,28 @@ const LastMilePage = () => {
|
|||||||
}, [filteredJobs, pagination.pageIndex, pagination.pageSize]);
|
}, [filteredJobs, pagination.pageIndex, pagination.pageSize]);
|
||||||
|
|
||||||
const openAssign = (jobId: string | null) => {
|
const openAssign = (jobId: string | null) => {
|
||||||
setActiveJobId(jobId);
|
const resolved = jobId ?? filteredJobs.find((job) => job.status === "UNASSIGNED")?.id ?? null;
|
||||||
|
setActiveJobId(resolved);
|
||||||
setVehicleValue(null);
|
setVehicleValue(null);
|
||||||
setAssignOpen(true);
|
setAssignOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openDetail = (jobId: string) => {
|
||||||
|
setActiveJobId(jobId);
|
||||||
|
setDetailOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
const closeAssign = () => {
|
const closeAssign = () => {
|
||||||
setAssignOpen(false);
|
setAssignOpen(false);
|
||||||
setActiveJobId(null);
|
setActiveJobId(null);
|
||||||
setVehicleValue(null);
|
setVehicleValue(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeDetail = () => {
|
||||||
|
setDetailOpen(false);
|
||||||
|
setActiveJobId(null);
|
||||||
|
};
|
||||||
|
|
||||||
const handleAssign = () => {
|
const handleAssign = () => {
|
||||||
const jobId = activeJobId ?? filteredJobs.find((job) => job.status === "UNASSIGNED")?.id;
|
const jobId = activeJobId ?? filteredJobs.find((job) => job.status === "UNASSIGNED")?.id;
|
||||||
if (!jobId || !vehicleValue) {
|
if (!jobId || !vehicleValue) {
|
||||||
@@ -191,18 +275,43 @@ const LastMilePage = () => {
|
|||||||
id: "actions",
|
id: "actions",
|
||||||
header: "Actions",
|
header: "Actions",
|
||||||
meta: { headerClassName, cellClassName: `${cellClassName} whitespace-nowrap` },
|
meta: { headerClassName, cellClassName: `${cellClassName} whitespace-nowrap` },
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => {
|
||||||
<Group gap={4} justify="flex-end" wrap="nowrap">
|
const isAssigned = row.original.status === "ASSIGNED";
|
||||||
<Button
|
return (
|
||||||
variant="light"
|
<Group gap={4} justify="flex-end" wrap="nowrap">
|
||||||
size="compact-sm"
|
<Menu position="bottom-end" width={190} withinPortal>
|
||||||
leftSection={<Truck size={14} />}
|
<Menu.Target>
|
||||||
onClick={() => openAssign(row.original.id)}
|
<ActionIcon variant="subtle" color="gray" aria-label="Delivery actions">
|
||||||
>
|
<MoreHorizontal size={16} />
|
||||||
{row.original.status === "ASSIGNED" ? "Reassign" : "Assign"}
|
</ActionIcon>
|
||||||
</Button>
|
</Menu.Target>
|
||||||
</Group>
|
<Menu.Dropdown>
|
||||||
),
|
<Menu.Item
|
||||||
|
leftSection={<Truck size={15} />}
|
||||||
|
disabled={isAssigned}
|
||||||
|
onClick={() => openAssign(row.original.id)}
|
||||||
|
>
|
||||||
|
Assign
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<RefreshCw size={15} />}
|
||||||
|
disabled={!isAssigned}
|
||||||
|
onClick={() => openAssign(row.original.id)}
|
||||||
|
>
|
||||||
|
Reassign
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Divider />
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<Eye size={15} />}
|
||||||
|
onClick={() => openDetail(row.original.id)}
|
||||||
|
>
|
||||||
|
View detail
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, []);
|
}, []);
|
||||||
@@ -268,10 +377,19 @@ const LastMilePage = () => {
|
|||||||
opened={assignOpen}
|
opened={assignOpen}
|
||||||
onClose={closeAssign}
|
onClose={closeAssign}
|
||||||
title={<Text fw={600}>Assign Vehicle</Text>}
|
title={<Text fw={600}>Assign Vehicle</Text>}
|
||||||
|
size="lg"
|
||||||
radius="lg"
|
radius="lg"
|
||||||
centered
|
centered
|
||||||
>
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
|
{activeJob ? (
|
||||||
|
<BookingInfo job={activeJob} />
|
||||||
|
) : (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
No unassigned deliveries available.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Divider />
|
||||||
<Select
|
<Select
|
||||||
label="Vehicle"
|
label="Vehicle"
|
||||||
placeholder="Select a vehicle"
|
placeholder="Select a vehicle"
|
||||||
@@ -284,7 +402,27 @@ const LastMilePage = () => {
|
|||||||
<Button variant="default" onClick={closeAssign}>
|
<Button variant="default" onClick={closeAssign}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleAssign}>Assign</Button>
|
<Button onClick={handleAssign} disabled={!activeJob}>
|
||||||
|
{activeJob?.status === "ASSIGNED" ? "Reassign" : "Assign"}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
opened={detailOpen}
|
||||||
|
onClose={closeDetail}
|
||||||
|
title={<Text fw={600}>Delivery Detail</Text>}
|
||||||
|
size="lg"
|
||||||
|
radius="lg"
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
{activeJob && <BookingInfo job={activeJob} />}
|
||||||
|
<Group justify="flex-end">
|
||||||
|
<Button variant="default" onClick={closeDetail}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
Reference in New Issue
Block a user