feat(warehouses): backoffice UI for container stack positions

Surfaces the physical stack/slot model in the staff app.

- ZoneLayoutModal: stacks drawn level by level with occupancy colours,
  configured-vs-built-vs-occupied counts, and stack create/delete plus
  block/reserve/free on empty levels
- SlotPicker in the store and move modals, offering only the next
  fillable level of each stack so the form cannot suggest a position
  the API will refuse
- move modal warns when a container is buried, lists the blockers, and
  disables the action instead of firing a 409
- fix: move() now asserts accessibility server-side, matching release —
  both are exits from a stack
This commit is contained in:
Hagernesh
2026-08-29 06:37:53 +00:00
parent abfd55d43e
commit 312c1f1da3
22 changed files with 1074 additions and 27 deletions

View File

@@ -12,7 +12,7 @@ import {
Tabs,
Text,
} from '@mantine/core';
import { ArrowLeft, Boxes, LayoutGrid, Package, Pencil, Plus, Trash2 } from 'lucide-react';
import { ArrowLeft, Boxes, Layers, LayoutGrid, Package, Pencil, Plus, Trash2 } from 'lucide-react';
import { useMutation, useQuery } from '@tanstack/react-query';
import { DataTable, type ColumnDef } from '@edr/ui-common';
@@ -25,6 +25,7 @@ import {
WarehouseTypeBadge,
ZoneContentsModal,
type ZoneRef,
ZoneLayoutModal,
ZoneOccupancyHeatmap,
formatCapacity,
humanizeEnum,
@@ -63,6 +64,7 @@ export default function WarehouseDetailPage() {
const [editingZone, setEditingZone] = useState<WarehouseZone | null>(null);
const [selectedYardId, setSelectedYardId] = useState<string | null>(null);
const [contentsZone, setContentsZone] = useState<ZoneRef | null>(null);
const [layoutZone, setLayoutZone] = useState<ZoneRef | null>(null);
const zonesQuery = useQuery(
api.warehouses.listZones.queryOptions({
@@ -203,6 +205,14 @@ export default function WarehouseDetailPage() {
meta: { headerClassName: 'text-right', cellClassName: 'text-right' },
cell: ({ row }) => (
<Group gap={4} justify="flex-end" wrap="nowrap" onClick={(e) => e.stopPropagation()}>
<ActionIcon
variant="subtle"
color="gray"
title="Stack layout"
onClick={() => setLayoutZone(row.original)}
>
<Layers size={16} />
</ActionIcon>
<ActionIcon
variant="subtle"
color="gray"
@@ -425,6 +435,12 @@ export default function WarehouseDetailPage() {
yard={editingYard}
/>
)}
<ZoneLayoutModal
opened={Boolean(layoutZone)}
onClose={() => setLayoutZone(null)}
zone={layoutZone}
/>
<ZoneContentsModal
opened={Boolean(contentsZone)}
onClose={() => setContentsZone(null)}