diff --git a/frontend/src/components/domain/analytics/AnalyticsDashboard.tsx.backup b/frontend/src/components/domain/analytics/AnalyticsDashboard.tsx.backup deleted file mode 100644 index 366136b1..00000000 --- a/frontend/src/components/domain/analytics/AnalyticsDashboard.tsx.backup +++ /dev/null @@ -1,617 +0,0 @@ -import React, { useState, useCallback, useEffect } from 'react'; -import { Card, Button, Badge, Select, DatePicker } from '../../ui'; -import { ChartWidget } from './ChartWidget'; -import { ReportsTable } from './ReportsTable'; -import { FilterPanel } from './FilterPanel'; -import { ExportOptions } from './ExportOptions'; -import type { - BakeryMetrics, - AnalyticsReport, - ChartWidget as ChartWidgetType, - FilterPanel as FilterPanelType, - AppliedFilter, - TimeRange, - ExportFormat -} from './types'; - -interface AnalyticsDashboardProps { - className?: string; - initialTimeRange?: TimeRange; - showFilters?: boolean; - showExport?: boolean; - customCharts?: ChartWidgetType[]; - onMetricsLoad?: (metrics: BakeryMetrics) => void; - onExport?: (format: ExportFormat, options: any) => void; -} - -const DEFAULT_TIME_RANGES: { value: TimeRange; label: string }[] = [ - { value: 'today', label: 'Hoy' }, - { value: 'yesterday', label: 'Ayer' }, - { value: 'thisWeek', label: 'Esta semana' }, - { value: 'lastWeek', label: 'Semana pasada' }, - { value: 'thisMonth', label: 'Este mes' }, - { value: 'lastMonth', label: 'Mes pasado' }, - { value: 'thisQuarter', label: 'Este trimestre' }, - { value: 'thisYear', label: 'Este año' }, - { value: 'custom', label: 'Personalizado' }, -]; - -export const AnalyticsDashboard: React.FC = ({ - className = '', - initialTimeRange = 'thisMonth', - showFilters = true, - showExport = true, - customCharts = [], - onMetricsLoad, - onExport, -}) => { - const [selectedTimeRange, setSelectedTimeRange] = useState(initialTimeRange); - const [customDateRange, setCustomDateRange] = useState<{ from: Date; to: Date } | null>(null); - const [bakeryMetrics, setBakeryMetrics] = useState(null); - const [reports, setReports] = useState([]); - const [appliedFilters, setAppliedFilters] = useState([]); - const [loading, setLoading] = useState(false); - const [activeView, setActiveView] = useState<'overview' | 'sales' | 'production' | 'inventory' | 'financial' | 'reports'>('overview'); - const [isExportModalOpen, setIsExportModalOpen] = useState(false); - const [selectedMetricCards, setSelectedMetricCards] = useState([]); - - const loadAnalyticsData = useCallback(async () => { - setLoading(true); - try { - // Mock data - in real implementation, this would fetch from API - const mockMetrics: BakeryMetrics = { - sales: { - total_revenue: 45250.75, - total_orders: 1247, - average_order_value: 36.32, - revenue_growth: 12.5, - order_growth: 8.3, - conversion_rate: 67.8, - top_products: [ - { - product_id: 'prod-1', - product_name: 'Pan de Molde Integral', - category: 'Pan', - quantity_sold: 342, - revenue: 8540.50, - profit_margin: 45.2, - growth_rate: 15.3, - stock_turns: 12.4 - }, - { - product_id: 'prod-2', - product_name: 'Croissant Mantequilla', - category: 'Bollería', - quantity_sold: 289, - revenue: 7225.00, - profit_margin: 52.1, - growth_rate: 22.7, - stock_turns: 8.9 - } - ], - sales_by_channel: [ - { - channel: 'Tienda Física', - revenue: 28500.45, - orders: 789, - customers: 634, - conversion_rate: 72.1, - average_order_value: 36.12, - growth_rate: 8.5 - }, - { - channel: 'Online', - revenue: 16750.30, - orders: 458, - customers: 412, - conversion_rate: 61.3, - average_order_value: 36.58, - growth_rate: 18.9 - } - ], - hourly_sales: [], - seasonal_trends: [] - }, - production: { - total_batches: 156, - success_rate: 94.2, - waste_percentage: 3.8, - production_efficiency: 87.5, - quality_score: 92.1, - capacity_utilization: 76.3, - production_costs: 18750.25, - batch_cycle_time: 4.2, - top_recipes: [ - { - recipe_id: 'rec-1', - recipe_name: 'Pan de Molde Tradicional', - batches_produced: 45, - success_rate: 96.8, - average_yield: 98.2, - cost_per_unit: 1.25, - quality_score: 94.5, - profitability: 65.2 - } - ], - quality_trends: [] - }, - inventory: { - total_stock_value: 12350.80, - turnover_rate: 8.4, - days_of_inventory: 12.5, - stockout_rate: 2.1, - waste_value: 245.30, - carrying_costs: 1850.75, - reorder_efficiency: 89.3, - supplier_performance: [ - { - supplier_id: 'sup-1', - supplier_name: 'Harinas Premium SA', - on_time_delivery: 94.2, - quality_rating: 4.6, - cost_competitiveness: 87.5, - total_orders: 24, - total_value: 8450.75, - reliability_score: 91.8 - } - ], - stock_levels: [], - expiry_alerts: [] - }, - financial: { - gross_profit: 26500.25, - net_profit: 18750.30, - profit_margin: 41.4, - cost_of_goods_sold: 18750.50, - operating_expenses: 7750.95, - break_even_point: 28500.00, - cash_flow: 15250.80, - roi: 23.8, - expense_breakdown: [ - { - category: 'Ingredientes', - amount: 12500.25, - percentage: 66.7, - change_from_previous: 5.2, - budget_variance: -2.1 - }, - { - category: 'Personal', - amount: 4250.25, - percentage: 22.7, - change_from_previous: 8.5, - budget_variance: 3.2 - }, - { - category: 'Suministros', - amount: 2000.00, - percentage: 10.6, - change_from_previous: -1.5, - budget_variance: -0.8 - } - ], - profit_trends: [] - }, - customer: { - total_customers: 1524, - new_customers: 234, - returning_customers: 1290, - customer_retention_rate: 84.6, - customer_lifetime_value: 285.40, - average_purchase_frequency: 2.8, - customer_satisfaction: 4.6, - customer_segments: [ - { - segment_id: 'seg-1', - segment_name: 'Clientes Frecuentes', - customer_count: 456, - revenue_contribution: 68.2, - average_order_value: 42.50, - purchase_frequency: 5.2, - retention_rate: 92.3, - growth_rate: 12.8 - } - ], - loyalty_program_performance: [] - }, - operational: { - staff_productivity: 87.5, - equipment_uptime: 94.2, - energy_consumption: 2450.75, - delivery_performance: 91.8, - food_safety_score: 98.5, - compliance_rate: 96.2, - maintenance_costs: 1250.30, - operational_efficiency: 89.4 - } - }; - - setBakeryMetrics(mockMetrics); - if (onMetricsLoad) { - onMetricsLoad(mockMetrics); - } - - // Mock reports - const mockReports: AnalyticsReport[] = [ - { - id: 'rep-1', - name: 'Reporte de Ventas Mensual', - description: 'Análisis completo de ventas del mes', - type: 'sales', - category: 'Ventas', - status: 'active', - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - tenant_id: 'tenant-1', - config: { - data_source: 'sales_db', - refresh_interval: 60, - cache_duration: 30, - max_records: 10000, - default_time_range: 'thisMonth', - default_filters: {}, - visualization_type: 'bar', - aggregation_rules: [] - }, - metrics: [], - filters: [], - access_permissions: [], - tags: ['ventas', 'mensual'] - } - ]; - - setReports(mockReports); - } catch (error) { - console.error('Error loading analytics data:', error); - } finally { - setLoading(false); - } - }, [selectedTimeRange, customDateRange, appliedFilters, onMetricsLoad]); - - useEffect(() => { - loadAnalyticsData(); - }, [loadAnalyticsData]); - - const handleTimeRangeChange = (newRange: TimeRange) => { - setSelectedTimeRange(newRange); - if (newRange !== 'custom') { - setCustomDateRange(null); - } - }; - - const handleFiltersChange = (filters: AppliedFilter[]) => { - setAppliedFilters(filters); - }; - - const handleExport = (format: ExportFormat, options: any) => { - if (onExport) { - onExport(format, { ...options, view: activeView, metrics: bakeryMetrics }); - } - setIsExportModalOpen(false); - }; - - const renderKPICard = (title: string, value: string | number, subtitle?: string, trend?: number, icon?: string, colorClass: string = 'text-blue-600') => ( - -
-
-

{title}

-

{value}

- {subtitle &&

{subtitle}

} - {trend !== undefined && ( -
- = 0 ? 'text-green-600' : 'text-red-600'}`}> - {trend >= 0 ? '↗' : '↘'} {Math.abs(trend).toFixed(1)}% - -
- )} -
- {icon && {icon}} -
-
- ); - - const renderOverviewDashboard = () => { - if (!bakeryMetrics) return null; - - return ( -
- {/* KPI Cards */} -
- {renderKPICard( - 'Ingresos Totales', - `€${bakeryMetrics.sales.total_revenue.toLocaleString()}`, - undefined, - bakeryMetrics.sales.revenue_growth, - '💰', - 'text-green-600' - )} - {renderKPICard( - 'Pedidos', - bakeryMetrics.sales.total_orders.toLocaleString(), - `Ticket medio: €${bakeryMetrics.sales.average_order_value.toFixed(2)}`, - bakeryMetrics.sales.order_growth, - '📦', - 'text-blue-600' - )} - {renderKPICard( - 'Margen de Beneficio', - `${bakeryMetrics.financial.profit_margin.toFixed(1)}%`, - `Beneficio: €${bakeryMetrics.financial.net_profit.toLocaleString()}`, - undefined, - '📈', - 'text-purple-600' - )} - {renderKPICard( - 'Tasa de Éxito Producción', - `${bakeryMetrics.production.success_rate.toFixed(1)}%`, - `${bakeryMetrics.production.total_batches} lotes`, - undefined, - '🏭', - 'text-orange-600' - )} -
- - {/* Charts Grid */} -
- -

Ventas por Canal

-
- {bakeryMetrics.sales.sales_by_channel.map((channel, index) => ( -
-
-

{channel.channel}

-

- {channel.orders} pedidos • {channel.customers} clientes -

-
-
-

- €{channel.revenue.toLocaleString()} -

-

- Conv. {channel.conversion_rate.toFixed(1)}% -

-
-
- ))} -
-
- - -

Productos Top

-
- {bakeryMetrics.sales.top_products.map((product, index) => ( -
-
-

{product.product_name}

-

- {product.category} • {product.quantity_sold} vendidos -

-
-
-

- €{product.revenue.toLocaleString()} -

-

- Margen {product.profit_margin.toFixed(1)}% -

-
-
- ))} -
-
-
- - {/* Operational Metrics */} - -

Métricas Operacionales

-
-
-

- {bakeryMetrics.operational.staff_productivity.toFixed(1)}% -

-

Productividad Personal

-
-
-

- {bakeryMetrics.operational.equipment_uptime.toFixed(1)}% -

-

Tiempo Activo Equipos

-
-
-

- {bakeryMetrics.production.waste_percentage.toFixed(1)}% -

-

Desperdicio

-
-
-

- {bakeryMetrics.operational.food_safety_score.toFixed(1)}% -

-

Seguridad Alimentaria

-
-
-

- {bakeryMetrics.inventory.stockout_rate.toFixed(1)}% -

-

Roturas Stock

-
-
-

- {bakeryMetrics.customer.customer_retention_rate.toFixed(1)}% -

-

Retención Clientes

-
-
-

- €{bakeryMetrics.customer.customer_lifetime_value.toFixed(0)} -

-

Valor Cliente

-
-
-

- {bakeryMetrics.inventory.turnover_rate.toFixed(1)} -

-

Rotación Inventario

-
-
-
-
- ); - }; - - const renderViewContent = () => { - switch (activeView) { - case 'overview': - return renderOverviewDashboard(); - case 'reports': - return ( - - console.log('View report:', report)} - onEditReport={(report) => console.log('Edit report:', report)} - onDeleteReport={(id) => console.log('Delete report:', id)} - onExportReport={(report, format) => console.log('Export report:', report, format)} - bulkActions={true} - sortable={true} - pagination={{ - current: 1, - pageSize: 10, - total: reports.length, - onChange: (page, pageSize) => console.log('Page change:', page, pageSize) - }} - /> - - ); - default: - return ( - -

Vista en desarrollo: {activeView}

-

- Esta vista estará disponible próximamente con métricas detalladas. -

-
- ); - } - }; - - return ( -
- {/* Header */} -
-
-

Analytics Dashboard

-

Análisis integral de rendimiento de la panadería

-
- -
- - - {selectedTimeRange === 'custom' && ( -
- setCustomDateRange(prev => ({ - ...prev, - from: date || new Date() - } as { from: Date; to: Date }))} - placeholder="Fecha inicio" - /> - setCustomDateRange(prev => ({ - ...prev, - to: date || new Date() - } as { from: Date; to: Date }))} - placeholder="Fecha fin" - /> -
- )} - - {showExport && ( - - )} - - -
-
- - {/* Navigation Tabs */} -
- -
- - {/* Content */} - {loading ? ( -
-
-
- ) : ( - renderViewContent() - )} - - {/* Export Modal */} - {isExportModalOpen && ( - setIsExportModalOpen(false)} - /> - )} -
- ); -}; - -export default AnalyticsDashboard; \ No newline at end of file diff --git a/frontend/src/components/domain/analytics/ChartWidget.tsx.backup b/frontend/src/components/domain/analytics/ChartWidget.tsx.backup deleted file mode 100644 index 7d342e1f..00000000 --- a/frontend/src/components/domain/analytics/ChartWidget.tsx.backup +++ /dev/null @@ -1,660 +0,0 @@ -import React, { useState, useRef, useEffect, useMemo } from 'react'; -import { Card, Button, Badge, Select, Modal } from '../../ui'; -import type { - ChartWidget as ChartWidgetType, - ChartSeries, - ChartConfig, - ChartFilter, - ExportFormat, - ChartType -} from './types'; - -interface ChartWidgetProps { - widget: ChartWidgetType; - data?: ChartSeries[]; - loading?: boolean; - error?: string; - onConfigChange?: (config: Partial) => void; - onFiltersChange?: (filters: ChartFilter[]) => void; - onRefresh?: () => void; - onExport?: (format: ExportFormat) => void; - onFullscreen?: () => void; - interactive?: boolean; - showControls?: boolean; - showTitle?: boolean; - showSubtitle?: boolean; - className?: string; -} - -interface ChartPoint { - x: number; - y: number; - label: string; -} - -const CHART_TYPE_LABELS: Record = { - line: 'Líneas', - bar: 'Barras', - pie: 'Circular', - area: 'Área', - scatter: 'Dispersión', - doughnut: 'Dona', - radar: 'Radar', - mixed: 'Mixto', -}; - -const EXPORT_FORMATS: ExportFormat[] = ['png', 'svg', 'pdf', 'csv', 'excel']; - -export const ChartWidget: React.FC = ({ - widget, - data = [], - loading = false, - error, - onConfigChange, - onFiltersChange, - onRefresh, - onExport, - onFullscreen, - interactive = true, - showControls = true, - showTitle = true, - showSubtitle = true, - className = '', -}) => { - const canvasRef = useRef(null); - const [isConfigModalOpen, setIsConfigModalOpen] = useState(false); - const [isFullscreen, setIsFullscreen] = useState(false); - const [hoveredPoint, setHoveredPoint] = useState<{ seriesIndex: number; pointIndex: number } | null>(null); - const [selectedExportFormat, setSelectedExportFormat] = useState('png'); - - const chartDimensions = useMemo(() => ({ - width: widget.dimensions?.width || '100%', - height: widget.dimensions?.height || 400, - aspectRatio: widget.dimensions?.aspect_ratio || '16:9', - }), [widget.dimensions]); - - // Simple chart rendering logic - useEffect(() => { - if (!canvasRef.current || !data.length || loading) return; - - const canvas = canvasRef.current; - const ctx = canvas.getContext('2d'); - if (!ctx) return; - - // Set canvas size - const rect = canvas.getBoundingClientRect(); - canvas.width = rect.width; - canvas.height = rect.height; - - // Clear canvas - ctx.clearRect(0, 0, canvas.width, canvas.height); - - // Draw based on chart type - switch (widget.type) { - case 'bar': - drawBarChart(ctx, canvas, data); - break; - case 'line': - drawLineChart(ctx, canvas, data); - break; - case 'pie': - drawPieChart(ctx, canvas, data); - break; - case 'area': - drawAreaChart(ctx, canvas, data); - break; - default: - drawBarChart(ctx, canvas, data); - } - }, [data, widget.type, loading]); - - const drawBarChart = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, chartData: ChartSeries[]) => { - if (!chartData.length) return; - - const padding = 60; - const chartWidth = canvas.width - 2 * padding; - const chartHeight = canvas.height - 2 * padding; - - // Get all data points - const allPoints = chartData.flatMap(series => series.data); - const maxY = Math.max(...allPoints.map(point => point.y)); - const minY = Math.min(...allPoints.map(point => point.y)); - - const firstSeries = chartData[0]; - const barWidth = chartWidth / firstSeries.data.length / chartData.length; - const barSpacing = barWidth * 0.1; - - // Draw grid - ctx.strokeStyle = '#e5e7eb'; - ctx.lineWidth = 1; - for (let i = 0; i <= 5; i++) { - const y = padding + (chartHeight * i) / 5; - ctx.beginPath(); - ctx.moveTo(padding, y); - ctx.lineTo(padding + chartWidth, y); - ctx.stroke(); - } - - // Draw bars - chartData.forEach((series, seriesIndex) => { - const color = series.color || widget.config.color_scheme[seriesIndex % widget.config.color_scheme.length]; - ctx.fillStyle = color; - - series.data.forEach((point, pointIndex) => { - const x = padding + (pointIndex * chartWidth) / firstSeries.data.length + (seriesIndex * barWidth); - const barHeight = ((point.y - minY) / (maxY - minY)) * chartHeight; - const y = padding + chartHeight - barHeight; - - ctx.fillRect(x + barSpacing, y, barWidth - barSpacing, barHeight); - - // Add value labels if enabled - if (widget.config.show_tooltips) { - ctx.fillStyle = '#374151'; - ctx.font = '12px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText(point.y.toString(), x + barWidth / 2, y - 5); - ctx.fillStyle = color; - } - }); - }); - - // Draw axes - ctx.strokeStyle = '#374151'; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.moveTo(padding, padding); - ctx.lineTo(padding, padding + chartHeight); - ctx.lineTo(padding + chartWidth, padding + chartHeight); - ctx.stroke(); - - // Draw labels - ctx.fillStyle = '#6b7280'; - ctx.font = '12px sans-serif'; - ctx.textAlign = 'center'; - - // X-axis labels - firstSeries.data.forEach((point, index) => { - const x = padding + (index + 0.5) * (chartWidth / firstSeries.data.length); - ctx.fillText(point.x.toString(), x, padding + chartHeight + 20); - }); - - // Y-axis labels - ctx.textAlign = 'right'; - for (let i = 0; i <= 5; i++) { - const y = padding + (chartHeight * i) / 5; - const value = maxY - ((maxY - minY) * i) / 5; - ctx.fillText(value.toFixed(0), padding - 10, y + 4); - } - }; - - const drawLineChart = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, chartData: ChartSeries[]) => { - if (!chartData.length) return; - - const padding = 60; - const chartWidth = canvas.width - 2 * padding; - const chartHeight = canvas.height - 2 * padding; - - const allPoints = chartData.flatMap(series => series.data); - const maxY = Math.max(...allPoints.map(point => point.y)); - const minY = Math.min(...allPoints.map(point => point.y)); - - // Draw grid - ctx.strokeStyle = '#e5e7eb'; - ctx.lineWidth = 1; - for (let i = 0; i <= 5; i++) { - const y = padding + (chartHeight * i) / 5; - ctx.beginPath(); - ctx.moveTo(padding, y); - ctx.lineTo(padding + chartWidth, y); - ctx.stroke(); - } - - // Draw lines - chartData.forEach((series, seriesIndex) => { - if (!series.visible) return; - - const color = series.color || widget.config.color_scheme[seriesIndex % widget.config.color_scheme.length]; - ctx.strokeStyle = color; - ctx.lineWidth = 2; - ctx.beginPath(); - - series.data.forEach((point, pointIndex) => { - const x = padding + (pointIndex * chartWidth) / (series.data.length - 1); - const y = padding + chartHeight - ((point.y - minY) / (maxY - minY)) * chartHeight; - - if (pointIndex === 0) { - ctx.moveTo(x, y); - } else { - ctx.lineTo(x, y); - } - }); - - ctx.stroke(); - - // Draw points - ctx.fillStyle = color; - series.data.forEach((point, pointIndex) => { - const x = padding + (pointIndex * chartWidth) / (series.data.length - 1); - const y = padding + chartHeight - ((point.y - minY) / (maxY - minY)) * chartHeight; - - ctx.beginPath(); - ctx.arc(x, y, 4, 0, 2 * Math.PI); - ctx.fill(); - }); - }); - - // Draw axes - ctx.strokeStyle = '#374151'; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.moveTo(padding, padding); - ctx.lineTo(padding, padding + chartHeight); - ctx.lineTo(padding + chartWidth, padding + chartHeight); - ctx.stroke(); - }; - - const drawPieChart = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, chartData: ChartSeries[]) => { - if (!chartData.length) return; - - const centerX = canvas.width / 2; - const centerY = canvas.height / 2; - const radius = Math.min(canvas.width, canvas.height) / 3; - - const firstSeries = chartData[0]; - const total = firstSeries.data.reduce((sum, point) => sum + point.y, 0); - - let startAngle = -Math.PI / 2; - - firstSeries.data.forEach((point, index) => { - const sliceAngle = (point.y / total) * 2 * Math.PI; - const color = widget.config.color_scheme[index % widget.config.color_scheme.length]; - - ctx.fillStyle = color; - ctx.beginPath(); - ctx.moveTo(centerX, centerY); - ctx.arc(centerX, centerY, radius, startAngle, startAngle + sliceAngle); - ctx.closePath(); - ctx.fill(); - - // Draw slice border - ctx.strokeStyle = '#ffffff'; - ctx.lineWidth = 2; - ctx.stroke(); - - // Draw labels - const labelAngle = startAngle + sliceAngle / 2; - const labelX = centerX + Math.cos(labelAngle) * (radius + 30); - const labelY = centerY + Math.sin(labelAngle) * (radius + 30); - - ctx.fillStyle = '#374151'; - ctx.font = '12px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText(point.x.toString(), labelX, labelY); - - const percentage = ((point.y / total) * 100).toFixed(1); - ctx.fillText(`${percentage}%`, labelX, labelY + 15); - - startAngle += sliceAngle; - }); - }; - - const drawAreaChart = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, chartData: ChartSeries[]) => { - if (!chartData.length) return; - - const padding = 60; - const chartWidth = canvas.width - 2 * padding; - const chartHeight = canvas.height - 2 * padding; - - const allPoints = chartData.flatMap(series => series.data); - const maxY = Math.max(...allPoints.map(point => point.y)); - const minY = Math.min(...allPoints.map(point => point.y)); - - chartData.forEach((series, seriesIndex) => { - if (!series.visible) return; - - const color = series.color || widget.config.color_scheme[seriesIndex % widget.config.color_scheme.length]; - - // Create gradient - const gradient = ctx.createLinearGradient(0, padding, 0, padding + chartHeight); - gradient.addColorStop(0, color + '80'); // Semi-transparent - gradient.addColorStop(1, color + '10'); // Very transparent - - ctx.fillStyle = gradient; - ctx.beginPath(); - - // Start from bottom-left - const firstX = padding; - const firstY = padding + chartHeight; - ctx.moveTo(firstX, firstY); - - // Draw the area path - series.data.forEach((point, pointIndex) => { - const x = padding + (pointIndex * chartWidth) / (series.data.length - 1); - const y = padding + chartHeight - ((point.y - minY) / (maxY - minY)) * chartHeight; - ctx.lineTo(x, y); - }); - - // Close the path at bottom-right - const lastX = padding + chartWidth; - const lastY = padding + chartHeight; - ctx.lineTo(lastX, lastY); - ctx.closePath(); - ctx.fill(); - - // Draw the line on top - ctx.strokeStyle = color; - ctx.lineWidth = 2; - ctx.beginPath(); - series.data.forEach((point, pointIndex) => { - const x = padding + (pointIndex * chartWidth) / (series.data.length - 1); - const y = padding + chartHeight - ((point.y - minY) / (maxY - minY)) * chartHeight; - - if (pointIndex === 0) { - ctx.moveTo(x, y); - } else { - ctx.lineTo(x, y); - } - }); - ctx.stroke(); - }); - }; - - const handleExport = () => { - if (onExport) { - onExport(selectedExportFormat); - } - }; - - const handleFullscreen = () => { - setIsFullscreen(!isFullscreen); - if (onFullscreen) { - onFullscreen(); - } - }; - - const renderLegend = () => { - if (!widget.config.show_legend || !data.length) return null; - - return ( -
- {data.map((series, index) => ( -
-
- {series.name} -
- ))} -
- ); - }; - - const renderControls = () => { - if (!showControls || !interactive) return null; - - return ( -
- - - - - - - - - {onRefresh && ( - - )} -
- ); - }; - - if (error) { - return ( - -
-

Error al cargar el gráfico

-

{error}

- {onRefresh && ( - - )} -
-
- ); - } - - return ( - <> - - {/* Header */} -
-
- {showTitle && ( -

- {widget.title} -

- )} - {showSubtitle && widget.subtitle && ( -

- {widget.subtitle} -

- )} -
- - {renderControls()} -
- - {/* Chart Area */} -
- {loading ? ( -
-
-
- ) : data.length === 0 ? ( -
-
-

No hay datos disponibles

-

- Ajusta los filtros o verifica la conexión de datos -

-
-
- ) : ( -
- - {renderLegend()} -
- )} -
- - {/* Last Updated */} - {widget.last_updated && ( -
- Actualizado: {new Date(widget.last_updated).toLocaleTimeString()} -
- )} -
- - {/* Configuration Modal */} - setIsConfigModalOpen(false)} - title="Configuración del Gráfico" - > -
-
- - -
- -
-
- -
- -
- -
- -
- -
- -
- -
-
- -
- - -
- -
- - -
-
-
- - ); -}; - -export default ChartWidget; \ No newline at end of file diff --git a/frontend/src/components/domain/analytics/ExportOptions.tsx.backup b/frontend/src/components/domain/analytics/ExportOptions.tsx.backup deleted file mode 100644 index f4fe8636..00000000 --- a/frontend/src/components/domain/analytics/ExportOptions.tsx.backup +++ /dev/null @@ -1,592 +0,0 @@ -import React, { useState } from 'react'; -import { Card, Button, Badge, Input, Select } from '../../ui'; -import type { - ExportOptionsProps, - ExportOptions, - ExportFormat, - ExportTemplate, - ReportSchedule -} from './types'; - -const FORMAT_LABELS: Record = { - pdf: 'PDF', - excel: 'Excel', - csv: 'CSV', - png: 'PNG', - svg: 'SVG', - json: 'JSON', -}; - -const FORMAT_DESCRIPTIONS: Record = { - pdf: 'Documento PDF con formato profesional', - excel: 'Hoja de cálculo de Microsoft Excel', - csv: 'Archivo de valores separados por comas', - png: 'Imagen PNG de alta calidad', - svg: 'Imagen vectorial escalable', - json: 'Datos estructurados en formato JSON', -}; - -const FORMAT_ICONS: Record = { - pdf: '📄', - excel: '📊', - csv: '📋', - png: '🖼️', - svg: '🎨', - json: '⚙️', -}; - -const FREQUENCY_OPTIONS = [ - { value: 'daily', label: 'Diario' }, - { value: 'weekly', label: 'Semanal' }, - { value: 'monthly', label: 'Mensual' }, - { value: 'quarterly', label: 'Trimestral' }, - { value: 'yearly', label: 'Anual' }, -]; - -const TIME_OPTIONS = [ - { value: '06:00', label: '6:00 AM' }, - { value: '09:00', label: '9:00 AM' }, - { value: '12:00', label: '12:00 PM' }, - { value: '15:00', label: '3:00 PM' }, - { value: '18:00', label: '6:00 PM' }, - { value: '21:00', label: '9:00 PM' }, -]; - -const DAYS_OF_WEEK = [ - { value: 1, label: 'Lunes' }, - { value: 2, label: 'Martes' }, - { value: 3, label: 'Miércoles' }, - { value: 4, label: 'Jueves' }, - { value: 5, label: 'Viernes' }, - { value: 6, label: 'Sábado' }, - { value: 0, label: 'Domingo' }, -]; - -interface ExportOptionsComponentProps extends ExportOptionsProps { - onClose?: () => void; -} - -export const ExportOptions: React.FC = ({ - type, - title, - description, - availableFormats = ['pdf', 'excel', 'csv', 'png'], - templates = [], - onExport, - onSchedule, - loading = false, - disabled = false, - showScheduling = true, - showTemplates = true, - showAdvanced = true, - defaultOptions, - className = '', - onClose, -}) => { - const [selectedFormat, setSelectedFormat] = useState( - defaultOptions?.format || availableFormats[0] - ); - const [selectedTemplate, setSelectedTemplate] = useState( - templates.find(t => t.is_default)?.id || '' - ); - const [exportOptions, setExportOptions] = useState>({ - include_headers: true, - include_filters: true, - include_summary: true, - date_format: 'DD/MM/YYYY', - number_format: '#,##0.00', - currency_format: '€#,##0.00', - locale: 'es-ES', - timezone: 'Europe/Madrid', - page_size: 'A4', - orientation: 'portrait', - password_protected: false, - ...defaultOptions, - }); - - const [scheduleOptions, setScheduleOptions] = useState>({ - enabled: false, - frequency: 'weekly', - time: '09:00', - days_of_week: [1], // Monday - recipients: [], - format: selectedFormat, - include_attachments: true, - }); - - const [recipientsInput, setRecipientsInput] = useState(''); - const [activeTab, setActiveTab] = useState<'export' | 'schedule'>('export'); - const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); - - const handleExportOptionChange = (key: keyof ExportOptions, value: any) => { - setExportOptions(prev => ({ ...prev, [key]: value })); - }; - - const handleScheduleOptionChange = (key: keyof ReportSchedule, value: any) => { - setScheduleOptions(prev => ({ ...prev, [key]: value })); - }; - - const handleExport = () => { - if (onExport) { - const fullOptions: ExportOptions = { - format: selectedFormat, - template: selectedTemplate || undefined, - include_headers: true, - include_filters: true, - include_summary: true, - date_format: 'DD/MM/YYYY', - number_format: '#,##0.00', - currency_format: '€#,##0.00', - locale: 'es-ES', - timezone: 'Europe/Madrid', - page_size: 'A4', - orientation: 'portrait', - ...exportOptions, - // format: selectedFormat, // Ensure format matches selection - handled by exportOptions - }; - onExport(fullOptions); - } - }; - - const handleSchedule = () => { - if (onSchedule) { - const recipients = recipientsInput - .split(',') - .map(email => email.trim()) - .filter(email => email.length > 0); - - const fullSchedule: ReportSchedule = { - enabled: true, - frequency: 'weekly', - time: '09:00', - recipients: recipients, - format: selectedFormat, - include_attachments: true, - ...scheduleOptions, - }; - onSchedule(fullSchedule); - } - }; - - const renderFormatSelector = () => ( -
-

Formato de exportación

-
- {availableFormats.map(format => ( - - ))} -
-
- ); - - const renderTemplateSelector = () => { - if (!showTemplates || templates.length === 0) return null; - - return ( -
-

Plantilla

- - - {selectedTemplate && ( -
- {templates.find(t => t.id === selectedTemplate)?.description} -
- )} -
- ); - }; - - const renderBasicOptions = () => ( -
-

Opciones básicas

- -
- - - - - -
-
- ); - - const renderAdvancedOptions = () => { - if (!showAdvanced || !showAdvancedOptions) return null; - - return ( -
-

Opciones avanzadas

- -
-
- - -
- -
- - -
- - {(['pdf'] as ExportFormat[]).includes(selectedFormat) && ( - <> -
- - -
- -
- - -
- - )} -
- -
- - - {exportOptions.password_protected && ( -
- handleExportOptionChange('password', e.target.value)} - disabled={disabled || loading} - /> -
- )} -
-
- ); - }; - - const renderScheduleTab = () => ( -
-
- -
- - {scheduleOptions.enabled && ( -
-
-
- - -
- -
- - -
-
- - {scheduleOptions.frequency === 'weekly' && ( -
- -
- {DAYS_OF_WEEK.map(day => ( - - ))} -
-
- )} - -
- - setRecipientsInput(e.target.value)} - placeholder="ejemplo@empresa.com, otro@empresa.com" - disabled={disabled || loading} - /> -
- -
- -
-
- )} -
- ); - - return ( -
- - {/* Header */} -
-
-

{title}

- {description && ( -

{description}

- )} -
- - {onClose && ( - - )} -
- - {/* Tabs */} -
- -
- - {/* Content */} -
- {activeTab === 'export' ? ( - <> - {renderFormatSelector()} - {renderTemplateSelector()} - {renderBasicOptions()} - - {showAdvanced && ( -
- -
- )} - - {renderAdvancedOptions()} - - ) : ( - renderScheduleTab() - )} -
- - {/* Footer */} -
- {onClose && ( - - )} - - {activeTab === 'export' ? ( - - ) : ( - - )} -
-
-
- ); -}; - -export default ExportOptions; \ No newline at end of file diff --git a/frontend/src/components/domain/analytics/FilterPanel.tsx.backup b/frontend/src/components/domain/analytics/FilterPanel.tsx.backup deleted file mode 100644 index bb29892e..00000000 --- a/frontend/src/components/domain/analytics/FilterPanel.tsx.backup +++ /dev/null @@ -1,632 +0,0 @@ -import React, { useState, useCallback, useMemo } from 'react'; -import { Card, Button, Badge, Input, Select, DatePicker } from '../../ui'; -import type { - FilterPanelProps, - AnalyticsFilter, - FilterOption, - AppliedFilter, - FilterPreset, - FilterType -} from './types'; - -const FILTER_TYPE_ICONS: Record = { - date: '📅', - select: '📋', - multiselect: '☑️', - range: '↔️', - text: '🔍', - number: '#️⃣', - boolean: '✓', -}; - -export const FilterPanel: React.FC = ({ - panel, - appliedFilters = [], - onFiltersChange, - onPresetSelect, - onPresetSave, - onPresetDelete, - onReset, - loading = false, - disabled = false, - compact = false, - showPresets = true, - showReset = true, - className = '', -}) => { - const [isCollapsed, setIsCollapsed] = useState(panel.default_collapsed); - const [localFilters, setLocalFilters] = useState>({}); - const [isPresetModalOpen, setIsPresetModalOpen] = useState(false); - const [newPresetName, setNewPresetName] = useState(''); - const [newPresetDescription, setNewPresetDescription] = useState(''); - const [validationErrors, setValidationErrors] = useState>({}); - - const appliedFiltersMap = useMemo(() => { - const map: Record = {}; - appliedFilters.forEach(filter => { - map[filter.filter_id] = filter; - }); - return map; - }, [appliedFilters]); - - const getFilterValue = useCallback((filter: AnalyticsFilter) => { - const applied = appliedFiltersMap[filter.id]; - if (applied) { - return applied.value; - } - - const local = localFilters[filter.id]; - if (local !== undefined) { - return local; - } - - return filter.default_value; - }, [appliedFiltersMap, localFilters]); - - const updateFilterValue = useCallback((filterId: string, value: any) => { - setLocalFilters(prev => ({ ...prev, [filterId]: value })); - - // Clear validation error for this filter - setValidationErrors(prev => { - const newErrors = { ...prev }; - delete newErrors[filterId]; - return newErrors; - }); - }, []); - - const validateFilter = useCallback((filter: AnalyticsFilter, value: any): string | null => { - if (!filter.validation_rules) return null; - - for (const rule of filter.validation_rules) { - switch (rule.rule) { - case 'required': - if (!value || (Array.isArray(value) && value.length === 0)) { - return rule.message; - } - break; - case 'min': - if (typeof value === 'number' && value < rule.value) { - return rule.message; - } - if (typeof value === 'string' && value.length < rule.value) { - return rule.message; - } - break; - case 'max': - if (typeof value === 'number' && value > rule.value) { - return rule.message; - } - if (typeof value === 'string' && value.length > rule.value) { - return rule.message; - } - break; - case 'pattern': - if (typeof value === 'string' && !new RegExp(rule.value).test(value)) { - return rule.message; - } - break; - } - } - - return null; - }, []); - - const applyFilters = useCallback(() => { - const errors: Record = {}; - const newAppliedFilters: AppliedFilter[] = []; - - panel.filters.forEach(filter => { - const value = getFilterValue(filter); - - // Validate the filter - const validationError = validateFilter(filter, value); - if (validationError) { - errors[filter.id] = validationError; - return; - } - - // Skip if no value and not required - if ((value === undefined || value === null || value === '') && !filter.required) { - return; - } - - // Create applied filter - const appliedFilter: AppliedFilter = { - filter_id: filter.id, - field: filter.field, - operator: getOperatorForFilterType(filter.type), - value, - display_value: formatDisplayValue(filter, value), - }; - - newAppliedFilters.push(appliedFilter); - }); - - setValidationErrors(errors); - - if (Object.keys(errors).length === 0 && onFiltersChange) { - onFiltersChange(newAppliedFilters); - } - }, [panel.filters, getFilterValue, validateFilter, onFiltersChange]); - - const resetFilters = useCallback(() => { - setLocalFilters({}); - setValidationErrors({}); - if (onReset) { - onReset(); - } - }, [onReset]); - - const getOperatorForFilterType = (type: FilterType): string => { - switch (type) { - case 'select': - case 'boolean': - return 'eq'; - case 'multiselect': - return 'in'; - case 'range': - return 'between'; - case 'text': - return 'like'; - case 'number': - case 'date': - return 'eq'; - default: - return 'eq'; - } - }; - - const formatDisplayValue = (filter: AnalyticsFilter, value: any): string => { - if (value === undefined || value === null) return ''; - - switch (filter.type) { - case 'select': - case 'multiselect': - if (filter.options) { - const options = Array.isArray(value) ? value : [value]; - return options - .map(v => filter.options?.find(opt => opt.value === v)?.label || v) - .join(', '); - } - return Array.isArray(value) ? value.join(', ') : String(value); - case 'boolean': - return value ? 'Sí' : 'No'; - case 'date': - return new Date(value).toLocaleDateString('es-ES'); - case 'range': - return Array.isArray(value) ? `${value[0]} - ${value[1]}` : String(value); - default: - return String(value); - } - }; - - const handlePresetSelect = (presetId: string) => { - const preset = panel.presets.find(p => p.id === presetId); - if (preset) { - setLocalFilters(preset.values); - if (onPresetSelect) { - onPresetSelect(presetId); - } - // Auto-apply filters when preset is selected - setTimeout(applyFilters, 0); - } - }; - - const handlePresetSave = () => { - if (!newPresetName.trim()) return; - - const currentValues: Record = {}; - panel.filters.forEach(filter => { - const value = getFilterValue(filter); - if (value !== undefined && value !== null && value !== '') { - currentValues[filter.id] = value; - } - }); - - const newPreset: Omit = { - name: newPresetName.trim(), - description: newPresetDescription.trim() || undefined, - is_default: false, - is_public: false, - values: currentValues, - }; - - if (onPresetSave) { - onPresetSave(newPreset); - } - - setIsPresetModalOpen(false); - setNewPresetName(''); - setNewPresetDescription(''); - }; - - const renderFilter = (filter: AnalyticsFilter) => { - const value = getFilterValue(filter); - const error = validationErrors[filter.id]; - const isDisabled = disabled || loading; - - const commonProps = { - disabled: isDisabled, - className: error ? 'border-red-300 focus:border-red-500' : '', - }; - - let filterInput: React.ReactNode; - - switch (filter.type) { - case 'text': - filterInput = ( - updateFilterValue(filter.id, e.target.value)} - {...commonProps} - /> - ); - break; - - case 'number': - filterInput = ( - updateFilterValue(filter.id, parseFloat(e.target.value) || null)} - {...commonProps} - /> - ); - break; - - case 'select': - filterInput = ( - - ); - break; - - case 'multiselect': - const selectedValues = Array.isArray(value) ? value : []; - filterInput = ( -
-
- {filter.options?.map((option) => ( - - ))} -
- {selectedValues.length > 0 && ( -
- {selectedValues.map((val) => { - const option = filter.options?.find(opt => opt.value === val); - return ( - - {option?.label || val} - - - ); - })} -
- )} -
- ); - break; - - case 'boolean': - filterInput = ( - - ); - break; - - case 'date': - filterInput = ( - updateFilterValue(filter.id, date?.toISOString())} - placeholder={filter.placeholder} - disabled={isDisabled} - /> - ); - break; - - case 'range': - const rangeValue = Array.isArray(value) ? value : [null, null]; - filterInput = ( -
- { - const newRange = [parseFloat(e.target.value) || null, rangeValue[1]]; - updateFilterValue(filter.id, newRange); - }} - {...commonProps} - className="flex-1" - /> - - - { - const newRange = [rangeValue[0], parseFloat(e.target.value) || null]; - updateFilterValue(filter.id, newRange); - }} - {...commonProps} - className="flex-1" - /> -
- ); - break; - - default: - filterInput = ( -
- Tipo de filtro no soportado: {filter.type} -
- ); - } - - return ( -
-
- - - {filter.help_text && ( - - ℹ️ - - )} -
- - {filterInput} - - {error && ( -

{error}

- )} -
- ); - }; - - const renderPresets = () => { - if (!showPresets || panel.presets.length === 0) return null; - - return ( -
-
-

Filtros guardados

- -
- -
- {panel.presets.map((preset) => ( -
- - - {onPresetDelete && !preset.is_public && ( - - )} -
- ))} -
-
- ); - }; - - if (panel.collapsible && isCollapsed) { - return ( - - - - ); - } - - const gridClass = panel.layout === 'grid' ? 'grid grid-cols-1 md:grid-cols-2 gap-4' : - panel.layout === 'horizontal' ? 'grid grid-cols-1 lg:grid-cols-3 gap-4' : - 'space-y-4'; - - return ( - <> - -
- {/* Header */} -
-

{panel.title}

-
- {loading && ( -
- )} - - {panel.collapsible && ( - - )} -
-
- - {/* Presets */} - {renderPresets()} - - {/* Filters */} -
- {panel.filters.map(filter => renderFilter(filter))} -
- - {/* Actions */} -
-
- {appliedFilters.length} filtro{appliedFilters.length !== 1 ? 's' : ''} aplicado{appliedFilters.length !== 1 ? 's' : ''} -
- -
- {showReset && appliedFilters.length > 0 && ( - - )} - - -
-
-
-
- - {/* Save Preset Modal */} - {isPresetModalOpen && ( -
- -

Guardar filtro

- -
-
- - setNewPresetName(e.target.value)} - placeholder="Ej: Ventas del último mes" - className="w-full" - /> -
- -
- - setNewPresetDescription(e.target.value)} - placeholder="Descripción del filtro" - className="w-full" - /> -
- -
- - -
-
-
-
- )} - - ); -}; - -export default FilterPanel; \ No newline at end of file diff --git a/frontend/src/components/domain/analytics/ReportsTable.tsx.backup b/frontend/src/components/domain/analytics/ReportsTable.tsx.backup deleted file mode 100644 index 04c96917..00000000 --- a/frontend/src/components/domain/analytics/ReportsTable.tsx.backup +++ /dev/null @@ -1,651 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import { Card, Button, Badge, Input, Select, Modal } from '../../ui'; -import { Table } from '../../ui'; -import type { - AnalyticsReport, - ReportsTableProps, - ExportFormat, - ReportType, - CustomAction -} from './types'; - -const REPORT_TYPE_LABELS: Record = { - sales: 'Ventas', - production: 'Producción', - inventory: 'Inventario', - financial: 'Financiero', - customer: 'Clientes', - performance: 'Rendimiento', -}; - -const STATUS_COLORS = { - active: 'bg-green-100 text-green-800 border-green-200', - inactive: 'bg-yellow-100 text-yellow-800 border-yellow-200', - archived: 'bg-gray-100 text-gray-800 border-gray-200', -}; - -const STATUS_LABELS = { - active: 'Activo', - inactive: 'Inactivo', - archived: 'Archivado', -}; - -export const ReportsTable: React.FC = ({ - reports = [], - loading = false, - error, - selectedReports = [], - onSelectionChange, - onReportClick, - onEditReport, - onDeleteReport, - onScheduleReport, - onShareReport, - onExportReport, - filters = [], - onFiltersChange, - sortable = true, - pagination, - bulkActions = false, - customActions = [], - tableConfig, -}) => { - const [searchTerm, setSearchTerm] = useState(''); - const [typeFilter, setTypeFilter] = useState('all'); - const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive' | 'archived'>('all'); - const [sortField, setSortField] = useState('updated_at'); - const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [reportToDelete, setReportToDelete] = useState(null); - const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false); - const [reportToSchedule, setReportToSchedule] = useState(null); - const [isShareModalOpen, setIsShareModalOpen] = useState(false); - const [reportToShare, setReportToShare] = useState(null); - - const filteredAndSortedReports = useMemo(() => { - let filtered = reports.filter(report => { - const matchesSearch = !searchTerm || - report.name.toLowerCase().includes(searchTerm.toLowerCase()) || - report.description?.toLowerCase().includes(searchTerm.toLowerCase()) || - report.category.toLowerCase().includes(searchTerm.toLowerCase()); - - const matchesType = typeFilter === 'all' || report.type === typeFilter; - const matchesStatus = statusFilter === 'all' || report.status === statusFilter; - - return matchesSearch && matchesType && matchesStatus; - }); - - if (sortable && sortField) { - filtered.sort((a, b) => { - const aVal = a[sortField]; - const bVal = b[sortField]; - - if (aVal === null || aVal === undefined) return 1; - if (bVal === null || bVal === undefined) return -1; - - let comparison = 0; - if (typeof aVal === 'string' && typeof bVal === 'string') { - comparison = aVal.localeCompare(bVal); - } else if (typeof aVal === 'number' && typeof bVal === 'number') { - comparison = aVal - bVal; - } else { - comparison = String(aVal).localeCompare(String(bVal)); - } - - return sortDirection === 'asc' ? comparison : -comparison; - }); - } - - return filtered; - }, [reports, searchTerm, typeFilter, statusFilter, sortField, sortDirection, sortable]); - - const handleSort = (field: keyof AnalyticsReport) => { - if (sortField === field) { - setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); - } else { - setSortField(field); - setSortDirection('asc'); - } - }; - - const handleSelectAll = (checked: boolean) => { - if (onSelectionChange) { - onSelectionChange(checked ? filteredAndSortedReports.map(r => r.id) : []); - } - }; - - const handleSelectReport = (reportId: string, checked: boolean) => { - if (onSelectionChange) { - if (checked) { - onSelectionChange([...selectedReports, reportId]); - } else { - onSelectionChange(selectedReports.filter(id => id !== reportId)); - } - } - }; - - const handleDeleteConfirm = () => { - if (reportToDelete && onDeleteReport) { - onDeleteReport(reportToDelete); - } - setIsDeleteModalOpen(false); - setReportToDelete(null); - }; - - const handleBulkDelete = () => { - if (selectedReports.length > 0 && onDeleteReport) { - selectedReports.forEach(id => onDeleteReport(id)); - if (onSelectionChange) { - onSelectionChange([]); - } - } - }; - - const renderActionButton = (report: AnalyticsReport, action: string) => { - switch (action) { - case 'view': - return ( - - ); - case 'edit': - return ( - - ); - case 'schedule': - return ( - - ); - case 'share': - return ( - - ); - case 'export': - return ( -
- - -
- ); - case 'delete': - return ( - - ); - default: - return null; - } - }; - - const tableColumns = [ - { - key: 'select', - title: ( - 0} - onChange={(e) => handleSelectAll(e.target.checked)} - className="rounded" - /> - ), - render: (report: AnalyticsReport) => ( - handleSelectReport(report.id, e.target.checked)} - className="rounded" - /> - ), - width: 50, - visible: bulkActions, - }, - { - key: 'name', - title: 'Nombre', - sortable: true, - render: (report: AnalyticsReport) => ( -
- - {report.description && ( -

{report.description}

- )} -
- ), - minWidth: 200, - }, - { - key: 'type', - title: 'Tipo', - sortable: true, - render: (report: AnalyticsReport) => ( - - {REPORT_TYPE_LABELS[report.type]} - - ), - width: 120, - }, - { - key: 'category', - title: 'Categoría', - sortable: true, - width: 120, - }, - { - key: 'status', - title: 'Estado', - sortable: true, - render: (report: AnalyticsReport) => ( - - {STATUS_LABELS[report.status]} - - ), - width: 100, - }, - { - key: 'last_run', - title: 'Última ejecución', - sortable: true, - render: (report: AnalyticsReport) => ( - report.last_run - ? new Date(report.last_run).toLocaleDateString('es-ES') - : '-' - ), - width: 140, - }, - { - key: 'next_run', - title: 'Próxima ejecución', - sortable: true, - render: (report: AnalyticsReport) => ( - report.next_run - ? new Date(report.next_run).toLocaleDateString('es-ES') - : '-' - ), - width: 140, - }, - { - key: 'updated_at', - title: 'Actualizado', - sortable: true, - render: (report: AnalyticsReport) => ( - new Date(report.updated_at).toLocaleDateString('es-ES') - ), - width: 120, - }, - { - key: 'actions', - title: 'Acciones', - render: (report: AnalyticsReport) => ( -
- {renderActionButton(report, 'view')} - {renderActionButton(report, 'edit')} - {renderActionButton(report, 'schedule')} - {renderActionButton(report, 'export')} - {renderActionButton(report, 'delete')} -
- ), - width: 200, - }, - ].filter(col => col.visible !== false); - - if (error) { - return ( - -
-

Error al cargar los reportes

-

{error}

-
-
- ); - } - - return ( - <> - - {/* Filters and Actions */} -
-
- {/* Search */} - {(tableConfig?.showSearch !== false) && ( -
- setSearchTerm(e.target.value)} - className="w-full" - /> -
- )} - - {/* Filters */} -
- - - -
- - {/* Bulk Actions */} - {bulkActions && selectedReports.length > 0 && ( -
- -
- )} -
-
- - {/* Table */} -
- - - - {/* Pagination */} - {pagination && ( -
-
-

- Mostrando {Math.min(pagination.pageSize * (pagination.current - 1) + 1, pagination.total)} - {Math.min(pagination.pageSize * pagination.current, pagination.total)} de {pagination.total} reportes -

- -
- - - - Página {pagination.current} de {Math.ceil(pagination.total / pagination.pageSize)} - - - -
-
-
- )} - - - {/* Delete Confirmation Modal */} - { - setIsDeleteModalOpen(false); - setReportToDelete(null); - }} - title="Confirmar eliminación" - > -
-

¿Estás seguro de que deseas eliminar este reporte? Esta acción no se puede deshacer.

- -
- - -
-
-
- - {/* Schedule Modal */} - { - setIsScheduleModalOpen(false); - setReportToSchedule(null); - }} - title="Programar reporte" - > -
-

Configurar programación para: {reportToSchedule?.name}

- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
-
- - {/* Share Modal */} - { - setIsShareModalOpen(false); - setReportToShare(null); - }} - title="Compartir reporte" - > -
-

Compartir: {reportToShare?.name}

- -
- -
- - -
-
- -
- - -
- -
- - -
- -
- - -
-
-
- - ); -}; - -export default ReportsTable; \ No newline at end of file diff --git a/frontend/src/components/domain/dashboard/DashboardCard.tsx.backup b/frontend/src/components/domain/dashboard/DashboardCard.tsx.backup deleted file mode 100644 index e0417223..00000000 --- a/frontend/src/components/domain/dashboard/DashboardCard.tsx.backup +++ /dev/null @@ -1,298 +0,0 @@ -import React, { forwardRef, HTMLAttributes, ReactNode, useState } from 'react'; -import { clsx } from 'clsx'; -import { Card, CardHeader, CardBody, CardFooter, Button, Badge } from '../../ui'; - -export interface DashboardCardProps extends Omit, 'title'> { - // Card variants for different dashboard contexts - variant?: 'metric' | 'chart' | 'list' | 'activity' | 'status' | 'action'; - - // Header props - title?: ReactNode; - subtitle?: ReactNode; - icon?: ReactNode; - headerActions?: ReactNode; - - // Loading and state management - isLoading?: boolean; - hasError?: boolean; - errorMessage?: string; - isEmpty?: boolean; - emptyMessage?: string; - - // Footer props - footerActions?: ReactNode; - footerText?: ReactNode; - - // Badge/notification support - badge?: string | number; - badgeVariant?: 'default' | 'primary' | 'success' | 'warning' | 'error'; - - // Interactive behavior - interactive?: boolean; - onClick?: () => void; - onRefresh?: () => void; - - // Layout customization - padding?: 'none' | 'sm' | 'md' | 'lg'; - headerPadding?: 'none' | 'sm' | 'md' | 'lg'; - bodyPadding?: 'none' | 'sm' | 'md' | 'lg'; - - // Accessibility - 'aria-label'?: string; - 'aria-describedby'?: string; -} - -const DashboardCard = forwardRef(({ - variant = 'metric', - title, - subtitle, - icon, - headerActions, - isLoading = false, - hasError = false, - errorMessage = 'Ha ocurrido un error', - isEmpty = false, - emptyMessage = 'No hay datos disponibles', - footerActions, - footerText, - badge, - badgeVariant = 'primary', - interactive = false, - onClick, - onRefresh, - padding = 'md', - headerPadding, - bodyPadding, - className, - children, - 'aria-label': ariaLabel, - 'aria-describedby': ariaDescribedby, - ...props -}, ref) => { - const [isRefreshing, setIsRefreshing] = useState(false); - - const handleRefresh = async () => { - if (onRefresh && !isRefreshing) { - setIsRefreshing(true); - try { - await onRefresh(); - } finally { - setIsRefreshing(false); - } - } - }; - - const variantStyles = { - metric: 'bg-gradient-to-br from-white to-blue-50 border-blue-200 hover:border-blue-300', - chart: 'bg-white border-gray-200 hover:border-gray-300', - list: 'bg-white border-gray-200 hover:border-gray-300', - activity: 'bg-gradient-to-br from-white to-green-50 border-green-200 hover:border-green-300', - status: 'bg-gradient-to-br from-white to-purple-50 border-purple-200 hover:border-purple-300', - action: 'bg-gradient-to-br from-white to-amber-50 border-amber-200 hover:border-amber-300 hover:shadow-lg' - }; - - const cardClasses = clsx( - variantStyles[variant], - 'transition-all duration-300', - { - 'cursor-pointer transform hover:-translate-y-1': interactive || onClick, - 'opacity-50': isLoading, - 'border-red-300 bg-red-50': hasError, - }, - className - ); - - const hasHeader = title || subtitle || icon || headerActions || badge || onRefresh; - - const renderSkeletonContent = () => { - switch (variant) { - case 'metric': - return ( -
-
-
-
-
- ); - case 'chart': - return ( -
-
-
-
- ); - case 'list': - return ( -
- {Array.from({ length: 3 }).map((_, i) => ( -
-
-
-
-
-
-
- ))} -
- ); - default: - return
; - } - }; - - const renderErrorContent = () => ( -
-
- - - -
-

{errorMessage}

- {onRefresh && ( - - )} -
- ); - - const renderEmptyContent = () => ( -
-
- - - -
-

{emptyMessage}

-
- ); - - return ( - - {hasHeader && ( - -
- {icon && ( -
- {icon} -
- )} -
- {title && ( -
-

- {title} -

- {badge && ( - - {badge} - - )} -
- )} - {subtitle && ( -

- {subtitle} -

- )} -
-
- -
- {onRefresh && ( - - )} - {headerActions} -
-
- )} - - - {isLoading - ? renderSkeletonContent() - : hasError - ? renderErrorContent() - : isEmpty - ? renderEmptyContent() - : children} - - - {(footerActions || footerText) && ( - -
- {footerText} -
-
- {footerActions} -
-
- )} -
- ); -}); - -DashboardCard.displayName = 'DashboardCard'; - -export default DashboardCard; \ No newline at end of file diff --git a/frontend/src/components/domain/dashboard/DashboardGrid.tsx.backup b/frontend/src/components/domain/dashboard/DashboardGrid.tsx.backup deleted file mode 100644 index ca10dee2..00000000 --- a/frontend/src/components/domain/dashboard/DashboardGrid.tsx.backup +++ /dev/null @@ -1,258 +0,0 @@ -import React from 'react'; -import { motion } from 'framer-motion'; -import { - TrendingUp, - Package, - AlertCircle, - Cloud, - Clock, - DollarSign, - Users, - ShoppingCart -} from 'lucide-react'; -import { Card } from '../../ui'; -import { Badge } from '../../ui'; -import { useQuery } from '@tanstack/react-query'; -import { dashboardService } from '../../../services/api/dashboard.service'; -import { ForecastChart } from '../forecasting/ForecastChart'; -import { SalesChart } from '../sales/SalesChart'; -import { AIInsightCard } from '../ai/AIInsightCard'; -import { ProductionStatusCard } from '../production/ProductionStatusCard'; -import { AlertsFeed } from '../alerts/AlertsFeed'; -import { useBakeryStore } from '../../../stores/bakery.store'; - -export const DashboardGrid: React.FC = () => { - const { currentTenant, bakeryType } = useBakeryStore(); - - const { data: dashboardData, isLoading } = useQuery({ - queryKey: ['dashboard', currentTenant?.id], - queryFn: () => dashboardService.getDashboardData(currentTenant!.id), - enabled: !!currentTenant, - refetchInterval: 60000, // Refresh every minute - }); - - const containerVariants = { - hidden: { opacity: 0 }, - visible: { - opacity: 1, - transition: { - staggerChildren: 0.1, - }, - }, - }; - - const itemVariants = { - hidden: { y: 20, opacity: 0 }, - visible: { - y: 0, - opacity: 1, - transition: { - type: 'spring', - stiffness: 100, - }, - }, - }; - - if (isLoading) { - return ; - } - - return ( - - {/* KPI Cards Row */} - - } - color="green" - /> - - - - } - color="blue" - /> - - - - } - color="orange" - /> - - - - - - - {/* Charts Row */} - - - - Predicción de Demanda - 7 Días - - - - - - - - - - - Análisis de Ventas - - - - - - - - {/* Status Cards */} - {bakeryType === 'individual' && ( - - - - )} - - {/* AI Insights */} - - console.log('AI Action:', action)} - /> - - - {/* Real-time Alerts Feed */} - - - - - ); -}; - -// KPI Card Component -interface KPICardProps { - title: string; - value: string | number; - change?: number; - urgent?: number; - icon: React.ReactNode; - color: 'green' | 'blue' | 'orange' | 'red'; -} - -const KPICard: React.FC = ({ - title, - value, - change, - urgent, - icon, - color -}) => { - const colorClasses = { - green: 'bg-green-50 text-green-600 border-green-200', - blue: 'bg-blue-50 text-blue-600 border-blue-200', - orange: 'bg-orange-50 text-orange-600 border-orange-200', - red: 'bg-red-50 text-red-600 border-red-200', - }; - - return ( - - -
-
- {icon} -
- {urgent !== undefined && urgent > 0 && ( - - {urgent} Urgente - - )} -
- -
-

{title}

-

{value}

- - {change !== undefined && ( -
- = 0 ? 'text-green-500' : 'text-red-500 rotate-180'}`} /> - = 0 ? 'text-green-600' : 'text-red-600'}> - {Math.abs(change)}% - - vs ayer -
- )} -
-
-
- ); -}; - -// Weather Impact Card -const WeatherCard: React.FC<{ - temperature?: number; - condition?: string; - impact?: string; -}> = ({ temperature, condition, impact }) => { - return ( - - -
- - - {impact === 'positive' ? '↑' : impact === 'negative' ? '↓' : '='} Impacto - -
-
-

{temperature}°C

-

{condition}

-
-
-
- ); -}; - -const DashboardSkeleton: React.FC = () => { - return ( -
- {[...Array(8)].map((_, i) => ( - - -
-
- - - ))} -
- ); -}; \ No newline at end of file diff --git a/frontend/src/components/domain/dashboard/QuickActions.tsx.backup b/frontend/src/components/domain/dashboard/QuickActions.tsx.backup deleted file mode 100644 index 27f64e26..00000000 --- a/frontend/src/components/domain/dashboard/QuickActions.tsx.backup +++ /dev/null @@ -1,382 +0,0 @@ -import React, { KeyboardEvent, useCallback, useMemo } from 'react'; -import { clsx } from 'clsx'; -import { Button, Badge } from '../../ui'; - -export interface QuickAction { - id: string; - title: string; - description?: string; - icon: React.ReactNode; - onClick: () => void; - href?: string; - - // Badge/notification support - badge?: string | number; - badgeVariant?: 'default' | 'primary' | 'success' | 'warning' | 'error'; - - // Access control - permissions?: string[]; - requiredRole?: string; - isDisabled?: boolean; - disabledReason?: string; - - // Styling - variant?: 'primary' | 'secondary' | 'outline' | 'success' | 'warning' | 'danger'; - color?: string; - backgroundGradient?: string; - - // Keyboard shortcuts - shortcut?: string; - - // Priority for ordering - priority?: number; -} - -export interface QuickActionsProps { - actions: QuickAction[]; - - // Layout configuration - columns?: 2 | 3 | 4 | 5 | 6; - gap?: 'sm' | 'md' | 'lg'; - size?: 'sm' | 'md' | 'lg'; - - // Filtering and user context - userRole?: string; - userPermissions?: string[]; - showDisabled?: boolean; - maxActions?: number; - - // Event handlers - onActionClick?: (action: QuickAction) => void; - onActionHover?: (action: QuickAction) => void; - - // Accessibility - 'aria-label'?: string; - className?: string; -} - -// Predefined bakery actions with Spanish context -export const BAKERY_QUICK_ACTIONS: QuickAction[] = [ - { - id: 'new-order', - title: 'Nuevo Pedido', - description: 'Crear un nuevo pedido de cliente', - icon: ( - - - - ), - onClick: () => console.log('Nuevo pedido'), - variant: 'primary', - backgroundGradient: 'from-blue-500 to-blue-600', - priority: 1 - }, - { - id: 'add-product', - title: 'Agregar Producto', - description: 'Añadir nuevo producto al inventario', - icon: ( - - - - ), - onClick: () => console.log('Agregar producto'), - variant: 'success', - backgroundGradient: 'from-green-500 to-green-600', - priority: 2 - }, - { - id: 'view-inventory', - title: 'Ver Inventario', - description: 'Consultar stock y productos', - icon: ( - - - - ), - onClick: () => console.log('Ver inventario'), - variant: 'outline', - backgroundGradient: 'from-purple-500 to-purple-600', - priority: 3, - badge: '5', - badgeVariant: 'warning' - }, - { - id: 'production-batch', - title: 'Nueva Producción', - description: 'Programar lote de producción', - icon: ( - - - - ), - onClick: () => console.log('Nueva producción'), - variant: 'warning', - backgroundGradient: 'from-orange-500 to-orange-600', - priority: 4 - }, - { - id: 'sales-report', - title: 'Reporte Ventas', - description: 'Ver análisis de ventas', - icon: ( - - - - ), - onClick: () => console.log('Reporte ventas'), - variant: 'secondary', - backgroundGradient: 'from-indigo-500 to-indigo-600', - priority: 5 - }, - { - id: 'manage-suppliers', - title: 'Proveedores', - description: 'Gestionar proveedores', - icon: ( - - - - ), - onClick: () => console.log('Proveedores'), - variant: 'outline', - backgroundGradient: 'from-teal-500 to-teal-600', - priority: 6 - }, - { - id: 'pos-system', - title: 'Sistema POS', - description: 'Punto de venta', - icon: ( - - - - ), - onClick: () => console.log('Sistema POS'), - variant: 'primary', - backgroundGradient: 'from-emerald-500 to-emerald-600', - priority: 7 - }, - { - id: 'quality-control', - title: 'Control Calidad', - description: 'Verificación de calidad', - icon: ( - - - - ), - onClick: () => console.log('Control calidad'), - variant: 'success', - backgroundGradient: 'from-lime-500 to-lime-600', - priority: 8, - requiredRole: 'quality_manager' - } -]; - -const QuickActions: React.FC = ({ - actions, - columns = 3, - gap = 'md', - size = 'md', - userRole, - userPermissions = [], - showDisabled = false, - maxActions, - onActionClick, - onActionHover, - 'aria-label': ariaLabel = 'Acciones rápidas', - className -}) => { - // Filter and sort actions - const visibleActions = useMemo(() => { - let filteredActions = actions.filter(action => { - // Role-based filtering - if (action.requiredRole && userRole !== action.requiredRole) { - return showDisabled; - } - - // Permission-based filtering - if (action.permissions && action.permissions.length > 0) { - const hasPermission = action.permissions.some(perm => - userPermissions.includes(perm) - ); - if (!hasPermission) { - return showDisabled; - } - } - - return true; - }); - - // Sort by priority - filteredActions.sort((a, b) => (a.priority || 999) - (b.priority || 999)); - - // Limit actions if specified - if (maxActions) { - filteredActions = filteredActions.slice(0, maxActions); - } - - return filteredActions; - }, [actions, userRole, userPermissions, showDisabled, maxActions]); - - const handleActionClick = useCallback((action: QuickAction) => { - if (action.isDisabled) return; - - onActionClick?.(action); - action.onClick(); - }, [onActionClick]); - - const handleKeyDown = useCallback((event: KeyboardEvent, action: QuickAction) => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - handleActionClick(action); - } - }, [handleActionClick]); - - const gridClasses = { - 2: 'grid-cols-2 sm:grid-cols-2', - 3: 'grid-cols-2 sm:grid-cols-3', - 4: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-4', - 5: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-5', - 6: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-6' - }; - - const gapClasses = { - sm: 'gap-2', - md: 'gap-4', - lg: 'gap-6' - }; - - const sizeClasses = { - sm: 'p-3 min-h-[80px]', - md: 'p-4 min-h-[100px]', - lg: 'p-6 min-h-[120px]' - }; - - if (visibleActions.length === 0) { - return ( -
-

No hay acciones disponibles

-
- ); - } - - return ( -
- {visibleActions.map((action) => { - const isDisabled = action.isDisabled || - (action.requiredRole && userRole !== action.requiredRole) || - (action.permissions && !action.permissions.some(perm => userPermissions.includes(perm))); - - const buttonClasses = clsx( - 'relative group transition-all duration-200', - 'border border-gray-200 rounded-xl', - 'flex flex-col items-center justify-center text-center', - 'hover:shadow-lg hover:-translate-y-1', - 'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2', - sizeClasses[size], - { - 'bg-white hover:bg-gray-50': !action.backgroundGradient, - 'bg-gradient-to-br text-white hover:opacity-90': action.backgroundGradient, - 'opacity-50 cursor-not-allowed hover:transform-none hover:shadow-none': isDisabled, - } - ); - - const gradientStyle = action.backgroundGradient ? { - background: `linear-gradient(135deg, var(--tw-gradient-stops))`, - } : undefined; - - return ( - - ); - })} -
- ); -}; - -QuickActions.displayName = 'QuickActions'; - -export default QuickActions; \ No newline at end of file diff --git a/frontend/src/components/domain/dashboard/RecentActivity.tsx.backup b/frontend/src/components/domain/dashboard/RecentActivity.tsx.backup deleted file mode 100644 index 98ea6d30..00000000 --- a/frontend/src/components/domain/dashboard/RecentActivity.tsx.backup +++ /dev/null @@ -1,524 +0,0 @@ -import React, { useMemo, useState, useCallback } from 'react'; -import { clsx } from 'clsx'; -import { Avatar, Badge, Button } from '../../ui'; - -export interface ActivityUser { - id: string; - name: string; - avatar?: string; - role?: string; -} - -export interface ActivityItem { - id: string; - type: ActivityType; - title: string; - description: string; - timestamp: string; - user?: ActivityUser; - metadata?: Record; - status?: ActivityStatus; - category?: string; - - // Navigation support - href?: string; - onClick?: () => void; - - // Visual styling - icon?: React.ReactNode; - color?: string; - priority?: ActivityPriority; -} - -export enum ActivityType { - ORDER = 'order', - PRODUCTION = 'production', - INVENTORY = 'inventory', - SALES = 'sales', - USER = 'user', - SYSTEM = 'system', - QUALITY = 'quality', - SUPPLIER = 'supplier', - FINANCE = 'finance', - ALERT = 'alert' -} - -export enum ActivityStatus { - SUCCESS = 'success', - WARNING = 'warning', - ERROR = 'error', - INFO = 'info', - PENDING = 'pending', - IN_PROGRESS = 'in_progress', - CANCELLED = 'cancelled' -} - -export enum ActivityPriority { - LOW = 'low', - MEDIUM = 'medium', - HIGH = 'high', - URGENT = 'urgent' -} - -export interface RecentActivityProps { - activities: ActivityItem[]; - - // Display configuration - maxItems?: number; - showTimestamp?: boolean; - showUserAvatar?: boolean; - showTypeIcons?: boolean; - compact?: boolean; - - // Filtering - allowFiltering?: boolean; - filterTypes?: ActivityType[]; - defaultFilter?: ActivityType | 'all'; - - // Pagination and loading - hasMore?: boolean; - isLoading?: boolean; - onLoadMore?: () => void; - - // Event handlers - onActivityClick?: (activity: ActivityItem) => void; - onRefresh?: () => void; - - // Accessibility and styling - className?: string; - 'aria-label'?: string; - emptyMessage?: string; -} - -// Spanish activity type labels and icons -const ACTIVITY_CONFIG = { - [ActivityType.ORDER]: { - label: 'Pedidos', - icon: ( - - - - ), - color: 'blue', - bgColor: 'bg-blue-100', - textColor: 'text-blue-600', - borderColor: 'border-blue-200' - }, - [ActivityType.PRODUCTION]: { - label: 'Producción', - icon: ( - - - - ), - color: 'orange', - bgColor: 'bg-orange-100', - textColor: 'text-orange-600', - borderColor: 'border-orange-200' - }, - [ActivityType.INVENTORY]: { - label: 'Inventario', - icon: ( - - - - ), - color: 'purple', - bgColor: 'bg-purple-100', - textColor: 'text-purple-600', - borderColor: 'border-purple-200' - }, - [ActivityType.SALES]: { - label: 'Ventas', - icon: ( - - - - ), - color: 'green', - bgColor: 'bg-green-100', - textColor: 'text-green-600', - borderColor: 'border-green-200' - }, - [ActivityType.USER]: { - label: 'Usuarios', - icon: ( - - - - ), - color: 'indigo', - bgColor: 'bg-indigo-100', - textColor: 'text-indigo-600', - borderColor: 'border-indigo-200' - }, - [ActivityType.SYSTEM]: { - label: 'Sistema', - icon: ( - - - - - ), - color: 'gray', - bgColor: 'bg-gray-100', - textColor: 'text-gray-600', - borderColor: 'border-gray-200' - }, - [ActivityType.QUALITY]: { - label: 'Calidad', - icon: ( - - - - ), - color: 'green', - bgColor: 'bg-green-100', - textColor: 'text-green-600', - borderColor: 'border-green-200' - }, - [ActivityType.SUPPLIER]: { - label: 'Proveedores', - icon: ( - - - - ), - color: 'teal', - bgColor: 'bg-teal-100', - textColor: 'text-teal-600', - borderColor: 'border-teal-200' - }, - [ActivityType.FINANCE]: { - label: 'Finanzas', - icon: ( - - - - ), - color: 'emerald', - bgColor: 'bg-emerald-100', - textColor: 'text-emerald-600', - borderColor: 'border-emerald-200' - }, - [ActivityType.ALERT]: { - label: 'Alertas', - icon: ( - - - - ), - color: 'red', - bgColor: 'bg-red-100', - textColor: 'text-red-600', - borderColor: 'border-red-200' - } -}; - -const STATUS_CONFIG = { - [ActivityStatus.SUCCESS]: { color: 'green', bgColor: 'bg-green-500' }, - [ActivityStatus.WARNING]: { color: 'yellow', bgColor: 'bg-yellow-500' }, - [ActivityStatus.ERROR]: { color: 'red', bgColor: 'bg-red-500' }, - [ActivityStatus.INFO]: { color: 'blue', bgColor: 'bg-blue-500' }, - [ActivityStatus.PENDING]: { color: 'gray', bgColor: 'bg-gray-500' }, - [ActivityStatus.IN_PROGRESS]: { color: 'purple', bgColor: 'bg-purple-500' }, - [ActivityStatus.CANCELLED]: { color: 'gray', bgColor: 'bg-gray-400' } -}; - -const formatRelativeTime = (timestamp: string): string => { - const date = new Date(timestamp); - const now = new Date(); - const diffInMs = now.getTime() - date.getTime(); - const diffInMinutes = Math.floor(diffInMs / (1000 * 60)); - const diffInHours = Math.floor(diffInMinutes / 60); - const diffInDays = Math.floor(diffInHours / 24); - - if (diffInMinutes < 1) { - return 'Ahora mismo'; - } else if (diffInMinutes < 60) { - return `Hace ${diffInMinutes} min`; - } else if (diffInHours < 24) { - return `Hace ${diffInHours}h`; - } else if (diffInDays === 1) { - return 'Ayer'; - } else if (diffInDays < 7) { - return `Hace ${diffInDays} días`; - } else { - return date.toLocaleDateString('es-ES', { - day: 'numeric', - month: 'short' - }); - } -}; - -const RecentActivity: React.FC = ({ - activities, - maxItems = 10, - showTimestamp = true, - showUserAvatar = true, - showTypeIcons = true, - compact = false, - allowFiltering = true, - filterTypes = Object.values(ActivityType), - defaultFilter = 'all', - hasMore = false, - isLoading = false, - onLoadMore, - onActivityClick, - onRefresh, - className, - 'aria-label': ariaLabel = 'Actividad reciente', - emptyMessage = 'No hay actividad reciente' -}) => { - const [activeFilter, setActiveFilter] = useState(defaultFilter); - - const filteredActivities = useMemo(() => { - let filtered = activities; - - if (activeFilter !== 'all') { - filtered = activities.filter(activity => activity.type === activeFilter); - } - - return filtered.slice(0, maxItems); - }, [activities, activeFilter, maxItems]); - - const handleActivityClick = useCallback((activity: ActivityItem) => { - onActivityClick?.(activity); - if (activity.onClick) { - activity.onClick(); - } - }, [onActivityClick]); - - const renderActivityItem = (activity: ActivityItem) => { - const config = ACTIVITY_CONFIG[activity.type] || { - label: 'Actividad', - icon:
, - color: 'gray', - bgColor: 'bg-gray-100', - textColor: 'text-gray-600', - borderColor: 'border-gray-200' - }; - const statusConfig = activity.status ? STATUS_CONFIG[activity.status] : null; - - const itemClasses = clsx( - 'group relative flex items-start gap-3 p-3 rounded-lg transition-all duration-200', - 'hover:bg-gray-50 hover:shadow-sm', - { - 'cursor-pointer': activity.onClick || activity.href, - 'p-2': compact, - 'border-l-4': !compact, - [config.borderColor]: !compact - } - ); - - return ( -
handleActivityClick(activity) : undefined} - role={activity.onClick || activity.href ? 'button' : undefined} - tabIndex={activity.onClick || activity.href ? 0 : undefined} - > - {/* Timeline indicator */} -
- {showTypeIcons && ( -
- {activity.icon || config.icon} -
- )} - - {/* Status indicator */} - {activity.status && statusConfig && ( -
- )} -
- - {/* Content */} -
-
-
-

- {activity.title} -

-

- {activity.description} -

- - {/* User info */} - {activity.user && showUserAvatar && ( -
- - - {activity.user.name} - -
- )} -
- - {/* Timestamp */} - {showTimestamp && ( - - )} -
-
-
- ); - }; - - if (activities.length === 0) { - return ( -
-
- - - -
-

{emptyMessage}

-
- ); - } - - return ( -
- {/* Filters */} - {allowFiltering && filterTypes.length > 1 && ( -
- - {filterTypes.map((type) => { - const config = ACTIVITY_CONFIG[type] || { - label: 'Actividad', - icon:
, - color: 'gray', - bgColor: 'bg-gray-100', - textColor: 'text-gray-600', - borderColor: 'border-gray-200' - }; - const count = activities.filter(a => a.type === type).length; - - return ( - - ); - })} - - {onRefresh && ( -
- -
- )} -
- )} - - {/* Activity list */} -
- {filteredActivities.map(renderActivityItem)} -
- - {/* Loading state */} - {isLoading && ( -
-
-
- )} - - {/* Load more */} - {hasMore && onLoadMore && !isLoading && ( -
- -
- )} -
- ); -}; - -RecentActivity.displayName = 'RecentActivity'; - -export default RecentActivity; \ No newline at end of file diff --git a/frontend/src/components/domain/forecasting/AlertsPanel.tsx.backup b/frontend/src/components/domain/forecasting/AlertsPanel.tsx.backup deleted file mode 100644 index 644953ba..00000000 --- a/frontend/src/components/domain/forecasting/AlertsPanel.tsx.backup +++ /dev/null @@ -1,725 +0,0 @@ -import React, { useState, useMemo, useCallback } from 'react'; -import { clsx } from 'clsx'; -import { Card, CardHeader, CardBody } from '../../ui'; -import { Button } from '../../ui'; -import { Badge } from '../../ui'; -import { Select } from '../../ui'; -import { Input } from '../../ui'; -import { - ForecastAlert, - ForecastAlertType, - AlertSeverity, -} from '../../../types/forecasting.types'; - -export interface AlertsPanelProps { - className?: string; - title?: string; - alerts?: ForecastAlert[]; - loading?: boolean; - error?: string | null; - onAlertAction?: (alertId: string, action: 'acknowledge' | 'resolve' | 'snooze' | 'production_adjust' | 'inventory_check') => void; - onAlertDismiss?: (alertId: string) => void; - onBulkAction?: (alertIds: string[], action: string) => void; - showFilters?: boolean; - compact?: boolean; - maxItems?: number; - autoRefresh?: boolean; - refreshInterval?: number; -} - -interface AlertFilter { - severity: AlertSeverity | 'all'; - type: ForecastAlertType | 'all'; - status: 'active' | 'acknowledged' | 'resolved' | 'all'; - product: string; - dateRange: 'today' | 'week' | 'month' | 'all'; -} - -interface AlertActionGroup { - critical: AlertAction[]; - high: AlertAction[]; - medium: AlertAction[]; - low: AlertAction[]; -} - -interface AlertAction { - id: string; - label: string; - icon: string; - variant: 'primary' | 'secondary' | 'success' | 'warning' | 'danger'; - description: string; -} - -const SPANISH_ALERT_TYPES: Record = { - [ForecastAlertType.HIGH_DEMAND_PREDICTED]: 'Alta Demanda Predicha', - [ForecastAlertType.LOW_DEMAND_PREDICTED]: 'Baja Demanda Predicha', - [ForecastAlertType.ACCURACY_DROP]: 'Caída de Precisión', - [ForecastAlertType.MODEL_DRIFT]: 'Deriva del Modelo', - [ForecastAlertType.DATA_ANOMALY]: 'Anomalía de Datos', - [ForecastAlertType.MISSING_DATA]: 'Datos Faltantes', - [ForecastAlertType.SEASONAL_SHIFT]: 'Cambio Estacional', -}; - -const SPANISH_SEVERITIES: Record = { - [AlertSeverity.CRITICAL]: 'Crítica', - [AlertSeverity.HIGH]: 'Alta', - [AlertSeverity.MEDIUM]: 'Media', - [AlertSeverity.LOW]: 'Baja', -}; - -const SEVERITY_COLORS: Record = { - [AlertSeverity.CRITICAL]: 'text-red-600 bg-red-50 border-red-200', - [AlertSeverity.HIGH]: 'text-orange-600 bg-orange-50 border-orange-200', - [AlertSeverity.MEDIUM]: 'text-yellow-600 bg-yellow-50 border-yellow-200', - [AlertSeverity.LOW]: 'text-blue-600 bg-blue-50 border-blue-200', -}; - -const SEVERITY_BADGE_VARIANTS: Record = { - [AlertSeverity.CRITICAL]: 'danger', - [AlertSeverity.HIGH]: 'warning', - [AlertSeverity.MEDIUM]: 'warning', - [AlertSeverity.LOW]: 'info', -}; - -const ALERT_TYPE_ICONS: Record = { - [ForecastAlertType.HIGH_DEMAND_PREDICTED]: '📈', - [ForecastAlertType.LOW_DEMAND_PREDICTED]: '📉', - [ForecastAlertType.ACCURACY_DROP]: '🎯', - [ForecastAlertType.MODEL_DRIFT]: '🔄', - [ForecastAlertType.DATA_ANOMALY]: '⚠️', - [ForecastAlertType.MISSING_DATA]: '📊', - [ForecastAlertType.SEASONAL_SHIFT]: '🍂', -}; - -const ALERT_ACTIONS: AlertActionGroup = { - critical: [ - { - id: 'production_adjust', - label: 'Ajustar Producción', - icon: '🏭', - variant: 'primary', - description: 'Modificar inmediatamente el plan de producción', - }, - { - id: 'inventory_check', - label: 'Verificar Inventario', - icon: '📦', - variant: 'warning', - description: 'Revisar niveles de stock y materias primas', - }, - { - id: 'emergency_order', - label: 'Pedido Urgente', - icon: '🚚', - variant: 'danger', - description: 'Realizar pedido urgente a proveedores', - }, - ], - high: [ - { - id: 'production_adjust', - label: 'Ajustar Producción', - icon: '🏭', - variant: 'primary', - description: 'Ajustar plan de producción para mañana', - }, - { - id: 'inventory_alert', - label: 'Alerta Inventario', - icon: '📋', - variant: 'warning', - description: 'Crear alerta de inventario preventiva', - }, - { - id: 'team_notify', - label: 'Notificar Equipo', - icon: '👥', - variant: 'secondary', - description: 'Informar al equipo de producción', - }, - ], - medium: [ - { - id: 'production_review', - label: 'Revisar Producción', - icon: '📊', - variant: 'secondary', - description: 'Revisar plan de producción esta semana', - }, - { - id: 'monitor', - label: 'Monitorear', - icon: '👁️', - variant: 'secondary', - description: 'Mantener bajo observación', - }, - ], - low: [ - { - id: 'monitor', - label: 'Monitorear', - icon: '👁️', - variant: 'secondary', - description: 'Mantener bajo observación', - }, - { - id: 'data_review', - label: 'Revisar Datos', - icon: '🔍', - variant: 'secondary', - description: 'Revisar calidad de los datos', - }, - ], -}; - -const RECOMMENDATION_MESSAGES: Record string> = { - [ForecastAlertType.HIGH_DEMAND_PREDICTED]: (product, value) => - `Se predice un aumento del ${value}% en la demanda de ${product}. Considera aumentar la producción.`, - [ForecastAlertType.LOW_DEMAND_PREDICTED]: (product, value) => - `Se predice una disminución del ${Math.abs(value)}% en la demanda de ${product}. Considera reducir la producción para evitar desperdicios.`, - [ForecastAlertType.ACCURACY_DROP]: (product, value) => - `La precisión del modelo para ${product} ha disminuido al ${value}%. Es recomendable reentrenar el modelo.`, - [ForecastAlertType.MODEL_DRIFT]: (product, value) => - `Detectada deriva en el modelo de ${product}. Los patrones de demanda han cambiado significativamente.`, - [ForecastAlertType.DATA_ANOMALY]: (product, value) => - `Anomalía detectada en los datos de ${product}. Verifica la calidad de los datos de entrada.`, - [ForecastAlertType.MISSING_DATA]: (product, value) => - `Faltan ${value} días de datos para ${product}. Esto puede afectar la precisión de las predicciones.`, - [ForecastAlertType.SEASONAL_SHIFT]: (product, value) => - `Detectado cambio en el patrón estacional de ${product}. El pico de demanda se ha adelantado/retrasado.`, -}; - -const AlertsPanel: React.FC = ({ - className, - title = 'Alertas y Recomendaciones', - alerts = [], - loading = false, - error = null, - onAlertAction, - onAlertDismiss, - onBulkAction, - showFilters = true, - compact = false, - maxItems, - autoRefresh = false, - refreshInterval = 30000, -}) => { - const [filters, setFilters] = useState({ - severity: 'all', - type: 'all', - status: 'active', - product: '', - dateRange: 'all', - }); - - const [selectedAlerts, setSelectedAlerts] = useState([]); - const [expandedAlerts, setExpandedAlerts] = useState([]); - - // Filter alerts - const filteredAlerts = useMemo(() => { - let filtered = [...alerts]; - - // Filter by status - if (filters.status !== 'all') { - filtered = filtered.filter(alert => { - if (filters.status === 'active') return alert.is_active && !alert.acknowledged_at && !alert.resolved_at; - if (filters.status === 'acknowledged') return !!alert.acknowledged_at && !alert.resolved_at; - if (filters.status === 'resolved') return !!alert.resolved_at; - return true; - }); - } - - // Filter by severity - if (filters.severity !== 'all') { - filtered = filtered.filter(alert => alert.severity === filters.severity); - } - - // Filter by type - if (filters.type !== 'all') { - filtered = filtered.filter(alert => alert.alert_type === filters.type); - } - - // Filter by product - if (filters.product) { - filtered = filtered.filter(alert => - alert.product_name?.toLowerCase().includes(filters.product.toLowerCase()) - ); - } - - // Filter by date range - if (filters.dateRange !== 'all') { - const now = new Date(); - const filterDate = new Date(); - - switch (filters.dateRange) { - case 'today': - filterDate.setHours(0, 0, 0, 0); - break; - case 'week': - filterDate.setDate(now.getDate() - 7); - break; - case 'month': - filterDate.setMonth(now.getMonth() - 1); - break; - } - - filtered = filtered.filter(alert => new Date(alert.created_at) >= filterDate); - } - - // Sort by severity and creation date - filtered.sort((a, b) => { - const severityOrder: Record = { - [AlertSeverity.CRITICAL]: 4, - [AlertSeverity.HIGH]: 3, - [AlertSeverity.MEDIUM]: 2, - [AlertSeverity.LOW]: 1, - }; - - if (a.severity !== b.severity) { - return severityOrder[b.severity] - severityOrder[a.severity]; - } - - return new Date(b.created_at).getTime() - new Date(a.created_at).getTime(); - }); - - // Limit items if specified - if (maxItems) { - filtered = filtered.slice(0, maxItems); - } - - return filtered; - }, [alerts, filters, maxItems]); - - // Get alert statistics - const alertStats = useMemo(() => { - const stats = { - total: alerts.length, - active: alerts.filter(a => a.is_active && !a.acknowledged_at && !a.resolved_at).length, - critical: alerts.filter(a => a.severity === AlertSeverity.CRITICAL && a.is_active).length, - high: alerts.filter(a => a.severity === AlertSeverity.HIGH && a.is_active).length, - }; - return stats; - }, [alerts]); - - // Handle alert expansion - const toggleAlertExpansion = useCallback((alertId: string) => { - setExpandedAlerts(prev => - prev.includes(alertId) - ? prev.filter(id => id !== alertId) - : [...prev, alertId] - ); - }, []); - - // Handle alert selection - const toggleAlertSelection = useCallback((alertId: string) => { - setSelectedAlerts(prev => - prev.includes(alertId) - ? prev.filter(id => id !== alertId) - : [...prev, alertId] - ); - }, []); - - // Handle bulk selection - const handleSelectAll = useCallback(() => { - const activeAlerts = filteredAlerts.filter(a => a.is_active).map(a => a.id); - setSelectedAlerts(prev => - prev.length === activeAlerts.length ? [] : activeAlerts - ); - }, [filteredAlerts]); - - // Get available actions for alert - const getAlertActions = useCallback((alert: ForecastAlert): AlertAction[] => { - return ALERT_ACTIONS[alert.severity] || []; - }, []); - - // Get recommendation message - const getRecommendation = useCallback((alert: ForecastAlert): string => { - const generator = RECOMMENDATION_MESSAGES[alert.alert_type]; - if (!generator) return alert.message; - - const value = alert.predicted_value || alert.threshold_value || 0; - return generator(alert.product_name || 'Producto', value); - }, []); - - // Format time ago - const formatTimeAgo = useCallback((dateString: string): string => { - const date = new Date(dateString); - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffMins = Math.floor(diffMs / (1000 * 60)); - const diffHours = Math.floor(diffMins / 60); - const diffDays = Math.floor(diffHours / 24); - - if (diffMins < 1) return 'Ahora mismo'; - if (diffMins < 60) return `Hace ${diffMins} min`; - if (diffHours < 24) return `Hace ${diffHours}h`; - if (diffDays < 7) return `Hace ${diffDays}d`; - return date.toLocaleDateString('es-ES'); - }, []); - - // Loading state - if (loading) { - return ( - - -

{title}

-
- -
-
-
- Cargando alertas... -
-
-
-
- ); - } - - // Error state - if (error) { - return ( - - -

{title}

-
- -
-
-
- - - -
-

{error}

-
-
-
-
- ); - } - - return ( - - -
-
-

{title}

- {alertStats.critical > 0 && ( - - {alertStats.critical} críticas - - )} - {alertStats.high > 0 && ( - - {alertStats.high} altas - - )} - - {alertStats.active}/{alertStats.total} activas - -
- -
- {selectedAlerts.length > 0 && onBulkAction && ( -
- - -
- )} - - {autoRefresh && ( -
-
- Auto-actualización -
- )} -
-
-
- - {showFilters && ( -
-
- - - - - - - setFilters(prev => ({ ...prev, product: e.target.value }))} - className="text-sm" - /> - - -
-
- )} - - - {filteredAlerts.length === 0 ? ( -
-
-
- - - -
-

No hay alertas que mostrar

-
-
- ) : ( -
- {/* Bulk actions bar */} - {filteredAlerts.some(a => a.is_active) && ( -
- a.is_active).length} - onChange={handleSelectAll} - /> - - {selectedAlerts.length > 0 - ? `${selectedAlerts.length} alertas seleccionadas` - : 'Seleccionar todas' - } - -
- )} - - {/* Alerts list */} - {filteredAlerts.map((alert) => { - const isExpanded = expandedAlerts.includes(alert.id); - const isSelected = selectedAlerts.includes(alert.id); - const availableActions = getAlertActions(alert); - const recommendation = getRecommendation(alert); - - return ( -
-
-
- {/* Selection checkbox */} - {alert.is_active && !alert.resolved_at && ( - toggleAlertSelection(alert.id)} - /> - )} - - {/* Alert icon */} -
- {ALERT_TYPE_ICONS[alert.alert_type]} -
- - {/* Alert content */} -
-
-

- {alert.title} -

- - {SPANISH_SEVERITIES[alert.severity]} - - {alert.product_name && ( - - {alert.product_name} - - )} -
- -

- {compact ? alert.message.slice(0, 100) + '...' : alert.message} -

- - {!compact && ( -

- 💡 {recommendation} -

- )} - -
-
- {formatTimeAgo(alert.created_at)} - {alert.acknowledged_at && ( - • Confirmada - )} - {alert.resolved_at && ( - • Resuelta - )} -
- -
- {/* Quick actions */} - {alert.is_active && !alert.resolved_at && ( -
- {availableActions.slice(0, compact ? 1 : 2).map((action) => ( - - ))} - - {availableActions.length > (compact ? 1 : 2) && ( - - )} -
- )} - - {/* Dismiss button */} - {onAlertDismiss && alert.is_active && ( - - )} -
-
-
-
- - {/* Expanded actions */} - {isExpanded && availableActions.length > (compact ? 1 : 2) && ( -
-
- {availableActions.slice(compact ? 1 : 2).map((action) => ( - - ))} -
-
- )} - - {/* Alert details */} - {!compact && isExpanded && ( -
-
-
- Tipo: -
{SPANISH_ALERT_TYPES[alert.alert_type]}
-
- {alert.predicted_value && ( -
- Valor Predicho: -
{alert.predicted_value}
-
- )} - {alert.threshold_value && ( -
- Umbral: -
{alert.threshold_value}
-
- )} - {alert.model_accuracy && ( -
- Precisión Modelo: -
{(alert.model_accuracy * 100).toFixed(1)}%
-
- )} -
-
- )} -
-
- ); - })} -
- )} -
-
- ); -}; - -export default AlertsPanel; \ No newline at end of file diff --git a/frontend/src/components/domain/forecasting/DemandChart.tsx.backup b/frontend/src/components/domain/forecasting/DemandChart.tsx.backup deleted file mode 100644 index 5f3cadef..00000000 --- a/frontend/src/components/domain/forecasting/DemandChart.tsx.backup +++ /dev/null @@ -1,575 +0,0 @@ -import React, { useState, useEffect, useMemo, useCallback } from 'react'; -import { clsx } from 'clsx'; -import { - LineChart, - Line, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, - ReferenceLine, - Area, - ComposedChart, - Legend, - Brush, - ReferenceDot, -} from 'recharts'; -import { Card, CardHeader, CardBody } from '../../ui'; -import { Button } from '../../ui'; -import { Select } from '../../ui'; -import { Badge } from '../../ui'; -import { - ForecastResponse, - DemandTrend, - TrendDirection, - WeatherCondition, - EventType, -} from '../../../types/forecasting.types'; - -export interface DemandChartProps { - className?: string; - title?: string; - data?: DemandTrend[]; - products?: string[]; - selectedProducts?: string[]; - onProductSelectionChange?: (products: string[]) => void; - timeframe?: 'weekly' | 'monthly' | 'quarterly' | 'yearly'; - onTimeframeChange?: (timeframe: 'weekly' | 'monthly' | 'quarterly' | 'yearly') => void; - showConfidenceInterval?: boolean; - showEvents?: boolean; - showWeatherOverlay?: boolean; - events?: Array<{ - date: string; - type: EventType; - name: string; - impact: 'high' | 'medium' | 'low'; - }>; - weatherData?: Array<{ - date: string; - condition: WeatherCondition; - temperature: number; - }>; - loading?: boolean; - error?: string | null; - height?: number; - onExport?: (format: 'png' | 'pdf' | 'csv') => void; -} - -interface ChartDataPoint { - date: string; - actualDemand?: number; - predictedDemand?: number; - confidenceLower?: number; - confidenceUpper?: number; - accuracy?: number; - trendDirection?: TrendDirection; - seasonalFactor?: number; - anomalyScore?: number; - hasEvent?: boolean; - eventType?: EventType; - eventName?: string; - eventImpact?: 'high' | 'medium' | 'low'; - weather?: WeatherCondition; - temperature?: number; -} - -const PRODUCT_COLORS = [ - '#8884d8', // Croissants - '#82ca9d', // Pan de molde - '#ffc658', // Roscón de Reyes - '#ff7c7c', // Torrijas - '#8dd1e1', // Magdalenas - '#d084d0', // Empanadas - '#ffb347', // Tarta de Santiago - '#87ceeb', // Mazapán -]; - -const EVENT_ICONS: Record = { - [EventType.HOLIDAY]: '🎉', - [EventType.FESTIVAL]: '🎪', - [EventType.SPORTS_EVENT]: '⚽', - [EventType.WEATHER_EVENT]: '🌧️', - [EventType.SCHOOL_EVENT]: '🎒', - [EventType.CONCERT]: '🎵', - [EventType.CONFERENCE]: '📊', - [EventType.CONSTRUCTION]: '🚧', -}; - -const WEATHER_ICONS: Record = { - [WeatherCondition.SUNNY]: '☀️', - [WeatherCondition.CLOUDY]: '☁️', - [WeatherCondition.RAINY]: '🌧️', - [WeatherCondition.STORMY]: '⛈️', - [WeatherCondition.SNOWY]: '❄️', - [WeatherCondition.FOGGY]: '🌫️', - [WeatherCondition.WINDY]: '🌪️', -}; - -const SPANISH_EVENT_NAMES: Record = { - [EventType.HOLIDAY]: 'Festividad', - [EventType.FESTIVAL]: 'Festival', - [EventType.SPORTS_EVENT]: 'Evento Deportivo', - [EventType.WEATHER_EVENT]: 'Evento Climático', - [EventType.SCHOOL_EVENT]: 'Evento Escolar', - [EventType.CONCERT]: 'Concierto', - [EventType.CONFERENCE]: 'Conferencia', - [EventType.CONSTRUCTION]: 'Construcción', -}; - -const DemandChart: React.FC = ({ - className, - title = 'Predicción de Demanda', - data = [], - products = [], - selectedProducts = [], - onProductSelectionChange, - timeframe = 'weekly', - onTimeframeChange, - showConfidenceInterval = true, - showEvents = true, - showWeatherOverlay = false, - events = [], - weatherData = [], - loading = false, - error = null, - height = 400, - onExport, -}) => { - const [selectedPeriod, setSelectedPeriod] = useState<{ start?: Date; end?: Date }>({}); - const [zoomedData, setZoomedData] = useState([]); - const [hoveredPoint, setHoveredPoint] = useState(null); - - // Process and merge data with events and weather - const chartData = useMemo(() => { - const processedData: ChartDataPoint[] = data.map(point => { - const dateStr = point.date; - const event = events.find(e => e.date === dateStr); - const weather = weatherData.find(w => w.date === dateStr); - - return { - date: dateStr, - actualDemand: point.actual_demand, - predictedDemand: point.predicted_demand, - confidenceLower: point.confidence_lower, - confidenceUpper: point.confidence_upper, - accuracy: point.accuracy, - trendDirection: point.trend_direction, - seasonalFactor: point.seasonal_factor, - anomalyScore: point.anomaly_score, - hasEvent: !!event, - eventType: event?.type, - eventName: event?.name, - eventImpact: event?.impact, - weather: weather?.condition, - temperature: weather?.temperature, - }; - }); - - return processedData; - }, [data, events, weatherData]); - - // Filter data based on selected period - const filteredData = useMemo(() => { - if (!selectedPeriod.start || !selectedPeriod.end) { - return chartData; - } - - return chartData.filter(point => { - const pointDate = new Date(point.date); - return pointDate >= selectedPeriod.start! && pointDate <= selectedPeriod.end!; - }); - }, [chartData, selectedPeriod]); - - // Update zoomed data when filtered data changes - useEffect(() => { - setZoomedData(filteredData); - }, [filteredData]); - - // Handle brush selection - const handleBrushChange = useCallback((brushData: any) => { - if (brushData && brushData.startIndex !== undefined && brushData.endIndex !== undefined) { - const newData = filteredData.slice(brushData.startIndex, brushData.endIndex + 1); - setZoomedData(newData); - } else { - setZoomedData(filteredData); - } - }, [filteredData]); - - // Reset zoom - const handleResetZoom = useCallback(() => { - setZoomedData(filteredData); - setSelectedPeriod({}); - }, [filteredData]); - - // Custom tooltip - const CustomTooltip = ({ active, payload, label }: any) => { - if (!active || !payload || !payload.length) return null; - - const data = payload[0].payload as ChartDataPoint; - const formattedDate = new Date(label).toLocaleDateString('es-ES', { - day: 'numeric', - month: 'short', - year: 'numeric', - }); - - return ( -
-

{formattedDate}

- -
- {data.actualDemand !== undefined && ( -
- Demanda Real: - {data.actualDemand} -
- )} - - {data.predictedDemand !== undefined && ( -
- Demanda Predicha: - {data.predictedDemand.toFixed(1)} -
- )} - - {showConfidenceInterval && data.confidenceLower !== undefined && data.confidenceUpper !== undefined && ( -
- Intervalo de Confianza: - - {data.confidenceLower.toFixed(1)} - {data.confidenceUpper.toFixed(1)} - -
- )} - - {data.accuracy !== undefined && ( -
- Precisión: - 0.8 ? 'success' : data.accuracy > 0.6 ? 'warning' : 'danger'} - size="sm" - > - {(data.accuracy * 100).toFixed(1)}% - -
- )} -
- - {showEvents && data.hasEvent && ( -
-
- {EVENT_ICONS[data.eventType!]} -
-
{data.eventName}
-
- {SPANISH_EVENT_NAMES[data.eventType!]} -
-
- - {data.eventImpact === 'high' ? 'Alto' : data.eventImpact === 'medium' ? 'Medio' : 'Bajo'} - -
-
- )} - - {showWeatherOverlay && data.weather && ( -
-
- {WEATHER_ICONS[data.weather]} -
-
- {data.weather.replace('_', ' ')} -
- {data.temperature && ( -
- {data.temperature}°C -
- )} -
-
-
- )} -
- ); - }; - - // Event reference dots - const renderEventDots = () => { - if (!showEvents) return null; - - return events - .filter(event => { - const eventData = zoomedData.find(d => d.date === event.date); - return eventData; - }) - .map((event, index) => { - const eventData = zoomedData.find(d => d.date === event.date); - if (!eventData) return null; - - const yValue = eventData.predictedDemand || eventData.actualDemand || 0; - - return ( - - ); - }); - }; - - // Loading state - if (loading) { - return ( - - -

{title}

-
- -
-
-
- Cargando gráfico de demanda... -
-
-
-
- ); - } - - // Error state - if (error) { - return ( - - -

{title}

-
- -
-
-
- - - -
-

{error}

-
-
-
-
- ); - } - - // Empty state - if (zoomedData.length === 0) { - return ( - - -

{title}

-
- -
-
-
- - - -
-

No hay datos de demanda disponibles

-
-
-
-
- ); - } - - return ( - - -
-

{title}

- -
- {/* Time period selector */} - - - {/* Export options */} - {onExport && ( -
- - -
- )} - - {/* Reset zoom */} - {zoomedData.length !== filteredData.length && ( - - )} -
-
-
- - -
- - - - { - const date = new Date(value); - return timeframe === 'weekly' - ? date.toLocaleDateString('es-ES', { month: 'short', day: 'numeric' }) - : timeframe === 'monthly' - ? date.toLocaleDateString('es-ES', { month: 'short', year: '2-digit' }) - : date.getFullYear().toString(); - }} - /> - `${value}`} - /> - } /> - - - {/* Confidence interval area */} - {showConfidenceInterval && ( - - )} - {showConfidenceInterval && ( - - )} - - {/* Actual demand line */} - - - {/* Predicted demand line */} - - - {renderEventDots()} - - -
- - {/* Brush for zooming */} - {filteredData.length > 20 && ( -
- - - - { - const date = new Date(value); - return date.toLocaleDateString('es-ES', { month: 'short', day: 'numeric' }); - }} - /> - - -
- )} - - {/* Chart legend */} -
-
-
- Demanda Real -
-
-
- Demanda Predicha -
- {showConfidenceInterval && ( -
-
- Intervalo de Confianza -
- )} - {showEvents && events.length > 0 && ( -
-
- Eventos -
- )} -
-
-
- ); -}; - -export default DemandChart; \ No newline at end of file diff --git a/frontend/src/components/domain/forecasting/ForecastTable.tsx.backup b/frontend/src/components/domain/forecasting/ForecastTable.tsx.backup deleted file mode 100644 index 7127a131..00000000 --- a/frontend/src/components/domain/forecasting/ForecastTable.tsx.backup +++ /dev/null @@ -1,653 +0,0 @@ -import React, { useState, useMemo, useCallback } from 'react'; -import { clsx } from 'clsx'; -import { Card, CardHeader, CardBody } from '../../ui'; -import { Button } from '../../ui'; -import { Badge } from '../../ui'; -import { Select } from '../../ui'; -import { Input } from '../../ui'; -import { Table, TableColumn } from '../../ui'; -import { - ForecastResponse, - TrendDirection, - ModelType, -} from '../../../types/forecasting.types'; - -export interface ForecastTableProps { - className?: string; - title?: string; - data?: ForecastResponse[]; - loading?: boolean; - error?: string | null; - pageSize?: number; - onExportCSV?: (data: ForecastResponse[]) => void; - onBulkAction?: (action: string, selectedItems: ForecastResponse[]) => void; - onProductClick?: (productName: string) => void; - showBulkActions?: boolean; - showFilters?: boolean; - compact?: boolean; -} - -interface FilterState { - productName: string; - category: string; - accuracyRange: 'all' | 'high' | 'medium' | 'low'; - trendDirection: TrendDirection | 'all'; - confidenceLevel: number | 'all'; - dateRange: 'today' | 'week' | 'month' | 'quarter' | 'all'; -} - -interface SortState { - field: string; - order: 'asc' | 'desc'; -} - -const SPANISH_TRENDS: Record = { - [TrendDirection.INCREASING]: 'Creciente', - [TrendDirection.DECREASING]: 'Decreciente', - [TrendDirection.STABLE]: 'Estable', - [TrendDirection.VOLATILE]: 'Volátil', - [TrendDirection.SEASONAL]: 'Estacional', -}; - -const TREND_COLORS: Record = { - [TrendDirection.INCREASING]: 'text-green-600', - [TrendDirection.DECREASING]: 'text-red-600', - [TrendDirection.STABLE]: 'text-blue-600', - [TrendDirection.VOLATILE]: 'text-yellow-600', - [TrendDirection.SEASONAL]: 'text-purple-600', -}; - -const TREND_ICONS: Record = { - [TrendDirection.INCREASING]: '↗️', - [TrendDirection.DECREASING]: '↘️', - [TrendDirection.STABLE]: '➡️', - [TrendDirection.VOLATILE]: '📈', - [TrendDirection.SEASONAL]: '🔄', -}; - -const BAKERY_CATEGORIES = [ - { value: 'all', label: 'Todas las categorías' }, - { value: 'pan', label: 'Pan y Bollería' }, - { value: 'dulces', label: 'Dulces y Postres' }, - { value: 'salados', label: 'Salados' }, - { value: 'estacional', label: 'Productos Estacionales' }, - { value: 'bebidas', label: 'Bebidas' }, -]; - -const SPANISH_PRODUCTS = [ - 'Croissants', - 'Pan de molde', - 'Roscón de Reyes', - 'Torrijas', - 'Magdalenas', - 'Empanadas', - 'Tarta de Santiago', - 'Mazapán', - 'Pan tostado', - 'Palmeras', - 'Napolitanas', - 'Ensaimadas', - 'Churros', - 'Polvorones', - 'Turrones', -]; - -const ForecastTable: React.FC = ({ - className, - title = 'Predicciones de Demanda', - data = [], - loading = false, - error = null, - pageSize = 20, - onExportCSV, - onBulkAction, - onProductClick, - showBulkActions = true, - showFilters = true, - compact = false, -}) => { - const [filters, setFilters] = useState({ - productName: '', - category: 'all', - accuracyRange: 'all', - trendDirection: 'all', - confidenceLevel: 'all', - dateRange: 'all', - }); - - const [sort, setSort] = useState({ - field: 'forecast_date', - order: 'desc', - }); - - const [selectedRows, setSelectedRows] = useState([]); - const [currentPage, setCurrentPage] = useState(1); - - // Get product category (simplified mapping) - const getProductCategory = useCallback((productName: string): string => { - const name = productName.toLowerCase(); - if (name.includes('pan') || name.includes('croissant') || name.includes('magdalena')) return 'pan'; - if (name.includes('roscón') || name.includes('tarta') || name.includes('mazapán') || name.includes('polvorón')) return 'dulces'; - if (name.includes('empanada') || name.includes('churro')) return 'salados'; - if (name.includes('torrija') || name.includes('turrón')) return 'estacional'; - return 'pan'; - }, []); - - // Filter and sort data - const filteredAndSortedData = useMemo(() => { - let filtered = [...data]; - - // Apply filters - if (filters.productName) { - filtered = filtered.filter(item => - item.product_name.toLowerCase().includes(filters.productName.toLowerCase()) - ); - } - - if (filters.category !== 'all') { - filtered = filtered.filter(item => getProductCategory(item.product_name) === filters.category); - } - - if (filters.accuracyRange !== 'all') { - filtered = filtered.filter(item => { - const accuracy = item.accuracy_score || 0; - switch (filters.accuracyRange) { - case 'high': return accuracy >= 0.8; - case 'medium': return accuracy >= 0.6 && accuracy < 0.8; - case 'low': return accuracy < 0.6; - default: return true; - } - }); - } - - if (filters.trendDirection !== 'all') { - filtered = filtered.filter(item => { - // Determine trend from predicted vs historical (simplified) - const trend = item.predicted_demand > (item.actual_demand || 0) ? TrendDirection.INCREASING : TrendDirection.DECREASING; - return trend === filters.trendDirection; - }); - } - - if (filters.confidenceLevel !== 'all') { - filtered = filtered.filter(item => item.confidence_level >= filters.confidenceLevel); - } - - if (filters.dateRange !== 'all') { - const now = new Date(); - const filterDate = new Date(); - - switch (filters.dateRange) { - case 'today': - filterDate.setHours(0, 0, 0, 0); - break; - case 'week': - filterDate.setDate(now.getDate() - 7); - break; - case 'month': - filterDate.setMonth(now.getMonth() - 1); - break; - case 'quarter': - filterDate.setMonth(now.getMonth() - 3); - break; - } - - filtered = filtered.filter(item => new Date(item.forecast_date) >= filterDate); - } - - // Apply sorting - filtered.sort((a, b) => { - let aValue: any; - let bValue: any; - - switch (sort.field) { - case 'product_name': - aValue = a.product_name; - bValue = b.product_name; - break; - case 'predicted_demand': - aValue = a.predicted_demand; - bValue = b.predicted_demand; - break; - case 'accuracy_score': - aValue = a.accuracy_score || 0; - bValue = b.accuracy_score || 0; - break; - case 'confidence_level': - aValue = a.confidence_level; - bValue = b.confidence_level; - break; - case 'forecast_date': - aValue = new Date(a.forecast_date); - bValue = new Date(b.forecast_date); - break; - default: - return 0; - } - - if (aValue < bValue) return sort.order === 'asc' ? -1 : 1; - if (aValue > bValue) return sort.order === 'asc' ? 1 : -1; - return 0; - }); - - return filtered; - }, [data, filters, sort, getProductCategory]); - - // Paginated data - const paginatedData = useMemo(() => { - const startIndex = (currentPage - 1) * pageSize; - return filteredAndSortedData.slice(startIndex, startIndex + pageSize); - }, [filteredAndSortedData, currentPage, pageSize]); - - // Handle sort change - const handleSort = useCallback((field: string, order: 'asc' | 'desc' | null) => { - if (order === null) { - setSort({ field: 'forecast_date', order: 'desc' }); - } else { - setSort({ field, order }); - } - }, []); - - // Handle filter change - const handleFilterChange = useCallback((key: keyof FilterState, value: any) => { - setFilters(prev => ({ ...prev, [key]: value })); - setCurrentPage(1); // Reset to first page when filtering - }, []); - - // Handle bulk actions - const handleBulkAction = useCallback((action: string) => { - const selectedData = filteredAndSortedData.filter(item => - selectedRows.includes(item.id) - ); - onBulkAction?.(action, selectedData); - }, [filteredAndSortedData, selectedRows, onBulkAction]); - - // Calculate accuracy percentage and trend - const getAccuracyInfo = useCallback((item: ForecastResponse) => { - const accuracy = item.accuracy_score || 0; - const percentage = (accuracy * 100).toFixed(1); - - let variant: 'success' | 'warning' | 'danger' = 'success'; - if (accuracy < 0.6) variant = 'danger'; - else if (accuracy < 0.8) variant = 'warning'; - - return { percentage, variant }; - }, []); - - // Get trend info - const getTrendInfo = useCallback((item: ForecastResponse) => { - // Simplified trend calculation - const trend = item.predicted_demand > (item.actual_demand || 0) ? - TrendDirection.INCREASING : TrendDirection.DECREASING; - - return { - direction: trend, - label: SPANISH_TRENDS[trend], - color: TREND_COLORS[trend], - icon: TREND_ICONS[trend], - }; - }, []); - - // Table columns - const columns: TableColumn[] = useMemo(() => { - const baseColumns: TableColumn[] = [ - { - key: 'product_name', - title: 'Producto', - dataIndex: 'product_name', - sortable: true, - width: compact ? 120 : 160, - render: (value: string, record: ForecastResponse) => ( -
- - - {getProductCategory(value)} - -
- ), - }, - { - key: 'predicted_demand', - title: 'Demanda Predicha', - dataIndex: 'predicted_demand', - sortable: true, - align: 'right' as const, - width: compact ? 100 : 120, - render: (value: number) => ( -
- {value.toFixed(0)} -
- ), - }, - { - key: 'actual_demand', - title: 'Demanda Real', - dataIndex: 'actual_demand', - align: 'right' as const, - width: compact ? 80 : 100, - render: (value?: number) => ( -
- {value ? value.toFixed(0) : '-'} -
- ), - }, - { - key: 'accuracy_score', - title: 'Precisión', - dataIndex: 'accuracy_score', - sortable: true, - align: 'center' as const, - width: 80, - render: (value: number | undefined, record: ForecastResponse) => { - const { percentage, variant } = getAccuracyInfo(record); - return ( - - {percentage}% - - ); - }, - }, - { - key: 'trend', - title: 'Tendencia', - align: 'center' as const, - width: compact ? 80 : 100, - render: (_, record: ForecastResponse) => { - const trend = getTrendInfo(record); - return ( -
- {trend.icon} - - {compact ? '' : trend.label} - -
- ); - }, - }, - { - key: 'confidence_interval', - title: 'Intervalo de Confianza', - align: 'center' as const, - width: compact ? 120 : 140, - render: (_, record: ForecastResponse) => ( -
- {record.confidence_lower.toFixed(0)} - {record.confidence_upper.toFixed(0)} -
- ({(record.confidence_level * 100).toFixed(0)}%) -
-
- ), - }, - ]; - - if (!compact) { - baseColumns.push( - { - key: 'forecast_date', - title: 'Fecha de Predicción', - dataIndex: 'forecast_date', - sortable: true, - width: 120, - render: (value: string) => ( -
- {new Date(value).toLocaleDateString('es-ES', { - day: 'numeric', - month: 'short', - year: '2-digit', - })} -
- ), - }, - { - key: 'model_version', - title: 'Modelo', - dataIndex: 'model_version', - width: 80, - render: (value: string) => ( - - v{value} - - ), - } - ); - } - - return baseColumns; - }, [compact, onProductClick, getProductCategory, getAccuracyInfo, getTrendInfo]); - - // Loading state - if (loading) { - return ( - - -

{title}

-
- -
-
-
- Cargando predicciones... -
-
-
-
- ); - } - - // Error state - if (error) { - return ( - - -

{title}

-
- -
-
-
- - - -
-

{error}

-
-
-
-
- ); - } - - return ( - - -
-
-

{title}

- - {filteredAndSortedData.length} predicciones - -
- -
- {onExportCSV && ( - - )} - - {showBulkActions && selectedRows.length > 0 && ( -
- - -
- )} -
-
-
- - {showFilters && ( -
-
- handleFilterChange('productName', e.target.value)} - className="text-sm" - /> - - - - - - - - - - -
-
- )} - - -
{ - setSelectedRows(selected ? selectedRows.map(row => row.id) : []); - }, - onSelect: (record, selected) => { - setSelectedRows(prev => - selected - ? [...prev, record.id] - : prev.filter(key => key !== record.id) - ); - }, - } : undefined} - pagination={{ - current: currentPage, - pageSize, - total: filteredAndSortedData.length, - showSizeChanger: true, - showTotal: (total, range) => - `${range[0]}-${range[1]} de ${total} predicciones`, - onChange: (page, size) => { - setCurrentPage(page); - }, - }} - sort={{ - field: sort.field, - order: sort.order, - }} - onSort={handleSort} - size={compact ? 'sm' : 'md'} - hover={true} - expandable={!compact ? { - expandedRowRender: (record) => ( -
-
-
-
ID de Predicción
-
{record.id}
-
-
-
Fecha de Creación
-
- {new Date(record.created_at).toLocaleString('es-ES')} -
-
-
-
Factores Externos
-
- {Object.entries(record.external_factors_impact).length > 0 - ? Object.keys(record.external_factors_impact).join(', ') - : 'Ninguno' - } -
-
-
-
Componente Estacional
-
- {record.seasonal_component - ? (record.seasonal_component * 100).toFixed(1) + '%' - : 'N/A' - } -
-
-
-
- ), - } : undefined} - /> - - - ); -}; - -export default ForecastTable; \ No newline at end of file diff --git a/frontend/src/components/domain/forecasting/SeasonalityIndicator.tsx.backup b/frontend/src/components/domain/forecasting/SeasonalityIndicator.tsx.backup deleted file mode 100644 index 69e03786..00000000 --- a/frontend/src/components/domain/forecasting/SeasonalityIndicator.tsx.backup +++ /dev/null @@ -1,634 +0,0 @@ -import React, { useState, useMemo, useCallback } from 'react'; -import { clsx } from 'clsx'; -import { - RadialBarChart, - RadialBar, - ResponsiveContainer, - PieChart, - Pie, - Cell, - Tooltip, - BarChart, - Bar, - XAxis, - YAxis, - CartesianGrid, - ComposedChart, - Line, - LineChart, - ReferenceLine, -} from 'recharts'; -import { Card, CardHeader, CardBody } from '../../ui'; -import { Button } from '../../ui'; -import { Badge } from '../../ui'; -import { Select } from '../../ui'; -import { - SeasonalPattern, - SeasonalComponent, - HolidayEffect, - WeeklyPattern, - YearlyTrend, - Season, - SeasonalPeriod, - DayOfWeek, -} from '../../../types/forecasting.types'; - -export interface SeasonalityIndicatorProps { - className?: string; - title?: string; - seasonalPatterns?: SeasonalPattern[]; - selectedProduct?: string; - onProductChange?: (product: string) => void; - viewMode?: 'circular' | 'calendar' | 'heatmap' | 'trends'; - onViewModeChange?: (mode: 'circular' | 'calendar' | 'heatmap' | 'trends') => void; - showComparison?: boolean; - comparisonYear?: number; - loading?: boolean; - error?: string | null; -} - -interface MonthlyData { - month: string; - value: number; - strength: number; - color: string; - season: Season; - holidays: string[]; -} - -interface WeeklyData { - day: string; - dayShort: string; - value: number; - variance: number; - peakHours?: number[]; -} - -interface HeatmapData { - month: number; - week: number; - intensity: number; - value: number; - holiday?: string; -} - -const SPANISH_MONTHS = [ - 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', - 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre' -]; - -const SPANISH_DAYS = [ - 'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo' -]; - -const SPANISH_DAYS_SHORT = [ - 'L', 'M', 'X', 'J', 'V', 'S', 'D' -]; - -const SPANISH_SEASONS: Record = { - [Season.SPRING]: 'Primavera', - [Season.SUMMER]: 'Verano', - [Season.FALL]: 'Otoño', - [Season.WINTER]: 'Invierno', -}; - -const SEASON_COLORS: Record = { - [Season.SPRING]: '#22c55e', // Green - [Season.SUMMER]: '#f59e0b', // Amber - [Season.FALL]: '#ea580c', // Orange - [Season.WINTER]: '#3b82f6', // Blue -}; - -const SPANISH_HOLIDAYS = [ - { month: 0, name: 'Año Nuevo' }, - { month: 0, name: 'Día de Reyes' }, - { month: 2, name: 'Semana Santa' }, // March/April - { month: 4, name: 'Día del Trabajador' }, - { month: 7, name: 'Asunción' }, - { month: 9, name: 'Día Nacional' }, - { month: 10, name: 'Todos los Santos' }, - { month: 11, name: 'Constitución' }, - { month: 11, name: 'Navidad' }, -]; - -const INTENSITY_COLORS = [ - '#f3f4f6', // Very low - '#dbeafe', // Low - '#bfdbfe', // Medium-low - '#93c5fd', // Medium - '#60a5fa', // Medium-high - '#3b82f6', // High - '#2563eb', // Very high - '#1d4ed8', // Extremely high -]; - -const SeasonalityIndicator: React.FC = ({ - className, - title = 'Patrones Estacionales', - seasonalPatterns = [], - selectedProduct = '', - onProductChange, - viewMode = 'circular', - onViewModeChange, - showComparison = false, - comparisonYear = new Date().getFullYear() - 1, - loading = false, - error = null, -}) => { - const [hoveredElement, setHoveredElement] = useState(null); - - // Get current pattern data - const currentPattern = useMemo(() => { - if (!selectedProduct || seasonalPatterns.length === 0) { - return seasonalPatterns[0] || null; - } - return seasonalPatterns.find(p => p.product_name === selectedProduct) || seasonalPatterns[0]; - }, [seasonalPatterns, selectedProduct]); - - // Process monthly seasonal data - const monthlyData = useMemo((): MonthlyData[] => { - if (!currentPattern) return []; - - const monthlyComponent = currentPattern.seasonal_components.find( - c => c.period === SeasonalPeriod.MONTHLY - ); - - if (!monthlyComponent) return []; - - return SPANISH_MONTHS.map((month, index) => { - const value = monthlyComponent.pattern[index] || 0; - const strength = Math.abs(value); - - // Determine season - let season: Season; - if (index >= 2 && index <= 4) season = Season.SPRING; - else if (index >= 5 && index <= 7) season = Season.SUMMER; - else if (index >= 8 && index <= 10) season = Season.FALL; - else season = Season.WINTER; - - // Get holidays for this month - const holidays = SPANISH_HOLIDAYS - .filter(h => h.month === index) - .map(h => h.name); - - return { - month, - value: value * 100, // Convert to percentage - strength: strength * 100, - color: SEASON_COLORS[season], - season, - holidays, - }; - }); - }, [currentPattern]); - - // Process weekly data - const weeklyData = useMemo((): WeeklyData[] => { - if (!currentPattern) return []; - - return currentPattern.weekly_patterns.map((pattern, index) => ({ - day: SPANISH_DAYS[index] || `Día ${index + 1}`, - dayShort: SPANISH_DAYS_SHORT[index] || `D${index + 1}`, - value: pattern.average_multiplier * 100, - variance: pattern.variance * 100, - peakHours: pattern.peak_hours, - })); - }, [currentPattern]); - - // Process heatmap data - const heatmapData = useMemo((): HeatmapData[] => { - if (!currentPattern) return []; - - const data: HeatmapData[] = []; - const monthlyComponent = currentPattern.seasonal_components.find( - c => c.period === SeasonalPeriod.MONTHLY - ); - - if (monthlyComponent) { - for (let month = 0; month < 12; month++) { - for (let week = 0; week < 4; week++) { - const value = monthlyComponent.pattern[month] || 0; - const intensity = Math.min(Math.max(Math.abs(value) * 8, 0), 7); // 0-7 scale - - const holiday = SPANISH_HOLIDAYS.find(h => h.month === month); - - data.push({ - month, - week, - intensity: Math.floor(intensity), - value: value * 100, - holiday: holiday?.name, - }); - } - } - } - - return data; - }, [currentPattern]); - - // Custom tooltip for radial chart - const RadialTooltip = ({ active, payload }: any) => { - if (!active || !payload || !payload.length) return null; - - const data = payload[0].payload as MonthlyData; - - return ( -
-

{data.month}

-
-
- Estación: - - {SPANISH_SEASONS[data.season]} - -
-
- Variación: - {data.value.toFixed(1)}% -
-
- Intensidad: - {data.strength.toFixed(1)}% -
- {data.holidays.length > 0 && ( -
-
Festividades:
-
- {data.holidays.map(holiday => ( - - {holiday} - - ))} -
-
- )} -
-
- ); - }; - - // Circular view (Radial chart) - const renderCircularView = () => ( -
- - - entry.color} - /> - } /> - - -
- ); - - // Calendar view (Bar chart by month) - const renderCalendarView = () => ( -
- - - - - - { - if (!active || !payload || !payload.length) return null; - const data = payload[0].payload as MonthlyData; - return ; - }} - /> - monthlyData[index]?.color || '#8884d8'} - /> - - - - -
- ); - - // Heatmap view - const renderHeatmapView = () => ( -
- {/* Month labels */} -
- {SPANISH_MONTHS.map(month => ( -
- {month.slice(0, 3)} -
- ))} -
- - {/* Heatmap grid */} - {[0, 1, 2, 3].map(week => ( -
- {heatmapData - .filter(d => d.week === week) - .map((cell, monthIndex) => ( -
setHoveredElement(cell)} - onMouseLeave={() => setHoveredElement(null)} - title={`${SPANISH_MONTHS[cell.month]} S${cell.week + 1}: ${cell.value.toFixed(1)}%`} - > - {cell.holiday && ( -
🎉
- )} -
- ))} -
- ))} - - {/* Legend */} -
- Baja -
- {INTENSITY_COLORS.map((color, index) => ( -
- ))} -
- Alta -
-
- ); - - // Trends view (Weekly patterns) - const renderTrendsView = () => ( -
- - - - - - { - if (!active || !payload || !payload.length) return null; - const data = payload[0].payload as WeeklyData; - return ( -
-

{data.day}

-
-
- Multiplicador Promedio: - {data.value.toFixed(1)}% -
-
- Varianza: - {data.variance.toFixed(1)}% -
- {data.peakHours && data.peakHours.length > 0 && ( -
- Horas Pico: - - {data.peakHours.map(h => `${h}:00`).join(', ')} - -
- )} -
-
- ); - }} - /> - -
-
-
- ); - - // Loading state - if (loading) { - return ( - - -

{title}

-
- -
-
-
- Cargando patrones estacionales... -
-
-
-
- ); - } - - // Error state - if (error) { - return ( - - -

{title}

-
- -
-
-
- - - -
-

{error}

-
-
-
-
- ); - } - - // Empty state - if (!currentPattern) { - return ( - - -

{title}

-
- -
-
-
- - - -
-

No hay datos de estacionalidad disponibles

-
-
-
-
- ); - } - - return ( - - -
-
-

{title}

- {currentPattern && ( - - {currentPattern.product_name} - - )} - - {(currentPattern.confidence_score * 100).toFixed(0)}% confianza - -
- -
- {/* Product selector */} - {onProductChange && seasonalPatterns.length > 1 && ( - - )} - - {/* View mode selector */} -
- {(['circular', 'calendar', 'heatmap', 'trends'] as const).map((mode) => ( - - ))} -
-
-
-
- - -
- {/* Main visualization */} -
- {viewMode === 'circular' && renderCircularView()} - {viewMode === 'calendar' && renderCalendarView()} - {viewMode === 'heatmap' && renderHeatmapView()} - {viewMode === 'trends' && renderTrendsView()} -
- - {/* Holiday effects summary */} - {currentPattern.holiday_effects && currentPattern.holiday_effects.length > 0 && ( -
-

Efectos de Festividades

-
- {currentPattern.holiday_effects.map((holiday, index) => ( -
-
- - {holiday.holiday_name} - - 1.2 ? 'success' : holiday.impact_factor < 0.8 ? 'danger' : 'warning'} - size="sm" - > - {((holiday.impact_factor - 1) * 100).toFixed(0)}% - -
-
-
Duración: {holiday.duration_days} días
-
Confianza: {(holiday.confidence * 100).toFixed(0)}%
-
-
- ))} -
-
- )} - - {/* Pattern strength indicators */} -
-

Intensidad de Patrones

-
- {currentPattern.seasonal_components.map((component, index) => { - const periodLabel = { - [SeasonalPeriod.WEEKLY]: 'Semanal', - [SeasonalPeriod.MONTHLY]: 'Mensual', - [SeasonalPeriod.QUARTERLY]: 'Trimestral', - [SeasonalPeriod.YEARLY]: 'Anual', - }[component.period] || component.period; - - return ( -
-
- {periodLabel} -
-
-
-
-
- - {(component.strength * 100).toFixed(0)}% - -
-
- ); - })} -
-
-
- - - ); -}; - -export default SeasonalityIndicator; \ No newline at end of file diff --git a/frontend/src/components/domain/onboarding/CompanyInfoStep.tsx.backup b/frontend/src/components/domain/onboarding/CompanyInfoStep.tsx.backup deleted file mode 100644 index abf35405..00000000 --- a/frontend/src/components/domain/onboarding/CompanyInfoStep.tsx.backup +++ /dev/null @@ -1,404 +0,0 @@ -import React from 'react'; -import { Input, Select, Badge } from '../../ui'; -import type { OnboardingStepProps } from './OnboardingWizard'; - -interface CompanyInfo { - name: string; - type: 'artisan' | 'industrial' | 'chain' | 'mixed'; - size: 'small' | 'medium' | 'large'; - locations: number; - specialties: string[]; - address: { - street: string; - city: string; - state: string; - postal_code: string; - country: string; - }; - contact: { - phone: string; - email: string; - website?: string; - }; - established_year?: number; - tax_id?: string; -} - -const BAKERY_TYPES = [ - { value: 'artisan', label: 'Artesanal', description: 'Producción tradicional y manual' }, - { value: 'industrial', label: 'Industrial', description: 'Producción automatizada a gran escala' }, - { value: 'chain', label: 'Cadena', description: 'Múltiples ubicaciones con procesos estandarizados' }, - { value: 'mixed', label: 'Mixta', description: 'Combinación de métodos artesanales e industriales' }, -]; - -const BAKERY_SIZES = [ - { value: 'small', label: 'Pequeña', description: '1-10 empleados' }, - { value: 'medium', label: 'Mediana', description: '11-50 empleados' }, - { value: 'large', label: 'Grande', description: '50+ empleados' }, -]; - -const COMMON_SPECIALTIES = [ - 'Pan tradicional', - 'Bollería', - 'Repostería', - 'Pan integral', - 'Pasteles', - 'Productos sin gluten', - 'Productos veganos', - 'Pan artesanal', - 'Productos de temporada', - 'Catering', -]; - -export const CompanyInfoStep: React.FC = ({ - data, - onDataChange, -}) => { - const companyData: CompanyInfo = { - name: '', - type: 'artisan', - size: 'small', - locations: 1, - specialties: [], - address: { - street: '', - city: '', - state: '', - postal_code: '', - country: 'España', - }, - contact: { - phone: '', - email: '', - website: '', - }, - ...data, - }; - - const handleInputChange = (field: string, value: any) => { - onDataChange({ - ...companyData, - [field]: value, - }); - }; - - const handleAddressChange = (field: string, value: string) => { - onDataChange({ - ...companyData, - address: { - ...companyData.address, - [field]: value, - }, - }); - }; - - const handleContactChange = (field: string, value: string) => { - onDataChange({ - ...companyData, - contact: { - ...companyData.contact, - [field]: value, - }, - }); - }; - - const handleSpecialtyToggle = (specialty: string) => { - const currentSpecialties = companyData.specialties || []; - const updatedSpecialties = currentSpecialties.includes(specialty) - ? currentSpecialties.filter(s => s !== specialty) - : [...currentSpecialties, specialty]; - - handleInputChange('specialties', updatedSpecialties); - }; - - return ( -
- {/* Basic Information */} -
-

- Información básica -

- -
-
- - handleInputChange('name', e.target.value)} - placeholder="Ej: Panadería San Miguel" - className="w-full" - /> -
- -
- - -
- -
- - -
- -
- - handleInputChange('locations', parseInt(e.target.value) || 1)} - className="w-full" - /> -
- -
- - handleInputChange('established_year', parseInt(e.target.value) || undefined)} - placeholder="Ej: 1995" - className="w-full" - /> -
- -
- - handleInputChange('tax_id', e.target.value)} - placeholder="Ej: B12345678" - className="w-full" - /> -
-
-
- - {/* Specialties */} -
-

- Especialidades -

-

- Selecciona los productos que produces habitualmente -

- -
- {COMMON_SPECIALTIES.map((specialty) => ( - - ))} -
- - {companyData.specialties && companyData.specialties.length > 0 && ( -
-

- Especialidades seleccionadas: -

-
- {companyData.specialties.map((specialty) => ( - - {specialty} - - - ))} -
-
- )} -
- - {/* Address */} -
-

- Dirección principal -

- -
-
- - handleAddressChange('street', e.target.value)} - placeholder="Calle, número, piso, puerta" - className="w-full" - /> -
- -
- - handleAddressChange('city', e.target.value)} - placeholder="Ej: Madrid" - className="w-full" - /> -
- -
- - handleAddressChange('state', e.target.value)} - placeholder="Ej: Madrid" - className="w-full" - /> -
- -
- - handleAddressChange('postal_code', e.target.value)} - placeholder="Ej: 28001" - className="w-full" - /> -
- -
- - -
-
-
- - {/* Contact Information */} -
-

- Información de contacto -

- -
-
- - handleContactChange('phone', e.target.value)} - placeholder="Ej: +34 911 234 567" - className="w-full" - /> -
- -
- - handleContactChange('email', e.target.value)} - placeholder="contacto@panaderia.com" - className="w-full" - /> -
- -
- - handleContactChange('website', e.target.value)} - placeholder="https://www.panaderia.com" - className="w-full" - /> -
-
-
- - {/* Summary */} -
-

Resumen

-
-

Panadería: {companyData.name || 'Sin especificar'}

-

Tipo: {BAKERY_TYPES.find(t => t.value === companyData.type)?.label}

-

Tamaño: {BAKERY_SIZES.find(s => s.value === companyData.size)?.label}

-

Especialidades: {companyData.specialties?.length || 0} seleccionadas

-

Ubicaciones: {companyData.locations}

-
-
-
- ); -}; - -export default CompanyInfoStep; \ No newline at end of file diff --git a/frontend/src/components/domain/onboarding/OnboardingWizard.tsx.backup b/frontend/src/components/domain/onboarding/OnboardingWizard.tsx.backup deleted file mode 100644 index 9df201f8..00000000 --- a/frontend/src/components/domain/onboarding/OnboardingWizard.tsx.backup +++ /dev/null @@ -1,255 +0,0 @@ -import React, { useState, useCallback } from 'react'; -import { Card, Button, Input, Select, Badge } from '../../ui'; - -export interface OnboardingStep { - id: string; - title: string; - description: string; - component: React.ComponentType; - isCompleted?: boolean; - isRequired?: boolean; - validation?: (data: any) => string | null; -} - -export interface OnboardingStepProps { - data: any; - onDataChange: (data: any) => void; - onNext: () => void; - onPrevious: () => void; - isFirstStep: boolean; - isLastStep: boolean; -} - -interface OnboardingWizardProps { - steps: OnboardingStep[]; - onComplete: (data: any) => void; - onExit?: () => void; - className?: string; -} - -export const OnboardingWizard: React.FC = ({ - steps, - onComplete, - onExit, - className = '', -}) => { - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [stepData, setStepData] = useState>({}); - const [completedSteps, setCompletedSteps] = useState>(new Set()); - const [validationErrors, setValidationErrors] = useState>({}); - - const currentStep = steps[currentStepIndex]; - - const updateStepData = useCallback((stepId: string, data: any) => { - setStepData(prev => ({ - ...prev, - [stepId]: { ...prev[stepId], ...data } - })); - - // Clear validation error for this step - setValidationErrors(prev => { - const newErrors = { ...prev }; - delete newErrors[stepId]; - return newErrors; - }); - }, []); - - const validateCurrentStep = useCallback(() => { - const step = currentStep; - const data = stepData[step.id] || {}; - - if (step.validation) { - const error = step.validation(data); - if (error) { - setValidationErrors(prev => ({ - ...prev, - [step.id]: error - })); - return false; - } - } - - // Mark step as completed - setCompletedSteps(prev => new Set(prev).add(step.id)); - return true; - }, [currentStep, stepData]); - - const goToNextStep = useCallback(() => { - if (validateCurrentStep()) { - if (currentStepIndex < steps.length - 1) { - setCurrentStepIndex(currentStepIndex + 1); - } else { - // All steps completed, call onComplete with all data - onComplete(stepData); - } - } - }, [currentStepIndex, steps.length, validateCurrentStep, onComplete, stepData]); - - const goToPreviousStep = useCallback(() => { - if (currentStepIndex > 0) { - setCurrentStepIndex(currentStepIndex - 1); - } - }, [currentStepIndex]); - - const goToStep = useCallback((stepIndex: number) => { - setCurrentStepIndex(stepIndex); - }, []); - - const calculateProgress = () => { - return (completedSteps.size / steps.length) * 100; - }; - - const renderStepIndicator = () => ( -
- {steps.map((step, index) => { - const isCompleted = completedSteps.has(step.id); - const isCurrent = index === currentStepIndex; - const hasError = validationErrors[step.id]; - - return ( -
- - -
-

- {step.title} - {step.isRequired && *} -

- {hasError && ( -

{hasError}

- )} -
- - {index < steps.length - 1 && ( -
- )} -
- ); - })} -
- ); - - const renderProgressBar = () => ( -
-
- - Progreso del onboarding - - - {completedSteps.size} de {steps.length} completados - -
-
-
-
-
- ); - - if (!currentStep) { - return ( - -

No hay pasos de onboarding configurados.

-
- ); - } - - const StepComponent = currentStep.component; - - return ( -
- {/* Header */} -
-
-

- Configuración inicial -

-

- Completa estos pasos para comenzar a usar la plataforma -

-
- - {onExit && ( - - )} -
- - {renderProgressBar()} - {renderStepIndicator()} - - {/* Current Step Content */} - -
-

- {currentStep.title} -

-

- {currentStep.description} -

-
- - updateStepData(currentStep.id, data)} - onNext={goToNextStep} - onPrevious={goToPreviousStep} - isFirstStep={currentStepIndex === 0} - isLastStep={currentStepIndex === steps.length - 1} - /> -
- - {/* Navigation */} -
- - -
- - Paso {currentStepIndex + 1} de {steps.length} - -
- - -
-
- ); -}; - -export default OnboardingWizard; \ No newline at end of file diff --git a/frontend/src/components/domain/onboarding/SystemSetupStep.tsx.backup b/frontend/src/components/domain/onboarding/SystemSetupStep.tsx.backup deleted file mode 100644 index 0cbd9689..00000000 --- a/frontend/src/components/domain/onboarding/SystemSetupStep.tsx.backup +++ /dev/null @@ -1,556 +0,0 @@ -import React from 'react'; -import { Card, Button, Input, Select, Badge } from '../../ui'; -import type { OnboardingStepProps } from './OnboardingWizard'; - -interface SystemConfig { - timezone: string; - currency: string; - language: string; - date_format: string; - number_format: string; - working_hours: { - start: string; - end: string; - days: number[]; - }; - notifications: { - email_enabled: boolean; - sms_enabled: boolean; - push_enabled: boolean; - alert_preferences: string[]; - }; - integrations: { - pos_system?: string; - accounting_software?: string; - payment_provider?: string; - }; - features: { - inventory_management: boolean; - production_planning: boolean; - sales_analytics: boolean; - customer_management: boolean; - financial_reporting: boolean; - quality_control: boolean; - }; -} - -const TIMEZONES = [ - { value: 'Europe/Madrid', label: 'Madrid (GMT+1)' }, - { value: 'Europe/London', label: 'London (GMT+0)' }, - { value: 'Europe/Paris', label: 'Paris (GMT+1)' }, - { value: 'America/New_York', label: 'New York (GMT-5)' }, - { value: 'America/Los_Angeles', label: 'Los Angeles (GMT-8)' }, - { value: 'America/Mexico_City', label: 'Mexico City (GMT-6)' }, - { value: 'America/Buenos_Aires', label: 'Buenos Aires (GMT-3)' }, -]; - -const CURRENCIES = [ - { value: 'EUR', label: 'Euro (€)', symbol: '€' }, - { value: 'USD', label: 'US Dollar ($)', symbol: '$' }, - { value: 'GBP', label: 'British Pound (£)', symbol: '£' }, - { value: 'MXN', label: 'Mexican Peso ($)', symbol: '$' }, - { value: 'ARS', label: 'Argentine Peso ($)', symbol: '$' }, - { value: 'COP', label: 'Colombian Peso ($)', symbol: '$' }, -]; - -const LANGUAGES = [ - { value: 'es', label: 'Español' }, - { value: 'en', label: 'English' }, - { value: 'fr', label: 'Français' }, - { value: 'pt', label: 'Português' }, - { value: 'it', label: 'Italiano' }, -]; - -const DAYS_OF_WEEK = [ - { value: 1, label: 'Lunes', short: 'L' }, - { value: 2, label: 'Martes', short: 'M' }, - { value: 3, label: 'Miércoles', short: 'X' }, - { value: 4, label: 'Jueves', short: 'J' }, - { value: 5, label: 'Viernes', short: 'V' }, - { value: 6, label: 'Sábado', short: 'S' }, - { value: 0, label: 'Domingo', short: 'D' }, -]; - -const ALERT_TYPES = [ - { value: 'low_stock', label: 'Stock bajo', description: 'Cuando los ingredientes están por debajo del mínimo' }, - { value: 'production_delays', label: 'Retrasos de producción', description: 'Cuando los lotes se retrasan' }, - { value: 'quality_issues', label: 'Problemas de calidad', description: 'Cuando se detectan problemas de calidad' }, - { value: 'financial_targets', label: 'Objetivos financieros', description: 'Cuando se alcanzan o no se cumplen objetivos' }, - { value: 'equipment_maintenance', label: 'Mantenimiento de equipos', description: 'Recordatorios de mantenimiento' }, - { value: 'food_safety', label: 'Seguridad alimentaria', description: 'Alertas relacionadas con seguridad alimentaria' }, -]; - -const FEATURES = [ - { - key: 'inventory_management', - title: 'Gestión de inventario', - description: 'Control de stock, ingredientes y materias primas', - recommended: true, - }, - { - key: 'production_planning', - title: 'Planificación de producción', - description: 'Programación de lotes y gestión de recetas', - recommended: true, - }, - { - key: 'sales_analytics', - title: 'Analytics de ventas', - description: 'Reportes y análisis de ventas y tendencias', - recommended: true, - }, - { - key: 'customer_management', - title: 'Gestión de clientes', - description: 'Base de datos de clientes y programa de fidelización', - recommended: false, - }, - { - key: 'financial_reporting', - title: 'Reportes financieros', - description: 'Análisis de costos, márgenes y rentabilidad', - recommended: true, - }, - { - key: 'quality_control', - title: 'Control de calidad', - description: 'Seguimiento de calidad y estándares', - recommended: true, - }, -]; - -export const SystemSetupStep: React.FC = ({ - data, - onDataChange, -}) => { - const systemData: SystemConfig = { - timezone: 'Europe/Madrid', - currency: 'EUR', - language: 'es', - date_format: 'DD/MM/YYYY', - number_format: 'European', - working_hours: { - start: '06:00', - end: '20:00', - days: [1, 2, 3, 4, 5, 6], // Monday to Saturday - }, - notifications: { - email_enabled: true, - sms_enabled: false, - push_enabled: true, - alert_preferences: ['low_stock', 'production_delays', 'quality_issues'], - }, - integrations: {}, - features: { - inventory_management: true, - production_planning: true, - sales_analytics: true, - customer_management: false, - financial_reporting: true, - quality_control: true, - }, - ...data, - }; - - const handleInputChange = (field: string, value: any) => { - onDataChange({ - ...systemData, - [field]: value, - }); - }; - - const handleWorkingHoursChange = (field: string, value: any) => { - onDataChange({ - ...systemData, - working_hours: { - ...systemData.working_hours, - [field]: value, - }, - }); - }; - - const handleNotificationChange = (field: string, value: any) => { - onDataChange({ - ...systemData, - notifications: { - ...systemData.notifications, - [field]: value, - }, - }); - }; - - const handleIntegrationChange = (field: string, value: string) => { - onDataChange({ - ...systemData, - integrations: { - ...systemData.integrations, - [field]: value, - }, - }); - }; - - const handleFeatureToggle = (feature: string) => { - onDataChange({ - ...systemData, - features: { - ...systemData.features, - [feature]: !systemData.features[feature as keyof typeof systemData.features], - }, - }); - }; - - const toggleWorkingDay = (day: number) => { - const currentDays = systemData.working_hours.days; - const updatedDays = currentDays.includes(day) - ? currentDays.filter(d => d !== day) - : [...currentDays, day].sort(); - - handleWorkingHoursChange('days', updatedDays); - }; - - const toggleAlertPreference = (alert: string) => { - const current = systemData.notifications.alert_preferences; - const updated = current.includes(alert) - ? current.filter(a => a !== alert) - : [...current, alert]; - - handleNotificationChange('alert_preferences', updated); - }; - - return ( -
- {/* Regional Settings */} -
-

- Configuración regional -

- -
-
- - -
- -
- - -
- -
- - -
- -
- - -
-
-
- - {/* Working Hours */} -
-

- Horario de trabajo -

- -
-
- - handleWorkingHoursChange('start', e.target.value)} - className="w-full" - /> -
- -
- - handleWorkingHoursChange('end', e.target.value)} - className="w-full" - /> -
-
- -
- -
- {DAYS_OF_WEEK.map((day) => ( - - ))} -
-
-
- - {/* Features */} -
-

- Módulos y características -

-

- Selecciona las características que quieres activar. Podrás cambiar esto más tarde. -

- -
- {FEATURES.map((feature) => ( - handleFeatureToggle(feature.key)} - > -
-
-
-

{feature.title}

- {feature.recommended && ( - - Recomendado - - )} -
-

{feature.description}

-
- -
- {systemData.features[feature.key as keyof typeof systemData.features] && ( - - )} -
-
-
- ))} -
-
- - {/* Notifications */} -
-

- Notificaciones -

- -
-
- - - - - -
- -
- -
- {ALERT_TYPES.map((alert) => ( -
toggleAlertPreference(alert.value)} - > -
-
-
{alert.label}
-

{alert.description}

-
-
- {systemData.notifications.alert_preferences.includes(alert.value) && ( - - )} -
-
-
- ))} -
-
-
-
- - {/* Integrations */} -
-

- Integraciones (opcional) -

-

- Estas integraciones se pueden configurar más tarde desde el panel de administración. -

- -
-
- - -
- -
- - -
- -
- - -
-
-
- - {/* Summary */} -
-

Configuración seleccionada

-
-

Zona horaria: {TIMEZONES.find(tz => tz.value === systemData.timezone)?.label}

-

Moneda: {CURRENCIES.find(c => c.value === systemData.currency)?.label}

-

Horario: {systemData.working_hours.start} - {systemData.working_hours.end}

-

Días operativos: {systemData.working_hours.days.length} días por semana

-

Módulos activados: {Object.values(systemData.features).filter(Boolean).length} de {Object.keys(systemData.features).length}

-
-
-
- ); -}; - -export default SystemSetupStep; \ No newline at end of file diff --git a/frontend/src/components/domain/production/BatchTracker.tsx.backup b/frontend/src/components/domain/production/BatchTracker.tsx.backup deleted file mode 100644 index c794e86b..00000000 --- a/frontend/src/components/domain/production/BatchTracker.tsx.backup +++ /dev/null @@ -1,680 +0,0 @@ -import React, { useState, useCallback, useEffect } from 'react'; -import { Card, Button, Badge, Input, Modal, Table, Select, DatePicker } from '../../ui'; -import { productionService, type ProductionBatchResponse, ProductionBatchStatus, ProductionPriority } from '../../../services/api/production.service'; -import type { ProductionBatch, QualityCheck } from '../../../types/production.types'; - -interface BatchTrackerProps { - className?: string; - batchId?: string; - onStageUpdate?: (batch: ProductionBatch, newStage: ProductionStage) => void; - onQualityCheckRequired?: (batch: ProductionBatch, stage: ProductionStage) => void; -} - -interface ProductionStage { - id: string; - name: string; - spanishName: string; - icon: string; - estimatedMinutes: number; - requiresQualityCheck: boolean; - criticalControlPoint: boolean; - completedAt?: string; - notes?: string; - nextStages: string[]; - temperature?: { - min: number; - max: number; - unit: string; - }; -} - -const PRODUCTION_STAGES: Record = { - mixing: { - id: 'mixing', - name: 'Mixing', - spanishName: 'Amasado', - icon: '🥄', - estimatedMinutes: 15, - requiresQualityCheck: true, - criticalControlPoint: true, - nextStages: ['resting'], - temperature: { min: 22, max: 26, unit: '°C' }, - }, - resting: { - id: 'resting', - name: 'Resting', - spanishName: 'Reposo', - icon: '⏰', - estimatedMinutes: 60, - requiresQualityCheck: false, - criticalControlPoint: false, - nextStages: ['shaping', 'fermentation'], - }, - shaping: { - id: 'shaping', - name: 'Shaping', - spanishName: 'Formado', - icon: '✋', - estimatedMinutes: 20, - requiresQualityCheck: true, - criticalControlPoint: false, - nextStages: ['fermentation'], - }, - fermentation: { - id: 'fermentation', - name: 'Fermentation', - spanishName: 'Fermentado', - icon: '🫧', - estimatedMinutes: 120, - requiresQualityCheck: true, - criticalControlPoint: true, - nextStages: ['baking'], - temperature: { min: 28, max: 32, unit: '°C' }, - }, - baking: { - id: 'baking', - name: 'Baking', - spanishName: 'Horneado', - icon: '🔥', - estimatedMinutes: 45, - requiresQualityCheck: true, - criticalControlPoint: true, - nextStages: ['cooling'], - temperature: { min: 180, max: 220, unit: '°C' }, - }, - cooling: { - id: 'cooling', - name: 'Cooling', - spanishName: 'Enfriado', - icon: '❄️', - estimatedMinutes: 90, - requiresQualityCheck: false, - criticalControlPoint: false, - nextStages: ['packaging'], - temperature: { min: 18, max: 25, unit: '°C' }, - }, - packaging: { - id: 'packaging', - name: 'Packaging', - spanishName: 'Empaquetado', - icon: '📦', - estimatedMinutes: 15, - requiresQualityCheck: true, - criticalControlPoint: false, - nextStages: ['completed'], - }, - completed: { - id: 'completed', - name: 'Completed', - spanishName: 'Completado', - icon: '✅', - estimatedMinutes: 0, - requiresQualityCheck: false, - criticalControlPoint: false, - nextStages: [], - }, -}; - -const STATUS_COLORS = { - [ProductionBatchStatus.PLANNED]: 'bg-blue-100 text-blue-800 border-blue-200', - [ProductionBatchStatus.IN_PROGRESS]: 'bg-yellow-100 text-yellow-800 border-yellow-200', - [ProductionBatchStatus.COMPLETED]: 'bg-green-100 text-green-800 border-green-200', - [ProductionBatchStatus.CANCELLED]: 'bg-red-100 text-red-800 border-red-200', - [ProductionBatchStatus.ON_HOLD]: 'bg-gray-100 text-gray-800 border-gray-200', -}; - -const PRIORITY_COLORS = { - [ProductionPriority.LOW]: 'bg-gray-100 text-gray-800', - [ProductionPriority.NORMAL]: 'bg-blue-100 text-blue-800', - [ProductionPriority.HIGH]: 'bg-orange-100 text-orange-800', - [ProductionPriority.URGENT]: 'bg-red-100 text-red-800', -}; - -export const BatchTracker: React.FC = ({ - className = '', - batchId, - onStageUpdate, - onQualityCheckRequired, -}) => { - const [batches, setBatches] = useState([]); - const [selectedBatch, setSelectedBatch] = useState(null); - const [loading, setLoading] = useState(false); - const [currentStage, setCurrentStage] = useState('mixing'); - const [stageNotes, setStageNotes] = useState>({}); - const [isStageModalOpen, setIsStageModalOpen] = useState(false); - const [selectedStageForUpdate, setSelectedStageForUpdate] = useState(null); - const [alerts, setAlerts] = useState>([]); - - const loadBatches = useCallback(async () => { - setLoading(true); - try { - const response = await productionService.getProductionBatches({ - status: ProductionBatchStatus.IN_PROGRESS, - }); - - if (response.success && response.data) { - setBatches(response.data.items || []); - - if (batchId) { - const specificBatch = response.data.items.find(b => b.id === batchId); - if (specificBatch) { - setSelectedBatch(specificBatch); - } - } else if (response.data.items.length > 0) { - setSelectedBatch(response.data.items[0]); - } - } - } catch (error) { - console.error('Error loading batches:', error); - } finally { - setLoading(false); - } - }, [batchId]); - - useEffect(() => { - loadBatches(); - - // Mock alerts for demonstration - setAlerts([ - { - id: '1', - batchId: 'batch-1', - stage: 'fermentation', - type: 'overdue', - message: 'El fermentado ha superado el tiempo estimado en 30 minutos', - severity: 'medium', - timestamp: new Date().toISOString(), - }, - { - id: '2', - batchId: 'batch-2', - stage: 'baking', - type: 'temperature', - message: 'Temperatura del horno fuera del rango óptimo (185°C)', - severity: 'high', - timestamp: new Date().toISOString(), - }, - ]); - }, [loadBatches]); - - const getCurrentStageInfo = (batch: ProductionBatchResponse): { stage: ProductionStage; progress: number } => { - // This would typically come from the batch data - // For demo purposes, we'll simulate based on batch status - let stageId = 'mixing'; - let progress = 0; - - if (batch.status === ProductionBatchStatus.IN_PROGRESS) { - // Simulate current stage based on time elapsed - const startTime = new Date(batch.actual_start_date || batch.planned_start_date); - const now = new Date(); - const elapsedMinutes = (now.getTime() - startTime.getTime()) / (1000 * 60); - - let cumulativeTime = 0; - const stageKeys = Object.keys(PRODUCTION_STAGES).slice(0, -1); // Exclude completed - - for (const key of stageKeys) { - const stage = PRODUCTION_STAGES[key]; - cumulativeTime += stage.estimatedMinutes; - if (elapsedMinutes <= cumulativeTime) { - stageId = key; - const stageStartTime = cumulativeTime - stage.estimatedMinutes; - progress = Math.min(100, ((elapsedMinutes - stageStartTime) / stage.estimatedMinutes) * 100); - break; - } - } - } - - return { - stage: PRODUCTION_STAGES[stageId], - progress: Math.max(0, progress), - }; - }; - - const getTimeRemaining = (batch: ProductionBatchResponse, stage: ProductionStage): string => { - const startTime = new Date(batch.actual_start_date || batch.planned_start_date); - const now = new Date(); - const elapsedMinutes = (now.getTime() - startTime.getTime()) / (1000 * 60); - - // Calculate when this stage should complete based on cumulative time - let cumulativeTime = 0; - const stageKeys = Object.keys(PRODUCTION_STAGES); - const currentStageIndex = stageKeys.indexOf(stage.id); - - for (let i = 0; i <= currentStageIndex; i++) { - cumulativeTime += PRODUCTION_STAGES[stageKeys[i]].estimatedMinutes; - } - - const remainingMinutes = Math.max(0, cumulativeTime - elapsedMinutes); - const hours = Math.floor(remainingMinutes / 60); - const minutes = Math.floor(remainingMinutes % 60); - - if (hours > 0) { - return `${hours}h ${minutes}m restantes`; - } else { - return `${minutes}m restantes`; - } - }; - - const updateStage = async (batchId: string, newStage: string, notes?: string) => { - try { - // This would update the batch stage in the backend - const updatedBatch = batches.find(b => b.id === batchId); - if (updatedBatch && onStageUpdate) { - onStageUpdate(updatedBatch as unknown as ProductionBatch, PRODUCTION_STAGES[newStage]); - } - - // Update local state - if (notes) { - setStageNotes(prev => ({ - ...prev, - [`${batchId}-${newStage}`]: notes, - })); - } - - await loadBatches(); - } catch (error) { - console.error('Error updating stage:', error); - } - }; - - const handleQualityCheck = (batch: ProductionBatchResponse, stage: ProductionStage) => { - if (onQualityCheckRequired) { - onQualityCheckRequired(batch as unknown as ProductionBatch, stage); - } - }; - - const renderStageProgress = (batch: ProductionBatchResponse) => { - const { stage: currentStage, progress } = getCurrentStageInfo(batch); - const stages = Object.values(PRODUCTION_STAGES).slice(0, -1); // Exclude completed - - return ( -
-
-

Progreso del lote

- - {batch.status === ProductionBatchStatus.IN_PROGRESS && 'En progreso'} - {batch.status === ProductionBatchStatus.PLANNED && 'Planificado'} - {batch.status === ProductionBatchStatus.COMPLETED && 'Completado'} - -
- -
- {stages.map((stage, index) => { - const isActive = stage.id === currentStage.id; - const isCompleted = stages.findIndex(s => s.id === currentStage.id) > index; - const isOverdue = false; // Would be calculated based on actual timing - - return ( - { - setSelectedStageForUpdate(stage); - setIsStageModalOpen(true); - }} - > -
-
- {stage.icon} - {stage.spanishName} -
- {stage.criticalControlPoint && ( - - PCC - - )} -
- - {isActive && ( -
-
- Progreso - {Math.round(progress)}% -
-
-
-
-

- {getTimeRemaining(batch, stage)} -

-
- )} - - {isCompleted && ( -
- - Completado -
- )} - - {!isActive && !isCompleted && ( -

- ~{stage.estimatedMinutes}min -

- )} - - {stage.temperature && isActive && ( -

- 🌡️ {stage.temperature.min}-{stage.temperature.max}{stage.temperature.unit} -

- )} - - ); - })} -
-
- ); - }; - - const renderBatchDetails = (batch: ProductionBatchResponse) => ( - -
-
-

{batch.recipe?.name || 'Producto'}

-

Lote #{batch.batch_number}

- - {batch.priority === ProductionPriority.LOW && 'Baja'} - {batch.priority === ProductionPriority.NORMAL && 'Normal'} - {batch.priority === ProductionPriority.HIGH && 'Alta'} - {batch.priority === ProductionPriority.URGENT && 'Urgente'} - -
- -
-

Cantidad planificada

-

{batch.planned_quantity} unidades

- {batch.actual_quantity && ( - <> -

Cantidad real

-

{batch.actual_quantity} unidades

- - )} -
- -
-

Inicio planificado

-

- {new Date(batch.planned_start_date).toLocaleString('es-ES')} -

- {batch.actual_start_date && ( - <> -

Inicio real

-

- {new Date(batch.actual_start_date).toLocaleString('es-ES')} -

- - )} -
-
- - {batch.notes && ( -
-

{batch.notes}

-
- )} -
- ); - - const renderAlerts = () => ( - -

- 🚨 Alertas activas -

- - {alerts.length === 0 ? ( -

No hay alertas activas

- ) : ( -
- {alerts.map((alert) => ( -
-
-
-

- Lote #{batches.find(b => b.id === alert.batchId)?.batch_number} - {PRODUCTION_STAGES[alert.stage]?.spanishName} -

-

{alert.message}

-
- - {alert.severity === 'high' && 'Alta'} - {alert.severity === 'medium' && 'Media'} - {alert.severity === 'low' && 'Baja'} - -
-

- {new Date(alert.timestamp).toLocaleTimeString('es-ES')} -

-
- ))} -
- )} -
- ); - - return ( -
-
-
-

Seguimiento de Lotes

-

Rastrea el progreso de los lotes a través de las etapas de producción

-
- - {selectedBatch && ( -
- - - -
- )} -
- - {loading ? ( -
-
-
- ) : ( - <> - {selectedBatch ? ( -
- {renderBatchDetails(selectedBatch)} - {renderStageProgress(selectedBatch)} - {renderAlerts()} -
- ) : ( - -

No hay lotes en producción actualmente

- -
- )} - - )} - - {/* Stage Update Modal */} - { - setIsStageModalOpen(false); - setSelectedStageForUpdate(null); - }} - title={`${selectedStageForUpdate?.spanishName} - Lote #${selectedBatch?.batch_number}`} - > - {selectedStageForUpdate && selectedBatch && ( -
-
-

- {selectedStageForUpdate.icon} - {selectedStageForUpdate.spanishName} -

-

- Duración estimada: {selectedStageForUpdate.estimatedMinutes} minutos -

- - {selectedStageForUpdate.temperature && ( -

- 🌡️ Temperatura: {selectedStageForUpdate.temperature.min}-{selectedStageForUpdate.temperature.max}{selectedStageForUpdate.temperature.unit} -

- )} - - {selectedStageForUpdate.criticalControlPoint && ( - - Punto Crítico de Control (PCC) - - )} -
- - {selectedStageForUpdate.requiresQualityCheck && ( -
-

- ⚠️ Esta etapa requiere control de calidad antes de continuar -

- -
- )} - -
- - setStageNotes(prev => ({ - ...prev, - [`${selectedBatch.id}-${selectedStageForUpdate.id}`]: e.target.value, - }))} - /> -
- -
- - - -
-
- )} -
- - {/* Quick Stats */} -
- -
-
-

Lotes activos

-

{batches.length}

-
- 📊 -
-
- - -
-
-

Alertas activas

-

{alerts.length}

-
- 🚨 -
-
- - -
-
-

En horneado

-

3

-
- 🔥 -
-
- - -
-
-

Completados hoy

-

12

-
- -
-
-
-
- ); -}; - -export default BatchTracker; \ No newline at end of file diff --git a/frontend/src/components/domain/production/BatchTracker/index.ts b/frontend/src/components/domain/production/BatchTracker/index.ts deleted file mode 100644 index 3ae2c92e..00000000 --- a/frontend/src/components/domain/production/BatchTracker/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from '../BatchTracker'; \ No newline at end of file diff --git a/frontend/src/components/domain/production/ProductionSchedule.tsx.backup b/frontend/src/components/domain/production/ProductionSchedule.tsx.backup deleted file mode 100644 index f5b35e7d..00000000 --- a/frontend/src/components/domain/production/ProductionSchedule.tsx.backup +++ /dev/null @@ -1,579 +0,0 @@ -import React, { useState, useCallback, useMemo } from 'react'; -import { Card, Button, Badge, Select, DatePicker, Input, Modal, Table } from '../../ui'; -import { productionService, type ProductionScheduleEntry, ProductionBatchStatus } from '../../../services/api/production.service'; -import type { ProductionSchedule as ProductionScheduleType, ProductionBatch, EquipmentReservation, StaffAssignment } from '../../../types/production.types'; - -interface ProductionScheduleProps { - className?: string; - onBatchSelected?: (batch: ProductionBatch) => void; - onScheduleUpdate?: (schedule: ProductionScheduleEntry) => void; -} - -interface TimeSlot { - time: string; - capacity: number; - utilized: number; - available: number; - batches: ProductionScheduleEntry[]; -} - -interface CapacityResource { - id: string; - name: string; - type: 'oven' | 'mixer' | 'staff'; - capacity: number; - slots: TimeSlot[]; -} - -const PRODUCT_TYPE_COLORS = { - pan: 'bg-[var(--color-warning)]/10 border-[var(--color-warning)]/20 text-[var(--color-warning)]', - bolleria: 'bg-[var(--color-primary)]/10 border-[var(--color-primary)]/20 text-[var(--color-primary)]', - reposteria: 'bg-[var(--color-error)]/10 border-[var(--color-error)]/20 text-[var(--color-error)]', - especial: 'bg-[var(--color-info)]/10 border-[var(--color-info)]/20 text-[var(--color-info)]', -}; - -const STATUS_COLORS = { - [ProductionBatchStatus.PLANNED]: 'bg-[var(--color-info)]/10 text-[var(--color-info)]', - [ProductionBatchStatus.IN_PROGRESS]: 'bg-[var(--color-warning)]/10 text-[var(--color-warning)]', - [ProductionBatchStatus.COMPLETED]: 'bg-[var(--color-success)]/10 text-[var(--color-success)]', - [ProductionBatchStatus.CANCELLED]: 'bg-[var(--color-error)]/10 text-[var(--color-error)]', - [ProductionBatchStatus.ON_HOLD]: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)]', -}; - -export const ProductionSchedule: React.FC = ({ - className = '', - onBatchSelected, - onScheduleUpdate, -}) => { - const [scheduleEntries, setScheduleEntries] = useState([]); - const [loading, setLoading] = useState(false); - const [selectedDate, setSelectedDate] = useState(new Date()); - const [viewMode, setViewMode] = useState<'calendar' | 'timeline' | 'capacity'>('timeline'); - const [filterStatus, setFilterStatus] = useState('all'); - const [filterProduct, setFilterProduct] = useState('all'); - const [draggedBatch, setDraggedBatch] = useState(null); - const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false); - const [selectedBatchForScheduling, setSelectedBatchForScheduling] = useState(null); - - // Mock data for demonstration - const mockCapacityResources: CapacityResource[] = [ - { - id: 'horno-1', - name: 'Horno Principal', - type: 'oven', - capacity: 4, - slots: generateTimeSlots('06:00', '20:00', 2), - }, - { - id: 'horno-2', - name: 'Horno Secundario', - type: 'oven', - capacity: 2, - slots: generateTimeSlots('06:00', '18:00', 2), - }, - { - id: 'amasadora-1', - name: 'Amasadora Industrial', - type: 'mixer', - capacity: 6, - slots: generateTimeSlots('05:00', '21:00', 1), - }, - { - id: 'panadero-1', - name: 'Equipo Panadería', - type: 'staff', - capacity: 8, - slots: generateTimeSlots('06:00', '14:00', 1), - }, - ]; - - function generateTimeSlots(startTime: string, endTime: string, intervalHours: number): TimeSlot[] { - const slots: TimeSlot[] = []; - const start = new Date(`2024-01-01 ${startTime}`); - const end = new Date(`2024-01-01 ${endTime}`); - - while (start < end) { - const timeStr = start.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' }); - slots.push({ - time: timeStr, - capacity: 100, - utilized: Math.floor(Math.random() * 80), - available: 100 - Math.floor(Math.random() * 80), - batches: [], - }); - start.setHours(start.getHours() + intervalHours); - } - - return slots; - } - - const loadSchedule = useCallback(async () => { - setLoading(true); - try { - const response = await productionService.getProductionSchedule({ - start_date: selectedDate.toISOString().split('T')[0], - end_date: selectedDate.toISOString().split('T')[0], - }); - - if (response.success) { - setScheduleEntries(response.data || []); - } - } catch (error) { - console.error('Error loading schedule:', error); - } finally { - setLoading(false); - } - }, [selectedDate]); - - const filteredEntries = useMemo(() => { - return scheduleEntries.filter(entry => { - if (filterStatus !== 'all' && entry.batch?.status !== filterStatus) { - return false; - } - if (filterProduct !== 'all' && entry.batch?.recipe?.category !== filterProduct) { - return false; - } - return true; - }); - }, [scheduleEntries, filterStatus, filterProduct]); - - const handleDragStart = (e: React.DragEvent, entry: ProductionScheduleEntry) => { - setDraggedBatch(entry); - e.dataTransfer.effectAllowed = 'move'; - }; - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - e.dataTransfer.dropEffect = 'move'; - }; - - const handleDrop = async (e: React.DragEvent, newTime: string) => { - e.preventDefault(); - - if (!draggedBatch) return; - - try { - const response = await productionService.updateScheduleEntry(draggedBatch.id, { - scheduled_start_time: newTime, - scheduled_end_time: calculateEndTime(newTime, draggedBatch.estimated_duration_minutes), - }); - - if (response.success && onScheduleUpdate) { - onScheduleUpdate(response.data); - await loadSchedule(); - } - } catch (error) { - console.error('Error updating schedule:', error); - } finally { - setDraggedBatch(null); - } - }; - - const calculateEndTime = (startTime: string, durationMinutes: number): string => { - const start = new Date(`2024-01-01 ${startTime}`); - start.setMinutes(start.getMinutes() + durationMinutes); - return start.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' }); - }; - - const handleScheduleBatch = async (scheduleData: { - batch_id: string; - scheduled_date: string; - scheduled_start_time: string; - scheduled_end_time: string; - equipment_reservations?: string[]; - staff_assignments?: string[]; - }) => { - try { - const response = await productionService.scheduleProductionBatch(scheduleData); - - if (response.success && onScheduleUpdate) { - onScheduleUpdate(response.data); - await loadSchedule(); - setIsScheduleModalOpen(false); - setSelectedBatchForScheduling(null); - } - } catch (error) { - console.error('Error scheduling batch:', error); - } - }; - - const getProductTypeColor = (category?: string) => { - return PRODUCT_TYPE_COLORS[category as keyof typeof PRODUCT_TYPE_COLORS] || 'bg-[var(--bg-tertiary)] border-[var(--border-secondary)] text-[var(--text-secondary)]'; - }; - - const getStatusColor = (status?: ProductionBatchStatus) => { - return STATUS_COLORS[status as keyof typeof STATUS_COLORS] || 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)]'; - }; - - const renderTimelineView = () => ( -
-
- setSelectedDate(date || new Date())} - dateFormat="dd/MM/yyyy" - className="w-40" - /> - - -
- -
-
-
Producto
- {Array.from({ length: 22 }, (_, i) => ( -
- {String(i + 3).padStart(2, '0')}:00 -
- ))} -
- -
- {filteredEntries.map((entry) => ( -
-
-
- {entry.batch?.recipe?.name} - - {entry.batch?.status === ProductionBatchStatus.PLANNED && 'Planificado'} - {entry.batch?.status === ProductionBatchStatus.IN_PROGRESS && 'En progreso'} - {entry.batch?.status === ProductionBatchStatus.COMPLETED && 'Completado'} - -
-
- - {Array.from({ length: 22 }, (_, hourIndex) => { - const hour = hourIndex + 3; - const hourStr = `${String(hour).padStart(2, '0')}:00`; - const startHour = parseInt(entry.scheduled_start_time.split(':')[0]); - const endHour = parseInt(entry.scheduled_end_time.split(':')[0]); - - const isInRange = hour >= startHour && hour < endHour; - const isStart = hour === startHour; - - return ( -
handleDrop(e, hourStr)} - > - {isStart && ( -
handleDragStart(e, entry)} - onClick={() => onBatchSelected?.(entry.batch!)} - > -
- {entry.batch?.recipe?.name} -
-
- {entry.batch?.planned_quantity} uds -
-
- )} -
- ); - })} -
- ))} -
-
-
- ); - - const renderCapacityView = () => ( -
-
- setSelectedDate(date || new Date())} - dateFormat="dd/MM/yyyy" - className="w-40" - /> -
- -
- {mockCapacityResources.map((resource) => ( - -
-
-

{resource.name}

-

- {resource.type === 'oven' && 'Horno'} - {resource.type === 'mixer' && 'Amasadora'} - {resource.type === 'staff' && 'Personal'} - - Capacidad: {resource.capacity} -

-
- - {Math.round(resource.slots.reduce((acc, slot) => acc + slot.utilized, 0) / resource.slots.length)}% utilizado - -
- -
- {resource.slots.slice(0, 8).map((slot, index) => ( -
- {slot.time} -
-
80 - ? 'bg-[var(--color-error)]' - : slot.utilized > 60 - ? 'bg-yellow-500' - : 'bg-[var(--color-success)]' - }`} - style={{ width: `${slot.utilized}%` }} - /> -
- - {slot.utilized}% - -
- ))} -
- - - - ))} -
-
- ); - - const renderCalendarView = () => ( -
-
- {['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'].map((day) => ( -
- {day} -
- ))} -
- -
- {Array.from({ length: 35 }, (_, i) => { - const date = new Date(selectedDate); - date.setDate(date.getDate() - date.getDay() + i); - const dayEntries = filteredEntries.filter(entry => - new Date(entry.scheduled_date).toDateString() === date.toDateString() - ); - - return ( -
-
{date.getDate()}
-
- {dayEntries.slice(0, 3).map((entry) => ( -
onBatchSelected?.(entry.batch!)} - > - {entry.batch?.recipe?.name} -
- ))} - {dayEntries.length > 3 && ( -
- +{dayEntries.length - 3} más -
- )} -
-
- ); - })} -
-
- ); - - return ( -
-
-
-

Programación de Producción

-

Gestiona y programa la producción diaria de la panadería

-
- -
-
- - - -
- - -
-
- - {loading ? ( -
-
-
- ) : ( - <> - {viewMode === 'calendar' && renderCalendarView()} - {viewMode === 'timeline' && renderTimelineView()} - {viewMode === 'capacity' && renderCapacityView()} - - )} - - {/* Schedule Creation Modal */} - { - setIsScheduleModalOpen(false); - setSelectedBatchForScheduling(null); - }} - title="Programar Lote de Producción" - > -
- - -
- setSelectedDate(date || new Date())} - dateFormat="dd/MM/yyyy" - /> - -
- -
- - -
- - - -
- - -
-
-
- - {/* Legend */} - -

Leyenda de productos

-
-
-
- Pan -
-
-
- Bollería -
-
-
- Repostería -
-
-
- Especial -
-
-
-
- ); -}; - -export default ProductionSchedule; \ No newline at end of file diff --git a/frontend/src/components/domain/production/ProductionSchedule/index.ts b/frontend/src/components/domain/production/ProductionSchedule/index.ts deleted file mode 100644 index 83f9fe2b..00000000 --- a/frontend/src/components/domain/production/ProductionSchedule/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from '../ProductionSchedule'; \ No newline at end of file diff --git a/frontend/src/components/domain/production/QualityControl.tsx.backup b/frontend/src/components/domain/production/QualityControl.tsx.backup deleted file mode 100644 index 673d22be..00000000 --- a/frontend/src/components/domain/production/QualityControl.tsx.backup +++ /dev/null @@ -1,882 +0,0 @@ -import React, { useState, useCallback, useRef } from 'react'; -import { Card, Button, Badge, Input, Modal, Table, Select, DatePicker } from '../../ui'; -import { productionService, type QualityCheckResponse, QualityCheckStatus } from '../../../services/api/production.service'; -import type { QualityCheck, QualityCheckCriteria, ProductionBatch, QualityCheckType } from '../../../types/production.types'; - -interface QualityControlProps { - className?: string; - batchId?: string; - checkType?: QualityCheckType; - onQualityCheckCompleted?: (result: QualityCheck) => void; - onCorrectiveActionRequired?: (check: QualityCheck, actions: string[]) => void; -} - -interface QualityCheckTemplate { - id: string; - name: string; - spanishName: string; - productTypes: string[]; - criteria: QualityChecklistItem[]; - requiresPhotos: boolean; - passThreshold: number; - criticalPoints: string[]; -} - -interface QualityChecklistItem { - id: string; - category: string; - description: string; - spanishDescription: string; - type: 'boolean' | 'numeric' | 'scale' | 'text'; - required: boolean; - minValue?: number; - maxValue?: number; - unit?: string; - acceptableCriteria?: string; - weight: number; - isCritical: boolean; -} - -interface QualityInspectionResult { - checklistId: string; - value: string | number | boolean; - notes?: string; - photo?: File; - timestamp: string; - inspector: string; -} - -const QUALITY_CHECK_TEMPLATES: Record = { - visual_inspection: { - id: 'visual_inspection', - name: 'Visual Inspection', - spanishName: 'Inspección Visual', - productTypes: ['pan', 'bolleria', 'reposteria'], - requiresPhotos: true, - passThreshold: 80, - criticalPoints: ['color_defects', 'structural_defects'], - criteria: [ - { - id: 'color_uniformity', - category: 'appearance', - description: 'Color uniformity', - spanishDescription: 'Uniformidad del color', - type: 'scale', - required: true, - minValue: 1, - maxValue: 5, - acceptableCriteria: 'Score 3 or higher', - weight: 20, - isCritical: false, - }, - { - id: 'surface_texture', - category: 'appearance', - description: 'Surface texture quality', - spanishDescription: 'Calidad de la textura superficial', - type: 'scale', - required: true, - minValue: 1, - maxValue: 5, - acceptableCriteria: 'Score 3 or higher', - weight: 15, - isCritical: false, - }, - { - id: 'shape_integrity', - category: 'structure', - description: 'Shape and form integrity', - spanishDescription: 'Integridad de forma y estructura', - type: 'boolean', - required: true, - acceptableCriteria: 'Must be true', - weight: 25, - isCritical: true, - }, - { - id: 'size_consistency', - category: 'dimensions', - description: 'Size consistency within batch', - spanishDescription: 'Consistencia de tamaño en el lote', - type: 'scale', - required: true, - minValue: 1, - maxValue: 5, - acceptableCriteria: 'Score 3 or higher', - weight: 20, - isCritical: false, - }, - { - id: 'defects_presence', - category: 'defects', - description: 'Visible defects or imperfections', - spanishDescription: 'Defectos visibles o imperfecciones', - type: 'boolean', - required: true, - acceptableCriteria: 'Must be false', - weight: 20, - isCritical: true, - }, - ], - }, - weight_check: { - id: 'weight_check', - name: 'Weight Check', - spanishName: 'Control de Peso', - productTypes: ['pan', 'bolleria', 'reposteria'], - requiresPhotos: false, - passThreshold: 95, - criticalPoints: ['weight_variance'], - criteria: [ - { - id: 'individual_weight', - category: 'weight', - description: 'Individual piece weight', - spanishDescription: 'Peso individual de la pieza', - type: 'numeric', - required: true, - minValue: 0, - unit: 'g', - acceptableCriteria: 'Within ±5% of target', - weight: 40, - isCritical: true, - }, - { - id: 'batch_average_weight', - category: 'weight', - description: 'Batch average weight', - spanishDescription: 'Peso promedio del lote', - type: 'numeric', - required: true, - minValue: 0, - unit: 'g', - acceptableCriteria: 'Within ±3% of target', - weight: 30, - isCritical: true, - }, - { - id: 'weight_variance', - category: 'consistency', - description: 'Weight variance within batch', - spanishDescription: 'Variación de peso dentro del lote', - type: 'numeric', - required: true, - minValue: 0, - unit: '%', - acceptableCriteria: 'Less than 5%', - weight: 30, - isCritical: true, - }, - ], - }, - temperature_check: { - id: 'temperature_check', - name: 'Temperature Check', - spanishName: 'Control de Temperatura', - productTypes: ['pan', 'bolleria'], - requiresPhotos: false, - passThreshold: 90, - criticalPoints: ['core_temperature'], - criteria: [ - { - id: 'core_temperature', - category: 'temperature', - description: 'Core temperature', - spanishDescription: 'Temperatura del núcleo', - type: 'numeric', - required: true, - minValue: 85, - maxValue: 98, - unit: '°C', - acceptableCriteria: '88-95°C', - weight: 60, - isCritical: true, - }, - { - id: 'cooling_temperature', - category: 'temperature', - description: 'Cooling temperature', - spanishDescription: 'Temperatura de enfriado', - type: 'numeric', - required: false, - minValue: 18, - maxValue: 25, - unit: '°C', - acceptableCriteria: '20-23°C', - weight: 40, - isCritical: false, - }, - ], - }, - packaging_quality: { - id: 'packaging_quality', - name: 'Packaging Quality', - spanishName: 'Calidad del Empaquetado', - productTypes: ['pan', 'bolleria', 'reposteria'], - requiresPhotos: true, - passThreshold: 85, - criticalPoints: ['seal_integrity', 'labeling_accuracy'], - criteria: [ - { - id: 'seal_integrity', - category: 'packaging', - description: 'Package seal integrity', - spanishDescription: 'Integridad del sellado del envase', - type: 'boolean', - required: true, - acceptableCriteria: 'Must be true', - weight: 30, - isCritical: true, - }, - { - id: 'labeling_accuracy', - category: 'labeling', - description: 'Label accuracy and placement', - spanishDescription: 'Precisión y colocación de etiquetas', - type: 'scale', - required: true, - minValue: 1, - maxValue: 5, - acceptableCriteria: 'Score 4 or higher', - weight: 25, - isCritical: true, - }, - { - id: 'package_appearance', - category: 'appearance', - description: 'Overall package appearance', - spanishDescription: 'Apariencia general del envase', - type: 'scale', - required: true, - minValue: 1, - maxValue: 5, - acceptableCriteria: 'Score 3 or higher', - weight: 20, - isCritical: false, - }, - { - id: 'barcode_readability', - category: 'labeling', - description: 'Barcode readability', - spanishDescription: 'Legibilidad del código de barras', - type: 'boolean', - required: true, - acceptableCriteria: 'Must be true', - weight: 25, - isCritical: false, - }, - ], - }, -}; - -const STATUS_COLORS = { - [QualityCheckStatus.SCHEDULED]: 'bg-blue-100 text-blue-800', - [QualityCheckStatus.IN_PROGRESS]: 'bg-yellow-100 text-yellow-800', - [QualityCheckStatus.PASSED]: 'bg-green-100 text-green-800', - [QualityCheckStatus.FAILED]: 'bg-red-100 text-red-800', - [QualityCheckStatus.REQUIRES_REVIEW]: 'bg-orange-100 text-orange-800', - [QualityCheckStatus.CANCELLED]: 'bg-gray-100 text-gray-800', -}; - -export const QualityControl: React.FC = ({ - className = '', - batchId, - checkType, - onQualityCheckCompleted, - onCorrectiveActionRequired, -}) => { - const [qualityChecks, setQualityChecks] = useState([]); - const [selectedTemplate, setSelectedTemplate] = useState(null); - const [activeCheck, setActiveCheck] = useState(null); - const [inspectionResults, setInspectionResults] = useState>({}); - const [loading, setLoading] = useState(false); - const [isInspectionModalOpen, setIsInspectionModalOpen] = useState(false); - const [uploadedPhotos, setUploadedPhotos] = useState>({}); - const [currentInspector, setCurrentInspector] = useState('inspector-1'); - const fileInputRef = useRef(null); - - const loadQualityChecks = useCallback(async () => { - setLoading(true); - try { - const params: any = {}; - if (batchId) params.batch_id = batchId; - if (checkType) params.check_type = checkType; - - const response = await productionService.getQualityChecks(params); - - if (response.success && response.data) { - setQualityChecks(response.data.items || []); - } - } catch (error) { - console.error('Error loading quality checks:', error); - } finally { - setLoading(false); - } - }, [batchId, checkType]); - - const startQualityCheck = (template: QualityCheckTemplate, batch?: any) => { - setSelectedTemplate(template); - setInspectionResults({}); - setUploadedPhotos({}); - setIsInspectionModalOpen(true); - - // Initialize empty results for all criteria - template.criteria.forEach(criterion => { - setInspectionResults(prev => ({ - ...prev, - [criterion.id]: { - checklistId: criterion.id, - value: criterion.type === 'boolean' ? false : criterion.type === 'numeric' ? 0 : '', - timestamp: new Date().toISOString(), - inspector: currentInspector, - } - })); - }); - }; - - const updateInspectionResult = (criterionId: string, value: string | number | boolean, notes?: string) => { - setInspectionResults(prev => ({ - ...prev, - [criterionId]: { - ...prev[criterionId], - value, - notes, - timestamp: new Date().toISOString(), - } - })); - }; - - const handlePhotoUpload = (criterionId: string, file: File) => { - setUploadedPhotos(prev => ({ - ...prev, - [criterionId]: file, - })); - - // Update inspection result with photo reference - setInspectionResults(prev => ({ - ...prev, - [criterionId]: { - ...prev[criterionId], - photo: file, - } - })); - }; - - const calculateOverallScore = (): number => { - if (!selectedTemplate) return 0; - - let totalScore = 0; - let totalWeight = 0; - - selectedTemplate.criteria.forEach(criterion => { - const result = inspectionResults[criterion.id]; - if (result) { - let score = 0; - - if (criterion.type === 'boolean') { - score = result.value ? 100 : 0; - } else if (criterion.type === 'scale') { - const numValue = Number(result.value); - score = (numValue / (criterion.maxValue || 5)) * 100; - } else if (criterion.type === 'numeric') { - // For numeric values, assume pass/fail based on acceptable range - score = 100; // Simplified - would need more complex logic - } - - totalScore += score * criterion.weight; - totalWeight += criterion.weight; - } - }); - - return totalWeight > 0 ? totalScore / totalWeight : 0; - }; - - const checkCriticalFailures = (): string[] => { - if (!selectedTemplate) return []; - - const failures: string[] = []; - - selectedTemplate.criteria.forEach(criterion => { - if (criterion.isCritical) { - const result = inspectionResults[criterion.id]; - if (result) { - if (criterion.type === 'boolean' && !result.value) { - failures.push(criterion.spanishDescription); - } else if (criterion.type === 'scale' && Number(result.value) < 3) { - failures.push(criterion.spanishDescription); - } - } - } - }); - - return failures; - }; - - const completeQualityCheck = async () => { - if (!selectedTemplate) return; - - const overallScore = calculateOverallScore(); - const criticalFailures = checkCriticalFailures(); - const passed = overallScore >= selectedTemplate.passThreshold && criticalFailures.length === 0; - - const checkData = { - status: passed ? QualityCheckStatus.PASSED : QualityCheckStatus.FAILED, - results: { - overallScore, - criticalFailures, - individualResults: inspectionResults, - photos: Object.keys(uploadedPhotos), - }, - notes: `Inspección completada. Puntuación: ${overallScore.toFixed(1)}%`, - corrective_actions: criticalFailures.length > 0 ? [ - `Fallas críticas encontradas: ${criticalFailures.join(', ')}`, - 'Revisar proceso de producción', - 'Re-entrenar personal si es necesario' - ] : undefined, - }; - - try { - // This would be the actual check ID from the created quality check - const mockCheckId = 'check-id'; - const response = await productionService.completeQualityCheck(mockCheckId, checkData); - - if (response.success) { - if (onQualityCheckCompleted) { - onQualityCheckCompleted(response.data as unknown as QualityCheck); - } - - if (!passed && checkData.corrective_actions && onCorrectiveActionRequired) { - onCorrectiveActionRequired(response.data as unknown as QualityCheck, checkData.corrective_actions); - } - - setIsInspectionModalOpen(false); - setSelectedTemplate(null); - await loadQualityChecks(); - } - } catch (error) { - console.error('Error completing quality check:', error); - } - }; - - const renderInspectionForm = () => { - if (!selectedTemplate) return null; - - return ( -
-
-

{selectedTemplate.spanishName}

-

- Umbral de aprobación: {selectedTemplate.passThreshold}% -

- {selectedTemplate.criticalPoints.length > 0 && ( -

- Puntos críticos: {selectedTemplate.criticalPoints.length} -

- )} -
- -
- {selectedTemplate.criteria.map((criterion) => ( - -
-
-

{criterion.spanishDescription}

-

{criterion.acceptableCriteria}

- {criterion.isCritical && ( - - Punto Crítico - - )} -
- Peso: {criterion.weight}% -
- -
- {criterion.type === 'boolean' && ( -
- - -
- )} - - {criterion.type === 'scale' && ( -
- 1 - updateInspectionResult(criterion.id, parseInt(e.target.value))} - className="flex-1" - /> - {criterion.maxValue || 5} - - {inspectionResults[criterion.id]?.value || criterion.minValue || 1} - -
- )} - - {criterion.type === 'numeric' && ( -
- updateInspectionResult(criterion.id, parseFloat(e.target.value) || 0)} - className="w-24" - /> - {criterion.unit && {criterion.unit}} -
- )} - - { - const currentResult = inspectionResults[criterion.id]; - if (currentResult) { - updateInspectionResult(criterion.id, currentResult.value, e.target.value); - } - }} - /> - - {selectedTemplate.requiresPhotos && ( -
- { - const file = e.target.files?.[0]; - if (file) { - handlePhotoUpload(criterion.id, file); - } - }} - className="hidden" - /> - - {uploadedPhotos[criterion.id] && ( -

- ✓ Foto capturada: {uploadedPhotos[criterion.id].name} -

- )} -
- )} -
-
- ))} -
- - -
- Puntuación general - - {calculateOverallScore().toFixed(1)}% - -
- -
-
= selectedTemplate.passThreshold - ? 'bg-green-500' - : 'bg-red-500' - }`} - style={{ width: `${calculateOverallScore()}%` }} - /> -
- -
- Umbral: {selectedTemplate.passThreshold}% - = selectedTemplate.passThreshold - ? 'text-green-600 font-medium' - : 'text-red-600 font-medium' - }> - {calculateOverallScore() >= selectedTemplate.passThreshold ? '✓ APROBADO' : '✗ REPROBADO'} - -
- - {checkCriticalFailures().length > 0 && ( -
-

Fallas críticas detectadas:

-
    - {checkCriticalFailures().map((failure, index) => ( -
  • • {failure}
  • - ))} -
-
- )} - -
- ); - }; - - const renderQualityChecksTable = () => { - const columns = [ - { key: 'batch', label: 'Lote', sortable: true }, - { key: 'type', label: 'Tipo de control', sortable: true }, - { key: 'status', label: 'Estado', sortable: true }, - { key: 'inspector', label: 'Inspector', sortable: false }, - { key: 'scheduled_date', label: 'Fecha programada', sortable: true }, - { key: 'completed_date', label: 'Fecha completada', sortable: true }, - { key: 'actions', label: 'Acciones', sortable: false }, - ]; - - const data = qualityChecks.map(check => ({ - id: check.id, - batch: `#${check.batch?.batch_number || 'N/A'}`, - type: QUALITY_CHECK_TEMPLATES[check.check_type]?.spanishName || check.check_type, - status: ( - - {check.status === QualityCheckStatus.SCHEDULED && 'Programado'} - {check.status === QualityCheckStatus.IN_PROGRESS && 'En progreso'} - {check.status === QualityCheckStatus.PASSED && 'Aprobado'} - {check.status === QualityCheckStatus.FAILED && 'Reprobado'} - {check.status === QualityCheckStatus.REQUIRES_REVIEW && 'Requiere revisión'} - - ), - inspector: check.inspector || 'No asignado', - scheduled_date: check.scheduled_date - ? new Date(check.scheduled_date).toLocaleDateString('es-ES') - : '-', - completed_date: check.completed_date - ? new Date(check.completed_date).toLocaleDateString('es-ES') - : '-', - actions: ( -
- {check.status === QualityCheckStatus.SCHEDULED && ( - - )} - -
- ), - })); - - return
; - }; - - return ( -
-
-
-

Control de Calidad

-

Gestiona las inspecciones de calidad y cumplimiento

-
- -
- - - -
-
- - {/* Quick Start Templates */} -
- {Object.values(QUALITY_CHECK_TEMPLATES).map((template) => ( - startQualityCheck(template)} - > -
-

{template.spanishName}

- {template.requiresPhotos && ( - - 📸 Fotos - - )} -
- -

- {template.criteria.length} criterios • Umbral: {template.passThreshold}% -

- -
- {template.productTypes.map((type) => ( - - {type === 'pan' && 'Pan'} - {type === 'bolleria' && 'Bollería'} - {type === 'reposteria' && 'Repostería'} - - ))} -
- - {template.criticalPoints.length > 0 && ( -

- {template.criticalPoints.length} puntos críticos -

- )} -
- ))} -
- - {/* Quality Checks Table */} - -
-

Controles de calidad recientes

- -
- - - -
-
- - {loading ? ( -
-
-
- ) : ( - renderQualityChecksTable() - )} -
- - {/* Quality Check Modal */} - { - setIsInspectionModalOpen(false); - setSelectedTemplate(null); - }} - title={`Control de Calidad: ${selectedTemplate?.spanishName}`} - size="lg" - > - {renderInspectionForm()} - -
- - - -
-
- - {/* Stats Cards */} -
- -
-
-

Tasa de aprobación

-

94.2%

-
- -
-
- - -
-
-

Controles pendientes

-

7

-
- -
-
- - -
-
-

Fallas críticas

-

2

-
- 🚨 -
-
- - -
-
-

Controles hoy

-

23

-
- 📋 -
-
-
-
- ); -}; - -export default QualityControl; \ No newline at end of file diff --git a/frontend/src/components/domain/production/QualityControl/index.ts b/frontend/src/components/domain/production/QualityControl/index.ts deleted file mode 100644 index 4e9db89d..00000000 --- a/frontend/src/components/domain/production/QualityControl/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from '../QualityControl'; \ No newline at end of file diff --git a/frontend/src/components/domain/production/RecipeDisplay.tsx.backup b/frontend/src/components/domain/production/RecipeDisplay.tsx.backup deleted file mode 100644 index 45f0ee13..00000000 --- a/frontend/src/components/domain/production/RecipeDisplay.tsx.backup +++ /dev/null @@ -1,794 +0,0 @@ -import React, { useState, useCallback, useMemo } from 'react'; -import { Card, Button, Badge, Input, Modal, Table, Select } from '../../ui'; -import type { Recipe, RecipeIngredient, RecipeInstruction, NutritionalInfo, DifficultyLevel } from '../../../types/production.types'; - -interface RecipeDisplayProps { - className?: string; - recipe: Recipe; - editable?: boolean; - showNutrition?: boolean; - showCosting?: boolean; - onScaleChange?: (scaleFactor: number, scaledRecipe: Recipe) => void; - onVersionUpdate?: (newVersion: Recipe) => void; - onCostCalculation?: (totalCost: number, costPerUnit: number) => void; -} - -interface ScaledIngredient extends RecipeIngredient { - scaledQuantity: number; - scaledCost?: number; -} - -interface ScaledInstruction extends RecipeInstruction { - scaledDuration?: number; -} - -interface ScaledRecipe extends Omit { - scaledYieldQuantity: number; - scaledTotalTime: number; - ingredients: ScaledIngredient[]; - instructions: ScaledInstruction[]; - scaleFactor: number; - estimatedTotalCost?: number; - costPerScaledUnit?: number; -} - -interface TimerState { - instructionId: string; - duration: number; - remaining: number; - isActive: boolean; - isComplete: boolean; -} - -const DIFFICULTY_COLORS = { - [DifficultyLevel.BEGINNER]: 'bg-green-100 text-green-800', - [DifficultyLevel.INTERMEDIATE]: 'bg-yellow-100 text-yellow-800', - [DifficultyLevel.ADVANCED]: 'bg-orange-100 text-orange-800', - [DifficultyLevel.EXPERT]: 'bg-red-100 text-red-800', -}; - -const DIFFICULTY_LABELS = { - [DifficultyLevel.BEGINNER]: 'Principiante', - [DifficultyLevel.INTERMEDIATE]: 'Intermedio', - [DifficultyLevel.ADVANCED]: 'Avanzado', - [DifficultyLevel.EXPERT]: 'Experto', -}; - -const ALLERGEN_ICONS: Record = { - gluten: '🌾', - milk: '🥛', - eggs: '🥚', - nuts: '🥜', - soy: '🫘', - sesame: '🌰', - fish: '🐟', - shellfish: '🦐', -}; - -const EQUIPMENT_ICONS: Record = { - oven: '🔥', - mixer: '🥄', - scale: '⚖️', - bowl: '🥣', - whisk: '🔄', - spatula: '🍴', - thermometer: '🌡️', - timer: '⏰', -}; - -export const RecipeDisplay: React.FC = ({ - className = '', - recipe, - editable = false, - showNutrition = true, - showCosting = false, - onScaleChange, - onVersionUpdate, - onCostCalculation, -}) => { - const [scaleFactor, setScaleFactor] = useState(1); - const [activeTimers, setActiveTimers] = useState>({}); - const [isNutritionModalOpen, setIsNutritionModalOpen] = useState(false); - const [isCostingModalOpen, setIsCostingModalOpen] = useState(false); - const [isEquipmentModalOpen, setIsEquipmentModalOpen] = useState(false); - const [selectedInstruction, setSelectedInstruction] = useState(null); - const [showAllergensDetail, setShowAllergensDetail] = useState(false); - - // Mock ingredient costs for demonstration - const ingredientCosts: Record = { - flour: 1.2, // €/kg - water: 0.001, // €/l - salt: 0.8, // €/kg - yeast: 8.5, // €/kg - sugar: 1.5, // €/kg - butter: 6.2, // €/kg - milk: 1.3, // €/l - eggs: 0.25, // €/unit - }; - - const scaledRecipe = useMemo((): ScaledRecipe => { - const scaledIngredients: ScaledIngredient[] = recipe.ingredients.map(ingredient => ({ - ...ingredient, - scaledQuantity: ingredient.quantity * scaleFactor, - scaledCost: ingredientCosts[ingredient.ingredient_id] - ? ingredientCosts[ingredient.ingredient_id] * ingredient.quantity * scaleFactor - : undefined, - })); - - const scaledInstructions: ScaledInstruction[] = recipe.instructions.map(instruction => ({ - ...instruction, - scaledDuration: instruction.duration_minutes ? Math.ceil(instruction.duration_minutes * scaleFactor) : undefined, - })); - - const estimatedTotalCost = scaledIngredients.reduce((total, ingredient) => - total + (ingredient.scaledCost || 0), 0 - ); - - const scaledYieldQuantity = recipe.yield_quantity * scaleFactor; - const costPerScaledUnit = scaledYieldQuantity > 0 ? estimatedTotalCost / scaledYieldQuantity : 0; - - return { - ...recipe, - scaledYieldQuantity, - scaledTotalTime: Math.ceil(recipe.total_time_minutes * scaleFactor), - ingredients: scaledIngredients, - instructions: scaledInstructions, - scaleFactor, - estimatedTotalCost, - costPerScaledUnit, - }; - }, [recipe, scaleFactor, ingredientCosts]); - - const handleScaleChange = useCallback((newScaleFactor: number) => { - setScaleFactor(newScaleFactor); - if (onScaleChange) { - onScaleChange(newScaleFactor, scaledRecipe as unknown as Recipe); - } - }, [scaledRecipe, onScaleChange]); - - const startTimer = (instruction: RecipeInstruction) => { - if (!instruction.duration_minutes) return; - - const timerState: TimerState = { - instructionId: instruction.step_number.toString(), - duration: instruction.duration_minutes * 60, // Convert to seconds - remaining: instruction.duration_minutes * 60, - isActive: true, - isComplete: false, - }; - - setActiveTimers(prev => ({ - ...prev, - [instruction.step_number.toString()]: timerState, - })); - - // Start countdown - const interval = setInterval(() => { - setActiveTimers(prev => { - const current = prev[instruction.step_number.toString()]; - if (!current || current.remaining <= 0) { - clearInterval(interval); - return { - ...prev, - [instruction.step_number.toString()]: { - ...current, - remaining: 0, - isActive: false, - isComplete: true, - } - }; - } - - return { - ...prev, - [instruction.step_number.toString()]: { - ...current, - remaining: current.remaining - 1, - } - }; - }); - }, 1000); - }; - - const stopTimer = (instructionId: string) => { - setActiveTimers(prev => ({ - ...prev, - [instructionId]: { - ...prev[instructionId], - isActive: false, - } - })); - }; - - const formatTime = (seconds: number): string => { - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - const secs = seconds % 60; - - if (hours > 0) { - return `${hours}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`; - } - return `${minutes}:${String(secs).padStart(2, '0')}`; - }; - - const renderScalingControls = () => ( - -

Escalado de receta

- -
-
- - handleScaleChange(parseFloat(e.target.value) || 1)} - className="w-full" - /> -
- -
- -

- {recipe.yield_quantity} {recipe.yield_unit} -

-
- -
- -

- {scaledRecipe.scaledYieldQuantity} {recipe.yield_unit} -

-
-
- -
- - - - - -
- - {scaleFactor !== 1 && ( -
-

- Tiempo total escalado: {Math.ceil(scaledRecipe.scaledTotalTime / 60)}h {scaledRecipe.scaledTotalTime % 60}m -

- {showCosting && scaledRecipe.estimatedTotalCost && ( -

- Costo estimado: €{scaledRecipe.estimatedTotalCost.toFixed(2)} - (€{scaledRecipe.costPerScaledUnit?.toFixed(3)}/{recipe.yield_unit}) -

- )} -
- )} -
- ); - - const renderRecipeHeader = () => ( - -
-
-
-
-

{recipe.name}

-

{recipe.description}

-
- -
- - {DIFFICULTY_LABELS[recipe.difficulty_level]} - - - v{recipe.version} - -
-
- -
-
-

Preparación

-

{recipe.prep_time_minutes} min

-
-
-

Cocción

-

{recipe.cook_time_minutes} min

-
-
-

Total

-

{Math.ceil(recipe.total_time_minutes / 60)}h {recipe.total_time_minutes % 60}m

-
-
-

Rendimiento

-

{scaledRecipe.scaledYieldQuantity} {recipe.yield_unit}

-
-
- - {recipe.allergen_warnings.length > 0 && ( -
-
-

Alérgenos:

- -
-
- {recipe.allergen_warnings.map((allergen) => ( - - {ALLERGEN_ICONS[allergen]} {allergen} - - ))} -
- - {showAllergensDetail && ( -
-

- ⚠️ Este producto contiene los siguientes alérgenos. Revisar cuidadosamente - antes del consumo si existe alguna alergia o intolerancia alimentaria. -

-
- )} -
- )} - - {recipe.storage_instructions && ( -
-

- Conservación: {recipe.storage_instructions} - {recipe.shelf_life_hours && ( - - • Vida útil: {Math.ceil(recipe.shelf_life_hours / 24)} días - - )} -

-
- )} -
- -
- {showNutrition && ( - - )} - - {showCosting && ( - - )} - - - - {editable && onVersionUpdate && ( - - )} -
-
-
- ); - - const renderIngredients = () => ( - -

Ingredientes

- -
- {scaledRecipe.ingredients.map((ingredient, index) => ( -
-
-

{ingredient.ingredient_name}

- {ingredient.preparation_notes && ( -

{ingredient.preparation_notes}

- )} - {ingredient.is_optional && ( - - Opcional - - )} -
- -
-

- {ingredient.scaledQuantity.toFixed(ingredient.scaledQuantity < 1 ? 2 : 0)} {ingredient.unit} -

- {scaleFactor !== 1 && ( -

- (original: {ingredient.quantity} {ingredient.unit}) -

- )} - {showCosting && ingredient.scaledCost && ( -

- €{ingredient.scaledCost.toFixed(2)} -

- )} -
-
- ))} -
- - {showCosting && scaledRecipe.estimatedTotalCost && ( -
-
- Costo total estimado: - - €{scaledRecipe.estimatedTotalCost.toFixed(2)} - -
-

- €{scaledRecipe.costPerScaledUnit?.toFixed(3)} por {recipe.yield_unit} -

-
- )} -
- ); - - const renderInstructions = () => ( - -

Instrucciones

- -
- {scaledRecipe.instructions.map((instruction, index) => { - const timer = activeTimers[instruction.step_number.toString()]; - - return ( -
-
-
-
- {instruction.step_number} -
-
- -
-
-
- {instruction.duration_minutes && ( - - ⏱️ {instruction.scaledDuration || instruction.duration_minutes} min - - )} - - {instruction.temperature && ( - - 🌡️ {instruction.temperature}°C - - )} - - {instruction.critical_control_point && ( - - 🚨 PCC - - )} -
- - {instruction.duration_minutes && ( -
- {!timer?.isActive && !timer?.isComplete && ( - - )} - - {timer?.isActive && ( -
- - {formatTime(timer.remaining)} - - -
- )} - - {timer?.isComplete && ( - - ✅ Completado - - )} -
- )} -
- -

- {instruction.instruction} -

- - {instruction.equipment && instruction.equipment.length > 0 && ( -
-

Equipo necesario:

-
- {instruction.equipment.map((equipment, idx) => ( - - {EQUIPMENT_ICONS[equipment.toLowerCase()] || '🔧'} {equipment} - - ))} -
-
- )} - - {instruction.tips && ( -
-

- 💡 Tip: {instruction.tips} -

-
- )} -
-
-
- ); - })} -
-
- ); - - const renderNutritionModal = () => ( - setIsNutritionModalOpen(false)} - title="Información Nutricional" - > - {recipe.nutritional_info ? ( -
-
-
-

Calorías por porción

-

- {recipe.nutritional_info.calories_per_serving || 'N/A'} -

-
- -
-

Tamaño de porción

-

- {recipe.nutritional_info.serving_size || 'N/A'} -

-
-
- -
-
-

Proteínas

-

{recipe.nutritional_info.protein_g || 0}g

-
-
-

Carbohidratos

-

{recipe.nutritional_info.carbohydrates_g || 0}g

-
-
-

Grasas

-

{recipe.nutritional_info.fat_g || 0}g

-
-
-

Fibra

-

{recipe.nutritional_info.fiber_g || 0}g

-
-
-

Azúcares

-

{recipe.nutritional_info.sugar_g || 0}g

-
-
-

Sodio

-

{recipe.nutritional_info.sodium_mg || 0}mg

-
-
- -
-

- Porciones por lote escalado: { - recipe.nutritional_info.servings_per_batch - ? Math.ceil(recipe.nutritional_info.servings_per_batch * scaleFactor) - : 'N/A' - } -

-
-
- ) : ( -

- No hay información nutricional disponible para esta receta. -

- )} -
- ); - - const renderCostingModal = () => ( - setIsCostingModalOpen(false)} - title="Análisis de Costos" - > -
-
-
- Costo total - - €{scaledRecipe.estimatedTotalCost?.toFixed(2)} - -
-

- €{scaledRecipe.costPerScaledUnit?.toFixed(3)} por {recipe.yield_unit} -

-
- -
-

Desglose por ingrediente

-
- {scaledRecipe.ingredients.map((ingredient, index) => ( -
-
-

{ingredient.ingredient_name}

-

- {ingredient.scaledQuantity.toFixed(2)} {ingredient.unit} -

-
-

- €{ingredient.scaledCost?.toFixed(2) || '0.00'} -

-
- ))} -
-
- -
-

Análisis de rentabilidad

-
-

• Costo de ingredientes: €{scaledRecipe.estimatedTotalCost?.toFixed(2)}

-

• Margen sugerido (300%): €{((scaledRecipe.estimatedTotalCost || 0) * 3).toFixed(2)}

-

• Precio de venta sugerido por {recipe.yield_unit}: €{((scaledRecipe.costPerScaledUnit || 0) * 4).toFixed(2)}

-
-
-
-
- ); - - const renderEquipmentModal = () => ( - setIsEquipmentModalOpen(false)} - title="Equipo Necesario" - > -
-
-

Equipo general de la receta

-
- {recipe.equipment_needed.map((equipment, index) => ( -
- - {EQUIPMENT_ICONS[equipment.toLowerCase()] || '🔧'} - - {equipment} -
- ))} -
-
- -
-

Equipo por instrucción

-
- {recipe.instructions - .filter(instruction => instruction.equipment && instruction.equipment.length > 0) - .map((instruction) => ( -
-

- Paso {instruction.step_number} -

-
- {instruction.equipment?.map((equipment, idx) => ( - - {EQUIPMENT_ICONS[equipment.toLowerCase()] || '🔧'} {equipment} - - ))} -
-
- ))} -
-
-
-
- ); - - return ( -
- {renderRecipeHeader()} - {renderScalingControls()} - -
- {renderIngredients()} -
- {renderInstructions()} -
-
- - {renderNutritionModal()} - {renderCostingModal()} - {renderEquipmentModal()} -
- ); -}; - -export default RecipeDisplay; \ No newline at end of file diff --git a/frontend/src/components/domain/production/RecipeDisplay/index.ts b/frontend/src/components/domain/production/RecipeDisplay/index.ts deleted file mode 100644 index e5419ae3..00000000 --- a/frontend/src/components/domain/production/RecipeDisplay/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from '../RecipeDisplay'; \ No newline at end of file diff --git a/frontend/src/components/domain/sales/CustomerInfo.tsx.backup b/frontend/src/components/domain/sales/CustomerInfo.tsx.backup deleted file mode 100644 index dff66a6c..00000000 --- a/frontend/src/components/domain/sales/CustomerInfo.tsx.backup +++ /dev/null @@ -1,1272 +0,0 @@ -import React, { useState, useEffect, useMemo } from 'react'; -import { - Card, - Button, - Badge, - Input, - Select, - Avatar, - Tooltip, - Modal -} from '../../ui'; -import { - SalesRecord, - SalesChannel, - PaymentMethod -} from '../../../types/sales.types'; -import { salesService } from '../../../services/api/sales.service'; -import { useSales } from '../../../hooks/api/useSales'; - -// Customer interfaces -interface Customer { - id: string; - name: string; - email?: string; - phone?: string; - address?: string; - city?: string; - postal_code?: string; - birth_date?: string; - registration_date: string; - status: CustomerStatus; - segment: CustomerSegment; - loyalty_points: number; - preferred_channel: SalesChannel; - notes?: string; - tags: string[]; - communication_preferences: CommunicationPreferences; -} - -interface CustomerOrder { - id: string; - date: string; - total: number; - items_count: number; - status: OrderStatus; - channel: SalesChannel; - payment_method?: PaymentMethod; - products: string[]; -} - -interface CustomerStats { - total_orders: number; - total_spent: number; - average_order_value: number; - last_order_date?: string; - favorite_products: string[]; - preferred_times: string[]; - loyalty_tier: LoyaltyTier; - lifetime_value: number; - churn_risk: ChurnRisk; -} - -interface CommunicationPreferences { - email_marketing: boolean; - sms_notifications: boolean; - push_notifications: boolean; - promotional_offers: boolean; - order_updates: boolean; - loyalty_updates: boolean; -} - -interface PaymentMethod { - id: string; - type: 'credit_card' | 'debit_card' | 'digital_wallet' | 'bank_account'; - last_four: string; - brand?: string; - is_default: boolean; - expires_at?: string; -} - -interface LoyaltyProgram { - current_points: number; - points_to_next_tier: number; - current_tier: LoyaltyTier; - benefits: string[]; - rewards_history: LoyaltyTransaction[]; -} - -interface LoyaltyTransaction { - id: string; - date: string; - type: 'earned' | 'redeemed'; - points: number; - description: string; - order_id?: string; -} - -enum CustomerStatus { - ACTIVE = 'active', - INACTIVE = 'inactive', - SUSPENDED = 'suspended', - VIP = 'vip' -} - -enum CustomerSegment { - NEW = 'new', - REGULAR = 'regular', - PREMIUM = 'premium', - ENTERPRISE = 'enterprise', - AT_RISK = 'at_risk' -} - -enum OrderStatus { - PENDING = 'pending', - CONFIRMED = 'confirmed', - PREPARING = 'preparing', - READY = 'ready', - DELIVERED = 'delivered', - CANCELLED = 'cancelled' -} - -enum LoyaltyTier { - BRONZE = 'bronze', - SILVER = 'silver', - GOLD = 'gold', - PLATINUM = 'platinum' -} - -enum ChurnRisk { - LOW = 'low', - MEDIUM = 'medium', - HIGH = 'high', - CRITICAL = 'critical' -} - -interface CustomerInfoProps { - customerId?: string; - onCustomerSelect?: (customer: Customer) => void; - onCustomerUpdate?: (customerId: string, updates: Partial) => void; - showOrderHistory?: boolean; - showLoyaltyProgram?: boolean; - allowEditing?: boolean; - className?: string; -} - -const StatusColors = { - [CustomerStatus.ACTIVE]: 'green', - [CustomerStatus.INACTIVE]: 'gray', - [CustomerStatus.SUSPENDED]: 'red', - [CustomerStatus.VIP]: 'purple' -} as const; - -const StatusLabels = { - [CustomerStatus.ACTIVE]: 'Activo', - [CustomerStatus.INACTIVE]: 'Inactivo', - [CustomerStatus.SUSPENDED]: 'Suspendido', - [CustomerStatus.VIP]: 'VIP' -} as const; - -const SegmentColors = { - [CustomerSegment.NEW]: 'blue', - [CustomerSegment.REGULAR]: 'gray', - [CustomerSegment.PREMIUM]: 'gold', - [CustomerSegment.ENTERPRISE]: 'purple', - [CustomerSegment.AT_RISK]: 'red' -} as const; - -const SegmentLabels = { - [CustomerSegment.NEW]: 'Nuevo', - [CustomerSegment.REGULAR]: 'Regular', - [CustomerSegment.PREMIUM]: 'Premium', - [CustomerSegment.ENTERPRISE]: 'Empresa', - [CustomerSegment.AT_RISK]: 'En Riesgo' -} as const; - -const TierColors = { - [LoyaltyTier.BRONZE]: 'orange', - [LoyaltyTier.SILVER]: 'gray', - [LoyaltyTier.GOLD]: 'yellow', - [LoyaltyTier.PLATINUM]: 'purple' -} as const; - -const TierLabels = { - [LoyaltyTier.BRONZE]: 'Bronce', - [LoyaltyTier.SILVER]: 'Plata', - [LoyaltyTier.GOLD]: 'Oro', - [LoyaltyTier.PLATINUM]: 'Platino' -} as const; - -const ChurnRiskColors = { - [ChurnRisk.LOW]: 'green', - [ChurnRisk.MEDIUM]: 'yellow', - [ChurnRisk.HIGH]: 'orange', - [ChurnRisk.CRITICAL]: 'red' -} as const; - -const ChurnRiskLabels = { - [ChurnRisk.LOW]: 'Bajo', - [ChurnRisk.MEDIUM]: 'Medio', - [ChurnRisk.HIGH]: 'Alto', - [ChurnRisk.CRITICAL]: 'Crítico' -} as const; - -const ChannelLabels = { - [SalesChannel.STORE_FRONT]: 'Tienda', - [SalesChannel.ONLINE]: 'Online', - [SalesChannel.PHONE_ORDER]: 'Teléfono', - [SalesChannel.DELIVERY]: 'Delivery', - [SalesChannel.CATERING]: 'Catering', - [SalesChannel.WHOLESALE]: 'Mayorista', - [SalesChannel.FARMERS_MARKET]: 'Mercado', - [SalesChannel.THIRD_PARTY]: 'Terceros' -} as const; - -export const CustomerInfo: React.FC = ({ - customerId, - onCustomerSelect, - onCustomerUpdate, - showOrderHistory = true, - showLoyaltyProgram = true, - allowEditing = true, - className = '' -}) => { - // State - const [customer, setCustomer] = useState(null); - const [customerStats, setCustomerStats] = useState(null); - const [orderHistory, setOrderHistory] = useState([]); - const [loyaltyProgram, setLoyaltyProgram] = useState(null); - const [paymentMethods, setPaymentMethods] = useState([]); - - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - // UI State - const [isEditing, setIsEditing] = useState(false); - const [editForm, setEditForm] = useState>({}); - const [activeTab, setActiveTab] = useState<'info' | 'orders' | 'loyalty' | 'communication'>('info'); - const [showAddNote, setShowAddNote] = useState(false); - const [newNote, setNewNote] = useState(''); - const [showLoyaltyModal, setShowLoyaltyModal] = useState(false); - const [rewardToRedeem, setRewardToRedeem] = useState(null); - - // Pagination for order history - const [orderPage, setOrderPage] = useState(1); - const [orderPageSize, setOrderPageSize] = useState(10); - - // Effects - useEffect(() => { - if (customerId) { - loadCustomerData(customerId); - } - }, [customerId]); - - // Load customer data - const loadCustomerData = async (id: string) => { - setLoading(true); - setError(null); - - try { - // In a real app, these would be separate API calls - const mockCustomer: Customer = { - id, - name: 'María García López', - email: 'maria.garcia@email.com', - phone: '+34 612 345 678', - address: 'Calle Mayor, 123', - city: 'Madrid', - postal_code: '28001', - birth_date: '1985-05-15', - registration_date: '2023-01-15T00:00:00Z', - status: CustomerStatus.VIP, - segment: CustomerSegment.PREMIUM, - loyalty_points: 2450, - preferred_channel: SalesChannel.ONLINE, - notes: 'Cliente preferente. Le gusta el pan integral sin gluten.', - tags: ['gluten-free', 'premium', 'weekly-order'], - communication_preferences: { - email_marketing: true, - sms_notifications: true, - push_notifications: false, - promotional_offers: true, - order_updates: true, - loyalty_updates: true - } - }; - - const mockStats: CustomerStats = { - total_orders: 47, - total_spent: 1247.85, - average_order_value: 26.55, - last_order_date: '2024-01-20T10:30:00Z', - favorite_products: ['Pan Integral', 'Croissant de Chocolate', 'Tarta de Santiago'], - preferred_times: ['09:00-11:00', '17:00-19:00'], - loyalty_tier: LoyaltyTier.GOLD, - lifetime_value: 1850.00, - churn_risk: ChurnRisk.LOW - }; - - const mockOrders: CustomerOrder[] = Array.from({ length: 47 }, (_, i) => ({ - id: `order_${i + 1}`, - date: new Date(Date.now() - i * 7 * 24 * 60 * 60 * 1000).toISOString(), - total: Math.random() * 50 + 15, - items_count: Math.floor(Math.random() * 5) + 1, - status: Object.values(OrderStatus)[Math.floor(Math.random() * Object.values(OrderStatus).length)], - channel: Object.values(SalesChannel)[Math.floor(Math.random() * Object.values(SalesChannel).length)], - payment_method: Object.values(PaymentMethod)[Math.floor(Math.random() * Object.values(PaymentMethod).length)], - products: ['Pan Integral', 'Croissant', 'Magdalenas'].slice(0, Math.floor(Math.random() * 3) + 1) - })); - - const mockLoyalty: LoyaltyProgram = { - current_points: 2450, - points_to_next_tier: 550, - current_tier: LoyaltyTier.GOLD, - benefits: [ - 'Descuento 10% en todos los productos', - 'Producto gratis cada 10 compras', - 'Reserva prioritaria para productos especiales', - 'Invitaciones exclusivas a eventos' - ], - rewards_history: Array.from({ length: 20 }, (_, i) => ({ - id: `loyalty_${i + 1}`, - date: new Date(Date.now() - i * 14 * 24 * 60 * 60 * 1000).toISOString(), - type: Math.random() > 0.7 ? 'redeemed' as const : 'earned' as const, - points: Math.floor(Math.random() * 200) + 50, - description: Math.random() > 0.7 ? 'Canje por producto gratis' : 'Puntos ganados por compra', - order_id: `order_${Math.floor(Math.random() * 47) + 1}` - })) - }; - - const mockPaymentMethods: PaymentMethod[] = [ - { - id: 'pm_1', - type: 'credit_card', - last_four: '4242', - brand: 'Visa', - is_default: true, - expires_at: '2026-12-31' - }, - { - id: 'pm_2', - type: 'digital_wallet', - last_four: 'PayPal', - is_default: false - } - ]; - - setCustomer(mockCustomer); - setCustomerStats(mockStats); - setOrderHistory(mockOrders); - setLoyaltyProgram(mockLoyalty); - setPaymentMethods(mockPaymentMethods); - } catch (err) { - setError('Error al cargar datos del cliente'); - console.error('Error loading customer data:', err); - } finally { - setLoading(false); - } - }; - - // Handle edit - const handleEditStart = () => { - if (customer) { - setEditForm({ ...customer }); - setIsEditing(true); - } - }; - - const handleEditSave = async () => { - if (!customer || !editForm) return; - - try { - const updatedCustomer = { ...customer, ...editForm }; - setCustomer(updatedCustomer); - onCustomerUpdate?.(customer.id, editForm); - setIsEditing(false); - setEditForm({}); - } catch (err) { - setError('Error al actualizar cliente'); - } - }; - - const handleEditCancel = () => { - setIsEditing(false); - setEditForm({}); - }; - - // Handle notes - const handleAddNote = () => { - if (!customer || !newNote.trim()) return; - - const updatedNotes = customer.notes ? `${customer.notes}\n\n${new Date().toLocaleDateString('es-ES')}: ${newNote}` : newNote; - const updatedCustomer = { ...customer, notes: updatedNotes }; - setCustomer(updatedCustomer); - onCustomerUpdate?.(customer.id, { notes: updatedNotes }); - setNewNote(''); - setShowAddNote(false); - }; - - // Handle loyalty redemption - const handleRedeemPoints = (rewardId: string, pointsCost: number) => { - if (!customer || !loyaltyProgram) return; - - if (loyaltyProgram.current_points >= pointsCost) { - const newTransaction: LoyaltyTransaction = { - id: `loyalty_${Date.now()}`, - date: new Date().toISOString(), - type: 'redeemed', - points: -pointsCost, - description: `Canje: ${rewardId}`, - }; - - const updatedLoyalty = { - ...loyaltyProgram, - current_points: loyaltyProgram.current_points - pointsCost, - rewards_history: [newTransaction, ...loyaltyProgram.rewards_history] - }; - - const updatedCustomer = { - ...customer, - loyalty_points: loyaltyProgram.current_points - pointsCost - }; - - setLoyaltyProgram(updatedLoyalty); - setCustomer(updatedCustomer); - onCustomerUpdate?.(customer.id, { loyalty_points: updatedCustomer.loyalty_points }); - } - }; - - // Filtered order history - const paginatedOrders = useMemo(() => { - const start = (orderPage - 1) * orderPageSize; - return orderHistory.slice(start, start + orderPageSize); - }, [orderHistory, orderPage, orderPageSize]); - - if (loading) { - return ( -
-
- Cargando información del cliente... -
- ); - } - - if (error || !customer) { - return ( -
-
-
- - - -
-

Error

-

{error || 'Cliente no encontrado'}

-
-
-
-
- ); - } - - return ( -
- {/* Header */} - -
-
- -
-

{customer.name}

-
- - {StatusLabels[customer.status]} - - - {SegmentLabels[customer.segment]} - - {customerStats && ( - - {TierLabels[customerStats.loyalty_tier]} - - )} -
-
-
- -
- {allowEditing && ( - <> - {isEditing ? ( - <> - - - - ) : ( - - )} - - )} - - -
-
-
- - {/* Stats Overview */} - {customerStats && ( -
- -
-
-

Total Gastado

-

- €{customerStats.total_spent.toFixed(2)} -

-
-
- - - -
-
-
- - -
-
-

Pedidos Totales

-

{customerStats.total_orders}

-
-
- - - -
-
-
- - -
-
-

Ticket Promedio

-

- €{customerStats.average_order_value.toFixed(2)} -

-
-
- - - -
-
-
- - -
-
-

Riesgo de Fuga

-
- - {ChurnRiskLabels[customerStats.churn_risk]} - -
-
-
- - - -
-
-
-
- )} - - {/* Tabs */} -
- -
- - {/* Tab Content */} - {activeTab === 'info' && ( -
- -

Datos Personales

-
-
-
- - {isEditing ? ( - setEditForm(prev => ({ ...prev, name: e.target.value }))} - /> - ) : ( -

{customer.name}

- )} -
-
- - {isEditing ? ( - setEditForm(prev => ({ ...prev, email: e.target.value }))} - /> - ) : ( -

{customer.email}

- )} -
-
- -
-
- - {isEditing ? ( - setEditForm(prev => ({ ...prev, phone: e.target.value }))} - /> - ) : ( -

{customer.phone}

- )} -
-
- - {isEditing ? ( - setEditForm(prev => ({ ...prev, birth_date: e.target.value }))} - /> - ) : ( -

- {customer.birth_date ? new Date(customer.birth_date).toLocaleDateString('es-ES') : 'No especificado'} -

- )} -
-
- -
- - {isEditing ? ( - setEditForm(prev => ({ ...prev, address: e.target.value }))} - /> - ) : ( -

{customer.address}

- )} -
- -
-
- - {isEditing ? ( - setEditForm(prev => ({ ...prev, city: e.target.value }))} - /> - ) : ( -

{customer.city}

- )} -
-
- - {isEditing ? ( - setEditForm(prev => ({ ...prev, postal_code: e.target.value }))} - /> - ) : ( -

{customer.postal_code}

- )} -
-
-
-
- - -

Preferencias y Segmentación

-
-
- - {isEditing ? ( - { - if (isEditing || allowEditing) { - const updated = { - ...customer, - communication_preferences: { - ...customer.communication_preferences, - email_marketing: e.target.checked - } - }; - setCustomer(updated); - onCustomerUpdate?.(customer.id, { communication_preferences: updated.communication_preferences }); - } - }} - className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" - disabled={!isEditing && !allowEditing} - /> - Marketing por email - - - - - -
-
- -
-

Tipos de Comunicación

-
- - - - - -
-
-
- - {paymentMethods.length > 0 && ( -
-

Métodos de Pago

-
- {paymentMethods.map(method => ( -
-
-
- - - -
-
-

- {method.brand} •••• {method.last_four} -

- {method.expires_at && ( -

- Expira {new Date(method.expires_at).toLocaleDateString('es-ES')} -

- )} -
-
- {method.is_default && ( - Por defecto - )} -
- ))} -
-
- )} -
- - )} - - {/* Add Note Modal */} - setShowAddNote(false)} - title="Agregar Nota" - > -
-
- -