This commit is contained in:
natib21
2026-07-03 21:21:42 +00:00
parent 7155ec7e01
commit 6bf8da2934
5 changed files with 51 additions and 431 deletions

View File

@@ -1,7 +1,6 @@
import { type ReactNode, useMemo, useState } from "react";
import {
ArrowRight,
Boxes,
Eye,
MoreHorizontal,
Plus,
@@ -55,10 +54,8 @@ import {
import { vehiclesService } from "@/services/vehicles.service";
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 { LastMileStepper, type LastMileStepState } from "@/components/operations/LastMileSteps";
import { api } from "@/auth/http";
const formatPrice = (amount: number) =>
`ETB ${amount.toLocaleString("en-US", {
@@ -118,19 +115,6 @@ const bookingContainerNumbers = (record: LastMileRecord): string[] =>
.map((c) => c.containerNumber)
.filter((n): n is string => Boolean(n));
/** Container rows for the per-container→vehicle allocation table. */
const allocationRowsFor = (record: LastMileRecord): LastMileContainerRow[] =>
(record.booking?.bookingContainers ?? []).map((c) => ({
id: c.id,
type:
c.containerNumber ??
c.containerType?.code ??
c.containerType?.label ??
c.containerType?.name ??
(c.containerSize || "Container"),
qty: c.quantity || 1,
}));
/** Container badges for a booking: the container number when known, else the
* type × quantity. */
const containerLabels = (record: LastMileRecord): string[] => {
@@ -500,8 +484,6 @@ const LastMilePage = () => {
// Record pending invoice-generation confirmation (shows a summary first).
const [invoiceConfirm, setInvoiceConfirm] = useState<LastMileRecord | null>(null);
const [allocationOpen, setAllocationOpen] = useState(false);
const [allocationContainers, setAllocationContainers] = useState<LastMileContainerRow[]>([]);
const [releaseItem, setReleaseItem] = useState<WarehouseInventoryItem | null>(null);
const [releaseTruckPrefill, setReleaseTruckPrefill] = useState<ReleaseOrderTruckPrefill | null>(null);
@@ -639,19 +621,6 @@ const LastMilePage = () => {
},
});
const allocateMutation = useMutation({
mutationFn: (data: Array<{ containerId: string; vehicleId: string }>) =>
api.post(`/last-mile/${activeId}/allocate-containers`, { allocations: data }),
onSuccess: () => {
toast({ title: "Containers allocated", variant: "default" });
void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.ROOT });
void qc.invalidateQueries({ queryKey: ["vehicles"] });
closeAllocation();
},
onError: () => {
toast({ title: "Allocation failed", variant: "destructive" });
},
});
const { data: arrivalQueueData, isLoading: arrivalLoading } = useQuery({
queryKey: ["warehouse-inventory", "arrival-queue"],
@@ -744,17 +713,6 @@ const LastMilePage = () => {
};
const openAllocation = (id: string, containers?: LastMileContainerRow[]) => {
setActiveId(id);
setAllocationContainers(containers ?? []);
setAllocationOpen(true);
};
const closeAllocation = () => {
setAllocationOpen(false);
setActiveId(null);
setAllocationContainers([]);
};
const handleSaveDistance = () => {
const distances = Object.entries(distanceRows)
@@ -984,6 +942,15 @@ const LastMilePage = () => {
setReleaseItem(toReleaseInventoryItem(row));
};
// Truck leaving the warehouse = the leg is now in transit. Advance the status
// (same as "Mark In Transit") alongside the warehouse exit-weighing flow.
const handleTruckLeaving = (record: LastMileRecord) => {
openTruckArrival(record);
if (record.status === "READY_TO_TRANSIT") {
updateMutation.mutate({ id: record.id, data: { status: "IN_TRANSIT" } });
}
};
const closeTruckArrival = () => {
setReleaseItem(null);
setReleaseTruckPrefill(null);
@@ -1089,7 +1056,11 @@ const LastMilePage = () => {
return (
<Group gap="xs" wrap="nowrap">
<UnstyledButton
onClick={() => navigate(`/dashboard/invoices/${invoice.id}`)}
onClick={() =>
invoice.id
? navigate(`/dashboard/invoices/${invoice.id}`)
: toast({ title: "Invoice link unavailable", description: "Refresh after the API restart.", variant: "destructive" })
}
c="blue"
fw={500}
style={{ textDecoration: "underline", cursor: "pointer" }}
@@ -1135,11 +1106,12 @@ const LastMilePage = () => {
(status === "IN_TRANSIT" && hasDistance);
const canAssignStep = !assigned && status !== "DELIVERED";
const canDistance = status === "IN_TRANSIT";
// Truck arrival/leaving are independent — each driven only by its own
// warehouse state: arrive once assigned & not arrived, leave once
// arrived & not departed.
const canArrive = assigned && !releaseRow?.releaseOrderReference;
const canLeave = Boolean(releaseRow?.releaseOrderReference) && !releaseRow?.releaseDate;
// Truck arrival/leaving are independent — each driven by its own
// warehouse state — but both are done once the leg is IN_TRANSIT/DELIVERED.
const pastTransit = status === "IN_TRANSIT" || status === "DELIVERED";
const canArrive = assigned && !releaseRow?.releaseOrderReference && !pastTransit;
const canLeave =
Boolean(releaseRow?.releaseOrderReference) && !releaseRow?.releaseDate && !pastTransit;
return (
<Group gap={4} justify="flex-end" wrap="nowrap">
<Menu position="bottom-end" width={200} withinPortal>
@@ -1189,13 +1161,6 @@ const LastMilePage = () => {
>
Unassign
</Menu.Item>
<Menu.Item
leftSection={<Boxes size={15} />}
disabled={allocationRowsFor(row.original).length === 0 || delivered}
onClick={() => openAllocation(row.original.id, allocationRowsFor(row.original))}
>
Allocate to trucks
</Menu.Item>
<Menu.Item
leftSection={<Truck size={15} />}
disabled={!canArrive}
@@ -1206,7 +1171,7 @@ const LastMilePage = () => {
<Menu.Item
leftSection={<Truck size={15} />}
disabled={!canLeave}
onClick={() => openTruckArrival(row.original)}
onClick={() => handleTruckLeaving(row.original)}
>
Truck Leaving
</Menu.Item>
@@ -1219,17 +1184,20 @@ const LastMilePage = () => {
</Menu.Item>
<Menu.Item
leftSection={<Ruler size={15} />}
disabled={!canDistance}
disabled={!canDistance || Boolean(row.original.invoice)}
onClick={() => openDistance(row.original.id)}
>
Add distance
</Menu.Item>
<Menu.Item
leftSection={<Receipt size={15} />}
disabled={!(row.original.exactKm != null && row.original.exactKm > 0)}
disabled={
!(row.original.exactKm != null && row.original.exactKm > 0) ||
Boolean(row.original.invoice)
}
onClick={() => setInvoiceConfirm(row.original)}
>
Generate Invoice
{row.original.invoice ? "Invoice generated" : "Generate Invoice"}
</Menu.Item>
{canPrint && (
<Menu.Item
@@ -1245,7 +1213,7 @@ const LastMilePage = () => {
<Menu.Item
leftSection={<Trash size={15} />}
color="red"
disabled={delivered}
disabled={delivered || Boolean(row.original.invoice)}
onClick={() => {
if (confirm(`Delete last-mile record ${bookingRef(row.original)}?`)) {
deleteMutation.mutate(row.original.id);
@@ -1829,75 +1797,6 @@ const LastMilePage = () => {
)}
</Modal>
{/* Container Allocation modal */}
<Modal
opened={allocationOpen}
onClose={closeAllocation}
title={<Text fw={600}>Allocate Containers to Vehicles</Text>}
size="xl"
radius="lg"
centered
>
<Stack gap="md">
{activeRecord && (
<>
<Card withBorder padding="md" radius="md" bg="var(--mantine-color-gray-0)">
<Stack gap="sm">
<Group justify="space-between">
<Stack gap={0}>
<Text fw={600} size="sm">{bookingRef(activeRecord)}</Text>
<Text size="xs" c="dimmed">{customerName(activeRecord)}</Text>
</Stack>
<Stack gap={0} align="flex-end">
<Text size="xs" c="dimmed" tt="uppercase">Cargo Type</Text>
<Text size="sm" fw={600}>{activeRecord.booking?.cargoType?.label ?? activeRecord.booking?.cargoType?.name ?? "—"}</Text>
</Stack>
</Group>
</Stack>
</Card>
{/* Capacity logic based on cargo type */}
{activeRecord.booking?.cargoType?.name === "BULK" ? (
<Card withBorder padding="md" radius="md" bg="var(--mantine-color-blue-0)" style={{ borderColor: "var(--mantine-color-blue-3)" }}>
<Stack gap="sm">
<Group gap="xs">
<Text fw={600} size="sm">Smart Capacity Allocation</Text>
</Group>
<Stack gap={2}>
<Text size="sm">Capacity: TBD</Text>
<Text size="xs" c="dimmed">
TODO: add vehicle capacity_tons to vehicle API if missing
</Text>
<Text size="xs" c="dimmed">
TODO: add container weight to booking if missing
</Text>
</Stack>
<Text size="sm" fw={500} mt="xs">
Select multiple containers per vehicle based on capacity
</Text>
</Stack>
</Card>
) : (
<Card withBorder padding="md" radius="md" bg="var(--mantine-color-gray-0)">
<Text size="sm" fw={500}>Up to 2 containers per vehicle (trailer)</Text>
</Card>
)}
</>
)}
<LastMileContainerAllocationTable
containers={allocationContainers}
onSave={async (mappings) => {
await allocateMutation.mutateAsync(mappings);
}}
/>
<Group justify="flex-end" gap="sm">
<Button variant="default" onClick={closeAllocation}>Close</Button>
</Group>
</Stack>
</Modal>
<ReleaseOrderModal
opened={Boolean(releaseItem)}
onClose={closeTruckArrival}