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