Files
zentral/src/backend/Zentral.API/Controllers/ReportsController.cs

1291 lines
54 KiB
C#

using Zentral.API.Services.Reports;
using Zentral.Domain.Entities;
using Zentral.Infrastructure.Data;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Zentral.API.Controllers;
[ApiController]
[Route("api/reports")]
public class ReportsController : ControllerBase
{
private readonly ReportGeneratorService _reportGenerator;
private readonly ZentralDbContext _context;
public ReportsController(ReportGeneratorService reportGenerator, ZentralDbContext context)
{
_reportGenerator = reportGenerator;
_context = context;
}
/// <summary>
/// Genera un PDF da un template con i dati forniti
/// </summary>
[HttpPost("generate")]
public async Task<IActionResult> Generate([FromBody] GenerateReportRequest request)
{
try
{
var pdf = await _reportGenerator.GeneratePdfAsync(request.TemplateId, request.DataContext);
return File(pdf, "application/pdf", "report.pdf");
}
catch (ArgumentException ex)
{
return NotFound(ex.Message);
}
catch (Exception ex)
{
return BadRequest($"Error generating report: {ex.Message}");
}
}
/// <summary>
/// Genera il PDF di un evento usando il template predefinito o specificato
/// </summary>
[HttpGet("evento/{eventoId}")]
public async Task<IActionResult> GenerateEvento(int eventoId, [FromQuery] int? templateId = null)
{
try
{
var pdf = await _reportGenerator.GenerateEventoPdfAsync(eventoId, templateId);
return File(pdf, "application/pdf", $"evento_{eventoId}.pdf");
}
catch (ArgumentException ex)
{
return NotFound(ex.Message);
}
catch (Exception ex)
{
return BadRequest($"Error generating report: {ex.Message}");
}
}
/// <summary>
/// Debug endpoint to test binding resolution
/// </summary>
[HttpPost("debug-binding")]
public async Task<IActionResult> DebugBinding([FromBody] DebugBindingRequest request)
{
var dataContext = await BuildDataContextAsync(request.DataSources);
var results = new Dictionary<string, object?>();
results["dataContextKeys"] = dataContext.Keys.ToList();
foreach (var kvp in dataContext)
{
var type = kvp.Value?.GetType();
results[$"dataset_{kvp.Key}_type"] = type?.Name;
// Try to get the property
if (kvp.Value != null && !string.IsNullOrEmpty(request.PropertyName))
{
var prop = type?.GetProperty(request.PropertyName,
System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.Instance |
System.Reflection.BindingFlags.IgnoreCase);
results[$"dataset_{kvp.Key}_property_found"] = prop != null;
if (prop != null)
{
var value = prop.GetValue(kvp.Value);
results[$"dataset_{kvp.Key}_value"] = value?.ToString();
results[$"dataset_{kvp.Key}_value_type"] = value?.GetType().Name;
}
}
}
return Ok(results);
}
/// <summary>
/// Genera un'anteprima del PDF con dati reali
/// </summary>
[HttpPost("preview")]
public async Task<IActionResult> Preview([FromBody] PreviewReportRequest request)
{
try
{
var dataContext = await BuildDataContextAsync(request.DataSources);
var pdf = await _reportGenerator.GeneratePdfAsync(request.TemplateId, dataContext);
return File(pdf, "application/pdf");
}
catch (ArgumentException ex)
{
return NotFound(ex.Message);
}
catch (Exception ex)
{
return BadRequest($"Error generating preview: {ex.Message}");
}
}
/// <summary>
/// Ottiene la lista dei tipi di dataset disponibili (inclusi Virtual Dataset)
/// </summary>
[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" },
};
// Aggiungi Virtual Dataset dal database
var virtualDatasets = await _context.VirtualDatasets
.Where(vd => vd.Attivo)
.OrderBy(vd => vd.DisplayName)
.Select(vd => new DatasetTypeDto
{
Id = $"virtual:{vd.Nome}", // Prefisso per identificare i virtual dataset
Name = vd.DisplayName,
Description = vd.Descrizione ?? "Dataset virtuale personalizzato",
Icon = vd.Icon,
Category = vd.Categoria,
IsVirtual = true
})
.ToListAsync();
datasets.AddRange(virtualDatasets);
return datasets;
}
/// <summary>
/// Ottiene le categorie dei dataset (incluse quelle dei Virtual Dataset)
/// </summary>
[HttpGet("datasets/categories")]
public async Task<ActionResult<List<string>>> GetDatasetCategories()
{
var baseCategories = new List<string> { "Principale", "Configurazione", "Liste" };
// Aggiungi categorie dai Virtual Dataset
var virtualCategories = await _context.VirtualDatasets
.Where(vd => vd.Attivo)
.Select(vd => vd.Categoria)
.Distinct()
.ToListAsync();
foreach (var cat in virtualCategories)
{
if (!baseCategories.Contains(cat))
baseCategories.Add(cat);
}
return baseCategories;
}
/// <summary>
/// Ottiene lo schema dei dati per un dataset (inclusi Virtual Dataset)
/// </summary>
[HttpGet("schema/{datasetId}")]
public async Task<ActionResult<DataSchemaDto>> GetSchema(string datasetId)
{
// Check se è un Virtual Dataset
if (datasetId.StartsWith("virtual:"))
{
var virtualName = datasetId.Substring("virtual:".Length);
var schema = await GetVirtualDatasetSchemaAsync(virtualName);
if (schema == null)
return NotFound($"Virtual Dataset '{virtualName}' not found");
return schema;
}
var staticSchema = GetSchemaForDataset(datasetId);
if (staticSchema == null)
return NotFound($"Dataset '{datasetId}' not found");
return staticSchema;
}
/// <summary>
/// Ottiene la lista delle entità disponibili per un dataset (per la preview)
/// </summary>
[HttpGet("datasets/{datasetId}/entities")]
public async Task<ActionResult<List<EntityListItemDto>>> GetEntitiesForDataset(
string datasetId,
[FromQuery] string? search = null,
[FromQuery] int limit = 50,
[FromQuery] int offset = 0)
{
// Virtual Dataset - restituisce le entità del dataset primario
if (datasetId.StartsWith("virtual:"))
{
var virtualName = datasetId.Substring("virtual:".Length);
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;
}
/// <summary>
/// Conta le entità disponibili per un dataset
/// </summary>
[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;
}
#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)
{
var context = new Dictionary<string, object>();
foreach (var ds in dataSources)
{
object? data;
// Check se è un Virtual Dataset
if (ds.DatasetId.StartsWith("virtual:"))
{
var virtualName = ds.DatasetId.Substring("virtual:".Length);
data = await LoadVirtualDatasetAsync(virtualName, ds.EntityId);
}
else
{
data = await LoadEntityDataAsync(ds.DatasetId, ds.EntityId);
}
if (data != null)
{
// Usa l'alias se fornito, altrimenti usa l'ID del dataset
var key = ds.Alias ?? ds.DatasetId;
// Per i virtual dataset, rimuovi il prefisso "virtual:" dalla chiave
if (key.StartsWith("virtual:"))
key = key.Substring("virtual:".Length);
context[key] = data;
}
}
return context;
}
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
};
}
#region Virtual Dataset Support
/// <summary>
/// Genera lo schema per un Virtual Dataset basato sulla sua configurazione
/// </summary>
private async Task<DataSchemaDto?> GetVirtualDatasetSchemaAsync(string virtualName)
{
var virtualDataset = await _context.VirtualDatasets
.FirstOrDefaultAsync(vd => vd.Nome == virtualName && vd.Attivo);
if (virtualDataset == null) return null;
var config = System.Text.Json.JsonSerializer.Deserialize<VirtualDatasetConfiguration>(
virtualDataset.ConfigurationJson,
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }
);
if (config == null) return null;
var fields = new List<DataFieldDto>();
// Se ci sono OutputFields definiti, usa quelli
if (config.OutputFields.Any(f => f.Included))
{
foreach (var outputField in config.OutputFields.Where(f => f.Included).OrderBy(f => f.Order))
{
var source = config.Sources.FirstOrDefault(s => s.Id == outputField.SourceId);
if (source == null) continue;
// Ottieni lo schema del dataset sorgente per determinare il tipo
var sourceSchema = GetSchemaForDataset(source.DatasetId);
var sourceField = sourceSchema?.Fields.FirstOrDefault(f =>
f.Name.Equals(outputField.FieldName, StringComparison.OrdinalIgnoreCase));
fields.Add(new DataFieldDto
{
Name = outputField.Alias ?? $"{source.Alias}.{outputField.FieldName}",
Label = outputField.Label ?? sourceField?.Label ?? outputField.FieldName,
Type = sourceField?.Type ?? "string",
Group = outputField.Group ?? source.Alias
});
}
}
else
{
// Se non ci sono OutputFields, includi tutti i campi di tutte le sorgenti
foreach (var source in config.Sources)
{
var sourceSchema = GetSchemaForDataset(source.DatasetId);
if (sourceSchema == null) continue;
foreach (var field in sourceSchema.Fields)
{
fields.Add(new DataFieldDto
{
Name = $"{source.Alias}.{field.Name}",
Label = $"{source.Alias} - {field.Label}",
Type = field.Type,
Group = source.Alias
});
}
}
}
return new DataSchemaDto
{
EntityType = virtualDataset.DisplayName,
DatasetId = $"virtual:{virtualDataset.Nome}",
Fields = fields,
ChildCollections = new List<DataCollectionDto>()
};
}
/// <summary>
/// Ottiene le entità disponibili per un Virtual Dataset (basato sul dataset primario)
/// </summary>
private async Task<List<EntityListItemDto>> GetVirtualDatasetEntities(string virtualName, string? search, int limit, int offset)
{
var virtualDataset = await _context.VirtualDatasets
.FirstOrDefaultAsync(vd => vd.Nome == virtualName && vd.Attivo);
if (virtualDataset == null) return new List<EntityListItemDto>();
var config = System.Text.Json.JsonSerializer.Deserialize<VirtualDatasetConfiguration>(
virtualDataset.ConfigurationJson,
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }
);
if (config == null) return new List<EntityListItemDto>();
// Trova il dataset primario
var primarySource = config.Sources.FirstOrDefault(s => s.IsPrimary)
?? config.Sources.FirstOrDefault();
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>()
};
}
/// <summary>
/// Carica i dati per un Virtual Dataset applicando JOIN e filtri
/// </summary>
private async Task<object?> LoadVirtualDatasetAsync(string virtualName, int primaryEntityId)
{
var virtualDataset = await _context.VirtualDatasets
.FirstOrDefaultAsync(vd => vd.Nome == virtualName && vd.Attivo);
if (virtualDataset == null) return null;
var config = System.Text.Json.JsonSerializer.Deserialize<VirtualDatasetConfiguration>(
virtualDataset.ConfigurationJson,
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }
);
if (config == null) return null;
// Carica tutti i dataset sorgente
var loadedData = new Dictionary<string, object?>();
foreach (var source in config.Sources)
{
if (source.IsPrimary)
{
// Carica l'entità primaria
loadedData[source.Id] = await LoadEntityDataAsync(source.DatasetId, primaryEntityId);
}
}
// Applica le relazioni per caricare i dati correlati
foreach (var relationship in config.Relationships)
{
var fromData = loadedData.GetValueOrDefault(relationship.FromSourceId);
if (fromData == null) continue;
var toSource = config.Sources.FirstOrDefault(s => s.Id == relationship.ToSourceId);
if (toSource == null) continue;
// Ottieni il valore della chiave esterna
var fromKeyValue = GetPropertyValue(fromData, relationship.FromField);
if (fromKeyValue == null) continue;
// Carica l'entità correlata
if (int.TryParse(fromKeyValue.ToString(), out int relatedId))
{
loadedData[relationship.ToSourceId] = await LoadEntityDataAsync(toSource.DatasetId, relatedId);
}
}
// Costruisci l'oggetto risultante con tutti i dati
var result = new Dictionary<string, object?>();
foreach (var source in config.Sources)
{
var data = loadedData.GetValueOrDefault(source.Id);
if (data != null)
{
result[source.Alias] = data;
}
}
// Se ci sono OutputFields, costruisci un oggetto piatto
if (config.OutputFields.Any(f => f.Included))
{
var flatResult = new Dictionary<string, object?>();
foreach (var outputField in config.OutputFields.Where(f => f.Included))
{
var source = config.Sources.FirstOrDefault(s => s.Id == outputField.SourceId);
if (source == null) continue;
var sourceData = loadedData.GetValueOrDefault(outputField.SourceId);
if (sourceData == null) continue;
var value = GetPropertyValue(sourceData, outputField.FieldName);
var fieldName = outputField.Alias ?? $"{source.Alias}_{outputField.FieldName}";
flatResult[fieldName] = value;
}
return flatResult;
}
return result;
}
private static object? GetPropertyValue(object obj, string propertyName)
{
var type = obj.GetType();
var prop = type.GetProperty(propertyName,
System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.Instance |
System.Reflection.BindingFlags.IgnoreCase);
return prop?.GetValue(obj);
}
#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
public class DebugBindingRequest
{
public List<DataSourceSelection> DataSources { get; set; } = new();
public string? PropertyName { get; set; }
}
public class PreviewReportRequest
{
public int TemplateId { get; set; }
public List<DataSourceSelection> DataSources { get; set; } = new();
}
public class DataSourceSelection
{
public string DatasetId { get; set; } = string.Empty;
public int EntityId { get; set; }
public string? Alias { get; set; }
}
public class DatasetTypeDto
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Icon { get; set; } = string.Empty;
public string Category { get; set; } = "Principale";
public bool IsVirtual { get; set; } = false;
}
public class EntityListItemDto
{
public int Id { get; set; }
public string Label { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string? SecondaryInfo { get; set; }
public string? Status { get; set; }
}
public class DataSchemaDto
{
public string EntityType { get; set; } = string.Empty;
public string DatasetId { get; set; } = string.Empty;
public List<DataFieldDto> Fields { get; set; } = new();
public List<DataCollectionDto> ChildCollections { get; set; } = new();
}
public class DataFieldDto
{
public string Name { get; set; } = string.Empty;
public string Type { get; set; } = "string";
public string Label { get; set; } = string.Empty;
public string? Group { get; set; }
}
public class DataCollectionDto
{
public string Name { get; set; } = string.Empty;
public string Label { get; set; } = string.Empty;
public string? Description { get; set; }
public List<DataFieldDto> Fields { get; set; } = new();
}