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:
@@ -52,6 +52,7 @@ import TrainScheduleV2DetailPage from "./pages/trainScheduling/TrainScheduleV2De
|
||||
import TrainScheduleTrackPage from "./pages/trainScheduling/TrainScheduleTrackPage";
|
||||
import TrainSchedulingGlobalRulesPage from "./pages/trainScheduling/TrainSchedulingGlobalRulesPage";
|
||||
import FleetResourcePage from "./pages/fleet/FleetResourcePage";
|
||||
import LastMilePage from "./pages/operations/LastMilePage";
|
||||
import { getCategorySidebarChildren } from "./pages/ruleEngine/config/resources";
|
||||
import { FREIGHT_PERMS, hasPermission as hasFreightPermission } from "./lib/permissions";
|
||||
import { RequirePermission } from "./components/auth/RequirePermission";
|
||||
@@ -113,6 +114,12 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
||||
icon: <LayoutGrid />,
|
||||
permission: FREIGHT_PERMS.trainScheduling.view,
|
||||
},
|
||||
{
|
||||
label: "Last Mile",
|
||||
href: "/dashboard/operations/last-mile",
|
||||
icon: <Truck />,
|
||||
permission: FREIGHT_PERMS.trainScheduling.view,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -408,6 +415,14 @@ const App = () => {
|
||||
</RequirePermission>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="operations/last-mile"
|
||||
element={
|
||||
<RequirePermission permission={FREIGHT_PERMS.trainScheduling.view}>
|
||||
<LastMilePage />
|
||||
</RequirePermission>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="operations/batch-board/:scheduleId"
|
||||
element={
|
||||
|
||||
@@ -50,6 +50,13 @@ const ROUTE_META: Array<{ prefix: string; meta: PageMeta }> = [
|
||||
subtitle: "View booking payment transactions",
|
||||
},
|
||||
},
|
||||
{
|
||||
prefix: "/dashboard/operations/last-mile",
|
||||
meta: {
|
||||
title: "Last Mile",
|
||||
subtitle: "Assign vehicles to final-leg deliveries",
|
||||
},
|
||||
},
|
||||
{
|
||||
prefix: "/dashboard/operations/train-scheduling-v2/",
|
||||
meta: {
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Truck } from "lucide-react";
|
||||
import type { ColumnDef } from "@edr/ui-common";
|
||||
import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common";
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Group,
|
||||
Modal,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
|
||||
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
type LastMileStatus = "UNASSIGNED" | "ASSIGNED";
|
||||
|
||||
interface LastMileJob {
|
||||
id: string;
|
||||
bookingRef: string;
|
||||
customer: string;
|
||||
destination: string;
|
||||
cargo: string;
|
||||
status: LastMileStatus;
|
||||
assignedVehicle: string | null;
|
||||
}
|
||||
|
||||
// Placeholder data — replace with a real last-mile service once the API exists.
|
||||
const PLACEHOLDER_JOBS: LastMileJob[] = [
|
||||
{
|
||||
id: "1",
|
||||
bookingRef: "BK-10241",
|
||||
customer: "Awash Trading PLC",
|
||||
destination: "Bole Sub-city, Addis Ababa",
|
||||
cargo: "20ft container · Electronics",
|
||||
status: "UNASSIGNED",
|
||||
assignedVehicle: null,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
bookingRef: "BK-10238",
|
||||
customer: "Dire Logistics",
|
||||
destination: "Industry Zone, Dire Dawa",
|
||||
cargo: "Bulk · 18t Cement",
|
||||
status: "ASSIGNED",
|
||||
assignedVehicle: "Isuzu FVR (3-AA-45821)",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
bookingRef: "BK-10233",
|
||||
customer: "Horizon Imports",
|
||||
destination: "Kality Terminal, Addis Ababa",
|
||||
cargo: "40ft container · Machinery",
|
||||
status: "UNASSIGNED",
|
||||
assignedVehicle: null,
|
||||
},
|
||||
];
|
||||
|
||||
// Placeholder vehicle options — replace with the vehicles service.
|
||||
const VEHICLE_OPTIONS = [
|
||||
{ value: "isuzu-fvr-45821", label: "Isuzu FVR (3-AA-45821)" },
|
||||
{ value: "sino-howo-12044", label: "Sinotruk Howo (3-AA-12044)" },
|
||||
{ value: "mercedes-actros-90113", label: "Mercedes Actros (3-AA-90113)" },
|
||||
];
|
||||
|
||||
const LastMilePage = () => {
|
||||
const { toast } = useToast();
|
||||
const { pagination, setPagination } = usePagination({ pageSize: 10 });
|
||||
|
||||
const [jobs, setJobs] = useState<LastMileJob[]>(PLACEHOLDER_JOBS);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const [assignOpen, setAssignOpen] = useState(false);
|
||||
const [activeJobId, setActiveJobId] = useState<string | null>(null);
|
||||
const [vehicleValue, setVehicleValue] = useState<string | null>(null);
|
||||
|
||||
const filteredJobs = useMemo(() => {
|
||||
const term = search.trim().toLowerCase();
|
||||
if (!term) return jobs;
|
||||
return jobs.filter((job) =>
|
||||
[job.bookingRef, job.customer, job.destination, job.cargo]
|
||||
.join(" ")
|
||||
.toLowerCase()
|
||||
.includes(term),
|
||||
);
|
||||
}, [jobs, search]);
|
||||
|
||||
const pageCount = Math.max(1, Math.ceil(filteredJobs.length / pagination.pageSize));
|
||||
const pagedJobs = useMemo(() => {
|
||||
const start = pagination.pageIndex * pagination.pageSize;
|
||||
return filteredJobs.slice(start, start + pagination.pageSize);
|
||||
}, [filteredJobs, pagination.pageIndex, pagination.pageSize]);
|
||||
|
||||
const openAssign = (jobId: string | null) => {
|
||||
setActiveJobId(jobId);
|
||||
setVehicleValue(null);
|
||||
setAssignOpen(true);
|
||||
};
|
||||
|
||||
const closeAssign = () => {
|
||||
setAssignOpen(false);
|
||||
setActiveJobId(null);
|
||||
setVehicleValue(null);
|
||||
};
|
||||
|
||||
const handleAssign = () => {
|
||||
const jobId = activeJobId ?? filteredJobs.find((job) => job.status === "UNASSIGNED")?.id;
|
||||
if (!jobId || !vehicleValue) {
|
||||
toast({
|
||||
title: "Select a vehicle",
|
||||
description: "Choose a vehicle to assign to this delivery.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const vehicleLabel =
|
||||
VEHICLE_OPTIONS.find((option) => option.value === vehicleValue)?.label ?? vehicleValue;
|
||||
|
||||
setJobs((current) =>
|
||||
current.map((job) =>
|
||||
job.id === jobId
|
||||
? { ...job, status: "ASSIGNED", assignedVehicle: vehicleLabel }
|
||||
: job,
|
||||
),
|
||||
);
|
||||
|
||||
toast({ title: "Vehicle assigned", description: vehicleLabel });
|
||||
closeAssign();
|
||||
};
|
||||
|
||||
const columns = useMemo((): ColumnDef<LastMileJob>[] => {
|
||||
const headerClassName = ruleEngineTable.headerCell;
|
||||
const cellClassName = ruleEngineTable.bodyCell;
|
||||
return [
|
||||
{
|
||||
id: "bookingRef",
|
||||
header: "Booking",
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) => (
|
||||
<Text size="sm" fw={600}>
|
||||
{row.original.bookingRef}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "customer",
|
||||
header: "Customer",
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) => row.original.customer,
|
||||
},
|
||||
{
|
||||
id: "destination",
|
||||
header: "Destination",
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) => row.original.destination,
|
||||
},
|
||||
{
|
||||
id: "cargo",
|
||||
header: "Cargo",
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) => row.original.cargo,
|
||||
},
|
||||
{
|
||||
id: "vehicle",
|
||||
header: "Vehicle",
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) =>
|
||||
row.original.assignedVehicle ?? <Text c="dimmed">—</Text>,
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
header: "Status",
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) => (
|
||||
<Badge
|
||||
color={row.original.status === "ASSIGNED" ? "green" : "orange"}
|
||||
variant="light"
|
||||
size="sm"
|
||||
>
|
||||
{row.original.status === "ASSIGNED" ? "Assigned" : "Unassigned"}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
meta: { headerClassName, cellClassName: `${cellClassName} whitespace-nowrap` },
|
||||
cell: ({ row }) => (
|
||||
<Group gap={4} justify="flex-end" wrap="nowrap">
|
||||
<Button
|
||||
variant="light"
|
||||
size="compact-sm"
|
||||
leftSection={<Truck size={14} />}
|
||||
onClick={() => openAssign(row.original.id)}
|
||||
>
|
||||
{row.original.status === "ASSIGNED" ? "Reassign" : "Assign"}
|
||||
</Button>
|
||||
</Group>
|
||||
),
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
|
||||
const tableStatus = "success" as const;
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<Card
|
||||
radius="lg"
|
||||
padding={0}
|
||||
withBorder
|
||||
style={{ borderColor: "var(--mantine-color-gray-2)" }}
|
||||
>
|
||||
<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}
|
||||
/>
|
||||
<Button
|
||||
leftSection={<Truck size={16} />}
|
||||
onClick={() => openAssign(null)}
|
||||
>
|
||||
Assign Mile
|
||||
</Button>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={pagedJobs}
|
||||
status={tableStatus}
|
||||
emptyMessage="No last-mile deliveries found"
|
||||
pagination={{
|
||||
pageIndex: pagination.pageIndex,
|
||||
pageSize: pagination.pageSize,
|
||||
pageCount,
|
||||
totalCount: filteredJobs.length,
|
||||
}}
|
||||
tableOptions={{
|
||||
manualPagination: true,
|
||||
pageCount,
|
||||
state: { pagination },
|
||||
onPaginationChange: setPagination,
|
||||
}}
|
||||
containerClassName="border-0 shadow-none bg-transparent"
|
||||
footer={({ table, pagination: footerPagination }) => (
|
||||
<DataTableFooter
|
||||
table={table}
|
||||
pagination={footerPagination}
|
||||
options={{ labels: { items: "deliveries" } }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
opened={assignOpen}
|
||||
onClose={closeAssign}
|
||||
title={<Text fw={600}>Assign Vehicle</Text>}
|
||||
radius="lg"
|
||||
centered
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Select
|
||||
label="Vehicle"
|
||||
placeholder="Select a vehicle"
|
||||
data={VEHICLE_OPTIONS}
|
||||
value={vehicleValue}
|
||||
onChange={setVehicleValue}
|
||||
searchable
|
||||
/>
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button variant="default" onClick={closeAssign}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleAssign}>Assign</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default LastMilePage;
|
||||
Reference in New Issue
Block a user