Feat Widgets

This commit is contained in:
2025-09-01 14:04:40 -03:00
parent 608ae655be
commit 12860f2406
30 changed files with 1904 additions and 247 deletions

View File

@@ -92,59 +92,69 @@ public class ResultadosController : ControllerBase
[HttpGet("provincia/{distritoId}")]
public async Task<IActionResult> GetResultadosProvinciales(string distritoId)
{
_logger.LogInformation("Solicitud de resultados para la provincia con distritoId: {DistritoId}", distritoId);
// PASO 1: Encontrar el ámbito geográfico de la provincia.
var provincia = await _dbContext.AmbitosGeograficos.AsNoTracking()
.FirstOrDefaultAsync(a => a.DistritoId == distritoId && a.NivelId == 10);
if (provincia == null)
{
_logger.LogWarning("No se encontró la provincia con distritoId: {DistritoId}", distritoId);
return NotFound(new { message = $"No se encontró la provincia con distritoId {distritoId}" });
}
// PASO 2: Obtener el estado general del recuento para la provincia.
// Como las estadísticas generales (mesas, participación) son las mismas para todas las categorías,
// simplemente tomamos la primera que encontremos para este ámbito.
var estadoGeneral = await _dbContext.EstadosRecuentosGenerales.AsNoTracking()
.FirstOrDefaultAsync(e => e.AmbitoGeograficoId == provincia.Id);
// PASO 3: Obtener el resumen de votos por agrupación para la provincia.
// Hacemos un JOIN manual entre ResumenesVotos y AgrupacionesPoliticas para obtener los nombres.
var resultados = await _dbContext.ResumenesVotos
.AsNoTracking()
.Where(r => r.AmbitoGeograficoId == provincia.Id)
.Join(
_dbContext.AgrupacionesPoliticas.AsNoTracking(),
resumen => resumen.AgrupacionPoliticaId,
agrupacion => agrupacion.Id,
(resumen, agrupacion) => new AgrupacionResultadoDto
{
Nombre = agrupacion.Nombre,
Votos = resumen.Votos,
Porcentaje = resumen.VotosPorcentaje
})
.OrderByDescending(r => r.Votos)
var todosLosResumenes = await _dbContext.ResumenesVotos.AsNoTracking()
.Include(r => r.AgrupacionPolitica)
.ToListAsync();
// PASO 4: Construir el objeto de respuesta (DTO).
// Si no hay datos de recuento aún, usamos valores por defecto para evitar errores en el frontend.
var respuestaDto = new ResumenProvincialDto
{
ProvinciaNombre = provincia.Nombre,
UltimaActualizacion = estadoGeneral?.FechaTotalizacion ?? DateTime.UtcNow,
PorcentajeEscrutado = estadoGeneral?.MesasTotalizadasPorcentaje ?? 0,
PorcentajeParticipacion = estadoGeneral?.ParticipacionPorcentaje ?? 0,
Resultados = resultados,
// NOTA: Los votos adicionales (nulos, en blanco) no están en la tabla de resumen provincial.
// Esto es una mejora pendiente en el Worker. Por ahora, devolvemos 0.
VotosAdicionales = new VotosAdicionalesDto { EnBlanco = 0, Nulos = 0, Recurridos = 0 }
};
// OBTENER TODOS LOS LOGOS EN UNA SOLA CONSULTA
var logosLookup = (await _dbContext.LogosAgrupacionesCategorias.AsNoTracking().ToListAsync())
.ToLookup(l => $"{l.AgrupacionPoliticaId}-{l.CategoriaId}");
_logger.LogInformation("Devolviendo {NumResultados} resultados de agrupaciones para la provincia.", respuestaDto.Resultados.Count);
if (provincia == null) return NotFound($"No se encontró la provincia con distritoId {distritoId}");
return Ok(respuestaDto);
var estadosPorCategoria = await _dbContext.EstadosRecuentosGenerales.AsNoTracking()
.Include(e => e.CategoriaElectoral)
.Where(e => e.AmbitoGeograficoId == provincia.Id)
.ToDictionaryAsync(e => e.CategoriaId);
var resultadosPorMunicipio = await _dbContext.ResultadosVotos
.AsNoTracking()
.Include(r => r.AgrupacionPolitica)
.Where(r => r.AmbitoGeografico.NivelId == 30)
.ToListAsync();
var resultadosAgrupados = resultadosPorMunicipio
.GroupBy(r => r.CategoriaId)
.Select(g => new
{
CategoriaId = g.Key,
TotalVotosCategoria = g.Sum(r => r.CantidadVotos),
// Agrupamos por el ID de la agrupación, no por el objeto
Resultados = g.GroupBy(r => r.AgrupacionPoliticaId)
.Select(partidoGroup => new
{
// El objeto Agrupacion lo tomamos del primer elemento del grupo
Agrupacion = partidoGroup.First().AgrupacionPolitica,
Votos = partidoGroup.Sum(r => r.CantidadVotos)
})
.ToList()
})
.Select(g => new
{
g.CategoriaId,
CategoriaNombre = estadosPorCategoria.ContainsKey(g.CategoriaId) ? estadosPorCategoria[g.CategoriaId].CategoriaElectoral.Nombre : "Desconocido",
EstadoRecuento = estadosPorCategoria.GetValueOrDefault(g.CategoriaId),
Resultados = g.Resultados
.Select(r => new
{
r.Agrupacion.Id,
r.Agrupacion.Nombre,
r.Agrupacion.NombreCorto,
r.Agrupacion.Color,
LogoUrl = logosLookup[$"{r.Agrupacion.Id}-{g.CategoriaId}"].FirstOrDefault()?.LogoUrl,
r.Votos,
VotosPorcentaje = g.TotalVotosCategoria > 0 ? ((decimal)r.Votos * 100 / g.TotalVotosCategoria) : 0
})
.OrderByDescending(r => r.Votos)
.ToList()
})
.OrderBy(c => c.CategoriaId)
.ToList();
return Ok(resultadosAgrupados);
}
@@ -503,4 +513,77 @@ public class ResultadosController : ControllerBase
return Ok(bancadasConOcupantes);
}
[HttpGet("configuracion-publica")]
public async Task<IActionResult> GetConfiguracionPublica()
{
// Definimos una lista de las claves de configuración que son seguras para el público.
// De esta manera, si en el futuro añadimos claves sensibles (como contraseñas de API, etc.),
// nunca se expondrán accidentalmente.
var clavesPublicas = new List<string>
{
"TickerResultadosCantidad",
"ConcejalesResultadosCantidad"
// "OtraClavePublica"
};
var configuracionPublica = await _dbContext.Configuraciones
.AsNoTracking()
.Where(c => clavesPublicas.Contains(c.Clave))
.ToDictionaryAsync(c => c.Clave, c => c.Valor);
return Ok(configuracionPublica);
}
[HttpGet("concejales/{seccionId}")]
public async Task<IActionResult> GetResultadosConcejalesPorSeccion(string seccionId)
{
// 1. Encontrar todos los municipios (Nivel 30) que pertenecen a la sección dada (Nivel 20)
var municipiosDeLaSeccion = await _dbContext.AmbitosGeograficos
.AsNoTracking()
.Where(a => a.NivelId == 30 && a.SeccionProvincialId == seccionId)
.Select(a => a.Id) // Solo necesitamos sus IDs
.ToListAsync();
if (!municipiosDeLaSeccion.Any())
{
return Ok(new List<object>());
}
// 2. Obtener todos los resultados de la categoría Concejales (ID 7) para esos municipios
var resultadosMunicipales = await _dbContext.ResultadosVotos
.AsNoTracking()
.Include(r => r.AgrupacionPolitica)
.Where(r => r.CategoriaId == 7 && municipiosDeLaSeccion.Contains(r.AmbitoGeograficoId))
.ToListAsync();
var logosConcejales = await _dbContext.LogosAgrupacionesCategorias
.AsNoTracking()
.Where(l => l.CategoriaId == 7)
.ToDictionaryAsync(l => l.AgrupacionPoliticaId);
// 3. Agrupar y sumar en memoria para obtener el total por partido para la sección
var totalVotosSeccion = resultadosMunicipales.Sum(r => r.CantidadVotos);
var resultadosFinales = resultadosMunicipales
.GroupBy(r => r.AgrupacionPolitica)
.Select(g => new
{
Agrupacion = g.Key,
Votos = g.Sum(r => r.CantidadVotos)
})
.OrderByDescending(r => r.Votos)
.Select(r => new
{
r.Agrupacion.Id,
r.Agrupacion.Nombre,
r.Agrupacion.NombreCorto,
r.Agrupacion.Color,
LogoUrl = logosConcejales.GetValueOrDefault(r.Agrupacion.Id)?.LogoUrl,
r.Votos,
votosPorcentaje = totalVotosSeccion > 0 ? ((decimal)r.Votos * 100 / totalVotosSeccion) : 0
})
.ToList();
return Ok(resultadosFinales);
}
}