From ae846f2d4834f3cd03079e91a8225e9f74cd073b Mon Sep 17 00:00:00 2001 From: dmolinari Date: Fri, 17 Oct 2025 15:49:15 -0300 Subject: [PATCH] Feat CarouselNacional y Fix Workers --- Elecciones-Web/frontend/src/apiService.ts | 11 ++ .../legislativas/DevAppLegislativas.tsx | 33 ++-- .../nacionales/HomeCarouselNacionalWidget.tsx | 146 ++++++++++++++++++ .../Controllers/ResultadosController.cs | 100 ++++++++++-- .../net9.0/Elecciones.Api.AssemblyInfo.cs | 2 +- .../Debug/net9.0/rjsmcshtml.dswa.cache.json | 2 +- .../Debug/net9.0/rjsmrazor.dswa.cache.json | 2 +- .../net9.0/Elecciones.Core.AssemblyInfo.cs | 2 +- .../Elecciones.Database.AssemblyInfo.cs | 2 +- .../Elecciones.Infrastructure.AssemblyInfo.cs | 2 +- .../Elecciones.Worker/CriticalDataWorker.cs | 138 ++++++++++------- .../LowPriorityDataWorker.cs | 138 ++++++++++------- 12 files changed, 430 insertions(+), 148 deletions(-) create mode 100644 Elecciones-Web/frontend/src/features/legislativas/nacionales/HomeCarouselNacionalWidget.tsx diff --git a/Elecciones-Web/frontend/src/apiService.ts b/Elecciones-Web/frontend/src/apiService.ts index 60d18df..a7dc479 100644 --- a/Elecciones-Web/frontend/src/apiService.ts +++ b/Elecciones-Web/frontend/src/apiService.ts @@ -317,4 +317,15 @@ export const getHomeResumen = async (eleccionId: number, distritoId: string, cat const url = `/elecciones/home-resumen?${queryParams.toString()}`; const { data } = await apiClient.get(url); return data; +}; + +export const getHomeResumenNacional = async (eleccionId: number, categoriaId: number): Promise => { + const queryParams = new URLSearchParams({ + eleccionId: eleccionId.toString(), + categoriaId: categoriaId.toString(), + }); + // Apunta al nuevo endpoint que creamos + const url = `/elecciones/home-resumen-nacional?${queryParams.toString()}`; + const { data } = await apiClient.get(url); + return data; }; \ No newline at end of file diff --git a/Elecciones-Web/frontend/src/features/legislativas/DevAppLegislativas.tsx b/Elecciones-Web/frontend/src/features/legislativas/DevAppLegislativas.tsx index 2bbe9dd..2374eec 100644 --- a/Elecciones-Web/frontend/src/features/legislativas/DevAppLegislativas.tsx +++ b/Elecciones-Web/frontend/src/features/legislativas/DevAppLegislativas.tsx @@ -5,6 +5,7 @@ import { CongresoNacionalWidget } from './nacionales/CongresoNacionalWidget'; import { PanelNacionalWidget } from './nacionales/PanelNacionalWidget'; import { HomeCarouselWidget } from './nacionales/HomeCarouselWidget'; import './DevAppStyle.css' +import { HomeCarouselNacionalWidget } from './nacionales/HomeCarouselNacionalWidget'; // --- NUEVO COMPONENTE REUTILIZABLE PARA CONTENIDO COLAPSABLE --- const CollapsibleWidgetWrapper = ({ children }: { children: React.ReactNode }) => { @@ -52,22 +53,34 @@ export const DevAppLegislativas = () => {
-

Widget: Carrusel de Resultados (Home)

+

Widget: Carrusel de Resultados Provincias (Home)

- Uso: <HomeCarouselWidget eleccionId={2} distritoId="02" categoriaId={2} titulo="Diputados - Provincia de Buenos Aires" /> + Uso: <HomeCarouselWidget eleccionId={2} distritoId="02" categoriaId={3} titulo="Diputados - Provincia de Buenos Aires" />

+
+

Widget: Carrusel de Resultados Nación (Home)

+

+ Uso: <HomeCarouselNacionalWidget eleccionId={2} categoriaId={3} titulo="Diputados - Argentina" /> +

+ +
+ {/* --- SECCIÓN PARA EL WIDGET DE TARJETAS CON EJEMPLOS --- */}

Widget: Resultados por Provincia (Tarjetas)

- +

1. Vista por Defecto

@@ -89,7 +102,7 @@ export const DevAppLegislativas = () => { Ejemplo Buenos Aires: <ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="02" />

- +

Ejemplo Chaco (que también renueva Senadores): <ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="06" />

@@ -101,10 +114,10 @@ export const DevAppLegislativas = () => {

Muestra todas las provincias que votan para una categoría específica.
- Ejemplo Senadores (ID 1): <ResultadosNacionalesCardsWidget eleccionId={2} focoCategoriaId={1} /> + Ejemplo Senadores (ID 2): <ResultadosNacionalesCardsWidget eleccionId={2} focoCategoriaId={2} />

- - + +

4. Indicando Cantidad de Resultados (cantidadResultados)

@@ -135,9 +148,9 @@ export const DevAppLegislativas = () => {
Ejemplo: Mostrar el TOP 1 (el ganador) para la categoría de SENADORES en la provincia de RÍO NEGRO (Distrito ID "16").
- Uso: <ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="16" focoCategoriaId={1} cantidadResultados={1} /> + Uso: <ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="16" focoCategoriaId={2} cantidadResultados={1} />

- +
diff --git a/Elecciones-Web/frontend/src/features/legislativas/nacionales/HomeCarouselNacionalWidget.tsx b/Elecciones-Web/frontend/src/features/legislativas/nacionales/HomeCarouselNacionalWidget.tsx new file mode 100644 index 0000000..a888395 --- /dev/null +++ b/Elecciones-Web/frontend/src/features/legislativas/nacionales/HomeCarouselNacionalWidget.tsx @@ -0,0 +1,146 @@ +// src/features/legislativas/nacionales/HomeCarouselNacionalWidget.tsx +import { useQuery } from '@tanstack/react-query'; +import { getHomeResumenNacional } from '../../../apiService'; +import { ImageWithFallback } from '../../../components/common/ImageWithFallback'; +import { assetBaseUrl } from '../../../apiService'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import { Navigation, A11y } from 'swiper/modules'; + +// @ts-ignore +import 'swiper/css'; +// @ts-ignore +import 'swiper/css/navigation'; +import styles from './HomeCarouselWidget.module.css'; + +const formatPercent = (num: number | null | undefined) => `${(num || 0).toFixed(2).replace('.', ',')}%`; +const formatNumber = (num: number) => num.toLocaleString('es-AR'); + +const formatDateTime = (dateString: string | undefined | null) => { + if (!dateString) return '...'; + try { + const date = new Date(dateString); + if (isNaN(date.getTime())) { + return dateString; + } + const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const year = date.getFullYear(); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + return `${day}/${month}/${year}, ${hours}:${minutes} hs.`; + } catch (e) { + return dateString; + } +}; + +// Las props ya no incluyen distritoId +interface Props { + eleccionId: number; + categoriaId: number; + titulo: string; +} + +export const HomeCarouselNacionalWidget = ({ eleccionId, categoriaId, titulo }: Props) => { + const { data, isLoading, error } = useQuery({ + // La queryKey ahora no necesita distritoId + queryKey: ['homeResumenNacional', eleccionId, categoriaId], + // Llama a la nueva función de la API + queryFn: () => getHomeResumenNacional(eleccionId, categoriaId), + }); + if (isLoading) return
Cargando widget...
; + if (error || !data) return
No se pudieron cargar los datos.
; + + return ( +
+

{titulo}

+ +
+
+ Participación + {formatPercent(data.estadoRecuento?.participacionPorcentaje)} +
+
+ Mesas escrutadas + Escrutado + {formatPercent(data.estadoRecuento?.mesasTotalizadasPorcentaje)} +
+
+ Votos en blanco + En blanco + {formatPercent(data.votosEnBlancoPorcentaje)} +
+
+ Votos totales + Votos + {formatNumber(data.votosTotales)} +
+
+ +
+ + {data.resultados.map(candidato => ( + +
+ +
+ +
+ +
+
+ {candidato.nombreCandidato ? ( + <> + + {candidato.nombreCandidato} + + + {candidato.nombreCortoAgrupacion || candidato.nombreAgrupacion} + + + ) : ( + + {candidato.nombreCortoAgrupacion || candidato.nombreAgrupacion} + + )} +
+
+ {formatPercent(candidato.porcentaje)} + {formatNumber(candidato.votos)} votos +
+
+ +
+
+ ))} +
+ +
+
+
+ +
+ Última actualización: {formatDateTime(data.ultimaActualizacion)} +
+
+ ); +}; \ No newline at end of file diff --git a/Elecciones-Web/src/Elecciones.Api/Controllers/ResultadosController.cs b/Elecciones-Web/src/Elecciones.Api/Controllers/ResultadosController.cs index 9181b61..3a72548 100644 --- a/Elecciones-Web/src/Elecciones.Api/Controllers/ResultadosController.cs +++ b/Elecciones-Web/src/Elecciones.Api/Controllers/ResultadosController.cs @@ -1442,18 +1442,18 @@ List overrides, string agrupacionId, int categoriaId, int? am } private LogoAgrupacionCategoria? FindBestLogoMatch( List logos, string agrupacionId, int categoriaId, int? ambitoId, int eleccionId) -{ - // Prioridad 1: Coincidencia exacta (Elección, Categoría, Ámbito) - return logos.FirstOrDefault(l => l.EleccionId == eleccionId && l.AgrupacionPoliticaId == agrupacionId && l.CategoriaId == categoriaId && l.AmbitoGeograficoId == ambitoId) - // Prioridad 2: Coincidencia por Elección y Categoría (Ámbito genérico) - ?? logos.FirstOrDefault(l => l.EleccionId == eleccionId && l.AgrupacionPoliticaId == agrupacionId && l.CategoriaId == categoriaId && l.AmbitoGeograficoId == null) - // Prioridad 3: Coincidencia de Fallback por Ámbito (Elección genérica) - ?? logos.FirstOrDefault(l => l.EleccionId == 0 && l.AgrupacionPoliticaId == agrupacionId && l.CategoriaId == categoriaId && l.AmbitoGeograficoId == ambitoId) - // Prioridad 4: Coincidencia de Fallback por Categoría (Elección y Ámbito genéricos) - ?? logos.FirstOrDefault(l => l.EleccionId == 0 && l.AgrupacionPoliticaId == agrupacionId && l.CategoriaId == categoriaId && l.AmbitoGeograficoId == null) - // Prioridad 5: LOGO GLOBAL. Coincidencia solo por Partido (Elección y Categoría genéricas) - ?? logos.FirstOrDefault(l => l.EleccionId == 0 && l.AgrupacionPoliticaId == agrupacionId && l.CategoriaId == 0 && l.AmbitoGeograficoId == null); -} + { + // Prioridad 1: Coincidencia exacta (Elección, Categoría, Ámbito) + return logos.FirstOrDefault(l => l.EleccionId == eleccionId && l.AgrupacionPoliticaId == agrupacionId && l.CategoriaId == categoriaId && l.AmbitoGeograficoId == ambitoId) + // Prioridad 2: Coincidencia por Elección y Categoría (Ámbito genérico) + ?? logos.FirstOrDefault(l => l.EleccionId == eleccionId && l.AgrupacionPoliticaId == agrupacionId && l.CategoriaId == categoriaId && l.AmbitoGeograficoId == null) + // Prioridad 3: Coincidencia de Fallback por Ámbito (Elección genérica) + ?? logos.FirstOrDefault(l => l.EleccionId == 0 && l.AgrupacionPoliticaId == agrupacionId && l.CategoriaId == categoriaId && l.AmbitoGeograficoId == ambitoId) + // Prioridad 4: Coincidencia de Fallback por Categoría (Elección y Ámbito genéricos) + ?? logos.FirstOrDefault(l => l.EleccionId == 0 && l.AgrupacionPoliticaId == agrupacionId && l.CategoriaId == categoriaId && l.AmbitoGeograficoId == null) + // Prioridad 5: LOGO GLOBAL. Coincidencia solo por Partido (Elección y Categoría genéricas) + ?? logos.FirstOrDefault(l => l.EleccionId == 0 && l.AgrupacionPoliticaId == agrupacionId && l.CategoriaId == 0 && l.AmbitoGeograficoId == null); + } [HttpGet("resumen-por-provincia")] public async Task GetResumenPorProvincia( @@ -1617,4 +1617,80 @@ List overrides, string agrupacionId, int categoriaId, int? am return Ok(respuesta); } + + [HttpGet("~/api/elecciones/home-resumen-nacional")] + public async Task GetHomeResumenNacional( + [FromQuery] int eleccionId, + [FromQuery] int categoriaId) + { + // Buscamos el ámbito que representa a toda la nación (NivelId 0 o 1 según tu diseño) + var ambitoNacional = await _dbContext.AmbitosGeograficos.AsNoTracking() + .FirstOrDefaultAsync(a => a.NivelId == 0 || a.NivelId == 1); + + if (ambitoNacional == null) return NotFound("No se encontró el ámbito geográfico a nivel nacional."); + + // Agregamos los votos de TODOS los distritos para la categoría especificada + var votosAgregados = await _dbContext.ResultadosVotos.AsNoTracking() + .Where(r => r.EleccionId == eleccionId && r.CategoriaId == categoriaId) + .GroupBy(r => r.AgrupacionPolitica) + .Select(g => new { Agrupacion = g.Key, Votos = g.Sum(r => r.CantidadVotos) }) + .OrderByDescending(x => x.Votos) + .ToListAsync(); + + // El resto de la lógica es muy similar al GetHomeResumen original + var todosLosOverrides = await _dbContext.CandidatosOverrides.AsNoTracking().Where(c => c.EleccionId == eleccionId || c.EleccionId == 0).ToListAsync(); + var todosLosLogos = await _dbContext.LogosAgrupacionesCategorias.AsNoTracking().Where(l => l.EleccionId == eleccionId || l.EleccionId == 0).ToListAsync(); + + // Para el estado del recuento, buscamos el registro general a nivel nacional + var estado = await _dbContext.EstadosRecuentosGenerales.AsNoTracking() + .Include(e => e.CategoriaElectoral) + .FirstOrDefaultAsync(e => e.EleccionId == eleccionId && e.CategoriaId == categoriaId && e.AmbitoGeograficoId == ambitoNacional.Id); + + // Sumamos los votos no positivos de todos los ámbitos + var votosNoPositivosAgregados = await _dbContext.EstadosRecuentos.AsNoTracking() + .Where(e => e.EleccionId == eleccionId && e.CategoriaId == categoriaId) + .GroupBy(e => 1) + .Select(g => new { VotosEnBlanco = g.Sum(e => e.VotosEnBlanco), VotosNulos = g.Sum(e => e.VotosNulos), VotosRecurridos = g.Sum(e => e.VotosRecurridos) }) + .FirstOrDefaultAsync(); + + var totalVotosPositivos = (decimal)votosAgregados.Sum(r => r.Votos); + var votosEnBlanco = votosNoPositivosAgregados?.VotosEnBlanco ?? 0; + var votosTotales = totalVotosPositivos + votosEnBlanco + (votosNoPositivosAgregados?.VotosNulos ?? 0) + (votosNoPositivosAgregados?.VotosRecurridos ?? 0); + + var respuesta = new CategoriaResumenHomeDto + { + CategoriaId = categoriaId, + CategoriaNombre = estado?.CategoriaElectoral.Nombre ?? (categoriaId == 3 ? "DIPUTADOS NACIONALES" : "SENADORES NACIONALES"), + UltimaActualizacion = estado?.FechaTotalizacion ?? DateTime.UtcNow, + EstadoRecuento = estado != null ? new EstadoRecuentoDto + { + ParticipacionPorcentaje = estado.ParticipacionPorcentaje, + MesasTotalizadasPorcentaje = estado.MesasTotalizadasPorcentaje, + CantidadVotantes = estado.CantidadVotantes + } : null, + VotosEnBlanco = votosEnBlanco, + VotosEnBlancoPorcentaje = votosTotales > 0 ? (votosEnBlanco / votosTotales) * 100 : 0, + VotosTotales = (long)votosTotales, + Resultados = votosAgregados.Select(r => + { + // A nivel nacional, no hay un ámbito provincial específico, así que pasamos null. + var candidatoMatch = FindBestCandidatoMatch(todosLosOverrides, r.Agrupacion.Id, categoriaId, null, eleccionId); + var logoMatch = FindBestLogoMatch(todosLosLogos, r.Agrupacion.Id, categoriaId, null, eleccionId); + + return new ResultadoCandidatoDto + { + AgrupacionId = r.Agrupacion.Id, + NombreAgrupacion = r.Agrupacion.Nombre, + NombreCortoAgrupacion = r.Agrupacion.NombreCorto, + NombreCandidato = candidatoMatch?.NombreCandidato, + Color = r.Agrupacion.Color, + Votos = r.Votos, + Porcentaje = totalVotosPositivos > 0 ? (r.Votos / totalVotosPositivos) * 100 : 0, + FotoUrl = logoMatch?.LogoUrl + }; + }).ToList() + }; + + return Ok(respuesta); + } } \ No newline at end of file diff --git a/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/Elecciones.Api.AssemblyInfo.cs b/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/Elecciones.Api.AssemblyInfo.cs index 83b7fee..487fd3b 100644 --- a/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/Elecciones.Api.AssemblyInfo.cs +++ b/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/Elecciones.Api.AssemblyInfo.cs @@ -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+903c2b6a9416b0f694d30b046bd8764b01696e1e")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+4bc257df43f5813ec432b89b47fa078c1cfa1fc8")] [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Api")] [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Api")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json b/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json index cbdeffa..1af09cc 100644 --- a/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json +++ b/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json @@ -1 +1 @@ -{"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["YB39loxHH43S4MF8aTOiogcIbBAIq5Qj3dlJkIfYVxI=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","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=","MI0hVVLYavEhzHq/Z1UbajfrxanA1aET19aOH8G2ImI=","2dY8CqW9fAY8yN0foa\u002BZp2gc0RfPoPmB/tKSj1QoTw0=","79rfGLH4UjfTPvc//\u002BZjnBqdz585pUtYZ0/hwE2iEic=","PUqgvMdfTQkF5lpBVtHv2teQLV5WaEH0xMKTmINe2YQ=","\u002BFI0b4ppdxel/pby/y/xKImHrtdxo2g83OhskdREyIg=","jEESu6\u002BhbDvNMjLt/6OufuK\u002B9cHmzx\u002BTCIn4fWa9nSc=","UaCPJEvR4nVxxGCB5CUnRlJiw4drDW3Q3Nss\u002Bya2cv4=","ZqF13CT3rok/Gzl\u002BMsw3q9X1nf65bwEVD670efE3k\u002Bk=","gH3W7phPzBCY1DAVn4YnP4SA8Uaq73TpctS0yFSvzNM=","u5F4J4\u002BLHUIOCz5ze5NSF42mDeAaAfi\u002BKN3Ay3rKLY8=","GeUUID0ymF5rrBWdX7YHzWA5GiGkNWCNUog4sp4xL3c=","3BxX4I0JXoDqmE8m0BrRZhixBRlHEueS3jAlmUXE/I8=","IlET7uqumshgFxIEvfKRskON\u002BeAKZ7OfD/kCeAwn0PM=","NN2rS\u002B89ZAITWlNODPcF/lHIh3ZNmAHvUX4EjqSkX4s=","OE89N/FsYhRU1Dy5Ne83ehzSwlNc/RcxHrJpHxPHfqY=","QI7IL4TkYEqfUiIEXQiVCaZx4vrM9/wZlvOrhnUd4jQ=","UIntj4QoiyGr7bnJN8KK5PGrhQd89m\u002BLfh4T8VKPxAk=","J\u002Bfv/j3QyIW9bxolc46wDka8641F622/QgIllt0Re80=","Y/o0rakw9VYzEfz9M659qW77P9kvz\u002B2gTe1Lv3zgUDE=","BY4GeeFiQbYpWuSzb2XIY4JatmLNOZ6dhKs4ZT92nsM=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","GY9iGdezAppjqYxnc92Ue2gI167CBYuqcWqyTUygx4E="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file +{"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["YB39loxHH43S4MF8aTOiogcIbBAIq5Qj3dlJkIfYVxI=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","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=","MI0hVVLYavEhzHq/Z1UbajfrxanA1aET19aOH8G2ImI=","2dY8CqW9fAY8yN0foa\u002BZp2gc0RfPoPmB/tKSj1QoTw0=","79rfGLH4UjfTPvc//\u002BZjnBqdz585pUtYZ0/hwE2iEic=","PUqgvMdfTQkF5lpBVtHv2teQLV5WaEH0xMKTmINe2YQ=","\u002BFI0b4ppdxel/pby/y/xKImHrtdxo2g83OhskdREyIg=","jEESu6\u002BhbDvNMjLt/6OufuK\u002B9cHmzx\u002BTCIn4fWa9nSc=","UaCPJEvR4nVxxGCB5CUnRlJiw4drDW3Q3Nss\u002Bya2cv4=","ZqF13CT3rok/Gzl\u002BMsw3q9X1nf65bwEVD670efE3k\u002Bk=","gH3W7phPzBCY1DAVn4YnP4SA8Uaq73TpctS0yFSvzNM=","u5F4J4\u002BLHUIOCz5ze5NSF42mDeAaAfi\u002BKN3Ay3rKLY8=","GeUUID0ymF5rrBWdX7YHzWA5GiGkNWCNUog4sp4xL3c=","3BxX4I0JXoDqmE8m0BrRZhixBRlHEueS3jAlmUXE/I8=","IlET7uqumshgFxIEvfKRskON\u002BeAKZ7OfD/kCeAwn0PM=","NN2rS\u002B89ZAITWlNODPcF/lHIh3ZNmAHvUX4EjqSkX4s=","OE89N/FsYhRU1Dy5Ne83ehzSwlNc/RcxHrJpHxPHfqY=","QI7IL4TkYEqfUiIEXQiVCaZx4vrM9/wZlvOrhnUd4jQ=","UIntj4QoiyGr7bnJN8KK5PGrhQd89m\u002BLfh4T8VKPxAk=","J\u002Bfv/j3QyIW9bxolc46wDka8641F622/QgIllt0Re80=","Y/o0rakw9VYzEfz9M659qW77P9kvz\u002B2gTe1Lv3zgUDE=","/UJFXzVO5Y6TKX\u002BD5m/A1RI/tbK98BAoQFTS7wCUAJI=","BY4GeeFiQbYpWuSzb2XIY4JatmLNOZ6dhKs4ZT92nsM=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","YO7Xv4ZJWkAJrh9hWzESGMBLWiXQVbzGiJis/zKzy9k="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file diff --git a/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json b/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json index ab356c9..dddb5aa 100644 --- a/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json +++ b/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json @@ -1 +1 @@ -{"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["YB39loxHH43S4MF8aTOiogcIbBAIq5Qj3dlJkIfYVxI=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","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=","MI0hVVLYavEhzHq/Z1UbajfrxanA1aET19aOH8G2ImI=","2dY8CqW9fAY8yN0foa\u002BZp2gc0RfPoPmB/tKSj1QoTw0=","79rfGLH4UjfTPvc//\u002BZjnBqdz585pUtYZ0/hwE2iEic=","PUqgvMdfTQkF5lpBVtHv2teQLV5WaEH0xMKTmINe2YQ=","\u002BFI0b4ppdxel/pby/y/xKImHrtdxo2g83OhskdREyIg=","jEESu6\u002BhbDvNMjLt/6OufuK\u002B9cHmzx\u002BTCIn4fWa9nSc=","UaCPJEvR4nVxxGCB5CUnRlJiw4drDW3Q3Nss\u002Bya2cv4=","ZqF13CT3rok/Gzl\u002BMsw3q9X1nf65bwEVD670efE3k\u002Bk=","gH3W7phPzBCY1DAVn4YnP4SA8Uaq73TpctS0yFSvzNM=","u5F4J4\u002BLHUIOCz5ze5NSF42mDeAaAfi\u002BKN3Ay3rKLY8=","GeUUID0ymF5rrBWdX7YHzWA5GiGkNWCNUog4sp4xL3c=","3BxX4I0JXoDqmE8m0BrRZhixBRlHEueS3jAlmUXE/I8=","IlET7uqumshgFxIEvfKRskON\u002BeAKZ7OfD/kCeAwn0PM=","NN2rS\u002B89ZAITWlNODPcF/lHIh3ZNmAHvUX4EjqSkX4s=","OE89N/FsYhRU1Dy5Ne83ehzSwlNc/RcxHrJpHxPHfqY=","QI7IL4TkYEqfUiIEXQiVCaZx4vrM9/wZlvOrhnUd4jQ=","UIntj4QoiyGr7bnJN8KK5PGrhQd89m\u002BLfh4T8VKPxAk=","J\u002Bfv/j3QyIW9bxolc46wDka8641F622/QgIllt0Re80=","Y/o0rakw9VYzEfz9M659qW77P9kvz\u002B2gTe1Lv3zgUDE=","BY4GeeFiQbYpWuSzb2XIY4JatmLNOZ6dhKs4ZT92nsM=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","GY9iGdezAppjqYxnc92Ue2gI167CBYuqcWqyTUygx4E="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file +{"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["YB39loxHH43S4MF8aTOiogcIbBAIq5Qj3dlJkIfYVxI=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","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=","MI0hVVLYavEhzHq/Z1UbajfrxanA1aET19aOH8G2ImI=","2dY8CqW9fAY8yN0foa\u002BZp2gc0RfPoPmB/tKSj1QoTw0=","79rfGLH4UjfTPvc//\u002BZjnBqdz585pUtYZ0/hwE2iEic=","PUqgvMdfTQkF5lpBVtHv2teQLV5WaEH0xMKTmINe2YQ=","\u002BFI0b4ppdxel/pby/y/xKImHrtdxo2g83OhskdREyIg=","jEESu6\u002BhbDvNMjLt/6OufuK\u002B9cHmzx\u002BTCIn4fWa9nSc=","UaCPJEvR4nVxxGCB5CUnRlJiw4drDW3Q3Nss\u002Bya2cv4=","ZqF13CT3rok/Gzl\u002BMsw3q9X1nf65bwEVD670efE3k\u002Bk=","gH3W7phPzBCY1DAVn4YnP4SA8Uaq73TpctS0yFSvzNM=","u5F4J4\u002BLHUIOCz5ze5NSF42mDeAaAfi\u002BKN3Ay3rKLY8=","GeUUID0ymF5rrBWdX7YHzWA5GiGkNWCNUog4sp4xL3c=","3BxX4I0JXoDqmE8m0BrRZhixBRlHEueS3jAlmUXE/I8=","IlET7uqumshgFxIEvfKRskON\u002BeAKZ7OfD/kCeAwn0PM=","NN2rS\u002B89ZAITWlNODPcF/lHIh3ZNmAHvUX4EjqSkX4s=","OE89N/FsYhRU1Dy5Ne83ehzSwlNc/RcxHrJpHxPHfqY=","QI7IL4TkYEqfUiIEXQiVCaZx4vrM9/wZlvOrhnUd4jQ=","UIntj4QoiyGr7bnJN8KK5PGrhQd89m\u002BLfh4T8VKPxAk=","J\u002Bfv/j3QyIW9bxolc46wDka8641F622/QgIllt0Re80=","Y/o0rakw9VYzEfz9M659qW77P9kvz\u002B2gTe1Lv3zgUDE=","/UJFXzVO5Y6TKX\u002BD5m/A1RI/tbK98BAoQFTS7wCUAJI=","BY4GeeFiQbYpWuSzb2XIY4JatmLNOZ6dhKs4ZT92nsM=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","YO7Xv4ZJWkAJrh9hWzESGMBLWiXQVbzGiJis/zKzy9k="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file diff --git a/Elecciones-Web/src/Elecciones.Core/obj/Debug/net9.0/Elecciones.Core.AssemblyInfo.cs b/Elecciones-Web/src/Elecciones.Core/obj/Debug/net9.0/Elecciones.Core.AssemblyInfo.cs index d5508fd..b24d21a 100644 --- a/Elecciones-Web/src/Elecciones.Core/obj/Debug/net9.0/Elecciones.Core.AssemblyInfo.cs +++ b/Elecciones-Web/src/Elecciones.Core/obj/Debug/net9.0/Elecciones.Core.AssemblyInfo.cs @@ -13,7 +13,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Core")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+903c2b6a9416b0f694d30b046bd8764b01696e1e")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+4bc257df43f5813ec432b89b47fa078c1cfa1fc8")] [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Core")] [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Core")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/Elecciones-Web/src/Elecciones.Database/obj/Debug/net9.0/Elecciones.Database.AssemblyInfo.cs b/Elecciones-Web/src/Elecciones.Database/obj/Debug/net9.0/Elecciones.Database.AssemblyInfo.cs index dc06839..69346f6 100644 --- a/Elecciones-Web/src/Elecciones.Database/obj/Debug/net9.0/Elecciones.Database.AssemblyInfo.cs +++ b/Elecciones-Web/src/Elecciones.Database/obj/Debug/net9.0/Elecciones.Database.AssemblyInfo.cs @@ -13,7 +13,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Database")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+903c2b6a9416b0f694d30b046bd8764b01696e1e")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+4bc257df43f5813ec432b89b47fa078c1cfa1fc8")] [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Database")] [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Database")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/Elecciones-Web/src/Elecciones.Infrastructure/obj/Debug/net9.0/Elecciones.Infrastructure.AssemblyInfo.cs b/Elecciones-Web/src/Elecciones.Infrastructure/obj/Debug/net9.0/Elecciones.Infrastructure.AssemblyInfo.cs index 741fd57..7895b9d 100644 --- a/Elecciones-Web/src/Elecciones.Infrastructure/obj/Debug/net9.0/Elecciones.Infrastructure.AssemblyInfo.cs +++ b/Elecciones-Web/src/Elecciones.Infrastructure/obj/Debug/net9.0/Elecciones.Infrastructure.AssemblyInfo.cs @@ -13,7 +13,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Infrastructure")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+903c2b6a9416b0f694d30b046bd8764b01696e1e")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+4bc257df43f5813ec432b89b47fa078c1cfa1fc8")] [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Infrastructure")] [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Infrastructure")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/Elecciones-Web/src/Elecciones.Worker/CriticalDataWorker.cs b/Elecciones-Web/src/Elecciones.Worker/CriticalDataWorker.cs index 8bfafc2..36e02b8 100644 --- a/Elecciones-Web/src/Elecciones.Worker/CriticalDataWorker.cs +++ b/Elecciones-Web/src/Elecciones.Worker/CriticalDataWorker.cs @@ -60,9 +60,9 @@ public class CriticalDataWorker : BackgroundService if (settings.Prioridad == "Resultados" && settings.ResultadosActivado) { _logger.LogInformation("Ejecutando tareas de Resultados en alta prioridad."); + await SondearEstadoRecuentoGeneralAsync(authToken, stoppingToken); await SondearResultadosMunicipalesAsync(authToken, stoppingToken); await SondearResumenProvincialAsync(authToken, stoppingToken); - await SondearEstadoRecuentoGeneralAsync(authToken, stoppingToken); } else if (settings.Prioridad == "Telegramas" && settings.BajasActivado) { @@ -588,92 +588,110 @@ public class CriticalDataWorker : BackgroundService { try { - // PASO 1: Crear un "scope" para obtener una instancia fresca de DbContext. - // Esto es una práctica recomendada para servicios de larga duración para evitar problemas de concurrencia. using var scope = _serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); - // PASO 2: Obtener el ámbito geográfico de la Provincia. - // Necesitamos este objeto para obtener su 'DistritoId' ("02"), que es requerido por la API. - var provincia = await dbContext.AmbitosGeograficos - .AsNoTracking() // Optimización: Solo necesitamos leer datos, no modificarlos. - .FirstOrDefaultAsync(a => a.NivelId == 10, stoppingToken); + var provinciasASondear = await dbContext.AmbitosGeograficos + .AsNoTracking() + .Where(a => a.NivelId == 10 && a.DistritoId != null) + .ToListAsync(stoppingToken); - // Comprobación de seguridad: Si la sincronización inicial falló y no tenemos el registro de la provincia, - // no podemos continuar. Registramos una advertencia y salimos del método. - if (provincia == null) - { - _logger.LogWarning("No se encontró el ámbito 'Provincia' (NivelId 10) en la BD. Omitiendo sondeo de estado general."); - return; - } + // Busca NivelId 1 (País) o 0 como fallback. + var ambitoNacional = await dbContext.AmbitosGeograficos + .AsNoTracking() + .FirstOrDefaultAsync(a => a.NivelId == 1 || a.NivelId == 0, stoppingToken); - // PASO 3: Obtener todas las categorías electorales disponibles desde nuestra base de datos. - // Esto hace que el método sea dinámico y no dependa de IDs fijos en el código. var categoriasParaSondear = await dbContext.CategoriasElectorales .AsNoTracking() .ToListAsync(stoppingToken); - if (!categoriasParaSondear.Any()) + if (!provinciasASondear.Any() || !categoriasParaSondear.Any()) { - _logger.LogWarning("No hay categorías en la BD para sondear el estado general del recuento."); + _logger.LogWarning("No se encontraron Provincias o Categorías para sondear estado general."); return; } - _logger.LogInformation("Iniciando sondeo de Estado Recuento General para {count} categorías...", categoriasParaSondear.Count); + _logger.LogInformation("Iniciando sondeo de Estado Recuento General para {provCount} provincias, el total nacional y {catCount} categorías...", provinciasASondear.Count, categoriasParaSondear.Count); - // PASO 4: Iterar sobre cada categoría para obtener su estado de recuento individual. - foreach (var categoria in categoriasParaSondear) + // Sondeo a nivel provincial + foreach (var provincia in provinciasASondear) { - // Salimos limpiamente del bucle si la aplicación se está deteniendo. if (stoppingToken.IsCancellationRequested) break; - - // Llamamos a la API con el distrito y la CATEGORÍA ACTUAL del bucle. - var estadoDto = await _apiService.GetEstadoRecuentoGeneralAsync(authToken, provincia.DistritoId!, categoria.Id); - - // Solo procedemos si la API devolvió datos válidos. - if (estadoDto != null) + foreach (var categoria in categoriasParaSondear) { - // Lógica "Upsert" (Update or Insert): - // Buscamos un registro existente usando la CLAVE PRIMARIA COMPUESTA. - var registroDb = await dbContext.EstadosRecuentosGenerales.FindAsync( - new object[] { provincia.Id, categoria.Id }, - cancellationToken: stoppingToken - ); + if (stoppingToken.IsCancellationRequested) break; - // Si no se encuentra (FindAsync devuelve null), es un registro nuevo. - if (registroDb == null) + var estadoDto = await _apiService.GetEstadoRecuentoGeneralAsync(authToken, provincia.DistritoId!, categoria.Id); + if (estadoDto != null) { - // Creamos una nueva instancia de la entidad. - registroDb = new EstadoRecuentoGeneral + var registroDb = await dbContext.EstadosRecuentosGenerales.FindAsync(new object[] { provincia.Id, categoria.Id }, stoppingToken); + if (registroDb == null) { - EleccionId = EleccionId, - AmbitoGeograficoId = provincia.Id, - CategoriaId = categoria.Id // Asignamos ambas partes de la clave primaria. - }; - // Y la añadimos al ChangeTracker de EF para que la inserte en la BD. - dbContext.EstadosRecuentosGenerales.Add(registroDb); + registroDb = new EstadoRecuentoGeneral { EleccionId = EleccionId, AmbitoGeograficoId = provincia.Id, CategoriaId = categoria.Id }; + dbContext.EstadosRecuentosGenerales.Add(registroDb); + } + registroDb.FechaTotalizacion = DateTime.UtcNow; + registroDb.MesasEsperadas = estadoDto.MesasEsperadas; + registroDb.MesasTotalizadas = estadoDto.MesasTotalizadas; + registroDb.MesasTotalizadasPorcentaje = estadoDto.MesasTotalizadasPorcentaje; + registroDb.CantidadElectores = estadoDto.CantidadElectores; + registroDb.CantidadVotantes = estadoDto.CantidadVotantes; + registroDb.ParticipacionPorcentaje = estadoDto.ParticipacionPorcentaje; } - - // Mapeamos los datos del DTO de la API a nuestra entidad de base de datos. - // Esto se hace tanto para registros nuevos como para los existentes que se van a actualizar. - registroDb.MesasEsperadas = estadoDto.MesasEsperadas; - registroDb.MesasTotalizadas = estadoDto.MesasTotalizadas; - registroDb.MesasTotalizadasPorcentaje = estadoDto.MesasTotalizadasPorcentaje; - registroDb.CantidadElectores = estadoDto.CantidadElectores; - registroDb.CantidadVotantes = estadoDto.CantidadVotantes; - registroDb.ParticipacionPorcentaje = estadoDto.ParticipacionPorcentaje; } } - // PASO 5: Guardar todos los cambios en la base de datos. - // Al llamar a SaveChangesAsync UNA SOLA VEZ fuera del bucle, EF Core agrupa - // todas las inserciones y actualizaciones en una única transacción eficiente. - await dbContext.SaveChangesAsync(stoppingToken); - _logger.LogInformation("Sondeo de Estado Recuento General completado para todas las categorías."); + // Bloque para el sondeo a nivel nacional + if (ambitoNacional != null && !stoppingToken.IsCancellationRequested) + { + _logger.LogInformation("Sondeando totales a nivel Nacional (Ambito ID: {ambitoId})...", ambitoNacional.Id); + foreach (var categoria in categoriasParaSondear) + { + if (stoppingToken.IsCancellationRequested) break; + + var estadoNacionalDto = await _apiService.GetEstadoRecuentoGeneralAsync(authToken, "", categoria.Id); + + if (estadoNacionalDto != null) + { + var registroNacionalDb = await dbContext.EstadosRecuentosGenerales.FindAsync(new object[] { ambitoNacional.Id, categoria.Id }, stoppingToken); + if (registroNacionalDb == null) + { + registroNacionalDb = new EstadoRecuentoGeneral { EleccionId = EleccionId, AmbitoGeograficoId = ambitoNacional.Id, CategoriaId = categoria.Id }; + dbContext.EstadosRecuentosGenerales.Add(registroNacionalDb); + } + registroNacionalDb.FechaTotalizacion = DateTime.UtcNow; + registroNacionalDb.MesasEsperadas = estadoNacionalDto.MesasEsperadas; + registroNacionalDb.MesasTotalizadas = estadoNacionalDto.MesasTotalizadas; + registroNacionalDb.MesasTotalizadasPorcentaje = estadoNacionalDto.MesasTotalizadasPorcentaje; + registroNacionalDb.CantidadElectores = estadoNacionalDto.CantidadElectores; + registroNacionalDb.CantidadVotantes = estadoNacionalDto.CantidadVotantes; + registroNacionalDb.ParticipacionPorcentaje = estadoNacionalDto.ParticipacionPorcentaje; + _logger.LogInformation("Datos nacionales para categoría '{catNombre}' actualizados.", categoria.Nombre); + } + } + } + else if (ambitoNacional == null) + { + _logger.LogWarning("No se encontró el ámbito geográfico para el Nivel Nacional (NivelId 1 o 0). No se pueden capturar los totales del país."); + } + + // Guardar todos los cambios + if (dbContext.ChangeTracker.HasChanges()) + { + await dbContext.SaveChangesAsync(stoppingToken); + _logger.LogInformation("Sondeo de Estado Recuento General completado. Se han guardado los cambios en la base de datos."); + } + else + { + _logger.LogInformation("Sondeo de Estado Recuento General completado. No se detectaron cambios."); + } + } + catch (OperationCanceledException) + { + _logger.LogInformation("Sondeo de Estado Recuento General cancelado."); } catch (Exception ex) { - // Capturamos cualquier excepción inesperada para que no detenga el worker y la registramos. _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Estado Recuento General."); } } diff --git a/Elecciones-Web/src/Elecciones.Worker/LowPriorityDataWorker.cs b/Elecciones-Web/src/Elecciones.Worker/LowPriorityDataWorker.cs index f03c931..04f77ab 100644 --- a/Elecciones-Web/src/Elecciones.Worker/LowPriorityDataWorker.cs +++ b/Elecciones-Web/src/Elecciones.Worker/LowPriorityDataWorker.cs @@ -56,9 +56,9 @@ public class LowPriorityDataWorker : BackgroundService if (settings.Prioridad == "Telegramas" && settings.ResultadosActivado) { _logger.LogInformation("Ejecutando tareas de Resultados en baja prioridad."); + await SondearEstadoRecuentoGeneralAsync(authToken, stoppingToken); await SondearResultadosMunicipalesAsync(authToken, stoppingToken); await SondearResumenProvincialAsync(authToken, stoppingToken); - await SondearEstadoRecuentoGeneralAsync(authToken, stoppingToken); } else if (settings.Prioridad == "Resultados" && settings.BajasActivado) { @@ -320,92 +320,110 @@ public class LowPriorityDataWorker : BackgroundService { try { - // PASO 1: Crear un "scope" para obtener una instancia fresca de DbContext. - // Esto es una práctica recomendada para servicios de larga duración para evitar problemas de concurrencia. using var scope = _serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); - // PASO 2: Obtener el ámbito geográfico de la Provincia. - // Necesitamos este objeto para obtener su 'DistritoId' ("02"), que es requerido por la API. - var provincia = await dbContext.AmbitosGeograficos - .AsNoTracking() // Optimización: Solo necesitamos leer datos, no modificarlos. - .FirstOrDefaultAsync(a => a.NivelId == 10, stoppingToken); + var provinciasASondear = await dbContext.AmbitosGeograficos + .AsNoTracking() + .Where(a => a.NivelId == 10 && a.DistritoId != null) + .ToListAsync(stoppingToken); - // Comprobación de seguridad: Si la sincronización inicial falló y no tenemos el registro de la provincia, - // no podemos continuar. Registramos una advertencia y salimos del método. - if (provincia == null) - { - _logger.LogWarning("No se encontró el ámbito 'Provincia' (NivelId 10) en la BD. Omitiendo sondeo de estado general."); - return; - } + // Busca NivelId 1 (País) o 0 como fallback. + var ambitoNacional = await dbContext.AmbitosGeograficos + .AsNoTracking() + .FirstOrDefaultAsync(a => a.NivelId == 1 || a.NivelId == 0, stoppingToken); - // PASO 3: Obtener todas las categorías electorales disponibles desde nuestra base de datos. - // Esto hace que el método sea dinámico y no dependa de IDs fijos en el código. var categoriasParaSondear = await dbContext.CategoriasElectorales .AsNoTracking() .ToListAsync(stoppingToken); - if (!categoriasParaSondear.Any()) + if (!provinciasASondear.Any() || !categoriasParaSondear.Any()) { - _logger.LogWarning("No hay categorías en la BD para sondear el estado general del recuento."); + _logger.LogWarning("No se encontraron Provincias o Categorías para sondear estado general."); return; } - _logger.LogInformation("Iniciando sondeo de Estado Recuento General para {count} categorías...", categoriasParaSondear.Count); + _logger.LogInformation("Iniciando sondeo de Estado Recuento General para {provCount} provincias, el total nacional y {catCount} categorías...", provinciasASondear.Count, categoriasParaSondear.Count); - // PASO 4: Iterar sobre cada categoría para obtener su estado de recuento individual. - foreach (var categoria in categoriasParaSondear) + // Sondeo a nivel provincial + foreach (var provincia in provinciasASondear) { - // Salimos limpiamente del bucle si la aplicación se está deteniendo. if (stoppingToken.IsCancellationRequested) break; - - // Llamamos a la API con el distrito y la CATEGORÍA ACTUAL del bucle. - var estadoDto = await _apiService.GetEstadoRecuentoGeneralAsync(authToken, provincia.DistritoId!, categoria.Id); - - // Solo procedemos si la API devolvió datos válidos. - if (estadoDto != null) + foreach (var categoria in categoriasParaSondear) { - // Lógica "Upsert" (Update or Insert): - // Buscamos un registro existente usando la CLAVE PRIMARIA COMPUESTA. - var registroDb = await dbContext.EstadosRecuentosGenerales.FindAsync( - new object[] { provincia.Id, categoria.Id }, - cancellationToken: stoppingToken - ); + if (stoppingToken.IsCancellationRequested) break; - // Si no se encuentra (FindAsync devuelve null), es un registro nuevo. - if (registroDb == null) + var estadoDto = await _apiService.GetEstadoRecuentoGeneralAsync(authToken, provincia.DistritoId!, categoria.Id); + if (estadoDto != null) { - // Creamos una nueva instancia de la entidad. - registroDb = new EstadoRecuentoGeneral + var registroDb = await dbContext.EstadosRecuentosGenerales.FindAsync(new object[] { provincia.Id, categoria.Id }, stoppingToken); + if (registroDb == null) { - EleccionId = EleccionId, - AmbitoGeograficoId = provincia.Id, - CategoriaId = categoria.Id // Asignamos ambas partes de la clave primaria. - }; - // Y la añadimos al ChangeTracker de EF para que la inserte en la BD. - dbContext.EstadosRecuentosGenerales.Add(registroDb); + registroDb = new EstadoRecuentoGeneral { EleccionId = EleccionId, AmbitoGeograficoId = provincia.Id, CategoriaId = categoria.Id }; + dbContext.EstadosRecuentosGenerales.Add(registroDb); + } + registroDb.FechaTotalizacion = DateTime.UtcNow; + registroDb.MesasEsperadas = estadoDto.MesasEsperadas; + registroDb.MesasTotalizadas = estadoDto.MesasTotalizadas; + registroDb.MesasTotalizadasPorcentaje = estadoDto.MesasTotalizadasPorcentaje; + registroDb.CantidadElectores = estadoDto.CantidadElectores; + registroDb.CantidadVotantes = estadoDto.CantidadVotantes; + registroDb.ParticipacionPorcentaje = estadoDto.ParticipacionPorcentaje; } - - // Mapeamos los datos del DTO de la API a nuestra entidad de base de datos. - // Esto se hace tanto para registros nuevos como para los existentes que se van a actualizar. - registroDb.MesasEsperadas = estadoDto.MesasEsperadas; - registroDb.MesasTotalizadas = estadoDto.MesasTotalizadas; - registroDb.MesasTotalizadasPorcentaje = estadoDto.MesasTotalizadasPorcentaje; - registroDb.CantidadElectores = estadoDto.CantidadElectores; - registroDb.CantidadVotantes = estadoDto.CantidadVotantes; - registroDb.ParticipacionPorcentaje = estadoDto.ParticipacionPorcentaje; } } - // PASO 5: Guardar todos los cambios en la base de datos. - // Al llamar a SaveChangesAsync UNA SOLA VEZ fuera del bucle, EF Core agrupa - // todas las inserciones y actualizaciones en una única transacción eficiente. - await dbContext.SaveChangesAsync(stoppingToken); - _logger.LogInformation("Sondeo de Estado Recuento General completado para todas las categorías."); + // Bloque para el sondeo a nivel nacional + if (ambitoNacional != null && !stoppingToken.IsCancellationRequested) + { + _logger.LogInformation("Sondeando totales a nivel Nacional (Ambito ID: {ambitoId})...", ambitoNacional.Id); + foreach (var categoria in categoriasParaSondear) + { + if (stoppingToken.IsCancellationRequested) break; + + var estadoNacionalDto = await _apiService.GetEstadoRecuentoGeneralAsync(authToken, "", categoria.Id); + + if (estadoNacionalDto != null) + { + var registroNacionalDb = await dbContext.EstadosRecuentosGenerales.FindAsync(new object[] { ambitoNacional.Id, categoria.Id }, stoppingToken); + if (registroNacionalDb == null) + { + registroNacionalDb = new EstadoRecuentoGeneral { EleccionId = EleccionId, AmbitoGeograficoId = ambitoNacional.Id, CategoriaId = categoria.Id }; + dbContext.EstadosRecuentosGenerales.Add(registroNacionalDb); + } + registroNacionalDb.FechaTotalizacion = DateTime.UtcNow; + registroNacionalDb.MesasEsperadas = estadoNacionalDto.MesasEsperadas; + registroNacionalDb.MesasTotalizadas = estadoNacionalDto.MesasTotalizadas; + registroNacionalDb.MesasTotalizadasPorcentaje = estadoNacionalDto.MesasTotalizadasPorcentaje; + registroNacionalDb.CantidadElectores = estadoNacionalDto.CantidadElectores; + registroNacionalDb.CantidadVotantes = estadoNacionalDto.CantidadVotantes; + registroNacionalDb.ParticipacionPorcentaje = estadoNacionalDto.ParticipacionPorcentaje; + _logger.LogInformation("Datos nacionales para categoría '{catNombre}' actualizados.", categoria.Nombre); + } + } + } + else if (ambitoNacional == null) + { + _logger.LogWarning("No se encontró el ámbito geográfico para el Nivel Nacional (NivelId 1 o 0). No se pueden capturar los totales del país."); + } + + // Guardar todos los cambios + if (dbContext.ChangeTracker.HasChanges()) + { + await dbContext.SaveChangesAsync(stoppingToken); + _logger.LogInformation("Sondeo de Estado Recuento General completado. Se han guardado los cambios en la base de datos."); + } + else + { + _logger.LogInformation("Sondeo de Estado Recuento General completado. No se detectaron cambios."); + } + } + catch (OperationCanceledException) + { + _logger.LogInformation("Sondeo de Estado Recuento General cancelado."); } catch (Exception ex) { - // Capturamos cualquier excepción inesperada para que no detenga el worker y la registramos. _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Estado Recuento General."); } }