559 lines
20 KiB
C#
559 lines
20 KiB
C#
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene tutti i virtual dataset
|
|
/// </summary>
|
|
[HttpGet]
|
|
public async Task<ActionResult<List<VirtualDatasetDto>>> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene un virtual dataset per ID
|
|
/// </summary>
|
|
[HttpGet("{id}")]
|
|
public async Task<ActionResult<VirtualDatasetDto>> 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
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene un virtual dataset per nome
|
|
/// </summary>
|
|
[HttpGet("by-name/{nome}")]
|
|
public async Task<ActionResult<VirtualDatasetDto>> 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
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Crea un nuovo virtual dataset
|
|
/// </summary>
|
|
[HttpPost]
|
|
public async Task<ActionResult<VirtualDatasetDto>> 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
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Aggiorna un virtual dataset
|
|
/// </summary>
|
|
[HttpPut("{id}")]
|
|
public async Task<ActionResult<VirtualDatasetDto>> 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
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Elimina un virtual dataset
|
|
/// </summary>
|
|
[HttpDelete("{id}")]
|
|
public async Task<IActionResult> Delete(int id)
|
|
{
|
|
var dataset = await _context.VirtualDatasets.FindAsync(id);
|
|
if (dataset == null)
|
|
return NotFound();
|
|
|
|
_context.VirtualDatasets.Remove(dataset);
|
|
await _context.SaveChangesAsync();
|
|
|
|
return NoContent();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Duplica un virtual dataset
|
|
/// </summary>
|
|
[HttpPost("{id}/clone")]
|
|
public async Task<ActionResult<VirtualDatasetDto>> 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
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene le categorie disponibili
|
|
/// </summary>
|
|
[HttpGet("categories")]
|
|
public async Task<ActionResult<List<string>>> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Valida la configurazione di un virtual dataset
|
|
/// </summary>
|
|
[HttpPost("validate")]
|
|
public ActionResult<ValidationResult> Validate([FromBody] VirtualDatasetConfiguration config)
|
|
{
|
|
var errors = new List<string>();
|
|
var warnings = new List<string>();
|
|
|
|
// 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
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene lo schema risultante dal virtual dataset
|
|
/// </summary>
|
|
[HttpPost("schema")]
|
|
public ActionResult<DataSchemaDto> GetVirtualSchema([FromBody] VirtualDatasetConfiguration config)
|
|
{
|
|
var fields = new List<DataFieldDto>();
|
|
|
|
// 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<DataCollectionDto>()
|
|
};
|
|
}
|
|
|
|
private static VirtualDatasetConfiguration DeserializeConfig(string json)
|
|
{
|
|
try
|
|
{
|
|
return JsonSerializer.Deserialize<VirtualDatasetConfiguration>(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<DataFieldDto>
|
|
{
|
|
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<DataCollectionDto>()
|
|
},
|
|
"cliente" => new DataSchemaDto
|
|
{
|
|
EntityType = "Cliente",
|
|
DatasetId = "cliente",
|
|
Fields = new List<DataFieldDto>
|
|
{
|
|
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<DataCollectionDto>()
|
|
},
|
|
"location" => new DataSchemaDto
|
|
{
|
|
EntityType = "Location",
|
|
DatasetId = "location",
|
|
Fields = new List<DataFieldDto>
|
|
{
|
|
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<DataCollectionDto>()
|
|
},
|
|
"articolo" => new DataSchemaDto
|
|
{
|
|
EntityType = "Articolo",
|
|
DatasetId = "articolo",
|
|
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 = "qtaDisponibile", Type = "number", Label = "Qtà Disponibile" },
|
|
new() { Name = "categoriaId", Type = "number", Label = "ID Categoria" },
|
|
},
|
|
ChildCollections = new List<DataCollectionDto>()
|
|
},
|
|
"risorsa" => new DataSchemaDto
|
|
{
|
|
EntityType = "Risorsa",
|
|
DatasetId = "risorsa",
|
|
Fields = new List<DataFieldDto>
|
|
{
|
|
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<DataCollectionDto>()
|
|
},
|
|
_ => 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<string> Errors { get; set; } = new();
|
|
public List<string> Warnings { get; set; } = new();
|
|
}
|