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 (
+
+
+
+
+ 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
{label}
{count !== undefined && (
{count}