2025-08-14 15:27:45 -03:00
|
|
|
// src/Elecciones.Api/Controllers/ResultadosController.cs
|
|
|
|
|
using Elecciones.Core.DTOs.ApiResponses;
|
|
|
|
|
using Elecciones.Database;
|
|
|
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
|
|
|
|
namespace Elecciones.Api.Controllers;
|
|
|
|
|
|
|
|
|
|
[ApiController]
|
|
|
|
|
[Route("api/[controller]")]
|
|
|
|
|
public class ResultadosController : ControllerBase
|
|
|
|
|
{
|
|
|
|
|
private readonly EleccionesDbContext _dbContext;
|
|
|
|
|
private readonly ILogger<ResultadosController> _logger;
|
|
|
|
|
|
|
|
|
|
public ResultadosController(EleccionesDbContext dbContext, ILogger<ResultadosController> logger)
|
|
|
|
|
{
|
|
|
|
|
_dbContext = dbContext;
|
|
|
|
|
_logger = logger;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-22 21:55:03 -03:00
|
|
|
[HttpGet("partido/{seccionId}")]
|
|
|
|
|
public async Task<IActionResult> GetResultadosPorPartido(string seccionId)
|
2025-08-14 15:27:45 -03:00
|
|
|
{
|
2025-08-22 21:55:03 -03:00
|
|
|
// 1. Buscamos el ámbito geográfico correspondiente al PARTIDO (Nivel 30)
|
2025-08-14 15:27:45 -03:00
|
|
|
var ambito = await _dbContext.AmbitosGeograficos
|
|
|
|
|
.AsNoTracking()
|
2025-08-22 21:55:03 -03:00
|
|
|
// CAMBIO CLAVE: Buscamos por SeccionId y NivelId para ser precisos
|
|
|
|
|
.FirstOrDefaultAsync(a => a.SeccionId == seccionId && a.NivelId == 30);
|
2025-08-14 15:27:45 -03:00
|
|
|
|
|
|
|
|
if (ambito == null)
|
|
|
|
|
{
|
2025-08-22 21:55:03 -03:00
|
|
|
return NotFound(new { message = $"No se encontró el partido con ID {seccionId}" });
|
2025-08-14 15:27:45 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Buscamos el estado del recuento para ese ámbito
|
|
|
|
|
var estadoRecuento = await _dbContext.EstadosRecuentos
|
|
|
|
|
.AsNoTracking()
|
|
|
|
|
.FirstOrDefaultAsync(e => e.AmbitoGeograficoId == ambito.Id);
|
|
|
|
|
|
|
|
|
|
if (estadoRecuento == null)
|
|
|
|
|
{
|
2025-08-22 21:55:03 -03:00
|
|
|
// Devolvemos una respuesta vacía pero válida para el frontend
|
|
|
|
|
return Ok(new MunicipioResultadosDto
|
|
|
|
|
{
|
|
|
|
|
MunicipioNombre = ambito.Nombre,
|
|
|
|
|
UltimaActualizacion = DateTime.UtcNow,
|
|
|
|
|
PorcentajeEscrutado = 0,
|
|
|
|
|
PorcentajeParticipacion = 0,
|
|
|
|
|
Resultados = new List<AgrupacionResultadoDto>(),
|
|
|
|
|
VotosAdicionales = new VotosAdicionalesDto()
|
|
|
|
|
});
|
2025-08-14 15:27:45 -03:00
|
|
|
}
|
|
|
|
|
|
2025-08-22 21:55:03 -03:00
|
|
|
// 3. Buscamos todos los votos para ese ámbito
|
2025-08-14 15:27:45 -03:00
|
|
|
var resultadosVotos = await _dbContext.ResultadosVotos
|
|
|
|
|
.AsNoTracking()
|
2025-08-22 21:55:03 -03:00
|
|
|
.Include(rv => rv.AgrupacionPolitica)
|
2025-08-14 15:27:45 -03:00
|
|
|
.Where(rv => rv.AmbitoGeograficoId == ambito.Id)
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
2025-08-22 21:55:03 -03:00
|
|
|
// 4. Calculamos el total de votos positivos
|
2025-08-14 15:27:45 -03:00
|
|
|
long totalVotosPositivos = resultadosVotos.Sum(r => r.CantidadVotos);
|
|
|
|
|
|
2025-08-22 21:55:03 -03:00
|
|
|
// 5. Mapeamos al DTO de respuesta
|
2025-08-14 15:27:45 -03:00
|
|
|
var respuestaDto = new MunicipioResultadosDto
|
|
|
|
|
{
|
|
|
|
|
MunicipioNombre = ambito.Nombre,
|
|
|
|
|
UltimaActualizacion = estadoRecuento.FechaTotalizacion,
|
2025-08-22 21:55:03 -03:00
|
|
|
PorcentajeEscrutado = estadoRecuento.MesasTotalizadasPorcentaje,
|
2025-08-14 15:27:45 -03:00
|
|
|
PorcentajeParticipacion = estadoRecuento.ParticipacionPorcentaje,
|
|
|
|
|
Resultados = resultadosVotos.Select(rv => new AgrupacionResultadoDto
|
|
|
|
|
{
|
|
|
|
|
Nombre = rv.AgrupacionPolitica.Nombre,
|
|
|
|
|
Votos = rv.CantidadVotos,
|
|
|
|
|
Porcentaje = totalVotosPositivos > 0 ? (rv.CantidadVotos * 100.0m / totalVotosPositivos) : 0
|
|
|
|
|
}).OrderByDescending(r => r.Votos).ToList(),
|
|
|
|
|
VotosAdicionales = new VotosAdicionalesDto
|
|
|
|
|
{
|
|
|
|
|
EnBlanco = estadoRecuento.VotosEnBlanco,
|
|
|
|
|
Nulos = estadoRecuento.VotosNulos,
|
|
|
|
|
Recurridos = estadoRecuento.VotosRecurridos
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return Ok(respuestaDto);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[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.
|
|
|
|
|
|
|
|
|
|
var ambito = await _dbContext.AmbitosGeograficos.AsNoTracking()
|
|
|
|
|
.FirstOrDefaultAsync(a => a.DistritoId == distritoId && a.NivelId == 10);
|
|
|
|
|
|
|
|
|
|
if (ambito == null)
|
|
|
|
|
{
|
|
|
|
|
return NotFound(new { message = "No se encontró la provincia" });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Simulación
|
|
|
|
|
var random = new Random();
|
|
|
|
|
var respuestaSimulada = 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 }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return Ok(await Task.FromResult(respuestaSimulada));
|
|
|
|
|
}
|
2025-08-15 17:31:51 -03:00
|
|
|
|
|
|
|
|
[HttpGet("bancas/{seccionId}")]
|
|
|
|
|
public async Task<IActionResult> GetBancasPorSeccion(string seccionId)
|
|
|
|
|
{
|
|
|
|
|
// 1. Buscamos el ámbito de la sección electoral
|
|
|
|
|
var seccion = await _dbContext.AmbitosGeograficos
|
|
|
|
|
.AsNoTracking()
|
|
|
|
|
.FirstOrDefaultAsync(a => a.SeccionId == seccionId && a.NivelId == 4); // Nivel 4 = Sección Electoral
|
|
|
|
|
|
|
|
|
|
if (seccion == null)
|
|
|
|
|
{
|
|
|
|
|
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
|
|
|
|
|
var proyecciones = await _dbContext.ProyeccionesBancas
|
|
|
|
|
.AsNoTracking()
|
|
|
|
|
.Include(p => p.AgrupacionPolitica) // Incluimos el nombre del partido
|
|
|
|
|
.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
|
|
|
|
|
})
|
|
|
|
|
.OrderByDescending(p => p.Bancas)
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
|
|
|
if (!proyecciones.Any())
|
|
|
|
|
{
|
|
|
|
|
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
|
|
|
|
|
return Ok(new
|
|
|
|
|
{
|
|
|
|
|
SeccionNombre = seccion.Nombre,
|
|
|
|
|
Proyeccion = proyecciones
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[HttpGet("mapa")]
|
|
|
|
|
public async Task<IActionResult> GetResultadosParaMapa()
|
|
|
|
|
{
|
|
|
|
|
var maxVotosPorAmbito = _dbContext.ResultadosVotos
|
|
|
|
|
.GroupBy(rv => rv.AmbitoGeograficoId)
|
|
|
|
|
.Select(g => new
|
|
|
|
|
{
|
|
|
|
|
AmbitoId = g.Key,
|
|
|
|
|
MaxVotos = g.Max(v => v.CantidadVotos)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var resultadosGanadores = await _dbContext.ResultadosVotos
|
|
|
|
|
.Join(
|
|
|
|
|
maxVotosPorAmbito,
|
|
|
|
|
voto => new { AmbitoId = voto.AmbitoGeograficoId, Votos = voto.CantidadVotos },
|
|
|
|
|
max => new { AmbitoId = max.AmbitoId, Votos = max.MaxVotos },
|
2025-08-22 21:55:03 -03:00
|
|
|
(voto, max) => voto
|
2025-08-15 17:31:51 -03:00
|
|
|
)
|
2025-08-22 21:55:03 -03:00
|
|
|
.Include(rv => rv.AmbitoGeografico)
|
|
|
|
|
.Where(rv => rv.AmbitoGeografico.NivelId == 30) // Aseguramos que solo sean los ámbitos de nivel 30
|
2025-08-15 17:31:51 -03:00
|
|
|
.Select(rv => new
|
|
|
|
|
{
|
2025-08-22 21:55:03 -03:00
|
|
|
// CORRECCIÓN CLAVE: Devolvemos los campos que el frontend necesita para funcionar.
|
|
|
|
|
|
|
|
|
|
// 1. El ID de la BD para hacer clic y pedir detalles.
|
|
|
|
|
AmbitoId = rv.AmbitoGeografico.Id,
|
|
|
|
|
|
|
|
|
|
// 2. El NOMBRE del departamento/municipio para encontrar y colorear el polígono.
|
|
|
|
|
DepartamentoNombre = rv.AmbitoGeografico.Nombre,
|
|
|
|
|
|
|
|
|
|
// 3. El ID del partido ganador.
|
2025-08-15 17:31:51 -03:00
|
|
|
AgrupacionGanadoraId = rv.AgrupacionPoliticaId
|
|
|
|
|
})
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
|
|
|
return Ok(resultadosGanadores);
|
|
|
|
|
}
|
2025-08-22 21:55:03 -03:00
|
|
|
|
|
|
|
|
[HttpGet("municipio/{ambitoId}")] // Cambiamos el nombre del parámetro de ruta
|
|
|
|
|
public async Task<IActionResult> GetResultadosPorMunicipio(int ambitoId) // Cambiamos el tipo de string a int
|
|
|
|
|
{
|
|
|
|
|
_logger.LogInformation("Buscando resultados para AmbitoGeograficoId: {AmbitoId}", ambitoId);
|
|
|
|
|
|
|
|
|
|
// PASO 1: Buscar el Ámbito Geográfico directamente por su CLAVE PRIMARIA (AmbitoGeograficoId).
|
|
|
|
|
var ambito = await _dbContext.AmbitosGeograficos
|
|
|
|
|
.AsNoTracking()
|
|
|
|
|
.FirstOrDefaultAsync(a => a.Id == ambitoId && a.NivelId == 30); // Usamos a.Id == ambitoId
|
|
|
|
|
|
|
|
|
|
if (ambito == null)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogWarning("No se encontró el ámbito para el ID interno: {AmbitoId} o no es Nivel 30.", ambitoId);
|
|
|
|
|
return NotFound(new { message = $"No se encontró el municipio con ID interno {ambitoId}" });
|
|
|
|
|
}
|
|
|
|
|
_logger.LogInformation("Ámbito encontrado: Id={AmbitoId}, Nombre={AmbitoNombre}", ambito.Id, ambito.Nombre);
|
|
|
|
|
|
|
|
|
|
// PASO 2: Usar la CLAVE PRIMARIA (ambito.Id) para buscar el estado del recuento.
|
|
|
|
|
var estadoRecuento = await _dbContext.EstadosRecuentos
|
|
|
|
|
.AsNoTracking()
|
|
|
|
|
.FirstOrDefaultAsync(e => e.AmbitoGeograficoId == ambito.Id);
|
|
|
|
|
|
|
|
|
|
if (estadoRecuento == null)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogWarning("No se encontró EstadoRecuento para AmbitoGeograficoId: {AmbitoId}", ambito.Id);
|
|
|
|
|
return NotFound(new { message = $"No se han encontrado resultados de recuento para el municipio {ambito.Nombre}" });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PASO 3: Usar la CLAVE PRIMARIA (ambito.Id) para buscar los votos.
|
|
|
|
|
var resultadosVotos = await _dbContext.ResultadosVotos
|
|
|
|
|
.AsNoTracking()
|
|
|
|
|
.Include(rv => rv.AgrupacionPolitica) // Incluimos el nombre del partido
|
|
|
|
|
.Where(rv => rv.AmbitoGeograficoId == ambito.Id)
|
|
|
|
|
.OrderByDescending(rv => rv.CantidadVotos)
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
|
|
|
// PASO 4: Calcular el total de votos positivos para el porcentaje.
|
|
|
|
|
long totalVotosPositivos = resultadosVotos.Sum(r => r.CantidadVotos);
|
|
|
|
|
|
|
|
|
|
// PASO 5: Mapear todo al DTO de respuesta que el frontend espera.
|
|
|
|
|
var respuestaDto = new MunicipioResultadosDto
|
|
|
|
|
{
|
|
|
|
|
MunicipioNombre = ambito.Nombre,
|
|
|
|
|
UltimaActualizacion = estadoRecuento.FechaTotalizacion,
|
|
|
|
|
PorcentajeEscrutado = estadoRecuento.MesasTotalizadasPorcentaje,
|
|
|
|
|
PorcentajeParticipacion = estadoRecuento.ParticipacionPorcentaje,
|
|
|
|
|
Resultados = resultadosVotos.Select(rv => new AgrupacionResultadoDto
|
|
|
|
|
{
|
|
|
|
|
Nombre = rv.AgrupacionPolitica.Nombre,
|
|
|
|
|
Votos = rv.CantidadVotos,
|
|
|
|
|
Porcentaje = totalVotosPositivos > 0 ? (rv.CantidadVotos * 100.0m / totalVotosPositivos) : 0
|
|
|
|
|
}).ToList(),
|
|
|
|
|
VotosAdicionales = new VotosAdicionalesDto
|
|
|
|
|
{
|
|
|
|
|
EnBlanco = estadoRecuento.VotosEnBlanco,
|
|
|
|
|
Nulos = estadoRecuento.VotosNulos,
|
|
|
|
|
Recurridos = estadoRecuento.VotosRecurridos
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return Ok(respuestaDto);
|
|
|
|
|
}
|
2025-08-14 15:27:45 -03:00
|
|
|
}
|