feat: Refactor dataset management logic into a new SchemaDiscoveryService, removing it from the ReportsController.

This commit is contained in:
2025-12-06 00:46:26 +01:00
parent 82d2680f5b
commit f48813c199
9 changed files with 484 additions and 954 deletions

View File

@@ -32,3 +32,7 @@ File riassuntivo dello stato di sviluppo di Zentral.
- Correzione import path nel modulo Report Designer e registrazione modulo nel backend. - Correzione import path nel modulo Report Designer e registrazione modulo nel backend.
- [2025-12-05 Rename Modules to Apps](./devlog/2025-12-05-194100_rename_modules_to_apps.md) - **Completato** - [2025-12-05 Rename Modules to Apps](./devlog/2025-12-05-194100_rename_modules_to_apps.md) - **Completato**
- Rinomina terminologia "Modulo" in "Applicazione" (App) su Backend e Frontend. - Rinomina terminologia "Modulo" in "Applicazione" (App) su Backend e Frontend.
- [2025-12-05 Remove Warehouse Tabs](./devlog/2025-12-05-224000_remove_warehouse_tabs.md) - **Completato**
- Rimozione tab interne e header dal modulo Magazzino per uniformità con la UI principale.
- [2025-12-05 Live Data Alignment](./devlog/2025-12-05-230000_live_data_alignment.md) - **Completato**
- Implementazione `SchemaDiscoveryService` per allineamento automatico dataset report con strutture dati live.

View File

@@ -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.

View File

@@ -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.

View File

@@ -12,11 +12,13 @@ public class ReportsController : ControllerBase
{ {
private readonly ReportGeneratorService _reportGenerator; private readonly ReportGeneratorService _reportGenerator;
private readonly ZentralDbContext _context; private readonly ZentralDbContext _context;
private readonly SchemaDiscoveryService _schemaDiscovery;
public ReportsController(ReportGeneratorService reportGenerator, ZentralDbContext context) public ReportsController(ReportGeneratorService reportGenerator, ZentralDbContext context, SchemaDiscoveryService schemaDiscovery)
{ {
_reportGenerator = reportGenerator; _reportGenerator = reportGenerator;
_context = context; _context = context;
_schemaDiscovery = schemaDiscovery;
} }
/// <summary> /// <summary>
@@ -126,26 +128,7 @@ public class ReportsController : ControllerBase
[HttpGet("datasets")] [HttpGet("datasets")]
public async Task<ActionResult<List<DatasetTypeDto>>> GetAvailableDatasets() public async Task<ActionResult<List<DatasetTypeDto>>> GetAvailableDatasets()
{ {
var datasets = new List<DatasetTypeDto> var datasets = _schemaDiscovery.GetAvailableDatasets();
{
// Dataset principali
new() { Id = "evento", Name = "Evento", Description = "Dati evento completi con cliente, location, ospiti, costi e risorse", Icon = "event", Category = "Principale" },
new() { Id = "cliente", Name = "Cliente", Description = "Anagrafica clienti completa", Icon = "people", Category = "Principale" },
new() { Id = "location", Name = "Location", Description = "Sedi e location eventi", Icon = "place", Category = "Principale" },
new() { Id = "articolo", Name = "Articolo", Description = "Catalogo articoli e materiali", Icon = "inventory", Category = "Principale" },
new() { Id = "risorsa", Name = "Risorsa", Description = "Staff e personale", Icon = "person", Category = "Principale" },
// Dataset di lookup/configurazione
new() { Id = "tipoEvento", Name = "Tipo Evento", Description = "Tipologie di evento (matrimonio, compleanno, etc.)", Icon = "category", Category = "Configurazione" },
new() { Id = "tipoOspite", Name = "Tipo Ospite", Description = "Tipologie di ospiti (adulti, bambini, etc.)", Icon = "groups", Category = "Configurazione" },
new() { Id = "categoria", Name = "Categoria Articoli", Description = "Categorie articoli con coefficienti di calcolo", Icon = "folder", Category = "Configurazione" },
new() { Id = "tipoRisorsa", Name = "Tipo Risorsa", Description = "Tipologie di risorse (cameriere, cuoco, etc.)", Icon = "badge", Category = "Configurazione" },
new() { Id = "tipoMateriale", Name = "Tipo Materiale", Description = "Tipologie di materiali", Icon = "category", Category = "Configurazione" },
// Dataset lista (per report con elenchi)
new() { Id = "listaEventi", Name = "Lista Eventi", Description = "Elenco eventi per report multipli", Icon = "list", Category = "Liste" },
new() { Id = "listaArticoli", Name = "Lista Articoli", Description = "Elenco articoli per catalogo", Icon = "list", Category = "Liste" },
};
// Aggiungi Virtual Dataset dal database // Aggiungi Virtual Dataset dal database
var virtualDatasets = await _context.VirtualDatasets var virtualDatasets = await _context.VirtualDatasets
@@ -173,7 +156,8 @@ public class ReportsController : ControllerBase
[HttpGet("datasets/categories")] [HttpGet("datasets/categories")]
public async Task<ActionResult<List<string>>> GetDatasetCategories() public async Task<ActionResult<List<string>>> GetDatasetCategories()
{ {
var baseCategories = new List<string> { "Principale", "Configurazione", "Liste" }; var datasets = _schemaDiscovery.GetAvailableDatasets();
var baseCategories = datasets.Select(d => d.Category).Distinct().ToList();
// Aggiungi categorie dai Virtual Dataset // Aggiungi categorie dai Virtual Dataset
var virtualCategories = await _context.VirtualDatasets var virtualCategories = await _context.VirtualDatasets
@@ -188,6 +172,8 @@ public class ReportsController : ControllerBase
baseCategories.Add(cat); baseCategories.Add(cat);
} }
baseCategories.Sort();
return baseCategories; return baseCategories;
} }
@@ -207,7 +193,7 @@ public class ReportsController : ControllerBase
return schema; return schema;
} }
var staticSchema = GetSchemaForDataset(datasetId); var staticSchema = _schemaDiscovery.GetSchema(datasetId);
if (staticSchema == null) if (staticSchema == null)
return NotFound($"Dataset '{datasetId}' not found"); return NotFound($"Dataset '{datasetId}' not found");
return staticSchema; return staticSchema;
@@ -230,22 +216,7 @@ public class ReportsController : ControllerBase
return await GetVirtualDatasetEntities(virtualName, search, limit, offset); return await GetVirtualDatasetEntities(virtualName, search, limit, offset);
} }
var entities = datasetId.ToLower() switch return await _schemaDiscovery.GetEntities(datasetId, search, limit, offset);
{
"evento" => await GetEventiEntities(search, limit, offset),
"cliente" => await GetClientiEntities(search, limit, offset),
"location" => await GetLocationEntities(search, limit, offset),
"articolo" => await GetArticoliEntities(search, limit, offset),
"risorsa" => await GetRisorseEntities(search, limit, offset),
"tipoevento" => await GetTipiEventoEntities(search, limit, offset),
"tipoospite" => await GetTipiOspiteEntities(search, limit, offset),
"categoria" => await GetCategorieEntities(search, limit, offset),
"tiporisorsa" => await GetTipiRisorsaEntities(search, limit, offset),
"tipomateriale" => await GetTipiMaterialeEntities(search, limit, offset),
_ => new List<EntityListItemDto>()
};
return entities;
} }
/// <summary> /// <summary>
@@ -254,358 +225,10 @@ public class ReportsController : ControllerBase
[HttpGet("datasets/{datasetId}/count")] [HttpGet("datasets/{datasetId}/count")]
public async Task<ActionResult<int>> GetEntityCount(string datasetId, [FromQuery] string? search = null) public async Task<ActionResult<int>> GetEntityCount(string datasetId, [FromQuery] string? search = null)
{ {
var count = datasetId.ToLower() switch return await _schemaDiscovery.CountEntities(datasetId, search);
{
"evento" => await CountEventi(search),
"cliente" => await CountClienti(search),
"location" => await CountLocation(search),
"articolo" => await CountArticoli(search),
"risorsa" => await CountRisorse(search),
"tipoevento" => await _context.TipiEvento.CountAsync(t => t.Attivo),
"tipoospite" => await _context.TipiOspite.CountAsync(t => t.Attivo),
"categoria" => await _context.CodiciCategoria.CountAsync(c => c.Attivo),
"tiporisorsa" => await _context.TipiRisorsa.CountAsync(t => t.Attivo),
"tipomateriale" => await _context.TipiMateriale.CountAsync(t => t.Attivo),
_ => 0
};
return count;
} }
#region Entity Queries
private async Task<List<EntityListItemDto>> GetEventiEntities(string? search, int limit, int offset)
{
var query = _context.Eventi
.Include(e => e.Cliente)
.Include(e => e.Location)
.Include(e => e.TipoEvento)
.AsQueryable();
if (!string.IsNullOrWhiteSpace(search))
{
search = search.ToLower();
query = query.Where(e =>
(e.Codice != null && e.Codice.ToLower().Contains(search)) ||
(e.Cliente != null && e.Cliente.RagioneSociale.ToLower().Contains(search)) ||
(e.Location != null && e.Location.Nome.ToLower().Contains(search)));
}
return await query
.OrderByDescending(e => e.DataEvento)
.Skip(offset)
.Take(limit)
.Select(e => new EntityListItemDto
{
Id = e.Id,
Label = $"{e.Codice ?? $"EVT-{e.Id}"} - {e.DataEvento:dd/MM/yyyy}",
Description = $"{e.Cliente!.RagioneSociale ?? "N/D"} @ {e.Location!.Nome ?? "N/D"}",
SecondaryInfo = e.TipoEvento != null ? e.TipoEvento.Descrizione : null,
Status = e.Stato.ToString()
})
.ToListAsync();
}
private async Task<int> CountEventi(string? search)
{
var query = _context.Eventi.AsQueryable();
if (!string.IsNullOrWhiteSpace(search))
{
search = search.ToLower();
query = query.Where(e =>
(e.Codice != null && e.Codice.ToLower().Contains(search)) ||
(e.Cliente != null && e.Cliente.RagioneSociale.ToLower().Contains(search)));
}
return await query.CountAsync();
}
private async Task<List<EntityListItemDto>> GetClientiEntities(string? search, int limit, int offset)
{
var query = _context.Clienti.Where(c => c.Attivo);
if (!string.IsNullOrWhiteSpace(search))
{
search = search.ToLower();
query = query.Where(c =>
c.RagioneSociale.ToLower().Contains(search) ||
(c.Citta != null && c.Citta.ToLower().Contains(search)) ||
(c.Email != null && c.Email.ToLower().Contains(search)));
}
return await query
.OrderBy(c => c.RagioneSociale)
.Skip(offset)
.Take(limit)
.Select(c => new EntityListItemDto
{
Id = c.Id,
Label = c.RagioneSociale,
Description = $"{c.Citta ?? "N/D"} - {c.Telefono ?? "N/D"}",
SecondaryInfo = c.Email
})
.ToListAsync();
}
private async Task<int> CountClienti(string? search)
{
var query = _context.Clienti.Where(c => c.Attivo);
if (!string.IsNullOrWhiteSpace(search))
{
search = search.ToLower();
query = query.Where(c => c.RagioneSociale.ToLower().Contains(search));
}
return await query.CountAsync();
}
private async Task<List<EntityListItemDto>> GetLocationEntities(string? search, int limit, int offset)
{
var query = _context.Location.Where(l => l.Attivo);
if (!string.IsNullOrWhiteSpace(search))
{
search = search.ToLower();
query = query.Where(l =>
l.Nome.ToLower().Contains(search) ||
(l.Citta != null && l.Citta.ToLower().Contains(search)));
}
return await query
.OrderBy(l => l.Nome)
.Skip(offset)
.Take(limit)
.Select(l => new EntityListItemDto
{
Id = l.Id,
Label = l.Nome,
Description = $"{l.Citta ?? "N/D"} ({l.Provincia ?? "N/D"})",
SecondaryInfo = l.DistanzaKm.HasValue ? $"{l.DistanzaKm} km" : null
})
.ToListAsync();
}
private async Task<int> CountLocation(string? search)
{
var query = _context.Location.Where(l => l.Attivo);
if (!string.IsNullOrWhiteSpace(search))
{
search = search.ToLower();
query = query.Where(l => l.Nome.ToLower().Contains(search));
}
return await query.CountAsync();
}
private async Task<List<EntityListItemDto>> GetArticoliEntities(string? search, int limit, int offset)
{
var query = _context.Articoli
.Include(a => a.Categoria)
.Where(a => a.Attivo);
if (!string.IsNullOrWhiteSpace(search))
{
search = search.ToLower();
query = query.Where(a =>
a.Codice.ToLower().Contains(search) ||
a.Descrizione.ToLower().Contains(search));
}
return await query
.OrderBy(a => a.Descrizione)
.Skip(offset)
.Take(limit)
.Select(a => new EntityListItemDto
{
Id = a.Id,
Label = $"{a.Codice} - {a.Descrizione}",
Description = $"Disponibile: {a.QtaDisponibile ?? 0}",
SecondaryInfo = a.Categoria != null ? a.Categoria.Descrizione : null
})
.ToListAsync();
}
private async Task<int> CountArticoli(string? search)
{
var query = _context.Articoli.Where(a => a.Attivo);
if (!string.IsNullOrWhiteSpace(search))
{
search = search.ToLower();
query = query.Where(a =>
a.Codice.ToLower().Contains(search) ||
a.Descrizione.ToLower().Contains(search));
}
return await query.CountAsync();
}
private async Task<List<EntityListItemDto>> GetRisorseEntities(string? search, int limit, int offset)
{
var query = _context.Risorse
.Include(r => r.TipoRisorsa)
.Where(r => r.Attivo);
if (!string.IsNullOrWhiteSpace(search))
{
search = search.ToLower();
query = query.Where(r =>
r.Nome.ToLower().Contains(search) ||
(r.Cognome != null && r.Cognome.ToLower().Contains(search)));
}
return await query
.OrderBy(r => r.Cognome).ThenBy(r => r.Nome)
.Skip(offset)
.Take(limit)
.Select(r => new EntityListItemDto
{
Id = r.Id,
Label = $"{r.Nome} {r.Cognome ?? ""}".Trim(),
Description = r.Telefono ?? "",
SecondaryInfo = r.TipoRisorsa != null ? r.TipoRisorsa.Descrizione : null
})
.ToListAsync();
}
private async Task<int> CountRisorse(string? search)
{
var query = _context.Risorse.Where(r => r.Attivo);
if (!string.IsNullOrWhiteSpace(search))
{
search = search.ToLower();
query = query.Where(r =>
r.Nome.ToLower().Contains(search) ||
(r.Cognome != null && r.Cognome.ToLower().Contains(search)));
}
return await query.CountAsync();
}
private async Task<List<EntityListItemDto>> GetTipiEventoEntities(string? search, int limit, int offset)
{
var query = _context.TipiEvento
.Include(t => t.TipoPasto)
.Where(t => t.Attivo);
if (!string.IsNullOrWhiteSpace(search))
{
search = search.ToLower();
query = query.Where(t =>
t.Codice.ToLower().Contains(search) ||
t.Descrizione.ToLower().Contains(search));
}
return await query
.OrderBy(t => t.Descrizione)
.Skip(offset)
.Take(limit)
.Select(t => new EntityListItemDto
{
Id = t.Id,
Label = t.Descrizione,
Description = $"Codice: {t.Codice}",
SecondaryInfo = t.TipoPasto != null ? t.TipoPasto.Descrizione : null
})
.ToListAsync();
}
private async Task<List<EntityListItemDto>> GetTipiOspiteEntities(string? search, int limit, int offset)
{
var query = _context.TipiOspite.Where(t => t.Attivo);
if (!string.IsNullOrWhiteSpace(search))
{
search = search.ToLower();
query = query.Where(t =>
t.Codice.ToLower().Contains(search) ||
t.Descrizione.ToLower().Contains(search));
}
return await query
.OrderBy(t => t.Descrizione)
.Skip(offset)
.Take(limit)
.Select(t => new EntityListItemDto
{
Id = t.Id,
Label = t.Descrizione,
Description = $"Codice: {t.Codice}"
})
.ToListAsync();
}
private async Task<List<EntityListItemDto>> GetCategorieEntities(string? search, int limit, int offset)
{
var query = _context.CodiciCategoria.Where(c => c.Attivo);
if (!string.IsNullOrWhiteSpace(search))
{
search = search.ToLower();
query = query.Where(c =>
c.Codice.ToLower().Contains(search) ||
c.Descrizione.ToLower().Contains(search));
}
return await query
.OrderBy(c => c.Descrizione)
.Skip(offset)
.Take(limit)
.Select(c => new EntityListItemDto
{
Id = c.Id,
Label = c.Descrizione,
Description = $"Codice: {c.Codice}",
SecondaryInfo = $"Coeff: A={c.CoeffA:F2}, B={c.CoeffB:F2}, S={c.CoeffS:F2}"
})
.ToListAsync();
}
private async Task<List<EntityListItemDto>> GetTipiRisorsaEntities(string? search, int limit, int offset)
{
var query = _context.TipiRisorsa.Where(t => t.Attivo);
if (!string.IsNullOrWhiteSpace(search))
{
search = search.ToLower();
query = query.Where(t =>
t.Codice.ToLower().Contains(search) ||
t.Descrizione.ToLower().Contains(search));
}
return await query
.OrderBy(t => t.Descrizione)
.Skip(offset)
.Take(limit)
.Select(t => new EntityListItemDto
{
Id = t.Id,
Label = t.Descrizione,
Description = $"Codice: {t.Codice}"
})
.ToListAsync();
}
private async Task<List<EntityListItemDto>> GetTipiMaterialeEntities(string? search, int limit, int offset)
{
var query = _context.TipiMateriale.Where(t => t.Attivo);
if (!string.IsNullOrWhiteSpace(search))
{
search = search.ToLower();
query = query.Where(t =>
t.Codice.ToLower().Contains(search) ||
t.Descrizione.ToLower().Contains(search));
}
return await query
.OrderBy(t => t.Descrizione)
.Skip(offset)
.Take(limit)
.Select(t => new EntityListItemDto
{
Id = t.Id,
Label = t.Descrizione,
Description = $"Codice: {t.Codice}"
})
.ToListAsync();
}
#endregion
private async Task<Dictionary<string, object>> BuildDataContextAsync(List<DataSourceSelection> dataSources) private async Task<Dictionary<string, object>> BuildDataContextAsync(List<DataSourceSelection> dataSources)
{ {
@@ -642,38 +265,7 @@ public class ReportsController : ControllerBase
private async Task<object?> LoadEntityDataAsync(string datasetId, int entityId) private async Task<object?> LoadEntityDataAsync(string datasetId, int entityId)
{ {
return datasetId.ToLower() switch return await _schemaDiscovery.LoadEntity(datasetId, entityId);
{
"evento" => await _context.Eventi
.Include(e => e.Cliente)
.Include(e => e.Location)
.Include(e => e.TipoEvento).ThenInclude(t => t!.TipoPasto)
.Include(e => e.DettagliOspiti).ThenInclude(d => d.TipoOspite)
.Include(e => e.DettagliPrelievo).ThenInclude(d => d.Articolo).ThenInclude(a => a!.Categoria)
.Include(e => e.DettagliRisorse).ThenInclude(d => d.Risorsa).ThenInclude(r => r!.TipoRisorsa)
.Include(e => e.Acconti)
.Include(e => e.AltriCosti)
.Include(e => e.Degustazioni)
.FirstOrDefaultAsync(e => e.Id == entityId),
"cliente" => await _context.Clienti.FindAsync(entityId),
"location" => await _context.Location.FindAsync(entityId),
"articolo" => await _context.Articoli
.Include(a => a.Categoria)
.Include(a => a.TipoMateriale)
.FirstOrDefaultAsync(a => a.Id == entityId),
"risorsa" => await _context.Risorse
.Include(r => r.TipoRisorsa)
.FirstOrDefaultAsync(r => r.Id == entityId),
"tipoevento" => await _context.TipiEvento
.Include(t => t.TipoPasto)
.FirstOrDefaultAsync(t => t.Id == entityId),
"tipoospite" => await _context.TipiOspite.FindAsync(entityId),
"categoria" => await _context.CodiciCategoria.FindAsync(entityId),
"tiporisorsa" => await _context.TipiRisorsa.FindAsync(entityId),
"tipomateriale" => await _context.TipiMateriale.FindAsync(entityId),
_ => null
};
} }
#region Virtual Dataset Support #region Virtual Dataset Support
@@ -706,7 +298,7 @@ public class ReportsController : ControllerBase
if (source == null) continue; if (source == null) continue;
// Ottieni lo schema del dataset sorgente per determinare il tipo // Ottieni lo schema del dataset sorgente per determinare il tipo
var sourceSchema = GetSchemaForDataset(source.DatasetId); var sourceSchema = _schemaDiscovery.GetSchema(source.DatasetId);
var sourceField = sourceSchema?.Fields.FirstOrDefault(f => var sourceField = sourceSchema?.Fields.FirstOrDefault(f =>
f.Name.Equals(outputField.FieldName, StringComparison.OrdinalIgnoreCase)); f.Name.Equals(outputField.FieldName, StringComparison.OrdinalIgnoreCase));
@@ -724,7 +316,7 @@ public class ReportsController : ControllerBase
// Se non ci sono OutputFields, includi tutti i campi di tutte le sorgenti // Se non ci sono OutputFields, includi tutti i campi di tutte le sorgenti
foreach (var source in config.Sources) foreach (var source in config.Sources)
{ {
var sourceSchema = GetSchemaForDataset(source.DatasetId); var sourceSchema = _schemaDiscovery.GetSchema(source.DatasetId);
if (sourceSchema == null) continue; if (sourceSchema == null) continue;
foreach (var field in sourceSchema.Fields) foreach (var field in sourceSchema.Fields)
@@ -773,15 +365,7 @@ public class ReportsController : ControllerBase
if (primarySource == null) return new List<EntityListItemDto>(); if (primarySource == null) return new List<EntityListItemDto>();
// Restituisce le entità del dataset primario // Restituisce le entità del dataset primario
return primarySource.DatasetId.ToLower() switch return await _schemaDiscovery.GetEntities(primarySource.DatasetId, search, limit, offset);
{
"evento" => await GetEventiEntities(search, limit, offset),
"cliente" => await GetClientiEntities(search, limit, offset),
"location" => await GetLocationEntities(search, limit, offset),
"articolo" => await GetArticoliEntities(search, limit, offset),
"risorsa" => await GetRisorseEntities(search, limit, offset),
_ => new List<EntityListItemDto>()
};
} }
/// <summary> /// <summary>
@@ -881,349 +465,7 @@ public class ReportsController : ControllerBase
#endregion #endregion
private static DataSchemaDto? GetSchemaForDataset(string datasetId)
{
return datasetId.ToLower() switch
{
"evento" => GetEventoSchema(),
"cliente" => GetClienteSchema(),
"location" => GetLocationSchema(),
"articolo" => GetArticoloSchema(),
"risorsa" => GetRisorsaSchema(),
"tipoevento" => GetTipoEventoSchema(),
"tipoospite" => GetTipoOspiteSchema(),
"categoria" => GetCategoriaSchema(),
"tiporisorsa" => GetTipoRisorsaSchema(),
"tipomateriale" => GetTipoMaterialeSchema(),
_ => null
};
}
#region Schema Definitions
private static DataSchemaDto GetEventoSchema() => new()
{
EntityType = "Evento",
DatasetId = "evento",
Fields = new List<DataFieldDto>
{
// Campi base
new() { Name = "id", Type = "number", Label = "ID", Group = "Base" },
new() { Name = "codice", Type = "string", Label = "Codice Evento", Group = "Base" },
new() { Name = "dataEvento", Type = "date", Label = "Data Evento", Group = "Base" },
new() { Name = "oraInizio", Type = "time", Label = "Ora Inizio", Group = "Base" },
new() { Name = "oraFine", Type = "time", Label = "Ora Fine", Group = "Base" },
new() { Name = "descrizione", Type = "string", Label = "Descrizione", Group = "Base" },
new() { Name = "stato", Type = "number", Label = "Stato (0=Scheda, 10=Preventivo, 20=Confermato)", Group = "Base" },
new() { Name = "confermato", Type = "boolean", Label = "Confermato", Group = "Base" },
// Ospiti
new() { Name = "numeroOspiti", Type = "number", Label = "Numero Ospiti Totale", Group = "Ospiti" },
new() { Name = "numeroOspitiAdulti", Type = "number", Label = "Ospiti Adulti", Group = "Ospiti" },
new() { Name = "numeroOspitiBambini", Type = "number", Label = "Ospiti Bambini", Group = "Ospiti" },
new() { Name = "numeroOspitiSeduti", Type = "number", Label = "Ospiti Seduti", Group = "Ospiti" },
new() { Name = "numeroOspitiBuffet", Type = "number", Label = "Ospiti Buffet", Group = "Ospiti" },
// Economici
new() { Name = "costoTotale", Type = "currency", Label = "Costo Totale", Group = "Economici" },
new() { Name = "costoPersona", Type = "currency", Label = "Costo per Persona", Group = "Economici" },
new() { Name = "totaleAcconti", Type = "currency", Label = "Totale Acconti", Group = "Economici" },
new() { Name = "saldo", Type = "currency", Label = "Saldo da Pagare", Group = "Economici" },
new() { Name = "dataScadenzaPreventivo", Type = "date", Label = "Scadenza Preventivo", Group = "Economici" },
// Note
new() { Name = "noteCliente", Type = "string", Label = "Note Cliente", Group = "Note" },
new() { Name = "noteInterne", Type = "string", Label = "Note Interne", Group = "Note" },
new() { Name = "noteCucina", Type = "string", Label = "Note Cucina", Group = "Note" },
new() { Name = "noteAllestimento", Type = "string", Label = "Note Allestimento", Group = "Note" },
// Cliente (relazione)
new() { Name = "cliente.id", Type = "number", Label = "ID Cliente", Group = "Cliente" },
new() { Name = "cliente.ragioneSociale", Type = "string", Label = "Ragione Sociale", Group = "Cliente" },
new() { Name = "cliente.indirizzo", Type = "string", Label = "Indirizzo Cliente", Group = "Cliente" },
new() { Name = "cliente.cap", Type = "string", Label = "CAP Cliente", Group = "Cliente" },
new() { Name = "cliente.citta", Type = "string", Label = "Città Cliente", Group = "Cliente" },
new() { Name = "cliente.provincia", Type = "string", Label = "Provincia Cliente", Group = "Cliente" },
new() { Name = "cliente.telefono", Type = "string", Label = "Telefono Cliente", Group = "Cliente" },
new() { Name = "cliente.email", Type = "string", Label = "Email Cliente", Group = "Cliente" },
new() { Name = "cliente.pec", Type = "string", Label = "PEC Cliente", Group = "Cliente" },
new() { Name = "cliente.codiceFiscale", Type = "string", Label = "Codice Fiscale", Group = "Cliente" },
new() { Name = "cliente.partitaIva", Type = "string", Label = "Partita IVA", Group = "Cliente" },
new() { Name = "cliente.codiceDestinatario", Type = "string", Label = "Codice SDI", Group = "Cliente" },
// Location (relazione)
new() { Name = "location.id", Type = "number", Label = "ID Location", Group = "Location" },
new() { Name = "location.nome", Type = "string", Label = "Nome Location", Group = "Location" },
new() { Name = "location.indirizzo", Type = "string", Label = "Indirizzo Location", Group = "Location" },
new() { Name = "location.cap", Type = "string", Label = "CAP Location", Group = "Location" },
new() { Name = "location.citta", Type = "string", Label = "Città Location", Group = "Location" },
new() { Name = "location.provincia", Type = "string", Label = "Provincia Location", Group = "Location" },
new() { Name = "location.telefono", Type = "string", Label = "Telefono Location", Group = "Location" },
new() { Name = "location.email", Type = "string", Label = "Email Location", Group = "Location" },
new() { Name = "location.referente", Type = "string", Label = "Referente Location", Group = "Location" },
new() { Name = "location.distanzaKm", Type = "number", Label = "Distanza (km)", Group = "Location" },
// Tipo Evento (relazione)
new() { Name = "tipoEvento.codice", Type = "string", Label = "Codice Tipo Evento", Group = "Tipo Evento" },
new() { Name = "tipoEvento.descrizione", Type = "string", Label = "Tipo Evento", Group = "Tipo Evento" },
new() { Name = "tipoEvento.tipoPasto.descrizione", Type = "string", Label = "Tipo Pasto", Group = "Tipo Evento" },
},
ChildCollections = new List<DataCollectionDto>
{
new()
{
Name = "dettagliOspiti",
Label = "Dettaglio Ospiti",
Description = "Breakdown ospiti per tipologia",
Fields = new List<DataFieldDto>
{
new() { Name = "tipoOspite.codice", Type = "string", Label = "Codice Tipo" },
new() { Name = "tipoOspite.descrizione", Type = "string", Label = "Tipo Ospite" },
new() { Name = "numero", Type = "number", Label = "Numero" },
new() { Name = "costoUnitario", Type = "currency", Label = "Costo Unitario" },
new() { Name = "sconto", Type = "percent", Label = "Sconto %" },
new() { Name = "totale", Type = "currency", Label = "Totale" },
new() { Name = "note", Type = "string", Label = "Note" }
}
},
new()
{
Name = "altriCosti",
Label = "Altri Costi",
Description = "Costi aggiuntivi dell'evento",
Fields = new List<DataFieldDto>
{
new() { Name = "descrizione", Type = "string", Label = "Descrizione" },
new() { Name = "costoUnitario", Type = "currency", Label = "Costo Unitario" },
new() { Name = "quantita", Type = "number", Label = "Quantità" },
new() { Name = "aliquotaIva", Type = "percent", Label = "Aliquota IVA" },
new() { Name = "totale", Type = "currency", Label = "Totale" }
}
},
new()
{
Name = "dettagliRisorse",
Label = "Risorse Assegnate",
Description = "Personale assegnato all'evento",
Fields = new List<DataFieldDto>
{
new() { Name = "risorsa.nome", Type = "string", Label = "Nome" },
new() { Name = "risorsa.cognome", Type = "string", Label = "Cognome" },
new() { Name = "risorsa.telefono", Type = "string", Label = "Telefono" },
new() { Name = "risorsa.tipoRisorsa.descrizione", Type = "string", Label = "Ruolo" },
new() { Name = "oraInizio", Type = "time", Label = "Ora Inizio" },
new() { Name = "oraFine", Type = "time", Label = "Ora Fine" },
new() { Name = "oreLavoro", Type = "number", Label = "Ore Lavoro" },
new() { Name = "costo", Type = "currency", Label = "Costo" },
new() { Name = "note", Type = "string", Label = "Note" }
}
},
new()
{
Name = "acconti",
Label = "Acconti",
Description = "Pagamenti anticipati ricevuti",
Fields = new List<DataFieldDto>
{
new() { Name = "descrizione", Type = "string", Label = "Descrizione" },
new() { Name = "importo", Type = "currency", Label = "Importo" },
new() { Name = "dataPagamento", Type = "date", Label = "Data Pagamento" },
new() { Name = "metodoPagamento", Type = "string", Label = "Metodo Pagamento" },
new() { Name = "note", Type = "string", Label = "Note" }
}
},
new()
{
Name = "dettagliPrelievo",
Label = "Lista Prelievo",
Description = "Articoli necessari per l'evento",
Fields = new List<DataFieldDto>
{
new() { Name = "articolo.codice", Type = "string", Label = "Codice Articolo" },
new() { Name = "articolo.descrizione", Type = "string", Label = "Descrizione" },
new() { Name = "articolo.categoria.descrizione", Type = "string", Label = "Categoria" },
new() { Name = "articolo.unitaMisura", Type = "string", Label = "U.M." },
new() { Name = "qtaRichiesta", Type = "number", Label = "Qtà Richiesta" },
new() { Name = "qtaCalcolata", Type = "number", Label = "Qtà Calcolata" },
new() { Name = "qtaEffettiva", Type = "number", Label = "Qtà Effettiva" },
new() { Name = "note", Type = "string", Label = "Note" }
}
},
new()
{
Name = "degustazioni",
Label = "Degustazioni",
Description = "Degustazioni programmate",
Fields = new List<DataFieldDto>
{
new() { Name = "data", Type = "date", Label = "Data" },
new() { Name = "ora", Type = "time", Label = "Ora" },
new() { Name = "numeroPartecipanti", Type = "number", Label = "Partecipanti" },
new() { Name = "note", Type = "string", Label = "Note" }
}
}
}
};
private static DataSchemaDto GetClienteSchema() => new()
{
EntityType = "Cliente",
DatasetId = "cliente",
Fields = new List<DataFieldDto>
{
new() { Name = "id", Type = "number", Label = "ID", Group = "Base" },
new() { Name = "ragioneSociale", Type = "string", Label = "Ragione Sociale", Group = "Base" },
new() { Name = "indirizzo", Type = "string", Label = "Indirizzo", Group = "Indirizzo" },
new() { Name = "cap", Type = "string", Label = "CAP", Group = "Indirizzo" },
new() { Name = "citta", Type = "string", Label = "Città", Group = "Indirizzo" },
new() { Name = "provincia", Type = "string", Label = "Provincia", Group = "Indirizzo" },
new() { Name = "telefono", Type = "string", Label = "Telefono", Group = "Contatti" },
new() { Name = "email", Type = "string", Label = "Email", Group = "Contatti" },
new() { Name = "pec", Type = "string", Label = "PEC", Group = "Contatti" },
new() { Name = "codiceFiscale", Type = "string", Label = "Codice Fiscale", Group = "Fiscale" },
new() { Name = "partitaIva", Type = "string", Label = "Partita IVA", Group = "Fiscale" },
new() { Name = "codiceDestinatario", Type = "string", Label = "Codice Destinatario SDI", Group = "Fiscale" },
new() { Name = "note", Type = "string", Label = "Note", Group = "Base" },
},
ChildCollections = new List<DataCollectionDto>()
};
private static DataSchemaDto GetLocationSchema() => new()
{
EntityType = "Location",
DatasetId = "location",
Fields = new List<DataFieldDto>
{
new() { Name = "id", Type = "number", Label = "ID", Group = "Base" },
new() { Name = "nome", Type = "string", Label = "Nome", Group = "Base" },
new() { Name = "indirizzo", Type = "string", Label = "Indirizzo", Group = "Indirizzo" },
new() { Name = "cap", Type = "string", Label = "CAP", Group = "Indirizzo" },
new() { Name = "citta", Type = "string", Label = "Città", Group = "Indirizzo" },
new() { Name = "provincia", Type = "string", Label = "Provincia", Group = "Indirizzo" },
new() { Name = "telefono", Type = "string", Label = "Telefono", Group = "Contatti" },
new() { Name = "email", Type = "string", Label = "Email", Group = "Contatti" },
new() { Name = "referente", Type = "string", Label = "Referente", Group = "Contatti" },
new() { Name = "distanzaKm", Type = "number", Label = "Distanza (km)", Group = "Base" },
new() { Name = "note", Type = "string", Label = "Note", Group = "Base" },
},
ChildCollections = new List<DataCollectionDto>()
};
private static DataSchemaDto GetArticoloSchema() => new()
{
EntityType = "Articolo",
DatasetId = "articolo",
Fields = new List<DataFieldDto>
{
new() { Name = "id", Type = "number", Label = "ID", Group = "Base" },
new() { Name = "codice", Type = "string", Label = "Codice", Group = "Base" },
new() { Name = "descrizione", Type = "string", Label = "Descrizione", Group = "Base" },
new() { Name = "unitaMisura", Type = "string", Label = "Unità di Misura", Group = "Base" },
new() { Name = "qtaDisponibile", Type = "number", Label = "Quantità Disponibile", Group = "Quantità" },
new() { Name = "qtaStdA", Type = "number", Label = "Qtà Standard Adulti", Group = "Quantità" },
new() { Name = "qtaStdB", Type = "number", Label = "Qtà Standard Buffet", Group = "Quantità" },
new() { Name = "qtaStdS", Type = "number", Label = "Qtà Standard Seduti", Group = "Quantità" },
new() { Name = "categoria.codice", Type = "string", Label = "Codice Categoria", Group = "Categoria" },
new() { Name = "categoria.descrizione", Type = "string", Label = "Categoria", Group = "Categoria" },
new() { Name = "categoria.coeffA", Type = "number", Label = "Coefficiente Adulti", Group = "Categoria" },
new() { Name = "categoria.coeffB", Type = "number", Label = "Coefficiente Buffet", Group = "Categoria" },
new() { Name = "categoria.coeffS", Type = "number", Label = "Coefficiente Seduti", Group = "Categoria" },
new() { Name = "tipoMateriale.codice", Type = "string", Label = "Codice Tipo Materiale", Group = "Materiale" },
new() { Name = "tipoMateriale.descrizione", Type = "string", Label = "Tipo Materiale", Group = "Materiale" },
new() { Name = "note", Type = "string", Label = "Note", Group = "Base" },
},
ChildCollections = new List<DataCollectionDto>()
};
private static DataSchemaDto GetRisorsaSchema() => new()
{
EntityType = "Risorsa",
DatasetId = "risorsa",
Fields = new List<DataFieldDto>
{
new() { Name = "id", Type = "number", Label = "ID", Group = "Base" },
new() { Name = "nome", Type = "string", Label = "Nome", Group = "Base" },
new() { Name = "cognome", Type = "string", Label = "Cognome", Group = "Base" },
new() { Name = "telefono", Type = "string", Label = "Telefono", Group = "Contatti" },
new() { Name = "email", Type = "string", Label = "Email", Group = "Contatti" },
new() { Name = "tipoRisorsa.codice", Type = "string", Label = "Codice Tipo", Group = "Tipo" },
new() { Name = "tipoRisorsa.descrizione", Type = "string", Label = "Tipo Risorsa", Group = "Tipo" },
new() { Name = "note", Type = "string", Label = "Note", Group = "Base" },
},
ChildCollections = new List<DataCollectionDto>()
};
private static DataSchemaDto GetTipoEventoSchema() => new()
{
EntityType = "Tipo Evento",
DatasetId = "tipoEvento",
Fields = new List<DataFieldDto>
{
new() { Name = "id", Type = "number", Label = "ID" },
new() { Name = "codice", Type = "string", Label = "Codice" },
new() { Name = "descrizione", Type = "string", Label = "Descrizione" },
new() { Name = "tipoPasto.codice", Type = "string", Label = "Codice Tipo Pasto" },
new() { Name = "tipoPasto.descrizione", Type = "string", Label = "Tipo Pasto" },
},
ChildCollections = new List<DataCollectionDto>()
};
private static DataSchemaDto GetTipoOspiteSchema() => new()
{
EntityType = "Tipo Ospite",
DatasetId = "tipoOspite",
Fields = new List<DataFieldDto>
{
new() { Name = "id", Type = "number", Label = "ID" },
new() { Name = "codice", Type = "string", Label = "Codice" },
new() { Name = "descrizione", Type = "string", Label = "Descrizione" },
},
ChildCollections = new List<DataCollectionDto>()
};
private static DataSchemaDto GetCategoriaSchema() => new()
{
EntityType = "Categoria Articoli",
DatasetId = "categoria",
Fields = new List<DataFieldDto>
{
new() { Name = "id", Type = "number", Label = "ID" },
new() { Name = "codice", Type = "string", Label = "Codice" },
new() { Name = "descrizione", Type = "string", Label = "Descrizione" },
new() { Name = "coeffA", Type = "number", Label = "Coefficiente Adulti" },
new() { Name = "coeffB", Type = "number", Label = "Coefficiente Buffet" },
new() { Name = "coeffS", Type = "number", Label = "Coefficiente Seduti" },
},
ChildCollections = new List<DataCollectionDto>()
};
private static DataSchemaDto GetTipoRisorsaSchema() => new()
{
EntityType = "Tipo Risorsa",
DatasetId = "tipoRisorsa",
Fields = new List<DataFieldDto>
{
new() { Name = "id", Type = "number", Label = "ID" },
new() { Name = "codice", Type = "string", Label = "Codice" },
new() { Name = "descrizione", Type = "string", Label = "Descrizione" },
},
ChildCollections = new List<DataCollectionDto>()
};
private static DataSchemaDto GetTipoMaterialeSchema() => new()
{
EntityType = "Tipo Materiale",
DatasetId = "tipoMateriale",
Fields = new List<DataFieldDto>
{
new() { Name = "id", Type = "number", Label = "ID" },
new() { Name = "codice", Type = "string", Label = "Codice" },
new() { Name = "descrizione", Type = "string", Label = "Descrizione" },
},
ChildCollections = new List<DataCollectionDto>()
};
#endregion
} }
// DTOs moved to AprtModels.cs // DTOs moved to AprtModels.cs

View File

@@ -12,10 +12,12 @@ namespace Zentral.API.Controllers;
public class VirtualDatasetsController : ControllerBase public class VirtualDatasetsController : ControllerBase
{ {
private readonly ZentralDbContext _context; private readonly ZentralDbContext _context;
private readonly SchemaDiscoveryService _schemaDiscovery;
public VirtualDatasetsController(ZentralDbContext context) public VirtualDatasetsController(ZentralDbContext context, SchemaDiscoveryService schemaDiscovery)
{ {
_context = context; _context = context;
_schemaDiscovery = schemaDiscovery;
} }
/// <summary> /// <summary>
@@ -365,11 +367,19 @@ public class VirtualDatasetsController : ControllerBase
var source = config.Sources.FirstOrDefault(s => s.Id == outputField.SourceId); var source = config.Sources.FirstOrDefault(s => s.Id == outputField.SourceId);
if (source == null) continue; if (source == null) continue;
var sourceSchema = _schemaDiscovery.GetSchema(source.DatasetId);
var fieldType = "string";
if (sourceSchema != null)
{
var sourceField = sourceSchema.Fields.FirstOrDefault(f => f.Name.Equals(outputField.FieldName, StringComparison.OrdinalIgnoreCase));
if (sourceField != null) fieldType = sourceField.Type;
}
fields.Add(new DataFieldDto fields.Add(new DataFieldDto
{ {
Name = outputField.Alias ?? $"{source.Alias}.{outputField.FieldName}", Name = outputField.Alias ?? $"{source.Alias}.{outputField.FieldName}",
Label = outputField.Label ?? outputField.FieldName, Label = outputField.Label ?? outputField.FieldName,
Type = "string", // TODO: determinare il tipo dal dataset sorgente Type = fieldType,
Group = outputField.Group ?? source.Alias Group = outputField.Group ?? source.Alias
}); });
} }
@@ -379,7 +389,7 @@ public class VirtualDatasetsController : ControllerBase
// Altrimenti, includi tutti i campi da tutte le sorgenti // Altrimenti, includi tutti i campi da tutte le sorgenti
foreach (var source in config.Sources) foreach (var source in config.Sources)
{ {
var sourceSchema = GetBaseDatasetSchema(source.DatasetId); var sourceSchema = _schemaDiscovery.GetSchema(source.DatasetId);
if (sourceSchema == null) continue; if (sourceSchema == null) continue;
foreach (var field in sourceSchema.Fields) foreach (var field in sourceSchema.Fields)
@@ -427,91 +437,7 @@ public class VirtualDatasetsController : ControllerBase
}); });
} }
private DataSchemaDto? GetBaseDatasetSchema(string datasetId)
{
// Riutilizza la logica di ReportsController per ottenere lo schema base
// TODO: estrarre in un servizio condiviso
return datasetId.ToLower() switch
{
"evento" => new DataSchemaDto
{
EntityType = "Evento",
DatasetId = "evento",
Fields = new List<DataFieldDto>
{
new() { Name = "id", Type = "number", Label = "ID" },
new() { Name = "codice", Type = "string", Label = "Codice" },
new() { Name = "dataEvento", Type = "date", Label = "Data Evento" },
new() { Name = "descrizione", Type = "string", Label = "Descrizione" },
new() { Name = "stato", Type = "number", Label = "Stato" },
new() { Name = "numeroOspiti", Type = "number", Label = "Numero Ospiti" },
new() { Name = "costoTotale", Type = "currency", Label = "Costo Totale" },
new() { Name = "clienteId", Type = "number", Label = "ID Cliente" },
new() { Name = "locationId", Type = "number", Label = "ID Location" },
},
ChildCollections = new List<DataCollectionDto>()
},
"cliente" => new DataSchemaDto
{
EntityType = "Cliente",
DatasetId = "cliente",
Fields = new List<DataFieldDto>
{
new() { Name = "id", Type = "number", Label = "ID" },
new() { Name = "ragioneSociale", Type = "string", Label = "Ragione Sociale" },
new() { Name = "indirizzo", Type = "string", Label = "Indirizzo" },
new() { Name = "citta", Type = "string", Label = "Città" },
new() { Name = "telefono", Type = "string", Label = "Telefono" },
new() { Name = "email", Type = "string", Label = "Email" },
new() { Name = "partitaIva", Type = "string", Label = "Partita IVA" },
},
ChildCollections = new List<DataCollectionDto>()
},
"location" => new DataSchemaDto
{
EntityType = "Location",
DatasetId = "location",
Fields = new List<DataFieldDto>
{
new() { Name = "id", Type = "number", Label = "ID" },
new() { Name = "nome", Type = "string", Label = "Nome" },
new() { Name = "indirizzo", Type = "string", Label = "Indirizzo" },
new() { Name = "citta", Type = "string", Label = "Città" },
new() { Name = "distanzaKm", Type = "number", Label = "Distanza (km)" },
},
ChildCollections = new List<DataCollectionDto>()
},
"articolo" => new DataSchemaDto
{
EntityType = "Articolo",
DatasetId = "articolo",
Fields = new List<DataFieldDto>
{
new() { Name = "id", Type = "number", Label = "ID" },
new() { Name = "codice", Type = "string", Label = "Codice" },
new() { Name = "descrizione", Type = "string", Label = "Descrizione" },
new() { Name = "qtaDisponibile", Type = "number", Label = "Qtà Disponibile" },
new() { Name = "categoriaId", Type = "number", Label = "ID Categoria" },
},
ChildCollections = new List<DataCollectionDto>()
},
"risorsa" => new DataSchemaDto
{
EntityType = "Risorsa",
DatasetId = "risorsa",
Fields = new List<DataFieldDto>
{
new() { Name = "id", Type = "number", Label = "ID" },
new() { Name = "nome", Type = "string", Label = "Nome" },
new() { Name = "cognome", Type = "string", Label = "Cognome" },
new() { Name = "telefono", Type = "string", Label = "Telefono" },
new() { Name = "tipoRisorsaId", Type = "number", Label = "ID Tipo Risorsa" },
},
ChildCollections = new List<DataCollectionDto>()
},
_ => null
};
}
} }
// DTOs // DTOs

View File

@@ -22,6 +22,7 @@ builder.Services.AddDbContext<ZentralDbContext>(options =>
builder.Services.AddScoped<EventoCostiService>(); builder.Services.AddScoped<EventoCostiService>();
builder.Services.AddScoped<DemoDataService>(); builder.Services.AddScoped<DemoDataService>();
builder.Services.AddScoped<ReportGeneratorService>(); builder.Services.AddScoped<ReportGeneratorService>();
builder.Services.AddScoped<SchemaDiscoveryService>();
builder.Services.AddScoped<AppService>(); builder.Services.AddScoped<AppService>();
builder.Services.AddScoped<AutoCodeService>(); builder.Services.AddScoped<AutoCodeService>();
builder.Services.AddScoped<CustomFieldService>(); builder.Services.AddScoped<CustomFieldService>();

View File

@@ -0,0 +1,397 @@
using System.Collections;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Zentral.Domain.Entities;
using Zentral.Infrastructure.Data;
namespace Zentral.API.Services.Reports;
public class SchemaDiscoveryService
{
private readonly ZentralDbContext _context;
public SchemaDiscoveryService(ZentralDbContext context)
{
_context = context;
}
private readonly Dictionary<string, (string Name, string Description, string Icon, string Category)> _datasetMetadata = new()
{
{ "Evento", ("Evento", "Dati evento completi con cliente, location, ospiti, costi e risorse", "event", "Principale") },
{ "Cliente", ("Cliente", "Anagrafica clienti completa", "people", "Principale") },
{ "Location", ("Location", "Sedi e location eventi", "place", "Principale") },
{ "Articolo", ("Articolo", "Catalogo articoli e materiali", "inventory", "Principale") },
{ "Risorsa", ("Risorsa", "Staff e personale", "person", "Principale") },
{ "TipoEvento", ("Tipo Evento", "Tipologie di evento (matrimonio, compleanno, etc.)", "category", "Configurazione") },
{ "TipoOspite", ("Tipo Ospite", "Tipologie di ospiti (adulti, bambini, etc.)", "groups", "Configurazione") },
{ "CodiceCategoria", ("Categoria Articoli", "Categorie articoli con coefficienti di calcolo", "folder", "Configurazione") },
{ "TipoRisorsa", ("Tipo Risorsa", "Tipologie di risorse (cameriere, cuoco, etc.)", "badge", "Configurazione") },
{ "TipoMateriale", ("Tipo Materiale", "Tipologie di materiali", "category", "Configurazione") }
};
public List<DatasetTypeDto> GetAvailableDatasets()
{
var datasets = new List<DatasetTypeDto>();
var properties = typeof(ZentralDbContext).GetProperties()
.Where(p => p.PropertyType.IsGenericType &&
p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>));
foreach (var prop in properties)
{
var entityType = prop.PropertyType.GetGenericArguments()[0];
// Skip join tables or non-entity types if necessary
if (entityType.Name.Contains("Dettaglio") || entityType.Name.Contains("Link"))
continue;
var datasetId = ToCamelCase(entityType.Name);
// Default values
var name = SplitCamelCase(entityType.Name);
var description = $"Dataset {entityType.Name}";
var icon = GetIconForType(entityType.Name);
var category = "Principale";
// Determine category from namespace if not in metadata
if (entityType.Namespace != null)
{
var parts = entityType.Namespace.Split('.');
if (parts.Length > 3 && parts[2] == "Entities")
{
category = parts[3];
}
}
// Apply metadata if available
if (_datasetMetadata.TryGetValue(entityType.Name, out var meta))
{
name = meta.Name;
description = meta.Description;
icon = meta.Icon;
category = meta.Category;
}
datasets.Add(new DatasetTypeDto
{
Id = datasetId,
Name = name,
Description = description,
Icon = icon,
Category = category,
IsVirtual = false
});
}
return datasets.OrderBy(d => d.Category).ThenBy(d => d.Name).ToList();
}
public DataSchemaDto? GetSchema(string datasetId)
{
var type = GetTypeForDataset(datasetId);
if (type == null) return null;
var fields = new List<DataFieldDto>();
var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead);
foreach (var prop in props)
{
// Skip collections for fields, handle them as child collections if needed
if (typeof(IEnumerable).IsAssignableFrom(prop.PropertyType) && prop.PropertyType != typeof(string))
continue;
// Skip complex types that are not mapped as owned types (simplification)
if (!IsSimpleType(prop.PropertyType))
continue;
fields.Add(new DataFieldDto
{
Name = ToCamelCase(prop.Name),
Label = SplitCamelCase(prop.Name),
Type = MapType(prop.PropertyType),
Group = "Fields"
});
}
return new DataSchemaDto
{
EntityType = type.Name,
DatasetId = datasetId,
Fields = fields,
ChildCollections = new List<DataCollectionDto>()
};
}
public async Task<List<EntityListItemDto>> GetEntities(string datasetId, string? search, int limit, int offset)
{
var type = GetTypeForDataset(datasetId);
if (type == null) return new List<EntityListItemDto>();
var method = this.GetType().GetMethod("GetEntitiesGeneric", BindingFlags.NonPublic | BindingFlags.Instance)!
.MakeGenericMethod(type);
var task = (Task<List<EntityListItemDto>>)method.Invoke(this, new object[] { search, limit, offset })!;
return await task;
}
public async Task<int> CountEntities(string datasetId, string? search)
{
var type = GetTypeForDataset(datasetId);
if (type == null) return 0;
var method = this.GetType().GetMethod("CountEntitiesGeneric", BindingFlags.NonPublic | BindingFlags.Instance)!
.MakeGenericMethod(type);
var task = (Task<int>)method.Invoke(this, new object[] { search })!;
return await task;
}
public async Task<object?> LoadEntity(string datasetId, int id)
{
var type = GetTypeForDataset(datasetId);
if (type == null) return null;
var method = this.GetType().GetMethod("LoadEntityGeneric", BindingFlags.NonPublic | BindingFlags.Instance)!
.MakeGenericMethod(type);
var task = (Task<object?>)method.Invoke(this, new object[] { id })!;
return await task;
}
// Generic implementations
private async Task<object?> LoadEntityGeneric<T>(int id) where T : class
{
var query = _context.Set<T>().AsQueryable();
// Eager load all navigation properties
var entityType = _context.Model.FindEntityType(typeof(T));
if (entityType != null)
{
foreach (var nav in entityType.GetNavigations())
{
query = query.Include(nav.Name);
}
}
return await query.FirstOrDefaultAsync(e => EF.Property<int>(e, "Id") == id);
}
private async Task<List<EntityListItemDto>> GetEntitiesGeneric<T>(string? search, int limit, int offset) where T : class
{
var query = _context.Set<T>().AsQueryable();
// Apply search if possible
if (!string.IsNullOrWhiteSpace(search))
{
var predicate = BuildSearchPredicate<T>(search);
if (predicate != null)
{
query = query.Where(predicate);
}
}
// Order by Label property if available (Alphabetical), otherwise by Id Descending
var labelProp = GetLabelProperty(typeof(T));
if (labelProp != null)
{
query = query.OrderBy(e => EF.Property<string>(e, labelProp.Name));
}
else
{
var idProp = typeof(T).GetProperty("Id");
if (idProp != null)
{
query = query.OrderByDescending(e => EF.Property<object>(e, "Id"));
}
}
var list = await query.Skip(offset).Take(limit).ToListAsync();
return list.Select(item => MapToListItem(item)).ToList();
}
private async Task<int> CountEntitiesGeneric<T>(string? search) where T : class
{
var query = _context.Set<T>().AsQueryable();
if (!string.IsNullOrWhiteSpace(search))
{
var predicate = BuildSearchPredicate<T>(search);
if (predicate != null)
{
query = query.Where(predicate);
}
}
return await query.CountAsync();
}
// Helpers
private Type? GetTypeForDataset(string datasetId)
{
// Case insensitive match
var properties = typeof(ZentralDbContext).GetProperties()
.Where(p => p.PropertyType.IsGenericType &&
p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>));
foreach (var prop in properties)
{
var entityType = prop.PropertyType.GetGenericArguments()[0];
if (entityType.Name.Equals(datasetId, StringComparison.OrdinalIgnoreCase))
{
return entityType;
}
}
return null;
}
private Expression<Func<T, bool>>? BuildSearchPredicate<T>(string search)
{
var parameter = Expression.Parameter(typeof(T), "e");
var searchLower = Expression.Constant(search.ToLower());
var stringProps = typeof(T).GetProperties()
.Where(p => p.PropertyType == typeof(string) && p.CanRead)
.Take(3) // Limit to first 3 string properties to avoid huge queries
.ToList();
if (!stringProps.Any()) return null;
Expression? body = null;
var containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) });
var toLowerMethod = typeof(string).GetMethod("ToLower", Type.EmptyTypes);
foreach (var prop in stringProps)
{
// e.Prop
var propExp = Expression.Property(parameter, prop);
// e.Prop != null
var notNull = Expression.NotEqual(propExp, Expression.Constant(null));
// e.Prop.ToLower()
var toLower = Expression.Call(propExp, toLowerMethod!);
// e.Prop.ToLower().Contains(search)
var contains = Expression.Call(toLower, containsMethod!, searchLower);
// e.Prop != null && e.Prop.ToLower().Contains(search)
var condition = Expression.AndAlso(notNull, contains);
body = body == null ? condition : Expression.OrElse(body, condition);
}
return body == null ? null : Expression.Lambda<Func<T, bool>>(body, parameter);
}
private EntityListItemDto MapToListItem(object item)
{
var type = item.GetType();
var idProp = type.GetProperty("Id");
var id = idProp?.GetValue(item) as int? ?? 0;
// Use the best label property
var labelProp = GetLabelProperty(type);
var label = labelProp?.GetValue(item)?.ToString();
// Fallback to Codice if label is empty and we haven't tried Codice yet
if (string.IsNullOrWhiteSpace(label) && labelProp?.Name != "Codice")
{
var codiceProp = type.GetProperty("Codice", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
label = codiceProp?.GetValue(item)?.ToString();
}
// Final fallback
if (string.IsNullOrWhiteSpace(label))
{
label = $"Item {id}";
}
// Description: try to find a secondary useful field
var description = "";
if (labelProp?.Name != "Descrizione")
{
var descProp = type.GetProperty("Descrizione", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
description = descProp?.GetValue(item)?.ToString();
}
if (string.IsNullOrWhiteSpace(description) && labelProp?.Name != "Codice")
{
var codiceProp = type.GetProperty("Codice", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
description = codiceProp?.GetValue(item)?.ToString();
}
return new EntityListItemDto
{
Id = id,
Label = label,
Description = description ?? ""
};
}
private PropertyInfo? GetLabelProperty(Type type)
{
var candidates = new[] { "RagioneSociale", "Nome", "Descrizione", "Titolo", "Codice", "Name", "Description", "Code", "Title" };
foreach (var candidate in candidates)
{
var prop = type.GetProperty(candidate, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
if (prop != null && prop.PropertyType == typeof(string))
{
return prop;
}
}
return null;
}
private string? GetStringProp(object item, string propName)
{
var prop = item.GetType().GetProperty(propName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
return prop?.GetValue(item)?.ToString();
}
private bool IsSimpleType(Type type)
{
return type.IsPrimitive ||
type.IsEnum ||
type == typeof(string) ||
type == typeof(decimal) ||
type == typeof(DateTime) ||
type == typeof(DateTime?) ||
type == typeof(int?) ||
type == typeof(decimal?) ||
type == typeof(bool) ||
type == typeof(bool?);
}
private string MapType(Type type)
{
if (type == typeof(string)) return "string";
if (type == typeof(int) || type == typeof(int?) ||
type == typeof(long) || type == typeof(long?) ||
type == typeof(short) || type == typeof(short?)) return "number";
if (type == typeof(decimal) || type == typeof(decimal?) ||
type == typeof(double) || type == typeof(double?) ||
type == typeof(float) || type == typeof(float?)) return "currency"; // or number
if (type == typeof(DateTime) || type == typeof(DateTime?)) return "date";
if (type == typeof(bool) || type == typeof(bool?)) return "boolean";
return "string";
}
private string SplitCamelCase(string input)
{
return System.Text.RegularExpressions.Regex.Replace(input, "([A-Z])", " $1", System.Text.RegularExpressions.RegexOptions.Compiled).Trim();
}
private string ToCamelCase(string str)
{
if (string.IsNullOrEmpty(str) || char.IsLower(str[0]))
return str;
return char.ToLower(str[0]) + str.Substring(1);
}
private string GetIconForType(string typeName)
{
typeName = typeName.ToLower();
if (typeName.Contains("evento")) return "event";
if (typeName.Contains("cliente") || typeName.Contains("persona")) return "people";
if (typeName.Contains("location") || typeName.Contains("indirizzo")) return "place";
if (typeName.Contains("articolo") || typeName.Contains("prodotto") || typeName.Contains("magazzino")) return "inventory";
if (typeName.Contains("risorsa") || typeName.Contains("dipendente")) return "person";
if (typeName.Contains("tipo") || typeName.Contains("categoria")) return "category";
return "table_chart";
}
}

View File

@@ -1,99 +1,10 @@
import { useState, useEffect } from "react"; import { Outlet } from "react-router-dom";
import { Outlet, useLocation, useNavigate } from "react-router-dom"; import { Box } from "@mui/material";
import { Box, Tabs, Tab, Paper, Typography, Breadcrumbs, Link } from "@mui/material";
import {
Dashboard as DashboardIcon,
Inventory as ArticleIcon,
Place as LocationIcon,
SwapHoriz as MovementIcon,
Assessment as StockIcon,
FactCheck as InventoryIcon,
} from "@mui/icons-material";
const navItems = [
{ label: "Dashboard", path: "/warehouse", icon: <DashboardIcon fontSize="small" /> },
{ label: "Articoli", path: "/warehouse/articles", icon: <ArticleIcon fontSize="small" /> },
{ label: "Magazzini", path: "/warehouse/locations", icon: <LocationIcon fontSize="small" /> },
{ label: "Movimenti", path: "/warehouse/movements", icon: <MovementIcon fontSize="small" /> },
{ label: "Giacenze", path: "/warehouse/stock", icon: <StockIcon fontSize="small" /> },
{ label: "Inventario", path: "/warehouse/inventory", icon: <InventoryIcon fontSize="small" /> },
];
export default function WarehouseLayout() { export default function WarehouseLayout() {
const location = useLocation();
const navigate = useNavigate();
const [value, setValue] = useState(0);
useEffect(() => {
// Find the matching tab based on current path
const index = navItems.findIndex((item) => {
if (item.path === "/warehouse") {
return location.pathname === "/warehouse";
}
return location.pathname.startsWith(item.path);
});
if (index !== -1) {
setValue(index);
} else {
// If no match (e.g. sub-pages), keep the closest parent or default
// Logic could be improved here but keeping it simple for now
if (location.pathname.includes("articles")) setValue(1);
else if (location.pathname.includes("locations")) setValue(2);
else if (location.pathname.includes("movements")) setValue(3);
else if (location.pathname.includes("stock")) setValue(4);
else if (location.pathname.includes("inventory")) setValue(5);
else setValue(0);
}
}, [location.pathname]);
const handleChange = (_event: React.SyntheticEvent, newValue: number) => {
setValue(newValue);
navigate(navItems[newValue].path);
};
return ( return (
<Box sx={{ display: "flex", flexDirection: "column", height: "100%", gap: 2 }}> <Box sx={{ display: "flex", flexDirection: "column", height: "100%" }}>
{/* Header & Navigation */} <Box sx={{ flex: 1, minHeight: 0, overflow: "auto", p: 3 }}>
<Paper sx={{ px: 2, pt: 2, pb: 0 }}>
<Box sx={{ mb: 2 }}>
<Typography variant="h5" component="h1" fontWeight="bold" gutterBottom>
Gestione Magazzino
</Typography>
<Breadcrumbs aria-label="breadcrumb">
<Link underline="hover" color="inherit" href="/">
Home
</Link>
<Typography color="text.primary">Magazzino</Typography>
{navItems[value]?.label !== "Dashboard" && (
<Typography color="text.primary">{navItems[value]?.label}</Typography>
)}
</Breadcrumbs>
</Box>
<Tabs
value={value}
onChange={handleChange}
variant="scrollable"
scrollButtons="auto"
aria-label="warehouse navigation tabs"
sx={{ borderBottom: 1, borderColor: "divider" }}
>
{navItems.map((item, index) => (
<Tab
key={item.path}
label={item.label}
icon={item.icon}
iconPosition="start"
id={`warehouse-tab-${index}`}
aria-controls={`warehouse-tabpanel-${index}`}
/>
))}
</Tabs>
</Paper>
{/* Content Area */}
<Box sx={{ flex: 1, minHeight: 0, overflow: "auto" }}>
<Outlet /> <Outlet />
</Box> </Box>
</Box> </Box>