Browse Source

Fix menù

Luca Parisio 1 month ago
parent
commit
a1caa0bea5
71 changed files with 6262 additions and 1440 deletions
  1. 36 9
      app/Console/Commands/DispatchDueEmails.php
  2. 54 0
      app/Console/Commands/DispatchDueSms.php
  3. 2 0
      app/Console/Kernel.php
  4. 2 2
      app/Http/Livewire/Azienda.php
  5. 1 1
      app/Http/Livewire/Category.php
  6. 19 2
      app/Http/Livewire/Causals.php
  7. 66 41
      app/Http/Livewire/Course.php
  8. 2 2
      app/Http/Livewire/CourseList.php
  9. 5 3
      app/Http/Livewire/CourseMember.php
  10. 1 1
      app/Http/Livewire/CourseMemberOne.php
  11. 118 28
      app/Http/Livewire/Dashboard.php
  12. 30 7
      app/Http/Livewire/EmailComunications.php
  13. 290 80
      app/Http/Livewire/Member.php
  14. 138 0
      app/Http/Livewire/MemberArchive.php
  15. 21 2
      app/Http/Livewire/Profile.php
  16. 38 22
      app/Http/Livewire/Rate.php
  17. 205 40
      app/Http/Livewire/RecordIN.php
  18. 2 2
      app/Http/Livewire/RecordINOUT.php
  19. 15 1
      app/Http/Livewire/Reports.php
  20. 245 129
      app/Http/Livewire/SmsComunications.php
  21. 284 0
      app/Http/Livewire/Subscription.php
  22. 105 0
      app/Http/Livewire/SubscriptionMember.php
  23. 16 0
      app/Http/Livewire/SubscriptionMembers.php
  24. 13 4
      app/Http/Livewire/User.php
  25. 134 0
      app/Jobs/SendSmsMessage.php
  26. 1 1
      app/Models/Azienda.php
  27. 15 0
      app/Models/Course.php
  28. 7 0
      app/Models/Member.php
  29. 52 0
      app/Models/MemberSubscription.php
  30. 6 1
      app/Models/Rate.php
  31. 12 0
      app/Models/ReceiptRow.php
  32. 11 0
      app/Models/RecordRow.php
  33. 69 0
      app/Models/SmsMessage.php
  34. 28 0
      app/Models/SmsMessageRecipient.php
  35. 35 0
      app/Models/Subscription.php
  36. 181 177
      composer.lock
  37. 1 1
      database/migrations/2025_10_18_135607_create_email_messages.php
  38. 34 0
      database/migrations/2025_11_12_164023_add_is_deleted_to_members.php
  39. 33 0
      database/migrations/2025_11_14_145909_add_course_id_to_records_rows.php
  40. 33 0
      database/migrations/2025_11_17_114817_add_course_id_to_receipts_rows.php
  41. 41 0
      database/migrations/2025_11_21_144756_create_sms_messages.php
  42. 87 0
      database/migrations/2025_11_26_132239_create_subscriptions_tables.php
  43. 17 1
      public/css/chart-reports.css
  44. 44 26
      public/css/new_style.css
  45. 10 0
      resources/views/first-login.blade.php
  46. 58 26
      resources/views/layouts/app.blade.php
  47. 45 53
      resources/views/livewire/category.blade.php
  48. 12 10
      resources/views/livewire/course.blade.php
  49. 2 2
      resources/views/livewire/course_list.blade.php
  50. 2 2
      resources/views/livewire/course_list_original.blade.php
  51. 23 5
      resources/views/livewire/course_member.blade.php
  52. 6 4
      resources/views/livewire/course_member_two.blade.php
  53. 2 2
      resources/views/livewire/course_subscription.blade.php
  54. 2 14
      resources/views/livewire/dashboard.blade.php
  55. 57 43
      resources/views/livewire/email_comunications.blade.php
  56. 523 291
      resources/views/livewire/member.blade.php
  57. 613 0
      resources/views/livewire/member_archive.blade.php
  58. 30 5
      resources/views/livewire/profile.blade.php
  59. 29 19
      resources/views/livewire/rate.blade.php
  60. 165 60
      resources/views/livewire/records_in.blade.php
  61. 24 3
      resources/views/livewire/records_in_out.blade.php
  62. 22 21
      resources/views/livewire/reports.blade.php
  63. 13 6
      resources/views/livewire/settings.blade.php
  64. 571 232
      resources/views/livewire/sms_comunications.blade.php
  65. 318 0
      resources/views/livewire/subscription.blade.php
  66. 503 0
      resources/views/livewire/subscription_member.blade.php
  67. 141 0
      resources/views/livewire/subscription_members.blade.php
  68. 55 15
      resources/views/livewire/user.blade.php
  69. 37 1
      resources/views/login.blade.php
  70. 6 0
      resources/views/receipt.blade.php
  71. 444 43
      routes/web.php

+ 36 - 9
app/Console/Commands/DispatchDueEmails.php

@@ -5,6 +5,8 @@ namespace App\Console\Commands;
 use Illuminate\Console\Command;
 use App\Models\EmailMessage;
 use App\Jobs\SendEmailMessage;
+use DateTimeZone;
+use Illuminate\Foundation\Auth\User;
 
 class DispatchDueEmails extends Command
 {
@@ -13,15 +15,40 @@ class DispatchDueEmails extends Command
 
     public function handle()
     {
-        app(\App\Http\Middleware\TenantMiddleware::class)->setupTenantConnection();
-
-        EmailMessage::where('status', 'scheduled')
-            ->where('schedule_at', '<=', now())
-            ->chunkById(100, function ($chunk) {
-                foreach ($chunk as $msg) {
-                    dispatch(new SendEmailMessage($msg->id));
-                }
-            });
+        $users = User::whereNotNull('tenant_host')
+            ->whereNotNull('tenant_database')
+            ->whereNotNull('tenant_username')
+            ->whereNotNull('tenant_password')
+            ->get([
+                'id',
+                'tenant_host',
+                'tenant_database',
+                'tenant_username',
+                'tenant_password',
+            ]);
+
+        if ($users->isEmpty()) {
+            $this->warn('Nessun utente con info di database trovata.');
+            return Command::SUCCESS;
+        }
+
+        $tenants = $users->unique(function ($u) {
+            return $u->tenant_host . '|' . $u->tenant_database . '|' . $u->tenant_username;
+        });
+
+        foreach ($tenants as $userTenant) {
+            $this->info("Processo tenant db={$userTenant->tenant_database} (user id={$userTenant->id})");
+
+            app(\App\Http\Middleware\TenantMiddleware::class)->setupTenantConnection($userTenant);
+
+            EmailMessage::where('status', 'scheduled')
+                ->where('schedule_at', '<=', now())
+                ->chunkById(100, function ($chunk) {
+                    foreach ($chunk as $msg) {
+                        dispatch(new SendEmailMessage($msg->id));
+                    }
+                });
+        }
 
         return Command::SUCCESS;
     }

+ 54 - 0
app/Console/Commands/DispatchDueSms.php

@@ -0,0 +1,54 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Models\SmsMessage;
+use App\Jobs\SendSmsMessage;
+use Illuminate\Foundation\Auth\User;
+
+class DispatchDueSms extends Command
+{
+    protected $signature = 'sms:dispatch-due';
+    protected $description = 'Invia gli sms programmati giunti a scadenza';
+
+    public function handle()
+    {
+        $users = User::whereNotNull('tenant_host')
+            ->whereNotNull('tenant_database')
+            ->whereNotNull('tenant_username')
+            ->whereNotNull('tenant_password')
+            ->get([
+                'id',
+                'tenant_host',
+                'tenant_database',
+                'tenant_username',
+                'tenant_password',
+            ]);
+
+        if ($users->isEmpty()) {
+            $this->warn('Nessun utente con info di database trovata.');
+            return Command::SUCCESS;
+        }
+
+        $tenants = $users->unique(function ($u) {
+            return $u->tenant_host . '|' . $u->tenant_database . '|' . $u->tenant_username;
+        });
+
+        foreach ($tenants as $userTenant) {
+            $this->info("Processo tenant db={$userTenant->tenant_database} (user id={$userTenant->id})");
+
+            app(\App\Http\Middleware\TenantMiddleware::class)->setupTenantConnection($userTenant);
+
+            // SmsMessage::where('status', 'scheduled')
+            SmsMessage::where('schedule_at', '<=', now())
+                ->chunkById(100, function ($chunk) {
+                    foreach ($chunk as $msg) {
+                        dispatch(new SendSmsMessage($msg->id));
+                    }
+                });
+        }
+
+        return Command::SUCCESS;
+    }
+}

+ 2 - 0
app/Console/Kernel.php

@@ -19,6 +19,8 @@ class Kernel extends ConsoleKernel
 
         // invia email programmate
         $schedule->command('emails:dispatch-due')->everyMinute();
+        // invia sms programmati
+        $schedule->command('sms:dispatch-due')->everyMinute();
     }
 
     /**

+ 2 - 2
app/Http/Livewire/Azienda.php

@@ -279,8 +279,8 @@ class Azienda extends Component
             }
 
             session()->flash('message', $this->azienda->wasRecentlyCreated ?
-                'Dati aziendali creati con successo!' :
-                'Dati aziendali aggiornati con successo!'
+                'Dati societari creati con successo' :
+                'Dati societari aggiornati con successo'
             );
 
             $this->update = false;

+ 1 - 1
app/Http/Livewire/Category.php

@@ -40,7 +40,7 @@ class Category extends Component
 
     public function render()
     {
-        $this->records = \App\Models\Category::where('parent_id', null)->get();
+        $this->records = \App\Models\Category::where('parent_id', null)->orderBy('name')->get();
         return view('livewire.category');
     }
 

+ 19 - 2
app/Http/Livewire/Causals.php

@@ -20,6 +20,7 @@ class Causals extends Component
     public $idx = -1;
     public $causal_id = null;
     public $show_hidden = true;
+    public $show_course_causals = true;
 
     public function boot()
     {
@@ -32,7 +33,7 @@ class Causals extends Component
         $this->idx = $idx;
         $this->causal_id = $causal_id;
 
-        if ($this->causal_id != null) {
+        if ($this->causal_id != null && $this->causal_id != 0) {
             $c = \App\Models\Causal::findOrFail($this->causal_id);
             $ids = array_reverse($c->recursiveParent($c->parent_id, [$c->id]));
 
@@ -75,16 +76,23 @@ class Causals extends Component
     {
         $reset = false;
 
-        // Build query with visibility filter
         $visibilityFilter = function($query) {
             return $query;
         };
+        $courseFilter = function($query) {
+            return $query;
+        };
 
         if ($this->show_hidden == false) {
             $visibilityFilter = function($query) {
                 return $query->where('hidden', false);
             };
         }
+        if ($this->show_course_causals == false) {
+            $courseFilter = function($query) {
+                return $query->whereNotIn('name', ['PAGAMENTO CORSO', 'PAGAMENTO ISCRIZIONE']);
+            };
+        }
 
         if ($this->level_1_id > 0) {
             $this->level_2 = \App\Models\Causal::where('parent_id', $this->level_1_id)
@@ -92,6 +100,9 @@ class Causals extends Component
                 ->where(function($query) use ($visibilityFilter) {
                     return $visibilityFilter($query);
                 })
+                ->where(function($query) use ($courseFilter) {
+                    return $courseFilter($query);
+                })
                 ->orderBy('name')
                 ->get();
 
@@ -107,6 +118,9 @@ class Causals extends Component
                 ->where(function($query) use ($visibilityFilter) {
                     return $visibilityFilter($query);
                 })
+                ->where(function($query) use ($courseFilter) {
+                    return $courseFilter($query);
+                })
                 ->orderBy('name')
                 ->get();
 
@@ -126,6 +140,9 @@ class Causals extends Component
             ->where(function($query) use ($visibilityFilter) {
                 return $visibilityFilter($query);
             })
+            ->where(function($query) use ($courseFilter) {
+                return $courseFilter($query);
+            })
             ->orderBy('name')
             ->get();
 

+ 66 - 41
app/Http/Livewire/Course.php

@@ -35,7 +35,7 @@ class Course extends Component
     public $msgPrices = '';
     public $msgWhen = '';
 
-    public $course_types = [];
+    // public $course_types = [];
     public $course_durations = [];
     public $course_frequencies = [];
     public $course_levels = [];
@@ -112,7 +112,7 @@ class Course extends Component
             // $this->categories[] = array('id' => $record->id, 'name' => str_repeat(" / ", $indentation) . $record->name);
             $this->categories[] = array('id' => $record->id, 'name' => $record->getTree(), 'indentation' => $indentation);
             if(count($record->childs))
-                $this->getCategories($record->childs, $indentation + 1);
+                $this->getCategories($record->childs->sortBy('name'), $indentation + 1);
         }
     }
 
@@ -123,7 +123,7 @@ class Course extends Component
 
         $this->categories = array();
 
-        $this->getCategories(\App\Models\Category::select('id', 'name')->where('parent_id', null)->get(), 0);
+        $this->getCategories(\App\Models\Category::select('id', 'name')->where('parent_id', null)->orderBy('name')->get(), 0);
 
         for($i=date("Y"); $i<=date("Y") + 1; $i++)
         {
@@ -140,7 +140,7 @@ class Course extends Component
             $this->monthList[$i][11] = "Novembre";
             $this->monthList[$i][12] = "Dicembre";
         }
-        $this->course_types = \App\Models\CourseType::select('*')->where('enabled', true)->get();
+        // $this->course_types = \App\Models\CourseType::select('*')->where('enabled', true)->get();
         $this->course_durations = \App\Models\CourseDuration::select('*')->where('enabled', true)->get();
         $this->course_levels = \App\Models\CourseLevel::select('*')->where('enabled', true)->get();
         $this->course_subscriptions = \App\Models\CourseSubscription::select('*')->where('enabled', true)->get();
@@ -201,7 +201,7 @@ class Course extends Component
             $subscriptions = array_column($this->prices, 'course_subscription_id');
             $unique_subscriptions = array_unique($subscriptions);
             if (count($subscriptions) != count($unique_subscriptions))
-                $this->msgPrices = 'È possibile aggiungere solo un abbonamento per ciascun tipo';
+                $this->msgPrices = 'Non è possibile aggiungere più volte la stessa tipologia di pagamento';
 
             if ($this->msgPrices == '' &&  $this->msgWhen == '')
             {
@@ -322,7 +322,8 @@ class Course extends Component
                 //$causal = "PAGAMENTO CORSO [nome corso]-LIVELLO-FREQUENZA-ANNO-[tipo abbonamento]-[mese inizio]/[mese fine]
 
                 /*
-                $causal = "PAGAMENTO CORSO " . $this->name . " - " . $lev . " - " . $freq . " - " . $this->year . " - " . $mFrom . "/" . $mTo;
+                // $causal = "PAGAMENTO CORSO " . $this->name . " - " . $lev . " - " . $freq . " - " . $this->year . " - " . $mFrom . "/" . $mTo;
+                $causal = "PAGAMENTO CORSO";
                 $cp = \App\Models\Causal::where('name', $causal)->first();
                 if (!$cp)
                 {
@@ -341,7 +342,8 @@ class Course extends Component
                 }
                 $course->causal_id = $cp->id;
 
-                $causal = "PAGAMENTO ISCRIZIONE " . $this->name . " - " . $lev . " - " . $freq . " - " . $this->year . " - " . $mFrom . "/" . $mTo;
+                // $causal = "PAGAMENTO ISCRIZIONE " . $this->name . " - " . $lev . " - " . $freq . " - " . $this->year . " - " . $mFrom . "/" . $mTo;
+                $causal = "PAGAMENTO ISCRIZIONE";
                 $ci = \App\Models\Causal::where('name', $causal)->first();
                 if (!$ci)
                 {
@@ -438,33 +440,52 @@ class Course extends Component
     {
         $this->validate();
         try {
-            \App\Models\Course::whereId($this->dataId)->update([
-                'name' => $this->name,
-                'parent_id' => $this->parent_id,
-                'course_type_id' => $this->course_type_id,
-                'course_duration_id' => $this->course_duration_id,
-                'course_frequency_id' => $this->course_frequency_id,
-                'course_level_id' => $this->course_level_id,
-                'date_from' => $this->date_from,
-                'date_to' => $this->date_to,
-                'category_id' => $this->category_id,
-                'causal_id' => $this->causal_id,
-                'sub_causal_id' => $this->sub_causal_id,
-                'max_members' => $this->max_members,
-                'instructor_id' => $this->instructor_id,
-                'year' => $this->year,
-                'price' => currencyToDouble($this->price),
-                'subscription_price' => currencyToDouble($this->subscription_price),
-                'months' => json_encode($this->months),
-                'type' => $this->type,
-                'when' => json_encode($this->when),
-                'prices' => json_encode($this->prices),
-                'enabled' => $this->enabled
-            ]);
-            session()->flash('success','Corso aggiornato');
-            $this->resetFields();
-            $this->update = false;
-            $this->emit('setEdit', false);
+            $this->msgPrices = '';
+            $this->msgWhen = '';
+
+            if ($this->type == 'standard') {
+                if ($this->when[0]['from'] == '')
+                    $this->msgWhen = 'Devi inserire almeno un giorno';
+            }
+    
+            if ($this->prices[0]['course_subscription_id'] == null)
+                $this->msgPrices = 'Devi inserire almeno un prezzo';
+            
+            $subscriptions = array_column($this->prices, 'course_subscription_id');
+            $unique_subscriptions = array_unique($subscriptions);
+            if (count($subscriptions) != count($unique_subscriptions))
+                $this->msgPrices = 'Non è possibile aggiungere più volte la stessa tipologia di pagamento';
+
+            if ($this->msgPrices == '' &&  $this->msgWhen == '')
+            {
+                \App\Models\Course::whereId($this->dataId)->update([
+                    'name' => $this->name,
+                    'parent_id' => $this->parent_id,
+                    'course_type_id' => $this->course_type_id,
+                    'course_duration_id' => $this->course_duration_id,
+                    'course_frequency_id' => $this->course_frequency_id,
+                    'course_level_id' => $this->course_level_id,
+                    'date_from' => $this->date_from,
+                    'date_to' => $this->date_to,
+                    'category_id' => $this->category_id,
+                    'causal_id' => $this->causal_id,
+                    'sub_causal_id' => $this->sub_causal_id,
+                    'max_members' => $this->max_members,
+                    'instructor_id' => $this->instructor_id,
+                    'year' => $this->year,
+                    'price' => currencyToDouble($this->price),
+                    'subscription_price' => currencyToDouble($this->subscription_price),
+                    'months' => json_encode($this->months),
+                    'type' => $this->type,
+                    'when' => json_encode($this->when),
+                    'prices' => json_encode($this->prices),
+                    'enabled' => $this->enabled
+                ]);
+                session()->flash('success','Corso aggiornato');
+                $this->resetFields();
+                $this->update = false;
+                $this->emit('setEdit', false);
+            }
         } catch (\Exception $ex) {
             session()->flash('error','Errore (' . $ex->getMessage() . ')');
         }
@@ -507,13 +528,17 @@ class Course extends Component
     public function duplicate($id, $isMultiple){
         $course = \App\Models\Course::findOrFail($id);
         $newCourse = $course->replicate();
-        $newYear = date("Y") . "-" . (date("Y") + 1);
-        if ($course->year != '')
-        {
-            list($u, $y) = explode("-", $course->year);
-            $newYear = ($u + 1) . "-" . ($u + 2);
-        }
-        $newCourse->year = $newYear;
+        // $newYear = date("Y") . "-" . (date("Y") + 1);
+        // if ($course->year != '')
+        // {
+        //     // list($u, $y) = explode("-", $course->year);
+        //     // $newYear = ($u + 1) . "-" . ($u + 2);
+        //     $newYear = $course->year;
+        // }
+        // $newCourse->year = $newYear;
+        $newCourse->date_from = null;
+        $newCourse->date_to = null;
+        $newCourse->year = null;
         $newCourse->save();
         if (!$isMultiple)
             $this->edit($newCourse->id);

+ 2 - 2
app/Http/Livewire/CourseList.php

@@ -41,7 +41,7 @@ class CourseList extends Component
     public $filterDuration = [];
 
     public $course_durations = [];
-    public $course_types = [];
+    // public $course_types = [];
     public $course_frequencies = [];
     public $course_levels = [];
     public $course_years = [];
@@ -64,7 +64,7 @@ class CourseList extends Component
         $this->selectedCourseId = 0;
         $this->selectedMemberId = 0;
 
-        $this->course_types = \App\Models\CourseType::select('*')->where('enabled', true)->get();
+        // $this->course_types = \App\Models\CourseType::select('*')->where('enabled', true)->get();
         $this->course_durations = \App\Models\CourseDuration::select('*')->where('enabled', true)->get();
         $this->course_levels = \App\Models\CourseLevel::select('*')->where('enabled', true)->get();
         $this->course_frequencies = \App\Models\CourseFrequency::select('*')->where('enabled', true)->get();

+ 5 - 3
app/Http/Livewire/CourseMember.php

@@ -11,9 +11,10 @@ class CourseMember extends Component
 
     public $records = array();
 
+    public $course = null;
     public $courses = [];
     public $course_frequencies = [];
-    public $course_types = [];
+    // public $course_types = [];
     public $course_levels = [];
     public $course_durations = [];
     public $course_years = [];
@@ -56,10 +57,11 @@ class CourseMember extends Component
         if (isset($_GET["id"]))
         {
             $this->filterFromPrevious = $_GET["id"];
-            $this->type = \App\Models\Course::findOrFail($_GET["id"])->type;
+            $this->course = \App\Models\Course::findOrFail($_GET["id"]);
+            $this->type = $this->course->type;
         }
 
-        $this->course_types = \App\Models\CourseType::select('*')->where('enabled', true)->get();
+        // $this->course_types = \App\Models\CourseType::select('*')->where('enabled', true)->get();
         $this->course_frequencies = \App\Models\CourseFrequency::select('*')->where('enabled', true)->get();
         $this->course_levels = \App\Models\CourseLevel::select('*')->where('enabled', true)->get();
         $this->course_durations = \App\Models\CourseDuration::select('*')->where('enabled', true)->get();

+ 1 - 1
app/Http/Livewire/CourseMemberOne.php

@@ -10,7 +10,7 @@ class CourseMemberOne extends Component
 
     public function render()
     {
-        $this->records[] = array('name' => 'Standard', 'type' => 'standard');
+        $this->records[] = array('name' => 'Singoli', 'type' => 'standard');
         $this->records[] = array('name' => 'Personalizzati', 'type' => 'custom');
         return view('livewire.course_member_one');
     }

+ 118 - 28
app/Http/Livewire/Dashboard.php

@@ -203,31 +203,132 @@ class Dashboard extends Component
             $currentDate = now()->format('Y-m-d');
             Log::info('Calculating financial stats for month', ['month' => $currentMonth]);
 
-            $this->received = \App\Models\Record::where('type', 'IN')
+            // excluded members
+            $excluded_members = \App\Models\Member::where('exclude_from_records', true)->pluck('id')->toArray();
+            // excluded payment method
+            $excluded_payment_methods = \App\Models\PaymentMethod::where('money', true)->pluck('id')->toArray();
+            // excluded causals
+            $excluded_causals = \App\Models\Causal::orWhereIn('name', [
+                'Corrispettivo fiscale giornaliero',
+                'CFG',
+            ])->orWhere('hidden', 1)->orWhere('enabled', 0)->pluck('id')->toArray();
+
+
+            // Incassato
+            $this->received = 0;
+            $records_in = \App\Models\Record::with('rows')->where('type', 'IN')
                 ->whereRaw('DATE_FORMAT(date, "%Y-%m") = ?', [$currentMonth])
                 ->where(function ($query) {
                     $query->where('deleted', false)->orWhere('deleted', null);
-                })
-                ->sum('amount') ?? 0;
+                })->get();
+            foreach ($records_in as $record) {
+                if (in_array($record->payment_method_id, $excluded_payment_methods)) {
+                    continue;
+                }
 
-            $this->paid = \App\Models\Record::where('type', 'OUT')
-                ->whereRaw('DATE_FORMAT(date, "%Y-%m") = ?', [$currentMonth])
-                ->where('data_pagamento', '<=', $currentDate)
+                if (in_array($record->member_id, $excluded_members)) {
+                    continue;
+                }
+
+                foreach ($record->rows as $row) {
+                    if ($row->causal_id) {
+                        $causal = \App\Models\Causal::find($row->causal_id);
+
+                        if (!$causal || (isset($causal->hidden) && $causal->hidden)) {
+                            continue;
+                        }
+
+                        if (in_array($row->causal_id, $excluded_causals)) {
+                            continue;
+                        }
+
+                        $this->received += $row->amount;
+                        if ($row->vat_id > 0) {
+                            $this->received += getVatValue($row->amount, $row->vat_id);
+                        }
+                    }
+                }
+            }
+            // END - Incassato
+
+
+            // Pagato
+            $this->paid = 0;
+            $records_out = \App\Models\Record::with('rows')->where('type', 'OUT')
+                ->whereRaw('DATE_FORMAT(data_pagamento, "%Y-%m") = ?', [$currentMonth])
                 ->where(function ($query) {
                     $query->where('deleted', false)->orWhere('deleted', null);
-                })
-                ->sum('amount') ?? 0;
+                })->get();
+            foreach ($records_out as $record) {
+                if (in_array($record->payment_method_id, $excluded_payment_methods)) {
+                    continue;
+                }
 
-            $this->toPay = \App\Models\Record::where('type', 'OUT')
-                ->whereRaw('DATE_FORMAT(date, "%Y-%m") = ?', [$currentMonth])
-                ->where(function ($query) use($currentDate) {
+                if (in_array($record->member_id, $excluded_members)) {
+                    continue;
+                }
+
+                foreach ($record->rows as $row) {
+                    if ($row->causal_id) {
+                        $causal = \App\Models\Causal::find($row->causal_id);
+
+                        if (!$causal || (isset($causal->hidden) && $causal->hidden)) {
+                            continue;
+                        }
+
+                        if (in_array($row->causal_id, $excluded_causals)) {
+                            continue;
+                        }
+
+                        $this->paid += $row->amount;
+                        if ($row->vat_id > 0) {
+                            $this->paid += getVatValue($row->amount, $row->vat_id);
+                        }
+                    }
+                }
+            }
+            // END - Pagato
+
+
+            // Da pagare
+            $this->toPay = 0;
+            $records_toPay = \App\Models\Record::with('rows')->where('type', 'OUT')
+                ->where(function ($query) use ($currentDate) {
                     $query->where('data_pagamento', '>', $currentDate)
                         ->orWhereNull('data_pagamento');
                 })
                 ->where(function ($query) {
                     $query->where('deleted', false)->orWhere('deleted', null);
-                })
-                ->sum('amount') ?? 0;
+                })->get();
+            foreach ($records_toPay as $record) {
+                if (in_array($record->payment_method_id, $excluded_payment_methods)) {
+                    continue;
+                }
+
+                if (in_array($record->member_id, $excluded_members)) {
+                    continue;
+                }
+
+                foreach ($record->rows as $row) {
+                    if ($row->causal_id) {
+                        $causal = \App\Models\Causal::find($row->causal_id);
+
+                        if (!$causal || (isset($causal->hidden) && $causal->hidden)) {
+                            continue;
+                        }
+
+                        if (in_array($row->causal_id, $excluded_causals)) {
+                            continue;
+                        }
+
+                        $this->toPay += $row->amount;
+                        if ($row->vat_id > 0) {
+                            $this->toPay += getVatValue($row->amount, $row->vat_id);
+                        }
+                    }
+                }
+            }
+            // END - Da pagare
 
             $endTime = microtime(true);
             Log::info('Financial stats loaded successfully', [
@@ -431,12 +532,7 @@ class Dashboard extends Component
                 $frequencyName = $course->frequency?->name ?? '';
                 $typeName = $course->getFormattedTypeField() ?? '';
 
-                $courseNameParts = [$courseName];
-                if ($levelName) $courseNameParts[] = $levelName;
-                if ($typeName) $courseNameParts[] = $typeName;
-                if ($frequencyName) $courseNameParts[] = $frequencyName;
-
-                $fullCourseName = implode(' - ', $courseNameParts);
+                $fullCourseName = $course->getDetailsName();
 
                 return [
                     'time' => $todaySchedule['from'] . ' - ' . $todaySchedule['to'],
@@ -451,9 +547,9 @@ class Dashboard extends Component
                     'course_id' => $course->id,
                     'member_course_id' => $memberCourse->id
                 ];
-            })->filter()->values();
+            })->filter()->unique('course_id')->values();
 
-            $sortedCourses = $this->courses->sortBy('from_time')->take(5);
+            $sortedCourses = $this->courses->sortBy('from_time')->take(5)->values();
 
             Log::info('Courses sorted by time', [
                 'sorted_courses' => $sortedCourses->map(function ($course) {
@@ -529,13 +625,7 @@ class Dashboard extends Component
                 $frequencyName = is_object($course->frequency) ? $course->frequency->name : '';
                 $typeName = $course->getFormattedTypeField() ?? '';
 
-                // Build display name with level and frequency
-                $displayNameParts = [$courseName];
-                if ($levelName) $displayNameParts[] = $levelName;
-                if ($typeName) $displayNameParts[] = $typeName;
-                if ($frequencyName) $displayNameParts[] = $frequencyName;
-
-                $displayName = implode(' - ', $displayNameParts);
+                $displayName = $course->getDetailsName();
 
                 // Assegna colori basati sul nome del corso
                 $color = $this->getCourseColor($courseName);

+ 30 - 7
app/Http/Livewire/EmailComunications.php

@@ -27,6 +27,7 @@ class EmailComunications extends Component
 
     public string $mode = 'now'; // 'now' | 'schedule'
     public ?string $schedule_at = null;
+    public ?string $timezone = 'UTC';
 
     public $records;
     public $categories;
@@ -55,7 +56,7 @@ class EmailComunications extends Component
         $this->courses = [];
         $this->getCourses(\App\Models\Course::select('id', 'name')->where('parent_id', null)->orderBy('name', 'ASC')->get(), 0);
 
-        $this->schedule_at = now()->addHour()->format('Y-m-d\TH:i');
+        $this->schedule_at = now($this->timezone)->addHour()->format('Y-m-d\TH:i');
     }
 
     public function render()
@@ -89,7 +90,10 @@ class EmailComunications extends Component
 
     protected function validateDraft(): void
     {
-        $this->validate($this->baseRules());
+        $rules = [];
+        // $rules = $this->baseRules();
+        $rules['subject'] = 'required|string|max:255';
+        $this->validate($rules);
     }
 
     protected function validateSend(): void
@@ -108,7 +112,7 @@ class EmailComunications extends Component
     {
         $this->reset(['messageId', 'subject', 'content_html', 'recipients', 'newAttachments', 'mode', 'schedule_at']);
         $this->mode = 'now';
-        $this->schedule_at = now()->addHour()->format('Y-m-d\TH:i');
+        $this->schedule_at = now($this->timezone)->addHour()->format('Y-m-d\TH:i');
         $this->existingAttachments = [];
 
         $this->showForm = true;
@@ -136,8 +140,15 @@ class EmailComunications extends Component
                 'first_name'    => optional($r->member)->first_name,
                 'last_name'     => optional($r->member)->last_name,
             ])->toArray();
+            usort($this->recipients, function($a, $b) {
+                $last_name = strcmp($a['last_name'], $b['last_name']);
+                $first_name = strcmp($a['first_name'], $b['first_name']);
+
+                return $last_name == 0 ? $first_name : $last_name;
+            });
+
             $this->mode = $msg->status === 'scheduled' ? 'schedule' : 'now';
-            $this->schedule_at = optional($msg->schedule_at)?->format('Y-m-d\TH:i');
+            $this->schedule_at = optional($msg->schedule_at)?->setTimezone($this->timezone)?->format('Y-m-d\TH:i');
             $this->existingAttachments = $msg->attachments->map(fn($a) => [
                 'id'   => $a->id,
                 'name' => $a->name ?: basename($a->path),
@@ -189,6 +200,7 @@ class EmailComunications extends Component
 
         $this->success = 'Bozza salvata';
 
+        $this->dispatchBrowserEvent('scroll-top');
         $this->dispatchBrowserEvent('load-editor', [
             'html'   => $this->content_html ?? '',
             'locked' => $this->locked,
@@ -220,6 +232,7 @@ class EmailComunications extends Component
         dispatch(new \App\Jobs\SendEmailMessage($this->messageId));
         $this->success = 'Invio avviato';
 
+        $this->dispatchBrowserEvent('scroll-top');
         $this->dispatchBrowserEvent('load-editor', [
             'html'   => $this->content_html ?? '',
             'locked' => $this->locked,
@@ -239,8 +252,10 @@ class EmailComunications extends Component
             }
         }
 
-        DB::transaction(function () {
-            $msg = $this->upsertMessage(status: 'scheduled', scheduleAt: \Carbon\Carbon::parse($this->schedule_at));
+        $scheduledAt = \Carbon\Carbon::parse($this->schedule_at, $this->timezone)->setTimezone('UTC');
+
+        DB::transaction(function () use ($scheduledAt) {
+            $msg = $this->upsertMessage(status: 'scheduled', scheduleAt: $scheduledAt);
             $this->upsertRecipients($msg, true);
             $this->upsertAttachments($msg, true);
             $this->messageId = $msg->id;
@@ -250,6 +265,7 @@ class EmailComunications extends Component
 
         $this->success = 'Email programmata';
 
+        $this->dispatchBrowserEvent('scroll-top');
         $this->dispatchBrowserEvent('load-editor', [
             'html'   => $this->content_html ?? '',
             'locked' => $this->locked,
@@ -310,7 +326,7 @@ class EmailComunications extends Component
         $this->showForm = false;
         $this->reset(['messageId', 'subject', 'content_html', 'recipients', 'newAttachments', 'mode', 'schedule_at']);
         $this->mode = 'now';
-        $this->schedule_at = now()->addHour()->format('Y-m-d\TH:i');
+        $this->schedule_at = now($this->timezone)->addHour()->format('Y-m-d\TH:i');
 
         $this->dispatchBrowserEvent('init-archive-table');
     }
@@ -353,6 +369,13 @@ class EmailComunications extends Component
             'first_name' => $m->first_name,
             'last_name' => $m->last_name,
         ];
+
+        usort($this->recipients, function($a, $b) {
+            $last_name = strcmp($a['last_name'], $b['last_name']);
+            $first_name = strcmp($a['first_name'], $b['first_name']);
+
+            return $last_name == 0 ? $first_name : $last_name;
+        });
     }
 
     public function removeNewAttachment(int $index): void

+ 290 - 80
app/Http/Livewire/Member.php

@@ -45,7 +45,7 @@ class Member extends Component
     public function change($type)
     {
         $this->type = $type;
-            if ($type === 'corsi' && $this->dataId > 0) {
+        if ($type === 'corsi' && $this->dataId > 0) {
             $this->loadMemberCards();
             $this->loadMemberCertificates();
             // TODO $this->checkCourseAvailability();
@@ -106,7 +106,7 @@ class Member extends Component
 
     public $course_names = array();
     public $course_levels = array();
-    public $course_types = array();
+    // public $course_types = array();
     public $course_frequencies = array();
 
     public $course_subscriptions = array();
@@ -164,6 +164,22 @@ class Member extends Component
         'birth_date' => 'before_or_equal:today'
     ];
 
+    /** Cambi Subscription */
+    public $member_subscriptions = [];
+
+    public $subscriptions;
+    public $subscription_exist;
+    public $subscription_subscriptions;
+
+    public $subscription_id;
+    public $subscription_course_subscription_id;
+    public $subscription_price;
+    public $subscription_subscription_price;
+    public $subscription_date_from;
+    public $subscription_date_to;
+    public $subscription_notes;
+    /** END - Cambi Subscription */
+
     public function resetFields()
     {
         $this->dataId = -1;
@@ -275,6 +291,21 @@ class Member extends Component
         $this->course_level_id = '';
         $this->course_type_id = '';
         $this->course_frequency_id = '';
+
+        $this->subscription_id = null;
+        $this->resetSubscriptionFields();
+    }
+
+    public function resetSubscriptionFields()
+    {
+        $this->subscription_exist = false;
+        $this->subscription_subscriptions = [];
+        $this->subscription_course_subscription_id = null;
+        $this->subscription_price = null;
+        $this->subscription_subscription_price = null;
+        $this->subscription_date_from = null;
+        $this->subscription_date_to = null;
+        $this->subscription_notes = null;
     }
 
     public function executeMultipleAction()
@@ -375,7 +406,6 @@ class Member extends Component
             }
 
             $this->course_subscriptions = \App\Models\CourseSubscription::select('*')->whereIn('id', $course_subscription_ids)->where('enabled', true)->get();
-
         } else {
             $this->course_price = 0;
             $this->course_subscription_price = 0;
@@ -554,12 +584,15 @@ class Member extends Component
             $this->showDetailF($_GET["member_detail"]);
             $this->refreshAfter = 1;
         }
+
+        $this->subscription_id = null;
+        $this->subscriptions = \App\Models\Subscription::where('enabled', 1)->get();
     }
 
-    public function boot(){
+    public function boot()
+    {
         $this->fileService = app(\App\Services\MemberFileService::class);
         app(abstract: TenantMiddleware::class)->setupTenantConnection();
-
     }
 
     public function updated()
@@ -776,6 +809,45 @@ class Member extends Component
         }
     }
 
+    public function updatedSubscriptionId()
+    {
+        $this->resetSubscriptionFields();
+
+        if ($this->subscription_id > 0 && $this->subscription_id != 'course') {
+            $subscription = \App\Models\Subscription::findOrFail($this->subscription_id);
+            $this->subscription_subscription_price = formatPrice($subscription->subscription_price);
+
+            // Controllo se l'utente ha già l'abbonamento
+            $this->subscription_exist = \App\Models\MemberSubscription::where('subscription_id', $this->subscription_id)->where('member_id', $this->dataId)->count() > 0;
+
+            $subscription_subscription_ids = [];
+            if ($subscription->prices != null) {
+                foreach (json_decode($subscription->prices) as $z) {
+                    $subscription_subscription_ids[] = $z->course_subscription_id;
+                }
+            }
+            $this->subscription_subscriptions = \App\Models\CourseSubscription::select('*')->whereIn('id', $subscription_subscription_ids)->where('enabled', true)->get();
+        } else {
+            $this->subscription_subscription_price = 0;
+            $this->subscription_exist = false;
+        }
+    }
+
+    public function updatedSubscriptionCourseSubscriptionId() {
+        if ($this->subscription_id > 0 && $this->subscription_id != 'course') {
+            $subscription = \App\Models\Subscription::findOrFail($this->subscription_id);
+
+            if ($subscription->prices != null) {
+                foreach (json_decode($subscription->prices) as $z) {
+                    if ($z->course_subscription_id == $this->subscription_course_subscription_id) {
+                        $this->subscription_price = $z->price;
+                        break;
+                    }
+                }
+            }
+        }
+    }
+
     public function loadMemberCards()
     {
         $this->member_cards = \App\Models\MemberCard::where('member_id', $this->dataId)->get();
@@ -784,11 +856,19 @@ class Member extends Component
     public function loadMemberCourses()
     {
         $this->member_courses = \App\Models\MemberCourse::where('member_id', $this->dataId)->get();
+        $this->member_subscriptions = \App\Models\MemberSubscription::where('member_id', $this->dataId)->get();
     }
 
     public function loadMemberCategories()
     {
-        $this->member_categories = \App\Models\MemberCategory::where('member_id', $this->dataId)->get();
+        $this->member_categories = [];
+        $categories = \App\Models\MemberCategory::with('category')->where('member_id', $this->dataId)->get();
+        foreach ($categories as $category) {
+            $this->member_categories[$category->category_id] = $category;
+        }
+        usort($this->member_categories, function ($a, $b) {
+            return strcmp($a->category->getTree(), $b->category->getTree());
+        });
     }
 
     public function loadMemberCertificates()
@@ -801,6 +881,14 @@ class Member extends Component
         if (!isset($_GET["from"]) && $this->from == '')
             $this->from = 'members';
         $this->currentMember = \App\Models\Member::findOrFail($id);
+        $categories = [];
+        foreach ($this->currentMember->categories as $category) {
+            $categories[$category->category_id] = $category;
+        }
+        usort($categories, function ($a, $b) {
+            return strcmp($a->category->getTree(), $b->category->getTree());
+        });
+        $this->currentMember->categories = $categories;
         $this->currentStatus = $this->currentMember->getStatus();
         $this->showDetail = true;
     }
@@ -895,7 +983,7 @@ class Member extends Component
             $rules['email'] = 'required';
             $rules['phone'] = 'required';
         }
-        
+
         $std_rules = $rules;
         // $rules = [
         //     'first_name' => 'required',
@@ -1184,15 +1272,15 @@ class Member extends Component
                 $this->age = $interval->y . " anni";
                 $this->under18 = $interval->y < 18;
                 $this->update = true;
-                $this->add = false;                
+                $this->add = false;
 
                 $this->emit('setIds', $this->nation_id, $this->birth_nation_id);
 
                 $this->emit('load-select');
-                
+
                 $this->emit('load-provinces', $this->nation_id, 'provinceClass');
                 $this->emit('load-provinces', $this->birth_nation_id, 'provinceBirthClass');
-                
+
                 $this->emit('load-cities', $this->province_id, 'cityClass');
                 $this->emit('load-cities', $this->birth_province_id, 'cityBirthClass');
 
@@ -1272,7 +1360,7 @@ class Member extends Component
             $rules['phone'] = 'required';
         }
 
-        
+
         $std_rules = $rules;
         // $rules = [
         //     'first_name' => 'required',
@@ -1653,19 +1741,32 @@ class Member extends Component
             $mc->save();
 
             // Se il corso ha associato una categoria iscrivo anche al gruppo
-            $c = \App\Models\Course::findOrFail($this->course_course_id);
-            if ($c) {
-                if ($c->category_id > 0) {
-                    \App\Models\MemberCategory::create([
-                        'member_id' => $this->dataId,
-                        'category_id' => $c->category_id,
-                        'date' => \Carbon\Carbon::now()
-                    ]);
+            if ($this->course_course_id) {
+                try {
+                    $c = \App\Models\Course::findOrFail($this->course_course_id);
+                    if ($c) {
+                        if ($c->category_id > 0) {
+                            if (!\App\Models\MemberCategory::where('member_id', $this->dataId)->where('category_id', $c->category_id)->first()) {
+                                \App\Models\MemberCategory::create([
+                                    'member_id' => $this->dataId,
+                                    'category_id' => $c->category_id,
+                                    'date' => \Carbon\Carbon::now()
+                                ]);
+                            }
+                        }
+                    }
+                } catch (\Throwable $e) {
                 }
             }
 
             // Creo i pagamenti in base alla tipologia
-            $r = \App\Models\CourseSubscription::findOrFail($this->course_course_subscription_id);
+            if ($this->course_course_subscription_id) {
+                try {
+                    $r = \App\Models\CourseSubscription::findOrFail($this->course_course_subscription_id);
+                } catch (\Throwable $e) {
+                    $r = new \App\Models\CourseSubscription();
+                }
+            }
 
             $start = $this->course_date_from;
 
@@ -1687,12 +1788,9 @@ class Member extends Component
                 $mms = [];
                 $mms[] = date("n", strtotime($start));
                 for ($jj = 1; $jj < $r->months; $jj++) {
-                    if (date('Ymd', strtotime("+" . $jj . " months", strtotime($start))) < date("Ymd", strtotime($this->course_date_to)))
-                    {
+                    if (date('Ymd', strtotime("+" . $jj . " months", strtotime($start))) < date("Ymd", strtotime($this->course_date_to))) {
                         $mms[] = date('n', strtotime("+" . $jj . " months", strtotime($start)));
-                    }
-                    else
-                    {
+                    } else {
                         break;
                     }
                 }
@@ -1710,7 +1808,7 @@ class Member extends Component
 
                 $start = date('Y-m-d', strtotime("+" . $r->months . " months", strtotime($start)));
 
-                if ($start > $this->course_date_to) {
+                if ($start > $this->course_date_to || $r->months == 0) {
                     $go = false;
                     break;
                 }
@@ -1753,7 +1851,7 @@ class Member extends Component
                 foreach ($all as $a) {
                     $types_ids[] = $a->course_type_id;
                 }
-                $this->course_types = \App\Models\CourseType::select('*')->where('enabled', true)->whereIn('id', $types_ids)->get();
+                // $this->course_types = \App\Models\CourseType::select('*')->where('enabled', true)->whereIn('id', $types_ids)->get();
 
                 $frequencies_ids = [];
                 $all = \App\Models\Course::where('name', 'like', '%' . $c->name . "%")->where('enabled', true)->where('course_level_id', $this->course_level_id)->where('course_type_id', $this->course_type_id)->get();
@@ -1790,10 +1888,13 @@ class Member extends Component
                 }
 
                 $this->course_subscriptions = \App\Models\CourseSubscription::select('*')->whereIn('id', $course_subscription_ids)->where('enabled', true)->get();
-                
+
                 $this->courseDataId = $memberCourse->id;
                 $this->updateCourse = true;
                 $this->addCourse = false;
+
+                $this->subscription_id = 'course';
+
                 $this->emit('setEditCorso', true);
             }
         } catch (\Exception $ex) {
@@ -1841,13 +1942,172 @@ class Member extends Component
     public function deleteCourse($id)
     {
         try {
-            \App\Models\MemberCourse::find($id)->delete();
+            $member_course = \App\Models\MemberCourse::find($id);
+
+            $category = \App\Models\Course::findOrFail($member_course->course_id);
+            if ($category) {
+                if ($category->category_id > 0) {
+                    \App\Models\MemberCategory::where('member_id', $this->dataId)->where('category_id', $category->category_id)->delete();
+                }
+            }
+
+            $member_course->delete();
             session()->flash('success', "Corso eliminato");
         } catch (\Exception $e) {
             session()->flash('error', 'Errore (' . $e->getMessage() . ')');
         }
     }
 
+    public function storeSubscription()
+    {
+        $this->validate(['subscription_id' => 'required']);
+        try {
+            $ms = new \App\Models\MemberSubscription();
+            $ms->member_id = $this->dataId;
+            $ms->subscription_id = $this->subscription_id;
+            $ms->date_from = $this->subscription_date_from;
+            $ms->date_to = $this->subscription_date_to;
+            $ms->course_subscription_id = $this->subscription_course_subscription_id;
+            $ms->price = currencyToDouble($this->subscription_price);
+            $ms->subscription_price = currencyToDouble($this->subscription_subscription_price);
+            $ms->notes = $this->subscription_notes;
+            $ms->save();
+
+            // Creo i pagamenti in base alla tipologia
+            if ($this->subscription_course_subscription_id) {
+                try {
+                    $r = \App\Models\CourseSubscription::findOrFail($this->subscription_course_subscription_id);
+                } catch (\Throwable $e) {
+                    $r = new \App\Models\CourseSubscription();
+                }
+            }
+
+            $start = $this->subscription_date_from;
+
+            // Creo il pagamento per l'iscrizione
+            $rate = new \App\Models\Rate();
+            $rate->member_id = $this->dataId;
+            $rate->member_subscription_id = $ms->id;
+            $rate->price = currencyToDouble($this->subscription_subscription_price);
+            $rate->date = $start;
+            $rate->months = json_encode([]);
+            $rate->note = '';
+            $rate->status = 0;
+            $rate->is_subscription = true;
+            $rate->save();
+            $go = true;
+            while ($go) {
+
+                $mms = [];
+                $mms[] = date("n", strtotime($start));
+                for ($jj = 1; $jj < $r->months; $jj++) {
+                    if (date('Ymd', strtotime("+" . $jj . " months", strtotime($start))) < date("Ymd", strtotime($this->subscription_date_to))) {
+                        $mms[] = date('n', strtotime("+" . $jj . " months", strtotime($start)));
+                    } else {
+                        break;
+                    }
+                }
+
+                $rate = new \App\Models\Rate();
+                $rate->member_id = $this->dataId;
+                $rate->member_subscription_id = $ms->id;
+                $rate->course_subscription_id = $this->subscription_course_subscription_id;
+                $rate->price = currencyToDouble($this->subscription_price);
+                $rate->date = $start;
+                $rate->months = json_encode($mms);
+                $rate->note = '';
+                $rate->status = 0;
+                $rate->save();
+
+                $start = date('Y-m-d', strtotime("+" . $r->months . " months", strtotime($start)));
+
+                if ($start > $this->subscription_date_to || $r->months == 0) {
+                    $go = false;
+                    break;
+                }
+            }
+
+            session()->flash('success, Abbonamento creato');
+            $this->resetCourseFields();
+            $this->addCourse = false;
+            $this->emit('setEditCorso', false);
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function editSubscription($id)
+    {
+        try {
+            $memberSubscription = \App\Models\MemberSubscription::findOrFail($id);
+            if (!$memberSubscription) {
+                session()->flash('error', 'Abbonamento non trovato');
+            } else {
+                $this->subscription_id = $memberSubscription->subscription_id;
+
+                $subscription = \App\Models\Subscription::findOrFail($this->subscription_id);
+
+                $this->subscription_date_from = $memberSubscription->date_from;
+                $this->subscription_date_to = $memberSubscription->date_to;
+                $this->subscription_course_subscription_id = $memberSubscription->course_subscription_id;
+                $this->subscription_price = formatPrice($memberSubscription->price);
+                $this->subscription_subscription_price = formatPrice($memberSubscription->subscription_price);
+                $this->subscription_notes = $memberSubscription->notes;
+
+                $subscription_subscription_ids = [];
+                if ($subscription->prices != null) {
+                    foreach (json_decode($subscription->prices) as $z) {
+                        $subscription_subscription_ids[] = $z->course_subscription_id;
+                    }
+                }
+
+                $this->subscription_subscriptions = \App\Models\CourseSubscription::select('*')->whereIn('id', $subscription_subscription_ids)->where('enabled', true)->get();
+
+                $this->courseDataId = $memberSubscription->id;
+                $this->updateCourse = true;
+                $this->addCourse = false;
+
+                $this->emit('setEditCorso', true);
+            }
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function updateSubscription()
+    {
+        $this->validate(['subscription_id' => 'required']);
+        try {
+            \App\Models\MemberSubscription::whereId($this->courseDataId)->update([
+                'member_id' => $this->dataId,
+                'subscription_id' => $this->subscription_id,
+                'date_from' => $this->subscription_date_from,
+                'date_to' => $this->subscription_date_to,
+                'course_subscription_id' => $this->subscription_course_subscription_id,
+                'price' => currencyToDouble($this->subscription_price),
+                'subscription_price' => currencyToDouble($this->subscription_subscription_price),
+                'notes' => $this->subscription_notes,
+            ]);
+            session()->flash('success', 'Abbonamento aggiornato');
+            $this->resetCourseFields();
+            $this->updateCourse = false;
+            $this->emit('setEditCorso', false);
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function deleteSubscription($id)
+    {
+        try {
+            $member_subscription = \App\Models\MemberSubscription::find($id);
+            $member_subscription->delete();
+            session()->flash('success', "Abbonamento eliminato");
+        } catch (\Exception $e) {
+            session()->flash('error', 'Errore (' . $e->getMessage() . ')');
+        }
+    }
+
     public function addRow()
     {
         $this->course_when[] = array('day' => array(), 'from' => '', 'to' => '');
@@ -2003,7 +2263,7 @@ class Member extends Component
         $this->groupMsg = '';
         try {
             if (\App\Models\MemberCategory::where('member_id', $this->dataId)->where('category_id', $id)->first()) {
-                $this->groupMsg = '<br>Attenzione, questo corso è stato già inserito';
+                $this->groupMsg = '<br>Attenzione, l\'utente fa già parte di questo gruppo';
             } else {
                 \App\Models\MemberCategory::create([
                     'member_id' => $this->dataId,
@@ -2385,58 +2645,9 @@ class Member extends Component
         try {
             $member = \App\Models\Member::findOrFail($id);
 
-            $originalFirstName = $member->first_name;
-            $originalLastName = $member->last_name;
-
             $member->update([
                 'is_archived' => true,
                 'archived_date' => now(),
-
-                'first_name' => 'Nome Archiviato',
-                'last_name' => 'Cognome Archiviato',
-
-                'fiscal_code' => null,
-                'email' => null,
-                'phone' => null,
-                'phone2' => null,
-                'phone3' => null,
-                'address' => null,
-                'zip_code' => null,
-
-                'birth_place' => null,
-                'birth_date' => null,
-                'birth_city_id' => null,
-                'birth_province_id' => null,
-                'birth_nation_id' => null,
-
-                'nation_id' => null,
-                'province_id' => null,
-                'city_id' => null,
-
-                'document_type' => null,
-                'document_number' => null,
-                'document_from' => null,
-                'document_expire_date' => null,
-                'document_files' => null,
-
-                'father_name' => null,
-                'mother_name' => null,
-                'father_email' => null,
-                'mother_email' => null,
-                'father_phone' => null,
-                'mother_phone' => null,
-                'father_fiscal_code' => null,
-                'mother_fiscal_code' => null,
-                'father_doc_number' => null,
-                'father_doc_type' => null,
-                'mother_doc_number' => null,
-                'mother_doc_type' => null,
-                'father_document_files' => null,
-                'mother_document_files' => null,
-
-                'image' => null,
-
-                'gender' => null,
                 'status' => 'archived',
 
                 'enabled' => false
@@ -2447,7 +2658,6 @@ class Member extends Component
             session()->flash('success', 'Membro archiviato con successo');
 
             $this->emit('reload');
-
         } catch (\Exception $e) {
             session()->flash('error', 'Errore durante l\'archiviazione: ' . $e->getMessage());
             Log::error('Archive member error', [

+ 138 - 0
app/Http/Livewire/MemberArchive.php

@@ -0,0 +1,138 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+use Illuminate\Support\Facades\Log;
+use App\Http\Middleware\TenantMiddleware;
+
+use DateTime;
+
+class MemberArchive extends Component
+{
+    public $categories = [];
+
+    public function boot(){
+        app(abstract: TenantMiddleware::class)->setupTenantConnection();
+    }
+
+    public function mount()
+    {
+        $this->categories = [];
+        $this->getCategories(\App\Models\Category::select('id', 'name')->where('parent_id', null)->orderBy('name')->get(), 0);
+    }
+
+    public function render()
+    {
+        return view('livewire.member_archive');
+    }
+
+    public function getCategories($records, $indentation)
+    {
+        foreach ($records as $record) {
+            $this->categories[] = array('id' => $record->id, 'name' => $record->getTree());
+            if (count($record->childs))
+                $this->getCategories($record->childs, $indentation + 1);
+        }
+    }
+    public function restore($id)
+    {
+        try {
+            $member = \App\Models\Member::findOrFail($id);
+
+            $member->update([
+                'is_archived' => false,
+                'archived_date' => null,
+                'status' => '',
+                'enabled' => true
+            ]);
+
+            updateMemberData($id);
+
+            session()->flash('success', 'Membro riattivato con successo');
+
+            $this->emit('reload');
+        } catch (\Exception $e) {
+            session()->flash('error', 'Errore durante la riattivazione: ' . $e->getMessage());
+            Log::error('Restore member error', [
+                'member_id' => $id,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+        }
+    }
+
+    public function archive($id)
+    {
+        try {
+            $member = \App\Models\Member::findOrFail($id);
+
+            $member->update([
+                'is_deleted' => true,
+                'deleted_date' => now(),
+
+                'first_name' => 'Nome Archiviato',
+                'last_name' => 'Cognome Archiviato',
+
+                'fiscal_code' => null,
+                'email' => null,
+                'phone' => null,
+                'phone2' => null,
+                'phone3' => null,
+                'address' => null,
+                'zip_code' => null,
+
+                'birth_place' => null,
+                'birth_date' => null,
+                'birth_city_id' => null,
+                'birth_province_id' => null,
+                'birth_nation_id' => null,
+
+                'nation_id' => null,
+                'province_id' => null,
+                'city_id' => null,
+
+                'document_type' => null,
+                'document_number' => null,
+                'document_from' => null,
+                'document_expire_date' => null,
+                'document_files' => null,
+
+                'father_name' => null,
+                'mother_name' => null,
+                'father_email' => null,
+                'mother_email' => null,
+                'father_phone' => null,
+                'mother_phone' => null,
+                'father_fiscal_code' => null,
+                'mother_fiscal_code' => null,
+                'father_doc_number' => null,
+                'father_doc_type' => null,
+                'mother_doc_number' => null,
+                'mother_doc_type' => null,
+                'father_document_files' => null,
+                'mother_document_files' => null,
+
+                'image' => null,
+
+                'gender' => null,
+                'status' => 'archived',
+
+                'enabled' => false
+            ]);
+
+            updateMemberData($id);
+
+            session()->flash('success', 'Membro archiviato con successo');
+
+            $this->emit('reload');
+        } catch (\Exception $e) {
+            session()->flash('error', 'Errore durante l\'archiviazione: ' . $e->getMessage());
+            Log::error('Archive member error', [
+                'member_id' => $id,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+        }
+    }
+}

+ 21 - 2
app/Http/Livewire/Profile.php

@@ -20,6 +20,7 @@ class Profile extends Component
     public $telefono;
     public $cellulare;
     public $password;
+    public $password_confirmation;
 
     public function boot()
     {
@@ -44,12 +45,28 @@ class Profile extends Component
 
     public function save()
     {
-        $this->validate([
+
+        $rules = [
             'name' => 'required',
             'cognome' => 'required',
             'email' => 'required|email',
             'password' => 'nullable|min:6',
-        ]);
+            'password_confirmation' => 'nullable|same:password'
+        ];
+
+        $messages = [
+            'name.required' => 'Il nome è obbligatorio',
+            'cognome.required' => 'Il cognome è obbligatorio',
+            'email.required' => 'La mail è obbligatoria',
+            'email.email' => 'La mail deve essere un indirizzo valido',
+            'email.unique' => 'Questa mail è già stata utilizzata',
+            'password.required' => 'La password è obbligatoria',
+            'password.min' => 'La password deve essere di almeno 6 caratteri',
+            'password_confirmation.required' => 'Ripeti la password inserita',
+            'password_confirmation.same' => 'Le password non coincidono',
+        ];
+
+        $this->validate($rules, $messages);
 
         $currentUser = Auth::user();
 
@@ -102,6 +119,7 @@ class Profile extends Component
 
             $this->editMode = false;
             $this->password = ''; // Clear password field
+            $this->password_confirmation = ''; // Clear password_confirmation field
 
         } catch (\Exception $e) {
             Log::error('Profile update failed', [
@@ -203,6 +221,7 @@ class Profile extends Component
         $this->telefono = '';
         $this->cellulare = '';
         $this->password = '';
+        $this->password_confirmation = '';
     }
 
     public function render()

+ 38 - 22
app/Http/Livewire/Rate.php

@@ -15,8 +15,11 @@ class Rate extends Component
 
     public $member_id = 0;
     public $member_course_id = 0;
+    public $member_subscription_id = 0;
+    public $rate_from = 'course';
 
     public $mc = null;
+    public $ms = null;
 
     public $filterStatus = '';
     public $hasFilter = false;
@@ -65,10 +68,11 @@ class Rate extends Component
         // Load members for the dropdown
         $this->member_id = isset($_GET["member_id"]) ? $_GET["member_id"] : 0;
         $this->member_course_id = isset($_GET["member_course_id"]) ? $_GET["member_course_id"] : 0;
+
         if ($this->member_id > 0 && $this->member_course_id > 0)
         {
             $this->mc = \App\Models\MemberCourse::findOrFail($this->member_course_id);
-            $this->detail = $this->mc->member->first_name . " " . $this->mc->member->last_name;
+            $this->detail = $this->mc->course->getDetailsName() . "<br/>" .  $this->mc->member->first_name . " " . $this->mc->member->last_name;
 
             $this->price_list = [];
             $c = $this->mc->course;
@@ -89,34 +93,47 @@ class Rate extends Component
             }
 
             $this->course_subscriptions = \App\Models\CourseSubscription::select('*')->whereIn('id', $course_subscription_ids)->where('enabled', true)->get();
+
+            $this->rate_from = 'course';
         }
 
-        //$this->course_subscriptions = \App\Models\CourseSubscription::select('*')->where('enabled', true)->get();
+        $this->member_subscription_id = isset($_GET["member_subscription_id"]) ? $_GET["member_subscription_id"] : 0;
+        if ($this->member_id > 0 && $this->member_subscription_id > 0)
+        {
+            $this->ms = \App\Models\MemberSubscription::findOrFail($this->member_subscription_id);
+            $this->detail = $this->ms->subscription->name . "<br/>" .  $this->ms->member->first_name . " " . $this->ms->member->last_name;
 
+            $this->price_list = [];
+            $s = $this->ms->subscription;
+            if ($s->prices != null)
+            {
+                foreach(json_decode($s->prices) as $z)
+                {
+                    $this->price_list[$z->course_subscription_id] = $z->price;
+                }
+            }
 
+            $course_subscription_ids = [];
+            if ($s->prices != null) {
+                foreach (json_decode($s->prices) as $z) {
+                    if ($z->price > 0)
+                        $course_subscription_ids[] = $z->course_subscription_id;
+                }
+            }
+
+            $this->course_subscriptions = \App\Models\CourseSubscription::select('*')->whereIn('id', $course_subscription_ids)->where('enabled', true)->get();
+            
+            $this->rate_from = 'subscription';
+        }
     }
 
     public function render()
     {
-
-        /*if ($this->hasFilter)
-        {
-            $r = \App\Models\Receipt::with('member');
-            if ($this->filterStatus != '')
-                $r = $r->where('status', $this->filterStatus);
-            if ($this->filterFrom != '')
-                $r = $r->where('date', '>=', $this->filterFrom);
-            if ($this->filterTo != '')
-                $r = $r->where('date', '<=', $this->filterTo);
-            if ($this->filteredMemberId != '')
-                $r = $r->where('member_id', $this->filteredMemberId);
-
-            $this->records = $r->get();
-        }
-        else
-        {*/
+        if ($this->member_id > 0 && $this->member_course_id > 0) {
             $this->records = \App\Models\Rate::with('member')->where('member_course_id', $this->member_course_id)->where('member_id', $this->member_id)->orderBy('date')->get();
-        //}
+        } elseif ($this->member_id > 0 && $this->member_subscription_id > 0) {
+            $this->records = \App\Models\Rate::with('member')->where('member_subscription_id', $this->member_subscription_id)->where('member_id', $this->member_id)->orderBy('date')->get();
+        }
 
         $this->disabled = [];
         foreach($this->records as $r)
@@ -126,8 +143,6 @@ class Rate extends Component
             }
         }
 
-        //$this->emit('load-data-table');
-
         return view('livewire.rate');
     }
 
@@ -171,6 +186,7 @@ class Rate extends Component
                     $rate = new \App\Models\Rate();
                     $rate->member_id = $this->member_id;
                     $rate->member_course_id = $this->member_course_id;
+                    $rate->member_subscription_id = $this->member_subscription_id;
                     $rate->course_subscription_id = $this->course_subscription_id;
                     $rate->price = currencyToDouble($this->price);
                     $rate->date = $this->date;

+ 205 - 40
app/Http/Livewire/RecordIN.php

@@ -10,6 +10,7 @@ use Illuminate\Support\Facades\Mail;
 use Illuminate\Contracts\Validation\Validator;
 use Illuminate\Support\Facades\Log;
 use App\Http\Middleware\TenantMiddleware;
+use Illuminate\Support\Carbon;
 
 class RecordIN extends Component
 {
@@ -49,9 +50,11 @@ class RecordIN extends Component
     }
 
     public $courseId = 0;
+    public $subscriptionId = 0;
+    public $course = null;
+    public $subscription = null;
     public $rateId = 0;
     public $months = array();
-    
 
     public $records, $dataId, $member_id, $supplier_id,
         $causal_id,
@@ -75,7 +78,7 @@ class RecordIN extends Component
 
     public $currentReceip;
 
-    public $filterMember = 0, $filterPaymentMethod = 0, $filterCausals = 0, $filterFrom = '', $filterTo = '', $filterCommercial = 0;
+    public $filterMember = 0, $filterPaymentMethod = 0, $filterCausals = 0, $filterCourses = 0, $filterFrom = '', $filterTo = '', $filterCommercial = 0;
 
     public $hasFilter = false;
 
@@ -100,6 +103,8 @@ class RecordIN extends Component
     public $newMemberFiscalCodeExist = false;
 
     public $causals = array();
+    public $courses = array();
+    public $subscriptions = array();
     public $payments = array();
     public $banks = array();
     public $members = array();
@@ -109,6 +114,8 @@ class RecordIN extends Component
 
     public $fromCourse = false;
 
+    public $backToRates = false;
+
     protected $rules = [
         'member_id' => 'required',
         'payment_method_id' => 'required',
@@ -164,6 +171,15 @@ class RecordIN extends Component
     public function hydrate()
     {
         $this->emit('load-select');
+
+        foreach ($this->rows as $i => $row) {
+            if (isset($row['course']) && is_array($row['course']) && isset($row['course']['id'])) {
+                $this->rows[$i]['course'] = \App\Models\Course::find($row['course']['id']);
+            }
+            if (isset($row['subscription']) && is_array($row['subscription']) && isset($row['subscription']['id'])) {
+                $this->rows[$i]['subscription'] = \App\Models\Subscription::find($row['subscription']['id']);
+            }
+        }
     }
 
     /*public function updated() {
@@ -261,10 +277,12 @@ class RecordIN extends Component
         $this->currentReceip = null;
         $this->parent = '';
         $this->courseId = 0;
+        $this->subscriptionId = 0;
         $this->rateId = 0;
+        $this->backToRates = false;
         $this->months = array();
         $this->rows = array();
-        $this->rows[] = array('causal_id' => isset($_GET["causalId"]) ? $_GET["causalId"] : null, 'when' => array(array('month' => date("n"), 'year' => date("Y"), 'period' => '')),  'amount' => null, 'vat_id' => null, 'note' => '', 'commercial' => 0, 'sconto' => 0);
+        $this->rows[] = array('causal_id' => isset($_GET["causalId"]) ? $_GET["causalId"] : null, 'course_id' => null, 'course' => null, 'subscription_id' => null, 'subscription' => null, 'when' => array(array('month' => date("n"), 'year' => date("Y"), 'period' => '')),  'amount' => null, 'vat_id' => null, 'note' => '', 'commercial' => 0, 'sconto' => 0);
         $this->corrispettivo = [];
         $this->emit('load-data-table');
     }
@@ -331,6 +349,22 @@ class RecordIN extends Component
         }
     }
 
+    public function getCourses($records, $indentation)
+    {
+        foreach ($records as $record) {
+            $this->courses[] = array('id' => $record->id, 'name' => $record->getDetailsName(), 'text' => $record->getDetailsName(), 'level' => $indentation);
+            if (count($record->childs))
+                $this->getCourses($record->childs, $indentation + 1);
+        }
+    }
+
+    public function getSubscriptions($records, $indentation)
+    {
+        foreach ($records as $record) {
+            $this->subscriptions[] = array('id' => $record->id, 'name' => $record->name, 'text' => $record->name, 'level' => $indentation);
+        }
+    }
+
     public function mount()
     {
 
@@ -339,6 +373,8 @@ class RecordIN extends Component
         }
 
         $this->causals = array();
+        $this->courses = array();
+        $this->subscriptions = array();
 
         $this->multiMonthFrom = date("n");
         $this->multiYearFrom = date("Y");
@@ -351,6 +387,9 @@ class RecordIN extends Component
 
         $this->getCausale(\App\Models\Causal::select('id', 'name')->where('parent_id', null)->where('type', 'IN')->get(), 0);
 
+        $this->getCourses(\App\Models\Course::where('parent_id', null)->get(), 0);
+        $this->getSubscriptions(\App\Models\Subscription::get(), 0);
+
         //$this->buildTree(\App\Models\Causal::all(), null);
 
         $this->refreshMembers();
@@ -385,14 +424,28 @@ class RecordIN extends Component
                 $this->edit($_GET["id"]);
             }
             $count = 0;
-            if (isset($_GET["courseId"])) {
+            if (isset($_GET["courseId"]) && $_GET["courseId"] > 0) {
+                $this->fromCourse = true;
                 $this->courseId = $_GET["courseId"];
                 $mc = \App\Models\MemberCourse::findOrFail($this->courseId);
-                $course = \App\Models\Course::findOrFail($mc->course_id);
+                $this->course = \App\Models\Course::findOrFail($mc->course_id);
+                $this->rows[0]['course_id'] = $this->course->id;
+                $this->rows[0]['course'] = $this->course;
+            }
+            if (isset($_GET["subscriptionId"]) && $_GET["subscriptionId"] > 0) {
+                $this->fromCourse = true;
+                $this->subscriptionId = $_GET["subscriptionId"];
+                $ms = \App\Models\MemberSubscription::findOrFail($this->subscriptionId);
+                $this->subscription = \App\Models\Subscription::findOrFail($ms->subscription_id);
+                $this->rows[0]['subscription_id'] = $this->subscription->id;
+                $this->rows[0]['subscription'] = $this->subscription;
             }
             if (isset($_GET["rateId"])) {
                 $this->rateId = $_GET["rateId"];
             }
+            if (isset($_GET["backToRates"])) {
+                $this->backToRates = true;
+            }
             if (isset($_GET["months"]) && $_GET["months"] != '') {
 
                 $this->fromCourse = true;
@@ -404,37 +457,62 @@ class RecordIN extends Component
                 $this->months = $months;
                 //$this->rows[0]["amount"] = formatPrice($price * sizeof($months));
                 $this->rows[0]["amount"] = formatPrice($price);
-                foreach ($months as $idx => $m) {
-                    $this->rows[0]["when"][$idx]["month"] = $m;
-
-                    if ($m >= date("m"))
-                        $this->rows[0]["when"][$idx]["year"] = date("Y");
-                    else
-                        $this->rows[0]["when"][$idx]["year"] = date("Y") + 1;
-                    /*if (env('FISCAL_YEAR_MONTH_FROM', 1) > 1)
-                    {
-                        if ($m > date("M"))
-                            $this->rows[0]["when"][$idx]["year"] = $m < env('FISCAL_YEAR_MONTH_FROM', 1) ? (date("Y")) : date("Y" + 1);
-                        else
-                            $this->rows[0]["when"][$idx]["year"] = $m > env('FISCAL_YEAR_MONTH_TO', 1) ? (date("Y")) : date("Y" + 1);
-                    }
-                    else
-                    {
 
+                $rates = [];
+                if (isset($_GET["rateId"])) {
+                    try {
+                        $rateIds = explode(",", $_GET["rateId"]);
+                        $rates = \App\Models\Rate::whereIn('id', $rateIds)->orderBy('date', 'asc')->get();
+                    } catch (\Throwable $e) {
                     }
 
-                    if ($m > date("M"))
-                        $this->rows[0]["when"][$idx]["year"] = $m < env('FISCAL_YEAR_MONTH_FROM', 1) ? (date("Y")) : date("Y" + 1);
-                    else
-                        $this->rows[0]["when"][$idx]["year"] = $m > env('FISCAL_YEAR_MONTH_TO', 1) ? (date("Y")) : date("Y" + 1);
-                    */
-                    if ($idx > 0) {
-                        if ($this->rows[0]["when"][$idx - 1]["year"] != $this->rows[0]["when"][$idx]["year"])
-                            $desc .= " " . $this->rows[0]["when"][$idx - 1]["year"] . " ";
+                    $order = [9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7, 8];
+                    $index = 0;
+                    foreach ($rates as $rate) {
+                        $rate_date = Carbon::createFromFormat("Y-m-d H:i:s", $rate->date);
+
+                        $rate_months = json_decode($rate->months, true);
+                        usort($rate_months, function ($a, $b) use ($order) {
+                            $posA = array_search($a, $order);
+                            $posB = array_search($b, $order);
+                            return $posA <=> $posB;
+                        });
+
+                        foreach ($rate_months as $month) {
+                            $rate_year = $rate_date->year;
+                            $rate_month = $rate_date->month;
+                            if ($rate_month < 9 && $rate_month >= 1) {
+                                $rate_year -= 1;
+                            }
+
+                            if ($month >= 9 && $month <= 12) {
+                            } elseif ($month < 9 && $month >= 1) {
+                                $rate_year += 1;
+                            }
+
+                            $this->rows[0]["when"][$index]["month"] = $month;
+                            $this->rows[0]["when"][$index]["year"] = $rate_year;
+                            $index++;
+
+                            $desc .= " " . $this->getMonth($month) . " " . $rate_year;
+                        }
                     }
-                    $desc .= " " . $this->getMonth($m); // . " " . $this->rows[0]["when"][$idx]["year"];
                 }
-                $desc .= " " . $this->rows[0]["when"][$idx]["year"];
+
+                // foreach ($months as $idx => $m) {
+                //     $this->rows[0]["when"][$idx]["month"] = $m;
+                //     if ($m >= date("m"))
+                //         $this->rows[0]["when"][$idx]["year"] = date("Y");
+                //     else
+                //         $this->rows[0]["when"][$idx]["year"] = date("Y") + 1;
+
+                //     if ($idx > 0) {
+                //         if ($this->rows[0]["when"][$idx - 1]["year"] != $this->rows[0]["when"][$idx]["year"])
+                //             $desc .= " " . $this->rows[0]["when"][$idx - 1]["year"] . " ";
+                //     }
+                //     $desc .= " " . $this->getMonth($m); // . " " . $this->rows[0]["when"][$idx]["year"];
+                // }
+                // $desc .= " " . $this->rows[0]["when"][$idx]["year"];
                 $this->rows[0]["note"] = $desc;
                 $count += 1;
             }
@@ -443,15 +521,38 @@ class RecordIN extends Component
             if (isset($_GET["createSubscription"]) && $_GET["createSubscription"] == 1) {
 
                 $this->createSubscription = 1;
-                $this->courseId = $_GET["courseId"];
+
+                if (isset($_GET["courseId"]) && $_GET["courseId"] > 0) {
+                    $this->courseId = $_GET["courseId"];
+                }
+                if (isset($_GET["subscriptionId"]) && $_GET["subscriptionId"] > 0) {
+                    $this->subscriptionId = $_GET["subscriptionId"];
+                }
+
+                if ($this->courseId && $this->courseId > 0) {
+                    $mc = \App\Models\MemberCourse::findOrFail($this->courseId);
+                    $this->course = \App\Models\Course::findOrFail($mc->course_id);
+                }
+                if ($this->subscriptionId && $this->subscriptionId > 0) {
+                    $ms = \App\Models\MemberSubscription::findOrFail($this->subscriptionId);
+                    $this->subscription = \App\Models\Subscription::findOrFail($ms->subscription_id);
+                }
                 $this->rateId = $_GET["rateId"];
                 $price = $_GET["subscription_price"] / 100 * 100;
                 $this->refreshAfter = 1;
                 if ($count == 1)
-                    $this->rows[] = array('causal_id' => null, 'when' => array(array('month' => date("n"), 'year' => date("Y"), 'period' => '')),  'amount' => null, 'vat_id' => null, 'note' => '', 'commercial' => 0);
+                    $this->rows[] = array('causal_id' => null, 'course_id' => null, 'course' => null, 'subscription_id' => null, 'subscription' => null, 'when' => array(array('month' => date("n"), 'year' => date("Y"), 'period' => '')),  'amount' => null, 'vat_id' => null, 'note' => '', 'commercial' => 0);
                 $this->rows[$count]["causal_id"] = null;
+                $this->rows[$count]["course_id"] = $this->course->id ?? null;
+                $this->rows[$count]["course"] = $this->course ?? null;
+                $this->rows[$count]["subscription_id"] = $this->subscription->id ?? null;
+                $this->rows[$count]["subscription"] = $this->subscription ?? null;
                 $this->rows[$count]["amount"] = formatPrice($price);
-                $this->rows[$count]["note"] = "Pagamento iscrizione " . $course->name;
+                $note = implode(", ", array_filter([
+                    $this->course ? $this->course->name : "",
+                    $this->subscription ? $this->subscription->name : ""
+                ]));
+                $this->rows[$count]["note"] = "Pagamento iscrizione " . $note;
                 if (isset($_GET["subCausalId"])) {
                     $this->refreshAfter = 1;
                     $this->rows[$count]["causal_id"] = $_GET["subCausalId"];
@@ -471,6 +572,8 @@ class RecordIN extends Component
         $this->filterMember = 0;
         $this->filterPaymentMethod = 0;
         $this->filterCausals = 0;
+        $this->filterCourses = 0;
+        $this->filterSubscriptions = 0;
         $this->filterTo = '';
         $this->filterFrom = '';
         $this->filterCommercial = 0;
@@ -480,7 +583,6 @@ class RecordIN extends Component
 
     public function render()
     {
-
         $datas = [];
         return view('livewire.records_in', ['datas' => $datas]);
     }
@@ -537,10 +639,21 @@ class RecordIN extends Component
                         'financial_movement' => $this->financial_movement,
                         'deleted' => $this->deleted
                     ]);
+                        
+                    if ($this->courseId && $this->courseId > 0) {
+                        $mc = \App\Models\MemberCourse::findOrFail($this->courseId);
+                        $this->course = \App\Models\Course::findOrFail($mc->course_id);
+                    }
+                    if ($this->subscriptionId && $this->subscriptionId > 0) {
+                        $ms = \App\Models\MemberSubscription::findOrFail($this->subscriptionId);
+                        $this->subscription = \App\Models\Subscription::findOrFail($ms->subscription_id);
+                    }
 
                     \App\Models\RecordRow::create([
                         'record_id' => $record->id,
                         'causal_id' => $this->corrispettivo_causal_id,
+                        'course_id' => $this->course->id ?? null,
+                        'subscription_id' => $this->subscription->id ?? null,
                         'note' => '',
                         'amount' => $price,
                         'vat_id' => null,
@@ -660,6 +773,8 @@ class RecordIN extends Component
                 \App\Models\RecordRow::create([
                     'record_id' => $this->dataId,
                     'causal_id' => $row["causal_id"],
+                    'course_id' => $row["course_id"],
+                    'subscription_id' => $row["subscription_id"],
                     'note' => $row["note"],
                     'amount' => $rowNet,
                     'prediscount_amount' => $rowAmount,
@@ -699,22 +814,27 @@ class RecordIN extends Component
             }
 
             if ($this->rateId > 0) {
-
                 $rates = explode(",", $this->rateId);
-                foreach($rates as $rid)
-                {
+                foreach ($rates as $rid) {
                     $rate = \App\Models\Rate::findOrFail($rid);
                     $rate->status = 1;
                     $rate->record_id = $record->id;
                     $rate->save();
                 }
+            }
 
+            if ($this->backToRates) {
+                if ($this->courseId > 0)
+                    return redirect()->to("/rates?member_id={$this->member_id}&member_course_id={$this->courseId}");
+                if ($this->subscriptionId > 0)
+                    return redirect()->to("/rates?member_id={$this->member_id}&member_subscription_id={$this->subscriptionId}");
             }
 
             session()->flash('success', 'Movimento creato');
             $this->resetFields();
             $this->add = false;
             $this->isDuplicate = false;
+
             $this->emit('setEdit', false);
         } catch (\Exception $ex) {
             Log::error("Error in store method: " . $ex->getMessage());
@@ -789,7 +909,7 @@ class RecordIN extends Component
                 $this->add = false;
 
                 $rows = \App\Models\RecordRow::where('record_id', $this->dataId)
-                    ->select('causal_id', 'note', 'commercial', 'when', 'amount', 'prediscount_amount', 'sconto', 'vat_id')
+                    ->select('causal_id', 'course_id', 'subscription_id', 'note', 'commercial', 'when', 'amount', 'prediscount_amount', 'sconto', 'vat_id')
                     ->get();
 
                 $this->rows = [];
@@ -804,8 +924,28 @@ class RecordIN extends Component
                         }
                     }
 
+                    $course = null;
+                    if ($r->course_id) {
+                        try {
+                            $course = \App\Models\Course::findOrFail($r->course_id);
+                        } catch (\Throwable $th) {
+                        }
+                    }
+
+                    $subscription = null;
+                    if ($r->subscription_id) {
+                        try {
+                            $subscription = \App\Models\Subscription::findOrFail($r->subscription_id);
+                        } catch (\Throwable $th) {
+                        }
+                    }
+
                     $this->rows[] = [
                         'causal_id' => $causalId,
+                        'course_id' => $r->course_id,
+                        'course' => $course,
+                        'subscription_id' => $r->subscription_id,
+                        'subscription' => $subscription,
                         'note' => $r->note,
                         'commercial' => $r->commercial,
                         'when' => json_decode($r->when),
@@ -971,6 +1111,8 @@ class RecordIN extends Component
                 \App\Models\RecordRow::create([
                     'record_id' => $this->dataId,
                     'causal_id' => $row["causal_id"],
+                    'course_id' => $row["course_id"],
+                    'subscription_id' => $row["subscription_id"],
                     'note' => $row["note"],
                     'vat_id' => $row["vat_id"],
                     'amount' => $rowNet,
@@ -997,6 +1139,14 @@ class RecordIN extends Component
                 }
             }
             session()->flash('success', 'Movimento aggiornato');
+
+            if ($this->backToRates) {
+                if ($this->courseId > 0)
+                    return redirect()->to("/rates?member_id={$this->member_id}&member_course_id={$this->courseId}");
+                if ($this->subscriptionId > 0)
+                    return redirect()->to("/rates?member_id={$this->member_id}&member_subscription_id={$this->subscriptionId}");
+            }
+
             $this->resetFields();
             $this->update = false;
             $this->isDuplicate = false;
@@ -1088,6 +1238,19 @@ class RecordIN extends Component
                 $c->save();
             }
 
+            if ($r->member_subscription_id > 0) {
+                $months = json_decode($r->months);
+                $s = \App\Models\MemberSubscription::findOrFail($r->member_subscription_id);
+                $xxx = json_decode($s->months);
+                foreach ($xxx as $idx => $mm) {
+                    if (in_array($mm->m, $months)) {
+                        $xxx[$idx]->status = "";
+                    }
+                }
+                $s->months = json_encode($xxx);
+                $s->save();
+            }
+
             $r->deleted = true;
             $r->save();
 
@@ -1218,6 +1381,8 @@ class RecordIN extends Component
                     \App\Models\ReceiptRow::create([
                         'receip_id' => $receipt->id,
                         'causal_id' => $row["causal_id"],
+                        'course_id' => $row["course_id"],
+                        'subscription_id' => $row["subscription_id"],
                         'note' => $row["note"],
                         'vat_id' => $row["vat_id"],
                         'amount' => $rowNet,
@@ -1259,7 +1424,7 @@ class RecordIN extends Component
 
     public function addRow()
     {
-        $this->rows[] = array('causal_id' => null, 'when' => array(array('month' => date("n"), 'year' => date("Y"), 'period' => '')),  'amount' => null, 'vat_id' => null, 'note' => '', 'commercial' => 0);
+        $this->rows[] = array('causal_id' => null, 'course_id' => null, 'course' => null, 'subscription_id' => null, 'subscription' => null, 'when' => array(array('month' => date("n"), 'year' => date("Y"), 'period' => '')),  'amount' => null, 'vat_id' => null, 'note' => '', 'commercial' => 0);
         $this->emit('load-select');
     }
 

+ 2 - 2
app/Http/Livewire/RecordINOUT.php

@@ -554,7 +554,7 @@ class RecordINOUT extends Component
             if ($level == 0) return true; // padre sempre incluso
 
             $pid = $row['parent_id'] ?? null;
-            // se non c'è il padre includi il figlio
+            // se non c'è il padre includi il figlio
             return !in_array($pid, $presentParentIds, true);
         };
 
@@ -650,7 +650,7 @@ class RecordINOUT extends Component
             $col = Coordinate::stringFromColumnIndex($firstDataIdx + $idx);
             $activeWorksheet->setCellValue($col . $count, $this->getMonth($column));
         }
-        $activeWorksheet->setCellValue($colRowTot . '1', 'Totale'); // già impostato sopra, qui lasciamo l’header a riga corrente
+        $activeWorksheet->setCellValue($colRowTot . '1', 'Totale'); // già impostato sopra, qui lasciamo l’header a riga corrente
         $activeWorksheet->setCellValue($colRowTot . $count, 'Totale');
 
         $activeWorksheet->getStyle($colA . $count . ':' . $colRowTot . $count)->getFont()->setBold(true);

+ 15 - 1
app/Http/Livewire/Reports.php

@@ -249,6 +249,8 @@ class Reports extends Component
         $query = DB::table('records_rows')
             ->join('records', 'records_rows.record_id', '=', 'records.id')
             ->join('causals', 'records_rows.causal_id', '=', 'causals.id')
+            ->leftJoin('courses', 'records_rows.course_id', '=', 'courses.id')
+            ->leftJoin('subscriptions', 'records_rows.subscription_id', '=', 'subscriptions.id')
             ->whereBetween('records.date', [$dateRange['start'], $dateRange['end']]);
 
         $query->where('records.type', 'IN');
@@ -259,13 +261,15 @@ class Reports extends Component
             'causals.id',
             'causals.name',
             'causals.parent_id',
+            'courses.id as course_id',
+            'subscriptions.id as subscription_id',
             DB::raw('SUM(records_rows.amount) as total_amount')
         )
             ->where(function ($query) {
                 $query->where('causals.no_reports', '=', '0')
                     ->orWhereNull('causals.no_reports');
             })
-            ->groupBy('causals.id', 'causals.name', 'causals.parent_id')
+            ->groupBy('causals.id', 'causals.name', 'causals.parent_id', 'courses.id', 'subscriptions.id')
             ->orderBy('total_amount', 'desc')
             ->limit($limit)
             ->get();
@@ -284,6 +288,16 @@ class Reports extends Component
 
             //$displayName = strlen($treeName) > 30 ? substr($treeName, 0, 27) . '...' : $treeName;
             $displayName = $treeName;
+
+            if (isset($causal->course_id) && $causal->course_id) {
+                $course = \App\Models\Course::find($causal->course_id);
+                $displayName .= " - " . $course->getDetailsName();
+            }
+            if (isset($causal->subscription_id) && $causal->subscription_id) {
+                $subscription = \App\Models\Subscription::find($causal->subscription_id);
+                $displayName .= " - " . $subscription->name;
+            }
+
             $inData[] = [
                 'label' => $displayName,
                 'value' => $causal->total_amount,

+ 245 - 129
app/Http/Livewire/SmsComunications.php

@@ -3,198 +3,314 @@
 namespace App\Http\Livewire;
 
 use Livewire\Component;
-use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\DB;
+use Carbon\Carbon;
+
 use App\Http\Middleware\TenantMiddleware;
-use App\Models\SmsTemplate;
-use App\Models\SmsScheduled;
-use App\Models\Category;
+use App\Models\SmsMessage;
 use App\Models\Member;
-use App\Models\User;
-use Carbon\Carbon;
-use Illuminate\Support\Facades\Log;
+
 class SmsComunications extends Component
 {
-    public $records, $subject, $message, $selectedRecipients = [], $scheduledDateTime, $sendNow = true;
-    public $dataId, $update = false, $add = false;
-    public $members = [];
-
-    protected $rules = [
-        'subject' => 'required|string|max:255',
-        'message' => 'required|string|max:160',
-        'selectedRecipients' => 'required|array|min:1',
-        'scheduledDateTime' => 'required_if:sendNow,false|date|after:now',
-    ];
-
-    protected $messages = [
-        'subject.required' => 'L\'oggetto è obbligatorio',
-        'message.required' => 'Il messaggio è obbligatorio',
-        'message.max' => 'Il messaggio non può superare 160 caratteri',
-        'selectedRecipients.required' => 'Seleziona almeno un gruppo di destinatari',
-        'scheduledDateTime.required_if' => 'La data di programmazione è obbligatoria',
-        'scheduledDateTime.after' => 'La data di programmazione deve essere futura',
-    ];
-
-    public $sortField = 'name';
-    public $sortAsc = true;
+    public ?int $messageId = null;
+    public string $subject = '';
+    public string $content = '';
+
+    public array $recipients = [];
+
+    public string $mode = 'now'; // 'now' | 'schedule'
+    public ?string $schedule_at = null;
+    public ?string $timezone = 'UTC';
+
+    public $records;
+    public $categories;
+    public $courses;
+
+    public bool $showForm = false;
+    public bool $locked = false;
+
+    public $success;
+    public $error;
 
     public function boot()
     {
         app(TenantMiddleware::class)->setupTenantConnection();
     }
 
-    public function sortBy($field)
+    public function mount()
     {
-        if($this->sortField === $field) {
-            $this->sortAsc = ! $this->sortAsc;
+        if (auth()->user()?->level != env('LEVEL_ADMIN', 0)) {
+            return redirect()->to('/dashboard');
+        }
+
+        $this->categories = [];
+        $this->getCategories(\App\Models\Category::select('id', 'name')->where('parent_id', null)->orderBy('name')->get(), 0);
+
+        $this->courses = [];
+        $this->getCourses(\App\Models\Course::select('id', 'name')->where('parent_id', null)->orderBy('name', 'ASC')->get(), 0);
+
+        $this->schedule_at = now($this->timezone)->addHour()->format('Y-m-d\TH:i');
+    }
+
+    public function render()
+    {
+        if (!$this->showForm) {
+            $this->records = SmsMessage::withCount(['recipients'])
+                ->orderBy('created_at', 'desc')
+                ->get();
         } else {
-            $this->sortAsc = true;
+            $this->categories = [];
+            $this->getCategories(\App\Models\Category::select('id', 'name')->where('parent_id', null)->orderBy('name')->get(), 0);
+
+            $this->courses = [];
+            $this->getCourses(\App\Models\Course::select('id', 'name')->where('parent_id', null)->orderBy('name', 'ASC')->get(), 0);
         }
 
-        $this->sortField = $field;
+        return view('livewire.sms_comunications');
     }
 
-    public function resetFields()
+    protected function baseRules(): array
     {
-        $this->subject = '';
-        $this->message = '';
-        $this->selectedRecipients = [];
-        $this->sendNow = true;
-        $this->scheduledDateTime = now()->addHour()->format('Y-m-d\TH:i');
-        $this->emit('load-data-table');
+        return [
+            'subject' => 'required|string|max:255',
+            'content' => 'required|string',
+            'recipients' => 'required|array|min:1',
+            'recipients.*.phone' => 'required|string',
+        ];
     }
 
-    public function mount()
+    protected function validateDraft(): void
     {
-        if(Auth::user()->level != env('LEVEL_ADMIN', 0))
-            return redirect()->to('/dashboard');
+        $rules = [];
+        // $rules = $this->baseRules();
+        $rules['subject'] = 'required|string|max:255';
+        $this->validate($rules);
+    }
 
-        $this->members = Member::select('id', 'last_name', 'email')->get();
-        $this->scheduledDateTime = now()->addHour()->format('Y-m-d\TH:i');
+    protected function validateSend(): void
+    {
+        $this->validate($this->baseRules());
     }
 
-    public function render()
+    protected function validateSchedule(): void
     {
-        $this->records = SmsTemplate::orderBy($this->sortField, $this->sortAsc ? 'asc' : 'desc')->get();
-        return view('livewire.sms_comunications');
+        $rules = $this->baseRules();
+        $rules['schedule_at'] = 'required|date|after:now';
+        $this->validate($rules);
     }
 
     public function add()
     {
-        $this->resetFields();
-        $this->add = true;
-        $this->update = false;
+        $this->reset(['messageId', 'subject', 'content', 'recipients', 'mode', 'schedule_at']);
+        $this->mode = 'now';
+        $this->schedule_at = now($this->timezone)->addHour()->format('Y-m-d\TH:i');
+
+        $this->showForm = true;
+
+        $this->dispatchBrowserEvent('init-recipients-table', [
+            'selected' => collect($this->recipients)->pluck('member_id')->filter()->values()->all(),
+        ]);
     }
 
-    public function store()
+    public function edit($id)
     {
-        $this->validate();
-
         try {
-            $template = SmsTemplate::create([
-                'name' => $this->subject,
-                'content' => $this->message,
-                'created_by' => Auth::id(),
-            ]);
+            $msg = SmsMessage::with(['recipients'])->findOrFail($id);
 
-            $recipients = User::whereIn('id', $this->selectedRecipients)->get();
+            $this->messageId = $msg->id;
+            $this->subject = $msg->subject;
+            $this->content = $msg->content;
+            $this->recipients = $msg->recipients->map(fn($r) => [
+                'member_id' => $r->member_id,
+                'phone' => $r->phone,
+                'first_name'    => optional($r->member)->first_name,
+                'last_name'     => optional($r->member)->last_name,
+            ])->toArray();
+            $this->mode = $msg->status === 'scheduled' ? 'schedule' : 'now';
+            $this->schedule_at = optional($msg->schedule_at)?->setTimezone($this->timezone)?->format('Y-m-d\TH:i');
 
-            if ($this->sendNow) {
-                $this->sendSmsNow($template, $recipients);
-                session()->flash('success', 'Template creato e SMS inviato a ' . $recipients->count() . ' destinatari!');
-            } else {
-                $this->scheduleSms($template, $recipients);
-                $scheduledDate = Carbon::parse($this->scheduledDateTime)->format('d/m/Y H:i');
-                session()->flash('success', 'Template creato e SMS programmato per ' . $scheduledDate);
-            }
+            $this->showForm = true;
+            $this->locked = $msg->isLocked();
 
-            $this->resetFields();
-            $this->add = false;
-        } catch (\Exception $ex) {
-            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+            $this->dispatchBrowserEvent('init-recipients-table', [
+                'selected' => collect($this->recipients)->pluck('member_id')->filter()->values()->all(),
+            ]);
+        } catch (\Throwable $ex) {
+            $this->error = 'Errore (' . $ex->getMessage() . ')';
         }
     }
 
-    public function edit($id)
+
+    public function duplicate($id, $withRecipients = true)
     {
         try {
-            $template = SmsTemplate::findOrFail($id);
-            if (!$template) {
-                session()->flash('error', 'Template SMS non trovato');
-            } else {
-                $this->subject = $template->name;
-                $this->message = $template->content;
-                $this->dataId = $template->id;
-                $this->update = true;
-                $this->add = false;
+            $copy = SmsMessage::with(['recipients'])->findOrFail($id)->duplicate($withRecipients);
+            $this->edit($copy->id);
+            $this->success = 'Bozza duplicata';
+        } catch (\Throwable $ex) {
+            $this->error = 'Errore (' . $ex->getMessage() . ')';
+        }
+    }
+
+    public function saveDraft()
+    {
+        $this->validateDraft();
+
+        DB::transaction(function () {
+            $msg = $this->upsertMessage(status: 'draft', scheduleAt: null);
+            $this->upsertRecipients($msg);
+            $this->messageId = $msg->id;
+            $this->locked = $msg->isLocked();
+        });
+
+        $this->success = 'Bozza salvata';
+
+        $this->dispatchBrowserEvent('scroll-top');
+    }
+
+    public function sendNow()
+    {
+        $this->validateSend();
+
+        if ($this->messageId) {
+            $existing = SmsMessage::findOrFail($this->messageId);
+            if ($existing->isLocked()) {
+                $this->error = 'Questo sms è già in invio o inviato e non può essere modificato.';
+                return;
             }
-        } catch (\Exception $ex) {
-            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
         }
+
+        DB::transaction(function () {
+            $msg = $this->upsertMessage(status: 'processing', scheduleAt: null);
+            $this->upsertRecipients($msg, true);
+            $this->messageId = $msg->id;
+            $this->locked = true;
+        });
+
+        dispatch(new \App\Jobs\SendSmsMessage($this->messageId));
+        $this->success = 'Invio avviato';
+
+        $this->dispatchBrowserEvent('scroll-top');
     }
 
-    public function update()
+    public function scheduleMessage()
     {
-        $this->validate([
-            'subject' => 'required|string|max:255',
-            'message' => 'required|string|max:160',
-        ]);
+        $this->validateSchedule();
 
-        try {
-            SmsTemplate::whereId($this->dataId)->update([
-                'name' => $this->subject,
-                'content' => $this->message,
-            ]);
-            session()->flash('success', 'Template SMS aggiornato');
-            $this->resetFields();
-            $this->update = false;
-        } catch (\Exception $ex) {
-            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        if ($this->messageId) {
+            $existing = SmsMessage::findOrFail($this->messageId);
+            if ($existing->isLocked()) {
+                $this->error = 'Questo sms è già in invio o inviato e non può essere modificato.';
+                return;
+            }
         }
+
+        $scheduledAt = \Carbon\Carbon::parse($this->schedule_at, $this->timezone)->setTimezone('UTC');
+
+        DB::transaction(function () use ($scheduledAt) {
+            $msg = $this->upsertMessage(status: 'scheduled', scheduleAt: $scheduledAt);
+            $this->upsertRecipients($msg, true);
+            $this->messageId = $msg->id;
+            $this->locked = $msg->isLocked();
+        });
+
+        $this->success = 'Sms programmato';
+
+        $this->dispatchBrowserEvent('scroll-top');
+    }
+
+    protected function upsertMessage(string $status, $scheduleAt): SmsMessage
+    {
+        return SmsMessage::updateOrCreate(
+            ['id' => $this->messageId],
+            [
+                'subject' => $this->subject,
+                'content' => $this->content,
+                'status' => $status,
+                'schedule_at' => $scheduleAt,
+                'created_by'   => auth()->id(),
+            ]
+        );
+    }
+
+    protected function upsertRecipients(SmsMessage $msg, bool $force = false): void
+    {
+        if (!$force && $msg->isLocked()) return;
+
+        $msg->recipients()->delete();
+
+        $rows = collect($this->recipients)->map(fn($r) => [
+            'sms_message_id' => $msg->id,
+            'member_id' => $r['member_id'] ?? null,
+            'phone' => $r['phone'],
+            'status' => 'pending',
+            'created_at' => now(),
+            'updated_at' => now(),
+        ])->values()->all();
+
+        if ($rows) \App\Models\SmsMessageRecipient::insert($rows);
     }
 
     public function cancel()
     {
-        $this->add = false;
-        $this->update = false;
-        $this->resetFields();
+        $this->showForm = false;
+        $this->reset(['messageId', 'subject', 'content', 'recipients', 'mode', 'schedule_at']);
+        $this->mode = 'now';
+        $this->schedule_at = now($this->timezone)->addHour()->format('Y-m-d\TH:i');
+
+        $this->dispatchBrowserEvent('init-archive-table');
     }
 
-    public function sendTemplate($id)
+    public function getCategories($records, $indentation)
     {
-        try {
-            $template = SmsTemplate::findOrFail($id);
-            $this->subject = $template->name;
-            $this->message = $template->content;
-            $this->add = true;
-            $this->update = false;
-        } catch (\Exception $ex) {
-            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        foreach ($records as $record) {
+            $this->categories[] = array('id' => $record->id, 'name' => $record->getTree());
+            if (count($record->childs))
+                $this->getCategories($record->childs, $indentation + 1);
         }
     }
 
-    private function sendSmsNow($template, $recipients)
+    public function getCourses($records, $indentation)
     {
-        // Simple SMS sending logic
-        foreach ($recipients as $recipient) {
-            if ($recipient->phone) {
-                // Here you would integrate with your SMS provider
-                Log::info("SMS sent to {$recipient->name} ({$recipient->phone}): {$template->content}");
-            }
+        /** @var \App\Models\Course $record */
+        foreach ($records as $record) {
+            $this->courses[] = array('id' => $record->id, 'name' => $record->getTree());
+            if (count($record->childs))
+                $this->getCourses($record->childs, $indentation + 1);
         }
     }
 
-    private function scheduleSms($template, $recipients)
+    public function toggleRecipient($id)
     {
-        $scheduled = SmsScheduled::create([
-            'template_id' => $template->id,
-            'content' => $template->content,
-            'scheduled_at' => Carbon::parse($this->scheduledDateTime),
-            'status' => 'scheduled',
-            'created_by' => Auth::id(),
-        ]);
+        $id = (int)$id;
+
+        $idx = collect($this->recipients)->search(fn($r) => (int)($r['member_id'] ?? 0) === $id);
+        if ($idx !== false) {
+            array_splice($this->recipients, $idx, 1);
+            return;
+        }
+
+        $m = Member::select('id', 'phone', 'first_name', 'last_name')->find($id);
+        if (!$m || empty($m->phone)) return;
+
+        $this->recipients[] = [
+            'member_id' => $m->id,
+            'phone' => $m->phone,
+            'first_name' => $m->first_name,
+            'last_name' => $m->last_name,
+        ];
+    }
+
+    public function deleteMessage(int $id)
+    {
+        $msg = \App\Models\SmsMessage::findOrFail($id);
+
+        if (! in_array($msg->status, ['draft', 'failed'], true)) {
+            return;
+        }
 
-        $scheduled->recipients()->attach($recipients->pluck('id'));
+        $msg->delete();
 
-        Log::info("SMS scheduled for {$this->scheduledDateTime} to {$recipients->count()} recipients: {$template->content}");
+        $this->dispatchBrowserEvent('sms-deleted', ['id' => $id]);
     }
 }

+ 284 - 0
app/Http/Livewire/Subscription.php

@@ -0,0 +1,284 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use App\Http\Middleware\TenantMiddleware;
+use Livewire\Component;
+use Illuminate\Support\Facades\Auth;
+
+class Subscription extends Component
+{
+
+    protected $listeners = ['setCausal' => 'setCausal', 'setSubscriptionCausal' => 'setSubscriptionCausal'];
+
+    public $records;
+
+    public $dataId;
+    public $update = false;
+    public $add = false;
+
+    public $name;
+    public $causal_id;
+    public $sub_causal_id;
+    public $subscription_price;
+    public $prices;
+    public $enabled;
+
+    public $msgPrices = '';
+
+    public $course_subscriptions = [];
+
+    public $typeIN = 'IN';
+    public $setSubscriptionCausal = 'setSubscriptionCausal';
+
+    protected $rules = [
+        'name' => 'required',
+        'subscription_price' => 'required|min:0|not_in:0'
+    ];
+
+    protected $messages = [
+        'name.required' => 'Il nome è obbligatorio',
+        'subscription_price.required' => 'Deve essere maggiore di zero',
+        'subscription_price.not_in' => 'Deve essere maggiore di zero',
+    ];
+
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+    }
+
+    public function resetFields()
+    {
+        $this->name = '';
+        $this->causal_id = null;
+        $this->sub_causal_id = null;
+        $this->subscription_price = 0;
+        $this->prices = [];
+        $this->prices[] = array('course_subscription_id' => null, 'price' => 0);
+        $this->enabled = true;
+
+        $this->emit('load-data-table');
+    }
+
+
+    public function mount()
+    {
+        if (Auth::user()->level != env('LEVEL_ADMIN', 0))
+            return redirect()->to('/dashboard');
+
+        $this->course_subscriptions = \App\Models\CourseSubscription::select('*')->where('enabled', true)->get();
+    }
+
+    public function render()
+    {
+        $this->records = \App\Models\Subscription::all();
+
+        return view('livewire.subscription');
+    }
+
+    public function add()
+    {
+        $this->resetFields();
+
+        $this->add = true;
+        $this->update = false;
+
+        $this->emit('setEdit', true);
+    }
+
+    public function store()
+    {
+        $this->validate();
+        try {
+            $this->msgPrices = '';
+
+            if ($this->prices[0]['course_subscription_id'] == null)
+                $this->msgPrices = 'Devi inserire almeno un prezzo';
+
+            $subscriptions = array_column($this->prices, 'course_subscription_id');
+            $unique_subscriptions = array_unique($subscriptions);
+            if (count($subscriptions) != count($unique_subscriptions))
+                $this->msgPrices = 'Non è possibile aggiungere più volte la stessa tipologia di pagamento';
+
+            if ($this->msgPrices == '') {
+                $subscription = new \App\Models\Subscription();
+
+                $subscription->name = $this->name;
+                $subscription->subscription_price = currencyToDouble($this->subscription_price);
+                $subscription->prices = json_encode($this->prices);
+                $subscription->enabled = $this->enabled;
+
+                $causal = "PAGAMENTO ABBONAMENTO";
+                $cp = \App\Models\Causal::where('name', $causal)->first();
+                if (!$cp) {
+                    $cp = new \App\Models\Causal();
+                    $cp->name = $causal;
+                    $cp->type = "IN";
+                    $cp->parent_id = null;
+                    $cp->money = false;
+                    $cp->corrispettivo_fiscale = false;
+                    $cp->no_receipt = false;
+                    $cp->user_status = false;
+                    $cp->no_first = false;
+                    $cp->no_records = false;
+                    $cp->enabled = true;
+                    $cp->save();
+                }
+                $subscription->causal_id = $cp->id;
+
+                $causal = "PAGAMENTO ISCRIZIONE";
+                $ci = \App\Models\Causal::where('name', $causal)->first();
+                if (!$ci) {
+                    $ci = new \App\Models\Causal();
+                    $ci->name = $causal;
+                    $ci->type = "IN";
+                    $ci->parent_id = null;
+                    $ci->money = false;
+                    $ci->corrispettivo_fiscale = false;
+                    $ci->no_receipt = false;
+                    $ci->user_status = false;
+                    $ci->no_first = false;
+                    $ci->no_records = false;
+                    $ci->enabled = true;
+                    $ci->save();
+                }
+                $subscription->sub_causal_id = $ci->id;
+
+                $subscription->save();
+
+                session()->flash('success', 'Abbonamento creato');
+                $this->resetFields();
+                $this->add = false;
+                $this->emit('setEdit', false);
+            }
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function edit($id)
+    {
+        $this->resetFields();
+
+        try {
+            $subscription = \App\Models\Subscription::findOrFail($id);
+
+            if (!$subscription) {
+                session()->flash('error', 'Abbonamento non trovato');
+            } else {
+                $this->name = $subscription->name;
+                $this->enabled = $subscription->enabled;
+                $this->causal_id = $subscription->causal_id;
+                $this->sub_causal_id = $subscription->sub_causal_id;
+                $this->subscription_price = formatPrice($subscription->subscription_price);
+                $this->prices = array();
+                if ($subscription->prices != null) {
+                    foreach (json_decode($subscription->prices) as $z) {
+                        $this->prices[] = array("course_subscription_id" => $z->course_subscription_id, "price" => $z->price);
+                    }
+                } else {
+                    $this->prices[] = array('course_subscription_id' => null, 'price' => 0);
+                }
+                $this->dataId = $subscription->id;
+                $this->update = true;
+                $this->add = false;
+            }
+            $this->emit('setEdit', true);
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function update()
+    {
+        $this->validate();
+        try {
+            $this->msgPrices = '';
+
+            if ($this->prices[0]['course_subscription_id'] == null)
+                $this->msgPrices = 'Devi inserire almeno un prezzo';
+
+            $subscriptions = array_column($this->prices, 'course_subscription_id');
+            $unique_subscriptions = array_unique($subscriptions);
+            if (count($subscriptions) != count($unique_subscriptions))
+                $this->msgPrices = 'Non è possibile aggiungere più volte la stessa tipologia di pagamento';
+
+            if ($this->msgPrices == '') {
+                \App\Models\Subscription::whereId($this->dataId)->update([
+                    'name' => $this->name,
+                    'causal_id' => $this->causal_id,
+                    'sub_causal_id' => $this->sub_causal_id,
+                    'subscription_price' => currencyToDouble($this->subscription_price),
+                    'prices' => json_encode($this->prices),
+                    'enabled' => $this->enabled
+                ]);
+                session()->flash('success', 'Abbonamento aggiornato');
+                $this->resetFields();
+                $this->update = false;
+                $this->emit('setEdit', false);
+            }
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function cancel()
+    {
+        $this->add = false;
+        $this->update = false;
+
+        $this->resetFields();
+
+        $this->emit('setEdit', false);
+    }
+
+    public function delete($id)
+    {
+        try {
+            \App\Models\Subscription::find($id)->delete();
+            session()->flash('success', "Abbonamento eliminato");
+
+            return redirect(request()->header('Referer'));
+        } catch (\Exception $e) {
+            session()->flash('error', 'Errore (' . $e->getMessage() . ')');
+        }
+    }
+
+    public function setCausal($id, $idx)
+    {
+        $this->causal_id = $id;
+    }
+
+    public function setSubscriptionCausal($id, $idx)
+    {
+        $this->sub_causal_id = $id;
+    }
+
+    public function duplicate($id, $isMultiple)
+    {
+        $subscription = \App\Models\Subscription::findOrFail($id);
+        $new_subscription = $subscription->replicate();
+        $new_subscription->save();
+
+        if (!$isMultiple)
+            $this->edit($new_subscription->id);
+    }
+
+    public function duplicateMultiple($ids)
+    {
+        foreach ($ids as $id) {
+            $this->duplicate($id, true);
+        }
+        return redirect()->to('/subscriptions');
+    }
+
+    public function addPrice()
+    {
+        $this->prices[] = array('course_subscription_id' => null, 'price' => 0);
+    }
+
+    public function delPrice($idx)
+    {
+        unset($this->prices[$idx]);
+    }
+}

+ 105 - 0
app/Http/Livewire/SubscriptionMember.php

@@ -0,0 +1,105 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use App\Http\Middleware\TenantMiddleware;
+use Livewire\Component;
+
+class SubscriptionMember extends Component
+{
+    public $records = [];
+
+    public $subscription_id;
+    public $subscription = null;
+
+    public $filter = "";
+
+    public $filterSubscription = "";
+    public $chkCertificateType = "";
+    public $chkCertificateScadenza = 0;
+    public $fromYear = "";
+    public $toYear = "";
+    public $fromFromYear = "";
+    public $toToYear = "";
+    public $chkCard = [];
+    public $filterStatus = [];
+
+
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+    }
+
+    public function mount($id)
+    {
+        $this->subscription_id = $id;
+        $this->subscription = \App\Models\Subscription::findOrFail($this->subscription_id);
+    }
+
+    public function render()
+    {
+        $this->filter = '';
+        $members = \App\Models\MemberSubscription::with('member')->where('subscription_id', $this->subscription_id);
+
+        if ($this->filterSubscription != "") {
+            $ids = \App\Models\MemberSubscription::where('subscribed', $this->filterSubscription == 1 ? true : false)->pluck('id');
+            $members = $members->whereIn('id', $ids);
+            $this->filter .= $this->filter != '' ? ', ' : '';
+            $this->filter .= "Pagata sottoscrizione: " . ($this->filterSubscription == 1 ? "SI" : "NO") . " ";
+        }
+        if ($this->chkCertificateType != "") {
+            $types = \App\Models\MemberCertificate::where('type', $this->chkCertificateType)->pluck('member_id');
+            $members = $members->whereIn('member_id', $types);
+        }
+        if ($this->chkCertificateScadenza > 0) {
+            if ($this->chkCertificateScadenza == "1")
+                $scad = \App\Models\MemberCertificate::where('expire_date', '<', date("Y-m-d"))->pluck('member_id');
+            if ($this->chkCertificateScadenza == "2")
+                $scad = \App\Models\MemberCertificate::whereBetween('expire_date', [date("Y-m-d"), date("Y-m-d", strtotime("+1 month"))])->pluck('member_id');
+            $members = $members->whereIn('member_id', $scad);
+        }
+        if ($this->fromYear != "") {
+            $m_ids = \App\Models\Member::where('birth_date', '<', date("Y-m-d", strtotime("-" . $this->fromYear . " year", time())))->pluck('id');
+            $members = $members->whereIn('member_id', $m_ids);
+        }
+        if ($this->toYear != "") {
+            $m_ids = \App\Models\Member::where('birth_date', '>', date("Y-m-d", strtotime("-" . $this->toYear . " year", time())))->pluck('id');
+            $members = $members->whereIn('member_id', $m_ids);
+        }
+        if ($this->fromFromYear != "") {
+            $m_ids = \App\Models\Member::whereYear('birth_date', '>=', $this->fromFromYear)->pluck('id');
+            $members = $members->whereIn('member_id', $m_ids);
+        }
+        if ($this->toToYear != "") {
+            $m_ids = \App\Models\Member::whereYear('birth_date', '<=', $this->toToYear)->pluck('id');
+            $members = $members->whereIn('member_id', $m_ids);
+        }
+        if (sizeof($this->chkCard) > 0) {
+            $card_ids = \App\Models\MemberCard::whereIn('card_id', $this->chkCard)->pluck('member_id');
+            $members = $members->whereIn('member_id', $card_ids);
+            $this->filter .= $this->filter != '' ? ', ' : '';
+            $this->filter .= "Tessera: ";
+            foreach ($this->chkCard as $card) {
+                $this->filter .= \App\Models\Card::findOrFail($card)->name . " ";
+            }
+        }
+
+        $aRet = [];
+        if (sizeof($this->filterStatus) > 0) {
+            foreach ($this->filterStatus as $s) {
+                foreach ($members->get() as $aaa) {
+                    $state = \App\Models\Member::findOrFail($aaa->member_id)->isActive();
+
+                    if ($state["status"] == $s)
+                        $aRet[] = $aaa;
+                }
+            }
+        } else {
+            $aRet = $members->get();
+        }
+
+        $this->records = $aRet;
+
+        return view('livewire.subscription_member');
+    }
+}

+ 16 - 0
app/Http/Livewire/SubscriptionMembers.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+
+class SubscriptionMembers extends Component
+{
+    public $records = [];
+
+    public function render()
+    {
+        $this->records = \App\Models\Subscription::where('enabled', true)->get();
+        return view('livewire.subscription_members');
+    }
+}

+ 13 - 4
app/Http/Livewire/User.php

@@ -19,12 +19,14 @@ class User extends Component
 
     public $records, $name, $cognome, $email, $password, $oldPassword, $level, $enabled, $dataId, $update = false, $add = false, $oldEmail = null;
     public $userExists = false;
+    public $password_confirmation;
 
     protected $rules = [
         'name' => 'required',
         'cognome' => 'required',
         'email' => 'required',
-        'password' => 'required'
+        'password' => 'required',
+        'password_confirmation' => 'required|same:password'
     ];
 
     protected $messages = [
@@ -32,6 +34,8 @@ class User extends Component
         'cognome.required' => 'Il cognome è obbligatorio',
         'email.required' => 'La mail è obbligatoria',
         'password.required' => 'La password è obbligatoria',
+        'password_confirmation.required' => 'Ripeti la password inserita',
+        'password_confirmation.same' => 'Le password non coincidono',
     ];
 
     /**
@@ -329,6 +333,7 @@ class User extends Component
         $this->cognome = '';
         $this->email = '';
         $this->password = '';
+        $this->password_confirmation = '';
         $this->oldPassword = '';
         $this->level = 0;
         $this->enabled = true;
@@ -383,7 +388,8 @@ class User extends Component
             'name' => 'required',
             'cognome' => 'required',
             'email' => 'required|email|unique:users,email',
-            'password' => 'required|min:6'
+            'password' => 'required|min:6',
+            'password_confirmation' => 'required|same:password'
         ];
 
         $messages = [
@@ -393,7 +399,9 @@ class User extends Component
             'email.email' => 'La mail deve essere un indirizzo valido',
             'email.unique' => 'Questa mail è già stata utilizzata',
             'password.required' => 'La password è obbligatoria',
-            'password.min' => 'La password deve essere di almeno 6 caratteri'
+            'password.min' => 'La password deve essere di almeno 6 caratteri',
+            'password_confirmation.required' => 'Ripeti la password inserita',
+            'password_confirmation.same' => 'Le password non coincidono',
         ];
 
         $this->validate($rules, $messages);
@@ -546,7 +554,8 @@ class User extends Component
             'name' => 'required',
             'cognome' => 'required',
             'email' => 'required|email',
-            'password' => 'nullable|min:6'
+            'password' => 'nullable|min:6',
+            'password_confirmation' => 'required|same:password'
         ];
 
         $this->validate($rules, $this->messages);

+ 134 - 0
app/Jobs/SendSmsMessage.php

@@ -0,0 +1,134 @@
+<?php
+
+namespace App\Jobs;
+
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\SerializesModels;
+use RuntimeException;
+
+class SendSmsMessage implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    public function __construct(public int $messageId) {}
+
+    public function handle()
+    {
+        $msg = \App\Models\SmsMessage::with(['recipients'])->findOrFail($this->messageId);
+        $msg->update(['status' => 'processing']);
+
+        foreach ($msg->recipients as $r) {
+            if (in_array($r->status, ['sent', 'failed'])) continue;
+
+            try {
+                $phone = $r->phone;
+                $message = $msg->content;
+
+                $params = array(
+                    'to'            => '+39' . $phone,
+                    'from'          => env('SMS_FROM', 'Leezard'),
+                    'message'       => $message,
+                    'format'        => 'json',
+                );
+                $this->sms_send($params);
+
+                $r->update(['status' => 'sent', 'sent_at' => now(), 'error_message' => null]);
+            } catch (\Throwable $e) {
+                $r->update(['status' => 'failed', 'error_message' => $e->getMessage()]);
+            }
+        }
+        $total = $msg->recipients()->count();
+        $sent = $msg->recipients()->where('status', 'sent')->count();
+        $failed = $msg->recipients()->where('status', 'failed')->count();
+
+        $newStatus = 'draft';
+        if ($total === 0) {
+            $newStatus = 'draft';
+        } elseif ($sent === $total) {
+            $newStatus = 'sent';
+        } elseif ($sent > 0 && $failed > 0) {
+            $newStatus = 'partial';
+        } else {
+            $newStatus = 'failed';
+        }
+
+        $msg->update([
+            'status' => $newStatus,
+            'sent_at' => $sent > 0 ? now() : $msg->sent_at,
+        ]);
+    }
+
+    public function sms_send(array $params, bool $backup = false)
+    {
+        if (!isset($params['format'])) {
+            $params['format'] = 'json';
+        }
+
+        $url = $backup
+            ? 'https://api2.smsapi.com/sms.do'
+            : 'https://api.smsapi.com/sms.do';
+
+        $ch = curl_init($url);
+
+        curl_setopt_array($ch, [
+            CURLOPT_POST => true,
+            CURLOPT_POSTFIELDS => $params,
+            CURLOPT_RETURNTRANSFER => true,
+            CURLOPT_HTTPHEADER => [
+                'Authorization: Bearer ' . env('SMS_TOKEN'),
+            ],
+            CURLOPT_TIMEOUT => 15,
+            CURLOPT_CONNECTTIMEOUT => 5,
+        ]);
+
+        $content = curl_exec($ch);
+        $httpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+        $curlError = curl_error($ch);
+
+        curl_close($ch);
+
+        if ($content === false) {
+            if (!$backup) {
+                return $this->sms_send($params, true);
+            }
+
+            throw new RuntimeException('SMS API cURL error: ' . $curlError);
+        }
+
+        if ($httpStatus !== 200) {
+            if (!$backup && $httpStatus >= 500 && $httpStatus < 600) {
+                return $this->sms_send($params, true);
+            }
+
+            throw new RuntimeException("SMS API HTTP {$httpStatus}: {$content}");
+        }
+
+        $data = json_decode($content, true);
+
+        if (json_last_error() !== JSON_ERROR_NONE) {
+            throw new RuntimeException('SMS API invalid JSON response: ' . $content);
+        }
+
+        if (isset($data['error'])) {
+            $msg = is_array($data['error'])
+                ? ($data['error']['message'] ?? json_encode($data['error']))
+                : $data['error'];
+
+            throw new RuntimeException('SMS API error: ' . $msg);
+        }
+
+        if (isset($data['list'][0]['status'])) {
+            $status = strtoupper($data['list'][0]['status']);
+
+            if (in_array($status, ['ERROR', 'FAILED', 'REJECTED'], true)) {
+                $errMsg = $data['list'][0]['error_message'] ?? 'Unknown SMS API error';
+                throw new RuntimeException("SMS API message error ({$status}): {$errMsg}");
+            }
+        }
+
+        return $data;
+    }
+}

+ 1 - 1
app/Models/Azienda.php

@@ -81,7 +81,7 @@ class Azienda extends Model
     }
 
     /**
-     * Restituisce true se l’azienda è valida (tutti i campi richiesti presenti).
+     * Restituisce true se l'azienda è valida (tutti i campi richiesti presenti).
      */
     public function isValid()
     {

+ 15 - 0
app/Models/Course.php

@@ -141,4 +141,19 @@ class Course extends Model
     {
         return ucfirst($this->attributes['type'] ?? 'No Type');
     }
+
+    public function getDetailsName() {
+        $courseName = $this->name ?? 'Corso Sconosciuto';
+        $levelName = is_object($this->level) ? $this->level->name : '';
+        $frequencyName = is_object($this->frequency) ? $this->frequency->name : '';
+        $typeName = $this->getFormattedTypeField() ?? '';
+
+        // Build display name with level and frequency
+        $displayNameParts = [$courseName];
+        if ($levelName) $displayNameParts[] = $levelName;
+        // if ($typeName) $displayNameParts[] = $typeName;
+        if ($frequencyName) $displayNameParts[] = $frequencyName;
+
+        return implode(' - ', $displayNameParts);
+    }
 }

+ 7 - 0
app/Models/Member.php

@@ -56,6 +56,8 @@ class Member extends Model
         'to_complete',
         'is_archived',
         'archived_date',
+        'is_deleted',
+        'deleted_date',
     ];
     public function nation()
     {
@@ -102,6 +104,11 @@ class Member extends Model
         return $this->hasMany(MemberCourse::class);
     }
 
+    public function subscriptions()
+    {
+        return $this->hasMany(MemberSubscription::class);
+    }
+
     public function certificates()
     {
         return $this->hasMany(MemberCertificate::class)->orderBy('expire_date', 'DESC');

+ 52 - 0
app/Models/MemberSubscription.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class MemberSubscription extends Model
+{
+    use HasFactory;
+
+    protected $fillable = [
+        'member_id',
+        'subscription_id',
+        'date_from',
+        'date_to',
+        'price',
+        'course_subscription_id',
+        'subscription_price',
+        'notes'
+    ];
+
+    public function member()
+    {
+        return $this->belongsTo(Member::class);
+    }
+
+    public function subscription()
+    {
+        return $this->belongsTo(Subscription::class);
+    }
+
+    public function course_subscription()
+    {
+        return $this->belongsTo(CourseSubscription::class);
+    }
+
+    public function getStatus()
+    {
+        $status = 0;
+        $rates = \App\Models\Rate::where('member_subscription_id', $this->id)->orderBy('date')->get();
+        foreach($rates as $rate)
+        {
+            if ($rate->date > date("Y-m-d"))
+            {
+                break;
+            }
+            $status = $rate->status;
+        }
+        return $status;
+    }
+}

+ 6 - 1
app/Models/Rate.php

@@ -19,7 +19,8 @@ class Rate extends Model
         'note',
         'status',
         'record_id',
-        'is_subscription'
+        'is_subscription',
+        'member_subscription_id',
     ];
 
     public function member()
@@ -36,6 +37,10 @@ class Rate extends Model
     {
         return $this->belongsTo(MemberCourse::class, 'member_course_id');
     }
+    public function member_subscription()
+    {
+        return $this->belongsTo(MemberSubscription::class, 'member_subscription_id');
+    }
 
     public function course_subscription()
     {

+ 12 - 0
app/Models/ReceiptRow.php

@@ -14,6 +14,7 @@ class ReceiptRow extends Model
     protected $fillable = [
         'receip_id',
         'causal_id',
+        'course_id',
         'when',
         'amount',
         'note',
@@ -27,6 +28,7 @@ class ReceiptRow extends Model
         'quantita',
         'prediscount_amount',
         'sconto',
+        'subscription_id',
     ];
 
     protected $casts = [
@@ -47,4 +49,14 @@ class ReceiptRow extends Model
     {
         return $this->belongsTo(Receipt::class, 'receip_id', 'id');
     }
+
+    public function course()
+    {
+        return $this->belongsTo(Course::class);
+    }
+
+    public function subscription()
+    {
+        return $this->belongsTo(Subscription::class, 'subscription_id');
+    }
 }

+ 11 - 0
app/Models/RecordRow.php

@@ -14,6 +14,7 @@ class RecordRow extends Model
     protected $fillable = [
         'record_id',
         'causal_id',
+        'course_id',
         'when',
         'amount',
         'vat_id',
@@ -27,6 +28,7 @@ class RecordRow extends Model
         'numero_linea',
         'sconto',
         'prediscount_amount',
+        'subscription_id',
     ];
 
     public function causal()
@@ -34,5 +36,14 @@ class RecordRow extends Model
         return $this->belongsTo(Causal::class);
     }
 
+    public function course()
+    {
+        return $this->belongsTo(Course::class);
+    }
+
+    public function subscription()
+    {
+        return $this->belongsTo(Subscription::class);
+    }
 }
 

+ 69 - 0
app/Models/SmsMessage.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Facades\DB;
+
+class SmsMessage extends Model
+{
+    protected $fillable = [
+        'subject',
+        'content',
+        'status',
+        'schedule_at',
+        'sent_at',
+        'created_by'
+    ];
+
+    protected $casts = [
+        'schedule_at' => 'datetime',
+        'sent_at' => 'datetime',
+    ];
+
+    public function recipients()
+    {
+        return $this->hasMany(SmsMessageRecipient::class);
+    }
+    public function creator()
+    {
+        return $this->belongsTo(User::class, 'created_by');
+    }
+
+    public function duplicate(bool $withRecipients = false)
+    {
+        return DB::transaction(function () use ($withRecipients) {
+            $copy = $this->replicate(['status', 'schedule_at', 'sent_at']);
+            $copy->status = 'draft';
+            $copy->schedule_at = null;
+            $copy->sent_at = null;
+            $copy->save();
+
+            foreach ($this->attachments as $a) {
+                $copy->attachments()->create($a->only(['disk', 'path', 'name', 'size']));
+            }
+
+            if ($withRecipients) {
+                $payload = $this->recipients()
+                    ->get(['member_id', 'phone'])
+                    ->map(fn($r) => [
+                        'member_id' => $r->member_id,
+                        'phone' => $r->phone,
+                        'status' => 'pending',
+                    ])
+                    ->all();
+
+                if ($payload) {
+                    $copy->recipients()->createMany($payload);
+                }
+            }
+
+            return $copy;
+        });
+    }
+
+    public function isLocked(): bool
+    {
+        return in_array($this->status, ['processing', 'sent']);
+    }
+}

+ 28 - 0
app/Models/SmsMessageRecipient.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class SmsMessageRecipient extends Model
+{
+    protected $fillable = [
+        'sms_message_id',
+        'member_id',
+        'phone',
+        'status',
+        'error_message',
+        'sent_at'
+    ];
+
+    protected $casts = ['sent_at' => 'datetime'];
+
+    public function message()
+    {
+        return $this->belongsTo(SmsMessage::class);
+    }
+    public function member()
+    {
+        return $this->belongsTo(Member::class);
+    }
+}

+ 35 - 0
app/Models/Subscription.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class Subscription extends Model
+{
+    use HasFactory;
+
+    protected $fillable = [
+        'name',
+        'causal_id',
+        'sub_causal_id',
+        'subscription_price',
+        'prices',
+        'enabled'
+    ];
+
+    public function getCount()
+    {
+        return \App\Models\MemberSubscription::where('subscription_id', $this->id)->count();
+    }
+
+    public function getCausal()
+    {
+        return $this->belongsTo(CourseSubscription::class);
+    }
+
+    public function getSubscriptionCausal()
+    {
+        return $this->belongsTo(CourseSubscription::class, 'sub_causal_id');
+    }
+}

File diff suppressed because it is too large
+ 181 - 177
composer.lock


+ 1 - 1
database/migrations/2025_10_18_135607_create_email_messages.php

@@ -16,7 +16,7 @@ return new class extends TenantMigration
             $t->enum('status', ['draft', 'scheduled', 'processing', 'sent', 'failed', 'canceled'])->default('draft')->index();
             $t->dateTime('schedule_at')->nullable()->index();
             $t->dateTime('sent_at')->nullable();
-            $t->foreignId('created_by')->constrained('users')->cascadeOnDelete();
+            $t->foreignId('created_by')->index();
             $t->timestamps();
         });
 

+ 34 - 0
database/migrations/2025_11_12_164023_add_is_deleted_to_members.php

@@ -0,0 +1,34 @@
+<?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('members', function (Blueprint $table) {
+            $table->boolean('is_deleted')->nullable();
+            $table->timestamp('deleted_date')->nullable()->after('is_deleted');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('members', function (Blueprint $table) {
+            $table->dropColumn('is_deleted');
+            $table->dropColumn('deleted_date');
+        });
+    }
+};

+ 33 - 0
database/migrations/2025_11_14_145909_add_course_id_to_records_rows.php

@@ -0,0 +1,33 @@
+<?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('records_rows', function (Blueprint $table) {
+            $table->unsignedBigInteger('course_id')->nullable()->after('causal_id');
+            $table->foreign('course_id')->nullable()->references('id')->on('courses')->onUpdate('cascade')->onDelete('cascade');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('records_rows', function (Blueprint $table) {
+            $table->dropColumn('course_id');
+        });
+    }
+};

+ 33 - 0
database/migrations/2025_11_17_114817_add_course_id_to_receipts_rows.php

@@ -0,0 +1,33 @@
+<?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('receipts_rows', function (Blueprint $table) {
+            $table->unsignedBigInteger('course_id')->nullable()->after('causal_id');
+            $table->foreign('course_id')->nullable()->references('id')->on('courses')->onUpdate('cascade')->onDelete('cascade');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('receipts_rows', function (Blueprint $table) {
+            $table->dropColumn('course_id');
+        });
+    }
+};

+ 41 - 0
database/migrations/2025_11_21_144756_create_sms_messages.php

@@ -0,0 +1,41 @@
+<?php
+
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+use App\Database\Migrations\TenantMigration;
+
+return new class extends TenantMigration
+{
+    public function up()
+    {
+        Schema::create('sms_messages', function (Blueprint $t) {
+            $t->id();
+            $t->string('subject');
+            $t->longText('content');
+            $t->enum('status', ['draft', 'scheduled', 'processing', 'sent', 'failed', 'canceled'])->default('draft')->index();
+            $t->dateTime('schedule_at')->nullable()->index();
+            $t->dateTime('sent_at')->nullable();
+            $t->unsignedBigInteger('created_by')->index();
+            $t->timestamps();
+        });
+
+        Schema::create('sms_message_recipients', function (Blueprint $t) {
+            $t->id();
+            $t->foreignId('sms_message_id')->constrained('sms_messages')->cascadeOnDelete();
+            $t->foreignId('member_id')->nullable()->constrained('members')->nullOnDelete();
+            $t->string('phone');
+            $t->enum('status', ['pending', 'sent', 'failed', 'bounced', 'skipped'])->default('pending')->index();
+            $t->text('error_message')->nullable();
+            $t->dateTime('sent_at')->nullable();
+            $t->timestamps();
+
+            $t->index(['sms_message_id', 'status']);
+        });
+    }
+
+    public function down()
+    {
+        Schema::dropIfExists('sms_message_recipients');
+        Schema::dropIfExists('sms_messages');
+    }
+};

+ 87 - 0
database/migrations/2025_11_26_132239_create_subscriptions_tables.php

@@ -0,0 +1,87 @@
+<?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::create('subscriptions', function (Blueprint $table) {
+            $table->id();
+            $table->string('name');
+            $table->unsignedBigInteger('causal_id')->nullable();
+            $table->unsignedBigInteger('sub_causal_id')->nullable();
+            $table->decimal('subscription_price', 8, 2);
+            $table->longText('prices')->nullable();
+            $table->boolean('enabled')->default(1);
+            $table->softDeletes();
+            $table->timestamps();
+
+            $table->foreign('causal_id')->nullable()->references('id')->on('causals')->onUpdate('cascade')->onDelete('cascade');
+            $table->foreign('sub_causal_id')->nullable()->references('id')->on('causals')->onUpdate('cascade')->onDelete('cascade');
+        });
+
+        Schema::table('records_rows', function (Blueprint $table) {
+            $table->unsignedBigInteger('subscription_id')->nullable();
+            $table->foreign('subscription_id')->nullable()->references('id')->on('subscriptions')->onUpdate('cascade')->onDelete('cascade');
+        });
+        Schema::table('receipts_rows', function (Blueprint $table) {
+            $table->unsignedBigInteger('subscription_id')->nullable();
+            $table->foreign('subscription_id')->nullable()->references('id')->on('subscriptions')->onUpdate('cascade')->onDelete('cascade');
+        });
+
+
+        Schema::create('member_subscriptions', function (Blueprint $table) {
+            $table->id();
+            $table->unsignedBigInteger('member_id');
+            $table->unsignedBigInteger('subscription_id');
+            $table->date('date_from')->nullable();
+            $table->date('date_to')->nullable();
+            $table->decimal('price', 8, 2);
+            $table->unsignedBigInteger('course_subscription_id')->nullable();
+            $table->decimal('subscription_price', 8, 2);
+            $table->string('notes')->nullable();
+            $table->timestamps();
+
+            $table->foreign('member_id')->nullable()->references('id')->on('members')->onUpdate('cascade')->onDelete('cascade');
+            $table->foreign('subscription_id')->nullable()->references('id')->on('subscriptions')->onUpdate('cascade')->onDelete('cascade');
+            $table->foreign('course_subscription_id')->nullable()->references('id')->on('course_subscriptions')->onUpdate('cascade')->onDelete('cascade');
+        });
+
+        Schema::table('rates', function (Blueprint $table) {
+            $table->unsignedBigInteger('member_subscription_id')->nullable();
+            $table->foreign('member_subscription_id')->nullable()->references('id')->on('member_subscriptions')->onUpdate('cascade')->onDelete('cascade');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('rates', function (Blueprint $table) {
+            $table->dropColumn('member_subscription_id');
+        });
+
+        Schema::dropIfExists('member_subscriptions');
+
+
+        Schema::table('records_rows', function (Blueprint $table) {
+            $table->dropColumn('subscription_id');
+        });
+        Schema::table('receipts_rows', function (Blueprint $table) {
+            $table->dropColumn('subscription_id');
+        });
+
+        Schema::dropIfExists('subscriptions');
+    }
+};

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

@@ -432,6 +432,7 @@
 
 .table-cell.net {
     font-weight: 600;
+    white-space: nowrap;
 }
 
 .table-row.positive .table-cell.net {
@@ -613,6 +614,11 @@
     font-weight: 600;
 }
 
+.course-table .table-cell.earned {
+    justify-content: flex-end;
+    font-weight: 600;
+}
+
 .course-table .table-cell.delta.positive {
     color: #10b981;
 }
@@ -629,9 +635,19 @@
     justify-content: center;
     font-weight: 600;
     font-size: 0.8rem;
+    color: #10b981;
+}
+
+.course-table .table-cell.earned.positive {
+    color: #10b981;
+}
+
+.course-table .table-cell.earned.neutral {
+    color: #6b7280;
 }
 
-/* .course-table .table-cell.percentage.good {
+/*
+.course-table .table-cell.percentage.good {
     color: #059669;
 }
 

+ 44 - 26
public/css/new_style.css

@@ -367,8 +367,8 @@ body label.form-label {
     color: black;
 }
 
-body .form-control,
-body form .form-control,
+body .form-control:not(textarea),
+body form .form-control:not(textarea),
 body .form-select,
 body form .form-select,
 body .select2-selection,
@@ -381,21 +381,25 @@ body .btn.dropdown-toggle {
     background-color: #f5f8fa !important;
 }
 
-body .form-check-input,
-body form .form-check-input {
+body textarea.form-control,
+body form textarea.form-control {
+    border-radius: 15px !important;
+    border: 1px solid #d3dce1 !important;
+    background-color: #f5f8fa !important;
+}
+
+body input.form-check-input,
+body form input.form-check-input {
     height: 20px !important;
     width: 20px !important;
     border-radius: 5px !important;
     padding: 0 !important;
 }
 
-body .form-check-input:checked[type="checkbox"],
-body
-    form
-    .form-check-input:checked[type="checkbox"]
-    body
-    .form-check-input:checked[type="radio"],
-body form .form-check-input:checked[type="radio"] {
+body input.form-check-input:checked[type="checkbox"],
+body form input.form-check-input:checked[type="checkbox"],
+body input.form-check-input:checked[type="radio"],
+body form input.form-check-input:checked[type="radio"] {
     background-color: var(--color-blu) !important;
 }
 
@@ -870,24 +874,9 @@ body .dashboard-container .participation-fill {
     height: 100%;
     transition: width 0.3s ease;
     border-radius: 10px;
-}
-
-body .dashboard-container .participation-fill.padel {
-    background: #ffd700;
-}
-
-body .dashboard-container .participation-fill.tennis {
     background: #8b4cf7;
 }
 
-body .dashboard-container .participation-fill.pallavolo {
-    background: #ff6b35;
-}
-
-body .dashboard-container .participation-fill.yoga {
-    background: var(--color-verde);
-}
-
 body .dashboard-container .chart-title {
     font-size: 16px;
     font-weight: 600;
@@ -1213,4 +1202,33 @@ a.member-file-url {
 
 .password-wrapper .password-eye.shown .password-shown {
     display: block;
+}
+
+.form-radio-wrapper {
+    display: inline-flex;
+    gap: 4px;
+    margin-right: 20px;
+    font-size: 1em;
+}
+
+.form-radio-wrapper:last-of-type {
+    margin-right: 0;
+}
+
+.table-actions button {
+    border-color: #d3dce1;
+    background-color: #f5f8fa !important;
+    width: 42px;
+    height: 40px;
+    display: inline-flex;
+    flex-direction: row;
+    flex-wrap: nowrap;
+    align-content: center;
+    justify-content: center;
+    align-items: center;
+}
+
+.table-actions button:hover {
+    color: white;
+    background-color: var(--color-blu) !important;
 }

+ 10 - 0
resources/views/first-login.blade.php

@@ -5,6 +5,16 @@
     <meta name="viewport" content="width=device-width, initial-scale=1">
     <title>Primo Accesso - Leezard.cloud</title>
 
+  {{-- Favicon --}}
+    <link rel="icon" type="image/png" href="{{asset('favicon-96x96.png')}}" sizes="96x96" />
+    <link rel="icon" type="image/svg+xml" href="{{asset('favicon.svg')}}" />
+    <link rel="shortcut icon" href="{{asset('favicon.ico')}}"/>
+    <link rel="shortcut icon" href="{{asset('favicon-light.ico')}}" media="(prefers-color-scheme: light)"/>
+    <link rel="shortcut icon" href="{{asset('favicon-dark.ico')}}" media="(prefers-color-scheme: dark)"/>
+    <link rel="apple-touch-icon" sizes="180x180" href="{{asset('apple-touch-icon.png')}}" />
+    <link rel="manifest" href="{{asset('site.webmanifest')}}" />
+  {{-- end Favicon --}}
+
     <!-- Bootstrap CSS -->
     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
     <!-- Font Awesome -->

+ 58 - 26
resources/views/layouts/app.blade.php

@@ -162,6 +162,8 @@
                 print "Dashboard";
             if (Request::is('members'))
                 print "Utenti";
+            if (Request::is('members_archive'))
+                print "Archivio utenti";
             if (Request::is('suppliers'))
                 print "Fornitori";
             if (Request::is('in'))
@@ -186,14 +188,14 @@
                 print "Pagamento corsi";
             if (Request::is('presence_reports'))
                 print "Report presenze";                
-            if (Request::is('calenda'))
+            if (Request::is('calendar'))
                 print "Calendario";                
             if (Request::is('reminders'))
                 print "Scadenze";
             if (Request::is('settings'))
                 print "Impostazioni";
             if (Request::is('categories'))
-                print "Gruppi di appartenenza";
+                print "Gruppi di interesse";
             if (Request::is('disciplines'))
                 print "Discipline";
             if (Request::is('cards'))
@@ -201,7 +203,7 @@
             if (Request::is('rates'))
                 print "Pagamenti";
             if (Request::is('course_subscriptions'))
-                print "Corsi - Abbonamenti";
+                print "Corsi - Frequenza di pagamenti";
             if (Request::is('courses'))
                 print "Corsi";
             if (Request::is('course_durations'))
@@ -210,10 +212,10 @@
                 print "Corsi - Frequenza";
             if (Request::is('course_levels'))
                 print "Corsi - Livelli";
-            if (Request::is('course_types'))
-                print "Corsi - Tipologie";
+            // if (Request::is('course_types'))
+            //     print "Corsi - Tipologie";
             if (Request::is('banks'))
-                print "Canali finanziari";
+                print "Banche";
             if (Request::is('causals'))
                 print "Causali";
             if (Request::is('vats'))
@@ -221,17 +223,21 @@
             if (Request::is('payment_methods'))
                 print "Metodi di pagamento";
             if (Request::is('users'))
-                print "Profilo utenti";
+                print "Utenze";
             if (Request::is('azienda'))
                 print "Società";
             if (Request::is('profile'))
                 print "Profilo Utente";
             if (Request::is('reports'))
                 print "Reports";
+            if (Request::is('subscriptions'))
+                print "Abbonamenti";
+            if (Request::is('subscription_member', 'subscription_member/*'))
+                print "Abbonamenti";
+            if (Request::is('mail_comunications'))
+                print "Email";
             if (Request::is('sms_comunications'))
-                print "Comunicazioni SMS";
-            if (Request::is('email_comunications'))
-                print "Comunicazioni Email";
+                print "Sms";
             @endphp
             </h3>
 
@@ -271,12 +277,12 @@
                 <div id="accordionExample" style="width:100%">
                     <div class="accordion-item">
                         <h2 class="accordion-header linkMenu" id="headingOne" style="margin-top:50px;">
-                            <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="{{Request::is('members') || Request::is('suppliers') ? 'true' : 'false'}}" aria-controls="collapseOne">
+                            <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="{{Request::is('members') || Request::is('suppliers') || Request::is('members_archive') ? 'true' : 'false'}}" aria-controls="collapseOne">
                                 <i class="fas fa-user"></i>
                                 <span>Anagrafiche</span>
                             </button>
                         </h2>
-                        <div id="collapseOne" class="accordion-collapse collapse {{Request::is('members') || Request::is('suppliers') ? 'show' : ''}}" aria-labelledby="headingOne" data-bs-parent="#accordionExample">
+                        <div id="collapseOne" class="accordion-collapse collapse {{Request::is('members') || Request::is('suppliers') || Request::is('members_archive') ? 'show' : ''}}" aria-labelledby="headingOne" 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-anagrafica" style="margin-top:0px;">
                                     <li class="nav-item {{Request::is('members') ? "nav-item-active" : ""}}">
@@ -291,6 +297,11 @@
                                             </a>
                                         </li>
                                     @endif
+                                    <li class="nav-item {{Request::is('members_archive') ? "nav-item-active" : ""}}">
+                                        <a href="/members_archive" class="nav-link d-flex align-items-center linkMenu">
+                                            <span class="ms-3 d-md-inline">Archivio</span>
+                                        </a>
+                                    </li>
                                 </ul>
                             </div>
                         </div>
@@ -345,17 +356,17 @@
                     </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="#collapseThree" aria-expanded="{{Request::is('course_member_one') || Request::is('course_member_two') || Request::is('course_list') || Request::is('course_member') || Request::is('presence_reports') || Request::is('calendar')  ? 'true' : 'false'}}" aria-controls="collapseThree">
+                            <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseThree" aria-expanded="{{Request::is('course_member_one') || Request::is('course_member_two') || Request::is('subscription_member', 'subscription_member/*') || Request::is('course_list') || Request::is('course_member') || Request::is('presence_reports') || Request::is('calendar') ? 'true' : 'false'}}" aria-controls="collapseThree">
                                 <i class="fas fa-calendar"></i>
                                 <span>Corsi</span>
                             </button>
                         </h2>
                         @php
                         $iscritti_types = [];
-                        $iscritti_types[] = array('name' => 'Standard', 'type' => 'standard');
+                        $iscritti_types[] = array('name' => 'Singoli', 'type' => 'standard');
                         $iscritti_types[] = array('name' => 'Personalizzati', 'type' => 'custom');   
                         @endphp
-                        <div id="collapseThree" class="accordion-collapse collapse {{Request::is('course_member_one') || Request::is('course_member_two') || Request::is('course_list') || Request::is('course_member') || Request::is('rates') || Request::is('presence_reports') || Request::is('calendar') ? 'show' : ''}}" aria-labelledby="headingThree" data-bs-parent="#accordionExample">
+                        <div id="collapseThree" class="accordion-collapse collapse {{Request::is('course_member_one') || Request::is('course_member_two') || Request::is('subscription_member', 'subscription_member/*') || Request::is('course_list') || Request::is('course_member') || Request::is('rates') || Request::is('presence_reports') || Request::is('calendar') ? '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('course_member_one') || Request::is('course_member_two') || Request::is('course_member') ? "nav-item-active" : ""}}">
@@ -381,6 +392,12 @@
                                     </li>
                                     @endforeach
 
+                                    <li class="nav-item {{Request::is('subscription_member', 'subscription_member/*') ? "nav-item-active" : ""}}">
+                                        <a href="/subscription_member" class="nav-link d-flex align-items-center linkMenu">
+                                            <span class="ms-3 d-md-inline">Abbonamenti</span>
+                                        </a>
+                                    </li>
+
                                     @if(false)
                                         <li class="nav-item {{Request::is('course_list') ? "nav-item-active" : ""}}">
                                             <a href="/course_list" class="nav-link d-flex align-items-center linkMenu">
@@ -413,7 +430,17 @@
                         </div>
                     @endif
                     @if(Auth::user()->level == env('LEVEL_ADMIN', 0))
-                        <div class="accordion-item {{Request::is('settings') || Request::is('categories') || Request::is('disciplines') || Request::is('cards') || Request::is('course_subscriptions') || Request::is('courses') || Request::is('course_durations') || Request::is('course_frequencies') || Request::is('course_levels') || Request::is('course_types') || Request::is('banks') || Request::is('causals') || Request::is('vats') || Request::is('payment_methods') || Request::is('users') ? "accordion-item-active" : ""}}">
+                        <div class="accordion-item {{Request::is('reports') ? "accordion-item-active" : ""}}">
+                            <h2 class="accordion-header linkMenu">
+                                <a class="accordion-button collapsed" href="/reports">
+                                    <i class="fas fa-chart-line"></i>
+                                    <span>Reports</span>
+                                </a>
+                            </h2>
+                        </div>
+                    @endif
+                    @if(Auth::user()->level == env('LEVEL_ADMIN', 0))
+                        <div class="accordion-item {{Request::is('settings') || Request::is('categories') || Request::is('disciplines') || Request::is('cards') || Request::is('course_subscriptions') || Request::is('courses') || Request::is('course_durations') || Request::is('course_frequencies') || Request::is('course_levels') {{-- || Request::is('course_types') --}} || Request::is('banks') || Request::is('causals') || Request::is('vats') || Request::is('payment_methods') || Request::is('users') ? "accordion-item-active" : ""}}">
                             <h2 class="accordion-header linkMenu">
                                 <a class="accordion-button collapsed" href="/settings">
                                     <i class="fas fa-gear"></i>
@@ -432,16 +459,6 @@
                             </div>
                         @endif
                     @endif
-                    @if(Auth::user()->level == env('LEVEL_ADMIN', 0))
-                        <div class="accordion-item {{Request::is('reports') ? "accordion-item-active" : ""}}">
-                            <h2 class="accordion-header linkMenu">
-                                <a class="accordion-button collapsed" href="/reports">
-                                    <i class="fas fa-chart-line"></i>
-                                    <span>Reports</span>
-                                </a>
-                            </h2>
-                        </div>
-                    @endif
                 </div>
                 </div>
             </div>
@@ -492,6 +509,21 @@
     @stack('scripts')
 
     <script>
+        function togglePassword(eye_icon) {
+            let wrapper = eye_icon.parentElement;
+            let password_html = wrapper.querySelector("input");
+
+            if (password_html) {
+                if (password_html.type == "password") {
+                    password_html.type = "text";
+                    eye_icon.classList.add("shown");
+                } else {
+                    password_html.type = "password";
+                    eye_icon.classList.remove("shown");
+                }
+            }
+        }
+
         function setToday(d)
         {
             $("#dateFrom").val(d);

+ 45 - 53
resources/views/livewire/category.blade.php

@@ -2,28 +2,24 @@
 
     @if(!$add && !$update)
 
-    <a class="btn--ui lightGrey" href="/settings?type=anagrafica"><i class="fa-solid fa-arrow-left"></i></a><br>
+        <a class="btn--ui lightGrey" href="/settings?type=anagrafica"><i class="fa-solid fa-arrow-left"></i></a>
 
-    <header id="title--section"  class="d-flex align-items-center justify-content-between">
-        <div class="title--section_name d-flex align-items-center justify-content-between" style="display:none !important" >
-            <i class="ico--ui title_section utenti me-2"></i>
-            <h2 class="primary">@if(!$add && !$update)Categorie @else Inserimento/modifica categoria @endif</h2>
-        </div>
+        <header id="title--section"  class="d-flex align-items-center justify-content-between" style="display:none !important">
+            <div class="title--section_name d-flex align-items-center justify-content-between">
+                <i class="ico--ui title_section utenti me-2"></i>
+                <h2 class="primary">@if(!$add && !$update)Categorie @else Inserimento/modifica categoria @endif</h2>
+            </div>
+        </header>
 
         @if(!$add && !$update)
-            <div class="title--section_addButton text-right "  wire:click="add()" style="cursor: pointer;margin-top:20px;">
-                <div class="btn--ui entrata d-flex justify-items-between">
+            <div class="title--section_addButton d-flex justify-content-end" >
+                {{-- <button type="button" class="btn--ui entrata" wire:click="openImportModal" onclick="openImportModal()">Importa</button> --}}
+                <div class="btn--ui entrata d-flex justify-items-between" wire:click="add()" style="cursor: pointer;">
                     <a href="#" wire:click="add()" style="color:white">AGGIUNGI</a>
                 </div>
-            </div>
-
+            </div><br>
         @endif
 
-    </header>
-
-
-
-
         <section id="resume-table">
             <div class="compare--chart_wrapper d-none"></div>
 
@@ -45,7 +41,7 @@
                             </td>
                         </tr>
                         @if(count($record->childs))
-                            @include('livewire/category_child',['records' => $record->childs, 'indentation' => 1])
+                            @include('livewire/category_child',['records' => $record->childs->sortBy('name'), 'indentation' => 1])
                         @endif
                     @endforeach
 
@@ -55,50 +51,46 @@
         </section>
 
     @else
+        <a class="btn--ui lightGrey" href="/categories"><i class="fa-solid fa-arrow-left"></i></a><br><br>
 
-        <div class="container">
-
-            <a class="btn--ui lightGrey" href="/categories"><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') }}
-                </div>
-            @endif
+        @if (session()->has('error'))
+            <div class="alert alert-danger" role="alert">
+                {{ session()->get('error') }}
+            </div>
+        @endif
 
-            <div class="row">
-                <div class="col">
+        <div class="row">
+            <div class="col">
 
-                    <form action="">
+                <form action="">
 
-                        @if($parent != '')
-                            <h3>Aggiungi livello a {{$parent}}</h3><br>
-                        @endif
+                    @if($parent != '')
+                        <h3>Aggiungi livello a {{$parent}}</h3><br>
+                    @endif
 
-                        <div class="row mb-3">
-                            <div class="col">
-                                <div class="form--item">
-                                    <label for="inputName" class="form-label">Nome</label>
-                                    <input class="form-control js-keyupTitle @error('name') is-invalid @enderror" type="text" id="name" placeholder="Nome" wire:model="name">
-                                    @error('name')
-                                        <div class="invalid-feedback">{{ $message }}</div>
-                                    @enderror
-                                </div>
+                    <div class="row mb-3">
+                        <div class="col">
+                            <div class="form--item">
+                                <label for="inputName" class="form-label">Nome</label>
+                                <input class="form-control js-keyupTitle @error('name') is-invalid @enderror" type="text" id="name" placeholder="Nome" wire:model="name">
+                                @error('name')
+                                    <div class="invalid-feedback">{{ $message }}</div>
+                                @enderror
                             </div>
                         </div>
-
-                        <div class="form--item">
-                            <button type="button" class="btn--ui lightGrey" wire:click="cancel()">Annulla</button>
-                        @if($add)
-                            <button type="submit" class="btn--ui" wire:click.prevent="store()">Salva</button>
-                        @endif
-                        @if($update)
-                            <button type="submit" class="btn--ui" wire:click.prevent="update()">Salva</button>
-                        @endif
-                        </div>
-
-                    </form>
-                </div>
+                    </div>
+
+                    <div class="form--item">
+                        <button type="button" class="btn--ui lightGrey" wire:click="cancel()">Annulla</button>
+                    @if($add)
+                        <button type="submit" class="btn--ui" wire:click.prevent="store()">Salva</button>
+                    @endif
+                    @if($update)
+                        <button type="submit" class="btn--ui" wire:click.prevent="update()">Salva</button>
+                    @endif
+                    </div>
+
+                </form>
             </div>
         </div>
 

+ 12 - 10
resources/views/livewire/course.blade.php

@@ -76,7 +76,7 @@
                         <tr>
                             <td style="text-align:center"><input type="checkbox" class="chkCourse" name="{{$record->id}}"></td>
                             <td>{{$record->year}}</td>
-                            <td>{{$record->type == 'standard' ? 'Standard' : 'Personalizzato'}}</td>
+                            <td>{{$record->type == 'standard' ? 'Singolo' : 'Personalizzato'}}</td>
                             <td>{{$record->name}}</td>
                             <td>{{$record->level->name ?? ""}}</td>
                             <td>{{$record->frequency->name ?? ""}}</td>
@@ -113,7 +113,7 @@
 
                         <div class="row mb-3 ">
                             <div class="col-12 mt-2">
-                                <input type="radio" name="chkType" value="standard" wire:model="type">&nbsp;Standard
+                                <input type="radio" name="chkType" value="standard" wire:model="type">&nbsp;Singolo
                                 &nbsp;&nbsp;&nbsp;
                                 <input type="radio" name="chkType" value="custom" wire:model="type">&nbsp;Personalizzato
                             </div>
@@ -127,7 +127,7 @@
                                 </div>
                             </div>
                             @if(false)
-                                <div class="col-6 mt-2">
+                                {{-- <div class="col-6 mt-2">
                                     <div class="form--item">
                                         <label for="inputName" class="form-label">Tipologia</label>
                                         <select class="form-select form-select-lg me-1 @error('course_type_id') is-invalid @enderror" wire:model="course_type_id">
@@ -137,7 +137,7 @@
                                             @endforeach
                                         </select>
                                     </div>
-                                </div>
+                                </div> --}}
                                 <div class="col-6 mt-2">
                                     <div class="form--item">
                                         <label for="inputName" class="form-label">Durata</label>
@@ -175,13 +175,13 @@
                             <div class="col-3 mt-2">
                                 <div class="form--item">
                                     <label for="inputName" class="form-label">Data inizio</label>
-                                    <input class="form-control @error('name') is-invalid @enderror" type="date" placeholder="Data inizio" wire:model="date_from">
+                                    <input class="form-control @error('date_from') is-invalid @enderror" type="date" placeholder="Data inizio" wire:model="date_from">
                                 </div>
                             </div>
                             <div class="col-3 mt-2">
                                 <div class="form--item">
                                     <label for="inputName" class="form-label">Data fine</label>
-                                    <input class="form-control @error('name') is-invalid @enderror" type="date" placeholder="Data fine" wire:model="date_to">
+                                    <input class="form-control @error('date_to') is-invalid @enderror" type="date" placeholder="Data fine" wire:model="date_to">
                                 </div>
                             </div>
                             @if(false)
@@ -258,7 +258,7 @@
                                     <select class="form-select form-select-lg" wire:model="instructor_id">
                                         <option value="">
                                         @foreach($instructors as $ins)
-                                            <option value="{{$ins["id"]}}">{{$ins["name"]}}
+                                            <option value="{{$ins["id"]}}">{{$ins["name"]}} {{$ins["cognome"]}}</option>
                                         @endforeach
                                     </select>
                                 </div>
@@ -266,7 +266,7 @@
 
                             <div class="col-6 mt-2">
                                 <div class="form--item">
-                                    <label for="inputName" class="form-label">Gruppo di appartenenza</label>
+                                    <label for="inputName" class="form-label">Gruppo di interesse</label>
                                     <select class="form-select form-select-lg me-1 @error('category_id') is-invalid @enderror" wire:model="category_id">
                                         <option value="">
                                         @foreach($categories as $category)
@@ -302,7 +302,7 @@
                                 @foreach($prices as $idP => $p)
                                     <div class="row mt-2 ">
                                         <div class="col-6">
-                                            <label for="abb" class="form-label">Abbonamento</label>
+                                            <label for="abb" class="form-label">Pagamento</label>
                                             <select class="form-control" wire:model="prices.{{$idP}}.course_subscription_id">
                                                 <option value=""></option>
                                                 @foreach($course_subscriptions as $s)
@@ -332,7 +332,9 @@
                             @if($type == 'standard')
 
                                 @foreach($when as $idW => $w)
-
+                                    @if ($idW > 0)
+                                    <div class="col-6"></div>
+                                    @endif
                                     <div class="col-6 mt-4">
                                         <div class="day durata--corso d-flex">
                                             <label for="" class="form-label">Giorno</label>

+ 2 - 2
resources/views/livewire/course_list.blade.php

@@ -118,7 +118,7 @@
                     </div>
                 </div>
             </div>
-            <div class="col-md-2">
+            {{-- <div class="col-md-2">
                 <div class="row">
                     <div class="col-md-12" style="margin-bottom:10px;">
                         <b>Tipologia</b>
@@ -131,7 +131,7 @@
                         </select>
                     </div>
                 </div>
-            </div>
+            </div> --}}
             <div class="col-md-2">
                 <div class="row">
                     <div class="col-md-12" style="margin-bottom:10px;">

+ 2 - 2
resources/views/livewire/course_list_original.blade.php

@@ -79,7 +79,7 @@
                     </div>
                 </div>
             </div>
-            <div class="col-md-2">
+            {{-- <div class="col-md-2">
                 <div class="row">
                     <div class="col-md-12" style="margin-bottom:10px;">
                         <b>Tipologia</b>
@@ -92,7 +92,7 @@
                         </select>
                     </div>
                 </div>
-            </div>
+            </div> --}}
             <div class="col-md-2">
                 <div class="row">
                     <div class="col-md-12" style="margin-bottom:10px;">

+ 23 - 5
resources/views/livewire/course_member.blade.php

@@ -12,6 +12,23 @@
 
     <a class="btn--ui lightGrey" href="/course_member_two?type={{$type}}"><i class="fa-solid fa-arrow-left"></i></a><br>
 
+    @if ($course)
+    @php
+        $courseName = $course->name ?? 'Corso Sconosciuto';
+        $levelName = is_object($course->level) ? $course->level->name : '';
+        $frequencyName = is_object($course->frequency) ? $course->frequency->name : '';
+
+        $courseInfoParts = [];
+        if ($levelName) $courseInfoParts[] = $levelName;
+        if ($frequencyName) $courseInfoParts[] = $frequencyName;
+
+        $courseInfo = implode(' - ', $courseInfoParts);
+    @endphp
+        <h3>{{$courseName}}</h3>
+        <p>{{$courseInfo}}</p>
+    @endif
+
+
     <div class="showFilter" style="display:none" wire:ignore.self>
         <hr size="1">
         <div class="row g-3">
@@ -57,7 +74,7 @@
                     </div>
                 </div>
             </div>
-            <div class="col-md-2">
+            {{-- <div class="col-md-2">
                 <div class="row">
                     <div class="col-md-12" style="margin-bottom:10px;">
                         <b>Tipologia</b>
@@ -70,7 +87,7 @@
                         </select>
                     </div>
                 </div>
-            </div>
+            </div> --}}
             <div class="col-md-2">
                 <div class="row">
                     <div class="col-md-12" style="margin-bottom:10px;">
@@ -272,7 +289,7 @@
             <thead>
                 <tr>
                     <th scope="col">#</th>
-                    <th scope="col">Corso</th>
+                    {{-- <th scope="col">Corso</th> --}}
                     <th scope="col">Cognome</th>
                     <th scope="col">Nome</th>
                     <th scope="col">Certificato</th>
@@ -653,7 +670,7 @@
                 },
                 columns: [
                     { data: 'column_0' },
-                    { data: 'column_8' },
+                    // { data: 'column_8' },
                     {
                         data: 'column_1',
                         render: function (data, type, row) {
@@ -693,7 +710,8 @@
                                 html += '<span style="float:right"></span>';
                             }else {
                                 console.log(status);
-                                html += 'Non Presente';
+                                html += '<i class="ico--ui check absent me-2"></i>';
+                                html += 'Non consegnato';
                             }
                             html += '</span>';
                             return html;

+ 6 - 4
resources/views/livewire/course_member_two.blade.php

@@ -9,7 +9,7 @@
 
     </header>
 
-    <a class="btn--ui lightGrey" href="/course_member_one"><i class="fa-solid fa-arrow-left"></i></a><br>
+    {{-- <a class="btn--ui lightGrey" href="/course_member_one"><i class="fa-solid fa-arrow-left"></i></a><br> --}}
 
     <section id="resume-table">
         <div class="compare--chart_wrapper d-none"></div>
@@ -20,11 +20,12 @@
                     <th scope="col">Nome</th>
                     <th scope="col">Frequenza</th>
                     <th scope="col">Livello</th>
-                    <th scope="col">Durata</th>
+                    {{-- <th scope="col">Durata</th> --}}
                     @if($_GET["type"] != "custom")
                         <th scope="col">Giorni</th>
                     @endif
                     <th scope="col">Istruttore</th>
+                    <th scope="col">N° partecipanti</th>
                     <th scope="col">...</th>
                 </tr>
             </thead>
@@ -34,7 +35,7 @@
                         <td>{{$record->name}}</td>
                         <td>{{$record->frequency->name ?? ""}}</td>
                         <td>{{$record->level->name ?? ""}}</td>
-                        <td>{{date("d/m/Y", strtotime($record->date_from))}} - {{date("d/m/Y", strtotime($record->date_to))}}                                                                </td>
+                        {{-- <td>{{date("d/m/Y", strtotime($record->date_from))}} - {{date("d/m/Y", strtotime($record->date_to))}}                                                         </td> --}}
                         @if($_GET["type"] != "custom")
                         <td>
                             @if($record->when != null)
@@ -50,7 +51,8 @@
                             @endif
                         </td>
                         @endif
-                        <td>{{$record->getins->name ?? ""}}</td>
+                        <td>{{$record->getins->name ?? ""}} {{$record->getins->cognome ?? ""}}</td>
+                        <td>{{$record->getCount()}}</td>
                         <td>
                             <a href="/course_member?id={{$record->id}}" type="button" class="btn" ><b> <i class="fa-solid fa-chevron-right"></i></b></button>
                         </td>

+ 2 - 2
resources/views/livewire/course_subscription.blade.php

@@ -49,10 +49,10 @@
 
     @else
 
+        <a class="btn--ui lightGrey" href="/course_subscriptions"><i class="fa-solid fa-arrow-left"></i></a><br><br>
+        
         <div class="container">
 
-            <a class="btn--ui lightGrey" href="/course_subscriptions"><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') }}

+ 2 - 14
resources/views/livewire/dashboard.blade.php

@@ -80,19 +80,6 @@
                         <div class="course-divider"></div>
                         <div class="course-name">{{$course['full_name'] ?? $course['course_name']}}</div>
                         <div class="course-days">{{$course['days']}}</div>
-                        {{-- @if(!empty($course['level_name']) || !empty($course['frequency_name']))
-                        <div style="font-size: 11px; color: #888; margin-top: 4px;">
-                            @if(!empty($course['level_name']))
-                            <span>{{$course['level_name']}}</span>
-                            @endif
-                            @if(!empty($course['level_name']) && !empty($course['frequency_name']))
-                            <span> • </span>
-                            @endif
-                            @if(!empty($course['frequency_name']))
-                            <span>{{$course['frequency_name']}}</span>
-                            @endif
-                        </div>
-                        @endif --}}
                     </div>
                     @endforeach
                 </div>
@@ -244,7 +231,8 @@
 
             <div class="dashboard-card dashboard-stat card-inverted card-topay">
                 <div class="dashboard-card-header">
-                    <div class="dashboard-card-title">Da pagare<br/><small><small><i>{{$current_month}}</i></small></small></div>
+                    {{-- <div class="dashboard-card-title">Da pagare<br/><small><small><i>{{$current_month}}</i></small></small></div> --}}
+                    <div class="dashboard-card-title">Da pagare<br/><small><small><i>&nbsp;</i></small></small></div>
                     <i class="dashboard-card-icon fa-solid fa-money-bill-transfer"></i>
                 </div>
                 <div class="dashboard-card-value">{{number_format($toPay, 2, ",", ".")}}</div>

+ 57 - 43
resources/views/livewire/email_comunications.blade.php

@@ -68,15 +68,15 @@
                             <td><span class="badge bg-{{$badgeMap[$state]}}">{{ $record->status }}</span></td>
                             <td>
                                 @if(!empty($record->schedule_at))
-                                    {{ optional($record->schedule_at)->setTimezone('Europe/Rome')->format('d/m/Y H:i') }}
+                                    {{ optional($record->schedule_at)->setTimezone('Europe/Rome')->format('d M Y - H:i') }}
                                 @endif
                             </td>
                             <td>
                                 @if(!empty($record->sent_at))
-                                    {{ optional($record->sent_at)->setTimezone('Europe/Rome')->format('d/m/Y H:i') }}
+                                    {{ optional($record->sent_at)->setTimezone('Europe/Rome')->format('d M Y - H:i') }}
                                 @endif
                             </td>
-                            <td>{{ optional($record->created_at)->setTimezone('Europe/Rome')->format('d/m/Y H:i') }}</td>
+                            <td>{{ optional($record->created_at)->setTimezone('Europe/Rome')->format('d M Y - H:i') }}</td>
                             <td class="d-flex gap-2">
                                 <button type="button" class="btn" wire:click="edit({{ $record->id }})" data-bs-toggle="tooltip" data-bs-trigger="hover focus" data-bs-placement="bottom" title="Modifica">
                                     <i class="fa-regular fa-pen-to-square"></i>
@@ -196,7 +196,7 @@
                                             </div>
                                             <div class="col-md-3">
                                                 <div class="row">
-                                                    <div class="col-md-12 mb-2"><b>Gruppo di appartenenza</b></div>
+                                                    <div class="col-md-12 mb-2"><b>Gruppo di interesse</b></div>
                                                     <div class="col-12">
                                                         <select name="filterCategories" class="form-select filterCategories" multiple="multiple">
                                                             <option value="">Tutte</option>
@@ -444,6 +444,8 @@
             </div>
         </div>
     </section>
+
+    <input type="hidden" name="timezone" id="timezone" wire:model="timezone">
 </div>
 
 @if (session()->has('success'))
@@ -491,13 +493,18 @@
                 dt.row(rowEl).remove().draw(false);
             }
         });
-    });
-
 
+        const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
+        @this.set('timezone', tz);
+    });
 
     window.addEventListener('init-recipients-table', (e) => {
         const selected = e.detail?.selected || [];
         loadDataTable(selected);
+
+        
+        const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
+        @this.set('timezone', tz);
     });
 
     function loadArchiveDataTable(){
@@ -690,7 +697,7 @@
                 { data: "categories" },
             ],
             order: [
-                [1, 'desc']
+                [1, 'asc']
             ],
             fixedHeader: false,
             thead: {
@@ -738,45 +745,52 @@
 import { ClassicEditor } from "ckeditor5";
 import { editorConfig } from "/assets/libraries/ckeditor5/config.js";
 
-window.addEventListener("load-editor", (e) => {
-    let detail = e.detail || {};
-    let latestLocked = !!detail.locked;
-    let html = detail.html ?? '';
-
-    let el = document.querySelector('#message');
-    if (el.ckeditorInstance) el.ckeditorInstance.destroy();
-    
-    editorConfig.simpleUpload = {
-        uploadUrl: "{{ route('ckeditor.upload', ['_token' => csrf_token()]) }}"
-    };
+    window.addEventListener("load-editor", (e) => {
+        let detail = e.detail || {};
+        let latestLocked = !!detail.locked;
+        let html = detail.html ?? '';
+
+        let el = document.querySelector('#message');
+        if (el.ckeditorInstance) el.ckeditorInstance.destroy();
+        
+        editorConfig.simpleUpload = {
+            uploadUrl: "{{ route('ckeditor.upload', ['_token' => csrf_token()]) }}"
+        };
+
+        ClassicEditor
+            .create(el, editorConfig)
+            .then(editor => {
+                el.ckeditorInstance = editor;
+
+                editor.setData(html);
+
+                if (latestLocked) {
+                    editor.enableReadOnlyMode('locked');
+                    return;
+                }
+            })
+            .catch(console.error)
+    });
 
-    ClassicEditor
-        .create(el, editorConfig)
-        .then(editor => {
-            el.ckeditorInstance = editor;
+    window.submitEmail = function(action){
+        const ed = document.querySelector('#message')?.ckeditorInstance;
+        const html = ed ? ed.getData() : '';
 
-            editor.setData(html);
+        if (action === 'draft') {
+            @this.call('saveDraft', html);
+        } else if (action === 'send') {
+            @this.call('sendNow', html);
+        } else {
+            @this.call('scheduleMessage', html);
+        }
+    };
 
-            if (latestLocked) {
-                editor.enableReadOnlyMode('locked');
-                return;
-            }
-        })
-        .catch(console.error)
-});
-
-window.submitEmail = function(action){
-    const ed = document.querySelector('#message')?.ckeditorInstance;
-    const html = ed ? ed.getData() : '';
-
-    if (action === 'draft') {
-        @this.call('saveDraft', html);
-    } else if (action === 'send') {
-        @this.call('sendNow', html);
-    } else {
-        @this.call('scheduleMessage', html);
-    }
-};
+    window.addEventListener("scroll-top", (e) => {
+        let wrapper = document.querySelector('#card--dashboard');
+        if (wrapper) {
+            wrapper.scrollTo({top: 0, behavior: 'smooth'});
+        }
+    });
 </script>
 
 {{-- END CKEditor --}}

File diff suppressed because it is too large
+ 523 - 291
resources/views/livewire/member.blade.php


+ 613 - 0
resources/views/livewire/member_archive.blade.php

@@ -0,0 +1,613 @@
+<div>
+    <div class="col card--ui" id="card--dashboard">
+        <header id="title--section" style="display:none !important"  class="d-flex align-items-center justify-content-between" style="display:none !important">
+            <div class="title--section_name d-flex align-items-center justify-content-between">
+                <i class="ico--ui title_section utenti me-2 "></i>
+                <h2 class="primary">Archivio utenti</h2>
+            </div>
+        </header>
+
+        <section id="subheader" class="d-flex align-items-center justify-content-between">
+            @if(false)
+            <form action="" class="search--form d-flex align-items-center">
+                <div class="input-group mb-3">
+                    <input type="text" class="form-control" placeholder="Cerca utente" aria-label="cerca utent" aria-describedby="button-addon2" wire:model="search">
+                    @if($showReset)
+                        <button class="btn--ui" type="button" id="button-addon2" onclick="reset()"><i class="ico--ui search"></i>Reset</button>
+                    @else
+                        <button class="btn--ui" type="button" id="button-addon2" wire:click="search()"><i class="ico--ui search"></i>Cerca</button>
+                    @endif
+                </div>
+            </form>
+            @endif
+        </section>
+
+        <div class="showFilter" style="display:none">
+            <hr size="1">
+            <div class="row g-3">
+                <div class="col-md-3">
+                    <div class="row">
+                        <div class="col-md-12" style="margin-bottom:10px;">
+                            <b>Età</b>
+                        </div>
+                        <div class="col-12">
+                            <div class="row mb-2">
+                                <div class="col-3"><label class="form-check-label ms-2" >Da</label></div>
+                                <div class="col-9"><input class="form-control " type="number" name="txtFromYear"></div>
+                            </div>
+                        </div>
+                        <div class="col-12">
+                            <div class="row">
+                                <div class="col-3"><label class="form-check-label ms-2" >A</label></div>
+                                <div class="col-9"><input class="form-control " type="number"  name="txtToYear"></div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-3">
+                    <div class="row">
+                        <div class="col-md-12" style="margin-bottom:10px;">
+                            <b>Tipologia di tesseramento</b>
+                        </div>
+                        <div class="col-12">
+                            <select name="filterCards" class="form-select filterCards">
+                                <option value="">Tutte
+                                @foreach(getCards() as $card)
+                                    <option value="{{$card->id}}">{{$card->name}}
+                                @endforeach
+                            </select>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-3">
+                    <div class="row">
+                        <div class="col-md-12" style="margin-bottom:10px;">
+                            <b>Stato tesseramento</b>
+                        </div>
+                        <div class="col-12">
+                            <select name="filterStatus" class="form-select filterStatus" multiple="multiple">
+                                <option value="2">Attivo
+                                <option value="1">Sospeso
+                                <option value="0">Non tesserato
+                            </select>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-3">
+                    <div class="row">
+                        <div class="col-md-12" style="margin-bottom:10px;">
+                            <b>Gruppo di interesse</b>
+                        </div>
+                        <div class="col-12">
+                            <select name="filterCategories" class="form-select filterCategories" multiple="multiple">
+                                <option value="">Tutte</option>
+                                @foreach($categories as $category)
+                                    <option value="{{$category["id"]}}">
+                                        {!! str_repeat('&bull; ', $category["indentation"] ?? 0) !!}{{$category["name"]}}
+                                    </option>
+                                @endforeach
+                            </select>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-3">
+                    <div class="row">
+                        <div class="col-md-12" style="margin-bottom:10px;">
+                            <b>Anno di nascita</b>
+                        </div>
+                        <div class="col-12">
+                            <div class="row mb-2">
+                                <div class="col-3"><label class="form-check-label ms-2" >Da</label></div>
+                                <div class="col-9"><input class="form-control " type="number" name="txtFromYearYear"></div>
+                            </div>
+                        </div>
+                        <div class="col-12">
+                            <div class="row">
+                                <div class="col-3"><label class="form-check-label ms-2" >A</label></div>
+                                <div class="col-9"><input class="form-control " type="number"  name="txtToYearYear"></div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-3">
+                    <div class="row">
+                        <div class="col-md-12" style="margin-bottom:10px;">
+                            <b>Scadenza certificato medico</b>
+                        </div>
+                        <div class="col-12">
+                            <select name="filterScadenza" class="form-select filterScadenza" multiple="multiple">
+                                <option value="1">Scaduti
+                                <option value="2">In scadenza
+                                <option value="3">Non consegnato
+                                <option value="4">Validi
+                            </select>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-3">
+                    <div class="row">
+                        <div class="col-md-12" style="margin-bottom:10px;">
+                            <b>Tipologia certificato medico</b>
+                        </div>
+                        <div class="col-12">
+                            <select name="filterCertificateType" class="form-select filterCertificateType" multiple="multiple">
+                                <option value="">Tutti
+                                <option value="N">Non agonistico
+                                <option value="A">Agonistico
+                            </select>
+                        </div>
+                    </div>
+                </div>
+
+            </div>
+            <div class="row g-3">
+                <div class="col-md-12" style="text-align:right">
+                    <button class="btn--ui lightGrey" onclick="reset()" onclick="destroyDataTable()">Reset</button>
+                    <button class="btn--ui" onclick="loadDataTable()">FILTRA</button>
+                </div>
+            </div>
+            <hr size="1">
+        </div>
+        <section id="anagrafiche--utenti">
+            <div class="compare--chart_wrapper d-none"></div>
+            <table class="table tablesaw tableHead tablesaw-stack table--lista_utenti tableHead" width="100%" id="tablesaw-350" width="100%">
+                <thead>
+                    <tr>
+                        <th scope="col" style="text-align: center">Cognome</th>
+                        <th scope="col">Nome</th>
+                        <th scope="col">Telefono</th>
+                        <th scope="col" style="text-align: center">Età</th>
+                        <th scope="col" style="text-align: center">Anno</th>
+                        <th scope="col">Tesseramento</th>
+                        <th scope="col">Certificato</th>
+                        <th scope="col">...</th>
+                    </tr>
+                </thead>
+                <tbody id="checkall-target"></tbody>
+            </table>
+        </section>
+    </div>
+</div>
+@push('scripts')
+    <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
+    <style>
+        table.tableHead thead
+        {
+            position: sticky;
+            z-index: 100;
+            top: 0;
+        }
+        .select2-container--default .select2-selection--single
+        {
+            background-color: #E9F0F5;
+            border: 0.0625rem solid #DFE5EB;
+            font-size: 0.75rem;
+        }
+        .select2-selection
+        {
+            height: 38px !important;
+        }
+        .select2-selection__rendered
+        {
+            padding-top:3px;
+        }
+        .select2
+        {
+            width:100% !important;
+        }
+        a.notSelected
+        {
+            display: flex;
+            margin-right:10px;
+            float:left;
+            height: 2.5rem !important;
+            width:50px;
+            align-items: center;
+            justify-content: center;
+            color: var(--color-blu);
+            background-color: #d3dce1 !important;
+            padding: 0 1.25rem;
+            font-size: 0.875rem;
+            font-family: greycliff-cf, sans-serif;
+            border-radius: 1.875rem !important;
+            -webkit-border-radius: 1.875rem !important;
+            -moz-border-radius: 1.875rem !important;
+            -webkit-transition: all 0.3s ease-in-out;
+            -moz-transition: all 0.3s ease-in-out;
+            -o-transition: all 0.3s ease-in-out;
+            transition: all 0.3s ease-in-out;
+            border: none;
+        }
+        a.selected
+        {
+            display: flex;
+            margin-right:10px;
+            float:left;
+            height: 2.5rem !important;
+            width:50px;
+            align-items: center;
+            justify-content: center;
+            color: #fff;
+            background-color: var(--color-blu) !important;
+            padding: 0 1.25rem;
+            font-size: 0.875rem;
+            font-family: greycliff-cf, sans-serif;
+            border-radius: 1.875rem !important;
+            -webkit-border-radius: 1.875rem !important;
+            -moz-border-radius: 1.875rem !important;
+            -webkit-transition: all 0.3s ease-in-out;
+            -moz-transition: all 0.3s ease-in-out;
+            -o-transition: all 0.3s ease-in-out;
+            transition: all 0.3s ease-in-out;
+            border: none;
+        }
+        div.day
+        {
+            margin-top:20px;
+            color: var(--color-blu);
+            background-color: #ffffff !important;
+            padding: 20px;
+            border-radius: 1.875rem !important;
+            -webkit-border-radius: 1.875rem !important;
+            -moz-border-radius: 1.875rem !important;
+            -webkit-transition: all 0.3s ease-in-out;
+            -moz-transition: all 0.3s ease-in-out;
+            -o-transition: all 0.3s ease-in-out;
+            transition: all 0.3s ease-in-out;
+            border: 2px solid #d3dce1;
+        }
+        .btn--ui.lightGrey, .btn--ui.extraLightGrey
+        {
+            -webkit-border-radius: 0.5rem !important;
+            background-color:#ffffff !important;
+            border: 1px solid grey;
+            color: black;
+            font-weight:normal;
+        }
+
+        .persistent-invalid
+        {
+            border-color: #dc3545 !important;
+            padding-right: calc(1.5em + 0.75rem) !important;
+            background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") !important;
+            background-repeat: no-repeat !important;
+            background-position: right calc(0.375em + 0.1875rem) center !important;
+            background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem) !important;
+        }
+    </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>
+@endpush
+
+@push('scripts')
+    <link href="/css/datatables.css" rel="stylesheet" />
+    <script src="/assets/js/datatables.js"></script>
+    <script src="https://cdn.datatables.net/buttons/3.0.2/js/buttons.dataTables.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/pdfmake.min.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/vfs_fonts.js"></script>
+@endpush
+
+@push('scripts')
+    <script>
+        function restoreData(id)
+        {
+            if (confirm('Sei sicuro? L\'utente verrà ripristinato.'))
+                @this.restore(id);
+        }
+
+        function archiveData(id)
+        {
+            if (confirm('Sei sicuro? Tutti i dati relativi a questo utente verranno archiviati e non saranno più visibili.'))
+                @this.archive(id);
+        }
+
+        var isFilter = false;
+        $(document).ready(function() {
+            $(document).on("click",".showHideFilter",function() {
+                if (isFilter)
+                {
+                    isFilter = false;
+                    $(".showFilter").hide();
+                }
+                else
+                {
+                    isFilter = true;
+                    $(".showFilter").show();
+                }
+            });
+        } );
+
+        $(document).ready(function() {
+            loadFilters();
+            loadDataTable();
+        } );
+
+        function destroyDataTable()
+        {
+            $('#tablesaw-350').DataTable().destroy();
+        }
+
+        $('.filterCards').select2({"language": {"noResults": function(){return "Nessun risultato";}}});
+        $('.filterStatus').select2({"language": {"noResults": function(){return "Nessun risultato";}}});
+        $('.filterScadenza').select2({"language": {"noResults": function(){return "Nessun risultato";}}});
+        $('.filterCertificateType').select2({"language": {"noResults": function(){return "Nessun risultato";}}});
+        $('.filterCategories').select2({"language": {"noResults": function(){return "Nessun risultato";}}});
+
+        function loadFilters() {
+            let show = false;
+            var filterStatus = localStorage.getItem("filterStatusMember");
+            if (filterStatus != null && filterStatus != "null" && filterStatus != "undefined" && filterStatus)
+            {
+                $('.filterStatus').val(filterStatus).trigger('change');
+                show = true;
+            }
+            var filterCards = localStorage.getItem("filterCardsMember");
+            if (filterCards != null && filterCards != "null" && filterCards != "undefined" && filterCards)
+            {
+                $('.filterCards').val(filterCards).trigger('change');
+                show = true;
+            }
+            var filterScadenza = localStorage.getItem("filterScadenzaMember");
+            if (filterScadenza != null && filterScadenza != "null" && filterScadenza != "undefined" && filterScadenza)
+            {
+                $('.filterScadenza').val(filterScadenza).trigger('change');
+                show = true;
+            }
+            var filterCertificateType = localStorage.getItem("filterCertificateTypeMember");
+            if (filterCertificateType != null && filterCertificateType != "null" && filterCertificateType != "undefined" && filterCertificateType)
+            {
+                $('.filterCertificateType').val(filterCertificateType).trigger('change');
+                show = true;
+            }
+            var filterCategories = localStorage.getItem("filterCategoriesMember");
+            if (filterCategories != null && filterCategories != "null" && filterCategories != "undefined" && filterCategories)
+            {
+                $('.filterCategories').val(filterCategories).trigger('change');
+                show = true;
+            }
+            var fromYear = localStorage.getItem("fromYearMember");
+            if (fromYear != null && fromYear != "null" && fromYear != "undefined" && fromYear)
+            {
+                $('input[name="txtFromYear"]').val(fromYear);
+                show = true;
+            }
+            var toYear = localStorage.getItem("toYearMember");
+            if (toYear != null && toYear != "null" && toYear != "undefined" && toYear)
+            {
+                $('input[name="txtToYear"]').val(toYear);
+                show = true;
+            }
+            var fromYearYear = localStorage.getItem("fromYearYearMember");
+            if (fromYearYear != null && fromYearYear != "null" && fromYearYear != "undefined" && fromYearYear)
+            {
+                $('input[name="txtFromYearYear"]').val(fromYearYear);
+                show = true;
+            }
+
+            var toYearYear = localStorage.getItem("toYearYearMember");
+            if (toYearYear != null && toYearYear != "null" && toYearYear != "undefined" && toYearYear)
+            {
+                $('input[name="txtToYearYear"]').val(toYearYear);
+                show = true;
+            }
+
+            if (show) {
+                $('.showHideFilter').trigger('click');
+            }
+        }
+
+        $(document).on("keypress", $('.filterCategories'), function (e) {
+
+            setTimeout(() => {
+                $(".select2-results__option").each(function(){
+                    var txt = $(this).html();
+                    var count = (txt.match(/-/g) || []).length;
+                    $(this).addClass('paddingLeftSelect' + count);
+                });
+            }, 100);
+        });
+
+        $('.filterCategories').on('select2:open', function (e) {
+            setTimeout(() => {
+                $(".select2-results__option").each(function(){
+                    var txt = $(this).html();
+                    var count = (txt.match(/-/g) || []).length;
+                    $(this).addClass('paddingLeftSelect' + count);
+                });
+            }, 100);
+        });
+
+        function reset()
+        {
+            $('.filterCards').val('').trigger('change');
+            $('.filterStatus').val('').trigger('change');
+            $('.filterScadenza').val('-1').trigger('change');
+            $('.filterCertificateType').val('-1').trigger('change');
+            $('.filterCategories').val('-1').trigger('change');
+
+
+            $('input[name="txtFromYear"]').val('');
+            $('input[name="txtToYear"]').val('');
+
+            $('input[name="txtFromYearYear"]').val('');
+            $('input[name="txtToYearYear"]').val('');
+            loadDataTable();
+        }
+
+        function loadDataTable() {
+            let date = new Date();
+            let date_export = `${date.getFullYear()}${date.getMonth()}${date.getDate()}_`;
+
+            const url = '/get_members?cards=' + $('.filterCards').val() + "&filterCategories=" + $('.filterCategories').val() + "&filterCertificateType=" + $('.filterCertificateType').val() + "&filterScadenza=" + $('.filterScadenza').val() + "&filterStatus=" + $('.filterStatus').val() + "&fromYear=" + $('input[name="txtFromYear"]').val() + "&toYear=" + $('input[name="txtToYear"]').val() + "&fromYearYear=" + $('input[name="txtFromYearYear"]').val() + "&toYearYear=" + $('input[name="txtToYearYear"]').val() + "&archived=1";
+
+            localStorage.setItem("filterCardsMember", $('.filterCards').val());
+            localStorage.setItem("filterCategoriesMember", $('.filterCategories').val());
+            localStorage.setItem("filterCertificateTypeMember", $('.filterCertificateType').val());
+            localStorage.setItem("filterScadenzaMember", $('.filterScadenza').val());
+            localStorage.setItem("filterStatusMember", $('.filterStatus').val());
+            localStorage.setItem("fromYearMember", $('input[name="txtFromYear"]').val());
+            localStorage.setItem("toYearMember", $('input[name="txtToYear"]').val());
+            localStorage.setItem("fromYearYearMember", $('input[name="txtFromYearYear"]').val());
+            localStorage.setItem("toYearYearMember", $('input[name="txtToYearYear"]').val());
+
+            if ($.fn.dataTable.isDataTable('#tablesaw-350')) {
+                $('#tablesaw-350').DataTable().destroy();
+            }
+
+            const pageLength = 10;
+
+            const dataTable = $('#tablesaw-350').DataTable({
+                serverSide: true,
+                processing: true,
+                ajax: url,
+                columns: [
+                    {
+                        data: "last_name",
+                        render: function (data){
+                            const d = data.split("|");
+                            var ret = d[0];
+                            return ret;
+                        }
+                    },
+                    {
+                        data: "first_name",
+                        render: function (data){
+                            const d = data.split("|");
+                            var ret = d[0];
+                            return ret;
+                        }
+                    },
+                    { data: "phone"},
+                    { data: "age", "type": "num", className:"dt-type-numeric"},
+                    { data: "year", className:"dt-type-numeric"},
+                    {
+                        data: "status",
+                        render: function (data){
+                            const d = data.split("|");
+                            var ret = '<span class="tablesaw-cell-content"><span class="badge tessera-badge ' + d[0] + '">' + d[1] + '</span></span>';
+                            return ret;
+                        }
+                    },
+                    {
+                        data: "certificate",
+                        render: function (data){
+                            var ret = '';
+                            if (data != "") {
+                                const d = data.split("|");
+                                ret += '<span class="tablesaw-cell-content d-flex align-items-center">';
+                                if (d[0] == "0") {
+                                    ret += '<i class="ico--ui check suspended me-2"></i>';
+                                    ret += 'Scaduto : ';
+                                }
+                                if (d[0] == "1") {
+                                    ret += '<i class="ico--ui check due me-2"></i>';
+                                    ret += 'In scadenza : ';
+                                }
+                                if (d[0] == "2") {
+                                    ret += '<i class="ico--ui check active me-2"></i>';
+                                    ret += 'Scadenza : ';
+                                }
+                                ret += d[1];
+                                ret += '</span>';
+                            }
+                            if(data == ""){
+                                ret += '<span class="tablesaw-cell-content d-flex align-items-center">';
+                                ret += '<i class="ico--ui check absent me-2"></i>';
+                                ret += 'Non consegnato';
+                                ret += '</span>';
+                            }
+                            return ret;
+                        }
+                    },
+                    {
+                        data: "action",
+                        render: function (data) {
+                            var ret = '<button type="button" class="btn" onclick="restoreData(' + data + ')" data-bs-toggle="popover"  data-bs-trigger="hover focus" data-bs-placement="bottom" data-bs-content="Ripristina"><i class="fa-solid fa-rotate-left"></i></button>&nbsp;';
+                            ret += '<button type="button" class="btn" onclick="archiveData(' + data + ')" data-bs-toggle="popover" data-bs-trigger="hover focus" data-bs-placement="bottom" data-bs-content="Elimina"><i class="fa-regular fa-trash-can"></i></button>';
+                            return ret;
+                        }
+                    },
+                ],
+                fixedHeader: false,
+                thead: {
+                    'th': {'background-color': 'blue'}
+                },
+                layout: {
+                    topStart : null,
+                    topEnd : null,
+                    top1A: {
+                        // buttons: [
+                        //     {
+                        //         extend: 'collection',
+                        //         text: 'ESPORTA',
+                                buttons: [
+                                    {
+                                        extend: 'excelHtml5',
+                                        text: '<i class="fa-solid fa-file-excel"></i>',
+                                        action: newexportaction,
+                                        title: date_export + 'Utenti archiviati',
+                                        exportOptions: {
+                                            columns: ":not(':last')",
+                                            page: 'all'
+                                        }
+                                    },
+                                    {
+                                        extend: 'pdfHtml5',
+                                        text: '<i class="fa-solid fa-file-pdf"></i>',
+                                        action: newexportaction,
+                                        title: date_export + 'Utenti archiviati',
+                                        exportOptions: {
+                                            columns: ":not(':last')"
+                                        },
+                                        customize: function(doc) {
+                                            doc.styles.tableHeader.alignment = 'left';
+                                        }
+                                    },
+                                    {
+                                        extend: 'print',
+                                        action: newexportaction,
+                                        text: '<i class="fa-solid fa-print"></i>',
+                                        title: date_export + 'Utenti archiviati',
+                                        exportOptions: {
+                                            columns: ":not(':last')"
+                                        }
+                                    }
+                                ],
+                        //         dropup: true
+                        //     }
+                        // ]
+                    },
+                    top1B : {
+                        pageLength: {
+                            menu: [[10, 25, 50, 100, 100000], [10, 25, 50, 100, "Tutti"]]
+                        }
+                    },
+                    top1C :'search',
+                },
+                pagingType: 'numbers',
+                "language": {
+                    "url": "/assets/js/Italian.json"
+                },
+                "fnInitComplete": function (oSettings, json) {
+                    var html = '&nbsp;<a style="cursor:pointer" class="showHideFilter btn--ui"><i class="fa-solid fa-sliders"></i></a>';
+                    html += '&nbsp;<a style="cursor:pointer" class="addData btn--ui"><i class="fa-solid fa-plus"></i></a>';
+                    $(".dt-search").append(html);
+                    
+                    loadFilters();
+                }
+            });
+
+            $('#tablesaw-350 thead tr th').addClass('col');
+            $('#tablesaw-350 thead tr th').css("background-color", "#f6f8fa");
+            $('#tablesaw-350').on('draw.dt', function() {
+                $('[data-bs-toggle="popover"]').popover()
+            });
+        }
+
+        Livewire.on('reload', (x) =>
+        {
+            location.reload();
+        });
+    </script>
+@endpush

+ 30 - 5
resources/views/livewire/profile.blade.php

@@ -48,18 +48,43 @@
                             <div class="col-md-6 mb-3">
                                 <label class="form-label">Email</label>
                                 <input type="email"
-                                    class="form-control @error('email') is-invalid @enderror"
-                                    wire:model="email" {{ $editMode ? '' : 'disabled' }}>
+                                class="form-control @error('email') is-invalid @enderror"
+                                wire:model="email" {{ $editMode ? '' : 'disabled' }}>
                                 @error('email') <span class="text-danger">{{ $message }}</span> @enderror
                             </div>
+                            <div class="col-md-6"></div>
                             <div class="col-md-6 mb-3">
                                 <label class="form-label">Password</label>
-                                <input type="password"
-                                    class="form-control @error('password') is-invalid @enderror"
-                                    wire:model="password" {{ $editMode ? '' : 'disabled' }}>
+                                @if ($editMode)
+                                <div class="password-wrapper">
+                                @endif
+                                    <input type="password" class="form-control @error('password') is-invalid @enderror" wire:model="password" {{ $editMode ? '' : 'disabled' }}>
+                                @if ($editMode)
+                                    <div class="password-eye" onclick="togglePassword(this)">
+                                        <i class="fas fa-eye password-hidden"></i>
+                                        <i class="fas fa-eye-slash password-shown"></i>
+                                    </div>
+                                </div>
+                                @endif
                                 @error('password') <span class="text-danger">{{ $message }}</span> @enderror
                                 <small class="form-text text-muted">Lasciare vuoto per non modificare</small>
                             </div>
+                            <div class="col-md-6 mb-3">
+                                <label class="form-label">Conferma password</label>
+                                @if ($editMode)
+                                <div class="password-wrapper">
+                                @endif
+                                    <input type="password" class="form-control @error('password_confirmation') is-invalid @enderror" wire:model="password_confirmation" {{ $editMode ? '' : 'disabled' }}>
+                                @if ($editMode)
+                                    <div class="password-eye" onclick="togglePassword(this)">
+                                        <i class="fas fa-eye password-hidden"></i>
+                                        <i class="fas fa-eye-slash password-shown"></i>
+                                    </div>
+                                </div>
+                                @endif
+                                @error('password_confirmation') <span class="text-danger">{{ $message }}</span> @enderror
+                                <small class="form-text text-muted">Lasciare vuoto per non modificare</small>
+                            </div>
                         </div>
                     </div>
 

+ 29 - 19
resources/views/livewire/rate.blade.php

@@ -8,7 +8,11 @@
         </div>
     </header>
 
+    @if ($rate_from == 'course')
     <a class="btn--ui lightGrey" href="/course_member?id={{$mc->course_id}}"><i class="fa-solid fa-arrow-left"></i></a><br>
+    @elseif($rate_from == 'subscription')
+    <a class="btn--ui lightGrey" href="/subscription_member/{{$ms->subscription_id}}"><i class="fa-solid fa-arrow-left"></i></a><br>
+    @endif
 
     <div class="showFilter" style="display:none" wire:ignore.self>
         <hr size="1">
@@ -79,16 +83,12 @@
 
         <div class="row">
             <div class="col-6">
-                <h3 class="mt-4">{{$detail}}</h3><br>
+                <h3>{!!$detail!!}</h3>
             </div>
-            <div class="col-2 right" style="margin-top:20px;text-align:right">
-                <button id="btPay" style="display:none;" class="btn--ui" >PAGA</button>
-            </div>
-            <div class="col-2 right" style="margin-top:20px;text-align:right">
-                <button id="btSuspend" style="display:none;" class="btn--ui" >SOSPENDI/ATTIVA SELEZIONATI</button>
-            </div>
-            <div class="col-2 right" style="margin-top:20px;text-align:right">
-                <button id="btRemove" style="display:none;" class="btn--ui" >ELIMINA SELEZIONATI</button>
+            <div class="col right table-actions" style="text-align: right">
+                <button id="btPay" style="display:none;" class="btn" data-bs-toggle="popover" data-bs-trigger="hover focus" data-bs-placement="bottom" data-bs-content="Crea tutte le entrate"><i class="fa-regular fa-file-text"></i></button>
+                <button id="btSuspend" style="display:none;" class="btn" data-bs-toggle="popover" data-bs-trigger="hover focus" data-bs-placement="bottom" data-bs-content="Sospendi/Riattiva tutto"><i class="fa-solid fa-ban"></i></button>
+                <button id="btRemove" style="display:none;" class="btn" data-bs-toggle="popover" data-bs-trigger="hover focus" data-bs-placement="bottom" data-bs-content="Elimina tutto"><i class="fa-regular fa-trash-can"></i></button>
             </div>
         </div>
         
@@ -140,21 +140,29 @@
                             @if($record->status == 0)
 
                                 <input type="hidden" id="memberId_{{$record->id}}" value="{{$record->member_id}}">
-                                <input type="hidden" id="causalId_{{$record->id}}" value="{{!$record->is_subscription ? $record->member_course->course->causal_id : ''}}">
-                                <input type="hidden" id="subCausalId_{{$record->id}}" value="{{$record->is_subscription ? $record->member_course->course->sub_causal_id : ''}}">
                                 <input type="hidden" id="createSubscription_{{$record->id}}" value="{{$record->is_subscription ? '1' : ''}}">
                                 <input type="hidden" id="months_{{$record->id}}" value="{{implode("|", json_decode($record->months))}}">
                                 <input type="hidden" id="price_{{$record->id}}" value="{{!$record->is_subscription ? $record->price : ''}}">
                                 <input type="hidden" id="subscription_price_{{$record->id}}" value="{{$record->is_subscription ? $record->price : ''}}">
-                                <input type="hidden" id="courseId_{{$record->id}}" value="{{$record->member_course_id}}">
                                 <input type="hidden" id="rateId_{{$record->id}}" value="{{$record->id}}">
 
-                                <a class="btn  u" href="/in?new=1&memberId={{$record->member_id}}&causalId={{!$record->is_subscription ? $record->member_course->course->causal_id : ''}}&subCausalId={{$record->is_subscription ? $record->member_course->course->sub_causal_id : ''}}&createSubscription={{$record->is_subscription ? '1' : ''}}&months={{implode("|", json_decode($record->months))}}&price={{!$record->is_subscription ? $record->price : ''}}&subscription_price={{$record->is_subscription ? $record->price : ''}}&courseId={{$record->member_course_id}}&rateId={{$record->id}}" data-bs-toggle="popover"
-                                data-bs-trigger="hover focus" data-bs-placement="bottom"
-                                data-bs-content="Crea entrata"><i class="fa-regular fa-file-lines"></i></a>
-                                <a class="btn u" onclick="confirmDelete({{$record->id}})" data-bs-toggle="popover"
-                                    data-bs-trigger="hover focus" data-bs-placement="bottom"
-                                    data-bs-content="Elimina"><i class="fa-regular fa-trash-can"></i></button>
+                                @if ($rate_from == 'course')
+                                <input type="hidden" id="courseId_{{$record->id}}" value="{{$record->member_course_id}}">
+                                <input type="hidden" id="subscriptionId_{{$record->id}}" value="0">
+                                <input type="hidden" id="causalId_{{$record->id}}" value="{{!$record->is_subscription ? $record->member_course->course->causal_id : ''}}">
+                                <input type="hidden" id="subCausalId_{{$record->id}}" value="{{$record->is_subscription ? $record->member_course->course->sub_causal_id : ''}}">
+
+                                <a class="btn  u" href="/in?new=1&memberId={{$record->member_id}}&causalId={{!$record->is_subscription ? $record->member_course->course->causal_id : ''}}&subCausalId={{$record->is_subscription ? $record->member_course->course->sub_causal_id : ''}}&createSubscription={{$record->is_subscription ? '1' : ''}}&months={{implode("|", json_decode($record->months))}}&price={{!$record->is_subscription ? $record->price : ''}}&subscription_price={{$record->is_subscription ? $record->price : ''}}&courseId={{$record->member_course_id}}&subscriptionId=0&rateId={{$record->id}}&backToRates" data-bs-toggle="popover" data-bs-trigger="hover focus" data-bs-placement="bottom" data-bs-content="Crea entrata"><i class="fa-regular fa-file-lines"></i></a>
+                                @elseif($rate_from == 'subscription')
+                                <input type="hidden" id="courseId_{{$record->id}}" value="0">
+                                <input type="hidden" id="subscriptionId_{{$record->id}}" value="{{$record->member_subscription_id}}">
+                                <input type="hidden" id="causalId_{{$record->id}}" value="{{!$record->is_subscription ? $record->member_subscription->subscription->causal_id : ''}}">
+                                <input type="hidden" id="subCausalId_{{$record->id}}" value="{{$record->is_subscription ? $record->member_subscription->subscription->sub_causal_id : ''}}">
+
+                                <a class="btn  u" href="/in?new=1&memberId={{$record->member_id}}&causalId={{!$record->is_subscription ? $record->member_subscription->subscription->causal_id : ''}}&subCausalId={{$record->is_subscription ? $record->member_subscription->subscription->sub_causal_id : ''}}&createSubscription={{$record->is_subscription ? '1' : ''}}&months={{implode("|", json_decode($record->months))}}&price={{!$record->is_subscription ? $record->price : ''}}&subscription_price={{$record->is_subscription ? $record->price : ''}}&courseId=0&subscriptionId={{$record->member_subscription_id}}&rateId={{$record->id}}&backToRates" data-bs-toggle="popover" data-bs-trigger="hover focus" data-bs-placement="bottom" data-bs-content="Crea entrata"><i class="fa-regular fa-file-lines"></i></a>
+                                @endif
+
+                                <a class="btn u" onclick="confirmDelete({{$record->id}})" data-bs-toggle="popover" data-bs-trigger="hover focus" data-bs-placement="bottom" data-bs-content="Elimina"><i class="fa-regular fa-trash-can"></i></button>
                             @endif
                         </td>
                     </tr>
@@ -565,6 +573,7 @@
                     var price = '0';
                     var subscription_price = '';
                     var courseId = 0;
+                    var subscriptionId = 0;
                     var rateId = 0;
 
                     $('.chkIds').each(function () {
@@ -588,6 +597,7 @@
                                 price = parseFloat(price) + parseFloat($("#price_" + id).val());                            
                             }
                             courseId = $("#courseId_" + id).val();;
+                            subscriptionId = $("#subscriptionId_" + id).val();;
                             if (rateId != '') 
                                 rateId += ",";
                             rateId += $("#rateId_" + id).val();;
@@ -595,7 +605,7 @@
                             //ids.push($(this).val());
                     });
 
-                    document.location.href = "/in?new=1&memberId=" + memberId + "&causalId=" + causalId + "&subCausalId=" + subCausalId + "&createSubscription=" + createSubscription + "&months=" + months + "&price=" + price + "&subscription_price=" + subscription_price + "&courseId=" + courseId + "&rateId=" + rateId;
+                    document.location.href = "/in?new=1&memberId=" + memberId + "&causalId=" + causalId + "&subCausalId=" + subCausalId + "&createSubscription=" + createSubscription + "&months=" + months + "&price=" + price + "&subscription_price=" + subscription_price + "&courseId=" + courseId + "&subscriptionId=" + subscriptionId + "&rateId=" + rateId;
 
                     /*if (confirm('Sei sicuro?'))
                     {

+ 165 - 60
resources/views/livewire/records_in.blade.php

@@ -57,60 +57,92 @@
                         </div>
                     </div>
                 </div>
-                <div class="col-md-2">
+                <div class="col-md-10">
                     <div class="row">
-                        <div class="col-md-12" style="margin-bottom:10px;">
-                            <b>Tipologia</b>
-                        </div>
-                        <div class="col-12">
-                            <select name="filterCommercial" class="form-select filterCommercial">
-                                <option value="">Entrambe
-                                <option value="1">Commerciale
-                                <option value="2">Non commerciale
-                            </select>
-                        </div>
-                    </div>
-                </div>
-                <div class="col-md-2">
-                    <div class="row">
-                        <div class="col-md-12" style="margin-bottom:10px;">
-                            <b>Utente</b>
+                        <div class="col-md-3 mt-2">
+                            <div class="row">
+                                <div class="col-md-12" style="margin-bottom:10px;">
+                                    <b>Tipologia</b>
+                                </div>
+                                <div class="col-12">
+                                    <select name="filterCommercial" class="form-select filterCommercial">
+                                        <option value="">Entrambe
+                                        <option value="1">Commerciale
+                                        <option value="2">Non commerciale
+                                    </select>
+                                </div>
+                            </div>
                         </div>
-                        <div class="col-12">
-                            <select name="search_member_id" class="form-select filterMember">
-                                <option value="">--Seleziona--
-                                @foreach($members as $member)
-                                    <option value="{{$member->id}}">{{$member->last_name}} {{$member->first_name}}
-                                @endforeach
-                            </select>
+                        <div class="col-md-3 mt-2">
+                            <div class="row">
+                                <div class="col-md-12" style="margin-bottom:10px;">
+                                    <b>Utente</b>
+                                </div>
+                                <div class="col-12">
+                                    <select name="search_member_id" class="form-select filterMember">
+                                        <option value="">--Seleziona--
+                                        @foreach($members as $member)
+                                            <option value="{{$member->id}}">{{$member->last_name}} {{$member->first_name}}
+                                        @endforeach
+                                    </select>
+                                </div>
+                            </div>
                         </div>
-                    </div>
-                </div>
-                <div class="col-md-2">
-                    <div class="row">
-                        <div class="col-md-12" style="margin-bottom:10px;">
-                            <b>Metodi di pagamento</b>
+                        <div class="col-md-3 mt-2">
+                            <div class="row">
+                                <div class="col-md-12" style="margin-bottom:10px;">
+                                    <b>Metodi di pagamento</b>
+                                </div>
+                                <div class="col-12">
+                                    <select name="search_payment_method_id" class="form-select filterPaymentMethod" multiple="multiple">
+                                        @foreach($payments as $payment)
+                                            <option value="{{$payment->id}}">{{$payment->name}}
+                                        @endforeach
+                                    </select>
+                                </div>
+                            </div>
                         </div>
-                        <div class="col-12">
-                            <select name="search_payment_method_id" class="form-select filterPaymentMethod" multiple="multiple">
-                                @foreach($payments as $payment)
-                                    <option value="{{$payment->id}}">{{$payment->name}}
-                                @endforeach
-                            </select>
+                        <div class="col-md-3 mt-2">
+                            <div class="row">
+                                <div class="col-md-12" style="margin-bottom:10px;">
+                                    <b>Causali</b>
+                                </div>
+                                <div class="col-12">
+                                    <select name="search_causal_id" class="form-select filterCausals" multiple="multiple">
+                                        @foreach($causals as $causal)
+                                            <option value="{{$causal["id"]}}">{!!$causal["name"]!!}
+                                        @endforeach
+                                    </select>
+                                </div>
+                            </div>
                         </div>
-                    </div>
-                </div>
-                <div class="col-md-2">
-                    <div class="row">
-                        <div class="col-md-12" style="margin-bottom:10px;">
-                            <b>Causali</b>
+                        <div class="col-md-3 mt-2">
+                            <div class="row">
+                                <div class="col-md-12" style="margin-bottom:10px;">
+                                    <b>Corsi</b>
+                                </div>
+                                <div class="col-12">
+                                    <select name="search_course_id" class="form-select filterCourses" multiple="multiple">
+                                        @foreach($courses as $course)
+                                            <option value="{{$course["id"]}}">{!!$course["name"]!!}
+                                        @endforeach
+                                    </select>
+                                </div>
+                            </div>
                         </div>
-                        <div class="col-12">
-                            <select name="search_causal_id" class="form-select filterCausals" multiple="multiple">
-                                @foreach($causals as $causal)
-                                    <option value="{{$causal["id"]}}">{!!$causal["name"]!!}
-                                @endforeach
-                            </select>
+                        <div class="col-md-3 mt-2">
+                            <div class="row">
+                                <div class="col-md-12" style="margin-bottom:10px;">
+                                    <b>Abbonamenti</b>
+                                </div>
+                                <div class="col-12">
+                                    <select name="search_subscription_id" class="form-select filterSubscriptions" multiple="multiple">
+                                        @foreach($subscriptions as $subscription)
+                                            <option value="{{$subscription["id"]}}">{!!$subscription["name"]!!}
+                                        @endforeach
+                                    </select>
+                                </div>
+                            </div>
                         </div>
                     </div>
                 </div>
@@ -137,6 +169,7 @@
                         <th scope="col">Nome</th>
                         <th scope="col">Commerciale</th>
                         <th scope="col">Causale</th>
+                        <th scope="col">Corso/Abbonamento</th>
                         <th scope="col">Pagamento</th>
 
                         <th scope="col">Stato</th>
@@ -362,22 +395,41 @@
                             </div>
 
                             @foreach($rows as $idx => $row)
-
                                 <div class="row gx-2 mt-3" >
-                                    <span class="title-form d-block w-100">Causale</span>
-                                    <div class="col-md-12">
-
-                                        @if($this->dataId > 0 && !$isDuplicate)
+                                    <div class="@if ($fromCourse || $row['course_id'] || $row['subscription_id'])col-md-6 @endif col-xs-12">
+                                        <span class="title-form d-block w-100">Causale</span>
+                                        @if ($fromCourse || $row['course_id'] || $row['subscription_id'])
                                             {{$this->getCausal($rows[$idx]['causal_id'])}}<br>
                                         @else
-                                            <livewire:causals :type="$typeIN" :idx="$idx" :show_hidden=0 :causal_id="$rows[$idx]['causal_id']" :wire:key="$idx"/>
-                                        @endif
+                                            @if($this->dataId > 0 && !$isDuplicate)
+                                                {{$this->getCausal($rows[$idx]['causal_id'])}}<br>
+                                            @else
+                                                <livewire:causals :type="$typeIN" :idx="$idx" :show_hidden=0 :show_course_causals=0 :causal_id="$rows[$idx]['causal_id']" :wire:key="$idx"/>
+                                            @endif
 
-                                        @error('rows.'. $idx . '.causal_id')
-                                            <span style="argin-top: 0.25rem; font-size: 0.875em; color: var(--bs-form-invalid-color);">{{ $message }}</span>
-                                        @enderror
+                                            @error('rows.'. $idx . '.causal_id')
+                                                <span style="argin-top: 0.25rem; font-size: 0.875em; color: var(--bs-form-invalid-color);">{{ $message }}</span>
+                                            @enderror
+                                        @endif
 
                                     </div>
+                                    @if ($fromCourse)
+                                        @if (isset($row['course']) && $row['course'] != null)
+                                            <div class="col-md-6 col-xs-12">
+                                                <span class="title-form d-block w-100">Corso</span>
+                                                @if (isset($row['course']))
+                                                    {{$row['course']->getDetailsName()}}
+                                                @endif
+                                            </div>
+                                        @elseif (isset($row['subscription']) && $row['subscription'] != null)
+                                            <div class="col-md-6 col-xs-12">
+                                                <span class="title-form d-block w-100">Abbonamento</span>
+                                                @if (isset($row['subscription']))
+                                                    {{$row['subscription']->name}}
+                                                @endif
+                                            </div>
+                                        @endif
+                                    @endif
                                 </div>
 
                                 <div class="row gx-2 mt-3">
@@ -578,7 +630,7 @@
 
                             @endforeach
 
-                            @if($this->dataId == 0)
+                            @if($this->dataId == 0 && !$fromCourse)
                             <br>
                             <button type="button" class="btn--ui primary" wire:click.prevent="addRow()">
                                 Aggiungi causale
@@ -1048,6 +1100,16 @@
                 //var data = $('.filterCausals').select2("val");
                 //@this.set('filterCausals', data);
             });
+            $('.filterCourses').select2({"language": {"noResults": function(){return "Nessun risultato";}}});
+            $('.filterCourses').on('change', function (e) {
+                //var data = $('.filterCourses').select2("val");
+                //@this.set('filterCourses', data);
+            });
+            $('.filterSubscriptions').select2({"language": {"noResults": function(){return "Nessun risultato";}}});
+            $('.filterSubscriptions').on('change', function (e) {
+                //var data = $('.filterSubscriptions').select2("val");
+                //@this.set('filterSubscriptions', data);
+            });
 
 
 
@@ -1067,6 +1129,16 @@
             //var data = $('.filterCausals').select2("val");
             //@this.set('filterCausals', data);
         });
+        $('.filterCourses').select2({"language": {"noResults": function(){return "Nessun risultato";}}});
+        $('.filterCourses').on('change', function (e) {
+            //var data = $('.filterCourses').select2("val");
+            //@this.set('filterCourses', data);
+        });
+        $('.filterSubscriptions').select2({"language": {"noResults": function(){return "Nessun risultato";}}});
+        $('.filterSubscriptions').on('change', function (e) {
+            //var data = $('.filterSubscriptions').select2("val");
+            //@this.set('filterSubscriptions', data);
+        });
 
         window.livewire.on('saved', () => {
             $('#userModal').modal('hide');
@@ -1140,6 +1212,18 @@
                 $('.filterCausals').val(filterCausals);
                 $('.filterCausals').trigger('change');
             }
+            var filterCourses = localStorage.getItem("filterCourses");
+            if (filterCourses)
+            {
+                $('.filterCourses').val(filterCourses);
+                $('.filterCourses').trigger('change');
+            }
+            var filterSubscriptions = localStorage.getItem("filterSubscriptions");
+            if (filterSubscriptions)
+            {
+                $('.filterSubscriptions').val(filterSubscriptions);
+                $('.filterSubscriptions').trigger('change');
+            }
             var filterFrom = localStorage.getItem("filterFrom");
             if (filterFrom)
             {
@@ -1181,6 +1265,18 @@
                 $('.filterCausals').val(filterCausals);
                 $('.filterCausals').trigger('change');
             }
+            var filterCourses = localStorage.getItem("filterCoursesRecordIn");
+            if (filterCourses && filterCourses != "null" && filterCourses != "undefined")
+            {
+                $('.filterCourses').val(filterCourses);
+                $('.filterCourses').trigger('change');
+            }
+            var filterSubscriptions = localStorage.getItem("filterSubscriptionsRecordIn");
+            if (filterSubscriptions && filterSubscriptions != "null" && filterSubscriptions != "undefined")
+            {
+                $('.filterSubscriptions').val(filterSubscriptions);
+                $('.filterSubscriptions').trigger('change');
+            }
             var filterFrom = localStorage.getItem("filterFromRecordIn");
             if (filterFrom && filterFrom != "null" && filterFrom != "undefined")
             {
@@ -1262,6 +1358,10 @@
             $('.filterPaymentMethod').trigger('change');
             $('.filterCausals').val('');
             $('.filterCausals').trigger('change');
+            $('.filterCourses').val('');
+            $('.filterCourses').trigger('change');
+            $('.filterSubscriptions').val('');
+            $('.filterSubscriptions').trigger('change');
             $('.filterFrom').val('');
             $('.filterTo').val('');
             //localStorage.clear();
@@ -1282,6 +1382,8 @@
             var filterMember = $('.filterMember').val();
             var filterPaymentMethod = $('.filterPaymentMethod').val();
             var filterCausals = $('.filterCausals').val();
+            var filterCourses = $('.filterCourses').val();
+            var filterSubscriptions = $('.filterSubscriptions').val();
             var filterFrom = $('.filterFrom').val();
             var filterTo = $('.filterTo').val();
 
@@ -1290,6 +1392,8 @@
             localStorage.setItem("filterMemberRecordIn", filterMember);
             localStorage.setItem("filterPaymentMethodRecordIn", filterPaymentMethod);
             localStorage.setItem("filterCausalsRecordIn", filterCausals);
+            localStorage.setItem("filterCoursesRecordIn", filterCourses);
+            localStorage.setItem("filterSubscriptionsRecordIn", filterSubscriptions);
             localStorage.setItem("filterFromRecordIn", filterFrom);
             localStorage.setItem("filterToRecordIn", filterTo);
 
@@ -1307,7 +1411,7 @@
                 serverSide: true,
                 processing: true,
                 ajax: {
-                    url : '/get_record_in?filterCommercial=' + filterCommercial + "&filterMember=" + filterMember + "&filterPaymentMethod=" + filterPaymentMethod + "&filterCausals=" + filterCausals + "&filterFrom=" + filterFrom + "&filterTo=" + filterTo,
+                    url : '/get_record_in?filterCommercial=' + filterCommercial + "&filterMember=" + filterMember + "&filterPaymentMethod=" + filterPaymentMethod + "&filterCausals=" + filterCausals + "&filterCourses=" + filterCourses + "&filterSubscriptions=" + filterSubscriptions + "&filterFrom=" + filterFrom + "&filterTo=" + filterTo,
                     dataSrc: function (json){
                         if(json.totals){
                             $(".totalDiv").html('Totale&nbsp;:&nbsp;<b>' + json.totals + '</b>');
@@ -1344,6 +1448,7 @@
                     { data: 'first_name' },
                     { data: 'commercial' },
                     { data: 'causals', "orderable": false, },
+                    { data: 'courses', "orderable": false, },
                     { data: 'payment', "orderable": false, },
                     //{ data: 'payment_date', "orderable": false, },
                     { data: 'status', "orderable": false, },

+ 24 - 3
resources/views/livewire/records_in_out.blade.php

@@ -15,14 +15,14 @@
 
         <form action="" class=" d-flex align-items-center ">
 
-            <select class="form-select form-select-lg me-1 " id="month" style="width:150px !important;">
-                <option value="x" {{$month == "x" ? 'selected' : ""}}>Anno completo</option>
+            <select class="form-select form-select-lg me-1 " id="month" style="width:150px !important;" onchange="updateYearSelect(this)">
+                <option value="x" {{$month == "x" ? 'selected' : ""}}>Stagione</option>
                 @foreach(getMonthList() as $mid => $mname)
                     <option value="{{$mid}}" {{$month == $mid ? 'selected' : ""}}>{{$mname}}</option>
                 @endforeach
             </select>
 
-            <select class="form-select " style="width:100px !important;" id="year">
+            <select class="form-select " {{-- style="width:100px !important;" --}} id="year">
                 @for($y=date("Y") + 5; $y>date("Y") - 2; $y--)
                     <option value="{{$y}}" {{$y == date("Y") ? 'selected' : ''}}>{{$y}}</option>
                 @endfor
@@ -503,6 +503,27 @@
             @this.exportYear(y);
         });
 
+        function updateYearSelect(month) {
+            let year = document.getElementById("year");
+
+            if (year) {
+                let options = Array.from(year.options);
+
+                if (month.value=='x') {
+                    options.forEach(option => {
+                        let y = Number(option.innerText);
+                        if (!isNaN(y)) {
+                            option.innerText = `${y}/${y+1}`;
+                        }
+                    });
+                } else {
+                    options.forEach(option => {
+                        let y = option.innerText;
+                        option.innerText = y.split('/')[0];
+                    });
+                }
+            }
+        }
 
     </script>
 

+ 22 - 21
resources/views/livewire/reports.blade.php

@@ -63,7 +63,7 @@
 
         <div class="chart-row">
             <div class="dashboard-card chart-card">
-                <div class="dashboard-card-title">Causali Performanti - <span id="causals-season-title">{{ $seasonFilter }}</span></div>
+                <div class="dashboard-card-title">Redditività per Causale - <span id="causals-season-title">{{ $seasonFilter }}</span></div>
                 <div class="chart-body">
                     <div style="display: grid; grid-template-columns: 1fr 700px; gap: 1rem; align-items: start;">
                         <div class="causals-table-container" id="causals-table">
@@ -95,7 +95,7 @@
 
     <div class="chart-row">
         <div class="dashboard-card chart-card modern-course-card">
-            <div class="dashboard-card-title">Analisi Corsi</div>
+            <div class="dashboard-card-title">Analisi incassi corsi</div>
             <div class="chart-body">
                 <div class="course-controls">
                     <div class="control-group">
@@ -135,8 +135,8 @@
     </div>
 </div>
 
-<script>
-    window.ReportsChartManager = window.ReportsChartManager || {
+    <script>
+        window.ReportsChartManager = window.ReportsChartManager || {
             charts: {},
             currentSeason: null,
 
@@ -288,7 +288,7 @@
                                 callbacks: {
                                     label: function (context) {
                                         return context.dataset.label + ': €' +
-                                            new Intl.NumberFormat('it-IT').format(context.parsed.y);
+                                            new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(context.parsed.y);
                                     }
                                 }
                             },
@@ -303,7 +303,7 @@
                                 grid: { color: 'rgba(0, 0, 0, 0.05)' },
                                 ticks: {
                                     callback: function (value) {
-                                        return '€' + new Intl.NumberFormat('it-IT').format(value);
+                                        return '€' + new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(value);
                                     }
                                 }
                             }
@@ -744,7 +744,7 @@
                                 },
                                 ticks: {
                                     callback: function (value) {
-                                        return '€' + new Intl.NumberFormat('it-IT').format(value);
+                                        return '€' + new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(value);
                                     }
                                 }
                             }
@@ -769,7 +769,7 @@
                                 callbacks: {
                                     label: function (context) {
                                         return context.dataset.label + ': €' +
-                                            new Intl.NumberFormat('it-IT').format(context.parsed.y);
+                                            new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(context.parsed.y);
                                     }
                                 }
                             }
@@ -886,11 +886,11 @@
                                         bottomLeft: 0,
                                         bottomRight: 0,
                                     },
-                                    borderSkipped: true,
+                                    // borderSkipped: true,
                                     data: earnedData,
                                     type: 'bar',
                                     barThickness: "flex",
-                                    barPercentage: 0.65,
+                                    barPercentage: 0.50,
                                     categoryPercentage: 0.25,
                                     order: 1,
                                     participantData: participantData,
@@ -908,11 +908,11 @@
                                         bottomLeft: 0,
                                         bottomRight: 0,
                                     },
-                                    borderSkipped: true,
+                                    // borderSkipped: true,
                                     data: totalData,
                                     type: 'bar',
                                     barThickness: "flex",
-                                    barPercentage: 0.65,
+                                    barPercentage: 0.50,
                                     categoryPercentage: 0.25,
                                     order: 2,
                                     participantData: participantData,
@@ -1026,14 +1026,14 @@
                                             let index = item.dataIndex;
                                             let monthNameExtended = item.dataset["monthNamesExtended"] ? item.dataset["monthNamesExtended"][index] : 0;
 
-                                            // return item.label + '\n' + 'TOTALE ATTESO: €' + new Intl.NumberFormat('it-IT').format(sum);
-                                            return monthNameExtended + '\n' + 'TOTALE ATTESO: €' + new Intl.NumberFormat('it-IT').format(sum);
+                                            // return item.label + '\n' + 'TOTALE ATTESO: €' + new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(sum);
+                                            return monthNameExtended + '\n' + 'TOTALE ATTESO: €' + new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(sum);
                                         },
                                         // labelTextColor: function(tooltipItems) {
                                         //     return tooltipItems.dataset.backgroundColor;
                                         // },
                                         label: function (tooltipItems) {
-                                            let label = tooltipItems.dataset.label + ': €' + new Intl.NumberFormat('it-IT').format(tooltipItems.parsed.y);
+                                            let label = tooltipItems.dataset.label + ': €' + new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(tooltipItems.parsed.y);
 
                                             return label;
                                         },
@@ -1082,7 +1082,7 @@
                 let tableHtml = `<div class="course-table">
                                     <div class="table-header">
                                         <div class="table-cell month">MESE</div>
-                                        <div class="table-cell percentage">%<br/>INCASSATO</div>
+                                        <div class="table-cell percentage">TOT. INCASSATO</div>
                                         <div class="table-cell delta">TOT. DA INCASSARE</div>
                                         <div class="table-cell suspended">SOSPESI</div>
                                     </div>`;
@@ -1111,14 +1111,15 @@
                     }
 
                     // Delta styling: positive when delta is 0 (fully paid), negative when there's missing amount
-                    const deltaClass = (total > 0 && delta === 0) ? 'positive' :
-                        (delta > 0) ? 'negative' : 'neutral';
+                    const deltaClass = (total > 0 && delta === 0) ? 'positive' : (delta > 0) ? 'negative' : 'neutral';
+                    const earnedClass = (earned > 0) ? 'positive' : 'neutral';
 
                     tableHtml += `
             <div class="table-row">
                 <div class="table-cell month">${row.month}</div>
-                <div class="table-cell percentage ${percentageClass}">${percentageDisplay}</div>
-                <div class="table-cell delta ${deltaClass}">€${new Intl.NumberFormat('it-IT').format(delta)}</div>
+                <!-- <div class="table-cell percentage ${percentageClass}">${percentageDisplay}</div> -->
+                <div class="table-cell earned ${earnedClass}">€${new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(earned)}</div>
+                <div class="table-cell delta ${deltaClass}">€${new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(delta)}</div>
                 <div class="table-cell suspended">${row.suspended}</div>
             </div>
         `;
@@ -1170,5 +1171,5 @@
                 }, 200);
             });
         });
-</script>
+    </script>
 </div>

+ 13 - 6
resources/views/livewire/settings.blade.php

@@ -16,14 +16,14 @@
             <h4 style="cursor:pointer;{{$type == 'corsi' ? 'border-bottom:2px solid var(--color-blu); color:var(--color-blu);' : ''}}" wire:click="change('corsi')">Corsi</h4>
             <h4 style="cursor:pointer;{{$type == 'contabilita' ? 'border-bottom:2px solid var(--color-blu); color:var(--color-blu);' : ''}}" wire:click="change('contabilita')">Contabilità</h4>
             <h4 style="cursor:pointer;{{$type == 'comunicazioni' ? 'border-bottom:2px solid var(--color-blu); color:var(--color-blu);' : ''}}" wire:click="change('comunicazioni')">Comunicazioni</h4>
-            <a href="/users"><h4>Profilo utenti</h4></a>
+            <a href="/users"><h4>Utenze</h4></a>
         </div>
 
         @if($type == 'anagrafica')
             <div id="anagrafica">
                 <a href="/categories">
                     <div class="row">
-                        <div class="col-md-11 p-2"><h5>Gruppi di appartenenza</h5></div>
+                        <div class="col-md-11 p-2"><h5>Gruppi di interesse</h5></div>
                         <div class="col-md-1 p-2"><i class="fa-solid fa-chevron-right"></i></div>
                     </div>
                 </a>
@@ -49,7 +49,7 @@
             <div id="corsi">
                 <a href="/course_subscriptions">
                     <div class="row">
-                        <div class="col-md-11 p-2"><h5>Abbonamenti</h5></div>
+                        <div class="col-md-11 p-2"><h5>Frequenza di pagamenti</h5></div>
                         <div class="col-md-1 p-2"><i class="fa-solid fa-chevron-right"></i></div>
                     </div>
                 </a>
@@ -61,6 +61,13 @@
                     </div>
                 </a>
                 <hr size="1">
+                <a href="/subscriptions">
+                    <div class="row">
+                        <div class="col-md-11 p-2"><h5>Abbonamenti</h5></div>
+                        <div class="col-md-1 p-2"><i class="fa-solid fa-chevron-right"></i></div>
+                    </div>
+                </a>
+                <hr size="1">
                 <a href="/course_durations">
                     <div class="row">
                         <div class="col-md-11 p-2"><h5>Durata</h5></div>
@@ -82,13 +89,13 @@
                     </div>
                 </a>
                 <hr size="1">
-                <a href="/course_types">
+                {{-- <a href="/course_types">
                     <div class="row">
                         <div class="col-md-11 p-2"><h5>Tipologie</h5></div>
                         <div class="col-md-1 p-2"><i class="fa-solid fa-chevron-right"></i></div>
                     </div>
                 </a>
-                <hr size="1">
+                <hr size="1"> --}}
             </div>
         @endif
 
@@ -96,7 +103,7 @@
             <div id="contabilita">
                 <a href="/banks">
                     <div class="row">
-                        <div class="col-md-11 p-2"><h5>Canali finanziari</h5></div>
+                        <div class="col-md-11 p-2"><h5>Banche</h5></div>
                         <div class="col-md-1 p-2"><i class="fa-solid fa-chevron-right"></i></div>
                     </div>
                 </a>

+ 571 - 232
resources/views/livewire/sms_comunications.blade.php

@@ -1,192 +1,367 @@
 <div class="col card--ui" id="card--dashboard">
 
-    @if(!$add && !$update)
-
-    <header id="title--section" style="display:none !important"  class="d-flex align-items-center justify-content-between">
+    <header id="title--section" style="display:none !important" class="d-flex align-items-center justify-content-between">
         <div class="title--section_name d-flex align-items-center justify-content-between">
             <i class="ico--ui title_section utenti me-2"></i>
-            <h2 class="primary">@if(!$add && !$update)SMS Templates @else Inserimento/modifica template SMS @endif</h2>
+            <h2 class="primary">Sms</h2>
         </div>
 
-        @if(!$add && !$update)
-            <div class="title--section_addButton" wire:click="add()" style="cursor: pointer;">
+        @if(!$showForm)
+            <div class="title--section_addButton" wire:click="add()" style="cursor: pointer;" wire:ignore>
                 <div class="btn--ui entrata d-flex justify-items-between">
-                    <a href="#" wire:click="add()" style="color:white">Aggiungi</a>
+                    <a href="#" style="color:white;">Aggiungi</a>
                 </div>
             </div>
         @endif
-
     </header>
 
-    <a class="btn--ui lightGrey" href="/settings?type=comunicazioni"><i class="fa-solid fa-arrow-left"></i></a><br>
-
-        <section id="resume-table">
-            <div class="compare--chart_wrapper d-none"></div>
+    <a class="btn--ui lightGrey" @if(!$showForm) href="/settings?type=comunicazioni" @else href="/sms_comunications" @endif><i class="fa-solid fa-arrow-left"></i></a><br/><br/>
 
+    {{-- LISTA MESSAGGI --}}
+    <section id="resume-table" @if($showForm) style="display:none" @endif>
+        <div wire:ignore>
             <table class="table tablesaw tableHead tablesaw-stack" id="tablesaw-350" width="100%">
                 <thead>
                     <tr>
-                        <th scope="col">Oggetto</th>
-                        <th scope="col">Messaggio</th>
-                        <th scope="col">Caratteri</th>
-                        <th scope="col">Data Creazione</th>
-                        <th scope="col">...</th>
+                        <th>ID</th>
+                        <th>Oggetto</th>
+                        <th># Destinatari</th>
+                        <th>Stato</th>
+                        <th>Programmato per</th>
+                        <th>Data invio</th>
+                        <th>Data creazione</th>
+                        <th>...</th>
                     </tr>
                 </thead>
                 <tbody id="checkall-target">
                     @foreach($records as $record)
-                        <tr>
+                        @php
+                            $state = $record->status;
+
+                            if (!$state) {
+                                if (($record->recipients_sent_count ?? 0) > 0 && ($record->recipients_failed_count ?? 0) > 0) {
+                                    $state = 'partial';
+                                } elseif (($record->recipients_sent_count ?? 0) > 0) {
+                                    $state = 'sent';
+                                } elseif (!empty($record->schedule_at)) {
+                                    $state = 'scheduled';
+                                } else {
+                                    $state = 'draft';
+                                }
+                            }
+
+                            $badgeMap = [
+                                'draft'      => 'secondary',
+                                'processing' => 'info',
+                                'scheduled'  => 'primary',
+                                'partial'    => 'warning',
+                                'sent'       => 'success',
+                                'failed'     => 'danger',
+                            ];
+                        @endphp
+                        <tr id="row_email_{{ $record->id }}">
+                            <td>{{ $record->id }}</td>
+                            <td><strong>{{ $record->subject }}</strong></td>
+                            <td style="padding-right: 20px">{{ $record->recipients_count ?? $record->recipients()->count() }}</td>
+                            <td><span class="badge bg-{{$badgeMap[$state]}}">{{ $record->status }}</span></td>
                             <td>
-                                <strong>{{$record->name}}</strong>
+                                @if(!empty($record->schedule_at))
+                                    {{ optional($record->schedule_at)->setTimezone('Europe/Rome')->format('d M Y - H:i') }}
+                                @endif
                             </td>
                             <td>
-                                {{ Str::limit($record->content, 50) }}
+                                @if(!empty($record->sent_at))
+                                    {{ optional($record->sent_at)->setTimezone('Europe/Rome')->format('d M Y - H:i') }}
+                                @endif
                             </td>
-                            <td>
-                                @php
-                                    $length = strlen($record->content);
-                                    $badgeClass = $length > 160 ? 'bg-danger' : ($length > 140 ? 'bg-warning' : 'bg-success');
-                                @endphp
-                                <span class="badge {{ $badgeClass }}">{{ $length }}/160</span>
-                            </td>
-                            <td>{{ $record->created_at->format('d/m/Y H:i') }}</td>
-                            <td>
-                                <button type="button" class="btn" wire:click="sendTemplate({{ $record->id }})" data-bs-toggle="popover" data-bs-trigger="hover focus" data-bs-placement="bottom" data-bs-content="Invia SMS"><i class="fa-solid fa-paper-plane"></i></button>
-                                <button type="button" class="btn" wire:click="edit({{ $record->id }})" data-bs-toggle="popover"  data-bs-trigger="hover focus" data-bs-placement="bottom" data-bs-content="Modifica"><i class="fa-regular fa-pen-to-square"></i></button>
+                            <td>{{ optional($record->created_at)->setTimezone('Europe/Rome')->format('d M Y - H:i') }}</td>
+                            <td class="d-flex gap-2">
+                                <button type="button" class="btn" wire:click="edit({{ $record->id }})" data-bs-toggle="tooltip" data-bs-trigger="hover focus" data-bs-placement="bottom" title="Modifica">
+                                    <i class="fa-regular fa-pen-to-square"></i>
+                                </button>
+                                <button type="button" class="btn" wire:click="duplicate({{ $record->id }})" data-bs-toggle="tooltip" data-bs-trigger="hover focus" data-bs-placement="bottom" title="Duplica">
+                                    <i class="fa-solid fa-copy"></i>
+                                </button>
+                                
+                                @if(in_array($record->status, ['draft','failed']))
+                                    <button type="button" class="btn text-danger"
+                                            onclick="if (confirm('Eliminare definitivamente questo sms?')) { Livewire.find('{{ $this->id }}').call('deleteMessage', {{ $record->id }}); }"
+                                            data-bs-toggle="tooltip" title="Elimina">
+                                        <i class="fa-solid fa-trash"></i>
+                                    </button>
+                                @endif
                             </td>
                         </tr>
                     @endforeach
                 </tbody>
             </table>
-        </section>
-
-    @else
+        </div>
+    </section>
 
+    {{-- FORM MESSAGGIO --}}
+    <section wire:key="email-form" @if(!$showForm) style="display:none" @endif>
         <div class="container">
 
-            <a class="btn--ui lightGrey" href="/sms_comunications"><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') }}
-                </div>
+            @if ($error)
+                <div class="alert alert-danger" role="alert">{{ $error }}</div>
+            @endif
+            @if ($success)
+                <div class="alert alert-success" role="alert">{{ $success }}</div>
             @endif
 
             <div class="row">
                 <div class="col">
 
-                    <form action="">
+                    <form>
 
-                        <div class="row mb-3">
-                            <div class="col">
-                                <div class="form--item">
-                                    <label for="subject" class="form-label">Oggetto</label>
-                                    <input type="text" class="form-control @error('subject') is-invalid @enderror" id="subject" wire:model="subject" placeholder="Inserisci l'oggetto del template">
-                                    @error('subject')
-                                        <div class="invalid-feedback">{{ $message }}</div>
-                                    @enderror
+                        {{-- Destinatari (selezionati) --}}
+                        <div class="row mb-5">
+                            <div class="col-12 mb-2">
+                                <h4>Destinatari</h4>
+                                <div class="recipients">
+                                    @if (empty($recipients))
+                                        <span>Nessun destinatario selezionato</span>
+                                    @else
+                                        @foreach ($recipients as $r)
+                                            @php
+                                                $fullName = trim(($r['last_name'] ?? '').' '.($r['first_name'] ?? ''));
+                                            @endphp
+                                            <div class="recipient">
+                                                <span class="recipient-name">{{ $fullName !== '' ? $fullName : '—' }}</span>
+                                                <span class="recipient-email">({{ $r['phone'] }})</span>
+                                            </div>
+                                        @endforeach
+                                    @endif
+                                </div>
+                                @error('recipients') <div class="invalid-feedback d-block">{{ $message }}</div> @enderror
+                            </div>
+                            <div class="col"></div>
+                            <div class="col-auto">
+                                <a style="cursor:pointer" class="addRecipients btn--ui"><i class="fa-solid fa-plus"></i></a>
+                            </div>
+                        </div>
+
+                        {{-- FILTRI + TABELLA DESTINATARI --}}
+                        <div class="row mb-5" wire:ignore id="addRecipientsRow" style="display: none">
+                            <div class="col-xs-12">
+                                <div class="showFilter" style="display: none">
+                                    <hr size="1">
+                                    <div class="row g-3">
+                                        <div class="col-md-3">
+                                            <div class="row">
+                                                <div class="col-md-12 mb-2"><b>Età</b></div>
+                                                <div class="col-12">
+                                                    <div class="row mb-2">
+                                                        <div class="col-3"><label class="form-check-label ms-2">Da</label></div>
+                                                        <div class="col-9"><input class="form-control" type="number" name="txtFromYear"></div>
+                                                    </div>
+                                                </div>
+                                                <div class="col-12">
+                                                    <div class="row">
+                                                        <div class="col-3"><label class="form-check-label ms-2">A</label></div>
+                                                        <div class="col-9"><input class="form-control" type="number" name="txtToYear"></div>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                        </div>
+
+                                        {{-- Altri filtri come da tua UI esistente --}}
+                                        <div class="col-md-3">
+                                            <div class="row">
+                                                <div class="col-md-12 mb-2"><b>Tipologia di tesseramento</b></div>
+                                                <div class="col-12">
+                                                    <select name="filterCards" class="form-select filterCards">
+                                                        <option value="">Tutte
+                                                        @foreach(getCards() as $card)
+                                                            <option value="{{$card->id}}">{{$card->name}}
+                                                        @endforeach
+                                                    </select>
+                                                </div>
+                                            </div>
+                                        </div>
+
+                                            <div class="col-md-3">
+                                                <div class="row">
+                                                    <div class="col-md-12 mb-2"><b>Stato tesseramento</b></div>
+                                                    <div class="col-12">
+                                                        <select name="filterStatus" class="form-select filterStatus" multiple="multiple">
+                                                            <option value="2">Attivo
+                                                            <option value="1">Sospeso
+                                                            <option value="0">Non tesserato
+                                                        </select>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                            <div class="col-md-3">
+                                                <div class="row">
+                                                    <div class="col-md-12 mb-2"><b>Gruppo di interesse</b></div>
+                                                    <div class="col-12">
+                                                        <select name="filterCategories" class="form-select filterCategories" multiple="multiple">
+                                                            <option value="">Tutte</option>
+                                                            @foreach($categories as $category)
+                                                                <option value="{{$category["id"]}}">
+                                                                    {!! str_repeat('&bull; ', $category["indentation"] ?? 0) !!}{{$category["name"]}}
+                                                                </option>
+                                                            @endforeach
+                                                        </select>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                            <div class="col-md-3">
+                                                <div class="row">
+                                                    <div class="col-md-12 mb-2"><b>Anno di nascita</b></div>
+                                                    <div class="col-12">
+                                                        <div class="row mb-2">
+                                                            <div class="col-3"><label class="form-check-label ms-2" >Da</label></div>
+                                                            <div class="col-9"><input class="form-control " type="number" name="txtFromYearYear"></div>
+                                                        </div>
+                                                    </div>
+                                                    <div class="col-12">
+                                                        <div class="row">
+                                                            <div class="col-3"><label class="form-check-label ms-2" >A</label></div>
+                                                            <div class="col-9"><input class="form-control " type="number"  name="txtToYearYear"></div>
+                                                        </div>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                            <div class="col-md-3">
+                                                <div class="row">
+                                                    <div class="col-md-12 mb-2"><b>Corso</b></div>
+                                                    <div class="col-12">
+                                                        <select name="filterCourses" class="form-select filterCourses" multiple="multiple">
+                                                            <option value="">Tutti</option>
+                                                            @foreach($courses as $course)
+                                                                <option value="{{ $course['id'] }}">
+                                                                    {!! str_repeat('&bull; ', $course['indentation'] ?? 0) !!}{{ $course['name'] }}
+                                                                </option>
+                                                            @endforeach
+                                                        </select>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                            <div class="col-md-3">
+                                                <div class="row">
+                                                    <div class="col-md-12 mb-2"><b>Scadenza certificato medico</b></div>
+                                                    <div class="col-12">
+                                                        <select name="filterScadenza" class="form-select filterScadenza" multiple="multiple">
+                                                            <option value="1">Scaduti
+                                                            <option value="2">In scadenza
+                                                            <option value="3">Non consegnato
+                                                            <option value="4">Validi
+                                                        </select>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                            <div class="col-md-3">
+                                                <div class="row">
+                                                    <div class="col-md-12 mb-2"><b>Tipologia certificato medico</b></div>
+                                                    <div class="col-12">
+                                                        <select name="filterCertificateType" class="form-select filterCertificateType" multiple="multiple">
+                                                            <option value="">Tutti
+                                                            <option value="N">Non agonistico
+                                                            <option value="A">Agonistico
+                                                        </select>
+                                                    </div>
+                                                </div>
+                                            </div>
+
+                                    </div>
+                                    <div class="row g-3">
+                                        <div class="col-md-12" style="text-align:right">
+                                            <button class="btn--ui lightGrey" onclick="resetFilters(event)">Reset</button>
+                                            <button class="btn--ui" onclick="event.preventDefault();loadDataTable()">FILTRA</button>
+                                        </div>
+                                    </div>
+                                    <hr size="1">
                                 </div>
                             </div>
+
+                            <div class="col-xs-12">
+                                <table id="recipients-table" class="table tablesaw tableHead tablesaw-stack w-100">
+                                    <thead>
+                                        <tr>
+                                            <th></th>
+                                            <th>Cognome</th>
+                                            <th>Nome</th>
+                                            <th>Email</th>
+                                            <th>Telefono</th>
+                                            <th>Età</th>
+                                            <th>Anno</th>
+                                            <th>Stato</th>
+                                            <th>Certificato</th>
+                                            <th>Gruppi</th>
+                                            {{-- <th>Corsi</th> --}}
+                                        </tr>
+                                    </thead>
+                                    <tbody></tbody>
+                                </table>
+                            </div>
                         </div>
 
-                        <div class="row mb-3">
+                        {{-- Oggetto --}}
+                        <div class="row mb-5">
                             <div class="col">
                                 <div class="form--item">
-                                    <label for="message" class="form-label">Messaggio</label>
-                                    <textarea class="form-control @error('message') is-invalid @enderror" id="message" wire:model="message" rows="4" placeholder="Inserisci il contenuto del messaggio (max 160 caratteri)" maxlength="160"></textarea>
-                                    <div class="form-text">
-                                        Caratteri: <span class="fw-bold">{{ strlen($message) }}</span>/160
-                                        @if(strlen($message) > 160)
-                                            <span class="text-danger"> - Troppi caratteri!</span>
-                                        @endif
-                                    </div>
-                                    @error('message')
-                                        <div class="invalid-feedback">{{ $message }}</div>
+                                    <h4>Oggetto</h4>
+                                    <input type="text" class="form-control @error('subject') is-invalid @enderror" id="subject" wire:model.defer="subject" placeholder="Oggetto sms" @if($locked) disabled @endif>
+                                    @error('subject')
+                                    <div class="invalid-feedback">{{ $message }}</div>
                                     @enderror
                                 </div>
                             </div>
                         </div>
 
-                        @if($add)
-                        <div class="row mb-3">
+                        {{-- Messaggio (CKEditor → content_html) --}}
+                        <div class="row mb-5">
                             <div class="col">
-                                <label class="form-label">Destinatari</label>
-                                <div class="mb-2">
-                                    <button type="button" class="btn btn-outline-primary btn-sm" onclick="selectAllUsers()">
-                                        <i class="fas fa-users me-1"></i>Seleziona Tutti
-                                    </button>
-                                    <button type="button" class="btn btn-outline-secondary btn-sm ms-2" onclick="deselectAllUsers()">
-                                        <i class="fas fa-times me-1"></i>Deseleziona Tutti
-                                    </button>
-                                </div>
-                                <div style="max-height: 200px; overflow-y: auto; border: 1px solid #dee2e6; border-radius: 0.375rem; padding: 10px;">
-                                    @foreach($members as $member)
-                                        <div class="form-check mb-1">
-                                            <input class="form-check-input" type="checkbox" value="{{ $member->id }}" wire:model="selectedRecipients" id="recipient_{{ $member->id }}">
-                                            <label class="form-check-label" for="recipient_{{ $member->id }}">
-                                                <strong>{{ $member->last_name }}</strong>
-                                                @if($member->phone)
-                                                    <small class="text-muted">({{ $member->phone }})</small>
-                                                @else
-                                                    <small class="text-danger">(no phone)</small>
-                                                @endif
-                                            </label>
-                                        </div>
-                                    @endforeach
-                                </div>
-                                <div class="form-text">
-                                    <small class="text-muted">Selezionati: <span id="selectedCount">{{ count($selectedRecipients) }}</span> utenti</small>
+                                <div class="form--item">
+                                    <h4>Messaggio</h4>
+                                    <textarea class="form-control" id="message" wire:model="content"></textarea>
+                                    @error('content')
+                                        <div class="invalid-feedback d-block">{{ $message }}</div>
+                                    @enderror
                                 </div>
-                                @error('selectedRecipients')
-                                    <div class="text-danger">{{ $message }}</div>
-                                @enderror
                             </div>
                         </div>
 
-                        <div class="row mb-3">
+                        {{-- Opzioni invio --}}
+                        <div class="row mb-5">
                             <div class="col">
-                                <label class="form-label">Opzioni di Invio</label>
-                                <div class="form-check">
-                                    <input class="form-check-input" type="radio" name="sendOption" id="sendNow" wire:model="sendNow" value="true">
-                                    <label class="form-check-label" for="sendNow">
-                                        <i class="fas fa-paper-plane me-2"></i>Invia Immediatamente
+                                <h4>Opzioni di Invio</h4>
+                                <div class="d-flex gap-3 comunication-send-options">
+                                    <label class="form-check">
+                                        <input class="form-check-input" type="radio" wire:model="mode" value="now">
+                                        <i class="fas fa-envelope me-2"></i> <span>Invia subito</span>
                                     </label>
-                                </div>
-                                <div class="form-check mt-2">
-                                    <input class="form-check-input" type="radio" name="sendOption" id="scheduleFor" wire:model="sendNow" value="false">
-                                    <label class="form-check-label" for="scheduleFor">
-                                        <i class="fas fa-clock me-2"></i>Programma per dopo
+                                    <label class="form-check">
+                                        <input class="form-check-input" type="radio" wire:model="mode" value="schedule">
+                                        <i class="fas fa-clock me-2"></i> <span>Programma</span>
                                     </label>
                                 </div>
                             </div>
                         </div>
 
-                        @if(!$sendNow)
-                        <div class="row mb-3">
-                            <div class="col-md-6">
-                                <label for="scheduledDateTime" class="form-label">Data e Ora di Invio</label>
-                                <input type="datetime-local" class="form-control @error('scheduledDateTime') is-invalid @enderror" id="scheduledDateTime" wire:model="scheduledDateTime">
-                                @error('scheduledDateTime')
-                                    <div class="invalid-feedback">{{ $message }}</div>
-                                @enderror
+                        @if(!$locked && $mode === 'schedule')
+                            <div class="row mb-5">
+                                <div class="col-md-6">
+                                    <label for="scheduledDateTime" class="form-label">Data e Ora di Invio</label>
+                                    <input type="datetime-local" class="form-control @error('schedule_at') is-invalid @enderror" id="scheduledDateTime" wire:model="schedule_at">
+                                    @error('schedule_at') <div class="invalid-feedback">{{ $message }}</div> @enderror
+                                </div>
                             </div>
-                        </div>
-                        @endif
                         @endif
 
-                        <div class="form--item">
-                            <button type="button" class="btn--ui lightGrey" wire:click="cancel()">Annulla</button>
-                            @if($add)
-                                <button type="submit" class="btn--ui" wire:click.prevent="store()">
-                                    @if($sendNow)
-                                        Salva e Invia
-                                    @else
-                                        Salva e Programma
-                                    @endif
-                                </button>
-                            @endif
-                            @if($update)
-                                <button type="submit" class="btn--ui" wire:click.prevent="update()">Salva</button>
+                        <div class="form--item mt-5 mb-5 d-flex gap-2">
+                            @if(!$locked)
+                                <a class="btn--ui lightGrey" href="/sms_comunications">Annulla</a>
+                                <button type="button" class="btn--ui" onclick="submitSMS('draft')" style="margin-right: auto">Salva bozza</button>
+                                @if($mode==='now')
+                                    <button type="button" class="btn--ui" onclick="submitSMS('send')">Invia ora</button>
+                                @else
+                                    <button type="button" class="btn--ui" onclick="submitSMS('schedule')">Salva & Programma</button>
+                                @endif
+                            @else
+                                <a class="btn--ui lightGrey" href="/sms_comunications">Torna indietro</a>
                             @endif
                         </div>
 
@@ -194,147 +369,311 @@
                 </div>
             </div>
         </div>
+    </section>
 
-    @endif
+    <input type="hidden" name="timezone" id="timezone" wire:model="timezone">
 </div>
 
 @if (session()->has('success'))
-    <div class="alert alert-success alert-dismissible fade show mt-3" role="alert">
-        {{ session()->get('success') }}
-        <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
-    </div>
+<div class="alert alert-success alert-dismissible fade show mt-3" role="alert">
+    {{ session('success') }}
+    <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
+</div>
 @endif
 
 @if (session()->has('error'))
-    <div class="alert alert-danger alert-dismissible fade show mt-3" role="alert">
-        {{ session()->get('error') }}
-        <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
-    </div>
+<div class="alert alert-danger alert-dismissible fade show mt-3" role="alert">
+    {{ session('error') }}
+    <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
+</div>
 @endif
 
 @push('scripts')
-    <link href="/css/datatables.css" rel="stylesheet" />
-    <script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
-    <script src="/assets/js/datatables.js"></script>
-    <script src="https://cdn.datatables.net/buttons/3.0.2/js/buttons.dataTables.js"></script>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/pdfmake.min.js"></script>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/vfs_fonts.js"></script>
+<link href="/css/datatables.css" rel="stylesheet" />
+<script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
+<script src="/assets/js/datatables.js"></script>
+<script src="https://cdn.datatables.net/buttons/3.0.2/js/buttons.dataTables.js"></script>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/pdfmake.min.js"></script>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/vfs_fonts.js"></script>
+
+<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
+<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
 @endpush
 
 @push('scripts')
-    <script>
+<script type="text/javascript">
+    document.addEventListener('livewire:load', () => {
+        if (!$.fn.DataTable.isDataTable('#tablesaw-350')) {
+            loadArchiveDataTable();
+        }
 
-        $(document).ready(function() {
-            loadDataTable();
-        });
+        window.addEventListener('sms-deleted', (e) => {
+            const id = e.detail?.id;
+            const table = $('#tablesaw-350');
+            if (!id || !$.fn.DataTable.isDataTable(table)) return;
 
-        Livewire.on('load-data-table', () => {
-            loadDataTable();
+            const dt = table.DataTable();
+            const rowEl = document.getElementById('row_email_' + id);
+            if (rowEl) {
+                dt.row(rowEl).remove().draw(false);
+            }
         });
 
-        function loadDataTable(){
-            let date = new Date();
-            let date_export = `${date.getFullYear()}${date.getMonth()}${date.getDate()}_`;
+        const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
+        @this.set('timezone', tz);
+    });
 
-            if ($.fn.DataTable.isDataTable('#tablesaw-350')) {
-                $('#tablesaw-350').DataTable().destroy();
-            }
+    window.addEventListener('init-recipients-table', (e) => {
+        const selected = e.detail?.selected || [];
+        loadDataTable(selected);
+
+        
+        const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
+        @this.set('timezone', tz);
+    });
+
+    function loadArchiveDataTable(){
+        let date = new Date();
+        let date_export = `${date.getFullYear()}${date.getMonth()}${date.getDate()}_`;
 
-            $('#tablesaw-350').DataTable({
-                processing: true,
-                thead: {
+        let table = $('#tablesaw-350').DataTable();
+        if ( $.fn.DataTable.isDataTable('#tablesaw-350') ) {
+            table.destroy();
+        }
+
+        $('#tablesaw-350').DataTable({
+            processing: true,
+            thead: {
                 'th': {'background-color': 'blue'}
-                },
-                layout: {
-                    topStart : null,
-                    topEnd : null,
-                    top1A: {
-                        // buttons: [
-                        //     {
-                        //         extend: 'collection',
-                        //         text: 'ESPORTA',
-                                buttons: [
-                                    {
-                                        extend: 'excelHtml5',
-                                        text: '<i class="fa-solid fa-file-excel"></i>',
-                                        action: newexportaction,
-                                        title: date_export + 'Templates SMS',
-                                        exportOptions: {
-                                            columns: ":not(':last')"
-                                        }
-                                    },
-                                    {
-                                        extend: 'pdfHtml5',
-                                        text: '<i class="fa-solid fa-file-pdf"></i>',
-                                        action: newexportaction,
-                                        title: date_export + 'Templates SMS',
+            },
+            order: [[7, 'desc']],
+            layout: {
+                topStart : null,
+                topEnd : null,
+                top1A: {
+                    // buttons: [
+                    //     {
+                    //         extend: 'collection',
+                    //         text: 'ESPORTA',
+                            buttons: [
+                                {
+                                    extend: 'excelHtml5',
+                                    text: '<i class="fa-solid fa-file-excel"></i>',
+                                    action: newexportaction,
+                                    title: date_export + 'SMS',
+                                    exportOptions: {
+                                        columns: ":not(':last')"
+                                    }
+                                },
+                                {
+                                    extend: 'pdfHtml5',
+                                    text: '<i class="fa-solid fa-file-pdf"></i>',
+                                    action: newexportaction,
+                                    title: date_export + 'SMS',
                                         exportOptions: {
                                             columns: ":not(':last')"
                                         }
                                     },
-                                    {
-                                        extend: 'print',
-                                        action: newexportaction,
-                                        text: '<i class="fa-solid fa-print"></i>',
-                                        title: date_export + 'Templates SMS',
-                                        exportOptions: {
-                                            columns: ":not(':last')"
-                                        }
+                                {
+                                    extend: 'print',
+                                    action: newexportaction,
+                                    text: '<i class="fa-solid fa-print"></i>',
+                                    title: date_export + 'SMS',
+                                    exportOptions: {
+                                        columns: ":not(':last')"
                                     }
-                                ],
-                        //         dropup: true
-                        //     }
-                        // ]
-                    },
-                    top1B : {
-                        pageLength: {
-                            menu: [[10, 25, 50, 100, 100000], [10, 25, 50, 100, "Tutti"]]
-                        }
-                    },
-                    top1C :'search',
+                                }
+                            ],
+                    //         dropup: true
+                    //     }
+                    // ]
                 },
-                pagingType: 'numbers',
-                "language": {
-                    "url": "/assets/js/Italian.json"
+                top1B : {
+                    pageLength: {
+                        menu: [[10, 25, 50, 100, 100000], [10, 25, 50, 100, "Tutti"]]
+                    }
                 },
-                "fnInitComplete": function (oSettings, json) {
-                    var html = '&nbsp;<a href="#" class="addData btn--ui"><i class="fa-solid fa-plus"></i></a>';
-                    $(".dt-search").append(html);
-                }
-            });
-            $('#tablesaw-350 thead tr th').addClass('col');
-            $('#tablesaw-350 thead tr th').css("background-color", "#f6f8fa");
+                top1C :'search',
+            },
+            pagingType: 'numbers',
+            language: {
+                "url": "/assets/js/Italian.json"
+            },
+            fnInitComplete: function (oSettings, json) {
+                var html = '&nbsp;<a href="#" class="addData btn--ui"><i class="fa-solid fa-plus"></i></a>';
+                $(".dt-search").append(html);
+            }
+        });
+
+        $('#tablesaw-350 thead tr th').addClass('col').css("background-color", "#f6f8fa");
 
-            $(document).ready(function() {
-                $(document).on("click",".addData",function() {
-                    $(".title--section_addButton").trigger("click")
-                });
+        $(document).on("click",".addData",function() {
+            @this.add();
+        });
+    }
+
+    $(document).on("click",".showHideFilter",function() {
+        $(".showFilter").toggle();
+        $('.filterCards,.filterStatus,.filterScadenza,.filterCertificateType,.filterCategories,.filterCourses').each(function(){
+            $(this).select2({
+                language: { noResults: ()=>"Nessun risultato" }
             });
+        });
+    });
 
-        }
+    $(document).on("click", ".addRecipients", function() {
+        $("#addRecipientsRow").toggle();
+    });
 
-        function selectAllUsers() {
-            $('input[wire\\:model="selectedRecipients"]').prop('checked', true).trigger('change');
-            updateSelectedCount();
-        }
+    window.resetFilters = function(event){
+        if (event) event.preventDefault();
 
-        function deselectAllUsers() {
-            $('input[wire\\:model="selectedRecipients"]').prop('checked', false).trigger('change');
-            updateSelectedCount();
-        }
+        $('.filterCards').val('').trigger('change');
+        $('.filterStatus').val('').trigger('change');
+        $('.filterScadenza').val('-1').trigger('change');
+        $('.filterCertificateType').val('-1').trigger('change');
+        $('.filterCategories').val('-1').trigger('change');
+        $('.filterCourses').val('-1').trigger('change');
+
+
+        $('input[name="txtFromYear"]').val('');
+        $('input[name="txtToYear"]').val('');
 
-        function updateSelectedCount() {
-            setTimeout(function() {
-                let count = $('input[wire\\:model="selectedRecipients"]:checked').length;
-                $('#selectedCount').text(count);
-            }, 100);
+        $('input[name="txtFromYearYear"]').val('');
+        $('input[name="txtToYearYear"]').val('');
+        loadDataTable();
+    }
+
+    function loadDataTable(preselected = []) {
+        const selectedIds = new Set((preselected || []).map(x => parseInt(x, 10)).filter(Boolean));
+
+        if ($.fn.DataTable.isDataTable('#recipients-table')) {
+            $('#recipients-table').DataTable().destroy();
         }
 
-        // Update count when checkboxes change
-        $(document).on('change', 'input[wire\\:model="selectedRecipients"]', function() {
-            updateSelectedCount();
+        var fromYear = $('input[name="txtFromYear"]').val();
+        var toYear = $('input[name="txtToYear"]').val();
+        var fromYearYear = $('input[name="txtFromYearYear"]').val();
+        var toYearYear = $('input[name="txtToYearYear"]').val();
+        var filterCards = $('.filterCards').val();
+        var filterStatus = $('.filterStatus').val();
+        var filterScadenza = $('.filterScadenza').val();
+        var filterCertificateType = $('.filterCertificateType').val();
+        var filterCategories = $('.filterCategories').val();
+        var filterCourses = $('.filterCourses').val();
+
+        const dataTable = $('#recipients-table').DataTable({
+            serverSide: true,
+            ajax: '/get_recipients?cards=' + filterCards + "&filterCategories=" + filterCategories + "&filterCertificateType=" + filterCertificateType + "&filterScadenza=" + filterScadenza + "&filterStatus=" + filterStatus + "&fromYear=" + fromYear + "&toYear=" + toYear + "&fromYearYear=" + fromYearYear + "&toYearYear=" + toYearYear + "&filterCourses=" + (filterCourses || ""),
+            columns: [
+                {
+                    orderable: false,
+                    data: "id",
+                    render: function (data){
+                        const id = parseInt(data, 10);
+                        const checked = selectedIds.has(id) ? 'checked' : '';
+                        return `<input type="checkbox" value="${id}" ${checked} onclick="toggleRecipient(${id})" id="recipient_${id}"/>`;
+                    }
+                },
+                {
+                    data: "last_name",
+                    render: function (data){
+                        const d = data.split("|");
+                        const id = d[1], value = d[0];
+                        return `<label for="recipient_${id}">${value}</label>`;
+                    }
+                },
+                {
+                    data: "first_name",
+                    render: function (data){
+                        const d = data.split("|");
+                        const id = d[1], value = d[0];
+                        return `<label for="recipient_${id}">${value}</label>`;
+                    }
+                },
+                {
+                    data: "email",
+                    render: function (data){
+                        const d = data.split("|");
+                        const id = d[1], value = d[0];
+                        return `<label for="recipient_${id}">${value}</label>`;
+                    }
+                },
+                { data: "phone"},
+                { data: "age", "type": "num", className:"dt-type-numeric"},
+                { data: "year", className:"dt-type-numeric"},
+                {
+                    data: "status",
+                    render: function (data){
+                        const d = data.split("|");
+                        return '<span class="tablesaw-cell-content"><span class="badge tessera-badge ' + d[0] + '">' + d[1] + '</span></span>';
+                    }
+                },
+                {
+                    data: "certificate",
+                    render: function (data){
+                        if (!data) return '<span class="tablesaw-cell-content d-flex align-items-center"><i class="ico--ui check absent me-2"></i>Non consegnato</span>';
+                        const d = data.split("|");
+                        const icon = d[0] === "0" ? "suspended" : (d[0] === "1" ? "due" : "active");
+                        const label = d[0] === "0" ? "Scaduto" : (d[0] === "1" ? "In scadenza" : "Scadenza");
+                        return '<span class="tablesaw-cell-content d-flex align-items-center"><i class="ico--ui check '+icon+' me-2"></i>'+label+' : '+d[1]+'</span>';
+                    }
+                },
+                { data: "categories" },
+            ],
+            order: [
+                [1, 'asc']
+            ],
+            fixedHeader: false,
+            thead: {
+                'th': {'background-color': 'blue'}
+            },
+            layout: {
+                topStart : null,
+                topEnd : null,
+                top1A: null,
+                top1B : {
+                    pageLength: {
+                        menu: [[10, 25, 50, 100, 100000], [10, 25, 50, 100, "Tutti"]]
+                    }
+                },
+                top1C :'search',
+            },
+            pagingType: 'numbers',
+            language: {
+                "url": "/assets/js/Italian.json"
+            },
+            fnInitComplete: function (oSettings, json) {
+                var html = '&nbsp;<a style="cursor:pointer" class="showHideFilter btn--ui"><i class="fa-solid fa-sliders"></i></a>';
+                $(".dt-search").append(html);
+            }
         });
 
-    </script>
-@endpush
+        $('#recipients-table thead tr th').addClass('col').css("background-color", "#f6f8fa");
+        $('#recipients-table').on('draw.dt', function() { $('[data-bs-toggle="popover"]').popover() });
+    }
+
+    window.toggleRecipient = function(id) { @this.toggleRecipient(id); }
+    
+
+    window.submitSMS = function(action){
+        if (action === 'draft') {
+            @this.call('saveDraft');
+        } else if (action === 'send') {
+            @this.call('sendNow');
+        } else {
+            @this.call('scheduleMessage');
+        }
+    };
+
+    window.addEventListener("scroll-top", (e) => {
+        let wrapper = document.querySelector('#card--dashboard');
+        if (wrapper) {
+            wrapper.scrollTo({top: 0, behavior: 'smooth'});
+        }
+    });
+</script>
+
+{{-- END CKEditor --}}
+@endpush

+ 318 - 0
resources/views/livewire/subscription.blade.php

@@ -0,0 +1,318 @@
+<div class="col card--ui" id="card--dashboard">
+
+    <header id="title--section" style="display:none !important"  class="d-flex align-items-center justify-content-between">
+        <div class="title--section_name d-flex align-items-center justify-content-between">
+            <i class="ico--ui title_section utenti me-2"></i>
+            <h2 class="primary">@if(!$add && !$update)Abbonamenti @else Inserimento/modifica abbonamento @endif</h2>
+        </div>
+
+        @if(!$add && !$update)
+            <div class="title--section_addButton"  wire:click="add()" style="cursor: pointer;">
+                <div class="btn--ui entrata d-flex justify-items-between">
+                    <a href="#" wire:click="add()" style="color:white">Aggiungi</a>
+                </div>
+            </div>
+        @endif
+
+    </header>
+
+
+    @if(!$add && !$update)
+
+        <a class="btn--ui lightGrey" href="/settings?type=corsi"><i class="fa-solid fa-arrow-left"></i></a><br>
+
+        <section id="resume-table">
+            <table class="table tablesaw tableHead tablesaw-stack" id="tablesaw-350" width="100%">
+                <thead>
+                    <tr>
+                        <th  style="text-align:center" scope="col"><input type="checkbox" class="checkAll"></th>
+                        <th scope="col">Nome</th>
+                        <th scope="col">N° partecipanti</th>
+                        <th scope="col">...</th>
+                    </tr>
+                </thead>
+                <tbody id="checkall-target">
+                    @foreach($records as $record)
+                        <tr>
+                            <td style="text-align:center"><input type="checkbox" class="chkCourse" name="{{$record->id}}"></td>
+                            <td>{{$record->name}}</td>
+                            <td style="padding-right: 50px">{{$record->getCount()}}</td>
+                            <td>
+                                <button type="button" class="btn" wire:click="edit({{ $record->id }})" data-bs-toggle="popover"  data-bs-trigger="hover focus" data-bs-placement="bottom" data-bs-content="Modifica"><i class="fa-regular fa-pen-to-square"></i></button>
+                                <button type="button" class="btn" onclick="confirm('Sei sicuro?') || event.stopImmediatePropagation()" wire:click="delete({{ $record->id }})" data-bs-toggle="popover" data-bs-trigger="hover focus" data-bs-placement="bottom" data-bs-content="Elimina"><i class="fa-regular fa-trash-can"></i></button>
+                                <button type="button" class="btn" wire:click="duplicate({{ $record->id }}, false)" data-bs-toggle="popover"  data-bs-trigger="hover focus" data-bs-placement="bottom" data-bs-content="Duplica"><i class="fa-regular fa-copy"></i></button>
+                            </td>
+                        </tr>
+                    @endforeach
+                </tbody>
+            </table>
+        </section>
+
+    @else
+
+        <a class="btn--ui lightGrey" href="/subscriptions"><i class="fa-solid fa-arrow-left"></i></a><br>
+
+        <div class="container">
+
+            @if (session()->has('error'))
+                <div class="alert alert-danger" role="alert">
+                    {{ session()->get('error') }}
+                </div>
+            @endif
+
+            <div class="row">
+                <div class="col">
+
+                    <form action="">
+
+                        <div class="row mb-3 ">
+                            <div class="col-6 mt-2">
+                                <div class="form--item">
+                                    <label for="inputName" class="form-label">Nome</label>
+                                    <input class="form-control js-keyupTitle @error('name') is-invalid @enderror" type="text" id="name" placeholder="Nome" wire:model="name">
+                                    @error('name')
+                                        <div class="invalid-feedback">{{ $message }}</div>
+                                    @enderror
+                                </div>
+                            </div>
+
+                            <div class="col-6 mt-2">
+                                <div class="form--item">
+                                    <label for="enabled" class="form-label">Abilitato</label>
+                                    <input class="form-check-input form-control" style="width:22px; height:22px;" type="checkbox" id="enabled" wire:model="enabled">
+                                </div>
+                            </div>
+                            
+                            {{-- <div class="col-3 mt-2">
+                                <div class="form--item">
+                                    <label for="inputName" class="form-label">Data inizio</label>
+                                    <input class="form-control @error('date_from') is-invalid @enderror" type="date" placeholder="Data inizio" wire:model="date_from">
+                                </div>
+                            </div>
+                            <div class="col-3 mt-2">
+                                <div class="form--item">
+                                    <label for="inputName" class="form-label">Data fine</label>
+                                    <input class="form-control @error('date_to') is-invalid @enderror" type="date" placeholder="Data fine" wire:model="date_to">
+                                </div>
+                            </div> --}}
+
+                            <div class="col-6 mt-2">
+                                <div class="form--item">
+                                    <label for="inputName" class="form-label">Prezzo iscrizione</label>
+                                    <input class="form-control js-keyupTitle @error('subscription_price') is-invalid @enderror" type="text" id="subscription_price" onkeyup="onlyNumberAmount(this)" placeholder="€ 0,00" wire:model="subscription_price">
+                                    @error('subscription_price')
+                                        <div class="invalid-feedback">{{ $message }}</div>
+                                    @enderror
+                                </div>
+                            </div>
+
+                            <div class="col-6 mt-2">
+                                @foreach($prices as $idP => $p)
+                                    <div class="row mt-2 ">
+                                        <div class="col-6">
+                                            <label for="abb" class="form-label">Pagamento</label>
+                                            <select class="form-control" wire:model="prices.{{$idP}}.course_subscription_id">
+                                                <option value=""></option>
+                                                @foreach($course_subscriptions as $s)
+                                                    <option value="{{$s["id"]}}">{{$s["name"]}}
+                                                @endforeach
+                                            </select>
+                                        </div>
+                                        <div class="col-4">
+                                            <label for="price" class="form-label">Prezzo</label>
+                                            <input class="form-control " type="text" onkeyup="onlyNumberAmount(this)" placeholder="€ 0,00" wire:model="prices.{{$idP}}.price">
+                                        </div>
+                                        <div class="col-2">
+                                            @if($idP == 0)
+                                                <br><button class="btn--ui primary add--daye" wire:click.prevent="addPrice()"><i class="fa-solid fa-plus"></i></button>
+                                            @endif
+                                            @if($idP > 0)
+                                                <br><button class="btn--ui primary add--daye" wire:click.prevent="delPrice({{$idP}})"><i class="fa-solid fa-minus"></i></button>
+                                            @endif
+                                        </div>
+                                        <div class="col-12">
+                                            <span style="color:red">{{$msgPrices}}</span>
+                                        </div>
+                                    </div>
+                                @endforeach
+                            </div>
+                        </div>
+
+                        <div class="form--item mt-4">
+                            <button type="button" class="btn--ui lightGrey" onclick="annulla()">Annulla</button>
+                            @if($add)
+                                <button type="submit" class="btn--ui" wire:click.prevent="store()">Salva</button>
+                            @endif
+                            @if($update)
+                                <button type="submit" class="btn--ui" wire:click.prevent="update()">Salva</button>
+                            @endif
+                        </div>
+
+                    </form>
+                </div>
+            </div>
+        </div>
+
+    @endif
+</div>
+
+@push('scripts')
+    <link href="/css/datatables.css" rel="stylesheet" />
+    <script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
+    <script src="/assets/js/datatables.js"></script>
+    <script src="https://cdn.datatables.net/buttons/3.0.2/js/buttons.dataTables.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/pdfmake.min.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/vfs_fonts.js"></script>
+@endpush
+
+@push('scripts')
+    <script>
+        function onlyNumberAmount(input) {
+            let v = input.value.replace(/\D+/g, '');
+            if (v.length > 14) v = v.slice(0, 14);
+            input.value = "€ " + v.replace(/(\d)(\d\d)$/, "$1,$2").replace(/(^\d{1,3}|\d{3})(?=(?:\d{3})+(?:,|$))/g, '$1.');
+        }
+
+        var isEdit = false;
+
+        Livewire.on('setEdit', (x) =>
+        {
+            isEdit = x;
+            console.log(isEdit);
+        });
+
+        function annulla()
+        {
+            window.onbeforeunload = null;
+            document.location.href = '/courses';
+        }
+
+        window.onbeforeunload = function(){
+            if (isEdit)
+                return 'Cambiando pagina le eventuali modifiche andranno perse';
+
+        };
+
+        $(document).ready(function() {
+            loadDataTable();
+        } );
+
+        Livewire.on('load-data-table', () => {
+            loadDataTable();
+        });
+
+        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();
+            }
+
+            $('#tablesaw-350').DataTable({
+                processing: true,
+                thead: {
+                'th': {'background-color': 'blue'}
+                },
+                layout: {
+                    topStart : null,
+                    topEnd : null,
+                    top1A: {
+                        buttons: [
+                            {
+                                extend: 'excelHtml5',
+                                text: '<i class="fa-solid fa-file-excel"></i>',
+                                action: newexportaction,
+                                title: date_export + 'Abbonamenti',
+                                exportOptions: {
+                                    columns: ":not(':last')"
+                                }
+                            },
+                            {
+                                extend: 'pdfHtml5',
+                                text: '<i class="fa-solid fa-file-pdf"></i>',
+                                action: newexportaction,
+                                title: date_export + 'Abbonamenti',
+                                exportOptions: {
+                                    columns: ":not(':last')"
+                                }
+                            },
+                            {
+                                extend: 'print',
+                                action: newexportaction,
+                                text: '<i class="fa-solid fa-print"></i>',
+                                title: date_export + 'Abbonamenti',
+                                exportOptions: {
+                                    columns: ":not(':last')"
+                                }
+                            }
+                        ],
+                    },
+                    top1B : {
+                        pageLength: {
+                            menu: [[10, 25, 50, 100, 100000], [10, 25, 50, 100, "Tutti"]]
+                        }
+                    },
+                    top1C :'search',
+                },
+                columnDefs: [
+                    {
+                        targets: 0,
+                        orderable: false
+                    }
+                ],
+                pagingType: 'numbers',
+                language: {
+                    url: "/assets/js/Italian.json"
+                },
+                fnInitComplete: function (oSettings, json) {
+                    var html = '&nbsp;<a href="#" class="addData btn--ui"><i class="fa-solid fa-plus"></i></a>';
+                    $(".dt-search").append(html);
+                    html = '&nbsp;<a href="#" class="duplicateData btn--ui" style="display:none"><i class="fa-solid fa-copy fa-lg" style="height:5px;"></i></a>';
+                    $(".dt-buttons").after(html);
+                }
+            });
+            $('#tablesaw-350 thead tr th').addClass('col');
+            $('#tablesaw-350 thead tr th').css("background-color", "#f6f8fa");
+
+            var subscriptions = [];
+            $(".chkCourse").click(function(){
+                var id = $(this).attr('name');
+                if(!subscriptions.includes(id)){
+                    subscriptions.push(id);
+                }else{
+                    subscriptions.splice(subscriptions.indexOf(id), 1);  //deleting
+                }
+                if (subscriptions.length > 0)
+                    $(".duplicateData").show();
+                else
+                    $(".duplicateData").hide();
+            });
+
+            $(document).ready(function() {
+                $(document).on("click",".addData",function() {
+                    $(".title--section_addButton").trigger("click");
+                });
+                $(document).on("click",".duplicateData",function() {
+                    @this.duplicateMultiple(subscriptions);
+                });
+            } );
+
+            var all = false;
+            $(".checkAll").click(function(){
+                all = !all;
+                subscriptions = [];
+                $('.chkCourse').each(function(){
+                    $(this).prop('checked', all);
+                    if (all)
+                        subscriptions.push($(this).attr('name'));
+                });
+                if (subscriptions.length > 0)
+                    $(".duplicateData").show();
+                else
+                    $(".duplicateData").hide();
+            });
+        }
+    </script>
+@endpush

+ 503 - 0
resources/views/livewire/subscription_member.blade.php

@@ -0,0 +1,503 @@
+<div class="col card--ui" id="card--dashboard">
+
+
+    <header id="title--section" style="display:none !important"
+        class="d-flex align-items-center justify-content-between">
+        <div class="title--section_name d-flex align-items-center justify-content-between">
+            <i class="ico--ui title_section utenti me-2"></i>
+            <h2 class="primary">Iscritti agli abbonamenti</h2>
+        </div>
+
+    </header>
+
+    <a class="btn--ui lightGrey" href="/subscription_member"><i class="fa-solid fa-arrow-left"></i></a><br>
+
+    @if ($subscription)
+        <h3>{{$subscription->name ?? 'Abbonamento Sconosciuto'}}</h3>
+    @endif
+
+
+    <div class="showFilter" style="display:none" wire:ignore.self>
+        <hr size="1">
+        <div class="row g-3 mt-1">
+            <div class="col-md-3">
+                <div class="row">
+                    <div class="col-md-12" style="margin-bottom:10px;">
+                        <b>Tipologia di tesseramento</b>
+                    </div>
+                    <div class="col-12">
+                        <select class="form-select filterCards" multiple="multiple">
+                            @foreach(getCards() as $card)
+                                <option value="{{$card->id}}">{{$card->name}}
+                            @endforeach
+                        </select>
+                    </div>
+                </div>
+            </div>
+            <div class="col-md-3">
+                <div class="row">
+                    <div class="col-md-12" style="margin-bottom:10px;">
+                        <b>Stato tesseramento</b>
+                    </div>
+                    <div class="col-12">
+                        <select class="form-select filterStatus" multiple="multiple">
+                            <option value="2">Attivo
+                            <option value="1">Sospeso
+                            <option value="0">Non tesserato
+                        </select>
+                    </div>
+                </div>
+            </div>
+            <div class="col-md-3">
+                <div class="row">
+                    <div class="col-md-12" style="margin-bottom:10px;">
+                        <b>Certificato medico</b>
+                    </div>
+                    <div class="col-12">
+                        <select class="form-select filterCertificateScadenza" multiple="multiple">
+                            <option value="1">Scaduti
+                            <option value="2">In scadenza
+                            <option value="3">Non presenti
+                            <option value="4">Validi
+                        </select>
+                    </div>
+                </div>
+            </div>
+            <div class="col-md-3">
+                <div class="row">
+                    <div class="col-md-12" style="margin-bottom:10px;">
+                        <b>Tipologia certificato medico</b>
+                    </div>
+                    <div class="col-12">
+                        <select class="form-select filterCertificateType" multiple="multiple">
+                            <option value="N">Non agonistico
+                            <option value="A">Agonistico
+                        </select>
+                    </div>
+                </div>
+            </div>
+
+        </div>
+        <div class="row g-3 mt-1">
+            <div class="col-md-3">
+                <div class="row">
+                    <div class="col-md-12" style="margin-bottom:10px;">
+                        <b>Età</b>
+                    </div>
+                    <div class="col-md-6">
+                        <div class="row align-items-center">
+                            <div class="col-auto"><label class="form-check-label ms-2">Da</label></div>
+                            <div class="col"><input class="form-control txt fromYear" type="number" min="0" step="1" name="fromYear"></div>
+                        </div>
+                    </div>
+                    <div class="col-md-6">
+                        <div class="row align-items-center">
+                            <div class="col-auto"><label class="form-check-label ms-2">A</label></div>
+                            <div class="col"><input class="form-control txt toYear" type="number" min="0" step="1" name="toYear"></div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="col-md-3">
+                <div class="row">
+                    <div class="col-md-12" style="margin-bottom:10px;">
+                        <b>Anno di nascita</b>
+                    </div>
+                    <div class="col-md-6">
+                        <div class="row align-items-center">
+                            <div class="col-auto"><label class="form-check-label ms-2">Da</label></div>
+                            <div class="col"><input class="form-control txt fromFromYear" name="fromYearYear" type="number" min="0" step="1" name="fromFromYear"></div>
+                        </div>
+                    </div>
+                    <div class="col-md-6">
+                        <div class="row align-items-center">
+                            <div class="col-auto"><label class="form-check-label ms-2">A</label></div>
+                            <div class="col"><input class="form-control txt toToYear" type="number" min="0" step="1" name="toToYear"></div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="row g-3 mt-2">
+            <div class="col-md-12" style="text-align:right">
+                <button class="btn--ui_outline light" onclick="reset()">RESET</button>
+                <button class="btn--ui" onclick="loadDataTable()">FILTRA</button>
+            </div>
+        </div>
+        <hr size="1">
+    </div>
+
+    <section id="resume-table">
+        <div class="compare--chart_wrapper d-none"></div>
+
+        <div id="filter">{{$filter}}</div>
+
+        <table class="table tablesaw tableHead tablesaw-stack" id="tablesaw-350" width="100%">
+            <thead>
+                <tr>
+                    <th scope="col">#</th>
+                    <th scope="col">Cognome</th>
+                    <th scope="col">Nome</th>
+                    <th scope="col">Certificato</th>
+                    <th scope="col">Stato</th>
+                    <th scope="col">...</th>
+                </tr>
+            </thead>
+            <tbody id="checkall-target">
+
+            </tbody>
+        </table>
+
+    </section>
+</div>
+
+@push('scripts')
+    <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
+    <style>
+        .select2-container--default .select2-selection--single {
+            background-color: #E9F0F5;
+            border: 0.0625rem solid #DFE5EB;
+            font-size: 0.75rem;
+        }
+
+        .select2-selection {
+            height: 38px !important;
+        }
+
+        .select2-selection__rendered {
+            padding-top: 3px;
+        }
+
+        .select2 {
+            width: 100% !important;
+        }
+    </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>
+@endpush
+
+@push('scripts')
+    <link href="/css/datatables.css" rel="stylesheet" />
+    <script src="https://code.jquery.com/jquery-2.2.4.min.js"
+        integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
+
+
+    <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
+    <style>
+        .select2-container--default .select2-selection--single {
+            background-color: #E9F0F5;
+            border: 0.0625rem solid #DFE5EB;
+            font-size: 0.75rem;
+        }
+
+        .select2-selection {
+            height: 38px !important;
+        }
+
+        .select2-selection__rendered {
+            padding-top: 3px;
+        }
+
+        .select2 {
+            width: 100% !important;
+        }
+
+        .select2-selection--multiple {
+            overflow: hidden !important;
+            height: auto !important;
+        }
+
+        .select2-container {
+            box-sizing: border-box;
+            display: inline-block;
+            margin: 0;
+            position: relative;
+            vertical-align: middle;
+        }
+
+        .select2-container .select2-selection--single {
+            box-sizing: border-box;
+            cursor: pointer;
+            display: block;
+            height: 38px;
+            user-select: none;
+            -webkit-user-select: none;
+        }
+
+        .select2-container .select2-selection--single .select2-selection__rendered {
+            display: block;
+            padding-left: 8px;
+            padding-right: 20px;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+        }
+
+        .select2-selection__choice__display {
+            color: #000000 !important;
+        }
+    </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="/assets/js/datatables.js"></script>
+    <script src="https://cdn.datatables.net/buttons/3.0.2/js/buttons.dataTables.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/pdfmake.min.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/vfs_fonts.js"></script>
+@endpush
+
+@push('scripts')
+    <script>
+        $(document).ready(function () {
+            loadDataTable();
+        });
+
+        $('.filterStatus').select2({ "language": { "noResults": function () { return "Nessun risultato"; } } });
+        $('.filterCertificateScadenza').select2({ "language": { "noResults": function () { return "Nessun risultato"; } } });
+        $('.filterCertificateType').select2({ "language": { "noResults": function () { return "Nessun risultato"; } } });
+        $('.filterCards').select2({ "language": { "noResults": function () { return "Nessun risultato"; } } });
+
+        Livewire.on('load-data-table', () => {
+            $('.filterStatus').select2({ "language": { "noResults": function () { return "Nessun risultato"; } } });
+            $('.filterCards').select2({ "language": { "noResults": function () { return "Nessun risultato"; } } });
+            $('.filterCertificateScadenza').select2({ "language": { "noResults": function () { return "Nessun risultato"; } } });
+            $('.filterCertificateType').select2({ "language": { "noResults": function () { return "Nessun risultato"; } } });
+            loadDataTable();
+        });
+
+        Livewire.on('destroy-data-table', () => {
+            $('#tablesaw-350').DataTable().destroy();
+        });
+
+        @if(isset($_GET["showFilters"]))
+            var filterStatus = localStorage.getItem("filterStatusSubscriptionMember");
+            if (filterStatus) {
+                $('.filterStatus').val(filterStatus).trigger('change');
+            }
+
+            var filterCards = localStorage.getItem("filterCardsSubscriptionMember");
+            if (filterCards) {
+                $('.filterCards').val(filterCards).trigger('change');
+            }
+
+            var filterCertificateScadenza = localStorage.getItem("filterCertificateScadenzaSubscriptionMember");
+            if (filterCertificateScadenza) {
+                $('.filterCertificateScadenza').val(filterCertificateScadenza).trigger('change');
+            }
+
+            var filterCertificateType = localStorage.getItem("filterCertificateTypeSubscriptionMember");
+            if (filterCertificateType) {
+                $('.filterCertificateType').val(filterCertificateType).trigger('change');
+            }
+
+            var fromYear = localStorage.getItem("fromYearSubscriptionMember");
+            if (fromYear) {
+                $('input[name="fromYear"]').val(fromYear);
+            }
+            var toYear = localStorage.getItem("toYearSubscriptionMember");
+            if (toYear) {
+                $('input[name="toYear"]').val(toYear);
+            }
+            var fromFromYear = localStorage.getItem("fromFromYearSubscriptionMember");
+            if (fromFromYear) {
+                $('input[name="fromYearYear"]').val(fromFromYear);
+            }
+
+            var toToYear = localStorage.getItem("toToYearSubscriptionMember");
+            if (toToYear) {
+                $('input[name="toToYear"]').val(toToYear);
+            }
+        @endif
+
+        function reset() {
+            $('.filterStatus').val(null).trigger("change");
+            $('.filterCards').val(null).trigger("change");
+            $('.filterCertificateScadenza').val(null).trigger("change");
+            $('.filterCertificateType').val(null).trigger("change");
+            $('.fromYear').val('');
+            $('.toYear').val('');
+            $('.fromFromYear').val('');
+            $('.toToYear').val('');
+            destroyDataTable();
+            loadDataTable();
+        }
+
+        function destroyDataTable() {
+            $('#tablesaw-350').DataTable().destroy();
+        }
+
+        var isFilter = false;
+        $(document).ready(function () {
+            $(document).on("click", ".showHideFilter", function () {
+                if (isFilter) {
+                    isFilter = false;
+                    $(".showFilter").hide();
+                }
+                else {
+                    isFilter = true;
+                    $(".showFilter").show();
+                }
+            });
+        });
+
+        function loadDataTable() {
+            if ($.fn.DataTable.isDataTable('#tablesaw-350')) {
+                $('#tablesaw-350').DataTable().destroy();
+            }
+
+            var filterStatus = $('.filterStatus').val();
+            localStorage.setItem("filterStatusSubscriptionMember", filterStatus);
+
+            var filterCards = $('.filterCards').val();
+            localStorage.setItem("filterCardsSubscriptionMember", filterCards);
+
+            var filterCertificateScadenza = $('.filterCertificateScadenza').val();
+            localStorage.setItem("filterCertificateScadenzaSubscriptionMember", filterCertificateScadenza);
+            console.log(filterCertificateScadenza);
+
+            var filterCertificateType = $('.filterCertificateType').val();
+            localStorage.setItem("filterCertificateTypeSubscriptionMember", filterCertificateType);
+
+            var fromYear = $('.fromYear').val();
+            localStorage.setItem("fromYearSubscriptionMember", fromYear);
+
+            var toYear = $('.toYear').val();
+            localStorage.setItem("toYearSubscriptionMember", toYear);
+
+            var fromFromYear = $('.fromFromYear').val();
+            localStorage.setItem("fromFromYearSubscriptionMember", fromFromYear);
+
+            var toToYear = $('.toToYear').val();
+            localStorage.setItem("toToYearSubscriptionMember", toToYear);
+
+            let date = new Date();
+            let date_export = `${date.getFullYear()}${date.getMonth()}${date.getDate()}_`;
+
+
+            $('#tablesaw-350').DataTable({
+                serverSide: true,
+                processing: true,
+                ajax: {
+                    url: '/get_subscription_members?subscription_id=' + @this.subscription_id + '&filterStatus=' + filterStatus + '&filterCards=' + filterCards + '&filterCertificateScadenza=' + filterCertificateScadenza + '&filterCertificateType=' + filterCertificateType + '&fromYear=' + fromYear + '&toYear=' + toYear + '&fromFromYear=' + fromFromYear + '&toToYear=' + toToYear,
+                },
+                thead: {
+                    'th': { 'background-color': 'blue' }
+                },
+                columns: [
+                    { data: 'column_0' },
+                    {
+                        data: 'column_1',
+                        render: function (data, type, row) {
+                            var ret = '<a href="/members?member_detail=' + row["column_7"] + '&from=subscription_member">' + data + '</a>';
+                            return ret;
+                        }
+                    },
+                    {
+                        data: 'column_2',
+                        render: function (data, type, row) {
+                            var ret = '<a href="/members?member_detail=' + row["column_7"] + '&from=subscription_member">' + data + '</a>';
+                            return ret;
+                        }
+                    },
+                    {
+                        data: 'column_3',
+                        render: function (data, type, row) {
+                            var certInfo = data.split("|");
+                            var status = certInfo[0];
+                            var expireDate = certInfo[1] || "";
+                            var html = '<span class="tablesaw-cell-content d-flex align-items-center">';
+                            if (status === "0") {
+                                html += '<i class="ico--ui check suspended me-2"></i> ';
+                                html += 'Scaduto: ' + expireDate;
+                            } else if (status === "1") {
+                                html += '<i class="ico--ui check due me-2"></i>';
+                                html += 'In scadenza: ' + expireDate;
+                            } else if (status === "2") {
+                                html += '<i class="ico--ui check active me-2"></i>';
+                                html += 'Scadenza: ' + expireDate;
+                                html += '<span style="float:right"></span>';
+                            }else {
+                                console.log(status);
+                                html += '<i class="ico--ui check absent me-2"></i>';
+                                html += 'Non consegnato';
+                            }
+                            html += '</span>';
+                            return html;
+                        }
+                    },
+                    {
+                        data: 'column_4',
+                        render: function (data, type, row) {
+                            var html = '<span class="badge tessera-badge ' + (data == 0 ? 'due' : (data == 1 ? 'active' : 'suspended')) + '">' + (data == 0 ? 'Da pagare' : (data == 1 ? 'Pagato' : 'Sospeso')) + '</span>';
+                            return html;
+                        }
+                    },
+                    { 
+                        data: 'column_5',
+                        render: function (data, type, row) {
+                            var html = '<a href="/rates?member_id=' + row["column_7"] + '&member_subscription_id=' + data + '"><b> <i class="fa-solid fa-edit"></i></b></a>';
+                            return html;
+                        }
+                    }],
+                layout: {
+                    topStart: null,
+                    topEnd: null,
+                    top1A: {
+                        buttons: [
+                            {
+                                extend: 'excelHtml5',
+                                text: '<i class="fa-solid fa-file-excel"></i>',
+                                action: newexportaction,
+                                title: date_export + 'Iscritti abbonamenti',
+                                exportOptions: {
+                                    columns: ":not(':last')"
+                                }
+                            },
+                            {
+                                extend: 'pdfHtml5',
+                                text: '<i class="fa-solid fa-file-pdf"></i>',
+                                action: newexportaction,
+                                title: date_export + 'Iscritti abbonamenti',
+                                exportOptions: {
+                                    columns: ":not(':last')"
+                                }
+                            },
+                            {
+                                extend: 'print',
+                                action: newexportaction,
+                                text: '<i class="fa-solid fa-print"></i>',
+                                title: date_export + 'Iscritti abbonamenti',
+                                exportOptions: {
+                                    columns: ":not(':last')"
+                                }
+                            }
+                        ],
+                    },
+                    top1B: {
+                        pageLength: {
+                            menu: [[10, 25, 50, 100, 100000], [10, 25, 50, 100, "Tutti"]]
+                        }
+                    },
+                    top1C: 'search',
+                },
+                order: [[0, 'asc'], [1, 'asc']],
+                pagingType: 'numbers',
+                language: {
+                    url: "/assets/js/Italian.json"
+                },
+                fnInitComplete: function (oSettings, json) {
+                    var html = '&nbsp;<a href="#" class="showHideFilter btn--ui"><i class="fa-solid fa-sliders"></i></a>';
+                    $(".dt-search").append(html);
+                }
+            });
+
+            $('#tablesaw-350 thead tr th').addClass('col');
+            $('#tablesaw-350 thead tr th').css("background-color", "#f6f8fa");
+            $('#tablesaw-350').on('draw.dt', function () {
+                $('[data-bs-toggle="popover"]').popover()
+            });
+        }
+    </script>
+@endpush

+ 141 - 0
resources/views/livewire/subscription_members.blade.php

@@ -0,0 +1,141 @@
+<div class="col card--ui" id="card--dashboard">
+
+    <header id="title--section" style="display:none !important"  class="d-flex align-items-center justify-content-between">
+        <div class="title--section_name d-flex align-items-center justify-content-between">
+            <i class="ico--ui title_section utenti me-2"></i>
+            <h2 class="primary">Durata corsi</h2>
+        </div>
+
+
+    </header>
+
+    <section id="resume-table">
+        <div class="compare--chart_wrapper d-none"></div>
+
+        <table class="table tablesaw tableHead tablesaw-stack" id="tablesaw-350" width="100%">
+            <thead>
+                <tr>
+                    <th scope="col">Nome</th>
+                    <th scope="col">N° partecipanti</th>
+                    <th scope="col">...</th>
+                </tr>
+            </thead>
+            <tbody id="checkall-target">
+                @foreach($records as $record)
+                    <tr>
+                        <td>{{$record->name}}</td>
+                        <td>{{$record->getCount()}}</td>
+                        <td>
+                            <a href="/subscription_member/{{$record->id}}" type="button" class="btn" ><b> <i class="fa-solid fa-chevron-right"></i></b></button>
+                        </td>
+                    </tr>
+                @endforeach
+            </tbody>
+        </table>
+    </section>
+</div>
+
+@push('scripts')
+    <link href="/css/datatables.css" rel="stylesheet" />
+    <script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
+    <script src="/assets/js/datatables.js"></script>
+    <script src="https://cdn.datatables.net/buttons/3.0.2/js/buttons.dataTables.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/pdfmake.min.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/vfs_fonts.js"></script>
+@endpush
+
+@push('scripts')
+    <script>
+
+        $(document).ready(function() {
+            loadDataTable();
+        } );
+
+        Livewire.on('load-data-table', () => {
+            loadDataTable();
+        });
+
+        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();
+            }
+            $('#tablesaw-350').DataTable({
+                processing: true,
+                thead: {
+                'th': {'background-color': 'blue'}
+                },
+                layout: {
+                    topStart : null,
+                    topEnd : null,
+                    top1A: {
+                        // buttons: [
+                        //     {
+                        //         extend: 'collection',
+                        //         text: 'ESPORTA',
+                                buttons: [
+                                    {
+                                        extend: 'excelHtml5',
+                                        text: '<i class="fa-solid fa-file-excel"></i>',
+                                        action: newexportaction,
+                                        title: date_export + 'Abbonamenti',
+                                        exportOptions: {
+                                            columns: ":not(':last')"
+                                        }
+                                    },
+                                    {
+                                        extend: 'pdfHtml5',
+                                        text: '<i class="fa-solid fa-file-pdf"></i>',
+                                        action: newexportaction,
+                                        title: date_export + 'Abbonamenti',
+                                        exportOptions: {
+                                            columns: ":not(':last')"
+                                        }
+                                    },
+                                    {
+                                        extend: 'print',
+                                        action: newexportaction,
+                                        text: '<i class="fa-solid fa-print"></i>',
+                                        title: date_export + 'Abbonamenti',
+                                        exportOptions: {
+                                            columns: ":not(':last')"
+                                        }
+                                    }
+                                ],
+                        //         dropup: true
+                        //     }
+                        // ]
+                    },
+                    top1B : {
+                        pageLength: {
+                            menu: [[10, 25, 50, 100, 100000], [10, 25, 50, 100, "Tutti"]]
+                        }
+                    },
+                    top1C :'search',
+                },
+                pagingType: 'numbers',
+                "language": {
+                    "url": "/assets/js/Italian.json"
+                }/*,
+                "fnInitComplete": function (oSettings, json) {
+                    var html = '&nbsp;<a href="#" class="addData btn--ui"><i class="fa-solid fa-plus"></i></a>';
+                    $(".dt-search").append(html);
+                }*/
+            });
+            $('#tablesaw-350 thead tr th').addClass('col');
+            $('#tablesaw-350 thead tr th').css("background-color", "#f6f8fa");
+
+            $(document).ready(function() {
+                $(document).on("click",".addData",function() {
+                    $(".title--section_addButton").trigger("click")
+                });
+            } );
+
+        }
+
+    </script>
+@endpush
+

+ 55 - 15
resources/views/livewire/user.blade.php

@@ -142,27 +142,80 @@
                                 </div>
                             @endif
 
+                            <div class="col-md-6 mt-3">
+                                <div class="form--item">
+                                    <label for="enabled" class="form-label">Livello</label>
+                                    <select class="form-control" id="level" wire:model="level">
+                                        <option value="0">Admin
+                                        <option value="1">Worker
+                                        <option value="2">Istruttore
+                                    </select>
+                                </div>
+                            </div>
+
                             @if ($add)
                                 <div class="col-md-6 mt-3">
                                     <div class="form--item">
                                         <label for="password" class="form-label">Password</label>
-                                        <input class="form-control js-keyupTitle @error('password') is-invalid @enderror" type="password" id="password" placeholder="Password" wire:model="password">
+                                        <div class="password-wrapper">
+                                            <input class="form-control js-keyupTitle @error('password') is-invalid @enderror" type="password" id="password" placeholder="Password" wire:model="password">
+                                            <div class="password-eye" onclick="togglePassword(this)">
+                                                <i class="fas fa-eye password-hidden"></i>
+                                                <i class="fas fa-eye-slash password-shown"></i>
+                                            </div>
+                                        </div>
                                         @error('password')
                                             <div class="invalid-feedback">{{ $message }}</div>
                                         @enderror
                                     </div>
                                 </div>
+                                <div class="col-md-6 mt-3">
+                                    <div class="form--item">
+                                        <label for="password_confirmation" class="form-label">Conferma password</label>
+                                        <div class="password-wrapper">
+                                            <input class="form-control js-keyupTitle @error('password_confirmation') is-invalid @enderror" type="password" id="password_confirmation" placeholder="Ripeti password" wire:model="password_confirmation">
+                                            <div class="password-eye" onclick="togglePassword(this)">
+                                                <i class="fas fa-eye password-hidden"></i>
+                                                <i class="fas fa-eye-slash password-shown"></i>
+                                            </div>
+                                        </div>
+                                        @error('password_confirmation')
+                                            <div class="invalid-feedback">{{ $message }}</div>
+                                        @enderror
+                                    </div>
+                                </div>
                             @elseif($canEditEmailAndPassword)
                                 <div class="col-md-6 mt-3">
                                     <div class="form--item">
                                         <label for="password" class="form-label">Password</label>
-                                        <input class="form-control js-keyupTitle @error('password') is-invalid @enderror" type="password" id="password" placeholder="Password" wire:model="password">
+                                        <div class="password-wrapper">
+                                            <input class="form-control js-keyupTitle @error('password') is-invalid @enderror" type="password" id="password" placeholder="Password" wire:model="password">
+                                            <div class="password-eye" onclick="togglePassword(this)">
+                                                <i class="fas fa-eye password-hidden"></i>
+                                                <i class="fas fa-eye-slash password-shown"></i>
+                                            </div>
+                                        </div>
                                         <small>Lasciare vuota per NON modificarla</small>
                                         @error('password')
                                             <div class="invalid-feedback">{{ $message }}</div>
                                         @enderror
                                     </div>
                                 </div>
+                                <div class="col-md-6 mt-3">
+                                    <div class="form--item">
+                                        <label for="password_confirmation" class="form-label">Conferma password</label>
+                                        <div class="password-wrapper">
+                                            <input class="form-control js-keyupTitle @error('password_confirmation') is-invalid @enderror" type="password" id="password_confirmation" placeholder="Ripeti password" wire:model="password_confirmation">
+                                            <div class="password-eye" onclick="togglePassword(this)">
+                                                <i class="fas fa-eye password-hidden"></i>
+                                                <i class="fas fa-eye-slash password-shown"></i>
+                                            </div>
+                                        </div>
+                                        @error('password_confirmation')
+                                            <div class="invalid-feedback">{{ $message }}</div>
+                                        @enderror
+                                    </div>
+                                </div>
                             @else
                                 <div class="col-md-6 mt-3">
                                     <div class="form--item">
@@ -174,19 +227,6 @@
                             @endif
 
                         </div>
-                        <div class="row mt-3">
-
-                            <div class="col-md-6">
-                                <div class="form--item">
-                                    <label for="enabled" class="form-label">Livello</label>
-                                    <select class="form-control" id="level" wire:model="level">
-                                        <option value="0">Admin
-                                        <option value="1">Worker
-                                        <option value="2">Istruttore
-                                    </select>
-                                </div>
-                            </div>
-                        </div>
                         <div class="row mt-3 mb-3">
                             <div class="col-md-6">
                                 <div class="form--item">

+ 37 - 1
resources/views/login.blade.php

@@ -6,6 +6,19 @@
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Dashboard</title>
+
+  {{-- Favicon --}}
+    <link rel="icon" type="image/png" href="{{asset('favicon-96x96.png')}}" sizes="96x96" />
+    <link rel="icon" type="image/svg+xml" href="{{asset('favicon.svg')}}" />
+    <link rel="shortcut icon" href="{{asset('favicon.ico')}}"/>
+    <link rel="shortcut icon" href="{{asset('favicon-light.ico')}}" media="(prefers-color-scheme: light)"/>
+    <link rel="shortcut icon" href="{{asset('favicon-dark.ico')}}" media="(prefers-color-scheme: dark)"/>
+    <link rel="apple-touch-icon" sizes="180x180" href="{{asset('apple-touch-icon.png')}}" />
+    <link rel="manifest" href="{{asset('site.webmanifest')}}" />
+  {{-- end Favicon --}}
+
+  <!-- Font Awesome -->
+  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
   <link rel="stylesheet" href="https://use.typekit.net/dit2bgs.css">
   <link rel="stylesheet" href="/css/style.css">
   <link rel="stylesheet" href="/css/new_style.css">
@@ -33,7 +46,13 @@
                 </div>
                 <div class="form--item input-group mb-3">
                     <label for="password">Password</label>
-                    <input type="password" class="form-control" placeholder="Password" id="password" name="password">
+                    <div class="password-wrapper">
+                        <input type="password" class="form-control" placeholder="Password" id="password" name="password">
+                        <div class="password-eye" onclick="togglePassword(this)">
+                            <i class="fas fa-eye password-hidden"></i>
+                            <i class="fas fa-eye-slash password-shown"></i>
+                        </div>
+                    </div>
                     <span class="input-group-text" id="basic-addon2"><i class="ico--ui lock"></i></span>
                 </div>
                 <div class="form--item input-group d-flex align-items-center justify-content-center">
@@ -52,6 +71,23 @@
 
   <script src="/assets/js/bootstrap.bundle.js"></script>
   <script src="/assets/js/app.js"></script>
+
+  <script>
+    function togglePassword(eye_icon) {
+        let wrapper = eye_icon.parentElement;
+        let password_html = wrapper.querySelector("input");
+
+        if (password_html) {
+            if (password_html.type == "password") {
+                password_html.type = "text";
+                eye_icon.classList.add("shown");
+            } else {
+                password_html.type = "password";
+                eye_icon.classList.remove("shown");
+            }
+        }
+    }
+  </script>
 </body>
 
 </html>

+ 6 - 0
resources/views/receipt.blade.php

@@ -140,6 +140,12 @@
 
     @foreach($receipt->rows as $row)
         <b>Causale</b>: {{@$row->causal->getTree()}}<br><br>
+        @if ($row->course)
+            <b>Corso</b>: {{$row->course->getDetailsName()}}<br><br>
+        @endif
+        @if ($row->subscription)
+            <b>Abbonamento</b>: {{$row->subscription->name}}<br><br>
+        @endif
         @if ($row->note != '')
             <b>Dettaglio causale</b>: {{$row->note}}<br><br>
         @endif

+ 444 - 43
routes/web.php

@@ -97,7 +97,7 @@ Route::group(['middleware' => 'tenant'], function () {
     Route::get('/banks', \App\Http\Livewire\Bank::class);
     Route::get('/vats', \App\Http\Livewire\Vat::class);
     Route::get('/disciplines', \App\Http\Livewire\Discipline::class);
-    Route::get('/course_types', \App\Http\Livewire\CourseType::class);
+    // Route::get('/course_types', \App\Http\Livewire\CourseType::class);
     Route::get('/course_subscriptions', \App\Http\Livewire\CourseSubscription::class);
     Route::get('/course_durations', \App\Http\Livewire\CourseDuration::class);
     Route::get('/course_levels', \App\Http\Livewire\CourseLevel::class);
@@ -126,6 +126,10 @@ Route::group(['middleware' => 'tenant'], function () {
     Route::get('/azienda', \App\Http\Livewire\Azienda::class);
     Route::get('/sms_comunications', \App\Http\Livewire\SmsComunications::class);
     Route::get('/mail_comunications', \App\Http\Livewire\EmailComunications::class);
+    Route::get('/members_archive', \App\Http\Livewire\MemberArchive::class);
+    Route::get('/subscriptions', \App\Http\Livewire\Subscription::class);
+    Route::get('/subscription_member', \App\Http\Livewire\SubscriptionMembers::class);
+    Route::get('/subscription_member/{id}', \App\Http\Livewire\SubscriptionMember::class);
 
     Route::get('/calendar', \App\Http\Livewire\Calendar::class);
     Route::get('/presences', \App\Http\Livewire\Presence::class);
@@ -257,10 +261,17 @@ Route::group(['middleware' => 'tenant'], function () {
 
         $datas = [];
 
+        $archived = (isset($_GET['archived']) && $_GET['archived'] == 1) ? true : false;
+
         $x = \App\Models\Member::select('id', 'first_name', 'last_name', 'phone', 'birth_date', 'to_complete', 'current_status', 'certificate', 'certificate_date')
+            ->where(function($query) use($archived) {
+                $query->where('is_archived', $archived);
+                if (!$archived)
+                    $query->orWhereNull('is_archived');
+            })
             ->where(function($query) {
-                $query->where('is_archived', false)
-                    ->orWhereNull('is_archived');
+                $query->where('is_deleted', false)
+                    ->orWhereNull('is_deleted');
             });
 
         if (isset($_GET["search"]["value"]) && $_GET["search"]["value"] != "") {
@@ -361,9 +372,14 @@ Route::group(['middleware' => 'tenant'], function () {
                 } else if ($filterValue == "3") {
                     // Only non-archived members without certificates
                     $scadIds = \App\Models\Member::whereNotIn('id', \App\Models\MemberCertificate::pluck('member_id'))
+                        ->where(function($query) use($archived) {
+                            $query->where('is_archived', $archived);
+                            if (!$archived)
+                                $query->orWhereNull('is_archived');
+                        })
                         ->where(function($query) {
-                            $query->where('is_archived', false)
-                                ->orWhereNull('is_archived');
+                            $query->where('is_deleted', false)
+                                ->orWhereNull('is_deleted');
                         })
                         ->pluck('id')->toArray();
                 } else if ($filterValue == "4") {
@@ -394,9 +410,16 @@ Route::group(['middleware' => 'tenant'], function () {
         if ($_GET["filterStatus"] != "null") {
             $status = explode(",", $_GET["filterStatus"]);
             // Only get non-archived members for status filtering
-            $members = \App\Models\Member::where(function($query) {
-                $query->where('is_archived', false)
-                    ->orWhereNull('is_archived');
+            $members = \App\Models\Member::where(function($query) use($archived) {
+                $query->where(function($q) use($archived) {
+                    $q->where('is_archived', $archived);
+                    if (!$archived)
+                        $q->orWhereNull('is_archived');
+                })
+                ->where(function($q) {
+                    $q->where('is_deleted', false)
+                        ->orWhereNull('is_deleted');
+                });
             })->get();
 
             foreach ($status as $s) {
@@ -426,7 +449,7 @@ Route::group(['middleware' => 'tenant'], function () {
             if ($_GET["order"][0]["column"] == 2)
                 $column = 'phone';
             if ($_GET["order"][0]["column"] == 3)
-                $column = 'birth_date';
+                $column = 'years';
             if ($_GET["order"][0]["column"] == 4)
                 $column = 'birth_date';
             if ($_GET["order"][0]["column"] == 5)
@@ -443,6 +466,14 @@ Route::group(['middleware' => 'tenant'], function () {
                 elseif ($column == 'current_status') {
                     $x = $x->orderBy('to_complete', 'DESC');
                     $x = $x->orderBy($column, $_GET["order"][0]["dir"]);
+                } elseif ($column == 'years') {
+                    $column = 'birth_date';
+                    $dirs = ['asc' => 'desc', 'desc' => 'asc'];
+                    $x = $x->orderByRaw($column . ' IS NULL ' . $dirs[$_GET["order"][0]["dir"]])
+                        ->orderBy($column, $dirs[$_GET["order"][0]["dir"]]);
+                } elseif ($column == 'birth_date') {
+                    $x = $x->orderByRaw($column . ' IS NULL ' . $_GET["order"][0]["dir"])
+                        ->orderBy($column, $_GET["order"][0]["dir"]);
                 } else {
                     $x = $x->orderBy($column, $_GET["order"][0]["dir"]);
                 }
@@ -532,11 +563,14 @@ Route::group(['middleware' => 'tenant'], function () {
 
         $datas = [];
 
-        // BASE IDENTICA ALLA TUA get_members
         $x = \App\Models\Member::select('id', 'first_name', 'last_name', 'email', 'phone', 'birth_date', 'to_complete', 'current_status', 'certificate', 'certificate_date')
             ->where(function($query) {
                 $query->where('is_archived', false)
                     ->orWhereNull('is_archived');
+            })
+            ->where(function($query) {
+                $query->where('is_deleted', false)
+                    ->orWhereNull('is_deleted');
             });
 
         if (isset($_GET["search"]["value"]) && $_GET["search"]["value"] != "") {
@@ -640,6 +674,10 @@ Route::group(['middleware' => 'tenant'], function () {
                             $query->where('is_archived', false)
                                 ->orWhereNull('is_archived');
                         })
+                        ->where(function($query) {
+                            $query->where('is_deleted', false)
+                                ->orWhereNull('is_deleted');
+                        })
                         ->pluck('id')->toArray();
                 } else if ($filterValue == "4") {
                     $memberLatestCerts = DB::table('member_certificates')
@@ -669,8 +707,14 @@ Route::group(['middleware' => 'tenant'], function () {
         if ($_GET["filterStatus"] != "null") {
             $status = explode(",", $_GET["filterStatus"]);
             $members = \App\Models\Member::where(function($query) {
-                $query->where('is_archived', false)
-                    ->orWhereNull('is_archived');
+                $query->where(function($q) {
+                    $q->where('is_archived', false)
+                        ->orWhereNull('is_archived');
+                })
+                ->where(function($q) {
+                    $q->where('is_deleted', false)
+                        ->orWhereNull('is_deleted');
+                });
             })->get();
 
             foreach ($status as $s) {
@@ -722,41 +766,35 @@ Route::group(['middleware' => 'tenant'], function () {
 
         if (isset($_GET["order"])) {
             $column = '';
-            if ($_GET["order"][0]["column"] == 0)
-                $column = 'last_name';
             if ($_GET["order"][0]["column"] == 1)
-                $column = 'first_name';
+                $column = 'last_name';
             if ($_GET["order"][0]["column"] == 2)
-                $column = 'email';
+                $column = 'first_name';
             if ($_GET["order"][0]["column"] == 3)
-                $column = 'phone';
+                $column = 'email';
             if ($_GET["order"][0]["column"] == 4)
-                $column = 'birth_date';
+                $column = 'phone';
             if ($_GET["order"][0]["column"] == 5)
                 $column = 'birth_date';
             if ($_GET["order"][0]["column"] == 6)
-                $column = 'current_status';
+                $column = 'birth_date';
             if ($_GET["order"][0]["column"] == 7)
+                $column = 'current_status';
+            if ($_GET["order"][0]["column"] == 8)
                 $column = 'certificate';
 
             if ($column != '') {
-                if ($column == 'last_name') {
-                    $x = $x->orderBy('to_complete', 'DESC');
-                }
                 if ($column == 'certificate')
                     $x = $x->orderBy('certificate_date', $_GET["order"][0]["dir"]);
                 elseif ($column == 'current_status') {
-                    $x = $x->orderBy('to_complete', 'DESC');
                     $x = $x->orderBy($column, $_GET["order"][0]["dir"]);
                 } else {
                     $x = $x->orderBy($column, $_GET["order"][0]["dir"]);
                 }
             } else {
-                $x = $x->orderBy('to_complete', 'DESC');
                 $x = $x->orderBy('last_name', 'ASC')->orderBy('first_name', 'ASC');
             }
         } else {
-            $x = $x->orderBy('to_complete', 'DESC');
             $x = $x->orderBy('last_name', 'ASC')->orderBy('first_name', 'ASC');
         }
 
@@ -864,7 +902,7 @@ Route::group(['middleware' => 'tenant'], function () {
             ->leftJoin('receipts', 'records.id', '=', 'receipts.record_id')
             ->where('records.type', 'IN');
 
-        $y = \App\Models\Record::select('records_rows.amount', 'records.member_id', 'records.corrispettivo_fiscale', 'records.deleted', 'records.financial_movement', 'records_rows.causal_id', DB::raw('members.first_name as first_name'), DB::raw('members.last_name as last_name')) // , \DB::raw('SUM(records.id) As total'))
+        $y = \App\Models\Record::select('records_rows.amount', 'records.member_id', 'records.corrispettivo_fiscale', 'records.deleted', 'records.financial_movement', 'records_rows.causal_id', 'records_rows.course_id', 'records_rows.subscription_id', DB::raw('members.first_name as first_name'), DB::raw('members.last_name as last_name')) // , \DB::raw('SUM(records.id) As total'))
             ->leftJoin('members', 'records.member_id', '=', 'members.id')
             ->leftJoin('records_rows', 'records.id', '=', 'records_rows.record_id')
             //->leftJoin('receipts', 'records.id', '=', 'receipts.record_id')
@@ -947,6 +985,35 @@ Route::group(['middleware' => 'tenant'], function () {
             $x = $x->whereIn('records.id', $causals);
             $y = $y->whereIn('records.id', $causals);
         }
+        if ($_GET["filterCourses"] != "null") {
+            $hasFilter = true;
+            $courses = explode(",", $_GET["filterCourses"]);
+
+            // Per ogni corso, se ha dei figli allora aggiungo i corsi figli
+            foreach ($courses as $c) {
+                $childs = \App\Models\Course::where('parent_id', $c)->get();
+                foreach ($childs as $cc) {
+                    $courses[] = $cc->id;
+                    $childss = \App\Models\Course::where('parent_id', $cc->id)->get();
+                    foreach ($childss as $ccc) {
+                        $courses[] = $ccc->id;
+                    }
+                }
+            }
+
+            //$courses = \App\Models\RecordRow::where('course_id', $_GET["filtercourses"])->pluck('record_id');
+            $courses = \App\Models\RecordRow::whereIn('course_id', $courses)->pluck('record_id');
+            $x = $x->whereIn('records.id', $courses);
+            $y = $y->whereIn('records.id', $courses);
+        }
+        if ($_GET["filterSubscriptions"] != "null") {
+            $hasFilter = true;
+            $subscriptions = explode(",", $_GET["filterSubscriptions"]);
+
+            $subscriptions = \App\Models\RecordRow::whereIn('subscription_id', $subscriptions)->pluck('record_id');
+            $x = $x->whereIn('records.id', $subscriptions);
+            $y = $y->whereIn('records.id', $subscriptions);
+        }
         if ($_GET["filterFrom"] != '') {
             $hasFilter = true;
             $x = $x->where('records.date', '>=', $_GET["filterFrom"]);
@@ -981,7 +1048,15 @@ Route::group(['middleware' => 'tenant'], function () {
                 $excludeCausals[] = $e->id;
             }*/
 
-        $exclude_from_records = \App\Models\Member::where('exclude_from_records', true)->pluck('id')->toArray();
+        $exclude_from_records = \App\Models\Member::where('exclude_from_records', true)
+            ->where(function($query) {
+                $query->where('is_archived', false)
+                    ->orWhereNull('is_archived');
+            })
+            ->where(function($query) {
+                $query->where('is_deleted', false)
+                    ->orWhereNull('is_deleted');
+            })->pluck('id')->toArray();
 
         // Pagamento money
         $moneys = \App\Models\PaymentMethod::where('money', true)->pluck('id')->toArray();
@@ -1036,8 +1111,15 @@ Route::group(['middleware' => 'tenant'], function () {
         foreach ($x as $idx => $r) {
 
             $causals = '';
+            $courses = '';
+            $subscriptions = '';
             foreach ($r->rows as $row) {
                 $causals .= $row->causal->getTree() . "<br>";
+
+                if ($row->course)
+                    $courses .= $row->course->getDetailsName() . "<br>";
+                if ($row->subscription)
+                    $subscriptions .= $row->subscription->name . "<br>";
             }
 
             $datas[] = array(
@@ -1049,6 +1131,7 @@ Route::group(['middleware' => 'tenant'], function () {
                 'last_name' => $r->last_name,
                 'commercial' => $r->financial_movement ? 'Movimento finanziario' : ($r->commercial ? 'SI' : 'NO'),
                 'causals' => $causals,
+                'courses' => $subscriptions . $courses,
                 'payment' => $r->payment_method->name,
                 //'payment_date' => date("d/m/Y", strtotime($r->date)),
                 'status' => $r->deleted ? 'Annullato' : '',
@@ -1172,9 +1255,17 @@ Route::group(['middleware' => 'tenant'], function () {
             if ($_GET["search"]["value"] != '') {
                 $v = str_replace("'", "\'", stripcslashes($_GET["search"]["value"]));
                 $member_ids = \App\Models\Member::where(function ($query) use ($v) {
-                    $query->whereRaw("CONCAT(first_name, ' ', last_name) like '%" . $v . "%'")
-                        ->orWhereRaw("CONCAT(last_name, ' ', first_name) like '%" . $v . "%'");
-                })->pluck('id');
+                        $query->whereRaw("CONCAT(first_name, ' ', last_name) like '%" . $v . "%'")
+                            ->orWhereRaw("CONCAT(last_name, ' ', first_name) like '%" . $v . "%'");
+                    })
+                    ->where(function($query) {
+                        $query->where('is_archived', false)
+                            ->orWhereNull('is_archived');
+                    })
+                    ->where(function($query) {
+                        $query->where('is_deleted', false)
+                            ->orWhereNull('is_deleted');
+                    })->pluck('id');
                 /*
                     $v = str_replace("'", "\'", stripcslashes($_GET["search"]["value"]));
                     $member_ids = \App\Models\Member::where(function ($query) use ($v) {
@@ -1212,7 +1303,7 @@ Route::group(['middleware' => 'tenant'], function () {
             $course_ids = \App\Models\Course::whereIn('course_frequency_id', $frequencies)->pluck('id');
             $member_course = $member_course->whereIn('course_id', $course_ids);
         }
-        if ($_GET["filterType"] != "null") {
+        if ($_GET["filterType"] != "null" && $_GET["filterType"] != "undefined") {
             $types = explode(",", $_GET["filterType"]);
             $course_ids = \App\Models\Course::whereIn('course_type_id', $types)->pluck('id');
             $member_course = $member_course->whereIn('course_id', $course_ids);
@@ -1543,13 +1634,21 @@ Route::group(['middleware' => 'tenant'], function () {
             ->leftJoin('courses', 'member_courses.course_id', '=', 'courses.id')
             ->leftJoin('members', 'member_courses.member_id', '=', 'members.id');
 
-        if (isset($_GET["search"]["value"])) {
+        if (isset($_GET["search"]["value"]) && trim($_GET["search"]["value"]) != "") {
             $v = str_replace("'", "\'", stripcslashes($_GET["search"]["value"]));
 
             $member_ids = \App\Models\Member::where(function ($query) use ($v) {
-                $query->whereRaw("CONCAT(first_name, ' ', last_name) like '%" . $v . "%'")
-                    ->orWhereRaw("CONCAT(last_name, ' ', first_name) like '%" . $v . "%'");
-            })->pluck('id');
+                    $query->whereRaw("CONCAT(first_name, ' ', last_name) like '%" . $v . "%'")
+                        ->orWhereRaw("CONCAT(last_name, ' ', first_name) like '%" . $v . "%'");
+                })
+                ->where(function($query) {
+                    $query->where('is_archived', false)
+                        ->orWhereNull('is_archived');
+                })
+                ->where(function($query) {
+                    $query->where('is_deleted', false)
+                        ->orWhereNull('is_deleted');
+                })->pluck('id');
 
             /*$member_ids = \App\Models\Member::where(function ($query) use ($v) {
                     $query->where('first_name', 'like', '%' . $v . '%')
@@ -1583,7 +1682,7 @@ Route::group(['middleware' => 'tenant'], function () {
             $course_ids = \App\Models\Course::whereIn('course_frequency_id', $frequencies)->pluck('id');
             $datas = $datas->whereIn('course_id', $course_ids);
         }
-        if ($_GET["filterType"] != "null") {
+        if ($_GET["filterType"] != "null" && $_GET["filterType"] != "undefined") {
             $types = explode(",", $_GET["filterType"]);
             $course_ids = \App\Models\Course::whereIn('course_type_id', $types)->pluck('id');
             $datas = $datas->whereIn('course_id', $course_ids);
@@ -1662,6 +1761,14 @@ Route::group(['middleware' => 'tenant'], function () {
                     $allScadIds = array_merge($allScadIds, $expiringMemberIds);
                 } else if ($filterValue == "3") {
                     $scadIds = \App\Models\Member::whereNotIn('id', \App\Models\MemberCertificate::pluck('member_id'))
+                        ->where(function($query) {
+                            $query->where('is_archived', false)
+                                ->orWhereNull('is_archived');
+                        })
+                        ->where(function($query) {
+                            $query->where('is_deleted', false)
+                                ->orWhereNull('is_deleted');
+                        })
                         ->pluck('id')
                         ->toArray();
                     $allScadIds = array_merge($allScadIds, $scadIds);
@@ -1693,21 +1800,53 @@ Route::group(['middleware' => 'tenant'], function () {
         }
 
         if ($_GET["fromYear"] != "") {
-            $m_ids = \App\Models\Member::where('birth_date', '<', date("Y-m-d", strtotime("-" . $_GET["fromYear"] . " year", time())))->pluck('id');
+            $m_ids = \App\Models\Member::where('birth_date', '<', date("Y-m-d", strtotime("-" . $_GET["fromYear"] . " year", time())))
+                ->where(function($query) {
+                    $query->where('is_archived', false)
+                        ->orWhereNull('is_archived');
+                })
+                ->where(function($query) {
+                    $query->where('is_deleted', false)
+                        ->orWhereNull('is_deleted');
+                })->pluck('id');
             $datas = $datas->whereIn('member_id', $m_ids);
         }
 
         if ($_GET["toYear"] != "") {
-            $m_ids = \App\Models\Member::where('birth_date', '>', date("Y-m-d", strtotime("-" . $_GET["toYear"] . " year", time())))->pluck('id');
+            $m_ids = \App\Models\Member::where('birth_date', '>', date("Y-m-d", strtotime("-" . $_GET["toYear"] . " year", time())))
+                ->where(function($query) {
+                    $query->where('is_archived', false)
+                        ->orWhereNull('is_archived');
+                })
+                ->where(function($query) {
+                    $query->where('is_deleted', false)
+                        ->orWhereNull('is_deleted');
+                })->pluck('id');
             $datas = $datas->whereIn('member_id', $m_ids);
         }
 
         if ($_GET["fromFromYear"] != "") {
-            $m_ids = \App\Models\Member::whereYear('birth_date', '>=', $_GET["fromFromYear"])->pluck('id');
+            $m_ids = \App\Models\Member::whereYear('birth_date', '>=', $_GET["fromFromYear"])
+                ->where(function($query) {
+                    $query->where('is_archived', false)
+                        ->orWhereNull('is_archived');
+                })
+                ->where(function($query) {
+                    $query->where('is_deleted', false)
+                        ->orWhereNull('is_deleted');
+                })->pluck('id');
             $datas = $datas->whereIn('member_id', $m_ids);
         }
         if ($_GET["toToYear"] != "") {
-            $m_ids = \App\Models\Member::whereYear('birth_date', '<=', $_GET["toToYear"])->pluck('id');
+            $m_ids = \App\Models\Member::whereYear('birth_date', '<=', $_GET["toToYear"])
+                ->where(function($query) {
+                    $query->where('is_archived', false)
+                        ->orWhereNull('is_archived');
+                })
+                ->where(function($query) {
+                    $query->where('is_deleted', false)
+                        ->orWhereNull('is_deleted');
+                })->pluck('id');
             $datas = $datas->whereIn('member_id', $m_ids);
         }
         if ($_GET["filterCards"] != "null") {
@@ -1844,6 +1983,260 @@ Route::group(['middleware' => 'tenant'], function () {
         return json_encode(array("data" => $ret, "recordsTotal" => sizeof($aRet), "recordsFiltered" => sizeof($aRet)));
     });
 
+    Route::get('/get_subscription_members', function () {
+        $subscription_id = 0;
+        if (!isset($_GET['subscription_id']) || is_null($_GET['subscription_id']) || $_GET['subscription_id'] <= 0) {
+            return json_encode(array("data" => [], "recordsTotal" => 0, "recordsFiltered" => 0));
+        }
+        $subscription_id = $_GET['subscription_id'];
+
+        $datas = \App\Models\MemberSubscription::select('member_subscriptions.*', 'subscriptions.name as course_name', 'members.first_name', 'members.last_name', 'members.birth_date')
+            ->leftJoin('subscriptions', 'member_subscriptions.subscription_id', '=', 'subscriptions.id')
+            ->leftJoin('members', 'member_subscriptions.member_id', '=', 'members.id')
+            ->where('member_subscriptions.subscription_id', $subscription_id);
+
+        if (isset($_GET["search"]["value"]) && trim($_GET["search"]["value"]) != "") {
+            $v = str_replace("'", "\'", stripcslashes($_GET["search"]["value"]));
+
+            $member_ids = \App\Models\Member::where(function ($query) use ($v) {
+                    $query->whereRaw("CONCAT(first_name, ' ', last_name) like '%" . $v . "%'")
+                        ->orWhereRaw("CONCAT(last_name, ' ', first_name) like '%" . $v . "%'");
+                })
+                ->where(function($query) {
+                    $query->where('is_archived', false)
+                        ->orWhereNull('is_archived');
+                })
+                ->where(function($query) {
+                    $query->where('is_deleted', false)
+                        ->orWhereNull('is_deleted');
+                })->pluck('id');
+
+            $datas = $datas->whereIn('member_id', $member_ids);
+        }
+
+        if ($_GET["filterCertificateScadenza"] != "null") {
+            $scadenzaValues = explode(",", $_GET["filterCertificateScadenza"]);
+            $allScadIds = [];
+
+            foreach ($scadenzaValues as $filterValue) {
+                if ($filterValue == "1") {
+                    $memberLatestCerts = DB::table('member_certificates')
+                        ->select('member_id', DB::raw('MAX(expire_date) as latest_expire_date'))
+                        ->groupBy('member_id')
+                        ->get();
+
+                    $expiredMemberIds = [];
+                    foreach ($memberLatestCerts as $cert) {
+                        if (date('Y-m-d', strtotime($cert->latest_expire_date)) < date('Y-m-d')) {
+                            $expiredMemberIds[] = $cert->member_id;
+                        }
+                    }
+                    $allScadIds = array_merge($allScadIds, $expiredMemberIds);
+
+                    Log::info("Course members - Expired certificates filter - member count: " . count($expiredMemberIds));
+                } else if ($filterValue == "2") {
+                    $memberLatestCerts = DB::table('member_certificates')
+                        ->select('member_id', DB::raw('MAX(expire_date) as latest_expire_date'))
+                        ->groupBy('member_id')
+                        ->get();
+
+                    $expiringMemberIds = [];
+                    foreach ($memberLatestCerts as $cert) {
+                        $expireDate = date('Y-m-d', strtotime($cert->latest_expire_date));
+                        $today = date('Y-m-d');
+                        $oneMonthLater = date('Y-m-d', strtotime("+1 month"));
+
+                        if ($expireDate >= $today && $expireDate <= $oneMonthLater) {
+                            $expiringMemberIds[] = $cert->member_id;
+                        }
+                    }
+                    $allScadIds = array_merge($allScadIds, $expiringMemberIds);
+                } else if ($filterValue == "3") {
+                    $scadIds = \App\Models\Member::whereNotIn('id', \App\Models\MemberCertificate::pluck('member_id'))
+                        ->where(function($query) {
+                            $query->where('is_archived', false)
+                                ->orWhereNull('is_archived');
+                        })
+                        ->where(function($query) {
+                            $query->where('is_deleted', false)
+                                ->orWhereNull('is_deleted');
+                        })
+                        ->pluck('id')
+                        ->toArray();
+                    $allScadIds = array_merge($allScadIds, $scadIds);
+                } else if ($filterValue == "4") {
+                    $memberLatestCerts = DB::table('member_certificates')
+                        ->select('member_id', DB::raw('MAX(expire_date) as latest_expire_date'))
+                        ->groupBy('member_id')
+                        ->get();
+
+                    $validMemberIds = [];
+                    foreach ($memberLatestCerts as $cert) {
+                        $expireDate = date('Y-m-d', strtotime($cert->latest_expire_date));
+                        $oneMonthLater = date('Y-m-d', strtotime("+1 month"));
+
+                        if ($expireDate > $oneMonthLater) {
+                            $validMemberIds[] = $cert->member_id;
+                        }
+                    }
+                    $allScadIds = array_merge($allScadIds, $validMemberIds);
+                }
+            }
+
+            // Remove duplicates
+            $allScadIds = array_unique($allScadIds);
+            Log::info("Course members - Total members after filterCertificateScadenza: " . count($allScadIds));
+
+            // Apply filter using the collected IDs
+            $datas = $datas->whereIn('member_id', $allScadIds);
+        }
+
+        if ($_GET["fromYear"] != "") {
+            $m_ids = \App\Models\Member::where('birth_date', '<', date("Y-m-d", strtotime("-" . $_GET["fromYear"] . " year", time())))
+                ->where(function($query) {
+                    $query->where('is_archived', false)
+                        ->orWhereNull('is_archived');
+                })
+                ->where(function($query) {
+                    $query->where('is_deleted', false)
+                        ->orWhereNull('is_deleted');
+                })->pluck('id');
+            $datas = $datas->whereIn('member_id', $m_ids);
+        }
+
+        if ($_GET["toYear"] != "") {
+            $m_ids = \App\Models\Member::where('birth_date', '>', date("Y-m-d", strtotime("-" . $_GET["toYear"] . " year", time())))
+                ->where(function($query) {
+                    $query->where('is_archived', false)
+                        ->orWhereNull('is_archived');
+                })
+                ->where(function($query) {
+                    $query->where('is_deleted', false)
+                        ->orWhereNull('is_deleted');
+                })->pluck('id');
+            $datas = $datas->whereIn('member_id', $m_ids);
+        }
+
+        if ($_GET["fromFromYear"] != "") {
+            $m_ids = \App\Models\Member::whereYear('birth_date', '>=', $_GET["fromFromYear"])
+                ->where(function($query) {
+                    $query->where('is_archived', false)
+                        ->orWhereNull('is_archived');
+                })
+                ->where(function($query) {
+                    $query->where('is_deleted', false)
+                        ->orWhereNull('is_deleted');
+                })->pluck('id');
+            $datas = $datas->whereIn('member_id', $m_ids);
+        }
+
+        if ($_GET["toToYear"] != "") {
+            $m_ids = \App\Models\Member::whereYear('birth_date', '<=', $_GET["toToYear"])
+                ->where(function($query) {
+                    $query->where('is_archived', false)
+                        ->orWhereNull('is_archived');
+                })
+                ->where(function($query) {
+                    $query->where('is_deleted', false)
+                        ->orWhereNull('is_deleted');
+                })->pluck('id');
+            $datas = $datas->whereIn('member_id', $m_ids);
+        }
+
+        if ($_GET["filterCards"] != "null") {
+            $cards = explode(",", $_GET["filterCards"]);
+            $card_ids = \App\Models\MemberCard::whereIn('card_id', $cards)->pluck('member_id');
+            $datas = $datas->whereIn('member_id', $card_ids);
+        }
+
+        $aRet = [];
+        if (isset($_GET["order"])) {
+            $column = '';
+            if ($_GET["order"][0]["column"] == 1)
+                $column = 'last_name';
+            if ($_GET["order"][0]["column"] == 2)
+                $column = 'first_name';
+            if ($_GET["order"][0]["column"] == 3)
+                $column = 'certificate_expire_date';
+            // if ($_GET["order"][0]["column"] == 4)
+            //     $column = 'gender';
+            if ($column != '')
+                $datas = $datas->orderBy($column, $_GET["order"][0]["dir"]);
+            else
+                $datas = $datas->orderBy('last_name', 'ASC')->orderBy('first_name', 'ASC');
+        } else
+            $datas = $datas->orderBy('last_name', 'ASC')->orderBy('first_name', 'ASC');
+
+        if ($_GET["filterStatus"] != "null") {
+            $status = explode(",", $_GET["filterStatus"]);
+            foreach ($status as $s) {
+                foreach ($datas->get() as $aaa) {
+
+                    $state = \App\Models\Member::findOrFail($aaa->member_id)->isActive();
+                    if ($state["status"] == $s)
+                        $aRet[] = $aaa;
+                }
+            }
+        } else
+            $aRet = $datas->get();
+
+        $ret = [];
+        foreach ($aRet as $idx => $r) {
+            $certificate = \App\Models\MemberCertificate::where('member_id', $r->member_id)
+                ->orderBy('expire_date', 'desc')
+                ->first();
+
+            $certificateInfo = "";
+            if ($certificate) {
+                // Format: status|expire_date|type
+                // Status: 0 = expired, 1 = expiring soon, 2 = valid
+                $today = new DateTime();
+                $expireDate = new DateTime($certificate->expire_date);
+                $oneMonthFromNow = (new DateTime())->modify('+1 month');
+
+                $status = "2"; // Default to valid
+                if ($expireDate < $today) {
+                    $status = "0"; // Expired
+                } elseif ($expireDate < $oneMonthFromNow) {
+                    $status = "1"; // Expiring soon
+                }
+
+                $certificateInfo = $status . "|" . date("d/m/Y", strtotime($certificate->expire_date)) . "|" . $certificate->type;
+            }
+
+            $status = 0;
+            $payed = 0;
+            $rates = \App\Models\Rate::where('member_course_id', $r->id)->where('date', '<', date("Y-m-d"))->get();
+            foreach($rates as $rate)
+            {
+                if ($rate->date > date("Y-m-d"))
+                {
+                    break;
+                }
+                $status = $rate->status;
+                if ($rate->status == 1)
+                    $payed += 1;
+            }
+            if ($status == 1 && $payed != sizeof($rates))
+                $status = 0;
+
+            $ret[] = array(
+                "column_0" => $idx + 1,
+                "column_1" => $r->last_name,
+                "column_2" => $r->first_name,
+                "column_3" => $certificateInfo,
+                "column_4" => $status,
+                "column_5" => $r->id,
+                "column_7" => $r->member_id,
+            );
+        }
+
+        if (isset($_GET["start"]))
+            $ret = array_slice($ret, $_GET["start"], $_GET["length"]);
+
+        return json_encode(array("data" => $ret, "recordsTotal" => sizeof($aRet), "recordsFiltered" => sizeof($aRet)));
+    });
+
     Route::get('/get_receipts', function () {
         $baseQuery = \App\Models\Receipt::select('receipts.id')
             ->leftJoin('members', 'receipts.member_id', '=', 'members.id');
@@ -1851,9 +2244,17 @@ Route::group(['middleware' => 'tenant'], function () {
         if (isset($_GET["search"]["value"]) && !empty($_GET["search"]["value"])) {
             $v = str_replace("'", "\'", stripcslashes($_GET["search"]["value"]));
             $member_ids = \App\Models\Member::where(function ($query) use ($v) {
-                $query->whereRaw("CONCAT(first_name, ' ', last_name) like '%" . $v . "%'")
-                    ->orWhereRaw("CONCAT(last_name, ' ', first_name) like '%" . $v . "%'");
-            })->pluck('id');
+                    $query->whereRaw("CONCAT(first_name, ' ', last_name) like '%" . $v . "%'")
+                        ->orWhereRaw("CONCAT(last_name, ' ', first_name) like '%" . $v . "%'");
+                })
+                ->where(function($query) {
+                    $query->where('is_archived', false)
+                        ->orWhereNull('is_archived');
+                })
+                ->where(function($query) {
+                    $query->where('is_deleted', false)
+                        ->orWhereNull('is_deleted');
+                })->pluck('id');
             $baseQuery = $baseQuery->whereIn('receipts.member_id', $member_ids);
         }
 

Some files were not shown because too many files changed in this diff