|
|
@@ -86,7 +86,7 @@
|
|
|
</div>
|
|
|
|
|
|
<div class="chart-row">
|
|
|
- <div class="chart-card">
|
|
|
+ <div class="chart-card modern-course-card">
|
|
|
<div class="chart-header">
|
|
|
<h3 class="chart-title">Analisi Corsi</h3>
|
|
|
</div>
|
|
|
@@ -94,36 +94,40 @@
|
|
|
<div class="course-controls">
|
|
|
<div class="control-group">
|
|
|
<label>Seleziona Corso ({{ $seasonFilter }}):</label>
|
|
|
- <select class="form-select" wire:model.live="selectedCourse">
|
|
|
+ <select class="form-select modern-select" wire:model.live="selectedCourse">
|
|
|
<option value="">Seleziona un Corso</option>
|
|
|
@foreach($this->getCoursesForSelect() as $course)
|
|
|
<option value="{{ $course['id'] }}">{{ $course['full_name'] }}</option>
|
|
|
@endforeach
|
|
|
</select>
|
|
|
</div>
|
|
|
- <div class="legend-container">
|
|
|
- <div class="legend-item">
|
|
|
- <div class="legend-color" style="background: rgba(0, 184, 148, 1);"></div>
|
|
|
- <span>Pagamenti Effettuati</span>
|
|
|
- </div>
|
|
|
- <div class="legend-item">
|
|
|
- <div class="legend-color" style="background: rgba(48, 51, 107, 1);"></div>
|
|
|
- <span>Pagamenti Attesi</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
</div>
|
|
|
|
|
|
@if($selectedCourse)
|
|
|
<div wire:ignore wire:key="course-chart-{{ $seasonFilter }}-{{ $selectedCourse }}">
|
|
|
- <div class="chart-container">
|
|
|
- <canvas
|
|
|
- id="courses-chart-{{ str_replace('-', '', $seasonFilter) }}-{{ $selectedCourse }}"></canvas>
|
|
|
+ <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>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@else
|
|
|
- <div class="chart-container"
|
|
|
- style="display: flex; align-items: center; justify-content: center; min-height: 400px; color: var(--secondary-color);">
|
|
|
- <p style="font-size: 1.1rem;">Seleziona un corso per visualizzare il grafico</p>
|
|
|
+ <div class="chart-placeholder">
|
|
|
+ <div style="text-align: center;">
|
|
|
+ <div style="font-size: 3rem; margin-bottom: 1rem; opacity: 0.3;">📊</div>
|
|
|
+ <p style="font-size: 1.25rem; font-weight: 600; margin: 0;">Seleziona un corso per
|
|
|
+ visualizzare il grafico</p>
|
|
|
+ <p style="font-size: 1rem; opacity: 0.7; margin-top: 0.5rem;">Usa il menu a tendina sopra
|
|
|
+ per scegliere un corso</p>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
@endif
|
|
|
</div>
|
|
|
@@ -133,7 +137,6 @@
|
|
|
|
|
|
<!-- Single JavaScript section -->
|
|
|
<script>
|
|
|
- // Global chart manager
|
|
|
window.ReportsChartManager = window.ReportsChartManager || {
|
|
|
charts: {},
|
|
|
currentSeason: null,
|
|
|
@@ -151,40 +154,66 @@
|
|
|
});
|
|
|
},
|
|
|
|
|
|
+ // NEW: Method to destroy season-specific charts
|
|
|
+ destroySeasonCharts: function (oldSeasonKey) {
|
|
|
+ const chartsToDestroy = Object.keys(this.charts).filter(chartId =>
|
|
|
+ chartId.includes(oldSeasonKey)
|
|
|
+ );
|
|
|
+ chartsToDestroy.forEach(chartId => this.destroyChart(chartId));
|
|
|
+ },
|
|
|
+
|
|
|
+ // Replace your updateMainCharts method with this version that uses fixed canvas IDs:
|
|
|
updateMainCharts: function () {
|
|
|
- const seasonFilter = '{{ $seasonFilter }}';
|
|
|
- const seasonKey = '{{ str_replace('-', '', $seasonFilter) }}';
|
|
|
+ console.log('=== updateMainCharts called ===');
|
|
|
|
|
|
- // Only update if season changed
|
|
|
- if (this.currentSeason === seasonFilter) {
|
|
|
- return;
|
|
|
- }
|
|
|
+ const seasonFilter = @this.get('seasonFilter');
|
|
|
|
|
|
- this.currentSeason = seasonFilter;
|
|
|
+ // Update titles
|
|
|
+ const monthlyTitle = document.getElementById('monthly-season-title');
|
|
|
+ const causalsTitle = document.getElementById('causals-season-title');
|
|
|
+ if (monthlyTitle) {
|
|
|
+ monthlyTitle.textContent = seasonFilter;
|
|
|
+ }
|
|
|
+ if (causalsTitle) {
|
|
|
+ causalsTitle.textContent = seasonFilter;
|
|
|
+ }
|
|
|
|
|
|
- // Get fresh data
|
|
|
- const monthlyData = @json($this->getMonthlyTotals());
|
|
|
- const causalsData = @json($this->getTopCausalsByAmount());
|
|
|
- const membersData = @json($this->getTesseratiData());
|
|
|
+ // Use the original canvas IDs from the blade template
|
|
|
+ const originalSeasonKey = '{{ str_replace('-', '', $seasonFilter) }}';
|
|
|
+ console.log('Using original season key for canvas IDs:', originalSeasonKey);
|
|
|
|
|
|
- // Update titles
|
|
|
- document.getElementById('monthly-season-title').textContent = seasonFilter;
|
|
|
- document.getElementById('causals-season-title').textContent = seasonFilter;
|
|
|
+ // Get fresh data and update charts
|
|
|
+ @this.call('getMonthlyTotals').then(monthlyData => {
|
|
|
+ console.log('Got monthly data:', monthlyData);
|
|
|
+ this.createMonthlyChart(originalSeasonKey, monthlyData);
|
|
|
+ this.updateMonthlyTable(monthlyData);
|
|
|
+ });
|
|
|
|
|
|
- // Create/update charts
|
|
|
- this.createMonthlyChart(seasonKey, monthlyData);
|
|
|
- this.createCausalsChart(seasonKey, causalsData);
|
|
|
- this.createMembersChart(seasonKey, membersData);
|
|
|
+ @this.call('getTopCausalsByAmount').then(causalsData => {
|
|
|
+ console.log('Got causals data:', causalsData);
|
|
|
+ this.createCausalsChart(originalSeasonKey, causalsData);
|
|
|
+ });
|
|
|
|
|
|
- // Update tables
|
|
|
- this.updateMonthlyTable(monthlyData);
|
|
|
- this.updateMembersTable(membersData);
|
|
|
+ @this.call('getTesseratiData').then(membersData => {
|
|
|
+ console.log('Got members data:', membersData);
|
|
|
+ this.createMembersChart(originalSeasonKey, membersData);
|
|
|
+ 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.updateMainCharts();
|
|
|
},
|
|
|
|
|
|
createMonthlyChart: function (seasonKey, monthlyData) {
|
|
|
const chartId = `monthly-chart-${seasonKey}`;
|
|
|
const canvas = document.getElementById(chartId);
|
|
|
- if (!canvas) return;
|
|
|
+ if (!canvas) {
|
|
|
+ console.error('Canvas not found for ID:', chartId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
this.destroyChart(chartId);
|
|
|
|
|
|
@@ -422,7 +451,7 @@
|
|
|
let tableHtml = `
|
|
|
<div class="monthly-table">
|
|
|
<div class="table-header">
|
|
|
- <div class="table-cell">Mese</div>
|
|
|
+ <div class="table-cell month">Mese</div>
|
|
|
<div class="table-cell">Entrate</div>
|
|
|
<div class="table-cell">Uscite</div>
|
|
|
<div class="table-cell">Delta</div>
|
|
|
@@ -457,9 +486,6 @@
|
|
|
const memberCounts = membersData.datasets[0].data;
|
|
|
|
|
|
let tableHtml = `
|
|
|
- <h4 style="margin-bottom: 1rem; font-size: 1rem; font-weight: 600; color: var(--dark-color);">
|
|
|
- Riepilogo Tesserati
|
|
|
- </h4>
|
|
|
<div class="members-table">
|
|
|
<div class="table-header">
|
|
|
<div class="table-cell">Stagione</div>
|
|
|
@@ -593,50 +619,226 @@
|
|
|
}
|
|
|
});
|
|
|
},
|
|
|
- createCourseChartWithValue: function (selectedCourseValue) {
|
|
|
- console.log('Creating course chart with value:', selectedCourseValue);
|
|
|
- const seasonFilter = '{{ $seasonFilter }}';
|
|
|
- const seasonKey = '{{ str_replace('-', '', $seasonFilter) }}';
|
|
|
-
|
|
|
- const chartId = `courses-chart-${seasonKey}-${selectedCourseValue}`;
|
|
|
- const canvas = document.getElementById(chartId);
|
|
|
- if (!canvas) {
|
|
|
- console.log('Canvas not found for chart ID:', chartId);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- this.destroyChart(chartId);
|
|
|
-
|
|
|
- // Call Livewire method to get fresh data
|
|
|
- @this.call('getCourseData', selectedCourseValue).then(courseData => {
|
|
|
- console.log('Received course data:', courseData);
|
|
|
-
|
|
|
- if (!courseData || !courseData.labels || courseData.labels.length === 0) {
|
|
|
- console.log('No data available for chart');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const ctx = canvas.getContext('2d');
|
|
|
- this.charts[chartId] = new Chart(ctx, {
|
|
|
- type: 'bar',
|
|
|
- data: {
|
|
|
- labels: courseData.labels,
|
|
|
- datasets: courseData.datasets
|
|
|
- },
|
|
|
- options: {
|
|
|
- responsive: true,
|
|
|
- maintainAspectRatio: false,
|
|
|
- scales: {
|
|
|
- y: {
|
|
|
- beginAtZero: true
|
|
|
+ createCourseChartWithValue: function (selectedCourseValue) {
|
|
|
+ console.log('Creating modern course chart with value:', selectedCourseValue);
|
|
|
+ const seasonFilter = '{{ $seasonFilter }}';
|
|
|
+ const seasonKey = '{{ str_replace('-', '', $seasonFilter) }}';
|
|
|
+
|
|
|
+ const chartId = `courses-chart-${seasonKey}-${selectedCourseValue}`;
|
|
|
+ const tableId = `course-delta-table-${seasonKey}-${selectedCourseValue}`;
|
|
|
+ const canvas = document.getElementById(chartId);
|
|
|
+ const tableContainer = document.getElementById(tableId);
|
|
|
+
|
|
|
+ if (!canvas) {
|
|
|
+ console.log('Canvas not found for chart ID:', chartId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.destroyChart(chartId);
|
|
|
+
|
|
|
+ // Call Livewire method to get fresh data
|
|
|
+ @this.call('getCourseData', selectedCourseValue).then(courseData => {
|
|
|
+ console.log('Received course data:', courseData);
|
|
|
+
|
|
|
+ if (!courseData || !courseData.labels || courseData.labels.length === 0) {
|
|
|
+ console.log('No data available for chart');
|
|
|
+ return;
|
|
|
}
|
|
|
+
|
|
|
+ // 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 = canvas.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)');
|
|
|
+
|
|
|
+ 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
|
|
|
+ };
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ options: {
|
|
|
+ responsive: true,
|
|
|
+ maintainAspectRatio: false, // This allows the chart to use full container height
|
|
|
+ interaction: {
|
|
|
+ mode: 'index',
|
|
|
+ intersect: false,
|
|
|
+ },
|
|
|
+ layout: {
|
|
|
+ padding: {
|
|
|
+ top: 20,
|
|
|
+ right: 20,
|
|
|
+ bottom: 20,
|
|
|
+ left: 10
|
|
|
+ }
|
|
|
+ },
|
|
|
+ scales: {
|
|
|
+ x: {
|
|
|
+ grid: {
|
|
|
+ display: false
|
|
|
+ },
|
|
|
+ ticks: {
|
|
|
+ font: {
|
|
|
+ weight: '600',
|
|
|
+ size: 13 // Slightly larger font for better visibility
|
|
|
+ },
|
|
|
+ color: '#6b7280'
|
|
|
+ },
|
|
|
+ border: {
|
|
|
+ display: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ y: {
|
|
|
+ beginAtZero: true,
|
|
|
+ grid: {
|
|
|
+ color: 'rgba(156, 163, 175, 0.15)',
|
|
|
+ borderDash: [3, 3]
|
|
|
+ },
|
|
|
+ border: {
|
|
|
+ display: false
|
|
|
+ },
|
|
|
+ ticks: {
|
|
|
+ font: {
|
|
|
+ size: 12, // Larger font for better visibility
|
|
|
+ weight: '500'
|
|
|
+ },
|
|
|
+ color: '#6b7280',
|
|
|
+ callback: function (value) {
|
|
|
+ return '€' + new Intl.NumberFormat('it-IT', {
|
|
|
+ minimumFractionDigits: 0,
|
|
|
+ maximumFractionDigits: 0
|
|
|
+ }).format(value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ plugins: {
|
|
|
+ legend: {
|
|
|
+ display: false
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ backgroundColor: 'rgba(255, 255, 255, 0.98)',
|
|
|
+ titleColor: '#111827',
|
|
|
+ bodyColor: '#374151',
|
|
|
+ borderColor: 'rgba(229, 231, 235, 0.8)',
|
|
|
+ borderWidth: 1,
|
|
|
+ cornerRadius: 12,
|
|
|
+ titleFont: {
|
|
|
+ weight: 'bold',
|
|
|
+ size: 15 // Larger tooltip font
|
|
|
+ },
|
|
|
+ bodyFont: {
|
|
|
+ size: 14,
|
|
|
+ weight: '500'
|
|
|
+ },
|
|
|
+ padding: 16,
|
|
|
+ boxPadding: 8,
|
|
|
+ usePointStyle: true,
|
|
|
+ displayColors: true,
|
|
|
+ callbacks: {
|
|
|
+ title: function (context) {
|
|
|
+ return context[0].label;
|
|
|
+ },
|
|
|
+ label: function (context) {
|
|
|
+ let label = context.dataset.label + ': €' +
|
|
|
+ new Intl.NumberFormat('it-IT').format(context.parsed.y);
|
|
|
+
|
|
|
+ if (context.dataset.label === 'Pagamenti Attesi' && participantData[context.dataIndex]) {
|
|
|
+ label += '\n👥 Partecipanti: ' + participantData[context.dataIndex];
|
|
|
+ }
|
|
|
+
|
|
|
+ return label;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ animation: {
|
|
|
+ duration: 1500,
|
|
|
+ easing: 'easeOutCubic'
|
|
|
+ },
|
|
|
+ elements: {
|
|
|
+ bar: {
|
|
|
+ borderRadius: {
|
|
|
+ topLeft: 8,
|
|
|
+ topRight: 8,
|
|
|
+ bottomLeft: 0,
|
|
|
+ bottomRight: 0
|
|
|
+ }
|
|
|
+ },
|
|
|
+ line: {
|
|
|
+ borderCapStyle: 'round',
|
|
|
+ borderJoinStyle: 'round'
|
|
|
+ },
|
|
|
+ point: {
|
|
|
+ hoverBorderWidth: 4,
|
|
|
+ borderWidth: 3
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }).catch(error => {
|
|
|
+ console.error('Error calling getCourseData:', error);
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ updateCourseTable: function (container, tableData) {
|
|
|
+ if (!container || !tableData) return;
|
|
|
+
|
|
|
+ let tableHtml = `
|
|
|
+ <div class="course-table">
|
|
|
+ <div class="table-header">
|
|
|
+ <div class="table-cell month">Mese</div>
|
|
|
+ <div class="table-cell participants">👥</div>
|
|
|
+ <div class="table-cell delta">Mancanti</div>
|
|
|
+ <div class="table-cell percentage">%</div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+
|
|
|
+ tableData.forEach(row => {
|
|
|
+ const deltaClass = row.delta > 0 ? 'negative' : (row.delta === 0 ? 'neutral' : 'positive');
|
|
|
+ const percentageClass = row.percentage >= 80 ? 'good' : (row.percentage >= 50 ? 'warning' : 'bad');
|
|
|
+
|
|
|
+ tableHtml += `
|
|
|
+ <div class="table-row">
|
|
|
+ <div class="table-cell month">${row.month}</div>
|
|
|
+ <div class="table-cell participants">${row.participants}</div>
|
|
|
+ <div class="table-cell delta ${deltaClass}">€${new Intl.NumberFormat('it-IT').format(Math.abs(row.delta))}</div>
|
|
|
+ <div class="table-cell percentage ${percentageClass}">${row.percentage}%</div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ });
|
|
|
+
|
|
|
+ tableHtml += '</div>';
|
|
|
+ container.innerHTML = tableHtml;
|
|
|
+ },
|
|
|
+ updateCourseChart: function () {
|
|
|
+ if (this.selectedCourse) {
|
|
|
+ const seasonFilter = @json($seasonFilter);
|
|
|
+ const seasonKey = seasonFilter.replace('-', '');
|
|
|
+ this.createCourseChartWithValue(this.selectedCourse);
|
|
|
}
|
|
|
}
|
|
|
- });
|
|
|
- }).catch(error => {
|
|
|
- console.error('Error calling getCourseData:', error);
|
|
|
- });
|
|
|
-},
|
|
|
};
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
|
@@ -650,6 +852,14 @@
|
|
|
window.ReportsChartManager.updateMainCharts();
|
|
|
}, 100);
|
|
|
});
|
|
|
+
|
|
|
+ document.addEventListener('livewire:updated', function (event) {
|
|
|
+ console.log('Livewire updated, waiting for component to fully update...');
|
|
|
+ setTimeout(() => {
|
|
|
+ console.log('Now updating charts after delay');
|
|
|
+ window.ReportsChartManager.forceUpdateCharts();
|
|
|
+ }, 800); // Increased delay to 800ms
|
|
|
+ });
|
|
|
document.addEventListener('livewire:load', function () {
|
|
|
Livewire.on('courseSelected', (courseId) => {
|
|
|
console.log('Course selected event received:', courseId);
|
|
|
@@ -657,6 +867,14 @@
|
|
|
window.ReportsChartManager.createCourseChartWithValue(courseId);
|
|
|
}, 200);
|
|
|
});
|
|
|
+
|
|
|
+ // Listen for the chartsUpdated event from your updateCharts method
|
|
|
+ Livewire.on('chartsUpdated', () => {
|
|
|
+ console.log('Charts updated event received from Livewire');
|
|
|
+ setTimeout(() => {
|
|
|
+ window.ReportsChartManager.forceUpdateCharts();
|
|
|
+ }, 200);
|
|
|
+ });
|
|
|
});
|
|
|
</script>
|
|
|
</div>
|