mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 21:15:41 +00:00
Backs the previously-dead header date-picker and Filters button with an actual scoped query. getDashboard() takes optional dateFrom/dateTo/ warehouseId: received (renamed from receivedToday) is the only activity counter and respects the date range, defaulting to today when omitted; the 10 status-backlog counters respect warehouseId only; totalWarehouses/emptyContainers/importTrains/exportTrains stay global since nothing ties them to a specific warehouse in the schema. Adds a warehouse Select + date-range picker to the dashboard header, with a caption spelling out what is and isn't scoped.
178 lines
6.2 KiB
TypeScript
178 lines
6.2 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 WarehouseDashboardFilter {
|
|
/** Inclusive day, `YYYY-MM-DD`. Both omitted → defaults to "today" (the original behaviour). */
|
|
dateFrom?: string;
|
|
dateTo?: string;
|
|
/** Scopes every warehouse_inventory-derived counter. Ignored by the always-global ones (see below). */
|
|
warehouseId?: string;
|
|
}
|
|
|
|
export interface WarehouseDashboard {
|
|
// ── Always current — dateFrom/dateTo have no effect on these ──────────────
|
|
totalWarehouses: number;
|
|
totalInventory: number;
|
|
awaitingInspection: number;
|
|
inspected: number;
|
|
stored: number;
|
|
reserved: number;
|
|
readyForLoading: number;
|
|
loaded: number;
|
|
dispatched: number;
|
|
readyForPickup: number;
|
|
delivered: number;
|
|
/** Never warehouse-scoped — the container fleet isn't tied to a specific warehouse. */
|
|
emptyContainers: number;
|
|
/** Never warehouse-scoped — trains aren't tied to a specific warehouse. */
|
|
importTrains: number;
|
|
exportTrains: number;
|
|
// ── The one activity counter — respects dateFrom/dateTo (default: today) ──
|
|
received: 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;
|
|
}
|
|
}
|
|
|
|
/** [inclusive start, exclusive end) for the "received" counter. Defaults to today. */
|
|
private resolveRange(filter: WarehouseDashboardFilter): { start: Date; end: Date } {
|
|
if (!filter.dateFrom && !filter.dateTo) {
|
|
const start = new Date();
|
|
start.setHours(0, 0, 0, 0);
|
|
return { start, end: new Date() };
|
|
}
|
|
const start = filter.dateFrom ? new Date(`${filter.dateFrom}T00:00:00`) : new Date(0);
|
|
// Exclusive end = start of the day AFTER dateTo, so the whole end day is included.
|
|
const end = filter.dateTo
|
|
? new Date(new Date(`${filter.dateTo}T00:00:00`).getTime() + 24 * 60 * 60 * 1000)
|
|
: new Date();
|
|
return { start, end };
|
|
}
|
|
|
|
private async safeReceived(range: { start: Date; end: Date }, warehouseId?: string): Promise<number> {
|
|
try {
|
|
const qb = this.dataSource
|
|
.getRepository(WarehouseInventory)
|
|
.createQueryBuilder('inv')
|
|
.where('inv.arrived_at >= :start AND inv.arrived_at < :end', range);
|
|
if (warehouseId) qb.andWhere('inv.warehouse_id = :warehouseId', { warehouseId });
|
|
return await qb.getCount();
|
|
} catch {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
async getDashboard(filter: WarehouseDashboardFilter = {}): Promise<WarehouseDashboard> {
|
|
const warehouseRepo = this.dataSource.getRepository(Warehouse);
|
|
const inventoryRepo = this.dataSource.getRepository(WarehouseInventory);
|
|
const warehouseId = filter.warehouseId || undefined;
|
|
const scope = warehouseId ? { warehouseId } : {};
|
|
const range = this.resolveRange(filter);
|
|
|
|
const [
|
|
totalWarehouses,
|
|
totalInventory,
|
|
awaitingInspection,
|
|
inspected,
|
|
stored,
|
|
reserved,
|
|
readyForLoading,
|
|
loaded,
|
|
dispatched,
|
|
readyForPickup,
|
|
delivered,
|
|
received,
|
|
emptyContainers,
|
|
importTrains,
|
|
exportTrains,
|
|
] = await Promise.all([
|
|
this.safeCount(warehouseRepo),
|
|
this.safeCount(inventoryRepo, { where: { ...scope } }),
|
|
this.safeCount(inventoryRepo, { where: { ...scope, status: 'RECEIVED', inspectionStatus: IsNull() } }),
|
|
this.safeCount(inventoryRepo, { where: { ...scope, inspectionStatus: 'PASSED' } }),
|
|
this.safeCount(inventoryRepo, { where: { ...scope, status: 'STORED' } }),
|
|
this.safeCount(inventoryRepo, { where: { ...scope, status: 'RESERVED' } }),
|
|
this.safeCount(inventoryRepo, { where: { ...scope, status: 'READY_FOR_LOADING' } }),
|
|
this.safeCount(inventoryRepo, { where: { ...scope, status: 'LOADED' } }),
|
|
this.safeCount(inventoryRepo, { where: { ...scope, status: 'DISPATCHED' } }),
|
|
this.safeCount(inventoryRepo, { where: { ...scope, status: 'READY_FOR_PICKUP' } }),
|
|
this.safeCount(inventoryRepo, { where: { ...scope, status: 'DELIVERED' } }),
|
|
this.safeReceived(range, warehouseId),
|
|
// 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,
|
|
received,
|
|
awaitingInspection,
|
|
inspected,
|
|
stored,
|
|
reserved,
|
|
readyForLoading,
|
|
loaded,
|
|
dispatched,
|
|
readyForPickup,
|
|
delivered,
|
|
emptyContainers,
|
|
importTrains,
|
|
exportTrains,
|
|
};
|
|
}
|
|
}
|