Jelajahi Sumber

report dinamico iscritti

ferrari 1 bulan lalu
induk
melakukan
c2b41fd468

+ 292 - 0
app/Http/Livewire/DynamicReport.php

@@ -0,0 +1,292 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+use Carbon\Carbon;
+
+class DynamicReport extends Component
+{
+    public $filters = [
+        'courses' => [],
+        'levels' => [],
+        'types' => [],
+        'seasons' => [],
+        'months' => [],
+    ];
+
+    public $filter_options = [
+        'courses' => [],
+        'levels' => [],
+        'types' => [],
+        'seasons' => [],
+        'months' => [],
+    ];
+
+    public $chart = [
+        'labels' => [],
+        'datasets' => [],
+    ];
+
+    public function mount()
+    {
+        $this->resetReport();
+        $this->initFiltersOptions();
+    }
+
+    public function resetReport()
+    {
+        $this->chart = [
+            'labels' => [],
+            'datasets' => []
+        ];
+    }
+
+    public function initFiltersOptions()
+    {
+        // courses
+        $this->filter_options['courses'] = \App\Models\Course::query()
+            ->select('name')
+            ->where('enabled', true)
+            ->whereNull('deleted_at')
+            ->groupBy('name')
+            ->orderBy('name')
+            ->pluck('name')
+            ->values()
+            ->toArray();
+
+        // levels
+        $this->filter_options['levels'] = \App\Models\CourseLevel::query()
+            ->where('enabled', true)
+            ->orderBy('name')
+            ->get(['id', 'name'])
+            ->toArray();
+
+        // types
+        $this->filter_options['types'] = \App\Models\CourseType::query()
+            ->where('enabled', true)
+            ->orderBy('name')
+            ->get(['id', 'name'])
+            ->toArray();
+
+        // seasons
+        $this->filter_options['seasons'] = $this->deriveSeasonOptionsFromCourseDateFrom();
+
+        // months
+        $this->filter_options['months'] = array_map(
+            fn($m) => ['id' => $m, 'name' => $this->monthLabels()[$m]],
+            $this->fiscalMonths()
+        );
+    }
+
+    public function applyFilters()
+    {
+        $monthsToShow = $this->fiscalMonths();
+        if (!empty($this->filters['months'])) {
+            $selected = array_map('intval', $this->filters['months']);
+            $monthsToShow = array_values(array_filter(
+                $monthsToShow,
+                fn($m) => in_array($m, $selected, true)
+            ));
+        }
+
+        $labels = array_map(fn($m) => $this->monthLabels()[$m], $monthsToShow);
+
+        $selectedSeasons = $this->filters['seasons'] ?? [];
+        $selectedSeasons = array_values(array_filter($selectedSeasons));
+
+        $rows = \App\Models\MemberCourse::query()
+            ->join('courses', 'courses.id', '=', 'member_courses.course_id')
+            ->leftJoin('course_levels', 'course_levels.id', '=', 'courses.course_level_id')
+            ->leftJoin('course_types', 'course_types.id', '=', 'courses.course_type_id')
+            ->whereNull('courses.deleted_at')
+            ->where('courses.enabled', 1)
+            ->when(!empty($this->filters['courses']), fn($q) => $q->whereIn('courses.name', $this->filters['courses']))
+            ->when(!empty($this->filters['levels']), fn($q) => $q->whereIn('courses.course_level_id', $this->filters['levels']))
+            ->when(!empty($this->filters['types']), fn($q) => $q->whereIn('courses.course_type_id', $this->filters['types']))
+            ->select([
+                'member_courses.id as member_course_id',
+                'member_courses.member_id',
+                'member_courses.months',
+                'courses.id as course_id',
+                'courses.name as course_name',
+                'courses.date_from',
+                'courses.date_to',
+                'course_levels.name as level_name',
+                'course_types.name as type_name',
+            ])
+            ->get();
+
+        $bucket = [];
+        foreach ($rows as $r) {
+            $seasonLabel = $this->fiscalSeasonFromDates($r->date_from, $r->date_to);
+
+            if (!empty($selectedSeasons)) {
+                if (!$seasonLabel || !in_array($seasonLabel, $selectedSeasons, true)) {
+                    continue;
+                }
+            }
+
+            $activeMonths = $this->activeMonthsFromJson($r->months);
+            if (empty($activeMonths)) continue;
+
+            if (!empty($this->filters['months'])) {
+                $activeMonths = array_values(array_intersect($activeMonths, array_map('intval', $this->filters['months'])));
+                if (empty($activeMonths)) continue;
+            }
+
+            $seriesKey = $this->makeSeriesKey($r, $seasonLabel);
+
+            foreach ($activeMonths as $m) {
+                if (!in_array($m, $monthsToShow, true)) continue;
+                $bucket[$seriesKey][$m][$r->member_course_id] = true;
+            }
+        }
+
+        $datasets = [];
+        foreach ($bucket as $seriesKey => $monthsMap) {
+            $data = [];
+            foreach ($monthsToShow as $m) {
+                $data[] = isset($monthsMap[$m]) ? count($monthsMap[$m]) : 0;
+            }
+            $datasets[] = [
+                'label' => $seriesKey,
+                'data'  => $data,
+            ];
+        }
+
+        $this->chart = [
+            'labels'   => $labels,
+            'datasets' => $datasets,
+        ];
+
+        $this->dispatchBrowserEvent('dynamic-report:updated', [
+            'chart' => $this->chart,
+        ]);
+    }
+
+    private function activeMonthsFromJson(?string $json)
+    {
+        if (!$json) return [];
+
+        $decoded = json_decode($json, true);
+        if (!is_array($decoded)) return [];
+
+        $out = [];
+        foreach ($decoded as $item) {
+            $m = $item['m'] ?? null;
+            $st = $item['status'] ?? null;
+
+            if ($m === null) continue;
+            $m = (int) $m;
+
+            if ((int) $st !== 1) continue;
+
+            if ($m >= 1 && $m <= 12) $out[] = $m;
+        }
+
+        $out = array_values(array_unique($out));
+        sort($out);
+
+        return $out;
+    }
+
+    private function fiscalSeasonFromDates(?string $dateFrom, ?string $dateTo): ?string
+    {
+        if (!$dateFrom) return null;
+
+        $from = \Carbon\Carbon::parse($dateFrom);
+        $to   = $dateTo ? \Carbon\Carbon::parse($dateTo) : null;
+
+        // sicurezza: se date_to esiste ed è prima di date_from, ignoriamo date_to
+        if ($to && $to->lessThan($from)) {
+            $to = null;
+        }
+
+        /**
+         * Strategia:
+         * - la stagione è determinata dal "cuore" del corso
+         * - se date_to esiste, prendiamo il punto medio
+         * - altrimenti usiamo date_from
+         */
+        if ($to) {
+            $midTimestamp = (int) (($from->timestamp + $to->timestamp) / 2);
+            $ref = \Carbon\Carbon::createFromTimestamp($midTimestamp);
+        } else {
+            $ref = $from;
+        }
+
+        $startYear = ($ref->month >= 9) ? $ref->year : ($ref->year - 1);
+
+        return $startYear . '-' . ($startYear + 1);
+    }
+
+
+    private function makeSeriesKey($r, ?string $seasonLabel): string
+    {
+        $parts = [];
+
+        if (!empty($this->filters['courses'])) $parts[] = $r->course_name ?? '';
+        if (!empty($this->filters['levels']))  $parts[] = $r->level_name ?? '';
+        if (!empty($this->filters['types']))   $parts[] = $r->type_name ?? '';
+
+        if (!empty($this->filters['seasons']) && $seasonLabel) $parts[] = $seasonLabel;
+
+        if (empty(array_filter($parts))) {
+            $parts[] = $r->course_name ?? 'Totale';
+        }
+
+        $parts = array_values(array_filter($parts, fn($p) => trim($p) !== ''));
+        return implode(' - ', $parts);
+    }
+
+    private function deriveSeasonOptionsFromCourseDateFrom()
+    {
+        $dates = \App\Models\Course::query()
+            ->where('enabled', 1)
+            ->whereNull('deleted_at')
+            ->whereNotNull('date_from')
+            ->whereNotNull('date_to')
+            ->pluck('date_to', 'date_from')
+            ->all();
+
+        $seasons = [];
+        foreach ($dates as $d_from => $d_to) {
+            $s = $this->fiscalSeasonFromDates($d_from, $d_to);
+            if ($s) $seasons[$s] = true;
+        }
+
+        $out = array_keys($seasons);
+        rsort($out);
+
+        return $out;
+    }
+
+    private function fiscalMonths()
+    {
+        return [9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7, 8];
+    }
+
+    private function monthLabels()
+    {
+        return [
+            1 => 'Gen',
+            2 => 'Feb',
+            3 => 'Mar',
+            4 => 'Apr',
+            5 => 'Mag',
+            6 => 'Giu',
+            7 => 'Lug',
+            8 => 'Ago',
+            9 => 'Set',
+            10 => 'Ott',
+            11 => 'Nov',
+            12 => 'Dic',
+        ];
+    }
+
+    public function render()
+    {
+        return view('livewire.dynamic_report');
+    }
+}

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

@@ -228,8 +228,8 @@
 }
 
 .chart-card:hover {
-    box-shadow: var(--shadow-lg);
-    transform: translateY(-2px);
+    /* box-shadow: var(--shadow-lg);
+    transform: translateY(-2px); */
 }
 
 .chart-header {

+ 22 - 4
resources/views/layouts/app.blade.php

@@ -218,6 +218,8 @@
                 print "Assenze";
             if (Request::is('financial_movements'))
                 print "Movimenti finanziari";
+            if (Request::is('dynamic_report'))
+                print "Report dinamico";
             @endphp
             </h3>
 
@@ -401,12 +403,28 @@
                             @endif
                         @endif
                         @if(Auth::user()->level == env('LEVEL_ADMIN', 0))
-                            <div class="accordion-item " style="{{Request::is('reports') ? 'background-color: #c5d9e6;' : ''}}">
-                                <h2 class="accordion-header linkMenu">
-                                    <a class="accordion-button collapsed" href="/reports">
+                            <div class="accordion-item">
+                                <h2 class="accordion-header linkMenu" id="headingFour">
+                                    <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseFour" aria-expanded="{{Request::is('reports') || Request::is('dynamic_report') ? 'true' : 'false'}}" aria-controls="collapseFour">
                                         Reports
-                                    </a>
+                                    </button>
                                 </h2>
+                                <div id="collapseFour" class="accordion-collapse collapse {{Request::is('reports') || Request::is('dynamic_report') ? 'show' : ''}}" aria-labelledby="headingFour" data-bs-parent="#accordionExample">
+                                    <div class="accordion-body">
+                                        <ul class="nav nav-pills flex-column align-items-center align-items-sm-start w-100" id="menu-contabilita" style="margin-top:0px;">
+                                            <li class="nav-item" style="{{Request::is('reports') ? 'background-color: #c5d9e6;' : ''}}">
+                                                <a href="/reports" class="nav-link d-flex align-items-center linkMenu">
+                                                    <span class="ms-3 d-md-inline">Report generale</span>
+                                                </a>
+                                            </li>
+                                            <li class="nav-item" style="{{Request::is('dynamic_report') ? 'background-color: #c5d9e6;' : ''}}">
+                                                <a href="/dynamic_report" class="nav-link d-flex align-items-center linkMenu">
+                                                    <span class="ms-3 d-md-inline">Report dinamico</span>
+                                                </a>
+                                            </li>
+                                        </ul>
+                                    </div>
+                                </div>
                             </div>
                         @endif
                     </div>

+ 223 - 0
resources/views/livewire/dynamic_report.blade.php

@@ -0,0 +1,223 @@
+{{-- resources/views/livewire/dynamic_report.blade.php --}}
+<div id="card--dashboard">
+    <div>
+        <div class="chart-row">
+            <div class="chart-card">
+                <div class="chart-body" style="padding-block: 20px">
+                    <div class="row row-gap-3">
+                        <div class="col-4">
+                            <div wire:ignore>
+                                <label for="filter-courses">Corso</label>
+                                <select id="filter-courses" multiple class="form-select">
+                                    @foreach ($filter_options['courses'] as $course)
+                                        <option value="{{ $course }}">{{ $course }}</option>
+                                    @endforeach
+                                </select>
+                            </div>
+                        </div>
+
+                        <div class="col-4">
+                            <div wire:ignore>
+                                <label for="filter-levels">Livello</label>
+                                <select id="filter-levels" multiple class="form-select">
+                                    @foreach ($filter_options['levels'] as $level)
+                                        <option value="{{ $level['id'] }}">{{ $level['name'] }}</option>
+                                    @endforeach
+                                </select>
+                            </div>
+                        </div>
+
+                        <div class="col-4">
+                            <div wire:ignore>
+                                <label for="filter-types">Tipo</label>
+                                <select id="filter-types" multiple class="form-select">
+                                    @foreach ($filter_options['types'] as $type)
+                                        <option value="{{ $type['id'] }}">{{ $type['name'] }}</option>
+                                    @endforeach
+                                </select>
+                            </div>
+                        </div>
+
+                        <div class="col-4">
+                            <div wire:ignore>
+                                <label for="filter-seasons">Stagione</label>
+                                <select id="filter-seasons" multiple class="form-select">
+                                    @foreach ($filter_options['seasons'] as $season)
+                                        <option value="{{ $season }}">{{ $season }}</option>
+                                    @endforeach
+                                </select>
+                            </div>
+                        </div>
+
+                        <div class="col-4">
+                            <div wire:ignore>
+                                <label for="filter-months">Mesi</label>
+                                <select id="filter-months" multiple class="form-select">
+                                    @foreach ($filter_options['months'] as $month)
+                                        <option value="{{ $month['id'] }}">{{ $month['name'] }}</option>
+                                    @endforeach
+                                </select>
+                            </div>
+                        </div>
+
+                        <div class="col text-end mt-4">
+                            <button type="button" class="btn--ui" wire:click="applyFilters">
+                                Applica filtri
+                            </button>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <div class="chart-row mb-0">
+            <div class="chart-card">
+                <div class="chart-body" wire:ignore>
+                    <div class="chart-wrapper" style="height: calc(100dvh - 450px)">
+                        <canvas id="dynamicReportChart"></canvas>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+
+@push('css')
+    <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
+    <link rel="stylesheet" href="/css/chart-reports.css">
+    <style>
+        .select2-container--default .select2-selection--multiple {
+            min-height: 40px !important;
+        }
+
+        .select2-container--default .select2-search--inline .select2-search__field {
+            margin-top: 0;
+        }
+
+        .select2-container--default .select2-search--inline {
+            display: inline-block;
+            padding-top: 0;
+            height: 26px !important;
+        }
+    </style>
+@endpush
+
+@push('scripts')
+    <script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
+    <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
+@endpush
+
+@push('scripts')
+<script>
+document.addEventListener("livewire:load", function () {
+	function initSelect2() {
+        const select2Options = {
+			language: {
+				noResults: function () {
+					return "Nessun risultato";
+				},
+			},
+		};
+
+		const $courses = $("#filter-courses").select2(select2Options);
+		$courses.off('change.dynamicReport').on('change.dynamicReport', function () {
+			@this.set("filters.courses", $(this).val() ?? []);
+		});
+
+		const $levels = $("#filter-levels").select2(select2Options);
+		$levels.off('change.dynamicReport').on('change.dynamicReport', function () {
+			@this.set("filters.levels", $(this).val() ?? []);
+		});
+
+		const $types = $("#filter-types").select2(select2Options);
+		$types.off('change.dynamicReport').on('change.dynamicReport', function () {
+			@this.set("filters.types", $(this).val() ?? []);
+		});
+
+		const $seasons = $("#filter-seasons").select2(select2Options);
+		$seasons.off('change.dynamicReport').on('change.dynamicReport', function () {
+			@this.set("filters.seasons", $(this).val() ?? []);
+		});
+
+		const $months = $("#filter-months").select2(select2Options);
+		$months.off('change.dynamicReport').on('change.dynamicReport', function () {
+			@this.set("filters.months", $(this).val() ?? []);
+		});
+	}
+	initSelect2();
+});
+</script>
+@endpush
+
+@push('scripts')
+    <script>
+        let dynamicReportChart = null;
+
+        function buildOrUpdateChart(payload) {
+            const ctx = document.getElementById('dynamicReportChart');
+            if (!ctx) return;
+
+            let data = payload.chart ?? { labels: [], datasets: [] };
+            data = applyDatasetStyles(data);
+
+            if (dynamicReportChart) {
+                dynamicReportChart.data.labels = data.labels;
+                dynamicReportChart.data.datasets = data.datasets;
+                dynamicReportChart.update();
+                return;
+            }
+
+            dynamicReportChart = new Chart(ctx, {
+                type: 'bar',
+                data: data,
+                options: {
+                    responsive: true,
+                    maintainAspectRatio: false,
+
+                    interaction: {
+                        mode: 'index',
+                        intersect: false,
+                    },
+
+                    plugins: {
+                        tooltip: {
+                            mode: 'index',
+                            intersect: false,
+                            itemSort: (a, b) => b.raw - a.raw,
+                        },
+                        legend: { display: true },
+                    },
+
+                    scales: {
+                        y: { beginAtZero: true }
+                    }
+                }
+            });
+        }
+
+        function applyDatasetStyles(data) {
+            if (!data?.datasets) return data;
+
+            data.datasets = data.datasets.map((ds, i) => {
+                const hue = (i * 47) % 360;
+
+                return {
+                    ...ds,
+                    barThickness: 10,
+                    maxBarThickness: 10,
+                    backgroundColor: `hsla(${hue}, 70%, 55%, 0.85)`,
+                    borderColor: `hsl(${hue}, 70%, 45%)`,
+                    borderWidth: 0,
+                };
+            });
+
+            return data;
+        }
+
+
+        window.addEventListener('dynamic-report:updated', (e) => {
+            buildOrUpdateChart(e.detail);
+        });
+    </script>
+@endpush

+ 1 - 0
routes/web.php

@@ -95,6 +95,7 @@ Route::group(['middleware' => 'auth'], function () {
     Route::get('/courts', \App\Http\Livewire\Court::class);
     Route::get('/motivations', \App\Http\Livewire\Motivation::class);
     Route::get('/reports', \App\Http\Livewire\Reports::class);
+    Route::get('/dynamic_report', \App\Http\Livewire\DynamicReport::class);
     Route::get('/members_archive', \App\Http\Livewire\MemberArchive::class);
     Route::get('/financial_movements', \App\Http\Livewire\FinancialMovements::class);
 });