This commit is contained in:
natib21
2026-06-20 14:00:34 +00:00
parent cb93576500
commit b28cf6ae91
3 changed files with 844 additions and 0 deletions

View File

@@ -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 FirstMilePage from "./pages/operations/FirstMilePage";
import LastMilePage from "./pages/operations/LastMilePage";
import { getCategorySidebarChildren } from "./pages/ruleEngine/config/resources";
import { FREIGHT_PERMS, hasPermission as hasFreightPermission } from "./lib/permissions";
@@ -114,6 +115,12 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
icon: <LayoutGrid />,
permission: FREIGHT_PERMS.trainScheduling.view,
},
{
label: "First Mile",
href: "/dashboard/operations/first-mile",
icon: <Truck />,
permission: FREIGHT_PERMS.trainScheduling.view,
},
{
label: "Last Mile",
href: "/dashboard/operations/last-mile",
@@ -415,6 +422,14 @@ const App = () => {
</RequirePermission>
}
/>
<Route
path="operations/first-mile"
element={
<RequirePermission permission={FREIGHT_PERMS.trainScheduling.view}>
<FirstMilePage />
</RequirePermission>
}
/>
<Route
path="operations/last-mile"
element={

View File

@@ -50,6 +50,13 @@ const ROUTE_META: Array<{ prefix: string; meta: PageMeta }> = [
subtitle: "View booking payment transactions",
},
},
{
prefix: "/dashboard/operations/first-mile",
meta: {
title: "First Mile",
subtitle: "Assign vehicles to initial-leg pickups",
},
},
{
prefix: "/dashboard/operations/last-mile",
meta: {

View File

@@ -0,0 +1,822 @@
import { type ReactNode, useMemo, useState } from "react";
import {
ArrowRight,
Eye,
MoreHorizontal,
Printer,
RefreshCw,
Truck,
} from "lucide-react";
import type { ColumnDef } from "@edr/ui-common";
import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common";
import {
ActionIcon,
Badge,
Box,
Button,
Card,
Divider,
Group,
Menu,
Modal,
Select,
SimpleGrid,
Stack,
Text,
TextInput,
} from "@mantine/core";
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
import { useToast } from "@/hooks/use-toast";
type FirstMileStatus = "UNASSIGNED" | "ASSIGNED";
type PickupStatus = "PAYMENT_PENDING" | "READY_FOR_PICKUP" | "PICKED_UP";
interface FirstMileJob {
id: string;
bookingRef: string;
customer: string;
pickup: string;
cargo: string;
status: FirstMileStatus;
pickupStatus: PickupStatus;
assignedVehicle: string | null;
// Booking info shown in the Assign / View Detail modals.
serviceType: string;
weight: string;
price: number;
destinationYard: string;
contactName: string;
contactPhone: string;
requestedDate: string;
}
const formatPrice = (amount: number) =>
`ETB ${amount.toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}`;
const PICKUP_STATUS_META: Record<
PickupStatus,
{ label: string; color: string }
> = {
PAYMENT_PENDING: { label: "Payment Pending", color: "yellow" },
READY_FOR_PICKUP: { label: "Ready for Pickup", color: "blue" },
PICKED_UP: { label: "Picked Up", color: "green" },
};
// Forward-only lifecycle: Payment Pending → Ready for Pickup → Picked Up.
const NEXT_PICKUP_STATUS: Partial<Record<PickupStatus, PickupStatus>> = {
PAYMENT_PENDING: "READY_FOR_PICKUP",
READY_FOR_PICKUP: "PICKED_UP",
};
// Single filter covering both the pickup lifecycle and assignment state.
type StatusFilter =
| "ALL"
| PickupStatus
| FirstMileStatus;
const FILTER_OPTIONS: { value: StatusFilter; label: string }[] = [
{ value: "ALL", label: "All statuses" },
{ value: "PAYMENT_PENDING", label: "Payment Pending" },
{ value: "READY_FOR_PICKUP", label: "Ready for Pickup" },
{ value: "PICKED_UP", label: "Picked Up" },
{ value: "ASSIGNED", label: "Assigned" },
{ value: "UNASSIGNED", label: "Unassigned" },
];
// Placeholder data — replace with a real first-mile service once the API exists.
const PLACEHOLDER_JOBS: FirstMileJob[] = [
{
id: "1",
bookingRef: "BK-10242",
customer: "Awash Trading PLC",
pickup: "Kera Warehouse, Addis Ababa",
cargo: "20ft container · Electronics",
status: "UNASSIGNED",
pickupStatus: "PAYMENT_PENDING",
assignedVehicle: null,
serviceType: "Door-to-terminal (First Mile)",
weight: "12.4 t",
price: 4200,
destinationYard: "Indode Dry Port",
contactName: "Selam Bekele",
contactPhone: "+251 911 234 567",
requestedDate: "2026-06-22",
},
{
id: "2",
bookingRef: "BK-10239",
customer: "Dire Logistics",
pickup: "Factory Gate 4, Dire Dawa",
cargo: "Bulk · 18t Cement",
status: "ASSIGNED",
pickupStatus: "READY_FOR_PICKUP",
assignedVehicle: "Isuzu FVR (3-AA-45821)",
serviceType: "Door-to-terminal (First Mile)",
weight: "18.0 t",
price: 3000,
destinationYard: "Dire Dawa Terminal",
contactName: "Yonas Tadesse",
contactPhone: "+251 912 887 010",
requestedDate: "2026-06-21",
},
{
id: "3",
bookingRef: "BK-10235",
customer: "Horizon Imports",
pickup: "Lebu Industrial Park, Addis Ababa",
cargo: "40ft container · Machinery",
status: "UNASSIGNED",
pickupStatus: "PICKED_UP",
assignedVehicle: null,
serviceType: "Door-to-terminal (First Mile)",
weight: "24.7 t",
price: 6500,
destinationYard: "Mojo Dry Port",
contactName: "Hanna Girma",
contactPhone: "+251 913 445 221",
requestedDate: "2026-06-23",
},
];
// 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 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: FirstMileJob }) => (
<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>
<Group gap="xs">
<Badge
color={PICKUP_STATUS_META[job.pickupStatus].color}
variant="light"
size="sm"
>
{PICKUP_STATUS_META[job.pickupStatus].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} />
<InfoRow label="Service type" value={job.serviceType} />
<InfoRow label="Pickup location" value={job.pickup} />
<InfoRow label="Destination yard" value={job.destinationYard} />
<InfoRow label="Cargo" value={job.cargo} />
<InfoRow label="Weight" value={job.weight} />
<InfoRow label="Price" value={formatPrice(job.price)} />
<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 tripSlipRows = (job: FirstMileJob): [string, string][] => [
["Customer", job.customer],
["Service", job.serviceType],
["Pickup location", job.pickup],
["Destination yard", job.destinationYard],
["Cargo", job.cargo],
["Weight", job.weight],
["Price", formatPrice(job.price)],
["Vehicle", job.assignedVehicle ?? "Unassigned"],
["Contact", `${job.contactName} · ${job.contactPhone}`],
["Requested date", job.requestedDate],
["Pickup status", PICKUP_STATUS_META[job.pickupStatus].label],
];
const SampleStamp = () => (
<Box style={{ height: 96, display: "flex", alignItems: "center" }}>
<Box
style={{
width: 96,
height: 96,
borderRadius: "50%",
border: "2px solid var(--mantine-color-teal-7)",
transform: "rotate(-12deg)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Box
style={{
width: 82,
height: 82,
borderRadius: "50%",
border: "1px solid var(--mantine-color-teal-7)",
color: "var(--mantine-color-teal-7)",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
textAlign: "center",
lineHeight: 1.1,
}}
>
<Text size="9px" fw={700} style={{ letterSpacing: 1 }}>
EDR FREIGHT
</Text>
<Text size="sm" fw={800}>
APPROVED
</Text>
<Text size="8px" fw={600}>
OPERATIONS
</Text>
</Box>
</Box>
</Box>
);
const SignatureBlock = ({ title, stamp }: { title: string; stamp?: ReactNode }) => (
<Stack gap="sm" style={{ flex: 1, position: "relative", minHeight: stamp ? 130 : undefined }}>
<Text fw={600} size="sm">
{title}
</Text>
<Group gap="xs" align="flex-end">
<Text size="sm" c="dimmed">
Name:
</Text>
<Box style={{ flex: 1, borderBottom: "1px solid var(--mantine-color-gray-5)", height: 18 }} />
</Group>
<Group gap="xs" align="flex-end">
<Text size="sm" c="dimmed">
Signature:
</Text>
<Box style={{ flex: 1, borderBottom: "1px solid var(--mantine-color-gray-5)", height: 18 }} />
</Group>
{stamp && (
<Box
style={{
position: "absolute",
right: 4,
top: 22,
opacity: 0.85,
pointerEvents: "none",
}}
>
{stamp}
</Box>
)}
</Stack>
);
const TripSlipDocument = ({ job }: { job: FirstMileJob }) => (
<Stack gap="md">
<Stack gap={2} align="center">
<Text fw={700}>EDR Freight</Text>
<Text size="sm" c="dimmed" tt="uppercase" fw={600}>
First Mile Trip Slip
</Text>
</Stack>
<Group justify="space-between">
<Text size="sm" fw={600}>
{job.bookingRef}
</Text>
<Text size="sm" c="dimmed">
{job.requestedDate}
</Text>
</Group>
<Divider />
<SimpleGrid cols={2} spacing="xs">
{tripSlipRows(job).map(([label, value]) => (
<InfoRow key={label} label={label} value={value} />
))}
</SimpleGrid>
<Divider label="Acknowledgement" labelPosition="center" />
<Group align="flex-start" gap="xl" wrap="nowrap">
<SignatureBlock title="Driver" />
<SignatureBlock title="Operator" stamp={<SampleStamp />} />
</Group>
</Stack>
);
const escapeHtml = (value: string) =>
value
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
const buildTripSlipHtml = (job: FirstMileJob) => {
const rows = tripSlipRows(job)
.map(
([label, value]) =>
`<tr><td class="lbl">${escapeHtml(label)}</td><td>${escapeHtml(value)}</td></tr>`,
)
.join("");
const signature = (title: string, withStamp: boolean) => `
<div class="sign-col">
<div class="sign-title">${title}</div>
<div class="sign-field"><span>Name:</span><span class="line"></span></div>
<div class="sign-field"><span>Signature:</span><span class="line"></span></div>
${
withStamp
? '<div class="stamp"><div class="ring"><div class="ring-inner"><span>EDR FREIGHT</span><strong>APPROVED</strong><span>OPERATIONS</span></div></div></div>'
: ""
}
</div>`;
return `<!doctype html><html><head><meta charset="utf-8" />
<title>Trip Slip ${escapeHtml(job.bookingRef)}</title>
<style>
* { box-sizing: border-box; }
body { font-family: Arial, Helvetica, sans-serif; color: #111; margin: 32px; }
.head { text-align: center; margin-bottom: 16px; }
.head h1 { font-size: 18px; margin: 0; }
.head p { font-size: 12px; letter-spacing: 1px; text-transform: uppercase; color: #555; margin: 2px 0 0; }
.meta { display: flex; justify-content: space-between; font-size: 13px; font-weight: 600; margin: 8px 0; }
table { width: 100%; border-collapse: collapse; font-size: 13px; margin: 8px 0 20px; }
td { padding: 5px 6px; border-bottom: 1px solid #eee; vertical-align: top; }
td.lbl { color: #666; text-transform: uppercase; font-size: 11px; font-weight: 700; width: 40%; }
.ack { text-align: center; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: #777; margin: 16px 0 8px; }
.signs { display: flex; gap: 32px; }
.sign-col { flex: 1; position: relative; min-height: 130px; }
.sign-title { font-weight: 700; font-size: 13px; margin-bottom: 12px; }
.sign-field { display: flex; gap: 6px; align-items: flex-end; font-size: 12px; color: #666; margin-bottom: 10px; }
.sign-field .line { flex: 1; border-bottom: 1px solid #888; height: 16px; }
.stamp { position: absolute; right: 4px; top: 22px; opacity: 0.85; }
.ring { width: 96px; height: 96px; border-radius: 50%; border: 2px solid #0c7a57; transform: rotate(-12deg); display: flex; align-items: center; justify-content: center; }
.ring-inner { width: 82px; height: 82px; border-radius: 50%; border: 1px solid #0c7a57; color: #0c7a57; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; line-height: 1.1; }
.ring-inner span { font-size: 9px; font-weight: 700; letter-spacing: 1px; }
.ring-inner strong { font-size: 13px; font-weight: 800; }
</style></head>
<body onload="window.print()">
<div class="head"><h1>EDR Freight</h1><p>First Mile Trip Slip</p></div>
<div class="meta"><span>${escapeHtml(job.bookingRef)}</span><span>${escapeHtml(job.requestedDate)}</span></div>
<table>${rows}</table>
<div class="ack">Acknowledgement</div>
<div class="signs">${signature("Driver", false)}${signature("Operator", true)}</div>
</body></html>`;
};
const FirstMilePage = () => {
const { toast } = useToast();
const { pagination, setPagination } = usePagination({ pageSize: 10 });
const [jobs, setJobs] = useState<FirstMileJob[]>(PLACEHOLDER_JOBS);
const [search, setSearch] = useState("");
const [statusFilter, setStatusFilter] = useState<StatusFilter>("ALL");
const [assignOpen, setAssignOpen] = useState(false);
const [detailOpen, setDetailOpen] = useState(false);
const [tripSlipOpen, setTripSlipOpen] = useState(false);
const [tripSlipJob, setTripSlipJob] = useState<FirstMileJob | null>(null);
const [activeJobId, setActiveJobId] = useState<string | null>(null);
const [vehicleValue, setVehicleValue] = useState<string | null>(null);
const activeJob = useMemo(
() => jobs.find((job) => job.id === activeJobId) ?? null,
[jobs, activeJobId],
);
const matchesStatusFilter = (job: FirstMileJob) => {
switch (statusFilter) {
case "ALL":
return true;
case "ASSIGNED":
case "UNASSIGNED":
return job.status === statusFilter;
default:
return job.pickupStatus === statusFilter;
}
};
const statusCounts = useMemo(() => {
const counts: Record<StatusFilter, number> = {
ALL: jobs.length,
PAYMENT_PENDING: 0,
READY_FOR_PICKUP: 0,
PICKED_UP: 0,
ASSIGNED: 0,
UNASSIGNED: 0,
};
for (const job of jobs) {
counts[job.pickupStatus] += 1;
counts[job.status] += 1;
}
return counts;
}, [jobs]);
const filteredJobs = useMemo(() => {
const term = search.trim().toLowerCase();
return jobs.filter((job) => {
if (!matchesStatusFilter(job)) return false;
if (!term) return true;
return [job.bookingRef, job.customer, job.pickup, job.cargo]
.join(" ")
.toLowerCase()
.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(() => {
const start = pagination.pageIndex * pagination.pageSize;
return filteredJobs.slice(start, start + pagination.pageSize);
}, [filteredJobs, pagination.pageIndex, pagination.pageSize]);
const openAssign = (jobId: string | null) => {
const resolved = jobId ?? filteredJobs.find((job) => job.status === "UNASSIGNED")?.id ?? null;
setActiveJobId(resolved);
setVehicleValue(null);
setAssignOpen(true);
};
const openDetail = (jobId: string) => {
setActiveJobId(jobId);
setDetailOpen(true);
};
const closeAssign = () => {
setAssignOpen(false);
setActiveJobId(null);
setVehicleValue(null);
};
const closeDetail = () => {
setDetailOpen(false);
setActiveJobId(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 pickup.",
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 handleAdvanceStatus = (job: FirstMileJob) => {
const next = NEXT_PICKUP_STATUS[job.pickupStatus];
if (!next) return;
setJobs((current) =>
current.map((item) =>
item.id === job.id ? { ...item, pickupStatus: next } : item,
),
);
toast({
title: "Status updated",
description: `${job.bookingRef}${PICKUP_STATUS_META[next].label}`,
});
};
const handlePrintTripSlip = (job: FirstMileJob) => {
setTripSlipJob(job);
setTripSlipOpen(true);
};
const printTripSlip = () => {
if (!tripSlipJob) return;
const win = window.open("", "_blank", "width=820,height=920");
if (!win) {
toast({
title: "Pop-up blocked",
description: "Allow pop-ups to print the trip slip.",
variant: "destructive",
});
return;
}
win.document.write(buildTripSlipHtml(tripSlipJob));
win.document.close();
};
const columns = useMemo((): ColumnDef<FirstMileJob>[] => {
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: "pickup",
header: "Pickup",
meta: { headerClassName, cellClassName },
cell: ({ row }) => row.original.pickup,
},
{
id: "cargo",
header: "Cargo",
meta: { headerClassName, cellClassName },
cell: ({ row }) => row.original.cargo,
},
{
id: "price",
header: "Price",
meta: { headerClassName, cellClassName },
cell: ({ row }) => formatPrice(row.original.price),
},
{
id: "vehicle",
header: "Vehicle",
meta: { headerClassName, cellClassName },
cell: ({ row }) =>
row.original.assignedVehicle ?? <Text c="dimmed"></Text>,
},
{
id: "pickupStatus",
header: "Status",
meta: { headerClassName, cellClassName },
cell: ({ row }) => {
const meta = PICKUP_STATUS_META[row.original.pickupStatus];
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"}
variant="light"
size="sm"
>
{row.original.status === "ASSIGNED" ? "Assigned" : "Unassigned"}
</Badge>
),
},
{
id: "actions",
header: "Actions",
meta: { headerClassName, cellClassName: `${cellClassName} whitespace-nowrap` },
cell: ({ row }) => {
const isAssigned = row.original.status === "ASSIGNED";
const nextStatus = NEXT_PICKUP_STATUS[row.original.pickupStatus];
const canPrintTripSlip =
row.original.pickupStatus === "READY_FOR_PICKUP" ||
row.original.pickupStatus === "PICKED_UP";
return (
<Group gap={4} justify="flex-end" wrap="nowrap">
<Menu position="bottom-end" width={200} withinPortal>
<Menu.Target>
<ActionIcon variant="subtle" color="gray" aria-label="Pickup actions">
<MoreHorizontal size={16} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
leftSection={<ArrowRight size={15} />}
disabled={!nextStatus}
onClick={() => handleAdvanceStatus(row.original)}
>
{nextStatus
? `Mark ${PICKUP_STATUS_META[nextStatus].label}`
: "Picked Up"}
</Menu.Item>
<Menu.Divider />
<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>
{canPrintTripSlip && (
<Menu.Item
leftSection={<Printer size={15} />}
onClick={() => handlePrintTripSlip(row.original)}
>
Print trip slip
</Menu.Item>
)}
</Menu.Dropdown>
</Menu>
</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%">
<Stack gap="sm">
<Group justify="space-between" wrap="wrap">
<TextInput
placeholder="Search pickups…"
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
w={260}
/>
<Button
leftSection={<Truck size={16} />}
onClick={() => openAssign(null)}
>
Assign Mile
</Button>
</Group>
<Group gap="xs" wrap="wrap">
{FILTER_OPTIONS.map((option) => {
const active = statusFilter === option.value;
return (
<Button
key={option.value}
size="xs"
variant={active ? "filled" : "default"}
onClick={() => {
setStatusFilter(option.value);
setPagination((p) => ({ ...p, pageIndex: 0 }));
}}
>
{option.label} ({statusCounts[option.value]})
</Button>
);
})}
</Group>
</Stack>
</Box>
<DataTable
columns={columns}
data={pagedJobs}
status={tableStatus}
emptyMessage="No first-mile pickups 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: "pickups" } }}
/>
)}
/>
</Stack>
</Card>
<Modal
opened={assignOpen}
onClose={closeAssign}
title={<Text fw={600}>Assign Vehicle</Text>}
size="lg"
radius="lg"
centered
>
<Stack gap="md">
{activeJob ? (
<BookingInfo job={activeJob} />
) : (
<Text size="sm" c="dimmed">
No unassigned pickups available.
</Text>
)}
<Divider />
<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} disabled={!activeJob}>
{activeJob?.status === "ASSIGNED" ? "Reassign" : "Assign"}
</Button>
</Group>
</Stack>
</Modal>
<Modal
opened={detailOpen}
onClose={closeDetail}
title={<Text fw={600}>Pickup 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>
</Stack>
</Modal>
<Modal
opened={tripSlipOpen}
onClose={() => setTripSlipOpen(false)}
title={<Text fw={600}>Trip Slip</Text>}
size="lg"
radius="lg"
centered
>
<Stack gap="md">
{tripSlipJob && <TripSlipDocument job={tripSlipJob} />}
<Divider />
<Group justify="flex-end" gap="sm">
<Button variant="default" onClick={() => setTripSlipOpen(false)}>
Close
</Button>
<Button leftSection={<Printer size={16} />} onClick={printTripSlip}>
Print
</Button>
</Group>
</Stack>
</Modal>
</Stack>
);
};
export default FirstMilePage;