mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 20:05:41 +00:00
wAREHOUSE kPI strips
This commit is contained in:
@@ -52,6 +52,12 @@ export class WarehouseInventoryController {
|
|||||||
return this.inventoryService.arrivalQueue();
|
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')
|
@Get('zone-occupancy')
|
||||||
@ApiOperation({ summary: 'Live occupancy per zone (rated capacity vs held) — heatmap data' })
|
@ApiOperation({ summary: 'Live occupancy per zone (rated capacity vs held) — heatmap data' })
|
||||||
zoneOccupancy(@Query('yardId') yardId?: string) {
|
zoneOccupancy(@Query('yardId') yardId?: string) {
|
||||||
|
|||||||
@@ -397,6 +397,45 @@ export class WarehouseInventoryService {
|
|||||||
* but has no customer truck assigned yet, nudge the customer to assign one — with
|
* 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.
|
* 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
|
* Live occupancy per zone: rated capacity vs the weight/items currently held
|
||||||
* (excludes items that have left — DELIVERED/DISPATCHED). Powers the yard
|
* (excludes items that have left — DELIVERED/DISPATCHED). Powers the yard
|
||||||
|
|||||||
@@ -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",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -30,3 +30,4 @@ export { WarehouseDashboardCharts } from './WarehouseDashboardCharts';
|
|||||||
export { InspectionReportModal } from './InspectionReportModal';
|
export { InspectionReportModal } from './InspectionReportModal';
|
||||||
export { FeePreviewModal } from './FeePreviewModal';
|
export { FeePreviewModal } from './FeePreviewModal';
|
||||||
export { ZoneOccupancyHeatmap } from './ZoneOccupancyHeatmap';
|
export { ZoneOccupancyHeatmap } from './ZoneOccupancyHeatmap';
|
||||||
|
export { WarehouseOpsKpiStrip } from './WarehouseOpsKpiStrip';
|
||||||
|
|||||||
@@ -486,6 +486,7 @@ export const URL_CONSTANTS = {
|
|||||||
MOVE: (id: string) => `/warehouse-inventory/${id}/move`,
|
MOVE: (id: string) => `/warehouse-inventory/${id}/move`,
|
||||||
RESERVE: "/warehouse-inventory/reserve",
|
RESERVE: "/warehouse-inventory/reserve",
|
||||||
ARRIVAL_QUEUE: "/warehouse-inventory/arrival-queue",
|
ARRIVAL_QUEUE: "/warehouse-inventory/arrival-queue",
|
||||||
|
OPS_STATS: "/warehouse-inventory/ops-stats",
|
||||||
ZONE_OCCUPANCY: (yardId?: string) =>
|
ZONE_OCCUPANCY: (yardId?: string) =>
|
||||||
yardId
|
yardId
|
||||||
? `/warehouse-inventory/zone-occupancy?yardId=${yardId}`
|
? `/warehouse-inventory/zone-occupancy?yardId=${yardId}`
|
||||||
|
|||||||
@@ -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() {
|
export function useCreateZone() {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { ChevronDown, ChevronRight, PackageOpen, Truck } from 'lucide-react';
|
|||||||
import { PageContainer, PageHeader } from '@/components/page';
|
import { PageContainer, PageHeader } from '@/components/page';
|
||||||
import {
|
import {
|
||||||
VisualEmptyState,
|
VisualEmptyState,
|
||||||
|
WarehouseOpsKpiStrip,
|
||||||
formatDate,
|
formatDate,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
} from '@/components/warehouses';
|
} from '@/components/warehouses';
|
||||||
@@ -291,6 +292,8 @@ export default function ArrivalQueuePage() {
|
|||||||
breadcrumbs={[{ label: 'Arrival queue' }]}
|
breadcrumbs={[{ label: 'Arrival queue' }]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<WarehouseOpsKpiStrip />
|
||||||
|
|
||||||
<Card withBorder radius="md" padding="lg">
|
<Card withBorder radius="md" padding="lg">
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb="md">
|
||||||
<Stack gap={2}>
|
<Stack gap={2}>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
ActivityTimeline,
|
ActivityTimeline,
|
||||||
InventoryMovementHistoryTable,
|
InventoryMovementHistoryTable,
|
||||||
VisualEmptyState,
|
VisualEmptyState,
|
||||||
|
WarehouseOpsKpiStrip,
|
||||||
formatDate,
|
formatDate,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
} from '@/components/warehouses';
|
} from '@/components/warehouses';
|
||||||
@@ -292,6 +293,8 @@ export default function ExportDjiboutiUnloadingQueuePage() {
|
|||||||
breadcrumbs={[{ label: 'Djibouti Arrival / Unloading Queue' }]}
|
breadcrumbs={[{ label: 'Djibouti Arrival / Unloading Queue' }]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<WarehouseOpsKpiStrip />
|
||||||
|
|
||||||
<Card withBorder radius="md" padding="lg">
|
<Card withBorder radius="md" padding="lg">
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb="md">
|
||||||
<Text fw={600}>{trains.length} arrived export train(s)</Text>
|
<Text fw={600}>{trains.length} arrived export train(s)</Text>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { PageContainer, PageHeader } from '@/components/page';
|
|||||||
import {
|
import {
|
||||||
InventoryWorkbench,
|
InventoryWorkbench,
|
||||||
VisualEmptyState,
|
VisualEmptyState,
|
||||||
|
WarehouseOpsKpiStrip,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
} from '@/components/warehouses';
|
} from '@/components/warehouses';
|
||||||
import { LoadToTrainPanel } from '@/components/warehouses/LoadToTrainPanel';
|
import { LoadToTrainPanel } from '@/components/warehouses/LoadToTrainPanel';
|
||||||
@@ -74,6 +75,8 @@ export default function LoadingQueuePage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<WarehouseOpsKpiStrip />
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<Tabs defaultValue="ready">
|
<Tabs defaultValue="ready">
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { api as apiClient } from '../auth/http';
|
|||||||
import { URL_CONSTANTS } from '@/constants/URLS';
|
import { URL_CONSTANTS } from '@/constants/URLS';
|
||||||
import type {
|
import type {
|
||||||
ZoneOccupancy,
|
ZoneOccupancy,
|
||||||
|
WarehouseOpsStats,
|
||||||
AllocationCriteria,
|
AllocationCriteria,
|
||||||
AllocationPreviewResult,
|
AllocationPreviewResult,
|
||||||
AllocationRule,
|
AllocationRule,
|
||||||
@@ -370,6 +371,8 @@ export const warehouseService = {
|
|||||||
apiClient.get<ZoneOccupancy[]>(
|
apiClient.get<ZoneOccupancy[]>(
|
||||||
URL_CONSTANTS.WAREHOUSE_INVENTORY.ZONE_OCCUPANCY(yardId),
|
URL_CONSTANTS.WAREHOUSE_INVENTORY.ZONE_OCCUPANCY(yardId),
|
||||||
),
|
),
|
||||||
|
opsStats: () =>
|
||||||
|
apiClient.get<WarehouseOpsStats>(URL_CONSTANTS.WAREHOUSE_INVENTORY.OPS_STATS),
|
||||||
autoUnloadArrived: () =>
|
autoUnloadArrived: () =>
|
||||||
apiClient.post<AutoUnloadResult>(URL_CONSTANTS.WAREHOUSE_INVENTORY.AUTO_UNLOAD_ARRIVED),
|
apiClient.post<AutoUnloadResult>(URL_CONSTANTS.WAREHOUSE_INVENTORY.AUTO_UNLOAD_ARRIVED),
|
||||||
autoLoadReady: () =>
|
autoLoadReady: () =>
|
||||||
|
|||||||
@@ -1107,3 +1107,11 @@ export interface ZoneOccupancy {
|
|||||||
/** 0–100+, container-count based (weight is a rough fallback). Null if no capacity set. */
|
/** 0–100+, container-count based (weight is a rough fallback). Null if no capacity set. */
|
||||||
occupancyPct: number | null;
|
occupancyPct: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** At-a-glance warehouse ops counters for the KPI strip. */
|
||||||
|
export interface WarehouseOpsStats {
|
||||||
|
receivedToday: number;
|
||||||
|
pendingInspection: number;
|
||||||
|
trucksOnSite: number;
|
||||||
|
itemsAging: number;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user