From 0066a87972be59b3d4b265c62a2b83909acbe838 Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Thu, 20 Aug 2026 10:46:42 +0000 Subject: [PATCH] feat(warehouses): add empty-containers/import-trains/export-trains to dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the warehouse dashboard with 3 metrics the screenshot target needed but the backend didn't expose: emptyContainers (AVAILABLE containers — closest proxy, no literal EMPTY status exists), importTrains (reuses the import arrival queue definition), and exportTrains (reuses the Djibouti export arrival queue definition) via SchedulingReadFacade. Reorders the frontend metric grid to match and fixes the Empty Containers card linking to a route that doesn't exist. --- .../warehouses/warehouse-dashboard.service.ts | 53 ++++++++++++++++++- .../warehouses/WarehouseDashboardPage.tsx | 19 ++++--- .../backoffice/src/types/warehouse.ts | 3 ++ 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-dashboard.service.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-dashboard.service.ts index 0bbcdee48..3eee095f8 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-dashboard.service.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-dashboard.service.ts @@ -3,6 +3,7 @@ import { DataSource, FindManyOptions, IsNull, ObjectLiteral, Repository } from ' import { Warehouse } from './entities/warehouse.entity'; import { WarehouseInventory } from './entities/warehouse-inventory.entity'; +import { SchedulingReadFacade } from './scheduling-read.facade'; export interface WarehouseDashboard { totalWarehouses: number; @@ -20,11 +21,45 @@ export interface WarehouseDashboard { // Import branch readyForPickup: number; delivered: number; + // Fleet / train snapshot + emptyContainers: number; + importTrains: number; + exportTrains: number; } @Injectable() export class WarehouseDashboardService { - constructor(private readonly dataSource: DataSource) {} + constructor( + private readonly dataSource: DataSource, + private readonly schedulingRead: SchedulingReadFacade, + ) {} + + private async safeQueryCount(sql: string, params: unknown[] = []): Promise { + try { + const rows = await this.dataSource.query(sql, params); + return Number(rows?.[0]?.count) || 0; + } catch { + return 0; + } + } + + /** ARRIVED trains with IMPORT-direction routing — same set as the import arrival queue. */ + private async safeImportTrains(): Promise { + try { + return (await this.schedulingRead.importArriveQueue()).length; + } catch { + return 0; + } + } + + /** Trains at/near Djibouti relevant to the export flow — same set as the Djibouti unloading queue. */ + private async safeExportTrains(): Promise { + try { + return (await this.schedulingRead.exportDjiboutiArrivalQueue()).length; + } catch { + return 0; + } + } private async safeCount( repo: Repository, @@ -69,6 +104,9 @@ export class WarehouseDashboardService { readyForPickup, delivered, receivedToday, + emptyContainers, + importTrains, + exportTrains, ] = await Promise.all([ this.safeCount(warehouseRepo), this.safeCount(inventoryRepo), @@ -82,6 +120,16 @@ export class WarehouseDashboardService { this.safeCount(inventoryRepo, { where: { status: 'READY_FOR_PICKUP' } }), this.safeCount(inventoryRepo, { where: { status: 'DELIVERED' } }), this.safeReceivedToday(startOfToday), + // ponytail: the container fleet has no literal EMPTY status (AVAILABLE | LOADED | + // IN_TRANSIT | MAINTENANCE | DAMAGED) — AVAILABLE (not on a wagon, not in transit, + // not flagged) is the closest proxy for "empty and free to use". Revisit if the + // domain ever grows a real empty/full distinction per container. + this.safeQueryCount( + `SELECT count(*)::int AS count FROM freight.containers WHERE deleted_at IS NULL AND status = $1`, + ['AVAILABLE'], + ), + this.safeImportTrains(), + this.safeExportTrains(), ]); return { @@ -97,6 +145,9 @@ export class WarehouseDashboardService { dispatched, readyForPickup, delivered, + emptyContainers, + importTrains, + exportTrains, }; } } diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseDashboardPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseDashboardPage.tsx index d8298761a..93dd830a2 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseDashboardPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseDashboardPage.tsx @@ -1,18 +1,17 @@ import { useNavigate } from 'react-router-dom'; import { Badge, Card, Center, Divider, Group, Loader, SimpleGrid, Stack, Text, ThemeIcon } from '@mantine/core'; import { - ClipboardCheck, ClipboardList, - ShieldCheck, PackageCheck, + PackageOpen, PackagePlus, PackageSearch, CircleCheck, Send, + Train, Truck, Warehouse as WarehouseIcon, Boxes, - Layers, } from 'lucide-react'; import { PageContainer, PageHeader } from '@/components/page'; @@ -53,13 +52,13 @@ const METRICS: Metric[] = [ { key: 'totalInventory', label: 'Total Inventory', icon: , to: '/dashboard/warehouse-inventory', theme: GREEN }, { key: 'receivedToday', label: 'Received Today', icon: , to: '/dashboard/warehouse-inventory?status=RECEIVED', theme: ORANGE }, { key: 'awaitingInspection', label: 'Awaiting Inspection', icon: , to: '/dashboard/warehouse-inventory?status=RECEIVED', theme: GREEN }, - { key: 'inspected', label: 'Inspected', icon: , to: '/dashboard/warehouse-inventory', theme: ORANGE }, - { key: 'stored', label: 'Stored', icon: , to: '/dashboard/warehouse-inventory?status=STORED', theme: GREEN }, - { key: 'reserved', label: 'Reserved', icon: , to: '/dashboard/warehouse-inventory?status=RESERVED', theme: ORANGE }, - { key: 'readyForLoading', label: 'Ready For Loading', icon: , to: '/dashboard/loading-queue', theme: GREEN }, - { key: 'loaded', label: 'Loaded', icon: , to: '/dashboard/loaded-inventory', theme: ORANGE }, - { key: 'dispatched', label: 'Dispatched', icon: , to: '/dashboard/dispatch-queue', theme: GREEN }, - { key: 'readyForPickup', label: 'Ready For Pickup', icon: , to: '/dashboard/warehouse-inventory?status=READY_FOR_PICKUP', theme: ORANGE }, + { key: 'emptyContainers', label: 'Empty Containers', icon: , to: '/dashboard/containers', theme: ORANGE }, + { key: 'importTrains', label: 'Import Trains', icon: , to: '/dashboard/import-warehouse', theme: GREEN }, + { key: 'exportTrains', label: 'Export Trains', icon: , to: '/dashboard/export-warehouse', theme: ORANGE }, + { key: 'loaded', label: 'Loaded', icon: , to: '/dashboard/loaded-inventory', theme: GREEN }, + { key: 'dispatched', label: 'Dispatched', icon: , to: '/dashboard/dispatch-queue', theme: ORANGE }, + { key: 'readyForPickup', label: 'Ready For Pickup', icon: , to: '/dashboard/warehouse-inventory?status=READY_FOR_PICKUP', theme: GREEN }, + { key: 'readyForLoading', label: 'Ready For Loading', icon: , to: '/dashboard/loading-queue', theme: ORANGE }, { key: 'delivered', label: 'Delivered', icon: , to: '/dashboard/warehouse-inventory?status=DELIVERED', theme: GREEN }, ]; diff --git a/apps/edr-freight-web/backoffice/src/types/warehouse.ts b/apps/edr-freight-web/backoffice/src/types/warehouse.ts index aac7e532d..1773fb9b9 100644 --- a/apps/edr-freight-web/backoffice/src/types/warehouse.ts +++ b/apps/edr-freight-web/backoffice/src/types/warehouse.ts @@ -299,6 +299,9 @@ export interface WarehouseDashboard { dispatched: number; readyForPickup: number; delivered: number; + emptyContainers: number; + importTrains: number; + exportTrains: number; } // ── Loading (Batch 3) ────────────────────────────────────────────────────────