feat: Refactor dataset management logic into a new SchemaDiscoveryService, removing it from the ReportsController.
This commit is contained in:
@@ -32,3 +32,7 @@ File riassuntivo dello stato di sviluppo di Zentral.
|
|||||||
- Correzione import path nel modulo Report Designer e registrazione modulo nel backend.
|
- Correzione import path nel modulo Report Designer e registrazione modulo nel backend.
|
||||||
- [2025-12-05 Rename Modules to Apps](./devlog/2025-12-05-194100_rename_modules_to_apps.md) - **Completato**
|
- [2025-12-05 Rename Modules to Apps](./devlog/2025-12-05-194100_rename_modules_to_apps.md) - **Completato**
|
||||||
- Rinomina terminologia "Modulo" in "Applicazione" (App) su Backend e Frontend.
|
- Rinomina terminologia "Modulo" in "Applicazione" (App) su Backend e Frontend.
|
||||||
|
- [2025-12-05 Remove Warehouse Tabs](./devlog/2025-12-05-224000_remove_warehouse_tabs.md) - **Completato**
|
||||||
|
- Rimozione tab interne e header dal modulo Magazzino per uniformità con la UI principale.
|
||||||
|
- [2025-12-05 Live Data Alignment](./devlog/2025-12-05-230000_live_data_alignment.md) - **Completato**
|
||||||
|
- Implementazione `SchemaDiscoveryService` per allineamento automatico dataset report con strutture dati live.
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Rimozione Tab Magazzino
|
||||||
|
|
||||||
|
## Obiettivo
|
||||||
|
Rimuovere le tab di navigazione interne al modulo Magazzino (`WarehouseLayout`), in quanto ridondanti rispetto alle tab principali dell'applicazione.
|
||||||
|
|
||||||
|
## Modifiche Apportate
|
||||||
|
- Modificato `src/frontend/src/apps/warehouse/components/WarehouseLayout.tsx`:
|
||||||
|
- Rimossa la componente `Tabs` e la logica associata (`navItems`, `useState`, `useEffect`).
|
||||||
|
- Rimosso l'header contenente il titolo "Gestione Magazzino" e i breadcrumbs.
|
||||||
|
- Semplificato il layout per mostrare solo l'`Outlet` all'interno di un `Box`.
|
||||||
|
- Aggiunto padding (`p: 3`) al contenitore del contenuto per garantire una spaziatura adeguata.
|
||||||
|
|
||||||
|
## Stato
|
||||||
|
Completato.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Live Data Alignment for Report Designer
|
||||||
|
|
||||||
|
## Obiettivo
|
||||||
|
Garantire che i dataset utilizzati nel report designer siano sempre automaticamente allineati con le strutture dati vive del gestionale, leggendo le strutture live invece di affidarsi a dati pre-configurati.
|
||||||
|
|
||||||
|
## Modifiche Apportate
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
1. **Nuovo Servizio `SchemaDiscoveryService`**:
|
||||||
|
* Creato un servizio che scansiona `ZentralDbContext` per trovare tutti i `DbSet` disponibili.
|
||||||
|
* Genera dinamicamente gli schemi dei dati basandosi sulle proprietà delle entità.
|
||||||
|
* Supporta il caricamento dinamico delle entità con eager loading delle proprietà di navigazione.
|
||||||
|
* Include un dizionario di metadati per mantenere descrizioni e icone curate per i dataset principali (Evento, Cliente, ecc.), pur supportando nuovi dataset automaticamente.
|
||||||
|
|
||||||
|
2. **Refactoring `ReportsController`**:
|
||||||
|
* Rimossi i metodi statici hardcoded per la generazione degli schemi (`GetEventoSchema`, ecc.).
|
||||||
|
* Rimossa la lista hardcoded dei dataset disponibili.
|
||||||
|
* Integrato `SchemaDiscoveryService` per ottenere la lista dei dataset, gli schemi e i dati.
|
||||||
|
* Aggiornato `GetVirtualDatasetEntities` per usare il servizio di discovery.
|
||||||
|
|
||||||
|
### Miglioramenti UX
|
||||||
|
1. **Etichette Leggibili**:
|
||||||
|
* Aggiornato `SchemaDiscoveryService` per rilevare automaticamente la proprietà migliore da usare come etichetta (RagioneSociale, Nome, Descrizione, ecc.).
|
||||||
|
* Implementato ordinamento alfabetico automatico basato sull'etichetta rilevata.
|
||||||
|
|
||||||
|
3. **Refactoring `VirtualDatasetsController`**:
|
||||||
|
* Rimosso il metodo hardcoded `GetBaseDatasetSchema`.
|
||||||
|
* Integrato `SchemaDiscoveryService` per la validazione e la generazione degli schemi dei dataset virtuali.
|
||||||
|
* Risolto un TODO per la determinazione automatica del tipo di campo negli schemi virtuali.
|
||||||
|
|
||||||
|
4. **Registrazione Servizio**:
|
||||||
|
* Registrato `SchemaDiscoveryService` in `Program.cs`.
|
||||||
|
|
||||||
|
## Risultato
|
||||||
|
Il Report Designer ora riflette automaticamente qualsiasi modifica al modello dati (nuove entità, nuovi campi) senza richiedere modifiche manuali al codice del controller. I dataset "core" mantengono le loro descrizioni user-friendly, mentre i nuovi dataset vengono esposti con nomi e descrizioni generati automaticamente.
|
||||||
@@ -12,11 +12,13 @@ public class ReportsController : ControllerBase
|
|||||||
{
|
{
|
||||||
private readonly ReportGeneratorService _reportGenerator;
|
private readonly ReportGeneratorService _reportGenerator;
|
||||||
private readonly ZentralDbContext _context;
|
private readonly ZentralDbContext _context;
|
||||||
|
private readonly SchemaDiscoveryService _schemaDiscovery;
|
||||||
|
|
||||||
public ReportsController(ReportGeneratorService reportGenerator, ZentralDbContext context)
|
public ReportsController(ReportGeneratorService reportGenerator, ZentralDbContext context, SchemaDiscoveryService schemaDiscovery)
|
||||||
{
|
{
|
||||||
_reportGenerator = reportGenerator;
|
_reportGenerator = reportGenerator;
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_schemaDiscovery = schemaDiscovery;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -126,26 +128,7 @@ public class ReportsController : ControllerBase
|
|||||||
[HttpGet("datasets")]
|
[HttpGet("datasets")]
|
||||||
public async Task<ActionResult<List<DatasetTypeDto>>> GetAvailableDatasets()
|
public async Task<ActionResult<List<DatasetTypeDto>>> GetAvailableDatasets()
|
||||||
{
|
{
|
||||||
var datasets = new List<DatasetTypeDto>
|
var datasets = _schemaDiscovery.GetAvailableDatasets();
|
||||||
{
|
|
||||||
// Dataset principali
|
|
||||||
new() { Id = "evento", Name = "Evento", Description = "Dati evento completi con cliente, location, ospiti, costi e risorse", Icon = "event", Category = "Principale" },
|
|
||||||
new() { Id = "cliente", Name = "Cliente", Description = "Anagrafica clienti completa", Icon = "people", Category = "Principale" },
|
|
||||||
new() { Id = "location", Name = "Location", Description = "Sedi e location eventi", Icon = "place", Category = "Principale" },
|
|
||||||
new() { Id = "articolo", Name = "Articolo", Description = "Catalogo articoli e materiali", Icon = "inventory", Category = "Principale" },
|
|
||||||
new() { Id = "risorsa", Name = "Risorsa", Description = "Staff e personale", Icon = "person", Category = "Principale" },
|
|
||||||
|
|
||||||
// Dataset di lookup/configurazione
|
|
||||||
new() { Id = "tipoEvento", Name = "Tipo Evento", Description = "Tipologie di evento (matrimonio, compleanno, etc.)", Icon = "category", Category = "Configurazione" },
|
|
||||||
new() { Id = "tipoOspite", Name = "Tipo Ospite", Description = "Tipologie di ospiti (adulti, bambini, etc.)", Icon = "groups", Category = "Configurazione" },
|
|
||||||
new() { Id = "categoria", Name = "Categoria Articoli", Description = "Categorie articoli con coefficienti di calcolo", Icon = "folder", Category = "Configurazione" },
|
|
||||||
new() { Id = "tipoRisorsa", Name = "Tipo Risorsa", Description = "Tipologie di risorse (cameriere, cuoco, etc.)", Icon = "badge", Category = "Configurazione" },
|
|
||||||
new() { Id = "tipoMateriale", Name = "Tipo Materiale", Description = "Tipologie di materiali", Icon = "category", Category = "Configurazione" },
|
|
||||||
|
|
||||||
// Dataset lista (per report con elenchi)
|
|
||||||
new() { Id = "listaEventi", Name = "Lista Eventi", Description = "Elenco eventi per report multipli", Icon = "list", Category = "Liste" },
|
|
||||||
new() { Id = "listaArticoli", Name = "Lista Articoli", Description = "Elenco articoli per catalogo", Icon = "list", Category = "Liste" },
|
|
||||||
};
|
|
||||||
|
|
||||||
// Aggiungi Virtual Dataset dal database
|
// Aggiungi Virtual Dataset dal database
|
||||||
var virtualDatasets = await _context.VirtualDatasets
|
var virtualDatasets = await _context.VirtualDatasets
|
||||||
@@ -173,7 +156,8 @@ public class ReportsController : ControllerBase
|
|||||||
[HttpGet("datasets/categories")]
|
[HttpGet("datasets/categories")]
|
||||||
public async Task<ActionResult<List<string>>> GetDatasetCategories()
|
public async Task<ActionResult<List<string>>> GetDatasetCategories()
|
||||||
{
|
{
|
||||||
var baseCategories = new List<string> { "Principale", "Configurazione", "Liste" };
|
var datasets = _schemaDiscovery.GetAvailableDatasets();
|
||||||
|
var baseCategories = datasets.Select(d => d.Category).Distinct().ToList();
|
||||||
|
|
||||||
// Aggiungi categorie dai Virtual Dataset
|
// Aggiungi categorie dai Virtual Dataset
|
||||||
var virtualCategories = await _context.VirtualDatasets
|
var virtualCategories = await _context.VirtualDatasets
|
||||||
@@ -188,6 +172,8 @@ public class ReportsController : ControllerBase
|
|||||||
baseCategories.Add(cat);
|
baseCategories.Add(cat);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
baseCategories.Sort();
|
||||||
|
|
||||||
return baseCategories;
|
return baseCategories;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +193,7 @@ public class ReportsController : ControllerBase
|
|||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
var staticSchema = GetSchemaForDataset(datasetId);
|
var staticSchema = _schemaDiscovery.GetSchema(datasetId);
|
||||||
if (staticSchema == null)
|
if (staticSchema == null)
|
||||||
return NotFound($"Dataset '{datasetId}' not found");
|
return NotFound($"Dataset '{datasetId}' not found");
|
||||||
return staticSchema;
|
return staticSchema;
|
||||||
@@ -230,22 +216,7 @@ public class ReportsController : ControllerBase
|
|||||||
return await GetVirtualDatasetEntities(virtualName, search, limit, offset);
|
return await GetVirtualDatasetEntities(virtualName, search, limit, offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
var entities = datasetId.ToLower() switch
|
return await _schemaDiscovery.GetEntities(datasetId, search, limit, offset);
|
||||||
{
|
|
||||||
"evento" => await GetEventiEntities(search, limit, offset),
|
|
||||||
"cliente" => await GetClientiEntities(search, limit, offset),
|
|
||||||
"location" => await GetLocationEntities(search, limit, offset),
|
|
||||||
"articolo" => await GetArticoliEntities(search, limit, offset),
|
|
||||||
"risorsa" => await GetRisorseEntities(search, limit, offset),
|
|
||||||
"tipoevento" => await GetTipiEventoEntities(search, limit, offset),
|
|
||||||
"tipoospite" => await GetTipiOspiteEntities(search, limit, offset),
|
|
||||||
"categoria" => await GetCategorieEntities(search, limit, offset),
|
|
||||||
"tiporisorsa" => await GetTipiRisorsaEntities(search, limit, offset),
|
|
||||||
"tipomateriale" => await GetTipiMaterialeEntities(search, limit, offset),
|
|
||||||
_ => new List<EntityListItemDto>()
|
|
||||||
};
|
|
||||||
|
|
||||||
return entities;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -254,358 +225,10 @@ public class ReportsController : ControllerBase
|
|||||||
[HttpGet("datasets/{datasetId}/count")]
|
[HttpGet("datasets/{datasetId}/count")]
|
||||||
public async Task<ActionResult<int>> GetEntityCount(string datasetId, [FromQuery] string? search = null)
|
public async Task<ActionResult<int>> GetEntityCount(string datasetId, [FromQuery] string? search = null)
|
||||||
{
|
{
|
||||||
var count = datasetId.ToLower() switch
|
return await _schemaDiscovery.CountEntities(datasetId, search);
|
||||||
{
|
|
||||||
"evento" => await CountEventi(search),
|
|
||||||
"cliente" => await CountClienti(search),
|
|
||||||
"location" => await CountLocation(search),
|
|
||||||
"articolo" => await CountArticoli(search),
|
|
||||||
"risorsa" => await CountRisorse(search),
|
|
||||||
"tipoevento" => await _context.TipiEvento.CountAsync(t => t.Attivo),
|
|
||||||
"tipoospite" => await _context.TipiOspite.CountAsync(t => t.Attivo),
|
|
||||||
"categoria" => await _context.CodiciCategoria.CountAsync(c => c.Attivo),
|
|
||||||
"tiporisorsa" => await _context.TipiRisorsa.CountAsync(t => t.Attivo),
|
|
||||||
"tipomateriale" => await _context.TipiMateriale.CountAsync(t => t.Attivo),
|
|
||||||
_ => 0
|
|
||||||
};
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Entity Queries
|
|
||||||
|
|
||||||
private async Task<List<EntityListItemDto>> GetEventiEntities(string? search, int limit, int offset)
|
|
||||||
{
|
|
||||||
var query = _context.Eventi
|
|
||||||
.Include(e => e.Cliente)
|
|
||||||
.Include(e => e.Location)
|
|
||||||
.Include(e => e.TipoEvento)
|
|
||||||
.AsQueryable();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(search))
|
|
||||||
{
|
|
||||||
search = search.ToLower();
|
|
||||||
query = query.Where(e =>
|
|
||||||
(e.Codice != null && e.Codice.ToLower().Contains(search)) ||
|
|
||||||
(e.Cliente != null && e.Cliente.RagioneSociale.ToLower().Contains(search)) ||
|
|
||||||
(e.Location != null && e.Location.Nome.ToLower().Contains(search)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return await query
|
|
||||||
.OrderByDescending(e => e.DataEvento)
|
|
||||||
.Skip(offset)
|
|
||||||
.Take(limit)
|
|
||||||
.Select(e => new EntityListItemDto
|
|
||||||
{
|
|
||||||
Id = e.Id,
|
|
||||||
Label = $"{e.Codice ?? $"EVT-{e.Id}"} - {e.DataEvento:dd/MM/yyyy}",
|
|
||||||
Description = $"{e.Cliente!.RagioneSociale ?? "N/D"} @ {e.Location!.Nome ?? "N/D"}",
|
|
||||||
SecondaryInfo = e.TipoEvento != null ? e.TipoEvento.Descrizione : null,
|
|
||||||
Status = e.Stato.ToString()
|
|
||||||
})
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<int> CountEventi(string? search)
|
|
||||||
{
|
|
||||||
var query = _context.Eventi.AsQueryable();
|
|
||||||
if (!string.IsNullOrWhiteSpace(search))
|
|
||||||
{
|
|
||||||
search = search.ToLower();
|
|
||||||
query = query.Where(e =>
|
|
||||||
(e.Codice != null && e.Codice.ToLower().Contains(search)) ||
|
|
||||||
(e.Cliente != null && e.Cliente.RagioneSociale.ToLower().Contains(search)));
|
|
||||||
}
|
|
||||||
return await query.CountAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<EntityListItemDto>> GetClientiEntities(string? search, int limit, int offset)
|
|
||||||
{
|
|
||||||
var query = _context.Clienti.Where(c => c.Attivo);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(search))
|
|
||||||
{
|
|
||||||
search = search.ToLower();
|
|
||||||
query = query.Where(c =>
|
|
||||||
c.RagioneSociale.ToLower().Contains(search) ||
|
|
||||||
(c.Citta != null && c.Citta.ToLower().Contains(search)) ||
|
|
||||||
(c.Email != null && c.Email.ToLower().Contains(search)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return await query
|
|
||||||
.OrderBy(c => c.RagioneSociale)
|
|
||||||
.Skip(offset)
|
|
||||||
.Take(limit)
|
|
||||||
.Select(c => new EntityListItemDto
|
|
||||||
{
|
|
||||||
Id = c.Id,
|
|
||||||
Label = c.RagioneSociale,
|
|
||||||
Description = $"{c.Citta ?? "N/D"} - {c.Telefono ?? "N/D"}",
|
|
||||||
SecondaryInfo = c.Email
|
|
||||||
})
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<int> CountClienti(string? search)
|
|
||||||
{
|
|
||||||
var query = _context.Clienti.Where(c => c.Attivo);
|
|
||||||
if (!string.IsNullOrWhiteSpace(search))
|
|
||||||
{
|
|
||||||
search = search.ToLower();
|
|
||||||
query = query.Where(c => c.RagioneSociale.ToLower().Contains(search));
|
|
||||||
}
|
|
||||||
return await query.CountAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<EntityListItemDto>> GetLocationEntities(string? search, int limit, int offset)
|
|
||||||
{
|
|
||||||
var query = _context.Location.Where(l => l.Attivo);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(search))
|
|
||||||
{
|
|
||||||
search = search.ToLower();
|
|
||||||
query = query.Where(l =>
|
|
||||||
l.Nome.ToLower().Contains(search) ||
|
|
||||||
(l.Citta != null && l.Citta.ToLower().Contains(search)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return await query
|
|
||||||
.OrderBy(l => l.Nome)
|
|
||||||
.Skip(offset)
|
|
||||||
.Take(limit)
|
|
||||||
.Select(l => new EntityListItemDto
|
|
||||||
{
|
|
||||||
Id = l.Id,
|
|
||||||
Label = l.Nome,
|
|
||||||
Description = $"{l.Citta ?? "N/D"} ({l.Provincia ?? "N/D"})",
|
|
||||||
SecondaryInfo = l.DistanzaKm.HasValue ? $"{l.DistanzaKm} km" : null
|
|
||||||
})
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<int> CountLocation(string? search)
|
|
||||||
{
|
|
||||||
var query = _context.Location.Where(l => l.Attivo);
|
|
||||||
if (!string.IsNullOrWhiteSpace(search))
|
|
||||||
{
|
|
||||||
search = search.ToLower();
|
|
||||||
query = query.Where(l => l.Nome.ToLower().Contains(search));
|
|
||||||
}
|
|
||||||
return await query.CountAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<EntityListItemDto>> GetArticoliEntities(string? search, int limit, int offset)
|
|
||||||
{
|
|
||||||
var query = _context.Articoli
|
|
||||||
.Include(a => a.Categoria)
|
|
||||||
.Where(a => a.Attivo);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(search))
|
|
||||||
{
|
|
||||||
search = search.ToLower();
|
|
||||||
query = query.Where(a =>
|
|
||||||
a.Codice.ToLower().Contains(search) ||
|
|
||||||
a.Descrizione.ToLower().Contains(search));
|
|
||||||
}
|
|
||||||
|
|
||||||
return await query
|
|
||||||
.OrderBy(a => a.Descrizione)
|
|
||||||
.Skip(offset)
|
|
||||||
.Take(limit)
|
|
||||||
.Select(a => new EntityListItemDto
|
|
||||||
{
|
|
||||||
Id = a.Id,
|
|
||||||
Label = $"{a.Codice} - {a.Descrizione}",
|
|
||||||
Description = $"Disponibile: {a.QtaDisponibile ?? 0}",
|
|
||||||
SecondaryInfo = a.Categoria != null ? a.Categoria.Descrizione : null
|
|
||||||
})
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<int> CountArticoli(string? search)
|
|
||||||
{
|
|
||||||
var query = _context.Articoli.Where(a => a.Attivo);
|
|
||||||
if (!string.IsNullOrWhiteSpace(search))
|
|
||||||
{
|
|
||||||
search = search.ToLower();
|
|
||||||
query = query.Where(a =>
|
|
||||||
a.Codice.ToLower().Contains(search) ||
|
|
||||||
a.Descrizione.ToLower().Contains(search));
|
|
||||||
}
|
|
||||||
return await query.CountAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<EntityListItemDto>> GetRisorseEntities(string? search, int limit, int offset)
|
|
||||||
{
|
|
||||||
var query = _context.Risorse
|
|
||||||
.Include(r => r.TipoRisorsa)
|
|
||||||
.Where(r => r.Attivo);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(search))
|
|
||||||
{
|
|
||||||
search = search.ToLower();
|
|
||||||
query = query.Where(r =>
|
|
||||||
r.Nome.ToLower().Contains(search) ||
|
|
||||||
(r.Cognome != null && r.Cognome.ToLower().Contains(search)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return await query
|
|
||||||
.OrderBy(r => r.Cognome).ThenBy(r => r.Nome)
|
|
||||||
.Skip(offset)
|
|
||||||
.Take(limit)
|
|
||||||
.Select(r => new EntityListItemDto
|
|
||||||
{
|
|
||||||
Id = r.Id,
|
|
||||||
Label = $"{r.Nome} {r.Cognome ?? ""}".Trim(),
|
|
||||||
Description = r.Telefono ?? "",
|
|
||||||
SecondaryInfo = r.TipoRisorsa != null ? r.TipoRisorsa.Descrizione : null
|
|
||||||
})
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<int> CountRisorse(string? search)
|
|
||||||
{
|
|
||||||
var query = _context.Risorse.Where(r => r.Attivo);
|
|
||||||
if (!string.IsNullOrWhiteSpace(search))
|
|
||||||
{
|
|
||||||
search = search.ToLower();
|
|
||||||
query = query.Where(r =>
|
|
||||||
r.Nome.ToLower().Contains(search) ||
|
|
||||||
(r.Cognome != null && r.Cognome.ToLower().Contains(search)));
|
|
||||||
}
|
|
||||||
return await query.CountAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<EntityListItemDto>> GetTipiEventoEntities(string? search, int limit, int offset)
|
|
||||||
{
|
|
||||||
var query = _context.TipiEvento
|
|
||||||
.Include(t => t.TipoPasto)
|
|
||||||
.Where(t => t.Attivo);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(search))
|
|
||||||
{
|
|
||||||
search = search.ToLower();
|
|
||||||
query = query.Where(t =>
|
|
||||||
t.Codice.ToLower().Contains(search) ||
|
|
||||||
t.Descrizione.ToLower().Contains(search));
|
|
||||||
}
|
|
||||||
|
|
||||||
return await query
|
|
||||||
.OrderBy(t => t.Descrizione)
|
|
||||||
.Skip(offset)
|
|
||||||
.Take(limit)
|
|
||||||
.Select(t => new EntityListItemDto
|
|
||||||
{
|
|
||||||
Id = t.Id,
|
|
||||||
Label = t.Descrizione,
|
|
||||||
Description = $"Codice: {t.Codice}",
|
|
||||||
SecondaryInfo = t.TipoPasto != null ? t.TipoPasto.Descrizione : null
|
|
||||||
})
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<EntityListItemDto>> GetTipiOspiteEntities(string? search, int limit, int offset)
|
|
||||||
{
|
|
||||||
var query = _context.TipiOspite.Where(t => t.Attivo);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(search))
|
|
||||||
{
|
|
||||||
search = search.ToLower();
|
|
||||||
query = query.Where(t =>
|
|
||||||
t.Codice.ToLower().Contains(search) ||
|
|
||||||
t.Descrizione.ToLower().Contains(search));
|
|
||||||
}
|
|
||||||
|
|
||||||
return await query
|
|
||||||
.OrderBy(t => t.Descrizione)
|
|
||||||
.Skip(offset)
|
|
||||||
.Take(limit)
|
|
||||||
.Select(t => new EntityListItemDto
|
|
||||||
{
|
|
||||||
Id = t.Id,
|
|
||||||
Label = t.Descrizione,
|
|
||||||
Description = $"Codice: {t.Codice}"
|
|
||||||
})
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<EntityListItemDto>> GetCategorieEntities(string? search, int limit, int offset)
|
|
||||||
{
|
|
||||||
var query = _context.CodiciCategoria.Where(c => c.Attivo);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(search))
|
|
||||||
{
|
|
||||||
search = search.ToLower();
|
|
||||||
query = query.Where(c =>
|
|
||||||
c.Codice.ToLower().Contains(search) ||
|
|
||||||
c.Descrizione.ToLower().Contains(search));
|
|
||||||
}
|
|
||||||
|
|
||||||
return await query
|
|
||||||
.OrderBy(c => c.Descrizione)
|
|
||||||
.Skip(offset)
|
|
||||||
.Take(limit)
|
|
||||||
.Select(c => new EntityListItemDto
|
|
||||||
{
|
|
||||||
Id = c.Id,
|
|
||||||
Label = c.Descrizione,
|
|
||||||
Description = $"Codice: {c.Codice}",
|
|
||||||
SecondaryInfo = $"Coeff: A={c.CoeffA:F2}, B={c.CoeffB:F2}, S={c.CoeffS:F2}"
|
|
||||||
})
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<EntityListItemDto>> GetTipiRisorsaEntities(string? search, int limit, int offset)
|
|
||||||
{
|
|
||||||
var query = _context.TipiRisorsa.Where(t => t.Attivo);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(search))
|
|
||||||
{
|
|
||||||
search = search.ToLower();
|
|
||||||
query = query.Where(t =>
|
|
||||||
t.Codice.ToLower().Contains(search) ||
|
|
||||||
t.Descrizione.ToLower().Contains(search));
|
|
||||||
}
|
|
||||||
|
|
||||||
return await query
|
|
||||||
.OrderBy(t => t.Descrizione)
|
|
||||||
.Skip(offset)
|
|
||||||
.Take(limit)
|
|
||||||
.Select(t => new EntityListItemDto
|
|
||||||
{
|
|
||||||
Id = t.Id,
|
|
||||||
Label = t.Descrizione,
|
|
||||||
Description = $"Codice: {t.Codice}"
|
|
||||||
})
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<EntityListItemDto>> GetTipiMaterialeEntities(string? search, int limit, int offset)
|
|
||||||
{
|
|
||||||
var query = _context.TipiMateriale.Where(t => t.Attivo);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(search))
|
|
||||||
{
|
|
||||||
search = search.ToLower();
|
|
||||||
query = query.Where(t =>
|
|
||||||
t.Codice.ToLower().Contains(search) ||
|
|
||||||
t.Descrizione.ToLower().Contains(search));
|
|
||||||
}
|
|
||||||
|
|
||||||
return await query
|
|
||||||
.OrderBy(t => t.Descrizione)
|
|
||||||
.Skip(offset)
|
|
||||||
.Take(limit)
|
|
||||||
.Select(t => new EntityListItemDto
|
|
||||||
{
|
|
||||||
Id = t.Id,
|
|
||||||
Label = t.Descrizione,
|
|
||||||
Description = $"Codice: {t.Codice}"
|
|
||||||
})
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
private async Task<Dictionary<string, object>> BuildDataContextAsync(List<DataSourceSelection> dataSources)
|
private async Task<Dictionary<string, object>> BuildDataContextAsync(List<DataSourceSelection> dataSources)
|
||||||
{
|
{
|
||||||
@@ -642,38 +265,7 @@ public class ReportsController : ControllerBase
|
|||||||
|
|
||||||
private async Task<object?> LoadEntityDataAsync(string datasetId, int entityId)
|
private async Task<object?> LoadEntityDataAsync(string datasetId, int entityId)
|
||||||
{
|
{
|
||||||
return datasetId.ToLower() switch
|
return await _schemaDiscovery.LoadEntity(datasetId, entityId);
|
||||||
{
|
|
||||||
"evento" => await _context.Eventi
|
|
||||||
.Include(e => e.Cliente)
|
|
||||||
.Include(e => e.Location)
|
|
||||||
.Include(e => e.TipoEvento).ThenInclude(t => t!.TipoPasto)
|
|
||||||
.Include(e => e.DettagliOspiti).ThenInclude(d => d.TipoOspite)
|
|
||||||
.Include(e => e.DettagliPrelievo).ThenInclude(d => d.Articolo).ThenInclude(a => a!.Categoria)
|
|
||||||
.Include(e => e.DettagliRisorse).ThenInclude(d => d.Risorsa).ThenInclude(r => r!.TipoRisorsa)
|
|
||||||
.Include(e => e.Acconti)
|
|
||||||
.Include(e => e.AltriCosti)
|
|
||||||
.Include(e => e.Degustazioni)
|
|
||||||
.FirstOrDefaultAsync(e => e.Id == entityId),
|
|
||||||
|
|
||||||
"cliente" => await _context.Clienti.FindAsync(entityId),
|
|
||||||
"location" => await _context.Location.FindAsync(entityId),
|
|
||||||
"articolo" => await _context.Articoli
|
|
||||||
.Include(a => a.Categoria)
|
|
||||||
.Include(a => a.TipoMateriale)
|
|
||||||
.FirstOrDefaultAsync(a => a.Id == entityId),
|
|
||||||
"risorsa" => await _context.Risorse
|
|
||||||
.Include(r => r.TipoRisorsa)
|
|
||||||
.FirstOrDefaultAsync(r => r.Id == entityId),
|
|
||||||
"tipoevento" => await _context.TipiEvento
|
|
||||||
.Include(t => t.TipoPasto)
|
|
||||||
.FirstOrDefaultAsync(t => t.Id == entityId),
|
|
||||||
"tipoospite" => await _context.TipiOspite.FindAsync(entityId),
|
|
||||||
"categoria" => await _context.CodiciCategoria.FindAsync(entityId),
|
|
||||||
"tiporisorsa" => await _context.TipiRisorsa.FindAsync(entityId),
|
|
||||||
"tipomateriale" => await _context.TipiMateriale.FindAsync(entityId),
|
|
||||||
_ => null
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Virtual Dataset Support
|
#region Virtual Dataset Support
|
||||||
@@ -706,7 +298,7 @@ public class ReportsController : ControllerBase
|
|||||||
if (source == null) continue;
|
if (source == null) continue;
|
||||||
|
|
||||||
// Ottieni lo schema del dataset sorgente per determinare il tipo
|
// Ottieni lo schema del dataset sorgente per determinare il tipo
|
||||||
var sourceSchema = GetSchemaForDataset(source.DatasetId);
|
var sourceSchema = _schemaDiscovery.GetSchema(source.DatasetId);
|
||||||
var sourceField = sourceSchema?.Fields.FirstOrDefault(f =>
|
var sourceField = sourceSchema?.Fields.FirstOrDefault(f =>
|
||||||
f.Name.Equals(outputField.FieldName, StringComparison.OrdinalIgnoreCase));
|
f.Name.Equals(outputField.FieldName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
@@ -724,7 +316,7 @@ public class ReportsController : ControllerBase
|
|||||||
// Se non ci sono OutputFields, includi tutti i campi di tutte le sorgenti
|
// Se non ci sono OutputFields, includi tutti i campi di tutte le sorgenti
|
||||||
foreach (var source in config.Sources)
|
foreach (var source in config.Sources)
|
||||||
{
|
{
|
||||||
var sourceSchema = GetSchemaForDataset(source.DatasetId);
|
var sourceSchema = _schemaDiscovery.GetSchema(source.DatasetId);
|
||||||
if (sourceSchema == null) continue;
|
if (sourceSchema == null) continue;
|
||||||
|
|
||||||
foreach (var field in sourceSchema.Fields)
|
foreach (var field in sourceSchema.Fields)
|
||||||
@@ -773,15 +365,7 @@ public class ReportsController : ControllerBase
|
|||||||
if (primarySource == null) return new List<EntityListItemDto>();
|
if (primarySource == null) return new List<EntityListItemDto>();
|
||||||
|
|
||||||
// Restituisce le entità del dataset primario
|
// Restituisce le entità del dataset primario
|
||||||
return primarySource.DatasetId.ToLower() switch
|
return await _schemaDiscovery.GetEntities(primarySource.DatasetId, search, limit, offset);
|
||||||
{
|
|
||||||
"evento" => await GetEventiEntities(search, limit, offset),
|
|
||||||
"cliente" => await GetClientiEntities(search, limit, offset),
|
|
||||||
"location" => await GetLocationEntities(search, limit, offset),
|
|
||||||
"articolo" => await GetArticoliEntities(search, limit, offset),
|
|
||||||
"risorsa" => await GetRisorseEntities(search, limit, offset),
|
|
||||||
_ => new List<EntityListItemDto>()
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -881,349 +465,7 @@ public class ReportsController : ControllerBase
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private static DataSchemaDto? GetSchemaForDataset(string datasetId)
|
|
||||||
{
|
|
||||||
return datasetId.ToLower() switch
|
|
||||||
{
|
|
||||||
"evento" => GetEventoSchema(),
|
|
||||||
"cliente" => GetClienteSchema(),
|
|
||||||
"location" => GetLocationSchema(),
|
|
||||||
"articolo" => GetArticoloSchema(),
|
|
||||||
"risorsa" => GetRisorsaSchema(),
|
|
||||||
"tipoevento" => GetTipoEventoSchema(),
|
|
||||||
"tipoospite" => GetTipoOspiteSchema(),
|
|
||||||
"categoria" => GetCategoriaSchema(),
|
|
||||||
"tiporisorsa" => GetTipoRisorsaSchema(),
|
|
||||||
"tipomateriale" => GetTipoMaterialeSchema(),
|
|
||||||
_ => null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Schema Definitions
|
|
||||||
|
|
||||||
private static DataSchemaDto GetEventoSchema() => new()
|
|
||||||
{
|
|
||||||
EntityType = "Evento",
|
|
||||||
DatasetId = "evento",
|
|
||||||
Fields = new List<DataFieldDto>
|
|
||||||
{
|
|
||||||
// Campi base
|
|
||||||
new() { Name = "id", Type = "number", Label = "ID", Group = "Base" },
|
|
||||||
new() { Name = "codice", Type = "string", Label = "Codice Evento", Group = "Base" },
|
|
||||||
new() { Name = "dataEvento", Type = "date", Label = "Data Evento", Group = "Base" },
|
|
||||||
new() { Name = "oraInizio", Type = "time", Label = "Ora Inizio", Group = "Base" },
|
|
||||||
new() { Name = "oraFine", Type = "time", Label = "Ora Fine", Group = "Base" },
|
|
||||||
new() { Name = "descrizione", Type = "string", Label = "Descrizione", Group = "Base" },
|
|
||||||
new() { Name = "stato", Type = "number", Label = "Stato (0=Scheda, 10=Preventivo, 20=Confermato)", Group = "Base" },
|
|
||||||
new() { Name = "confermato", Type = "boolean", Label = "Confermato", Group = "Base" },
|
|
||||||
|
|
||||||
// Ospiti
|
|
||||||
new() { Name = "numeroOspiti", Type = "number", Label = "Numero Ospiti Totale", Group = "Ospiti" },
|
|
||||||
new() { Name = "numeroOspitiAdulti", Type = "number", Label = "Ospiti Adulti", Group = "Ospiti" },
|
|
||||||
new() { Name = "numeroOspitiBambini", Type = "number", Label = "Ospiti Bambini", Group = "Ospiti" },
|
|
||||||
new() { Name = "numeroOspitiSeduti", Type = "number", Label = "Ospiti Seduti", Group = "Ospiti" },
|
|
||||||
new() { Name = "numeroOspitiBuffet", Type = "number", Label = "Ospiti Buffet", Group = "Ospiti" },
|
|
||||||
|
|
||||||
// Economici
|
|
||||||
new() { Name = "costoTotale", Type = "currency", Label = "Costo Totale", Group = "Economici" },
|
|
||||||
new() { Name = "costoPersona", Type = "currency", Label = "Costo per Persona", Group = "Economici" },
|
|
||||||
new() { Name = "totaleAcconti", Type = "currency", Label = "Totale Acconti", Group = "Economici" },
|
|
||||||
new() { Name = "saldo", Type = "currency", Label = "Saldo da Pagare", Group = "Economici" },
|
|
||||||
new() { Name = "dataScadenzaPreventivo", Type = "date", Label = "Scadenza Preventivo", Group = "Economici" },
|
|
||||||
|
|
||||||
// Note
|
|
||||||
new() { Name = "noteCliente", Type = "string", Label = "Note Cliente", Group = "Note" },
|
|
||||||
new() { Name = "noteInterne", Type = "string", Label = "Note Interne", Group = "Note" },
|
|
||||||
new() { Name = "noteCucina", Type = "string", Label = "Note Cucina", Group = "Note" },
|
|
||||||
new() { Name = "noteAllestimento", Type = "string", Label = "Note Allestimento", Group = "Note" },
|
|
||||||
|
|
||||||
// Cliente (relazione)
|
|
||||||
new() { Name = "cliente.id", Type = "number", Label = "ID Cliente", Group = "Cliente" },
|
|
||||||
new() { Name = "cliente.ragioneSociale", Type = "string", Label = "Ragione Sociale", Group = "Cliente" },
|
|
||||||
new() { Name = "cliente.indirizzo", Type = "string", Label = "Indirizzo Cliente", Group = "Cliente" },
|
|
||||||
new() { Name = "cliente.cap", Type = "string", Label = "CAP Cliente", Group = "Cliente" },
|
|
||||||
new() { Name = "cliente.citta", Type = "string", Label = "Città Cliente", Group = "Cliente" },
|
|
||||||
new() { Name = "cliente.provincia", Type = "string", Label = "Provincia Cliente", Group = "Cliente" },
|
|
||||||
new() { Name = "cliente.telefono", Type = "string", Label = "Telefono Cliente", Group = "Cliente" },
|
|
||||||
new() { Name = "cliente.email", Type = "string", Label = "Email Cliente", Group = "Cliente" },
|
|
||||||
new() { Name = "cliente.pec", Type = "string", Label = "PEC Cliente", Group = "Cliente" },
|
|
||||||
new() { Name = "cliente.codiceFiscale", Type = "string", Label = "Codice Fiscale", Group = "Cliente" },
|
|
||||||
new() { Name = "cliente.partitaIva", Type = "string", Label = "Partita IVA", Group = "Cliente" },
|
|
||||||
new() { Name = "cliente.codiceDestinatario", Type = "string", Label = "Codice SDI", Group = "Cliente" },
|
|
||||||
|
|
||||||
// Location (relazione)
|
|
||||||
new() { Name = "location.id", Type = "number", Label = "ID Location", Group = "Location" },
|
|
||||||
new() { Name = "location.nome", Type = "string", Label = "Nome Location", Group = "Location" },
|
|
||||||
new() { Name = "location.indirizzo", Type = "string", Label = "Indirizzo Location", Group = "Location" },
|
|
||||||
new() { Name = "location.cap", Type = "string", Label = "CAP Location", Group = "Location" },
|
|
||||||
new() { Name = "location.citta", Type = "string", Label = "Città Location", Group = "Location" },
|
|
||||||
new() { Name = "location.provincia", Type = "string", Label = "Provincia Location", Group = "Location" },
|
|
||||||
new() { Name = "location.telefono", Type = "string", Label = "Telefono Location", Group = "Location" },
|
|
||||||
new() { Name = "location.email", Type = "string", Label = "Email Location", Group = "Location" },
|
|
||||||
new() { Name = "location.referente", Type = "string", Label = "Referente Location", Group = "Location" },
|
|
||||||
new() { Name = "location.distanzaKm", Type = "number", Label = "Distanza (km)", Group = "Location" },
|
|
||||||
|
|
||||||
// Tipo Evento (relazione)
|
|
||||||
new() { Name = "tipoEvento.codice", Type = "string", Label = "Codice Tipo Evento", Group = "Tipo Evento" },
|
|
||||||
new() { Name = "tipoEvento.descrizione", Type = "string", Label = "Tipo Evento", Group = "Tipo Evento" },
|
|
||||||
new() { Name = "tipoEvento.tipoPasto.descrizione", Type = "string", Label = "Tipo Pasto", Group = "Tipo Evento" },
|
|
||||||
},
|
|
||||||
ChildCollections = new List<DataCollectionDto>
|
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Name = "dettagliOspiti",
|
|
||||||
Label = "Dettaglio Ospiti",
|
|
||||||
Description = "Breakdown ospiti per tipologia",
|
|
||||||
Fields = new List<DataFieldDto>
|
|
||||||
{
|
|
||||||
new() { Name = "tipoOspite.codice", Type = "string", Label = "Codice Tipo" },
|
|
||||||
new() { Name = "tipoOspite.descrizione", Type = "string", Label = "Tipo Ospite" },
|
|
||||||
new() { Name = "numero", Type = "number", Label = "Numero" },
|
|
||||||
new() { Name = "costoUnitario", Type = "currency", Label = "Costo Unitario" },
|
|
||||||
new() { Name = "sconto", Type = "percent", Label = "Sconto %" },
|
|
||||||
new() { Name = "totale", Type = "currency", Label = "Totale" },
|
|
||||||
new() { Name = "note", Type = "string", Label = "Note" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Name = "altriCosti",
|
|
||||||
Label = "Altri Costi",
|
|
||||||
Description = "Costi aggiuntivi dell'evento",
|
|
||||||
Fields = new List<DataFieldDto>
|
|
||||||
{
|
|
||||||
new() { Name = "descrizione", Type = "string", Label = "Descrizione" },
|
|
||||||
new() { Name = "costoUnitario", Type = "currency", Label = "Costo Unitario" },
|
|
||||||
new() { Name = "quantita", Type = "number", Label = "Quantità" },
|
|
||||||
new() { Name = "aliquotaIva", Type = "percent", Label = "Aliquota IVA" },
|
|
||||||
new() { Name = "totale", Type = "currency", Label = "Totale" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Name = "dettagliRisorse",
|
|
||||||
Label = "Risorse Assegnate",
|
|
||||||
Description = "Personale assegnato all'evento",
|
|
||||||
Fields = new List<DataFieldDto>
|
|
||||||
{
|
|
||||||
new() { Name = "risorsa.nome", Type = "string", Label = "Nome" },
|
|
||||||
new() { Name = "risorsa.cognome", Type = "string", Label = "Cognome" },
|
|
||||||
new() { Name = "risorsa.telefono", Type = "string", Label = "Telefono" },
|
|
||||||
new() { Name = "risorsa.tipoRisorsa.descrizione", Type = "string", Label = "Ruolo" },
|
|
||||||
new() { Name = "oraInizio", Type = "time", Label = "Ora Inizio" },
|
|
||||||
new() { Name = "oraFine", Type = "time", Label = "Ora Fine" },
|
|
||||||
new() { Name = "oreLavoro", Type = "number", Label = "Ore Lavoro" },
|
|
||||||
new() { Name = "costo", Type = "currency", Label = "Costo" },
|
|
||||||
new() { Name = "note", Type = "string", Label = "Note" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Name = "acconti",
|
|
||||||
Label = "Acconti",
|
|
||||||
Description = "Pagamenti anticipati ricevuti",
|
|
||||||
Fields = new List<DataFieldDto>
|
|
||||||
{
|
|
||||||
new() { Name = "descrizione", Type = "string", Label = "Descrizione" },
|
|
||||||
new() { Name = "importo", Type = "currency", Label = "Importo" },
|
|
||||||
new() { Name = "dataPagamento", Type = "date", Label = "Data Pagamento" },
|
|
||||||
new() { Name = "metodoPagamento", Type = "string", Label = "Metodo Pagamento" },
|
|
||||||
new() { Name = "note", Type = "string", Label = "Note" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Name = "dettagliPrelievo",
|
|
||||||
Label = "Lista Prelievo",
|
|
||||||
Description = "Articoli necessari per l'evento",
|
|
||||||
Fields = new List<DataFieldDto>
|
|
||||||
{
|
|
||||||
new() { Name = "articolo.codice", Type = "string", Label = "Codice Articolo" },
|
|
||||||
new() { Name = "articolo.descrizione", Type = "string", Label = "Descrizione" },
|
|
||||||
new() { Name = "articolo.categoria.descrizione", Type = "string", Label = "Categoria" },
|
|
||||||
new() { Name = "articolo.unitaMisura", Type = "string", Label = "U.M." },
|
|
||||||
new() { Name = "qtaRichiesta", Type = "number", Label = "Qtà Richiesta" },
|
|
||||||
new() { Name = "qtaCalcolata", Type = "number", Label = "Qtà Calcolata" },
|
|
||||||
new() { Name = "qtaEffettiva", Type = "number", Label = "Qtà Effettiva" },
|
|
||||||
new() { Name = "note", Type = "string", Label = "Note" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Name = "degustazioni",
|
|
||||||
Label = "Degustazioni",
|
|
||||||
Description = "Degustazioni programmate",
|
|
||||||
Fields = new List<DataFieldDto>
|
|
||||||
{
|
|
||||||
new() { Name = "data", Type = "date", Label = "Data" },
|
|
||||||
new() { Name = "ora", Type = "time", Label = "Ora" },
|
|
||||||
new() { Name = "numeroPartecipanti", Type = "number", Label = "Partecipanti" },
|
|
||||||
new() { Name = "note", Type = "string", Label = "Note" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private static DataSchemaDto GetClienteSchema() => new()
|
|
||||||
{
|
|
||||||
EntityType = "Cliente",
|
|
||||||
DatasetId = "cliente",
|
|
||||||
Fields = new List<DataFieldDto>
|
|
||||||
{
|
|
||||||
new() { Name = "id", Type = "number", Label = "ID", Group = "Base" },
|
|
||||||
new() { Name = "ragioneSociale", Type = "string", Label = "Ragione Sociale", Group = "Base" },
|
|
||||||
new() { Name = "indirizzo", Type = "string", Label = "Indirizzo", Group = "Indirizzo" },
|
|
||||||
new() { Name = "cap", Type = "string", Label = "CAP", Group = "Indirizzo" },
|
|
||||||
new() { Name = "citta", Type = "string", Label = "Città", Group = "Indirizzo" },
|
|
||||||
new() { Name = "provincia", Type = "string", Label = "Provincia", Group = "Indirizzo" },
|
|
||||||
new() { Name = "telefono", Type = "string", Label = "Telefono", Group = "Contatti" },
|
|
||||||
new() { Name = "email", Type = "string", Label = "Email", Group = "Contatti" },
|
|
||||||
new() { Name = "pec", Type = "string", Label = "PEC", Group = "Contatti" },
|
|
||||||
new() { Name = "codiceFiscale", Type = "string", Label = "Codice Fiscale", Group = "Fiscale" },
|
|
||||||
new() { Name = "partitaIva", Type = "string", Label = "Partita IVA", Group = "Fiscale" },
|
|
||||||
new() { Name = "codiceDestinatario", Type = "string", Label = "Codice Destinatario SDI", Group = "Fiscale" },
|
|
||||||
new() { Name = "note", Type = "string", Label = "Note", Group = "Base" },
|
|
||||||
},
|
|
||||||
ChildCollections = new List<DataCollectionDto>()
|
|
||||||
};
|
|
||||||
|
|
||||||
private static DataSchemaDto GetLocationSchema() => new()
|
|
||||||
{
|
|
||||||
EntityType = "Location",
|
|
||||||
DatasetId = "location",
|
|
||||||
Fields = new List<DataFieldDto>
|
|
||||||
{
|
|
||||||
new() { Name = "id", Type = "number", Label = "ID", Group = "Base" },
|
|
||||||
new() { Name = "nome", Type = "string", Label = "Nome", Group = "Base" },
|
|
||||||
new() { Name = "indirizzo", Type = "string", Label = "Indirizzo", Group = "Indirizzo" },
|
|
||||||
new() { Name = "cap", Type = "string", Label = "CAP", Group = "Indirizzo" },
|
|
||||||
new() { Name = "citta", Type = "string", Label = "Città", Group = "Indirizzo" },
|
|
||||||
new() { Name = "provincia", Type = "string", Label = "Provincia", Group = "Indirizzo" },
|
|
||||||
new() { Name = "telefono", Type = "string", Label = "Telefono", Group = "Contatti" },
|
|
||||||
new() { Name = "email", Type = "string", Label = "Email", Group = "Contatti" },
|
|
||||||
new() { Name = "referente", Type = "string", Label = "Referente", Group = "Contatti" },
|
|
||||||
new() { Name = "distanzaKm", Type = "number", Label = "Distanza (km)", Group = "Base" },
|
|
||||||
new() { Name = "note", Type = "string", Label = "Note", Group = "Base" },
|
|
||||||
},
|
|
||||||
ChildCollections = new List<DataCollectionDto>()
|
|
||||||
};
|
|
||||||
|
|
||||||
private static DataSchemaDto GetArticoloSchema() => new()
|
|
||||||
{
|
|
||||||
EntityType = "Articolo",
|
|
||||||
DatasetId = "articolo",
|
|
||||||
Fields = new List<DataFieldDto>
|
|
||||||
{
|
|
||||||
new() { Name = "id", Type = "number", Label = "ID", Group = "Base" },
|
|
||||||
new() { Name = "codice", Type = "string", Label = "Codice", Group = "Base" },
|
|
||||||
new() { Name = "descrizione", Type = "string", Label = "Descrizione", Group = "Base" },
|
|
||||||
new() { Name = "unitaMisura", Type = "string", Label = "Unità di Misura", Group = "Base" },
|
|
||||||
new() { Name = "qtaDisponibile", Type = "number", Label = "Quantità Disponibile", Group = "Quantità" },
|
|
||||||
new() { Name = "qtaStdA", Type = "number", Label = "Qtà Standard Adulti", Group = "Quantità" },
|
|
||||||
new() { Name = "qtaStdB", Type = "number", Label = "Qtà Standard Buffet", Group = "Quantità" },
|
|
||||||
new() { Name = "qtaStdS", Type = "number", Label = "Qtà Standard Seduti", Group = "Quantità" },
|
|
||||||
new() { Name = "categoria.codice", Type = "string", Label = "Codice Categoria", Group = "Categoria" },
|
|
||||||
new() { Name = "categoria.descrizione", Type = "string", Label = "Categoria", Group = "Categoria" },
|
|
||||||
new() { Name = "categoria.coeffA", Type = "number", Label = "Coefficiente Adulti", Group = "Categoria" },
|
|
||||||
new() { Name = "categoria.coeffB", Type = "number", Label = "Coefficiente Buffet", Group = "Categoria" },
|
|
||||||
new() { Name = "categoria.coeffS", Type = "number", Label = "Coefficiente Seduti", Group = "Categoria" },
|
|
||||||
new() { Name = "tipoMateriale.codice", Type = "string", Label = "Codice Tipo Materiale", Group = "Materiale" },
|
|
||||||
new() { Name = "tipoMateriale.descrizione", Type = "string", Label = "Tipo Materiale", Group = "Materiale" },
|
|
||||||
new() { Name = "note", Type = "string", Label = "Note", Group = "Base" },
|
|
||||||
},
|
|
||||||
ChildCollections = new List<DataCollectionDto>()
|
|
||||||
};
|
|
||||||
|
|
||||||
private static DataSchemaDto GetRisorsaSchema() => new()
|
|
||||||
{
|
|
||||||
EntityType = "Risorsa",
|
|
||||||
DatasetId = "risorsa",
|
|
||||||
Fields = new List<DataFieldDto>
|
|
||||||
{
|
|
||||||
new() { Name = "id", Type = "number", Label = "ID", Group = "Base" },
|
|
||||||
new() { Name = "nome", Type = "string", Label = "Nome", Group = "Base" },
|
|
||||||
new() { Name = "cognome", Type = "string", Label = "Cognome", Group = "Base" },
|
|
||||||
new() { Name = "telefono", Type = "string", Label = "Telefono", Group = "Contatti" },
|
|
||||||
new() { Name = "email", Type = "string", Label = "Email", Group = "Contatti" },
|
|
||||||
new() { Name = "tipoRisorsa.codice", Type = "string", Label = "Codice Tipo", Group = "Tipo" },
|
|
||||||
new() { Name = "tipoRisorsa.descrizione", Type = "string", Label = "Tipo Risorsa", Group = "Tipo" },
|
|
||||||
new() { Name = "note", Type = "string", Label = "Note", Group = "Base" },
|
|
||||||
},
|
|
||||||
ChildCollections = new List<DataCollectionDto>()
|
|
||||||
};
|
|
||||||
|
|
||||||
private static DataSchemaDto GetTipoEventoSchema() => new()
|
|
||||||
{
|
|
||||||
EntityType = "Tipo Evento",
|
|
||||||
DatasetId = "tipoEvento",
|
|
||||||
Fields = new List<DataFieldDto>
|
|
||||||
{
|
|
||||||
new() { Name = "id", Type = "number", Label = "ID" },
|
|
||||||
new() { Name = "codice", Type = "string", Label = "Codice" },
|
|
||||||
new() { Name = "descrizione", Type = "string", Label = "Descrizione" },
|
|
||||||
new() { Name = "tipoPasto.codice", Type = "string", Label = "Codice Tipo Pasto" },
|
|
||||||
new() { Name = "tipoPasto.descrizione", Type = "string", Label = "Tipo Pasto" },
|
|
||||||
},
|
|
||||||
ChildCollections = new List<DataCollectionDto>()
|
|
||||||
};
|
|
||||||
|
|
||||||
private static DataSchemaDto GetTipoOspiteSchema() => new()
|
|
||||||
{
|
|
||||||
EntityType = "Tipo Ospite",
|
|
||||||
DatasetId = "tipoOspite",
|
|
||||||
Fields = new List<DataFieldDto>
|
|
||||||
{
|
|
||||||
new() { Name = "id", Type = "number", Label = "ID" },
|
|
||||||
new() { Name = "codice", Type = "string", Label = "Codice" },
|
|
||||||
new() { Name = "descrizione", Type = "string", Label = "Descrizione" },
|
|
||||||
},
|
|
||||||
ChildCollections = new List<DataCollectionDto>()
|
|
||||||
};
|
|
||||||
|
|
||||||
private static DataSchemaDto GetCategoriaSchema() => new()
|
|
||||||
{
|
|
||||||
EntityType = "Categoria Articoli",
|
|
||||||
DatasetId = "categoria",
|
|
||||||
Fields = new List<DataFieldDto>
|
|
||||||
{
|
|
||||||
new() { Name = "id", Type = "number", Label = "ID" },
|
|
||||||
new() { Name = "codice", Type = "string", Label = "Codice" },
|
|
||||||
new() { Name = "descrizione", Type = "string", Label = "Descrizione" },
|
|
||||||
new() { Name = "coeffA", Type = "number", Label = "Coefficiente Adulti" },
|
|
||||||
new() { Name = "coeffB", Type = "number", Label = "Coefficiente Buffet" },
|
|
||||||
new() { Name = "coeffS", Type = "number", Label = "Coefficiente Seduti" },
|
|
||||||
},
|
|
||||||
ChildCollections = new List<DataCollectionDto>()
|
|
||||||
};
|
|
||||||
|
|
||||||
private static DataSchemaDto GetTipoRisorsaSchema() => new()
|
|
||||||
{
|
|
||||||
EntityType = "Tipo Risorsa",
|
|
||||||
DatasetId = "tipoRisorsa",
|
|
||||||
Fields = new List<DataFieldDto>
|
|
||||||
{
|
|
||||||
new() { Name = "id", Type = "number", Label = "ID" },
|
|
||||||
new() { Name = "codice", Type = "string", Label = "Codice" },
|
|
||||||
new() { Name = "descrizione", Type = "string", Label = "Descrizione" },
|
|
||||||
},
|
|
||||||
ChildCollections = new List<DataCollectionDto>()
|
|
||||||
};
|
|
||||||
|
|
||||||
private static DataSchemaDto GetTipoMaterialeSchema() => new()
|
|
||||||
{
|
|
||||||
EntityType = "Tipo Materiale",
|
|
||||||
DatasetId = "tipoMateriale",
|
|
||||||
Fields = new List<DataFieldDto>
|
|
||||||
{
|
|
||||||
new() { Name = "id", Type = "number", Label = "ID" },
|
|
||||||
new() { Name = "codice", Type = "string", Label = "Codice" },
|
|
||||||
new() { Name = "descrizione", Type = "string", Label = "Descrizione" },
|
|
||||||
},
|
|
||||||
ChildCollections = new List<DataCollectionDto>()
|
|
||||||
};
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DTOs moved to AprtModels.cs
|
// DTOs moved to AprtModels.cs
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ namespace Zentral.API.Controllers;
|
|||||||
public class VirtualDatasetsController : ControllerBase
|
public class VirtualDatasetsController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly ZentralDbContext _context;
|
private readonly ZentralDbContext _context;
|
||||||
|
private readonly SchemaDiscoveryService _schemaDiscovery;
|
||||||
|
|
||||||
public VirtualDatasetsController(ZentralDbContext context)
|
public VirtualDatasetsController(ZentralDbContext context, SchemaDiscoveryService schemaDiscovery)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_schemaDiscovery = schemaDiscovery;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -365,11 +367,19 @@ public class VirtualDatasetsController : ControllerBase
|
|||||||
var source = config.Sources.FirstOrDefault(s => s.Id == outputField.SourceId);
|
var source = config.Sources.FirstOrDefault(s => s.Id == outputField.SourceId);
|
||||||
if (source == null) continue;
|
if (source == null) continue;
|
||||||
|
|
||||||
|
var sourceSchema = _schemaDiscovery.GetSchema(source.DatasetId);
|
||||||
|
var fieldType = "string";
|
||||||
|
if (sourceSchema != null)
|
||||||
|
{
|
||||||
|
var sourceField = sourceSchema.Fields.FirstOrDefault(f => f.Name.Equals(outputField.FieldName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (sourceField != null) fieldType = sourceField.Type;
|
||||||
|
}
|
||||||
|
|
||||||
fields.Add(new DataFieldDto
|
fields.Add(new DataFieldDto
|
||||||
{
|
{
|
||||||
Name = outputField.Alias ?? $"{source.Alias}.{outputField.FieldName}",
|
Name = outputField.Alias ?? $"{source.Alias}.{outputField.FieldName}",
|
||||||
Label = outputField.Label ?? outputField.FieldName,
|
Label = outputField.Label ?? outputField.FieldName,
|
||||||
Type = "string", // TODO: determinare il tipo dal dataset sorgente
|
Type = fieldType,
|
||||||
Group = outputField.Group ?? source.Alias
|
Group = outputField.Group ?? source.Alias
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -379,7 +389,7 @@ public class VirtualDatasetsController : ControllerBase
|
|||||||
// Altrimenti, includi tutti i campi da tutte le sorgenti
|
// Altrimenti, includi tutti i campi da tutte le sorgenti
|
||||||
foreach (var source in config.Sources)
|
foreach (var source in config.Sources)
|
||||||
{
|
{
|
||||||
var sourceSchema = GetBaseDatasetSchema(source.DatasetId);
|
var sourceSchema = _schemaDiscovery.GetSchema(source.DatasetId);
|
||||||
if (sourceSchema == null) continue;
|
if (sourceSchema == null) continue;
|
||||||
|
|
||||||
foreach (var field in sourceSchema.Fields)
|
foreach (var field in sourceSchema.Fields)
|
||||||
@@ -427,91 +437,7 @@ public class VirtualDatasetsController : ControllerBase
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private DataSchemaDto? GetBaseDatasetSchema(string datasetId)
|
|
||||||
{
|
|
||||||
// Riutilizza la logica di ReportsController per ottenere lo schema base
|
|
||||||
// TODO: estrarre in un servizio condiviso
|
|
||||||
return datasetId.ToLower() switch
|
|
||||||
{
|
|
||||||
"evento" => new DataSchemaDto
|
|
||||||
{
|
|
||||||
EntityType = "Evento",
|
|
||||||
DatasetId = "evento",
|
|
||||||
Fields = new List<DataFieldDto>
|
|
||||||
{
|
|
||||||
new() { Name = "id", Type = "number", Label = "ID" },
|
|
||||||
new() { Name = "codice", Type = "string", Label = "Codice" },
|
|
||||||
new() { Name = "dataEvento", Type = "date", Label = "Data Evento" },
|
|
||||||
new() { Name = "descrizione", Type = "string", Label = "Descrizione" },
|
|
||||||
new() { Name = "stato", Type = "number", Label = "Stato" },
|
|
||||||
new() { Name = "numeroOspiti", Type = "number", Label = "Numero Ospiti" },
|
|
||||||
new() { Name = "costoTotale", Type = "currency", Label = "Costo Totale" },
|
|
||||||
new() { Name = "clienteId", Type = "number", Label = "ID Cliente" },
|
|
||||||
new() { Name = "locationId", Type = "number", Label = "ID Location" },
|
|
||||||
},
|
|
||||||
ChildCollections = new List<DataCollectionDto>()
|
|
||||||
},
|
|
||||||
"cliente" => new DataSchemaDto
|
|
||||||
{
|
|
||||||
EntityType = "Cliente",
|
|
||||||
DatasetId = "cliente",
|
|
||||||
Fields = new List<DataFieldDto>
|
|
||||||
{
|
|
||||||
new() { Name = "id", Type = "number", Label = "ID" },
|
|
||||||
new() { Name = "ragioneSociale", Type = "string", Label = "Ragione Sociale" },
|
|
||||||
new() { Name = "indirizzo", Type = "string", Label = "Indirizzo" },
|
|
||||||
new() { Name = "citta", Type = "string", Label = "Città" },
|
|
||||||
new() { Name = "telefono", Type = "string", Label = "Telefono" },
|
|
||||||
new() { Name = "email", Type = "string", Label = "Email" },
|
|
||||||
new() { Name = "partitaIva", Type = "string", Label = "Partita IVA" },
|
|
||||||
},
|
|
||||||
ChildCollections = new List<DataCollectionDto>()
|
|
||||||
},
|
|
||||||
"location" => new DataSchemaDto
|
|
||||||
{
|
|
||||||
EntityType = "Location",
|
|
||||||
DatasetId = "location",
|
|
||||||
Fields = new List<DataFieldDto>
|
|
||||||
{
|
|
||||||
new() { Name = "id", Type = "number", Label = "ID" },
|
|
||||||
new() { Name = "nome", Type = "string", Label = "Nome" },
|
|
||||||
new() { Name = "indirizzo", Type = "string", Label = "Indirizzo" },
|
|
||||||
new() { Name = "citta", Type = "string", Label = "Città" },
|
|
||||||
new() { Name = "distanzaKm", Type = "number", Label = "Distanza (km)" },
|
|
||||||
},
|
|
||||||
ChildCollections = new List<DataCollectionDto>()
|
|
||||||
},
|
|
||||||
"articolo" => new DataSchemaDto
|
|
||||||
{
|
|
||||||
EntityType = "Articolo",
|
|
||||||
DatasetId = "articolo",
|
|
||||||
Fields = new List<DataFieldDto>
|
|
||||||
{
|
|
||||||
new() { Name = "id", Type = "number", Label = "ID" },
|
|
||||||
new() { Name = "codice", Type = "string", Label = "Codice" },
|
|
||||||
new() { Name = "descrizione", Type = "string", Label = "Descrizione" },
|
|
||||||
new() { Name = "qtaDisponibile", Type = "number", Label = "Qtà Disponibile" },
|
|
||||||
new() { Name = "categoriaId", Type = "number", Label = "ID Categoria" },
|
|
||||||
},
|
|
||||||
ChildCollections = new List<DataCollectionDto>()
|
|
||||||
},
|
|
||||||
"risorsa" => new DataSchemaDto
|
|
||||||
{
|
|
||||||
EntityType = "Risorsa",
|
|
||||||
DatasetId = "risorsa",
|
|
||||||
Fields = new List<DataFieldDto>
|
|
||||||
{
|
|
||||||
new() { Name = "id", Type = "number", Label = "ID" },
|
|
||||||
new() { Name = "nome", Type = "string", Label = "Nome" },
|
|
||||||
new() { Name = "cognome", Type = "string", Label = "Cognome" },
|
|
||||||
new() { Name = "telefono", Type = "string", Label = "Telefono" },
|
|
||||||
new() { Name = "tipoRisorsaId", Type = "number", Label = "ID Tipo Risorsa" },
|
|
||||||
},
|
|
||||||
ChildCollections = new List<DataCollectionDto>()
|
|
||||||
},
|
|
||||||
_ => null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DTOs
|
// DTOs
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ builder.Services.AddDbContext<ZentralDbContext>(options =>
|
|||||||
builder.Services.AddScoped<EventoCostiService>();
|
builder.Services.AddScoped<EventoCostiService>();
|
||||||
builder.Services.AddScoped<DemoDataService>();
|
builder.Services.AddScoped<DemoDataService>();
|
||||||
builder.Services.AddScoped<ReportGeneratorService>();
|
builder.Services.AddScoped<ReportGeneratorService>();
|
||||||
|
builder.Services.AddScoped<SchemaDiscoveryService>();
|
||||||
builder.Services.AddScoped<AppService>();
|
builder.Services.AddScoped<AppService>();
|
||||||
builder.Services.AddScoped<AutoCodeService>();
|
builder.Services.AddScoped<AutoCodeService>();
|
||||||
builder.Services.AddScoped<CustomFieldService>();
|
builder.Services.AddScoped<CustomFieldService>();
|
||||||
|
|||||||
@@ -0,0 +1,397 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Zentral.Domain.Entities;
|
||||||
|
using Zentral.Infrastructure.Data;
|
||||||
|
|
||||||
|
namespace Zentral.API.Services.Reports;
|
||||||
|
|
||||||
|
public class SchemaDiscoveryService
|
||||||
|
{
|
||||||
|
private readonly ZentralDbContext _context;
|
||||||
|
|
||||||
|
public SchemaDiscoveryService(ZentralDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Dictionary<string, (string Name, string Description, string Icon, string Category)> _datasetMetadata = new()
|
||||||
|
{
|
||||||
|
{ "Evento", ("Evento", "Dati evento completi con cliente, location, ospiti, costi e risorse", "event", "Principale") },
|
||||||
|
{ "Cliente", ("Cliente", "Anagrafica clienti completa", "people", "Principale") },
|
||||||
|
{ "Location", ("Location", "Sedi e location eventi", "place", "Principale") },
|
||||||
|
{ "Articolo", ("Articolo", "Catalogo articoli e materiali", "inventory", "Principale") },
|
||||||
|
{ "Risorsa", ("Risorsa", "Staff e personale", "person", "Principale") },
|
||||||
|
{ "TipoEvento", ("Tipo Evento", "Tipologie di evento (matrimonio, compleanno, etc.)", "category", "Configurazione") },
|
||||||
|
{ "TipoOspite", ("Tipo Ospite", "Tipologie di ospiti (adulti, bambini, etc.)", "groups", "Configurazione") },
|
||||||
|
{ "CodiceCategoria", ("Categoria Articoli", "Categorie articoli con coefficienti di calcolo", "folder", "Configurazione") },
|
||||||
|
{ "TipoRisorsa", ("Tipo Risorsa", "Tipologie di risorse (cameriere, cuoco, etc.)", "badge", "Configurazione") },
|
||||||
|
{ "TipoMateriale", ("Tipo Materiale", "Tipologie di materiali", "category", "Configurazione") }
|
||||||
|
};
|
||||||
|
|
||||||
|
public List<DatasetTypeDto> GetAvailableDatasets()
|
||||||
|
{
|
||||||
|
var datasets = new List<DatasetTypeDto>();
|
||||||
|
var properties = typeof(ZentralDbContext).GetProperties()
|
||||||
|
.Where(p => p.PropertyType.IsGenericType &&
|
||||||
|
p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>));
|
||||||
|
|
||||||
|
foreach (var prop in properties)
|
||||||
|
{
|
||||||
|
var entityType = prop.PropertyType.GetGenericArguments()[0];
|
||||||
|
|
||||||
|
// Skip join tables or non-entity types if necessary
|
||||||
|
if (entityType.Name.Contains("Dettaglio") || entityType.Name.Contains("Link"))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var datasetId = ToCamelCase(entityType.Name);
|
||||||
|
|
||||||
|
// Default values
|
||||||
|
var name = SplitCamelCase(entityType.Name);
|
||||||
|
var description = $"Dataset {entityType.Name}";
|
||||||
|
var icon = GetIconForType(entityType.Name);
|
||||||
|
var category = "Principale";
|
||||||
|
|
||||||
|
// Determine category from namespace if not in metadata
|
||||||
|
if (entityType.Namespace != null)
|
||||||
|
{
|
||||||
|
var parts = entityType.Namespace.Split('.');
|
||||||
|
if (parts.Length > 3 && parts[2] == "Entities")
|
||||||
|
{
|
||||||
|
category = parts[3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply metadata if available
|
||||||
|
if (_datasetMetadata.TryGetValue(entityType.Name, out var meta))
|
||||||
|
{
|
||||||
|
name = meta.Name;
|
||||||
|
description = meta.Description;
|
||||||
|
icon = meta.Icon;
|
||||||
|
category = meta.Category;
|
||||||
|
}
|
||||||
|
|
||||||
|
datasets.Add(new DatasetTypeDto
|
||||||
|
{
|
||||||
|
Id = datasetId,
|
||||||
|
Name = name,
|
||||||
|
Description = description,
|
||||||
|
Icon = icon,
|
||||||
|
Category = category,
|
||||||
|
IsVirtual = false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return datasets.OrderBy(d => d.Category).ThenBy(d => d.Name).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public DataSchemaDto? GetSchema(string datasetId)
|
||||||
|
{
|
||||||
|
var type = GetTypeForDataset(datasetId);
|
||||||
|
if (type == null) return null;
|
||||||
|
|
||||||
|
var fields = new List<DataFieldDto>();
|
||||||
|
var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||||
|
.Where(p => p.CanRead);
|
||||||
|
|
||||||
|
foreach (var prop in props)
|
||||||
|
{
|
||||||
|
// Skip collections for fields, handle them as child collections if needed
|
||||||
|
if (typeof(IEnumerable).IsAssignableFrom(prop.PropertyType) && prop.PropertyType != typeof(string))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Skip complex types that are not mapped as owned types (simplification)
|
||||||
|
if (!IsSimpleType(prop.PropertyType))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
fields.Add(new DataFieldDto
|
||||||
|
{
|
||||||
|
Name = ToCamelCase(prop.Name),
|
||||||
|
Label = SplitCamelCase(prop.Name),
|
||||||
|
Type = MapType(prop.PropertyType),
|
||||||
|
Group = "Fields"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DataSchemaDto
|
||||||
|
{
|
||||||
|
EntityType = type.Name,
|
||||||
|
DatasetId = datasetId,
|
||||||
|
Fields = fields,
|
||||||
|
ChildCollections = new List<DataCollectionDto>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<EntityListItemDto>> GetEntities(string datasetId, string? search, int limit, int offset)
|
||||||
|
{
|
||||||
|
var type = GetTypeForDataset(datasetId);
|
||||||
|
if (type == null) return new List<EntityListItemDto>();
|
||||||
|
|
||||||
|
var method = this.GetType().GetMethod("GetEntitiesGeneric", BindingFlags.NonPublic | BindingFlags.Instance)!
|
||||||
|
.MakeGenericMethod(type);
|
||||||
|
|
||||||
|
var task = (Task<List<EntityListItemDto>>)method.Invoke(this, new object[] { search, limit, offset })!;
|
||||||
|
return await task;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> CountEntities(string datasetId, string? search)
|
||||||
|
{
|
||||||
|
var type = GetTypeForDataset(datasetId);
|
||||||
|
if (type == null) return 0;
|
||||||
|
|
||||||
|
var method = this.GetType().GetMethod("CountEntitiesGeneric", BindingFlags.NonPublic | BindingFlags.Instance)!
|
||||||
|
.MakeGenericMethod(type);
|
||||||
|
|
||||||
|
var task = (Task<int>)method.Invoke(this, new object[] { search })!;
|
||||||
|
return await task;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object?> LoadEntity(string datasetId, int id)
|
||||||
|
{
|
||||||
|
var type = GetTypeForDataset(datasetId);
|
||||||
|
if (type == null) return null;
|
||||||
|
|
||||||
|
var method = this.GetType().GetMethod("LoadEntityGeneric", BindingFlags.NonPublic | BindingFlags.Instance)!
|
||||||
|
.MakeGenericMethod(type);
|
||||||
|
|
||||||
|
var task = (Task<object?>)method.Invoke(this, new object[] { id })!;
|
||||||
|
return await task;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic implementations
|
||||||
|
private async Task<object?> LoadEntityGeneric<T>(int id) where T : class
|
||||||
|
{
|
||||||
|
var query = _context.Set<T>().AsQueryable();
|
||||||
|
|
||||||
|
// Eager load all navigation properties
|
||||||
|
var entityType = _context.Model.FindEntityType(typeof(T));
|
||||||
|
if (entityType != null)
|
||||||
|
{
|
||||||
|
foreach (var nav in entityType.GetNavigations())
|
||||||
|
{
|
||||||
|
query = query.Include(nav.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await query.FirstOrDefaultAsync(e => EF.Property<int>(e, "Id") == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<EntityListItemDto>> GetEntitiesGeneric<T>(string? search, int limit, int offset) where T : class
|
||||||
|
{
|
||||||
|
var query = _context.Set<T>().AsQueryable();
|
||||||
|
|
||||||
|
// Apply search if possible
|
||||||
|
if (!string.IsNullOrWhiteSpace(search))
|
||||||
|
{
|
||||||
|
var predicate = BuildSearchPredicate<T>(search);
|
||||||
|
if (predicate != null)
|
||||||
|
{
|
||||||
|
query = query.Where(predicate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order by Label property if available (Alphabetical), otherwise by Id Descending
|
||||||
|
var labelProp = GetLabelProperty(typeof(T));
|
||||||
|
if (labelProp != null)
|
||||||
|
{
|
||||||
|
query = query.OrderBy(e => EF.Property<string>(e, labelProp.Name));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var idProp = typeof(T).GetProperty("Id");
|
||||||
|
if (idProp != null)
|
||||||
|
{
|
||||||
|
query = query.OrderByDescending(e => EF.Property<object>(e, "Id"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var list = await query.Skip(offset).Take(limit).ToListAsync();
|
||||||
|
return list.Select(item => MapToListItem(item)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<int> CountEntitiesGeneric<T>(string? search) where T : class
|
||||||
|
{
|
||||||
|
var query = _context.Set<T>().AsQueryable();
|
||||||
|
if (!string.IsNullOrWhiteSpace(search))
|
||||||
|
{
|
||||||
|
var predicate = BuildSearchPredicate<T>(search);
|
||||||
|
if (predicate != null)
|
||||||
|
{
|
||||||
|
query = query.Where(predicate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return await query.CountAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
private Type? GetTypeForDataset(string datasetId)
|
||||||
|
{
|
||||||
|
// Case insensitive match
|
||||||
|
var properties = typeof(ZentralDbContext).GetProperties()
|
||||||
|
.Where(p => p.PropertyType.IsGenericType &&
|
||||||
|
p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>));
|
||||||
|
|
||||||
|
foreach (var prop in properties)
|
||||||
|
{
|
||||||
|
var entityType = prop.PropertyType.GetGenericArguments()[0];
|
||||||
|
if (entityType.Name.Equals(datasetId, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return entityType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Expression<Func<T, bool>>? BuildSearchPredicate<T>(string search)
|
||||||
|
{
|
||||||
|
var parameter = Expression.Parameter(typeof(T), "e");
|
||||||
|
var searchLower = Expression.Constant(search.ToLower());
|
||||||
|
|
||||||
|
var stringProps = typeof(T).GetProperties()
|
||||||
|
.Where(p => p.PropertyType == typeof(string) && p.CanRead)
|
||||||
|
.Take(3) // Limit to first 3 string properties to avoid huge queries
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (!stringProps.Any()) return null;
|
||||||
|
|
||||||
|
Expression? body = null;
|
||||||
|
var containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) });
|
||||||
|
var toLowerMethod = typeof(string).GetMethod("ToLower", Type.EmptyTypes);
|
||||||
|
|
||||||
|
foreach (var prop in stringProps)
|
||||||
|
{
|
||||||
|
// e.Prop
|
||||||
|
var propExp = Expression.Property(parameter, prop);
|
||||||
|
// e.Prop != null
|
||||||
|
var notNull = Expression.NotEqual(propExp, Expression.Constant(null));
|
||||||
|
// e.Prop.ToLower()
|
||||||
|
var toLower = Expression.Call(propExp, toLowerMethod!);
|
||||||
|
// e.Prop.ToLower().Contains(search)
|
||||||
|
var contains = Expression.Call(toLower, containsMethod!, searchLower);
|
||||||
|
// e.Prop != null && e.Prop.ToLower().Contains(search)
|
||||||
|
var condition = Expression.AndAlso(notNull, contains);
|
||||||
|
|
||||||
|
body = body == null ? condition : Expression.OrElse(body, condition);
|
||||||
|
}
|
||||||
|
|
||||||
|
return body == null ? null : Expression.Lambda<Func<T, bool>>(body, parameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private EntityListItemDto MapToListItem(object item)
|
||||||
|
{
|
||||||
|
var type = item.GetType();
|
||||||
|
var idProp = type.GetProperty("Id");
|
||||||
|
var id = idProp?.GetValue(item) as int? ?? 0;
|
||||||
|
|
||||||
|
// Use the best label property
|
||||||
|
var labelProp = GetLabelProperty(type);
|
||||||
|
var label = labelProp?.GetValue(item)?.ToString();
|
||||||
|
|
||||||
|
// Fallback to Codice if label is empty and we haven't tried Codice yet
|
||||||
|
if (string.IsNullOrWhiteSpace(label) && labelProp?.Name != "Codice")
|
||||||
|
{
|
||||||
|
var codiceProp = type.GetProperty("Codice", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||||
|
label = codiceProp?.GetValue(item)?.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final fallback
|
||||||
|
if (string.IsNullOrWhiteSpace(label))
|
||||||
|
{
|
||||||
|
label = $"Item {id}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description: try to find a secondary useful field
|
||||||
|
var description = "";
|
||||||
|
if (labelProp?.Name != "Descrizione")
|
||||||
|
{
|
||||||
|
var descProp = type.GetProperty("Descrizione", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||||
|
description = descProp?.GetValue(item)?.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(description) && labelProp?.Name != "Codice")
|
||||||
|
{
|
||||||
|
var codiceProp = type.GetProperty("Codice", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||||
|
description = codiceProp?.GetValue(item)?.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new EntityListItemDto
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Label = label,
|
||||||
|
Description = description ?? ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private PropertyInfo? GetLabelProperty(Type type)
|
||||||
|
{
|
||||||
|
var candidates = new[] { "RagioneSociale", "Nome", "Descrizione", "Titolo", "Codice", "Name", "Description", "Code", "Title" };
|
||||||
|
foreach (var candidate in candidates)
|
||||||
|
{
|
||||||
|
var prop = type.GetProperty(candidate, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||||
|
if (prop != null && prop.PropertyType == typeof(string))
|
||||||
|
{
|
||||||
|
return prop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? GetStringProp(object item, string propName)
|
||||||
|
{
|
||||||
|
var prop = item.GetType().GetProperty(propName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||||
|
return prop?.GetValue(item)?.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsSimpleType(Type type)
|
||||||
|
{
|
||||||
|
return type.IsPrimitive ||
|
||||||
|
type.IsEnum ||
|
||||||
|
type == typeof(string) ||
|
||||||
|
type == typeof(decimal) ||
|
||||||
|
type == typeof(DateTime) ||
|
||||||
|
type == typeof(DateTime?) ||
|
||||||
|
type == typeof(int?) ||
|
||||||
|
type == typeof(decimal?) ||
|
||||||
|
type == typeof(bool) ||
|
||||||
|
type == typeof(bool?);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string MapType(Type type)
|
||||||
|
{
|
||||||
|
if (type == typeof(string)) return "string";
|
||||||
|
if (type == typeof(int) || type == typeof(int?) ||
|
||||||
|
type == typeof(long) || type == typeof(long?) ||
|
||||||
|
type == typeof(short) || type == typeof(short?)) return "number";
|
||||||
|
if (type == typeof(decimal) || type == typeof(decimal?) ||
|
||||||
|
type == typeof(double) || type == typeof(double?) ||
|
||||||
|
type == typeof(float) || type == typeof(float?)) return "currency"; // or number
|
||||||
|
if (type == typeof(DateTime) || type == typeof(DateTime?)) return "date";
|
||||||
|
if (type == typeof(bool) || type == typeof(bool?)) return "boolean";
|
||||||
|
return "string";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string SplitCamelCase(string input)
|
||||||
|
{
|
||||||
|
return System.Text.RegularExpressions.Regex.Replace(input, "([A-Z])", " $1", System.Text.RegularExpressions.RegexOptions.Compiled).Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ToCamelCase(string str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str) || char.IsLower(str[0]))
|
||||||
|
return str;
|
||||||
|
return char.ToLower(str[0]) + str.Substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetIconForType(string typeName)
|
||||||
|
{
|
||||||
|
typeName = typeName.ToLower();
|
||||||
|
if (typeName.Contains("evento")) return "event";
|
||||||
|
if (typeName.Contains("cliente") || typeName.Contains("persona")) return "people";
|
||||||
|
if (typeName.Contains("location") || typeName.Contains("indirizzo")) return "place";
|
||||||
|
if (typeName.Contains("articolo") || typeName.Contains("prodotto") || typeName.Contains("magazzino")) return "inventory";
|
||||||
|
if (typeName.Contains("risorsa") || typeName.Contains("dipendente")) return "person";
|
||||||
|
if (typeName.Contains("tipo") || typeName.Contains("categoria")) return "category";
|
||||||
|
return "table_chart";
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -1,99 +1,10 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { Outlet } from "react-router-dom";
|
||||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
import { Box } from "@mui/material";
|
||||||
import { Box, Tabs, Tab, Paper, Typography, Breadcrumbs, Link } from "@mui/material";
|
|
||||||
import {
|
|
||||||
Dashboard as DashboardIcon,
|
|
||||||
Inventory as ArticleIcon,
|
|
||||||
Place as LocationIcon,
|
|
||||||
SwapHoriz as MovementIcon,
|
|
||||||
Assessment as StockIcon,
|
|
||||||
FactCheck as InventoryIcon,
|
|
||||||
} from "@mui/icons-material";
|
|
||||||
|
|
||||||
const navItems = [
|
|
||||||
{ label: "Dashboard", path: "/warehouse", icon: <DashboardIcon fontSize="small" /> },
|
|
||||||
{ label: "Articoli", path: "/warehouse/articles", icon: <ArticleIcon fontSize="small" /> },
|
|
||||||
{ label: "Magazzini", path: "/warehouse/locations", icon: <LocationIcon fontSize="small" /> },
|
|
||||||
{ label: "Movimenti", path: "/warehouse/movements", icon: <MovementIcon fontSize="small" /> },
|
|
||||||
{ label: "Giacenze", path: "/warehouse/stock", icon: <StockIcon fontSize="small" /> },
|
|
||||||
{ label: "Inventario", path: "/warehouse/inventory", icon: <InventoryIcon fontSize="small" /> },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function WarehouseLayout() {
|
export default function WarehouseLayout() {
|
||||||
const location = useLocation();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [value, setValue] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Find the matching tab based on current path
|
|
||||||
const index = navItems.findIndex((item) => {
|
|
||||||
if (item.path === "/warehouse") {
|
|
||||||
return location.pathname === "/warehouse";
|
|
||||||
}
|
|
||||||
return location.pathname.startsWith(item.path);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (index !== -1) {
|
|
||||||
setValue(index);
|
|
||||||
} else {
|
|
||||||
// If no match (e.g. sub-pages), keep the closest parent or default
|
|
||||||
// Logic could be improved here but keeping it simple for now
|
|
||||||
if (location.pathname.includes("articles")) setValue(1);
|
|
||||||
else if (location.pathname.includes("locations")) setValue(2);
|
|
||||||
else if (location.pathname.includes("movements")) setValue(3);
|
|
||||||
else if (location.pathname.includes("stock")) setValue(4);
|
|
||||||
else if (location.pathname.includes("inventory")) setValue(5);
|
|
||||||
else setValue(0);
|
|
||||||
}
|
|
||||||
}, [location.pathname]);
|
|
||||||
|
|
||||||
const handleChange = (_event: React.SyntheticEvent, newValue: number) => {
|
|
||||||
setValue(newValue);
|
|
||||||
navigate(navItems[newValue].path);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", height: "100%", gap: 2 }}>
|
<Box sx={{ display: "flex", flexDirection: "column", height: "100%" }}>
|
||||||
{/* Header & Navigation */}
|
<Box sx={{ flex: 1, minHeight: 0, overflow: "auto", p: 3 }}>
|
||||||
<Paper sx={{ px: 2, pt: 2, pb: 0 }}>
|
|
||||||
<Box sx={{ mb: 2 }}>
|
|
||||||
<Typography variant="h5" component="h1" fontWeight="bold" gutterBottom>
|
|
||||||
Gestione Magazzino
|
|
||||||
</Typography>
|
|
||||||
<Breadcrumbs aria-label="breadcrumb">
|
|
||||||
<Link underline="hover" color="inherit" href="/">
|
|
||||||
Home
|
|
||||||
</Link>
|
|
||||||
<Typography color="text.primary">Magazzino</Typography>
|
|
||||||
{navItems[value]?.label !== "Dashboard" && (
|
|
||||||
<Typography color="text.primary">{navItems[value]?.label}</Typography>
|
|
||||||
)}
|
|
||||||
</Breadcrumbs>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Tabs
|
|
||||||
value={value}
|
|
||||||
onChange={handleChange}
|
|
||||||
variant="scrollable"
|
|
||||||
scrollButtons="auto"
|
|
||||||
aria-label="warehouse navigation tabs"
|
|
||||||
sx={{ borderBottom: 1, borderColor: "divider" }}
|
|
||||||
>
|
|
||||||
{navItems.map((item, index) => (
|
|
||||||
<Tab
|
|
||||||
key={item.path}
|
|
||||||
label={item.label}
|
|
||||||
icon={item.icon}
|
|
||||||
iconPosition="start"
|
|
||||||
id={`warehouse-tab-${index}`}
|
|
||||||
aria-controls={`warehouse-tabpanel-${index}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Tabs>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
{/* Content Area */}
|
|
||||||
<Box sx={{ flex: 1, minHeight: 0, overflow: "auto" }}>
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user