From 806fc4a8ca2d47a9d6a6339db5d18d6612fbec20 Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Sat, 20 Jun 2026 18:03:11 +0000 Subject: [PATCH] =?UTF-8?q?feat(warehouse):=20Batch=205=20=E2=80=94=20Expo?= =?UTF-8?q?rt=20Ready=20To=20Load=20queue=20+=20Auto=20Load=20Ready=20Item?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /warehouse-inventory/ready-to-load-export: EXPORT+PASSED+READY_FOR_LOADING items with booking details (customer, container, cargo type, route, inspection status) - Direction filtering via deriveTradeDirection (route-based, not stored field) - Frontend ReadyToLoadTab: full table (checkbox, booking ref/id, customer, container, cargo type, weight, route, inspection status, current status) with selection - Auto Load Ready Items button reuses existing POST /load-passed-export endpoint - Replaces "Ready To Load — coming in the next batch" placeholder in Export sub-tabs Co-Authored-By: Claude Sonnet 4.6 --- .../warehouse-inventory.controller.ts | 6 + .../warehouses/warehouse-inventory.service.ts | 55 +++++++ .../warehouses/ReceiveInventoryModal.tsx | 137 +++++++++++++++++- .../backoffice/src/constants/URLS.ts | 1 + .../backoffice/src/hooks/useWarehouses.ts | 8 + .../src/services/warehouse.service.ts | 3 + .../backoffice/src/types/warehouse.ts | 15 ++ 7 files changed, 221 insertions(+), 4 deletions(-) diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.controller.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.controller.ts index aec3d3311..cb6147e42 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.controller.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.controller.ts @@ -78,6 +78,12 @@ export class WarehouseInventoryController { return this.inventoryService.loadPassedExport(performedBy); } + @Get('ready-to-load-export') + @ApiOperation({ summary: 'EXPORT inventory that passed inspection and is READY_FOR_LOADING' }) + readyToLoadExport() { + return this.inventoryService.readyToLoadExport(); + } + @Post('bulk-mark-inspected') @ApiOperation({ summary: 'Bulk mark received inventory inspection PASSED (EXPORT → READY_FOR_LOADING)' }) bulkMarkInspected(@Body() dto: BulkInspectDto) { diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts index 355a79839..2cf586f14 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts @@ -154,6 +154,21 @@ export interface BulkInspectResult { results: { inventoryId: string; status: string; reason?: string }[]; } +export interface ReadyToLoadRow { + id: string; + bookingId: string | null; + bookingReference: string | null; + customerId: string | null; + customerName: string | null; + containerNumber: string | null; + cargoType: string | null; + weight: number | null; + origin: string | null; + destination: string | null; + inspectionStatus: string | null; + status: string; +} + @Injectable() export class WarehouseInventoryService { constructor( @@ -601,6 +616,46 @@ export class WarehouseInventoryService { return result; } + /** EXPORT inventory that passed inspection and is waiting to be loaded (READY_FOR_LOADING). */ + async readyToLoadExport(): Promise { + const rows: Array< + ReadyToLoadRow & { originCountry: string | null; destinationCountry: string | null } + > = await this.dataSource.query( + `SELECT inv.id, + inv.booking_id AS "bookingId", + b.reference AS "bookingReference", + b.company_id AS "customerId", + company.name AS "customerName", + ct.container_number AS "containerNumber", + COALESCE(cgt.cargo_type_name, b.cargo_free_text) AS "cargoType", + inv.weight AS "weight", + oy.code AS "origin", + dy.code AS "destination", + oy.country AS "originCountry", + dy.country AS "destinationCountry", + inv.inspection_status AS "inspectionStatus", + inv.status + FROM freight.warehouse_inventory inv + LEFT JOIN freight.bookings b ON b.id = inv.booking_id AND b.deleted_at IS NULL + LEFT JOIN freight.companies company ON company.id = b.company_id + LEFT JOIN freight.yards oy ON oy.id = b.origin_yard_id + LEFT JOIN freight.yards dy ON dy.id = b.destination_yard_id + LEFT JOIN freight.cargo_types cgt ON cgt.id = b.cargo_type_id + LEFT JOIN freight.containers ct ON ct.id = inv.container_id + WHERE inv.deleted_at IS NULL + AND inv.status = 'READY_FOR_LOADING' + AND inv.inspection_status = 'PASSED' + ORDER BY inv.created_at DESC`, + ); + + return rows + .filter((r) => { + const dir = deriveTradeDirection({ country: r.originCountry }, { country: r.destinationCountry }); + return dir === 'EXPORT'; + }) + .map(({ originCountry: _oc, destinationCountry: _dc, ...rest }) => rest); + } + /** * Bulk-mark received items inspection PASSED (reusing the inspection service to create a minimal * report + sync inspectionStatus/inspectedAt). EXPORT items advance straight to READY_FOR_LOADING. diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/ReceiveInventoryModal.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/ReceiveInventoryModal.tsx index c597b9d0b..c0e513f27 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/ReceiveInventoryModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/ReceiveInventoryModal.tsx @@ -23,12 +23,13 @@ import { useBulkReceive, useEligibleBookings, useLoadPassedExport, + useReadyToLoadExport, useReceiveInventory, useWarehouseYards, useWarehouseZones, useWarehouses, } from '@/hooks/useWarehouses'; -import type { BulkReceiveResult, LoadPassedExportResult, ReceiveInventoryPayload } from '@/types/warehouse'; +import type { BulkReceiveResult, LoadPassedExportResult, ReadyToLoadRow, ReceiveInventoryPayload } from '@/types/warehouse'; import { BookingSelect } from './BookingSelect'; import { extractErrorMessage, formatNumber } from './options'; @@ -327,6 +328,136 @@ function EligibleTab({ ); } +/** Export items that passed inspection and are queued to be loaded onto a train. */ +function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?: () => void }) { + const { toast } = useToast(); + const { data: rows = [], isLoading } = useReadyToLoadExport(enabled); + const loadPassed = useLoadPassedExport(); + const [selected, setSelected] = useState>(new Set()); + + const allSelected = rows.length > 0 && selected.size === rows.length; + const someSelected = selected.size > 0 && !allSelected; + const toggleAll = () => setSelected(allSelected ? new Set() : new Set(rows.map((r) => r.id))); + const toggleOne = (id: string) => + setSelected((prev) => { + const next = new Set(prev); + next.has(id) ? next.delete(id) : next.add(id); + return next; + }); + + const autoLoad = async () => { + try { + const res = (await loadPassed.mutateAsync(undefined)) as { data: LoadPassedExportResult }; + const r = res.data; + toast({ + title: `${r.loadedCount} items loaded`, + description: r.skippedCount ? `${r.skippedCount} skipped` : undefined, + }); + setSelected(new Set()); + onChanged?.(); + } catch (error) { + toast({ variant: 'destructive', title: 'Load failed', description: extractErrorMessage(error) }); + } + }; + + return ( + + + + {rows.length} item{rows.length !== 1 ? 's' : ''} ready to load + + + + + {isLoading ? ( + + + + ) : rows.length === 0 ? ( + + No EXPORT items with inspection PASSED waiting to be loaded. + + ) : ( + + + + + + + + Booking Ref + Booking ID + Customer ID + Customer Name + Container # + Cargo Type + Weight + Route + Inspection + Status + + + + {rows.map((r: ReadyToLoadRow) => ( + + + toggleOne(r.id)} + /> + + + {r.bookingReference ?? '—'} + + + {r.bookingId ? `${r.bookingId.slice(0, 8)}…` : '—'} + + + {r.customerId ? `${r.customerId.slice(0, 8)}…` : '—'} + + {r.customerName ?? '—'} + {r.containerNumber ?? '—'} + {r.cargoType ?? '—'} + {formatNumber(Number(r.weight))} + + {r.origin || r.destination ? `${r.origin ?? '?'} → ${r.destination ?? '?'}` : '—'} + + + + {r.inspectionStatus ?? '—'} + + + + + {r.status} + + + + ))} + +
+
+ )} +
+ ); +} + /** New bulk Receive: Import / Export tabs with eligible PAID bookings. */ function BulkReceiveModal({ opened, onClose, onReceived }: ReceiveInventoryModalProps) { const [location, setLocation] = useState({ warehouseId: '', yardId: '', zoneId: '' }); @@ -367,9 +498,7 @@ function BulkReceiveModal({ opened, onClose, onReceived }: ReceiveInventoryModal - - Ready To Load — coming in the next batch. - + diff --git a/apps/edr-freight-web/backoffice/src/constants/URLS.ts b/apps/edr-freight-web/backoffice/src/constants/URLS.ts index 6cdce3931..cb8a83d94 100644 --- a/apps/edr-freight-web/backoffice/src/constants/URLS.ts +++ b/apps/edr-freight-web/backoffice/src/constants/URLS.ts @@ -311,6 +311,7 @@ export const URL_CONSTANTS = { RECEIVE_BULK: '/warehouse-inventory/receive-bulk', LOAD_PASSED_EXPORT: '/warehouse-inventory/load-passed-export', BULK_MARK_INSPECTED: '/warehouse-inventory/bulk-mark-inspected', + READY_TO_LOAD_EXPORT: '/warehouse-inventory/ready-to-load-export', }, WAREHOUSE_LOADINGS: { diff --git a/apps/edr-freight-web/backoffice/src/hooks/useWarehouses.ts b/apps/edr-freight-web/backoffice/src/hooks/useWarehouses.ts index 6683d0b85..5b9a3d6e5 100644 --- a/apps/edr-freight-web/backoffice/src/hooks/useWarehouses.ts +++ b/apps/edr-freight-web/backoffice/src/hooks/useWarehouses.ts @@ -203,6 +203,14 @@ export const useLoadPassedExport = () => export const useBulkMarkInspected = () => useInventoryMutation((payload: BulkInspectPayload) => warehouseService.bulkMarkInspected(payload)); +export function useReadyToLoadExport(enabled = true) { + return useQuery({ + queryKey: ['warehouse-inventory', 'ready-to-load-export'], + queryFn: () => warehouseService.readyToLoadExport().then((r) => r.data), + enabled, + }); +} + // ── Loading (Batch 3) ──────────────────────────────────────────────────────── export function useLoadableWagons(enabled = true) { diff --git a/apps/edr-freight-web/backoffice/src/services/warehouse.service.ts b/apps/edr-freight-web/backoffice/src/services/warehouse.service.ts index c77167bb0..275fa6c61 100644 --- a/apps/edr-freight-web/backoffice/src/services/warehouse.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/warehouse.service.ts @@ -35,6 +35,7 @@ import type { LoadPassedExportResult, BulkInspectPayload, BulkInspectResult, + ReadyToLoadRow, ReserveInventoryPayload, SaveWarehousePayload, SaveYardPayload, @@ -130,6 +131,8 @@ export const warehouseService = { apiClient.post(URL_CONSTANTS.WAREHOUSE_INVENTORY.LOAD_PASSED_EXPORT, {}), bulkMarkInspected: (payload: BulkInspectPayload) => apiClient.post(URL_CONSTANTS.WAREHOUSE_INVENTORY.BULK_MARK_INSPECTED, payload), + readyToLoadExport: () => + apiClient.get(URL_CONSTANTS.WAREHOUSE_INVENTORY.READY_TO_LOAD_EXPORT), move: (id: string, payload: MoveInventoryPayload) => apiClient.post(URL_CONSTANTS.WAREHOUSE_INVENTORY.MOVE(id), payload), movements: (id: string) => diff --git a/apps/edr-freight-web/backoffice/src/types/warehouse.ts b/apps/edr-freight-web/backoffice/src/types/warehouse.ts index cc7663ae9..8968e50e2 100644 --- a/apps/edr-freight-web/backoffice/src/types/warehouse.ts +++ b/apps/edr-freight-web/backoffice/src/types/warehouse.ts @@ -380,6 +380,21 @@ export interface BulkInspectResult { results: { inventoryId: string; status: string; reason?: string }[]; } +export interface ReadyToLoadRow { + id: string; + bookingId: string | null; + bookingReference: string | null; + customerId: string | null; + customerName: string | null; + containerNumber: string | null; + cargoType: string | null; + weight: number | null; + origin: string | null; + destination: string | null; + inspectionStatus: string | null; + status: string; +} + export interface InventoryInquiryResult { id: string; bookingId: string;