localgreenchain/components/demand/DemandSignalCard.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

163 lines
6.5 KiB
TypeScript

import { DemandSignal, DemandItem } from '../../lib/demand/types';
interface DemandSignalCardProps {
signal: DemandSignal;
onViewDetails?: () => void;
}
const STATUS_COLORS: Record<DemandSignal['supplyStatus'], { bg: string; text: string; icon: string }> = {
surplus: { bg: 'bg-green-100', text: 'text-green-800', icon: '📈' },
balanced: { bg: 'bg-blue-100', text: 'text-blue-800', icon: '⚖️' },
shortage: { bg: 'bg-yellow-100', text: 'text-yellow-800', icon: '⚠️' },
critical: { bg: 'bg-red-100', text: 'text-red-800', icon: '🚨' },
};
function formatCategory(category: string): string {
return category.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase());
}
export default function DemandSignalCard({ signal, onViewDetails }: DemandSignalCardProps) {
const statusStyle = STATUS_COLORS[signal.supplyStatus];
// Get top demand items
const topItems = [...signal.demandItems]
.sort((a, b) => b.weeklyDemandKg - a.weeklyDemandKg)
.slice(0, 5);
const gapPercentage = signal.totalWeeklyDemandKg > 0
? (signal.supplyGapKg / signal.totalWeeklyDemandKg) * 100
: 0;
return (
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
{/* Header */}
<div className="bg-gradient-to-r from-blue-600 to-indigo-600 text-white p-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-bold">{signal.region.name}</h3>
<p className="text-blue-200 text-sm">
{signal.seasonalPeriod.charAt(0).toUpperCase() + signal.seasonalPeriod.slice(1)} Season
</p>
</div>
<div className={`px-3 py-1 rounded-full ${statusStyle.bg}`}>
<span className={`text-sm font-semibold ${statusStyle.text}`}>
{statusStyle.icon} {signal.supplyStatus.charAt(0).toUpperCase() + signal.supplyStatus.slice(1)}
</span>
</div>
</div>
</div>
<div className="p-6 space-y-4">
{/* Key metrics */}
<div className="grid grid-cols-3 gap-4">
<div className="text-center p-3 bg-gray-50 rounded-lg">
<p className="text-2xl font-bold text-gray-900">{signal.totalConsumers}</p>
<p className="text-xs text-gray-500">Consumers</p>
</div>
<div className="text-center p-3 bg-blue-50 rounded-lg">
<p className="text-2xl font-bold text-blue-600">{signal.totalWeeklyDemandKg.toFixed(0)}</p>
<p className="text-xs text-gray-500">Weekly Demand (kg)</p>
</div>
<div className="text-center p-3 bg-green-50 rounded-lg">
<p className="text-2xl font-bold text-green-600">{signal.currentSupplyKg.toFixed(0)}</p>
<p className="text-xs text-gray-500">Current Supply (kg)</p>
</div>
</div>
{/* Supply Gap Indicator */}
<div className="p-4 bg-gray-50 rounded-lg">
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-gray-700">Supply Coverage</span>
<span className={`text-sm font-bold ${gapPercentage > 20 ? 'text-red-600' : 'text-green-600'}`}>
{(100 - gapPercentage).toFixed(0)}% covered
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-3">
<div
className={`h-3 rounded-full transition-all ${
gapPercentage > 50 ? 'bg-red-500' : gapPercentage > 20 ? 'bg-yellow-500' : 'bg-green-500'
}`}
style={{ width: `${Math.min(100 - gapPercentage, 100)}%` }}
></div>
</div>
{signal.supplyGapKg > 0 && (
<p className="text-xs text-red-600 mt-2">
Gap: {signal.supplyGapKg.toFixed(0)} kg needed
</p>
)}
</div>
{/* Top Demand Items */}
<div>
<h4 className="text-sm font-semibold text-gray-900 mb-3">Top Demanded Items</h4>
<div className="space-y-2">
{topItems.map((item, index) => (
<DemandItemRow key={index} item={item} rank={index + 1} />
))}
</div>
</div>
{/* Confidence indicator */}
<div className="flex items-center justify-between pt-4 border-t border-gray-200">
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500">Confidence:</span>
<div className="flex gap-1">
{[1, 2, 3, 4, 5].map((level) => (
<div
key={level}
className={`w-2 h-4 rounded-sm ${
level <= signal.confidenceLevel / 20 ? 'bg-green-500' : 'bg-gray-200'
}`}
></div>
))}
</div>
<span className="text-sm text-gray-600">{signal.confidenceLevel}%</span>
</div>
<span className="text-xs text-gray-400">
Updated: {new Date(signal.timestamp).toLocaleDateString()}
</span>
</div>
{/* View Details Button */}
{onViewDetails && (
<button
onClick={onViewDetails}
className="w-full mt-4 py-2 px-4 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-lg transition"
>
View Full Details
</button>
)}
</div>
</div>
);
}
function DemandItemRow({ item, rank }: { item: DemandItem; rank: number }) {
const urgencyColors: Record<DemandItem['urgency'], string> = {
immediate: 'bg-red-100 text-red-700',
this_week: 'bg-orange-100 text-orange-700',
this_month: 'bg-yellow-100 text-yellow-700',
next_season: 'bg-blue-100 text-blue-700',
};
return (
<div className="flex items-center p-2 bg-gray-50 rounded-lg hover:bg-gray-100 transition">
<span className="w-6 h-6 flex items-center justify-center bg-indigo-100 text-indigo-700 rounded-full text-xs font-bold">
{rank}
</span>
<div className="ml-3 flex-1">
<p className="text-sm font-medium text-gray-900">{item.produceType}</p>
<p className="text-xs text-gray-500">{formatCategory(item.category)}</p>
</div>
<div className="text-right">
<p className="text-sm font-bold text-gray-900">{item.weeklyDemandKg.toFixed(1)} kg</p>
<span className={`text-xs px-2 py-0.5 rounded-full ${urgencyColors[item.urgency]}`}>
{item.urgency.replace(/_/g, ' ')}
</span>
</div>
{item.inSeason && (
<span className="ml-2 text-green-500" title="In Season">🌿</span>
)}
</div>
);
}