From 7f772efcb30b56ff1bc267f43f47a93b411a1691 Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Thu, 20 Aug 2026 13:29:37 +0000 Subject: [PATCH] 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. --- .../warehouses/warehouse-dashboard.service.ts | 76 ++++++++++----- .../warehouses/warehouses.controller.ts | 17 +++- .../backoffice/src/hooks/useWarehouses.ts | 7 +- .../warehouses/WarehouseDashboardPage.tsx | 96 ++++++++++++++----- .../src/services/warehouse.service.ts | 6 +- .../backoffice/src/types/warehouse.ts | 10 +- 6 files changed, 155 insertions(+), 57 deletions(-) diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-dashboard.service.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-dashboard.service.ts index 3eee095f8..8d7d60d49 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-dashboard.service.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-dashboard.service.ts @@ -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 { + /** [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 { 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 { + async getDashboard(filter: WarehouseDashboardFilter = {}): Promise { 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, diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouses.controller.ts b/apps/edr-freight-api/src/modules/warehouses/warehouses.controller.ts index f3d894dfc..275afadf6 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouses.controller.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouses.controller.ts @@ -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() diff --git a/apps/edr-freight-web/backoffice/src/hooks/useWarehouses.ts b/apps/edr-freight-web/backoffice/src/hooks/useWarehouses.ts index 54f1cc4d3..bf19542b4 100644 --- a/apps/edr-freight-web/backoffice/src/hooks/useWarehouses.ts +++ b/apps/edr-freight-web/backoffice/src/hooks/useWarehouses.ts @@ -20,6 +20,7 @@ import type { SaveWarehousePayload, SaveYardPayload, SaveZonePayload, + WarehouseDashboardFilter, WarehouseFilter, } from '@/types/warehouse'; @@ -461,10 +462,10 @@ export function useInventoryActivity(id?: string) { }); } -export function useWarehouseDashboard() { +export function useWarehouseDashboard(filter?: WarehouseDashboardFilter) { return useQuery({ - queryKey: ['warehouses', 'dashboard'], - queryFn: () => warehouseService.dashboard().then((r) => r.data), + queryKey: ['warehouses', 'dashboard', filter ?? {}], + queryFn: () => warehouseService.dashboard(filter).then((r) => r.data), refetchInterval: DASHBOARD_REFETCH_MS, }); } diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseDashboardPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseDashboardPage.tsx index 93dd830a2..c63852caa 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseDashboardPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/WarehouseDashboardPage.tsx @@ -1,5 +1,7 @@ +import { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Badge, Card, Center, Divider, Group, Loader, SimpleGrid, Stack, Text, ThemeIcon } from '@mantine/core'; +import { Badge, Card, Center, Divider, Group, Loader, Select, SimpleGrid, Stack, Text, ThemeIcon } from '@mantine/core'; +import { DatePickerInput } from '@mantine/dates'; import { ClipboardList, PackageCheck, @@ -15,6 +17,7 @@ import { } from 'lucide-react'; import { PageContainer, PageHeader } from '@/components/page'; +import { getDateRangePresets } from '@/components/common/dateRangePresets'; import { AccrualDashboard, CycleTimeCard, @@ -24,7 +27,7 @@ import { WarehouseOpsKpiStrip, ZoneOccupancyHeatmap, } from '@/components/warehouses'; -import { useWarehouseDashboard } from '@/hooks/useWarehouses'; +import { useWarehouseDashboard, useWarehouses } from '@/hooks/useWarehouses'; import type { WarehouseDashboard } from '@/types/warehouse'; function SectionTitle({ children }: { children: React.ReactNode }) { @@ -50,7 +53,7 @@ const GREEN = '#084b21'; const METRICS: Metric[] = [ { key: 'totalWarehouses', label: 'Total Warehouses', icon: , to: '/dashboard/warehouses', theme: ORANGE }, { key: 'totalInventory', label: 'Total Inventory', icon: , to: '/dashboard/warehouse-inventory', theme: GREEN }, - { key: 'receivedToday', label: 'Received Today', icon: , to: '/dashboard/warehouse-inventory?status=RECEIVED', theme: ORANGE }, + { key: 'received', label: 'Received Today', icon: , to: '/dashboard/warehouse-inventory?status=RECEIVED', theme: ORANGE }, { key: 'awaitingInspection', label: 'Awaiting Inspection', icon: , to: '/dashboard/warehouse-inventory?status=RECEIVED', theme: GREEN }, { key: 'emptyContainers', label: 'Empty Containers', icon: , to: '/dashboard/containers', theme: ORANGE }, { key: 'importTrains', label: 'Import Trains', icon: , to: '/dashboard/import-warehouse', theme: GREEN }, @@ -64,7 +67,23 @@ const METRICS: Metric[] = [ export default function WarehouseDashboardPage() { const navigate = useNavigate(); - const { data, isError, isLoading } = useWarehouseDashboard(); + // Both null → the API defaults `received` to "today", matching the page's original behaviour. + const [dateRange, setDateRange] = useState<[string | null, string | null]>([null, null]); + const [warehouseId, setWarehouseId] = useState(null); + const [dateFrom, dateTo] = dateRange; + const hasCustomRange = Boolean(dateFrom || dateTo); + + const warehousesQuery = useWarehouses(); + const warehouseOptions = useMemo( + () => (warehousesQuery.data ?? []).map((w) => ({ value: w.id, label: `${w.name} (${w.code})` })), + [warehousesQuery.data], + ); + + const { data, isError, isLoading } = useWarehouseDashboard({ + dateFrom: dateFrom ?? undefined, + dateTo: dateTo ?? undefined, + warehouseId: warehouseId ?? undefined, + }); return ( @@ -72,27 +91,58 @@ export default function WarehouseDashboardPage() { title="Warehouse Dashboard" subtitle="Live overview of warehouse capacity and inventory lifecycle." action={ - - } - > - Live · updates every 60s - + +