Compare commits
No commits in common. "dd647538c5d6d17b5e19894e394c887947c1b13c" and "c049aac16ec1eaaecf134156481231d23d80f530" have entirely different histories.
dd647538c5
...
c049aac16e
4 changed files with 49 additions and 364 deletions
|
|
@ -1,112 +0,0 @@
|
||||||
const { PrismaClient } = require('@prisma/client');
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
console.log('🌱 Seeding METRC demo data...');
|
|
||||||
|
|
||||||
// 1. Get existing Batches
|
|
||||||
const batches = await prisma.batch.findMany({
|
|
||||||
take: 5
|
|
||||||
});
|
|
||||||
|
|
||||||
if (batches.length === 0) {
|
|
||||||
console.error('No Batches found. Please run main seed first.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Get existing FacilityPositions with Room info
|
|
||||||
const positions = await prisma.facilityPosition.findMany({
|
|
||||||
take: 50,
|
|
||||||
include: {
|
|
||||||
section: {
|
|
||||||
include: {
|
|
||||||
room: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (positions.length === 0) {
|
|
||||||
console.error('No FacilityPositions found. Please run main seed first.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Found ${batches.length} batches and ${positions.length} positions.`);
|
|
||||||
|
|
||||||
// 3. Clear existing demo plants
|
|
||||||
const deleted = await prisma.facilityPlant.deleteMany({
|
|
||||||
where: {
|
|
||||||
tagNumber: { startsWith: '1A40603000' }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log(`Cleared ${deleted.count} existing demo plants.`);
|
|
||||||
|
|
||||||
// 4. Create Plants
|
|
||||||
let createdCount = 0;
|
|
||||||
|
|
||||||
for (const [index, pos] of positions.entries()) {
|
|
||||||
const batch = batches[index % batches.length];
|
|
||||||
|
|
||||||
// METRC Tag format: 1A40603000 + 7 digits
|
|
||||||
const tagSuffix = (1000000 + index).toString();
|
|
||||||
const tag = `1A40603000${tagSuffix}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const plant = await prisma.facilityPlant.create({
|
|
||||||
data: {
|
|
||||||
tagNumber: tag,
|
|
||||||
// valid fields from schema:
|
|
||||||
batchId: batch.id,
|
|
||||||
positionId: pos.id,
|
|
||||||
address: `${pos.section.room.code}-${pos.section.code}-R${pos.row}C${pos.column}`,
|
|
||||||
status: 'ACTIVE'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 5. Create Location History (Audit)
|
|
||||||
await prisma.plantLocationHistory.create({
|
|
||||||
data: {
|
|
||||||
plantId: plant.id,
|
|
||||||
fromAddress: null,
|
|
||||||
toAddress: plant.address,
|
|
||||||
movedAt: new Date(),
|
|
||||||
reason: 'Initial planting',
|
|
||||||
movedById: 'system'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Maybe a move for some plants
|
|
||||||
if (index % 3 === 0) {
|
|
||||||
const oldAddress = `NUR-1-R1C1`;
|
|
||||||
await prisma.plantLocationHistory.create({
|
|
||||||
data: {
|
|
||||||
plantId: plant.id,
|
|
||||||
fromAddress: oldAddress,
|
|
||||||
toAddress: plant.address,
|
|
||||||
movedAt: new Date(Date.now() - Math.random() * 5 * 24 * 60 * 60 * 1000),
|
|
||||||
reason: 'Moved to flower',
|
|
||||||
movedById: 'system'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
createdCount++;
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore unique constraint errors if position already taken (though we cleared matches)
|
|
||||||
if (!error.message.includes('Unique constraint')) {
|
|
||||||
console.error(`Failed to create plant ${tag}:`, error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`✨ Successfully created ${createdCount} METRC demo plants linked to batches.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
process.exit(1);
|
|
||||||
})
|
|
||||||
.finally(async () => {
|
|
||||||
await prisma.$disconnect();
|
|
||||||
});
|
|
||||||
|
|
@ -36,7 +36,7 @@ This audit reviews the CA Grow Ops Manager against the defined specification doc
|
||||||
| `labor-and-hours.md` | ✅ Complete | ✅ | ✅ | ✅ 14 days | Timeclock page |
|
| `labor-and-hours.md` | ✅ Complete | ✅ | ✅ | ✅ 14 days | Timeclock page |
|
||||||
| `inventory-and-materials.md` | ✅ Complete | ✅ | ✅ | ✅ 12 items | Supplies page |
|
| `inventory-and-materials.md` | ✅ Complete | ✅ | ✅ | ✅ 12 items | Supplies page |
|
||||||
| `photo-management.md` | ⚠️ Partial | ✅ Upload UI | ✅ | 🔲 | Photo compression, no demo photos |
|
| `photo-management.md` | ⚠️ Partial | ✅ Upload UI | ✅ | 🔲 | Photo compression, no demo photos |
|
||||||
| `metrc-integration.md` | ✅ Dashboard | ✅ Dashboard | ✅ Routes | 🔲 Demo mode | Added Dec 17 - CSV export, audit |
|
| `metrc-integration.md` | ⚠️ Spec Only | 🔲 | ⚠️ Routes exist | 🔲 | Placeholder for future |
|
||||||
| `paperless-integration.md` | 🔲 Spec Only | 🔲 | 🔲 | 🔲 | Future integration |
|
| `paperless-integration.md` | 🔲 Spec Only | 🔲 | 🔲 | 🔲 | Future integration |
|
||||||
| `hardware-integration.md` | 🔲 Spec Only | 🔲 | 🔲 | 🔲 | Sensors, etc. |
|
| `hardware-integration.md` | 🔲 Spec Only | 🔲 | 🔲 | 🔲 | Sensors, etc. |
|
||||||
| `communications-and-notifications.md` | ⚠️ Partial | ✅ Announcements | ✅ | ✅ 3 announcements | No push/email |
|
| `communications-and-notifications.md` | ⚠️ Partial | ✅ Announcements | ✅ | ✅ 3 announcements | No push/email |
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Plus, MoveRight, Sprout, Leaf, Flower, Archive, Scale, ClipboardList, Bug, Search, ChevronRight, Calendar, Cloud, CloudOff, CheckCircle } from 'lucide-react';
|
import { Plus, MoveRight, Sprout, Leaf, Flower, Archive, Scale, ClipboardList, Bug, Search, ChevronRight, Calendar } from 'lucide-react';
|
||||||
import { Batch, batchesApi } from '../lib/batchesApi';
|
import { Batch, batchesApi } from '../lib/batchesApi';
|
||||||
import { useToast } from '../context/ToastContext';
|
import { useToast } from '../context/ToastContext';
|
||||||
import BatchTransitionModal from '../components/BatchTransitionModal';
|
import BatchTransitionModal from '../components/BatchTransitionModal';
|
||||||
|
|
@ -62,24 +62,6 @@ function DaysBadge({ startDate }: { startDate: string }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// METRC Sync Status Badge
|
|
||||||
function MetrcBadge({ synced = true }: { synced?: boolean }) {
|
|
||||||
if (synced) {
|
|
||||||
return (
|
|
||||||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-medium bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400" title="Synced with METRC">
|
|
||||||
<Cloud size={9} />
|
|
||||||
METRC
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-medium bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400" title="Not synced with METRC">
|
|
||||||
<CloudOff size={9} />
|
|
||||||
Pending
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function BatchesPage() {
|
export default function BatchesPage() {
|
||||||
const { addToast } = useToast();
|
const { addToast } = useToast();
|
||||||
const [batches, setBatches] = useState<Batch[]>([]);
|
const [batches, setBatches] = useState<Batch[]>([]);
|
||||||
|
|
@ -177,7 +159,6 @@ export default function BatchesPage() {
|
||||||
<span className="text-[10px] font-mono font-medium px-1.5 py-0.5 rounded bg-accent-muted text-accent">
|
<span className="text-[10px] font-mono font-medium px-1.5 py-0.5 rounded bg-accent-muted text-accent">
|
||||||
{batchCode}
|
{batchCode}
|
||||||
</span>
|
</span>
|
||||||
<MetrcBadge synced={true} />
|
|
||||||
<h4 className="font-medium text-primary text-sm truncate">
|
<h4 className="font-medium text-primary text-sm truncate">
|
||||||
{batch.strain}
|
{batch.strain}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
|
||||||
|
|
@ -1,141 +1,23 @@
|
||||||
import { useEffect, useState, Suspense, useRef, useMemo } from 'react';
|
import { useEffect, useState, Suspense } from 'react';
|
||||||
import { Canvas, useFrame } from '@react-three/fiber';
|
import { Canvas } from '@react-three/fiber';
|
||||||
import { OrbitControls, Text, Instances, Instance, Html } from '@react-three/drei';
|
|
||||||
import { layoutApi, Floor3DData } from '../lib/layoutApi';
|
import { layoutApi, Floor3DData } from '../lib/layoutApi';
|
||||||
|
// No drei, no complex icons to isolate crash
|
||||||
|
// import { OrbitControls } from '@react-three/drei';
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { Loader2, ArrowLeft } from 'lucide-react';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
// Colors
|
|
||||||
const COLORS = {
|
|
||||||
VEG: '#4ade80', // green-400
|
|
||||||
FLOWER: '#a855f7', // purple-500
|
|
||||||
DRY: '#f59e0b', // amber-500
|
|
||||||
CURE: '#78716c', // stone-500
|
|
||||||
EMPTY_SLOT: '#374151', // gray-700
|
|
||||||
ROOM_FLOOR: '#1f2937', // gray-800
|
|
||||||
ROOM_WALL: '#374151', // gray-700
|
|
||||||
};
|
|
||||||
|
|
||||||
function PlantInstances({ positions, onPlantClick }: { positions: any[], onPlantClick: (p: any) => void }) {
|
|
||||||
if (!positions || positions.length === 0) return null;
|
|
||||||
|
|
||||||
const plants = positions.filter(p => p.plant);
|
|
||||||
const emptySlots = positions.filter(p => !p.plant);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group>
|
|
||||||
{/* Active Plants */}
|
|
||||||
<Instances
|
|
||||||
range={plants.length}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const index = e.instanceId;
|
|
||||||
if (index !== undefined && plants[index]) {
|
|
||||||
onPlantClick(plants[index]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<sphereGeometry args={[0.15, 16, 16]} />
|
|
||||||
<meshStandardMaterial />
|
|
||||||
{plants.map((pos, i) => (
|
|
||||||
<Instance
|
|
||||||
key={pos.id}
|
|
||||||
position={[pos.x, pos.y, pos.z]}
|
|
||||||
color={pos.plant?.stage === 'FLOWER' ? COLORS.FLOWER :
|
|
||||||
pos.plant?.stage === 'DRYING' ? COLORS.DRY :
|
|
||||||
pos.plant?.stage === 'CURE' ? COLORS.CURE : COLORS.VEG}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Instances>
|
|
||||||
|
|
||||||
{/* Empty Slots (Small dots) */}
|
|
||||||
<Instances range={emptySlots.length}>
|
|
||||||
<sphereGeometry args={[0.05, 8, 8]} />
|
|
||||||
<meshStandardMaterial color={COLORS.EMPTY_SLOT} opacity={0.5} transparent />
|
|
||||||
{emptySlots.map((pos, i) => (
|
|
||||||
<Instance
|
|
||||||
key={pos.id}
|
|
||||||
position={[pos.x, pos.y, pos.z]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Instances>
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FacilityScene({ data, onSelectPlant }: { data: Floor3DData, onSelectPlant: (p: any) => void }) {
|
|
||||||
// Process data into flat list of objects for rendering
|
|
||||||
const roomMeshes = useMemo(() => {
|
|
||||||
return data.rooms.map(room => {
|
|
||||||
// Room creates a floor area
|
|
||||||
return (
|
|
||||||
<group key={room.id} position={[room.posX, 0, room.posY]}>
|
|
||||||
{/* Floor Label */}
|
|
||||||
<Text
|
|
||||||
position={[room.width / 2, 0.1, room.height / 2]}
|
|
||||||
rotation={[-Math.PI / 2, 0, 0]}
|
|
||||||
fontSize={0.5}
|
|
||||||
color="white"
|
|
||||||
fillOpacity={0.5}
|
|
||||||
>
|
|
||||||
{room.name}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{/* Floor Plane */}
|
|
||||||
<mesh
|
|
||||||
rotation={[-Math.PI / 2, 0, 0]}
|
|
||||||
position={[room.width / 2, 0, room.height / 2]}
|
|
||||||
>
|
|
||||||
<planeGeometry args={[room.width, room.height]} />
|
|
||||||
<meshStandardMaterial color={COLORS.ROOM_FLOOR} />
|
|
||||||
</mesh>
|
|
||||||
|
|
||||||
{/* Render sections and positions */}
|
|
||||||
{room.sections.map(section => {
|
|
||||||
// Calculate positions for this section
|
|
||||||
const positions = section.positions.map(pos => {
|
|
||||||
// Simple grid layout logic for position coordinates relative to section
|
|
||||||
// Assuming typical rack dimensions
|
|
||||||
const spacing = 0.5;
|
|
||||||
const x = section.posX + (pos.column * spacing);
|
|
||||||
const z = section.posY + (pos.row * spacing); // Z is depth/row
|
|
||||||
const y = 0.5 + (pos.tier * 0.5); // Y is height/tier
|
|
||||||
|
|
||||||
return { ...pos, x, y, z };
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group key={section.id}>
|
|
||||||
{/* Section Label */}
|
|
||||||
<Text
|
|
||||||
position={[section.posX, 2.5, section.posY]}
|
|
||||||
fontSize={0.3}
|
|
||||||
color="#9ca3af"
|
|
||||||
>
|
|
||||||
{section.code}
|
|
||||||
</Text>
|
|
||||||
<PlantInstances positions={positions} onPlantClick={onSelectPlant} />
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, [data, onSelectPlant]);
|
|
||||||
|
|
||||||
|
function SafeScene({ data }: { data: Floor3DData | null }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ambientLight intensity={0.7} />
|
<ambientLight intensity={0.5} />
|
||||||
<pointLight position={[10, 10, 10]} intensity={1} />
|
<directionalLight position={[10, 10, 5]} />
|
||||||
<directionalLight position={[-10, 20, -10]} intensity={0.5} />
|
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -1, 0]}>
|
||||||
|
<planeGeometry args={[20, 20]} />
|
||||||
<group position={[-data.floor.width / 2, 0, -data.floor.height / 2]}>
|
<meshStandardMaterial color="gray" />
|
||||||
{roomMeshes}
|
</mesh>
|
||||||
</group>
|
<mesh position={[0, 0, 0]}>
|
||||||
|
<boxGeometry args={[1, 1, 1]} />
|
||||||
<OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI / 2.2} />
|
<meshStandardMaterial color="blue" />
|
||||||
<gridHelper args={[100, 100, 0x444444, 0x222222]} />
|
</mesh>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -143,111 +25,45 @@ function FacilityScene({ data, onSelectPlant }: { data: Floor3DData, onSelectPla
|
||||||
export default function Facility3DViewerPage() {
|
export default function Facility3DViewerPage() {
|
||||||
const [status, setStatus] = useState('Initializing...');
|
const [status, setStatus] = useState('Initializing...');
|
||||||
const [floorData, setFloorData] = useState<Floor3DData | null>(null);
|
const [floorData, setFloorData] = useState<Floor3DData | null>(null);
|
||||||
const [selectedPlant, setSelectedPlant] = useState<any | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData();
|
setStatus('Loading floors...');
|
||||||
}, []);
|
layoutApi.getProperties()
|
||||||
|
.then(props => {
|
||||||
async function loadData() {
|
setStatus('Floors loaded. Fetching 3D data...');
|
||||||
setStatus('Loading layout...');
|
// Just grab first floor for test
|
||||||
try {
|
|
||||||
const props = await layoutApi.getProperties();
|
|
||||||
if (props[0]?.buildings[0]?.floors[0]) {
|
if (props[0]?.buildings[0]?.floors[0]) {
|
||||||
const floorId = props[0].buildings[0].floors[0].id;
|
const floorId = props[0].buildings[0].floors[0].id;
|
||||||
setStatus('Fetching 3D assets...');
|
return layoutApi.getFloor3D(floorId);
|
||||||
const data = await layoutApi.getFloor3D(floorId);
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data) {
|
||||||
setFloorData(data);
|
setFloorData(data);
|
||||||
setStatus('');
|
setStatus('Data loaded. Rendering...');
|
||||||
} else {
|
} else {
|
||||||
setStatus('No floor layout found');
|
setStatus('No floor data found.');
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setStatus('Error: ' + (err as Error).message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
setStatus('Error loading data: ' + err.message);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-full relative bg-gray-950 text-white overflow-hidden">
|
<div className="h-screen w-full flex flex-col items-center justify-center bg-gray-900 text-white">
|
||||||
{/* Header Overlay */}
|
<h1 className="text-xl font-bold mb-4">3D Viewer Safe Mode</h1>
|
||||||
<div className="absolute top-0 left-0 right-0 p-4 z-10 flex items-center justify-between bg-gradient-to-b from-black/80 to-transparent pointer-events-none">
|
<p className="mb-4 text-yellow-400">{status}</p>
|
||||||
<div className="pointer-events-auto flex items-center gap-4">
|
|
||||||
<Link to="/metrc" className="btn btn-ghost btn-sm text-white">
|
|
||||||
<ArrowLeft size={16} /> Back
|
|
||||||
</Link>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-xl font-bold flex items-center gap-2">
|
|
||||||
Facility Viewer 3D
|
|
||||||
<span className="badge badge-accent text-xs">BETA</span>
|
|
||||||
</h1>
|
|
||||||
<p className="text-xs text-gray-400">
|
|
||||||
{floorData ? `${floorData.floor.name} • ${floorData.stats.occupiedPositions} Plants` : 'Loading...'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Legend */}
|
<div className="w-[800px] h-[600px] bg-black border border-gray-700">
|
||||||
<div className="flex gap-4 text-xs bg-black/50 p-2 rounded-lg backdrop-blur pointer-events-auto">
|
<Canvas>
|
||||||
<div className="flex items-center gap-1"><div className="w-2 h-2 rounded-full bg-[#4ade80]"></div> Veg</div>
|
|
||||||
<div className="flex items-center gap-1"><div className="w-2 h-2 rounded-full bg-[#a855f7]"></div> Flower</div>
|
|
||||||
<div className="flex items-center gap-1"><div className="w-2 h-2 rounded-full bg-[#f59e0b]"></div> Dry</div>
|
|
||||||
<div className="flex items-center gap-1"><div className="w-2 h-2 rounded-full bg-[#374151]"></div> Empty</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Error/Status Overlay */}
|
|
||||||
{status && (
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center z-50 bg-black/80 backdrop-blur-sm">
|
|
||||||
<div className="text-center">
|
|
||||||
<Loader2 size={32} className="animate-spin text-accent mx-auto mb-2" />
|
|
||||||
<p className="text-gray-300">{status}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Selection Overlay */}
|
|
||||||
{selectedPlant && (
|
|
||||||
<div className="absolute bottom-4 right-4 z-20 w-80 bg-gray-900/90 border border-gray-700 rounded-lg p-4 shadow-xl backdrop-blur-md animate-in slide-in-from-right-10 pointer-events-auto">
|
|
||||||
<div className="flex justify-between items-start mb-2">
|
|
||||||
<h3 className="font-bold text-accent">{selectedPlant.plant.tagNumber}</h3>
|
|
||||||
<button
|
|
||||||
onClick={() => setSelectedPlant(null)}
|
|
||||||
className="text-gray-400 hover:text-white"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 text-sm">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-400">Strain:</span>
|
|
||||||
<span>{selectedPlant.plant.strain || 'Unknown'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-400">Stage:</span>
|
|
||||||
<span className="badge badge-accent bg-opacity-20">{selectedPlant.plant.stage || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-400">Batch:</span>
|
|
||||||
<span>{selectedPlant.plant.batchName || '-'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-400">Location:</span>
|
|
||||||
<span className="font-mono text-xs">R{selectedPlant.row} T{selectedPlant.tier} S{selectedPlant.slot}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Canvas camera={{ position: [10, 15, 10], fov: 50 }}>
|
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
{floorData && (
|
<SafeScene data={floorData} />
|
||||||
<FacilityScene
|
|
||||||
data={floorData}
|
|
||||||
onSelectPlant={setSelectedPlant}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue