localgreenchain/components/analytics/KPICard.tsx
Claude 816c3b3f2e
Implement Agent 7: Advanced Analytics Dashboard
Add comprehensive analytics system with:
- Analytics data layer (aggregator, metrics, trends, cache)
- 6 API endpoints (overview, plants, transport, farms, sustainability, export)
- 6 chart components (LineChart, BarChart, PieChart, AreaChart, Gauge, Heatmap)
- 5 dashboard widgets (KPICard, TrendIndicator, DataTable, DateRangePicker, FilterPanel)
- 5 dashboard pages (overview, plants, transport, farms, sustainability)
- Export functionality (CSV, JSON)

Dependencies added: recharts, d3, date-fns

Also includes minor fixes:
- Fix EnvironmentalForm spread type error
- Fix AgentOrchestrator Map iteration issues
- Fix next.config.js image domains undefined error
- Add downlevelIteration to tsconfig
2025-11-23 04:02:07 +00:00

129 lines
3.6 KiB
TypeScript

/**
* KPI Card Component
* Displays key performance indicators with trend indicators
*/
import { TrendDirection } from '../../lib/analytics/types';
interface KPICardProps {
title: string;
value: number | string;
unit?: string;
change?: number;
changePercent?: number;
trend?: TrendDirection;
color?: 'green' | 'blue' | 'purple' | 'orange' | 'red' | 'teal';
icon?: React.ReactNode;
loading?: boolean;
}
const colorClasses = {
green: {
bg: 'bg-green-50',
text: 'text-green-600',
icon: 'text-green-500',
},
blue: {
bg: 'bg-blue-50',
text: 'text-blue-600',
icon: 'text-blue-500',
},
purple: {
bg: 'bg-purple-50',
text: 'text-purple-600',
icon: 'text-purple-500',
},
orange: {
bg: 'bg-orange-50',
text: 'text-orange-600',
icon: 'text-orange-500',
},
red: {
bg: 'bg-red-50',
text: 'text-red-600',
icon: 'text-red-500',
},
teal: {
bg: 'bg-teal-50',
text: 'text-teal-600',
icon: 'text-teal-500',
},
};
export default function KPICard({
title,
value,
unit,
change,
changePercent,
trend = 'stable',
color = 'green',
icon,
loading = false,
}: KPICardProps) {
const classes = colorClasses[color];
const getTrendIcon = () => {
if (trend === 'up') {
return (
<svg className="w-4 h-4 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 10l7-7m0 0l7 7m-7-7v18" />
</svg>
);
}
if (trend === 'down') {
return (
<svg className="w-4 h-4 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
</svg>
);
}
return (
<svg className="w-4 h-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14" />
</svg>
);
};
const getTrendColor = () => {
if (trend === 'up') return 'text-green-600';
if (trend === 'down') return 'text-red-600';
return 'text-gray-500';
};
if (loading) {
return (
<div className={`${classes.bg} rounded-lg p-6 animate-pulse`}>
<div className="h-4 bg-gray-200 rounded w-1/2 mb-4"></div>
<div className="h-8 bg-gray-200 rounded w-3/4 mb-2"></div>
<div className="h-3 bg-gray-200 rounded w-1/3"></div>
</div>
);
}
return (
<div className={`${classes.bg} rounded-lg p-6 transition-all hover:shadow-md`}>
<div className="flex items-center justify-between mb-2">
<p className="text-sm font-medium text-gray-600">{title}</p>
{icon && <span className={classes.icon}>{icon}</span>}
</div>
<div className="flex items-baseline space-x-2">
<p className={`text-3xl font-bold ${classes.text}`}>{value}</p>
{unit && <span className="text-sm text-gray-500">{unit}</span>}
</div>
{(change !== undefined || changePercent !== undefined) && (
<div className={`flex items-center mt-2 space-x-1 ${getTrendColor()}`}>
{getTrendIcon()}
<span className="text-sm font-medium">
{changePercent !== undefined
? `${changePercent > 0 ? '+' : ''}${changePercent.toFixed(1)}%`
: change !== undefined
? `${change > 0 ? '+' : ''}${change}`
: ''}
</span>
<span className="text-xs text-gray-500">vs prev period</span>
</div>
)}
</div>
);
}