2025-08-28 10:41:04 +02:00
|
|
|
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<AnalyticsDashboardProps> = ({
|
|
|
|
|
className = '',
|
|
|
|
|
initialTimeRange = 'thisMonth',
|
|
|
|
|
showFilters = true,
|
|
|
|
|
showExport = true,
|
|
|
|
|
customCharts = [],
|
|
|
|
|
onMetricsLoad,
|
|
|
|
|
onExport,
|
|
|
|
|
}) => {
|
|
|
|
|
const [selectedTimeRange, setSelectedTimeRange] = useState<TimeRange>(initialTimeRange);
|
|
|
|
|
const [customDateRange, setCustomDateRange] = useState<{ from: Date; to: Date } | null>(null);
|
|
|
|
|
const [bakeryMetrics, setBakeryMetrics] = useState<BakeryMetrics | null>(null);
|
|
|
|
|
const [reports, setReports] = useState<AnalyticsReport[]>([]);
|
|
|
|
|
const [appliedFilters, setAppliedFilters] = useState<AppliedFilter[]>([]);
|
|
|
|
|
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<string[]>([]);
|
|
|
|
|
|
|
|
|
|
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-[var(--color-info)]') => (
|
|
|
|
|
<Card className="p-4">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm text-[var(--text-secondary)]">{title}</p>
|
|
|
|
|
<p className={`text-2xl font-bold ${colorClass}`}>{value}</p>
|
|
|
|
|
{subtitle && <p className="text-sm text-[var(--text-tertiary)]">{subtitle}</p>}
|
|
|
|
|
{trend !== undefined && (
|
|
|
|
|
<div className="flex items-center mt-1">
|
|
|
|
|
<span className={`text-xs ${trend >= 0 ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'}`}>
|
|
|
|
|
{trend >= 0 ? '↗' : '↘'} {Math.abs(trend).toFixed(1)}%
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{icon && <span className="text-2xl">{icon}</span>}
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const renderOverviewDashboard = () => {
|
|
|
|
|
if (!bakeryMetrics) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
{/* KPI Cards */}
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
|
|
|
{renderKPICard(
|
|
|
|
|
'Ingresos Totales',
|
|
|
|
|
`€${bakeryMetrics.sales.total_revenue.toLocaleString()}`,
|
|
|
|
|
undefined,
|
|
|
|
|
bakeryMetrics.sales.revenue_growth,
|
|
|
|
|
'💰',
|
|
|
|
|
'text-[var(--color-success)]'
|
|
|
|
|
)}
|
|
|
|
|
{renderKPICard(
|
|
|
|
|
'Pedidos',
|
|
|
|
|
bakeryMetrics.sales.total_orders.toLocaleString(),
|
|
|
|
|
`Ticket medio: €${bakeryMetrics.sales.average_order_value.toFixed(2)}`,
|
|
|
|
|
bakeryMetrics.sales.order_growth,
|
|
|
|
|
'📦',
|
|
|
|
|
'text-[var(--color-info)]'
|
|
|
|
|
)}
|
|
|
|
|
{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-[var(--color-primary)]'
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Charts Grid */}
|
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
|
|
|
<Card className="p-6">
|
|
|
|
|
<h3 className="text-lg font-semibold mb-4">Ventas por Canal</h3>
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{bakeryMetrics.sales.sales_by_channel.map((channel, index) => (
|
|
|
|
|
<div key={index} className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded-lg">
|
|
|
|
|
<div>
|
|
|
|
|
<p className="font-medium">{channel.channel}</p>
|
|
|
|
|
<p className="text-sm text-[var(--text-secondary)]">
|
|
|
|
|
{channel.orders} pedidos • {channel.customers} clientes
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-right">
|
|
|
|
|
<p className="font-semibold text-[var(--color-success)]">
|
|
|
|
|
€{channel.revenue.toLocaleString()}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-sm text-[var(--text-secondary)]">
|
|
|
|
|
Conv. {channel.conversion_rate.toFixed(1)}%
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
<Card className="p-6">
|
|
|
|
|
<h3 className="text-lg font-semibold mb-4">Productos Top</h3>
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{bakeryMetrics.sales.top_products.map((product, index) => (
|
|
|
|
|
<div key={product.product_id} className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded-lg">
|
|
|
|
|
<div>
|
|
|
|
|
<p className="font-medium">{product.product_name}</p>
|
|
|
|
|
<p className="text-sm text-[var(--text-secondary)]">
|
|
|
|
|
{product.category} • {product.quantity_sold} vendidos
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-right">
|
|
|
|
|
<p className="font-semibold text-[var(--color-success)]">
|
|
|
|
|
€{product.revenue.toLocaleString()}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-sm text-[var(--text-secondary)]">
|
|
|
|
|
Margen {product.profit_margin.toFixed(1)}%
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Operational Metrics */}
|
|
|
|
|
<Card className="p-6">
|
|
|
|
|
<h3 className="text-lg font-semibold mb-4">Métricas Operacionales</h3>
|
|
|
|
|
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-4">
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<p className="text-2xl font-bold text-[var(--color-info)]">
|
|
|
|
|
{bakeryMetrics.operational.staff_productivity.toFixed(1)}%
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-sm text-[var(--text-secondary)]">Productividad Personal</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<p className="text-2xl font-bold text-[var(--color-success)]">
|
|
|
|
|
{bakeryMetrics.operational.equipment_uptime.toFixed(1)}%
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-sm text-[var(--text-secondary)]">Tiempo Activo Equipos</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<p className="text-2xl font-bold text-[var(--color-primary)]">
|
|
|
|
|
{bakeryMetrics.production.waste_percentage.toFixed(1)}%
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-sm text-[var(--text-secondary)]">Desperdicio</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<p className="text-2xl font-bold text-purple-600">
|
|
|
|
|
{bakeryMetrics.operational.food_safety_score.toFixed(1)}%
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-sm text-[var(--text-secondary)]">Seguridad Alimentaria</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<p className="text-2xl font-bold text-[var(--color-error)]">
|
|
|
|
|
{bakeryMetrics.inventory.stockout_rate.toFixed(1)}%
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-sm text-[var(--text-secondary)]">Roturas Stock</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<p className="text-2xl font-bold text-indigo-600">
|
|
|
|
|
{bakeryMetrics.customer.customer_retention_rate.toFixed(1)}%
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-sm text-[var(--text-secondary)]">Retención Clientes</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<p className="text-2xl font-bold text-pink-600">
|
|
|
|
|
€{bakeryMetrics.customer.customer_lifetime_value.toFixed(0)}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-sm text-[var(--text-secondary)]">Valor Cliente</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<p className="text-2xl font-bold text-teal-600">
|
|
|
|
|
{bakeryMetrics.inventory.turnover_rate.toFixed(1)}
|
|
|
|
|
</p>
|
2025-09-17 16:06:30 +02:00
|
|
|
<p className="text-sm text-[var(--text-secondary)]">Velocidad de Venta</p>
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const renderViewContent = () => {
|
|
|
|
|
switch (activeView) {
|
|
|
|
|
case 'overview':
|
|
|
|
|
return renderOverviewDashboard();
|
|
|
|
|
case 'reports':
|
|
|
|
|
return (
|
|
|
|
|
<Card className="p-6">
|
|
|
|
|
<ReportsTable
|
|
|
|
|
reports={reports}
|
|
|
|
|
loading={loading}
|
|
|
|
|
onReportClick={(report) => 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)
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
default:
|
|
|
|
|
return (
|
|
|
|
|
<Card className="p-8 text-center">
|
|
|
|
|
<p className="text-[var(--text-tertiary)]">Vista en desarrollo: {activeView}</p>
|
|
|
|
|
<p className="text-sm text-[var(--text-tertiary)] mt-2">
|
|
|
|
|
Esta vista estará disponible próximamente con métricas detalladas.
|
|
|
|
|
</p>
|
|
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className={`space-y-6 ${className}`}>
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4">
|
|
|
|
|
<div>
|
|
|
|
|
<h1 className="text-3xl font-bold text-[var(--text-primary)]">Analytics Dashboard</h1>
|
|
|
|
|
<p className="text-[var(--text-secondary)]">Análisis integral de rendimiento de la panadería</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
<Select
|
|
|
|
|
value={selectedTimeRange}
|
|
|
|
|
onChange={(e) => handleTimeRangeChange(e.target.value as TimeRange)}
|
|
|
|
|
className="w-48"
|
|
|
|
|
>
|
|
|
|
|
{DEFAULT_TIME_RANGES.map((range) => (
|
|
|
|
|
<option key={range.value} value={range.value}>
|
|
|
|
|
{range.label}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</Select>
|
|
|
|
|
|
|
|
|
|
{selectedTimeRange === 'custom' && (
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<DatePicker
|
|
|
|
|
value={customDateRange?.from}
|
|
|
|
|
onChange={(date) => setCustomDateRange(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
from: date || new Date()
|
|
|
|
|
} as { from: Date; to: Date }))}
|
|
|
|
|
placeholder="Fecha inicio"
|
|
|
|
|
/>
|
|
|
|
|
<DatePicker
|
|
|
|
|
value={customDateRange?.to}
|
|
|
|
|
onChange={(date) => setCustomDateRange(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
to: date || new Date()
|
|
|
|
|
} as { from: Date; to: Date }))}
|
|
|
|
|
placeholder="Fecha fin"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{showExport && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => setIsExportModalOpen(true)}
|
|
|
|
|
disabled={loading}
|
|
|
|
|
>
|
|
|
|
|
📊 Exportar
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={loadAnalyticsData}
|
|
|
|
|
disabled={loading}
|
|
|
|
|
>
|
|
|
|
|
{loading ? '🔄 Cargando...' : '🔄 Actualizar'}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Navigation Tabs */}
|
|
|
|
|
<div className="border-b border-[var(--border-primary)]">
|
|
|
|
|
<nav className="flex space-x-8">
|
|
|
|
|
{[
|
|
|
|
|
{ key: 'overview', label: 'Resumen', icon: '📊' },
|
|
|
|
|
{ key: 'sales', label: 'Ventas', icon: '💰' },
|
|
|
|
|
{ key: 'production', label: 'Producción', icon: '🏭' },
|
|
|
|
|
{ key: 'inventory', label: 'Inventario', icon: '📦' },
|
|
|
|
|
{ key: 'financial', label: 'Financiero', icon: '💳' },
|
|
|
|
|
{ key: 'reports', label: 'Reportes', icon: '📋' },
|
|
|
|
|
].map((tab) => (
|
|
|
|
|
<button
|
|
|
|
|
key={tab.key}
|
|
|
|
|
onClick={() => setActiveView(tab.key as any)}
|
|
|
|
|
className={`py-2 px-1 border-b-2 font-medium text-sm whitespace-nowrap ${
|
|
|
|
|
activeView === tab.key
|
|
|
|
|
? 'border-blue-500 text-[var(--color-info)]'
|
|
|
|
|
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<span className="mr-1">{tab.icon}</span>
|
|
|
|
|
{tab.label}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</nav>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Content */}
|
|
|
|
|
{loading ? (
|
|
|
|
|
<div className="flex justify-center items-center h-64">
|
|
|
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
renderViewContent()
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Export Modal */}
|
|
|
|
|
{isExportModalOpen && (
|
|
|
|
|
<ExportOptions
|
|
|
|
|
type="report"
|
|
|
|
|
title={`Analytics Dashboard - ${activeView}`}
|
|
|
|
|
description="Exportar datos del dashboard de analytics"
|
|
|
|
|
onExport={handleExport}
|
|
|
|
|
loading={false}
|
|
|
|
|
showScheduling={true}
|
|
|
|
|
showTemplates={true}
|
|
|
|
|
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4"
|
|
|
|
|
onClose={() => setIsExportModalOpen(false)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default AnalyticsDashboard;
|