feat: Implementa pagina di gestione profili avanzata
- Aggiunge nuova pagina ProfilesManagement (/profiles) con dashboard completo - Implementa statistiche profili (totali, attivi, recenti, non utilizzati) - Aggiunge filtri avanzati per ricerca e ordinamento profili - Implementa visualizzazione dettagli profili con mappature campi - Aggiunge funzionalità di eliminazione profili con conferma - Implementa esportazione profili in formato JSON - Aggiunge sistema di notifiche toast per feedback utente - Integra navigazione nel menu principale - Risolve errori di compilazione e duplicazione file - Migliora UX con design responsive e interfaccia moderna
This commit is contained in:
@@ -11,6 +11,9 @@
|
||||
@using System.Text
|
||||
@using System.Data
|
||||
@using ExcelDataReader
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Components
|
||||
@inject IDataConnectionCredentialService CredentialService
|
||||
@inject IDataConnectionFactory ConnectionFactory
|
||||
@inject IJSRuntime JSRuntime
|
||||
|
||||
@@ -0,0 +1,413 @@
|
||||
@page "/profiles"
|
||||
@using CredentialManager.Models
|
||||
@using CredentialManager.Services
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.JSInterop
|
||||
@using System.Text.Json
|
||||
|
||||
<PageTitle>Gestione Profili - Data Coupler</PageTitle>
|
||||
|
||||
<div class="container-fluid">
|
||||
<!-- Header della pagina -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h2><i class="fas fa-layer-group"></i> Gestione Profili</h2>
|
||||
<p class="text-muted">Gestisci e monitora tutti i profili di configurazione Data Coupler</p>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="btn btn-primary" @onclick="RefreshProfiles">
|
||||
<i class="fas fa-sync-alt"></i> Aggiorna
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard con statistiche -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card text-white bg-primary">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h4 class="card-title">@totalProfiles</h4>
|
||||
<p class="card-text">Profili Totali</p>
|
||||
</div>
|
||||
<div class="align-self-center">
|
||||
<i class="fas fa-layer-group fa-2x"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-white bg-success">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h4 class="card-title">@activeProfiles</h4>
|
||||
<p class="card-text">Profili Attivi</p>
|
||||
</div>
|
||||
<div class="align-self-center">
|
||||
<i class="fas fa-check-circle fa-2x"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-white bg-info">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h4 class="card-title">@profilesThisWeek</h4>
|
||||
<p class="card-text">Creati Questa Settimana</p>
|
||||
</div>
|
||||
<div class="align-self-center">
|
||||
<i class="fas fa-calendar-week fa-2x"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-white bg-warning">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h4 class="card-title">@unusedProfiles</h4>
|
||||
<p class="card-text">Non Utilizzati</p>
|
||||
</div>
|
||||
<div class="align-self-center">
|
||||
<i class="fas fa-exclamation-triangle fa-2x"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filtri e ricerca -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="fas fa-filter"></i> Filtri e Ricerca</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Ricerca:</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
||||
<input type="text" class="form-control" @bind="searchTerm" @oninput="FilterProfiles" placeholder="Cerca profili..." />
|
||||
@if (!string.IsNullOrEmpty(searchTerm))
|
||||
{
|
||||
<button class="btn btn-outline-secondary" @onclick="ClearSearch">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Tipo sorgente:</label>
|
||||
<select class="form-select" @bind="selectedSourceType">
|
||||
<option value="">Tutti i tipi</option>
|
||||
<option value="database">Database</option>
|
||||
<option value="file">File</option>
|
||||
<option value="rest">REST</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Utilizzo:</label>
|
||||
<select class="form-select" @bind="selectedUsageFilter">
|
||||
<option value="">Tutti</option>
|
||||
<option value="used">Utilizzati</option>
|
||||
<option value="unused">Mai utilizzati</option>
|
||||
<option value="recent">Utilizzati di recente</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Ordinamento:</label>
|
||||
<select class="form-select" @bind="sortOrder">
|
||||
<option value="name">Nome</option>
|
||||
<option value="created">Data creazione</option>
|
||||
<option value="lastused">Ultimo utilizzo</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lista profili -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0"><i class="fas fa-list"></i> Elenco Profili (@GetFilteredProfiles().Count())</h5>
|
||||
<button class="btn btn-success btn-sm" @onclick="ExportProfiles">
|
||||
<i class="fas fa-download"></i> Esporta JSON
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (isLoading)
|
||||
{
|
||||
<div class="text-center p-4">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Caricamento...</span>
|
||||
</div>
|
||||
<p class="mt-2">Caricamento profili in corso...</p>
|
||||
</div>
|
||||
}
|
||||
else if (!GetFilteredProfiles().Any())
|
||||
{
|
||||
<div class="text-center p-4">
|
||||
<i class="fas fa-folder-open fa-3x text-muted mb-3"></i>
|
||||
<h5>Nessun profilo trovato</h5>
|
||||
<p class="text-muted">Non ci sono profili che corrispondono ai criteri di ricerca.</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
<th>Descrizione</th>
|
||||
<th>Tipo</th>
|
||||
<th>Sorgente → Destinazione</th>
|
||||
<th>Mappings</th>
|
||||
<th>Creato</th>
|
||||
<th>Ultimo Uso</th>
|
||||
<th>Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var profile in GetFilteredProfiles())
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<strong>@profile.Name</strong>
|
||||
@if (profile.LastUsedAt.HasValue && profile.LastUsedAt.Value > DateTime.Now.AddDays(-7))
|
||||
{
|
||||
<span class="badge bg-success ms-2">Recente</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (!string.IsNullOrEmpty(profile.Description))
|
||||
{
|
||||
<span title="@profile.Description">
|
||||
@(profile.Description.Length > 50 ? profile.Description.Substring(0, 50) + "..." : profile.Description)
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">-</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-@GetSourceTypeBadgeClass(profile.SourceType)">
|
||||
@GetSourceTypeDisplayName(profile.SourceType)
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<small>@profile.SourceTable → @profile.DestinationEndpoint</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-info">@GetMappingCount(profile)</span>
|
||||
</td>
|
||||
<td>
|
||||
<small>@profile.CreatedAt.ToString("dd/MM/yyyy")</small>
|
||||
</td>
|
||||
<td>
|
||||
@if (profile.LastUsedAt.HasValue)
|
||||
{
|
||||
<small>@profile.LastUsedAt.Value.ToString("dd/MM/yyyy")</small>
|
||||
}
|
||||
else
|
||||
{
|
||||
<small class="text-muted">Mai</small>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button type="button" class="btn btn-outline-info" @onclick="() => ShowDetails(profile)">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger" @onclick="() => ConfirmDelete(profile)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Dettagli -->
|
||||
@if (showDetailsModal && selectedProfile != null)
|
||||
{
|
||||
<div class="modal fade show d-block" style="background-color: rgba(0,0,0,0.5);">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-info-circle"></i> Dettagli Profilo: @selectedProfile.Name
|
||||
</h5>
|
||||
<button type="button" class="btn-close" @onclick="CloseDetailsModal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6><i class="fas fa-cog"></i> Informazioni Generali</h6>
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4">Nome:</dt>
|
||||
<dd class="col-sm-8">@selectedProfile.Name</dd>
|
||||
|
||||
<dt class="col-sm-4">Descrizione:</dt>
|
||||
<dd class="col-sm-8">@(selectedProfile.Description ?? "Nessuna descrizione")</dd>
|
||||
|
||||
<dt class="col-sm-4">Tipo Sorgente:</dt>
|
||||
<dd class="col-sm-8">
|
||||
<span class="badge bg-@GetSourceTypeBadgeClass(selectedProfile.SourceType)">
|
||||
@GetSourceTypeDisplayName(selectedProfile.SourceType)
|
||||
</span>
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-4">Sorgente:</dt>
|
||||
<dd class="col-sm-8">@selectedProfile.SourceTable</dd>
|
||||
|
||||
<dt class="col-sm-4">Destinazione:</dt>
|
||||
<dd class="col-sm-8">@selectedProfile.DestinationEndpoint</dd>
|
||||
|
||||
<dt class="col-sm-4">Creato:</dt>
|
||||
<dd class="col-sm-8">@selectedProfile.CreatedAt.ToString("dd/MM/yyyy HH:mm")</dd>
|
||||
|
||||
<dt class="col-sm-4">Ultimo Uso:</dt>
|
||||
<dd class="col-sm-8">
|
||||
@if (selectedProfile.LastUsedAt.HasValue)
|
||||
{
|
||||
@selectedProfile.LastUsedAt.Value.ToString("dd/MM/yyyy HH:mm")
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">Mai utilizzato</span>
|
||||
}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6><i class="fas fa-exchange-alt"></i> Mappature Campi</h6>
|
||||
@if (!string.IsNullOrEmpty(selectedProfile.FieldMappingJson))
|
||||
{
|
||||
var mappings = GetProfileMappings(selectedProfile);
|
||||
<div class="table-responsive" style="max-height: 300px; overflow-y: auto;">
|
||||
<table class="table table-sm table-striped">
|
||||
<thead class="table-light sticky-top">
|
||||
<tr>
|
||||
<th>Sorgente</th>
|
||||
<th>Destinazione</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var mapping in mappings)
|
||||
{
|
||||
<tr>
|
||||
<td><code>@mapping.SourceField</code></td>
|
||||
<td><code>@mapping.DestinationField</code></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
Nessuna mappatura configurata
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @onclick="CloseDetailsModal">
|
||||
<i class="fas fa-times"></i> Chiudi
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Modal Eliminazione -->
|
||||
@if (showDeleteModal && profileToDelete != null)
|
||||
{
|
||||
<div class="modal fade show d-block" style="background-color: rgba(0,0,0,0.5);">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-exclamation-triangle text-warning"></i> Conferma Eliminazione
|
||||
</h5>
|
||||
<button type="button" class="btn-close" @onclick="CancelDelete"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Sei sicuro di voler eliminare il profilo <strong>@profileToDelete.Name</strong>?</p>
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>Attenzione:</strong> Questa operazione non può essere annullata.
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @onclick="CancelDelete">
|
||||
<i class="fas fa-times"></i> Annulla
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger" @onclick="DeleteProfile" disabled="@isDeleting">
|
||||
@if (isDeleting)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm" role="status"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="fas fa-trash"></i>
|
||||
}
|
||||
Elimina
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Toast Notifiche -->
|
||||
@if (!string.IsNullOrEmpty(toastMessage))
|
||||
{
|
||||
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 11">
|
||||
<div class="toast show" role="alert">
|
||||
<div class="toast-header">
|
||||
<i class="fas fa-@(toastType == "success" ? "check-circle text-success" : "exclamation-triangle text-warning") me-2"></i>
|
||||
<strong class="me-auto">Data Coupler</strong>
|
||||
<button type="button" class="btn-close" @onclick="ClearToast"></button>
|
||||
</div>
|
||||
<div class="toast-body">
|
||||
@toastMessage
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using CredentialManager.Models;
|
||||
using CredentialManager.Services;
|
||||
using Microsoft.JSInterop;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Data_Coupler.Pages;
|
||||
|
||||
public partial class ProfilesManagement : ComponentBase
|
||||
{
|
||||
[Inject] private IDataCouplerProfileService ProfileService { get; set; } = null!;
|
||||
[Inject] private IJSRuntime JSRuntime { get; set; } = null!;
|
||||
[Inject] private ILogger<ProfilesManagement> Logger { get; set; } = null!;
|
||||
|
||||
// State delle liste
|
||||
private List<DataCouplerProfile> allProfiles = new();
|
||||
|
||||
// State dei filtri
|
||||
private string searchTerm = "";
|
||||
private string selectedSourceType = "";
|
||||
private string selectedUsageFilter = "";
|
||||
private string sortOrder = "name";
|
||||
|
||||
// State delle modali
|
||||
private bool showDetailsModal = false;
|
||||
private bool showDeleteModal = false;
|
||||
private DataCouplerProfile? selectedProfile = null;
|
||||
private DataCouplerProfile? profileToDelete = null;
|
||||
|
||||
// State operazioni
|
||||
private bool isLoading = true;
|
||||
private bool isDeleting = false;
|
||||
|
||||
// Toast notifications
|
||||
private string toastMessage = "";
|
||||
private string toastType = "info";
|
||||
|
||||
// Statistiche
|
||||
private int totalProfiles => allProfiles.Count;
|
||||
private int activeProfiles => allProfiles.Count(p => p.LastUsedAt.HasValue && p.LastUsedAt.Value > DateTime.Now.AddDays(-30));
|
||||
private int profilesThisWeek => allProfiles.Count(p => p.CreatedAt > DateTime.Now.AddDays(-7));
|
||||
private int unusedProfiles => allProfiles.Count(p => !p.LastUsedAt.HasValue);
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadProfiles();
|
||||
}
|
||||
|
||||
private async Task LoadProfiles()
|
||||
{
|
||||
try
|
||||
{
|
||||
isLoading = true;
|
||||
var profiles = await ProfileService.GetAllProfilesAsync();
|
||||
allProfiles = profiles.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nel caricamento dei profili");
|
||||
ShowToast("Errore nel caricamento dei profili: " + ex.Message, "error");
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshProfiles()
|
||||
{
|
||||
await LoadProfiles();
|
||||
}
|
||||
|
||||
// Filtri e ricerca
|
||||
private IEnumerable<DataCouplerProfile> GetFilteredProfiles()
|
||||
{
|
||||
var filtered = allProfiles.AsEnumerable();
|
||||
|
||||
// Filtro per testo
|
||||
if (!string.IsNullOrEmpty(searchTerm))
|
||||
{
|
||||
filtered = filtered.Where(p =>
|
||||
p.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) ||
|
||||
(p.Description?.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) ?? false));
|
||||
}
|
||||
|
||||
// Filtro per tipo sorgente
|
||||
if (!string.IsNullOrEmpty(selectedSourceType))
|
||||
{
|
||||
filtered = filtered.Where(p => p.SourceType.Equals(selectedSourceType, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
// Filtro per utilizzo
|
||||
if (!string.IsNullOrEmpty(selectedUsageFilter))
|
||||
{
|
||||
filtered = selectedUsageFilter switch
|
||||
{
|
||||
"used" => filtered.Where(p => p.LastUsedAt.HasValue),
|
||||
"unused" => filtered.Where(p => !p.LastUsedAt.HasValue),
|
||||
"recent" => filtered.Where(p => p.LastUsedAt.HasValue && p.LastUsedAt.Value > DateTime.Now.AddDays(-7)),
|
||||
_ => filtered
|
||||
};
|
||||
}
|
||||
|
||||
// Ordinamento
|
||||
filtered = sortOrder switch
|
||||
{
|
||||
"created" => filtered.OrderByDescending(p => p.CreatedAt),
|
||||
"lastused" => filtered.OrderByDescending(p => p.LastUsedAt ?? DateTime.MinValue),
|
||||
_ => filtered.OrderBy(p => p.Name)
|
||||
};
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private void FilterProfiles()
|
||||
{
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void ClearSearch()
|
||||
{
|
||||
searchTerm = "";
|
||||
FilterProfiles();
|
||||
}
|
||||
|
||||
// Operazioni sui profili
|
||||
private void ShowDetails(DataCouplerProfile profile)
|
||||
{
|
||||
selectedProfile = profile;
|
||||
showDetailsModal = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void CloseDetailsModal()
|
||||
{
|
||||
showDetailsModal = false;
|
||||
selectedProfile = null;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void ConfirmDelete(DataCouplerProfile profile)
|
||||
{
|
||||
profileToDelete = profile;
|
||||
showDeleteModal = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void CancelDelete()
|
||||
{
|
||||
showDeleteModal = false;
|
||||
profileToDelete = null;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task DeleteProfile()
|
||||
{
|
||||
if (profileToDelete == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
isDeleting = true;
|
||||
StateHasChanged();
|
||||
|
||||
await ProfileService.DeleteProfileAsync(profileToDelete.Id);
|
||||
allProfiles.Remove(profileToDelete);
|
||||
|
||||
ShowToast("Profilo eliminato con successo", "success");
|
||||
CancelDelete();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nell'eliminazione del profilo {ProfileId}", profileToDelete.Id);
|
||||
ShowToast("Errore nell'eliminazione: " + ex.Message, "error");
|
||||
}
|
||||
finally
|
||||
{
|
||||
isDeleting = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExportProfiles()
|
||||
{
|
||||
try
|
||||
{
|
||||
var profilesToExport = GetFilteredProfiles().ToList();
|
||||
var json = JsonSerializer.Serialize(profilesToExport, new JsonSerializerOptions { WriteIndented = true });
|
||||
var fileName = $"profiles_export_{DateTime.Now:yyyyMMdd_HHmmss}.json";
|
||||
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(json);
|
||||
var stream = new MemoryStream(bytes);
|
||||
|
||||
using var streamRef = new DotNetStreamReference(stream);
|
||||
await JSRuntime.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef);
|
||||
|
||||
ShowToast("Esportazione completata", "success");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nell'esportazione dei profili");
|
||||
ShowToast("Errore nell'esportazione: " + ex.Message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods per la UI
|
||||
private string GetSourceTypeBadgeClass(string sourceType)
|
||||
{
|
||||
return sourceType?.ToLower() switch
|
||||
{
|
||||
"database" => "primary",
|
||||
"file" => "success",
|
||||
"rest" => "info",
|
||||
_ => "secondary"
|
||||
};
|
||||
}
|
||||
|
||||
private string GetSourceTypeDisplayName(string sourceType)
|
||||
{
|
||||
return sourceType?.ToLower() switch
|
||||
{
|
||||
"database" => "Database",
|
||||
"file" => "File",
|
||||
"rest" => "REST API",
|
||||
_ => "Sconosciuto"
|
||||
};
|
||||
}
|
||||
|
||||
private int GetMappingCount(DataCouplerProfile profile)
|
||||
{
|
||||
if (string.IsNullOrEmpty(profile.FieldMappingJson))
|
||||
return 0;
|
||||
|
||||
try
|
||||
{
|
||||
var mappings = JsonSerializer.Deserialize<List<FieldMappingDto>>(profile.FieldMappingJson);
|
||||
return mappings?.Count ?? 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private List<FieldMappingDto> GetProfileMappings(DataCouplerProfile profile)
|
||||
{
|
||||
if (string.IsNullOrEmpty(profile.FieldMappingJson))
|
||||
return new List<FieldMappingDto>();
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<List<FieldMappingDto>>(profile.FieldMappingJson) ?? new List<FieldMappingDto>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new List<FieldMappingDto>();
|
||||
}
|
||||
}
|
||||
|
||||
// Toast notifications
|
||||
private void ShowToast(string message, string type = "info")
|
||||
{
|
||||
toastMessage = message;
|
||||
toastType = type;
|
||||
StateHasChanged();
|
||||
|
||||
// Auto-hide dopo 5 secondi
|
||||
_ = Task.Delay(5000).ContinueWith(_ => ClearToast());
|
||||
}
|
||||
|
||||
private void ClearToast()
|
||||
{
|
||||
toastMessage = "";
|
||||
toastType = "info";
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
@@ -32,5 +32,18 @@
|
||||
|
||||
<script src="_framework/blazor.server.js"></script>
|
||||
<script src="js/site.js"></script>
|
||||
<script>
|
||||
window.downloadFileFromStream = async (fileName, contentStreamReference) => {
|
||||
const arrayBuffer = await contentStreamReference.arrayBuffer();
|
||||
const blob = new Blob([arrayBuffer]);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchorElement = document.createElement('a');
|
||||
anchorElement.href = url;
|
||||
anchorElement.download = fileName ?? '';
|
||||
anchorElement.click();
|
||||
anchorElement.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user