Bladeren bron

riportato modulo presenze - [report presenze, report assenze, motivazioni, sezione presenze membro]

ferrari 1 maand geleden
bovenliggende
commit
00b069df4a

+ 301 - 46
app/Http/Livewire/AbsenceReport.php

@@ -2,83 +2,338 @@
 
 namespace App\Http\Livewire;
 
+use App\Http\Middleware\TenantMiddleware;
+use Illuminate\Support\Carbon;
 use Livewire\Component;
 
 class AbsenceReport extends Component
 {
-
     public $records;
+    public $record_assenze;
+    public $original_record_assenze;
 
+    public $fiscalStartMonth = 9;
     public $year;
 
-    public function mount()
+    public $search;
+
+    public function boot()
     {
-        setlocale(LC_ALL, 'it_IT');
+        app(TenantMiddleware::class)->setupTenantConnection();
     }
 
-    public function render()
+    public function mount()
     {
         setlocale(LC_ALL, 'it_IT');
 
-        $this->records = [];
+        $this->record_assenze = [];
 
-        // $to = date("Y-m-d 23:59:59");
-        // $calendars = \App\Models\Calendar::where('from', '<=', $to)->orderBy('from')->get();
+        $end_day = now()->yesterday();
+        $limit = $end_day->endOfDay();
 
-        $month = date("n");
-        $this->year = ($month >= 9) ? date("Y") : date("Y") - 1;
+        $this->year = ($end_day->month >= $this->fiscalStartMonth) ? $end_day->year : $end_day->year - 1;
 
-        $start = date("Y-m-d H:i:s", mktime(0, 0, 0, 9, 1, $this->year));
-        $end   = date("Y-m-d 23:59:59");
+        $dayMap = [
+            'lun' => 1,
+            'mar' => 2,
+            'mer' => 3,
+            'gio' => 4,
+            'ven' => 5,
+            'sab' => 6,
+            'dom' => 0,
+        ];
 
-        $calendars = \App\Models\Calendar::whereBetween('from', [$start, $end])->orderBy('from')->get();
+        try {
+            $courses = \App\Models\Course::whereDate('date_from', '<=', $limit)
+                ->whereDate('date_to', '>=', $limit)
+                ->where('active', true)
+                ->where('enabled', true)
+                ->get();
 
-        foreach ($calendars as $calendar) {
+            foreach ($courses as $course) {
+                $course_members = \App\Models\MemberCourse::with(['member' => function ($q) {
+                    $q->where(function ($q) {
+                        $q->where('is_archived', false)->orWhereNull('is_archived');
+                    })->where(function ($q) {
+                        $q->where('is_deleted', false)->orWhereNull('is_deleted');
+                    });
+                }])
+                    ->where('course_id', $course->id)
+                    ->whereHas('member', function ($q) {
+                        $q->where(function ($q) {
+                            $q->where('is_archived', false)->orWhereNull('is_archived');
+                        })->where(function ($q) {
+                            $q->where('is_deleted', false)->orWhereNull('is_deleted');
+                        });
+                    })
+                    ->whereDate('date_from', '<=', $limit)
+                    ->whereDate('date_to', '>=', Carbon::parse($course->date_from)->startOfDay())
+                    ->get();
 
-            $presences = \App\Models\Presence::where('calendar_id', $calendar->id)->where('status', '<>', 99);
-            $presences = $presences->pluck('member_id')->toArray();
-            $presences_annullate = \App\Models\Presence::where('calendar_id', $calendar->id)->where('status', 99)->pluck('member_id')->toArray();
+                if ($course_members->isEmpty()) {
+                    continue;
+                }
 
-            $days = ['dom', 'lun', 'mar', 'mer', 'gio', 'ven', 'sab'];
-            $dow = date('w', strtotime($calendar->from));
-            $d = $days[$dow];
+                $courseCalendars = \App\Models\Calendar::where('course_id', $course->id)->where('from', '<=', $limit)->where('status', '<>', 99)->get();
 
-            $h = date('H:i', strtotime($calendar->from));
+                if ($courseCalendars->isEmpty()) {
+                    continue;
+                }
 
-            // Elenco corsi per tipologia in base al calendario
-            $courses = \App\Models\Course::where('name', $calendar->name)->where('date_from', '<=', $calendar->from)->where('date_to', '>=', $calendar->to);
-            $courses = $courses->pluck('id')->toArray();
+                $calendarIndex = [];
+                foreach ($courseCalendars as $cal) {
+                    $fromKey = Carbon::parse($cal->from)->toDateTimeString();
+                    $toKey = Carbon::parse($cal->to)->toDateTimeString();
+                    $key = $fromKey . '|' . $toKey;
 
-            $months = date("n", strtotime($calendar->from));
+                    $calendarIndex[$key] = $cal;
+                }
 
-            // Elenco utenti iscritti al corso "padre"
-            $members = \App\Models\MemberCourse::where('when', 'like', "%" . $d . "%")
-                ->where('when', 'like', '%"from":"' . $h . '"%')
-                ->whereNot('months', 'like', '%"m":' . $months . ',"status":2%')
-                ->whereDate('date_from', '<=', $calendar->from)                                 
-                ->whereDate('date_to', '>=', $calendar->from)      
-                ->whereIn('course_id', $courses)
-                ->orderBy('member_id')
-                ->get();
-            //$members = \App\Models\MemberCourse::where('when', 'like', "%" . $d . "%")->where('when', 'like', '%"from":"' . $h . '"%')->whereIn('member_id', $presences)->whereIn('course_id', $courses)->get();
-            foreach ($members as $member) {
-
-                $presence = \App\Models\Presence::where('member_id', $member->member->id)->where('calendar_id', $calendar->id)->first();
-
-                if (!in_array($member->member->id, $presences)) {
-                    if (!in_array($member->member->id, $presences_annullate)) {
-                        if (array_key_exists($member->member->id, $this->records)) {
-                            $this->records[$member->member->id]['total'] += 1;
-                            $this->records[$member->member->id]['date'] .= " - " . date("d/m", strtotime($calendar->from));
-                        } else
-                            $this->records[$member->member->id] = array("last_name" => $member->member->last_name, "first_name" => $member->member->first_name, "course" => $calendar->name, "total" => 1, "date" => date("d/m", strtotime($calendar->from)));
+                $memberIds = $course_members->pluck('member_id')->unique()->values()->all();
+
+                $presences = \App\Models\Presence::query()
+                    ->whereIn('calendar_id', $courseCalendars->pluck('id')->all())
+                    ->whereIn('member_id', $memberIds)
+                    ->where('status', '<>', 99)
+                    ->get(['member_id', 'calendar_id']);
+
+                $presenceIndex = [];
+                foreach ($presences as $p) {
+                    $presenceIndex[$p->member_id . '|' . $p->calendar_id] = true;
+                }
+
+                $makeups = \App\Models\Presence::query()
+                    ->join('calendars', 'presences.calendar_id', '=', 'calendars.id')
+                    ->where('presences.motivation_course_id', $course->id)
+                    ->whereIn('presences.member_id', $memberIds)
+                    ->where('presences.status', '<>', 99)
+                    ->where('calendars.from', '<=', $limit)
+                    ->where('calendars.status', '<>', 99)
+                    ->selectRaw('presences.member_id, MAX(calendars.from) as last_makeup_from')
+                    ->groupBy('presences.member_id')
+                    ->get();
+
+                $lastMakeupByMember = [];
+                foreach ($makeups as $m) {
+                    $lastMakeupByMember[$m->member_id] = Carbon::parse($m->last_makeup_from);
+                }
+
+                $this->record_assenze[$course->id] = [
+                    'course' => [
+                        'id' => $course->id,
+                        'name' => $course->getDetailsName(),
+                    ],
+                    'members' => [],
+                ];
+
+                $courseWhen = json_decode($course->when ?? '[]', true);
+                if (!is_array($courseWhen) || empty($courseWhen)) {
+                    unset($this->record_assenze[$course->id]);
+                    continue;
+                }
+
+                foreach ($course_members as $course_member) {
+                    $this->record_assenze[$course->id]['members'][$course_member->id] = [
+                        'member' => [
+                            'id' => $course_member->member->id,
+                            'first_name' => $course_member->member->first_name,
+                            'last_name' => $course_member->member->last_name,
+                        ],
+                        'count' => 0,
+                        'dates' => [],
+                    ];
+
+                    $mid = $course_member->member_id;
+
+                    $rangeStart = Carbon::parse(max($course->date_from, $course_member->date_from))->startOfDay();
+                    $rangeEnd = Carbon::parse(min($course->date_to, $course_member->date_to, $limit->toDateString()))->endOfDay();
+
+                    if ($rangeStart->gt($rangeEnd)) {
+                        unset($this->record_assenze[$course->id]['members'][$course_member->id]);
+                        continue;
+                    }
+
+                    $memberCalendars = [];
+
+                    foreach ($courseWhen as $period) {
+                        $days = $period['day'] ?? [];
+                        if (empty($days)) continue;
+
+                        $fromTime = $period['from'] ?? null;
+                        $toTime   = $period['to'] ?? null;
+                        if (!$fromTime || !$toTime) continue;
+
+                        $ranges = $this->generateDateRanges(
+                            $rangeStart,
+                            $rangeEnd,
+                            $days,
+                            $dayMap,
+                            $fromTime,
+                            $toTime
+                        );
+
+                        foreach ($ranges as $range) {
+                            $key = $range['from']->toDateTimeString() . '|' . $range['to']->toDateTimeString();
+
+                            if (isset($calendarIndex[$key])) {
+                                $memberCalendars[] = $calendarIndex[$key];
+                            }
+                        }
+                    }
+
+                    if (empty($memberCalendars)) {
+                        unset($this->record_assenze[$course->id]['members'][$course_member->id]);
+                        continue;
+                    }
+
+                    usort($memberCalendars, fn($a, $b) => $b->to <=> $a->to);
+
+                    $lastAttendance = $lastMakeupByMember[$mid] ?? null;
+
+                    foreach ($memberCalendars as $calendar) {
+                        $lessonFrom = Carbon::parse($calendar->from);
+
+                        if ($lastAttendance && $lessonFrom->lte($lastAttendance)) {
+                            break;
+                        }
+
+                        $pKey = $mid . '|' . $calendar->id;
+                        if (isset($presenceIndex[$pKey])) {
+                            break;
+                        }
+
+                        // assenza vera
+                        $this->record_assenze[$course->id]['members'][$course_member->id]['count']++;
+                        $this->record_assenze[$course->id]['members'][$course_member->id]['dates'][] = [
+                            'calendar_id' => $calendar->id,
+                            'date' => $lessonFrom->translatedFormat('d/m'),
+                        ];
+                    }
+
+                    if ($this->record_assenze[$course->id]['members'][$course_member->id]['count'] < 2) {
+                        unset($this->record_assenze[$course->id]['members'][$course_member->id]);
                     }
                 }
+
+                if (empty($this->record_assenze[$course->id]['members'])) {
+                    unset($this->record_assenze[$course->id]);
+                } else {
+                    $members = $this->record_assenze[$course->id]['members'];
+                    usort($members, function ($a, $b) {
+                        if ($a['count'] !== $b['count']) {
+                            return $b['count'] <=> $a['count'];
+                        }
+
+                        $last = strcmp($a['member']['last_name'], $b['member']['last_name']);
+                        if ($last !== 0) return $last;
+
+                        return strcmp($a['member']['first_name'], $b['member']['first_name']);
+                    });
+                    $this->record_assenze[$course->id]['members'] = $members;
+                }
             }
+
+            usort($this->record_assenze, fn($a, $b) => $a['course']['name'] <=> $b['course']['name']);
+        } catch (\Throwable $e) {
+            dd($e->getMessage());
         }
 
-        array_multisort(array_column($this->records, 'total'), SORT_DESC, $this->records);
+        $this->original_record_assenze = $this->record_assenze;
+    }
+
+    public function render()
+    {
+        setlocale(LC_ALL, 'it_IT');
 
         return view('livewire.absence_report');
     }
+
+    public function applySearch()
+    {
+        $this->search = trim($this->search);
+        $this->filter();
+    }
+
+    public function resetSearch()
+    {
+        $this->search = '';
+        $this->record_assenze = $this->original_record_assenze;
+    }
+
+    protected function filter()
+    {
+        $this->record_assenze = $this->original_record_assenze;
+
+        if ($this->search === '') return;
+
+        $needle = mb_strtolower($this->search);
+
+        foreach ($this->record_assenze as $courseId => $courseData) {
+            $courseData['members'] = array_values(array_filter($courseData['members'], function ($m) use ($needle) {
+                $full  = mb_strtolower((trim($m['member']['last_name']) ?? '') . ' ' . (trim($m['member']['first_name']) ?? ''));
+                $full2 = mb_strtolower((trim($m['member']['first_name']) ?? '') . ' ' . (trim($m['member']['last_name']) ?? ''));
+                return str_contains($full, $needle) || str_contains($full2, $needle);
+            }));
+
+            if (empty($courseData['members'])) {
+                unset($this->record_assenze[$courseId]);
+            } else {
+                $this->record_assenze[$courseId] = $courseData;
+            }
+        }
+    }
+
+    protected function generateDateRanges($rangeStart, $rangeEnd, $days, $dayMap, $fromTime, $toTime)
+    {
+        $allowedDow = collect($days)
+            ->map(fn($d) => $dayMap[$d] ?? null)
+            ->filter(fn($v) => $v !== null)
+            ->unique()
+            ->values()
+            ->all();
+
+        // parse orari
+        $fromC = Carbon::parse($fromTime);
+        $toC = Carbon::parse($toTime);
+
+        $fromHour = $fromC->hour;
+        $fromMinute = $fromC->minute;
+        $fromSecond = $fromC->second;
+
+        $toHour = $toC->hour;
+        $toMinute = $toC->minute;
+        $toSecond = $toC->second;
+
+        $ranges = [];
+
+        foreach ($allowedDow as $dow) {
+            $current = $rangeStart->copy()->startOfDay();
+
+            $offset  = ($dow - $current->dayOfWeek + 7) % 7;
+            $current->addDays($offset);
+
+            while ($current->lte($rangeEnd)) {
+                $fromDateTime = $current->copy()->setTime($fromHour, $fromMinute, $fromSecond);
+                $toDateTime = $current->copy()->setTime($toHour, $toMinute, $toSecond);
+
+                if ($fromDateTime->lt($rangeStart)) {
+                    $current->addWeek();
+                    continue;
+                }
+                if ($fromDateTime->gt($rangeEnd)) {
+                    break;
+                }
+
+                $ranges[] = [
+                    'from' => $fromDateTime,
+                    'to' => $toDateTime,
+                ];
+
+                $current->addWeek();
+            }
+        }
+
+        usort($ranges, fn($a, $b) => $a['from'] <=> $b['from']);
+        return $ranges;
+    }
 }

+ 279 - 1
app/Http/Livewire/Member.php

@@ -50,7 +50,9 @@ class Member extends Component
             $this->loadMemberCertificates();
             // TODO $this->checkCourseAvailability();
         }
-
+        if ($type === 'presenze') {
+            $this->loadMemberPresences();
+        }
         $this->dispatchBrowserEvent('scroll-to-top');
     }
 
@@ -156,6 +158,21 @@ class Member extends Component
 
     public $hasCertificate = false;
 
+    public $presenceYears = [];
+    public $presenceYearFilter = '';
+    public $presenceTitle = [];
+    public $presenceTitleFilter = '';
+    public $member_presences = [];
+
+    public $presenceCourses = [];
+    public $presenceCourseFilter = '';
+
+    public $totals = 0;
+    public $presenze = 0;
+    public $assenze = 0;
+    public $annullate = 0;
+    public $recuperi = [];
+
     protected $rules = [
         'first_name' => 'required',
         'last_name' => 'required',
@@ -877,6 +894,267 @@ class Member extends Component
         $this->member_certificates = \App\Models\MemberCertificate::where('member_id', $this->dataId)->orderBy('expire_date', 'DESC')->get();
     }
 
+    public function updatedPresenceYearFilter()
+    {
+        $this->presenceCourseFilter = '';
+        $this->loadPresenceCombos();
+        $this->loadMemberPresences();
+    }
+
+    public function updatedPresenceCourseFilter()
+    {
+        $this->loadMemberPresences();
+    }
+
+    public function loadPresenceCombos()
+    {
+        $this->presenceYears = [];
+        $this->presenceCourses = [];
+
+        if (!$this->dataId) {
+            $this->presenceYearFilter = '';
+            $this->presenceCourseFilter = '';
+            return;
+        }
+
+        $subscribedCourseIds = \App\Models\MemberCourse::where('member_id', $this->dataId)
+            ->pluck('course_id')
+            ->all();
+
+        $makeupCourseIds = \App\Models\Presence::where('member_id', $this->dataId)
+            ->where('status', '<>', 99)
+            ->whereNotNull('motivation_course_id')
+            ->pluck('motivation_course_id')
+            ->all();
+
+        $allCourseIds = array_values(array_unique(array_merge($subscribedCourseIds, $makeupCourseIds)));
+
+        if (!empty($allCourseIds)) {
+            $this->presenceYears = \App\Models\Course::whereIn('id', $allCourseIds)
+                ->orderBy('year', 'desc')
+                ->groupBy('year')
+                ->pluck('year')
+                ->map(fn($y) => (string)$y)
+                ->all();
+        }
+
+        if (($this->presenceYearFilter === '' || !in_array((string)$this->presenceYearFilter, $this->presenceYears, true)) && !empty($this->presenceYears)) {
+            $this->presenceYearFilter = $this->presenceYears[0];
+        }
+
+        if ($this->presenceYearFilter !== '' && !empty($allCourseIds)) {
+            $this->presenceCourses = \App\Models\Course::whereIn('id', $allCourseIds)
+                ->where('year', $this->presenceYearFilter)
+                ->orderBy('name')
+                ->get(['id', 'name', 'year', 'course_level_id', 'course_type_id', 'course_frequency_id']);
+        }
+
+        $validCourseIds = collect($this->presenceCourses)->pluck('id')->map(fn($id) => (string)$id)->all();
+        if (($this->presenceCourseFilter === '' || !in_array((string)$this->presenceCourseFilter, $validCourseIds, true)) && !empty($validCourseIds)) {
+            $this->presenceCourseFilter = $validCourseIds[0];
+        }
+    }
+
+    public function loadMemberPresences()
+    {
+        $mesi = ['Settembre', 'Ottobre', 'Novembre', 'Dicembre', 'Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno', 'Luglio', 'Agosto'];
+        $valori = array_fill(0, 12, 0);
+
+        $monthMap = [9 => 0, 10 => 1, 11 => 2, 12 => 3, 1 => 4, 2 => 5, 3 => 6, 4 => 7, 5 => 8, 6 => 9, 7 => 10, 8 => 11];
+
+        // reset output
+        $this->member_presences = [];
+        $this->totals = 0;
+        $this->presenze = 0;
+        $this->assenze = 0;
+        $this->annullate = 0;
+        $this->recuperi = [];
+
+        $this->loadPresenceCombos();
+
+        if (!$this->dataId || $this->presenceYearFilter === '' || $this->presenceCourseFilter === '') {
+            $this->emit('load-chart', $mesi, $valori, []);
+            return;
+        }
+
+        $courseId = (int)$this->presenceCourseFilter;
+
+        $chartData = [
+            0 => ["label" => "Presenze", "backgroundColor" => "#0618be", "data" => array_fill(0, 12, 0), "grouped" => true, "stack" => "chartData", "barThickness" => "flex", "barPercentage" => 0.5, "categoryPercentage" => 0.3],
+            1 => ["label" => "Assenze", "backgroundColor" => "#ff0000", "data" => array_fill(0, 12, 0), "grouped" => true, "stack" => "chartData", "barThickness" => "flex", "barPercentage" => 0.5, "categoryPercentage" => 0.3],
+            2 => ["label" => "Annullate", "backgroundColor" => "#808080", "data" => array_fill(0, 12, 0), "grouped" => true, "stack" => "chartData", "barThickness" => "flex", "barPercentage" => 0.5, "categoryPercentage" => 0.3],
+        ];
+
+        $course = \App\Models\Course::find($courseId);
+        $courseWhen = $course?->when;
+
+        $calendars = \App\Models\Calendar::whereNull('manual')
+            ->where('course_id', $courseId)
+            ->orderBy('from')
+            ->get(['id', 'from', 'to', 'status']);
+
+        $calendarIds = $calendars->pluck('id')->all();
+
+        $presencesByCalendar = [];
+        if (!empty($calendarIds)) {
+            $presencesByCalendar = \App\Models\Presence::where('member_id', $this->dataId)
+                ->whereIn('calendar_id', $calendarIds)
+                ->with('motivation:id,name,show_in_member_presences')
+                ->get(['calendar_id', 'status', 'motivation_id'])
+                ->keyBy('calendar_id')
+                ->all();
+        }
+
+        $memberCourse = \App\Models\MemberCourse::where('member_id', $this->dataId)
+            ->where('course_id', $courseId)
+            ->first();
+
+        $expected = null;
+
+        if ($memberCourse) {
+            $expected = [
+                'date_from' => $memberCourse->date_from,
+                'date_to'   => $memberCourse->date_to,
+                'course_when' => $courseWhen,
+            ];
+        }
+
+        $isExpected = function (string $calendarFrom) use ($expected) {
+            if (!$expected) return false;
+
+            if ($calendarFrom < $expected['date_from'] || $calendarFrom > $expected['date_to']) return false;
+
+            $days = ['dom', 'lun', 'mar', 'mer', 'gio', 'ven', 'sab'];
+            $dow = (int)date('w', strtotime($calendarFrom));
+            $d = $days[$dow];
+            $h = date('H:i', strtotime($calendarFrom));
+
+            return $this->courseMatchesSlot($expected['course_when'] ?? null, $d, $h);
+        };
+
+        if ($expected) {
+            foreach ($calendars as $calendar) {
+                if (!$isExpected($calendar->from)) continue;
+
+                $idxMonth = $monthMap[(int)date('n', strtotime($calendar->from))] ?? null;
+
+                $presence = $presencesByCalendar[$calendar->id] ?? null;
+
+                $calendarCancelled = ((int)$calendar->status === 99);
+                $presenceCancelled = ($presence && (int)$presence->status === 99);
+
+                $statusHtml = '';
+                $motivationName = null;
+
+                if ($calendarCancelled || $presenceCancelled) {
+                    $statusHtml = "<span style=\"color:gray\">Annullata</span>";
+                    $this->annullate++;
+                    if ($idxMonth !== null) $chartData[2]['data'][$idxMonth]++;
+                } elseif ($presence) {
+                    $statusHtml = "<span style=\"color:#0618be\">Presenza</span>";
+                    $this->presenze++;
+                    if ($idxMonth !== null) {
+                        $valori[$idxMonth]++;
+                        $chartData[0]['data'][$idxMonth]++;
+                    }
+
+                    if ($presence->motivation && $presence->motivation->show_in_member_presences) {
+                        $motivationName = ucfirst($presence->motivation->name);
+                        $this->recuperi[$motivationName] = ($this->recuperi[$motivationName] ?? 0) + 1;
+                    }
+                } else {
+                    if (date('Ymd') > date('Ymd', strtotime($calendar->from))) {
+                        $statusHtml = "<span style=\"color:red\">Assenza</span>";
+                        $this->assenze++;
+                        if ($idxMonth !== null) $chartData[1]['data'][$idxMonth]++;
+                    } else {
+                        $statusHtml = "";
+                    }
+                }
+
+                $this->member_presences[] = [
+                    'calendar_id' => $calendar->id,
+                    'from' => $calendar->from,
+                    'to'   => $calendar->to,
+                    'status' => $statusHtml,
+                    'motivation' => $motivationName,
+                ];
+            }
+        }
+
+        $makeups = \App\Models\Presence::where('member_id', $this->dataId)
+            ->where('status', '<>', 99)
+            ->where('motivation_course_id', $courseId)
+            ->with([
+                'calendar:id,from,to,status',
+                'motivation:id,name,show_in_member_presences',
+            ])
+            ->get(['calendar_id', 'status', 'motivation_id', 'motivation_course_id']);
+
+        $alreadyCalendar = array_flip(array_column($this->member_presences, 'calendar_id'));
+
+        foreach ($makeups as $p) {
+            if (!$p->calendar) continue;
+
+            if (isset($alreadyCalendar[$p->calendar_id])) continue;
+
+            if (!$p->motivation || !$p->motivation->show_in_member_presences) continue;
+
+            $idxMonth = $monthMap[(int)date('n', strtotime($p->calendar->from))] ?? null;
+
+            if ((int)$p->calendar->status === 99) {
+                $statusHtml = "<span style=\"color:gray\">Annullata</span>";
+                $this->annullate++;
+                if ($idxMonth !== null) $chartData[2]['data'][$idxMonth]++;
+            } else {
+                $statusHtml = "<span style=\"color:#0c6197\">Presenza</span>";
+                $this->presenze++;
+                if ($idxMonth !== null) {
+                    $valori[$idxMonth]++;
+                    $chartData[0]['data'][$idxMonth]++;
+                }
+
+                $mot = ucfirst($p->motivation->name);
+                $this->recuperi[$mot] = ($this->recuperi[$mot] ?? 0) + 1;
+            }
+
+            $this->member_presences[] = [
+                'calendar_id' => $p->calendar->id,
+                'from' => $p->calendar->from,
+                'to'   => $p->calendar->to,
+                'status' => $statusHtml,
+                'motivation' => $p->motivation->name,
+            ];
+        }
+
+        usort($this->member_presences, fn($a, $b) => strcmp($a['from'], $b['from']));
+
+        $this->emit('load-chart', $mesi, $valori, $chartData);
+    }
+
+    private function courseMatchesSlot(?string $whenJson, string $day, string $hhmm): bool
+    {
+        if (!$whenJson) return false;
+
+        $when = json_decode($whenJson, true);
+        if (!is_array($when)) return false;
+
+        foreach ($when as $period) {
+            $days = $period['day'] ?? [];
+            $from = $period['from'] ?? null;
+
+            if (!$from || empty($days)) continue;
+
+            $from = substr((string)$from, 0, 5);
+
+            if ($from === $hhmm && in_array($day, $days, true)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     public function showDetailF($id)
     {
         if (!isset($_GET["from"]) && $this->from == '')

+ 139 - 0
app/Http/Livewire/Motivation.php

@@ -0,0 +1,139 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+use App\Http\Middleware\TenantMiddleware;
+
+class Motivation extends Component
+{
+    public $records, $name, $type, $enabled, $show_in_member_presences, $dataId, $update = false, $add = false;
+
+    protected $rules = [
+        'name' => 'required'
+    ];
+
+    protected $messages = [
+        'name.required' => 'Il nome è obbligatorio'
+    ];
+
+    public $sortField ='name';
+    public $sortAsc = true;
+
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+    }
+
+    public function mount(){
+
+        if(\Auth::user()->level != env('LEVEL_ADMIN', 0))
+            return redirect()->to('/dashboard');
+
+    }
+
+    public function sortBy($field)
+    {
+        if($this->sortField === $field)
+        {
+            $this->sortAsc = ! $this->sortAsc;
+        } else {
+            $this->sortAsc = true;
+        }
+
+        $this->sortField = $field;
+    }
+
+    public function resetFields(){
+        $this->name = '';
+        $this->type = '';
+        $this->enabled = true;
+        $this->emit('load-data-table');
+    }
+
+    public function render()
+    {
+        $this->records = \App\Models\Motivation::select('id', 'name', 'type', 'show_in_member_presences', 'enabled')->get();
+        return view('livewire.motivation');
+    }
+
+    public function add()
+    {
+        $this->resetFields();
+        $this->add = true;
+        $this->update = false;
+    }
+
+    public function store()
+    {
+        $this->validate();
+        try {
+            \App\Models\Motivation::create([
+                'name' => $this->name,
+                'type' => $this->type,
+                'enabled' => $this->enabled,
+                'show_in_member_presences' => $this->show_in_member_presences,
+            ]);
+            session()->flash('success','Campo creata');
+            $this->resetFields();
+            $this->add = false;
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function edit($id){
+        try {
+            $motivation = \App\Models\Motivation::findOrFail($id);
+            if( !$motivation) {
+                session()->flash('error','Motivazione non trovata');
+            } else {
+                $this->name = $motivation->name;
+                $this->type = $motivation->type;
+                $this->enabled = $motivation->enabled;
+                $this->show_in_member_presences = $motivation->show_in_member_presences;
+                $this->dataId = $motivation->id;
+                $this->update = true;
+                $this->add = false;
+            }
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function update()
+    {
+        $this->validate();
+        try {
+            \App\Models\Motivation::whereId($this->dataId)->update([
+                'name' => $this->name,
+                'type' => $this->type,
+                'enabled' => $this->enabled,
+                'show_in_member_presences' => $this->show_in_member_presences,
+            ]);
+            session()->flash('success','Motivazione aggiornata');
+            $this->resetFields();
+            $this->update = false;
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function cancel()
+    {
+        $this->add = false;
+        $this->update = false;
+        $this->resetFields();
+    }
+
+    public function delete($id)
+    {
+        try{
+            \App\Models\Motivation::find($id)->delete();
+            session()->flash('success',"Motivazione eliminata");
+            return redirect(request()->header('Referer'));
+        }catch(\Exception $e){
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+}

+ 79 - 74
app/Http/Livewire/Presence.php

@@ -3,6 +3,7 @@
 namespace App\Http\Livewire;
 
 use Livewire\Component;
+use App\Http\Middleware\TenantMiddleware;
 
 class Presence extends Component
 {
@@ -34,12 +35,10 @@ class Presence extends Component
     public $motivation_course_id = null;
     public $motivation_course_name = 0;
     public $motivation_course_level = 0;
-    public $motivation_course_type = 0;
     public $motivation_course_frequency = 0;
 
     public $course_names = [];
     public $course_levels = [];
-    public $course_types = [];
     public $course_frequencies = [];
 
     public $members = [];
@@ -48,6 +47,11 @@ class Presence extends Component
 
     public $ids = [];
 
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+    }
+
     public function mount()
     {
 
@@ -59,11 +63,11 @@ class Presence extends Component
         $this->motivation_manual_id = $this->calendar->motivation_manual_id;
         $this->manual = $this->calendar->manual;
         $this->members = \App\Models\Member::select(['id', 'first_name', 'last_name', 'fiscal_code'])
-            ->where(function($query) {
+            ->where(function ($query) {
                 $query->where('is_archived', false)
                     ->orWhereNull('is_archived');
             })
-            ->where(function($query) {
+            ->where(function ($query) {
                 $query->where('is_deleted', false)
                     ->orWhereNull('is_deleted');
             })->orderBy('last_name')->orderBy('first_name')->get();
@@ -81,15 +85,14 @@ class Presence extends Component
 
         $this->course_names = \App\Models\Course::whereDate('date_from', '<=', $this->course_limit)->whereDate('date_to', '>=', $this->course_limit)->where('active', true)->where('enabled', true)->orderBy('name')->groupBy('name')->pluck('name');
         $this->course_levels = [];
-        $this->course_types = [];
         $this->course_frequencies = [];
     }
 
-    function updatedMotivationCourseName() {
+    function updatedMotivationCourseName()
+    {
         if ($this->motivation_course_name > 0 && $this->motivation_course_name != '') {
             $this->motivation_course_id = null;
             $this->motivation_course_level = 0;
-            $this->motivation_course_type = 0;
             $this->motivation_course_frequency = 0;
 
             $levels_ids = [];
@@ -99,42 +102,21 @@ class Presence extends Component
             }
 
             $this->course_levels = \App\Models\CourseLevel::where('enabled', true)->whereIn('id', $levels_ids)->orderBy('name')->get();
-            $this->course_types = [];
             $this->course_frequencies = [];
         } else {
             $this->course_levels = [];
-            $this->course_types = [];
             $this->course_frequencies = [];
         }
     }
 
-    function updatedMotivationCourseLevel() {
+    function updatedMotivationCourseLevel()
+    {
         if ($this->motivation_course_name > 0 && $this->motivation_course_name != '' && $this->motivation_course_level > 0 && $this->motivation_course_level != '') {
-            $this->motivation_course_id = null;
-            $this->motivation_course_type = 0;
-            $this->motivation_course_frequency = 0;
-
-            $type_ids = [];
-            $types = \App\Models\Course::whereDate('date_from', '<=', $this->course_limit)->whereDate('date_to', '>=', $this->course_limit)->where('active', true)->where('enabled', true)->where('name', $this->motivation_course_name)->where('course_level_id', $this->motivation_course_level)->get();
-            foreach ($types as $t) {
-                $type_ids[] = $t->course_type_id;
-            }
-
-            $this->course_types = \App\Models\CourseType::where('enabled', true)->whereIn('id', $type_ids)->orderBy('name')->get();
-            $this->course_frequencies = [];
-        } else {
-            $this->course_types = [];
-            $this->course_frequencies = [];
-        }
-    }
-
-    function updatedMotivationCourseType() {
-        if ($this->motivation_course_name > 0 && $this->motivation_course_name != '' && $this->motivation_course_level > 0 && $this->motivation_course_level != '' && $this->motivation_course_type > 0 && $this->motivation_course_type != '') {
             $this->motivation_course_id = null;
             $this->motivation_course_frequency = 0;
 
             $frequency_ids = [];
-            $frequencies = \App\Models\Course::whereDate('date_from', '<=', $this->course_limit)->whereDate('date_to', '>=', $this->course_limit)->where('active', true)->where('enabled', true)->where('name', $this->motivation_course_name)->where('course_level_id', $this->motivation_course_level)->where('course_type_id', $this->motivation_course_type)->get();
+            $frequencies = \App\Models\Course::whereDate('date_from', '<=', $this->course_limit)->whereDate('date_to', '>=', $this->course_limit)->where('active', true)->where('enabled', true)->where('name', $this->motivation_course_name)->where('course_level_id', $this->motivation_course_level)->get();
             foreach ($frequencies as $f) {
                 $frequency_ids[] = $f->course_frequency_id;
             }
@@ -145,11 +127,12 @@ class Presence extends Component
         }
     }
 
-    function updatedMotivationCourseFrequency() {
-        if ($this->motivation_course_name > 0 && $this->motivation_course_name != '' && $this->motivation_course_level > 0 && $this->motivation_course_level != '' && $this->motivation_course_type > 0 && $this->motivation_course_type != '' && $this->motivation_course_frequency > 0 && $this->motivation_course_frequency != '') {
+    function updatedMotivationCourseFrequency()
+    {
+        if ($this->motivation_course_name > 0 && $this->motivation_course_name != '' && $this->motivation_course_level > 0 && $this->motivation_course_level != '' && $this->motivation_course_frequency > 0 && $this->motivation_course_frequency != '') {
             $this->motivation_course_id = null;
 
-            $course = \App\Models\Course::whereDate('date_from', '<=', $this->course_limit)->whereDate('date_to', '>=', $this->course_limit)->where('active', true)->where('enabled', true)->where('name', $this->motivation_course_name)->where('course_level_id', $this->motivation_course_level)->where('course_type_id', $this->motivation_course_type)->where('course_frequency_id', $this->motivation_course_frequency)->first();
+            $course = \App\Models\Course::whereDate('date_from', '<=', $this->course_limit)->whereDate('date_to', '>=', $this->course_limit)->where('active', true)->where('enabled', true)->where('name', $this->motivation_course_name)->where('course_level_id', $this->motivation_course_level)->where('course_frequency_id', $this->motivation_course_frequency)->first();
 
             $this->motivation_course_id = $course->id;
         } else {
@@ -181,50 +164,49 @@ class Presence extends Component
             $h = date('H:i', strtotime($this->calendar->from));
 
             // Elenco corsi per tipologia in base al calendario
-            $courses = \App\Models\Course::where('name', $this->calendar->name)->where('date_from', '<=', $this->calendar->from)->where('date_to', '>=', $this->calendar->to)->pluck('id')->toArray();
-
-            $months = date("n", strtotime($this->calendar->from));
-
-            // Elenco utenti iscritti al corso "padre"
-            // $members_courses = \App\Models\MemberCourse::where('when', 'like', "%" . $d . "%")
-            //     ->where('when', 'like', '%"from":"' . $h . '"%')
-            //     ->whereNot('months', 'like', '%"m":' . $months . ',"status":2%')
-            //     ->whereDate('date_from', '<=', $this->calendar->from)                                 
-            //     ->whereDate('date_to', '>=', $this->calendar->from)      
-            //     ->whereIn('course_id', $courses)
-            //     ->pluck('member_id')->toArray();
-
-            // $members_courses = \App\Models\MemberCourse::whereRaw("JSON_CONTAINS(`when`, JSON_OBJECT('day', JSON_ARRAY(?), 'from', ?), '$')", [$d, $h])->where('months', 'like', '%"m":' . $months . ',%')->whereIn('course_id', $courses)->pluck('member_id')->toArray();
-            $members_courses = \App\Models\MemberCourse::query()
-                        ->whereRaw("JSON_CONTAINS(`when`, JSON_OBJECT('day', JSON_ARRAY(?), 'from', ?), '$')", [$d, $h])
-                        ->whereRaw("JSON_CONTAINS(months, JSON_OBJECT('m', CAST(? AS UNSIGNED)), '$')", [$months])
-                        ->whereRaw("NOT JSON_CONTAINS(months, JSON_OBJECT('m', CAST(? AS UNSIGNED), 'status', 2), '$')", [$months])
-                        ->whereRaw("NOT JSON_CONTAINS(months, JSON_OBJECT('m', CAST(? AS UNSIGNED), 'status', '2'), '$')", [$months])
-                        ->whereIn('course_id', $courses)
-                        ->pluck('member_id')
-                        ->toArray();
+            $courses = \App\Models\Course::query()
+                ->where('name', $this->calendar->name)
+                ->where('date_from', '<=', $this->calendar->from)
+                ->where('date_to', '>=', $this->calendar->to)
+                ->get(['id', 'when']);
+
+            $slotCourseIds = $courses
+                ->filter(fn($c) => $this->courseMatchesSlot($c->when, $d, $h))
+                ->pluck('id')
+                ->all();
+
+            if (empty($slotCourseIds)) {
+                $members_courses = [];
+            } else {
+                $members_courses = \App\Models\MemberCourse::query()
+                    ->whereIn('course_id', $slotCourseIds)
+                    ->whereDate('date_from', '<=', $this->calendar->from)
+                    ->whereDate('date_to', '>=', $this->calendar->from)
+                    ->pluck('member_id')
+                    ->toArray();
+            }
 
             if ($this->filter != '') {
                 $filter = $this->filter;
                 $members = \App\Models\Member::whereIn('id', $members_courses)->where(function ($query) use ($filter) {
-                        $query->whereRaw("CONCAT(first_name, ' ', last_name) like '%" . $filter . "%'")
-                            ->orWhereRaw("CONCAT(last_name, ' ', first_name) like '%" . $filter . "%'");
-                    })
-                    ->where(function($query) {
+                    $query->whereRaw("CONCAT(first_name, ' ', last_name) like '%" . $filter . "%'")
+                        ->orWhereRaw("CONCAT(last_name, ' ', first_name) like '%" . $filter . "%'");
+                })
+                    ->where(function ($query) {
                         $query->where('is_archived', false)
                             ->orWhereNull('is_archived');
                     })
-                    ->where(function($query) {
+                    ->where(function ($query) {
                         $query->where('is_deleted', false)
                             ->orWhereNull('is_deleted');
                     })->orderBy('last_name')->orderBy('first_name')->get();
             } else
                 $members = \App\Models\Member::whereIn('id', $members_courses)
-                    ->where(function($query) {
+                    ->where(function ($query) {
                         $query->where('is_archived', false)
                             ->orWhereNull('is_archived');
                     })
-                    ->where(function($query) {
+                    ->where(function ($query) {
                         $query->where('is_deleted', false)
                             ->orWhereNull('is_deleted');
                     })->orderBy('last_name')->orderBy('first_name')->get();
@@ -245,24 +227,24 @@ class Presence extends Component
         if ($this->filter != '') {
             $filter = $this->filter;
             $members = \App\Models\Member::whereIn('id', $members_presences)->where(function ($query) use ($filter) {
-                    $query->whereRaw("CONCAT(first_name, ' ', last_name) like '%" . $filter . "%'")
-                        ->orWhereRaw("CONCAT(last_name, ' ', first_name) like '%" . $filter . "%'");
-                })
-                ->where(function($query) {
+                $query->whereRaw("CONCAT(first_name, ' ', last_name) like '%" . $filter . "%'")
+                    ->orWhereRaw("CONCAT(last_name, ' ', first_name) like '%" . $filter . "%'");
+            })
+                ->where(function ($query) {
                     $query->where('is_archived', false)
                         ->orWhereNull('is_archived');
                 })
-                ->where(function($query) {
+                ->where(function ($query) {
                     $query->where('is_deleted', false)
                         ->orWhereNull('is_deleted');
                 })->get();
         } else
             $members = \App\Models\Member::whereIn('id', $members_presences)
-                ->where(function($query) {
+                ->where(function ($query) {
                     $query->where('is_archived', false)
                         ->orWhereNull('is_archived');
                 })
-                ->where(function($query) {
+                ->where(function ($query) {
                     $query->where('is_deleted', false)
                         ->orWhereNull('is_deleted');
                 })->get();
@@ -286,6 +268,29 @@ class Presence extends Component
         return view('livewire.presence');
     }
 
+    private function courseMatchesSlot(?string $whenJson, string $day, string $hhmm): bool
+    {
+        if (!$whenJson) return false;
+
+        $when = json_decode($whenJson, true);
+        if (!is_array($when)) return false;
+
+        foreach ($when as $period) {
+            $days = $period['day'] ?? [];
+            $from = $period['from'] ?? null;
+
+            if (!$from || empty($days)) continue;
+
+            $from = substr((string)$from, 0, 5);
+
+            if ($from === $hhmm && in_array($day, $days, true)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     public function getDateX()
     {
         setlocale(LC_ALL, 'it_IT');
@@ -332,13 +337,13 @@ class Presence extends Component
                 $motivation = \App\Models\Motivation::findOrFail($has_presence->motivation_id)->name;
             }
             $status = $has_presence->status;
-            $instructor = \App\Models\User::findOrFail($has_presence->user_id)->name;
+            $instructor = \App\Models\User::find($has_presence->user_id)?->name;
 
             if ($has_presence->court_id > 0) {
                 $court = \App\Models\Court::findOrFail($has_presence->court_id)->name;
             }
             if ($has_presence->instructor_id > 0 && $has_presence->instructor_id !== $has_presence->user_id) {
-                $additional_instructor = \App\Models\User::findOrFail($has_presence->instructor_id)->name;
+                $additional_instructor = \App\Models\User::find($has_presence->instructor_id)?->name;
             }
             if (!is_null($has_presence->notes)) {
                 $notes = $has_presence->notes;
@@ -586,12 +591,12 @@ class Presence extends Component
         $this->resetCreationFields();
     }
 
-    public function resetCreationFields() {
+    public function resetCreationFields()
+    {
         $this->insertUser = 'new';
         $this->motivation_course_id = null;
         $this->motivation_course_name = null;
         $this->motivation_course_level = null;
-        $this->motivation_course_type = null;
         $this->motivation_course_frequency = null;
         $this->newMemberMotivationId = null;
         $this->newMemberFirstName = null;

+ 222 - 200
app/Http/Livewire/PresenceReport.php

@@ -3,36 +3,19 @@
 namespace App\Http\Livewire;
 
 use Livewire\Component;
+use Illuminate\Support\Carbon;
 use App\Http\Middleware\TenantMiddleware;
 
 class PresenceReport extends Component
 {
-
-    public $calendar;
-
-    public $records;
-
+    public $records = [];
     public $date;
 
-    public $member_ids = [];
-
-
     public $courses = [];
     public $courts = [];
     public $instructors = [];
     public $motivations = [];
 
-    public $court_filter;
-    public $instructor_filter;
-    public $motivation_filter;
-
-
-    public $members = [];
-
-    public $newMembers = [];
-
-    public $ids = [];
-
     public $course_name;
     public $from;
     public $to;
@@ -50,14 +33,14 @@ class PresenceReport extends Component
 
     public function mount()
     {
-        $this->courts = \App\Models\Court::select('*')->where('enabled', true)->get();
-        $this->instructors = \App\Models\User::select('*')->where('level', 2)->where('enabled', true)->orderBy('name', 'asc')->get();
-        $this->motivations = \App\Models\Motivation::select('*')->where('enabled', true)->where('type', 'del')->get();
+        $this->courts = \App\Models\Court::where('enabled', true)->orderBy('name')->get();
+        $this->instructors = \App\Models\User::where('level', 2)->where('enabled', true)->orderBy('name')->get();
+        $this->motivations = \App\Models\Motivation::where('enabled', true)->where('type', 'del')->orderBy('name')->get();
 
         $this->from = "00:00:00";
         $this->to = "23:59:59";
-
         $this->date = date("Y-m-d");
+
         setlocale(LC_ALL, 'it_IT');
     }
 
@@ -66,222 +49,261 @@ class PresenceReport extends Component
         setlocale(LC_ALL, 'it_IT');
 
         $this->records = [];
-        $this->courses = [];
 
-        $from = $this->date . " " . $this->from;
-        $to = $this->date . " " . $this->to;
+        $fromDt = Carbon::parse($this->date . ' ' . ($this->from ?: '00:00:00'));
+        $toDt = Carbon::parse($this->date . ' ' . ($this->to ?: '23:59:59'));
+
+        $this->court_name = '';
+        if (!empty($this->court_id)) {
+            $court = $this->courts->firstWhere('id', (int)$this->court_id);
+            $this->court_name = $court?->name ?? '';
+        }
+
+        $this->instructor_name = '';
+        if (!empty($this->instructor_id)) {
+            $instr = $this->instructors->firstWhere('id', (int)$this->instructor_id);
+            $this->instructor_name = $instr?->name ?? '';
+        }
+
+        $calendars = \App\Models\Calendar::with(['course.level'])
+            ->whereBetween('from', [$fromDt->toDateTimeString(), $toDt->toDateTimeString()])
+            ->orderBy('from')
+            ->get();
+
+        if ($calendars->isEmpty()) {
+            $this->courses = \App\Models\Calendar::orderBy('name')->groupBy('name')->pluck('name')->toArray();
+            return view('livewire.presence_report');
+        }
+
+        $calendarIds = $calendars->pluck('id')->all();
+
+        $presencesAll = \App\Models\Presence::with([
+            'member:id,first_name,last_name',
+            'court:id,name',
+            'user:id,name',
+            'instructor:id,name',
+            'motivation:id,name',
+            'motivationCourse:id,name,course_level_id',
+            'motivationCourse.level:id,name',
+        ])
+            ->whereIn('calendar_id', $calendarIds)
+            ->when(!empty($this->court_id), fn($q) => $q->where('court_id', (int)$this->court_id))
+            ->when(!empty($this->instructor_id), function ($q) {
+                $iid = (int)$this->instructor_id;
+                $q->where(function ($qq) use ($iid) {
+                    $qq->where('instructor_id', $iid)->orWhere('user_id', $iid);
+                });
+            })
+            ->when(!empty($this->search), function ($q) {
+                $s = trim($this->search);
+                $q->whereHas('member', function ($mq) use ($s) {
+                    $mq->where(function ($qq) use ($s) {
+                        $qq->whereRaw("CONCAT(TRIM(first_name), ' ', TRIM(last_name)) LIKE ?", ["%{$s}%"])
+                            ->orWhereRaw("CONCAT(TRIM(last_name), ' ', TRIM(first_name)) LIKE ?", ["%{$s}%"]);
+                    });
+                });
+            })
+            ->get();
 
-        $calendars = \App\Models\Calendar::where('from', '>=', $from)->where('from', '<=', $to)->orderBy('from')->get();
+        $presenceByCalMember = [];
+        $presencesByCalendar = [];
 
-        if (!is_null($this->court_id) && $this->court_id > 0)
-            $this->court_name = \App\Models\Court::findOrFail($this->court_id)->name;
-        else
-            $this->court_name = '';
+        foreach ($presencesAll as $p) {
+            $presenceByCalMember[$p->calendar_id . '|' . $p->member_id] = $p;
+            $presencesByCalendar[$p->calendar_id][] = $p;
+        }
 
-        if (!is_null($this->instructor_id) && $this->instructor_id > 0)
-            $this->instructor_name = \App\Models\User::findOrFail($this->instructor_id)->name;
-        else
-            $this->instructor_name = '';
+        $days = ['dom', 'lun', 'mar', 'mer', 'gio', 'ven', 'sab'];
 
         foreach ($calendars as $calendar) {
+            $h = Carbon::parse($calendar->from)->format('H:i');
+            $dow = (int) Carbon::parse($calendar->from)->format('w'); // 0..6
+            $d = $days[$dow];
 
-            $presences = \App\Models\Presence::where('calendar_id', $calendar->id)->where('status', '<>', 99);
-            $presences_annullate = \App\Models\Presence::where('calendar_id', $calendar->id)->where('status', 99);
-            
-            // filtra per campo court_id
-            if (!is_null($this->court_id) && $this->court_id > 0) {
-                $presences->where('court_id', $this->court_id);
-                $presences_annullate->where('court_id', $this->court_id);
+            $courseIds = \App\Models\Course::query()
+                ->where('name', $calendar->name)
+                ->where('date_from', '<=', $calendar->from)
+                ->where('date_to', '>=', $calendar->to)
+                ->when(!empty($this->course_name), fn($q) => $q->where('name', $this->course_name))
+                ->pluck('id')
+                ->toArray();
+
+            if (empty($courseIds)) {
+                continue;
             }
 
-            // filtra per campo istructor_id/user_id
-            if (!is_null($this->instructor_id) && $this->instructor_id > 0) {
-                $presences->where(function ($query) {
-                    $query->where('instructor_id', $this->instructor_id)
-                        ->orWhere('user_id', $this->instructor_id);
-                });
-                $presences_annullate->where(function ($query) {
-                    $query->where('instructor_id', $this->instructor_id)
-                        ->orWhere('user_id', $this->instructor_id);
-                });
+            $slotCourseIds = \App\Models\Course::query()
+                ->whereIn('id', $courseIds)
+                ->get(['id', 'when'])
+                ->filter(fn($c) => $this->courseMatchesSlot($c->when, $d, $h))
+                ->pluck('id')
+                ->all();
+
+            if (empty($slotCourseIds)) {
+                continue;
             }
 
-            // filtra per campo search (nome/cognome)
-            if (!is_null($this->search) && $this->search != "") {
-                $search_value = $this->search;
-                $presences->whereHas('member', function ($q) use ($search_value) {
-                    $q->where(function ($qq) use ($search_value) {
-                        $qq->whereRaw("CONCAT(TRIM(first_name), ' ', TRIM(last_name)) LIKE ?", ["%{$search_value}%"])
-                        ->orWhereRaw("CONCAT(TRIM(last_name), ' ', TRIM(first_name)) LIKE ?", ["%{$search_value}%"]);
-                    });
-                });
-                $presences_annullate->whereHas('member', function ($q) use ($search_value) {
-                    $q->where(function ($qq) use ($search_value) {
-                        $qq->whereRaw("CONCAT(TRIM(first_name), ' ', TRIM(last_name)) LIKE ?", ["%{$search_value}%"])
-                        ->orWhereRaw("CONCAT(TRIM(last_name), ' ', TRIM(first_name)) LIKE ?", ["%{$search_value}%"]);
+            $membersQuery = \App\Models\MemberCourse::query()
+                ->whereIn('course_id', $slotCourseIds)
+                ->whereDate('date_from', '<=', $calendar->from)
+                ->whereDate('date_to', '>=', $calendar->from)
+                ->with(['member', 'course.level']);
+
+            if (!empty($this->search)) {
+                $s = trim(mb_strtolower($this->search));
+                $membersQuery->whereHas('member', function ($mq) use ($s) {
+                    $mq->where(function ($qq) use ($s) {
+                        $qq->whereRaw('LOWER(CONCAT(TRIM(first_name), " ", TRIM(last_name))) LIKE ?', ["%{$s}%"])
+                            ->orWhereRaw('LOWER(CONCAT(TRIM(last_name), " ", TRIM(first_name))) LIKE ?', ["%{$s}%"]);
                     });
                 });
             }
 
-            $presences = $presences->pluck('member_id')->toArray();
-            $presences_annullate = $presences_annullate->pluck('member_id')->toArray();
+            $members = $membersQuery->get();
 
-            $days = ['dom', 'lun', 'mar', 'mer', 'gio', 'ven', 'sab'];
-            $dow = date('w', strtotime($calendar->from));
-            $d = $days[$dow];
+            $expectedMemberIds = [];
+
+            foreach ($members as $mc) {
+                $mid = $mc->member->id;
+                $expectedMemberIds[] = $mid;
+
+                $p = $presenceByCalMember[$calendar->id . '|' . $mid] ?? null;
+
+                [$court, $instructor, $motivation, $motivation_course] = $this->presenceMeta($p);
+                $status = $this->presenceStatusHtml($p, $calendar);
 
-            $h = date('H:i', strtotime($calendar->from));
+                $course_level = '';
+                if ($mc->course && $mc->course->level) {
+                    $course_level = trim($mc->course->level->name);
+                }
 
-            // Elenco corsi per tipologia in base al calendario
-            $courses = \App\Models\Course::where('name', $calendar->name)->where('date_from', '<=', $calendar->from)->where('date_to', '>=', $calendar->to);
-            if (!is_null($this->course_name)) {
-                $courses = $courses->where('name', $this->course_name);
+                $this->records[$calendar->name][$h][] = [
+                    'course_level' => $course_level,
+                    'last_name' => $mc->member->last_name,
+                    'first_name' => $mc->member->first_name,
+                    'court' => $court,
+                    'instructor' => $instructor,
+                    'status' => $status,
+                    'motivation' => $motivation,
+                ];
             }
-            $courses = $courses->pluck('id')->toArray();
-
-            $mids = [];
-
-            $months = date("n", strtotime($calendar->from));
-
-            // Elenco utenti iscritti al corso "padre"
-            $members = \App\Models\MemberCourse::where('when', 'like', "%" . $d . "%")
-                ->where('when', 'like', '%"from":"' . $h . '"%')
-                ->whereDate('date_from', '<=', $calendar->from)                                 
-                ->whereDate('date_to', '>=', $calendar->from)      
-                ->whereNot('months', 'like', '%"m":' . $months . ',"status":2%')
-                ->whereIn('course_id', $courses)->get();
-
-            //$members = \App\Models\MemberCourse::where('when', 'like', "%" . $d . "%")->where('when', 'like', '%"from":"' . $h . '"%')->whereIn('member_id', $presences)->whereIn('course_id', $courses)->get();
-            foreach ($members as $member) {
-
-                $court = '';
-                $instructor = '';
-                $motivation = '';
-
-                $presence = \App\Models\Presence::where('member_id', $member->member->id)->where('calendar_id', $calendar->id)->first();
-                if ($presence) {
-                    $court = $presence->court ? $presence->court->name : "";
-                    $instructor = [
-                        $presence->user ? $presence->user->name : "",
-                        $presence->instuctor && $presence->instructor !== $presence->user ? $presence->instuctor->name : "",
-                    ];
-                    $instructor = implode(", ", array_filter($instructor));
-                    $motivation = $presence->motivation ? $presence->motivation->name : "";
+
+            $extras = $presencesByCalendar[$calendar->id] ?? [];
+
+            foreach ($extras as $p) {
+                if (in_array($p->member_id, $expectedMemberIds, true)) {
+                    continue;
                 }
 
-                $status = '';
-                if (in_array($member->member->id, $presences)) {
-                    $status = "<span class='fw-bold' style='color:#0c6197'>Presente</span>";
-                } else {
-                    if (in_array($member->member->id, $presences_annullate)) {
-                        $status = "<span class='fw-bold' style='color:gray'>Annullata</span>";
-                    } else {
-                        if (date("Ymd") > date("Ymd", strtotime($calendar->from))) {
-                            $status = "<span class='fw-bold' style='color:red'>Assente</span>";
-                        }
+                if (!empty($this->course_name)) {
+                    $motCourseName = $p->motivationCourse?->name;
+                    if (!$motCourseName || $motCourseName !== $this->course_name) {
+                        continue;
                     }
                 }
 
-                if ($calendar->status == 99) {
-                    $status = "<span class='fw-bold' style='color:gray'>Annullata</span>";
-                }
+                [$court, $instructor, $motivation, $motivation_course] = $this->presenceMeta($p);
+                $status = $this->presenceStatusHtml($p, $calendar);
 
-                $show = true;
-                if ($this->court_name != '')
-                    $show = $this->court_name == $court;
-                if ($show && $this->instructor_name != '')
-                    $show = $this->instructor_name == $instructor;
-
-                if ($show)
-                {
-                    $this->records[$calendar->name][$h][] = array(
-                        "last_name" => $member->member->last_name,
-                        "first_name" => $member->member->first_name,
-                        "court" => $court,
-                        "instructor" => $instructor,
-                        "status" => $status,
-                        'motivation' => $motivation
-                    );
-
-                    $mids[] = $member->member->id;
+                $course_level = '';
+                if ($calendar->course && $calendar->course->level) {
+                    $course_level = trim($calendar->course->level->name);
+                }
+                if ($motivation_course) {
+                    $course_level = $motivation_course->level?->name;
                 }
-            }
 
-            $presences_recuperi = \App\Models\Presence::where('calendar_id', $calendar->id)->whereNotIn('member_id', $mids);
-            if (!is_null($this->court_id) && $this->court_id > 0) {
-                $presences_recuperi->where('court_id', $this->court_id);
-            }
-            if (!is_null($this->instructor_id) && $this->instructor_id > 0) {
-                $presences_recuperi->where(function ($query) {
-                    $query->where('instructor_id', $this->instructor_id)
-                        ->orWhere('user_id', $this->instructor_id);
-                });
-            }
-            if (!is_null($this->search) && $this->search != "") {
-                $search_value = $this->search;
-                $presences_recuperi->whereHas('member', function ($q) use ($search_value) {
-                    $q->where(function ($qq) use ($search_value) {
-                        $qq->whereRaw("CONCAT(TRIM(first_name), ' ', TRIM(last_name)) LIKE ?", ["%{$search_value}%"])
-                        ->orWhereRaw("CONCAT(TRIM(last_name), ' ', TRIM(first_name)) LIKE ?", ["%{$search_value}%"]);
-                    });
-                });
-            }
-            $presences_recuperi = $presences_recuperi->get();
-            foreach ($presences_recuperi as $p) {
-                $court = $p->court ? $p->court->name : "";
-                $instructor = [
-                    $p->user ? $p->user->name : "",
-                    $p->instuctor && $p->instructor !== $p->user ? $p->instuctor->name : "",
+                $this->records[$calendar->name][$h][] = [
+                    'course_level' => $course_level,
+                    'last_name' => $p->member?->last_name ?? '',
+                    'first_name' => $p->member?->first_name ?? '',
+                    'court' => $court,
+                    'instructor' => $instructor,
+                    'status' => $status,
+                    'motivation' => $motivation,
                 ];
-                $instructor = implode(", ", array_filter($instructor));
-                $motivation = $p->motivation ? $p->motivation->name : "";
-                $status = "<span class='fw-bold' style='color:gray'>Recupero</span>";
-                $this->records[$calendar->name][$h][] = array(
-                    "last_name" => $p->member->last_name,
-                    "first_name" => $p->member->first_name,
-                    "court" => $court,
-                    "instructor" => $instructor,
-                    "status" => $status,
-                    'motivation' => $motivation
-                );
             }
 
-            /*
-            $calendar_recuperi = \App\Models\Calendar::where('manual', 1)->where('id', $calendar->id)->pluck('id')->toArray();
-            $presences_recuperi = \App\Models\Presence::whereIn('calendar_id', $calendar_recuperi)->where('member_id', $this->dataId)->get();
-            foreach($presences_recuperi as $p)
-            {
-                $this->member_presences[] = array('calendar_id' => $p->calendar->id, 'from' => $p->calendar->from, 'to' => $p->calendar->to, 'status' => '<span style="color:#7136f6">Recupero</span>');//\App\Models\Presence::where('member_id', $this->dataId)->get();
-                $this->recuperi += 1;
-            } 
-                */
-
-            //array_push($this->courses, $calendar->course_id);
-
-            // sort records per cognome-nome
-            if (isset($this->records[$calendar->name]) && isset($this->records[$calendar->name][$h])) {
-                usort($this->records[$calendar->name][$h], function($a, $b) {
+            if (isset($this->records[$calendar->name][$h])) {
+                usort($this->records[$calendar->name][$h], function ($a, $b) {
+                    $course_level_compare = strcmp($a['course_level'], $b['course_level']);
+                    if ($course_level_compare !== 0) return $course_level_compare;
+
                     $last_name_compare = strcmp($a['last_name'], $b['last_name']);
-                    $first_name_compare = strcmp($a['first_name'], $b['first_name']);
-    
-                    return $last_name_compare != 0 ? $last_name_compare : $first_name_compare;
+                    if ($last_name_compare !== 0) return $last_name_compare;
+
+                    return strcmp($a['first_name'], $b['first_name']);
                 });
             }
         }
 
         $this->courses = \App\Models\Calendar::orderBy('name')->groupBy('name')->pluck('name')->toArray();
 
-        /*$this->courses = array_unique($this->courses);
-        $this->courses = array_map(function ($course_id) {
-            try {
-                return \App\Models\Course::findOrFail($course_id);
-            } catch (\Throwable $e) {
-                return null;
+        return view('livewire.presence_report');
+    }
+
+    private function courseMatchesSlot(?string $whenJson, string $day, string $hhmm): bool
+    {
+        if (!$whenJson) return false;
+
+        $when = json_decode($whenJson, true);
+        if (!is_array($when)) return false;
+
+        foreach ($when as $period) {
+            $days = $period['day'] ?? [];
+            $from = $period['from'] ?? null;
+
+            if (!$from || empty($days)) continue;
+
+            $from = substr((string)$from, 0, 5);
+
+            if ($from === $hhmm && in_array($day, $days, true)) {
+                return true;
             }
-        }, $this->courses);
-        $this->courses = array_filter($this->courses);*/
+        }
 
-        return view('livewire.presence_report');
+        return false;
+    }
+
+    protected function presenceStatusHtml($presence, $calendar): string
+    {
+        if ($calendar->status == 99) {
+            return "<span class='fw-bold' style='color:gray'>Annullata</span>";
+        }
+
+        if ($presence) {
+            if ((int)$presence->status === 99) {
+                return "<span class='fw-bold' style='color:gray'>Annullata</span>";
+            }
+            return "<span class='fw-bold' style='color:#0c6197'>Presente</span>";
+        }
+
+        if (Carbon::now()->format('Ymd') > Carbon::parse($calendar->from)->format('Ymd')) {
+            return "<span class='fw-bold' style='color:red'>Assente</span>";
+        }
+
+        return '';
+    }
+
+    protected function presenceMeta($presence): array
+    {
+        if (!$presence) return ['', '', '', null];
+
+        $court = $presence->court?->name ?? '';
+
+        $instructorParts = [
+            $presence->user?->name ?? '',
+            ($presence->instructor && $presence->user && $presence->instructor->id !== $presence->user->id)
+                ? $presence->instructor->name
+                : ($presence->instructor?->name ?? ''),
+        ];
+        $instructor = implode(', ', array_values(array_filter(array_unique($instructorParts))));
+
+        $motivation = $presence->motivation?->name ?? '';
+
+        $motivation_course = $presence->motivationCourse ?? null;
+
+        return [$court, $instructor, $motivation, $motivation_course];
     }
 
     public function prev()

+ 2 - 1
app/Models/Motivation.php

@@ -12,6 +12,7 @@ class Motivation extends Model
     protected $fillable = [
         'name',
         'enabled',
-        'type'
+        'type',
+        'show_in_member_presences'
     ];
 }

+ 60 - 0
database/migrations/2025_12_17_121609_remove_user_id_foreign_from_presences.php

@@ -0,0 +1,60 @@
+<?php
+
+use App\Database\Migrations\TenantMigration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends TenantMigration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        $table = DB::getTablePrefix() . 'presences';
+
+        // 1) FK su user_id (se esiste)
+        $fkName = DB::selectOne("
+            SELECT CONSTRAINT_NAME AS name
+            FROM information_schema.KEY_COLUMN_USAGE
+            WHERE TABLE_SCHEMA = DATABASE()
+                AND TABLE_NAME = ?
+                AND COLUMN_NAME = 'user_id'
+                AND REFERENCED_TABLE_NAME IS NOT NULL
+            LIMIT 1
+        ", [$table])?->name ?? null;
+
+        if ($fkName) {
+            DB::statement("ALTER TABLE `$table` DROP FOREIGN KEY `$fkName`");
+        }
+
+        // 2) Indice chiamato 'presences_user_id_foreign' (se esiste)
+        $indexName = DB::selectOne("
+            SELECT INDEX_NAME AS name
+            FROM information_schema.STATISTICS
+            WHERE TABLE_SCHEMA = DATABASE()
+                AND TABLE_NAME = ?
+                AND INDEX_NAME = 'presences_user_id_foreign'
+            LIMIT 1
+        ", [$table])?->name ?? null;
+
+        if ($indexName) {
+            DB::statement("ALTER TABLE `$table` DROP INDEX `presences_user_id_foreign`");
+        }
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('presences', function (Blueprint $table) {
+            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
+        });
+    }
+};

+ 32 - 0
database/migrations/2025_12_17_125513_add_show_in_member_presences_to_motivations.php

@@ -0,0 +1,32 @@
+<?php
+
+use App\Database\Migrations\TenantMigration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends TenantMigration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('motivations', function (Blueprint $table) {
+            $table->integer('show_in_member_presences')->default(1);
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('motivations', function (Blueprint $table) {
+            $table->dropColumn('show_in_member_presences');
+        });
+    }
+};

+ 50 - 4
public/css/new_style.css

@@ -1114,7 +1114,10 @@ body div.dt-processing {
     z-index: 1000;
 }
 
-body .select2-container .select2-selection--single .select2-selection__rendered {
+body
+    .select2-container
+    .select2-selection--single
+    .select2-selection__rendered {
     display: flex;
     flex-wrap: wrap;
     align-content: center;
@@ -1122,7 +1125,8 @@ body .select2-container .select2-selection--single .select2-selection__rendered
     padding-top: 9px;
 }
 
-body .form-select, body form .form-select {
+body .form-select,
+body form .form-select {
     /* padding-top: 11px; */
     line-height: 1;
 }
@@ -1149,7 +1153,6 @@ a.member-file-url {
     align-items: center;
 }
 
-
 .dt-buttons.btn-group .btn span i:before {
     color: var(--color-text);
 }
@@ -1286,4 +1289,47 @@ a.member-file-url {
     border: transparent;
     font-weight: bold;
 }
-/* END - Calendar styles */
+/* END - Calendar styles */
+
+.box-presenze {
+    padding: 10px;
+    border: 2px solid var(--color-blu);
+    margin: 10px;
+    border-radius: 10px;
+    text-align: center;
+    color: var(--color-blu);
+}
+.box-assenze {
+    padding: 10px;
+    border: 2px solid red;
+    margin: 10px;
+    border-radius: 10px;
+    text-align: center;
+    color: red;
+}
+.box-recupero {
+    padding: 10px;
+    border: 2px solid var(--color-viola);
+    margin: 10px;
+    border-radius: 10px;
+    text-align: center;
+    color: var(--color-viola);
+}
+.box-annullate {
+    padding: 10px;
+    border: 2px solid gray;
+    margin: 10px;
+    border-radius: 10px;
+    text-align: center;
+    color: gray;
+}
+
+.presenzechart-wrapper {
+    position: relative;
+}
+
+button.download-png {
+    position: absolute;
+    top: 0;
+    right: 0;
+}

+ 46 - 0
public/css/presence_report.css

@@ -0,0 +1,46 @@
+.report-table {
+    width: 100%;
+    border-spacing: 0;
+    border-collapse: separate;
+    border-radius: 8px;
+    overflow: hidden;
+    border: 1px solid #8d9aa4;
+}
+
+.report-table thead {
+    width: 100%;
+    background-color: #f6f8fa;
+}
+
+.report-table tbody {
+    width: 100%;
+}
+
+.report-table thead td {
+    padding: 15px 35px;
+    text-transform: uppercase;
+    font-weight: bold;
+}
+
+.report-table tbody td {
+    padding: 15px 35px;
+    text-transform: uppercase;
+    font-size: 14px;
+    border-bottom: 1px solid #c1c9cf;
+}
+
+.report-table tbody > tr:last-of-type td {
+    border-bottom: none;
+}
+
+.datepicker--btn {
+    position: relative;
+}
+
+.datepicker--btn > input {
+    position: absolute;
+    opacity: 0;
+    left: 0;
+    bottom: 0;
+    pointer-events: none;
+}

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

@@ -189,7 +189,7 @@
             if (Request::is('presence_reports'))
                 print "Report presenze";                
             if (Request::is('calendar'))
-                print "Calendario";                
+                print "Calendario";
             if (Request::is('reminders'))
                 print "Scadenze";
             if (Request::is('settings'))
@@ -215,7 +215,7 @@
             // if (Request::is('course_types'))
             //     print "Corsi - Tipologie";
             if (Request::is('banks'))
-                print "Banche";
+                print "Canali finanziari";
             if (Request::is('causals'))
                 print "Causali";
             if (Request::is('vats'))
@@ -227,7 +227,7 @@
             if (Request::is('azienda'))
                 print "Società";
             if (Request::is('profile'))
-                print "Profilo Utente";
+                print "Profilo utenti";
             if (Request::is('reports'))
                 print "Reports";
             if (Request::is('subscriptions'))
@@ -238,6 +238,10 @@
                 print "Email";
             if (Request::is('sms_comunications'))
                 print "Sms";
+            if (Request::is('presence_reports'))
+                print "Report presenze";
+            if (Request::is('absence_reports'))
+                print "Assenze";
             @endphp
             </h3>
 
@@ -411,12 +415,12 @@
                     </div>
                     <div class="accordion-item">
                         <h2 class="accordion-header linkMenu" id="headingThree">
-                            <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseFour" aria-expanded="{{Request::is('calendar') || Request::is('presence_reports') ? 'true' : 'false'}}" aria-controls="collapseFour">
+                            <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseFour" aria-expanded="{{Request::is('calendar') || Request::is('presence_reports') || Request::is('absence_reports') ? 'true' : 'false'}}" aria-controls="collapseFour">
                                 <i class="fas fa-calendar"></i>
                                 <span>Presenze</span>
                             </button>
                         </h2>
-                        <div id="collapseFour" class="accordion-collapse collapse {{Request::is('calendar') || Request::is('presence_reports') ? 'show' : ''}}" aria-labelledby="headingThree" data-bs-parent="#accordionExample">
+                        <div id="collapseFour" class="accordion-collapse collapse {{Request::is('calendar') || Request::is('presence_reports') || Request::is('absence_reports') ? 'show' : ''}}" aria-labelledby="headingThree" 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 {{Request::is('calendar') ? "nav-item-active" : ""}}">
@@ -424,7 +428,7 @@
                                             <span class="ms-3 d-md-inline">Calendario</span>
                                         </a>
                                     </li>
-                                    <li class="nav-item {{Request::is('presence_reports') ? "nav-item-active" : ""}}">
+                                    <li class="nav-item {{Request::is('presence_reports') || Request::is('absence_reports') ? "nav-item-active" : ""}}">
                                         <a href="/presence_reports" class="nav-link d-flex align-items-center linkMenu">
                                             <span class="ms-3 d-md-inline">Report</span>
                                         </a>

+ 61 - 4
resources/views/livewire/absence_report.blade.php

@@ -1,6 +1,6 @@
 <div class="col card--ui" id="card--dashboard">
 
-    <a class="btn--ui lightGrey" href="/presence_reports"><i class="fa-solid fa-arrow-left"></i></a><br>
+    <a class="btn--ui lightGrey" href="/presence_reports"><i class="fa-solid fa-arrow-left"></i></a>
 
     <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">
@@ -8,10 +8,67 @@
             <h2 class="primary">Assenze</h2>
         </div>
     </header>
+    
+    <div class="row mb-2">
+        <div class="col">
+            <div class="alert alert-warning text-center" role="alert">Attenzione: se un utente viene aggiunto manualmente a un corso diverso da quello a cui è associato, l'utente continuerà a risultare assente nel corso a cui è regolarmente iscritto.</div>
+        </div>
+    </div>
+    
+    <div class="row mb-2">
+        <div class="col">
+            <div class="alert alert-info text-center" role="alert">I dati sono aggiornati al giorno precedente della data odierna.</div>
+        </div>
+    </div>
 
-    <br>
+    <div class="row mb-3">
+        <div class="col-8"></div>
+        <div class="col text-end">
+            <div class="input-group">
+                <input type="text" class="form-control" placeholder="Cerca utente" aria-label="Cerca utente" wire:model.defer="search">
+            </div>
+        </div>
+        <div class="col-auto text-end">
+            <button class="btn--ui" type="button" wire:click="resetSearch()">Reset</button>
+            &nbsp;
+            <button class="btn--ui" type="button" wire:click="applySearch()">Cerca</button>
+        </div>
+    </div>
 
-    <div class="row">
+    @foreach ($record_assenze as $record_data)
+        <div class="row mb-5">
+            <div class="col-12 mb-2">
+                <h3 class="primary">{{$record_data['course']['name']}}</h3>
+            </div>
+            <div class="col-12">
+                <table class="report-table">
+                    <thead>
+                        <tr>
+                            <td style="width: 20%">Cognome</td>
+                            <td style="width: 20%">Nome</td>
+                            <td style="width: 10%">N. assenze</td>
+                            <td>Date</td>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        @foreach($record_data['members'] as $member)
+                        <tr @if($loop->index % 2 == 1)style="background-color: rgba(12 97 151 / 0.05);"@endif>
+                            <td>{{$member['member']['last_name']}}</td>
+                            <td>{{$member['member']['first_name']}}</td>
+                            <td>{{$member["count"]}}</td>
+                            <td>
+                                @foreach ($member["dates"] as $calendar_date)
+                                    <a href="/presences?calendarId={{$calendar_date['calendar_id']}}" target="_blank">{{$calendar_date['date']}}</a>@if (!$loop->last) - @endif
+                                @endforeach
+                            </td>
+                        </tr>
+                        @endforeach
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    @endforeach
+    {{-- <div class="row">
         <div class="col-12 mb-3">
             <h3 class="primary">Assenze {{$year}}/{{$year+1}}</h3>
         </div>
@@ -39,7 +96,7 @@
                 </tbody>
             </table>
         </div>
-    </div>
+    </div> --}}
 </div>
 
 @push('css')

+ 176 - 1
resources/views/livewire/member.blade.php

@@ -519,6 +519,7 @@
                                 <h4 style="cursor:pointer;{{$type == 'dati' ? 'border-bottom:2px solid var(--color-blu); color:var(--color-blu);' : ''}}" wire:click="change('dati')">Anagrafica</h4>
                                 <h4 style="cursor:pointer;{{$type == 'tesseramento' ? 'border-bottom:2px solid var(--color-blu); color:var(--color-blu);' : ''}}" wire:click="change('tesseramento')">Tesseramento</h4>
                                 <h4 style="cursor:pointer;{{$type == 'corsi' ? 'border-bottom:2px solid var(--color-blu); color:var(--color-blu);' : ''}}" wire:click="change('corsi')">Corsi e Abbonamenti</h4>
+                                <h4 style="cursor:pointer;{{$type == 'presenze' ? 'border-bottom:2px solid var(--color-blu); color:var(--color-blu);' : ''}}" wire:click="change('presenze')">Presenze</h4>
                                 <h4 style="cursor:pointer;{{$type == 'gruppi' ? 'border-bottom:2px solid var(--color-blu); color:var(--color-blu);' : ''}}" wire:click="change('gruppi')">Gruppi</h4>
                             </div>
                             <div class="row">
@@ -1060,6 +1061,103 @@
                                         @endif
                                     @endif
                                 @endif
+
+                                @if($type == 'presenze')
+
+                                    <div class="form--wrapper">
+                                        <div class="row">
+                                            <div class="col-md-6">
+                                                <select class="form-control" wire:model="presenceYearFilter">
+                                                    <option value="">-- Stagione --</option>
+                                                    @foreach($presenceYears as $y)
+                                                        <option value="{{$y}}">{{$y}}</option>
+                                                    @endforeach
+                                                </select>
+                                            </div>
+
+                                            <div class="col-md-6">
+                                                <select class="form-control" wire:model="presenceCourseFilter" @if(empty($presenceCourses)) disabled @endif>
+                                                    <option value="">-- Corso --</option>
+                                                    @foreach($presenceCourses as $c)
+                                                        <option value="{{$c->id}}">
+                                                            {{$c->getDetailsName()}}
+                                                        </option>
+                                                    @endforeach
+                                                </select>
+                                            </div>
+                                        </div>
+
+                                        <br>
+
+                                        <div class="row align-items-center">
+                                            <div class="col">
+                                                <div class="box-presenze">
+                                                    <span><b>Presenze</b>: {{ $presenze }}</span>
+                                                </div>
+                                            </div>
+                                            <div class="col">
+                                                <div class="box-assenze">
+                                                    <span><b>Assenze</b>: {{ $assenze }}</span>
+                                                </div>
+                                            </div>
+                                            <div class="col"@if (!empty($recuperi)) style="border-right: 1px solid gray"@endif>
+                                                <div class="box-annullate">
+                                                    <span><b>Annullate</b>: {{ $annullate }}</span>
+                                                </div>
+                                            </div>
+                                            @if (!empty($recuperi))
+                                                <div class="col">
+                                                    <div class="box-presenze text-start">
+                                                        @foreach ($recuperi as $name => $count)
+                                                            <span><b>{!! $name !!}</b>: {{ $count }}</span><br>
+                                                        @endforeach
+                                                    </div>
+                                                </div>
+                                            @endif
+                                        </div>
+
+                                        <div class="row">
+                                            <div class="col-md-12">
+                                                <div class="presenzechart-wrapper">
+                                                    <canvas id="presenzeChart" style="padding:20px"></canvas>
+                                                    <button class="btn--ui download-png" onclick="downloadChart('presenzeChart')" data-bs-toggle="popover"
+                                                        data-bs-trigger="hover focus" data-bs-placement="bottom" data-bs-content="Scarica grafico">
+                                                        <i class="fas fa-download"></i>
+                                                    </button>
+                                                </div>
+                                            </div>
+                                        </div>
+
+                                        @if (!empty($member_presences))
+                                            <div class="row" style="height: 300px; overflow:auto;">
+                                                <div class="col-md-12">
+                                                    <table class="table tablesaw tableHead tablesaw-stack tabella--presenze" style="min-width:700px">
+                                                        <thead>
+                                                            <tr>
+                                                                <th style="width: 20%">Data</th>
+                                                                <th style="width: 20%">Orario</th>
+                                                                <th style="width: 15%">Stato</th>
+                                                                <th style="width: 45%">Motivazione</th>
+                                                            </tr>
+                                                        </thead>
+                                                        <tbody>
+                                                            @foreach($member_presences as $mp)
+                                                                <tr>
+                                                                    <td>{{ date("d/m/Y", strtotime($mp["from"])) }}</td>
+                                                                    <td>{{ date("H:i", strtotime($mp["from"])) }} - {{ date("H:i", strtotime($mp["to"])) }}</td>
+                                                                    <td>{!! $mp["status"] !!}</td>
+                                                                    <td>{!! $mp["motivation"] !!}</td>
+                                                                </tr>
+                                                            @endforeach
+                                                        </tbody>
+                                                    </table>
+                                                </div>
+                                            </div>
+                                        @endif
+                                    </div>
+
+                                @endif
+
                                 @if($type == 'gruppi')
                                     <form class="form--tesseramento mt-4">
                                         @livewire('categories')
@@ -1935,6 +2033,8 @@
     </style>
     <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')
@@ -2157,7 +2257,7 @@
         });
 
         function getNextTab(currentTab) {
-            const tabs = ['dati', 'tesseramento', 'corsi', 'gruppi'];
+            const tabs = ['dati', 'tesseramento', 'corsi', 'presenze', 'gruppi'];
             const currentIndex = tabs.indexOf(currentTab);
             return currentIndex < tabs.length - 1 ? tabs[currentIndex + 1] : currentTab;
         }
@@ -2746,5 +2846,80 @@
                 }
             });
         });
+
+        function downloadChart(chartId) {
+            x.toBase64Image()
+            var a = document.createElement('a');
+            a.href = Chart.getChart(chartId).toBase64Image()
+            a.download = @this.first_name + " " + @this.last_name + ' - Grafico Presenze.png';
+            a.click();
+            a.remove();
+        }
+
+        Livewire.on('load-chart', (mesi, valori, chartData) => {
+
+            try {
+
+                const ctx = document.getElementById('presenzeChart');
+
+                try {
+                    x.destroy();
+                }
+                catch (ee) {}
+
+                x = new Chart(ctx, {
+                        type: 'bar',
+                        data: {
+                            labels: mesi,
+                            // datasets: [
+                            //     {
+                            //         label: 'Presenze',
+                            //         data: valori,
+                            //         borderWidth: 1
+                            //     }
+                            // ],
+                            datasets: chartData,
+                        },
+                        options: {
+                            responsive: true,
+                            // maintainAspectRatio: false,
+                            interaction: {
+                                mode: 'index',
+                                intersect: false,
+                            },
+                            scales: {
+                                y: {
+                                beginAtZero: true
+                                }
+                            },
+                            plugins: {
+                                legend: {
+                                    display: false,
+                                },
+                                tooltip: {
+                                    backgroundColor: 'rgba(255, 255, 255, 1)',
+                                    titleColor: '#212529',
+                                    bodyColor: '#495057',
+                                    borderColor: '#e9ecef',
+                                    borderWidth: 2,
+                                    cornerRadius: 0,
+                                    titleFont: {
+                                        size: 18,
+                                        weight: 'bold',
+                                    },
+                                    bodyFont: {
+                                        size: 16,
+                                        weight: '400',
+                                    },
+                                },
+                            },
+                        }
+                    }
+                );
+            }
+            catch(e) {
+                console.log(e);
+            }
+        });
     </script>
 @endpush

+ 26 - 13
resources/views/livewire/motivation.blade.php

@@ -52,10 +52,9 @@
 
     @else
 
+        <a class="btn--ui lightGrey" href="/motivations"><i class="fa-solid fa-arrow-left"></i></a><br><br>
         <div class="container">
 
-            <a class="btn--ui lightGrey" href="/banks"><i class="fa-solid fa-arrow-left"></i></a><br><br>
-
             @if (session()->has('error'))
                 <div class="alert alert-danger" role="alert">
                     {{ session()->get('error') }}
@@ -93,6 +92,12 @@
                                     <input class="form-check-input form-control" style="width:22px; height:22px;" type="checkbox" id="enabled" wire:model="enabled">
                                 </div>
                             </div>
+                            <div class="col">
+                                <div class="form--item">
+                                    <label for="show_in_member_presences" class="form-label">Visualizza in Presenze Utente</label>
+                                    <input class="form-check-input form-control" style="width:22px; height:22px;" type="checkbox" id="show_in_member_presences" wire:model="show_in_member_presences">
+                                </div>
+                            </div>
                         </div>
 
                         <!-- // inline input field -->
@@ -137,6 +142,9 @@
         });
 
         function loadDataTable(){
+            let date = new Date();
+            let date_export = `${date.getFullYear()}${date.getMonth()}${date.getDate()}_`;
+
             if ( $.fn.DataTable.isDataTable('#tablesaw-350') ) {
                 $('#tablesaw-350').DataTable().destroy();
             }
@@ -148,37 +156,42 @@
                     topStart : null,
                     topEnd : null,
                     top1A: {
-                        buttons: [
-                            {
-                                extend: 'collection',
-                                text: 'ESPORTA',
+                        // buttons: [
+                            // {
+                                // extend: 'collection',
+                                // text: 'ESPORTA',
                                 buttons: [
                                     {
                                     extend: 'excelHtml5',
-                                        title: 'Motivazioni annullamento',
+                                        text: '<i class="fa-solid fa-file-excel"></i>',
+                                        action: newexportaction,
+                                        title: date_export + 'Motivazioni',
                                         exportOptions: {
                                             columns: ":not(':last')"
                                         }
                                     },
                                     {
                                         extend: 'pdfHtml5',
-                                        title: 'Motivazioni annullamento',
+                                        text: '<i class="fa-solid fa-file-pdf"></i>',
+                                        action: newexportaction,
+                                        title: date_export + 'Motivazioni',
                                         exportOptions: {
                                             columns: ":not(':last')"
                                         }
                                     },
                                     {
                                         extend: 'print',
-                                        text: 'Stampa',
-                                        title: 'Motivazioni annullamento',
+                                        text: '<i class="fa-solid fa-print"></i>',
+                                        action: newexportaction,
+                                        title: date_export + 'Motivazioni',
                                         exportOptions: {
                                             columns: ":not(':last')"
                                         }
                                     }
                                 ],
-                                dropup: true
-                            }
-                        ]
+                                // dropup: true
+                            // }
+                        // ]
                     },
                     top1B : {
                         pageLength: {

+ 17 - 32
resources/views/livewire/presence.blade.php

@@ -243,10 +243,10 @@
                             <div class="col-md-6" wire:ignore>
                                 <label for="member_id" class="form-label">Aggiungere una o più persone</label>
                                 <select name="member_id" class="form-select memberClass" aria-label="Seleziona una persona" multiple>
-                                    <option value="">--Seleziona--
-                                        @foreach($members as $member)
-                                    <option value="{{$member->id}}">{{$member->last_name}} {{$member->first_name}} ({{$member->fiscal_code}})
-                                        @endforeach
+                                    <option value="">--Seleziona--</option>
+                                    @foreach($members as $member)
+                                    <option value="{{$member->id}}">{{$member->last_name}} {{$member->first_name}} ({{$member->fiscal_code}})</option>
+                                    @endforeach
                                 </select>
                             </div>
                             <div class="col-md-6">
@@ -279,25 +279,14 @@
                                 </div>
                                 @if ($motivation_course_level)
                                     <div class="col-md-6">
-                                        <label for="motivation_course_type" class="form-label">Tipologia</label>
-                                        <select class="form-select form-select-lg me-1 @error('motivation_course_type') is-invalid @enderror" id="motivation_course_type" wire:model="motivation_course_type">
+                                        <label for="motivation_course_frequency" class="form-label">Frequenza</label>
+                                        <select class="form-select form-select-lg me-1 @error('motivation_course_frequency') is-invalid @enderror" id="motivation_course_frequency" wire:model="motivation_course_frequency">
                                             <option value="">
-                                            @foreach($course_types as $m)
+                                            @foreach($course_frequencies as $m)
                                                 <option value="{{$m["id"]}}">{{$m["name"]}}</option>
                                             @endforeach
                                         </select>
                                     </div>
-                                    @if ($motivation_course_type)
-                                        <div class="col-md-6">
-                                            <label for="motivation_course_frequency" class="form-label">Frequenza</label>
-                                            <select class="form-select form-select-lg me-1 @error('motivation_course_frequency') is-invalid @enderror" id="motivation_course_frequency" wire:model="motivation_course_frequency">
-                                                <option value="">
-                                                @foreach($course_frequencies as $m)
-                                                    <option value="{{$m["id"]}}">{{$m["name"]}}</option>
-                                                @endforeach
-                                            </select>
-                                        </div>
-                                    @endif
                                 @endif
                             @endif
                         </div>
@@ -368,25 +357,14 @@
                                 </div>
                                 @if ($motivation_course_level)
                                     <div class="col-md-6">
-                                        <label for="motivation_course_type" class="form-label">Tipologia</label>
-                                        <select class="form-select form-select-lg me-1 @error('motivation_course_type') is-invalid @enderror" id="motivation_course_type" wire:model="motivation_course_type">
+                                        <label for="motivation_course_frequency" class="form-label">Frequenza</label>
+                                        <select class="form-select form-select-lg me-1 @error('motivation_course_frequency') is-invalid @enderror" id="motivation_course_frequency" wire:model="motivation_course_frequency">
                                             <option value="">
-                                            @foreach($course_types as $m)
+                                            @foreach($course_frequencies as $m)
                                                 <option value="{{$m["id"]}}">{{$m["name"]}}</option>
                                             @endforeach
                                         </select>
                                     </div>
-                                    @if ($motivation_course_type)
-                                        <div class="col-md-6">
-                                            <label for="motivation_course_frequency" class="form-label">Frequenza</label>
-                                            <select class="form-select form-select-lg me-1 @error('motivation_course_frequency') is-invalid @enderror" id="motivation_course_frequency" wire:model="motivation_course_frequency">
-                                                <option value="">
-                                                @foreach($course_frequencies as $m)
-                                                    <option value="{{$m["id"]}}">{{$m["name"]}}</option>
-                                                @endforeach
-                                            </select>
-                                        </div>
-                                    @endif
                                 @endif
                             @endif
                         </div>
@@ -529,6 +507,7 @@
         margin: 0;
         position: relative;
         vertical-align: middle;
+        z-index: 10000 !important;
     }
 
     .select2-container .select2-selection--single {
@@ -787,6 +766,9 @@
         function change(val) {
             if (val == 1) {
                 @this.insertUser = 'exist';
+                setTimeout(() => {
+                    $('.memberClass').select2();
+                }, 100);
                 $(".existUser").css("display", "block");
                 $(".newUser").css("display", "none");
             } else if (val == 2) {
@@ -799,6 +781,9 @@
 
         Livewire.on('resetCreationForm', () => {
             @this.insertUser = 'exist';
+            setTimeout(() => {
+                $('.memberClass').select2();
+            }, 100);
             $(".existUser").css("display", "block");
             $(".newUser").css("display", "none");
         });

+ 27 - 10
resources/views/livewire/presence_report.blade.php

@@ -24,13 +24,18 @@
                 <i class="fa-solid fa-chevron-left"></i>
             </a>
             @php
-                $date_title = \Illuminate\Support\Carbon::parse($date)->locale('it-IT')->translatedFormat("j F Y");
+                $date = \Illuminate\Support\Carbon::parse($date);
+                $date_back = $date->format('Y-m-d');
+                $date_title = $date->locale('it-IT')->translatedFormat("j F Y");
             @endphp
             <h4 class="text-uppercase m-0">{{$date_title}}</h4>
             <a style="cursor:pointer;" wire:click="next()">
                 <i class="fa-solid fa-chevron-right"></i>
             </a>
         </div>
+        <div class="col-auto">
+            <a class="btn--ui btn-primary" style="cursor:pointer;" href='/calendar?last_date={{$date_back}}'>Vai al calendario</a>
+        </div>
         <div class="col-auto">
             <a class="btn--ui btn-primary" style="cursor:pointer;" href='/absence_reports'>Alert assenze</a>
         </div>
@@ -48,7 +53,6 @@
                 @endforeach
             </select>
         </div>
-        @if(false)
             <div class="col-auto d-flex gap-3 align-items-center mb-3">
                 <label for="from" class="form-label fw-medium text-uppercase mb-0" style="white-space:nowrap;">Ora inizio</label>
                 <select wire:model="from" class="form-select" id="from" style="width: fit-content">
@@ -59,6 +63,7 @@
                     @endfor
                 </select>
             </div>
+        @if(false)
             <div class="col-auto d-flex gap-3 align-items-center mb-3">
                 <label for="to" class="form-label fw-medium text-uppercase mb-0" style="white-space:nowrap;">Ora fine</label>
                 <select wire:model="to" class="form-select" id="to" style="width: fit-content">
@@ -91,7 +96,7 @@
 
         <div class="col-lg-9 col-md-12"></div>
         <div class="col-lg-3 col-md-12 d-flex gap-3 align-items-center mt-5 mb-3">
-            <label class="form-label fw-medium text-uppercase mb-0" for="course_id">Cerca</label>
+            <label class="form-label fw-medium text-uppercase mb-0" for="search">Cerca</label>
             <input wire:model="search" type="search" class="form-control form-control-sm" id="search" />
         </div>
     </div>
@@ -109,17 +114,29 @@
                     <table class="report-table">
                         <thead>
                             <tr>
-                                <td>Cognome</td>
-                                <td>Nome</td>
-                                <td>Campo</td>
-                                <td>Istruttore</td>
-                                <td>Stato</td>
-                                <td>Motivazione</td>
+                                <td style="width: 15%">Livello</td>
+                                <td style="width: 15%">Cognome</td>
+                                <td style="width: 15%">Nome</td>
+                                <td style="width: 10%">Campo</td>
+                                <td style="width: 15%">Istruttore</td>
+                                <td style="width: 10%">Stato</td>
+                                <td style="width: 20%">Motivazione</td>
                             </tr>
                         </thead>
                         <tbody>
+                            @php
+                                $last_level = "";
+                                $index = 0;
+                            @endphp
                             @foreach($presences as $presence)
-                            <tr>
+                            @php
+                                if ($last_level != $presence["course_level"]) {
+                                    $index++;
+                                }
+                                $last_level = $presence["course_level"];
+                            @endphp
+                            <tr @if ($index % 2 == 0) style="background-color: rgba(12 97 151 / 0.15)" @endif>
+                                <td>{{$presence["course_level"]}}</td>
                                 <td>{{$presence["last_name"]}}</td>
                                 <td>{{$presence["first_name"]}}</td>
                                 <td>{{$presence["court"]}}</td>

+ 1 - 0
routes/web.php

@@ -131,6 +131,7 @@ Route::group(['middleware' => 'tenant'], function () {
     Route::get('/subscription_member', \App\Http\Livewire\SubscriptionMembers::class);
     Route::get('/subscription_member/{id}', \App\Http\Livewire\SubscriptionMember::class);
 
+    Route::get('/motivations', \App\Http\Livewire\Motivation::class);
     Route::get('/calendar', \App\Http\Livewire\Calendar::class);
     Route::get('/presences', \App\Http\Livewire\Presence::class);
     Route::get('/presence_reports', \App\Http\Livewire\PresenceReport::class);