Preparación Legislativas Nacionales 2025
This commit is contained in:
		| @@ -1,4 +1,5 @@ | ||||
| // src/Elecciones.Api/Controllers/ResultadosController.cs | ||||
| using Elecciones.Core.DTOs; | ||||
| using Elecciones.Core.DTOs.ApiResponses; | ||||
| using Elecciones.Database; | ||||
| using Elecciones.Database.Entities; | ||||
| @@ -8,7 +9,7 @@ using Microsoft.EntityFrameworkCore; | ||||
| namespace Elecciones.Api.Controllers; | ||||
|  | ||||
| [ApiController] | ||||
| [Route("api/[controller]")] | ||||
| [Route("api/elecciones/{eleccionId}")] | ||||
| public class ResultadosController : ControllerBase | ||||
| { | ||||
|     private readonly EleccionesDbContext _dbContext; | ||||
| @@ -46,7 +47,7 @@ public class ResultadosController : ControllerBase | ||||
|     } | ||||
|  | ||||
|     [HttpGet("partido/{municipioId}")] | ||||
|     public async Task<IActionResult> GetResultadosPorPartido(string municipioId, [FromQuery] int categoriaId) | ||||
|     public async Task<IActionResult> GetResultadosPorPartido([FromRoute] int eleccionId, string municipioId, [FromQuery] int categoriaId) | ||||
|     { | ||||
|         var ambito = await _dbContext.AmbitosGeograficos.AsNoTracking() | ||||
|             .FirstOrDefaultAsync(a => a.SeccionId == municipioId && a.NivelId == 30); | ||||
| @@ -57,7 +58,7 @@ public class ResultadosController : ControllerBase | ||||
|         } | ||||
|  | ||||
|         var estadoRecuento = await _dbContext.EstadosRecuentos.AsNoTracking() | ||||
|         .FirstOrDefaultAsync(e => e.AmbitoGeograficoId == ambito.Id && e.CategoriaId == categoriaId); | ||||
|             .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) | ||||
| @@ -71,7 +72,7 @@ public class ResultadosController : ControllerBase | ||||
|  | ||||
|         var resultadosVotos = await _dbContext.ResultadosVotos.AsNoTracking() | ||||
|             .Include(rv => rv.AgrupacionPolitica) | ||||
|             .Where(rv => rv.AmbitoGeograficoId == ambito.Id && rv.CategoriaId == categoriaId) | ||||
|             .Where(rv => rv.EleccionId == eleccionId && rv.AmbitoGeograficoId == ambito.Id && rv.CategoriaId == categoriaId) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         var candidatosRelevantes = await _dbContext.CandidatosOverrides.AsNoTracking() | ||||
| @@ -119,7 +120,7 @@ public class ResultadosController : ControllerBase | ||||
|     } | ||||
|  | ||||
|     [HttpGet("provincia/{distritoId}")] | ||||
|     public async Task<IActionResult> GetResultadosProvinciales(string distritoId) | ||||
|     public async Task<IActionResult> GetResultadosProvinciales([FromRoute] int eleccionId, string distritoId) | ||||
|     { | ||||
|         var provincia = await _dbContext.AmbitosGeograficos.AsNoTracking() | ||||
|             .FirstOrDefaultAsync(a => a.DistritoId == distritoId && a.NivelId == 10); | ||||
| @@ -131,13 +132,13 @@ public class ResultadosController : ControllerBase | ||||
|  | ||||
|         var estadosPorCategoria = await _dbContext.EstadosRecuentosGenerales.AsNoTracking() | ||||
|             .Include(e => e.CategoriaElectoral) | ||||
|             .Where(e => e.AmbitoGeograficoId == provincia.Id) | ||||
|             .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.AmbitoGeografico.NivelId == 30) // Nivel 30 = Municipio | ||||
|             .Where(r => r.EleccionId == eleccionId && r.AmbitoGeografico.NivelId == 30) // Nivel 30 = Municipio | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         // Obtenemos TODOS los logos relevantes en una sola consulta | ||||
| @@ -194,8 +195,8 @@ public class ResultadosController : ControllerBase | ||||
|     } | ||||
|  | ||||
|  | ||||
|     [HttpGet("bancas-por-seccion/{seccionId}/{camara}")] // <-- CAMBIO 1: Modificar la ruta | ||||
|     public async Task<IActionResult> GetBancasPorSeccion(string seccionId, string camara) // <-- CAMBIO 2: Añadir el nuevo parámetro | ||||
|     [HttpGet("bancas-por-seccion/{seccionId}/{camara}")] | ||||
|     public async Task<IActionResult> 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. | ||||
| @@ -228,7 +229,7 @@ public class ResultadosController : ControllerBase | ||||
|         var proyecciones = await _dbContext.ProyeccionesBancas | ||||
|             .AsNoTracking() | ||||
|             .Include(p => p.AgrupacionPolitica) | ||||
|             .Where(p => p.AmbitoGeograficoId == seccion.Id && p.CategoriaId == CategoriaId) // <-- AÑADIDO EL FILTRO | ||||
|             .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 | ||||
| @@ -347,7 +348,7 @@ public class ResultadosController : ControllerBase | ||||
|     } | ||||
|  | ||||
|     [HttpGet("composicion-congreso")] | ||||
|     public async Task<IActionResult> GetComposicionCongreso() | ||||
|     public async Task<IActionResult> GetComposicionCongreso([FromRoute] int eleccionId) | ||||
|     { | ||||
|         var config = await _dbContext.Configuraciones | ||||
|             .AsNoTracking() | ||||
| @@ -360,23 +361,24 @@ public class ResultadosController : ControllerBase | ||||
|         if (usarDatosOficiales) | ||||
|         { | ||||
|             // Si el interruptor está en 'true', llama a este método | ||||
|             return await GetComposicionDesdeBancadasOficiales(config); | ||||
|             return await GetComposicionDesdeBancadasOficiales(config, eleccionId); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             // Si está en 'false' o no existe, llama a este otro | ||||
|             return await GetComposicionDesdeProyecciones(config); | ||||
|             return await GetComposicionDesdeProyecciones(config, eleccionId); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // En ResultadosController.cs | ||||
|     private async Task<IActionResult> GetComposicionDesdeBancadasOficiales(Dictionary<string, string> config) | ||||
|     private async Task<IActionResult> GetComposicionDesdeBancadasOficiales(Dictionary<string, string> config, int eleccionId) | ||||
|     { | ||||
|         config.TryGetValue("MostrarOcupantes", out var mostrarOcupantesValue); | ||||
|         bool mostrarOcupantes = mostrarOcupantesValue == "true"; | ||||
|  | ||||
|         IQueryable<Bancada> bancadasQuery = _dbContext.Bancadas.AsNoTracking() | ||||
|                                              .Include(b => b.AgrupacionPolitica); | ||||
|                                       .Where(b => b.EleccionId == eleccionId) | ||||
|                                       .Include(b => b.AgrupacionPolitica); | ||||
|         if (mostrarOcupantes) | ||||
|         { | ||||
|             bancadasQuery = bancadasQuery.Include(b => b.Ocupante); | ||||
| @@ -459,7 +461,7 @@ public class ResultadosController : ControllerBase | ||||
|         return Ok(new { Diputados = diputados, Senadores = senadores }); | ||||
|     } | ||||
|  | ||||
|     private async Task<IActionResult> GetComposicionDesdeProyecciones(Dictionary<string, string> config) | ||||
|     private async Task<IActionResult> GetComposicionDesdeProyecciones(Dictionary<string, string> config, int eleccionId) | ||||
|     { | ||||
|         // --- INICIO DE LA CORRECCIÓN --- | ||||
|         // 1. Obtenemos el ID del ámbito provincial para usarlo en el filtro. | ||||
| @@ -480,8 +482,7 @@ public class ResultadosController : ControllerBase | ||||
|  | ||||
|         var bancasPorAgrupacion = await _dbContext.ProyeccionesBancas | ||||
|             .AsNoTracking() | ||||
|             // --- CAMBIO CLAVE: Añadimos el filtro por AmbitoGeograficoId --- | ||||
|             .Where(p => p.AmbitoGeograficoId == provincia.Id) | ||||
|             .Where(p => p.EleccionId == eleccionId && p.AmbitoGeograficoId == provincia.Id) | ||||
|             .GroupBy(p => new { p.AgrupacionPoliticaId, p.CategoriaId }) | ||||
|             .Select(g => new | ||||
|             { | ||||
| @@ -551,7 +552,7 @@ public class ResultadosController : ControllerBase | ||||
|     } | ||||
|  | ||||
|     [HttpGet("bancadas-detalle")] | ||||
|     public async Task<IActionResult> GetBancadasConOcupantes() | ||||
|     public async Task<IActionResult> GetBancadasConOcupantes([FromRoute] int eleccionId) | ||||
|     { | ||||
|         var config = await _dbContext.Configuraciones.AsNoTracking().ToDictionaryAsync(c => c.Clave, c => c.Valor); | ||||
|  | ||||
| @@ -565,6 +566,7 @@ public class ResultadosController : ControllerBase | ||||
|         // 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 | ||||
|             { | ||||
| @@ -1012,4 +1014,342 @@ public class ResultadosController : ControllerBase | ||||
|             Resultados = resultadosPorMunicipio | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("panel/{ambitoId?}")] | ||||
|     public async Task<IActionResult> 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<IActionResult> 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<ResultadoMapaDto>(), // El mapa estará vacío en la vista de un solo municipio | ||||
|                 ResultadosPanel = new List<AgrupacionResultadoDto>(), | ||||
|                 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<ResultadoMapaDto>(), // 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<IActionResult> GetPanelProvincial(int eleccionId, string distritoId, int categoriaId) | ||||
|     { | ||||
|         // 1. Validar y obtener la entidad de la provincia | ||||
|         var provincia = await _dbContext.AmbitosGeograficos.AsNoTracking() | ||||
|         .FirstOrDefaultAsync(a => a.DistritoId == distritoId && a.NivelId == 10); | ||||
|  | ||||
|         if (provincia == null) | ||||
|         { | ||||
|             _logger.LogWarning("Panel Provincial: La provincia {DistritoId} no fue encontrada en AmbitosGeograficos.", distritoId); | ||||
|             return NotFound($"No se encontró la provincia con distritoId {distritoId}"); | ||||
|         } | ||||
|  | ||||
|         _logger.LogInformation("Panel Provincial: Provincia {Nombre} ({Id}) encontrada. Buscando municipios...", provincia.Nombre, provincia.Id); | ||||
|  | ||||
|         // 2. Obtener los votos de TODOS los municipios de ESA provincia. | ||||
|         var resultadosCrudos = await _dbContext.ResultadosVotos.AsNoTracking() | ||||
|             .Include(r => r.AgrupacionPolitica) | ||||
|             .Include(r => r.AmbitoGeografico) | ||||
|             .Where(r => r.EleccionId == eleccionId && | ||||
|                         r.CategoriaId == categoriaId && | ||||
|                         r.AmbitoGeografico.DistritoId == distritoId && | ||||
|                         r.AmbitoGeografico.NivelId == 30) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         if (!resultadosCrudos.Any()) | ||||
|         { | ||||
|             return Ok(new PanelElectoralDto | ||||
|             { | ||||
|                 AmbitoNombre = provincia.Nombre, | ||||
|                 MapaData = new List<ResultadoMapaDto>(), | ||||
|                 ResultadosPanel = new List<AgrupacionResultadoDto>(), | ||||
|                 EstadoRecuento = new EstadoRecuentoDto() | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         // 3. Calcular los resultados para el PANEL LATERAL (agregado provincial) | ||||
|         var totalVotosProvincia = (decimal)resultadosCrudos.Sum(r => r.CantidadVotos); | ||||
|         var resultadosPanel = resultadosCrudos | ||||
|             .GroupBy(r => r.AgrupacionPolitica.Id) | ||||
|             .Select(g => new AgrupacionResultadoDto | ||||
|             { | ||||
|                 Id = g.Key, | ||||
|                 Nombre = g.First().AgrupacionPolitica.Nombre, | ||||
|                 NombreCorto = g.First().AgrupacionPolitica.NombreCorto, | ||||
|                 Color = g.First().AgrupacionPolitica.Color, | ||||
|                 Votos = g.Sum(r => r.CantidadVotos), | ||||
|                 Porcentaje = totalVotosProvincia > 0 ? (g.Sum(r => r.CantidadVotos) / totalVotosProvincia) * 100 : 0 | ||||
|             }) | ||||
|             .OrderByDescending(r => r.Votos) | ||||
|             .ToList(); | ||||
|  | ||||
|         // --- CORRECCIÓN DEFINITIVA DE LA LÓGICA DEL MAPA --- | ||||
|         var mapaData = resultadosCrudos | ||||
|             .GroupBy(r => r.AmbitoGeografico) // 1. Agrupar por la entidad del municipio | ||||
|             .Select(g => | ||||
|             { | ||||
|                 // 2. DENTRO de cada grupo (municipio), encontrar la fila con más votos | ||||
|                 var ganador = g.OrderByDescending(r => r.CantidadVotos).First(); | ||||
|  | ||||
|                 // 3. Crear UN SOLO objeto ResultadoMapaDto para este municipio | ||||
|                 return new ResultadoMapaDto | ||||
|                 { | ||||
|                     AmbitoId = g.Key.SeccionId!, | ||||
|                     AmbitoNombre = g.Key.Nombre, | ||||
|                     AgrupacionGanadoraId = ganador.AgrupacionPolitica.Id, | ||||
|                     ColorGanador = ganador.AgrupacionPolitica.Color ?? "#808080" | ||||
|                 }; | ||||
|             }) | ||||
|             .ToList(); | ||||
|  | ||||
|         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 = mapaData, | ||||
|             ResultadosPanel = resultadosPanel, | ||||
|             EstadoRecuento = new EstadoRecuentoDto() // Simplificado para el ejemplo | ||||
|         }; | ||||
|  | ||||
|         return Ok(respuesta); | ||||
|     } | ||||
|  | ||||
|     private async Task<IActionResult> GetPanelNacional(int eleccionId, int categoriaId) | ||||
|     { | ||||
|         var resultadosCrudos = await _dbContext.ResultadosVotos.AsNoTracking() | ||||
|             .Include(r => r.AgrupacionPolitica) | ||||
|             .Include(r => r.AmbitoGeografico) | ||||
|             .Where(r => r.EleccionId == eleccionId && r.CategoriaId == categoriaId) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         if (!resultadosCrudos.Any()) | ||||
|         { | ||||
|             return Ok(new PanelElectoralDto | ||||
|             { | ||||
|                 AmbitoNombre = "Argentina", | ||||
|                 MapaData = new List<ResultadoMapaDto>(), | ||||
|                 ResultadosPanel = new List<AgrupacionResultadoDto>(), | ||||
|                 EstadoRecuento = new EstadoRecuentoDto() | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         // 2. Calcular los resultados para el panel | ||||
|         var resultadosPanel = resultadosCrudos | ||||
|         .GroupBy(r => r.AgrupacionPolitica.Id) | ||||
|         .Select(g => new AgrupacionResultadoDto | ||||
|         { | ||||
|             // g.Key ahora es el ID de la agrupación | ||||
|             Id = g.Key, | ||||
|             // Tomamos los datos de la agrupación del primer elemento del grupo | ||||
|             Nombre = g.First().AgrupacionPolitica.Nombre, | ||||
|             NombreCorto = g.First().AgrupacionPolitica.NombreCorto, | ||||
|             Color = g.First().AgrupacionPolitica.Color, | ||||
|             Votos = g.Sum(r => r.CantidadVotos), | ||||
|             Porcentaje = (decimal)g.Sum(r => r.CantidadVotos) * 100 / resultadosCrudos.Sum(r => r.CantidadVotos) | ||||
|         }) | ||||
|             .OrderByDescending(r => r.Votos) | ||||
|             .ToList(); | ||||
|  | ||||
|         // 3. Calcular los datos para el mapa (ganador por provincia/distrito) | ||||
|         var mapaData = resultadosCrudos | ||||
|         .Where(r => !string.IsNullOrEmpty(r.AmbitoGeografico.DistritoId)) | ||||
|         .GroupBy(r => r.AmbitoGeografico.DistritoId) | ||||
|         .Select(g => | ||||
|         { | ||||
|             var ganador = g.GroupBy(r => r.AgrupacionPoliticaId) | ||||
|                 .Select(pg => new { Votos = pg.Sum(r => r.CantidadVotos), Agrupacion = pg.First().AgrupacionPolitica }) | ||||
|                 .OrderByDescending(x => x.Votos) | ||||
|                 .First(); | ||||
|  | ||||
|             var provinciaAmbito = _dbContext.AmbitosGeograficos.AsNoTracking() | ||||
|                 .FirstOrDefault(a => a.DistritoId == g.Key && a.NivelId == 10); | ||||
|  | ||||
|             return new ResultadoMapaDto | ||||
|             { | ||||
|                 AmbitoId = g.Key!, | ||||
|                 AmbitoNombre = provinciaAmbito?.Nombre ?? "Desconocido", // <-- ENVIAMOS EL NOMBRE DE LA PROVINCIA | ||||
|                 AgrupacionGanadoraId = ganador.Agrupacion.Id, | ||||
|                 ColorGanador = ganador.Agrupacion.Color ?? "#808080" | ||||
|             }; | ||||
|         }) | ||||
|         .ToList(); | ||||
|  | ||||
|         // 4. Obtener el estado del recuento general | ||||
|         // Asumimos que existe un registro en EstadosRecuentosGenerales para el ámbito de la elección | ||||
|         var estadoRecuento = await _dbContext.EstadosRecuentosGenerales | ||||
|             .AsNoTracking() | ||||
|             .FirstOrDefaultAsync(e => e.EleccionId == eleccionId); | ||||
|  | ||||
|         var respuesta = new PanelElectoralDto | ||||
|         { | ||||
|             AmbitoNombre = "Argentina", | ||||
|             MapaData = mapaData, | ||||
|             ResultadosPanel = resultadosPanel, | ||||
|             EstadoRecuento = new EstadoRecuentoDto() | ||||
|         }; | ||||
|         return Ok(respuesta); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("mapa-resultados")] | ||||
|     public async Task<IActionResult> 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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -215,6 +215,116 @@ using (var scope = app.Services.CreateScope()) | ||||
|     Console.WriteLine("--> Seeded default configurations."); | ||||
| } | ||||
|  | ||||
| // --- SEEDER FINAL Y AUTOSUFICIENTE: Resultados Nacionales de Simulación para todo el país --- | ||||
| using (var scope = app.Services.CreateScope()) | ||||
| { | ||||
|     var services = scope.ServiceProvider; | ||||
|     var context = services.GetRequiredService<EleccionesDbContext>(); | ||||
|     var logger = services.GetRequiredService<ILogger<Program>>(); | ||||
|  | ||||
|     const int eleccionNacionalId = 2; | ||||
|  | ||||
|     if (!context.ResultadosVotos.Any(r => r.EleccionId == eleccionNacionalId)) | ||||
|     { | ||||
|         logger.LogInformation("--> No se encontraron datos para la elección nacional ID {EleccionId}. Generando datos de simulación para TODO EL PAÍS...", eleccionNacionalId); | ||||
|  | ||||
|         var eleccionNacional = await context.Elecciones.FindAsync(eleccionNacionalId) ?? new Eleccion { Id = eleccionNacionalId, Nombre = "Elecciones Nacionales 2025", Nivel = "Nacional", DistritoId = "00", Fecha = new DateOnly(2025, 10, 26) }; | ||||
|         if (!context.Elecciones.Local.Any(e => e.Id == eleccionNacionalId)) context.Elecciones.Add(eleccionNacional); | ||||
|          | ||||
|         var categoriaDiputadosNac = await context.CategoriasElectorales.FindAsync(2) ?? new CategoriaElectoral { Id = 2, Nombre = "DIPUTADOS NACIONALES", Orden = 3 }; | ||||
|         if (!context.CategoriasElectorales.Local.Any(c => c.Id == 2)) context.CategoriasElectorales.Add(categoriaDiputadosNac); | ||||
|         await context.SaveChangesAsync(); | ||||
|  | ||||
|         var provinciasMaestras = new Dictionary<string, string> | ||||
|         { | ||||
|             { "01", "CABA" }, { "02", "BUENOS AIRES" }, { "03", "CATAMARCA" }, { "04", "CORDOBA" }, | ||||
|             { "05", "CORRIENTES" },{ "06", "CHACO" }, { "07", "CHUBUT" }, { "08", "ENTRE RIOS" }, | ||||
|             { "09", "FORMOSA" }, { "10", "JUJUY" }, { "11", "LA PAMPA" }, { "12", "LA RIOJA" }, | ||||
|             { "13", "MENDOZA" }, { "14", "MISIONES" }, { "15", "NEUQUEN" }, { "16", "RIO NEGRO" }, | ||||
|             { "17", "SALTA" }, { "18", "SAN JUAN" }, { "19", "SAN LUIS" }, { "20", "SANTA CRUZ" }, | ||||
|             { "21", "SANTA FE" }, { "22", "SANTIAGO DEL ESTERO" }, { "23", "TIERRA DEL FUEGO" }, { "24", "TUCUMAN" } | ||||
|         }; | ||||
|  | ||||
|         foreach (var p in provinciasMaestras) | ||||
|         { | ||||
|             if (!await context.AmbitosGeograficos.AnyAsync(a => a.NivelId == 10 && a.DistritoId == p.Key)) | ||||
|             { | ||||
|                 context.AmbitosGeograficos.Add(new AmbitoGeografico { Nombre = p.Value, NivelId = 10, DistritoId = p.Key }); | ||||
|             } | ||||
|         } | ||||
|         await context.SaveChangesAsync(); | ||||
|         logger.LogInformation("--> Verificados/creados los 24 ámbitos geográficos de Nivel 10."); | ||||
|  | ||||
|         // --- INICIO DE LA LÓGICA DE CREACIÓN DE MUNICIPIOS DE EJEMPLO --- | ||||
|         logger.LogInformation("--> Verificando existencia de municipios (Nivel 30) para cada provincia..."); | ||||
|         var provinciasEnDb = await context.AmbitosGeograficos.AsNoTracking().Where(a => a.NivelId == 10).ToListAsync(); | ||||
|         foreach (var provincia in provinciasEnDb) | ||||
|         { | ||||
|             bool existenMunicipios = await context.AmbitosGeograficos.AnyAsync(a => a.NivelId == 30 && a.DistritoId == provincia.DistritoId); | ||||
|             if (!existenMunicipios) | ||||
|             { | ||||
|                 logger.LogWarning("--> No se encontraron municipios para {Provincia}. Creando 5 municipios de ejemplo...", provincia.Nombre); | ||||
|                 for (int i = 1; i <= 5; i++) | ||||
|                 { | ||||
|                     context.AmbitosGeograficos.Add(new AmbitoGeografico | ||||
|                     { | ||||
|                         Nombre = $"{provincia.Nombre} - Municipio de Ejemplo {i}", | ||||
|                         NivelId = 30, | ||||
|                         DistritoId = provincia.DistritoId | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         await context.SaveChangesAsync(); | ||||
|         // --- FIN DE LA LÓGICA DE CREACIÓN DE MUNICIPIOS DE EJEMPLO --- | ||||
|  | ||||
|         var todosLosPartidos = await context.AgrupacionesPoliticas.Take(5).ToListAsync(); | ||||
|         if (!todosLosPartidos.Any()) { | ||||
|             logger.LogWarning("--> No hay agrupaciones políticas en la BD. No se pueden generar votos de simulación."); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         var nuevosResultados = new List<ResultadoVoto>(); | ||||
|         var rand = new Random(); | ||||
|          | ||||
|         foreach (var provincia in provinciasEnDb) | ||||
|         { | ||||
|             var municipiosDeProvincia = await context.AmbitosGeograficos.AsNoTracking() | ||||
|                 .Where(a => a.NivelId == 30 && a.DistritoId == provincia.DistritoId).ToListAsync(); | ||||
|  | ||||
|             if (!municipiosDeProvincia.Any()) continue; | ||||
|  | ||||
|             logger.LogInformation("--> Generando votos para {Count} municipios en {Provincia}...", municipiosDeProvincia.Count, provincia.Nombre); | ||||
|  | ||||
|             int partidoIndex = rand.Next(todosLosPartidos.Count); | ||||
|             foreach (var municipio in municipiosDeProvincia) | ||||
|             { | ||||
|                 var partidoGanador = todosLosPartidos[partidoIndex % todosLosPartidos.Count]; | ||||
|                 partidoIndex++; | ||||
|                 nuevosResultados.Add(new ResultadoVoto { | ||||
|                     EleccionId = eleccionNacional.Id, AmbitoGeograficoId = municipio.Id, CategoriaId = categoriaDiputadosNac.Id, | ||||
|                     AgrupacionPoliticaId = partidoGanador.Id, CantidadVotos = rand.Next(25000, 70000) | ||||
|                 }); | ||||
|                 var otrosPartidos = todosLosPartidos.Where(p => p.Id != partidoGanador.Id).OrderBy(p => rand.Next()).Take(rand.Next(3, 6)); | ||||
|                 foreach (var competidor in otrosPartidos) { | ||||
|                     nuevosResultados.Add(new ResultadoVoto { | ||||
|                         EleccionId = eleccionNacional.Id, AmbitoGeograficoId = municipio.Id, CategoriaId = categoriaDiputadosNac.Id, | ||||
|                         AgrupacionPoliticaId = competidor.Id, CantidadVotos = rand.Next(1000, 24000) | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         if (nuevosResultados.Any()) { | ||||
|             await context.ResultadosVotos.AddRangeAsync(nuevosResultados); | ||||
|             await context.SaveChangesAsync(); | ||||
|             logger.LogInformation("--> Se generaron {Count} registros de votos de simulación para todo el país.", nuevosResultados.Count); | ||||
|         } else { | ||||
|             logger.LogWarning("--> No se encontraron municipios en la BD para generar votos de simulación."); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Configurar el pipeline de peticiones HTTP. | ||||
| // Añadimos el logging de peticiones de Serilog aquí. | ||||
| app.UseSerilogRequestLogging(); | ||||
|   | ||||
| @@ -14,7 +14,7 @@ using System.Reflection; | ||||
| [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Api")] | ||||
| [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] | ||||
| [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+843c0f725893a48eae1236473cb5eeb00ef3d91c")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+64dc7ef440f585cb5e7723585b6327d5387f1b32")] | ||||
| [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Api")] | ||||
| [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Api")] | ||||
| [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| {"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["TyIJk/eQMWjmB5LsDE\u002BZIJC9P9ciVxd7bnzRiTZsGt4=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/GfbpJthEWmsuz0uFx1QLHM7gyM1wLLeQgAIl4SzUD4=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","AvSkxVPIg0GjnB1RJ4hDNyo9p9GONrzDs8uVuixH\u002BOE=","IgT9pOgRnK37qfILj2QcjFoBZ180HMt\u002BScgje2iYOo4=","ezUlOMzNZmyKDIe1wwXqvX\u002BvhwfB992xNVts7r2zcTc=","y2BV4WpkQuLfqQhfOQBtmuzh940c3s4LAopGKfztfTE=","lHTUEsMkDu8nqXtfTwl7FRfgocyyc7RI5O/edTHN1\u002B0=","A7nz7qgOtQ1CwZZLvNnr0b5QZB3fTi3y4i6y7rBIcxQ=","znnuRi2tsk7AACuYo4WSgj7NcLriG4PKVaF4L35SvDk=","GelE32odx/vTului267wqi6zL3abBnF9yvwC2Q66LoM=","TEsXImnzxFKTIq2f5fiDu7i6Ar/cbecW5MZ3z8Wb/a4=","5WogJu\u002BUPlF\u002BE5mq/ILtDXpVwqwmhHtsEB13nmT5JJk=","dcHQRkttjMjo2dvhL7hA9t4Pg\u002B7OnjZpkFmakT4QR9U=","Of8nTYw5l\u002BgiAJo7z6XYIntG2tUtCFcILzHbTiiXn\u002Bw=","PDy\u002BTiayvNAoXXBEgwC/kCojpgOOMI6RQOIoSXs3LJc=","ePXrkee3hv3wHUr8S7aYmRVvXUTxQf76zApKGv3/l3o=","DXx5dQywLo3UsY2zQaUG\u002BbW4ObiYbybxPBWxeJD2bhk=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","/f\u002B\u002BdIRysg7dipW05N4RtxXuPBXZZIhhi3aMiCZ\u002BB2w="],"CachedAssets":{},"CachedCopyCandidates":{}} | ||||
| {"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["TyIJk/eQMWjmB5LsDE\u002BZIJC9P9ciVxd7bnzRiTZsGt4=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/GfbpJthEWmsuz0uFx1QLHM7gyM1wLLeQgAIl4SzUD4=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","AvSkxVPIg0GjnB1RJ4hDNyo9p9GONrzDs8uVuixH\u002BOE=","IgT9pOgRnK37qfILj2QcjFoBZ180HMt\u002BScgje2iYOo4=","ezUlOMzNZmyKDIe1wwXqvX\u002BvhwfB992xNVts7r2zcTc=","y2BV4WpkQuLfqQhfOQBtmuzh940c3s4LAopGKfztfTE=","lHTUEsMkDu8nqXtfTwl7FRfgocyyc7RI5O/edTHN1\u002B0=","A7nz7qgOtQ1CwZZLvNnr0b5QZB3fTi3y4i6y7rBIcxQ=","znnuRi2tsk7AACuYo4WSgj7NcLriG4PKVaF4L35SvDk=","GelE32odx/vTului267wqi6zL3abBnF9yvwC2Q66LoM=","TEsXImnzxFKTIq2f5fiDu7i6Ar/cbecW5MZ3z8Wb/a4=","5WogJu\u002BUPlF\u002BE5mq/ILtDXpVwqwmhHtsEB13nmT5JJk=","dcHQRkttjMjo2dvhL7hA9t4Pg\u002B7OnjZpkFmakT4QR9U=","Of8nTYw5l\u002BgiAJo7z6XYIntG2tUtCFcILzHbTiiXn\u002Bw=","PDy\u002BTiayvNAoXXBEgwC/kCojpgOOMI6RQOIoSXs3LJc=","ePXrkee3hv3wHUr8S7aYmRVvXUTxQf76zApKGv3/l3o=","DXx5dQywLo3UsY2zQaUG\u002BbW4ObiYbybxPBWxeJD2bhk=","muVh5sjH3sgdvuz4TbuTwTggX1uDnsWXgoosMKST/r4=","nrP5gSIA5vzgp8v12CAOr943QYLxU4Til6oiCcWSNI8=","yMd45U9BK07I3b3fBQ627PWTYyZ2ZjrmFc5VD\u002BQVx1Q=","xKskvcoJU0RVRN1a5dRqKRM7IP5vmmbraUaPFYjhnCc=","p7BjQw7aSZjfOCqmKm7/kPO9qegEQZBfirMjlOx/I1I=","xf7fvi/A0tUBZMxhPJDER8vuJQH2E3gyAnDWXXhOyD0=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","sfQbd729CZjIZCr20t2H\u002BLd7hI/GEf6\u002BdyvRnmo8MpU="],"CachedAssets":{},"CachedCopyCandidates":{}} | ||||
| @@ -1 +1 @@ | ||||
| {"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["TyIJk/eQMWjmB5LsDE\u002BZIJC9P9ciVxd7bnzRiTZsGt4=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/GfbpJthEWmsuz0uFx1QLHM7gyM1wLLeQgAIl4SzUD4=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","AvSkxVPIg0GjnB1RJ4hDNyo9p9GONrzDs8uVuixH\u002BOE=","IgT9pOgRnK37qfILj2QcjFoBZ180HMt\u002BScgje2iYOo4=","ezUlOMzNZmyKDIe1wwXqvX\u002BvhwfB992xNVts7r2zcTc=","y2BV4WpkQuLfqQhfOQBtmuzh940c3s4LAopGKfztfTE=","lHTUEsMkDu8nqXtfTwl7FRfgocyyc7RI5O/edTHN1\u002B0=","A7nz7qgOtQ1CwZZLvNnr0b5QZB3fTi3y4i6y7rBIcxQ=","znnuRi2tsk7AACuYo4WSgj7NcLriG4PKVaF4L35SvDk=","GelE32odx/vTului267wqi6zL3abBnF9yvwC2Q66LoM=","TEsXImnzxFKTIq2f5fiDu7i6Ar/cbecW5MZ3z8Wb/a4=","5WogJu\u002BUPlF\u002BE5mq/ILtDXpVwqwmhHtsEB13nmT5JJk=","dcHQRkttjMjo2dvhL7hA9t4Pg\u002B7OnjZpkFmakT4QR9U=","Of8nTYw5l\u002BgiAJo7z6XYIntG2tUtCFcILzHbTiiXn\u002Bw=","PDy\u002BTiayvNAoXXBEgwC/kCojpgOOMI6RQOIoSXs3LJc=","ePXrkee3hv3wHUr8S7aYmRVvXUTxQf76zApKGv3/l3o=","DXx5dQywLo3UsY2zQaUG\u002BbW4ObiYbybxPBWxeJD2bhk=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","/f\u002B\u002BdIRysg7dipW05N4RtxXuPBXZZIhhi3aMiCZ\u002BB2w="],"CachedAssets":{},"CachedCopyCandidates":{}} | ||||
| {"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["TyIJk/eQMWjmB5LsDE\u002BZIJC9P9ciVxd7bnzRiTZsGt4=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/GfbpJthEWmsuz0uFx1QLHM7gyM1wLLeQgAIl4SzUD4=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","AvSkxVPIg0GjnB1RJ4hDNyo9p9GONrzDs8uVuixH\u002BOE=","IgT9pOgRnK37qfILj2QcjFoBZ180HMt\u002BScgje2iYOo4=","ezUlOMzNZmyKDIe1wwXqvX\u002BvhwfB992xNVts7r2zcTc=","y2BV4WpkQuLfqQhfOQBtmuzh940c3s4LAopGKfztfTE=","lHTUEsMkDu8nqXtfTwl7FRfgocyyc7RI5O/edTHN1\u002B0=","A7nz7qgOtQ1CwZZLvNnr0b5QZB3fTi3y4i6y7rBIcxQ=","znnuRi2tsk7AACuYo4WSgj7NcLriG4PKVaF4L35SvDk=","GelE32odx/vTului267wqi6zL3abBnF9yvwC2Q66LoM=","TEsXImnzxFKTIq2f5fiDu7i6Ar/cbecW5MZ3z8Wb/a4=","5WogJu\u002BUPlF\u002BE5mq/ILtDXpVwqwmhHtsEB13nmT5JJk=","dcHQRkttjMjo2dvhL7hA9t4Pg\u002B7OnjZpkFmakT4QR9U=","Of8nTYw5l\u002BgiAJo7z6XYIntG2tUtCFcILzHbTiiXn\u002Bw=","PDy\u002BTiayvNAoXXBEgwC/kCojpgOOMI6RQOIoSXs3LJc=","ePXrkee3hv3wHUr8S7aYmRVvXUTxQf76zApKGv3/l3o=","DXx5dQywLo3UsY2zQaUG\u002BbW4ObiYbybxPBWxeJD2bhk=","muVh5sjH3sgdvuz4TbuTwTggX1uDnsWXgoosMKST/r4=","nrP5gSIA5vzgp8v12CAOr943QYLxU4Til6oiCcWSNI8=","yMd45U9BK07I3b3fBQ627PWTYyZ2ZjrmFc5VD\u002BQVx1Q=","xKskvcoJU0RVRN1a5dRqKRM7IP5vmmbraUaPFYjhnCc=","p7BjQw7aSZjfOCqmKm7/kPO9qegEQZBfirMjlOx/I1I=","xf7fvi/A0tUBZMxhPJDER8vuJQH2E3gyAnDWXXhOyD0=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","sfQbd729CZjIZCr20t2H\u002BLd7hI/GEf6\u002BdyvRnmo8MpU="],"CachedAssets":{},"CachedCopyCandidates":{}} | ||||
		Reference in New Issue
	
	Block a user