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
264 lines
11 KiB
TypeScript
264 lines
11 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { useRouter } from 'next/router';
|
|
import Link from 'next/link';
|
|
import Head from 'next/head';
|
|
import { VerticalFarm, CropBatch } from '../../../lib/vertical-farming/types';
|
|
import ZoneGrid from '../../../components/vertical-farm/ZoneGrid';
|
|
import AlertPanel from '../../../components/vertical-farm/AlertPanel';
|
|
|
|
export default function FarmDetail() {
|
|
const router = useRouter();
|
|
const { farmId } = router.query;
|
|
const [farm, setFarm] = useState<VerticalFarm | null>(null);
|
|
const [batches, setBatches] = useState<CropBatch[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
if (farmId) {
|
|
fetchFarmDetails();
|
|
}
|
|
}, [farmId]);
|
|
|
|
const fetchFarmDetails = async () => {
|
|
try {
|
|
const [farmRes, batchesRes] = await Promise.all([
|
|
fetch(`/api/vertical-farm/${farmId}`),
|
|
fetch(`/api/vertical-farm/${farmId}/batches`),
|
|
]);
|
|
|
|
const farmData = await farmRes.json();
|
|
const batchesData = await batchesRes.json();
|
|
|
|
if (farmData.success) {
|
|
setFarm(farmData.farm);
|
|
}
|
|
if (batchesData.success) {
|
|
setBatches(batchesData.batches);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching farm:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
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="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-green-600 mx-auto" />
|
|
<p className="text-gray-600 mt-4">Loading farm details...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!farm) {
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100 flex items-center justify-center">
|
|
<div className="text-center">
|
|
<h1 className="text-2xl font-bold text-gray-900 mb-4">Farm Not Found</h1>
|
|
<Link href="/vertical-farm">
|
|
<a className="text-green-600 hover:text-green-700">Back to Dashboard</a>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const allAlerts = farm.zones.flatMap(z => z.currentEnvironment?.alerts || []);
|
|
const activeBatches = batches.filter(b => b.status !== 'completed' && b.status !== 'failed');
|
|
const upcomingHarvests = activeBatches.filter(b => {
|
|
const daysUntil = (new Date(b.expectedHarvestDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24);
|
|
return daysUntil <= 7 && daysUntil >= 0;
|
|
});
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100">
|
|
<Head>
|
|
<title>{farm.name} - LocalGreenChain</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">
|
|
<a className="text-gray-600 hover:text-gray-900">← Back</a>
|
|
</Link>
|
|
<h1 className="text-2xl font-bold text-gray-900">{farm.name}</h1>
|
|
<span
|
|
className={`px-3 py-1 rounded-full text-xs font-semibold ${
|
|
farm.status === 'operational'
|
|
? 'bg-green-100 text-green-800'
|
|
: farm.status === 'maintenance'
|
|
? 'bg-yellow-100 text-yellow-800'
|
|
: 'bg-gray-100 text-gray-800'
|
|
}`}
|
|
>
|
|
{farm.status}
|
|
</span>
|
|
</div>
|
|
<nav className="flex gap-3">
|
|
<Link href={`/vertical-farm/${farmId}/zones`}>
|
|
<a className="px-4 py-2 bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200">Zones</a>
|
|
</Link>
|
|
<Link href={`/vertical-farm/${farmId}/batches`}>
|
|
<a className="px-4 py-2 bg-purple-100 text-purple-700 rounded-lg hover:bg-purple-200">Batches</a>
|
|
</Link>
|
|
<Link href={`/vertical-farm/${farmId}/analytics`}>
|
|
<a className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700">Analytics</a>
|
|
</Link>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="max-w-7xl mx-auto px-4 py-8 sm:px-6 lg:px-8">
|
|
{/* Quick Stats */}
|
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-8">
|
|
<div className="bg-white rounded-lg shadow p-4">
|
|
<p className="text-sm text-gray-600">Active Plants</p>
|
|
<p className="text-2xl font-bold text-green-600">{farm.specs.currentActivePlants.toLocaleString()}</p>
|
|
</div>
|
|
<div className="bg-white rounded-lg shadow p-4">
|
|
<p className="text-sm text-gray-600">Growing Area</p>
|
|
<p className="text-2xl font-bold text-blue-600">{farm.specs.growingAreaSqm}m²</p>
|
|
</div>
|
|
<div className="bg-white rounded-lg shadow p-4">
|
|
<p className="text-sm text-gray-600">Active Batches</p>
|
|
<p className="text-2xl font-bold text-purple-600">{activeBatches.length}</p>
|
|
</div>
|
|
<div className="bg-white rounded-lg shadow p-4">
|
|
<p className="text-sm text-gray-600">Capacity</p>
|
|
<p className="text-2xl font-bold text-orange-600">{farm.currentCapacityUtilization}%</p>
|
|
</div>
|
|
<div className="bg-white rounded-lg shadow p-4">
|
|
<p className="text-sm text-gray-600">Efficiency</p>
|
|
<p className="text-2xl font-bold text-teal-600">{farm.energyEfficiencyScore}%</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Alerts */}
|
|
{allAlerts.length > 0 && (
|
|
<div className="mb-8">
|
|
<AlertPanel alerts={allAlerts} />
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
{/* Main Content */}
|
|
<div className="lg:col-span-2 space-y-6">
|
|
{/* Zone Overview */}
|
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h2 className="text-xl font-bold text-gray-900">Zone Overview</h2>
|
|
<Link href={`/vertical-farm/${farmId}/zones`}>
|
|
<a className="text-green-600 hover:text-green-700 text-sm">View All →</a>
|
|
</Link>
|
|
</div>
|
|
<ZoneGrid zones={farm.zones.slice(0, 6)} />
|
|
</div>
|
|
|
|
{/* Upcoming Harvests */}
|
|
{upcomingHarvests.length > 0 && (
|
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
<h2 className="text-xl font-bold text-gray-900 mb-4">Upcoming Harvests</h2>
|
|
<div className="space-y-3">
|
|
{upcomingHarvests.map(batch => {
|
|
const daysUntil = Math.ceil(
|
|
(new Date(batch.expectedHarvestDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24)
|
|
);
|
|
return (
|
|
<div
|
|
key={batch.id}
|
|
className="flex justify-between items-center p-3 bg-purple-50 rounded-lg"
|
|
>
|
|
<div>
|
|
<p className="font-medium text-gray-900">{batch.cropType}</p>
|
|
<p className="text-sm text-gray-600">{batch.plantCount} plants</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="font-semibold text-purple-600">
|
|
{daysUntil === 0 ? 'Today' : `${daysUntil} day${daysUntil > 1 ? 's' : ''}`}
|
|
</p>
|
|
<p className="text-sm text-gray-600">{batch.expectedYieldKg.toFixed(1)} kg expected</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Sidebar */}
|
|
<div className="space-y-6">
|
|
{/* Farm Info */}
|
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
<h2 className="text-xl font-bold text-gray-900 mb-4">Farm Details</h2>
|
|
<div className="space-y-3 text-sm">
|
|
<div>
|
|
<p className="text-gray-600">Location</p>
|
|
<p className="font-medium">{farm.location.city}, {farm.location.country}</p>
|
|
<p className="text-gray-500">{farm.location.address}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-600">Building</p>
|
|
<p className="font-medium">{farm.specs.buildingType.replace('_', ' ')}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-600">Levels</p>
|
|
<p className="font-medium">{farm.specs.numberOfLevels}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-600">Automation</p>
|
|
<p className="font-medium">{farm.automationLevel.replace('_', ' ')}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-600">Operational Since</p>
|
|
<p className="font-medium">{new Date(farm.operationalSince).toLocaleDateString()}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Actions */}
|
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
<h2 className="text-xl font-bold text-gray-900 mb-4">Quick Actions</h2>
|
|
<div className="space-y-2">
|
|
<Link href={`/vertical-farm/${farmId}/batches?action=new`}>
|
|
<a className="block w-full px-4 py-2 bg-green-100 text-green-700 rounded-lg text-center hover:bg-green-200 transition">
|
|
Start New Batch
|
|
</a>
|
|
</Link>
|
|
<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-purple-100 text-purple-700 rounded-lg hover:bg-purple-200 transition">
|
|
Schedule Maintenance
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Certifications */}
|
|
{farm.specs.certifications.length > 0 && (
|
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
<h2 className="text-lg font-bold text-gray-900 mb-3">Certifications</h2>
|
|
<div className="flex flex-wrap gap-2">
|
|
{farm.specs.certifications.map(cert => (
|
|
<span
|
|
key={cert}
|
|
className="px-3 py-1 bg-green-100 text-green-700 rounded-full text-sm"
|
|
>
|
|
{cert.toUpperCase()}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|