// src/Elecciones.Api/Controllers/ResultadosController.cs using Elecciones.Core.DTOs.ApiResponses; using Elecciones.Database; using Elecciones.Database.Entities; 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 _logger; private readonly IConfiguration _configuration; public ResultadosController(EleccionesDbContext dbContext, ILogger logger, IConfiguration configuration) { _dbContext = dbContext; _logger = logger; _configuration = configuration; } [HttpGet("partido/{municipioId}")] // Renombramos el parámetro para mayor claridad public async Task GetResultadosPorPartido(string municipioId, [FromQuery] int categoriaId) { var ambito = await _dbContext.AmbitosGeograficos.AsNoTracking() .FirstOrDefaultAsync(a => a.SeccionId == municipioId && a.NivelId == 30); if (ambito == null) { return NotFound(new { message = $"No se encontró el partido con ID {municipioId}" }); } var estadoRecuento = await _dbContext.EstadosRecuentos.AsNoTracking() .FirstOrDefaultAsync(e => e.AmbitoGeograficoId == ambito.Id && e.CategoriaId == categoriaId); var agrupacionIds = await _dbContext.ResultadosVotos .Where(rv => rv.AmbitoGeograficoId == ambito.Id && rv.CategoriaId == categoriaId) .Select(rv => rv.AgrupacionPoliticaId).Distinct().ToListAsync(); var logosRelevantes = await _dbContext.LogosAgrupacionesCategorias.AsNoTracking() .Where(l => l.CategoriaId == categoriaId && // <-- Usamos la categoría del parámetro agrupacionIds.Contains(l.AgrupacionPoliticaId) && (l.AmbitoGeograficoId == null || l.AmbitoGeograficoId == ambito.Id)) .ToListAsync(); var resultadosVotos = await _dbContext.ResultadosVotos.AsNoTracking() .Include(rv => rv.AgrupacionPolitica) .Where(rv => rv.AmbitoGeograficoId == ambito.Id && rv.CategoriaId == categoriaId) .ToListAsync(); long totalVotosPositivos = resultadosVotos.Sum(r => r.CantidadVotos); var respuestaDto = new MunicipioResultadosDto { MunicipioNombre = ambito.Nombre, UltimaActualizacion = estadoRecuento?.FechaTotalizacion ?? DateTime.UtcNow, PorcentajeEscrutado = estadoRecuento?.MesasTotalizadasPorcentaje ?? 0, PorcentajeParticipacion = estadoRecuento?.ParticipacionPorcentaje ?? 0, Resultados = resultadosVotos.Select(rv => { var logoUrl = logosRelevantes.FirstOrDefault(l => l.AgrupacionPoliticaId == rv.AgrupacionPoliticaId && l.AmbitoGeograficoId == ambito.Id)?.LogoUrl ?? logosRelevantes.FirstOrDefault(l => l.AgrupacionPoliticaId == rv.AgrupacionPoliticaId && l.AmbitoGeograficoId == null)?.LogoUrl; return new AgrupacionResultadoDto { Id = rv.AgrupacionPolitica.Id, Nombre = rv.AgrupacionPolitica.Nombre, NombreCorto = rv.AgrupacionPolitica.NombreCorto, Color = rv.AgrupacionPolitica.Color, LogoUrl = logoUrl, Votos = rv.CantidadVotos, Porcentaje = totalVotosPositivos > 0 ? (rv.CantidadVotos * 100.0m / totalVotosPositivos) : 0 }; }).OrderByDescending(r => r.Votos).ToList(), VotosAdicionales = new VotosAdicionalesDto { EnBlanco = estadoRecuento?.VotosEnBlanco ?? 0, Nulos = estadoRecuento?.VotosNulos ?? 0, Recurridos = estadoRecuento?.VotosRecurridos ?? 0 } }; return Ok(respuestaDto); } [HttpGet("provincia/{distritoId}")] public async Task GetResultadosProvinciales(string distritoId) { var provincia = await _dbContext.AmbitosGeograficos.AsNoTracking() .FirstOrDefaultAsync(a => a.DistritoId == distritoId && a.NivelId == 10); if (provincia == null) { return NotFound($"No se encontró la provincia con distritoId {distritoId}"); } 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) // Nivel 30 = Municipio .ToListAsync(); var logos = await _dbContext.LogosAgrupacionesCategorias.AsNoTracking().ToListAsync(); // --- LÓGICA DE AGRUPACIÓN Y CÁLCULO CORREGIDA --- var resultadosAgrupados = resultadosPorMunicipio .GroupBy(r => r.CategoriaId) .Select(g => new { CategoriaId = g.Key, CategoriaNombre = estadosPorCategoria.ContainsKey(g.Key) ? estadosPorCategoria[g.Key].CategoriaElectoral.Nombre : "Desconocido", EstadoRecuento = estadosPorCategoria.GetValueOrDefault(g.Key), TotalVotosCategoria = g.Sum(r => r.CantidadVotos), // Agrupamos por el ID de la agrupación, no por el objeto, para evitar duplicados ResultadosAgrupados = g.GroupBy(r => r.AgrupacionPoliticaId) .Select(partidoGroup => new { Agrupacion = partidoGroup.First().AgrupacionPolitica, Votos = partidoGroup.Sum(r => r.CantidadVotos) }) .ToList() }) .Select(g => new { g.CategoriaId, g.CategoriaNombre, g.EstadoRecuento, Resultados = g.ResultadosAgrupados .Select(r => { var logoUrl = logos.FirstOrDefault(l => l.AgrupacionPoliticaId == r.Agrupacion.Id && l.CategoriaId == g.CategoriaId && l.AmbitoGeograficoId != null)?.LogoUrl ?? logos.FirstOrDefault(l => l.AgrupacionPoliticaId == r.Agrupacion.Id && l.CategoriaId == g.CategoriaId && l.AmbitoGeograficoId == null)?.LogoUrl; return new { Id = r.Agrupacion.Id, r.Agrupacion.Nombre, r.Agrupacion.NombreCorto, r.Agrupacion.Color, LogoUrl = logoUrl, r.Votos, Porcentaje = g.TotalVotosCategoria > 0 ? ((decimal)r.Votos * 100 / g.TotalVotosCategoria) : 0 }; }) .OrderByDescending(r => r.Votos) .ToList() }) .OrderBy(c => c.CategoriaId) .ToList(); return Ok(resultadosAgrupados); } [HttpGet("bancas/{seccionId}")] public async Task GetBancasPorSeccion(string seccionId) { // 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.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 (usando su clave primaria 'Id') var proyecciones = await _dbContext.ProyeccionesBancas .AsNoTracking() .Include(p => p.AgrupacionPolitica) .Where(p => p.AmbitoGeograficoId == seccion.Id) .Select(p => new { AgrupacionNombre = p.AgrupacionPolitica.Nombre, Bancas = p.NroBancas }) .OrderByDescending(p => p.Bancas) .ToListAsync(); 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 la respuesta return Ok(new { SeccionNombre = seccion.Nombre, Proyeccion = proyecciones }); } [HttpGet("mapa")] public async Task 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 }, (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(); return Ok(resultadosGanadores); } [HttpGet("municipio/{ambitoId}")] public async Task GetResultadosPorMunicipio(int ambitoId, [FromQuery] int categoriaId) { _logger.LogInformation("Buscando resultados para AmbitoGeograficoId: {AmbitoId}, CategoriaId: {CategoriaId}", ambitoId, categoriaId); // Validamos que el ámbito exista var ambito = await _dbContext.AmbitosGeograficos.AsNoTracking() .FirstOrDefaultAsync(a => a.Id == ambitoId && a.NivelId == 30); if (ambito == null) { _logger.LogWarning("No se encontró el municipio con ID: {AmbitoId}", ambitoId); return NotFound($"No se encontró el municipio con ID {ambitoId}"); } // Obtenemos el estado del recuento para el ámbito var estadoRecuento = await _dbContext.EstadosRecuentos .AsNoTracking() .FirstOrDefaultAsync(e => e.AmbitoGeograficoId == ambito.Id); // Obtenemos los votos para ESE municipio y ESA categoría var resultadosVotos = await _dbContext.ResultadosVotos .AsNoTracking() .Include(rv => rv.AgrupacionPolitica) .Where(rv => rv.AmbitoGeograficoId == ambitoId && rv.CategoriaId == categoriaId) .ToListAsync(); // Calculamos el total de votos solo para esta selección var totalVotosPositivos = (decimal)resultadosVotos.Sum(r => r.CantidadVotos); // Mapeamos los resultados de los partidos var resultadosPartidosDto = resultadosVotos .OrderByDescending(r => r.CantidadVotos) .Select(rv => new AgrupacionResultadoDto { Id = rv.AgrupacionPolitica.Id, Nombre = rv.AgrupacionPolitica.NombreCorto ?? rv.AgrupacionPolitica.Nombre, Color = rv.AgrupacionPolitica.Color, Votos = rv.CantidadVotos, Porcentaje = totalVotosPositivos > 0 ? (rv.CantidadVotos / totalVotosPositivos) * 100 : 0 }).ToList(); // Construimos la respuesta completa del DTO var respuestaDto = new MunicipioResultadosDto { MunicipioNombre = ambito.Nombre, UltimaActualizacion = estadoRecuento?.FechaTotalizacion ?? DateTime.UtcNow, // Use null-conditional operator PorcentajeEscrutado = estadoRecuento?.MesasTotalizadasPorcentaje ?? 0, PorcentajeParticipacion = estadoRecuento?.ParticipacionPorcentaje ?? 0, Resultados = resultadosPartidosDto, VotosAdicionales = new VotosAdicionalesDto // Assuming default constructor is fine { EnBlanco = estadoRecuento?.VotosEnBlanco ?? 0, Nulos = estadoRecuento?.VotosNulos ?? 0, Recurridos = estadoRecuento?.VotosRecurridos ?? 0 } }; return Ok(respuestaDto); } [HttpGet("composicion-congreso")] public async Task GetComposicionCongreso() { var config = await _dbContext.Configuraciones .AsNoTracking() .ToDictionaryAsync(c => c.Clave, c => c.Valor); // Aquí está el interruptor config.TryGetValue("UsarDatosDeBancadasOficiales", out var usarDatosOficialesValue); bool usarDatosOficiales = usarDatosOficialesValue == "true"; if (usarDatosOficiales) { // Si el interruptor está en 'true', llama a este método return await GetComposicionDesdeBancadasOficiales(config); } else { // Si está en 'false' o no existe, llama a este otro return await GetComposicionDesdeProyecciones(config); } } // En ResultadosController.cs private async Task GetComposicionDesdeBancadasOficiales(Dictionary config) { config.TryGetValue("MostrarOcupantes", out var mostrarOcupantesValue); bool mostrarOcupantes = mostrarOcupantesValue == "true"; IQueryable bancadasQuery = _dbContext.Bancadas.AsNoTracking() .Include(b => b.AgrupacionPolitica); if (mostrarOcupantes) { bancadasQuery = bancadasQuery.Include(b => b.Ocupante); } var bancadas = await bancadasQuery.ToListAsync(); var bancasPorAgrupacion = bancadas .Where(b => b.AgrupacionPolitica != null) // 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(); return new { Agrupacion = agrupacion, Camara = primeraBancaDelGrupo.Camara, BancasTotales = g.Count(), Ocupantes = ocupantesDelPartido }; }) .ToList(); 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); object MapearPartidos(Core.Enums.TipoCamara camara) { var partidosDeCamara = bancasPorAgrupacion.Where(b => b.Camara == camara); if (camara == Core.Enums.TipoCamara.Diputados) partidosDeCamara = partidosDeCamara.OrderBy(p => p.Agrupacion.OrdenDiputados ?? 999); else partidosDeCamara = partidosDeCamara.OrderBy(p => p.Agrupacion.OrdenSenadores ?? 999); 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(); } var diputados = new { CamaraNombre = "Cámara de Diputados", TotalBancas = 92, BancasEnJuego = 0, Partidos = MapearPartidos(Core.Enums.TipoCamara.Diputados), PresidenteBancada = presidenteDiputados != null ? new { presidenteDiputados.Color } : null }; var senadores = new { CamaraNombre = "Cámara de Senadores", TotalBancas = 46, BancasEnJuego = 0, Partidos = MapearPartidos(Core.Enums.TipoCamara.Senadores), PresidenteBancada = presidenteSenadores != null ? new { presidenteSenadores.Color } : null }; return Ok(new { Diputados = diputados, Senadores = senadores }); } private async Task GetComposicionDesdeProyecciones(Dictionary config) { 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(); 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) { 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 .Select(b => new { b.Agrupacion.Id, b.Agrupacion.Nombre, b.Agrupacion.NombreCorto, b.Agrupacion.Color, b.Bancas.BancasTotales, Ocupantes = new List() // <-- Siempre vacío en modo proyección }) .ToList(); } 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 GetBancadasConOcupantes() { var config = await _dbContext.Configuraciones.AsNoTracking().ToDictionaryAsync(c => c.Clave, c => c.Valor); config.TryGetValue("UsarDatosDeBancadasOficiales", out var usarDatosOficialesValue); if (usarDatosOficialesValue != "true") { // Si el modo oficial no está activo, SIEMPRE devolvemos un array vacío. return Ok(new List()); } // Si el modo oficial SÍ está activo, devolvemos los detalles. var bancadasConOcupantes = await _dbContext.Bancadas .AsNoTracking() .Include(b => b.Ocupante) .Select(b => new { b.Id, b.Camara, b.NumeroBanca, b.AgrupacionPoliticaId, Ocupante = b.Ocupante }) .OrderBy(b => b.Id) .ToListAsync(); return Ok(bancadasConOcupantes); } [HttpGet("configuracion-publica")] public async Task 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 { "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("seccion-resultados/{seccionId}")] public async Task GetResultadosAgregadosPorSeccion(string seccionId, [FromQuery] int categoriaId) { var municipiosDeLaSeccion = await _dbContext.AmbitosGeograficos .AsNoTracking() .Where(a => a.NivelId == 30 && a.SeccionProvincialId == seccionId) .Select(a => a.Id) .ToListAsync(); if (!municipiosDeLaSeccion.Any()) { return Ok(new List()); } var resultadosMunicipales = await _dbContext.ResultadosVotos .AsNoTracking() .Include(r => r.AgrupacionPolitica) // Usamos la categoriaId del parámetro .Where(r => r.CategoriaId == categoriaId && municipiosDeLaSeccion.Contains(r.AmbitoGeograficoId)) .ToListAsync(); var logos = await _dbContext.LogosAgrupacionesCategorias .AsNoTracking() // Usamos la categoriaId del parámetro .Where(l => l.CategoriaId == categoriaId) .ToDictionaryAsync(l => l.AgrupacionPoliticaId); var totalVotosSeccion = (decimal)resultadosMunicipales.Sum(r => r.CantidadVotos); var resultadosFinales = resultadosMunicipales .GroupBy(r => r.AgrupacionPoliticaId) .Select(g => new { Agrupacion = g.First().AgrupacionPolitica, Votos = g.Sum(r => r.CantidadVotos) }) .OrderByDescending(r => r.Votos) .Select(r => new { Id = r.Agrupacion.Id, r.Agrupacion.Nombre, r.Agrupacion.NombreCorto, r.Agrupacion.Color, LogoUrl = logos.GetValueOrDefault(r.Agrupacion.Id)?.LogoUrl, Votos = r.Votos, Porcentaje = totalVotosSeccion > 0 ? ((decimal)r.Votos * 100 / totalVotosSeccion) : 0 }) .ToList(); // Devolvemos un objeto para poder añadir la fecha de actualización var seccionAmbito = await _dbContext.AmbitosGeograficos.AsNoTracking() .FirstOrDefaultAsync(a => a.SeccionProvincialId == seccionId && a.NivelId == 20); var estadoRecuento = seccionAmbito != null ? await _dbContext.EstadosRecuentos.AsNoTracking() .FirstOrDefaultAsync(e => e.AmbitoGeograficoId == seccionAmbito.Id && e.CategoriaId == categoriaId) : null; return Ok(new { UltimaActualizacion = estadoRecuento?.FechaTotalizacion ?? DateTime.UtcNow, Resultados = resultadosFinales }); } [HttpGet("mapa-por-seccion")] public async Task GetResultadosMapaPorSeccion([FromQuery] int categoriaId) { // 1. Obtenemos todos los resultados a nivel de MUNICIPIO para la categoría dada. var resultadosMunicipales = await _dbContext.ResultadosVotos .AsNoTracking() .Include(r => r.AmbitoGeografico) .Include(r => r.AgrupacionPolitica) .Where(r => r.CategoriaId == categoriaId && r.AmbitoGeografico.NivelId == 30) .ToListAsync(); // 2. Agrupamos en memoria por Sección Electoral y sumamos los votos. var ganadoresPorSeccion = resultadosMunicipales .GroupBy(r => r.AmbitoGeografico.SeccionProvincialId) .Select(g => { // Para cada sección, encontramos al partido con más votos. var ganador = g .GroupBy(r => r.AgrupacionPolitica) .Select(pg => new { Agrupacion = pg.Key, TotalVotos = pg.Sum(r => r.CantidadVotos) }) .OrderByDescending(x => x.TotalVotos) .FirstOrDefault(); // Buscamos el nombre de la sección var seccionInfo = _dbContext.AmbitosGeograficos .FirstOrDefault(a => a.SeccionProvincialId == g.Key && a.NivelId == 20); return new { SeccionId = g.Key, SeccionNombre = seccionInfo?.Nombre, AgrupacionGanadoraId = ganador?.Agrupacion.Id, ColorGanador = ganador?.Agrupacion.Color }; }) .Where(r => r.SeccionId != null) // Filtramos cualquier posible nulo .ToList(); return Ok(ganadoresPorSeccion); } [HttpGet("seccion/{seccionId}")] public async Task GetResultadosDetallePorSeccion(string seccionId, [FromQuery] int categoriaId) { var municipiosDeLaSeccion = await _dbContext.AmbitosGeograficos .AsNoTracking() .Where(a => a.NivelId == 30 && a.SeccionProvincialId == seccionId) .Select(a => a.Id) .ToListAsync(); if (!municipiosDeLaSeccion.Any()) return Ok(new List()); var resultadosMunicipales = await _dbContext.ResultadosVotos .AsNoTracking() .Include(r => r.AgrupacionPolitica) .Where(r => r.CategoriaId == categoriaId && municipiosDeLaSeccion.Contains(r.AmbitoGeograficoId)) .ToListAsync(); var totalVotosSeccion = (decimal)resultadosMunicipales.Sum(r => r.CantidadVotos); var resultadosFinales = resultadosMunicipales // 1. Agrupamos por el ID del partido para evitar duplicados. .GroupBy(r => r.AgrupacionPoliticaId) .Select(g => new { // 2. Tomamos la entidad completa del primer elemento del grupo. Agrupacion = g.First().AgrupacionPolitica, Votos = g.Sum(r => r.CantidadVotos) }) .OrderByDescending(r => r.Votos) .Select(r => new { id = r.Agrupacion.Id, nombre = r.Agrupacion.NombreCorto ?? r.Agrupacion.Nombre, votos = r.Votos, porcentaje = totalVotosSeccion > 0 ? ((decimal)r.Votos * 100 / totalVotosSeccion) : 0, // 3. Añadimos el color a la respuesta. color = r.Agrupacion.Color }) .ToList(); return Ok(resultadosFinales); } [HttpGet("mapa-por-municipio")] public async Task GetResultadosMapaPorMunicipio([FromQuery] int categoriaId) { // Obtenemos los votos primero var votosPorMunicipio = await _dbContext.ResultadosVotos .AsNoTracking() .Where(r => r.CategoriaId == categoriaId && r.AmbitoGeografico.NivelId == 30) .ToListAsync(); // Luego, los agrupamos en memoria var ganadores = votosPorMunicipio .GroupBy(r => r.AmbitoGeograficoId) .Select(g => g.OrderByDescending(r => r.CantidadVotos).First()) .ToList(); // Ahora, obtenemos los detalles necesarios en una sola consulta adicional var idsAgrupacionesGanadoras = ganadores.Select(g => g.AgrupacionPoliticaId).ToList(); var idsAmbitosGanadores = ganadores.Select(g => g.AmbitoGeograficoId).ToList(); var agrupacionesInfo = await _dbContext.AgrupacionesPoliticas .AsNoTracking() .Where(a => idsAgrupacionesGanadoras.Contains(a.Id)) .ToDictionaryAsync(a => a.Id); var ambitosInfo = await _dbContext.AmbitosGeograficos .AsNoTracking() .Where(a => idsAmbitosGanadores.Contains(a.Id)) .ToDictionaryAsync(a => a.Id); // Finalmente, unimos todo en memoria var resultadoFinal = ganadores.Select(g => new { AmbitoId = g.AmbitoGeograficoId, DepartamentoNombre = ambitosInfo.GetValueOrDefault(g.AmbitoGeograficoId)?.Nombre, AgrupacionGanadoraId = g.AgrupacionPoliticaId, ColorGanador = agrupacionesInfo.GetValueOrDefault(g.AgrupacionPoliticaId)?.Color }) .Where(r => r.DepartamentoNombre != null) // Filtramos por si acaso .ToList(); return Ok(resultadoFinal); } }