This commit is contained in:
yaschalew
2026-07-02 17:56:17 +03:00
273 changed files with 22748 additions and 5431 deletions

View File

@@ -4,6 +4,7 @@ import {
ChevronRight,
Eye,
MoreHorizontal,
PackageCheck,
Printer,
RefreshCw,
Ruler,
@@ -36,6 +37,7 @@ import {
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
import { FirstMileContainerAllocationTable } from "@/components/FirstMileContainerAllocationTable";
import { ReceiveInventoryModal } from "@/components/warehouses/ReceiveInventoryModal";
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
import { useToast } from "@/hooks/use-toast";
import {
@@ -342,6 +344,8 @@ const FirstMilePage = () => {
const [distanceValue, setDistanceValue] = useState("");
const [invoiceOpen, setInvoiceOpen] = useState(false);
const [invoiceRecord, setInvoiceRecord] = useState<FirstMileRecord | null>(null);
const [warehouseReceiveOpen, setWarehouseReceiveOpen] = useState(false);
const [warehouseReceiveRecord, setWarehouseReceiveRecord] = useState<FirstMileRecord | null>(null);
const [containerAllocationOpen, setContainerAllocationOpen] = useState(false);
const [containerAllocationFirstMileId, setContainerAllocationFirstMileId] = useState<string | null>(null);
@@ -550,6 +554,16 @@ const FirstMilePage = () => {
setInvoiceRecord(null);
};
const openWarehouseReceive = (record: FirstMileRecord) => {
setWarehouseReceiveRecord(record);
setWarehouseReceiveOpen(true);
};
const closeWarehouseReceive = () => {
setWarehouseReceiveOpen(false);
setWarehouseReceiveRecord(null);
};
const openContainerAllocation = (firstMileId: string) => {
setContainerAllocationFirstMileId(firstMileId);
setContainerAllocationOpen(true);
@@ -853,6 +867,7 @@ const FirstMilePage = () => {
const nextStatus = NEXT_STATUS[row.original.status];
const canPrint = row.original.status !== "PAYMENT_PENDING";
const isPaid = (row.original as any).paid;
const canReceiveToWarehouse = row.original.status === "RECEIVED_TO_PORT";
return (
<Group gap={4} justify="flex-end" wrap="nowrap">
<Menu position="bottom-end" width={200} withinPortal>
@@ -891,6 +906,13 @@ const FirstMilePage = () => {
>
View detail
</Menu.Item>
<Menu.Item
leftSection={<PackageCheck size={15} />}
disabled={!canReceiveToWarehouse}
onClick={() => openWarehouseReceive(row.original)}
>
Receive to warehouse
</Menu.Item>
<Menu.Item
leftSection={<Ruler size={15} />}
onClick={() => openDistance(row.original.id)}
@@ -1101,6 +1123,19 @@ const FirstMilePage = () => {
</Stack>
</Modal>
<ReceiveInventoryModal
opened={warehouseReceiveOpen}
onClose={closeWarehouseReceive}
mode="bulk"
direction="EXPORT"
bookingId={warehouseReceiveRecord?.bookingId}
bookingLabel={warehouseReceiveRecord ? bookingRef(warehouseReceiveRecord) : undefined}
onReceived={() => {
void qc.invalidateQueries({ queryKey: QUERY_KEYS.FIRST_MILE.list() });
closeWarehouseReceive();
}}
/>
{/* Accept Booking modal — step 1: booking list, step 2: details + vehicle */}
<Modal
opened={acceptOpen}

View File

@@ -32,7 +32,7 @@ import {
TextInput,
UnstyledButton,
} from "@mantine/core";
import type { ArrivalQueueItem } from "@/types/warehouse";
import type { ArrivalQueueItem, ImportUnloadedItem, WarehouseInventoryItem } from "@/types/warehouse";
import { warehouseService } from "@/services/warehouse.service";
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
@@ -45,8 +45,10 @@ import {
lastMileService,
} from "@/services/last-mile.service";
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 { api } from "@/auth/http";
const formatPrice = (amount: number) =>
@@ -111,6 +113,59 @@ const requestedDate = (r: LastMileRecord) => {
const serviceTypeName = (r: LastMileRecord) =>
r.booking?.serviceType?.label ?? r.booking?.serviceType?.name ?? "—";
const toReleaseInventoryItem = (row: ImportUnloadedItem): WarehouseInventoryItem =>
({
id: row.id,
bookingId: row.bookingId,
quantity: 1,
weight: Number(row.weight) || 0,
grnNumber: row.grnNumber,
status: row.currentStatus,
arrivedAt: row.arrivalTime,
unloadedAt: row.arrivalTime,
inspectionStatus: row.inspectionStatus,
releaseDate: row.releaseDate,
releaseOrderReference: row.releaseOrderReference,
handoverDocumentReference: row.handoverDocumentReference,
handoverDocumentDate: row.handoverDocumentDate,
deliveredAt: row.deliveredAt,
booking: row.bookingId
? {
id: row.bookingId,
reference: row.bookingReference ?? row.bookingId,
tradeDirection: "IMPORT",
lastMileDeliveryAddress: row.lastMileRequested ? row.pickupOption : null,
customerTruckPlateNumber: row.customerTruckPlateNumber,
customerTruckDriverName: row.customerTruckDriverName,
customerTruckType: row.customerTruckType,
customerTruckContainerNumber: row.customerTruckContainerNumber,
customerTruckAssignedAt: row.customerTruckAssignedAt,
}
: null,
}) as unknown as WarehouseInventoryItem;
const releasePrefillFromLastMile = (
record: LastMileRecord,
row?: ImportUnloadedItem | null,
driversById?: Map<string, Driver>,
): ReleaseOrderTruckPrefill => {
const vehicle = record.vehicle;
const truckType = [vehicle?.manufacturer, vehicle?.model].filter(Boolean).join(" ").trim();
const assignedDriver = vehicle?.assignedDriverId ? driversById?.get(vehicle.assignedDriverId) : undefined;
const assignedDriverName = assignedDriver
? `${assignedDriver.firstName ?? ""} ${assignedDriver.lastName ?? ""}`.trim()
: "";
return {
truckPlateNumber: vehicle?.powerPlateNo || vehicle?.plateNumber || null,
trailerPlateNumber: vehicle?.trailerPlateNo || null,
driverName: vehicle?.assignedDriverName || assignedDriverName || null,
driverLicense: assignedDriver?.licenseNumber || null,
driverPhone: assignedDriver?.phoneNumber || null,
truckType: vehicle?.vehicleType || truckType || null,
containerNumber: row?.containerNumber ?? null,
};
};
const InfoRow = ({ label, value }: { label: string; value: string }) => (
<Stack gap={2}>
<Text size="xs" c="dimmed" tt="uppercase" fw={600}>{label}</Text>
@@ -329,6 +384,8 @@ const LastMilePage = () => {
const [allocationOpen, setAllocationOpen] = useState(false);
const [allocationContainers, setAllocationContainers] = useState<LastMileContainerRow[]>([]);
const [releaseItem, setReleaseItem] = useState<WarehouseInventoryItem | null>(null);
const [releaseTruckPrefill, setReleaseTruckPrefill] = useState<ReleaseOrderTruckPrefill | null>(null);
const { data: listData, isLoading } = useQuery({
queryKey: QUERY_KEYS.LAST_MILE.list(),
@@ -360,6 +417,27 @@ const LastMilePage = () => {
[records],
);
const needsDriverLookup = records.some((record) => record.vehicle?.assignedDriverId);
const { data: driversData } = useQuery({
queryKey: ["drivers", "list", "ACTIVE"],
queryFn: async () => {
const res = await driversService.getAll({ status: "ACTIVE" });
return res.data;
},
enabled: needsDriverLookup,
});
const driversById = useMemo(
() => new Map((driversData ?? []).map((driver) => [driver.id, driver])),
[driversData],
);
const { data: pickupReadyRows = [] } = useQuery({
queryKey: ["warehouse-inventory", "import-pickup-ready-queue"],
queryFn: () => warehouseService.importPickupReadyQueue().then((r) => r.data),
});
const vehicleOptions = useMemo(
() =>
(Array.isArray(vehiclesData) ? vehiclesData : []).map((v) => {
@@ -556,6 +634,15 @@ const LastMilePage = () => {
[records, activeId],
);
const pickupReadyByBooking = useMemo(() => {
const map = new Map<string, ImportUnloadedItem>();
for (const row of pickupReadyRows) {
if (row.bookingId) map.set(row.bookingId, row);
if (row.bookingReference) map.set(row.bookingReference, row);
}
return map;
}, [pickupReadyRows]);
const selectedIds = useMemo(
() => Object.keys(rowSelection).filter((id) => rowSelection[id]),
[rowSelection],
@@ -592,6 +679,7 @@ const LastMilePage = () => {
const term = search.trim().toLowerCase();
return records.filter((r) => {
if (!matchesFilter(r)) return false;
if (filterPostPaymentPending && !(r.remainingPayment > 0)) return false;
if (!term) return true;
return [bookingRef(r), customerName(r), deliveryLocation(r), cargoDesc(r)]
.join(" ")
@@ -672,6 +760,37 @@ const LastMilePage = () => {
setTripSlipOpen(true);
};
const openTruckArrival = (record: LastMileRecord) => {
if (!isAssigned(record)) {
toast({
title: "Assign a truck first",
description: "Truck arrival opens after a last-mile vehicle is assigned.",
variant: "destructive",
});
return;
}
const row = pickupReadyByBooking.get(record.bookingId) ?? pickupReadyByBooking.get(bookingRef(record));
if (!row) {
toast({
title: "Import inventory is not pickup-ready",
description: `${bookingRef(record)} must be unloaded and pass inspection before truck arrival.`,
variant: "destructive",
});
return;
}
setReleaseTruckPrefill(releasePrefillFromLastMile(record, row, driversById));
setReleaseItem(toReleaseInventoryItem(row));
};
const closeTruckArrival = () => {
setReleaseItem(null);
setReleaseTruckPrefill(null);
void qc.invalidateQueries({ queryKey: ["warehouse-inventory"] });
void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.ROOT });
};
const printTripSlip = () => {
if (!tripSlipRecord) return;
const win = window.open("", "_blank", "width=820,height=920");
@@ -834,6 +953,9 @@ const LastMilePage = () => {
const canPrint = row.original.status !== "PAYMENT_PENDING";
const isPaid = (row.original as any).paid;
const delivered = row.original.status === "DELIVERED";
const releaseRow =
pickupReadyByBooking.get(row.original.bookingId) ?? pickupReadyByBooking.get(bookingRef(row.original));
const truckArrivalLabel = releaseRow?.releaseOrderReference ? "Truck Leaving" : "Truck Arrival";
return (
<Group gap={4} justify="flex-end" wrap="nowrap">
<Menu position="bottom-end" width={200} withinPortal>
@@ -865,6 +987,13 @@ const LastMilePage = () => {
>
Reassign
</Menu.Item>
<Menu.Item
leftSection={<Truck size={15} />}
disabled={!assigned}
onClick={() => openTruckArrival(row.original)}
>
{truckArrivalLabel}
</Menu.Item>
<Menu.Divider />
<Menu.Item
leftSection={<Eye size={15} />}
@@ -912,7 +1041,7 @@ const LastMilePage = () => {
},
];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [vehicleOptions]);
}, [vehicleOptions, pickupReadyByBooking]);
return (
<Stack gap="md">
@@ -1413,6 +1542,13 @@ const LastMilePage = () => {
</Group>
</Stack>
</Modal>
<ReleaseOrderModal
opened={Boolean(releaseItem)}
onClose={closeTruckArrival}
item={releaseItem}
truckPrefill={releaseTruckPrefill}
/>
</Stack>
);
};