Luca Parisio 2 місяців тому
коміт
a02c2dee17
100 змінених файлів з 22021 додано та 0 видалено
  1. BIN
      .DS_Store
  2. 18 0
      .editorconfig
  3. 75 0
      .env.ciccio
  4. 58 0
      .env.example
  5. 11 0
      .gitattributes
  6. 18 0
      .gitignore
  7. 66 0
      README.md
  8. BIN
      app/.DS_Store
  9. 125 0
      app/Console/Commands/CleanupPasswordResets.php
  10. 28 0
      app/Console/Commands/DispatchDueEmails.php
  11. 26 0
      app/Console/Commands/MigrateMaster.php
  12. 30 0
      app/Console/Commands/MigrateTenants.php
  13. 102 0
      app/Console/Commands/ProcessScheduledEmailCommand.php
  14. 63 0
      app/Console/Commands/SendSms.php
  15. 65 0
      app/Console/Commands/SetMemberDatas.php
  16. 35 0
      app/Console/Kernel.php
  17. 17 0
      app/Database/Migrations/MasterMigration.php
  18. 17 0
      app/Database/Migrations/TenantMigration.php
  19. 64 0
      app/Events/ExportCompleted.php
  20. 58 0
      app/Events/ExportFailed.php
  21. 50 0
      app/Exceptions/Handler.php
  22. BIN
      app/Http/.DS_Store
  23. 13 0
      app/Http/Controllers/Controller.php
  24. 29 0
      app/Http/Controllers/FileUpload.php
  25. 222 0
      app/Http/Controllers/FirstLoginController.php
  26. 482 0
      app/Http/Controllers/PasswordResetController.php
  27. 69 0
      app/Http/Controllers/ReceiptExportController.php
  28. 138 0
      app/Http/Controllers/SmsTemplateController.php
  29. 69 0
      app/Http/Kernel.php
  30. 385 0
      app/Http/Livewire/Azienda.php
  31. 131 0
      app/Http/Livewire/Bank.php
  32. 157 0
      app/Http/Livewire/Card.php
  33. 60 0
      app/Http/Livewire/Categories.php
  34. 132 0
      app/Http/Livewire/Category.php
  35. 268 0
      app/Http/Livewire/Causal.php
  36. 134 0
      app/Http/Livewire/Causals.php
  37. 142 0
      app/Http/Livewire/City.php
  38. 487 0
      app/Http/Livewire/Course.php
  39. 136 0
      app/Http/Livewire/CourseDuration.php
  40. 130 0
      app/Http/Livewire/CourseFrequency.php
  41. 132 0
      app/Http/Livewire/CourseLevel.php
  42. 918 0
      app/Http/Livewire/CourseList.php
  43. 303 0
      app/Http/Livewire/CourseMember.php
  44. 17 0
      app/Http/Livewire/CourseMemberOne.php
  45. 16 0
      app/Http/Livewire/CourseMemberTwo.php
  46. 135 0
      app/Http/Livewire/CourseSubscription.php
  47. 135 0
      app/Http/Livewire/CourseType.php
  48. 94 0
      app/Http/Livewire/Courses.php
  49. 834 0
      app/Http/Livewire/Dashboard.php
  50. 131 0
      app/Http/Livewire/Discipline.php
  51. 429 0
      app/Http/Livewire/EmailComunications.php
  52. 189 0
      app/Http/Livewire/FirstLogin.php
  53. 2889 0
      app/Http/Livewire/Member.php
  54. 127 0
      app/Http/Livewire/Nation.php
  55. 199 0
      app/Http/Livewire/PaymentMethod.php
  56. 243 0
      app/Http/Livewire/Profile.php
  57. 142 0
      app/Http/Livewire/Province.php
  58. 275 0
      app/Http/Livewire/Rate.php
  59. 87 0
      app/Http/Livewire/Receipt.php
  60. 1410 0
      app/Http/Livewire/Record.php
  61. 1443 0
      app/Http/Livewire/RecordIN.php
  62. 811 0
      app/Http/Livewire/RecordINOUT.php
  63. 1907 0
      app/Http/Livewire/RecordOUT.php
  64. 118 0
      app/Http/Livewire/Reminder.php
  65. 727 0
      app/Http/Livewire/Reports.php
  66. 32 0
      app/Http/Livewire/Setting.php
  67. 200 0
      app/Http/Livewire/SmsComunications.php
  68. 397 0
      app/Http/Livewire/Sponsor.php
  69. 359 0
      app/Http/Livewire/Supplier.php
  70. 770 0
      app/Http/Livewire/User.php
  71. 136 0
      app/Http/Livewire/Vat.php
  72. 21 0
      app/Http/Middleware/Authenticate.php
  73. 28 0
      app/Http/Middleware/CheckFirstLoginCompleted.php
  74. 17 0
      app/Http/Middleware/EncryptCookies.php
  75. 25 0
      app/Http/Middleware/PasswordResetThrottle.php
  76. 17 0
      app/Http/Middleware/PreventRequestsDuringMaintenance.php
  77. 32 0
      app/Http/Middleware/RedirectIfAuthenticated.php
  78. 60 0
      app/Http/Middleware/TenantMiddleware.php
  79. 19 0
      app/Http/Middleware/TrimStrings.php
  80. 20 0
      app/Http/Middleware/TrustHosts.php
  81. 28 0
      app/Http/Middleware/TrustProxies.php
  82. 22 0
      app/Http/Middleware/ValidateSignature.php
  83. 17 0
      app/Http/Middleware/VerifyCsrfToken.php
  84. 880 0
      app/Jobs/ExportPrimaNota.php
  85. 255 0
      app/Jobs/ProcessRecordAttachment.php
  86. 74 0
      app/Jobs/SendEmailMessage.php
  87. 32 0
      app/Mail/CustomEmail.php
  88. 66 0
      app/Mail/ExportNotification.php
  89. 58 0
      app/Mail/GenericMail.php
  90. 63 0
      app/Mail/ReceipDeleteEmail.php
  91. 63 0
      app/Mail/ReceipEmail.php
  92. 114 0
      app/Models/Azienda.php
  93. 22 0
      app/Models/Bank.php
  94. 20 0
      app/Models/Card.php
  95. 58 0
      app/Models/Category.php
  96. 80 0
      app/Models/Causal.php
  97. 24 0
      app/Models/City.php
  98. 24 0
      app/Models/Configuration.php
  99. 144 0
      app/Models/Course.php
  100. 23 0
      app/Models/CourseDuration.php

+ 18 - 0
.editorconfig

@@ -0,0 +1,18 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 4
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.{yml,yaml}]
+indent_size = 2
+
+[docker-compose.yml]
+indent_size = 4

+ 75 - 0
.env.ciccio

@@ -0,0 +1,75 @@
+APP_NAME=IaoTeam
+APP_ENV=local
+APP_KEY=base64:4txSOZHKaUplEA5+qjdLBIc1QiASfjwwUTb0NmMVnKY=
+APP_DEBUG=true
+APP_URL=http://127.0.0.1:8000
+
+LOG_CHANNEL=stack
+LOG_DEPRECATIONS_CHANNEL=null
+LOG_LEVEL=debug
+
+DB_CONNECTION=mysql
+DB_HOST=127.0.0.1
+DB_PORT=3306
+DB_DATABASE=iao_team_prod
+DB_USERNAME=root
+DB_PASSWORD=_brUce80!
+
+BROADCAST_DRIVER=log
+CACHE_DRIVER=file
+FILESYSTEM_DISK=local
+QUEUE_CONNECTION=sync
+SESSION_DRIVER=file
+SESSION_LIFETIME=120
+
+MEMCACHED_HOST=127.0.0.1
+
+REDIS_HOST=127.0.0.1
+REDIS_PASSWORD=null
+REDIS_PORT=6379
+
+MAIL_MAILER=smtp
+MAIL_HOST=out.postassl.it
+MAIL_PORT=465
+MAIL_USERNAME=info@iaoteam.itxxx
+MAIL_PASSWORD="Piffero#2012"
+MAIL_ENCRYPTION=ssl
+MAIL_FROM_ADDRESS="noreply@iaoteam.it"
+MAIL_FROM_NAME="${APP_NAME}"
+
+AWS_ACCESS_KEY_ID=
+AWS_SECRET_ACCESS_KEY=
+AWS_DEFAULT_REGION=us-east-1
+AWS_BUCKET=
+AWS_USE_PATH_STYLE_ENDPOINT=false
+
+PUSHER_APP_ID=
+PUSHER_APP_KEY=
+PUSHER_APP_SECRET=
+PUSHER_HOST=
+PUSHER_PORT=443
+PUSHER_SCHEME=https
+PUSHER_APP_CLUSTER=mt1
+
+VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
+VITE_PUSHER_HOST="${PUSHER_HOST}"
+VITE_PUSHER_PORT="${PUSHER_PORT}"
+VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
+VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
+
+LOGO="/img/logo-madonnella.webp"
+RAGIONE_SOCIALE="La Madonnella"
+INDIRIZZO="Via di Villa Grazioli snc 00046"
+LOCALITA="Grottaferrata"
+PROVINCIA="RM"
+CODICE_FISCALE=""
+PARTITA_IVA=""
+
+VAT_MANAGE=0
+FISCAL_YEAR_MONTH_FROM=1
+FISCAL_YEAR_MONTH_TO=12
+
+RECEIPT_MAIL_SUBJECT="Centro Sportivo La Madonnella - Ricevuta di pagamento "
+RECEIPT_DELETE_MAIL_SUBJECT="Centro Sportivo La Madonnella - Annullamento ricevuta di pagamento "
+
+MAIL_CCN="l.parisio@webmagistri.it"

+ 58 - 0
.env.example

@@ -0,0 +1,58 @@
+APP_NAME=Laravel
+APP_ENV=local
+APP_KEY=
+APP_DEBUG=true
+APP_URL=http://localhost
+
+LOG_CHANNEL=stack
+LOG_DEPRECATIONS_CHANNEL=null
+LOG_LEVEL=debug
+
+DB_CONNECTION=mysql
+DB_HOST=127.0.0.1
+DB_PORT=3306
+DB_DATABASE=laravel
+DB_USERNAME=root
+DB_PASSWORD=
+
+BROADCAST_DRIVER=log
+CACHE_DRIVER=file
+FILESYSTEM_DISK=local
+QUEUE_CONNECTION=sync
+SESSION_DRIVER=file
+SESSION_LIFETIME=120
+
+MEMCACHED_HOST=127.0.0.1
+
+REDIS_HOST=127.0.0.1
+REDIS_PASSWORD=null
+REDIS_PORT=6379
+
+MAIL_MAILER=smtp
+MAIL_HOST=mailpit
+MAIL_PORT=1025
+MAIL_USERNAME=null
+MAIL_PASSWORD=null
+MAIL_ENCRYPTION=null
+MAIL_FROM_ADDRESS="hello@example.com"
+MAIL_FROM_NAME="${APP_NAME}"
+
+AWS_ACCESS_KEY_ID=
+AWS_SECRET_ACCESS_KEY=
+AWS_DEFAULT_REGION=us-east-1
+AWS_BUCKET=
+AWS_USE_PATH_STYLE_ENDPOINT=false
+
+PUSHER_APP_ID=
+PUSHER_APP_KEY=
+PUSHER_APP_SECRET=
+PUSHER_HOST=
+PUSHER_PORT=443
+PUSHER_SCHEME=https
+PUSHER_APP_CLUSTER=mt1
+
+VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
+VITE_PUSHER_HOST="${PUSHER_HOST}"
+VITE_PUSHER_PORT="${PUSHER_PORT}"
+VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
+VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

+ 11 - 0
.gitattributes

@@ -0,0 +1,11 @@
+* text=auto
+
+*.blade.php diff=html
+*.css diff=css
+*.html diff=html
+*.md diff=markdown
+*.php diff=php
+
+/.github export-ignore
+CHANGELOG.md export-ignore
+.styleci.yml export-ignore

+ 18 - 0
.gitignore

@@ -0,0 +1,18 @@
+/node_modules
+/public/build
+/public/hot
+/public/storage
+/storage/*.key
+/vendor
+.env
+.env.backup
+.env.production
+.phpunit.result.cache
+Homestead.json
+Homestead.yaml
+auth.json
+npm-debug.log
+yarn-error.log
+/.fleet
+/.idea
+/.vscode

+ 66 - 0
README.md

@@ -0,0 +1,66 @@
+<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
+
+<p align="center">
+<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
+<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
+<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
+<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
+</p>
+
+## About Laravel
+
+Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
+
+- [Simple, fast routing engine](https://laravel.com/docs/routing).
+- [Powerful dependency injection container](https://laravel.com/docs/container).
+- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
+- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
+- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
+- [Robust background job processing](https://laravel.com/docs/queues).
+- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
+
+Laravel is accessible, powerful, and provides tools required for large, robust applications.
+
+## Learning Laravel
+
+Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
+
+You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
+
+If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains over 2000 video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
+
+## Laravel Sponsors
+
+We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the Laravel [Patreon page](https://patreon.com/taylorotwell).
+
+### Premium Partners
+
+- **[Vehikl](https://vehikl.com/)**
+- **[Tighten Co.](https://tighten.co)**
+- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
+- **[64 Robots](https://64robots.com)**
+- **[Cubet Techno Labs](https://cubettech.com)**
+- **[Cyber-Duck](https://cyber-duck.co.uk)**
+- **[Many](https://www.many.co.uk)**
+- **[Webdock, Fast VPS Hosting](https://www.webdock.io/en)**
+- **[DevSquad](https://devsquad.com)**
+- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
+- **[OP.GG](https://op.gg)**
+- **[WebReinvent](https://webreinvent.com/?utm_source=laravel&utm_medium=github&utm_campaign=patreon-sponsors)**
+- **[Lendio](https://lendio.com)**
+
+## Contributing
+
+Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
+
+## Code of Conduct
+
+In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
+
+## Security Vulnerabilities
+
+If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
+
+## License
+
+The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).


+ 125 - 0
app/Console/Commands/CleanupPasswordResets.php

@@ -0,0 +1,125 @@
+<?php
+
+namespace App\Console\Commands;
+
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\DB;
+use Carbon\Carbon;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Facades\Log;
+
+class CleanupPasswordResets extends Command
+{
+    protected $signature = 'password:cleanup';
+    protected $description = 'Clean up expired password reset tokens';
+
+    public function handle()
+    {
+        try {
+            $masterConfig = [
+                'driver' => 'mysql',
+                'host' => env('DB_HOST', '127.0.0.1'),
+                'port' => env('DB_PORT', '3306'),
+                'database' => env('DB_DATABASE'),
+                'username' => env('DB_USERNAME'),
+                'password' => env('DB_PASSWORD'),
+                'charset' => 'utf8mb4',
+                'collation' => 'utf8mb4_unicode_ci',
+                'prefix' => '',
+                'strict' => true,
+                'engine' => null,
+            ];
+
+            config(['database.connections.master_cleanup' => $masterConfig]);
+
+            // Delete tokens older than 24 hours
+            $deletedCount = DB::connection('master_cleanup')
+                ->table('password_resets')
+                ->where('created_at', '<', Carbon::now()->subHours(24))
+                ->delete();
+
+            DB::purge('master_cleanup');
+
+            $this->info("Cleaned up {$deletedCount} expired password reset tokens.");
+
+        } catch (\Exception $e) {
+            $this->error("Failed to cleanup password reset tokens: " . $e->getMessage());
+        }
+    }
+}
+
+// ENHANCED AUTHENTICATION FUNCTION
+// Update your existing authentication function to handle password resets
+
+function authenticateUser($email, $password)
+{
+    try {
+        // Step 1: Get user from master database (current default connection)
+        $masterUser = DB::table('users')->where('email', $email)->first();
+
+        if (!$masterUser) {
+            Log::info('User not found in master database', ['email' => $email]);
+            return false;
+        }
+
+        // Step 2: Check password in master database
+        if (!Hash::check($password, $masterUser->password)) {
+            Log::info('Password incorrect in master database', ['email' => $email]);
+            return false;
+        }
+
+        // Step 3: Set up tenant connection
+        $tenantConfig = [
+            'driver' => 'mysql',
+            'host' => env('DB_HOST', '127.0.0.1'),
+            'port' => env('DB_PORT', '3306'),
+            'database' => $masterUser->tenant_database,
+            'username' => $masterUser->tenant_username,
+            'password' => $masterUser->tenant_password,
+            'charset' => 'utf8mb4',
+            'collation' => 'utf8mb4_unicode_ci',
+            'prefix' => '',
+            'strict' => true,
+            'engine' => null,
+        ];
+
+        // Add tenant connection to config
+        config(['database.connections.tenant_check' => $tenantConfig]);
+
+        // Step 4: Check user in tenant database
+        $tenantUser = DB::connection('tenant_check')->table('users')->where('email', $email)->first();
+
+        if (!$tenantUser) {
+            Log::info('User not found in tenant database', [
+                'email' => $email,
+                'tenant_db' => $masterUser->tenant_database
+            ]);
+            return false;
+        }
+
+        // Step 5: Check password in tenant database
+        if (!Hash::check($password, $tenantUser->password)) {
+            Log::info('Password incorrect in tenant database', [
+                'email' => $email,
+                'tenant_db' => $masterUser->tenant_database
+            ]);
+            return false;
+        }
+
+        Log::info('Authentication successful in both databases', [
+            'email' => $email,
+            'tenant_db' => $masterUser->tenant_database
+        ]);
+
+        return $masterUser;
+
+    } catch (\Exception $e) {
+        Log::error('Authentication error', [
+            'email' => $email,
+            'error' => $e->getMessage(),
+            'trace' => $e->getTraceAsString()
+        ]);
+        return false;
+    }
+}

+ 28 - 0
app/Console/Commands/DispatchDueEmails.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Models\EmailMessage;
+use App\Jobs\SendEmailMessage;
+
+class DispatchDueEmails extends Command
+{
+    protected $signature = 'emails:dispatch-due';
+    protected $description = 'Invia le email programmate giunte a scadenza';
+
+    public function handle()
+    {
+        app(\App\Http\Middleware\TenantMiddleware::class)->setupTenantConnection();
+
+        EmailMessage::where('status', 'scheduled')
+            ->where('schedule_at', '<=', now())
+            ->chunkById(100, function ($chunk) {
+                foreach ($chunk as $msg) {
+                    dispatch(new SendEmailMessage($msg->id));
+                }
+            });
+
+        return Command::SUCCESS;
+    }
+}

+ 26 - 0
app/Console/Commands/MigrateMaster.php

@@ -0,0 +1,26 @@
+<?php
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Services\MigrationService;
+
+class MigrateMaster extends Command
+{
+    protected $signature = 'migrate:master {--rollback} {--step=1}';
+    protected $description = 'Run migrations on master database only';
+
+    public function handle(MigrationService $migrationService)
+    {
+        if ($this->option('rollback')) {
+            $result = $migrationService->rollbackMasterMigrations($this->option('step'));
+            $this->info($result['message']);
+            $this->line($result['output']);
+        } else {
+            $result = $migrationService->runMasterMigrations();
+            $this->info($result['message']);
+            if (!empty($result['migrations'])) {
+                $this->table(['Master Migrations'], array_map(fn($m) => [$m], $result['migrations']));
+            }
+        }
+    }
+}

+ 30 - 0
app/Console/Commands/MigrateTenants.php

@@ -0,0 +1,30 @@
+<?php
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Services\MigrationService;
+
+class MigrateTenants extends Command
+{
+    protected $signature = 'migrate:tenants {--rollback} {--step=1}';
+    protected $description = 'Run migrations on all tenant databases';
+
+    public function handle(MigrationService $migrationService)
+    {
+        if ($this->option('rollback')) {
+            $result = $migrationService->rollbackTenantMigrations($this->option('step'));
+        } else {
+            $result = $migrationService->runTenantMigrations();
+        }
+
+        $this->info($result['message']);
+
+        if (!empty($result['results'])) {
+            $tableData = [];
+            foreach ($result['results'] as $r) {
+                $tableData[] = [$r['tenant'], $r['status'], $r['message']];
+            }
+            $this->table(['Tenant Database', 'Status', 'Message'], $tableData);
+        }
+    }
+}

+ 102 - 0
app/Console/Commands/ProcessScheduledEmailCommand.php

@@ -0,0 +1,102 @@
+<?php
+namespace App\Console\Commands;
+
+use App\Models\EmailScheduled;
+use App\Models\EmailScheduledRecipient;
+use App\Mail\CustomEmail;
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Mail;
+use Carbon\Carbon;
+
+class ProcessScheduledEmailCommand extends Command
+{
+    protected $signature = 'email:process-scheduled';
+    protected $description = 'Process scheduled email messages that are due to be sent';
+
+    public function handle()
+    {
+        $this->info('🔍 Checking for scheduled Email messages...');
+
+        $dueMessages = EmailScheduled::where('status', 'scheduled')
+            ->where('scheduled_at', '<=', now())
+            ->with('recipients')
+            ->get();
+
+        if ($dueMessages->isEmpty()) {
+            $this->info('✅ No scheduled emails due for sending.');
+            return 0;
+        }
+
+        $this->info("📧 Found {$dueMessages->count()} email batches due for sending.");
+
+        foreach ($dueMessages as $message) {
+            $this->info("📤 Processing email batch ID: {$message->id}");
+
+            try {
+                $message->update(['status' => 'sending']);
+
+                $successCount = 0;
+                $failCount = 0;
+
+                foreach ($message->recipients as $recipient) {
+                    if ($recipient->email) {
+                        $recipientRecord = EmailScheduledRecipient::create([
+                            'email_scheduled_id' => $message->id,
+                            'user_id' => $recipient->id,
+                            'email_address' => $recipient->email,
+                            'status' => 'pending'
+                        ]);
+
+                        try {
+                            Mail::to($recipient->email)->send(
+                                new CustomEmail($message->subject, $message->content, $recipient->name)
+                            );
+
+                            $recipientRecord->markAsSent();
+                            $successCount++;
+                            $this->line("   ✅ Sent to {$recipient->name} ({$recipient->email})");
+
+                            usleep(100000);
+
+                        } catch (\Exception $e) {
+                            $recipientRecord->markAsFailed($e->getMessage());
+                            $failCount++;
+                            $this->line("   ❌ Failed to {$recipient->name} ({$recipient->email}): " . $e->getMessage());
+                        }
+                    } else {
+                        $failCount++;
+                        $this->line("   ⚠️  No email address for {$recipient->name}");
+                    }
+                }
+
+                // Update final status
+                $finalStatus = $failCount === 0 ? 'sent' : ($successCount === 0 ? 'failed' : 'sent');
+
+                $message->update([
+                    'status' => $finalStatus,
+                    'delivery_report' => [
+                        'total' => $successCount + $failCount,
+                        'successful' => $successCount,
+                        'failed' => $failCount,
+                        'completed_at' => now()
+                    ]
+                ]);
+
+                $this->info("✅ Email batch {$message->id} completed: {$successCount} sent, {$failCount} failed");
+
+            } catch (\Exception $e) {
+                $message->update([
+                    'status' => 'failed',
+                    'delivery_report' => [
+                        'error' => $e->getMessage(),
+                        'failed_at' => now()
+                    ]
+                ]);
+                $this->error("❌ Failed to process email batch {$message->id}: " . $e->getMessage());
+            }
+        }
+
+        $this->info('🎉 Scheduled email processing completed!');
+        return 0;
+    }
+}

+ 63 - 0
app/Console/Commands/SendSms.php

@@ -0,0 +1,63 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+
+class SendSms extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'send:sms';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Send SMS';
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+
+        $expire_date = date("Y-m-d", strtotime("+1 month"));
+        $expire_date_it = date("d/m/Y", strtotime("+1 month"));
+        $certificates = \App\Models\MemberCertificate::where('expire_date', $expire_date)->get();
+        foreach ($certificates as $certificate) {
+            $phone = $certificate->member->phone;
+            $message = 'Ciao ' . $certificate->member->first_name . ', ci risulta che il tuo certificato medico scade il ' . $expire_date_it . '. Per continuare ad allenarti senza problemi, ricordati di rinnovarlo in tempo. Ti aspettiamo in campo! Centro Sportivo La Madonnella';
+            $params = array(
+                'to'            => '+39' . $phone,
+                'from'          => env('SMS_FROM', 'Test'),
+                'message'       => $message,
+                'format'        => 'json',
+            );
+            sms_send($params);
+        }
+
+        $expire_date = date("Y-m-d", strtotime("+15 days"));
+        $expire_date_it = date("d/m/Y", strtotime("+15 days"));
+        $certificates = \App\Models\MemberCertificate::where('expire_date', $expire_date)->get();
+        foreach ($certificates as $certificate) {
+            $phone = $certificate->member->phone;
+            $message = 'Ciao ' . $certificate->member->first_name . ', ci risulta che il tuo certificato medico scade il ' . $expire_date_it . '. Per continuare ad allenarti senza problemi, ricordati di rinnovarlo in tempo. Ti aspettiamo in campo! Centro Sportivo La Madonnella';
+            $params = array(
+                'to'            => '+39' . $phone,
+                'from'          => env('SMS_FROM', 'Test'),
+                'message'       => $message,
+                'format'        => 'json',
+            );
+            sms_send($params);
+        }
+
+        return Command::SUCCESS;
+    }
+}

+ 65 - 0
app/Console/Commands/SetMemberDatas.php

@@ -0,0 +1,65 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+
+class SetMemberDatas extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'update:data';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Update member data';
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+
+        $members = \App\Models\Member::all();
+
+        foreach($members as $member)
+        {
+
+            $status = $member->getStatus();
+            $status = $status["status"];
+
+            $has_certificate = $member->hasCertificate();
+            $certificate = 0;
+            $certificate_date = null;
+            if($has_certificate["date"] != '')
+            {
+                if($has_certificate["date"] < date("Y-m-d"))
+                    $certificate = 0;
+
+                if($has_certificate["date"] >= date("Y-m-d") && $has_certificate["date"] < date("Y-m-d", strtotime("+1 month")))
+                    $certificate = 1;
+
+                if($has_certificate["date"] >= date("Y-m-d", strtotime("+1 month")))
+                    $certificate = 2;
+
+                $certificate_date = $has_certificate["date"] != '' ? $has_certificate["date"] : null;
+            }
+
+            $member->current_status = $status;
+            $member->certificate = $certificate;
+            $member->certificate_date = $certificate_date;
+            $member->save();
+
+        }
+
+        return Command::SUCCESS;
+    }
+}

+ 35 - 0
app/Console/Kernel.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace App\Console;
+
+use Illuminate\Console\Scheduling\Schedule;
+use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
+
+class Kernel extends ConsoleKernel
+{
+    /**
+     * Define the application's command schedule.
+     *
+     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
+     * @return void
+     */
+    protected function schedule(Schedule $schedule)
+    {
+        $schedule->command('password:cleanup')->dailyAt('02:00');
+
+        // invia email programmate
+        $schedule->command('emails:dispatch-due')->everyMinute();
+    }
+
+    /**
+     * Register the commands for the application.
+     *
+     * @return void
+     */
+    protected function commands()
+    {
+        $this->load(__DIR__ . '/Commands');
+
+        require base_path('routes/console.php');
+    }
+}

+ 17 - 0
app/Database/Migrations/MasterMigration.php

@@ -0,0 +1,17 @@
+<?php
+namespace App\Database\Migrations;
+
+use Illuminate\Database\Migrations\Migration as BaseMigration;
+
+abstract class MasterMigration extends BaseMigration
+{
+    public function getConnection()
+    {
+        return 'master';
+    }
+
+    public function isMasterMigration(): bool
+    {
+        return true;
+    }
+}

+ 17 - 0
app/Database/Migrations/TenantMigration.php

@@ -0,0 +1,17 @@
+<?php
+namespace App\Database\Migrations;
+
+use Illuminate\Database\Migrations\Migration as BaseMigration;
+
+abstract class TenantMigration extends BaseMigration
+{
+    public function getConnection()
+    {
+        return 'tenant';
+    }
+
+    public function isTenantMigration(): bool
+    {
+        return true;
+    }
+}

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

+ 50 - 0
app/Exceptions/Handler.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace App\Exceptions;
+
+use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
+use Throwable;
+
+class Handler extends ExceptionHandler
+{
+    /**
+     * A list of exception types with their corresponding custom log levels.
+     *
+     * @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
+     */
+    protected $levels = [
+        //
+    ];
+
+    /**
+     * A list of the exception types that are not reported.
+     *
+     * @var array<int, class-string<\Throwable>>
+     */
+    protected $dontReport = [
+        //
+    ];
+
+    /**
+     * A list of the inputs that are never flashed to the session on validation exceptions.
+     *
+     * @var array<int, string>
+     */
+    protected $dontFlash = [
+        'current_password',
+        'password',
+        'password_confirmation',
+    ];
+
+    /**
+     * Register the exception handling callbacks for the application.
+     *
+     * @return void
+     */
+    public function register()
+    {
+        $this->reportable(function (Throwable $e) {
+            //
+        });
+    }
+}

BIN
app/Http/.DS_Store


+ 13 - 0
app/Http/Controllers/Controller.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
+use Illuminate\Foundation\Bus\DispatchesJobs;
+use Illuminate\Foundation\Validation\ValidatesRequests;
+use Illuminate\Routing\Controller as BaseController;
+
+class Controller extends BaseController
+{
+    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
+}

+ 29 - 0
app/Http/Controllers/FileUpload.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+
+class FileUpload extends Controller
+{
+    public function upload(Request $request)
+    {
+        if ($request->hasFile('upload')) {
+            $file = $request->file('upload');
+            $originName = $file->getClientOriginalName();
+            $extension = $file->getClientOriginalExtension();
+            $fileName = pathinfo($originName, PATHINFO_FILENAME) . '_' . time() . '.' . $extension;
+            
+            $file->move(public_path('uploads'), $fileName);
+
+            $url = asset('uploads/' . $fileName);
+
+
+            return response()->json([
+                'fileName' => $fileName,
+                'uploaded' => true,
+                'url' => $url
+            ]);
+        }
+    }
+}

+ 222 - 0
app/Http/Controllers/FirstLoginController.php

@@ -0,0 +1,222 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Mail;
+use App\Http\Middleware\TenantMiddleware;
+
+class FirstLoginController extends Controller
+{
+    public function __construct()
+    {
+        $this->middleware('auth');
+        app(TenantMiddleware::class)->setupTenantConnection();
+    }
+
+    public function show()
+    {
+        $user = Auth::user();
+
+        // If already completed first login, redirect to dashboard
+        if ($user->first_login_completed) {
+            return redirect('/dashboard');
+        }
+
+        return view('first-login');
+    }
+
+    public function complete(Request $request)
+    {
+        // Validate only password
+        $request->validate([
+            'password' => 'required|min:6|confirmed',
+        ], [
+            'password.required' => 'La password è obbligatoria',
+            'password.min' => 'La password deve essere di almeno 6 caratteri',
+            'password.confirmed' => 'Le password non coincidono',
+        ]);
+
+        $currentUser = Auth::user();
+
+        Log::info('Starting first login completion', [
+            'user_id' => $currentUser->id,
+            'email' => $currentUser->email
+        ]);
+
+        try {
+            $this->updateTenantDatabase($currentUser, $request->password);
+
+            $this->updateMasterDatabaseSafely($currentUser, $request->password);
+
+            $this->sendAccountActivationEmailSafely($currentUser);
+
+            return redirect('/dashboard')->with('success', 'Password impostata con successo! Benvenuto in Leezard.cloud.');
+
+        } catch (\Exception $e) {
+            Log::error('First login completion failed', [
+                'user_id' => $currentUser->id,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+
+            return back()
+                ->withErrors(['error' => 'Errore durante l\'impostazione della password. Riprova.']);
+        }
+    }
+
+    private function updateTenantDatabase($currentUser, $password)
+    {
+        try {
+            Log::info('Updating tenant database', ['user_id' => $currentUser->id]);
+
+            $user = \App\Models\User::findOrFail($currentUser->id);
+            $user->password = Hash::make($password);
+            $user->first_login_completed = true;
+            $user->email_verified_at = now();
+            $user->save();
+
+            Log::info('Tenant database updated successfully', ['user_id' => $currentUser->id]);
+
+        } catch (\Exception $e) {
+            Log::error('Failed to update tenant database', [
+                'user_id' => $currentUser->id,
+                'error' => $e->getMessage()
+            ]);
+            throw new \Exception('Errore durante l\'aggiornamento del database locale: ' . $e->getMessage());
+        }
+    }
+
+    private function updateMasterDatabaseSafely($currentUser, $password)
+    {
+        try {
+            Log::info('Attempting to update master database', ['user_id' => $currentUser->id]);
+
+            if (!env('DB_DATABASE')) {
+                Log::warning('No master database configured, skipping master update');
+                return;
+            }
+
+            $this->updateMasterDatabaseWithTimeout($currentUser, $password);
+
+        } catch (\Exception $e) {
+            Log::error('Master database update failed (non-critical)', [
+                'user_id' => $currentUser->id,
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+
+    private function updateMasterDatabaseWithTimeout($currentUser, $password)
+    {
+        $originalTimeout = ini_get('max_execution_time');
+        set_time_limit(10);
+        try {
+            $masterConfig = [
+                'driver' => 'mysql',
+                'host' => env('DB_HOST', '127.0.0.1'),
+                'port' => env('DB_PORT', '3306'),
+                'database' => env('DB_DATABASE'),
+                'username' => env('DB_USERNAME'),
+                'password' => env('DB_PASSWORD'),
+                'charset' => 'utf8mb4',
+                'collation' => 'utf8mb4_unicode_ci',
+                'prefix' => '',
+                'strict' => true,
+                'engine' => null,
+                'options' => [
+                    \PDO::ATTR_TIMEOUT => 5,
+                ],
+            ];
+
+            config(['database.connections.master_temp' => $masterConfig]);
+
+            $connection = DB::connection('master_temp');
+            $connection->getPdo();
+
+            Log::info('Master database connection established');
+
+            $updateData = [
+                'password' => Hash::make($password),
+                'first_login_completed' => true,
+                'email_verified_at' => now(),
+                'updated_at' => now()
+            ];
+
+            $updated = $connection->table('users')
+                ->where('email', $currentUser->email)
+                ->update($updateData);
+
+            if ($updated) {
+                Log::info('Master database updated successfully', [
+                    'email' => $currentUser->email,
+                    'rows_updated' => $updated
+                ]);
+            } else {
+                Log::warning('No rows updated in master database', [
+                    'email' => $currentUser->email
+                ]);
+            }
+
+            DB::purge('master_temp');
+
+        } catch (\Exception $e) {
+            Log::error('Master database update timeout or error', [
+                'error' => $e->getMessage(),
+                'user_email' => $currentUser->email
+            ]);
+
+            try {
+                DB::purge('master_temp');
+            } catch (\Exception $cleanupError) {
+                // Ignore cleanup errors
+            }
+
+            throw $e;
+        } finally {
+            set_time_limit($originalTimeout);
+        }
+    }
+
+    private function sendAccountActivationEmailSafely($user)
+    {
+        try {
+            Log::info('Attempting to send activation email', ['user_id' => $user->id]);
+
+            $emailData = [
+                'name' => $user->name,
+                'email' => $user->email,
+                'login_url' => url('/'),
+                'activation_time' => now()->format('d/m/Y H:i'),
+                'platform_name' => 'Leezard.cloud'
+            ];
+
+            $originalTimeout = ini_get('max_execution_time');
+            set_time_limit(15);
+
+            Mail::send('emails.account-activated', $emailData, function ($message) use ($user) {
+                $message->to($user->email, $user->name)
+                        ->subject('Il tuo account è attivo – Benvenuto in Leezard.cloud')
+                        ->from(config('mail.from.address'), config('mail.from.name'));
+            });
+
+            set_time_limit($originalTimeout);
+
+            Log::info('Activation email sent successfully', [
+                'user_id' => $user->id,
+                'email' => $user->email
+            ]);
+
+        } catch (\Exception $e) {
+            Log::error('Failed to send activation email (non-critical)', [
+                'user_id' => $user->id,
+                'email' => $user->email,
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+}

+ 482 - 0
app/Http/Controllers/PasswordResetController.php

@@ -0,0 +1,482 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Facades\Mail;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Str;
+use Carbon\Carbon;
+
+class PasswordResetController extends Controller
+{
+    /**
+     * Show password reset request form
+     */
+    public function showResetRequestForm()
+    {
+        return view('auth.password-reset-request');
+    }
+
+    /**
+     * Send password reset email
+     */
+    public function sendResetEmail(Request $request)
+    {
+        $request->validate([
+            'email' => 'required|email'
+        ], [
+            'email.required' => 'La mail è obbligatoria',
+            'email.email' => 'Inserisci un indirizzo email valido'
+        ]);
+
+        $this->logSecurityEvent('reset_requested', $request->email);
+
+        try {
+            $user = $this->findUserInMasterDatabase($request->email);
+
+            if (!$user) {
+                $this->logSecurityEvent('reset_requested_invalid_email', $request->email);
+                return back()->with('success', 'Se l\'email esiste nel sistema, riceverai le istruzioni per il reset della password.');
+            }
+
+            if ($this->hasRecentResetRequest($request->email)) {
+                $this->logSecurityEvent('reset_requested_too_soon', $request->email);
+                return back()->with('error', 'È già stata inviata una richiesta di reset per questa email. Controlla la tua casella di posta o riprova più tardi.');
+            }
+
+            $token = Str::random(64);
+
+            $this->storeResetToken($request->email, $token);
+
+            $this->sendPasswordResetEmail($request->email, $token, $user);
+
+            $this->logSecurityEvent('reset_email_sent', $request->email);
+
+            return back()->with('success', 'Se l\'email esiste nel sistema, riceverai le istruzioni per il reset della password.');
+        } catch (\Exception $e) {
+            $this->logSecurityEvent('reset_request_failed', $request->email, ['error' => $e->getMessage()]);
+            return back()->with('error', 'Errore durante l\'invio dell\'email. Riprova più tardi.');
+        }
+    }
+
+    /**
+     * Show password reset form
+     */
+    public function showResetForm($token)
+    {
+        return view('auth.password-reset-form', ['token' => $token]);
+    }
+
+    /**
+     * Reset password
+     */
+    public function resetPassword(Request $request)
+    {
+        $request->validate([
+            'token' => 'required',
+            'email' => 'required|email',
+            'password' => 'required|min:6|confirmed'
+        ], [
+            'email.required' => 'La mail è obbligatoria',
+            'email.email' => 'Inserisci un indirizzo email valido',
+            'password.required' => 'La password è obbligatoria',
+            'password.min' => 'La password deve essere di almeno 6 caratteri',
+            'password.confirmed' => 'Le password non coincidono'
+        ]);
+
+        try {
+            // Verify reset token
+            $resetRecord = $this->verifyResetToken($request->email, $request->token);
+
+            if (!$resetRecord) {
+                return back()->with('error', 'Token non valido o scaduto.');
+            }
+
+            // Get user from master database
+            $user = $this->findUserInMasterDatabase($request->email);
+
+            if (!$user) {
+                return back()->with('error', 'Utente non trovato.');
+            }
+
+            // Update password in both databases
+            $this->updatePasswordInBothDatabases($request->email, $request->password, $user);
+
+            // Delete reset token
+            $this->deleteResetToken($request->email);
+
+            // Send password change notification
+            $this->sendPasswordChangeNotification($request->email, $user->name);
+
+            Log::info('Password reset completed with notification', [
+                'email' => $request->email
+            ]);
+
+            return redirect('/')->with('success', 'Password aggiornata con successo. Ti abbiamo inviato una email di conferma.');
+        } catch (\Exception $e) {
+            Log::error('Password reset failed', [
+                'email' => $request->email,
+                'error' => $e->getMessage()
+            ]);
+
+            return back()->with('error', 'Errore durante il reset della password. Riprova più tardi.');
+        }
+    }
+
+    /**
+     * Find user in master database
+     */
+    private function findUserInMasterDatabase($email)
+    {
+        try {
+            $masterConfig = [
+                'driver' => 'mysql',
+                'host' => env('DB_HOST', '127.0.0.1'),
+                'port' => env('DB_PORT', '3306'),
+                'database' => env('DB_DATABASE'),
+                'username' => env('DB_USERNAME'),
+                'password' => env('DB_PASSWORD'),
+                'charset' => 'utf8mb4',
+                'collation' => 'utf8mb4_unicode_ci',
+                'prefix' => '',
+                'strict' => true,
+                'engine' => null,
+            ];
+
+            config(['database.connections.master_reset' => $masterConfig]);
+
+            $user = DB::connection('master_reset')
+                ->table('users')
+                ->where('email', $email)
+                ->first();
+
+            DB::purge('master_reset');
+
+            return $user;
+        } catch (\Exception $e) {
+            Log::error('Failed to find user in master database', [
+                'email' => $email,
+                'error' => $e->getMessage()
+            ]);
+            return null;
+        }
+    }
+
+    /**
+     * Store reset token in master database
+     */
+    private function storeResetToken($email, $token)
+    {
+        try {
+            $masterConfig = [
+                'driver' => 'mysql',
+                'host' => env('DB_HOST', '127.0.0.1'),
+                'port' => env('DB_PORT', '3306'),
+                'database' => env('DB_DATABASE'),
+                'username' => env('DB_USERNAME'),
+                'password' => env('DB_PASSWORD'),
+                'charset' => 'utf8mb4',
+                'collation' => 'utf8mb4_unicode_ci',
+                'prefix' => '',
+                'strict' => true,
+                'engine' => null,
+            ];
+
+            config(['database.connections.master_reset_token' => $masterConfig]);
+
+            DB::connection('master_reset_token')
+                ->table('password_resets')
+                ->where('email', $email)
+                ->delete();
+
+            DB::connection('master_reset_token')
+                ->table('password_resets')
+                ->insert([
+                    'email' => $email,
+                    'token' => Hash::make($token),
+                    'created_at' => Carbon::now()
+                ]);
+
+            DB::purge('master_reset_token');
+        } catch (\Exception $e) {
+            Log::error('Failed to store reset token', [
+                'email' => $email,
+                'error' => $e->getMessage()
+            ]);
+            throw $e;
+        }
+    }
+
+    /**
+     * Verify reset token
+     */
+    private function verifyResetToken($email, $token)
+    {
+        try {
+            $masterConfig = [
+                'driver' => 'mysql',
+                'host' => env('DB_HOST', '127.0.0.1'),
+                'port' => env('DB_PORT', '3306'),
+                'database' => env('DB_DATABASE'),
+                'username' => env('DB_USERNAME'),
+                'password' => env('DB_PASSWORD'),
+                'charset' => 'utf8mb4',
+                'collation' => 'utf8mb4_unicode_ci',
+                'prefix' => '',
+                'strict' => true,
+                'engine' => null,
+            ];
+
+            config(['database.connections.master_verify' => $masterConfig]);
+
+            $resetRecord = DB::connection('master_verify')
+                ->table('password_resets')
+                ->where('email', $email)
+                ->first();
+
+            DB::purge('master_verify');
+
+            if (!$resetRecord) {
+                return null;
+            }
+
+            $isValidToken = Hash::check($token, $resetRecord->token);
+            $isNotExpired = Carbon::parse($resetRecord->created_at)->addHours(24)->isFuture();
+
+            if ($isValidToken && $isNotExpired) {
+                return $resetRecord;
+            }
+
+            return null;
+        } catch (\Exception $e) {
+            Log::error('Failed to verify reset token', [
+                'email' => $email,
+                'error' => $e->getMessage()
+            ]);
+            return null;
+        }
+    }
+
+    /**
+     * Update password in both master and tenant databases
+     */
+    private function updatePasswordInBothDatabases($email, $newPassword, $masterUser)
+    {
+        $hashedPassword = Hash::make($newPassword);
+
+        try {
+            $masterConfig = [
+                'driver' => 'mysql',
+                'host' => env('DB_HOST', '127.0.0.1'),
+                'port' => env('DB_PORT', '3306'),
+                'database' => env('DB_DATABASE'),
+                'username' => env('DB_USERNAME'),
+                'password' => env('DB_PASSWORD'),
+                'charset' => 'utf8mb4',
+                'collation' => 'utf8mb4_unicode_ci',
+                'prefix' => '',
+                'strict' => true,
+                'engine' => null,
+            ];
+
+            config(['database.connections.master_update' => $masterConfig]);
+
+            DB::connection('master_update')
+                ->table('users')
+                ->where('email', $email)
+                ->update(['password' => $hashedPassword]);
+
+            DB::purge('master_update');
+
+            Log::info('Password updated in master database', ['email' => $email]);
+
+            if ($masterUser->tenant_database) {
+                $tenantConfig = [
+                    'driver' => 'mysql',
+                    'host' => env('DB_HOST', '127.0.0.1'),
+                    'port' => env('DB_PORT', '3306'),
+                    'database' => $masterUser->tenant_database,
+                    'username' => $masterUser->tenant_username,
+                    'password' => $masterUser->tenant_password,
+                    'charset' => 'utf8mb4',
+                    'collation' => 'utf8mb4_unicode_ci',
+                    'prefix' => '',
+                    'strict' => true,
+                    'engine' => null,
+                ];
+
+                config(['database.connections.tenant_update' => $tenantConfig]);
+
+                DB::connection('tenant_update')
+                    ->table('users')
+                    ->where('email', $email)
+                    ->update(['password' => $hashedPassword]);
+
+                DB::purge('tenant_update');
+
+                Log::info('Password updated in tenant database', [
+                    'email' => $email,
+                    'tenant_database' => $masterUser->tenant_database
+                ]);
+            }
+        } catch (\Exception $e) {
+            Log::error('Failed to update password in databases', [
+                'email' => $email,
+                'error' => $e->getMessage()
+            ]);
+            throw $e;
+        }
+    }
+
+    /**
+     * Delete reset token
+     */
+    private function deleteResetToken($email)
+    {
+        try {
+            $masterConfig = [
+                'driver' => 'mysql',
+                'host' => env('DB_HOST', '127.0.0.1'),
+                'port' => env('DB_PORT', '3306'),
+                'database' => env('DB_DATABASE'),
+                'username' => env('DB_USERNAME'),
+                'password' => env('DB_PASSWORD'),
+                'charset' => 'utf8mb4',
+                'collation' => 'utf8mb4_unicode_ci',
+                'prefix' => '',
+                'strict' => true,
+                'engine' => null,
+            ];
+
+            config(['database.connections.master_delete_token' => $masterConfig]);
+
+            DB::connection('master_delete_token')
+                ->table('password_resets')
+                ->where('email', $email)
+                ->delete();
+
+            DB::purge('master_delete_token');
+        } catch (\Exception $e) {
+            Log::error('Failed to delete reset token', [
+                'email' => $email,
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+
+    private function sendPasswordChangeNotification($email, $name)
+    {
+        try {
+            $emailData = [
+                'name' => $name,
+                'email' => $email,
+                'change_time' => now()->format('d/m/Y H:i'),
+                'ip_address' => request()->ip()
+            ];
+
+            Mail::send('emails.password-changed', $emailData, function ($message) use ($email, $name) {
+                $message->to($email, $name)
+                    ->subject('La tua password è stata modificata')
+                    ->from(config('mail.from.address'), config('mail.from.name'));
+            });
+
+            Log::info('Password change notification sent', [
+                'email' => $email,
+                'name' => $name
+            ]);
+
+            return true;
+        } catch (\Exception $e) {
+            Log::error('Failed to send password change notification', [
+                'email' => $email,
+                'error' => $e->getMessage()
+            ]);
+            return false;
+        }
+    }
+
+
+    /**
+     * Send password reset email
+     */
+    private function sendPasswordResetEmail($email, $token, $user)
+    {
+        try {
+            $resetUrl = url('/password-reset/' . $token . '?email=' . urlencode($email));
+            $companyName = 'Leezard';
+
+            $emailData = [
+                'name' => $user->name,
+                'email' => $email,
+                'reset_url' => $resetUrl,
+                'company' => $companyName,
+                'expires_at' => Carbon::now()->addHours(24)->format('d/m/Y H:i')
+            ];
+
+            Mail::send('emails.password-reset', $emailData, function ($message) use ($email, $companyName, $user) {
+                $message->to($email, $user->name)
+                    ->subject('Reset Password -  Leezard')
+                    ->from(config('mail.from.address'), config('mail.from.name'));
+            });
+
+            Log::info('Password reset email sent', ['email' => $email]);
+        } catch (\Exception $e) {
+            Log::error('Failed to send password reset email', [
+                'email' => $email,
+                'error' => $e->getMessage()
+            ]);
+            throw $e;
+        }
+    }
+
+    private function logSecurityEvent($event, $email, $additionalData = [])
+    {
+        Log::info('Password Reset Security Event', array_merge([
+            'event' => $event,
+            'email' => $email,
+            'ip' => request()->ip(),
+            'user_agent' => request()->userAgent(),
+            'timestamp' => now()
+        ], $additionalData));
+    }
+
+    private function hasRecentResetRequest($email)
+    {
+        try {
+            $masterConfig = [
+                'driver' => 'mysql',
+                'host' => env('DB_HOST', '127.0.0.1'),
+                'port' => env('DB_PORT', '3306'),
+                'database' => env('DB_DATABASE'),
+                'username' => env('DB_USERNAME'),
+                'password' => env('DB_PASSWORD'),
+                'charset' => 'utf8mb4',
+                'collation' => 'utf8mb4_unicode_ci',
+                'prefix' => '',
+                'strict' => true,
+                'engine' => null,
+            ];
+
+            config(['database.connections.master_check_recent' => $masterConfig]);
+
+            $recentRequest = DB::connection('master_check_recent')
+                ->table('password_resets')
+                ->where('email', $email)
+                ->where('created_at', '>', Carbon::now()->subMinutes(5)) // 5 minutes cooldown
+                ->first();
+
+            DB::purge('master_check_recent');
+
+            return $recentRequest !== null;
+        } catch (\Exception $e) {
+            Log::error('Failed to check recent reset requests', [
+                'email' => $email,
+                'error' => $e->getMessage()
+            ]);
+            return false;
+        }
+    }
+}

+ 69 - 0
app/Http/Controllers/ReceiptExportController.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use App\Models\Receipt;
+use Illuminate\Support\Str;
+use ZipArchive;
+use Barryvdh\DomPDF\Facade\Pdf;
+
+class ReceiptExportController extends Controller
+{
+    public function exportZip(Request $request)
+    {
+        $q = Receipt::query()->with('member');
+
+        if ($request->filled('filterStatus')) {
+            $q->where('status', $request->input('filterStatus'));
+        }
+        if ($request->filled('filterFrom')) {
+            $q->whereDate('date', '>=', $request->input('filterFrom'));
+        }
+        if ($request->filled('filterTo')) {
+            $q->whereDate('date', '<=', $request->input('filterTo'));
+        }
+        if ($request->filled('filterMember')) {
+            $q->where('member_id', $request->input('filterMember'));
+        }
+
+        // Eventuale ricerca globale
+        if ($search = $request->input('search')) {
+            $q->where(function ($sub) use ($search) {
+                $sub->where('number', 'like', "%{$search}%")
+                    ->orWhere('status', 'like', "%{$search}%")
+                    ->orWhereHas('member', function ($mq) use ($search) {
+                        $mq->where('first_name', 'like', "%{$search}%")
+                            ->orWhere('last_name', 'like', "%{$search}%");
+                    });
+            });
+        }
+
+        $receipts = $q->get();
+
+        if ($receipts->isEmpty()) {
+            return abort(404, 'Nessuna ricevuta trovata.');
+        }
+
+        $zipFileName = now()->format('Ymd') . '_Ricevute.zip';
+        $zipPath = storage_path('app/' . $zipFileName);
+
+        $zip = new \ZipArchive();
+        if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
+            return abort(500, 'Impossibile creare lo ZIP.');
+        }
+
+        foreach ($receipts as $receipt) {
+            $lastName  = Str::slug($receipt->member->last_name ?? 'sconosciuto', '_');
+            $firstName = Str::slug($receipt->member->first_name ?? 'sconosciuto', '_');
+            $pdfName   = "Ricevuta_{$receipt->number}_{$lastName}_{$firstName}.pdf";
+
+            $pdf = PDF::loadView('receipt', ['receipt' => $receipt]);
+            $zip->addFromString($pdfName, $pdf->output());
+        }
+
+        $zip->close();
+
+        return response()->download($zipPath)->deleteFileAfterSend(true);
+    }
+}

+ 138 - 0
app/Http/Controllers/SmsTemplateController.php

@@ -0,0 +1,138 @@
+<?php
+
+// Controller: SmsTemplateController.php
+namespace App\Http\Controllers;
+
+use App\Models\SmsTemplate;
+use App\Models\SmsScheduled;
+use App\Models\Category;
+use App\Models\User;
+use Illuminate\Http\Request;
+use Carbon\Carbon;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Log;
+class SmsTemplateController extends Controller
+{
+    public function index()
+    {
+        return view('sms.index');
+    }
+
+    public function getTemplatesJson(Request $request)
+    {
+        $templates = SmsTemplate::with('creator:id,name')->latest()->get();
+
+        $data = $templates->map(function($template) {
+            return [
+                'id' => $template->id,
+                'name' => $template->name,
+                'content' => $template->content,
+                'character_count' => strlen($template->content),
+                'created_at' => $template->created_at->format('d/m/Y H:i'),
+                'creator' => $template->creator->name ?? 'N/A'
+            ];
+        });
+
+        return response()->json(['data' => $data]);
+    }
+
+    public function store(Request $request)
+    {
+        $request->validate([
+            'subject' => 'required|string|max:255',
+            'message' => 'required|string|max:160',
+            'recipients' => 'required|array|min:1',
+            'send_now' => 'required|boolean',
+            'scheduled_at' => 'required_if:send_now,false|date|after:now',
+        ]);
+
+        $template = SmsTemplate::create([
+            'name' => $request->subject,
+            'content' => $request->message,
+            'created_by' => Auth::id(),
+        ]);
+
+        $recipients = User::whereHas('categories', function ($query) use ($request) {
+            $query->whereIn('categories.id', $request->recipients);
+        })->get();
+
+        if ($request->send_now) {
+            $this->sendSmsNow($template, $recipients);
+            $message = 'Template creato e SMS inviato a ' . $recipients->count() . ' destinatari!';
+        } else {
+            $this->scheduleSms($template, $recipients, $request->scheduled_at);
+            $scheduledDate = Carbon::parse($request->scheduled_at)->format('d/m/Y H:i');
+            $message = 'Template creato e SMS programmato per ' . $scheduledDate;
+        }
+
+        return response()->json([
+            'success' => true,
+            'message' => $message,
+            'template' => [
+                'id' => $template->id,
+                'name' => $template->name,
+                'content' => $template->content,
+                'character_count' => strlen($template->content),
+                'created_at' => $template->created_at->format('d/m/Y H:i'),
+                'creator' => Auth::user()->name
+            ]
+        ]);
+    }
+
+    public function destroy($id)
+    {
+        $template = SmsTemplate::findOrFail($id);
+        $template->delete();
+
+        return response()->json([
+            'success' => true,
+            'message' => 'Template eliminato con successo!'
+        ]);
+    }
+
+    public function getCategories()
+    {
+        $categories = Category::with('users')->get()->map(function($category) {
+            return [
+                'id' => $category->id,
+                'name' => $category->name,
+                'users_count' => $category->users->count()
+            ];
+        });
+
+        return response()->json($categories);
+    }
+
+    public function getTemplate($id)
+    {
+        $template = SmsTemplate::findOrFail($id);
+
+        return response()->json([
+            'id' => $template->id,
+            'name' => $template->name,
+            'content' => $template->content
+        ]);
+    }
+
+    private function sendSmsNow($template, $recipients)
+    {
+        foreach ($recipients as $recipient) {
+            if ($recipient->phone) {
+                Log::info("SMS sent to {$recipient->name} ({$recipient->phone}): {$template->content}");
+            }
+        }
+    }
+
+    private function scheduleSms($template, $recipients, $scheduledAt)
+    {
+        $scheduled = SmsScheduled::create([
+            'template_id' => $template->id,
+            'content' => $template->content,
+            'scheduled_at' => Carbon::parse($scheduledAt),
+            'status' => 'scheduled',
+            'created_by' => Auth::id(),
+        ]);
+
+        $scheduled->recipients()->attach($recipients->pluck('id'));
+    }
+}

+ 69 - 0
app/Http/Kernel.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace App\Http;
+
+use Illuminate\Foundation\Http\Kernel as HttpKernel;
+
+class Kernel extends HttpKernel
+{
+    /**
+     * The application's global HTTP middleware stack.
+     *
+     * These middleware are run during every request to your application.
+     *
+     * @var array<int, class-string|string>
+     */
+    protected $middleware = [
+        // \App\Http\Middleware\TrustHosts::class,
+        \App\Http\Middleware\TrustProxies::class,
+        \Illuminate\Http\Middleware\HandleCors::class,
+        \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
+        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
+        \App\Http\Middleware\TrimStrings::class,
+        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
+    ];
+
+    /**
+     * The application's route middleware groups.
+     *
+     * @var array<string, array<int, class-string|string>>
+     */
+    protected $middlewareGroups = [
+        'web' => [
+            \App\Http\Middleware\EncryptCookies::class,
+            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
+            \Illuminate\Session\Middleware\StartSession::class,
+            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
+            \App\Http\Middleware\VerifyCsrfToken::class,
+            \Illuminate\Routing\Middleware\SubstituteBindings::class,
+        ],
+
+        'api' => [
+            // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
+            'throttle:api',
+            \Illuminate\Routing\Middleware\SubstituteBindings::class,
+        ],
+    ];
+
+    /**
+     * The application's route middleware.
+     *
+     * These middleware may be assigned to groups or used individually.
+     *
+     * @var array<string, class-string|string>
+     */
+    protected $routeMiddleware = [
+        'auth' => \App\Http\Middleware\Authenticate::class,
+        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
+        'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
+        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
+        'can' => \Illuminate\Auth\Middleware\Authorize::class,
+        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
+        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
+        'signed' => \App\Http\Middleware\ValidateSignature::class,
+        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
+        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
+        'tenant' => \App\Http\Middleware\TenantMiddleware::class,
+        'first.login.completed' => \App\Http\Middleware\CheckFirstLoginCompleted::class,
+    ];
+}

+ 385 - 0
app/Http/Livewire/Azienda.php

@@ -0,0 +1,385 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+use Livewire\WithFileUploads;
+use Illuminate\Support\Str;
+
+use App\Models\Azienda as AziendaModel;
+use App\Services\LogoUploadServices;
+use Illuminate\Support\Facades\Log;
+use App\Http\Middleware\TenantMiddleware;
+
+class Azienda extends Component
+{
+    use WithFileUploads;
+
+    public $update = false;
+    public $azienda;
+    public $ragione_sociale;
+    public $nome_associazione;
+    public $tipologia;
+    public $logo;
+    public $temp_logo;
+    public $sede_legale_nazione;
+    public $sede_legale_provincia;
+    public $sede_legale_comune;
+    public $sede_legale_indirizzo;
+    public $sede_legale_cap;
+    public $same_address = false;
+    public $sede_operativa_nazione;
+    public $sede_operativa_provincia;
+    public $sede_operativa_comune;
+    public $sede_operativa_indirizzo;
+    public $sede_operativa_cap;
+    public $email;
+    public $pec;
+    public $telefono;
+    public $cellulare;
+    public $partita_iva;
+    public $codice_fiscale;
+    public $codice_sdi;
+    public $chiusura_anno_fiscale;
+    public $scadenza_abbonamenti;
+    public $scadenza_pagamenti_uscita;
+
+    public $search = '';
+    public $selectedDisciplines = [];
+    public $disciplineId = '';
+    public $discipline = [];
+    public $activeTab = 'generale';
+
+    protected $rules = [
+        'ragione_sociale' => 'required|string|max:255',
+        'email' => 'required|email|max:255',
+        'pec' => 'required|email|max:255',
+        'cellulare' => 'required|string|max:20',
+    ];
+
+    public function resetFields()
+    {
+        $this->ragione_sociale = null;
+        $this->nome_associazione = null;
+        $this->tipologia = null;
+        $this->discipline = null;
+        $this->temp_logo = null;
+
+        $this->sede_legale_nazione = null;
+        $this->sede_legale_provincia = null;
+        $this->sede_legale_comune = null;
+        $this->sede_legale_indirizzo = null;
+        $this->sede_legale_cap = null;
+
+        $this->same_address = false;
+        $this->sede_operativa_nazione = null;
+        $this->sede_operativa_provincia = null;
+        $this->sede_operativa_comune = null;
+        $this->sede_operativa_indirizzo = null;
+        $this->sede_operativa_cap = null;
+
+        $this->email = null;
+        $this->pec = null;
+        $this->telefono = null;
+        $this->cellulare = null;
+
+        $this->partita_iva = null;
+        $this->codice_fiscale = null;
+        $this->codice_sdi = null;
+    }
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+    }
+
+    public function mount()
+    {
+        $this->azienda = AziendaModel::first();
+
+        if ($this->azienda) {
+            $this->ragione_sociale = $this->azienda->ragione_sociale;
+            $this->nome_associazione = $this->azienda->nome_associazione;
+            $this->tipologia = $this->azienda->tipologia;
+            $this->discipline = $this->azienda->discipline;
+            $this->logo = $this->azienda->logo;
+
+            $this->sede_legale_nazione = $this->azienda->sede_legale_nazione;
+            $this->sede_legale_provincia = $this->azienda->sede_legale_provincia;
+            $this->sede_legale_comune = $this->azienda->sede_legale_comune;
+            $this->sede_legale_indirizzo = $this->azienda->sede_legale_indirizzo;
+            $this->sede_legale_cap = $this->azienda->sede_legale_cap;
+
+            $this->sede_operativa_nazione = $this->azienda->sede_operativa_nazione;
+            $this->sede_operativa_provincia = $this->azienda->sede_operativa_provincia;
+            $this->sede_operativa_comune = $this->azienda->sede_operativa_comune;
+            $this->sede_operativa_indirizzo = $this->azienda->sede_operativa_indirizzo;
+            $this->sede_operativa_cap = $this->azienda->sede_operativa_cap;
+
+            $this->email = $this->azienda->email;
+            $this->pec = $this->azienda->pec;
+            $this->telefono = $this->azienda->telefono;
+            $this->cellulare = $this->azienda->cellulare;
+
+            $this->partita_iva = $this->azienda->partita_iva;
+            $this->codice_fiscale = $this->azienda->codice_fiscale;
+            $this->codice_sdi = $this->azienda->codice_sdi;
+
+
+            if (
+                $this->sede_legale_nazione == $this->sede_operativa_nazione &&
+                $this->sede_legale_provincia == $this->sede_operativa_provincia &&
+                $this->sede_legale_comune == $this->sede_operativa_comune &&
+                $this->sede_legale_indirizzo == $this->sede_operativa_indirizzo &&
+                $this->sede_legale_cap == $this->sede_operativa_cap
+            ) {
+                $this->same_address = true;
+            }
+            Log::info('Azienda disciplines: ' . json_encode($this->azienda->discipline));
+            if ($this->azienda && $this->azienda->discipline) {
+                $this->selectedDisciplines = array_map('trim', array: explode(';', $this->azienda->discipline));
+            }
+        } else {
+            $this->update = true;
+            $this->resetFields();
+        }
+        $this->loadDisciplines();
+    }
+
+    public function render()
+    {
+        return view('livewire.azienda');
+    }
+
+    public function updatedSameAddress()
+    {
+        if ($this->same_address) {
+            $this->sede_operativa_nazione = $this->sede_legale_nazione;
+            $this->sede_operativa_provincia = $this->sede_legale_provincia;
+            $this->sede_operativa_comune = $this->sede_legale_comune;
+            $this->sede_operativa_indirizzo = $this->sede_legale_indirizzo;
+            $this->sede_operativa_cap = $this->sede_legale_cap;
+        } else {
+            $this->sede_operativa_nazione = null;
+            $this->sede_operativa_provincia = null;
+            $this->sede_operativa_comune = null;
+            $this->sede_operativa_indirizzo = null;
+            $this->sede_operativa_cap = null;
+        }
+    }
+
+    public function updatedSedeOperativaNazione()
+    {
+        $this->checkAddressDifference();
+    }
+
+    public function updatedSedeOperativaProvincia()
+    {
+        $this->checkAddressDifference();
+    }
+
+    public function updatedSedeOperativaComune()
+    {
+        $this->checkAddressDifference();
+    }
+
+    public function updatedSedeOperativaIndirizzo()
+    {
+        $this->checkAddressDifference();
+    }
+
+    public function updatedSedeOperativaCap()
+    {
+        $this->checkAddressDifference();
+    }
+
+    public function checkAddressDifference()
+    {
+        if (
+            $this->sede_legale_nazione == $this->sede_operativa_nazione &&
+            $this->sede_legale_provincia == $this->sede_operativa_provincia &&
+            $this->sede_legale_comune == $this->sede_operativa_comune &&
+            $this->sede_legale_indirizzo == $this->sede_operativa_indirizzo &&
+            $this->sede_legale_cap == $this->sede_operativa_cap
+        ) {
+            $this->same_address = true;
+        } else {
+            $this->same_address = false;
+        }
+    }
+
+    public function setTab($tab)
+    {
+        $this->activeTab = $tab;
+    }
+
+    public function edit()
+    {
+        $this->update = true;
+    }
+
+    public function save()
+    {
+        $this->validate();
+        Log::info('Saving discipline: ' . json_encode(implode('; ', $this->selectedDisciplines)));
+
+        try {
+            $data = [
+                'ragione_sociale' => $this->ragione_sociale,
+                'nome_associazione' => $this->nome_associazione,
+                'tipologia' => $this->tipologia,
+                'discipline' => implode('; ', $this->selectedDisciplines),
+
+                'sede_legale_nazione' => $this->sede_legale_nazione,
+                'sede_legale_provincia' => $this->sede_legale_provincia,
+                'sede_legale_comune' => $this->sede_legale_comune,
+                'sede_legale_indirizzo' => $this->sede_legale_indirizzo,
+                'sede_legale_cap' => $this->sede_legale_cap,
+
+                'sede_operativa_nazione' => $this->sede_operativa_nazione,
+                'sede_operativa_provincia' => $this->sede_operativa_provincia,
+                'sede_operativa_comune' => $this->sede_operativa_comune,
+                'sede_operativa_indirizzo' => $this->sede_operativa_indirizzo,
+                'sede_operativa_cap' => $this->sede_operativa_cap,
+
+                'same_address' => $this->same_address,
+
+                'email' => $this->email,
+                'pec' => $this->pec,
+                'telefono' => $this->telefono,
+                'cellulare' => $this->cellulare,
+
+                'partita_iva' => $this->partita_iva,
+                'codice_fiscale' => $this->codice_fiscale,
+                'codice_sdi' => $this->codice_sdi,
+            ];
+
+                      // Create or update azienda first
+            if ($this->azienda) {
+                $this->azienda->update($data);
+            } else {
+                $this->azienda = AziendaModel::create($data);
+            }
+
+            // Handle logo upload using the service
+            if ($this->temp_logo) {
+                try {
+                    Log::info('Starting logo upload with service');
+                    $logoService = app(LogoUploadServices::class);
+                    $logoPath = $logoService->uploadLogo($this->temp_logo, $this->azienda);
+                    Log::info('Logo uploaded successfully to: ' . $logoPath);
+
+                    // Reset temp logo after successful upload
+                    $this->temp_logo = null;
+
+                } catch (\Exception $e) {
+                    Log::error('Error uploading logo via service: ' . $e->getMessage());
+                    session()->flash('error', 'Errore durante il caricamento del logo: ' . $e->getMessage());
+                    return;
+                }
+            }
+
+            session()->flash('message', $this->azienda->wasRecentlyCreated ?
+                'Dati aziendali creati con successo!' :
+                'Dati aziendali aggiornati con successo!'
+            );
+
+            $this->update = false;
+
+        } catch (\Exception $ex) {
+            Log::error('Error in save method: ' . $ex->getMessage());
+            session()->flash('error', 'Errore: ' . $ex->getMessage());
+        }
+    }
+
+    public function getSelectedDisciplineNamesProperty()
+    {
+        if (empty($this->selectedDisciplines)) {
+            return [];
+        }
+        Log::info('Selected disciplines: ' . json_encode($this->selectedDisciplines));
+
+        return \App\Models\Discipline::whereIn('id', $this->selectedDisciplines)
+            ->pluck('name')
+            ->toArray();
+    }
+
+    public function cancel()
+    {
+        $this->resetFields();
+        $this->mount();
+        $this->update = false;
+    }
+
+    public function loadDisciplines()
+    {
+        $this->discipline = \App\Models\Discipline::select('id', 'name')->get();
+    }
+
+    public function addDiscipline()
+    {
+        if (!empty($this->disciplineId)) {
+            $discipline = \App\Models\Discipline::find($this->disciplineId);
+
+            if ($discipline && !in_array($discipline->name, $this->selectedDisciplines)) {
+                $this->selectedDisciplines[] = $discipline->name;
+                $this->disciplineId = '';
+            }
+        }
+    }
+
+    public function removeDiscipline($index)
+    {
+        if (isset($this->selectedDisciplines[$index])) {
+            unset($this->selectedDisciplines[$index]);
+            $this->selectedDisciplines = array_values($this->selectedDisciplines);
+        }
+    }
+
+    public function updatedSearch()
+    {
+        $this->loadDisciplines();
+    }
+
+      public function removeLogo()
+    {
+        if ($this->azienda) {
+            try {
+                $logoService = app(LogoUploadServices::class);
+                if ($logoService->deleteLogo($this->azienda)) {
+                    $this->logo = null;
+                    session()->flash('message', 'Logo rimosso con successo!');
+                    Log::info('Logo removed successfully for azienda: ' . $this->azienda->id);
+                } else {
+                    session()->flash('error', 'Nessun logo da rimuovere.');
+                }
+            } catch (\Exception $e) {
+                Log::error('Error removing logo: ' . $e->getMessage());
+                session()->flash('error', 'Errore durante la rimozione del logo: ' . $e->getMessage());
+            }
+        }
+    }
+
+    /**
+     * Get logo URL for display
+     */
+    public function getLogoUrlProperty()
+    {
+        if ($this->azienda && $this->azienda->logo) {
+            $logoService = app(LogoUploadServices::class);
+            return $logoService->getLogoUrl($this->azienda);
+        }
+        return null;
+    }
+
+    /**
+     * Check if logo exists
+     */
+    public function getHasLogoProperty()
+    {
+        if ($this->azienda) {
+            $logoService = app(LogoUploadServices::class);
+            return $logoService->logoExists($this->azienda);
+        }
+        return false;
+    }
+}

+ 131 - 0
app/Http/Livewire/Bank.php

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

+ 157 - 0
app/Http/Livewire/Card.php

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

+ 60 - 0
app/Http/Livewire/Categories.php

@@ -0,0 +1,60 @@
+<?php
+namespace App\Http\Livewire;
+use Livewire\Component;
+use App\Http\Middleware\TenantMiddleware;
+
+class Categories extends Component
+{
+    public $level_1 = [];
+    public $level_2 = [];
+    public $level_3 = [];
+
+    public $level_1_id = 0;
+    public $level_2_id = 0;
+    public $level_3_id = 0;
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+    }
+
+
+    public function render()
+    {
+        $this->emit('hideMsg');
+        $reset = false;
+        if ($this->level_1_id > 0)
+        {
+            $this->level_2 = \App\Models\Category::where('parent_id', $this->level_1_id)->orderBy('name')->get();
+            if (sizeof($this->level_2) == 0)
+            {
+                $this->emit('storeCategoryWithID', $this->level_1_id);
+                $reset = true;
+            }
+        }
+        if ($this->level_2_id > 0)
+        {
+            $this->level_3 = \App\Models\Category::where('parent_id', $this->level_2_id)->orderBy('name')->get();
+            if (sizeof($this->level_3) == 0)
+            {
+                $this->emit('storeCategoryWithID', $this->level_2_id);
+                $reset = true;
+            }
+        }
+        if ($this->level_3_id > 0)
+        {
+            $this->emit('storeCategoryWithID', $this->level_3_id);
+            $reset = true;
+        }
+        $this->level_1 = \App\Models\Category::where('parent_id', null)->orderBy('name')->get();
+        if ($reset)
+        {
+            $this->level_1_id = 0;
+            $this->level_2_id = 0;
+            $this->level_3_id = 0;
+            $this->level_2 = [];
+            $this->level_3 = [];
+        }
+
+        return view('livewire.categories');
+    }
+}

+ 132 - 0
app/Http/Livewire/Category.php

@@ -0,0 +1,132 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+use App\Http\Middleware\TenantMiddleware;
+use Illuminate\Support\Facades\Auth;
+
+class Category extends Component
+{
+    public $records, $parent_id, $name, $enabled, $dataId, $update = false, $add = false;
+
+    public $parent = '';
+
+    protected $rules = [
+        'name' => 'required'
+    ];
+
+    protected $messages = [
+        'name.required' => 'Il nome è obbligatorio'
+    ];
+
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+    }
+    public function mount(){
+
+        if(Auth::user()->level != env('LEVEL_ADMIN', 0))
+            return redirect()->to('/dashboard');
+
+    }
+
+    public function resetFields(){
+        $this->name = '';
+        $this->parent_id = null;
+        $this->parent = '';
+        $this->enabled = true;
+    }
+
+    public function render()
+    {
+        $this->records = \App\Models\Category::where('parent_id', null)->get();
+        return view('livewire.category');
+    }
+
+    public function add()
+    {
+        $this->resetFields();
+        $this->add = true;
+        $this->update = false;
+    }
+
+    public function addLevel($parent_id)
+    {
+        $this->resetFields();
+        $this->parent_id = $parent_id;
+        $this->parent = \App\Models\Category::findOrFail($parent_id)->name;
+        $this->add = true;
+        $this->update = false;
+    }
+
+    public function store()
+    {
+        $this->validate();
+        try {
+            \App\Models\Category::create([
+                'name' => $this->name,
+                'parent_id' => $this->parent_id,
+                'enabled' => $this->enabled
+            ]);
+            session()->flash('success','Categoria creata');
+            $this->resetFields();
+            $this->add = false;
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function edit($id){
+        try {
+            $category = \App\Models\Category::findOrFail($id);
+            if( !$category) {
+                session()->flash('error','Categoria non trovata');
+            } else {
+                $this->name = $category->name;
+                $this->enabled = $category->enabled;
+                $this->parent_id = $category->parent_id;
+                $this->dataId = $category->id;
+                $this->update = true;
+                $this->add = false;
+            }
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function update()
+    {
+        $this->validate();
+        try {
+            \App\Models\Category::whereId($this->dataId)->update([
+                'name' => $this->name,
+                'parent_id' => $this->parent_id,
+                'enabled' => $this->enabled
+            ]);
+            session()->flash('success','Categoria aggiornata');
+            $this->resetFields();
+            $this->update = false;
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function cancel()
+    {
+        $this->add = false;
+        $this->update = false;
+        $this->resetFields();
+    }
+
+    public function delete($id)
+    {
+        try{
+            \App\Models\Category::find($id)->delete();
+            session()->flash('success',"Categoria eliminata");
+            return redirect(request()->header('Referer'));
+        }catch(\Exception $e){
+            session()->flash('error','Errore (' . $e->getMessage() . ')');
+        }
+    }
+}

+ 268 - 0
app/Http/Livewire/Causal.php

@@ -0,0 +1,268 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Illuminate\Support\Facades\Auth;
+use Livewire\Component;
+use App\Http\Middleware\TenantMiddleware;
+
+class Causal extends Component
+{
+    public $recordsIn, $recordsOut, $parent_id,  $name, $enabled, $corrispettivo_fiscale, $no_receipt, $money, $user_status, $no_first, $no_records, $no_reports, $type, $dataId, $update = false, $add = false;
+
+    public $corrispettivo_causal_id = 0;
+
+    public $parent = '';
+    public $showHidden = false;
+
+    protected $rules = [
+        'name' => 'required',
+        'type' => 'required'
+    ];
+
+    protected $messages = [
+        'name.required' => 'Il nome è obbligatorio'
+    ];
+
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+    }
+
+    public function mount()
+    {
+        $this->loadRecords();
+    }
+
+    public function hide($id)
+    {
+        try {
+            \App\Models\Causal::whereId($id)->update(['hidden' => true]);
+            session()->flash('success', 'Causale nascosta');
+            $this->loadRecords(); // Refresh the data
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function show($id)
+    {
+        try {
+            \App\Models\Causal::whereId($id)->update(['hidden' => false]);
+            session()->flash('success', 'Causale ripristinata');
+            $this->loadRecords(); // Refresh the data
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function toggleHidden()
+    {
+        $this->showHidden = !$this->showHidden;
+        $this->loadRecords();
+    }
+
+    public function loadRecords()
+    {
+        if ($this->showHidden) {
+            // Show all records including hidden ones
+            $this->recordsIn = \App\Models\Causal::where('type', 'IN')
+                ->where('parent_id', null)
+                ->with(['childs'])
+                ->orderBy('name')
+                ->get();
+
+            $this->recordsOut = \App\Models\Causal::where('type', 'OUT')
+                ->where('parent_id', null)
+                ->with(['childs'])
+                ->orderBy('name')
+                ->get();
+        } else {
+            // Show only non-hidden records
+            $this->recordsIn = \App\Models\Causal::where('type', 'IN')
+                ->where('parent_id', null)
+                ->where(function ($query) {
+                    $query->where('hidden', false)->orWhereNull('hidden');
+                })
+                ->with(['childs' => function ($query) {
+                    $query->where(function ($q) {
+                        $q->where('hidden', false)->orWhereNull('hidden');
+                    });
+                }])
+                ->orderBy('name')
+                ->get();
+
+            $this->recordsOut = \App\Models\Causal::where('type', 'OUT')
+                ->where('parent_id', null)
+                ->where(function ($query) {
+                    $query->where('hidden', false)->orWhereNull('hidden');
+                })
+                ->with(['childs' => function ($query) {
+                    $query->where(function ($q) {
+                        $q->where('hidden', false)->orWhereNull('hidden');
+                    });
+                }])
+                ->orderBy('name')
+                ->get();
+        }
+    }
+
+    public function resetFields()
+    {
+        $this->name = '';
+        $this->parent_id = null;
+        $this->parent = '';
+        $this->type = null;
+        $this->money = false;
+        $this->corrispettivo_fiscale = false;
+        $this->no_receipt = false;
+        $this->user_status = false;
+        $this->no_first = false;
+        $this->no_records = false;
+        $this->no_reports = false;
+        $this->enabled = true;
+    }
+
+    public function render()
+    {
+        // Remove the duplicate queries - loadRecords() already handles this
+        // and make sure it respects the showHidden state
+        if (!isset($this->recordsIn) || !isset($this->recordsOut)) {
+            $this->loadRecords();
+        }
+
+        return view('livewire.causal');
+    }
+
+    public function add()
+    {
+        $this->resetFields();
+        $this->add = true;
+        $this->update = false;
+    }
+
+    public function addLevel($parent_id)
+    {
+        $this->resetFields();
+        $this->parent_id = $parent_id;
+        $p = \App\Models\Causal::findOrFail($parent_id);
+        $this->type = $p->type;
+        $this->parent = $p->name;
+        $this->add = true;
+        $this->update = false;
+    }
+
+    public function store()
+    {
+        $this->validate();
+        try {
+            \App\Models\Causal::create([
+                'name' => $this->name,
+                'type' => $this->type,
+                'parent_id' => $this->parent_id,
+                'money' => $this->money,
+                'corrispettivo_fiscale' => $this->corrispettivo_fiscale,
+                'no_receipt' => $this->no_receipt,
+                'user_status' => $this->user_status,
+                'no_first' => $this->no_first,
+                'no_records' => $this->no_records,
+                'no_reports' => $this->no_reports,
+                'enabled' => $this->enabled
+            ]);
+            session()->flash('success', 'Causale creata');
+            $this->resetFields();
+            $this->add = false;
+            $this->loadRecords();
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function edit($id)
+    {
+        try {
+            $causal = \App\Models\Causal::findOrFail($id);
+            if (!$causal) {
+                session()->flash('error', 'Causale non trovata');
+            } else {
+                $this->name = $causal->name;
+                $this->money = $causal->money;
+                $this->no_receipt = $causal->no_receipt;
+                $this->user_status = $causal->user_status;
+                $this->no_first = $causal->no_first;
+                $this->no_records = $causal->no_records;
+                $this->no_reports = $causal->no_reports;
+                $this->enabled = $causal->enabled;
+                $this->corrispettivo_fiscale = $causal->corrispettivo_fiscale;
+                $this->type = $causal->type;
+                $this->parent_id = $causal->parent_id;
+                $this->dataId = $causal->id;
+                $this->update = true;
+                $this->add = false;
+            }
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function update()
+    {
+        $this->validate();
+        try {
+            \App\Models\Causal::whereId($this->dataId)->update([
+                'name' => $this->name,
+                'type' => $this->type,
+                'parent_id' => $this->parent_id,
+                'user_status' => $this->user_status,
+                'no_first' => $this->no_first,
+                'no_records' => $this->no_records,
+                'no_reports' => $this->no_reports,
+                'money' => $this->money,
+                'no_receipt' => $this->no_receipt,
+                'corrispettivo_fiscale' => $this->corrispettivo_fiscale,
+                'enabled' => $this->enabled
+            ]);
+            session()->flash('success', 'Causale aggiornata');
+            $this->resetFields();
+            $this->update = false;
+            $this->loadRecords();
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function cancel()
+    {
+        $this->add = false;
+        $this->update = false;
+        $this->resetFields();
+    }
+
+    public function duplicate($id)
+    {
+        $old = \App\Models\Causal::find($id);
+        $new = $old->replicate();
+        $new->name = $new->name . ' - COPIA';
+        $new->save();
+
+        $this->duplicateRecursive($old, $new);
+        $this->loadRecords(); // Refresh after duplicate
+    }
+
+    public function duplicateRecursive($old, $new)
+    {
+        foreach ($old->childs as $c) {
+            $old1 = \App\Models\Causal::find($c->id);
+            $new1 = $old1->replicate();
+            $new1->parent_id = $new->id;
+            $new1->save();
+
+            $this->duplicateRecursive($old1, $new1);
+        }
+    }
+
+    public function reorder()
+    {
+        $this->loadRecords(); // Refresh after reorder
+    }
+}

+ 134 - 0
app/Http/Livewire/Causals.php

@@ -0,0 +1,134 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+use App\Http\Middleware\TenantMiddleware;
+
+class Causals extends Component
+{
+    public $level_1 = [];
+    public $level_2 = [];
+    public $level_3 = [];
+
+    public $level_1_id = 0;
+    public $level_2_id = 0;
+    public $level_3_id = 0;
+
+    public $type = '';
+    public $emit = 'setCausal';
+    public $idx = -1;
+    public $causal_id = null;
+    public $show_hidden = true;
+
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+    }
+
+    public function mount($type, $idx, $causal_id)
+    {
+        $this->type = $type;
+        $this->idx = $idx;
+        $this->causal_id = $causal_id;
+
+        if ($this->causal_id != null) {
+            $c = \App\Models\Causal::findOrFail($this->causal_id);
+            $ids = array_reverse($c->recursiveParent($c->parent_id, [$c->id]));
+
+            foreach($ids as $ii => $i) {
+                if ($ii == 0) {
+                    $this->level_1_id = $i;
+                }
+                if ($ii == 1) {
+                    $this->level_2_id = $i;
+                }
+                if ($ii == 2) {
+                    $this->level_3_id = $i;
+                }
+            }
+        }
+    }
+
+    public function updatedLevel1Id()
+    {
+        $this->emit($this->emit, null, $this->idx);
+        $this->level_2_id = 0;
+        $this->level_2 = [];
+        $this->level_3_id = 0;
+        $this->level_3 = [];
+    }
+
+    public function updatedLevel2Id()
+    {
+        $this->emit($this->emit, null, $this->idx);
+        $this->level_3_id = 0;
+        $this->level_3 = [];
+    }
+
+    public function updatedLevel3Id()
+    {
+        $this->emit($this->emit, null, $this->idx);
+    }
+
+    public function render()
+    {
+        $reset = false;
+
+        // Build query with visibility filter
+        $visibilityFilter = function($query) {
+            return $query;
+        };
+
+        if ($this->show_hidden == false) {
+            $visibilityFilter = function($query) {
+                return $query->where('hidden', false);
+            };
+        }
+
+        if ($this->level_1_id > 0) {
+            $this->level_2 = \App\Models\Causal::where('parent_id', $this->level_1_id)
+                ->where('type', $this->type)
+                ->where(function($query) use ($visibilityFilter) {
+                    return $visibilityFilter($query);
+                })
+                ->orderBy('name')
+                ->get();
+
+            if (sizeof($this->level_2) == 0) {
+                $this->emit($this->emit, $this->level_1_id, $this->idx);
+                $reset = true;
+            }
+        }
+
+        if ($this->level_2_id > 0) {
+            $this->level_3 = \App\Models\Causal::where('parent_id', $this->level_2_id)
+                ->where('type', $this->type)
+                ->where(function($query) use ($visibilityFilter) {
+                    return $visibilityFilter($query);
+                })
+                ->orderBy('name')
+                ->get();
+
+            if (sizeof($this->level_3) == 0) {
+                $this->emit($this->emit, $this->level_2_id, $this->idx);
+                $reset = true;
+            }
+        }
+
+        if ($this->level_3_id > 0) {
+            $this->emit($this->emit, $this->level_3_id, $this->idx);
+            $reset = true;
+        }
+
+        $this->level_1 = \App\Models\Causal::where('parent_id', null)
+            ->where('type', $this->type)
+            ->where(function($query) use ($visibilityFilter) {
+                return $visibilityFilter($query);
+            })
+            ->orderBy('name')
+            ->get();
+
+        return view('livewire.causals');
+    }
+}

+ 142 - 0
app/Http/Livewire/City.php

@@ -0,0 +1,142 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+use App\Http\Middleware\TenantMiddleware;
+use Livewire\WithPagination;
+
+class City extends Component
+{
+
+    use WithPagination;
+
+    protected $paginationTheme = 'bootstrap';
+
+    public $sortField ='cities.name';
+    public $sortAsc = true;
+    public function sortBy($field)
+    {
+        if($this->sortField === $field)
+        {
+            $this->sortAsc = ! $this->sortAsc;
+        } else {
+            $this->sortAsc = true;
+        }
+
+        $this->sortField = $field;
+    }
+
+    public $name, $enabled, $dataId, $province_id, $update = false, $add = false;
+
+    public $provinces = array();
+
+    protected $rules = [
+        'name' => 'required',
+        'province_id' => 'required'
+    ];
+
+    protected $messages = [
+        'name.required' => 'Il nome è obbligatorio'
+    ];
+
+    public function resetFields(){
+        $this->name = '';
+        $this->enabled = true;
+        $this->province_id = null;
+    }
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+    }
+    public function mount()
+    {
+        $this->provinces = \App\Models\Province::select('id', 'name')->get();
+    }
+
+    public function render()
+    {
+        $rows = \App\Models\City::select('cities.name', 'cities.id', 'cities.enabled', 'provinces.name as province_name')->join('provinces', 'cities.province_id', '=', 'provinces.id')->get();
+            //->orderBy($this->sortField, $this->sortAsc ? 'asc' : 'desc')
+            //->paginate(10);
+
+        //$rows = \App\Models\City::with('province')->orderBy($this->sortField, $this->sortAsc ? 'asc' : 'desc')->paginate(10);
+        return view('livewire.city', ['records' => $rows]);
+    }
+
+    public function add()
+    {
+        $this->resetFields();
+        $this->add = true;
+        $this->update = false;
+    }
+
+    public function store()
+    {
+        $this->validate();
+        try {
+            \App\Models\City::create([
+                'name' => $this->name,
+                'province_id' => $this->province_id,
+                'enabled' => $this->enabled
+            ]);
+            session()->flash('success','Città creata');
+            $this->resetFields();
+            $this->add = false;
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function edit($id){
+        try {
+            $city = \App\Models\City::findOrFail($id);
+            if( !$city) {
+                session()->flash('error','Città non trovata');
+            } else {
+                $this->name = $city->name;
+                $this->enabled = $city->enabled;
+                $this->province_id = $city->province_id;
+                $this->dataId = $city->id;
+                $this->update = true;
+                $this->add = false;
+            }
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function update()
+    {
+        $this->validate();
+        try {
+            \App\Models\City::whereId($this->dataId)->update([
+                'name' => $this->name,
+                'province_id' => $this->province_id,
+                'enabled' => $this->enabled
+            ]);
+            session()->flash('success','Città aggiornata');
+            $this->resetFields();
+            $this->update = false;
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function cancel()
+    {
+        $this->add = false;
+        $this->update = false;
+        $this->resetFields();
+    }
+
+    public function delete($id)
+    {
+        try{
+            \App\Models\City::find($id)->delete();
+            session()->flash('success',"Città eliminata");
+        }catch(\Exception $e){
+            session()->flash('error','Errore (' . $e->getMessage() . ')');
+        }
+    }
+}

+ 487 - 0
app/Http/Livewire/Course.php

@@ -0,0 +1,487 @@
+<?php
+
+namespace App\Http\Livewire;
+use App\Http\Middleware\TenantMiddleware;
+use Livewire\Component;
+use Illuminate\Support\Facades\Auth;
+class Course extends Component
+{
+
+    protected $listeners = ['setCausal' => 'setCausal', 'setSubscriptionCausal' => 'setSubscriptionCausal'];
+
+    public $records, $parent_id, $name, $enabled, $dataId, $update = false, $add = false;
+    public $course_type_id,
+        $course_duration_id,
+        $course_frequency_id,
+        $course_level_id,
+        $causal_id,
+        $category_id,
+        $sub_causal_id,
+        $max_members,
+        $instructor_id,
+        $year,
+        $price,
+        $subscription_price,
+        $months,
+        $when,
+        $prices,
+        $type,
+        $date_from, $date_to;
+
+    public $categories = array();
+
+    public $selectedYear = '';
+
+    public $msgPrices = '';
+    public $msgWhen = '';
+
+    public $course_types = [];
+    public $course_durations = [];
+    public $course_frequencies = [];
+    public $course_levels = [];
+    public $course_subscriptions = [];
+    public $instructors = [];
+    public $causals = [];
+
+    public $course_years = [];
+
+    public $monthList = [];
+
+    public $typeIN = 'IN';
+    public $setSubscriptionCausal = 'setSubscriptionCausal';
+
+    // public $selectedMonthList = [];
+
+    protected $rules = [
+        'name' => 'required',
+        'course_frequency_id' => 'required',
+        'course_level_id' => 'required',
+        'date_from' => 'required',
+        'date_to' => 'required',
+        'year' => 'required',
+        'subscription_price' => 'required|min:0|not_in:0',
+        /*'course_type_id' => 'required',
+        'course_duration_id' => 'required',
+
+        'causal_id' => 'required',
+        'sub_causal_id' => 'required',*/
+    ];
+
+    protected $messages = [
+        'name.required' => 'Il nome è obbligatorio',
+        'subscription_price.required' => 'Deve essere maggionre di zero',
+        'subscription_price.not_in' => 'Deve essere maggionre di zero',
+        /*'causal_id' => 'Campo obbligatorio',
+        'sub_causal_id' => 'Campo obbligatorio',*/
+    ];
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+    }
+    public function resetFields(){
+        $this->name = '';
+        $this->parent_id = null;
+        $this->course_type_id = null;
+        $this->course_duration_id = null;
+        $this->course_frequency_id = null;
+        $this->course_level_id = null;
+        $this->category_id = null;
+        $this->causal_id = null;
+        $this->sub_causal_id = null;
+        $this->max_members = 0;
+        $this->instructor_id = null;
+        $this->year = null;
+        $this->price = 0;
+        $this->subscription_price = 0;
+        $this->date_from = null;
+        $this->date_to = null;
+        $this->months = array();
+        $this->enabled = true;
+        $this->type = 'standard';
+        $this->when = array();
+        $this->when[] = array('day' => array(), 'from' => '', 'to' => '');
+        $this->prices = [];
+        $this->prices[] = array('course_subscription_id' => null, 'price' => 0);
+        $this->emit('load-data-table');
+    }
+
+    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);
+        }
+    }
+
+    public function mount(){
+
+        if(Auth::user()->level != env('LEVEL_ADMIN', 0))
+            return redirect()->to('/dashboard');
+
+        $this->categories = array();
+
+        $this->getCategories(\App\Models\Category::select('id', 'name')->where('parent_id', null)->get(), 0);
+
+        for($i=date("Y"); $i<=date("Y") + 1; $i++)
+        {
+            $this->monthList[$i][1] = "Gennaio";
+            $this->monthList[$i][2] = "Febbraio";
+            $this->monthList[$i][3] = "Marzo";
+            $this->monthList[$i][4] = "Aprile";
+            $this->monthList[$i][5] = "Maggio";
+            $this->monthList[$i][6] = "Giugno";
+            $this->monthList[$i][7] = "Luglio";
+            $this->monthList[$i][8] = "Agosto";
+            $this->monthList[$i][9] = "Settembre";
+            $this->monthList[$i][10] = "Ottobre";
+            $this->monthList[$i][11] = "Novembre";
+            $this->monthList[$i][12] = "Dicembre";
+        }
+        $this->course_types = \App\Models\CourseType::select('*')->where('enabled', true)->get();
+        $this->course_durations = \App\Models\CourseDuration::select('*')->where('enabled', true)->get();
+        $this->course_levels = \App\Models\CourseLevel::select('*')->where('enabled', true)->get();
+        $this->course_subscriptions = \App\Models\CourseSubscription::select('*')->where('enabled', true)->get();
+        $this->course_frequencies = \App\Models\CourseFrequency::select('*')->where('enabled', true)->get();
+        $this->causals = \App\Models\Causal::select('*')->where('type', 'IN')->where('enabled', true)->get();
+        $this->instructors = \App\Models\User::select('*')->where('level', 2)->get();
+
+        $this->course_years = \App\Models\Course::select('year')->where('year', '<>', '')->groupBy('year')->pluck('year');
+
+    }
+
+    public function render()
+    {
+        if (isset($_GET["year"]))
+            $this->selectedYear = $_GET["year"];
+        else
+            // $this->selectedYear =  sizeof($this->course_years) > 0 ? $this->course_years[0] : '';
+            $this->selectedYear = date("Y") . "-" . (date("Y") + 1);
+
+        $this->records = \App\Models\Course::where('parent_id', null)->where('year', $this->selectedYear)->with('type', 'duration')->get();
+        return view('livewire.course');
+    }
+
+    public function add()
+    {
+        $this->resetFields();
+        $this->add = true;
+        $this->update = false;
+
+        $this->emit('setEdit', true);
+    }
+
+    /*
+    public function addLevel($parent_id)
+    {
+        $this->resetFields();
+        $this->parent_id = $parent_id;
+        $this->add = true;
+        $this->update = false;
+    }
+    */
+    public function store()
+    {
+        $this->validate();
+        try {
+
+            $this->msgPrices = '';
+            $this->msgWhen = '';
+
+            if ($this->type == 'standard') {
+                if ($this->when[0]['from'] == '')
+                    $this->msgWhen = 'Devi inserire almeno un giorno';
+            }
+    
+            if ($this->prices[0]['course_subscription_id'] == null)
+                $this->msgPrices = 'Devi inserire almeno un prezzo';
+            
+            $subscriptions = array_column($this->prices, 'course_subscription_id');
+            $unique_subscriptions = array_unique($subscriptions);
+            if (count($subscriptions) != count($unique_subscriptions))
+                $this->msgPrices = 'È possibile aggiungere solo un abbonamento per ciascun tipo';
+
+            if ($this->msgPrices == '' &&  $this->msgWhen == '')
+            {
+
+                $course = new \App\Models\Course();
+
+                $course->name = $this->name;
+                $course->parent_id = $this->parent_id;
+                $course->course_type_id = $this->course_type_id;
+                $course->course_duration_id = $this->course_duration_id;
+                $course->course_frequency_id = $this->course_frequency_id;
+                $course->course_level_id = $this->course_level_id;
+                $course->date_from = $this->date_from;
+                $course->date_to = $this->date_to;
+                $course->category_id = $this->category_id;
+                //$course->causal_id = $this->causal_id;
+                //$course->sub_causal_id = $this->sub_causal_id;
+                $course->max_members = $this->max_members;
+                $course->instructor_id = $this->instructor_id;
+                $course->year = $this->year;
+                $course->price = currencyToDouble($this->price);
+                $course->subscription_price = currencyToDouble($this->subscription_price);
+                $course->months = json_encode($this->months);
+                $course->type = $this->type;
+                $course->when = json_encode($this->when);
+                $course->prices = json_encode($this->prices);
+                $course->enabled = $this->enabled;
+                $course->save();
+
+                $lev = '';
+                if ($this->course_level_id > 0)
+                    $lev = \App\Models\CourseLevel::findOrFail($this->course_level_id)->name;
+
+                $freq = '';
+                if ($this->course_frequency_id > 0)
+                    $freq = \App\Models\CourseFrequency::findOrFail($this->course_frequency_id)->name;
+
+                $mFrom = getMonthName(date("n", strtotime($this->date_from)));
+                $mTo = getMonthName(date("n", strtotime($this->date_to)));
+                // Creo le causali di pagamento
+                //$causal = "PAGAMENTO CORSO [nome corso]-LIVELLO-FREQUENZA-ANNO-[tipo abbonamento]-[mese inizio]/[mese fine]
+
+                $causal = "PAGAMENTO CORSO " . $this->name . " - " . $lev . " - " . $freq . " - " . $this->year . " - " . $mFrom . "/" . $mTo;
+                $cp = \App\Models\Causal::where('name', $causal)->first();
+                if (!$cp)
+                {
+                    $cp = new \App\Models\Causal();
+                    $cp->name = $causal;
+                    $cp->type = "IN";
+                    $cp->parent_id = null;
+                    $cp->money = false;
+                    $cp->corrispettivo_fiscale = false;
+                    $cp->no_receipt = false;
+                    $cp->user_status = false;
+                    $cp->no_first = false;
+                    $cp->no_records = false;
+                    $cp->enabled = true;
+                    $cp->save();
+                }
+                $course->causal_id = $cp->id;
+
+                $causal = "PAGAMENTO ISCRIZIONE " . $this->name . " - " . $lev . " - " . $freq . " - " . $this->year . " - " . $mFrom . "/" . $mTo;
+                $ci = \App\Models\Causal::where('name', $causal)->first();
+                if (!$ci)
+                {
+                    $ci = new \App\Models\Causal();
+                    $ci->name = $causal;
+                    $ci->type = "IN";
+                    $ci->parent_id = null;
+                    $ci->money = false;
+                    $ci->corrispettivo_fiscale = false;
+                    $ci->no_receipt = false;
+                    $ci->user_status = false;
+                    $ci->no_first = false;
+                    $ci->no_records = false;
+                    $ci->enabled = true;
+                    $ci->save();
+                }
+                $course->sub_causal_id = $ci->id;
+
+                $course->save();
+
+                session()->flash('success','Corso creato');
+                $this->resetFields();
+                $this->add = false;
+                $this->emit('setEdit', false);
+            }
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function edit($id){
+        $this->resetFields();
+        try {
+            $course = \App\Models\Course::findOrFail($id);
+            if( !$course) {
+                session()->flash('error','Corso non trovato');
+            } else {
+                $this->name = $course->name;
+                $this->enabled = $course->enabled;
+                $this->parent_id = $course->parent_id;
+                $this->course_type_id = $course->course_type_id;
+                $this->course_duration_id = $course->course_duration_id;
+                $this->course_frequency_id = $course->course_frequency_id;
+                $this->course_level_id = $course->course_level_id;
+                $this->date_from = $course->date_from;
+                $this->date_to = $course->date_to;
+                $this->category_id = $course->category_id;
+                $this->causal_id = $course->causal_id;
+                $this->sub_causal_id = $course->sub_causal_id;
+                $this->max_members = $course->max_members;
+                $this->instructor_id = $course->instructor_id;
+                $this->year = $course->year;
+                $this->price = formatPrice($course->price);
+                $this->subscription_price = formatPrice($course->subscription_price);
+                $this->months = json_decode($course->months);
+                $this->when = array();
+                if ($course->when != null)
+                {
+                    foreach(json_decode($course->when) as $z)
+                    {
+                        $this->when[] = array("day" => $z->day, "from" => $z->from, "to" => $z->to);
+                    }
+                }
+                else
+                {
+                    $this->when[] = array('day' => array(), 'from' => '', 'to' => '');
+                }
+
+                $this->prices = array();
+                if ($course->prices != null)
+                {
+                    foreach(json_decode($course->prices) as $z)
+                    {
+                        $this->prices[] = array("course_subscription_id" => $z->course_subscription_id, "price" => $z->price);
+                    }
+                }
+                else
+                {
+                    $this->prices[] = array('course_subscription_id' => null, 'price' => 0);
+                }
+                $this->type = $course->type;
+                $this->dataId = $course->id;
+                $this->update = true;
+                $this->add = false;
+            }
+            $this->emit('setEdit', true);
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function update()
+    {
+        $this->validate();
+        try {
+            \App\Models\Course::whereId($this->dataId)->update([
+                'name' => $this->name,
+                'parent_id' => $this->parent_id,
+                'course_type_id' => $this->course_type_id,
+                'course_duration_id' => $this->course_duration_id,
+                'course_frequency_id' => $this->course_frequency_id,
+                'course_level_id' => $this->course_level_id,
+                'date_from' => $this->date_from,
+                'date_to' => $this->date_to,
+                'category_id' => $this->category_id,
+                'causal_id' => $this->causal_id,
+                'sub_causal_id' => $this->sub_causal_id,
+                'max_members' => $this->max_members,
+                'instructor_id' => $this->instructor_id,
+                'year' => $this->year,
+                'price' => currencyToDouble($this->price),
+                'subscription_price' => currencyToDouble($this->subscription_price),
+                'months' => json_encode($this->months),
+                'type' => $this->type,
+                'when' => json_encode($this->when),
+                'prices' => json_encode($this->prices),
+                'enabled' => $this->enabled
+            ]);
+            session()->flash('success','Corso aggiornato');
+            $this->resetFields();
+            $this->update = false;
+            $this->emit('setEdit', false);
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function cancel()
+    {
+        $this->add = false;
+        $this->update = false;
+        $this->resetFields();
+        $this->emit('setEdit', false);
+    }
+
+    public function delete($id)
+    {
+        try{
+            \App\Models\Course::find($id)->delete();
+            session()->flash('success',"Corso eliminato");
+            return redirect(request()->header('Referer'));
+        }catch(\Exception $e){
+            session()->flash('error','Errore (' . $e->getMessage() . ')');
+        }
+    }
+
+    public function setCausal($id, $idx)
+    {
+        $this->causal_id = $id;
+    }
+
+    public function setSubscriptionCausal($id, $idx)
+    {
+        $this->sub_causal_id = $id;
+    }
+
+    public function setCategory($id)
+    {
+        $this->category_id = $id;
+    }
+
+    public function duplicate($id, $isMultiple){
+        $course = \App\Models\Course::findOrFail($id);
+        $newCourse = $course->replicate();
+        $newYear = date("Y") . "-" . (date("Y") + 1);
+        if ($course->year != '')
+        {
+            list($u, $y) = explode("-", $course->year);
+            $newYear = ($u + 1) . "-" . ($u + 2);
+        }
+        $newCourse->year = $newYear;
+        $newCourse->save();
+        if (!$isMultiple)
+            $this->edit($newCourse->id);
+    }
+
+    public function duplicateMultiple($ids){
+        foreach($ids as $id)
+        {
+            $this->duplicate($id, true);
+        }
+        return redirect()->to('/courses');
+    }
+
+    public function setDay($idx, $d)
+    {
+
+        if (in_array($d, $this->when[$idx]["day"]))
+        {
+            $i = array_search($d, $this->when[$idx]["day"]);
+            array_splice($this->when[$idx]["day"], $i, 1);
+
+        }
+        else
+        {
+            $this->when[$idx]["day"][] = $d;
+        }
+
+    }
+
+    public function addRow()
+    {
+        $this->when[] = array('day' => array(), 'from' => '', 'to' => '');
+    }
+
+    public function delRow($idx)
+    {
+        unset($this->when[$idx]);
+    }
+
+    public function addPrice()
+    {
+        $this->prices[] = array('course_subscription_id' => null, 'price' => 0);
+    }
+
+    public function delPrice($idx)
+    {
+        unset($this->prices[$idx]);
+    }
+
+}

+ 136 - 0
app/Http/Livewire/CourseDuration.php

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

+ 130 - 0
app/Http/Livewire/CourseFrequency.php

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

+ 132 - 0
app/Http/Livewire/CourseLevel.php

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

+ 918 - 0
app/Http/Livewire/CourseList.php

@@ -0,0 +1,918 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+use App\Models\MemberCourse;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Facades\Auth;
+use App\Http\Middleware\TenantMiddleware;
+class CourseList extends Component
+{
+
+    public $records = array();
+    public $recordsNoPaginate = array();
+    public $courses = array();
+
+    public $start = 0;
+    public $totalRecords = 0;
+    public $pages = 0;
+    public $currentPage = 1;
+    public $pageLength = 10;
+
+    public $totS = [];
+    public $totSExcel = [];
+
+    public $sort = '';
+    public $dir = '';
+
+    public $hasFilter = false;
+
+    public $courseId = 0;
+
+    public $filterCourse = [];
+    public $filterLevel = [];
+    public $filterFrequency = [];
+    public $filterType = [];
+    public $filterDuration = [];
+
+    public $course_durations = [];
+    public $course_types = [];
+    public $course_frequencies = [];
+    public $course_levels = [];
+    public $course_years = [];
+
+    public $totals = [];
+    public $totalIsc = [];
+
+    public $selectedCourseId;
+    public $selectedMemberId;
+    public $aaa;
+
+    public $months = array('Set', 'Ott', 'Nov', 'Dic', 'Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago');
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+    }
+    public function mount()
+    {
+
+        $this->selectedCourseId = 0;
+        $this->selectedMemberId = 0;
+
+        $this->course_types = \App\Models\CourseType::select('*')->where('enabled', true)->get();
+        $this->course_durations = \App\Models\CourseDuration::select('*')->where('enabled', true)->get();
+        $this->course_levels = \App\Models\CourseLevel::select('*')->where('enabled', true)->get();
+        $this->course_frequencies = \App\Models\CourseFrequency::select('*')->where('enabled', true)->get();
+        $this->course_years = \App\Models\Course::select('year')->where('year', '<>', '')->groupBy('year')->pluck('year');
+
+        $this->courses = \App\Models\Course::orderBy('name')->groupBy('name')->pluck('name');
+
+        //if (sizeof($this->courses) > 0)
+        //    $this->courseId = $this->courses[0]->id;
+
+    }
+
+    public function updatedfilterCourse($value)
+    {
+        $this->emit('load-data-table');
+    }
+
+    public function updatedfilterLevel($value)
+    {
+        $this->emit('load-data-table');
+    }
+
+    public function updatedfilterFrequency($value)
+    {
+        $this->emit('load-data-table');
+    }
+
+    public function updatedfilterType($value)
+    {
+        $this->emit('load-data-table');
+    }
+
+    public function updatedfilterDuration($value)
+    {
+        $this->emit('load-data-table');
+    }
+
+    public function updatedpageLength($value)
+    {
+        $this->emit('load-data-table');
+    }
+
+
+    public function render()
+    {
+
+        $member_course = \App\Models\MemberCourse::with('member')->with('course');
+
+        if ($this->hasFilter) {
+            if (isset($_GET["search"]["value"])) {
+
+                if ($_GET["search"]["value"] != '') {
+                    $v = str_replace("'", "\'", stripcslashes($_GET["search"]["value"]));
+                    $member_ids = \App\Models\Member::where(function ($query) use ($v) {
+                        $query->whereRaw("CONCAT(first_name, ' ', last_name) like '%" . $v . "%'")
+                            ->orWhereRaw("CONCAT(last_name, ' ', first_name) like '%" . $v . "%'");
+                    })->pluck('id');
+                    /*
+                    $v = str_replace("'", "\'", stripcslashes($_GET["search"]["value"]));
+                    $member_ids = \App\Models\Member::where(function ($query) use ($v) {
+                        $query->where('first_name', 'like', '%' . $v . '%')
+                                ->orWhere('last_name', 'like', '%' . $v . '%');
+                    })->pluck('id');*/
+                    $member_course = $member_course->whereIn('member_id', $member_ids);
+                }
+            }
+
+            if (sizeof($this->filterCourse) > 0) {
+                $course_ids = [];
+                $courses = $this->filterCourse;
+                foreach ($courses as $c) {
+                    $all = \App\Models\Course::where('name', 'like', '%' . $c . "%")->get();
+                    foreach ($all as $a) {
+                        $course_ids[] = $a->id;
+                    }
+                }
+                $member_course = $member_course->whereIn('course_id', $course_ids);
+            }
+
+            if (sizeof($this->filterLevel) > 0) {
+                $course_ids = \App\Models\Course::whereIn('course_level_id', $this->filterLevel)->pluck('id');
+                $member_course = $member_course->whereIn('course_id', $course_ids);
+            }
+
+            if (sizeof($this->filterFrequency) > 0) {
+                $course_ids = \App\Models\Course::whereIn('course_frequency_id', $this->filterFrequency)->pluck('id');
+                $member_course = $member_course->whereIn('course_id', $course_ids);
+            }
+            if (sizeof($this->filterType) > 0) {
+                $course_ids = \App\Models\Course::whereIn('course_type_id', $this->filterType)->pluck('id');
+                $member_course = $member_course->whereIn('course_id', $course_ids);
+            }
+            if (sizeof($this->filterDuration) > 0) {
+                $course_ids = \App\Models\Course::whereIn('course_duration_id', $this->filterDuration)->pluck('id');
+                $member_course = $member_course->whereIn('course_id', $course_ids);
+            }
+        }
+
+        $totals = [];
+        $totalIsc = [];
+
+        $datas = [];
+
+        $xxx = 1;
+
+        /*
+        $sortColumn = '';
+        if (isset($_GET["order"]))
+        {
+
+        }
+        */
+
+        $column = '';
+        $sort_value = 0;
+        if ($this->sort != '') {
+            $f = $this->sort;
+            $d = $this->dir;
+            if ($f >= 5 && $f <= 16) {
+                $column = 'column_' . ($f - 2);
+                if (session()->get('sort_column')) {
+                    if (session()->get('sort_column') != $f) {
+                        session()->put('sort_column', $f);
+                        session()->put('sort_order', $d);
+                        session()->put('sort_value', 0);
+                        $sort_value = 0;
+                    } else {
+                        if (session()->get('sort_order') == $d) {
+                            //session()->put('sort_value', 0);
+                            $sort_value = session()->get('sort_value', 0);
+                        } else {
+                            if (session()->get('sort_value', 0) == 0) {
+                                $sort_value = 1;
+                            }
+                            if (session()->get('sort_value', 0) == 1) {
+                                $sort_value = 2;
+                            }
+                            if (session()->get('sort_value', 0) == 2) {
+                                $sort_value = 3;
+                            }
+                            if (session()->get('sort_value', 0) == 3) {
+                                $sort_value = 0;
+                            }
+                            session()->put('sort_value', $sort_value);
+                        }
+                        session()->put('sort_order', $d);
+                    }
+                } else {
+                    session()->put('sort_column', $f);
+                    session()->put('sort_order', $d);
+                    session()->put('sort_value', 0);
+                    $sort_value = 0;
+                }
+            }
+        }
+
+        //print $sort_value;
+
+        $totals = [];
+        $prices = [];
+
+        $member_course_totals = $member_course->get();
+        foreach ($member_course_totals as $x) {
+
+            $price = 0;
+            $price = $x->price; // $x->course->price;
+            $subPrice = $x->subscription_price; // $x->course->subscription_price;
+            $records = \App\Models\Record::where('member_course_id', $x->id)->where('deleted', 0)->get();
+            $prices = [];
+            foreach ($records as $record) {
+                foreach ($record->rows as $row) {
+
+                    if ($row->causal_id == $x->course->sub_causal_id) //  || str_contains(strtolower($row->note), 'iscrizione'))
+                    //if (str_contains(strtolower($row->note), 'iscrizione'))
+                    {
+                        $subPrice = $row->amount;
+                    }
+                    if ($row->causal_id == $x->course->causal_id && !str_contains(strtolower($row->note), 'iscrizione')) {
+                        $tot = sizeof(json_decode($row->when));
+                        foreach (json_decode($row->when) as $m) {
+                            if (isset($prices[$m->month]))
+                                $prices[$m->month] += $row->amount / $tot;
+                            else
+                                $prices[$m->month] = $row->amount / $tot;
+                        }
+                    }
+                }
+            }
+
+            for ($i = 1; $i <= 12; $i++) {
+
+                $cls = $this->getColor($x->months, $i, isset($prices[$i]) && $prices[$i] == $price);
+                if ($cls != 'wgrey') {
+
+                    if (!isset($totals[$i])) {
+                        $totals[$i]['green'] = 0;
+                        $totals[$i]['orange'] = 0;
+                        $totals[$i]['yellow'] = 0;
+                    }
+                    if ($cls == 'yellow') {
+                        $totals[$i][$cls] += 1;
+                    } else {
+                        $p = isset($prices[$i]) ? $prices[$i] : $price;
+                        //if (isset($totals[$i][$cls]))
+                        $totals[$i][$cls] += $p;
+                        //else
+                        //$totals[$i][$cls] = $p;
+                    }
+                }
+            }
+
+            $sub = $x->subscribed ? "Y" : "N";
+            if (isset($totalIsc[$sub]))
+                $totalIsc[$sub] += $subPrice;
+            else
+                $totalIsc[$sub] = $subPrice;
+
+            $s = 0;
+            if ($column != '') {
+                $z = 0;
+                switch ($column) {
+                    case 'column_3':
+                        $z = 9;
+                        break;
+                    case 'column_4':
+                        $z = 10;
+                        break;
+                    case 'column_5':
+                        $z = 11;
+                        break;
+                    case 'column_6':
+                        $z = 12;
+                        break;
+                    case 'column_7':
+                        $z = 1;
+                        break;
+                    case 'column_8':
+                        $z = 2;
+                        break;
+                    case 'column_9':
+                        $z = 3;
+                        break;
+                    case 'column_10':
+                        $z = 4;
+                        break;
+                    case 'column_11':
+                        $z = 5;
+                        break;
+                    case 'column_12':
+                        $z = 6;
+                        break;
+                    case 'column_13':
+                        $z = 7;
+                        break;
+                    case 'column_14':
+                        $z = 8;
+                        break;
+                    default:
+                        $z = 0;
+                        break;
+                }
+                $c = getColor($x->months, $z);
+                if ($sort_value == 0) {
+                    switch ($c) {
+                        case 'wgrey':
+                            $s = 0;
+                            break;
+                        case 'orange':
+                            $s = 1;
+                            break;
+                        case 'green':
+                            $s = 2;
+                            break;
+                        case 'yellow':
+                            $s = 3;
+                            break;
+                        default:
+                            $s = 0;
+                            break;
+                    }
+                }
+                if ($sort_value == 1) {
+                    switch ($c) {
+                        case 'wgrey':
+                            $s = 3;
+                            break;
+                        case 'orange':
+                            $s = 0;
+                            break;
+                        case 'green':
+                            $s = 1;
+                            break;
+                        case 'yellow':
+                            $s = 2;
+                            break;
+                        default:
+                            $s = 0;
+                            break;
+                    }
+                }
+                if ($sort_value == 2) {
+                    switch ($c) {
+                        case 'wgrey':
+                            $s = 2;
+                            break;
+                        case 'orange':
+                            $s = 3;
+                            break;
+                        case 'green':
+                            $s = 0;
+                            break;
+                        case 'yellow':
+                            $s = 1;
+                            break;
+                        default:
+                            $s = 0;
+                            break;
+                    }
+                }
+                if ($sort_value == 3) {
+                    switch ($c) {
+                        case 'wgrey':
+                            $s = 1;
+                            break;
+                        case 'orange':
+                            $s = 2;
+                            break;
+                        case 'green':
+                            $s = 3;
+                            break;
+                        case 'yellow':
+                            $s = 0;
+                            break;
+                        default:
+                            $s = 0;
+                            break;
+                    }
+                }
+            }
+
+            $datas[] = array(
+                "column_19" => $x->course->name,
+                "column_0" => $x->member->last_name,
+                "column_1" => $x->member->first_name,
+                "column_2" => $x->subscribed . "§" . formatPrice($subPrice),
+                "column_3" => $this->getColor($x->months, 9, isset($prices[9]) && $prices[9] >= $price) . "§" . formatPrice(isset($prices[9]) ? $prices[9] : $price) . "§" . (isset($prices[9]) && $prices[9] <= $price ? 'X' : ''),
+                "column_4" => $this->getColor($x->months, 10, isset($prices[10]) && $prices[10] >= $price) . "§" . formatPrice(isset($prices[10]) ? $prices[10] : $price) . "§" . (isset($prices[10]) && $prices[10] <= $price ? 'X' : ''),
+                "column_5" => $this->getColor($x->months, 11, isset($prices[11]) && $prices[11] >= $price) . "§" . formatPrice(isset($prices[11]) ? $prices[11] : $price) . "§" . (isset($prices[11]) && $prices[11] <= $price ? 'X' : ''),
+                "column_6" => $this->getColor($x->months, 12, isset($prices[12]) && $prices[12] >= $price) . "§" . formatPrice(isset($prices[12]) ? $prices[12] : $price) . "§" . (isset($prices[12]) && $prices[12] <= $price ? 'X' : ''),
+                "column_7" => $this->getColor($x->months, 1, isset($prices[1]) && $prices[1] >= $price) . "§" . formatPrice(isset($prices[1]) ? $prices[1] : $price) . "§" . (isset($prices[1]) && $prices[1] <= $price ? 'X' : ''),
+                "column_8" => $this->getColor($x->months, 2, isset($prices[2]) && $prices[2] >= $price) . "§" . formatPrice(isset($prices[2]) ? $prices[2] : $price) . "§" . (isset($prices[2]) && $prices[2] <= $price ? 'X' : ''),
+                "column_9" => $this->getColor($x->months, 3, isset($prices[3]) && $prices[3] >= $price) . "§" . formatPrice(isset($prices[3]) ? $prices[3] : $price) . "§" . (isset($prices[3]) && $prices[3] <= $price ? 'X' : ''),
+                "column_10" => $this->getColor($x->months, 4, isset($prices[4]) && $prices[4] >= $price) . "§" . formatPrice(isset($prices[4]) ? $prices[4] : $price) . "§" . (isset($prices[4]) && $prices[4] <= $price ? 'X' : ''),
+                "column_11" => $this->getColor($x->months, 5, isset($prices[5]) && $prices[5] >= $price) . "§" . formatPrice(isset($prices[5]) ? $prices[5] : $price) . "§" . (isset($prices[5]) && $prices[5] <= $price ? 'X' : ''),
+                "column_12" => $this->getColor($x->months, 6, isset($prices[6]) && $prices[6] >= $price) . "§" . formatPrice(isset($prices[6]) ? $prices[6] : $price) . "§" . (isset($prices[6]) && $prices[6] <= $price ? 'X' : ''),
+                "column_13" => $this->getColor($x->months, 7, isset($prices[7]) && $prices[7] >= $price) . "§" . formatPrice(isset($prices[7]) ? $prices[7] : $price) . "§" . (isset($prices[7]) && $prices[7] <= $price ? 'X' : ''),
+                "column_14" => $this->getColor($x->months, 8, isset($prices[8]) && $prices[8] >= $price) . "§" . formatPrice(isset($prices[8]) ? $prices[8] : $price) . "§" . (isset($prices[8]) && $prices[8] <= $price ? 'X' : ''),
+                "column_15" => $x->course_id,
+                "column_16" => $x->id,
+                "column_17" => $x->member_id,
+                "column_18" => $xxx++,
+                "column_20" => $s
+            );
+        }
+
+        $count = $member_course->count();
+
+        $this->totSExcel = [];
+        $this->totS = [];
+
+        //$js = '';
+        $xx = 4;
+        $str = '';
+        if ($count > 0) {
+            $str .= "<a style='width:100%;float:right; text-align:right; display:block;' class=green><small>" . (isset($totalIsc["Y"]) ? formatPrice($totalIsc["Y"]) : 0) . "</small></a><br>";
+            $str .= "<a style='width:100%;float:right; text-align:right; display:block;' class=orange><small>" . (isset($totalIsc["N"]) ? formatPrice($totalIsc["N"]) : 0) . "</small></a><br>";
+            $str .= "<a style='width:100%;float:right; text-align:right; display:block;' class=yellow><small>0</small></a><br>";
+
+            $this->totSExcel[] = array('green' => (isset($totalIsc["Y"]) ? formatPrice($totalIsc["Y"]) : 0), 'orange' => (isset($totalIsc["N"]) ? formatPrice($totalIsc["N"]) : 0), 'yellow' => 0);
+        }
+        $this->totS[] = $str;
+
+        $str = "";
+        foreach ($totals as $z => $t) {
+            if ($z == 1) $xx = 5;
+            if ($z == 2) $xx = 6;
+            if ($z == 3) $xx = 7;
+            if ($z == 4) $xx = 8;
+            if ($z == 5) $xx = 9;
+            if ($z == 6) $xx = 10;
+            if ($z == 7) $xx = 11;
+            if ($z == 8) $xx = 12;
+            if ($z == 9) $xx = 1;
+            if ($z == 10) $xx = 2;
+            if ($z == 11) $xx = 3;
+            if ($z == 12) $xx = 4;
+            $str = '';
+            $aaa = [];
+            foreach ($t as $x => $c) {
+                $y = $x == 'yellow' ? $c : formatPrice($c);
+                $str .= "<a style='width:100%;float:right; text-align:right; display:block;' class=" . $x . "><small>" . $y . "</small></a><br>";
+                $aaa[$x] = $y;
+            }
+
+            $this->totSExcel[$xx] = $aaa;
+            $this->totS[$xx] = $str;
+            //$js .= $xx . "§" . $str . "_";
+
+            $xx += 1;
+        }
+
+        for ($e = sizeof($this->totS); $e <= 12; $e++) {
+            $this->totS[] = '';
+        }
+
+        if ($this->sort != '') {
+            $s = $this->sort;
+            if ($s == 1) $s = 21;
+            if ($column != '')
+                array_multisort(array_column($datas, 'column_20'), SORT_ASC, SORT_NATURAL | SORT_FLAG_CASE, $datas);
+            else
+                array_multisort(array_column($datas, 'column_' . ($s - 2)), $this->dir == "ASC" ? SORT_ASC : SORT_DESC, SORT_NATURAL | SORT_FLAG_CASE, $datas);
+        }
+
+        $xxx = 1;
+        foreach ($datas as $yyy => $d) {
+            $datas[$yyy]["column_18"] = $xxx++;
+        }
+
+        $this->totalRecords = sizeof($datas);
+
+        //$start = 0;
+
+        $this->recordsNoPaginate = [];
+        $this->recordsNoPaginate = $datas;
+        //if (isset($_GET["start"]))
+        $datas = array_slice($datas, $this->start, $this->pageLength);
+
+        $this->pages = ceil($this->totalRecords / $this->pageLength);
+        $this->records = [];
+        $this->records = $datas;
+
+        //$this->totS = $js;
+
+        return view('livewire.course_list');
+    }
+
+    public function setPage($page)
+    {
+        $this->currentPage = $page;
+        $this->start = $this->pageLength * ($page - 1);
+        $this->emit('load-data-table');
+    }
+
+    public function setSort($sort)
+    {
+        $this->sort = $sort;
+        if ($this->dir == '')
+            $this->dir = 'ASC';
+        else
+            $this->dir = $this->dir == 'ASC' ? 'DESC' : 'ASC';
+        $this->emit('load-data-table');
+    }
+
+    public function search()
+    {
+        $this->currentPage = 1;
+        $this->start = 0;
+        $this->hasFilter = true;
+        $this->emit('load-data-table');
+    }
+
+    public function getColor($months, $m, $all)
+    {
+        $class = "wgrey";
+        foreach (json_decode($months) as $mm) {
+            if ($mm->m == $m) {
+                if ($mm->status == "") {
+                    $class = "orange";
+                }
+                if ($mm->status == "1") {
+                    $class = "green";
+                }
+                if ($mm->status == "2") {
+                    $class = "yellow";
+                }
+                if (!$all && $class == "green") {
+                    $class = "orange";
+                }
+            }
+        }
+        return $class;
+    }
+
+    public function newPayment($course_id, $months, $member_id, $id, $subscription)
+    {
+
+        $newMonths = array();
+        if ($months != '') {
+            $mm = explode(",", $months);
+            foreach ($mm as $month) {
+                if ($month < 5) $month += 12;
+                if ($month >= 5) $month -= 4;
+                $newMonths[] = $month;
+            }
+        }
+
+        $c = \App\Models\Course::findOrFail($course_id);
+        $m = \App\Models\MemberCourse::findOrFail($id);
+
+        $price = $m->price;
+        $subscription_price = $m->subscription_price;
+
+        $records = \App\Models\Record::where('member_course_id', $m->id)->where('deleted', 0)->get();
+        foreach ($records as $record) {
+
+            if (in_array($month, json_decode($record->months))) {
+
+                foreach ($record->rows as $row) {
+
+
+                    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) {
+                            $price -= $row->amount / $tot;
+                        }
+                    }
+                }
+            }
+        }
+
+        return redirect()->to('/in?new=1&memberId=' . $member_id . (sizeof($newMonths) > 0 ? '&causalId=' . $c->causal_id : '') . '&subCausalId=' . $c->sub_causal_id . '&createSubscription=' . ($subscription ? 1 : 0) . (sizeof($newMonths) > 0 ? '&months=' . implode("|", $newMonths) : '') . (sizeof($newMonths) > 0 ? ('&price=' . $price) : '') . '&subscription_price=' . $subscription_price . "&courseId=" . $id);
+    }
+
+    public function suspendPayment($course_id, $month, $member_id, $id, $subscription)
+    {
+
+        $monthMap = [
+            1 => 9,  // September
+            2 => 10, // October
+            3 => 11, // November
+            4 => 12, // December
+            5 => 1,  // January
+            6 => 2,  // February
+            7 => 3,  // March
+            8 => 4,  // April
+            9 => 5,  // May
+            10 => 6, // June
+            11 => 7, // July
+            12 => 8  // August
+        ];
+
+        $dbMonth = isset($monthMap[$month]) ? $monthMap[$month] : $month;
+
+
+        $memberCourse = MemberCourse::where('id', $id)
+            ->where('member_id', $member_id)
+            ->where('course_id', $course_id)
+            ->first();
+
+
+        if (!$memberCourse) {
+            return response()->json(['error' => 'Non Trovato'], 404);
+        }
+
+        $monthsData = json_decode($memberCourse->months, true);
+        if (!is_array($monthsData)) {
+            return response()->json(['error' => 'Invalid months data format'], 400);
+        }
+
+        $monthUpdated = false;
+        foreach ($monthsData as &$monthData) {
+            if ($monthData['m'] == $dbMonth) {
+                $monthData['status'] = 2;
+                $monthUpdated = true;
+            }
+        }
+
+        if (!$monthUpdated) {
+            return response()->json(['error' => 'Month not found in data'], 404);
+        }
+
+        $memberCourse->months = json_encode($monthsData);
+        $memberCourse->save();
+        session()->flash('success', 'Payment suspended successfully');
+        return redirect()->to('/course_list');
+    }
+
+    public function resumePayment($course_id, $month, $member_id, $id, $subscription)
+    {
+        Log::info('resumePayment');
+        Log::info($course_id);
+        Log::info($month);
+        Log::info($member_id);
+        Log::info($id);
+
+        $monthMap = [
+            1 => 9,  // September
+            2 => 10, // October
+            3 => 11, // November
+            4 => 12, // December
+            5 => 1,  // January
+            6 => 2,  // February
+            7 => 3,  // March
+            8 => 4,  // April
+            9 => 5,  // May
+            10 => 6, // June
+            11 => 7, // July
+            12 => 8  // August
+        ];
+
+        $dbMonth = isset($monthMap[$month]) ? $monthMap[$month] : $month;
+
+
+        $memberCourse = MemberCourse::where('id', $id)
+            ->where('member_id', $member_id)
+            ->where('course_id', $course_id)
+            ->first();
+
+        if (!$memberCourse) {
+            return response()->json(['error' => 'Non Trovato'], 404);
+        }
+
+        $monthsData = json_decode($memberCourse->months, true);
+        Log::info('data mese', $monthsData);
+        if (!is_array($monthsData)) {
+            return response()->json(['error' => 'Invalid months data format'], 400);
+        }
+
+        $monthUpdated = false;
+        foreach ($monthsData as &$monthData) {
+            if ($monthData['m'] == $dbMonth) {
+                $monthData['status'] = "";
+                $monthUpdated = true;
+            }
+        }
+        Log::info($monthUpdated);
+
+        if (!$monthUpdated) {
+            return response()->json(['error' => 'Month not found in data'], 404);
+        }
+
+        $memberCourse->months = json_encode($monthsData);
+        $memberCourse->save();
+        session()->flash('success', 'Payment resumed successfully');
+        return redirect()->to('/course_list');
+    }
+    /*
+    public function newPayment()
+    {
+        $c = \App\Models\Course::findOrFail($this->selectedCourseId);
+        return redirect()->to('/in?new=1&memberId=' . $this->selectedMemberId . '&causalId=' . $c->causal_id . '&subCausalId=' . $c->sub_causal_id . '&createSubscription=0' . (sizeof($this->payMonths) > 0 ? '&months=' . implode("|", $this->payMonths) : "") . '&price=' . $c->price . '&subscription_price=' . $c->subscription_price . "&courseId=" . $this->selectedCourseId);
+    }
+    */
+    public function newSubscription($course_id, $member_id, $id)
+    {
+        $c = \App\Models\Course::findOrFail($course_id);
+        return redirect()->to('/in?new=1&memberId=' . $member_id . '&causalId=' . $c->causal_id . '&subCausalId=' . $c->sub_causal_id . '&createSubscription=1&price=0.00&subscription_price=' . $c->subscription_price . "&courseId=" . $id);
+    }
+
+    public function disableSearch()
+    {
+        $this->filterCourse = [];
+        $this->filterLevel = [];
+        $this->filterType = [];
+        $this->filterDuration = [];
+        $this->filterFrequency = [];
+        $this->hasFilter = false;
+    }
+
+    public function export()
+    {
+
+        $letters = array('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z');
+        $spreadsheet = new Spreadsheet();
+        $activeWorksheet = $spreadsheet->getActiveSheet();
+
+        //$activeWorksheet->setCellValue('A1', 'PrimaNota');
+        $activeWorksheet->setCellValue('A1', "Corso");
+        $activeWorksheet->setCellValue('B1', "Cognome");
+        $activeWorksheet->setCellValue('C1', "Nome");
+        $activeWorksheet->setCellValue('D1', "Iscrizione");
+        $activeWorksheet->setCellValue('E1', "Settembre");
+        $activeWorksheet->setCellValue('F1', "Ottobre");
+        $activeWorksheet->setCellValue('G1', "Novembre");
+        $activeWorksheet->setCellValue('H1', "Dicembre");
+        $activeWorksheet->setCellValue('I1', "Gennaio");
+        $activeWorksheet->setCellValue('J1', "Febbraio");
+        $activeWorksheet->setCellValue('K1', "Marzo");
+        $activeWorksheet->setCellValue('L1', "Aprile");
+        $activeWorksheet->setCellValue('M1', "Maggio");
+        $activeWorksheet->setCellValue('N1', "Giugno");
+        $activeWorksheet->setCellValue('O1', "Luglio");
+        $activeWorksheet->setCellValue('P1', "Agosto");
+
+        $count = 2;
+        foreach ($this->recordsNoPaginate as $idx => $record) {
+
+            $activeWorksheet->setCellValue('A' . $count, $record["column_19"]);
+            $activeWorksheet->setCellValue('B' . $count, $record["column_0"]);
+            $activeWorksheet->setCellValue('C' . $count, $record["column_1"]);
+            list($color, $value) = explode("§", $record["column_2"]);
+            $activeWorksheet->setCellValue('D' . $count, $value);
+            $c = '#FFFFFF';
+            if ($color == 0)
+                $c = 'ffa500';
+            if ($color == 1)
+                $c = '00ff00';
+
+            $activeWorksheet->getStyle('D' . $count . ':D' . $count)
+                ->getFill()
+                ->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
+                ->getStartColor()
+                ->setARGB($c);
+
+            for ($ii = 3; $ii <= 14; $ii++) {
+                list($color, $value) = explode("§", $record["column_" . $ii]);
+                $c = 'FFFFFF';
+                if ($color == 'orange')
+                    $c = 'ffa500';
+                if ($color == 'green')
+                    $c = '00ff00';
+                if ($color == 'yellow') {
+                    $c = '5088bf';
+                    $activeWorksheet->getStyle($letters[$ii + 1] . $count . ':' . $letters[$ii + 1] . $count)
+                        ->getFont()->getColor()->setARGB(\PhpOffice\PhpSpreadsheet\Style\Color::COLOR_WHITE);;
+                }
+                if ($color == 'wgrey')
+                    $value = '';
+
+                $activeWorksheet->setCellValue($letters[$ii + 1] . $count, $value);
+
+                $activeWorksheet->getStyle($letters[$ii + 1] . $count . ':' . $letters[$ii + 1] . $count)
+                    ->getFill()
+                    ->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
+                    ->getStartColor()
+                    ->setARGB($c);
+                /*$activeWorksheet->cells($letters[$ii + 1] . $count . ':' . $letters[$ii + 1] . $count, function ($cells) use ($c)
+                {
+                    $cells->setBackground($c);
+                    //$cells->setAlignment('center');
+                });*/
+            }
+
+            $activeWorksheet->getStyle("A1:P1")->getFont()->setBold(true);
+
+            $count++;
+        }
+
+        // Totali
+        $activeWorksheet->setCellValue('A' . (1 + $count), '');
+        $activeWorksheet->setCellValue('B' . (1 + $count), '');
+        $activeWorksheet->setCellValue('C' . (1 + $count), '');
+        for ($x = 0; $x <= sizeof($this->totSExcel); $x++) {
+
+            if (isset($this->totSExcel[$x])) {
+
+                $activeWorksheet->setCellValue($letters[$x + 3] . (1 + $count), isset($this->totSExcel[$x]['green']) ? $this->totSExcel[$x]['green'] : 0);
+                $activeWorksheet->getStyle($letters[$x + 3] . (1 + $count) . ':' . $letters[$x + 3] . (1 + $count))
+                    ->getFill()
+                    ->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
+                    ->getStartColor()
+                    ->setARGB('00ff00');
+
+                $activeWorksheet->setCellValue($letters[$x + 3] . (2 + $count), isset($this->totSExcel[$x]['orange']) ? $this->totSExcel[$x]['orange'] : 0);
+                $activeWorksheet->getStyle($letters[$x + 3] . (2 + $count) . ':' . $letters[$x + 3] . (2 + $count))
+                    ->getFill()
+                    ->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
+                    ->getStartColor()
+                    ->setARGB('ffa500');
+
+                $activeWorksheet->setCellValue($letters[$x + 3] . (3 + $count), isset($this->totSExcel[$x]['yellow']) ? $this->totSExcel[$x]['yellow'] : 0);
+                $activeWorksheet->getStyle($letters[$x + 3] . (3 + $count) . ':' . $letters[$x + 3] . (3 + $count))
+                    ->getFill()
+                    ->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
+                    ->getStartColor()
+                    ->setARGB('5088bf');
+                $activeWorksheet->getStyle($letters[$x + 3] . (3 + $count) . ':' . $letters[$x + 3] . (3 + $count))
+                    ->getFont()->getColor()->setARGB(\PhpOffice\PhpSpreadsheet\Style\Color::COLOR_WHITE);;
+            }
+        }
+
+        try {
+            $currentClient = session('currentClient', 'default');
+
+            $filename = 'pagamento_corsi_' . date("YmdHis") . '.xlsx';
+
+            $tempPath = sys_get_temp_dir() . '/' . $filename;
+
+            $writer = new Xlsx($spreadsheet);
+            $writer->save($tempPath);
+
+            $disk = Storage::disk('s3');
+
+            $s3Path = $currentClient . '/exports/' . $filename;
+
+            $exportFolderPath = $currentClient . '/exports/.gitkeep';
+            if (!$disk->exists($exportFolderPath)) {
+                $disk->put($exportFolderPath, '');
+                Log::info("Created exports folder for client: {$currentClient}");
+            }
+
+            $fileContent = file_get_contents($tempPath);
+            $uploaded = $disk->put($s3Path, $fileContent, 'private');
+
+            if (!$uploaded) {
+                throw new \Exception('Failed to upload file to Wasabi S3');
+            }
+
+            Log::info("Excel file uploaded to Wasabi", [
+                'client' => $currentClient,
+                'path' => $s3Path,
+                'size' => filesize($tempPath)
+            ]);
+
+            $downloadUrl = $disk->temporaryUrl($s3Path, now()->addHour());
+
+            if (file_exists($tempPath)) {
+                unlink($tempPath);
+            }
+
+            $this->emit('load-data-table');
+
+            return redirect($downloadUrl);
+        } catch (\Exception $e) {
+            Log::error('Error exporting to Wasabi S3', [
+                'error' => $e->getMessage(),
+                'client' => session('currentClient', 'unknown')
+            ]);
+
+            $currentClient = session('currentClient', 'default');
+            $clientFolder = storage_path('app/exports/' . $currentClient);
+
+            if (!is_dir($clientFolder)) {
+                mkdir($clientFolder, 0755, true);
+                Log::info("Created local client folder: {$clientFolder}");
+            }
+
+            $localPath = $clientFolder . '/pagamento_corsi_' . date("YmdHis") . '.xlsx';
+            $writer = new Xlsx($spreadsheet);
+            $writer->save($localPath);
+
+            $this->emit('load-data-table');
+
+            // Show error message to user
+            session()->flash('warning', 'File saved locally due to cloud storage error.');
+
+            return response()->download($localPath)->deleteFileAfterSend();
+        }
+    }
+}

+ 303 - 0
app/Http/Livewire/CourseMember.php

@@ -0,0 +1,303 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Illuminate\Support\Facades\Auth;
+use App\Http\Middleware\TenantMiddleware;
+use Livewire\Component;
+
+class CourseMember extends Component
+{
+
+    public $records = array();
+
+    public $courses = [];
+    public $course_frequencies = [];
+    public $course_types = [];
+    public $course_levels = [];
+    public $course_durations = [];
+    public $course_years = [];
+
+    public $fromYear = "";
+    public $toYear = "";
+    public $fromFromYear = "";
+    public $toToYear = "";
+
+    public $filterCourse = [];
+    public $filterLevel = [];
+    public $filterFrequency = [];
+    public $filterType = [];
+    public $filterDuration = [];
+
+    public $filterDays = [];
+    public $filterHours = [];
+    public $filterSubscription = "";
+    public $filterStatus = [];
+    public $filterYear = "";
+
+    /*public $chkCertificateNormal = 0;
+    public $chkCertificateAgonistico = 0;
+    public $chkCertificateScaduti = 0;*/
+    public $chkCertificateType = "";
+    public $chkCertificateScadenza = 0;
+    public $chkCard = [];
+
+    public $filter = '';
+    public $type = '';
+
+    public $filterFromPrevious = '';
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+    }
+    public function mount()
+    {
+
+        if (isset($_GET["id"]))
+        {
+            $this->filterFromPrevious = $_GET["id"];
+            $this->type = \App\Models\Course::findOrFail($_GET["id"])->type;
+        }
+
+        $this->course_types = \App\Models\CourseType::select('*')->where('enabled', true)->get();
+        $this->course_frequencies = \App\Models\CourseFrequency::select('*')->where('enabled', true)->get();
+        $this->course_levels = \App\Models\CourseLevel::select('*')->where('enabled', true)->get();
+        $this->course_durations = \App\Models\CourseDuration::select('*')->where('enabled', true)->get();
+        $this->course_years = \App\Models\Course::select('year')->where('year', '<>', '')->groupBy('year')->pluck('year');
+        $this->courses = \App\Models\Course::orderBy('name')->groupBy('name')->pluck('name');
+
+    }
+
+    public function render()
+    {
+
+        // Carico tutti i corsi associati
+        $this->filter = '';
+        $datas = \App\Models\MemberCourse::with('member');
+
+        if (sizeof($this->filterCourse) > 0)
+        {
+            $course_ids = [];
+            foreach($this->filterCourse as $c)
+            {
+                $all = \App\Models\Course::where('name', 'like', '%' . $c . "%")->get();
+                foreach($all as $a)
+                {
+                    $course_ids[] = $a->id;
+                }
+            }
+            $datas = $datas->whereIn('course_id', $course_ids);
+        }
+        if (sizeof($this->filterLevel) > 0)
+        {
+            $course_ids = \App\Models\Course::whereIn('course_level_id', $this->filterLevel)->pluck('id');
+            $datas = $datas->whereIn('course_id', $course_ids);
+        }
+        if (sizeof($this->filterFrequency) > 0)
+        {
+            $course_ids = \App\Models\Course::whereIn('course_frequency_id', $this->filterFrequency)->pluck('id');
+            $datas = $datas->whereIn('course_id', $course_ids);
+        }
+        if (sizeof($this->filterType) > 0)
+        {
+            $course_ids = \App\Models\Course::whereIn('course_type_id', $this->filterType)->pluck('id');
+            $datas = $datas->whereIn('course_id', $course_ids);
+        }
+        if (sizeof($this->filterDuration) > 0)
+        {
+            $course_ids = \App\Models\Course::whereIn('course_duration_id', $this->filterDuration)->pluck('id');
+            $datas = $datas->whereIn('course_id', $course_ids);
+        }
+        if (sizeof($this->filterDays) > 0)
+        {
+            $m_ids = [];
+            foreach($this->filterDays as $c)
+            {
+                $all = \App\Models\MemberCourse::where('when', 'like', "%" . $c . "%")->get();
+                foreach($all as $a)
+                {
+                    $m_ids[] = $a->member_id;
+                }
+            }
+            $datas = $datas->whereIn('member_id', $m_ids);
+        }
+        if (sizeof($this->filterHours) > 0)
+        {
+            $m_ids = [];
+            foreach($this->filterHours as $c)
+            {
+                $all = \App\Models\MemberCourse::where('when', 'like', '%"from":"' . $c . "%")->get();
+                foreach($all as $a)
+                {
+                    $m_ids[] = $a->member_id;
+                }
+            }
+            $datas = $datas->whereIn('member_id', $m_ids);
+        }
+        /*if ($this->filterDays != "")
+        {
+            $members_ids = \App\Models\MemberCourse::where('when', 'like', "%" . $this->filterDays . "%")->pluck('member_id');
+            $datas = $datas->whereIn('member_id', $members_ids);
+            $this->filter .= $this->filter != '' ? ', ' : '';
+            $this->filter .= "Giorni : " . $this->filterDays . " ";
+        }
+        if ($this->filterHours != "")
+        {
+            $members_ids = \App\Models\MemberCourse::where('when', 'like', '%"from":"' . $this->filterHours . "%")->pluck('member_id');
+            $datas = $datas->whereIn('member_id', $members_ids);
+            $this->filter .= $this->filter != '' ? ', ' : '';
+            $this->filter .= "Ore : " . $this->filterHours . " ";
+        }*/
+
+        if ($this->filterSubscription != "")
+        {
+            $ids = \App\Models\MemberCourse::where('subscribed', $this->filterSubscription == 1 ? true : false)->pluck('id');
+            $datas = $datas->whereIn('id', $ids);
+            $this->filter .= $this->filter != '' ? ', ' : '';
+            $this->filter .= "Pagata sottoscrizione : " . ($this->filterSubscription == 1 ? "SI" : "NO") . " ";
+        }
+        if ($this->chkCertificateType != "")
+        {
+            $types = \App\Models\MemberCertificate::where('type', $this->chkCertificateType)->pluck('member_id');
+            $datas = $datas->whereIn('member_id', $types);
+        }
+        if ($this->chkCertificateScadenza > 0)
+        {
+            if ($this->chkCertificateScadenza == "1")
+                $scad = \App\Models\MemberCertificate::where('expire_date', '<', date("Y-m-d"))->pluck('member_id');
+            if ($this->chkCertificateScadenza == "2")
+                $scad = \App\Models\MemberCertificate::whereBetween('expire_date', [date("Y-m-d"), date("Y-m-d", strtotime("+1 month"))])->pluck('member_id');
+            $datas = $datas->whereIn('member_id', $scad);
+        }
+        if ($this->fromYear != "")
+        {
+            $m_ids = \App\Models\Member::where('birth_date', '<', date("Y-m-d", strtotime("-" . $this->fromYear . " year", time())))->pluck('id');
+            $datas = $datas->whereIn('member_id', $m_ids);
+        }
+        if ($this->toYear != "")
+        {
+            $m_ids = \App\Models\Member::where('birth_date', '>', date("Y-m-d", strtotime("-" . $this->toYear . " year", time())))->pluck('id');
+            $datas = $datas->whereIn('member_id', $m_ids);
+        }
+        if ($this->fromFromYear != "")
+        {
+            $m_ids = \App\Models\Member::whereYear('birth_date', '>=', $this->fromFromYear)->pluck('id');
+            $datas = $datas->whereIn('member_id', $m_ids);
+        }
+        if ($this->toToYear != "")
+        {
+            $m_ids = \App\Models\Member::whereYear('birth_date', '<=', $this->toToYear)->pluck('id');
+            $datas = $datas->whereIn('member_id', $m_ids);
+        }
+        /*if ($_GET["toYear"] != "")
+        {
+            $x = $x->where('birth_date', '>', date("Y-m-d", strtotime("-" . $_GET["toYear"] . " year", time())));
+        }
+        if ($_GET["fromYearYear"] != "")
+        {
+            $x = $x->whereYear('birth_date', '>=', $_GET["fromYearYear"]);
+        }
+        if ($_GET["toYearYear"] != "")
+        {
+            $x = $x->whereYear('birth_date', '<=', $_GET["toYearYear"]);
+        }*/
+        /*
+        if ($this->chkCertificateNormal > 0)
+        {
+            $normal = \App\Models\MemberCertificate::where('type', 'N')->pluck('member_id');
+            $datas = $datas->whereIn('member_id', $normal);
+            $this->filter .= $this->filter != '' ? ', ' : '';
+            $this->filter .= "Certificato normale : SI ";
+        }
+        if ($this->chkCertificateAgonistico > 0)
+        {
+            $agonistic = \App\Models\MemberCertificate::where('type', 'A')->pluck('member_id');
+            $datas = $datas->whereIn('member_id', $agonistic);
+            $this->filter .= $this->filter != '' ? ', ' : '';
+            $this->filter .= "Certificato agonistico : SI ";
+        }
+        if ($this->chkCertificateScaduti > 0)
+        {
+            $scaduto = \App\Models\MemberCertificate::where('expire_date', '<', date("Y-m-d"))->pluck('member_id');
+            $datas = $datas->whereIn('member_id', $scaduto);
+            $this->filter .= $this->filter != '' ? ', ' : '';
+            $this->filter .= "Certificato scaduto : SI ";
+        }
+        if ($this->chkCertificateScadenza > 0)
+        {
+            $scadenza = \App\Models\MemberCertificate::whereBetween('expire_date', [date("Y-m-d"), date("Y-m-d", strtotime("+1 month"))])->pluck('member_id');
+            $datas = $datas->whereIn('member_id', $scadenza);
+            $this->filter .= $this->filter != '' ? ', ' : '';
+            $this->filter .= "Certificato in scadenza : SI ";
+        }*/
+        if (sizeof($this->chkCard) > 0)
+        {
+            $card_ids = \App\Models\MemberCard::whereIn('card_id', $this->chkCard)->pluck('member_id');
+            $datas = $datas->whereIn('member_id', $card_ids);
+            $this->filter .= $this->filter != '' ? ', ' : '';
+            $this->filter .= "Tessera : ";
+            foreach($this->chkCard as $card)
+            {
+                $this->filter .= \App\Models\Card::findOrFail($card)->name . " ";
+            }
+
+        }
+        if ($this->filterYear != "")
+        {
+            $course_ids = \App\Models\Course::where('year', $this->filterYear)->pluck('id');
+            $datas = $datas->whereIn('course_id', $course_ids);
+            $this->filter .= $this->filter != '' ? ', ' : '';
+            $this->filter .= "Anno : " . $this->filterYear . " ";
+        }
+
+        $aRet = [];
+        if (sizeof($this->filterStatus) > 0)
+        {
+            foreach($this->filterStatus as $s)
+            {
+                foreach($datas->get() as $aaa)
+                {
+
+                    $state = \App\Models\Member::findOrFail($aaa->member_id)->isActive();
+                    if ($state["status"] == $s)
+                        $aRet[] = $aaa;
+                }
+            }
+        }
+        else
+            $aRet = $datas->get();
+
+        $this->records = $aRet;
+
+        $this->emit('load-data-table');
+
+        return view('livewire.course_member');
+
+    }
+
+    public function disableSearch()
+    {
+        $this->filterCourse = [];
+        $this->filterLevel = [];
+        $this->filterType = [];
+        $this->filterDuration = [];
+        $this->filterFrequency = [];
+        $this->filterDays = [];
+        $this->filterHours = [];
+        $this->filterSubscription = "";
+        $this->filterStatus = [];
+        $this->chkCertificateNormal = 0;
+        $this->chkCertificateAgonistico = 0;
+        $this->chkCertificateScaduti = 0;
+        $this->chkCertificateScadenza = 0;
+        $this->chkCard = [];
+
+        $this->filterYear = "";
+        $this->fromYear = "";
+        $this->toYear = "";
+        $this->fromFromYear = "";
+        $this->toToYear = "";
+
+    }
+
+}

+ 17 - 0
app/Http/Livewire/CourseMemberOne.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+
+class CourseMemberOne extends Component
+{
+    public $records = [];
+
+    public function render()
+    {
+        $this->records[] = array('name' => 'Standard', 'type' => 'standard');
+        $this->records[] = array('name' => 'Personalizzati', 'type' => 'custom');
+        return view('livewire.course_member_one');
+    }
+}

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

@@ -0,0 +1,16 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+
+class CourseMemberTwo extends Component
+{
+    public $records = [];
+
+    public function render()
+    {
+        $this->records = \App\Models\Course::where('parent_id', null)->where('type', $_GET["type"])->with('type', 'duration', 'getins')->get();
+        return view('livewire.course_member_two');
+    }
+}

+ 135 - 0
app/Http/Livewire/CourseSubscription.php

@@ -0,0 +1,135 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+use Illuminate\Support\Facades\Auth;
+use App\Http\Middleware\TenantMiddleware;
+
+class CourseSubscription extends Component
+{
+    public $records, $name, $months, $month_day, $when_start, $enabled, $dataId, $update = false, $add = false;
+
+    protected $rules = [
+        'name' => 'required'
+    ];
+
+    protected $messages = [
+        'name.required' => 'Il nome è obbligatorio'
+    ];
+
+    public $sortField ='name';
+    public $sortAsc = true;
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+    }
+    public function sortBy($field)
+    {
+        if($this->sortField === $field)
+        {
+            $this->sortAsc = ! $this->sortAsc;
+        } else {
+            $this->sortAsc = true;
+        }
+
+        $this->sortField = $field;
+    }
+
+    public function resetFields(){
+        $this->name = '';
+        $this->months = 0;
+        $this->month_day = 0;
+        $this->when_start = '';
+        $this->enabled = true;
+        $this->emit('load-data-table');
+    }
+
+    public function render()
+    {
+        $this->records = \App\Models\CourseSubscription::select('id', 'name', 'enabled', 'months')->get();
+        return view('livewire.course_subscription');
+    }
+
+    public function add()
+    {
+        $this->resetFields();
+        $this->add = true;
+        $this->update = false;
+    }
+
+    public function store()
+    {
+        $this->validate();
+        try {
+            \App\Models\CourseSubscription::create([
+                'name' => $this->name,
+                'months' => $this->months,
+                'month_day' => $this->month_day,
+                'when_start' => $this->when_start,
+                'enabled' => $this->enabled
+            ]);
+            session()->flash('success','Dato creato');
+            $this->resetFields();
+            $this->add = false;
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function edit($id){
+        try {
+            $course_subscription = \App\Models\CourseSubscription::findOrFail($id);
+            if( !$course_subscription) {
+                session()->flash('error','Dato non trovato');
+            } else {
+                $this->name = $course_subscription->name;
+                $this->months = $course_subscription->months;
+                $this->month_day = $course_subscription->month_day;
+                $this->when_start = $course_subscription->when_start;
+                $this->enabled = $course_subscription->enabled;
+                $this->dataId = $course_subscription->id;
+                $this->update = true;
+                $this->add = false;
+            }
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function update()
+    {
+        $this->validate();
+        try {
+            \App\Models\CourseSubscription::whereId($this->dataId)->update([
+                'name' => $this->name,
+                'months' => $this->months,
+                'month_day' => $this->month_day,
+                'when_start' => $this->when_start,
+                'enabled' => $this->enabled
+            ]);
+            session()->flash('success','Dato aggiornato');
+            $this->resetFields();
+            $this->update = false;
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function cancel()
+    {
+        $this->add = false;
+        $this->update = false;
+        $this->resetFields();
+    }
+
+    public function delete($id)
+    {
+        try{
+            \App\Models\CourseSubscription::find($id)->delete();
+            session()->flash('success',"Dato eliminato");
+        }catch(\Exception $e){
+            session()->flash('error','Errore (' . $e->getMessage() . ')');
+        }
+    }
+}

+ 135 - 0
app/Http/Livewire/CourseType.php

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

+ 94 - 0
app/Http/Livewire/Courses.php

@@ -0,0 +1,94 @@
+<?php
+namespace App\Http\Livewire;
+use Livewire\Component;
+use App\Http\Middleware\TenantMiddleware;
+use Livewire\WithPagination;
+use Illuminate\Support\Facades\Auth;
+class Courses extends Component
+{
+    public $level_1 = [];
+    public $level_2 = [];
+    public $level_3 = [];
+
+    public $level_1_id = 0;
+    public $level_2_id = 0;
+    public $level_3_id = 0;
+
+    public $course_id = null;
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+    }
+    public function mount($course_id)
+    {
+        $this->course_id = $course_id;
+        if ($this->course_id != null)
+        {
+            $c = \App\Models\Course::findOrFail($this->course_id);
+            $ids = array_reverse($c->recursiveParent($c->parent_id, [$c->id]));
+            foreach($ids as $ii => $i)
+            {
+                if ($ii == 0)
+                {
+                    $this->level_1_id = $i;
+                }
+                if ($ii == 1)
+                {
+                    $this->level_2_id = $i;
+                }
+                if ($ii == 2)
+                {
+                    $this->level_3_id = $i;
+                }
+            }
+
+        }
+    }
+
+    public function updatedLevel1Id() {
+        $this->emit('setCourse', null);
+        $this->level_2_id = 0;
+        $this->level_2 = [];
+        $this->level_3_id = 0;
+        $this->level_3 = [];
+    }
+    public function updatedLevel2Id() {
+        $this->emit('setCourse', null);
+        $this->level_3_id = 0;
+        $this->level_3 = [];
+    }
+    public function updatedLevel3Id() {
+        $this->emit('setCourse', null);
+    }
+
+    public function render()
+    {
+        $reset = false;
+        if ($this->level_1_id > 0)
+        {
+            $this->level_2 = \App\Models\Course::where('parent_id', $this->level_1_id)->orderBy('name')->get();
+            if (sizeof($this->level_2) == 0)
+            {
+                $this->emit('setCourse', $this->level_1_id);
+                $reset = true;
+            }
+        }
+        if ($this->level_2_id > 0)
+        {
+            $this->level_3 = \App\Models\Course::where('parent_id', $this->level_2_id)->orderBy('name')->get();
+            if (sizeof($this->level_3) == 0)
+            {
+                $this->emit('setCourse', $this->level_2_id);
+                $reset = true;
+            }
+        }
+        if ($this->level_3_id > 0)
+        {
+            $this->emit('setCourse', $this->level_3_id);
+            $reset = true;
+        }
+        $this->level_1 = \App\Models\Course::where('parent_id', null)->orderBy('name')->get();
+
+        return view('livewire.courses');
+    }
+}

+ 834 - 0
app/Http/Livewire/Dashboard.php

@@ -0,0 +1,834 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+use Carbon\Carbon;
+use DateTimeZone;
+use Illuminate\Support\Facades\Log;
+
+class Dashboard extends Component
+{
+    // Existing properties
+    public $totMembers = 0;
+    public $totSuppliers = 0;
+    public $totTodayIn = 0;
+    public $totTodayOut = 0;
+    public $dayName;
+    public $in;
+    public $out;
+    public $members;
+    public $activeUsers = 0;
+    public $registeredUsers = 0;
+    public $expiredCertificates = 0;
+    public $suspendedSubscriptions = 0;
+    public $activeUsersChange = 0;
+    public $registeredUsersChange = 0;
+    public $expiredCertificatesChange = 0;
+    public $suspendedSubscriptionsChange = 0;
+
+    public $received = 0;
+    public $toPay = 0;
+    public $paid = 0;
+
+    public $courses = [];
+    public $fields = [];
+    public $recentUsers = [];
+    public $recentTransactions = [];
+    public $coursesParticipation = [];
+    public $notes = '';
+    public $savedNotes = [];
+    public array $membersDatas = [];
+    public array $recordDatas = [];
+    public array $labels = [];
+    public array $monthlyLabels = [];
+    public array $monthlyIncomeData = [];
+    public array $monthlyExpenseData = [];
+
+    public function render()
+    {
+        Log::info('Dashboard render method called');
+        return view('livewire.dashboard');
+    }
+
+    public function mount()
+    {
+        Log::info('Dashboard mount started');
+        $startTime = microtime(true);
+
+        $this->dayName = Carbon::now()->locale('it_IT')->dayName;
+        Log::info('Day name set', ['day' => $this->dayName]);
+
+        $this->loadBasicStats();
+        $this->loadUserStats();
+        $this->loadFinancialStats();
+        $this->loadRecentData();
+        $this->loadSavedNotes();
+
+        $endTime = microtime(true);
+        $executionTime = round(($endTime - $startTime) * 1000, 2);
+        Log::info('Dashboard mount completed', [
+            'execution_time_ms' => $executionTime,
+            'active_users' => $this->activeUsers,
+            'courses_count' => count($this->courses),
+            'participation_count' => count($this->coursesParticipation)
+        ]);
+    }
+
+    private function loadBasicStats()
+    {
+        Log::info('Loading basic stats');
+        $startTime = microtime(true);
+
+        try {
+            $this->totMembers = \App\Models\Member::count();
+            $this->totSuppliers = \App\Models\Supplier::count();
+            Log::info('Basic counts loaded', [
+                'total_members' => $this->totMembers,
+                'total_suppliers' => $this->totSuppliers
+            ]);
+
+            // Calculate today's income and expenses
+            $this->totTodayIn = 0;
+            $todayRecordsIn = \App\Models\Record::where('type', 'IN')
+                ->where('date', date("Y-m-d"))
+                ->get();
+
+            foreach ($todayRecordsIn as $record) {
+                foreach ($record->rows as $row) {
+                    $this->totTodayIn += $row->amount;
+                }
+            }
+
+            $this->totTodayOut = 0;
+            $todayRecordsOut = \App\Models\Record::where('type', 'OUT')
+                ->where('date', date("Y-m-d"))
+                ->get();
+
+            foreach ($todayRecordsOut as $record) {
+                foreach ($record->rows as $row) {
+                    $this->totTodayOut += $row->amount;
+                }
+            }
+
+            $endTime = microtime(true);
+            Log::info('Basic stats loaded successfully', [
+                'today_income' => $this->totTodayIn,
+                'today_expenses' => $this->totTodayOut,
+                'execution_time_ms' => round(($endTime - $startTime) * 1000, 2)
+            ]);
+        } catch (\Exception $e) {
+            Log::error('Error loading basic stats', [
+                'error' => $e->getMessage(),
+                'file' => $e->getFile(),
+                'line' => $e->getLine()
+            ]);
+        }
+    }
+
+    private function loadUserStats()
+    {
+        Log::info('Loading user stats');
+        $startTime = microtime(true);
+
+        try {
+            $this->activeUsers = \App\Models\Member::where('is_archived', 0)->orWhere('is_archived', NULL)->count();
+            $this->registeredUsers = \App\Models\Member::where('current_status', 2)->count();
+            $this->suspendedSubscriptions = \App\Models\Member::where('current_status', 1)->count();
+
+            Log::info('User counts loaded', [
+                'active_users' => $this->activeUsers,
+                'registered_users' => $this->registeredUsers,
+                'suspended_subscriptions' => $this->suspendedSubscriptions
+            ]);
+
+            $this->expiredCertificates = \App\Models\Member::whereHas('certificates', function ($query) {
+                $query->where('expire_date', '<', now());
+            })->whereDoesntHave('certificates', function ($query) {
+                $query->where('expire_date', '>=', now());
+            })->count();
+
+            Log::info('Expired certificates count', ['expired_certificates' => $this->expiredCertificates]);
+
+            // Calculate changes from last month
+            $lastMonth = now()->subMonth();
+            $endOfLastMonth = $lastMonth->copy()->endOfMonth();
+
+            $lastMonthActiveUsers = \App\Models\Member::where('is_archived', false)
+                ->where('created_at', '<=', $endOfLastMonth)
+                ->count();
+            $lastMonthRegisteredUsers = \App\Models\Member::where('current_status', 2)
+                ->where('updated_at', '<=', $endOfLastMonth)
+                ->count();
+            $lastMonthSuspendedSubscriptions = \App\Models\Member::where('current_status', 1)
+                ->where('updated_at', '<=', $endOfLastMonth)
+                ->count();
+            $lastMonthExpiredCertificates = \App\Models\Member::whereHas('certificates', function ($query) use ($endOfLastMonth) {
+                $query->where('expire_date', '<', $endOfLastMonth);
+            })->whereDoesntHave('certificates', function ($query) use ($endOfLastMonth) {
+                $query->where('expire_date', '>=', $endOfLastMonth);
+            })->count();
+
+            $this->activeUsersChange = $this->activeUsers - $lastMonthActiveUsers;
+            $this->registeredUsersChange = $this->registeredUsers - $lastMonthRegisteredUsers;
+            $this->expiredCertificatesChange = $this->expiredCertificates - $lastMonthExpiredCertificates;
+            $this->suspendedSubscriptionsChange = $this->suspendedSubscriptions - $lastMonthSuspendedSubscriptions;
+
+            $endTime = microtime(true);
+            Log::info('User stats loaded successfully', [
+                'changes' => [
+                    'active_users' => $this->activeUsersChange,
+                    'registered_users' => $this->registeredUsersChange,
+                    'expired_certificates' => $this->expiredCertificatesChange,
+                    'suspended_subscriptions' => $this->suspendedSubscriptionsChange
+                ],
+                'execution_time_ms' => round(($endTime - $startTime) * 1000, 2)
+            ]);
+        } catch (\Exception $e) {
+            Log::error('Error loading user stats', [
+                'error' => $e->getMessage(),
+                'file' => $e->getFile(),
+                'line' => $e->getLine()
+            ]);
+        }
+    }
+
+    private function loadFinancialStats()
+    {
+        Log::info('Loading financial stats');
+        $startTime = microtime(true);
+
+        try {
+            $currentMonth = now()->format('Y-m');
+            $currentDate = now()->format('Y-m-d');
+            Log::info('Calculating financial stats for month', ['month' => $currentMonth]);
+
+            $this->received = \App\Models\Record::where('type', 'IN')
+                ->whereRaw('DATE_FORMAT(date, "%Y-%m") = ?', [$currentMonth])
+                ->where(function ($query) {
+                    $query->where('deleted', false)->orWhere('deleted', null);
+                })
+                ->sum('amount') ?? 0;
+
+            $this->paid = \App\Models\Record::where('type', 'OUT')
+                ->whereRaw('DATE_FORMAT(date, "%Y-%m") = ?', [$currentMonth])
+                ->where('data_pagamento', '<=', $currentDate)
+                ->where(function ($query) {
+                    $query->where('deleted', false)->orWhere('deleted', null);
+                })
+                ->sum('amount') ?? 0;
+
+            $this->toPay = \App\Models\Record::where('type', 'OUT')
+                ->whereRaw('DATE_FORMAT(date, "%Y-%m") = ?', [$currentMonth])
+                ->where(function ($query) use($currentDate) {
+                    $query->where('data_pagamento', '>', $currentDate)
+                        ->orWhereNull('data_pagamento');
+                })
+                ->where(function ($query) {
+                    $query->where('deleted', false)->orWhere('deleted', null);
+                })
+                ->sum('amount') ?? 0;
+
+            $endTime = microtime(true);
+            Log::info('Financial stats loaded successfully', [
+                'received' => $this->received,
+                'paid' => $this->paid,
+                'to_pay' => $this->toPay,
+                'execution_time_ms' => round(($endTime - $startTime) * 1000, 2)
+            ]);
+        } catch (\Exception $e) {
+            Log::error('Error loading financial stats', [
+                'error' => $e->getMessage(),
+                'file' => $e->getFile(),
+                'line' => $e->getLine()
+            ]);
+        }
+    }
+
+    private function loadRecentData()
+    {
+        Log::info('Loading recent data');
+        $startTime = microtime(true);
+
+        try {
+            // Load recent users
+            $recentMembers = \App\Models\Member::where('is_archived', 0)
+                ->orWhere('is_archived', NULL)
+                ->orderBy('created_at', 'desc')
+                ->limit(5)
+                ->get();
+
+            $this->recentUsers = $recentMembers->map(function ($member) {
+                return [
+                    'surname' => strtoupper($member->last_name),
+                    'name' => strtoupper($member->first_name),
+                    'phone' => $member->phone ?? '',
+                    'email' => $member->email ?? ''
+                ];
+            })->toArray();
+
+            Log::info('Recent users loaded', ['count' => count($this->recentUsers)]);
+
+            // Load recent transactions
+            $recentRecords = \App\Models\Record::where('date', '>=', now()->subDays(30))
+                ->with(['member', 'supplier'])
+                ->orderBy('date', 'desc')
+                ->orderBy('created_at', 'desc')
+                ->limit(10)
+                ->get();
+
+            $this->recentTransactions = $recentRecords->map(function ($record) {
+                if ($record->type == 'IN') {
+                    $name = $record->member ?
+                        strtoupper($record->member->last_name) . ' ' . strtoupper($record->member->first_name) :
+                        'MEMBRO SCONOSCIUTO';
+                } else {
+                    $name = $record->supplier ?
+                        strtoupper($record->supplier->name) :
+                        'FORNITORE SCONOSCIUTO';
+                }
+
+                $totalAmount = 0;
+                foreach ($record->rows as $row) {
+                    $totalAmount += $row->amount;
+                }
+
+                return [
+                    'name' => $name,
+                    'amount' => $totalAmount,
+                    'type' => $record->type == 'IN' ? 'ENTRATA' : 'USCITA'
+                ];
+            })->toArray();
+
+            Log::info('Recent transactions loaded', ['count' => count($this->recentTransactions)]);
+
+            $this->loadCoursesData();
+            $this->loadCoursesParticipation();
+
+            $endTime = microtime(true);
+            Log::info('Recent data loaded successfully', [
+                'execution_time_ms' => round(($endTime - $startTime) * 1000, 2)
+            ]);
+        } catch (\Exception $e) {
+            Log::error('Error loading recent data', [
+                'error' => $e->getMessage(),
+                'file' => $e->getFile(),
+                'line' => $e->getLine()
+            ]);
+        }
+    }
+
+    private function loadCoursesData()
+    {
+        Log::info('Loading courses data');
+        $startTime = microtime(true);
+
+        try {
+            $today = now()->format('N');
+            $dayNames = [
+                1 => 'lun',
+                2 => 'mar',
+                3 => 'mer',
+                4 => 'gio',
+                5 => 'ven',
+                6 => 'sab',
+                7 => 'dom'
+            ];
+            $todayName = $dayNames[$today];
+
+            Log::info('Searching courses for today', [
+                'today_number' => $today,
+                'today_name' => $todayName
+            ]);
+
+            $memberCourses = \App\Models\MemberCourse::with(['course.level', 'course.frequency', 'member'])
+                ->whereIn('status', [0, 1])
+                ->whereHas('course', function ($query) {
+                    $query->whereNotNull('when');
+                })
+                ->get();
+
+            Log::info('Total member courses found', [
+                'count' => $memberCourses->count()
+            ]);
+
+            $activeCourses = $memberCourses->filter(function ($memberCourse) use ($todayName) {
+                try {
+                    $whenData = json_decode($memberCourse->course->when, true);
+
+                    if (!is_array($whenData)) {
+                        return false;
+                    }
+
+                    foreach ($whenData as $schedule) {
+                        if (
+                            isset($schedule['day']) &&
+                            is_array($schedule['day']) &&
+                            in_array($todayName, $schedule['day'])
+                        ) {
+                            return true;
+                        }
+                    }
+                } catch (\Exception $e) {
+                    Log::debug('Error parsing course schedule', [
+                        'member_course_id' => $memberCourse->id,
+                        'course_id' => $memberCourse->course->id,
+                        'when' => $memberCourse->course->when,
+                        'error' => $e->getMessage()
+                    ]);
+                }
+                return false;
+            });
+
+            Log::info('Active courses found for today', [
+                'count' => $activeCourses->count(),
+                'course_ids' => $activeCourses->pluck('course.id')->toArray()
+            ]);
+
+            $this->courses = $activeCourses->map(function ($memberCourse) use ($todayName) {
+                $whenData = json_decode($memberCourse->course->when, true);
+
+                Log::debug('Processing course schedule', [
+                    'member_course_id' => $memberCourse->id,
+                    'course_id' => $memberCourse->course->id,
+                    'course_name' => $memberCourse->course->name,
+                    'when_data' => $whenData,
+                    'looking_for_day' => $todayName
+                ]);
+
+                $todaySchedule = null;
+
+                if (is_array($whenData)) {
+                    foreach ($whenData as $schedule) {
+                        if (
+                            isset($schedule['day']) &&
+                            is_array($schedule['day']) &&
+                            in_array($todayName, $schedule['day'])
+                        ) {
+                            $todaySchedule = $schedule;
+                            Log::debug('Found matching schedule', [
+                                'schedule' => $schedule,
+                                'course_id' => $memberCourse->course->id
+                            ]);
+                            break;
+                        }
+                    }
+                }
+
+                if (!$todaySchedule) {
+                    Log::debug('No matching schedule found for today', [
+                        'course_id' => $memberCourse->course->id,
+                        'when_data' => $whenData
+                    ]);
+                    return null;
+                }
+
+                $days = implode('-', array_map('ucfirst', $todaySchedule['day']));
+                $course = $memberCourse->course;
+
+                $courseName = $course->name ?? 'Corso Sconosciuto';
+                $levelName = $course->level?->name ?? '';
+                $frequencyName = $course->frequency?->name ?? '';
+                $typeName = $course->getFormattedTypeField() ?? '';
+
+                $courseNameParts = [$courseName];
+                if ($levelName) $courseNameParts[] = $levelName;
+                if ($typeName) $courseNameParts[] = $typeName;
+                if ($frequencyName) $courseNameParts[] = $frequencyName;
+
+                $fullCourseName = implode(' - ', $courseNameParts);
+
+                return [
+                    'time' => $todaySchedule['from'] . ' - ' . $todaySchedule['to'],
+                    'course_name' => $courseName,
+                    'full_name' => $fullCourseName,
+                    'level_name' => $levelName,
+                    'type_name' => $typeName,
+                    'frequency_name' => $frequencyName,
+                    'days' => $days,
+                    'type' => $course->type ?? 'Standard',
+                    'from_time' => $todaySchedule['from'],
+                    'course_id' => $course->id,
+                    'member_course_id' => $memberCourse->id
+                ];
+            })->filter()->values();
+
+            $sortedCourses = $this->courses->sortBy('from_time')->take(5);
+
+            Log::info('Courses sorted by time', [
+                'sorted_courses' => $sortedCourses->map(function ($course) {
+                    return [
+                        'time' => $course['time'],
+                        'from_time' => $course['from_time'],
+                        'course_name' => $course['course_name'],
+                        'course_id' => $course['course_id']
+                    ];
+                })->toArray()
+            ]);
+
+            $this->courses = $sortedCourses->map(function ($course) {
+                unset($course['from_time'], $course['member_course_id'], $course['course_id']);
+                return $course;
+            })->toArray();
+
+            $endTime = microtime(true);
+            Log::info('Courses data loaded successfully', [
+                'final_courses_count' => count($this->courses),
+                'execution_time_ms' => round(($endTime - $startTime) * 1000, 2),
+                'final_courses_display' => $this->courses
+            ]);
+        } catch (\Exception $e) {
+            Log::error('Error loading courses data', [
+                'error' => $e->getMessage(),
+                'file' => $e->getFile(),
+                'line' => $e->getLine()
+            ]);
+            $this->courses = [];
+        }
+    }
+
+    private function loadCoursesParticipation()
+    {
+        Log::info('Loading courses participation');
+        $startTime = microtime(true);
+
+        try {
+            // Conta le partecipazioni per corso (include tutti gli status)
+            $courseStats = \App\Models\MemberCourse::with(['course.level', 'course.frequency'])
+                ->whereIn('status', [0, 1]) // Include both statuses
+                ->selectRaw('course_id, COUNT(*) as participants')
+                ->groupBy('course_id')
+                ->orderBy('participants', 'desc')
+                ->limit(4)
+                ->get();
+
+            Log::info('Course participation stats', [
+                'courses_found' => $courseStats->count(),
+                'stats' => $courseStats->map(function ($stat) {
+                    $course = $stat->course;
+                    $levelName = is_object($course->level) ? $course->level->name : '';
+                    $frequencyName = is_object($course->frequency) ? $course->frequency->name : '';
+
+                    return [
+                        'course_id' => $stat->course_id,
+                        'course_name' => $course->name ?? 'Unknown',
+                        'level_name' => $levelName,
+                        'frequency_name' => $frequencyName,
+                        'participants' => $stat->participants
+                    ];
+                })->toArray()
+            ]);
+
+            $totalParticipants = $courseStats->sum('participants');
+
+            $this->coursesParticipation = $courseStats->map(function ($stat) use ($totalParticipants) {
+                $percentage = $totalParticipants > 0 ? ($stat->participants / $totalParticipants) * 100 : 0;
+                $course = $stat->course;
+                $courseName = $course->name ?? 'Corso Sconosciuto';
+                $levelName = is_object($course->level) ? $course->level->name : '';
+                $frequencyName = is_object($course->frequency) ? $course->frequency->name : '';
+                $typeName = $course->getFormattedTypeField() ?? '';
+
+                // Build display name with level and frequency
+                $displayNameParts = [$courseName];
+                if ($levelName) $displayNameParts[] = $levelName;
+                if ($typeName) $displayNameParts[] = $typeName;
+                if ($frequencyName) $displayNameParts[] = $frequencyName;
+
+                $displayName = implode(' - ', $displayNameParts);
+
+                // Assegna colori basati sul nome del corso
+                $color = $this->getCourseColor($courseName);
+
+                return [
+                    'course_name' => $displayName,
+                    'base_course_name' => $courseName,
+                    'level_name' => $levelName,
+                    'type_name' => $typeName,
+                    'frequency_name' => $frequencyName,
+                    'participants' => $stat->participants,
+                    'percentage' => round($percentage, 1),
+                    'color' => $color
+                ];
+            })->toArray();
+
+            $endTime = microtime(true);
+            Log::info('Courses participation loaded successfully', [
+                'total_participants' => $totalParticipants,
+                'participation_data' => $this->coursesParticipation,
+                'execution_time_ms' => round(($endTime - $startTime) * 1000, 2)
+            ]);
+        } catch (\Exception $e) {
+            Log::error('Error loading courses participation', [
+                'error' => $e->getMessage(),
+                'file' => $e->getFile(),
+                'line' => $e->getLine()
+            ]);
+            $this->coursesParticipation = [];
+        }
+    }
+
+    private function getCourseColor($courseName)
+    {
+        $colors = [
+            'padel',
+            'tennis',
+            'pallavolo',
+            'yoga',
+            'blue',
+            'pink',
+            'green',
+            'red',
+            'indigo',
+            'amber',
+            'cyan',
+            'lime'
+        ];
+        $hash = crc32($courseName);
+        $colorIndex = abs($hash) % count($colors);
+        $assignedColor = $colors[$colorIndex];
+
+        Log::debug('Course color assigned', [
+            'course_name' => $courseName,
+            'hash' => $hash,
+            'color_index' => $colorIndex,
+            'assigned_color' => $assignedColor
+        ]);
+
+        return $assignedColor;
+    }
+
+    private function loadSavedNotes()
+    {
+        Log::info('Loading saved notes');
+
+        try {
+            $this->savedNotes = session()->get('dashboard_notes', []);
+
+            Log::info('Saved notes loaded', [
+                'notes_count' => count($this->savedNotes)
+            ]);
+        } catch (\Exception $e) {
+            Log::error('Error loading saved notes', [
+                'error' => $e->getMessage()
+            ]);
+            $this->savedNotes = [];
+        }
+    }
+
+    private function saveSavedNotes()
+    {
+        try {
+            session()->put('dashboard_notes', $this->savedNotes);
+
+            Log::info('Notes saved to session', [
+                'notes_count' => count($this->savedNotes)
+            ]);
+        } catch (\Exception $e) {
+            Log::error('Error saving notes', [
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+
+    public function saveNote()
+    {
+        Log::info('Save note called', ['note_text' => $this->notes]);
+
+        try {
+            if (trim($this->notes) !== '') {
+                $newNote = [
+                    'id' => uniqid(),
+                    'text' => trim($this->notes),
+                    'created_at' => now()->timezone('Europe/Rome')->format('d/m/Y H:i'),
+                    'completed' => false
+                ];
+
+                array_unshift($this->savedNotes, $newNote);
+
+                $this->saveSavedNotes();
+
+                $this->notes = '';
+
+                $this->dispatchBrowserEvent('note-saved');
+
+                Log::info('Note saved successfully', [
+                    'note_id' => $newNote['id'],
+                    'total_notes' => count($this->savedNotes)
+                ]);
+            } else {
+                Log::warning('Attempted to save empty note');
+            }
+        } catch (\Exception $e) {
+            Log::error('Error saving note', [
+                'error' => $e->getMessage(),
+                'file' => $e->getFile(),
+                'line' => $e->getLine()
+            ]);
+        }
+    }
+
+    public function completeNote($noteId)
+    {
+        Log::info('Complete note called', ['note_id' => $noteId]);
+
+        try {
+            $initialCount = count($this->savedNotes);
+
+            $this->savedNotes = array_filter($this->savedNotes, function ($note) use ($noteId) {
+                return $note['id'] !== $noteId;
+            });
+
+            $this->savedNotes = array_values($this->savedNotes);
+
+            $finalCount = count($this->savedNotes);
+
+            if ($initialCount > $finalCount) {
+                $this->saveSavedNotes();
+
+                $this->dispatchBrowserEvent('note-completed');
+
+                Log::info('Note completed successfully', [
+                    'note_id' => $noteId,
+                    'remaining_notes' => $finalCount
+                ]);
+            } else {
+                Log::warning('Note not found for completion', ['note_id' => $noteId]);
+            }
+        } catch (\Exception $e) {
+            Log::error('Error completing note', [
+                'note_id' => $noteId,
+                'error' => $e->getMessage(),
+                'file' => $e->getFile(),
+                'line' => $e->getLine()
+            ]);
+        }
+    }
+
+    public function addMember()
+    {
+        Log::info('Redirecting to add member');
+        return redirect()->to('/members?new=1');
+    }
+
+    public function addSupplier()
+    {
+        Log::info('Redirecting to add supplier');
+        return redirect()->to('/suppliers?new=1');
+    }
+
+    public function addIn()
+    {
+        Log::info('Redirecting to add income record');
+        return redirect()->to('/in?new=1');
+    }
+
+    public function addOut()
+    {
+        Log::info('Redirecting to add expense record');
+        return redirect()->to('/out?new=1');
+    }
+
+    public function debugCourses()
+    {
+        Log::info('=== DEBUG COURSES CALLED ===');
+
+        $today = now()->format('N');
+        $dayNames = [
+            1 => 'lun',
+            2 => 'mar',
+            3 => 'mer',
+            4 => 'gio',
+            5 => 'ven',
+            6 => 'sab',
+            7 => 'dom'
+        ];
+        $todayName = $dayNames[$today];
+
+        // Get all member courses
+        $allCourses = \App\Models\MemberCourse::with('course')->get();
+
+        Log::info('All courses debug', [
+            'total_courses' => $allCourses->count(),
+            'today_name' => $todayName,
+            'courses' => $allCourses->map(function ($mc) {
+                return [
+                    'id' => $mc->id,
+                    'status' => $mc->status,
+                    'when' => $mc->when,
+                    'course_name' => $mc->course->name ?? 'Unknown'
+                ];
+            })->toArray()
+        ]);
+
+        $this->dispatchBrowserEvent('debug-completed');
+    }
+
+    public function activateTestCourses()
+    {
+        try {
+            $updated = \App\Models\MemberCourse::whereIn('id', [21, 22, 23, 24])
+                ->update(['status' => 1]);
+
+            Log::info('Activated test courses', ['updated_count' => $updated]);
+
+            $this->loadCoursesData();
+            $this->loadCoursesParticipation();
+
+            $this->dispatchBrowserEvent('courses-activated', [
+                'message' => "Attivati $updated corsi per test"
+            ]);
+        } catch (\Exception $e) {
+            Log::error('Error activating courses', ['error' => $e->getMessage()]);
+        }
+    }
+
+    private function getLabels()
+    {
+        $labels = array();
+        for ($i = 6; $i >= 0; $i--) {
+            $labels[] = date("d/M", strtotime('-' . $i . ' days'));
+        }
+        return $labels;
+    }
+
+    private function getRecordData($type)
+    {
+        $data = [];
+        for ($i = 6; $i >= 0; $i--) {
+            $found = false;
+            $records = $type == 'IN' ? $this->in : $this->out;
+
+            foreach ($records as $record) {
+                if (date("Y-m-d", strtotime($record->date)) == date("Y-m-d", strtotime('-' . $i . ' days'))) {
+                    $data[] = $record->total;
+                    $found = true;
+                    break;
+                }
+            }
+            if (!$found) {
+                $data[] = 0;
+            }
+        }
+        return $data;
+    }
+
+    private function getMemberData()
+    {
+        $data = [];
+        for ($i = 6; $i >= 0; $i--) {
+            $found = false;
+            foreach ($this->members as $member) {
+                if (date("Y-m-d", strtotime($member->created_at)) == date("Y-m-d", strtotime('-' . $i . ' days'))) {
+                    $data[] = $member->total;
+                    $found = true;
+                    break;
+                }
+            }
+            if (!$found) {
+                $data[] = 0;
+            }
+        }
+        return $data;
+    }
+}

+ 131 - 0
app/Http/Livewire/Discipline.php

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

+ 429 - 0
app/Http/Livewire/EmailComunications.php

@@ -0,0 +1,429 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+use Livewire\WithFileUploads;
+use Illuminate\Support\Facades\DB;
+use Carbon\Carbon;
+use Illuminate\Support\Facades\Storage;
+
+use App\Http\Middleware\TenantMiddleware;
+use App\Models\EmailMessage;
+use App\Models\Member;
+
+class EmailComunications extends Component
+{
+    use WithFileUploads;
+
+    public ?int $messageId = null;
+    public string $subject = '';
+    public string $content_html = '';
+
+    public array $recipients = [];
+
+    public array $existingAttachments = [];
+    public $newAttachments = [];
+
+    public string $mode = 'now'; // 'now' | 'schedule'
+    public ?string $schedule_at = null;
+
+    public $records;
+    public $categories;
+    public $courses;
+
+    public bool $showForm = false;
+    public bool $locked = false;
+
+    public $success;
+    public $error;
+
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+    }
+
+    public function mount()
+    {
+        if (auth()->user()?->level != env('LEVEL_ADMIN', 0)) {
+            return redirect()->to('/dashboard');
+        }
+
+        $this->categories = [];
+        $this->getCategories(\App\Models\Category::select('id', 'name')->where('parent_id', null)->orderBy('name')->get(), 0);
+
+        $this->courses = [];
+        $this->getCourses(\App\Models\Course::select('id', 'name')->where('parent_id', null)->orderBy('name', 'ASC')->get(), 0);
+
+        $this->schedule_at = now()->addHour()->format('Y-m-d\TH:i');
+    }
+
+    public function render()
+    {
+        if (!$this->showForm) {
+            $this->records = EmailMessage::withCount(['attachments', 'recipients'])
+                ->orderBy('created_at', 'desc')
+                ->get();
+        } else {
+            $this->categories = [];
+            $this->getCategories(\App\Models\Category::select('id', 'name')->where('parent_id', null)->orderBy('name')->get(), 0);
+
+            $this->courses = [];
+            $this->getCourses(\App\Models\Course::select('id', 'name')->where('parent_id', null)->orderBy('name', 'ASC')->get(), 0);
+        }
+
+        return view('livewire.email_comunications');
+    }
+
+    protected function baseRules(): array
+    {
+        return [
+            'subject' => 'required|string|max:255',
+            'content_html' => 'required|string',
+            'recipients' => 'required|array|min:1',
+            'recipients.*.email_address' => 'required|email',
+            'newAttachments' => 'nullable',
+            'newAttachments.*' => 'file|max:20480',
+        ];
+    }
+
+    protected function validateDraft(): void
+    {
+        $this->validate($this->baseRules());
+    }
+
+    protected function validateSend(): void
+    {
+        $this->validate($this->baseRules());
+    }
+
+    protected function validateSchedule(): void
+    {
+        $rules = $this->baseRules();
+        $rules['schedule_at'] = 'required|date|after:now';
+        $this->validate($rules);
+    }
+
+    public function add()
+    {
+        $this->reset(['messageId', 'subject', 'content_html', 'recipients', 'newAttachments', 'mode', 'schedule_at']);
+        $this->mode = 'now';
+        $this->schedule_at = now()->addHour()->format('Y-m-d\TH:i');
+        $this->existingAttachments = [];
+
+        $this->showForm = true;
+
+        $this->dispatchBrowserEvent('load-editor', [
+            'html'   => $this->content_html ?? '',
+            'locked' => $this->locked,
+        ]);
+        $this->dispatchBrowserEvent('init-recipients-table', [
+            'selected' => collect($this->recipients)->pluck('member_id')->filter()->values()->all(),
+        ]);
+    }
+
+    public function edit($id)
+    {
+        try {
+            $msg = EmailMessage::with(['recipients', 'attachments'])->findOrFail($id);
+
+            $this->messageId = $msg->id;
+            $this->subject = $msg->subject;
+            $this->content_html = $msg->content_html;
+            $this->recipients = $msg->recipients->map(fn($r) => [
+                'member_id' => $r->member_id,
+                'email_address' => $r->email_address,
+                'first_name'    => optional($r->member)->first_name,
+                'last_name'     => optional($r->member)->last_name,
+            ])->toArray();
+            $this->mode = $msg->status === 'scheduled' ? 'schedule' : 'now';
+            $this->schedule_at = optional($msg->schedule_at)?->format('Y-m-d\TH:i');
+            $this->existingAttachments = $msg->attachments->map(fn($a) => [
+                'id'   => $a->id,
+                'name' => $a->name ?: basename($a->path),
+                'size' => $a->size_human,
+                'url'  => $a->public_url,
+                'img'  => $a->is_image,
+            ])->toArray();
+
+            $this->showForm = true;
+            $this->locked = $msg->isLocked();
+
+            $this->dispatchBrowserEvent('load-editor', [
+                'html' => $this->content_html ?? '',
+                'locked' => $this->locked,
+            ]);
+            $this->dispatchBrowserEvent('init-recipients-table', [
+                'selected' => collect($this->recipients)->pluck('member_id')->filter()->values()->all(),
+            ]);
+        } catch (\Throwable $ex) {
+            $this->error = 'Errore (' . $ex->getMessage() . ')';
+        }
+    }
+
+
+    public function duplicate($id, $withRecipients = true)
+    {
+        try {
+            $copy = EmailMessage::with(['recipients', 'attachments'])->findOrFail($id)->duplicate($withRecipients);
+            $this->edit($copy->id);
+            $this->success = 'Bozza duplicata';
+        } catch (\Throwable $ex) {
+            $this->error = 'Errore (' . $ex->getMessage() . ')';
+        }
+    }
+
+    public function saveDraft($html = null)
+    {
+        if ($html !== null) $this->content_html = $html;
+        $this->validateDraft();
+
+        DB::transaction(function () {
+            $msg = $this->upsertMessage(status: 'draft', scheduleAt: null);
+            $this->upsertRecipients($msg);
+            $this->upsertAttachments($msg);
+            $this->messageId = $msg->id;
+            $this->locked = $msg->isLocked();
+            $this->refreshAttachments($msg);
+        });
+
+        $this->success = 'Bozza salvata';
+
+        $this->dispatchBrowserEvent('load-editor', [
+            'html'   => $this->content_html ?? '',
+            'locked' => $this->locked,
+        ]);
+    }
+
+    public function sendNow($html = null)
+    {
+        if ($html !== null) $this->content_html = $html;
+        $this->validateSend();
+
+        if ($this->messageId) {
+            $existing = EmailMessage::findOrFail($this->messageId);
+            if ($existing->isLocked()) {
+                $this->error = 'Questa email è già in invio o inviata e non può essere modificata.';
+                return;
+            }
+        }
+
+        DB::transaction(function () {
+            $msg = $this->upsertMessage(status: 'processing', scheduleAt: null);
+            $this->upsertRecipients($msg, true);
+            $this->upsertAttachments($msg, true);
+            $this->messageId = $msg->id;
+            $this->locked = true;
+            $this->refreshAttachments($msg);
+        });
+
+        dispatch(new \App\Jobs\SendEmailMessage($this->messageId));
+        $this->success = 'Invio avviato';
+
+        $this->dispatchBrowserEvent('load-editor', [
+            'html'   => $this->content_html ?? '',
+            'locked' => $this->locked,
+        ]);
+    }
+
+    public function scheduleMessage($html = null)
+    {
+        if ($html !== null) $this->content_html = $html;
+        $this->validateSchedule();
+
+        if ($this->messageId) {
+            $existing = EmailMessage::findOrFail($this->messageId);
+            if ($existing->isLocked()) {
+                $this->error = 'Questa email è già in invio o inviata e non può essere modificata.';
+                return;
+            }
+        }
+
+        DB::transaction(function () {
+            $msg = $this->upsertMessage(status: 'scheduled', scheduleAt: \Carbon\Carbon::parse($this->schedule_at));
+            $this->upsertRecipients($msg, true);
+            $this->upsertAttachments($msg, true);
+            $this->messageId = $msg->id;
+            $this->locked = $msg->isLocked();
+            $this->refreshAttachments($msg);
+        });
+
+        $this->success = 'Email programmata';
+
+        $this->dispatchBrowserEvent('load-editor', [
+            'html'   => $this->content_html ?? '',
+            'locked' => $this->locked,
+        ]);
+    }
+
+    protected function upsertMessage(string $status, $scheduleAt): EmailMessage
+    {
+        return EmailMessage::updateOrCreate(
+            ['id' => $this->messageId],
+            [
+                'subject'      => $this->subject,
+                'content_html' => $this->content_html,
+                'status'       => $status,
+                'schedule_at'  => $scheduleAt,
+                'created_by'   => auth()->id(),
+            ]
+        );
+    }
+
+    protected function upsertRecipients(EmailMessage $msg, bool $force = false): void
+    {
+        if (!$force && $msg->isLocked()) return;
+
+        $msg->recipients()->delete();
+
+        $rows = collect($this->recipients)->map(fn($r) => [
+            'email_message_id' => $msg->id,
+            'member_id' => $r['member_id'] ?? null,
+            'email_address' => $r['email_address'],
+            'status' => 'pending',
+            'created_at' => now(),
+            'updated_at' => now(),
+        ])->values()->all();
+
+        if ($rows) \App\Models\EmailMessageRecipient::insert($rows);
+    }
+
+    protected function upsertAttachments(EmailMessage $msg, bool $force = false): void
+    {
+        if (!$force && $msg->isLocked()) return;
+
+        $files = is_array($this->newAttachments) ? $this->newAttachments : [$this->newAttachments];
+        foreach ($files as $upload) {
+            if (!$upload) continue;
+            $path = $upload->store('emails/' . \Illuminate\Support\Str::uuid(), 'public');
+            $msg->attachments()->create([
+                'disk' => 'public',
+                'path' => $path,
+                'name' => $upload->getClientOriginalName(),
+                'size' => $upload->getSize(),
+            ]);
+        }
+    }
+
+    public function cancel()
+    {
+        $this->showForm = false;
+        $this->reset(['messageId', 'subject', 'content_html', 'recipients', 'newAttachments', 'mode', 'schedule_at']);
+        $this->mode = 'now';
+        $this->schedule_at = now()->addHour()->format('Y-m-d\TH:i');
+
+        $this->dispatchBrowserEvent('init-archive-table');
+    }
+
+    public function getCategories($records, $indentation)
+    {
+        foreach ($records as $record) {
+            $this->categories[] = array('id' => $record->id, 'name' => $record->getTree());
+            if (count($record->childs))
+                $this->getCategories($record->childs, $indentation + 1);
+        }
+    }
+
+    public function getCourses($records, $indentation)
+    {
+        /** @var \App\Models\Course $record */
+        foreach ($records as $record) {
+            $this->courses[] = array('id' => $record->id, 'name' => $record->getTree());
+            if (count($record->childs))
+                $this->getCourses($record->childs, $indentation + 1);
+        }
+    }
+
+    public function toggleRecipient($id)
+    {
+        $id = (int)$id;
+
+        $idx = collect($this->recipients)->search(fn($r) => (int)($r['member_id'] ?? 0) === $id);
+        if ($idx !== false) {
+            array_splice($this->recipients, $idx, 1);
+            return;
+        }
+
+        $m = Member::select('id', 'email', 'first_name', 'last_name')->find($id);
+        if (!$m || empty($m->email)) return;
+
+        $this->recipients[] = [
+            'member_id' => $m->id,
+            'email_address' => $m->email,
+            'first_name' => $m->first_name,
+            'last_name' => $m->last_name,
+        ];
+    }
+
+    public function removeNewAttachment(int $index): void
+    {
+        if ($this->locked) return;
+        if (is_array($this->newAttachments) && array_key_exists($index, $this->newAttachments)) {
+            array_splice($this->newAttachments, $index, 1);
+        }
+    }
+
+    public function removeExistingAttachment(int $id): void
+    {
+        if ($this->locked || !$this->messageId) return;
+
+        $att = \App\Models\EmailMessageAttachment::find($id);
+        if (!$att || $att->email_message_id !== $this->messageId) return;
+
+        try {
+            if ($att->disk && $att->path) {
+                $disk = Storage::disk($att->disk);
+                $dir = dirname($att->path);
+                $disk->delete($att->path);
+
+                if (empty($disk->files($dir)) && empty($disk->directories($dir))) {
+                    $disk->deleteDirectory($dir);
+                }
+            }
+        } catch (\Throwable $e) {
+        }
+
+        $att->delete();
+
+        $this->existingAttachments = array_values(array_filter(
+            $this->existingAttachments,
+            fn($a) => (int)$a['id'] !== (int)$id
+        ));
+    }
+
+
+    protected function refreshAttachments(EmailMessage $msg): void
+    {
+        $this->existingAttachments = $msg->attachments()
+            ->get()
+            ->map(fn($a) => [
+                'id'   => $a->id,
+                'name' => $a->name ?: basename($a->path),
+                'size' => $a->size_human,
+                'url'  => $a->public_url,
+                'img'  => $a->is_image,
+            ])->toArray();
+
+        $this->newAttachments = [];
+    }
+
+    public function deleteMessage(int $id)
+    {
+        $msg = \App\Models\EmailMessage::with(['attachments'])->findOrFail($id);
+
+        if (! in_array($msg->status, ['draft', 'failed'], true)) {
+            return;
+        }
+
+        foreach ($msg->attachments as $a) {
+            try {
+                Storage::disk($a->disk ?? 'public')->delete($a->path);
+            } catch (\Throwable $e) {
+            }
+        }
+
+        $msg->delete();
+
+        $this->dispatchBrowserEvent('email-deleted', ['id' => $id]);
+    }
+}

+ 189 - 0
app/Http/Livewire/FirstLogin.php

@@ -0,0 +1,189 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+use Livewire\Component;
+use App\Http\Middleware\TenantMiddleware;
+use Illuminate\Support\Facades\Mail;
+
+class FirstLogin extends Component
+{
+    // Component properties
+    public $name;
+    public $cognome;
+    public $email;
+    public $telefono;
+    public $cellulare;
+    public $password;
+    public $password_confirmation;
+    public $isLoading = false; // Make sure this is defined
+
+    protected $rules = [
+        'password' => 'required|min:6|confirmed',
+        'password_confirmation' => 'required|min:6',
+    ];
+
+    protected $messages = [
+        'password.required' => 'La password è obbligatoria',
+        'password.min' => 'La password deve essere di almeno 6 caratteri',
+        'password.confirmed' => 'Le password non coincidono',
+        'password_confirmation.required' => 'Conferma la password',
+    ];
+
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+    }
+
+    public function mount()
+    {
+        $currentUser = Auth::user();
+
+        if ($currentUser->first_login_completed) {
+            return redirect('/dashboard');
+        }
+
+        $user = \App\Models\User::findOrFail($currentUser->id);
+
+        $this->name = $user->name ?? '';
+        $this->cognome = $user->cognome ?? '';
+        $this->email = $user->email ?? '';
+        $this->telefono = $user->telefono ?? '';
+        $this->cellulare = $user->cellulare ?? '';
+    }
+
+    public function completeProfile()
+    {
+        $this->isLoading = true;
+
+        $this->validate();
+
+        $currentUser = Auth::user();
+
+        try {
+            DB::beginTransaction();
+
+            $user = \App\Models\User::findOrFail($currentUser->id);
+            $oldEmail = $user->email;
+
+            $user->password = Hash::make($this->password);
+            $user->first_login_completed = true;
+            $user->email_verified_at = now();
+            $user->save();
+
+            $this->updateMasterDatabase($currentUser, $oldEmail);
+
+            DB::commit();
+
+            $this->sendAccountActivationEmail($user);
+
+            session()->flash('success', 'Profilo completato con successo! Benvenuto in Leezard.cloud.');
+
+            $this->isLoading = false;
+
+            return redirect('/dashboard');
+
+        } catch (\Exception $e) {
+            DB::rollback();
+
+            Log::error('First login profile completion failed', [
+                'user_id' => $currentUser->id,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+
+            $this->isLoading = false;
+            session()->flash('error', 'Errore durante il completamento del profilo: ' . $e->getMessage());
+        }
+    }
+
+    private function updateMasterDatabase($currentUser, $oldEmail)
+    {
+        try {
+            $masterConfig = [
+                'driver' => 'mysql',
+                'host' => env('DB_HOST', '127.0.0.1'),
+                'port' => env('DB_PORT', '3306'),
+                'database' => env('DB_DATABASE'),
+                'username' => env('DB_USERNAME'),
+                'password' => env('DB_PASSWORD'),
+                'charset' => 'utf8mb4',
+                'collation' => 'utf8mb4_unicode_ci',
+                'prefix' => '',
+                'strict' => true,
+                'engine' => null,
+            ];
+
+            config(['database.connections.master_temp' => $masterConfig]);
+
+            $updateData = [
+                'password' => Hash::make($this->password),
+                'first_login_completed' => true,
+                'email_verified_at' => now(),
+                'updated_at' => now()
+            ];
+
+            $updated = DB::connection('master_temp')
+                ->table('users')
+                ->where('email', $oldEmail)
+                ->update($updateData);
+
+            if ($updated) {
+                Log::info('Successfully updated user in master database during first login', [
+                    'old_email' => $oldEmail,
+                    'new_email' => $this->email
+                ]);
+            }
+
+            DB::purge('master_temp');
+        } catch (\Exception $e) {
+            Log::error('Failed to update master database during first login', [
+                'error' => $e->getMessage(),
+                'user_id' => $currentUser->id,
+                'old_email' => $oldEmail,
+                'new_email' => $this->email
+            ]);
+            throw $e;
+        }
+    }
+
+    private function sendAccountActivationEmail($user)
+    {
+        try {
+            $emailData = [
+                'name' => $user->name,
+                'email' => $user->email,
+                'login_url' => url('/'),
+                'activation_time' => now()->format('d/m/Y H:i'),
+                'platform_name' => 'Leezard.cloud'
+            ];
+
+            Mail::send('emails.account-activated', $emailData, function ($message) use ($user) {
+                $message->to($user->email, $user->name)
+                        ->subject('Il tuo account è attivo – Benvenuto in Leezard.cloud')
+                        ->from(config('mail.from.address'), config('mail.from.name'));
+            });
+
+            Log::info('Account activation email sent after first login completion', [
+                'user_id' => $user->id,
+                'email' => $user->email
+            ]);
+
+        } catch (\Exception $e) {
+            Log::error('Failed to send account activation email', [
+                'user_id' => $user->id,
+                'email' => $user->email,
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+
+    public function render()
+    {
+        return view('livewire.first-login');
+    }
+}

+ 2889 - 0
app/Http/Livewire/Member.php

@@ -0,0 +1,2889 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+use Livewire\Attributes\Url;
+use Livewire\WithFileUploads;
+use Livewire\WithPagination;
+use Illuminate\Support\Facades\Log;
+use App\Http\Middleware\TenantMiddleware;
+
+use DateTime;
+
+class Member extends Component
+{
+
+    use WithPagination;
+    protected $paginationTheme = 'bootstrap';
+
+    protected $listeners = ['storeCategoryWithID' => 'storeCategoryWithID',  'setCourse' => 'setCourse', 'hideMsg' => 'hideMsg'];
+
+    use WithFileUploads;
+
+    public $sortField = 'id';
+    public $sortAsc = false;
+
+    public $groupMsg = '';
+
+    public $type = 'dati';
+
+    public $from = '';
+
+    public $selectedCourseMember = 0;
+
+    protected $messages = [
+        'birth_nation_id.required' => 'birth_nation_id',
+        'birth_province_id.required' => 'birth_province_id',
+        'birth_city_id.required' => 'birth_city_id',
+        'nation_id.required' => 'nation_id',
+        'province_id.required' => 'province_id',
+        'city_id.required' => 'city_id',
+        'gender' => 'gender'
+    ];
+
+    public function change($type)
+    {
+        $this->type = $type;
+            if ($type === 'corsi' && $this->dataId > 0) {
+            $this->loadMemberCards();
+            $this->loadMemberCertificates();
+            // TODO $this->checkCourseAvailability();
+        }
+
+        $this->dispatchBrowserEvent('scroll-to-top');
+    }
+
+    public function sortBy($field)
+    {
+        if ($this->sortField === $field) {
+            $this->sortAsc = ! $this->sortAsc;
+        } else {
+            $this->sortAsc = true;
+        }
+
+        $this->sortField = $field;
+    }
+
+    public $records, $first_name, $last_name, $image, $image_old, $status, $birth_place, $birth_city_id, $birth_province_id, $birth_nation_id, $birth_date, $gender, $no_send_mail, $exclude_from_records, $fiscal_code, $address, $zip_code, $nation_id, $province_id, $city_id, $phone, $phone2, $phone3, $email, $enabled, $dataId, $update = false, $add = false;
+
+    public $isSaving = false;
+
+    public $importoBorsellino, $father_name, $mother_name, $father_email, $mother_email, $father_phone, $mother_phone, $father_fiscal_code, $mother_fiscal_code, $father_doc_number, $father_doc_type, $mother_doc_number, $mother_doc_type, $mother_document_files, $father_document_files;
+
+    public $document_type, $document_number, $document_from, $document_expire_date, $document_files, $document_files_old;
+
+    public $selectId = 0;
+
+    public $causalId = 0;
+
+    public $error_fc = false;
+
+    public $under18 = false;
+
+    public $money = 0;
+
+    public $refreshAfter = 0;
+
+    public $checkedAll = false;
+    public $multipleIds = [];
+    public $multipleAction = '';
+
+    public $isItaly = true;
+    public $isBirthItaly = true;
+
+    public $showDetail = false;
+
+    public $currentMember;
+    public $currentStatus;
+
+    public $age = '';
+
+    public $disciplines = array();
+    public $cards = array();
+    public $categories = array();
+    public $courses = array();
+
+    public $course_names = array();
+    public $course_levels = array();
+    public $course_types = array();
+    public $course_frequencies = array();
+
+    public $course_subscriptions = array();
+
+    public $createSubscription = false;
+    public $payMonths = array();
+    public $suspendedMonths = array();
+    public $showCourse = array();
+
+    public $searchTxt;
+    public $search;
+    public $showReset = false;
+
+    public $advanced = false;
+
+    public $birthNations = array();
+    public $birthProvinces = array();
+    public $birthCities = array();
+
+    public $active;
+
+    // Card data
+    public $member_cards = array(), $card_card_id, $card_number, $card_date, $card_accept_date, $card_status, $card_discipline1_id, $card_discipline2_id, $card_discipline3_id, $addCard, $updateCard, $cardDataId;
+
+    // Categories data
+    public $member_categories = array(), $category_category_id;
+
+    // Courses data
+    public $member_courses = array(), $course_course_id, $course_course_type, $course_date_from, $course_date_to, $course_when = array(), $addCourse, $updateCourse, $courseDataId, $course_course_subscription_id, $course_status, $course_note, $course_price, $course_subscription_price, $course_subscribed, $course_months = array(), $course_exist;
+
+    public $course_months_list = [];
+    public $course_price_list = [];
+
+    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, $addCertificate, $updateCertificate, $certificateDataId, $cardCertificateId;
+
+    public $filterCard = [];
+    public $filterCategory = [];
+    public $filterCertNormal = 0;
+    public $filterCertAgonistic = 0;
+    public $filterCertScaduto = 0;
+    public $filterCertInScadenza = 0;
+    public $already_existing = false;
+    private $fileService;
+
+    public $hasCertificate = false;
+
+    protected $rules = [
+        'first_name' => 'required',
+        'last_name' => 'required',
+        // 'email' => 'required',
+        // 'phone' => 'required',
+        'birth_date' => 'before_or_equal:today'
+    ];
+
+    public function resetFields()
+    {
+        $this->dataId = -1;
+        $this->first_name = '';
+        $this->last_name = '';
+        $this->status = '';
+        $this->birth_city_id = null;
+
+        $this->birth_province_id = null;
+        $this->birth_nation_id = null;
+
+        $this->birth_date = null;
+        $this->birth_place = '';
+        $this->importoBorsellino = 0;
+        $this->father_name = '';
+        $this->mother_name = '';
+        $this->father_email = '';
+        $this->mother_email = '';
+        $this->father_phone = '';
+        $this->mother_phone = '';
+        $this->father_fiscal_code = '';
+        $this->mother_fiscal_code = '';
+        $this->father_doc_number = '';
+        $this->mother_doc_number = '';
+        $this->father_doc_type = '';
+        $this->mother_doc_type = '';
+        $this->mother_document_files = array();
+        $this->father_document_files = array();
+
+        $this->gender = null;
+        $this->no_send_mail = false;
+        $this->exclude_from_records = false;
+        $this->fiscal_code = '';
+        $this->address = '';
+        $this->zip_code = '';
+
+        $this->document_type = '';
+        $this->document_number = '';
+        $this->document_from = '';
+        $this->document_expire_date = null;
+        $this->document_files = array();
+
+        //$this->nation_id = null;
+        //$this->province_id = null;
+        $this->nation_id = null;
+        $this->province_id = null;
+
+        $this->city_id = null;
+        $this->phone = '';
+        $this->phone2 = '';
+        $this->phone3 = '';
+        $this->email = '';
+        $this->enabled = true;
+        $this->under18 = false;
+        $this->hasCertificate = false;
+        $this->money = 0;
+        $this->image = null;
+
+        $this->age = '';
+
+        $this->error_fc = false;
+        $this->already_existing = false;
+
+        $this->isSaving = false;
+
+        $this->emit('load-data-table');
+    }
+
+    public function resetCardFields()
+    {
+        $this->card_card_id = null;
+        $this->card_number = '';
+        $this->card_date = null;
+        $this->card_accept_date = null;
+        $this->card_status = 0;
+        $this->card_discipline1_id = null;
+        $this->card_discipline2_id = null;
+        $this->card_discipline3_id = null;
+    }
+
+    public function resetCertificateFields()
+    {
+        $this->certificate_type = '';
+        $this->certificate_filename = '';
+        $this->certificate_filename_old = '';
+        $this->certificate_expire_date = date('Y-m-d', strtotime('+1 year'));
+        $this->certificate_status = 0;
+    }
+
+    public function resetCourseFields()
+    {
+        $this->course_course_id = null;
+        $this->course_course_type = 'standard';
+        $this->course_months_list = [];
+        $this->course_price_list = [];
+        $this->course_when = array();
+        $this->course_when[] = array('day' => array(), 'from' => '', 'to' => '');
+        $this->course_date_from = null;
+        $this->course_date_to = null;
+        $this->course_course_subscription_id = null;
+        $this->course_status = 0;
+        $this->course_price = 0;
+        $this->course_subscription_price = 0;
+        $this->course_exist = false;
+        $this->course_subscribed = false;
+        $this->course_months = array();
+        $this->course_note = '';
+        $this->course_name = '';
+        $this->course_level_id = '';
+        $this->course_type_id = '';
+        $this->course_frequency_id = '';
+    }
+
+    public function executeMultipleAction()
+    {
+
+        if ($this->multipleAction == 'delete')
+            $this->multipleDelete();
+    }
+
+    public function updatedImage()
+    {
+        $this->validate([
+            'image' => 'image|max:1024',
+        ]);
+        $this->image_old = '';
+    }
+
+    public function updatedCourseName()
+    {
+        $this->course_course_id = null;
+        $this->course_level_id = '';
+        $this->course_frequency_id = '';
+        $levels_ids = [];
+        if ($this->course_name != '') {
+            list($n, $y) = explode("(", $this->course_name);
+            $y = trim(str_replace(")", "", $y));
+            $all = \App\Models\Course::where('name', 'like', '%' . trim($n) . "%")->where('year', $y)->where('enabled', true)->get();
+            foreach ($all as $a) {
+                $levels_ids[] = $a->course_level_id;
+            }
+        }
+        $this->course_levels = \App\Models\CourseLevel::select('*')->where('enabled', true)->whereIn('id', $levels_ids)->get();
+        $this->course_frequencies = [];
+    }
+
+    public function updatedCourseLevelId()
+    {
+        $this->course_course_id = null;
+        $this->course_frequency_id = '';
+        $frequencies_ids = [];
+        if ($this->course_level_id != '') {
+            list($n, $y) = explode("(", $this->course_name);
+            $y = trim(str_replace(")", "", $y));
+            $all = \App\Models\Course::where('name', 'like', '%' . trim($n) . "%")->where('year', $y)->where('enabled', true)->where('course_level_id', $this->course_level_id)->get();
+            foreach ($all as $a) {
+                $frequencies_ids[] = $a->course_frequency_id;
+            }
+        }
+        $this->course_frequencies = \App\Models\CourseFrequency::select('*')->where('enabled', true)->whereIn('id', $frequencies_ids)->get();
+    }
+
+    /*
+    public function updatedCourseTypeId()
+    {
+        $this->course_course_id = null;
+        $this->course_frequency_id = '';
+        $frequencies_ids = [];
+        if ($this->course_type_id != '') {
+            list($n, $y) = explode("(", $this->course_name);
+            $y = trim(str_replace(")", "", $y));
+            $all = \App\Models\Course::where('name', 'like', '%' . trim($n) . "%")->where('year', $y)->where('enabled', true)->where('course_level_id', $this->course_level_id)->where('course_type_id', $this->course_type_id)->get();
+            foreach ($all as $a) {
+                $frequencies_ids[] = $a->course_frequency_id;
+            }
+        }
+        $this->course_frequencies = \App\Models\CourseFrequency::select('*')->where('enabled', true)->whereIn('id', $frequencies_ids)->get();
+    }*/
+
+    public function updatedCourseFrequencyId()
+    {
+        $this->course_course_id = null;
+        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_frequency_id', $this->course_frequency_id)->first()->id;
+
+            $c = \App\Models\Course::findOrFail($this->course_course_id);
+            $this->course_price = formatPrice($c->price);
+            $this->course_subscription_price = formatPrice($c->subscription_price);
+            $this->course_date_from = $c->date_from;
+            $this->course_date_to = $c->date_to;
+
+            // Carico gli abbonamenti e i mesi
+            $period = \Carbon\CarbonPeriod::create($c->date_from, '1 month', $c->date_to);
+            $this->course_months_list = [];
+            foreach ($period as $dt) {
+                $this->course_months_list[] = $dt->format("m");
+            }
+
+            $course_subscription_ids = [];
+            $this->course_price_list = [];
+            if ($c->prices != null) {
+                foreach (json_decode($c->prices) as $z) {
+                    $this->course_price_list[$z->course_subscription_id] = $z->price;
+                    if ($z->price > 0)
+                        $course_subscription_ids[] = $z->course_subscription_id;
+                }
+            }
+
+            $this->course_subscriptions = \App\Models\CourseSubscription::select('*')->whereIn('id', $course_subscription_ids)->where('enabled', true)->get();
+
+        } else {
+            $this->course_price = 0;
+            $this->course_subscription_price = 0;
+            $this->course_exist = false;
+        }
+    }
+
+    public function updatedCourseCourseType()
+    {
+        $this->course_level_id = '';
+        $this->course_frequency_id = '';
+        $this->course_course_id = null;
+    }
+
+    public function updatedCourseCourseSubscriptionId()
+    {
+        $this->course_price = 0;
+        if (isset($this->course_price_list[$this->course_course_subscription_id]))
+            $this->course_price = $this->course_price_list[$this->course_course_subscription_id];
+    }
+
+    public $documents = [];
+
+    public $father_documents = [];
+
+    public $mother_documents = [];
+
+
+    public function removeDocument($idx, $type)
+    {
+        try {
+            if ($type == 'father') {
+                if (isset($this->father_document_files[$idx])) {
+                    $filePath = $this->father_document_files[$idx];
+                    $this->fileService->deleteFile($filePath);
+                    unset($this->father_document_files[$idx]);
+                    $this->father_document_files = array_values($this->father_document_files);
+                }
+            } elseif ($type == 'mother') {
+                if (isset($this->mother_document_files[$idx])) {
+                    $filePath = $this->mother_document_files[$idx];
+                    $this->fileService->deleteFile($filePath);
+                    unset($this->mother_document_files[$idx]);
+                    $this->mother_document_files = array_values($this->mother_document_files);
+                }
+            } else {
+                if (isset($this->document_files[$idx])) {
+                    $filePath = $this->document_files[$idx];
+                    $this->fileService->deleteFile($filePath);
+                    unset($this->document_files[$idx]);
+                    $this->document_files = array_values($this->document_files);
+                }
+            }
+        } catch (\Exception $e) {
+            session()->flash('error', 'Error removing document: ' . $e->getMessage());
+        }
+    }
+
+
+    public function updatedDocuments()
+    {
+        try {
+            foreach ($this->documents as $document) {
+                if ($this->dataId > 0) {
+                    $s3Path = $this->fileService->uploadDocument($document, $this->dataId, 'self');
+                    $this->document_files[] = $s3Path;
+                } else {
+                    $name = $document->getClientOriginalName();
+                    $document->storeAs('public', $name);
+                    $this->document_files[] = $name;
+                }
+            }
+            $this->documents = [];
+        } catch (\Exception $e) {
+            session()->flash('error', 'Error uploading documents: ' . $e->getMessage());
+        }
+    }
+
+    public function updatedFatherDocuments()
+    {
+        try {
+            foreach ($this->father_documents as $document) {
+                if ($this->dataId > 0) {
+                    $s3Path = $this->fileService->uploadDocument($document, $this->dataId, 'father');
+                    $this->father_document_files[] = $s3Path;
+                } else {
+                    $name = $document->getClientOriginalName();
+                    $document->storeAs('public', $name);
+                    $this->father_document_files[] = $name;
+                }
+            }
+            $this->father_documents = [];
+        } catch (\Exception $e) {
+            session()->flash('error', 'Error uploading father documents: ' . $e->getMessage());
+        }
+    }
+
+    public function updatedMotherDocuments()
+    {
+        try {
+            foreach ($this->mother_documents as $document) {
+                if ($this->dataId > 0) {
+                    $s3Path = $this->fileService->uploadDocument($document, $this->dataId, 'mother');
+                    $this->mother_document_files[] = $s3Path;
+                } else {
+                    $name = $document->getClientOriginalName();
+                    $document->storeAs('public', $name);
+                    $this->mother_document_files[] = $name;
+                }
+            }
+            $this->mother_documents = [];
+        } catch (\Exception $e) {
+            session()->flash('error', 'Error uploading mother documents: ' . $e->getMessage());
+        }
+    }
+    public function resetCategoryFields()
+    {
+        $this->category_category_id = null;
+    }
+
+    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());
+            if (count($record->childs))
+                $this->getCategories($record->childs, $indentation + 1);
+        }
+    }
+
+    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);
+        }
+    }
+
+    public function updatedBirthDate()
+    {
+        $this->validateOnly('birth_date');
+        $date1 = new DateTime($this->birth_date);
+        $date2 = new DateTime("now");
+        $interval = $date1->diff($date2);
+        $this->age = $interval->y . " anni";
+        $this->under18 = $interval->y < 18;
+    }
+
+    public function mount()
+    {
+
+        if (isset($_GET["new"])) {
+            $this->refreshAfter = 1;
+            $this->add();
+        }
+
+        if (isset($_GET["from"])) {
+            $this->from = $_GET["from"];
+        }
+
+        $this->cards = \App\Models\Card::select('id', 'name')->get();
+
+        $this->disciplines = \App\Models\Discipline::select('id', 'name')->get();
+
+        $this->categories = array();
+
+        $this->getCategories(\App\Models\Category::select('id', 'name')->where('parent_id', null)->orderBy('name')->get(), 0);
+
+        $c = \App\Models\Causal::where('type', 'IN')->where('money', true)->first();
+        if ($c)
+            $this->causalId = $c->id;
+
+        if (isset($_GET["member_detail"])) {
+            $this->showDetailF($_GET["member_detail"]);
+            $this->refreshAfter = 1;
+        }
+    }
+
+    public function boot(){
+        $this->fileService = app(\App\Services\MemberFileService::class);
+        app(abstract: TenantMiddleware::class)->setupTenantConnection();
+
+    }
+
+    public function updated()
+    {
+        if ($this->isSaving) {
+            $zzz = [];
+            if ($this->birth_nation_id == null)
+                $zzz[] = 'birth_nation_id=' . $this->birth_nation_id;
+            if ($this->isBirthItaly) {
+                if ($this->birth_province_id == null)
+                    $zzz[] = 'birth_province_id=' . $this->birth_province_id;
+                if ($this->birth_city_id == null)
+                    $zzz[] = 'birth_city_id=' . $this->birth_city_id;
+            }
+            if ($this->nation_id == null)
+                $zzz[] = 'nation_id=' . $this->nation_id;
+            if ($this->isItaly) {
+                if ($this->province_id == null)
+                    $zzz[] = 'province_id=' . $this->province_id;
+                if ($this->city_id == null)
+                    $zzz[] = 'city_id=' . $this->city_id;
+            }
+
+            $this->emit('setErrorMsg', $zzz);
+        }
+        // $this->emit('load-select');
+    }
+
+    public function hydrate()
+    {
+
+        if ($this->isSaving) {
+            $zzz = [];
+            if ($this->birth_nation_id == null)
+                $zzz[] = 'birth_nation_id=' . $this->birth_nation_id;
+            if ($this->isBirthItaly) {
+                if ($this->birth_province_id == null)
+                    $zzz[] = 'birth_province_id=' . $this->birth_province_id;
+                if ($this->birth_city_id == null)
+                    $zzz[] = 'birth_city_id=' . $this->birth_city_id;
+            }
+            if ($this->nation_id == null)
+                $zzz[] = 'nation_id=' . $this->nation_id;
+            if ($this->isItaly) {
+                if ($this->province_id == null)
+                    $zzz[] = 'province_id=' . $this->province_id;
+                if ($this->city_id == null)
+                    $zzz[] = 'city_id=' . $this->city_id;
+            }
+
+            $this->emit('setErrorMsg', $zzz);
+        }
+
+        $this->emit('load-select');
+        // $this->emit('destroy-data-table');
+
+    }
+
+    public function checkIsItaly()
+    {
+        if ($this->nation_id > 0) {
+            $n = \App\Models\Nation::findOrFail($this->nation_id);
+            $this->isItaly = $n->is_italy;
+        } else {
+            $this->isItaly = false;
+        }
+    }
+
+    public function checkIsBirthItaly()
+    {
+        if ($this->birth_nation_id > 0) {
+            $n = \App\Models\Nation::findOrFail($this->birth_nation_id);
+            $this->isBirthItaly = $n->is_italy;
+        } else
+            $this->isBirthItaly = false;
+    }
+    public function search()
+    {
+        if ($this->searchTxt != '') {
+            $this->search = $this->searchTxt;
+            $this->showReset = true;
+        }
+    }
+    public function resetSearch()
+    {
+        $this->showReset = false;
+        $this->searchTxt = '';
+        $this->search = $this->searchTxt;
+    }
+
+    public function advancedSearch()
+    {
+        $this->advanced = true;
+    }
+
+    public function advancedSearchCancel()
+    {
+        $this->filterCard = [];
+        $this->filterCategory = [];
+        $this->filterCertNormal = 0;
+        $this->filterCertAgonistic = 0;
+        $this->filterCertScaduto = 0;
+        $this->filterCertInScadenza = 0;
+        $this->advanced = false;
+    }
+
+    public function render()
+    {
+
+        $this->course_names = [];
+        $allC = \App\Models\Course::where('type', $this->course_course_type)->where('enabled', true)->orderBy('name')->get();
+        foreach ($allC as $c) {
+            $cN = $c->name . " (" . $c->year . ")";
+            if (!in_array($cN, $this->course_names))
+                $this->course_names[] = $cN;
+        }
+
+
+        $datas = [];
+        if (false) {
+
+            if (!$this->advanced) {
+                $this->records = \App\Models\Member::select('id', 'first_name', 'last_name', 'phone')->get();
+            } else {
+                $datas = \App\Models\Member::select('members.*')->where('id', '>', 0);
+                if (sizeof($this->filterCard) > 0) {
+                    $card_ids = \App\Models\MemberCard::whereIn('card_id', $this->filterCard)->pluck('member_id');
+                    $datas = $datas->whereIn('id', $card_ids);
+                }
+                if (sizeof($this->filterCategory) > 0) {
+                    $cats_ids = \App\Models\MemberCategory::whereIn('category_id', $this->filterCategory)->pluck('member_id');
+                    $datas = $datas->whereIn('id', $cats_ids);
+                }
+                $certs = [];
+
+                if ($this->filterCertNormal > 0) {
+                    $normal = \App\Models\MemberCertificate::where('type', 'N')->pluck('member_id');
+                    $datas = $datas->whereIn('id', $normal);;
+                }
+                if ($this->filterCertAgonistic > 0) {
+                    $agonistic = \App\Models\MemberCertificate::where('type', 'A')->pluck('member_id');
+                    $datas = $datas->whereIn('id', $agonistic);
+                }
+                if ($this->filterCertScaduto > 0) {
+                    $scaduto = \App\Models\MemberCertificate::where('expire_date', '<', date("Y-m-d"))->pluck('member_id');
+                    $datas = $datas->whereIn('id', $scaduto);
+                }
+                if ($this->filterCertInScadenza > 0) {
+                    $scaduto = \App\Models\MemberCertificate::whereBetween('expire_date', [date("Y-m-d"), date("Y-m-d", strtotime("+1 month"))])->pluck('member_id');
+                    $datas = $datas->whereIn('id', $scaduto);
+                }
+                if (sizeof($certs) > 0) {
+                    $datas = $datas->whereIn('id', $certs);
+                }
+                $this->records = $datas->get();
+            }
+
+            foreach ($this->records as $r) {
+                $r->age = $r->getAge();
+                $active = $r->isActive();
+                $r->status = $active["status"];
+                $r->date = $active["date"];
+                $r->certificate = $r->hasCertificate()["date"];
+                $r->state = $r->getStatus()["status"];
+            }
+
+
+            $this->emit('load-data-table');
+        }
+
+        $this->loadMemberCards();
+        $this->loadMemberCourses();
+        $this->loadMemberCategories();
+        $this->loadMemberCertificates();
+
+        $this->courses = \App\Models\Course::select('id', 'name')->where('type', $this->course_course_type)->get();
+
+        return view('livewire.member', ['datas' => $datas]);
+    }
+
+    public function updatedCourseCourseId()
+    {
+        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);
+            $this->course_date_from = $c->date_from;
+            $this->course_date_to = $c->date_to;
+            // 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;
+
+            // Carico gli abbonamenti e i mesi
+            $period = \Carbon\CarbonPeriod::create($c->date_from, '1 month', $c->date_to);
+            $this->course_months_list = [];
+            foreach ($period as $dt) {
+                $this->course_months_list[] = $dt->format("m");
+            }
+
+            $course_subscription_ids = [];
+            $this->course_price_list = [];
+            if ($c->prices != null) {
+                foreach (json_decode($c->prices) as $z) {
+                    $this->course_price_list[$z->course_subscription_id] = $z->price;
+                    if ($z->price > 0)
+                        $course_subscription_ids[] = $z->course_subscription_id;
+                }
+            }
+
+            $this->course_subscriptions = \App\Models\CourseSubscription::select('*')->whereIn('id', $course_subscription_ids)->where('enabled', true)->get();
+        } else {
+            $this->course_price = 0;
+            $this->course_subscription_price = 0;
+            $this->course_exist = false;
+        }
+    }
+
+    public function loadMemberCards()
+    {
+        $this->member_cards = \App\Models\MemberCard::where('member_id', $this->dataId)->get();
+    }
+
+    public function loadMemberCourses()
+    {
+        $this->member_courses = \App\Models\MemberCourse::where('member_id', $this->dataId)->get();
+    }
+
+    public function loadMemberCategories()
+    {
+        $this->member_categories = \App\Models\MemberCategory::where('member_id', $this->dataId)->get();
+    }
+
+    public function loadMemberCertificates()
+    {
+        $this->member_certificates = \App\Models\MemberCertificate::where('member_id', $this->dataId)->orderBy('expire_date', 'DESC')->get();
+    }
+
+    public function showDetailF($id)
+    {
+        if (!isset($_GET["from"]) && $this->from == '')
+            $this->from = 'members';
+        $this->currentMember = \App\Models\Member::findOrFail($id);
+        $this->currentStatus = $this->currentMember->getStatus();
+        $this->showDetail = true;
+    }
+
+    public function add()
+    {
+        //$this->from = 'members';
+        $this->emit('load-select');
+        $this->emit('hide-search');
+        $this->showDetail = false;
+        $this->resetFields();
+        $this->add = true;
+        $this->update = false;
+        $this->emit('setEdit', true);
+        $this->emit('setEditCorso', false);
+        $this->dispatchBrowserEvent('scroll-to-top');
+    }
+
+    public function store($close)
+    {
+
+        $this->isSaving = true;
+
+        $this->emit('load-select');
+
+        $this->emit('goToTop');
+
+        $rules = [
+            'first_name' => 'required',
+            'last_name' => 'required',
+            'birth_nation_id' => 'required',
+            // 'email' => 'required',
+            // 'phone' => 'required',
+            'address' => 'required',
+            'zip_code' => 'required',
+            'nation_id' => 'required',
+            'birth_date' => 'before_or_equal:today',
+            'gender' => 'required'
+        ];
+
+        if ($this->isBirthItaly) {
+            $rules['birth_province_id'] = 'required';
+            $rules['birth_city_id'] = 'required';
+            $rules['fiscal_code'] = 'required';
+        } else {
+            $rules['birth_place'] = 'required';
+        }
+
+        if ($this->isItaly) {
+            $rules['province_id'] = 'required';
+            $rules['city_id'] = 'required';
+        }
+
+        $zzz = [];
+        if ($this->birth_nation_id == null)
+            $zzz[] = 'birth_nation_id=' . $this->birth_nation_id;
+        if ($this->isBirthItaly) {
+            if ($this->birth_province_id == null)
+                $zzz[] = 'birth_province_id=' . $this->birth_province_id;
+            if ($this->birth_city_id == null)
+                $zzz[] = 'birth_city_id=' . $this->birth_city_id;
+        }
+        if ($this->nation_id == null)
+            $zzz[] = 'nation_id=' . $this->nation_id;
+        if ($this->isItaly) {
+            if ($this->province_id == null)
+                $zzz[] = 'province_id=' . $this->province_id;
+            if ($this->city_id == null)
+                $zzz[] = 'city_id=' . $this->city_id;
+        }
+
+        $this->emit('setErrorMsg', $zzz);
+
+        if (!empty($this->fiscal_code)) {
+            $existingMember = \App\Models\Member::where('fiscal_code', $this->fiscal_code)->first();
+            if ($existingMember) {
+                $this->already_existing = true;
+                $this->emit('focus-error-field', 'fiscal_code');
+
+                return;
+            }
+        }
+
+        if ($this->under18) {
+            $rules['father_name'] = 'required_without:mother_name';
+            $rules['father_email'] = 'required_without:mother_email|email';
+            $rules['father_fiscal_code'] = 'required_without:mother_fiscal_code';
+            $rules['mother_name'] = 'required_without:father_name';
+            $rules['mother_email'] = 'required_without:father_email|email';
+            $rules['mother_fiscal_code'] = 'required_without:father_fiscal_code';
+        } else {
+            $rules['email'] = 'required';
+            $rules['phone'] = 'required';
+        }
+        
+        $std_rules = $rules;
+        // $rules = [
+        //     'first_name' => 'required',
+        //     'last_name' => 'required'
+        // ];
+
+        try {
+            $this->validate($rules);
+        } catch (\Illuminate\Validation\ValidationException $e) {
+            $this->isSaving = false;
+            $errorFields = array_keys($e->errors());
+            if (!empty($errorFields)) {
+                $this->emit('focus-error-field', $errorFields[0]);
+            }
+            Log::error('Validation failed', ['errors' => $e->errors()]);
+            return;
+        }
+        try {
+
+            $imageName = '';
+            if ($this->image) {
+                $imageName = md5($this->image . microtime()) . '.' . $this->image->extension();
+                $this->image->storeAs('public', $imageName);
+            }
+
+            $docs = implode("|", $this->document_files);
+            $father_docs = implode("|", $this->father_document_files);
+            $mother_docs = implode("|", $this->mother_document_files);
+
+
+            $to_complete = false;
+            // try {
+            //     $this->validate($std_rules);
+            // } catch(\Illuminate\Validation\ValidationException $e) {
+            //     $to_complete = true;
+            // } catch (\Exception $e) {
+            //     $to_complete = true;
+            // }
+
+            $member = \App\Models\Member::create([
+                'first_name' => strtoupper($this->first_name),
+                'last_name' => strtoupper($this->last_name),
+                'status' => $this->status,
+                'birth_city_id' => $this->birth_city_id > 0 ? $this->birth_city_id : null,
+                'birth_province_id' => $this->birth_province_id > 0 ? $this->birth_province_id : null,
+                'birth_nation_id' => $this->birth_nation_id > 0 ? $this->birth_nation_id : null,
+                'birth_date' => $this->birth_date,
+                'birth_place' => $this->birth_place,
+                'father_name' => $this->father_name,
+                'mother_name' => $this->mother_name,
+                'father_email' => strtolower($this->father_email),
+                'mother_email' => strtolower($this->mother_email),
+                'father_phone' => $this->father_phone,
+                'mother_phone' => $this->mother_phone,
+                'father_fiscal_code' => $this->father_fiscal_code,
+                'mother_fiscal_code' => $this->mother_fiscal_code,
+                'father_doc_number' => $this->father_doc_number,
+                'father_doc_type' => $this->father_doc_type,
+                'mother_doc_number' => $this->mother_doc_number,
+                'mother_doc_type' => $this->mother_doc_type,
+                'document_type' => $this->document_type,
+                'document_number' => $this->document_number,
+                'document_from' => $this->document_from,
+                'document_expire_date' => $this->document_expire_date,
+                'document_files' => $docs,
+                'mother_document_files' => $mother_docs,
+                'father_document_files' => $father_docs,
+                'gender' => $this->gender,
+                'no_send_mail' => $this->no_send_mail,
+                'exclude_from_records' => $this->exclude_from_records,
+                'fiscal_code' => $this->fiscal_code,
+                'address' => $this->address,
+                'zip_code' => $this->zip_code,
+                'nation_id' => $this->nation_id > 0 ? $this->nation_id : null,
+                'province_id' => $this->province_id > 0 ? $this->province_id : null,
+                'city_id' => $this->city_id > 0 ? $this->city_id : null,
+                'phone' => $this->phone,
+                'phone2' => $this->phone2,
+                'phone3' => $this->phone3,
+                'email' => strtolower($this->email),
+                'image' => $imageName,
+                'to_complete' => $to_complete,
+                'enabled' => $this->enabled
+            ]);
+            $this->fileService->createMemberFolders($member->id);
+
+            if ($this->image) {
+                $s3ImagePath = $this->fileService->uploadProfileImage($this->image, $member->id);
+                $member->update(['image' => $s3ImagePath]);
+            }
+            $this->migrateTemporaryFiles($member->id);
+
+
+            session()->flash('success, Tesserato creato');
+            updateMemberData($member->id);
+            $this->resetFields();
+            if ($close) {
+                $this->add = false;
+                return redirect()->to('/members');
+            } else {
+                $this->edit($member->id);
+                $this->emit('saved-and-continue', $this->type);
+                $this->dispatchBrowserEvent('scroll-to-top');
+            }
+            $this->emit('setEdit', false);
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    private function migrateTemporaryFiles($memberId)
+    {
+        try {
+            $updatedPaths = [];
+
+            // Migrate document files
+            $newDocumentFiles = [];
+            foreach ($this->document_files as $filePath) {
+                if (strpos($filePath, '/members/') === false) {
+                    // This is a temporary local file, move to S3
+                    $localPath = storage_path('app/public/' . $filePath);
+                    if (file_exists($localPath)) {
+                        $uploadedFile = new \Illuminate\Http\UploadedFile(
+                            $localPath,
+                            basename($filePath),
+                            mime_content_type($localPath),
+                            filesize($localPath),
+                            0,
+                            true
+                        );
+                        $s3Path = $this->fileService->uploadDocument($uploadedFile, $memberId, 'self');
+                        $newDocumentFiles[] = $s3Path;
+                        // Delete temporary file
+                        unlink($localPath);
+                    }
+                } else {
+                    $newDocumentFiles[] = $filePath;
+                }
+            }
+            if (!empty($newDocumentFiles)) {
+                $updatedPaths['document_files'] = implode('|', $newDocumentFiles);
+            }
+
+            // Migrate father document files
+            $newFatherFiles = [];
+            foreach ($this->father_document_files as $filePath) {
+                if (strpos($filePath, '/members/') === false) {
+                    $localPath = storage_path('app/public/' . $filePath);
+                    if (file_exists($localPath)) {
+                        $uploadedFile = new \Illuminate\Http\UploadedFile(
+                            $localPath,
+                            basename($filePath),
+                            mime_content_type($localPath),
+                            filesize($localPath),
+                            0,
+                            true
+                        );
+                        $s3Path = $this->fileService->uploadDocument($uploadedFile, $memberId, 'father');
+                        $newFatherFiles[] = $s3Path;
+                        unlink($localPath);
+                    }
+                } else {
+                    $newFatherFiles[] = $filePath;
+                }
+            }
+            if (!empty($newFatherFiles)) {
+                $updatedPaths['father_document_files'] = implode('|', $newFatherFiles);
+            }
+
+            // Migrate mother document files
+            $newMotherFiles = [];
+            foreach ($this->mother_document_files as $filePath) {
+                if (strpos($filePath, '/members/') === false) {
+                    $localPath = storage_path('app/public/' . $filePath);
+                    if (file_exists($localPath)) {
+                        $uploadedFile = new \Illuminate\Http\UploadedFile(
+                            $localPath,
+                            basename($filePath),
+                            mime_content_type($localPath),
+                            filesize($localPath),
+                            0,
+                            true
+                        );
+                        $s3Path = $this->fileService->uploadDocument($uploadedFile, $memberId, 'mother');
+                        $newMotherFiles[] = $s3Path;
+                        unlink($localPath);
+                    }
+                } else {
+                    $newMotherFiles[] = $filePath;
+                }
+            }
+            if (!empty($newMotherFiles)) {
+                $updatedPaths['mother_document_files'] = implode('|', $newMotherFiles);
+            }
+
+            // Update member with new S3 paths
+            if (!empty($updatedPaths)) {
+                \App\Models\Member::whereId($memberId)->update($updatedPaths);
+            }
+        } catch (\Exception $e) {
+            Log::error('Error migrating temporary files', [
+                'member_id' => $memberId,
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+
+
+    public function duplicate($id)
+    {
+        $member = \App\Models\Member::findOrFail($id);
+        $newMember = $member->replicate();
+        $newMember->save();
+        $this->edit($newMember->id);
+    }
+
+    public function edit($id)
+    {
+
+        if (!isset($_GET["from"]) && $this->from == '')
+            $this->from = 'members';
+        $this->showDetail = false;
+        $this->emit('setEdit', true);
+        $this->emit('setEditCorso', false);
+        $this->emit('hide-search');
+        try {
+            $member = \App\Models\Member::findOrFail($id);
+            if (!$member) {
+                session()->flash('error', 'Tesserato non trovato');
+            } else {
+
+                $this->first_name = $member->first_name;
+                $this->last_name = $member->last_name;
+                $this->status = $member->status;
+                $this->birth_city_id = $member->birth_city_id;
+                $this->birth_province_id = $member->birth_province_id;
+                $this->birth_nation_id = $member->birth_nation_id;
+                $this->birth_date = $member->birth_date;
+                $this->birth_place = $member->birth_place;
+                $this->father_name = $member->father_name;
+                $this->mother_name = $member->mother_name;
+                $this->father_email = strtolower($member->father_email);
+                $this->mother_email = strtolower($member->mother_email);
+                $this->father_phone = $member->father_phone;
+                $this->mother_phone = $member->mother_phone;
+                $this->father_fiscal_code = $member->father_fiscal_code;
+                $this->mother_fiscal_code = $member->mother_fiscal_code;
+                $this->father_doc_number = $member->father_doc_number;
+                $this->mother_doc_number = $member->mother_doc_number;
+                $this->father_doc_type = $member->father_doc_type;
+                $this->mother_doc_type = $member->mother_doc_type;
+                $this->father_document_files = explode("|", $member->father_document_files);
+                $this->mother_document_files = explode("|", $member->mother_document_files);
+                $this->document_type = $member->document_type;
+                $this->document_number = $member->document_number;
+                $this->document_from = $member->document_from;
+                $this->document_expire_date = $member->document_expire_date;
+                $this->document_files = explode("|", $member->document_files);
+                $this->gender = $member->gender;
+                $this->no_send_mail = $member->no_send_mail;
+                $this->exclude_from_records = $member->exclude_from_records;
+                $this->fiscal_code = $member->fiscal_code;
+                $this->address = $member->address;
+                $this->zip_code = $member->zip_code;
+                $this->nation_id = $member->nation_id;
+                $this->province_id = $member->province_id;
+                $this->city_id = $member->city_id;
+                $this->phone = $member->phone;
+                $this->phone2 = $member->phone2;
+                $this->phone3 = $member->phone3;
+                $this->email = strtolower($member->email);
+                $this->image_old = $member->image;
+                $this->enabled = $member->enabled;
+                $this->dataId = $member->id;
+
+                $this->active = $member->getStatus();
+                $this->hasCertificate = $member->hasCertificate()["status"];
+
+                $this->money = $member->getMoney();
+
+                $this->checkIsBirthItaly();
+
+                $date1 = new DateTime($this->birth_date);
+                $date2 = new DateTime("now");
+                $interval = $date1->diff($date2);
+                $this->age = $interval->y . " anni";
+                $this->under18 = $interval->y < 18;
+                $this->update = true;
+                $this->add = false;                
+
+                $this->emit('setIds', $this->nation_id, $this->birth_nation_id);
+
+                $this->emit('load-select');
+                
+                $this->emit('load-provinces', $this->nation_id, 'provinceClass');
+                $this->emit('load-provinces', $this->birth_nation_id, 'provinceBirthClass');
+                
+                $this->emit('load-cities', $this->province_id, 'cityClass');
+                $this->emit('load-cities', $this->birth_province_id, 'cityBirthClass');
+
+                $this->dispatchBrowserEvent('scroll-to-top');
+            }
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function updateAAA($close)
+    {
+
+        $this->emit('goToTop');
+
+        $this->isSaving = true;
+
+        $this->emit('load-select');
+
+        $rules = [
+            'first_name' => 'required',
+            'last_name' => 'required',
+            // 'email' => 'required',
+            // 'phone' => 'required',
+            'birth_nation_id' => 'required',
+            'address' => 'required',
+            'zip_code' => 'required',
+            'nation_id' => 'required',
+            'birth_date' => 'before_or_equal:today'
+        ];
+
+        if ($this->isBirthItaly) {
+            $rules['birth_province_id'] = 'required';
+            $rules['birth_city_id'] = 'required';
+            $rules['fiscal_code'] = 'required';
+        } else {
+            $rules['birth_place'] = 'required';
+        }
+        Log::info('isItaly', ['isItaly' => $this->isItaly]);
+        $this->isItaly = $this->checkIsItaly();
+
+
+        if ($this->isItaly) {
+            $rules['province_id'] = 'required';
+            $rules['city_id'] = 'required';
+        }
+
+        $zzz = [];
+        if ($this->birth_nation_id == null)
+            $zzz[] = 'birth_nation_id=' . $this->birth_nation_id;
+        if ($this->isBirthItaly) {
+            if ($this->birth_province_id == null)
+                $zzz[] = 'birth_province_id=' . $this->birth_province_id;
+            if ($this->birth_city_id == null)
+                $zzz[] = 'birth_city_id=' . $this->birth_city_id;
+        }
+        if ($this->nation_id == null)
+            $zzz[] = 'nation_id=' . $this->nation_id;
+        if ($this->isItaly) {
+            if ($this->province_id == null)
+                $zzz[] = 'province_id=' . $this->province_id;
+            if ($this->city_id == null)
+                $zzz[] = 'city_id=' . $this->city_id;
+        }
+
+        $this->emit('setErrorMsg', $zzz);
+
+        if ($this->under18) {
+            $rules['father_name'] = 'required_without:mother_name';
+            $rules['father_email'] = 'required_without:mother_email|email';
+            $rules['father_fiscal_code'] = 'required_without:mother_fiscal_code';
+            $rules['mother_name'] = 'required_without:father_name';
+            $rules['mother_email'] = 'required_without:father_email|email';
+            $rules['mother_fiscal_code'] = 'required_without:father_fiscal_code';
+        } else {
+            $rules['email'] = 'required';
+            $rules['phone'] = 'required';
+        }
+
+        
+        $std_rules = $rules;
+        // $rules = [
+        //     'first_name' => 'required',
+        //     'last_name' => 'required'
+        // ];
+
+        try {
+            $this->validate($rules);
+        } catch (\Illuminate\Validation\ValidationException $e) {
+            $this->isSaving = false;
+            $errorFields = array_keys($e->errors());
+            if (!empty($errorFields)) {
+                $this->emit('focus-error-field', $errorFields[0]);
+            }
+            Log::error('Validation failed', ['errors' => $e->errors()]);
+            return;
+        }
+
+        try {
+
+            $imagePath = $this->image_old; // Keep existing if no new image
+            if ($this->image) {
+                $imagePath = $this->fileService->uploadProfileImage($this->image, $this->dataId);
+            }
+
+
+            $docs = implode("|", $this->document_files);
+            $father_docs = implode("|", $this->father_document_files);
+            $mother_docs = implode("|", $this->mother_document_files);
+
+            $to_complete = false;
+            // try {
+            //     $this->validate($std_rules);
+            // } catch(\Illuminate\Validation\ValidationException $e) {
+            //     $to_complete = true;
+            // } catch (Exception $e) {
+            //     $to_complete = true;
+            // }
+
+            \App\Models\Member::whereId($this->dataId)->update([
+                'first_name' => strtoupper($this->first_name),
+                'last_name' => strtoupper($this->last_name),
+                'status' => $this->status,
+                'birth_city_id' => $this->birth_city_id > 0 ? $this->birth_city_id : null,
+                'birth_province_id' => $this->birth_province_id > 0 ? $this->birth_province_id : null,
+                'birth_nation_id' => $this->birth_nation_id > 0 ? $this->birth_nation_id : null,
+                'birth_date' => $this->birth_date,
+                'birth_place' => $this->birth_place,
+                'father_name' => $this->father_name,
+                'mother_name' => $this->mother_name,
+                'father_email' => strtolower($this->father_email),
+                'mother_email' => strtolower($this->mother_email),
+                'father_phone' => $this->father_phone,
+                'mother_phone' => $this->mother_phone,
+                'father_fiscal_code' => $this->father_fiscal_code,
+                'mother_fiscal_code' => $this->mother_fiscal_code,
+                'father_doc_number' => $this->father_doc_number,
+                'father_doc_type' => $this->father_doc_type,
+                'mother_doc_number' => $this->mother_doc_number,
+                'mother_doc_type' => $this->mother_doc_type,
+                'father_document_files' => $father_docs,
+                'mother_document_files' => $mother_docs,
+                'document_type' => $this->document_type,
+                'document_number' => $this->document_number,
+                'document_from' => $this->document_from,
+                'document_expire_date' => $this->document_expire_date,
+                'document_files' => $docs,
+                'gender' => $this->gender,
+                'no_send_mail' => $this->no_send_mail,
+                'exclude_from_records' => $this->exclude_from_records,
+                'fiscal_code' => $this->fiscal_code,
+                'address' => $this->address,
+                'zip_code' => $this->zip_code,
+                'nation_id' => $this->nation_id > 0 ? $this->nation_id : null,
+                'province_id' => $this->province_id > 0 ? $this->province_id : null,
+                'city_id' => $this->city_id > 0 ? $this->city_id : null,
+                'image' => $imagePath,
+                'phone' => $this->phone,
+                'phone2' => $this->phone2,
+                'phone3' => $this->phone3,
+                'email' => strtolower($this->email),
+                'to_complete' => $to_complete,
+                'enabled' => $this->enabled
+            ]);
+            updateMemberData($this->dataId);
+            session()->flash('success', 'Tesserato aggiornato');
+            if ($close) {
+                $this->resetFields();
+                $this->update = false;
+                return redirect()->to('/members');
+            } else {
+                $this->emit('saved-and-continue', $this->type);
+                $this->dispatchBrowserEvent('scroll-to-top');
+            }
+            $this->emit('setEdit', false);
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function cancel()
+    {
+        $this->add = false;
+        $this->update = false;
+        $this->showDetail = false;
+        $this->resetFields();
+        $this->emit('setEdit', false);
+        //$this->emit('setEditCorso', false);
+        $this->emit('reload');
+    }
+
+    public function delete($id)
+    {
+        try {
+            \App\Models\Member::find($id)->delete();
+            session()->flash('success', "Tesserato eliminato");
+            $this->emit('reload');
+        } catch (\Exception $e) {
+            session()->flash('error', 'Errore (' . $e->getMessage() . ')');
+        }
+    }
+
+    public function checkUncheckAll()
+    {
+        if (!$this->checkedAll) {
+            $this->multipleIds = array();
+        } else {
+            foreach ($this->records as $r) {
+                $this->multipleIds[] = $r->id;
+            }
+        }
+    }
+
+    public function multipleDelete()
+    {
+        try {
+            foreach ($this->multipleIds as $id) {
+                \App\Models\Member::find($id)->delete();
+            }
+        } catch (\Exception $e) {
+            session()->flash('error', 'Errore (' . $e->getMessage() . ')');
+        }
+        $this->multipleAction = '';
+    }
+
+    // Card
+
+    public function addCard()
+    {
+        $this->resetCardFields();
+        $this->addCard = true;
+        $this->updateCard = false;
+    }
+
+    public function storeCard()
+    {
+
+        $this->validate(['card_card_id' => 'required']);
+        try {
+            // in base alla card selezionata calcolo la scadenza
+            $expire_date = null;
+            if ($this->card_date != '') {
+                $y = date("Y", strtotime($this->card_date));
+                $card = \App\Models\Card::findOrFail($this->card_card_id);
+                if ($card->next_day_expire > 0 && $card->next_month_expire > 0) {
+
+                    $m = strlen($card->next_month_expire) == 1 ? ('0' . $card->next_month_expire) : $card->next_month_expire;
+
+                    if (date("md", strtotime($this->card_date)) > ($m . $card->next_day_expire))
+                        $y += 1;
+
+                    $next_exp = date($y . "-" . $m . "-" . $card->next_day_expire);
+
+                    if ($next_exp > $this->card_date) {
+                        $expire_date = $next_exp;
+                    } else
+                        $expire_date = date($y . "-" . $card->next_month_expire . "-" . $card->next_day_expire, strtotime(' + 1 years'));
+                } else {
+                    if ($card->one_year_expire) {
+                        $expire_date = date("Y-m-d", strtotime($this->card_date . ' + 1 years'));
+                    }
+                }
+            }
+            \App\Models\MemberCard::create([
+                'member_id' => $this->dataId,
+                'card_id' => $this->card_card_id,
+                'number' => $this->card_number,
+                'date' => $this->card_date,
+                'accept_date' => $this->card_accept_date != '' ? $this->card_accept_date : $this->card_date,
+                'expire_date' => $expire_date,
+                'status' => $this->card_status,
+                'discipline1_id' => $this->card_discipline1_id,
+                'discipline2_id' => $this->card_discipline2_id,
+                'discipline3_id' => $this->card_discipline3_id,
+            ]);
+            updateMemberData($this->dataId);
+            session()->flash('success, Tesserato creato');
+            $this->resetCardFields();
+            $this->addCard = false;
+            $this->loadMemberCards();
+            // TODO $this->checkCourseAvailability();
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function editCard($id)
+    {
+        try {
+            $memberCard = \App\Models\MemberCard::findOrFail($id);
+            if (!$memberCard) {
+                session()->flash('error', 'Tesserato non trovato');
+            } else {
+                $this->card_card_id = $memberCard->card_id;
+                $this->card_number = $memberCard->number;
+                $this->card_date = $memberCard->date;
+                $this->card_accept_date = $memberCard->accept_date;
+                $this->card_status = $memberCard->status;
+                $this->card_discipline1_id = $memberCard->discipline1_id;
+                $this->card_discipline2_id = $memberCard->discipline2_id;
+                $this->card_discipline3_id = $memberCard->discipline3_id;
+                $this->cardDataId = $memberCard->id;
+                $this->updateCard = true;
+                $this->addCard = false;
+            }
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function updateCard()
+    {
+        $this->validate(['card_card_id' => 'required']);
+
+        try {
+            Log::info('Starting card update', [
+                'member_id' => $this->dataId,
+                'card_id' => $this->card_card_id,
+                'card_number' => $this->card_number
+            ]);
+
+            $expire_date = null;
+            if ($this->card_date != '') {
+                $card = \App\Models\Card::findOrFail($this->card_card_id);
+
+                Log::info('Card details', [
+                    'card_id' => $card->id,
+                    'next_day_expire' => $card->next_day_expire,
+                    'next_month_expire' => $card->next_month_expire,
+                    'one_year_expire' => $card->one_year_expire
+                ]);
+
+                if ($card->next_day_expire > 0 && $card->next_month_expire > 0) {
+                    $y = date("Y", strtotime($this->card_date));
+                    $m = strlen($card->next_month_expire) == 1 ? ('0' . $card->next_month_expire) : $card->next_month_expire;
+                    $d = strlen($card->next_day_expire) == 1 ? ('0' . $card->next_day_expire) : $card->next_day_expire;
+                    $next_exp = $y . "-" . $m . "-" . $d;
+
+                    // Format dates for comparison
+                    $input_date_obj = new \DateTime($this->card_date);
+                    $next_exp_obj = new \DateTime($next_exp);
+
+                    if ($next_exp_obj > $input_date_obj) {
+                        $expire_date = $next_exp;
+                    } else {
+                        // Add 1 year to the expiration date if the input date is after the expiration date
+                        $next_exp_obj->modify('+1 year');
+                        $expire_date = $next_exp_obj->format('Y-m-d');
+                    }
+
+                    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,
+                        'comparison' => ($next_exp_obj > $input_date_obj) ? 'next_exp is after input_date' : 'next_exp is before or equal to input_date'
+                    ]);
+                } else {
+                    if ($card->one_year_expire) {
+                        $expire_date = date("Y-m-d", strtotime($this->card_date . ' + 1 years'));
+
+                        Log::info('Calculated expiration date (one_year_expire rule)', [
+                            'input_date' => $this->card_date,
+                            'expire_date' => $expire_date
+                        ]);
+                    }
+                }
+            }
+
+            Log::info('Updating member card', [
+                'card_id' => $this->cardDataId,
+                'member_id' => $this->dataId,
+                'card_number' => $this->card_number,
+                'date' => $this->card_date,
+                'expire_date' => $expire_date,
+                'status' => $this->card_status
+            ]);
+
+            \App\Models\MemberCard::whereId($this->cardDataId)->update([
+                'member_id' => $this->dataId,
+                'card_id' => $this->card_card_id,
+                'number' => $this->card_number,
+                'date' => $this->card_date,
+                'accept_date' => $this->card_accept_date != '' ? $this->card_accept_date : $this->card_date,
+                'expire_date' => $expire_date,
+                'status' => $this->card_status,
+                'discipline1_id' => $this->card_discipline1_id,
+                'discipline2_id' => $this->card_discipline2_id,
+                'discipline3_id' => $this->card_discipline3_id,
+            ]);
+
+            updateMemberData($this->dataId);
+
+            Log::info('Card updated successfully', [
+                'card_id' => $this->cardDataId,
+                'member_id' => $this->dataId
+            ]);
+
+            session()->flash('success', 'Tesserato aggiornato');
+            $this->resetCardFields();
+            $this->updateCard = false;
+            $this->loadMemberCards();
+            // TODO $this->checkCourseAvailability();
+        } catch (\Exception $ex) {
+            Log::error('Error updating card', [
+                'card_id' => $this->cardDataId,
+                'member_id' => $this->dataId,
+                'error_message' => $ex->getMessage(),
+                'error_trace' => $ex->getTraceAsString()
+            ]);
+
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+    public function cancelCard()
+    {
+        $this->addCard = false;
+        $this->updateCard = false;
+        $this->resetCardFields();
+    }
+
+    public function deleteCard($id)
+    {
+        try {
+            \App\Models\MemberCard::find($id)->delete();
+            session()->flash('success', "Tesserato eliminato");
+        } catch (\Exception $e) {
+            session()->flash('error', 'Errore (' . $e->getMessage() . ')');
+        }
+    }
+
+    public function addCourse()
+    {
+        $this->resetCourseFields();
+        $this->addCourse = true;
+        $this->updateCourse = false;
+        $this->emit('setEditCorso', true);
+    }
+
+    public function storeCourse()
+    {
+
+        $this->validate(['course_course_id' => 'required']);
+        try {
+
+            $mc = new \App\Models\MemberCourse();
+            $mc->member_id = $this->dataId;
+            $mc->course_id = $this->course_course_id;
+            $mc->date_from = $this->course_date_from;
+            $mc->date_to = $this->course_date_to;
+            $mc->course_subscription_id = $this->course_course_subscription_id;
+            $mc->status = $this->course_status;
+            $mc->subscribed = false;
+            $mc->price = currencyToDouble($this->course_price);
+            $mc->subscription_price = currencyToDouble($this->course_subscription_price);
+            $mc->notes = $this->course_note;
+            $mc->months = json_encode($this->course_months);
+            $mc->when = json_encode($this->course_when);
+            $mc->save();
+
+            // Se il corso ha associato una categoria iscrivo anche al gruppo
+            $c = \App\Models\Course::findOrFail($this->course_course_id);
+            if ($c) {
+                if ($c->category_id > 0) {
+                    \App\Models\MemberCategory::create([
+                        'member_id' => $this->dataId,
+                        'category_id' => $c->category_id,
+                        'date' => \Carbon\Carbon::now()
+                    ]);
+                }
+            }
+
+            // Creo i pagamenti in base alla tipologia
+            $r = \App\Models\CourseSubscription::findOrFail($this->course_course_subscription_id);
+
+            $start = $this->course_date_from;
+
+            // Creo il pagamento per l'iscrizione
+            $rate = new \App\Models\Rate();
+            $rate->member_id = $this->dataId;
+            $rate->member_course_id = $mc->id;
+            $rate->course_subscription_id = null;
+            $rate->price = currencyToDouble($this->course_subscription_price);
+            $rate->date = $start;
+            $rate->months = json_encode([]);
+            $rate->note = '';
+            $rate->status = 0;
+            $rate->is_subscription = true;
+            $rate->save();
+            $go = true;
+            while ($go) {
+
+                $mms = [];
+                $mms[] = date("n", strtotime($start));
+                for ($jj = 1; $jj < $r->months; $jj++) {
+                    if (date('Ymd', strtotime("+" . $jj . " months", strtotime($start))) < date("Ymd", strtotime($this->course_date_to)))
+                    {
+                        $mms[] = date('n', strtotime("+" . $jj . " months", strtotime($start)));
+                    }
+                    else
+                    {
+                        break;
+                    }
+                }
+
+                $rate = new \App\Models\Rate();
+                $rate->member_id = $this->dataId;
+                $rate->member_course_id = $mc->id;
+                $rate->course_subscription_id = $this->course_course_subscription_id;
+                $rate->price = currencyToDouble($this->course_price);
+                $rate->date = $start;
+                $rate->months = json_encode($mms);
+                $rate->note = '';
+                $rate->status = 0;
+                $rate->save();
+
+                $start = date('Y-m-d', strtotime("+" . $r->months . " months", strtotime($start)));
+
+                if ($start > $this->course_date_to) {
+                    $go = false;
+                    break;
+                }
+            }
+
+            session()->flash('success, Corso creato');
+            $this->resetCourseFields();
+            $this->addCourse = false;
+            $this->emit('setEditCorso', false);
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function editCourse($id)
+    {
+        try {
+
+            $memberCourse = \App\Models\MemberCourse::findOrFail($id);
+            if (!$memberCourse) {
+                session()->flash('error', 'Corso non trovato');
+            } else {
+                $this->course_course_id = $memberCourse->course_id;
+
+                // Carico i dati relativi alla struttura ad albero
+                $c = \App\Models\Course::findOrFail($this->course_course_id);
+                $this->course_name = $c->name . " (" . $c->year . ")";
+                $this->course_level_id = $c->course_level_id;
+                $this->course_course_type = $c->type;
+                $this->course_type_id = $c->course_type_id;
+                $this->course_frequency_id = $c->course_frequency_id;
+
+                $all = \App\Models\Course::where('name', 'like', '%' . $c->name . "%")->where('enabled', true)->get();
+                foreach ($all as $a) {
+                    $levels_ids[] = $a->course_level_id;
+                }
+                $this->course_levels = \App\Models\CourseLevel::select('*')->where('enabled', true)->whereIn('id', $levels_ids)->get();
+
+                $all = \App\Models\Course::where('name', 'like', '%' . $c->name . "%")->where('enabled', true)->where('course_level_id', $this->course_level_id)->get();
+                foreach ($all as $a) {
+                    $types_ids[] = $a->course_type_id;
+                }
+                $this->course_types = \App\Models\CourseType::select('*')->where('enabled', true)->whereIn('id', $types_ids)->get();
+
+                $frequencies_ids = [];
+                $all = \App\Models\Course::where('name', 'like', '%' . $c->name . "%")->where('enabled', true)->where('course_level_id', $this->course_level_id)->where('course_type_id', $this->course_type_id)->get();
+                foreach ($all as $a) {
+                    $frequencies_ids[] = $a->course_frequency_id;
+                }
+                $this->course_frequencies = \App\Models\CourseFrequency::select('*')->where('enabled', true)->whereIn('id', $frequencies_ids)->get();
+
+                $this->course_when = array();
+                $this->course_date_from = $memberCourse->date_from;
+                $this->course_date_to = $memberCourse->date_to;
+                foreach (json_decode($memberCourse->when) as $z) {
+                    $this->course_when[] = array("day" => $z->day, "from" => $z->from, "to" => $z->to);
+                }
+                //$this->course_when = json_decode($memberCourse->when);
+                $this->course_course_subscription_id = $memberCourse->course_subscription_id;
+                $this->course_status = $memberCourse->status;
+                $this->course_subscribed = $memberCourse->subscribed == 1 ? true : false;
+                $this->course_price = formatPrice($memberCourse->price);
+                $this->course_subscription_price = formatPrice($memberCourse->subscription_price);
+                $this->course_note = $memberCourse->notes;
+                foreach (json_decode($memberCourse->months) as $z) {
+                    $this->course_months[] = array("m" => $z->m, "status" => $z->status);
+                }
+
+                $course_subscription_ids = [];
+                $this->course_price_list = [];
+                if ($c->prices != null) {
+                    foreach (json_decode($c->prices) as $z) {
+                        $this->course_price_list[$z->course_subscription_id] = $z->price;
+                        if ($z->price > 0)
+                            $course_subscription_ids[] = $z->course_subscription_id;
+                    }
+                }
+
+                $this->course_subscriptions = \App\Models\CourseSubscription::select('*')->whereIn('id', $course_subscription_ids)->where('enabled', true)->get();
+                
+                $this->courseDataId = $memberCourse->id;
+                $this->updateCourse = true;
+                $this->addCourse = false;
+                $this->emit('setEditCorso', true);
+            }
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function updateCourse()
+    {
+        //$this->validate();
+        $this->validate(['course_course_id' => 'required']);
+        try {
+
+
+            \App\Models\MemberCourse::whereId($this->courseDataId)->update([
+                'member_id' => $this->dataId,
+                'course_id' => $this->course_course_id,
+                'date_from' => $this->course_date_from,
+                'date_to' => $this->course_date_to,
+                'course_subscription_id' => $this->course_course_subscription_id,
+                'status' => $this->course_status,
+                'price' => currencyToDouble($this->course_price),
+                'subscription_price' => currencyToDouble($this->course_subscription_price),
+                'notes' => $this->course_note,
+                'months' => json_encode($this->course_months),
+                'when' => json_encode($this->course_when)
+            ]);
+            session()->flash('success', 'Corso aggiornato');
+            $this->resetCourseFields();
+            $this->updateCourse = false;
+            $this->emit('setEditCorso', false);
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function cancelCourse()
+    {
+        $this->addCourse = false;
+        $this->updateCourse = false;
+        $this->emit('setEditCorso', false);
+        $this->resetCourseFields();
+    }
+
+    public function deleteCourse($id)
+    {
+        try {
+            \App\Models\MemberCourse::find($id)->delete();
+            session()->flash('success', "Corso eliminato");
+        } catch (\Exception $e) {
+            session()->flash('error', 'Errore (' . $e->getMessage() . ')');
+        }
+    }
+
+    public function addRow()
+    {
+        $this->course_when[] = array('day' => array(), 'from' => '', 'to' => '');
+    }
+
+    public function delRow($idx)
+    {
+        unset($this->course_when[$idx]);
+    }
+
+    // Certificates
+
+    public function addCertificate()
+    {
+        $this->resetCertificateFields();
+        $this->addCertificate = true;
+        $this->updateCertificate = false;
+    }
+
+    public function storeCertificate()
+    {
+        $this->validate(['certificate_expire_date' => 'required']);
+
+        try {
+            $certificatePath = '';
+            if ($this->certificate_filename) {
+                $certificatePath = $this->fileService->uploadCertificate($this->certificate_filename, $this->dataId);
+            }
+
+            if ($this->dataId > -1) {
+                \App\Models\MemberCertificate::create([
+                    'member_id' => $this->dataId,
+                    'type' => $this->certificate_type,
+                    'filename' => $certificatePath,
+                    'expire_date' => $this->certificate_expire_date,
+                    'status' => $this->certificate_status
+                ]);
+                updateMemberData($this->dataId);
+            }
+
+            session()->flash('success', 'Certificato creato');
+            $this->resetCertificateFields();
+            $this->addCertificate = false;
+            $this->loadMemberCertificates();
+            // TODO $this->checkCourseAvailability();
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function editCertificate($id)
+    {
+        try {
+            $memberCertificate = \App\Models\MemberCertificate::findOrFail($id);
+            if (!$memberCertificate) {
+                session()->flash('error', 'Tesserato non trovato');
+            } else {
+                $this->certificate_type = $memberCertificate->type;
+                $this->certificate_filename_old = $memberCertificate->filename;
+                $this->certificate_expire_date = $memberCertificate->expire_date;
+                $this->certificate_status = $memberCertificate->status;
+                $this->cardCertificateId = $memberCertificate->id;
+                $this->updateCertificate = true;
+                $this->addCertificate = false;
+            }
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function updateCertificate()
+    {
+        $this->validate(['certificate_expire_date' => 'required']);
+
+        try {
+            $certificatePath = $this->certificate_filename_old; // Keep existing if no new file
+            if ($this->certificate_filename) {
+                $certificatePath = $this->fileService->uploadCertificate($this->certificate_filename, $this->dataId);
+            }
+
+            \App\Models\MemberCertificate::whereId($this->cardCertificateId)->update([
+                'member_id' => $this->dataId,
+                'type' => $this->certificate_type,
+                'filename' => $certificatePath,
+                'expire_date' => $this->certificate_expire_date,
+                'status' => $this->certificate_status
+            ]);
+
+            updateMemberData($this->dataId);
+            session()->flash('success', 'Certificato aggiornato');
+            $this->resetCertificateFields();
+            $this->updateCertificate = false;
+            $this->loadMemberCertificates();
+            // TODO $this->checkCourseAvailability();
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function getFileUrl($filePath)
+    {
+        if (!$filePath) {
+            return null;
+        }
+
+        return $this->fileService->getFileUrl($filePath);
+    }
+
+    public function cancelCertificate()
+    {
+        $this->addCertificate = false;
+        $this->updateCertificate = false;
+        $this->resetCertificateFields();
+    }
+
+    public function deleteCertificate($id)
+    {
+        try {
+            \App\Models\MemberCertificate::find($id)->delete();
+            session()->flash('success', "Tesserato eliminato");
+        } catch (\Exception $e) {
+            session()->flash('error', 'Errore (' . $e->getMessage() . ')');
+        }
+    }
+
+    // Gruppi di appartenenza
+
+    public function storeCategory()
+    {
+
+        $this->validate(['category_category_id' => 'required']);
+        try {
+            \App\Models\MemberCategory::create([
+                'member_id' => $this->dataId,
+                'category_id' => $this->category_category_id,
+                'date' => \Carbon\Carbon::now()
+            ]);
+            session()->flash('success, Associazione creato');
+            $this->resetCategoryFields();
+            $this->addCard = false;
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function hideMsg()
+    {
+        $this->groupMsg = '';
+    }
+
+    public function storeCategoryWithID($id)
+    {
+        $this->groupMsg = '';
+        try {
+            if (\App\Models\MemberCategory::where('member_id', $this->dataId)->where('category_id', $id)->first()) {
+                $this->groupMsg = '<br>Attenzione, questo corso è stato già inserito';
+            } else {
+                \App\Models\MemberCategory::create([
+                    'member_id' => $this->dataId,
+                    'category_id' => $id,
+                    'date' => \Carbon\Carbon::now()
+                ]);
+                session()->flash('success, Associazione creato');
+                $this->resetCategoryFields();
+                $this->addCard = false;
+            }
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function deleteCategory($id)
+    {
+        try {
+            \App\Models\MemberCategory::find($id)->delete();
+            session()->flash('success', "Associazione eliminata");
+        } catch (\Exception $e) {
+            session()->flash('error', 'Errore (' . $e->getMessage() . ')');
+        }
+    }
+
+    public function setCourse($id)
+    {
+        $this->course_course_id = $id;
+    }
+
+    public function getNation($nation)
+    {
+        if ($nation > 0) {
+            $ret = \App\Models\Nation::findOrFail($nation);
+            return $ret->name;
+        }
+        return "";
+    }
+
+    public function getProvince($province)
+    {
+        if ($province > 0) {
+            $ret = \App\Models\Province::findOrFail($province);
+            return $ret->name;
+        }
+        return "";
+    }
+
+    public function getCity($city)
+    {
+        if ($city > 0) {
+            $ret = \App\Models\City::findOrFail($city);
+            return $ret->name;
+        }
+        return "";
+    }
+
+    public function getFiscalCode()
+    {
+
+        $this->error_fc = false;
+
+        $cf = new codicefiscale();
+        $cf->setDateSeparator('-');
+
+        if ($this->first_name != '' && $this->last_name != '' && $this->birth_date != '' && $this->gender != '' && $this->birth_city_id > 0) {
+
+            $code = '';
+            if ($this->birth_city_id > 0) {
+                $code = \App\Models\City::findOrFail($this->birth_city_id)->code;
+            }
+
+            $codice = $cf->calcola($this->first_name, $this->last_name, $this->birth_date, $this->gender, $code);
+
+            $this->fiscal_code = $codice;
+        } else
+            $this->error_fc = true;
+    }
+
+    public function setMonth($m)
+    {
+
+        $exist = -1;
+        foreach ($this->course_months as $idx => $x) {
+            if ($x["m"] == $m) {
+                $exist = $idx;
+                break;
+            }
+        }
+        if ($exist > -1) {
+            array_splice($this->course_months, $exist, 1);
+        } else {
+            $this->course_months[] = array("m" => $m, "status" => "");
+        }
+    }
+
+    public function newPayment($course)
+    {
+        $c = \App\Models\Course::findOrFail($course["course_id"]);
+
+        $price = $course["price"];
+
+        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) {
+
+                if (in_array($month, json_decode($record->months))) {
+
+                    foreach ($record->rows as $row) {
+
+
+                        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) {
+                                $price -= $row->amount / $tot;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        $this->emit('setEdit', false);
+        $this->emit('setEditCorso', false);
+        return redirect()->to('/in?new=1&memberId=' . $this->dataId . '&causalId=' . $c->causal_id . '&subCausalId=' . $c->sub_causal_id . '&createSubscription=' . $this->createSubscription .  (sizeof($this->payMonths) > 0 ? '&months=' . implode("|", $this->payMonths) : "") . '&price=' . $price . '&subscription_price=' . $course["subscription_price"] . "&courseId=" . $course["id"]);
+    }
+
+    public function block($course)
+    {
+        $xxx = json_decode($course["months"]);
+        foreach ($xxx as $idx => $mm) {
+            if (in_array($mm->m, $this->payMonths)) {
+                $xxx[$idx]->status = 2;
+            }
+        }
+        $c = \App\Models\MemberCourse::findOrFail($course["id"]);
+        $c->months = json_encode($xxx);
+        $c->save();
+        $this->payMonths = array();
+    }
+
+    public function reactivate($course)
+    {
+        $xxx = json_decode($course["months"]);
+        foreach ($xxx as $idx => $mm) {
+            if (in_array($mm->m, $this->suspendedMonths)) {
+                $xxx[$idx]->status = "";
+            }
+        }
+        $c = \App\Models\MemberCourse::findOrFail($course["id"]);
+        $c->months = json_encode($xxx);
+        $c->save();
+        $this->suspendedMonths = array();
+    }
+
+    public function setPayMonth($m, $months, $selectedCourseMember)
+    {
+
+        if ($this->selectedCourseMember != $selectedCourseMember)
+            $this->payMonths = array();
+        $this->selectedCourseMember = $selectedCourseMember;
+
+        if (in_array($m, $this->payMonths)) {
+            $i = array_search($m, $this->payMonths);
+            array_splice($this->payMonths, $i, 1);
+        } else if (in_array($m, $this->suspendedMonths)) {
+            $i = array_search($m, $this->suspendedMonths);
+            array_splice($this->suspendedMonths, $i, 1);
+        } else {
+            foreach (json_decode($months) as $mm) {
+                if ($mm->m == $m) {
+                    if ($mm->status == "")
+                        $this->payMonths[] = $m;
+                    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')) {
+                                        $tot = sizeof(json_decode($row->when));
+                                        $payed += $row->amount / $tot;
+                                    }
+                                }
+                                if ($payed < $price)
+                                    $this->payMonths[] = $m;
+                            }
+                        }
+                    }
+
+                    if ($mm->status == "2")
+                        $this->suspendedMonths[] = $m;
+                }
+            }
+        }
+        if (sizeof($this->payMonths) > 0 && sizeof($this->suspendedMonths) > 0) {
+            $this->payMonths = array();
+            $this->suspendedMonths = array();
+        }
+    }
+
+    public function setCreateSubscription($subscribed, $selectedCourseMember)
+    {
+
+        if ($this->selectedCourseMember != $selectedCourseMember)
+            $this->payMonths = array();
+        $this->selectedCourseMember = $selectedCourseMember;
+
+        if (!$subscribed)
+            $this->createSubscription = !$this->createSubscription;
+    }
+
+    public function showHideCourse($id)
+    {
+        if (in_array($id, $this->showCourse)) {
+            $i = array_search($id, $this->showCourse);
+            array_splice($this->showCourse, $i, 1);
+        } else {
+            $this->showCourse[] = $id;
+        }
+    }
+
+    public function getMonthStatus($m, $months, $selectedCourseMember)
+    {
+
+        $class = "grey";
+
+        foreach (json_decode($months) as $mm) {
+            if ($mm->m == $m) {
+                if ($mm->status == "") {
+                    if (in_array($m, $this->payMonths) && $this->selectedCourseMember == $selectedCourseMember)
+                        $class = "blue";
+                    else
+                        $class = "orange";
+                }
+                if ($mm->status == "1") {
+                    $class = "green";
+                }
+                if ($mm->status == "2") {
+                    if (in_array($m, $this->suspendedMonths) && $this->selectedCourseMember == $selectedCourseMember)
+                        $class = "blue";
+                    else
+                        $class = "yellow";
+                }
+
+                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')) {
+                                    $tot = sizeof(json_decode($row->when));
+                                    $payed += $row->amount / $tot;
+                                }
+                            }
+                        }
+                    }
+                    if ($payed < $price)
+                        $class = 'orange half';
+                }
+            }
+        }
+
+        return $class;
+    }
+
+    public function getSubscriptionStatus($subscribed, $selectedCourseMember)
+    {
+
+        $class = "grey";
+
+        if ($this->createSubscription && $this->selectedCourseMember == $selectedCourseMember)
+            $class = "blue";
+        else
+            $class = "orange";
+        if ($subscribed)
+            $class = "green";
+
+        return $class;
+    }
+
+    public function checkMonth($m)
+    {
+        $ret = false;
+        foreach ($this->course_months as $idx => $x) {
+            if ($x["m"] == $m) {
+                $ret = true;
+                break;
+            }
+        }
+        return $ret;
+    }
+
+    public function setDay($idx, $d)
+    {
+
+        if (in_array($d, $this->course_when[$idx]["day"])) {
+            $i = array_search($d, $this->course_when[$idx]["day"]);
+            array_splice($this->course_when[$idx]["day"], $i, 1);
+        } else {
+            $this->course_when[$idx]["day"][] = $d;
+        }
+    }
+
+    public function updateBorsellino($importoBorsellino, $s)
+    {
+        if ($importoBorsellino > 0) {
+            $imp = $importoBorsellino;
+            if ($s == "-")
+                $imp = $imp * -1;
+            $money = new \App\Models\Money();
+            $money->member_id = $this->currentMember->id;
+            $money->record_id = null;
+            $money->amount = $imp;
+            $money->date = date("Y-m-d");
+            $money->note = 'Forzato ' . $imp;
+            $money->save();
+        }
+        $this->emit('saved');
+    }
+
+    public function getMonth($m)
+    {
+        $ret = '';
+        switch ($m) {
+            case 1:
+                $ret = 'Gennaio';
+                break;
+            case 2:
+                $ret = 'Febbraio';
+                break;
+            case 3:
+                $ret = 'Marzo';
+                break;
+            case 4:
+                $ret = 'Aprile';
+                break;
+            case 5:
+                $ret = 'Maggio';
+                break;
+            case 6:
+                $ret = 'Giugno';
+                break;
+            case 7:
+                $ret = 'Luglio';
+                break;
+            case 8:
+                $ret = 'Agosto';
+                break;
+            case 9:
+                $ret = 'Settembre';
+                break;
+            case 10:
+                $ret = 'Ottobre';
+                break;
+            case 11:
+                $ret = 'Novembre';
+                break;
+            case 12:
+                $ret = 'Dicembre';
+                break;
+            default:
+                $ret = '';
+                break;
+        }
+        return $ret;
+    }
+    public function archive($id)
+    {
+        try {
+            $member = \App\Models\Member::findOrFail($id);
+
+            $originalFirstName = $member->first_name;
+            $originalLastName = $member->last_name;
+
+            $member->update([
+                'is_archived' => true,
+                'archived_date' => now(),
+
+                'first_name' => 'Nome Archiviato',
+                'last_name' => 'Cognome Archiviato',
+
+                'fiscal_code' => null,
+                'email' => null,
+                'phone' => null,
+                'phone2' => null,
+                'phone3' => null,
+                'address' => null,
+                'zip_code' => null,
+
+                'birth_place' => null,
+                'birth_date' => null,
+                'birth_city_id' => null,
+                'birth_province_id' => null,
+                'birth_nation_id' => null,
+
+                'nation_id' => null,
+                'province_id' => null,
+                'city_id' => null,
+
+                'document_type' => null,
+                'document_number' => null,
+                'document_from' => null,
+                'document_expire_date' => null,
+                'document_files' => null,
+
+                'father_name' => null,
+                'mother_name' => null,
+                'father_email' => null,
+                'mother_email' => null,
+                'father_phone' => null,
+                'mother_phone' => null,
+                'father_fiscal_code' => null,
+                'mother_fiscal_code' => null,
+                'father_doc_number' => null,
+                'father_doc_type' => null,
+                'mother_doc_number' => null,
+                'mother_doc_type' => null,
+                'father_document_files' => null,
+                'mother_document_files' => null,
+
+                'image' => null,
+
+                'gender' => null,
+                'status' => 'archived',
+
+                'enabled' => false
+            ]);
+
+            updateMemberData($id);
+
+            session()->flash('success', 'Membro archiviato con successo');
+
+            $this->emit('reload');
+
+        } catch (\Exception $e) {
+            session()->flash('error', 'Errore durante l\'archiviazione: ' . $e->getMessage());
+            Log::error('Archive member error', [
+                'member_id' => $id,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+        }
+    }
+}
+
+class codicefiscale
+{
+
+    /**
+     * Array delle consonanti
+     */
+    protected $_consonanti = array(
+        'B',
+        'C',
+        'D',
+        'F',
+        'G',
+        'H',
+        'J',
+        'K',
+        'L',
+        'M',
+        'N',
+        'P',
+        'Q',
+        'R',
+        'S',
+        'T',
+        'V',
+        'W',
+        'X',
+        'Y',
+        'Z'
+    );
+
+    /**
+     * Array delle vocali
+     */
+    protected $_vocali = array(
+        'A',
+        'E',
+        'I',
+        'O',
+        'U'
+    );
+
+    /**
+     * Array per il calcolo della lettera del mese
+     * Al numero del mese corrisponde una lettera
+     */
+    protected $_mesi = array(
+        1  => 'A',
+        2 => 'B',
+        3 => 'C',
+        4 => 'D',
+        5 => 'E',
+        6  => 'H',
+        7 => 'L',
+        8 => 'M',
+        9 => 'P',
+        10 => 'R',
+        11 => 'S',
+        12 => 'T'
+    );
+
+
+    protected $_pari = array(
+        '0' =>  0,
+        '1' =>  1,
+        '2' =>  2,
+        '3' =>  3,
+        '4' =>  4,
+        '5' =>  5,
+        '6' =>  6,
+        '7' =>  7,
+        '8' =>  8,
+        '9' =>  9,
+        'A' =>  0,
+        'B' =>  1,
+        'C' =>  2,
+        'D' =>  3,
+        'E' =>  4,
+        'F' =>  5,
+        'G' =>  6,
+        'H' =>  7,
+        'I' =>  8,
+        'J' =>  9,
+        'K' => 10,
+        'L' => 11,
+        'M' => 12,
+        'N' => 13,
+        'O' => 14,
+        'P' => 15,
+        'Q' => 16,
+        'R' => 17,
+        'S' => 18,
+        'T' => 19,
+        'U' => 20,
+        'V' => 21,
+        'W' => 22,
+        'X' => 23,
+        'Y' => 24,
+        'Z' => 25
+    );
+
+    protected $_dispari = array(
+        '0' =>  1,
+        '1' =>  0,
+        '2' =>  5,
+        '3' =>  7,
+        '4' =>  9,
+        '5' => 13,
+        '6' => 15,
+        '7' => 17,
+        '8' => 19,
+        '9' => 21,
+        'A' =>  1,
+        'B' =>  0,
+        'C' =>  5,
+        'D' =>  7,
+        'E' =>  9,
+        'F' => 13,
+        'G' => 15,
+        'H' => 17,
+        'I' => 19,
+        'J' => 21,
+        'K' =>  2,
+        'L' =>  4,
+        'M' => 18,
+        'N' => 20,
+        'O' => 11,
+        'P' =>  3,
+        'Q' =>  6,
+        'R' =>  8,
+        'S' => 12,
+        'T' => 14,
+        'U' => 16,
+        'V' => 10,
+        'W' => 22,
+        'X' => 25,
+        'Y' => 24,
+        'Z' => 23
+    );
+
+    protected $_controllo = array(
+        '0'  => 'A',
+        '1'  => 'B',
+        '2'  => 'C',
+        '3'  => 'D',
+        '4'  => 'E',
+        '5'  => 'F',
+        '6'  => 'G',
+        '7'  => 'H',
+        '8'  => 'I',
+        '9'  => 'J',
+        '10' => 'K',
+        '11' => 'L',
+        '12' => 'M',
+        '13' => 'N',
+        '14' => 'O',
+        '15' => 'P',
+        '16' => 'Q',
+        '17' => 'R',
+        '18' => 'S',
+        '19' => 'T',
+        '20' => 'U',
+        '21' => 'V',
+        '22' => 'W',
+        '23' => 'X',
+        '24' => 'Y',
+        '25' => 'Z'
+    );
+
+    /**
+     * Stringa di errore
+     */
+    protected $_error = null;
+
+    /**
+     * Separatore per la data di nascita
+     */
+    protected $_dateSeparator = '/';
+
+
+    /**
+     * Percorso del file del database SQLite
+     * dei codici catastali
+     */
+    protected $_dbCatastali = null;
+
+
+    /**
+     * Trasforma la stringa passata in un array di lettere
+     * e lo incrocia con un ulteriore array
+     */
+    protected function _getLettere($string, array $haystack)
+    {
+        $letters = array();
+        foreach (str_split($string) as $needle) {
+            if (in_array($needle, $haystack)) {
+                $letters[] = $needle;
+            }
+        }
+        return $letters;
+    }
+
+    /**
+     * Ritorna un array con le vocali di una data stringa
+     */
+    protected function _getVocali($string)
+    {
+        return $this->_getLettere($string, $this->_vocali);
+    }
+
+    /**
+     * Ritorna un array con le consonanti di una data stringa
+     */
+    protected function _getConsonanti($string)
+    {
+        return $this->_getLettere($string, $this->_consonanti);
+    }
+
+    /**
+     * Pulisce la stringa filtrando tutti i caratteri che
+     * non sono lettere. Lo switch $toupper se impostato a TRUE
+     * converte la stringa risultante in MAIUSCOLO.
+     */
+    protected function _sanitize($string, $toupper = true)
+    {
+        $result = preg_replace('/[^A-Za-z]*/', '', $string);
+        return ($toupper) ? strtoupper($result) : $result;
+    }
+
+    /**
+     * Se la stringa passata a funzione e' costituita
+     * da meno di 3 caratteri, rimpiazza le lettere
+     * mancanti con la lettera X.
+     */
+    protected function _addMissingX($string)
+    {
+        $code = $string;
+        while (strlen($code) < 3) {
+            $code .= 'X';
+        }
+        return $code;
+    }
+
+    /**
+     * Ottiene il codice identificativo del nome
+     */
+    protected function _calcolaNome($string)
+    {
+        $nome = $this->_sanitize($string);
+        $code = '';
+        // Se il nome inserito e' piu' corto di 3 lettere
+        // si aggiungono tante X quanti sono i caratteri
+        // mancanti.
+        if (strlen($nome) < 3) {
+            return $this->_addMissingX($nome);
+        }
+
+        $nome_cons = $this->_getConsonanti($nome);
+
+        // Se le consonanti contenute nel nome sono minori
+        // o uguali a 3 vengono considerate nell'ordine in cui
+        // compaiono.
+        if (count($nome_cons) <= 3) {
+            $code = implode('', $nome_cons);
+        } else {
+            // Se invece abbiamo almeno 4 consonanti, prendiamo
+            // la prima, la terza e la quarta.
+            for ($i = 0; $i < 4; $i++) {
+                if ($i == 1) continue;
+                if (!empty($nome_cons[$i])) {
+                    $code .= $nome_cons[$i];
+                }
+            }
+        }
+
+        // Se compaiono meno di 3 consonanti nel nome, si
+        // utilizzano le vocali, nell'ordine in cui compaiono
+        // nel nome.
+        if (strlen($code) < 3) {
+            $nome_voc = $this->_getVocali($nome);
+            while (strlen($code) < 3) {
+                $code .= array_shift($nome_voc);
+            }
+        }
+
+        return $code;
+    }
+
+    protected function _calcolaCognome($string)
+    {
+        $cognome = $this->_sanitize($string);
+        $code = '';
+        // Se il cognome inserito e' piu' corto di 3 lettere
+        // si aggiungono tante X quanti sono i caratteri
+        // mancanti.
+        if (strlen($cognome) < 3) {
+            return $this->_addMissingX($cognome);
+        }
+
+        $cognome_cons = $this->_getConsonanti($cognome);
+
+        // Per il calcolo del cognome si prendono le prime
+        // 3 consonanti.
+        for ($i = 0; $i < 3; $i++) {
+            if (array_key_exists($i, $cognome_cons)) {
+                $code .= $cognome_cons[$i];
+            }
+        }
+
+        // Se le consonanti non bastano, vengono prese
+        // le vocali nell'ordine in cui compaiono.
+        if (strlen($code) < 3) {
+            $cognome_voc = $this->_getVocali($cognome);
+            while (strlen($code) < 3) {
+                $code .= array_shift($cognome_voc);
+            }
+        }
+
+        return $code;
+    }
+
+    /**
+     * Imposta il separatore di data ( default: / )
+     */
+    public function setDateSeparator($char)
+    {
+        $this->_dateSeparator = $char;
+        return $this;
+    }
+
+    /**
+     * Ritorna la parte di codice fiscale corrispondente
+     * alla data di nascita del soggetto (Forma: AAMGG)
+     */
+    protected function _calcolaDataNascita($data, $sesso)
+    {
+        $dn = explode($this->_dateSeparator, $data);
+
+        $giorno = (int) @$dn[2];
+        $mese   = (int) @$dn[1];
+        $anno   = (int) @$dn[0];
+
+        // Le ultime due cifre dell'anno di nascita
+        $aa = substr($anno, -2);
+
+        // La lettera corrispondente al mese di nascita
+        $mm = $this->_mesi[$mese];
+
+        // Il giorno viene calcolato a seconda del sesso
+        // del soggetto di cui si calcola il codice:
+        // se e' Maschio si mette il giorno reale, se e'
+        // Femmina viene aggiungo 40 a questo numero.
+        $gg = (strtoupper($sesso) == 'M') ? $giorno : ($giorno + 40);
+
+        // Bug #1: Thanks to Luca
+        if (strlen($gg) < 2) $gg = '0' . $gg;
+
+
+        return $aa . $mm . $gg;
+    }
+
+    /**
+     * Ritorna la cifra di controllo sulla base dei
+     * 15 caratteri del codice fiscale calcolati.
+     */
+    protected function _calcolaCifraControllo($codice)
+    {
+        $code = str_split($codice);
+        $sum  = 0;
+
+        for ($i = 1; $i <= count($code); $i++) {
+            $cifra = $code[$i - 1];
+            $sum += ($i % 2) ? $this->_dispari[$cifra] : $this->_pari[$cifra];
+        }
+
+        $sum %= 26;
+
+        return $this->_controllo[$sum];
+    }
+
+    /**
+     * Imposta il messaggio di errore
+     */
+    protected function _setError($string)
+    {
+        $this->_error = $string;
+    }
+
+    /**
+     * Verifica la presenza di un errore.
+     * Ritorna TRUE se presente, FALSE altrimenti.
+     */
+    public function hasError()
+    {
+        return !is_null($this->_error);
+    }
+
+    /**
+     * Ritorna la stringa di errore
+     */
+    public function getError()
+    {
+        return $this->_error;
+    }
+
+    /**
+     * Ritorna il codice fiscale utilizzando i parametri
+     * passati a funzione. Se si verifica
+     */
+    public function calcola($nome, $cognome, $data, $sesso, $comune)
+    {
+        $codice = $this->_calcolaCognome($cognome) .
+            $this->_calcolaNome($nome) .
+            $this->_calcolaDataNascita($data, $sesso) .
+            $comune;
+
+        if ($this->hasError()) {
+            return false;
+        }
+
+        $codice .= $this->_calcolaCifraControllo($codice);
+
+        if (strlen($codice) != 16) {
+            return 'ERROR';
+        }
+
+        return $codice;
+    }
+}

+ 127 - 0
app/Http/Livewire/Nation.php

@@ -0,0 +1,127 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+
+class Nation extends Component
+{
+    public $records, $code, $name, $enabled, $isItaly, $dataId, $update = false, $add = false;
+
+    protected $rules = [
+        'name' => 'required'
+    ];
+
+    protected $messages = [
+        'name.required' => 'Il nome è obbligatorio'
+    ];
+
+    public $sortField ='name';
+    public $sortAsc = true;
+
+    public function sortBy($field)
+    {
+        if($this->sortField === $field)
+        {
+            $this->sortAsc = ! $this->sortAsc;
+        } else {
+            $this->sortAsc = true;
+        }
+
+        $this->sortField = $field;
+    }
+
+    public function resetFields(){
+        $this->name = '';
+        $this->code = '';
+        $this->enabled = true;
+        $this->isItaly = false;
+        $this->emit('load-data-table');
+    }
+
+    public function render()
+    {
+
+        $this->records = \App\Models\Nation::select('id', 'name', 'code', 'enabled')->get();//->orderBy($this->sortField, $this->sortAsc ? 'asc' : 'desc')->get();
+        return view('livewire.nation');
+    }
+
+    public function add()
+    {
+        $this->resetFields();
+        $this->add = true;
+        $this->update = false;
+    }
+
+    public function store()
+    {
+        $this->validate();
+        try {
+            \App\Models\Nation::create([
+                'name' => $this->name,
+                'code' => $this->code,
+                //'enabled' => $this->enabled,
+                //'is_italy' => $this->isItaly,
+            ]);
+            session()->flash('success','Nazione creata');
+            $this->resetFields();
+            $this->add = false;
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function edit($id){
+        try {
+            $nation = \App\Models\Nation::findOrFail($id);
+            if( !$nation) {
+                session()->flash('error','Nazione non trovata');
+            } else {
+                $this->name = $nation->name;
+                $this->code = $nation->code;
+                $this->enabled = $nation->enabled;
+                $this->isItaly = $nation->is_italy;
+                $this->dataId = $nation->id;
+                $this->update = true;
+                $this->add = false;
+            }
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function update()
+    {
+        $this->validate();
+        try {
+            \App\Models\Nation::whereId($this->dataId)->update([
+                'name' => $this->name,
+                'code' => $this->code,
+                'enabled' => $this->enabled,
+                'is_italy' => $this->isItaly,
+            ]);
+            session()->flash('success','Nazione aggiornata');
+            $this->resetFields();
+            $this->update = false;
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function cancel()
+    {
+        $this->add = false;
+        $this->update = false;
+        $this->resetFields();
+    }
+
+    public function delete($id)
+    {
+        try{
+            \App\Models\Nation::find($id)->delete();
+            session()->flash('success',"Nazione eliminata");
+        }catch(\Exception $e){
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+}

+ 199 - 0
app/Http/Livewire/PaymentMethod.php

@@ -0,0 +1,199 @@
+<?php
+
+namespace App\Http\Livewire;
+use Livewire\Component;
+use Illuminate\Support\Facades\Auth;
+use App\Http\Middleware\TenantMiddleware;
+
+class PaymentMethod extends Component
+{
+    public $records, $name, $enabled, $money, $type, $corrispettivo_fiscale, $dataId, $bank_id, $update = false, $add = false;
+    public $paymentMethods = [];
+    public $banks = array();
+    public $showHidden = false;
+
+    protected $rules = [
+        'name' => 'required'
+    ];
+
+    protected $messages = [
+        'name.required' => 'Il nome è obbligatorio'
+    ];
+
+    public $sortField ='name';
+    public $sortAsc = true;
+
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+    }
+
+    public function sortBy($field)
+    {
+        if($this->sortField === $field) {
+            $this->sortAsc = ! $this->sortAsc;
+        } else {
+            $this->sortAsc = true;
+        }
+
+        $this->sortField = $field;
+    }
+
+    public function resetFields(){
+        $this->name = '';
+        $this->money = false;
+        $this->type = 'ALL';
+        $this->corrispettivo_fiscale = false;
+        $this->enabled = true;
+        $this->emit('load-data-table');
+    }
+
+    public function mount(){
+        if(Auth::user()->level != env('LEVEL_ADMIN', 0))
+            return redirect()->to('/dashboard');
+
+        $this->banks = \App\Models\Bank::select('id', 'name')->get();
+
+        $this->loadPaymentMethodOptions();
+    }
+
+    public function hide($id)
+    {
+        try {
+            \App\Models\PaymentMethod::whereId($id)->update(['hidden' => true]);
+            session()->flash('success', 'Metodo pagamento nascosto');
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function show($id)
+    {
+        try {
+            \App\Models\PaymentMethod::whereId($id)->update(['hidden' => false]);
+            session()->flash('success', 'Metodo pagamento ripristinato');
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function toggleShowHidden()
+    {
+        $this->showHidden = !$this->showHidden;
+    }
+
+    public function render()
+    {
+        if ($this->showHidden) {
+            $this->records = \App\Models\PaymentMethod::all();
+        } else {
+            $this->records = \App\Models\PaymentMethod::where(function ($query) {
+                $query->where('hidden', false)->orWhereNull('hidden');
+            })->get();
+        }
+
+        foreach($this->records as $r) {
+            $r->bank = $r->bank ? $r->bank->name : '';
+        }
+
+        return view('livewire.payment_method');
+    }
+
+    public function add()
+    {
+        $this->resetFields();
+        $this->add = true;
+        $this->update = false;
+    }
+
+    public function store()
+    {
+        $this->validate();
+        try {
+            \App\Models\PaymentMethod::create([
+                'name' => $this->name,
+                'bank_id' => $this->bank_id,
+                'money' => $this->money,
+                'type' => $this->type,
+                'corrispettivo_fiscale' => $this->corrispettivo_fiscale,
+                'enabled' => $this->enabled
+            ]);
+            session()->flash('success','Metodo pagamento creato');
+            $this->resetFields();
+            $this->add = false;
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function edit($id){
+        try {
+            $payment_method = \App\Models\PaymentMethod::findOrFail($id);
+            if( !$payment_method) {
+                session()->flash('error','Metodo pagamento non trovato');
+            } else {
+                $this->name = $payment_method->name;
+                $this->enabled = $payment_method->enabled;
+                $this->corrispettivo_fiscale = $payment_method->corrispettivo_fiscale;
+                $this->money = $payment_method->money;
+                $this->type = $payment_method->type;
+                $this->bank_id = $payment_method->bank_id;
+                $this->dataId = $payment_method->id;
+                $this->update = true;
+                $this->add = false;
+            }
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function update()
+    {
+        $this->validate();
+        try {
+            \App\Models\PaymentMethod::whereId($this->dataId)->update([
+                'name' => $this->name,
+                'bank_id' => $this->bank_id,
+                'money' => $this->money,
+                'type' => $this->type,
+                'corrispettivo_fiscale' => $this->corrispettivo_fiscale,
+                'enabled' => $this->enabled
+            ]);
+            session()->flash('success','Metodo pagamento aggiornato');
+            $this->resetFields();
+            $this->update = false;
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function cancel()
+    {
+        $this->add = false;
+        $this->update = false;
+        $this->resetFields();
+    }
+
+    protected function loadPaymentMethodOptions()
+    {
+        $this->paymentMethods = [
+            ['name' => 'Contanti', 'code' => 'MP01'],
+            ['name' => 'Assegno', 'code' => 'MP02'],
+            ['name' => 'Assegno circolare', 'code' => 'MP03'],
+            ['name' => 'Contanti presso Tesoreria', 'code' => 'MP04'],
+            ['name' => 'Bonifico', 'code' => 'MP05'],
+            ['name' => 'Vaglia cambiario', 'code' => 'MP06'],
+            ['name' => 'Bollettino bancario', 'code' => 'MP07'],
+            ['name' => 'Carta di credito', 'code' => 'MP08'],
+            ['name' => 'RID', 'code' => 'MP09'],
+            ['name' => 'RID utenze', 'code' => 'MP10'],
+            ['name' => 'RID veloce', 'code' => 'MP11'],
+            ['name' => 'Riba', 'code' => 'MP12'],
+            ['name' => 'MAV', 'code' => 'MP13'],
+            ['name' => 'Quietanza erario stato', 'code' => 'MP14'],
+            ['name' => 'Giroconto su conti di contabilità speciale', 'code' => 'MP15'],
+            ['name' => 'Domiciliazione bancaria', 'code' => 'MP16'],
+            ['name' => 'Domiciliazione postale', 'code' => 'MP17']
+        ];
+    }
+}

+ 243 - 0
app/Http/Livewire/Profile.php

@@ -0,0 +1,243 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+use Livewire\Component;
+use App\Http\Middleware\TenantMiddleware;
+use Illuminate\Support\Facades\Mail;
+
+class Profile extends Component
+{
+    public $editMode = false;
+
+    public $name;
+    public $cognome;
+    public $email;
+    public $telefono;
+    public $cellulare;
+    public $password;
+
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+    }
+
+    public function mount()
+    {
+        $currentUser = Auth::user();
+        $user = \App\Models\User::findOrFail($currentUser->id);
+        $this->name = $user->name;
+        $this->cognome = $user->cognome;
+        $this->email = $user->email;
+        $this->telefono = $user->telefono;
+        $this->cellulare = $user->cellulare;
+    }
+
+    public function enableEditMode()
+    {
+        $this->editMode = true;
+    }
+
+    public function save()
+    {
+        $this->validate([
+            'name' => 'required',
+            'cognome' => 'required',
+            'email' => 'required|email',
+            'password' => 'nullable|min:6',
+        ]);
+
+        $currentUser = Auth::user();
+
+        try {
+            // Update user in tenant database
+            $user = \App\Models\User::findOrFail($currentUser->id);
+            $oldEmail = $user->email;
+            $oldName = $user->name;
+            $passwordChanged = !empty($this->password);
+            $emailChanged = $oldEmail !== $this->email;
+            $nameChanged = $oldName !== $this->name;
+
+            $user->name = $this->name;
+            $user->cognome = $this->cognome;
+            $user->email = $this->email;
+            $user->telefono = $this->telefono;
+            $user->cellulare = $this->cellulare;
+
+            if ($passwordChanged) {
+                $user->password = Hash::make($this->password);
+            }
+
+            $user->save();
+
+            Log::info('Updated user in tenant database', [
+                'user_id' => $user->id,
+                'tenant_database' => DB::connection()->getDatabaseName(),
+                'email_changed' => $emailChanged,
+                'name_changed' => $nameChanged,
+                'password_changed' => $passwordChanged
+            ]);
+
+            // Update master database if needed
+            if ($emailChanged || $passwordChanged || $nameChanged) {
+                $masterUpdated = $this->updateMasterDatabase($currentUser, $oldEmail, $passwordChanged);
+            }
+
+            // Send password change notification if password was changed
+            if ($passwordChanged) {
+                $notificationSent = $this->sendPasswordChangeNotification($this->email, $this->name);
+
+                if ($notificationSent) {
+                    session()->flash('message', 'Profilo aggiornato con successo! Ti abbiamo inviato una email di conferma per la modifica della password.');
+                } else {
+                    session()->flash('message', 'Profilo aggiornato con successo! (Errore nell\'invio dell\'email di notifica)');
+                }
+            } else {
+                session()->flash('message', 'Profilo aggiornato con successo!');
+            }
+
+            $this->editMode = false;
+            $this->password = ''; // Clear password field
+
+        } catch (\Exception $e) {
+            Log::error('Profile update failed', [
+                'user_id' => $currentUser->id,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+
+            session()->flash('error', 'Errore durante l\'aggiornamento: ' . $e->getMessage());
+        }
+    }
+    /**
+     * Update user information in master database
+     */
+    private function updateMasterDatabase($currentUser, $oldEmail, $passwordChanged)
+    {
+        try {
+            // Store current tenant connection info
+            $currentConnection = DB::getDefaultConnection();
+            $currentDatabase = DB::connection()->getDatabaseName();
+
+            Log::info('Updating master database', [
+                'current_connection' => $currentConnection,
+                'current_database' => $currentDatabase,
+                'user_id' => $currentUser->id,
+                'old_email' => $oldEmail,
+                'new_email' => $this->email
+            ]);
+
+            $masterConfig = [
+                'driver' => 'mysql',
+                'host' => env('DB_HOST', '127.0.0.1'),
+                'port' => env('DB_PORT', '3306'),
+                'database' => env('DB_DATABASE'),
+                'username' => env('DB_USERNAME'),
+                'password' => env('DB_PASSWORD'),
+                'charset' => 'utf8mb4',
+                'collation' => 'utf8mb4_unicode_ci',
+                'prefix' => '',
+                'strict' => true,
+                'engine' => null,
+            ];
+
+            config(['database.connections.master_temp' => $masterConfig]);
+
+            $updateData = [
+                'name' => $this->name,
+                'email' => $this->email
+            ];
+
+            if ($passwordChanged) {
+                $updateData['password'] = Hash::make($this->password);
+            }
+
+            $updated = DB::connection('master_temp')
+                ->table('users')
+                ->where('email', $oldEmail)
+                ->update($updateData);
+
+            if ($updated) {
+                Log::info('Successfully updated user in master database', [
+                    'old_email' => $oldEmail,
+                    'new_email' => $this->email,
+                    'password_changed' => $passwordChanged
+                ]);
+            } else {
+                Log::warning('No user found in master database to update', [
+                    'email' => $oldEmail
+                ]);
+            }
+
+            config(['database.default' => $currentConnection]);
+            DB::purge('master_temp');
+        } catch (\Exception $e) {
+            Log::error('Failed to update master database', [
+                'error' => $e->getMessage(),
+                'user_id' => $currentUser->id,
+                'old_email' => $oldEmail,
+                'new_email' => $this->email
+            ]);
+        }
+    }
+
+    public function cancel()
+    {
+        return redirect()->to('/dashboard');
+
+        // $this->editMode = false;
+        // $this->password = '';
+
+        // $this->mount();
+    }
+
+    private function resetInputFields()
+    {
+        $this->name = '';
+        $this->cognome = '';
+        $this->email = '';
+        $this->telefono = '';
+        $this->cellulare = '';
+        $this->password = '';
+    }
+
+    public function render()
+    {
+        return view('livewire.profile');
+    }
+
+    private function sendPasswordChangeNotification($email, $name)
+    {
+        try {
+            $emailData = [
+                'name' => $name,
+                'email' => $email,
+                'change_time' => now()->format('d/m/Y H:i'),
+                'ip_address' => request()->ip()
+            ];
+
+            Mail::send('emails.password-changed', $emailData, function ($message) use ($email, $name) {
+                $message->to($email, $name)
+                    ->subject('La tua password è stata modificata')
+                    ->from(config('mail.from.address'), config('mail.from.name'));
+            });
+
+            Log::info('Password change notification sent from profile', [
+                'email' => $email,
+                'name' => $name
+            ]);
+
+            return true;
+        } catch (\Exception $e) {
+            Log::error('Failed to send password change notification from profile', [
+                'email' => $email,
+                'error' => $e->getMessage()
+            ]);
+            return false;
+        }
+    }
+}

+ 142 - 0
app/Http/Livewire/Province.php

@@ -0,0 +1,142 @@
+<?php
+
+namespace App\Http\Livewire;
+use Livewire\Component;
+
+class Province extends Component
+{
+    public $records, $name, $code, $enabled, $dataId, $nation_id, $update = false, $add = false;
+
+    public $nations = array();
+
+    public $sortField ='name';
+
+    public $sortAsc = true;
+    public function sortBy($field)
+    {
+        if($this->sortField === $field)
+        {
+            $this->sortAsc = ! $this->sortAsc;
+        } else {
+            $this->sortAsc = true;
+        }
+
+        $this->sortField = $field;
+    }
+
+    protected $rules = [
+        'name' => 'required',
+        'nation_id' => 'required'
+    ];
+
+    protected $messages = [
+        'name.required' => 'Il nome è obbligatorio'
+    ];
+
+    public function resetFields(){
+        $this->name = '';
+        $this->code = '';
+        $this->nation_id = null;
+        $this->enabled = true;
+        $this->emit('load-data-table');
+    }
+
+    public function mount()
+    {
+        $this->nations = \App\Models\Nation::select('id', 'name')->get();
+    }
+
+    public function render()
+    {
+        $this->records = \App\Models\Province::with('nation')->get();
+        foreach($this->records as $r)
+        {
+            $r->nation = $r->nation->name;
+        }
+        /*if ($this->sortAsc)
+            $this->records = $this->records->sortBy($this->sortField);
+        else
+            $this->records = $this->records->sortByDesc($this->sortField);*/
+
+        return view('livewire.province');
+    }
+
+    public function add()
+    {
+        $this->resetFields();
+        $this->add = true;
+        $this->update = false;
+    }
+
+    public function store()
+    {
+        $this->validate();
+        try {
+            \App\Models\Province::create([
+                'name' => $this->name,
+                'code' => $this->code,
+                'nation_id' => $this->nation_id,
+                'enabled' => $this->enabled
+            ]);
+            session()->flash('success','Provincia creata');
+            $this->resetFields();
+            $this->add = false;
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function edit($id){
+        try {
+            $province = \App\Models\Province::findOrFail($id);
+            if( !$province) {
+                session()->flash('error','Provincia non trovata');
+            } else {
+                $this->name = $province->name;
+                $this->code = $province->code;
+                $this->enabled = $province->enabled;
+                $this->nation_id = $province->nation_id;
+                $this->dataId = $province->id;
+                $this->update = true;
+                $this->add = false;
+            }
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function update()
+    {
+        $this->validate();
+        try {
+            \App\Models\Province::whereId($this->dataId)->update([
+                'name' => $this->name,
+                'code' => $this->code,
+                'nation_id' => $this->nation_id,
+                'enabled' => $this->enabled
+            ]);
+            session()->flash('success','Provincia aggiornata');
+            $this->resetFields();
+            $this->update = false;
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function cancel()
+    {
+        $this->add = false;
+        $this->update = false;
+        $this->resetFields();
+    }
+
+    public function delete($id)
+    {
+        try{
+            \App\Models\Province::find($id)->delete();
+            session()->flash('success',"Provincia eliminata");
+        }catch(\Exception $e){
+            session()->flash('error','Errore (' . $e->getMessage() . ')');
+        }
+    }
+}

+ 275 - 0
app/Http/Livewire/Rate.php

@@ -0,0 +1,275 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+use Barryvdh\DomPDF\Facade\Pdf;
+use App\Models\Member;
+use App\Http\Middleware\TenantMiddleware;
+use Illuminate\Support\Facades\DB;
+
+
+class Rate extends Component
+{
+    public $records;
+
+    public $member_id = 0;
+    public $member_course_id = 0;
+
+    public $mc = null;
+
+    public $filterStatus = '';
+    public $hasFilter = false;
+    public $filterFrom = '', $filterTo = '';
+    public $filteredMemberId = '';
+    public $members = [];
+    public $detail = '';
+
+    public $course_subscription_id;
+    public $price;
+    public $date;
+    public $month = '';
+    public $months = [];
+    public $disabled = [];
+    public $course_subscriptions = [];
+    public $price_list = [];
+    public $type = '';
+
+    public $errorMsg = '';
+
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+    }
+
+    protected function setupTenantConnection()
+    {
+        $user = auth()->user();
+
+        config(['database.connections.tenant' => [
+            'driver' => 'mysql',
+            'host' => '127.0.0.1',
+            'port' => '3306',
+            'database' => $user->tenant_database,
+            'username' => $user->tenant_username,
+            'password' => $user->tenant_password,
+        ]]);
+
+        config(['database.default' => 'tenant']);
+        DB::purge('tenant');
+        DB::reconnect('tenant');
+    }
+
+    public function mount()
+    {
+        // Load members for the dropdown
+        $this->member_id = isset($_GET["member_id"]) ? $_GET["member_id"] : 0;
+        $this->member_course_id = isset($_GET["member_course_id"]) ? $_GET["member_course_id"] : 0;
+        if ($this->member_id > 0 && $this->member_course_id > 0)
+        {
+            $this->mc = \App\Models\MemberCourse::findOrFail($this->member_course_id);
+            $this->detail = $this->mc->member->first_name . " " . $this->mc->member->last_name;
+
+            $this->price_list = [];
+            $c = $this->mc->course;
+            if ($c->prices != null)
+            {
+                foreach(json_decode($c->prices) as $z)
+                {
+                    $this->price_list[$z->course_subscription_id] = $z->price;
+                }
+            }
+
+            $course_subscription_ids = [];
+            if ($c->prices != null) {
+                foreach (json_decode($c->prices) as $z) {
+                    if ($z->price > 0)
+                        $course_subscription_ids[] = $z->course_subscription_id;
+                }
+            }
+
+            $this->course_subscriptions = \App\Models\CourseSubscription::select('*')->whereIn('id', $course_subscription_ids)->where('enabled', true)->get();
+        }
+
+        //$this->course_subscriptions = \App\Models\CourseSubscription::select('*')->where('enabled', true)->get();
+
+
+    }
+
+    public function render()
+    {
+
+        /*if ($this->hasFilter)
+        {
+            $r = \App\Models\Receipt::with('member');
+            if ($this->filterStatus != '')
+                $r = $r->where('status', $this->filterStatus);
+            if ($this->filterFrom != '')
+                $r = $r->where('date', '>=', $this->filterFrom);
+            if ($this->filterTo != '')
+                $r = $r->where('date', '<=', $this->filterTo);
+            if ($this->filteredMemberId != '')
+                $r = $r->where('member_id', $this->filteredMemberId);
+
+            $this->records = $r->get();
+        }
+        else
+        {*/
+            $this->records = \App\Models\Rate::with('member')->where('member_course_id', $this->member_course_id)->where('member_id', $this->member_id)->orderBy('date')->get();
+        //}
+
+        $this->disabled = [];
+        foreach($this->records as $r)
+        {
+            foreach (json_decode($r->months) as $m) {
+                $this->disabled[] = $m;
+            }
+        }
+
+        //$this->emit('load-data-table');
+
+        return view('livewire.rate');
+    }
+
+    public function updatedCourseSubscriptionId()
+    {
+        $this->price = 0;
+        if (isset($this->price_list[$this->course_subscription_id]))
+            $this->price = $this->price_list[$this->course_subscription_id];
+
+        $this->type = '';
+        if ($this->course_subscription_id > 0)
+            $this->type = \App\Models\CourseSubscription::findOrFail($this->course_subscription_id)->months;
+    }
+
+    public function add()
+    {
+
+        $this->errorMsg = '';
+        if ($this->date == '')
+        {
+            $this->errorMsg = 'La data di scadenza è obbligatoria';
+        }
+        else
+        {
+
+            if (!$this->course_subscription_id > 0)
+            {
+                $this->errorMsg = 'Devi selezionare un tipo di abbonamento';
+            }
+            else
+            {
+
+                if ($this->type > 1 && $this->type < sizeof($this->months))
+                {
+                    // $this->errorMsg = 'Hai selezionato un numero di mesi errato in base all\'abbonamento selezionato' . $this->type .'.'. sizeof($this->months);
+                    $this->errorMsg = 'Mesi selezionati superiori a quelli previsti dall’abbonamento';
+                }
+                elseif ($this->type == 1 && !$this->month) {
+                    $this->errorMsg = 'Seleziona un mese';
+                } else {
+                    $rate = new \App\Models\Rate();
+                    $rate->member_id = $this->member_id;
+                    $rate->member_course_id = $this->member_course_id;
+                    $rate->course_subscription_id = $this->course_subscription_id;
+                    $rate->price = currencyToDouble($this->price);
+                    $rate->date = $this->date;
+                    if ($this->type == '1')
+                        $this->months[] = $this->month;
+                    $rate->months = json_encode($this->months);
+                    $rate->note = '';
+                    $rate->status = 0;
+                    $rate->save();
+
+                    $this->course_subscription_id = null;
+                    $this->price = 0;
+                    $this->date = null;
+                    $this->month = '';
+                    $this->months = [];
+
+                    $this->emit('close-popup');
+                }
+            }
+
+        }
+
+    }
+
+    public function printReceipt($id)
+    {
+        $this->emit('load-data-table');
+        $receipt = \App\Models\Receipt::findOrFail($id);
+        //$pdf = PDF::loadView('pdf/receipt', array('datas' => $datas, 'from' => $x, 'to' => $y, 'who' => '', 'matricola' => $matricola));
+        $pdf = PDF::loadView('receipt', array('receipt' => $receipt));//->output();
+        return $pdf->stream('aaa.pdf');
+
+    }
+
+    public function search()
+    {
+        $this->hasFilter = true;
+    }
+
+    public function disableSearch()
+    {
+        $this->filterStatus = "";
+        $this->filterTo = '';
+        $this->filterFrom = '';
+        $this->filteredMemberId = '';
+        $this->hasFilter = false;
+    }
+
+    public function delete($id)
+    {
+        try{
+            \App\Models\Rate::find($id)->delete();
+            //$this->emit('load-data-table');
+            session()->flash('success',"Rata eliminata");
+        }catch(\Exception $e){
+            session()->flash('error','Errore (' . $e->getMessage() . ')');
+        }
+    }
+
+    public function deleteMultiple($ids)
+    {
+        try{
+            foreach($ids as $id)
+            {
+                \App\Models\Rate::find($id)->delete();
+            }
+            //$this->emit('load-data-table');
+            session()->flash('success',"Rata eliminata");
+        }catch(\Exception $e){
+            session()->flash('error','Errore (' . $e->getMessage() . ')');
+        }
+    }
+
+    public function suspendMultiple($ids)
+    {
+        try{
+            foreach($ids as $id)
+            {
+                $r = \App\Models\Rate::find($id);
+                $r->status = $r->status == 2 ? 0 : 2;
+                $r->save();
+            }
+
+            //$this->emit('load-data-table');
+            session()->flash('success',"Rata eliminata");
+        }catch(\Exception $e){
+            session()->flash('error','Errore (' . $e->getMessage() . ')');
+        }
+    }
+
+    public function addDeleteMonth($m)
+    {
+        if (!in_array($m, $this->months)) {
+            $this->months[] = $m;
+        } else {
+            if (($key = array_search($m, $this->months)) !== false) {
+                unset($this->months[$key]);
+                $this->months = array_values($this->months);
+            }
+        }
+    }
+}

+ 87 - 0
app/Http/Livewire/Receipt.php

@@ -0,0 +1,87 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+use Barryvdh\DomPDF\Facade\Pdf;
+use App\Models\Member;
+
+
+class Receipt extends Component
+{
+    public $records;
+
+    public $filterStatus = '';
+    public $hasFilter = false;
+    public $filterFrom = '', $filterTo = '';
+    public $filteredMemberId = '';
+    public $members = [];
+
+    public function mount()
+    {
+        // Load members for the dropdown
+        $this->members = Member::orderBy('last_name')->orderBy('first_name')->get();
+    }
+
+    public function updatedFilterFrom()
+    {
+        $this->emit('destroy-data-table');
+    }
+
+    public function updatedFilterTo()
+    {
+        $this->emit('destroy-data-table');
+    }
+
+    public function render()
+    {
+
+        if ($this->hasFilter)
+        {
+            $r = \App\Models\Receipt::with('member');
+            if ($this->filterStatus != '')
+                $r = $r->where('status', $this->filterStatus);
+            if ($this->filterFrom != '')
+                $r = $r->where('date', '>=', $this->filterFrom);
+            if ($this->filterTo != '')
+                $r = $r->where('date', '<=', $this->filterTo);
+            if ($this->filteredMemberId != '')
+                $r = $r->where('member_id', $this->filteredMemberId);
+
+            $this->records = $r->get();
+        }
+        else
+        {
+            $this->records = \App\Models\Receipt::with('member')->get();
+        }
+
+        $this->emit('load-data-table');
+
+        return view('livewire.receipt');
+    }
+
+    public function printReceipt($id)
+    {
+        $this->emit('load-data-table');
+        $receipt = \App\Models\Receipt::findOrFail($id);
+        //$pdf = PDF::loadView('pdf/receipt', array('datas' => $datas, 'from' => $x, 'to' => $y, 'who' => '', 'matricola' => $matricola));
+        $pdf = PDF::loadView('receipt', array('receipt' => $receipt));//->output();
+        return $pdf->stream('aaa.pdf');
+
+    }
+
+    public function search()
+    {
+        $this->hasFilter = true;
+    }
+
+    public function disableSearch()
+    {
+        $this->filterStatus = "";
+        $this->filterTo = '';
+        $this->filterFrom = '';
+        $this->filteredMemberId = '';
+        $this->hasFilter = false;
+    }
+
+}

+ 1410 - 0
app/Http/Livewire/Record.php

@@ -0,0 +1,1410 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+use DateInterval;
+use DatePeriod;
+use DateTime;
+
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Mail;
+use App\Mail\ExportNotification;
+use App\Jobs\ExportPrimaNota;
+use App\Http\Middleware\TenantMiddleware;
+
+class Record extends Component
+{
+    public $records, $dataId, $totals;
+    public $in;
+    public $out;
+    public $payments = [];
+    public $fromDate;
+    public $toDate;
+    public $appliedFromDate;
+    public $appliedToDate;
+    public $exportFromDate;
+    public $exportToDate;
+    public $isExporting = false;
+    public $selectedPeriod = 'OGGI';
+    public $filterCausals = null;
+    public $filterMember = null;
+    public $isFiltering = false;
+    public array $recordDatas = [];
+    public array $labels = [];
+    public array $causals = [];
+    public $members = array();
+    public $sendViaEmail = false;
+    public $exportEmailAddress = '';
+    public $exportEmailSubject = 'Prima Nota - Export';
+    private $causalAmounts = [];
+
+    protected $rules = [
+        'exportEmailAddress' => 'required_if:sendViaEmail,true|email',
+        'exportEmailSubject' => 'required_if:sendViaEmail,true|string|max:255',
+    ];
+
+    protected $messages = [
+        'exportEmailAddress.required_if' => 'L\'indirizzo email è obbligatorio quando si sceglie di inviare via email.',
+        'exportEmailAddress.email' => 'Inserisci un indirizzo email valido.',
+        'exportEmailSubject.required_if' => 'L\'oggetto dell\'email è obbligatorio.',
+        'exportEmailSubject.max' => 'L\'oggetto dell\'email non può superare i 255 caratteri.',
+    ];
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+
+    }
+    public function hydrate()
+    {
+        $this->emit('load-select');
+    }
+
+    public function mount()
+    {
+        $this->fromDate = date("Y-m-d");
+        $this->toDate = date("Y-m-d");
+
+        $this->appliedFromDate = date("Y-m-d");
+        $this->appliedToDate = date("Y-m-d");
+
+        $this->exportFromDate = date("Y-m-d");
+        $this->exportToDate = date("Y-m-d");
+
+        $this->exportEmailSubject = 'Prima Nota - Export del ' . date('d/m/Y');
+
+
+        $this->getCausals(\App\Models\Causal::select('id', 'name')->where('parent_id', null)->get(), 0);
+
+        $this->members = \App\Models\Member::select(['id', 'first_name', 'last_name', 'fiscal_code'])->orderBy('last_name')->orderBy('first_name')->get();
+
+        $this->payments = \App\Models\PaymentMethod::select('id', 'name', 'type')->where('enabled', true)->where('money', false)->get();
+    }
+
+    private function generateExportDataAndTotals($fromDate, $toDate)
+    {
+        Log::info('generateExportDataAndTotals: Start (combined method)', [
+            'from_date' => $fromDate,
+            'to_date' => $toDate,
+            'memory_before' => memory_get_usage(true)
+        ]);
+
+        $exportRecords = array();
+        $exportTotals = array();
+
+        Log::info('generateExportDataAndTotals: Getting excluded members');
+        $exclude_from_records = \App\Models\Member::where('exclude_from_records', true)->pluck('id')->toArray();
+        Log::info('generateExportDataAndTotals: Excluded members retrieved', ['count' => count($exclude_from_records)]);
+
+        Log::info('generateExportDataAndTotals: Building main query');
+        $datas = \App\Models\Record::with('member', 'supplier', 'payment_method')
+            ->select(
+                'records.*',
+                'records_rows.id as row_id',
+                'records_rows.record_id',
+                'records_rows.causal_id',
+                'records_rows.amount',
+                'records_rows.note',
+                'records_rows.when',
+                'records_rows.vat_id',
+                'records_rows.created_at as row_created_at',
+                'records_rows.updated_at as row_updated_at'
+            )
+            ->join('records_rows', 'records.id', '=', 'records_rows.record_id')
+            ->whereBetween('date', [$fromDate, $toDate])
+            ->where(function ($query) {
+                $query->where('type', 'OUT')
+                    ->orWhere(function ($query) {
+                        $query->where('records.corrispettivo_fiscale', true)
+                            ->orWhere('records.commercial', false);
+                    });
+            })
+            ->where(function ($query) use ($exclude_from_records) {
+                $query->where('type', 'OUT')
+                    ->orWhere(function ($subquery) use ($exclude_from_records) {
+                        $subquery->whereNotIn('member_id', $exclude_from_records);
+                    });
+            });
+
+        Log::info('generateExportDataAndTotals: Applying causal filters');
+        if ($this->filterCausals != null && sizeof($this->filterCausals) > 0) {
+            $causals = array();
+            foreach ($this->filterCausals as $z) {
+                $causals[] = $z;
+                $childs = \App\Models\Causal::where('parent_id', $z)->get();
+                foreach ($childs as $c) {
+                    $causals[] = $c->id;
+                    $childsX = \App\Models\Causal::where('parent_id', $c->id)->get();
+                    foreach ($childsX as $cX) {
+                        $causals[] = $cX->id;
+                    }
+                }
+            }
+            $datas->whereIn('causal_id', $causals);
+            Log::info('generateExportDataAndTotals: Causal filters applied', ['causal_count' => count($causals)]);
+        }
+
+        if ($this->filterMember != null && $this->filterMember > 0) {
+            $datas->where('member_id', $this->filterMember);
+            Log::info('generateExportDataAndTotals: Member filter applied', ['member_id' => $this->filterMember]);
+        }
+
+        Log::info('generateExportDataAndTotals: Executing query');
+        $queryStart = microtime(true);
+
+        $datas = $datas->orderBy('date', 'ASC')
+            ->orderBy('records.created_at', 'ASC')
+            ->orderBy('records_rows.id', 'ASC')
+            ->get();
+
+        $queryTime = microtime(true) - $queryStart;
+        Log::info('generateExportDataAndTotals: Query executed', [
+            'record_count' => $datas->count(),
+            'query_time' => $queryTime,
+            'memory_after_query' => memory_get_usage(true)
+        ]);
+
+        $groupedData = [];
+        $causalsCount = [];
+        $processedCount = 0;
+
+        // Initialize totals array
+        foreach ($this->payments as $p) {
+            $exportTotals[$p->name] = ["IN" => 0, "OUT" => 0];
+        }
+
+        Log::info('generateExportDataAndTotals: Starting combined data processing loop');
+        $loopStart = microtime(true);
+
+        foreach ($datas as $idx => $data) {
+            if ($processedCount % 100 == 0) {
+                Log::info('generateExportDataAndTotals: Processing progress', [
+                    'processed' => $processedCount,
+                    'total' => $datas->count(),
+                    'memory_current' => memory_get_usage(true),
+                    'memory_peak' => memory_get_peak_usage(true)
+                ]);
+            }
+
+            try {
+                $causalCheck = \App\Models\Causal::findOrFail($data->causal_id);
+                $paymentCheck = $data->payment_method->money;
+
+                if (!$paymentCheck && ($causalCheck->no_first == null || !$causalCheck->no_first)) {
+                    if (!$data->deleted) {
+                        $amount = $data->amount;
+                        $amount += getVatValue($amount, $data->vat_id);
+                    } else {
+                        $amount = $data->amount;
+                    }
+
+                    // CALCULATE TOTALS HERE (in the same loop)
+                    if (!$data->deleted) {
+                        $exportTotals[$data->payment_method->name][$data->type] += $amount;
+                    }
+
+                    $isCommercial = ($data->commercial == 1 || $data->commercial === '1' || $data->commercial === true);
+                    $typeLabel = $isCommercial ? 'Commerciale' : 'Non Commerciale';
+
+                    $nominativo = '';
+                    if ($data->type == "IN") {
+                        if ($data->member) {
+                            $nominativo = $data->member->last_name . " " . $data->member->first_name;
+                        }
+                    } else {
+                        if ($data->supplier) {
+                            $nominativo = $data->supplier->name;
+                        }
+                    }
+
+                    $groupKey = $data->date . '|' . $typeLabel . '|' . $data->payment_method->name . '|' . $data->type . '|' . $nominativo;
+
+                    if (!isset($groupedData[$groupKey])) {
+                        $groupedData[$groupKey] = [
+                            'date' => $data->date,
+                            'type_label' => $typeLabel,
+                            'payment_method' => $data->payment_method->name,
+                            'transaction_type' => $data->type,
+                            'nominativo' => $nominativo,
+                            'amount' => 0,
+                            'deleted' => false,
+                            'causals' => [],
+                            'notes' => []
+                        ];
+                        $causalsCount[$groupKey] = [];
+                    }
+
+                    $groupedData[$groupKey]['amount'] += $amount;
+                    $causalsCount[$groupKey][$causalCheck->getTree()] = true;
+
+                    if (!empty($data->note)) {
+                        $groupedData[$groupKey]['notes'][] = $data->note;
+                    }
+
+                    if ($data->deleted) {
+                        $groupedData[$groupKey]['deleted'] = true;
+                    }
+                }
+
+                $processedCount++;
+            } catch (\Exception $e) {
+                Log::error('generateExportDataAndTotals: Error processing individual record', [
+                    'record_id' => $data->id ?? 'unknown',
+                    'error' => $e->getMessage(),
+                    'processed_so_far' => $processedCount
+                ]);
+                throw $e;
+            }
+        }
+
+        $loopTime = microtime(true) - $loopStart;
+        Log::info('generateExportDataAndTotals: Combined processing loop completed', [
+            'total_processed' => $processedCount,
+            'grouped_records' => count($groupedData),
+            'loop_time' => $loopTime,
+            'memory_after_loop' => memory_get_usage(true)
+        ]);
+
+        Log::info('generateExportDataAndTotals: Building final export records');
+        $finalStart = microtime(true);
+
+        foreach ($groupedData as $groupKey => $group) {
+            $causalsInGroup = array_keys($causalsCount[$groupKey]);
+
+            $causalDisplay = $group['type_label'];
+
+            if (count($causalsInGroup) > 1) {
+                $detailDisplay = 'Varie|' . implode('|', $causalsInGroup);
+            } else {
+                $detailDisplay = $causalsInGroup[0];
+            }
+
+            $recordKey = $group['date'] . "§" . $causalDisplay . "§" . $group['nominativo'] . "§" . $detailDisplay . "§" . ($group['deleted'] ? 'DELETED' : '') . "§";
+
+            if (!isset($exportRecords[$recordKey][$group['payment_method']][$group['transaction_type']])) {
+                $exportRecords[$recordKey][$group['payment_method']][$group['transaction_type']] = 0;
+            }
+
+            $exportRecords[$recordKey][$group['payment_method']][$group['transaction_type']] += $group['amount'];
+        }
+
+        $finalTime = microtime(true) - $finalStart;
+        Log::info('generateExportDataAndTotals: Final processing completed', [
+            'final_export_records' => count($exportRecords),
+            'final_export_totals' => count($exportTotals),
+            'final_time' => $finalTime,
+            'total_time' => microtime(true) - $loopStart + $queryTime,
+            'memory_final' => memory_get_usage(true),
+            'memory_peak' => memory_get_peak_usage(true)
+        ]);
+
+        // Cleanup
+        unset($datas, $groupedData, $causalsCount);
+        gc_collect_cycles();
+
+        Log::info('generateExportDataAndTotals: Completed with cleanup', [
+            'memory_after_cleanup' => memory_get_usage(true)
+        ]);
+
+        return ['records' => $exportRecords, 'totals' => $exportTotals];
+    }
+
+    private function generateExportTotals($fromDate, $toDate)
+    {
+        $exportTotals = array();
+
+        $exclude_from_records = \App\Models\Member::where('exclude_from_records', true)->pluck('id')->toArray();
+
+        $datas = \App\Models\Record::with('member', 'supplier', 'payment_method')
+            ->select(
+                'records.*',
+                'records_rows.id as row_id',
+                'records_rows.record_id',
+                'records_rows.causal_id',
+                'records_rows.amount',
+                'records_rows.note',
+                'records_rows.when',
+                'records_rows.vat_id',
+            )
+            ->join('records_rows', 'records.id', '=', 'records_rows.record_id')
+            ->whereBetween('date', [$fromDate, $toDate])
+            ->where(function ($query) {
+                $query->where('type', 'OUT')
+                    ->orWhere(function ($query) {
+                        $query->where('records.corrispettivo_fiscale', true)
+                            ->orWhere('records.commercial', false);
+                    });
+            })
+            ->where(function ($query) use ($exclude_from_records) {
+                $query->where('type', 'OUT')
+                    ->orWhere(function ($subquery) use ($exclude_from_records) {
+                        $subquery->whereNotIn('member_id', $exclude_from_records);
+                    });
+            });
+
+        if ($this->filterCausals != null && sizeof($this->filterCausals) > 0) {
+            $causals = array();
+            foreach ($this->filterCausals as $z) {
+                $causals[] = $z;
+                $childs = \App\Models\Causal::where('parent_id', $z)->get();
+                foreach ($childs as $c) {
+                    $causals[] = $c->id;
+                    $childsX = \App\Models\Causal::where('parent_id', $c->id)->get();
+                    foreach ($childsX as $cX) {
+                        $causals[] = $cX->id;
+                    }
+                }
+            }
+            $datas->whereIn('causal_id', $causals);
+        }
+        if ($this->filterMember != null && $this->filterMember > 0) {
+            $datas->where('member_id', $this->filterMember);
+        }
+        $datas = $datas->orderBy('date', 'ASC')
+            ->orderBy('records.created_at', 'ASC')
+            ->orderBy('records_rows.id', 'ASC')
+            ->get();
+
+        foreach ($datas as $data) {
+            $causalCheck = \App\Models\Causal::findOrFail($data->causal_id);
+            $paymentCheck = $data->payment_method->money;
+
+            if (!$paymentCheck && ($causalCheck->no_first == null || !$causalCheck->no_first)) {
+                if (!$data->deleted) {
+                    $amount = $data->amount;
+                    $amount += getVatValue($amount, $data->vat_id);
+                } else {
+                    $amount = $data->amount;
+                }
+
+                if (!isset($exportTotals[$data->payment_method->name])) {
+                    $exportTotals[$data->payment_method->name]["IN"] = 0;
+                    $exportTotals[$data->payment_method->name]["OUT"] = 0;
+                }
+
+                if (!$data->deleted)
+                    $exportTotals[$data->payment_method->name][$data->type] += $amount;
+            }
+        }
+
+        return $exportTotals;
+    }
+    public function resetFilters()
+    {
+        $this->selectedPeriod = 'OGGI';
+        $this->filterCausals = [];
+        $this->filterMember = null;
+
+        $today = date("Y-m-d");
+        $this->fromDate = $today;
+        $this->toDate = $today;
+        $this->appliedFromDate = $today;
+        $this->appliedToDate = $today;
+
+        $this->emit('filters-reset');
+    }
+
+    public function applyFilters()
+    {
+        $this->isFiltering = true;
+
+        $this->setPeriodDates();
+
+        $this->appliedFromDate = $this->fromDate;
+        $this->appliedToDate = $this->toDate;
+
+        $this->render();
+
+        $this->isFiltering = false;
+
+        $this->emit('filters-applied');
+    }
+
+    private function setPeriodDates()
+    {
+        $today = now();
+
+        switch ($this->selectedPeriod) {
+            case 'OGGI':
+                $this->fromDate = $today->format('Y-m-d');
+                $this->toDate = $today->format('Y-m-d');
+                break;
+
+            case 'IERI':
+                $yesterday = $today->copy()->subDay();
+                $this->fromDate = $yesterday->format('Y-m-d');
+                $this->toDate = $yesterday->format('Y-m-d');
+                break;
+
+            case 'MESE CORRENTE':
+                $this->fromDate = $today->copy()->startOfMonth()->format('Y-m-d');
+                $this->toDate = $today->copy()->endOfMonth()->format('Y-m-d');
+                break;
+
+            case 'MESE PRECEDENTE':
+                $lastMonth = $today->copy()->subMonth();
+                $this->fromDate = $lastMonth->startOfMonth()->format('Y-m-d');
+                $this->toDate = $lastMonth->endOfMonth()->format('Y-m-d');
+                break;
+
+            case 'ULTIMO TRIMESTRE':
+                $this->fromDate = $today->copy()->subMonths(3)->format('Y-m-d');
+                $this->toDate = $today->format('Y-m-d');
+                break;
+
+            case 'ULTIMO QUADRIMESTRE':
+                $this->fromDate = $today->copy()->subMonths(4)->format('Y-m-d');
+                $this->toDate = $today->format('Y-m-d');
+                break;
+        }
+    }
+
+    public function getCausals($records, $indentation)
+    {
+        foreach ($records as $record) {
+            $this->causals[] = array('id' => $record->id, 'name' => $record->getTree(), 'text' => $record->getTree(), 'level' => $indentation);
+            if (count($record->childs))
+                $this->getCausals($record->childs, $indentation + 1);
+        }
+    }
+
+    public function getMonth($m)
+    {
+        $ret = '';
+        switch ($m) {
+            case 1:
+                $ret = 'Gennaio';
+                break;
+            case 2:
+                $ret = 'Febbraio';
+                break;
+            case 3:
+                $ret = 'Marzo';
+                break;
+            case 4:
+                $ret = 'Aprile';
+                break;
+            case 5:
+                $ret = 'Maggio';
+                break;
+            case 6:
+                $ret = 'Giugno';
+                break;
+            case 7:
+                $ret = 'Luglio';
+                break;
+            case 8:
+                $ret = 'Agosto';
+                break;
+            case 9:
+                $ret = 'Settembre';
+                break;
+            case 10:
+                $ret = 'Ottobre';
+                break;
+            case 11:
+                $ret = 'Novembre';
+                break;
+            case 12:
+                $ret = 'Dicembre';
+                break;
+            default:
+                $ret = '';
+                break;
+        }
+        return $ret;
+    }
+
+
+
+    public function render()
+    {
+        $month = 0;
+        $year = 0;
+
+        $this->records = array();
+        $this->totals = array();
+        $this->causalAmounts = array();
+
+        $exclude_from_records = \App\Models\Member::where('exclude_from_records', true)->pluck('id')->toArray();
+
+        $datas = \App\Models\Record::with('member', 'supplier', 'payment_method')
+            ->select(
+                'records.*',
+                'records_rows.id as row_id',
+                'records_rows.record_id',
+                'records_rows.causal_id',
+                'records_rows.amount',
+                'records_rows.note',
+                'records_rows.when',
+                'records_rows.vat_id',
+            )
+            ->join('records_rows', 'records.id', '=', 'records_rows.record_id')
+            ->whereBetween('date', [$this->appliedFromDate, $this->appliedToDate])
+            ->where(function ($query) {
+                $query->where('type', 'OUT')
+                    ->orWhere(function ($query) {
+                        $query->where('records.corrispettivo_fiscale', true)
+                            ->orWhere('records.commercial', false);
+                    });
+            })
+            ->where(function ($query) use ($exclude_from_records) {
+                $query->where('type', 'OUT')
+                    ->orWhere(function ($subquery) use ($exclude_from_records) {
+                        $subquery->whereNotIn('member_id', $exclude_from_records);
+                    });
+            });
+
+        if ($this->filterCausals != null && sizeof($this->filterCausals) > 0) {
+            $causals = array();
+            foreach ($this->filterCausals as $z) {
+                $causals[] = $z;
+                $childs = \App\Models\Causal::where('parent_id', $z)->get();
+                foreach ($childs as $c) {
+                    $causals[] = $c->id;
+                    $childsX = \App\Models\Causal::where('parent_id', $c->id)->get();
+                    foreach ($childsX as $cX) {
+                        $causals[] = $cX->id;
+                    }
+                }
+            }
+            $datas->whereIn('causal_id', $causals);
+        }
+        if ($this->filterMember != null && $this->filterMember > 0) {
+            $datas->where('member_id', $this->filterMember);
+        }
+        $datas = $datas->orderBy('date', 'ASC')
+            ->orderBy('records.created_at', 'ASC')
+            ->orderBy('records_rows.id', 'ASC')
+            ->get();
+
+        $groupedData = [];
+        $causalsCount = [];
+        $causalsAmounts = [];
+        $nominativi = [];
+
+        foreach ($datas as $idx => $data) {
+
+            $causalCheck = \App\Models\Causal::findOrFail($data->causal_id);
+            $paymentCheck = $data->payment_method->money;
+
+            if (!$paymentCheck && ($causalCheck->no_first == null || !$causalCheck->no_first)) {
+
+                if (!$data->deleted) {
+                    $amount = $data->amount;
+                    $amount += getVatValue($amount, $data->vat_id);
+                } else {
+                    $amount = $data->amount;
+                }
+
+                $isCommercial = ($data->commercial == 1 || $data->commercial === '1' || $data->commercial === true);
+                $typeLabel = $isCommercial ? 'Commerciale' : 'Non Commerciale';
+
+                $nominativo = '';
+                if ($data->type == "IN") {
+                    if ($data->member) {
+                        $nominativo = $data->member->last_name . " " . $data->member->first_name;
+                    }
+                } else {
+                    if ($data->supplier) {
+                        $nominativo = $data->supplier->name;
+                    }
+                }
+
+                $groupKey = $data->date . '|' . $typeLabel . '|' . $data->payment_method->name . '|' . $data->type . '|' . $nominativo;
+
+                if (!isset($groupedData[$groupKey])) {
+                    $groupedData[$groupKey] = [
+                        'date' => $data->date,
+                        'type_label' => $typeLabel,
+                        'payment_method' => $data->payment_method->name,
+                        'transaction_type' => $data->type,
+                        'nominativo' => $nominativo,
+                        'amount' => 0,
+                        'deleted' => false,
+                        'causals' => [],
+                        'notes' => []
+                    ];
+                    $causalsCount[$groupKey] = [];
+                    $causalsAmounts[$groupKey] = []; // Initialize causal amounts for this group
+                    $nominativi[$groupKey] = $nominativo;
+                }
+
+                $groupedData[$groupKey]['amount'] += $amount;
+                $causalsCount[$groupKey][$causalCheck->getTree()] = true;
+
+                $causalName = $causalCheck->getTree();
+                if (!isset($causalsAmounts[$groupKey][$causalName])) {
+                    $causalsAmounts[$groupKey][$causalName] = 0;
+                }
+                $causalsAmounts[$groupKey][$causalName] += $amount;
+
+                if (!empty($data->note)) {
+                    $groupedData[$groupKey]['notes'][] = $data->note;
+                }
+                if ($data->deleted) {
+                    $groupedData[$groupKey]['deleted'] = true;
+                }
+            }
+        }
+
+        foreach ($groupedData as $groupKey => $group) {
+            $causalsInGroup = array_keys($causalsCount[$groupKey]);
+
+            $causalDisplay = $group['type_label'];
+
+            if (count($causalsInGroup) > 1) {
+                $causalAmountsForJs = [];
+                foreach ($causalsInGroup as $causalName) {
+                    $causalAmountsForJs[] = $causalName . ':::' . formatPrice($causalsAmounts[$groupKey][$causalName]);
+                }
+                $detailDisplay = 'Varie|' . implode('|', $causalAmountsForJs);
+            } else {
+                $detailDisplay = $causalsInGroup[0];
+            }
+
+            $recordKey = $group['date'] . "§" . $causalDisplay . "§" . $group['nominativo'] . "§" . $detailDisplay . "§" . ($group['deleted'] ? 'DELETED' : '') . "§";
+
+            if (!isset($this->records[$recordKey][$group['payment_method']][$group['transaction_type']])) {
+                $this->records[$recordKey][$group['payment_method']][$group['transaction_type']] = 0;
+            }
+
+            $this->records[$recordKey][$group['payment_method']][$group['transaction_type']] += $group['amount'];
+
+            if (!isset($this->totals[$group['payment_method']])) {
+                $this->totals[$group['payment_method']]["IN"] = 0;
+                $this->totals[$group['payment_method']]["OUT"] = 0;
+            }
+
+            if (!$group['deleted'])
+                $this->totals[$group['payment_method']][$group['transaction_type']] += $group['amount'];
+        }
+
+        return view('livewire.records');
+    }
+
+    private function getLabels($fromDate, $toDate)
+    {
+        $begin = new DateTime($fromDate);
+        $end = new DateTime($toDate);
+
+        $interval = DateInterval::createFromDateString('1 day');
+        $date_range = new DatePeriod($begin, $interval, $end);
+        foreach ($date_range as $date) {
+            $labels[] = $date->format('d/M');
+        }
+
+        return $labels;
+    }
+
+    private function getRecordData($type, $fromDate, $toDate)
+    {
+        $data = [];
+        $begin = new DateTime($fromDate);
+        $end = new DateTime($toDate);
+
+        $interval = DateInterval::createFromDateString('1 day');
+        $date_range = new DatePeriod($begin, $interval, $end);
+
+        foreach ($date_range as $date) {
+            if ($type == 'IN') {
+                $found = false;
+                foreach ($this->in as $in) {
+                    if (date("Y-m-d", strtotime($in->date)) == $date->format('Y-m-d')) {
+                        $data[] = number_format($in->total, 0, "", "");
+                        $found = true;
+                    }
+                }
+                if (!$found)
+                    $data[] = 0;
+            }
+            if ($type == 'OUT') {
+                $found = false;
+                foreach ($this->out as $out) {
+                    if (date("Y-m-d", strtotime($out->date)) == $date->format('Y-m-d')) {
+                        $data[] = number_format($out->total, 0, "", "");
+                        $found = true;
+                    }
+                }
+                if (!$found)
+                    $data[] = 0;
+            }
+        }
+
+        return $data;
+    }
+
+    public function openExportModal()
+    {
+        $this->exportFromDate = $this->appliedFromDate;
+        $this->exportToDate = $this->appliedToDate;
+
+        // Reset email options
+        $this->sendViaEmail = false;
+        $this->exportEmailAddress = $this->getPreferredEmail();
+        $this->updateEmailSubject();
+
+        $this->emit('show-export-modal');
+    }
+
+    public function exportWithDateRange()
+    {
+
+        Log::info('=== EXPORT START ===', [
+            'user_id' => auth()->id(),
+            'from_date' => $this->exportFromDate,
+            'to_date' => $this->exportToDate,
+            'send_via_email' => $this->sendViaEmail,
+            'memory_usage' => memory_get_usage(true),
+            'memory_peak' => memory_get_peak_usage(true),
+            'time_limit' => ini_get('max_execution_time')
+        ]);
+
+        $this->isExporting = true;
+        $this->emit('$refresh');
+        usleep(100000);
+
+        if ($this->sendViaEmail) {
+            Log::info('Export: Validating email fields');
+            $this->validate([
+                'exportEmailAddress' => 'required|email',
+                'exportEmailSubject' => 'required|string|max:255',
+            ]);
+            Log::info('Export: Email validation passed');
+        }
+
+        $this->isExporting = true;
+
+        try {
+            Log::info('Export: Starting COMBINED data generation phase (NO SEPARATE CALLS)');
+            $startTime = microtime(true);
+
+            // *** THIS IS THE KEY CHANGE - USE ONLY THE COMBINED METHOD ***
+            $result = $this->generateExportDataAndTotals($this->exportFromDate, $this->exportToDate);
+            $exportRecords = $result['records'];
+            $exportTotals = $result['totals'];
+
+            $dataGenTime = microtime(true) - $startTime;
+            Log::info('Export: COMBINED data generation completed (NO SEPARATE TOTALS CALL)', [
+                'records_count' => count($exportRecords),
+                'totals_count' => count($exportTotals),
+                'generation_time' => $dataGenTime,
+                'memory_usage' => memory_get_usage(true),
+                'memory_peak' => memory_get_peak_usage(true)
+            ]);
+
+
+            if ($this->sendViaEmail) {
+                Log::info('Export: Dispatching to background job');
+                $this->dispatchExportJob($exportRecords, $exportTotals);
+                Log::info('Export: Job dispatched successfully');
+            } else {
+                Log::info('Export: Starting direct download export');
+                $exportStartTime = microtime(true);
+
+                $result = $this->exportWithData($exportRecords, $exportTotals);
+
+                $exportTime = microtime(true) - $exportStartTime;
+                Log::info('Export: Direct export completed', [
+                    'export_time' => $exportTime,
+                    'total_time' => microtime(true) - $startTime,
+                    'memory_usage' => memory_get_usage(true),
+                    'memory_peak' => memory_get_peak_usage(true)
+                ]);
+
+                return $result;
+            }
+        } catch (\Illuminate\Validation\ValidationException $e) {
+            Log::error('Export: Validation error', [
+                'error' => $e->getMessage(),
+                'errors' => $e->errors()
+            ]);
+            $this->isExporting = false;
+            throw $e;
+        } catch (\Exception $e) {
+            Log::error('Export: General error', [
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString(),
+                'memory_usage' => memory_get_usage(true),
+                'memory_peak' => memory_get_peak_usage(true),
+                'execution_time' => microtime(true) - ($startTime ?? 0)
+            ]);
+
+            $this->isExporting = false;
+
+            if ($this->sendViaEmail) {
+                $this->emit('export-email-error', 'Errore durante l\'invio dell\'email: ' . $e->getMessage());
+            } else {
+                session()->flash('error', 'Errore durante l\'export: ' . $e->getMessage());
+            }
+        } finally {
+            Log::info('Export: Cleanup phase');
+            $this->isExporting = false;
+            $this->emit('export-complete');
+            $this->emit('hide-export-modal');
+            Log::info('=== EXPORT END ===');
+        }
+    }
+    private function getEstimatedRecordCount($fromDate, $toDate)
+    {
+        $exclude_from_records = \App\Models\Member::where('exclude_from_records', true)->pluck('id')->toArray();
+
+        $query = \App\Models\Record::join('records_rows', 'records.id', '=', 'records_rows.record_id')
+            ->whereBetween('date', [$fromDate, $toDate])
+            ->where(function ($query) {
+                $query->where('type', 'OUT')
+                    ->orWhere(function ($query) {
+                        $query->where('records.corrispettivo_fiscale', true)
+                            ->orWhere('records.commercial', false);
+                    });
+            })
+            ->where(function ($query) use ($exclude_from_records) {
+                $query->where('type', 'OUT')
+                    ->orWhere(function ($subquery) use ($exclude_from_records) {
+                        $subquery->whereNotIn('member_id', $exclude_from_records);
+                    });
+            });
+
+        if ($this->filterCausals != null && sizeof($this->filterCausals) > 0) {
+            $causals = array();
+            foreach ($this->filterCausals as $z) {
+                $causals[] = $z;
+                $childs = \App\Models\Causal::where('parent_id', $z)->get();
+                foreach ($childs as $c) {
+                    $causals[] = $c->id;
+                    $childsX = \App\Models\Causal::where('parent_id', $c->id)->get();
+                    foreach ($childsX as $cX) {
+                        $causals[] = $cX->id;
+                    }
+                }
+            }
+            $query->whereIn('causal_id', $causals);
+        }
+
+        if ($this->filterMember != null && $this->filterMember > 0) {
+            $query->where('member_id', $this->filterMember);
+        }
+
+        return $query->count();
+    }
+
+    private function getMemberName($memberId)
+    {
+        $member = \App\Models\Member::find($memberId);
+        return $member ? $member->last_name . ' ' . $member->first_name : 'Sconosciuto';
+    }
+
+    private function getCausalsNames($causalIds)
+    {
+        if (!is_array($causalIds)) {
+            return null;
+        }
+
+        $causals = \App\Models\Causal::whereIn('id', $causalIds)->pluck('name')->toArray();
+        return implode(', ', $causals);
+    }
+
+    public function updatedExportFromDate()
+    {
+        $this->updateEmailSubject();
+    }
+
+    public function updatedExportToDate()
+    {
+        $this->updateEmailSubject();
+    }
+
+    public function updatedSendViaEmail($value)
+    {
+        if ($value && empty($this->exportEmailAddress)) {
+            $this->exportEmailAddress = $this->getPreferredEmail();
+        }
+    }
+
+    public function resetEmailForm()
+    {
+        $this->sendViaEmail = false;
+        $this->exportEmailAddress = $this->getPreferredEmail();
+        $this->updateEmailSubject();
+    }
+
+    private function updateEmailSubject()
+    {
+        if (!empty($this->exportFromDate) && !empty($this->exportToDate)) {
+            $fromFormatted = date('d/m/Y', strtotime($this->exportFromDate));
+            $toFormatted = date('d/m/Y', strtotime($this->exportToDate));
+
+            if ($this->exportFromDate === $this->exportToDate) {
+                $this->exportEmailSubject = "Prima Nota - Export del {$fromFormatted}";
+            } else {
+                $this->exportEmailSubject = "Prima Nota - Export dal {$fromFormatted} al {$toFormatted}";
+            }
+        }
+    }
+
+
+    /**
+     * Dispatch export job to queue
+     */
+    private function dispatchExportJob($exportRecords, $exportTotals)
+    {
+        try {
+            // Prepare filter descriptions for the job
+            $filterDescriptions = [
+                'member' => $this->filterMember ? $this->getMemberName($this->filterMember) : null,
+                'causals' => $this->filterCausals ? $this->getCausalsNames($this->filterCausals) : null,
+            ];
+
+            $paymentsArray = $this->payments->map(function ($payment) {
+                return [
+                    'id' => $payment->id,
+                    'name' => $payment->name,
+                    'type' => $payment->type
+                ];
+            })->toArray();
+
+            // Dispatch job to background queue
+            ExportPrimaNota::dispatch(
+                $exportRecords,
+                $exportTotals,
+                $this->exportEmailAddress,
+                $this->exportEmailSubject,
+                [
+                    'from' => date('d/m/Y', strtotime($this->exportFromDate)),
+                    'to' => date('d/m/Y', strtotime($this->exportToDate))
+                ],
+                auth()->id(),
+                $paymentsArray,
+                $filterDescriptions
+            );
+
+            $this->emit('export-email-queued');
+            session()->flash('success', 'Export in corso! Riceverai l\'email a breve alla casella: ' . $this->exportEmailAddress);
+
+            Log::info('Export job dispatched', [
+                'user_id' => auth()->id(),
+                'email' => $this->exportEmailAddress,
+                'date_range' => [$this->exportFromDate, $this->exportToDate],
+                'total_records' => count($exportRecords)
+            ]);
+        } catch (\Exception $e) {
+            Log::error('Failed to dispatch export job', [
+                'user_id' => auth()->id(),
+                'email' => $this->exportEmailAddress,
+                'error' => $e->getMessage()
+            ]);
+
+            throw new \Exception('Errore nell\'avvio dell\'export: ' . $e->getMessage());
+        }
+    }
+
+
+    function export()
+    {
+        $result = $this->generateExportDataAndTotals($this->exportFromDate, $this->exportToDate);
+        $exportRecords = $result['records'];
+        $exportTotals = $result['totals'];
+
+        return $this->exportWithData($exportRecords, $exportTotals);
+    }
+
+    private function exportWithData($exportRecords, $exportTotals)
+    {
+        Log::info('exportWithData: Starting Excel generation', [
+            'records_count' => count($exportRecords),
+            'totals_count' => count($exportTotals),
+            'memory_before' => memory_get_usage(true)
+        ]);
+
+        $startTime = microtime(true);
+
+        Log::info('exportWithData: Setting memory and GC');
+        ini_set('memory_limit', '512M');
+        gc_enable();
+
+        Log::info('exportWithData: 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('exportWithData: Creating spreadsheet object');
+        $spreadsheet = new Spreadsheet();
+        $activeWorksheet = $spreadsheet->getActiveSheet();
+
+        Log::info('exportWithData: Setting basic headers');
+        $activeWorksheet->setCellValue('A1', "Data");
+        $activeWorksheet->setCellValue('B1', "Tipologia");
+        $activeWorksheet->setCellValue('C1', "Causale");
+        $activeWorksheet->setCellValue('D1', "Nominativo");
+        $activeWorksheet->setCellValue('E1', "Stato");
+        $activeWorksheet->getStyle('A1:Q1')->getFont()->setBold(true);
+        $activeWorksheet->getStyle('A2:Q2')->getFont()->setBold(true);
+
+        $activeWorksheet->getStyle('A1:Q1')->getFill()
+            ->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
+            ->getStartColor()->setARGB('FF0C6197');
+
+        $activeWorksheet->getStyle('A1:Q1')->getFont()->getColor()->setARGB('FFFFFFFF');
+
+        $idx = 0;
+        foreach ($this->payments as $p) {
+            if ($idx >= count($letters)) {
+                Log::warning('exportWithData: Reached letter limit', ['payment_index' => $idx]);
+                break;
+            }
+            Log::debug('exportWithData: Setting payment header', [
+                'payment_name' => $p->name,
+                'column_index' => $idx,
+                'column_letter' => $letters[$idx]
+            ]);
+
+            $activeWorksheet->setCellValue($letters[$idx] . '1', $p->name);
+            $idx++;
+            if ($idx >= count($letters)) {
+                break;
+            }
+            $activeWorksheet->mergeCells($letters[$idx] . '1:' . $letters[$idx] . '1');
+            $idx++;
+        }
+
+        Log::info('exportWithData: Setting sub-headers');
+        $idx = 0;
+        $activeWorksheet->setCellValue('A2', "");
+        $activeWorksheet->setCellValue('B2', "");
+        $activeWorksheet->setCellValue('C2', "");
+        $activeWorksheet->setCellValue('D2', "");
+        $activeWorksheet->setCellValue('E2', "");
+
+        foreach ($this->payments as $p) {
+            if ($p->type == 'ALL') {
+                if ($idx >= count($letters)) {
+                    break;
+                }
+                $activeWorksheet->setCellValue($letters[$idx] . '2', "Entrate");
+                $idx++;
+                $activeWorksheet->setCellValue($letters[$idx] . '2', "Uscite");
+                $idx++;
+            } elseif ($p->type == 'IN') {
+                if ($idx >= count($letters)) {
+                    break;
+                }
+                $activeWorksheet->setCellValue($letters[$idx] . '2', "Entrate");
+                $idx++;
+                $activeWorksheet->setCellValue($letters[$idx] . '2', "");
+                $idx++;
+            } elseif ($p->type == 'OUT') {
+                if ($idx >= count($letters)) {
+                    break;
+                }
+                $activeWorksheet->setCellValue($letters[$idx] . '2', "");
+                $idx++;
+                $activeWorksheet->setCellValue($letters[$idx] . '2', "Uscite");
+                $idx++;
+            }
+        }
+
+        Log::info('exportWithData: Applying header styles');
+        $activeWorksheet->getStyle('A1:Q1')->getFont()->setBold(true);
+        $activeWorksheet->getStyle('A2:Q2')->getFont()->setBold(true);
+
+        Log::info('exportWithData: Starting data row processing');
+        $count = 3;
+        $batchSize = 1000;
+        $recordsProcessed = 0;
+
+        $totalRecords = count($exportRecords);
+        Log::info('exportWithData: Processing records in batches', [
+            'total_records' => $totalRecords,
+            'batch_size' => $batchSize
+        ]);
+
+        $recordsArray = array_chunk($exportRecords, $batchSize, true);
+        Log::info('exportWithData: Created batches', ['batch_count' => count($recordsArray)]);
+
+        foreach ($recordsArray as $batchIndex => $recordsBatch) {
+            Log::info('exportWithData: Processing batch', [
+                'batch_index' => $batchIndex,
+                'batch_size' => count($recordsBatch),
+                'memory_current' => memory_get_usage(true),
+                'time_elapsed' => microtime(true) - $startTime
+            ]);
+
+            foreach ($recordsBatch as $causal => $record) {
+                if ($recordsProcessed % 250 == 0) {
+                    Log::info('exportWithData: Record processing progress', [
+                        'processed' => $recordsProcessed,
+                        'total' => $totalRecords,
+                        'current_row' => $count,
+                        'memory_usage' => memory_get_usage(true),
+                        'time_elapsed' => microtime(true) - $startTime
+                    ]);
+                }
+
+                try {
+                    $check = $causal;
+                    $parts = explode("§", $check);
+                    $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('exportWithData: Setting row cells', ['row' => $count]);
+                    $activeWorksheet->setCellValue('A' . $count, !empty($d) ? date("d/m/Y", strtotime($d)) : '');
+                    $activeWorksheet->setCellValue('B' . $count, $c);
+                    $activeWorksheet->setCellValue('C' . $count, $exportDetail);
+                    $activeWorksheet->setCellValue('D' . $count, $j);
+
+                    $stato = ($deleted === 'DELETED') ? 'ANNULLATA' : '';
+                    $activeWorksheet->setCellValue('E' . $count, $stato);
+
+                    if ($stato === 'ANNULLATA') {
+                        $activeWorksheet->getStyle('E' . $count)->getFont()->getColor()->setARGB('FFFF0000');
+                    }
+
+                    $idx = 0;
+                    foreach ($this->payments as $p) {
+                        if ($idx >= count($letters) - 1) {
+                            break;
+                        }
+
+                        if (isset($record[$p->name])) {
+                            $inValue = isset($record[$p->name]["IN"]) ? formatPrice($record[$p->name]["IN"]) : "";
+                            $outValue = isset($record[$p->name]["OUT"]) ? 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++;
+                    $recordsProcessed++;
+
+                    if ($recordsProcessed % 500 === 0) {
+                        Log::debug('exportWithData: Garbage collection');
+                        gc_collect_cycles();
+                    }
+                } catch (\Exception $e) {
+                    Log::error('exportWithData: Error processing record row', [
+                        'row' => $count,
+                        'causal' => $causal,
+                        'error' => $e->getMessage(),
+                        'processed_so_far' => $recordsProcessed
+                    ]);
+                    throw $e;
+                }
+            }
+
+            Log::info('exportWithData: Batch completed', [
+                'batch_index' => $batchIndex,
+                'records_in_batch' => count($recordsBatch),
+                'total_processed' => $recordsProcessed
+            ]);
+
+            unset($recordsBatch);
+            gc_collect_cycles();
+        }
+
+        Log::info('exportWithData: Adding totals row');
+        $count++;
+        $idx = 0;
+
+        $activeWorksheet->setCellValue('A' . $count, 'Totale');
+        $activeWorksheet->setCellValue('B' . $count, '');
+        $activeWorksheet->setCellValue('C' . $count, '');
+        $activeWorksheet->setCellValue('D' . $count, '');
+        $activeWorksheet->setCellValue('E' . $count, '');
+
+        foreach ($this->payments as $p) {
+            if ($idx >= count($letters) - 1) {
+                break;
+            }
+
+            if (isset($exportTotals[$p->name])) {
+                if ($p->type == 'ALL') {
+                    $activeWorksheet->setCellValue($letters[$idx] . $count, formatPrice($exportTotals[$p->name]["IN"] ?? 0));
+                    $idx++;
+                    $activeWorksheet->setCellValue($letters[$idx] . $count, formatPrice($exportTotals[$p->name]["OUT"] ?? 0));
+                    $idx++;
+                } elseif ($p->type == 'IN') {
+                    $activeWorksheet->setCellValue($letters[$idx] . $count, formatPrice($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, formatPrice($exportTotals[$p->name]["OUT"] ?? 0));
+                    $idx++;
+                }
+            } else {
+                if ($p->type == 'ALL') {
+                    $activeWorksheet->setCellValue($letters[$idx] . $count, "0");
+                    $idx++;
+                    $activeWorksheet->setCellValue($letters[$idx] . $count, "0");
+                    $idx++;
+                } elseif ($p->type == 'IN') {
+                    $activeWorksheet->setCellValue($letters[$idx] . $count, "0");
+                    $idx++;
+                    $activeWorksheet->setCellValue($letters[$idx] . $count, "");
+                    $idx++;
+                } elseif ($p->type == 'OUT') {
+                    $activeWorksheet->setCellValue($letters[$idx] . $count, "");
+                    $idx++;
+                    $activeWorksheet->setCellValue($letters[$idx] . $count, "0");
+                    $idx++;
+                }
+            }
+        }
+
+        Log::info('exportWithData: Applying final styles');
+        $activeWorksheet->getStyle('A' . $count . ':Q' . $count)->getFont()->setBold(true);
+
+        Log::info('exportWithData: Setting column dimensions');
+        $activeWorksheet->getColumnDimension('A')->setWidth(20);
+        $activeWorksheet->getColumnDimension('B')->setWidth(40);
+        $activeWorksheet->getColumnDimension('C')->setWidth(40);
+        $activeWorksheet->getColumnDimension('D')->setWidth(40);
+        $activeWorksheet->getColumnDimension('E')->setWidth(20);
+        foreach ($letters as $l) {
+            $activeWorksheet->getColumnDimension($l)->setWidth(20);
+        }
+
+        $filename = 'prima_nota_' . date("YmdHis") . '.xlsx';
+        Log::info('exportWithData: Preparing to save file', [
+            'filename' => $filename,
+            'total_processing_time' => microtime(true) - $startTime,
+            'memory_before_save' => memory_get_usage(true)
+        ]);
+
+        try {
+            $currentClient = session('currentClient', 'default');
+            $tempPath = sys_get_temp_dir() . '/' . $filename;
+
+            Log::info('exportWithData: Creating Excel writer');
+            $writer = new Xlsx($spreadsheet);
+
+            Log::info('exportWithData: Saving to temp path', ['temp_path' => $tempPath]);
+            $writerStart = microtime(true);
+            $writer->save($tempPath);
+            $writerTime = microtime(true) - $writerStart;
+
+            Log::info('exportWithData: File saved to temp', [
+                'writer_time' => $writerTime,
+                'file_size' => file_exists($tempPath) ? filesize($tempPath) : 'unknown',
+                'memory_after_save' => memory_get_usage(true)
+            ]);
+
+            unset($spreadsheet, $activeWorksheet, $writer);
+            gc_collect_cycles();
+
+            Log::info('exportWithData: Uploading to S3');
+            $disk = Storage::disk('s3');
+            $s3Path = $currentClient . '/prima_nota/' . $filename;
+
+            $primaNotaFolderPath = $currentClient . '/prima_nota/.gitkeep';
+            if (!$disk->exists($primaNotaFolderPath)) {
+                $disk->put($primaNotaFolderPath, '');
+                Log::info("Created prima_nota folder for client: {$currentClient}");
+            }
+
+            $uploadStart = microtime(true);
+            $fileContent = file_get_contents($tempPath);
+            $uploaded = $disk->put($s3Path, $fileContent, 'private');
+            $uploadTime = microtime(true) - $uploadStart;
+
+            if (!$uploaded) {
+                throw new \Exception('Failed to upload file to Wasabi S3');
+            }
+
+            Log::info("Export completed successfully", [
+                'client' => $currentClient,
+                'path' => $s3Path,
+                'file_size' => filesize($tempPath),
+                'records_processed' => $recordsProcessed,
+                'upload_time' => $uploadTime,
+                'total_time' => microtime(true) - $startTime,
+                'memory_peak' => memory_get_peak_usage(true)
+            ]);
+
+            if (file_exists($tempPath)) {
+                unlink($tempPath);
+            }
+
+            $downloadUrl = $disk->temporaryUrl($s3Path, now()->addHour());
+            return redirect($downloadUrl);
+        } catch (\Exception $e) {
+            Log::error('Export S3 error - falling back to local', [
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString(),
+                'client' => session('currentClient', 'unknown'),
+                'filename' => $filename,
+                'records_processed' => $recordsProcessed ?? 0,
+                'time_elapsed' => microtime(true) - $startTime
+            ]);
+
+            // Fallback logic remains the same...
+            $currentClient = session('currentClient', 'default');
+            $clientFolder = storage_path('app/prima_nota/' . $currentClient);
+
+            if (!is_dir($clientFolder)) {
+                mkdir($clientFolder, 0755, true);
+                Log::info("Created local client prima_nota folder: {$clientFolder}");
+            }
+
+            $localPath = $clientFolder . '/' . $filename;
+
+            if (isset($tempPath) && file_exists($tempPath)) {
+                rename($tempPath, $localPath);
+            } else {
+                $writer = new Xlsx($spreadsheet);
+                $writer->save($localPath);
+                unset($spreadsheet, $activeWorksheet, $writer);
+            }
+
+            gc_collect_cycles();
+
+            Log::warning("Export saved locally due to S3 error", [
+                'client' => $currentClient,
+                'local_path' => $localPath,
+                'error' => $e->getMessage()
+            ]);
+
+            session()->flash('warning', 'File salvato localmente a causa di un errore del cloud storage.');
+            return response()->download($localPath)->deleteFileAfterSend();
+        }
+    }
+
+    private function getPreferredEmail()
+    {
+        // Try multiple sources in order of preference
+        $email = auth()->user()->email ?? null;
+
+        if (empty($email)) {
+            $email = session('user_email', null);
+        }
+
+        if (empty($email)) {
+            $member = \App\Models\Member::where('user_id', auth()->id())->first();
+            $email = $member ? $member->email : null;
+        }
+
+        if (empty($email)) {
+            // Get from user input or company default
+            $email = config('mail.default_recipient', '');
+        }
+
+        return $email;
+    }
+}

+ 1443 - 0
app/Http/Livewire/RecordIN.php

@@ -0,0 +1,1443 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+use Livewire\WithPagination;
+use Barryvdh\DomPDF\Facade\Pdf;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Facades\Mail;
+use Illuminate\Contracts\Validation\Validator;
+use Illuminate\Support\Facades\Log;
+use App\Http\Middleware\TenantMiddleware;
+
+class RecordIN extends Component
+{
+    use WithPagination;
+    protected $paginationTheme = 'bootstrap';
+
+    protected $listeners = ['setCausal' => 'setCausal', 'refreshMembers' => 'refreshMembers', 'scontoChanged' => 'updateSconto'];
+
+    public $sortField = 'date';
+    public $sortAsc = false;
+
+    public $typeIN = "IN";
+
+    public $fromPage = '';
+
+    public $isDuplicate = false;
+    public $multiP = false;
+    public $from, $to;
+
+    public $multiMonthTo = 0;
+    public $multiYearTo = 0;
+    public $multiMonthFrom = 0;
+    public $multiYearFrom = 0;
+    public $createSubscription = 0;
+
+    public $corrispettivo_causal_id = 0;
+
+    public function sortBy($field)
+    {
+        if ($this->sortField === $field) {
+            $this->sortAsc = ! $this->sortAsc;
+        } else {
+            $this->sortAsc = true;
+        }
+
+        $this->sortField = $field;
+    }
+
+    public $courseId = 0;
+    public $rateId = 0;
+    public $months = array();
+
+    public $records, $dataId, $member_id, $supplier_id,
+        $causal_id,
+        $payment_method_id,
+        $date,
+        $month,
+        $year,
+        $type,
+        $deleted,
+        $financial_movement,
+        $amount,
+        $vat,
+        $virtual,
+        $note,
+        $parent,
+        $corrispettivo_fiscale,
+        $commercial, $update = false, $add = false;
+
+    public $corrispettivo = [];
+
+    public $currentReceip;
+
+    public $filterMember = 0, $filterPaymentMethod = 0, $filterCausals = 0, $filterFrom = '', $filterTo = '', $filterCommercial = 0;
+
+    public $hasFilter = false;
+
+    public $total = 0;
+
+    public $selectedFilter = '';
+
+    public $multipleIds = [];
+    public $multipleAction = '';
+
+    public $first = true;
+
+    public $selectId = 0;
+    public $refreshAfter = 0;
+    public $sconto = 0;
+    public $canSave = true;
+    public $newMemberFirstName = '';
+    public $newMemberLastName = '';
+    public $newMemberEmail = '';
+    public $newMemberToComplete = false;
+    public $newMemberFiscalCode = '';
+    public $newMemberFiscalCodeExist = false;
+
+    public $causals = array();
+    public $payments = array();
+    public $members = array();
+    public $vats = array();
+
+    public $rows = array();
+
+    public $fromCourse = false;
+
+    protected $rules = [
+        'member_id' => 'required',
+        'payment_method_id' => 'required',
+        'rows.*.causal_id' => 'required',
+        'rows.*.amount' => 'required'
+    ];
+
+    protected $messages = [
+        'member_id.required' => 'La persona è obbligatorio',
+        'payment_method_id.required' => 'Il metodo di pagamento è obbligatorio',
+        'rows.*.causal_id.required' => 'La causale è obbligatoria',
+        'rows.*.amount.required' => 'L\'importo è obbligatorio',
+    ];
+
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+    }
+
+    public function updatedMemberId()
+    {
+        $this->emit('refresh');
+        if ($this->member_id > 0) {
+            $member = \App\Models\Member::findOrFail($this->member_id);
+            if (!$member->isAdult()) {
+                if ($member->father_name != '')
+                    $this->parent = $member->father_name . " (C.F. " . $member->father_fiscal_code . ")";
+                else if ($member->mother_name != '')
+                    $this->parent = $member->mother_name . " (C.F. " . $member->mother_fiscal_code . ")";
+                else
+                    $this->parent = '';
+            }
+            $this->virtual = $member->getMoney();
+            $this->newMemberFirstName = '';
+            $this->newMemberLastName = '';
+            $this->newMemberEmail = '';
+            $this->newMemberToComplete = false;
+            $this->newMemberFiscalCode = '';
+            $this->newMemberFiscalCodeExist = false;
+        }
+    }
+
+    public function updatedCausalId()
+    {
+        //$this->emit('refresh');
+    }
+
+    public function updatedDate()
+    {
+        //$this->emit('refresh');
+    }
+
+    public function hydrate()
+    {
+        $this->emit('load-select');
+    }
+
+    /*public function updated() {
+        $this->emit('refresh');
+    }*/
+
+    public function updatedPaymentMethodId()
+    {
+        //$this->emit('refresh');
+        $this->canSave = $this->checkCanSave();
+    }
+
+    public function updatedAmount()
+    {
+        // $this->emit('refresh');
+        $this->canSave = $this->checkCanSave();
+    }
+
+    public function updatedCommercial($value)
+    {
+        $this->emitSelf('refreshMembers');
+    }
+
+    public function updateSconto()
+    {
+        $totalSconto = 0;
+        foreach ($this->rows as $row) {
+            if (isset($row['sconto']) && $row['sconto'] != null && $row['sconto'] != "") {
+                $totalSconto += $this->currencyToDouble($row['sconto']);
+            }
+        }
+        $this->sconto = formatPrice($totalSconto);
+        Log::info("Total sconto updated to: " . $this->sconto);
+    }
+
+
+    public function checkCanSave()
+    {
+        $ret = true;
+        if ($this->payment_method_id != null) {
+            $payment_method = \App\Models\PaymentMethod::findOrFail($this->payment_method_id);
+            if ($payment_method->money) {
+                $from = str_replace(".", "", str_replace(",", "", number_format($this->virtual, 2)));
+                $this->from = $this->virtual;
+                $from = $this->from;
+                $to = str_replace(".", "", str_replace(",", "", $this->currencyToDouble($this->amount)));
+                $this->to = $this->amount;
+                $to = $this->to;
+                $ret = $from >= $to;
+            }
+        }
+        return $ret;
+    }
+
+    public function refreshMembers()
+    {
+        if ($this->add || $this->update) {
+            if ($this->commercial) {
+                $this->members = \App\Models\Member::select(['id', 'first_name', 'last_name', 'fiscal_code'])->orderBy('last_name')->orderBy('first_name')->get();
+            } else {
+                $this->members = \App\Models\Member::select(['id', 'first_name', 'last_name', 'fiscal_code'])->where('current_status', 2)->orWhere('current_status', 1)->orderBy('last_name')->orderBy('first_name')->get();
+            }
+        } else
+            $this->members = \App\Models\Member::select(['id', 'first_name', 'last_name', 'fiscal_code'])->orderBy('last_name')->orderBy('first_name')->get();
+    }
+
+    public function setAmount()
+    {
+        $tot = 0;
+        foreach ($this->rows as $r) {
+            $tot += $this->currencyToDouble($r["amount"]);
+        }
+        $this->amount = $tot;
+        $this->canSave = $this->checkCanSave();
+    }
+
+    public function resetFields()
+    {
+        $this->member_id = null;
+        $this->supplier_id = null;
+        $this->payment_method_id = null;
+        $this->commercial = 0;
+        $this->corrispettivo_fiscale = false;
+        $this->date = date("Y-m-d");
+        $this->type = 'IN';
+        $this->deleted = false;
+        $this->financial_movement = false;
+        $this->newMemberFirstName = '';
+        $this->newMemberLastName = '';
+        $this->newMemberEmail = '';
+        $this->newMemberToComplete = false;
+        $this->newMemberFiscalCode = '';
+        $this->newMemberFiscalCodeExist = false;
+        $this->currentReceip = null;
+        $this->parent = '';
+        $this->courseId = 0;
+        $this->rateId = 0;
+        $this->months = array();
+        $this->rows = array();
+        $this->rows[] = array('causal_id' => isset($_GET["causalId"]) ? $_GET["causalId"] : null, 'when' => array(array('month' => date("n"), 'year' => date("Y"), 'period' => '')),  'amount' => null, 'vat_id' => null, 'note' => '', 'commercial' => 0, 'sconto' => 0);
+        $this->corrispettivo = [];
+        $this->emit('load-data-table');
+    }
+
+    public function getMemberProperty()
+    {
+        $ret = null;
+        if ($this->member_id > 0) {
+            $ret = \App\Models\Member::findOrFail($this->member_id);
+        }
+        return $ret;
+    }
+
+    public function getCausalProperty()
+    {
+        $ret = null;
+        if ($this->causal_id > 0) {
+            $ret = \App\Models\Causal::findOrFail($this->causal_id);
+        }
+        return $ret;
+    }
+
+    public function getCausal($causal)
+    {
+        $ret = '';
+        if ($causal > 0) {
+            $ret = \App\Models\Causal::findOrFail($causal)->getTree();
+        }
+        return $ret;
+    }
+
+    public function getPaymentMethod($payment)
+    {
+        $ret = '';
+        if ($payment > 0) {
+            $ret = \App\Models\PaymentMethod::findOrFail($payment)->name;
+        }
+        return $ret;
+    }
+
+    function buildTree($records, $parentId = 0)
+    {
+        $this->causals = array();
+
+        foreach ($records as $record) {
+            if ($record->parent_id == $parentId) {
+                $children = $this->buildTree($record, $record->id);
+                if ($children) {
+                    $record->children = $children;
+                }
+                $this->causals[] = $record;
+            }
+        }
+
+        return $this->causals;
+    }
+
+    public function getCausale($records, $indentation)
+    {
+        foreach ($records as $record) {
+            $this->causals[] = array('id' => $record->id, 'name' => $record->getTree(), 'text' => $record->getTree(), 'level' => $indentation);
+            if (count($record->childs))
+                $this->getCausale($record->childs, $indentation + 1);
+        }
+    }
+
+    public function mount()
+    {
+
+        if (isset($_GET["from"])) {
+            $this->fromPage = $_GET["from"];
+        }
+
+        $this->causals = array();
+
+        $this->multiMonthFrom = date("n");
+        $this->multiYearFrom = date("Y");
+        $this->multiMonthTo = date("n");
+        $this->multiYearTo = date("Y");
+
+        $fisc = \App\Models\Causal::where('corrispettivo_fiscale', true)->first();
+        if ($fisc)
+            $this->corrispettivo_causal_id = $fisc->id;
+
+        $this->getCausale(\App\Models\Causal::select('id', 'name')->where('parent_id', null)->where('type', 'IN')->get(), 0);
+
+        //$this->buildTree(\App\Models\Causal::all(), null);
+
+        $this->refreshMembers();
+        $this->payments = \App\Models\PaymentMethod::select('id', 'name')->where('enabled', true)->whereIn('type', array('ALL', 'IN'))->orderBy('name')->get();
+        $this->vats = \App\Models\Vat::select('id', 'name', 'value')->orderBy('value')->get();
+
+        if ($this->first) {
+            if (isset($_GET["new"])) {
+                $this->refreshAfter = 1;
+                $this->add();
+            }
+            if (isset($_GET["memberId"])) {
+                $this->refreshAfter = 1;
+                $this->member_id = $_GET["memberId"];
+                $member = \App\Models\Member::findOrFail($this->member_id);
+                if (!$member->isAdult()) {
+                    if ($member->father_name != '')
+                        $this->parent = $member->father_name . " (C.F. " . $member->father_fiscal_code . ")";
+                    else if ($member->mother_name != '')
+                        $this->parent = $member->mother_name . " (C.F. " . $member->mother_fiscal_code . ")";
+                    else
+                        $this->parent = '';
+                }
+            }
+            if (isset($_GET["causalId"])) {
+                $this->refreshAfter = 1;
+                $this->causal_id = $_GET["causalId"];
+            }
+            if (isset($_GET["id"])) {
+                $this->refreshAfter = 1;
+                $this->edit($_GET["id"]);
+            }
+            $count = 0;
+            if (isset($_GET["courseId"])) {
+                $this->courseId = $_GET["courseId"];
+                $mc = \App\Models\MemberCourse::findOrFail($this->courseId);
+                $course = \App\Models\Course::findOrFail($mc->course_id);
+            }
+            if (isset($_GET["rateId"])) {
+                $this->rateId = $_GET["rateId"];
+            }
+            if (isset($_GET["months"]) && $_GET["months"] != '') {
+
+                $this->fromCourse = true;
+
+                $price = $_GET["price"] / 100 * 100;
+                $this->refreshAfter = 1;
+                $desc = "Pagamento";
+                $months = explode("|", $_GET["months"]);
+                $this->months = $months;
+                //$this->rows[0]["amount"] = formatPrice($price * sizeof($months));
+                $this->rows[0]["amount"] = formatPrice($price);
+                foreach ($months as $idx => $m) {
+                    $this->rows[0]["when"][$idx]["month"] = $m;
+
+                    if ($m >= date("m"))
+                        $this->rows[0]["when"][$idx]["year"] = date("Y");
+                    else
+                        $this->rows[0]["when"][$idx]["year"] = date("Y") + 1;
+                    /*if (env('FISCAL_YEAR_MONTH_FROM', 1) > 1)
+                    {
+                        if ($m > date("M"))
+                            $this->rows[0]["when"][$idx]["year"] = $m < env('FISCAL_YEAR_MONTH_FROM', 1) ? (date("Y")) : date("Y" + 1);
+                        else
+                            $this->rows[0]["when"][$idx]["year"] = $m > env('FISCAL_YEAR_MONTH_TO', 1) ? (date("Y")) : date("Y" + 1);
+                    }
+                    else
+                    {
+
+                    }
+
+                    if ($m > date("M"))
+                        $this->rows[0]["when"][$idx]["year"] = $m < env('FISCAL_YEAR_MONTH_FROM', 1) ? (date("Y")) : date("Y" + 1);
+                    else
+                        $this->rows[0]["when"][$idx]["year"] = $m > env('FISCAL_YEAR_MONTH_TO', 1) ? (date("Y")) : date("Y" + 1);
+                    */
+                    if ($idx > 0) {
+                        if ($this->rows[0]["when"][$idx - 1]["year"] != $this->rows[0]["when"][$idx]["year"])
+                            $desc .= " " . $this->rows[0]["when"][$idx - 1]["year"] . " ";
+                    }
+                    $desc .= " " . $this->getMonth($m); // . " " . $this->rows[0]["when"][$idx]["year"];
+                }
+                $desc .= " " . $this->rows[0]["when"][$idx]["year"];
+                $this->rows[0]["note"] = $desc;
+                $count += 1;
+            }
+
+
+            if (isset($_GET["createSubscription"]) && $_GET["createSubscription"] == 1) {
+
+                $this->createSubscription = 1;
+                $this->courseId = $_GET["courseId"];
+                $this->rateId = $_GET["rateId"];
+                $price = $_GET["subscription_price"] / 100 * 100;
+                $this->refreshAfter = 1;
+                if ($count == 1)
+                    $this->rows[] = array('causal_id' => null, 'when' => array(array('month' => date("n"), 'year' => date("Y"), 'period' => '')),  'amount' => null, 'vat_id' => null, 'note' => '', 'commercial' => 0);
+                $this->rows[$count]["causal_id"] = null;
+                $this->rows[$count]["amount"] = formatPrice($price);
+                $this->rows[$count]["note"] = "Pagamento iscrizione " . $course->name;
+                if (isset($_GET["subCausalId"])) {
+                    $this->refreshAfter = 1;
+                    $this->rows[$count]["causal_id"] = $_GET["subCausalId"];
+                }
+            }
+        }
+        $this->first = false;
+    }
+
+    public function search()
+    {
+        $this->hasFilter = true;
+    }
+
+    public function disableSearch()
+    {
+        $this->filterMember = 0;
+        $this->filterPaymentMethod = 0;
+        $this->filterCausals = 0;
+        $this->filterTo = '';
+        $this->filterFrom = '';
+        $this->filterCommercial = 0;
+        $this->hasFilter = false;
+        $this->total = 0;
+    }
+
+    public function render()
+    {
+
+        $datas = [];
+        return view('livewire.records_in', ['datas' => $datas]);
+    }
+
+    public function executeMultipleAction()
+    {
+
+        if ($this->multipleAction == 'delete')
+            $this->multipleDelete();
+    }
+
+    public function add()
+    {
+
+        $this->emit('load-select');
+        //if ($this->hasFilter)
+        $this->emit('hide-search');
+        $this->resetFields();
+        $this->dataId = 0;
+        $this->add = true;
+        $this->update = false;
+        $this->emit('setEdit', true);
+    }
+
+    public function storeCorrispettivo()
+    {
+
+        $this->emit('refresh');
+
+        $rules = [
+            'member_id' => 'required'
+        ];
+
+        $this->validate($rules);
+
+
+
+        foreach ($this->payments as $p) {
+            if ($p->corrispettivo_fiscale) {
+                $price = isset($this->corrispettivo[$p->id]) ? $this->currencyToDouble($this->corrispettivo[$p->id]) : 0;
+
+                if ($price > 0) {
+
+                    $record = \App\Models\Record::create([
+                        'member_id' => $this->member_id,
+                        'supplier_id' => null,
+                        'payment_method_id' => $p->id,
+                        'commercial' => $this->commercial,
+                        'corrispettivo_fiscale' => $this->corrispettivo_fiscale,
+                        'date' => $this->date,
+                        'type' => $this->type,
+                        'amount' => $price,
+                        'financial_movement' => $this->financial_movement,
+                        'deleted' => $this->deleted
+                    ]);
+
+                    \App\Models\RecordRow::create([
+                        'record_id' => $record->id,
+                        'causal_id' => $this->corrispettivo_causal_id,
+                        'note' => '',
+                        'amount' => $price,
+                        'vat_id' => null,
+                        'commercial' => $this->commercial,
+                        'when' => json_encode([])
+                    ]);
+                }
+            }
+        }
+
+        $this->resetFields();
+        $this->add = false;
+        $this->isDuplicate = false;
+        $this->emit('setEdit', false);
+    }
+
+    public function store($generate)
+    {
+        Log::info("Starting store method");
+        $this->emit('refresh');
+
+        $rules = [
+            'payment_method_id' => 'required',
+            'rows.*.causal_id' => 'required',
+            'rows.*.amount' => 'required'
+        ];
+
+        if (!$this->commercial)
+            $rules["member_id"] = 'required';
+
+        $this->validate($rules);
+
+        /* Validazione campi azienda per generazione ricevuta */
+        try {
+            if (!$this->financial_movement && !$this->commercial) {
+                $no_receipt_causal_azienda = false;
+                foreach ($this->rows as $row) {
+                    $no_receipt_causal_azienda = false;
+
+                    $cau = \App\Models\Causal::findOrFail($row["causal_id"]);
+                    if ($cau->no_receipt)
+                        $no_receipt_causal_azienda = true;
+                }
+
+                $azienda = \App\Models\Azienda::first();
+
+                $payment_method = \App\Models\PaymentMethod::findOrFail($this->payment_method_id);
+                if (!$payment_method->money) {
+                    if (!$no_receipt_causal_azienda) {
+                        if (!$azienda->isValid()) {
+                            session()->flash('error_ricevuta', implode("", array_map(function ($item) {
+                                return "<li>$item</li>";
+                            }, $azienda->validate())));
+                            return false;
+                        }
+                    }
+                }
+            }
+        } catch (\Exception $ex) {
+            Log::error("Error in store method: " . $ex->getMessage());
+            Log::error("Stack trace: " . $ex->getTraceAsString());
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+        /* END - Validazione campi azienda per generazione ricevuta */
+
+        try {
+
+
+            $totalGross = 0;
+            $totalSconto = 0;
+            $totalNet = 0;
+
+            foreach ($this->rows as $row) {
+                $rowAmount = $this->currencyToDouble($row["amount"]);
+                $rowSconto = isset($row['sconto']) ? $this->currencyToDouble($row['sconto']) : 0;
+                $rowNet = max(0, $rowAmount - $rowSconto);
+
+                $totalGross += $rowAmount;
+                $totalSconto += $rowSconto;
+                $totalNet += $rowNet;
+            }
+
+            Log::info("Total gross: " . $totalGross . ", Total sconto: " . $totalSconto . ", Total net: " . $totalNet);
+
+            $record = \App\Models\Record::create([
+                'member_id' => $this->member_id,
+                'supplier_id' => $this->supplier_id,
+                'payment_method_id' => $this->payment_method_id,
+                'commercial' => $this->commercial,
+                'corrispettivo_fiscale' => $this->corrispettivo_fiscale,
+                'date' => $this->date,
+                'type' => $this->type,
+                'amount' => $totalNet,
+                'prediscount_amount' => $totalGross,
+                'financial_movement' => $this->financial_movement,
+                'deleted' => $this->deleted,
+                'numero_fattura' => '',
+            ]);
+
+            $this->dataId = $record->id;
+
+            $no_receipt_causal = false;
+            $vat = 0;
+
+            foreach ($this->rows as $row) {
+                foreach ($row["when"] as $x => $y) {
+                    $row["when"][$x]['period'] = $row["when"][$x]['month'] . "-" . $row["when"][$x]['year'];
+                }
+
+                $rowAmount = $this->currencyToDouble($row["amount"]);
+                $rowSconto = isset($row['sconto']) ? $this->currencyToDouble($row['sconto']) : 0;
+                $rowNet = max(0, $rowAmount - $rowSconto);
+
+                Log::info("Row - Gross: " . $rowAmount . ", Sconto: " . $rowSconto . ", Net: " . $rowNet);
+
+                \App\Models\RecordRow::create([
+                    'record_id' => $this->dataId,
+                    'causal_id' => $row["causal_id"],
+                    'note' => $row["note"],
+                    'amount' => $rowNet,
+                    'prediscount_amount' => $rowAmount,
+                    'sconto' => $rowSconto,
+                    'vat_id' => $row["vat_id"],
+                    'commercial' => $this->commercial,
+                    'when' => json_encode($row["when"])
+                ]);
+
+                $vat += getVatValue($rowNet, $row["vat_id"]);
+
+                $cau = \App\Models\Causal::findOrFail($row["causal_id"]);
+                if ($cau->no_receipt)
+                    $no_receipt_causal = true;
+
+                if ($cau->money) {
+                    if ($rowNet >= env('MIN_MONEY', 100)) {
+                        $money = new \App\Models\Money();
+                        $money->member_id = $this->member_id;
+                        $money->record_id = $this->dataId;
+                        $money->amount = env('EXTRA_MONEY', 10);
+                        $money->date = date("Y-m-d");
+                        $money->note = env('EXTRA_MONEY', 10) . ' in più per aver caricato ' . $rowNet;
+                        $money->save();
+                    }
+                }
+            }
+
+            Log::info("Final amount saved to record: " . $totalNet);
+
+            if (!$this->financial_movement) {
+                $payment_method = \App\Models\PaymentMethod::findOrFail($this->payment_method_id);
+                if (!$payment_method->money) {
+                    if ($generate && !$no_receipt_causal)
+                        $this->createReceipt();
+                }
+            }
+
+            if ($this->rateId > 0) {
+
+                $rates = explode(",", $this->rateId);
+                foreach($rates as $rid)
+                {
+                    $rate = \App\Models\Rate::findOrFail($rid);
+                    $rate->status = 1;
+                    $rate->record_id = $record->id;
+                    $rate->save();
+                }
+
+            }
+
+            session()->flash('success', 'Movimento creato');
+            $this->resetFields();
+            $this->add = false;
+            $this->isDuplicate = false;
+            $this->emit('setEdit', false);
+        } catch (\Exception $ex) {
+            Log::error("Error in store method: " . $ex->getMessage());
+            Log::error("Stack trace: " . $ex->getTraceAsString());
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function duplicate($id)
+    {
+        $record = \App\Models\Record::findOrFail($id);
+        $newRecord = $record->replicate();
+        $newRecord->save();
+        $rows = \App\Models\RecordRow::where('record_id', $id)->get();
+        foreach ($rows as $r) {
+            $newRow = $r->replicate();
+            $newRow->record_id = $newRecord->id;
+            $newRow->save();
+        }
+        $this->edit($newRecord->id);
+        $this->isDuplicate = true;
+    }
+
+
+    public function edit($id)
+    {
+        if (!isset($_GET["from"]) && $this->fromPage == '')
+            $this->fromPage = 'in';
+        $this->emit('setEdit', true);
+        //if ($this->hasFilter)
+        $this->emit('hide-search');
+        $this->emit('load-select');
+
+        try {
+            $record = \App\Models\Record::findOrFail($id);
+            if (!$record) {
+                session()->flash('error', 'Movimento non trovato');
+            } else {
+                if ($record->member_id) {
+                    $member = \App\Models\Member::find($record->member_id);
+                    if (!$member || $member->status == 'archived') {
+                        $this->member_id = null;
+                    } else {
+                        $this->member_id = $record->member_id;
+                    }
+                } else {
+                    $this->member_id = $record->member_id;
+                }
+
+                if ($record->payment_method_id) {
+                    $paymentMethod = \App\Models\PaymentMethod::find($record->payment_method_id);
+                    if (!$paymentMethod || (isset($paymentMethod->hidden) && $paymentMethod->hidden)) {
+                        $this->payment_method_id = null;
+                    } else {
+                        $this->payment_method_id = $record->payment_method_id;
+                    }
+                } else {
+                    $this->payment_method_id = $record->payment_method_id;
+                }
+
+                $this->supplier_id = $record->supplier_id;
+                $this->amount = $record->amount;
+                $this->commercial = $record->commercial;
+                $this->corrispettivo_fiscale = $record->corrispettivo_fiscale;
+                $this->date = date("Y-m-d", strtotime($record->date));
+                $this->type = $record->type;
+                $this->deleted = $record->deleted;
+                $this->financial_movement = $record->financial_movement;
+                $this->dataId = $record->id;
+                $this->update = true;
+                $this->add = false;
+
+                $rows = \App\Models\RecordRow::where('record_id', $this->dataId)
+                    ->select('causal_id', 'note', 'commercial', 'when', 'amount', 'prediscount_amount', 'sconto', 'vat_id')
+                    ->get();
+
+                $this->rows = [];
+                foreach ($rows as $i => $r) {
+                    $grossAmount = $r->prediscount_amount ?? ($r->amount + $r->sconto);
+
+                    $causalId = $r->causal_id;
+                    if ($causalId) {
+                        $causal = \App\Models\Causal::find($causalId);
+                        if (!$causal || (isset($causal->hidden) && $causal->hidden)) {
+                            $causalId = null;
+                        }
+                    }
+
+                    $this->rows[] = [
+                        'causal_id' => $causalId,
+                        'note' => $r->note,
+                        'commercial' => $r->commercial,
+                        'when' => json_decode($r->when),
+                        'amount' => formatPrice($grossAmount),
+                        'sconto' => formatPrice($r->sconto),
+                        'vat_id' => $r->vat_id
+                    ];
+                }
+
+                $exist = \App\Models\Receipt::where('record_id', $id)->orderBy('id', 'DESC')->first();
+                if ($exist != null) {
+                    $this->currentReceip = $exist;
+                    $this->parent = $this->currentReceip->parent;
+                }
+
+                $this->refreshEditOptions();
+            }
+        } catch (\Exception $ex) {
+            Log::error("Error in edit method: " . $ex->getMessage());
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    private function refreshEditOptions()
+    {
+        $this->causals = array();
+        $visibleCausals = \App\Models\Causal::select('id', 'name')
+            ->where('parent_id', null)
+            ->where('type', 'IN')
+            ->where(function ($query) {
+                $query->where('hidden', false)->orWhereNull('hidden');
+            })
+            ->get();
+        $this->getCausale($visibleCausals, 0);
+
+        $this->payments = \App\Models\PaymentMethod::select('id', 'name')
+            ->where('enabled', true)
+            ->whereIn('type', array('ALL', 'IN'))
+            ->where(function ($query) {
+                $query->where('hidden', false)->orWhereNull('hidden');
+            })
+            ->orderBy('name')
+            ->get();
+
+        if ($this->commercial) {
+            $this->members = \App\Models\Member::select(['id', 'first_name', 'last_name', 'fiscal_code'])
+                ->where(function ($query) {
+                    $query->where('status', '!=', 'archived');
+                })
+                ->orderBy('last_name')
+                ->orderBy('first_name')
+                ->get();
+        } else {
+            $this->members = \App\Models\Member::select(['id', 'first_name', 'last_name', 'fiscal_code'])
+                ->where(function ($query) {
+                    $query->where('current_status', 2)->orWhere('current_status', 1);
+                })
+                ->where(function ($query) {
+                    $query->where('status', '!=', 'archived');
+                })
+                ->orderBy('last_name')
+                ->orderBy('first_name')
+                ->get();
+        }
+    }
+
+    public function update($generate = false)
+    {
+        Log::info("Starting update method");
+        $this->emit('refresh');
+        $rules = [
+            'payment_method_id' => 'required',
+            'rows.*.causal_id' => 'required',
+            'rows.*.amount' => 'required'
+        ];
+
+        if (!$this->commercial)
+            $rules["member_id"] = 'required';
+
+        $this->validate($rules);
+
+        /* Validazione campi azienda per generazione ricevuta */
+        try {
+            if (!$this->financial_movement && !$this->commercial) {
+                $no_receipt_causal_azienda = false;
+                foreach ($this->rows as $row) {
+                    $no_receipt_causal_azienda = false;
+
+                    $cau = \App\Models\Causal::findOrFail($row["causal_id"]);
+                    if ($cau->no_receipt)
+                        $no_receipt_causal_azienda = true;
+                }
+
+                $azienda = \App\Models\Azienda::first();
+
+                $payment_method = \App\Models\PaymentMethod::findOrFail($this->payment_method_id);
+                if (!$payment_method->money) {
+                    if (!$no_receipt_causal_azienda) {
+                        if (!$azienda->isValid()) {
+                            session()->flash('error_ricevuta', implode("", array_map(function ($item) {
+                                return "<li>$item</li>";
+                            }, $azienda->validate())));
+                            return false;
+                        }
+                    }
+                }
+            }
+        } catch (\Exception $ex) {
+            Log::error("Error in store method: " . $ex->getMessage());
+            Log::error("Stack trace: " . $ex->getTraceAsString());
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+        /* END - Validazione campi azienda per generazione ricevuta */
+
+        try {
+            $totalGross = 0;
+            $totalSconto = 0;
+            $totalNet = 0;
+
+            foreach ($this->rows as $row) {
+                $rowAmount = $this->currencyToDouble($row["amount"]);
+                $rowSconto = isset($row['sconto']) ? $this->currencyToDouble($row['sconto']) : 0;
+                $rowNet = max(0, $rowAmount - $rowSconto);
+
+                $totalGross += $rowAmount;
+                $totalSconto += $rowSconto;
+                $totalNet += $rowNet;
+            }
+
+            Log::info("Total gross: " . $totalGross . ", Total sconto: " . $totalSconto . ", Total net: " . $totalNet);
+
+            \App\Models\Record::whereId($this->dataId)->update([
+                'member_id' => $this->member_id,
+                'supplier_id' => $this->supplier_id,
+                'payment_method_id' => $this->payment_method_id,
+                'commercial' => $this->commercial,
+                'corrispettivo_fiscale' => $this->corrispettivo_fiscale,
+                'date' => date("Y-m-d", strtotime($this->date)),
+                'type' => $this->type,
+                'financial_movement' => $this->financial_movement,
+                'deleted' => $this->deleted,
+                'amount' => $totalNet,
+                'prediscount_amount' => $totalGross,
+            ]);
+
+            $no_receipt_causal = false;
+            $vat = 0;
+
+            \App\Models\RecordRow::where('record_id', $this->dataId)->delete();
+
+            foreach ($this->rows as $row) {
+                foreach ($row["when"] as $x => $y) {
+                    $row["when"][$x]['period'] = $row["when"][$x]['month'] . "-" . $row["when"][$x]['year'];
+                }
+
+                $rowAmount = $this->currencyToDouble($row["amount"]);
+                $rowSconto = isset($row['sconto']) ? $this->currencyToDouble($row['sconto']) : 0;
+                $rowNet = max(0, $rowAmount - $rowSconto);
+
+                Log::info("Row - Gross: " . $rowAmount . ", Sconto: " . $rowSconto . ", Net: " . $rowNet);
+
+                \App\Models\RecordRow::create([
+                    'record_id' => $this->dataId,
+                    'causal_id' => $row["causal_id"],
+                    'note' => $row["note"],
+                    'vat_id' => $row["vat_id"],
+                    'amount' => $rowNet,
+                    'prediscount_amount' => $rowAmount,
+                    'sconto' => $rowSconto,
+                    'commercial' => $this->commercial,
+                    'when' => json_encode($row["when"])
+                ]);
+
+                $vat += getVatValue($rowNet, $row["vat_id"]);
+
+                $cau = \App\Models\Causal::findOrFail($row["causal_id"]);
+                if ($cau->no_receipt)
+                    $no_receipt_causal = true;
+            }
+
+            Log::info("Final amount saved to record: " . $totalNet);
+
+            if (!$this->financial_movement) {
+                $payment_method = \App\Models\PaymentMethod::findOrFail($this->payment_method_id);
+                if (!$payment_method->money) {
+                    if ($generate && !$no_receipt_causal)
+                        $this->createReceipt();
+                }
+            }
+            session()->flash('success', 'Movimento aggiornato');
+            $this->resetFields();
+            $this->update = false;
+            $this->isDuplicate = false;
+            $this->emit('setEdit', false);
+        } catch (\Exception $ex) {
+            Log::error("Error in update method: " . $ex->getMessage());
+            Log::error("Stack trace: " . $ex->getTraceAsString());
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function rigenerate()
+    {
+        $this->emit('refresh');
+
+        try {
+
+            $record = \App\Models\Record::findOrFail($this->dataId);
+            $newRecord = $record->replicate();
+            $newRecord->date = date("Y-m-d");
+            $newRecord->save();
+
+            $recordRows = \App\Models\RecordRow::where('record_id', $this->dataId)->get();
+            // Inserisco le righe
+            foreach ($recordRows as $row) {
+                $newRow = $row->replicate();
+                $newRow->record_id = $newRecord->id;
+                $newRow->save();
+            }
+
+            $this->dataId = $newRecord->id;
+
+            $this->createReceipt();
+
+            session()->flash('success', 'Movimento aggiornato');
+            $this->resetFields();
+            $this->update = false;
+            $this->isDuplicate = false;
+            $this->emit('setEdit', false);
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function cancel()
+    {
+        // Se arrivo da duplica elimino
+        if ($this->isDuplicate) {
+            try {
+                \App\Models\Record::find($this->dataId)->delete();
+                session()->flash('success', "Movimento eliminato");
+            } catch (\Exception $e) {
+                session()->flash('error', 'Errore (' . $e->getMessage() . ')');
+            }
+        }
+        $this->isDuplicate = false;
+
+        $this->add = false;
+        $this->update = false;
+        $this->resetFields();
+        $this->emit('setEdit', false);
+        return redirect()->to('/in');
+    }
+
+    public function delete($id)
+    {
+        try {
+
+            $r = \App\Models\Record::find($id);
+
+            // Annullo la ricevuta
+            $receipt = \App\Models\Receipt::where('record_id', $r->id)->first();
+            if ($receipt) {
+                $receipt->status = 99;
+                sendReceiptDeleteEmail($receipt);
+                $receipt->save();
+            }
+
+            if ($r->member_course_id > 0) {
+                $months = json_decode($r->months);
+                $c = \App\Models\MemberCourse::findOrFail($r->member_course_id);
+                $xxx = json_decode($c->months);
+                foreach ($xxx as $idx => $mm) {
+                    if (in_array($mm->m, $months)) {
+                        $xxx[$idx]->status = "";
+                    }
+                }
+                $c->months = json_encode($xxx);
+                $c->save();
+            }
+
+            $r->deleted = true;
+            $r->save();
+
+            session()->flash('success', 'Movimento aggiornato');
+            $this->resetFields();
+            $this->update = false;
+            $this->isDuplicate = false;
+            $this->emit('setEdit', false);
+
+            $this->emit('reload');
+
+            //$this->emit('load-data-table');
+
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function multipleDelete()
+    {
+        try {
+            foreach ($this->multipleIds as $id) {
+                \App\Models\Record::find($id)->delete();
+            }
+        } catch (\Exception $e) {
+            session()->flash('error', 'Errore (' . $e->getMessage() . ')');
+        }
+        $this->multipleAction = '';
+    }
+
+    public function createMember()
+    {
+
+        $this->newMemberFiscalCodeExist = false;
+        $this->validate([
+            // 'newMemberFiscalCode'=>'required|max:16',
+            'newMemberFirstName' => 'required',
+            'newMemberLastName' => 'required',
+            //'newMemberEmail'=>'required',
+        ]);
+
+        // Check fiscal code exist
+        $exist = false;
+        if ($this->newMemberFiscalCode != '') {
+            $check = \App\Models\Member::where('fiscal_code', $this->newMemberFiscalCode)->get();
+            $exist = $check->count() > 0;
+        }
+        if (!$exist) {
+            $member = \App\Models\Member::create([
+                'first_name' => strtoupper($this->newMemberFirstName),
+                'last_name' => strtoupper($this->newMemberLastName),
+                'email' => strtoupper($this->newMemberEmail),
+                'to_complete' => $this->newMemberToComplete,
+                'fiscal_code' => $this->newMemberFiscalCode,
+                'status' => true
+            ]);
+            $this->member_id = $member->id;
+            $this->members = \App\Models\Member::select(['id', 'first_name', 'last_name', 'fiscal_code'])->get();
+            // $this->emit('reloadMembers');
+            $this->emit('refresh');
+            $this->newMemberFirstName = '';
+            $this->newMemberLastName = '';
+            $this->newMemberEmail = '';
+            $this->newMemberToComplete = false;
+            $this->newMemberFiscalCode = '';
+            $this->emit('saved');
+            //$this->validate();
+            $this->selectId++;
+        } else {
+            $this->newMemberFiscalCodeExist = true;
+        }
+    }
+
+    public function createReceipt()
+    {
+        // Ulteriore controllo commerciale/non commerciale
+        if (!$this->commercial) {
+            $create = false;
+            $receipt = \App\Models\Receipt::where('record_id', $this->dataId)->orderBy('id', 'DESC')->first();
+            if ($receipt != null) {
+
+                // Controllo lo stato, se 99 ne genero una nuova
+                if ($receipt->status == 99) {
+                    $create = true;
+                }
+            } else {
+                $create = true;
+            }
+
+            if ($create) {
+                $number = 1;
+                $exist = \App\Models\Receipt::where('year', date("Y"))->orderBy('number', 'DESC')->first();
+                if ($exist != null)
+                    $number = $exist->number + 1;
+
+                // Calculate totals for the receipt
+                $totalGross = 0;
+                $totalNet = 0;
+                foreach ($this->rows as $row) {
+                    $rowAmount = $this->currencyToDouble($row["amount"]);
+                    $rowSconto = isset($row['sconto']) ? $this->currencyToDouble($row['sconto']) : 0;
+                    $rowNet = max(0, $rowAmount - $rowSconto);
+
+                    $totalGross += $rowAmount;
+                    $totalNet += $rowNet;
+                }
+
+                $receipt = \App\Models\Receipt::create([
+                    'record_id' => $this->dataId,
+                    'member_id' => $this->member_id,
+                    'supplier_id' => $this->supplier_id,
+                    'payment_method_id' => $this->payment_method_id,
+                    'number' => $number,
+                    'date' => $this->date,
+                    'year' => date("Y"),
+                    'type' => $this->type,
+                    'parent' => $this->parent,
+                    'status' => 1,
+                ]);
+
+                foreach ($this->rows as $row) {
+                    $rowAmount = $this->currencyToDouble($row["amount"]);
+                    $rowSconto = isset($row['sconto']) ? $this->currencyToDouble($row['sconto']) : 0;
+                    $rowNet = max(0, $rowAmount - $rowSconto);
+
+                    Log::info("Receipt Row - Gross: " . $rowAmount . ", Sconto: " . $rowSconto . ", Net: " . $rowNet);
+
+                    \App\Models\ReceiptRow::create([
+                        'receip_id' => $receipt->id,
+                        'causal_id' => $row["causal_id"],
+                        'note' => $row["note"],
+                        'vat_id' => $row["vat_id"],
+                        'amount' => $rowNet,
+                        'prediscount_amount' => $rowAmount,
+                        'sconto' => $rowSconto,
+                        'commercial' => $row["commercial"],
+                        'when' => json_encode($row["when"])
+                    ]);
+                }
+
+                $this->currentReceip = $receipt;
+
+                sendReceiptEmail($receipt);
+
+                session()->flash('receipt', "Ricevuta " . $number . "/" . date("Y") . " creata correttamente");
+
+                $this->emit('showReceipt', $this->currentReceip->id);
+            }
+        }
+    }
+
+    public function removeReceipt()
+    {
+        $receipt = \App\Models\Receipt::findOrFail($this->currentReceip->id);
+        $receipt->status = 99;
+        $receipt->save();
+        sendReceiptDeleteEmail($receipt);
+        $this->currentReceip = $receipt;
+    }
+
+    function currencyToDouble($val)
+    {
+        $x = str_replace("€", "", $val);
+        $x = str_replace(".", "", $x);
+        $x = str_replace(",", ".", $x);
+        return floatval(trim($x));
+    }
+
+
+    public function addRow()
+    {
+        $this->rows[] = array('causal_id' => null, 'when' => array(array('month' => date("n"), 'year' => date("Y"), 'period' => '')),  'amount' => null, 'vat_id' => null, 'note' => '', 'commercial' => 0);
+        $this->emit('load-select');
+    }
+
+    public function delRow($idx)
+    {
+        unset($this->rows[$idx]);
+        // $this->emit('load-select');
+    }
+
+    public function addPeriod($idx)
+    {
+        $x = sizeof($this->rows[$idx]['when']) - 1;
+        $newDate = \Carbon\Carbon::create($this->rows[$idx]['when'][$x]["year"] . "-" . $this->rows[$idx]['when'][$x]["month"] . '-01')->addMonth();
+        $this->rows[$idx]['when'][] = array('month' => $newDate->format("n"), 'year' => $newDate->format("Y"), 'period' => '');
+    }
+
+    public function delPeriod($idx, $xxx)
+    {
+        array_splice($this->rows[$idx]['when'], $xxx, $xxx);
+        //unset($this->rows[$idx]['when'][$xxx]);
+        // $this->emit('load-select');
+    }
+    public function getTotal()
+    {
+        $total = 0.00;
+
+        foreach ($this->rows as $r) {
+            if ($r["amount"] != null && $r["amount"] != "") {
+                $rowAmount = $this->currencyToDouble($r["amount"]);
+                $rowSconto = isset($r['sconto']) ? $this->currencyToDouble($r['sconto']) : 0;
+                $netAmount = max(0, $rowAmount - $rowSconto);
+
+                $total += $netAmount;
+
+                if ($r["vat_id"] > 0)
+                    $total += getVatValue($netAmount, $r["vat_id"]);
+            }
+        }
+
+        return formatPrice($total);
+    }
+    public function getTotalCorrispettivo()
+    {
+        $total = 0.00;
+        foreach ($this->corrispettivo as $r) {
+            $total += $this->currencyToDouble($r);
+        }
+        return formatPrice($total);
+        // $this->emit('load-select');
+    }
+
+    public function getPrice()
+    {
+        $total = 0.00;
+        foreach ($this->rows as $r) {
+            if ($r["amount"] != null && $r["amount"] != "")
+                $total += $this->currencyToDouble($r["amount"]);
+        }
+        return formatPrice($total);
+        // $this->emit('load-select');
+    }
+
+    public function getVat()
+    {
+        $total = 0.00;
+        foreach ($this->rows as $r) {
+            if ($r["amount"] != null && $r["amount"] != "" && $r["vat_id"] > 0)
+                $total += getVatValue($this->currencyToDouble($r["amount"]), $r["vat_id"]);
+        }
+        return formatPrice($total);
+        // $this->emit('load-select');
+    }
+
+    public function getVats()
+    {
+        $vats = array();
+        foreach ($this->rows as $r) {
+            if ($r["amount"] != null && $r["amount"] != "" && $r["vat_id"] > 0) {
+                $vat = getVatValue($this->currencyToDouble($r["amount"]), $r["vat_id"]);
+                $vatName = "";
+                foreach ($this->vats as $v) {
+                    if ($v->id == $r["vat_id"])
+                        $vatName = $v->name;
+                }
+                if (isset($vats[$vatName]))
+                    $vats[$vatName] += $vat;
+                else
+                    $vats[$vatName] = $vat;
+            }
+        }
+        return $vats;
+        // $this->emit('load-select');
+    }
+
+    public function setCausal($id, $idx)
+    {
+        $this->rows[$idx]["causal_id"] = $id;
+    }
+
+    public function printReceipt()
+    {
+        //$pdf = PDF::loadView('pdf/receipt', array('datas' => $datas, 'from' => $x, 'to' => $y, 'who' => '', 'matricola' => $matricola));
+        $pdf = PDF::loadView('receipt', array('receipt' => $this->currentReceip)); //->output();
+        $pdfName = "Ricevuta_" . $this->currentReceip->member->last_name . "_" . $this->currentReceip->number . "_" . $this->currentReceip->year . ".pdf";
+        return $pdf->stream();
+        /*return response()->streamDownload(
+            fn () => print($pdf),
+            "ricevuta_" . $this->currentReceip->number . "_" . $this->currentReceip->year . ".pdf"
+        );*/
+        /*return response()->streamDownload(function () {
+            echo $pdf->stream();
+        }, 'test.pdf');*/
+    }
+
+    public function multiPeriod()
+    {
+        $this->multiP = true;
+    }
+
+    public function multiPeriodCreate($idx)
+    {
+
+        $period = \Carbon\CarbonPeriod::create($this->multiYearFrom . '-' . $this->multiMonthFrom . '-01', '1 month', $this->multiYearTo . '-' . $this->multiMonthTo . '-01');
+        $this->rows[$idx]['when'] = [];
+        foreach ($period as $dt) {
+            if (!in_array(array('month' => $dt->format("n"), 'year' => $dt->format("Y"), 'period' => ''), $this->rows[$idx]['when']))
+                $this->rows[$idx]['when'][] = array('month' => $dt->format("n"), 'year' => $dt->format("Y"), 'period' => '');
+        }
+
+        $this->multiP = false;
+    }
+
+    public function multiPeriodCancel()
+    {
+        $this->multiP = false;
+    }
+
+    public function getMonth($m)
+    {
+        $ret = '';
+        switch ($m) {
+            case 1:
+                $ret = 'Gennaio';
+                break;
+            case 2:
+                $ret = 'Febbraio';
+                break;
+            case 3:
+                $ret = 'Marzo';
+                break;
+            case 4:
+                $ret = 'Aprile';
+                break;
+            case 5:
+                $ret = 'Maggio';
+                break;
+            case 6:
+                $ret = 'Giugno';
+                break;
+            case 7:
+                $ret = 'Luglio';
+                break;
+            case 8:
+                $ret = 'Agosto';
+                break;
+            case 9:
+                $ret = 'Settembre';
+                break;
+            case 10:
+                $ret = 'Ottobre';
+                break;
+            case 11:
+                $ret = 'Novembre';
+                break;
+            case 12:
+                $ret = 'Dicembre';
+                break;
+            default:
+                $ret = '';
+                break;
+        }
+        return $ret;
+    }
+
+    public function setYear($idx, $xxx)
+    {
+        $m = $this->rows[$idx]["when"][$xxx]["month"];
+        $this->rows[$idx]["when"][$xxx]["year"] = $m < env('FISCAL_YEAR_MONTH_FROM', 1) ? (date("Y") + 1) : date("Y");
+    }
+}

+ 811 - 0
app/Http/Livewire/RecordINOUT.php

@@ -0,0 +1,811 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\Cell\DataType;
+use Illuminate\Support\Facades\Log;
+use SimpleXMLElement;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Storage;
+use App\Http\Middleware\TenantMiddleware;
+
+class RecordINOUT extends Component
+{
+    //public $records_in, $records_out;
+    public $total_in = 0;
+    public $total_out = 0;
+
+    public $datas = [];
+    public $month;
+    public $year;
+
+    public $months_1 = [];
+    public $year_1;
+
+    public $months_2 = [];
+    public $year_2;
+    public $show_block_2;
+
+    //public $borsellino_id = 0;
+
+    public $columns = array();
+    public $rows_in = array();
+    public $rows_out = array();
+    public $records_in = array();
+    public $records_out = array();
+
+    public $showData = true;
+
+    public $hasFilter = false;
+
+    public $total = 0;
+
+    public $selectedFilter = '';
+
+    public $causalsIn = array();
+    public $causalsOut = array();
+    public $payments = array();
+    public $members = array();
+    public $filterCausalsIn = null;
+    public $filterCausalsOut = null;
+
+    public $excludeCausals = array();
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+    }
+
+
+    public function mount()
+    {
+
+        if (Auth::user()->level != env('LEVEL_ADMIN', 0))
+            return redirect()->to('/dashboard');
+
+        $borsellino = \App\Models\Causal::where('money', true)->first();
+        if ($borsellino)
+            $this->excludeCausals[] = $borsellino->id;
+        //$this->borsellino_id = $borsellino->id;
+
+        // Aggiungo
+        $excludes = \App\Models\Causal::where('no_records', true)->get();
+        foreach ($excludes as $e) {
+            $this->excludeCausals[] = $e->id;
+        }
+
+
+        $this->month = date("m");
+        $this->year = date("Y");
+        $this->year_1 = date("Y");
+        $this->year_2 = date("Y");
+        $this->show_block_2 = false;
+
+        $this->getCausale(\App\Models\Causal::select('id', 'name')->where('parent_id', null)->where('type', 'IN')->whereNotIn('id', $this->excludeCausals)->get(), 'IN', 0);
+        $this->getCausale(\App\Models\Causal::select('id', 'name')->where('parent_id', null)->where('type', 'OUT')->whereNotIn('id', $this->excludeCausals)->get(), 'OUT', 0);
+
+        $this->getCausalsIn(\App\Models\Causal::select('id', 'name')->where('parent_id', null)->where('type', 'IN')->get(), 0);
+
+        $this->getCausalsOut(\App\Models\Causal::select('id', 'name')->where('parent_id', null)->where('type', 'OUT')->get(), 0);
+
+        //$this->causalsIn = \App\Models\Causal::where('parent_id', null)->where('type', 'IN')->whereNotIn('id', $this->excludeCausals)->get();
+        //$this->causalsOut = \App\Models\Causal::where('parent_id', null)->where('type', 'OUT')->whereNotIn('id', $this->excludeCausals)->get();
+    }
+
+    public function getCausalsIn($records, $indentation)
+    {
+        foreach ($records as $record) {
+            $this->causalsIn[] = array('id' => $record->id, 'name' => $record->getTree(), 'text' => $record->getTree(), 'level' => $indentation);
+            if (count($record->childs))
+                $this->getCausalsIn($record->childs, $indentation + 1);
+        }
+    }
+
+    public function getCausalsOut($records, $indentation)
+    {
+        foreach ($records as $record) {
+            $this->causalsOut[] = array('id' => $record->id, 'name' => $record->getTree(), 'text' => $record->getTree(), 'level' => $indentation);
+            if (count($record->childs))
+                $this->getCausalsOut($record->childs, $indentation + 1);
+        }
+    }
+
+    public function getCausale($records, $type, $indentation)
+    {
+        foreach ($records as $record) {
+            $first_parent_id = null;
+            if ($record->parent_id != null) {
+                $first_parent_id = \App\Models\Causal::where('id', $record->parent_id)->first()->parent_id;
+            }
+            if ($type == 'IN')
+                $this->rows_in[] = array('id' => $record->id, 'name' => $record->name, 'level' => $indentation, 'parent_id' => $record->parent_id, 'parent_name' => $this->getCausalName($record->parent_id), 'first_parent_id' => $first_parent_id, 'first_parent_name' => $this->getCausalName($first_parent_id), 'all_childs' => $this->getAllChild($record->id));
+            if ($type == 'OUT')
+                $this->rows_out[] = array('id' => $record->id, 'name' => $record->name, 'level' => $indentation, 'parent_id' => $record->parent_id, 'parent_name' => $this->getCausalName($record->parent_id), 'first_parent_id' => $first_parent_id, 'first_parent_name' => $this->getCausalName($first_parent_id), 'all_childs' => $this->getAllChild($record->id));
+            if (count($record->childs))
+                $this->getCausale($record->childs, $type, $indentation + 1);
+        }
+    }
+
+    public function getAllChild($id)
+    {
+        $aChilds = array();
+        $aChilds[] = $id;
+
+
+        $record = \App\Models\Causal::findOrFail($id);
+        $aChilds[] = $record->parent_id;
+        if ($record->parent_id != null) {
+            $first_parent_id = \App\Models\Causal::where('id', $record->parent_id)->first()->parent_id;
+            $aChilds[] = $first_parent_id;
+        }
+
+        $childs = \App\Models\Causal::where('parent_id', $id)->get();
+        foreach ($childs as $child) {
+            $aChilds[] = $child->id;
+            $childs2 = \App\Models\Causal::where('parent_id', $child->id)->get();
+            foreach ($childs2 as $child2) {
+                $aChilds[] = $child2->id;
+                $childs3 = \App\Models\Causal::where('parent_id', $child2->id)->get();
+                foreach ($childs3 as $child3) {
+                    $aChilds[] = $child3->id;
+                }
+            }
+        }
+        return $aChilds;
+    }
+
+
+    public function getCausalName($id)
+    {
+        if ($id > 0)
+            return \App\Models\Causal::findOrFail($id)->name;
+        else
+            return "";
+    }
+
+    public function render()
+    {
+        return view('livewire.records_in_out');
+    }
+
+    public function hydrate()
+    {
+        $this->emit('load-select');
+    }
+
+    public function show($m, $y)
+    {
+        if ($m != "" && $y != "" && !in_array($m . "-" . $y, $this->datas))
+            $this->datas[] = $m . "-" . $y;
+
+        $this->columns = array();
+        $this->records_in = [];
+        $this->records_out = [];
+
+        $exclude_from_records = \App\Models\Member::where('exclude_from_records', true)->pluck('id')->toArray();
+
+        if (sizeof($this->datas) > 0) {
+            foreach ($this->datas as $filter) {
+                $this->columns[] = $filter;
+
+                $f = $filter;
+                if ($m == 'x')
+                    $f = str_replace("x-", "", $filter);
+                $records = \App\Models\Record::where('type', 'IN')
+                    ->join('records_rows', 'records.id', '=', 'records_rows.record_id')
+                    ->whereNotIn('records_rows.causal_id', $this->excludeCausals)
+                    ->where(function ($query) {
+                        $query->where('deleted', false)->orWhere('deleted', null);
+                    })
+                    ->where(function ($query) {
+                        $query->where('financial_movement', false)->orWhere('financial_movement', null);
+                    })
+                    ->whereNotIn('member_id', $exclude_from_records)
+                    ->where('records_rows.when', 'like', '%"' . $f . '"%')
+                    ->get();
+
+                //Log::info('Record In' . $records);
+                foreach ($records as $record) {
+                    $amount = $record->amount;
+                    $amount += getVatValue($amount, $record->vat_id);
+                    $when = sizeof(json_decode($record->when));
+                    if ($when > 1) {
+                        $amount = $amount / $when;
+                        $record->amount = $amount;
+                    }
+                    // Aggiungo/aggiorno i dati
+                    if (isset($this->records_in[$filter][$record->causal_id]))
+                        $this->records_in[$filter][$record->causal_id] += $amount;
+                    else
+                        $this->records_in[$filter][$record->causal_id] = $amount;
+                    // Aggiorno i dati del padre
+                    $this->updateParent("IN", $record->causal_id, $amount, $filter);
+                }
+                $records = \App\Models\Record::where('type', 'OUT')
+                    ->join('records_rows', 'records.id', '=', 'records_rows.record_id')
+                    ->whereNotIn('records_rows.causal_id', $this->excludeCausals)
+                    ->where(function ($query) {
+                        $query->where('deleted', false)->orWhere('deleted', null);
+                    })
+
+                    ->where(function ($query) use ($exclude_from_records) {
+                        $query->whereNull('member_id')
+                            ->orWhereNotIn('member_id', $exclude_from_records);
+                    })
+                    ->where('records_rows.when', 'like', '%"' . $f . '"%')->get();
+                Log::info('Record Out' . $records);
+                foreach ($records as $record) {
+                    $amount = $record->amount;
+                    $amount += getVatValue($amount, $record->vat_id);
+                    $when = sizeof(json_decode($record->when));
+                    if ($when > 1) {
+                        $amount = $amount / $when;
+                        $record->amount = $amount;
+                    }
+                    // Aggiungo/aggiorno i dati
+                    if (isset($this->records_out[$filter][$record->causal_id]))
+                        $this->records_out[$filter][$record->causal_id] += $amount;
+                    else
+                        $this->records_out[$filter][$record->causal_id] = $amount;
+                    $this->updateParent("OUT", $record->causal_id, $amount, $filter);
+                }
+            }
+        }
+
+        //$this->showData = true;
+        $this->emit('load-table');
+    }
+
+    public function updateParent($type, $causal_id, $amount, $filter)
+    {
+        if ($type == "IN") {
+            foreach ($this->rows_in as $r) {
+                if ($r["id"] == $causal_id) {
+                    if (isset($this->records_in[$filter][$r["parent_id"]]))
+                        $this->records_in[$filter][$r["parent_id"]] += $amount;
+                    else
+                        $this->records_in[$filter][$r["parent_id"]] = $amount;
+                    if ($r["parent_id"] > 0)
+                        $this->updateParent("IN", $r["parent_id"], $amount, $filter);
+                }
+            }
+        }
+        if ($type == "OUT") {
+            foreach ($this->rows_out as $r) {
+                if ($r["id"] == $causal_id) {
+                    if (isset($this->records_out[$filter][$r["parent_id"]]))
+                        $this->records_out[$filter][$r["parent_id"]] += $amount;
+                    else
+                        $this->records_out[$filter][$r["parent_id"]] = $amount;
+                    if ($r["parent_id"] > 0)
+                        $this->updateParent("OUT", $r["parent_id"], $amount, $filter);
+                }
+            }
+        }
+    }
+
+    public function updateParentYear($type, $causal_id, $amount, $filter, &$records_in, &$records_out)
+    {
+        if ($type == "IN") {
+            foreach ($this->rows_in as $r) {
+                if ($r["id"] == $causal_id) {
+                    if (isset($records_in[$filter][$r["parent_id"]]))
+                        $records_in[$filter][$r["parent_id"]] += $amount;
+                    else
+                        $records_in[$filter][$r["parent_id"]] = $amount;
+                    if ($r["parent_id"] > 0)
+                        $this->updateParentYear("IN", $r["parent_id"], $amount, $filter, $records_in, $records_out);
+                }
+            }
+        }
+        if ($type == "OUT") {
+            foreach ($this->rows_out as $r) {
+                if ($r["id"] == $causal_id) {
+                    if (isset($records_out[$filter][$r["parent_id"]]))
+                        $records_out[$filter][$r["parent_id"]] += $amount;
+                    else
+                        $records_out[$filter][$r["parent_id"]] = $amount;
+                    if ($r["parent_id"] > 0)
+                        $this->updateParentYear("OUT", $r["parent_id"], $amount, $filter, $records_in, $records_out);
+                }
+            }
+        }
+    }
+
+    public function getCausal($causal)
+    {
+        $ret = '';
+        if ($causal > 0) {
+            $ret = \App\Models\Causal::findOrFail($causal)->getTree();
+        }
+        return $ret;
+    }
+
+    public function getMonth($str)
+    {
+        $ret = '';
+        list($m, $y) = explode("-", $str);
+        switch ($m) {
+            case 'x':
+                $ret = '';
+                break;
+            case '01':
+                $ret = 'Gennaio ';
+                break;
+            case '02':
+                $ret = 'Febbraio ';
+                break;
+            case '03':
+                $ret = 'Marzo ';
+                break;
+            case '04':
+                $ret = 'Aprile ';
+                break;
+            case '05':
+                $ret = 'Maggio ';
+                break;
+            case '06':
+                $ret = 'Giugno ';
+                break;
+            case '07':
+                $ret = 'Luglio ';
+                break;
+            case '08':
+                $ret = 'Agosto ';
+                break;
+            case '09':
+                $ret = 'Settembre ';
+                break;
+            case '10':
+                $ret = 'Ottobre ';
+                break;
+            case '11':
+                $ret = 'Novembre ';
+                break;
+            case '12':
+                $ret = 'Dicembre ';
+                break;
+            default:
+                $ret = '';
+                break;
+        }
+        $ret .= $y;
+        return $ret;
+    }
+
+    public function clear()
+    {
+        $this->columns = [];
+        $this->datas = [];
+        $this->records_out = [];
+        $this->records_in = [];
+        $this->emit('load-select');
+        $this->emit('reset-collapse');
+        //$this->showData = false;
+    }
+
+    public function remove($idx)
+    {
+
+        if (sizeof($this->datas) > 1)
+            array_splice($this->datas, $idx, 1);
+        //unset($this->datas[$idx]);
+        else
+            $this->datas = array();
+
+        $this->show('', '');
+    }
+
+    public function export()
+    {
+
+        $rows_in = array();
+        $rows_out = array();
+
+        if ($this->filterCausalsIn != null && sizeof($this->filterCausalsIn) > 0) {
+            foreach ($this->rows_in as $r) {
+                if (in_array($r["id"], $this->filterCausalsIn) || in_array($r["parent_id"], $this->filterCausalsIn) || in_array($r["first_parent_id"], $this->filterCausalsIn)) {
+                    $rows_in[] = $r;
+                }
+            }
+        } else {
+            $rows_in = $this->rows_in;
+        }
+
+        if ($this->filterCausalsOut != null && sizeof($this->filterCausalsOut) > 0) {
+            foreach ($this->rows_out as $r) {
+                if (in_array($r["id"], $this->filterCausalsOut) || in_array($r["parent_id"], $this->filterCausalsOut) || in_array($r["first_parent_id"], $this->filterCausalsOut)) {
+                    $rows_out[] = $r;
+                }
+            }
+        } else {
+            $rows_out = $this->rows_out;
+        }
+
+        $result = $this->generateExcel($this->columns, $rows_in, $this->records_in, $rows_out, $this->records_out, false);
+
+        if ($result['storage_type'] === 's3') {
+            try {
+                $disk = Storage::disk('s3');
+                $downloadUrl = $disk->temporaryUrl($result['path'], now()->addHour());
+
+                return redirect($downloadUrl);
+            } catch (\Exception $e) {
+                Log::error('Error generating S3 download URL for export', [
+                    'error' => $e->getMessage(),
+                    'path' => $result['path']
+                ]);
+                session()->flash('error', 'Errore durante la generazione del link di download. Riprova.');
+                return back();
+            }
+        } else {
+            return response()->download($result['path'])->deleteFileAfterSend();
+        }
+    }
+
+    public function exportYear($year)
+    {
+        $records_in = [];
+        $records_out = [];
+        $datas = [];
+        if (env('FISCAL_YEAR_MONTH_FROM', 1) > 1) {
+            if (date("m") < env('FISCAL_YEAR_MONTH_FROM', 1))
+                $year -= 1;
+            for ($m = env('FISCAL_YEAR_MONTH_FROM', 1); $m <= 12; $m++) {
+                $datas[] = $m . "-" . $year;
+            }
+            for ($m = 1; $m <= env('FISCAL_YEAR_MONTH_TO', 12); $m++) {
+                $datas[] = $m . "-" . ($year + 1);
+            }
+        } else {
+            for ($m = 1; $m <= 12; $m++) {
+                $datas[] = $m . "-" . $year;
+            }
+        }
+
+        $exclude_from_records = \App\Models\Member::where('exclude_from_records', true)->pluck('id')->toArray();
+
+        foreach ($datas as $filter) {
+
+            $columns[] = $filter;
+
+            $records = \App\Models\Record::where('type', 'IN')
+                ->join('records_rows', 'records.id', '=', 'records_rows.record_id')
+                ->whereNotIn('records_rows.causal_id', $this->excludeCausals)
+                ->where(function ($query) {
+                    $query->where('deleted', false)->orWhere('deleted', null);
+                })
+                ->where(function ($query) {
+                    $query->where('financial_movement', false)->orWhere('financial_movement', null);
+                })
+                ->whereNotIn('member_id', $exclude_from_records)
+                ->where('records_rows.when', 'like', '%"' . $filter . '"%')->get();
+            //$records = $records->orderBy('date', 'DESC')->get();
+
+            foreach ($records as $record) {
+                $amount = $record->amount;
+                $amount += getVatValue($amount, $record->vat_id);
+                $when = sizeof(json_decode($record->when));
+                if ($when > 1) {
+                    $amount = $amount / $when;
+                    $record->amount = $amount;
+                }
+                // Aggiungo/aggiorno i dati
+                if (isset($records_in[$filter][$record->causal_id]))
+                    $records_in[$filter][$record->causal_id] += $amount;
+                else
+                    $records_in[$filter][$record->causal_id] = $amount;
+                // Aggiorno i dati del padre
+                $this->updateParentYear("IN", $record->causal_id, $amount, $filter, $records_in, $records_out);
+            }
+
+            $records = \App\Models\Record::where('type', 'OUT')
+                ->join('records_rows', 'records.id', '=', 'records_rows.record_id')
+                ->whereNotIn('records_rows.causal_id', $this->excludeCausals)
+                ->where(function ($query) {
+                    $query->where('deleted', false)->orWhere('deleted', null);
+                })
+                ->where(function ($query) use ($exclude_from_records) {
+                    $query->whereNull('member_id')
+                        ->orWhereNotIn('member_id', $exclude_from_records);
+                })
+                ->where('records_rows.when', 'like', '%"' . $filter . '"%')->get();
+            //$records = $records->orderBy('date', 'DESC')->get();
+            foreach ($records as $record) {
+                $amount = $record->amount;
+                $when = sizeof(json_decode($record->when));
+                if ($when > 1) {
+                    $amount = $amount / $when;
+                    $record->amount = $amount;
+                }
+                // Aggiungo/aggiorno i dati
+                if (isset($records_out[$filter][$record->causal_id]))
+                    $records_out[$filter][$record->causal_id] += $amount;
+                else
+                    $records_out[$filter][$record->causal_id] = $amount;
+                $this->updateParentYear("OUT", $record->causal_id, $amount, $filter, $records_in, $records_out);
+            }
+        }
+
+        $result = $this->generateExcel($columns, $this->rows_in, $records_in, $this->rows_out, $records_out, true);
+
+        if ($result['storage_type'] === 's3') {
+            $disk = Storage::disk('s3');
+            $downloadUrl = $disk->temporaryUrl($result['path'], now()->addHour());
+            return redirect($downloadUrl);
+        } else {
+            return response()->download($result['path'])->deleteFileAfterSend();
+        }
+    }
+
+    public function generateExcel($columns, $rows_in, $records_in, $rows_out, $records_out, $isYearExport)
+    {
+        $writeNumber = function ($sheet, string $cell, $value, string $format = '#,##0.00') {
+            $sheet->setCellValueExplicit($cell, (float)$value, DataType::TYPE_NUMERIC);
+            $sheet->getStyle($cell)->getNumberFormat()->setFormatCode($format);
+        };
+
+        $shouldCountFn = function (array $row, array $presentParentIds): bool {
+            $level = $row['level'] ?? 0;
+            if ($level == 0) return true; // padre sempre incluso
+
+            $pid = $row['parent_id'] ?? null;
+            // se non c'è il padre includi il figlio
+            return !in_array($pid, $presentParentIds, true);
+        };
+
+        $spreadsheet = new Spreadsheet();
+        $activeWorksheet = $spreadsheet->getActiveSheet();
+
+        // Mappa colonne dinamica
+        $monthCount   = count($columns);
+        $nameColIdx   = 1;
+        $firstDataIdx = 2;
+        $lastDataIdx  = 1 + $monthCount;
+        $rowTotalIdx  = $lastDataIdx + 1;
+
+        $colA = Coordinate::stringFromColumnIndex($nameColIdx);
+        $colStart = Coordinate::stringFromColumnIndex($firstDataIdx);
+        $colEnd = Coordinate::stringFromColumnIndex($lastDataIdx);
+        $colRowTot = Coordinate::stringFromColumnIndex($rowTotalIdx);
+
+        // ========== ENTRATE ==========
+        // Header
+        $activeWorksheet->setCellValue($colA . '1', 'Entrate');
+        foreach ($columns as $idx => $column) {
+            $col = Coordinate::stringFromColumnIndex($firstDataIdx + $idx);
+            $activeWorksheet->setCellValue($col . '1', $this->getMonth($column));
+        }
+        $activeWorksheet->setCellValue($colRowTot . '1', 'Totale');
+
+        $activeWorksheet->getStyle($colA . '1:' . $colRowTot . '1')->getFont()->setBold(true);
+        $activeWorksheet->getStyle($colA . '1:' . $colRowTot . '1')
+            ->getFill()->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
+            ->getStartColor()->setARGB('C6E0B4'); // verde chiaro
+
+        $count = 2;
+
+        $parentIdsIn = array_column(array_filter($rows_in, function ($r) {
+            return (int)($r['level'] ?? 0) === 0;
+        }), 'id');
+
+        $totalsIn = [];
+
+        foreach ($rows_in as $in) {
+            $activeWorksheet->setCellValue($colA . $count, str_repeat("  ", (int)$in["level"]) . $in["name"]);
+
+            $rowSum = 0.0;
+
+            foreach ($columns as $idx => $column) {
+                $col = Coordinate::stringFromColumnIndex($firstDataIdx + $idx);
+
+                if (isset($records_in[$column][$in["id"]])) {
+                    $val = (float)$records_in[$column][$in["id"]];
+                    $writeNumber($activeWorksheet, $col . $count, $val);
+
+                    if ($shouldCountFn($in, $parentIdsIn)) {
+                        $totalsIn[$idx] = ($totalsIn[$idx] ?? 0) + $val;
+                    }
+
+                    $rowSum += $val;
+                }
+            }
+
+            $writeNumber($activeWorksheet, $colRowTot . $count, $rowSum);
+
+            if ((int)$in["level"] === 0) {
+                $activeWorksheet->getStyle($colA . $count . ':' . $colRowTot . $count)->getFont()->setBold(true);
+            }
+
+            $count++;
+        }
+
+        // Riga "Totale" ENTRATE
+        $activeWorksheet->setCellValue($colA . $count, 'Totale');
+
+        $grandTotalIn = 0.0;
+        foreach ($totalsIn as $idx => $total) {
+            $col = Coordinate::stringFromColumnIndex($firstDataIdx + $idx);
+            $writeNumber($activeWorksheet, $col . $count, (float)$total);
+            $grandTotalIn += (float)$total;
+        }
+        // Totale complessivo nella colonna "Totale"
+        $writeNumber($activeWorksheet, $colRowTot . $count, $grandTotalIn);
+
+        $activeWorksheet->getStyle($colA . $count . ':' . $colRowTot . $count)->getFont()->setBold(true);
+        $activeWorksheet->getStyle($colA . $count . ':' . $colRowTot . $count)
+            ->getFill()->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
+            ->getStartColor()->setARGB('C6E0B4');
+
+        // ========== USCITE ==========
+        $count += 2;
+
+        // Header
+        $activeWorksheet->setCellValue($colA . $count, 'Uscite');
+        foreach ($columns as $idx => $column) {
+            $col = Coordinate::stringFromColumnIndex($firstDataIdx + $idx);
+            $activeWorksheet->setCellValue($col . $count, $this->getMonth($column));
+        }
+        $activeWorksheet->setCellValue($colRowTot . '1', 'Totale'); // già impostato sopra, qui lasciamo l’header a riga corrente
+        $activeWorksheet->setCellValue($colRowTot . $count, 'Totale');
+
+        $activeWorksheet->getStyle($colA . $count . ':' . $colRowTot . $count)->getFont()->setBold(true);
+        $activeWorksheet->getStyle($colA . $count . ':' . $colRowTot . $count)
+            ->getFill()->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
+            ->getStartColor()->setARGB('F8CBAD'); // rosso chiaro
+
+        $count++;
+
+        $parentIdsOut = array_column(array_filter($rows_out, function ($r) {
+            return (int)($r['level'] ?? 0) === 0;
+        }), 'id');
+
+        $totalsOut = [];
+
+        foreach ($rows_out as $out) {
+            $activeWorksheet->setCellValue($colA . $count, str_repeat("  ", (int)$out["level"]) . $out["name"]);
+
+            $rowSum = 0.0;
+
+            foreach ($columns as $idx => $column) {
+                $col = Coordinate::stringFromColumnIndex($firstDataIdx + $idx);
+
+                if (isset($records_out[$column][$out["id"]])) {
+                    $val = (float)$records_out[$column][$out["id"]];
+                    $writeNumber($activeWorksheet, $col . $count, $val);
+
+                    if ($shouldCountFn($out, $parentIdsOut)) {
+                        $totalsOut[$idx] = ($totalsOut[$idx] ?? 0) + $val;
+                    }
+
+                    $rowSum += $val;
+                }
+            }
+
+            $writeNumber($activeWorksheet, $colRowTot . $count, $rowSum);
+
+            if ((int)$out["level"] === 0) {
+                $activeWorksheet->getStyle($colA . $count . ':' . $colRowTot . $count)->getFont()->setBold(true);
+            }
+
+            $count++;
+        }
+
+        // Riga "Totale" USCITE
+        $activeWorksheet->setCellValue($colA . $count, 'Totale');
+
+        $grandTotalOut = 0.0;
+        foreach ($totalsOut as $idx => $total) {
+            $col = Coordinate::stringFromColumnIndex($firstDataIdx + $idx);
+            $writeNumber($activeWorksheet, $col . $count, (float)$total);
+            $grandTotalOut += (float)$total;
+        }
+        $writeNumber($activeWorksheet, $colRowTot . $count, $grandTotalOut);
+
+        $activeWorksheet->getStyle($colA . $count . ':' . $colRowTot . $count)->getFont()->setBold(true);
+        $activeWorksheet->getStyle($colA . $count . ':' . $colRowTot . $count)
+            ->getFill()->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
+            ->getStartColor()->setARGB('F8CBAD');
+
+        // ========== Larghezze colonne ==========
+        $activeWorksheet->getColumnDimension($colA)->setWidth(35);
+        for ($i = $firstDataIdx; $i <= $rowTotalIdx; $i++) {
+            $activeWorksheet->getColumnDimension(Coordinate::stringFromColumnIndex($i))->setWidth(20);
+        }
+
+        // ========== Salvataggio ==========
+        $fileSuffix = $isYearExport ? 'AnnoFiscale' : 'Selezione';
+        $filename = date("Ymd") . '_Gestionale_' . $fileSuffix . '.xlsx';
+
+        try {
+            // Get current client for folder organization
+            $currentClient = session('currentClient', 'default');
+
+            // Create temporary file to write Excel data
+            $tempPath = sys_get_temp_dir() . '/' . $filename;
+
+            // Write to temporary file
+            $writer = new Xlsx($spreadsheet);
+            $writer->save($tempPath);
+
+            // Get Wasabi disk
+            $disk = Storage::disk('s3');
+
+            // Create the S3 path (client folder + reports subfolder)
+            $s3Path = $currentClient . '/reports/' . $filename;
+
+            // Ensure the client reports folder exists
+            $reportsFolderPath = $currentClient . '/reports/.gitkeep';
+            if (!$disk->exists($reportsFolderPath)) {
+                $disk->put($reportsFolderPath, '');
+                Log::info("Created reports folder for client: {$currentClient}");
+            }
+
+            // Upload file to Wasabi S3
+            $fileContent = file_get_contents($tempPath);
+            $uploaded = $disk->put($s3Path, $fileContent, 'private');
+
+            if (!$uploaded) {
+                throw new \Exception('Failed to upload file to Wasabi S3');
+            }
+
+            Log::info("Management report uploaded to Wasabi", [
+                'client' => $currentClient,
+                'path' => $s3Path,
+                'size' => filesize($tempPath),
+                'type' => $fileSuffix
+            ]);
+
+            // Clean up temporary file
+            if (file_exists($tempPath)) {
+                unlink($tempPath);
+            }
+
+            // Return S3 path for further processing or download URL generation
+            return [
+                'success' => true,
+                'path' => $s3Path,
+                'local_path' => null,
+                'storage_type' => 's3'
+            ];
+        } catch (\Exception $e) {
+            Log::error('Error uploading management report to Wasabi S3', [
+                'error' => $e->getMessage(),
+                'client' => session('currentClient', 'unknown'),
+                'filename' => $filename
+            ]);
+
+            // Fallback to local storage if S3 fails
+            $currentClient = session('currentClient', 'default');
+            $clientFolder = storage_path('app/reports/' . $currentClient);
+
+            // Ensure client folder exists
+            if (!is_dir($clientFolder)) {
+                mkdir($clientFolder, 0755, true);
+                Log::info("Created local client reports folder: {$clientFolder}");
+            }
+
+            $localPath = $clientFolder . '/' . $filename;
+            $writer = new Xlsx($spreadsheet);
+            $writer->save($localPath);
+
+            Log::warning("Management report saved locally due to S3 error", [
+                'client' => $currentClient,
+                'local_path' => $localPath,
+                'error' => $e->getMessage()
+            ]);
+
+            // Return local path for backward compatibility
+            return [
+                'success' => true,
+                'path' => $localPath,
+                'local_path' => $localPath,
+                'storage_type' => 'local'
+            ];
+        }
+    }
+}

+ 1907 - 0
app/Http/Livewire/RecordOUT.php

@@ -0,0 +1,1907 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use App\Http\Middleware\TenantMiddleware;
+use Livewire\Component;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Log;
+use SimpleXMLElement;
+use Livewire\WithFileUploads;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Schema;
+use App\Services\RecordFileService;
+use App\Jobs\ProcessRecordAttachment;
+
+class RecordOUT extends Component
+{
+    use WithFileUploads;
+
+    protected $listeners = [
+        'setCausal' => 'setCausal',
+        'closeImportModal' => 'closeImportModal',
+    ];
+
+    public $sortField = 'date';
+    public $sortAsc = false;
+
+    public $typeOUT = "OUT";
+
+    public $fromPage = '';
+
+    public $multiP = false;
+
+    public $multiMonthTo = 0;
+    public $multiYearTo = 0;
+    public $multiMonthFrom = 0;
+    public $multiYearFrom = 0;
+
+    public function sortBy($field)
+    {
+        if ($this->sortField === $field) {
+            $this->sortAsc = ! $this->sortAsc;
+        } else {
+            $this->sortAsc = true;
+        }
+
+        $this->sortField = $field;
+    }
+
+    public $records,
+        $dataId,
+        $member_id,
+        $supplier_id,
+        $causal_id,
+        $payment_method_id,
+        $date,
+        $data_pagamento,
+        $month,
+        $year,
+        $type,
+        $amount,
+        $note,
+        $commercial,
+        $update = false,
+        $add = false,
+        $is_paid = false;
+    public $attachment;
+    public $attachment_old;
+
+    public $filterSupplier = 0, $filterPaymentMethod = 0, $filterCausals = [], $filterFrom = '', $filterTo = '', $filterCommercial = 0;
+
+    public $hasFilter = false;
+
+    public $total = 0;
+
+    public $selectedFilter = '';
+
+    public $multipleIds = [];
+    public $multipleAction = '';
+
+    public $selectId = 0;
+
+    public $causals = array();
+    public $payments = array();
+    public $suppliers = array();
+
+    public $rows = array();
+    public $receiptFiles = [];
+    public $selectedCausal = '';
+    public $importCausals = array();
+
+    public $vats = array();
+
+    public $numero_fattura;
+    public $attachmentUploadStatus = 'none';
+    public $uploadProgress = 0;
+    public $uploadStartTime = null;
+
+
+    protected $rules = [
+        'supplier_id' => 'required',
+        'payment_method_id' => 'required',
+        'rows.*.causal_id' => 'required',
+        'rows.*.amount' => 'required'
+    ];
+
+    protected $messages = [
+        'supplier_id.required' => 'Il fornitore è obbligatorio',
+        'payment_method_id.required' => 'Il metodo di pagamento è obbligatorio',
+        'rows.*.amount.required' => 'L\'importo è obbligatorio',
+        'rows.*.causal_id.required' => 'La causale è obbligatoria'
+    ];
+
+    protected $recordFileService;
+
+    public function boot()
+    {
+        $this->recordFileService = app(RecordFileService::class);
+        app(TenantMiddleware::class)->setupTenantConnection();
+
+    }
+
+    public function getSupplierProperty()
+    {
+        $ret = null;
+        if ($this->supplier_id > 0) {
+            $ret = \App\Models\Supplier::findOrFail($this->supplier_id);
+        }
+        return $ret;
+    }
+
+    public function getCausalProperty()
+    {
+        $ret = null;
+        if ($this->causal_id > 0) {
+            $ret = \App\Models\Causal::findOrFail($this->causal_id);
+        }
+        return $ret;
+    }
+
+    public function resetFields()
+    {
+        $this->member_id = null;
+        $this->supplier_id = null;
+        $this->payment_method_id = null;
+        $this->date = date("Y-m-d");
+        $this->data_pagamento = date("Y-m-d");
+        $this->attachment = null;
+        $this->attachment_old = null;
+        $this->numero_fattura = null;
+        $this->type = 'OUT';
+        $this->commercial = 1;
+        $this->dataId = null;
+        $this->rows = array();
+        $this->rows[] = array(
+            'causal_id' => null,
+            'when' => array(array('month' => date("n"), 'year' => date("Y"), 'period' => '')),
+            'amount' => null,
+            'imponibile' => null,
+            'aliquota_iva' => '',
+            'note' => '',
+            'commercial' => 0
+        );
+        $this->emit('load-data-table');
+    }
+    public function getCausale($records, $indentation)
+    {
+        foreach ($records as $record) {
+            $this->causals[] = array('id' => $record->id, 'name' => $record->getTree());
+            if (count($record->childs))
+                $this->getCausale($record->childs, $indentation + 1);
+        }
+    }
+
+    public $isImportModalOpen = false;
+
+    public function openImportModal()
+    {
+        $this->isImportModalOpen = true;
+    }
+
+    public function closeImportModal()
+    {
+        $this->isImportModalOpen = false;
+        $this->reset(['selectedCausal', 'receiptFiles']);
+        $this->resetValidation();
+        $this->dispatchBrowserEvent('closeModal');
+    }
+
+    public function hydrate()
+    {
+        $this->emit('load-select');
+
+        if (empty($this->importCausals)) {
+            $this->loadImportCausals();
+        }
+
+        $this->vats = \App\Models\Vat::select('id', 'name', 'value')->orderBy('value')->get();
+    }
+
+    public function mount()
+    {
+
+        if (isset($_GET["from"])) {
+            $this->fromPage = $_GET["from"];
+        }
+
+        if (Auth::user()->level != env('LEVEL_ADMIN', 0))
+            return redirect()->to('/dashboard');
+
+        $this->multiMonthFrom = date("n");
+        $this->multiYearFrom = date("Y");
+        $this->multiMonthTo = date("n");
+        $this->multiYearTo = date("Y");
+
+        if (isset($_GET["new"]))
+            $this->add();
+
+        $this->causals = array();
+
+        $this->getCausale(\App\Models\Causal::select('id', 'name')->where('parent_id', null)->where('type', 'OUT')->orderBy('name')->get(), 0);
+
+        $this->suppliers = \App\Models\Supplier::select('name', 'id')->orderBy('name')->get();
+        $this->payments = \App\Models\PaymentMethod::select('id', 'name')->whereIn('type', array('ALL', 'OUT'))->where('enabled', true)->orderBy('name')->get();
+        $this->vats = \App\Models\Vat::select('id', 'name', 'value')->orderBy('value')->get();
+
+        $this->importCausals = [];
+        $this->loadImportCausals();
+    }
+
+    public function loadImportCausals()
+    {
+        $causals = \App\Models\Causal::select('id', 'name', 'parent_id')
+            ->where('type', 'OUT')
+            ->orderBy('name')
+            ->get();
+
+        $this->importCausals = [];
+        $this->buildCausalTree($causals);
+    }
+
+    private function buildCausalTree($causals, $parentId = null, $level = 0)
+    {
+        foreach ($causals as $causal) {
+            if ($causal->parent_id == $parentId) {
+                $dotString = str_repeat('● ', $level);
+                $this->importCausals[] = [
+                    'id' => $causal->id,
+                    'name' => $dotString . $causal->name
+                ];
+
+                $this->buildCausalTree($causals, $causal->id, $level + 1);
+            }
+        }
+    }
+
+    private function getCausalLevel($causal, $level = 0)
+    {
+        if ($causal->parent_id == null) {
+            return $level;
+        }
+
+        $parent = \App\Models\Causal::find($causal->parent_id);
+        if (!$parent) {
+            return $level;
+        }
+
+        return $this->getCausalLevel($parent, $level + 1);
+    }
+
+    public function getCausal($causal)
+    {
+        $ret = '';
+        if ($causal > 0) {
+            $ret = \App\Models\Causal::findOrFail($causal)->getTree();
+        }
+        return $ret;
+    }
+
+    public function search()
+    {
+        $this->hasFilter = true;
+    }
+
+    public function disableSearch()
+    {
+        $this->filterSupplier = 0;
+        $this->filterPaymentMethod = 0;
+        $this->filterCausals = [];
+        $this->filterTo = '';
+        $this->filterFrom = '';
+        $this->filterCommercial = 0;
+        $this->hasFilter = false;
+        $this->hasFilter = false;
+    }
+
+    public function render()
+    {
+        $datas = [];
+
+        if (false) {
+            if ($this->hasFilter) {
+
+                $datas = \App\Models\Record::where('type', 'OUT')->with('supplier', 'payment_method', 'is_paid');
+                if ($this->filterSupplier > 0) {
+                    $datas = $datas->where('supplier_id', $this->filterSupplier);
+                }
+                if ($this->filterPaymentMethod > 0) {
+                    $datas = $datas->where('payment_method_id', $this->filterPaymentMethod);
+                }
+                if ($this->filterFrom != '') {
+                    $datas = $datas->where('date', '>=', $this->filterFrom);
+                }
+                if ($this->filterTo != '') {
+                    $datas = $datas->where('date', '<=', $this->filterTo);
+                }
+
+                $this->records = $datas->get();
+
+
+                $this->total = 0;
+                foreach ($this->records as $r) {
+                    foreach ($r->rows as $rr) {
+                        $this->total += $rr->amount;
+                    }
+                }
+            } else {
+                $fromDate = date("Y-m-d");
+                $toDate = date("Y-m-d");
+                if ($this->selectedFilter == '') {
+                    $this->records = \App\Models\Record::where('type', 'OUT')->with('supplier', 'payment_method')->limit(20)->orderBy('date', 'DESC')->orderBy('id', 'DESC')->get();
+                } else {
+                    if ($this->selectedFilter == 0) {
+                        $fromDate = date("Y-m-d");
+                        $toDate = date("Y-m-d");
+                    }
+                    if ($this->selectedFilter == 1) {
+                        $fromDate = date("Y-m-01");
+                        $toDate = date("Y-m-t");
+                    }
+                    if ($this->selectedFilter == 2) {
+                        $fromDate = date("Y-01-01");
+                        $toDate = date("Y-12-31");
+                    }
+                    if ($this->selectedFilter == 3) {
+                        $fromDate = date("2000-01-01");
+                        $toDate = date("Y-12-31");
+                    }
+                    $this->records = \App\Models\Record::where('type', 'OUT')->whereBetween('date', [$fromDate, $toDate])->with('supplier', 'payment_method')->get();
+                }
+            }
+
+            foreach ($this->records as $r) {
+                $r->total = $r->getTotal();
+                $r->supplier = $r->supplier ? $r->supplier->name : '';
+                $r->payment = $r->payment_method ? $r->payment_method->name : '';
+                $r->is_paid = $r->is_paid ? 'Pagato' : 'Non Pagato';
+            }
+        }
+        return view('livewire.records_out');
+    }
+
+    public function executeMultipleAction()
+    {
+
+        if ($this->multipleAction == 'delete')
+            $this->multipleDelete();
+    }
+
+    public function add()
+    {
+
+        $this->emit('load-select');
+        $this->emit('hide-search');
+        $this->resetFields();
+        $this->add = true;
+        $this->update = false;
+        $this->emit('setEdit', true);
+    }
+
+
+    public function store()
+    {
+        $this->emit('start-loading', 'Validazione dati...');
+        $this->emit('refresh');
+
+        if ($this->numero_fattura == null || $this->numero_fattura == '') {
+            $this->numero_fattura = 'USC-' . date('Ymd');
+        }
+
+        if (empty($this->data_pagamento) || $this->data_pagamento == '1970-01-01') {
+            $this->data_pagamento = null;
+            $is_paid = false;
+        } else {
+            $is_paid = true;
+        }
+
+        if ($this->attachment) {
+            $fileSize = $this->attachment->getSize();
+            $fileName = $this->attachment->getClientOriginalName();
+            $mimeType = $this->attachment->getMimeType();
+            $maxSize = 50 * 1024 * 1024;
+
+            Log::info("=== FILE VALIDATION ===");
+            Log::info("File: {$fileName}");
+            Log::info("Size: " . round($fileSize / 1024 / 1024, 2) . " MB");
+            Log::info("MIME: {$mimeType}");
+
+            if ($fileSize > $maxSize) {
+                $this->emit('stop-loading');
+                $this->emit('flash-error', 'File troppo grande. Dimensione massima: 50MB');
+                return;
+            }
+
+            $allowedMimes = [
+                'image/jpeg',
+                'image/jpg',
+                'image/png',
+                'image/gif',
+                'image/webp',
+                'application/pdf',
+                'application/msword',
+                'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+                'text/xml',
+                'application/xml',
+                'text/plain',
+                'text/csv',
+                'application/vnd.ms-excel',
+                'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+            ];
+
+            if (!in_array($mimeType, $allowedMimes)) {
+                $this->emit('stop-loading');
+                $this->emit('flash-error', 'Tipo di file non supportato');
+                return;
+            }
+
+            Log::info("File validation passed");
+        }
+
+        $this->validate();
+
+        try {
+            $this->emit('update-loading', 'Salvataggio record...');
+
+            DB::beginTransaction();
+
+            $record = \App\Models\Record::create([
+                'member_id' => $this->member_id,
+                'supplier_id' => $this->supplier_id,
+                'payment_method_id' => $this->payment_method_id,
+                'date' => $this->date,
+                'data_pagamento' => $this->data_pagamento,
+                'attachment' => '',
+                'type' => $this->type,
+                'amount' => 0,
+                'commercial' => $this->commercial,
+                'numero_fattura' => $this->numero_fattura,
+                'is_paid' => $is_paid,
+                'attachment_status' => $this->attachment ? 'pending' : 'none',
+            ]);
+
+            $this->dataId = $record->id;
+            Log::info("Record created with ID: {$this->dataId}");
+
+            $tot = 0;
+            $rowsData = [];
+
+            foreach ($this->rows as $row) {
+                foreach ($row["when"] as $x => $y) {
+                    $row["when"][$x]['period'] = $row["when"][$x]['month'] . "-" . $row["when"][$x]['year'];
+                }
+
+                $imponibile = isset($row["imponibile"]) ? $this->currencyToDouble($row["imponibile"]) : null;
+                $aliquota_iva = isset($row["aliquota_iva"]) ? floatval(str_replace('%', '', $row["aliquota_iva"])) : null;
+                $amount = $this->currencyToDouble($row["amount"]);
+
+                $rowsData[] = [
+                    'record_id' => $this->dataId,
+                    'causal_id' => $row["causal_id"],
+                    'note' => $row["note"],
+                    'amount' => $amount,
+                    'imponibile' => $imponibile,
+                    'aliquota_iva' => $aliquota_iva,
+                    'commercial' => $row["commercial"],
+                    'when' => json_encode($row["when"]),
+                    'created_at' => now(),
+                    'updated_at' => now()
+                ];
+                $tot += $amount;
+            }
+
+            \App\Models\RecordRow::insert($rowsData);
+            $record->amount = $tot;
+            $record->save();
+
+            DB::commit();
+            Log::info("✅ Database transaction committed");
+
+            if ($this->attachment) {
+                $this->emit('update-loading', 'Preparazione file per elaborazione...');
+
+                try {
+                    Log::info("=== STARTING FILE PROCESSING ===");
+
+                    $tempPath = $this->recordFileService->storeTemporarily($this->attachment);
+                    $originalFileName = $this->attachment->getClientOriginalName();
+
+                    Log::info(" File stored temporarily at: {$tempPath}");
+
+                    $clientName = session('clientName', 'default');
+
+                    ProcessRecordAttachment::dispatch(
+                        $record->id,
+                        $tempPath,
+                        $originalFileName,
+                        'out',
+                        $clientName
+                    )->onQueue('attachments');
+
+                    Log::info("File processing job dispatched for record {$record->id}");
+                } catch (\Exception $ex) {
+                    Log::error("Failed to process file: " . $ex->getMessage());
+                    Log::error("Stack trace: " . $ex->getTraceAsString());
+                    DB::table('records')
+                        ->where('id', $record->id)
+                        ->update(['attachment_status' => 'failed']);
+
+                    session()->flash('warning', 'Record creato ma elaborazione allegato fallita. Prova a modificare il record e caricare nuovamente il file.');
+                }
+            }
+
+            $this->emit('stop-loading');
+
+            if ($this->attachment) {
+                session()->flash('success', 'Movimento creato con successo. L\'allegato verrà elaborato in background. Aggiorna la pagina tra qualche secondo per vedere lo stato.');
+            } else {
+                session()->flash('success', 'Movimento creato con successo.');
+            }
+
+            $this->resetFields();
+            $this->add = false;
+            $this->emit('setEdit', false);
+        } catch (\Exception $ex) {
+            DB::rollback();
+            $this->emit('stop-loading');
+
+            Log::error("Store error: " . $ex->getMessage());
+            Log::error("Stack trace: " . $ex->getTraceAsString());
+            $this->emit('flash-error', 'Errore durante il salvataggio: ' . $ex->getMessage());
+        }
+    }
+
+    public function setDataPagamentoAttribute($value)
+    {
+        if (empty($value) || $value == '1970-01-01' || $value == '0000-00-00') {
+            $this->attributes['data_pagamento'] = null;
+        } else {
+            $this->attributes['data_pagamento'] = $value;
+        }
+    }
+
+    public function getDataPagamentoAttribute($value)
+    {
+        if ($value == '1970-01-01' || $value == '0000-00-00') {
+            return null;
+        }
+        return $value;
+    }
+
+    public function removeAttachment()
+    {
+        if ($this->attachment_old) {
+            try {
+                $this->recordFileService->deleteAttachment($this->attachment_old);
+
+                if ($this->dataId) {
+                    DB::table('records')
+                        ->where('id', $this->dataId)
+                        ->update([
+                            'attachment' => '',
+                        ]);
+                }
+
+                $this->attachment_old = '';
+                $this->attachmentUploadStatus = 'none';
+
+                session()->flash('success', 'Allegato rimosso con successo');
+            } catch (\Exception $e) {
+                Log::error("Error removing attachment: " . $e->getMessage());
+                $this->emit('flash-error', 'Errore durante la rimozione dell\'allegato');
+            }
+        }
+    }
+
+    public function edit($id)
+    {
+        if (!isset($_GET["from"]) && $this->fromPage == '')
+            $this->fromPage = 'out';
+        $this->emit('setEdit', true);
+        $this->emit('load-select');
+        $this->emit('hide-search');
+
+        try {
+            $record = \App\Models\Record::findOrFail($id);
+            if (!$record) {
+                $this->emit('flash-error', 'Movimento non trovato');
+            } else {
+
+                $this->member_id = $record->member_id;
+                $this->supplier_id = $record->supplier_id;
+                $this->payment_method_id = $record->payment_method_id;
+                $this->date = date("Y-m-d", strtotime($record->date));
+                $this->data_pagamento = $record->data_pagamento;
+                $this->type = $record->type;
+                $this->numero_fattura = $record->numero_fattura;
+
+                if (!empty($record->attachment) && $record->attachment !== null && $record->attachment !== '') {
+                    $this->attachment_old = $record->attachment;
+                } else {
+                    $this->attachment_old = '';
+                }
+
+                $this->attachment = null;
+
+                $this->commercial = $record->commercial;
+                $this->dataId = $record->id;
+                $this->update = true;
+                $this->add = false;
+
+                $this->rows = [];
+                $this->recordFileService->createRecordFolders($record->id, 'OUT');
+
+                $recordRows = \App\Models\RecordRow::where('record_id', $this->dataId)->get();
+
+                foreach ($recordRows as $recordRow) {
+                    $rowData = [
+                        'causal_id' => $recordRow->causal_id,
+                        'note' => $recordRow->note,
+                        'commercial' => $recordRow->commercial,
+                        'when' => json_decode($recordRow->when),
+                        'amount' => formatPrice($recordRow->amount)
+                    ];
+
+                    if (isset($recordRow->imponibile)) {
+                        $rowData['imponibile'] = formatPrice($recordRow->imponibile);
+                    }
+
+                    $vatLookup = [];
+                    foreach ($this->vats as $vat) {
+                        $vatLookup[(string)$vat->value] = $vat->value;
+                    }
+
+                    if (isset($recordRow->aliquota_iva)) {
+                        $dbVatValue = (string)$recordRow->aliquota_iva;
+                        if (isset($vatLookup[$dbVatValue])) {
+                            $rowData['aliquota_iva'] = (string)$vatLookup[$dbVatValue];
+                        } else {
+                            $dbVatValueFloat = floatval($dbVatValue);
+                            $closestMatch = null;
+                            $minDiff = PHP_FLOAT_MAX;
+
+                            foreach ($vatLookup as $vatValue) {
+                                $diff = abs($dbVatValueFloat - floatval($vatValue));
+                                if ($diff < $minDiff && $diff < 0.1) {
+                                    $minDiff = $diff;
+                                    $closestMatch = $vatValue;
+                                }
+                            }
+                            if ($closestMatch !== null) {
+                                $rowData['aliquota_iva'] = (string)$closestMatch;
+                            } else {
+                                $rowData['aliquota_iva'] = $dbVatValue;
+                            }
+                        }
+                    }
+
+                    $this->rows[] = $rowData;
+                }
+
+                Log::info("EDIT DEBUG - Final attachment_old value: '" . $this->attachment_old . "'");
+
+                $this->emit('refresh');
+            }
+        } catch (\Exception $ex) {
+            $this->emit('flash-error', 'Errore (' . $ex->getMessage() . ')');
+            Log::error("EDIT ERROR: " . $ex->getMessage());
+        }
+    }
+
+    public function update()
+    {
+        $this->emit('start-loading', 'Validazione dati...');
+        $this->emit('refresh');
+
+        if ($this->numero_fattura == null || $this->numero_fattura == '') {
+            $this->numero_fattura = 'USC-' . date('Ymd');
+        }
+
+        if (empty($this->data_pagamento) || $this->data_pagamento == '1970-01-01') {
+            $this->data_pagamento = null;
+            $is_paid = false;
+        } else {
+            $is_paid = true;
+        }
+
+        if ($this->attachment) {
+            $fileSize = $this->attachment->getSize();
+            $fileName = $this->attachment->getClientOriginalName();
+            $mimeType = $this->attachment->getMimeType();
+            $maxSize = 50 * 1024 * 1024;
+
+            Log::info("=== FILE VALIDATION (UPDATE) ===");
+            Log::info("File: {$fileName}");
+            Log::info("Size: " . round($fileSize / 1024 / 1024, 2) . " MB");
+            Log::info("MIME: {$mimeType}");
+
+            if ($fileSize > $maxSize) {
+                $this->emit('stop-loading');
+                $this->emit('flash-error', 'File troppo grande. Dimensione massima: 50MB');
+                return;
+            }
+
+            $allowedMimes = [
+                'image/jpeg',
+                'image/jpg',
+                'image/png',
+                'image/gif',
+                'image/webp',
+                'application/pdf',
+                'application/msword',
+                'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+                'text/xml',
+                'application/xml',
+                'text/plain',
+                'text/csv',
+                'application/vnd.ms-excel',
+                'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+            ];
+
+            if (!in_array($mimeType, $allowedMimes)) {
+                $this->emit('stop-loading');
+                $this->emit('flash-error', 'Tipo di file non supportato');
+                return;
+            }
+
+            Log::info("File validation passed");
+        }
+
+        $this->validate();
+
+        try {
+            $this->emit('update-loading', 'Aggiornamento record...');
+
+            DB::beginTransaction();
+
+            $record = \App\Models\Record::findOrFail($this->dataId);
+
+            $record->update([
+                'member_id' => $this->member_id,
+                'supplier_id' => $this->supplier_id,
+                'payment_method_id' => $this->payment_method_id,
+                'date' => $this->date,
+                'data_pagamento' => $this->data_pagamento,
+                'type' => $this->type,
+                'commercial' => $this->commercial,
+                'numero_fattura' => $this->numero_fattura,
+                'is_paid' => $is_paid,
+                'attachment_status' => $this->attachment ? 'pending' : ($this->attachment_old ? 'completed' : 'none'),
+            ]);
+
+            Log::info("Record updated with ID: {$this->dataId}");
+
+            $existingRows = \App\Models\RecordRow::where('record_id', $this->dataId)
+                ->select('id', 'quantita', 'numero_linea')
+                ->get()
+                ->keyBy('id')
+                ->toArray();
+
+            \App\Models\RecordRow::where('record_id', $this->dataId)->delete();
+
+            $tot = 0;
+            $newRowsData = [];
+
+            foreach ($this->rows as $row) {
+                foreach ($row["when"] as $x => $y) {
+                    $row["when"][$x]['period'] = $row["when"][$x]['month'] . "-" . $row["when"][$x]['year'];
+                }
+
+                $imponibile = isset($row["imponibile"]) ? $this->currencyToDouble($row["imponibile"]) : null;
+                $aliquota_iva = isset($row["aliquota_iva"]) ? floatval(str_replace('%', '', $row["aliquota_iva"])) : null;
+                $amount = $this->currencyToDouble($row["amount"]);
+                $imposta = $imponibile !== null ? $amount - $imponibile : null;
+
+                $recordRowData = [
+                    'record_id' => $this->dataId,
+                    'causal_id' => $row["causal_id"],
+                    'note' => $row["note"],
+                    'amount' => $amount,
+                    'imponibile' => $imponibile,
+                    'aliquota_iva' => $aliquota_iva,
+                    'imposta' => $imposta,
+                    'commercial' => $row["commercial"],
+                    'when' => json_encode($row["when"]),
+                    'divisa' => 'EUR',
+                    'created_at' => now(),
+                    'updated_at' => now()
+                ];
+
+                if (isset($row["id"]) && isset($existingRows[$row["id"]])) {
+                    $existingRow = $existingRows[$row["id"]];
+                    $recordRowData['quantita'] = $existingRow['quantita'];
+                    $recordRowData['numero_linea'] = $existingRow['numero_linea'];
+                }
+
+                $newRowsData[] = $recordRowData;
+                $tot += $amount;
+            }
+
+            \App\Models\RecordRow::insert($newRowsData);
+            $record->amount = $tot;
+            $record->save();
+
+            DB::commit();
+            Log::info("Database transaction committed");
+
+            if ($this->attachment) {
+                $this->emit('update-loading', 'Preparazione file per elaborazione...');
+
+                try {
+                    Log::info("=== STARTING FILE PROCESSING (UPDATE) ===");
+
+                    if ($this->attachment_old) {
+                        $this->recordFileService->deleteAttachment($this->attachment_old);
+                        Log::info("Old attachment deleted: {$this->attachment_old}");
+                    }
+
+                    $tempPath = $this->recordFileService->storeTemporarily($this->attachment);
+                    $originalFileName = $this->attachment->getClientOriginalName();
+
+                    Log::info("File stored temporarily at: {$tempPath}");
+
+                    $clientName = session('clientName', 'default');
+
+                    ProcessRecordAttachment::dispatch(
+                        $record->id,
+                        $tempPath,
+                        $originalFileName,
+                        'out',
+                        $clientName
+                    )->onQueue('attachments');
+
+                    Log::info("File processing job dispatched for record {$record->id}");
+                } catch (\Exception $ex) {
+                    Log::error("Failed to process file: " . $ex->getMessage());
+                    Log::error("Stack trace: " . $ex->getTraceAsString());
+
+                    DB::table('records')
+                        ->where('id', $record->id)
+                        ->update(['attachment_status' => 'failed']);
+
+                    session()->flash('warning', 'Record aggiornato ma elaborazione allegato fallita. Prova a modificare il record e caricare nuovamente il file.');
+                }
+            }
+
+            $this->emit('stop-loading');
+
+            if ($this->attachment) {
+                session()->flash('success', 'Movimento aggiornato con successo. L\'allegato verrà elaborato in background. Aggiorna la pagina tra qualche secondo per vedere lo stato.');
+            } else {
+                session()->flash('success', 'Movimento aggiornato con successo.');
+            }
+
+            $this->resetFields();
+            $this->update = false;
+            $this->emit('setEdit', false);
+        } catch (\Exception $ex) {
+            DB::rollback();
+            $this->emit('stop-loading');
+
+            Log::error("Update error: " . $ex->getMessage());
+            Log::error("Stack trace: " . $ex->getTraceAsString());
+            $this->emit('flash-error', 'Errore durante l\'aggiornamento: ' . $ex->getMessage());
+        }
+    }
+
+    public function cancel()
+    {
+        $this->add = false;
+        $this->update = false;
+        $this->emit('setEdit', false);
+        $this->resetFields();
+    }
+
+    public function delete($id)
+    {
+        try {
+            $record = \App\Models\Record::find($id);
+            if ($record->attachment) {
+                $this->recordFileService->deleteAttachment($record->attachment);
+            }
+
+            $record->delete();
+            session()->flash('success', "Movimento eliminato");
+        } catch (\Exception $e) {
+            $this->emit('flash-error', 'Errore (' . $e->getMessage() . ')');
+        }
+    }
+    public function multipleDelete()
+    {
+        try {
+            foreach ($this->multipleIds as $id) {
+                $record = \App\Models\Record::find($id);
+
+                if ($record->attachment) {
+                    $this->recordFileService->deleteAttachment($record->attachment);
+                }
+
+                $record->delete();
+            }
+        } catch (\Exception $e) {
+            $this->emit('flash-error', 'Errore (' . $e->getMessage() . ')');
+        }
+        $this->multipleAction = '';
+    }
+
+    public function getAttachmentUrl($filePath)
+    {
+        if (!$filePath) {
+            return null;
+        }
+
+        return $this->recordFileService->getAttachmentUrl($filePath);
+    }
+
+    function currencyToDouble($val)
+    {
+        $x = str_replace("€", "", $val);
+        $x = str_replace(".", "", $x);
+        $x = str_replace(",", ".", $x);
+        return floatval(trim($x));
+    }
+
+    public function addRow()
+    {
+        $this->rows[] = array(
+            'causal_id' => null,
+            'when' => array(array('month' => date("n"), 'year' => date("Y"), 'period' => '')),
+            'amount' => null,
+            'imponibile' => null,
+            'aliquota_iva' => null,
+            'note' => '',
+            'commercial' => 0
+        );
+        $this->emit('load-select');
+    }
+
+    public function delRow($idx)
+    {
+        unset($this->rows[$idx]);
+        // $this->emit('load-select');
+    }
+
+    public function addPeriod($idx)
+    {
+        $x = sizeof($this->rows[$idx]['when']) - 1;
+        $newDate = \Carbon\Carbon::create($this->rows[$idx]['when'][$x]["year"] . "-" . $this->rows[$idx]['when'][$x]["month"] . '-01')->addMonth();
+        $this->rows[$idx]['when'][] = array('month' => $newDate->format("n"), 'year' => $newDate->format("Y"), 'period' => '');
+        //$this->rows[$idx]['when'][] = array('month' => date("n"), 'year' => date("Y"), 'period' => '');
+    }
+
+    public function delPeriod($idx, $xxx)
+    {
+        array_splice($this->rows[$idx]['when'], $xxx, 1);
+        // $this->emit('load-select');
+    }
+
+    public function getTotal()
+    {
+        $total = 0.00;
+        foreach ($this->rows as $r) {
+            $total += $this->currencyToDouble($r["amount"] != null ? $r["amount"] : 0);
+        }
+        return formatPrice($total);
+        // $this->emit('load-select');
+    }
+
+    public function setCausal($id, $idx)
+    {
+        $this->rows[$idx]["causal_id"] = $id;
+    }
+
+    public function multiPeriod()
+    {
+        $this->multiP = true;
+    }
+
+    public function multiPeriodCreate($idx)
+    {
+
+        $period = \Carbon\CarbonPeriod::create($this->multiYearFrom . '-' . $this->multiMonthFrom . '-01', '1 month', $this->multiYearTo . '-' . $this->multiMonthTo . '-01');
+        $this->rows[$idx]['when'] = [];
+        foreach ($period as $dt) {
+            if (!in_array(array('month' => $dt->format("n"), 'year' => $dt->format("Y"), 'period' => ''), $this->rows[$idx]['when']))
+                $this->rows[$idx]['when'][] = array('month' => $dt->format("n"), 'year' => $dt->format("Y"), 'period' => '');
+        }
+
+        $this->multiP = false;
+    }
+
+    public function multiPeriodCancel()
+    {
+        $this->multiP = false;
+    }
+
+
+
+    public function importReceipts()
+    {
+        $this->validate([
+            'selectedCausal' => 'required|exists:causals,id',
+        ]);
+        Log::info("Importazione ricevute: " . json_encode($this->receiptFiles));
+
+        try {
+            $importCount = 0;
+            $updateCount = 0;
+            $errorsCount = 0;
+            $errorMessages = [];
+            $importedFiles = [];
+            $updatedFiles = [];
+            $errorFiles = [];
+            $totalFiles = count($this->receiptFiles);
+
+            // disabilita select
+            $this->emit('import-started');
+            Log::info("Import iniziato");
+
+            foreach ($this->receiptFiles as $index => $receiptFile) {
+                try {
+                    $fileName = $receiptFile->getClientOriginalName();
+                    Log::info("Elaborazione file: " . $fileName);
+
+                    // Carica e analizza il file XML
+                    $xmlString = file_get_contents($receiptFile->getRealPath());
+                    $xml = simplexml_load_string($xmlString);
+
+                    if (empty($xmlString)) {
+                        throw new \Exception("Il file è vuoto.");
+                    }
+
+                    // Verifica se il file sembra essere un XML
+                    if (strpos($xmlString, '<?xml') === false && strpos($xmlString, '<') === false) {
+                        throw new \Exception("Il file non sembra essere in formato XML.");
+                    }
+
+                    $xml = simplexml_load_string($xmlString);
+                    if (!$xml) {
+                        throw new \Exception("Impossibile analizzare il file XML");
+                    }
+
+                    // Estrai i dati dalla fattura elettronica
+                    $fatturaData = $this->extractFatturaData($xml);
+
+                    // Trova o crea il fornitore
+                    $supplier = $this->findOrCreateSupplier($fatturaData);
+
+                    // Trova il metodo di pagamento
+                    $paymentMethodId = $this->findPaymentMethod($fatturaData['modalitaPagamento']);
+
+                    // Crea il record principale
+                    $isUpdate = false;
+                    $existingRecord = \App\Models\Record::where('supplier_id', $supplier->id)
+                        ->where('numero_fattura', $fatturaData['numeroFattura'])
+                        ->first();
+
+                    if ($existingRecord) {
+                        $record = $this->updateRecord($existingRecord, $paymentMethodId, $fatturaData);
+                        $isUpdate = true;
+                        $updateCount++;
+                        $updatedFiles[] = $fileName;
+                        Log::info("Fattura aggiornata con successo: {$fatturaData['numeroFattura']}, Fornitore: {$supplier->name}");
+                    } else {
+                        // Crea un nuovo record
+                        $record = $this->createRecord($supplier->id, $paymentMethodId, $fatturaData);
+                        $importCount++;
+                        $importedFiles[] = $fileName;
+                        Log::info("Fattura importata con successo: {$fatturaData['numeroFattura']}, Fornitore: {$supplier->name}");
+                    }
+
+                    $this->recordFileService->createRecordFolders($record->id, 'OUT');
+
+                    try {
+                        $xmlAttachmentPath = $this->recordFileService->uploadXmlReceipt($receiptFile, $record->id, 'OUT');
+                        $record->update(['attachment' => $xmlAttachmentPath]);
+
+                        Log::info("XML receipt stored as attachment: " . $xmlAttachmentPath);
+                    } catch (\Exception $ex) {
+                        Log::warning("Could not store XML as attachment: " . $ex->getMessage());
+                    }
+
+                    // Crea il record row
+                    $this->createRecordRow($record->id, $fatturaData);
+
+                    Log::info("Fattura importata con successo: {$fatturaData['numeroFattura']}, Fornitore: {$fatturaData['denominazione']} OR {$fatturaData['cognome']}, {$fatturaData['nome']}");
+                } catch (\Exception $e) {
+                    $errorMsg = $e->getMessage();
+
+                    $friendlyErrorMsg = $this->getFriendlyErrorMessage($errorMsg);
+
+                    Log::error("Errore originale: " . $errorMsg);
+                    $errorMessages[] = $friendlyErrorMsg;
+                    $errorFiles[] = $fileName;
+                    $errorsCount++;
+                }
+
+                $progress = ($index + 1) / $totalFiles * 100;
+                $this->emit('update-progress', $progress);
+            }
+
+            $this->showResultMessages($importCount, $updateCount, $errorsCount, $importedFiles, $updatedFiles, $errorFiles, $errorMessages);
+        } catch (\Exception $e) {
+            $errorMsg = 'Errore durante l\'importazione dei file XML: ' . $e->getMessage();
+            Log::error($errorMsg);
+            $this->emit('show-import-result', [
+                'message' => $errorMsg,
+                'type' => 'error'
+            ]);
+        } finally {
+            $this->emit('load-data-table');
+        }
+    }
+    /**
+     * Estrae i dati dalla fattura elettronica XML
+     */
+    private function extractFatturaData($xml)
+    {
+        $data = [];
+
+        try {
+            // Stampa la struttura XML per debug
+            Log::info("Nomi dei children dell'elemento radice: " . implode(", ", array_map(function ($node) {
+                return $node->getName();
+            }, iterator_to_array($xml->children()))));
+
+            $headerNode = $xml->FatturaElettronicaHeader;
+            $bodyNode = $xml->FatturaElettronicaBody;
+
+            if (!$headerNode || !$bodyNode) {
+                throw new \Exception("Struttura XML non standard, FatturaElettronicaHeader o FatturaElettronicaBody non trovati");
+            }
+
+            // Estrai dati del fornitore (cedente/prestatore)
+            $cedenteNode = $headerNode->CedentePrestatore;
+            $datiAnagrafici = $cedenteNode->DatiAnagrafici;
+            $idFiscaleIVA = $datiAnagrafici->IdFiscaleIVA;
+
+            $data['idPaese'] = (string)$idFiscaleIVA->IdPaese;
+            $data['idCodice'] = (string)$idFiscaleIVA->IdCodice;
+            $data['partitaIva'] = $data['idPaese'] . $data['idCodice'];
+            $data['codiceFiscale'] = (string)$datiAnagrafici->CodiceFiscale;
+            $data['denominazione'] = (string)$datiAnagrafici->Anagrafica->Denominazione;
+
+            $data['cognome'] = (string)$datiAnagrafici->Anagrafica->Cognome;
+            $data['nome'] = (string)$datiAnagrafici->Anagrafica->Nome;
+
+            // Estrai dati della sede
+            $sede = $cedenteNode->Sede;
+            $data['indirizzo'] = (string)$sede->Indirizzo;
+            $data['cap'] = (string)$sede->CAP;
+            $data['comune'] = (string)$sede->Comune;
+            $data['provincia'] = (string)$sede->Provincia;
+            $data['nazione'] = (string)$sede->Nazione ?: 'IT';
+
+            // Email, se presente
+            $data['email'] = '';
+            if (isset($cedenteNode->Contatti) && isset($cedenteNode->Contatti->Email)) {
+                $data['email'] = (string)$cedenteNode->Contatti->Email;
+            }
+
+            // Dati generali della fattura
+            $datiGeneraliDocumento = $bodyNode->DatiGenerali->DatiGeneraliDocumento;
+            $data['tipoDocumento'] = (string)$datiGeneraliDocumento->TipoDocumento;
+            $data['divisa'] = (string)$datiGeneraliDocumento->Divisa ?: 'EUR';
+            $data['dataDocumento'] = (string)$datiGeneraliDocumento->Data;
+            $data['numeroFattura'] = (string)$datiGeneraliDocumento->Numero;
+            $data['importoTotale'] = (float)$datiGeneraliDocumento->ImportoTotaleDocumento;
+
+            // Dati di pagamento
+            $data['condizioniPagamento'] = '';
+            $data['modalitaPagamento'] = '';
+            $data['dataScadenza'] = '';
+            $data['iban'] = '';
+            $data['bic'] = '';
+            $data['istitutoFinanziario'] = '';
+
+            if (isset($bodyNode->DatiPagamento)) {
+                $datiPagamento = $bodyNode->DatiPagamento;
+                $data['condizioniPagamento'] = (string)$datiPagamento->CondizioniPagamento;
+
+                if (isset($datiPagamento->DettaglioPagamento)) {
+                    $dettaglioPagamento = $datiPagamento->DettaglioPagamento;
+                    $data['modalitaPagamento'] = (string)$dettaglioPagamento->ModalitaPagamento;
+                    $data['dataScadenza'] = (string)$dettaglioPagamento->DataScadenzaPagamento;
+                    $data['iban'] = (string)$dettaglioPagamento->IBAN;
+                    $data['bic'] = (string)$dettaglioPagamento->BIC;
+                    $data['istitutoFinanziario'] = (string)$dettaglioPagamento->IstitutoFinanziario;
+                }
+            }
+
+            // Estrai le linee di dettaglio
+            $data['linee'] = [];
+            if (isset($bodyNode->DatiBeniServizi) && isset($bodyNode->DatiBeniServizi->DettaglioLinee)) {
+                foreach ($bodyNode->DatiBeniServizi->DettaglioLinee as $index => $linea) {
+                    if (isset($linea->Quantita)) {
+                        $lineaData = [
+                            'numeroLinea' => (int)($linea->NumeroLinea ?? ($index + 1)),
+                            'descrizione' => (string)($linea->Descrizione ?? ''),
+                            'quantita' => (float)($linea->Quantita ?? 1),
+                            'prezzoUnitario' => (float)($linea->PrezzoUnitario ?? 0),
+                            'prezzoTotale' => (float)($linea->PrezzoTotale ?? 0),
+                            'aliquotaIva' => (float)($linea->AliquotaIVA ?? 0),
+                        ];
+
+                        // Calcola il prezzo totale se non presente
+                        if ($lineaData['prezzoTotale'] == 0) {
+                            $lineaData['prezzoTotale'] = $lineaData['quantita'] * $lineaData['prezzoUnitario'];
+                        }
+
+                        $data['linee'][] = $lineaData;
+                    } else {
+                        // Se la linea ha un campo Quantita, non la consideriamo
+                        Log::info("Linea con Quantita non considerata: " . json_encode($linea));
+                    }
+                }
+            }
+
+            $data['riepilogo'] = null;
+            if (isset($bodyNode->DatiBeniServizi) && isset($bodyNode->DatiBeniServizi->DatiRiepilogo)) {
+                $riepilogoNode = $bodyNode->DatiBeniServizi->DatiRiepilogo;
+                if ($riepilogoNode) {
+                    $data['riepilogo'] = [
+                        'aliquotaIva' => (float)($riepilogoNode->AliquotaIVA ?? 0),
+                        'imponibile' => (float)($riepilogoNode->ImponibileImporto ?? 0),
+                        'imposta' => (float)($riepilogoNode->Imposta ?? 0),
+                    ];
+                }
+            }
+
+            Log::info("Dati estratti dalla fattura: P.IVA={$data['partitaIva']}, Denominazione={$data['denominazione']},Nome={$data['nome']},Cognome={$data['cognome']} , Numero={$data['numeroFattura']}, Importo={$data['importoTotale']}");
+
+            return $data;
+        } catch (\Exception $e) {
+            Log::error("Errore nell'estrazione dei dati XML: " . $e->getMessage());
+            throw $e;
+        }
+    }
+
+
+    /**
+     * Trova o crea un fornitore basato sui dati della fattura
+     */
+    private function findOrCreateSupplier($fatturaData)
+    {
+        $supplier = \App\Models\Supplier::where('vat', $fatturaData['partitaIva'])->first();
+
+        if (!$supplier) {
+            Log::info("Creazione nuovo fornitore con P.IVA: {$fatturaData['partitaIva']} ({$fatturaData['denominazione']}) OR ({$fatturaData['nome']}), ({$fatturaData['cognome']})");
+
+            $countryId = $this->getCountryId($fatturaData['nazione']);
+            $provinceId = $this->getProvinceId($fatturaData['provincia']);
+            $cityId = $this->getCityId($fatturaData['comune']);
+
+            $supplier = new \App\Models\Supplier();
+            $supplier->name = $fatturaData['denominazione'] ?: $fatturaData['cognome'] . ' ' . $fatturaData['nome'];
+            Log::info("Nome fornitore: " . $supplier->name);
+            $supplier->vat = $fatturaData['partitaIva'];
+            $supplier->fiscal_code = $fatturaData['codiceFiscale'];
+            $supplier->address = $fatturaData['indirizzo'];
+            $supplier->city_id = $cityId;
+            $supplier->zip_code = $fatturaData['cap'];
+            $supplier->province_id = $provinceId;
+            $supplier->nation_id = $countryId;
+            $supplier->email = $fatturaData['email'];
+            $supplier->save();
+
+            Log::info("Fornitore creato con ID: " . $supplier->id);
+        }
+
+        return $supplier;
+    }
+
+    /**
+     * Trova l'ID della nazione dal codice
+     */
+    private function getCountryId($nationCode)
+    {
+        $country = DB::table('nations')->where('code', $nationCode)->first();
+        return $country ? $country->id : null;
+    }
+
+    /**
+     * Trova l'ID della provincia dal codice
+     */
+    private function getProvinceId($provinceCode)
+    {
+        $province = DB::table('provinces')->where('code', $provinceCode)->first();
+        return $province ? $province->id : null;
+    }
+
+    /**
+     * Trova l'ID della città dal nome
+     */
+    private function getCityId($cityName)
+    {
+        if (empty($cityName)) return null;
+
+        $city = DB::table('cities')->where('name', $cityName)->first();
+        return $city ? $city->id : null;
+    }
+
+    /**
+     * Trova l'ID del metodo di pagamento dal codice
+     */
+    private function findPaymentMethod($paymentCode)
+    {
+        if (!empty($paymentCode)) {
+            $paymentMethod = DB::table('payment_methods')->where('code', $paymentCode)->first();
+            if ($paymentMethod) {
+                Log::info("Metodo di pagamento trovato: $paymentCode (ID: {$paymentMethod->id})");
+                return $paymentMethod->id;
+            }
+        }
+
+        // Cerca il metodo di pagamento predefinito - corretto per evitare l'errore della colonna 'default'
+        // Verifica se esiste una colonna 'is_default' o simile
+        $possibleDefaultColumns = ['is_default', 'is_default_method', 'default_method', 'primary'];
+        $defaultPaymentMethod = null;
+
+        foreach ($possibleDefaultColumns as $column) {
+            if (Schema::hasColumn('payment_methods', $column)) {
+                $defaultPaymentMethod = DB::table('payment_methods')->where($column, 1)->first();
+                if ($defaultPaymentMethod) {
+                    Log::info("Usando metodo di pagamento predefinito (colonna $column) ID: {$defaultPaymentMethod->id}");
+                    break;
+                }
+            }
+        }
+
+        // Se non è stato trovato un metodo predefinito, prendi il primo disponibile
+        if (!$defaultPaymentMethod) {
+            $defaultPaymentMethod = DB::table('payment_methods')->first();
+            if ($defaultPaymentMethod) {
+                Log::info("Usando il primo metodo di pagamento disponibile ID: {$defaultPaymentMethod->id}");
+            }
+        }
+
+        if ($defaultPaymentMethod) {
+            return $defaultPaymentMethod->id;
+        }
+
+        throw new \Exception("Nessun metodo di pagamento disponibile nel sistema");
+    }
+
+    /**
+     * Crea un record nella tabella records
+     */
+    private function createRecord($supplierId, $paymentMethodId, $fatturaData)
+    {
+        $record = new \App\Models\Record();
+        $record->supplier_id = $supplierId;
+        $record->payment_method_id = $paymentMethodId;
+        $record->date = $fatturaData['dataDocumento'];
+        //$record->data_pagamento = $fatturaData['dataDocumento'];
+        $record->numero_fattura = $fatturaData['numeroFattura'];
+        $record->type = 'OUT';
+        $record->commercial = 1;
+        $record->corrispettivo_fiscale = 0;
+        $record->deleted = 0;
+        $record->financial_movement = 1;
+        $record->amount = $fatturaData['importoTotale'];
+        $record->tipo_documento = $this->mapTipoDocumento($fatturaData['tipoDocumento']);
+
+        $record->is_ricevuta = true;
+
+        if ($record->data_pagamento != null) {
+            $record->is_paid = true;
+        } else {
+            $record->is_paid = false;
+        }
+
+        if (isset($fatturaData['condizioniPagamento']) && !empty($fatturaData['condizioniPagamento'])) {
+            $record->condizioni_pagamento = $this->mapCondizioniPagamento($fatturaData['condizioniPagamento']);
+        }
+        if (isset($fatturaData['iban']) && !empty($fatturaData['iban'])) {
+            $record->IBAN = $fatturaData['iban'];
+        }
+        if (isset($fatturaData['bic']) && !empty($fatturaData['bic'])) {
+            $record->BIC = $fatturaData['bic'];
+        }
+        if (isset($fatturaData['dataScadenza']) && !empty($fatturaData['dataScadenza'])) {
+            $record->data_scadenza = $fatturaData['dataScadenza'];
+        }
+        $record->save();
+
+        Log::info("Nuovo record creato con ID: " . $record->id);
+
+        return $record;
+    }
+
+    /**
+     * Aggiorna un record esistente nella tabella records
+     */
+    private function updateRecord($record, $paymentMethodId, $fatturaData)
+    {
+        $record->payment_method_id = $paymentMethodId;
+        $record->date = $fatturaData['dataDocumento'];
+        $record->data_pagamento = $fatturaData['dataDocumento'];
+        $record->type = 'OUT';
+        $record->commercial = 1;
+        $record->corrispettivo_fiscale = 0;
+        $record->deleted = 0;
+        $record->financial_movement = 1;
+        $record->amount = $fatturaData['importoTotale'];
+        $record->tipo_documento = $this->mapTipoDocumento($fatturaData['tipoDocumento']);
+
+        $record->is_ricevuta = true;
+        if (isset($fatturaData['condizioniPagamento']) && !empty($fatturaData['condizioniPagamento'])) {
+            $record->condizioni_pagamento = $this->mapCondizioniPagamento($fatturaData['condizioniPagamento']);
+        }
+        if (isset($fatturaData['iban']) && !empty($fatturaData['iban'])) {
+            $record->IBAN = $fatturaData['iban'];
+        }
+        if (isset($fatturaData['bic']) && !empty($fatturaData['bic'])) {
+            $record->BIC = $fatturaData['bic'];
+        }
+        if (isset($fatturaData['dataScadenza']) && !empty($fatturaData['dataScadenza'])) {
+            $record->data_scadenza = $fatturaData['dataScadenza'];
+        }
+        $record->save();
+
+        Log::info("Record esistente aggiornato con ID: " . $record->id);
+
+        return $record;
+    }
+
+
+    /**
+     * Crea un record row per il record specificato
+     */
+    private function createRecordRow($recordId, $fatturaData)
+    {
+        Log::info("Inizio creazione RecordRow per Record ID: " . $recordId);
+        $existingRows = \App\Models\RecordRow::where('record_id', $recordId)->get();
+
+        // Delete existing rows if they exist
+        if ($existingRows->count() > 0) {
+            Log::info("Eliminazione di " . $existingRows->count() . " righe record esistenti per Record ID: " . $recordId);
+            foreach ($existingRows as $row) {
+                $row->delete();
+            }
+        }
+
+        if (!empty($fatturaData['linee'])) {
+            foreach ($fatturaData['linee'] as $linea) {
+                $this->createSingleRecordRow($recordId, $fatturaData, $linea);
+            }
+        } else if ($fatturaData['riepilogo']) {
+            $this->createRecordRowFromRiepilogo($recordId, $fatturaData);
+            Log::info("Creata una riga di record dai dati di riepilogo");
+        } else {
+            $this->createDefaultRecordRow($recordId, $fatturaData);
+            Log::info("Creata una riga di record predefinita dall'importo totale");
+        }
+    }
+
+    private function createSingleRecordRow($recordId, $fatturaData, $linea)
+    {
+        Log::info("Inizio creazione riga record singola per Record ID: " . $recordId . ", Linea: " . json_encode($linea));
+
+        $dataObj = new \DateTime($fatturaData['dataDocumento']);
+        $month = $dataObj->format('n');
+        $year = $dataObj->format('Y');
+        $period = "$month-$year";
+
+        $whenData = [[
+            'month' => $month,
+            'year' => $year,
+            'period' => $period
+        ]];
+
+        $vatId = null;
+        if (isset($linea['aliquotaIva']) && $linea['aliquotaIva'] > 0) {
+            $vatId = $this->findOrCreateVat($linea['aliquotaIva']);
+            Log::info("Using VAT ID: $vatId for rate: {$linea['aliquotaIva']}%");
+        }
+
+        $recordRow = new \App\Models\RecordRow();
+        $recordRow->record_id = $recordId;
+        $recordRow->causal_id = $this->selectedCausal;
+        $recordRow->amount = $linea['prezzoTotale'] + $linea['prezzoTotale'] * ($linea['aliquotaIva'] / 100);
+        $recordRow->note = $linea['descrizione'];
+        $recordRow->commercial = 1;
+
+        $recordRow->when = json_encode($whenData);
+        $recordRow->aliquota_iva = $linea['aliquotaIva'];
+        $recordRow->imponibile = $linea['prezzoTotale'];
+        $recordRow->imposta = $linea['prezzoTotale'] * ($linea['aliquotaIva'] / 100);
+        $recordRow->divisa = $fatturaData['divisa'];
+        $recordRow->numero_linea = $linea['numeroLinea'];
+        $recordRow->prezzo_unitario = $linea['prezzoUnitario'];
+        $recordRow->quantita = $linea['quantita'];
+
+        Log::info("Dati riga record prima del salvataggio: " . json_encode([
+            'record_id' => $recordRow->record_id,
+            'causal_id' => $recordRow->causal_id,
+            'amount' => $recordRow->amount,
+            'note' => $recordRow->note,
+            'aliquota_iva' => $recordRow->aliquota_iva,
+            'imponibile' => $recordRow->imponibile,
+            'imposta' => $recordRow->imposta,
+            'divisa' => $recordRow->divisa,
+            'numero_linea' => $recordRow->numero_linea,
+            'prezzo_unitario' => $recordRow->prezzo_unitario,
+            'quantita' => $recordRow->quantita,
+        ]));
+
+        $recordRow->save();
+
+        Log::info("Riga record creata per linea {$linea['numeroLinea']}: {$linea['descrizione']} (€{$linea['prezzoTotale']})");
+    }
+
+    private function createRecordRowFromRiepilogo($recordId, $fatturaData)
+    {
+        Log::info("Inizio creazione riga record da riepilogo per Record ID: " . $recordId . ", Riepilogo: " . json_encode($fatturaData['riepilogo']));
+
+        $riepilogo = $fatturaData['riepilogo'];
+
+        $dataObj = new \DateTime($fatturaData['dataDocumento']);
+        $month = $dataObj->format('n');
+        $year = $dataObj->format('Y');
+        $period = "$month-$year";
+
+        $whenData = [[
+            'month' => $month,
+            'year' => $year,
+            'period' => $period
+        ]];
+
+        $vatId = null;
+        if (isset($riepilogo['aliquotaIva']) && $riepilogo['aliquotaIva'] > 0) {
+            $vatId = $this->findOrCreateVat($riepilogo['aliquotaIva']);
+            Log::info("Using VAT ID: $vatId for rate: {$riepilogo['aliquotaIva']}%");
+        }
+        $total_amount = $riepilogo['imponibile'] + $riepilogo['imposta'];
+        $recordRow = new \App\Models\RecordRow();
+        $recordRow->record_id = $recordId;
+        $recordRow->causal_id = $this->selectedCausal;
+        $recordRow->amount = $total_amount;
+        $recordRow->commercial = 1;
+
+        $recordRow->when = json_encode($whenData);
+        $recordRow->aliquota_iva = $riepilogo['aliquotaIva'];
+        $recordRow->imponibile = $riepilogo['imponibile'];
+        $recordRow->imposta = $riepilogo['imposta'];
+        $recordRow->divisa = $fatturaData['divisa'];
+        $recordRow->numero_linea = 1;
+        $recordRow->quantita = 1;
+
+        Log::info("Dati riga record da riepilogo prima del salvataggio: " . json_encode([
+            'record_id' => $recordRow->record_id,
+            'causal_id' => $recordRow->causal_id,
+            'amount' => $recordRow->amount,
+            'aliquota_iva' => $recordRow->aliquota_iva,
+            'imponibile' => $recordRow->imponibile,
+            'imposta' => $recordRow->imposta,
+            'divisa' => $recordRow->divisa,
+        ]));
+
+        $recordRow->save();
+
+        Log::info("Riga record creata da riepilogo: Imponibile={$riepilogo['imponibile']}, IVA={$riepilogo['aliquotaIva']}%");
+    }
+
+    private function createDefaultRecordRow($recordId, $fatturaData)
+    {
+        Log::info("Inizio creazione riga record predefinita per Record ID: " . $recordId . ", Importo Totale: " . $fatturaData['importoTotale']);
+
+        $dataObj = new \DateTime($fatturaData['dataDocumento']);
+        $month = $dataObj->format('n');
+        $year = $dataObj->format('Y');
+        $period = "$month-$year";
+
+        $whenData = [[
+            'month' => $month,
+            'year' => $year,
+            'period' => $period
+        ]];
+        $recordRow = new \App\Models\RecordRow();
+        $recordRow->record_id = $recordId;
+        $recordRow->causal_id = $this->selectedCausal;
+        $recordRow->amount = $fatturaData['importoTotale'];
+        $recordRow->commercial = 1;
+
+
+        $recordRow->when = json_encode($whenData);
+        $recordRow->divisa = $fatturaData['divisa'];
+        $recordRow->numero_linea = 1;
+        $recordRow->quantita = 1;
+
+        Log::info("Dati riga record predefinita prima del salvataggio: " . json_encode([
+            'record_id' => $recordRow->record_id,
+            'causal_id' => $recordRow->causal_id,
+            'amount' => $recordRow->amount,
+            'divisa' => $recordRow->divisa,
+        ]));
+
+        $recordRow->save();
+
+        Log::info("Riga record predefinita creata con importo totale: {$fatturaData['importoTotale']}");
+    }
+
+    private function mapTipoDocumento($codice)
+    {
+        $tipiDocumento = [
+            'TD01' => 'Fattura',
+            'TD02' => 'Acconto/Anticipo su fattura',
+            'TD03' => 'Acconto/Anticipo su parcella',
+            'TD04' => 'Nota di credito',
+            'TD05' => 'Nota di debito',
+            'TD06' => 'Parcella',
+            'TD16' => 'Integrazione fattura reverse charge interno',
+            'TD17' => 'Integrazione/autofattura per acquisto servizi dall\'estero',
+            'TD18' => 'Integrazione per acquisto di beni intracomunitari',
+            'TD19' => 'Integrazione/autofattura per acquisto di beni ex art.17 c.2 DPR 633/72',
+            'TD20' => 'Autofattura per regolarizzazione e integrazione delle fatture',
+            'TD21' => 'Autofattura per splafonamento',
+            'TD22' => 'Estrazione beni da Deposito IVA',
+            'TD23' => 'Estrazione beni da Deposito IVA con versamento dell\'IVA',
+            'TD24' => 'Fattura differita di cui all\'art.21, comma 4, lett. a)',
+            'TD25' => 'Fattura differita di cui all\'art.21, comma 4, terzo periodo lett. b)',
+            'TD26' => 'Cessione di beni ammortizzabili e per passaggi interni',
+            'TD27' => 'Fattura per autoconsumo o per cessioni gratuite senza rivalsa'
+        ];
+
+        return $tipiDocumento[$codice] ?? $codice;
+    }
+
+    private function mapCondizioniPagamento($codice)
+    {
+        $condizioniPagamento = [
+            'TP01' => 'Pagamento a rate',
+            'TP02' => 'Pagamento completo',
+            'TP03' => 'Anticipo'
+        ];
+
+        return $condizioniPagamento[$codice] ?? $codice;
+    }
+
+    private function showResultMessages($importCount, $updateCount, $errorsCount, $importedFiles = [], $updatedFiles = [], $errorFiles = [], $errorMessages = [])
+    {
+        $message = "";
+        $messageType = "success";
+
+        if ($importCount > 0) {
+            $message .= "Importate correttamente n° $importCount nuove fatture.<br>";
+            if (!empty($importedFiles)) {
+                $message .= "<details><summary>File importati (clicca per espandere)</summary><ul>";
+                foreach ($importedFiles as $file) {
+                    $message .= "<li>" . htmlspecialchars($file) . "</li>";
+                }
+                $message .= "</ul></details><br>";
+            }
+        }
+
+        if ($updateCount > 0) {
+            $message .= "Aggiornate n° $updateCount fatture perchè già presenti.<br>";
+            if (!empty($updatedFiles)) {
+                $message .= "<details><summary>File aggiornati (clicca per espandere)</summary><ul>";
+                foreach ($updatedFiles as $file) {
+                    $message .= "<li>" . htmlspecialchars($file) . "</li>";
+                }
+                $message .= "</ul></details><br>";
+            }
+        }
+
+        if ($errorsCount > 0) {
+            $message .= "Importazione fallita per n° $errorsCount fatture.<br>";
+
+            // Aggiungi i dettagli degli errori se disponibili
+            if (!empty($errorFiles)) {
+                $message .= "<details><summary>File con errori (clicca per espandere)</summary><ul>";
+                foreach ($errorFiles as $index => $file) {
+                    $errorMessage = isset($errorMessages[$index]) ? ': ' . preg_replace('/^Errore durante l\'importazione della fattura \([^)]+\):/', '', $errorMessages[$index]) : '';
+                    $message .= "<li>" . htmlspecialchars($file) . $errorMessage . "</li>";
+                }
+                $message .= "</ul></details><br>";
+            }
+
+            if ($importCount == 0 && $updateCount == 0) {
+                $messageType = "error";
+            } else {
+                $messageType = "warning";
+            }
+        }
+
+        if ($importCount > 0 || $updateCount > 0 || $errorsCount > 0) {
+            $this->emit('show-import-result', [
+                'message' => $message,
+                'type' => $messageType
+            ]);
+        } else {
+            $this->emit('show-import-result', [
+                'message' => 'Nessuna fattura importata. Controlla i file XML e riprova.',
+                'type' => 'error'
+            ]);
+        }
+
+        // Resetta i campi del form dopo l'importazione
+        $this->reset(['receiptFiles']);
+    }
+
+    private function getFriendlyErrorMessage($errorMessage)
+    {
+        // Errore di parsing XML iniziale
+        if (strpos($errorMessage, "simplexml_load_string(): Entity: line 1: parser error : Start tag expected, '<' not found") !== false) {
+            return "Il file non è in formato valido. Potrebbe essere danneggiato o in un formato diverso.";
+        }
+
+        // Errore di struttura XML
+        if (strpos($errorMessage, "FatturaElettronicaHeader o FatturaElettronicaBody non trovati") !== false) {
+            return "Il file non sembra essere una fattura elettronica valida. Mancano le sezioni principali.";
+        }
+
+        // Altri errori comuni di parsing XML
+        if (strpos($errorMessage, "parser error") !== false) {
+            return "Il file XML contiene errori di formattazione e non può essere letto correttamente.";
+        }
+
+        // Errori di struttura interna
+        if (
+            strpos($errorMessage, "Undefined index") !== false ||
+            strpos($errorMessage, "Trying to get property") !== false
+        ) {
+            return "La fattura è incompleta o non contiene tutti i dati necessari.";
+        }
+
+        // Per altri errori, conserva il messaggio originale ma semplificalo
+        return $errorMessage;
+    }
+
+    public function getVats()
+    {
+        $vats = array();
+        foreach ($this->rows as $r) {
+            if ($r["amount"] != null && $r["amount"] != "" && $r["vat_id"] > 0) {
+                $vat = getVatValue($this->currencyToDouble($r["amount"]), $r["vat_id"]);
+                $vatName = "";
+                foreach ($this->vats as $v) {
+                    if ($v->id == $r["vat_id"])
+                        $vatName = $v->name;
+                }
+                if (isset($vats[$vatName]))
+                    $vats[$vatName] += $vat;
+                else
+                    $vats[$vatName] = $vat;
+            }
+        }
+        return $vats;
+    }
+
+    private function findOrCreateVat($vatRate)
+    {
+        $vatRate = (float)$vatRate;
+        Log::info("Searching for VAT rate: $vatRate%");
+
+        $existingVat = \App\Models\Vat::where(function ($query) use ($vatRate) {
+            $query->whereRaw('ABS(value - ?) < 0.01', [$vatRate]);
+        })->first();
+
+        if ($existingVat) {
+            Log::info("Found existing VAT rate: ID={$existingVat->id}, Name={$existingVat->name}, Value={$existingVat->value}");
+            return $existingVat->id;
+        }
+
+        Log::info("Creating new VAT rate: $vatRate%");
+
+        $vatName = "IVA $vatRate%";
+
+        $newVat = new \App\Models\Vat();
+        $newVat->name = $vatName;
+        $newVat->value = $vatRate;
+        $newVat->enabled = 1;
+        $newVat->save();
+
+        Log::info("Created new VAT rate: ID={$newVat->id}, Name={$newVat->name}, Value={$newVat->value}");
+
+        $this->vats = \App\Models\Vat::select('id', 'name', 'value')->orderBy('value')->get();
+
+        return $newVat->id;
+    }
+
+    public function viewData($id)
+    {
+        Log::info("Visualizzazione dati per ID: " . $id);
+        try {
+            $record = \App\Models\Record::with(['supplier', 'payment_method', 'rows.causal'])->findOrFail($id);
+
+            if (!$record) {
+                $this->emit('flash-error', 'Movimento non trovato');
+                return;
+            }
+
+            $record->formatted_date = date("d/m/Y", strtotime($record->date));
+            $record->formatted_data_pagamento = $record->data_pagamento ? date("d/m/Y", strtotime($record->data_pagamento)) : 'Non impostata';
+            $record->supplier_name = $record->supplier ? $record->supplier->name : 'N/A';
+            $record->payment_method_name = $record->payment_method ? $record->payment_method->name : 'N/A';
+            $record->formatted_amount = formatPrice($record->amount);
+            $record->payment_status = $record->is_paid ? 'Pagato' : 'Da Pagare';
+
+            foreach ($record->rows as $row) {
+                $row->causal_name = $row->causal ? $row->causal->getTree() : 'N/A';
+                $row->formatted_imponibile = $row->imponibile ? formatPrice($row->imponibile) : 'N/A';
+                $row->iva = $row->aliquota_iva ? formatPrice($row->aliquota_iva) : 'N/A';
+                $row->formatted_imposta = $row->imposta ? formatPrice($row->imposta) : 'N/A';
+                $row->formatted_amount = formatPrice($row->amount);
+            }
+
+            if ($record->attachment && !empty($record->attachment)) {
+                $record->attachment_url = $this->getAttachmentUrl($record->attachment);
+
+                $attachmentPath = $record->attachment;
+                if (strpos($attachmentPath, '/') !== false) {
+                    $record->attachment_filename = basename($attachmentPath);
+                } else {
+                    $record->attachment_filename = $attachmentPath;
+                }
+
+                Log::info("Allegato trovato: " . $record->attachment_url);
+            } else {
+                $record->attachment_url = null;
+                $record->attachment_filename = null;
+                Log::info("Nessun allegato presente");
+            }
+
+            Log::info("Emitting show-record-details event");
+            $this->dispatchBrowserEvent('show-record-details', ['record' => $record]);
+        } catch (\Exception $e) {
+            Log::error("Errore nel caricamento dei dettagli: " . $e->getMessage());
+            $this->emit('flash-error', 'Errore nel caricamento dei dettagli: ' . $e->getMessage());
+        }
+    }
+
+    public function getUploadProgressProperty()
+    {
+        return $this->uploadProgress;
+    }
+
+    /**
+     * Get upload status
+     */
+    public function getUploadStatusProperty()
+    {
+        return $this->attachmentUploadStatus;
+    }
+
+    /**
+     * Check if file upload is in progress
+     */
+    public function getIsUploadingProperty()
+    {
+        return $this->attachmentUploadStatus === 'pending';
+    }
+
+    /**
+     * Get estimated upload time remaining (in seconds)
+     */
+    public function getEstimatedTimeRemainingProperty()
+    {
+        if (!$this->uploadStartTime || $this->uploadProgress <= 0) {
+            return null;
+        }
+
+        $elapsed = microtime(true) - $this->uploadStartTime;
+        $rate = $this->uploadProgress / $elapsed;
+        $remaining = (100 - $this->uploadProgress) / $rate;
+
+        return max(0, round($remaining));
+    }
+
+    /**
+     * Get file upload speed (KB/s)
+     */
+    public function getUploadSpeedProperty()
+    {
+        if (!$this->uploadStartTime || !$this->attachment) {
+            return null;
+        }
+
+        $elapsed = microtime(true) - $this->uploadStartTime;
+        $fileSize = $this->attachment->getSize();
+        $uploadedBytes = ($this->uploadProgress / 100) * $fileSize;
+        $speed = $uploadedBytes / $elapsed; // bytes per second
+
+        return round($speed / 1024, 1); // KB per second
+    }
+}

+ 118 - 0
app/Http/Livewire/Reminder.php

@@ -0,0 +1,118 @@
+<?php
+
+namespace App\Http\Livewire;
+use Livewire\Component;
+
+class Reminder extends Component
+{
+    public $records, $dataId;
+
+    public $filterC = false;
+    public $filterT = false;
+    public $filterType = [];
+    public $hasFilter = false;
+    public $filterFrom = '', $filterTo = '';
+
+    public $sortField ='date';
+    public $sortAsc = true;
+
+    public $cards = array();
+
+    public function mount()
+    {
+        $this->cards = \App\Models\Card::select('id', 'name')->get();
+    }
+
+    public function sortBy($field)
+    {
+        if($this->sortField === $field)
+        {
+            $this->sortAsc = ! $this->sortAsc;
+        } else {
+            $this->sortAsc = true;
+        }
+
+        $this->sortField = $field;
+    }
+
+    public function render()
+    {
+
+        $data = [];
+        $member_cards = \App\Models\MemberCard::with('member')->where('expire_date', '>', date("y-m-d"));
+        if ($this->filterFrom != '')
+            $member_cards = $member_cards->where('expire_date', '>=', $this->filterFrom);
+        if ($this->filterTo != '')
+            $member_cards = $member_cards->where('expire_date', '<=', $this->filterTo);
+        $member_cards = $member_cards->orderBy('expire_date')->get();
+
+        $member_certificates = \App\Models\MemberCertificate::with('member')->where('expire_date', '>', date("y-m-d"));
+        if ($this->filterFrom != '')
+            $member_certificates = $member_certificates->where('expire_date', '>=', $this->filterFrom);
+        if ($this->filterTo != '')
+            $member_certificates = $member_certificates->where('expire_date', '<=', $this->filterTo);
+        $member_certificates = $member_certificates->orderBy('expire_date')->get();
+
+        if (sizeof($this->filterType) > 0)
+        {
+
+            foreach($this->filterType as $type)
+            {
+                if ($type == "C")
+                {
+                    foreach($member_certificates as $member_certificate)
+                    {
+                        $data[] = array('type' => 'Certificato', 'first_name' => $member_certificate->member->first_name, "last_name" => $member_certificate->member->last_name, 'date' => $member_certificate->expire_date);
+                    }
+                }
+                else
+                {
+                    foreach($member_cards as $member_card)
+                    {
+                        if ($member_card->card_id == $type)
+                            $data[] = array('type' => 'Tessera ' . $member_card->card->name, 'first_name' => $member_card->member->first_name, "last_name" => $member_card->member->last_name, 'date' => $member_card->expire_date);
+                    }
+                }
+            }
+        }
+        else
+        {
+            foreach($member_certificates as $member_certificate)
+            {
+                $data[] = array('type' => 'Certificato', 'first_name' => $member_certificate->member->first_name, "last_name" => $member_certificate->member->last_name, 'date' => $member_certificate->expire_date);
+            }
+            foreach($member_cards as $member_card)
+            {
+                $data[] = array('type' => 'Tessera ' . $member_card->card->name, 'first_name' => $member_card->member->first_name, "last_name" => $member_card->member->last_name, 'date' => $member_card->expire_date);
+            }
+        }
+        if ($data)
+        {
+            $key_values = array_column($data, 'date');
+            array_multisort($key_values, SORT_ASC, $data);
+            $this->records = $data;
+        }
+        else
+        {
+            $this->records = [];
+        }
+        $this->emit('load-data-table');
+
+        return view('livewire.reminders');
+    }
+
+    public function search()
+    {
+        $this->hasFilter = true;
+    }
+
+    public function disableSearch()
+    {
+        $this->filterC = true;
+        $this->filterT = true;
+        $this->filterTo = '';
+        $this->filterFrom = '';
+        $this->hasFilter = false;
+    }
+
+}

+ 727 - 0
app/Http/Livewire/Reports.php

@@ -0,0 +1,727 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+use Illuminate\Support\Facades\Auth;
+use Carbon\Carbon;
+
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+use App\Models\Course;
+use App\Models\MemberCard;
+use App\Http\Middleware\TenantMiddleware;
+
+class Reports extends Component
+{
+    public $type = 'anagrafica';
+    public $seasonFilter;
+
+    public $courses = [];
+    public $selectedCourse = null;
+
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+
+    }
+
+    public function mount()
+    {
+        if (Auth::user()->level != env('LEVEL_ADMIN', 0))
+            return redirect()->to('/reports');
+
+        if (isset($_GET["type"]))
+            $this->type = $_GET["type"];
+
+        $this->seasonFilter = $this->getCurrentSeason();
+        $this->courses = $this->getCoursesForSelect();
+    }
+
+    public function render()
+    {
+        return view('livewire.reports');
+    }
+
+
+    private function getCurrentSeason()
+    {
+        $now = Carbon::now();
+        $currentYear = $now->year;
+        if ($now->month >= 9) {
+            return $currentYear . '-' . ($currentYear + 1);
+        } else {
+            return ($currentYear - 1) . '-' . $currentYear;
+        }
+    }
+
+    public function getAvailableSeasons()
+    {
+        $seasons = [];
+        $currentYear = Carbon::now()->year;
+        $startYear = 2023;
+
+        $endYear = Carbon::now()->month >= 9 ? $currentYear + 1 : $currentYear;
+
+        for ($year = $startYear; $year < $endYear; $year++) {
+            $seasons[] = $year . '-' . ($year + 1);
+        }
+
+        return array_reverse($seasons);
+    }
+
+
+    private function parseSeason($season)
+    {
+        $parts = explode('-', $season);
+        return [
+            'start_year' => (int)$parts[0],
+            'end_year' => (int)$parts[1]
+        ];
+    }
+
+
+    private function getSeasonDateRange($season)
+    {
+        $years = $this->parseSeason($season);
+
+        return [
+            'start' => Carbon::create($years['start_year'], 9, 1),
+            'end' => Carbon::create($years['end_year'], 8, 31)
+        ];
+    }
+
+    public function setSelectedCourse($courseId)
+    {
+        $this->selectedCourse = $courseId;
+        Log::info('Selected course set to: ' . $courseId);
+        return $this->getCourseMonthlyEarnings();
+    }
+
+    public function getTesseratiData()
+    {
+        $endYear = $this->parseSeason($this->seasonFilter)['end_year'];
+        return self::getMemberCountChartData($endYear, 3);
+    }
+
+    public function change($type)
+    {
+        $this->type = $type;
+    }
+
+    public function updateCharts()
+    {
+        $this->courses = $this->getCoursesForSelect();
+        $this->emit('chartsUpdated');
+        $this->dispatchBrowserEvent('chartsUpdated');
+    }
+
+    public function updateCourseChart()
+    {
+        $this->emit('chartsUpdated');
+        $this->dispatchBrowserEvent('chartsUpdated');
+    }
+
+    public function updatedSeasonFilter()
+    {
+        $this->courses = $this->getCoursesForSelect();
+        $this->emit('chartsUpdated');
+        $this->dispatchBrowserEvent('chartsUpdated');
+    }
+
+    public function setSeasonFilter($season)
+    {
+        $this->seasonFilter = $season;
+    }
+
+    protected function setupTenantConnection()
+    {
+        $user = auth()->user();
+
+        config(['database.connections.tenant' => [
+            'driver' => 'mysql',
+            'host' => '127.0.0.1',
+            'port' => '3306',
+            'database' => $user->tenant_database,
+            'username' => $user->tenant_username,
+            'password' => $user->tenant_password,
+        ]]);
+
+        config(['database.default' => 'tenant']);
+        DB::purge('tenant');
+        DB::reconnect('tenant');
+    }
+    public function getMonthlyTotals()
+    {
+        Log::info('=== getMonthlyTotals called ===');
+        Log::info('Current seasonFilter: ' . $this->seasonFilter);
+
+        $dateRange = $this->getSeasonDateRange($this->seasonFilter);
+
+        Log::info('Date range start: ' . $dateRange['start']);
+        Log::info('Date range end: ' . $dateRange['end']);
+        $monthOrder = [9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7, 8];
+        $monthNames = ['Set', 'Ott', 'Nov', 'Dic', 'Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago'];
+
+        $incomeData = array_fill(0, 12, 0);
+        $expenseData = array_fill(0, 12, 0);
+        $this->setupTenantConnection();
+
+        $incomeRecords = DB::table('records')
+            ->join('records_rows', 'records.id', '=', 'records_rows.record_id')
+            ->whereBetween('records.date', [$dateRange['start'], $dateRange['end']])
+            ->where('records.type', 'IN')
+            ->select(DB::raw('MONTH(records.date) as month_num'), DB::raw('SUM(records_rows.amount) as total'))
+            ->groupBy('month_num')
+            ->get();
+
+        $expenseRecords = DB::table('records')
+            ->join('records_rows', 'records.id', '=', 'records_rows.record_id')
+            ->whereBetween('records.date', [$dateRange['start'], $dateRange['end']])
+            ->where('records.type', 'OUT')
+            ->select(DB::raw('MONTH(records.date) as month_num'), DB::raw('SUM(records_rows.amount) as total'))
+            ->groupBy('month_num')
+            ->get();
+
+        foreach ($incomeRecords as $record) {
+            $monthIndex = array_search($record->month_num, $monthOrder);
+            if ($monthIndex !== false) {
+                $incomeData[$monthIndex] = $record->total;
+            }
+        }
+
+        foreach ($expenseRecords as $record) {
+            $monthIndex = array_search($record->month_num, $monthOrder);
+            if ($monthIndex !== false) {
+                $expenseData[$monthIndex] = $record->total;
+            }
+        }
+
+        Log::info('Income data: ' . json_encode($incomeData));
+        Log::info('Expense data: ' . json_encode($expenseData));
+
+        return [
+            'labels' => $monthNames,
+            'datasets' => [
+                [
+                    'label' => 'Entrate',
+                    'data' => $incomeData,
+                    'backgroundColor' => 'rgba(54, 162, 235, 0.5)'
+                ],
+                [
+                    'label' => 'Uscite',
+                    'data' => $expenseData,
+                    'backgroundColor' => 'rgba(255, 99, 132, 0.5)'
+                ],
+            ]
+        ];
+    }
+
+    public function getYearlySummary()
+    {
+        $dateRange = $this->getSeasonDateRange($this->seasonFilter);
+
+        $totalIncome = DB::table('records')
+            ->join('records_rows', 'records.id', '=', 'records_rows.record_id')
+            ->whereBetween('records.date', [$dateRange['start'], $dateRange['end']])
+            ->where('records.type', 'IN')
+            ->sum('records_rows.amount');
+
+        $totalExpenses = DB::table('records')
+            ->join('records_rows', 'records.id', '=', 'records_rows.record_id')
+            ->whereBetween('records.date', [$dateRange['start'], $dateRange['end']])
+            ->where('records.type', 'OUT')
+            ->sum('records_rows.amount');
+
+        $delta = $totalIncome - $totalExpenses;
+
+        return [
+            'totalIncome' => $totalIncome,
+            'totalExpenses' => $totalExpenses,
+            'delta' => $delta
+        ];
+    }
+
+    public function getTopCausalsByAmount($limit = 10)
+    {
+        $dateRange = $this->getSeasonDateRange($this->seasonFilter);
+
+        $query = DB::table('records_rows')
+            ->join('records', 'records_rows.record_id', '=', 'records.id')
+            ->join('causals', 'records_rows.causal_id', '=', 'causals.id')
+            ->whereBetween('records.date', [$dateRange['start'], $dateRange['end']]);
+
+        $query->where('records.type', 'IN');
+
+        Log::info('Query: ' . $query->toSql());
+
+        $causals = $query->select(
+            'causals.id',
+            'causals.name',
+            'causals.parent_id',
+            DB::raw('SUM(records_rows.amount) as total_amount')
+        )
+            ->where(function ($query) {
+                $query->where('causals.no_reports', '=', '0')
+                    ->orWhereNull('causals.no_reports');
+            })
+            ->groupBy('causals.id', 'causals.name', 'causals.parent_id')
+            ->orderBy('total_amount', 'desc')
+            ->limit($limit)
+            ->get();
+
+        Log::info('Causals: ' . json_encode($causals));
+
+        $inData = [];
+
+        foreach ($causals as $causal) {
+            $tempCausal = new \App\Models\Causal();
+            $tempCausal->id = $causal->id;
+            $tempCausal->name = $causal->name;
+            $tempCausal->parent_id = $causal->parent_id;
+
+            $treeName = $tempCausal->getTree();
+
+            //$displayName = strlen($treeName) > 30 ? substr($treeName, 0, 27) . '...' : $treeName;
+            $displayName = $treeName;
+            $inData[] = [
+                'label' => $displayName,
+                'value' => $causal->total_amount,
+                'fullName' => $treeName
+            ];
+        }
+
+        usort($inData, function ($a, $b) {
+            return $b['value'] <=> $a['value'];
+        });
+
+        $inData = array_slice($inData, 0, $limit);
+
+        return [
+            'inLabels' => array_column($inData, 'label'),
+            'inData' => $inData,
+            'datasets' => [
+                [
+                    'label' => 'Entrate per Causale',
+                    'data' => array_column($inData, 'value'),
+                ]
+            ]
+        ];
+    }
+
+    public function getCoursesForSelect()
+    {
+        $seasonYears = $this->parseSeason($this->seasonFilter);
+
+        Log::info('Getting courses for season: ' . $this->seasonFilter);
+        Log::info('Season years: ' . json_encode($seasonYears));
+
+        $courses = Course::with(['level', 'frequency'])
+            ->where('active', true)
+            ->where(function ($query) use ($seasonYears) {
+                $query->where('year', $this->seasonFilter)
+                    ->orWhere('year', 'like', '%' . $seasonYears['start_year'] . '-' . $seasonYears['end_year'] . '%')
+                    ->orWhere('year', 'like', '%' . $seasonYears['start_year'] . '%')
+                    ->orWhere('year', 'like', '%' . $seasonYears['end_year'] . '%');
+            })
+            ->orderBy('name')
+            ->get()
+            ->filter(function ($course) use ($seasonYears) {
+                $courseYear = $course->year;
+
+                if ($courseYear === $this->seasonFilter) {
+                    return true;
+                }
+
+                if (
+                    str_contains($courseYear, $seasonYears['start_year']) &&
+                    str_contains($courseYear, $seasonYears['end_year'])
+                ) {
+                    return true;
+                }
+
+                if ($courseYear == $seasonYears['start_year'] || $courseYear == $seasonYears['end_year']) {
+                    return true;
+                }
+
+                return false;
+            })
+            ->map(function ($course) {
+                Log::info('Processing course: ' . $course->name . ' (ID: ' . $course->id . ')' . $course);
+
+                $levelName = is_object($course->level) ? $course->level->name : 'No Level';
+                $typeName = $course->getFormattedTypeField();
+                $frequencyName = is_object($course->frequency) ? $course->frequency->name : 'No Frequency';
+                $year = $course->year ?? '';
+
+                return [
+                    'id' => $course->id,
+                    'name' => $course->name,
+                    'full_name' => "{$course->name} - {$levelName} - {$typeName} - {$frequencyName} ({$year})",
+                    'level_name' => $levelName,
+                    'type_name' => $typeName,
+                    'frequency_name' => $frequencyName,
+                    'year' => $year
+                ];
+            })->values()->toArray();
+
+        Log::info('Found ' . count($courses) . ' courses for season ' . $this->seasonFilter);
+
+        return $courses;
+    }
+
+    public function getMonthlyTotalsForSeason($season)
+    {
+        $originalSeason = $this->seasonFilter;
+        $this->seasonFilter = $season;
+
+        $result = $this->getMonthlyTotals();
+
+        $this->seasonFilter = $originalSeason;
+        return $result;
+    }
+
+
+    public function getTopCausalsByAmountForSeason($season, $limit = 10)
+    {
+        $originalSeason = $this->seasonFilter;
+        $this->seasonFilter = $season;
+
+        $result = $this->getTopCausalsByAmount($limit);
+
+        $this->seasonFilter = $originalSeason;
+        return $result;
+    }
+
+    public function getTesseratiDataForSeason($season)
+    {
+        $originalSeason = $this->seasonFilter;
+        $this->seasonFilter = $season;
+
+        $result = $this->getTesseratiData();
+
+        $this->seasonFilter = $originalSeason;
+        return $result;
+    }
+
+    public function updatedSelectedCourse()
+    {
+        Log::info('updatedSelectedCourse called with: ' . $this->selectedCourse);
+        if ($this->selectedCourse) {
+            $this->emit('courseSelected', $this->selectedCourse);
+            Log::info('Event emitted with course ID: ' . $this->selectedCourse);
+        }
+    }
+    public function getCourseData($courseId)
+    {
+        $this->selectedCourse = $courseId;
+        return $this->getCourseMonthlyEarnings($courseId);
+    }
+    public function getCourseMonthlyEarnings($courseId = null)
+    {
+        $courseId = $courseId ?? $this->selectedCourse;
+        Log::info('Getting earnings for course ID: ' . $courseId);
+
+        if (!$courseId) {
+            return [
+                'labels' => [],
+                'datasets' => [],
+                'tableData' => [],
+                'isEmpty' => true,
+                'message' => 'Seleziona un corso per visualizzare i dati'
+            ];
+        }
+
+        $monthOrder = [9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7, 8];
+        $monthNames = [
+            9 => 'Set',
+            10 => 'Ott',
+            11 => 'Nov',
+            12 => 'Dic',
+            1 => 'Gen',
+            2 => 'Feb',
+            3 => 'Mar',
+            4 => 'Apr',
+            5 => 'Mag',
+            6 => 'Giu',
+            7 => 'Lug',
+            8 => 'Ago'
+        ];
+        $monthNamesExtended = [
+            0 => 'Settembre',
+            1 => 'Ottobre',
+            2 => 'Novembre',
+            3 => 'Dicembre',
+            4 => 'Gennaio',
+            5 => 'Febbraio',
+            6 => 'Marzo',
+            7 => 'Aprile',
+            8 => 'Maggio',
+            9 => 'Giugno',
+            10 => 'Luglio',
+            11 => 'Agosto'
+        ];
+
+        $monthlyData = [];
+        foreach ($monthOrder as $i) {
+            $monthlyData[$i] = [
+                'earned' => 0,
+                'total' => 0,
+                'suspended' => 0,
+                'participants' => 0
+            ];
+        }
+
+        $rates = \App\Models\Rate::whereHas('member_course', function ($query) use ($courseId) {
+            $query->where('course_id', $courseId);
+        })->with('member_course')->get();
+
+        if ($rates->isEmpty()) {
+            return [
+                'labels' => [],
+                'datasets' => [],
+                'tableData' => [],
+                'isEmpty' => true,
+                'message' => 'Nessun dato disponibile per questo corso nella stagione ' . $this->seasonFilter
+            ];
+        }
+
+        $hasData = false;
+
+        foreach ($rates as $rate) {
+            $totalPrice = (float)($rate->price ?? 0);
+
+            if ($rate->months) {
+                $monthsData = json_decode($rate->months, true);
+
+                if (is_array($monthsData) && count($monthsData) > 0) {
+                    $pricePerMonth = $totalPrice / count($monthsData);
+
+                    foreach ($monthsData as $month) {
+                        $monthNumber = (int)$month;
+
+                        if (isset($monthlyData[$monthNumber])) {
+                            $monthlyData[$monthNumber]['total'] += $pricePerMonth;
+                            $monthlyData[$monthNumber]['participants']++;
+                            $hasData = true;
+
+                            // if (!is_null($rate->record_id) && $rate->record_id !== '') {
+                            //     $monthlyData[$monthNumber]['earned'] += $pricePerMonth;
+                            // }
+
+                            // pagamenti effettuati
+                            if (!is_null($rate->record_id) && $rate->record_id !== '' && $rate->status == 1) {
+                                $monthlyData[$monthNumber]['participants']--;
+                                $monthlyData[$monthNumber]['earned'] += $pricePerMonth;
+                            }
+                            // pagamenti sospesi
+                            if ($rate->status == 2) {
+                                $monthlyData[$monthNumber]['participants']--;
+                                $monthlyData[$monthNumber]['total'] -= $pricePerMonth;
+                                $monthlyData[$monthNumber]['suspended']++;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        if (!$hasData) {
+            return [
+                'labels' => [],
+                'datasets' => [],
+                'tableData' => [],
+                'isEmpty' => true,
+                'message' => 'Nessun pagamento registrato per questo corso nella stagione ' . $this->seasonFilter
+            ];
+        }
+
+        $labels = [];
+        $earnedData = [];
+        $totalData = [];
+        $participantData = [];
+        $tableData = [];
+
+        foreach ($monthOrder as $month) {
+            $earned = round($monthlyData[$month]['earned'], 2);
+            $total = round($monthlyData[$month]['total'], 2);
+            $delta = max(0, $total - $earned);
+            $participants = $monthlyData[$month]['participants'];
+            $suspended = $monthlyData[$month]['suspended'];
+
+            $labels[] = $monthNames[$month];
+            $earnedData[] = $earned;
+            $totalData[] = $total;
+            $participantData[] = $participants;
+            $suspendedData[] = $suspended;
+
+            $percentage = $total > 0 ? round(($earned / $total) * 100, 1) : 0;
+
+            $tableData[] = [
+                'month' => $monthNames[$month],
+                'participants' => $participants,
+                'suspended' => $suspended,
+                'earned' => $earned,
+                'total' => $total,
+                'delta' => $delta,
+                'percentage' => $percentage
+            ];
+        }
+
+        $daIncassareData = array_map(function($tot, $inc) {
+            return $tot - $inc;
+        }, $totalData, $earnedData);
+
+
+        return [
+            'labels' => $labels,
+            'datasets' => [
+                [
+                    'label' => 'TOT. INCASSATO',
+                    'data' => $earnedData,
+                    'participantData' => $participantData,
+                    'suspendedData' => $suspendedData,
+                    'monthNamesExtended' => $monthNamesExtended,
+                ],
+                [
+                    'label' => 'TOT. DA INCASSARE',
+                    'data' => $daIncassareData,
+                    'participantData' => $participantData,
+                    'suspendedData' => $suspendedData,
+                    'monthNamesExtended' => $monthNamesExtended,
+                ]
+            ],
+            'tableData' => $tableData,
+            'isEmpty' => false
+        ];
+    }
+
+    public static function getMemberCountChartData($endYear = null, $span = 5)
+    {
+        if ($endYear === null) {
+            $endYear = date('Y');
+        }
+
+        $startYear = $endYear - $span + 1;
+
+        $memberCards = MemberCard::select('member_id', 'expire_date', 'card_id')
+            ->with('card:id,name')
+            ->whereNotNull('expire_date')
+            ->whereNotNull('member_id')
+            ->whereNotNull('card_id')
+            ->where('status', '!=', 'cancelled')
+            ->whereRaw('YEAR(expire_date) >= ?', [$startYear])
+            ->whereRaw('YEAR(expire_date) <= ?', [$endYear])
+            ->get();
+
+        $cardTypes = $memberCards->pluck('card.name')->unique()->filter()->sort()->values();
+
+        $seasonCounts = [];
+        $seasonCardCounts = [];
+
+        for ($year = $startYear; $year <= $endYear; $year++) {
+            $seasonPeriod = ($year - 1) . '-' . $year;
+            $seasonCounts[$seasonPeriod] = [];
+            $seasonCardCounts[$seasonPeriod] = [];
+
+            foreach ($cardTypes as $cardType) {
+                $seasonCardCounts[$seasonPeriod][$cardType] = [];
+            }
+        }
+
+        foreach ($memberCards as $card) {
+            $expireYear = date('Y', strtotime($card->expire_date));
+            $expireMonth = date('n', strtotime($card->expire_date));
+
+            if ($expireMonth >= 9) {
+                $seasonPeriod = $expireYear . '-' . ($expireYear + 1);
+            } else {
+                $seasonPeriod = ($expireYear - 1) . '-' . $expireYear;
+            }
+
+            if (isset($seasonCounts[$seasonPeriod])) {
+                $seasonCounts[$seasonPeriod][$card->member_id] = true;
+
+                $cardTypeName = $card->card->name ?? 'Unknown';
+                if (isset($seasonCardCounts[$seasonPeriod][$cardTypeName])) {
+                    $seasonCardCounts[$seasonPeriod][$cardTypeName][$card->member_id] = true;
+                }
+            }
+        }
+
+        $seasonLabels = [];
+        $memberCountData = [];
+        $cardTypeDatasets = [];
+
+        $colors = [
+            'rgba(255, 99, 132, 0.2)',
+            'rgba(54, 162, 235, 0.2)',
+            'rgba(255, 205, 86, 0.2)',
+            'rgba(75, 192, 192, 0.2)',
+            'rgba(153, 102, 255, 0.2)',
+            'rgba(255, 159, 64, 0.2)',
+            'rgba(199, 199, 199, 0.2)',
+            'rgba(83, 102, 255, 0.2)',
+        ];
+
+        $borderColors = [
+            'rgba(255, 99, 132, 1)',
+            'rgba(54, 162, 235, 1)',
+            'rgba(255, 205, 86, 1)',
+            'rgba(75, 192, 192, 1)',
+            'rgba(153, 102, 255, 1)',
+            'rgba(255, 159, 64, 1)',
+            'rgba(199, 199, 199, 1)',
+            'rgba(83, 102, 255, 1)',
+        ];
+
+        foreach ($cardTypes as $index => $cardType) {
+            $cardTypeDatasets[$cardType] = [
+                'label' => $cardType,
+                'data' => [],
+                'backgroundColor' => $colors[$index % count($colors)],
+                'borderColor' => $borderColors[$index % count($borderColors)],
+                'borderWidth' => 2,
+                'pointBackgroundColor' => $borderColors[$index % count($borderColors)],
+                'pointRadius' => 4,
+                'tension' => 0.3,
+                'fill' => true
+            ];
+        }
+
+        foreach ($seasonCounts as $seasonPeriod => $members) {
+            $seasonLabels[] = $seasonPeriod;
+            $memberCountData[] = count($members);
+
+            foreach ($cardTypes as $cardType) {
+                $cardTypeCount = isset($seasonCardCounts[$seasonPeriod][$cardType])
+                    ? count($seasonCardCounts[$seasonPeriod][$cardType])
+                    : 0;
+                $cardTypeDatasets[$cardType]['data'][] = $cardTypeCount;
+            }
+        }
+
+        $datasets = [
+            [
+                'label' => 'Totale Membri Tesserati',
+                'data' => $memberCountData,
+                'backgroundColor' => 'rgba(54, 162, 235, 0.2)',
+                'borderColor' => 'rgba(54, 162, 235, 1)',
+                'borderWidth' => 3,
+                'pointBackgroundColor' => 'rgba(54, 162, 235, 1)',
+                'pointRadius' => 6,
+                'tension' => 0.3,
+                'fill' => true,
+                'type' => 'line'
+            ]
+        ];
+        foreach ($cardTypeDatasets as $dataset) {
+            $datasets[] = $dataset;
+        }
+
+        return [
+            'labels' => $seasonLabels,
+            'datasets' => $datasets
+        ];
+    }
+}

+ 32 - 0
app/Http/Livewire/Setting.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+
+class Setting extends Component
+{
+
+    public $type = 'anagrafica';
+
+    public function mount()
+    {
+
+        if(\Auth::user()->level != env('LEVEL_ADMIN', 0))
+            return redirect()->to('/dashboard');
+
+        if (isset($_GET["type"]))
+            $this->type = $_GET["type"];
+    }
+
+    public function render()
+    {
+        return view('livewire.settings');
+    }
+
+    public function change($type)
+    {
+        $this->type = $type;
+    }
+
+}

+ 200 - 0
app/Http/Livewire/SmsComunications.php

@@ -0,0 +1,200 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+use Illuminate\Support\Facades\Auth;
+use App\Http\Middleware\TenantMiddleware;
+use App\Models\SmsTemplate;
+use App\Models\SmsScheduled;
+use App\Models\Category;
+use App\Models\Member;
+use App\Models\User;
+use Carbon\Carbon;
+use Illuminate\Support\Facades\Log;
+class SmsComunications extends Component
+{
+    public $records, $subject, $message, $selectedRecipients = [], $scheduledDateTime, $sendNow = true;
+    public $dataId, $update = false, $add = false;
+    public $members = [];
+
+    protected $rules = [
+        'subject' => 'required|string|max:255',
+        'message' => 'required|string|max:160',
+        'selectedRecipients' => 'required|array|min:1',
+        'scheduledDateTime' => 'required_if:sendNow,false|date|after:now',
+    ];
+
+    protected $messages = [
+        'subject.required' => 'L\'oggetto è obbligatorio',
+        'message.required' => 'Il messaggio è obbligatorio',
+        'message.max' => 'Il messaggio non può superare 160 caratteri',
+        'selectedRecipients.required' => 'Seleziona almeno un gruppo di destinatari',
+        'scheduledDateTime.required_if' => 'La data di programmazione è obbligatoria',
+        'scheduledDateTime.after' => 'La data di programmazione deve essere futura',
+    ];
+
+    public $sortField = 'name';
+    public $sortAsc = true;
+
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+    }
+
+    public function sortBy($field)
+    {
+        if($this->sortField === $field) {
+            $this->sortAsc = ! $this->sortAsc;
+        } else {
+            $this->sortAsc = true;
+        }
+
+        $this->sortField = $field;
+    }
+
+    public function resetFields()
+    {
+        $this->subject = '';
+        $this->message = '';
+        $this->selectedRecipients = [];
+        $this->sendNow = true;
+        $this->scheduledDateTime = now()->addHour()->format('Y-m-d\TH:i');
+        $this->emit('load-data-table');
+    }
+
+    public function mount()
+    {
+        if(Auth::user()->level != env('LEVEL_ADMIN', 0))
+            return redirect()->to('/dashboard');
+
+        $this->members = Member::select('id', 'last_name', 'email')->get();
+        $this->scheduledDateTime = now()->addHour()->format('Y-m-d\TH:i');
+    }
+
+    public function render()
+    {
+        $this->records = SmsTemplate::orderBy($this->sortField, $this->sortAsc ? 'asc' : 'desc')->get();
+        return view('livewire.sms_comunications');
+    }
+
+    public function add()
+    {
+        $this->resetFields();
+        $this->add = true;
+        $this->update = false;
+    }
+
+    public function store()
+    {
+        $this->validate();
+
+        try {
+            $template = SmsTemplate::create([
+                'name' => $this->subject,
+                'content' => $this->message,
+                'created_by' => Auth::id(),
+            ]);
+
+            $recipients = User::whereIn('id', $this->selectedRecipients)->get();
+
+            if ($this->sendNow) {
+                $this->sendSmsNow($template, $recipients);
+                session()->flash('success', 'Template creato e SMS inviato a ' . $recipients->count() . ' destinatari!');
+            } else {
+                $this->scheduleSms($template, $recipients);
+                $scheduledDate = Carbon::parse($this->scheduledDateTime)->format('d/m/Y H:i');
+                session()->flash('success', 'Template creato e SMS programmato per ' . $scheduledDate);
+            }
+
+            $this->resetFields();
+            $this->add = false;
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function edit($id)
+    {
+        try {
+            $template = SmsTemplate::findOrFail($id);
+            if (!$template) {
+                session()->flash('error', 'Template SMS non trovato');
+            } else {
+                $this->subject = $template->name;
+                $this->message = $template->content;
+                $this->dataId = $template->id;
+                $this->update = true;
+                $this->add = false;
+            }
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function update()
+    {
+        $this->validate([
+            'subject' => 'required|string|max:255',
+            'message' => 'required|string|max:160',
+        ]);
+
+        try {
+            SmsTemplate::whereId($this->dataId)->update([
+                'name' => $this->subject,
+                'content' => $this->message,
+            ]);
+            session()->flash('success', 'Template SMS aggiornato');
+            $this->resetFields();
+            $this->update = false;
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function cancel()
+    {
+        $this->add = false;
+        $this->update = false;
+        $this->resetFields();
+    }
+
+    public function sendTemplate($id)
+    {
+        try {
+            $template = SmsTemplate::findOrFail($id);
+            $this->subject = $template->name;
+            $this->message = $template->content;
+            $this->add = true;
+            $this->update = false;
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    private function sendSmsNow($template, $recipients)
+    {
+        // Simple SMS sending logic
+        foreach ($recipients as $recipient) {
+            if ($recipient->phone) {
+                // Here you would integrate with your SMS provider
+                Log::info("SMS sent to {$recipient->name} ({$recipient->phone}): {$template->content}");
+            }
+        }
+    }
+
+    private function scheduleSms($template, $recipients)
+    {
+        $scheduled = SmsScheduled::create([
+            'template_id' => $template->id,
+            'content' => $template->content,
+            'scheduled_at' => Carbon::parse($this->scheduledDateTime),
+            'status' => 'scheduled',
+            'created_by' => Auth::id(),
+        ]);
+
+        $scheduled->recipients()->attach($recipients->pluck('id'));
+
+        Log::info("SMS scheduled for {$this->scheduledDateTime} to {$recipients->count()} recipients: {$template->content}");
+    }
+}

+ 397 - 0
app/Http/Livewire/Sponsor.php

@@ -0,0 +1,397 @@
+<?php
+
+namespace App\Http\Livewire;
+use Livewire\Component;
+use Livewire\WithFileUploads;
+
+class Sponsor extends Component
+{
+
+    use WithFileUploads;
+
+    public $records, $name, $fiscal_code, $vat, $address, $zip_code,$nation_id,$province_id,$city_id,$phone,$email,$enabled, $dataId, $update = false, $add = false;
+    public $first_name, $last_name;
+    public $from_date, $to_date, $subscription_date, $amount, $file, $file_old, $updateContract = false, $addContract = false, $contractId;
+    public $isItaly = true;
+
+    public $searchTxt;
+    public $search;
+
+    public $contracts = array();
+
+    public $showReset = false;
+    protected $rules = [
+        'name' => 'required'
+    ];
+
+    protected $messages = [
+        'name.required' => 'Il nome è obbligatorio',
+        'from_date.required' => 'La data è obbligatoria',
+        'to_date.required' => 'La data è obbligatoria',
+        'subscription_date.required' => 'La data è obbligatoria',
+    ];
+
+    public function resetFields(){
+        $this->name = '';
+        $this->first_name = '';
+        $this->last_name = '';
+        $this->fiscal_code = '';
+        $this->vat = '';
+        $this->address = '';
+        $this->zip_code = '';
+        $this->nation_id = null;
+        $this->province_id = null;
+        $this->city_id = null;
+        $this->phone = '';
+        $this->email = '';
+        $this->enabled = true;
+        $this->contracts = array();
+        $this->emit('load-data-table');
+    }
+
+    public function resetContract(){
+        $this->from_date = '';
+        $this->to_date = '';
+        $this->subscription_date = '';
+        $this->amount = 0;
+        $this->file = '';
+        $this->file_old = '';
+    }
+
+    public $sortField ='name';
+    public $sortAsc = true;
+
+    public function sortBy($field)
+    {
+        if($this->sortField === $field)
+        {
+            $this->sortAsc = ! $this->sortAsc;
+        } else {
+            $this->sortAsc = true;
+        }
+
+        $this->sortField = $field;
+    }
+
+    public function mount()
+    {
+
+        if (isset($_GET["new"]))
+            $this->add();
+
+    }
+
+    public function search()
+    {
+        if ($this->searchTxt != '')
+        {
+            $this->search = $this->searchTxt;
+            $this->showReset = true;
+        }
+    }
+    public function resetSearch()
+    {
+        $this->showReset = false;
+        $this->searchTxt = '';
+        $this->search = $this->searchTxt;
+    }
+
+    public function render()
+    {
+        //if ($this->search != '')
+        //    $this->records = \App\Models\Sponsor::where('name', 'LIKE', '%' . $this->search . '%')->orWhere('first_name', 'LIKE', '%' . $this->search . '%')->orWhere('last_name', 'LIKE', '%' . $this->search . '%')->orderBy($this->sortField, $this->sortAsc ? 'asc' : 'desc')->get();
+        //else
+            $this->records = \App\Models\Sponsor::get();
+
+        $this->contracts = array();
+        if ($this->dataId > 0)
+            $this->contracts = \App\Models\SponsorContract::where('sponsor_id', $this->dataId)->orderBy('from_date', 'asc')->get();
+
+        return view('livewire.sponsor');
+    }
+
+    public function hydrate()
+    {
+        $this->emit('load-select');
+    }
+
+    public function checkIsItaly()
+    {
+        $n = \App\Models\Nation::findOrFail($this->nation_id);
+        $this->isItaly = $n->is_italy;
+    }
+
+    public function add()
+    {
+        $this->resetFields();
+        $this->resetContract();
+        $this->emit('load-select');
+        $this->add = true;
+        $this->update = false;
+    }
+
+    public function store()
+    {
+        $this->validate();
+        try {
+            \App\Models\Sponsor::create([
+                'name' => $this->name,
+                'first_name' => $this->first_name,
+                'last_name' => $this->last_name,
+                'fiscal_code' => $this->fiscal_code,
+                'vat' => $this->vat,
+                'address' => $this->address,
+                'zip_code' => $this->zip_code,
+                'nation_id' => $this->nation_id,
+                'province_id' => $this->province_id,
+                'city_id' => $this->city_id,
+                'phone' => $this->phone,
+                'email' => $this->email,
+                'enabled' => $this->enabled
+            ]);
+            session()->flash('success','Sponsor creato');
+            $this->resetFields();
+            $this->add = false;
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function edit($id){
+        $this->emit('load-select');
+        try {
+            $sponsor = \App\Models\Sponsor::findOrFail($id);
+            if( !$sponsor) {
+                session()->flash('error','Sponsor non trovato');
+            } else {
+                $this->name = $sponsor->name;
+                $this->first_name = $sponsor->first_name;
+                $this->last_name = $sponsor->last_name;
+                $this->fiscal_code = $sponsor->fiscal_code;
+                $this->vat = $sponsor->vat;
+                $this->address = $sponsor->address;
+                $this->zip_code = $sponsor->zip_code;
+                $this->nation_id = $sponsor->nation_id;
+                $this->province_id = $sponsor->province_id;
+                $this->city_id = $sponsor->city_id;
+                $this->phone = $sponsor->phone;
+                $this->email = $sponsor->email;
+                $this->enabled = $sponsor->enabled;
+                $this->dataId = $sponsor->id;
+                $this->update = true;
+                $this->add = false;
+                $this->emit('load-provinces', $this->nation_id, 'provinceClass');
+                $this->emit('load-cities', $this->province_id, 'cityClass');
+            }
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function update()
+    {
+        $this->validate();
+        try {
+            \App\Models\Sponsor::whereId($this->dataId)->update([
+                'name' => $this->name,
+                'first_name' => $this->first_name,
+                'last_name' => $this->last_name,
+                'fiscal_code' => $this->fiscal_code,
+                'vat' => $this->vat,
+                'address' => $this->address,
+                'zip_code' => $this->zip_code,
+                'nation_id' => $this->nation_id,
+                'province_id' => $this->province_id,
+                'city_id' => $this->city_id,
+                'phone' => $this->phone,
+                'email' => $this->email,
+                'enabled' => $this->enabled
+            ]);
+            session()->flash('success','Sponsor aggiornato');
+            $this->resetFields();
+            $this->update = false;
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function cancel()
+    {
+        $this->add = false;
+        $this->update = false;
+        $this->resetFields();
+    }
+
+    public function delete($id)
+    {
+        try{
+            \App\Models\Sponsor::find($id)->delete();
+            session()->flash('success',"Sponsor eliminato");
+        }catch(\Exception $e){
+            session()->flash('error','Errore (' . $e->getMessage() . ')');
+        }
+    }
+
+    public function addContract()
+    {
+        $this->resetContract();
+        $this->addContract = true;
+        $this->updateContract = false;
+    }
+
+    public function storeContract()
+    {
+        $rules = [
+            'from_date' => 'required|date',
+            'to_date' => 'required|date',
+            'subscription_date' => 'required|date'
+        ];
+        $this->validate($rules);
+        try {
+
+            $name = '';
+            try{
+
+                if ($this->file)
+                {
+                    $name = md5($this->file . microtime()).'.'.$this->file->extension();
+                    $this->file->storeAs('public', $name);
+                }
+            } catch (\Exception $ex) {
+                //session()->flash('error','Errore (' . $ex->getMessage() . ')');
+            }
+
+            \App\Models\SponsorContract::create([
+                'sponsor_id' => $this->dataId,
+                'from_date' => $this->from_date,
+                'to_date' => $this->to_date,
+                'subscription_date' => $this->subscription_date,
+                'amount' => $this->currencyToDouble($this->amount),
+                'file' => $name
+            ]);
+            session()->flash('success','Contratto creato');
+            $this->resetContract();
+            $this->addContract = false;
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function editContract($id){
+        // $this->emit('load-select');
+        try {
+            $sponsorContract = \App\Models\SponsorContract::findOrFail($id);
+            if( !$sponsorContract) {
+                session()->flash('error','Contratto non trovato');
+            } else {
+                $this->from_date = $sponsorContract->from_date ? date("Y-m-d", strtotime($sponsorContract->from_date)) : "";
+                $this->to_date = $sponsorContract->to_date ? date("Y-m-d", strtotime($sponsorContract->to_date)) : "";
+                $this->subscription_date = $sponsorContract->subscription_date ? date("Y-m-d", strtotime($sponsorContract->subscription_date)) : "";
+                $this->amount = formatPrice($sponsorContract->amount);
+                $this->file_old = $sponsorContract->file;
+                $this->contractId = $sponsorContract->id;
+                $this->updateContract = true;
+                $this->addContract = false;
+            }
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function updateContract()
+    {
+        $rules = [
+            'from_date' => 'required|date',
+            'to_date' => 'required|date',
+            'subscription_date' => 'required|date'
+        ];
+        $this->validate($rules);
+        try {
+
+            $name = '';
+            try{
+
+                if ($this->file)
+                {
+                    $name = md5($this->file . microtime()).'.'.$this->file->extension();
+                    $this->file->storeAs('public', $name);
+                }
+            } catch (\Exception $ex) {
+                //session()->flash('error','Errore (' . $ex->getMessage() . ')');
+            }
+
+            \App\Models\SponsorContract::whereId($this->contractId)->update([
+                'from_date' => $this->from_date,
+                'to_date' => $this->to_date,
+                'subscription_date' => $this->subscription_date,
+                'amount' => $this->currencyToDouble($this->amount),
+                'file' => $name != '' ? $name : $this->file_old,
+            ]);
+            session()->flash('success','Contratto aggiornato');
+            $this->resetContract();
+            $this->updateContract = false;
+        } catch (\Exception $ex) {
+            session()->flash('error','Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function cancelContract()
+    {
+        $this->addContract = false;
+        $this->updateContract = false;
+        $this->resetContract();
+    }
+
+    public function removeFile()
+    {
+        $this->file_old = '';
+    }
+
+    public function deleteContract($id)
+    {
+        try{
+            \App\Models\SponsorContract::find($id)->delete();
+            session()->flash('success',"Contratto eliminato");
+        }catch(\Exception $e){
+            session()->flash('error','Errore (' . $e->getMessage() . ')');
+        }
+    }
+
+    public function getNation($nation)
+    {
+        if ($nation > 0)
+        {
+            $ret = \App\Models\Nation::findOrFail($nation);
+            return $ret->name;
+        }
+        return "";
+    }
+
+    public function getProvince($province)
+    {
+        if ($province > 0)
+        {
+            $ret = \App\Models\Province::findOrFail($province);
+            return $ret->name;
+        }
+        return "";
+    }
+
+    public function getCity($city)
+    {
+        if ($city > 0)
+        {
+            $ret = \App\Models\City::findOrFail($city);
+            return $ret->name;
+        }
+        return "";
+    }
+
+    function currencyToDouble($val)
+    {
+        $x = str_replace("€", "", $val);
+        $x = str_replace(".", "", $x);
+        $x = str_replace(",", ".", $x);
+        return floatval(trim($x));
+    }
+}

+ 359 - 0
app/Http/Livewire/Supplier.php

@@ -0,0 +1,359 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Log;
+use App\Http\Middleware\TenantMiddleware;
+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, $showReset = false;
+    public $isItaly = true;
+
+    public $searchTxt;
+    public $search;
+    public $showArchived = false;
+    protected $rules = [
+        'name' => 'required',
+        'vat' => 'nullable|unique:suppliers,vat',
+        'fiscal_code' => 'nullable|unique:suppliers,fiscal_code'
+    ];
+
+    protected $messages = [
+        'name.required' => 'Il nome è obbligatorio',
+        'vat.unique' => 'Un fornitore con questa Partita IVA esiste già',
+        'fiscal_code.unique' => 'Un fornitore con questo Codice Fiscale esiste già'
+    ];
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+    }
+
+
+    public function resetFields()
+    {
+        $this->name = '';
+        $this->fiscal_code = '';
+        $this->vat = '';
+        $this->address = '';
+        $this->zip_code = '';
+        $this->nation_id = null;
+        $this->province_id = null;
+        $this->city_id = null;
+        $this->referent = '';
+        $this->website = '';
+        $this->phone = '';
+        $this->email = '';
+        $this->enabled = true;
+        $this->referent_first_name = '';
+        $this->referent_last_name = '';
+        $this->referent_email = '';
+        $this->referent_phone = '';
+        $this->referent_mobile = '';
+        $this->emit('load-data-table');
+    }
+
+    public $sortField = 'name';
+    public $sortAsc = true;
+
+    public function sortBy($field)
+    {
+        if ($this->sortField === $field) {
+            $this->sortAsc = ! $this->sortAsc;
+        } else {
+            $this->sortAsc = true;
+        }
+
+        $this->sortField = $field;
+    }
+
+    public function mount()
+    {
+        if (Auth::user()->level != env('LEVEL_ADMIN', 0))
+            return redirect()->to('/dashboard');
+
+        if (isset($_GET["new"]))
+            $this->add();
+    }
+
+    public function search()
+    {
+        if ($this->searchTxt != '') {
+            $this->search = $this->searchTxt;
+            $this->showReset = true;
+        }
+    }
+
+    public function resetSearch()
+    {
+        $this->showReset = false;
+        $this->searchTxt = '';
+        $this->search = $this->searchTxt;
+    }
+
+    public function render()
+    {
+        if ($this->showArchived) {
+            // Show all suppliers including archived ones
+            $this->records = \App\Models\Supplier::orderBy('name')->get();
+        } else {
+            // Show only non-archived suppliers
+            $this->records = \App\Models\Supplier::where(function($query) {
+                $query->where('archived', 0)->orWhereNull('archived');
+            })->orderBy('name')->get();
+        }
+
+        return view('livewire.supplier');
+    }
+    public function hydrate()
+    {
+        $this->emit('load-select');
+    }
+
+    public function checkIsItaly()
+    {
+        $n = \App\Models\Nation::findOrFail($this->nation_id);
+        $this->isItaly = $n->is_italy;
+    }
+
+    public function add()
+    {
+        $this->resetFields();
+        $this->emit('load-select');
+        $this->add = true;
+        $this->update = false;
+        $this->emit('setEdit', true);
+    }
+
+    private function cleanEmptyFields()
+    {
+        $this->vat = trim($this->vat) === '' ? null : trim($this->vat);
+        $this->fiscal_code = trim($this->fiscal_code) === '' ? null : trim($this->fiscal_code);
+    }
+
+    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,
+                'fiscal_code' => !empty($fiscalCodeToCheck) ? $fiscalCodeToCheck : null,
+                'vat' => !empty($vatToCheck) ? $vatToCheck : null,
+                'address' => $this->address,
+                'zip_code' => $this->zip_code,
+                'nation_id' => $this->nation_id,
+                'province_id' => $this->province_id > 0 ? $this->province_id : null,
+                'city_id' => $this->city_id > 0 ? $this->city_id : null,
+                'referent' => $this->referent,
+                'website' => $this->website,
+                'phone' => $this->phone,
+                'email' => $this->email,
+                'referent_first_name' => $this->referent_first_name,
+                'referent_last_name' => $this->referent_last_name,
+                'referent_email' => $this->referent_email,
+                'referent_phone' => $this->referent_phone,
+                'referent_mobile' => $this->referent_mobile,
+                'enabled' => $this->enabled
+            ]);
+            session()->flash('success', 'Fornitore creato');
+            $this->resetFields();
+            $this->add = false;
+            $this->emit('setEdit', false);
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function edit($id)
+    {
+        $this->emit('load-select');
+        try {
+            $supplier = \App\Models\Supplier::findOrFail($id);
+            if (!$supplier) {
+                session()->flash('error', 'Fornitore non trovato');
+            } else {
+                $this->name = $supplier->name;
+                $this->fiscal_code = $supplier->fiscal_code;
+                $this->vat = $supplier->vat;
+                $this->address = $supplier->address;
+                $this->zip_code = $supplier->zip_code;
+                $this->nation_id = $supplier->nation_id;
+                $this->province_id = $supplier->province_id;
+                $this->city_id = $supplier->city_id;
+                $this->referent = $supplier->referent;
+                $this->website = $supplier->website;
+                $this->phone = $supplier->phone;
+                $this->email = $supplier->email;
+                $this->referent_first_name = $supplier->referent_first_name;
+                $this->referent_last_name = $supplier->referent_last_name;
+                $this->referent_email = $supplier->referent_email;
+                $this->referent_phone = $supplier->referent_phone;
+                $this->referent_mobile = $supplier->referent_mobile;
+                $this->enabled = $supplier->enabled;
+                $this->dataId = $supplier->id;
+                $this->update = true;
+                $this->add = false;
+                $this->checkIsItaly();
+                $this->emit('load-provinces', $this->nation_id, 'provinceClass');
+                $this->emit('load-cities', $this->province_id, 'cityClass');
+
+                $this->emit('setEdit', true);
+            }
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function cancel()
+    {
+        $this->add = false;
+        $this->update = false;
+        $this->resetFields();
+        $this->emit('setEdit', false);
+    }
+
+    // Replace delete method with anonymize method
+    public function anonymize($id)
+    {
+        try {
+            $supplier = \App\Models\Supplier::findOrFail($id);
+
+            // Anonymize all fields
+            $supplier->update([
+                'name' => 'Fornitore Anonimizzato',
+                'fiscal_code' => null,
+                'vat' => null,
+                'address' => null,
+                'zip_code' => null,
+                'nation_id' => null,
+                'province_id' => null,
+                'city_id' => null,
+                'referent' => null,
+                'website' => null,
+                'phone' => null,
+                'email' => null,
+                'referent_first_name' => null,
+                'referent_last_name' => null,
+                'referent_email' => null,
+                'referent_phone' => null,
+                'referent_mobile' => null,
+                'archived' => true, // Mark as archived
+                'enabled' => false // Also disable the supplier
+            ]);
+
+            session()->flash('success', "Fornitore anonimizzato");
+            return redirect(request()->header('Referer'));
+        } catch (\Exception $e) {
+            session()->flash('error', 'Errore (' . $e->getMessage() . ')');
+        }
+    }
+
+    public function getNation($nation)
+    {
+        if ($nation > 0) {
+            $ret = \App\Models\Nation::findOrFail($nation);
+            return $ret->name;
+        }
+        return "";
+    }
+
+    public function getProvince($province)
+    {
+        if ($province > 0) {
+            $ret = \App\Models\Province::findOrFail($province);
+            return $ret->name;
+        }
+        return "";
+    }
+
+    public function getCity($city)
+    {
+        if ($city > 0) {
+            $ret = \App\Models\City::findOrFail($city);
+            return $ret->name;
+        }
+        return "";
+    }
+
+    protected function getRulesForUpdate()
+    {
+        return [
+            'name' => 'required',
+            'vat' => 'nullable|unique:suppliers,vat,' . $this->dataId,
+            'fiscal_code' => 'nullable|unique:suppliers,fiscal_code,' . $this->dataId
+        ];
+    }
+
+    public function update()
+    {
+        // Clean empty fields first
+        $this->cleanEmptyFields();
+
+        $this->validate($this->getRulesForUpdate());
+
+        try {
+            \App\Models\Supplier::whereId($this->dataId)->update([
+                'name' => $this->name,
+                'fiscal_code' => $this->fiscal_code,
+                'vat' => $this->vat,
+                'address' => $this->address,
+                'zip_code' => $this->zip_code,
+                'nation_id' => $this->nation_id,
+                'province_id' => $this->province_id > 0 ? $this->province_id : null,
+                'city_id' => $this->city_id > 0 ? $this->city_id : null,
+                'referent' => $this->referent,
+                'website' => $this->website,
+                'phone' => $this->phone,
+                'email' => $this->email,
+                'referent_first_name' => $this->referent_first_name,
+                'referent_last_name' => $this->referent_last_name,
+                'referent_email' => $this->referent_email,
+                'referent_phone' => $this->referent_phone,
+                'referent_mobile' => $this->referent_mobile,
+                'enabled' => $this->enabled
+            ]);
+            session()->flash('success', 'Fornitore aggiornato');
+            $this->resetFields();
+            $this->update = false;
+
+            $this->emit('setEdit', false);
+        } catch (\Exception $ex) {
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+}

+ 770 - 0
app/Http/Livewire/User.php

@@ -0,0 +1,770 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Mail;
+use Livewire\Component;
+use App\Http\Middleware\TenantMiddleware;
+use Illuminate\Support\Facades\Auth;
+
+class User extends Component
+{
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+        $this->logCurrentDatabase('After tenant connection setup in boot()');
+    }
+
+    public $records, $name, $cognome, $email, $password, $oldPassword, $level, $enabled, $dataId, $update = false, $add = false, $oldEmail = null;
+    public $userExists = false;
+
+    protected $rules = [
+        'name' => 'required',
+        'cognome' => 'required',
+        'email' => 'required',
+        'password' => 'required'
+    ];
+
+    protected $messages = [
+        'name.required' => 'Il nome è obbligatorio',
+        'cognome.required' => 'Il cognome è obbligatorio',
+        'email.required' => 'La mail è obbligatoria',
+        'password.required' => 'La password è obbligatoria',
+    ];
+
+    /**
+     * Helper method to log current database information
+     */
+    private function logCurrentDatabase($context = '')
+    {
+        try {
+            $currentConnection = DB::getDefaultConnection();
+            $currentDatabase = DB::connection()->getDatabaseName();
+            $user = Auth::user();
+
+            Log::info('Database Connection Info', [
+                'context' => $context,
+                'current_connection' => $currentConnection,
+                'current_database' => $currentDatabase,
+                'user_id' => $user ? $user->id : null,
+                'user_tenant_database' => $user ? $user->tenant_database : null,
+                'user_tenant_username' => $user ? $user->tenant_username : null,
+            ]);
+        } catch (\Exception $e) {
+            Log::error('Failed to get database info', [
+                'context' => $context,
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+
+    /**
+     * Create or update user in master database
+     */
+    private function syncUserToMasterDatabase($userData, $action = 'create', $oldEmail = null)
+    {
+        try {
+            Log::info('Syncing user to master database', [
+                'action' => $action,
+                'email' => $userData['email'],
+                'old_email' => $oldEmail
+            ]);
+
+            $masterConfig = [
+                'driver' => 'mysql',
+                'host' => env('DB_HOST', '127.0.0.1'),
+                'port' => env('DB_PORT', '3306'),
+                'database' => env('DB_DATABASE'),
+                'username' => env('DB_USERNAME'),
+                'password' => env('DB_PASSWORD'),
+                'charset' => 'utf8mb4',
+                'collation' => 'utf8mb4_unicode_ci',
+                'prefix' => '',
+                'strict' => true,
+                'engine' => null,
+            ];
+
+            config(['database.connections.master_sync' => $masterConfig]);
+
+            $currentUser = Auth::user();
+
+            $masterData = [
+                'name' => $userData['name'],
+                'email' => $userData['email'],
+                'password' => $userData['password'],
+                'tenant_database' => $currentUser->tenant_database,
+                'tenant_username' => $currentUser->tenant_username,
+                'tenant_password' => $currentUser->tenant_password,
+                'tenant_host' => '127.0.0.1',
+                'created_at' => now(),
+                'updated_at' => now()
+            ];
+
+            if ($action === 'create') {
+                $inserted = DB::connection('master_sync')
+                    ->table('users')
+                    ->insert($masterData);
+
+                if ($inserted) {
+                    Log::info('Successfully created user in master database', [
+                        'email' => $userData['email'],
+                        'tenant_database' => $currentUser->tenant_database
+                    ]);
+                    return true;
+                } else {
+                    Log::warning('Failed to create user in master database', [
+                        'email' => $userData['email']
+                    ]);
+                    return false;
+                }
+            } elseif ($action === 'update') {
+                $searchEmail = $oldEmail ?: $userData['email'];
+
+                unset($masterData['created_at']);
+
+                $updated = DB::connection('master_sync')
+                    ->table('users')
+                    ->where('email', $searchEmail)
+                    ->update($masterData);
+
+                if ($updated) {
+                    Log::info('Successfully updated user in master database', [
+                        'old_email' => $searchEmail,
+                        'new_email' => $userData['email']
+                    ]);
+                    return true;
+                } else {
+                    Log::warning('No user found in master database to update', [
+                        'search_email' => $searchEmail
+                    ]);
+                    return false;
+                }
+            }
+        } catch (\Exception $e) {
+            Log::error('Failed to sync user to master database', [
+                'action' => $action,
+                'email' => $userData['email'],
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+            return false;
+        } finally {
+            // Clean up the temporary connection
+            try {
+                DB::purge('master_sync');
+            } catch (\Exception $e) {
+                // Ignore cleanup errors
+            }
+        }
+    }
+
+    /**
+     * Delete user from master database
+     */
+    private function deleteUserFromMasterDatabase($email)
+    {
+        try {
+            Log::info('Deleting user from master database', [
+                'email' => $email
+            ]);
+
+            $masterConfig = [
+                'driver' => 'mysql',
+                'host' => env('DB_HOST', '127.0.0.1'),
+                'port' => env('DB_PORT', '3306'),
+                'database' => env('DB_DATABASE'),
+                'username' => env('DB_USERNAME'),
+                'password' => env('DB_PASSWORD'),
+                'charset' => 'utf8mb4',
+                'collation' => 'utf8mb4_unicode_ci',
+                'prefix' => '',
+                'strict' => true,
+                'engine' => null,
+            ];
+
+            config(['database.connections.master_delete' => $masterConfig]);
+
+            $deleted = DB::connection('master_delete')
+                ->table('users')
+                ->where('email', $email)
+                ->delete();
+
+            if ($deleted) {
+                Log::info('Successfully deleted user from master database', [
+                    'email' => $email,
+                    'rows_affected' => $deleted
+                ]);
+                return true;
+            } else {
+                Log::warning('No user found in master database to delete', [
+                    'email' => $email
+                ]);
+                return false;
+            }
+        } catch (\Exception $e) {
+            Log::error('Failed to delete user from master database', [
+                'email' => $email,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+            return false;
+        } finally {
+            try {
+                DB::purge('master_delete');
+            } catch (\Exception $e) {
+            }
+        }
+    }
+
+    /**
+     * Send welcome email to new user
+     */
+    private function sendWelcomeEmail($userData, $plainPassword)
+    {
+        try {
+            $currentUser = Auth::user();
+            $companyName = 'Leezard';
+
+            Log::info('Preparing to send welcome email', [
+                'recipient' => $userData['email'],
+                'company' => $companyName,
+                'mail_from' => config('mail.from.address'),
+                'mail_host' => config('mail.mailers.smtp.host'),
+                'mail_port' => config('mail.mailers.smtp.port')
+            ]);
+
+            $emailData = [
+                'name' => $userData['name'],
+                'cognome' => $userData['cognome'],
+                'email' => $userData['email'],
+                'password' => $plainPassword,
+                'level' => $userData['level'],
+                'company' => $companyName,
+                'login_url' => url('/'),
+                'created_by' => $currentUser->name
+            ];
+
+            try {
+                $viewContent = view('emails.welcome-user', $emailData)->render();
+                Log::info('Email template rendered successfully', ['template_length' => strlen($viewContent)]);
+            } catch (\Exception $viewException) {
+                Log::error('Email template rendering failed', ['error' => $viewException->getMessage()]);
+                throw new \Exception('Email template error: ' . $viewException->getMessage());
+            }
+
+            Mail::send('emails.welcome-user', $emailData, function ($message) use ($userData, $companyName) {
+                $message->to($userData['email'], $userData['name'] . ' ' . $userData['cognome'])
+                    ->subject('Benvenuto su Leezard - Account Creato')
+                    ->from(config('mail.from.address'), config('mail.from.name'));
+
+                if (env('MAIL_CCN')) {
+                    $message->bcc(env('MAIL_CCN'));
+                }
+            });
+
+            Log::info('Welcome email sent successfully', [
+                'recipient' => $userData['email'],
+                'company' => $companyName,
+                'subject' => 'Benvenuto in ' . $companyName . ' - Account Creato'
+            ]);
+
+            return true;
+        } catch (\Exception $e) {
+            Log::error('SMTP Transport error when sending welcome email', [
+                'recipient' => $userData['email'],
+                'error' => $e->getMessage(),
+                'mail_config' => [
+                    'host' => config('mail.mailers.smtp.host'),
+                    'port' => config('mail.mailers.smtp.port'),
+                    'encryption' => config('mail.mailers.smtp.encryption'),
+                    'username' => config('mail.mailers.smtp.username')
+                ]
+            ]);
+            return false;
+        } catch (\Exception $e) {
+            Log::error('General error when sending welcome email', [
+                'recipient' => $userData['email'],
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+            return false;
+        }
+    }
+    public function sendSimpleWelcomeEmail($email, $userData)
+    {
+        try {
+            $subject = "Benvenuto su " . $userData['company'] . " – Account Creato";
+
+            $message = "Ciao " . $userData['name'] . " " . $userData['cognome'] . ",\n\n";
+            $message .= "Il tuo account è stato creato con successo su " . $userData['company'] . ".\n\n";
+            $message .= "Le tue credenziali di accesso:\n";
+            $message .= "Email: " . $userData['email'] . "\n";
+            $message .= "Password: " . $userData['password'] . "\n\n";
+            $message .= "Per motivi di sicurezza, dovrai cambiare la password al primo accesso.\n\n";
+            $message .= "Accedi ora: " . $userData['login_url'] . "\n\n";
+            $message .= "Grazie e benvenuto!\n";
+            $message .= "Il team di " . $userData['company'];
+
+            // Send simple text email
+            mail($email, $subject, $message, [
+                'From' => config('mail.from.address'),
+                'Reply-To' => config('mail.from.address'),
+                'Content-Type' => 'text/plain; charset=UTF-8'
+            ]);
+
+            return true;
+        } catch (\Exception $e) {
+            Log::error('Simple email sending failed', [
+                'email' => $email,
+                'error' => $e->getMessage()
+            ]);
+            return false;
+        }
+    }
+    public function resetFields()
+    {
+        $this->name = '';
+        $this->cognome = '';
+        $this->email = '';
+        $this->password = '';
+        $this->oldPassword = '';
+        $this->level = 0;
+        $this->enabled = true;
+        $this->emit('load-data-table');
+    }
+
+    public function render()
+    {
+        $this->logCurrentDatabase('Before fetching users in render()');
+
+        $this->records = \App\Models\User::select('id', 'name', 'cognome', 'email', 'password', 'level', 'enabled')->get();
+
+        $this->logCurrentDatabase('After fetching users in render()');
+
+        return view('livewire.user');
+    }
+
+    public function add()
+    {
+        if (!$this->canAddUser()) {
+            session()->flash('error', 'Non hai i permessi per aggiungere utenti.');
+            return;
+        }
+
+        $this->logCurrentDatabase('In add() method');
+
+        $this->resetFields();
+        $this->add = true;
+        $this->update = false;
+        $this->enabled = true;
+        $this->userExists = false;
+    }
+
+    public function store()
+    {
+        if (!$this->canAddUser()) {
+            session()->flash('error', 'Non hai i permessi per aggiungere utenti.');
+            return;
+        }
+
+        $this->logCurrentDatabase('Start of store() method');
+
+        Log::info('User store', [
+            'name' => $this->name,
+            'cognome' => $this->cognome,
+            'email' => $this->email,
+            'level' => $this->level,
+            'enabled' => $this->enabled
+        ]);
+
+        $rules = [
+            'name' => 'required',
+            'cognome' => 'required',
+            'email' => 'required|email|unique:users,email',
+            'password' => 'required|min:6'
+        ];
+
+        $messages = [
+            'name.required' => 'Il nome è obbligatorio',
+            'cognome.required' => 'Il cognome è obbligatorio',
+            'email.required' => 'La mail è obbligatoria',
+            'email.email' => 'La mail deve essere un indirizzo valido',
+            'email.unique' => 'Questa mail è già stata utilizzata',
+            'password.required' => 'La password è obbligatoria',
+            'password.min' => 'La password deve essere di almeno 6 caratteri'
+        ];
+
+        $this->validate($rules, $messages);
+
+        $this->logCurrentDatabase('Before creating user in store()');
+
+        try {
+            $plainPassword = $this->password;
+
+            $hashedPassword = bcrypt($this->password);
+
+            $user = \App\Models\User::create([
+                'name' => $this->name,
+                'cognome' => $this->cognome,
+                'email' => $this->email,
+                'password' => $hashedPassword,
+                'level' => $this->level,
+                'enabled' => $this->enabled
+            ]);
+
+            $this->logCurrentDatabase('After creating user in tenant database');
+
+            Log::info('User created successfully in tenant database', [
+                'user_id' => $user->id,
+                'name' => $this->name,
+                'cognome' => $this->cognome,
+                'email' => $this->email,
+                'level' => $this->level,
+                'enabled' => $this->enabled,
+                'database' => DB::connection()->getDatabaseName()
+            ]);
+
+            $masterSyncSuccess = $this->syncUserToMasterDatabase([
+                'name' => $this->name,
+                'cognome' => $this->cognome,
+                'email' => $this->email,
+                'password' => $hashedPassword,
+                'level' => $this->level,
+                'enabled' => $this->enabled
+            ], 'create');
+
+            if ($masterSyncSuccess) {
+                $emailSent = $this->sendWelcomeEmail([
+                    'name' => $this->name,
+                    'cognome' => $this->cognome,
+                    'email' => $this->email,
+                    'level' => $this->level
+                ], $plainPassword);
+
+                if (!$emailSent) {
+                    Log::info('HTML email failed, trying simple email', ['email' => $this->email]);
+                    $emailSent = $this->sendSimpleWelcomeEmail($this->email, [
+                        'name' => $this->name,
+                        'cognome' => $this->cognome,
+                        'email' => $this->email,
+                        'password' => $plainPassword,
+                        'level' => $this->level,
+                        'company' => 'Leezard',
+                        'login_url' => url('/')
+                    ]);
+                }
+
+                if ($emailSent) {
+                    session()->flash('success', 'Utente creato e email di benvenuto inviata con successo');
+                } else {
+                    session()->flash('success', 'Utente creato ma errore nell\'invio dell\'email. Controlla i log per dettagli.');
+                }
+            } else {
+                session()->flash('success', 'Utente creato nel database tenant ma errore nella sincronizzazione master');
+            }
+
+            $this->resetFields();
+            $this->add = false;
+        } catch (\Exception $ex) {
+            $this->logCurrentDatabase('Error in store() method');
+
+            Log::error('User creation failed', [
+                'error' => $ex->getMessage(),
+                'database' => DB::connection()->getDatabaseName(),
+                'user_data' => [
+                    'name' => $this->name,
+                    'cognome' => $this->cognome,
+                    'email' => $this->email,
+                    'level' => $this->level,
+                    'enabled' => $this->enabled
+                ]
+            ]);
+
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function edit($id)
+    {
+        if (!$this->canEditUser($id)) {
+            session()->flash('error', 'Non hai i permessi per modificare questo utente.');
+            return;
+        }
+
+        $this->logCurrentDatabase('Start of edit() method');
+
+        try {
+            $user = \App\Models\User::findOrFail($id);
+
+            $this->logCurrentDatabase('After finding user in edit()');
+
+            if (!$user) {
+                session()->flash('error', 'Utente non trovato');
+            } else {
+                $this->name = $user->name;
+                $this->cognome = $user->cognome;
+                $this->email = $user->email;
+                $this->level = $user->level;
+                $this->dataId = $user->id;
+                $this->update = true;
+                $this->add = false;
+                $this->enabled = $user->enabled;
+                $this->userExists = true;
+                // Store old email for master database update
+                $this->oldEmail = $user->email;
+            }
+
+            Log::info('User edit loaded', [
+                'user_id' => $id,
+                'name' => $this->name,
+                'cognome' => $this->cognome,
+                'email' => $this->email,
+                'level' => $this->level,
+                'database' => DB::connection()->getDatabaseName()
+            ]);
+        } catch (\Exception $ex) {
+            $this->logCurrentDatabase('Error in edit() method');
+
+            Log::error('User edit failed', [
+                'user_id' => $id,
+                'error' => $ex->getMessage(),
+                'database' => DB::connection()->getDatabaseName()
+            ]);
+
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+
+    public function update()
+    {
+        $this->logCurrentDatabase('Start of update() method');
+
+        $rules = [
+            'name' => 'required',
+            'cognome' => 'required',
+            'email' => 'required|email',
+            'password' => 'nullable|min:6'
+        ];
+
+        $this->validate($rules, $this->messages);
+
+        try {
+            $currentUser = \App\Models\User::findOrFail($this->dataId);
+            $oldEmail = $currentUser->email;
+            $oldName = $currentUser->name;
+
+            $updateData = [
+                'name' => $this->name,
+                'cognome' => $this->cognome,
+                'email' => $this->email,
+                'level' => $this->level,
+                'enabled' => $this->enabled
+            ];
+
+            $passwordChanged = !empty($this->password);
+            if ($passwordChanged) {
+                $hashedPassword = bcrypt($this->password);
+                $updateData['password'] = $hashedPassword;
+            }
+
+            \App\Models\User::whereId($this->dataId)->update($updateData);
+
+            $this->logCurrentDatabase('After updating user');
+
+            Log::info('User updated successfully in tenant database', [
+                'user_id' => $this->dataId,
+                'name' => $this->name,
+                'cognome' => $this->cognome,
+                'email' => $this->email,
+                'level' => $this->level,
+                'enabled' => $this->enabled,
+                'password_changed' => $passwordChanged,
+                'database' => DB::connection()->getDatabaseName()
+            ]);
+
+            $emailChanged = $oldEmail !== $this->email;
+            $nameChanged = $oldName !== $this->name;
+
+            if ($emailChanged || $nameChanged || $passwordChanged) {
+                $masterData = [
+                    'name' => $this->name,
+                    'email' => $this->email
+                ];
+
+                if ($passwordChanged) {
+                    $masterData['password'] = $hashedPassword;
+                }
+
+                $this->syncUserToMasterDatabase($masterData, 'update', $oldEmail);
+            }
+
+            session()->flash('success', 'Dato aggiornato');
+            $this->resetFields();
+            $this->update = false;
+        } catch (\Exception $ex) {
+            $this->logCurrentDatabase('Error in update() method');
+
+            Log::error('User update failed', [
+                'user_id' => $this->dataId,
+                'error' => $ex->getMessage(),
+                'database' => DB::connection()->getDatabaseName()
+            ]);
+
+            session()->flash('error', 'Errore (' . $ex->getMessage() . ')');
+        }
+    }
+
+    public function cancel()
+    {
+        $this->logCurrentDatabase('In cancel() method');
+
+        $this->resetFields();
+        $this->add = false;
+        $this->update = false;
+        $this->userExists = false;
+        $this->enabled = false;
+    }
+
+    public function delete($id)
+    {
+        Log::info('Delete method called', [
+            'user_id_to_delete' => $id,
+            'current_user_id' => Auth::id(),
+            'current_user_level' => Auth::user()->level
+        ]);
+
+        if (!$this->canDeleteUser($id)) {
+            Log::warning('Delete permission denied', [
+                'user_id_to_delete' => $id,
+                'current_user_id' => Auth::id(),
+                'current_user_level' => Auth::user()->level
+            ]);
+            session()->flash('error', 'Non hai i permessi per eliminare questo utente.');
+            return;
+        }
+
+        $this->logCurrentDatabase('Start of delete() method');
+
+        try {
+            $user = \App\Models\User::find($id);
+
+            if (!$user) {
+                Log::error('User not found for deletion', ['user_id' => $id]);
+                session()->flash('error', 'Utente non trovato.');
+                return;
+            }
+
+            $userEmail = $user->email;
+            $userName = $user->name;
+            $userCognome = $user->cognome;
+
+            Log::info('Found user for deletion', [
+                'user_id' => $id,
+                'user_email' => $userEmail,
+                'user_name' => $userName . ' ' . $userCognome
+            ]);
+
+            $deleted = $user->delete();
+
+            if (!$deleted) {
+                throw new \Exception('Failed to delete user from tenant database');
+            }
+
+            $this->logCurrentDatabase('After deleting user from tenant');
+
+            Log::info('User deleted successfully from tenant database', [
+                'user_id' => $id,
+                'user_email' => $userEmail,
+                'database' => DB::connection()->getDatabaseName()
+            ]);
+
+            $masterDeleted = $this->deleteUserFromMasterDatabase($userEmail);
+
+            if ($masterDeleted) {
+                Log::info('User deleted from both databases successfully', [
+                    'user_id' => $id,
+                    'user_email' => $userEmail
+                ]);
+                session()->flash('success', "Utente {$userName} {$userCognome} eliminato con successo");
+            } else {
+                Log::warning('User deleted from tenant but failed to delete from master', [
+                    'user_id' => $id,
+                    'user_email' => $userEmail
+                ]);
+                session()->flash('success', "Utente eliminato dal database tenant, ma errore nella sincronizzazione master");
+            }
+
+            $this->emit('userDeleted');
+            $this->emit('load-data-table');
+        } catch (\Exception $e) {
+            $this->logCurrentDatabase('Error in delete() method');
+
+            Log::error('User deletion failed', [
+                'user_id' => $id,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString(),
+                'database' => DB::connection()->getDatabaseName()
+            ]);
+
+            session()->flash('error', 'Errore durante l\'eliminazione: ' . $e->getMessage());
+        }
+    }
+
+
+    private function canEditUser($userId)
+    {
+        $currentUser = Auth::user();
+
+        if ($currentUser->level == 0) {
+            $targetUser = \App\Models\User::find($userId);
+            return $targetUser && $targetUser->email != 'admin@admin.com';
+        }
+
+        return $userId == $currentUser->id;
+    }
+
+    /**
+     * Check if current user can edit email and password of a specific user
+     */
+    private function canEditEmailAndPassword($userId)
+    {
+        $currentUser = Auth::user();
+
+        return $userId == $currentUser->id;
+    }
+
+    /**
+     * Check if current user can delete a specific user
+     */
+    private function canDeleteUser($userId)
+    {
+        $currentUser = Auth::user();
+
+        if ($currentUser->level != 0) {
+            return false;
+        }
+
+        if ($userId == $currentUser->id) {
+            return false;
+        }
+
+        $targetUser = \App\Models\User::find($userId);
+        if ($targetUser && $targetUser->email == 'admin@admin.com') {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Check if current user can add users
+     */
+    private function canAddUser()
+    {
+        $currentUser = Auth::user();
+        return $currentUser->level == 0;
+    }
+}

+ 136 - 0
app/Http/Livewire/Vat.php

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

+ 21 - 0
app/Http/Middleware/Authenticate.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Illuminate\Auth\Middleware\Authenticate as Middleware;
+
+class Authenticate extends Middleware
+{
+    /**
+     * Get the path the user should be redirected to when they are not authenticated.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return string|null
+     */
+    protected function redirectTo($request)
+    {
+        if (! $request->expectsJson()) {
+            return route('login');
+        }
+    }
+}

+ 28 - 0
app/Http/Middleware/CheckFirstLoginCompleted.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Closure;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+
+class CheckFirstLoginCompleted
+{
+    public function handle(Request $request, Closure $next)
+    {
+        $user = Auth::user();
+
+        if (!$user) {
+            return $next($request);
+        }
+
+        if (!$user->first_login_completed) {
+            if (!$request->routeIs(['first-login', 'logout', 'login'])) {
+                return redirect('/first-login')
+                    ->with('warning', 'Devi completare il tuo profilo prima di accedere alla piattaforma.');
+            }
+        }
+
+        return $next($request);
+    }
+}

+ 17 - 0
app/Http/Middleware/EncryptCookies.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
+
+class EncryptCookies extends Middleware
+{
+    /**
+     * The names of the cookies that should not be encrypted.
+     *
+     * @var array<int, string>
+     */
+    protected $except = [
+        //
+    ];
+}

+ 25 - 0
app/Http/Middleware/PasswordResetThrottle.php

@@ -0,0 +1,25 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Closure;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\RateLimiter;
+use Symfony\Component\HttpFoundation\Response;
+
+class PasswordResetThrottle
+{
+    public function handle(Request $request, Closure $next): Response
+    {
+        $key = 'password-reset:' . $request->ip();
+
+        if (RateLimiter::tooManyAttempts($key, 5)) {
+            $seconds = RateLimiter::availableIn($key);
+            return back()->with('error', "Troppi tentativi. Riprova tra {$seconds} secondi.");
+        }
+
+        RateLimiter::hit($key, 3600); // 1 hour
+
+        return $next($request);
+    }
+}

+ 17 - 0
app/Http/Middleware/PreventRequestsDuringMaintenance.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
+
+class PreventRequestsDuringMaintenance extends Middleware
+{
+    /**
+     * The URIs that should be reachable while maintenance mode is enabled.
+     *
+     * @var array<int, string>
+     */
+    protected $except = [
+        //
+    ];
+}

+ 32 - 0
app/Http/Middleware/RedirectIfAuthenticated.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use App\Providers\RouteServiceProvider;
+use Closure;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+
+class RedirectIfAuthenticated
+{
+    /**
+     * Handle an incoming request.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse)  $next
+     * @param  string|null  ...$guards
+     * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
+     */
+    public function handle(Request $request, Closure $next, ...$guards)
+    {
+        $guards = empty($guards) ? [null] : $guards;
+
+        foreach ($guards as $guard) {
+            if (Auth::guard($guard)->check()) {
+                return redirect(RouteServiceProvider::HOME);
+            }
+        }
+
+        return $next($request);
+    }
+}

+ 60 - 0
app/Http/Middleware/TenantMiddleware.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Closure;
+use Illuminate\Http\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+
+class TenantMiddleware
+{
+    /**
+     * Handle an incoming request.
+     */
+    public function handle(Request $request, Closure $next): Response
+    {
+        if (! $request->user()) {
+            return redirect('/');
+        }
+
+        $this->setupTenantConnection($request->user());
+
+        return $next($request);
+    }
+
+    /**
+     * Set up the tenant database connection
+     */
+    public function setupTenantConnection($user = null)
+    {
+        $user = $user ?: auth()->user();
+
+        if ($user) {
+            Log::info('Setting database connection', [
+                'database' => $user->tenant_database,
+                'username' => $user->tenant_username,
+                'password' => $user->tenant_password
+            ]);
+
+            config(['database.connections.tenant' => [
+                'driver' => 'mysql',
+                'host' => '127.0.0.1',
+                'port' => '3306',
+                'database' => $user->tenant_database,
+                'username' => $user->tenant_username,
+                'password' => $user->tenant_password,
+            ]]);
+
+            config(['database.default' => 'tenant']);
+            DB::purge('tenant');
+            DB::reconnect('tenant');
+
+            session(['currentClient' => $user->tenant_database]);
+
+            Log::info('Current database after setup: ' . DB::connection()->getDatabaseName());
+            Log::info('Current default connection: ' . DB::getDefaultConnection());
+        }
+    }
+}

+ 19 - 0
app/Http/Middleware/TrimStrings.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
+
+class TrimStrings extends Middleware
+{
+    /**
+     * The names of the attributes that should not be trimmed.
+     *
+     * @var array<int, string>
+     */
+    protected $except = [
+        'current_password',
+        'password',
+        'password_confirmation',
+    ];
+}

+ 20 - 0
app/Http/Middleware/TrustHosts.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Illuminate\Http\Middleware\TrustHosts as Middleware;
+
+class TrustHosts extends Middleware
+{
+    /**
+     * Get the host patterns that should be trusted.
+     *
+     * @return array<int, string|null>
+     */
+    public function hosts()
+    {
+        return [
+            $this->allSubdomainsOfApplicationUrl(),
+        ];
+    }
+}

+ 28 - 0
app/Http/Middleware/TrustProxies.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Illuminate\Http\Middleware\TrustProxies as Middleware;
+use Illuminate\Http\Request;
+
+class TrustProxies extends Middleware
+{
+    /**
+     * The trusted proxies for this application.
+     *
+     * @var array<int, string>|string|null
+     */
+    protected $proxies ='*';
+
+    /**
+     * The headers that should be used to detect proxies.
+     *
+     * @var int
+     */
+    protected $headers =
+        Request::HEADER_X_FORWARDED_FOR |
+        Request::HEADER_X_FORWARDED_HOST |
+        Request::HEADER_X_FORWARDED_PORT |
+        Request::HEADER_X_FORWARDED_PROTO |
+        Request::HEADER_X_FORWARDED_AWS_ELB;
+}

+ 22 - 0
app/Http/Middleware/ValidateSignature.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Illuminate\Routing\Middleware\ValidateSignature as Middleware;
+
+class ValidateSignature extends Middleware
+{
+    /**
+     * The names of the query string parameters that should be ignored.
+     *
+     * @var array<int, string>
+     */
+    protected $except = [
+        // 'fbclid',
+        // 'utm_campaign',
+        // 'utm_content',
+        // 'utm_medium',
+        // 'utm_source',
+        // 'utm_term',
+    ];
+}

+ 17 - 0
app/Http/Middleware/VerifyCsrfToken.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
+
+class VerifyCsrfToken extends Middleware
+{
+    /**
+     * The URIs that should be excluded from CSRF verification.
+     *
+     * @var array<int, string>
+     */
+    protected $except = [
+        //
+    ];
+}

+ 880 - 0
app/Jobs/ExportPrimaNota.php

@@ -0,0 +1,880 @@
+<?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', "Tipologia");
+        $activeWorksheet->setCellValue('C1', "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);
+    }
+
+    private function applyStylesEfficiently($activeWorksheet, $letters)
+    {
+        $maxCol = min(count($letters) - 1, count($this->payments) * 2 + 4);
+        $lastCol = $letters[$maxCol];
+        $totalRows = count($this->exportData) + 4;
+
+        $headerRange = 'A1:' . $lastCol . '2';
+        $activeWorksheet->getStyle($headerRange)->getFont()->setBold(true);
+
+        $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');
+
+        $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');
+    }
+
+    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);
+        }
+
+        for ($i = 5; $i < count($letters) && $i < 50; $i++) {
+            $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 {
+            $lastCheck = $startTime;
+            $result = null;
+
+            register_tick_function(function () use ($startTime, $maxExecutionTime, $description, &$lastCheck) {
+                $currentTime = microtime(true);
+                if ($currentTime - $lastCheck >= 5) {
+                    $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;
+        }
+
+        $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;
+    }
+
+    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()
+        ]);
+    }
+}

+ 255 - 0
app/Jobs/ProcessRecordAttachment.php

@@ -0,0 +1,255 @@
+<?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\DB;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Str;
+use App\Services\RecordFileService;
+use App\Http\Middleware\TenantMiddleware;
+
+class ProcessRecordAttachment implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    protected $recordId;
+    protected $tempFilePath;
+    protected $originalFileName;
+    protected $type;
+    protected $clientName;
+
+    public $timeout = 300;
+    public $tries = 3;
+    public $backoff = [10, 30, 60];
+
+    public function boot()
+    {
+        app(TenantMiddleware::class)->setupTenantConnection();
+
+    }
+    public function __construct($recordId, $tempFilePath, $originalFileName, $type = 'OUT', $clientName = null)
+    {
+        $this->recordId = $recordId;
+        $this->tempFilePath = $tempFilePath;
+        $this->originalFileName = $originalFileName;
+        $this->type = strtolower($type);
+        $this->clientName = $clientName ?: session('clientName', 'default');
+
+        $this->clientName = Str::slug($this->clientName, '_');
+    }
+
+    public function handle(RecordFileService $recordFileService)
+    {
+        try {
+            Log::info("=== PROCESSING ATTACHMENT JOB START ===");
+            Log::info("Client: {$this->clientName}");
+            Log::info("Record ID: {$this->recordId}");
+            Log::info("Temp file: {$this->tempFilePath}");
+            Log::info("Original name: {$this->originalFileName}");
+            Log::info("Type: {$this->type}");
+
+            DB::table('records')
+                ->where('id', $this->recordId)
+                ->update([
+                    'attachment_status' => 'processing',
+                    'updated_at' => now()
+                ]);
+
+            if (!Storage::disk('s3')->exists($this->tempFilePath)) {
+                Log::error("Temp file not found on S3: {$this->tempFilePath}");
+
+                try {
+                    $tempFiles = Storage::disk('s3')->files("{$this->clientName}/temp/uploads");
+                    Log::info("Available temp files for client '{$this->clientName}' on S3: " . json_encode($tempFiles));
+                } catch (\Exception $e) {
+                    Log::error("Could not list temp files for client '{$this->clientName}': " . $e->getMessage());
+                }
+
+                throw new \Exception("Temp file not found on S3: {$this->tempFilePath}");
+            }
+
+            $tempFileSize = Storage::disk('s3')->size($this->tempFilePath);
+            Log::info("Temp file size: {$tempFileSize} bytes");
+
+            $extension = pathinfo($this->originalFileName, PATHINFO_EXTENSION);
+            $fileName = time() . '_' . Str::random(10) . '.' . $extension;
+            $finalPath = "{$this->clientName}/records/{$this->type}/{$this->recordId}/attachments/{$fileName}";
+
+            Log::info("Final path: {$finalPath}");
+
+            $copySuccess = $this->copyFileOnS3($this->tempFilePath, $finalPath);
+
+            if (!$copySuccess) {
+                throw new \Exception("Failed to copy file from {$this->tempFilePath} to {$finalPath}");
+            }
+
+            if (!Storage::disk('s3')->exists($finalPath)) {
+                throw new \Exception("Final file not found after copy: {$finalPath}");
+            }
+
+            $finalFileSize = Storage::disk('s3')->size($finalPath);
+            Log::info("Final file size: {$finalFileSize} bytes");
+
+            if ($finalFileSize !== $tempFileSize) {
+                Log::warning("File size mismatch! Temp: {$tempFileSize}, Final: {$finalFileSize}");
+            } else {
+                Log::info("File sizes match - copy successful");
+            }
+
+            DB::table('records')
+                ->where('id', $this->recordId)
+                ->update([
+                    'attachment' => $finalPath,
+                    'attachment_status' => 'completed',
+                    'updated_at' => now()
+                ]);
+
+            $this->cleanupTempFile($this->tempFilePath);
+
+            Log::info("Attachment processing completed successfully for record {$this->recordId}: {$finalPath}");
+            Log::info("=== PROCESSING ATTACHMENT JOB END ===");
+
+        } catch (\Exception $e) {
+            Log::error("Failed to process attachment for record {$this->recordId}: " . $e->getMessage());
+            Log::error("Stack trace: " . $e->getTraceAsString());
+
+            DB::table('records')
+                ->where('id', $this->recordId)
+                ->update([
+                    'attachment_status' => 'failed',
+                    'updated_at' => now()
+                ]);
+
+            $this->cleanupTempFile($this->tempFilePath);
+
+            throw $e;
+        }
+    }
+
+    /**
+     * Enhanced S3 copy with multiple fallback approaches
+     */
+    private function copyFileOnS3($sourcePath, $destinationPath)
+    {
+        Log::info("Attempting S3 copy from {$sourcePath} to {$destinationPath}");
+
+        try {
+            Log::info("Trying Method 1: Standard S3 copy");
+            $copyResult = Storage::disk('s3')->copy($sourcePath, $destinationPath);
+
+            if ($copyResult && Storage::disk('s3')->exists($destinationPath)) {
+                Log::info("Method 1 successful: Standard S3 copy");
+                return true;
+            } else {
+                Log::warning("Method 1 failed: Standard S3 copy returned " . ($copyResult ? 'true' : 'false'));
+            }
+        } catch (\Exception $e) {
+            Log::warning("Method 1 exception: " . $e->getMessage());
+        }
+
+        try {
+            Log::info("Trying Method 2: Read and write");
+
+            $fileContent = Storage::disk('s3')->get($sourcePath);
+            if (!$fileContent) {
+                throw new \Exception("Could not read source file content");
+            }
+
+            $writeResult = Storage::disk('s3')->put($destinationPath, $fileContent);
+
+            if ($writeResult && Storage::disk('s3')->exists($destinationPath)) {
+                Log::info("Method 2 successful: Read and write");
+                return true;
+            } else {
+                Log::warning("Method 2 failed: Write returned " . ($writeResult ? 'true' : 'false'));
+            }
+        } catch (\Exception $e) {
+            Log::warning("Method 2 exception: " . $e->getMessage());
+        }
+
+        try {
+            Log::info("Trying Method 3: Stream copy");
+
+            $sourceStream = Storage::disk('s3')->readStream($sourcePath);
+            if (!$sourceStream) {
+                throw new \Exception("Could not open source stream");
+            }
+
+            $writeResult = Storage::disk('s3')->writeStream($destinationPath, $sourceStream);
+
+            if (is_resource($sourceStream)) {
+                fclose($sourceStream);
+            }
+
+            if ($writeResult && Storage::disk('s3')->exists($destinationPath)) {
+                Log::info(" Method 3 successful: Stream copy");
+                return true;
+            } else {
+                Log::warning("Method 3 failed: Stream write returned " . ($writeResult ? 'true' : 'false'));
+            }
+        } catch (\Exception $e) {
+            Log::warning("Method 3 exception: " . $e->getMessage());
+        }
+
+        Log::error("All S3 copy methods failed");
+        return false;
+    }
+
+    /**
+     * Clean up temp file with error handling
+     */
+    private function cleanupTempFile($tempPath)
+    {
+        try {
+            if (Storage::disk('s3')->exists($tempPath)) {
+                $deleted = Storage::disk('s3')->delete($tempPath);
+                if ($deleted) {
+                    Log::info("Temp file deleted: {$tempPath}");
+                } else {
+                    Log::warning("Failed to delete temp file: {$tempPath}");
+                }
+            } else {
+                Log::info("Temp file already gone: {$tempPath}");
+            }
+        } catch (\Exception $e) {
+            Log::error("Error deleting temp file {$tempPath}: " . $e->getMessage());
+        }
+    }
+
+    public function failed(\Exception $exception)
+    {
+        Log::error("=== JOB PERMANENTLY FAILED ===");
+        Log::error("Client: {$this->clientName}");
+        Log::error("Record ID: {$this->recordId}");
+        Log::error("Exception: " . $exception->getMessage());
+
+        DB::table('records')
+            ->where('id', $this->recordId)
+            ->update([
+                'attachment_status' => 'failed',
+                'updated_at' => now()
+            ]);
+
+        $this->cleanupTempFile($this->tempFilePath);
+    }
+
+    /**
+     * Get job tags for monitoring
+     */
+    public function tags()
+    {
+        return [
+            'attachment',
+            'client:' . $this->clientName,
+            'record:' . $this->recordId,
+            'type:' . $this->type,
+            'file:' . basename($this->tempFilePath)
+        ];
+    }
+}

+ 74 - 0
app/Jobs/SendEmailMessage.php

@@ -0,0 +1,74 @@
+<?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;
+
+class SendEmailMessage implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    public function __construct(public int $messageId) {}
+
+    public function handle()
+    {
+        $msg = \App\Models\EmailMessage::with(['recipients', 'attachments'])->findOrFail($this->messageId);
+
+        $msg->update(['status' => 'processing']);
+        try {
+            $attachments = $msg->attachments
+                ->map(fn($a) => [
+                    'disk' => $a->disk,
+                    'path' => $a->path,
+                    'name' => $a->name ?? basename($a->path),
+                ])
+                ->values()->all();
+        } catch (\Throwable $e) {
+            $msg->update(['status' => 'failed', 'error_message' => $e->getMessage()]);
+        }
+
+
+        foreach ($msg->recipients as $r) {
+            if (in_array($r->status, ['sent', 'failed'])) continue;
+
+            try {
+
+                $mailable = new \App\Mail\GenericMail(
+                    $msg->subject,
+                    $msg->content_html,
+                    $attachments
+                );
+
+                Mail::to($r->email_address)->send($mailable);
+
+                $r->update(['status' => 'sent', 'sent_at' => now(), 'error_message' => null]);
+            } catch (\Throwable $e) {
+                $r->update(['status' => 'failed', 'error_message' => $e->getMessage()]);
+            }
+        }
+        $total = $msg->recipients()->count();
+        $sent = $msg->recipients()->where('status', 'sent')->count();
+        $failed = $msg->recipients()->where('status', 'failed')->count();
+
+        $newStatus = 'draft';
+        if ($total === 0) {
+            $newStatus = 'draft';
+        } elseif ($sent === $total) {
+            $newStatus = 'sent';
+        } elseif ($sent > 0 && $failed > 0) {
+            $newStatus = 'partial';
+        } else {
+            $newStatus = 'failed';
+        }
+
+        $msg->update([
+            'status' => $newStatus,
+            'sent_at' => $sent > 0 ? now() : $msg->sent_at,
+        ]);
+    }
+}

+ 32 - 0
app/Mail/CustomEmail.php

@@ -0,0 +1,32 @@
+<?php
+namespace App\Mail;
+
+use Illuminate\Bus\Queueable;
+use Illuminate\Mail\Mailable;
+use Illuminate\Queue\SerializesModels;
+
+class CustomEmail extends Mailable
+{
+    use Queueable, SerializesModels;
+
+    public $emailSubject;
+    public $emailContent;
+    public $recipientName;
+
+    public function __construct($subject, $content, $recipientName = null)
+    {
+        $this->emailSubject = $subject;
+        $this->emailContent = $content;
+        $this->recipientName = $recipientName;
+    }
+
+    public function build()
+    {
+        return $this->subject($this->emailSubject)
+                    ->view('emails.custom')
+                    ->with([
+                        'content' => $this->emailContent,
+                        'recipientName' => $this->recipientName
+                    ]);
+    }
+}

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

+ 58 - 0
app/Mail/GenericMail.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace App\Mail;
+
+use Illuminate\Bus\Queueable;
+use Illuminate\Mail\Mailable;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Collection;
+
+class GenericMail extends Mailable
+{
+    use Queueable, SerializesModels;
+
+    protected array $filesData = [];
+
+    public function __construct(string $subjectLine, string $html, iterable $attachments = [])
+    {
+        $this->subject = $subjectLine;
+        $this->html = $html;
+
+        $items = $attachments instanceof Collection ? $attachments->all()
+            : (is_array($attachments) ? $attachments : []);
+
+        $this->filesData = array_values(array_map(function ($a) {
+            if (is_object($a)) {
+                $disk = $a->disk ?? null;
+                $path = $a->path ?? null;
+                $name = $a->name ?? null;
+            } else {
+                $disk = $a['disk'] ?? null;
+                $path = $a['path'] ?? null;
+                $name = $a['name'] ?? null;
+            }
+            return [
+                'disk' => $disk,
+                'path' => $path,
+                'name' => $name ?? ($path ? basename($path) : null),
+            ];
+        }, $items));
+    }
+
+    public function build()
+    {
+        $mail = $this->subject($this->subject)
+            ->html($this->html);
+
+        foreach ($this->filesData as $att) {
+            $disk = $att['disk'] ?? null;
+            $path = $att['path'] ?? null;
+            $name = $att['name'] ?? ($path ? basename($path) : null);
+            if ($disk && $path) {
+                $mail->attachFromStorageDisk($disk, $path, $name);
+            }
+        }
+
+        return $mail;
+    }
+}

+ 63 - 0
app/Mail/ReceipDeleteEmail.php

@@ -0,0 +1,63 @@
+<?php
+
+namespace App\Mail;
+
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Mail\Mailable;
+use Illuminate\Mail\Mailables\Content;
+use Illuminate\Mail\Mailables\Envelope;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Mail\Mailables\Attachment;
+
+class ReceipDeleteEmail extends Mailable
+{
+    use Queueable, SerializesModels;
+    public $mailData;
+    /**
+     * Create a new message instance.
+     *
+     * @return void
+     */
+    public function __construct($mailData)
+    {
+        $this->mailData = $mailData;
+    }
+
+    /**
+     * Get the message envelope.
+     *
+     * @return \Illuminate\Mail\Mailables\Envelope
+     */
+    public function envelope()
+    {
+        return new Envelope(
+            subject: env('RECEIPT_DELETE_MAIL_SUBJECT', 'Centro Sportivo La Madonnella - Annullamento ricevuta di pagamento ') . $this->mailData["number"],
+        );
+    }
+
+    /**
+     * Get the message content definition.
+     *
+     * @return \Illuminate\Mail\Mailables\Content
+     */
+    public function content()
+    {
+        return new Content(
+            view: 'emails.receipt_delete',
+        );
+    }
+
+    /**
+     * Get the attachments for the message.
+     *
+     * @return array
+     */
+    public function attachments()
+    {
+        return [
+            Attachment::fromStorage($this->mailData["pdf"]),
+            //$this->mailData["pdf"]
+        ];
+    }
+}

+ 63 - 0
app/Mail/ReceipEmail.php

@@ -0,0 +1,63 @@
+<?php
+
+namespace App\Mail;
+
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Mail\Mailable;
+use Illuminate\Mail\Mailables\Content;
+use Illuminate\Mail\Mailables\Envelope;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Mail\Mailables\Attachment;
+
+class ReceipEmail extends Mailable
+{
+    use Queueable, SerializesModels;
+    public $mailData;
+    /**
+     * Create a new message instance.
+     *
+     * @return void
+     */
+    public function __construct($mailData)
+    {
+        $this->mailData = $mailData;
+    }
+
+    /**
+     * Get the message envelope.
+     *
+     * @return \Illuminate\Mail\Mailables\Envelope
+     */
+    public function envelope()
+    {
+        return new Envelope(
+            subject: env('RECEIPT_MAIL_SUBJECT', 'Centro Sportivo La Madonnella - Ricevuta di pagamento ') . $this->mailData["number"],
+        );
+    }
+
+    /**
+     * Get the message content definition.
+     *
+     * @return \Illuminate\Mail\Mailables\Content
+     */
+    public function content()
+    {
+        return new Content(
+            view: 'emails.receipt',
+        );
+    }
+
+    /**
+     * Get the attachments for the message.
+     *
+     * @return array
+     */
+    public function attachments()
+    {
+        return [
+            Attachment::fromStorage($this->mailData["pdf"]),
+            //$this->mailData["pdf"]
+        ];
+    }
+}

+ 114 - 0
app/Models/Azienda.php

@@ -0,0 +1,114 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Facades\Validator;
+
+class Azienda extends Model
+{
+    use HasFactory;
+
+    protected $fillable = [
+        'ragione_sociale',
+        'nome_associazione',
+        'tipologia',
+        'logo',
+
+        'sede_legale_nazione',
+        'sede_legale_provincia',
+        'sede_legale_comune',
+        'sede_legale_indirizzo',
+        'sede_legale_cap',
+
+        'sede_operativa_nazione',
+        'sede_operativa_provincia',
+        'sede_operativa_comune',
+        'sede_operativa_indirizzo',
+        'sede_operativa_cap',
+
+        'same_address',
+        'email',
+        'pec',
+        'telefono',
+        'cellulare',
+
+        'partita_iva',
+        'codice_fiscale',
+        'codice_sdi',
+        'discipline',
+    ];
+
+    protected $casts = [
+        'chiusura_anno_fiscale' => 'date',
+        'scadenza_abbonamenti' => 'date',
+        'scadenza_pagamenti_uscita' => 'date',
+    ];
+
+    /**
+     * Valida i campi richiesti prima della generazione del PDF.
+     *
+     * @return array|bool  true se valido, oppure array di errori se non valido
+     */
+    public function validate()
+    {
+        $rules = [
+            'ragione_sociale' => 'required|string|max:255',
+            'email' => 'required|email|max:255',
+            'pec' => 'required|email|max:255',
+            'cellulare' => 'required|string|max:20',
+        ];
+
+        $rules_human = [
+            'ragione_sociale' => "Ragione sociale",
+            'email' => "Email",
+            'pec' => "Pec",
+            'cellulare' => "Cellulare",
+        ];
+
+        $validator = Validator::make($this->attributesToArray(), $rules);
+
+        if ($validator->fails()) {
+            $errors = [];
+            foreach ($validator->errors()->messages() as $field => $error) {
+                $errors[$field] = isset($rules_human[$field]) ? $rules_human[$field] : $field;
+            }
+            return $errors;
+        }
+
+        return true;
+    }
+
+    /**
+     * Restituisce true se l’azienda è valida (tutti i campi richiesti presenti).
+     */
+    public function isValid()
+    {
+        return $this->validate() === true;
+    }
+
+    /**
+     * Get the logo URL attribute.
+     *
+     * @return string|null
+     */
+    public function getLogoUrlAttribute()
+    {
+        if ($this->logo) {
+            return asset('storage/' . $this->logo);
+        }
+
+        return null;
+    }
+
+    /**
+     * Get a formatted list of discipline names.
+     *
+     * @return string
+     */
+    public function getDisciplineListAttribute()
+    {
+        return $this->disciplines->pluck('name')->implode(', ');
+    }
+}

+ 22 - 0
app/Models/Bank.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class Bank extends Model
+{
+    use HasFactory;
+
+    protected $fillable = [
+        'name',
+        'enabled',
+    ];
+
+    public function canDelete()
+    {
+        return \App\Models\PaymentMethod::where('bank_id', $this->id)->count() == 0;
+    }
+
+}

+ 20 - 0
app/Models/Card.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class Card extends Model
+{
+    use HasFactory;
+
+    protected $fillable = [
+        'name',
+        'next_day_expire',
+        'next_month_expire',
+        'use_for_user_check',
+        'one_year_expire',
+        'enabled'
+    ];
+}

+ 58 - 0
app/Models/Category.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class Category extends Model
+{
+    use HasFactory;
+
+    protected $fillable = [
+        'parent_id',
+        'name',
+        'enabled'
+    ];
+
+    public function parent()
+    {
+        return $this->belongsTo(Category::class);
+    }
+
+    public function childs() {
+        return $this->hasMany(\App\Models\Category::class,'parent_id','id') ;
+    }
+
+    public function getTree()
+    {
+        $str = '';
+        if ($this->parent_id != null)
+        {
+            $a = $this->recursiveName($this->parent_id, array($this->name));
+            $a = array_reverse($a);
+            $str = implode(" - ", $a);
+        }
+        else
+        {
+            $str = $this->name;
+        }
+        return $str;
+    }
+
+    public function recursiveName($parent_id, $array)
+    {
+        $x = \App\Models\Category::findOrFail($parent_id);
+        $array[] = $x->name;
+
+        if ($x->parent_id != null)
+        {
+            return $this->recursiveName($x->parent_id, $array);
+        }
+        else
+        {
+            return $array;
+        }
+    }
+
+}

+ 80 - 0
app/Models/Causal.php

@@ -0,0 +1,80 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class Causal extends Model
+{
+    use HasFactory;
+
+    protected $fillable = [
+        'parent_id',
+        'name',
+        'type',
+        'hidden',
+        'money',
+        'corrispettivo_fiscale',
+        'no_receipt',
+        'user_status',
+        'no_first',
+        'no_records',
+        'enabled',
+        'no_reports',
+    ];
+
+
+    public function childs() {
+        return $this->hasMany(\App\Models\Causal::class,'parent_id','id') ;
+    }
+
+    public function getTree()
+    {
+        $str = '';
+        if ($this->parent_id != null)
+        {
+            $a = $this->recursiveName($this->parent_id, array($this->name));
+            $a = array_reverse($a);
+            $str = implode(" - ", $a);
+        }
+        else
+        {
+            $str = $this->name;
+        }
+        return $str;
+    }
+
+    public function recursiveName($parent_id, $array)
+    {
+        $x = \App\Models\Causal::findOrFail($parent_id);
+        $array[] = $x->name;
+
+        if ($x->parent_id != null)
+        {
+            return $this->recursiveName($x->parent_id, $array);
+        }
+        else
+        {
+            return $array;
+        }
+    }
+
+    public function recursiveParent($parent_id, $array)
+    {
+        if ($parent_id == null)
+            return $array;
+        $x = \App\Models\Causal::findOrFail($parent_id);
+        $array[] = $x->id;
+
+        if ($x->parent_id != null)
+        {
+            return $this->recursiveParent($x->parent_id, $array);
+        }
+        else
+        {
+            return $array;
+        }
+    }
+
+}

+ 24 - 0
app/Models/City.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class City extends Model
+{
+    use HasFactory;
+
+    protected $fillable = [
+        'name',
+        'enabled',
+        'code',
+        'province_id'
+    ];
+
+    public function province()
+    {
+        return $this->belongsTo(\App\Models\Province::class);
+    }
+
+}

+ 24 - 0
app/Models/Configuration.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class Configuration extends Model
+{
+    use HasFactory;
+
+    protected $fillable = [
+        'logo',
+        'name',
+        'address',
+        'countrry',
+        'province',
+        'fiscal_code',
+        'vat',
+        'fiscal_year_month_from',
+        'fiscal_year_month_from'
+    ];
+
+}

+ 144 - 0
app/Models/Course.php

@@ -0,0 +1,144 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class Course extends Model
+{
+    use HasFactory;
+
+    protected $fillable = [
+        'parent_id',
+        'name',
+        'course_type_id',
+        'causal_id',
+        'max_members',
+        'instructor',
+        'price',
+        'months',
+        'date_from',
+        'date_to',
+        'course_duration_id',
+        'course_frequency_id',
+        'course_level_id',
+        'enabled',
+        'year',
+        'subscription_price',
+        'sub_causal_id',
+        'category_id',
+        'active',
+        'type',
+        'when',
+        'prices',
+        'instructor_id'
+    ];
+
+    public function parent()
+    {
+        return $this->belongsTo(Course::class);
+    }
+
+    public function type()
+    {
+        return $this->belongsTo(CourseType::class, 'course_type_id');
+    }
+
+    public function duration()
+    {
+        return $this->belongsTo(CourseDuration::class, 'course_duration_id');
+    }
+
+    public function frequency()
+    {
+        return $this->belongsTo(CourseFrequency::class, 'course_frequency_id');
+    }
+
+    public function level()
+    {
+        return $this->belongsTo(CourseLevel::class, 'course_level_id');
+    }
+
+    public function category()
+    {
+        return $this->belongsTo(Category::class, 'category_id');
+    }
+
+    public function member()
+    {
+        return $this->belongsTo(Member::class);
+    }
+
+    public function getins()
+    {
+        return $this->belongsTo(User::class, 'instructor_id');
+    }
+
+    public function childs() {
+        return $this->hasMany(\App\Models\Course::class,'parent_id','id') ;
+    }
+
+    public function getTree()
+    {
+        $str = '';
+        if ($this->parent_id != null)
+        {
+            $a = $this->recursiveName($this->parent_id, array($this->name));
+            $a = array_reverse($a);
+            $str = implode(" - ", $a);
+        }
+        else
+        {
+            $str = $this->name;
+        }
+        return $str;
+    }
+
+    public function recursiveName($parent_id, $array)
+    {
+        $x = \App\Models\Course::findOrFail($parent_id);
+        $array[] = $x->name;
+
+        if ($x->parent_id != null)
+        {
+            return $this->recursiveName($x->parent_id, $array);
+        }
+        else
+        {
+            return $array;
+        }
+    }
+
+    public function recursiveParent($parent_id, $array)
+    {
+        if ($parent_id == null)
+            return $array;
+        $x = \App\Models\Course::findOrFail($parent_id);
+        $array[] = $x->id;
+
+        if ($x->parent_id != null)
+        {
+            return $this->recursiveParent($x->parent_id, $array);
+        }
+        else
+        {
+            return $array;
+        }
+    }
+
+    public function getCount()
+    {
+        return \App\Models\MemberCourse::where('course_id', $this->id)->count();
+    }
+    public function getTypeFieldAttribute()
+    {
+        return $this->attributes['type'] ?? null;
+    }
+
+    // Method to get type field with capitalization
+    public function getFormattedTypeField()
+    {
+        return ucfirst($this->attributes['type'] ?? 'No Type');
+    }
+}

+ 23 - 0
app/Models/CourseDuration.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class CourseDuration extends Model
+{
+    use HasFactory;
+
+    protected $fillable = [
+        'name',
+        'duration',
+        'enabled',
+    ];
+
+    public function canDelete()
+    {
+        return true;
+    }
+
+}

Деякі файли не було показано, через те що забагато файлів було змінено