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

@@ -118,6 +118,7 @@ const blockNegative = (event: KeyboardEvent<HTMLInputElement>) => {
interface UnitErrors {
containerNumber?: string;
sealNumber?: string;
vgmTons?: string;
}
@@ -886,6 +887,9 @@ export default function GlCreateBookingForm() {
} else if ((numberCounts.get(key) ?? 0) > 1) {
errs.containerNumber = "Duplicate container number in this shipment.";
}
if (u.sealNumber.trim() === "") {
errs.sealNumber = "Seal number is required.";
}
const vgm = Number(u.vgmTons);
if (u.vgmTons.trim() === "" || Number.isNaN(vgm) || vgm <= 0) {
errs.vgmTons = "Enter a valid VGM.";
@@ -1185,7 +1189,7 @@ export default function GlCreateBookingForm() {
: {}),
units: l.units.map((u) => ({
containerNumber: u.containerNumber.trim().toUpperCase(),
...(u.sealNumber ? { sealNumber: u.sealNumber } : {}),
sealNumber: u.sealNumber.trim(),
vgmTons: Number(u.vgmTons) || 0,
// Per-container handling — the server rolls these into the line
// counts and bills each surcharge on the ticked containers only.
@@ -1247,7 +1251,7 @@ export default function GlCreateBookingForm() {
reeferQuantity: Number(l.reeferQuantity || 0) || undefined,
units: l.units.map((u) => ({
containerNumber: u.containerNumber.trim().toUpperCase(),
...(u.sealNumber ? { sealNumber: u.sealNumber } : {}),
sealNumber: u.sealNumber.trim(),
vgmTons: Number(u.vgmTons) || 0,
isHazardous: Boolean(u.isHazardous),
isReefer: Boolean(u.isReefer),
@@ -1857,7 +1861,7 @@ export default function GlCreateBookingForm() {
Container number *
</Text>
<Text fz={12} fw={600} c="#10202F" style={{ flex: 1 }}>
Seal number
Seal number *
</Text>
<Text fz={12} fw={600} c="#10202F" style={{ flex: 1 }}>
VGM (tons) *
@@ -1901,8 +1905,13 @@ export default function GlCreateBookingForm() {
style={{ flex: 1 }}
/>
<TextInput
placeholder="Optional"
placeholder="e.g. SL-0099231"
value={unit.sealNumber}
error={
showErrors
? unitErrors[lineIdx]?.[unitIdx]?.sealNumber
: undefined
}
onChange={(e) =>
patchUnit(lineIdx, unitIdx, {
sealNumber: e.currentTarget.value,