localgreenchain/components/transport/TransportEventForm.tsx
Claude 0cce5e2345
Add UI components for transport tracking, demand visualization, and analytics
Transport components:
- TransportTimeline: Visual timeline of transport events with status badges
- JourneyMap: SVG-based map visualization of plant journey locations
- CarbonFootprintCard: Carbon metrics display with comparison charts
- QRCodeDisplay: QR code generation for traceability verification
- TransportEventForm: Form for recording transport events

Demand components:
- DemandSignalCard: Regional demand signal with supply status indicators
- PreferencesForm: Multi-section consumer preference input form
- RecommendationList: Planting recommendations with risk assessment
- SupplyGapChart: Supply vs demand visualization with gap indicators
- SeasonalCalendar: Seasonal produce availability calendar view

Analytics components:
- EnvironmentalImpact: Comprehensive carbon and food miles analysis
- FoodMilesTracker: Food miles tracking with daily charts and targets
- SavingsCalculator: Environmental savings vs conventional agriculture

All components follow existing patterns, use Tailwind CSS, and are fully typed.
2025-11-22 18:34:51 +00:00

324 lines
11 KiB
TypeScript

import { useState } from 'react';
import { TransportEventType, TransportMethod, TransportLocation } from '../../lib/transport/types';
interface TransportEventFormProps {
onSubmit: (data: TransportEventFormData) => void;
plantId?: string;
batchId?: string;
loading?: boolean;
defaultEventType?: TransportEventType;
}
export interface TransportEventFormData {
eventType: TransportEventType;
fromLocation: Partial<TransportLocation>;
toLocation: Partial<TransportLocation>;
transportMethod: TransportMethod;
notes?: string;
plantIds?: string[];
seedBatchId?: string;
}
const EVENT_TYPES: { value: TransportEventType; label: string; icon: string }[] = [
{ value: 'seed_acquisition', label: 'Seed Acquisition', icon: '🌱' },
{ value: 'planting', label: 'Planting', icon: '🌿' },
{ value: 'growing_transport', label: 'Growing Transport', icon: '🚚' },
{ value: 'harvest', label: 'Harvest', icon: '🥬' },
{ value: 'processing', label: 'Processing', icon: '⚙️' },
{ value: 'distribution', label: 'Distribution', icon: '📦' },
{ value: 'consumer_delivery', label: 'Consumer Delivery', icon: '🏠' },
{ value: 'seed_saving', label: 'Seed Saving', icon: '💾' },
{ value: 'seed_sharing', label: 'Seed Sharing', icon: '🤝' },
];
const TRANSPORT_METHODS: { value: TransportMethod; label: string; carbonInfo: string }[] = [
{ value: 'walking', label: 'Walking', carbonInfo: '0 kg CO2/km' },
{ value: 'bicycle', label: 'Bicycle', carbonInfo: '0 kg CO2/km' },
{ value: 'electric_vehicle', label: 'Electric Vehicle', carbonInfo: '0.02 kg CO2/km' },
{ value: 'hybrid_vehicle', label: 'Hybrid Vehicle', carbonInfo: '0.08 kg CO2/km' },
{ value: 'gasoline_vehicle', label: 'Gasoline Vehicle', carbonInfo: '0.12 kg CO2/km' },
{ value: 'diesel_truck', label: 'Diesel Truck', carbonInfo: '0.15 kg CO2/km' },
{ value: 'electric_truck', label: 'Electric Truck', carbonInfo: '0.03 kg CO2/km' },
{ value: 'refrigerated_truck', label: 'Refrigerated Truck', carbonInfo: '0.25 kg CO2/km' },
{ value: 'rail', label: 'Rail', carbonInfo: '0.01 kg CO2/km' },
{ value: 'ship', label: 'Ship', carbonInfo: '0.008 kg CO2/km' },
{ value: 'air', label: 'Air Freight', carbonInfo: '0.5 kg CO2/km' },
{ value: 'drone', label: 'Drone', carbonInfo: '0.01 kg CO2/km' },
{ value: 'local_delivery', label: 'Local Delivery', carbonInfo: '0.05 kg CO2/km' },
{ value: 'customer_pickup', label: 'Customer Pickup', carbonInfo: '0.1 kg CO2/km' },
];
const LOCATION_TYPES = [
'farm',
'greenhouse',
'vertical_farm',
'warehouse',
'hub',
'market',
'consumer',
'seed_bank',
'other',
] as const;
export default function TransportEventForm({
onSubmit,
plantId,
batchId,
loading = false,
defaultEventType = 'harvest',
}: TransportEventFormProps) {
const [eventType, setEventType] = useState<TransportEventType>(defaultEventType);
const [transportMethod, setTransportMethod] = useState<TransportMethod>('electric_vehicle');
const [notes, setNotes] = useState('');
const [useCurrentLocation, setUseCurrentLocation] = useState(false);
const [gettingLocation, setGettingLocation] = useState(false);
const [fromLocation, setFromLocation] = useState<Partial<TransportLocation>>({
latitude: 0,
longitude: 0,
locationType: 'farm',
city: '',
region: '',
});
const [toLocation, setToLocation] = useState<Partial<TransportLocation>>({
latitude: 0,
longitude: 0,
locationType: 'market',
city: '',
region: '',
});
const getCurrentLocation = async (target: 'from' | 'to') => {
if (!navigator.geolocation) {
alert('Geolocation is not supported by your browser');
return;
}
setGettingLocation(true);
navigator.geolocation.getCurrentPosition(
(position) => {
const update = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
};
if (target === 'from') {
setFromLocation((prev) => ({ ...prev, ...update }));
} else {
setToLocation((prev) => ({ ...prev, ...update }));
}
setGettingLocation(false);
},
(error) => {
console.error('Error getting location:', error);
alert('Unable to get your location. Please enter coordinates manually.');
setGettingLocation(false);
}
);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit({
eventType,
fromLocation,
toLocation,
transportMethod,
notes: notes || undefined,
plantIds: plantId ? [plantId] : undefined,
seedBatchId: batchId,
});
};
const LocationInput = ({
label,
value,
onChange,
onGetCurrent,
}: {
label: string;
value: Partial<TransportLocation>;
onChange: (loc: Partial<TransportLocation>) => void;
onGetCurrent: () => void;
}) => (
<div className="space-y-3 p-4 bg-gray-50 rounded-lg">
<div className="flex items-center justify-between">
<h4 className="font-medium text-gray-900">{label}</h4>
<button
type="button"
onClick={onGetCurrent}
disabled={gettingLocation}
className="text-sm text-blue-600 hover:text-blue-800 disabled:text-gray-400"
>
{gettingLocation ? 'Getting...' : '📍 Use Current'}
</button>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs text-gray-600 mb-1">Latitude</label>
<input
type="number"
step="any"
value={value.latitude || ''}
onChange={(e) => onChange({ ...value, latitude: parseFloat(e.target.value) || 0 })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-green-500"
placeholder="0.0000"
/>
</div>
<div>
<label className="block text-xs text-gray-600 mb-1">Longitude</label>
<input
type="number"
step="any"
value={value.longitude || ''}
onChange={(e) => onChange({ ...value, longitude: parseFloat(e.target.value) || 0 })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-green-500"
placeholder="0.0000"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs text-gray-600 mb-1">City</label>
<input
type="text"
value={value.city || ''}
onChange={(e) => onChange({ ...value, city: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-green-500"
placeholder="City name"
/>
</div>
<div>
<label className="block text-xs text-gray-600 mb-1">Region</label>
<input
type="text"
value={value.region || ''}
onChange={(e) => onChange({ ...value, region: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-green-500"
placeholder="State/Province"
/>
</div>
</div>
<div>
<label className="block text-xs text-gray-600 mb-1">Location Type</label>
<select
value={value.locationType || 'other'}
onChange={(e) => onChange({ ...value, locationType: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-green-500"
>
{LOCATION_TYPES.map((type) => (
<option key={type} value={type}>
{type.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase())}
</option>
))}
</select>
</div>
</div>
);
return (
<form onSubmit={handleSubmit} className="bg-white rounded-lg shadow-lg p-6 space-y-6">
<h3 className="text-xl font-bold text-gray-900">Record Transport Event</h3>
{/* Event Type */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Event Type</label>
<div className="grid grid-cols-3 gap-2">
{EVENT_TYPES.map((type) => (
<button
key={type.value}
type="button"
onClick={() => setEventType(type.value)}
className={`p-3 rounded-lg border-2 text-sm font-medium transition ${
eventType === type.value
? 'border-green-500 bg-green-50 text-green-700'
: 'border-gray-200 hover:border-gray-300 text-gray-600'
}`}
>
<span className="text-lg mr-1">{type.icon}</span>
<span className="hidden sm:inline">{type.label}</span>
</button>
))}
</div>
</div>
{/* Locations */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<LocationInput
label="From Location"
value={fromLocation}
onChange={setFromLocation}
onGetCurrent={() => getCurrentLocation('from')}
/>
<LocationInput
label="To Location"
value={toLocation}
onChange={setToLocation}
onGetCurrent={() => getCurrentLocation('to')}
/>
</div>
{/* Transport Method */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Transport Method</label>
<select
value={transportMethod}
onChange={(e) => setTransportMethod(e.target.value as TransportMethod)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
{TRANSPORT_METHODS.map((method) => (
<option key={method.value} value={method.value}>
{method.label} - {method.carbonInfo}
</option>
))}
</select>
<p className="mt-1 text-xs text-gray-500">
Carbon emissions are calculated based on distance and transport method
</p>
</div>
{/* Notes */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Notes (optional)</label>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
placeholder="Any additional details about this transport event..."
/>
</div>
{/* IDs info */}
{(plantId || batchId) && (
<div className="p-3 bg-blue-50 rounded-lg text-sm">
{plantId && (
<p className="text-blue-700">
<strong>Plant ID:</strong> {plantId}
</p>
)}
{batchId && (
<p className="text-blue-700">
<strong>Batch ID:</strong> {batchId}
</p>
)}
</div>
)}
{/* Submit */}
<button
type="submit"
disabled={loading}
className="w-full py-3 px-4 bg-green-600 hover:bg-green-700 text-white font-semibold rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Recording...' : 'Record Transport Event'}
</button>
</form>
);
}