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
129 lines
3.6 KiB
TypeScript
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>
|
|
);
|
|
}
|