diff --git a/frontend/src/components/layout-editor/LayoutEditor.tsx b/frontend/src/components/layout-editor/LayoutEditor.tsx index 617351b..bc2d8ba 100644 --- a/frontend/src/components/layout-editor/LayoutEditor.tsx +++ b/frontend/src/components/layout-editor/LayoutEditor.tsx @@ -6,6 +6,7 @@ import { useState, useEffect, useCallback } from 'react'; import { TypeLibrary } from './TypeLibrary'; import { RackVisualizer, RackData, PlantSlotData } from './RackVisualizer'; +import { RackConfigModal } from './RackConfigModal'; import { cn } from '../../lib/utils'; import { layoutApi, LayoutPlantType, Floor3DData } from '../../lib/layoutApi'; @@ -21,6 +22,7 @@ export function LayoutEditor({ floorId, className }: LayoutEditorProps) { const [error, setError] = useState(null); const [selectedSlot, setSelectedSlot] = useState(null); const [draggingType, setDraggingType] = useState(null); + const [configRack, setConfigRack] = useState(null); // Load floor data and plant types useEffect(() => { @@ -79,6 +81,34 @@ export function LayoutEditor({ floorId, className }: LayoutEditorProps) { setDraggingType(plantType); }, []); + const handleConfigClick = useCallback((rack: RackData) => { + setConfigRack(rack); + }, []); + + const handleConfigSave = useCallback(async (updates: Partial) => { + if (!configRack) return; + + try { + // Call API to update section + await layoutApi.updateSection(configRack.id, { + name: updates.name, + code: updates.code, + type: updates.subtype, + rows: updates.rows, + columns: updates.columns, + }); + + // Reload floor data to get updated positions + const floor = await layoutApi.getFloor3D(floorId); + setFloorData(floor); + + console.log('Section updated:', configRack.id, updates); + } catch (e) { + console.error('Failed to update section:', e); + throw e; + } + }, [configRack, floorId]); + if (loading) { return (
@@ -138,6 +168,7 @@ export function LayoutEditor({ floorId, className }: LayoutEditorProps) { rack={rack} onSlotClick={handleSlotClick} onSlotDrop={handleSlotDrop} + onConfigClick={handleConfigClick} /> ))}
@@ -222,6 +253,16 @@ export function LayoutEditor({ floorId, className }: LayoutEditorProps) { )} + + {/* Rack Config Modal */} + {configRack && ( + setConfigRack(null)} + onSave={handleConfigSave} + /> + )} ); } diff --git a/frontend/src/components/layout-editor/RackConfigModal.tsx b/frontend/src/components/layout-editor/RackConfigModal.tsx new file mode 100644 index 0000000..f188c15 --- /dev/null +++ b/frontend/src/components/layout-editor/RackConfigModal.tsx @@ -0,0 +1,223 @@ +/** + * RackConfigModal - Modal for configuring rack/section dimensions + */ + +import { useState, useEffect } from 'react'; +import { cn } from '../../lib/utils'; +import type { RackData } from './RackVisualizer'; + +interface RackConfigModalProps { + rack: RackData; + isOpen: boolean; + onClose: () => void; + onSave: (updates: Partial) => void; +} + +const SUBTYPE_OPTIONS = [ + { value: 'TABLE', label: 'Table', icon: '🪵', desc: 'Flat growing surface' }, + { value: 'RACK', label: 'Rack', icon: '🗄️', desc: 'Multi-tier vertical rack' }, + { value: 'TRAY', label: 'Tray', icon: '🌿', desc: 'Flood/drain tray system' }, + { value: 'HANGER', label: 'Hanger', icon: '🪝', desc: 'Hanging/drying racks' }, + { value: 'FLOOR', label: 'Floor', icon: '⬜', desc: 'Ground-level positions' }, +]; + +export function RackConfigModal({ rack, isOpen, onClose, onSave }: RackConfigModalProps) { + const [name, setName] = useState(rack.name); + const [code, setCode] = useState(rack.code || ''); + const [subtype, setSubtype] = useState(rack.subtype); + const [rows, setRows] = useState(rack.rows); + const [columns, setColumns] = useState(rack.columns); + const [tiers, setTiers] = useState(rack.tiers); + const [saving, setSaving] = useState(false); + + // Reset form when rack changes + useEffect(() => { + setName(rack.name); + setCode(rack.code || ''); + setSubtype(rack.subtype); + setRows(rack.rows); + setColumns(rack.columns); + setTiers(rack.tiers); + }, [rack]); + + if (!isOpen) return null; + + const handleSave = async () => { + setSaving(true); + try { + await onSave({ + name, + code: code || undefined, + subtype, + rows, + columns, + tiers, + }); + onClose(); + } catch (e) { + console.error('Failed to save rack config:', e); + } finally { + setSaving(false); + } + }; + + const totalSlots = rows * columns * tiers; + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
+ {/* Header */} +
+

Configure Section

+ +
+ + {/* Body */} +
+ {/* Name & Code */} +
+
+ + setName(e.target.value)} + className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-white focus:outline-none focus:border-emerald-500" + placeholder="e.g., Table 1" + /> +
+
+ + setCode(e.target.value)} + className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-white focus:outline-none focus:border-emerald-500" + placeholder="e.g., T1" + /> +
+
+ + {/* Section Type */} +
+ +
+ {SUBTYPE_OPTIONS.map((opt) => ( + + ))} +
+
+ + {/* Dimensions */} +
+ +
+
+ + setRows(Math.max(1, Math.min(50, parseInt(e.target.value) || 1)))} + min={1} + max={50} + className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-white text-center focus:outline-none focus:border-emerald-500" + /> +
+
+ + setColumns(Math.max(1, Math.min(50, parseInt(e.target.value) || 1)))} + min={1} + max={50} + className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-white text-center focus:outline-none focus:border-emerald-500" + /> +
+
+ + setTiers(Math.max(1, Math.min(10, parseInt(e.target.value) || 1)))} + min={1} + max={10} + className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-white text-center focus:outline-none focus:border-emerald-500" + /> +
+
+

+ Total positions: {totalSlots} +

+
+ + {/* Visual Preview */} +
+

Preview

+
+ {Array.from({ length: Math.min(rows * columns, 50) }, (_, i) => ( +
+ ))} +
+ {rows * columns > 50 && ( +

Showing first 50 slots

+ )} +
+
+ + {/* Footer */} +
+ + +
+
+
+ ); +} + +export default RackConfigModal; diff --git a/frontend/src/components/layout-editor/RackVisualizer.tsx b/frontend/src/components/layout-editor/RackVisualizer.tsx index 5524f46..9eacdd1 100644 --- a/frontend/src/components/layout-editor/RackVisualizer.tsx +++ b/frontend/src/components/layout-editor/RackVisualizer.tsx @@ -33,6 +33,7 @@ interface RackVisualizerProps { selectedTier?: number; onSlotClick?: (slot: PlantSlotData) => void; onSlotDrop?: (slot: PlantSlotData, plantType: LayoutPlantType) => void; + onConfigClick?: (rack: RackData) => void; highlightedSlots?: Set; className?: string; } @@ -55,6 +56,7 @@ export function RackVisualizer({ selectedTier = 1, onSlotClick, onSlotDrop, + onConfigClick, highlightedSlots, className }: RackVisualizerProps) { @@ -91,15 +93,29 @@ export function RackVisualizer({ }; return ( -
+
{/* Rack header */}
{style.icon} {rack.name} ({rack.code}) - {rack.tiers > 1 && ( - Tier {selectedTier}/{rack.tiers} - )} +
+ {rack.tiers > 1 && ( + Tier {selectedTier}/{rack.tiers} + )} + {onConfigClick && ( + + )} +
{/* Tier tabs (if multi-tier) */} diff --git a/frontend/src/components/layout-editor/index.ts b/frontend/src/components/layout-editor/index.ts index 9239e56..9065f1c 100644 --- a/frontend/src/components/layout-editor/index.ts +++ b/frontend/src/components/layout-editor/index.ts @@ -1,4 +1,5 @@ export { LayoutEditor } from './LayoutEditor'; export { TypeLibrary } from './TypeLibrary'; export { RackVisualizer } from './RackVisualizer'; +export { RackConfigModal } from './RackConfigModal'; export type { RackData, PlantSlotData } from './RackVisualizer';