FabioFratini 10 maanden geleden
bovenliggende
commit
d2b2e98769
4 gewijzigde bestanden met toevoegingen van 1027 en 1 verwijderingen
  1. 437 0
      app/Http/Livewire/Reports.php
  2. 10 1
      resources/views/layouts/app.blade.php
  3. 579 0
      resources/views/livewire/reports.blade.php
  4. 1 0
      routes/web.php

+ 437 - 0
app/Http/Livewire/Reports.php

@@ -0,0 +1,437 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+use Illuminate\Support\Facades\Auth;
+use Carbon\Carbon;
+use App\Models\Receipt;
+use App\Models\ReceiptRow;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+use App\Models\Course;
+use App\Models\MemberCard;
+
+class Reports extends Component
+{
+    public $type = 'anagrafica';
+    public $yearFilter;
+
+    public $courses = [];
+    public $selectedCourse = null;
+    protected $listeners = ['refreshData' => '$refresh'];
+
+    public function mount()
+    {
+        if (Auth::user()->level != env('LEVEL_ADMIN', 0))
+            return redirect()->to('/reports');
+
+        if (isset($_GET["type"]))
+            $this->type = $_GET["type"];
+
+        $this->yearFilter = Carbon::now()->year;
+        $this->courses = $this->getCoursesForSelect();
+        $this->emit('dataUpdated');
+    }
+
+    public function render()
+    {
+        return view('livewire.reports');
+    }
+    public function updateCourseChart()
+    {
+        $this->emit('courseDataUpdated');
+    }
+
+    public function updatedSelectedCourse($value)
+    {
+        Log::info('Selected course changed to: ' . $value);
+        $this->emit('courseDataUpdated', $value);
+    }
+
+    public function getTesseratiData()
+    {
+        $endYear = $this->yearFilter;
+        return self::getMemberCountChartData($endYear);
+    }
+
+    public function change($type)
+    {
+        $this->type = $type;
+    }
+
+    public function setYearFilter($year)
+    {
+        $this->yearFilter = $year;
+        $this->emit('dataUpdated');
+    }
+
+    public function getMonthlyTotals()
+    {
+        $year = $this->yearFilter;
+
+        $months = range(1, 12);
+        $monthNames = [];
+        $incomeData = array_fill(0, 12, 0);
+        $expenseData = array_fill(0, 12, 0);
+
+        foreach ($months as $month) {
+            $date = Carbon::createFromDate($year, $month, 1);
+            $monthNames[] = ucfirst($date->locale('it')->monthName);
+        }
+
+        $incomeReceipts = DB::table('receipts')
+            ->join('receipts_rows', 'receipts.id', '=', 'receipts_rows.receip_id')
+            ->where('receipts.year', $year)
+            ->where('receipts.type', 'IN')
+            ->select(DB::raw('MONTH(receipts.date) as month_num'), DB::raw('SUM(receipts_rows.amount) as total'))
+            ->groupBy('month_num')
+            ->get();
+
+        $expenseReceipts = DB::table('receipts')
+            ->join('receipts_rows', 'receipts.id', '=', 'receipts_rows.receip_id')
+            ->where('receipts.year', $year)
+            ->where('receipts.type', 'OUT')
+            ->select(DB::raw('MONTH(receipts.date) as month_num'), DB::raw('SUM(receipts_rows.amount) as total'))
+            ->groupBy('month_num')
+            ->get();
+
+        foreach ($incomeReceipts as $receipt) {
+            $index = $receipt->month_num - 1;
+            if (isset($incomeData[$index])) {
+                $incomeData[$index] = $receipt->total;
+            }
+        }
+
+        foreach ($expenseReceipts as $receipt) {
+            $index = $receipt->month_num - 1;
+            if (isset($expenseData[$index])) {
+                $expenseData[$index] = $receipt->total;
+            }
+        }
+
+
+        return [
+            'labels' => $monthNames,
+            'datasets' => [
+                [
+                    'label' => 'Entrate',
+                    'data' => $incomeData,
+                    'backgroundColor' => 'rgba(54, 162, 235, 0.5)'
+                ],
+                [
+                    'label' => 'Uscite',
+                    'data' => $expenseData,
+                    'backgroundColor' => 'rgba(255, 99, 132, 0.5)'
+                ],
+            ]
+        ];
+    }
+
+    public function getYearlySummary()
+    {
+        $year = $this->yearFilter;
+
+        $totalIncome = DB::table('receipts')
+            ->join('receipts_rows', 'receipts.id', '=', 'receipts_rows.receip_id')
+            ->where('receipts.year', $year)
+            ->where('receipts.type', 'IN')
+            ->sum('receipts_rows.amount');
+
+        $totalExpenses = DB::table('receipts')
+            ->join('receipts_rows', 'receipts.id', '=', 'receipts_rows.receip_id')
+            ->where('receipts.year', $year)
+            ->where('receipts.type', 'OUT')
+            ->sum('receipts_rows.amount');
+
+        $delta = $totalIncome - $totalExpenses;
+
+        return [
+            'totalIncome' => $totalIncome,
+            'totalExpenses' => $totalExpenses,
+            'delta' => $delta
+        ];
+    }
+
+    public function getTopCausalsByAmount($limit = 10)
+    {
+        $year = $this->yearFilter;
+
+        $query = DB::table('receipts_rows')
+            ->join('receipts', 'receipts_rows.receip_id', '=', 'receipts.id')
+            ->join('causals', 'receipts_rows.causal_id', '=', 'causals.id')
+            ->where('receipts.year', $year);
+
+        $query->where('receipts.type', 'IN');
+
+        Log::info('Query: ' . $query->toSql());
+
+        $causals = $query->select(
+            'causals.id',
+            'causals.name',
+            'causals.parent_id',
+            DB::raw('SUM(receipts_rows.amount) as total_amount')
+        )
+            ->groupBy('causals.id', 'causals.name', 'causals.parent_id')
+            ->orderBy('total_amount', 'desc')
+            ->limit($limit)
+            ->get();
+
+        Log::info('Causals: ' . json_encode($causals));
+
+        $inData = [];
+
+        foreach ($causals as $causal) {
+            $tempCausal = new \App\Models\Causal();
+            $tempCausal->id = $causal->id;
+            $tempCausal->name = $causal->name;
+            $tempCausal->parent_id = $causal->parent_id;
+
+            $treeName = $tempCausal->getTree();
+
+            $displayName = strlen($treeName) > 30 ? substr($treeName, 0, 27) . '...' : $treeName;
+
+            $inData[] = [
+                'label' => $displayName,
+                'value' => $causal->total_amount,
+                'fullName' => $treeName
+            ];
+        }
+
+        usort($inData, function ($a, $b) {
+            return $b['value'] <=> $a['value'];
+        });
+
+        $inData = array_slice($inData, 0, $limit);
+
+        return [
+            'inLabels' => array_column($inData, 'label'),
+            'inData' => $inData,
+            'datasets' => [
+                [
+                    'label' => 'Entrate per Causale',
+                    'data' => array_column($inData, 'value'),
+                ]
+            ]
+        ];
+    }
+
+    public function getCoursesForSelect()
+    {
+        $currentYear = date('Y');
+
+        $courses = Course::with(['level', 'type', 'frequency'])
+            ->where('active', true)
+            ->where('year', 'like', '%' . $currentYear . '%')
+            ->orderBy('name')
+            ->get()
+            ->map(function ($course) {
+                $levelName = $course->level ? $course->level->name : 'No Level';
+                $typeName = $course->type ? $course->type->name : 'No Type';
+                $frequencyName = $course->frequency ? $course->frequency->name : 'No Frequency';
+                $year = $course->year ?? '';
+
+                return [
+                    'id' => $course->id,
+                    'name' => $course->name,
+                    'full_name' => "{$course->name} - {$levelName} - {$typeName} - {$frequencyName} ({$year})",
+                    'level_name' => $levelName,
+                    'type_name' => $typeName,
+                    'frequency_name' => $frequencyName,
+                    'year' => $year
+                ];
+            })->toArray();
+
+        return $courses;
+    }
+
+
+    public function getCourseMonthlyEarnings()
+    {
+        $courseId = $this->selectedCourse;
+        Log::info('Getting earnings for course ID: ' . $courseId);
+
+        if (empty($courseId)) {
+
+            return [
+                'labels' => ['Set', 'Ott', 'Nov', 'Dic', 'Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago'],
+                'datasets' => [
+                    [
+                        'label' => 'Pagamenti Effettuati',
+                        'backgroundColor' => 'rgba(0, 184, 148, 1)',
+                        'data' => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                        'type' => 'bar',
+                        'order' => 3
+                    ],
+                    [
+                        'label' => 'Pagamenti Totali',
+                        'backgroundColor' => 'transparent',
+                        'borderColor' => 'rgba(48, 51, 107, 1)',
+                        'borderWidth' => 3,
+                        'pointBackgroundColor' => 'rgba(48, 51, 107, 1)',
+                        'pointRadius' => 5,
+                        'data' => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                        'type' => 'line',
+                        'tension' => 0.2,
+                        'order' => 2
+                    ]
+                ]
+            ];
+        }
+
+        $monthOrder = [9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7, 8];
+
+        $monthlyData = [];
+        foreach ($monthOrder as $i) {
+            $monthlyData[$i] = [
+                'earned' => 0,
+                'total' => 0,
+                'participants' => 0
+            ];
+        }
+
+        $memberCourses = \App\Models\MemberCourse::where('course_id', $courseId)
+            ->with('member')
+            ->get();
+
+        foreach ($memberCourses as $memberCourse) {
+            $price = (float)($memberCourse->price ?? 0);
+
+            if ($memberCourse->months) {
+                $monthsData = json_decode($memberCourse->months, true);
+
+                if (is_array($monthsData)) {
+                    foreach ($monthsData as $monthData) {
+                        $month = $monthData['m'] ?? null;
+                        $status = $monthData['status'] ?? '';
+
+                        if ($month !== null && isset($monthlyData[$month])) {
+                            $monthlyData[$month]['total'] += $price;
+
+                            if ($status === 1) {
+                                $monthlyData[$month]['earned'] += $price;
+                            }
+
+                            $monthlyData[$month]['participants']++;
+                        }
+                    }
+                }
+            }
+        }
+
+        $monthNames = [
+            9 => 'Set',
+            10 => 'Ott',
+            11 => 'Nov',
+            12 => 'Dic',
+            1 => 'Gen',
+            2 => 'Feb',
+            3 => 'Mar',
+            4 => 'Apr',
+            5 => 'Mag',
+            6 => 'Giu',
+            7 => 'Lug',
+            8 => 'Ago',
+        ];
+
+        $labels = [];
+        $earnedData = [];
+        $totalData = [];
+        $participantData = [];
+        $missingData = [];
+
+        foreach ($monthOrder as $month) {
+            $labels[] = $monthNames[$month];
+            $earnedData[] = round($monthlyData[$month]['earned'], 2);
+            $totalData[] = round($monthlyData[$month]['total'], 2);
+            $participantData[] = $monthlyData[$month]['participants'];
+            $missingData[] = round($monthlyData[$month]['total'] - $monthlyData[$month]['earned'], 2);
+        }
+
+        return [
+            'labels' => $labels,
+            'datasets' => [
+                [
+                    'label' => 'Pagamenti Effettuati',
+                    'backgroundColor' => 'rgba(0, 184, 148, 1)',
+                    'data' => $earnedData,
+                    'type' => 'bar',
+                    'order' => 3
+                ],
+                [
+                    'label' => 'Pagamenti Attesi',
+                    'backgroundColor' => 'transparent',
+                    'borderColor' => 'rgba(48, 51, 107, 1)',
+                    'borderWidth' => 3,
+                    'pointBackgroundColor' => 'rgba(48, 51, 107, 1)',
+                    'pointRadius' => 5,
+                    'data' => $totalData,
+                    'type' => 'line',
+                    'tension' => 0.2,
+                    'order' => 2,
+                    'participants' => $participantData,
+                    'missing' => $missingData
+                ]
+            ]
+        ];
+    }
+
+    public static function getMemberCountChartData($endYear = null, $span = 5)
+    {
+        if ($endYear === null) {
+            $endYear = date('Y');
+        }
+
+        $startYear = $endYear - $span + 1;
+
+        $memberCards = MemberCard::select('member_id', 'expire_date')
+            ->whereNotNull('expire_date')
+            ->whereNotNull('member_id')
+            ->where('status', '!=', 'cancelled')
+            ->whereRaw('YEAR(expire_date) >= ?', [$startYear])
+            ->whereRaw('YEAR(expire_date) <= ?', [$endYear])
+            ->get();
+
+        $yearCounts = [];
+        for ($year = $startYear; $year <= $endYear; $year++) {
+            $yearPeriod = ($year - 1) . '-' . $year;
+            $yearCounts[$yearPeriod] = [];
+        }
+
+        foreach ($memberCards as $card) {
+            $expireYear = date('Y', strtotime($card->expire_date));
+
+            $previousYear = $expireYear - 1;
+            $yearPeriod = $previousYear . '-' . $expireYear;
+
+            if (isset($yearCounts[$yearPeriod])) {
+                $yearCounts[$yearPeriod][$card->member_id] = true;
+            }
+        }
+
+        $yearLabels = [];
+        $memberCountData = [];
+
+        foreach ($yearCounts as $yearPeriod => $members) {
+            $yearLabels[] = $yearPeriod;
+            $memberCountData[] = count($members);
+        }
+
+        return [
+            'labels' => $yearLabels,
+            'datasets' => [
+                [
+                    'label' => 'Membri Tesserati',
+                    'data' => $memberCountData,
+                    'backgroundColor' => 'rgba(54, 162, 235, 0.2)',
+                    'borderColor' => 'rgba(54, 162, 235, 1)',
+                    'borderWidth' => 2,
+                    'pointBackgroundColor' => 'rgba(54, 162, 235, 1)',
+                    'pointRadius' => 4,
+                    'tension' => 0.3,
+                    'fill' => true
+                ]
+            ]
+        ];
+    }
+}

+ 10 - 1
resources/views/layouts/app.blade.php

@@ -353,13 +353,22 @@
                             </div>
                         @endif
                     @endif
+                    @if(Auth::user()->level == env('LEVEL_ADMIN', 0))
+                        <div class="accordion-item " style="{{Request::is('settings') || Request::is('categories') || Request::is('disciplines') || Request::is('cards') || Request::is('course_subscriptions') || Request::is('courses') || Request::is('course_durations') || Request::is('course_frequencies') || Request::is('course_levels') || Request::is('course_types') || Request::is('banks') || Request::is('causals') || Request::is('vats') || Request::is('payment_methods') || Request::is('users') ? 'background-color: #c5d9e6;' : ''}}">
+                            <h2 class="accordion-header linkMenu">
+                                <a class="accordion-button collapsed" href="/reports">
+                                    Reports
+                                </a>
+                            </h2>
+                        </div>
+                    @endif
                 </div>
                 </div>
             </div>
         </div>
 
         <button id="open-filter" onclick="pcsh1()"></button>
-        
+
 
         <div class="col">
             <div class="row h-100">

+ 579 - 0
resources/views/livewire/reports.blade.php

@@ -0,0 +1,579 @@
+<!-- filepath: /Users/fabiofratini/Desktop/Projects/iao_team/resources/views/livewire/reports.blade.php -->
+<div class="col card--ui" id="card--dashboard">
+
+    <header id="title--section" style="display:none !important"
+        class="d-flex align-items-center justify-content-between">
+        <div class="title--section_name d-flex align-items-center justify-content-between">
+            <i class="ico--ui title_section utenti me-2"></i>
+            <h2 class="primary">Reports</h2>
+        </div>
+
+    </header>
+
+
+    <section id="subheader" class="d-flex align-items-center">
+    </section>
+
+    <section id="reports-section">
+
+        <div class="row">
+            <div class="col-md-12">
+                <canvas id="monthly-in-out-chart"></canvas>
+            </div>
+
+        </div>
+        <div class="col-md-12 chart-container">
+            <canvas id="causals-chart" style="height: 300px; max-height: 300px;"></canvas>
+        </div>
+        <div class="row mt-5">
+            <div class="col-md-12">
+                <canvas id="tesserati-chart"></canvas>
+            </div>
+        </div>
+        <div class="row mt-5">
+            <div class="col-md-6">
+                <select wire:model="selectedCourse" wire:change="updateCourseChart">
+                    <option value="">Seleziona un Corso</option>
+                    @foreach($courses as $course)
+                        <option value="{{ $course['id'] }}">{{ $course['full_name'] }}</option>
+                    @endforeach
+                </select>
+            </div>
+            <div class="col-md-12 mt-3">
+                <canvas id="courses-chart" style="height: 250px; max-height: 250px;"></canvas>
+            </div>
+        </div>
+    </section>
+
+</div>
+
+@push('scripts')
+    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
+    <script>
+        document.addEventListener('DOMContentLoaded', function () {
+            initializeChartSizes();
+
+            window.livewire.on('dataUpdated', () => {
+                updateCharts();
+                updateCausalsChart();
+            });
+
+            window.livewire.on('courseDataUpdated', async (courseId) => {
+                console.log('Course data update event received for course ID:', courseId);
+                await updateCoursesChart(courseId);
+                updateCausalsChart();
+                Object.keys(window.chartSizes).forEach(chartId => {
+                    restoreChartSize(chartId);
+                });
+            });
+
+            updateCharts();
+            updateCausalsChart();
+            updateTesseratiChart();
+
+
+            async function updateCharts() {
+                try {
+                    const monthlyData = await @this.getMonthlyTotals();
+
+                    if (window.monthlyInOutChart) {
+                        window.monthlyInOutChart.destroy();
+                    }
+
+                    const monthlyInOutCtx = document.getElementById('monthly-in-out-chart').getContext('2d');
+
+                    window.monthlyInOutChart = new Chart(monthlyInOutCtx, {
+                        type: 'bar',
+                        data: monthlyData,
+                        options: {
+                            responsive: true,
+                            scales: {
+                                x: {
+                                    title: {
+                                        display: false,
+                                        text: 'Mese'
+                                    },
+                                    grid: {
+                                        display: false
+                                    }
+                                },
+                                y: {
+                                    display: false,
+                                    title: {
+                                        display: false,
+                                        text: 'Importo (€)'
+                                    },
+                                    beginAtZero: true,
+                                    ticks: {
+                                        display: false
+                                    },
+                                    grid: {
+                                        display: false
+                                    }
+                                }
+                            },
+                            plugins: {
+                                legend: {
+                                    display: true
+                                },
+                                title: {
+                                    display: true,
+                                    text: 'Entrate/Uscite Mensili',
+                                    font: {
+                                        size: 16
+                                    }
+                                },
+                                tooltip: {
+                                    callbacks: {
+                                        label: function (context) {
+                                            let label = context.dataset.label || '';
+                                            if (label) {
+                                                label += ': ';
+                                            }
+                                            if (context.parsed.y !== null) {
+                                                label += new Intl.NumberFormat('it-IT', {
+                                                    style: 'currency',
+                                                    currency: 'EUR'
+                                                }).format(context.parsed.y);
+                                            }
+                                            return label;
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    });
+
+                    const summaryData = await @this.getYearlySummary();
+
+                } catch (error) {
+                    console.error('Error updating charts:', error);
+                    document.getElementById('monthly-in-out-chart').insertAdjacentHTML(
+                        'afterend',
+                        '<div class="alert alert-danger">Errore nel caricamento dei dati finanziari</div>'
+                    );
+                }
+            }
+
+            async function updateCausalsChart() {
+                try {
+                    const causalsData = await @this.getTopCausalsByAmount(10, 'IN');
+
+                    if (window.causalsChart) {
+                        window.causalsChart.destroy();
+                    }
+
+                    const causalsCtx = document.getElementById('causals-chart').getContext('2d');
+
+                    const existingTabs = document.querySelector('.causals-tabs');
+                    if (existingTabs) {
+                        existingTabs.remove();
+                    }
+
+                    const existingTitle = document.querySelector('.causals-title');
+                    if (existingTitle) {
+                        existingTitle.remove();
+                    }
+
+                    const chartTitle = document.createElement('h4');
+                    chartTitle.className = 'text-center mt-2 mb-3 causals-title';
+
+                    const chartCanvas = document.getElementById('causals-chart');
+                    chartCanvas.parentNode.insertBefore(chartTitle, chartCanvas);
+
+                    const inData = causalsData.inData;
+
+                    const colors = [
+                        'rgba(54, 162, 235, 0.8)',   // Blue
+                        'rgba(75, 192, 192, 0.8)',   // Teal
+                        'rgba(153, 102, 255, 0.8)',  // Purple
+                        'rgba(255, 159, 64, 0.8)',   // Orange
+                        'rgba(39, 174, 96, 0.8)',    // Green
+                        'rgba(41, 128, 185, 0.8)',   // Dark blue
+                        'rgba(142, 68, 173, 0.8)',   // Dark purple
+                        'rgba(230, 126, 34, 0.8)',   // Dark orange
+                        'rgba(46, 204, 113, 0.8)',   // Light green
+                        'rgba(52, 152, 219, 0.8)'    // Light blue
+                    ];
+
+                    const commonOptions = {
+                        responsive: true,
+                        maintainAspectRatio: false,
+                        plugins: {
+                            title: {
+                                display: true,
+                                text: 'Causali performanti',
+                                font: {
+                                    size: 16
+                                }
+                            },
+                            tooltip: {
+                                callbacks: {
+                                    label: function (context) {
+                                        const fullName = inData[context.dataIndex]?.fullName || context.label;
+                                        const value = context.raw;
+
+                                        return fullName + ': ' + new Intl.NumberFormat('it-IT', {
+                                            style: 'currency',
+                                            currency: 'EUR'
+                                        }).format(value);
+                                    }
+                                }
+                            },
+                            legend: {
+                                display: true,
+                                position: 'right',
+                                labels: {
+                                    boxWidth: 15,
+                                    padding: 10,
+                                    generateLabels: function (chart) {
+                                        const data = chart.data;
+                                        if (data.labels.length && data.datasets.length) {
+                                            return data.labels.map(function (label, i) {
+                                                const meta = chart.getDatasetMeta(0);
+                                                const style = meta.controller.getStyle(i);
+
+                                                let shortenedLabel = label;
+                                                if (label.length > 20) {
+                                                    shortenedLabel = label.substring(0, 17) + '...';
+                                                }
+
+                                                return {
+                                                    text: shortenedLabel,
+                                                    fillStyle: style.backgroundColor,
+                                                    hidden: !chart.getDataVisibility(i),
+                                                    index: i,
+                                                    datasetIndex: 0
+                                                };
+                                            });
+                                        }
+                                        return [];
+                                    }
+                                }
+                            }
+                        }
+                    };
+
+                    let chartData = {
+                        labels: inData.map(item => item.label),
+                        datasets: [{
+                            label: 'Importo',
+                            data: inData.map(item => item.value),
+                            backgroundColor: inData.map((item, index) => colors[index % colors.length]),
+                            borderWidth: 1,
+                            borderColor: '#fff'
+                        }]
+                    };
+
+                    window.causalsChart = new Chart(causalsCtx, {
+                        type: 'doughnut',
+                        data: chartData,
+                        options: commonOptions
+                    });
+
+                } catch (error) {
+                    console.error('Error updating causals chart:', error);
+                    document.getElementById('causals-chart').insertAdjacentHTML(
+                        'afterend',
+                        '<div class="alert alert-danger">Errore nel caricamento dei dati delle causali</div>'
+                    );
+                }
+            }
+        });
+
+
+        async function updateCoursesChart() {
+            try {
+                const courseData = await @this.getCourseMonthlyEarnings();
+                console.log('Course data received:', courseData);
+
+                if (window.coursesChart) {
+                    window.coursesChart.destroy();
+                }
+
+                const coursesCtx = document.getElementById('courses-chart').getContext('2d');
+
+                const dashedLinesPlugin = {
+                    // Plugin definition unchanged
+                    id: 'dashedLines',
+                    beforeDatasetsDraw: (chart) => {
+                        const ctx = chart.ctx;
+                        const lineDataset = chart.data.datasets.find(d => d.type === 'line' && d.label === 'Pagamenti Attesi');
+                        const barDataset = chart.data.datasets.find(d => d.type === 'bar' && d.label === 'Pagamenti Effettuati');
+
+                        if (!lineDataset || !barDataset) return;
+
+                        const lineMeta = chart.getDatasetMeta(chart.data.datasets.indexOf(lineDataset));
+                        const barMeta = chart.getDatasetMeta(chart.data.datasets.indexOf(barDataset));
+
+                        if (!lineMeta.data.length || !barMeta.data.length) return;
+
+                        const missingData = lineDataset.missing || [];
+
+                        ctx.save();
+                        ctx.lineWidth = 2;
+                        ctx.setLineDash([8, 4]);
+                        ctx.strokeStyle = 'rgba(48, 51, 107, 0.3)';
+
+                        for (let i = 0; i < lineMeta.data.length; i++) {
+                            const linePoint = lineMeta.data[i];
+                            const barPoint = barMeta.data[i];
+
+                            if (!linePoint || !barPoint) continue;
+
+                            ctx.beginPath();
+                            ctx.moveTo(linePoint.x, linePoint.y);
+                            ctx.lineTo(linePoint.x, barPoint.y);
+                            ctx.stroke();
+
+                            if (missingData[i] && missingData[i] > 0) {
+                                const midY = (linePoint.y + barPoint.y) / 2;
+                                ctx.textAlign = 'center';
+                                ctx.font = '10px Arial';
+                                ctx.fillStyle = 'rgba(48, 51, 107, 0.8)';
+                            }
+                        }
+
+                        ctx.restore();
+                    }
+                };
+
+                window.coursesChart = new Chart(coursesCtx, {
+                    type: 'bar',
+                    plugins: [dashedLinesPlugin],
+                    data: courseData,
+                    options: {
+                        responsive: true,
+                        maintainAspectRatio: false,
+                        layout: {
+                            padding: {
+                                top: 20
+                            }
+                        },
+                        // Rest of options unchanged
+                        scales: {
+                            x: {
+                                grid: {
+                                    display: false
+                                }
+                            },
+                            y: {
+                                display: true,
+                                beginAtZero: true,
+                                grid: {
+                                    color: 'rgba(0, 0, 0, 0.1)',
+                                    borderDash: [5, 5]
+                                },
+                                ticks: {
+                                    callback: function (value) {
+                                        return new Intl.NumberFormat('it-IT', {
+                                            style: 'currency',
+                                            currency: 'EUR',
+                                            maximumFractionDigits: 0
+                                        }).format(value);
+                                    }
+                                }
+                            }
+                        },
+                        plugins: {
+                            legend: {
+                                display: true,
+                                position: 'top',
+                                align: 'center',
+                                labels: {
+                                    usePointStyle: true,
+                                    padding: 20
+                                }
+                            },
+                            title: {
+                                display: true,
+                                text: 'Pagamenti per corso',
+                                font: {
+                                    size: 16
+                                }
+                            },
+                            tooltip: {
+                                callbacks: {
+                                    label: function (context) {
+                                        if (context.dataset.label === 'Pagamenti Attesi') {
+                                            let parts = [];
+
+                                            const participants = context.dataset.participants ? context.dataset.participants[context.dataIndex] : 0;
+                                            parts.push('N° iscritti: ' + participants);
+
+                                            const expectedAmount = context.parsed.y;
+                                            parts.push('Pagamenti attesi: ' + new Intl.NumberFormat('it-IT', {
+                                                style: 'currency',
+                                                currency: 'EUR'
+                                            }).format(expectedAmount));
+
+                                            const missingAmount = context.dataset.missing ? context.dataset.missing[context.dataIndex] : 0;
+                                            parts.push('Ancora da pagare: ' + new Intl.NumberFormat('it-IT', {
+                                                style: 'currency',
+                                                currency: 'EUR'
+                                            }).format(missingAmount));
+
+                                            return parts.join(' | ');
+                                        }
+
+                                        let label = context.dataset.label || '';
+                                        if (label) {
+                                            label += ': ';
+                                        }
+
+                                        if (context.parsed.y !== null) {
+                                            label += new Intl.NumberFormat('it-IT', {
+                                                style: 'currency',
+                                                currency: 'EUR'
+                                            }).format(context.parsed.y);
+                                        }
+
+                                        return label;
+                                    }
+                                }
+                            }
+                        },
+                        elements: {
+                            point: {
+                                radius: 4,
+                                hoverRadius: 6
+                            },
+                            line: {
+                                tension: 0.4
+                            }
+                        }
+                    }
+                });
+
+                // Maintain chart dimensions
+                coursesCtx.canvas.style.height = '250px';
+                coursesCtx.canvas.style.maxHeight = '250px';
+
+            } catch (error) {
+                console.error('Error updating courses chart:', error);
+                document.getElementById('courses-chart').insertAdjacentHTML(
+                    'afterend',
+                    '<div class="alert alert-danger">Errore nel caricamento dei dati del corso</div>'
+                );
+            }
+        }
+
+        async function updateTesseratiChart() {
+            try {
+                const tesseratiData = await @this.getTesseratiData();
+
+                if (window.tesseratiChart) {
+                    window.tesseratiChart.destroy();
+                }
+
+                const tesseratiCtx = document.getElementById('tesserati-chart').getContext('2d');
+
+                window.tesseratiChart = new Chart(tesseratiCtx, {
+                    type: 'line',
+                    data: tesseratiData,
+                    options: {
+                        responsive: true,
+                        scales: {
+                            x: {
+                                title: {
+                                    display: true,
+                                    text: 'Anno Tesseramento'
+                                },
+                                grid: {
+                                    display: false
+                                }
+                            },
+                            y: {
+                                title: {
+                                    display: true,
+                                    text: 'Numero di Tesserati'
+                                },
+                                beginAtZero: true,
+                                ticks: {
+                                    precision: 0
+                                }
+                            }
+                        },
+                        plugins: {
+                            legend: {
+                                display: true,
+                                position: 'top'
+                            },
+                            title: {
+                                display: true,
+                                text: 'Andamento Tesserati per Anno',
+                                font: {
+                                    size: 16
+                                }
+                            },
+                            tooltip: {
+                                callbacks: {
+                                    label: function (context) {
+                                        let label = context.dataset.label || '';
+                                        if (label) {
+                                            label += ': ';
+                                        }
+                                        if (context.parsed.y !== null) {
+                                            label += context.parsed.y + ' tesserati';
+                                        }
+                                        return label;
+                                    }
+                                }
+                            }
+                        }
+                    }
+                });
+            } catch (error) {
+                console.error('Error updating tesserati chart:', error);
+                document.getElementById('tesserati-chart').insertAdjacentHTML(
+                    'afterend',
+                    '<div class="alert alert-danger">Errore nel caricamento dei dati tesserati</div>'
+                );
+            }
+        }
+
+        document.addEventListener('DOMContentLoaded', function () {
+            const selectElement = document.querySelector('select[name="selectedCourse"]');
+            if (selectElement) {
+                selectElement.addEventListener('change', function () {
+                    const selectedValue = this.value;
+                    console.log('Selected course ID:', selectedValue);
+                    @this.set('selectedCourse', selectedValue);
+                });
+            }
+        });
+
+        function initializeChartSizes() {
+            window.chartSizes = {
+                'monthly-in-out-chart': {
+                    height: document.getElementById('monthly-in-out-chart').style.height || 'auto',
+                    maxHeight: document.getElementById('monthly-in-out-chart').style.maxHeight || 'none'
+                },
+                'causals-chart': {
+                    height: '300px',
+                    maxHeight: '300px'
+                },
+                'tesserati-chart': {
+                    height: document.getElementById('tesserati-chart').style.height || 'auto',
+                    maxHeight: document.getElementById('tesserati-chart').style.maxHeight || 'none'
+                },
+                'courses-chart': {
+                    height: '250px',
+                    maxHeight: '250px'
+                }
+            };
+        }
+
+        function restoreChartSize(chartId) {
+            if (!window.chartSizes || !window.chartSizes[chartId]) return;
+
+            const canvas = document.getElementById(chartId);
+            if (canvas) {
+                canvas.style.height = window.chartSizes[chartId].height;
+                canvas.style.maxHeight = window.chartSizes[chartId].maxHeight;
+            }
+        }
+
+
+    </script>
+@endpush

+ 1 - 0
routes/web.php

@@ -81,6 +81,7 @@ Route::group(['middleware' => 'auth'], function () {
     Route::get('/records_in_out', \App\Http\Livewire\RecordINOUT::class);
     Route::get('/users', \App\Http\Livewire\User::class);
     Route::get('/profile', \App\Http\Livewire\Profile::class);
+    Route::get('/reports', \App\Http\Livewire\Reports::class);
 });
 
 Route::get('/receipt/{id}', function ($id) {