|
|
@@ -33,7 +33,6 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- Main Charts Section - Protected with wire:ignore -->
|
|
|
<div wire:ignore>
|
|
|
<div class="chart-row">
|
|
|
<div class="chart-card">
|
|
|
@@ -106,13 +105,10 @@
|
|
|
@if($selectedCourse)
|
|
|
<div wire:ignore wire:key="course-chart-{{ $seasonFilter }}-{{ $selectedCourse }}">
|
|
|
<div class="modern-chart-layout">
|
|
|
- <!-- Delta Table on the left -->
|
|
|
<div class="course-delta-table"
|
|
|
id="course-delta-table-{{ str_replace('-', '', $seasonFilter) }}-{{ $selectedCourse }}">
|
|
|
- <!-- Table will be populated by JavaScript -->
|
|
|
</div>
|
|
|
|
|
|
- <!-- Chart on the right -->
|
|
|
<div class="modern-chart-container">
|
|
|
<canvas
|
|
|
id="courses-chart-{{ str_replace('-', '', $seasonFilter) }}-{{ $selectedCourse }}"></canvas>
|
|
|
@@ -135,7 +131,6 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- Single JavaScript section -->
|
|
|
<script>
|
|
|
window.ReportsChartManager = window.ReportsChartManager || {
|
|
|
charts: {},
|
|
|
@@ -154,7 +149,6 @@
|
|
|
});
|
|
|
},
|
|
|
|
|
|
- // NEW: Method to destroy season-specific charts
|
|
|
destroySeasonCharts: function (oldSeasonKey) {
|
|
|
const chartsToDestroy = Object.keys(this.charts).filter(chartId =>
|
|
|
chartId.includes(oldSeasonKey)
|
|
|
@@ -162,13 +156,11 @@
|
|
|
chartsToDestroy.forEach(chartId => this.destroyChart(chartId));
|
|
|
},
|
|
|
|
|
|
- // Replace your updateMainCharts method with this version that uses fixed canvas IDs:
|
|
|
updateMainCharts: function () {
|
|
|
console.log('=== updateMainCharts called ===');
|
|
|
|
|
|
const seasonFilter = @this.get('seasonFilter');
|
|
|
|
|
|
- // Update titles
|
|
|
const monthlyTitle = document.getElementById('monthly-season-title');
|
|
|
const causalsTitle = document.getElementById('causals-season-title');
|
|
|
if (monthlyTitle) {
|
|
|
@@ -178,11 +170,9 @@
|
|
|
causalsTitle.textContent = seasonFilter;
|
|
|
}
|
|
|
|
|
|
- // Use the original canvas IDs from the blade template
|
|
|
const originalSeasonKey = '{{ str_replace('-', '', $seasonFilter) }}';
|
|
|
console.log('Using original season key for canvas IDs:', originalSeasonKey);
|
|
|
|
|
|
- // Get fresh data and update charts
|
|
|
@this.call('getMonthlyTotals').then(monthlyData => {
|
|
|
console.log('Got monthly data:', monthlyData);
|
|
|
this.createMonthlyChart(originalSeasonKey, monthlyData);
|
|
|
@@ -200,10 +190,9 @@
|
|
|
this.updateMembersTable(membersData);
|
|
|
});
|
|
|
},
|
|
|
- // Add this new method to force chart updates
|
|
|
forceUpdateCharts: function () {
|
|
|
console.log('Force updating charts...');
|
|
|
- this.currentSeason = null; // Reset to force update
|
|
|
+ this.currentSeason = null;
|
|
|
this.updateMainCharts();
|
|
|
},
|
|
|
|
|
|
@@ -298,6 +287,7 @@
|
|
|
});
|
|
|
},
|
|
|
|
|
|
+ // Replace your createCausalsChart method with this fixed version:
|
|
|
createCausalsChart: function (seasonKey, causalsData) {
|
|
|
const chartId = `causals-chart-${seasonKey}`;
|
|
|
const canvas = document.getElementById(chartId);
|
|
|
@@ -320,13 +310,16 @@
|
|
|
'rgba(231, 76, 60, 0.8)'
|
|
|
];
|
|
|
|
|
|
+ const dataValues = causalsData.inData.map(item => parseFloat(item.value));
|
|
|
+ const total = dataValues.reduce((sum, value) => sum + value, 0);
|
|
|
+
|
|
|
this.charts[chartId] = new Chart(ctx, {
|
|
|
type: 'doughnut',
|
|
|
data: {
|
|
|
labels: causalsData.inLabels,
|
|
|
datasets: [{
|
|
|
label: 'Importo',
|
|
|
- data: causalsData.inData.map(item => item.value),
|
|
|
+ data: dataValues,
|
|
|
backgroundColor: colors,
|
|
|
borderColor: colors.map(color => color.replace('0.8', '1')),
|
|
|
borderWidth: 2,
|
|
|
@@ -356,10 +349,12 @@
|
|
|
callbacks: {
|
|
|
label: function (context) {
|
|
|
const value = context.raw;
|
|
|
- const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
|
|
- const percentage = Math.round((value / total) * 100);
|
|
|
+ const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0;
|
|
|
return context.label + ': €' +
|
|
|
- new Intl.NumberFormat('it-IT').format(value) +
|
|
|
+ new Intl.NumberFormat('it-IT', {
|
|
|
+ minimumFractionDigits: 2,
|
|
|
+ maximumFractionDigits: 2
|
|
|
+ }).format(value) +
|
|
|
` (${percentage}%)`;
|
|
|
}
|
|
|
}
|
|
|
@@ -631,25 +626,43 @@
|
|
|
|
|
|
if (!canvas) {
|
|
|
console.log('Canvas not found for chart ID:', chartId);
|
|
|
+
|
|
|
+ const chartContainer = document.querySelector('.modern-chart-container');
|
|
|
+ if (chartContainer) {
|
|
|
+ chartContainer.innerHTML = `
|
|
|
+ <div class="chart-empty-state">
|
|
|
+ <div style="text-align: center; padding: 4rem 2rem;">
|
|
|
+ <div style="font-size: 4rem; margin-bottom: 1.5rem; opacity: 0.3;">📊</div>
|
|
|
+ <h3 style="font-size: 1.5rem; font-weight: 600; margin-bottom: 1rem; color: #374151;">
|
|
|
+ Grafico non disponibile
|
|
|
+ </h3>
|
|
|
+ <p style="font-size: 1rem; opacity: 0.7; margin: 0; max-width: 400px; margin-left: auto; margin-right: auto; line-height: 1.5;">
|
|
|
+ Il grafico per questo corso non può essere visualizzato nella stagione selezionata.
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (tableContainer) {
|
|
|
+ tableContainer.innerHTML = '';
|
|
|
+ }
|
|
|
+
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
this.destroyChart(chartId);
|
|
|
|
|
|
- // Call Livewire method to get fresh data
|
|
|
@this.call('getCourseData', selectedCourseValue).then(courseData => {
|
|
|
console.log('Received course data:', courseData);
|
|
|
|
|
|
- // Check if data is empty
|
|
|
if (courseData.isEmpty) {
|
|
|
console.log('No data available for course, showing message');
|
|
|
|
|
|
- // Clear the table
|
|
|
if (tableContainer) {
|
|
|
tableContainer.innerHTML = '';
|
|
|
}
|
|
|
|
|
|
- // Show message instead of chart
|
|
|
const chartContainer = canvas.parentElement;
|
|
|
chartContainer.innerHTML = `
|
|
|
<div class="chart-empty-state">
|
|
|
@@ -672,7 +685,6 @@
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // Restore canvas if it was replaced by empty state
|
|
|
let canvasElement = document.getElementById(chartId);
|
|
|
if (!canvasElement) {
|
|
|
const chartContainer = canvas.parentElement;
|
|
|
@@ -680,38 +692,121 @@
|
|
|
canvasElement = document.getElementById(chartId);
|
|
|
}
|
|
|
|
|
|
- // Update the delta table
|
|
|
this.updateCourseTable(tableContainer, courseData.tableData);
|
|
|
|
|
|
- // Store participant data for tooltip
|
|
|
const participantData = courseData.datasets.find(d => d.participantData)?.participantData || [];
|
|
|
|
|
|
+
|
|
|
const ctx = canvasElement.getContext('2d');
|
|
|
|
|
|
- // Create gradients
|
|
|
const earnedGradient = ctx.createLinearGradient(0, 0, 0, 400);
|
|
|
earnedGradient.addColorStop(0, 'rgba(16, 185, 129, 0.9)');
|
|
|
earnedGradient.addColorStop(1, 'rgba(16, 185, 129, 0.3)');
|
|
|
|
|
|
+ const totalData = courseData.datasets.find(d => d.label === 'Pagamenti Attesi')?.data || [];
|
|
|
+ const earnedData = courseData.datasets.find(d => d.label === 'Pagamenti Effettuati')?.data || [];
|
|
|
+
|
|
|
+ const verticalMissingLinesPlugin = {
|
|
|
+ id: 'verticalMissingLines',
|
|
|
+ afterDatasetsDraw: function (chart) {
|
|
|
+ const ctx = chart.ctx;
|
|
|
+ const meta0 = chart.getDatasetMeta(0);
|
|
|
+ const meta1 = chart.getDatasetMeta(1);
|
|
|
+
|
|
|
+ ctx.save();
|
|
|
+ ctx.strokeStyle = 'rgba(239, 68, 68, 0.8)';
|
|
|
+ ctx.lineWidth = 2;
|
|
|
+ ctx.setLineDash([6, 4]);
|
|
|
+
|
|
|
+ totalData.forEach((totalValue, index) => {
|
|
|
+ const earnedValue = parseFloat(earnedData[index]) || 0;
|
|
|
+ const totalVal = parseFloat(totalValue) || 0;
|
|
|
+
|
|
|
+ if (totalVal > 0 && totalVal > earnedValue) {
|
|
|
+ let barX;
|
|
|
+ if (meta0.data[index] && earnedValue > 0) {
|
|
|
+ barX = meta0.data[index].x;
|
|
|
+ } else {
|
|
|
+ const chartArea = chart.chartArea;
|
|
|
+ const xScale = chart.scales.x;
|
|
|
+ barX = xScale.getPixelForValue(index);
|
|
|
+ }
|
|
|
+
|
|
|
+ let startY;
|
|
|
+ if (meta0.data[index] && earnedValue > 0) {
|
|
|
+ startY = meta0.data[index].y;
|
|
|
+ } else {
|
|
|
+ const yScale = chart.scales.y;
|
|
|
+ startY = yScale.getPixelForValue(0);
|
|
|
+ }
|
|
|
+
|
|
|
+ const linePoint = meta1.data[index];
|
|
|
+ if (linePoint) {
|
|
|
+ const endY = linePoint.y;
|
|
|
+
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(barX, startY);
|
|
|
+ ctx.lineTo(barX, endY);
|
|
|
+ ctx.stroke();
|
|
|
+
|
|
|
+ ctx.setLineDash([]);
|
|
|
+ ctx.lineWidth = 1;
|
|
|
+
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(barX - 3, endY);
|
|
|
+ ctx.lineTo(barX + 3, endY);
|
|
|
+ ctx.stroke();
|
|
|
+
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(barX - 3, startY);
|
|
|
+ ctx.lineTo(barX + 3, startY);
|
|
|
+ ctx.stroke();
|
|
|
+
|
|
|
+ ctx.setLineDash([6, 4]);
|
|
|
+ ctx.lineWidth = 2;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ ctx.restore();
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ Chart.register(verticalMissingLinesPlugin);
|
|
|
+
|
|
|
this.charts[chartId] = new Chart(ctx, {
|
|
|
type: 'bar',
|
|
|
data: {
|
|
|
labels: courseData.labels,
|
|
|
- datasets: courseData.datasets.map(dataset => {
|
|
|
- if (dataset.type === 'line') {
|
|
|
- return {
|
|
|
- ...dataset,
|
|
|
- type: 'line',
|
|
|
- fill: false,
|
|
|
- backgroundColor: 'transparent'
|
|
|
- };
|
|
|
- } else {
|
|
|
- return {
|
|
|
- ...dataset,
|
|
|
- backgroundColor: earnedGradient
|
|
|
- };
|
|
|
+ datasets: [
|
|
|
+ {
|
|
|
+ label: 'Pagamenti Effettuati',
|
|
|
+ backgroundColor: earnedGradient,
|
|
|
+ borderColor: 'rgba(16, 185, 129, 1)',
|
|
|
+ borderWidth: 0,
|
|
|
+ borderRadius: 8,
|
|
|
+ borderSkipped: false,
|
|
|
+ data: earnedData,
|
|
|
+ type: 'bar',
|
|
|
+ order: 2
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: 'Pagamenti Attesi',
|
|
|
+ backgroundColor: 'transparent',
|
|
|
+ borderColor: 'rgba(59, 130, 246, 1)',
|
|
|
+ borderWidth: 3,
|
|
|
+ pointBackgroundColor: 'rgba(59, 130, 246, 1)',
|
|
|
+ pointBorderColor: '#ffffff',
|
|
|
+ pointBorderWidth: 3,
|
|
|
+ pointRadius: 7,
|
|
|
+ pointHoverRadius: 9,
|
|
|
+ data: totalData,
|
|
|
+ type: 'line',
|
|
|
+ tension: 0.3,
|
|
|
+ order: 1,
|
|
|
+ participantData: participantData
|
|
|
}
|
|
|
- })
|
|
|
+ ]
|
|
|
},
|
|
|
options: {
|
|
|
responsive: true,
|
|
|
@@ -770,7 +865,27 @@
|
|
|
},
|
|
|
plugins: {
|
|
|
legend: {
|
|
|
- display: false
|
|
|
+ display: true,
|
|
|
+ position: 'top',
|
|
|
+ labels: {
|
|
|
+ usePointStyle: true,
|
|
|
+ padding: 15,
|
|
|
+ font: { weight: '500', size: 12 },
|
|
|
+ generateLabels: function (chart) {
|
|
|
+ const original = Chart.defaults.plugins.legend.labels.generateLabels(chart);
|
|
|
+
|
|
|
+ original.push({
|
|
|
+ text: 'Pagamenti Mancanti',
|
|
|
+ fillStyle: 'transparent',
|
|
|
+ strokeStyle: 'rgba(239, 68, 68, 0.8)',
|
|
|
+ lineDash: [6, 4],
|
|
|
+ lineWidth: 2,
|
|
|
+ pointStyle: 'line'
|
|
|
+ });
|
|
|
+
|
|
|
+ return original;
|
|
|
+ }
|
|
|
+ }
|
|
|
},
|
|
|
tooltip: {
|
|
|
backgroundColor: 'rgba(255, 255, 255, 0.98)',
|
|
|
@@ -799,6 +914,20 @@
|
|
|
let label = context.dataset.label + ': €' +
|
|
|
new Intl.NumberFormat('it-IT').format(context.parsed.y);
|
|
|
|
|
|
+ if (context.dataset.label === 'Pagamenti Effettuati') {
|
|
|
+ const earnedValue = parseFloat(context.parsed.y) || 0;
|
|
|
+ const totalValue = parseFloat(totalData[context.dataIndex]) || 0;
|
|
|
+ const missingValue = Math.max(0, totalValue - earnedValue);
|
|
|
+
|
|
|
+ if (participantData[context.dataIndex]) {
|
|
|
+ label += '\n👥 Partecipanti: ' + participantData[context.dataIndex];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (missingValue > 0) {
|
|
|
+ label += '\n🔴 Mancanti: €' + new Intl.NumberFormat('it-IT').format(missingValue);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
if (context.dataset.label === 'Pagamenti Attesi' && participantData[context.dataIndex]) {
|
|
|
label += '\n👥 Partecipanti: ' + participantData[context.dataIndex];
|
|
|
}
|
|
|
@@ -893,7 +1022,7 @@
|
|
|
setTimeout(() => {
|
|
|
console.log('Now updating charts after delay');
|
|
|
window.ReportsChartManager.forceUpdateCharts();
|
|
|
- }, 800); // Increased delay to 800ms
|
|
|
+ }, 800);
|
|
|
});
|
|
|
document.addEventListener('livewire:load', function () {
|
|
|
Livewire.on('courseSelected', (courseId) => {
|
|
|
@@ -903,7 +1032,6 @@
|
|
|
}, 200);
|
|
|
});
|
|
|
|
|
|
- // Listen for the chartsUpdated event from your updateCharts method
|
|
|
Livewire.on('chartsUpdated', () => {
|
|
|
console.log('Charts updated event received from Livewire');
|
|
|
setTimeout(() => {
|