From a81f1fe8943d84eeeb1862a3d99fc5ec89bf4840 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Wed, 3 Sep 2025 13:49:35 -0300 Subject: [PATCH] Test Public Side --- Elecciones-Web/frontend-admin/Dockerfile | 18 +++ .../frontend-admin/frontend.nginx.conf | 18 +++ Elecciones-Web/frontend-admin/index.html | 4 +- .../frontend-admin/public/eldia.svg | 6 + Elecciones-Web/frontend/index.html | 6 +- Elecciones-Web/frontend/public/bootstrap.js | 71 ++++++++++ Elecciones-Web/frontend/public/eldia.svg | 6 + Elecciones-Web/frontend/src/App.tsx | 35 ++++- Elecciones-Web/frontend/src/apiService.ts | 15 ++- .../components/ConcejalesPorSeccionWidget.tsx | 124 +++++++++++++++++ .../src/components/ConcejalesTickerWidget.tsx | 84 ++++++++++++ .../frontend/src/components/DevApp.tsx | 46 +++++++ ...ickerWidget.tsx => DipSenTickerWidget.tsx} | 4 +- .../components/DiputadosPorSeccionWidget.tsx | 127 ++++++++++++++++++ .../src/components/DiputadosTickerWidget.tsx | 84 ++++++++++++ .../src/components/ResumenGeneralWidget.tsx | 117 ++++++++++++++++ .../components/SenadoresPorSeccionWidget.tsx | 127 ++++++++++++++++++ .../src/components/SenadoresTickerWidget.tsx | 84 ++++++++++++ .../src/components/SenadoresWidget.tsx | 4 +- Elecciones-Web/frontend/src/main.tsx | 94 +++++++++++-- Elecciones-Web/frontend/src/types/types.ts | 5 + Elecciones-Web/frontend/vite.config.ts | 34 ++++- .../Controllers/CatalogosController.cs | 41 ++++-- .../Controllers/ResultadosController.cs | 35 +++-- .../net9.0/Elecciones.Api.AssemblyInfo.cs | 2 +- .../Debug/net9.0/rjsmcshtml.dswa.cache.json | 2 +- .../Debug/net9.0/rjsmrazor.dswa.cache.json | 2 +- ...iones.Database.EntityFrameworkCore.targets | 28 ---- .../net9.0/Elecciones.Core.AssemblyInfo.cs | 2 +- .../Elecciones.Database.AssemblyInfo.cs | 2 +- .../Elecciones.Infrastructure.AssemblyInfo.cs | 2 +- docker-compose.yml | 47 ++++--- proxy/nginx.conf | 62 +++++---- 33 files changed, 1205 insertions(+), 133 deletions(-) create mode 100644 Elecciones-Web/frontend-admin/Dockerfile create mode 100644 Elecciones-Web/frontend-admin/frontend.nginx.conf create mode 100644 Elecciones-Web/frontend-admin/public/eldia.svg create mode 100644 Elecciones-Web/frontend/public/bootstrap.js create mode 100644 Elecciones-Web/frontend/public/eldia.svg create mode 100644 Elecciones-Web/frontend/src/components/ConcejalesPorSeccionWidget.tsx create mode 100644 Elecciones-Web/frontend/src/components/ConcejalesTickerWidget.tsx create mode 100644 Elecciones-Web/frontend/src/components/DevApp.tsx rename Elecciones-Web/frontend/src/components/{TickerWidget.tsx => DipSenTickerWidget.tsx} (97%) create mode 100644 Elecciones-Web/frontend/src/components/DiputadosPorSeccionWidget.tsx create mode 100644 Elecciones-Web/frontend/src/components/DiputadosTickerWidget.tsx create mode 100644 Elecciones-Web/frontend/src/components/ResumenGeneralWidget.tsx create mode 100644 Elecciones-Web/frontend/src/components/SenadoresPorSeccionWidget.tsx create mode 100644 Elecciones-Web/frontend/src/components/SenadoresTickerWidget.tsx delete mode 100644 Elecciones-Web/src/Elecciones.Api/src/obj/Elecciones.Database.EntityFrameworkCore.targets diff --git a/Elecciones-Web/frontend-admin/Dockerfile b/Elecciones-Web/frontend-admin/Dockerfile new file mode 100644 index 0000000..ee4d327 --- /dev/null +++ b/Elecciones-Web/frontend-admin/Dockerfile @@ -0,0 +1,18 @@ +# frontend-admin/Dockerfile + +# --- Etapa 1: Build (Construcción) --- +FROM node:20-alpine AS build +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +RUN npm run build + +# --- Etapa 2: Serve (Servir con Nginx configurado para SPA) --- +FROM nginx:1.25-alpine +COPY --from=build /app/dist /usr/share/nginx/html + +# Copiamos nuestra configuración de Nginx para manejar el enrutamiento de React +COPY frontend.nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 \ No newline at end of file diff --git a/Elecciones-Web/frontend-admin/frontend.nginx.conf b/Elecciones-Web/frontend-admin/frontend.nginx.conf new file mode 100644 index 0000000..d1f6335 --- /dev/null +++ b/Elecciones-Web/frontend-admin/frontend.nginx.conf @@ -0,0 +1,18 @@ +# frontend-admin/frontend.nginx.conf + +server { + listen 80; + server_name localhost; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location ~* \.(?:css|js|jpg|jpeg|gif|png|ico|svg|woff|woff2)$ { + expires 1y; + add_header Cache-Control "public"; + } +} \ No newline at end of file diff --git a/Elecciones-Web/frontend-admin/index.html b/Elecciones-Web/frontend-admin/index.html index e4b78ea..29721e9 100644 --- a/Elecciones-Web/frontend-admin/index.html +++ b/Elecciones-Web/frontend-admin/index.html @@ -2,9 +2,9 @@ - + - Vite + React + TS + Elecciones 2025 - El Día
diff --git a/Elecciones-Web/frontend-admin/public/eldia.svg b/Elecciones-Web/frontend-admin/public/eldia.svg new file mode 100644 index 0000000..91ce072 --- /dev/null +++ b/Elecciones-Web/frontend-admin/public/eldia.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/Elecciones-Web/frontend/index.html b/Elecciones-Web/frontend/index.html index e4b78ea..b8d4c21 100644 --- a/Elecciones-Web/frontend/index.html +++ b/Elecciones-Web/frontend/index.html @@ -2,12 +2,12 @@ - + - Vite + React + TS + Elecciones 2025 - Dev Showcase
- + \ No newline at end of file diff --git a/Elecciones-Web/frontend/public/bootstrap.js b/Elecciones-Web/frontend/public/bootstrap.js new file mode 100644 index 0000000..73609b1 --- /dev/null +++ b/Elecciones-Web/frontend/public/bootstrap.js @@ -0,0 +1,71 @@ +// public/bootstrap.js +(function() { + // --- CONFIGURACIÓN --- + // Cambie esto por el dominio final en producción + const WIDGETS_HOST = 'https://elecciones2025.eldia.com'; + + // --- FUNCIONES AUXILIARES (sin cambios) --- + function loadScript(src) { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.type = 'module'; + script.src = src; + script.onload = resolve; + script.onerror = reject; + document.head.appendChild(script); + }); + } + + function loadCSS(href) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = href; + document.head.appendChild(link); + } + + // --- LÓGICA PRINCIPAL --- + async function initWidgets() { + try { + // 1. Obtener el manifest.json + const response = await fetch(`${WIDGETS_HOST}/manifest.json`); + if (!response.ok) throw new Error('No se pudo cargar el manifest.'); + const manifest = await response.json(); + + // 2. Encontrar el punto de entrada principal (nuestro main.tsx) + // En modo 'lib', la entrada es el propio archivo de entrada. + const entryKey = 'src/main.tsx'; + const entry = manifest[entryKey]; + + if (!entry) throw new Error('No se encontró el punto de entrada en el manifest.'); + + const jsUrl = `${WIDGETS_HOST}/${entry.file}`; + + // 3. Cargar el CSS asociado, si existe + if (entry.css && entry.css.length > 0) { + entry.css.forEach(cssFile => { + const cssUrl = `${WIDGETS_HOST}/${cssFile}`; + loadCSS(cssUrl); + }); + } + + // 4. Cargar el script principal de la librería + await loadScript(jsUrl); + + // 5. Esperar a que la página esté completamente cargada + // y luego llamar a la función de renderizado. + window.addEventListener('load', function () { + if (window.EleccionesWidgets && typeof window.EleccionesWidgets.render === 'function') { + console.log('Elecciones Widgets listos. Renderizando...'); + window.EleccionesWidgets.render(); + } + }); + + } catch (error) { + console.error('Error al inicializar los widgets de Elecciones:', error); + } + } + + // Iniciar el proceso + initWidgets(); + +})(); \ No newline at end of file diff --git a/Elecciones-Web/frontend/public/eldia.svg b/Elecciones-Web/frontend/public/eldia.svg new file mode 100644 index 0000000..91ce072 --- /dev/null +++ b/Elecciones-Web/frontend/public/eldia.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/Elecciones-Web/frontend/src/App.tsx b/Elecciones-Web/frontend/src/App.tsx index dabfc74..f70ac2d 100644 --- a/Elecciones-Web/frontend/src/App.tsx +++ b/Elecciones-Web/frontend/src/App.tsx @@ -3,26 +3,55 @@ import './App.css' import { BancasWidget } from './components/BancasWidget' import { CongresoWidget } from './components/CongresoWidget' import MapaBsAs from './components/MapaBsAs' -import { TickerWidget } from './components/TickerWidget' +import { DipSenTickerWidget } from './components/DipSenTickerWidget' import { TelegramaWidget } from './components/TelegramaWidget' import { ConcejalesWidget } from './components/ConcejalesWidget' import MapaBsAsSecciones from './components/MapaBsAsSecciones' import { SenadoresWidget } from './components/SenadoresWidget' import { DiputadosWidget } from './components/DiputadosWidget' +import { ResumenGeneralWidget } from './components/ResumenGeneralWidget' +import { SenadoresTickerWidget } from './components/SenadoresTickerWidget' +import { DiputadosTickerWidget } from './components/DiputadosTickerWidget' +import { ConcejalesTickerWidget } from './components/ConcejalesTickerWidget' +import { DiputadosPorSeccionWidget } from './components/DiputadosPorSeccionWidget' +import { SenadoresPorSeccionWidget } from './components/SenadoresPorSeccionWidget' +import { ConcejalesPorSeccionWidget } from './components/ConcejalesPorSeccionWidget' function App() { return ( <>

Resultados Electorales - Provincia de Buenos Aires

-
- +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+
+
+
+
+
diff --git a/Elecciones-Web/frontend/src/apiService.ts b/Elecciones-Web/frontend/src/apiService.ts index ee38df0..2ce7fdc 100644 --- a/Elecciones-Web/frontend/src/apiService.ts +++ b/Elecciones-Web/frontend/src/apiService.ts @@ -1,6 +1,6 @@ // src/apiService.ts import axios from 'axios'; -import type { ProyeccionBancas, MunicipioSimple, TelegramaData, CatalogoItem, CategoriaResumen, ResultadoTicker } from './types/types'; +import type { ProyeccionBancas, MunicipioSimple, TelegramaData, CatalogoItem, CategoriaResumen, ResultadoTicker, ApiResponseResultadosPorSeccion } from './types/types'; const API_BASE_URL = 'http://localhost:5217/api'; @@ -81,8 +81,13 @@ export const getBancasPorSeccion = async (seccionId: string): Promise => { - const response = await apiClient.get('/catalogos/secciones-electorales'); +export const getSeccionesElectorales = async (categoriaId?: number): Promise => { + let url = '/catalogos/secciones-electorales'; + // Si se proporciona una categoría, la añadimos a la URL + if (categoriaId) { + url += `?categoriaId=${categoriaId}`; + } + const response = await apiClient.get(url); return response.data; }; @@ -134,8 +139,8 @@ export const getConfiguracionPublica = async (): Promise = return response.data; }; -export const getResultadosConcejales = async (seccionId: string): Promise => { - const response = await apiClient.get(`/resultados/concejales/${seccionId}`); +export const getResultadosPorSeccion = async (seccionId: string, categoriaId: number): Promise => { + const response = await apiClient.get(`/resultados/seccion-resultados/${seccionId}?categoriaId=${categoriaId}`); return response.data; }; diff --git a/Elecciones-Web/frontend/src/components/ConcejalesPorSeccionWidget.tsx b/Elecciones-Web/frontend/src/components/ConcejalesPorSeccionWidget.tsx new file mode 100644 index 0000000..908768d --- /dev/null +++ b/Elecciones-Web/frontend/src/components/ConcejalesPorSeccionWidget.tsx @@ -0,0 +1,124 @@ +// src/components/ConcejalesPorSeccionWidget.tsx +import { useState, useMemo, useEffect } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import Select from 'react-select'; +import { getSeccionesElectorales, getResultadosPorSeccion, getConfiguracionPublica } from '../apiService'; +import type { MunicipioSimple, ResultadoTicker, ApiResponseResultadosPorSeccion } from '../types/types'; +import { ImageWithFallback } from './ImageWithFallback'; +import './TickerWidget.css'; // Reutilizamos los estilos del ticker + +const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`; + +// Estilos personalizados para que el selector se vea bien +const customSelectStyles = { + control: (base: any) => ({ ...base, minWidth: '220px', border: '1px solid #ced4da' }), + menu: (base: any) => ({ ...base, zIndex: 10 }), // Para que el menú se superponga +}; + +const CATEGORIA_ID = 7; // ID para Concejales + +export const ConcejalesPorSeccionWidget = () => { + const [secciones, setSecciones] = useState([]); + const [selectedSeccion, setSelectedSeccion] = useState<{ value: string; label: string } | null>(null); + + const { data: configData } = useQuery({ + queryKey: ['configuracionPublica'], + queryFn: getConfiguracionPublica, + staleTime: 0, + }); + + const cantidadAMostrar = parseInt(configData?.TickerResultadosCantidad || '5', 10); + + // useEffect para obtener la lista de secciones una sola vez + useEffect(() => { + getSeccionesElectorales().then(seccionesData => { + if (seccionesData && seccionesData.length > 0) { + const orden = new Map([ + ['Capital', 0], ['Primera', 1], ['Segunda', 2], ['Tercera', 3], + ['Cuarta', 4], ['Quinta', 5], ['Sexta', 6], ['Séptima', 7] + ]); + const getOrden = (nombre: string) => { + const match = nombre.match(/Capital|Primera|Segunda|Tercera|Cuarta|Quinta|Sexta|Séptima/); + return match ? orden.get(match[0]) ?? 99 : 99; + }; + seccionesData.sort((a, b) => getOrden(a.nombre) - getOrden(b.nombre)); + + setSecciones(seccionesData); + // Establecemos la primera sección de la lista ordenada como la por defecto + if (!selectedSeccion) { + setSelectedSeccion({ value: seccionesData[0].id, label: seccionesData[0].nombre }); + } + } + }); + }, [selectedSeccion]); // Dependencia para asegurar que no se resetee la selección del usuario + + // Transformamos los datos para react-select + const seccionOptions = useMemo(() => + secciones.map(s => ({ value: s.id, label: s.nombre })), + [secciones]); + + // Query para obtener los resultados de la sección seleccionada + const { data, isLoading: isLoadingResultados } = useQuery({ + queryKey: ['resultadosPorSeccion', selectedSeccion?.value, CATEGORIA_ID], + queryFn: () => getResultadosPorSeccion(selectedSeccion!.value, CATEGORIA_ID), + enabled: !!selectedSeccion, + }); + + const resultados = data?.resultados || []; + + // Lógica para "Otros" + let displayResults: ResultadoTicker[] = resultados; + if (resultados && resultados.length > cantidadAMostrar) { + const topParties = resultados.slice(0, cantidadAMostrar - 1); + const otherParties = resultados.slice(cantidadAMostrar - 1); + const otrosPorcentaje = otherParties.reduce((sum, party) => sum + (party.porcentaje || 0), 0); + const otrosEntry: ResultadoTicker = { + id: `otros-concejales-${selectedSeccion?.value}`, + nombre: 'Otros', + nombreCorto: 'Otros', + color: '#888888', + logoUrl: null, + votos: 0, + porcentaje: otrosPorcentaje, + }; + displayResults = [...topParties, otrosEntry]; + } else if (resultados) { + displayResults = resultados.slice(0, cantidadAMostrar); + } + + return ( +
+
+

CONCEJALES POR SECCIÓN

+ setSelectedSeccion(option)} + isLoading={isLoadingSecciones} + placeholder="Seleccionar sección..." + styles={customSelectStyles} + /> +
+
+ {(isLoadingResultados && selectedSeccion) &&

Cargando...

} + {!selectedSeccion && !isLoadingSecciones &&

Seleccione una sección.

} + {displayResults.map(partido => ( +
+
+ +
+
+
+ {partido.nombreCorto || partido.nombre} + {formatPercent(partido.porcentaje)} +
+
+
+
+
+
+ ))} +
+
+ ); +}; \ No newline at end of file diff --git a/Elecciones-Web/frontend/src/components/DiputadosTickerWidget.tsx b/Elecciones-Web/frontend/src/components/DiputadosTickerWidget.tsx new file mode 100644 index 0000000..7160462 --- /dev/null +++ b/Elecciones-Web/frontend/src/components/DiputadosTickerWidget.tsx @@ -0,0 +1,84 @@ +// src/components/DiputadosTickerWidget.tsx +import { useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { getResumenProvincial, getConfiguracionPublica } from '../apiService'; +import type { CategoriaResumen, ResultadoTicker } from '../types/types'; +import { ImageWithFallback } from './ImageWithFallback'; +import './TickerWidget.css'; + +const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`; +const CATEGORIA_ID = 6; // ID para Diputados + +export const DiputadosTickerWidget = () => { + const { data: categorias, isLoading, error } = useQuery({ + queryKey: ['resumenProvincial'], + queryFn: getResumenProvincial, + refetchInterval: 30000, + }); + + const { data: configData } = useQuery({ + queryKey: ['configuracionPublica'], + queryFn: getConfiguracionPublica, + staleTime: 0, + }); + + const cantidadAMostrar = parseInt(configData?.TickerResultadosCantidad || '5', 10); + + // Usamos useMemo para encontrar los datos específicos de Diputados + const diputadosData = useMemo(() => { + return categorias?.find(c => c.categoriaId === CATEGORIA_ID); + }, [categorias]); + + if (isLoading) return

Cargando...

; + if (error || !diputadosData) return

Datos de Diputados no disponibles.

; + + // Lógica para "Otros" + let displayResults: ResultadoTicker[] = diputadosData.resultados; + if (diputadosData.resultados.length > cantidadAMostrar) { + const topParties = diputadosData.resultados.slice(0, cantidadAMostrar - 1); + const otherParties = diputadosData.resultados.slice(cantidadAMostrar - 1); + const otrosPorcentaje = otherParties.reduce((sum, party) => sum + party.porcentaje, 0); + const otrosEntry: ResultadoTicker = { + id: `otros-diputados`, + nombre: 'Otros', + nombreCorto: 'Otros', + color: '#888888', + logoUrl: null, + votos: 0, + porcentaje: otrosPorcentaje, + }; + displayResults = [...topParties, otrosEntry]; + } else { + displayResults = diputadosData.resultados.slice(0, cantidadAMostrar); + } + + return ( +
+
+

RESUMEN DE {diputadosData.categoriaNombre}

+
+ Mesas: {formatPercent(diputadosData.estadoRecuento?.mesasTotalizadasPorcentaje || 0)} + Part: {formatPercent(diputadosData.estadoRecuento?.participacionPorcentaje || 0)} +
+
+
+ {displayResults.map(partido => ( +
+
+ +
+
+
+ {partido.nombreCorto || partido.nombre} + {formatPercent(partido.porcentaje)} +
+
+
+
+
+
+ ))} +
+
+ ); +}; \ No newline at end of file diff --git a/Elecciones-Web/frontend/src/components/ResumenGeneralWidget.tsx b/Elecciones-Web/frontend/src/components/ResumenGeneralWidget.tsx new file mode 100644 index 0000000..7fb45fd --- /dev/null +++ b/Elecciones-Web/frontend/src/components/ResumenGeneralWidget.tsx @@ -0,0 +1,117 @@ +// src/components/ResumenGeneralWidget.tsx +import { useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { getResumenProvincial, getConfiguracionPublica } from '../apiService'; +import type { CategoriaResumen, ResultadoTicker } from '../types/types'; +import { ImageWithFallback } from './ImageWithFallback'; +import './TickerWidget.css'; + +const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`; + +export const ResumenGeneralWidget = () => { + const { data: categorias, isLoading, error } = useQuery({ + queryKey: ['resumenProvincial'], + queryFn: getResumenProvincial, + refetchInterval: 30000, + }); + + const { data: configData } = useQuery({ + queryKey: ['configuracionPublica'], + queryFn: getConfiguracionPublica, + staleTime: 0, + }); + + const cantidadAMostrar = parseInt(configData?.TickerResultadosCantidad || '5', 10); + + const aggregatedData = useMemo(() => { + if (!categorias) return null; + + const legislativeCategories = categorias.filter(c => c.categoriaId === 5 || c.categoriaId === 6); + if (legislativeCategories.length === 0) return null; + + const partyMap = new Map>(); + + legislativeCategories.forEach(category => { + category.resultados.forEach(party => { + const existing = partyMap.get(party.id); + if (existing) { + existing.votos += party.votos; + } else { + // Clonamos el objeto para no modificar el original + partyMap.set(party.id, { ...party }); + } + }); + }); + + const resultsArray = Array.from(partyMap.values()); + const grandTotalVotes = resultsArray.reduce((sum, party) => sum + party.votos, 0); + + const finalResults: ResultadoTicker[] = resultsArray + .map(party => ({ + ...party, + porcentaje: grandTotalVotes > 0 ? (party.votos * 100 / grandTotalVotes) : 0, + })) + .sort((a, b) => b.votos - a.votos); + + const avgMesas = legislativeCategories.reduce((sum, cat) => sum + (cat.estadoRecuento?.mesasTotalizadasPorcentaje ?? 0), 0) / legislativeCategories.length; + const avgParticipacion = legislativeCategories.reduce((sum, cat) => sum + (cat.estadoRecuento?.participacionPorcentaje ?? 0), 0) / legislativeCategories.length; + + return { + resultados: finalResults, + estadoRecuento: { mesasTotalizadasPorcentaje: avgMesas, participacionPorcentaje: avgParticipacion } + }; + }, [categorias]); + + if (isLoading) return
Cargando resumen general...
; + if (error || !aggregatedData) return
No hay datos para el resumen general.
; + + // Lógica para "Otros" + let displayResults: ResultadoTicker[] = aggregatedData.resultados; + if (aggregatedData.resultados.length > cantidadAMostrar) { + const topParties = aggregatedData.resultados.slice(0, cantidadAMostrar - 1); + const otherParties = aggregatedData.resultados.slice(cantidadAMostrar - 1); + const otrosPorcentaje = otherParties.reduce((sum, party) => sum + party.porcentaje, 0); + const otrosEntry: ResultadoTicker = { + id: `otros-general`, + nombre: 'Otros', + nombreCorto: 'Otros', + color: '#888888', + logoUrl: null, + votos: 0, + porcentaje: otrosPorcentaje, + }; + displayResults = [...topParties, otrosEntry]; + } else { + displayResults = aggregatedData.resultados.slice(0, cantidadAMostrar); + } + + return ( +
+
+

RESUMEN LEGISLATIVO PROVINCIAL

+
+ Mesas (Prom.): {formatPercent(aggregatedData.estadoRecuento.mesasTotalizadasPorcentaje)} + Part (Prom.): {formatPercent(aggregatedData.estadoRecuento.participacionPorcentaje)} +
+
+
+ {displayResults.map(partido => ( +
+
+ +
+
+
+ {partido.nombreCorto || partido.nombre} + {formatPercent(partido.porcentaje)} +
+
+
+
+
+
+ ))} +
+
+ ); +}; \ No newline at end of file diff --git a/Elecciones-Web/frontend/src/components/SenadoresPorSeccionWidget.tsx b/Elecciones-Web/frontend/src/components/SenadoresPorSeccionWidget.tsx new file mode 100644 index 0000000..9723eab --- /dev/null +++ b/Elecciones-Web/frontend/src/components/SenadoresPorSeccionWidget.tsx @@ -0,0 +1,127 @@ +// src/components/SenadoresPorSeccionWidget.tsx +import { useState, useMemo, useEffect } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import Select from 'react-select'; +import { getSeccionesElectorales, getResultadosPorSeccion, getConfiguracionPublica } from '../apiService'; +import type { MunicipioSimple, ResultadoTicker, ApiResponseResultadosPorSeccion } from '../types/types'; +import { ImageWithFallback } from './ImageWithFallback'; +import './TickerWidget.css'; + +const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`; +const customSelectStyles = { + control: (base: any) => ({ ...base, minWidth: '220px', border: '1px solid #ced4da' }), + menu: (base: any) => ({ ...base, zIndex: 10 }), +}; + +const CATEGORIA_ID = 5; // ID para Senadores + +export const SenadoresPorSeccionWidget = () => { + const [selectedSeccion, setSelectedSeccion] = useState<{ value: string; label: string } | null>(null); + + const { data: configData } = useQuery({ + queryKey: ['configuracionPublica'], + queryFn: getConfiguracionPublica, + staleTime: 0, + }); + + const cantidadAMostrar = parseInt(configData?.ConcejalesResultadosCantidad || '5', 10); + + // Ahora usamos useQuery para obtener las secciones filtradas + const { data: secciones = [], isLoading: isLoadingSecciones } = useQuery({ + queryKey: ['seccionesElectorales', CATEGORIA_ID], // Key única para la caché + queryFn: () => getSeccionesElectorales(CATEGORIA_ID), // Pasamos el ID de la categoría + }); + + // useEffect para establecer la primera sección por defecto + useEffect(() => { + if (secciones.length > 0 && !selectedSeccion) { + // Ordenamos aquí solo para la selección inicial + const orden = new Map([ + ['Capital', 0], ['Primera', 1], ['Segunda', 2], ['Tercera', 3], + ['Cuarta', 4], ['Quinta', 5], ['Sexta', 6], ['Séptima', 7] + ]); + const getOrden = (nombre: string) => { + const match = nombre.match(/Capital|Primera|Segunda|Tercera|Cuarta|Quinta|Sexta|Séptima/); + return match ? orden.get(match[0]) ?? 99 : 99; + }; + const seccionesOrdenadas = [...secciones].sort((a, b) => getOrden(a.nombre) - getOrden(b.nombre)); + + setSelectedSeccion({ value: seccionesOrdenadas[0].id, label: seccionesOrdenadas[0].nombre }); + } + }, [secciones, selectedSeccion]); + + const seccionOptions = useMemo(() => + secciones + .map(s => ({ value: s.id, label: s.nombre })) + .sort((a, b) => { // Mantenemos el orden en el dropdown + const orden = new Map([ + ['Sección Capital', 0], ['Sección Primera', 1], ['Sección Segunda', 2], ['Sección Tercera', 3], + ['Sección Cuarta', 4], ['Sección Quinta', 5], ['Sección Sexta', 6], ['Sección Séptima', 7] + ]); + return (orden.get(a.label) ?? 99) - (orden.get(b.label) ?? 99); + }), + [secciones]); + + const { data, isLoading: isLoadingResultados } = useQuery({ + queryKey: ['resultadosPorSeccion', selectedSeccion?.value, CATEGORIA_ID], + queryFn: () => getResultadosPorSeccion(selectedSeccion!.value, CATEGORIA_ID), + enabled: !!selectedSeccion, + }); + + const resultados = data?.resultados || []; + + let displayResults: ResultadoTicker[] = resultados; + if (resultados && resultados.length > cantidadAMostrar) { + const topParties = resultados.slice(0, cantidadAMostrar - 1); + const otherParties = resultados.slice(cantidadAMostrar - 1); + const otrosPorcentaje = otherParties.reduce((sum, party) => sum + (party.porcentaje || 0), 0); + const otrosEntry: ResultadoTicker = { + id: `otros-senadores-${selectedSeccion?.value}`, + nombre: 'Otros', + nombreCorto: 'Otros', + color: '#888888', + logoUrl: null, + votos: 0, + porcentaje: otrosPorcentaje, + }; + displayResults = [...topParties, otrosEntry]; + } else if (resultados) { + displayResults = resultados.slice(0, cantidadAMostrar); + } + + return ( +
+
+

SENADORES POR SECCIÓN

+