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.
324 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|