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.
181 lines
6.7 KiB
TypeScript
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>
|
|
);
|
|
}
|