diff --git a/frontend/src/components/facility2d/RoomLayout2D.tsx b/frontend/src/components/facility2d/RoomLayout2D.tsx new file mode 100644 index 0000000..143176e --- /dev/null +++ b/frontend/src/components/facility2d/RoomLayout2D.tsx @@ -0,0 +1,251 @@ +import { useState, useMemo } from 'react'; +import { cn } from '../../lib/utils'; + +interface RackData { + id: string; + name: string; + posX: number; + posY: number; + width: number; + height: number; + plantCount?: number; + healthScore?: number; +} + +interface RoomLayout2DProps { + room: { + id: string; + name: string; + width?: number; + height?: number; + sections?: RackData[]; + racks?: RackData[]; + }; + onRackClick?: (rack: RackData) => void; +} + +export function RoomLayout2D({ room, onRackClick }: RoomLayout2DProps) { + const [viewMode, setViewMode] = useState<'overhead' | 'isometric'>('isometric'); + + // Room dimensions (use defaults if not provided) + const roomWidth = room.width || 50; + const roomHeight = room.height || 40; + + // Get racks from sections or racks property, or generate mock data + const racks: RackData[] = useMemo(() => { + if (room.sections && room.sections.length > 0) { + return room.sections; + } + if (room.racks && room.racks.length > 0) { + return room.racks; + } + // Generate mock racks for demo + return Array.from({ length: 4 }).map((_, i) => ({ + id: `rack-${i}`, + name: `Rack ${String(i + 1).padStart(2, '0')}`, + posX: 5 + (i * 11), + posY: 8, + width: 8, + height: 24, + plantCount: 12 + (i * 4), + healthScore: 85 + (i * 3), + })); + }, [room.sections, room.racks]); + + // SVG viewBox (add padding) + const padding = 4; + const viewBox = `${-padding} ${-padding} ${roomWidth + padding * 2} ${roomHeight + padding * 2}`; + + // Health score to color + const getHealthColor = (score?: number) => { + if (!score) return '#64748b'; + if (score >= 90) return '#22c55e'; + if (score >= 75) return '#eab308'; + return '#ef4444'; + }; + + return ( +
+ {/* SVG Container with perspective for isometric mode */} +
+ + {/* Room Floor */} + + + {/* Grid Lines */} + {Array.from({ length: Math.floor(roomWidth / 5) + 1 }).map((_, i) => ( + + ))} + {Array.from({ length: Math.floor(roomHeight / 5) + 1 }).map((_, i) => ( + + ))} + + {/* Racks */} + {racks.map((rack) => ( + onRackClick?.(rack)} + > + {/* Rack base (shadow) */} + + {/* Rack body */} + + {/* Rack label */} + + {rack.name} + + {/* Plant count indicator */} + {rack.plantCount && ( + + + + {rack.plantCount} + + + )} + + ))} + + {/* Room Name Label */} + + {room.name.replace('[DEMO] ', '')} + + +
+ + {/* View Mode Toggle */} +
+ +
+ {viewMode === 'overhead' ? 'OVERHEAD' : 'ISOMETRIC'} +
+
+ + {/* Legend */} +
+
+ + Healthy (90%+) +
+
+ + Warning (75-89%) +
+
+ + Critical (<75%) +
+
+
+ ); +} diff --git a/frontend/src/pages/RoomDetailPage.tsx b/frontend/src/pages/RoomDetailPage.tsx index b557b92..4f44a20 100644 --- a/frontend/src/pages/RoomDetailPage.tsx +++ b/frontend/src/pages/RoomDetailPage.tsx @@ -9,7 +9,7 @@ import api from '../lib/api'; import { Card } from '../components/ui/card'; import { cn } from '../lib/utils'; import { motion } from 'framer-motion'; -import { Room3DViewer } from '../components/facility3d/Room3DViewer'; +import { RoomLayout2D } from '../components/facility2d/RoomLayout2D'; // Mock sensor data generator for Sparklines function generateMockData(count: number, min: number, max: number) { @@ -178,7 +178,7 @@ export default function RoomDetailPage() {
{activeTab === '3d' && ( - + )} {activeTab === 'batches' && (