mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 12:58:13 +00:00
feat(warehouse): Batch 7 — Import Arrive Queue (arrived import trains, read-only)
- SchedulingReadFacade: importArriveQueue() + importTrainDetail() — read-only SELECTs only, direction derived from origin/destination station countries (route-based), so EXPORT/DOMESTIC trains are excluded. Per-train booking/container/cargo counts. - GET /warehouse-inventory/import/arrive-queue + /import/trains/:scheduleId/items - Import tab restructured into sub-tabs: Arrive Queue (implemented) / Unloaded Queue / Dispatch Queue (placeholders for next batch) - ImportArriveQueueTab: train table (schedule id, train #, route, origin, destination, arrival, bookings/containers/cargoes, status) with Open (expandable assigned-items detail) + Auto Unload (reuses existing per-booking unload endpoint over the train's bookings) - Batch7TestDataSeeder: 1 arrived IMPORT train (SEED-IMP-001) + 1 arrived EXPORT train (SEED-EXP-001, proves exclusion); idempotent - Train schedule service logic untouched (facade is SELECT-only) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
@@ -16,17 +16,20 @@ import {
|
||||
Textarea,
|
||||
TextInput,
|
||||
} from '@mantine/core';
|
||||
import { Info, PackageSearch, Truck } from 'lucide-react';
|
||||
import { ChevronDown, ChevronRight, Info, PackageSearch, Train, Truck } from 'lucide-react';
|
||||
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import {
|
||||
useBulkDispatchExport,
|
||||
useBulkReceive,
|
||||
useEligibleBookings,
|
||||
useImportArriveQueue,
|
||||
useImportTrainItems,
|
||||
useLoadPassedExport,
|
||||
useLoadedExport,
|
||||
useReadyToLoadExport,
|
||||
useReceiveInventory,
|
||||
useUnloadBooking,
|
||||
useWarehouseYards,
|
||||
useWarehouseZones,
|
||||
useWarehouses,
|
||||
@@ -34,12 +37,15 @@ import {
|
||||
import type {
|
||||
BulkDispatchResult,
|
||||
BulkReceiveResult,
|
||||
ImportTrain,
|
||||
ImportTrainItem,
|
||||
LoadPassedExportResult,
|
||||
ReadyToLoadRow,
|
||||
ReceiveInventoryPayload,
|
||||
} from '@/types/warehouse';
|
||||
import { warehouseService } from '@/services/warehouse.service';
|
||||
import { BookingSelect } from './BookingSelect';
|
||||
import { extractErrorMessage, formatNumber } from './options';
|
||||
import { extractErrorMessage, formatDate, formatNumber } from './options';
|
||||
|
||||
interface ReceiveInventoryModalProps {
|
||||
opened: boolean;
|
||||
@@ -644,6 +650,218 @@ function LoadedExportTab({
|
||||
);
|
||||
}
|
||||
|
||||
/** Assigned bookings/items for an arrived import train (read-only detail view). */
|
||||
function ImportTrainDetailTable({ scheduleId }: { scheduleId: string }) {
|
||||
const { data: items = [], isLoading } = useImportTrainItems(scheduleId);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Group justify="center" py="md">
|
||||
<Loader size="sm" />
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<Text c="dimmed" ta="center" py="md" size="sm">
|
||||
No assigned bookings on this train.
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Table withTableBorder verticalSpacing="xs" fz="xs">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Booking ID</Table.Th>
|
||||
<Table.Th>Booking Ref</Table.Th>
|
||||
<Table.Th>Customer ID</Table.Th>
|
||||
<Table.Th>Customer Name</Table.Th>
|
||||
<Table.Th>Container #</Table.Th>
|
||||
<Table.Th>Cargo Type</Table.Th>
|
||||
<Table.Th>Weight</Table.Th>
|
||||
<Table.Th>Arrival</Table.Th>
|
||||
<Table.Th>Current Status</Table.Th>
|
||||
<Table.Th>Last Mile</Table.Th>
|
||||
<Table.Th>Pickup Option</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{items.map((it: ImportTrainItem) => (
|
||||
<Table.Tr key={it.bookingId}>
|
||||
<Table.Td>
|
||||
<Text size="xs" c="dimmed">{it.bookingId.slice(0, 8)}…</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs" fw={600}>{it.bookingReference ?? '—'}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs" c="dimmed">{it.customerId ? `${it.customerId.slice(0, 8)}…` : '—'}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>{it.customerName ?? '—'}</Table.Td>
|
||||
<Table.Td>{it.containerNumber ?? '—'}</Table.Td>
|
||||
<Table.Td>{it.cargoType ?? '—'}</Table.Td>
|
||||
<Table.Td>{formatNumber(Number(it.weight))}</Table.Td>
|
||||
<Table.Td>{formatDate(it.arrivalTime)}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge size="xs" variant="light" color="gray">{it.currentStatus ?? '—'}</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge size="xs" variant="light" color={it.lastMileRequested ? 'blue' : 'gray'}>
|
||||
{it.lastMileRequested ? 'Yes' : 'No'}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>{it.pickupOption}</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
/** Import Arrive Queue: arrived IMPORT trains, with Open (detail) + Auto Unload per train. */
|
||||
function ImportArriveQueueTab({
|
||||
location,
|
||||
enabled,
|
||||
onChanged,
|
||||
}: {
|
||||
location: Location;
|
||||
enabled: boolean;
|
||||
onChanged?: () => void;
|
||||
}) {
|
||||
const { toast } = useToast();
|
||||
const { data: trains = [], isLoading } = useImportArriveQueue(enabled);
|
||||
const unloadBooking = useUnloadBooking();
|
||||
const [openId, setOpenId] = useState<string | null>(null);
|
||||
const [busyId, setBusyId] = useState<string | null>(null);
|
||||
|
||||
const locationPayload = location.warehouseId
|
||||
? { warehouseId: location.warehouseId, yardId: location.yardId, zoneId: location.zoneId }
|
||||
: undefined;
|
||||
|
||||
const autoUnload = async (train: ImportTrain) => {
|
||||
setBusyId(train.scheduleId);
|
||||
try {
|
||||
const items = (await warehouseService.importTrainItems(train.scheduleId)).data;
|
||||
if (items.length === 0) {
|
||||
toast({ title: 'No bookings to unload on this train' });
|
||||
return;
|
||||
}
|
||||
let ok = 0;
|
||||
for (const it of items) {
|
||||
try {
|
||||
await unloadBooking.mutateAsync({ bookingId: it.bookingId, payload: locationPayload });
|
||||
ok += 1;
|
||||
} catch {
|
||||
/* already unloaded / not eligible — skip */
|
||||
}
|
||||
}
|
||||
toast({
|
||||
title: `Auto-unloaded ${ok}/${items.length} booking(s)`,
|
||||
description: ok < items.length ? `${items.length - ok} skipped (already unloaded or not eligible)` : undefined,
|
||||
});
|
||||
onChanged?.();
|
||||
} catch (error) {
|
||||
toast({ variant: 'destructive', title: 'Auto unload failed', description: extractErrorMessage(error) });
|
||||
} finally {
|
||||
setBusyId(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="sm" mt="sm">
|
||||
<Text size="sm" c="dimmed">
|
||||
<b>{trains.length}</b> arrived import train{trains.length !== 1 ? 's' : ''}
|
||||
</Text>
|
||||
|
||||
{isLoading ? (
|
||||
<Group justify="center" py="lg">
|
||||
<Loader size="sm" />
|
||||
</Group>
|
||||
) : trains.length === 0 ? (
|
||||
<Text c="dimmed" ta="center" py="lg" size="sm">
|
||||
No arrived import trains. Trains appear here once their schedule status is ARRIVED.
|
||||
</Text>
|
||||
) : (
|
||||
<Table.ScrollContainer minWidth={1500}>
|
||||
<Table highlightOnHover verticalSpacing="xs" striped>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Schedule ID</Table.Th>
|
||||
<Table.Th>Train #</Table.Th>
|
||||
<Table.Th>Route</Table.Th>
|
||||
<Table.Th>Origin</Table.Th>
|
||||
<Table.Th>Destination</Table.Th>
|
||||
<Table.Th>Arrival Time</Table.Th>
|
||||
<Table.Th ta="center">Bookings</Table.Th>
|
||||
<Table.Th ta="center">Containers</Table.Th>
|
||||
<Table.Th ta="center">Cargoes</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th ta="right">Actions</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{trains.map((t: ImportTrain) => {
|
||||
const isOpen = openId === t.scheduleId;
|
||||
return (
|
||||
<Fragment key={t.scheduleId}>
|
||||
<Table.Tr>
|
||||
<Table.Td>
|
||||
<Text size="xs" c="dimmed">{t.scheduleId.slice(0, 8)}…</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="sm" fw={600}>{t.trainNumber ?? '—'}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>{t.route ?? '—'}</Table.Td>
|
||||
<Table.Td>{t.origin ?? '—'}</Table.Td>
|
||||
<Table.Td>{t.destination ?? '—'}</Table.Td>
|
||||
<Table.Td>{formatDate(t.arrivalTime)}</Table.Td>
|
||||
<Table.Td ta="center">{t.totalBookings}</Table.Td>
|
||||
<Table.Td ta="center">{t.totalContainers}</Table.Td>
|
||||
<Table.Td ta="center">{t.totalCargoes}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color="indigo" variant="light" size="sm">{t.status}</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td ta="right">
|
||||
<Group gap="xs" justify="flex-end" wrap="nowrap">
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
leftSection={isOpen ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
onClick={() => setOpenId(isOpen ? null : t.scheduleId)}
|
||||
>
|
||||
Open
|
||||
</Button>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
color="indigo"
|
||||
leftSection={<Truck size={14} />}
|
||||
loading={busyId === t.scheduleId}
|
||||
onClick={() => autoUnload(t)}
|
||||
>
|
||||
Auto Unload
|
||||
</Button>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
{isOpen && (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={11} bg="var(--mantine-color-gray-0)">
|
||||
<ImportTrainDetailTable scheduleId={t.scheduleId} />
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
/** New bulk Receive: Import / Export tabs with eligible PAID bookings. */
|
||||
function BulkReceiveModal({ opened, onClose, onReceived }: ReceiveInventoryModalProps) {
|
||||
const [location, setLocation] = useState<Location>({ warehouseId: '', yardId: '', zoneId: '' });
|
||||
@@ -669,7 +887,29 @@ function BulkReceiveModal({ opened, onClose, onReceived }: ReceiveInventoryModal
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value="IMPORT">
|
||||
<EligibleTab direction="IMPORT" location={location} enabled={opened} onChanged={onReceived} />
|
||||
<Tabs defaultValue="arrive-queue" mt="xs">
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="arrive-queue" leftSection={<Train size={14} />}>
|
||||
Arrive Queue
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="unloaded-queue">Unloaded Queue</Tabs.Tab>
|
||||
<Tabs.Tab value="dispatch-queue">Dispatch Queue</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value="arrive-queue">
|
||||
<ImportArriveQueueTab location={location} enabled={opened} onChanged={onReceived} />
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="unloaded-queue">
|
||||
<Text c="dimmed" ta="center" py="lg" size="sm">
|
||||
Unloaded Queue — coming in the next batch.
|
||||
</Text>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="dispatch-queue">
|
||||
<Text c="dimmed" ta="center" py="lg" size="sm">
|
||||
Dispatch Queue — coming in the next batch.
|
||||
</Text>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="EXPORT">
|
||||
<Tabs defaultValue="receive-queue" mt="xs">
|
||||
|
||||
Reference in New Issue
Block a user