| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043 |
- {{-- 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 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 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>
- </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, 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'
- }
- }
- });
- },
- // Replace your createCausalsChart method with this fixed version:
- 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: '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 percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0;
- return context.label + ': €' +
- new Intl.NumberFormat('it-IT', {
- minimumFractionDigits: 2,
- maximumFractionDigits: 2
- }).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 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 memberCounts = membersData.datasets[0].data;
- let tableHtml = `
- <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 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, 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: [
- {
- 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 },
- 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)',
- 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 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);
- }
- }
- };
- 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>
|