mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 07:38:10 +00:00
feat(warehouses): track physical container stack and slot positions
Extends the warehouse hierarchy below zone with ground stacks and vertical slots, so a container's exact position is recorded rather than only its zone. - freight.warehouse_zone_stacks / warehouse_zone_slots, plus nullable stack_id / slot_id on warehouse_inventory (existing rows stay valid) - slot occupancy is derived from inventory status, guarded by a partial unique index, so no exit path has to remember to free a slot - placement service: hierarchy validation, bottom-up stacking rules, accessibility/blocking-container reads, capacity vs slot summaries - stack CRUD with auto-generated slots; reuses warehouse-zone permissions - slot support folded into the existing move()/store() paths - fix: validateLocation now rejects a mismatched warehouse/yard/zone triple - seed:warehouse-layout builds the layout from a JSON config
This commit is contained in:
@@ -4,12 +4,19 @@ import { DataTable, type ColumnDef } from '@edr/ui-common';
|
||||
|
||||
import { formatDateTime, humanize } from '@/lib/format';
|
||||
import { api } from '@/services/api';
|
||||
import type { WarehouseZone, ZoneContentItem } from '@/types/warehouse';
|
||||
import type { ZoneContentItem } from '@/types/warehouse';
|
||||
|
||||
/** Enough to name the zone and fetch it — satisfied by WarehouseZone and ZoneOccupancy alike. */
|
||||
export interface ZoneRef {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
interface ZoneContentsModalProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
zone: WarehouseZone | null;
|
||||
zone: ZoneRef | null;
|
||||
}
|
||||
|
||||
const columns: ColumnDef<ZoneContentItem>[] = [
|
||||
|
||||
@@ -25,13 +25,15 @@ function capacityLabel(z: ZoneOccupancy): string {
|
||||
interface ZoneOccupancyHeatmapProps {
|
||||
/** Scope to one yard; omit for all zones. */
|
||||
yardId?: string;
|
||||
/** Pass to make each tile open that zone; omitted leaves the tiles inert. */
|
||||
onZoneClick?: (zone: ZoneOccupancy) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Occupancy heatmap: one tile per zone, coloured by how full it is. Occupancy is
|
||||
* container-count based (unit-consistent); weight is shown as context only.
|
||||
*/
|
||||
export function ZoneOccupancyHeatmap({ yardId }: ZoneOccupancyHeatmapProps) {
|
||||
export function ZoneOccupancyHeatmap({ yardId, onZoneClick }: ZoneOccupancyHeatmapProps) {
|
||||
const { data: zones = [], isLoading } = useZoneOccupancy(yardId);
|
||||
|
||||
if (isLoading) {
|
||||
@@ -67,7 +69,15 @@ export function ZoneOccupancyHeatmap({ yardId }: ZoneOccupancyHeatmapProps) {
|
||||
const t = tone(z.occupancyPct);
|
||||
const pct = z.occupancyPct ?? 0;
|
||||
return (
|
||||
<Card key={z.id} withBorder radius="md" padding="sm">
|
||||
<Card
|
||||
key={z.id}
|
||||
withBorder
|
||||
radius="md"
|
||||
padding="sm"
|
||||
onClick={onZoneClick ? () => onZoneClick(z) : undefined}
|
||||
style={onZoneClick ? { cursor: 'pointer' } : undefined}
|
||||
title={onZoneClick ? `View what is stored in ${z.name}` : undefined}
|
||||
>
|
||||
<Stack gap={6}>
|
||||
<Group justify="space-between" wrap="nowrap" gap="xs">
|
||||
<Text fw={600} size="sm" truncate title={z.name}>
|
||||
|
||||
@@ -9,7 +9,7 @@ export { WarehouseInquiryTable } from './WarehouseInquiryTable';
|
||||
export { CreateWarehouseModal } from './CreateWarehouseModal';
|
||||
export { CreateYardModal } from './CreateYardModal';
|
||||
export { CreateZoneModal } from './CreateZoneModal';
|
||||
export { ZoneContentsModal } from './ZoneContentsModal';
|
||||
export { ZoneContentsModal, type ZoneRef } from './ZoneContentsModal';
|
||||
export { ReceiveInventoryModal, WarehouseFlowWorkbench } from './ReceiveInventoryModal';
|
||||
export { WarehouseInfoCard } from './WarehouseInfoCard';
|
||||
export { MoveInventoryModal } from './MoveInventoryModal';
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
WarehouseStatusBadge,
|
||||
WarehouseTypeBadge,
|
||||
ZoneContentsModal,
|
||||
type ZoneRef,
|
||||
ZoneOccupancyHeatmap,
|
||||
formatCapacity,
|
||||
humanizeEnum,
|
||||
@@ -61,7 +62,7 @@ export default function WarehouseDetailPage() {
|
||||
const [zoneModalOpen, setZoneModalOpen] = useState(false);
|
||||
const [editingZone, setEditingZone] = useState<WarehouseZone | null>(null);
|
||||
const [selectedYardId, setSelectedYardId] = useState<string | null>(null);
|
||||
const [contentsZone, setContentsZone] = useState<WarehouseZone | null>(null);
|
||||
const [contentsZone, setContentsZone] = useState<ZoneRef | null>(null);
|
||||
|
||||
const zonesQuery = useQuery(
|
||||
api.warehouses.listZones.queryOptions({
|
||||
@@ -373,7 +374,10 @@ export default function WarehouseDetailPage() {
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<ZoneOccupancyHeatmap yardId={selectedYardId ?? undefined} />
|
||||
<ZoneOccupancyHeatmap
|
||||
yardId={selectedYardId ?? undefined}
|
||||
onZoneClick={(zone) => setContentsZone(zone)}
|
||||
/>
|
||||
|
||||
{!selectedYardId ? (
|
||||
<Text c="dimmed" ta="center" py="lg">
|
||||
|
||||
Reference in New Issue
Block a user