localgreenchain/components/EnvironmentalForm.tsx
Claude 507df5912f
Deploy GrowerAdvisoryAgent (Agent 10) and fix type errors
- Add GrowerAdvisoryAgent test file
- Fix PlantChain constructor initialization order (plantIndex before genesis block)
- Fix blockchain.getChain() calls to use blockchain.chain property
- Add PropagationType export to blockchain types
- Fix SoilComposition.type property references (was soilType)
- Fix ClimateConditions.temperatureDay property references (was avgTemperature)
- Fix ClimateConditions.humidityAverage property references (was avgHumidity)
- Fix LightingConditions.naturalLight.hoursPerDay nested access
- Add 'critical' severity to QualityReport issues
- Add 'sqm' unit to PlantingRecommendation.quantityUnit
- Fix GrowerAdvisoryAgent growthMetrics property access
- Update TypeScript to v5 for react-hook-form compatibility
- Enable downlevelIteration in tsconfig for Map iteration
- Fix crypto Buffer type issues in anonymity.ts
- Fix zones.tsx status type comparison
- Fix next.config.js images.domains filter
- Rename [[...slug]].tsx to [...slug].tsx to resolve routing conflict
2025-11-23 00:44:58 +00:00

708 lines
25 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react';
import {
GrowingEnvironment,
SoilComposition,
ClimateConditions,
LightingConditions,
NutrientProfile,
WateringSchedule,
ContainerInfo,
EnvironmentLocation,
SurroundingEnvironment,
} from '../lib/environment/types';
interface EnvironmentalFormProps {
value: Partial<GrowingEnvironment>;
onChange: (env: Partial<GrowingEnvironment>) => void;
compact?: boolean;
}
export default function EnvironmentalForm({
value,
onChange,
compact = false,
}: EnvironmentalFormProps) {
const [activeSection, setActiveSection] = useState<string>('soil');
const updateSection = <K extends keyof GrowingEnvironment>(
section: K,
updates: Partial<GrowingEnvironment[K]>
) => {
onChange({
...value,
[section]: {
...(value[section] as object || {}),
...updates,
},
});
};
const sections = [
{ id: 'soil', name: '🌱 Soil', icon: '🌱' },
{ id: 'nutrients', name: '🧪 Nutrients', icon: '🧪' },
{ id: 'lighting', name: '☀️ Light', icon: '☀️' },
{ id: 'climate', name: '🌡️ Climate', icon: '🌡️' },
{ id: 'location', name: '📍 Location', icon: '📍' },
{ id: 'container', name: '🪴 Container', icon: '🪴' },
{ id: 'watering', name: '💧 Water', icon: '💧' },
{ id: 'surroundings', name: '🌿 Surroundings', icon: '🌿' },
];
return (
<div className="bg-white rounded-lg shadow-lg">
{/* Section Tabs */}
<div className="flex overflow-x-auto border-b border-gray-200 bg-gray-50">
{sections.map((section) => (
<button
key={section.id}
onClick={() => setActiveSection(section.id)}
className={`px-4 py-3 text-sm font-medium whitespace-nowrap transition ${
activeSection === section.id
? 'border-b-2 border-green-600 text-green-600 bg-white'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
}`}
>
<span className="mr-2">{section.icon}</span>
{section.name}
</button>
))}
</div>
{/* Section Content */}
<div className="p-6">
{activeSection === 'soil' && (
<SoilSection
value={value.soil}
onChange={(soil) => updateSection('soil', soil)}
/>
)}
{activeSection === 'nutrients' && (
<NutrientsSection
value={value.nutrients}
onChange={(nutrients) => updateSection('nutrients', nutrients)}
/>
)}
{activeSection === 'lighting' && (
<LightingSection
value={value.lighting}
onChange={(lighting) => updateSection('lighting', lighting)}
/>
)}
{activeSection === 'climate' && (
<ClimateSection
value={value.climate}
onChange={(climate) => updateSection('climate', climate)}
/>
)}
{activeSection === 'location' && (
<LocationSection
value={value.location}
onChange={(location) => updateSection('location', location)}
/>
)}
{activeSection === 'container' && (
<ContainerSection
value={value.container}
onChange={(container) => updateSection('container', container)}
/>
)}
{activeSection === 'watering' && (
<WateringSection
value={value.watering}
onChange={(watering) => updateSection('watering', watering)}
/>
)}
{activeSection === 'surroundings' && (
<SurroundingsSection
value={value.surroundings}
onChange={(surroundings) => updateSection('surroundings', surroundings)}
/>
)}
</div>
</div>
);
}
// Soil Section Component
function SoilSection({
value,
onChange,
}: {
value?: Partial<SoilComposition>;
onChange: (soil: Partial<SoilComposition>) => void;
}) {
const soil = value || {};
return (
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900">Soil Composition</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Soil Type *
</label>
<select
value={soil.type || 'loam'}
onChange={(e) => onChange({ ...soil, type: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="clay">Clay</option>
<option value="sand">Sand</option>
<option value="silt">Silt</option>
<option value="loam">Loam (balanced)</option>
<option value="peat">Peat</option>
<option value="chalk">Chalk</option>
<option value="custom">Custom Mix</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Soil pH * <span className="text-xs text-gray-500">(most plants: 6.0-7.0)</span>
</label>
<input
type="number"
step="0.1"
min="0"
max="14"
value={soil.pH || 6.5}
onChange={(e) => onChange({ ...soil, pH: parseFloat(e.target.value) })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
<div className="mt-1 text-xs text-gray-600">
{soil.pH && soil.pH < 5.5 && '⚠️ Acidic - may need lime'}
{soil.pH && soil.pH >= 5.5 && soil.pH <= 7.5 && '✓ Good range'}
{soil.pH && soil.pH > 7.5 && '⚠️ Alkaline - may need sulfur'}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Texture</label>
<select
value={soil.texture || 'medium'}
onChange={(e) => onChange({ ...soil, texture: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="heavy">Heavy (clay-rich)</option>
<option value="medium">Medium (balanced)</option>
<option value="light">Light (sandy)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Drainage</label>
<select
value={soil.drainage || 'good'}
onChange={(e) => onChange({ ...soil, drainage: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="poor">Poor (stays wet)</option>
<option value="moderate">Moderate</option>
<option value="good">Good</option>
<option value="excellent">Excellent (fast draining)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Organic Matter % <span className="text-xs text-gray-500">(ideal: 5-10%)</span>
</label>
<input
type="number"
min="0"
max="100"
value={soil.organicMatter || 5}
onChange={(e) => onChange({ ...soil, organicMatter: parseFloat(e.target.value) })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
</div>
<div className="pt-4 border-t border-gray-200">
<p className="text-sm text-gray-600 mb-2">
💡 <strong>Tip:</strong> Test soil pH with a meter or test kit for accuracy. Most vegetables prefer pH 6.0-7.0.
</p>
</div>
</div>
);
}
// Nutrients Section Component
function NutrientsSection({
value,
onChange,
}: {
value?: Partial<NutrientProfile>;
onChange: (nutrients: Partial<NutrientProfile>) => void;
}) {
const nutrients = value || {};
return (
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900">Nutrient Profile</h3>
<div>
<h4 className="font-medium text-gray-900 mb-3">Primary Nutrients (NPK)</h4>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nitrogen (N) %
</label>
<input
type="number"
step="0.1"
min="0"
value={nutrients.nitrogen || 0}
onChange={(e) => onChange({ ...nutrients, nitrogen: parseFloat(e.target.value) })}
className="w-full px-3 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">
Phosphorus (P) %
</label>
<input
type="number"
step="0.1"
min="0"
value={nutrients.phosphorus || 0}
onChange={(e) => onChange({ ...nutrients, phosphorus: parseFloat(e.target.value) })}
className="w-full px-3 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">
Potassium (K) %
</label>
<input
type="number"
step="0.1"
min="0"
value={nutrients.potassium || 0}
onChange={(e) => onChange({ ...nutrients, potassium: parseFloat(e.target.value) })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
</div>
<p className="mt-2 text-xs text-gray-600">
NPK ratio: {nutrients.nitrogen || 0}-{nutrients.phosphorus || 0}-{nutrients.potassium || 0}
</p>
</div>
<div className="pt-4 border-t border-gray-200">
<p className="text-sm text-gray-600">
💡 <strong>Tip:</strong> Leave at 0 if unknown. Use soil test kit for accurate NPK values.
</p>
</div>
</div>
);
}
// Lighting Section Component
function LightingSection({
value,
onChange,
}: {
value?: Partial<LightingConditions>;
onChange: (lighting: Partial<LightingConditions>) => void;
}) {
const lighting = value || { type: 'natural' };
return (
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900">Lighting Conditions</h3>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Light Type</label>
<select
value={lighting.type || 'natural'}
onChange={(e) => onChange({ ...lighting, type: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="natural">Natural Sunlight</option>
<option value="artificial">Artificial Light Only</option>
<option value="mixed">Mixed (Natural + Artificial)</option>
</select>
</div>
{(lighting.type === 'natural' || lighting.type === 'mixed') && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 bg-yellow-50 rounded-lg">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Sun Exposure
</label>
<select
value={lighting.naturalLight?.exposure || 'full_sun'}
onChange={(e) =>
onChange({
...lighting,
naturalLight: {
...lighting.naturalLight,
exposure: e.target.value as any,
} as any,
})
}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="full_sun">Full Sun (6+ hours)</option>
<option value="partial_sun">Partial Sun (4-6 hours)</option>
<option value="partial_shade">Partial Shade (2-4 hours)</option>
<option value="full_shade">Full Shade (&lt;2 hours)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Hours of Sunlight/Day
</label>
<input
type="number"
min="0"
max="24"
value={lighting.naturalLight?.hoursPerDay || 8}
onChange={(e) =>
onChange({
...lighting,
naturalLight: {
...lighting.naturalLight,
hoursPerDay: parseInt(e.target.value),
} as any,
})
}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
</div>
)}
<div className="pt-4 border-t border-gray-200">
<p className="text-sm text-gray-600">
💡 <strong>Tip:</strong> Most vegetables need 6+ hours of direct sunlight. Herbs can do well with 4-6 hours.
</p>
</div>
</div>
);
}
// Climate Section Component
function ClimateSection({
value,
onChange,
}: {
value?: Partial<ClimateConditions>;
onChange: (climate: Partial<ClimateConditions>) => void;
}) {
const climate = value || {};
return (
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900">Climate Conditions</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Day Temperature (°C) *
</label>
<input
type="number"
value={climate.temperatureDay || 22}
onChange={(e) => onChange({ ...climate, temperatureDay: parseFloat(e.target.value) })}
className="w-full px-3 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">
Night Temperature (°C) *
</label>
<input
type="number"
value={climate.temperatureNight || 18}
onChange={(e) => onChange({ ...climate, temperatureNight: parseFloat(e.target.value) })}
className="w-full px-3 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">
Average Humidity (%) *
</label>
<input
type="number"
min="0"
max="100"
value={climate.humidityAverage || 50}
onChange={(e) => onChange({ ...climate, humidityAverage: parseFloat(e.target.value) })}
className="w-full px-3 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">Airflow</label>
<select
value={climate.airflow || 'moderate'}
onChange={(e) => onChange({ ...climate, airflow: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="none">None (still air)</option>
<option value="minimal">Minimal</option>
<option value="moderate">Moderate (good)</option>
<option value="strong">Strong/Windy</option>
</select>
</div>
</div>
<div className="pt-4 border-t border-gray-200">
<p className="text-sm text-gray-600">
💡 <strong>Tip:</strong> Most plants thrive at 18-25°C. Good airflow prevents disease.
</p>
</div>
</div>
);
}
// Location Section Component
function LocationSection({
value,
onChange,
}: {
value?: Partial<EnvironmentLocation>;
onChange: (location: Partial<EnvironmentLocation>) => void;
}) {
const location = value || {};
return (
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900">Growing Location</h3>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Location Type *</label>
<select
value={location.type || 'outdoor'}
onChange={(e) => onChange({ ...location, type: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="indoor">Indoor</option>
<option value="outdoor">Outdoor</option>
<option value="greenhouse">Greenhouse</option>
<option value="polytunnel">Polytunnel</option>
<option value="shade_house">Shade House</option>
<option value="window">Window</option>
<option value="balcony">Balcony</option>
</select>
</div>
<div className="pt-4 border-t border-gray-200">
<p className="text-sm text-gray-600">
💡 <strong>Tip:</strong> Location type affects climate control and pest exposure.
</p>
</div>
</div>
);
}
// Container Section Component
function ContainerSection({
value,
onChange,
}: {
value?: Partial<ContainerInfo>;
onChange: (container: Partial<ContainerInfo>) => void;
}) {
const container = value || {};
return (
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900">Container Information</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Container Type</label>
<select
value={container.type || 'pot'}
onChange={(e) => onChange({ ...container, type: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="pot">Pot</option>
<option value="raised_bed">Raised Bed</option>
<option value="ground">In Ground</option>
<option value="fabric_pot">Fabric Pot</option>
<option value="hydroponic">Hydroponic</option>
<option value="hanging_basket">Hanging Basket</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Drainage * <span className="text-xs text-red-600">(CRITICAL!)</span>
</label>
<select
value={container.drainage || 'yes'}
onChange={(e) => onChange({ ...container, drainage: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="yes">Yes (has holes)</option>
<option value="no">No (sealed)</option>
</select>
{container.drainage === 'no' && (
<p className="mt-1 text-xs text-red-600 font-semibold">
WARNING: No drainage will likely kill your plant!
</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Size (optional)
</label>
<input
type="text"
placeholder="e.g., 5 gallon, 30cm diameter"
value={container.size || ''}
onChange={(e) => onChange({ ...container, size: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
</div>
<div className="pt-4 border-t border-gray-200">
<p className="text-sm text-gray-600">
💡 <strong>Tip:</strong> Always ensure drainage! Sitting water = root rot = dead plant.
</p>
</div>
</div>
);
}
// Watering Section Component
function WateringSection({
value,
onChange,
}: {
value?: Partial<WateringSchedule>;
onChange: (watering: Partial<WateringSchedule>) => void;
}) {
const watering = value || {};
return (
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900">Watering Schedule</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Method</label>
<select
value={watering.method || 'hand_water'}
onChange={(e) => onChange({ ...watering, method: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="hand_water">Hand Watering</option>
<option value="drip">Drip Irrigation</option>
<option value="soaker_hose">Soaker Hose</option>
<option value="sprinkler">Sprinkler</option>
<option value="self_watering">Self-Watering</option>
<option value="rain">Rain Fed</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Water Source</label>
<select
value={watering.waterSource || 'tap'}
onChange={(e) => onChange({ ...watering, waterSource: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="tap">Tap Water</option>
<option value="well">Well Water</option>
<option value="rain">Rain Water</option>
<option value="filtered">Filtered</option>
<option value="distilled">Distilled</option>
<option value="RO">Reverse Osmosis (RO)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Frequency (optional)
</label>
<input
type="text"
placeholder="e.g., daily, every 2-3 days"
value={watering.frequency || ''}
onChange={(e) => onChange({ ...watering, frequency: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
</div>
<div className="pt-4 border-t border-gray-200">
<p className="text-sm text-gray-600">
💡 <strong>Tip:</strong> Water when top inch of soil is dry. Overwatering kills more plants than underwatering!
</p>
</div>
</div>
);
}
// Surroundings Section Component
function SurroundingsSection({
value,
onChange,
}: {
value?: Partial<SurroundingEnvironment>;
onChange: (surroundings: Partial<SurroundingEnvironment>) => void;
}) {
const surroundings = value || {};
return (
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900">Surrounding Environment</h3>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Ecosystem Type</label>
<select
value={surroundings.ecosystem || 'urban'}
onChange={(e) => onChange({ ...surroundings, ecosystem: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="urban">Urban</option>
<option value="suburban">Suburban</option>
<option value="rural">Rural</option>
<option value="forest">Forest</option>
<option value="desert">Desert</option>
<option value="coastal">Coastal</option>
<option value="mountain">Mountain</option>
<option value="tropical">Tropical</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Wind Exposure</label>
<select
value={surroundings.windExposure || 'moderate'}
onChange={(e) => onChange({ ...surroundings, windExposure: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="sheltered">Sheltered (protected)</option>
<option value="moderate">Moderate</option>
<option value="exposed">Exposed</option>
<option value="windy">Very Windy</option>
</select>
</div>
<div className="pt-4 border-t border-gray-200">
<p className="text-sm text-gray-600">
💡 <strong>Tip:</strong> Track companion plants and pests to learn what works in your ecosystem.
</p>
</div>
</div>
);
}