mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 19:58:11 +00:00
fix
This commit is contained in:
@@ -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 (
|
||||||
|
<Group gap={4} wrap="nowrap">
|
||||||
|
{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 (
|
||||||
|
<Tooltip
|
||||||
|
key={i}
|
||||||
|
withArrow
|
||||||
|
label={`${s.label}${s.detail ? ` · ${s.detail}` : ""}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
borderRadius: "50%",
|
||||||
|
background: s.done ? color : "transparent",
|
||||||
|
border: `2px solid ${color}`,
|
||||||
|
boxShadow: s.active
|
||||||
|
? "0 0 0 2px var(--mantine-color-blue-1)"
|
||||||
|
: undefined,
|
||||||
|
display: "inline-block",
|
||||||
|
flex: "0 0 auto",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (
|
||||||
|
<Timeline
|
||||||
|
active={activeIndex === -1 ? steps.length : doneCount}
|
||||||
|
bulletSize={22}
|
||||||
|
lineWidth={2}
|
||||||
|
color="green"
|
||||||
|
>
|
||||||
|
{steps.map((s, i) => (
|
||||||
|
<Timeline.Item
|
||||||
|
key={i}
|
||||||
|
bullet={s.done ? <Check size={12} /> : undefined}
|
||||||
|
title={
|
||||||
|
<Text size="sm" fw={s.active ? 600 : 500} c={s.active ? "blue" : undefined}>
|
||||||
|
{s.label}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
lineVariant={s.done ? "solid" : "dashed"}
|
||||||
|
>
|
||||||
|
<Stack gap={0}>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{s.done ? "Done" : s.active ? "Current step" : "Pending"}
|
||||||
|
</Text>
|
||||||
|
{s.detail && (
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{s.detail}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Timeline.Item>
|
||||||
|
))}
|
||||||
|
</Timeline>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -49,6 +49,7 @@ import { driversService, type Driver } from "@/services/drivers.service";
|
|||||||
import { ratesService } from "@/services/rates.service";
|
import { ratesService } from "@/services/rates.service";
|
||||||
import { LastMileContainerAllocationTable, type LastMileContainerRow } from "@/components/LastMileContainerAllocationTable";
|
import { LastMileContainerAllocationTable, type LastMileContainerRow } from "@/components/LastMileContainerAllocationTable";
|
||||||
import { ReleaseOrderModal, type ReleaseOrderTruckPrefill } from "@/components/warehouses/ReleaseOrderModal";
|
import { ReleaseOrderModal, type ReleaseOrderTruckPrefill } from "@/components/warehouses/ReleaseOrderModal";
|
||||||
|
import { LastMileStepBar, LastMileStepper, type LastMileStepState } from "@/components/operations/LastMileSteps";
|
||||||
import { api } from "@/auth/http";
|
import { api } from "@/auth/http";
|
||||||
|
|
||||||
const formatPrice = (amount: number) =>
|
const formatPrice = (amount: number) =>
|
||||||
@@ -92,6 +93,50 @@ const vehicleLabel = (record: LastMileRecord) => {
|
|||||||
|
|
||||||
const isAssigned = (record: LastMileRecord) => Boolean(record.vehicleId);
|
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 bookingRef = (r: LastMileRecord) => r.booking?.reference ?? r.bookingId;
|
||||||
const customerName = (r: LastMileRecord) => r.booking?.company?.name ?? "—";
|
const customerName = (r: LastMileRecord) => r.booking?.company?.name ?? "—";
|
||||||
const deliveryLocation = (r: LastMileRecord) => r.booking?.lastMileDeliveryAddress ?? "—";
|
const deliveryLocation = (r: LastMileRecord) => r.booking?.lastMileDeliveryAddress ?? "—";
|
||||||
@@ -943,6 +988,20 @@ const LastMilePage = () => {
|
|||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "progress",
|
||||||
|
header: "Progress",
|
||||||
|
meta: { headerClassName, cellClassName },
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<LastMileStepBar
|
||||||
|
steps={computeLastMileSteps(
|
||||||
|
row.original,
|
||||||
|
pickupReadyByBooking.get(row.original.bookingId) ??
|
||||||
|
pickupReadyByBooking.get(bookingRef(row.original)),
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
header: "Actions",
|
header: "Actions",
|
||||||
@@ -1324,6 +1383,18 @@ const LastMilePage = () => {
|
|||||||
>
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
{activeRecord && <BookingInfo record={activeRecord} />}
|
{activeRecord && <BookingInfo record={activeRecord} />}
|
||||||
|
{activeRecord && (
|
||||||
|
<Card withBorder padding="md" radius="md">
|
||||||
|
<Text fw={600} size="sm" mb="sm">Delivery steps</Text>
|
||||||
|
<LastMileStepper
|
||||||
|
steps={computeLastMileSteps(
|
||||||
|
activeRecord,
|
||||||
|
pickupReadyByBooking.get(activeRecord.bookingId) ??
|
||||||
|
pickupReadyByBooking.get(bookingRef(activeRecord)),
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
<Group justify="flex-end">
|
<Group justify="flex-end">
|
||||||
<Button variant="default" onClick={() => { setDetailOpen(false); setActiveId(null); }}>Close</Button>
|
<Button variant="default" onClick={() => { setDetailOpen(false); setActiveId(null); }}>Close</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
Reference in New Issue
Block a user