localgreenchain/components/transport/TransportTimeline.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

181 lines
6.7 KiB
TypeScript

import { TransportEvent, TransportEventType } from '../../lib/transport/types';
interface TransportTimelineProps {
plantId: string;
events: TransportEvent[];
}
const EVENT_ICONS: Record<TransportEventType, string> = {
seed_acquisition: '🌱',
planting: '🌿',
growing_transport: '🚚',
harvest: '🥬',
processing: '⚙️',
distribution: '📦',
consumer_delivery: '🏠',
seed_saving: '💾',
seed_sharing: '🤝',
};
const EVENT_COLORS: Record<TransportEventType, string> = {
seed_acquisition: 'bg-green-500',
planting: 'bg-emerald-500',
growing_transport: 'bg-blue-500',
harvest: 'bg-yellow-500',
processing: 'bg-orange-500',
distribution: 'bg-purple-500',
consumer_delivery: 'bg-pink-500',
seed_saving: 'bg-teal-500',
seed_sharing: 'bg-indigo-500',
};
function formatEventType(type: TransportEventType): string {
return type.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase());
}
function formatDate(dateString: string): string {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
function formatLocation(location: { city?: string; region?: string; address?: string }): string {
if (location.city && location.region) {
return `${location.city}, ${location.region}`;
}
return location.city || location.region || location.address || 'Unknown location';
}
export default function TransportTimeline({ plantId, events }: TransportTimelineProps) {
if (events.length === 0) {
return (
<div className="bg-white rounded-lg shadow-lg p-6">
<h3 className="text-xl font-bold text-gray-900 mb-4">Transport Timeline</h3>
<p className="text-gray-500 text-center py-8">No transport events recorded for this plant yet.</p>
</div>
);
}
const sortedEvents = [...events].sort(
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
);
return (
<div className="bg-white rounded-lg shadow-lg p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-bold text-gray-900">Transport Timeline</h3>
<span className="text-sm text-gray-500">Plant ID: {plantId.slice(0, 8)}...</span>
</div>
<div className="relative">
{/* Vertical line */}
<div className="absolute left-6 top-0 bottom-0 w-0.5 bg-gray-200"></div>
<div className="space-y-6">
{sortedEvents.map((event, index) => (
<div key={event.id} className="relative flex items-start">
{/* Event icon */}
<div
className={`relative z-10 flex items-center justify-center w-12 h-12 rounded-full ${EVENT_COLORS[event.eventType]} text-white text-xl shadow-lg`}
>
{EVENT_ICONS[event.eventType]}
</div>
{/* Event content */}
<div className="ml-4 flex-1 min-w-0">
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
<div className="flex items-center justify-between mb-2">
<h4 className="font-semibold text-gray-900">
{formatEventType(event.eventType)}
</h4>
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${
event.status === 'verified'
? 'bg-green-100 text-green-800'
: event.status === 'delivered'
? 'bg-blue-100 text-blue-800'
: event.status === 'in_transit'
? 'bg-yellow-100 text-yellow-800'
: 'bg-gray-100 text-gray-800'
}`}
>
{event.status}
</span>
</div>
<p className="text-sm text-gray-600 mb-3">{formatDate(event.timestamp)}</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
<div>
<span className="text-gray-500">From:</span>
<p className="font-medium text-gray-900">
{formatLocation(event.fromLocation)}
</p>
</div>
<div>
<span className="text-gray-500">To:</span>
<p className="font-medium text-gray-900">
{formatLocation(event.toLocation)}
</p>
</div>
</div>
<div className="mt-3 pt-3 border-t border-gray-200 grid grid-cols-3 gap-2 text-sm">
<div>
<span className="text-gray-500">Distance</span>
<p className="font-medium text-gray-900">{event.distanceKm.toFixed(1)} km</p>
</div>
<div>
<span className="text-gray-500">Duration</span>
<p className="font-medium text-gray-900">{event.durationMinutes} min</p>
</div>
<div>
<span className="text-gray-500">Carbon</span>
<p className="font-medium text-gray-900">
{event.carbonFootprintKg.toFixed(3)} kg
</p>
</div>
</div>
<div className="mt-2 text-xs text-gray-500">
Transport: {event.transportMethod.replace(/_/g, ' ')}
</div>
{event.notes && (
<p className="mt-2 text-sm text-gray-600 italic">"{event.notes}"</p>
)}
</div>
</div>
</div>
))}
</div>
</div>
{/* Summary */}
<div className="mt-6 pt-4 border-t border-gray-200">
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<p className="text-2xl font-bold text-gray-900">{events.length}</p>
<p className="text-sm text-gray-500">Total Events</p>
</div>
<div>
<p className="text-2xl font-bold text-blue-600">
{events.reduce((sum, e) => sum + e.distanceKm, 0).toFixed(1)} km
</p>
<p className="text-sm text-gray-500">Total Distance</p>
</div>
<div>
<p className="text-2xl font-bold text-green-600">
{events.reduce((sum, e) => sum + e.carbonFootprintKg, 0).toFixed(2)} kg
</p>
<p className="text-sm text-gray-500">Total Carbon</p>
</div>
</div>
</div>
</div>
);
}