feat(warehouses): add empty-containers/import-trains/export-trains to dashboard

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.
This commit is contained in:
Hagernesh
2026-08-20 10:46:42 +00:00
parent f9987e515c
commit 0066a87972
3 changed files with 64 additions and 11 deletions

View File

@@ -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<number> {
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<number> {
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<number> {
try {
return (await this.schedulingRead.exportDjiboutiArrivalQueue()).length;
} catch {
return 0;
}
}
private async safeCount<T extends ObjectLiteral>(
repo: Repository<T>,
@@ -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,
};
}
}

View File

@@ -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: <Boxes size={22} />, to: '/dashboard/warehouse-inventory', theme: GREEN },
{ key: 'receivedToday', label: 'Received Today', icon: <PackagePlus size={22} />, to: '/dashboard/warehouse-inventory?status=RECEIVED', theme: ORANGE },
{ key: 'awaitingInspection', label: 'Awaiting Inspection', icon: <ClipboardList size={22} />, to: '/dashboard/warehouse-inventory?status=RECEIVED', theme: GREEN },
{ key: 'inspected', label: 'Inspected', icon: <ShieldCheck size={22} />, to: '/dashboard/warehouse-inventory', theme: ORANGE },
{ key: 'stored', label: 'Stored', icon: <Layers size={22} />, to: '/dashboard/warehouse-inventory?status=STORED', theme: GREEN },
{ key: 'reserved', label: 'Reserved', icon: <ClipboardCheck size={22} />, to: '/dashboard/warehouse-inventory?status=RESERVED', theme: ORANGE },
{ key: 'readyForLoading', label: 'Ready For Loading', icon: <PackageCheck size={22} />, to: '/dashboard/loading-queue', theme: GREEN },
{ key: 'loaded', label: 'Loaded', icon: <Truck size={22} />, to: '/dashboard/loaded-inventory', theme: ORANGE },
{ key: 'dispatched', label: 'Dispatched', icon: <Send size={22} />, to: '/dashboard/dispatch-queue', theme: GREEN },
{ key: 'readyForPickup', label: 'Ready For Pickup', icon: <PackageSearch size={22} />, to: '/dashboard/warehouse-inventory?status=READY_FOR_PICKUP', theme: ORANGE },
{ key: 'emptyContainers', label: 'Empty Containers', icon: <PackageOpen size={22} />, to: '/dashboard/containers', theme: ORANGE },
{ key: 'importTrains', label: 'Import Trains', icon: <Train size={22} />, to: '/dashboard/import-warehouse', theme: GREEN },
{ key: 'exportTrains', label: 'Export Trains', icon: <Train size={22} />, to: '/dashboard/export-warehouse', theme: ORANGE },
{ key: 'loaded', label: 'Loaded', icon: <Truck size={22} />, to: '/dashboard/loaded-inventory', theme: GREEN },
{ key: 'dispatched', label: 'Dispatched', icon: <Send size={22} />, to: '/dashboard/dispatch-queue', theme: ORANGE },
{ key: 'readyForPickup', label: 'Ready For Pickup', icon: <PackageSearch size={22} />, to: '/dashboard/warehouse-inventory?status=READY_FOR_PICKUP', theme: GREEN },
{ key: 'readyForLoading', label: 'Ready For Loading', icon: <PackageCheck size={22} />, to: '/dashboard/loading-queue', theme: ORANGE },
{ key: 'delivered', label: 'Delivered', icon: <CircleCheck size={22} />, to: '/dashboard/warehouse-inventory?status=DELIVERED', theme: GREEN },
];

View File

@@ -299,6 +299,9 @@ export interface WarehouseDashboard {
dispatched: number;
readyForPickup: number;
delivered: number;
emptyContainers: number;
importTrains: number;
exportTrains: number;
}
// ── Loading (Batch 3) ────────────────────────────────────────────────────────