import { useMemo } from 'react'; import { Select, Text } from '@mantine/core'; import { useQuery } from '@tanstack/react-query'; import { api } from '@/services/api'; import type { ZoneLayout } from '@/types/warehouse'; interface SlotPickerProps { zoneId: string; value: string; onChange: (slotId: string) => void; label?: string; disabled?: boolean; } /** * The stack levels a container may actually be put on right now. * * Only the next fillable level of each stack is offered: a box cannot stand on * level 2 while level 1 is empty, so listing level 3 of an empty stack would * only produce a rejected request. The server enforces the same rule — this * mirrors it so the operator never sees a 400 for a position the form offered. */ export function fillableLevels(layout: ZoneLayout | undefined) { if (!layout) return []; return layout.stacks .filter((stack) => stack.isActive && stack.status === 'ACTIVE') .flatMap((stack) => { const occupied = stack.slots .filter((slot) => slot.effectiveStatus === 'OCCUPIED') .map((slot) => slot.level); const top = occupied.length > 0 ? Math.max(...occupied) : 0; if (top >= stack.maxStackHeight) return []; const next = stack.slots.find( (slot) => slot.level === top + 1 && slot.effectiveStatus === 'AVAILABLE', ); if (!next) return []; return [ { value: next.slotId, label: `${stack.code} — level ${next.level}${top > 0 ? ` (on ${top} container${top > 1 ? 's' : ''})` : ' (ground)'}`, }, ]; }); } export function SlotPicker({ zoneId, value, onChange, label = 'Stack position', disabled }: SlotPickerProps) { const { data, isLoading } = useQuery( api.warehouses.zoneLayout.queryOptions({ input: { zoneId }, enabled: Boolean(zoneId), }), ); const options = useMemo(() => fillableLevels(data), [data]); // A zone with no stacks configured keeps plain zone-level placement — showing // an empty picker there would imply a choice that does not exist. if (!zoneId || (!isLoading && (data?.stacks.length ?? 0) === 0)) return null; return (