diff --git a/components/vertical-farm/AlertPanel.tsx b/components/vertical-farm/AlertPanel.tsx new file mode 100644 index 0000000..faae033 --- /dev/null +++ b/components/vertical-farm/AlertPanel.tsx @@ -0,0 +1,117 @@ +import { EnvironmentAlert } from '../../lib/vertical-farming/types'; + +interface AlertPanelProps { + alerts: EnvironmentAlert[]; + onAcknowledge?: (alert: EnvironmentAlert) => void; + title?: string; +} + +export default function AlertPanel({ alerts, onAcknowledge, title = 'Environment Alerts' }: AlertPanelProps) { + const activeAlerts = alerts.filter(a => !a.acknowledged); + const acknowledgedAlerts = alerts.filter(a => a.acknowledged); + + const alertStyles: Record = { + low: { bg: 'bg-blue-50', border: 'border-blue-400', icon: 'arrow-down' }, + high: { bg: 'bg-orange-50', border: 'border-orange-400', icon: 'arrow-up' }, + critical_low: { bg: 'bg-red-50', border: 'border-red-500', icon: 'exclamation' }, + critical_high: { bg: 'bg-red-50', border: 'border-red-500', icon: 'exclamation' }, + sensor_fault: { bg: 'bg-gray-50', border: 'border-gray-500', icon: 'warning' }, + }; + + const formatParameter = (param: string) => { + const names: Record = { + temperature: 'Temperature', + humidity: 'Humidity', + co2: 'CO2 Level', + ec: 'EC (Nutrient)', + ph: 'pH Level', + ppfd: 'Light (PPFD)', + }; + return names[param] || param; + }; + + if (alerts.length === 0) { + return ( +
+

All systems operating within normal parameters

+
+ ); + } + + return ( +
+
+

{title}

+ a.type.includes('critical')) + ? 'bg-red-100 text-red-800' + : activeAlerts.length > 0 + ? 'bg-yellow-100 text-yellow-800' + : 'bg-green-100 text-green-800' + }`}> + {activeAlerts.length} Active + +
+ + {activeAlerts.length > 0 && ( +
+ {activeAlerts.map((alert, idx) => { + const style = alertStyles[alert.type] || alertStyles.low; + return ( +
+
+
+
+ + {alert.type.includes('critical') && '!! '} + {formatParameter(alert.parameter)} + {alert.type.includes('low') ? ' LOW' : ' HIGH'} + +
+

+ Current: {alert.value} + {' | '} + Threshold: {alert.threshold} +

+

+ {new Date(alert.timestamp).toLocaleString()} +

+
+ {onAcknowledge && ( + + )} +
+
+ ); + })} +
+ )} + + {acknowledgedAlerts.length > 0 && ( +
+ + {acknowledgedAlerts.length} acknowledged alert(s) + +
+ {acknowledgedAlerts.map((alert, idx) => ( +

+ {formatParameter(alert.parameter)} {alert.type}: {alert.value} + - {new Date(alert.timestamp).toLocaleTimeString()} +

+ ))} +
+
+ )} +
+ ); +} diff --git a/components/vertical-farm/BatchProgress.tsx b/components/vertical-farm/BatchProgress.tsx new file mode 100644 index 0000000..9e65a8d --- /dev/null +++ b/components/vertical-farm/BatchProgress.tsx @@ -0,0 +1,105 @@ +import { CropBatch, GrowingRecipe } from '../../lib/vertical-farming/types'; + +interface BatchProgressProps { + batch: CropBatch; + recipe?: GrowingRecipe; + showDetails?: boolean; +} + +export default function BatchProgress({ batch, recipe, showDetails = true }: BatchProgressProps) { + const progressPercent = recipe + ? Math.min(100, (batch.currentDay / recipe.expectedDays) * 100) + : 0; + + const statusColors: Record = { + germinating: 'bg-yellow-500', + growing: 'bg-green-500', + ready: 'bg-blue-500', + harvesting: 'bg-purple-500', + completed: 'bg-gray-500', + failed: 'bg-red-500', + }; + + const healthColor = + batch.healthScore >= 80 + ? 'text-green-600' + : batch.healthScore >= 60 + ? 'text-yellow-600' + : 'text-red-600'; + + return ( +
+
+
+

+ {batch.cropType} + {batch.variety && - {batch.variety}} +

+

Batch: {batch.id.slice(0, 12)}...

+
+
+ + {batch.status} + +

+ Health: {batch.healthScore}% +

+
+
+ +
+
+ Progress + + Day {batch.currentDay} {recipe && `/ ${recipe.expectedDays}`} + +
+
+
+
+
+ + {showDetails && ( +
+
+

Current Stage

+

{batch.currentStage}

+
+
+

Plants

+

{batch.plantCount.toLocaleString()}

+
+
+

Expected Yield

+

{batch.expectedYieldKg.toFixed(1)} kg

+
+
+

Harvest Date

+

+ {new Date(batch.expectedHarvestDate).toLocaleDateString()} +

+
+
+ )} + + {batch.issues.length > 0 && ( +
+

+ {batch.issues.filter(i => !i.resolvedAt).length} Active Issue(s) +

+ {batch.issues + .filter(i => !i.resolvedAt) + .slice(0, 2) + .map(issue => ( +

+ - {issue.type}: {issue.description} +

+ ))} +
+ )} +
+ ); +} diff --git a/components/vertical-farm/EnvironmentGauge.tsx b/components/vertical-farm/EnvironmentGauge.tsx new file mode 100644 index 0000000..82a1379 --- /dev/null +++ b/components/vertical-farm/EnvironmentGauge.tsx @@ -0,0 +1,89 @@ +interface EnvironmentGaugeProps { + label: string; + value: number; + unit: string; + target: number; + min: number; + max: number; + compact?: boolean; +} + +export default function EnvironmentGauge({ + label, + value, + unit, + target, + min, + max, + compact = false, +}: EnvironmentGaugeProps) { + const isLow = value < min; + const isHigh = value > max; + const isOptimal = value >= min && value <= max; + + const statusColor = isOptimal + ? 'text-green-600' + : isLow || isHigh + ? 'text-red-600' + : 'text-yellow-600'; + + const bgColor = isOptimal + ? 'bg-green-100' + : isLow || isHigh + ? 'bg-red-100' + : 'bg-yellow-100'; + + if (compact) { + return ( +
+

{label}

+

+ {typeof value === 'number' ? value.toFixed(0) : value} + {unit && {unit}} +

+
+ ); + } + + const deviation = value - target; + const deviationPercent = ((value - min) / (max - min)) * 100; + + return ( +
+
+ {label} + + {value.toFixed(1)}{unit} + +
+ +
+
+
+
+ +
+ {min}{unit} + Target: {target}{unit} + {max}{unit} +
+ + {deviation !== 0 && ( +

+ {deviation > 0 ? '+' : ''}{deviation.toFixed(1)}{unit} from target +

+ )} +
+ ); +} diff --git a/components/vertical-farm/FarmCard.tsx b/components/vertical-farm/FarmCard.tsx new file mode 100644 index 0000000..93a3916 --- /dev/null +++ b/components/vertical-farm/FarmCard.tsx @@ -0,0 +1,83 @@ +import Link from 'next/link'; +import { VerticalFarm } from '../../lib/vertical-farming/types'; + +interface FarmCardProps { + farm: VerticalFarm; +} + +export default function FarmCard({ farm }: FarmCardProps) { + const statusColors: Record = { + offline: 'bg-gray-100 text-gray-800', + starting: 'bg-yellow-100 text-yellow-800', + operational: 'bg-green-100 text-green-800', + maintenance: 'bg-orange-100 text-orange-800', + emergency: 'bg-red-100 text-red-800', + }; + + const activeZones = farm.zones.filter(z => z.status !== 'empty' && z.status !== 'cleaning').length; + + return ( + + +
+
+

{farm.name}

+

+ {farm.location.city}, {farm.location.country} +

+
+ + {farm.status.replace('_', ' ')} + +
+ +
+
+

Active Zones

+

+ {activeZones}/{farm.zones.length} +

+
+
+

Active Plants

+

+ {farm.specs.currentActivePlants.toLocaleString()} +

+
+
+ +
+
+ Capacity + {farm.currentCapacityUtilization}% +
+
+
+
+
+ +
+
+

Area

+

{farm.specs.growingAreaSqm}m²

+
+
+

Levels

+

{farm.specs.numberOfLevels}

+
+
+

Efficiency

+

{farm.energyEfficiencyScore}%

+
+
+
+ + ); +} diff --git a/components/vertical-farm/GrowthStageIndicator.tsx b/components/vertical-farm/GrowthStageIndicator.tsx new file mode 100644 index 0000000..18fb0b0 --- /dev/null +++ b/components/vertical-farm/GrowthStageIndicator.tsx @@ -0,0 +1,111 @@ +import { GrowthStage } from '../../lib/vertical-farming/types'; + +interface GrowthStageIndicatorProps { + stages: GrowthStage[]; + currentStage: string; + currentDay: number; +} + +export default function GrowthStageIndicator({ stages, currentStage, currentDay }: GrowthStageIndicatorProps) { + const currentStageIndex = stages.findIndex(s => s.name === currentStage); + + return ( +
+
+ {stages.map((stage, idx) => { + const isCompleted = idx < currentStageIndex; + const isCurrent = idx === currentStageIndex; + const isFuture = idx > currentStageIndex; + + return ( +
+ {/* Connector line */} + {idx > 0 && ( +
+ )} + + {/* Stage dot */} +
+ {isCompleted ? '\\u2713' : idx + 1} +
+ + {/* Stage name */} +

+ {stage.name} +

+ + {/* Days range */} +

+ Day {stage.daysStart}-{stage.daysEnd} +

+
+ ); + })} +
+ + {/* Current stage details */} + {currentStageIndex >= 0 && ( +
+

+ Current: {stages[currentStageIndex].name} (Day {currentDay}) +

+
+
+

Temperature

+

+ {stages[currentStageIndex].temperature.day}°C / {stages[currentStageIndex].temperature.night}°C +

+
+
+

Humidity

+

+ {stages[currentStageIndex].humidity.day}% / {stages[currentStageIndex].humidity.night}% +

+
+
+

Light

+

+ {stages[currentStageIndex].lightHours}h @ {stages[currentStageIndex].lightPpfd} PPFD +

+
+
+

Nutrients

+

+ EC {stages[currentStageIndex].targetEc} / pH {stages[currentStageIndex].targetPh} +

+
+
+ + {stages[currentStageIndex].actions.length > 0 && ( +
+

Scheduled Actions:

+ {stages[currentStageIndex].actions.map((action, idx) => ( +

+ Day {action.day}: {action.action} - {action.description} + {action.automated && (automated)} +

+ ))} +
+ )} +
+ )} +
+ ); +} diff --git a/components/vertical-farm/RecipeSelector.tsx b/components/vertical-farm/RecipeSelector.tsx new file mode 100644 index 0000000..cbc1fb5 --- /dev/null +++ b/components/vertical-farm/RecipeSelector.tsx @@ -0,0 +1,117 @@ +import { useState, useEffect } from 'react'; +import { GrowingRecipe } from '../../lib/vertical-farming/types'; + +interface RecipeSelectorProps { + onSelect: (recipe: GrowingRecipe) => void; + selectedRecipeId?: string; +} + +export default function RecipeSelector({ onSelect, selectedRecipeId }: RecipeSelectorProps) { + const [recipes, setRecipes] = useState([]); + const [loading, setLoading] = useState(true); + const [filter, setFilter] = useState(''); + + useEffect(() => { + fetchRecipes(); + }, []); + + const fetchRecipes = async () => { + try { + const response = await fetch('/api/vertical-farm/recipes'); + const data = await response.json(); + if (data.success) { + setRecipes(data.recipes); + } + } catch (error) { + console.error('Error fetching recipes:', error); + } finally { + setLoading(false); + } + }; + + const filteredRecipes = recipes.filter( + r => + r.name.toLowerCase().includes(filter.toLowerCase()) || + r.cropType.toLowerCase().includes(filter.toLowerCase()) + ); + + if (loading) { + return ( +
+
+

Loading recipes...

+
+ ); + } + + return ( +
+ setFilter(e.target.value)} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" + /> + +
+ {filteredRecipes.map(recipe => ( +
onSelect(recipe)} + className={`p-4 rounded-lg border-2 cursor-pointer transition ${ + selectedRecipeId === recipe.id + ? 'border-green-500 bg-green-50' + : 'border-gray-200 hover:border-green-300' + }`} + > +
+

{recipe.name}

+ {recipe.rating && ( + + {'*'.repeat(Math.round(recipe.rating))} {recipe.rating.toFixed(1)} + + )} +
+ +
+
+ Crop: {recipe.cropType} +
+
+ Days: {recipe.expectedDays} +
+
+ Yield: {recipe.expectedYieldGrams}g +
+
+ Uses: {recipe.timesUsed} +
+
+ +
+ {recipe.stages.map(stage => ( + + {stage.name} + + ))} +
+ +
+ Source: {recipe.source} | Zone: {recipe.requirements.zoneType} +
+
+ ))} +
+ + {filteredRecipes.length === 0 && ( +

+ No recipes found matching "{filter}" +

+ )} +
+ ); +} diff --git a/components/vertical-farm/ResourceUsageChart.tsx b/components/vertical-farm/ResourceUsageChart.tsx new file mode 100644 index 0000000..a7367f3 --- /dev/null +++ b/components/vertical-farm/ResourceUsageChart.tsx @@ -0,0 +1,124 @@ +import { ResourceUsage } from '../../lib/vertical-farming/types'; + +interface ResourceUsageChartProps { + usage: ResourceUsage; + showBenchmarks?: boolean; +} + +export default function ResourceUsageChart({ usage, showBenchmarks = true }: ResourceUsageChartProps) { + const formatCurrency = (value: number) => + new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value); + + const efficiencyColor = usage.efficiencyVsBenchmark >= 100 + ? 'text-green-600' + : usage.efficiencyVsBenchmark >= 80 + ? 'text-yellow-600' + : 'text-red-600'; + + return ( +
+ {/* Summary Stats */} +
+
+

Electricity

+

{usage.electricityKwh.toLocaleString()} kWh

+

{formatCurrency(usage.electricityCostUsd)}

+
+
+

Water

+

{usage.waterUsageL.toLocaleString()} L

+

{formatCurrency(usage.waterCostUsd)}

+
+
+

CO2

+

{usage.co2UsedKg.toFixed(1)} kg

+

{formatCurrency(usage.co2CostUsd)}

+
+
+

Nutrients

+

{usage.nutrientsUsedL.toFixed(1)} L

+

{formatCurrency(usage.nutrientCostUsd)}

+
+
+ + {/* Efficiency Metrics */} +
+

Efficiency Per kg Produce

+
+
+

{usage.kwhPerKgProduce.toFixed(1)}

+

kWh/kg

+ {showBenchmarks && ( +

+ Industry: {usage.industryBenchmarkKwhPerKg} kWh/kg +

+ )} +
+
+

{usage.litersPerKgProduce.toFixed(1)}

+

L/kg

+ {showBenchmarks && ( +

+ Industry: {usage.industryBenchmarkLitersPerKg} L/kg +

+ )} +
+
+

{formatCurrency(usage.costPerKgProduce)}

+

$/kg

+
+
+
+ + {/* Renewable & Recycled */} +
+
+
+ Renewable Energy + {usage.renewablePercent}% +
+
+
+
+
+
+
+ Water Recycled + {usage.waterRecycledPercent}% +
+
+
+
+
+
+ + {/* Overall Efficiency */} + {showBenchmarks && ( +
+

Overall Efficiency vs Industry Benchmark

+

+ {usage.efficiencyVsBenchmark.toFixed(0)}% +

+

+ {usage.efficiencyVsBenchmark >= 100 + ? 'Outperforming industry average' + : usage.efficiencyVsBenchmark >= 80 + ? 'Near industry average' + : 'Below industry average - optimization needed'} +

+
+ )} + + {/* Period Info */} +

+ Period: {new Date(usage.periodStart).toLocaleDateString()} - {new Date(usage.periodEnd).toLocaleDateString()} +

+
+ ); +} diff --git a/components/vertical-farm/ZoneDetailCard.tsx b/components/vertical-farm/ZoneDetailCard.tsx new file mode 100644 index 0000000..55ab692 --- /dev/null +++ b/components/vertical-farm/ZoneDetailCard.tsx @@ -0,0 +1,112 @@ +import { GrowingZone } from '../../lib/vertical-farming/types'; +import EnvironmentGauge from './EnvironmentGauge'; + +interface ZoneDetailCardProps { + zone: GrowingZone; + onClick?: () => void; + isSelected?: boolean; + expanded?: boolean; +} + +export default function ZoneDetailCard({ zone, onClick, isSelected, expanded }: ZoneDetailCardProps) { + const statusColors: Record = { + empty: 'bg-gray-100 text-gray-600 border-gray-300', + preparing: 'bg-yellow-100 text-yellow-800 border-yellow-400', + planted: 'bg-blue-100 text-blue-800 border-blue-400', + growing: 'bg-green-100 text-green-800 border-green-400', + harvesting: 'bg-purple-100 text-purple-800 border-purple-400', + cleaning: 'bg-orange-100 text-orange-800 border-orange-400', + }; + + const daysUntilHarvest = zone.expectedHarvestDate + ? Math.max(0, Math.ceil((new Date(zone.expectedHarvestDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24))) + : null; + + return ( +
+
+
+

{zone.name}

+

{zone.growingMethod.replace('_', ' ')}

+
+ {zone.status} +
+ + {zone.currentCrop && ( +
+

{zone.currentCrop}

+ {daysUntilHarvest !== null && ( +

+ Harvest in {daysUntilHarvest} days +

+ )} +
+ )} + +
+
+ Plants: + {zone.plantIds.length}/{zone.plantPositions} +
+
+ Area: + {zone.areaSqm}m² +
+
+ + {zone.currentEnvironment && ( +
+
+ + + + +
+ + {zone.currentEnvironment.alerts.length > 0 && ( +
+ {zone.currentEnvironment.alerts.length} active alert(s) +
+ )} +
+ )} +
+ ); +} diff --git a/components/vertical-farm/ZoneGrid.tsx b/components/vertical-farm/ZoneGrid.tsx new file mode 100644 index 0000000..701b0a1 --- /dev/null +++ b/components/vertical-farm/ZoneGrid.tsx @@ -0,0 +1,44 @@ +import { GrowingZone } from '../../lib/vertical-farming/types'; +import ZoneDetailCard from './ZoneDetailCard'; + +interface ZoneGridProps { + zones: GrowingZone[]; + onZoneSelect?: (zone: GrowingZone) => void; + selectedZoneId?: string; +} + +export default function ZoneGrid({ zones, onZoneSelect, selectedZoneId }: ZoneGridProps) { + const zonesByLevel = zones.reduce((acc, zone) => { + if (!acc[zone.level]) { + acc[zone.level] = []; + } + acc[zone.level].push(zone); + return acc; + }, {} as Record); + + const levels = Object.keys(zonesByLevel) + .map(Number) + .sort((a, b) => b - a); + + return ( +
+ {levels.map(level => ( +
+

+ Level {level} +

+
+ {zonesByLevel[level].map(zone => ( + onZoneSelect?.(zone)} + isSelected={selectedZoneId === zone.id} + /> + ))} +
+
+ ))} +
+ ); +} diff --git a/pages/vertical-farm/[farmId]/analytics.tsx b/pages/vertical-farm/[farmId]/analytics.tsx new file mode 100644 index 0000000..3e85b73 --- /dev/null +++ b/pages/vertical-farm/[farmId]/analytics.tsx @@ -0,0 +1,289 @@ +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(null); + const [analytics, setAnalytics] = useState(null); + const [resourceUsage, setResourceUsage] = useState(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 ( +
+
+
+ ); + } + + return ( +
+ + Analytics - {farm?.name || 'Vertical Farm'} + + +
+
+
+
+ + ← Back + +

Farm Analytics

+
+
+ {[7, 30, 90].map(p => ( + + ))} +
+
+
+
+ +
+ {analytics ? ( +
+ {/* Production Overview */} +
+
+

Total Yield

+

{analytics.totalYieldKg} kg

+

{period} day period

+
+
+

Yield/m²/year

+

{analytics.yieldPerSqmPerYear} kg

+
+
+

Crop Cycles

+

{analytics.cropCyclesCompleted}

+

Avg {analytics.averageCyclesDays.toFixed(0)} days

+
+
+

Success Rate

+

{analytics.cropSuccessRate}%

+
+
+ + {/* Quality Metrics */} +
+

Quality Metrics

+
+
+
+ + + + + + {analytics.averageQualityScore.toFixed(0)} + +
+

Quality Score

+
+
+

{analytics.gradeAPercent}%

+

Grade A

+
+
+

{analytics.wastagePercent}%

+

Wastage

+
+
+

{analytics.spaceUtilization}%

+

Space Utilization

+
+
+
+ + {/* Financial Overview */} +
+

Financial Performance

+
+
+

Revenue

+

+ ${analytics.revenueUsd.toLocaleString()} +

+
+
+

Costs

+

+ ${analytics.costUsd.toLocaleString()} +

+
+
+

Profit Margin

+

{analytics.profitMarginPercent}%

+
+
+

Revenue/m²/year

+

+ ${analytics.revenuePerSqm.toLocaleString()} +

+
+
+
+ + {/* Environmental Impact */} +
+

Environmental Impact

+
+
+

+ {analytics.carbonFootprintKgPerKg.toFixed(2)} +

+

kg CO2 / kg produce

+

90% less than field farming

+
+
+

+ {analytics.waterUseLPerKg.toFixed(1)} +

+

L water / kg produce

+

95% less than field farming

+
+
+

+ {analytics.energyUseKwhPerKg.toFixed(1)} +

+

kWh / kg produce

+
+
+
+ + {/* Top Crops */} +
+
+

Top Crops by Yield

+
+ {analytics.topCropsByYield.map((crop, idx) => ( +
+ + {idx + 1} + {crop.crop} + + {crop.yieldKg} kg +
+ ))} +
+
+ +
+

Top Crops by Revenue

+
+ {analytics.topCropsByRevenue.map((crop, idx) => ( +
+ + {idx + 1} + {crop.crop} + + ${crop.revenueUsd} +
+ ))} +
+
+ +
+

Top Crops by Efficiency

+
+ {analytics.topCropsByEfficiency.map((crop, idx) => ( +
+ + {idx + 1} + {crop.crop} + + {crop.efficiencyScore}% +
+ ))} +
+
+
+ + {/* Resource Usage */} + {resourceUsage && ( +
+

Resource Usage

+ +
+ )} + + {/* Generated Timestamp */} +

+ Analytics generated: {new Date(analytics.generatedAt).toLocaleString()} +

+
+ ) : ( +
+

No analytics data available yet

+

+ Complete some crop batches to see analytics data +

+
+ )} +
+
+ ); +} diff --git a/pages/vertical-farm/[farmId]/batches.tsx b/pages/vertical-farm/[farmId]/batches.tsx new file mode 100644 index 0000000..ff1d057 --- /dev/null +++ b/pages/vertical-farm/[farmId]/batches.tsx @@ -0,0 +1,327 @@ +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(null); + const [batches, setBatches] = useState([]); + const [recipes, setRecipes] = useState>(new Map()); + const [selectedBatch, setSelectedBatch] = useState(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(); + 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 ( +
+
+
+ ); + } + + return ( +
+ + Crop Batches - {farm?.name || 'Vertical Farm'} + + +
+
+
+
+ + ← Back + +

Crop Batches

+
+ + + Start New Batch + + +
+
+
+ +
+ {/* Filter Tabs */} +
+ {(['active', 'completed', 'all'] as const).map(f => ( + + ))} +
+ +
+ {/* Batch List */} +
+ {filteredBatches.length === 0 ? ( +
+

No {filter} batches found

+ + Start a new batch → + +
+ ) : ( + filteredBatches.map(batch => ( +
setSelectedBatch(batch)} + className={`cursor-pointer transition ${ + selectedBatch?.id === batch.id ? 'ring-2 ring-green-500' : '' + }`} + > + +
+ )) + )} +
+ + {/* Batch Detail */} +
+ {selectedBatch ? ( + <> +
+

+ {selectedBatch.cropType} + {selectedBatch.variety && ` - ${selectedBatch.variety}`} +

+ +
+
+
+

Batch ID

+

{selectedBatch.id}

+
+
+

Status

+

{selectedBatch.status}

+
+
+

Planted

+

+ {new Date(selectedBatch.plantingDate).toLocaleDateString()} +

+
+
+

Expected Harvest

+

+ {new Date(selectedBatch.expectedHarvestDate).toLocaleDateString()} +

+
+
+

Plant Count

+

{selectedBatch.plantCount.toLocaleString()}

+
+
+

Health Score

+

= 80 + ? 'text-green-600' + : selectedBatch.healthScore >= 60 + ? 'text-yellow-600' + : 'text-red-600' + }`} + > + {selectedBatch.healthScore}% +

+
+
+ + {selectedBatch.actualHarvestDate && ( +
+

Harvest Results

+
+
+

Actual Yield

+

+ {selectedBatch.actualYieldKg?.toFixed(1)} kg +

+
+
+

Quality Grade

+

{selectedBatch.qualityGrade}

+
+
+
+ )} +
+
+ + {/* Growth Stage Progress */} + {recipes.get(selectedBatch.recipeId) && ( +
+

Growth Progress

+ +
+ )} + + {/* Issues */} + {selectedBatch.issues.length > 0 && ( +
+

+ Issues ({selectedBatch.issues.filter(i => !i.resolvedAt).length} active) +

+
+ {selectedBatch.issues.map(issue => ( +
+
+ {issue.type} + + {issue.severity} + +
+

{issue.description}

+ {issue.resolvedAt && ( +

+ Resolved: {issue.resolution} +

+ )} +
+ ))} +
+
+ )} + + {/* Actions */} +
+

Actions

+
+ {selectedBatch.status === 'ready' && ( + + )} + + +
+
+ + ) : ( +
+

Select a batch to view details

+
+ )} +
+
+
+
+ ); +} diff --git a/pages/vertical-farm/[farmId]/index.tsx b/pages/vertical-farm/[farmId]/index.tsx new file mode 100644 index 0000000..6a28519 --- /dev/null +++ b/pages/vertical-farm/[farmId]/index.tsx @@ -0,0 +1,264 @@ +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(null); + const [batches, setBatches] = useState([]); + 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 ( +
+
+
+

Loading farm details...

+
+
+ ); + } + + if (!farm) { + return ( +
+
+

Farm Not Found

+ + Back to Dashboard + +
+
+ ); + } + + 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 ( +
+ + {farm.name} - LocalGreenChain + + +
+
+
+
+ + ← Back + +

{farm.name}

+ + {farm.status} + +
+ +
+
+
+ +
+ {/* Quick Stats */} +
+
+

Active Plants

+

{farm.specs.currentActivePlants.toLocaleString()}

+
+
+

Growing Area

+

{farm.specs.growingAreaSqm}m²

+
+
+

Active Batches

+

{activeBatches.length}

+
+
+

Capacity

+

{farm.currentCapacityUtilization}%

+
+
+

Efficiency

+

{farm.energyEfficiencyScore}%

+
+
+ + {/* Alerts */} + {allAlerts.length > 0 && ( +
+ +
+ )} + +
+ {/* Main Content */} +
+ {/* Zone Overview */} +
+
+

Zone Overview

+ + View All → + +
+ +
+ + {/* Upcoming Harvests */} + {upcomingHarvests.length > 0 && ( +
+

Upcoming Harvests

+
+ {upcomingHarvests.map(batch => { + const daysUntil = Math.ceil( + (new Date(batch.expectedHarvestDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24) + ); + return ( +
+
+

{batch.cropType}

+

{batch.plantCount} plants

+
+
+

+ {daysUntil === 0 ? 'Today' : `${daysUntil} day${daysUntil > 1 ? 's' : ''}`} +

+

{batch.expectedYieldKg.toFixed(1)} kg expected

+
+
+ ); + })} +
+
+ )} +
+ + {/* Sidebar */} +
+ {/* Farm Info */} +
+

Farm Details

+
+
+

Location

+

{farm.location.city}, {farm.location.country}

+

{farm.location.address}

+
+
+

Building

+

{farm.specs.buildingType.replace('_', ' ')}

+
+
+

Levels

+

{farm.specs.numberOfLevels}

+
+
+

Automation

+

{farm.automationLevel.replace('_', ' ')}

+
+
+

Operational Since

+

{new Date(farm.operationalSince).toLocaleDateString()}

+
+
+
+ + {/* Quick Actions */} +
+

Quick Actions

+
+ + + Start New Batch + + + + +
+
+ + {/* Certifications */} + {farm.specs.certifications.length > 0 && ( +
+

Certifications

+
+ {farm.specs.certifications.map(cert => ( + + {cert.toUpperCase()} + + ))} +
+
+ )} +
+
+
+
+ ); +} diff --git a/pages/vertical-farm/[farmId]/zones.tsx b/pages/vertical-farm/[farmId]/zones.tsx new file mode 100644 index 0000000..ba834eb --- /dev/null +++ b/pages/vertical-farm/[farmId]/zones.tsx @@ -0,0 +1,315 @@ +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(null); + const [selectedZone, setSelectedZone] = useState(null); + const [loading, setLoading] = useState(true); + const [showStartBatch, setShowStartBatch] = useState(false); + const [selectedRecipe, setSelectedRecipe] = useState(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 ( +
+
+
+ ); + } + + if (!farm) { + return ( +
+

Farm not found

+
+ ); + } + + return ( +
+ + Zone Management - {farm.name} + + +
+
+
+
+ + ← Back + +

Zone Management

+
+

{farm.name}

+
+
+
+ +
+
+ {/* Zone Grid */} +
+
+

All Zones

+ +
+
+ + {/* Zone Detail / Actions */} +
+ {selectedZone ? ( + <> +
+

{selectedZone.name}

+ +
+
+
+

Status

+

{selectedZone.status}

+
+
+

Method

+

{selectedZone.growingMethod}

+
+
+

Level

+

{selectedZone.level}

+
+
+

Area

+

{selectedZone.areaSqm}m²

+
+
+

Positions

+

{selectedZone.plantPositions}

+
+
+

Active Plants

+

{selectedZone.plantIds.length}

+
+
+ + {selectedZone.currentCrop && ( +
+

Current Crop

+

{selectedZone.currentCrop}

+ {selectedZone.expectedHarvestDate && ( +

+ Harvest: {new Date(selectedZone.expectedHarvestDate).toLocaleDateString()} +

+ )} +
+ )} +
+
+ + {/* Environment Readings */} + {selectedZone.currentEnvironment && ( +
+

Current Environment

+
+ + + +
+
+ )} + + {/* Actions */} +
+

Actions

+
+ {selectedZone.status === 'empty' && ( + + )} + {(selectedZone.status === 'growing' || selectedZone.status === 'ready') && ( + + )} + + +
+
+ + ) : ( +
+

Select a zone to view details and actions

+
+ )} +
+
+ + {/* Start Batch Modal */} + {showStartBatch && selectedZone && ( +
+
+
+
+

Start New Batch

+ +
+ +

Zone: {selectedZone.name}

+ +
+
+ + +
+ +
+
+ + 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" + /> +
+
+ + 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" + /> +
+
+ +
+ + +
+
+
+
+
+ )} +
+
+ ); +} diff --git a/pages/vertical-farm/index.tsx b/pages/vertical-farm/index.tsx new file mode 100644 index 0000000..68be92b --- /dev/null +++ b/pages/vertical-farm/index.tsx @@ -0,0 +1,161 @@ +import { useState, useEffect } from 'react'; +import Link from 'next/link'; +import Head from 'next/head'; +import FarmCard from '../../components/vertical-farm/FarmCard'; +import { VerticalFarm } from '../../lib/vertical-farming/types'; + +export default function VerticalFarmDashboard() { + const [farms, setFarms] = useState([]); + const [loading, setLoading] = useState(true); + const [stats, setStats] = useState({ + totalFarms: 0, + totalPlants: 0, + upcomingHarvests: 0, + activeZones: 0, + }); + + useEffect(() => { + fetchFarms(); + }, []); + + const fetchFarms = async () => { + try { + const response = await fetch('/api/vertical-farm/list'); + const data = await response.json(); + if (data.success) { + setFarms(data.farms); + calculateStats(data.farms); + } + } catch (error) { + console.error('Error fetching farms:', error); + } finally { + setLoading(false); + } + }; + + const calculateStats = (farmList: VerticalFarm[]) => { + const totalPlants = farmList.reduce((sum, f) => sum + f.specs.currentActivePlants, 0); + const activeZones = farmList.reduce( + (sum, f) => sum + f.zones.filter(z => z.status === 'growing' || z.status === 'planted').length, + 0 + ); + const upcomingHarvests = farmList.reduce( + (sum, f) => + sum + + f.zones.filter(z => { + if (!z.expectedHarvestDate) return false; + const daysUntil = (new Date(z.expectedHarvestDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24); + return daysUntil <= 7 && daysUntil >= 0; + }).length, + 0 + ); + + setStats({ + totalFarms: farmList.length, + totalPlants, + upcomingHarvests, + activeZones, + }); + }; + + return ( +
+ + Vertical Farms - LocalGreenChain + + + {/* Header */} +
+ +
+ +
+
+

Vertical Farm Dashboard

+
+ + {/* Stats Overview */} +
+
+

Total Farms

+

{stats.totalFarms}

+
+
+

Active Plants

+

{stats.totalPlants.toLocaleString()}

+
+
+

Active Zones

+

{stats.activeZones}

+
+
+

Harvests This Week

+

{stats.upcomingHarvests}

+
+
+ + {/* Farms Grid */} + {loading ? ( +
+
+

Loading farms...

+
+ ) : farms.length > 0 ? ( +
+ {farms.map(farm => ( + + ))} +
+ ) : ( +
+
🌱
+

No Vertical Farms Yet

+

+ Start your controlled environment agriculture journey by registering your first vertical farm. +

+ + + Register Your First Farm + + +
+ )} + + {/* Quick Actions */} + {farms.length > 0 && ( +
+

Quick Actions

+
+ + + + Add New Farm + + + + +
+
+ )} +
+
+ ); +} diff --git a/pages/vertical-farm/register.tsx b/pages/vertical-farm/register.tsx new file mode 100644 index 0000000..06eaa99 --- /dev/null +++ b/pages/vertical-farm/register.tsx @@ -0,0 +1,492 @@ +import { useState } from 'react'; +import { useRouter } from 'next/router'; +import Link from 'next/link'; +import Head from 'next/head'; +import { VerticalFarm, FacilitySpecs, GrowingZone } from '../../lib/vertical-farming/types'; + +export default function RegisterVerticalFarm() { + const router = useRouter(); + const [step, setStep] = useState(1); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const [formData, setFormData] = useState({ + name: '', + address: '', + city: '', + country: '', + latitude: '', + longitude: '', + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + totalAreaSqm: '', + growingAreaSqm: '', + numberOfLevels: '1', + ceilingHeightM: '3', + totalGrowingPositions: '', + powerCapacityKw: '', + waterStorageL: '', + buildingType: 'warehouse' as const, + automationLevel: 'semi_automated' as const, + zones: [] as Partial[], + }); + + const updateField = (field: string, value: any) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + + const addZone = () => { + const newZone: Partial = { + id: `zone-${Date.now()}`, + name: `Zone ${formData.zones.length + 1}`, + level: 1, + areaSqm: 10, + lengthM: 5, + widthM: 2, + growingMethod: 'NFT', + plantPositions: 100, + status: 'empty', + plantIds: [], + }; + setFormData(prev => ({ ...prev, zones: [...prev.zones, newZone] })); + }; + + const updateZone = (index: number, updates: Partial) => { + setFormData(prev => ({ + ...prev, + zones: prev.zones.map((z, i) => (i === index ? { ...z, ...updates } : z)), + })); + }; + + const removeZone = (index: number) => { + setFormData(prev => ({ + ...prev, + zones: prev.zones.filter((_, i) => i !== index), + })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(''); + + try { + const farmData = { + name: formData.name, + location: { + latitude: parseFloat(formData.latitude), + longitude: parseFloat(formData.longitude), + address: formData.address, + city: formData.city, + country: formData.country, + timezone: formData.timezone, + }, + specs: { + totalAreaSqm: parseFloat(formData.totalAreaSqm), + growingAreaSqm: parseFloat(formData.growingAreaSqm), + numberOfLevels: parseInt(formData.numberOfLevels), + ceilingHeightM: parseFloat(formData.ceilingHeightM), + totalGrowingPositions: parseInt(formData.totalGrowingPositions), + currentActivePlants: 0, + powerCapacityKw: parseFloat(formData.powerCapacityKw) || 0, + waterStorageL: parseFloat(formData.waterStorageL) || 0, + backupPowerHours: 0, + certifications: [], + buildingType: formData.buildingType, + insulation: 'standard', + }, + zones: formData.zones, + automationLevel: formData.automationLevel, + }; + + const response = await fetch('/api/vertical-farm/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(farmData), + }); + + const data = await response.json(); + + if (data.success) { + router.push(`/vertical-farm/${data.farm.id}`); + } else { + setError(data.error || 'Failed to register farm'); + } + } catch (err) { + setError('Network error. Please try again.'); + } finally { + setLoading(false); + } + }; + + return ( +
+ + Register Vertical Farm - LocalGreenChain + + +
+ +
+ +
+

Register Vertical Farm

+ + {/* Progress Steps */} +
+ {[1, 2, 3].map(s => ( +
+
= s ? 'bg-green-600 text-white' : 'bg-gray-200 text-gray-600' + }`} + > + {s} +
+ {s < 3 && ( +
s ? 'bg-green-600' : 'bg-gray-200'}`} /> + )} +
+ ))} +
+
+ Basic Info + Facility Specs + Zones +
+ + {error && ( +
+ {error} +
+ )} + +
+ {/* Step 1: Basic Info */} + {step === 1 && ( +
+

Basic Information

+ +
+ + updateField('name', e.target.value)} + placeholder="My Vertical Farm" + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" + /> +
+ +
+ + updateField('address', e.target.value)} + placeholder="123 Farm Street" + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" + /> +
+ +
+
+ + updateField('city', e.target.value)} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" + /> +
+
+ + updateField('country', e.target.value)} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" + /> +
+
+ +
+
+ + updateField('latitude', e.target.value)} + placeholder="40.7128" + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" + /> +
+
+ + updateField('longitude', e.target.value)} + placeholder="-74.0060" + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" + /> +
+
+
+ )} + + {/* Step 2: Facility Specs */} + {step === 2 && ( +
+

Facility Specifications

+ +
+
+ + updateField('totalAreaSqm', e.target.value)} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" + /> +
+
+ + updateField('growingAreaSqm', e.target.value)} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" + /> +
+
+ +
+
+ + updateField('numberOfLevels', e.target.value)} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" + /> +
+
+ + updateField('ceilingHeightM', e.target.value)} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" + /> +
+
+ +
+ + updateField('totalGrowingPositions', e.target.value)} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" + /> +
+ +
+
+ + updateField('powerCapacityKw', e.target.value)} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" + /> +
+
+ + updateField('waterStorageL', e.target.value)} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" + /> +
+
+ +
+
+ + +
+
+ + +
+
+
+ )} + + {/* Step 3: Zones */} + {step === 3 && ( +
+
+

Growing Zones

+ +
+ + {formData.zones.length === 0 ? ( +
+

No zones added yet. Click "Add Zone" to create your first growing zone.

+
+ ) : ( +
+ {formData.zones.map((zone, index) => ( +
+
+ updateZone(index, { name: e.target.value })} + className="text-lg font-medium border-none focus:ring-0 p-0" + /> + +
+
+
+ + updateZone(index, { level: parseInt(e.target.value) })} + className="w-full px-2 py-1 border border-gray-300 rounded text-sm" + /> +
+
+ + updateZone(index, { areaSqm: parseFloat(e.target.value) })} + className="w-full px-2 py-1 border border-gray-300 rounded text-sm" + /> +
+
+ + updateZone(index, { plantPositions: parseInt(e.target.value) })} + className="w-full px-2 py-1 border border-gray-300 rounded text-sm" + /> +
+
+ + +
+
+
+ ))} +
+ )} +
+ )} + + {/* Navigation Buttons */} +
+ {step > 1 ? ( + + ) : ( +
+ )} + + {step < 3 ? ( + + ) : ( + + )} +
+ +
+
+ ); +}