Просмотр исходного кода

Merge branch 'iao_team' of http://host.webmagistri.biz:3000/parisio/iao_team into iao_team

Luca Parisio 6 месяцев назад
Родитель
Сommit
a100a7dd5f

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

+ 77 - 108
app/Http/Livewire/Member.php

@@ -40,12 +40,16 @@ class Member extends Component
         'city_id.required' => 'city_id',
         'gender' => 'gender'
     ];
-
     public function change($type)
     {
         $this->type = $type;
-    }
 
+        if ($type === 'corsi' && $this->dataId > 0) {
+            $this->loadMemberCards();
+            $this->checkCourseAvailability();
+        }
+        $this->dispatchBrowserEvent('scroll-to-top');
+    }
     public function sortBy($field)
     {
         if ($this->sortField === $field) {
@@ -132,7 +136,7 @@ class Member extends Component
     public $course_name, $course_level_id, $course_type_id, $course_frequency_id;
 
     // Certificates data
-    public $member_certificates = array(), $certificate_type, $certificate_filename_old, $certificate_filename, $certificate_expire_date, $certificate_status,$cardCertificateId, $addCertificate, $updateCertificate, $certificateDataId;
+    public $member_certificates = array(), $certificate_type, $certificate_filename_old, $certificate_filename, $certificate_expire_date, $certificate_status, $cardCertificateId, $addCertificate, $updateCertificate, $certificateDataId;
 
     public $filterCard = [];
     public $filterCategory = [];
@@ -141,6 +145,7 @@ class Member extends Component
     public $filterCertScaduto = 0;
     public $filterCertInScadenza = 0;
     public $already_existing = false;
+    public $tabOrder = ['dati', 'tesseramento', 'corsi', 'gruppi'];
 
     protected $rules = [
         'first_name' => 'required',
@@ -230,10 +235,10 @@ class Member extends Component
 
     public function resetCertificateFields()
     {
-        $this->certificate_type = 'N';
+        $this->certificate_type = '';
         $this->certificate_filename = '';
         $this->certificate_filename_old = '';
-        $this->certificate_expire_date = null;
+        $this->certificate_expire_date = date('Y-m-d', strtotime('+1 year'));
         $this->certificate_status = 0;
     }
 
@@ -333,14 +338,18 @@ class Member extends Component
         if ($this->course_frequency_id != '') {
             list($n, $y) = explode("(", $this->course_name);
             $y = trim(str_replace(")", "", $y));
-            $this->course_course_id = \App\Models\Course::where('name', 'like', '%' . trim($n) . "%")->where('year', $y)->where('course_level_id', $this->course_level_id)->where('course_type_id', $this->course_type_id)->where('course_frequency_id', $this->course_frequency_id)->first()->id;
+            $course = \App\Models\Course::where('name', 'like', '%' . trim($n) . "%")
+                ->where('year', $y)
+                ->where('course_level_id', $this->course_level_id)
+                ->where('course_type_id', $this->course_type_id)
+                ->where('course_frequency_id', $this->course_frequency_id)
+                ->first();
+
+            if ($course) {
+                $this->course_course_id = $course->id;
+                $this->course_price = formatPrice($course->price);
+                $this->course_subscription_price = formatPrice($course->subscription_price);
 
-            if ($this->course_course_id > 0) {
-                $c = \App\Models\Course::findOrFail($this->course_course_id);
-                $this->course_price = formatPrice($c->price);
-                $this->course_subscription_price = formatPrice($c->subscription_price);
-                // Controllo se sono già iscritto la corso
-                $this->course_exist = \App\Models\MemberCourse::where('course_id', $this->course_course_id)->where('member_id', $this->dataId)->count() > 0;
             } else {
                 $this->course_price = 0;
                 $this->course_subscription_price = 0;
@@ -409,7 +418,6 @@ class Member extends Component
     public function getCategories($records, $indentation)
     {
         foreach ($records as $record) {
-            // $this->categories[] = array('id' => $record->id, 'name' => str_repeat(" / ", $indentation) . $record->name);
             $this->categories[] = array('id' => $record->id, 'name' => $record->getTree(), 'indentation' => $indentation);
             if (count($record->childs))
                 $this->getCategories($record->childs, $indentation + 1);
@@ -419,7 +427,6 @@ class Member extends Component
     public function getCourses($records, $indentation)
     {
         foreach ($records as $record) {
-            // $this->categories[] = array('id' => $record->id, 'name' => str_repeat(" / ", $indentation) . $record->name);
             $this->courses[] = array('id' => $record->id, 'name' => $record->getTree());
             if (count($record->childs))
                 $this->getCourses($record->childs, $indentation + 1);
@@ -460,12 +467,6 @@ class Member extends Component
 
         $this->course_subscriptions = \App\Models\CourseSubscription::select('*')->where('enabled', true)->get();
 
-        // $this->getCourses(\App\Models\Course::select('id', 'name')->where('parent_id', null)->get(), 0);
-
-        /*$this->nations = \App\Models\Nation::select('id', 'name')->orderBy('name')->get();
-        $this->provinces = \App\Models\Province::where('nation_id', 1)->orderBy('name')->get();
-        $this->cities = \App\Models\City::where('province_id', 178)->orderBy('name')->orderBy('name')->get();*/
-
         $c = \App\Models\Causal::where('type', 'IN')->where('money', true)->first();
         if ($c)
             $this->causalId = $c->id;
@@ -474,9 +475,6 @@ class Member extends Component
             $this->showDetailF($_GET["member_detail"]);
             $this->refreshAfter = 1;
         }
-        /*$this->birthNations = \App\Models\Nation::select('id', 'name')->orderBy('name')->get();
-        $this->birthProvinces = \App\Models\Province::where('nation_id', 1)->orderBy('name')->get();
-        $this->birthCities = \App\Models\City::where('province_id', 178)->orderBy('name')->orderBy('name')->get();*/
     }
 
     public function updated()
@@ -531,8 +529,6 @@ class Member extends Component
         }
 
         $this->emit('load-select');
-        // $this->emit('destroy-data-table');
-
     }
 
     public function checkIsItaly()
@@ -550,36 +546,6 @@ class Member extends Component
             $this->isBirthItaly = false;
     }
 
-    /*public function loadProvinces()
-    {
-        $n = \App\Models\Nation::findOrFail($this->nation_id);
-        $this->isItaly = $n->is_italy;
-        $this->provinces = \App\Models\Province::where('nation_id', $this->nation_id)->orderBy('name')->get();
-        $this->cities = array();
-        $this->selectId++;
-    }
-
-    public function loadCities()
-    {
-        $this->cities = \App\Models\City::where('province_id', $this->province_id)->orderBy('name')->orderBy('name')->get();
-        $this->selectId++;
-    }
-
-    public function loadBirthProvinces()
-    {
-        $n = \App\Models\Nation::findOrFail($this->birth_nation_id);
-        $this->isBirthItaly = $n->is_italy;
-        $this->birthProvinces = \App\Models\Province::where('nation_id', $this->birth_nation_id)->orderBy('name')->get();
-        $this->birthCities = array();
-        $this->selectId++;
-    }
-
-    public function loadBirthCities()
-    {
-        $this->birthCities = \App\Models\City::where('province_id', $this->birth_province_id)->get();
-        $this->selectId++;
-    }
-    */
     public function search()
     {
         if ($this->searchTxt != '') {
@@ -770,6 +736,14 @@ class Member extends Component
         $this->emit('setEdit', true);
         $this->emit('setEditCorso', false);
     }
+    private function moveToNextTab()
+    {
+        $currentIndex = array_search($this->type, $this->tabOrder);
+
+        if ($currentIndex !== false && $currentIndex < count($this->tabOrder) - 1) {
+            $this->type = $this->tabOrder[$currentIndex + 1];
+        }
+    }
 
     public function store($close)
     {
@@ -925,6 +899,8 @@ class Member extends Component
                 $this->add = false;
             } else {
                 $this->edit($member->id);
+                $this->emit('saved-and-continue', $this->type);
+                $this->dispatchBrowserEvent('scroll-to-top');
             }
             $this->emit('setEdit', false);
         } catch (\Exception $ex) {
@@ -1170,6 +1146,9 @@ class Member extends Component
             if ($close) {
                 $this->resetFields();
                 $this->update = false;
+            } else {
+                $this->emit('saved-and-continue', $this->type);
+                $this->dispatchBrowserEvent('scroll-to-top');
             }
             $this->emit('setEdit', false);
         } catch (\Exception $ex) {
@@ -1231,7 +1210,23 @@ class Member extends Component
         $this->addCard = true;
         $this->updateCard = false;
     }
+    private function checkCourseAvailability()
+    {
+        $this->active = $this->getActiveStatus();
+
+        $this->emit('course-availability-updated');
+    }
 
+    private function getActiveStatus()
+    {
+        if ($this->dataId > 0) {
+            $member = \App\Models\Member::find($this->dataId);
+            if ($member) {
+                return $member->getStatus();
+            }
+        }
+        return ["status" => 0, "date" => null];
+    }
     public function storeCard()
     {
 
@@ -1277,6 +1272,8 @@ class Member extends Component
             session()->flash('success, Tesserato creato');
             $this->resetCardFields();
             $this->addCard = false;
+            $this->loadMemberCards();
+            $this->checkCourseAvailability();
         } catch (\Exception $ex) {
             session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
         }
@@ -1311,7 +1308,7 @@ class Member extends Component
         $this->validate(['card_card_id' => 'required']);
 
         try {
-            \Illuminate\Support\Facades\Log::info('Starting card update', [
+            Log::info('Starting card update', [
                 'member_id' => $this->dataId,
                 'card_id' => $this->card_card_id,
                 'card_number' => $this->card_number
@@ -1321,7 +1318,7 @@ class Member extends Component
             if ($this->card_date != '') {
                 $card = \App\Models\Card::findOrFail($this->card_card_id);
 
-                \Illuminate\Support\Facades\Log::info('Card details', [
+                Log::info('Card details', [
                     'card_id' => $card->id,
                     'next_day_expire' => $card->next_day_expire,
                     'next_month_expire' => $card->next_month_expire,
@@ -1346,7 +1343,7 @@ class Member extends Component
                         $expire_date = $next_exp_obj->format('Y-m-d');
                     }
 
-                    \Illuminate\Support\Facades\Log::info('Calculated expiration date (next_day_expire/next_month_expire rule)', [
+                    Log::info('Calculated expiration date (next_day_expire/next_month_expire rule)', [
                         'input_date' => $this->card_date,
                         'next_exp' => $next_exp,
                         'expire_date' => $expire_date,
@@ -1356,7 +1353,7 @@ class Member extends Component
                     if ($card->one_year_expire) {
                         $expire_date = date("Y-m-d", strtotime($this->card_date . ' + 1 years'));
 
-                        \Illuminate\Support\Facades\Log::info('Calculated expiration date (one_year_expire rule)', [
+                        Log::info('Calculated expiration date (one_year_expire rule)', [
                             'input_date' => $this->card_date,
                             'expire_date' => $expire_date
                         ]);
@@ -1364,7 +1361,7 @@ class Member extends Component
                 }
             }
 
-            \Illuminate\Support\Facades\Log::info('Updating member card', [
+            Log::info('Updating member card', [
                 'card_id' => $this->cardDataId,
                 'member_id' => $this->dataId,
                 'card_number' => $this->card_number,
@@ -1388,7 +1385,7 @@ class Member extends Component
 
             updateMemberData($this->dataId);
 
-            \Illuminate\Support\Facades\Log::info('Card updated successfully', [
+            Log::info('Card updated successfully', [
                 'card_id' => $this->cardDataId,
                 'member_id' => $this->dataId
             ]);
@@ -1396,8 +1393,10 @@ class Member extends Component
             session()->flash('success', 'Tesserato aggiornato');
             $this->resetCardFields();
             $this->updateCard = false;
+            $this->loadMemberCards();
+            $this->checkCourseAvailability();
         } catch (\Exception $ex) {
-            \Illuminate\Support\Facades\Log::error('Error updating card', [
+            Log::error('Error updating card', [
                 'card_id' => $this->cardDataId,
                 'member_id' => $this->dataId,
                 'error_message' => $ex->getMessage(),
@@ -1429,19 +1428,6 @@ class Member extends Component
     public function addCourse()
     {
         $this->resetCourseFields();
-        /*if ($this->under18)
-        {
-            $this->course_months[] = array("m" => 9, "status" => "");
-            $this->course_months[] = array("m" => 10, "status" => "");
-            $this->course_months[] = array("m" => 11, "status" => "");
-            $this->course_months[] = array("m" => 12, "status" => "");
-            $this->course_months[] = array("m" => 1, "status" => "");
-            $this->course_months[] = array("m" => 2, "status" => "");
-            $this->course_months[] = array("m" => 3, "status" => "");
-            $this->course_months[] = array("m" => 4, "status" => "");
-            $this->course_months[] = array("m" => 5, "status" => "");
-            $this->course_months[] = array("m" => 6, "status" => "");
-        }*/
         $this->addCourse = true;
         $this->updateCourse = false;
         $this->emit('setEditCorso', true);
@@ -1874,25 +1860,19 @@ class Member extends Component
 
         $price = $course["price"];
 
-        if (sizeof($this->payMonths) == 1)
-        {
+        if (sizeof($this->payMonths) == 1) {
             $month = $this->payMonths[0];
             $records = \App\Models\Record::where('member_course_id', $this->selectedCourseMember)->where('deleted', 0)->get();
-            foreach ($records as $record)
-            {
+            foreach ($records as $record) {
 
-                if (in_array($month, json_decode($record->months)))
-                {
+                if (in_array($month, json_decode($record->months))) {
 
-                    foreach ($record->rows as $row)
-                    {
+                    foreach ($record->rows as $row) {
 
 
-                        if ($row->causal_id == $c->causal_id && !str_contains(strtolower($row->note), 'iscrizione'))
-                        {
+                        if ($row->causal_id == $c->causal_id && !str_contains(strtolower($row->note), 'iscrizione')) {
                             $tot = sizeof(json_decode($row->when));
-                            foreach(json_decode($row->when) as $m)
-                            {
+                            foreach (json_decode($row->when) as $m) {
                                 $price -= $row->amount / $tot;
                             }
                         }
@@ -1952,21 +1932,16 @@ class Member extends Component
                 if ($mm->m == $m) {
                     if ($mm->status == "")
                         $this->payMonths[] = $m;
-                    if ($mm->status == "1")
-                    {
+                    if ($mm->status == "1") {
                         $mc = \App\Models\MemberCourse::findOrFail($selectedCourseMember);
                         $price = $mc->price;
                         $payed = 0;
                         $extraC = '';
                         $recordsPayed = \App\Models\Record::where('member_course_id', $selectedCourseMember)->where('deleted', 0)->get();
-                        foreach ($recordsPayed as $record)
-                        {
-                            if (in_array($m, json_decode($record->months)))
-                            {
-                                foreach ($record->rows as $row)
-                                {
-                                    if ($row->causal_id == $mc->course->causal_id && !str_contains(strtolower($row->note), 'iscrizione'))
-                                    {
+                        foreach ($recordsPayed as $record) {
+                            if (in_array($m, json_decode($record->months))) {
+                                foreach ($record->rows as $row) {
+                                    if ($row->causal_id == $mc->course->causal_id && !str_contains(strtolower($row->note), 'iscrizione')) {
                                         $tot = sizeof(json_decode($row->when));
                                         $payed += $row->amount / $tot;
                                     }
@@ -2032,22 +2007,17 @@ class Member extends Component
                         $class = "yellow";
                 }
 
-                if ($class == 'green')
-                {
+                if ($class == 'green') {
 
                     $mc = \App\Models\MemberCourse::findOrFail($selectedCourseMember);
                     $price = $mc->price;
                     $payed = 0;
                     $extraC = '';
                     $recordsPayed = \App\Models\Record::where('member_course_id', $selectedCourseMember)->where('deleted', 0)->get();
-                    foreach ($recordsPayed as $record)
-                    {
-                        if (in_array($m, json_decode($record->months)))
-                        {
-                            foreach ($record->rows as $row)
-                            {
-                                if ($row->causal_id == $mc->course->causal_id && !str_contains(strtolower($row->note), 'iscrizione'))
-                                {
+                    foreach ($recordsPayed as $record) {
+                        if (in_array($m, json_decode($record->months))) {
+                            foreach ($record->rows as $row) {
+                                if ($row->causal_id == $mc->course->causal_id && !str_contains(strtolower($row->note), 'iscrizione')) {
                                     $tot = sizeof(json_decode($row->when));
                                     $payed += $row->amount / $tot;
                                 }
@@ -2058,7 +2028,6 @@ class Member extends Component
                         $class = 'orange half';
                     //$class .= $extraC;
                 }
-
             }
         }
 

Разница между файлами не показана из-за своего большого размера
+ 844 - 67
app/Http/Livewire/Record.php


+ 38 - 7
app/Http/Livewire/Supplier.php

@@ -2,13 +2,14 @@
 
 namespace App\Http\Livewire;
 use Livewire\Component;
-
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Log;
 class Supplier extends Component
 {
     public $records, $name, $fiscal_code, $vat, $address, $zip_code,$nation_id,$province_id,$city_id,$referent,$website,$phone,$email,$enabled, $dataId, $update = false, $add = false;
     public $referent_first_name, $referent_last_name, $referent_email, $referent_phone, $referent_mobile;
     public $isItaly = true;
-
+    public $showReset = false;
     public $searchTxt;
     public $search;
 
@@ -60,7 +61,7 @@ class Supplier extends Component
     public function mount()
     {
 
-        if(\Auth::user()->level != env('LEVEL_ADMIN', 0))
+        if(Auth::user()->level != env('LEVEL_ADMIN', 0))
             return redirect()->to('/dashboard');
 
         if (isset($_GET["new"]))
@@ -85,9 +86,6 @@ class Supplier extends Component
 
     public function render()
     {
-        /*if ($this->search != '')
-            $this->records = \App\Models\Supplier::with('nation')->where('name', 'LIKE', '%' . $this->search . '%')->orderBy($this->sortField, $this->sortAsc ? 'asc' : 'desc')->get();
-        else*/
             $this->records = \App\Models\Supplier::with('nation')->get();
         return view('livewire.supplier');
     }
@@ -114,6 +112,39 @@ class Supplier extends Component
     public function store()
     {
         $this->validate();
+
+        $vatToCheck = trim($this->vat);
+        $existingSupplier = \App\Models\Supplier::where('vat', $vatToCheck)->first();
+
+        Log::info('VAT validation detailed debug', [
+            'vat_input' => $this->vat,
+            'vat_trimmed' => $vatToCheck,
+            'vat_empty_check' => empty($vatToCheck),
+            'existing_supplier_id' => $existingSupplier ? $existingSupplier->id : null,
+            'existing_supplier_name' => $existingSupplier ? $existingSupplier->name : null,
+            'existing_supplier_vat' => $existingSupplier ? $existingSupplier->vat : null,
+            'validation_rules' => $this->rules
+        ]);
+
+        if (!empty($vatToCheck)) {
+            $duplicateCount = \App\Models\Supplier::where('vat', $vatToCheck)->count();
+            if ($duplicateCount > 0) {
+                Log::info('VAT duplicate found, adding error');
+                $this->addError('vat', 'Già esiste un fornitore con questa Partita IVA');
+                return;
+            }
+        }
+
+        $fiscalCodeToCheck = trim($this->fiscal_code);
+        if (!empty($fiscalCodeToCheck)) {
+            $duplicateFiscalCount = \App\Models\Supplier::where('fiscal_code', $fiscalCodeToCheck)->count();
+            if ($duplicateFiscalCount > 0) {
+                Log::info('Fiscal code duplicate found, adding error');
+                $this->addError('fiscal_code', 'Già esiste un fornitore con questo codice fiscale ');
+                return;
+            }
+        }
+
         try {
             \App\Models\Supplier::create([
                 'name' => $this->name,
@@ -226,7 +257,7 @@ class Supplier extends Component
             session()->flash('success',"Fornitore eliminato");
             return redirect(request()->header('Referer'));
         }catch(\Exception $e){
-            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+            session()->flash('error','Errore (' . $e->getMessage() . ')');
         }
     }
 

+ 891 - 0
app/Jobs/ExportPrimaNota.php

@@ -0,0 +1,891 @@
+<?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 = 1200;
+    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)
+    {
+        Log::info('Job createExcelFile: Starting Excel file creation', [
+            'file_path' => $filePath,
+            'export_data_count' => count($this->exportData),
+            'payments_count' => count($this->payments),
+            'memory_before' => memory_get_usage(true),
+            'time_limit' => ini_get('max_execution_time')
+        ]);
+
+        $startTime = microtime(true);
+
+        Log::info('Job createExcelFile: Preparing column letters');
+        $letters = array('F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'AA');
+
+        Log::info('Job createExcelFile: Creating Spreadsheet object');
+        $spreadsheet = new Spreadsheet();
+        $activeWorksheet = $spreadsheet->getActiveSheet();
+
+        $activeWorksheet->setTitle('Prima Nota');
+
+        Log::info('Job createExcelFile: Setting document properties');
+        $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']);
+
+        Log::info('Job createExcelFile: Setting basic headers');
+        $activeWorksheet->setCellValue('A1', "Data");
+        $activeWorksheet->setCellValue('B1', "Causale");
+        $activeWorksheet->setCellValue('C1', "Dettaglio Causale");
+        $activeWorksheet->setCellValue('D1', "Nominativo");
+        $activeWorksheet->setCellValue('E1', "Stato");
+
+        Log::info('Job createExcelFile: Setting payment method headers and merging cells');
+        $idx = 0;
+        foreach ($this->payments as $p) {
+            if ($idx >= count($letters)) {
+                Log::warning('Job createExcelFile: Reached letter limit during header setup', [
+                    'payment_index' => $idx,
+                    'payment_name' => $p['name']
+                ]);
+                break;
+            }
+
+            Log::debug('Job createExcelFile: Setting payment header', [
+                'payment_name' => $p['name'],
+                'column_index' => $idx,
+                'column_letter' => $letters[$idx]
+            ]);
+
+            $activeWorksheet->setCellValue($letters[$idx] . '1', $p['name']);
+            $activeWorksheet->mergeCells($letters[$idx] . '1:' . $letters[$idx + 1] . '1');
+            $idx += 2;
+        }
+
+        Log::info('Job createExcelFile: Setting sub-headers (row 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) {
+                Log::warning('Job createExcelFile: Reached letter limit during sub-header setup', [
+                    'payment_index' => $idx,
+                    'payment_name' => $p['name']
+                ]);
+                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++;
+            }
+        }
+
+        Log::info('Job createExcelFile: Applying header styles');
+        $maxCol = min(count($letters) - 1, count($this->payments) * 2 + 4);
+        $lastColumnLetter = $letters[$maxCol];
+
+        $activeWorksheet->getStyle('A1:' . $lastColumnLetter . '2')
+            ->getFont()->setBold(true);
+
+        $activeWorksheet->getStyle('A1:' . $lastColumnLetter . '1')
+            ->getFill()
+            ->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
+            ->getStartColor()->setARGB('FF0C6197');
+
+        $activeWorksheet->getStyle('A1:' . $lastColumnLetter . '1')
+            ->getFont()->getColor()->setARGB('FFFFFFFF');
+
+        Log::info('Job createExcelFile: Starting data row processing', [
+            'total_export_records' => count($this->exportData),
+            'time_elapsed_so_far' => microtime(true) - $startTime
+        ]);
+
+        $count = 3;
+        $batchSize = 500;
+        $processed = 0;
+        $batchNumber = 0;
+
+        foreach ($this->exportData as $causal => $record) {
+            if ($processed % 100 == 0) {
+                Log::info('Job createExcelFile: Processing progress', [
+                    'processed' => $processed,
+                    'total' => count($this->exportData),
+                    'current_row' => $count,
+                    'memory_usage' => memory_get_usage(true),
+                    'memory_peak' => memory_get_peak_usage(true),
+                    'time_elapsed' => microtime(true) - $startTime
+                ]);
+            }
+
+            try {
+                $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;
+
+                Log::debug('Job createExcelFile: Setting row data', [
+                    'row' => $count,
+                    'causal_parts' => count($parts)
+                ]);
+
+                $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');
+                }
+
+                Log::debug('Job createExcelFile: Setting payment data for row', ['row' => $count]);
+                $idx = 0;
+                foreach ($this->payments as $p) {
+                    if ($idx >= count($letters) - 1) {
+                        Log::warning('Job createExcelFile: Reached letter limit during payment data', [
+                            'row' => $count,
+                            'payment_index' => $idx
+                        ]);
+                        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) {
+                    $batchNumber++;
+                    Log::info('Job createExcelFile: Batch completed, running garbage collection', [
+                        'batch_number' => $batchNumber,
+                        'processed' => $processed,
+                        'memory_before_gc' => memory_get_usage(true)
+                    ]);
+                    gc_collect_cycles();
+                    Log::info('Job createExcelFile: Garbage collection completed', [
+                        'memory_after_gc' => memory_get_usage(true)
+                    ]);
+                }
+            } catch (\Exception $e) {
+                Log::error('Job createExcelFile: Error processing data row', [
+                    'row' => $count,
+                    'processed_so_far' => $processed,
+                    'causal' => $causal,
+                    'error' => $e->getMessage(),
+                    'trace' => $e->getTraceAsString()
+                ]);
+                throw $e;
+            }
+        }
+
+        Log::info('Job createExcelFile: Data processing completed, adding totals row', [
+            'total_processed' => $processed,
+            'final_row' => $count,
+            'processing_time' => microtime(true) - $startTime
+        ]);
+
+        $count++;
+        $activeWorksheet->setCellValue('A' . $count, 'TOTALE');
+        $activeWorksheet->setCellValue('B' . $count, '');
+        $activeWorksheet->setCellValue('C' . $count, '');
+        $activeWorksheet->setCellValue('D' . $count, '');
+        $activeWorksheet->setCellValue('E' . $count, '');
+
+        Log::info('Job createExcelFile: Setting totals data');
+        $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++;
+            }
+        }
+
+        Log::info('Job createExcelFile: Applying totals row styling');
+        $activeWorksheet->getStyle('A' . $count . ':' . $lastColumnLetter . $count)
+            ->getFont()->setBold(true);
+
+        $activeWorksheet->getStyle('A' . $count . ':' . $lastColumnLetter . $count)
+            ->getFill()
+            ->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
+            ->getStartColor()->setARGB('FFF0F0F0');
+
+        Log::info('Job createExcelFile: Setting column dimensions');
+        $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);
+        }
+
+        Log::info('Job createExcelFile: Setting freeze panes');
+        $activeWorksheet->freezePane('A3');
+
+        Log::info('Job createExcelFile: Creating Excel writer and saving file', [
+            'file_path' => $filePath,
+            'memory_before_save' => memory_get_usage(true)
+        ]);
+
+        try {
+            $writerStart = microtime(true);
+            $writer = new Xlsx($spreadsheet);
+            $writer->save($filePath);
+            $writerTime = microtime(true) - $writerStart;
+
+            Log::info('Job createExcelFile: File saved successfully', [
+                'file_path' => $filePath,
+                'file_exists' => file_exists($filePath),
+                'file_size' => file_exists($filePath) ? filesize($filePath) : 0,
+                'writer_time' => $writerTime,
+                'total_time' => microtime(true) - $startTime,
+                'memory_peak' => memory_get_peak_usage(true)
+            ]);
+        } catch (\Exception $e) {
+            Log::error('Job createExcelFile: Error during file save', [
+                'file_path' => $filePath,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString(),
+                'memory_usage' => memory_get_usage(true),
+                'time_elapsed' => microtime(true) - $startTime
+            ]);
+            throw $e;
+        }
+
+        Log::info('Job createExcelFile: Cleaning up memory');
+        unset($spreadsheet, $activeWorksheet, $writer);
+        gc_collect_cycles();
+
+        Log::info('Job createExcelFile: Completed successfully', [
+            'total_time' => microtime(true) - $startTime,
+            'memory_after_cleanup' => memory_get_usage(true)
+        ]);
+    }
+
+    // Generate column letters more efficiently
+    private function generateColumnLetters($count)
+    {
+        $letters = [];
+        for ($i = 0; $i < $count && $i < 100; $i++) { // Limit to prevent infinite loops
+            if ($i < 26) {
+                $letters[] = chr(65 + $i); // A-Z
+            } else {
+                $letters[] = 'A' . chr(65 + ($i - 26)); // AA, AB, AC...
+            }
+        }
+        return $letters;
+    }
+
+    // Build headers more efficiently
+    private function buildHeaders($activeWorksheet, $letters)
+    {
+        // Set basic headers
+        $basicHeaders = [
+            'A1' => 'Data',
+            'B1' => 'Causale',
+            'C1' => 'Dettaglio Causale',
+            'D1' => 'Nominativo',
+            'E1' => 'Stato'
+        ];
+
+        // Use fromArray for faster header setting
+        $activeWorksheet->fromArray(array_values($basicHeaders), null, 'A1', true);
+
+        // Set payment method headers
+        $paymentHeaders = [];
+        $subHeaders = [];
+        $idx = 5; // Start after basic headers (F column = index 5)
+
+        foreach ($this->payments as $p) {
+            if ($idx >= count($letters)) break;
+
+            $paymentHeaders[] = $p['name'];
+            $activeWorksheet->mergeCells($letters[$idx] . '1:' . $letters[$idx + 1] . '1');
+
+            // Sub headers for row 2
+            if ($p['type'] == 'ALL') {
+                $subHeaders[$letters[$idx] . '2'] = 'Entrate';
+                $subHeaders[$letters[$idx + 1] . '2'] = 'Uscite';
+            } elseif ($p['type'] == 'IN') {
+                $subHeaders[$letters[$idx] . '2'] = 'Entrate';
+                $subHeaders[$letters[$idx + 1] . '2'] = '';
+            } elseif ($p['type'] == 'OUT') {
+                $subHeaders[$letters[$idx] . '2'] = '';
+                $subHeaders[$letters[$idx + 1] . '2'] = 'Uscite';
+            }
+
+            $idx += 2;
+        }
+
+        // Set payment headers
+        $col = 5;
+        foreach ($paymentHeaders as $header) {
+            if ($col < count($letters)) {
+                $activeWorksheet->setCellValue($letters[$col] . '1', $header);
+            }
+            $col += 2;
+        }
+
+        // Set sub headers
+        foreach ($subHeaders as $cell => $value) {
+            $activeWorksheet->setCellValue($cell, $value);
+        }
+    }
+
+    // Build data rows with batch processing
+    private function buildDataRows($activeWorksheet, $letters)
+    {
+        $rowNum = 3; // Start after headers
+        $batchSize = 100; // Process in smaller batches
+        $batch = [];
+        $batchCount = 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;
+
+            // Prepare row data
+            $rowData = [
+                !empty($d) ? date("d/m/Y", strtotime($d)) : '',
+                $c,
+                $exportDetail,
+                $j,
+                ($deleted === 'DELETED') ? 'ANNULLATA' : ''
+            ];
+
+            // Add payment method values
+            $idx = 0;
+            foreach ($this->payments as $p) {
+                if ($idx >= count($letters) - 6) break; // Leave room for basic columns
+
+                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"]) : "";
+                    $rowData[] = $inValue;
+                    $rowData[] = $outValue;
+                } else {
+                    $rowData[] = "";
+                    $rowData[] = "";
+                }
+                $idx += 2;
+            }
+
+            $batch[] = $rowData;
+            $batchCount++;
+
+            // Process batch when it reaches batch size
+            if ($batchCount >= $batchSize) {
+                $this->writeBatchToWorksheet($activeWorksheet, $batch, $rowNum);
+                $rowNum += $batchCount;
+                $batch = [];
+                $batchCount = 0;
+                gc_collect_cycles(); // Force garbage collection
+            }
+        }
+
+        // Process remaining batch
+        if (!empty($batch)) {
+            $this->writeBatchToWorksheet($activeWorksheet, $batch, $rowNum);
+        }
+    }
+
+    // Write batch data efficiently
+    private function writeBatchToWorksheet($activeWorksheet, $batch, $startRow)
+    {
+        if (empty($batch)) return;
+
+        try {
+            // Use fromArray for much faster bulk insertion
+            $activeWorksheet->fromArray($batch, null, 'A' . $startRow, true);
+
+            // Apply conditional formatting for deleted records
+            foreach ($batch as $index => $row) {
+                $currentRow = $startRow + $index;
+                if (isset($row[4]) && $row[4] === 'ANNULLATA') {
+                    $activeWorksheet->getStyle('E' . $currentRow)
+                        ->getFont()->getColor()->setARGB('FFFF0000');
+                }
+            }
+        } catch (\Exception $e) {
+            Log::error('Error writing batch to worksheet', [
+                'error' => $e->getMessage(),
+                'batch_size' => count($batch),
+                'start_row' => $startRow
+            ]);
+
+            // Fallback to individual cell setting
+            foreach ($batch as $index => $row) {
+                $currentRow = $startRow + $index;
+                foreach ($row as $colIndex => $value) {
+                    $col = $this->getColumnLetter($colIndex);
+                    $activeWorksheet->setCellValue($col . $currentRow, $value);
+                }
+            }
+        }
+    }
+
+    // Helper to get column letter by index
+    private function getColumnLetter($index)
+    {
+        if ($index < 26) {
+            return chr(65 + $index);
+        } else {
+            return 'A' . chr(65 + ($index - 26));
+        }
+    }
+
+    // Build totals row efficiently
+    private function buildTotalsRow($activeWorksheet, $letters)
+    {
+        $totalRows = count($this->exportData) + 3; // +3 for headers and spacing
+        $totalRow = $totalRows + 1;
+
+        $totalsData = ['TOTALE', '', '', '', ''];
+
+        $idx = 0;
+        foreach ($this->payments as $p) {
+            if ($idx >= count($letters) - 6) break;
+
+            if (isset($this->exportTotals[$p['name']])) {
+                if ($p['type'] == 'ALL') {
+                    $totalsData[] = $this->formatPrice($this->exportTotals[$p['name']]["IN"] ?? 0);
+                    $totalsData[] = $this->formatPrice($this->exportTotals[$p['name']]["OUT"] ?? 0);
+                } elseif ($p['type'] == 'IN') {
+                    $totalsData[] = $this->formatPrice($this->exportTotals[$p['name']]["IN"] ?? 0);
+                    $totalsData[] = "";
+                } elseif ($p['type'] == 'OUT') {
+                    $totalsData[] = "";
+                    $totalsData[] = $this->formatPrice($this->exportTotals[$p['name']]["OUT"] ?? 0);
+                }
+            } else {
+                $totalsData[] = "0,00";
+                $totalsData[] = "0,00";
+            }
+            $idx += 2;
+        }
+
+        // Write totals row
+        $activeWorksheet->fromArray([$totalsData], null, 'A' . $totalRow, true);
+    }
+
+    // Apply styles more efficiently
+    private function applyStylesEfficiently($activeWorksheet, $letters)
+    {
+        $maxCol = min(count($letters) - 1, count($this->payments) * 2 + 4);
+        $lastCol = $letters[$maxCol];
+        $totalRows = count($this->exportData) + 4; // +4 for headers, spacing, and totals
+
+        // Apply header styles
+        $headerRange = 'A1:' . $lastCol . '2';
+        $activeWorksheet->getStyle($headerRange)->getFont()->setBold(true);
+
+        // Apply header background
+        $activeWorksheet->getStyle('A1:' . $lastCol . '1')
+            ->getFill()
+            ->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
+            ->getStartColor()->setARGB('FF0C6197');
+
+        $activeWorksheet->getStyle('A1:' . $lastCol . '1')
+            ->getFont()->getColor()->setARGB('FFFFFFFF');
+
+        // Apply totals row styles
+        $totalsRange = 'A' . $totalRows . ':' . $lastCol . $totalRows;
+        $activeWorksheet->getStyle($totalsRange)->getFont()->setBold(true);
+        $activeWorksheet->getStyle($totalsRange)
+            ->getFill()
+            ->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
+            ->getStartColor()->setARGB('FFF0F0F0');
+    }
+
+    // Set column dimensions efficiently
+    private function setColumnDimensions($activeWorksheet, $letters)
+    {
+        $dimensions = [
+            'A' => 15,
+            'B' => 25,
+            'C' => 30,
+            'D' => 25,
+            'E' => 15
+        ];
+
+        foreach ($dimensions as $col => $width) {
+            $activeWorksheet->getColumnDimension($col)->setWidth($width);
+        }
+
+        // Set payment method column widths
+        for ($i = 5; $i < count($letters) && $i < 50; $i++) { // Limit to prevent excessive loops
+            $activeWorksheet->getColumnDimension($letters[$i])->setWidth(15);
+        }
+    }
+    /**
+     * 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);
+    }
+
+    public function executeWithTimeoutMonitoring($callback, $description = 'Operation')
+    {
+        $startTime = microtime(true);
+        $maxExecutionTime = ini_get('max_execution_time');
+
+        Log::info("Starting monitored operation: {$description}", [
+            'start_time' => $startTime,
+            'max_execution_time' => $maxExecutionTime,
+            'memory_start' => memory_get_usage(true)
+        ]);
+
+        try {
+            // Execute every 5 seconds to monitor progress
+            $lastCheck = $startTime;
+            $result = null;
+
+            // For non-blocking operations, we can't easily interrupt,
+            // but we can log progress
+            register_tick_function(function () use ($startTime, $maxExecutionTime, $description, &$lastCheck) {
+                $currentTime = microtime(true);
+                if ($currentTime - $lastCheck >= 5) { // Log every 5 seconds
+                    $elapsed = $currentTime - $startTime;
+                    $remaining = $maxExecutionTime > 0 ? $maxExecutionTime - $elapsed : 'unlimited';
+
+                    Log::info("Operation progress: {$description}", [
+                        'elapsed_time' => $elapsed,
+                        'remaining_time' => $remaining,
+                        'memory_current' => memory_get_usage(true),
+                        'memory_peak' => memory_get_peak_usage(true)
+                    ]);
+
+                    if ($maxExecutionTime > 0 && $elapsed > ($maxExecutionTime * 0.8)) {
+                        Log::warning("Operation approaching timeout: {$description}", [
+                            'elapsed_time' => $elapsed,
+                            'max_time' => $maxExecutionTime,
+                            'percentage_used' => ($elapsed / $maxExecutionTime) * 100
+                        ]);
+                    }
+
+                    $lastCheck = $currentTime;
+                }
+            });
+
+            declare(ticks=1000);
+            $result = $callback();
+
+            $totalTime = microtime(true) - $startTime;
+            Log::info("Operation completed successfully: {$description}", [
+                'total_time' => $totalTime,
+                'memory_peak' => memory_get_peak_usage(true)
+            ]);
+
+            return $result;
+        } catch (\Exception $e) {
+            $totalTime = microtime(true) - $startTime;
+            Log::error("Operation failed: {$description}", [
+                'error' => $e->getMessage(),
+                'total_time' => $totalTime,
+                'memory_peak' => memory_get_peak_usage(true),
+                'trace' => $e->getTraceAsString()
+            ]);
+            throw $e;
+        }
+    }
+
+    private function checkTimeoutRisk($operationName, $startTime = null)
+    {
+        if ($startTime === null) {
+            $startTime = $_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(true);
+        }
+
+        $maxExecutionTime = ini_get('max_execution_time');
+        if ($maxExecutionTime <= 0) {
+            return false; // No limit set
+        }
+
+        $elapsed = microtime(true) - $startTime;
+        $remaining = $maxExecutionTime - $elapsed;
+        $percentageUsed = ($elapsed / $maxExecutionTime) * 100;
+
+        Log::info("Timeout check: {$operationName}", [
+            'elapsed_time' => $elapsed,
+            'remaining_time' => $remaining,
+            'percentage_used' => $percentageUsed,
+            'memory_usage' => memory_get_usage(true)
+        ]);
+
+        if ($percentageUsed > 80) {
+            Log::warning("High timeout risk detected: {$operationName}", [
+                'elapsed_time' => $elapsed,
+                'remaining_time' => $remaining,
+                'percentage_used' => $percentageUsed
+            ]);
+            return true;
+        }
+
+        return false;
+    }
+
+    // SOLUTION 8: Log configuration and environment info at start
+
+    public function logEnvironmentInfo()
+    {
+        Log::info('=== EXPORT ENVIRONMENT INFO ===', [
+            'php_version' => PHP_VERSION,
+            'memory_limit' => ini_get('memory_limit'),
+            'max_execution_time' => ini_get('max_execution_time'),
+            'max_input_time' => ini_get('max_input_time'),
+            'post_max_size' => ini_get('post_max_size'),
+            'upload_max_filesize' => ini_get('upload_max_filesize'),
+            'default_socket_timeout' => ini_get('default_socket_timeout'),
+            'current_memory_usage' => memory_get_usage(true),
+            'current_memory_peak' => memory_get_peak_usage(true),
+            'server_time' => date('Y-m-d H:i:s'),
+            'timezone' => date_default_timezone_get(),
+            'sapi_name' => php_sapi_name(),
+            'loaded_extensions' => get_loaded_extensions()
+        ]);
+    }
+}

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

+ 416 - 0
app/Services/RecordFileService.php

@@ -0,0 +1,416 @@
+<?php
+
+namespace App\Services;
+
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Str;
+use Illuminate\Http\UploadedFile;
+
+class RecordFileService
+{
+    /**
+     * Get client name from session, fallback to 'default'
+     */
+    private function getClientName()
+    {
+        $clientName = session('clientName', 'iao');
+
+        $clientName = Str::slug($clientName, '_');
+
+        Log::info("Using client name for folders: {$clientName}");
+        return $clientName;
+    }
+
+    /**
+     * Create record folders with client structure
+     */
+    public function createRecordFolders($recordId, $type)
+    {
+        $clientName = 'iao';
+        $type = strtolower($type);
+
+        Log::info("Preparing S3 structure for client: {$clientName}, record {$recordId}, type: {$type}");
+        $folderPath = "{$clientName}/records/{$type}/{$recordId}/attachments";
+        Log::info("S3 folder structure: {$folderPath}");
+
+        return true;
+    }
+
+    /**
+     * Store file temporarily with client structure
+     */
+    public function storeTemporarily($uploadedFile)
+    {
+        try {
+            $clientName = 'iao';
+            $extension = $uploadedFile->getClientOriginalExtension();
+            $fileName = time() . '_' . Str::random(10) . '.' . $extension;
+            $tempPath = "{$clientName}/temp/uploads/{$fileName}";
+
+            Log::info("=== STORING FILE TEMPORARILY ===");
+            Log::info("Client: {$clientName}");
+            Log::info("Original filename: " . $uploadedFile->getClientOriginalName());
+            Log::info("File size: " . $uploadedFile->getSize() . " bytes");
+            Log::info("Temp path: {$tempPath}");
+
+            try {
+                $storedPath = Storage::disk('s3')->putFileAs("{$clientName}/temp/uploads", $uploadedFile, $fileName);
+                Log::info("Method 1 success - putFileAs returned: {$storedPath}");
+
+                if (Storage::disk('s3')->exists($tempPath)) {
+                    $storedSize = Storage::disk('s3')->size($tempPath);
+                    Log::info("File verification successful - size: {$storedSize} bytes");
+
+                    if ($storedSize === $uploadedFile->getSize()) {
+                        Log::info("File sizes match perfectly");
+                        return $tempPath;
+                    } else {
+                        Log::warning("⚠ File size mismatch - Original: {$uploadedFile->getSize()}, Stored: {$storedSize}");
+                        return $tempPath;
+                    }
+                } else {
+                    throw new \Exception("File not found after putFileAs");
+                }
+            } catch (\Exception $e) {
+                Log::warning("Method 1 failed: " . $e->getMessage());
+            }
+
+            try {
+                Log::info("Trying Method 2: put with file contents");
+                $fileContent = file_get_contents($uploadedFile->getRealPath());
+
+                if (!$fileContent) {
+                    throw new \Exception("Could not read file contents");
+                }
+
+                $stored = Storage::disk('s3')->put($tempPath, $fileContent);
+
+                if ($stored && Storage::disk('s3')->exists($tempPath)) {
+                    Log::info("Method 2 success - put with contents");
+                    return $tempPath;
+                } else {
+                    throw new \Exception("Put method failed");
+                }
+            } catch (\Exception $e) {
+                Log::warning("Method 2 failed: " . $e->getMessage());
+            }
+
+            throw new \Exception("All temp storage methods failed");
+
+        } catch (\Exception $e) {
+            Log::error("Error storing file temporarily: " . $e->getMessage());
+            Log::error("Stack trace: " . $e->getTraceAsString());
+            throw $e;
+        }
+    }
+
+    /**
+     * Upload attachment directly to final S3 location with client structure
+     */
+    public function uploadAttachment($file, $recordId, $type)
+    {
+        try {
+            $clientName = $this->getClientName();
+            $type = strtolower($type);
+            $extension = $file->getClientOriginalExtension();
+            $fileName = time() . '_' . Str::random(10) . '.' . $extension;
+            $finalPath = "{$clientName}/records/{$type}/{$recordId}/attachments/{$fileName}";
+
+            Log::info("Uploading attachment to S3:");
+            Log::info("- Client: {$clientName}");
+            Log::info("- Record ID: {$recordId}");
+            Log::info("- Type: {$type}");
+            Log::info("- File path: {$finalPath}");
+            Log::info("- File size: " . $file->getSize() . " bytes");
+
+            $storedPath = Storage::disk('s3')->putFileAs(
+                "{$clientName}/records/{$type}/{$recordId}/attachments",
+                $file,
+                $fileName
+            );
+
+            Log::info("File uploaded successfully to S3: {$storedPath}");
+
+            if (Storage::disk('s3')->exists($finalPath)) {
+                Log::info("S3 upload verified successfully");
+                return $finalPath;
+            } else {
+                throw new \Exception("File verification failed - not found on S3");
+            }
+
+        } catch (\Exception $e) {
+            Log::error("Error uploading attachment to S3: " . $e->getMessage());
+            throw $e;
+        }
+    }
+
+    /**
+     * Upload XML receipt for import functionality with client structure
+     */
+    public function uploadXmlReceipt($file, $recordId, $type)
+    {
+        try {
+            $clientName = $this->getClientName();
+            $type = strtolower($type);
+            $extension = $file->getClientOriginalExtension() ?: 'xml';
+            $fileName = 'receipt_' . time() . '_' . Str::random(8) . '.' . $extension;
+            $finalPath = "{$clientName}/records/{$type}/{$recordId}/attachments/{$fileName}";
+
+            Log::info("Uploading XML receipt to S3:");
+            Log::info("- Client: {$clientName}");
+            Log::info("- Path: {$finalPath}");
+
+            $storedPath = Storage::disk('s3')->putFileAs(
+                "{$clientName}/records/{$type}/{$recordId}/attachments",
+                $file,
+                $fileName
+            );
+
+            Log::info("XML receipt uploaded to S3: {$storedPath}");
+            return $finalPath;
+
+        } catch (\Exception $e) {
+            Log::error("Error uploading XML receipt to S3: " . $e->getMessage());
+            throw $e;
+        }
+    }
+
+    /**
+     * Get S3 attachment URL
+     */
+    public function getAttachmentUrl($filePath)
+    {
+        try {
+            if (!$filePath) {
+                return null;
+            }
+
+            Log::info("Getting S3 attachment URL for: {$filePath}");
+
+            if (!Storage::disk('s3')->exists($filePath)) {
+                Log::warning("S3 attachment file not found: {$filePath}");
+
+                $directory = dirname($filePath);
+                try {
+                    $files = Storage::disk('s3')->files($directory);
+                    Log::info("Files in S3 directory {$directory}: " . json_encode($files));
+                } catch (\Exception $e) {
+                    Log::warning("Could not list S3 directory {$directory}: " . $e->getMessage());
+                }
+
+                return null;
+            }
+
+            $url = Storage::disk('s3')->temporaryUrl($filePath, now()->addHours(1));
+            Log::info("Generated S3 temporary URL for: {$filePath}");
+            return $url;
+
+        } catch (\Exception $e) {
+            Log::error("Error getting S3 attachment URL for {$filePath}: " . $e->getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * Delete attachment from S3
+     */
+    public function deleteAttachment($filePath)
+    {
+        try {
+            if (!$filePath) {
+                return false;
+            }
+
+            Log::info("Deleting S3 attachment: {$filePath}");
+
+            if (Storage::disk('s3')->exists($filePath)) {
+                $deleted = Storage::disk('s3')->delete($filePath);
+                if ($deleted) {
+                    Log::info("S3 attachment deleted successfully: {$filePath}");
+                    return true;
+                } else {
+                    Log::error("Failed to delete S3 attachment: {$filePath}");
+                    return false;
+                }
+            } else {
+                Log::warning("S3 attachment not found for deletion: {$filePath}");
+                return false;
+            }
+        } catch (\Exception $e) {
+            Log::error("Error deleting S3 attachment: " . $e->getMessage());
+            throw $e;
+        }
+    }
+
+    /**
+     * Debug S3 configuration and connectivity
+     */
+    public function debugFileSystem()
+    {
+        $clientName = $this->getClientName();
+
+        Log::info("=== S3 DEBUG ===");
+        Log::info("Client Name: {$clientName}");
+        Log::info("S3 Configuration:");
+        Log::info("- Bucket: " . config('filesystems.disks.s3.bucket'));
+        Log::info("- Region: " . config('filesystems.disks.s3.region'));
+        Log::info("- URL: " . config('filesystems.disks.s3.url'));
+        Log::info("- Key: " . (config('filesystems.disks.s3.key') ? 'Set' : 'Not set'));
+        Log::info("- Secret: " . (config('filesystems.disks.s3.secret') ? 'Set' : 'Not set'));
+
+        try {
+            $testFile = "{$clientName}/test_connection_" . time() . '.txt';
+            $testContent = 'S3 connection test: ' . now();
+
+            Log::info("Testing S3 connection with client structure...");
+            Storage::disk('s3')->put($testFile, $testContent);
+
+            if (Storage::disk('s3')->exists($testFile)) {
+                Log::info("S3 connection test: SUCCESS");
+                Storage::disk('s3')->delete($testFile);
+            } else {
+                Log::error("S3 connection test: FAILED - file not found after upload");
+            }
+        } catch (\Exception $e) {
+            Log::error("S3 connection test: FAILED - " . $e->getMessage());
+        }
+
+        Log::info("=== END S3 DEBUG ===");
+    }
+
+    /**
+     * Clean up old temp files from S3 for specific client or all clients
+     */
+    public function cleanupTempFiles($olderThanHours = 24, $specificClient = null)
+    {
+        try {
+            $clientName = $specificClient ?: $this->getClientName();
+            $tempPath = $specificClient ? "{$specificClient}/temp/uploads" : "{$clientName}/temp/uploads";
+
+            $tempFiles = Storage::disk('s3')->files($tempPath);
+            $cutoffTime = now()->subHours($olderThanHours);
+            $deletedCount = 0;
+
+            Log::info("Cleaning up S3 temp files for client '{$clientName}' older than {$olderThanHours} hours");
+
+            foreach ($tempFiles as $file) {
+                $fileTime = Storage::disk('s3')->lastModified($file);
+                if ($fileTime < $cutoffTime->timestamp) {
+                    if (Storage::disk('s3')->delete($file)) {
+                        $deletedCount++;
+                        Log::info("Cleaned up old S3 temp file: {$file}");
+                    }
+                }
+            }
+
+            Log::info("S3 cleanup completed for client '{$clientName}'. Deleted {$deletedCount} temp files.");
+
+        } catch (\Exception $e) {
+            Log::error("Error cleaning up S3 temp files: " . $e->getMessage());
+        }
+    }
+
+    /**
+     * Clean up temp files for all clients
+     */
+    public function cleanupAllClientTempFiles($olderThanHours = 24)
+    {
+        try {
+            $allDirectories = Storage::disk('s3')->directories('');
+            $deletedCount = 0;
+
+            Log::info("Cleaning up temp files for all clients older than {$olderThanHours} hours");
+
+            foreach ($allDirectories as $clientDir) {
+                $tempPath = "{$clientDir}/temp/uploads";
+
+                if (Storage::disk('s3')->exists($tempPath)) {
+                    $tempFiles = Storage::disk('s3')->files($tempPath);
+                    $cutoffTime = now()->subHours($olderThanHours);
+
+                    foreach ($tempFiles as $file) {
+                        $fileTime = Storage::disk('s3')->lastModified($file);
+                        if ($fileTime < $cutoffTime->timestamp) {
+                            if (Storage::disk('s3')->delete($file)) {
+                                $deletedCount++;
+                                Log::info("Cleaned up old S3 temp file: {$file}");
+                            }
+                        }
+                    }
+                }
+            }
+
+            Log::info("S3 cleanup completed for all clients. Deleted {$deletedCount} temp files.");
+
+        } catch (\Exception $e) {
+            Log::error("Error cleaning up all client temp files: " . $e->getMessage());
+        }
+    }
+
+    /**
+     * Check if a file exists on S3
+     */
+    public function fileExists($filePath)
+    {
+        try {
+            return Storage::disk('s3')->exists($filePath);
+        } catch (\Exception $e) {
+            Log::error("Error checking if S3 file exists: " . $e->getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * Get file size from S3
+     */
+    public function getFileSize($filePath)
+    {
+        try {
+            if (Storage::disk('s3')->exists($filePath)) {
+                return Storage::disk('s3')->size($filePath);
+            }
+            return 0;
+        } catch (\Exception $e) {
+            Log::error("Error getting S3 file size: " . $e->getMessage());
+            return 0;
+        }
+    }
+
+    /**
+     * Get file last modified time from S3
+     */
+    public function getFileLastModified($filePath)
+    {
+        try {
+            if (Storage::disk('s3')->exists($filePath)) {
+                return Storage::disk('s3')->lastModified($filePath);
+            }
+            return null;
+        } catch (\Exception $e) {
+            Log::error("Error getting S3 file last modified: " . $e->getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * Get all files for a specific client and type
+     */
+    public function getClientFiles($type = null, $clientName = null)
+    {
+        try {
+            $clientName = $clientName ?: $this->getClientName();
+            $type = $type ? strtolower($type) : '*';
+
+            $basePath = $type === '*' ? "{$clientName}/records" : "{$clientName}/records/{$type}";
+
+            $files = Storage::disk('s3')->allFiles($basePath);
+            Log::info("Found " . count($files) . " files for client '{$clientName}' and type '{$type}'");
+
+            return $files;
+        } catch (\Exception $e) {
+            Log::error("Error getting client files: " . $e->getMessage());
+            return [];
+        }
+    }
+}

+ 36 - 0
database/migrations/2025_06_13_092624_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>

+ 53 - 16
resources/views/livewire/member.blade.php

@@ -896,6 +896,7 @@
                                                                         <div class="col-md-6">
                                                                             <label for="certificate_type" class="form-label">Tipo</label>
                                                                             <select id="certificate_type" class="form-select certificate_type" aria-label="Tessera" wire:model="certificate_type">
+                                                                                <option value="">--Seleziona--</option>
                                                                                 <option value="N">Non agonistico
                                                                                 <option value="A">Agonistico
                                                                             </select>
@@ -1130,10 +1131,14 @@
                                 @endif
 
                                 @if($type == 'corsi')
-
                                     @if($dataId > 0)
 
                                         @if(!$addCourse && !$updateCourse)
+                                            @if($active["status"] != 2 && $active["status"] != 1 )
+                                                <div class="alert alert-warning" role="alert">
+                                                    <strong>Attenzione:</strong> Per aggiungere corsi è necessario avere un tesseramento.
+                                                </div>
+                                            @endif
                                             <table class="table tablesaw tableHead tablesaw-stack tabella--corsi" id="tablesaw-350-2" style="min-width:800px">
                                                 <tbody id="checkall-target">
                                                     @foreach($member_courses as $member_course)
@@ -1224,9 +1229,15 @@
 
                                                 </tbody>
                                             </table>
-                                            <button class="btn--ui primary"wire:click.prevent="addCourse()" style="max-width:200px">Aggiungi</button>
+                                            @if($dataId > 0)
+                                                @if ($active["status"] == 2 || $active["status"] == 1)
+                                                    <button class="btn--ui primary" wire:click.prevent="addCourse()" style="max-width:200px">Aggiungi</button>
+                                                @else
+                                                    <button class="btn--ui lightGrey" disabled style="max-width:200px">Aggiungi corso (non disponibile)</button>
+                                                    <small class="text-muted d-block mt-1">Completa prima il tesseramento per abilitare l'aggiunta di corsi</small>
+                                                @endif
+                                            @endif
                                         @else
-
                                             <div class="form--wrapper">
                                                 <form class="form--utente">
                                                     @if(false)
@@ -1553,19 +1564,6 @@
                                                 </div>
                                             @endif
                                         @endforeach
-                                        <!--
-                                        <div class="resume--wrapper d-flex align-items-start justify-content-between mb-2">
-                                            <div class="resume--info d-flex align-items-start">
-                                                <i class="ico--ui tessera me-2"></i>
-                                                <div class="title--tessera_added">
-                                                    <h4>Tennis/Corso Tennis/Adulti/Bisettimanale</h4>
-                                                    <span class="title-detail">Iscrizione: <span class="title-detail_date">12 marzo 2022</span></span></small>
-                                                </div>
-                                            </div>
-
-                                            <span class="badge tessera-badge active">attiva</span>
-                                        </div>-->
-
                                     </div>
                                     <div class="resume--tab_info tesseramento">
                                         <h2 class="mb-3">Certificato</h2>
@@ -2704,5 +2702,44 @@
             });
         });
 
+
+               function getNextTab(currentTab) {
+            const tabs = ['dati', 'tesseramento', 'corsi', 'gruppi'];
+            const currentIndex = tabs.indexOf(currentTab);
+            return currentIndex < tabs.length - 1 ? tabs[currentIndex + 1] : currentTab;
+        }
+
+        function scrollToFormTop() {
+            const formSection = document.querySelector('.section--tab, .form--wrapper, #card--resume');
+            if (formSection) {
+                formSection.scrollIntoView({
+                    behavior: 'instant',
+                    block: 'start',
+                    inline: 'nearest'
+                });
+                return true;
+            }
+            return false;
+        }
+
+        Livewire.on('saved-and-continue', (currentTab) => {
+            const nextTab = getNextTab(currentTab);
+            if (nextTab !== currentTab) {
+                @this.change(nextTab);
+            }
+
+            setTimeout(() => {
+                if (!scrollToFormTop()) {
+                    window.scrollTo({ top: 0, behavior: 'instant' });
+                    document.body.scrollTop = 0;
+                    document.documentElement.scrollTop = 0;
+                }
+                const firstInput = document.querySelector('#first_name');
+                if (firstInput) {
+                    firstInput.focus();
+                }
+            }, 200);
+        });
+
     </script>
 @endpush

Разница между файлами не показана из-за своего большого размера
+ 1451 - 518
resources/views/livewire/records.blade.php


Некоторые файлы не были показаны из-за большого количества измененных файлов