FabioFratini 7 mesi fa
parent
commit
39e8616e59

+ 64 - 0
app/Events/ExportCompleted.php

@@ -0,0 +1,64 @@
+<?php
+
+// File: app/Events/ExportCompleted.php
+
+namespace App\Events;
+
+use Illuminate\Broadcasting\Channel;
+use Illuminate\Broadcasting\InteractsWithSockets;
+use Illuminate\Broadcasting\PresenceChannel;
+use Illuminate\Broadcasting\PrivateChannel;
+use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
+use Illuminate\Foundation\Events\Dispatchable;
+use Illuminate\Queue\SerializesModels;
+
+class ExportCompleted implements ShouldBroadcast
+{
+    use Dispatchable, InteractsWithSockets, SerializesModels;
+
+    public $userId;
+    public $filename;
+    public $emailAddress;
+    public $message;
+
+    /**
+     * Create a new event instance.
+     */
+    public function __construct($userId, $filename, $emailAddress)
+    {
+        $this->userId = $userId;
+        $this->filename = $filename;
+        $this->emailAddress = $emailAddress;
+        $this->message = 'Export completato! Controlla la tua email.';
+    }
+
+    /**
+     * Get the channels the event should broadcast on.
+     */
+    public function broadcastOn()
+    {
+        return new PrivateChannel('exports.' . $this->userId);
+    }
+
+    /**
+     * Get the data to broadcast.
+     */
+    public function broadcastWith()
+    {
+        return [
+            'type' => 'export_completed',
+            'message' => $this->message,
+            'filename' => $this->filename,
+            'email' => $this->emailAddress,
+            'timestamp' => now()->toISOString()
+        ];
+    }
+
+    /**
+     * The event's broadcast name.
+     */
+    public function broadcastAs()
+    {
+        return 'export.completed';
+    }
+}

+ 58 - 0
app/Events/ExportFailed.php

@@ -0,0 +1,58 @@
+<?php
+namespace App\Events;
+
+use Illuminate\Broadcasting\Channel;
+use Illuminate\Broadcasting\InteractsWithSockets;
+use Illuminate\Broadcasting\PresenceChannel;
+use Illuminate\Broadcasting\PrivateChannel;
+use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
+use Illuminate\Foundation\Events\Dispatchable;
+use Illuminate\Queue\SerializesModels;
+
+class ExportFailed implements ShouldBroadcast
+{
+    use Dispatchable, InteractsWithSockets, SerializesModels;
+
+    public $userId;
+    public $errorMessage;
+    public $message;
+
+    /**
+     * Create a new event instance.
+     */
+    public function __construct($userId, $errorMessage)
+    {
+        $this->userId = $userId;
+        $this->errorMessage = $errorMessage;
+        $this->message = 'Export fallito. Riprova o contatta il supporto.';
+    }
+
+    /**
+     * Get the channels the event should broadcast on.
+     */
+    public function broadcastOn()
+    {
+        return new PrivateChannel('exports.' . $this->userId);
+    }
+
+    /**
+     * Get the data to broadcast.
+     */
+    public function broadcastWith()
+    {
+        return [
+            'type' => 'export_failed',
+            'message' => $this->message,
+            'error' => $this->errorMessage,
+            'timestamp' => now()->toISOString()
+        ];
+    }
+
+    /**
+     * The event's broadcast name.
+     */
+    public function broadcastAs()
+    {
+        return 'export.failed';
+    }
+}

+ 192 - 6
app/Http/Livewire/Record.php

@@ -11,7 +11,9 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet;
 use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
 use Illuminate\Support\Facades\Storage;
 use Illuminate\Support\Facades\Log;
-
+use Illuminate\Support\Facades\Mail;
+use App\Mail\ExportNotification;
+use App\Jobs\ExportPrimaNota;
 
 class Record extends Component
 {
@@ -34,6 +36,21 @@ class Record extends Component
     public array $labels = [];
     public array $causals = [];
     public $members = array();
+    public $sendViaEmail = false;
+    public $exportEmailAddress = '';
+    public $exportEmailSubject = 'Prima Nota - Export';
+
+    protected $rules = [
+        'exportEmailAddress' => 'required_if:sendViaEmail,true|email',
+        'exportEmailSubject' => 'required_if:sendViaEmail,true|string|max:255',
+    ];
+
+    protected $messages = [
+        'exportEmailAddress.required_if' => 'L\'indirizzo email è obbligatorio quando si sceglie di inviare via email.',
+        'exportEmailAddress.email' => 'Inserisci un indirizzo email valido.',
+        'exportEmailSubject.required_if' => 'L\'oggetto dell\'email è obbligatorio.',
+        'exportEmailSubject.max' => 'L\'oggetto dell\'email non può superare i 255 caratteri.',
+    ];
 
     public function hydrate()
     {
@@ -51,6 +68,9 @@ class Record extends Component
         $this->exportFromDate = date("Y-m-d");
         $this->exportToDate = date("Y-m-d");
 
+        $this->exportEmailSubject = 'Prima Nota - Export del ' . date('d/m/Y');
+
+
         $this->getCausals(\App\Models\Causal::select('id', 'name')->where('parent_id', null)->get(), 0);
 
         $this->members = \App\Models\Member::select(['id', 'first_name', 'last_name', 'fiscal_code'])->orderBy('last_name')->orderBy('first_name')->get();
@@ -636,22 +656,166 @@ class Record extends Component
         $this->exportFromDate = $this->appliedFromDate;
         $this->exportToDate = $this->appliedToDate;
 
+        // Reset email options
+        $this->sendViaEmail = false;
+        $this->exportEmailAddress = $this->getPreferredEmail();
+        $this->updateEmailSubject();
+
         $this->emit('show-export-modal');
     }
 
     public function exportWithDateRange()
     {
+        // Real-time validation for email fields
+        if ($this->sendViaEmail) {
+            $this->validate([
+                'exportEmailAddress' => 'required|email',
+                'exportEmailSubject' => 'required|string|max:255',
+            ]);
+        }
+
         $this->isExporting = true;
 
-        $exportRecords = $this->generateExportData($this->exportFromDate, $this->exportToDate);
-        $exportTotals = $this->generateExportTotals($this->exportFromDate, $this->exportToDate);
+        try {
+            $exportRecords = $this->generateExportData($this->exportFromDate, $this->exportToDate);
+            $exportTotals = $this->generateExportTotals($this->exportFromDate, $this->exportToDate);
+
+            if ($this->sendViaEmail) {
+                // Dispatch job to background queue
+                $this->dispatchExportJob($exportRecords, $exportTotals);
+            } else {
+                // Direct download (synchronous)
+                return $this->exportWithData($exportRecords, $exportTotals);
+            }
+        } catch (\Illuminate\Validation\ValidationException $e) {
+            $this->isExporting = false;
+            throw $e; // Re-throw validation exceptions to show field errors
+        } catch (\Exception $e) {
+            $this->isExporting = false;
+            Log::error('Export error: ' . $e->getMessage());
+
+            if ($this->sendViaEmail) {
+                $this->emit('export-email-error', 'Errore durante l\'invio dell\'email: ' . $e->getMessage());
+            } else {
+                session()->flash('error', 'Errore durante l\'export: ' . $e->getMessage());
+            }
+        } finally {
+            $this->isExporting = false;
+            $this->emit('hide-export-modal');
+        }
+    }
+
+    private function getMemberName($memberId)
+    {
+        $member = \App\Models\Member::find($memberId);
+        return $member ? $member->last_name . ' ' . $member->first_name : 'Sconosciuto';
+    }
+
+    private function getCausalsNames($causalIds)
+    {
+        if (!is_array($causalIds)) {
+            return null;
+        }
+
+        $causals = \App\Models\Causal::whereIn('id', $causalIds)->pluck('name')->toArray();
+        return implode(', ', $causals);
+    }
+
+        public function updatedExportFromDate()
+    {
+        $this->updateEmailSubject();
+    }
+
+    public function updatedExportToDate()
+    {
+        $this->updateEmailSubject();
+    }
+
+    public function updatedSendViaEmail($value)
+    {
+        if ($value && empty($this->exportEmailAddress)) {
+            $this->exportEmailAddress = $this->getPreferredEmail();
+        }
+    }
+
+    public function resetEmailForm()
+    {
+        $this->sendViaEmail = false;
+        $this->exportEmailAddress = $this->getPreferredEmail();
+        $this->updateEmailSubject();
+    }
+
+    private function updateEmailSubject()
+    {
+        if (!empty($this->exportFromDate) && !empty($this->exportToDate)) {
+            $fromFormatted = date('d/m/Y', strtotime($this->exportFromDate));
+            $toFormatted = date('d/m/Y', strtotime($this->exportToDate));
 
-        $result = $this->exportWithData($exportRecords, $exportTotals);
+            if ($this->exportFromDate === $this->exportToDate) {
+                $this->exportEmailSubject = "Prima Nota - Export del {$fromFormatted}";
+            } else {
+                $this->exportEmailSubject = "Prima Nota - Export dal {$fromFormatted} al {$toFormatted}";
+            }
+        }
+    }
 
-        $this->isExporting = false;
 
-        $this->emit('hide-export-modal');
+    /**
+     * Dispatch export job to queue
+     */
+    private function dispatchExportJob($exportRecords, $exportTotals)
+    {
+        try {
+            // Prepare filter descriptions for the job
+            $filterDescriptions = [
+                'member' => $this->filterMember ? $this->getMemberName($this->filterMember) : null,
+                'causals' => $this->filterCausals ? $this->getCausalsNames($this->filterCausals) : null,
+            ];
+
+            $paymentsArray = $this->payments->map(function ($payment) {
+                return [
+                    'id' => $payment->id,
+                    'name' => $payment->name,
+                    'type' => $payment->type
+                ];
+            })->toArray();
+
+            // Dispatch job to background queue
+            ExportPrimaNota::dispatch(
+                $exportRecords,
+                $exportTotals,
+                $this->exportEmailAddress,
+                $this->exportEmailSubject,
+                [
+                    'from' => date('d/m/Y', strtotime($this->exportFromDate)),
+                    'to' => date('d/m/Y', strtotime($this->exportToDate))
+                ],
+                auth()->id(),
+                $paymentsArray,
+                $filterDescriptions
+            );
+
+            $this->emit('export-email-queued');
+            session()->flash('success', 'Export in corso! Riceverai l\'email a breve alla casella: ' . $this->exportEmailAddress);
+
+            Log::info('Export job dispatched', [
+                'user_id' => auth()->id(),
+                'email' => $this->exportEmailAddress,
+                'date_range' => [$this->exportFromDate, $this->exportToDate],
+                'total_records' => count($exportRecords)
+            ]);
+        } catch (\Exception $e) {
+            Log::error('Failed to dispatch export job', [
+                'user_id' => auth()->id(),
+                'email' => $this->exportEmailAddress,
+                'error' => $e->getMessage()
+            ]);
+
+            throw new \Exception('Errore nell\'avvio dell\'export: ' . $e->getMessage());
+        }
     }
+
+
     function export()
     {
         $exportRecords = $this->generateExportData($this->appliedFromDate, $this->appliedToDate);
@@ -938,4 +1102,26 @@ class Record extends Component
             return response()->download($localPath)->deleteFileAfterSend();
         }
     }
+
+    private function getPreferredEmail()
+    {
+        // Try multiple sources in order of preference
+        $email = auth()->user()->email ?? null;
+
+        if (empty($email)) {
+            $email = session('user_email', null);
+        }
+
+        if (empty($email)) {
+            $member = \App\Models\Member::where('user_id', auth()->id())->first();
+            $email = $member ? $member->email : null;
+        }
+
+        if (empty($email)) {
+            // Get from user input or company default
+            $email = config('mail.default_recipient', '');
+        }
+
+        return $email;
+    }
 }

+ 378 - 0
app/Jobs/ExportPrimaNota.php

@@ -0,0 +1,378 @@
+<?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 Illuminate\Support\Facades\Mail;
+use Illuminate\Support\Facades\Log;
+use App\Mail\ExportNotification;
+use App\Events\ExportCompleted;
+use App\Events\ExportFailed;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+
+class ExportPrimaNota implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    public $timeout = 600;
+    public $tries = 3;
+    public $maxExceptions = 3;
+
+    protected $exportData;
+    protected $exportTotals;
+    protected $emailAddress;
+    protected $emailSubject;
+    protected $dateRange;
+    protected $userId;
+    protected $payments;
+    protected $filters;
+
+    /**
+     * Create a new job instance.
+     */
+    public function __construct($exportData, $exportTotals, $emailAddress, $emailSubject, $dateRange, $userId, $payments, $filters = [])
+    {
+        $this->exportData = $exportData;
+        $this->exportTotals = $exportTotals;
+        $this->emailAddress = $emailAddress;
+        $this->emailSubject = $emailSubject;
+        $this->dateRange = $dateRange;
+        $this->userId = $userId;
+        $this->payments = $payments;
+        $this->filters = $filters;
+
+        $this->onQueue('exports');
+    }
+
+    /**
+     * Execute the job.
+     */
+    public function handle()
+    {
+        try {
+            Log::info('Starting background export', [
+                'user_id' => $this->userId,
+                'email' => $this->emailAddress,
+                'date_range' => $this->dateRange,
+                'total_records' => count($this->exportData)
+            ]);
+
+            ini_set('memory_limit', '1024M');
+
+            $filename = 'prima_nota_' . date("Ymd_His") . '_' . $this->userId . '.xlsx';
+            $tempPath = sys_get_temp_dir() . '/' . $filename;
+
+            $this->createExcelFile($tempPath);
+
+            if (!file_exists($tempPath) || filesize($tempPath) === 0) {
+                throw new \Exception('Excel file creation failed');
+            }
+
+            $fileSize = filesize($tempPath);
+            $maxSize = 25 * 1024 * 1024;
+
+            if ($fileSize > $maxSize) {
+                throw new \Exception('File too large for email attachment (' . round($fileSize / 1024 / 1024, 2) . 'MB > 25MB)');
+            }
+
+            $user = \App\Models\User::find($this->userId);
+
+            $emailData = [
+                'subject' => $this->emailSubject,
+                'from_date' => $this->dateRange['from'],
+                'to_date' => $this->dateRange['to'],
+                'total_records' => count($this->exportData),
+                'user_name' => $user ? $user->name : 'Utente',
+                'generated_at' => now()->format('d/m/Y H:i:s'),
+                'filters_applied' => $this->getFiltersDescription(),
+                'file_size' => round($fileSize / 1024 / 1024, 2) . ' MB'
+            ];
+
+            Mail::to($this->emailAddress)->send(new ExportNotification($emailData, $tempPath, $filename));
+
+            if (class_exists(ExportCompleted::class)) {
+                broadcast(new ExportCompleted($this->userId, $filename, $this->emailAddress));
+            }
+
+            Log::info('Background export completed successfully', [
+                'user_id' => $this->userId,
+                'email' => $this->emailAddress,
+                'filename' => $filename,
+                'file_size' => $fileSize,
+                'processing_time' => microtime(true) - LARAVEL_START ?? 0
+            ]);
+
+        } catch (\Exception $e) {
+            Log::error('Background export failed', [
+                'user_id' => $this->userId,
+                'email' => $this->emailAddress,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+
+            if (class_exists(ExportFailed::class)) {
+                broadcast(new ExportFailed($this->userId, $e->getMessage()));
+            }
+
+            throw $e;
+        } finally {
+            if (isset($tempPath) && file_exists($tempPath)) {
+                unlink($tempPath);
+            }
+
+            gc_collect_cycles();
+        }
+    }
+
+    /**
+     * Handle a job failure.
+     */
+    public function failed(\Throwable $exception)
+    {
+        Log::error('Export job failed permanently', [
+            'user_id' => $this->userId,
+            'email' => $this->emailAddress,
+            'attempts' => $this->attempts(),
+            'error' => $exception->getMessage()
+        ]);
+
+        try {
+            Mail::raw(
+                "Il tuo export della Prima Nota non è riuscito dopo {$this->tries} tentativi.\n\n" .
+                "Errore: {$exception->getMessage()}\n\n" .
+                "Contatta il supporto tecnico se il problema persiste.",
+                function ($message) {
+                    $message->to($this->emailAddress)
+                           ->subject('Export Prima Nota - Errore');
+                }
+            );
+        } catch (\Exception $e) {
+            Log::error('Failed to send failure notification email', [
+                'user_id' => $this->userId,
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+
+    /**
+     * Create Excel file with export data
+     */
+    private function createExcelFile($filePath)
+    {
+        $letters = array('F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'AA');
+
+        $spreadsheet = new Spreadsheet();
+        $activeWorksheet = $spreadsheet->getActiveSheet();
+
+        $activeWorksheet->setTitle('Prima Nota');
+
+        $spreadsheet->getProperties()
+            ->setCreator('Prima Nota System')
+            ->setLastModifiedBy('Sistema')
+            ->setTitle('Prima Nota Export')
+            ->setSubject('Export Prima Nota')
+            ->setDescription('Export dei dati Prima Nota dal ' . $this->dateRange['from'] . ' al ' . $this->dateRange['to']);
+
+        $activeWorksheet->setCellValue('A1', "Data");
+        $activeWorksheet->setCellValue('B1', "Causale");
+        $activeWorksheet->setCellValue('C1', "Dettaglio Causale");
+        $activeWorksheet->setCellValue('D1', "Nominativo");
+        $activeWorksheet->setCellValue('E1', "Stato");
+
+        $idx = 0;
+        foreach ($this->payments as $p) {
+            if ($idx >= count($letters)) break;
+
+            $activeWorksheet->setCellValue($letters[$idx] . '1', $p['name']);
+            $activeWorksheet->mergeCells($letters[$idx] . '1:' . $letters[$idx + 1] . '1');
+            $idx += 2;
+        }
+
+        $activeWorksheet->setCellValue('A2', "");
+        $activeWorksheet->setCellValue('B2', "");
+        $activeWorksheet->setCellValue('C2', "");
+        $activeWorksheet->setCellValue('D2', "");
+        $activeWorksheet->setCellValue('E2', "");
+
+        $idx = 0;
+        foreach ($this->payments as $p) {
+            if ($idx >= count($letters) - 1) break;
+
+            if ($p['type'] == 'ALL') {
+                $activeWorksheet->setCellValue($letters[$idx] . '2', "Entrate");
+                $idx++;
+                $activeWorksheet->setCellValue($letters[$idx] . '2', "Uscite");
+                $idx++;
+            } elseif ($p['type'] == 'IN') {
+                $activeWorksheet->setCellValue($letters[$idx] . '2', "Entrate");
+                $idx++;
+                $activeWorksheet->setCellValue($letters[$idx] . '2', "");
+                $idx++;
+            } elseif ($p['type'] == 'OUT') {
+                $activeWorksheet->setCellValue($letters[$idx] . '2', "");
+                $idx++;
+                $activeWorksheet->setCellValue($letters[$idx] . '2', "Uscite");
+                $idx++;
+            }
+        }
+
+        $activeWorksheet->getStyle('A1:' . $letters[min(count($letters) - 1, count($this->payments) * 2 + 4)] . '2')
+                       ->getFont()->setBold(true);
+
+        $activeWorksheet->getStyle('A1:' . $letters[min(count($letters) - 1, count($this->payments) * 2 + 4)] . '1')
+                       ->getFill()
+                       ->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
+                       ->getStartColor()->setARGB('FF0C6197');
+
+        $activeWorksheet->getStyle('A1:' . $letters[min(count($letters) - 1, count($this->payments) * 2 + 4)] . '1')
+                       ->getFont()->getColor()->setARGB('FFFFFFFF');
+
+        $count = 3;
+        $batchSize = 500;
+        $processed = 0;
+
+        foreach ($this->exportData as $causal => $record) {
+            $parts = explode("§", $causal);
+            $d = $parts[0] ?? '';
+            $c = $parts[1] ?? '';
+            $j = $parts[2] ?? '';
+            $det = $parts[3] ?? '';
+            $deleted = $parts[4] ?? '';
+
+            $detailParts = explode('|', $det);
+            $exportDetail = count($detailParts) > 1 ? implode(', ', array_slice($detailParts, 1)) : $det;
+
+            $activeWorksheet->setCellValue('A' . $count, !empty($d) ? date("d/m/Y", strtotime($d)) : '');
+            $activeWorksheet->setCellValue('B' . $count, $c);
+            $activeWorksheet->setCellValue('C' . $count, $exportDetail);
+            $activeWorksheet->setCellValue('D' . $count, $j);
+
+            $stato = ($deleted === 'DELETED') ? 'ANNULLATA' : '';
+            $activeWorksheet->setCellValue('E' . $count, $stato);
+
+            if ($stato === 'ANNULLATA') {
+                $activeWorksheet->getStyle('E' . $count)->getFont()->getColor()->setARGB('FFFF0000');
+            }
+
+            $idx = 0;
+            foreach ($this->payments as $p) {
+                if ($idx >= count($letters) - 1) break;
+
+                if (isset($record[$p['name']])) {
+                    $inValue = isset($record[$p['name']]["IN"]) ? $this->formatPrice($record[$p['name']]["IN"]) : "";
+                    $outValue = isset($record[$p['name']]["OUT"]) ? $this->formatPrice($record[$p['name']]["OUT"]) : "";
+
+                    $activeWorksheet->setCellValue($letters[$idx] . $count, $inValue);
+                    $idx++;
+                    $activeWorksheet->setCellValue($letters[$idx] . $count, $outValue);
+                    $idx++;
+                } else {
+                    $activeWorksheet->setCellValue($letters[$idx] . $count, "");
+                    $idx++;
+                    $activeWorksheet->setCellValue($letters[$idx] . $count, "");
+                    $idx++;
+                }
+            }
+
+            $count++;
+            $processed++;
+
+            if ($processed % $batchSize === 0) {
+                gc_collect_cycles();
+            }
+        }
+
+        $count++;
+        $activeWorksheet->setCellValue('A' . $count, 'TOTALE');
+        $activeWorksheet->setCellValue('B' . $count, '');
+        $activeWorksheet->setCellValue('C' . $count, '');
+        $activeWorksheet->setCellValue('D' . $count, '');
+        $activeWorksheet->setCellValue('E' . $count, '');
+
+        $idx = 0;
+        foreach ($this->payments as $p) {
+            if ($idx >= count($letters) - 1) break;
+
+            if (isset($this->exportTotals[$p['name']])) {
+                if ($p['type'] == 'ALL') {
+                    $activeWorksheet->setCellValue($letters[$idx] . $count, $this->formatPrice($this->exportTotals[$p['name']]["IN"] ?? 0));
+                    $idx++;
+                    $activeWorksheet->setCellValue($letters[$idx] . $count, $this->formatPrice($this->exportTotals[$p['name']]["OUT"] ?? 0));
+                    $idx++;
+                } elseif ($p['type'] == 'IN') {
+                    $activeWorksheet->setCellValue($letters[$idx] . $count, $this->formatPrice($this->exportTotals[$p['name']]["IN"] ?? 0));
+                    $idx++;
+                    $activeWorksheet->setCellValue($letters[$idx] . $count, "");
+                    $idx++;
+                } elseif ($p['type'] == 'OUT') {
+                    $activeWorksheet->setCellValue($letters[$idx] . $count, "");
+                    $idx++;
+                    $activeWorksheet->setCellValue($letters[$idx] . $count, $this->formatPrice($this->exportTotals[$p['name']]["OUT"] ?? 0));
+                    $idx++;
+                }
+            } else {
+                $activeWorksheet->setCellValue($letters[$idx] . $count, "0,00");
+                $idx++;
+                $activeWorksheet->setCellValue($letters[$idx] . $count, "0,00");
+                $idx++;
+            }
+        }
+
+        $activeWorksheet->getStyle('A' . $count . ':' . $letters[min(count($letters) - 1, count($this->payments) * 2 + 4)] . $count)
+                       ->getFont()->setBold(true);
+
+        $activeWorksheet->getStyle('A' . $count . ':' . $letters[min(count($letters) - 1, count($this->payments) * 2 + 4)] . $count)
+                       ->getFill()
+                       ->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
+                       ->getStartColor()->setARGB('FFF0F0F0');
+
+        $activeWorksheet->getColumnDimension('A')->setWidth(15);
+        $activeWorksheet->getColumnDimension('B')->setWidth(25);
+        $activeWorksheet->getColumnDimension('C')->setWidth(30);
+        $activeWorksheet->getColumnDimension('D')->setWidth(25);
+        $activeWorksheet->getColumnDimension('E')->setWidth(15);
+
+        foreach ($letters as $l) {
+            $activeWorksheet->getColumnDimension($l)->setWidth(15);
+        }
+
+        $activeWorksheet->freezePane('A3');
+
+        $writer = new Xlsx($spreadsheet);
+        $writer->save($filePath);
+
+        unset($spreadsheet, $activeWorksheet, $writer);
+        gc_collect_cycles();
+    }
+
+    /**
+     * Format price for display
+     */
+    private function formatPrice($amount)
+    {
+        return number_format($amount, 2, ',', '.');
+    }
+
+    /**
+     * Get description of applied filters
+     */
+    private function getFiltersDescription()
+    {
+        $descriptions = [];
+
+        if (!empty($this->filters['member'])) {
+            $descriptions[] = "Utente: {$this->filters['member']}";
+        }
+
+        if (!empty($this->filters['causals'])) {
+            $descriptions[] = "Causali: " . (is_array($this->filters['causals']) ? implode(', ', $this->filters['causals']) : $this->filters['causals']);
+        }
+
+        return empty($descriptions) ? 'Nessun filtro applicato' : implode(' | ', $descriptions);
+    }
+}

+ 66 - 0
app/Mail/ExportNotification.php

@@ -0,0 +1,66 @@
+<?php
+
+namespace App\Mail;
+
+use Illuminate\Bus\Queueable;
+use Illuminate\Mail\Mailable;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\Log;
+
+class ExportNotification extends Mailable
+{
+    use Queueable, SerializesModels;
+
+    public $emailData;
+    private $filePath;
+    private $fileName;
+
+    /**
+     * Create a new message instance.
+     */
+    public function __construct($emailData, $filePath, $fileName)
+    {
+        $this->emailData = $emailData;
+        $this->filePath = $filePath;
+        $this->fileName = $fileName;
+    }
+
+    /**
+     * Build the message.
+     */
+    public function build()
+    {
+        try {
+            $email = $this->subject($this->emailData['subject'])
+                        ->view('emails.export-notification')
+                        ->with('data', $this->emailData);
+
+            if (file_exists($this->filePath)) {
+                $email->attach($this->filePath, [
+                    'as' => $this->fileName,
+                    'mime' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+                ]);
+
+                Log::info('Email attachment added', [
+                    'file_path' => $this->filePath,
+                    'file_name' => $this->fileName,
+                    'file_size' => filesize($this->filePath)
+                ]);
+            } else {
+                Log::warning('Export file not found for email attachment', [
+                    'file_path' => $this->filePath
+                ]);
+            }
+
+            return $email;
+
+        } catch (\Exception $e) {
+            Log::error('Error building export notification email', [
+                'error' => $e->getMessage(),
+                'file_path' => $this->filePath,
+                'file_name' => $this->fileName
+            ]);
+            throw $e;
+        }
+    }
+}

+ 36 - 0
database/migrations/2025_06_12_080802_create_jobs_table.php

@@ -0,0 +1,36 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('jobs', function (Blueprint $table) {
+            $table->bigIncrements('id');
+            $table->string('queue')->index();
+            $table->longText('payload');
+            $table->unsignedTinyInteger('attempts');
+            $table->unsignedInteger('reserved_at')->nullable();
+            $table->unsignedInteger('available_at');
+            $table->unsignedInteger('created_at');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('jobs');
+    }
+};

+ 206 - 0
resources/views/emails/export-notification.blade.php

@@ -0,0 +1,206 @@
+<!DOCTYPE html>
+<html lang="it">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Prima Nota - Export</title>
+    <style>
+        body {
+            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+            line-height: 1.6;
+            color: #333;
+            background-color: #f8f9fa;
+            margin: 0;
+            padding: 0;
+        }
+        .container {
+            max-width: 600px;
+            margin: 0 auto;
+            background-color: #ffffff;
+            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+        }
+        .header {
+            background: linear-gradient(135deg, #0C6197 0%, #084c6b 100%);
+            color: white;
+            padding: 30px 20px;
+            text-align: center;
+        }
+        .header h1 {
+            margin: 0;
+            font-size: 24px;
+            font-weight: 600;
+        }
+        .header .subtitle {
+            margin: 10px 0 0 0;
+            font-size: 14px;
+            opacity: 0.9;
+        }
+        .content {
+            padding: 30px 20px;
+        }
+        .greeting {
+            font-size: 16px;
+            margin-bottom: 20px;
+        }
+        .export-details {
+            background-color: #f8f9fa;
+            border-left: 4px solid #0C6197;
+            padding: 20px;
+            margin: 20px 0;
+            border-radius: 0 6px 6px 0;
+        }
+        .export-details h3 {
+            margin: 0 0 15px 0;
+            color: #0C6197;
+            font-size: 18px;
+        }
+        .detail-item {
+            display: flex;
+            justify-content: space-between;
+            margin-bottom: 10px;
+            padding: 8px 0;
+            border-bottom: 1px solid #e9ecef;
+        }
+        .detail-item:last-child {
+            border-bottom: none;
+            margin-bottom: 0;
+        }
+        .detail-label {
+            font-weight: 600;
+            color: #495057;
+        }
+        .detail-value {
+            color: #0C6197;
+            font-weight: 500;
+        }
+        .attachment-info {
+            background-color: #e8f4f8;
+            border: 1px solid #b8daff;
+            border-radius: 6px;
+            padding: 15px;
+            margin: 20px 0;
+            text-align: center;
+        }
+        .attachment-icon {
+            font-size: 24px;
+            color: #0C6197;
+            margin-bottom: 10px;
+        }
+        .footer {
+            background-color: #f8f9fa;
+            padding: 20px;
+            text-align: center;
+            border-top: 1px solid #dee2e6;
+        }
+        .footer p {
+            margin: 5px 0;
+            font-size: 12px;
+            color: #6c757d;
+        }
+        .btn {
+            display: inline-block;
+            padding: 12px 24px;
+            background-color: #0C6197;
+            color: white;
+            text-decoration: none;
+            border-radius: 6px;
+            font-weight: 500;
+            margin: 10px 0;
+        }
+        .btn:hover {
+            background-color: #084c6b;
+        }
+        @media (max-width: 600px) {
+            .container {
+                margin: 0;
+                border-radius: 0;
+            }
+            .content {
+                padding: 20px 15px;
+            }
+            .detail-item {
+                flex-direction: column;
+                gap: 5px;
+            }
+        }
+    </style>
+</head>
+<body>
+    <div class="container">
+        <div class="header">
+            <h1>📊Export Prima Nota</h1>
+            <p class="subtitle">I tuoi dati sono pronti per il download</p>
+        </div>
+
+        <div class="content">
+            <div class="greeting">
+                Ciao {{ $data['user_name'] ?? 'Utente' }},
+            </div>
+
+            <p>
+                Il tuo export della Prima Nota è stato generato con successo. Troverai il file Excel allegato a questa email con tutti i dati richiesti.
+            </p>
+
+            <div class="export-details">
+                <h3>📋 Dettagli Export</h3>
+
+                <div class="detail-item">
+                    <span class="detail-label">📅 Periodo:</span>
+                    <span class="detail-value">{{ $data['from_date'] ?? 'N/A' }} - {{ $data['to_date'] ?? 'N/A' }}</span>
+                </div>
+
+                <div class="detail-item">
+                    <span class="detail-label">📊 Record totali:</span>
+                    <span class="detail-value">{{ $data['total_records'] ?? 0 }}</span>
+                </div>
+
+                <div class="detail-item">
+                    <span class="detail-label">🕒 Generato il:</span>
+                    <span class="detail-value">{{ $data['generated_at'] ?? now()->format('d/m/Y H:i:s') }}</span>
+                </div>
+
+                @if(isset($data['filters_applied']))
+                <div class="detail-item">
+                    <span class="detail-label">🔍 Filtri applicati:</span>
+                    <span class="detail-value">{{ $data['filters_applied'] }}</span>
+                </div>
+                @endif
+
+                @if(isset($data['file_size']))
+                <div class="detail-item">
+                    <span class="detail-label">📁 Dimensione file:</span>
+                    <span class="detail-value">{{ $data['file_size'] }}</span>
+                </div>
+                @endif
+            </div>
+
+            <div class="attachment-info">
+                <div class="attachment-icon">📎</div>
+                <strong>File allegato:</strong> Prima Nota Export<br>
+                <small>Il file Excel contiene tutti i dati del periodo selezionato con i filtri applicati.</small>
+            </div>
+
+            <p>
+                <strong>Cosa include questo export:</strong>
+            </p>
+            <ul>
+                <li>📋 Tutte le transazioni del periodo specificato</li>
+                <li>💰 Dettaglio entrate e uscite per metodo di pagamento</li>
+                <li>📊 Totali riassuntivi</li>
+                <li>🏷️ Causali e dettagli delle operazioni</li>
+                <li>👥 Nominativi associati alle transazioni</li>
+            </ul>
+
+            <p>
+                Se hai bisogno di assistenza o hai domande sui dati esportati, non esitare a contattare il supporto.
+            </p>
+        </div>
+
+        <div class="footer">
+            <p>Questa email è stata generata automaticamente dal sistema.</p>
+            <p>Per qualsiasi problema tecnico, contatta il supporto.</p>
+            <p><small>Generato: {{ now()->format('d/m/Y H:i:s') }}</small></p>
+        </div>
+    </div>
+</body>
+</html>

+ 102 - 17
resources/views/emails/receipt.blade.php

@@ -1,17 +1,102 @@
-Gentile cliente,
-<br><br>
-si allega la ricevuta del pagamento effettuato.
-<br><br>
-<br><br>
-Cordiali saluti,
-<br><br>
-<span style="color:blue">
-La segreteria S.S.D. IAO TEAM a r.l.<br>
-contatti: 06 60674794 - sede: via di Villa Grazioli snc - 00046 Grottaferrata (RM) - c.f. 92015570580 - P. IVA 12576361005<br>
-<small>
-Avvertenze ai sensi del "Regolamento generale sulla protezione dei dati o "GDPR" (General Data Protection Regulation)<br>
-le informazioni contenute in questo messaggio di posta elettronica e/o nel/i file/s allegato/i sono da considerarsi strettamente riservate. Il loro utilizzo è consentito esclusivamente al destinatario sopra indicato. Qualora riceveste questo messaggio senza essere il destinatario Vi preghiamo cortesemente di informarci con apposito messaggio e procedere alla distruzione del messaggio stesso, cancellandolo dal Vostro sistema; costituisce comportamento contrario ai principi dettati dal "GDPR" (General Data Protection Regulation) il trattenere il messaggio stesso, divulgarlo anche in parte, distribuirlo ad altri soggetti, ovvero copiarlo.<br>
-Grazie.
-</small>
-</span>
-<br><br>
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Invio Ricevuta</title>
+    <style>
+        body {
+            font-family: Helvetica, Arial, sans-serif;
+            font-size: 14px;
+            line-height: 1.6;
+        }
+        .header {
+            margin-bottom: 20px;
+        }
+        .company-info {
+            color: blue;
+            margin-top: 30px;
+        }
+        .privacy-notice {
+            font-size: 12px;
+            margin-top: 10px;
+        }
+    </style>
+</head>
+<body>
+    @php
+        $azienda = App\Models\Azienda::first();
+    @endphp
+
+    <div class="header">
+        Gentile cliente,
+    </div>
+
+    <p>
+        Si allega la ricevuta del pagamento effettuato.
+    </p>
+
+    <br><br>
+
+    <p>Cordiali saluti,</p>
+
+    <div class="company-info">
+        @if($azienda)
+            <strong>
+                @if($azienda->ragione_sociale)
+                    La segreteria {{$azienda->ragione_sociale}}
+                @elseif($azienda->nome_associazione)
+                    La segreteria {{$azienda->nome_associazione}}
+                @else
+                    La segreteria
+                @endif
+            </strong><br>
+
+            @php
+                $contactInfo = [];
+                if($azienda->telefono) $contactInfo[] = $azienda->telefono;
+                if($azienda->email) $contactInfo[] = $azienda->email;
+            @endphp
+
+            @if(!empty($contactInfo))
+                contatti: {{ implode(' - ', $contactInfo) }}
+            @endif
+
+            @php
+                $addressParts = array_filter([
+                    $azienda->sede_legale_indirizzo,
+                    $azienda->sede_legale_cap,
+                    $azienda->sede_legale_comune,
+                    $azienda->sede_legale_provincia ? '(' . $azienda->sede_legale_provincia . ')' : null
+                ]);
+            @endphp
+
+            @if(!empty($addressParts))
+                - sede: {{ implode(' ', $addressParts) }}
+            @endif
+
+            @if($azienda->codice_fiscale)
+                - c.f. {{$azienda->codice_fiscale}}
+            @endif
+
+            @if($azienda->partita_iva)
+                - P. IVA {{$azienda->partita_iva}}
+            @endif
+
+        @else
+            <strong>La segreteria</strong><br>
+            <span style="color: red; font-weight: bold;">
+                ATTENZIONE: Configurare i dati aziendali nel sistema
+            </span>
+        @endif
+
+        <div class="privacy-notice">
+            <small>
+                Avvertenze ai sensi del "Regolamento generale sulla protezione dei dati o "GDPR" (General Data Protection Regulation)<br>
+                Le informazioni contenute in questo messaggio di posta elettronica e/o nel/i file/s allegato/i sono da considerarsi strettamente riservate.
+                Il loro utilizzo è consentito esclusivamente al destinatario sopra indicato.
+                Qualora riceveste questo messaggio senza essere il destinatario Vi preghiamo cortesemente di informarci con apposito messaggio e procedere alla distruzione del messaggio stesso, cancellandolo dal Vostro sistema;
+                costituisce comportamento contrario ai principi dettati dal "GDPR" (General Data Protection Regulation) il trattenere il messaggio stesso, divulgarlo anche in parte, distribuirlo ad altri soggetti, ovvero copiarlo.<br>
+                Grazie.
+            </small>
+        </div>
+    </div>
+</body>

+ 113 - 21
resources/views/emails/receipt_delete.blade.php

@@ -1,21 +1,113 @@
-Gentile cliente,
-<br><br>
-la ricevuta di pagamento {{$mailData["number"]}}, emessa il {{$mailData["date"]}}, è stata annullata.
-<br><br>
-Ci scusiamo per l’eventuale inconveniente e rimaniamo a disposizione per qualsiasi chiarimento.
-<br><br>
-Grazie per la sua comprensione e collaborazione.
-<br><br>
-<br><br>
-Cordiali saluti,
-<br><br>
-<span style="color:blue">
-La segreteria S.S.D. IAO TEAM a r.l.<br>
-contatti: 06 60674794 - sede: via di Villa Grazioli snc - 00046 Grottaferrata (RM) - c.f. 92015570580 - P. IVA 12576361005<br>
-<small>
-Avvertenze ai sensi del "Regolamento generale sulla protezione dei dati o "GDPR" (General Data Protection Regulation)<br>
-le informazioni contenute in questo messaggio di posta elettronica e/o nel/i file/s allegato/i sono da considerarsi strettamente riservate. Il loro utilizzo è consentito esclusivamente al destinatario sopra indicato. Qualora riceveste questo messaggio senza essere il destinatario Vi preghiamo cortesemente di informarci con apposito messaggio e procedere alla distruzione del messaggio stesso, cancellandolo dal Vostro sistema; costituisce comportamento contrario ai principi dettati dal "GDPR" (General Data Protection Regulation) il trattenere il messaggio stesso, divulgarlo anche in parte, distribuirlo ad altri soggetti, ovvero copiarlo.<br>
-Grazie.
-</small>
-</span>
-<br><br>
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Ricevuta Annullata</title>
+    <style>
+        body {
+            font-family: Helvetica, Arial, sans-serif;
+            font-size: 14px;
+            line-height: 1.6;
+        }
+        .header {
+            margin-bottom: 20px;
+        }
+        .company-info {
+            color: blue;
+            margin-top: 30px;
+        }
+        .privacy-notice {
+            font-size: 12px;
+            margin-top: 10px;
+        }
+    </style>
+</head>
+<body>
+    @php
+        $azienda = App\Models\Azienda::first();
+    @endphp
+
+    <div class="header">
+        Gentile cliente,
+    </div>
+
+    <p>
+        La ricevuta di pagamento <strong>{{$receipt->number . "/" . $receipt->year}}</strong>,
+        emessa il <strong>{{date("d/m/Y", strtotime($receipt->created_at))}}</strong>,
+        è stata annullata.
+    </p>
+
+    <p>
+        Ci scusiamo per l'eventuale inconveniente e rimaniamo a disposizione per qualsiasi chiarimento.
+    </p>
+
+    <p>
+        Grazie per la sua comprensione e collaborazione.
+    </p>
+
+    <br><br>
+
+    <p>Cordiali saluti,</p>
+
+    <div class="company-info">
+        @if($azienda)
+            <strong>
+                @if($azienda->ragione_sociale)
+                    La segreteria {{$azienda->ragione_sociale}}
+                @elseif($azienda->nome_associazione)
+                    La segreteria {{$azienda->nome_associazione}}
+                @else
+                    La segreteria
+                @endif
+            </strong><br>
+
+            @php
+                $contactInfo = [];
+                if($azienda->telefono) $contactInfo[] = $azienda->telefono;
+                if($azienda->email) $contactInfo[] = $azienda->email;
+            @endphp
+
+            @if(!empty($contactInfo))
+                contatti: {{ implode(' - ', $contactInfo) }}
+            @endif
+
+            @php
+                $addressParts = array_filter([
+                    $azienda->sede_legale_indirizzo,
+                    $azienda->sede_legale_cap,
+                    $azienda->sede_legale_comune,
+                    $azienda->sede_legale_provincia ? '(' . $azienda->sede_legale_provincia . ')' : null
+                ]);
+            @endphp
+
+            @if(!empty($addressParts))
+                - sede: {{ implode(' ', $addressParts) }}
+            @endif
+
+            @if($azienda->codice_fiscale)
+                - c.f. {{$azienda->codice_fiscale}}
+            @endif
+
+            @if($azienda->partita_iva)
+                - P. IVA {{$azienda->partita_iva}}
+            @endif
+
+        @else
+            <strong>La segreteria</strong><br>
+            <span style="color: red; font-weight: bold;">
+                ATTENZIONE: Configurare i dati aziendali nel sistema
+            </span>
+        @endif
+
+        <div class="privacy-notice">
+            <small>
+                Avvertenze ai sensi del "Regolamento generale sulla protezione dei dati o "GDPR" (General Data Protection Regulation)<br>
+                Le informazioni contenute in questo messaggio di posta elettronica e/o nel/i file/s allegato/i sono da considerarsi strettamente riservate.
+                Il loro utilizzo è consentito esclusivamente al destinatario sopra indicato.
+                Qualora riceveste questo messaggio senza essere il destinatario Vi preghiamo cortesemente di informarci con apposito messaggio e procedere alla distruzione del messaggio stesso, cancellandolo dal Vostro sistema;
+                costituisce comportamento contrario ai principi dettati dal "GDPR" (General Data Protection Regulation) il trattenere il messaggio stesso, divulgarlo anche in parte, distribuirlo ad altri soggetti, ovvero copiarlo.<br>
+                Grazie.
+            </small>
+        </div>
+    </div>
+</body>
+</html>

+ 423 - 30
resources/views/livewire/records.blade.php

@@ -246,49 +246,109 @@
         </div>
     </div>
 
-    <!-- Modal for Export Date Range -->
     <div class="modal fade" id="exportModal" tabindex="-1" aria-labelledby="exportModalLabel" aria-hidden="true">
-        <div class="modal-dialog">
-            <div class="modal-content">
-                <div class="modal-header" style="background-color: #0C6197!important;">
-                    <h5 class="modal-title" id="exportModalLabel">Seleziona Periodo per Export</h5>
-                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="CHIUDI"></button>
+    <div class="modal-dialog">
+        <div class="modal-content">
+            <div class="modal-header" style="background-color: #0C6197!important;">
+                <h5 class="modal-title" id="exportModalLabel">Seleziona Periodo per Export</h5>
+                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="CHIUDI"></button>
+            </div>
+            <div class="modal-body">
+                <div class="row g-3">
+                    <div class="col-md-6">
+                        <label for="exportFromDate" class="form-label">Data Inizio</label>
+                        <input type="date" class="form-control" id="exportFromDate" wire:model.defer="exportFromDate">
+                    </div>
+                    <div class="col-md-6">
+                        <label for="exportToDate" class="form-label">Data Fine</label>
+                        <input type="date" class="form-control" id="exportToDate" wire:model.defer="exportToDate">
+                    </div>
                 </div>
-                <div class="modal-body">
-                    <div class="row g-3">
-                        <div class="col-md-6">
-                            <label for="exportFromDate" class="form-label">Data Inizio</label>
-                            <input type="date" class="form-control" id="exportFromDate" wire:model.defer="exportFromDate">
-                        </div>
-                        <div class="col-md-6">
-                            <label for="exportToDate" class="form-label">Data Fine</label>
-                            <input type="date" class="form-control" id="exportToDate" wire:model.defer="exportToDate">
+
+                <div class="row mt-4">
+                    <div class="col-12">
+                        <div class="form-check export-method-check">
+                            <input class="form-check-input" type="checkbox" id="sendViaEmail" wire:model.defer="sendViaEmail">
+                            <label class="form-check-label" for="sendViaEmail">
+                                <i class="fas fa-envelope me-2"></i>Invia via Email
+                                <small class="d-block text-muted mt-1">L'export verrà elaborato in background e inviato alla tua email</small>
+                            </label>
                         </div>
                     </div>
-                    <div class="row mt-3">
-                        <div class="col-12">
-                            <small class="text-muted">
-                                <i class="fas fa-info-circle me-1"></i>
-                                L'export includerà tutti i record nel periodo selezionato con i filtri attualmente applicati.
-                            </small>
+                </div>
+
+                <div class="row mt-3" style="display: none;" id="emailAddressRow">
+                    <div class="col-12">
+                        <label for="exportEmailAddress" class="form-label">
+                            <i class="fas fa-envelope me-1"></i>Indirizzo Email
+                        </label>
+                        <input type="email" class="form-control" id="exportEmailAddress"
+                               wire:model.defer="exportEmailAddress"
+                               placeholder="inserisci@email.com">
+                        <div class="invalid-feedback" id="emailValidationFeedback">
+                            Inserisci un indirizzo email valido
                         </div>
+                        <small class="form-text text-muted">
+                            Il file Excel verrà inviato a questo indirizzo
+                        </small>
                     </div>
                 </div>
-                <div class="modal-footer" style="background-color: #FFF!important;">
-                    <button type="button" class="btn--ui lightGrey me-2" data-bs-dismiss="modal">ANNULLA</button>
-                    <button type="button" class="btn--ui primary" wire:click="exportWithDateRange" @if($isExporting) disabled @endif>
-                        @if($isExporting)
-                            <i class="fas fa-spinner fa-spin me-1"></i> ESPORTANDO...
-                        @else
-                            <i class="fas fa-download me-1"></i> ESPORTA
-                        @endif
-                    </button>
+
+                <div class="row mt-3" style="display: none;" id="emailSubjectRow">
+                    <div class="col-12">
+                        <label for="exportEmailSubject" class="form-label">
+                            <i class="fas fa-tag me-1"></i>Oggetto Email
+                        </label>
+                        <input type="text" class="form-control" id="exportEmailSubject"
+                               wire:model.defer="exportEmailSubject"
+                               placeholder="Prima Nota - Export">
+                        <small class="form-text text-muted">
+                            Personalizza l'oggetto dell'email
+                        </small>
+                    </div>
+                </div>
+
+                <div class="row mt-3">
+                    <div class="col-12">
+                        <div class="alert alert-info d-flex align-items-start">
+                            <i class="fas fa-info-circle me-2 mt-1"></i>
+                            <div>
+                                <strong>Informazioni Export:</strong>
+                                <ul class="mb-0 mt-1">
+                                    <li>L'export includerà tutti i record nel periodo selezionato</li>
+                                    <li>Verranno applicati i filtri attualmente attivi</li>
+                                    <li id="emailProcessingInfo" style="display: none;">L'elaborazione avverrà in background, potrai continuare a usare l'applicazione</li>
+                                </ul>
+                            </div>
+                        </div>
+                    </div>
                 </div>
             </div>
+            <div class="modal-footer" style="background-color: #FFF!important;">
+                <button type="button" class="btn--ui lightGrey me-2" data-bs-dismiss="modal">ANNULLA</button>
+                <button type="button" class="btn--ui primary" wire:click="exportWithDateRange" @if($isExporting) disabled @endif>
+                    @if($isExporting)
+                        <div class="d-flex align-items-center">
+                            <div class="spinner-border spinner-border-sm me-2" role="status">
+                                <span class="visually-hidden">Loading...</span>
+                            </div>
+                            <span id="exportButtonText">ELABORAZIONE...</span>
+                        </div>
+                    @else
+                        <i class="fas fa-download me-1" id="exportIcon"></i>
+                        <span id="exportButtonText">ESPORTA</span>
+                    @endif
+                </button>
+            </div>
         </div>
     </div>
 </div>
 
+<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 11000;">
+    <!-- Toasts will be dynamically added here -->
+</div>
+</div>
+
 @push('scripts')
     <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
 @endpush
@@ -536,6 +596,106 @@
                 text-align: center;
             }
         }
+
+        .export-method-check {
+            padding: 16px 16px 16px 50px;
+            background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
+            border-radius: 8px;
+            border: 2px solid #e9ecef;
+            transition: all 0.3s ease;
+            cursor: pointer;
+            position: relative;
+        }
+
+        .export-method-check:hover {
+            border-color: #0C6197;
+            background: linear-gradient(135deg, #e8f4f8 0%, #d1ecf1 100%);
+        }
+
+        .export-method-check .form-check-input {
+            position: absolute;
+            left: 16px;
+            top: 50%;
+            transform: translateY(-50%);
+            margin: 0;
+            width: 20px;
+            height: 20px;
+            background-color: #fff;
+            border: 2px solid #dee2e6;
+            border-radius: 4px;
+            cursor: pointer;
+        }
+
+        .export-method-check .form-check-input:checked {
+            background-color: #0C6197;
+            border-color: #0C6197;
+        }
+
+        .export-method-check .form-check-input:checked ~ .form-check-label {
+            color: #0C6197;
+            font-weight: 600;
+        }
+
+        .export-method-check .form-check-label {
+            font-weight: 500;
+            color: #495057;
+            cursor: pointer;
+            margin-left: 0;
+            display: block;
+        }
+
+        .form-check-input:focus {
+            border-color: #0C6197;
+            outline: 0;
+            box-shadow: 0 0 0 0.2rem rgba(12, 97, 151, 0.25);
+        }
+        #emailAddressRow.show, #emailSubjectRow.show {
+            display: block !important;
+            animation: slideDown 0.3s ease-out;
+        }
+
+        @keyframes slideDown {
+            from {
+                opacity: 0;
+                transform: translateY(-10px);
+            }
+            to {
+                opacity: 1;
+                transform: translateY(0);
+            }
+        }
+
+        .invalid-feedback {
+            display: none;
+        }
+
+        .is-invalid ~ .invalid-feedback {
+            display: block;
+        }
+
+        .alert-info {
+            background-color: rgba(12, 97, 151, 0.1);
+            border-color: rgba(12, 97, 151, 0.2);
+            color: #0C6197;
+        }
+
+        .spinner-border-sm {
+            width: 1rem;
+            height: 1rem;
+        }
+
+        .toast {
+            min-width: 300px;
+        }
+
+        .toast-body {
+            font-weight: 500;
+        }
+
+        .btn--ui:disabled {
+            opacity: 0.7;
+            cursor: not-allowed;
+        }
     </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>
@@ -735,5 +895,238 @@
         Livewire.on('hide-export-modal', () => {
             $('#exportModal').modal('hide');
         });
+
+        function showToast(type, message, duration = 5000) {
+            const toastContainer = document.querySelector('.toast-container');
+            if (!toastContainer) {
+                console.error('Toast container not found');
+                return;
+            }
+
+            const toastId = 'toast-' + Date.now();
+
+            const toastColors = {
+                success: 'bg-success',
+                error: 'bg-danger',
+                warning: 'bg-warning',
+                info: 'bg-info'
+            };
+
+            const toastIcons = {
+                success: 'fa-check-circle',
+                error: 'fa-exclamation-circle',
+                warning: 'fa-exclamation-triangle',
+                info: 'fa-info-circle'
+            };
+
+            const toast = document.createElement('div');
+            toast.id = toastId;
+            toast.className = `toast align-items-center text-white ${toastColors[type]} border-0`;
+            toast.setAttribute('role', 'alert');
+            toast.innerHTML = `
+                <div class="d-flex">
+                    <div class="toast-body">
+                        <i class="fas ${toastIcons[type]} me-2"></i>
+                        ${message}
+                    </div>
+                    <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
+                </div>
+            `;
+
+            toastContainer.appendChild(toast);
+
+            if (typeof bootstrap !== 'undefined') {
+                const bsToast = new bootstrap.Toast(toast, { delay: duration });
+                bsToast.show();
+
+                toast.addEventListener('hidden.bs.toast', function() {
+                    if (toastContainer.contains(toast)) {
+                        toastContainer.removeChild(toast);
+                    }
+                });
+
+                return bsToast;
+            } else {
+                toast.style.display = 'block';
+                setTimeout(() => {
+                    if (toastContainer.contains(toast)) {
+                        toastContainer.removeChild(toast);
+                    }
+                }, duration);
+            }
+        }
+
+        document.addEventListener('DOMContentLoaded', function() {
+            const sendViaEmailCheckbox = document.getElementById('sendViaEmail');
+            const emailAddressRow = document.getElementById('emailAddressRow');
+            const emailSubjectRow = document.getElementById('emailSubjectRow');
+            const emailProcessingInfo = document.getElementById('emailProcessingInfo');
+            const exportIcon = document.getElementById('exportIcon');
+            const exportButtonText = document.getElementById('exportButtonText');
+            const emailInput = document.getElementById('exportEmailAddress');
+
+            function toggleEmailFields() {
+                if (sendViaEmailCheckbox && sendViaEmailCheckbox.checked) {
+                    if (emailAddressRow) {
+                        emailAddressRow.style.display = 'block';
+                        emailAddressRow.classList.add('show');
+                    }
+                    if (emailSubjectRow) {
+                        emailSubjectRow.style.display = 'block';
+                        emailSubjectRow.classList.add('show');
+                    }
+                    if (emailProcessingInfo) {
+                        emailProcessingInfo.style.display = 'list-item';
+                    }
+
+                    if (exportIcon) exportIcon.className = 'fas fa-paper-plane me-1';
+                    if (exportButtonText) exportButtonText.textContent = 'INVIA EMAIL';
+                } else {
+                    if (emailAddressRow) {
+                        emailAddressRow.style.display = 'none';
+                        emailAddressRow.classList.remove('show');
+                    }
+                    if (emailSubjectRow) {
+                        emailSubjectRow.style.display = 'none';
+                        emailSubjectRow.classList.remove('show');
+                    }
+                    if (emailProcessingInfo) {
+                        emailProcessingInfo.style.display = 'none';
+                    }
+
+                    if (exportIcon) exportIcon.className = 'fas fa-download me-1';
+                    if (exportButtonText) exportButtonText.textContent = 'ESPORTA';
+                }
+            }
+
+            function validateEmail(email) {
+                const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+                return emailRegex.test(email);
+            }
+
+            if (sendViaEmailCheckbox) {
+                sendViaEmailCheckbox.addEventListener('change', toggleEmailFields);
+            }
+
+            if (emailInput) {
+                emailInput.addEventListener('blur', function() {
+                    if (sendViaEmailCheckbox && sendViaEmailCheckbox.checked && this.value) {
+                        if (validateEmail(this.value)) {
+                            this.classList.remove('is-invalid');
+                            this.classList.add('is-valid');
+                        } else {
+                            this.classList.remove('is-valid');
+                            this.classList.add('is-invalid');
+                        }
+                    }
+                });
+
+                emailInput.addEventListener('input', function() {
+                    this.classList.remove('is-invalid', 'is-valid');
+                });
+            }
+
+            if (typeof $ !== 'undefined') {
+                $('#exportModal').on('shown.bs.modal', function() {
+                    toggleEmailFields();
+                });
+            }
+        });
+
+        document.addEventListener('livewire:load', function () {
+            console.log('Livewire loaded, setting up export event listeners');
+
+            Livewire.on('export-email-queued', function() {
+                console.log('Export email queued event received');
+                showToast('info',
+                    '<strong>Export avviato!</strong><br>' +
+                    'L\'elaborazione è in corso in background. Riceverai l\'email a breve.',
+                    8000
+                );
+            });
+
+            Livewire.on('export-email-sent', function() {
+                console.log('Export email sent event received');
+                showToast('success',
+                    '<strong>Email inviata!</strong><br>' +
+                    'L\'export è stato completato e inviato alla tua email.',
+                    6000
+                );
+            });
+
+            Livewire.on('export-email-error', function(message) {
+                console.log('Export email error event received:', message);
+                showToast('error',
+                    '<strong>Errore nell\'export:</strong><br>' +
+                    (message || 'Si è verificato un errore durante l\'elaborazione.'),
+                    10000
+                );
+            });
+
+            Livewire.on('show-export-modal', function() {
+                console.log('Show export modal event received');
+                if (typeof $ !== 'undefined') {
+                    $('#exportModal').modal('show');
+                }
+            });
+
+            Livewire.on('hide-export-modal', function() {
+                console.log('Hide export modal event received');
+                if (typeof $ !== 'undefined') {
+                    $('#exportModal').modal('hide');
+                }
+            });
+        });
+
+        if (typeof Livewire !== 'undefined') {
+            document.addEventListener('livewire:initialized', function () {
+                console.log('Livewire initialized, setting up export event listeners');
+
+                Livewire.on('export-email-queued', function() {
+                    showToast('info',
+                        '<strong>Export avviato!</strong><br>' +
+                        'L\'elaborazione è in corso in background. Riceverai l\'email a breve.',
+                        8000
+                    );
+                });
+
+                Livewire.on('export-email-sent', function() {
+                    showToast('success',
+                        '<strong>Email inviata!</strong><br>' +
+                        'L\'export è stato completato e inviato alla tua email.',
+                        6000
+                    );
+                });
+
+                Livewire.on('export-email-error', function(message) {
+                    showToast('error',
+                        '<strong>Errore nell\'export:</strong><br>' +
+                        (message || 'Si è verificato un errore durante l\'elaborazione.'),
+                        10000
+                    );
+                });
+
+                Livewire.on('show-export-modal', function() {
+                    if (typeof $ !== 'undefined') {
+                        $('#exportModal').modal('show');
+                    }
+                });
+
+                Livewire.on('hide-export-modal', function() {
+                    if (typeof $ !== 'undefined') {
+                        $('#exportModal').modal('hide');
+                    }
+                });
+            });
+        }
+
+        window.addEventListener('load', function() {
+            if (typeof showToast === 'function') {
+                console.log('showToast function is available globally');
+            } else {
+                console.error('showToast function is not available globally');
+            }
+        });
+
     </script>
 @endpush