From 477c076d03de2511c4a37165208237f0a09b4553 Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Sat, 27 Dec 2025 16:24:42 -0800 Subject: [PATCH] feat: Add interactive 3D tab to Room Detail and refactor styling --- .../components/facility3d/Room3DViewer.tsx | 74 ++++++++++++++++ frontend/src/pages/RoomDetailPage.tsx | 88 +++++++++++-------- 2 files changed, 126 insertions(+), 36 deletions(-) create mode 100644 frontend/src/components/facility3d/Room3DViewer.tsx diff --git a/frontend/src/components/facility3d/Room3DViewer.tsx b/frontend/src/components/facility3d/Room3DViewer.tsx new file mode 100644 index 0000000..d1d5daf --- /dev/null +++ b/frontend/src/components/facility3d/Room3DViewer.tsx @@ -0,0 +1,74 @@ +import { PlantPosition } from './types'; +// ... +interface Room3DViewerProps { + room: any; + visMode?: 'STANDARD' | 'HEALTH' | 'YIELD' | 'TEMP' | 'HUMIDITY'; + onPlantClick?: (plant: PlantPosition) => void; +} + +export function Room3DViewer({ room, visMode = 'STANDARD', onPlantClick }: Room3DViewerProps) { + // Transform API room data to Room3D structure if needed + const room3DData: Room3D = useMemo(() => { + // If it already looks like a Room3D object (has racks/sections), use it. + // Otherwise, construct a mock layout for visualization. + if (room.racks || room.sections) { + return { + ...room, + // Ensure dimensions exist + width: room.width || 20, + height: room.height || 30, + x: 0, + y: 0, + sections: room.sections || room.racks || [], // Map racks/sections to standard field + }; + } + + // Fallback: Create a mock layout based on capacity + return { + id: room.id, + name: room.name, + type: room.type, + width: 20, + height: 30, + x: 0, + y: 0, + sections: [], // Empty for now, or could generatively fill + }; + }, [room]); + + return ( +
+ + + + + + { })} + highlightedTags={[]} + dimMode={false} + hierarchy={{ + facility: 'Main', + building: 'A', + floor: '1', + }} + /> + + + + + + + +
+ Interactive 3D View +
+
+ ); +} diff --git a/frontend/src/pages/RoomDetailPage.tsx b/frontend/src/pages/RoomDetailPage.tsx index a2cf453..d07174c 100644 --- a/frontend/src/pages/RoomDetailPage.tsx +++ b/frontend/src/pages/RoomDetailPage.tsx @@ -1,14 +1,15 @@ import { useState, useEffect } from 'react'; import { useParams, useNavigate, Link } from 'react-router-dom'; import { - ArrowLeft, Thermometer, Droplets, Wind, Zap, Sun, + ArrowLeft, Thermometer, Droplets, Wind, Zap, Sprout, Calendar, Edit2, Layers, Clock, CheckCircle2, - Activity, History, ClipboardList, Filter, MoreHorizontal, Plus + Activity, ClipboardList, Plus, Box } from 'lucide-react'; 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'; // Mock sensor data generator for Sparklines function generateMockData(count: number, min: number, max: number) { @@ -22,7 +23,7 @@ export default function RoomDetailPage() { const navigate = useNavigate(); const [room, setRoom] = useState(null); const [loading, setLoading] = useState(true); - const [activeTab, setActiveTab] = useState<'batches' | 'tasks' | 'environment'>('batches'); + const [activeTab, setActiveTab] = useState<'batches' | 'tasks' | 'environment' | '3d'>('batches'); useEffect(() => { if (id) { @@ -37,7 +38,7 @@ export default function RoomDetailPage() { if (loading) { return (
-
+
); } @@ -58,47 +59,47 @@ export default function RoomDetailPage() {
-

+

{room.name?.replace('[DEMO] ', '')}

{room.type} Phase
- + {room.sqft?.toLocaleString()} SQFT
- + {room.capacity || 0} Plants max
- System Nominal + System Nominal
- -
@@ -112,6 +113,7 @@ export default function RoomDetailPage() { value={room.targetTemp || 75.5} unit="°F" color="text-[var(--color-error)]" + fill="bg-[var(--color-error)]" data={sensorData.temp} />
{/* Content Tabs */}
-
+
setActiveTab('batches')} /> + setActiveTab('3d')} + />
+ {activeTab === '3d' && ( + + )} + {activeTab === 'batches' && (
{room.batches?.map((batch: any) => ( -
+
-
+

{batch.name}

-

{batch.strain} • {batch.plantCount} plants

+

{batch.strain} • {batch.plantCount} plants

-
+
{batch.stage}
-
+
Day {Math.floor((Date.now() - new Date(batch.startDate).getTime()) / 86400000)}
))} -
)} {activeTab === 'tasks' && ( -
+
{[ { title: 'De-fan Fan Leaves', assignee: 'John D.', priority: 'HIGH', due: 'Today' }, { title: 'Irrigation Maintenance', assignee: 'Sarah K.', priority: 'MED', due: 'Tomorrow' }, { title: 'IPM Foliar Spray', assignee: 'John D.', priority: 'CRITICAL', due: '2h ago' }, { title: 'Verify Light Intensity', assignee: 'None', priority: 'LOW', due: 'Friday' }, ].map((task, i) => ( -
+

{task.title}

- Assigned: {task.assignee} - Due: {task.due} + Assigned: {task.assignee} + Due: {task.due}
@@ -236,21 +251,22 @@ export default function RoomDetailPage() { ); } -function MetricCard({ icon: Icon, label, value, unit, color, data }: { +function MetricCard({ icon: Icon, label, value, unit, color, fill, data }: { icon: any; label: string; value: string | number; unit: string; color: string; + fill: string; // added back to control sparkline background opacity data: { value: number }[]; }) { return ( - +
- {label} + {label}
{value} @@ -258,11 +274,11 @@ function MetricCard({ icon: Icon, label, value, unit, color, data }: {
- +
-
-
+
+
); @@ -300,16 +316,16 @@ function TabButton({ active, icon: Icon, label, count, onClick }: { active: bool