|
|
@@ -0,0 +1,1132 @@
|
|
|
+{{-- resources/views/livewire/reports.blade.php --}}
|
|
|
+<div>
|
|
|
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
|
+ <link rel="stylesheet" href="{{ asset('css/chart-reports.css') }}">
|
|
|
+
|
|
|
+ <div class="dashboard-container">
|
|
|
+ <div class="controls-section">
|
|
|
+ <div class="control-group">
|
|
|
+ <label for="season-filter">Stagione di Riferimento:</label>
|
|
|
+ <select class="form-select" wire:model="seasonFilter" wire:change="updateCharts">
|
|
|
+ @foreach($this->getAvailableSeasons() as $season)
|
|
|
+ <option value="{{ $season }}">{{ $season }}</option>
|
|
|
+ @endforeach
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ @php
|
|
|
+ $summary = $this->getYearlySummary();
|
|
|
+ @endphp
|
|
|
+ <div class="summary-cards">
|
|
|
+ <div class="summary-card income">
|
|
|
+ <h3>Entrate Totali</h3>
|
|
|
+ <div class="value">€{{ number_format($summary['totalIncome'], 2, ',', '.') }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="summary-card expense">
|
|
|
+ <h3>Uscite Totali</h3>
|
|
|
+ <div class="value">€{{ number_format($summary['totalExpenses'], 2, ',', '.') }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="summary-card delta {{ $summary['delta'] < 0 ? 'negative' : '' }}">
|
|
|
+ <h3>Bilancio Netto</h3>
|
|
|
+ <div class="value">€{{ number_format($summary['delta'], 2, ',', '.') }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div wire:ignore>
|
|
|
+ <div class="chart-row">
|
|
|
+ <div class="chart-card">
|
|
|
+ <div class="chart-header">
|
|
|
+ <h3 class="chart-title">Entrate e Uscite Mensili - <span
|
|
|
+ id="monthly-season-title">{{ $seasonFilter }}</span></h3>
|
|
|
+ </div>
|
|
|
+ <div class="chart-body">
|
|
|
+ <div style="display: grid; grid-template-columns: 1fr 300px; align-items: start;">
|
|
|
+ <div class="chart-container">
|
|
|
+ <canvas id="monthly-chart-{{ str_replace('-', '', $seasonFilter) }}"></canvas>
|
|
|
+ </div>
|
|
|
+ <div class="monthly-table-container" id="monthly-table">
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="chart-row">
|
|
|
+ <div class="chart-card">
|
|
|
+ <div class="chart-header">
|
|
|
+ <h3 class="chart-title">Causali Performanti - <span
|
|
|
+ id="causals-season-title">{{ $seasonFilter }}</span></h3>
|
|
|
+ </div>
|
|
|
+ <div class="chart-body">
|
|
|
+ <div style="display: grid; grid-template-columns: 1fr 700px; gap: 1rem; align-items: start;">
|
|
|
+ <div class="causals-table-container" id="causals-table">
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="chart-container">
|
|
|
+ <canvas id="causals-chart-{{ str_replace('-', '', $seasonFilter) }}"></canvas>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="chart-row">
|
|
|
+ <div class="chart-card">
|
|
|
+ <div class="chart-header">
|
|
|
+ <h3 class="chart-title">Tesserati per Stagione</h3>
|
|
|
+ </div>
|
|
|
+ <div class="chart-body">
|
|
|
+ <div style="display: grid; grid-template-columns: 1fr 500px; gap: 1rem; align-items: start;">
|
|
|
+ <div class="chart-container">
|
|
|
+ <canvas id="members-chart-{{ str_replace('-', '', $seasonFilter) }}"></canvas>
|
|
|
+ </div>
|
|
|
+ <div class="members-table-container" id="members-table">
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ @if(false)
|
|
|
+ <div class="chart-row">
|
|
|
+ <div class="chart-card modern-course-card">
|
|
|
+ <div class="chart-header">
|
|
|
+ <h3 class="chart-title">Analisi Corsi</h3>
|
|
|
+ </div>
|
|
|
+ <div class="chart-body">
|
|
|
+ <div class="course-controls">
|
|
|
+ <div class="control-group">
|
|
|
+ <label>Seleziona Corso ({{ $seasonFilter }}):</label>
|
|
|
+ <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>
|
|
|
+
|
|
|
+ @if($selectedCourse)
|
|
|
+ <div wire:ignore wire:key="course-chart-{{ $seasonFilter }}-{{ $selectedCourse }}">
|
|
|
+ <div class="modern-chart-layout">
|
|
|
+ <div class="course-delta-table"
|
|
|
+ id="course-delta-table-{{ str_replace('-', '', $seasonFilter) }}-{{ $selectedCourse }}">
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="modern-chart-container">
|
|
|
+ <canvas
|
|
|
+ id="courses-chart-{{ str_replace('-', '', $seasonFilter) }}-{{ $selectedCourse }}"></canvas>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ @else
|
|
|
+ <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>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ @endif
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <script>
|
|
|
+ window.ReportsChartManager = window.ReportsChartManager || {
|
|
|
+ charts: {},
|
|
|
+ currentSeason: null,
|
|
|
+
|
|
|
+ destroyChart: function (chartId) {
|
|
|
+ if (this.charts[chartId]) {
|
|
|
+ this.charts[chartId].destroy();
|
|
|
+ delete this.charts[chartId];
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ destroyAllCharts: function () {
|
|
|
+ Object.keys(this.charts).forEach(chartId => {
|
|
|
+ this.destroyChart(chartId);
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ destroySeasonCharts: function (oldSeasonKey) {
|
|
|
+ const chartsToDestroy = Object.keys(this.charts).filter(chartId =>
|
|
|
+ chartId.includes(oldSeasonKey)
|
|
|
+ );
|
|
|
+ chartsToDestroy.forEach(chartId => this.destroyChart(chartId));
|
|
|
+ },
|
|
|
+
|
|
|
+ updateMainCharts: function () {
|
|
|
+ console.log('=== updateMainCharts called ===');
|
|
|
+
|
|
|
+ const seasonFilter = @this.get('seasonFilter');
|
|
|
+
|
|
|
+ const monthlyTitle = document.getElementById('monthly-season-title');
|
|
|
+ const causalsTitle = document.getElementById('causals-season-title');
|
|
|
+ if (monthlyTitle) {
|
|
|
+ monthlyTitle.textContent = seasonFilter;
|
|
|
+ }
|
|
|
+ if (causalsTitle) {
|
|
|
+ causalsTitle.textContent = seasonFilter;
|
|
|
+ }
|
|
|
+
|
|
|
+ const originalSeasonKey = '{{ str_replace('-', '', $seasonFilter) }}';
|
|
|
+ console.log('Using original season key for canvas IDs:', originalSeasonKey);
|
|
|
+
|
|
|
+ @this.call('getMonthlyTotals').then(monthlyData => {
|
|
|
+ console.log('Got monthly data:', monthlyData);
|
|
|
+ this.createMonthlyChart(originalSeasonKey, monthlyData);
|
|
|
+ this.updateMonthlyTable(monthlyData);
|
|
|
+ });
|
|
|
+
|
|
|
+ @this.call('getTopCausalsByAmount').then(causalsData => {
|
|
|
+ console.log('Got causals data:', causalsData);
|
|
|
+ this.createCausalsChart(originalSeasonKey, causalsData);
|
|
|
+ });
|
|
|
+
|
|
|
+ @this.call('getTesseratiData').then(membersData => {
|
|
|
+ console.log('Got members data:', membersData);
|
|
|
+ this.createMembersChart(originalSeasonKey, membersData);
|
|
|
+ this.updateMembersTable(membersData);
|
|
|
+ });
|
|
|
+ },
|
|
|
+ forceUpdateCharts: function () {
|
|
|
+ console.log('Force updating charts...');
|
|
|
+ this.currentSeason = null;
|
|
|
+ this.updateMainCharts();
|
|
|
+ },
|
|
|
+
|
|
|
+ createMonthlyChart: function (seasonKey, monthlyData) {
|
|
|
+ const chartId = `monthly-chart-${seasonKey}`;
|
|
|
+ const canvas = document.getElementById(chartId);
|
|
|
+ if (!canvas) {
|
|
|
+ console.error('Canvas not found for ID:', chartId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.destroyChart(chartId);
|
|
|
+
|
|
|
+ const ctx = canvas.getContext('2d');
|
|
|
+
|
|
|
+ const incomeGradient = ctx.createLinearGradient(0, 0, 0, 400);
|
|
|
+ incomeGradient.addColorStop(0, 'rgba(0, 184, 148, 1)');
|
|
|
+ incomeGradient.addColorStop(1, 'rgba(0, 184, 148, 1)');
|
|
|
+
|
|
|
+ const expenseGradient = ctx.createLinearGradient(0, 0, 0, 400);
|
|
|
+ expenseGradient.addColorStop(0, 'rgba(255, 107, 107, 1)');
|
|
|
+ expenseGradient.addColorStop(1, 'rgba(255, 107, 107, 1)');
|
|
|
+
|
|
|
+ this.charts[chartId] = new Chart(ctx, {
|
|
|
+ type: 'bar',
|
|
|
+ data: {
|
|
|
+ labels: monthlyData.labels,
|
|
|
+ datasets: [
|
|
|
+ {
|
|
|
+ label: 'Entrate',
|
|
|
+ data: monthlyData.datasets[0].data,
|
|
|
+ backgroundColor: incomeGradient,
|
|
|
+ borderColor: '#00b894',
|
|
|
+ borderWidth: 2,
|
|
|
+ borderRadius: 12,
|
|
|
+ borderSkipped: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: 'Uscite',
|
|
|
+ data: monthlyData.datasets[1].data,
|
|
|
+ backgroundColor: expenseGradient,
|
|
|
+ borderColor: '#ff6b6b',
|
|
|
+ borderWidth: 2,
|
|
|
+ borderRadius: 12,
|
|
|
+ borderSkipped: false,
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ options: {
|
|
|
+ responsive: true,
|
|
|
+ maintainAspectRatio: false,
|
|
|
+ plugins: {
|
|
|
+ legend: {
|
|
|
+ position: 'top',
|
|
|
+ labels: {
|
|
|
+ usePointStyle: true,
|
|
|
+ padding: 20,
|
|
|
+ font: { weight: '500' }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ backgroundColor: 'rgba(255, 255, 255,1)',
|
|
|
+ titleColor: '#212529',
|
|
|
+ bodyColor: '#495057',
|
|
|
+ borderColor: '#e9ecef',
|
|
|
+ borderWidth: 12,
|
|
|
+ cornerRadius: 8,
|
|
|
+ callbacks: {
|
|
|
+ label: function (context) {
|
|
|
+ return context.dataset.label + ': €' +
|
|
|
+ new Intl.NumberFormat('it-IT').format(context.parsed.y);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ scales: {
|
|
|
+ x: {
|
|
|
+ grid: { display: false },
|
|
|
+ ticks: { font: { weight: '500' } }
|
|
|
+ },
|
|
|
+ y: {
|
|
|
+ beginAtZero: true,
|
|
|
+ grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
|
|
+ ticks: {
|
|
|
+ callback: function (value) {
|
|
|
+ return '€' + new Intl.NumberFormat('it-IT').format(value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ elements: {
|
|
|
+ bar: {
|
|
|
+ borderRadius: {
|
|
|
+ topLeft: 12,
|
|
|
+ topRight: 12,
|
|
|
+ bottomLeft: 0,
|
|
|
+ bottomRight: 0
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ animation: {
|
|
|
+ duration: 1000,
|
|
|
+ easing: 'easeOutQuart'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ createCausalsChart: function (seasonKey, causalsData) {
|
|
|
+ const chartId = `causals-chart-${seasonKey}`;
|
|
|
+ const canvas = document.getElementById(chartId);
|
|
|
+ if (!canvas) return;
|
|
|
+
|
|
|
+ this.destroyChart(chartId);
|
|
|
+
|
|
|
+ const ctx = canvas.getContext('2d');
|
|
|
+
|
|
|
+ const colors = [
|
|
|
+ 'rgba(59, 91, 219, 0.8)',
|
|
|
+ 'rgba(0, 184, 148, 0.8)',
|
|
|
+ 'rgba(34, 184, 207, 0.8)',
|
|
|
+ 'rgba(255, 212, 59, 0.8)',
|
|
|
+ 'rgba(255, 107, 107, 0.8)',
|
|
|
+ 'rgba(142, 68, 173, 0.8)',
|
|
|
+ 'rgba(230, 126, 34, 0.8)',
|
|
|
+ 'rgba(149, 165, 166, 0.8)',
|
|
|
+ 'rgba(241, 196, 15, 0.8)',
|
|
|
+ '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: dataValues,
|
|
|
+ backgroundColor: colors,
|
|
|
+ borderColor: colors.map(color => color.replace('0.8', '1')),
|
|
|
+ borderWidth: 2,
|
|
|
+ hoverOffset: 8
|
|
|
+ }]
|
|
|
+ },
|
|
|
+ options: {
|
|
|
+ responsive: true,
|
|
|
+ maintainAspectRatio: false,
|
|
|
+ cutout: '30%',
|
|
|
+ layout: {
|
|
|
+ padding: {
|
|
|
+ top: 10,
|
|
|
+ right: 10,
|
|
|
+ bottom: 10,
|
|
|
+ left: 10
|
|
|
+ }
|
|
|
+ },
|
|
|
+ plugins: {
|
|
|
+ legend: {
|
|
|
+ display: false
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
|
|
+ titleColor: '#212529',
|
|
|
+ bodyColor: '#495057',
|
|
|
+ borderColor: '#e9ecef',
|
|
|
+ borderWidth: 1,
|
|
|
+ cornerRadius: 8,
|
|
|
+ titleFont: {
|
|
|
+ size: 13,
|
|
|
+ weight: 'bold'
|
|
|
+ },
|
|
|
+ bodyFont: {
|
|
|
+ size: 12,
|
|
|
+ weight: '500'
|
|
|
+ },
|
|
|
+ padding: 12,
|
|
|
+ callbacks: {
|
|
|
+ title: function (context) {
|
|
|
+ return context[0].label;
|
|
|
+ },
|
|
|
+ label: function (context) {
|
|
|
+ const value = context.raw;
|
|
|
+ const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0;
|
|
|
+ return [
|
|
|
+ `Importo: €${new Intl.NumberFormat('it-IT', {
|
|
|
+ minimumFractionDigits: 2,
|
|
|
+ maximumFractionDigits: 2
|
|
|
+ }).format(value)}`,
|
|
|
+ `Percentuale: ${percentage}%`
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+ animation: {
|
|
|
+ animateRotate: true,
|
|
|
+ duration: 1000
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ this.updateCausalsTable(causalsData, dataValues, total);
|
|
|
+ },
|
|
|
+
|
|
|
+ updateCausalsTable: function (causalsData, dataValues, total) {
|
|
|
+ const container = document.getElementById('causals-table');
|
|
|
+ if (!container) return;
|
|
|
+
|
|
|
+ const colors = [
|
|
|
+ '#3b5bdb', '#00b894', '#22b8cf', '#ffd43b', '#ff6b6b',
|
|
|
+ '#8e44ad', '#e67e22', '#95a5a6', '#f1c40f', '#e74c3c'
|
|
|
+ ];
|
|
|
+
|
|
|
+ let tableHtml = `
|
|
|
+ <div class="causals-table compact">
|
|
|
+ <div class="table-header">
|
|
|
+ <div class="table-cell causale">Causale</div>
|
|
|
+ <div class="table-cell euro">Importo</div>
|
|
|
+ <div class="table-cell percent">%</div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+
|
|
|
+ causalsData.inLabels.forEach((label, index) => {
|
|
|
+ const value = dataValues[index] || 0;
|
|
|
+ const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0;
|
|
|
+ const color = colors[index % colors.length];
|
|
|
+
|
|
|
+ tableHtml += `
|
|
|
+ <div class="table-row">
|
|
|
+ <div class="table-cell causale">
|
|
|
+ <span class="causale-indicator" style="background-color: ${color}"></span>
|
|
|
+ ${label}
|
|
|
+ </div>
|
|
|
+ <div class="table-cell euro">€${new Intl.NumberFormat('it-IT', {
|
|
|
+ minimumFractionDigits: 2,
|
|
|
+ maximumFractionDigits: 2
|
|
|
+ }).format(value)}</div>
|
|
|
+ <div class="table-cell percent">${percentage}%</div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ });
|
|
|
+
|
|
|
+ tableHtml += '</div>';
|
|
|
+ container.innerHTML = tableHtml;
|
|
|
+ },
|
|
|
+ createMembersChart: function (seasonKey, membersData) {
|
|
|
+ const chartId = `members-chart-${seasonKey}`;
|
|
|
+ const canvas = document.getElementById(chartId);
|
|
|
+ if (!canvas) return;
|
|
|
+
|
|
|
+ this.destroyChart(chartId);
|
|
|
+
|
|
|
+ const ctx = canvas.getContext('2d');
|
|
|
+
|
|
|
+ const gradient = ctx.createLinearGradient(0, 0, 0, 400);
|
|
|
+ gradient.addColorStop(0, 'rgba(59, 91, 219, 0.3)');
|
|
|
+ gradient.addColorStop(1, 'rgba(59, 91, 219, 0.05)');
|
|
|
+
|
|
|
+ const processedDatasets = membersData.datasets.map((dataset, index) => {
|
|
|
+ if (dataset.label === 'Totale Membri Tesserati') {
|
|
|
+ return {
|
|
|
+ ...dataset,
|
|
|
+ backgroundColor: gradient,
|
|
|
+ borderColor: '#3b5bdb',
|
|
|
+ borderWidth: 3,
|
|
|
+ pointBackgroundColor: '#3b5bdb',
|
|
|
+ pointBorderColor: '#ffffff',
|
|
|
+ pointBorderWidth: 2,
|
|
|
+ pointRadius: 6,
|
|
|
+ pointHoverRadius: 8,
|
|
|
+ type: 'line',
|
|
|
+ order: 1,
|
|
|
+ fill: true
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ return {
|
|
|
+ ...dataset,
|
|
|
+ borderWidth: 2,
|
|
|
+ pointRadius: 4,
|
|
|
+ pointHoverRadius: 6,
|
|
|
+ pointBorderColor: '#ffffff',
|
|
|
+ pointBorderWidth: 1,
|
|
|
+ type: 'line',
|
|
|
+ order: 2,
|
|
|
+ fill: false,
|
|
|
+ backgroundColor: 'transparent'
|
|
|
+ };
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ this.charts[chartId] = new Chart(ctx, {
|
|
|
+ type: 'line',
|
|
|
+ data: {
|
|
|
+ labels: membersData.labels,
|
|
|
+ datasets: processedDatasets
|
|
|
+ },
|
|
|
+ options: {
|
|
|
+ responsive: true,
|
|
|
+ maintainAspectRatio: false,
|
|
|
+ interaction: {
|
|
|
+ mode: 'index',
|
|
|
+ intersect: false,
|
|
|
+ },
|
|
|
+ plugins: {
|
|
|
+ legend: {
|
|
|
+ display: true,
|
|
|
+ position: 'top',
|
|
|
+ labels: {
|
|
|
+ usePointStyle: true,
|
|
|
+ padding: 15,
|
|
|
+ font: { weight: '500', size: 12 }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
|
|
+ titleColor: '#212529',
|
|
|
+ bodyColor: '#495057',
|
|
|
+ borderColor: '#e9ecef',
|
|
|
+ borderWidth: 1,
|
|
|
+ cornerRadius: 8,
|
|
|
+ callbacks: {
|
|
|
+ title: function (context) {
|
|
|
+ return 'Stagione: ' + context[0].label;
|
|
|
+ },
|
|
|
+ label: function (context) {
|
|
|
+ return context.dataset.label + ': ' + context.parsed.y;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ scales: {
|
|
|
+ x: {
|
|
|
+ grid: { display: false },
|
|
|
+ ticks: {
|
|
|
+ font: { weight: '500' }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ y: {
|
|
|
+ beginAtZero: true,
|
|
|
+ grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
|
|
+ ticks: {
|
|
|
+ precision: 0,
|
|
|
+ callback: function (value) {
|
|
|
+ return Math.floor(value); // Ensure integer values
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ animation: {
|
|
|
+ duration: 1000,
|
|
|
+ easing: 'easeOutQuart'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ updateMonthlyTable: function (monthlyData) {
|
|
|
+ const container = document.getElementById('monthly-table');
|
|
|
+ if (!container) return;
|
|
|
+
|
|
|
+ const incomeData = monthlyData.datasets[0].data;
|
|
|
+ const expenseData = monthlyData.datasets[1].data;
|
|
|
+ const monthNames = monthlyData.labels;
|
|
|
+
|
|
|
+ let tableHtml = `
|
|
|
+ <div class="monthly-table">
|
|
|
+ <div class="table-header">
|
|
|
+ <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>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+
|
|
|
+ monthNames.forEach((month, index) => {
|
|
|
+ const income = parseFloat(incomeData[index] || 0);
|
|
|
+ const expense = parseFloat(expenseData[index] || 0);
|
|
|
+ const net = income - expense;
|
|
|
+ const rowClass = net < 0 ? 'negative' : (net > 0 ? 'positive' : 'neutral');
|
|
|
+
|
|
|
+ tableHtml += `
|
|
|
+ <div class="table-row ${rowClass}">
|
|
|
+ <div class="table-cell month-name">${month}</div>
|
|
|
+ <div class="table-cell income">€${new Intl.NumberFormat('it-IT').format(income)}</div>
|
|
|
+ <div class="table-cell expense">€${new Intl.NumberFormat('it-IT').format(expense)}</div>
|
|
|
+ <div class="table-cell net">€${new Intl.NumberFormat('it-IT').format(net)}</div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ });
|
|
|
+
|
|
|
+ tableHtml += '</div>';
|
|
|
+ container.innerHTML = tableHtml;
|
|
|
+ },
|
|
|
+
|
|
|
+ updateMembersTable: function (membersData) {
|
|
|
+ const container = document.getElementById('members-table');
|
|
|
+ if (!container) return;
|
|
|
+
|
|
|
+ const seasonLabels = membersData.labels;
|
|
|
+ const totalDataset = membersData.datasets.find(d => d.label === 'Totale Membri Tesserati');
|
|
|
+ const cardTypeDatasets = membersData.datasets.filter(d => d.label !== 'Totale Membri Tesserati');
|
|
|
+
|
|
|
+ const memberCounts = totalDataset ? totalDataset.data : [];
|
|
|
+
|
|
|
+ let tableHtml = `
|
|
|
+ <div class="members-table">
|
|
|
+ <div class="table-header">
|
|
|
+ <div class="table-cell">Stagione</div>
|
|
|
+ <div class="table-cell">Totale</div>
|
|
|
+ <div class="table-cell">Variazione</div>
|
|
|
+ <div class="table-cell">Tipologie</div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+
|
|
|
+ seasonLabels.forEach((season, index) => {
|
|
|
+ const current = parseInt(memberCounts[index] || 0);
|
|
|
+ const previous = index > 0 ? parseInt(memberCounts[index - 1] || 0) : 0;
|
|
|
+ const variation = index > 0 ? current - previous : 0;
|
|
|
+ const variationPercent = previous > 0 ? Math.round((variation / previous) * 100 * 10) / 10 : 0;
|
|
|
+ const rowClass = variation > 0 ? 'positive' : (variation < 0 ? 'negative' : 'neutral');
|
|
|
+
|
|
|
+ let variationText = '—';
|
|
|
+ if (index > 0) {
|
|
|
+ if (variation > 0) {
|
|
|
+ variationText = `<span class="variation-positive">+${variation} (+${variationPercent}%)</span>`;
|
|
|
+ } else if (variation < 0) {
|
|
|
+ variationText = `<span class="variation-negative">${variation} (${variationPercent}%)</span>`;
|
|
|
+ } else {
|
|
|
+ variationText = `<span class="variation-neutral">${variation}</span>`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Build card type breakdown
|
|
|
+ let cardTypeBreakdown = '';
|
|
|
+ cardTypeDatasets.forEach((dataset, datasetIndex) => {
|
|
|
+ const count = dataset.data[index] || 0;
|
|
|
+ if (count > 0) {
|
|
|
+ const color = dataset.borderColor || '#6b7280';
|
|
|
+ cardTypeBreakdown += `
|
|
|
+ <div class="card-type-item">
|
|
|
+ <span class="card-type-indicator" style="background-color: ${color}"></span>
|
|
|
+ <span class="card-type-name">${dataset.label}</span>
|
|
|
+ <span class="card-type-count">${count}</span>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!cardTypeBreakdown) {
|
|
|
+ cardTypeBreakdown = '<div class="no-card-types">Nessun dettaglio</div>';
|
|
|
+ }
|
|
|
+
|
|
|
+ tableHtml += `
|
|
|
+ <div class="table-row ${rowClass}">
|
|
|
+ <div class="table-cell season-name">${season}</div>
|
|
|
+ <div class="table-cell members-count">${new Intl.NumberFormat('it-IT').format(current)}</div>
|
|
|
+ <div class="table-cell variation">${variationText}</div>
|
|
|
+ <div class="table-cell card-types">
|
|
|
+ <div class="card-types-container">
|
|
|
+ ${cardTypeBreakdown}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ });
|
|
|
+
|
|
|
+ tableHtml += '</div>';
|
|
|
+ container.innerHTML = tableHtml;
|
|
|
+ },
|
|
|
+
|
|
|
+ createCourseChart: function () {
|
|
|
+ console.log('Creating course chart...');
|
|
|
+ const seasonFilter = '{{ $seasonFilter }}';
|
|
|
+ const selectedCourse = '{{ $selectedCourse ?? '' }}';
|
|
|
+ const seasonKey = '{{ str_replace('-', '', $seasonFilter) }}';
|
|
|
+ console.log('Selected course:', selectedCourse, 'for season:', seasonFilter);
|
|
|
+
|
|
|
+ if (!selectedCourse || selectedCourse.trim() === '') {
|
|
|
+ console.log('No course selected, skipping chart creation');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const chartId = `courses-chart-${seasonKey}-${selectedCourse}`;
|
|
|
+ const canvas = document.getElementById(chartId);
|
|
|
+ if (!canvas) return;
|
|
|
+
|
|
|
+ this.destroyChart(chartId);
|
|
|
+
|
|
|
+ const courseData = @json($this->getCourseMonthlyEarnings());
|
|
|
+ const ctx = canvas.getContext('2d');
|
|
|
+ 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'
|
|
|
+ };
|
|
|
+ }
|
|
|
+ return dataset;
|
|
|
+ })
|
|
|
+ },
|
|
|
+ options: {
|
|
|
+ responsive: true,
|
|
|
+ maintainAspectRatio: false,
|
|
|
+ interaction: {
|
|
|
+ mode: 'index',
|
|
|
+ intersect: false,
|
|
|
+ },
|
|
|
+ scales: {
|
|
|
+ x: {
|
|
|
+ grid: { display: false },
|
|
|
+ ticks: { font: { weight: '500' } }
|
|
|
+ },
|
|
|
+ y: {
|
|
|
+ beginAtZero: true,
|
|
|
+ grid: {
|
|
|
+ color: 'rgba(0, 0, 0, 1)',
|
|
|
+ borderDash: [5, 5]
|
|
|
+ },
|
|
|
+ ticks: {
|
|
|
+ callback: function (value) {
|
|
|
+ return '€' + new Intl.NumberFormat('it-IT').format(value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ plugins: {
|
|
|
+ legend: {
|
|
|
+ display: true,
|
|
|
+ position: 'top',
|
|
|
+ labels: {
|
|
|
+ usePointStyle: true,
|
|
|
+ padding: 20,
|
|
|
+ font: { weight: '500' }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ backgroundColor: 'rgba(255, 255, 255, 1)',
|
|
|
+ titleColor: '#212529',
|
|
|
+ bodyColor: '#495057',
|
|
|
+ borderColor: '#e9ecef',
|
|
|
+ borderWidth: 1,
|
|
|
+ cornerRadius: 8,
|
|
|
+ callbacks: {
|
|
|
+ label: function (context) {
|
|
|
+ return context.dataset.label + ': €' +
|
|
|
+ new Intl.NumberFormat('it-IT').format(context.parsed.y);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ animation: {
|
|
|
+ duration: 1000,
|
|
|
+ easing: 'easeOutQuart'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ 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}`;
|
|
|
+ let canvas = document.getElementById(chartId);
|
|
|
+ const tableContainer = document.getElementById(tableId);
|
|
|
+
|
|
|
+ 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);
|
|
|
+
|
|
|
+ @this.call('getCourseData', selectedCourseValue).then(courseData => {
|
|
|
+ console.log('Received course data:', courseData);
|
|
|
+
|
|
|
+ if (courseData.isEmpty) {
|
|
|
+ console.log('No data available for course, showing message');
|
|
|
+
|
|
|
+ if (tableContainer) {
|
|
|
+ tableContainer.innerHTML = '';
|
|
|
+ }
|
|
|
+
|
|
|
+ const chartContainer = canvas.parentElement;
|
|
|
+ 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;">
|
|
|
+ ${courseData.message}
|
|
|
+ </h3>
|
|
|
+ <p style="font-size: 1rem; opacity: 0.7; margin: 0; max-width: 400px; margin-left: auto; margin-right: auto; line-height: 1.5;">
|
|
|
+ Questo corso non ha pagamenti registrati per la stagione selezionata.
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!courseData || !courseData.labels || courseData.labels.length === 0) {
|
|
|
+ console.log('No data available for chart');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ let canvasElement = document.getElementById(chartId);
|
|
|
+ if (!canvasElement) {
|
|
|
+ const chartContainer = canvas.parentElement;
|
|
|
+ chartContainer.innerHTML = `<canvas id="${chartId}"></canvas>`;
|
|
|
+ canvasElement = document.getElementById(chartId);
|
|
|
+ }
|
|
|
+
|
|
|
+ this.updateCourseTable(tableContainer, courseData.tableData);
|
|
|
+
|
|
|
+ const participantData = courseData.datasets.find(d => d.participantData)?.participantData || [];
|
|
|
+
|
|
|
+ const ctx = canvasElement.getContext('2d');
|
|
|
+
|
|
|
+ const earnedGradient = ctx.createLinearGradient(0, 0, 0, 400);
|
|
|
+ earnedGradient.addColorStop(0, 'rgba(16, 185, 129, 1)');
|
|
|
+ earnedGradient.addColorStop(1, 'rgba(16, 185, 129, 1)');
|
|
|
+
|
|
|
+ const totalData = courseData.datasets.find(d => d.label === 'Pagamenti Attesi')?.data || [];
|
|
|
+ const earnedData = courseData.datasets.find(d => d.label === 'Pagamenti Effettuati')?.data || [];
|
|
|
+
|
|
|
+ this.charts[chartId] = new Chart(ctx, {
|
|
|
+ type: 'bar',
|
|
|
+ data: {
|
|
|
+ labels: courseData.labels,
|
|
|
+ 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,
|
|
|
+ maintainAspectRatio: false,
|
|
|
+ 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
|
|
|
+ },
|
|
|
+ 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,
|
|
|
+ weight: '500'
|
|
|
+ },
|
|
|
+ color: '#6b7280',
|
|
|
+ callback: function (value) {
|
|
|
+ return '€' + new Intl.NumberFormat('it-IT', {
|
|
|
+ minimumFractionDigits: 0,
|
|
|
+ maximumFractionDigits: 0
|
|
|
+ }).format(value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ plugins: {
|
|
|
+ legend: {
|
|
|
+ display: true,
|
|
|
+ position: 'top',
|
|
|
+ labels: {
|
|
|
+ usePointStyle: true,
|
|
|
+ padding: 15,
|
|
|
+ font: { weight: '500', size: 12 },
|
|
|
+ }
|
|
|
+ },
|
|
|
+ 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
|
|
|
+ },
|
|
|
+ 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 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];
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 earned = parseFloat(row.earned) || 0;
|
|
|
+ const total = parseFloat(row.total) || 0;
|
|
|
+ const delta = Math.max(0, total - earned);
|
|
|
+
|
|
|
+ let percentage = 0;
|
|
|
+ let percentageDisplay = '—';
|
|
|
+ let percentageClass = 'neutral';
|
|
|
+
|
|
|
+ if (total > 0) {
|
|
|
+ percentage = Math.round((earned / total) * 100);
|
|
|
+ percentageDisplay = percentage + '%';
|
|
|
+
|
|
|
+ // Color based on completion
|
|
|
+ if (percentage >= 100) {
|
|
|
+ percentageClass = 'good';
|
|
|
+ } else if (percentage >= 80) {
|
|
|
+ percentageClass = 'warning';
|
|
|
+ } else {
|
|
|
+ percentageClass = 'bad';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Delta styling: positive when delta is 0 (fully paid), negative when there's missing amount
|
|
|
+ const deltaClass = (total > 0 && delta === 0) ? 'positive' :
|
|
|
+ (delta > 0) ? 'negative' : 'neutral';
|
|
|
+
|
|
|
+ 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(delta)}</div>
|
|
|
+ <div class="table-cell percentage ${percentageClass}">${percentageDisplay}</div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ });
|
|
|
+
|
|
|
+ tableHtml += '</div>';
|
|
|
+ container.innerHTML = tableHtml;
|
|
|
+ },
|
|
|
+ updateCourseChart: function () {
|
|
|
+ if (this.selectedCourse) {
|
|
|
+ const seasonFilter = @json($seasonFilter);
|
|
|
+ const seasonKey = seasonFilter.replace('-', '');
|
|
|
+ this.createCourseChartWithValue(this.selectedCourse);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ document.addEventListener('DOMContentLoaded', function () {
|
|
|
+ setTimeout(() => {
|
|
|
+ window.ReportsChartManager.updateMainCharts();
|
|
|
+ }, 100);
|
|
|
+ });
|
|
|
+
|
|
|
+ document.addEventListener('livewire:navigated', function () {
|
|
|
+ setTimeout(() => {
|
|
|
+ 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);
|
|
|
+ });
|
|
|
+ document.addEventListener('livewire:load', function () {
|
|
|
+ Livewire.on('courseSelected', (courseId) => {
|
|
|
+ console.log('Course selected event received:', courseId);
|
|
|
+ setTimeout(() => {
|
|
|
+ window.ReportsChartManager.createCourseChartWithValue(courseId);
|
|
|
+ }, 200);
|
|
|
+ });
|
|
|
+
|
|
|
+ Livewire.on('chartsUpdated', () => {
|
|
|
+ console.log('Charts updated event received from Livewire');
|
|
|
+ setTimeout(() => {
|
|
|
+ window.ReportsChartManager.forceUpdateCharts();
|
|
|
+ }, 200);
|
|
|
+ });
|
|
|
+ });
|
|
|
+ </script>
|
|
|
+</div>
|