mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 21:15:41 +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);
|
||||
|
||||
Reference in New Issue
Block a user