feat: Implementa gestione intelligente della chiave sorgente con rilevamento PK
- Aggiunge rilevamento automatico Primary Key per connessioni database - Rimuove completamente il fallback automatico per lato sorgente - Implementa selezione manuale obbligatoria per file e sorgenti non-DB - Migliora UI con suggerimenti intelligenti e feedback visivo - Aggiunge validazione multi-livello (UI, pre-transfer, runtime) - Introduce metodo GetPrimaryKeyFieldAsync in IDatabaseManager - Modifica GenerateSourceKey per richiedere sempre campo specifico - Implementa controllo IsTransferButtonEnabled per validazione form Breaking changes: - La generazione automatica delle chiavi sorgente è stata rimossa - Il campo chiave sorgente è ora obbligatorio quando si usa il sistema associazioni Fixes: Risolve problema di discovery schema vuoto con selezione database
This commit is contained in:
@@ -610,11 +610,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sezione Mappature Correnti -->
|
||||
@if (fieldMappings.Any())
|
||||
<!-- Sezione Mappature Correnti --> @if (fieldMappings.Any())
|
||||
{
|
||||
<div class="mt-4">
|
||||
<h6>Mappature Correnti (@fieldMappings.Count)</h6>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h6>Mappature Correnti (@fieldMappings.Count)</h6>
|
||||
@if (keyFields.Any())
|
||||
{
|
||||
<small class="text-info">
|
||||
<i class="fas fa-key"></i> @keyFields.Count campo/i chiave: @string.Join(", ", keyFields)
|
||||
</small>
|
||||
}
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
@@ -626,7 +633,9 @@
|
||||
<th>Tipo REST</th>
|
||||
<th>Azioni</th>
|
||||
</tr>
|
||||
</thead> <tbody> @foreach (var mapping in fieldMappings)
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var mapping in fieldMappings)
|
||||
{
|
||||
DbColumnInfo? dbColumn = null;
|
||||
if (selectedSourceType == "database" && !string.IsNullOrEmpty(selectedTable))
|
||||
@@ -652,10 +661,118 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
} <div class="mt-3">
|
||||
}
|
||||
|
||||
<!-- Configurazione Chiave Sorgente -->
|
||||
@if (fieldMappings.Any())
|
||||
{
|
||||
<div class="mt-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-key"></i> Configurazione Chiave Sorgente
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-check mb-3">
|
||||
<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>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@if (useRecordAssociations)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Campo Chiave Sorgente: <span class="text-danger">*</span></label>
|
||||
<select class="form-select" @bind="sourceKeyField">
|
||||
<option value="">-- Seleziona Campo Chiave --</option>
|
||||
@if (!string.IsNullOrEmpty(suggestedPrimaryKey))
|
||||
{
|
||||
<option value="@suggestedPrimaryKey">@suggestedPrimaryKey (Primary Key - Consigliato)</option>
|
||||
}
|
||||
@if (selectedSourceType == "database" && databaseTables.ContainsKey(selectedTable))
|
||||
{
|
||||
@foreach (var column in databaseTables[selectedTable].Where(c => c.Name != suggestedPrimaryKey))
|
||||
{
|
||||
<option value="@column.Name">@column.Name (@column.DataType)</option>
|
||||
}
|
||||
}
|
||||
else if (selectedSourceType == "file" && fileSheets.ContainsKey(selectedSheet))
|
||||
{
|
||||
@foreach (var column in fileSheets[selectedSheet])
|
||||
{
|
||||
<option value="@column">@column</option>
|
||||
}
|
||||
}
|
||||
</select>
|
||||
@if (requiresManualKeySelection || selectedSourceType != "database")
|
||||
{
|
||||
<small class="form-text text-danger">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
Selezione del campo chiave obbligatoria. Scegli un campo che identifichi univocamente ogni record.
|
||||
</small>
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(suggestedPrimaryKey))
|
||||
{
|
||||
<small class="form-text text-success">
|
||||
<i class="fas fa-key"></i>
|
||||
Primary Key rilevata: <strong>@suggestedPrimaryKey</strong> (consigliato per l'identificazione univoca)
|
||||
</small>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@if (!string.IsNullOrEmpty(sourceKeyField))
|
||||
{
|
||||
<div class="mt-4">
|
||||
<div class="alert alert-success">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<strong>Campo chiave selezionato:</strong> @sourceKeyField
|
||||
<br><small>Questo campo verrà utilizzato per identificare univocamente i record sorgente</small>
|
||||
@if (sourceKeyField == suggestedPrimaryKey)
|
||||
{
|
||||
<br><small class="text-success"><i class="fas fa-thumbs-up"></i> Ottima scelta! Stai usando la Primary Key della tabella.</small>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="mt-4">
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<strong>Campo chiave richiesto</strong>
|
||||
<br><small>Seleziona un campo che identifichi univocamente ogni record per abilitare il sistema di associazioni.</small>
|
||||
@if (!string.IsNullOrEmpty(suggestedPrimaryKey))
|
||||
{
|
||||
<br><small class="text-info"><i class="fas fa-lightbulb"></i> Consiglio: seleziona <strong>@suggestedPrimaryKey</strong> (Primary Key rilevata)</small>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<strong>Sistema associazioni disabilitato</strong><br>
|
||||
Tutti i record verranno sempre inseriti come nuovi. Non sarà possibile tracciare aggiornamenti automatici.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<button class="btn btn-success" @onclick="StartDataTransfer" disabled="@(!fieldMappings.Any() || isTransferringData)"> @if (isTransferringData)
|
||||
<button class="btn btn-success" @onclick="StartDataTransfer" disabled="@(!IsTransferButtonEnabled() || isTransferringData)"> @if (isTransferringData)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||
<i class="fas fa-sync-alt"></i> @("Trasferimento in corso")
|
||||
@@ -672,13 +789,27 @@
|
||||
<i class="fas fa-list"></i> Riepilogo Mapping
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="text-muted">
|
||||
</div> <div class="text-muted">
|
||||
@if (fieldMappings.Any())
|
||||
{
|
||||
<small>
|
||||
<i class="fas fa-arrow-right"></i> @fieldMappings.Count mapping(s) configurati
|
||||
<i class="fas fa-arrow-right"></i> @fieldMappings.Count mapping(s) configurati<br/>
|
||||
@if (useRecordAssociations)
|
||||
{
|
||||
<span><i class="fas fa-sync-alt text-info"></i> <strong>Modalità Smart Update</strong></span>
|
||||
@if (!string.IsNullOrEmpty(sourceKeyField))
|
||||
{
|
||||
<span> (Chiave: @sourceKeyField)</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span> (Rilevamento automatico)</span>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<span><i class="fas fa-plus text-success"></i> <strong>Modalità Insert Only</strong></span>
|
||||
}
|
||||
</small>
|
||||
}
|
||||
else
|
||||
@@ -689,14 +820,95 @@
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(transferMessage))
|
||||
@if (!string.IsNullOrEmpty(transferMessage))
|
||||
{
|
||||
<div class="alert @(transferMessageType == "success" ? "alert-success" : "alert-danger") mt-3" role="alert">
|
||||
<i class="fas @(transferMessageType == "success" ? "fa-check-circle" : "fa-exclamation-circle")"></i>
|
||||
<div class="alert @(transferMessageType == "success" ? "alert-success" : transferMessageType == "warning" ? "alert-warning" : "alert-danger") mt-3" role="alert">
|
||||
<i class="fas @(transferMessageType == "success" ? "fa-check-circle" : transferMessageType == "warning" ? "fa-exclamation-triangle" : "fa-exclamation-circle")"></i>
|
||||
@transferMessage
|
||||
</div>
|
||||
} </div>
|
||||
}
|
||||
|
||||
@if (transferResults.Any())
|
||||
{
|
||||
<div class="mt-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h6><i class="fas fa-list-alt"></i> Risultati Dettagliati Trasferimento (@transferResults.Count record)</h6>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
data-bs-toggle="collapse" data-bs-target="#transferResults"
|
||||
aria-expanded="@showDetailedResults.ToString().ToLower()" aria-controls="transferResults"
|
||||
@onclick="() => showDetailedResults = !showDetailedResults">
|
||||
<i class="fas @(showDetailedResults ? "fa-chevron-up" : "fa-chevron-down")"></i>
|
||||
@(showDetailedResults ? "Nascondi" : "Mostra") Dettagli
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="collapse @(showDetailedResults ? "show" : "")" id="transferResults">
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<div class="row text-center">
|
||||
<div class="col-3">
|
||||
<small class="text-success"><i class="fas fa-check-circle"></i>
|
||||
Inseriti: @transferResults.Count(r => r.Status == "success")</small>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<small class="text-info"><i class="fas fa-edit"></i>
|
||||
Aggiornati: @transferResults.Count(r => r.Status == "updated")</small>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<small class="text-warning"><i class="fas fa-exclamation-triangle"></i>
|
||||
Duplicati: @transferResults.Count(r => r.Status == "duplicate")</small>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<small class="text-danger"><i class="fas fa-times-circle"></i>
|
||||
Errori: @transferResults.Count(r => r.Status == "error")</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body" style="max-height: 400px; overflow-y: auto;">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 10%;">#</th>
|
||||
<th style="width: 15%;">Stato</th>
|
||||
<th style="width: 20%;">ID Entità</th>
|
||||
<th style="width: 55%;">Messaggio</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var result in transferResults)
|
||||
{
|
||||
<tr class="@GetResultRowClass(result.Status)">
|
||||
<td>@result.RecordNumber</td>
|
||||
<td>
|
||||
<span class="badge @GetResultBadgeClass(result.Status)">
|
||||
<i class="fas @GetResultIcon(result.Status)"></i>
|
||||
@GetResultStatusText(result.Status)
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
@if (!string.IsNullOrEmpty(result.EntityId))
|
||||
{
|
||||
<small class="text-muted">@result.EntityId</small>
|
||||
}
|
||||
else
|
||||
{
|
||||
<small class="text-muted">-</small>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<small>@result.Message</small>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -704,7 +916,69 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Modal per la selezione del database -->
|
||||
@if (showDatabaseSelectionModal)
|
||||
{
|
||||
<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-database"></i> Seleziona Database
|
||||
</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-muted">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Il server non ha un database predefinito. Seleziona il database su cui eseguire le operazioni:
|
||||
</p>
|
||||
|
||||
@if (availableDatabases != null && availableDatabases.Any())
|
||||
{
|
||||
<div class="mb-3">
|
||||
<label for="databaseSelect" class="form-label">Database disponibili:</label>
|
||||
<select id="databaseSelect" class="form-select" @bind="selectedDatabase">
|
||||
<option value="">-- Seleziona un database --</option>
|
||||
@foreach (var db in availableDatabases)
|
||||
{
|
||||
<option value="@db">@db</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
Nessun database trovato o errore nel caricamento.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @onclick="CancelDatabaseSelection">
|
||||
<i class="fas fa-times"></i> Annulla
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" @onclick="OnDatabaseSelected"
|
||||
disabled="@string.IsNullOrEmpty(selectedDatabase)">
|
||||
<i class="fas fa-check"></i> Conferma
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
// Classe per i risultati del trasferimento
|
||||
public class TransferResult
|
||||
{
|
||||
public int RecordNumber { get; set; }
|
||||
public string Status { get; set; } = ""; // "success", "error", "updated", "duplicate"
|
||||
public string Message { get; set; } = "";
|
||||
public string? EntityId { get; set; }
|
||||
public Dictionary<string, object> RecordData { get; set; } = new();
|
||||
}
|
||||
|
||||
// Stato delle credenziali
|
||||
private List<DatabaseCredential> databaseCredentials = new();
|
||||
private List<RestApiCredential> restApiCredentials = new();
|
||||
@@ -729,7 +1003,14 @@
|
||||
// Database discovery
|
||||
private Dictionary<string, IEnumerable<DbColumnInfo>> databaseTables = new();
|
||||
private string selectedTable = "";
|
||||
private string databaseSearchTerm = ""; // File handling
|
||||
private string databaseSearchTerm = "";
|
||||
|
||||
// Database selection
|
||||
private List<string> availableDatabases = new();
|
||||
private string selectedDatabase = "";
|
||||
private bool showDatabaseSelection = false;
|
||||
private bool showDatabaseSelectionModal = false;
|
||||
private bool isLoadingDatabases = false; // File handling
|
||||
private string selectedFileName = "";
|
||||
private bool isProcessingFile = false;
|
||||
private string fileErrorMessage = "";
|
||||
@@ -748,16 +1029,25 @@
|
||||
private RestEntitySummary? selectedRestEntity = null;
|
||||
private RestEntityInfo? restEntityDetails = null;
|
||||
private string restSearchTerm = "";
|
||||
|
||||
// Mapping campi
|
||||
// Mapping campi
|
||||
private Dictionary<string, string> fieldMappings = new(); // DbColumn -> RestProperty
|
||||
private HashSet<string> keyFields = new(); // REST properties marked as keys
|
||||
private string selectedDbColumn = "";
|
||||
private string selectedRestProperty = "";
|
||||
|
||||
// Gestione chiavi sorgente e associazioni
|
||||
private string sourceKeyField = ""; // Campo che identifica univocamente il record sorgente
|
||||
private string suggestedPrimaryKey = ""; // Campo PK suggerito per database
|
||||
private bool requiresManualKeySelection = false; // Flag per indicare se è richiesta selezione manuale
|
||||
private Dictionary<string, string> sourceKeyMappings = new(); // Per CSV: mapppatura colonna -> nome campo chiave
|
||||
private bool useRecordAssociations = true; // Se utilizzare il sistema di associazioni
|
||||
|
||||
// Trasferimento dati
|
||||
private bool isTransferringData = false;
|
||||
private string transferMessage = "";
|
||||
private string transferMessageType = "";
|
||||
private List<TransferResult> transferResults = new();
|
||||
private bool showDetailedResults = false;
|
||||
|
||||
// Servizi
|
||||
private IDatabaseManager? currentDatabaseManager = null;
|
||||
@@ -1048,7 +1338,7 @@
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
}private void SelectSheet(string sheetName)
|
||||
} private void SelectSheet(string sheetName)
|
||||
{
|
||||
selectedSheet = sheetName;
|
||||
|
||||
@@ -1058,6 +1348,11 @@
|
||||
// Clear mappings when changing sheet
|
||||
ClearAllMappings();
|
||||
|
||||
// For file sources, always require manual key selection
|
||||
sourceKeyField = "";
|
||||
suggestedPrimaryKey = "";
|
||||
requiresManualKeySelection = true;
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
@@ -1178,31 +1473,39 @@
|
||||
databaseErrorMessage = $"Connessione fallita: {message}";
|
||||
return;
|
||||
} // Crea il database manager usando il factory con le credenziali complete
|
||||
Logger.LogInformation("Creando database manager per credenziale: {CredentialName}", selectedDatabaseCredential);
|
||||
currentDatabaseManager = await ConnectionFactory.CreateDatabaseManagerAsync(selectedDatabaseCredential);
|
||||
Logger.LogInformation("Database manager creato con successo");
|
||||
|
||||
Logger.LogInformation("Iniziando discovery dello schema per database {DatabaseType} con credenziale: {CredentialName}", credential.DatabaseType, selectedDatabaseCredential);
|
||||
|
||||
// Discovery dello schema
|
||||
var schema = await currentDatabaseManager.GetDatabaseSchemaAsync();
|
||||
|
||||
Logger.LogInformation("Schema discovery completato. Tipo restituito: {SchemaType}, Numero elementi: {Count}",
|
||||
schema?.GetType().Name ?? "null",
|
||||
schema?.Count() ?? 0);
|
||||
|
||||
if (schema != null)
|
||||
// Discovery dello schema con try-catch specifico
|
||||
try
|
||||
{
|
||||
foreach (var item in schema.Take(5)) // Log primi 5 elementi per debug
|
||||
var schema = await currentDatabaseManager.GetDatabaseSchemaAsync();
|
||||
|
||||
Logger.LogInformation("Schema discovery completato. Tipo restituito: {SchemaType}, Numero elementi: {Count}",
|
||||
schema?.GetType().Name ?? "null",
|
||||
schema?.Count() ?? 0);
|
||||
|
||||
databaseTables = schema as Dictionary<string, IEnumerable<DbColumnInfo>> ??
|
||||
(schema != null ? new Dictionary<string, IEnumerable<DbColumnInfo>>(schema) : new Dictionary<string, IEnumerable<DbColumnInfo>>());
|
||||
|
||||
Logger.LogInformation("Database tables dopo conversione: {Count} tabelle", databaseTables.Count);
|
||||
|
||||
if (databaseTables.Count == 0)
|
||||
{
|
||||
Logger.LogInformation("Schema item - Key: {Key}, Value type: {ValueType}, Column count: {ColumnCount}",
|
||||
item.Key,
|
||||
item.Value?.GetType().Name ?? "null",
|
||||
item.Value?.Count() ?? 0);
|
||||
// Se non ci sono tabelle, potrebbe essere perché non è stato selezionato un database specifico
|
||||
await HandleDatabaseSelectionRequired();
|
||||
return;
|
||||
}
|
||||
}
|
||||
databaseTables = schema as Dictionary<string, IEnumerable<DbColumnInfo>> ??
|
||||
(schema != null ? new Dictionary<string, IEnumerable<DbColumnInfo>>(schema) : new Dictionary<string, IEnumerable<DbColumnInfo>>());
|
||||
|
||||
Logger.LogInformation("Database tables dopo conversione: {Count} tabelle", databaseTables.Count);
|
||||
catch (Exception schemaEx)
|
||||
{
|
||||
Logger.LogError(schemaEx, "Errore specifico durante lo schema discovery");
|
||||
databaseErrorMessage = $"Errore nello schema discovery: {schemaEx.Message}";
|
||||
throw;
|
||||
}
|
||||
|
||||
isDatabaseConnected = true;
|
||||
}
|
||||
@@ -1277,11 +1580,49 @@
|
||||
{
|
||||
isConnectingRest = false;
|
||||
}
|
||||
} private void SelectTable(string tableName)
|
||||
} private async void SelectTable(string tableName)
|
||||
{
|
||||
selectedTable = tableName;
|
||||
// Clear mappings when changing table
|
||||
ClearAllMappings();
|
||||
|
||||
// Reset key field logic
|
||||
sourceKeyField = "";
|
||||
suggestedPrimaryKey = "";
|
||||
requiresManualKeySelection = false;
|
||||
|
||||
// If it's a database source, try to detect the primary key
|
||||
if (selectedSourceType == "database" && currentDatabaseManager != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var primaryKey = await currentDatabaseManager.GetPrimaryKeyFieldAsync(tableName);
|
||||
if (!string.IsNullOrEmpty(primaryKey))
|
||||
{
|
||||
suggestedPrimaryKey = primaryKey;
|
||||
// Suggest the primary key but don't auto-select it
|
||||
Logger.LogInformation("Primary key detected for table {TableName}: {PrimaryKey}", tableName, primaryKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No primary key found, require manual selection
|
||||
requiresManualKeySelection = true;
|
||||
Logger.LogInformation("No primary key found for table {TableName}, manual selection required", tableName);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error detecting primary key for table {TableName}", tableName);
|
||||
requiresManualKeySelection = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// For non-database sources, always require manual selection
|
||||
requiresManualKeySelection = true;
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
} private async Task SelectRestEntity(RestEntitySummary entity)
|
||||
{
|
||||
selectedRestEntity = entity;
|
||||
@@ -1390,23 +1731,24 @@
|
||||
|
||||
fieldMappings.Remove(selectedDbColumn);
|
||||
Logger.LogInformation("Rimosso mapping per campo: {DbColumn}", selectedDbColumn);
|
||||
}
|
||||
|
||||
private void RemoveSpecificMapping(string dbColumn)
|
||||
} private void RemoveSpecificMapping(string dbColumn)
|
||||
{
|
||||
if (fieldMappings.ContainsKey(dbColumn))
|
||||
{
|
||||
fieldMappings.Remove(dbColumn);
|
||||
Logger.LogInformation("Rimosso mapping specifico per campo: {DbColumn}", dbColumn);
|
||||
}
|
||||
} private void ClearAllMappings()
|
||||
}
|
||||
|
||||
private void ClearAllMappings()
|
||||
{
|
||||
fieldMappings.Clear();
|
||||
selectedDbColumn = "";
|
||||
selectedRestProperty = "";
|
||||
sourceKeyField = "";
|
||||
transferMessage = "";
|
||||
transferMessageType = "";
|
||||
Logger.LogInformation("Tutti i mapping sono stati cancellati");
|
||||
Logger.LogInformation("Tutti i mapping e le configurazioni sono stati cancellati");
|
||||
}
|
||||
|
||||
private void AutoMapFields()
|
||||
@@ -1436,12 +1778,20 @@
|
||||
} Logger.LogInformation("Auto-mapping completato. Creati {Count} mapping automatici", mappingsCreated);
|
||||
} private async Task ShowMappingSummary()
|
||||
{
|
||||
var summary = "Riepilogo Mapping:\n\n";
|
||||
var summary = "Riepilogo Configurazione:\n\n";
|
||||
summary += "=== MAPPING CAMPI ===\n";
|
||||
foreach (var mapping in fieldMappings)
|
||||
{
|
||||
summary += $"• {mapping.Key} → {mapping.Value}\n";
|
||||
}
|
||||
|
||||
summary += "\n=== CONFIGURAZIONE ASSOCIAZIONI ===\n";
|
||||
summary += $"• Sistema associazioni: {(useRecordAssociations ? "Abilitato" : "Disabilitato")}\n";
|
||||
if (useRecordAssociations)
|
||||
{
|
||||
summary += $"• Campo chiave sorgente: {(!string.IsNullOrEmpty(sourceKeyField) ? sourceKeyField : "Rilevamento automatico")}\n";
|
||||
}
|
||||
|
||||
await JSRuntime.InvokeVoidAsync("alert", summary);
|
||||
} private async Task StartDataTransfer()
|
||||
{
|
||||
@@ -1466,10 +1816,19 @@
|
||||
transferMessageType = "error";
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate source key field when using record associations
|
||||
if (useRecordAssociations && string.IsNullOrEmpty(sourceKeyField))
|
||||
{
|
||||
transferMessage = "Campo chiave sorgente richiesto. Seleziona un campo che identifichi univocamente ogni record per utilizzare il sistema di associazioni.";
|
||||
transferMessageType = "error";
|
||||
return;
|
||||
}
|
||||
|
||||
isTransferringData = true;
|
||||
transferMessage = "";
|
||||
transferMessageType = "";
|
||||
transferResults.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
@@ -1488,53 +1847,184 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Trasforma e trasferisci ogni record
|
||||
// 2. Ottieni i campi obbligatori dell'entità REST (se non ci sono campi chiave)
|
||||
var requiredFields = new HashSet<string>();
|
||||
if (!keyFields.Any() && restEntityDetails != null)
|
||||
{
|
||||
requiredFields = restEntityDetails.Properties
|
||||
.Where(p => p.IsRequired && fieldMappings.ContainsValue(p.Name))
|
||||
.Select(p => p.Name)
|
||||
.ToHashSet();
|
||||
|
||||
Logger.LogInformation("Nessun campo chiave definito. Utilizzo {RequiredFieldsCount} campi obbligatori per controllo duplicati: {RequiredFields}",
|
||||
requiredFields.Count, string.Join(", ", requiredFields));
|
||||
}
|
||||
|
||||
// 3. Trasforma e trasferisci ogni record
|
||||
int successCount = 0;
|
||||
int errorCount = 0;
|
||||
int updatedCount = 0;
|
||||
int duplicateCount = 0;
|
||||
var errors = new List<string>();
|
||||
int recordNumber = 1;
|
||||
|
||||
foreach (var record in records)
|
||||
{
|
||||
var transferResult = new TransferResult
|
||||
{
|
||||
RecordNumber = recordNumber,
|
||||
RecordData = new Dictionary<string, object>(record)
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
// Trasforma il record in base ai mapping
|
||||
var restData = TransformRecordToRestEntity(record);
|
||||
|
||||
// Esegui upsert (crea o aggiorna)
|
||||
var result = await currentRestClient.UpsertEntityAsync(selectedRestEntity.Name, restData);
|
||||
// Genera la chiave sorgente per questo record
|
||||
var sourceKey = GenerateSourceKey(record);
|
||||
var currentSourceName = selectedSourceType == "database" ? selectedTable : selectedSheet;
|
||||
|
||||
// NUOVA LOGICA: Cerca associazione esistente
|
||||
if (useRecordAssociations && !string.IsNullOrEmpty(sourceKey))
|
||||
{
|
||||
var existingAssociation = await CredentialService.FindRecordAssociationAsync(
|
||||
currentSourceName, sourceKey, selectedRestEntity.Name);
|
||||
|
||||
if (existingAssociation != null && existingAssociation.IsActive)
|
||||
{
|
||||
// Prova ad aggiornare il record esistente
|
||||
var updateResult = await currentRestClient.UpdateEntityAsync(
|
||||
selectedRestEntity.Name, existingAssociation.DestinationId, restData);
|
||||
|
||||
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
|
||||
existingAssociation.UpdatedAt = DateTime.UtcNow;
|
||||
await CredentialService.UpdateRecordAssociationAsync(existingAssociation);
|
||||
|
||||
Logger.LogDebug("Record aggiornato tramite associazione: {EntityId} per chiave sorgente {SourceKey}",
|
||||
existingAssociation.DestinationId, sourceKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
transferResults.Add(transferResult);
|
||||
recordNumber++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
CreateNewRecord:
|
||||
// Crea un nuovo record
|
||||
var result = await currentRestClient.CreateEntityAsync(selectedRestEntity.Name, restData);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
successCount++;
|
||||
transferResult.Status = "success";
|
||||
transferResult.Message = "Record inserito con successo";
|
||||
transferResult.EntityId = result.ContainsKey("id") ? result["id"]?.ToString() :
|
||||
result.ContainsKey("Id") ? result["Id"]?.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
|
||||
{
|
||||
SourceName = currentSourceName,
|
||||
SourceType = selectedSourceType,
|
||||
SourceKey = sourceKey,
|
||||
DestinationEntity = selectedRestEntity.Name,
|
||||
DestinationId = transferResult.EntityId,
|
||||
RestCredentialName = selectedRestCredential,
|
||||
AdditionalInfo = System.Text.Json.JsonSerializer.Serialize(new
|
||||
{
|
||||
TransferDate = DateTime.UtcNow,
|
||||
RecordNumber = recordNumber,
|
||||
MappingCount = fieldMappings.Count
|
||||
})
|
||||
};
|
||||
|
||||
await CredentialService.SaveRecordAssociationAsync(association);
|
||||
Logger.LogDebug("Associazione creata: {SourceKey} -> {DestinationId}", sourceKey, transferResult.EntityId);
|
||||
}
|
||||
catch (Exception assocEx)
|
||||
{
|
||||
Logger.LogWarning(assocEx, "Errore nella creazione dell'associazione per record {RecordNumber}", recordNumber);
|
||||
// Non interrompiamo il trasferimento per errori di associazione
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogDebug("Record trasferito con successo: {Data}", string.Join(", ", restData.Select(kvp => $"{kvp.Key}={kvp.Value}")));
|
||||
}
|
||||
else
|
||||
{
|
||||
errorCount++;
|
||||
errors.Add($"Errore nel trasferimento del record (result null)");
|
||||
transferResult.Status = "error";
|
||||
transferResult.Message = "Errore nel trasferimento del record (result null)";
|
||||
errors.Add($"Errore nel trasferimento del record {recordNumber}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorCount++;
|
||||
errors.Add($"Errore nel trasferimento: {ex.Message}");
|
||||
Logger.LogError(ex, "Errore nel trasferimento di un record");
|
||||
transferResult.Status = "error";
|
||||
transferResult.Message = $"Errore: {ex.Message}";
|
||||
errors.Add($"Errore nel trasferimento del record {recordNumber}: {ex.Message}");
|
||||
Logger.LogError(ex, "Errore nel trasferimento del record {RecordNumber}", recordNumber);
|
||||
}
|
||||
|
||||
transferResults.Add(transferResult);
|
||||
recordNumber++;
|
||||
}
|
||||
|
||||
// 3. Mostra risultati
|
||||
// 4. Mostra risultati
|
||||
if (errorCount == 0)
|
||||
{
|
||||
transferMessage = $"Trasferimento completato con successo! {successCount} record trasferiti.";
|
||||
var message = $"Trasferimento completato con successo! ";
|
||||
var messageParts = new List<string>();
|
||||
|
||||
if (successCount > 0) messageParts.Add($"{successCount} record inseriti");
|
||||
if (updatedCount > 0) messageParts.Add($"{updatedCount} record aggiornati");
|
||||
if (duplicateCount > 0) messageParts.Add($"{duplicateCount} duplicati rilevati (warning)");
|
||||
|
||||
message += string.Join(", ", messageParts) + ".";
|
||||
transferMessage = message;
|
||||
transferMessageType = "success";
|
||||
}
|
||||
else
|
||||
{
|
||||
transferMessage = $"Trasferimento completato con errori. Successi: {successCount}, Errori: {errorCount}. Primi errori: {string.Join("; ", errors.Take(3))}";
|
||||
transferMessageType = "error";
|
||||
var message = $"Trasferimento completato con {(duplicateCount > 0 ? "warning e " : "")}errori. ";
|
||||
var messageParts = new List<string>();
|
||||
|
||||
if (successCount > 0) messageParts.Add($"Inserimenti: {successCount}");
|
||||
if (updatedCount > 0) messageParts.Add($"Aggiornamenti: {updatedCount}");
|
||||
if (duplicateCount > 0) messageParts.Add($"Duplicati (warning): {duplicateCount}");
|
||||
messageParts.Add($"Errori: {errorCount}");
|
||||
|
||||
message += string.Join(", ", messageParts);
|
||||
if (errors.Any())
|
||||
{
|
||||
message += $". Primi errori: {string.Join("; ", errors.Take(3))}";
|
||||
}
|
||||
transferMessage = message;
|
||||
transferMessageType = errorCount > 0 ? "error" : "warning";
|
||||
}
|
||||
|
||||
Logger.LogInformation("Trasferimento completato. Successi: {SuccessCount}, Errori: {ErrorCount}", successCount, errorCount);
|
||||
Logger.LogInformation("Trasferimento completato. Inserimenti: {SuccessCount}, Aggiornamenti: {UpdatedCount}, Duplicati: {DuplicateCount}, Errori: {ErrorCount}",
|
||||
successCount, updatedCount, duplicateCount, errorCount);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -1717,8 +2207,190 @@
|
||||
{
|
||||
return mostCommon.Key;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return ','; // Default fallback
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifica se il pulsante di trasferimento può essere abilitato
|
||||
/// </summary>
|
||||
private bool IsTransferButtonEnabled()
|
||||
{
|
||||
// Base requirements
|
||||
if (!fieldMappings.Any())
|
||||
return false;
|
||||
|
||||
// Se il sistema di associazioni è abilitato, il campo chiave sorgente è obbligatorio
|
||||
if (useRecordAssociations && string.IsNullOrEmpty(sourceKeyField))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helper methods per UI risultati
|
||||
private string GetResultRowClass(string status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
"success" => "",
|
||||
"updated" => "table-info",
|
||||
"duplicate" => "table-warning",
|
||||
"error" => "table-danger",
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
|
||||
private string GetResultBadgeClass(string status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
"success" => "bg-success",
|
||||
"updated" => "bg-info",
|
||||
"duplicate" => "bg-warning text-dark",
|
||||
"error" => "bg-danger",
|
||||
_ => "bg-secondary"
|
||||
};
|
||||
}
|
||||
|
||||
private string GetResultIcon(string status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
"success" => "fa-check-circle",
|
||||
"updated" => "fa-edit",
|
||||
"duplicate" => "fa-exclamation-triangle",
|
||||
"error" => "fa-times-circle",
|
||||
_ => "fa-question-circle"
|
||||
};
|
||||
}
|
||||
|
||||
private string GetResultStatusText(string status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
"success" => "Inserito",
|
||||
"updated" => "Aggiornato",
|
||||
"duplicate" => "Duplicato",
|
||||
"error" => "Errore",
|
||||
_ => "Sconosciuto"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Genera una chiave univoca per il record sorgente
|
||||
/// </summary>
|
||||
private string GenerateSourceKey(Dictionary<string, object> record)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Il campo chiave sorgente deve essere sempre specificato
|
||||
if (string.IsNullOrEmpty(sourceKeyField))
|
||||
{
|
||||
throw new InvalidOperationException("Campo chiave sorgente non specificato. La selezione del campo chiave è obbligatoria.");
|
||||
}
|
||||
|
||||
if (!record.ContainsKey(sourceKeyField))
|
||||
{
|
||||
throw new InvalidOperationException($"Il campo chiave '{sourceKeyField}' non è presente nel record sorgente.");
|
||||
}
|
||||
|
||||
var keyValue = record[sourceKeyField]?.ToString();
|
||||
if (string.IsNullOrEmpty(keyValue))
|
||||
{
|
||||
throw new InvalidOperationException($"Il valore del campo chiave '{sourceKeyField}' è vuoto o null per questo record.");
|
||||
}
|
||||
|
||||
return keyValue;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nella generazione della chiave sorgente per il campo {SourceKeyField}", sourceKeyField);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleDatabaseSelectionRequired()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (currentDatabaseManager == null)
|
||||
{
|
||||
databaseErrorMessage = "Database manager non inizializzato";
|
||||
return;
|
||||
}
|
||||
|
||||
// Ottieni la lista dei database disponibili
|
||||
availableDatabases = await currentDatabaseManager.GetAvailableDatabasesAsync();
|
||||
|
||||
if (availableDatabases != null && availableDatabases.Any())
|
||||
{
|
||||
// Mostra il modal per la selezione del database
|
||||
showDatabaseSelectionModal = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
else
|
||||
{
|
||||
databaseErrorMessage = "Nessun database disponibile per la selezione";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nell'ottenere la lista dei database disponibili");
|
||||
databaseErrorMessage = $"Errore nel recupero dei database: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnDatabaseSelected()
|
||||
{
|
||||
if (string.IsNullOrEmpty(selectedDatabase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentDatabaseManager == null)
|
||||
{
|
||||
databaseErrorMessage = "Database manager non inizializzato";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Cambia il database attivo
|
||||
await currentDatabaseManager.ChangeDatabaseAsync(selectedDatabase);
|
||||
|
||||
// Nasconde il modal
|
||||
showDatabaseSelectionModal = false;
|
||||
|
||||
// Ritenta il discovery dello schema
|
||||
var schema = await currentDatabaseManager.GetDatabaseSchemaAsync();
|
||||
databaseTables = schema as Dictionary<string, IEnumerable<DbColumnInfo>> ??
|
||||
(schema != null ? new Dictionary<string, IEnumerable<DbColumnInfo>>(schema) : new Dictionary<string, IEnumerable<DbColumnInfo>>());
|
||||
|
||||
if (databaseTables.Count == 0)
|
||||
{
|
||||
databaseErrorMessage = $"Il database '{selectedDatabase}' non contiene tabelle accessibili";
|
||||
}
|
||||
else
|
||||
{
|
||||
isDatabaseConnected = true;
|
||||
databaseErrorMessage = "";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nel cambio di database a {Database}", selectedDatabase);
|
||||
databaseErrorMessage = $"Errore nel cambio di database: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelDatabaseSelection()
|
||||
{
|
||||
showDatabaseSelectionModal = false;
|
||||
selectedDatabase = "";
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user