// Analytics View Component const { useState, useEffect, useRef, useMemo } = React; // Chart colors - using a distinctive palette const CHART_COLORS = { all: { primary: 'rgb(100, 116, 139)', // slate-500 background: 'rgba(100, 116, 139, 0.7)', border: 'rgb(71, 85, 105)', // slate-600 }, email: { primary: 'rgb(59, 130, 246)', // blue-500 background: 'rgba(59, 130, 246, 0.7)', border: 'rgb(37, 99, 235)', // blue-600 }, crif: { primary: 'rgb(236, 72, 153)', // pink-500 (brand) background: 'rgba(236, 72, 153, 0.7)', border: 'rgb(219, 39, 119)', // pink-600 } }; // Status-specific colors for the completion status chart const STATUS_COLORS = { success: { background: 'rgba(16, 185, 129, 0.7)', border: 'rgb(5, 150, 105)' }, // emerald error: { background: 'rgba(239, 68, 68, 0.7)', border: 'rgb(220, 38, 38)' }, // red cancelled: { background: 'rgba(251, 191, 36, 0.7)', border: 'rgb(245, 158, 11)' } // amber }; const FILTER_OPTIONS = [ { value: 'all', label: 'Tutti', color: CHART_COLORS.all.primary }, { value: 'email', label: 'Solo Email', color: CHART_COLORS.email.primary }, { value: 'crif', label: 'Email + CRIF', color: CHART_COLORS.crif.primary }, ]; // Filter toggle component const ChartFilter = ({ value, onChange }) => (
{FILTER_OPTIONS.map(option => ( ))}
); // Status legend component for the completion status chart const StatusLegend = () => (
OK
Errore
Annullato
); // Reusable chart component const ChartCard = ({ chartRef, title, subtitle, icon, filter, onFilterChange, showFilter = true, customLegend = null }) => (

{icon} {title}

{subtitle &&

{subtitle}

}
{showFilter && }
{customLegend &&
{customLegend}
}
); const AnalyticsView = () => { // Chart refs const accountsChartRef = useRef(null); const timeChartRef = useRef(null); const statusChartRef = useRef(null); const accountsChartInstance = useRef(null); const timeChartInstance = useRef(null); const statusChartInstance = useRef(null); // Chart data state const [chartData, setChartData] = useState(null); const [chartLoading, setChartLoading] = useState(true); const [chartError, setChartError] = useState(null); // Filter states for each chart const [accountsFilter, setAccountsFilter] = useState('all'); const [timeFilter, setTimeFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState('all'); // Fetch chart data useEffect(() => { const fetchChartData = async () => { try { setChartLoading(true); const response = await fetch('/queue/stats/charts?days=14'); const data = await response.json(); if (data.status === 'success') { setChartData(data); setChartError(null); } else { setChartError('Failed to load chart data'); } } catch (err) { console.error('Error fetching chart data:', err); setChartError('Failed to load chart data'); } finally { setChartLoading(false); } }; fetchChartData(); // Refresh every 30 seconds const interval = setInterval(fetchChartData, 30000); return () => clearInterval(interval); }, []); // Get data for a specific filter const getFilteredData = (datasets, filter) => { if (filter === 'all') { // Sum email and crif for each data point return datasets.email.map((val, idx) => val + datasets.crif[idx]); } return datasets[filter]; }; // Get color config for filter const getColorConfig = (filter) => CHART_COLORS[filter]; // Compute a single aligned Y-axis max for ALL charts so they have the same height const globalMaxY = useMemo(() => { if (!chartData) return 5; // Get max values across all charts and all filter combinations const accountsAllMax = Math.max(...chartData.accounts_per_day.datasets.email.map((v, i) => v + chartData.accounts_per_day.datasets.crif[i]), 1); const timeAllMax = Math.max(...chartData.completion_times.datasets.email.map((v, i) => v + chartData.completion_times.datasets.crif[i]), 1); const statusAllMax = Math.max(...chartData.completion_status.datasets.email.map((v, i) => v + chartData.completion_status.datasets.crif[i]), 1); // Find the overall maximum across all charts const overallMax = Math.max(accountsAllMax, timeAllMax, statusAllMax); // Round up to next nice number if (overallMax <= 5) return 5; if (overallMax <= 10) return 10; return Math.ceil(overallMax / 5) * 5; }, [chartData]); // Common chart options generator - uses globalMaxY for consistent axis alignment const getChartOptions = (tooltipLabel) => ({ responsive: true, maintainAspectRatio: false, animation: { duration: 300, easing: 'easeOutQuart', }, plugins: { legend: { display: false }, tooltip: { backgroundColor: 'rgb(15, 23, 42)', titleColor: 'rgb(241, 245, 249)', bodyColor: 'rgb(203, 213, 225)', padding: 12, cornerRadius: 8, displayColors: false, callbacks: { label: (ctx) => `${ctx.parsed.y} ${tooltipLabel}` } } }, scales: { x: { grid: { display: false }, ticks: { color: 'rgb(100, 116, 139)', font: { size: 10 } } }, y: { beginAtZero: true, max: globalMaxY, grid: { color: 'rgba(100, 116, 139, 0.1)' }, ticks: { color: 'rgb(100, 116, 139)', font: { size: 10 }, stepSize: globalMaxY <= 10 ? 1 : Math.ceil(globalMaxY / 5), } } } }); // Initialize/update accounts chart useEffect(() => { if (!chartData || !accountsChartRef.current) return; if (accountsChartInstance.current) { accountsChartInstance.current.destroy(); } const formattedLabels = chartData.accounts_per_day.labels.map(dateStr => { const date = new Date(dateStr); return `${date.getMonth() + 1}/${date.getDate()}`; }); const colorConfig = getColorConfig(accountsFilter); const data = getFilteredData(chartData.accounts_per_day.datasets, accountsFilter); const accountsCtx = accountsChartRef.current.getContext('2d'); accountsChartInstance.current = new Chart(accountsCtx, { type: 'bar', data: { labels: formattedLabels, datasets: [{ label: FILTER_OPTIONS.find(o => o.value === accountsFilter)?.label || 'Account', data: data, backgroundColor: colorConfig.background, borderColor: colorConfig.border, borderWidth: 1, borderRadius: 4, borderSkipped: false, }] }, options: getChartOptions('account') }); return () => { if (accountsChartInstance.current) { accountsChartInstance.current.destroy(); } }; }, [chartData, accountsFilter, globalMaxY]); // Initialize/update time distribution chart useEffect(() => { if (!chartData || !timeChartRef.current) return; if (timeChartInstance.current) { timeChartInstance.current.destroy(); } const colorConfig = getColorConfig(timeFilter); const data = getFilteredData(chartData.completion_times.datasets, timeFilter); const timeCtx = timeChartRef.current.getContext('2d'); timeChartInstance.current = new Chart(timeCtx, { type: 'bar', data: { labels: chartData.completion_times.labels.map(l => l + 'm'), datasets: [{ label: FILTER_OPTIONS.find(o => o.value === timeFilter)?.label || 'Completamenti', data: data, backgroundColor: colorConfig.background, borderColor: colorConfig.border, borderWidth: 1, borderRadius: 4, borderSkipped: false, }] }, options: getChartOptions('automazioni') }); return () => { if (timeChartInstance.current) { timeChartInstance.current.destroy(); } }; }, [chartData, timeFilter, globalMaxY]); // Initialize/update status chart useEffect(() => { if (!chartData || !statusChartRef.current) return; if (statusChartInstance.current) { statusChartInstance.current.destroy(); } const data = getFilteredData(chartData.completion_status.datasets, statusFilter); // Use status-specific colors for this chart const backgroundColors = [ STATUS_COLORS.success.background, STATUS_COLORS.error.background, STATUS_COLORS.cancelled.background ]; const borderColors = [ STATUS_COLORS.success.border, STATUS_COLORS.error.border, STATUS_COLORS.cancelled.border ]; const statusCtx = statusChartRef.current.getContext('2d'); statusChartInstance.current = new Chart(statusCtx, { type: 'bar', data: { labels: chartData.completion_status.labels, datasets: [{ label: FILTER_OPTIONS.find(o => o.value === statusFilter)?.label || 'Status', data: data, backgroundColor: backgroundColors, borderColor: borderColors, borderWidth: 1, borderRadius: 4, borderSkipped: false, }] }, options: getChartOptions('lavori') }); return () => { if (statusChartInstance.current) { statusChartInstance.current.destroy(); } }; }, [chartData, statusFilter, globalMaxY]); return (
{/* Page Header */}

Statistiche e Analisi

Panoramica delle performance della pipeline

{/* Summary Stats */} {chartData?.summary && (
Totale Completati
{chartData.summary.total_completed}
Solo Email
{chartData.summary.total_email}
Email + CRIF
{chartData.summary.total_crif}
Tempo Medio Email
{chartData.summary.avg_time_email} min
Tempo Medio CRIF
{chartData.summary.avg_time_crif} min
)} {/* Charts Grid - 3 columns */} {chartLoading ? (
) : chartError ? (
📊

{chartError}

) : (
} />
)}
); }; window.AnalyticsView = AnalyticsView;