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
289 lines
13 KiB
TypeScript
289 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 { FarmAnalytics, ResourceUsage, VerticalFarm } from '../../../lib/vertical-farming/types';
|
|
import ResourceUsageChart from '../../../components/vertical-farm/ResourceUsageChart';
|
|
|
|
export default function FarmAnalyticsPage() {
|
|
const router = useRouter();
|
|
const { farmId } = router.query;
|
|
const [farm, setFarm] = useState<VerticalFarm | null>(null);
|
|
const [analytics, setAnalytics] = useState<FarmAnalytics | null>(null);
|
|
const [resourceUsage, setResourceUsage] = useState<ResourceUsage | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [period, setPeriod] = useState(30);
|
|
|
|
useEffect(() => {
|
|
if (farmId) {
|
|
fetchData();
|
|
}
|
|
}, [farmId, period]);
|
|
|
|
const fetchData = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const [farmRes, analyticsRes] = await Promise.all([
|
|
fetch(`/api/vertical-farm/${farmId}`),
|
|
fetch(`/api/vertical-farm/${farmId}/analytics?period=${period}`),
|
|
]);
|
|
|
|
const farmData = await farmRes.json();
|
|
const analyticsData = await analyticsRes.json();
|
|
|
|
if (farmData.success) setFarm(farmData.farm);
|
|
if (analyticsData.success) {
|
|
setAnalytics(analyticsData.analytics);
|
|
if (analyticsData.resourceUsage) {
|
|
setResourceUsage(analyticsData.resourceUsage);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching analytics:', 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="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>Analytics - {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">← Back</a>
|
|
</Link>
|
|
<h1 className="text-2xl font-bold text-gray-900">Farm Analytics</h1>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
{[7, 30, 90].map(p => (
|
|
<button
|
|
key={p}
|
|
onClick={() => setPeriod(p)}
|
|
className={`px-3 py-1 rounded-lg text-sm ${
|
|
period === p
|
|
? 'bg-green-600 text-white'
|
|
: 'bg-white text-gray-700 hover:bg-gray-100'
|
|
}`}
|
|
>
|
|
{p}d
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="max-w-7xl mx-auto px-4 py-8 sm:px-6 lg:px-8">
|
|
{analytics ? (
|
|
<div className="space-y-8">
|
|
{/* Production Overview */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<p className="text-sm text-gray-600">Total Yield</p>
|
|
<p className="text-3xl font-bold text-green-600">{analytics.totalYieldKg} kg</p>
|
|
<p className="text-xs text-gray-500">{period} day period</p>
|
|
</div>
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<p className="text-sm text-gray-600">Yield/m²/year</p>
|
|
<p className="text-3xl font-bold text-blue-600">{analytics.yieldPerSqmPerYear} kg</p>
|
|
</div>
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<p className="text-sm text-gray-600">Crop Cycles</p>
|
|
<p className="text-3xl font-bold text-purple-600">{analytics.cropCyclesCompleted}</p>
|
|
<p className="text-xs text-gray-500">Avg {analytics.averageCyclesDays.toFixed(0)} days</p>
|
|
</div>
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<p className="text-sm text-gray-600">Success Rate</p>
|
|
<p className="text-3xl font-bold text-teal-600">{analytics.cropSuccessRate}%</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quality Metrics */}
|
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
<h2 className="text-xl font-bold text-gray-900 mb-6">Quality Metrics</h2>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
|
<div className="text-center">
|
|
<div className="w-24 h-24 mx-auto mb-2 relative">
|
|
<svg className="w-24 h-24 transform -rotate-90">
|
|
<circle
|
|
cx="48"
|
|
cy="48"
|
|
r="40"
|
|
stroke="#e5e7eb"
|
|
strokeWidth="8"
|
|
fill="none"
|
|
/>
|
|
<circle
|
|
cx="48"
|
|
cy="48"
|
|
r="40"
|
|
stroke="#10b981"
|
|
strokeWidth="8"
|
|
fill="none"
|
|
strokeDasharray={`${(analytics.averageQualityScore / 100) * 251.2} 251.2`}
|
|
/>
|
|
</svg>
|
|
<span className="absolute inset-0 flex items-center justify-center text-xl font-bold text-gray-900">
|
|
{analytics.averageQualityScore.toFixed(0)}
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-gray-600">Quality Score</p>
|
|
</div>
|
|
<div className="text-center">
|
|
<p className="text-4xl font-bold text-green-600">{analytics.gradeAPercent}%</p>
|
|
<p className="text-sm text-gray-600">Grade A</p>
|
|
</div>
|
|
<div className="text-center">
|
|
<p className="text-4xl font-bold text-orange-600">{analytics.wastagePercent}%</p>
|
|
<p className="text-sm text-gray-600">Wastage</p>
|
|
</div>
|
|
<div className="text-center">
|
|
<p className="text-4xl font-bold text-blue-600">{analytics.spaceUtilization}%</p>
|
|
<p className="text-sm text-gray-600">Space Utilization</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Financial Overview */}
|
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
<h2 className="text-xl font-bold text-gray-900 mb-6">Financial Performance</h2>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
|
<div className="bg-green-50 rounded-lg p-4">
|
|
<p className="text-sm text-green-700">Revenue</p>
|
|
<p className="text-2xl font-bold text-green-900">
|
|
${analytics.revenueUsd.toLocaleString()}
|
|
</p>
|
|
</div>
|
|
<div className="bg-red-50 rounded-lg p-4">
|
|
<p className="text-sm text-red-700">Costs</p>
|
|
<p className="text-2xl font-bold text-red-900">
|
|
${analytics.costUsd.toLocaleString()}
|
|
</p>
|
|
</div>
|
|
<div className="bg-blue-50 rounded-lg p-4">
|
|
<p className="text-sm text-blue-700">Profit Margin</p>
|
|
<p className="text-2xl font-bold text-blue-900">{analytics.profitMarginPercent}%</p>
|
|
</div>
|
|
<div className="bg-purple-50 rounded-lg p-4">
|
|
<p className="text-sm text-purple-700">Revenue/m²/year</p>
|
|
<p className="text-2xl font-bold text-purple-900">
|
|
${analytics.revenuePerSqm.toLocaleString()}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Environmental Impact */}
|
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
<h2 className="text-xl font-bold text-gray-900 mb-6">Environmental Impact</h2>
|
|
<div className="grid grid-cols-3 gap-6">
|
|
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
|
<p className="text-3xl font-bold text-gray-900">
|
|
{analytics.carbonFootprintKgPerKg.toFixed(2)}
|
|
</p>
|
|
<p className="text-sm text-gray-600">kg CO2 / kg produce</p>
|
|
<p className="text-xs text-green-600 mt-1">90% less than field farming</p>
|
|
</div>
|
|
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
|
<p className="text-3xl font-bold text-gray-900">
|
|
{analytics.waterUseLPerKg.toFixed(1)}
|
|
</p>
|
|
<p className="text-sm text-gray-600">L water / kg produce</p>
|
|
<p className="text-xs text-green-600 mt-1">95% less than field farming</p>
|
|
</div>
|
|
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
|
<p className="text-3xl font-bold text-gray-900">
|
|
{analytics.energyUseKwhPerKg.toFixed(1)}
|
|
</p>
|
|
<p className="text-sm text-gray-600">kWh / kg produce</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Top Crops */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
<h3 className="font-semibold text-gray-900 mb-4">Top Crops by Yield</h3>
|
|
<div className="space-y-3">
|
|
{analytics.topCropsByYield.map((crop, idx) => (
|
|
<div key={crop.crop} className="flex justify-between items-center">
|
|
<span className="flex items-center gap-2">
|
|
<span className="text-sm font-semibold text-gray-500">{idx + 1}</span>
|
|
<span className="font-medium">{crop.crop}</span>
|
|
</span>
|
|
<span className="text-green-600 font-semibold">{crop.yieldKg} kg</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
<h3 className="font-semibold text-gray-900 mb-4">Top Crops by Revenue</h3>
|
|
<div className="space-y-3">
|
|
{analytics.topCropsByRevenue.map((crop, idx) => (
|
|
<div key={crop.crop} className="flex justify-between items-center">
|
|
<span className="flex items-center gap-2">
|
|
<span className="text-sm font-semibold text-gray-500">{idx + 1}</span>
|
|
<span className="font-medium">{crop.crop}</span>
|
|
</span>
|
|
<span className="text-blue-600 font-semibold">${crop.revenueUsd}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
<h3 className="font-semibold text-gray-900 mb-4">Top Crops by Efficiency</h3>
|
|
<div className="space-y-3">
|
|
{analytics.topCropsByEfficiency.map((crop, idx) => (
|
|
<div key={crop.crop} className="flex justify-between items-center">
|
|
<span className="flex items-center gap-2">
|
|
<span className="text-sm font-semibold text-gray-500">{idx + 1}</span>
|
|
<span className="font-medium">{crop.crop}</span>
|
|
</span>
|
|
<span className="text-purple-600 font-semibold">{crop.efficiencyScore}%</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Resource Usage */}
|
|
{resourceUsage && (
|
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
<h2 className="text-xl font-bold text-gray-900 mb-6">Resource Usage</h2>
|
|
<ResourceUsageChart usage={resourceUsage} />
|
|
</div>
|
|
)}
|
|
|
|
{/* Generated Timestamp */}
|
|
<p className="text-center text-sm text-gray-500">
|
|
Analytics generated: {new Date(analytics.generatedAt).toLocaleString()}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="bg-white rounded-lg shadow-lg p-12 text-center">
|
|
<p className="text-gray-600 mb-4">No analytics data available yet</p>
|
|
<p className="text-sm text-gray-500">
|
|
Complete some crop batches to see analytics data
|
|
</p>
|
|
</div>
|
|
)}
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|