using System.Text.Json; 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/virtual-datasets")] public class VirtualDatasetsController : ControllerBase { private readonly ZentralDbContext _context; public VirtualDatasetsController(ZentralDbContext context) { _context = context; } /// /// Ottiene tutti i virtual dataset /// [HttpGet] public async Task>> GetAll([FromQuery] string? categoria = null) { var query = _context.VirtualDatasets.Where(v => v.Attivo); if (!string.IsNullOrWhiteSpace(categoria)) { query = query.Where(v => v.Categoria == categoria); } var datasets = await query .OrderBy(v => v.DisplayName) .Select(v => new VirtualDatasetDto { Id = v.Id, Nome = v.Nome, DisplayName = v.DisplayName, Descrizione = v.Descrizione, Icon = v.Icon, Categoria = v.Categoria, Configuration = DeserializeConfig(v.ConfigurationJson), Attivo = v.Attivo, CreatedAt = v.CreatedAt, UpdatedAt = v.UpdatedAt }) .ToListAsync(); return datasets; } /// /// Ottiene un virtual dataset per ID /// [HttpGet("{id}")] public async Task> GetById(int id) { var dataset = await _context.VirtualDatasets.FindAsync(id); if (dataset == null) return NotFound(); return new VirtualDatasetDto { Id = dataset.Id, Nome = dataset.Nome, DisplayName = dataset.DisplayName, Descrizione = dataset.Descrizione, Icon = dataset.Icon, Categoria = dataset.Categoria, Configuration = DeserializeConfig(dataset.ConfigurationJson), Attivo = dataset.Attivo, CreatedAt = dataset.CreatedAt, UpdatedAt = dataset.UpdatedAt }; } /// /// Ottiene un virtual dataset per nome /// [HttpGet("by-name/{nome}")] public async Task> GetByName(string nome) { var dataset = await _context.VirtualDatasets .FirstOrDefaultAsync(v => v.Nome == nome && v.Attivo); if (dataset == null) return NotFound(); return new VirtualDatasetDto { Id = dataset.Id, Nome = dataset.Nome, DisplayName = dataset.DisplayName, Descrizione = dataset.Descrizione, Icon = dataset.Icon, Categoria = dataset.Categoria, Configuration = DeserializeConfig(dataset.ConfigurationJson), Attivo = dataset.Attivo, CreatedAt = dataset.CreatedAt, UpdatedAt = dataset.UpdatedAt }; } /// /// Crea un nuovo virtual dataset /// [HttpPost] public async Task> Create([FromBody] CreateVirtualDatasetRequest request) { // Verifica nome univoco var exists = await _context.VirtualDatasets.AnyAsync(v => v.Nome == request.Nome); if (exists) return BadRequest($"Un dataset con nome '{request.Nome}' esiste già"); var dataset = new VirtualDataset { Nome = request.Nome, DisplayName = request.DisplayName, Descrizione = request.Descrizione, Icon = request.Icon ?? "dataset", Categoria = request.Categoria ?? "Personalizzato", ConfigurationJson = SerializeConfig(request.Configuration), Attivo = true, CreatedAt = DateTime.UtcNow }; _context.VirtualDatasets.Add(dataset); await _context.SaveChangesAsync(); return CreatedAtAction(nameof(GetById), new { id = dataset.Id }, new VirtualDatasetDto { Id = dataset.Id, Nome = dataset.Nome, DisplayName = dataset.DisplayName, Descrizione = dataset.Descrizione, Icon = dataset.Icon, Categoria = dataset.Categoria, Configuration = request.Configuration, Attivo = dataset.Attivo, CreatedAt = dataset.CreatedAt }); } /// /// Aggiorna un virtual dataset /// [HttpPut("{id}")] public async Task> Update(int id, [FromBody] UpdateVirtualDatasetRequest request) { var dataset = await _context.VirtualDatasets.FindAsync(id); if (dataset == null) return NotFound(); // Verifica nome univoco se cambiato if (request.Nome != null && request.Nome != dataset.Nome) { var exists = await _context.VirtualDatasets.AnyAsync(v => v.Nome == request.Nome && v.Id != id); if (exists) return BadRequest($"Un dataset con nome '{request.Nome}' esiste già"); dataset.Nome = request.Nome; } if (request.DisplayName != null) dataset.DisplayName = request.DisplayName; if (request.Descrizione != null) dataset.Descrizione = request.Descrizione; if (request.Icon != null) dataset.Icon = request.Icon; if (request.Categoria != null) dataset.Categoria = request.Categoria; if (request.Configuration != null) dataset.ConfigurationJson = SerializeConfig(request.Configuration); if (request.Attivo.HasValue) dataset.Attivo = request.Attivo.Value; dataset.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); return new VirtualDatasetDto { Id = dataset.Id, Nome = dataset.Nome, DisplayName = dataset.DisplayName, Descrizione = dataset.Descrizione, Icon = dataset.Icon, Categoria = dataset.Categoria, Configuration = DeserializeConfig(dataset.ConfigurationJson), Attivo = dataset.Attivo, CreatedAt = dataset.CreatedAt, UpdatedAt = dataset.UpdatedAt }; } /// /// Elimina un virtual dataset /// [HttpDelete("{id}")] public async Task Delete(int id) { var dataset = await _context.VirtualDatasets.FindAsync(id); if (dataset == null) return NotFound(); _context.VirtualDatasets.Remove(dataset); await _context.SaveChangesAsync(); return NoContent(); } /// /// Duplica un virtual dataset /// [HttpPost("{id}/clone")] public async Task> Clone(int id) { var source = await _context.VirtualDatasets.FindAsync(id); if (source == null) return NotFound(); // Genera nome univoco var baseName = $"{source.Nome}_copia"; var newName = baseName; var counter = 1; while (await _context.VirtualDatasets.AnyAsync(v => v.Nome == newName)) { newName = $"{baseName}_{counter++}"; } var clone = new VirtualDataset { Nome = newName, DisplayName = $"{source.DisplayName} (Copia)", Descrizione = source.Descrizione, Icon = source.Icon, Categoria = source.Categoria, ConfigurationJson = source.ConfigurationJson, Attivo = true, CreatedAt = DateTime.UtcNow }; _context.VirtualDatasets.Add(clone); await _context.SaveChangesAsync(); return CreatedAtAction(nameof(GetById), new { id = clone.Id }, new VirtualDatasetDto { Id = clone.Id, Nome = clone.Nome, DisplayName = clone.DisplayName, Descrizione = clone.Descrizione, Icon = clone.Icon, Categoria = clone.Categoria, Configuration = DeserializeConfig(clone.ConfigurationJson), Attivo = clone.Attivo, CreatedAt = clone.CreatedAt }); } /// /// Ottiene le categorie disponibili /// [HttpGet("categories")] public async Task>> GetCategories() { var categories = await _context.VirtualDatasets .Where(v => v.Attivo) .Select(v => v.Categoria) .Distinct() .OrderBy(c => c) .ToListAsync(); // Aggiungi categoria default se non presente if (!categories.Contains("Personalizzato")) categories.Insert(0, "Personalizzato"); return categories; } /// /// Valida la configurazione di un virtual dataset /// [HttpPost("validate")] public ActionResult Validate([FromBody] VirtualDatasetConfiguration config) { var errors = new List(); var warnings = new List(); // Verifica che ci sia almeno una sorgente if (config.Sources.Count == 0) { errors.Add("È necessario almeno un dataset sorgente"); } // Verifica che ci sia un dataset primario if (config.Sources.Count > 0 && !config.Sources.Any(s => s.IsPrimary)) { errors.Add("È necessario definire un dataset primario"); } // Verifica che gli alias siano univoci var duplicateAliases = config.Sources .GroupBy(s => s.Alias) .Where(g => g.Count() > 1) .Select(g => g.Key) .ToList(); if (duplicateAliases.Count > 0) { errors.Add($"Alias duplicati: {string.Join(", ", duplicateAliases)}"); } // Verifica le relazioni foreach (var rel in config.Relationships) { var fromSource = config.Sources.FirstOrDefault(s => s.Id == rel.FromSourceId); var toSource = config.Sources.FirstOrDefault(s => s.Id == rel.ToSourceId); if (fromSource == null) errors.Add($"Relazione '{rel.Label ?? rel.Id}': sorgente di partenza non trovata"); if (toSource == null) errors.Add($"Relazione '{rel.Label ?? rel.Id}': sorgente di destinazione non trovata"); } // Verifica i filtri foreach (var filter in config.Filters.Where(f => f.Enabled)) { var source = config.Sources.FirstOrDefault(s => s.Id == filter.SourceId); if (source == null) errors.Add($"Filtro su campo '{filter.Field}': sorgente non trovata"); } // Warning per configurazioni potenzialmente problematiche if (config.Sources.Count > 1 && config.Relationships.Count == 0) { warnings.Add("Sono presenti più sorgenti ma nessuna relazione è definita. I dati saranno combinati come prodotto cartesiano."); } if (config.OutputFields.Count == 0) { warnings.Add("Nessun campo di output selezionato. Verranno inclusi tutti i campi disponibili."); } return new ValidationResult { IsValid = errors.Count == 0, Errors = errors, Warnings = warnings }; } /// /// Ottiene lo schema risultante dal virtual dataset /// [HttpPost("schema")] public ActionResult GetVirtualSchema([FromBody] VirtualDatasetConfiguration config) { var fields = new List(); // Se ci sono output fields specifici, usa quelli if (config.OutputFields.Count > 0 && 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; fields.Add(new DataFieldDto { Name = outputField.Alias ?? $"{source.Alias}.{outputField.FieldName}", Label = outputField.Label ?? outputField.FieldName, Type = "string", // TODO: determinare il tipo dal dataset sorgente Group = outputField.Group ?? source.Alias }); } } else { // Altrimenti, includi tutti i campi da tutte le sorgenti foreach (var source in config.Sources) { var sourceSchema = GetBaseDatasetSchema(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 = "Virtual Dataset", DatasetId = "virtual", Fields = fields, ChildCollections = new List() }; } private static VirtualDatasetConfiguration DeserializeConfig(string json) { try { return JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new VirtualDatasetConfiguration(); } catch { return new VirtualDatasetConfiguration(); } } private static string SerializeConfig(VirtualDatasetConfiguration? config) { return JsonSerializer.Serialize(config ?? new VirtualDatasetConfiguration(), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); } private DataSchemaDto? GetBaseDatasetSchema(string datasetId) { // Riutilizza la logica di ReportsController per ottenere lo schema base // TODO: estrarre in un servizio condiviso return datasetId.ToLower() switch { "evento" => new DataSchemaDto { EntityType = "Evento", DatasetId = "evento", Fields = new List { new() { Name = "id", Type = "number", Label = "ID" }, new() { Name = "codice", Type = "string", Label = "Codice" }, new() { Name = "dataEvento", Type = "date", Label = "Data Evento" }, new() { Name = "descrizione", Type = "string", Label = "Descrizione" }, new() { Name = "stato", Type = "number", Label = "Stato" }, new() { Name = "numeroOspiti", Type = "number", Label = "Numero Ospiti" }, new() { Name = "costoTotale", Type = "currency", Label = "Costo Totale" }, new() { Name = "clienteId", Type = "number", Label = "ID Cliente" }, new() { Name = "locationId", Type = "number", Label = "ID Location" }, }, ChildCollections = new List() }, "cliente" => new DataSchemaDto { EntityType = "Cliente", DatasetId = "cliente", Fields = new List { new() { Name = "id", Type = "number", Label = "ID" }, new() { Name = "ragioneSociale", Type = "string", Label = "Ragione Sociale" }, new() { Name = "indirizzo", Type = "string", Label = "Indirizzo" }, new() { Name = "citta", Type = "string", Label = "Città" }, new() { Name = "telefono", Type = "string", Label = "Telefono" }, new() { Name = "email", Type = "string", Label = "Email" }, new() { Name = "partitaIva", Type = "string", Label = "Partita IVA" }, }, ChildCollections = new List() }, "location" => new DataSchemaDto { EntityType = "Location", DatasetId = "location", Fields = new List { new() { Name = "id", Type = "number", Label = "ID" }, new() { Name = "nome", Type = "string", Label = "Nome" }, new() { Name = "indirizzo", Type = "string", Label = "Indirizzo" }, new() { Name = "citta", Type = "string", Label = "Città" }, new() { Name = "distanzaKm", Type = "number", Label = "Distanza (km)" }, }, ChildCollections = new List() }, "articolo" => new DataSchemaDto { EntityType = "Articolo", DatasetId = "articolo", 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 = "qtaDisponibile", Type = "number", Label = "Qtà Disponibile" }, new() { Name = "categoriaId", Type = "number", Label = "ID Categoria" }, }, ChildCollections = new List() }, "risorsa" => new DataSchemaDto { EntityType = "Risorsa", DatasetId = "risorsa", Fields = new List { new() { Name = "id", Type = "number", Label = "ID" }, new() { Name = "nome", Type = "string", Label = "Nome" }, new() { Name = "cognome", Type = "string", Label = "Cognome" }, new() { Name = "telefono", Type = "string", Label = "Telefono" }, new() { Name = "tipoRisorsaId", Type = "number", Label = "ID Tipo Risorsa" }, }, ChildCollections = new List() }, _ => null }; } } // DTOs public class VirtualDatasetDto { public int Id { get; set; } public string Nome { get; set; } = string.Empty; public string DisplayName { get; set; } = string.Empty; public string? Descrizione { get; set; } public string Icon { get; set; } = "dataset"; public string Categoria { get; set; } = "Personalizzato"; public VirtualDatasetConfiguration? Configuration { get; set; } public bool Attivo { get; set; } public DateTime? CreatedAt { get; set; } public DateTime? UpdatedAt { get; set; } } public class CreateVirtualDatasetRequest { public string Nome { get; set; } = string.Empty; public string DisplayName { get; set; } = string.Empty; public string? Descrizione { get; set; } public string? Icon { get; set; } public string? Categoria { get; set; } public VirtualDatasetConfiguration? Configuration { get; set; } } public class UpdateVirtualDatasetRequest { public string? Nome { get; set; } public string? DisplayName { get; set; } public string? Descrizione { get; set; } public string? Icon { get; set; } public string? Categoria { get; set; } public VirtualDatasetConfiguration? Configuration { get; set; } public bool? Attivo { get; set; } } public class ValidationResult { public bool IsValid { get; set; } public List Errors { get; set; } = new(); public List Warnings { get; set; } = new(); }