Bladeren bron

tochhi finali report

FabioFratini 8 maanden geleden
bovenliggende
commit
40f00409b9
3 gewijzigde bestanden met toevoegingen van 176 en 74 verwijderingen
  1. 6 32
      app/Http/Livewire/Reports.php
  2. 1 1
      public/css/chart-reports.css
  3. 169 41
      resources/views/livewire/reports.blade.php

+ 6 - 32
app/Http/Livewire/Reports.php

@@ -36,9 +36,7 @@ class Reports extends Component
         return view('livewire.reports');
     }
 
-    /**
-     * Get current season in format "2024-2025"
-     */
+
     private function getCurrentSeason()
     {
         $now = Carbon::now();
@@ -50,9 +48,6 @@ class Reports extends Component
         }
     }
 
-    /**
-     * Get available seasons for dropdown
-     */
     public function getAvailableSeasons()
     {
         $seasons = [];
@@ -68,9 +63,7 @@ class Reports extends Component
         return array_reverse($seasons);
     }
 
-    /**
-     * Parse season string to get start and end years
-     */
+
     private function parseSeason($season)
     {
         $parts = explode('-', $season);
@@ -80,16 +73,14 @@ class Reports extends Component
         ];
     }
 
-    /**
-     * Get date range for a season (September 1st to August 31st)
-     */
+
     private function getSeasonDateRange($season)
     {
         $years = $this->parseSeason($season);
 
         return [
-            'start' => Carbon::create($years['start_year'], 9, 1), // September 1st
-            'end' => Carbon::create($years['end_year'], 8, 31)     // August 31st
+            'start' => Carbon::create($years['start_year'], 9, 1),
+            'end' => Carbon::create($years['end_year'], 8, 31)
         ];
     }
 
@@ -145,14 +136,12 @@ class Reports extends Component
 
         Log::info('Date range start: ' . $dateRange['start']);
         Log::info('Date range end: ' . $dateRange['end']);
-        // September to August order
         $monthOrder = [9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7, 8];
         $monthNames = ['Set', 'Ott', 'Nov', 'Dic', 'Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago'];
 
         $incomeData = array_fill(0, 12, 0);
         $expenseData = array_fill(0, 12, 0);
 
-        // Get income records within season date range
         $incomeRecords = DB::table('records')
             ->join('records_rows', 'records.id', '=', 'records_rows.record_id')
             ->whereBetween('records.date', [$dateRange['start'], $dateRange['end']])
@@ -161,7 +150,6 @@ class Reports extends Component
             ->groupBy('month_num')
             ->get();
 
-        // Get expense records within season date range
         $expenseRecords = DB::table('records')
             ->join('records_rows', 'records.id', '=', 'records_rows.record_id')
             ->whereBetween('records.date', [$dateRange['start'], $dateRange['end']])
@@ -170,7 +158,6 @@ class Reports extends Component
             ->groupBy('month_num')
             ->get();
 
-        // Map income data to correct position in season array
         foreach ($incomeRecords as $record) {
             $monthIndex = array_search($record->month_num, $monthOrder);
             if ($monthIndex !== false) {
@@ -178,7 +165,6 @@ class Reports extends Component
             }
         }
 
-        // Map expense data to correct position in season array
         foreach ($expenseRecords as $record) {
             $monthIndex = array_search($record->month_num, $monthOrder);
             if ($monthIndex !== false) {
@@ -304,7 +290,6 @@ class Reports extends Component
         $courses = Course::with(['level', 'type', 'frequency'])
             ->where('active', true)
             ->where(function ($query) use ($seasonYears) {
-                // Match courses that belong to the exact season
                 $query->where('year', $this->seasonFilter)
                     ->orWhere('year', 'like', '%' . $seasonYears['start_year'] . '-' . $seasonYears['end_year'] . '%')
                     ->orWhere('year', 'like', '%' . $seasonYears['start_year'] . '%')
@@ -313,15 +298,12 @@ class Reports extends Component
             ->orderBy('name')
             ->get()
             ->filter(function ($course) use ($seasonYears) {
-                // Additional filtering to ensure course belongs to the season
                 $courseYear = $course->year;
 
-                // Check if course year matches the season format (e.g., "2024-2025")
                 if ($courseYear === $this->seasonFilter) {
                     return true;
                 }
 
-                // Check if course year contains the season years
                 if (
                     str_contains($courseYear, $seasonYears['start_year']) &&
                     str_contains($courseYear, $seasonYears['end_year'])
@@ -329,7 +311,6 @@ class Reports extends Component
                     return true;
                 }
 
-                // Check if course year is exactly one of the season years
                 if ($courseYear == $seasonYears['start_year'] || $courseYear == $seasonYears['end_year']) {
                     return true;
                 }
@@ -374,9 +355,7 @@ class Reports extends Component
         return $result;
     }
 
-    /**
-     * Get top causals for a specific season
-     */
+
     public function getTopCausalsByAmountForSeason($season, $limit = 10)
     {
         $originalSeason = $this->seasonFilter;
@@ -388,9 +367,6 @@ class Reports extends Component
         return $result;
     }
 
-    /**
-     * Get tesserati data for a specific season
-     */
     public function getTesseratiDataForSeason($season)
     {
         $originalSeason = $this->seasonFilter;
@@ -459,7 +435,6 @@ class Reports extends Component
             ->with('member')
             ->get();
 
-        // Check if there are any member courses
         if ($memberCourses->isEmpty()) {
             return [
                 'labels' => [],
@@ -497,7 +472,6 @@ class Reports extends Component
             }
         }
 
-        // Check if we actually have any data
         if (!$hasData) {
             return [
                 'labels' => [],

+ 1 - 1
public/css/chart-reports.css

@@ -500,7 +500,7 @@
 }
 
 .modern-course-card {
-    background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
+    background: rgb(255, 255, 255, 0.9);
     border: 1px solid #e2e8f0;
     box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
 }

+ 169 - 41
resources/views/livewire/reports.blade.php

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