| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662 |
- {{-- 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>
- <!-- Main Charts Section - Protected with wire:ignore -->
- <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 class="chart-container">
- <canvas id="causals-chart-{{ str_replace('-', '', $seasonFilter) }}"></canvas>
- </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 300px; 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>
- <div class="chart-row">
- <div class="chart-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" 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>
- </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>
- @endif
- </div>
- </div>
- </div>
- </div>
- <!-- Single JavaScript section -->
- <script>
- // Global chart manager
- 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);
- });
- },
- updateMainCharts: function () {
- const seasonFilter = '{{ $seasonFilter }}';
- const seasonKey = '{{ str_replace('-', '', $seasonFilter) }}';
- // Only update if season changed
- if (this.currentSeason === seasonFilter) {
- return;
- }
- this.currentSeason = seasonFilter;
- // Get fresh data
- const monthlyData = @json($this->getMonthlyTotals());
- const causalsData = @json($this->getTopCausalsByAmount());
- const membersData = @json($this->getTesseratiData());
- // Update titles
- document.getElementById('monthly-season-title').textContent = seasonFilter;
- document.getElementById('causals-season-title').textContent = seasonFilter;
- // Create/update charts
- this.createMonthlyChart(seasonKey, monthlyData);
- this.createCausalsChart(seasonKey, causalsData);
- this.createMembersChart(seasonKey, membersData);
- // Update tables
- this.updateMonthlyTable(monthlyData);
- this.updateMembersTable(membersData);
- },
- createMonthlyChart: function (seasonKey, monthlyData) {
- const chartId = `monthly-chart-${seasonKey}`;
- const canvas = document.getElementById(chartId);
- if (!canvas) return;
- this.destroyChart(chartId);
- const ctx = canvas.getContext('2d');
- const incomeGradient = ctx.createLinearGradient(0, 0, 0, 400);
- incomeGradient.addColorStop(0, 'rgba(0, 184, 148, 0.8)');
- incomeGradient.addColorStop(1, 'rgba(0, 184, 148, 0.2)');
- const expenseGradient = ctx.createLinearGradient(0, 0, 0, 400);
- expenseGradient.addColorStop(0, 'rgba(255, 107, 107, 0.8)');
- expenseGradient.addColorStop(1, 'rgba(255, 107, 107, 0.2)');
- 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,
- },
- {
- label: 'Uscite',
- data: monthlyData.datasets[1].data,
- backgroundColor: expenseGradient,
- borderColor: '#ff6b6b',
- borderWidth: 2,
- }
- ]
- },
- options: {
- responsive: true,
- maintainAspectRatio: false,
- plugins: {
- legend: {
- position: 'top',
- labels: {
- usePointStyle: true,
- padding: 20,
- font: { weight: '500' }
- }
- },
- tooltip: {
- backgroundColor: 'rgba(255, 255, 255, 0.95)',
- 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);
- }
- }
- }
- },
- 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);
- }
- }
- }
- },
- 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)'
- ];
- this.charts[chartId] = new Chart(ctx, {
- type: 'doughnut',
- data: {
- labels: causalsData.inLabels,
- datasets: [{
- label: 'Importo',
- data: causalsData.inData.map(item => item.value),
- backgroundColor: colors,
- borderColor: colors.map(color => color.replace('0.8', '1')),
- borderWidth: 2,
- hoverOffset: 8
- }]
- },
- options: {
- responsive: true,
- maintainAspectRatio: false,
- cutout: '60%',
- plugins: {
- legend: {
- position: 'left',
- labels: {
- usePointStyle: true,
- padding: 15,
- font: { size: 11, weight: '500' }
- }
- },
- tooltip: {
- backgroundColor: 'rgba(255, 255, 255, 0.95)',
- titleColor: '#212529',
- bodyColor: '#495057',
- borderColor: '#e9ecef',
- borderWidth: 1,
- cornerRadius: 8,
- 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);
- return context.label + ': €' +
- new Intl.NumberFormat('it-IT').format(value) +
- ` (${percentage}%)`;
- }
- }
- }
- },
- animation: {
- animateRotate: true,
- duration: 1000
- }
- }
- });
- },
- 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)');
- this.charts[chartId] = new Chart(ctx, {
- type: 'line',
- data: {
- labels: membersData.labels,
- datasets: [{
- label: 'Membri Tesserati',
- data: membersData.datasets[0].data,
- borderColor: '#3b5bdb',
- backgroundColor: gradient,
- borderWidth: 3,
- fill: true,
- tension: 0.4,
- pointBackgroundColor: '#3b5bdb',
- pointBorderColor: '#ffffff',
- pointBorderWidth: 2,
- pointRadius: 6,
- pointHoverRadius: 8
- }]
- },
- options: {
- responsive: true,
- maintainAspectRatio: false,
- plugins: {
- legend: { display: false },
- tooltip: {
- backgroundColor: 'rgba(255, 255, 255, 0.95)',
- titleColor: '#212529',
- bodyColor: '#495057',
- borderColor: '#e9ecef',
- borderWidth: 1,
- cornerRadius: 8,
- callbacks: {
- label: function (context) {
- return 'Tesserati: ' + context.parsed.y;
- }
- }
- }
- },
- scales: {
- x: { grid: { display: false } },
- y: {
- beginAtZero: true,
- grid: { color: 'rgba(0, 0, 0, 0.05)' },
- ticks: { precision: 0 }
- }
- },
- 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">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 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>
- <div class="table-cell">Tesserati</div>
- <div class="table-cell">Variazione</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>`;
- }
- }
- 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>
- `;
- });
- 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);
- // Add this check at the beginning
- 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, 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, 0.95)',
- 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 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
- }
- }
- }
- });
- }).catch(error => {
- console.error('Error calling getCourseData:', error);
- });
- },
- };
- document.addEventListener('DOMContentLoaded', function () {
- setTimeout(() => {
- window.ReportsChartManager.updateMainCharts();
- }, 100);
- });
- document.addEventListener('livewire:navigated', function () {
- setTimeout(() => {
- window.ReportsChartManager.updateMainCharts();
- }, 100);
- });
- document.addEventListener('livewire:load', function () {
- Livewire.on('courseSelected', (courseId) => {
- console.log('Course selected event received:', courseId);
- setTimeout(() => {
- window.ReportsChartManager.createCourseChartWithValue(courseId);
- }, 200);
- });
- });
- </script>
- </div>
|