mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
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:
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
];
|
||||
|
||||
|
||||
@@ -299,6 +299,9 @@ export interface WarehouseDashboard {
|
||||
dispatched: number;
|
||||
readyForPickup: number;
|
||||
delivered: number;
|
||||
emptyContainers: number;
|
||||
importTrains: number;
|
||||
exportTrains: number;
|
||||
}
|
||||
|
||||
// ── Loading (Batch 3) ────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user