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:
2025-07-02 01:15:23 +02:00
parent 99da631aea
commit fb3b3142a7
5 changed files with 711 additions and 0 deletions
+413
View File
@@ -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>
}