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 (
{title}
{formattedValue}
{subtitle}
)}Objetivo: {formattedTarget}
)} {contextInfo && ({contextInfo}
)}Tendencia últimos 7 días
{subtitle}
)}{comparisonLabel}
)}