114 lines
3.3 KiB
TypeScript
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>
|
|
);
|
|
}
|