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();
}