Files
Data-Coupler/Data_Coupler/Pages/DataCoupler.razor
T
Alessio Dal Santo 33bd5e2bbf feat: Implementa supporto completo per file Excel/CSV come fonte dati
- Aggiunge selezione tipo fonte dati (database o file) nella UI
- Implementa caricamento e parsing di file Excel (.xlsx, .xls) usando ExcelDataReader
- Implementa parsing CSV con rilevamento automatico separatore (,;|\t)
- Aggiunge preview paginato dei dati file con controlli navigazione
- Estende mapping campi per supportare sia database che file
- Corregge errori strutturali HTML/Razor e gestione chiavi dizionario
- Migliora logica trasferimento dati per fonti multiple
- Aggiunge supporto encoding per file Excel legacy (.xls)

Modifiche principali:
- DataCoupler.razor: UI completa per gestione file + correzioni strutturali
- Data_Coupler.csproj: Dipendenze ExcelDataReader per supporto Excel
- Program.cs: Registrazione provider encoding per compatibilità .xls

Il sistema ora supporta completamente sia database che file come fonte dati
con parsing robusto, preview interattivo e mapping flessibile.
2025-06-17 18:29:35 +02:00

1725 lines
83 KiB
Plaintext

@page "/data-coupler"
@using CredentialManager.Models
@using DataConnection.Interfaces
@using DataConnection.CredentialManagement.Interfaces
@using DataConnection.REST.Interfaces
@using DataConnection.REST.Models
@using Data_Coupler.Services
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.JSInterop
@using System.IO
@using System.Text
@using System.Data
@using ExcelDataReader
@inject IDataConnectionCredentialService CredentialService
@inject IDataConnectionFactory ConnectionFactory
@inject IJSRuntime JSRuntime
@inject ILogger<DataCoupler> Logger
<PageTitle>Data Coupler</PageTitle>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<h3><i class="fas fa-exchange-alt"></i> Data Coupler - Coupling Database e REST API</h3>
<p class="text-muted">Connetti database e servizi REST per il trasferimento dati</p>
</div>
</div> <div class="row">
<!-- Lato Sinistro - Fonte Dati -->
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-primary text-white">
<h5><i class="fas fa-database"></i> Fonte Dati</h5>
</div>
<div class="card-body">
<!-- Selezione Tipo Fonte -->
<div class="mb-3">
<label class="form-label">Tipo di Fonte:</label>
<select class="form-select" @onchange="OnSourceTypeChanged" value="@selectedSourceType">
<option value="">-- Seleziona Tipo --</option>
<option value="database">Database</option>
<option value="file">File (Excel/CSV)</option>
</select>
</div>
<!-- Sezione Database -->
@if (selectedSourceType == "database")
{
<!-- Selezione Credenziali Database -->
<div class="mb-3">
<label class="form-label">Credenziali Database:</label>
<select class="form-select" @onchange="OnDatabaseCredentialChanged" value="@selectedDatabaseCredential">
<option value="">-- Seleziona Database --</option>
@foreach (var cred in databaseCredentials)
{
<option value="@cred.Name">@cred.Name (@cred.DatabaseType - @cred.Host)</option>
}
</select>
</div>
@if (!string.IsNullOrEmpty(selectedDatabaseCredential))
{
<div class="mb-3">
<button class="btn btn-success btn-sm" @onclick="ConnectToDatabase" disabled="@isConnectingDatabase">
@if (isConnectingDatabase)
{
<span class="spinner-border spinner-border-sm me-2"></span>
}
<i class="fas fa-plug"></i> Connetti e Scopri Schema
</button>
@if (isDatabaseConnected)
{
<span class="badge bg-success ms-2">Connesso</span>
}
</div>
} @if (!string.IsNullOrEmpty(databaseErrorMessage))
{
<div class="alert alert-danger" role="alert">
@databaseErrorMessage
</div>
}
<!-- Lista Tabelle -->
@if (databaseTables.Any())
{
<div class="mb-3">
<h6>Tabelle Database (@databaseTables.Count disponibili):</h6>
<!-- Campo di ricerca -->
<div class="mb-2">
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-search"></i>
</span>
<input type="text" class="form-control" placeholder="Cerca tabelle..."
@bind="databaseSearchTerm" @oninput="FilterDatabaseTables" />
@if (!string.IsNullOrEmpty(databaseSearchTerm))
{
<button class="btn btn-outline-secondary" @onclick="ClearDatabaseSearch">
<i class="fas fa-times"></i>
</button>
}
</div>
</div>
<!-- Lista tabelle filtrate -->
<div class="list-group" style="max-height: 300px; overflow-y: auto;">
@foreach (var table in GetFilteredDatabaseTables())
{
<a class="list-group-item list-group-item-action @(selectedTable == table ? "active" : "")"
@onclick="@(() => SelectTable(table))">
<div class="d-flex justify-content-between align-items-center">
<div>
<i class="fas fa-table"></i> @table
@if (databaseTables.ContainsKey(table))
{
<small class="text-muted d-block">@databaseTables[table].Count() campi</small>
}
</div>
@if (selectedTable == table)
{
<span class="badge bg-primary">Selezionata</span>
}
</div>
</a>
}
</div>
@if (!GetFilteredDatabaseTables().Any())
{
<div class="alert alert-info mt-2">
<i class="fas fa-info-circle"></i> Nessuna tabella trovata con il termine di ricerca "@databaseSearchTerm"
</div>
}
</div>
}
}
<!-- Sezione File -->
@if (selectedSourceType == "file")
{
<div class="mb-3">
<label class="form-label">Seleziona File (Excel/CSV):</label>
<InputFile class="form-control" OnChange="OnFileSelected" accept=".xlsx,.xls,.csv" />
@if (!string.IsNullOrEmpty(selectedFileName))
{
<small class="text-muted">File selezionato: @selectedFileName</small>
}
</div>
@if (isProcessingFile)
{
<div class="mb-3">
<div class="d-flex align-items-center">
<span class="spinner-border spinner-border-sm me-2"></span>
Elaborazione file in corso...
</div>
</div>
}
@if (!string.IsNullOrEmpty(fileErrorMessage))
{
<div class="alert alert-danger" role="alert">
@fileErrorMessage
</div>
} <!-- Lista Fogli/Sheet -->
@if (fileSheets.Any())
{
<div class="mb-3">
@{
var fileExtension = Path.GetExtension(selectedFileName).ToLowerInvariant();
var isExcel = fileExtension == ".xlsx" || fileExtension == ".xls";
var fileTypeIcon = isExcel ? "fa-file-excel text-success" : "fa-file-csv text-info";
var fileTypeName = isExcel ? "Excel" : "CSV";
}
<h6>
<i class="fas @fileTypeIcon"></i>
Fogli @fileTypeName (@fileSheets.Count):
</h6>
<div class="list-group" style="max-height: 300px; overflow-y: auto;">
@foreach (var sheet in fileSheets)
{
<a class="list-group-item list-group-item-action @(selectedSheet == sheet.Key ? "active" : "")"
@onclick="@(() => SelectSheet(sheet.Key))">
<div class="d-flex justify-content-between align-items-center">
<div>
<i class="fas @fileTypeIcon"></i> @sheet.Key
<small class="text-muted d-block">@sheet.Value.Count() colonne</small>
</div>
@if (selectedSheet == sheet.Key)
{
<span class="badge bg-primary">Selezionato</span>
}
</div>
</a>
} </div>
</div>
} else if (selectedSourceType == "file" && !string.IsNullOrEmpty(selectedFileName) && !isProcessingFile && string.IsNullOrEmpty(fileErrorMessage))
{
<div class="alert alert-info" role="alert">
<i class="fas fa-info-circle"></i>
<strong>File selezionato:</strong> @selectedFileName
<br><small>Il file è stato elaborato ma non contiene dati validi o fogli leggibili.</small>
</div>
}
else if (selectedSourceType == "file" && string.IsNullOrEmpty(selectedFileName))
{
<div class="alert alert-light border" role="alert">
<div class="text-center">
<i class="fas fa-upload fa-2x text-muted mb-2"></i>
<h6>Carica un file per iniziare</h6>
<small class="text-muted">
<strong>Formati supportati:</strong><br>
• <i class="fas fa-file-excel text-success"></i> Excel (.xlsx, .xls) - Supporto completo per fogli multipli<br>
• <i class="fas fa-file-csv text-info"></i> CSV (.csv) - Prima riga come intestazioni<br><br>
Una volta caricato, potrai visualizzare immediatamente il contenuto e navigare tra tutti i record
</small>
</div>
</div>
}
<!-- Anteprima Dati File -->
@if (!string.IsNullOrEmpty(selectedSheet) && fileData.ContainsKey(selectedSheet) && fileData[selectedSheet].Any())
{
<div class="mb-3">
<div class="alert alert-success" role="alert">
<div class="d-flex justify-content-between align-items-center">
<div>
@{
var fileExtension = Path.GetExtension(selectedFileName).ToLowerInvariant();
var isExcel = fileExtension == ".xlsx" || fileExtension == ".xls";
var fileTypeIcon = isExcel ? "fa-file-excel text-white" : "fa-file-csv text-white";
var fileTypeName = isExcel ? "Excel" : "CSV";
}
<i class="fas fa-check-circle"></i> <strong>File @fileTypeName Caricato!</strong>
<div class="mt-1">
<small>
<i class="fas @fileTypeIcon"></i>
Foglio: <strong>@selectedSheet</strong> |
Record: <strong>@fileData[selectedSheet].Count</strong> |
Colonne: <strong>@fileSheets[selectedSheet].Count()</strong>
</small>
</div>
</div>
<div class="d-flex align-items-center gap-2">
<div class="d-flex align-items-center">
<small class="me-2">Righe per pagina:</small>
<select class="form-select form-select-sm" style="width: auto;"
@onchange="OnPageSizeChanged" value="@pageSize">
<option value="10">10</option>
<option value="20">20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
<button class="btn btn-outline-primary btn-sm" type="button"
data-bs-toggle="collapse" data-bs-target="#fileDataPreview"
aria-expanded="true" aria-controls="fileDataPreview">
<i class="fas fa-eye"></i> Mostra/Nascondi Preview
</button>
</div>
</div> </div>
<div class="collapse show mt-2" id="fileDataPreview">
<div class="card">
<div class="card-header bg-light">
<div class="d-flex justify-content-between align-items-center">
<h6 class="mb-0"><i class="fas fa-table"></i> Preview Dati - @selectedSheet</h6>
<div class="d-flex align-items-center gap-2">
<small class="text-muted">
Record @GetStartRecord()-@GetEndRecord() di @fileData[selectedSheet].Count
</small>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary"
@onclick="FirstPage"
disabled="@(currentPage == 1)">
<i class="fas fa-angle-double-left"></i>
</button>
<button type="button" class="btn btn-outline-secondary"
@onclick="PreviousPage"
disabled="@(currentPage == 1)">
<i class="fas fa-angle-left"></i>
</button>
<span class="btn btn-outline-secondary disabled">
@currentPage di @GetTotalPages(selectedSheet)
</span>
<button type="button" class="btn btn-outline-secondary"
@onclick="NextPage"
disabled="@(currentPage >= GetTotalPages(selectedSheet))">
<i class="fas fa-angle-right"></i>
</button>
<button type="button" class="btn btn-outline-secondary"
@onclick="LastPage"
disabled="@(currentPage >= GetTotalPages(selectedSheet))">
<i class="fas fa-angle-double-right"></i>
</button>
</div>
</div>
</div>
</div>
<div class="card-body p-0" style="max-height: 400px; overflow: auto;">
@if (fileSheets.ContainsKey(selectedSheet))
{
<div class="table-responsive">
<table class="table table-sm table-striped table-hover mb-0">
<thead class="table-dark sticky-top">
<tr>
<th style="width: 50px;">#</th>
@foreach (var column in fileSheets[selectedSheet])
{
<th>@column</th>
}
</tr>
</thead>
<tbody>
@{
var dataToShow = GetCurrentPageData();
var startRecord = GetStartRecord();
}
@for (int i = 0; i < dataToShow.Count; i++)
{
var row = dataToShow[i];
var absoluteRowIndex = startRecord + i;
<tr>
<td><small class="text-muted">@absoluteRowIndex</small></td>
@foreach (var column in fileSheets[selectedSheet])
{
var cellValue = row.ContainsKey(column) ? row[column]?.ToString() : "";
var displayValue = string.IsNullOrEmpty(cellValue) ? "-" :
(cellValue.Length > 50 ? cellValue.Substring(0, 50) + "..." : cellValue);
<td>
<span title="@cellValue">@displayValue</span>
</td>
}
</tr>
}
</tbody>
</table>
</div>
}
</div>
<!-- Footer con controlli paginazione e informazioni -->
<div class="card-footer bg-light">
<div class="row align-items-center">
<div class="col-md-6">
<small class="text-muted">
<i class="fas fa-info-circle"></i>
Visualizzazione: @pageSize record per pagina
</small>
</div>
<div class="col-md-6 text-end">
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-primary"
@onclick="FirstPage"
disabled="@(currentPage == 1)">
Prima
</button>
<button type="button" class="btn btn-outline-primary"
@onclick="PreviousPage"
disabled="@(currentPage == 1)">
Precedente
</button>
<button type="button" class="btn btn-outline-primary"
@onclick="NextPage"
disabled="@(currentPage >= GetTotalPages(selectedSheet))">
Successiva
</button>
<button type="button" class="btn btn-outline-primary"
@onclick="LastPage"
disabled="@(currentPage >= GetTotalPages(selectedSheet))">
Ultima
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div> }
}
</div>
</div>
</div>
<!-- Lato Destro - REST API -->
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-info text-white">
<h5><i class="fas fa-cloud"></i> REST API Destination</h5>
</div>
<div class="card-body">
<!-- Selezione Credenziali REST -->
<div class="mb-3">
<label class="form-label">Credenziali REST API:</label>
<select class="form-select" @onchange="OnRestCredentialChanged" value="@selectedRestCredential">
<option value="">-- Seleziona REST API --</option>
@foreach (var cred in restApiCredentials)
{
<option value="@cred.Name">@cred.Name (@cred.ServiceType - @cred.BaseUrl)</option>
}
</select>
</div>
@if (!string.IsNullOrEmpty(selectedRestCredential))
{
<div class="mb-3">
<button class="btn btn-success btn-sm" @onclick="ConnectToRestApi" disabled="@isConnectingRest">
@if (isConnectingRest)
{
<span class="spinner-border spinner-border-sm me-2"></span>
}
<i class="fas fa-plug"></i> Connetti e Scopri Entità
</button>
@if (isRestConnected)
{
<span class="badge bg-success ms-2">Connesso</span>
}
</div>
}
@if (!string.IsNullOrEmpty(restErrorMessage))
{
<div class="alert alert-danger" role="alert">
@restErrorMessage
</div>
} <!-- Lista Entità REST -->
@if (restEntities.Any())
{
<div class="mb-3">
<h6>Entità REST (@restEntities.Count disponibili):</h6>
<!-- Campo di ricerca -->
<div class="mb-2">
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-search"></i>
</span>
<input type="text" class="form-control" placeholder="Cerca entità..."
@bind="restSearchTerm" @oninput="FilterRestEntities" />
@if (!string.IsNullOrEmpty(restSearchTerm))
{
<button class="btn btn-outline-secondary" @onclick="ClearRestSearch">
<i class="fas fa-times"></i>
</button>
}
</div>
</div>
<!-- Lista entità filtrate -->
<div class="list-group" style="max-height: 300px; overflow-y: auto;">
@foreach (var entity in GetFilteredRestEntities())
{
<a class="list-group-item list-group-item-action @(selectedRestEntity?.Name == entity.Name ? "active" : "")"
@onclick="@(() => SelectRestEntity(entity))">
<div class="d-flex justify-content-between align-items-center">
<div>
<i class="fas fa-cube"></i> @entity.Name
@if (!string.IsNullOrEmpty(entity.Label))
{
<small class="text-muted d-block">@entity.Label</small>
}
</div>
@if (selectedRestEntity?.Name == entity.Name)
{
<span class="badge bg-primary">Selezionata</span>
}
</div>
</a>
}
</div>
@if (!GetFilteredRestEntities().Any())
{
<div class="alert alert-info mt-2">
<i class="fas fa-info-circle"></i> Nessuna entità trovata con il termine di ricerca "@restSearchTerm"
</div>
} </div>
}
</div>
</div>
</div>
</div> <!-- Sezione Mapping (quando la fonte è selezionata e REST è connesso) -->
@{
var isSourceReady = (selectedSourceType == "database" && isDatabaseConnected && !string.IsNullOrEmpty(selectedTable)) ||
(selectedSourceType == "file" && !string.IsNullOrEmpty(selectedSheet));
}
@if (isSourceReady && isRestConnected && selectedRestEntity != null)
{
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header bg-success text-white">
<h5><i class="fas fa-exchange-alt"></i> Mapping Campi</h5>
</div> <div class="card-body">
@{
var sourceDisplayName = selectedSourceType == "database" ? selectedTable : selectedSheet;
var sourceTypeName = selectedSourceType == "database" ? "Tabella" : "Foglio";
}
<h6>Mapping tra @sourceTypeName @sourceDisplayName e @selectedRestEntity.Name</h6>
<p class="text-muted">Configura il mapping tra i campi della fonte dati e le proprietà dell'entità REST</p>
<div class="row">
<!-- Colonna Sinistra: Campi Fonte -->
<div class="col-5">
<h6>Campi @sourceTypeName (@sourceDisplayName)</h6>
<div class="list-group" style="max-height: 400px; overflow-y: auto;">
@if (selectedSourceType == "database" && databaseTables.ContainsKey(selectedTable))
{
@foreach (var column in databaseTables[selectedTable])
{
<a class="list-group-item list-group-item-action @(selectedDbColumn == column.Name ? "active" : "")"
@onclick="@(() => SelectDbColumn(column.Name))">
<div class="d-flex justify-content-between align-items-center">
<div>
<strong>@column.Name</strong>
<small class="text-muted d-block">@column.DataType</small>
</div>
<div>
@if (column.IsPrimaryKey)
{
<span class="badge bg-primary">PK</span>
}
@if (fieldMappings.ContainsKey(column.Name))
{
<span class="badge bg-success">Mapped</span>
}
</div>
</div>
</a>
}
}
else if (selectedSourceType == "file" && fileSheets.ContainsKey(selectedSheet))
{
@foreach (var column in fileSheets[selectedSheet])
{
<a class="list-group-item list-group-item-action @(selectedDbColumn == column ? "active" : "")"
@onclick="@(() => SelectDbColumn(column))">
<div class="d-flex justify-content-between align-items-center">
<div>
<strong>@column</strong>
<small class="text-muted d-block">Colonna File</small>
</div>
<div>
@if (fieldMappings.ContainsKey(column))
{
<span class="badge bg-success">Mapped</span>
}
</div>
</div>
</a>
}
}
</div>
</div>
<!-- Colonna Centrale: Controlli Mapping -->
<div class="col-2 text-center">
<div class="d-flex flex-column justify-content-center h-100">
<button class="btn btn-success mb-2" @onclick="CreateMapping"
disabled="@(string.IsNullOrEmpty(selectedDbColumn) || string.IsNullOrEmpty(selectedRestProperty))">
<i class="fas fa-arrow-right"></i>
<small class="d-block">Map</small>
</button>
<button class="btn btn-danger mb-2" @onclick="RemoveMapping"
disabled="@(string.IsNullOrEmpty(selectedDbColumn) || !fieldMappings.ContainsKey(selectedDbColumn))">
<i class="fas fa-times"></i>
<small class="d-block">Remove</small>
</button>
<button class="btn btn-warning mb-2" @onclick="AutoMapFields">
<i class="fas fa-magic"></i>
<small class="d-block">Auto</small>
</button>
<button class="btn btn-secondary" @onclick="ClearAllMappings">
<i class="fas fa-trash"></i>
<small class="d-block">Clear</small>
</button>
</div>
</div>
<!-- Colonna Destra: Proprietà REST -->
<div class="col-5">
<h6>Proprietà REST (@selectedRestEntity.Name)</h6>
<div class="list-group" style="max-height: 400px; overflow-y: auto;">
@if (restEntityDetails != null)
{
@foreach (var property in restEntityDetails.Properties)
{
<a class="list-group-item list-group-item-action @(selectedRestProperty == property.Name ? "active" : "")"
@onclick="@(() => SelectRestProperty(property.Name))">
<div class="d-flex justify-content-between align-items-center">
<div>
<strong>@property.Name</strong>
<small class="text-muted d-block">@property.Type</small>
</div>
<div>
@if (property.IsRequired)
{
<span class="badge bg-danger">Required</span>
}
@if (fieldMappings.ContainsValue(property.Name))
{
<span class="badge bg-success">Mapped</span>
}
</div>
</div>
</a>
}
}
</div>
</div>
</div>
<!-- Sezione Mappature Correnti -->
@if (fieldMappings.Any())
{
<div class="mt-4">
<h6>Mappature Correnti (@fieldMappings.Count)</h6>
<div class="table-responsive">
<table class="table table-sm table-striped">
<thead>
<tr>
<th>Campo Database</th>
<th>Tipo DB</th>
<th>→</th>
<th>Proprietà REST</th>
<th>Tipo REST</th>
<th>Azioni</th>
</tr>
</thead> <tbody> @foreach (var mapping in fieldMappings)
{
DbColumnInfo? dbColumn = null;
if (selectedSourceType == "database" && !string.IsNullOrEmpty(selectedTable))
{
dbColumn = databaseTables.ContainsKey(selectedTable) ?
databaseTables[selectedTable].FirstOrDefault(c => c.Name == mapping.Key) : null;
}
var restProperty = restEntityDetails?.Properties.FirstOrDefault(p => p.Name == mapping.Value);
<tr>
<td><strong>@mapping.Key</strong></td>
<td><small class="text-muted">@(dbColumn?.DataType ?? (selectedSourceType == "file" ? "Text" : "Unknown"))</small></td>
<td><i class="fas fa-arrow-right text-success"></i></td>
<td><strong>@mapping.Value</strong></td>
<td><small class="text-muted">@(restProperty?.Type ?? "Unknown")</small></td>
<td>
<button class="btn btn-sm btn-danger" @onclick="@(() => RemoveSpecificMapping(mapping.Key))">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
} <div class="mt-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<button class="btn btn-success" @onclick="StartDataTransfer" disabled="@(!fieldMappings.Any() || isTransferringData)"> @if (isTransferringData)
{
<span class="spinner-border spinner-border-sm me-2"></span>
<i class="fas fa-sync-alt"></i> @("Trasferimento in corso")
}
else
{
<i class="fas fa-play"></i> @("Avvia Trasferimento Dati")
}
</button>
@if (fieldMappings.Any())
{
<button class="btn btn-info ms-2" @onclick="ShowMappingSummary">
<i class="fas fa-list"></i> Riepilogo Mapping
</button>
}
</div>
<div class="text-muted">
@if (fieldMappings.Any())
{
<small>
<i class="fas fa-arrow-right"></i> @fieldMappings.Count mapping(s) configurati
</small>
}
else
{
<small>
<i class="fas fa-exclamation-triangle"></i> Configura almeno una mappatura per iniziare
</small>
}
</div>
</div>
@if (!string.IsNullOrEmpty(transferMessage))
{
<div class="alert @(transferMessageType == "success" ? "alert-success" : "alert-danger") mt-3" role="alert">
<i class="fas @(transferMessageType == "success" ? "fa-check-circle" : "fa-exclamation-circle")"></i>
@transferMessage
</div>
} </div>
</div>
</div>
</div>
</div>
}
</div>
@code {
// Stato delle credenziali
private List<DatabaseCredential> databaseCredentials = new();
private List<RestApiCredential> 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<string, IEnumerable<DbColumnInfo>> databaseTables = new();
private string selectedTable = "";
private string databaseSearchTerm = ""; // File handling
private string selectedFileName = "";
private bool isProcessingFile = false;
private string fileErrorMessage = "";
private Dictionary<string, IEnumerable<string>> fileSheets = new(); // SheetName -> Columns
private Dictionary<string, List<Dictionary<string, object>>> 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<RestEntitySummary> restEntities = new();
private RestEntitySummary? selectedRestEntity = null;
private RestEntityInfo? restEntityDetails = null;
private string restSearchTerm = "";
// Mapping campi
private Dictionary<string, string> fieldMappings = new(); // DbColumn -> RestProperty
private string selectedDbColumn = "";
private string selectedRestProperty = "";
// Trasferimento dati
private bool isTransferringData = false;
private string transferMessage = "";
private string transferMessageType = "";
// 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<Dictionary<string, object>>();
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<string, object>();
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<string> ParseCsvLine(string line, char separator = ',')
{
var result = new List<string>();
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<string>();
var dataRows = new List<Dictionary<string, object>>();
// 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<string, object>();
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();
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<Dictionary<string, object>> GetCurrentPageData()
{
if (string.IsNullOrEmpty(selectedSheet) || !fileData.ContainsKey(selectedSheet))
return new List<Dictionary<string, object>>();
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 = "";
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
currentDatabaseManager = await ConnectionFactory.CreateDatabaseManagerAsync(selectedDatabaseCredential);
Logger.LogInformation("Iniziando discovery dello schema per database {DatabaseType} con credenziale: {CredentialName}", credential.DatabaseType, selectedDatabaseCredential);
// Discovery dello schema
var schema = await currentDatabaseManager.GetDatabaseSchemaAsync();
Logger.LogInformation("Schema discovery completato. Tipo restituito: {SchemaType}, Numero elementi: {Count}",
schema?.GetType().Name ?? "null",
schema?.Count() ?? 0);
if (schema != null)
{
foreach (var item in schema.Take(5)) // Log primi 5 elementi per debug
{
Logger.LogInformation("Schema item - Key: {Key}, Value type: {ValueType}, Column count: {ColumnCount}",
item.Key,
item.Value?.GetType().Name ?? "null",
item.Value?.Count() ?? 0);
}
}
databaseTables = schema as Dictionary<string, IEnumerable<DbColumnInfo>> ??
(schema != null ? new Dictionary<string, IEnumerable<DbColumnInfo>>(schema) : new Dictionary<string, IEnumerable<DbColumnInfo>>());
Logger.LogInformation("Database tables dopo conversione: {Count} tabelle", databaseTables.Count);
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 void SelectTable(string tableName)
{
selectedTable = tableName;
// Clear mappings when changing table
ClearAllMappings();
} 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<string> GetFilteredDatabaseTables()
{
if (string.IsNullOrEmpty(databaseSearchTerm))
return databaseTables.Keys;
return databaseTables.Keys.Where(table =>
table.Contains(databaseSearchTerm, StringComparison.OrdinalIgnoreCase));
}
private IEnumerable<RestEntitySummary> 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 = "";
transferMessage = "";
transferMessageType = "";
Logger.LogInformation("Tutti i mapping sono stati cancellati");
}
private void AutoMapFields()
{
if (!databaseTables.ContainsKey(selectedTable) || restEntityDetails == null)
return;
var dbColumns = databaseTables[selectedTable];
var restProperties = restEntityDetails.Properties;
int mappingsCreated = 0;
foreach (var dbColumn in dbColumns)
{
// Trova una proprietà REST con nome simile
var matchingProperty = restProperties.FirstOrDefault(p =>
string.Equals(p.Name, dbColumn.Name, StringComparison.OrdinalIgnoreCase) ||
string.Equals(p.Name.Replace("_", ""), dbColumn.Name.Replace("_", ""), StringComparison.OrdinalIgnoreCase) ||
string.Equals(p.Name.Replace("Id", ""), dbColumn.Name.Replace("Id", ""), StringComparison.OrdinalIgnoreCase)
);
if (matchingProperty != null && !fieldMappings.ContainsKey(dbColumn.Name))
{
fieldMappings[dbColumn.Name] = matchingProperty.Name;
mappingsCreated++;
}
} Logger.LogInformation("Auto-mapping completato. Creati {Count} mapping automatici", mappingsCreated);
} private async Task ShowMappingSummary()
{
var summary = "Riepilogo Mapping:\n\n";
foreach (var mapping in fieldMappings)
{
summary += $"• {mapping.Key} → {mapping.Value}\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" && (currentDatabaseManager == null || string.IsNullOrEmpty(selectedTable)))
{
transferMessage = "Database non connesso o tabella non selezionata.";
transferMessageType = "error";
return;
}
if (selectedSourceType == "file" && string.IsNullOrEmpty(selectedSheet))
{
transferMessage = "File non caricato o foglio non selezionato.";
transferMessageType = "error";
return;
}
isTransferringData = true;
transferMessage = "";
transferMessageType = "";
try
{
var sourceName = selectedSourceType == "database" ? 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. Trasforma e trasferisci ogni record
int successCount = 0;
int errorCount = 0;
var errors = new List<string>();
foreach (var record in records)
{
try
{
// Trasforma il record in base ai mapping
var restData = TransformRecordToRestEntity(record);
// Esegui upsert (crea o aggiorna)
var result = await currentRestClient.UpsertEntityAsync(selectedRestEntity.Name, restData);
if (result != null)
{
successCount++;
Logger.LogDebug("Record trasferito con successo: {Data}", string.Join(", ", restData.Select(kvp => $"{kvp.Key}={kvp.Value}")));
}
else
{
errorCount++;
errors.Add($"Errore nel trasferimento del record (result null)");
}
}
catch (Exception ex)
{
errorCount++;
errors.Add($"Errore nel trasferimento: {ex.Message}");
Logger.LogError(ex, "Errore nel trasferimento di un record");
}
}
// 3. Mostra risultati
if (errorCount == 0)
{
transferMessage = $"Trasferimento completato con successo! {successCount} record trasferiti.";
transferMessageType = "success";
}
else
{
transferMessage = $"Trasferimento completato con errori. Successi: {successCount}, Errori: {errorCount}. Primi errori: {string.Join("; ", errors.Take(3))}";
transferMessageType = "error";
}
Logger.LogInformation("Trasferimento completato. Successi: {SuccessCount}, Errori: {ErrorCount}", successCount, 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<IEnumerable<Dictionary<string, object>>> GetAllRecordsFromSource()
{
if (selectedSourceType == "database")
{
return await GetAllRecordsFromDatabase();
}
else if (selectedSourceType == "file")
{
return await GetAllRecordsFromFile();
}
return new List<Dictionary<string, object>>();
}
private async Task<IEnumerable<Dictionary<string, object>>> GetAllRecordsFromDatabase()
{
if (currentDatabaseManager == null || string.IsNullOrEmpty(selectedTable))
return new List<Dictionary<string, object>>();
try
{
// Usa il database manager per eseguire una query che ottiene tutti i record
// Questo è un esempio semplificato - potresti voler implementare paginazione per tabelle grandi
return await currentDatabaseManager.GetAllRecordsAsync(selectedTable);
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nell'ottenere i record dalla tabella {Table}", selectedTable);
throw;
}
} private async Task<IEnumerable<Dictionary<string, object>>> GetAllRecordsFromFile()
{
if (string.IsNullOrEmpty(selectedSheet) || !fileData.ContainsKey(selectedSheet))
{
return new List<Dictionary<string, object>>();
}
await Task.CompletedTask;
return fileData[selectedSheet];
}
private Dictionary<string, object> TransformRecordToRestEntity(Dictionary<string, object> dbRecord)
{
var restData = new Dictionary<string, object>();
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<char, int>();
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
}
}