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; } /// /// Genera un PDF da un template con i dati forniti /// [HttpPost("generate")] public async Task 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}"); } } /// /// Genera il PDF di un evento usando il template predefinito o specificato /// [HttpGet("evento/{eventoId}")] public async Task 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}"); } } /// /// Debug endpoint to test binding resolution /// [HttpPost("debug-binding")] public async Task DebugBinding([FromBody] DebugBindingRequest request) { var dataContext = await BuildDataContextAsync(request.DataSources); var results = new Dictionary(); 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); } /// /// Genera un'anteprima del PDF con dati reali /// [HttpPost("preview")] public async Task 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}"); } } /// /// Ottiene la lista dei tipi di dataset disponibili (inclusi Virtual Dataset) /// [HttpGet("datasets")] public async Task>> GetAvailableDatasets() { var datasets = new List { // Dataset principali new() { Id = "evento", Name = "Evento", Description = "Dati evento completi con cliente, location, ospiti, costi e risorse", Icon = "event", Category = "Principale" }, new() { Id = "cliente", Name = "Cliente", Description = "Anagrafica clienti completa", Icon = "people", Category = "Principale" }, new() { Id = "location", Name = "Location", Description = "Sedi e location eventi", Icon = "place", Category = "Principale" }, new() { Id = "articolo", Name = "Articolo", Description = "Catalogo articoli e materiali", Icon = "inventory", Category = "Principale" }, new() { Id = "risorsa", Name = "Risorsa", Description = "Staff e personale", Icon = "person", Category = "Principale" }, // Dataset di lookup/configurazione new() { Id = "tipoEvento", Name = "Tipo Evento", Description = "Tipologie di evento (matrimonio, compleanno, etc.)", Icon = "category", Category = "Configurazione" }, new() { Id = "tipoOspite", Name = "Tipo Ospite", Description = "Tipologie di ospiti (adulti, bambini, etc.)", Icon = "groups", Category = "Configurazione" }, new() { Id = "categoria", Name = "Categoria Articoli", Description = "Categorie articoli con coefficienti di calcolo", Icon = "folder", Category = "Configurazione" }, new() { Id = "tipoRisorsa", Name = "Tipo Risorsa", Description = "Tipologie di risorse (cameriere, cuoco, etc.)", Icon = "badge", Category = "Configurazione" }, new() { Id = "tipoMateriale", Name = "Tipo Materiale", Description = "Tipologie di materiali", Icon = "category", Category = "Configurazione" }, // Dataset lista (per report con elenchi) new() { Id = "listaEventi", Name = "Lista Eventi", Description = "Elenco eventi per report multipli", Icon = "list", Category = "Liste" }, new() { Id = "listaArticoli", Name = "Lista Articoli", Description = "Elenco articoli per catalogo", Icon = "list", Category = "Liste" }, }; // 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; } /// /// Ottiene le categorie dei dataset (incluse quelle dei Virtual Dataset) /// [HttpGet("datasets/categories")] public async Task>> GetDatasetCategories() { var baseCategories = new List { "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; } /// /// Ottiene lo schema dei dati per un dataset (inclusi Virtual Dataset) /// [HttpGet("schema/{datasetId}")] public async Task> 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; } /// /// Ottiene la lista delle entità disponibili per un dataset (per la preview) /// [HttpGet("datasets/{datasetId}/entities")] public async Task>> 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() }; return entities; } /// /// Conta le entità disponibili per un dataset /// [HttpGet("datasets/{datasetId}/count")] public async Task> GetEntityCount(string datasetId, [FromQuery] string? search = null) { var count = datasetId.ToLower() switch { "evento" => await CountEventi(search), "cliente" => await CountClienti(search), "location" => await CountLocation(search), "articolo" => await CountArticoli(search), "risorsa" => await CountRisorse(search), "tipoevento" => await _context.TipiEvento.CountAsync(t => t.Attivo), "tipoospite" => await _context.TipiOspite.CountAsync(t => t.Attivo), "categoria" => await _context.CodiciCategoria.CountAsync(c => c.Attivo), "tiporisorsa" => await _context.TipiRisorsa.CountAsync(t => t.Attivo), "tipomateriale" => await _context.TipiMateriale.CountAsync(t => t.Attivo), _ => 0 }; return count; } #region Entity Queries private async Task> GetEventiEntities(string? search, int limit, int offset) { var query = _context.Eventi .Include(e => e.Cliente) .Include(e => e.Location) .Include(e => e.TipoEvento) .AsQueryable(); if (!string.IsNullOrWhiteSpace(search)) { search = search.ToLower(); query = query.Where(e => (e.Codice != null && e.Codice.ToLower().Contains(search)) || (e.Cliente != null && e.Cliente.RagioneSociale.ToLower().Contains(search)) || (e.Location != null && e.Location.Nome.ToLower().Contains(search))); } return await query .OrderByDescending(e => e.DataEvento) .Skip(offset) .Take(limit) .Select(e => new EntityListItemDto { Id = e.Id, Label = $"{e.Codice ?? $"EVT-{e.Id}"} - {e.DataEvento:dd/MM/yyyy}", Description = $"{e.Cliente!.RagioneSociale ?? "N/D"} @ {e.Location!.Nome ?? "N/D"}", SecondaryInfo = e.TipoEvento != null ? e.TipoEvento.Descrizione : null, Status = e.Stato.ToString() }) .ToListAsync(); } private async Task CountEventi(string? search) { var query = _context.Eventi.AsQueryable(); if (!string.IsNullOrWhiteSpace(search)) { search = search.ToLower(); query = query.Where(e => (e.Codice != null && e.Codice.ToLower().Contains(search)) || (e.Cliente != null && e.Cliente.RagioneSociale.ToLower().Contains(search))); } return await query.CountAsync(); } private async Task> GetClientiEntities(string? search, int limit, int offset) { var query = _context.Clienti.Where(c => c.Attivo); if (!string.IsNullOrWhiteSpace(search)) { search = search.ToLower(); query = query.Where(c => c.RagioneSociale.ToLower().Contains(search) || (c.Citta != null && c.Citta.ToLower().Contains(search)) || (c.Email != null && c.Email.ToLower().Contains(search))); } return await query .OrderBy(c => c.RagioneSociale) .Skip(offset) .Take(limit) .Select(c => new EntityListItemDto { Id = c.Id, Label = c.RagioneSociale, Description = $"{c.Citta ?? "N/D"} - {c.Telefono ?? "N/D"}", SecondaryInfo = c.Email }) .ToListAsync(); } private async Task CountClienti(string? search) { var query = _context.Clienti.Where(c => c.Attivo); if (!string.IsNullOrWhiteSpace(search)) { search = search.ToLower(); query = query.Where(c => c.RagioneSociale.ToLower().Contains(search)); } return await query.CountAsync(); } private async Task> GetLocationEntities(string? search, int limit, int offset) { var query = _context.Location.Where(l => l.Attivo); if (!string.IsNullOrWhiteSpace(search)) { search = search.ToLower(); query = query.Where(l => l.Nome.ToLower().Contains(search) || (l.Citta != null && l.Citta.ToLower().Contains(search))); } return await query .OrderBy(l => l.Nome) .Skip(offset) .Take(limit) .Select(l => new EntityListItemDto { Id = l.Id, Label = l.Nome, Description = $"{l.Citta ?? "N/D"} ({l.Provincia ?? "N/D"})", SecondaryInfo = l.DistanzaKm.HasValue ? $"{l.DistanzaKm} km" : null }) .ToListAsync(); } private async Task CountLocation(string? search) { var query = _context.Location.Where(l => l.Attivo); if (!string.IsNullOrWhiteSpace(search)) { search = search.ToLower(); query = query.Where(l => l.Nome.ToLower().Contains(search)); } return await query.CountAsync(); } private async Task> GetArticoliEntities(string? search, int limit, int offset) { var query = _context.Articoli .Include(a => a.Categoria) .Where(a => a.Attivo); if (!string.IsNullOrWhiteSpace(search)) { search = search.ToLower(); query = query.Where(a => a.Codice.ToLower().Contains(search) || a.Descrizione.ToLower().Contains(search)); } return await query .OrderBy(a => a.Descrizione) .Skip(offset) .Take(limit) .Select(a => new EntityListItemDto { Id = a.Id, Label = $"{a.Codice} - {a.Descrizione}", Description = $"Disponibile: {a.QtaDisponibile ?? 0}", SecondaryInfo = a.Categoria != null ? a.Categoria.Descrizione : null }) .ToListAsync(); } private async Task CountArticoli(string? search) { var query = _context.Articoli.Where(a => a.Attivo); if (!string.IsNullOrWhiteSpace(search)) { search = search.ToLower(); query = query.Where(a => a.Codice.ToLower().Contains(search) || a.Descrizione.ToLower().Contains(search)); } return await query.CountAsync(); } private async Task> GetRisorseEntities(string? search, int limit, int offset) { var query = _context.Risorse .Include(r => r.TipoRisorsa) .Where(r => r.Attivo); if (!string.IsNullOrWhiteSpace(search)) { search = search.ToLower(); query = query.Where(r => r.Nome.ToLower().Contains(search) || (r.Cognome != null && r.Cognome.ToLower().Contains(search))); } return await query .OrderBy(r => r.Cognome).ThenBy(r => r.Nome) .Skip(offset) .Take(limit) .Select(r => new EntityListItemDto { Id = r.Id, Label = $"{r.Nome} {r.Cognome ?? ""}".Trim(), Description = r.Telefono ?? "", SecondaryInfo = r.TipoRisorsa != null ? r.TipoRisorsa.Descrizione : null }) .ToListAsync(); } private async Task CountRisorse(string? search) { var query = _context.Risorse.Where(r => r.Attivo); if (!string.IsNullOrWhiteSpace(search)) { search = search.ToLower(); query = query.Where(r => r.Nome.ToLower().Contains(search) || (r.Cognome != null && r.Cognome.ToLower().Contains(search))); } return await query.CountAsync(); } private async Task> GetTipiEventoEntities(string? search, int limit, int offset) { var query = _context.TipiEvento .Include(t => t.TipoPasto) .Where(t => t.Attivo); if (!string.IsNullOrWhiteSpace(search)) { search = search.ToLower(); query = query.Where(t => t.Codice.ToLower().Contains(search) || t.Descrizione.ToLower().Contains(search)); } return await query .OrderBy(t => t.Descrizione) .Skip(offset) .Take(limit) .Select(t => new EntityListItemDto { Id = t.Id, Label = t.Descrizione, Description = $"Codice: {t.Codice}", SecondaryInfo = t.TipoPasto != null ? t.TipoPasto.Descrizione : null }) .ToListAsync(); } private async Task> GetTipiOspiteEntities(string? search, int limit, int offset) { var query = _context.TipiOspite.Where(t => t.Attivo); if (!string.IsNullOrWhiteSpace(search)) { search = search.ToLower(); query = query.Where(t => t.Codice.ToLower().Contains(search) || t.Descrizione.ToLower().Contains(search)); } return await query .OrderBy(t => t.Descrizione) .Skip(offset) .Take(limit) .Select(t => new EntityListItemDto { Id = t.Id, Label = t.Descrizione, Description = $"Codice: {t.Codice}" }) .ToListAsync(); } private async Task> GetCategorieEntities(string? search, int limit, int offset) { var query = _context.CodiciCategoria.Where(c => c.Attivo); if (!string.IsNullOrWhiteSpace(search)) { search = search.ToLower(); query = query.Where(c => c.Codice.ToLower().Contains(search) || c.Descrizione.ToLower().Contains(search)); } return await query .OrderBy(c => c.Descrizione) .Skip(offset) .Take(limit) .Select(c => new EntityListItemDto { Id = c.Id, Label = c.Descrizione, Description = $"Codice: {c.Codice}", SecondaryInfo = $"Coeff: A={c.CoeffA:F2}, B={c.CoeffB:F2}, S={c.CoeffS:F2}" }) .ToListAsync(); } private async Task> GetTipiRisorsaEntities(string? search, int limit, int offset) { var query = _context.TipiRisorsa.Where(t => t.Attivo); if (!string.IsNullOrWhiteSpace(search)) { search = search.ToLower(); query = query.Where(t => t.Codice.ToLower().Contains(search) || t.Descrizione.ToLower().Contains(search)); } return await query .OrderBy(t => t.Descrizione) .Skip(offset) .Take(limit) .Select(t => new EntityListItemDto { Id = t.Id, Label = t.Descrizione, Description = $"Codice: {t.Codice}" }) .ToListAsync(); } private async Task> GetTipiMaterialeEntities(string? search, int limit, int offset) { var query = _context.TipiMateriale.Where(t => t.Attivo); if (!string.IsNullOrWhiteSpace(search)) { search = search.ToLower(); query = query.Where(t => t.Codice.ToLower().Contains(search) || t.Descrizione.ToLower().Contains(search)); } return await query .OrderBy(t => t.Descrizione) .Skip(offset) .Take(limit) .Select(t => new EntityListItemDto { Id = t.Id, Label = t.Descrizione, Description = $"Codice: {t.Codice}" }) .ToListAsync(); } #endregion private async Task> BuildDataContextAsync(List dataSources) { var context = new Dictionary(); 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 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 /// /// Genera lo schema per un Virtual Dataset basato sulla sua configurazione /// private async Task 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( virtualDataset.ConfigurationJson, new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true } ); if (config == null) return null; var fields = new List(); // 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() }; } /// /// Ottiene le entità disponibili per un Virtual Dataset (basato sul dataset primario) /// private async Task> 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(); var config = System.Text.Json.JsonSerializer.Deserialize( virtualDataset.ConfigurationJson, new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true } ); if (config == null) return new List(); // Trova il dataset primario var primarySource = config.Sources.FirstOrDefault(s => s.IsPrimary) ?? config.Sources.FirstOrDefault(); if (primarySource == null) return new List(); // Restituisce le entità del dataset primario return primarySource.DatasetId.ToLower() switch { "evento" => await GetEventiEntities(search, limit, offset), "cliente" => await GetClientiEntities(search, limit, offset), "location" => await GetLocationEntities(search, limit, offset), "articolo" => await GetArticoliEntities(search, limit, offset), "risorsa" => await GetRisorseEntities(search, limit, offset), _ => new List() }; } /// /// Carica i dati per un Virtual Dataset applicando JOIN e filtri /// private async Task 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( virtualDataset.ConfigurationJson, new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true } ); if (config == null) return null; // Carica tutti i dataset sorgente var loadedData = new Dictionary(); 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(); 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(); 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 { // Campi base new() { Name = "id", Type = "number", Label = "ID", Group = "Base" }, new() { Name = "codice", Type = "string", Label = "Codice Evento", Group = "Base" }, new() { Name = "dataEvento", Type = "date", Label = "Data Evento", Group = "Base" }, new() { Name = "oraInizio", Type = "time", Label = "Ora Inizio", Group = "Base" }, new() { Name = "oraFine", Type = "time", Label = "Ora Fine", Group = "Base" }, new() { Name = "descrizione", Type = "string", Label = "Descrizione", Group = "Base" }, new() { Name = "stato", Type = "number", Label = "Stato (0=Scheda, 10=Preventivo, 20=Confermato)", Group = "Base" }, new() { Name = "confermato", Type = "boolean", Label = "Confermato", Group = "Base" }, // Ospiti new() { Name = "numeroOspiti", Type = "number", Label = "Numero Ospiti Totale", Group = "Ospiti" }, new() { Name = "numeroOspitiAdulti", Type = "number", Label = "Ospiti Adulti", Group = "Ospiti" }, new() { Name = "numeroOspitiBambini", Type = "number", Label = "Ospiti Bambini", Group = "Ospiti" }, new() { Name = "numeroOspitiSeduti", Type = "number", Label = "Ospiti Seduti", Group = "Ospiti" }, new() { Name = "numeroOspitiBuffet", Type = "number", Label = "Ospiti Buffet", Group = "Ospiti" }, // Economici new() { Name = "costoTotale", Type = "currency", Label = "Costo Totale", Group = "Economici" }, new() { Name = "costoPersona", Type = "currency", Label = "Costo per Persona", Group = "Economici" }, new() { Name = "totaleAcconti", Type = "currency", Label = "Totale Acconti", Group = "Economici" }, new() { Name = "saldo", Type = "currency", Label = "Saldo da Pagare", Group = "Economici" }, new() { Name = "dataScadenzaPreventivo", Type = "date", Label = "Scadenza Preventivo", Group = "Economici" }, // Note new() { Name = "noteCliente", Type = "string", Label = "Note Cliente", Group = "Note" }, new() { Name = "noteInterne", Type = "string", Label = "Note Interne", Group = "Note" }, new() { Name = "noteCucina", Type = "string", Label = "Note Cucina", Group = "Note" }, new() { Name = "noteAllestimento", Type = "string", Label = "Note Allestimento", Group = "Note" }, // Cliente (relazione) new() { Name = "cliente.id", Type = "number", Label = "ID Cliente", Group = "Cliente" }, new() { Name = "cliente.ragioneSociale", Type = "string", Label = "Ragione Sociale", Group = "Cliente" }, new() { Name = "cliente.indirizzo", Type = "string", Label = "Indirizzo Cliente", Group = "Cliente" }, new() { Name = "cliente.cap", Type = "string", Label = "CAP Cliente", Group = "Cliente" }, new() { Name = "cliente.citta", Type = "string", Label = "Città Cliente", Group = "Cliente" }, new() { Name = "cliente.provincia", Type = "string", Label = "Provincia Cliente", Group = "Cliente" }, new() { Name = "cliente.telefono", Type = "string", Label = "Telefono Cliente", Group = "Cliente" }, new() { Name = "cliente.email", Type = "string", Label = "Email Cliente", Group = "Cliente" }, new() { Name = "cliente.pec", Type = "string", Label = "PEC Cliente", Group = "Cliente" }, new() { Name = "cliente.codiceFiscale", Type = "string", Label = "Codice Fiscale", Group = "Cliente" }, new() { Name = "cliente.partitaIva", Type = "string", Label = "Partita IVA", Group = "Cliente" }, new() { Name = "cliente.codiceDestinatario", Type = "string", Label = "Codice SDI", Group = "Cliente" }, // Location (relazione) new() { Name = "location.id", Type = "number", Label = "ID Location", Group = "Location" }, new() { Name = "location.nome", Type = "string", Label = "Nome Location", Group = "Location" }, new() { Name = "location.indirizzo", Type = "string", Label = "Indirizzo Location", Group = "Location" }, new() { Name = "location.cap", Type = "string", Label = "CAP Location", Group = "Location" }, new() { Name = "location.citta", Type = "string", Label = "Città Location", Group = "Location" }, new() { Name = "location.provincia", Type = "string", Label = "Provincia Location", Group = "Location" }, new() { Name = "location.telefono", Type = "string", Label = "Telefono Location", Group = "Location" }, new() { Name = "location.email", Type = "string", Label = "Email Location", Group = "Location" }, new() { Name = "location.referente", Type = "string", Label = "Referente Location", Group = "Location" }, new() { Name = "location.distanzaKm", Type = "number", Label = "Distanza (km)", Group = "Location" }, // Tipo Evento (relazione) new() { Name = "tipoEvento.codice", Type = "string", Label = "Codice Tipo Evento", Group = "Tipo Evento" }, new() { Name = "tipoEvento.descrizione", Type = "string", Label = "Tipo Evento", Group = "Tipo Evento" }, new() { Name = "tipoEvento.tipoPasto.descrizione", Type = "string", Label = "Tipo Pasto", Group = "Tipo Evento" }, }, ChildCollections = new List { new() { Name = "dettagliOspiti", Label = "Dettaglio Ospiti", Description = "Breakdown ospiti per tipologia", Fields = new List { new() { Name = "tipoOspite.codice", Type = "string", Label = "Codice Tipo" }, new() { Name = "tipoOspite.descrizione", Type = "string", Label = "Tipo Ospite" }, new() { Name = "numero", Type = "number", Label = "Numero" }, new() { Name = "costoUnitario", Type = "currency", Label = "Costo Unitario" }, new() { Name = "sconto", Type = "percent", Label = "Sconto %" }, new() { Name = "totale", Type = "currency", Label = "Totale" }, new() { Name = "note", Type = "string", Label = "Note" } } }, new() { Name = "altriCosti", Label = "Altri Costi", Description = "Costi aggiuntivi dell'evento", Fields = new List { new() { Name = "descrizione", Type = "string", Label = "Descrizione" }, new() { Name = "costoUnitario", Type = "currency", Label = "Costo Unitario" }, new() { Name = "quantita", Type = "number", Label = "Quantità" }, new() { Name = "aliquotaIva", Type = "percent", Label = "Aliquota IVA" }, new() { Name = "totale", Type = "currency", Label = "Totale" } } }, new() { Name = "dettagliRisorse", Label = "Risorse Assegnate", Description = "Personale assegnato all'evento", Fields = new List { new() { Name = "risorsa.nome", Type = "string", Label = "Nome" }, new() { Name = "risorsa.cognome", Type = "string", Label = "Cognome" }, new() { Name = "risorsa.telefono", Type = "string", Label = "Telefono" }, new() { Name = "risorsa.tipoRisorsa.descrizione", Type = "string", Label = "Ruolo" }, new() { Name = "oraInizio", Type = "time", Label = "Ora Inizio" }, new() { Name = "oraFine", Type = "time", Label = "Ora Fine" }, new() { Name = "oreLavoro", Type = "number", Label = "Ore Lavoro" }, new() { Name = "costo", Type = "currency", Label = "Costo" }, new() { Name = "note", Type = "string", Label = "Note" } } }, new() { Name = "acconti", Label = "Acconti", Description = "Pagamenti anticipati ricevuti", Fields = new List { new() { Name = "descrizione", Type = "string", Label = "Descrizione" }, new() { Name = "importo", Type = "currency", Label = "Importo" }, new() { Name = "dataPagamento", Type = "date", Label = "Data Pagamento" }, new() { Name = "metodoPagamento", Type = "string", Label = "Metodo Pagamento" }, new() { Name = "note", Type = "string", Label = "Note" } } }, new() { Name = "dettagliPrelievo", Label = "Lista Prelievo", Description = "Articoli necessari per l'evento", Fields = new List { new() { Name = "articolo.codice", Type = "string", Label = "Codice Articolo" }, new() { Name = "articolo.descrizione", Type = "string", Label = "Descrizione" }, new() { Name = "articolo.categoria.descrizione", Type = "string", Label = "Categoria" }, new() { Name = "articolo.unitaMisura", Type = "string", Label = "U.M." }, new() { Name = "qtaRichiesta", Type = "number", Label = "Qtà Richiesta" }, new() { Name = "qtaCalcolata", Type = "number", Label = "Qtà Calcolata" }, new() { Name = "qtaEffettiva", Type = "number", Label = "Qtà Effettiva" }, new() { Name = "note", Type = "string", Label = "Note" } } }, new() { Name = "degustazioni", Label = "Degustazioni", Description = "Degustazioni programmate", Fields = new List { new() { Name = "data", Type = "date", Label = "Data" }, new() { Name = "ora", Type = "time", Label = "Ora" }, new() { Name = "numeroPartecipanti", Type = "number", Label = "Partecipanti" }, new() { Name = "note", Type = "string", Label = "Note" } } } } }; private static DataSchemaDto GetClienteSchema() => new() { EntityType = "Cliente", DatasetId = "cliente", Fields = new List { new() { Name = "id", Type = "number", Label = "ID", Group = "Base" }, new() { Name = "ragioneSociale", Type = "string", Label = "Ragione Sociale", Group = "Base" }, new() { Name = "indirizzo", Type = "string", Label = "Indirizzo", Group = "Indirizzo" }, new() { Name = "cap", Type = "string", Label = "CAP", Group = "Indirizzo" }, new() { Name = "citta", Type = "string", Label = "Città", Group = "Indirizzo" }, new() { Name = "provincia", Type = "string", Label = "Provincia", Group = "Indirizzo" }, new() { Name = "telefono", Type = "string", Label = "Telefono", Group = "Contatti" }, new() { Name = "email", Type = "string", Label = "Email", Group = "Contatti" }, new() { Name = "pec", Type = "string", Label = "PEC", Group = "Contatti" }, new() { Name = "codiceFiscale", Type = "string", Label = "Codice Fiscale", Group = "Fiscale" }, new() { Name = "partitaIva", Type = "string", Label = "Partita IVA", Group = "Fiscale" }, new() { Name = "codiceDestinatario", Type = "string", Label = "Codice Destinatario SDI", Group = "Fiscale" }, new() { Name = "note", Type = "string", Label = "Note", Group = "Base" }, }, ChildCollections = new List() }; private static DataSchemaDto GetLocationSchema() => new() { EntityType = "Location", DatasetId = "location", Fields = new List { new() { Name = "id", Type = "number", Label = "ID", Group = "Base" }, new() { Name = "nome", Type = "string", Label = "Nome", Group = "Base" }, new() { Name = "indirizzo", Type = "string", Label = "Indirizzo", Group = "Indirizzo" }, new() { Name = "cap", Type = "string", Label = "CAP", Group = "Indirizzo" }, new() { Name = "citta", Type = "string", Label = "Città", Group = "Indirizzo" }, new() { Name = "provincia", Type = "string", Label = "Provincia", Group = "Indirizzo" }, new() { Name = "telefono", Type = "string", Label = "Telefono", Group = "Contatti" }, new() { Name = "email", Type = "string", Label = "Email", Group = "Contatti" }, new() { Name = "referente", Type = "string", Label = "Referente", Group = "Contatti" }, new() { Name = "distanzaKm", Type = "number", Label = "Distanza (km)", Group = "Base" }, new() { Name = "note", Type = "string", Label = "Note", Group = "Base" }, }, ChildCollections = new List() }; private static DataSchemaDto GetArticoloSchema() => new() { EntityType = "Articolo", DatasetId = "articolo", Fields = new List { new() { Name = "id", Type = "number", Label = "ID", Group = "Base" }, new() { Name = "codice", Type = "string", Label = "Codice", Group = "Base" }, new() { Name = "descrizione", Type = "string", Label = "Descrizione", Group = "Base" }, new() { Name = "unitaMisura", Type = "string", Label = "Unità di Misura", Group = "Base" }, new() { Name = "qtaDisponibile", Type = "number", Label = "Quantità Disponibile", Group = "Quantità" }, new() { Name = "qtaStdA", Type = "number", Label = "Qtà Standard Adulti", Group = "Quantità" }, new() { Name = "qtaStdB", Type = "number", Label = "Qtà Standard Buffet", Group = "Quantità" }, new() { Name = "qtaStdS", Type = "number", Label = "Qtà Standard Seduti", Group = "Quantità" }, new() { Name = "categoria.codice", Type = "string", Label = "Codice Categoria", Group = "Categoria" }, new() { Name = "categoria.descrizione", Type = "string", Label = "Categoria", Group = "Categoria" }, new() { Name = "categoria.coeffA", Type = "number", Label = "Coefficiente Adulti", Group = "Categoria" }, new() { Name = "categoria.coeffB", Type = "number", Label = "Coefficiente Buffet", Group = "Categoria" }, new() { Name = "categoria.coeffS", Type = "number", Label = "Coefficiente Seduti", Group = "Categoria" }, new() { Name = "tipoMateriale.codice", Type = "string", Label = "Codice Tipo Materiale", Group = "Materiale" }, new() { Name = "tipoMateriale.descrizione", Type = "string", Label = "Tipo Materiale", Group = "Materiale" }, new() { Name = "note", Type = "string", Label = "Note", Group = "Base" }, }, ChildCollections = new List() }; private static DataSchemaDto GetRisorsaSchema() => new() { EntityType = "Risorsa", DatasetId = "risorsa", Fields = new List { new() { Name = "id", Type = "number", Label = "ID", Group = "Base" }, new() { Name = "nome", Type = "string", Label = "Nome", Group = "Base" }, new() { Name = "cognome", Type = "string", Label = "Cognome", Group = "Base" }, new() { Name = "telefono", Type = "string", Label = "Telefono", Group = "Contatti" }, new() { Name = "email", Type = "string", Label = "Email", Group = "Contatti" }, new() { Name = "tipoRisorsa.codice", Type = "string", Label = "Codice Tipo", Group = "Tipo" }, new() { Name = "tipoRisorsa.descrizione", Type = "string", Label = "Tipo Risorsa", Group = "Tipo" }, new() { Name = "note", Type = "string", Label = "Note", Group = "Base" }, }, ChildCollections = new List() }; private static DataSchemaDto GetTipoEventoSchema() => new() { EntityType = "Tipo Evento", DatasetId = "tipoEvento", Fields = new List { new() { Name = "id", Type = "number", Label = "ID" }, new() { Name = "codice", Type = "string", Label = "Codice" }, new() { Name = "descrizione", Type = "string", Label = "Descrizione" }, new() { Name = "tipoPasto.codice", Type = "string", Label = "Codice Tipo Pasto" }, new() { Name = "tipoPasto.descrizione", Type = "string", Label = "Tipo Pasto" }, }, ChildCollections = new List() }; private static DataSchemaDto GetTipoOspiteSchema() => new() { EntityType = "Tipo Ospite", DatasetId = "tipoOspite", Fields = new List { new() { Name = "id", Type = "number", Label = "ID" }, new() { Name = "codice", Type = "string", Label = "Codice" }, new() { Name = "descrizione", Type = "string", Label = "Descrizione" }, }, ChildCollections = new List() }; private static DataSchemaDto GetCategoriaSchema() => new() { EntityType = "Categoria Articoli", DatasetId = "categoria", Fields = new List { new() { Name = "id", Type = "number", Label = "ID" }, new() { Name = "codice", Type = "string", Label = "Codice" }, new() { Name = "descrizione", Type = "string", Label = "Descrizione" }, new() { Name = "coeffA", Type = "number", Label = "Coefficiente Adulti" }, new() { Name = "coeffB", Type = "number", Label = "Coefficiente Buffet" }, new() { Name = "coeffS", Type = "number", Label = "Coefficiente Seduti" }, }, ChildCollections = new List() }; private static DataSchemaDto GetTipoRisorsaSchema() => new() { EntityType = "Tipo Risorsa", DatasetId = "tipoRisorsa", Fields = new List { new() { Name = "id", Type = "number", Label = "ID" }, new() { Name = "codice", Type = "string", Label = "Codice" }, new() { Name = "descrizione", Type = "string", Label = "Descrizione" }, }, ChildCollections = new List() }; private static DataSchemaDto GetTipoMaterialeSchema() => new() { EntityType = "Tipo Materiale", DatasetId = "tipoMateriale", Fields = new List { new() { Name = "id", Type = "number", Label = "ID" }, new() { Name = "codice", Type = "string", Label = "Codice" }, new() { Name = "descrizione", Type = "string", Label = "Descrizione" }, }, ChildCollections = new List() }; #endregion } // DTOs public class DebugBindingRequest { public List DataSources { get; set; } = new(); public string? PropertyName { get; set; } } public class PreviewReportRequest { public int TemplateId { get; set; } public List 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 Fields { get; set; } = new(); public List 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 Fields { get; set; } = new(); }