feat: Implementato sistema di associazioni chiave per prevenire duplicati nel data coupling

BREAKING CHANGE: Rimosso completamente il vecchio sistema RecordAssociation

Modifiche principali:
- Sostituito RecordAssociation con KeyAssociation basato sui valori delle chiavi
- Implementata logica robusta di UPDATE vs INSERT basata su associazioni esistenti
- Aggiunta normalizzazione delle chiavi (.Trim()) per consistenza
- Implementato fallback nella ricerca associazioni per maggiore affidabilità
- Sostituita verifica pre-UPDATE con tentativo diretto più efficiente

Componenti modificati:
- Nuovo modello: KeyAssociation.cs con campi ottimizzati
- Nuovo servizio: KeyAssociationService.cs con metodi completi
- Aggiornato: DataCoupler.razor con logica migliorata di gestione associazioni
- Aggiornato: CredentialDbContext per gestire solo KeyAssociations
- Aggiornati: tutti i servizi di interfaccia per supportare il nuovo sistema
- Creata: pagina KeyAssociations.razor per gestione associazioni
- Aggiornato: NavMenu.razor con link alla gestione associazioni

Miglioramenti tecnici:
- Logica di UPDATE più robusta: tenta direttamente l'aggiornamento invece di verificare prima l'esistenza
- Gestione errori migliorata con cleanup automatico delle associazioni non valide
- Debug logging estensivo per troubleshooting
- Fallback nella ricerca associazioni se parametri specifici falliscono
- Normalizzazione valori chiave per prevenire problemi di whitespace

Risultato:
Il sistema ora previene correttamente i duplicati utilizzando le associazioni per decidere
se fare INSERT (nuovo record) o UPDATE (record esistente) basandosi sui valori delle chiavi.

Database:
- Creata migrazione EF per rimuovere RecordAssociations e aggiungere KeyAssociations
- Eliminati file e codice legacy non più necessari
This commit is contained in:
2025-06-29 20:44:20 +02:00
parent 2238ddc4bf
commit 04f0403f12
23 changed files with 2051 additions and 1161 deletions
+112 -68
View File
@@ -834,13 +834,24 @@
<input class="form-check-input" type="checkbox" id="useAssociations"
@bind="useRecordAssociations" />
<label class="form-check-label" for="useAssociations">
<strong>Utilizza sistema di associazioni per tracking automatico degli aggiornamenti</strong>
<br><small class="text-muted">Raccomandato: il sistema manterrà traccia delle associazioni tra record sorgente e destinazione</small>
<strong>Utilizza sistema di associazioni basato sui valori delle chiavi</strong>
<br><small class="text-muted">Raccomandato: il sistema manterrà traccia delle associazioni tra valori chiave e record di destinazione, permettendo aggiornamenti indipendentemente dalla sorgente</small>
</label>
</div>
@if (useRecordAssociations)
{
<div class="alert alert-info">
<i class="fas fa-lightbulb"></i>
<strong>Come funziona il nuovo sistema:</strong>
<ul class="mb-0 mt-2">
<li>Ogni valore di chiave univoco viene associato a un record di destinazione</li>
<li>Più sorgenti diverse possono gestire lo stesso oggetto business usando lo stesso valore chiave</li>
<li>Gli aggiornamenti avvengono automaticamente quando si trova un'associazione esistente</li>
<li>Il sistema individua automaticamente le chiavi dove possibile, ma puoi sempre scegliere manualmente</li>
</ul>
</div>
<div class="row">
<div class="col-md-6">
<label class="form-label">Campo Chiave Sorgente: <span class="text-danger">*</span></label>
@@ -2126,79 +2137,101 @@
// Genera la chiave sorgente per questo record
var sourceKey = GenerateSourceKey(record);
var currentSourceName = selectedSourceType == "database"
? (useCustomQuery ? "custom_query" : selectedTable)
: selectedSheet;
// NUOVA LOGICA: Cerca associazione esistente
// NUOVO SISTEMA: Cerca associazione esistente basata sul valore della chiave
if (useRecordAssociations && !string.IsNullOrEmpty(sourceKey))
{
var existingAssociation = await CredentialService.FindRecordAssociationAsync(
currentSourceName, sourceKey, selectedRestEntity.Name);
Logger.LogInformation("ASSOCIATION DEBUG: Cerco associazione - KeyValue: '{KeyValue}', Entity: '{Entity}', Credential: '{Credential}'",
sourceKey, selectedRestEntity.Name, selectedRestCredential);
// Cerca se esiste già un'associazione per questo valore chiave
var existingAssociation = await CredentialService.FindKeyAssociationByValueAsync(
sourceKey, selectedRestEntity.Name, selectedRestCredential);
// FALLBACK: Se non troviamo l'associazione con tutti i parametri, proviamo solo con il KeyValue
if (existingAssociation == null)
{
Logger.LogWarning("ASSOCIATION DEBUG: Associazione non trovata con parametri specifici, provo solo con KeyValue: '{KeyValue}'", sourceKey);
existingAssociation = await CredentialService.FindKeyAssociationByValueAsync(sourceKey);
if (existingAssociation != null)
{
Logger.LogWarning("ASSOCIATION DEBUG: Trovata associazione con fallback - ID: {AssociationId}, Entity: '{Entity}', Credential: '{Credential}'",
existingAssociation.Id, existingAssociation.DestinationEntity, existingAssociation.RestCredentialName);
// Verifica se l'associazione trovata è compatibile
if (existingAssociation.DestinationEntity != selectedRestEntity.Name ||
existingAssociation.RestCredentialName != selectedRestCredential)
{
Logger.LogWarning("ASSOCIATION DEBUG: Associazione non compatibile - Entity: '{FoundEntity}' vs '{ExpectedEntity}', Credential: '{FoundCredential}' vs '{ExpectedCredential}'",
existingAssociation.DestinationEntity, selectedRestEntity.Name, existingAssociation.RestCredentialName, selectedRestCredential);
existingAssociation = null;
}
}
}
Logger.LogInformation("ASSOCIATION DEBUG: Associazione finale: {Found}. ID: {AssociationId}, DestinationId: '{DestinationId}', IsActive: {IsActive}",
existingAssociation != null, existingAssociation?.Id, existingAssociation?.DestinationId, existingAssociation?.IsActive);
if (existingAssociation != null && existingAssociation.IsActive)
{
// VALIDAZIONE: Verifica se l'ID di destinazione esiste ancora nel sistema target
bool destinationExists = false;
// Prova direttamente l'aggiornamento - più efficiente che verificare prima l'esistenza
Logger.LogInformation("ASSOCIATION DEBUG: Tentativo aggiornamento record esistente - DestinationId: '{DestinationId}'", existingAssociation.DestinationId);
try
{
// Usa il campo ID appropriato per cercare l'entità
var idField = GetEntityIdField(); // Potrebbe essere "DocEntry", "id", "Id", etc.
var searchKeys = new Dictionary<string, object> { { idField, existingAssociation.DestinationId } };
var foundEntities = await currentRestClient.FindEntitiesByKeysAsync(
selectedRestEntity.Name, searchKeys);
destinationExists = foundEntities != null && foundEntities.Any();
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Errore nella verifica dell'esistenza dell'entità {EntityId} - assumo che non esista", existingAssociation.DestinationId);
destinationExists = false;
}
if (!destinationExists)
{
// L'ID di destinazione non esiste più - elimina l'associazione non valida
Logger.LogWarning("ID destinazione {DestinationId} non più valido per associazione {AssociationId} - eliminazione associazione",
existingAssociation.DestinationId, existingAssociation.Id);
var updateResult = await currentRestClient.UpdateEntityAsync(
selectedRestEntity.Name, existingAssociation.DestinationId, restData);
await CredentialService.DeleteRecordAssociationAsync(existingAssociation.Id);
transferResult.Status = "error";
transferResult.Message = $"Associazione non valida eliminata (ID destinazione {existingAssociation.DestinationId} non esiste più) - creazione nuovo record";
// Procedi con la creazione di un nuovo record
goto CreateNewRecord;
if (updateResult != null)
{
updatedCount++;
transferResult.Status = "updated";
transferResult.Message = $"Record aggiornato con successo tramite associazione (ID: {existingAssociation.DestinationId})";
transferResult.EntityId = existingAssociation.DestinationId;
// Aggiorna l'associazione con la data di ultimo aggiornamento e verifica
existingAssociation.UpdatedAt = DateTime.UtcNow;
existingAssociation.LastVerifiedAt = DateTime.UtcNow;
await CredentialService.UpdateKeyAssociationAsync(existingAssociation);
Logger.LogInformation("ASSOCIATION DEBUG: Record aggiornato con successo tramite associazione: {EntityId} per valore chiave {KeyValue}",
existingAssociation.DestinationId, sourceKey);
transferResults.Add(transferResult);
recordNumber++;
continue;
}
else
{
// Update fallito ma senza eccezione - probabilmente l'entità non esiste più
Logger.LogWarning("ASSOCIATION DEBUG: Aggiornamento fallito (result null) per associazione {AssociationId} - elimino associazione e creo nuovo record", existingAssociation.Id);
goto HandleInvalidAssociation;
}
}
catch (Exception updateEx)
{
// Update fallito con eccezione - probabilmente l'entità non esiste più
Logger.LogWarning(updateEx, "ASSOCIATION DEBUG: Aggiornamento fallito per associazione {AssociationId} - elimino associazione e creo nuovo record", existingAssociation.Id);
goto HandleInvalidAssociation;
}
// L'ID di destinazione esiste - procedi con l'aggiornamento
var updateResult = await currentRestClient.UpdateEntityAsync(
selectedRestEntity.Name, existingAssociation.DestinationId, restData);
if (updateResult != null)
HandleInvalidAssociation:
// L'ID di destinazione non esiste più o l'update è fallito - elimina l'associazione non valida
try
{
updatedCount++;
transferResult.Status = "updated";
transferResult.Message = $"Record aggiornato con successo tramite associazione (ID: {existingAssociation.DestinationId})";
transferResult.EntityId = existingAssociation.DestinationId;
// Aggiorna l'associazione con la data di ultimo aggiornamento
existingAssociation.UpdatedAt = DateTime.UtcNow;
await CredentialService.UpdateRecordAssociationAsync(existingAssociation);
Logger.LogDebug("Record aggiornato tramite associazione: {EntityId} per chiave sorgente {SourceKey}",
existingAssociation.DestinationId, sourceKey);
await CredentialService.DeleteKeyAssociationAsync(existingAssociation.Id);
Logger.LogInformation("ASSOCIATION DEBUG: Associazione non valida eliminata: {AssociationId}", existingAssociation.Id);
}
else
catch (Exception delEx)
{
// Se l'aggiornamento fallisce, prova a creare un nuovo record
Logger.LogWarning("Aggiornamento fallito per associazione {AssociationId}, provo a creare nuovo record", existingAssociation.Id);
goto CreateNewRecord;
Logger.LogWarning(delEx, "Errore nell'eliminazione dell'associazione non valida {AssociationId}", existingAssociation.Id);
}
transferResults.Add(transferResult);
recordNumber++;
continue;
transferResult.Status = "info";
transferResult.Message = $"Associazione non valida eliminata (aggiornamento fallito) - creazione nuovo record";
// Procedi con la creazione di un nuovo record (non aggiungere il result qui, sarà aggiunto dopo CreateNewRecord)
}
}
@@ -2212,31 +2245,41 @@
transferResult.Status = "success";
transferResult.Message = "Record inserito con successo";
transferResult.EntityId = result.ContainsKey("id") ? result["id"]?.ToString() :
result.ContainsKey("Id") ? result["Id"]?.ToString() : null;
result.ContainsKey("Id") ? result["Id"]?.ToString() :
result.ContainsKey("DocEntry") ? result["DocEntry"]?.ToString() : null;
// Crea associazione solo se abbiamo una chiave sorgente e un ID destinazione
if (useRecordAssociations && !string.IsNullOrEmpty(sourceKey) && !string.IsNullOrEmpty(transferResult.EntityId))
{
try
{
var association = new RecordAssociation
// Determina i campi chiave automaticamente
var destinationKeyField = GetEntityIdField(); // Campo chiave nella destinazione
var association = new KeyAssociation
{
SourceName = currentSourceName,
SourceType = selectedSourceType,
SourceKey = sourceKey,
KeyValue = sourceKey,
SourceKeyField = sourceKeyField,
DestinationKeyField = destinationKeyField,
DestinationEntity = selectedRestEntity.Name,
DestinationId = transferResult.EntityId,
RestCredentialName = selectedRestCredential,
CreatedAt = DateTime.UtcNow,
LastVerifiedAt = DateTime.UtcNow,
AdditionalInfo = System.Text.Json.JsonSerializer.Serialize(new
{
TransferDate = DateTime.UtcNow,
RecordNumber = recordNumber,
MappingCount = fieldMappings.Count
MappingCount = fieldMappings.Count,
SourceType = selectedSourceType
})
};
await CredentialService.SaveRecordAssociationAsync(association);
Logger.LogDebug("Associazione creata: {SourceKey} -> {DestinationId}", sourceKey, transferResult.EntityId);
Logger.LogInformation("ASSOCIATION DEBUG: Creazione nuova associazione - KeyValue: '{KeyValue}', Entity: '{Entity}', DestinationId: '{DestinationId}', Credential: '{Credential}'",
sourceKey, selectedRestEntity.Name, transferResult.EntityId, selectedRestCredential);
var associationId = await CredentialService.SaveKeyAssociationAsync(association);
Logger.LogInformation("DEBUG: Associazione salvata con ID: {AssociationId}", associationId);
}
catch (Exception assocEx)
{
@@ -2599,7 +2642,8 @@
throw new InvalidOperationException($"Il valore del campo chiave '{sourceKeyField}' è vuoto o null per questo record.");
}
return keyValue;
// Normalizza il valore della chiave (trim e gestione case-sensitive)
return keyValue.Trim();
}
catch (Exception ex)
{
@@ -1,27 +1,129 @@
@page "/record-associations"
@page "/key-associations"
@using CredentialManager.Models
@using CredentialManager.Services
@using DataConnection.CredentialManagement.Interfaces
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.JSInterop
@inject IDataConnectionCredentialService CredentialService
@inject IJSRuntime JSRuntime
@inject ILogger<RecordAssociations> Logger
@inject ILogger<KeyAssociations> Logger
<PageTitle>Associazioni Record</PageTitle>
<PageTitle>Gestione Associazioni Chiavi</PageTitle>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<h3><i class="fas fa-link"></i> Associazioni Record</h3>
<p class="text-muted">Visualizza e gestisci le associazioni tra record sorgente e destinazione</p>
<h3><i class="fas fa-key"></i> Gestione Associazioni Chiavi</h3>
<p class="text-muted">
Visualizza e gestisci le associazioni basate sui valori delle chiavi.
Ogni associazione lega un valore di chiave univoco a un record di destinazione,
indipendentemente dalla sorgente che ha generato quel valore.
</p>
</div>
</div>
<!-- Statistiche -->
@if (statistics != null)
{
<div class="row mb-4">
<div class="col-md-2">
<div class="card bg-primary text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">@statistics.TotalAssociations</h4>
<p class="card-text">Totali</p>
</div>
<div class="align-self-center">
<i class="fas fa-link fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card bg-success text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">@statistics.ActiveAssociations</h4>
<p class="card-text">Attive</p>
</div>
<div class="align-self-center">
<i class="fas fa-check-circle fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card bg-warning text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">@statistics.InactiveAssociations</h4>
<p class="card-text">Disattive</p>
</div>
<div class="align-self-center">
<i class="fas fa-pause-circle fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card bg-info text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">@statistics.UniqueKeyValues</h4>
<p class="card-text">Chiavi Uniche</p>
</div>
<div class="align-self-center">
<i class="fas fa-key fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card bg-secondary text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">@statistics.UniqueDestinationEntities</h4>
<p class="card-text">Entità</p>
</div>
<div class="align-self-center">
<i class="fas fa-database fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card bg-dark text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h6 class="card-title">@(statistics.OldestAssociation?.ToString("dd/MM/yy") ?? "N/A")</h6>
<p class="card-text">Più Vecchia</p>
</div>
<div class="align-self-center">
<i class="fas fa-calendar fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
</div>
}
<!-- Filtri -->
<div class="row mb-4">
<div class="col-md-3">
<label class="form-label">Filtra per Sorgente:</label>
<input class="form-control" @bind="sourceFilter" @bind:event="oninput" @onkeyup="ApplyFilters" placeholder="Nome sorgente..." />
<label class="form-label">Filtra per Valore Chiave:</label>
<input class="form-control" @bind="keyValueFilter" @bind:event="oninput" @onkeyup="ApplyFilters" placeholder="Valore chiave..." />
</div>
<div class="col-md-3">
<label class="form-label">Filtra per Entità:</label>
@@ -50,36 +152,25 @@
</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
<div class="col-md-6">
<h6>Operazioni di Pulizia</h6>
<div class="btn-group me-3">
<button class="btn btn-warning" @onclick="ValidateAssociations" disabled="@isProcessing">
<i class="fas fa-check-double"></i> Valida Associazioni
</button>
<button class="btn btn-danger" @onclick="() => ShowClearConfirmation(true)" disabled="@isProcessing">
<i class="fas fa-trash"></i> Elimina TUTTE le Associazioni
<button class="btn btn-danger" @onclick="CleanupInvalidAssociations" disabled="@isProcessing">
<i class="fas fa-broom"></i> Pulisci Non Valide
</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
<div class="col-md-6">
<h6>Operazioni Avanzate</h6>
<div class="btn-group">
<button class="btn btn-info" @onclick="ExportAssociations" disabled="@isProcessing">
<i class="fas fa-download"></i> Esporta CSV
</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 class="btn btn-danger" @onclick="ClearAllAssociations" disabled="@isProcessing">
<i class="fas fa-trash-alt"></i> Elimina Tutte
</button>
</div>
</div>
@@ -89,18 +180,17 @@
{
<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 class="progress-bar progress-bar-striped progress-bar-animated" style="width: 100%"></div>
</div>
<small class="text-muted">@processingMessage</small>
</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>
<div class="alert alert-@operationMessageType alert-dismissible fade show mt-3" role="alert">
@operationMessage
<button type="button" class="btn-close" @onclick="() => operationMessage = string.Empty"></button>
</div>
}
</div>
@@ -108,70 +198,6 @@
</div>
</div>
<!-- Statistiche -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card bg-primary text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">@filteredAssociations.Count</h4>
<p class="card-text">Associazioni Totali</p>
</div>
<div class="align-self-center">
<i class="fas fa-link fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">@filteredAssociations.Where(a => a.IsActive).Count()</h4>
<p class="card-text">Attive</p>
</div>
<div class="align-self-center">
<i class="fas fa-check-circle fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-warning text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">@filteredAssociations.Where(a => !a.IsActive).Count()</h4>
<p class="card-text">Disattivate</p>
</div>
<div class="align-self-center">
<i class="fas fa-pause-circle fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-info text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">@filteredAssociations.Select(a => a.SourceName).Distinct().Count()</h4>
<p class="card-text">Sorgenti Diverse</p>
</div>
<div class="align-self-center">
<i class="fas fa-database fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Tabella Associazioni -->
@if (isLoading)
{
@@ -188,7 +214,7 @@
<i class="fas fa-info-circle"></i>
@if (!allAssociations.Any())
{
<span>Nessuna associazione trovata. Le associazioni vengono create automaticamente durante il trasferimento dati.</span>
<span>Nessuna associazione trovata. Le associazioni vengono create automaticamente durante il trasferimento dati quando il sistema di associazioni è abilitato.</span>
}
else
{
@@ -201,7 +227,7 @@
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-table"></i> Associazioni Record
<i class="fas fa-table"></i> Associazioni Chiavi
<span class="badge bg-primary ms-2">@filteredAssociations.Count</span>
</h5>
</div>
@@ -210,15 +236,15 @@
<table class="table table-striped table-hover mb-0">
<thead class="table-dark">
<tr>
<th>Sorgente</th>
<th>Tipo</th>
<th>Chiave Sorgente</th>
<th>Valore Chiave</th>
<th>Campo Sorgente</th>
<th>Campo Destinazione</th>
<th>Entità Destinazione</th>
<th>ID Destinazione</th>
<th>Credenziale REST</th>
<th>Credenziale</th>
<th>Stato</th>
<th>Creata</th>
<th>Aggiornata</th>
<th>Verificata</th>
<th>Azioni</th>
</tr>
</thead>
@@ -227,15 +253,13 @@
{
<tr class="@(association.IsActive ? "" : "table-secondary")">
<td>
<strong>@association.SourceName</strong>
<code class="small">@association.KeyValue</code>
</td>
<td>
<span class="badge @(association.SourceType == "database" ? "bg-primary" : "bg-info")">
@association.SourceType
</span>
<span class="badge bg-info">@association.SourceKeyField</span>
</td>
<td>
<code class="small">@association.SourceKey</code>
<span class="badge bg-secondary">@association.DestinationKeyField</span>
</td>
<td>
<strong>@association.DestinationEntity</strong>
@@ -267,32 +291,29 @@
</td>
<td>
<small class="text-muted">
@(association.UpdatedAt?.ToString("dd/MM/yyyy HH:mm") ?? "-")
@(association.LastVerifiedAt?.ToString("dd/MM/yyyy HH:mm") ?? "Mai")
</small>
</td>
<td>
<div class="btn-group btn-group-sm">
@if (association.IsActive)
{
<button class="btn btn-warning" @onclick="() => DeactivateAssociation(association.Id)" title="Disattiva">
<button class="btn btn-outline-warning" @onclick="() => DeactivateAssociation(association.Id)" title="Disattiva">
<i class="fas fa-pause"></i>
</button>
}
else
{
<button class="btn btn-success" @onclick="() => ActivateAssociation(association.Id)" title="Riattiva">
<button class="btn btn-outline-success" @onclick="() => ActivateAssociation(association.Id)" title="Riattiva">
<i class="fas fa-play"></i>
</button>
}
<button class="btn btn-danger" @onclick="() => DeleteAssociation(association.Id)" title="Elimina definitivamente">
<button class="btn btn-outline-info" @onclick="() => ShowAssociationDetails(association)" title="Dettagli">
<i class="fas fa-info"></i>
</button>
<button class="btn btn-outline-danger" @onclick="() => DeleteAssociation(association.Id)" title="Elimina">
<i class="fas fa-trash"></i>
</button>
@if (!string.IsNullOrEmpty(association.AdditionalInfo))
{
<button class="btn btn-info" @onclick="() => ShowAdditionalInfo(association)" title="Mostra dettagli">
<i class="fas fa-info"></i>
</button>
}
</div>
</td>
</tr>
@@ -304,116 +325,54 @@
</div>
<!-- Paginazione -->
@if (filteredAssociations.Count > pageSize)
@if (totalPages > 1)
{
<nav class="mt-3">
<nav aria-label="Paginazione associazioni" class="mt-3">
<ul class="pagination justify-content-center">
<li class="page-item @(currentPage == 1 ? "disabled" : "")">
<button class="page-link" @onclick="() => ChangePage(currentPage - 1)">Precedente</button>
<a class="page-link" @onclick="() => ChangePage(currentPage - 1)">Precedente</a>
</li>
@for (int i = Math.Max(1, currentPage - 2); i <= Math.Min(totalPages, currentPage + 2); i++)
{
var pageNum = i;
<li class="page-item @(currentPage == pageNum ? "active" : "")">
<button class="page-link" @onclick="() => ChangePage(pageNum)">@pageNum</button>
<li class="page-item @(i == currentPage ? "active" : "")">
<a class="page-link" @onclick="() => ChangePage(i)">@i</a>
</li>
}
<li class="page-item @(currentPage == totalPages ? "disabled" : "")">
<button class="page-link" @onclick="() => ChangePage(currentPage + 1)">Successivo</button>
<a class="page-link" @onclick="() => ChangePage(currentPage + 1)">Successiva</a>
</li>
</ul>
</nav>
}
}
<!-- Azioni di massa -->
@if (filteredAssociations.Any())
{
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h6 class="mb-0">Azioni di Massa</h6>
</div>
<div class="card-body">
<div class="btn-group">
<button class="btn btn-warning" @onclick="DeactivateAllInactive">
<i class="fas fa-pause"></i> Disattiva Tutte Inattive
</button>
<button class="btn btn-danger" @onclick="DeleteAllInactive">
<i class="fas fa-trash"></i> Elimina Tutte Disattivate
</button>
<button class="btn btn-info" @onclick="ExportAssociations">
<i class="fas fa-download"></i> Esporta CSV
</button>
</div>
</div>
</div>
</div>
</div>
}
</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();
private List<RecordAssociation> pagedAssociations = new();
private bool isLoading = true;
// Dati
private List<KeyAssociation> allAssociations = new();
private List<KeyAssociation> filteredAssociations = new();
private List<KeyAssociation> pagedAssociations = new();
private AssociationStatistics? statistics;
// Filtri
private string sourceFilter = "";
private string keyValueFilter = "";
private string entityFilter = "";
private string credentialFilter = "";
// Paginazione
private int currentPage = 1;
private int pageSize = 25;
private int totalPages => (int)Math.Ceiling((double)filteredAssociations.Count / pageSize);
private int totalPages = 1;
// Gestione operazioni
// Stato
private bool isLoading = true;
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();
@@ -421,27 +380,32 @@
private async Task RefreshAssociations()
{
isLoading = true;
operationMessage = "";
StateHasChanged();
try
{
isLoading = true;
allAssociations = await CredentialService.GetAllActiveRecordAssociationsAsync();
allAssociations = await CredentialService.GetAllKeyAssociationsAsync();
statistics = await CredentialService.GetKeyAssociationStatisticsAsync();
ApplyFilters();
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nel caricamento delle associazioni");
await JSRuntime.InvokeVoidAsync("alert", $"Errore nel caricamento delle associazioni: {ex.Message}");
SetOperationMessage($"Errore nel caricamento: {ex.Message}", "danger");
}
finally
{
isLoading = false;
StateHasChanged();
}
}
private void ApplyFilters()
{
filteredAssociations = allAssociations.Where(a =>
(string.IsNullOrEmpty(sourceFilter) || a.SourceName.Contains(sourceFilter, StringComparison.OrdinalIgnoreCase)) &&
(string.IsNullOrEmpty(keyValueFilter) || a.KeyValue.Contains(keyValueFilter, StringComparison.OrdinalIgnoreCase)) &&
(string.IsNullOrEmpty(entityFilter) || a.DestinationEntity.Contains(entityFilter, StringComparison.OrdinalIgnoreCase)) &&
(string.IsNullOrEmpty(credentialFilter) || a.RestCredentialName.Contains(credentialFilter, StringComparison.OrdinalIgnoreCase))
).OrderByDescending(a => a.CreatedAt).ToList();
@@ -453,7 +417,7 @@
private void ClearFilters()
{
sourceFilter = "";
keyValueFilter = "";
entityFilter = "";
credentialFilter = "";
ApplyFilters();
@@ -470,6 +434,7 @@
private void UpdatePagedAssociations()
{
totalPages = (int)Math.Ceiling((double)filteredAssociations.Count / pageSize);
var startIndex = (currentPage - 1) * pageSize;
pagedAssociations = filteredAssociations.Skip(startIndex).Take(pageSize).ToList();
}
@@ -480,21 +445,21 @@
{
try
{
var success = await CredentialService.DeactivateRecordAssociationAsync(id);
var success = await CredentialService.DeactivateKeyAssociationAsync(id);
if (success)
{
await JSRuntime.InvokeVoidAsync("alert", "Associazione disattivata con successo!");
SetOperationMessage("Associazione disattivata con successo!", "success");
await RefreshAssociations();
}
else
{
await JSRuntime.InvokeVoidAsync("alert", "Errore nella disattivazione dell'associazione.");
SetOperationMessage("Errore nella disattivazione dell'associazione.", "danger");
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nella disattivazione dell'associazione {Id}", id);
await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}");
SetOperationMessage($"Errore: {ex.Message}", "danger");
}
}
}
@@ -507,214 +472,79 @@
if (association != null)
{
association.IsActive = true;
var success = await CredentialService.UpdateRecordAssociationAsync(association);
association.UpdatedAt = DateTime.UtcNow;
var success = await CredentialService.UpdateKeyAssociationAsync(association);
if (success)
{
await JSRuntime.InvokeVoidAsync("alert", "Associazione riattivata con successo!");
SetOperationMessage("Associazione riattivata con successo!", "success");
await RefreshAssociations();
}
else
{
await JSRuntime.InvokeVoidAsync("alert", "Errore nella riattivazione dell'associazione.");
SetOperationMessage("Errore nella riattivazione dell'associazione.", "danger");
}
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nella riattivazione dell'associazione {Id}", id);
await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}");
SetOperationMessage($"Errore: {ex.Message}", "danger");
}
}
private async Task DeleteAssociation(int id)
{
if (await JSRuntime.InvokeAsync<bool>("confirm", "Sei sicuro di voler eliminare definitivamente questa associazione? Questa azione non può essere annullata."))
if (await JSRuntime.InvokeAsync<bool>("confirm", "Sei sicuro di voler eliminare questa associazione? Questa operazione non può essere annullata."))
{
try
{
var success = await CredentialService.DeleteRecordAssociationAsync(id);
var success = await CredentialService.DeleteKeyAssociationAsync(id);
if (success)
{
await JSRuntime.InvokeVoidAsync("alert", "Associazione eliminata con successo!");
SetOperationMessage("Associazione eliminata con successo!", "success");
await RefreshAssociations();
}
else
{
await JSRuntime.InvokeVoidAsync("alert", "Errore nell'eliminazione dell'associazione.");
SetOperationMessage("Errore nell'eliminazione dell'associazione.", "danger");
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nell'eliminazione dell'associazione {Id}", id);
await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}");
SetOperationMessage($"Errore: {ex.Message}", "danger");
}
}
}
private async Task ShowAdditionalInfo(RecordAssociation association)
private async Task ShowAssociationDetails(KeyAssociation association)
{
var info = $"Informazioni aggiuntive per l'associazione:\n\n";
var info = $"Dettagli associazione:\n\n";
info += $"ID: {association.Id}\n";
info += $"Sorgente: {association.SourceName} ({association.SourceType})\n";
info += $"Chiave Sorgente: {association.SourceKey}\n";
info += $"Destinazione: {association.DestinationEntity}\n";
info += $"Valore Chiave: {association.KeyValue}\n";
info += $"Campo Sorgente: {association.SourceKeyField}\n";
info += $"Campo Destinazione: {association.DestinationKeyField}\n";
info += $"Entità: {association.DestinationEntity}\n";
info += $"ID Destinazione: {association.DestinationId}\n";
info += $"Credenziale REST: {association.RestCredentialName}\n";
info += $"Creata: {association.CreatedAt}\n";
info += $"Credenziale: {association.RestCredentialName}\n";
info += $"Creata: {association.CreatedAt:dd/MM/yyyy HH:mm}\n";
if (association.UpdatedAt.HasValue)
info += $"Aggiornata: {association.UpdatedAt}\n";
info += $"Aggiornata: {association.UpdatedAt:dd/MM/yyyy HH:mm}\n";
if (association.LastVerifiedAt.HasValue)
info += $"Verificata: {association.LastVerifiedAt:dd/MM/yyyy HH:mm}\n";
info += $"Stato: {(association.IsActive ? "Attiva" : "Disattivata")}\n";
if (!string.IsNullOrEmpty(association.SourcesInfo))
info += $"\nSorgenti:\n{association.SourcesInfo}\n";
if (!string.IsNullOrEmpty(association.AdditionalInfo))
info += $"\nInformazioni aggiuntive:\n{association.AdditionalInfo}";
await JSRuntime.InvokeVoidAsync("alert", info);
}
private async Task DeactivateAllInactive()
{
if (await JSRuntime.InvokeAsync<bool>("confirm", "Sei sicuro di voler disattivare tutte le associazioni che non sono attualmente in uso?"))
{
try
{
// Implementa logica per disattivare associazioni inattive
await JSRuntime.InvokeVoidAsync("alert", "Funzionalità in via di sviluppo.");
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nella disattivazione di massa");
await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}");
}
}
}
private async Task DeleteAllInactive()
{
if (await JSRuntime.InvokeAsync<bool>("confirm", "Sei sicuro di voler eliminare definitivamente tutte le associazioni disattivate? Questa azione non può essere annullata."))
{
try
{
var inactiveAssociations = allAssociations.Where(a => !a.IsActive).ToList();
var deletedCount = 0;
foreach (var association in inactiveAssociations)
{
if (await CredentialService.DeleteRecordAssociationAsync(association.Id))
{
deletedCount++;
}
}
await JSRuntime.InvokeVoidAsync("alert", $"Eliminate {deletedCount} associazioni disattivate.");
await RefreshAssociations();
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nell'eliminazione di massa");
await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}");
}
}
}
private async Task ExportAssociations()
{
try
{
var csv = "Sorgente,Tipo,Chiave Sorgente,Entità Destinazione,ID Destinazione,Credenziale REST,Stato,Creata,Aggiornata\n";
foreach (var association in filteredAssociations)
{
csv += $"\"{association.SourceName}\",\"{association.SourceType}\",\"{association.SourceKey}\",";
csv += $"\"{association.DestinationEntity}\",\"{association.DestinationId}\",\"{association.RestCredentialName}\",";
csv += $"\"{(association.IsActive ? "Attiva" : "Disattivata")}\",\"{association.CreatedAt:dd/MM/yyyy HH:mm}\",";
csv += $"\"{(association.UpdatedAt?.ToString("dd/MM/yyyy HH:mm") ?? "")}\"\n";
}
var fileName = $"associazioni_record_{DateTime.Now:yyyyMMdd_HHmmss}.csv";
var bytes = System.Text.Encoding.UTF8.GetBytes(csv);
var base64 = Convert.ToBase64String(bytes);
await JSRuntime.InvokeVoidAsync("downloadFile", fileName, base64);
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nell'esportazione delle associazioni");
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()
private async Task ValidateAssociations()
{
isProcessing = true;
processingMessage = "Validazione associazioni in corso...";
@@ -729,7 +559,7 @@
foreach (var group in uniqueDestinations)
{
var invalidAssociations = await CredentialService.GetInvalidRecordAssociationsAsync(
var invalidAssociations = await CredentialService.GetInvalidKeyAssociationsAsync(
group.Key.DestinationEntity,
group.Key.RestCredentialName);
invalidCount += invalidAssociations.Count;
@@ -741,7 +571,7 @@
}
else
{
SetOperationMessage($"Trovate {invalidCount} associazioni con ID di destinazione non più validi. Usa 'Pulisci Associazioni Non Valide' per rimuoverle.", "warning");
SetOperationMessage($"Trovate {invalidCount} associazioni con ID di destinazione non più validi. Usa 'Pulisci Non Valide' per rimuoverle.", "warning");
}
}
catch (Exception ex)
@@ -772,7 +602,7 @@
foreach (var group in uniqueDestinations)
{
var cleanedCount = await CredentialService.CleanupInvalidRecordAssociationsAsync(
var cleanedCount = await CredentialService.CleanupInvalidKeyAssociationsAsync(
group.Key.DestinationEntity,
group.Key.RestCredentialName);
totalCleaned += cleanedCount;
@@ -784,13 +614,13 @@
}
else
{
SetOperationMessage($"Pulite {totalCleaned} associazioni con ID di destinazione non più validi.", "success");
SetOperationMessage($"Pulite {totalCleaned} associazioni non valide!", "success");
await RefreshAssociations();
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nella pulizia delle associazioni non valide");
Logger.LogError(ex, "Errore nella pulizia delle associazioni");
SetOperationMessage($"Errore nella pulizia: {ex.Message}", "danger");
}
finally
@@ -801,50 +631,77 @@
}
}
private async Task ExportToCsv()
private async Task ExportAssociations()
{
isProcessing = true;
processingMessage = "Esportazione in corso...";
StateHasChanged();
try
{
await ExportAssociations();
SetOperationMessage($"Esportate {filteredAssociations.Count} associazioni in CSV.", "success");
var csv = "Valore Chiave,Campo Sorgente,Campo Destinazione,Entità Destinazione,ID Destinazione,Credenziale,Stato,Creata,Aggiornata,Verificata\n";
foreach (var association in filteredAssociations)
{
csv += $"\"{association.KeyValue}\",\"{association.SourceKeyField}\",\"{association.DestinationKeyField}\",";
csv += $"\"{association.DestinationEntity}\",\"{association.DestinationId}\",\"{association.RestCredentialName}\",";
csv += $"\"{(association.IsActive ? "Attiva" : "Disattivata")}\",\"{association.CreatedAt:dd/MM/yyyy HH:mm}\",";
csv += $"\"{(association.UpdatedAt?.ToString("dd/MM/yyyy HH:mm") ?? "")}\",";
csv += $"\"{(association.LastVerifiedAt?.ToString("dd/MM/yyyy HH:mm") ?? "")}\"\n";
}
var bytes = System.Text.Encoding.UTF8.GetBytes(csv);
var fileName = $"associazioni_chiavi_{DateTime.Now:yyyyMMdd_HHmmss}.csv";
await JSRuntime.InvokeVoidAsync("downloadFile", fileName, "text/csv", System.Convert.ToBase64String(bytes));
SetOperationMessage($"File {fileName} esportato con successo!", "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()
private async Task ClearAllAssociations()
{
// Placeholder per import dialog
await JSRuntime.InvokeVoidAsync("alert", "Funzionalità di importazione non ancora implementata.");
if (await JSRuntime.InvokeAsync<bool>("confirm", "ATTENZIONE: Questa operazione eliminerà TUTTE le associazioni dal sistema. Sei assolutamente sicuro di voler procedere?"))
{
try
{
isProcessing = true;
processingMessage = "Eliminazione di tutte le associazioni...";
StateHasChanged();
var deletedCount = await CredentialService.ClearAllKeyAssociationsAsync();
SetOperationMessage($"Eliminate {deletedCount} associazioni dal sistema!", "success");
await RefreshAssociations();
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nell'eliminazione di massa");
SetOperationMessage($"Errore: {ex.Message}", "danger");
}
finally
{
isProcessing = false;
processingMessage = "";
StateHasChanged();
}
}
}
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();
});
}
}
}
<script>
window.downloadFile = function (fileName, contentType, data) {
const link = document.createElement('a');
link.download = fileName;
link.href = 'data:' + contentType + ';base64,' + data;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
</script>
+316
View File
@@ -0,0 +1,316 @@
@page "/test-associations"
@using CredentialManager.Models
@using DataConnection.CredentialManagement.Interfaces
@inject IDataConnectionCredentialService CredentialService
@inject ILogger<TestAssociations> Logger
<PageTitle>Test Associazioni</PageTitle>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<h3><i class="fas fa-vial"></i> Test Sistema Associazioni Chiave</h3>
<div class="row mt-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5><i class="fas fa-plus"></i> Crea Associazione Test</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Valore Chiave:</label>
<input type="text" class="form-control" @bind="testKeyValue" placeholder="es. 12345" />
</div>
<div class="mb-3">
<label class="form-label">Entità Destinazione:</label>
<input type="text" class="form-control" @bind="testDestinationEntity" placeholder="es. BusinessPartners" />
</div>
<div class="mb-3">
<label class="form-label">ID Destinazione:</label>
<input type="text" class="form-control" @bind="testDestinationId" placeholder="es. BP001" />
</div>
<div class="mb-3">
<label class="form-label">Credenziale REST:</label>
<input type="text" class="form-control" @bind="testRestCredential" placeholder="es. SAP_B1" />
</div>
<button class="btn btn-primary" @onclick="CreateTestAssociation">
<i class="fas fa-save"></i> Crea Associazione
</button>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5><i class="fas fa-search"></i> Cerca Associazione</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Valore Chiave da Cercare:</label>
<input type="text" class="form-control" @bind="searchKeyValue" placeholder="es. 12345" />
</div>
<div class="mb-3">
<label class="form-label">Entità Destinazione:</label>
<input type="text" class="form-control" @bind="searchDestinationEntity" placeholder="es. BusinessPartners" />
</div>
<div class="mb-3">
<label class="form-label">Credenziale REST:</label>
<input type="text" class="form-control" @bind="searchRestCredential" placeholder="es. SAP_B1" />
</div>
<button class="btn btn-info" @onclick="SearchAssociation">
<i class="fas fa-search"></i> Cerca
</button>
</div>
</div>
</div>
</div>
@if (!string.IsNullOrEmpty(resultMessage))
{
<div class="alert @(resultType == "success" ? "alert-success" : resultType == "error" ? "alert-danger" : "alert-info") mt-4">
<i class="fas @(resultType == "success" ? "fa-check-circle" : resultType == "error" ? "fa-exclamation-triangle" : "fa-info-circle")"></i>
@resultMessage
</div>
}
@if (foundAssociation != null)
{
<div class="card mt-4">
<div class="card-header">
<h5><i class="fas fa-link"></i> Associazione Trovata</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<strong>ID:</strong> @foundAssociation.Id<br>
<strong>Valore Chiave:</strong> @foundAssociation.KeyValue<br>
<strong>Campo Chiave Sorgente:</strong> @foundAssociation.SourceKeyField<br>
<strong>Campo Chiave Destinazione:</strong> @foundAssociation.DestinationKeyField<br>
</div>
<div class="col-md-6">
<strong>Entità Destinazione:</strong> @foundAssociation.DestinationEntity<br>
<strong>ID Destinazione:</strong> @foundAssociation.DestinationId<br>
<strong>Credenziale REST:</strong> @foundAssociation.RestCredentialName<br>
<strong>Attiva:</strong> @(foundAssociation.IsActive ? "Sì" : "No")<br>
</div>
</div>
<div class="row mt-2">
<div class="col-12">
<strong>Creata:</strong> @foundAssociation.CreatedAt.ToString("dd/MM/yyyy HH:mm:ss")<br>
@if (foundAssociation.UpdatedAt.HasValue)
{
<strong>Aggiornata:</strong> @foundAssociation.UpdatedAt.Value.ToString("dd/MM/yyyy HH:mm:ss")<br>
}
@if (foundAssociation.LastVerifiedAt.HasValue)
{
<strong>Ultima Verifica:</strong> @foundAssociation.LastVerifiedAt.Value.ToString("dd/MM/yyyy HH:mm:ss")<br>
}
</div>
</div>
</div>
</div>
}
<div class="card mt-4">
<div class="card-header">
<h5><i class="fas fa-list"></i> Tutte le Associazioni Attive</h5>
</div>
<div class="card-body">
<button class="btn btn-secondary mb-3" @onclick="LoadAllAssociations">
<i class="fas fa-refresh"></i> Carica Associazioni
</button>
@if (allAssociations != null && allAssociations.Any())
{
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>Valore Chiave</th>
<th>Entità</th>
<th>ID Destinazione</th>
<th>Credenziale</th>
<th>Creata</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
@foreach (var assoc in allAssociations)
{
<tr>
<td>@assoc.Id</td>
<td>@assoc.KeyValue</td>
<td>@assoc.DestinationEntity</td>
<td>@assoc.DestinationId</td>
<td>@assoc.RestCredentialName</td>
<td>@assoc.CreatedAt.ToString("dd/MM/yyyy HH:mm")</td>
<td>
<button class="btn btn-sm btn-danger" @onclick="() => DeleteAssociation(assoc.Id)">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
}
else if (allAssociations != null)
{
<div class="alert alert-info">
<i class="fas fa-info-circle"></i> Nessuna associazione trovata.
</div>
}
</div>
</div>
</div>
</div>
</div>
@code {
private string testKeyValue = "";
private string testDestinationEntity = "";
private string testDestinationId = "";
private string testRestCredential = "";
private string searchKeyValue = "";
private string searchDestinationEntity = "";
private string searchRestCredential = "";
private string resultMessage = "";
private string resultType = "";
private KeyAssociation? foundAssociation;
private List<KeyAssociation>? allAssociations;
private async Task CreateTestAssociation()
{
try
{
if (string.IsNullOrEmpty(testKeyValue) || string.IsNullOrEmpty(testDestinationEntity) ||
string.IsNullOrEmpty(testDestinationId) || string.IsNullOrEmpty(testRestCredential))
{
resultMessage = "Tutti i campi sono obbligatori.";
resultType = "error";
return;
}
var association = new KeyAssociation
{
KeyValue = testKeyValue,
SourceKeyField = "test_field",
DestinationKeyField = "id",
DestinationEntity = testDestinationEntity,
DestinationId = testDestinationId,
RestCredentialName = testRestCredential,
CreatedAt = DateTime.UtcNow,
LastVerifiedAt = DateTime.UtcNow,
AdditionalInfo = "{\"test\": true}"
};
var id = await CredentialService.SaveKeyAssociationAsync(association);
resultMessage = $"Associazione creata con successo! ID: {id}";
resultType = "success";
Logger.LogInformation("Associazione test creata: ID={Id}, KeyValue={KeyValue}", id, testKeyValue);
}
catch (Exception ex)
{
resultMessage = $"Errore nella creazione: {ex.Message}";
resultType = "error";
Logger.LogError(ex, "Errore nella creazione dell'associazione test");
}
}
private async Task SearchAssociation()
{
try
{
if (string.IsNullOrEmpty(searchKeyValue))
{
resultMessage = "Valore chiave obbligatorio per la ricerca.";
resultType = "error";
return;
}
foundAssociation = null;
if (!string.IsNullOrEmpty(searchDestinationEntity) && !string.IsNullOrEmpty(searchRestCredential))
{
foundAssociation = await CredentialService.FindKeyAssociationByValueAsync(
searchKeyValue, searchDestinationEntity, searchRestCredential);
}
else
{
foundAssociation = await CredentialService.FindKeyAssociationByValueAsync(searchKeyValue);
}
if (foundAssociation != null)
{
resultMessage = "Associazione trovata!";
resultType = "success";
Logger.LogInformation("Associazione trovata: ID={Id} per KeyValue={KeyValue}", foundAssociation.Id, searchKeyValue);
}
else
{
resultMessage = "Nessuna associazione trovata con i criteri specificati.";
resultType = "info";
Logger.LogInformation("Nessuna associazione trovata per KeyValue={KeyValue}", searchKeyValue);
}
}
catch (Exception ex)
{
resultMessage = $"Errore nella ricerca: {ex.Message}";
resultType = "error";
Logger.LogError(ex, "Errore nella ricerca dell'associazione");
}
}
private async Task LoadAllAssociations()
{
try
{
allAssociations = await CredentialService.GetAllActiveKeyAssociationsAsync();
resultMessage = $"Caricate {allAssociations.Count} associazioni attive.";
resultType = "info";
}
catch (Exception ex)
{
resultMessage = $"Errore nel caricamento: {ex.Message}";
resultType = "error";
Logger.LogError(ex, "Errore nel caricamento delle associazioni");
}
}
private async Task DeleteAssociation(int id)
{
try
{
var result = await CredentialService.DeleteKeyAssociationAsync(id);
if (result)
{
resultMessage = $"Associazione {id} eliminata con successo.";
resultType = "success";
await LoadAllAssociations(); // Ricarica la lista
}
else
{
resultMessage = $"Errore nell'eliminazione dell'associazione {id}.";
resultType = "error";
}
}
catch (Exception ex)
{
resultMessage = $"Errore nell'eliminazione: {ex.Message}";
resultType = "error";
Logger.LogError(ex, "Errore nell'eliminazione dell'associazione {Id}", id);
}
}
protected override async Task OnInitializedAsync()
{
await LoadAllAssociations();
}
}