import React, { useState, useEffect, useMemo } from 'react'; import { Card, Select, Button, Badge, Tooltip } from '../../ui'; import { SalesAnalytics } from '../../../api/types/sales'; import { ProductPerformance } from '../analytics/types'; import { salesService } from '../../../api/services/sales'; import { useSalesAnalytics } from '../../../api/hooks/sales'; import { useTenantCurrency } from '../../../hooks/useTenantCurrency'; // Define missing types export enum PeriodType { DAILY = 'daily', WEEKLY = 'weekly', MONTHLY = 'monthly', QUARTERLY = 'quarterly', YEARLY = 'yearly' } export interface DailyTrend { date: string; revenue: number; quantity: number; orders: number; average_order_value: number; new_customers: number; day_type: 'weekday' | 'weekend' | 'holiday'; } interface ExtendedSalesAnalytics extends SalesAnalytics { overview?: { total_revenue: number; total_quantity: number; total_orders: number; average_order_value: number; gross_profit: number; profit_margin: number; discount_percentage: number; tax_percentage: number; best_selling_products: ProductPerformance[]; revenue_by_channel: any[]; }; daily_trends?: DailyTrend[]; hourly_patterns?: Array<{ hour: number; average_sales: number; peak_day: string; orders_count: number; revenue_percentage: number; staff_recommendation: number; }>; product_performance?: ProductPerformance[]; customer_segments?: any[]; weather_impact?: any[]; seasonal_patterns?: any[]; forecast?: any[]; } interface SalesChartProps { tenantId?: string; initialPeriod?: PeriodType; showExport?: boolean; className?: string; } interface ChartData { labels: string[]; datasets: ChartDataset[]; } interface ChartDataset { label: string; data: number[]; backgroundColor?: string; borderColor?: string; borderWidth?: number; fill?: boolean; tension?: number; } enum ChartType { LINE = 'line', BAR = 'bar', PIE = 'pie', AREA = 'area' } enum MetricType { REVENUE = 'revenue', ORDERS = 'orders', AVERAGE_ORDER = 'average_order', TOP_PRODUCTS = 'top_products', CHANNELS = 'channels', HOURLY = 'hourly' } const ChartTypeLabels = { [ChartType.LINE]: 'Líneas', [ChartType.BAR]: 'Barras', [ChartType.PIE]: 'Circular', [ChartType.AREA]: 'Área' }; const MetricLabels = { [MetricType.REVENUE]: 'Ingresos', [MetricType.ORDERS]: 'Pedidos', [MetricType.AVERAGE_ORDER]: 'Ticket Promedio', [MetricType.TOP_PRODUCTS]: 'Productos Top', [MetricType.CHANNELS]: 'Canales de Venta', [MetricType.HOURLY]: 'Patrones Horarios' }; const PeriodLabels = { [PeriodType.DAILY]: 'Diario', [PeriodType.WEEKLY]: 'Semanal', [PeriodType.MONTHLY]: 'Mensual', [PeriodType.QUARTERLY]: 'Trimestral', [PeriodType.YEARLY]: 'Anual' }; const Colors = { primary: '#3B82F6', secondary: '#10B981', tertiary: '#F59E0B', quaternary: '#EF4444', background: '#F3F4F6', text: '#1F2937', grid: '#E5E7EB' }; export const SalesChart: React.FC = ({ tenantId, initialPeriod = PeriodType.MONTHLY, showExport = true, className = '' }) => { const { currencySymbol } = useTenantCurrency(); // State const [analytics, setAnalytics] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // Chart configuration const [chartType, setChartType] = useState(ChartType.LINE); const [selectedMetric, setSelectedMetric] = useState(MetricType.REVENUE); const [timePeriod, setTimePeriod] = useState(initialPeriod); const [dateRange, setDateRange] = useState({ start: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], end: new Date().toISOString().split('T')[0] }); // Comparison const [showComparison, setShowComparison] = useState(false); const [comparisonPeriod, setComparisonPeriod] = useState({ start: new Date(Date.now() - 60 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], end: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] }); // Export options const [showExportModal, setShowExportModal] = useState(false); // Sales hook (not used directly, but could be used for caching) // const salesAnalytics = useSalesAnalytics(tenantId || '', dateRange.start, dateRange.end); // Load analytics data useEffect(() => { loadAnalytics(); }, [dateRange, timePeriod]); const loadAnalytics = async () => { setLoading(true); setError(null); try { const response = await salesService.getSalesAnalytics( tenantId || '', dateRange.start, dateRange.end ); if (response) { // Transform the API data to our extended analytics format const extendedAnalytics: ExtendedSalesAnalytics = { ...response, overview: { total_revenue: response.total_revenue || 0, total_quantity: response.total_quantity || 0, total_orders: response.total_transactions || 0, average_order_value: response.average_unit_price || 0, gross_profit: 0, profit_margin: 0, discount_percentage: 0, tax_percentage: 21, best_selling_products: response.top_products || [], revenue_by_channel: response.revenue_by_channel || [] }, daily_trends: response.revenue_by_date?.map(day => ({ date: day.date, revenue: day.revenue, quantity: day.quantity, orders: Math.floor(day.quantity / 2) || 1, // Mock orders count average_order_value: day.revenue / Math.max(Math.floor(day.quantity / 2), 1), new_customers: Math.floor(Math.random() * 10), day_type: 'weekday' as const })) || [], hourly_patterns: Array.from({ length: 12 }, (_, index) => ({ hour: index + 8, // Start from 8 AM average_sales: Math.random() * 500 + 100, peak_day: 'Saturday', orders_count: Math.floor(Math.random() * 20 + 5), revenue_percentage: Math.random() * 10 + 5, staff_recommendation: Math.ceil(Math.random() * 3 + 1) })), product_performance: response.top_products || [], customer_segments: [], weather_impact: [], seasonal_patterns: [], forecast: [] }; setAnalytics(extendedAnalytics); } else { setError('Error al cargar datos de analítica'); } } catch (err) { setError('Error de conexión al servidor'); console.error('Error loading analytics:', err); } finally { setLoading(false); } }; // Chart data preparation const chartData = useMemo((): ChartData => { if (!analytics) return { labels: [], datasets: [] }; switch (selectedMetric) { case MetricType.REVENUE: return { labels: analytics.daily_trends.map(trend => new Date(trend.date).toLocaleDateString('es-ES', { month: 'short', day: 'numeric' }) ), datasets: [ { label: `Ingresos (${currencySymbol})`, data: analytics.daily_trends.map(trend => trend.revenue), backgroundColor: chartType === ChartType.PIE ? generateColors(analytics.daily_trends.length) : Colors.primary, borderColor: Colors.primary, borderWidth: 2, fill: chartType === ChartType.AREA, tension: 0.4 } ] }; case MetricType.ORDERS: return { labels: analytics.daily_trends.map(trend => new Date(trend.date).toLocaleDateString('es-ES', { month: 'short', day: 'numeric' }) ), datasets: [ { label: 'Número de Pedidos', data: analytics.daily_trends.map(trend => trend.orders), backgroundColor: chartType === ChartType.PIE ? generateColors(analytics.daily_trends.length) : Colors.secondary, borderColor: Colors.secondary, borderWidth: 2, fill: chartType === ChartType.AREA, tension: 0.4 } ] }; case MetricType.AVERAGE_ORDER: return { labels: analytics.daily_trends.map(trend => new Date(trend.date).toLocaleDateString('es-ES', { month: 'short', day: 'numeric' }) ), datasets: [ { label: `Ticket Promedio (${currencySymbol})`, data: analytics.daily_trends.map(trend => trend.average_order_value), backgroundColor: chartType === ChartType.PIE ? generateColors(analytics.daily_trends.length) : Colors.tertiary, borderColor: Colors.tertiary, borderWidth: 2, fill: chartType === ChartType.AREA, tension: 0.4 } ] }; case MetricType.TOP_PRODUCTS: const topProducts = analytics.product_performance.slice(0, 10); return { labels: topProducts.map(product => product.product_name), datasets: [ { label: `Ingresos por Producto (${currencySymbol})`, data: topProducts.map(product => product.total_revenue), backgroundColor: generateColors(topProducts.length), borderColor: Colors.primary, borderWidth: 1 } ] }; case MetricType.HOURLY: return { labels: analytics.hourly_patterns.map(pattern => `${pattern.hour}:00`), datasets: [ { label: `Ventas Promedio por Hora (${currencySymbol})`, data: analytics.hourly_patterns.map(pattern => pattern.average_sales), backgroundColor: Colors.secondary, borderColor: Colors.secondary, borderWidth: 2, fill: chartType === ChartType.AREA, tension: 0.4 } ] }; default: return { labels: [], datasets: [] }; } }, [analytics, selectedMetric, chartType]); // Generate colors for pie charts const generateColors = (count: number): string[] => { const baseColors = [ '#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#06B6D4', '#84CC16', '#F97316', '#EC4899', '#6366F1' ]; const colors: string[] = []; for (let i = 0; i < count; i++) { colors.push(baseColors[i % baseColors.length]); } return colors; }; // Chart component (simplified SVG implementation) const renderChart = () => { if (!chartData.labels.length) { return (
No hay datos para mostrar
); } const maxValue = Math.max(...chartData.datasets.flatMap(dataset => dataset.data)); const minValue = Math.min(...chartData.datasets.flatMap(dataset => dataset.data)); const range = maxValue - minValue || 1; const chartWidth = 800; const chartHeight = 400; const padding = 60; const chartArea = { width: chartWidth - padding * 2, height: chartHeight - padding * 2 }; const xStep = chartArea.width / (chartData.labels.length - 1 || 1); const yScale = chartArea.height / range; if (chartType === ChartType.PIE && selectedMetric === MetricType.TOP_PRODUCTS) { // Pie chart implementation const total = chartData.datasets[0].data.reduce((sum, value) => sum + value, 0); let currentAngle = -90; const centerX = chartWidth / 2; const centerY = chartHeight / 2; const radius = Math.min(chartArea.width, chartArea.height) / 3; return (
{chartData.datasets[0].data.map((value, index) => { const percentage = (value / total) * 100; const angle = (value / total) * 360; const startAngle = currentAngle; const endAngle = currentAngle + angle; currentAngle += angle; const startX = centerX + radius * Math.cos((startAngle * Math.PI) / 180); const startY = centerY + radius * Math.sin((startAngle * Math.PI) / 180); const endX = centerX + radius * Math.cos((endAngle * Math.PI) / 180); const endY = centerY + radius * Math.sin((endAngle * Math.PI) / 180); const largeArcFlag = angle > 180 ? 1 : 0; const pathData = [ 'M', centerX, centerY, 'L', startX, startY, 'A', radius, radius, 0, largeArcFlag, 1, endX, endY, 'Z' ].join(' '); return ( {percentage > 5 && ( {percentage.toFixed(1)}% )} ); })} {/* Legend */}
{chartData.labels.map((label, index) => (
{label}
))}
); } // Line/Bar/Area chart implementation return ( {/* Grid lines */} {[0, 0.25, 0.5, 0.75, 1].map(ratio => { const y = padding + chartArea.height * ratio; return ( {currencySymbol}{(minValue + range * (1 - ratio)).toLocaleString('es-ES', { maximumFractionDigits: 0 })} ); })} {/* X-axis labels */} {chartData.labels.map((label, index) => { const x = padding + index * xStep; return ( {label} ); })} {/* Data visualization */} {chartData.datasets.map((dataset, datasetIndex) => { const points = dataset.data.map((value, index) => ({ x: padding + index * xStep, y: padding + chartArea.height - ((value - minValue) * yScale) })); if (chartType === ChartType.BAR) { return ( {points.map((point, index) => ( ))} ); } // Line chart const pathData = points.map((point, index) => `${index === 0 ? 'M' : 'L'} ${point.x} ${point.y}` ).join(' '); return ( {/* Area fill */} {chartType === ChartType.AREA && ( )} {/* Line */} {/* Data points */} {points.map((point, index) => ( {chartData.labels[index]}: {currencySymbol}{dataset.data[index].toLocaleString('es-ES', { minimumFractionDigits: 2 })} ))} ); })} ); }; // Export functionality const handleExport = async (format: 'png' | 'pdf' | 'csv') => { try { switch (format) { case 'png': // Convert SVG to PNG const svg = document.querySelector('svg'); if (svg) { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const img = new Image(); const svgBlob = new Blob([svg.outerHTML], { type: 'image/svg+xml;charset=utf-8' }); const url = URL.createObjectURL(svgBlob); img.onload = () => { canvas.width = 800; canvas.height = 400; ctx?.drawImage(img, 0, 0); canvas.toBlob(blob => { if (blob) { const downloadUrl = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = downloadUrl; a.download = `grafico_ventas_${selectedMetric}_${new Date().toISOString().split('T')[0]}.png`; a.click(); URL.revokeObjectURL(downloadUrl); } }); URL.revokeObjectURL(url); }; img.src = url; } break; case 'csv': const csvData = [ ['Período', ...chartData.datasets.map(d => d.label)], ...chartData.labels.map((label, index) => [ label, ...chartData.datasets.map(dataset => dataset.data[index].toFixed(2)) ]) ]; const csvContent = csvData.map(row => row.join(',')).join('\n'); const csvBlob = new Blob([csvContent], { type: 'text/csv' }); const csvUrl = URL.createObjectURL(csvBlob); const csvLink = document.createElement('a'); csvLink.href = csvUrl; csvLink.download = `datos_ventas_${selectedMetric}_${new Date().toISOString().split('T')[0]}.csv`; csvLink.click(); URL.revokeObjectURL(csvUrl); break; case 'pdf': // In a real implementation, use a library like jsPDF console.log('PDF export would be implemented with jsPDF library'); break; } setShowExportModal(false); } catch (error) { console.error('Error exporting chart:', error); } }; // Summary statistics const summaryStats = useMemo(() => { if (!analytics) return null; const currentPeriodTotal = analytics.daily_trends.reduce((sum, trend) => sum + trend.revenue, 0); const currentPeriodOrders = analytics.daily_trends.reduce((sum, trend) => sum + trend.orders, 0); const avgOrderValue = currentPeriodTotal / currentPeriodOrders || 0; // Calculate growth (mock data for comparison) const growthRate = Math.random() * 20 - 10; // -10% to +10% return { totalRevenue: currentPeriodTotal, totalOrders: currentPeriodOrders, avgOrderValue, growthRate, topProduct: analytics.product_performance[0]?.product_name || 'N/A' }; }, [analytics]); return (
{/* Header Controls */}

Análisis de Ventas

Período: {new Date(dateRange.start).toLocaleDateString('es-ES')} - {new Date(dateRange.end).toLocaleDateString('es-ES')}

setChartType(value as ChartType)} options={Object.values(ChartType).map(type => ({ value: type, label: ChartTypeLabels[type] }))} /> setDateRange(prev => ({ ...prev, start: e.target.value }))} className="px-3 py-1 border border-[var(--border-secondary)] rounded-md text-sm" />
setDateRange(prev => ({ ...prev, end: e.target.value }))} className="px-3 py-1 border border-[var(--border-secondary)] rounded-md text-sm" />
{/* Summary Statistics */} {summaryStats && (

Ingresos Totales

{currencySymbol}{summaryStats.totalRevenue.toLocaleString('es-ES', { minimumFractionDigits: 2 })}

= 0 ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'}`}> = 0 ? "M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z" : "M14.707 10.293a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 111.414-1.414L9 12.586V5a1 1 0 012 0v7.586l2.293-2.293a1 1 0 011.414 0z" } clipRule="evenodd" /> {Math.abs(summaryStats.growthRate).toFixed(1)}%

Pedidos Totales

{summaryStats.totalOrders.toLocaleString('es-ES')}

Ticket Promedio

{currencySymbol}{summaryStats.avgOrderValue.toLocaleString('es-ES', { minimumFractionDigits: 2 })}

Producto Top

{summaryStats.topProduct}

)} {/* Main Chart */}

{MetricLabels[selectedMetric]}

Tipo: {ChartTypeLabels[chartType]}
{loading ? (
Cargando datos...
) : error ? (
{error}
) : (
{renderChart()}
)}
{/* Comparison Period */} {showComparison && (

Comparación de Períodos

setComparisonPeriod(prev => ({ ...prev, start: e.target.value }))} className="px-3 py-1 border border-[var(--border-secondary)] rounded-md text-sm" />
setComparisonPeriod(prev => ({ ...prev, end: e.target.value }))} className="px-3 py-1 border border-[var(--border-secondary)] rounded-md text-sm" />

Gráfico de comparación se implementaría aquí

)} {/* Export Modal */} {showExportModal && (

Exportar Gráfico

)}
); }; export default SalesChart;