localgreenchain/components/analytics/EnvironmentalImpact.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

272 lines
12 KiB
TypeScript

import { EnvironmentalImpact as ImpactData, TransportEventType, TransportMethod } from '../../lib/transport/types';
interface EnvironmentalImpactProps {
impact: ImpactData;
title?: string;
showDetails?: boolean;
}
const METHOD_LABELS: Record<TransportMethod, string> = {
walking: 'Walking',
bicycle: 'Bicycle',
electric_vehicle: 'Electric Vehicle',
hybrid_vehicle: 'Hybrid Vehicle',
gasoline_vehicle: 'Gas Vehicle',
diesel_truck: 'Diesel Truck',
electric_truck: 'Electric Truck',
refrigerated_truck: 'Refrigerated Truck',
rail: 'Rail',
ship: 'Ship',
air: 'Air Freight',
drone: 'Drone',
local_delivery: 'Local Delivery',
customer_pickup: 'Customer Pickup',
};
const EVENT_LABELS: Record<TransportEventType, string> = {
seed_acquisition: 'Seed Acquisition',
planting: 'Planting',
growing_transport: 'Growing Transport',
harvest: 'Harvest',
processing: 'Processing',
distribution: 'Distribution',
consumer_delivery: 'Consumer Delivery',
seed_saving: 'Seed Saving',
seed_sharing: 'Seed Sharing',
};
function getCarbonRating(carbon: number): { label: string; color: string; bgColor: string } {
if (carbon < 0.5) return { label: 'Excellent', color: 'text-green-600', bgColor: 'bg-green-100' };
if (carbon < 1) return { label: 'Very Good', color: 'text-emerald-600', bgColor: 'bg-emerald-100' };
if (carbon < 2) return { label: 'Good', color: 'text-lime-600', bgColor: 'bg-lime-100' };
if (carbon < 5) return { label: 'Moderate', color: 'text-yellow-600', bgColor: 'bg-yellow-100' };
if (carbon < 10) return { label: 'High', color: 'text-orange-600', bgColor: 'bg-orange-100' };
return { label: 'Very High', color: 'text-red-600', bgColor: 'bg-red-100' };
}
export default function EnvironmentalImpact({
impact,
title = 'Environmental Impact Report',
showDetails = true,
}: EnvironmentalImpactProps) {
const rating = getCarbonRating(impact.totalCarbonKg);
// Sort methods by carbon impact
const sortedMethods = Object.entries(impact.breakdownByMethod)
.filter(([_, data]) => data.carbon > 0 || data.distance > 0)
.sort((a, b) => b[1].carbon - a[1].carbon);
// Sort events by count
const sortedEvents = Object.entries(impact.breakdownByEventType)
.filter(([_, data]) => data.count > 0)
.sort((a, b) => b[1].count - a[1].count);
const totalMethods = sortedMethods.length;
const totalEvents = sortedEvents.reduce((sum, [_, data]) => sum + data.count, 0);
return (
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
{/* Header */}
<div className="bg-gradient-to-r from-teal-600 to-cyan-600 text-white p-6">
<h3 className="text-xl font-bold">{title}</h3>
<p className="text-teal-200 text-sm mt-1">Complete carbon and food miles analysis</p>
</div>
<div className="p-6 space-y-6">
{/* Main metrics */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center p-4 bg-green-50 rounded-lg">
<p className="text-3xl font-bold text-green-600">{impact.totalCarbonKg.toFixed(2)}</p>
<p className="text-sm text-gray-500">Total Carbon (kg CO2)</p>
</div>
<div className="text-center p-4 bg-blue-50 rounded-lg">
<p className="text-3xl font-bold text-blue-600">{impact.totalFoodMiles.toFixed(1)}</p>
<p className="text-sm text-gray-500">Total Food Miles (km)</p>
</div>
<div className="text-center p-4 bg-purple-50 rounded-lg">
<p className="text-3xl font-bold text-purple-600">{impact.carbonPerKgProduce.toFixed(3)}</p>
<p className="text-sm text-gray-500">kg CO2 / kg Produce</p>
</div>
<div className="text-center p-4 bg-orange-50 rounded-lg">
<p className="text-3xl font-bold text-orange-600">{impact.milesPerKgProduce.toFixed(1)}</p>
<p className="text-sm text-gray-500">Miles / kg Produce</p>
</div>
</div>
{/* Rating badge */}
<div className="flex justify-center">
<div className={`px-6 py-3 rounded-full ${rating.bgColor}`}>
<span className={`text-lg font-bold ${rating.color}`}>
Environmental Rating: {rating.label}
</span>
</div>
</div>
{/* Comparison with conventional */}
{impact.comparisonToConventional && (
<div className="p-6 bg-gradient-to-r from-green-50 to-emerald-50 rounded-lg border border-green-200">
<h4 className="text-lg font-bold text-gray-900 mb-4">
Comparison vs Conventional Agriculture
</h4>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="text-center">
<div className="w-24 h-24 mx-auto mb-3 rounded-full bg-green-100 flex items-center justify-center">
<span className="text-3xl font-bold text-green-600">
{impact.comparisonToConventional.percentageReduction.toFixed(0)}%
</span>
</div>
<p className="text-sm text-gray-600">Carbon Reduction</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-green-600">
{impact.comparisonToConventional.carbonSaved.toFixed(1)}
</p>
<p className="text-sm text-gray-500">kg CO2 Saved</p>
<div className="mt-2 flex items-center justify-center gap-1 text-green-600">
<span>🌲</span>
<span className="text-xs">
= {(impact.comparisonToConventional.carbonSaved / 21).toFixed(1)} trees/year
</span>
</div>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-blue-600">
{impact.comparisonToConventional.milesSaved.toFixed(0)}
</p>
<p className="text-sm text-gray-500">km Saved</p>
<div className="mt-2 flex items-center justify-center gap-1 text-blue-600">
<span>🚗</span>
<span className="text-xs">
= {(impact.comparisonToConventional.milesSaved / 15).toFixed(0)} car trips
</span>
</div>
</div>
</div>
{/* Visual bar comparison */}
<div className="mt-6 space-y-3">
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-gray-600">Your Carbon Footprint</span>
<span className="font-medium text-green-600">{impact.totalCarbonKg.toFixed(2)} kg</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-4 overflow-hidden">
<div
className="h-full bg-green-500 rounded-full"
style={{
width: `${100 - impact.comparisonToConventional.percentageReduction}%`
}}
></div>
</div>
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-gray-600">Conventional Average</span>
<span className="font-medium text-gray-500">
{(impact.totalCarbonKg + impact.comparisonToConventional.carbonSaved).toFixed(2)} kg
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-4 overflow-hidden">
<div className="h-full bg-gray-400 rounded-full w-full"></div>
</div>
</div>
</div>
</div>
)}
{/* Detailed breakdowns */}
{showDetails && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* By Transport Method */}
{sortedMethods.length > 0 && (
<div className="p-4 border border-gray-200 rounded-lg">
<h4 className="font-semibold text-gray-900 mb-4">By Transport Method</h4>
<div className="space-y-3">
{sortedMethods.map(([method, data]) => {
const percentage = impact.totalCarbonKg > 0
? (data.carbon / impact.totalCarbonKg) * 100
: 0;
return (
<div key={method}>
<div className="flex justify-between text-sm mb-1">
<span className="text-gray-600">
{METHOD_LABELS[method as TransportMethod] || method}
</span>
<span className="font-medium text-gray-900">
{data.carbon.toFixed(3)} kg ({percentage.toFixed(0)}%)
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="h-full bg-teal-500 rounded-full"
style={{ width: `${percentage}%` }}
></div>
</div>
<p className="text-xs text-gray-400 mt-0.5">
{data.distance.toFixed(1)} km traveled
</p>
</div>
);
})}
</div>
</div>
)}
{/* By Event Type */}
{sortedEvents.length > 0 && (
<div className="p-4 border border-gray-200 rounded-lg">
<h4 className="font-semibold text-gray-900 mb-4">By Event Type</h4>
<div className="space-y-3">
{sortedEvents.map(([eventType, data]) => {
const percentage = totalEvents > 0
? (data.count / totalEvents) * 100
: 0;
return (
<div key={eventType}>
<div className="flex justify-between text-sm mb-1">
<span className="text-gray-600">
{EVENT_LABELS[eventType as TransportEventType] || eventType}
</span>
<span className="font-medium text-gray-900">
{data.count} events {data.carbon.toFixed(3)} kg
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="h-full bg-cyan-500 rounded-full"
style={{ width: `${percentage}%` }}
></div>
</div>
</div>
);
})}
</div>
</div>
)}
</div>
)}
{/* Tips Section */}
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<h4 className="font-semibold text-yellow-800 mb-2">Tips to Reduce Impact</h4>
<ul className="text-sm text-yellow-700 space-y-1">
<li> Prefer walking, cycling, or electric vehicles for short distances</li>
<li> Consolidate multiple transports into single trips</li>
<li> Source from local producers within 25km when possible</li>
<li> Use rail transport for longer distances when available</li>
<li> Avoid air freight unless absolutely necessary</li>
</ul>
</div>
{/* Summary stats */}
<div className="flex justify-between text-xs text-gray-500 pt-4 border-t border-gray-200">
<span>Transport methods used: {totalMethods}</span>
<span>Total events tracked: {totalEvents}</span>
</div>
</div>
</div>
);
}