Warehouse Management System
Event-sourced warehouse management with zones, slots, transport units, floor plan editing, and immutable placement ledger built on Nextcloud Tables.
Last updated: 2025-02-18
Warehouse Management System (WHMS)
The WHMS module provides event-sourced warehouse management with an immutable placement ledger. Current state is always derived from the event history — never stored directly.
Interactive 3D Warehouse
Explore the warehouse in 3D — orbit, zoom, and click on any slot to inspect its contents, zone utilization, and recent activity.
3D warehouse visualization — 3 zones, 24 slots, 13 occupied.
LiDAR Scan Visualization
LiDAR sensors provide millimeter-precision spatial mapping. Certexi processes point clouds in real time to verify slot occupancy.
Simulated LiDAR scan — 400 classified points across walls, racks, pallets, and floor surfaces.
Architecture
Design Principles
Immutable Event Ledger
Events are append-only — no updates, no deletes. Every placement, removal, and verification is recorded as a new row. Current state is derived by replaying events.
Warehouse-Local Coordinates
The system uses floor-plan-relative (x, y) coordinates rather than GPS. This works indoors, offline, and survives floor plan updates since the stable slot_id is the primary reference.
Evidence-First Design
Every placement requires evidence:
- Scan proof — Barcode/QR of slot + asset
- Photo proof — Timestamped photo at placement
- CCTV link — Optional clip from nearby camera
- Scale data — Optional weight verification
Event Types
| Type | Description |
|---|---|
PLACED | Asset placed in a slot |
REMOVED | Asset removed from a slot |
VERIFIED | Supervisor verified a placement |
DISPUTED | Supervisor disputed a placement |
Data Model
Slots (Master Data)
Physical storage locations with polygon geofences:
| Field | Type | Description |
|---|---|---|
slot_id | text | Unique identifier (e.g., "A-01-01") |
zone | selection | Zone category (A, B, C, etc.) |
zone_type | selection | storage, staging, receiving, shipping, cold, hazmat |
floor_plan_polygon | JSON | Array of points defining the geofence |
constraints | JSON | max_weight, max_height, allowed_types |
Assets
Items that can be placed in slots:
| Field | Type | Description |
|---|---|---|
asset_id | text | Unique identifier (e.g., "PLT-2024-00001") |
type | selection | pallet, container, box, drum, equipment |
weight_kg | number | Weight in kilograms |
dimensions | JSON | in cm |
Placement Events (Immutable)
Append-Only
The placement events table is an immutable ledger. Rows must never be updated or deleted. All state changes are recorded as new events.
Each event records: event_type, slot_id, asset_barcode, operator, timestamp, photo_url, scale_weight_kg, evidence_hash (SHA-256), and optional cctv_clip_url, work_order_ref, and verification_notes.
Deriving Current State
Since the ledger is immutable, current state is computed:
async function getSlotOccupancy(slotId: number) {
const events = await queryEvents({
slot_id: slotId,
orderBy: 'timestamp',
order: 'DESC',
});
const lastPlacement = events.find(
(e) => e.event_type === 'PLACED' || e.event_type === 'REMOVED'
);
if (!lastPlacement || lastPlacement.event_type === 'REMOVED') {
return { isOccupied: false, assetBarcode: null };
}
const isVerified = events.some(
(e) => e.event_type === 'VERIFIED' && e.timestamp > lastPlacement.timestamp
);
return {
isOccupied: true,
assetBarcode: lastPlacement.asset_barcode,
isVerified,
};
}
API Endpoints
Events
POST /api/whms/events— Create a placement eventGET /api/whms/events?slot_id=X&limit=50— Query event history
Slots
GET /api/whms/slots— List all slotsGET /api/whms/slots/:id— Get slot with computed occupancy statusPOST /api/whms/slots— Create a slotPUT /api/whms/slots/:id— Update slot metadata
Assets
GET /api/whms/assets— List all assetsGET /api/whms/assets/:barcode/location— Get asset's current location
Dashboard
GET /api/whms/dashboard/utilization— Zone utilization percentages
Floor Plan Editor
The layout editor uses Konva.js for polygon drawing:
- Upload a floor plan image
- Draw polygons to define slot boundaries
- Assign slot IDs, zones, and constraints
- Zone color coding for visual clarity
Component Inventory
The WHMS module is implemented across the following components. Use these paths when importing or extending behavior.
Layout & map
| Component | Path | Description |
|---|---|---|
FloorPlanCanvas | @/components/whms/layout-editor/floor-plan-canvas | Konva-based floor plan with polygon slots |
GeoSlotEditor | @/components/whms/layout-editor/geo-slot-editor | Edit slot polygons and metadata |
SlotSidebar | @/components/whms/layout-editor/slot-sidebar | Slot properties and zone assignment |
WhmsGeofenceMap | @/components/whms/WhmsGeofenceMap | Leaflet map with zone/slot layers |
MinecraftGrid3D | @/components/whms/MinecraftGrid3D | 3D grid view of zones and slots |
IsometricZoneViewer | @/components/whms/IsometricZoneViewer | Isometric zone visualization |
BirdEyeView | @/components/whms/BirdEyeView | Top-down zone overview |
LidarZoneViewer | @/components/whms/LidarZoneViewer | Lidar-style zone display |
Scanner & placement
| Component | Path | Description |
|---|---|---|
PlacementFlow | @/components/whms/scanner/placement-flow | Scan slot → scan asset → photo → confirm |
RemovalFlow | @/components/whms/scanner/removal-flow | Scan slot → confirm removal |
ZoneScanner | @/components/whms/ZoneScanner | Zone/slot barcode scanning |
AssetDropPopup | @/components/whms/AssetDropPopup | In-place drop confirmation |
Zones & slots
| Component | Path | Description |
|---|---|---|
ZoneDetailModal | @/components/whms/ZoneDetailModal | Zone info, slots, CCTV, analytics |
ZoneDrilldownPanel | @/components/whms/ZoneDrilldownPanel | Sliding panel with zone details |
ZoneCCTVTab | @/components/whms/ZoneCCTVTab | CCTV feeds for a zone |
ZoneAnalyticsPanel | @/components/whms/ZoneAnalyticsPanel | Zone metrics and charts |
SlotsTab | @/components/whms/SlotsTab | List/grid of slots with occupancy |
CCTVTab | @/components/whms/CCTVTab | Camera list and status |
Dashboard & utilization
| Component | Path | Description |
|---|---|---|
UtilizationWidget | @/components/whms/dashboard/utilization-widget | Zone capacity and usage |
LiveOperationsFeed | @/components/whms/LiveOperationsFeed | Real-time placement/removal feed |
RecordsTab | @/components/whms/RecordsTab | Table records linked to WHMS |
Gamification (optional)
| Component | Path | Description |
|---|---|---|
MissionsTab | @/components/whms/MissionsTab | Missions and transport units on map |
MissionCard / MissionMarker | @/components/whms/ | Mission UI and map markers |
QuestsTab / QuestCard / QuestCreator | @/components/whms/ | Quests and challenges |
GamificationHeader | @/components/whms/GamificationHeader | XP, level, streak |
AdminGameSetup | @/components/whms/admin/AdminGameSetup | Configure games and leaderboards |
3D & panels
| Component | Path | Description |
|---|---|---|
DroppableSlot3D / DraggableAsset | @/components/whms/ | Drag-and-drop in 3D view |
AssetBankDrawer / RecordPalette | @/components/whms/ | Asset/record picker for placement |
FloatingPanelManager / FloatingWindow | @/components/whms/panels/ | Floating inventory and panels |
PanelInventory / InventoryGrid | @/components/whms/panels/ | Inventory grid in panels |
UI patterns (sandboxes)
The following sandboxes illustrate the UI building blocks used across WHMS screens. Real components use the same primitives (Card, Badge, Button, Progress, Tabs) plus map/3D and API data.
Slot status card
A single slot’s occupancy and verification status in list or grid views:
<Card className="max-w-xs"> <CardHeader className="pb-2 flex flex-row items-center justify-between"> <CardTitle className="text-sm font-medium">A-01-03</CardTitle> <Badge variant="secondary">Staging</Badge> </CardHeader> <CardContent className="space-y-2"> <p className="text-xs text-muted-foreground">Occupied</p> <p className="text-sm font-mono">PLT-2024-00042</p> <div className="flex gap-2"> <Badge variant="outline">Verified</Badge> <Button size="sm" variant="ghost">View</Button> </div> </CardContent> </Card>
Zone utilization row
One row of the utilization widget (zone name + capacity bar):
<div className="space-y-3 max-w-sm"> <div className="flex justify-between text-sm"> <span>Zone A — Storage</span> <span className="text-muted-foreground">72%</span> </div> <Progress value={72} className="h-2" /> <div className="flex justify-between text-sm"> <span>Zone B — Staging</span> <span className="text-muted-foreground">24 / 32 slots</span> </div> <Progress value={75} className="h-2" /> </div>
Placement flow step
A single step in the placement wizard (scan → photo → confirm):
<Card className="max-w-sm"> <CardHeader> <CardTitle className="text-base">Step 2: Photo</CardTitle> <CardDescription>Capture proof of placement at slot</CardDescription> </CardHeader> <CardContent className="space-y-4"> <div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground"> Camera preview or placeholder </div> <div className="flex gap-2"> <Button className="flex-1">Capture</Button> <Button variant="outline">Skip</Button> </div> </CardContent> </Card>
Zone tabs (detail view)
Tabs used in zone detail (Slots, CCTV, Analytics, Incidents):
<Tabs defaultValue="slots" className="max-w-md"> <TabsList className="grid w-full grid-cols-4"> <TabsTrigger value="slots">Slots</TabsTrigger> <TabsTrigger value="cctv">CCTV</TabsTrigger> <TabsTrigger value="analytics">Analytics</TabsTrigger> <TabsTrigger value="incidents">Incidents</TabsTrigger> </TabsList> <TabsContent value="slots" className="p-4 border rounded-b-md"> <p className="text-sm text-muted-foreground">Slot list with occupancy and actions.</p> </TabsContent> <TabsContent value="cctv" className="p-4 border rounded-b-md"> <p className="text-sm text-muted-foreground">Camera feeds for this zone.</p> </TabsContent> <TabsContent value="analytics" className="p-4 border rounded-b-md"> <p className="text-sm text-muted-foreground">Charts and utilization.</p> </TabsContent> <TabsContent value="incidents" className="p-4 border rounded-b-md"> <p className="text-sm text-muted-foreground">Incidents and disputes.</p> </TabsContent> </Tabs>
Operations feed item
One line in the live operations feed (placement/removal events):
<div className="flex items-center gap-3 rounded-lg border p-3 max-w-md"> <Badge>PLACED</Badge> <div className="flex-1 min-w-0"> <p className="text-sm font-medium truncate">A-02-01 ← PLT-2024-00011</p> <p className="text-xs text-muted-foreground">Op. García · 2 min ago</p> </div> <Button size="sm" variant="ghost">Details</Button> </div>