localgreenchain/pages/vertical-farm/[farmId]/batches.tsx
Claude 2f7f22ca22
Add vertical farming UI components and pages
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
2025-11-22 18:35:57 +00:00

327 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 { CropBatch, GrowingRecipe, VerticalFarm } from '../../../lib/vertical-farming/types';
import BatchProgress from '../../../components/vertical-farm/BatchProgress';
import GrowthStageIndicator from '../../../components/vertical-farm/GrowthStageIndicator';
export default function BatchManagement() {
const router = useRouter();
const { farmId } = router.query;
const [farm, setFarm] = useState<VerticalFarm | null>(null);
const [batches, setBatches] = useState<CropBatch[]>([]);
const [recipes, setRecipes] = useState<Map<string, GrowingRecipe>>(new Map());
const [selectedBatch, setSelectedBatch] = useState<CropBatch | null>(null);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('active');
useEffect(() => {
if (farmId) {
fetchData();
}
}, [farmId]);
const fetchData = async () => {
try {
const [farmRes, batchesRes, recipesRes] = await Promise.all([
fetch(`/api/vertical-farm/${farmId}`),
fetch(`/api/vertical-farm/${farmId}/batches`),
fetch('/api/vertical-farm/recipes'),
]);
const farmData = await farmRes.json();
const batchesData = await batchesRes.json();
const recipesData = await recipesRes.json();
if (farmData.success) setFarm(farmData.farm);
if (batchesData.success) setBatches(batchesData.batches);
if (recipesData.success) {
const recipeMap = new Map<string, GrowingRecipe>();
recipesData.recipes.forEach((r: GrowingRecipe) => recipeMap.set(r.id, r));
setRecipes(recipeMap);
}
} catch (error) {
console.error('Error fetching data:', error);
} finally {
setLoading(false);
}
};
const filteredBatches = batches.filter(batch => {
if (filter === 'active') {
return batch.status !== 'completed' && batch.status !== 'failed';
}
if (filter === 'completed') {
return batch.status === 'completed' || batch.status === 'failed';
}
return true;
});
const handleHarvest = async (batch: CropBatch) => {
const yieldKg = prompt('Enter actual yield in kg:', batch.expectedYieldKg.toString());
if (!yieldKg) return;
const grade = prompt('Enter quality grade (A, B, C):', 'A');
if (!grade) return;
try {
const response = await fetch(`/api/vertical-farm/batch/${batch.id}/harvest`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
actualYieldKg: parseFloat(yieldKg),
qualityGrade: grade,
}),
});
const data = await response.json();
if (data.success) {
fetchData();
setSelectedBatch(null);
}
} catch (error) {
console.error('Error completing harvest:', 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>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100">
<Head>
<title>Crop Batches - {farm?.name || 'Vertical Farm'}</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">&larr; Back</a>
</Link>
<h1 className="text-2xl font-bold text-gray-900">Crop Batches</h1>
</div>
<Link href={`/vertical-farm/${farmId}/zones`}>
<a className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700">
Start New Batch
</a>
</Link>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 py-8 sm:px-6 lg:px-8">
{/* Filter Tabs */}
<div className="flex gap-4 mb-6">
{(['active', 'completed', 'all'] as const).map(f => (
<button
key={f}
onClick={() => setFilter(f)}
className={`px-4 py-2 rounded-lg font-medium transition ${
filter === f
? 'bg-green-600 text-white'
: 'bg-white text-gray-700 hover:bg-gray-100'
}`}
>
{f.charAt(0).toUpperCase() + f.slice(1)}
{f === 'active' && (
<span className="ml-2 px-2 py-0.5 bg-white bg-opacity-20 rounded-full text-sm">
{batches.filter(b => b.status !== 'completed' && b.status !== 'failed').length}
</span>
)}
</button>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Batch List */}
<div className="lg:col-span-2 space-y-4">
{filteredBatches.length === 0 ? (
<div className="bg-white rounded-lg shadow p-8 text-center">
<p className="text-gray-600 mb-4">No {filter} batches found</p>
<Link href={`/vertical-farm/${farmId}/zones`}>
<a className="text-green-600 hover:text-green-700">Start a new batch &rarr;</a>
</Link>
</div>
) : (
filteredBatches.map(batch => (
<div
key={batch.id}
onClick={() => setSelectedBatch(batch)}
className={`cursor-pointer transition ${
selectedBatch?.id === batch.id ? 'ring-2 ring-green-500' : ''
}`}
>
<BatchProgress
batch={batch}
recipe={recipes.get(batch.recipeId)}
showDetails={selectedBatch?.id !== batch.id}
/>
</div>
))
)}
</div>
{/* Batch Detail */}
<div className="space-y-6">
{selectedBatch ? (
<>
<div className="bg-white rounded-lg shadow-lg p-6">
<h2 className="text-xl font-bold text-gray-900 mb-4">
{selectedBatch.cropType}
{selectedBatch.variety && ` - ${selectedBatch.variety}`}
</h2>
<div className="space-y-4 text-sm">
<div className="grid grid-cols-2 gap-3">
<div>
<p className="text-gray-600">Batch ID</p>
<p className="font-mono text-xs">{selectedBatch.id}</p>
</div>
<div>
<p className="text-gray-600">Status</p>
<p className="font-semibold capitalize">{selectedBatch.status}</p>
</div>
<div>
<p className="text-gray-600">Planted</p>
<p className="font-medium">
{new Date(selectedBatch.plantingDate).toLocaleDateString()}
</p>
</div>
<div>
<p className="text-gray-600">Expected Harvest</p>
<p className="font-medium">
{new Date(selectedBatch.expectedHarvestDate).toLocaleDateString()}
</p>
</div>
<div>
<p className="text-gray-600">Plant Count</p>
<p className="font-medium">{selectedBatch.plantCount.toLocaleString()}</p>
</div>
<div>
<p className="text-gray-600">Health Score</p>
<p
className={`font-bold ${
selectedBatch.healthScore >= 80
? 'text-green-600'
: selectedBatch.healthScore >= 60
? 'text-yellow-600'
: 'text-red-600'
}`}
>
{selectedBatch.healthScore}%
</p>
</div>
</div>
{selectedBatch.actualHarvestDate && (
<div className="pt-4 border-t">
<h4 className="font-medium text-gray-900 mb-2">Harvest Results</h4>
<div className="grid grid-cols-2 gap-3">
<div>
<p className="text-gray-600">Actual Yield</p>
<p className="font-bold text-green-600">
{selectedBatch.actualYieldKg?.toFixed(1)} kg
</p>
</div>
<div>
<p className="text-gray-600">Quality Grade</p>
<p className="font-bold">{selectedBatch.qualityGrade}</p>
</div>
</div>
</div>
)}
</div>
</div>
{/* Growth Stage Progress */}
{recipes.get(selectedBatch.recipeId) && (
<div className="bg-white rounded-lg shadow-lg p-6">
<h3 className="font-semibold text-gray-900 mb-4">Growth Progress</h3>
<GrowthStageIndicator
stages={recipes.get(selectedBatch.recipeId)!.stages}
currentStage={selectedBatch.currentStage}
currentDay={selectedBatch.currentDay}
/>
</div>
)}
{/* Issues */}
{selectedBatch.issues.length > 0 && (
<div className="bg-white rounded-lg shadow-lg p-6">
<h3 className="font-semibold text-gray-900 mb-4">
Issues ({selectedBatch.issues.filter(i => !i.resolvedAt).length} active)
</h3>
<div className="space-y-2">
{selectedBatch.issues.map(issue => (
<div
key={issue.id}
className={`p-3 rounded-lg ${
issue.resolvedAt ? 'bg-gray-100' : 'bg-red-50'
}`}
>
<div className="flex justify-between">
<span className="font-medium capitalize">{issue.type}</span>
<span
className={`text-xs font-semibold ${
issue.severity === 'critical'
? 'text-red-600'
: issue.severity === 'severe'
? 'text-orange-600'
: 'text-yellow-600'
}`}
>
{issue.severity}
</span>
</div>
<p className="text-sm text-gray-600">{issue.description}</p>
{issue.resolvedAt && (
<p className="text-xs text-green-600 mt-1">
Resolved: {issue.resolution}
</p>
)}
</div>
))}
</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">
{selectedBatch.status === 'ready' && (
<button
onClick={() => handleHarvest(selectedBatch)}
className="w-full px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
>
Complete Harvest
</button>
)}
<button className="w-full px-4 py-2 bg-yellow-100 text-yellow-700 rounded-lg hover:bg-yellow-200">
Report Issue
</button>
<button className="w-full px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200">
View Environment Log
</button>
</div>
</div>
</>
) : (
<div className="bg-white rounded-lg shadow-lg p-6 text-center">
<p className="text-gray-600">Select a batch to view details</p>
</div>
)}
</div>
</div>
</main>
</div>
);
}