From f48813c1997eb84f6a507c86e8e03b5ab2fb5abe Mon Sep 17 00:00:00 2001 From: dnviti Date: Sat, 6 Dec 2025 00:46:26 +0100 Subject: [PATCH] feat: Refactor dataset management logic into a new SchemaDiscoveryService, removing it from the ReportsController. --- docs/development/ZENTRAL.md | 4 + ...2025-12-05-224000_remove_warehouse_tabs.md | 14 + .../2025-12-05-230000_live_data_alignment.md | 35 + .../Controllers/ReportsController.cs | 788 +----------------- .../Controllers/VirtualDatasetsController.cs | 102 +-- src/backend/Zentral.API/Program.cs | 1 + .../Reports/SchemaDiscoveryService.cs | 397 +++++++++ src/backend/Zentral.API/apollinare.db.backup | Bin 245760 -> 0 bytes .../warehouse/components/WarehouseLayout.tsx | 97 +-- 9 files changed, 484 insertions(+), 954 deletions(-) create mode 100644 docs/development/devlog/2025-12-05-224000_remove_warehouse_tabs.md create mode 100644 docs/development/devlog/2025-12-05-230000_live_data_alignment.md create mode 100644 src/backend/Zentral.API/Services/Reports/SchemaDiscoveryService.cs delete mode 100644 src/backend/Zentral.API/apollinare.db.backup 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 4aba3c1c5cbe8222e40035c7f52f1fa54c0b4cca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 245760 zcmeI5eT*CDedlL5+~snm-LWFcjJ#Rzh_Wm((#m|p%>q`2H2u6G5B7Pt%Uf-}(Oat9=LMQ}MJ*EWY-f(Gp& zZGt8jBt0Pg<9^S)a5#L?TC3PzDc`Zooq1k7S4=6z zJ;Vh9+(#sd+wQOf1ZQf^~yO-ko;rE*awcPU!9ve^@}P|U8&#pk6f zndc>Wqnyv?NYQ#GS3YlxOzTyxqDG~1<~fq*$aMTx)KdwfwSW zHcmN|U9WYmu9j~!JL&V(7nE(aq}}ZJ__gN^O>LNpNk%eQ`N6P}*UV5Ta{4s?#2_S%POxDoen7wJ0}y`^A9G08IQg7ue8Z<+dipVg}7&d|F1w5o0I zP{r+SZXCN=)oPAW3<+>RioG}*ikv#dzy3m3v9Ib4(aX&U{-#{5g>joOg<$h`M!5%)=<-X0$bMW(0uH@2-pUDGR8^W**<^%tk>d{<@Z zMcRKsW*b$F?w&X4{%GG^y(TrSGzU0pCacK3VGD-7t2HWK>+gvvt2K2?Zwye7savgm zudJfFr5e;+cWlR#wW765rEycaQtwYfP9GHGVASweuLVPqSd71|TgH&q8(Z3T+fdk3 zi{`%?@f(KU^RCRJH|kzSlHSpj-41kreZH#Hbfe?fRUrKjU!G_B-FdQl?J2=}vU)w? zz@=wJt#ePZ9v!{QU@u-@;Jf786L(L*_wp2**43>o*)@BA=~#Ztckn9>_B$ArqYxNn z55~`1rPq3~MqRDVmil5_ZB*2rOTkxqJ)UP9N?koqF=Mya`A}qPivReFwnfy~)1Q{M z{{(z8qK@m`mAow}D(zL`MP-}yOG-MmjKl7Q2e1V$s1?^>x@xm!y{lTQ(v=m@yPz1R zW-8g;0e1(x`$pXdQ)|$3prH-8861@6n>xf(@28*R$LMF`A^JHSo}k~6$xEF0N8+=S zKc4(7o#Fri5C8!X009sH0T2KI5C8!X0D&$7%E(df%q%x`oBIx>BoZQeu8pH4Md2Rx|PWR5FoBB$LSsy)9ShBj!gM6tQL0E@)=_ z;?m;6{2YadmxD*S*)v?kQbE!!6}A*jx~#QZYO|>_Eg5=UPk-j))K#x5g<^a@nV6fK zOQllV7wC;EY1KZWvRASfDti68aFm;Qf;)5Maa8kHSQT2tu#DATj9+`OqZG$}d1u2zs3J5 zz9s$}@zdfb#2aE=+!TxAvbaE=I6wddKmY_l00ck)1V8`;KmY_l;6W#FbW%9S_lz_S zj86zB_{=UFdJ&vFES%)opl|D1A3JnNnBt3COE+3-=^RAOHd&00JNY0w4eaAOHd&00JNY z0zWqbQt&V*o;-T=s5LTRW22$bNP-qkKS*4N+y5k?J4C6V8d6EEwoQ$^;};g_R|<58 zSY{!X+gn?z8NaY(1zM8$?hrByAy?ZqN5(t_V*P(XOmX6$iXWvvI6wddKmY_l00ck) z1V8`;KmY_l00e9T%R-n-9ob+Pg-kI`OSx^xYne?sO$))5^Qk!XvMcn7zfGNaNvW_I z4!TsQVg7YpGBg_hPiqC}mjhu=KJpBcaXFhW=CYfmw7inZU6VHp#mrTbnZ24%%wN`Y zgXRa&%!FN~rd2ibOaDx&sx_%;sMg?vUHzB%F!zL5VuIG?=Qi!Ao0iOcD%Y;71}Rr) z$UkW(F?KYmNiPCuQiIXZ?fL&7=fuy8y7`B5xa-*`|dGs570dNd=v)48UZim3mX5+4i&oLC{Xq3mQp|M0+7kaHmOE1LZ-JmMEWCc%9f>{5b7X8No0w4eaAOHd& z00JNY0w4eaAOHd&@BkA)|Nj9lTx1OdKmY_l00ck)1V8`;KmY_l00bNYtpCp+-sb2Z z4iEqV5C8!X009sH0T2KI5cqizc;n%~LtJcP`jfO#+>z;Unq=nM`mQdO)q1n0nCfMf zc7UVt&o4*WI%u@BeDqS3mM4pzkJeRFiC%g++ED5$Ir^iKKMlNAOWIB5C(qHfZfG>h zTQO}JP2Hdo-W%~7BQ!6YHt;!B< zL`N=85T>C;UX)r#u3A&;lmT}2a#X9bpiA>h$+)^WyOpd~X6Kd?@!5+P=c==+I=`h> zQ`al%LV`q_wklJ7i79I8EzM@|a`dI>rPShu#8Pr`ZfS8T9$#8W%$<+kh+bM)xR6|m zFD}ld63L~d#YFOa^rKqU+=*UVA~-v$O(%6>eu=_dWXJqs>LS51beC|PWx%{qqg=im zWq4K<+Ki86N1s+}yDF`GuTW~3hh0ca2fKRK5gu*mv}Iq-4P#649BJKr-5_D7;?}{k zN%@m&+BUNgtDMd=lVN<}`oMR-i^zHJ-KeL)o#f(|rPM_mZQzMGN&;DRxt-R<*`< z^b*CaX$|#hC%uUalmWe|R5X(^nV{UW8U9pYU8Q|pYgln9WlF8>S*^*Y{*!F=C7Qrb zbMh@Kw+-6zdimwYm^IV1{p+gn$}3S4zN@vg>kO`Gv{g!pUU@+!o7hrnExL5-1Vgf! zSX`J(E-c1V3m4~>qObhQEApIc{KG1|EPF{ z6Tc_^8La{EDX~TuI6wddKmY_l00ck)1V8`;KmY_l00f2+m>CUor%qYx`LATs`J#L+ zo6lue)Cw(PaFhD_+S!pXcRFPSkjv#_=Gry6yit^8(==3?^iL}b&>a7EMQbS2!7z97 zq!qH9zbao|%gW_^S>IK*Yno!}$7uz^6DKV1LcS!IGX=dx@6e`bQNfuR%XvAQPCuQ^ z6tl}(g)KQ~Xvb-xKsOFaD`#n?0n^l2|6dUQJ?sDfclv_^1V8`;KmY_l00ck)1V8`; zKmY_l;NTF5&>MiMDXXiW%j9$Pb|9zf4SF>Yp?3g|$FjV>oXzEzl{$?_(7XEo^2)|q zId5$QXs!Pr=Dx#C{PD!E9{Rm;d2DWUGq@92A*a81r?NXbK5B`6Re7g0cyeD=!lB5S zGyKPRQ=xGbWv!{L)2k9gqp?Q5i}7?ZLvK=~vb?;Ok)l4osC0H*q9dBEMx|`7oVlDS zO1XSl%5AKzNwn=lsa%xFU5Xa2Z1%(~6tnAc@phruC{; zQKM2h^Bl=@WIBE;YO7*s?A_6j0QBle+tvHiGVJHv<9k=6`{M4!I;YbRvbRgRixuJ6 zreS5u;J9v1f4N2Dc?L=^@6d>zhN9J?((+n<*)kia96Da>T3s!(5nPT0Pkq51;5JZp zzH7MKzyJrsMqV>Rp~&ge{2S-GY$R(9p|gw8J|7!o9k$m#Ov8=1f4fNEx#%qgD>U}c zEm(j3?JZNkuRSuYuRU=L!x?Wj%zIU<*`pqZ1vntZUK|ZYPMzXkf1#_`S82eU(=1r0 zp+SXi`Rt?2op}2%awfnT!#J=$F@Jlo;85@VTEu-)sJF*QLXqid{*7&`P}lT|HQLtw zJL)e^*ZHo>(u=hJg3Qt&OuBpCr2C_NbM>0kw9*{lsF|!H_l7MP`mWZfc&)!Drc7gX zw`eKq{z#d+)!O&UDymzmL1RS+wDK#o+O^WSsa&b|OUu#X)%~sqqlUkFEf|W#V*G6y z5ljzQv_|!owoPBMtS?^vs}a9p_&x8+JbI(3Gzy-D9 zrnRd!Th_a(wb%;${ob@afT@{Ec6Y$t!S23MTVc{?dz0-^&>#OGH{TP7KgWgmCGJo* z{FAXykN&;z!sH0QME}LTJ~Yle0;5OlcNe$g583r_(W-~ESA}l0ROio8e?4?u@2Vbl z;_OuqhxcjI`Zn#7F~FYh`mu#xY3O!>6yF;VuLd1l6Suol-S22tuWUMWt#>s}#@Kmx zCRg*tO!jildT(=uy3xnY@%%;qf7WFz#6&Ua-sUZlOu=AK?VF&dq>XhSTb^OQ2g{~1%E zN8Tn4i@Hu-+5Se8{7YuF^HkuOn(fhfBWsmGpIN(W6`MT|jGMkP@*8}&m*3obgxta82UfjKV#tWRSFu*@^O=-CIlUI= zPm)j69TGGid+p3bD01Qi|9Z@-gBxt(o@W0(M;?DIEHGc#mh~Vwl%B+Z;Mz-rz z^p{ze&s`OqFWz3IcTX+n9?0JTPn+P+hp^=y8HZcab3MV}|%emG92FZQ%*pP}aaPG*D&`tHwh z;V}JhT*cZLz}_Cf{{28;f;#(56#Z~qou<+%>+DCa*0siIo_u2+Uxm5Jb^4+A3${ut z80-HZo;b#dej$CJ(uY_u$jM5|?NZ7I@qO=(p0U~nlOzYtHj{|eVW za)?Ac*)JmDZsKOgRA@i;_)DpHJQz$c3GoXF`(NSO#c`4_+asZnkEf{fM?1PHnl72j zF0Jw+2=k6|I|+qrOJRz7u0Lurt=CMoMyYpV1{YY&q?K&?C8cA7l_pkoqq0Naua%)u zmqG)SF300@tF^YNN|ha@PJ7!o)Ifl;>w+R;g=@bwN--CEl1njD?%s8(L<_L$4(ot0 z*OT3+MkxM#fBa-md_!-w^#16}L5lw2{^$uWx>9+utw^*KJAIlbd;yzVS8*>0BqJ8~ zTTFqnU~RIeOI5W+YyOx)H|ciN!nF?vDBhF(@sdtFT3oiOOSGJNg^EpSD1Al!VSccH zgd?Cy_Sr2U=-P^#0@nW@69b(1ZSjlZXT{gV>ta@Xj2v))00@8p2!H?xfB*=900@8p z2!Ox?ML-%G=cZybMxG8@lkh6-rDrd|Clw5Bmp;x1PKCy~nc1)vLcW&G3+n$o6SLWG%-agNpnwPL69>xH$9oXh3Ydc9dwl}1Ak zygx_@n6kywDvKrBFOJrMZE1BSDv;uHj!%AlBbUwRGL)*iB2oNygKfa@fdGY>vBNCO zw(j@G7mCSJ9m*`V?$oVSz#}jyyKPM(THh$&D)Y`|TO~6l#8co&qiePtwT$ zTuuzFaf&wP3`TvuTaN@_sk;&ho6C5A_ z0w4eaAOHd&00JNY0w4eazhDGf!AH57)bun@2hC*QdG5q<2Z8l1J99M|Ptt()Ir_6O z8&Aw8=cRc3lJzftmnJ6w5l40VrUQ5v;b4+C>?u>4MGo0 zxk|^Fa#v}}Rho!L_hp)J!6p*S)Bm@) zKu0bAL?^E)rqQQ1Nowb36Uo^`!c&{Plw#RVEZAxTfxvj@`@6KvCW!24h1K!GoHa>= z<U8?hQne0Vj#c=xG8H^Kc&D&JK@GnE_C81F4~%DOuxhbnD>QWD$=wE zZPv;<_bKkxW89|_GzoxGVa+(P`v1ZoapV7bY<~1KoqUD+BFpxDbBsPbYNhpKmE)mE zBEf&c-qY3^o}sO%+ijY&agzp0*t;tFTu%7+wCs;^S9@CyioVw#m-ZkM-yW5Dd%JjN zE6){$wn_Ig(b*^6A1Xg+WAj{_mUz?)s?nlRB3*kxdwWVdA(U-}Ho+bkBCXRrr$PEX zAuB2^C*kbjp3Rjrmor5v=TF|c+GzK0V7k`Uw+C!5J$P?*d$^2AG`iC1*Yd9Hojh>Q z&i<0$w4LlfIxAoQuAQB7_ix#0#~QF-XId_$<(15M>~`x|C^9?C|KDSXWOs*~m0Z(aZ0n{SW{`^a+U~p(dFQNd;%F!mi}7!4_^hs` zZY!S6*;kRl7H50z%ii3m_iSxBy{@imwIQaqPD}EZZ!|l;UJIAj8z!}2CK=%JT7Fr| zd0s;rD|-EJs#V!^ZPJV27|YLL*XBF4MC@w@E17rB9-8k9u|2tebEexKDis3dHbM5q z!vn3s_S=s!xH0e97TA5YKsP>X8Rq_C%GPPlB`fej6@ITah3sWZrRAE3-lTul(%#iW({?~@&MumF%;Y|?aU>L>@wIP`beWue`#Uzs$o64fQE4x;O6ygvGN6TE z)@0w(hgpo0ZLs^C>+KacJ-V%0cPQJ^JdbpZ1{RyD@7d7vUDGR6!S%)vZ?^rpouDBB zO7tj2sTuOXc1HuXpBnS6ChauVCBaKucL2HCuDy+5%S!z^txUG3xRP3>1=9Bnw%p#@ zQmOUrWY0^S^+Zvo`?+@?=<&3lm^;rCp06ECcH5xd0xK!C>VN`fwo$4mRkd+bDH!T5 z6)J7e)^7-`T4$-i4ewdVQ_VabgBTps{zN?3uj}LX0KIN&bq8~Jub*%>?W}9gU#m{| zpQpXb;(wraB2r@zrHT-f@;PZGvz94mq*A84pOwz%O68(V_98`BuegnvTgf)v6IPl0 zio1OLnq7~5s-_BT-%0gNe_i&g_xbq~;7Pbc62@b!|Ia;i-}Ls8P!IqC5C8!X009sH z0T2KI5C8!X0D*lFK>vRq&=3U#KmY_l00ck)1V8`;KmY_l00izc0_gwWX9b4jf&d7B z00@8p2!H?xfB*=900@Ac} zC%!eYJ0Twa4~MTD`s$(e@h=S#|LaqsNP3#Tby{mw)t9o*ZOVqJRrH!>EnP@E5vYbn z%WY<>uDj$c*qY6)WS*0vcZd;XD>g>mcx)jGbjSMXf+NaxH-L+Sx;~s0C2t}SGO~?B+Q69H-%B7dbM~CMz?S<~oU$lDdP@uLo62@g(~DY5H(IK_2yUBafuGLIHtE)sA< z7>|8Sk5J`%lK(_|P)(q9b!oNaU3~8=Usa+BBlG zezCv8JYy$ULSin6j@y4f9r`s7H==VZR_sXyY2YM9j)D6 zw|oB$zboOMEuG!UEn)se&i%G-FD5?p&MH%6h_ z+3Rt>?cF^5HZ5t?8ZwbZ^e!>9<`!I81#{B%gM75&Z7lzwj76te)U+9q}voEnsdZ zJ~l+9FKD+umP=+v9_o!#qmgqb_;!yjYhMFf$=P8_oV^dc;$+fo$Cw%oMdm3FVXc7; z#jO35yvEeoE({xrNzr#@8=tOy*WS6UeDM8$$Xg9Kg8&GC00@8p2!H?xfB*=900@A< z14ID*{|BfrkrfaC0T2KI5C8!X009sH0T2KI5Ew#WoZJtKU*N<)7k?stSNxXvU&Nn@ ze=mMd{F?aR#6KYq93TJ!AOHd&00JNY0w4eaAOHd&00QqFff0e{gNLo(L+n==x6Z@X z?-=_Hge><_>vzO*4_d#1zuN#+{R7*f~jz zv6JO^d@kgiq(+^SU2(ptxJf0GqlVl(y91oLA!tVdS#fkqY{)6~`#qU$!|2yJ0 z>0Q8A#V^y_fVUoS(#ReNfB*=900@8p2!H?xfB*=900{vSHU0RkWZ0w4eaAOHd&00JNY0w4ea zAaLLbh{7p8esO7WVSX-^OvFcm0>=j}$3!wUH@~pBbTLl-e~$J4Ij8^s2b}nA@h$NW z#2<-YJMbAq0zm)-KmY_l00ck)1V8`;KmY_l-~lFZL^#9eR6~;k9sR)p0w4eaAOHd&00JNY0w4eaAOHd&a6kw|gcM)M6w{epc|%^yY|80$ zK3C2Qj|wt>Ih!x$vYVx}ypqXXlQ#>+%vB1Py_ydk6P^gK%g=4v4x3iUJnR1l#lL0a z|9>j}kWO%b00@8p2!H?xfB*=900@8p2!H?x+{XmG4FCjd1Asth{(n&X3Mc+p{A=0+ z@GJLm29b0S009sH0T2KI5C8!X009sH0T6ib39xbhnXAcoa*p-_ux0_cS1F;}FBv#2 z%!FM(YyJPA_&HAen)p51_3v{JJ}Bf61V8`;KmY_l00ck)1V8`;KmY^|AOSYmpL+ER zv+=}ia$btZFIoSB-R_Ba$6avT=Vuei*+in_9(Y(tg-MR(o?Pg-`^NwOt@y(ONB~I! z0T2KI5C8!X009sH0T2KI5C8!Xc-sWn_w8f|4;M(e<=P${2Au|zipfd2m&Ag z0w4eaAOHd&00JNY0w4ea2a*8m`5y}gg)!dh{*MQO0v|qx@Bas~u#hMa009sH0T2KI z5C8!X009sH0T9?H0q^^NsOS4X_s~8?A{q#Q00@8p2!H?xfB*=900@8p2!OzSL4bY# zNB{r6C@Lfp1V8`;KmY_l00ck)1V8`;KmY{xO#uD>eWOD}5C8!X009sH0T2KI5C8!X z009uVZwR3Of8P`p5()w!00JNY0w4eaAOHd&00JNY0{bQ~K`|fYuW;gTi;qqItH~!P z{_})<_*;ixJN$t|pFi}g<3AcV!#@sdV}Cw&G4wm5-yMBwT|H&3sbzLam%d4-dO>NKdbaAh zKIy3Rba{T=F>IBdA5+^UFd8%FLXoGY_**Aj%|)%H8!gomVxKG7s^fU6PwT+Ywo=E- z)!300jeRr~iadFazjeH$Q*G*oSyt=KnqsQ0&759Wi+;C@{i^)|tVoV#zYj|RlNgQN z8cl{G8PXgZra4Xew{=5PdekrNslF3)Xd1i_v6~4h@6%o7ZL7=Hm2Jv?Ct$bh;*b>d z#_3jX`?36YL`Gvb;-vdjpYBz?VYW8e@2XPQYBy}%mg~Y0)xA-=Ra<^cvnMecyY|Fr zWOkar;nH8zD~hS<4Xfe+Y$);!B~soak(M;7@q?wN-fkLQF?%Jk6E1e^ z)Oo`Arc(p~$n8%Jm@}-DsP# zVQLk<){oK+k$Btb^hE8M(ypMfTW3BfGnKB8JyO^S7P}>$4MnmE{#K$lb=&Qh zsobP?MC;g+n>5dBc4)f1FrDcBM0o*vtT1$jZYQ%{w-Z~fTr&+Vt+%L9-Q@RroF9^! z0a3ft;(7a1@*9tbB65tsb<&SUuGQ3SYGM15VLP82nuwlg{v_C*{sg>ynr=-f8p%rN0zz+MeqJ~8#= zU^Od>sn~h63-0k@YP^8GYPx)4&q;KnKjGczZK0cKy{c7I`{ML44R)Yj>5gmc`b;R2 zB2FVcoYHhtYM7m-^Rc1Q-Ee&hIv%lG+@thROx5X(t0b*AwzTcG;j}X89ZlI){f?&` zjeVT}f3QtEuC3kg#ca*-jkTw#KG9=&r$b=pPunai+tkphCB334HPvyA_T_ePa7U|^ zQ;n(fON+*|4}>C%q;{f{aXaj0L81GosbqH*NAk&`D!rh6>bksQzdjX;q^J2?r(IRf zJ=4AsuG4ckJ%!h(KwWpKPowK&)!wccwqDoQt-{>_Vmpse$BwjjI(BYo$41@DnPH{s z25`dnt8@*vd4f7Sq^Z-{@nX7t4KI(64$ouf2|imLpv)5$jp6%0US5Cz2!H?xfB*=9 z00@8p2!H?xfWU)K0R8_5zl@PX5C8!X009sH0T2KI5C8!X009s{{~tC00w4eaAOHd& z00JNY0w4eaAOHdnJ^}RqAN(>#4nY6}KmY_l00ck)1V8`;KmY_l0R4a100@8p2!H?x zfB*=900@8p2!H?xJop6A|9|ky7&!z15C8!X009sH0T2KI5C8!X00Gwj9}(Z=#6K1P zQ2eI&ruYLe0Ra#I0T2KI5C8!X009sH0T2KI5O`MzM1&Mi8$p}8q0ly!(uyLjD~6_z zyibVnPwS>ylhU+*r9nF$22Ti!yu58_X4}wObzREVq=LRp+dc{`a6zdlH*f0FO1naV z1Md^gg|BM0nj%$Ijbu~}4d4IYl@fC8!X009sH0T2KI5C8!X009tqPYIy^|DNK9 zau5Ik5C8!X009sH0T2KI5C8!XcvlFp{y#7Nn4^C9t{*pdLb zw!W)NWwqX{DW={34~c)tiT^16gZLxy@5H|rzfG4oKmY_l00ck)1V8`;KmY_l00ck) z1nvg{BLdHlhS=}ODEkeLuwNm_egi^K2-B|+4zut7W8ybB@u$@L|GxOY#Gi?OBYs2t z&i#-@q!9!_00ck)1V8`;KmY_l00ck)1b(3jSbh94*2lMA00`C#fWQcI2(ll$`yUke zur>ZaD1M!zum67~{(w$!fB*=900@8p2!H?xfB*=900@8p2;3J0CWIMYZ_@mJ+CWeZ ejteLFO1q*p6d^1`_`2TEl)#t}2`kmQ*7*OEB9_kp 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 */} - + +