import React, { useMemo } from 'react'; import { clsx } from 'clsx'; import { Badge } from '../../ui'; import DashboardCard from './DashboardCard'; export interface KPIValue { current: number; previous?: number; target?: number; format: 'currency' | 'number' | 'percentage'; prefix?: string; suffix?: string; } export interface KPITrend { direction: 'up' | 'down' | 'stable'; value: number; isPositive: boolean; comparisonPeriod: string; description?: string; } export interface KPIThreshold { excellent: number; good: number; warning: number; critical: number; } export interface SparklineDataPoint { date: string; value: number; label?: string; } export interface KPIWidgetProps { title: string; subtitle?: string; value: KPIValue; trend?: KPITrend; // Visual configuration icon?: React.ReactNode; color?: 'blue' | 'green' | 'orange' | 'red' | 'purple' | 'indigo' | 'teal'; variant?: 'default' | 'compact' | 'detailed' | 'chart'; // Chart data sparklineData?: SparklineDataPoint[]; showSparkline?: boolean; // Thresholds and status thresholds?: KPIThreshold; status?: 'excellent' | 'good' | 'warning' | 'critical' | 'neutral'; // Comparison and context comparisonLabel?: string; contextInfo?: string; // Interactive features isLoading?: boolean; onRefresh?: () => void; onClick?: () => void; // Accessibility and styling className?: string; 'aria-label'?: string; } // Predefined bakery KPI configurations export const BAKERY_KPI_CONFIGS = { dailyRevenue: { title: 'Ingresos Hoy', subtitle: 'Ventas del día actual', icon: ( ), color: 'green' as const, format: 'currency' as const }, orderCount: { title: 'Pedidos', subtitle: 'Órdenes procesadas hoy', icon: ( ), color: 'blue' as const, format: 'number' as const }, productivity: { title: 'Productividad', subtitle: 'Unidades producidas por hora', icon: ( ), color: 'orange' as const, format: 'number' as const, suffix: '/h' }, stockLevel: { title: 'Nivel Stock', subtitle: 'Porcentaje de stock disponible', icon: ( ), color: 'purple' as const, format: 'percentage' as const } }; const formatValue = (value: number, format: KPIValue['format'], prefix?: string, suffix?: string): string => { let formatted: string; switch (format) { case 'currency': formatted = new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR', minimumFractionDigits: 0, maximumFractionDigits: 2 }).format(value); break; case 'percentage': formatted = new Intl.NumberFormat('es-ES', { style: 'percent', minimumFractionDigits: 0, maximumFractionDigits: 1 }).format(value / 100); break; case 'number': default: formatted = new Intl.NumberFormat('es-ES').format(value); break; } return `${prefix || ''}${formatted}${suffix || ''}`; }; const calculateTrend = (current: number, previous: number): KPITrend => { // Handle undefined or null values if (current == null || previous == null || typeof current !== 'number' || typeof previous !== 'number' || isNaN(current) || isNaN(previous)) { return { direction: 'stable', value: 0, isPositive: true, comparisonPeriod: 'vs período anterior' }; } const change = current - previous; const percentChange = previous !== 0 ? (change / previous) * 100 : 0; return { direction: change > 0 ? 'up' : change < 0 ? 'down' : 'stable', value: Math.abs(percentChange), isPositive: change >= 0, comparisonPeriod: 'vs período anterior' }; }; const getStatusColor = (status: KPIWidgetProps['status']) => { switch (status) { case 'excellent': return { bg: 'bg-[var(--color-success)]/10', text: 'text-[var(--color-success)]', border: 'border-[var(--color-success)]/20', icon: 'text-[var(--color-success)]' }; case 'good': return { bg: 'bg-[var(--color-info)]/10', text: 'text-[var(--color-info)]', border: 'border-[var(--color-info)]/20', icon: 'text-[var(--color-info)]' }; case 'warning': return { bg: 'bg-[var(--color-warning)]/10', text: 'text-[var(--color-warning)]', border: 'border-[var(--color-warning)]/20', icon: 'text-[var(--color-warning)]' }; case 'critical': return { bg: 'bg-[var(--color-error)]/10', text: 'text-[var(--color-error)]', border: 'border-[var(--color-error)]/20', icon: 'text-[var(--color-error)]' }; default: return { bg: 'bg-[var(--bg-tertiary)]', text: 'text-[var(--text-secondary)]', border: 'border-[var(--border-primary)]', icon: 'text-[var(--text-tertiary)]' }; } }; const SimpleSparkline: React.FC<{ data: SparklineDataPoint[]; color: string }> = ({ data, color }) => { const max = Math.max(...data.map(d => d.value)); const min = Math.min(...data.map(d => d.value)); const range = max - min; const points = data.map((point, index) => { const x = (index / (data.length - 1)) * 100; const y = range === 0 ? 50 : ((max - point.value) / range) * 100; return `${x},${y}`; }).join(' '); const colorClasses = { blue: 'stroke-[var(--color-info)]', green: 'stroke-[var(--color-success)]', orange: 'stroke-[var(--color-primary)]', red: 'stroke-[var(--color-error)]', purple: 'stroke-[var(--color-info)]', indigo: 'stroke-[var(--color-info)]', teal: 'stroke-[var(--color-success)]' }; return (
); }; const KPIWidget: React.FC = ({ title, subtitle, value, trend, icon, color = 'blue', variant = 'default', sparklineData, showSparkline = false, thresholds, status, comparisonLabel, contextInfo, isLoading = false, onRefresh, onClick, className, 'aria-label': ariaLabel }) => { // Calculate trend if not provided const calculatedTrend = useMemo(() => { if (trend) return trend; if (value.previous !== undefined) { return calculateTrend(value.current, value.previous); } return null; }, [trend, value.current, value.previous]); // Determine status based on thresholds const calculatedStatus = useMemo(() => { if (status) return status; if (!thresholds) return 'neutral'; const { current } = value; if (current >= thresholds.excellent) return 'excellent'; if (current >= thresholds.good) return 'good'; if (current >= thresholds.warning) return 'warning'; return 'critical'; }, [status, thresholds, value.current]); const statusStyles = getStatusColor(calculatedStatus); const formattedValue = formatValue(value.current, value.format, value.prefix, value.suffix); const formattedTarget = value.target ? formatValue(value.target, value.format, value.prefix, value.suffix) : null; const colorClasses = { blue: 'text-[var(--color-info)]', green: 'text-[var(--color-success)]', orange: 'text-[var(--color-primary)]', red: 'text-[var(--color-error)]', purple: 'text-[var(--color-info)]', indigo: 'text-[var(--color-info)]', teal: 'text-[var(--color-success)]' }; const renderTrendIcon = (direction: KPITrend['direction']) => { const iconClass = calculatedTrend?.isPositive ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'; switch (direction) { case 'up': return ( ); case 'down': return ( ); default: return ( ); } }; const renderCompactVariant = () => (
{icon && (
{icon}
)}

{title}

{formattedValue}

{calculatedTrend && (
{renderTrendIcon(calculatedTrend.direction)} {(calculatedTrend.value || 0).toFixed(1)}%
)}
); const renderDetailedVariant = () => (
{/* Header */}
{icon && (
{icon}
)}

{title}

{subtitle && (

{subtitle}

)}
{calculatedStatus !== 'neutral' && ( {calculatedStatus === 'excellent' ? 'Excelente' : calculatedStatus === 'good' ? 'Bueno' : calculatedStatus === 'warning' ? 'Atención' : 'Crítico'} )}
{/* Value and trend */}
{formattedValue} {calculatedTrend && (
{renderTrendIcon(calculatedTrend.direction)} {(calculatedTrend.value || 0).toFixed(1)}% {calculatedTrend.comparisonPeriod}
)}
{formattedTarget && (

Objetivo: {formattedTarget}

)} {contextInfo && (

{contextInfo}

)}
{/* Sparkline chart */} {showSparkline && sparklineData && sparklineData.length > 0 && (

Tendencia últimos 7 días

)}
); const renderDefaultVariant = () => (
{icon && (
{icon}
)}

{title}

{subtitle && (

{subtitle}

)}
{formattedValue} {calculatedTrend && (
{renderTrendIcon(calculatedTrend.direction)} {(calculatedTrend.value || 0).toFixed(1)}%
)}
{comparisonLabel && (

{comparisonLabel}

)}
); const renderContent = () => { switch (variant) { case 'compact': return renderCompactVariant(); case 'detailed': return renderDetailedVariant(); case 'chart': return renderDetailedVariant(); default: return renderDefaultVariant(); } }; return ( {renderContent()} ); }; KPIWidget.displayName = 'KPIWidget'; export default KPIWidget;