Преглед изворни кода

modificata sezione sms modulo comunicazione

ferrari пре 2 месеци
родитељ
комит
3c0ac61b0c

+ 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();
     }
 
     /**

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

@@ -3,198 +3,305 @@
 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 (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->sortField === $field) {
-            $this->sortAsc = ! $this->sortAsc;
+        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');
+        $this->validate($this->baseRules());
+    }
 
-        $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';
+    }
+
+    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';
     }
 
-    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';
+    }
+
+    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]);
     }
 }

+ 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;
+    }
+}

+ 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);
+    }
+}

+ 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');
+    }
+};

+ 9 - 2
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,6 +381,13 @@ body .btn.dropdown-toggle {
     background-color: #f5f8fa !important;
 }
 
+body textarea.form-control,
+body form textarea.form-control {
+    border-radius: 15px !important;
+    border: 1px solid #d3dce1 !important;
+    background-color: #f5f8fa !important;
+}
+
 body .form-check-input,
 body form .form-check-input {
     height: 20px !important;

+ 564 - 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>
 
-                        <div class="row mb-3">
+                        {{-- 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>
+
+                        {{-- 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 === false || $sendNow === 'false')
-                        <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 === false || $sendNow === 'false')
-                                        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,304 @@
                 </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()}_`;
+
+        let table = $('#tablesaw-350').DataTable();
+        if ( $.fn.DataTable.isDataTable('#tablesaw-350') ) {
+            table.destroy();
+        }
 
-            $('#tablesaw-350').DataTable({
-                processing: true,
-                thead: {
+        $('#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);
+            }
+        });
 
-            $(document).ready(function() {
-                $(document).on("click",".addData",function() {
-                    $(".title--section_addButton").trigger("click")
-                });
+        $('#tablesaw-350 thead tr th').addClass('col').css("background-color", "#f6f8fa");
+
+        $(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');
 
-        function updateSelectedCount() {
-            setTimeout(function() {
-                let count = $('input[wire\\:model="selectedRecipients"]:checked').length;
-                $('#selectedCount').text(count);
-            }, 100);
+
+        $('input[name="txtFromYear"]').val('');
+        $('input[name="txtToYear"]').val('');
+
+        $('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, 'desc']
+            ],
+            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');
+        }
+    };
+</script>
+
+{{-- END CKEditor --}}
+@endpush