diff --git a/apps/edr-freight-api/src/modules/warehouses/current-actor.util.ts b/apps/edr-freight-api/src/modules/warehouses/current-actor.util.ts new file mode 100644 index 000000000..f4d04a6a5 --- /dev/null +++ b/apps/edr-freight-api/src/modules/warehouses/current-actor.util.ts @@ -0,0 +1,13 @@ +import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type'; + +/** + * Human-readable actor label for audit stamps (`performed_by` / `moved_by`). + * Prefers a display name, then username/email, so the activity log shows a + * person rather than a UUID. Returns undefined when there is no authenticated + * user (internal/cron calls), letting callers fall back to their prior value. + */ +export function actorLabel(user?: TCurrentUser | null): string | undefined { + if (!user) return undefined; + const name = user.name?.en?.trim() || user.name?.am?.trim(); + return name || user.username || user.email || user.id || undefined; +} 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 f64fddb3a..499e40f2e 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 @@ -1,7 +1,10 @@ import { Body, Controller, Get, Param, ParseUUIDPipe, Patch, Post, Query, Request, Res } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import type { Response } from 'express'; +import { CurrentUser } from '@edr/api-common'; +import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type'; +import { actorLabel } from './current-actor.util'; import { BookingStaff } from '../../common/booking-guards'; import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry'; import { BulkReceiveDto } from './dto/bulk-receive.dto'; @@ -95,6 +98,13 @@ export class WarehouseInventoryController { return this.inventoryService.cycleStats(); } + @Get('gate-stats') + @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) + @ApiOperation({ summary: 'Gate/dock throughput: cleared today, turnaround, hourly clearances' }) + gateStats() { + return this.inventoryService.gateStats(); + } + @Post('auto-unload-arrived') @BookingStaff(FREIGHT_PERMS.warehouseInventory.unload) @ApiOperation({ summary: 'Bulk auto-unload all arrived bookings into the warehouse' }) @@ -120,7 +130,8 @@ export class WarehouseInventoryController { @Post('receive-bulk') @BookingStaff(FREIGHT_PERMS.warehouseInventory.receive) @ApiOperation({ summary: 'Bulk-receive selected eligible PAID bookings into a location' }) - receiveBulk(@Body() dto: BulkReceiveDto) { + receiveBulk(@Body() dto: BulkReceiveDto, @CurrentUser() user: TCurrentUser) { + dto.performedBy = actorLabel(user) ?? dto.performedBy; return this.inventoryService.bulkReceive(dto); } @@ -166,15 +177,16 @@ export class WarehouseInventoryController { loadItemsOntoTrain( @Param('scheduleId', ParseUUIDPipe) scheduleId: string, @Body() dto: { inventoryIds: string[]; performedBy?: string }, + @CurrentUser() user: TCurrentUser, ) { - return this.inventoryService.loadItemsOntoTrain(scheduleId, dto.inventoryIds ?? [], dto.performedBy); + return this.inventoryService.loadItemsOntoTrain(scheduleId, dto.inventoryIds ?? [], actorLabel(user) ?? dto.performedBy); } @Post('bulk-dispatch-export') @BookingStaff(FREIGHT_PERMS.warehouseInventory.dispatch) @ApiOperation({ summary: 'Bulk-dispatch loaded EXPORT inventory (LOADED → DISPATCHED)' }) - bulkDispatchExport(@Body() dto: { inventoryIds: string[]; performedBy?: string }) { - return this.inventoryService.bulkDispatchExport(dto.inventoryIds ?? [], dto.performedBy); + bulkDispatchExport(@Body() dto: { inventoryIds: string[]; performedBy?: string }, @CurrentUser() user: TCurrentUser) { + return this.inventoryService.bulkDispatchExport(dto.inventoryIds ?? [], actorLabel(user) ?? dto.performedBy); } @Post('bulk-mark-inspected') @@ -197,8 +209,12 @@ export class WarehouseInventoryController { @Post(':id/gate-clearance') @BookingStaff(FREIGHT_PERMS.warehouseInventory.gatePass) @ApiOperation({ summary: 'Final terminal release / gate clearance (blocked while fees unpaid)' }) - gateClearance(@Param('id', ParseUUIDPipe) id: string, @Body('performedBy') performedBy?: string) { - return this.inventoryService.gateClearance(id, performedBy); + gateClearance( + @Param('id', ParseUUIDPipe) id: string, + @Body('performedBy') performedBy: string | undefined, + @CurrentUser() user: TCurrentUser, + ) { + return this.inventoryService.gateClearance(id, actorLabel(user) ?? performedBy); } @Get('import/arrive-queue') @@ -223,10 +239,10 @@ export class WarehouseInventoryController { warehouseId?: string; performedBy?: string; assignments?: { bookingId: string; warehouseId: string; yardId: string; zoneId: string }[]; - }) { + }, @CurrentUser() user: TCurrentUser) { return this.inventoryService.autoUnloadArrivedBookings( dto.scheduleId, - dto.performedBy, + actorLabel(user) ?? dto.performedBy, dto.warehouseId, dto.assignments, ); @@ -268,8 +284,8 @@ export class WarehouseInventoryController { @Post('export/auto-unload-at-djibouti') @BookingStaff(FREIGHT_PERMS.warehouseInventory.unload) @ApiOperation({ summary: 'Unload all eligible export items assigned to an arrived Djibouti-side train' }) - autoUnloadExportAtDjibouti(@Body() dto: { scheduleId: string; performedBy?: string }) { - return this.inventoryService.autoUnloadExportAtDjibouti(dto.scheduleId, dto.performedBy); + autoUnloadExportAtDjibouti(@Body() dto: { scheduleId: string; performedBy?: string }, @CurrentUser() user: TCurrentUser) { + return this.inventoryService.autoUnloadExportAtDjibouti(dto.scheduleId, actorLabel(user) ?? dto.performedBy); } @Get('import/pickup-ready-queue') @@ -296,14 +312,16 @@ export class WarehouseInventoryController { @Post('receive') @BookingStaff(FREIGHT_PERMS.warehouseInventory.receive) @ApiOperation({ summary: 'Receive inventory at a warehouse location' }) - receive(@Body() dto: ReceiveWarehouseInventoryDto) { + receive(@Body() dto: ReceiveWarehouseInventoryDto, @CurrentUser() user: TCurrentUser) { + dto.performedBy = actorLabel(user) ?? dto.performedBy; return this.inventoryService.receive(dto); } @Post('reserve') @BookingStaff(FREIGHT_PERMS.warehouseInventory.move) @ApiOperation({ summary: 'Reserve stored inventory for a PAID booking' }) - reserve(@Body() dto: ReserveInventoryDto) { + reserve(@Body() dto: ReserveInventoryDto, @CurrentUser() user: TCurrentUser) { + dto.performedBy = actorLabel(user) ?? dto.performedBy; return this.inventoryService.reserve(dto); } @@ -338,15 +356,19 @@ export class WarehouseInventoryController { @Post(':id/store') @BookingStaff(FREIGHT_PERMS.warehouseInventory.move) @ApiOperation({ summary: 'Mark received inventory as STORED (optional explicit warehouse/yard/zone)' }) - store(@Param('id', ParseUUIDPipe) id: string, @Body() dto: StoreInventoryDto) { - return this.inventoryService.store(id, dto.performedBy, dto); + store(@Param('id', ParseUUIDPipe) id: string, @Body() dto: StoreInventoryDto, @CurrentUser() user: TCurrentUser) { + return this.inventoryService.store(id, actorLabel(user) ?? dto.performedBy, dto); } @Post(':id/ready-for-loading') @BookingStaff(FREIGHT_PERMS.warehouseInventory.move) @ApiOperation({ summary: 'Mark reserved inventory READY_FOR_LOADING' }) - readyForLoading(@Param('id', ParseUUIDPipe) id: string, @Body('performedBy') performedBy?: string) { - return this.inventoryService.readyForLoading(id, performedBy); + readyForLoading( + @Param('id', ParseUUIDPipe) id: string, + @Body('performedBy') performedBy: string | undefined, + @CurrentUser() user: TCurrentUser, + ) { + return this.inventoryService.readyForLoading(id, actorLabel(user) ?? performedBy); } @Post(':id/load') @@ -359,8 +381,12 @@ export class WarehouseInventoryController { @Post(':id/ready-for-pickup') @BookingStaff(FREIGHT_PERMS.warehouseInventory.move) @ApiOperation({ summary: 'Mark inspected IMPORT inventory READY_FOR_PICKUP' }) - readyForPickup(@Param('id', ParseUUIDPipe) id: string, @Body('performedBy') performedBy?: string) { - return this.inventoryService.readyForPickup(id, performedBy); + readyForPickup( + @Param('id', ParseUUIDPipe) id: string, + @Body('performedBy') performedBy: string | undefined, + @CurrentUser() user: TCurrentUser, + ) { + return this.inventoryService.readyForPickup(id, actorLabel(user) ?? performedBy); } @Post(':id/release') @@ -422,10 +448,11 @@ export class WarehouseInventoryController { @Param('bookingId', ParseUUIDPipe) bookingId: string, @Body() dto: ApproveDeliveryDto, @Request() req: { user?: { id?: string; sub?: string } }, + @CurrentUser() user: TCurrentUser, ) { return this.inventoryService.approveDeliveryForBooking( bookingId, - req.user?.id ?? req.user?.sub, + user?.id ?? req.user?.id ?? req.user?.sub, dto.signerName, ); } @@ -487,14 +514,19 @@ export class WarehouseInventoryController { @Post(':id/deliver') @BookingStaff(FREIGHT_PERMS.warehouseInventory.deliver) @ApiOperation({ summary: 'Deliver import goods to the customer + capture proof of delivery' }) - deliver(@Param('id', ParseUUIDPipe) id: string, @Body() dto: DeliverInventoryDto) { + deliver(@Param('id', ParseUUIDPipe) id: string, @Body() dto: DeliverInventoryDto, @CurrentUser() user: TCurrentUser) { + dto.performedBy = actorLabel(user) ?? dto.performedBy; return this.inventoryService.deliver(id, dto); } @Patch(':id/dispatch') @BookingStaff(FREIGHT_PERMS.warehouseInventory.dispatch) @ApiOperation({ summary: 'Mark loaded inventory DISPATCHED (left the terminal)' }) - dispatch(@Param('id', ParseUUIDPipe) id: string, @Body('performedBy') performedBy?: string) { - return this.inventoryService.dispatch(id, performedBy); + dispatch( + @Param('id', ParseUUIDPipe) id: string, + @Body('performedBy') performedBy: string | undefined, + @CurrentUser() user: TCurrentUser, + ) { + return this.inventoryService.dispatch(id, actorLabel(user) ?? performedBy); } } 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 9c269270a..6dd941d26 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 @@ -537,6 +537,58 @@ export class WarehouseInventoryService { }; } + /** + * Gate / dock throughput: items cleared through the gate today, the average + * arrival→gate-clearance turnaround (hours, last 30 days), and gate clearances + * bucketed per hour over the last 24 hours. Powers the gate throughput card. + */ + async gateStats(): Promise<{ + clearedToday: number; + avgTurnaroundHours: number | null; + byHour: Array<{ hour: string; count: number }>; + }> { + const [scalar]: Array<{ clearedToday: number; avgTurnaroundHours: number | null }> = + await this.dataSource.query( + `SELECT + count(*) FILTER (WHERE gate_cleared_at::date = CURRENT_DATE)::int AS "clearedToday", + round( + avg(EXTRACT(EPOCH FROM (gate_cleared_at - arrived_at)) / 3600.0) + FILTER ( + WHERE gate_cleared_at IS NOT NULL AND arrived_at IS NOT NULL + AND gate_cleared_at > now() - interval '30 days' + )::numeric, + 1 + )::float8 AS "avgTurnaroundHours" + FROM freight.warehouse_inventory + WHERE deleted_at IS NULL`, + ); + const byHour: Array<{ hour: string; count: number }> = await this.dataSource.query( + `WITH hours AS ( + SELECT gs AS h + FROM generate_series( + date_trunc('hour', now()) - interval '23 hours', + date_trunc('hour', now()), + interval '1 hour' + ) gs + ) + SELECT to_char(hours.h, 'HH24:00') AS hour, + COALESCE(g.cnt, 0)::int AS count + FROM hours + LEFT JOIN ( + SELECT date_trunc('hour', gate_cleared_at) AS ph, count(*) AS cnt + FROM freight.warehouse_inventory + WHERE deleted_at IS NULL AND gate_cleared_at IS NOT NULL + GROUP BY 1 + ) g ON g.ph = hours.h + ORDER BY hours.h`, + ); + return { + clearedToday: scalar?.clearedToday ?? 0, + avgTurnaroundHours: scalar?.avgTurnaroundHours ?? null, + byHour, + }; + } + /** * Received-vs-dispatched throughput as a server-side time series. Buckets by * date_trunc over the last N periods (8 weeks / 12 months / 5 years) with a diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-invoice.controller.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-invoice.controller.ts index 635ca1a10..4ad469ce0 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-invoice.controller.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-invoice.controller.ts @@ -1,7 +1,10 @@ import { Body, Controller, Get, Param, ParseUUIDPipe, Patch, Post, Query, Res } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import type { Response } from 'express'; +import { CurrentUser } from '@edr/api-common'; +import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type'; +import { actorLabel } from './current-actor.util'; import { BookingStaff } from '../../common/booking-guards'; import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry'; import { PayInvoiceDto as GatewayPayInvoiceDto } from '../billing/dto/pay-invoice.dto'; @@ -17,7 +20,8 @@ export class WarehouseInvoiceController { @Post('warehouse-inventory/:id/generate-fee-invoice') @BookingStaff(FREIGHT_PERMS.warehouseFeeInvoices.generate) @ApiOperation({ summary: 'Generate a warehouse fee invoice from Batch 5 fee calculation' }) - generate(@Param('id', ParseUUIDPipe) id: string, @Body() dto: GenerateInvoiceDto) { + generate(@Param('id', ParseUUIDPipe) id: string, @Body() dto: GenerateInvoiceDto, @CurrentUser() user: TCurrentUser) { + dto.performedBy = actorLabel(user) ?? dto.performedBy; return this.invoiceService.generateForInventory(id, dto); } diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/GateThroughputCard.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/GateThroughputCard.tsx new file mode 100644 index 000000000..dfd4574f5 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/GateThroughputCard.tsx @@ -0,0 +1,90 @@ +import { Card, Group, Loader, SimpleGrid, Stack, Text, ThemeIcon } from '@mantine/core'; +import { DoorOpen } from 'lucide-react'; +import { + Bar, + BarChart, + CartesianGrid, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; + +import { useWarehouseGateStats } from '@/hooks/useWarehouses'; + +/** + * Gate / dock throughput: items cleared through the gate today, the average + * arrival→gate-clearance turnaround, and clearances per hour over the last 24h. + */ +export function GateThroughputCard() { + const { data, isLoading } = useWarehouseGateStats(); + const byHour = data?.byHour ?? []; + const hasActivity = byHour.some((h) => h.count > 0); + + return ( + + + + + + + Gate & dock throughput + + Gate clearances over the last 24 hours + + + + + {isLoading ? ( + + + + ) : ( + + + + + Cleared today + + + {data?.clearedToday ?? 0} + + + through the gate + + + + + Avg turnaround + + + {data?.avgTurnaroundHours == null ? '—' : `${data.avgTurnaroundHours} h`} + + + arrival → gate (30d) + + + + + {hasActivity ? ( + + + + + + + + + + ) : ( + + + No gate clearances in the last 24 hours. + + + )} + + )} + + ); +} 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 78b9ef1f6..dae0e1d7f 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/index.ts +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/index.ts @@ -34,3 +34,4 @@ export { WarehouseOpsKpiStrip } from './WarehouseOpsKpiStrip'; export { AccrualDashboard } from './AccrualDashboard'; export { DwellAgingCard } from './DwellAgingCard'; export { CycleTimeCard } from './CycleTimeCard'; +export { GateThroughputCard } from './GateThroughputCard'; diff --git a/apps/edr-freight-web/backoffice/src/constants/URLS.ts b/apps/edr-freight-web/backoffice/src/constants/URLS.ts index 5b718dad2..87c30044c 100644 --- a/apps/edr-freight-web/backoffice/src/constants/URLS.ts +++ b/apps/edr-freight-web/backoffice/src/constants/URLS.ts @@ -494,6 +494,7 @@ export const URL_CONSTANTS = { `/warehouse-inventory/throughput?granularity=${granularity}`, DWELL_STATS: "/warehouse-inventory/dwell-stats", CYCLE_STATS: "/warehouse-inventory/cycle-stats", + GATE_STATS: "/warehouse-inventory/gate-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 a4aac4411..9327e5ee0 100644 --- a/apps/edr-freight-web/backoffice/src/hooks/useWarehouses.ts +++ b/apps/edr-freight-web/backoffice/src/hooks/useWarehouses.ts @@ -141,6 +141,7 @@ export function useZoneOccupancy(yardId?: string) { return useQuery({ queryKey: ['warehouse-zones', 'occupancy', yardId ?? 'all'], queryFn: () => warehouseService.zoneOccupancy(yardId).then((r) => r.data), + refetchInterval: DASHBOARD_REFETCH_MS, }); } @@ -149,14 +150,19 @@ export function useWarehouseOpsStats() { return useQuery({ queryKey: ['warehouse-inventory', 'ops-stats'], queryFn: () => warehouseService.opsStats().then((r) => r.data), + refetchInterval: DASHBOARD_REFETCH_MS, }); } +/** How often the live warehouse dashboard widgets auto-refresh (ms). */ +export const DASHBOARD_REFETCH_MS = 60_000; + /** Server-side received-vs-dispatched throughput time series. */ export function useWarehouseThroughput(granularity: 'week' | 'month' | 'year') { return useQuery({ queryKey: ['warehouse-inventory', 'throughput', granularity], queryFn: () => warehouseService.throughput(granularity).then((r) => r.data), + refetchInterval: DASHBOARD_REFETCH_MS, }); } @@ -165,6 +171,7 @@ export function useWarehouseDwellStats() { return useQuery({ queryKey: ['warehouse-inventory', 'dwell-stats'], queryFn: () => warehouseService.dwellStats().then((r) => r.data), + refetchInterval: DASHBOARD_REFETCH_MS, }); } @@ -173,6 +180,16 @@ export function useWarehouseCycleStats() { return useQuery({ queryKey: ['warehouse-inventory', 'cycle-stats'], queryFn: () => warehouseService.cycleStats().then((r) => r.data), + refetchInterval: DASHBOARD_REFETCH_MS, + }); +} + +/** Gate / dock throughput (cleared today, turnaround, hourly clearances). */ +export function useWarehouseGateStats() { + return useQuery({ + queryKey: ['warehouse-inventory', 'gate-stats'], + queryFn: () => warehouseService.gateStats().then((r) => r.data), + refetchInterval: DASHBOARD_REFETCH_MS, }); } @@ -181,6 +198,7 @@ export function useOnTimeDispatch() { return useQuery({ queryKey: ['warehouse-fees', 'on-time-dispatch'], queryFn: () => warehouseService.onTimeDispatch().then((r) => r.data), + refetchInterval: DASHBOARD_REFETCH_MS, }); } @@ -189,6 +207,7 @@ export function useAccrualDashboard(billingCurrency?: 'ETB' | 'USD') { return useQuery({ queryKey: ['warehouse-fees', 'accrual-dashboard', billingCurrency ?? 'USD'], queryFn: () => warehouseService.accrualDashboard(billingCurrency).then((r) => r.data), + refetchInterval: DASHBOARD_REFETCH_MS, }); } @@ -437,6 +456,7 @@ export function useWarehouseDashboard() { return useQuery({ queryKey: ['warehouses', 'dashboard'], queryFn: () => warehouseService.dashboard().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 f7932f7d6..d8298761a 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,5 @@ import { useNavigate } from 'react-router-dom'; -import { Card, Center, Divider, Group, Loader, SimpleGrid, Stack, Text, ThemeIcon } from '@mantine/core'; +import { Badge, Card, Center, Divider, Group, Loader, SimpleGrid, Stack, Text, ThemeIcon } from '@mantine/core'; import { ClipboardCheck, ClipboardList, @@ -20,6 +20,7 @@ import { AccrualDashboard, CycleTimeCard, DwellAgingCard, + GateThroughputCard, WarehouseDashboardCharts, WarehouseOpsKpiStrip, ZoneOccupancyHeatmap, @@ -71,6 +72,26 @@ export default function WarehouseDashboardPage() { + } + > + Live · updates every 60s + + } /> {isLoading ? ( @@ -129,9 +150,10 @@ export default function WarehouseDashboardPage() { Performance - + + 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 fdf259169..2788d875a 100644 --- a/apps/edr-freight-web/backoffice/src/services/warehouse.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/warehouse.service.ts @@ -10,6 +10,7 @@ import type { WarehouseDwellStats, WarehouseCycleStats, WarehouseOnTimeStats, + WarehouseGateStats, AccrualDashboardRow, AllocationCriteria, AllocationPreviewResult, @@ -406,6 +407,8 @@ export const warehouseService = { apiClient.get(URL_CONSTANTS.WAREHOUSE_INVENTORY.DWELL_STATS), cycleStats: () => apiClient.get(URL_CONSTANTS.WAREHOUSE_INVENTORY.CYCLE_STATS), + gateStats: () => + apiClient.get(URL_CONSTANTS.WAREHOUSE_INVENTORY.GATE_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 f85e66b62..197b1974f 100644 --- a/apps/edr-freight-web/backoffice/src/types/warehouse.ts +++ b/apps/edr-freight-web/backoffice/src/types/warehouse.ts @@ -1149,6 +1149,13 @@ export interface WarehouseOnTimeStats { onTimePct: number | null; } +/** Gate / dock throughput: cleared today, turnaround, and hourly clearances. */ +export interface WarehouseGateStats { + clearedToday: number; + avgTurnaroundHours: number | null; + byHour: Array<{ hour: string; count: number }>; +} + export type AccrualAlert = 'OK' | 'WARNING' | 'CHARGING'; /** One item's live fee accrual for the accrual dashboard. */