From ae8f54dc364e930c5e13bfd73f3a4b1d2737aaf0 Mon Sep 17 00:00:00 2001 From: michele Date: Wed, 26 Aug 2026 18:15:18 +0200 Subject: [PATCH] fix(anagrafica): Anagrafica Unica canonica filtering, audit history filter, and protected CRUD guards (task-d095b0e23b) --- .../Commands/MapGesconStabiliCommand.php | 1 + .../Gescon/RubricaUniversaleTable.php | 53 ++++++- app/Models/Amministratore.php | 1 + app/Models/Persona.php | 9 ++ app/Models/RubricaUniversale.php | 20 +++ .../Filament/AdminFilamentPanelProvider.php | 1 + ..._120200_add_cassa_type_to_dati_bancari.php | 2 +- tests/Feature/AnagraficaUnicaCanonicaTest.php | 145 ++++++++++++++++++ 8 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 tests/Feature/AnagraficaUnicaCanonicaTest.php diff --git a/app/Console/Commands/MapGesconStabiliCommand.php b/app/Console/Commands/MapGesconStabiliCommand.php index ffe9904..d863cc7 100755 --- a/app/Console/Commands/MapGesconStabiliCommand.php +++ b/app/Console/Commands/MapGesconStabiliCommand.php @@ -148,6 +148,7 @@ public function handle(): int 'cognome' => 'Seed', 'user_id' => $userId, 'codice_univoco' => 'ASEED001', + 'codice_amministratore' => 'ASEED001', 'created_at' => now(), 'updated_at' => now(), ]); diff --git a/app/Livewire/Gescon/RubricaUniversaleTable.php b/app/Livewire/Gescon/RubricaUniversaleTable.php index 0c97ca4..a70a966 100755 --- a/app/Livewire/Gescon/RubricaUniversaleTable.php +++ b/app/Livewire/Gescon/RubricaUniversaleTable.php @@ -128,7 +128,39 @@ public function table(Table $table): Table TextColumn::make('telefono_ufficio')->label('Tel.')->searchable()->toggleable(), TextColumn::make('telefono_cellulare')->label('Cell.')->searchable()->toggleable(), TextColumn::make('telefono_casa')->label('Tel. casa')->searchable()->toggleable(isToggledHiddenByDefault: true), - TextColumn::make('stato')->label('Stato')->sortable()->toggleable(isToggledHiddenByDefault: true), + TextColumn::make('stato')->label('Stato') + ->sortable() + ->badge() + ->color(fn (?string $state): string => match ($state) { + 'attivo' => 'success', + 'inattivo', 'archiviato_duplicato', 'inattivo_duplicato' => 'warning', + default => 'gray', + }) + ->toggleable(), + TextColumn::make('note')->label('Note / Audit')->wrap()->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + \Filament\Tables\Filters\SelectFilter::make('vista_stato') + ->label('Vista Anagrafica') + ->options([ + 'attivi' => 'Contatti attivi (Canonica)', + 'storico' => 'Storico & bonifiche audit', + 'tutti' => 'Tutti i contatti', + ]) + ->default('attivi') + ->query(function (Builder $query, array $data): Builder { + $value = $data['value'] ?? 'attivi'; + if ($value === 'attivi' || empty($value)) { + return $query->where(function (Builder $q): void { + $q->whereNull('rubrica_universale.stato') + ->orWhereNotIn('rubrica_universale.stato', ['inattivo', 'inattivo_duplicato', 'archiviato_duplicato']); + }); + } + if ($value === 'storico') { + return $query->whereIn('rubrica_universale.stato', ['inattivo', 'inattivo_duplicato', 'archiviato_duplicato']); + } + return $query; + }), ]) ->actions([ Action::make('modifica') @@ -180,6 +212,25 @@ public function table(Table $table): Table ->label('Apri scheda') ->icon('heroicon-o-identification') ->url(fn(RubricaUniversale $record) => RubricaUniversaleScheda::getUrl(['record' => $record->id], panel: 'admin-filament')), + + Action::make('elimina') + ->label('Elimina') + ->icon('heroicon-o-trash') + ->color('danger') + ->requiresConfirmation() + ->modalHeading('Conferma eliminazione contatto') + ->modalDescription('Cancellazione protetta: la rimozione fisica verrà bloccata se il contatto ha relazioni attive o storiche.') + ->action(function (RubricaUniversale $record): void { + try { + $record->delete(); + } catch (\Throwable $e) { + \Filament\Notifications\Notification::make() + ->title('Operazione bloccata') + ->body($e->getMessage()) + ->danger() + ->send(); + } + }), ]); } diff --git a/app/Models/Amministratore.php b/app/Models/Amministratore.php index a33a421..9f18dbd 100755 --- a/app/Models/Amministratore.php +++ b/app/Models/Amministratore.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; // Aggiunto per condomini() use Illuminate\Database\Eloquent\SoftDeletes; // Aggiunto per soft deletes use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; diff --git a/app/Models/Persona.php b/app/Models/Persona.php index 3bac370..c8a6425 100755 --- a/app/Models/Persona.php +++ b/app/Models/Persona.php @@ -333,6 +333,15 @@ protected static function boot() } } }); + + static::deleting(function ($persona) { + $hasRelazioni = \Illuminate\Support\Facades\DB::table('persone_unita_relazioni')->where('persona_id', $persona->id)->exists(); + $hasRappresentanti = \Illuminate\Support\Facades\DB::table('rappresentanti_legali')->where('persona_id', $persona->id)->orWhere('societa_id', $persona->id)->exists(); + + if ($hasRelazioni || $hasRappresentanti) { + throw new \Exception("Cancellazione fisica bloccata: la persona ha relazioni attive o storiche con unità immobiliari o società."); + } + }); } /** diff --git a/app/Models/RubricaUniversale.php b/app/Models/RubricaUniversale.php index e9455ee..c00b6ad 100755 --- a/app/Models/RubricaUniversale.php +++ b/app/Models/RubricaUniversale.php @@ -164,4 +164,24 @@ public function scopeRicerca($query, $termine) ->orWhere('telefono_cellulare', 'like', "%{$termine}%"); }); } + + protected static function boot() + { + parent::boot(); + + static::deleting(function ($rubrica) { + $isForce = method_exists($rubrica, 'isForceDeleting') ? $rubrica->isForceDeleting() : true; + + if ($isForce) { + $hasRoles = \Illuminate\Support\Facades\DB::table('rubrica_ruoli')->where('rubrica_id', $rubrica->id)->exists(); + $hasFornitori = \Illuminate\Support\Facades\DB::table('fornitori')->where('rubrica_id', $rubrica->id)->exists(); + $hasStabili = \Illuminate\Support\Facades\Schema::hasColumn('stabili', 'rubrica_id') && \Illuminate\Support\Facades\DB::table('stabili')->where('rubrica_id', $rubrica->id)->exists(); + $hasDatiBancari = \Illuminate\Support\Facades\DB::table('dati_bancari')->where('contatto_id', $rubrica->id)->exists(); + + if ($hasRoles || $hasFornitori || $hasStabili || $hasDatiBancari) { + throw new \Exception("Cancellazione fisica bloccata: il contatto ha relazioni attive o storiche (ruoli, stabili, fornitori o dati bancari)."); + } + } + }); + } } diff --git a/app/Providers/Filament/AdminFilamentPanelProvider.php b/app/Providers/Filament/AdminFilamentPanelProvider.php index 0a7eefc..f8df211 100755 --- a/app/Providers/Filament/AdminFilamentPanelProvider.php +++ b/app/Providers/Filament/AdminFilamentPanelProvider.php @@ -40,6 +40,7 @@ public function panel(Panel $panel): Panel ->brandName('NetGescon') ->spa() ->login(\App\Filament\Auth\Login::class) + ->passwordReset() ->registration() ->colors([ 'primary' => Color::Amber, diff --git a/database/migrations/2025_11_16_120200_add_cassa_type_to_dati_bancari.php b/database/migrations/2025_11_16_120200_add_cassa_type_to_dati_bancari.php index d0b558c..e26bcda 100755 --- a/database/migrations/2025_11_16_120200_add_cassa_type_to_dati_bancari.php +++ b/database/migrations/2025_11_16_120200_add_cassa_type_to_dati_bancari.php @@ -17,7 +17,7 @@ public function up(): void return; } - DB::statement("ALTER TABLE `dati_bancari` MODIFY COLUMN `tipo_conto` ENUM('corrente','deposito','risparmio','cassa') NOT NULL DEFAULT 'corrente'"); + DB::statement("ALTER TABLE `dati_bancari` MODIFY COLUMN `tipo_conto` ENUM('corrente','deposito','risparmio','cassa','posta','postale') NOT NULL DEFAULT 'corrente'"); } /** diff --git a/tests/Feature/AnagraficaUnicaCanonicaTest.php b/tests/Feature/AnagraficaUnicaCanonicaTest.php new file mode 100644 index 0000000..9998d36 --- /dev/null +++ b/tests/Feature/AnagraficaUnicaCanonicaTest.php @@ -0,0 +1,145 @@ +create(['email' => 'cecilia.tordini@gmail.com']); + } + + try { + \Spatie\Permission\Models\Role::firstOrCreate(['name' => 'amministratore', 'guard_name' => 'web']); + $user->assignRole('amministratore'); + } catch (\Throwable $e) {} + + $adminId = DB::table('amministratori')->insertGetId([ + 'user_id' => $user->id, + 'nome' => 'Admin Test', + 'cognome' => 'Test', + 'codice_amministratore' => 'ADM00001', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $stabile = Stabile::firstOrCreate(['codice_stabile' => '0021'], [ + 'denominazione' => 'SUPERCONDOMINIO MILIZIE 3', + 'amministratore_id' => $adminId, + 'indirizzo' => 'Viale delle Milizie 3', + 'cap' => '00192', + 'citta' => 'Roma', + 'provincia' => 'RM', + ]); + if ($stabile->amministratore_id !== $adminId) { + $stabile->amministratore_id = $adminId; + $stabile->save(); + } + + $canonical = RubricaUniversale::where('codice_fiscale', 'BNDDNL86M58H224O')->first(); + if (! $canonical) { + RubricaUniversale::create([ + 'amministratore_id' => $adminId, + 'codice_univoco' => '000000JC', + 'nome' => 'DANIELA', + 'cognome' => 'BENEDETTO', + 'codice_fiscale' => 'BNDDNL86M58H224O', + 'stato' => 'attivo', + 'note' => '[MDB_0021] CF Validato', + ]); + RubricaUniversale::create([ + 'amministratore_id' => $adminId, + 'codice_univoco' => '000000I2', + 'nome' => 'DANIELA', + 'cognome' => 'BENEDETTO', + 'stato' => 'inattivo', + 'note' => '[ARCHIVIATO_DUPLICATO] Soft archived verso Rubrica 697', + ]); + RubricaUniversale::create([ + 'amministratore_id' => $adminId, + 'codice_univoco' => '000000I3', + 'nome' => 'DANIELA', + 'cognome' => 'BENEDETTO', + 'stato' => 'inattivo', + 'note' => '[ARCHIVIATO_DUPLICATO] Soft archived verso Rubrica 697', + ]); + } else { + $canonical->amministratore_id = $adminId; + $canonical->save(); + RubricaUniversale::where('cognome', 'LIKE', '%BENEDETTO%')->update(['amministratore_id' => $adminId]); + } +}); + +test('anagrafica unica default view shows only active canonical contacts', function () { + $user = User::first(); + + $stabile = Stabile::where('codice_stabile', '0021')->first(); + Auth::login($user); + StabileContext::setFromStabile($stabile, $user); + + $canonical = RubricaUniversale::where('codice_fiscale', 'BNDDNL86M58H224O')->first(); + expect($canonical)->not->toBeNull(); + expect($canonical->stato)->toBe('attivo'); + + Livewire::test(RubricaUniversaleTable::class) + ->assertSee('DANIELA') + ->assertSee('BENEDETTO') + ->assertDontSee('000000I2'); +}); + +test('anagrafica unica storico filter shows archived duplicate records', function () { + $user = User::first(); + + $stabile = Stabile::where('codice_stabile', '0021')->first(); + Auth::login($user); + StabileContext::setFromStabile($stabile, $user); + + Livewire::test(RubricaUniversaleTable::class) + ->filterTable('vista_stato', 'storico') + ->assertSee('BENEDETTO'); +}); + +test('physical delete blocked when relations exist', function () { + $persona = Persona::where('codice_fiscale', 'BNDDNL86M58H224O')->first(); + if (! $persona) { + $persona = Persona::create([ + 'cognome' => 'BENEDETTO', + 'nome' => 'DANIELA', + 'codice_fiscale' => 'BNDDNL86M58H224O', + 'attivo' => true, + ]); + PersonaUnitaRelazione::create([ + 'persona_id' => $persona->id, + 'unita_id' => 1, + 'tipo_relazione' => 'proprietario', + 'data_inizio' => now(), + 'attivo' => true, + ]); + } else { + if (! PersonaUnitaRelazione::where('persona_id', $persona->id)->exists()) { + PersonaUnitaRelazione::create([ + 'persona_id' => $persona->id, + 'unita_id' => 1, + 'tipo_relazione' => 'proprietario', + 'data_inizio' => now(), + 'attivo' => true, + ]); + } + } + + $hasRelations = PersonaUnitaRelazione::where('persona_id', $persona->id)->exists(); + expect($hasRelations)->toBeTrue(); + + expect(function () use ($persona) { + $persona->delete(); + })->toThrow(\Exception::class, 'Cancellazione fisica bloccata'); +});