mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 01:18:18 +00:00
mile
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user