feat: Implementa sistema completo di gestione associazioni record

- Aggiunge modello RecordAssociation con migrazione database
- Implementa servizio CRUD completo per gestione associazioni
- Crea interfaccia utente avanzata per visualizzazione e gestione
- Aggiunge funzionalità di filtro, paginazione e ricerca
- Implementa azioni di massa (eliminazione, validazione, pulizia)
- Aggiunge esportazione CSV delle associazioni
- Integra validazione automatica degli ID destinazione
- Implementa logica upsert robusta con controllo validità associazioni
- Aggiunge selezione manuale chiavi per sorgenti non-database
- Migliora UI con statistiche, modali di conferma e feedback operazioni
- Refactoring completo logica trasferimento dati per utilizzare associazioni
This commit is contained in:
2025-06-28 02:22:46 +02:00
parent 51c61eabf7
commit 34b47a2bd4
9 changed files with 707 additions and 1 deletions
+315
View File
@@ -41,6 +41,73 @@
</div>
</div>
<!-- Azioni di Gestione -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-tools"></i> Gestione Associazioni</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<h6><i class="fas fa-broom"></i> Pulizia Associazioni</h6>
<div class="d-grid gap-2">
<button class="btn btn-warning" @onclick="() => ShowClearConfirmation(false)" disabled="@isProcessing">
<i class="fas fa-trash-alt"></i> Pulisci Associazioni Filtrate
</button>
<button class="btn btn-danger" @onclick="() => ShowClearConfirmation(true)" disabled="@isProcessing">
<i class="fas fa-trash"></i> Elimina TUTTE le Associazioni
</button>
</div>
</div>
<div class="col-md-4">
<h6><i class="fas fa-check-circle"></i> Validazione Associazioni</h6>
<div class="d-grid gap-2">
<button class="btn btn-info" @onclick="ValidateAllAssociations" disabled="@isProcessing">
<i class="fas fa-search"></i> Verifica Associazioni Non Valide
</button>
<button class="btn btn-warning" @onclick="CleanupInvalidAssociations" disabled="@isProcessing">
<i class="fas fa-broom"></i> Pulisci Associazioni Non Valide
</button>
</div>
</div>
<div class="col-md-4">
<h6><i class="fas fa-download"></i> Esportazione</h6>
<div class="d-grid gap-2">
<button class="btn btn-success" @onclick="ExportToCsv" disabled="@isProcessing">
<i class="fas fa-file-csv"></i> Esporta in CSV
</button>
<button class="btn btn-outline-primary" @onclick="ShowImportDialog" disabled="@isProcessing">
<i class="fas fa-upload"></i> Importa da CSV
</button>
</div>
</div>
</div>
@if (isProcessing)
{
<div class="mt-3">
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 100%">
@processingMessage
</div>
</div>
</div>
}
@if (!string.IsNullOrEmpty(operationMessage))
{
<div class="alert @(operationMessageType == "success" ? "alert-success" : operationMessageType == "warning" ? "alert-warning" : "alert-danger") mt-3">
<i class="fas @(operationMessageType == "success" ? "fa-check-circle" : operationMessageType == "warning" ? "fa-exclamation-triangle" : "fa-exclamation-circle")"></i>
@operationMessage
</div>
}
</div>
</div>
</div>
</div>
<!-- Statistiche -->
<div class="row mb-4">
<div class="col-md-3">
@@ -289,6 +356,37 @@
}
</div>
<!-- Modal di Conferma -->
@if (showConfirmModal)
{
<div class="modal fade show d-block" style="background-color: rgba(0,0,0,0.5);">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-exclamation-triangle text-warning"></i> Conferma Eliminazione
</h5>
</div>
<div class="modal-body">
<p>@confirmMessage</p>
<div class="alert alert-warning">
<i class="fas fa-info-circle"></i>
<strong>Attenzione:</strong> Questa operazione eliminerà definitivamente le associazioni dal database e non potrà essere annullata.
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="CancelClearConfirmation">
<i class="fas fa-times"></i> Annulla
</button>
<button type="button" class="btn btn-danger" @onclick="ConfirmClearAssociations">
<i class="fas fa-trash"></i> Conferma Eliminazione
</button>
</div>
</div>
</div>
</div>
}
@code {
private List<RecordAssociation> allAssociations = new();
private List<RecordAssociation> filteredAssociations = new();
@@ -305,6 +403,17 @@
private int pageSize = 25;
private int totalPages => (int)Math.Ceiling((double)filteredAssociations.Count / pageSize);
// Gestione operazioni
private bool isProcessing = false;
private string processingMessage = "";
private string operationMessage = "";
private string operationMessageType = "";
// Modal di conferma
private bool showConfirmModal = false;
private bool isDeleteAll = false;
private string confirmMessage = "";
protected override async Task OnInitializedAsync()
{
await RefreshAssociations();
@@ -532,4 +641,210 @@
await JSRuntime.InvokeVoidAsync("alert", $"Errore nell'esportazione: {ex.Message}");
}
}
private void ShowClearConfirmation(bool deleteAll)
{
isDeleteAll = deleteAll;
if (deleteAll)
{
confirmMessage = "Sei sicuro di voler eliminare TUTTE le associazioni dal sistema? Questa operazione non può essere annullata.";
}
else
{
var filteredCount = filteredAssociations.Count;
if (filteredCount == 0)
{
SetOperationMessage("Nessuna associazione da eliminare con i filtri attuali.", "warning");
return;
}
confirmMessage = $"Sei sicuro di voler eliminare {filteredCount} associazioni filtrate? Questa operazione non può essere annullata.";
}
showConfirmModal = true;
StateHasChanged();
}
private async Task ConfirmClearAssociations()
{
showConfirmModal = false;
isProcessing = true;
try
{
int deletedCount = 0;
if (isDeleteAll)
{
processingMessage = "Eliminazione di tutte le associazioni...";
StateHasChanged();
deletedCount = await CredentialService.ClearAllRecordAssociationsAsync();
SetOperationMessage($"Eliminate tutte le {deletedCount} associazioni dal sistema.", "success");
}
else
{
processingMessage = "Eliminazione associazioni filtrate...";
StateHasChanged();
// Elimina le associazioni filtrate una per una
foreach (var association in filteredAssociations.ToList())
{
await CredentialService.DeleteRecordAssociationAsync(association.Id);
deletedCount++;
}
SetOperationMessage($"Eliminate {deletedCount} associazioni filtrate.", "success");
}
await RefreshAssociations();
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nell'eliminazione delle associazioni");
SetOperationMessage($"Errore nell'eliminazione: {ex.Message}", "danger");
}
finally
{
isProcessing = false;
processingMessage = "";
StateHasChanged();
}
}
private void CancelClearConfirmation()
{
showConfirmModal = false;
StateHasChanged();
}
private async Task ValidateAllAssociations()
{
isProcessing = true;
processingMessage = "Validazione associazioni in corso...";
StateHasChanged();
try
{
int invalidCount = 0;
var uniqueDestinations = allAssociations
.GroupBy(a => new { a.DestinationEntity, a.RestCredentialName })
.ToList();
foreach (var group in uniqueDestinations)
{
var invalidAssociations = await CredentialService.GetInvalidRecordAssociationsAsync(
group.Key.DestinationEntity,
group.Key.RestCredentialName);
invalidCount += invalidAssociations.Count;
}
if (invalidCount == 0)
{
SetOperationMessage("Tutte le associazioni sono valide! Non sono stati trovati ID di destinazione non più esistenti.", "success");
}
else
{
SetOperationMessage($"Trovate {invalidCount} associazioni con ID di destinazione non più validi. Usa 'Pulisci Associazioni Non Valide' per rimuoverle.", "warning");
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nella validazione delle associazioni");
SetOperationMessage($"Errore nella validazione: {ex.Message}", "danger");
}
finally
{
isProcessing = false;
processingMessage = "";
StateHasChanged();
}
}
private async Task CleanupInvalidAssociations()
{
isProcessing = true;
processingMessage = "Pulizia associazioni non valide...";
StateHasChanged();
try
{
int totalCleaned = 0;
var uniqueDestinations = allAssociations
.GroupBy(a => new { a.DestinationEntity, a.RestCredentialName })
.ToList();
foreach (var group in uniqueDestinations)
{
var cleanedCount = await CredentialService.CleanupInvalidRecordAssociationsAsync(
group.Key.DestinationEntity,
group.Key.RestCredentialName);
totalCleaned += cleanedCount;
}
if (totalCleaned == 0)
{
SetOperationMessage("Nessuna associazione non valida trovata da pulire.", "info");
}
else
{
SetOperationMessage($"Pulite {totalCleaned} associazioni con ID di destinazione non più validi.", "success");
await RefreshAssociations();
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nella pulizia delle associazioni non valide");
SetOperationMessage($"Errore nella pulizia: {ex.Message}", "danger");
}
finally
{
isProcessing = false;
processingMessage = "";
StateHasChanged();
}
}
private async Task ExportToCsv()
{
isProcessing = true;
processingMessage = "Esportazione in corso...";
StateHasChanged();
try
{
await ExportAssociations();
SetOperationMessage($"Esportate {filteredAssociations.Count} associazioni in CSV.", "success");
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nell'esportazione");
SetOperationMessage($"Errore nell'esportazione: {ex.Message}", "danger");
}
finally
{
isProcessing = false;
processingMessage = "";
StateHasChanged();
}
}
private async Task ShowImportDialog()
{
// Placeholder per import dialog
await JSRuntime.InvokeVoidAsync("alert", "Funzionalità di importazione non ancora implementata.");
}
private void SetOperationMessage(string message, string type)
{
operationMessage = message;
operationMessageType = type;
StateHasChanged();
// Auto-hide success messages after 5 seconds
if (type == "success")
{
_ = Task.Delay(5000).ContinueWith(_ =>
{
operationMessage = "";
StateHasChanged();
});
}
}
}