feat(warehouses): make dashboard date range + warehouse filter real

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.
This commit is contained in:
Hagernesh
2026-08-20 13:29:37 +00:00
parent 572b42343f
commit 7f772efcb3
6 changed files with 155 additions and 57 deletions

View File

@@ -5,26 +5,34 @@ 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;
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
/** 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()
@@ -72,24 +80,40 @@ export class WarehouseDashboardService {
}
}
private async safeReceivedToday(startOfToday: Date): Promise<number> {
/** [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 {
return await this.dataSource
const qb = this.dataSource
.getRepository(WarehouseInventory)
.createQueryBuilder('inv')
.where('inv.arrived_at >= :start', { start: startOfToday })
.getCount();
.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(): Promise<WarehouseDashboard> {
async getDashboard(filter: WarehouseDashboardFilter = {}): 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 warehouseId = filter.warehouseId || undefined;
const scope = warehouseId ? { warehouseId } : {};
const range = this.resolveRange(filter);
const [
totalWarehouses,
@@ -103,23 +127,23 @@ export class WarehouseDashboardService {
dispatched,
readyForPickup,
delivered,
receivedToday,
received,
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),
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
@@ -135,7 +159,7 @@ export class WarehouseDashboardService {
return {
totalWarehouses,
totalInventory,
receivedToday,
received,
awaitingInspection,
inspected,
stored,

View File

@@ -43,9 +43,20 @@ export class WarehousesController {
@Get('dashboard')
@BookingStaff(FREIGHT_PERMS.warehouseDashboard.view)
@ApiOperation({ summary: 'Warehouse dashboard metrics' })
dashboard() {
return this.dashboardService.getDashboard();
@ApiOperation({
summary: 'Warehouse dashboard metrics',
description:
'dateFrom/dateTo scope only the activity counters (currently just "received"); ' +
'status-backlog and fleet counters are always current. Omit both for "received today" ' +
'(the original default). warehouseId scopes every warehouse_inventory-derived counter; ' +
'totalWarehouses/emptyContainers/importTrains/exportTrains are never warehouse-scoped.',
})
dashboard(
@Query('dateFrom') dateFrom?: string,
@Query('dateTo') dateTo?: string,
@Query('warehouseId') warehouseId?: string,
) {
return this.dashboardService.getDashboard({ dateFrom, dateTo, warehouseId });
}
@Post()