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.
This commit is contained in:
@@ -11,4 +11,9 @@
|
||||
<ProjectReference Include="..\CredentialManager\CredentialManager.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ExcelDataReader" Version="3.7.0" />
|
||||
<PackageReference Include="ExcelDataReader.DataSet" Version="3.7.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
@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
|
||||
@@ -20,16 +24,27 @@
|
||||
<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 - Database -->
|
||||
</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> Database Source</h5>
|
||||
<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>
|
||||
@@ -57,14 +72,14 @@
|
||||
<span class="badge bg-success ms-2">Connesso</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(databaseErrorMessage))
|
||||
} @if (!string.IsNullOrEmpty(databaseErrorMessage))
|
||||
{
|
||||
<div class="alert alert-danger" role="alert">
|
||||
@databaseErrorMessage
|
||||
</div>
|
||||
} <!-- Lista Tabelle -->
|
||||
}
|
||||
|
||||
<!-- Lista Tabelle -->
|
||||
@if (databaseTables.Any())
|
||||
{
|
||||
<div class="mb-3">
|
||||
@@ -118,6 +133,252 @@
|
||||
}
|
||||
</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>
|
||||
@@ -219,26 +480,31 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sezione Mapping (quando entrambi sono connessi) -->
|
||||
@if (isDatabaseConnected && isRestConnected && !string.IsNullOrEmpty(selectedTable) && selectedRestEntity != null)
|
||||
</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">
|
||||
<h6>Mapping tra @selectedTable e @selectedRestEntity.Name</h6>
|
||||
<p class="text-muted">Configura il mapping tra i campi del database e le proprietà dell'entità REST</p>
|
||||
</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 Database -->
|
||||
<!-- Colonna Sinistra: Campi Fonte -->
|
||||
<div class="col-5">
|
||||
<h6>Campi Database (@selectedTable)</h6>
|
||||
<h6>Campi @sourceTypeName (@sourceDisplayName)</h6>
|
||||
<div class="list-group" style="max-height: 400px; overflow-y: auto;">
|
||||
@if (databaseTables.ContainsKey(selectedTable))
|
||||
@if (selectedSourceType == "database" && databaseTables.ContainsKey(selectedTable))
|
||||
{
|
||||
@foreach (var column in databaseTables[selectedTable])
|
||||
{
|
||||
@@ -263,6 +529,27 @@
|
||||
</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>
|
||||
|
||||
@@ -339,15 +626,18 @@
|
||||
<th>Tipo REST</th>
|
||||
<th>Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var mapping in fieldMappings)
|
||||
</thead> <tbody> @foreach (var mapping in fieldMappings)
|
||||
{
|
||||
var dbColumn = databaseTables[selectedTable].FirstOrDefault(c => c.Name == mapping.Key);
|
||||
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 ?? "Unknown")</small></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>
|
||||
@@ -406,8 +696,7 @@
|
||||
<i class="fas @(transferMessageType == "success" ? "fa-check-circle" : "fa-exclamation-circle")"></i>
|
||||
@transferMessage
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -420,6 +709,9 @@
|
||||
private List<DatabaseCredential> databaseCredentials = new();
|
||||
private List<RestApiCredential> restApiCredentials = new();
|
||||
|
||||
// Selezione tipo fonte
|
||||
private string selectedSourceType = "";
|
||||
|
||||
// Credenziali selezionate
|
||||
private string selectedDatabaseCredential = "";
|
||||
private string selectedRestCredential = "";
|
||||
@@ -433,10 +725,24 @@
|
||||
// Messaggi di errore
|
||||
private string databaseErrorMessage = "";
|
||||
private string restErrorMessage = "";
|
||||
|
||||
// Database discovery
|
||||
private Dictionary<string, IEnumerable<DbColumnInfo>> databaseTables = new();
|
||||
private string selectedTable = "";
|
||||
private string databaseSearchTerm = "";
|
||||
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;
|
||||
@@ -461,9 +767,7 @@
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadCredentials();
|
||||
}
|
||||
|
||||
private async Task LoadCredentials()
|
||||
} private async Task LoadCredentials()
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -475,6 +779,339 @@
|
||||
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() ?? "";
|
||||
@@ -806,13 +1443,26 @@
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private async Task StartDataTransfer()
|
||||
// Check source-specific requirements
|
||||
if (selectedSourceType == "database" && (currentDatabaseManager == null || string.IsNullOrEmpty(selectedTable)))
|
||||
{
|
||||
if (!fieldMappings.Any() || currentDatabaseManager == null || currentRestClient == null || selectedRestEntity == null)
|
||||
transferMessage = "Database non connesso o tabella non selezionata.";
|
||||
transferMessageType = "error";
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedSourceType == "file" && string.IsNullOrEmpty(selectedSheet))
|
||||
{
|
||||
transferMessage = "Configurazione incompleta. Assicurati di aver selezionato tabella, entità e configurato almeno una mappatura.";
|
||||
transferMessage = "File non caricato o foglio non selezionato.";
|
||||
transferMessageType = "error";
|
||||
return;
|
||||
}
|
||||
@@ -823,16 +1473,17 @@
|
||||
|
||||
try
|
||||
{
|
||||
Logger.LogInformation("Iniziando trasferimento dati da {Table} a {Entity} con {MappingCount} mappature",
|
||||
selectedTable, selectedRestEntity.Name, fieldMappings.Count);
|
||||
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 tabella database
|
||||
var records = await GetAllRecordsFromTable();
|
||||
Logger.LogInformation("Ottenuti {RecordCount} record dalla tabella {Table}", records.Count(), selectedTable);
|
||||
// 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 tabella selezionata.";
|
||||
transferMessage = "Nessun record trovato nella fonte dati selezionata.";
|
||||
transferMessageType = "error";
|
||||
return;
|
||||
}
|
||||
@@ -895,9 +1546,21 @@
|
||||
{
|
||||
isTransferringData = false;
|
||||
}
|
||||
} private async Task<IEnumerable<Dictionary<string, object>>> GetAllRecordsFromSource()
|
||||
{
|
||||
if (selectedSourceType == "database")
|
||||
{
|
||||
return await GetAllRecordsFromDatabase();
|
||||
}
|
||||
else if (selectedSourceType == "file")
|
||||
{
|
||||
return await GetAllRecordsFromFile();
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<Dictionary<string, object>>> GetAllRecordsFromTable()
|
||||
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>>();
|
||||
@@ -913,6 +1576,15 @@
|
||||
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)
|
||||
@@ -1014,4 +1686,39 @@
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ using Data_Coupler.Services;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
// Registra il provider di encoding per ExcelDataReader (necessario per file .xls)
|
||||
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user