From 1435c013d3233ea42abba4afbced60a3b9d4d137 Mon Sep 17 00:00:00 2001 From: Alessio Dal Santo Date: Tue, 1 Jul 2025 20:50:25 +0200 Subject: [PATCH] Summarized conversation historyEcco il messaggio di commit tradotto in italiano: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Refactoring: Separazione logica C# da markup Razor nel componente DataCoupler** **Modifiche principali:** - Spostamento di tutta la logica C# dal file DataCoupler.razor al file DataCoupler.razor.cs come partial class - Rimozione completa del blocco @code dal file .razor mantenendo solo il markup HTML/Razor - Aggiunta delle using directive mancanti (System.Data, System.Text, ExcelDataReader, ecc.) - Correzione delle firme dei metodi e degli handler di eventi per la compatibilità Blazor - Sistemazione delle proprietà di iniezione dei servizi [Inject] nel code-behind - Risoluzione di tutti gli errori di compilazione relativi alla separazione dei file **Miglioramenti strutturali:** - Migliore separazione delle responsabilità tra presentazione e logica business - Struttura del codice più pulita e manutenibile seguendo le best practice Blazor - Codice più facilmente testabile con la logica isolata nel file .cs - Rimozione di codice duplicato e ottimizzazione delle funzioni di utilità **File modificati:** - Data_Coupler/Pages/DataCoupler.razor: Pulizia markup, rimozione blocco @code - Data_Coupler/Pages/DataCoupler.razor.cs: Implementazione completa della logica C# come partial class Questo refactoring migliora significativamente la struttura del codice seguendo le convenzioni standard di Blazor per la separazione tra markup e logica applicativa. --- Data_Coupler/Pages/DataCoupler.razor | 2003 ---------------------- Data_Coupler/Pages/DataCoupler.razor.cs | 2018 +++++++++++++++++++++++ 2 files changed, 2018 insertions(+), 2003 deletions(-) create mode 100644 Data_Coupler/Pages/DataCoupler.razor.cs diff --git a/Data_Coupler/Pages/DataCoupler.razor b/Data_Coupler/Pages/DataCoupler.razor index 19b8d9e..c4d4298 100644 --- a/Data_Coupler/Pages/DataCoupler.razor +++ b/Data_Coupler/Pages/DataCoupler.razor @@ -1154,2006 +1154,3 @@ } - -@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 RecordData { get; set; } = new(); - } - - // Stato delle credenziali - private List databaseCredentials = new(); - private List restApiCredentials = new(); - - // Selezione tipo fonte - private string selectedSourceType = ""; - - // Credenziali selezionate - private string selectedDatabaseCredential = ""; - private string selectedRestCredential = ""; - - // Stato connessioni - private bool isConnectingDatabase = false; - private bool isConnectingRest = false; - private bool isDatabaseConnected = false; - private bool isRestConnected = false; - - // Messaggi di errore - private string databaseErrorMessage = ""; - private string restErrorMessage = ""; - - // Database discovery - private Dictionary> databaseTables = new(); - private string selectedTable = ""; - private string databaseSearchTerm = ""; - - // Database selection - private List availableDatabases = new(); - private string selectedDatabase = ""; - private bool showDatabaseSelection = false; - private bool showDatabaseSelectionModal = false; - private bool isLoadingDatabases = false; - - // Custom query functionality - private bool useCustomQuery = false; - private string customQuery = ""; - private bool isValidatingQuery = false; - private bool isQueryValid = false; - private string queryValidationMessage = ""; - private List> queryPreviewData = new(); - private List queryColumns = new(); - private bool showQueryPreview = false; - private bool isLoadingPreview = false; // File handling - private string selectedFileName = ""; - private bool isProcessingFile = false; - private string fileErrorMessage = ""; - private Dictionary> fileSheets = new(); // SheetName -> Columns - private Dictionary>> fileData = new(); // SheetName -> Data rows - private string selectedSheet = ""; - - // File preview pagination - private int currentPage = 1; - private int pageSize = 20; - private int GetTotalPages(string sheetName) => fileData.ContainsKey(sheetName) ? - (int)Math.Ceiling((double)fileData[sheetName].Count / pageSize) : 0; - - // REST discovery - private List restEntities = new(); - private RestEntitySummary? selectedRestEntity = null; - private RestEntityInfo? restEntityDetails = null; - private string restSearchTerm = ""; - // Mapping campi - private Dictionary fieldMappings = new(); // DbColumn -> RestProperty - private HashSet 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 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 transferResults = new(); - private bool showDetailedResults = false; - - // Servizi - private IDatabaseManager? currentDatabaseManager = null; - private IRestMetadataDiscovery? currentRestDiscovery = null; - private IRestServiceClient? currentRestClient = null; - - protected override async Task OnInitializedAsync() - { - await LoadCredentials(); - } private async Task LoadCredentials() - { - try - { - databaseCredentials = await CredentialService.GetAllDatabaseCredentialsAsync(); - restApiCredentials = await CredentialService.GetAllRestApiCredentialsAsync(); - } - catch (Exception ex) - { - Logger.LogError(ex, "Errore nel caricamento delle credenziali"); - await JSRuntime.InvokeVoidAsync("alert", $"Errore nel caricamento delle credenziali: {ex.Message}"); - } - } - - private void OnSourceTypeChanged(ChangeEventArgs e) - { - selectedSourceType = e.Value?.ToString() ?? ""; - - // Reset state when changing source type - ResetSourceState(); } private void ResetSourceState() - { - // Reset database state - ResetDatabaseState(); - - // Reset file state - selectedFileName = ""; - isProcessingFile = false; - fileErrorMessage = ""; - fileSheets.Clear(); - fileData.Clear(); - selectedSheet = ""; - - // Reset pagination - currentPage = 1; - - // Reset mappings - ClearAllMappings(); - } - - private async Task OnFileSelected(InputFileChangeEventArgs e) - { try - { - isProcessingFile = true; - fileErrorMessage = ""; - fileSheets.Clear(); - fileData.Clear(); - selectedSheet = ""; - - var file = e.File; - selectedFileName = file.Name; - - // Validate file type - var extension = Path.GetExtension(file.Name).ToLowerInvariant(); - if (extension != ".xlsx" && extension != ".xls" && extension != ".csv") - { - fileErrorMessage = "Formato file non supportato. Utilizzare Excel (.xlsx, .xls) o CSV (.csv)"; - return; - } - - // Process file based on type - if (extension == ".csv") - { - await ProcessCsvFile(file); - } - else - { - await ProcessExcelFile(file); - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Errore nell'elaborazione del file"); - fileErrorMessage = $"Errore nell'elaborazione del file: {ex.Message}"; - } - finally - { - isProcessingFile = false; - StateHasChanged(); - } - } private async Task ProcessCsvFile(IBrowserFile file) - { - using var stream = file.OpenReadStream(maxAllowedSize: 50 * 1024 * 1024); // Aumentato a 50MB - using var reader = new StreamReader(stream); - - var firstLine = await reader.ReadLineAsync(); - if (string.IsNullOrEmpty(firstLine)) - { - fileErrorMessage = "Il file CSV è vuoto"; - return; - } - - Logger.LogInformation("CSV first line: {FirstLine}", firstLine); - - // Detect separator automatically - var separator = DetectCsvSeparator(firstLine); - Logger.LogInformation("CSV separator detected: '{Separator}'", separator); - - // Parse headers (first row) - gestisce meglio i separatori - var headers = ParseCsvLine(firstLine, separator); - - Logger.LogInformation("CSV headers parsed: {Headers}", string.Join(" | ", headers)); - // For CSV, we create a single "sheet" with the filename - var sheetName = Path.GetFileNameWithoutExtension(file.Name); - fileSheets[sheetName] = headers; - - // Read data rows - rimuovo il limite di 1000 righe - var dataRows = new List>(); - string? line; - int rowNumber = 2; // Starting from row 2 (after header) - - while ((line = await reader.ReadLineAsync()) != null) - { - if (string.IsNullOrWhiteSpace(line)) continue; - - var values = ParseCsvLine(line, separator); - var row = new Dictionary(); - for (int i = 0; i < headers.Count; i++) - { - var value = i < values.Count ? values[i] : ""; - row[headers[i]] = string.IsNullOrEmpty(value) ? "" : value; - } - - dataRows.Add(row); - rowNumber++; - - // Log delle prime 3 righe per debug - if (rowNumber <= 5) - { - Logger.LogInformation("CSV row {RowNumber}: {Values}", rowNumber - 1, string.Join(" | ", values)); - } - } - fileData[sheetName] = dataRows; - - // Auto-seleziona il foglio per i CSV dato che ce n'è solo uno - selectedSheet = sheetName; - - Logger.LogInformation("CSV file processed: {FileName}, Headers: {HeaderCount} ({Headers}), Rows: {RowCount}, Auto-selected sheet: {SheetName}", - file.Name, headers.Count, string.Join(", ", headers), dataRows.Count, selectedSheet); - } private List ParseCsvLine(string line, char separator = ',') - { - var result = new List(); - var current = new StringBuilder(); - bool inQuotes = false; - - for (int i = 0; i < line.Length; i++) - { - char c = line[i]; - - if (c == '"') - { - if (inQuotes && i + 1 < line.Length && line[i + 1] == '"') - { - // Double quote - escaped quote - current.Append('"'); - i++; // Skip next quote - } - else - { - // Toggle quote mode - inQuotes = !inQuotes; - } - } - else if (c == separator && !inQuotes) - { - // End of field - result.Add(current.ToString().Trim()); - current.Clear(); - } - else - { - current.Append(c); - } - } - - // Add the last field - result.Add(current.ToString().Trim()); - - return result; - }private async Task ProcessExcelFile(IBrowserFile file) - { - try - { - using var stream = file.OpenReadStream(maxAllowedSize: 50 * 1024 * 1024); // 50MB max - - // Crea il reader Excel basato sull'estensione - IExcelDataReader reader; - var extension = Path.GetExtension(file.Name).ToLowerInvariant(); - - if (extension == ".xlsx") - { - reader = ExcelReaderFactory.CreateOpenXmlReader(stream); - } - else if (extension == ".xls") - { - reader = ExcelReaderFactory.CreateBinaryReader(stream); - } - else - { - fileErrorMessage = "Formato Excel non supportato. Utilizzare .xlsx o .xls"; - return; - } - - using (reader) - { - // Configura per utilizzare la prima riga come header - var configuration = new ExcelDataSetConfiguration() - { - ConfigureDataTable = (_) => new ExcelDataTableConfiguration() - { - UseHeaderRow = true // Prima riga come header - } - }; - - // Converti in DataSet - var dataSet = reader.AsDataSet(configuration); - - Logger.LogInformation("Excel file processed: {FileName}, Sheets: {SheetCount}", - file.Name, dataSet.Tables.Count); - - // Processa ogni foglio - foreach (DataTable table in dataSet.Tables) - { - var sheetName = table.TableName; - var headers = new List(); - var dataRows = new List>(); - - // Estrai i nomi delle colonne (headers) - foreach (DataColumn column in table.Columns) - { - headers.Add(column.ColumnName); - } - - Logger.LogInformation("Processing Excel sheet: {SheetName}, Columns: {ColumnCount}, Rows: {RowCount}", - sheetName, headers.Count, table.Rows.Count); - - // Processa le righe di dati - for (int i = 0; i < table.Rows.Count; i++) - { - var row = table.Rows[i]; - var rowData = new Dictionary(); - - for (int j = 0; j < headers.Count; j++) - { - var cellValue = row[j]?.ToString() ?? ""; - rowData[headers[j]] = string.IsNullOrEmpty(cellValue) ? "" : cellValue; - } - - dataRows.Add(rowData); - - // Log delle prime 3 righe per debug - if (i < 3) - { - Logger.LogInformation("Excel row {RowNumber} in {Sheet}: {Values}", - i + 1, sheetName, string.Join(" | ", rowData.Values)); - } - } - - // Salva i dati del foglio - fileSheets[sheetName] = headers; - fileData[sheetName] = dataRows; - - Logger.LogInformation("Excel sheet completed: {SheetName}, Headers: {Headers}, Rows: {RowCount}", - sheetName, string.Join(", ", headers), dataRows.Count); - } - - // Auto-seleziona il primo foglio se non c'è una selezione - if (fileSheets.Any() && string.IsNullOrEmpty(selectedSheet)) - { - selectedSheet = fileSheets.First().Key; - Logger.LogInformation("Auto-selected first sheet: {SheetName}", selectedSheet); - } Logger.LogInformation("Excel file processing completed: {FileName}, Total sheets: {SheetCount}, Selected: {SelectedSheet}", - file.Name, fileSheets.Count, selectedSheet); - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Errore nell'elaborazione del file Excel: {FileName}", file.Name); - fileErrorMessage = $"Errore nell'elaborazione del file Excel: {ex.Message}"; - } - - await Task.CompletedTask; - } private void SelectSheet(string sheetName) - { - selectedSheet = sheetName; - - // Reset pagination when changing sheet - currentPage = 1; - - // Clear mappings when changing sheet - ClearAllMappings(); - - // For file sources, always require manual key selection - sourceKeyField = ""; - suggestedPrimaryKey = ""; - requiresManualKeySelection = true; - - StateHasChanged(); - } - - // File preview pagination methods - private void GoToPage(int page) - { - if (string.IsNullOrEmpty(selectedSheet) || !fileData.ContainsKey(selectedSheet)) - return; - - var totalPages = GetTotalPages(selectedSheet); - if (page >= 1 && page <= totalPages) - { - currentPage = page; - StateHasChanged(); - } - } - - private void FirstPage() => GoToPage(1); - private void PreviousPage() => GoToPage(currentPage - 1); - private void NextPage() => GoToPage(currentPage + 1); - private void LastPage() => GoToPage(GetTotalPages(selectedSheet)); - - private List> GetCurrentPageData() - { - if (string.IsNullOrEmpty(selectedSheet) || !fileData.ContainsKey(selectedSheet)) - return new List>(); - - var allData = fileData[selectedSheet]; - var skip = (currentPage - 1) * pageSize; - return allData.Skip(skip).Take(pageSize).ToList(); - } - - private int GetStartRecord() - { - if (string.IsNullOrEmpty(selectedSheet) || !fileData.ContainsKey(selectedSheet)) - return 0; - return (currentPage - 1) * pageSize + 1; - } private int GetEndRecord() - { - if (string.IsNullOrEmpty(selectedSheet) || !fileData.ContainsKey(selectedSheet)) - return 0; - var totalRecords = fileData[selectedSheet].Count; - var endRecord = currentPage * pageSize; - return Math.Min(endRecord, totalRecords); - } - - private void OnPageSizeChanged(ChangeEventArgs e) - { - if (int.TryParse(e.Value?.ToString(), out int newPageSize)) - { - pageSize = newPageSize; - currentPage = 1; // Reset to first page when changing page size - StateHasChanged(); - } - }private void OnDatabaseCredentialChanged(ChangeEventArgs e) - { - selectedDatabaseCredential = e.Value?.ToString() ?? ""; - ResetDatabaseState(); - } private void OnRestCredentialChanged(ChangeEventArgs e) - { - var newCredential = e.Value?.ToString() ?? ""; - - // Clear the cache if we're switching to a different credential - if (!string.IsNullOrEmpty(selectedRestCredential) && selectedRestCredential != newCredential) - { - ConnectionFactory.ClearRestClientCache(selectedRestCredential); - Logger.LogInformation("Cleared REST client cache for credential: {CredentialName}", selectedRestCredential); - } - - selectedRestCredential = newCredential; - ResetRestState(); - } private void ResetDatabaseState() - { - isDatabaseConnected = false; - databaseTables.Clear(); - selectedTable = ""; - databaseSearchTerm = ""; - databaseErrorMessage = ""; - - // Reset custom query state - useCustomQuery = false; - customQuery = ""; - isValidatingQuery = false; - isQueryValid = false; - queryValidationMessage = ""; - queryPreviewData.Clear(); - queryColumns.Clear(); - showQueryPreview = false; - isLoadingPreview = false; - - currentDatabaseManager?.Dispose(); - currentDatabaseManager = null; - - // Clear mappings when resetting database state - ClearAllMappings(); - } private void ResetRestState() - { - isRestConnected = false; - restEntities.Clear(); - selectedRestEntity = null; - restEntityDetails = null; - restSearchTerm = ""; - restErrorMessage = ""; - currentRestDiscovery = null; - currentRestClient = null; - - // Clear mappings when resetting REST state - ClearAllMappings(); - }private async Task ConnectToDatabase() - { - if (string.IsNullOrEmpty(selectedDatabaseCredential)) - return; - - isConnectingDatabase = true; - databaseErrorMessage = ""; - - try - { // Trova la credenziale - var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential); - if (credential == null) - { - databaseErrorMessage = "Credenziale database non trovata"; - return; - } - - // Test della connessione - var (success, message) = await CredentialService.TestDatabaseConnectionAsync(credential.Name); - if (!success) - { - 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 con try-catch specifico - try - { - 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> ?? - (schema != null ? new Dictionary>(schema) : new Dictionary>()); - - Logger.LogInformation("Database tables dopo conversione: {Count} tabelle", databaseTables.Count); - - if (databaseTables.Count == 0) - { - // Se non ci sono tabelle, potrebbe essere perché non è stato selezionato un database specifico - await HandleDatabaseSelectionRequired(); - return; - } - } - catch (Exception schemaEx) - { - Logger.LogError(schemaEx, "Errore specifico durante lo schema discovery"); - databaseErrorMessage = $"Errore nello schema discovery: {schemaEx.Message}"; - throw; - } - - isDatabaseConnected = true; - } - catch (Exception ex) - { - Logger.LogError(ex, "Errore nella connessione al database"); - databaseErrorMessage = $"Errore: {ex.Message}"; - } - finally - { - isConnectingDatabase = false; - } - } private async Task ConnectToRestApi() - { - if (string.IsNullOrEmpty(selectedRestCredential)) - return; - - isConnectingRest = true; - restErrorMessage = ""; - - try - { - // Trova la credenziale - var credential = restApiCredentials.FirstOrDefault(c => c.Name == selectedRestCredential); - if (credential == null) - { - restErrorMessage = "Credenziale REST API non trovata"; - return; - } - - // Test della connessione - var (success, message) = await CredentialService.TestRestApiConnectionAsync(credential.Name); - if (!success) - { - restErrorMessage = $"Connessione fallita: {message}"; - return; - } // Crea i client REST usando il factory con le credenziali complete - currentRestClient = await ConnectionFactory.CreateRestServiceClientAsync(selectedRestCredential); - currentRestDiscovery = await ConnectionFactory.CreateRestMetadataDiscoveryAsync(selectedRestCredential); Logger.LogInformation("Iniziando autenticazione per il servizio REST {ServiceType} con credenziale: {CredentialName}", credential.ServiceType, selectedRestCredential); - - // Autenticazione prima del discovery - var authResult = await currentRestClient.AuthenticateAsync(); - if (!authResult) - { - Logger.LogWarning("Autenticazione fallita per il servizio REST {ServiceType}", credential.ServiceType); - restErrorMessage = "Autenticazione fallita per il servizio REST"; - return; - } - - Logger.LogInformation("Autenticazione completata. Iniziando discovery delle entità REST per {ServiceType}", credential.ServiceType); - - // Discovery delle entità disponibili - restEntities = await currentRestDiscovery.DiscoverEntitySummariesAsync(); - - Logger.LogInformation("Discovery completato. Trovate {Count} entità", restEntities?.Count ?? 0); - - if (restEntities == null || !restEntities.Any()) - { - Logger.LogWarning("Nessuna entità trovata dal servizio REST"); - restErrorMessage = "Nessuna entità disponibile dal servizio REST"; - return; - } - - isRestConnected = true; - } - catch (Exception ex) - { - Logger.LogError(ex, "Errore nella connessione al servizio REST"); - restErrorMessage = $"Errore: {ex.Message}"; - } - finally - { - isConnectingRest = false; - } - } private async void SelectTable(string tableName) - { - selectedTable = tableName; - - // Clear custom query state when selecting a table - useCustomQuery = false; - customQuery = ""; - isQueryValid = false; - queryValidationMessage = ""; - queryPreviewData.Clear(); - queryColumns.Clear(); - showQueryPreview = false; - - // 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; - - // Clear mappings when changing entity - ClearAllMappings(); - - try - { - if (currentRestDiscovery != null) - { - // Discovery dei dettagli dell'entità - restEntityDetails = await currentRestDiscovery.DiscoverEntityDetailsAsync(entity.Name); } - else - { - restErrorMessage = "Servizio di discovery REST non disponibile"; - return; - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Errore nel caricamento dettagli entità {EntityName}", entity.Name); - restErrorMessage = $"Errore nel caricamento dettagli entità: {ex.Message}"; - } } - - // Metodi per la ricerca e il filtraggio - private IEnumerable GetFilteredDatabaseTables() - { - if (string.IsNullOrEmpty(databaseSearchTerm)) - return databaseTables.Keys; - - return databaseTables.Keys.Where(table => - table.Contains(databaseSearchTerm, StringComparison.OrdinalIgnoreCase)); - } - - private IEnumerable GetFilteredRestEntities() - { - if (string.IsNullOrEmpty(restSearchTerm)) - return restEntities; - - return restEntities.Where(entity => - entity.Name.Contains(restSearchTerm, StringComparison.OrdinalIgnoreCase) || - (!string.IsNullOrEmpty(entity.Label) && entity.Label.Contains(restSearchTerm, StringComparison.OrdinalIgnoreCase))); - } - - private async Task FilterDatabaseTables(ChangeEventArgs e) - { - databaseSearchTerm = e.Value?.ToString() ?? ""; - await InvokeAsync(StateHasChanged); - } - - private async Task FilterRestEntities(ChangeEventArgs e) - { - restSearchTerm = e.Value?.ToString() ?? ""; - await InvokeAsync(StateHasChanged); - } - - private async Task ClearDatabaseSearch() - { - databaseSearchTerm = ""; - await InvokeAsync(StateHasChanged); - } - - private async Task ClearRestSearch() - { - restSearchTerm = ""; - await InvokeAsync(StateHasChanged); - } - - // Metodi per il mapping dei campi - private void SelectDbColumn(string columnName) - { - selectedDbColumn = columnName; - } - - private void SelectRestProperty(string propertyName) - { - selectedRestProperty = propertyName; - } - - private void CreateMapping() - { - if (string.IsNullOrEmpty(selectedDbColumn) || string.IsNullOrEmpty(selectedRestProperty)) - return; - - // Rimuovi eventuali mapping esistenti per questo campo database - if (fieldMappings.ContainsKey(selectedDbColumn)) - { - fieldMappings.Remove(selectedDbColumn); - } - - // Crea il nuovo mapping - fieldMappings[selectedDbColumn] = selectedRestProperty; - - Logger.LogInformation("Creato mapping: {DbColumn} -> {RestProperty}", selectedDbColumn, selectedRestProperty); - - // Deseleziona i campi - selectedDbColumn = ""; - selectedRestProperty = ""; - } - - private void RemoveMapping() - { - if (string.IsNullOrEmpty(selectedDbColumn) || !fieldMappings.ContainsKey(selectedDbColumn)) - return; - - fieldMappings.Remove(selectedDbColumn); - Logger.LogInformation("Rimosso mapping per campo: {DbColumn}", selectedDbColumn); - } private void RemoveSpecificMapping(string dbColumn) - { - if (fieldMappings.ContainsKey(dbColumn)) - { - fieldMappings.Remove(dbColumn); - Logger.LogInformation("Rimosso mapping specifico per campo: {DbColumn}", dbColumn); - } - } - - private void ClearAllMappings() - { - fieldMappings.Clear(); - selectedDbColumn = ""; - selectedRestProperty = ""; - sourceKeyField = ""; - transferMessage = ""; - transferMessageType = ""; - Logger.LogInformation("Tutti i mapping e le configurazioni sono stati cancellati"); - } - - private void AutoMapFields() - { - if (restEntityDetails == null) - return; - - IEnumerable sourceColumns = new List(); - - // Ottiene le colonne in base al tipo di sorgente - if (selectedSourceType == "database") - { - if (useCustomQuery && queryColumns.Any()) - { - sourceColumns = queryColumns; - } - else if (!useCustomQuery && databaseTables.ContainsKey(selectedTable)) - { - sourceColumns = databaseTables[selectedTable].Select(c => c.Name); - } - } - else if (selectedSourceType == "file" && fileSheets.ContainsKey(selectedSheet)) - { - sourceColumns = fileSheets[selectedSheet]; - } - - if (!sourceColumns.Any()) - return; - - var restProperties = restEntityDetails.Properties; - int mappingsCreated = 0; - - foreach (var sourceColumn in sourceColumns) - { - // Trova una proprietà REST con nome simile - var matchingProperty = restProperties.FirstOrDefault(p => - string.Equals(p.Name, sourceColumn, StringComparison.OrdinalIgnoreCase) || - string.Equals(p.Name.Replace("_", ""), sourceColumn.Replace("_", ""), StringComparison.OrdinalIgnoreCase) || - string.Equals(p.Name.Replace("Id", ""), sourceColumn.Replace("Id", ""), StringComparison.OrdinalIgnoreCase) - ); - - if (matchingProperty != null && !fieldMappings.ContainsKey(sourceColumn)) - { - fieldMappings[sourceColumn] = matchingProperty.Name; - mappingsCreated++; - } - } - - Logger.LogInformation("Auto-mapping completato. Creati {Count} mapping automatici da {SourceType}", - mappingsCreated, useCustomQuery ? "query custom" : selectedSourceType); - } private async Task ShowMappingSummary() - { - 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() - { - if (!fieldMappings.Any() || currentRestClient == null || selectedRestEntity == null) - { - transferMessage = "Configurazione incompleta. Assicurati di aver selezionato la fonte dati, entità e configurato almeno una mappatura."; - transferMessageType = "error"; - return; - } - - // Check source-specific requirements - if (selectedSourceType == "database") - { - if (currentDatabaseManager == null) - { - transferMessage = "Database non connesso."; - transferMessageType = "error"; - return; - } - - if (useCustomQuery) - { - if (!isQueryValid || string.IsNullOrWhiteSpace(customQuery)) - { - transferMessage = "Query custom non valida. Validare la query prima di procedere."; - transferMessageType = "error"; - return; - } - } - else if (string.IsNullOrEmpty(selectedTable)) - { - transferMessage = "Tabella non selezionata."; - transferMessageType = "error"; - return; - } - } - - if (selectedSourceType == "file" && string.IsNullOrEmpty(selectedSheet)) - { - transferMessage = "File non caricato o foglio non selezionato."; - 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 - { - var sourceName = selectedSourceType == "database" - ? (useCustomQuery ? "custom_query" : selectedTable) - : selectedSheet; - Logger.LogInformation("Iniziando trasferimento dati da {SourceType} {Source} a {Entity} con {MappingCount} mappature", - selectedSourceType, sourceName, selectedRestEntity.Name, fieldMappings.Count); - - // 1. Ottieni tutti i record dalla fonte dati - var records = await GetAllRecordsFromSource(); - Logger.LogInformation("Ottenuti {RecordCount} record da {SourceType} {Source}", records.Count(), selectedSourceType, sourceName); - - if (!records.Any()) - { - transferMessage = "Nessun record trovato nella fonte dati selezionata."; - transferMessageType = "error"; - return; - } - - // 2. Ottieni i campi obbligatori dell'entità REST (se non ci sono campi chiave) - var requiredFields = new HashSet(); - 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(); - int recordNumber = 1; - - foreach (var record in records) - { - var transferResult = new TransferResult - { - RecordNumber = recordNumber, - RecordData = new Dictionary(record) - }; - - try - { - // Trasforma il record in base ai mapping - var restData = TransformRecordToRestEntity(record); - - // Genera la chiave sorgente per questo record - var sourceKey = GenerateSourceKey(record); - - // NUOVO SISTEMA: Cerca associazione esistente basata sul valore della chiave - if (useRecordAssociations && !string.IsNullOrEmpty(sourceKey)) - { - 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) - { - // Prova direttamente l'aggiornamento - più efficiente che verificare prima l'esistenza - Logger.LogInformation("ASSOCIATION DEBUG: Tentativo aggiornamento record esistente - DestinationId: '{DestinationId}'", existingAssociation.DestinationId); - - try - { - 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 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; - } - - HandleInvalidAssociation: - // L'ID di destinazione non esiste più o l'update è fallito - elimina l'associazione non valida - try - { - await CredentialService.DeleteKeyAssociationAsync(existingAssociation.Id); - Logger.LogInformation("ASSOCIATION DEBUG: Associazione non valida eliminata: {AssociationId}", existingAssociation.Id); - } - catch (Exception delEx) - { - Logger.LogWarning(delEx, "Errore nell'eliminazione dell'associazione non valida {AssociationId}", existingAssociation.Id); - } - - 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) - } - } - - 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() : - 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 - { - // Determina i campi chiave automaticamente - var destinationKeyField = GetEntityIdField(); // Campo chiave nella destinazione - - var association = new KeyAssociation - { - 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, - SourceType = selectedSourceType - }) - }; - - 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) - { - 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++; - transferResult.Status = "error"; - transferResult.Message = "Errore nel trasferimento del record (result null)"; - errors.Add($"Errore nel trasferimento del record {recordNumber}"); - } - } - catch (Exception ex) - { - errorCount++; - 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++; - } - - // 4. Mostra risultati - if (errorCount == 0) - { - var message = $"Trasferimento completato con successo! "; - var messageParts = new List(); - - 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 - { - var message = $"Trasferimento completato con {(duplicateCount > 0 ? "warning e " : "")}errori. "; - var messageParts = new List(); - - 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. Inserimenti: {SuccessCount}, Aggiornamenti: {UpdatedCount}, Duplicati: {DuplicateCount}, Errori: {ErrorCount}", - successCount, updatedCount, duplicateCount, errorCount); - } - catch (Exception ex) - { - Logger.LogError(ex, "Errore generale nel trasferimento dati"); - transferMessage = $"Errore nel trasferimento dati: {ex.Message}"; - transferMessageType = "error"; - } - finally - { - isTransferringData = false; - } - } private async Task>> GetAllRecordsFromSource() - { - if (selectedSourceType == "database") - { - return await GetAllRecordsFromDatabase(); - } - else if (selectedSourceType == "file") - { - return await GetAllRecordsFromFile(); - } - - return new List>(); - } - - private async Task>> GetAllRecordsFromDatabase() - { - if (currentDatabaseManager == null) - return new List>(); - - try - { - if (useCustomQuery) - { - // Usa la query custom per ottenere tutti i record - if (!isQueryValid || string.IsNullOrWhiteSpace(customQuery)) - { - throw new InvalidOperationException("Query custom non valida. Validare la query prima di procedere."); - } - - // CONTROLLO DI SICUREZZA AGGIUNTIVO: Verifica che sia ancora una SELECT - if (!IsSelectQuery(customQuery)) - { - throw new InvalidOperationException("ERRORE DI SICUREZZA: Tentativo di eseguire una query non SELECT. Operazione bloccata per sicurezza."); - } - - var cleanQuery = CleanQuery(customQuery); - Logger.LogInformation("Esecuzione query custom per trasferimento dati: {Query}", cleanQuery); - - return await currentDatabaseManager.ExecuteRawQueryAsync(cleanQuery); - } - else - { - // Usa il metodo standard per tabelle - if (string.IsNullOrEmpty(selectedTable)) - { - throw new InvalidOperationException("Nessuna tabella selezionata."); - } - - return await currentDatabaseManager.GetAllRecordsAsync(selectedTable); - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Errore nell'ottenere i record dal database. UseCustomQuery: {UseCustomQuery}, Table: {Table}, Query: {Query}", - useCustomQuery, selectedTable, useCustomQuery ? customQuery : "N/A"); - throw; - } - } private async Task>> GetAllRecordsFromFile() - { - if (string.IsNullOrEmpty(selectedSheet) || !fileData.ContainsKey(selectedSheet)) - { - return new List>(); - } - - await Task.CompletedTask; - return fileData[selectedSheet]; - } - - private Dictionary TransformRecordToRestEntity(Dictionary dbRecord) - { - var restData = new Dictionary(); - - foreach (var mapping in fieldMappings) - { - string dbColumn = mapping.Key; - string restProperty = mapping.Value; - - if (dbRecord.ContainsKey(dbColumn)) - { - var value = dbRecord[dbColumn]; - - // Trasforma il valore se necessario (es. date format, null handling, etc.) - var transformedValue = TransformValue(value, dbColumn, restProperty); - - if (transformedValue != null) - { - restData[restProperty] = transformedValue; - } - } - } - - Logger.LogDebug("Record trasformato: {DbColumns} → {RestProperties}", - string.Join(", ", dbRecord.Keys), - string.Join(", ", restData.Keys)); - - return restData; - } - - private object? TransformValue(object? value, string dbColumn, string restProperty) - { - if (value == null || value == DBNull.Value) - return null; - - // Ottieni informazioni sui tipi per fare trasformazioni intelligenti - var dbColumnInfo = databaseTables.ContainsKey(selectedTable) - ? databaseTables[selectedTable].FirstOrDefault(c => c.Name == dbColumn) - : null; - - var restPropertyInfo = restEntityDetails?.Properties.FirstOrDefault(p => p.Name == restProperty); - - // Trasformazioni specifiche per tipo - if (restPropertyInfo != null) - { - switch (restPropertyInfo.Type.ToLower()) - { - case "edm.string": - return value.ToString(); - - case "edm.int32": - case "edm.int64": - if (int.TryParse(value.ToString(), out int intVal)) - return intVal; - break; - - case "edm.decimal": - case "edm.double": - if (decimal.TryParse(value.ToString(), out decimal decVal)) - return decVal; - break; - - case "edm.boolean": - if (bool.TryParse(value.ToString(), out bool boolVal)) - return boolVal; - // Gestisci anche valori numerici (0/1) come boolean - if (value.ToString() == "1") return true; - if (value.ToString() == "0") return false; - break; - - case "edm.datetime": - case "edm.datetimeoffset": - if (DateTime.TryParse(value.ToString(), out DateTime dateVal)) - return dateVal.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); - break; - } - } - - // Fallback: restituisci il valore convertito a stringa - return value.ToString(); - } - - private string GetPropertyPlaceholder(RestPropertyInfo property) - { - return property.Type switch - { - "Edm.String" => $"Inserisci {property.Name}" + (property.MaxLength.HasValue ? $" (max {property.MaxLength})" : ""), - "Edm.Int32" => "Numero intero", - "Edm.Decimal" => "Numero decimale", - "Edm.DateTime" => "Data/Ora (YYYY-MM-DD)", - "Edm.Boolean" => "true/false", - _ => $"Valore per {property.Name}" - }; - } - - public void Dispose() - { - currentDatabaseManager?.Dispose(); - } - - private char DetectCsvSeparator(string line) - { - // Common separators to check - var separators = new[] { ',', ';', '\t', '|' }; - var counts = new Dictionary(); - - bool inQuotes = false; - - // Count separators outside of quotes - foreach (char c in line) - { - if (c == '"') - { - inQuotes = !inQuotes; - } - else if (!inQuotes && separators.Contains(c)) - { - counts[c] = counts.GetValueOrDefault(c, 0) + 1; - } - } - - // Return the separator with the highest count, default to comma - if (counts.Any()) - { - var mostCommon = counts.OrderByDescending(x => x.Value).First(); - // Make sure we have at least one occurrence to avoid single-column files - if (mostCommon.Value > 0) - { - return mostCommon.Key; - } - } - return ','; // Default fallback - } - - /// - /// Verifica se il pulsante di trasferimento può essere abilitato - /// - 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" - }; - } - - /// - /// Genera una chiave univoca per il record sorgente - /// - private string GenerateSourceKey(Dictionary 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."); - } - - // Normalizza il valore della chiave (trim e gestione case-sensitive) - return keyValue.Trim(); - } - 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> ?? - (schema != null ? new Dictionary>(schema) : new Dictionary>()); - - 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(); - } - - /// - /// Ottiene il nome del campo ID per l'entità corrente - /// - private string GetEntityIdField() - { - // Fallback predefiniti in base al tipo di servizio/entità - if (selectedRestEntity?.Name != null) - { - // Per SAP B1, la maggior parte delle entità usa DocEntry - if (selectedRestEntity.Name.Contains("BusinessPartner") || - selectedRestEntity.Name.Contains("Customer") || - selectedRestEntity.Name.Contains("Vendor")) - { - return "CardCode"; - } - - if (selectedRestEntity.Name.Contains("Item") || - selectedRestEntity.Name.Contains("Product")) - { - return "ItemCode"; - } - } - - // Usa campi ID comuni come fallback - var commonIdFields = new[] { "DocEntry", "Id", "ID", "id", "Key", "key", "Code", "code" }; - - // Per ora usa DocEntry come default per SAP B1 - return "DocEntry"; - } - - // Custom Query Methods - private void OnQueryModeChanged(ChangeEventArgs e) - { - useCustomQuery = bool.Parse(e.Value?.ToString() ?? "false"); - - if (useCustomQuery) - { - // Reset table selection when switching to custom query - selectedTable = ""; - ClearAllMappings(); - - // Reset query-specific state - customQuery = ""; - isQueryValid = false; - queryValidationMessage = ""; - queryPreviewData.Clear(); - queryColumns.Clear(); - showQueryPreview = false; - - // For custom queries, require manual key selection - sourceKeyField = ""; - suggestedPrimaryKey = ""; - requiresManualKeySelection = true; - } - else - { - // Reset custom query when switching to table mode - customQuery = ""; - isQueryValid = false; - queryValidationMessage = ""; - queryPreviewData.Clear(); - queryColumns.Clear(); - showQueryPreview = false; - ClearAllMappings(); - - // Reset key field selection - sourceKeyField = ""; - suggestedPrimaryKey = ""; - requiresManualKeySelection = false; - } - - StateHasChanged(); - } - - private async Task ValidateCustomQuery() - { - if (string.IsNullOrWhiteSpace(customQuery) || currentDatabaseManager == null) - { - isQueryValid = false; - queryValidationMessage = "Query vuota o database non connesso"; - return; - } - - // CONTROLLO DI SICUREZZA: Verifica che sia una SELECT - if (!IsSelectQuery(customQuery)) - { - isQueryValid = false; - queryValidationMessage = "ERRORE DI SICUREZZA: Sono permesse solo query SELECT. Operazioni come INSERT, UPDATE, DELETE, DROP, CREATE, ALTER, TRUNCATE non sono consentite."; - Logger.LogWarning("Tentativo di eseguire query non SELECT bloccato: {Query}", customQuery.Length > 100 ? customQuery.Substring(0, 100) + "..." : customQuery); - return; - } - - isValidatingQuery = true; - queryValidationMessage = ""; - queryColumns.Clear(); - - try - { - // Converte la query per testare solo 1 riga - var testQuery = ConvertQueryForValidation(customQuery); - - Logger.LogInformation("Validazione query: {TestQuery}", testQuery); - - // Esegue la query di test - var testResults = await currentDatabaseManager.ExecuteRawQueryAsync(testQuery); - - if (testResults != null && testResults.Any()) - { - isQueryValid = true; - - // Estrae i nomi delle colonne dal primo record - var firstRecord = testResults.First(); - queryColumns = firstRecord.Keys.ToList(); - - // Non mostra più messaggi di successo per ridurre l'ingombro visivo - queryValidationMessage = ""; - - Logger.LogInformation("Query validata con successo. Colonne: {Columns}", string.Join(", ", queryColumns)); - - // Clear existing mappings since we have new columns - fieldMappings.Clear(); - selectedDbColumn = ""; - selectedRestProperty = ""; - - // For custom queries, always require manual key selection - sourceKeyField = ""; - suggestedPrimaryKey = ""; - requiresManualKeySelection = true; - - StateHasChanged(); - } - else - { - isQueryValid = false; - queryValidationMessage = "Query valida ma non restituisce risultati"; - queryColumns.Clear(); - } - } - catch (Exception ex) - { - isQueryValid = false; - queryValidationMessage = $"Errore nella validazione: {ex.Message}"; - queryColumns.Clear(); - Logger.LogError(ex, "Errore nella validazione della query custom"); - } - finally - { - isValidatingQuery = false; - StateHasChanged(); - } - } - - private async Task LoadQueryPreview() - { - if (!isQueryValid || string.IsNullOrWhiteSpace(customQuery) || currentDatabaseManager == null) - { - return; - } - - isLoadingPreview = true; - - try - { - // Usa la query limitata per il preview (max 50 righe per performance) - var previewQuery = ConvertQueryForPreview(customQuery, 50); - - Logger.LogInformation("Caricamento preview query: {PreviewQuery}", previewQuery); - - queryPreviewData = await currentDatabaseManager.ExecuteRawQueryAsync(previewQuery); - showQueryPreview = true; - - Logger.LogInformation("Preview caricato: {RowCount} righe", queryPreviewData.Count); - } - catch (Exception ex) - { - queryValidationMessage = $"Errore nel caricamento preview: {ex.Message}"; - Logger.LogError(ex, "Errore nel caricamento del preview della query"); - } - finally - { - isLoadingPreview = false; - StateHasChanged(); - } - } - - private void HideQueryPreview() - { - showQueryPreview = false; - queryPreviewData.Clear(); - StateHasChanged(); - } - - private string ConvertQueryForValidation(string originalQuery) - { - // Rimuove commenti e spazi extra - var cleanQuery = CleanQuery(originalQuery); - - // Se la query ha già un LIMIT/TOP, la usa così com'è per il test - if (HasLimitClause(cleanQuery)) - { - return cleanQuery; - } - - // Aggiunge LIMIT/TOP in base al tipo di database - return AddLimitClause(cleanQuery, 1); - } - - private string ConvertQueryForPreview(string originalQuery, int maxRows = 50) - { - var cleanQuery = CleanQuery(originalQuery); - - // Se la query ha già un LIMIT/TOP con un valore minore, la mantiene - if (HasLimitClause(cleanQuery)) - { - return cleanQuery; - } - - return AddLimitClause(cleanQuery, maxRows); - } - - private string CleanQuery(string query) - { - if (string.IsNullOrWhiteSpace(query)) - return ""; - - // Rimuove commenti SQL - var lines = query.Split('\n') - .Select(line => line.Contains("--") ? line.Substring(0, line.IndexOf("--")) : line) - .Where(line => !string.IsNullOrWhiteSpace(line)); - - var cleanQuery = string.Join(" ", lines).Trim(); - - // Rimuove il punto e virgola finale se presente - if (cleanQuery.EndsWith(";")) - { - cleanQuery = cleanQuery.Substring(0, cleanQuery.Length - 1); - } - - return cleanQuery; - } - - private bool HasLimitClause(string query) - { - var upperQuery = query.ToUpperInvariant(); - return upperQuery.Contains(" LIMIT ") || - upperQuery.Contains(" TOP ") || - upperQuery.Contains("ROWNUM") || - upperQuery.Contains("FETCH FIRST"); - } - - private string AddLimitClause(string query, int limit) - { - var upperQuery = query.ToUpperInvariant(); - - // Per SQL Server, Oracle, e altri che supportano TOP - if (upperQuery.Contains("SELECT ")) - { - var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential); - if (credential != null) - { - var dbType = credential.DatabaseType.ToString().ToLowerInvariant(); - switch (dbType) - { - case "sqlserver": - case "oracle": - // Aggiunge TOP dopo SELECT - return query.Replace("SELECT ", $"SELECT TOP {limit} ", StringComparison.OrdinalIgnoreCase); - - case "mysql": - case "postgresql": - case "sqlite": - default: - // Aggiunge LIMIT alla fine - return $"{query} LIMIT {limit}"; - } - } - } - - // Fallback: aggiunge LIMIT - return $"{query} LIMIT {limit}"; - } - - private void OnCustomQueryChanged(ChangeEventArgs e) - { - customQuery = e.Value?.ToString() ?? ""; - - // Reset validation quando la query cambia - isQueryValid = false; - queryValidationMessage = ""; - queryPreviewData.Clear(); - queryColumns.Clear(); - showQueryPreview = false; - - // Clear mappings quando la query cambia - ClearAllMappings(); - - // Reset key field selection - sourceKeyField = ""; - suggestedPrimaryKey = ""; - requiresManualKeySelection = true; - - StateHasChanged(); - } - - /// - /// Verifica che la query sia una SELECT e non contenga operazioni pericolose - /// - private bool IsSelectQuery(string query) - { - if (string.IsNullOrWhiteSpace(query)) - return false; - - // Rimuovi commenti e normalizza la query - var cleanQuery = CleanQueryForSecurityCheck(query); - - // Lista delle operazioni pericolose che non sono permesse - var dangerousKeywords = new[] - { - "INSERT", "UPDATE", "DELETE", "DROP", "CREATE", "ALTER", - "TRUNCATE", "REPLACE", "MERGE", "EXEC", "EXECUTE", - "DECLARE", "SET", "GRANT", "REVOKE", "BACKUP", "RESTORE", - "SHUTDOWN", "KILL", "LOAD", "BULK", "OPENROWSET", "OPENDATASOURCE" - }; - - // Verifica che non contenga operazioni pericolose - foreach (var keyword in dangerousKeywords) - { - if (cleanQuery.Contains($" {keyword} ", StringComparison.OrdinalIgnoreCase) || - cleanQuery.StartsWith($"{keyword} ", StringComparison.OrdinalIgnoreCase) || - cleanQuery.Contains($";{keyword} ", StringComparison.OrdinalIgnoreCase) || - cleanQuery.Contains($"\n{keyword} ", StringComparison.OrdinalIgnoreCase) || - cleanQuery.Contains($"\r{keyword} ", StringComparison.OrdinalIgnoreCase)) - { - Logger.LogWarning("Query bloccata: contiene keyword pericolosa '{Keyword}' in query: {QueryStart}", - keyword, query.Length > 50 ? query.Substring(0, 50) + "..." : query); - return false; - } - } - - // Verifica che inizi con SELECT (permettendo spazi e commenti iniziali) - var trimmedQuery = cleanQuery.TrimStart(); - if (!trimmedQuery.StartsWith("SELECT", StringComparison.OrdinalIgnoreCase)) - { - Logger.LogWarning("Query bloccata: non inizia con SELECT. Query: {QueryStart}", - query.Length > 50 ? query.Substring(0, 50) + "..." : query); - return false; - } - - // Verifica addizionale: non deve contenere punto e virgola seguito da altra query - var statements = cleanQuery.Split(';', StringSplitOptions.RemoveEmptyEntries); - if (statements.Length > 1) - { - // Se ci sono multiple statements, tutte devono essere SELECT o commenti vuoti - foreach (var statement in statements) - { - var trimmedStatement = statement.Trim(); - if (!string.IsNullOrEmpty(trimmedStatement) && - !trimmedStatement.StartsWith("SELECT", StringComparison.OrdinalIgnoreCase)) - { - Logger.LogWarning("Query bloccata: contiene multiple statements non SELECT. Query: {QueryStart}", - query.Length > 50 ? query.Substring(0, 50) + "..." : query); - return false; - } - } - } - - return true; - } - - /// - /// Pulisce la query per il controllo di sicurezza rimuovendo commenti - /// - private string CleanQueryForSecurityCheck(string query) - { - if (string.IsNullOrEmpty(query)) - return ""; - - var lines = query.Split('\n'); - var cleanedLines = new List(); - - foreach (var line in lines) - { - var cleanedLine = line.Trim(); - - // Rimuovi commenti SQL (-- e /* */) - var dashCommentIndex = cleanedLine.IndexOf("--"); - if (dashCommentIndex >= 0) - { - cleanedLine = cleanedLine.Substring(0, dashCommentIndex).Trim(); - } - - // Gestione commenti multiline /* */ - implementazione base - cleanedLine = System.Text.RegularExpressions.Regex.Replace(cleanedLine, @"/\*.*?\*/", " ", - System.Text.RegularExpressions.RegexOptions.IgnoreCase); - - if (!string.IsNullOrWhiteSpace(cleanedLine)) - { - cleanedLines.Add(cleanedLine); - } - } - - return string.Join(" ", cleanedLines); - } -} diff --git a/Data_Coupler/Pages/DataCoupler.razor.cs b/Data_Coupler/Pages/DataCoupler.razor.cs new file mode 100644 index 0000000..baa9c24 --- /dev/null +++ b/Data_Coupler/Pages/DataCoupler.razor.cs @@ -0,0 +1,2018 @@ +using System; +using System.Data; +using System.Text; +using CredentialManager.Models; +using DataConnection.Interfaces; +using DataConnection.REST.Interfaces; +using DataConnection.REST.Models; +using ExcelDataReader; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Forms; +using Microsoft.JSInterop; + +namespace Data_Coupler.Pages; + +public partial class DataCoupler +{ +// 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 RecordData { get; set; } = new(); + } + + // Stato delle credenziali + private List databaseCredentials = new(); + private List restApiCredentials = new(); + + // Selezione tipo fonte + private string selectedSourceType = ""; + + // Credenziali selezionate + private string selectedDatabaseCredential = ""; + private string selectedRestCredential = ""; + + // Stato connessioni + private bool isConnectingDatabase = false; + private bool isConnectingRest = false; + private bool isDatabaseConnected = false; + private bool isRestConnected = false; + + // Messaggi di errore + private string databaseErrorMessage = ""; + private string restErrorMessage = ""; + + // Database discovery + private Dictionary> databaseTables = new(); + private string selectedTable = ""; + private string databaseSearchTerm = ""; + + // Database selection + private List availableDatabases = new(); + private string selectedDatabase = ""; + private bool showDatabaseSelection = false; + private bool showDatabaseSelectionModal = false; + private bool isLoadingDatabases = false; + + // Custom query functionality + private bool useCustomQuery = false; + private string customQuery = ""; + private bool isValidatingQuery = false; + private bool isQueryValid = false; + private string queryValidationMessage = ""; + private List> queryPreviewData = new(); + private List queryColumns = new(); + private bool showQueryPreview = false; + private bool isLoadingPreview = false; // File handling + private string selectedFileName = ""; + private bool isProcessingFile = false; + private string fileErrorMessage = ""; + private Dictionary> fileSheets = new(); // SheetName -> Columns + private Dictionary>> fileData = new(); // SheetName -> Data rows + private string selectedSheet = ""; + + // File preview pagination + private int currentPage = 1; + private int pageSize = 20; + private int GetTotalPages(string sheetName) => fileData.ContainsKey(sheetName) ? + (int)Math.Ceiling((double)fileData[sheetName].Count / pageSize) : 0; + + // REST discovery + private List restEntities = new(); + private RestEntitySummary? selectedRestEntity = null; + private RestEntityInfo? restEntityDetails = null; + private string restSearchTerm = ""; + // Mapping campi + private Dictionary fieldMappings = new(); // DbColumn -> RestProperty + private HashSet 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 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 transferResults = new(); + private bool showDetailedResults = false; + + // Servizi + private IDatabaseManager? currentDatabaseManager = null; + private IRestMetadataDiscovery? currentRestDiscovery = null; + private IRestServiceClient? currentRestClient = null; + + protected override async Task OnInitializedAsync() + { + await LoadCredentials(); + } private async Task LoadCredentials() + { + try + { + databaseCredentials = await CredentialService.GetAllDatabaseCredentialsAsync(); + restApiCredentials = await CredentialService.GetAllRestApiCredentialsAsync(); + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nel caricamento delle credenziali"); + await JSRuntime.InvokeVoidAsync("alert", $"Errore nel caricamento delle credenziali: {ex.Message}"); + } + } + + private void OnSourceTypeChanged(ChangeEventArgs e) + { + selectedSourceType = e.Value?.ToString() ?? ""; + + // Reset state when changing source type + ResetSourceState(); } private void ResetSourceState() + { + // Reset database state + ResetDatabaseState(); + + // Reset file state + selectedFileName = ""; + isProcessingFile = false; + fileErrorMessage = ""; + fileSheets.Clear(); + fileData.Clear(); + selectedSheet = ""; + + // Reset pagination + currentPage = 1; + + // Reset mappings + ClearAllMappings(); + } + + private async Task OnFileSelected(InputFileChangeEventArgs e) + { try + { + isProcessingFile = true; + fileErrorMessage = ""; + fileSheets.Clear(); + fileData.Clear(); + selectedSheet = ""; + + var file = e.File; + selectedFileName = file.Name; + + // Validate file type + var extension = Path.GetExtension(file.Name).ToLowerInvariant(); + if (extension != ".xlsx" && extension != ".xls" && extension != ".csv") + { + fileErrorMessage = "Formato file non supportato. Utilizzare Excel (.xlsx, .xls) o CSV (.csv)"; + return; + } + + // Process file based on type + if (extension == ".csv") + { + await ProcessCsvFile(file); + } + else + { + await ProcessExcelFile(file); + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nell'elaborazione del file"); + fileErrorMessage = $"Errore nell'elaborazione del file: {ex.Message}"; + } + finally + { + isProcessingFile = false; + StateHasChanged(); + } + } private async Task ProcessCsvFile(IBrowserFile file) + { + using var stream = file.OpenReadStream(maxAllowedSize: 50 * 1024 * 1024); // Aumentato a 50MB + using var reader = new StreamReader(stream); + + var firstLine = await reader.ReadLineAsync(); + if (string.IsNullOrEmpty(firstLine)) + { + fileErrorMessage = "Il file CSV è vuoto"; + return; + } + + Logger.LogInformation("CSV first line: {FirstLine}", firstLine); + + // Detect separator automatically + var separator = DetectCsvSeparator(firstLine); + Logger.LogInformation("CSV separator detected: '{Separator}'", separator); + + // Parse headers (first row) - gestisce meglio i separatori + var headers = ParseCsvLine(firstLine, separator); + + Logger.LogInformation("CSV headers parsed: {Headers}", string.Join(" | ", headers)); + // For CSV, we create a single "sheet" with the filename + var sheetName = Path.GetFileNameWithoutExtension(file.Name); + fileSheets[sheetName] = headers; + + // Read data rows - rimuovo il limite di 1000 righe + var dataRows = new List>(); + string? line; + int rowNumber = 2; // Starting from row 2 (after header) + + while ((line = await reader.ReadLineAsync()) != null) + { + if (string.IsNullOrWhiteSpace(line)) continue; + + var values = ParseCsvLine(line, separator); + var row = new Dictionary(); + for (int i = 0; i < headers.Count; i++) + { + var value = i < values.Count ? values[i] : ""; + row[headers[i]] = string.IsNullOrEmpty(value) ? "" : value; + } + + dataRows.Add(row); + rowNumber++; + + // Log delle prime 3 righe per debug + if (rowNumber <= 5) + { + Logger.LogInformation("CSV row {RowNumber}: {Values}", rowNumber - 1, string.Join(" | ", values)); + } + } + fileData[sheetName] = dataRows; + + // Auto-seleziona il foglio per i CSV dato che ce n'è solo uno + selectedSheet = sheetName; + + Logger.LogInformation("CSV file processed: {FileName}, Headers: {HeaderCount} ({Headers}), Rows: {RowCount}, Auto-selected sheet: {SheetName}", + file.Name, headers.Count, string.Join(", ", headers), dataRows.Count, selectedSheet); + } private List ParseCsvLine(string line, char separator = ',') + { + var result = new List(); + var current = new StringBuilder(); + bool inQuotes = false; + + for (int i = 0; i < line.Length; i++) + { + char c = line[i]; + + if (c == '"') + { + if (inQuotes && i + 1 < line.Length && line[i + 1] == '"') + { + // Double quote - escaped quote + current.Append('"'); + i++; // Skip next quote + } + else + { + // Toggle quote mode + inQuotes = !inQuotes; + } + } + else if (c == separator && !inQuotes) + { + // End of field + result.Add(current.ToString().Trim()); + current.Clear(); + } + else + { + current.Append(c); + } + } + + // Add the last field + result.Add(current.ToString().Trim()); + + return result; + }private async Task ProcessExcelFile(IBrowserFile file) + { + try + { + using var stream = file.OpenReadStream(maxAllowedSize: 50 * 1024 * 1024); // 50MB max + + // Crea il reader Excel basato sull'estensione + IExcelDataReader reader; + var extension = Path.GetExtension(file.Name).ToLowerInvariant(); + + if (extension == ".xlsx") + { + reader = ExcelReaderFactory.CreateOpenXmlReader(stream); + } + else if (extension == ".xls") + { + reader = ExcelReaderFactory.CreateBinaryReader(stream); + } + else + { + fileErrorMessage = "Formato Excel non supportato. Utilizzare .xlsx o .xls"; + return; + } + + using (reader) + { + // Configura per utilizzare la prima riga come header + var configuration = new ExcelDataSetConfiguration() + { + ConfigureDataTable = (_) => new ExcelDataTableConfiguration() + { + UseHeaderRow = true // Prima riga come header + } + }; + + // Converti in DataSet + var dataSet = reader.AsDataSet(configuration); + + Logger.LogInformation("Excel file processed: {FileName}, Sheets: {SheetCount}", + file.Name, dataSet.Tables.Count); + + // Processa ogni foglio + foreach (DataTable table in dataSet.Tables) + { + var sheetName = table.TableName; + var headers = new List(); + var dataRows = new List>(); + + // Estrai i nomi delle colonne (headers) + foreach (DataColumn column in table.Columns) + { + headers.Add(column.ColumnName); + } + + Logger.LogInformation("Processing Excel sheet: {SheetName}, Columns: {ColumnCount}, Rows: {RowCount}", + sheetName, headers.Count, table.Rows.Count); + + // Processa le righe di dati + for (int i = 0; i < table.Rows.Count; i++) + { + var row = table.Rows[i]; + var rowData = new Dictionary(); + + for (int j = 0; j < headers.Count; j++) + { + var cellValue = row[j]?.ToString() ?? ""; + rowData[headers[j]] = string.IsNullOrEmpty(cellValue) ? "" : cellValue; + } + + dataRows.Add(rowData); + + // Log delle prime 3 righe per debug + if (i < 3) + { + Logger.LogInformation("Excel row {RowNumber} in {Sheet}: {Values}", + i + 1, sheetName, string.Join(" | ", rowData.Values)); + } + } + + // Salva i dati del foglio + fileSheets[sheetName] = headers; + fileData[sheetName] = dataRows; + + Logger.LogInformation("Excel sheet completed: {SheetName}, Headers: {Headers}, Rows: {RowCount}", + sheetName, string.Join(", ", headers), dataRows.Count); + } + + // Auto-seleziona il primo foglio se non c'è una selezione + if (fileSheets.Any() && string.IsNullOrEmpty(selectedSheet)) + { + selectedSheet = fileSheets.First().Key; + Logger.LogInformation("Auto-selected first sheet: {SheetName}", selectedSheet); + } Logger.LogInformation("Excel file processing completed: {FileName}, Total sheets: {SheetCount}, Selected: {SelectedSheet}", + file.Name, fileSheets.Count, selectedSheet); + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nell'elaborazione del file Excel: {FileName}", file.Name); + fileErrorMessage = $"Errore nell'elaborazione del file Excel: {ex.Message}"; + } + + await Task.CompletedTask; + } private void SelectSheet(string sheetName) + { + selectedSheet = sheetName; + + // Reset pagination when changing sheet + currentPage = 1; + + // Clear mappings when changing sheet + ClearAllMappings(); + + // For file sources, always require manual key selection + sourceKeyField = ""; + suggestedPrimaryKey = ""; + requiresManualKeySelection = true; + + StateHasChanged(); + } + + // File preview pagination methods + private void GoToPage(int page) + { + if (string.IsNullOrEmpty(selectedSheet) || !fileData.ContainsKey(selectedSheet)) + return; + + var totalPages = GetTotalPages(selectedSheet); + if (page >= 1 && page <= totalPages) + { + currentPage = page; + StateHasChanged(); + } + } + + private void FirstPage() => GoToPage(1); + private void PreviousPage() => GoToPage(currentPage - 1); + private void NextPage() => GoToPage(currentPage + 1); + private void LastPage() => GoToPage(GetTotalPages(selectedSheet)); + + private List> GetCurrentPageData() + { + if (string.IsNullOrEmpty(selectedSheet) || !fileData.ContainsKey(selectedSheet)) + return new List>(); + + var allData = fileData[selectedSheet]; + var skip = (currentPage - 1) * pageSize; + return allData.Skip(skip).Take(pageSize).ToList(); + } + + private int GetStartRecord() + { + if (string.IsNullOrEmpty(selectedSheet) || !fileData.ContainsKey(selectedSheet)) + return 0; + return (currentPage - 1) * pageSize + 1; + } private int GetEndRecord() + { + if (string.IsNullOrEmpty(selectedSheet) || !fileData.ContainsKey(selectedSheet)) + return 0; + var totalRecords = fileData[selectedSheet].Count; + var endRecord = currentPage * pageSize; + return Math.Min(endRecord, totalRecords); + } + + private void OnPageSizeChanged(ChangeEventArgs e) + { + if (int.TryParse(e.Value?.ToString(), out int newPageSize)) + { + pageSize = newPageSize; + currentPage = 1; // Reset to first page when changing page size + StateHasChanged(); + } + }private void OnDatabaseCredentialChanged(ChangeEventArgs e) + { + selectedDatabaseCredential = e.Value?.ToString() ?? ""; + ResetDatabaseState(); + } private void OnRestCredentialChanged(ChangeEventArgs e) + { + var newCredential = e.Value?.ToString() ?? ""; + + // Clear the cache if we're switching to a different credential + if (!string.IsNullOrEmpty(selectedRestCredential) && selectedRestCredential != newCredential) + { + ConnectionFactory.ClearRestClientCache(selectedRestCredential); + Logger.LogInformation("Cleared REST client cache for credential: {CredentialName}", selectedRestCredential); + } + + selectedRestCredential = newCredential; + ResetRestState(); + } private void ResetDatabaseState() + { + isDatabaseConnected = false; + databaseTables.Clear(); + selectedTable = ""; + databaseSearchTerm = ""; + databaseErrorMessage = ""; + + // Reset custom query state + useCustomQuery = false; + customQuery = ""; + isValidatingQuery = false; + isQueryValid = false; + queryValidationMessage = ""; + queryPreviewData.Clear(); + queryColumns.Clear(); + showQueryPreview = false; + isLoadingPreview = false; + + currentDatabaseManager?.Dispose(); + currentDatabaseManager = null; + + // Clear mappings when resetting database state + ClearAllMappings(); + } private void ResetRestState() + { + isRestConnected = false; + restEntities.Clear(); + selectedRestEntity = null; + restEntityDetails = null; + restSearchTerm = ""; + restErrorMessage = ""; + currentRestDiscovery = null; + currentRestClient = null; + + // Clear mappings when resetting REST state + ClearAllMappings(); + }private async Task ConnectToDatabase() + { + if (string.IsNullOrEmpty(selectedDatabaseCredential)) + return; + + isConnectingDatabase = true; + databaseErrorMessage = ""; + + try + { // Trova la credenziale + var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential); + if (credential == null) + { + databaseErrorMessage = "Credenziale database non trovata"; + return; + } + + // Test della connessione + var (success, message) = await CredentialService.TestDatabaseConnectionAsync(credential.Name); + if (!success) + { + 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 con try-catch specifico + try + { + 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> ?? + (schema != null ? new Dictionary>(schema) : new Dictionary>()); + + Logger.LogInformation("Database tables dopo conversione: {Count} tabelle", databaseTables.Count); + + if (databaseTables.Count == 0) + { + // Se non ci sono tabelle, potrebbe essere perché non è stato selezionato un database specifico + await HandleDatabaseSelectionRequired(); + return; + } + } + catch (Exception schemaEx) + { + Logger.LogError(schemaEx, "Errore specifico durante lo schema discovery"); + databaseErrorMessage = $"Errore nello schema discovery: {schemaEx.Message}"; + throw; + } + + isDatabaseConnected = true; + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nella connessione al database"); + databaseErrorMessage = $"Errore: {ex.Message}"; + } + finally + { + isConnectingDatabase = false; + } + } private async Task ConnectToRestApi() + { + if (string.IsNullOrEmpty(selectedRestCredential)) + return; + + isConnectingRest = true; + restErrorMessage = ""; + + try + { + // Trova la credenziale + var credential = restApiCredentials.FirstOrDefault(c => c.Name == selectedRestCredential); + if (credential == null) + { + restErrorMessage = "Credenziale REST API non trovata"; + return; + } + + // Test della connessione + var (success, message) = await CredentialService.TestRestApiConnectionAsync(credential.Name); + if (!success) + { + restErrorMessage = $"Connessione fallita: {message}"; + return; + } // Crea i client REST usando il factory con le credenziali complete + currentRestClient = await ConnectionFactory.CreateRestServiceClientAsync(selectedRestCredential); + currentRestDiscovery = await ConnectionFactory.CreateRestMetadataDiscoveryAsync(selectedRestCredential); Logger.LogInformation("Iniziando autenticazione per il servizio REST {ServiceType} con credenziale: {CredentialName}", credential.ServiceType, selectedRestCredential); + + // Autenticazione prima del discovery + var authResult = await currentRestClient.AuthenticateAsync(); + if (!authResult) + { + Logger.LogWarning("Autenticazione fallita per il servizio REST {ServiceType}", credential.ServiceType); + restErrorMessage = "Autenticazione fallita per il servizio REST"; + return; + } + + Logger.LogInformation("Autenticazione completata. Iniziando discovery delle entità REST per {ServiceType}", credential.ServiceType); + + // Discovery delle entità disponibili + restEntities = await currentRestDiscovery.DiscoverEntitySummariesAsync(); + + Logger.LogInformation("Discovery completato. Trovate {Count} entità", restEntities?.Count ?? 0); + + if (restEntities == null || !restEntities.Any()) + { + Logger.LogWarning("Nessuna entità trovata dal servizio REST"); + restErrorMessage = "Nessuna entità disponibile dal servizio REST"; + return; + } + + isRestConnected = true; + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nella connessione al servizio REST"); + restErrorMessage = $"Errore: {ex.Message}"; + } + finally + { + isConnectingRest = false; + } + } private async void SelectTable(string tableName) + { + selectedTable = tableName; + + // Clear custom query state when selecting a table + useCustomQuery = false; + customQuery = ""; + isQueryValid = false; + queryValidationMessage = ""; + queryPreviewData.Clear(); + queryColumns.Clear(); + showQueryPreview = false; + + // 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; + + // Clear mappings when changing entity + ClearAllMappings(); + + try + { + if (currentRestDiscovery != null) + { + // Discovery dei dettagli dell'entità + restEntityDetails = await currentRestDiscovery.DiscoverEntityDetailsAsync(entity.Name); } + else + { + restErrorMessage = "Servizio di discovery REST non disponibile"; + return; + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nel caricamento dettagli entità {EntityName}", entity.Name); + restErrorMessage = $"Errore nel caricamento dettagli entità: {ex.Message}"; + } } + + // Metodi per la ricerca e il filtraggio + private IEnumerable GetFilteredDatabaseTables() + { + if (string.IsNullOrEmpty(databaseSearchTerm)) + return databaseTables.Keys; + + return databaseTables.Keys.Where(table => + table.Contains(databaseSearchTerm, StringComparison.OrdinalIgnoreCase)); + } + + private IEnumerable GetFilteredRestEntities() + { + if (string.IsNullOrEmpty(restSearchTerm)) + return restEntities; + + return restEntities.Where(entity => + entity.Name.Contains(restSearchTerm, StringComparison.OrdinalIgnoreCase) || + (!string.IsNullOrEmpty(entity.Label) && entity.Label.Contains(restSearchTerm, StringComparison.OrdinalIgnoreCase))); + } + + private async Task FilterDatabaseTables(ChangeEventArgs e) + { + databaseSearchTerm = e.Value?.ToString() ?? ""; + await InvokeAsync(StateHasChanged); + } + + private async Task FilterRestEntities(ChangeEventArgs e) + { + restSearchTerm = e.Value?.ToString() ?? ""; + await InvokeAsync(StateHasChanged); + } + + private async Task ClearDatabaseSearch() + { + databaseSearchTerm = ""; + await InvokeAsync(StateHasChanged); + } + + private async Task ClearRestSearch() + { + restSearchTerm = ""; + await InvokeAsync(StateHasChanged); + } + + // Metodi per il mapping dei campi + private void SelectDbColumn(string columnName) + { + selectedDbColumn = columnName; + } + + private void SelectRestProperty(string propertyName) + { + selectedRestProperty = propertyName; + } + + private void CreateMapping() + { + if (string.IsNullOrEmpty(selectedDbColumn) || string.IsNullOrEmpty(selectedRestProperty)) + return; + + // Rimuovi eventuali mapping esistenti per questo campo database + if (fieldMappings.ContainsKey(selectedDbColumn)) + { + fieldMappings.Remove(selectedDbColumn); + } + + // Crea il nuovo mapping + fieldMappings[selectedDbColumn] = selectedRestProperty; + + Logger.LogInformation("Creato mapping: {DbColumn} -> {RestProperty}", selectedDbColumn, selectedRestProperty); + + // Deseleziona i campi + selectedDbColumn = ""; + selectedRestProperty = ""; + } + + private void RemoveMapping() + { + if (string.IsNullOrEmpty(selectedDbColumn) || !fieldMappings.ContainsKey(selectedDbColumn)) + return; + + fieldMappings.Remove(selectedDbColumn); + Logger.LogInformation("Rimosso mapping per campo: {DbColumn}", selectedDbColumn); + } private void RemoveSpecificMapping(string dbColumn) + { + if (fieldMappings.ContainsKey(dbColumn)) + { + fieldMappings.Remove(dbColumn); + Logger.LogInformation("Rimosso mapping specifico per campo: {DbColumn}", dbColumn); + } + } + + private void ClearAllMappings() + { + fieldMappings.Clear(); + selectedDbColumn = ""; + selectedRestProperty = ""; + sourceKeyField = ""; + transferMessage = ""; + transferMessageType = ""; + Logger.LogInformation("Tutti i mapping e le configurazioni sono stati cancellati"); + } + + private void AutoMapFields() + { + if (restEntityDetails == null) + return; + + IEnumerable sourceColumns = new List(); + + // Ottiene le colonne in base al tipo di sorgente + if (selectedSourceType == "database") + { + if (useCustomQuery && queryColumns.Any()) + { + sourceColumns = queryColumns; + } + else if (!useCustomQuery && databaseTables.ContainsKey(selectedTable)) + { + sourceColumns = databaseTables[selectedTable].Select(c => c.Name); + } + } + else if (selectedSourceType == "file" && fileSheets.ContainsKey(selectedSheet)) + { + sourceColumns = fileSheets[selectedSheet]; + } + + if (!sourceColumns.Any()) + return; + + var restProperties = restEntityDetails.Properties; + int mappingsCreated = 0; + + foreach (var sourceColumn in sourceColumns) + { + // Trova una proprietà REST con nome simile + var matchingProperty = restProperties.FirstOrDefault(p => + string.Equals(p.Name, sourceColumn, StringComparison.OrdinalIgnoreCase) || + string.Equals(p.Name.Replace("_", ""), sourceColumn.Replace("_", ""), StringComparison.OrdinalIgnoreCase) || + string.Equals(p.Name.Replace("Id", ""), sourceColumn.Replace("Id", ""), StringComparison.OrdinalIgnoreCase) + ); + + if (matchingProperty != null && !fieldMappings.ContainsKey(sourceColumn)) + { + fieldMappings[sourceColumn] = matchingProperty.Name; + mappingsCreated++; + } + } + + Logger.LogInformation("Auto-mapping completato. Creati {Count} mapping automatici da {SourceType}", + mappingsCreated, useCustomQuery ? "query custom" : selectedSourceType); + } private async Task ShowMappingSummary() + { + 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() + { + if (!fieldMappings.Any() || currentRestClient == null || selectedRestEntity == null) + { + transferMessage = "Configurazione incompleta. Assicurati di aver selezionato la fonte dati, entità e configurato almeno una mappatura."; + transferMessageType = "error"; + return; + } + + // Check source-specific requirements + if (selectedSourceType == "database") + { + if (currentDatabaseManager == null) + { + transferMessage = "Database non connesso."; + transferMessageType = "error"; + return; + } + + if (useCustomQuery) + { + if (!isQueryValid || string.IsNullOrWhiteSpace(customQuery)) + { + transferMessage = "Query custom non valida. Validare la query prima di procedere."; + transferMessageType = "error"; + return; + } + } + else if (string.IsNullOrEmpty(selectedTable)) + { + transferMessage = "Tabella non selezionata."; + transferMessageType = "error"; + return; + } + } + + if (selectedSourceType == "file" && string.IsNullOrEmpty(selectedSheet)) + { + transferMessage = "File non caricato o foglio non selezionato."; + 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 + { + var sourceName = selectedSourceType == "database" + ? (useCustomQuery ? "custom_query" : selectedTable) + : selectedSheet; + Logger.LogInformation("Iniziando trasferimento dati da {SourceType} {Source} a {Entity} con {MappingCount} mappature", + selectedSourceType, sourceName, selectedRestEntity.Name, fieldMappings.Count); + + // 1. Ottieni tutti i record dalla fonte dati + var records = await GetAllRecordsFromSource(); + Logger.LogInformation("Ottenuti {RecordCount} record da {SourceType} {Source}", records.Count(), selectedSourceType, sourceName); + + if (!records.Any()) + { + transferMessage = "Nessun record trovato nella fonte dati selezionata."; + transferMessageType = "error"; + return; + } + + // 2. Ottieni i campi obbligatori dell'entità REST (se non ci sono campi chiave) + var requiredFields = new HashSet(); + 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(); + int recordNumber = 1; + + foreach (var record in records) + { + var transferResult = new TransferResult + { + RecordNumber = recordNumber, + RecordData = new Dictionary(record) + }; + + try + { + // Trasforma il record in base ai mapping + var restData = TransformRecordToRestEntity(record); + + // Genera la chiave sorgente per questo record + var sourceKey = GenerateSourceKey(record); + + // NUOVO SISTEMA: Cerca associazione esistente basata sul valore della chiave + if (useRecordAssociations && !string.IsNullOrEmpty(sourceKey)) + { + 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) + { + // Prova direttamente l'aggiornamento - più efficiente che verificare prima l'esistenza + Logger.LogInformation("ASSOCIATION DEBUG: Tentativo aggiornamento record esistente - DestinationId: '{DestinationId}'", existingAssociation.DestinationId); + + try + { + 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 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; + } + + HandleInvalidAssociation: + // L'ID di destinazione non esiste più o l'update è fallito - elimina l'associazione non valida + try + { + await CredentialService.DeleteKeyAssociationAsync(existingAssociation.Id); + Logger.LogInformation("ASSOCIATION DEBUG: Associazione non valida eliminata: {AssociationId}", existingAssociation.Id); + } + catch (Exception delEx) + { + Logger.LogWarning(delEx, "Errore nell'eliminazione dell'associazione non valida {AssociationId}", existingAssociation.Id); + } + + 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) + } + } + + 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() : + 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 + { + // Determina i campi chiave automaticamente + var destinationKeyField = GetEntityIdField(); // Campo chiave nella destinazione + + var association = new KeyAssociation + { + 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, + SourceType = selectedSourceType + }) + }; + + 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) + { + 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++; + transferResult.Status = "error"; + transferResult.Message = "Errore nel trasferimento del record (result null)"; + errors.Add($"Errore nel trasferimento del record {recordNumber}"); + } + } + catch (Exception ex) + { + errorCount++; + 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++; + } + + // 4. Mostra risultati + if (errorCount == 0) + { + var message = $"Trasferimento completato con successo! "; + var messageParts = new List(); + + 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 + { + var message = $"Trasferimento completato con {(duplicateCount > 0 ? "warning e " : "")}errori. "; + var messageParts = new List(); + + 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. Inserimenti: {SuccessCount}, Aggiornamenti: {UpdatedCount}, Duplicati: {DuplicateCount}, Errori: {ErrorCount}", + successCount, updatedCount, duplicateCount, errorCount); + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore generale nel trasferimento dati"); + transferMessage = $"Errore nel trasferimento dati: {ex.Message}"; + transferMessageType = "error"; + } + finally + { + isTransferringData = false; + } + } private async Task>> GetAllRecordsFromSource() + { + if (selectedSourceType == "database") + { + return await GetAllRecordsFromDatabase(); + } + else if (selectedSourceType == "file") + { + return await GetAllRecordsFromFile(); + } + + return new List>(); + } + + private async Task>> GetAllRecordsFromDatabase() + { + if (currentDatabaseManager == null) + return new List>(); + + try + { + if (useCustomQuery) + { + // Usa la query custom per ottenere tutti i record + if (!isQueryValid || string.IsNullOrWhiteSpace(customQuery)) + { + throw new InvalidOperationException("Query custom non valida. Validare la query prima di procedere."); + } + + // CONTROLLO DI SICUREZZA AGGIUNTIVO: Verifica che sia ancora una SELECT + if (!IsSelectQuery(customQuery)) + { + throw new InvalidOperationException("ERRORE DI SICUREZZA: Tentativo di eseguire una query non SELECT. Operazione bloccata per sicurezza."); + } + + var cleanQuery = CleanQuery(customQuery); + Logger.LogInformation("Esecuzione query custom per trasferimento dati: {Query}", cleanQuery); + + return await currentDatabaseManager.ExecuteRawQueryAsync(cleanQuery); + } + else + { + // Usa il metodo standard per tabelle + if (string.IsNullOrEmpty(selectedTable)) + { + throw new InvalidOperationException("Nessuna tabella selezionata."); + } + + return await currentDatabaseManager.GetAllRecordsAsync(selectedTable); + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nell'ottenere i record dal database. UseCustomQuery: {UseCustomQuery}, Table: {Table}, Query: {Query}", + useCustomQuery, selectedTable, useCustomQuery ? customQuery : "N/A"); + throw; + } + } private async Task>> GetAllRecordsFromFile() + { + if (string.IsNullOrEmpty(selectedSheet) || !fileData.ContainsKey(selectedSheet)) + { + return new List>(); + } + + await Task.CompletedTask; + return fileData[selectedSheet]; + } + + private Dictionary TransformRecordToRestEntity(Dictionary dbRecord) + { + var restData = new Dictionary(); + + foreach (var mapping in fieldMappings) + { + string dbColumn = mapping.Key; + string restProperty = mapping.Value; + + if (dbRecord.ContainsKey(dbColumn)) + { + var value = dbRecord[dbColumn]; + + // Trasforma il valore se necessario (es. date format, null handling, etc.) + var transformedValue = TransformValue(value, dbColumn, restProperty); + + if (transformedValue != null) + { + restData[restProperty] = transformedValue; + } + } + } + + Logger.LogDebug("Record trasformato: {DbColumns} → {RestProperties}", + string.Join(", ", dbRecord.Keys), + string.Join(", ", restData.Keys)); + + return restData; + } + + private object? TransformValue(object? value, string dbColumn, string restProperty) + { + if (value == null || value == DBNull.Value) + return null; + + // Ottieni informazioni sui tipi per fare trasformazioni intelligenti + var dbColumnInfo = databaseTables.ContainsKey(selectedTable) + ? databaseTables[selectedTable].FirstOrDefault(c => c.Name == dbColumn) + : null; + + var restPropertyInfo = restEntityDetails?.Properties.FirstOrDefault(p => p.Name == restProperty); + + // Trasformazioni specifiche per tipo + if (restPropertyInfo != null) + { + switch (restPropertyInfo.Type.ToLower()) + { + case "edm.string": + return value.ToString(); + + case "edm.int32": + case "edm.int64": + if (int.TryParse(value.ToString(), out int intVal)) + return intVal; + break; + + case "edm.decimal": + case "edm.double": + if (decimal.TryParse(value.ToString(), out decimal decVal)) + return decVal; + break; + + case "edm.boolean": + if (bool.TryParse(value.ToString(), out bool boolVal)) + return boolVal; + // Gestisci anche valori numerici (0/1) come boolean + if (value.ToString() == "1") return true; + if (value.ToString() == "0") return false; + break; + + case "edm.datetime": + case "edm.datetimeoffset": + if (DateTime.TryParse(value.ToString(), out DateTime dateVal)) + return dateVal.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); + break; + } + } + + // Fallback: restituisci il valore convertito a stringa + return value.ToString(); + } + + private string GetPropertyPlaceholder(RestPropertyInfo property) + { + return property.Type switch + { + "Edm.String" => $"Inserisci {property.Name}" + (property.MaxLength.HasValue ? $" (max {property.MaxLength})" : ""), + "Edm.Int32" => "Numero intero", + "Edm.Decimal" => "Numero decimale", + "Edm.DateTime" => "Data/Ora (YYYY-MM-DD)", + "Edm.Boolean" => "true/false", + _ => $"Valore per {property.Name}" + }; + } + + public void Dispose() + { + currentDatabaseManager?.Dispose(); + } + + private char DetectCsvSeparator(string line) + { + // Common separators to check + var separators = new[] { ',', ';', '\t', '|' }; + var counts = new Dictionary(); + + bool inQuotes = false; + + // Count separators outside of quotes + foreach (char c in line) + { + if (c == '"') + { + inQuotes = !inQuotes; + } + else if (!inQuotes && separators.Contains(c)) + { + counts[c] = counts.GetValueOrDefault(c, 0) + 1; + } + } + + // Return the separator with the highest count, default to comma + if (counts.Any()) + { + var mostCommon = counts.OrderByDescending(x => x.Value).First(); + // Make sure we have at least one occurrence to avoid single-column files + if (mostCommon.Value > 0) + { + return mostCommon.Key; + } + } + return ','; // Default fallback + } + + /// + /// Verifica se il pulsante di trasferimento può essere abilitato + /// + 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" + }; + } + + /// + /// Genera una chiave univoca per il record sorgente + /// + private string GenerateSourceKey(Dictionary 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."); + } + + // Normalizza il valore della chiave (trim e gestione case-sensitive) + return keyValue.Trim(); + } + 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> ?? + (schema != null ? new Dictionary>(schema) : new Dictionary>()); + + 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(); + } + + /// + /// Ottiene il nome del campo ID per l'entità corrente + /// + private string GetEntityIdField() + { + // Fallback predefiniti in base al tipo di servizio/entità + if (selectedRestEntity?.Name != null) + { + // Per SAP B1, la maggior parte delle entità usa DocEntry + if (selectedRestEntity.Name.Contains("BusinessPartner") || + selectedRestEntity.Name.Contains("Customer") || + selectedRestEntity.Name.Contains("Vendor")) + { + return "CardCode"; + } + + if (selectedRestEntity.Name.Contains("Item") || + selectedRestEntity.Name.Contains("Product")) + { + return "ItemCode"; + } + } + + // Usa campi ID comuni come fallback + var commonIdFields = new[] { "DocEntry", "Id", "ID", "id", "Key", "key", "Code", "code" }; + + // Per ora usa DocEntry come default per SAP B1 + return "DocEntry"; + } + + // Custom Query Methods + private void OnQueryModeChanged(ChangeEventArgs e) + { + useCustomQuery = bool.Parse(e.Value?.ToString() ?? "false"); + + if (useCustomQuery) + { + // Reset table selection when switching to custom query + selectedTable = ""; + ClearAllMappings(); + + // Reset query-specific state + customQuery = ""; + isQueryValid = false; + queryValidationMessage = ""; + queryPreviewData.Clear(); + queryColumns.Clear(); + showQueryPreview = false; + + // For custom queries, require manual key selection + sourceKeyField = ""; + suggestedPrimaryKey = ""; + requiresManualKeySelection = true; + } + else + { + // Reset custom query when switching to table mode + customQuery = ""; + isQueryValid = false; + queryValidationMessage = ""; + queryPreviewData.Clear(); + queryColumns.Clear(); + showQueryPreview = false; + ClearAllMappings(); + + // Reset key field selection + sourceKeyField = ""; + suggestedPrimaryKey = ""; + requiresManualKeySelection = false; + } + + StateHasChanged(); + } + + private async Task ValidateCustomQuery() + { + if (string.IsNullOrWhiteSpace(customQuery) || currentDatabaseManager == null) + { + isQueryValid = false; + queryValidationMessage = "Query vuota o database non connesso"; + return; + } + + // CONTROLLO DI SICUREZZA: Verifica che sia una SELECT + if (!IsSelectQuery(customQuery)) + { + isQueryValid = false; + queryValidationMessage = "ERRORE DI SICUREZZA: Sono permesse solo query SELECT. Operazioni come INSERT, UPDATE, DELETE, DROP, CREATE, ALTER, TRUNCATE non sono consentite."; + Logger.LogWarning("Tentativo di eseguire query non SELECT bloccato: {Query}", customQuery.Length > 100 ? customQuery.Substring(0, 100) + "..." : customQuery); + return; + } + + isValidatingQuery = true; + queryValidationMessage = ""; + queryColumns.Clear(); + + try + { + // Converte la query per testare solo 1 riga + var testQuery = ConvertQueryForValidation(customQuery); + + Logger.LogInformation("Validazione query: {TestQuery}", testQuery); + + // Esegue la query di test + var testResults = await currentDatabaseManager.ExecuteRawQueryAsync(testQuery); + + if (testResults != null && testResults.Any()) + { + isQueryValid = true; + + // Estrae i nomi delle colonne dal primo record + var firstRecord = testResults.First(); + queryColumns = firstRecord.Keys.ToList(); + + // Non mostra più messaggi di successo per ridurre l'ingombro visivo + queryValidationMessage = ""; + + Logger.LogInformation("Query validata con successo. Colonne: {Columns}", string.Join(", ", queryColumns)); + + // Clear existing mappings since we have new columns + fieldMappings.Clear(); + selectedDbColumn = ""; + selectedRestProperty = ""; + + // For custom queries, always require manual key selection + sourceKeyField = ""; + suggestedPrimaryKey = ""; + requiresManualKeySelection = true; + + StateHasChanged(); + } + else + { + isQueryValid = false; + queryValidationMessage = "Query valida ma non restituisce risultati"; + queryColumns.Clear(); + } + } + catch (Exception ex) + { + isQueryValid = false; + queryValidationMessage = $"Errore nella validazione: {ex.Message}"; + queryColumns.Clear(); + Logger.LogError(ex, "Errore nella validazione della query custom"); + } + finally + { + isValidatingQuery = false; + StateHasChanged(); + } + } + + private async Task LoadQueryPreview() + { + if (!isQueryValid || string.IsNullOrWhiteSpace(customQuery) || currentDatabaseManager == null) + { + return; + } + + isLoadingPreview = true; + + try + { + // Usa la query limitata per il preview (max 50 righe per performance) + var previewQuery = ConvertQueryForPreview(customQuery, 50); + + Logger.LogInformation("Caricamento preview query: {PreviewQuery}", previewQuery); + + queryPreviewData = await currentDatabaseManager.ExecuteRawQueryAsync(previewQuery); + showQueryPreview = true; + + Logger.LogInformation("Preview caricato: {RowCount} righe", queryPreviewData.Count); + } + catch (Exception ex) + { + queryValidationMessage = $"Errore nel caricamento preview: {ex.Message}"; + Logger.LogError(ex, "Errore nel caricamento del preview della query"); + } + finally + { + isLoadingPreview = false; + StateHasChanged(); + } + } + + private void HideQueryPreview() + { + showQueryPreview = false; + queryPreviewData.Clear(); + StateHasChanged(); + } + + private string ConvertQueryForValidation(string originalQuery) + { + // Rimuove commenti e spazi extra + var cleanQuery = CleanQuery(originalQuery); + + // Se la query ha già un LIMIT/TOP, la usa così com'è per il test + if (HasLimitClause(cleanQuery)) + { + return cleanQuery; + } + + // Aggiunge LIMIT/TOP in base al tipo di database + return AddLimitClause(cleanQuery, 1); + } + + private string ConvertQueryForPreview(string originalQuery, int maxRows = 50) + { + var cleanQuery = CleanQuery(originalQuery); + + // Se la query ha già un LIMIT/TOP con un valore minore, la mantiene + if (HasLimitClause(cleanQuery)) + { + return cleanQuery; + } + + return AddLimitClause(cleanQuery, maxRows); + } + + private string CleanQuery(string query) + { + if (string.IsNullOrWhiteSpace(query)) + return ""; + + // Rimuove commenti SQL + var lines = query.Split('\n') + .Select(line => line.Contains("--") ? line.Substring(0, line.IndexOf("--")) : line) + .Where(line => !string.IsNullOrWhiteSpace(line)); + + var cleanQuery = string.Join(" ", lines).Trim(); + + // Rimuove il punto e virgola finale se presente + if (cleanQuery.EndsWith(";")) + { + cleanQuery = cleanQuery.Substring(0, cleanQuery.Length - 1); + } + + return cleanQuery; + } + + private bool HasLimitClause(string query) + { + var upperQuery = query.ToUpperInvariant(); + return upperQuery.Contains(" LIMIT ") || + upperQuery.Contains(" TOP ") || + upperQuery.Contains("ROWNUM") || + upperQuery.Contains("FETCH FIRST"); + } + + private string AddLimitClause(string query, int limit) + { + var upperQuery = query.ToUpperInvariant(); + + // Per SQL Server, Oracle, e altri che supportano TOP + if (upperQuery.Contains("SELECT ")) + { + var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential); + if (credential != null) + { + var dbType = credential.DatabaseType.ToString().ToLowerInvariant(); + switch (dbType) + { + case "sqlserver": + case "oracle": + // Aggiunge TOP dopo SELECT + return query.Replace("SELECT ", $"SELECT TOP {limit} ", StringComparison.OrdinalIgnoreCase); + + case "mysql": + case "postgresql": + case "sqlite": + default: + // Aggiunge LIMIT alla fine + return $"{query} LIMIT {limit}"; + } + } + } + + // Fallback: aggiunge LIMIT + return $"{query} LIMIT {limit}"; + } + + private void OnCustomQueryChanged(ChangeEventArgs e) + { + customQuery = e.Value?.ToString() ?? ""; + + // Reset validation quando la query cambia + isQueryValid = false; + queryValidationMessage = ""; + queryPreviewData.Clear(); + queryColumns.Clear(); + showQueryPreview = false; + + // Clear mappings quando la query cambia + ClearAllMappings(); + + // Reset key field selection + sourceKeyField = ""; + suggestedPrimaryKey = ""; + requiresManualKeySelection = true; + + StateHasChanged(); + } + + /// + /// Verifica che la query sia una SELECT e non contenga operazioni pericolose + /// + private bool IsSelectQuery(string query) + { + if (string.IsNullOrWhiteSpace(query)) + return false; + + // Rimuovi commenti e normalizza la query + var cleanQuery = CleanQueryForSecurityCheck(query); + + // Lista delle operazioni pericolose che non sono permesse + var dangerousKeywords = new[] + { + "INSERT", "UPDATE", "DELETE", "DROP", "CREATE", "ALTER", + "TRUNCATE", "REPLACE", "MERGE", "EXEC", "EXECUTE", + "DECLARE", "SET", "GRANT", "REVOKE", "BACKUP", "RESTORE", + "SHUTDOWN", "KILL", "LOAD", "BULK", "OPENROWSET", "OPENDATASOURCE" + }; + + // Verifica che non contenga operazioni pericolose + foreach (var keyword in dangerousKeywords) + { + if (cleanQuery.Contains($" {keyword} ", StringComparison.OrdinalIgnoreCase) || + cleanQuery.StartsWith($"{keyword} ", StringComparison.OrdinalIgnoreCase) || + cleanQuery.Contains($";{keyword} ", StringComparison.OrdinalIgnoreCase) || + cleanQuery.Contains($"\n{keyword} ", StringComparison.OrdinalIgnoreCase) || + cleanQuery.Contains($"\r{keyword} ", StringComparison.OrdinalIgnoreCase)) + { + Logger.LogWarning("Query bloccata: contiene keyword pericolosa '{Keyword}' in query: {QueryStart}", + keyword, query.Length > 50 ? query.Substring(0, 50) + "..." : query); + return false; + } + } + + // Verifica che inizi con SELECT (permettendo spazi e commenti iniziali) + var trimmedQuery = cleanQuery.TrimStart(); + if (!trimmedQuery.StartsWith("SELECT", StringComparison.OrdinalIgnoreCase)) + { + Logger.LogWarning("Query bloccata: non inizia con SELECT. Query: {QueryStart}", + query.Length > 50 ? query.Substring(0, 50) + "..." : query); + return false; + } + + // Verifica addizionale: non deve contenere punto e virgola seguito da altra query + var statements = cleanQuery.Split(';', StringSplitOptions.RemoveEmptyEntries); + if (statements.Length > 1) + { + // Se ci sono multiple statements, tutte devono essere SELECT o commenti vuoti + foreach (var statement in statements) + { + var trimmedStatement = statement.Trim(); + if (!string.IsNullOrEmpty(trimmedStatement) && + !trimmedStatement.StartsWith("SELECT", StringComparison.OrdinalIgnoreCase)) + { + Logger.LogWarning("Query bloccata: contiene multiple statements non SELECT. Query: {QueryStart}", + query.Length > 50 ? query.Substring(0, 50) + "..." : query); + return false; + } + } + } + + return true; + } + + /// + /// Pulisce la query per il controllo di sicurezza rimuovendo commenti + /// + private string CleanQueryForSecurityCheck(string query) + { + if (string.IsNullOrEmpty(query)) + return ""; + + var lines = query.Split('\n'); + var cleanedLines = new List(); + + foreach (var line in lines) + { + var cleanedLine = line.Trim(); + + // Rimuovi commenti SQL (-- e /* */) + var dashCommentIndex = cleanedLine.IndexOf("--"); + if (dashCommentIndex >= 0) + { + cleanedLine = cleanedLine.Substring(0, dashCommentIndex).Trim(); + } + + // Gestione commenti multiline /* */ - implementazione base + cleanedLine = System.Text.RegularExpressions.Regex.Replace(cleanedLine, @"/\*.*?\*/", " ", + System.Text.RegularExpressions.RegexOptions.IgnoreCase); + + if (!string.IsNullOrWhiteSpace(cleanedLine)) + { + cleanedLines.Add(cleanedLine); + } + } + + return string.Join(" ", cleanedLines); + } +} +