mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 10:58:14 +00:00
Truck Assign by customer plus Handover signature on portal
This commit is contained in:
@@ -41,7 +41,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { api } from '@/services/api';
|
||||
import { QUERY_KEYS } from '@/constants/QUERY_KEYS';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { useInventoryInquiry } from '@/hooks/useWarehouses';
|
||||
import { useAllWarehouseYards, useAllWarehouseZones, useInventoryInquiry } from '@/hooks/useWarehouses';
|
||||
import { firstMileService } from '@/services/first-mile.service';
|
||||
import { warehouseService } from '@/services/warehouse.service';
|
||||
import type {
|
||||
@@ -54,7 +54,10 @@ import type {
|
||||
ReadyToLoadRow,
|
||||
ReceiveInventoryPayload,
|
||||
TruckEntrancePayload,
|
||||
Warehouse,
|
||||
WarehouseInventoryItem,
|
||||
WarehouseYard,
|
||||
WarehouseZone,
|
||||
} from '@/types/warehouse';
|
||||
import { BookingSelect } from './BookingSelect';
|
||||
import { DeliverInventoryModal } from './DeliverInventoryModal';
|
||||
@@ -70,6 +73,9 @@ import { extractErrorMessage, formatDate, formatNumber, inventoryStatusOptions }
|
||||
import { openPdfBlob } from './pdf';
|
||||
import '@/components/overview/overview.css';
|
||||
|
||||
type ImportUnloadAssignment = { bookingId: string; warehouseId: string; yardId: string; zoneId: string };
|
||||
type ImportUnloadAssignmentDraft = Partial<Omit<ImportUnloadAssignment, 'bookingId'>>;
|
||||
|
||||
interface ReceiveInventoryModalProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
@@ -1741,14 +1747,59 @@ function LoadedExportTab({
|
||||
);
|
||||
}
|
||||
|
||||
/** Assigned bookings/items for an arrived import train (read-only detail view). */
|
||||
function ImportTrainDetailTable({ train }: { train: ImportTrain }) {
|
||||
const importLocationTypesForFreight = (freightType: string | null | undefined) => {
|
||||
const normalized = (freightType ?? '').toUpperCase();
|
||||
if (normalized === 'CONTAINER') {
|
||||
return { yardTypes: ['CONTAINER_YARD', 'GENERAL_CARGO_YARD'], zoneTypes: ['CONTAINER_ZONE', 'GENERAL_CARGO_ZONE'] };
|
||||
}
|
||||
return { yardTypes: ['BULK_YARD', 'GENERAL_CARGO_YARD'], zoneTypes: ['BULK_ZONE', 'GENERAL_CARGO_ZONE'] };
|
||||
};
|
||||
|
||||
const isImportContainerFreight = (freightType: string | null | undefined) =>
|
||||
(freightType ?? '').toUpperCase() === 'CONTAINER';
|
||||
|
||||
const isImportUnloadPending = (item: ImportTrainItem) =>
|
||||
!item.currentStatus || item.currentStatus === 'RECEIVED';
|
||||
|
||||
/** Assigned bookings/items for an arrived import train with per-booking unload locations. */
|
||||
function ImportTrainDetailTable({
|
||||
train,
|
||||
warehouses,
|
||||
yards,
|
||||
zones,
|
||||
assignments,
|
||||
onAssignmentChange,
|
||||
onReadyChange,
|
||||
}: {
|
||||
train: ImportTrain;
|
||||
warehouses: Warehouse[];
|
||||
yards: WarehouseYard[];
|
||||
zones: WarehouseZone[];
|
||||
assignments: Record<string, ImportUnloadAssignmentDraft>;
|
||||
onAssignmentChange: (bookingId: string, draft: ImportUnloadAssignmentDraft) => void;
|
||||
onReadyChange: (ready: boolean) => void;
|
||||
}) {
|
||||
const { data: items = [], isLoading } = useQuery(
|
||||
api.warehouses.importTrainItems.queryOptions({
|
||||
input: { scheduleId: train.scheduleId },
|
||||
enabled: Boolean(train.scheduleId),
|
||||
}),
|
||||
);
|
||||
const warehouseOptions = useMemo(
|
||||
() => warehouses.map((warehouse) => ({ value: warehouse.id, label: `${warehouse.name} (${warehouse.code})` })),
|
||||
[warehouses],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const pending = items.filter(isImportUnloadPending);
|
||||
onReadyChange(
|
||||
pending.length > 0 &&
|
||||
pending.every((item) => {
|
||||
const draft = assignments[item.bookingId];
|
||||
return Boolean(draft?.warehouseId && draft.yardId && draft.zoneId);
|
||||
}),
|
||||
);
|
||||
}, [assignments, items]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -1778,6 +1829,9 @@ function ImportTrainDetailTable({ train }: { train: ImportTrain }) {
|
||||
<Table.Th>Cargo Type</Table.Th>
|
||||
<Table.Th>Weight</Table.Th>
|
||||
<Table.Th>Arrival</Table.Th>
|
||||
<Table.Th>Warehouse</Table.Th>
|
||||
<Table.Th>Yard</Table.Th>
|
||||
<Table.Th>Zone</Table.Th>
|
||||
<Table.Th>Inspection</Table.Th>
|
||||
<Table.Th>Current Status</Table.Th>
|
||||
<Table.Th>Last Mile</Table.Th>
|
||||
@@ -1785,7 +1839,18 @@ function ImportTrainDetailTable({ train }: { train: ImportTrain }) {
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{items.map((it: ImportTrainItem) => (
|
||||
{items.map((it: ImportTrainItem) => {
|
||||
const draft = assignments[it.bookingId] ?? {};
|
||||
const { yardTypes, zoneTypes } = importLocationTypesForFreight(it.freightType);
|
||||
const yardOptions = yards
|
||||
.filter((yard) => yard.warehouseId === draft.warehouseId && yardTypes.includes(yard.type))
|
||||
.map((yard) => ({ value: yard.id, label: `${yard.name} (${yard.code})` }));
|
||||
const zoneOptions = zones
|
||||
.filter((zone) => zone.yardId === draft.yardId && zoneTypes.includes(zone.type))
|
||||
.map((zone) => ({ value: zone.id, label: `${zone.name} (${zone.code})` }));
|
||||
const pending = isImportUnloadPending(it);
|
||||
|
||||
return (
|
||||
<Table.Tr key={`${it.bookingId}-${it.sequenceNo ?? 'wagon'}`}>
|
||||
<Table.Td>
|
||||
<Text size="xs" fw={600}>
|
||||
@@ -1806,6 +1871,41 @@ function ImportTrainDetailTable({ train }: { train: ImportTrain }) {
|
||||
<Table.Td>{it.cargoType ?? '—'}</Table.Td>
|
||||
<Table.Td>{formatNumber(Number(it.weight))}</Table.Td>
|
||||
<Table.Td>{formatDate(it.arrivalTime)}</Table.Td>
|
||||
<Table.Td>
|
||||
<Select
|
||||
placeholder="Warehouse"
|
||||
data={warehouseOptions}
|
||||
value={draft.warehouseId ?? null}
|
||||
onChange={(value) => onAssignmentChange(it.bookingId, { warehouseId: value ?? undefined })}
|
||||
searchable
|
||||
disabled={!pending}
|
||||
w={210}
|
||||
/>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Select
|
||||
placeholder={isImportContainerFreight(it.freightType) ? 'Container yard' : 'Bulk yard'}
|
||||
data={yardOptions}
|
||||
value={draft.yardId ?? null}
|
||||
onChange={(value) =>
|
||||
onAssignmentChange(it.bookingId, { ...draft, yardId: value ?? undefined, zoneId: undefined })
|
||||
}
|
||||
searchable
|
||||
disabled={!pending || !draft.warehouseId}
|
||||
w={190}
|
||||
/>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Select
|
||||
placeholder={isImportContainerFreight(it.freightType) ? 'Container zone' : 'Bulk zone'}
|
||||
data={zoneOptions}
|
||||
value={draft.zoneId ?? null}
|
||||
onChange={(value) => onAssignmentChange(it.bookingId, { ...draft, zoneId: value ?? undefined })}
|
||||
searchable
|
||||
disabled={!pending || !draft.yardId}
|
||||
w={190}
|
||||
/>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge size="xs" variant="light" color={it.inspectionStatus === 'PASSED' ? 'green' : 'gray'}>
|
||||
{it.inspectionStatus ?? 'Not inspected'}
|
||||
@@ -1821,7 +1921,8 @@ function ImportTrainDetailTable({ train }: { train: ImportTrain }) {
|
||||
</Table.Td>
|
||||
<Table.Td>{it.pickupOption}</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
);
|
||||
@@ -1845,13 +1946,42 @@ function ImportArriveQueueTab({
|
||||
const { data: trains = [], isLoading } = useQuery(
|
||||
api.warehouses.importArriveQueue.queryOptions({ enabled }),
|
||||
);
|
||||
const { data: warehouses = [], isLoading: warehousesLoading } = useQuery(
|
||||
api.warehouses.list.queryOptions({ input: { filter: { status: 'ACTIVE' } }, enabled }),
|
||||
);
|
||||
const { data: yards = [] } = useAllWarehouseYards();
|
||||
const { data: zones = [] } = useAllWarehouseZones();
|
||||
const autoUnloadMutation = useMutation(
|
||||
api.warehouses.autoUnloadArrivedBookings.mutationOptions(),
|
||||
);
|
||||
const [openId, setOpenId] = useState<string | null>(null);
|
||||
const [busyId, setBusyId] = useState<string | null>(null);
|
||||
const [assignmentsBySchedule, setAssignmentsBySchedule] = useState<
|
||||
Record<string, Record<string, ImportUnloadAssignmentDraft>>
|
||||
>({});
|
||||
const [readyBySchedule, setReadyBySchedule] = useState<Record<string, boolean>>({});
|
||||
|
||||
const autoUnload = async (train: ImportTrain) => {
|
||||
const assignments = Object.entries(assignmentsBySchedule[train.scheduleId] ?? {})
|
||||
.filter((entry): entry is [string, Required<ImportUnloadAssignmentDraft>] =>
|
||||
Boolean(entry[1].warehouseId && entry[1].yardId && entry[1].zoneId),
|
||||
)
|
||||
.map(([bookingId, draft]) => ({
|
||||
bookingId,
|
||||
warehouseId: draft.warehouseId,
|
||||
yardId: draft.yardId,
|
||||
zoneId: draft.zoneId,
|
||||
}));
|
||||
|
||||
if (!readyBySchedule[train.scheduleId] || assignments.length === 0) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: 'Assign locations',
|
||||
description: 'Select warehouse, yard and zone for each pending booking before unloading.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFullyUnloaded(train)) {
|
||||
toast({
|
||||
title: 'Already unloaded',
|
||||
@@ -1862,7 +1992,7 @@ function ImportArriveQueueTab({
|
||||
|
||||
setBusyId(train.scheduleId);
|
||||
try {
|
||||
const r = await autoUnloadMutation.mutateAsync(train.scheduleId);
|
||||
const r = await autoUnloadMutation.mutateAsync({ scheduleId: train.scheduleId, assignments });
|
||||
const alreadyUnloaded = r.unloadedCount === 0 && r.skippedCount > 0 && r.failedCount === 0;
|
||||
const firstReason = r.results.find((item) => item.reason)?.reason;
|
||||
const extra = [
|
||||
@@ -1961,7 +2091,7 @@ function ImportArriveQueueTab({
|
||||
color={fullyUnloaded ? 'gray' : 'indigo'}
|
||||
leftSection={<Truck size={14} />}
|
||||
loading={busyId === t.scheduleId}
|
||||
disabled={fullyUnloaded || t.totalBookings === 0}
|
||||
disabled={fullyUnloaded || t.totalBookings === 0 || !readyBySchedule[t.scheduleId] || warehousesLoading}
|
||||
onClick={() => autoUnload(t)}
|
||||
>
|
||||
{fullyUnloaded ? 'Already Unloaded' : 'Auto Unload Arrived Bookings'}
|
||||
@@ -1972,7 +2102,25 @@ function ImportArriveQueueTab({
|
||||
{isOpen && (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={11} bg="var(--mantine-color-gray-0)">
|
||||
<ImportTrainDetailTable train={t} />
|
||||
<ImportTrainDetailTable
|
||||
train={t}
|
||||
warehouses={warehouses}
|
||||
yards={yards}
|
||||
zones={zones}
|
||||
assignments={assignmentsBySchedule[t.scheduleId] ?? {}}
|
||||
onAssignmentChange={(bookingId, draft) =>
|
||||
setAssignmentsBySchedule((current) => ({
|
||||
...current,
|
||||
[t.scheduleId]: {
|
||||
...(current[t.scheduleId] ?? {}),
|
||||
[bookingId]: draft.warehouseId ? draft : {},
|
||||
},
|
||||
}))
|
||||
}
|
||||
onReadyChange={(ready) =>
|
||||
setReadyBySchedule((current) => ({ ...current, [t.scheduleId]: ready }))
|
||||
}
|
||||
/>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
|
||||
@@ -158,13 +158,13 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea
|
||||
const assignedContainerNumber = assignedTruckValue(item, 'customerTruckContainerNumber');
|
||||
const prefillContainerNumber = truckPrefill?.containerNumber ?? '';
|
||||
setReference(item?.releaseOrderReference ?? generateReleaseReference(item));
|
||||
setTruckPlateNumber(inspection.truckPlateNumber || assignedTruckPlate || truckPrefill?.truckPlateNumber || '');
|
||||
setTruckPlateNumber(inspection.truckPlateNumber || truckPrefill?.truckPlateNumber || assignedTruckPlate || '');
|
||||
setTrailerPlateNumber(inspection.trailerPlateNumber || truckPrefill?.trailerPlateNumber || '');
|
||||
setDriverName(inspection.driverName || assignedDriverName || truckPrefill?.driverName || '');
|
||||
setDriverName(inspection.driverName || truckPrefill?.driverName || assignedDriverName || '');
|
||||
setDriverLicense(inspection.driverLicense || truckPrefill?.driverLicense || '');
|
||||
setDriverPhone(inspection.driverPhone || truckPrefill?.driverPhone || '');
|
||||
setTruckType(inspection.truckType || assignedTruckType || truckPrefill?.truckType || '');
|
||||
setContainerNumbers(initialContainerNumbers(item, inspection.containerNumber || assignedContainerNumber || prefillContainerNumber));
|
||||
setTruckType(inspection.truckType || truckPrefill?.truckType || assignedTruckType || '');
|
||||
setContainerNumbers(initialContainerNumbers(item, inspection.containerNumber || prefillContainerNumber || assignedContainerNumber));
|
||||
setGateInTime(inspection.gateInTime);
|
||||
setTareWeight(inspection.tareWeight);
|
||||
setGrossWeight(inspection.grossWeight);
|
||||
|
||||
@@ -280,7 +280,13 @@ export function useImportTrainItems(scheduleId?: string) {
|
||||
|
||||
/** Unload all eligible assigned bookings of an ARRIVED import train (→ UNLOADED). */
|
||||
export const useAutoUnloadArrivedBookings = () =>
|
||||
useInventoryMutation((scheduleId: string) => warehouseService.autoUnloadArrivedBookings(scheduleId));
|
||||
useInventoryMutation((payload: {
|
||||
scheduleId: string;
|
||||
warehouseId?: string;
|
||||
assignments?: { bookingId: string; warehouseId: string; yardId: string; zoneId: string }[];
|
||||
}) =>
|
||||
warehouseService.autoUnloadArrivedBookings(payload),
|
||||
);
|
||||
|
||||
/** Arrived EXPORT trains at Djibouti-side ports. Read-only. */
|
||||
export function useExportDjiboutiArrivalQueue(enabled = true) {
|
||||
|
||||
@@ -44,6 +44,7 @@ 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";
|
||||
@@ -145,13 +146,20 @@ const toReleaseInventoryItem = (row: ImportUnloadedItem): 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 || null,
|
||||
driverName: vehicle?.assignedDriverName || assignedDriverName || null,
|
||||
driverLicense: assignedDriver?.licenseNumber || null,
|
||||
driverPhone: assignedDriver?.phoneNumber || null,
|
||||
truckType: vehicle?.vehicleType || truckType || null,
|
||||
containerNumber: row?.containerNumber ?? null,
|
||||
};
|
||||
@@ -401,6 +409,22 @@ const LastMilePage = () => {
|
||||
|
||||
const records = listData?.data ?? [];
|
||||
|
||||
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),
|
||||
@@ -733,7 +757,7 @@ const LastMilePage = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
setReleaseTruckPrefill(releasePrefillFromLastMile(record, row));
|
||||
setReleaseTruckPrefill(releasePrefillFromLastMile(record, row, driversById));
|
||||
setReleaseItem(toReleaseInventoryItem(row));
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Fragment, useState } from 'react';
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Container,
|
||||
Group,
|
||||
Loader,
|
||||
Select,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
@@ -21,11 +22,17 @@ import {
|
||||
} from '@/components/warehouses';
|
||||
import {
|
||||
useAutoUnloadArrivedBookings,
|
||||
useAllWarehouseYards,
|
||||
useAllWarehouseZones,
|
||||
useImportArriveQueue,
|
||||
useImportTrainItems,
|
||||
useWarehouses,
|
||||
} from '@/hooks/useWarehouses';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import type { AutoUnloadArrivedResult, ImportTrain, ImportTrainItem } from '@/types/warehouse';
|
||||
import type { AutoUnloadArrivedResult, ImportTrain, ImportTrainItem, Warehouse, WarehouseYard, WarehouseZone } from '@/types/warehouse';
|
||||
|
||||
type UnloadAssignment = { bookingId: string; warehouseId: string; yardId: string; zoneId: string };
|
||||
type AssignmentDraft = Partial<Omit<UnloadAssignment, 'bookingId'>>;
|
||||
|
||||
const getErrorMessage = (error: unknown) => {
|
||||
if (error && typeof error === 'object' && 'response' in error) {
|
||||
@@ -43,8 +50,54 @@ const getPendingUnloadBookings = (train: ImportTrain) =>
|
||||
const isFullyUnloaded = (train: ImportTrain) =>
|
||||
Boolean(train.fullyUnloaded) || (train.totalBookings > 0 && getPendingUnloadBookings(train) === 0);
|
||||
|
||||
function ImportTrainDetailRows({ scheduleId }: { scheduleId: string }) {
|
||||
const locationTypesForFreight = (freightType: string | null | undefined) => {
|
||||
const normalized = (freightType ?? '').toUpperCase();
|
||||
if (normalized === 'CONTAINER') {
|
||||
return { yardTypes: ['CONTAINER_YARD', 'GENERAL_CARGO_YARD'], zoneTypes: ['CONTAINER_ZONE', 'GENERAL_CARGO_ZONE'] };
|
||||
}
|
||||
return { yardTypes: ['BULK_YARD', 'GENERAL_CARGO_YARD'], zoneTypes: ['BULK_ZONE', 'GENERAL_CARGO_ZONE'] };
|
||||
};
|
||||
|
||||
const isContainerFreight = (freightType: string | null | undefined) =>
|
||||
(freightType ?? '').toUpperCase() === 'CONTAINER';
|
||||
|
||||
function isUnloadPending(item: ImportTrainItem) {
|
||||
return !item.currentStatus || item.currentStatus === 'RECEIVED';
|
||||
}
|
||||
|
||||
function ImportTrainDetailRows({
|
||||
scheduleId,
|
||||
warehouses,
|
||||
yards,
|
||||
zones,
|
||||
assignments,
|
||||
onAssignmentChange,
|
||||
onReadyChange,
|
||||
}: {
|
||||
scheduleId: string;
|
||||
warehouses: Warehouse[];
|
||||
yards: WarehouseYard[];
|
||||
zones: WarehouseZone[];
|
||||
assignments: Record<string, AssignmentDraft>;
|
||||
onAssignmentChange: (bookingId: string, draft: AssignmentDraft) => void;
|
||||
onReadyChange: (ready: boolean) => void;
|
||||
}) {
|
||||
const { data: items = [], isLoading } = useImportTrainItems(scheduleId);
|
||||
const warehouseOptions = useMemo(
|
||||
() => warehouses.map((warehouse) => ({ value: warehouse.id, label: `${warehouse.name} (${warehouse.code})` })),
|
||||
[warehouses],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const pending = items.filter(isUnloadPending);
|
||||
onReadyChange(
|
||||
pending.length > 0 &&
|
||||
pending.every((item) => {
|
||||
const draft = assignments[item.bookingId];
|
||||
return Boolean(draft?.warehouseId && draft.yardId && draft.zoneId);
|
||||
}),
|
||||
);
|
||||
}, [assignments, items]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -73,12 +126,26 @@ function ImportTrainDetailRows({ scheduleId }: { scheduleId: string }) {
|
||||
<Table.Th>Weight</Table.Th>
|
||||
<Table.Th>Arrival</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>Warehouse</Table.Th>
|
||||
<Table.Th>Yard</Table.Th>
|
||||
<Table.Th>Zone</Table.Th>
|
||||
<Table.Th>Inspection</Table.Th>
|
||||
<Table.Th>Pickup</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{items.map((item: ImportTrainItem) => (
|
||||
{items.map((item: ImportTrainItem) => {
|
||||
const draft = assignments[item.bookingId] ?? {};
|
||||
const { yardTypes, zoneTypes } = locationTypesForFreight(item.freightType);
|
||||
const yardOptions = yards
|
||||
.filter((yard) => yard.warehouseId === draft.warehouseId && yardTypes.includes(yard.type))
|
||||
.map((yard) => ({ value: yard.id, label: `${yard.name} (${yard.code})` }));
|
||||
const zoneOptions = zones
|
||||
.filter((zone) => zone.yardId === draft.yardId && zoneTypes.includes(zone.type))
|
||||
.map((zone) => ({ value: zone.id, label: `${zone.name} (${zone.code})` }));
|
||||
const pending = isUnloadPending(item);
|
||||
|
||||
return (
|
||||
<Table.Tr key={item.bookingId}>
|
||||
<Table.Td>
|
||||
<Text size="sm" fw={600}>
|
||||
@@ -95,6 +162,41 @@ function ImportTrainDetailRows({ scheduleId }: { scheduleId: string }) {
|
||||
{item.currentStatus ?? 'PENDING'}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Select
|
||||
placeholder="Warehouse"
|
||||
data={warehouseOptions}
|
||||
value={draft.warehouseId ?? null}
|
||||
onChange={(value) => onAssignmentChange(item.bookingId, { warehouseId: value ?? undefined })}
|
||||
searchable
|
||||
disabled={!pending}
|
||||
w={210}
|
||||
/>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Select
|
||||
placeholder={isContainerFreight(item.freightType) ? 'Container yard' : 'Bulk yard'}
|
||||
data={yardOptions}
|
||||
value={draft.yardId ?? null}
|
||||
onChange={(value) =>
|
||||
onAssignmentChange(item.bookingId, { ...draft, yardId: value ?? undefined, zoneId: undefined })
|
||||
}
|
||||
searchable
|
||||
disabled={!pending || !draft.warehouseId}
|
||||
w={190}
|
||||
/>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Select
|
||||
placeholder={isContainerFreight(item.freightType) ? 'Container zone' : 'Bulk zone'}
|
||||
data={zoneOptions}
|
||||
value={draft.zoneId ?? null}
|
||||
onChange={(value) => onAssignmentChange(item.bookingId, { ...draft, zoneId: value ?? undefined })}
|
||||
searchable
|
||||
disabled={!pending || !draft.yardId}
|
||||
w={190}
|
||||
/>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge variant="light" color={item.inspectionStatus === 'PASSED' ? 'green' : 'gray'} size="sm">
|
||||
{item.inspectionStatus ?? 'Not inspected'}
|
||||
@@ -102,7 +204,8 @@ function ImportTrainDetailRows({ scheduleId }: { scheduleId: string }) {
|
||||
</Table.Td>
|
||||
<Table.Td>{item.pickupOption.replace(/_/g, ' ')}</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
);
|
||||
@@ -112,11 +215,36 @@ function ImportTrainDetailRows({ scheduleId }: { scheduleId: string }) {
|
||||
export default function ArrivalQueuePage() {
|
||||
const { toast } = useToast();
|
||||
const { data: trains = [], isLoading } = useImportArriveQueue();
|
||||
const { data: warehouses = [], isLoading: warehousesLoading } = useWarehouses({ status: 'ACTIVE' });
|
||||
const { data: yards = [] } = useAllWarehouseYards();
|
||||
const { data: zones = [] } = useAllWarehouseZones();
|
||||
const autoUnload = useAutoUnloadArrivedBookings();
|
||||
const [openScheduleId, setOpenScheduleId] = useState<string | null>(null);
|
||||
const [busyScheduleId, setBusyScheduleId] = useState<string | null>(null);
|
||||
const [assignmentsBySchedule, setAssignmentsBySchedule] = useState<Record<string, Record<string, AssignmentDraft>>>({});
|
||||
const [readyBySchedule, setReadyBySchedule] = useState<Record<string, boolean>>({});
|
||||
|
||||
const unloadTrain = async (train: ImportTrain) => {
|
||||
const assignments = Object.entries(assignmentsBySchedule[train.scheduleId] ?? {})
|
||||
.filter((entry): entry is [string, Required<AssignmentDraft>] =>
|
||||
Boolean(entry[1].warehouseId && entry[1].yardId && entry[1].zoneId),
|
||||
)
|
||||
.map(([bookingId, draft]) => ({
|
||||
bookingId,
|
||||
warehouseId: draft.warehouseId,
|
||||
yardId: draft.yardId,
|
||||
zoneId: draft.zoneId,
|
||||
}));
|
||||
|
||||
if (!readyBySchedule[train.scheduleId] || assignments.length === 0) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: 'Assign locations',
|
||||
description: 'Select warehouse, yard and zone for each pending booking before unloading.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFullyUnloaded(train)) {
|
||||
toast({
|
||||
title: 'Already unloaded',
|
||||
@@ -127,7 +255,9 @@ export default function ArrivalQueuePage() {
|
||||
|
||||
setBusyScheduleId(train.scheduleId);
|
||||
try {
|
||||
const res = (await autoUnload.mutateAsync(train.scheduleId)) as { data: AutoUnloadArrivedResult };
|
||||
const res = (await autoUnload.mutateAsync({ scheduleId: train.scheduleId, assignments })) as {
|
||||
data: AutoUnloadArrivedResult;
|
||||
};
|
||||
const result = res.data;
|
||||
const alreadyUnloaded = result.unloadedCount === 0 && result.skippedCount > 0 && result.failedCount === 0;
|
||||
const firstReason = result.results.find((item) => item.reason)?.reason;
|
||||
@@ -169,10 +299,12 @@ export default function ArrivalQueuePage() {
|
||||
|
||||
<Card withBorder radius="md" padding="lg">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Text fw={600}>{trains.length} arrived import train(s)</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Open a train to review assigned bookings, then auto unload it.
|
||||
</Text>
|
||||
<Stack gap={2}>
|
||||
<Text fw={600}>{trains.length} arrived import train(s)</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Open a train, assign each booking to a warehouse yard and zone, then unload it.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
|
||||
{isLoading ? (
|
||||
@@ -254,7 +386,7 @@ export default function ArrivalQueuePage() {
|
||||
color={fullyUnloaded ? 'gray' : 'orange'}
|
||||
leftSection={busyScheduleId === train.scheduleId ? <PackageOpen size={14} /> : <Truck size={14} />}
|
||||
loading={busyScheduleId === train.scheduleId}
|
||||
disabled={fullyUnloaded || train.totalBookings === 0}
|
||||
disabled={fullyUnloaded || train.totalBookings === 0 || !readyBySchedule[train.scheduleId] || warehousesLoading}
|
||||
onClick={() => unloadTrain(train)}
|
||||
>
|
||||
{fullyUnloaded ? 'Already Unloaded' : 'Auto Unload'}
|
||||
@@ -265,7 +397,27 @@ export default function ArrivalQueuePage() {
|
||||
{isOpen && (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={10} bg="var(--mantine-color-gray-0)">
|
||||
<ImportTrainDetailRows scheduleId={train.scheduleId} />
|
||||
<ImportTrainDetailRows
|
||||
scheduleId={train.scheduleId}
|
||||
warehouses={warehouses}
|
||||
yards={yards}
|
||||
zones={zones}
|
||||
assignments={assignmentsBySchedule[train.scheduleId] ?? {}}
|
||||
onAssignmentChange={(bookingId, draft) =>
|
||||
setAssignmentsBySchedule((current) => ({
|
||||
...current,
|
||||
[train.scheduleId]: {
|
||||
...(current[train.scheduleId] ?? {}),
|
||||
[bookingId]: draft.warehouseId
|
||||
? draft
|
||||
: {},
|
||||
},
|
||||
}))
|
||||
}
|
||||
onReadyChange={(ready) =>
|
||||
setReadyBySchedule((current) => ({ ...current, [train.scheduleId]: ready }))
|
||||
}
|
||||
/>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
|
||||
@@ -945,12 +945,19 @@ export const api = {
|
||||
() => INVENTORY_INVALIDATIONS,
|
||||
),
|
||||
|
||||
autoUnloadArrivedBookings: endpoint<string, AutoUnloadArrivedResult>(
|
||||
autoUnloadArrivedBookings: endpoint<
|
||||
{
|
||||
scheduleId: string;
|
||||
warehouseId?: string;
|
||||
assignments?: { bookingId: string; warehouseId: string; yardId: string; zoneId: string }[];
|
||||
},
|
||||
AutoUnloadArrivedResult
|
||||
>(
|
||||
"warehouse-inventory",
|
||||
"auto-unload-arrived-bookings",
|
||||
(scheduleId) =>
|
||||
({ scheduleId, warehouseId, assignments }) =>
|
||||
warehouseService
|
||||
.autoUnloadArrivedBookings(scheduleId)
|
||||
.autoUnloadArrivedBookings({ scheduleId, warehouseId, assignments })
|
||||
.then((r) => r.data),
|
||||
undefined,
|
||||
() => INVENTORY_INVALIDATIONS,
|
||||
|
||||
@@ -26,6 +26,8 @@ export interface Vehicle {
|
||||
capacity: number;
|
||||
status: VehicleStatus;
|
||||
description?: string | null;
|
||||
assignedDriverId?: string | null;
|
||||
assignedDriverName?: string | null;
|
||||
code?: string | null;
|
||||
powerPlateNo?: string | null;
|
||||
trailerPlateNo?: string | null;
|
||||
|
||||
@@ -173,10 +173,14 @@ export const warehouseService = {
|
||||
apiClient.get<ImportTrain[]>(URL_CONSTANTS.WAREHOUSE_INVENTORY.IMPORT_ARRIVE_QUEUE),
|
||||
importTrainItems: (scheduleId: string) =>
|
||||
apiClient.get<ImportTrainItem[]>(URL_CONSTANTS.WAREHOUSE_INVENTORY.IMPORT_TRAIN_ITEMS(scheduleId)),
|
||||
autoUnloadArrivedBookings: (scheduleId: string) =>
|
||||
autoUnloadArrivedBookings: (payload: {
|
||||
scheduleId: string;
|
||||
warehouseId?: string;
|
||||
assignments?: { bookingId: string; warehouseId: string; yardId: string; zoneId: string }[];
|
||||
}) =>
|
||||
apiClient.post<AutoUnloadArrivedResult>(
|
||||
URL_CONSTANTS.WAREHOUSE_INVENTORY.IMPORT_AUTO_UNLOAD_ARRIVED,
|
||||
{ scheduleId },
|
||||
payload,
|
||||
),
|
||||
importUnloadedQueue: () =>
|
||||
apiClient.get<ImportUnloadedItem[]>(URL_CONSTANTS.WAREHOUSE_INVENTORY.IMPORT_UNLOADED_QUEUE),
|
||||
|
||||
@@ -605,6 +605,7 @@ export interface ImportTrainItem {
|
||||
wagonNumber: string | null;
|
||||
sequenceNo: number | null;
|
||||
allocatedWeightTons: number | null;
|
||||
freightType: string | null;
|
||||
containerNumber: string | null;
|
||||
cargoType: string | null;
|
||||
weight: number | null;
|
||||
|
||||
Reference in New Issue
Block a user