| 
									
										
										
										
											2025-08-14 15:27:45 -03:00
										 |  |  | // src/Elecciones.Api/Controllers/ResultadosController.cs | 
					
						
							|  |  |  | using Elecciones.Core.DTOs.ApiResponses; | 
					
						
							|  |  |  | using Elecciones.Database; | 
					
						
							| 
									
										
										
										
											2025-08-29 09:54:22 -03:00
										 |  |  | using Elecciones.Database.Entities; | 
					
						
							| 
									
										
										
										
											2025-08-14 15:27:45 -03:00
										 |  |  | using Microsoft.AspNetCore.Mvc; | 
					
						
							|  |  |  | using Microsoft.EntityFrameworkCore; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | namespace Elecciones.Api.Controllers; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | [ApiController] | 
					
						
							|  |  |  | [Route("api/[controller]")]
 | 
					
						
							|  |  |  | public class ResultadosController : ControllerBase | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     private readonly EleccionesDbContext _dbContext; | 
					
						
							|  |  |  |     private readonly ILogger<ResultadosController> _logger; | 
					
						
							| 
									
										
										
										
											2025-08-25 15:04:09 -03:00
										 |  |  |     private readonly IConfiguration _configuration; | 
					
						
							| 
									
										
										
										
											2025-08-14 15:27:45 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-25 15:04:09 -03:00
										 |  |  |     public ResultadosController(EleccionesDbContext dbContext, ILogger<ResultadosController> logger, IConfiguration configuration) | 
					
						
							| 
									
										
										
										
											2025-08-14 15:27:45 -03:00
										 |  |  |     { | 
					
						
							|  |  |  |         _dbContext = dbContext; | 
					
						
							|  |  |  |         _logger = logger; | 
					
						
							| 
									
										
										
										
											2025-08-25 15:04:09 -03:00
										 |  |  |         _configuration = configuration; | 
					
						
							| 
									
										
										
										
											2025-08-14 15:27:45 -03:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											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) | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2025-08-25 10:25:54 -03:00
										 |  |  |         _logger.LogInformation("Solicitud de resultados para la provincia con distritoId: {DistritoId}", distritoId); | 
					
						
							| 
									
										
										
										
											2025-08-14 15:27:45 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-25 10:25:54 -03:00
										 |  |  |         // PASO 1: Encontrar el ámbito geográfico de la provincia. | 
					
						
							|  |  |  |         var provincia = await _dbContext.AmbitosGeograficos.AsNoTracking() | 
					
						
							|  |  |  |             .FirstOrDefaultAsync(a => a.DistritoId == distritoId && a.NivelId == 10); | 
					
						
							| 
									
										
										
										
											2025-08-14 15:27:45 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-25 10:25:54 -03:00
										 |  |  |         if (provincia == null) | 
					
						
							| 
									
										
										
										
											2025-08-14 15:27:45 -03:00
										 |  |  |         { | 
					
						
							| 
									
										
										
										
											2025-08-25 10:25:54 -03:00
										 |  |  |             _logger.LogWarning("No se encontró la provincia con distritoId: {DistritoId}", distritoId); | 
					
						
							|  |  |  |             return NotFound(new { message = $"No se encontró la provincia con distritoId {distritoId}" }); | 
					
						
							| 
									
										
										
										
											2025-08-14 15:27:45 -03:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-25 10:25:54 -03:00
										 |  |  |         // 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 | 
					
						
							| 
									
										
										
										
											2025-08-14 15:27:45 -03:00
										 |  |  |         { | 
					
						
							| 
									
										
										
										
											2025-08-25 10:25:54 -03:00
										 |  |  |             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 } | 
					
						
							| 
									
										
										
										
											2025-08-14 15:27:45 -03:00
										 |  |  |         }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-25 10:25:54 -03:00
										 |  |  |         _logger.LogInformation("Devolviendo {NumResultados} resultados de agrupaciones para la provincia.", respuestaDto.Resultados.Count); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return Ok(respuestaDto); | 
					
						
							| 
									
										
										
										
											2025-08-14 15:27:45 -03:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-08-15 17:31:51 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-25 10:25:54 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-15 17:31:51 -03:00
										 |  |  |     [HttpGet("bancas/{seccionId}")] | 
					
						
							|  |  |  |     public async Task<IActionResult> GetBancasPorSeccion(string seccionId) | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2025-08-25 10:25:54 -03:00
										 |  |  |         // 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. | 
					
						
							| 
									
										
										
										
											2025-08-15 17:31:51 -03:00
										 |  |  |         var seccion = await _dbContext.AmbitosGeograficos | 
					
						
							|  |  |  |             .AsNoTracking() | 
					
						
							| 
									
										
										
										
											2025-08-25 10:25:54 -03:00
										 |  |  |             .FirstOrDefaultAsync(a => a.SeccionProvincialId == seccionId && a.NivelId == 20); // Nivel 20 = Sección Electoral Provincial | 
					
						
							| 
									
										
										
										
											2025-08-15 17:31:51 -03:00
										 |  |  | 
 | 
					
						
							|  |  |  |         if (seccion == null) | 
					
						
							|  |  |  |         { | 
					
						
							| 
									
										
										
										
											2025-08-25 10:25:54 -03:00
										 |  |  |             _logger.LogWarning("No se encontró la sección electoral con SeccionProvincialId: {SeccionId}", seccionId); | 
					
						
							| 
									
										
										
										
											2025-08-15 17:31:51 -03:00
										 |  |  |             return NotFound(new { message = $"No se encontró la sección electoral con ID {seccionId}" }); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-25 10:25:54 -03:00
										 |  |  |         // 2. Buscamos todas las proyecciones para ese ámbito (usando su clave primaria 'Id') | 
					
						
							| 
									
										
										
										
											2025-08-15 17:31:51 -03:00
										 |  |  |         var proyecciones = await _dbContext.ProyeccionesBancas | 
					
						
							|  |  |  |             .AsNoTracking() | 
					
						
							| 
									
										
										
										
											2025-08-25 10:25:54 -03:00
										 |  |  |             .Include(p => p.AgrupacionPolitica) | 
					
						
							| 
									
										
										
										
											2025-08-15 17:31:51 -03:00
										 |  |  |             .Where(p => p.AmbitoGeograficoId == seccion.Id) | 
					
						
							|  |  |  |             .Select(p => new | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 AgrupacionNombre = p.AgrupacionPolitica.Nombre, | 
					
						
							|  |  |  |                 Bancas = p.NroBancas | 
					
						
							|  |  |  |             }) | 
					
						
							|  |  |  |             .OrderByDescending(p => p.Bancas) | 
					
						
							|  |  |  |             .ToListAsync(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (!proyecciones.Any()) | 
					
						
							|  |  |  |         { | 
					
						
							| 
									
										
										
										
											2025-08-25 10:25:54 -03:00
										 |  |  |             // 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); | 
					
						
							| 
									
										
										
										
											2025-08-15 17:31:51 -03:00
										 |  |  |             return NotFound(new { message = $"No se han encontrado proyecciones de bancas para la sección {seccion.Nombre}" }); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-25 10:25:54 -03:00
										 |  |  |         // 3. Devolvemos la respuesta | 
					
						
							| 
									
										
										
										
											2025-08-15 17:31:51 -03:00
										 |  |  |         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 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:54:22 -03:00
										 |  |  |     .Join( | 
					
						
							|  |  |  |         maxVotosPorAmbito, | 
					
						
							|  |  |  |         voto => new { AmbitoId = voto.AmbitoGeograficoId, Votos = voto.CantidadVotos }, | 
					
						
							|  |  |  |         max => new { AmbitoId = max.AmbitoId, Votos = max.MaxVotos }, | 
					
						
							|  |  |  |         (voto, max) => voto | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  |     .Include(rv => rv.AmbitoGeografico) | 
					
						
							|  |  |  |     .Include(rv => rv.AgrupacionPolitica) | 
					
						
							|  |  |  |     .Where(rv => rv.AmbitoGeografico.NivelId == 30) | 
					
						
							|  |  |  |     .Select(rv => new | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         AmbitoId = rv.AmbitoGeografico.Id, | 
					
						
							|  |  |  |         DepartamentoNombre = rv.AmbitoGeografico.Nombre, | 
					
						
							|  |  |  |         AgrupacionGanadoraId = rv.AgrupacionPoliticaId, | 
					
						
							|  |  |  |         ColorGanador = rv.AgrupacionPolitica.Color | 
					
						
							|  |  |  |     }) | 
					
						
							|  |  |  |     .ToListAsync(); | 
					
						
							| 
									
										
										
										
											2025-08-15 17:31:51 -03:00
										 |  |  | 
 | 
					
						
							|  |  |  |         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-25 10:25:54 -03:00
										 |  |  | 
 | 
					
						
							|  |  |  |     [HttpGet("composicion-congreso")] | 
					
						
							| 
									
										
										
										
											2025-08-29 09:54:22 -03:00
										 |  |  |     public async Task<IActionResult> GetComposicionCongreso() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         var config = await _dbContext.Configuraciones | 
					
						
							|  |  |  |             .AsNoTracking() | 
					
						
							|  |  |  |             .ToDictionaryAsync(c => c.Clave, c => c.Valor); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 15:49:13 -03:00
										 |  |  |         // Aquí está el interruptor | 
					
						
							| 
									
										
										
										
											2025-08-29 09:54:22 -03:00
										 |  |  |         config.TryGetValue("UsarDatosDeBancadasOficiales", out var usarDatosOficialesValue); | 
					
						
							|  |  |  |         bool usarDatosOficiales = usarDatosOficialesValue == "true"; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (usarDatosOficiales) | 
					
						
							|  |  |  |         { | 
					
						
							| 
									
										
										
										
											2025-08-29 15:49:13 -03:00
										 |  |  |             // Si el interruptor está en 'true', llama a este método | 
					
						
							| 
									
										
										
										
											2025-08-29 09:54:22 -03:00
										 |  |  |             return await GetComposicionDesdeBancadasOficiales(config); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         else | 
					
						
							|  |  |  |         { | 
					
						
							| 
									
										
										
										
											2025-08-29 15:49:13 -03:00
										 |  |  |             // Si está en 'false' o no existe, llama a este otro | 
					
						
							| 
									
										
										
										
											2025-08-29 09:54:22 -03:00
										 |  |  |             return await GetComposicionDesdeProyecciones(config); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-30 11:31:45 -03:00
										 |  |  |     // En ResultadosController.cs | 
					
						
							| 
									
										
										
										
											2025-08-29 09:54:22 -03:00
										 |  |  |     private async Task<IActionResult> GetComposicionDesdeBancadasOficiales(Dictionary<string, string> config) | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2025-08-30 11:31:45 -03:00
										 |  |  |         config.TryGetValue("MostrarOcupantes", out var mostrarOcupantesValue); | 
					
						
							|  |  |  |         bool mostrarOcupantes = mostrarOcupantesValue == "true"; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         IQueryable<Bancada> bancadasQuery = _dbContext.Bancadas.AsNoTracking() | 
					
						
							|  |  |  |                                              .Include(b => b.AgrupacionPolitica); | 
					
						
							|  |  |  |         if (mostrarOcupantes) | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |             bancadasQuery = bancadasQuery.Include(b => b.Ocupante); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         var bancadas = await bancadasQuery.ToListAsync(); | 
					
						
							| 
									
										
										
										
											2025-08-29 09:54:22 -03:00
										 |  |  | 
 | 
					
						
							|  |  |  |         var bancasPorAgrupacion = bancadas | 
					
						
							| 
									
										
										
										
											2025-08-29 15:49:13 -03:00
										 |  |  |             .Where(b => b.AgrupacionPolitica != null) | 
					
						
							| 
									
										
										
										
											2025-08-30 11:31:45 -03:00
										 |  |  |             // Agrupamos por el ID del partido, que es un valor único y estable | 
					
						
							|  |  |  |             .GroupBy(b => b.AgrupacionPolitica!.Id) | 
					
						
							|  |  |  |             .Select(g => | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 // Tomamos la información de la agrupación del primer elemento (todas son iguales) | 
					
						
							|  |  |  |                 var primeraBancaDelGrupo = g.First(); | 
					
						
							|  |  |  |                 var agrupacion = primeraBancaDelGrupo.AgrupacionPolitica!; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 // Filtramos los ocupantes solo para este grupo | 
					
						
							|  |  |  |                 var ocupantesDelPartido = mostrarOcupantes | 
					
						
							|  |  |  |                     ? g.Select(b => b.Ocupante).Where(o => o != null).ToList() | 
					
						
							|  |  |  |                     : new List<OcupanteBanca?>(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 return new | 
					
						
							|  |  |  |                 { | 
					
						
							|  |  |  |                     Agrupacion = agrupacion, | 
					
						
							|  |  |  |                     Camara = primeraBancaDelGrupo.Camara, | 
					
						
							|  |  |  |                     BancasTotales = g.Count(), | 
					
						
							|  |  |  |                     Ocupantes = ocupantesDelPartido | 
					
						
							|  |  |  |                 }; | 
					
						
							|  |  |  |             }) | 
					
						
							| 
									
										
										
										
											2025-08-29 09:54:22 -03:00
										 |  |  |             .ToList(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-30 11:31:45 -03:00
										 |  |  |         // --- FIN DE LA CORRECCIÓN CLAVE --- | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:54:22 -03:00
										 |  |  |         var presidenteDiputados = bancasPorAgrupacion | 
					
						
							|  |  |  |             .Where(b => b.Camara == Core.Enums.TipoCamara.Diputados) | 
					
						
							|  |  |  |             .OrderByDescending(b => b.BancasTotales) | 
					
						
							|  |  |  |             .FirstOrDefault()?.Agrupacion; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         config.TryGetValue("PresidenciaSenadores", out var idPartidoPresidenteSenadores); | 
					
						
							|  |  |  |         var presidenteSenadores = await _dbContext.AgrupacionesPoliticas.FindAsync(idPartidoPresidenteSenadores); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-30 11:31:45 -03:00
										 |  |  |         object MapearPartidos(Core.Enums.TipoCamara camara) | 
					
						
							| 
									
										
										
										
											2025-08-29 09:54:22 -03:00
										 |  |  |         { | 
					
						
							| 
									
										
										
										
											2025-08-29 15:49:13 -03:00
										 |  |  |             var partidosDeCamara = bancasPorAgrupacion.Where(b => b.Camara == camara); | 
					
						
							| 
									
										
										
										
											2025-08-29 09:54:22 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-30 11:31:45 -03:00
										 |  |  |             if (camara == Core.Enums.TipoCamara.Diputados) | 
					
						
							|  |  |  |                 partidosDeCamara = partidosDeCamara.OrderBy(p => p.Agrupacion.OrdenDiputados ?? 999); | 
					
						
							|  |  |  |             else | 
					
						
							|  |  |  |                 partidosDeCamara = partidosDeCamara.OrderBy(p => p.Agrupacion.OrdenSenadores ?? 999); | 
					
						
							| 
									
										
										
										
											2025-08-29 09:54:22 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-30 11:31:45 -03:00
										 |  |  |             return partidosDeCamara | 
					
						
							|  |  |  |                 .OrderByDescending(p => p.BancasTotales) | 
					
						
							|  |  |  |                 .Select(p => new | 
					
						
							|  |  |  |                 { | 
					
						
							|  |  |  |                     p.Agrupacion.Id, | 
					
						
							|  |  |  |                     p.Agrupacion.Nombre, | 
					
						
							|  |  |  |                     p.Agrupacion.NombreCorto, | 
					
						
							|  |  |  |                     p.Agrupacion.Color, | 
					
						
							|  |  |  |                     p.BancasTotales, | 
					
						
							|  |  |  |                     p.Ocupantes // Pasamos la lista de ocupantes ya filtrada | 
					
						
							|  |  |  |                 }).ToList(); | 
					
						
							| 
									
										
										
										
											2025-08-29 09:54:22 -03:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         var diputados = new | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |             CamaraNombre = "Cámara de Diputados", | 
					
						
							|  |  |  |             TotalBancas = 92, | 
					
						
							|  |  |  |             BancasEnJuego = 0, | 
					
						
							| 
									
										
										
										
											2025-08-30 11:31:45 -03:00
										 |  |  |             Partidos = MapearPartidos(Core.Enums.TipoCamara.Diputados), | 
					
						
							| 
									
										
										
										
											2025-08-29 09:54:22 -03:00
										 |  |  |             PresidenteBancada = presidenteDiputados != null ? new { presidenteDiputados.Color } : null | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         var senadores = new | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |             CamaraNombre = "Cámara de Senadores", | 
					
						
							|  |  |  |             TotalBancas = 46, | 
					
						
							|  |  |  |             BancasEnJuego = 0, | 
					
						
							| 
									
										
										
										
											2025-08-30 11:31:45 -03:00
										 |  |  |             Partidos = MapearPartidos(Core.Enums.TipoCamara.Senadores), | 
					
						
							| 
									
										
										
										
											2025-08-29 09:54:22 -03:00
										 |  |  |             PresidenteBancada = presidenteSenadores != null ? new { presidenteSenadores.Color } : null | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return Ok(new { Diputados = diputados, Senadores = senadores }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     private async Task<IActionResult> GetComposicionDesdeProyecciones(Dictionary<string, string> config) | 
					
						
							| 
									
										
										
										
											2025-08-25 10:25:54 -03:00
										 |  |  |     { | 
					
						
							| 
									
										
										
										
											2025-08-29 09:54:22 -03:00
										 |  |  |         var bancasPorAgrupacion = await _dbContext.ProyeccionesBancas | 
					
						
							|  |  |  |             .AsNoTracking() | 
					
						
							|  |  |  |             .GroupBy(p => new { p.AgrupacionPoliticaId, p.CategoriaId }) | 
					
						
							|  |  |  |             .Select(g => new | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 AgrupacionId = g.Key.AgrupacionPoliticaId, | 
					
						
							|  |  |  |                 CategoriaId = g.Key.CategoriaId, | 
					
						
							|  |  |  |                 BancasTotales = g.Sum(p => p.NroBancas) | 
					
						
							|  |  |  |             }) | 
					
						
							|  |  |  |             .ToListAsync(); | 
					
						
							| 
									
										
										
										
											2025-08-25 10:25:54 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:54:22 -03:00
										 |  |  |         var todasAgrupaciones = await _dbContext.AgrupacionesPoliticas.AsNoTracking().ToDictionaryAsync(a => a.Id); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         config.TryGetValue("PresidenciaSenadores", out var idPartidoPresidenteSenadores); | 
					
						
							|  |  |  |         todasAgrupaciones.TryGetValue(idPartidoPresidenteSenadores ?? "", out var presidenteSenadores); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         string? idPartidoPresidenteDiputados = bancasPorAgrupacion | 
					
						
							|  |  |  |             .Where(b => b.CategoriaId == 6) | 
					
						
							|  |  |  |             .OrderByDescending(b => b.BancasTotales) | 
					
						
							|  |  |  |             .FirstOrDefault()?.AgrupacionId; | 
					
						
							|  |  |  |         todasAgrupaciones.TryGetValue(idPartidoPresidenteDiputados ?? "", out var presidenteDiputados); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         object MapearPartidos(int categoriaId) | 
					
						
							| 
									
										
										
										
											2025-08-25 10:25:54 -03:00
										 |  |  |         { | 
					
						
							| 
									
										
										
										
											2025-08-29 09:54:22 -03:00
										 |  |  |             var partidosDeCamara = bancasPorAgrupacion | 
					
						
							|  |  |  |                 .Where(b => b.CategoriaId == categoriaId && b.BancasTotales > 0) | 
					
						
							|  |  |  |                 .Select(b => new { Bancas = b, Agrupacion = todasAgrupaciones[b.AgrupacionId] }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if (categoriaId == 6) // Diputados | 
					
						
							|  |  |  |                 partidosDeCamara = partidosDeCamara.OrderBy(b => b.Agrupacion.OrdenDiputados ?? 999) | 
					
						
							|  |  |  |                                                    .ThenByDescending(b => b.Bancas.BancasTotales); | 
					
						
							|  |  |  |             else // Senadores | 
					
						
							|  |  |  |                 partidosDeCamara = partidosDeCamara.OrderBy(b => b.Agrupacion.OrdenSenadores ?? 999) | 
					
						
							|  |  |  |                                                    .ThenByDescending(b => b.Bancas.BancasTotales); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             return partidosDeCamara | 
					
						
							| 
									
										
										
										
											2025-08-29 15:49:13 -03:00
										 |  |  |                     .Select(b => new | 
					
						
							|  |  |  |                     { | 
					
						
							|  |  |  |                         b.Agrupacion.Id, | 
					
						
							|  |  |  |                         b.Agrupacion.Nombre, | 
					
						
							|  |  |  |                         b.Agrupacion.NombreCorto, | 
					
						
							|  |  |  |                         b.Agrupacion.Color, | 
					
						
							|  |  |  |                         b.Bancas.BancasTotales, | 
					
						
							|  |  |  |                         Ocupantes = new List<object>() // <-- Siempre vacío en modo proyección | 
					
						
							|  |  |  |                     }) | 
					
						
							|  |  |  |         .ToList(); | 
					
						
							| 
									
										
										
										
											2025-08-29 09:54:22 -03:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         var diputados = new | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |             CamaraNombre = "Cámara de Diputados", | 
					
						
							|  |  |  |             TotalBancas = 92, | 
					
						
							|  |  |  |             BancasEnJuego = 46, | 
					
						
							|  |  |  |             Partidos = MapearPartidos(6), | 
					
						
							|  |  |  |             PresidenteBancada = presidenteDiputados != null ? new { presidenteDiputados.Color } : null | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         var senadores = new | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |             CamaraNombre = "Cámara de Senadores", | 
					
						
							|  |  |  |             TotalBancas = 46, | 
					
						
							|  |  |  |             BancasEnJuego = 23, | 
					
						
							|  |  |  |             Partidos = MapearPartidos(5), | 
					
						
							|  |  |  |             PresidenteBancada = presidenteSenadores != null ? new { presidenteSenadores.Color } : null | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return Ok(new { Diputados = diputados, Senadores = senadores }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     [HttpGet("bancadas-detalle")] | 
					
						
							|  |  |  |     public async Task<IActionResult> GetBancadasConOcupantes() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         var config = await _dbContext.Configuraciones.AsNoTracking().ToDictionaryAsync(c => c.Clave, c => c.Valor); | 
					
						
							| 
									
										
										
										
											2025-08-29 15:49:13 -03:00
										 |  |  | 
 | 
					
						
							|  |  |  |         config.TryGetValue("UsarDatosDeBancadasOficiales", out var usarDatosOficialesValue); | 
					
						
							|  |  |  |         if (usarDatosOficialesValue != "true") | 
					
						
							| 
									
										
										
										
											2025-08-29 09:54:22 -03:00
										 |  |  |         { | 
					
						
							| 
									
										
										
										
											2025-08-29 15:49:13 -03:00
										 |  |  |             // Si el modo oficial no está activo, SIEMPRE devolvemos un array vacío. | 
					
						
							| 
									
										
										
										
											2025-08-29 09:54:22 -03:00
										 |  |  |             return Ok(new List<object>()); | 
					
						
							| 
									
										
										
										
											2025-08-25 15:04:09 -03:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2025-08-25 10:25:54 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 15:49:13 -03:00
										 |  |  |         // Si el modo oficial SÍ está activo, devolvemos los detalles. | 
					
						
							| 
									
										
										
										
											2025-08-29 09:54:22 -03:00
										 |  |  |         var bancadasConOcupantes = await _dbContext.Bancadas | 
					
						
							|  |  |  |             .AsNoTracking() | 
					
						
							|  |  |  |             .Include(b => b.Ocupante) | 
					
						
							| 
									
										
										
										
											2025-08-30 11:31:45 -03:00
										 |  |  |             .Select(b => new | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 b.Id, | 
					
						
							|  |  |  |                 b.Camara, | 
					
						
							|  |  |  |                 b.NumeroBanca, | 
					
						
							|  |  |  |                 b.AgrupacionPoliticaId, | 
					
						
							|  |  |  |                 Ocupante = b.Ocupante | 
					
						
							|  |  |  |             }) | 
					
						
							| 
									
										
										
										
											2025-08-29 15:49:13 -03:00
										 |  |  |             .OrderBy(b => b.Id) | 
					
						
							| 
									
										
										
										
											2025-08-29 09:54:22 -03:00
										 |  |  |             .ToListAsync(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return Ok(bancadasConOcupantes); | 
					
						
							| 
									
										
										
										
											2025-08-25 10:25:54 -03:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-08-14 15:27:45 -03:00
										 |  |  | } |