// src/Elecciones.Api/Controllers/ResultadosController.cs using Elecciones.Core.DTOs; 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/elecciones/{eleccionId}")] 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; } private string? GetLogoUrl( string agrupacionId, int categoriaId, int? ambitoId, List todosLosLogos) { // Prioridad 1: Buscar un logo específico para este partido, categoría Y ámbito. var logoEspecifico = todosLosLogos.FirstOrDefault(l => l.AgrupacionPoliticaId == agrupacionId && l.CategoriaId == categoriaId && l.AmbitoGeograficoId == ambitoId); if (logoEspecifico != null) return logoEspecifico.LogoUrl; // Prioridad 2: Si no hay uno específico, buscar un logo general (sin ámbito) para este partido y categoría. var logoGeneral = todosLosLogos.FirstOrDefault(l => l.AgrupacionPoliticaId == agrupacionId && l.CategoriaId == categoriaId && l.AmbitoGeograficoId == null); return logoGeneral?.LogoUrl; } [HttpGet("partido/{municipioId}")] public async Task GetResultadosPorPartido([FromRoute] int eleccionId, 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.EleccionId == eleccionId && 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.EleccionId == eleccionId && rv.AmbitoGeograficoId == ambito.Id && rv.CategoriaId == categoriaId) .ToListAsync(); var candidatosRelevantes = await _dbContext.CandidatosOverrides.AsNoTracking() .Where(c => c.CategoriaId == categoriaId && agrupacionIds.Contains(c.AgrupacionPoliticaId) && (c.AmbitoGeograficoId == null || c.AmbitoGeograficoId == ambito.Id)) .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; var nombreCandidato = candidatosRelevantes.FirstOrDefault(c => c.AgrupacionPoliticaId == rv.AgrupacionPoliticaId && c.AmbitoGeograficoId == ambito.Id)?.NombreCandidato ?? candidatosRelevantes.FirstOrDefault(c => c.AgrupacionPoliticaId == rv.AgrupacionPoliticaId && c.AmbitoGeograficoId == null)?.NombreCandidato; return new AgrupacionResultadoDto { Id = rv.AgrupacionPolitica.Id, Nombre = rv.AgrupacionPolitica.Nombre, NombreCorto = rv.AgrupacionPolitica.NombreCorto, Color = rv.AgrupacionPolitica.Color, LogoUrl = logoUrl, Votos = rv.CantidadVotos, NombreCandidato = nombreCandidato, 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([FromRoute] int eleccionId, 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.EleccionId == eleccionId && e.AmbitoGeograficoId == provincia.Id) .ToDictionaryAsync(e => e.CategoriaId); var resultadosPorMunicipio = await _dbContext.ResultadosVotos .AsNoTracking() .Include(r => r.AgrupacionPolitica) .Where(r => r.EleccionId == eleccionId && r.AmbitoGeografico.NivelId == 30) // Nivel 30 = Municipio .ToListAsync(); // Obtenemos TODOS los logos relevantes en una sola consulta var todosLosLogos = 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 => { // --- USAMOS EL NUEVO MÉTODO HELPER --- // Para el resumen provincial, el ámbito es siempre el de la provincia. var logoUrl = GetLogoUrl(r.Agrupacion.Id, g.CategoriaId, provincia.Id, todosLosLogos); 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-por-seccion/{seccionId}/{camara}")] public async Task GetBancasPorSeccion([FromRoute] int eleccionId, string seccionId, string camara) { // Convertimos el string de la cámara a un enum o un valor numérico para la base de datos // 0 = Diputados, 1 = Senadores. Esto debe coincidir con cómo lo guardas en la DB. // O puedes usar un enum si lo tienes definido. int CategoriaId; if (camara.Equals("diputados", StringComparison.OrdinalIgnoreCase)) { CategoriaId = 6; // Asume que 5 es el CategoriaId para Diputados Provinciales } else if (camara.Equals("senadores", StringComparison.OrdinalIgnoreCase)) { CategoriaId = 5; // Asume que 6 es el CategoriaId para Senadores Provinciales } else { return BadRequest(new { message = "El tipo de cámara especificado no es válido. Use 'diputados' o 'senadores'." }); } var seccion = await _dbContext.AmbitosGeograficos .AsNoTracking() .FirstOrDefaultAsync(a => a.SeccionProvincialId == seccionId && a.NivelId == 20); 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}" }); } // --- CAMBIO 3: Filtrar también por el cargo (cámara) --- var proyecciones = await _dbContext.ProyeccionesBancas .AsNoTracking() .Include(p => p.AgrupacionPolitica) .Where(p => p.EleccionId == eleccionId && p.AmbitoGeograficoId == seccion.Id && p.CategoriaId == CategoriaId) .Select(p => new { AgrupacionId = p.AgrupacionPolitica.Id, // Añadir para el 'key' en React AgrupacionNombre = p.AgrupacionPolitica.Nombre, NombreCorto = p.AgrupacionPolitica.NombreCorto, // Añadir para el frontend Color = p.AgrupacionPolitica.Color, // Añadir para el frontend Bancas = p.NroBancas }) .OrderByDescending(p => p.Bancas) .ToListAsync(); if (!proyecciones.Any()) { _logger.LogWarning("No se encontraron proyecciones de bancas para la sección: {SeccionNombre} y cámara: {Camara}", seccion.Nombre, camara); return NotFound(new { message = $"No se han encontrado proyecciones de bancas para la sección {seccion.Nombre} ({camara})" }); } 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([FromRoute] int eleccionId) { 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, eleccionId); } else { // Si está en 'false' o no existe, llama a este otro return await GetComposicionDesdeProyecciones(config, eleccionId); } } // En ResultadosController.cs private async Task GetComposicionDesdeBancadasOficiales(Dictionary config, int eleccionId) { config.TryGetValue("MostrarOcupantes", out var mostrarOcupantesValue); bool mostrarOcupantes = mostrarOcupantesValue == "true"; IQueryable bancadasQuery = _dbContext.Bancadas.AsNoTracking() .Where(b => b.EleccionId == eleccionId) .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, int eleccionId) { // --- INICIO DE LA CORRECCIÓN --- // 1. Obtenemos el ID del ámbito provincial para usarlo en el filtro. var provincia = await _dbContext.AmbitosGeograficos.AsNoTracking() .FirstOrDefaultAsync(a => a.NivelId == 10); if (provincia == null) { // Si no se encuentra la provincia, no podemos continuar. // Devolvemos un objeto vacío para no romper el frontend. return Ok(new { Diputados = new { Partidos = new List() }, Senadores = new { Partidos = new List() } }); } // --- FIN DE LA CORRECCIÓN --- var bancasPorAgrupacion = await _dbContext.ProyeccionesBancas .AsNoTracking() .Where(p => p.EleccionId == eleccionId && p.AmbitoGeograficoId == provincia.Id) .GroupBy(p => new { p.AgrupacionPoliticaId, p.CategoriaId }) .Select(g => new { AgrupacionId = g.Key.AgrupacionPoliticaId, CategoriaId = g.Key.CategoriaId, // Ahora la suma es correcta porque solo considera los registros a nivel provincial 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([FromRoute] int eleccionId) { 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() .Where(b => b.EleccionId == eleccionId) .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 { UltimaActualizacion = DateTime.UtcNow, Resultados = new List() }); } // --- INICIO DE LA CORRECCIÓN DE LOGOS --- // 1. Buscamos logos que sean para esta categoría Y que sean generales (ámbito null). var logosGenerales = await _dbContext.LogosAgrupacionesCategorias .AsNoTracking() .Where(l => l.CategoriaId == categoriaId && l.AmbitoGeograficoId == null) .ToDictionaryAsync(l => l.AgrupacionPoliticaId); // --- FIN DE LA CORRECCIÓN DE LOGOS --- 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 .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, // 2. Usamos el diccionario de logos generales para buscar la URL. LogoUrl = logosGenerales.GetValueOrDefault(r.Agrupacion.Id)?.LogoUrl, Votos = r.Votos, Porcentaje = totalVotosSeccion > 0 ? ((decimal)r.Votos * 100 / totalVotosSeccion) : 0 }) .ToList(); 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 => { // --- INICIO DE LA CORRECCIÓN --- // Para cada sección, encontramos al partido con más votos. var ganador = g // CAMBIO CLAVE: Agrupamos por el ID de la agrupación, no por el objeto. .GroupBy(r => r.AgrupacionPolitica.Id) .Select(pg => new { // Obtenemos la entidad completa del primer elemento del grupo Agrupacion = pg.First().AgrupacionPolitica, TotalVotos = pg.Sum(r => r.CantidadVotos) }) .OrderByDescending(x => x.TotalVotos) .FirstOrDefault(); // --- FIN DE LA CORRECCIÓN --- // 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) .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); } [HttpGet("secciones-electorales-con-cargos")] public async Task GetSeccionesElectoralesConCargos() { var secciones = await _dbContext.AmbitosGeograficos .AsNoTracking() .Where(a => a.NivelId == 20) // Nivel 20 = Sección Electoral Provincial .Select(a => new { Id = a.SeccionProvincialId, // Usamos el ID que el frontend espera Nombre = a.Nombre, // Obtenemos los CategoriaId de las relaciones que tiene esta sección. // Esto asume que tienes una tabla que relaciona Ámbitos con Cargos. // Por ejemplo, a través de la tabla de Proyecciones o Resultados. Cargos = _dbContext.ProyeccionesBancas .Where(p => p.AmbitoGeograficoId == a.Id) .Select(p => p.CategoriaId) .Distinct() .ToList() }) .ToListAsync(); // Mapeamos los CategoriaId a los nombres que usa el frontend var resultado = secciones.Select(s => new { s.Id, s.Nombre, // Convertimos la lista de IDs de cargo a una lista de strings ("diputados", "senadores") CamarasDisponibles = s.Cargos.Select(CategoriaId => CategoriaId == 6 ? "diputados" : // Asume 5 = Diputados CategoriaId == 5 ? "senadores" : // Asume 6 = Senadores null ).Where(c => c != null).ToList() }); return Ok(resultado); } // En src/Elecciones.Api/Controllers/ResultadosController.cs [HttpGet("tabla-ranking-seccion/{seccionId}")] public async Task GetTablaRankingPorSeccion(string seccionId) { // 1. Obtener los ámbitos de los municipios de la sección var municipios = await _dbContext.AmbitosGeograficos.AsNoTracking() .Where(a => a.NivelId == 30 && a.SeccionProvincialId == seccionId) .OrderBy(a => a.Nombre).Select(a => new { a.Id, a.Nombre }).ToListAsync(); if (!municipios.Any()) { return Ok(new { Categorias = new List(), PartidosPrincipales = new Dictionary>(), ResultadosPorMunicipio = new List() }); } var municipiosIds = municipios.Select(m => m.Id).ToList(); // 2. Obtener todos los resultados de votos para esos municipios en una sola consulta var resultadosCrudos = await _dbContext.ResultadosVotos.AsNoTracking() .Include(r => r.AgrupacionPolitica) .Where(r => municipiosIds.Contains(r.AmbitoGeograficoId)) .ToListAsync(); var categoriasMap = await _dbContext.CategoriasElectorales.AsNoTracking().ToDictionaryAsync(c => c.Id); // 3. Determinar las categorías activas en la sección var categoriasActivas = resultadosCrudos.Select(r => r.CategoriaId).Distinct() .Select(id => categoriasMap.GetValueOrDefault(id)).Where(c => c != null) .OrderBy(c => c!.Orden).Select(c => new { c!.Id, c.Nombre }).ToList(); // 4. Determinar los 2 partidos principales POR CATEGORÍA a nivel SECCIÓN var partidosPorCategoria = categoriasActivas.ToDictionary( c => c.Id, c => { var resultadosCategoriaSeccion = resultadosCrudos.Where(r => r.CategoriaId == c.Id); var totalVotosSeccionCategoria = (decimal)resultadosCategoriaSeccion.Sum(r => r.CantidadVotos); return resultadosCategoriaSeccion // --- CAMBIO CLAVE: Agrupamos por el ID (string), no por el objeto --- .GroupBy(r => r.AgrupacionPolitica.Id) .Select(g => new { // g.Key ahora es el AgrupacionPoliticaId // Tomamos la entidad completa del primer elemento del grupo Agrupacion = g.First().AgrupacionPolitica, TotalVotos = g.Sum(r => r.CantidadVotos) }) .OrderByDescending(x => x.TotalVotos) .Take(2) .Select((x, index) => new { Puesto = index + 1, x.Agrupacion.Id, Nombre = x.Agrupacion.NombreCorto ?? x.Agrupacion.Nombre, PorcentajeTotalSeccion = totalVotosSeccionCategoria > 0 ? (x.TotalVotos / totalVotosSeccionCategoria) * 100 : 0 }) .ToList(); } ); // 5. Construir los datos para las filas de la tabla (resultados por municipio) var resultadosPorMunicipio = municipios.Select(municipio => { var resultadosDelMunicipio = resultadosCrudos.Where(r => r.AmbitoGeograficoId == municipio.Id); var celdas = resultadosDelMunicipio .GroupBy(r => r.CategoriaId) .ToDictionary( g => g.Key, // CategoriaId g => { var totalVotosMunicipioCategoria = (decimal)g.Sum(r => r.CantidadVotos); return g.ToDictionary( r => r.AgrupacionPoliticaId, // PartidoId r => totalVotosMunicipioCategoria > 0 ? (r.CantidadVotos / totalVotosMunicipioCategoria) * 100 : 0 ); } ); return new { MunicipioId = municipio.Id, MunicipioNombre = municipio.Nombre, Celdas = celdas }; }).ToList(); return Ok(new { Categorias = categoriasActivas, PartidosPorCategoria = partidosPorCategoria, ResultadosPorMunicipio = resultadosPorMunicipio }); } [HttpGet("ranking-municipios-por-seccion/{seccionId}")] public async Task GetRankingMunicipiosPorSeccion(string seccionId) { // 1. Obtener los municipios de la sección var municipios = await _dbContext.AmbitosGeograficos.AsNoTracking() .Where(a => a.NivelId == 30 && a.SeccionProvincialId == seccionId) .OrderBy(a => a.Nombre) .Select(a => new { a.Id, a.Nombre }) .ToListAsync(); if (!municipios.Any()) { return Ok(new List()); } var municipiosIds = municipios.Select(m => m.Id).ToList(); // 2. Obtener todos los resultados de esos municipios en una sola consulta var resultadosCrudos = await _dbContext.ResultadosVotos.AsNoTracking() .Include(r => r.AgrupacionPolitica) .Where(r => municipiosIds.Contains(r.AmbitoGeograficoId)) .ToListAsync(); // 3. Procesar los datos por cada municipio var resultadosPorMunicipio = municipios.Select(municipio => { var resultadosDelMunicipio = resultadosCrudos.Where(r => r.AmbitoGeograficoId == municipio.Id); var resultadosPorCategoria = resultadosDelMunicipio .GroupBy(r => r.CategoriaId) .Select(g => { var totalVotosCategoria = (decimal)g.Sum(r => r.CantidadVotos); var ranking = g .OrderByDescending(r => r.CantidadVotos) .Take(2) .Select(r => new { NombreCorto = r.AgrupacionPolitica.NombreCorto ?? r.AgrupacionPolitica.Nombre, Porcentaje = totalVotosCategoria > 0 ? (r.CantidadVotos / totalVotosCategoria) * 100 : 0, Votos = r.CantidadVotos // <-- AÑADIR ESTE CAMPO }) .ToList(); return new { CategoriaId = g.Key, Ranking = ranking }; }) .ToDictionary(r => r.CategoriaId); // Lo convertimos a diccionario para fácil acceso return new { MunicipioId = municipio.Id, MunicipioNombre = municipio.Nombre, ResultadosPorCategoria = resultadosPorCategoria }; }).ToList(); // Devolvemos las categorías que tuvieron resultados en esta sección para construir la cabecera var categoriasMap = await _dbContext.CategoriasElectorales.AsNoTracking().ToDictionaryAsync(c => c.Id); var categoriasActivas = resultadosCrudos .Select(r => r.CategoriaId).Distinct() .Select(id => categoriasMap.GetValueOrDefault(id)).Where(c => c != null) .OrderBy(c => c!.Orden) .Select(c => new { Id = c!.Id, Nombre = c.Nombre }) .ToList(); return Ok(new { Categorias = categoriasActivas, Resultados = resultadosPorMunicipio }); } [HttpGet("panel/{ambitoId?}")] public async Task GetPanelElectoral(int eleccionId, string? ambitoId, [FromQuery] int categoriaId) { if (string.IsNullOrEmpty(ambitoId)) { // CASO 1: No hay ID -> Vista Nacional return await GetPanelNacional(eleccionId, categoriaId); } // CASO 2: El ID es un número (y no un string corto como "02") -> Vista Municipal // La condición clave es que los IDs de distrito son cortos. Los IDs de BD son más largos. // O simplemente, un ID de distrito nunca será un ID de municipio. if (int.TryParse(ambitoId, out int idNumerico) && ambitoId.Length > 2) { return await GetPanelMunicipal(eleccionId, idNumerico, categoriaId); } else { // CASO 3: El ID es un string corto como "02" o "06" -> Vista Provincial return await GetPanelProvincial(eleccionId, ambitoId, categoriaId); } } private async Task GetPanelMunicipal(int eleccionId, int ambitoId, int categoriaId) { // 1. Validar y obtener la entidad del municipio var municipio = await _dbContext.AmbitosGeograficos.AsNoTracking() .FirstOrDefaultAsync(a => a.Id == ambitoId && a.NivelId == 30); if (municipio == null) return NotFound($"No se encontró el municipio con ID {ambitoId}."); // 2. Obtener los votos solo para ESE municipio var resultadosCrudos = await _dbContext.ResultadosVotos.AsNoTracking() .Include(r => r.AgrupacionPolitica) .Where(r => r.EleccionId == eleccionId && r.CategoriaId == categoriaId && r.AmbitoGeograficoId == ambitoId) .ToListAsync(); if (!resultadosCrudos.Any()) { // Devolver un DTO vacío pero válido si no hay resultados return Ok(new PanelElectoralDto { AmbitoNombre = municipio.Nombre, MapaData = new List(), // El mapa estará vacío en la vista de un solo municipio ResultadosPanel = new List(), EstadoRecuento = new EstadoRecuentoDto() }); } // 3. Calcular los resultados para el panel lateral (son los mismos datos crudos) var totalVotosMunicipio = (decimal)resultadosCrudos.Sum(r => r.CantidadVotos); var resultadosPanel = resultadosCrudos .Select(g => new AgrupacionResultadoDto { Id = g.AgrupacionPolitica.Id, Nombre = g.AgrupacionPolitica.Nombre, NombreCorto = g.AgrupacionPolitica.NombreCorto, Color = g.AgrupacionPolitica.Color, Votos = g.CantidadVotos, Porcentaje = totalVotosMunicipio > 0 ? (g.CantidadVotos / totalVotosMunicipio) * 100 : 0 }) .OrderByDescending(r => r.Votos) .ToList(); var estadoRecuento = await _dbContext.EstadosRecuentos .AsNoTracking() .FirstOrDefaultAsync(e => e.EleccionId == eleccionId && e.AmbitoGeograficoId == ambitoId && e.CategoriaId == categoriaId); var respuesta = new PanelElectoralDto { AmbitoNombre = municipio.Nombre, MapaData = new List(), // El mapa no muestra sub-geografías aquí ResultadosPanel = resultadosPanel, EstadoRecuento = new EstadoRecuentoDto { ParticipacionPorcentaje = estadoRecuento?.ParticipacionPorcentaje ?? 0, MesasTotalizadasPorcentaje = estadoRecuento?.MesasTotalizadasPorcentaje ?? 0 } }; return Ok(respuesta); } // Este método se ejecutará cuando la URL sea, por ejemplo, /api/elecciones/2/panel/02?categoriaId=2 private async Task GetPanelProvincial(int eleccionId, string distritoId, int categoriaId) { 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}."); // --- INICIO DE LA OPTIMIZACIÓN --- // 1. Agrupar y sumar directamente en la base de datos. EF lo traducirá a un SQL eficiente. var resultadosAgregados = await _dbContext.ResultadosVotos.AsNoTracking() .Where(r => r.EleccionId == eleccionId && r.CategoriaId == categoriaId && r.AmbitoGeografico.DistritoId == distritoId && r.AmbitoGeografico.NivelId == 30) .GroupBy(r => r.AgrupacionPolitica) // Agrupar por la entidad .Select(g => new { Agrupacion = g.Key, TotalVotos = g.Sum(r => r.CantidadVotos) }) .ToListAsync(); // 2. Calcular el total de votos en memoria (sobre una lista ya pequeña) var totalVotosProvincia = (decimal)resultadosAgregados.Sum(r => r.TotalVotos); // 3. Mapear a DTO (muy rápido) var resultadosPanel = resultadosAgregados .Select(g => new AgrupacionResultadoDto { Id = g.Agrupacion.Id, Nombre = g.Agrupacion.Nombre, NombreCorto = g.Agrupacion.NombreCorto, Color = g.Agrupacion.Color, Votos = g.TotalVotos, Porcentaje = totalVotosProvincia > 0 ? (g.TotalVotos / totalVotosProvincia) * 100 : 0 }) .OrderByDescending(r => r.Votos) .ToList(); // --- FIN DE LA OPTIMIZACIÓN --- var estadoRecuento = await _dbContext.EstadosRecuentosGenerales.AsNoTracking() .FirstOrDefaultAsync(e => e.EleccionId == eleccionId && e.AmbitoGeograficoId == provincia.Id && e.CategoriaId == categoriaId); var respuesta = new PanelElectoralDto { AmbitoNombre = provincia.Nombre, MapaData = new List(), // Se carga por separado ResultadosPanel = resultadosPanel, EstadoRecuento = new EstadoRecuentoDto { ParticipacionPorcentaje = estadoRecuento?.ParticipacionPorcentaje ?? 0, MesasTotalizadasPorcentaje = estadoRecuento?.MesasTotalizadasPorcentaje ?? 0 } }; return Ok(respuesta); } private async Task GetPanelNacional(int eleccionId, int categoriaId) { // --- INICIO DE LA OPTIMIZACIÓN --- // 1. Agrupar y sumar directamente en la base de datos a nivel nacional. var resultadosAgregados = await _dbContext.ResultadosVotos.AsNoTracking() .Where(r => r.EleccionId == eleccionId && r.CategoriaId == categoriaId) .GroupBy(r => r.AgrupacionPolitica) .Select(g => new { Agrupacion = g.Key, TotalVotos = g.Sum(r => r.CantidadVotos) }) .ToListAsync(); // 2. Calcular el total de votos en memoria var totalVotosNacional = (decimal)resultadosAgregados.Sum(r => r.TotalVotos); // 3. Mapear a DTO var resultadosPanel = resultadosAgregados .Select(g => new AgrupacionResultadoDto { Id = g.Agrupacion.Id, Nombre = g.Agrupacion.Nombre, NombreCorto = g.Agrupacion.NombreCorto, Color = g.Agrupacion.Color, Votos = g.TotalVotos, Porcentaje = totalVotosNacional > 0 ? (g.TotalVotos / totalVotosNacional) * 100 : 0 }) .OrderByDescending(r => r.Votos) .ToList(); // --- FIN DE LA OPTIMIZACIÓN --- var estadoRecuento = await _dbContext.EstadosRecuentosGenerales.AsNoTracking() .FirstOrDefaultAsync(e => e.EleccionId == eleccionId && e.CategoriaId == categoriaId); var respuesta = new PanelElectoralDto { AmbitoNombre = "Argentina", MapaData = new List(), // Se carga por separado ResultadosPanel = resultadosPanel, EstadoRecuento = new EstadoRecuentoDto { ParticipacionPorcentaje = estadoRecuento?.ParticipacionPorcentaje ?? 0, MesasTotalizadasPorcentaje = estadoRecuento?.MesasTotalizadasPorcentaje ?? 0 } }; return Ok(respuesta); } [HttpGet("mapa-resultados")] public async Task GetResultadosMapaPorMunicipio( [FromRoute] int eleccionId, [FromQuery] int categoriaId, [FromQuery] string? distritoId = null) { if (string.IsNullOrEmpty(distritoId)) { // --- VISTA NACIONAL (Ya corregida y funcionando) --- var votosAgregadosPorProvincia = await _dbContext.ResultadosVotos .AsNoTracking() .Where(r => r.EleccionId == eleccionId && r.CategoriaId == categoriaId && r.AmbitoGeografico.NivelId == 30 && r.AmbitoGeografico.DistritoId != null) .GroupBy(r => new { r.AmbitoGeografico.DistritoId, r.AgrupacionPoliticaId }) .Select(g => new { g.Key.DistritoId, g.Key.AgrupacionPoliticaId, TotalVotos = g.Sum(r => r.CantidadVotos) }) .ToListAsync(); var agrupacionesInfo = await _dbContext.AgrupacionesPoliticas.AsNoTracking().ToDictionaryAsync(a => a.Id); var provinciasInfo = await _dbContext.AmbitosGeograficos.AsNoTracking().Where(a => a.NivelId == 10).ToListAsync(); var ganadoresPorProvincia = votosAgregadosPorProvincia .GroupBy(r => r.DistritoId) .Select(g => g.OrderByDescending(x => x.TotalVotos).First()) .ToList(); var mapaDataNacional = ganadoresPorProvincia.Select(g => new ResultadoMapaDto { AmbitoId = g.DistritoId!, AmbitoNombre = provinciasInfo.FirstOrDefault(p => p.DistritoId == g.DistritoId)?.Nombre ?? "Desconocido", AgrupacionGanadoraId = g.AgrupacionPoliticaId, ColorGanador = agrupacionesInfo.GetValueOrDefault(g.AgrupacionPoliticaId)?.Color ?? "#808080" }).ToList(); return Ok(mapaDataNacional); } else { // --- VISTA PROVINCIAL (AHORA CORREGIDA CON LA MISMA LÓGICA) --- // PASO 1: Agrupar por IDs y sumar votos en la base de datos. var votosAgregadosPorMunicipio = await _dbContext.ResultadosVotos .AsNoTracking() .Where(r => r.EleccionId == eleccionId && r.CategoriaId == categoriaId && r.AmbitoGeografico.DistritoId == distritoId && r.AmbitoGeografico.NivelId == 30) // Agrupamos por los IDs (int y string) .GroupBy(r => new { r.AmbitoGeograficoId, r.AgrupacionPoliticaId }) .Select(g => new { g.Key.AmbitoGeograficoId, g.Key.AgrupacionPoliticaId, TotalVotos = g.Sum(r => r.CantidadVotos) }) .ToListAsync(); // PASO 2: Encontrar el ganador para cada municipio en memoria. var ganadoresPorMunicipio = votosAgregadosPorMunicipio .GroupBy(r => r.AmbitoGeograficoId) .Select(g => g.OrderByDescending(x => x.TotalVotos).First()) .ToList(); // PASO 3: Hidratar con los nombres y colores (muy rápido). var idsMunicipios = ganadoresPorMunicipio.Select(g => g.AmbitoGeograficoId).ToList(); var idsAgrupaciones = ganadoresPorMunicipio.Select(g => g.AgrupacionPoliticaId).ToList(); var municipiosInfo = await _dbContext.AmbitosGeograficos.AsNoTracking() .Where(a => idsMunicipios.Contains(a.Id)).ToDictionaryAsync(a => a.Id); var agrupacionesInfo = await _dbContext.AgrupacionesPoliticas.AsNoTracking() .Where(a => idsAgrupaciones.Contains(a.Id)).ToDictionaryAsync(a => a.Id); // Mapeo final a DTO. var mapaDataProvincial = ganadoresPorMunicipio.Select(g => new ResultadoMapaDto { AmbitoId = g.AmbitoGeograficoId.ToString(), AmbitoNombre = municipiosInfo.GetValueOrDefault(g.AmbitoGeograficoId)?.Nombre ?? "Desconocido", AgrupacionGanadoraId = g.AgrupacionPoliticaId, ColorGanador = agrupacionesInfo.GetValueOrDefault(g.AgrupacionPoliticaId)?.Color ?? "#808080" }).ToList(); return Ok(mapaDataProvincial); } } }