Components: - FarmCard: Farm summary display with status and metrics - ZoneGrid: Multi-level zone layout visualization - ZoneDetailCard: Individual zone details with environment readings - EnvironmentGauge: Real-time environmental parameter display - BatchProgress: Crop batch progress tracking with health scores - RecipeSelector: Growing recipe browser and selector - AlertPanel: Environment alerts display and management - GrowthStageIndicator: Visual growth stage progress tracker - ResourceUsageChart: Energy/water usage analytics visualization Pages: - /vertical-farm: Dashboard with farm listing and stats - /vertical-farm/register: Multi-step farm registration form - /vertical-farm/[farmId]: Farm detail view with zones and alerts - /vertical-farm/[farmId]/zones: Zone management with batch starting - /vertical-farm/[farmId]/batches: Batch management and harvesting - /vertical-farm/[farmId]/analytics: Farm analytics and performance metrics
315 lines
13 KiB
TypeScript
315 lines
13 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { useRouter } from 'next/router';
|
|
import Link from 'next/link';
|
|
import Head from 'next/head';
|
|
import { VerticalFarm, GrowingZone, GrowingRecipe } from '../../../lib/vertical-farming/types';
|
|
import ZoneGrid from '../../../components/vertical-farm/ZoneGrid';
|
|
import ZoneDetailCard from '../../../components/vertical-farm/ZoneDetailCard';
|
|
import EnvironmentGauge from '../../../components/vertical-farm/EnvironmentGauge';
|
|
import RecipeSelector from '../../../components/vertical-farm/RecipeSelector';
|
|
|
|
export default function ZoneManagement() {
|
|
const router = useRouter();
|
|
const { farmId } = router.query;
|
|
const [farm, setFarm] = useState<VerticalFarm | null>(null);
|
|
const [selectedZone, setSelectedZone] = useState<GrowingZone | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [showStartBatch, setShowStartBatch] = useState(false);
|
|
const [selectedRecipe, setSelectedRecipe] = useState<GrowingRecipe | null>(null);
|
|
const [plantCount, setPlantCount] = useState('');
|
|
const [seedBatchId, setSeedBatchId] = useState('');
|
|
|
|
useEffect(() => {
|
|
if (farmId) {
|
|
fetchFarm();
|
|
}
|
|
}, [farmId]);
|
|
|
|
const fetchFarm = async () => {
|
|
try {
|
|
const response = await fetch(`/api/vertical-farm/${farmId}`);
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
setFarm(data.farm);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching farm:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleStartBatch = async () => {
|
|
if (!selectedZone || !selectedRecipe) return;
|
|
|
|
try {
|
|
const response = await fetch('/api/vertical-farm/batch/start', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
farmId,
|
|
zoneId: selectedZone.id,
|
|
recipeId: selectedRecipe.id,
|
|
seedBatchId: seedBatchId || `seed-${Date.now()}`,
|
|
plantCount: parseInt(plantCount) || selectedZone.plantPositions,
|
|
}),
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
setShowStartBatch(false);
|
|
setSelectedRecipe(null);
|
|
setPlantCount('');
|
|
setSeedBatchId('');
|
|
fetchFarm();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error starting batch:', error);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100 flex items-center justify-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-green-600" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!farm) {
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100 flex items-center justify-center">
|
|
<p>Farm not found</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100">
|
|
<Head>
|
|
<title>Zone Management - {farm.name}</title>
|
|
</Head>
|
|
|
|
<header className="bg-white shadow-sm">
|
|
<div className="max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<Link href={`/vertical-farm/${farmId}`}>
|
|
<a className="text-gray-600 hover:text-gray-900">← Back</a>
|
|
</Link>
|
|
<h1 className="text-2xl font-bold text-gray-900">Zone Management</h1>
|
|
</div>
|
|
<p className="text-gray-600">{farm.name}</p>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="max-w-7xl mx-auto px-4 py-8 sm:px-6 lg:px-8">
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
{/* Zone Grid */}
|
|
<div className="lg:col-span-2">
|
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
<h2 className="text-xl font-bold text-gray-900 mb-4">All Zones</h2>
|
|
<ZoneGrid
|
|
zones={farm.zones}
|
|
onZoneSelect={setSelectedZone}
|
|
selectedZoneId={selectedZone?.id}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Zone Detail / Actions */}
|
|
<div className="space-y-6">
|
|
{selectedZone ? (
|
|
<>
|
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
<h2 className="text-xl font-bold text-gray-900 mb-4">{selectedZone.name}</h2>
|
|
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
|
<div>
|
|
<p className="text-gray-600">Status</p>
|
|
<p className="font-semibold capitalize">{selectedZone.status}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-600">Method</p>
|
|
<p className="font-semibold">{selectedZone.growingMethod}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-600">Level</p>
|
|
<p className="font-semibold">{selectedZone.level}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-600">Area</p>
|
|
<p className="font-semibold">{selectedZone.areaSqm}m²</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-600">Positions</p>
|
|
<p className="font-semibold">{selectedZone.plantPositions}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-600">Active Plants</p>
|
|
<p className="font-semibold">{selectedZone.plantIds.length}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{selectedZone.currentCrop && (
|
|
<div className="pt-4 border-t">
|
|
<p className="text-gray-600">Current Crop</p>
|
|
<p className="font-semibold text-lg">{selectedZone.currentCrop}</p>
|
|
{selectedZone.expectedHarvestDate && (
|
|
<p className="text-sm text-gray-600">
|
|
Harvest: {new Date(selectedZone.expectedHarvestDate).toLocaleDateString()}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Environment Readings */}
|
|
{selectedZone.currentEnvironment && (
|
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
<h3 className="font-semibold text-gray-900 mb-4">Current Environment</h3>
|
|
<div className="space-y-3">
|
|
<EnvironmentGauge
|
|
label="Temperature"
|
|
value={selectedZone.currentEnvironment.temperatureC}
|
|
unit="°C"
|
|
target={selectedZone.environmentTargets.temperatureC.target}
|
|
min={selectedZone.environmentTargets.temperatureC.min}
|
|
max={selectedZone.environmentTargets.temperatureC.max}
|
|
/>
|
|
<EnvironmentGauge
|
|
label="Humidity"
|
|
value={selectedZone.currentEnvironment.humidityPercent}
|
|
unit="%"
|
|
target={selectedZone.environmentTargets.humidityPercent.target}
|
|
min={selectedZone.environmentTargets.humidityPercent.min}
|
|
max={selectedZone.environmentTargets.humidityPercent.max}
|
|
/>
|
|
<EnvironmentGauge
|
|
label="CO2"
|
|
value={selectedZone.currentEnvironment.co2Ppm}
|
|
unit=" ppm"
|
|
target={selectedZone.environmentTargets.co2Ppm.target}
|
|
min={selectedZone.environmentTargets.co2Ppm.min}
|
|
max={selectedZone.environmentTargets.co2Ppm.max}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Actions */}
|
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
<h3 className="font-semibold text-gray-900 mb-4">Actions</h3>
|
|
<div className="space-y-2">
|
|
{selectedZone.status === 'empty' && (
|
|
<button
|
|
onClick={() => setShowStartBatch(true)}
|
|
className="w-full px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition"
|
|
>
|
|
Start New Batch
|
|
</button>
|
|
)}
|
|
{(selectedZone.status === 'growing' || selectedZone.status === 'ready') && (
|
|
<button className="w-full px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition">
|
|
Harvest
|
|
</button>
|
|
)}
|
|
<button className="w-full px-4 py-2 bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 transition">
|
|
Record Environment
|
|
</button>
|
|
<button className="w-full px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition">
|
|
Edit Zone
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div className="bg-white rounded-lg shadow-lg p-6 text-center">
|
|
<p className="text-gray-600">Select a zone to view details and actions</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Start Batch Modal */}
|
|
{showStartBatch && selectedZone && (
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto m-4">
|
|
<div className="p-6">
|
|
<div className="flex justify-between items-center mb-6">
|
|
<h2 className="text-xl font-bold text-gray-900">Start New Batch</h2>
|
|
<button
|
|
onClick={() => setShowStartBatch(false)}
|
|
className="text-gray-400 hover:text-gray-600"
|
|
>
|
|
×
|
|
</button>
|
|
</div>
|
|
|
|
<p className="text-gray-600 mb-4">Zone: {selectedZone.name}</p>
|
|
|
|
<div className="space-y-6">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Select Growing Recipe
|
|
</label>
|
|
<RecipeSelector
|
|
onSelect={setSelectedRecipe}
|
|
selectedRecipeId={selectedRecipe?.id}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Plant Count
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={plantCount}
|
|
onChange={e => setPlantCount(e.target.value)}
|
|
placeholder={`Max: ${selectedZone.plantPositions}`}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Seed Batch ID (optional)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={seedBatchId}
|
|
onChange={e => setSeedBatchId(e.target.value)}
|
|
placeholder="Auto-generated if empty"
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-4">
|
|
<button
|
|
onClick={() => setShowStartBatch(false)}
|
|
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={handleStartBatch}
|
|
disabled={!selectedRecipe}
|
|
className="flex-1 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50"
|
|
>
|
|
Start Batch
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|