diff --git a/docs/development/ZENTRAL.md b/docs/development/ZENTRAL.md index 6e69e37..158767d 100644 --- a/docs/development/ZENTRAL.md +++ b/docs/development/ZENTRAL.md @@ -32,3 +32,7 @@ File riassuntivo dello stato di sviluppo di Zentral. - 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** - 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. diff --git a/docs/development/devlog/2025-12-05-224000_remove_warehouse_tabs.md b/docs/development/devlog/2025-12-05-224000_remove_warehouse_tabs.md new file mode 100644 index 0000000..39e039c --- /dev/null +++ b/docs/development/devlog/2025-12-05-224000_remove_warehouse_tabs.md @@ -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. diff --git a/docs/development/devlog/2025-12-05-230000_live_data_alignment.md b/docs/development/devlog/2025-12-05-230000_live_data_alignment.md new file mode 100644 index 0000000..819034d --- /dev/null +++ b/docs/development/devlog/2025-12-05-230000_live_data_alignment.md @@ -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. diff --git a/src/backend/Zentral.API/Apps/ReportDesigner/Controllers/ReportsController.cs b/src/backend/Zentral.API/Apps/ReportDesigner/Controllers/ReportsController.cs index 0bd7c18..39aa169 100644 --- a/src/backend/Zentral.API/Apps/ReportDesigner/Controllers/ReportsController.cs +++ b/src/backend/Zentral.API/Apps/ReportDesigner/Controllers/ReportsController.cs @@ -12,11 +12,13 @@ public class ReportsController : ControllerBase { private readonly ReportGeneratorService _reportGenerator; private readonly ZentralDbContext _context; + private readonly SchemaDiscoveryService _schemaDiscovery; - public ReportsController(ReportGeneratorService reportGenerator, ZentralDbContext context) + public ReportsController(ReportGeneratorService reportGenerator, ZentralDbContext context, SchemaDiscoveryService schemaDiscovery) { _reportGenerator = reportGenerator; _context = context; + _schemaDiscovery = schemaDiscovery; } /// @@ -126,26 +128,7 @@ public class ReportsController : ControllerBase [HttpGet("datasets")] public async Task>> GetAvailableDatasets() { - var datasets = new List - { - // 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" }, - }; + var datasets = _schemaDiscovery.GetAvailableDatasets(); // Aggiungi Virtual Dataset dal database var virtualDatasets = await _context.VirtualDatasets @@ -173,7 +156,8 @@ public class ReportsController : ControllerBase [HttpGet("datasets/categories")] public async Task>> GetDatasetCategories() { - var baseCategories = new List { "Principale", "Configurazione", "Liste" }; + var datasets = _schemaDiscovery.GetAvailableDatasets(); + var baseCategories = datasets.Select(d => d.Category).Distinct().ToList(); // Aggiungi categorie dai Virtual Dataset var virtualCategories = await _context.VirtualDatasets @@ -187,6 +171,8 @@ public class ReportsController : ControllerBase if (!baseCategories.Contains(cat)) baseCategories.Add(cat); } + + baseCategories.Sort(); return baseCategories; } @@ -207,7 +193,7 @@ public class ReportsController : ControllerBase return schema; } - var staticSchema = GetSchemaForDataset(datasetId); + var staticSchema = _schemaDiscovery.GetSchema(datasetId); if (staticSchema == null) return NotFound($"Dataset '{datasetId}' not found"); return staticSchema; @@ -230,22 +216,7 @@ public class ReportsController : ControllerBase return await GetVirtualDatasetEntities(virtualName, search, limit, offset); } - var entities = datasetId.ToLower() switch - { - "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() - }; - - return entities; + return await _schemaDiscovery.GetEntities(datasetId, search, limit, offset); } /// @@ -254,358 +225,10 @@ public class ReportsController : ControllerBase [HttpGet("datasets/{datasetId}/count")] public async Task> GetEntityCount(string datasetId, [FromQuery] string? search = null) { - var count = datasetId.ToLower() switch - { - "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; + return await _schemaDiscovery.CountEntities(datasetId, search); } - #region Entity Queries - private async Task> 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 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> 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 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> 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 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> 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 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> 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 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> 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> 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> 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> 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> 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> BuildDataContextAsync(List dataSources) { @@ -642,38 +265,7 @@ public class ReportsController : ControllerBase private async Task LoadEntityDataAsync(string datasetId, int entityId) { - return datasetId.ToLower() switch - { - "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 - }; + return await _schemaDiscovery.LoadEntity(datasetId, entityId); } #region Virtual Dataset Support @@ -706,7 +298,7 @@ public class ReportsController : ControllerBase if (source == null) continue; // 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 => 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 foreach (var source in config.Sources) { - var sourceSchema = GetSchemaForDataset(source.DatasetId); + var sourceSchema = _schemaDiscovery.GetSchema(source.DatasetId); if (sourceSchema == null) continue; foreach (var field in sourceSchema.Fields) @@ -773,15 +365,7 @@ public class ReportsController : ControllerBase if (primarySource == null) return new List(); // Restituisce le entità del dataset primario - return primarySource.DatasetId.ToLower() switch - { - "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() - }; + return await _schemaDiscovery.GetEntities(primarySource.DatasetId, search, limit, offset); } /// @@ -881,349 +465,7 @@ public class ReportsController : ControllerBase #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 - { - // 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 - { - new() - { - Name = "dettagliOspiti", - Label = "Dettaglio Ospiti", - Description = "Breakdown ospiti per tipologia", - Fields = new List - { - 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 - { - 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 - { - 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 - { - 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 - { - 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 - { - 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 - { - 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() - }; - - private static DataSchemaDto GetLocationSchema() => new() - { - EntityType = "Location", - DatasetId = "location", - Fields = new List - { - 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() - }; - - private static DataSchemaDto GetArticoloSchema() => new() - { - EntityType = "Articolo", - DatasetId = "articolo", - Fields = new List - { - 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() - }; - - private static DataSchemaDto GetRisorsaSchema() => new() - { - EntityType = "Risorsa", - DatasetId = "risorsa", - Fields = new List - { - 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() - }; - - private static DataSchemaDto GetTipoEventoSchema() => new() - { - EntityType = "Tipo Evento", - DatasetId = "tipoEvento", - Fields = new List - { - 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() - }; - - private static DataSchemaDto GetTipoOspiteSchema() => new() - { - EntityType = "Tipo Ospite", - DatasetId = "tipoOspite", - Fields = new List - { - new() { Name = "id", Type = "number", Label = "ID" }, - new() { Name = "codice", Type = "string", Label = "Codice" }, - new() { Name = "descrizione", Type = "string", Label = "Descrizione" }, - }, - ChildCollections = new List() - }; - - private static DataSchemaDto GetCategoriaSchema() => new() - { - EntityType = "Categoria Articoli", - DatasetId = "categoria", - Fields = new List - { - 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() - }; - - private static DataSchemaDto GetTipoRisorsaSchema() => new() - { - EntityType = "Tipo Risorsa", - DatasetId = "tipoRisorsa", - Fields = new List - { - new() { Name = "id", Type = "number", Label = "ID" }, - new() { Name = "codice", Type = "string", Label = "Codice" }, - new() { Name = "descrizione", Type = "string", Label = "Descrizione" }, - }, - ChildCollections = new List() - }; - - private static DataSchemaDto GetTipoMaterialeSchema() => new() - { - EntityType = "Tipo Materiale", - DatasetId = "tipoMateriale", - Fields = new List - { - new() { Name = "id", Type = "number", Label = "ID" }, - new() { Name = "codice", Type = "string", Label = "Codice" }, - new() { Name = "descrizione", Type = "string", Label = "Descrizione" }, - }, - ChildCollections = new List() - }; - - #endregion } // DTOs moved to AprtModels.cs diff --git a/src/backend/Zentral.API/Controllers/VirtualDatasetsController.cs b/src/backend/Zentral.API/Controllers/VirtualDatasetsController.cs index 1c076ce..2af61b0 100644 --- a/src/backend/Zentral.API/Controllers/VirtualDatasetsController.cs +++ b/src/backend/Zentral.API/Controllers/VirtualDatasetsController.cs @@ -12,10 +12,12 @@ namespace Zentral.API.Controllers; public class VirtualDatasetsController : ControllerBase { private readonly ZentralDbContext _context; + private readonly SchemaDiscoveryService _schemaDiscovery; - public VirtualDatasetsController(ZentralDbContext context) + public VirtualDatasetsController(ZentralDbContext context, SchemaDiscoveryService schemaDiscovery) { _context = context; + _schemaDiscovery = schemaDiscovery; } /// @@ -365,11 +367,19 @@ public class VirtualDatasetsController : ControllerBase var source = config.Sources.FirstOrDefault(s => s.Id == outputField.SourceId); 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 { Name = outputField.Alias ?? $"{source.Alias}.{outputField.FieldName}", Label = outputField.Label ?? outputField.FieldName, - Type = "string", // TODO: determinare il tipo dal dataset sorgente + Type = fieldType, Group = outputField.Group ?? source.Alias }); } @@ -379,7 +389,7 @@ public class VirtualDatasetsController : ControllerBase // Altrimenti, includi tutti i campi da tutte le sorgenti foreach (var source in config.Sources) { - var sourceSchema = GetBaseDatasetSchema(source.DatasetId); + var sourceSchema = _schemaDiscovery.GetSchema(source.DatasetId); if (sourceSchema == null) continue; 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 - { - 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() - }, - "cliente" => new DataSchemaDto - { - EntityType = "Cliente", - DatasetId = "cliente", - Fields = new List - { - 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() - }, - "location" => new DataSchemaDto - { - EntityType = "Location", - DatasetId = "location", - Fields = new List - { - 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() - }, - "articolo" => new DataSchemaDto - { - EntityType = "Articolo", - DatasetId = "articolo", - Fields = new List - { - 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() - }, - "risorsa" => new DataSchemaDto - { - EntityType = "Risorsa", - DatasetId = "risorsa", - Fields = new List - { - 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() - }, - _ => null - }; - } + } // DTOs diff --git a/src/backend/Zentral.API/Program.cs b/src/backend/Zentral.API/Program.cs index edba731..b1c9245 100644 --- a/src/backend/Zentral.API/Program.cs +++ b/src/backend/Zentral.API/Program.cs @@ -22,6 +22,7 @@ builder.Services.AddDbContext(options => builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/src/backend/Zentral.API/Services/Reports/SchemaDiscoveryService.cs b/src/backend/Zentral.API/Services/Reports/SchemaDiscoveryService.cs new file mode 100644 index 0000000..4a71d87 --- /dev/null +++ b/src/backend/Zentral.API/Services/Reports/SchemaDiscoveryService.cs @@ -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 _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 GetAvailableDatasets() + { + var datasets = new List(); + 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(); + 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() + }; + } + + public async Task> GetEntities(string datasetId, string? search, int limit, int offset) + { + var type = GetTypeForDataset(datasetId); + if (type == null) return new List(); + + var method = this.GetType().GetMethod("GetEntitiesGeneric", BindingFlags.NonPublic | BindingFlags.Instance)! + .MakeGenericMethod(type); + + var task = (Task>)method.Invoke(this, new object[] { search, limit, offset })!; + return await task; + } + + public async Task 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)method.Invoke(this, new object[] { search })!; + return await task; + } + + public async Task 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)method.Invoke(this, new object[] { id })!; + return await task; + } + + // Generic implementations + private async Task LoadEntityGeneric(int id) where T : class + { + var query = _context.Set().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(e, "Id") == id); + } + + private async Task> GetEntitiesGeneric(string? search, int limit, int offset) where T : class + { + var query = _context.Set().AsQueryable(); + + // Apply search if possible + if (!string.IsNullOrWhiteSpace(search)) + { + var predicate = BuildSearchPredicate(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(e, labelProp.Name)); + } + else + { + var idProp = typeof(T).GetProperty("Id"); + if (idProp != null) + { + query = query.OrderByDescending(e => EF.Property(e, "Id")); + } + } + + var list = await query.Skip(offset).Take(limit).ToListAsync(); + return list.Select(item => MapToListItem(item)).ToList(); + } + + private async Task CountEntitiesGeneric(string? search) where T : class + { + var query = _context.Set().AsQueryable(); + if (!string.IsNullOrWhiteSpace(search)) + { + var predicate = BuildSearchPredicate(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>? BuildSearchPredicate(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>(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"; + } +} diff --git a/src/backend/Zentral.API/apollinare.db.backup b/src/backend/Zentral.API/apollinare.db.backup deleted file mode 100644 index 4aba3c1..0000000 Binary files a/src/backend/Zentral.API/apollinare.db.backup and /dev/null differ diff --git a/src/frontend/src/apps/warehouse/components/WarehouseLayout.tsx b/src/frontend/src/apps/warehouse/components/WarehouseLayout.tsx index edea8a2..d37f21e 100644 --- a/src/frontend/src/apps/warehouse/components/WarehouseLayout.tsx +++ b/src/frontend/src/apps/warehouse/components/WarehouseLayout.tsx @@ -1,99 +1,10 @@ -import { useState, useEffect } from "react"; -import { Outlet, useLocation, useNavigate } from "react-router-dom"; -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: }, - { label: "Articoli", path: "/warehouse/articles", icon: }, - { label: "Magazzini", path: "/warehouse/locations", icon: }, - { label: "Movimenti", path: "/warehouse/movements", icon: }, - { label: "Giacenze", path: "/warehouse/stock", icon: }, - { label: "Inventario", path: "/warehouse/inventory", icon: }, -]; +import { Outlet } from "react-router-dom"; +import { Box } from "@mui/material"; 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 ( - - {/* Header & Navigation */} - - - - Gestione Magazzino - - - - Home - - Magazzino - {navItems[value]?.label !== "Dashboard" && ( - {navItems[value]?.label} - )} - - - - - {navItems.map((item, index) => ( - - ))} - - - - {/* Content Area */} - + +