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
492 lines
20 KiB
TypeScript
492 lines
20 KiB
TypeScript
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<GrowingZone>[],
|
|
});
|
|
|
|
const updateField = (field: string, value: any) => {
|
|
setFormData(prev => ({ ...prev, [field]: value }));
|
|
};
|
|
|
|
const addZone = () => {
|
|
const newZone: Partial<GrowingZone> = {
|
|
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<GrowingZone>) => {
|
|
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 (
|
|
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100">
|
|
<Head>
|
|
<title>Register Vertical Farm - 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">
|
|
<Link href="/">
|
|
<a className="text-2xl font-bold text-green-800">LocalGreenChain</a>
|
|
</Link>
|
|
<Link href="/vertical-farm">
|
|
<a className="text-gray-600 hover:text-gray-900">Back to Dashboard</a>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="max-w-3xl mx-auto px-4 py-12 sm:px-6 lg:px-8">
|
|
<h1 className="text-3xl font-bold text-gray-900 mb-8">Register Vertical Farm</h1>
|
|
|
|
{/* Progress Steps */}
|
|
<div className="flex items-center justify-center mb-8">
|
|
{[1, 2, 3].map(s => (
|
|
<div key={s} className="flex items-center">
|
|
<div
|
|
className={`w-10 h-10 rounded-full flex items-center justify-center font-semibold ${
|
|
step >= s ? 'bg-green-600 text-white' : 'bg-gray-200 text-gray-600'
|
|
}`}
|
|
>
|
|
{s}
|
|
</div>
|
|
{s < 3 && (
|
|
<div className={`w-20 h-1 ${step > s ? 'bg-green-600' : 'bg-gray-200'}`} />
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="flex justify-center gap-8 text-sm text-gray-600 mb-8">
|
|
<span className={step === 1 ? 'font-semibold text-green-600' : ''}>Basic Info</span>
|
|
<span className={step === 2 ? 'font-semibold text-green-600' : ''}>Facility Specs</span>
|
|
<span className={step === 3 ? 'font-semibold text-green-600' : ''}>Zones</span>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handleSubmit} className="bg-white rounded-lg shadow-lg p-6">
|
|
{/* Step 1: Basic Info */}
|
|
{step === 1 && (
|
|
<div className="space-y-4">
|
|
<h2 className="text-xl font-semibold text-gray-900 mb-4">Basic Information</h2>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Farm Name *</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={formData.name}
|
|
onChange={e => 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"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Address *</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={formData.address}
|
|
onChange={e => 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"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">City *</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={formData.city}
|
|
onChange={e => 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"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Country *</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={formData.country}
|
|
onChange={e => 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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Latitude</label>
|
|
<input
|
|
type="number"
|
|
step="any"
|
|
value={formData.latitude}
|
|
onChange={e => 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"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Longitude</label>
|
|
<input
|
|
type="number"
|
|
step="any"
|
|
value={formData.longitude}
|
|
onChange={e => 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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 2: Facility Specs */}
|
|
{step === 2 && (
|
|
<div className="space-y-4">
|
|
<h2 className="text-xl font-semibold text-gray-900 mb-4">Facility Specifications</h2>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Total Area (m²) *</label>
|
|
<input
|
|
type="number"
|
|
required
|
|
value={formData.totalAreaSqm}
|
|
onChange={e => 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"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Growing Area (m²) *</label>
|
|
<input
|
|
type="number"
|
|
required
|
|
value={formData.growingAreaSqm}
|
|
onChange={e => 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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Number of Levels *</label>
|
|
<input
|
|
type="number"
|
|
required
|
|
min="1"
|
|
value={formData.numberOfLevels}
|
|
onChange={e => 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"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Ceiling Height (m)</label>
|
|
<input
|
|
type="number"
|
|
step="0.1"
|
|
value={formData.ceilingHeightM}
|
|
onChange={e => 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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Total Growing Positions *</label>
|
|
<input
|
|
type="number"
|
|
required
|
|
value={formData.totalGrowingPositions}
|
|
onChange={e => 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"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Power Capacity (kW)</label>
|
|
<input
|
|
type="number"
|
|
value={formData.powerCapacityKw}
|
|
onChange={e => 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"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Water Storage (L)</label>
|
|
<input
|
|
type="number"
|
|
value={formData.waterStorageL}
|
|
onChange={e => 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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Building Type</label>
|
|
<select
|
|
value={formData.buildingType}
|
|
onChange={e => updateField('buildingType', e.target.value)}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
|
|
>
|
|
<option value="warehouse">Warehouse</option>
|
|
<option value="greenhouse">Greenhouse</option>
|
|
<option value="container">Container</option>
|
|
<option value="purpose_built">Purpose Built</option>
|
|
<option value="retrofit">Retrofit</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Automation Level</label>
|
|
<select
|
|
value={formData.automationLevel}
|
|
onChange={e => updateField('automationLevel', e.target.value)}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
|
|
>
|
|
<option value="manual">Manual</option>
|
|
<option value="semi_automated">Semi-Automated</option>
|
|
<option value="fully_automated">Fully Automated</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 3: Zones */}
|
|
{step === 3 && (
|
|
<div className="space-y-4">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h2 className="text-xl font-semibold text-gray-900">Growing Zones</h2>
|
|
<button
|
|
type="button"
|
|
onClick={addZone}
|
|
className="px-4 py-2 bg-green-100 text-green-700 rounded-lg hover:bg-green-200 transition"
|
|
>
|
|
+ Add Zone
|
|
</button>
|
|
</div>
|
|
|
|
{formData.zones.length === 0 ? (
|
|
<div className="text-center py-8 bg-gray-50 rounded-lg">
|
|
<p className="text-gray-600">No zones added yet. Click "Add Zone" to create your first growing zone.</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{formData.zones.map((zone, index) => (
|
|
<div key={zone.id} className="border border-gray-200 rounded-lg p-4">
|
|
<div className="flex justify-between items-start mb-3">
|
|
<input
|
|
type="text"
|
|
value={zone.name}
|
|
onChange={e => updateZone(index, { name: e.target.value })}
|
|
className="text-lg font-medium border-none focus:ring-0 p-0"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => removeZone(index)}
|
|
className="text-red-600 hover:text-red-800"
|
|
>
|
|
Remove
|
|
</button>
|
|
</div>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
<div>
|
|
<label className="block text-xs text-gray-600 mb-1">Level</label>
|
|
<input
|
|
type="number"
|
|
min="1"
|
|
value={zone.level}
|
|
onChange={e => updateZone(index, { level: parseInt(e.target.value) })}
|
|
className="w-full px-2 py-1 border border-gray-300 rounded text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs text-gray-600 mb-1">Area (m²)</label>
|
|
<input
|
|
type="number"
|
|
value={zone.areaSqm}
|
|
onChange={e => updateZone(index, { areaSqm: parseFloat(e.target.value) })}
|
|
className="w-full px-2 py-1 border border-gray-300 rounded text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs text-gray-600 mb-1">Positions</label>
|
|
<input
|
|
type="number"
|
|
value={zone.plantPositions}
|
|
onChange={e => updateZone(index, { plantPositions: parseInt(e.target.value) })}
|
|
className="w-full px-2 py-1 border border-gray-300 rounded text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs text-gray-600 mb-1">Method</label>
|
|
<select
|
|
value={zone.growingMethod}
|
|
onChange={e => updateZone(index, { growingMethod: e.target.value as any })}
|
|
className="w-full px-2 py-1 border border-gray-300 rounded text-sm"
|
|
>
|
|
<option value="NFT">NFT</option>
|
|
<option value="DWC">DWC</option>
|
|
<option value="ebb_flow">Ebb & Flow</option>
|
|
<option value="aeroponics">Aeroponics</option>
|
|
<option value="vertical_towers">Vertical Towers</option>
|
|
<option value="rack_system">Rack System</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Navigation Buttons */}
|
|
<div className="flex justify-between mt-8 pt-6 border-t border-gray-200">
|
|
{step > 1 ? (
|
|
<button
|
|
type="button"
|
|
onClick={() => setStep(step - 1)}
|
|
className="px-6 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition"
|
|
>
|
|
Previous
|
|
</button>
|
|
) : (
|
|
<div />
|
|
)}
|
|
|
|
{step < 3 ? (
|
|
<button
|
|
type="button"
|
|
onClick={() => setStep(step + 1)}
|
|
className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition"
|
|
>
|
|
Next
|
|
</button>
|
|
) : (
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition disabled:opacity-50"
|
|
>
|
|
{loading ? 'Registering...' : 'Register Farm'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</form>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|