Add safe millesimali archive cleanup

This commit is contained in:
michele 2026-05-03 08:48:29 +00:00
parent 119f486bb8
commit b37171a438
3 changed files with 253 additions and 1 deletions

View File

@ -76,11 +76,17 @@ class TabelleMillesimaliArchivio extends Page implements HasTable
/** @var array<int, array<string, mixed>> */
public array $tabelle = [];
/** @var array<int, int> */
public array $selectedArchiveIds = [];
/** @var array<int, array<string, mixed>> */
public array $righe = [];
public ?array $tabellaInfo = null;
/** @var array<int, array<string, mixed>> */
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;

View File

@ -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.

View File

@ -33,6 +33,18 @@ class="inline-flex items-center rounded-lg border px-4 py-2 text-sm font-semibol
</div>
@if(($tab ?? 'archivio') === 'archivio')
<x-filament::section class="p-4!">
<div class="flex items-start justify-between gap-4 flex-wrap">
<div class="space-y-1">
<div class="text-sm font-semibold text-gray-900">Pulizia archivi millesimali</div>
<div class="text-sm text-gray-600">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.</div>
</div>
<div class="rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800">
Selezionate: {{ count($selectedArchiveIds ?? []) }}
</div>
</div>
</x-filament::section>
{{ $this->table }}
@elseif(($tab ?? 'archivio') === 'prospetto')
@php