feat: Refactor dataset management logic into a new SchemaDiscoveryService, removing it from the ReportsController.
This commit is contained in:
@@ -32,3 +32,7 @@ File riassuntivo dello stato di sviluppo di Zentral.
|
||||
- Correzione import path nel modulo Report Designer e registrazione modulo nel backend.
|
||||
- [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.
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Rimozione Tab Magazzino
|
||||
|
||||
## Obiettivo
|
||||
Rimuovere le tab di navigazione interne al modulo Magazzino (`WarehouseLayout`), in quanto ridondanti rispetto alle tab principali dell'applicazione.
|
||||
|
||||
## Modifiche Apportate
|
||||
- Modificato `src/frontend/src/apps/warehouse/components/WarehouseLayout.tsx`:
|
||||
- Rimossa la componente `Tabs` e la logica associata (`navItems`, `useState`, `useEffect`).
|
||||
- Rimosso l'header contenente il titolo "Gestione Magazzino" e i breadcrumbs.
|
||||
- Semplificato il layout per mostrare solo l'`Outlet` all'interno di un `Box`.
|
||||
- Aggiunto padding (`p: 3`) al contenitore del contenuto per garantire una spaziatura adeguata.
|
||||
|
||||
## Stato
|
||||
Completato.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Live Data Alignment for Report Designer
|
||||
|
||||
## Obiettivo
|
||||
Garantire che i dataset utilizzati nel report designer siano sempre automaticamente allineati con le strutture dati vive del gestionale, leggendo le strutture live invece di affidarsi a dati pre-configurati.
|
||||
|
||||
## Modifiche Apportate
|
||||
|
||||
### Backend
|
||||
1. **Nuovo Servizio `SchemaDiscoveryService`**:
|
||||
* Creato un servizio che scansiona `ZentralDbContext` per trovare tutti i `DbSet` disponibili.
|
||||
* Genera dinamicamente gli schemi dei dati basandosi sulle proprietà delle entità.
|
||||
* Supporta il caricamento dinamico delle entità con eager loading delle proprietà di navigazione.
|
||||
* Include un dizionario di metadati per mantenere descrizioni e icone curate per i dataset principali (Evento, Cliente, ecc.), pur supportando nuovi dataset automaticamente.
|
||||
|
||||
2. **Refactoring `ReportsController`**:
|
||||
* Rimossi i metodi statici hardcoded per la generazione degli schemi (`GetEventoSchema`, ecc.).
|
||||
* Rimossa la lista hardcoded dei dataset disponibili.
|
||||
* Integrato `SchemaDiscoveryService` per ottenere la lista dei dataset, gli schemi e i dati.
|
||||
* Aggiornato `GetVirtualDatasetEntities` per usare il servizio di discovery.
|
||||
|
||||
### Miglioramenti UX
|
||||
1. **Etichette Leggibili**:
|
||||
* Aggiornato `SchemaDiscoveryService` per rilevare automaticamente la proprietà migliore da usare come etichetta (RagioneSociale, Nome, Descrizione, ecc.).
|
||||
* Implementato ordinamento alfabetico automatico basato sull'etichetta rilevata.
|
||||
|
||||
3. **Refactoring `VirtualDatasetsController`**:
|
||||
* Rimosso il metodo hardcoded `GetBaseDatasetSchema`.
|
||||
* Integrato `SchemaDiscoveryService` per la validazione e la generazione degli schemi dei dataset virtuali.
|
||||
* Risolto un TODO per la determinazione automatica del tipo di campo negli schemi virtuali.
|
||||
|
||||
4. **Registrazione Servizio**:
|
||||
* Registrato `SchemaDiscoveryService` in `Program.cs`.
|
||||
|
||||
## Risultato
|
||||
Il Report Designer ora riflette automaticamente qualsiasi modifica al modello dati (nuove entità, nuovi campi) senza richiedere modifiche manuali al codice del controller. I dataset "core" mantengono le loro descrizioni user-friendly, mentre i nuovi dataset vengono esposti con nomi e descrizioni generati automaticamente.
|
||||
@@ -12,11 +12,13 @@ public class ReportsController : ControllerBase
|
||||
{
|
||||
private readonly ReportGeneratorService _reportGenerator;
|
||||
private readonly 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -126,26 +128,7 @@ public class ReportsController : ControllerBase
|
||||
[HttpGet("datasets")]
|
||||
public async Task<ActionResult<List<DatasetTypeDto>>> GetAvailableDatasets()
|
||||
{
|
||||
var datasets = new List<DatasetTypeDto>
|
||||
{
|
||||
// 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<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
|
||||
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<EntityListItemDto>()
|
||||
};
|
||||
|
||||
return entities;
|
||||
return await _schemaDiscovery.GetEntities(datasetId, search, limit, offset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -254,358 +225,10 @@ public class ReportsController : ControllerBase
|
||||
[HttpGet("datasets/{datasetId}/count")]
|
||||
public async Task<ActionResult<int>> 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<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)
|
||||
{
|
||||
@@ -642,38 +265,7 @@ public class ReportsController : ControllerBase
|
||||
|
||||
private async Task<object?> 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<EntityListItemDto>();
|
||||
|
||||
// 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<EntityListItemDto>()
|
||||
};
|
||||
return await _schemaDiscovery.GetEntities(primarySource.DatasetId, search, limit, offset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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<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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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<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
|
||||
|
||||
@@ -22,6 +22,7 @@ builder.Services.AddDbContext<ZentralDbContext>(options =>
|
||||
builder.Services.AddScoped<EventoCostiService>();
|
||||
builder.Services.AddScoped<DemoDataService>();
|
||||
builder.Services.AddScoped<ReportGeneratorService>();
|
||||
builder.Services.AddScoped<SchemaDiscoveryService>();
|
||||
builder.Services.AddScoped<AppService>();
|
||||
builder.Services.AddScoped<AutoCodeService>();
|
||||
builder.Services.AddScoped<CustomFieldService>();
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
using System.Collections;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Zentral.Domain.Entities;
|
||||
using Zentral.Infrastructure.Data;
|
||||
|
||||
namespace Zentral.API.Services.Reports;
|
||||
|
||||
public class SchemaDiscoveryService
|
||||
{
|
||||
private readonly ZentralDbContext _context;
|
||||
|
||||
public SchemaDiscoveryService(ZentralDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, (string Name, string Description, string Icon, string Category)> _datasetMetadata = new()
|
||||
{
|
||||
{ "Evento", ("Evento", "Dati evento completi con cliente, location, ospiti, costi e risorse", "event", "Principale") },
|
||||
{ "Cliente", ("Cliente", "Anagrafica clienti completa", "people", "Principale") },
|
||||
{ "Location", ("Location", "Sedi e location eventi", "place", "Principale") },
|
||||
{ "Articolo", ("Articolo", "Catalogo articoli e materiali", "inventory", "Principale") },
|
||||
{ "Risorsa", ("Risorsa", "Staff e personale", "person", "Principale") },
|
||||
{ "TipoEvento", ("Tipo Evento", "Tipologie di evento (matrimonio, compleanno, etc.)", "category", "Configurazione") },
|
||||
{ "TipoOspite", ("Tipo Ospite", "Tipologie di ospiti (adulti, bambini, etc.)", "groups", "Configurazione") },
|
||||
{ "CodiceCategoria", ("Categoria Articoli", "Categorie articoli con coefficienti di calcolo", "folder", "Configurazione") },
|
||||
{ "TipoRisorsa", ("Tipo Risorsa", "Tipologie di risorse (cameriere, cuoco, etc.)", "badge", "Configurazione") },
|
||||
{ "TipoMateriale", ("Tipo Materiale", "Tipologie di materiali", "category", "Configurazione") }
|
||||
};
|
||||
|
||||
public List<DatasetTypeDto> GetAvailableDatasets()
|
||||
{
|
||||
var datasets = new List<DatasetTypeDto>();
|
||||
var properties = typeof(ZentralDbContext).GetProperties()
|
||||
.Where(p => p.PropertyType.IsGenericType &&
|
||||
p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>));
|
||||
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
var entityType = prop.PropertyType.GetGenericArguments()[0];
|
||||
|
||||
// Skip join tables or non-entity types if necessary
|
||||
if (entityType.Name.Contains("Dettaglio") || entityType.Name.Contains("Link"))
|
||||
continue;
|
||||
|
||||
var datasetId = ToCamelCase(entityType.Name);
|
||||
|
||||
// Default values
|
||||
var name = SplitCamelCase(entityType.Name);
|
||||
var description = $"Dataset {entityType.Name}";
|
||||
var icon = GetIconForType(entityType.Name);
|
||||
var category = "Principale";
|
||||
|
||||
// Determine category from namespace if not in metadata
|
||||
if (entityType.Namespace != null)
|
||||
{
|
||||
var parts = entityType.Namespace.Split('.');
|
||||
if (parts.Length > 3 && parts[2] == "Entities")
|
||||
{
|
||||
category = parts[3];
|
||||
}
|
||||
}
|
||||
|
||||
// Apply metadata if available
|
||||
if (_datasetMetadata.TryGetValue(entityType.Name, out var meta))
|
||||
{
|
||||
name = meta.Name;
|
||||
description = meta.Description;
|
||||
icon = meta.Icon;
|
||||
category = meta.Category;
|
||||
}
|
||||
|
||||
datasets.Add(new DatasetTypeDto
|
||||
{
|
||||
Id = datasetId,
|
||||
Name = name,
|
||||
Description = description,
|
||||
Icon = icon,
|
||||
Category = category,
|
||||
IsVirtual = false
|
||||
});
|
||||
}
|
||||
|
||||
return datasets.OrderBy(d => d.Category).ThenBy(d => d.Name).ToList();
|
||||
}
|
||||
|
||||
public DataSchemaDto? GetSchema(string datasetId)
|
||||
{
|
||||
var type = GetTypeForDataset(datasetId);
|
||||
if (type == null) return null;
|
||||
|
||||
var fields = new List<DataFieldDto>();
|
||||
var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.CanRead);
|
||||
|
||||
foreach (var prop in props)
|
||||
{
|
||||
// Skip collections for fields, handle them as child collections if needed
|
||||
if (typeof(IEnumerable).IsAssignableFrom(prop.PropertyType) && prop.PropertyType != typeof(string))
|
||||
continue;
|
||||
|
||||
// Skip complex types that are not mapped as owned types (simplification)
|
||||
if (!IsSimpleType(prop.PropertyType))
|
||||
continue;
|
||||
|
||||
fields.Add(new DataFieldDto
|
||||
{
|
||||
Name = ToCamelCase(prop.Name),
|
||||
Label = SplitCamelCase(prop.Name),
|
||||
Type = MapType(prop.PropertyType),
|
||||
Group = "Fields"
|
||||
});
|
||||
}
|
||||
|
||||
return new DataSchemaDto
|
||||
{
|
||||
EntityType = type.Name,
|
||||
DatasetId = datasetId,
|
||||
Fields = fields,
|
||||
ChildCollections = new List<DataCollectionDto>()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<List<EntityListItemDto>> GetEntities(string datasetId, string? search, int limit, int offset)
|
||||
{
|
||||
var type = GetTypeForDataset(datasetId);
|
||||
if (type == null) return new List<EntityListItemDto>();
|
||||
|
||||
var method = this.GetType().GetMethod("GetEntitiesGeneric", BindingFlags.NonPublic | BindingFlags.Instance)!
|
||||
.MakeGenericMethod(type);
|
||||
|
||||
var task = (Task<List<EntityListItemDto>>)method.Invoke(this, new object[] { search, limit, offset })!;
|
||||
return await task;
|
||||
}
|
||||
|
||||
public async Task<int> CountEntities(string datasetId, string? search)
|
||||
{
|
||||
var type = GetTypeForDataset(datasetId);
|
||||
if (type == null) return 0;
|
||||
|
||||
var method = this.GetType().GetMethod("CountEntitiesGeneric", BindingFlags.NonPublic | BindingFlags.Instance)!
|
||||
.MakeGenericMethod(type);
|
||||
|
||||
var task = (Task<int>)method.Invoke(this, new object[] { search })!;
|
||||
return await task;
|
||||
}
|
||||
|
||||
public async Task<object?> LoadEntity(string datasetId, int id)
|
||||
{
|
||||
var type = GetTypeForDataset(datasetId);
|
||||
if (type == null) return null;
|
||||
|
||||
var method = this.GetType().GetMethod("LoadEntityGeneric", BindingFlags.NonPublic | BindingFlags.Instance)!
|
||||
.MakeGenericMethod(type);
|
||||
|
||||
var task = (Task<object?>)method.Invoke(this, new object[] { id })!;
|
||||
return await task;
|
||||
}
|
||||
|
||||
// Generic implementations
|
||||
private async Task<object?> LoadEntityGeneric<T>(int id) where T : class
|
||||
{
|
||||
var query = _context.Set<T>().AsQueryable();
|
||||
|
||||
// Eager load all navigation properties
|
||||
var entityType = _context.Model.FindEntityType(typeof(T));
|
||||
if (entityType != null)
|
||||
{
|
||||
foreach (var nav in entityType.GetNavigations())
|
||||
{
|
||||
query = query.Include(nav.Name);
|
||||
}
|
||||
}
|
||||
|
||||
return await query.FirstOrDefaultAsync(e => EF.Property<int>(e, "Id") == id);
|
||||
}
|
||||
|
||||
private async Task<List<EntityListItemDto>> GetEntitiesGeneric<T>(string? search, int limit, int offset) where T : class
|
||||
{
|
||||
var query = _context.Set<T>().AsQueryable();
|
||||
|
||||
// Apply search if possible
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
var predicate = BuildSearchPredicate<T>(search);
|
||||
if (predicate != null)
|
||||
{
|
||||
query = query.Where(predicate);
|
||||
}
|
||||
}
|
||||
|
||||
// Order by Label property if available (Alphabetical), otherwise by Id Descending
|
||||
var labelProp = GetLabelProperty(typeof(T));
|
||||
if (labelProp != null)
|
||||
{
|
||||
query = query.OrderBy(e => EF.Property<string>(e, labelProp.Name));
|
||||
}
|
||||
else
|
||||
{
|
||||
var idProp = typeof(T).GetProperty("Id");
|
||||
if (idProp != null)
|
||||
{
|
||||
query = query.OrderByDescending(e => EF.Property<object>(e, "Id"));
|
||||
}
|
||||
}
|
||||
|
||||
var list = await query.Skip(offset).Take(limit).ToListAsync();
|
||||
return list.Select(item => MapToListItem(item)).ToList();
|
||||
}
|
||||
|
||||
private async Task<int> CountEntitiesGeneric<T>(string? search) where T : class
|
||||
{
|
||||
var query = _context.Set<T>().AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
var predicate = BuildSearchPredicate<T>(search);
|
||||
if (predicate != null)
|
||||
{
|
||||
query = query.Where(predicate);
|
||||
}
|
||||
}
|
||||
return await query.CountAsync();
|
||||
}
|
||||
|
||||
// Helpers
|
||||
private Type? GetTypeForDataset(string datasetId)
|
||||
{
|
||||
// Case insensitive match
|
||||
var properties = typeof(ZentralDbContext).GetProperties()
|
||||
.Where(p => p.PropertyType.IsGenericType &&
|
||||
p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>));
|
||||
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
var entityType = prop.PropertyType.GetGenericArguments()[0];
|
||||
if (entityType.Name.Equals(datasetId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return entityType;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Expression<Func<T, bool>>? BuildSearchPredicate<T>(string search)
|
||||
{
|
||||
var parameter = Expression.Parameter(typeof(T), "e");
|
||||
var searchLower = Expression.Constant(search.ToLower());
|
||||
|
||||
var stringProps = typeof(T).GetProperties()
|
||||
.Where(p => p.PropertyType == typeof(string) && p.CanRead)
|
||||
.Take(3) // Limit to first 3 string properties to avoid huge queries
|
||||
.ToList();
|
||||
|
||||
if (!stringProps.Any()) return null;
|
||||
|
||||
Expression? body = null;
|
||||
var containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) });
|
||||
var toLowerMethod = typeof(string).GetMethod("ToLower", Type.EmptyTypes);
|
||||
|
||||
foreach (var prop in stringProps)
|
||||
{
|
||||
// e.Prop
|
||||
var propExp = Expression.Property(parameter, prop);
|
||||
// e.Prop != null
|
||||
var notNull = Expression.NotEqual(propExp, Expression.Constant(null));
|
||||
// e.Prop.ToLower()
|
||||
var toLower = Expression.Call(propExp, toLowerMethod!);
|
||||
// e.Prop.ToLower().Contains(search)
|
||||
var contains = Expression.Call(toLower, containsMethod!, searchLower);
|
||||
// e.Prop != null && e.Prop.ToLower().Contains(search)
|
||||
var condition = Expression.AndAlso(notNull, contains);
|
||||
|
||||
body = body == null ? condition : Expression.OrElse(body, condition);
|
||||
}
|
||||
|
||||
return body == null ? null : Expression.Lambda<Func<T, bool>>(body, parameter);
|
||||
}
|
||||
|
||||
private EntityListItemDto MapToListItem(object item)
|
||||
{
|
||||
var type = item.GetType();
|
||||
var idProp = type.GetProperty("Id");
|
||||
var id = idProp?.GetValue(item) as int? ?? 0;
|
||||
|
||||
// Use the best label property
|
||||
var labelProp = GetLabelProperty(type);
|
||||
var label = labelProp?.GetValue(item)?.ToString();
|
||||
|
||||
// Fallback to Codice if label is empty and we haven't tried Codice yet
|
||||
if (string.IsNullOrWhiteSpace(label) && labelProp?.Name != "Codice")
|
||||
{
|
||||
var codiceProp = type.GetProperty("Codice", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||
label = codiceProp?.GetValue(item)?.ToString();
|
||||
}
|
||||
|
||||
// Final fallback
|
||||
if (string.IsNullOrWhiteSpace(label))
|
||||
{
|
||||
label = $"Item {id}";
|
||||
}
|
||||
|
||||
// Description: try to find a secondary useful field
|
||||
var description = "";
|
||||
if (labelProp?.Name != "Descrizione")
|
||||
{
|
||||
var descProp = type.GetProperty("Descrizione", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||
description = descProp?.GetValue(item)?.ToString();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(description) && labelProp?.Name != "Codice")
|
||||
{
|
||||
var codiceProp = type.GetProperty("Codice", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||
description = codiceProp?.GetValue(item)?.ToString();
|
||||
}
|
||||
|
||||
return new EntityListItemDto
|
||||
{
|
||||
Id = id,
|
||||
Label = label,
|
||||
Description = description ?? ""
|
||||
};
|
||||
}
|
||||
|
||||
private PropertyInfo? GetLabelProperty(Type type)
|
||||
{
|
||||
var candidates = new[] { "RagioneSociale", "Nome", "Descrizione", "Titolo", "Codice", "Name", "Description", "Code", "Title" };
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
var prop = type.GetProperty(candidate, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||
if (prop != null && prop.PropertyType == typeof(string))
|
||||
{
|
||||
return prop;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private string? GetStringProp(object item, string propName)
|
||||
{
|
||||
var prop = item.GetType().GetProperty(propName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||
return prop?.GetValue(item)?.ToString();
|
||||
}
|
||||
|
||||
private bool IsSimpleType(Type type)
|
||||
{
|
||||
return type.IsPrimitive ||
|
||||
type.IsEnum ||
|
||||
type == typeof(string) ||
|
||||
type == typeof(decimal) ||
|
||||
type == typeof(DateTime) ||
|
||||
type == typeof(DateTime?) ||
|
||||
type == typeof(int?) ||
|
||||
type == typeof(decimal?) ||
|
||||
type == typeof(bool) ||
|
||||
type == typeof(bool?);
|
||||
}
|
||||
|
||||
private string MapType(Type type)
|
||||
{
|
||||
if (type == typeof(string)) return "string";
|
||||
if (type == typeof(int) || type == typeof(int?) ||
|
||||
type == typeof(long) || type == typeof(long?) ||
|
||||
type == typeof(short) || type == typeof(short?)) return "number";
|
||||
if (type == typeof(decimal) || type == typeof(decimal?) ||
|
||||
type == typeof(double) || type == typeof(double?) ||
|
||||
type == typeof(float) || type == typeof(float?)) return "currency"; // or number
|
||||
if (type == typeof(DateTime) || type == typeof(DateTime?)) return "date";
|
||||
if (type == typeof(bool) || type == typeof(bool?)) return "boolean";
|
||||
return "string";
|
||||
}
|
||||
|
||||
private string SplitCamelCase(string input)
|
||||
{
|
||||
return System.Text.RegularExpressions.Regex.Replace(input, "([A-Z])", " $1", System.Text.RegularExpressions.RegexOptions.Compiled).Trim();
|
||||
}
|
||||
|
||||
private string ToCamelCase(string str)
|
||||
{
|
||||
if (string.IsNullOrEmpty(str) || char.IsLower(str[0]))
|
||||
return str;
|
||||
return char.ToLower(str[0]) + str.Substring(1);
|
||||
}
|
||||
|
||||
private string GetIconForType(string typeName)
|
||||
{
|
||||
typeName = typeName.ToLower();
|
||||
if (typeName.Contains("evento")) return "event";
|
||||
if (typeName.Contains("cliente") || typeName.Contains("persona")) return "people";
|
||||
if (typeName.Contains("location") || typeName.Contains("indirizzo")) return "place";
|
||||
if (typeName.Contains("articolo") || typeName.Contains("prodotto") || typeName.Contains("magazzino")) return "inventory";
|
||||
if (typeName.Contains("risorsa") || typeName.Contains("dipendente")) return "person";
|
||||
if (typeName.Contains("tipo") || typeName.Contains("categoria")) return "category";
|
||||
return "table_chart";
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -1,99 +1,10 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Outlet, 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: <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" /> },
|
||||
];
|
||||
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 (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", height: "100%", gap: 2 }}>
|
||||
{/* Header & Navigation */}
|
||||
<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" }}>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", height: "100%" }}>
|
||||
<Box sx={{ flex: 1, minHeight: 0, overflow: "auto", p: 3 }}>
|
||||
<Outlet />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user