Feat Widgets
Se añade la tabla CandidatosOverrides Se añade el Overrides de Candidatos al panel de administrador Se Añade el nombre de los candidatos a los Widgets de categorias por municipio
This commit is contained in:
		| @@ -235,4 +235,77 @@ public class AdminController : ControllerBase | ||||
|  | ||||
|     return Ok(municipios); | ||||
|   } | ||||
|  | ||||
|   /// <summary> | ||||
|   /// Obtiene todos los overrides de candidatos configurados. | ||||
|   /// </summary> | ||||
|   [HttpGet("candidatos")] | ||||
|   public async Task<IActionResult> GetCandidatos() | ||||
|   { | ||||
|     var candidatos = await _dbContext.CandidatosOverrides | ||||
|         .AsNoTracking() | ||||
|         .ToListAsync(); | ||||
|     return Ok(candidatos); | ||||
|   } | ||||
|  | ||||
|   /// <summary> | ||||
|   /// Guarda (actualiza o crea) una lista de overrides de candidatos. | ||||
|   /// </summary> | ||||
|   [HttpPut("candidatos")] | ||||
|   public async Task<IActionResult> UpdateCandidatos([FromBody] List<CandidatoOverride> candidatos) | ||||
|   { | ||||
|     foreach (var candidatoDto in candidatos) | ||||
|     { | ||||
|       // Buscamos un override existente basado en la combinación única | ||||
|       var candidatoExistente = await _dbContext.CandidatosOverrides | ||||
|           .FirstOrDefaultAsync(c => | ||||
|               c.AgrupacionPoliticaId == candidatoDto.AgrupacionPoliticaId && | ||||
|               c.CategoriaId == candidatoDto.CategoriaId && | ||||
|               c.AmbitoGeograficoId == candidatoDto.AmbitoGeograficoId); | ||||
|  | ||||
|       if (candidatoExistente != null) | ||||
|       { | ||||
|         // Si existe y el nombre es diferente, lo actualizamos. | ||||
|         if (candidatoExistente.NombreCandidato != candidatoDto.NombreCandidato) | ||||
|         { | ||||
|           candidatoExistente.NombreCandidato = candidatoDto.NombreCandidato; | ||||
|         } | ||||
|       } | ||||
|       else | ||||
|       { | ||||
|         // Si no se encontró un registro exacto, lo añadimos a la base de datos, | ||||
|         // pero solo si el nombre del candidato no está vacío. | ||||
|         if (!string.IsNullOrWhiteSpace(candidatoDto.NombreCandidato)) | ||||
|         { | ||||
|           _dbContext.CandidatosOverrides.Add(new CandidatoOverride | ||||
|           { | ||||
|             AgrupacionPoliticaId = candidatoDto.AgrupacionPoliticaId, | ||||
|             CategoriaId = candidatoDto.CategoriaId, | ||||
|             AmbitoGeograficoId = candidatoDto.AmbitoGeograficoId, | ||||
|             NombreCandidato = candidatoDto.NombreCandidato | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // También necesitamos manejar los casos donde se borra un nombre (se envía un string vacío) | ||||
|     var overridesAEliminar = await _dbContext.CandidatosOverrides | ||||
|         .Where(c => candidatos.Any(dto => | ||||
|             dto.AgrupacionPoliticaId == c.AgrupacionPoliticaId && | ||||
|             dto.CategoriaId == c.CategoriaId && | ||||
|             dto.AmbitoGeograficoId == c.AmbitoGeograficoId && | ||||
|             string.IsNullOrWhiteSpace(dto.NombreCandidato) | ||||
|         )) | ||||
|         .ToListAsync(); | ||||
|  | ||||
|     if (overridesAEliminar.Any()) | ||||
|     { | ||||
|       _dbContext.CandidatosOverrides.RemoveRange(overridesAEliminar); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     await _dbContext.SaveChangesAsync(); | ||||
|     _logger.LogInformation("Se procesaron {Count} overrides de candidatos.", candidatos.Count); | ||||
|     return NoContent(); // Respuesta estándar para un PUT exitoso | ||||
|   } | ||||
| } | ||||
| @@ -22,7 +22,7 @@ public class ResultadosController : ControllerBase | ||||
|         _configuration = configuration; | ||||
|     } | ||||
|  | ||||
|     [HttpGet("partido/{municipioId}")] // Renombramos el parámetro para mayor claridad | ||||
|     [HttpGet("partido/{municipioId}")] | ||||
|     public async Task<IActionResult> GetResultadosPorPartido(string municipioId, [FromQuery] int categoriaId) | ||||
|     { | ||||
|         var ambito = await _dbContext.AmbitosGeograficos.AsNoTracking() | ||||
| @@ -51,6 +51,11 @@ public class ResultadosController : ControllerBase | ||||
|             .Where(rv => 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 | ||||
| @@ -64,6 +69,9 @@ public class ResultadosController : ControllerBase | ||||
|                 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, | ||||
| @@ -72,6 +80,7 @@ public class ResultadosController : ControllerBase | ||||
|                     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(), | ||||
| @@ -557,27 +566,18 @@ public class ResultadosController : ControllerBase | ||||
|  | ||||
|         if (!municipiosDeLaSeccion.Any()) | ||||
|         { | ||||
|             return Ok(new List<object>()); | ||||
|             return Ok(new { UltimaActualizacion = DateTime.UtcNow, Resultados = new List<object>() }); | ||||
|         } | ||||
|  | ||||
|         // --- INICIO DE LA CORRECCIÓN --- | ||||
|         // --- INICIO DE LA CORRECCIÓN DE LOGOS --- | ||||
|  | ||||
|         // 1. Obtenemos TODOS los logos para la categoría, sin convertirlos a diccionario todavía. | ||||
|         var todosLosLogos = await _dbContext.LogosAgrupacionesCategorias | ||||
|         // 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) | ||||
|             .ToListAsync(); | ||||
|             .Where(l => l.CategoriaId == categoriaId && l.AmbitoGeograficoId == null) | ||||
|             .ToDictionaryAsync(l => l.AgrupacionPoliticaId); | ||||
|  | ||||
|         // 2. Procesamos los logos en memoria para manejar duplicados, priorizando los que tienen ámbito. | ||||
|         //    El resultado es un diccionario limpio sin claves duplicadas. | ||||
|         var logos = todosLosLogos | ||||
|             .GroupBy(l => l.AgrupacionPoliticaId) // Agrupamos por la clave que causa el problema | ||||
|             .ToDictionary( | ||||
|                 g => g.Key, // La clave del diccionario es AgrupacionPoliticaId | ||||
|                 g => g.OrderByDescending(l => l.AmbitoGeograficoId).First() // Para cada grupo, tomamos el logo más específico (el que tiene un AmbitoId) o el general si es el único. | ||||
|             ); | ||||
|  | ||||
|         // --- FIN DE LA CORRECCIÓN --- | ||||
|         // --- FIN DE LA CORRECCIÓN DE LOGOS --- | ||||
|  | ||||
|         var resultadosMunicipales = await _dbContext.ResultadosVotos | ||||
|             .AsNoTracking() | ||||
| @@ -601,7 +601,8 @@ public class ResultadosController : ControllerBase | ||||
|                 r.Agrupacion.Nombre, | ||||
|                 r.Agrupacion.NombreCorto, | ||||
|                 r.Agrupacion.Color, | ||||
|                 LogoUrl = logos.GetValueOrDefault(r.Agrupacion.Id)?.LogoUrl, | ||||
|                 // 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 | ||||
|             }) | ||||
|   | ||||
| @@ -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+0ce5e2e2c9bd6b64a7ea8629eeef35d204013a33")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+479c2c60f214472aeffd4404b482ffb940c3049e")] | ||||
| [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=","hGxHNQ\u002B1azEK\u002Bi1edq5HWPFILZgOw/WtDwYY8eIiOpE=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","DJ\u002BouDXt6xAa6457U4R5nMU/5dFpuSVR7W9oUVdiomg="],"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=","/hrHm\u002B3v8DuHSlyFsxHPCFUvDW\u002BZsLR4kaMxNwUHl2M=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","v\u002BEjGaN1m59e9gwl3kXTpjNw\u002B3kwhJD2SLKx38/opjM="],"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=","hGxHNQ\u002B1azEK\u002Bi1edq5HWPFILZgOw/WtDwYY8eIiOpE=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","DJ\u002BouDXt6xAa6457U4R5nMU/5dFpuSVR7W9oUVdiomg="],"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=","/hrHm\u002B3v8DuHSlyFsxHPCFUvDW\u002BZsLR4kaMxNwUHl2M=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","v\u002BEjGaN1m59e9gwl3kXTpjNw\u002B3kwhJD2SLKx38/opjM="],"CachedAssets":{},"CachedCopyCandidates":{}} | ||||
		Reference in New Issue
	
	Block a user