mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 08:48:11 +00:00
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.
154 lines
4.6 KiB
TypeScript
154 lines
4.6 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { DataSource, FindManyOptions, IsNull, ObjectLiteral, Repository } from 'typeorm';
|
|
|
|
import { Warehouse } from './entities/warehouse.entity';
|
|
import { WarehouseInventory } from './entities/warehouse-inventory.entity';
|
|
import { SchedulingReadFacade } from './scheduling-read.facade';
|
|
|
|
export interface WarehouseDashboard {
|
|
totalWarehouses: number;
|
|
totalInventory: number;
|
|
receivedToday: number;
|
|
// Inspection gate
|
|
awaitingInspection: number;
|
|
inspected: number;
|
|
// Export branch
|
|
stored: number;
|
|
reserved: number;
|
|
readyForLoading: number;
|
|
loaded: number;
|
|
dispatched: number;
|
|
// Import branch
|
|
readyForPickup: number;
|
|
delivered: number;
|
|
// Fleet / train snapshot
|
|
emptyContainers: number;
|
|
importTrains: number;
|
|
exportTrains: number;
|
|
}
|
|
|
|
@Injectable()
|
|
export class WarehouseDashboardService {
|
|
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>,
|
|
options?: FindManyOptions<T>,
|
|
): Promise<number> {
|
|
try {
|
|
return await repo.count(options);
|
|
} catch {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
private async safeReceivedToday(startOfToday: Date): Promise<number> {
|
|
try {
|
|
return await this.dataSource
|
|
.getRepository(WarehouseInventory)
|
|
.createQueryBuilder('inv')
|
|
.where('inv.arrived_at >= :start', { start: startOfToday })
|
|
.getCount();
|
|
} catch {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
async getDashboard(): Promise<WarehouseDashboard> {
|
|
const warehouseRepo = this.dataSource.getRepository(Warehouse);
|
|
const inventoryRepo = this.dataSource.getRepository(WarehouseInventory);
|
|
|
|
const startOfToday = new Date();
|
|
startOfToday.setHours(0, 0, 0, 0);
|
|
|
|
const [
|
|
totalWarehouses,
|
|
totalInventory,
|
|
awaitingInspection,
|
|
inspected,
|
|
stored,
|
|
reserved,
|
|
readyForLoading,
|
|
loaded,
|
|
dispatched,
|
|
readyForPickup,
|
|
delivered,
|
|
receivedToday,
|
|
emptyContainers,
|
|
importTrains,
|
|
exportTrains,
|
|
] = await Promise.all([
|
|
this.safeCount(warehouseRepo),
|
|
this.safeCount(inventoryRepo),
|
|
this.safeCount(inventoryRepo, { where: { status: 'RECEIVED', inspectionStatus: IsNull() } }),
|
|
this.safeCount(inventoryRepo, { where: { inspectionStatus: 'PASSED' } }),
|
|
this.safeCount(inventoryRepo, { where: { status: 'STORED' } }),
|
|
this.safeCount(inventoryRepo, { where: { status: 'RESERVED' } }),
|
|
this.safeCount(inventoryRepo, { where: { status: 'READY_FOR_LOADING' } }),
|
|
this.safeCount(inventoryRepo, { where: { status: 'LOADED' } }),
|
|
this.safeCount(inventoryRepo, { where: { status: 'DISPATCHED' } }),
|
|
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 {
|
|
totalWarehouses,
|
|
totalInventory,
|
|
receivedToday,
|
|
awaitingInspection,
|
|
inspected,
|
|
stored,
|
|
reserved,
|
|
readyForLoading,
|
|
loaded,
|
|
dispatched,
|
|
readyForPickup,
|
|
delivered,
|
|
emptyContainers,
|
|
importTrains,
|
|
exportTrains,
|
|
};
|
|
}
|
|
}
|