diff --git a/app/Filament/Pages/Condomini/TabelleMillesimaliArchivio.php b/app/Filament/Pages/Condomini/TabelleMillesimaliArchivio.php index 5582f61..7856f41 100644 --- a/app/Filament/Pages/Condomini/TabelleMillesimaliArchivio.php +++ b/app/Filament/Pages/Condomini/TabelleMillesimaliArchivio.php @@ -76,11 +76,17 @@ class TabelleMillesimaliArchivio extends Page implements HasTable /** @var array> */ public array $tabelle = []; + /** @var array */ + public array $selectedArchiveIds = []; + /** @var array> */ public array $righe = []; public ?array $tabellaInfo = null; + /** @var array> */ + protected array $archiveDeletionAnalysisCache = []; + public static function canAccess(): bool { $user = Auth::user(); @@ -321,7 +327,22 @@ public function table(Table $table): Table } TabellaMillesimale::query()->create($payload); + $this->resetArchiveDeletionAnalysisCache(); + $this->loadTabelle(); + $this->tabellaId = $this->pickTabellaId($this->tabellaId); + $this->loadDettaglioTabella(); + $this->loadVociSpesa(); }), + + Action::make('delete_selected_archives') + ->label(fn (): string => 'Elimina selezionate (' . count($this->normalizeSelectedArchiveIds()) . ')') + ->icon('heroicon-o-trash') + ->color('danger') + ->requiresConfirmation() + ->modalHeading('Elimina archivi millesimali selezionati') + ->modalDescription('Saranno eliminate solo le tabelle senza collegamenti attivi. Le tabelle ancora usate da dettagli, voci o altri archivi verranno saltate.') + ->disabled(fn (): bool => $this->normalizeSelectedArchiveIds() === []) + ->action(fn (): mixed => $this->deleteSelectedArchives()), ]) ->columns([ TextColumn::make( @@ -433,6 +454,14 @@ public function table(Table $table): Table ->sortable() ->toggleable(), + TextColumn::make('cleanup_status') + ->label('Integrità') + ->getStateUsing(fn(TabellaMillesimale $record): string => $this->getArchiveCleanupBadge($record)) + ->tooltip(fn(TabellaMillesimale $record): string => $this->getArchiveCleanupTooltip($record)) + ->badge() + ->color(fn(TabellaMillesimale $record): string => $this->analyzeArchiveDeletion($record)['deletable'] ? 'success' : 'warning') + ->toggleable(), + TextColumn::make('preventivo') ->label('Prev.') ->getStateUsing(function (TabellaMillesimale $record) { @@ -476,6 +505,13 @@ public function table(Table $table): Table ->boolean(), ]) ->actions([ + Action::make('select_cleanup') + ->label(fn(TabellaMillesimale $record): string => in_array((int) $record->id, $this->normalizeSelectedArchiveIds(), true) ? 'Desel.' : 'Sel.') + ->icon(fn(TabellaMillesimale $record): string => in_array((int) $record->id, $this->normalizeSelectedArchiveIds(), true) ? 'heroicon-o-check-circle' : 'heroicon-o-plus-circle') + ->color(fn(TabellaMillesimale $record): string => in_array((int) $record->id, $this->normalizeSelectedArchiveIds(), true) ? 'primary' : 'gray') + ->tooltip(fn(TabellaMillesimale $record): string => $this->getArchiveCleanupTooltip($record)) + ->action(fn(TabellaMillesimale $record): mixed => $this->toggleArchiveSelection((int) $record->id)), + Action::make('edit') ->label('Modifica') ->icon('heroicon-o-pencil-square') @@ -588,6 +624,7 @@ public function table(Table $table): Table } $record->update($payload); + $this->resetArchiveDeletionAnalysisCache(); }), Action::make('delete') @@ -595,7 +632,7 @@ public function table(Table $table): Table ->icon('heroicon-o-trash') ->color('danger') ->requiresConfirmation() - ->action(fn(TabellaMillesimale $record) => $record->delete()), + ->action(fn(TabellaMillesimale $record): mixed => $this->deleteSingleArchive($record)), Action::make('prospetto') ->label('Prospetto') @@ -604,6 +641,194 @@ public function table(Table $table): Table ]); } + protected function normalizeSelectedArchiveIds(): array + { + $selected = array_values(array_unique(array_map( + static fn ($id): int => (int) $id, + array_filter($this->selectedArchiveIds, static fn ($id): bool => is_numeric($id) && (int) $id > 0) + ))); + + $this->selectedArchiveIds = $selected; + + return $selected; + } + + public function toggleArchiveSelection(int $tabellaId): void + { + $selected = $this->normalizeSelectedArchiveIds(); + + if (in_array($tabellaId, $selected, true)) { + $this->selectedArchiveIds = array_values(array_filter($selected, static fn (int $id): bool => $id !== $tabellaId)); + return; + } + + $selected[] = $tabellaId; + $this->selectedArchiveIds = array_values(array_unique($selected)); + } + + protected function deleteSelectedArchives(): void + { + $selected = $this->normalizeSelectedArchiveIds(); + if ($selected === []) { + Notification::make() + ->title('Nessuna tabella selezionata') + ->warning() + ->send(); + return; + } + + $records = TabellaMillesimale::query() + ->where('stabile_id', $this->stabileId) + ->whereIn('id', $selected) + ->get(); + + $deleted = 0; + $blocked = []; + + foreach ($records as $record) { + $analysis = $this->analyzeArchiveDeletion($record); + if (! $analysis['deletable']) { + $blocked[] = sprintf('%s (%s)', $record->codice_tabella ?: ('Tabella #' . $record->id), $analysis['summary']); + continue; + } + + $record->delete(); + $deleted++; + } + + $this->selectedArchiveIds = array_values(array_diff($selected, $records->pluck('id')->map(fn ($id): int => (int) $id)->all())); + $this->resetArchiveDeletionAnalysisCache(); + $this->loadTabelle(); + $this->tabellaId = $this->pickTabellaId($this->tabellaId && ! in_array($this->tabellaId, $selected, true) ? $this->tabellaId : null); + $this->loadDettaglioTabella(); + $this->loadVociSpesa(); + + $notification = Notification::make(); + + if ($deleted > 0) { + $notification->title('Pulizia archivi completata')->success(); + } else { + $notification->title('Nessun archivio eliminato')->warning(); + } + + $body = []; + if ($deleted > 0) { + $body[] = $deleted . ' tabella/e eliminata/e.'; + } + if ($blocked !== []) { + $body[] = 'Saltate per collegamenti attivi: ' . implode('; ', array_slice($blocked, 0, 5)); + if (count($blocked) > 5) { + $body[] = 'Altre tabelle bloccate: ' . (count($blocked) - 5) . '.'; + } + } + + if ($body !== []) { + $notification->body(implode(' ', $body)); + } + + $notification->send(); + } + + protected function deleteSingleArchive(TabellaMillesimale $record): void + { + $analysis = $this->analyzeArchiveDeletion($record); + if (! $analysis['deletable']) { + Notification::make() + ->title('Eliminazione bloccata') + ->body('La tabella e ancora collegata: ' . $analysis['summary']) + ->warning() + ->send(); + return; + } + + $record->delete(); + $this->selectedArchiveIds = array_values(array_filter($this->normalizeSelectedArchiveIds(), static fn (int $id): bool => $id !== (int) $record->id)); + $this->resetArchiveDeletionAnalysisCache(); + $this->loadTabelle(); + $this->tabellaId = $this->pickTabellaId($this->tabellaId === (int) $record->id ? null : $this->tabellaId); + $this->loadDettaglioTabella(); + $this->loadVociSpesa(); + + Notification::make() + ->title('Tabella eliminata') + ->success() + ->send(); + } + + protected function analyzeArchiveDeletion(TabellaMillesimale $record): array + { + $recordId = (int) $record->id; + + if (isset($this->archiveDeletionAnalysisCache[$recordId])) { + return $this->archiveDeletionAnalysisCache[$recordId]; + } + + $sources = [ + 'dettaglio_millesimi' => ['fk' => 'tabella_millesimale_id', 'label' => 'dettagli millesimi'], + 'dettagli_tabelle_millesimali' => ['fk' => 'tabella_millesimale_id', 'label' => 'dettagli tabelle'], + 'dettagli_millesimali' => ['fk' => 'tabella_id', 'label' => 'dettagli legacy'], + 'voci_spesa' => ['fk' => 'tabella_millesimale_default_id', 'label' => 'voci spesa'], + 'voci_preventivo' => ['fk' => 'tabella_millesimale_id', 'label' => 'voci preventivo'], + 'ripartizione_spese' => ['fk' => 'tabella_millesimale_id', 'label' => 'ripartizioni'], + 'ordine_giorno' => ['fk' => 'tabella_millesimale_id', 'label' => 'ordini del giorno'], + 'dettaglio_movimenti' => ['fk' => 'tabella_millesimale_id', 'label' => 'dettagli movimenti'], + ]; + + $counts = []; + foreach ($sources as $table => $config) { + if (! Schema::hasTable($table) || ! Schema::hasColumn($table, $config['fk'])) { + continue; + } + + $count = DB::table($table) + ->where($config['fk'], $recordId) + ->count(); + + if ($count > 0) { + $counts[$config['label']] = $count; + } + } + + $summaryParts = []; + foreach ($counts as $label => $count) { + $summaryParts[] = $label . ': ' . $count; + } + + return $this->archiveDeletionAnalysisCache[$recordId] = [ + 'deletable' => $counts === [], + 'total_links' => array_sum($counts), + 'counts' => $counts, + 'summary' => $summaryParts === [] ? 'nessun collegamento attivo' : implode(', ', $summaryParts), + ]; + } + + protected function getArchiveCleanupBadge(TabellaMillesimale $record): string + { + $analysis = $this->analyzeArchiveDeletion($record); + + if ($analysis['deletable']) { + return 'Orfana'; + } + + return $analysis['total_links'] . ' colleg.'; + } + + protected function getArchiveCleanupTooltip(TabellaMillesimale $record): string + { + $analysis = $this->analyzeArchiveDeletion($record); + + if ($analysis['deletable']) { + return 'Tabella non collegata: eliminabile in pulizia.'; + } + + return 'Tabella collegata, pulizia bloccata: ' . $analysis['summary']; + } + + protected function resetArchiveDeletionAnalysisCache(): void + { + $this->archiveDeletionAnalysisCache = []; + } + protected function loadTabelle(): void { $this->tabelle = []; @@ -1101,6 +1326,8 @@ private function formatCodiceUnita(?string $codice): ?string return null; } + $prefix = trim((string) ($this->codicePrefix ?? '')); + if ($prefix !== '') { $pattern = '/^' . preg_quote($prefix, '/') . '[\s\-\/]+/'; $codice = preg_replace($pattern, '', $codice) ?? $codice; diff --git a/docs/support/MANUALE-SVILUPPO-OPERATIVO.md b/docs/support/MANUALE-SVILUPPO-OPERATIVO.md index adbabab..d107c74 100644 --- a/docs/support/MANUALE-SVILUPPO-OPERATIVO.md +++ b/docs/support/MANUALE-SVILUPPO-OPERATIVO.md @@ -100,6 +100,19 @@ ### 2. Allineamento stabile/anno nelle pagine gestionali - [U] base piu affidabile per estrazioni acqua e verifiche contabili per gestione - [P] meno logica sparsa con chiavi session legacy non coerenti tra le pagine +### 2-bis. Pulizia sicura archivi tabelle millesimali + +- In `condomini/tabelle-millesimali`, tab `Archivio`, ogni riga ha ora l azione `Sel.` per marcare archivi sospetti o dati sporchi. +- Il pulsante `Elimina selezionate` rimuove solo le tabelle orfane e salta automaticamente quelle ancora referenziate. +- Il controllo integrita verifica in modo adattivo le tabelle realmente presenti nell ambiente, tra cui `dettaglio_millesimi`, `dettagli_tabelle_millesimali`, `voci_spesa` e `voci_preventivo`. +- Anche l azione singola `Elimina` e ora protetta: se la tabella e ancora usata, la delete viene bloccata con dettaglio dei collegamenti attivi. + +Effetto pratico: + +- [U] si possono ripulire archivi legacy o tabelle non valorizzate senza rompere collegamenti gia attivi +- [U] l operatore vede subito in colonna `Integrita` se una tabella e `Orfana` oppure quanti collegamenti ha +- [P] la pulizia resta coerente tra ambienti con strutture database leggermente diverse + ### 3. Supporto > Modifiche come pannello unico - La pagina ha gia tab per aggiornamenti, bug, commit e migliorie. diff --git a/resources/views/filament/pages/condomini/tabelle-millesimali.blade.php b/resources/views/filament/pages/condomini/tabelle-millesimali.blade.php index 6c930ce..b9c75a5 100644 --- a/resources/views/filament/pages/condomini/tabelle-millesimali.blade.php +++ b/resources/views/filament/pages/condomini/tabelle-millesimali.blade.php @@ -33,6 +33,18 @@ class="inline-flex items-center rounded-lg border px-4 py-2 text-sm font-semibol @if(($tab ?? 'archivio') === 'archivio') + +
+
+
Pulizia archivi millesimali
+
Usa `Sel.` per marcare gli archivi sporchi. La cancellazione elimina solo le tabelle orfane e blocca quelle ancora collegate a dettagli, voci o altri riferimenti attivi.
+
+
+ Selezionate: {{ count($selectedArchiveIds ?? []) }} +
+
+
+ {{ $this->table }} @elseif(($tab ?? 'archivio') === 'prospetto') @php