[Feature] Salesforce come fonte e Database come destinazione nel Data Coupler
Implementata la possibilita' di usare Salesforce come fonte dati e un database relazionale come destinazione nel flusso di trasferimento dati. ## Modifiche principali ### Nuovi file partial class - Data_Coupler/Extensions/DataCoupler/SalesforceSourceMethod.cs - Stato e metodi per Salesforce come fonte (credenziali, connessione, discovery SObject) - ConnectToSalesforceSource(): autenticazione parallela + discovery summaries/details - SelectSalesforceSourceEntity(): selezione SObject e caricamento campi - GetAllRecordsFromSalesforceSource(): estrazione via ExtractAllEntitiesAsync con solo i campi mappati - Filtro/ricerca SObject in tempo reale - Data_Coupler/Extensions/DataCoupler/DatabaseDestinationMethod.cs - Stato e metodi per database come destinazione - ConnectToDestinationDatabase(): connessione e discovery tabelle - SelectDestinationTable(): caricamento schema tabella on-demand - IsDestinationDatabaseReady: proprieta' calcolata per validazione - Toggle UI tra destinazione REST e Database ### IDatabaseManager interface - Aggiunto UpsertRecordAsync(tableName, keyField, keyValue, record): - Esegue SELECT COUNT(*) per verificare esistenza record - UPDATE se esiste, INSERT se non esiste - Implementato in EFCoreDatabaseManager (parametri named @p0..@pN) - Implementato in OdbcDatabaseManager (parametri posizionali ?) ### DataCoupler.razor (UI) - Aggiunto 'Salesforce (REST API)' nel dropdown tipo fonte - Sezione UI Salesforce fonte: selettore credenziali, bottone connessione, lista SObject con ricerca - Toggle destinazione REST/Database nella card destra - Sezione UI Database destinazione: selettore credenziali, bottone connessione, lista tabelle con ricerca - Colonna destra mapping aggiornata: mostra colonne DB se destinazione e' database, proprieta' REST altrimenti - Colonna sinistra mapping: aggiunta sezione campi SObject Salesforce - isSourceReady aggiornato per includere fonte Salesforce - isDestinationReady aggiornato per includere destinazione database - Etichette mapping dinamiche in base ai tipi selezionati ### DataCoupler.razor.cs (logica) - LoadCredentials(): aggiunto caricamento credenziali Salesforce fonte - ResetSourceState(): aggiunto reset stato Salesforce fonte - ResetDestinationState(): aggiunto reset stato database destinazione - GetAllRecordsFromSource(): aggiunto branch Salesforce - StartDataTransfer(): routing verso StartDataTransferToDatabase() se dest=database - Aggiunto StartDataTransferToDatabase(): estrae record fonte, applica mapping e default values, chiama UpsertRecordAsync per ogni record - Rimosso codice duplicato in StartDataTransfer()
This commit is contained in:
@@ -50,6 +50,7 @@
|
||||
<option value="">-- Seleziona Tipo --</option>
|
||||
<option value="database">Database</option>
|
||||
<option value="file">File (Excel/CSV)</option>
|
||||
<option value="salesforce">Salesforce (REST API)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -709,17 +710,133 @@
|
||||
</div>
|
||||
</div> }
|
||||
}
|
||||
|
||||
<!-- Sezione Salesforce come Fonte -->
|
||||
@if (selectedSourceType == "salesforce")
|
||||
{
|
||||
<!-- Selezione Credenziali Salesforce -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Credenziali Salesforce:</label>
|
||||
<select class="form-select" @onchange="OnSalesforceSourceCredentialChanged" value="@selectedSalesforceSourceCredential">
|
||||
<option value="">-- Seleziona Salesforce --</option>
|
||||
@foreach (var cred in salesforceSourceCredentials)
|
||||
{
|
||||
<option value="@cred.Name">@cred.Name (@cred.BaseUrl)</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(selectedSalesforceSourceCredential))
|
||||
{
|
||||
<div class="mb-3">
|
||||
<button class="btn btn-success btn-sm" @onclick="ConnectToSalesforceSource" disabled="@isConnectingSalesforceSource">
|
||||
@if (isConnectingSalesforceSource)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||
}
|
||||
<i class="fas fa-plug"></i> Connetti e Scopri SObject
|
||||
</button>
|
||||
@if (isSalesforceSourceConnected)
|
||||
{
|
||||
<span class="badge bg-success ms-2">Connesso</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(salesforceSourceErrorMessage))
|
||||
{
|
||||
<div class="alert alert-danger" role="alert">
|
||||
@salesforceSourceErrorMessage
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Lista SObject Salesforce -->
|
||||
@if (salesforceSourceEntities.Any())
|
||||
{
|
||||
<div class="mb-3">
|
||||
<h6>SObject Salesforce (@salesforceSourceEntities.Count disponibili):</h6>
|
||||
<div class="mb-2">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
||||
<input type="text" class="form-control" placeholder="Cerca SObject..."
|
||||
@bind="salesforceSourceSearchTerm" @oninput="FilterSalesforceSourceEntities" />
|
||||
@if (!string.IsNullOrEmpty(salesforceSourceSearchTerm))
|
||||
{
|
||||
<button class="btn btn-outline-secondary" @onclick="ClearSalesforceSourceSearch">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group" style="max-height: 300px; overflow-y: auto;">
|
||||
@foreach (var entity in GetFilteredSalesforceSourceEntities())
|
||||
{
|
||||
<a class="list-group-item list-group-item-action @(selectedSalesforceSourceEntity?.Name == entity.Name ? "active" : "")"
|
||||
@onclick="@(async () => await SelectSalesforceSourceEntity(entity))">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<i class="fas fa-cloud"></i> @entity.Name
|
||||
@if (!string.IsNullOrEmpty(entity.Label))
|
||||
{
|
||||
<small class="text-muted d-block">@entity.Label</small>
|
||||
}
|
||||
</div>
|
||||
@if (selectedSalesforceSourceEntity?.Name == entity.Name)
|
||||
{
|
||||
<span class="badge bg-primary">Selezionato</span>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
@if (!GetFilteredSalesforceSourceEntities().Any())
|
||||
{
|
||||
<div class="alert alert-info mt-2">
|
||||
<i class="fas fa-info-circle"></i> Nessun SObject trovato con il termine "@salesforceSourceSearchTerm"
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lato Destro - REST API -->
|
||||
<!-- Lato Destro - Destinazione (REST API o Database) -->
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h5><i class="fas fa-cloud"></i> REST API Destination</h5>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
@if (selectedDestinationType == "database")
|
||||
{
|
||||
<i class="fas fa-database"></i>
|
||||
<span> Database Destination</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="fas fa-cloud"></i>
|
||||
<span> REST API Destination</span>
|
||||
}
|
||||
</h5>
|
||||
<!-- Toggle Tipo Destinazione -->
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button" class="btn @(selectedDestinationType == "rest" ? "btn-light" : "btn-outline-light")"
|
||||
@onclick="@(() => OnDestinationTypeChanged(new ChangeEventArgs { Value = "rest" }))">
|
||||
<i class="fas fa-cloud"></i> REST API
|
||||
</button>
|
||||
<button type="button" class="btn @(selectedDestinationType == "database" ? "btn-light" : "btn-outline-light")"
|
||||
@onclick="@(() => OnDestinationTypeChanged(new ChangeEventArgs { Value = "database" }))">
|
||||
<i class="fas fa-database"></i> Database
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<!-- ===== DESTINAZIONE REST API ===== -->
|
||||
@if (selectedDestinationType == "rest")
|
||||
{
|
||||
<!-- Selezione Credenziali REST -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Credenziali REST API:</label>
|
||||
@@ -807,19 +924,111 @@
|
||||
</div>
|
||||
} </div>
|
||||
}
|
||||
} @* fine @if (selectedDestinationType == "rest") *@
|
||||
|
||||
<!-- ===== DESTINAZIONE DATABASE ===== -->
|
||||
@if (selectedDestinationType == "database")
|
||||
{
|
||||
<!-- Selezione Credenziali Database Destinazione -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Credenziali Database:</label>
|
||||
<select class="form-select" @onchange="OnDestinationDatabaseCredentialChanged" value="@selectedDestinationDatabaseCredential">
|
||||
<option value="">-- Seleziona Database --</option>
|
||||
@foreach (var cred in databaseCredentials)
|
||||
{
|
||||
<option value="@cred.Name">@cred.Name (@cred.DatabaseType - @cred.Host)</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(selectedDestinationDatabaseCredential))
|
||||
{
|
||||
<div class="mb-3">
|
||||
<button class="btn btn-success btn-sm" @onclick="ConnectToDestinationDatabase" disabled="@isConnectingDestinationDatabase">
|
||||
@if (isConnectingDestinationDatabase)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||
}
|
||||
<i class="fas fa-plug"></i> Connetti e Scopri Tabelle
|
||||
</button>
|
||||
@if (isDestinationDatabaseConnected)
|
||||
{
|
||||
<span class="badge bg-success ms-2">Connesso</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(destinationDatabaseErrorMessage))
|
||||
{
|
||||
<div class="alert alert-danger" role="alert">
|
||||
@destinationDatabaseErrorMessage
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Lista Tabelle Database Destinazione -->
|
||||
@if (destAvailableTableNames.Any())
|
||||
{
|
||||
<div class="mb-3">
|
||||
<h6>Tabelle Database (@destAvailableTableNames.Count disponibili):</h6>
|
||||
<div class="mb-2">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
||||
<input type="text" class="form-control" placeholder="Cerca tabelle..."
|
||||
@bind="destDatabaseSearchTerm" @oninput="FilterDestinationTables" />
|
||||
@if (!string.IsNullOrEmpty(destDatabaseSearchTerm))
|
||||
{
|
||||
<button class="btn btn-outline-secondary" @onclick="ClearDestinationTableSearch">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group" style="max-height: 300px; overflow-y: auto;">
|
||||
@foreach (var table in GetFilteredDestinationTables())
|
||||
{
|
||||
<a class="list-group-item list-group-item-action @(selectedDestinationTable == table ? "active" : "")"
|
||||
@onclick="@(async () => await SelectDestinationTable(table))">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<i class="fas fa-table"></i> @table
|
||||
@if (destDatabaseTables.ContainsKey(table))
|
||||
{
|
||||
<small class="text-muted d-block">@destDatabaseTables[table].Count() colonne</small>
|
||||
}
|
||||
</div>
|
||||
@if (selectedDestinationTable == table)
|
||||
{
|
||||
<span class="badge bg-primary">Selezionata</span>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
@if (!GetFilteredDestinationTables().Any())
|
||||
{
|
||||
<div class="alert alert-info mt-2">
|
||||
<i class="fas fa-info-circle"></i> Nessuna tabella trovata con "@destDatabaseSearchTerm"
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- Sezione Mapping (quando la fonte è selezionata e REST è connesso) -->
|
||||
</div> <!-- Sezione Mapping -->
|
||||
@{
|
||||
// Per ODBC: non richiede isDatabaseConnected, basta query validata
|
||||
// Per altri database: richiede connessione + (query validata OR tabella selezionata)
|
||||
var isSourceReady = (selectedSourceType == "database" &&
|
||||
((IsOdbcConnection() && useCustomQuery && isQueryValid) ||
|
||||
var isSourceReady = (selectedSourceType == "database" &&
|
||||
((IsOdbcConnection() && useCustomQuery && isQueryValid) ||
|
||||
(!IsOdbcConnection() && isDatabaseConnected && ((useCustomQuery && isQueryValid) || (!useCustomQuery && !string.IsNullOrEmpty(selectedTable)))))) ||
|
||||
(selectedSourceType == "file" && !string.IsNullOrEmpty(selectedSheet));
|
||||
(selectedSourceType == "file" && !string.IsNullOrEmpty(selectedSheet)) ||
|
||||
(selectedSourceType == "salesforce" && isSalesforceSourceConnected && selectedSalesforceSourceEntity != null);
|
||||
var isDestinationReady = (selectedDestinationType == "rest" && isRestConnected && selectedRestEntity != null) ||
|
||||
(selectedDestinationType == "database" && IsDestinationDatabaseReady);
|
||||
}
|
||||
@if (isSourceReady && isRestConnected && selectedRestEntity != null)
|
||||
@if (isSourceReady && isDestinationReady)
|
||||
{
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
@@ -828,11 +1037,17 @@
|
||||
<h5><i class="fas fa-exchange-alt"></i> Mapping Campi</h5>
|
||||
</div> <div class="card-body">
|
||||
@{
|
||||
var sourceDisplayName = selectedSourceType == "database" ? selectedTable : selectedSheet;
|
||||
var sourceTypeName = selectedSourceType == "database" ? "Tabella" : "Foglio";
|
||||
var sourceDisplayName = selectedSourceType == "database" ? selectedTable :
|
||||
selectedSourceType == "salesforce" ? selectedSalesforceSourceEntity?.Name ?? "" :
|
||||
selectedSheet;
|
||||
var sourceTypeName = selectedSourceType == "database" ? "Tabella" :
|
||||
selectedSourceType == "salesforce" ? "SObject" :
|
||||
"Foglio";
|
||||
var destDisplayName = selectedDestinationType == "database" ? selectedDestinationTable : selectedRestEntity?.Name ?? "";
|
||||
var destTypeName = selectedDestinationType == "database" ? "Tabella DB" : "Entità REST";
|
||||
}
|
||||
<h6>Mapping tra @sourceTypeName @sourceDisplayName e @selectedRestEntity.Name</h6>
|
||||
<p class="text-muted">Configura il mapping tra i campi della fonte dati e le proprietà dell'entità REST</p>
|
||||
<h6>Mapping tra @sourceTypeName @sourceDisplayName e @destDisplayName</h6>
|
||||
<p class="text-muted">Configura il mapping tra i campi della fonte dati e le proprietà della destinazione</p>
|
||||
<div class="row">
|
||||
<!-- Colonna Sinistra: Campi Fonte -->
|
||||
<div class="col-5">
|
||||
@@ -914,6 +1129,27 @@
|
||||
</a>
|
||||
}
|
||||
}
|
||||
else if (selectedSourceType == "salesforce" && salesforceSourceEntityDetails != null)
|
||||
{
|
||||
@foreach (var field in salesforceSourceEntityDetails.Properties)
|
||||
{
|
||||
<a class="list-group-item list-group-item-action @(selectedDbColumn == field.Name ? "active" : "")"
|
||||
@onclick="@(() => SelectDbColumn(field.Name))">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>@field.Name</strong>
|
||||
<small class="text-muted d-block">@field.Type</small>
|
||||
</div>
|
||||
<div>
|
||||
@if (fieldMappings.ContainsKey(field.Name))
|
||||
{
|
||||
<span class="badge bg-success">Mapped</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -998,40 +1234,71 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Colonna Destra: Proprietà REST -->
|
||||
<!-- Colonna Destra: Proprietà Destinazione -->
|
||||
<div class="col-5">
|
||||
<h6>Proprietà REST (@selectedRestEntity.Name)</h6>
|
||||
<div class="list-group" style="max-height: 400px; overflow-y: auto;">
|
||||
@if (restEntityDetails != null)
|
||||
{
|
||||
@foreach (var property in restEntityDetails.Properties)
|
||||
@if (selectedDestinationType == "database" && !string.IsNullOrEmpty(selectedDestinationTable) && destDatabaseTables.ContainsKey(selectedDestinationTable))
|
||||
{
|
||||
<h6>Colonne Tabella (@selectedDestinationTable)</h6>
|
||||
<div class="list-group" style="max-height: 400px; overflow-y: auto;">
|
||||
@foreach (var col in destDatabaseTables[selectedDestinationTable])
|
||||
{
|
||||
<a class="list-group-item list-group-item-action @(selectedRestProperty == property.Name ? "active" : "")"
|
||||
@onclick="@(() => SelectRestProperty(property.Name))">
|
||||
<a class="list-group-item list-group-item-action @(selectedRestProperty == col.Name ? "active" : "")"
|
||||
@onclick="@(() => SelectRestProperty(col.Name))">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>@property.Name</strong>
|
||||
<small class="text-muted d-block">@property.Type</small>
|
||||
<strong>@col.Name</strong>
|
||||
<small class="text-muted d-block">@col.DataType</small>
|
||||
</div>
|
||||
<div>
|
||||
@if (property.IsRequired)
|
||||
@if (col.IsPrimaryKey)
|
||||
{
|
||||
<span class="badge bg-danger">Required</span>
|
||||
<span class="badge bg-warning text-dark">PK</span>
|
||||
}
|
||||
@if (fieldMappings.ContainsValue(property.Name))
|
||||
@if (fieldMappings.ContainsValue(col.Name))
|
||||
{
|
||||
<span class="badge bg-success">Mapped</span>
|
||||
}
|
||||
@if (defaultValues.ContainsKey(property.Name))
|
||||
{
|
||||
<span class="badge bg-warning text-dark">Default</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h6>Proprietà REST (@(selectedRestEntity?.Name ?? ""))</h6>
|
||||
<div class="list-group" style="max-height: 400px; overflow-y: auto;">
|
||||
@if (restEntityDetails != null)
|
||||
{
|
||||
@foreach (var property in restEntityDetails.Properties)
|
||||
{
|
||||
<a class="list-group-item list-group-item-action @(selectedRestProperty == property.Name ? "active" : "")"
|
||||
@onclick="@(() => SelectRestProperty(property.Name))">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>@property.Name</strong>
|
||||
<small class="text-muted d-block">@property.Type</small>
|
||||
</div>
|
||||
<div>
|
||||
@if (property.IsRequired)
|
||||
{
|
||||
<span class="badge bg-danger">Required</span>
|
||||
}
|
||||
@if (fieldMappings.ContainsValue(property.Name))
|
||||
{
|
||||
<span class="badge bg-success">Mapped</span>
|
||||
}
|
||||
@if (defaultValues.ContainsKey(property.Name))
|
||||
{
|
||||
<span class="badge bg-warning text-dark">Default</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ public partial class DataCoupler : ComponentBase
|
||||
{
|
||||
databaseCredentials = await CredentialService.GetAllDatabaseCredentialsAsync();
|
||||
await LoadRestCredentials(); // Carica le credenziali REST dalla classe parziale
|
||||
await LoadSalesforceSourceCredentials(); // Carica le credenziali Salesforce per la fonte
|
||||
// Carica anche i profili disponibili
|
||||
await LoadProfiles();
|
||||
}
|
||||
@@ -809,6 +810,7 @@ public partial class DataCoupler : ComponentBase
|
||||
restSearchTerm = "";
|
||||
currentRestDiscovery = null;
|
||||
currentRestClient = null;
|
||||
ResetDestinationDatabaseState();
|
||||
}
|
||||
|
||||
private void OnSourceTypeChanged(ChangeEventArgs e)
|
||||
@@ -833,6 +835,9 @@ public partial class DataCoupler : ComponentBase
|
||||
fileData.Clear();
|
||||
selectedSheet = "";
|
||||
|
||||
// Reset Salesforce source state
|
||||
ResetSalesforceSourceState();
|
||||
|
||||
// Reset pagination
|
||||
currentPage = 1;
|
||||
|
||||
@@ -1766,25 +1771,103 @@ public partial class DataCoupler : ComponentBase
|
||||
}
|
||||
private async Task StartDataTransfer()
|
||||
{
|
||||
// Verifica se possiamo utilizzare le chiamate Composite (solo per Salesforce)
|
||||
// Se destinazione è database, usa il metodo dedicato
|
||||
if (selectedDestinationType == "database")
|
||||
{
|
||||
await StartDataTransferToDatabase();
|
||||
return;
|
||||
}
|
||||
|
||||
// Verifica se possiamo utilizzare le chiamate Composite (solo per Salesforce REST dest)
|
||||
if (currentRestClient is DataConnection.REST.Implementations.SalesforceServiceClient)
|
||||
{
|
||||
await StartDataTransferWithComposite();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback al metodo originale per altri client REST
|
||||
// Se siamo con Salesforce, usa il nuovo metodo Composite
|
||||
if (currentRestClient is DataConnection.REST.Implementations.SalesforceServiceClient)
|
||||
{
|
||||
await StartDataTransferWithComposite();
|
||||
return;
|
||||
}
|
||||
|
||||
// Per altri client, usa il metodo originale
|
||||
// Per altri client REST, usa il metodo originale
|
||||
await StartDataTransferOriginal();
|
||||
}
|
||||
|
||||
private async Task StartDataTransferToDatabase()
|
||||
{
|
||||
if (!fieldMappings.Any() || currentDestinationDatabaseManager == null || string.IsNullOrEmpty(selectedDestinationTable))
|
||||
{
|
||||
transferMessage = "Configurazione incompleta. Assicurati di aver selezionato la fonte, la tabella di destinazione e configurato almeno un mapping.";
|
||||
transferMessageType = "error";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(sourceKeyField))
|
||||
{
|
||||
transferMessage = "Seleziona un campo chiave sorgente per l'operazione di upsert.";
|
||||
transferMessageType = "error";
|
||||
return;
|
||||
}
|
||||
|
||||
isTransferringData = true;
|
||||
transferMessage = "";
|
||||
int successCount = 0;
|
||||
int errorCount = 0;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
var sourceRecords = await GetAllRecordsFromSource();
|
||||
var recordsList = sourceRecords.ToList();
|
||||
transferMessage = $"Elaborazione di {recordsList.Count} record...";
|
||||
StateHasChanged();
|
||||
|
||||
foreach (var sourceRecord in recordsList)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Applica mapping
|
||||
var destRecord = new Dictionary<string, object?>();
|
||||
foreach (var mapping in fieldMappings)
|
||||
{
|
||||
if (sourceRecord.TryGetValue(mapping.Key, out var value))
|
||||
destRecord[mapping.Value] = value;
|
||||
}
|
||||
// Aggiungi default values
|
||||
foreach (var dv in defaultValues)
|
||||
{
|
||||
if (!destRecord.ContainsKey(dv.Key))
|
||||
destRecord[dv.Key] = dv.Value;
|
||||
}
|
||||
|
||||
// Determina chiave di destinazione
|
||||
string destKeyField = fieldMappings.TryGetValue(sourceKeyField, out var mapped) ? mapped : sourceKeyField;
|
||||
object? keyValue = sourceRecord.TryGetValue(sourceKeyField, out var kv) ? kv : null;
|
||||
|
||||
var ok = await currentDestinationDatabaseManager.UpsertRecordAsync(
|
||||
selectedDestinationTable, destKeyField, keyValue, destRecord);
|
||||
|
||||
if (ok) successCount++;
|
||||
else errorCount++;
|
||||
}
|
||||
catch
|
||||
{
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
transferMessage = $"Trasferimento completato: {successCount} record elaborati, {errorCount} errori.";
|
||||
transferMessageType = errorCount == 0 ? "success" : "warning";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
transferMessage = $"Errore durante il trasferimento: {ex.Message}";
|
||||
transferMessageType = "error";
|
||||
Logger.LogError(ex, "Errore nel trasferimento verso database");
|
||||
}
|
||||
finally
|
||||
{
|
||||
isTransferringData = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartDataTransferOriginal()
|
||||
{
|
||||
if (!fieldMappings.Any() || currentRestClient == null || selectedRestEntity == null)
|
||||
@@ -2248,6 +2331,10 @@ public partial class DataCoupler : ComponentBase
|
||||
{
|
||||
return await GetAllRecordsFromFile();
|
||||
}
|
||||
else if (selectedSourceType == "salesforce")
|
||||
{
|
||||
return await GetAllRecordsFromSalesforceSource();
|
||||
}
|
||||
|
||||
return new List<Dictionary<string, object>>();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user