From a542a1389396eff4a092f281e845fe4fd9275d7e Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Mon, 13 Jul 2026 13:38:28 +0000 Subject: [PATCH] wAREHOUSE kPI strips --- .../warehouse-inventory.controller.ts | 6 +++ .../warehouses/warehouse-inventory.service.ts | 39 ++++++++++++++++ .../warehouses/WarehouseOpsKpiStrip.tsx | 45 +++++++++++++++++++ .../src/components/warehouses/index.ts | 1 + .../backoffice/src/constants/URLS.ts | 1 + .../backoffice/src/hooks/useWarehouses.ts | 8 ++++ .../src/pages/warehouses/ArrivalQueuePage.tsx | 3 ++ .../ExportDjiboutiUnloadingQueuePage.tsx | 3 ++ .../src/pages/warehouses/LoadingQueuePage.tsx | 3 ++ .../src/services/warehouse.service.ts | 3 ++ .../backoffice/src/types/warehouse.ts | 8 ++++ 11 files changed, 120 insertions(+) create mode 100644 apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseOpsKpiStrip.tsx diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.controller.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.controller.ts index 21f2663b6..dcd32a10a 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.controller.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.controller.ts @@ -52,6 +52,12 @@ export class WarehouseInventoryController { return this.inventoryService.arrivalQueue(); } + @Get('ops-stats') + @ApiOperation({ summary: 'At-a-glance warehouse ops counters for the KPI strip' }) + opsStats() { + return this.inventoryService.opsStats(); + } + @Get('zone-occupancy') @ApiOperation({ summary: 'Live occupancy per zone (rated capacity vs held) — heatmap data' }) zoneOccupancy(@Query('yardId') yardId?: string) { diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts index 7c8edcce0..a96b3f804 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts @@ -397,6 +397,45 @@ export class WarehouseInventoryService { * but has no customer truck assigned yet, nudge the customer to assign one — with * a deep-link to the booking's truck-assignment card. Fire-and-forget. */ + /** + * At-a-glance warehouse ops counters for the KPI strip: + * - receivedToday: items received today + * - pendingInspection: RECEIVED items not yet inspected + * - trucksOnSite: customer trucks arrived but not departed + * - itemsAging: in-warehouse items older than 7 days (demurrage risk) + */ + async opsStats(): Promise<{ + receivedToday: number; + pendingInspection: number; + trucksOnSite: number; + itemsAging: number; + }> { + const [row]: Array<{ + receivedToday: number; + pendingInspection: number; + trucksOnSite: number; + itemsAging: number; + }> = await this.dataSource.query( + `SELECT + (SELECT count(*)::int FROM freight.warehouse_inventory + WHERE deleted_at IS NULL AND created_at::date = CURRENT_DATE) AS "receivedToday", + (SELECT count(*)::int FROM freight.warehouse_inventory + WHERE deleted_at IS NULL AND status = 'RECEIVED' AND inspection_status IS NULL) AS "pendingInspection", + (SELECT count(*)::int FROM freight.customer_truck_assignments + WHERE deleted_at IS NULL AND arrived_at IS NOT NULL AND departed_at IS NULL) AS "trucksOnSite", + (SELECT count(*)::int FROM freight.warehouse_inventory + WHERE deleted_at IS NULL + AND status IN ('RECEIVED', 'STORED', 'READY_FOR_LOADING', 'READY_FOR_PICKUP', 'RESERVED') + AND created_at < now() - interval '7 days') AS "itemsAging"`, + ); + return { + receivedToday: row?.receivedToday ?? 0, + pendingInspection: row?.pendingInspection ?? 0, + trucksOnSite: row?.trucksOnSite ?? 0, + itemsAging: row?.itemsAging ?? 0, + }; + } + /** * Live occupancy per zone: rated capacity vs the weight/items currently held * (excludes items that have left — DELIVERED/DISPATCHED). Powers the yard diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseOpsKpiStrip.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseOpsKpiStrip.tsx new file mode 100644 index 000000000..cf8e34343 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseOpsKpiStrip.tsx @@ -0,0 +1,45 @@ +import { AlertTriangle, ClipboardCheck, PackageCheck, Truck } from "lucide-react"; + +import { KpiStrip } from "@/components/page"; +import { useWarehouseOpsStats } from "@/hooks/useWarehouses"; + +/** + * At-a-glance warehouse ops KPIs (received today, pending inspection, trucks + * on-site, items aging). Drop-in for any warehouse ops page header. + */ +export function WarehouseOpsKpiStrip() { + const { data, isLoading } = useWarehouseOpsStats(); + + return ( + 7d)", + value: data?.itemsAging ?? 0, + icon: AlertTriangle, + color: (data?.itemsAging ?? 0) > 0 ? "red" : "edr-green", + hint: "In warehouse over 7 days", + }, + ]} + /> + ); +} diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/index.ts b/apps/edr-freight-web/backoffice/src/components/warehouses/index.ts index 075237198..da20d6fa7 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/index.ts +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/index.ts @@ -30,3 +30,4 @@ export { WarehouseDashboardCharts } from './WarehouseDashboardCharts'; export { InspectionReportModal } from './InspectionReportModal'; export { FeePreviewModal } from './FeePreviewModal'; export { ZoneOccupancyHeatmap } from './ZoneOccupancyHeatmap'; +export { WarehouseOpsKpiStrip } from './WarehouseOpsKpiStrip'; diff --git a/apps/edr-freight-web/backoffice/src/constants/URLS.ts b/apps/edr-freight-web/backoffice/src/constants/URLS.ts index 2eaee8090..f76dbbde2 100644 --- a/apps/edr-freight-web/backoffice/src/constants/URLS.ts +++ b/apps/edr-freight-web/backoffice/src/constants/URLS.ts @@ -486,6 +486,7 @@ export const URL_CONSTANTS = { MOVE: (id: string) => `/warehouse-inventory/${id}/move`, RESERVE: "/warehouse-inventory/reserve", ARRIVAL_QUEUE: "/warehouse-inventory/arrival-queue", + OPS_STATS: "/warehouse-inventory/ops-stats", ZONE_OCCUPANCY: (yardId?: string) => yardId ? `/warehouse-inventory/zone-occupancy?yardId=${yardId}` diff --git a/apps/edr-freight-web/backoffice/src/hooks/useWarehouses.ts b/apps/edr-freight-web/backoffice/src/hooks/useWarehouses.ts index 59709d2d7..da1bfb867 100644 --- a/apps/edr-freight-web/backoffice/src/hooks/useWarehouses.ts +++ b/apps/edr-freight-web/backoffice/src/hooks/useWarehouses.ts @@ -144,6 +144,14 @@ export function useZoneOccupancy(yardId?: string) { }); } +/** At-a-glance warehouse ops counters for the KPI strip. */ +export function useWarehouseOpsStats() { + return useQuery({ + queryKey: ['warehouse-inventory', 'ops-stats'], + queryFn: () => warehouseService.opsStats().then((r) => r.data), + }); +} + export function useCreateZone() { const qc = useQueryClient(); return useMutation({ diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/ArrivalQueuePage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/ArrivalQueuePage.tsx index 821589271..61afa426c 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/ArrivalQueuePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/ArrivalQueuePage.tsx @@ -15,6 +15,7 @@ import { ChevronDown, ChevronRight, PackageOpen, Truck } from 'lucide-react'; import { PageContainer, PageHeader } from '@/components/page'; import { VisualEmptyState, + WarehouseOpsKpiStrip, formatDate, formatNumber, } from '@/components/warehouses'; @@ -291,6 +292,8 @@ export default function ArrivalQueuePage() { breadcrumbs={[{ label: 'Arrival queue' }]} /> + + diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/ExportDjiboutiUnloadingQueuePage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/ExportDjiboutiUnloadingQueuePage.tsx index 6e694ed1e..735026fa7 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/ExportDjiboutiUnloadingQueuePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/ExportDjiboutiUnloadingQueuePage.tsx @@ -29,6 +29,7 @@ import { ActivityTimeline, InventoryMovementHistoryTable, VisualEmptyState, + WarehouseOpsKpiStrip, formatDate, formatNumber, } from '@/components/warehouses'; @@ -292,6 +293,8 @@ export default function ExportDjiboutiUnloadingQueuePage() { breadcrumbs={[{ label: 'Djibouti Arrival / Unloading Queue' }]} /> + + {trains.length} arrived export train(s) diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/LoadingQueuePage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/LoadingQueuePage.tsx index e9bb3a162..a03c7038e 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/LoadingQueuePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/LoadingQueuePage.tsx @@ -8,6 +8,7 @@ import { PageContainer, PageHeader } from '@/components/page'; import { InventoryWorkbench, VisualEmptyState, + WarehouseOpsKpiStrip, formatNumber, } from '@/components/warehouses'; import { LoadToTrainPanel } from '@/components/warehouses/LoadToTrainPanel'; @@ -74,6 +75,8 @@ export default function LoadingQueuePage() { } /> + + diff --git a/apps/edr-freight-web/backoffice/src/services/warehouse.service.ts b/apps/edr-freight-web/backoffice/src/services/warehouse.service.ts index ea0baa32e..08aa01476 100644 --- a/apps/edr-freight-web/backoffice/src/services/warehouse.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/warehouse.service.ts @@ -5,6 +5,7 @@ import { api as apiClient } from '../auth/http'; import { URL_CONSTANTS } from '@/constants/URLS'; import type { ZoneOccupancy, + WarehouseOpsStats, AllocationCriteria, AllocationPreviewResult, AllocationRule, @@ -370,6 +371,8 @@ export const warehouseService = { apiClient.get( URL_CONSTANTS.WAREHOUSE_INVENTORY.ZONE_OCCUPANCY(yardId), ), + opsStats: () => + apiClient.get(URL_CONSTANTS.WAREHOUSE_INVENTORY.OPS_STATS), autoUnloadArrived: () => apiClient.post(URL_CONSTANTS.WAREHOUSE_INVENTORY.AUTO_UNLOAD_ARRIVED), autoLoadReady: () => diff --git a/apps/edr-freight-web/backoffice/src/types/warehouse.ts b/apps/edr-freight-web/backoffice/src/types/warehouse.ts index 4023bf986..eb04c39bc 100644 --- a/apps/edr-freight-web/backoffice/src/types/warehouse.ts +++ b/apps/edr-freight-web/backoffice/src/types/warehouse.ts @@ -1107,3 +1107,11 @@ export interface ZoneOccupancy { /** 0–100+, container-count based (weight is a rough fallback). Null if no capacity set. */ occupancyPct: number | null; } + +/** At-a-glance warehouse ops counters for the KPI strip. */ +export interface WarehouseOpsStats { + receivedToday: number; + pendingInspection: number; + trucksOnSite: number; + itemsAging: number; +}