Files
edr-platform/apps/edr-freight-api/src/modules/warehouses/warehouse-dashboard.service.ts
Hagernesh 0066a87972 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.
2026-08-20 10:46:42 +00:00

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,
};
}
}