wAREHOUSE kPI strips

This commit is contained in:
Hagernesh
2026-07-13 13:38:28 +00:00
parent 452355e292
commit a542a13893
11 changed files with 120 additions and 0 deletions

View File

@@ -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) {

View File

@@ -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

View File

@@ -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 (
<KpiStrip
loading={isLoading}
items={[
{
label: "Received today",
value: data?.receivedToday ?? 0,
icon: PackageCheck,
color: "edr-green",
},
{
label: "Pending inspection",
value: data?.pendingInspection ?? 0,
icon: ClipboardCheck,
color: "yellow",
},
{
label: "Trucks on-site",
value: data?.trucksOnSite ?? 0,
icon: Truck,
color: "blue",
},
{
label: "Items aging (>7d)",
value: data?.itemsAging ?? 0,
icon: AlertTriangle,
color: (data?.itemsAging ?? 0) > 0 ? "red" : "edr-green",
hint: "In warehouse over 7 days",
},
]}
/>
);
}

View File

@@ -30,3 +30,4 @@ export { WarehouseDashboardCharts } from './WarehouseDashboardCharts';
export { InspectionReportModal } from './InspectionReportModal';
export { FeePreviewModal } from './FeePreviewModal';
export { ZoneOccupancyHeatmap } from './ZoneOccupancyHeatmap';
export { WarehouseOpsKpiStrip } from './WarehouseOpsKpiStrip';

View File

@@ -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}`

View File

@@ -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({

View File

@@ -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' }]}
/>
<WarehouseOpsKpiStrip />
<Card withBorder radius="md" padding="lg">
<Group justify="space-between" mb="md">
<Stack gap={2}>

View File

@@ -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' }]}
/>
<WarehouseOpsKpiStrip />
<Card withBorder radius="md" padding="lg">
<Group justify="space-between" mb="md">
<Text fw={600}>{trains.length} arrived export train(s)</Text>

View File

@@ -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() {
}
/>
<WarehouseOpsKpiStrip />
<Card>
<Tabs defaultValue="ready">
<Tabs.List>

View File

@@ -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<ZoneOccupancy[]>(
URL_CONSTANTS.WAREHOUSE_INVENTORY.ZONE_OCCUPANCY(yardId),
),
opsStats: () =>
apiClient.get<WarehouseOpsStats>(URL_CONSTANTS.WAREHOUSE_INVENTORY.OPS_STATS),
autoUnloadArrived: () =>
apiClient.post<AutoUnloadResult>(URL_CONSTANTS.WAREHOUSE_INVENTORY.AUTO_UNLOAD_ARRIVED),
autoLoadReady: () =>

View File

@@ -1107,3 +1107,11 @@ export interface ZoneOccupancy {
/** 0100+, 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;
}