// 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 = () => (
);
// 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;