Test Public Side
This commit is contained in:
		| @@ -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 ( | ||||
|     <> | ||||
|       <h1>Resultados Electorales - Provincia de Buenos Aires</h1> | ||||
|       <main> | ||||
|         <TickerWidget /> | ||||
|       <main className="space-y-6"> | ||||
|         <ResumenGeneralWidget /> | ||||
|         <hr className="border-gray-300" /> | ||||
|         <SenadoresTickerWidget /> | ||||
|         <hr className="border-gray-300" /> | ||||
|         <DiputadosTickerWidget /> | ||||
|         <hr className="border-gray-300" /> | ||||
|         <ConcejalesTickerWidget /> | ||||
|         <hr className="border-gray-300" /> | ||||
|         <DipSenTickerWidget /> | ||||
|         <hr className="border-gray-300" /> | ||||
|         <SenadoresPorSeccionWidget /> | ||||
|         <hr className="border-gray-300" /> | ||||
|         <DiputadosPorSeccionWidget /> | ||||
|         <hr className="border-gray-300" /> | ||||
|         <ConcejalesPorSeccionWidget /> | ||||
|         <hr className="border-gray-300" /> | ||||
|         <SenadoresWidget /> | ||||
|         <hr className="border-gray-300" /> | ||||
|         <DiputadosWidget /> | ||||
|         <hr className="border-gray-300" /> | ||||
|         <ConcejalesWidget /> | ||||
|         <hr className="border-gray-300" /> | ||||
|         <CongresoWidget /> | ||||
|         <hr className="border-gray-300" /> | ||||
|         <BancasWidget /> | ||||
|         <hr className="border-gray-300" /> | ||||
|         <MapaBsAs /> | ||||
|         <hr className="border-gray-300" /> | ||||
|         <MapaBsAsSecciones /> | ||||
|         <hr className="border-gray-300" /> | ||||
|         <TelegramaWidget /> | ||||
|       </main> | ||||
|     </> | ||||
|   | ||||
| @@ -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<Proyeccion | ||||
| /** | ||||
|  * Obtiene la lista de Secciones Electorales desde la API. | ||||
|  */ | ||||
| export const getSeccionesElectorales = async (): Promise<MunicipioSimple[]> => { | ||||
|   const response = await apiClient.get('/catalogos/secciones-electorales'); | ||||
| export const getSeccionesElectorales = async (categoriaId?: number): Promise<MunicipioSimple[]> => { | ||||
|   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<ConfiguracionPublica> = | ||||
|   return response.data; | ||||
| }; | ||||
|  | ||||
| export const getResultadosConcejales = async (seccionId: string): Promise<ResultadoTicker[]> => { | ||||
|   const response = await apiClient.get(`/resultados/concejales/${seccionId}`); | ||||
| export const getResultadosPorSeccion = async (seccionId: string, categoriaId: number): Promise<ApiResponseResultadosPorSeccion> => { | ||||
|   const response = await apiClient.get(`/resultados/seccion-resultados/${seccionId}?categoriaId=${categoriaId}`); | ||||
|   return response.data; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -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<MunicipioSimple[]>([]); | ||||
|   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<ApiResponseResultadosPorSeccion>({ | ||||
|     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 ( | ||||
|     <div className="ticker-card"> | ||||
|       <div className="ticker-header"> | ||||
|         <h3>CONCEJALES POR SECCIÓN</h3> | ||||
|         <Select | ||||
|           options={seccionOptions} | ||||
|           value={selectedSeccion} | ||||
|           onChange={(option) => setSelectedSeccion(option)} | ||||
|           isLoading={secciones.length === 0} | ||||
|           placeholder="Seleccionar sección..." | ||||
|           styles={customSelectStyles} | ||||
|         /> | ||||
|       </div> | ||||
|       <div className="ticker-results"> | ||||
|         {(isLoadingResultados && selectedSeccion) && <p>Cargando...</p>} | ||||
|         {!selectedSeccion && <p style={{textAlign: 'center', color: '#666'}}>Seleccione una sección.</p>} | ||||
|         {displayResults.map(partido => ( | ||||
|           <div key={partido.id} className="ticker-party"> | ||||
|             <div className="party-logo"> | ||||
|               <ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc="/default-avatar.png" alt={`Logo de ${partido.nombre}`} /> | ||||
|             </div> | ||||
|             <div className="party-details"> | ||||
|               <div className="party-info"> | ||||
|                 <span className="party-name">{partido.nombreCorto || partido.nombre}</span> | ||||
|                 <span className="party-percent">{formatPercent(partido.porcentaje)}</span> | ||||
|               </div> | ||||
|               <div className="party-bar-background"> | ||||
|                 <div className="party-bar-foreground" style={{ width: `${partido.porcentaje}%`, backgroundColor: partido.color || '#888' }}></div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         ))} | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,84 @@ | ||||
| // src/components/ConcejalesTickerWidget.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'; // Reutilizamos los mismos estilos | ||||
|  | ||||
| const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`; | ||||
| const CATEGORIA_ID = 7; // ID para Concejales | ||||
|  | ||||
| export const ConcejalesTickerWidget = () => { | ||||
|   const { data: categorias, isLoading, error } = useQuery<CategoriaResumen[]>({ | ||||
|     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 Concejales | ||||
|   const ConcejalesData = useMemo(() => { | ||||
|     return categorias?.find(c => c.categoriaId === CATEGORIA_ID); | ||||
|   }, [categorias]); | ||||
|  | ||||
|   if (isLoading) return <div className="ticker-card loading"><p>Cargando...</p></div>; | ||||
|   if (error || !ConcejalesData) return <div className="ticker-card error"><p>Datos de Concejales no disponibles.</p></div>; | ||||
|  | ||||
|   // Lógica para "Otros" | ||||
|   let displayResults: ResultadoTicker[] = ConcejalesData.resultados; | ||||
|   if (ConcejalesData.resultados.length > cantidadAMostrar) { | ||||
|     const topParties = ConcejalesData.resultados.slice(0, cantidadAMostrar - 1); | ||||
|     const otherParties = ConcejalesData.resultados.slice(cantidadAMostrar - 1); | ||||
|     const otrosPorcentaje = otherParties.reduce((sum, party) => sum + party.porcentaje, 0); | ||||
|     const otrosEntry: ResultadoTicker = { | ||||
|       id: `otros-Concejales`, | ||||
|       nombre: 'Otros', | ||||
|       nombreCorto: 'Otros', | ||||
|       color: '#888888', | ||||
|       logoUrl: null, | ||||
|       votos: 0, | ||||
|       porcentaje: otrosPorcentaje, | ||||
|     }; | ||||
|     displayResults = [...topParties, otrosEntry]; | ||||
|   } else { | ||||
|     displayResults = ConcejalesData.resultados.slice(0, cantidadAMostrar); | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <div className="ticker-card"> | ||||
|       <div className="ticker-header"> | ||||
|         <h3>RESUMEN DE {ConcejalesData.categoriaNombre}</h3> | ||||
|         <div className="ticker-stats"> | ||||
|           <span>Mesas: <strong>{formatPercent(ConcejalesData.estadoRecuento?.mesasTotalizadasPorcentaje || 0)}</strong></span> | ||||
|           <span>Part: <strong>{formatPercent(ConcejalesData.estadoRecuento?.participacionPorcentaje || 0)}</strong></span> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div className="ticker-results"> | ||||
|         {displayResults.map(partido => ( | ||||
|           <div key={partido.id} className="ticker-party"> | ||||
|             <div className="party-logo"> | ||||
|               <ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc="/default-avatar.png" alt={`Logo de ${partido.nombre}`} /> | ||||
|             </div> | ||||
|             <div className="party-details"> | ||||
|               <div className="party-info"> | ||||
|                 <span className="party-name">{partido.nombreCorto || partido.nombre}</span> | ||||
|                 <span className="party-percent">{formatPercent(partido.porcentaje)}</span> | ||||
|               </div> | ||||
|               <div className="party-bar-background"> | ||||
|                 <div className="party-bar-foreground" style={{ width: `${partido.porcentaje}%`, backgroundColor: partido.color || '#888' }}></div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         ))} | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										46
									
								
								Elecciones-Web/frontend/src/components/DevApp.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								Elecciones-Web/frontend/src/components/DevApp.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| // src/components/DevApp.tsx | ||||
| import { BancasWidget } from './BancasWidget' | ||||
| import { CongresoWidget } from './CongresoWidget' | ||||
| import MapaBsAs from './MapaBsAs' | ||||
| import { DipSenTickerWidget } from './DipSenTickerWidget' | ||||
| import { TelegramaWidget } from './TelegramaWidget' | ||||
| import { ConcejalesWidget } from './ConcejalesWidget' | ||||
| import MapaBsAsSecciones from './MapaBsAsSecciones' | ||||
| import { SenadoresWidget } from './SenadoresWidget' | ||||
| import { DiputadosWidget } from './DiputadosWidget' | ||||
| import { ResumenGeneralWidget } from './ResumenGeneralWidget' | ||||
| import { SenadoresTickerWidget } from './SenadoresTickerWidget' | ||||
| import { DiputadosTickerWidget } from './DiputadosTickerWidget' | ||||
| import { ConcejalesTickerWidget } from './ConcejalesTickerWidget' | ||||
| import { DiputadosPorSeccionWidget } from './DiputadosPorSeccionWidget' | ||||
| import { SenadoresPorSeccionWidget } from './SenadoresPorSeccionWidget' | ||||
| import { ConcejalesPorSeccionWidget } from './ConcejalesPorSeccionWidget' | ||||
| import '../App.css'; | ||||
|  | ||||
| export const DevApp = () => { | ||||
|   return ( | ||||
|     <> | ||||
|       <h1 style={{ textAlign: 'center', fontFamily: 'sans-serif' }}> | ||||
|         Showcase de Widgets - Elecciones 2025 | ||||
|       </h1> | ||||
|       <main>         | ||||
|         <DipSenTickerWidget /> | ||||
|         <ResumenGeneralWidget /> | ||||
|         <SenadoresWidget /> | ||||
|         <DiputadosWidget /> | ||||
|         <ConcejalesWidget /> | ||||
|         <SenadoresTickerWidget /> | ||||
|         <DiputadosTickerWidget /> | ||||
|         <ConcejalesTickerWidget /> | ||||
|         <DiputadosPorSeccionWidget /> | ||||
|         <SenadoresPorSeccionWidget /> | ||||
|         <ConcejalesPorSeccionWidget /> | ||||
|         <CongresoWidget /> | ||||
|         <BancasWidget /> | ||||
|         <MapaBsAs /> | ||||
|         <MapaBsAsSecciones /> | ||||
|         <TelegramaWidget />         | ||||
|       </main> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,4 +1,4 @@ | ||||
| // src/components/TickerWidget.tsx
 | ||||
| // src/components/DipSenTickerWidget.tsx
 | ||||
| import { useQuery } from '@tanstack/react-query'; | ||||
| import { getResumenProvincial, getConfiguracionPublica } from '../apiService'; | ||||
| import type { CategoriaResumen, ResultadoTicker } from '../types/types'; | ||||
| @@ -8,7 +8,7 @@ import { useMemo } from 'react'; | ||||
| 
 | ||||
| const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`; | ||||
| 
 | ||||
| export const TickerWidget = () => { | ||||
| export const DipSenTickerWidget = () => { | ||||
|   const { data: categorias, isLoading, error } = useQuery<CategoriaResumen[]>({ | ||||
|     queryKey: ['resumenProvincial'], | ||||
|     queryFn: getResumenProvincial, | ||||
| @@ -0,0 +1,127 @@ | ||||
| // src/components/DiputadosPorSeccionWidget.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 = 6; // ID para Diputados | ||||
|  | ||||
| export const DiputadosPorSeccionWidget = () => { | ||||
|   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<MunicipioSimple[]>({ | ||||
|     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<ApiResponseResultadosPorSeccion>({ | ||||
|     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-diputados-${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 ( | ||||
|     <div className="ticker-card"> | ||||
|       <div className="ticker-header"> | ||||
|         <h3>DIPUTADOS POR SECCIÓN</h3> | ||||
|         <Select | ||||
|           options={seccionOptions} | ||||
|           value={selectedSeccion} | ||||
|           onChange={(option) => setSelectedSeccion(option)} | ||||
|           isLoading={isLoadingSecciones} | ||||
|           placeholder="Seleccionar sección..." | ||||
|           styles={customSelectStyles} | ||||
|         /> | ||||
|       </div> | ||||
|       <div className="ticker-results"> | ||||
|         {(isLoadingResultados && selectedSeccion) && <p>Cargando...</p>} | ||||
|         {!selectedSeccion && !isLoadingSecciones && <p style={{textAlign: 'center', color: '#666'}}>Seleccione una sección.</p>} | ||||
|         {displayResults.map(partido => ( | ||||
|           <div key={partido.id} className="ticker-party"> | ||||
|             <div className="party-logo"> | ||||
|               <ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc="/default-avatar.png" alt={`Logo de ${partido.nombre}`} /> | ||||
|             </div> | ||||
|             <div className="party-details"> | ||||
|               <div className="party-info"> | ||||
|                 <span className="party-name">{partido.nombreCorto || partido.nombre}</span> | ||||
|                 <span className="party-percent">{formatPercent(partido.porcentaje)}</span> | ||||
|               </div> | ||||
|               <div className="party-bar-background"> | ||||
|                 <div className="party-bar-foreground" style={{ width: `${partido.porcentaje}%`, backgroundColor: partido.color || '#888' }}></div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         ))} | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @@ -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<CategoriaResumen[]>({ | ||||
|     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 <div className="ticker-card loading"><p>Cargando...</p></div>; | ||||
|   if (error || !diputadosData) return <div className="ticker-card error"><p>Datos de Diputados no disponibles.</p></div>; | ||||
|  | ||||
|   // 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 ( | ||||
|     <div className="ticker-card"> | ||||
|       <div className="ticker-header"> | ||||
|         <h3>RESUMEN DE {diputadosData.categoriaNombre}</h3> | ||||
|         <div className="ticker-stats"> | ||||
|           <span>Mesas: <strong>{formatPercent(diputadosData.estadoRecuento?.mesasTotalizadasPorcentaje || 0)}</strong></span> | ||||
|           <span>Part: <strong>{formatPercent(diputadosData.estadoRecuento?.participacionPorcentaje || 0)}</strong></span> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div className="ticker-results"> | ||||
|         {displayResults.map(partido => ( | ||||
|           <div key={partido.id} className="ticker-party"> | ||||
|             <div className="party-logo"> | ||||
|               <ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc="/default-avatar.png" alt={`Logo de ${partido.nombre}`} /> | ||||
|             </div> | ||||
|             <div className="party-details"> | ||||
|               <div className="party-info"> | ||||
|                 <span className="party-name">{partido.nombreCorto || partido.nombre}</span> | ||||
|                 <span className="party-percent">{formatPercent(partido.porcentaje)}</span> | ||||
|               </div> | ||||
|               <div className="party-bar-background"> | ||||
|                 <div className="party-bar-foreground" style={{ width: `${partido.porcentaje}%`, backgroundColor: partido.color || '#888' }}></div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         ))} | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										117
									
								
								Elecciones-Web/frontend/src/components/ResumenGeneralWidget.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								Elecciones-Web/frontend/src/components/ResumenGeneralWidget.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -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<CategoriaResumen[]>({ | ||||
|     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<string, Omit<ResultadoTicker, 'porcentaje'>>(); | ||||
|      | ||||
|     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 <div className="ticker-card loading" style={{ gridColumn: '1 / -1' }}>Cargando resumen general...</div>; | ||||
|   if (error || !aggregatedData) return <div className="ticker-card error" style={{ gridColumn: '1 / -1' }}>No hay datos para el resumen general.</div>; | ||||
|  | ||||
|   // 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 ( | ||||
|     <div className="ticker-card" style={{ gridColumn: '1 / -1' }}> | ||||
|       <div className="ticker-header"> | ||||
|         <h3>RESUMEN LEGISLATIVO PROVINCIAL</h3> | ||||
|         <div className="ticker-stats"> | ||||
|           <span>Mesas (Prom.): <strong>{formatPercent(aggregatedData.estadoRecuento.mesasTotalizadasPorcentaje)}</strong></span> | ||||
|           <span>Part (Prom.): <strong>{formatPercent(aggregatedData.estadoRecuento.participacionPorcentaje)}</strong></span> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div className="ticker-results"> | ||||
|         {displayResults.map(partido => ( | ||||
|           <div key={partido.id} className="ticker-party"> | ||||
|             <div className="party-logo"> | ||||
|               <ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc="/default-avatar.png" alt={`Logo de ${partido.nombre}`} /> | ||||
|             </div> | ||||
|             <div className="party-details"> | ||||
|               <div className="party-info"> | ||||
|                 <span className="party-name">{partido.nombreCorto || partido.nombre}</span> | ||||
|                 <span className="party-percent">{formatPercent(partido.porcentaje)}</span> | ||||
|               </div> | ||||
|               <div className="party-bar-background"> | ||||
|                 <div className="party-bar-foreground" style={{ width: `${partido.porcentaje}%`, backgroundColor: partido.color || '#888' }}></div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         ))} | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @@ -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<MunicipioSimple[]>({ | ||||
|     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<ApiResponseResultadosPorSeccion>({ | ||||
|     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 ( | ||||
|     <div className="ticker-card"> | ||||
|       <div className="ticker-header"> | ||||
|         <h3>SENADORES POR SECCIÓN</h3> | ||||
|         <Select | ||||
|           options={seccionOptions} | ||||
|           value={selectedSeccion} | ||||
|           onChange={(option) => setSelectedSeccion(option)} | ||||
|           isLoading={isLoadingSecciones} | ||||
|           placeholder="Seleccionar sección..." | ||||
|           styles={customSelectStyles} | ||||
|         /> | ||||
|       </div> | ||||
|       <div className="ticker-results"> | ||||
|         {(isLoadingResultados && selectedSeccion) && <p>Cargando...</p>} | ||||
|         {!selectedSeccion && !isLoadingSecciones && <p style={{textAlign: 'center', color: '#666'}}>Seleccione una sección.</p>} | ||||
|         {displayResults.map(partido => ( | ||||
|           <div key={partido.id} className="ticker-party"> | ||||
|             <div className="party-logo"> | ||||
|               <ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc="/default-avatar.png" alt={`Logo de ${partido.nombre}`} /> | ||||
|             </div> | ||||
|             <div className="party-details"> | ||||
|               <div className="party-info"> | ||||
|                 <span className="party-name">{partido.nombreCorto || partido.nombre}</span> | ||||
|                 <span className="party-percent">{formatPercent(partido.porcentaje)}</span> | ||||
|               </div> | ||||
|               <div className="party-bar-background"> | ||||
|                 <div className="party-bar-foreground" style={{ width: `${partido.porcentaje}%`, backgroundColor: partido.color || '#888' }}></div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         ))} | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,84 @@ | ||||
| // src/components/SenadoresTickerWidget.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'; // Reutilizamos los mismos estilos | ||||
|  | ||||
| const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`; | ||||
| const CATEGORIA_ID = 5; // ID para Senadores | ||||
|  | ||||
| export const SenadoresTickerWidget = () => { | ||||
|   const { data: categorias, isLoading, error } = useQuery<CategoriaResumen[]>({ | ||||
|     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 Senadores | ||||
|   const senadoresData = useMemo(() => { | ||||
|     return categorias?.find(c => c.categoriaId === CATEGORIA_ID); | ||||
|   }, [categorias]); | ||||
|  | ||||
|   if (isLoading) return <div className="ticker-card loading"><p>Cargando...</p></div>; | ||||
|   if (error || !senadoresData) return <div className="ticker-card error"><p>Datos de Senadores no disponibles.</p></div>; | ||||
|  | ||||
|   // Lógica para "Otros" | ||||
|   let displayResults: ResultadoTicker[] = senadoresData.resultados; | ||||
|   if (senadoresData.resultados.length > cantidadAMostrar) { | ||||
|     const topParties = senadoresData.resultados.slice(0, cantidadAMostrar - 1); | ||||
|     const otherParties = senadoresData.resultados.slice(cantidadAMostrar - 1); | ||||
|     const otrosPorcentaje = otherParties.reduce((sum, party) => sum + party.porcentaje, 0); | ||||
|     const otrosEntry: ResultadoTicker = { | ||||
|       id: `otros-senadores`, | ||||
|       nombre: 'Otros', | ||||
|       nombreCorto: 'Otros', | ||||
|       color: '#888888', | ||||
|       logoUrl: null, | ||||
|       votos: 0, | ||||
|       porcentaje: otrosPorcentaje, | ||||
|     }; | ||||
|     displayResults = [...topParties, otrosEntry]; | ||||
|   } else { | ||||
|     displayResults = senadoresData.resultados.slice(0, cantidadAMostrar); | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <div className="ticker-card"> | ||||
|       <div className="ticker-header"> | ||||
|         <h3>RESUMEN DE {senadoresData.categoriaNombre}</h3> | ||||
|         <div className="ticker-stats"> | ||||
|           <span>Mesas: <strong>{formatPercent(senadoresData.estadoRecuento?.mesasTotalizadasPorcentaje || 0)}</strong></span> | ||||
|           <span>Part: <strong>{formatPercent(senadoresData.estadoRecuento?.participacionPorcentaje || 0)}</strong></span> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div className="ticker-results"> | ||||
|         {displayResults.map(partido => ( | ||||
|           <div key={partido.id} className="ticker-party"> | ||||
|             <div className="party-logo"> | ||||
|               <ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc="/default-avatar.png" alt={`Logo de ${partido.nombre}`} /> | ||||
|             </div> | ||||
|             <div className="party-details"> | ||||
|               <div className="party-info"> | ||||
|                 <span className="party-name">{partido.nombreCorto || partido.nombre}</span> | ||||
|                 <span className="party-percent">{formatPercent(partido.porcentaje)}</span> | ||||
|               </div> | ||||
|               <div className="party-bar-background"> | ||||
|                 <div className="party-bar-foreground" style={{ width: `${partido.porcentaje}%`, backgroundColor: partido.color || '#888' }}></div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         ))} | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @@ -35,10 +35,10 @@ export const SenadoresWidget = () => { | ||||
|     queryFn: () => getMunicipios(CATEGORIA_ID), // Pasamos el ID de la categoría | ||||
|   }); | ||||
|  | ||||
|   // useEffect para establecer "LA PLATA" por defecto | ||||
|   // useEffect para establecer "ALBERTI" por defecto | ||||
|   useEffect(() => { | ||||
|     if (municipios.length > 0 && !selectedMunicipio) { | ||||
|       const laPlata = municipios.find(m => m.nombre.toUpperCase() === 'LA PLATA'); | ||||
|       const laPlata = municipios.find(m => m.nombre.toUpperCase() === 'ALBERTI'); | ||||
|       if (laPlata) { | ||||
|         setSelectedMunicipio({ value: laPlata.id, label: laPlata.nombre }); | ||||
|       } | ||||
|   | ||||
| @@ -1,17 +1,83 @@ | ||||
| // src/main.tsx | ||||
| import React from 'react' | ||||
| import ReactDOM from 'react-dom/client' | ||||
| import { QueryClient, QueryClientProvider } from '@tanstack/react-query' | ||||
| import App from './App.tsx' | ||||
| import './index.css' | ||||
| import React from 'react'; | ||||
| import ReactDOM from 'react-dom/client'; | ||||
| import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; | ||||
|  | ||||
| // Crear un cliente de React Query | ||||
| const queryClient = new QueryClient() | ||||
| import { BancasWidget } from './components/BancasWidget' | ||||
| import { CongresoWidget } from './components/CongresoWidget' | ||||
| import MapaBsAs from './components/MapaBsAs' | ||||
| 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' | ||||
|  | ||||
| ReactDOM.createRoot(document.getElementById('root')!).render( | ||||
|   <React.StrictMode> | ||||
|     <QueryClientProvider client={queryClient}> | ||||
|       <App /> | ||||
|     </QueryClientProvider> | ||||
|   </React.StrictMode>, | ||||
| ) | ||||
| import './index.css'; | ||||
| import { DevApp } from './components/DevApp'; | ||||
|  | ||||
| const queryClient = new QueryClient(); | ||||
|  | ||||
| // Mapeamos el nombre del widget (del atributo data) al componente de React | ||||
| const WIDGET_MAP: Record<string, React.ElementType> = { | ||||
|     'resumen-senadores': SenadoresWidget, | ||||
|     'resumen-diputados': DiputadosWidget, | ||||
|     'resumen-concejales': ConcejalesWidget, | ||||
|     'congreso-provincial': CongresoWidget, | ||||
|     'distribucion-bancas': BancasWidget, | ||||
|     'mapa-municipios': MapaBsAs, | ||||
|     'mapa-secciones': MapaBsAsSecciones, | ||||
|     'consulta-telegramas': TelegramaWidget, | ||||
|     'ticker-senadores': SenadoresTickerWidget, | ||||
|     'ticker-diputados': DiputadosTickerWidget, | ||||
|     'ticker-concejales': ConcejalesTickerWidget, | ||||
|     'ticker-dip-sen': DipSenTickerWidget, | ||||
|     'resumen-general': ResumenGeneralWidget, | ||||
|     'diputados-por-seccion': DiputadosPorSeccionWidget, | ||||
|     'senadores-por-seccion': SenadoresPorSeccionWidget, | ||||
|     'concejales-por-seccion': ConcejalesPorSeccionWidget, | ||||
| }; | ||||
|  | ||||
| // Vite establece `import.meta.env.DEV` a `true` cuando ejecutamos 'npm run dev' | ||||
| if (import.meta.env.DEV) { | ||||
|     // --- MODO DESARROLLO --- | ||||
|     // Renderizamos nuestra página de showcase en el div#root | ||||
|     ReactDOM.createRoot(document.getElementById('root')!).render( | ||||
|         <React.StrictMode> | ||||
|             <QueryClientProvider client={queryClient}> | ||||
|                 <DevApp /> | ||||
|             </QueryClientProvider> | ||||
|         </React.StrictMode> | ||||
|     ); | ||||
| } else { | ||||
|     // --- MODO PRODUCCIÓN --- | ||||
|     // Exponemos la función de renderizado para el bootstrap.js | ||||
|     const renderWidgets = () => { | ||||
|         const widgetContainers = document.querySelectorAll('[data-elecciones-widget]'); | ||||
|         widgetContainers.forEach(container => { | ||||
|             const widgetName = (container as HTMLElement).dataset.eleccionesWidget; | ||||
|             if (widgetName && WIDGET_MAP[widgetName]) { | ||||
|                 const WidgetComponent = WIDGET_MAP[widgetName]; | ||||
|                 const root = ReactDOM.createRoot(container); | ||||
|                 root.render( | ||||
|                     <React.StrictMode> | ||||
|                         <QueryClientProvider client={queryClient}> | ||||
|                             <WidgetComponent /> | ||||
|                         </QueryClientProvider> | ||||
|                     </React.StrictMode> | ||||
|                 ); | ||||
|             } | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     (window as any).EleccionesWidgets = { | ||||
|         render: renderWidgets | ||||
|     }; | ||||
| } | ||||
| @@ -105,4 +105,9 @@ export interface TelegramaData { | ||||
| export interface CatalogoItem { | ||||
|   id: string; | ||||
|   nombre: string; | ||||
| } | ||||
|  | ||||
| export interface ApiResponseResultadosPorSeccion { | ||||
|     ultimaActualizacion: string; | ||||
|     resultados: ResultadoTicker[]; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user