last mile

This commit is contained in:
natib21
2026-06-20 12:46:02 +00:00
parent 28dd7f9c72
commit 4e926c0d39

View File

@@ -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>