Files
whyrating-engine-legacy/web/components/dashboard/widgets/StatCard.tsx
2026-02-02 18:19:00 +00:00

114 lines
3.3 KiB
TypeScript

'use client';
import {
TrendingUp,
TrendingDown,
MessageSquare,
CheckCircle,
AlertTriangle,
Star,
Activity,
} from 'lucide-react';
import type { WidgetConfig, StatCardData, StatCardConfig } from '@/lib/pipeline-types';
import { WidgetWrapper } from './WidgetWrapper';
interface StatCardProps {
config: WidgetConfig;
data: StatCardData | null;
loading: boolean;
error?: string;
onRefresh?: () => void;
}
// Icon mapping
const ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
'message-square': MessageSquare,
'check-circle': CheckCircle,
'alert-triangle': AlertTriangle,
star: Star,
activity: Activity,
};
// Color mapping (light mode optimized for bg-gray-50 background)
const COLORS: Record<string, string> = {
blue: 'text-blue-700 bg-blue-100',
green: 'text-green-700 bg-green-100',
red: 'text-red-700 bg-red-100',
yellow: 'text-yellow-700 bg-yellow-100',
purple: 'text-purple-700 bg-purple-100',
gray: 'text-gray-700 bg-gray-100',
};
/**
* Format a value according to a format string.
* Supports: {value:,} for thousands, {value:.1f} for decimals, {value:.1%} for percentages
*/
function formatValue(value: number | string, format?: string): string {
if (!format) return String(value);
const num = typeof value === 'string' ? parseFloat(value) : value;
if (isNaN(num)) return String(value);
// Simple format parsing
if (format.includes(':,}')) {
return num.toLocaleString();
}
if (format.includes(':.1f}')) {
return num.toFixed(1);
}
if (format.includes(':.2f}')) {
return num.toFixed(2);
}
if (format.includes(':.1%}')) {
return (num * 100).toFixed(1) + '%';
}
return String(value);
}
/**
* Stat card widget for displaying KPIs.
*/
export function StatCard({ config, data, loading, error, onRefresh }: StatCardProps) {
const widgetConfig = config.config as unknown as StatCardConfig;
const Icon = widgetConfig.icon ? ICONS[widgetConfig.icon] : Activity;
const colorClass = widgetConfig.color ? COLORS[widgetConfig.color] : COLORS.gray;
// Extract value and trend from data
const value = data?.[widgetConfig.value_key] ?? 0;
const trend = widgetConfig.trend_key ? data?.[widgetConfig.trend_key] : undefined;
return (
<WidgetWrapper config={config} loading={loading} error={error} onRefresh={onRefresh}>
<div className="flex items-center justify-between h-full">
<div className="flex-1">
<p className="text-3xl font-bold text-gray-900">
{formatValue(value, widgetConfig.format)}
</p>
{trend !== undefined && (
<div className="flex items-center mt-1">
{Number(trend) >= 0 ? (
<TrendingUp className="w-4 h-4 text-green-500 mr-1" />
) : (
<TrendingDown className="w-4 h-4 text-red-500 mr-1" />
)}
<span
className={`text-sm ${
Number(trend) >= 0 ? 'text-green-600' : 'text-red-600'
}`}
>
{formatValue(trend, widgetConfig.trend_format || '{value:.1f}')}
</span>
</div>
)}
</div>
{Icon && (
<div className={`p-3 rounded-full ${colorClass}`}>
<Icon className="w-6 h-6" />
</div>
)}
</div>
</WidgetWrapper>
);
}