diff --git a/apps/edr-freight-web/backoffice/src/components/operations/LastMileSteps.tsx b/apps/edr-freight-web/backoffice/src/components/operations/LastMileSteps.tsx
new file mode 100644
index 000000000..d4860a414
--- /dev/null
+++ b/apps/edr-freight-web/backoffice/src/components/operations/LastMileSteps.tsx
@@ -0,0 +1,93 @@
+import { Group, Stack, Text, Timeline, Tooltip } from "@mantine/core";
+import { Check } from "lucide-react";
+
+/** One stage of the last-mile delivery workflow. */
+export interface LastMileStepState {
+ label: string;
+ done: boolean;
+ active: boolean;
+ /** Optional stamp/value shown next to the step (plate, time, distance…). */
+ detail?: string | null;
+}
+
+/**
+ * Compact 6-dot progress bar for a table row — filled = done, ringed = current,
+ * hollow = pending. Hover a dot for its label + stamp.
+ */
+export function LastMileStepBar({ steps }: { steps: LastMileStepState[] }) {
+ return (
+
+ {steps.map((s, i) => {
+ const color = s.done
+ ? "var(--mantine-color-green-6)"
+ : s.active
+ ? "var(--mantine-color-blue-5)"
+ : "var(--mantine-color-gray-4)";
+ return (
+
+
+
+ );
+ })}
+
+ );
+}
+
+/**
+ * Vertical stepper for the detail view — completed steps bulleted + green, the
+ * current step highlighted, each showing its stamp/value when known.
+ */
+export function LastMileStepper({ steps }: { steps: LastMileStepState[] }) {
+ const activeIndex = steps.findIndex((s) => s.active);
+ // Timeline highlights items with index < `active`; count of done steps drives it.
+ const doneCount = steps.filter((s) => s.done).length;
+ return (
+
+ {steps.map((s, i) => (
+ : undefined}
+ title={
+
+ {s.label}
+
+ }
+ lineVariant={s.done ? "solid" : "dashed"}
+ >
+
+
+ {s.done ? "Done" : s.active ? "Current step" : "Pending"}
+
+ {s.detail && (
+
+ {s.detail}
+
+ )}
+
+
+ ))}
+
+ );
+}
diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx
index 0a40e9d08..56fbcc3fc 100644
--- a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx
@@ -49,6 +49,7 @@ import { driversService, type Driver } from "@/services/drivers.service";
import { ratesService } from "@/services/rates.service";
import { LastMileContainerAllocationTable, type LastMileContainerRow } from "@/components/LastMileContainerAllocationTable";
import { ReleaseOrderModal, type ReleaseOrderTruckPrefill } from "@/components/warehouses/ReleaseOrderModal";
+import { LastMileStepBar, LastMileStepper, type LastMileStepState } from "@/components/operations/LastMileSteps";
import { api } from "@/auth/http";
const formatPrice = (amount: number) =>
@@ -92,6 +93,50 @@ const vehicleLabel = (record: LastMileRecord) => {
const isAssigned = (record: LastMileRecord) => Boolean(record.vehicleId);
+const fmtStamp = (iso?: string | null) => {
+ if (!iso) return null;
+ const d = new Date(iso);
+ return Number.isNaN(d.getTime()) ? null : d.toLocaleString();
+};
+
+/**
+ * Derive the 6-step last-mile workflow state for a record. Step completion is
+ * read from the record + its pickup-ready (warehouse release) row:
+ * assign→vehicleId, arrived→release order issued, leave→releaseDate,
+ * in-transit/delivered→status, distance→exactKm.
+ */
+const computeLastMileSteps = (
+ record: LastMileRecord,
+ releaseRow?: ImportUnloadedItem,
+): LastMileStepState[] => {
+ const exactKm = (record as { exactKm?: number | null }).exactKm;
+ const flags = [
+ Boolean(record.vehicleId),
+ Boolean(releaseRow?.releaseOrderReference),
+ Boolean(releaseRow?.releaseDate),
+ record.status === "IN_TRANSIT" || record.status === "DELIVERED",
+ exactKm != null,
+ record.status === "DELIVERED",
+ ];
+ // Current step = earliest incomplete one.
+ const activeIdx = flags.findIndex((f) => !f);
+ const labels = ["Assign vehicle", "Truck arrived", "Truck leave", "In transit", "Add distance", "Delivered"];
+ const details: (string | null)[] = [
+ record.vehicle?.plateNumber ?? null,
+ releaseRow?.releaseOrderReference ?? null,
+ fmtStamp(releaseRow?.releaseDate),
+ null,
+ exactKm != null ? `${exactKm} KM` : null,
+ fmtStamp(releaseRow?.deliveredAt),
+ ];
+ return labels.map((label, i) => ({
+ label,
+ done: flags[i],
+ active: i === activeIdx,
+ detail: details[i],
+ }));
+};
+
const bookingRef = (r: LastMileRecord) => r.booking?.reference ?? r.bookingId;
const customerName = (r: LastMileRecord) => r.booking?.company?.name ?? "—";
const deliveryLocation = (r: LastMileRecord) => r.booking?.lastMileDeliveryAddress ?? "—";
@@ -943,6 +988,20 @@ const LastMilePage = () => {
),
},
+ {
+ id: "progress",
+ header: "Progress",
+ meta: { headerClassName, cellClassName },
+ cell: ({ row }) => (
+
+ ),
+ },
{
id: "actions",
header: "Actions",
@@ -1324,6 +1383,18 @@ const LastMilePage = () => {
>
{activeRecord && }
+ {activeRecord && (
+
+ Delivery steps
+
+
+ )}