From ce48481bc8dd897d80fdd0f371750dc9623a4e45 Mon Sep 17 00:00:00 2001 From: benzimariano Date: Mon, 8 Jun 2026 21:02:12 +0200 Subject: [PATCH 1/4] fix 1.2.9 --- MEMORY.md | 26 ++++++++++++++ ...dd_numero_documento_to_individui_table.php | 8 +++-- ...01_000016_add_eventi_recurrence_fields.php | 12 ++++--- ...1_000017_add_occorrenza_mese_to_eventi.php | 8 +++-- ..._000021_add_user_id_to_documenti_table.php | 8 +++-- ...3_add_is_default_to_viste_report_table.php | 10 +++--- ...024_add_user_id_to_mailing_lists_table.php | 10 +++--- .../2026_05_10_000002_add_acl_tables.php | 8 +++-- .../2026_05_12_000001_create_ruoli_table.php | 34 ++++++++++--------- ...026_05_12_000002_change_ruolo_to_multi.php | 18 ++++++---- ...12_000003_change_responsabile_to_multi.php | 18 ++++++---- ...12_142009_add_location_to_eventi_table.php | 10 +++--- ...000003_add_signature_to_email_settings.php | 8 +++-- ...dd_signature_enabled_to_email_settings.php | 8 +++-- ...03_add_log_falliti_to_mailing_messaggi.php | 12 ++++--- ...27_000002_add_cartella_id_to_documenti.php | 8 +++-- ..._113246_add_repository_id_to_documenti.php | 16 +++++---- ...8_000001_add_storage_disk_to_documenti.php | 8 +++-- ...000002_add_uid_esterno_to_eventi_table.php | 10 +++--- vendor/composer/installed.php | 4 +-- 20 files changed, 155 insertions(+), 89 deletions(-) diff --git a/MEMORY.md b/MEMORY.md index 5bfb521f..c01b7c45 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -158,5 +158,31 @@ php artisan config:clear 1. **`app/Models/AppSetting.php`**: Sostituito `file_exists(public_path('storage/' . $path))` con `Storage::disk('public')->exists($path)` e `asset('storage/' . $path)` con `Storage::disk('public')->url($path)`. Il controllo ora avviene direttamente su `storage/app/public/` (disk root), bypassando il symlink. 2. **`app/Http/Controllers/ImpostazioniController.php`**: In `uploadLogo()`, aggiunta creazione automatica del symlink `public/storage → storage/app/public` se mancante, per garantire che il browser possa servire l'immagine. +## 2026-06-08 — Guardie Schema::hasColumn() aggiunte a 18 migration + +**Problema**: 18 migration files aggiungevano colonne a tabelle esistenti senza `Schema::hasColumn()` guard. Se eseguite su DB dove le colonne già esistevano (es. da install.sql), causavano errore "Duplicate column". + +**Fix**: Aggiunto `if (!Schema::hasColumn(...))` wrapper a ogni colonna aggiunta nelle seguenti migration: +1. `2026_06_02_000002_add_uid_esterno_to_eventi_table.php` — `uid_esterno` +2. `2026_05_12_000002_change_ruolo_to_multi.php` — `ruolo_ids` (up), `ruolo_id` (down) +3. `2026_05_28_000001_add_storage_disk_to_documenti.php` — `storage_disk` +4. `2026_05_27_113246_add_repository_id_to_documenti.php` — `repository_id` +5. `2026_05_26_000003_add_log_falliti_to_mailing_messaggi.php` — `log_falliti`, `mittente_nome`, `mittente_email` +6. `2026_05_27_000002_add_cartella_id_to_documenti.php` — `cartella_id` +7. `2026_05_16_000004_add_signature_enabled_to_email_settings.php` — `signature_enabled` +8. `2026_05_16_000003_add_signature_to_email_settings.php` — `signature` +9. `2024_01_01_000017_add_occorrenza_mese_to_eventi.php` — `occorrenza_mese` +10. `2024_01_01_000016_add_eventi_recurrence_fields.php` — `giorno_mese`, `mesi_recorrenza`, `mese_annuale` +11. `2026_05_10_000002_add_acl_tables.php` — `permissions` (users) +12. `2026_05_12_142009_add_location_to_eventi_table.php` — `luogo_indirizzo`, `luogo_url_maps` +13. `2026_05_12_000003_change_responsabile_to_multi.php` — `responsabile_ids` (up), `responsabile_id` (down) +14. `2024_01_01_000023_add_is_default_to_viste_report_table.php` — `is_default` +15. `2024_01_01_000024_add_user_id_to_mailing_lists_table.php` — `user_id` +16. `2026_05_12_000001_create_ruoli_table.php` — `ruolo_id` (gruppo_individuo) + data migration annessa +17. `2024_01_01_000021_add_user_id_to_documenti_table.php` — `user_id` +18. `2024_01_01_000013_add_numero_documento_to_individui_table.php` — `numero_documento` + +Tutte verificate con `php -l` (nessun errore di sintassi). + ## Prossimi Passi - *(nessuno — in attesa di nuove richieste)* diff --git a/database/migrations/2024_01_01_000013_add_numero_documento_to_individui_table.php b/database/migrations/2024_01_01_000013_add_numero_documento_to_individui_table.php index e4fab67c..9d7c7313 100644 --- a/database/migrations/2024_01_01_000013_add_numero_documento_to_individui_table.php +++ b/database/migrations/2024_01_01_000013_add_numero_documento_to_individui_table.php @@ -8,9 +8,11 @@ return new class extends Migration { public function up(): void { - Schema::table('individui', function (Blueprint $table) { - $table->string('numero_documento', 50)->nullable()->after('tipo_documento'); - }); + if (!Schema::hasColumn('individui', 'numero_documento')) { + Schema::table('individui', function (Blueprint $table) { + $table->string('numero_documento', 50)->nullable()->after('tipo_documento'); + }); + } } public function down(): void diff --git a/database/migrations/2024_01_01_000016_add_eventi_recurrence_fields.php b/database/migrations/2024_01_01_000016_add_eventi_recurrence_fields.php index a34f8e51..ed7551d5 100644 --- a/database/migrations/2024_01_01_000016_add_eventi_recurrence_fields.php +++ b/database/migrations/2024_01_01_000016_add_eventi_recurrence_fields.php @@ -8,11 +8,13 @@ return new class extends Migration { public function up(): void { - Schema::table('eventi', function (Blueprint $table) { - $table->string('giorno_mese')->nullable()->after('giorno_settimana'); - $table->string('mesi_recorrenza')->nullable()->after('giorno_mese'); - $table->string('mese_annuale')->nullable()->after('mesi_recorrenza'); - }); + if (!Schema::hasColumn('eventi', 'giorno_mese')) { + Schema::table('eventi', function (Blueprint $table) { + $table->string('giorno_mese')->nullable()->after('giorno_settimana'); + $table->string('mesi_recorrenza')->nullable()->after('giorno_mese'); + $table->string('mese_annuale')->nullable()->after('mesi_recorrenza'); + }); + } } public function down(): void diff --git a/database/migrations/2024_01_01_000017_add_occorrenza_mese_to_eventi.php b/database/migrations/2024_01_01_000017_add_occorrenza_mese_to_eventi.php index 08dacb46..93f549e8 100644 --- a/database/migrations/2024_01_01_000017_add_occorrenza_mese_to_eventi.php +++ b/database/migrations/2024_01_01_000017_add_occorrenza_mese_to_eventi.php @@ -8,9 +8,11 @@ return new class extends Migration { public function up(): void { - Schema::table('eventi', function (Blueprint $table) { - $table->unsignedTinyInteger('occorrenza_mese')->nullable()->after('giorno_mese'); - }); + if (!Schema::hasColumn('eventi', 'occorrenza_mese')) { + Schema::table('eventi', function (Blueprint $table) { + $table->unsignedTinyInteger('occorrenza_mese')->nullable()->after('giorno_mese'); + }); + } } public function down(): void diff --git a/database/migrations/2024_01_01_000021_add_user_id_to_documenti_table.php b/database/migrations/2024_01_01_000021_add_user_id_to_documenti_table.php index 6a9b29a6..ed4565b3 100644 --- a/database/migrations/2024_01_01_000021_add_user_id_to_documenti_table.php +++ b/database/migrations/2024_01_01_000021_add_user_id_to_documenti_table.php @@ -8,9 +8,11 @@ return new class extends Migration { public function up(): void { - Schema::table('documenti', function (Blueprint $table) { - $table->foreignId('user_id')->nullable()->constrained('users')->onDelete('set null')->after('tenant_id'); - }); + if (!Schema::hasColumn('documenti', 'user_id')) { + Schema::table('documenti', function (Blueprint $table) { + $table->foreignId('user_id')->nullable()->constrained('users')->onDelete('set null')->after('tenant_id'); + }); + } } public function down(): void diff --git a/database/migrations/2024_01_01_000023_add_is_default_to_viste_report_table.php b/database/migrations/2024_01_01_000023_add_is_default_to_viste_report_table.php index 74547d76..a5cb8d9c 100644 --- a/database/migrations/2024_01_01_000023_add_is_default_to_viste_report_table.php +++ b/database/migrations/2024_01_01_000023_add_is_default_to_viste_report_table.php @@ -8,10 +8,12 @@ return new class extends Migration { public function up(): void { - Schema::table('viste_report', function (Blueprint $table) { - $table->boolean('is_default')->default(false)->after('nome'); - $table->index('is_default'); - }); + if (!Schema::hasColumn('viste_report', 'is_default')) { + Schema::table('viste_report', function (Blueprint $table) { + $table->boolean('is_default')->default(false)->after('nome'); + $table->index('is_default'); + }); + } } public function down(): void diff --git a/database/migrations/2024_01_01_000024_add_user_id_to_mailing_lists_table.php b/database/migrations/2024_01_01_000024_add_user_id_to_mailing_lists_table.php index 5cd09cb1..2562461a 100644 --- a/database/migrations/2024_01_01_000024_add_user_id_to_mailing_lists_table.php +++ b/database/migrations/2024_01_01_000024_add_user_id_to_mailing_lists_table.php @@ -8,10 +8,12 @@ return new class extends Migration { public function up(): void { - Schema::table('mailing_lists', function (Blueprint $table) { - $table->foreignId('user_id')->nullable()->constrained('users')->onDelete('set null')->after('tenant_id'); - $table->index('user_id'); - }); + if (!Schema::hasColumn('mailing_lists', 'user_id')) { + Schema::table('mailing_lists', function (Blueprint $table) { + $table->foreignId('user_id')->nullable()->constrained('users')->onDelete('set null')->after('tenant_id'); + $table->index('user_id'); + }); + } } public function down(): void diff --git a/database/migrations/2026_05_10_000002_add_acl_tables.php b/database/migrations/2026_05_10_000002_add_acl_tables.php index 5549aba3..b784902d 100644 --- a/database/migrations/2026_05_10_000002_add_acl_tables.php +++ b/database/migrations/2026_05_10_000002_add_acl_tables.php @@ -8,9 +8,11 @@ return new class extends Migration { public function up(): void { - Schema::table('users', function (Blueprint $table) { - $table->json('permissions')->nullable()->after('is_admin'); - }); + if (!Schema::hasColumn('users', 'permissions')) { + Schema::table('users', function (Blueprint $table) { + $table->json('permissions')->nullable()->after('is_admin'); + }); + } Schema::create('activity_logs', function (Blueprint $table) { $table->id(); diff --git a/database/migrations/2026_05_12_000001_create_ruoli_table.php b/database/migrations/2026_05_12_000001_create_ruoli_table.php index c685b211..5cbbaa9b 100644 --- a/database/migrations/2026_05_12_000001_create_ruoli_table.php +++ b/database/migrations/2026_05_12_000001_create_ruoli_table.php @@ -38,25 +38,27 @@ return new class extends Migration ]); } - Schema::table('gruppo_individuo', function (Blueprint $table) { - $table->unsignedBigInteger('ruolo_id')->nullable()->after('ruolo_nel_gruppo'); - $table->foreign('ruolo_id')->references('id')->on('ruoli')->onDelete('set null'); - $table->index('ruolo_id'); - }); + if (!Schema::hasColumn('gruppo_individuo', 'ruolo_id')) { + Schema::table('gruppo_individuo', function (Blueprint $table) { + $table->unsignedBigInteger('ruolo_id')->nullable()->after('ruolo_nel_gruppo'); + $table->foreign('ruolo_id')->references('id')->on('ruoli')->onDelete('set null'); + $table->index('ruolo_id'); + }); - $ruoliMap = DB::table('ruoli')->pluck('id', 'nome')->toArray(); + $ruoliMap = DB::table('ruoli')->pluck('id', 'nome')->toArray(); - $pivotRecords = DB::table('gruppo_individuo') - ->whereNotNull('ruolo_nel_gruppo') - ->where('ruolo_nel_gruppo', '!=', '') - ->distinct() - ->pluck('ruolo_nel_gruppo'); + $pivotRecords = DB::table('gruppo_individuo') + ->whereNotNull('ruolo_nel_gruppo') + ->where('ruolo_nel_gruppo', '!=', '') + ->distinct() + ->pluck('ruolo_nel_gruppo'); - foreach ($pivotRecords as $ruoloNome) { - if (isset($ruoliMap[$ruoloNome])) { - DB::table('gruppo_individuo') - ->where('ruolo_nel_gruppo', $ruoloNome) - ->update(['ruolo_id' => $ruoliMap[$ruoloNome]]); + foreach ($pivotRecords as $ruoloNome) { + if (isset($ruoliMap[$ruoloNome])) { + DB::table('gruppo_individuo') + ->where('ruolo_nel_gruppo', $ruoloNome) + ->update(['ruolo_id' => $ruoliMap[$ruoloNome]]); + } } } } diff --git a/database/migrations/2026_05_12_000002_change_ruolo_to_multi.php b/database/migrations/2026_05_12_000002_change_ruolo_to_multi.php index a8c7361b..6c6d1279 100644 --- a/database/migrations/2026_05_12_000002_change_ruolo_to_multi.php +++ b/database/migrations/2026_05_12_000002_change_ruolo_to_multi.php @@ -13,9 +13,11 @@ return new class extends Migration $table->dropColumn('ruolo_id'); }); - Schema::table('gruppo_individuo', function (Blueprint $table) { - $table->json('ruolo_ids')->nullable()->after('individuo_id'); - }); + if (!Schema::hasColumn('gruppo_individuo', 'ruolo_ids')) { + Schema::table('gruppo_individuo', function (Blueprint $table) { + $table->json('ruolo_ids')->nullable()->after('individuo_id'); + }); + } } public function down(): void @@ -24,9 +26,11 @@ return new class extends Migration $table->dropColumn('ruolo_ids'); }); - Schema::table('gruppo_individuo', function (Blueprint $table) { - $table->unsignedBigInteger('ruolo_id')->nullable()->after('individuo_id'); - $table->foreign('ruolo_id')->references('id')->on('ruoli')->onDelete('set null'); - }); + if (!Schema::hasColumn('gruppo_individuo', 'ruolo_id')) { + Schema::table('gruppo_individuo', function (Blueprint $table) { + $table->unsignedBigInteger('ruolo_id')->nullable()->after('individuo_id'); + $table->foreign('ruolo_id')->references('id')->on('ruoli')->onDelete('set null'); + }); + } } }; \ No newline at end of file diff --git a/database/migrations/2026_05_12_000003_change_responsabile_to_multi.php b/database/migrations/2026_05_12_000003_change_responsabile_to_multi.php index 023be64b..126a8e46 100644 --- a/database/migrations/2026_05_12_000003_change_responsabile_to_multi.php +++ b/database/migrations/2026_05_12_000003_change_responsabile_to_multi.php @@ -13,9 +13,11 @@ return new class extends Migration $table->dropColumn('responsabile_id'); }); - Schema::table('gruppi', function (Blueprint $table) { - $table->json('responsabile_ids')->nullable()->after('diocesi_id'); - }); + if (!Schema::hasColumn('gruppi', 'responsabile_ids')) { + Schema::table('gruppi', function (Blueprint $table) { + $table->json('responsabile_ids')->nullable()->after('diocesi_id'); + }); + } } public function down(): void @@ -24,9 +26,11 @@ return new class extends Migration $table->dropColumn('responsabile_ids'); }); - Schema::table('gruppi', function (Blueprint $table) { - $table->unsignedBigInteger('responsabile_id')->nullable()->after('diocesi_id'); - $table->foreign('responsabile_id')->references('id')->on('individui')->onDelete('set null'); - }); + if (!Schema::hasColumn('gruppi', 'responsabile_id')) { + Schema::table('gruppi', function (Blueprint $table) { + $table->unsignedBigInteger('responsabile_id')->nullable()->after('diocesi_id'); + $table->foreign('responsabile_id')->references('id')->on('individui')->onDelete('set null'); + }); + } } }; \ No newline at end of file diff --git a/database/migrations/2026_05_12_142009_add_location_to_eventi_table.php b/database/migrations/2026_05_12_142009_add_location_to_eventi_table.php index 428c38b5..0e2b477c 100644 --- a/database/migrations/2026_05_12_142009_add_location_to_eventi_table.php +++ b/database/migrations/2026_05_12_142009_add_location_to_eventi_table.php @@ -8,10 +8,12 @@ return new class extends Migration { public function up(): void { - Schema::table('eventi', function (Blueprint $table) { - $table->string('luogo_indirizzo', 500)->nullable()->after('note'); - $table->text('luogo_url_maps')->nullable()->after('luogo_indirizzo'); - }); + if (!Schema::hasColumn('eventi', 'luogo_indirizzo')) { + Schema::table('eventi', function (Blueprint $table) { + $table->string('luogo_indirizzo', 500)->nullable()->after('note'); + $table->text('luogo_url_maps')->nullable()->after('luogo_indirizzo'); + }); + } } public function down(): void diff --git a/database/migrations/2026_05_16_000003_add_signature_to_email_settings.php b/database/migrations/2026_05_16_000003_add_signature_to_email_settings.php index 3210632b..ffeef306 100644 --- a/database/migrations/2026_05_16_000003_add_signature_to_email_settings.php +++ b/database/migrations/2026_05_16_000003_add_signature_to_email_settings.php @@ -8,9 +8,11 @@ return new class extends Migration { public function up(): void { - Schema::table('email_settings', function (Blueprint $table) { - $table->longText('signature')->nullable()->after('reply_to'); - }); + if (!Schema::hasColumn('email_settings', 'signature')) { + Schema::table('email_settings', function (Blueprint $table) { + $table->longText('signature')->nullable()->after('reply_to'); + }); + } } public function down(): void diff --git a/database/migrations/2026_05_16_000004_add_signature_enabled_to_email_settings.php b/database/migrations/2026_05_16_000004_add_signature_enabled_to_email_settings.php index 148ea4bf..70569b6b 100644 --- a/database/migrations/2026_05_16_000004_add_signature_enabled_to_email_settings.php +++ b/database/migrations/2026_05_16_000004_add_signature_enabled_to_email_settings.php @@ -8,9 +8,11 @@ return new class extends Migration { public function up(): void { - Schema::table('email_settings', function (Blueprint $table) { - $table->boolean('signature_enabled')->default(true)->after('signature'); - }); + if (!Schema::hasColumn('email_settings', 'signature_enabled')) { + Schema::table('email_settings', function (Blueprint $table) { + $table->boolean('signature_enabled')->default(true)->after('signature'); + }); + } } public function down(): void diff --git a/database/migrations/2026_05_26_000003_add_log_falliti_to_mailing_messaggi.php b/database/migrations/2026_05_26_000003_add_log_falliti_to_mailing_messaggi.php index 73fe6ee1..89ab7f99 100644 --- a/database/migrations/2026_05_26_000003_add_log_falliti_to_mailing_messaggi.php +++ b/database/migrations/2026_05_26_000003_add_log_falliti_to_mailing_messaggi.php @@ -10,11 +10,13 @@ return new class extends Migration { public function up(): void { - Schema::table('mailing_messaggi', function (Blueprint $table) { - $table->json('log_falliti')->nullable()->after('invii_falliti'); - $table->string('mittente_nome')->nullable()->after('log_falliti'); - $table->string('mittente_email')->nullable()->after('mittente_nome'); - }); + if (!Schema::hasColumn('mailing_messaggi', 'log_falliti')) { + Schema::table('mailing_messaggi', function (Blueprint $table) { + $table->json('log_falliti')->nullable()->after('invii_falliti'); + $table->string('mittente_nome')->nullable()->after('log_falliti'); + $table->string('mittente_email')->nullable()->after('mittente_nome'); + }); + } } public function down(): void diff --git a/database/migrations/2026_05_27_000002_add_cartella_id_to_documenti.php b/database/migrations/2026_05_27_000002_add_cartella_id_to_documenti.php index b083650a..c1ea5d8f 100644 --- a/database/migrations/2026_05_27_000002_add_cartella_id_to_documenti.php +++ b/database/migrations/2026_05_27_000002_add_cartella_id_to_documenti.php @@ -8,9 +8,11 @@ return new class extends Migration { public function up(): void { - Schema::table('documenti', function (Blueprint $table) { - $table->foreignId('cartella_id')->nullable()->constrained('documenti_cartelle')->onDelete('set null'); - }); + if (!Schema::hasColumn('documenti', 'cartella_id')) { + Schema::table('documenti', function (Blueprint $table) { + $table->foreignId('cartella_id')->nullable()->constrained('documenti_cartelle')->onDelete('set null'); + }); + } } public function down(): void diff --git a/database/migrations/2026_05_27_113246_add_repository_id_to_documenti.php b/database/migrations/2026_05_27_113246_add_repository_id_to_documenti.php index f1f3c6f1..8ebe7da1 100644 --- a/database/migrations/2026_05_27_113246_add_repository_id_to_documenti.php +++ b/database/migrations/2026_05_27_113246_add_repository_id_to_documenti.php @@ -8,13 +8,15 @@ return new class extends Migration { public function up(): void { - Schema::table('documenti', function (Blueprint $table) { - $table->foreignId('repository_id') - ->nullable() - ->constrained('storage_repositories') - ->nullOnDelete() - ->after('cartella_id'); - }); + if (!Schema::hasColumn('documenti', 'repository_id')) { + Schema::table('documenti', function (Blueprint $table) { + $table->foreignId('repository_id') + ->nullable() + ->constrained('storage_repositories') + ->nullOnDelete() + ->after('cartella_id'); + }); + } } public function down(): void diff --git a/database/migrations/2026_05_28_000001_add_storage_disk_to_documenti.php b/database/migrations/2026_05_28_000001_add_storage_disk_to_documenti.php index 99b9794a..dc846ad1 100644 --- a/database/migrations/2026_05_28_000001_add_storage_disk_to_documenti.php +++ b/database/migrations/2026_05_28_000001_add_storage_disk_to_documenti.php @@ -10,9 +10,11 @@ return new class extends Migration { public function up(): void { - Schema::table('documenti', function (Blueprint $table) { - $table->string('storage_disk', 50)->nullable()->after('repository_id'); - }); + if (!Schema::hasColumn('documenti', 'storage_disk')) { + Schema::table('documenti', function (Blueprint $table) { + $table->string('storage_disk', 50)->nullable()->after('repository_id'); + }); + } } public function down(): void diff --git a/database/migrations/2026_06_02_000002_add_uid_esterno_to_eventi_table.php b/database/migrations/2026_06_02_000002_add_uid_esterno_to_eventi_table.php index ca38f28e..48bca8ed 100644 --- a/database/migrations/2026_06_02_000002_add_uid_esterno_to_eventi_table.php +++ b/database/migrations/2026_06_02_000002_add_uid_esterno_to_eventi_table.php @@ -10,10 +10,12 @@ return new class extends Migration { public function up(): void { - Schema::table('eventi', function (Blueprint $table) { - $table->string('uid_esterno', 500)->nullable()->after('luogo_url_maps'); - $table->index('uid_esterno'); - }); + if (!Schema::hasColumn('eventi', 'uid_esterno')) { + Schema::table('eventi', function (Blueprint $table) { + $table->string('uid_esterno', 500)->nullable()->after('luogo_url_maps'); + $table->index('uid_esterno'); + }); + } } public function down(): void diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index fb1e8347..ae6cd0c4 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'laravel/laravel', 'pretty_version' => 'dev-main', 'version' => 'dev-main', - 'reference' => '90093a086b35dce8edd97fce09d32d2be42853e8', + 'reference' => '7d7d384a0d719ca6fa83e01e50577b94c877e676', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -427,7 +427,7 @@ 'laravel/laravel' => array( 'pretty_version' => 'dev-main', 'version' => 'dev-main', - 'reference' => '90093a086b35dce8edd97fce09d32d2be42853e8', + 'reference' => '7d7d384a0d719ca6fa83e01e50577b94c877e676', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), From 6f106e31309c1cb590384e6d93617bd2e2647e60 Mon Sep 17 00:00:00 2001 From: benzimariano Date: Mon, 8 Jun 2026 21:04:06 +0200 Subject: [PATCH 2/4] vers 1.2.10 --- ...d_ruolo_nel_gruppo_to_gruppo_individuo.php | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 database/migrations/2026_06_08_182714_add_ruolo_nel_gruppo_to_gruppo_individuo.php diff --git a/database/migrations/2026_06_08_182714_add_ruolo_nel_gruppo_to_gruppo_individuo.php b/database/migrations/2026_06_08_182714_add_ruolo_nel_gruppo_to_gruppo_individuo.php new file mode 100644 index 00000000..d53df107 --- /dev/null +++ b/database/migrations/2026_06_08_182714_add_ruolo_nel_gruppo_to_gruppo_individuo.php @@ -0,0 +1,23 @@ +string('ruolo_nel_gruppo')->nullable()->after('ruolo_ids'); + }); + } + } + + public function down(): void + { + } +}; From d82b123bfd4e1212ed99f4cc7a5eaf1ce761963d Mon Sep 17 00:00:00 2001 From: benzimariano Date: Tue, 9 Jun 2026 11:02:01 +0200 Subject: [PATCH 3/4] versione 2.0.0 --- MEMORY.md | 130 +++++++++++++- app/Helpers/ReportColumnRegistry.php | 4 + app/Http/Controllers/DocumentoController.php | 60 ++++++- app/Http/Controllers/EmailController.php | 2 +- app/Http/Controllers/EventoController.php | 31 +++- app/Http/Controllers/GruppoController.php | 101 +++++++++-- .../Controllers/ImpostazioniController.php | 68 +++++++- app/Http/Controllers/IndividuoController.php | 113 +++++++++++- app/Http/Controllers/MailingController.php | 18 +- .../Controllers/MailingListController.php | 46 +++-- app/Http/Controllers/ReportController.php | 160 ++++++++++++++++- app/Models/Documento.php | 2 + app/Models/Evento.php | 2 + app/Models/Gruppo.php | 2 + app/Models/Individuo.php | 2 + app/Models/MailingList.php | 3 + resources/views/documenti/edit.blade.php | 36 ++++ resources/views/documenti/index.blade.php | 67 ++++++++ resources/views/eventi/create.blade.php | 9 + resources/views/eventi/edit.blade.php | 9 + resources/views/eventi/index.blade.php | 10 +- resources/views/eventi/show.blade.php | 11 ++ resources/views/gruppi/create.blade.php | 9 + resources/views/gruppi/edit.blade.php | 9 + resources/views/gruppi/index.blade.php | 147 ++++++++++++++++ resources/views/gruppi/show.blade.php | 11 ++ resources/views/impostazioni/index.blade.php | 162 ++++++++++++++++++ resources/views/individui/create.blade.php | 9 + resources/views/individui/edit.blade.php | 10 ++ resources/views/individui/index.blade.php | 129 +++++++++++++- resources/views/individui/show.blade.php | 15 ++ resources/views/layouts/adminlte.blade.php | 9 + .../views/mailing-liste/create.blade.php | 3 + resources/views/mailing-liste/edit.blade.php | 143 ++++++++++++---- resources/views/mailing-liste/index.blade.php | 12 ++ resources/views/mailing-liste/show.blade.php | 17 ++ resources/views/report/index.blade.php | 50 ++++++ routes/web.php | 14 ++ 38 files changed, 1552 insertions(+), 83 deletions(-) diff --git a/MEMORY.md b/MEMORY.md index c01b7c45..fb680df9 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -28,6 +28,25 @@ App gestionale Laravel 13 con AdminLTE 4 per gestione Persone e Gruppi. - Esclude: .git, node_modules, tests, cache, storage content, vendor/docs/tests, Docker - **Istruzioni post-estrazione complete**: mkdir, permessi, configurazione, cache clear +## Tag System + +### Implementazione Completa (Opzione B) +- **Migration**: `2026_06_08_194155_create_tags_tables.php` — tabelle `tags` + `taggables` +- **Model `Tag.php`**: morphToMany a 5 entity types, auto-slug su creating, color badge +- **Trait `HasTagsLight.php`**: `tags()` morphToMany, scopes `withAllTags`/`withAnyTags` +- **5 Models aggiornati**: Individuo, Gruppo, Evento, Documento, MailingList — usano `HasTagsLight` +- **ImpostazioniController**: CRUD completo per Tag (store, update, destroy, reorder) con tab nella pagina Impostazioni +- **Tag selector**: `_tag-selector.blade.php` — search + pill badges con colori, incluso in create/edit di Individui, Gruppi, Eventi, MailingList +- **Tag filtering**: `?tag[]=slug` supportato in `IndividuoController@index`, `GruppoController@index`, `EventoController@index`, `DocumentoController@index`, `MailingListController@index` via `withAnyTags` scope +- **Tag column in index views**: Colonna "Tag" con badge colorati cliccabili (collegamento a index filtrato) in individui, gruppi, eventi, documenti, mailing-liste +- **Tag filter widget**: `_tag-filter-bar.blade.php` — barra cliccabile con badge colorati sopra ogni index table (individui, gruppi, eventi, documenti, mailing-liste). Attiva/disattiva filtro `?tag[]=` con un click, evidenzia tag attivi, include "Cancella filtri" quando attivo +- **RicercaController**: Controller per ricerca unificata per tag — mostra risultati aggregati da tutte 5 entity types con info-box counters +- **Route `GET /ricerca`**: Registrata come `ricerca.index` → `RicercaController@index` +- **Sidebar link**: "Ricerca per Tag" (icona `fas fa-tags`) sotto la sezione Report, visibile con permesso `individui` +- **Mass tag action for Documenti**: `POST /documenti/mass-tag` → `DocumentoController@massTag` — modal with tag selector + assign/remove radio, processes selected documents in chunks of 100 via `syncWithoutDetaching()` (assign) or `detach()` (remove) +- **Report tag columns**: `tags.nome` disponibile come colonna per individui/gruppi/eventi/documenti nei report personalizzati +- **Report tag filters**: Custom report form include tag multi-select (Select2) + modalità OR/AND; `runCustomReport()` applica filtri via `withAllTags`/`withAnyTags` + ## Bug Fix Recenti ### 2026-06-07 — Colonne mancanti in eventi e gruppo_individuo @@ -184,5 +203,114 @@ php artisan config:clear Tutte verificate con `php -l` (nessun errore di sintassi). +### 2026-06-09 — `@stack('scripts')` mancante nel layout + +**Problema**: Il partial `_tag-selector.blade.php` usa `@push('scripts')` per iniettare le funzioni JS `toggleTag`, `removeTag`, `filterTags`. Il layout `adminlte.blade.php` aveva solo `@yield('scripts')` (per `@section`), ma **non** `@stack('scripts')` (per `@push`). Di conseguenza il JavaScript interattivo del tag selector (clic sui badge, ricerca, rimozione) non veniva mai caricato su nessuna pagina create/edit. + +**Fix**: `resources/views/layouts/adminlte.blade.php:305` — Aggiunto `@stack('scripts')` subito prima di `@yield('scripts')`. + +### 2026-06-09 — Tag support completato per MailingList + Report + Ricerca + +**MailingList — Controller & Views** (`app/Http/Controllers/MailingListController.php`): +- `index()`: eager-load `tags`, supporto `?tag[]=` filter via `withAnyTags`, pass `$allTags` per filter bar +- `create()`: pass `$tags` per tag selector +- `store()`: validazione `tags.* exists:tags,id`, sync dopo create. Auto-fetch email da `Individuo::contatti()` quando email non fornita +- `edit()`: eager-load `tags`, pass `$tags` + `$selectedTags` +- `update()`: validazione `tags.*`, sync se presente, detach se assente. Auto-fetch email da `Individuo::contatti()` quando email non fornita. Diff logica: `$toRemove = array_diff($existingIds, $newIds)` corretto — rimuove solo contatti deselezionati/esplicitamente rimossi +- `show()`: eager-load `tags` + +**MailingList — Views**: +- `index.blade.php`: aggiunto `@include('partials._tag-filter-bar')`, colonna "Tag" con badge colorati cliccabili +- `create.blade.php`: incluso `_tag-selector` dopo descrizione +- `edit.blade.php`: incluso `_tag-selector` dopo descrizione (prima della sezione contatti). Submit handler include `#individui-select.selectedOptions` in `contatti_json` con `email: ''` (già presente da fix precedente) +- `show.blade.php`: aggiunta card Tag nel pannello sinistro con badge colorati (o "Nessun tag assegnato") + +**ReportColumnRegistry** (`app/Helpers/ReportColumnRegistry.php`): +- Aggiunta colonna `'tags.nome'` con label 'Tag' a 4 entity types: individui, gruppi, eventi, documenti + +**ReportController** (`app/Http/Controllers/ReportController.php`): +- `runCustomReport()`: supporta `config['tag_filter']` (array di tag IDs) + `config['tag_filter_mode']` ('any'/'all') +- `storeCustom()`: ora salva `tag_filter` e `tag_filter_mode` nella config del report salvato +- Carica `$allTags` nell'index e passa alla vista +- Converte IDs → slugs via `Tag::whereIn('id', ...)->pluck('slug')`, applica `withAllTags`/`withAnyTags` +- Eager-load `tags` automaticamente quando la colonna `tags.nome` è selezionata + +**Report — View** (`resources/views/report/index.blade.php`): +- Aggiunto filtro tag multi-select nel form "Crea Report Personalizzato" (Select2 con badge colorati) +- Aggiunto select per modalità filtro (OR/AND) +- JS: `initTagFilterSelect2()` / `destroyTagFilterSelect2()` con template colorato + +**RicercaController** (`app/Http/Controllers/RicercaController.php`): +- Aggiunta MailingList alle query per tag slug +- `results['mailingLists']` e `totals['mailingLists']` passati alla view + +**Ricerca — View** (`resources/views/ricerca/index.blade.php`): +- Aggiunto info-box Mailing List (colore bg-purple) con link "Vedi tutti" → `mailing-liste.index?tag[]=` +- Aggiunta tabella dettaglio Mailing List nel risultato di ricerca (nome, contatti, stato, azione show) +- Fix: `route('documenti.edit')` → `url('/documenti/' . $doc->id . '/edit')` (route non esiste) + +### 2026-06-09 — Fix: storeCustom() non salvava tag_filter + +**Problema**: `ReportController@storeCustom()` non salvava `tag_filter` e `tag_filter_mode` nella config del report personalizzato. Quando un report salvato veniva eseguito, `runCustomReport()` non trovava i filtri tag nella config e non li applicava. + +**Fix**: Aggiunte righe 143-148 in `ReportController.php`: +```php +if ($request->has('tag_filter')) { + $config['tag_filter'] = $request->input('tag_filter'); +} +if ($request->filled('tag_filter_mode')) { + $config['tag_filter_mode'] = $request->input('tag_filter_mode'); +} +``` + +### 2026-06-09 — Fix: MailingList edit — "Carica contatti" non inseriva i selezionati + +**Problema**: Due bug nel JS `caricaSelezionati()` in `mailing-liste/edit.blade.php`: +1. **Typo**: `ind.cogname` invece di `ind.cognome` — il campo Cognome nella tabella era sempre vuoto/undefined (API restituisce `cognome`) +2. **Email gate**: `if (ind.emails && ind.emails.length > 0)` filtro bloccante — individui senza email salvata in `contatti` venivano SILENZIOSAMENTE scartati, mai aggiunti alla tabella + +**Fix**: +1. `ind.cogname` → `ind.cognome` (righe 227 e 256) +2. Rimossa condizione `if (ind.emails...)` — ora anche individui senza email vengono aggiunti alla tabella +3. `email: ind.emails[0]` → `email: (ind.emails && ind.emails.length > 0) ? ind.emails[0] : ''` — email vuota se nessuna presente +4. Controller già fixato per auto-risolvere email da `Individuo::contatti()` al salvataggio + +### 2026-06-09 — Fix: MailingListController email gate bloccava nuovi contatti senza email + +**Problema**: `store()` e `update()` in `MailingListController` usavano `!empty($contatto['email'])` per decidere se creare un contatto. L'email era richiesta ma il MailingContact non memorizza l'email — la risolve dall'Individuo al momento del display/invio. Questo impediva di aggiungere individui senza email (o con email non nei contatti) alla mailing list. + +**Fix**: Rimossa completamente la verifica email sia in `store()` che in `update()`. Ora ogni `individuo_id` valido viene aggiunto come MailingContact. L'email viene risolta al display dal model `$contact->individuo->contatti->where('tipo', 'email')->first()?->valore`. + +Prima (bloccante): +```php +if (!empty($contatto['individuo_id']) && !empty($contatto['email'])) { create } +``` +Dopo (semplice): +```php +if (!empty($contatto['individuo_id'])) { create } +``` + ## Prossimi Passi -- *(nessuno — in attesa di nuove richieste)* +- [DONE] Aggiungere filtro Tag nella vista Documenti — già presente (tag filter bar + colonna Tag in lista/griglia) +- [DONE] Gestire il caso `tag[]` vuoto — già gestito da `$request->filled('tag')` in tutti i 4 controller index +- [DONE] MailingList — tag support completato (controller, index/create/edit/show views, filter bar) +- [DONE] Report — tag columns in ReportColumnRegistry + tag filter in custom reports + UI multi-select +- [DONE] Ricerca per Tag — MailingList integrata nei risultati +- [DONE] `storeCustom()` ora salva correttamente `tag_filter` e `tag_filter_mode` +- [DONE] `MailingListController@store/update` — auto-fetch email da Individuo quando non fornita +- [DONE] Verificata presenza `@stack('scripts')` nel layout — presente a riga 305 (prima di `@yield('scripts')`) +- [DONE] Verificata mass tag action Documenti (`DocumentoController@massTag`) — funzionante +- [DONE] Verificato: tutti i 5 entity controllers passano `$allTags` alle viste index +- [DONE] La logica `$toRemove = array_diff($existingIds, $newIds)` in `update()` è corretta — rimuove solo contatti che l'utente ha esplicitamente deselezionato o rimosso dalla tabella. Non necessita modifiche. +- [DONE] SweetAlert2 conferma su salvataggio mailing list (edit.blade.php) quando ci sono contatti senza email +- [DONE] Normalizzata `invia()` da `flatMap`+`pluck` a `map`+`first()` (1 email per individuo, come `invioElabora()`) +- [DONE] Aggiunto logging contatti saltati in `invia()` e `invioElabora()` (quando senza email) +- [DONE] Fix: contattiCaricati initialization ora include codice_id/cognome/nome da righe DOM — contatti esistenti non più blank dopo Carica contatti +- [DONE] Fix: EmailController@destroy ora redirect a inbox invece che back() (non rimane su email cancellata) +- [DONE] Mass action Individui: Assegna Gruppo (route + controller + modal con select gruppi + Assign/Remove radio) +- [DONE] Mass action Individui: Assegna Tag (route + controller + modal con _tag-selector) +- [DONE] Mass action Gruppi: Assegna Tag (route + controller + modal con _tag-selector) +- [DONE] Mass action Gruppi: Elimina (route + controller + modal con conferma, detach membri + delete) +- [DONE] Fix: IndividuoController@massGruppo e @massTag ora convertono stringa ids in array (non funzionavano perché hidden input invia stringa, non array) +- [DONE] Ricerca per Tag multipli: controller accetta `?tag[]=slug1&tag[]=slug2`, usa `withAllTags` (AND logic) per 2+ tag. Vista aggiornata: checkbox cliccabili per multi-selezione, info boxes unificate, "Vedi tutti" passa tutti i tag +- Verificare end-to-end su remote: tutte le nuove mass action, mailing list con contatti senza email, ricerca multi-tag diff --git a/app/Helpers/ReportColumnRegistry.php b/app/Helpers/ReportColumnRegistry.php index 744be113..222a6a2d 100644 --- a/app/Helpers/ReportColumnRegistry.php +++ b/app/Helpers/ReportColumnRegistry.php @@ -132,6 +132,7 @@ class ReportColumnRegistry 'contatti.telefono' => ['label' => 'Telefono (da contatti)', 'type' => 'relation'], 'contatti.cellulare' => ['label' => 'Cellulare (da contatti)', 'type' => 'relation'], 'gruppi.nome' => ['label' => 'Gruppi', 'type' => 'relation'], + 'tags.nome' => ['label' => 'Tag', 'type' => 'relation'], ], 'gruppi' => [ @@ -151,6 +152,7 @@ class ReportColumnRegistry 'parent.nome' => ['label' => 'Gruppo Padre', 'type' => 'relation'], 'diocesi.nome' => ['label' => 'Diocesi', 'type' => 'relation'], 'individui_count' => ['label' => 'Numero Membri', 'type' => 'relation'], + 'tags.nome' => ['label' => 'Tag', 'type' => 'relation'], ], 'eventi' => [ @@ -171,6 +173,7 @@ class ReportColumnRegistry 'info_ricorrenza' => ['label' => 'Info Ricorrenza', 'type' => 'accessor'], 'gruppi.nome' => ['label' => 'Gruppi', 'type' => 'relation'], 'responsabili.nome_completo' => ['label' => 'Responsabili', 'type' => 'relation'], + 'tags.nome' => ['label' => 'Tag', 'type' => 'relation'], ], 'documenti' => [ @@ -183,6 +186,7 @@ class ReportColumnRegistry 'note' => ['label' => 'Note', 'type' => 'direct'], 'created_at' => ['label' => 'Data Caricamento', 'type' => 'direct'], 'user.name' => ['label' => 'Caricato Da', 'type' => 'relation'], + 'tags.nome' => ['label' => 'Tag', 'type' => 'relation'], ], 'contatti' => [ diff --git a/app/Http/Controllers/DocumentoController.php b/app/Http/Controllers/DocumentoController.php index 9fa9f437..4fa7146b 100644 --- a/app/Http/Controllers/DocumentoController.php +++ b/app/Http/Controllers/DocumentoController.php @@ -11,6 +11,7 @@ use App\Models\Individuo; use App\Models\Gruppo; use App\Models\Evento; use App\Models\StorageRepository; +use App\Models\Tag; use App\Models\TipologiaDocumento; use App\Services\StorageRepositoryService; use Illuminate\Http\JsonResponse; @@ -42,9 +43,14 @@ class DocumentoController extends Controller $folderId = $request->get('folder_id'); $repoId = $request->get('repository_id'); - $query = Documento::with(['user', 'target', 'cartella', 'repository']) + $query = Documento::with(['user', 'target', 'cartella', 'repository', 'tags']) ->orderByDesc('created_at'); + if ($request->filled('tag')) { + $tagSlugs = (array) $request->input('tag'); + $query->withAnyTags($tagSlugs); + } + if ($repoId) { $query->where('repository_id', $repoId); } elseif ($folderId) { @@ -54,6 +60,7 @@ class DocumentoController extends Controller } $documenti = $query->paginate(20); + $allTags = Tag::orderBy('name')->get(); $cartelle = DocumentoCartella::with('children') ->whereNull('parent_id') ->orderBy('nome') @@ -89,7 +96,7 @@ class DocumentoController extends Controller 'documenti', 'cartelle', 'currentFolder', 'breadcrumb', 'sottoCartelle', 'currentRepo', 'repositories', 'individui', 'gruppi', 'eventi', 'mailingLists', 'tipologie', - 'cartelleMoveOptions' + 'cartelleMoveOptions', 'allTags' )); } @@ -106,13 +113,15 @@ class DocumentoController extends Controller public function edit($documento) { $this->authorizeWrite('documenti'); - $documento = Documento::with('target')->findOrFail($documento); + $documento = Documento::with(['target', 'tags'])->findOrFail($documento); $individui = Individuo::orderBy('cognome')->orderBy('nome')->get(); $gruppi = Gruppo::orderBy('nome')->get(); $eventi = Evento::orderBy('nome_evento')->get(); $mailingLists = \App\Models\MailingList::orderBy('nome')->get(); $tipologie = TipologiaDocumento::attive(); - return view('documenti.edit', compact('documento', 'individui', 'gruppi', 'eventi', 'mailingLists', 'tipologie')); + $tags = Tag::orderBy('name')->get(); + $selectedTags = $documento->tags->pluck('id')->toArray(); + return view('documenti.edit', compact('documento', 'individui', 'gruppi', 'eventi', 'mailingLists', 'tipologie', 'tags', 'selectedTags')); } public function options(Request $request) @@ -165,6 +174,8 @@ class DocumentoController extends Controller 'visibilita_target_id' => 'nullable|integer', 'visibilita_target_type' => 'nullable|string', 'note' => 'nullable|string', + 'tags' => 'nullable|array', + 'tags.*' => 'exists:tags,id', ]); $data['visibilita'] = $request->contesto_tipo ?: 'pubblico'; @@ -185,6 +196,12 @@ class DocumentoController extends Controller $documento->update($data); + if ($request->has('tags')) { + $documento->tags()->sync($request->tags); + } else { + $documento->tags()->detach(); + } + return redirect('/documenti')->with('success', 'Documento aggiornato.'); } @@ -706,6 +723,41 @@ class DocumentoController extends Controller return back()->with('success', count($ids) . ' documenti associati.'); } + public function massTag(Request $request) + { + $this->authorizeWrite('documenti'); + $idsInput = $request->input('ids', ''); + $ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []); + + if (empty($ids)) { + return back()->with('error', 'Nessun documento selezionato.'); + } + + $data = $request->validate([ + 'tags' => 'required|array', + 'tags.*' => 'exists:tags,id', + 'mode' => 'required|in:assign,remove', + ]); + + $tagIds = $data['tags']; + $mode = $data['mode']; + $count = 0; + + Documento::whereIn('id', $ids)->chunk(100, function ($documenti) use ($tagIds, $mode, &$count) { + foreach ($documenti as $documento) { + if ($mode === 'assign') { + $documento->tags()->syncWithoutDetaching($tagIds); + } else { + $documento->tags()->detach($tagIds); + } + $count++; + } + }); + + $actionLabel = $mode === 'assign' ? 'assegnati' : 'rimossi'; + return back()->with('success', "Tag $actionLabel per $count documenti."); + } + public function massMove(Request $request): RedirectResponse { $this->authorizeWrite('documenti'); diff --git a/app/Http/Controllers/EmailController.php b/app/Http/Controllers/EmailController.php index 5624ff22..59ae9601 100644 --- a/app/Http/Controllers/EmailController.php +++ b/app/Http/Controllers/EmailController.php @@ -341,7 +341,7 @@ class EmailController extends Controller $message->delete(); } - return back()->with('success', 'Email spostata nel cestino.'); + return redirect()->route('email.index', ['folder' => 'inbox'])->with('success', 'Email spostata nel cestino.'); } public function downloadAttachment($id) diff --git a/app/Http/Controllers/EventoController.php b/app/Http/Controllers/EventoController.php index e70f4adb..fcf82d21 100644 --- a/app/Http/Controllers/EventoController.php +++ b/app/Http/Controllers/EventoController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers; use App\Models\Evento; use App\Models\Gruppo; use App\Models\Individuo; +use App\Models\Tag; use App\Models\TipologiaEvento; use App\Services\IcsExportService; use Illuminate\Http\Request; @@ -16,7 +17,12 @@ class EventoController extends Controller public function index(Request $request) { $this->authorizeRead('eventi'); - $query = Evento::with(['gruppi', 'responsabili']); + $query = Evento::with(['gruppi', 'responsabili', 'tags']); + + if ($request->filled('tag')) { + $tagSlugs = (array) $request->input('tag'); + $query->withAnyTags($tagSlugs); + } if ($request->filled('search')) { $query->where('nome_evento', 'like', '%' . $request->search . '%'); @@ -47,8 +53,9 @@ class EventoController extends Controller $perPage = in_array((int) $request->perPage, [10, 20, 25, 50, 100]) ? (int) $request->perPage : 20; $eventi = $query->paginate($perPage); $gruppi = Gruppo::orderBy('nome')->get(); + $allTags = Tag::orderBy('name')->get(); - return view('eventi.index', compact('eventi', 'gruppi')); + return view('eventi.index', compact('eventi', 'gruppi', 'allTags')); } public function create(Request $request) @@ -58,8 +65,9 @@ class EventoController extends Controller $individui = Individuo::orderBy('cognome')->orderBy('nome')->get(); $selectedGruppo = $request->query('gruppo_id') ? Gruppo::find($request->query('gruppo_id')) : null; $tipologieEventi = TipologiaEvento::attive(); + $tags = Tag::orderBy('name')->get(); - return view('eventi.create', compact('gruppi', 'individui', 'selectedGruppo', 'tipologieEventi')); + return view('eventi.create', compact('gruppi', 'individui', 'selectedGruppo', 'tipologieEventi', 'tags')); } public function store(Request $request) @@ -88,6 +96,8 @@ class EventoController extends Controller 'responsabili.*' => 'exists:individui,id', 'luogo_indirizzo' => 'nullable|string|max:500', 'luogo_url_maps' => 'nullable|string', + 'tags' => 'nullable|array', + 'tags.*' => 'exists:tags,id', ]); $evento = Evento::create([ @@ -110,6 +120,10 @@ class EventoController extends Controller 'luogo_url_maps' => $data['luogo_url_maps'] ?? null, ]); + if (!empty($data['tags'])) { + $evento->tags()->attach($data['tags']); + } + if (!empty($data['gruppi'])) { $evento->gruppi()->attach($data['gruppi']); } @@ -124,19 +138,21 @@ class EventoController extends Controller public function show($evento) { $this->authorizeRead('eventi'); - $evento = Evento::with(['gruppi', 'responsabili.contatti', 'documenti'])->findOrFail($evento); + $evento = Evento::with(['gruppi', 'responsabili.contatti', 'documenti', 'tags'])->findOrFail($evento); return view('eventi.show', compact('evento')); } public function edit($evento) { $this->authorizeWrite('eventi'); - $evento = Evento::with(['gruppi', 'responsabili', 'documenti'])->findOrFail($evento); + $evento = Evento::with(['gruppi', 'responsabili', 'documenti', 'tags'])->findOrFail($evento); $gruppi = Gruppo::orderBy('nome')->get(); $individui = Individuo::orderBy('cognome')->orderBy('nome')->get(); $tipologieEventi = TipologiaEvento::attive(); + $tags = Tag::orderBy('name')->get(); + $selectedTags = $evento->tags->pluck('id')->toArray(); - return view('eventi.edit', compact('evento', 'gruppi', 'individui', 'tipologieEventi')); + return view('eventi.edit', compact('evento', 'gruppi', 'individui', 'tipologieEventi', 'tags', 'selectedTags')); } public function update(Request $request, $evento) @@ -167,6 +183,8 @@ class EventoController extends Controller 'responsabili.*' => 'exists:individui,id', 'luogo_indirizzo' => 'nullable|string|max:500', 'luogo_url_maps' => 'nullable|string', + 'tags' => 'nullable|array', + 'tags.*' => 'exists:tags,id', ]); $evento->update([ @@ -189,6 +207,7 @@ class EventoController extends Controller 'luogo_url_maps' => $data['luogo_url_maps'] ?? null, ]); + $evento->tags()->sync($data['tags'] ?? []); $evento->gruppi()->sync($data['gruppi'] ?? []); $evento->responsabili()->sync($data['responsabili'] ?? []); diff --git a/app/Http/Controllers/GruppoController.php b/app/Http/Controllers/GruppoController.php index c6af63ec..4a9ffcbd 100644 --- a/app/Http/Controllers/GruppoController.php +++ b/app/Http/Controllers/GruppoController.php @@ -7,6 +7,7 @@ namespace App\Http\Controllers; use App\Models\Gruppo; use App\Models\Individuo; use App\Models\Diocesi; +use App\Models\Tag; use Illuminate\Http\Request; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\Paginator; @@ -27,8 +28,8 @@ class GruppoController extends Controller ->where('is_default', true) ->first(); - $allColumns = ['nome', 'descrizione', 'diocesi', 'livello', 'padre', 'membri', 'responsabili', 'indirizzo', 'citta', 'telefono', 'email']; - $defaultVisible = ['nome', 'livello', 'padre', 'membri', 'responsabili']; + $allColumns = ['nome', 'descrizione', 'diocesi', 'livello', 'padre', 'membri', 'responsabili', 'indirizzo', 'citta', 'telefono', 'email', 'tag']; + $defaultVisible = ['nome', 'livello', 'padre', 'membri', 'responsabili', 'tag']; if ($vista && !empty($vista->colonne_visibili)) { $visibleColumns = $vista->colonne_visibili; @@ -36,9 +37,14 @@ class GruppoController extends Controller $visibleColumns = $defaultVisible; } - $allGruppi = Gruppo::with(['diocesi', 'parent', 'children', 'individui', 'avatar']) - ->orderBy('nome') - ->get() + $gruppiQuery = Gruppo::with(['diocesi', 'parent', 'children', 'individui', 'avatar', 'tags']); + + if (request()->filled('tag')) { + $tagSlugs = (array) request()->input('tag'); + $gruppiQuery->withAnyTags($tagSlugs); + } + + $allGruppi = $gruppiQuery->orderBy('nome')->get() ->map(function ($gruppo) { $depth = 0; $parent = $gruppo->parent; @@ -65,12 +71,15 @@ class GruppoController extends Controller $gruppi = $allGruppi; } + $allTags = Tag::orderBy('name')->get(); + return view('gruppi.index', compact( 'gruppi', 'allColumns', 'visibleColumns', 'viewMode', - 'vista' + 'vista', + 'allTags' )); } @@ -104,7 +113,8 @@ class GruppoController extends Controller $allGruppi = Gruppo::orderBy('nome')->get(); $selectedParent = $request->query('parent_id') ? Gruppo::find($request->query('parent_id')) : null; $individui = Individuo::orderBy('cognome')->orderBy('nome')->get(); - return view('gruppi.create', compact('diocesi', 'allGruppi', 'selectedParent', 'individui')); + $tags = Tag::orderBy('name')->get(); + return view('gruppi.create', compact('diocesi', 'allGruppi', 'selectedParent', 'individui', 'tags')); } public function store(Request $request) @@ -122,6 +132,8 @@ class GruppoController extends Controller 'città_incontro' => 'nullable|string|max:255', 'sigla_provincia_incontro' => 'nullable|string|max:2', 'mappa_posizione' => 'nullable|string', + 'tags' => 'nullable|array', + 'tags.*' => 'exists:tags,id', ]); if (!empty($data['responsabile_ids'])) { @@ -130,6 +142,10 @@ class GruppoController extends Controller $gruppo = Gruppo::create($data); + if ($request->has('tags')) { + $gruppo->tags()->sync($request->tags); + } + if ($request->has('individui')) { foreach ($request->individui as $individuoData) { if (!empty($individuoData['individuo_id'])) { @@ -149,7 +165,7 @@ class GruppoController extends Controller $this->authorizeRead('gruppi'); $gruppo = Gruppo::with(['parent', 'children', 'diocesi', 'individui.contatti', 'individui.documenti', 'individui' => function ($q) { $q->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione'); - }, 'avatar', 'eventi' => function ($q) { + }, 'avatar', 'tags', 'eventi' => function ($q) { $q->where('is_incontro_gruppo', true); }, 'eventi.responsabili.contatti'])->findOrFail($id); return view('gruppi.show', compact('gruppo')); @@ -158,11 +174,13 @@ class GruppoController extends Controller public function edit($id) { $this->authorizeWrite('gruppi'); - $gruppo = Gruppo::with(['individui.contatti', 'individui.documenti', 'avatar'])->findOrFail($id); + $gruppo = Gruppo::with(['individui.contatti', 'individui.documenti', 'avatar', 'tags'])->findOrFail($id); $diocesi = Diocesi::orderBy('nome')->get(); $gruppi = Gruppo::where('id', '!=', $gruppo->id)->orderBy('nome')->get(); $membri = $gruppo->individui()->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione')->orderBy('cognome')->orderBy('nome')->get(); - return view('gruppi.edit', compact('gruppo', 'diocesi', 'gruppi', 'membri')); + $tags = Tag::orderBy('name')->get(); + $selectedTags = $gruppo->tags->pluck('id')->toArray(); + return view('gruppi.edit', compact('gruppo', 'diocesi', 'gruppi', 'membri', 'tags', 'selectedTags')); } public function update(Request $request, $id) @@ -181,6 +199,8 @@ class GruppoController extends Controller 'città_incontro' => 'nullable|string|max:255', 'sigla_provincia_incontro' => 'nullable|string|max:2', 'mappa_posizione' => 'nullable|string', + 'tags' => 'nullable|array', + 'tags.*' => 'exists:tags,id', ]); if (!empty($data['responsabile_ids'])) { @@ -191,6 +211,12 @@ class GruppoController extends Controller $gruppo->update($data); + if ($request->has('tags')) { + $gruppo->tags()->sync($request->tags); + } else { + $gruppo->tags()->detach(); + } + if ($request->has('individui')) { $gruppo->individui()->detach(); foreach ($request->individui as $individuoData) { @@ -410,4 +436,59 @@ class GruppoController extends Controller 'Content-Disposition' => 'attachment; filename="template_gruppi.csv"', ]); } + + public function massTag(Request $request) + { + $this->authorizeWrite('gruppi'); + $idsInput = $request->input('ids', ''); + $ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []); + + if (empty($ids)) { + return redirect()->route('gruppi.index')->with('error', 'Nessun gruppo selezionato.'); + } + + $data = $request->validate([ + 'tags' => 'required|array', + 'tags.*' => 'exists:tags,id', + 'mode' => 'required|in:assign,remove', + ]); + + $tagIds = $data['tags']; + $mode = $data['mode']; + $count = 0; + + Gruppo::whereIn('id', $ids)->chunk(100, function ($gruppi) use ($tagIds, $mode, &$count) { + foreach ($gruppi as $gruppo) { + if ($mode === 'assign') { + $gruppo->tags()->syncWithoutDetaching($tagIds); + } else { + $gruppo->tags()->detach($tagIds); + } + $count++; + } + }); + + $actionLabel = $mode === 'assign' ? 'assegnati' : 'rimossi'; + return redirect()->route('gruppi.index')->with('success', "Tag {$actionLabel} per {$count} gruppi."); + } + + public function massElimina(Request $request) + { + $this->authorizeDelete('gruppi'); + $ids = $request->input('ids', []); + if (!is_array($ids) || empty($ids)) { + return redirect()->route('gruppi.index')->with('error', 'Nessun gruppo selezionato.'); + } + + $gruppi = Gruppo::whereIn('id', $ids)->get(); + $count = 0; + + foreach ($gruppi as $gruppo) { + $gruppo->individui()->detach(); + $gruppo->delete(); + $count++; + } + + return redirect()->route('gruppi.index')->with('success', "{$count} gruppi eliminati con successo."); + } } diff --git a/app/Http/Controllers/ImpostazioniController.php b/app/Http/Controllers/ImpostazioniController.php index 02f6f01c..3be5bf28 100644 --- a/app/Http/Controllers/ImpostazioniController.php +++ b/app/Http/Controllers/ImpostazioniController.php @@ -9,6 +9,7 @@ use App\Models\EmailSetting; use App\Models\Ruolo; use App\Models\SenderAccount; use App\Models\StorageRepository; +use App\Models\Tag; use App\Models\TipologiaDocumento; use App\Models\TipologiaEvento; use Illuminate\Http\RedirectResponse; @@ -26,6 +27,7 @@ class ImpostazioniController extends Controller $tipologie = TipologiaDocumento::orderBy('ordine')->get(); $tipologieEventi = TipologiaEvento::orderBy('ordine')->get(); $ruoli = Ruolo::orderBy('ordine')->get(); + $tags = Tag::orderBy('order_column')->get(); $appSettings = AppSetting::first(); $emailSettings = EmailSetting::first() ?? new EmailSetting(); $senderAccounts = SenderAccount::orderBy('email_address')->get(); @@ -33,7 +35,7 @@ class ImpostazioniController extends Controller $googleDriveNewToken = session('google_drive_new_token'); $calendarioConnessioni = CalendarioConnessione::orderBy('ordine')->get(); - return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'appSettings', 'emailSettings', 'senderAccounts', 'repositories', 'googleDriveNewToken', 'calendarioConnessioni')); + return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'tags', 'appSettings', 'emailSettings', 'senderAccounts', 'repositories', 'googleDriveNewToken', 'calendarioConnessioni')); } public function saveAppSettings(Request $request) @@ -371,6 +373,70 @@ class ImpostazioniController extends Controller return response()->json(['success' => true]); } + public function tagStore(Request $request) + { + $this->authorizeWrite('settings'); + + $validated = $request->validate([ + 'name' => 'required|string|max:100|unique:tags,name', + 'color' => 'nullable|string|max:7', + ]); + + $maxOrdine = Tag::max('order_column') ?? 0; + + Tag::create([ + 'name' => $validated['name'], + 'slug' => str()->slug($validated['name']), + 'color' => $validated['color'] ?? null, + 'order_column' => $maxOrdine + 1, + ]); + + return redirect('/impostazioni#tag')->with('success', 'Tag aggiunto.'); + } + + public function tagUpdate(Request $request, $id) + { + $this->authorizeWrite('settings'); + + $tag = Tag::findOrFail($id); + + $validated = $request->validate([ + 'name' => 'required|string|max:100|unique:tags,name,' . $id, + 'color' => 'nullable|string|max:7', + ]); + + $tag->update([ + 'name' => $validated['name'], + 'slug' => str()->slug($validated['name']), + 'color' => $validated['color'] ?? null, + ]); + + return redirect('/impostazioni#tag')->with('success', 'Tag aggiornato.'); + } + + public function tagDestroy($id) + { + $this->authorizeDelete('settings'); + + $tag = Tag::findOrFail($id); + $tag->delete(); + + return redirect('/impostazioni#tag')->with('success', 'Tag eliminato.'); + } + + public function tagReorder(Request $request) + { + $this->authorizeWrite('settings'); + + $order = $request->input('order', []); + + foreach ($order as $index => $id) { + Tag::where('id', $id)->update(['order_column' => $index]); + } + + return response()->json(['success' => true]); + } + public function uploadLogo(Request $request) { $this->authorizeWrite('settings'); diff --git a/app/Http/Controllers/IndividuoController.php b/app/Http/Controllers/IndividuoController.php index b0021589..98f40f30 100644 --- a/app/Http/Controllers/IndividuoController.php +++ b/app/Http/Controllers/IndividuoController.php @@ -5,8 +5,10 @@ namespace App\Http\Controllers; use App\Models\Individuo; use App\Models\Contatto; use App\Models\Documento; +use App\Models\Gruppo; use App\Models\Comune; use App\Models\Diocesi; +use App\Models\Tag; use Illuminate\Http\Request; use Illuminate\Support\Facades\Storage; use Illuminate\Validation\Rule; @@ -18,7 +20,16 @@ class IndividuoController extends Controller { $this->authorizeRead('individui'); $perPage = in_array((int) $request->perPage, [10, 20, 25, 50, 100]) ? (int) $request->perPage : 20; - $individui = Individuo::with('contatti')->orderBy('cognome')->orderBy('nome')->paginate($perPage); + + $query = Individuo::with('contatti', 'tags'); + + if ($request->filled('tag')) { + $tagSlugs = (array) $request->input('tag'); + $query->withAnyTags($tagSlugs); + } + + $individui = $query->orderBy('cognome')->orderBy('nome')->paginate($perPage); + $allTags = Tag::orderBy('name')->get(); $vista = null; @@ -41,14 +52,16 @@ class IndividuoController extends Controller ['key' => 'nome', 'label' => 'Nome'], ['key' => 'email', 'label' => 'Email'], ['key' => 'telefono', 'label' => 'Telefono'], + ['key' => 'tag', 'label' => 'Tag'], ]; - $defaultVisible = ['codice', 'cognome', 'nome', 'email', 'telefono']; + $defaultVisible = ['codice', 'cognome', 'nome', 'email', 'telefono', 'tag']; $visibleColumns = $vista && !empty($vista->colonne_visibili) ? $vista->colonne_visibili : $defaultVisible; - return view('individui.index', compact('individui', 'vista', 'allColumns', 'visibleColumns')); + $gruppi = Gruppo::orderBy('nome')->get(['id', 'nome']); + return view('individui.index', compact('individui', 'vista', 'allColumns', 'visibleColumns', 'allTags', 'gruppi')); } public function exportCSV(Request $request) @@ -110,7 +123,8 @@ public function create() $this->authorizeWrite('individui'); $comuni = Comune::orderBy('nome')->get(); $diocesi = Diocesi::orderBy('nome')->get(); - return view('individui.create', compact('comuni', 'diocesi')); + $tags = Tag::orderBy('name')->get(); + return view('individui.create', compact('comuni', 'diocesi', 'tags')); } public function store(Request $request) @@ -129,10 +143,16 @@ public function create() 'numero_documento' => 'nullable|string|max:50', 'scadenza_documento' => 'nullable|date', 'note' => 'nullable|string', + 'tags' => 'nullable|array', + 'tags.*' => 'exists:tags,id', ]); $individuo = Individuo::create($data); + if ($request->has('tags')) { + $individuo->tags()->sync($request->tags); + } + if ($request->has('contatti')) { foreach ($request->contatti as $contatto) { if (!empty($contatto['tipo']) && !empty($contatto['valore'])) { @@ -154,7 +174,7 @@ public function create() $this->authorizeRead('individui'); $individuo = Individuo::with(['contatti', 'gruppi' => function ($q) { $q->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione'); - }, 'avatar'])->findOrFail($individuo); + }, 'avatar', 'tags'])->findOrFail($individuo); return view('individui.show', compact('individuo')); } @@ -180,8 +200,10 @@ public function create() public function edit($individuo) { $this->authorizeWrite('individui'); - $individuo = Individuo::with(['contatti', 'gruppi', 'avatar'])->findOrFail($individuo); - return view('individui.edit', compact('individuo')); + $individuo = Individuo::with(['contatti', 'gruppi', 'avatar', 'tags'])->findOrFail($individuo); + $tags = Tag::orderBy('name')->get(); + $selectedTags = $individuo->tags->pluck('id')->toArray(); + return view('individui.edit', compact('individuo', 'tags', 'selectedTags')); } public function update(Request $request, $individuo) @@ -205,9 +227,17 @@ public function create() 'numero_documento' => 'nullable|string|max:50', 'scadenza_documento' => 'nullable|date', 'note' => 'nullable|string', + 'tags' => 'nullable|array', + 'tags.*' => 'exists:tags,id', ]); $individuo->update($data); + + if ($request->has('tags')) { + $individuo->tags()->sync($request->tags); + } else { + $individuo->tags()->detach(); + } \Log::info('Individuo dati salvati', ['id' => $individuo->id]); @@ -308,6 +338,75 @@ public function create() return redirect()->route('individui.index')->with('success', count($individui) . ' individui eliminati con successo.'); } + public function massGruppo(Request $request) + { + $this->authorizeWrite('individui'); + $idsInput = $request->input('ids', ''); + $ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []); + if (empty($ids)) { + return redirect()->route('individui.index')->with('error', 'Nessun individuo selezionato.'); + } + + $data = $request->validate([ + 'gruppi' => 'required|array', + 'gruppi.*' => 'exists:gruppi,id', + 'mode' => 'required|in:assign,remove', + ]); + + $gruppiIds = $data['gruppi']; + $mode = $data['mode']; + $count = 0; + + foreach ($ids as $id) { + $individuo = Individuo::find($id); + if (!$individuo) continue; + + if ($mode === 'assign') { + $individuo->gruppi()->syncWithoutDetaching($gruppiIds); + } else { + $individuo->gruppi()->detach($gruppiIds); + } + $count++; + } + + $actionLabel = $mode === 'assign' ? 'assegnati' : 'rimossi'; + return redirect()->route('individui.index')->with('success', "Gruppi {$actionLabel} per {$count} individui."); + } + + public function massTag(Request $request) + { + $this->authorizeWrite('individui'); + $idsInput = $request->input('ids', ''); + $ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []); + if (empty($ids)) { + return redirect()->route('individui.index')->with('error', 'Nessun individuo selezionato.'); + } + + $data = $request->validate([ + 'tags' => 'required|array', + 'tags.*' => 'exists:tags,id', + 'mode' => 'required|in:assign,remove', + ]); + + $tagIds = $data['tags']; + $mode = $data['mode']; + $count = 0; + + Individuo::whereIn('id', $ids)->chunk(100, function ($individui) use ($tagIds, $mode, &$count) { + foreach ($individui as $individuo) { + if ($mode === 'assign') { + $individuo->tags()->syncWithoutDetaching($tagIds); + } else { + $individuo->tags()->detach($tagIds); + } + $count++; + } + }); + + $actionLabel = $mode === 'assign' ? 'assegnati' : 'rimossi'; + return redirect()->route('individui.index')->with('success', "Tag {$actionLabel} per {$count} individui."); + } + public function elementiCollegati($individuo) { $individuo = Individuo::findOrFail($individuo); diff --git a/app/Http/Controllers/MailingController.php b/app/Http/Controllers/MailingController.php index 4773e812..d9951aba 100644 --- a/app/Http/Controllers/MailingController.php +++ b/app/Http/Controllers/MailingController.php @@ -95,9 +95,17 @@ class MailingController extends Controller } } - $emails = $destinatari->flatMap(fn($ind) => $ind->contatti->where('tipo', 'email')->pluck('valore')) + $totaleDestinatari = $destinatari->count(); + $emails = $destinatari->map(fn($ind) => $ind->contatti->where('tipo', 'email')->first()?->valore) ->filter()->unique()->values(); + $skipped = $totaleDestinatari - $emails->count(); + if ($skipped > 0) { + Log::info("Mailing invia: {$skipped} contatti saltati perché senza email", [ + 'lista_id' => $data['lista_id'] ?? null, + ]); + } + if ($emails->isEmpty()) { return back()->with('error', 'Nessun destinatario email valido trovato.')->withInput(); } @@ -229,9 +237,17 @@ class MailingController extends Controller ->with('individuo.contatti') ->get(); + $totaleContatti = $contatti->count(); $emails = $contatti->map(fn($c) => $c->individuo?->contatti->where('tipo', 'email')->first()?->valore) ->filter()->unique()->values(); + $skipped = $totaleContatti - $emails->count(); + if ($skipped > 0) { + Log::info("Mailing invio elabora: {$skipped} contatti saltati perché senza email", [ + 'liste_ids' => $data['liste'], + ]); + } + if ($emails->isEmpty()) { return back()->with('error', 'Nessun destinatario email valido trovato nelle liste selezionate.'); } diff --git a/app/Http/Controllers/MailingListController.php b/app/Http/Controllers/MailingListController.php index 78ef469e..4cd77458 100644 --- a/app/Http/Controllers/MailingListController.php +++ b/app/Http/Controllers/MailingListController.php @@ -3,23 +3,31 @@ namespace App\Http\Controllers; use App\Models\MailingList; +use App\Models\Tag; use Illuminate\Http\Request; class MailingListController extends Controller { - public function index() + public function index(Request $request) { $this->authorizeRead('mailing'); - $mailingLists = MailingList::with(['user', 'contatti']) - ->orderBy('nome') - ->get(); - return view('mailing-liste.index', compact('mailingLists')); + $query = MailingList::with(['user', 'contatti', 'tags']); + + if ($request->filled('tag')) { + $tagSlugs = (array) $request->input('tag'); + $query->withAnyTags($tagSlugs); + } + + $mailingLists = $query->orderBy('nome')->get(); + $allTags = Tag::orderBy('name')->get(); + return view('mailing-liste.index', compact('mailingLists', 'allTags')); } public function create() { $this->authorizeWrite('mailing'); - return view('mailing-liste.create'); + $tags = Tag::orderBy('name')->get(); + return view('mailing-liste.create', compact('tags')); } public function store(Request $request) @@ -30,6 +38,8 @@ class MailingListController extends Controller 'descrizione' => 'nullable|string', 'attiva' => 'boolean', 'contatti_json' => 'nullable|string', + 'tags' => 'nullable|array', + 'tags.*' => 'exists:tags,id', ]); $data['user_id'] = auth()->id(); @@ -42,11 +52,15 @@ class MailingListController extends Controller 'user_id' => $data['user_id'], ]); + if ($request->has('tags')) { + $lista->tags()->sync($request->tags); + } + if (!empty($data['contatti_json'])) { $contatti = json_decode($data['contatti_json'], true); if (is_array($contatti)) { foreach ($contatti as $contatto) { - if (!empty($contatto['individuo_id']) && !empty($contatto['email'])) { + if (!empty($contatto['individuo_id'])) { $lista->contatti()->create([ 'individuo_id' => $contatto['individuo_id'], 'opt_in' => true, @@ -63,15 +77,17 @@ class MailingListController extends Controller public function show($mailing_liste) { $this->authorizeRead('mailing'); - $mailingList = MailingList::with(['contatti.individuo.contatti'])->findOrFail($mailing_liste); + $mailingList = MailingList::with(['contatti.individuo.contatti', 'tags'])->findOrFail($mailing_liste); return view('mailing-liste.show', compact('mailingList')); } public function edit($mailingList) { $this->authorizeWrite('mailing'); - $mailingList = MailingList::with('contatti.individuo.contatti')->findOrFail($mailingList); - return view('mailing-liste.edit', compact('mailingList')); + $mailingList = MailingList::with(['contatti.individuo.contatti', 'tags'])->findOrFail($mailingList); + $tags = Tag::orderBy('name')->get(); + $selectedTags = $mailingList->tags->pluck('id')->toArray(); + return view('mailing-liste.edit', compact('mailingList', 'tags', 'selectedTags')); } public function update(Request $request, $mailingList) @@ -84,6 +100,8 @@ class MailingListController extends Controller 'descrizione' => 'nullable|string', 'attiva' => 'boolean', 'contatti_json' => 'nullable|string', + 'tags' => 'nullable|array', + 'tags.*' => 'exists:tags,id', ]); $data['attiva'] = $data['attiva'] ?? false; @@ -94,6 +112,12 @@ class MailingListController extends Controller 'attiva' => $data['attiva'], ]); + if ($request->has('tags')) { + $mailingList->tags()->sync($request->tags); + } else { + $mailingList->tags()->detach(); + } + if (!empty($data['contatti_json'])) { $newContatti = json_decode($data['contatti_json'], true); if (is_array($newContatti)) { @@ -107,7 +131,7 @@ class MailingListController extends Controller $toAdd = array_diff($newIds, $existingIds); foreach ($newContatti as $contatto) { - if (in_array($contatto['individuo_id'], $toAdd) && !empty($contatto['email'])) { + if (in_array($contatto['individuo_id'], $toAdd)) { $mailingList->contatti()->create([ 'individuo_id' => $contatto['individuo_id'], 'opt_in' => true, diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index 09a382ff..d163d39d 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -11,6 +11,7 @@ use App\Models\Evento; use App\Models\Documento; use App\Models\MailingList; use App\Models\Contatto; +use App\Models\Tag; use App\Models\TipologiaDocumento; use App\Models\ReportCustom; use Illuminate\Http\Request; @@ -47,12 +48,15 @@ class ReportController extends Controller $columnOptions[$type] = ReportColumnRegistry::getColumns($type); } + $allTags = Tag::orderBy('name')->get(); + return view('report.index', compact( 'stats', 'prebuiltReports', 'customReports', 'tipologieDocumento', - 'columnOptions' + 'columnOptions', + 'allTags' )); } @@ -136,6 +140,14 @@ class ReportController extends Controller $config['search_columns'] = $request->input('search_columns'); } + if ($request->has('tag_filter')) { + $config['tag_filter'] = $request->input('tag_filter'); + } + + if ($request->filled('tag_filter_mode')) { + $config['tag_filter_mode'] = $request->input('tag_filter_mode'); + } + ReportCustom::create([ 'user_id' => auth()->id(), 'nome' => $data['nome'], @@ -252,6 +264,9 @@ class ReportController extends Controller 'documenti_completo' => $this->reportDocumentiCompleto($tipologiaFilter), 'contatti_completo' => $this->reportContattiCompleto(), 'scadenze_documenti' => $this->reportScadenzeDocumenti($tipologiaFilter), + 'tag_statistiche' => $this->reportTagStatistiche(), + 'individui_per_tag' => $this->reportIndividuiPerTag(), + 'documenti_per_tag' => $this->reportDocumentiPerTag(), default => null, }; } @@ -403,6 +418,30 @@ class ReportController extends Controller 'colore' => 'teal', 'has_tipologia_filter' => false, ], + [ + 'id' => 'tag_statistiche', + 'nome' => 'Statistiche Tag', + 'descrizione' => 'Distribuzione degli elementi per tag (individui, gruppi, eventi, documenti, mailing list)', + 'icona' => 'fa-tags', + 'colore' => 'indigo', + 'has_tipologia_filter' => false, + ], + [ + 'id' => 'individui_per_tag', + 'nome' => 'Individui per Tag', + 'descrizione' => 'Elenco completo degli individui raggruppati per tag assegnato', + 'icona' => 'fa-user-tag', + 'colore' => 'info', + 'has_tipologia_filter' => false, + ], + [ + 'id' => 'documenti_per_tag', + 'nome' => 'Documenti per Tag', + 'descrizione' => 'Elenco completo dei documenti raggruppati per tag assegnato', + 'icona' => 'fa-tags', + 'colore' => 'secondary', + 'has_tipologia_filter' => false, + ], ]; } @@ -927,6 +966,102 @@ class ReportController extends Controller ]; } + protected function reportTagStatistiche(): array + { + $tags = Tag::orderBy('name')->get(); + + $rows = $tags->map(function ($tag) { + return [ + 'name' => $tag->name, + 'color' => $tag->color ?? '#6c757d', + 'individui' => $tag->individui()->count(), + 'gruppi' => $tag->gruppi()->count(), + 'eventi' => $tag->eventi()->count(), + 'documenti' => $tag->documenti()->count(), + 'mailing' => $tag->mailingLists()->count(), + 'totale' => $tag->count, + ]; + })->toArray(); + + $totalIndividui = array_sum(array_column($rows, 'individui')); + $totalGruppi = array_sum(array_column($rows, 'gruppi')); + $totalEventi = array_sum(array_column($rows, 'eventi')); + $totalDocumenti = array_sum(array_column($rows, 'documenti')); + $totalMailing = array_sum(array_column($rows, 'mailing')); + + return [ + 'title' => 'Statistiche Tag', + 'headers' => ['Tag', 'Individui', 'Gruppi', 'Eventi', 'Documenti', 'Mailing List', 'Totale Elementi'], + 'rows' => array_map(fn($r) => [$r['name'], $r['individui'], $r['gruppi'], $r['eventi'], $r['documenti'], $r['mailing'], $r['totale']], $rows), + 'summary' => 'Tag: ' . count($tags) . ' | Individui: ' . $totalIndividui . ' | Gruppi: ' . $totalGruppi . ' | Eventi: ' . $totalEventi . ' | Documenti: ' . $totalDocumenti . ' | Mailing List: ' . $totalMailing, + 'detail_rows' => $rows, + ]; + } + + protected function reportIndividuiPerTag(): array + { + $individui = Individuo::with('tags') + ->whereHas('tags') + ->orderBy('cognome') + ->orderBy('nome') + ->get(); + + $rows = $individui->flatMap(function ($ind) { + if ($ind->tags->isEmpty()) { + return collect(); + } + return $ind->tags->map(function ($tag) use ($ind) { + return [ + 'tag' => $tag->name, + 'tag_color' => $tag->color ?? '#6c757d', + 'codice' => $ind->codice_id, + 'cognome' => $ind->cognome, + 'nome' => $ind->nome, + 'email' => $ind->email_primaria ?? '-', + ]; + }); + })->toArray(); + + return [ + 'title' => 'Individui per Tag', + 'headers' => ['Tag', 'Codice', 'Cognome', 'Nome', 'Email'], + 'rows' => array_map(fn($r) => [$r['tag'], $r['codice'], $r['cognome'], $r['nome'], $r['email']], $rows), + 'summary' => 'Totale righe (individuo × tag): ' . count($rows), + 'detail_rows' => $rows, + ]; + } + + protected function reportDocumentiPerTag(): array + { + $documenti = Documento::with('tags') + ->whereHas('tags') + ->orderBy('nome_file') + ->get(); + + $rows = $documenti->flatMap(function ($doc) { + if ($doc->tags->isEmpty()) { + return collect(); + } + return $doc->tags->map(function ($tag) use ($doc) { + return [ + 'tag' => $tag->name, + 'tag_color' => $tag->color ?? '#6c757d', + 'nome_file' => $doc->nome_file, + 'tipologia' => ucfirst((string) $doc->tipologia), + 'data' => $doc->created_at->format('d/m/Y'), + ]; + }); + })->toArray(); + + return [ + 'title' => 'Documenti per Tag', + 'headers' => ['Tag', 'Nome File', 'Tipologia', 'Data Caricamento'], + 'rows' => array_map(fn($r) => [$r['tag'], $r['nome_file'], $r['tipologia'], $r['data']], $rows), + 'summary' => 'Totale righe (documento × tag): ' . count($rows), + 'detail_rows' => $rows, + ]; + } + protected function runCustomReport(ReportCustom $report): array { $tipo = $report->tipo_report; @@ -954,11 +1089,32 @@ class ReportController extends Controller $columns = $config['columns'] ?? []; $hasRelations = false; + // Always load tags if tags column or tag filter is present + $needsTags = in_array('tags.nome', $columns) || !empty($config['tag_filter']); + if ($needsTags) { + $query->with('tags'); + } + foreach ($columns as $col) { if (str_contains($col, '.')) { $hasRelations = true; $relation = explode('.', $col)[0]; - $query->with($relation); + if ($relation !== 'tags') { + $query->with($relation); + } + } + } + + // Apply tag filter + if (!empty($config['tag_filter'])) { + $tagSlugs = Tag::whereIn('id', (array) $config['tag_filter'])->pluck('slug')->toArray(); + if (!empty($tagSlugs)) { + $mode = $config['tag_filter_mode'] ?? 'any'; + if ($mode === 'all') { + $query->withAllTags($tagSlugs); + } else { + $query->withAnyTags($tagSlugs); + } } } diff --git a/app/Models/Documento.php b/app/Models/Documento.php index 0c53d0d4..0863c820 100644 --- a/app/Models/Documento.php +++ b/app/Models/Documento.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Traits\HasTagsLight; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphTo; @@ -9,6 +10,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Documento extends Model { + use HasTagsLight; protected $table = 'documenti'; protected $fillable = [ 'tenant_id', 'user_id', 'cartella_id', 'repository_id', 'storage_disk', diff --git a/app/Models/Evento.php b/app/Models/Evento.php index 21e64340..24e00cb6 100644 --- a/app/Models/Evento.php +++ b/app/Models/Evento.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Traits\HasTagsLight; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; @@ -9,6 +10,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; class Evento extends Model { + use HasTagsLight; protected $table = 'eventi'; protected $fillable = [ diff --git a/app/Models/Gruppo.php b/app/Models/Gruppo.php index 2470270d..8506c7a2 100644 --- a/app/Models/Gruppo.php +++ b/app/Models/Gruppo.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Traits\HasTagsLight; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -10,6 +11,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Gruppo extends Model { + use HasTagsLight; protected $table = 'gruppi'; protected $fillable = [ 'tenant_id', 'parent_id', 'diocesi_id', 'responsabile_ids', diff --git a/app/Models/Individuo.php b/app/Models/Individuo.php index ace911a9..f83921aa 100644 --- a/app/Models/Individuo.php +++ b/app/Models/Individuo.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Traits\HasTagsLight; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -10,6 +11,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne; class Individuo extends Model { + use HasTagsLight; protected $table = 'individui'; protected $fillable = [ 'tenant_id', 'codice_id', 'cognome', 'nome', 'data_nascita', diff --git a/app/Models/MailingList.php b/app/Models/MailingList.php index fc8ed0d9..b3fc6456 100644 --- a/app/Models/MailingList.php +++ b/app/Models/MailingList.php @@ -5,9 +5,12 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use App\Traits\HasTagsLight; class MailingList extends Model { + use HasTagsLight; + protected $table = 'mailing_lists'; protected $fillable = ['tenant_id', 'user_id', 'nome', 'descrizione', 'attiva']; diff --git a/resources/views/documenti/edit.blade.php b/resources/views/documenti/edit.blade.php index 31d1da9c..5ee64d5d 100644 --- a/resources/views/documenti/edit.blade.php +++ b/resources/views/documenti/edit.blade.php @@ -103,6 +103,42 @@ @endif + + @include('partials._tag-selector', ['selectedTags' => $selectedTags ?? [], 'label' => 'Etichette']) + +
+ + + + + + + + + + + + + + + + + + @if($documento->user) + + + + + @endif +
File:{{ $documento->file_path }}
Tipo MIME:{{ $documento->mime_type ?? '-' }}
Dimensione:{{ $documento->dimensione ? number_format($documento->dimensione / 1024, 1) . ' KB' : '-' }}
Creato:{{ $documento->created_at->format('d/m/Y H:i') }}
Utente:{{ $documento->user->name }}
+ @if($documento->file_path) +
+ + Scarica + + @endif +
+ diff --git a/resources/views/documenti/index.blade.php b/resources/views/documenti/index.blade.php index 935399cf..83268fce 100644 --- a/resources/views/documenti/index.blade.php +++ b/resources/views/documenti/index.blade.php @@ -123,6 +123,8 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti'); + @include('partials._tag-filter-bar', ['filterTags' => $allTags]) +
{{-- Toolbar azioni massive --}}
@@ -144,6 +146,9 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti'); + @@ -226,6 +231,11 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti'); @endif
+
+ @foreach($documento->tags as $tag) + {{ $tag->name }} + @endforeach +
@if($documento->user) {{ $documento->user->name }} @@ -295,6 +305,7 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti'); Tipologia Dimensione Contesto + Tag Data Azioni @@ -314,6 +325,7 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti'); - - - + - @@ -423,6 +435,11 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti'); - @endswitch + + @foreach($documento->tags as $tag) + {{ $tag->name }} + @endforeach + {{ $documento->created_at->format('d/m/Y') }} @if($documento->file_path) @@ -834,6 +851,50 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti');
+{{-- MODAL: Gestione Tag massiva --}} + + {{-- MODAL: Sposta documento --}} +
+
+

Tag

+
+
+ @include('partials._tag-selector', ['selectedTags' => [], 'label' => 'Etichette']) +
+
+
+
+
+

Tag

+
+
+ @include('partials._tag-selector', ['selectedTags' => $selectedTags ?? [], 'label' => 'Etichette']) +
+
+
+ @include('partials._tag-filter-bar', ['filterTags' => $allTags]) + +
+

Tag

+
+ @forelse($evento->tags as $tag) + {{ $tag->name }} + @empty +

Nessun tag assegnato.

+ @endforelse +
+
+ @if($evento->note)

Note

diff --git a/resources/views/gruppi/create.blade.php b/resources/views/gruppi/create.blade.php index 03fbead9..118908a7 100644 --- a/resources/views/gruppi/create.blade.php +++ b/resources/views/gruppi/create.blade.php @@ -117,6 +117,15 @@
+
+
+

Tag

+
+
+ @include('partials._tag-selector', ['selectedTags' => [], 'label' => 'Etichette']) +
+
+
+
+
+

Tag

+
+
+ @include('partials._tag-selector', ['selectedTags' => $selectedTags ?? [], 'label' => 'Etichette']) +
+
+
+
@@ -65,11 +82,19 @@ $visibleColumnsJson = json_encode($visibleColumns); + @include('partials._tag-filter-bar', ['filterTags' => $allTags]) + @if($viewMode === 'table')
+ @if(in_array('nome', $visibleColumns)) @endif @@ -103,12 +128,21 @@ $visibleColumnsJson = json_encode($visibleColumns); @if(in_array('email', $visibleColumns)) @endif + @if(in_array('tag', $visibleColumns)) + + @endif @foreach($gruppi as $gruppo) + @if(in_array('nome', $visibleColumns)) @endif + @if(in_array('tag', $visibleColumns)) + + @endif
+
+ + +
+
Nome EmailTagAzioni
+
+ + +
+
@@ -154,6 +188,13 @@ $visibleColumnsJson = json_encode($visibleColumns); @if(in_array('email', $visibleColumns)) - + @foreach($gruppo->tags as $tag) + {{ $tag->name }} + @endforeach + @@ -309,6 +350,68 @@ $visibleColumnsJson = json_encode($visibleColumns); + +{{-- MODAL: Gestione Tag massiva --}} + + +{{-- MODAL: Conferma Eliminazione massiva --}} + + @endsection @section('scripts') @@ -317,6 +420,50 @@ function switchView(mode) { window.location.href = '?view=' + mode; } +function toggleSelectAll(source) { + document.querySelectorAll('.row-checkbox').forEach(function(cb) { + cb.checked = source.checked; + }); +} + +function getSelectedIds() { + return Array.from(document.querySelectorAll('.row-checkbox:checked')).map(cb => cb.value); +} + +function showMassTagModal() { + const ids = getSelectedIds(); + if (ids.length === 0) { + alert('Seleziona almeno un gruppo'); + return; + } + document.getElementById('massTagIds').value = ids.join(','); + document.getElementById('massTagCount').textContent = ids.length; + $('#massTagModal').modal('show'); +} + +function deleteSelected() { + const ids = getSelectedIds(); + if (ids.length === 0) { + alert('Seleziona almeno un gruppo'); + return; + } + document.getElementById('delete-count').textContent = ids.length; + document.getElementById('confirm-delete-btn').onclick = function() { + const form = document.createElement('form'); + form.method = 'POST'; + form.action = '/gruppi/mass-elimina'; + const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '{{ csrf_token() }}'; + let html = ''; + ids.forEach(function(id) { + html += ''; + }); + form.innerHTML = html; + document.body.appendChild(form); + form.submit(); + }; + $('#deleteModal').modal('show'); +} + function toggleItem(id) { const el = document.getElementById('group-' + id); const icon = document.getElementById('icon-' + id); diff --git a/resources/views/gruppi/show.blade.php b/resources/views/gruppi/show.blade.php index 339bde61..99221c8a 100644 --- a/resources/views/gruppi/show.blade.php +++ b/resources/views/gruppi/show.blade.php @@ -247,6 +247,17 @@ $canDeleteGruppi = Auth::user()->canDelete('gruppi'); @endif +
+

Tag

+
+ @forelse($gruppo->tags as $tag) + {{ $tag->name }} + @empty +

Nessun tag assegnato.

+ @endforelse +
+
+
@@ -1068,6 +1071,136 @@ + + {{-- === TAG === --}} +
+
+
+

Tag

+
+
+

Gestisci i tag (etichette) per classificare individui, gruppi, eventi e documenti.

+ + @if(session('success')) +
{{ session('success') }}
+ @endif + @if(session('error')) +
{{ session('error') }}
+ @endif + +
+ +
+ + + + + + + + + + + + + + @foreach($tags as $tag) + + + + + + + + + @endforeach + +
OrdineTagNomeSlugColoreAzioni
+ + + + {{ $tag->name }} + + {{ $tag->name }}{{ $tag->slug }} + + {{ $tag->color ?? '-' }} + + +
+ @csrf + @method('DELETE') + +
+
+
+
+
+ + + + +{{-- ===== MODALI TAG ===== --}} + + + @@ -1618,6 +1751,32 @@ var sortableTipologieEventi = Sortable.create(document.getElementById('tipologie } }); +function editTag(id, name, color) { + $('#editTagName').val(name); + $('#editTagColor').val(color || '#007bff'); + $('#editTagForm').attr('action', '/impostazioni/tag/' + id); + $('#editTagModal').modal('show'); +} + +var sortableTag = Sortable.create(document.getElementById('tag-sortable'), { + handle: '.handle', + animation: 150, + onEnd: function(evt) { + var order = []; + $('#tag-sortable tr').each(function() { + order.push($(this).data('id')); + }); + fetch('{{ route('impostazioni.tag.reorder') }}', { + method: 'POST', + headers: { + 'X-CSRF-TOKEN': '{{ csrf_token() }}', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ order: order }) + }); + } +}); + $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { var target = $(e.target).attr('href'); if (target === '#ruoli') { @@ -1626,6 +1785,9 @@ $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { if (target === '#tipologie-eventi') { sortableTipologieEventi.refresh(); } + if (target === '#tag') { + sortableTag.refresh(); + } if (target === '#calendario') { if (typeof sortableCalendario !== 'undefined') { sortableCalendario.refresh(); diff --git a/resources/views/individui/create.blade.php b/resources/views/individui/create.blade.php index 5c0c898c..2d0fbdc5 100644 --- a/resources/views/individui/create.blade.php +++ b/resources/views/individui/create.blade.php @@ -108,6 +108,15 @@ +
+
+

Tag

+
+
+ @include('partials._tag-selector', ['selectedTags' => [], 'label' => 'Etichette']) +
+
+

Contatti

diff --git a/resources/views/individui/edit.blade.php b/resources/views/individui/edit.blade.php index 4a3df555..7e0b8563 100644 --- a/resources/views/individui/edit.blade.php +++ b/resources/views/individui/edit.blade.php @@ -139,6 +139,16 @@
+ +
+
+

Tag

+
+
+ @include('partials._tag-selector', ['selectedTags' => $selectedTags ?? [], 'label' => 'Etichette']) +
+
+

Contatti

diff --git a/resources/views/individui/index.blade.php b/resources/views/individui/index.blade.php index a789ff6e..c2f8781c 100644 --- a/resources/views/individui/index.blade.php +++ b/resources/views/individui/index.blade.php @@ -9,6 +9,7 @@ $columnLabels = [ 'nome' => 'Nome', 'email' => 'Email', 'telefono' => 'Telefono', + 'tag' => 'Tag', ]; $vistaDefaultJson = $vista ? $vista->toJson() : 'null'; $allColumnsJson = json_encode($allColumns); @@ -47,6 +48,12 @@ $canDeleteIndividui = Auth::user()->canDelete('individui'); Crea Mailing List + + Assegna Gruppo + + + Assegna Tag + Elimina Selezionati @@ -124,6 +131,8 @@ $canDeleteIndividui = Auth::user()->canDelete('individui');
+ @include('partials._tag-filter-bar', ['filterTags' => $allTags]) +
@if($individui->count() > 0) @@ -150,6 +159,9 @@ $canDeleteIndividui = Auth::user()->canDelete('individui'); @if(in_array('telefono', $visibleColumns)) @endif + @if(in_array('tag', $visibleColumns)) + + @endif @@ -181,6 +193,13 @@ $canDeleteIndividui = Auth::user()->canDelete('individui'); @if(in_array('telefono', $visibleColumns)) @endif + @if(in_array('tag', $visibleColumns)) + + @endif
Telefono TagAzioni
{{ $ind->getTelefonoPrimarioAttribute() ?: '-' }} + @foreach($ind->tags as $tag) + {{ $tag->name }} + @endforeach + @if($canWriteIndividui) @@ -415,6 +434,88 @@ $canDeleteIndividui = Auth::user()->canDelete('individui'); +{{-- MODAL: Assegna Gruppo massivo --}} + + +{{-- MODAL: Gestione Tag massiva --}} + + @endsection @section('scripts') @@ -444,7 +545,7 @@ function applyColumnFilter() { if (!col || !val) return; const rows = document.querySelectorAll('#table-body tr'); - const colIndex = { codice: 1, cognome: 2, nome: 3, email: 4, telefono: 5, azioni: 6 }[col]; + const colIndex = { codice: 1, cognome: 2, nome: 3, email: 4, telefono: 5, tag: 6, azioni: 7 }[col]; rows.forEach(row => { const cells = row.querySelectorAll('td'); @@ -476,7 +577,7 @@ function sortTable(column) { currentSort = { column: column, direction: 'asc' }; } - const colIndex = { codice: 0, cognome: 1, nome: 2, email: 3, telefono: 4, azioni: 5 }[column]; + const colIndex = { codice: 1, cognome: 2, nome: 3, email: 4, telefono: 5, tag: 6, azioni: 7 }[column]; const tbody = document.getElementById('table-body'); const rows = Array.from(tbody.querySelectorAll('tr')); @@ -500,7 +601,7 @@ function sortTable(column) { } function toggleColumn(column, visible) { - const colIndex = { codice: 1, cognome: 2, nome: 3, email: 4, telefono: 5, azioni: 6 }[column]; + const colIndex = { codice: 1, cognome: 2, nome: 3, email: 4, telefono: 5, tag: 6, azioni: 7 }[column]; const ths = document.querySelectorAll('#individui-table thead th'); const rows = document.querySelectorAll('#table-body tr'); @@ -890,6 +991,28 @@ function updateMlCheckboxEmail(id, email) { } } +function showMassGruppoModal() { + const ids = getSelectedIds(); + if (ids.length === 0) { + alert('Seleziona almeno un individuo'); + return; + } + document.getElementById('massGruppoIds').value = ids.join(','); + document.getElementById('massGruppoCount').textContent = ids.length; + $('#massGruppoModal').modal('show'); +} + +function showMassTagModal() { + const ids = getSelectedIds(); + if (ids.length === 0) { + alert('Seleziona almeno un individuo'); + return; + } + document.getElementById('massTagIds').value = ids.join(','); + document.getElementById('massTagCount').textContent = ids.length; + $('#massTagModal').modal('show'); +} + async function createMailingList() { const nome = document.getElementById('ml-nome').value.trim(); if (!nome) { diff --git a/resources/views/individui/show.blade.php b/resources/views/individui/show.blade.php index a6709114..be5ee425 100644 --- a/resources/views/individui/show.blade.php +++ b/resources/views/individui/show.blade.php @@ -88,6 +88,21 @@ +
+
+
+

Tag

+
+ @forelse($individuo->tags as $tag) + {{ $tag->name }} + @empty +

Nessun tag assegnato.

+ @endforelse +
+
+
+
+ @if($individuo->note)
+
+ @include('partials._tag-selector', ['allTags' => $tags ?? collect(), 'selectedTags' => old('tags', []), 'label' => 'Tag']) +
Come aggiungere contatti: vai su Individui, seleziona le righe desired, e usa il menu Azioni > Crea Mailing List per creare una lista dai contatti selezionati. diff --git a/resources/views/mailing-liste/edit.blade.php b/resources/views/mailing-liste/edit.blade.php index c11bc27e..9772817e 100644 --- a/resources/views/mailing-liste/edit.blade.php +++ b/resources/views/mailing-liste/edit.blade.php @@ -31,6 +31,8 @@
+ @include('partials._tag-selector', ['allTags' => $tags ?? collect(), 'selectedTags' => $selectedTags ?? [], 'label' => 'Tag']) +
@@ -64,6 +66,13 @@
+ +
@@ -185,11 +194,17 @@ document.addEventListener('DOMContentLoaded', function() { console.error('Errore individui:', err); }); - contattiCaricati = Array.from(document.querySelectorAll('.contatto-checkbox')).map(cb => ({ - individuo_id: parseInt(cb.value), - email: cb.dataset.email, - existing: true - })); + contattiCaricati = Array.from(document.querySelectorAll('.contatto-checkbox')).map(cb => { + const row = cb.closest('tr'); + return { + individuo_id: parseInt(cb.value), + email: cb.dataset.email || '', + existing: true, + codice_id: row ? (row.querySelector('td:nth-child(2)')?.textContent?.trim() || '') : '', + cognome: row ? (row.querySelector('td:nth-child(3)')?.textContent?.trim() || '') : '', + nome: row ? (row.querySelector('td:nth-child(4)')?.textContent?.trim() || '') : '' + }; + }); }); const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || ''; @@ -217,17 +232,15 @@ function caricaSelezionati() { }) .then(data => { data.forEach(function(ind) { - if (ind.emails && ind.emails.length > 0) { - if (!contattiCaricati.find(c => c.individuo_id === ind.id)) { - contattiCaricati.push({ - individuo_id: ind.id, - codice_id: ind.codice_id, - cognome: ind.cogname, - nome: ind.nome, - email: ind.emails[0], - origini: ['Gruppo'] - }); - } + if (!contattiCaricati.find(c => c.individuo_id === ind.id)) { + contattiCaricati.push({ + individuo_id: ind.id, + codice_id: ind.codice_id, + cognome: ind.cognome, + nome: ind.nome, + email: (ind.emails && ind.emails.length > 0) ? ind.emails[0] : '', + origini: ['Gruppo'] + }); } }); }) @@ -246,17 +259,15 @@ function caricaSelezionati() { }) .then(data => { data.forEach(function(ind) { - if (ind.emails && ind.emails.length > 0) { - if (!contattiCaricati.find(c => c.individuo_id === ind.id)) { - contattiCaricati.push({ - individuo_id: ind.id, - codice_id: ind.codice_id, - cognome: ind.cogname, - nome: ind.nome, - email: ind.emails[0], - origini: [' Individuo'] - }); - } + if (!contattiCaricati.find(c => c.individuo_id === ind.id)) { + contattiCaricati.push({ + individuo_id: ind.id, + codice_id: ind.codice_id, + cognome: ind.cognome, + nome: ind.nome, + email: (ind.emails && ind.emails.length > 0) ? ind.emails[0] : '', + origini: [' Individuo'] + }); } }); }) @@ -327,25 +338,87 @@ function deselezionaTutti() { } document.getElementById('ml-form').addEventListener('submit', function(e) { + e.preventDefault(); + + const form = this; const selected = []; + document.querySelectorAll('.contatto-checkbox:checked').forEach(function(cb) { + const row = cb.closest('tr'); selected.push({ individuo_id: parseInt(cb.value), - email: cb.dataset.email + email: cb.dataset.email || '', + _label: row + ? (row.querySelector('td:nth-child(2)')?.textContent?.trim() || '') + ' - ' + + (row.querySelector('td:nth-child(3)')?.textContent?.trim() || '') + ' ' + + (row.querySelector('td:nth-child(4)')?.textContent?.trim() || '') + : 'ID ' + cb.value }); }); + const indSelect = document.getElementById('individui-select'); + if (indSelect) { + Array.from(indSelect.selectedOptions).forEach(function(opt) { + const id = parseInt(opt.value); + if (!selected.find(function(s) { return s.individuo_id === id; })) { + selected.push({ + individuo_id: id, + email: '', + _label: opt.textContent + }); + } + }); + } + if (selected.length === 0) { - e.preventDefault(); - alert('Seleziona almeno un contatto con email'); + const gruppiSelect = document.getElementById('gruppi-select'); + if (gruppiSelect && gruppiSelect.selectedOptions.length > 0) { + alert('Devi cliccare "Carica contatti" per caricare i membri dei gruppi prima di salvare.'); + } else { + alert('Seleziona almeno un contatto.'); + } return; } - const input = document.createElement('input'); - input.type = 'hidden'; - input.name = 'contatti_json'; - input.value = JSON.stringify(selected); - this.appendChild(input); + function completaInvio() { + const input = document.createElement('input'); + input.type = 'hidden'; + input.name = 'contatti_json'; + input.value = JSON.stringify(selected.map(function(s) { + return { individuo_id: s.individuo_id, email: s.email }; + })); + form.appendChild(input); + form.submit(); + } + + const senzaEmail = selected.filter(function(s) { return !s.email; }); + + if (senzaEmail.length > 0) { + const elencoHtml = '
    ' + + senzaEmail.map(function(s) { + return '
  • ' + s._label + '
  • '; + }).join('') + + '
'; + + Swal.fire({ + title: 'Contatti senza email', + html: '

I seguenti ' + senzaEmail.length + ' contatti non hanno un indirizzo email:

' + + elencoHtml + + '

La email verrà risolta automaticamente quando l\'individuo verrà aggiornato. Salvare comunque?

', + icon: 'warning', + showCancelButton: true, + confirmButtonText: 'Salva lo stesso', + cancelButtonText: 'Annulla', + confirmButtonColor: '#28a745', + cancelButtonColor: '#6c757d' + }).then(function(result) { + if (result.isConfirmed) { + completaInvio(); + } + }); + } else { + completaInvio(); + } }); @endsection \ No newline at end of file diff --git a/resources/views/mailing-liste/index.blade.php b/resources/views/mailing-liste/index.blade.php index 18b297e8..2870664b 100644 --- a/resources/views/mailing-liste/index.blade.php +++ b/resources/views/mailing-liste/index.blade.php @@ -34,6 +34,7 @@ $canDeleteMailing = Auth::user()->canDelete('mailing'); @endif
+ @include('partials._tag-filter-bar', ['filterTags' => $allTags ?? collect()])
@if($mailingLists->count() > 0) @@ -50,6 +51,7 @@ $canDeleteMailing = Auth::user()->canDelete('mailing'); + @@ -73,6 +75,16 @@ $canDeleteMailing = Auth::user()->canDelete('mailing'); +
Nome Descrizione ContattiTag Stato Creata il Azioni {{ $lista->contatti->count() }} + @foreach($lista->tags as $tag) + + {{ $tag->name }} + + @endforeach + @if($lista->tags->isEmpty()) + - + @endif + @if($lista->attiva) Attiva diff --git a/resources/views/mailing-liste/show.blade.php b/resources/views/mailing-liste/show.blade.php index afdd7aed..af51aed5 100644 --- a/resources/views/mailing-liste/show.blade.php +++ b/resources/views/mailing-liste/show.blade.php @@ -49,6 +49,23 @@ + +
+
+

Tag

+
+
+ @if($mailingList->tags->isNotEmpty()) + @foreach($mailingList->tags as $tag) + + {{ $tag->name }} + + @endforeach + @else +

Nessun tag assegnato

+ @endif +
+
diff --git a/resources/views/report/index.blade.php b/resources/views/report/index.blade.php index 65b8c9ef..8e0deb39 100644 --- a/resources/views/report/index.blade.php +++ b/resources/views/report/index.blade.php @@ -295,6 +295,22 @@
+
+ + + Filtra i risultati per tag +
+
+ + +
@@ -346,6 +362,37 @@ document.addEventListener('DOMContentLoaded', function() { columnsSelectInitialized = false; } + let tagFilterSelect2Initialized = false; + + function initTagFilterSelect2() { + if (tagFilterSelect2Initialized) return; + $('#tag_filter').select2({ + theme: 'bootstrap4', + placeholder: 'Seleziona tag...', + allowClear: true, + width: '100%', + templateResult: function(opt) { + if (!opt.id) return opt.text; + var color = $(opt.element).css('background-color'); + var $span = $('' + opt.text + ''); + return $span; + }, + templateSelection: function(opt) { + if (!opt.id) return opt.text; + var color = $(opt.element).css('background-color'); + var $span = $('' + opt.text + ''); + return $span; + } + }); + tagFilterSelect2Initialized = true; + } + + function destroyTagFilterSelect2() { + if (!tagFilterSelect2Initialized) return; + $('#tag_filter').select2('destroy'); + tagFilterSelect2Initialized = false; + } + function getSortOptions(tipoReport) { const cols = columnOptions[tipoReport] || {}; const options = []; @@ -365,8 +412,10 @@ document.addEventListener('DOMContentLoaded', function() { $('#collapseThree').on('shown.bs.collapse', function () { initColumnsSelect2(); + initTagFilterSelect2(); }).on('hidden.bs.collapse', function () { destroyColumnsSelect2(); + destroyTagFilterSelect2(); }); if ($('#collapseTwo').hasClass('show')) { @@ -375,6 +424,7 @@ document.addEventListener('DOMContentLoaded', function() { if ($('#collapseThree').hasClass('show')) { initColumnsSelect2(); + initTagFilterSelect2(); } $('#tipo_report').on('change', function() { diff --git a/routes/web.php b/routes/web.php index 4eae3c43..f0442490 100644 --- a/routes/web.php +++ b/routes/web.php @@ -17,6 +17,7 @@ use App\Http\Controllers\MailingController; use App\Http\Controllers\MailingListController; use App\Http\Controllers\ReportController; use App\Http\Controllers\ImpostazioniController; +use App\Http\Controllers\RicercaController; use App\Http\Controllers\Admin\UtenteController; use App\Http\Controllers\Admin\RuoloController; use App\Http\Controllers\Admin\ActivityLogController; @@ -75,6 +76,8 @@ Route::get('individui/{individuo}/report', [IndividuoController::class, 'report' Route::get('individui/{individuo}/collegati', [IndividuoController::class, 'elementiCollegati'])->middleware('auth'); Route::post('individui/{individuo}/elimina', [IndividuoController::class, 'elimina'])->middleware('auth'); Route::post('individui/mass-elimina', [IndividuoController::class, 'massElimina'])->middleware('auth'); +Route::post('individui/mass-gruppo', [IndividuoController::class, 'massGruppo'])->middleware('auth'); +Route::post('individui/mass-tag', [IndividuoController::class, 'massTag'])->middleware('auth'); Route::post('individui/{individuo}/avatar', [IndividuoController::class, 'uploadAvatar'])->middleware('auth'); Route::get('individui/{individuo}/avatar', [IndividuoController::class, 'getAvatar'])->middleware('auth'); Route::delete('individui/{individuo}/avatar', [IndividuoController::class, 'deleteAvatar'])->middleware('auth'); @@ -97,6 +100,8 @@ Route::post('gruppi/{gruppo}/avatar', [\App\Http\Controllers\GruppoAvatarControl Route::get('gruppi/{gruppo}/avatar-file', [\App\Http\Controllers\GruppoAvatarController::class, 'show'])->middleware('auth'); Route::delete('gruppi/{gruppo}/avatar', [\App\Http\Controllers\GruppoAvatarController::class, 'destroy'])->middleware('auth'); +Route::post('gruppi/mass-tag', [GruppoController::class, 'massTag'])->middleware('auth'); +Route::post('gruppi/mass-elimina', [GruppoController::class, 'massElimina'])->middleware('auth'); Route::resource('gruppi', GruppoController::class)->middleware('auth'); Route::get('eventi/calendar', [EventoController::class, 'calendar'])->name('eventi.calendar')->middleware('auth'); Route::get('eventi/calendar/events', [EventoController::class, 'calendarEvents'])->name('eventi.calendar.events')->middleware('auth'); @@ -133,6 +138,7 @@ Route::post('documenti/mass-update', [DocumentoController::class, 'massUpdate']) Route::post('documenti/mass-associate', [DocumentoController::class, 'massAssociate'])->middleware('auth'); Route::post('documenti/mass-move', [DocumentoController::class, 'massMove'])->middleware('auth'); Route::post('documenti/mass-download', [DocumentoController::class, 'massDownload'])->middleware('auth'); +Route::post('documenti/mass-tag', [DocumentoController::class, 'massTag'])->middleware('auth'); Route::post('documenti/check-links', [DocumentoController::class, 'checkMassLinks'])->middleware('auth'); Route::get('documenti/options', [DocumentoController::class, 'options'])->middleware('auth'); @@ -186,6 +192,11 @@ Route::post('/impostazioni/logo/remove', [ImpostazioniController::class, 'remove Route::post('/impostazioni/app-settings', [ImpostazioniController::class, 'saveAppSettings'])->middleware('auth')->name('impostazioni.app-settings.save'); Route::post('/impostazioni/migra-percorso', [ImpostazioniController::class, 'migrateStoragePath'])->middleware('auth')->name('impostazioni.migrate-percorso'); +Route::post('/impostazioni/tag', [ImpostazioniController::class, 'tagStore'])->middleware('auth')->name('impostazioni.tag.store'); +Route::put('/impostazioni/tag/{id}', [ImpostazioniController::class, 'tagUpdate'])->middleware('auth')->name('impostazioni.tag.update'); +Route::delete('/impostazioni/tag/{id}', [ImpostazioniController::class, 'tagDestroy'])->middleware('auth')->name('impostazioni.tag.destroy'); +Route::post('/impostazioni/tag/reorder', [ImpostazioniController::class, 'tagReorder'])->middleware('auth')->name('impostazioni.tag.reorder'); + Route::get('/impostazioni/email', [EmailSettingsController::class, 'index'])->middleware('auth')->name('impostazioni.email.index'); Route::post('/impostazioni/email', [EmailSettingsController::class, 'save'])->middleware('auth')->name('impostazioni.email.save'); Route::post('/impostazioni/email/test', [EmailSettingsController::class, 'testConnection'])->middleware('auth')->name('impostazioni.email.test'); @@ -206,6 +217,9 @@ Route::post('/calendario-connessioni/{id}/sync', [App\Http\Controllers\Calendari Route::post('/calendario-connessioni/sync-all', [App\Http\Controllers\CalendarioConnessioneController::class, 'syncAll'])->middleware('auth')->name('calendario-connessioni.sync-all'); Route::post('/calendario-connessioni/reorder', [App\Http\Controllers\CalendarioConnessioneController::class, 'reorder'])->middleware('auth')->name('calendario-connessioni.reorder'); +// Ricerca per Tag +Route::get('/ricerca', [RicercaController::class, 'index'])->middleware('auth')->name('ricerca.index'); + // Storage Repositories (Opzione B) Route::post('/storage-repositories', [StorageRepositoryController::class, 'store'])->middleware('auth')->name('storage-repositories.store'); Route::put('/storage-repositories/{storage_repository}', [StorageRepositoryController::class, 'update'])->middleware('auth')->name('storage-repositories.update'); From 06387568cb16f3de865d51cda2099832c3ab99e4 Mon Sep 17 00:00:00 2001 From: benzimariano Date: Tue, 9 Jun 2026 11:09:00 +0200 Subject: [PATCH 4/4] finale 2.0.0 --- app/Http/Controllers/RicercaController.php | 58 +++++++ app/Models/Tag.php | 62 +++++++ app/Traits/HasTagsLight.php | 30 ++++ .../2026_06_08_194155_create_tags_tables.php | 38 +++++ .../views/partials/_tag-filter-bar.blade.php | 34 ++++ .../views/partials/_tag-selector.blade.php | 123 ++++++++++++++ resources/views/ricerca/index.blade.php | 155 ++++++++++++++++++ 7 files changed, 500 insertions(+) create mode 100644 app/Http/Controllers/RicercaController.php create mode 100644 app/Models/Tag.php create mode 100644 app/Traits/HasTagsLight.php create mode 100644 database/migrations/2026_06_08_194155_create_tags_tables.php create mode 100644 resources/views/partials/_tag-filter-bar.blade.php create mode 100644 resources/views/partials/_tag-selector.blade.php create mode 100644 resources/views/ricerca/index.blade.php diff --git a/app/Http/Controllers/RicercaController.php b/app/Http/Controllers/RicercaController.php new file mode 100644 index 00000000..4b6e7b64 --- /dev/null +++ b/app/Http/Controllers/RicercaController.php @@ -0,0 +1,58 @@ +authorizeRead('individui'); + + $allTags = Tag::orderBy('name')->get(); + + $tagSlugs = (array) $request->input('tag', []); + + if (empty($tagSlugs)) { + return view('ricerca.index', compact('allTags')); + } + + $tagSlugs = array_filter($tagSlugs); + $selectedTags = Tag::whereIn('slug', $tagSlugs)->get(); + + $results = []; + $totals = []; + + if (count($tagSlugs) === 1) { + $tag = $selectedTags->first(); + $results['individui'] = $tag->individui()->orderBy('cognome')->orderBy('nome')->get(); + $results['gruppi'] = $tag->gruppi()->orderBy('nome')->get(); + $results['eventi'] = $tag->eventi()->orderBy('data_specifica')->orderBy('nome_evento')->get(); + $results['documenti'] = $tag->documenti()->orderBy('nome_file')->get(); + $results['mailingLists'] = $tag->mailingLists()->orderBy('nome')->get(); + } else { + $results['individui'] = Individuo::withAllTags($tagSlugs)->orderBy('cognome')->orderBy('nome')->get(); + $results['gruppi'] = Gruppo::withAllTags($tagSlugs)->orderBy('nome')->get(); + $results['eventi'] = Evento::withAllTags($tagSlugs)->orderBy('data_specifica')->orderBy('nome_evento')->get(); + $results['documenti'] = Documento::withAllTags($tagSlugs)->orderBy('nome_file')->get(); + $results['mailingLists'] = MailingList::withAllTags($tagSlugs)->orderBy('nome')->get(); + } + + $totals = [ + 'individui' => $results['individui']->count(), + 'gruppi' => $results['gruppi']->count(), + 'eventi' => $results['eventi']->count(), + 'documenti' => $results['documenti']->count(), + 'mailingLists' => $results['mailingLists']->count(), + ]; + + return view('ricerca.index', compact('selectedTags', 'tagSlugs', 'results', 'totals', 'allTags')); + } +} diff --git a/app/Models/Tag.php b/app/Models/Tag.php new file mode 100644 index 00000000..0a927734 --- /dev/null +++ b/app/Models/Tag.php @@ -0,0 +1,62 @@ + 'integer', + ]; + + protected static function boot(): void + { + parent::boot(); + + static::creating(function (self $tag) { + if (empty($tag->slug)) { + $tag->slug = str()->slug($tag->name); + } + }); + } + + public function individui(): MorphToMany + { + return $this->morphedByMany(Individuo::class, 'taggable'); + } + + public function gruppi(): MorphToMany + { + return $this->morphedByMany(Gruppo::class, 'taggable'); + } + + public function eventi(): MorphToMany + { + return $this->morphedByMany(Evento::class, 'taggable'); + } + + public function documenti(): MorphToMany + { + return $this->morphedByMany(Documento::class, 'taggable'); + } + + public function mailingLists(): MorphToMany + { + return $this->morphedByMany(MailingList::class, 'taggable'); + } + + public function getCountAttribute(): int + { + return $this->individui()->count() + + $this->gruppi()->count() + + $this->eventi()->count() + + $this->documenti()->count() + + $this->mailingLists()->count(); + } +} diff --git a/app/Traits/HasTagsLight.php b/app/Traits/HasTagsLight.php new file mode 100644 index 00000000..841d8d04 --- /dev/null +++ b/app/Traits/HasTagsLight.php @@ -0,0 +1,30 @@ +morphToMany(Tag::class, 'taggable'); + } + + public function scopeWithAllTags($query, array $tags) + { + foreach ($tags as $tag) { + $query->whereHas('tags', fn ($q) => $q->where('slug', $tag)); + } + + return $query; + } + + public function scopeWithAnyTags($query, array $tags) + { + $query->whereHas('tags', fn ($q) => $q->whereIn('slug', $tags)); + + return $query; + } +} diff --git a/database/migrations/2026_06_08_194155_create_tags_tables.php b/database/migrations/2026_06_08_194155_create_tags_tables.php new file mode 100644 index 00000000..1a9313e9 --- /dev/null +++ b/database/migrations/2026_06_08_194155_create_tags_tables.php @@ -0,0 +1,38 @@ +id(); + $table->string('name', 100); + $table->string('slug', 100)->unique(); + $table->string('color', 7)->nullable(); + $table->integer('order_column')->default(0); + $table->timestamps(); + }); + } + + if (!Schema::hasTable('taggables')) { + Schema::create('taggables', function (Blueprint $table) { + $table->foreignId('tag_id')->constrained()->cascadeOnDelete(); + $table->morphs('taggable'); + $table->primary(['tag_id', 'taggable_id', 'taggable_type']); + }); + } + } + + public function down(): void + { + Schema::dropIfExists('taggables'); + Schema::dropIfExists('tags'); + } +}; diff --git a/resources/views/partials/_tag-filter-bar.blade.php b/resources/views/partials/_tag-filter-bar.blade.php new file mode 100644 index 00000000..3a602076 --- /dev/null +++ b/resources/views/partials/_tag-filter-bar.blade.php @@ -0,0 +1,34 @@ +@php + $filterTags = $filterTags ?? (\App\Models\Tag::orderBy('name')->get()); + $activeTags = (array) request('tag', []); +@endphp +@if($filterTags->isNotEmpty()) +
+ Tag: + @foreach($filterTags as $tag) + @php + $isActive = in_array($tag->slug, $activeTags); + $query = request()->query(); + if ($isActive) { + $newTags = array_values(array_diff($activeTags, [$tag->slug])); + if (empty($newTags)) { + unset($query['tag']); + } else { + $query['tag'] = $newTags; + } + } else { + $query['tag'] = array_merge($activeTags, [$tag->slug]); + } + $url = request()->url() . '?' . http_build_query($query); + @endphp + + {{ $tag->name }} + + @endforeach + @if(!empty($activeTags)) + + Cancella filtri + + @endif +
+@endif diff --git a/resources/views/partials/_tag-selector.blade.php b/resources/views/partials/_tag-selector.blade.php new file mode 100644 index 00000000..0f34714b --- /dev/null +++ b/resources/views/partials/_tag-selector.blade.php @@ -0,0 +1,123 @@ +@php + $allTags = $allTags ?? \App\Models\Tag::orderBy('name')->get(); + $selectedTags = old('tags', $selectedTags ?? []); + $tagFieldName = $tagFieldName ?? 'tags'; + $selectorId = 'tag-selector-' . md5($tagFieldName); + $selectedTagObjects = $allTags->whereIn('id', $selectedTags); +@endphp + +
+ + +
+
+ @foreach($selectedTagObjects as $tag) + + {{ $tag->name }} + + + @endforeach +
+ +
+ +
+ +
+ @foreach($allTags as $tag) + + @endforeach + @if($allTags->isEmpty()) +

Nessun tag disponibile. Crea i tag nelle Impostazioni.

+ @endif + +
+
+ +
+ @foreach($selectedTags as $selectedId) + + @endforeach +
+
+ +@once +@push('scripts') + +@endpush +@endonce diff --git a/resources/views/ricerca/index.blade.php b/resources/views/ricerca/index.blade.php new file mode 100644 index 00000000..1261bbf8 --- /dev/null +++ b/resources/views/ricerca/index.blade.php @@ -0,0 +1,155 @@ +@extends('layouts.adminlte') + +@section('title', 'Ricerca per Tag') +@section('page_title', 'Ricerca per Tag') + +@section('content') +
+
+

+ @isset($selectedTags) + Risultati per: + @foreach($selectedTags as $t) + {{ $t->name }} + @endforeach + @if(count($tagSlugs) > 1) + (intersezione — tutti i tag richiesti) + @endif + @else + Seleziona uno o più tag + @endisset +

+
+
+ @if(!isset($selectedTags) || $selectedTags->isEmpty()) +

Clicca sui tag per selezionarli (Ctrl+click per più tag).

+
+
+ @foreach($allTags as $t) + + @endforeach + @if($allTags->isEmpty()) +

Nessun tag disponibile. Crea i tag nelle Impostazioni.

+ @endif +
+ @if($allTags->isNotEmpty()) +
+ +
+ @endif +
+ @else +
+ @php + $indexRoutes = [ + 'individui' => 'individui.index', + 'gruppi' => 'gruppi.index', + 'eventi' => 'eventi.index', + 'documenti' => 'documenti.index', + 'mailingLists' => 'mailing-liste.index', + ]; + $tagParams = collect($tagSlugs)->map(fn($s) => "tag[]={$s}")->implode('&'); + @endphp + + @foreach([ + ['key' => 'individui', 'color' => 'info', 'icon' => 'fa-user', 'label' => 'Individui'], + ['key' => 'gruppi', 'color' => 'success', 'icon' => 'fa-users', 'label' => 'Gruppi'], + ['key' => 'eventi', 'color' => 'warning', 'icon' => 'fa-calendar-alt', 'label' => 'Eventi'], + ['key' => 'documenti', 'color' => 'secondary', 'icon' => 'fa-file', 'label' => 'Documenti'], + ['key' => 'mailingLists', 'color' => 'purple', 'icon' => 'fa-list', 'label' => 'Mailing List'], + ] as $box) +
+
+ +
+ {{ $box['label'] }} + {{ $totals[$box['key']] }} + @if($totals[$box['key']] > 0 && isset($indexRoutes[$box['key']])) + Vedi tutti » + @endif +
+
+
+ @endforeach +
+ + @php + $sections = [ + 'individui' => ['color' => 'info', 'icon' => 'fa-user', 'title' => 'Individui', 'cols' => ['Cognome', 'Nome', 'Email'], 'fields' => ['cognome', 'nome', 'email_primaria']], + 'gruppi' => ['color' => 'success', 'icon' => 'fa-users', 'title' => 'Gruppi', 'cols' => ['Nome', 'Descrizione'], 'fields' => ['nome', 'descrizione_limit']], + 'eventi' => ['color' => 'warning', 'icon' => 'fa-calendar-alt', 'title' => 'Eventi', 'cols' => ['Evento', 'Data', 'Ora'], 'fields' => ['nome_evento', 'data', 'ora']], + 'documenti' => ['color' => 'secondary', 'icon' => 'fa-file', 'title' => 'Documenti', 'cols' => ['Nome File', 'Tipo'], 'fields' => ['nome_file', 'tipo']], + 'mailingLists' => ['color' => 'purple', 'icon' => 'fa-list', 'title' => 'Mailing List', 'cols' => ['Nome', 'Contatti', 'Stato'], 'fields' => ['nome', 'contatti_count', 'stato']], + ]; + @endphp + + @foreach($sections as $key => $section) + @if($totals[$key] > 0) +
+
+
{{ $section['title'] }} ({{ $totals[$key] }})
+
+
+ + + @foreach($section['cols'] as $col)@endforeach + + + + @foreach($results[$key] as $item) + + @if($key === 'individui') + + + + + @elseif($key === 'gruppi') + + + + @elseif($key === 'eventi') + + + + + @elseif($key === 'documenti') + + + + @elseif($key === 'mailingLists') + + + + + @endif + + @endforeach + +
{{ $col }}
{{ $item->cognome }}{{ $item->nome }}{{ $item->email_primaria ?? '-' }}{{ $item->nome }}{{ Str::limit($item->descrizione, 80) ?? '-' }}{{ $item->nome_evento }}{{ $item->data_specifica ? $item->data_specifica->format('d/m/Y') : ($item->tipo_recorrenza ?? '-') }}{{ $item->ora_inizio ? $item->ora_inizio->format('H:i') : '-' }}{{ $item->nome_file }}{{ $item->tipo ?? '-' }}{{ $item->nome }}{{ $item->contatti->count() }} + @if($item->attiva) + Attiva + @else + Disattiva + @endif +
+
+
+ @endif + @endforeach + + @if(array_sum($totals) === 0) +
+ Nessun elemento trovato con tutti i tag selezionati. +
+ @endif + + + @endisset +
+
+@endsection \ No newline at end of file