mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 14:08:11 +00:00
Warehouse zone occupancy heatmap
Visualize live per-zone occupancy on the warehouse detail page.
This commit is contained in:
@@ -52,6 +52,12 @@ export class WarehouseInventoryController {
|
||||
return this.inventoryService.arrivalQueue();
|
||||
}
|
||||
|
||||
@Get('zone-occupancy')
|
||||
@ApiOperation({ summary: 'Live occupancy per zone (rated capacity vs held) — heatmap data' })
|
||||
zoneOccupancy(@Query('yardId') yardId?: string) {
|
||||
return this.inventoryService.zoneOccupancy(yardId);
|
||||
}
|
||||
|
||||
@Post('auto-unload-arrived')
|
||||
@ApiOperation({ summary: 'Bulk auto-unload all arrived bookings into the warehouse' })
|
||||
autoUnloadArrived() {
|
||||
|
||||
@@ -397,6 +397,87 @@ 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.
|
||||
*/
|
||||
/**
|
||||
* Live occupancy per zone: rated capacity vs the weight/items currently held
|
||||
* (excludes items that have left — DELIVERED/DISPATCHED). Powers the yard
|
||||
* occupancy heatmap. Optionally scoped to one yard.
|
||||
*/
|
||||
async zoneOccupancy(yardId?: string): Promise<
|
||||
Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
type: string;
|
||||
yardId: string;
|
||||
capacityWeight: number | null;
|
||||
capacityContainers: number | null;
|
||||
usedWeight: number;
|
||||
usedItems: number;
|
||||
occupancyPct: number | null;
|
||||
}>
|
||||
> {
|
||||
const rows: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
type: string;
|
||||
yardId: string;
|
||||
capacityWeight: string | null;
|
||||
capacityContainers: number | null;
|
||||
usedWeight: number;
|
||||
usedItems: number;
|
||||
}> = await this.dataSource.query(
|
||||
`SELECT z.id,
|
||||
z.name,
|
||||
z.code,
|
||||
z.type,
|
||||
z.yard_id AS "yardId",
|
||||
z.capacity_weight AS "capacityWeight",
|
||||
z.capacity_containers AS "capacityContainers",
|
||||
COALESCE(SUM(inv.weight) FILTER (
|
||||
WHERE inv.deleted_at IS NULL
|
||||
AND inv.status NOT IN ('DELIVERED', 'DISPATCHED')
|
||||
), 0)::float8 AS "usedWeight",
|
||||
COALESCE(COUNT(inv.id) FILTER (
|
||||
WHERE inv.deleted_at IS NULL
|
||||
AND inv.status NOT IN ('DELIVERED', 'DISPATCHED')
|
||||
), 0)::int AS "usedItems"
|
||||
FROM freight.warehouse_zones z
|
||||
LEFT JOIN freight.warehouse_inventory inv ON inv.zone_id = z.id
|
||||
WHERE z.is_active = true
|
||||
AND z.deleted_at IS NULL
|
||||
AND ($1::uuid IS NULL OR z.yard_id = $1)
|
||||
GROUP BY z.id
|
||||
ORDER BY z.name`,
|
||||
[yardId ?? null],
|
||||
);
|
||||
|
||||
return rows.map((r) => {
|
||||
const capWeight = r.capacityWeight != null ? Number(r.capacityWeight) : null;
|
||||
const byWeight =
|
||||
capWeight && capWeight > 0 ? (r.usedWeight / capWeight) * 100 : null;
|
||||
const byItems =
|
||||
r.capacityContainers && r.capacityContainers > 0
|
||||
? (r.usedItems / r.capacityContainers) * 100
|
||||
: null;
|
||||
// Prefer container-count occupancy (unit-consistent). Weight capacity is
|
||||
// tonnes while inventory weight is kg, so weight% is only a rough fallback.
|
||||
const pct = byItems ?? byWeight;
|
||||
return {
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
code: r.code,
|
||||
type: r.type,
|
||||
yardId: r.yardId,
|
||||
capacityWeight: capWeight,
|
||||
capacityContainers: r.capacityContainers,
|
||||
usedWeight: r.usedWeight,
|
||||
usedItems: r.usedItems,
|
||||
occupancyPct: pct == null ? null : Math.round(pct * 10) / 10,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Recurring nudge: keep reminding self-haul IMPORT customers to assign a
|
||||
* collection truck while their goods are still in the warehouse
|
||||
|
||||
Reference in New Issue
Block a user