Feat Prototipos Widgets y Fix Worker Telegramas

This commit is contained in:
2025-08-25 10:25:54 -03:00
parent 8192185bc5
commit 4a6318c18a
42 changed files with 1635 additions and 1092 deletions

View File

@@ -3,6 +3,8 @@ using Elecciones.Core.DTOs.ApiResponses;
using Elecciones.Database;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@@ -91,59 +93,85 @@ public class ResultadosController : ControllerBase
[HttpGet("provincia/{distritoId}")]
public async Task<IActionResult> GetResultadosProvinciales(string distritoId)
{
// TODO: Esta lógica debe ser reemplazada para leer datos reales de la BD
// cuando el worker comience a ingestar los totales a nivel provincial.
// Por ahora, devolvemos datos simulados para permitir el desarrollo del frontend.
_logger.LogInformation("Solicitud de resultados para la provincia con distritoId: {DistritoId}", distritoId);
var ambito = await _dbContext.AmbitosGeograficos.AsNoTracking()
.FirstOrDefaultAsync(a => a.DistritoId == distritoId && a.NivelId == 10);
// 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 (ambito == null)
if (provincia == null)
{
return NotFound(new { message = "No se encontró la provincia" });
_logger.LogWarning("No se encontró la provincia con distritoId: {DistritoId}", distritoId);
return NotFound(new { message = $"No se encontró la provincia con distritoId {distritoId}" });
}
// Simulación
var random = new Random();
var respuestaSimulada = new ResumenProvincialDto
// 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)
.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 = ambito.Nombre,
UltimaActualizacion = DateTime.UtcNow,
PorcentajeEscrutado = 78.45m,
PorcentajeParticipacion = 65.12m,
Resultados =
[
new() { Nombre = "ALIANZA POR EL FUTURO", Votos = 2500000 + random.Next(1, 1000), Porcentaje = 45.12m },
new() { Nombre = "FRENTE DE AVANZADA", Votos = 2100000 + random.Next(1, 1000), Porcentaje = 38.78m },
new() { Nombre = "UNION POPULAR", Votos = 800000 + random.Next(1, 1000), Porcentaje = 14.10m },
],
VotosAdicionales = new VotosAdicionalesDto { EnBlanco = 150000, Nulos = 80000, Recurridos = 1200 }
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 }
};
return Ok(await Task.FromResult(respuestaSimulada));
_logger.LogInformation("Devolviendo {NumResultados} resultados de agrupaciones para la provincia.", respuestaDto.Resultados.Count);
return Ok(respuestaDto);
}
[HttpGet("bancas/{seccionId}")]
public async Task<IActionResult> GetBancasPorSeccion(string seccionId)
{
// 1. Buscamos el ámbito de la sección electoral
// 1. Buscamos el ámbito usando 'SeccionProvincialId'.
// La API oficial usa este campo para las secciones electorales.
// Además, el worker guarda estas secciones con NivelId = 20, por lo que lo usamos aquí para consistencia.
var seccion = await _dbContext.AmbitosGeograficos
.AsNoTracking()
.FirstOrDefaultAsync(a => a.SeccionId == seccionId && a.NivelId == 4); // Nivel 4 = Sección Electoral
.FirstOrDefaultAsync(a => a.SeccionProvincialId == seccionId && a.NivelId == 20); // Nivel 20 = Sección Electoral Provincial
if (seccion == null)
{
_logger.LogWarning("No se encontró la sección electoral con SeccionProvincialId: {SeccionId}", seccionId);
return NotFound(new { message = $"No se encontró la sección electoral con ID {seccionId}" });
}
// 2. Buscamos todas las proyecciones para ese ámbito, incluyendo el nombre de la agrupación
// 2. Buscamos todas las proyecciones para ese ámbito (usando su clave primaria 'Id')
var proyecciones = await _dbContext.ProyeccionesBancas
.AsNoTracking()
.Include(p => p.AgrupacionPolitica) // Incluimos el nombre del partido
.Include(p => p.AgrupacionPolitica)
.Where(p => p.AmbitoGeograficoId == seccion.Id)
.Select(p => new
{
// Creamos un objeto anónimo para la respuesta, más limpio que un DTO para este caso simple
AgrupacionNombre = p.AgrupacionPolitica.Nombre,
Bancas = p.NroBancas
})
@@ -152,10 +180,12 @@ public class ResultadosController : ControllerBase
if (!proyecciones.Any())
{
// Este caso es posible si aún no hay proyecciones calculadas para esta sección.
_logger.LogWarning("No se encontraron proyecciones de bancas para la sección: {SeccionNombre}", seccion.Nombre);
return NotFound(new { message = $"No se han encontrado proyecciones de bancas para la sección {seccion.Nombre}" });
}
// 3. Devolvemos un objeto que contiene el nombre de la sección y la lista de resultados
// 3. Devolvemos la respuesta
return Ok(new
{
SeccionNombre = seccion.Nombre,
@@ -263,4 +293,40 @@ public class ResultadosController : ControllerBase
return Ok(respuestaDto);
}
[HttpGet("composicion-congreso")]
public IActionResult GetComposicionCongreso()
{
var diputados = new
{
CamaraNombre = "Cámara de Diputados",
TotalBancas = 92,
BancasEnJuego = 46,
Partidos = new[]
{
// --- DATOS ACTUALIZADOS CON 'BANCASENJUEGO' ---
new { Id = "501", Nombre = "CANDIDATURA 501", BancasTotales = 2, BancasEnJuego = 1, Color = "#d62728" },
new { Id = "513", Nombre = "CANDIDATURA 513", BancasTotales = 37, BancasEnJuego = 19, Color = "#1f77b4" },
new { Id = "516", Nombre = "CANDIDATURA 516", BancasTotales = 18, BancasEnJuego = 8, Color = "#2ca02c" },
new { Id = "511", Nombre = "CANDIDATURA 511", BancasTotales = 22, BancasEnJuego = 12, Color = "#ff7f0e" },
new { Id = "507", Nombre = "CANDIDATURA 507", BancasTotales = 13, BancasEnJuego = 6, Color = "#9467bd" }
}
};
var senadores = new
{
CamaraNombre = "Cámara de Senadores",
TotalBancas = 46,
BancasEnJuego = 23,
Partidos = new[]
{
new { Id = "513_S", Nombre = "CANDIDATURA 513", BancasTotales = 21, BancasEnJuego = 10, Color = "#1f77b4" },
new { Id = "516_S", Nombre = "CANDIDATURA 516", BancasTotales = 9, BancasEnJuego = 5, Color = "#2ca02c" },
new { Id = "511_S", Nombre = "CANDIDATURA 511", BancasTotales = 11, BancasEnJuego = 6, Color = "#ff7f0e" },
new { Id = "507_S", Nombre = "CANDIDATURA 507", BancasTotales = 5, BancasEnJuego = 2, Color = "#9467bd" }
}
};
return Ok(new { Diputados = diputados, Senadores = senadores });
}
}