Feat Prototipos Widgets y Fix Worker Telegramas
This commit is contained in:
		| @@ -1,11 +1,14 @@ | ||||
| /* src/components/BancasWidget.css */ | ||||
| .bancas-widget-container { | ||||
|     background-color: #2a2a2e; | ||||
|     /* Mismo estilo de tarjeta que el Ticker */ | ||||
|     background-color: #ffffff; | ||||
|     border: 1px solid #e0e0e0; | ||||
|     box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); | ||||
|     padding: 15px 20px; | ||||
|     border-radius: 8px; | ||||
|     max-width: 800px; | ||||
|     margin: 20px auto; | ||||
|     color: #e0e0e0; | ||||
|     color: #333333; | ||||
| } | ||||
|  | ||||
| .bancas-header { | ||||
| @@ -17,19 +20,22 @@ | ||||
|  | ||||
| .bancas-header h4 { | ||||
|     margin: 0; | ||||
|     color: white; | ||||
|     color: #212529; | ||||
|     font-size: 1.2em; | ||||
|     font-weight: 700; | ||||
| } | ||||
|  | ||||
| .bancas-header select { | ||||
|     background-color: #3a3a3a; | ||||
|     color: white; | ||||
|     border: 1px solid #555; | ||||
|     background-color: #ffffff; | ||||
|     color: #333333; | ||||
|     border: 1px solid #ced4da; /* Borde estándar para inputs */ | ||||
|     border-radius: 4px; | ||||
|     padding: 5px 10px; | ||||
|     font-family: inherit; | ||||
|     font-size: 0.9em; | ||||
| } | ||||
|  | ||||
| .waffle-chart-container { | ||||
|     height: 300px; | ||||
|     font-family: system-ui, sans-serif; | ||||
|     font-family: "Public Sans", system-ui, sans-serif; | ||||
| } | ||||
| @@ -1,89 +1,125 @@ | ||||
| // src/components/BancasWidget.tsx | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { ResponsiveWaffle } from '@nivo/waffle'; | ||||
| import { getBancasPorSeccion } from '../apiService'; | ||||
| import type { ProyeccionBancas } from '../types/types'; | ||||
| // Se cambia la importación: de ResponsiveWaffle a ResponsiveBar | ||||
| import { ResponsiveBar } from '@nivo/bar'; | ||||
| import { getBancasPorSeccion, getSeccionesElectorales } from '../apiService'; | ||||
| import type { ProyeccionBancas, MunicipioSimple } from '../types/types'; | ||||
| import './BancasWidget.css'; | ||||
|  | ||||
| // Paleta de colores consistente | ||||
| // La paleta de colores se mantiene para consistencia visual | ||||
| const NIVO_COLORS = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"]; | ||||
|  | ||||
| // Las Secciones Electorales de la Provincia (esto podría venir de la API en el futuro) | ||||
| const secciones = [ | ||||
|     { id: '1', nombre: 'Primera Sección' }, | ||||
|     { id: '2', nombre: 'Segunda Sección' }, | ||||
|     { id: '3', nombre: 'Tercera Sección' }, | ||||
|     { id: '4', nombre: 'Cuarta Sección' }, | ||||
|     { id: '5', nombre: 'Quinta Sección' }, | ||||
|     { id: '6', nombre: 'Sexta Sección' }, | ||||
|     { id: '7', nombre: 'Séptima Sección' }, | ||||
|     { id: '8', nombre: 'Octava Sección (Capital)' }, | ||||
| ]; | ||||
|  | ||||
| export const BancasWidget = () => { | ||||
|     const [seccionActual, setSeccionActual] = useState('1'); // Empezamos con la Primera Sección | ||||
|     const [secciones, setSecciones] = useState<MunicipioSimple[]>([]); | ||||
|     const [seccionActual, setSeccionActual] = useState<string>(''); | ||||
|     const [data, setData] = useState<ProyeccionBancas | null>(null); | ||||
|     const [loading, setLoading] = useState(true); | ||||
|     const [error, setError] = useState<string | null>(null); | ||||
|  | ||||
|     // useEffect para cargar la lista de secciones una sola vez | ||||
|     useEffect(() => { | ||||
|         const fetchSecciones = async () => { | ||||
|             try { | ||||
|                 const seccionesData = await getSeccionesElectorales(); | ||||
|                 if (seccionesData && seccionesData.length > 0) { | ||||
|                      | ||||
|                     // --- INICIO DE LA LÓGICA DE ORDENAMIENTO --- | ||||
|                     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)); | ||||
|                     // --- FIN DE LA LÓGICA DE ORDENAMIENTO --- | ||||
|  | ||||
|                     setSecciones(seccionesData); | ||||
|                     setSeccionActual(seccionesData[0].id); | ||||
|                 } else { | ||||
|                     setError("No se encontraron secciones electorales."); | ||||
|                 } | ||||
|             } catch (err) { | ||||
|                 console.error("Error cargando secciones electorales:", err); | ||||
|                 setError("No se pudo cargar la lista de secciones."); | ||||
|             } | ||||
|         }; | ||||
|         fetchSecciones(); | ||||
|     }, []); | ||||
|  | ||||
|     // useEffect para cargar los datos de bancas cuando cambia la sección | ||||
|     useEffect(() => { | ||||
|         if (!seccionActual) return; | ||||
|  | ||||
|         const fetchData = async () => { | ||||
|             setLoading(true); | ||||
|             setError(null); | ||||
|             try { | ||||
|                 const result = await getBancasPorSeccion(seccionActual); | ||||
|                 setData(result); | ||||
|             } catch (error) { | ||||
|                 console.error(`Error cargando datos de bancas para sección ${seccionActual}:`, error); | ||||
|                 setData(null); // Limpiar datos en caso de error | ||||
|             } catch (err) { | ||||
|                 console.error(`Error cargando datos de bancas para sección ${seccionActual}:`, err); | ||||
|                 setData(null); | ||||
|                 setError("No hay datos de bancas disponibles para esta sección."); | ||||
|             } finally { | ||||
|                 setLoading(false); | ||||
|             } | ||||
|         }; | ||||
|         fetchData(); | ||||
|     }, [seccionActual]); // Se ejecuta cada vez que cambia la sección | ||||
|     }, [seccionActual]); | ||||
|  | ||||
|     const waffleData = data?.proyeccion.map(p => ({ | ||||
|         id: p.agrupacionNombre, | ||||
|         label: p.agrupacionNombre, | ||||
|         value: p.bancas, | ||||
|     })) || []; | ||||
|  | ||||
|     const totalBancas = waffleData.reduce((sum, current) => sum + current.value, 0); | ||||
|     // Se preparan los datos para el gráfico de barras. | ||||
|     // Se invierte el array para que el partido con más bancas aparezca arriba. | ||||
|     const barData = data ? [...data.proyeccion].reverse() : []; | ||||
|  | ||||
|     return ( | ||||
|         <div className="bancas-widget-container"> | ||||
|             <div className="bancas-header"> | ||||
|                 <h4>Distribución de Bancas</h4> | ||||
|                 <select value={seccionActual} onChange={e => setSeccionActual(e.target.value)}> | ||||
|                 <select value={seccionActual} onChange={e => setSeccionActual(e.target.value)} disabled={secciones.length === 0}> | ||||
|                     {secciones.map(s => <option key={s.id} value={s.id}>{s.nombre}</option>)} | ||||
|                 </select> | ||||
|             </div> | ||||
|             <div className="waffle-chart-container"> | ||||
|                 {loading ? <p>Cargando...</p> : !data ? <p>No hay datos disponibles para esta sección.</p> : | ||||
|                 <ResponsiveWaffle | ||||
|                     data={waffleData} | ||||
|                     total={totalBancas} | ||||
|                     rows={8} | ||||
|                     columns={10} | ||||
|                     fillDirection="bottom" | ||||
|                     padding={3} | ||||
|                     colors={NIVO_COLORS} | ||||
|                     borderColor={{ from: 'color', modifiers: [['darker', 0.3]] }} | ||||
|                 {loading ? <p>Cargando...</p> : error ? <p>{error}</p> : | ||||
|                 // --- SE REEMPLAZA EL GRÁFICO WAFFLE POR EL GRÁFICO DE BARRAS --- | ||||
|                 <ResponsiveBar | ||||
|                     data={barData} | ||||
|                     keys={['bancas']} | ||||
|                     indexBy="agrupacionNombre" | ||||
|                     layout="horizontal" | ||||
|                     margin={{ top: 10, right: 30, bottom: 25, left: 160 }} | ||||
|                     padding={0.3} | ||||
|                     valueScale={{ type: 'linear' }} | ||||
|                     indexScale={{ type: 'band', round: true }} | ||||
|                     colors={({ index }) => NIVO_COLORS[index % NIVO_COLORS.length]} | ||||
|                     borderColor={{ from: 'color', modifiers: [['darker', 1.6]] }} | ||||
|                     axisTop={null} | ||||
|                     axisRight={null} | ||||
|                     axisBottom={{ | ||||
|                         tickSize: 5, | ||||
|                         tickPadding: 5, | ||||
|                         tickRotation: 0, | ||||
|                         legend: 'Cantidad de Bancas', | ||||
|                         legendPosition: 'middle', | ||||
|                         legendOffset: 20, | ||||
|                         // Asegura que los ticks del eje sean números enteros | ||||
|                         format: (value) => Math.floor(value) === value ? value : '' | ||||
|                     }} | ||||
|                     axisLeft={{ | ||||
|                         tickSize: 5, | ||||
|                         tickPadding: 5, | ||||
|                         tickRotation: 0, | ||||
|                     }} | ||||
|                     labelSkipWidth={12} | ||||
|                     labelSkipHeight={12} | ||||
|                     labelTextColor={{ from: 'color', modifiers: [['darker', 3]] }} | ||||
|                     animate={true} | ||||
|                     legends={[ | ||||
|                         { | ||||
|                             anchor: 'bottom', | ||||
|                             direction: 'row', | ||||
|                             justify: false, | ||||
|                             translateX: 0, | ||||
|                             translateY: 40, | ||||
|                             itemsSpacing: 4, | ||||
|                             itemWidth: 100, | ||||
|                             itemHeight: 20, | ||||
|                             itemTextColor: '#999', | ||||
|                             itemDirection: 'left-to-right', | ||||
|                             symbolSize: 20, | ||||
|                         }, | ||||
|                     ]} | ||||
|                     // Se elimina la leyenda, ya que las etiquetas en el eje son suficientes | ||||
|                     legends={[]} | ||||
|                 />} | ||||
|             </div> | ||||
|         </div> | ||||
|   | ||||
							
								
								
									
										148
									
								
								Elecciones-Web/frontend/src/components/CongresoWidget.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								Elecciones-Web/frontend/src/components/CongresoWidget.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,148 @@ | ||||
| /* src/components/CongresoWidget.css */ | ||||
| .congreso-container { | ||||
|   display: flex; | ||||
|   /* Se reduce ligeramente el espacio entre el gráfico y el panel */ | ||||
|   gap: 1rem; | ||||
|   background-color: #ffffff; | ||||
|   border: 1px solid #e0e0e0; | ||||
|   box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); | ||||
|   padding: 1rem; | ||||
|   border-radius: 8px; | ||||
|   max-width: 800px; | ||||
|   margin: 20px auto; | ||||
|   font-family: "Public Sans", system-ui, sans-serif; | ||||
|   color: #333333; | ||||
|   align-items: center; | ||||
| } | ||||
|  | ||||
| .congreso-grafico { | ||||
|   /* --- CAMBIO PRINCIPAL: Se aumenta la proporción del gráfico --- */ | ||||
|   flex: 1 1 65%; | ||||
|   min-width: 300px; | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
| } | ||||
|  | ||||
| .congreso-grafico svg { | ||||
|   width: 100%; | ||||
|   height: auto; | ||||
|   animation: fadeIn 0.8s ease-in-out; | ||||
| } | ||||
|  | ||||
| @keyframes fadeIn { | ||||
|   from { opacity: 0; transform: scale(0.9); } | ||||
|   to { opacity: 1; transform: scale(1); } | ||||
| } | ||||
|  | ||||
| .congreso-summary { | ||||
|   /* --- CAMBIO PRINCIPAL: Se reduce la proporción del panel de datos --- */ | ||||
|   flex: 1 1 35%; | ||||
|   border-left: 1px solid #e0e0e0; | ||||
|   /* Se reduce el padding para dar aún más espacio al gráfico */ | ||||
|   padding-left: 1rem; | ||||
| } | ||||
|  | ||||
| .congreso-summary h3 { | ||||
|   margin-top: 0; | ||||
|   font-size: 1.4em; | ||||
|   color: #212529; | ||||
| } | ||||
|  | ||||
| .chamber-tabs { | ||||
|   display: flex; | ||||
|   margin-bottom: 1.5rem; | ||||
|   border: 1px solid #dee2e6; | ||||
|   border-radius: 6px; | ||||
|   overflow: hidden; | ||||
| } | ||||
|  | ||||
| .chamber-tabs button { | ||||
|   flex: 1; | ||||
|   padding: 0.75rem 0.5rem; | ||||
|   border: none; | ||||
|   background-color: #f8f9fa; | ||||
|   color: #6c757d; | ||||
|   font-family: inherit; | ||||
|   font-size: 1em; | ||||
|   font-weight: 500; | ||||
|   cursor: pointer; | ||||
|   transition: all 0.2s ease-in-out; | ||||
| } | ||||
|  | ||||
| .chamber-tabs button:first-child { | ||||
|   border-right: 1px solid #dee2e6; | ||||
| } | ||||
|  | ||||
| .chamber-tabs button:hover { | ||||
|   background-color: #e9ecef; | ||||
| } | ||||
|  | ||||
| .chamber-tabs button.active { | ||||
|   background-color: var(--primary-accent-color); | ||||
|   color: #ffffff; | ||||
| } | ||||
|  | ||||
| .summary-metric { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   align-items: baseline; | ||||
|   margin-bottom: 0.5rem; | ||||
|   font-size: 1.1em; | ||||
| } | ||||
|  | ||||
| .summary-metric strong { | ||||
|   font-size: 1.5em; | ||||
|   font-weight: 700; | ||||
|   color: var(--primary-accent-color); | ||||
| } | ||||
|  | ||||
| .congreso-summary hr { | ||||
|   border: none; | ||||
|   border-top: 1px solid #e0e0e0; | ||||
|   margin: 1.5rem 0; | ||||
| } | ||||
|  | ||||
| .partido-lista { | ||||
|   list-style: none; | ||||
|   padding: 0; | ||||
|   margin: 0; | ||||
| } | ||||
|  | ||||
| .partido-lista li { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   margin-bottom: 0.75rem; | ||||
| } | ||||
|  | ||||
| .partido-color-box { | ||||
|   width: 14px; | ||||
|   height: 14px; | ||||
|   border-radius: 3px; | ||||
|   margin-right: 10px; | ||||
|   flex-shrink: 0; | ||||
| } | ||||
|  | ||||
| .partido-nombre { | ||||
|   flex-grow: 1; | ||||
| } | ||||
|  | ||||
| .partido-bancas { | ||||
|   font-weight: 700; | ||||
|   font-size: 1.1em; | ||||
| } | ||||
|  | ||||
| /* --- Media Query para Responsividad Móvil --- */ | ||||
| @media (max-width: 768px) { | ||||
|   .congreso-container { | ||||
|     flex-direction: column; | ||||
|     padding: 1.5rem; | ||||
|   } | ||||
|   .congreso-summary { | ||||
|     border-left: none; | ||||
|     padding-left: 0; | ||||
|     margin-top: 2rem; | ||||
|     border-top: 1px solid #e0e0e0; | ||||
|     padding-top: 1.5rem; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										125
									
								
								Elecciones-Web/frontend/src/components/CongresoWidget.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								Elecciones-Web/frontend/src/components/CongresoWidget.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,125 @@ | ||||
| // src/components/CongresoWidget.tsx | ||||
| import { useState, useEffect } from 'react'; | ||||
| // 1. Importar ambos layouts | ||||
| import { ParliamentLayout } from './ParliamentLayout'; | ||||
| import { SenateLayout } from './SenateLayout';  | ||||
| import './CongresoWidget.css'; | ||||
|  | ||||
| // ... (Interfaces sin cambios) | ||||
| type CamaraType = 'diputados' | 'senadores'; | ||||
|  | ||||
| interface PartidoData { | ||||
|   id: string; | ||||
|   nombre: string; | ||||
|   bancasTotales: number; | ||||
|   bancasEnJuego: number; | ||||
|   color: string; | ||||
| } | ||||
|  | ||||
| interface CamaraData { | ||||
|   camaraNombre: string; | ||||
|   totalBancas: number; | ||||
|   bancasEnJuego: number; | ||||
|   partidos: PartidoData[]; | ||||
| } | ||||
|  | ||||
| interface ComposicionData { | ||||
|   diputados: CamaraData; | ||||
|   senadores: CamaraData; | ||||
| } | ||||
|  | ||||
|  | ||||
| export const CongresoWidget = () => { | ||||
|   const [data, setData] = useState<ComposicionData | null>(null); | ||||
|   const [camaraActiva, setCamaraActiva] = useState<CamaraType>('diputados'); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     // ... (fetchData sin cambios) | ||||
|     const fetchData = async () => { | ||||
|       try { | ||||
|         const response = await fetch('http://localhost:5217/api/resultados/composicion-congreso'); | ||||
|         if (!response.ok) throw new Error('La respuesta de la red no fue exitosa'); | ||||
|         const result: ComposicionData = await response.json(); | ||||
|         setData(result); | ||||
|       } catch (err) { | ||||
|         console.error("Error cargando datos de composición:", err); | ||||
|         setError("No se pudieron cargar los datos de composición del congreso."); | ||||
|       } finally { | ||||
|         setLoading(false); | ||||
|       } | ||||
|     }; | ||||
|     fetchData(); | ||||
|   }, []); | ||||
|  | ||||
|   if (loading) return <div className="congreso-container loading">Cargando...</div>; | ||||
|   if (error) return <div className="congreso-container error">{error}</div>; | ||||
|    | ||||
|   const datosCamaraActual = data ? data[camaraActiva] : null; | ||||
|  | ||||
|   if (!datosCamaraActual) { | ||||
|     return <div className="congreso-container error">Datos no disponibles para la cámara seleccionada.</div>; | ||||
|   } | ||||
|    | ||||
|   const partidosOrdenados = datosCamaraActual.partidos; | ||||
|    | ||||
|   const seatFillData = partidosOrdenados.flatMap(party => { | ||||
|     const retainedSeats = party.bancasTotales - party.bancasEnJuego; | ||||
|     const inPlaySeats = party.bancasEnJuego; | ||||
|     return [ | ||||
|       ...Array(retainedSeats).fill({ color: party.color, isEnJuego: false }), | ||||
|       ...Array(inPlaySeats).fill({ color: party.color, isEnJuego: true }), | ||||
|     ]; | ||||
|   }); | ||||
|    | ||||
|   return ( | ||||
|     <div className="congreso-container"> | ||||
|       <div className="congreso-grafico"> | ||||
|         {/* --- INICIO DE LA MODIFICACIÓN: RENDERIZADO CONDICIONAL --- */} | ||||
|         {camaraActiva === 'diputados' ? ( | ||||
|           <ParliamentLayout seatData={seatFillData} /> | ||||
|         ) : ( | ||||
|           <SenateLayout seatData={seatFillData} /> | ||||
|         )} | ||||
|         {/* --- FIN DE LA MODIFICACIÓN --- */} | ||||
|       </div> | ||||
|       <div className="congreso-summary"> | ||||
|         <div className="chamber-tabs"> | ||||
|             <button  | ||||
|                 className={camaraActiva === 'diputados' ? 'active' : ''}  | ||||
|                 onClick={() => setCamaraActiva('diputados')} | ||||
|             > | ||||
|                 Diputados | ||||
|             </button> | ||||
|             <button  | ||||
|                 className={camaraActiva === 'senadores' ? 'active' : ''}  | ||||
|                 onClick={() => setCamaraActiva('senadores')} | ||||
|             > | ||||
|                 Senadores | ||||
|             </button> | ||||
|         </div> | ||||
|          | ||||
|         <h3>{datosCamaraActual.camaraNombre}</h3> | ||||
|         <div className="summary-metric"> | ||||
|           <span>Total de Bancas</span> | ||||
|           <strong>{datosCamaraActual.totalBancas}</strong> | ||||
|         </div> | ||||
|         <div className="summary-metric"> | ||||
|           <span>Bancas en juego</span> | ||||
|           <strong>{datosCamaraActual.bancasEnJuego}</strong> | ||||
|         </div> | ||||
|         <hr /> | ||||
|         <ul className="partido-lista"> | ||||
|           {partidosOrdenados.map(partido => ( | ||||
|             <li key={partido.id}> | ||||
|               <span className="partido-color-box" style={{ backgroundColor: partido.color }}></span> | ||||
|               <span className="partido-nombre">{partido.nombre}</span> | ||||
|               <strong className="partido-bancas">{partido.bancasTotales}</strong> | ||||
|             </li> | ||||
|           ))} | ||||
|         </ul> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,25 +1,27 @@ | ||||
| /* src/components/MapaBsAs.css */ | ||||
| :root { | ||||
|   --primary-accent-color: #FF5722; | ||||
|   --background-panel-color: #2f2f2f; | ||||
|   --border-color: #444; | ||||
|   --text-color: #f0f0f0; | ||||
|   --text-color-muted: #aaa; | ||||
|   --progress-bar-background: #4a4a4a; | ||||
|   --scrollbar-thumb-color: #666; | ||||
|   --scrollbar-track-color: #333; | ||||
|   --map-background-color: #242424; /* Color de fondo del mapa */ | ||||
|   --primary-accent-color: #0073e6; | ||||
|   --background-panel-color: #ffffff; | ||||
|   --border-color: #dee2e6; | ||||
|   --text-color: #212529; | ||||
|   --text-color-muted: #6c757d; | ||||
|   --progress-bar-background: #e9ecef; | ||||
|   --scrollbar-thumb-color: #ced4da; | ||||
|   --scrollbar-track-color: #f1f1f1; | ||||
|   --map-background-color: #f8f9fa; | ||||
| } | ||||
|  | ||||
| .mapa-wrapper { | ||||
|   display: flex; | ||||
|   gap: 1.5rem; | ||||
|   background-color: var(--map-background-color); | ||||
|   background-color: #ffffff; | ||||
|   border: 1px solid var(--border-color); | ||||
|   box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); | ||||
|   padding: 1rem; | ||||
|   border-radius: 12px; | ||||
|   max-width: 1600px; /* Incrementado para pantallas más grandes */ | ||||
|   border-radius: 8px; | ||||
|   max-width: 1600px; | ||||
|   margin: auto; | ||||
|   height: 88vh; /* Ligeramente más alto */ | ||||
|   height: 88vh; | ||||
|   min-height: 650px; | ||||
| } | ||||
|  | ||||
| @@ -30,7 +32,6 @@ | ||||
|   border-radius: 8px; | ||||
|   position: relative; | ||||
|   overflow: hidden; | ||||
|   /* CORRECCIÓN: Se añade el color de fondo para eliminar el marco blanco */ | ||||
|   background-color: var(--map-background-color); | ||||
| } | ||||
|  | ||||
| @@ -43,46 +44,47 @@ | ||||
| } | ||||
|  | ||||
| .rsm-geography { | ||||
|   transition: opacity 0.3s ease-in-out, transform 0.2s ease-in-out, filter 0.2s ease-in-out, fill 0.3s ease; | ||||
|   transition: opacity 0.3s ease, transform 0.2s ease, filter 0.2s ease, fill 0.3s ease; | ||||
|   cursor: pointer; | ||||
|   stroke-width: 1px; | ||||
|   stroke: #b0b0b0; | ||||
|   stroke-width: 0.5px; | ||||
| } | ||||
|  | ||||
| .rsm-geography:hover { | ||||
|   filter: drop-shadow(0px 0px 6px rgba(255, 255, 255, 0.6)); | ||||
|   transform: translateY(-1px); | ||||
|   stroke: var(--primary-accent-color); | ||||
|   stroke-width: 1.5px; | ||||
|   filter: brightness(1.05); | ||||
| } | ||||
|  | ||||
| .rsm-geography.selected { | ||||
|   fill: var(--primary-accent-color); /* Rellena el partido seleccionado con el color principal */ | ||||
|   stroke: #ffffff; /* Añade un borde blanco para un mejor contraste */ | ||||
|   stroke-width: 2px; /* Un grosor de borde definido */ | ||||
|   filter: none; /* Elimina el efecto de sombra/resplandor */ | ||||
|   outline: none; /* Previene el recuadro de enfoque del navegador */ | ||||
|   pointer-events: none; /* Mantenemos esto para evitar interacciones no deseadas */ | ||||
|   stroke: #333; | ||||
|   stroke-width: 2px; | ||||
|   filter: none; | ||||
|   pointer-events: none; | ||||
| } | ||||
|  | ||||
| .rsm-geography.faded { | ||||
|   opacity: 0.15; | ||||
|   opacity: 0.25; | ||||
|   pointer-events: none; | ||||
| } | ||||
|  | ||||
| .info-panel { | ||||
|   flex: 1; | ||||
|   height: 100%; | ||||
|   background-color: var(--background-panel-color); | ||||
|   border-radius: 8px; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   overflow-y: auto; | ||||
|   min-height: 0; | ||||
|   background-color: var(--background-panel-color); | ||||
|   border-radius: 8px; | ||||
|   padding: 1.5rem; | ||||
|   border: none; | ||||
| } | ||||
|  | ||||
| .info-panel::-webkit-scrollbar { width: 8px; } | ||||
| .info-panel::-webkit-scrollbar-track { background: var(--scrollbar-track-color); border-radius: 4px; } | ||||
| .info-panel::-webkit-scrollbar-thumb { background-color: var(--scrollbar-thumb-color); border-radius: 4px; border: 2px solid var(--scrollbar-track-color); } | ||||
| .info-panel::-webkit-scrollbar-thumb:hover { background-color: #888; } | ||||
| .info-panel::-webkit-scrollbar-thumb:hover { background-color: #adb5bd; } | ||||
|  | ||||
| .info-panel h3 { margin-top: 0; color: var(--primary-accent-color); border-bottom: 2px solid var(--border-color); padding-bottom: 0.5rem; } | ||||
| .info-panel p { color: var(--text-color-muted); } | ||||
|  | ||||
| @@ -91,14 +93,17 @@ | ||||
| } | ||||
| .reset-button-panel:hover { background-color: var(--primary-accent-color); color: white; } | ||||
|  | ||||
| .detalle-placeholder { text-align: center; margin: auto; } | ||||
| .detalle-loading, .detalle-error { text-align: center; margin: auto; color: var(--text-color-muted); } | ||||
| .detalle-placeholder {  | ||||
|   text-align: center;  | ||||
|   margin: auto 0;  | ||||
| } | ||||
| .detalle-loading, .detalle-error { text-align: center; margin: auto 0; color: var(--text-color-muted); } | ||||
| .detalle-metricas { display: flex; justify-content: space-between; font-size: 0.9em; padding-bottom: 1rem; border-bottom: 1px solid var(--border-color); margin-bottom: 1rem; } | ||||
| .resultados-lista { list-style: none; padding: 0; margin: 0; } | ||||
| .resultados-lista li { margin-bottom: 1rem; } | ||||
| .resultado-info { display: flex; justify-content: space-between; margin-bottom: 0.25rem; font-size: 0.9em; } | ||||
| .partido-nombre { font-weight: 500; } | ||||
| .partido-votos { font-weight: 300; color: var(--text-color-muted); } | ||||
| .partido-votos { font-weight: 400; color: var(--text-color-muted); } | ||||
| .progress-bar { height: 8px; background-color: var(--progress-bar-background); border-radius: 4px; overflow: hidden; } | ||||
| .progress-fill { height: 100%; background-color: var(--primary-accent-color); border-radius: 4px; transition: width 0.5s ease-out; } | ||||
|  | ||||
| @@ -112,14 +117,81 @@ | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   gap: 5px; | ||||
|   z-index: 10; /* <-- AÑADIDO: Esta línea asegura que los controles estén por encima del mapa. */ | ||||
|   z-index: 10; | ||||
| } | ||||
| .map-controls button { | ||||
|   width: 32px; height: 32px; font-size: 1.2rem; font-weight: bold; background-color: rgba(0, 0, 0, 0.7); color: white; border: 1px solid var(--border-color); border-radius: 4px; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; justify-content: center; padding: 0; line-height: 1; | ||||
| } | ||||
| .map-controls button:hover { background-color: rgba(0, 0, 0, 0.9); border-color: var(--primary-accent-color); } | ||||
|  | ||||
| .legend { margin-top: auto; padding-top: 1rem; border-top: 1px solid var(--border-color); } | ||||
| .legend h4 { margin-top: 0; } | ||||
| /* --- ESTILOS PARA EL BOTÓN DE "VOLVER" (VISTA DESKTOP) --- */ | ||||
| .map-controls button { | ||||
|   /* Se elimina el ancho y alto fijos para que el botón se ajuste al texto. */ | ||||
|   width: auto; | ||||
|   height: auto; | ||||
|   /* Se define un padding para dar espacio interno al texto. */ | ||||
|   padding: 0.5rem 1rem; | ||||
|   /* Se ajusta el tamaño de fuente para el texto. */ | ||||
|   font-size: 0.9em; | ||||
|   font-weight: bold; | ||||
|   background-color: #ffffff; | ||||
|   color: #333; | ||||
|   border: 1px solid var(--border-color); | ||||
|   box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); | ||||
|   border-radius: 4px; | ||||
|   cursor: pointer; | ||||
|   transition: all 0.2s; | ||||
|   /* Se eliminan las propiedades de centrado de íconos que ya no son necesarias. */ | ||||
| } | ||||
| .map-controls button:hover { | ||||
|   background-color: #f8f9fa; | ||||
|   border-color: var(--primary-accent-color); | ||||
|   color: var(--primary-accent-color); | ||||
| } | ||||
|  | ||||
| .legend { | ||||
|   padding-top: 1rem; | ||||
|   border-top: 1px solid var(--border-color); | ||||
| } | ||||
| .legend h4 { | ||||
|   margin-top: 0; | ||||
| } | ||||
| .legend-item { display: flex; align-items: center; margin-bottom: 0.5rem; font-size: 0.85em; } | ||||
| .legend-color-box { width: 16px; height: 16px; margin-right: 8px; border-radius: 3px; } | ||||
| .legend-color-box { width: 16px; height: 16px; margin-right: 8px; border-radius: 3px; border: 1px solid #ccc; } | ||||
|  | ||||
| /* --- ESTILOS PARA RESPONSIVIDAD MÓVIL --- */ | ||||
| @media (max-width: 992px) { | ||||
|   .mapa-wrapper { | ||||
|     flex-direction: column; | ||||
|     height: auto; | ||||
|     min-height: 100vh; | ||||
|     padding: 0.5rem; | ||||
|   } | ||||
|  | ||||
|   .mapa-container { | ||||
|     flex-basis: auto; | ||||
|     width: 100%; | ||||
|     height: 50vh; | ||||
|     min-height: 400px; | ||||
|   } | ||||
|  | ||||
|   .info-panel { | ||||
|     flex-basis: auto; | ||||
|     width: 100%; | ||||
|     height: auto; | ||||
|     overflow-y: visible; | ||||
|     box-sizing: border-box; | ||||
|   } | ||||
|  | ||||
|   .map-controls { | ||||
|     top: 10px; | ||||
|     right: 10px; | ||||
|   } | ||||
|  | ||||
|   /* --- ESTILOS PARA EL BOTÓN DE "VOLVER" (VISTA MÓVIL) --- */ | ||||
|   .map-controls button { | ||||
|     /* Se elimina el ancho y alto fijos para que el botón se ajuste al texto. */ | ||||
|     width: auto; | ||||
|     height: auto; | ||||
|     /* Se ajusta el padding para que sea un buen objetivo táctil (tappable). */ | ||||
|     padding: 0.6rem 1.2rem; | ||||
|     /* Un tamaño de fuente legible en móviles. */ | ||||
|     font-size: 1em; | ||||
|   } | ||||
| } | ||||
| @@ -81,7 +81,8 @@ const MapaBsAs = () => { | ||||
|         coloresMap.set(agrupacion.id, COLORES_BASE[index % COLORES_BASE.length]); | ||||
|       }); | ||||
|     } | ||||
|     coloresMap.set('default', '#D6D6DA'); | ||||
|     // Se cambia el color por defecto a uno más apropiado para fondo claro | ||||
|     coloresMap.set('default', '#E0E0E0'); | ||||
|     if (resultadosData) { | ||||
|       resultadosData.forEach(r => resultadosMap.set(r.departamentoNombre.toUpperCase(), r)); | ||||
|     } | ||||
| @@ -135,11 +136,6 @@ const MapaBsAs = () => { | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleZoomOut = () => { | ||||
|     // Al presionar el botón de zoom out, siempre se vuelve al estado inicial. | ||||
|     handleReset(); | ||||
|   }; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const handleKeyDown = (e: KeyboardEvent) => e.key === 'Escape' && handleReset(); | ||||
|     window.addEventListener('keydown', handleKeyDown); | ||||
| @@ -164,7 +160,8 @@ const MapaBsAs = () => { | ||||
|   return ( | ||||
|     <div className="mapa-wrapper"> | ||||
|       <div className="mapa-container"> | ||||
|         <ComposableMap projection="geoMercator" projectionConfig={{ scale: 4700, center: [-60.5, -37.2] }} className="rsm-svg" style={{ backgroundColor: "#242424" }}> | ||||
|         {/* Se elimina el 'style' con el backgroundColor para que lo controle el CSS */} | ||||
|         <ComposableMap projection="geoMercator" projectionConfig={{ scale: 4400, center: [-60.5, -37.2] }} className="rsm-svg"> | ||||
|           <ZoomableGroup | ||||
|             center={position.center} | ||||
|             zoom={position.zoom} | ||||
| @@ -177,7 +174,7 @@ const MapaBsAs = () => { | ||||
|               // Detectamos si la rueda se mueve hacia atrás (zoom out) | ||||
|               if (e.deltaY > 0) { | ||||
|                 handleReset(); | ||||
|               }else if (e.deltaY < 0) { | ||||
|               } else if (e.deltaY < 0) { | ||||
|                 handleZoomIn(); | ||||
|               } | ||||
|               return true; | ||||
| @@ -201,7 +198,7 @@ const MapaBsAs = () => { | ||||
|                         data-tooltip-content={`${geo.properties.departamento}: ${nombreAgrupacionGanadora}`} | ||||
|                         className={`rsm-geography ${isSelected ? 'selected' : ''} ${isFaded ? 'faded' : ''}`} | ||||
|                         fill={getPartyFillColor(geo.properties.departamento)} | ||||
|                         stroke="#FFF" | ||||
|                         // Se elimina la prop 'stroke' para que la controle el CSS | ||||
|                         onClick={() => handleGeographyClick(geo)} | ||||
|                         onMouseEnter={handleMouseEnter} | ||||
|                       /> | ||||
| @@ -212,8 +209,9 @@ const MapaBsAs = () => { | ||||
|             )} | ||||
|           </ZoomableGroup> | ||||
|         </ComposableMap> | ||||
|         <Tooltip id="partido-tooltip" /> | ||||
|         <ControlesMapa onZoomIn={handleZoomIn} onZoomOut={handleZoomOut} onReset={handleReset} /> | ||||
|         {/* Se añade una variante 'light' al Tooltip para que combine mejor */} | ||||
|         <Tooltip id="partido-tooltip" variant="light" /> | ||||
|         {selectedAmbitoId !== null && <ControlesMapa onReset={handleReset} />} | ||||
|       </div> | ||||
|       <div className="info-panel"> | ||||
|         <DetalleMunicipio ambitoId={selectedAmbitoId} onReset={handleReset} /> | ||||
| @@ -223,12 +221,10 @@ const MapaBsAs = () => { | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| // --- Sub-componentes (sin cambios) --- | ||||
| const ControlesMapa = ({ onZoomIn, onZoomOut, onReset }: { onZoomIn: () => void; onZoomOut: () => void; onReset: () => void }) => ( | ||||
| // --- Sub-componentes --- | ||||
| const ControlesMapa = ({ onReset }: { onReset: () => void }) => ( | ||||
|   <div className="map-controls"> | ||||
|     <button onClick={onZoomIn}>+</button> | ||||
|     <button onClick={onZoomOut}>-</button> | ||||
|     <button onClick={onReset}>⌖</button> | ||||
|     <button onClick={onReset}>← VOLVER</button> | ||||
|   </div> | ||||
| ); | ||||
|  | ||||
| @@ -245,13 +241,25 @@ const DetalleMunicipio = ({ ambitoId, onReset }: { ambitoId: number | null; onRe | ||||
|  | ||||
|   return ( | ||||
|     <div className="detalle-content"> | ||||
|       <button className="reset-button-panel" onClick={onReset}>← Ver Provincia</button> | ||||
|       <button className="reset-button-panel" onClick={onReset}>← VOLVER</button> | ||||
|       <h3>{data?.municipioNombre}</h3> | ||||
|       <div className="detalle-metricas"> | ||||
|         <span><strong>Escrutado:</strong> {data?.porcentajeEscrutado.toFixed(2)}%</span> | ||||
|         <span><strong>Participación:</strong> {data?.porcentajeParticipacion.toFixed(2)}%</span> | ||||
|       </div> | ||||
|       <ul className="resultados-lista">{data?.resultados.map(r => (<li key={r.nombre}><div className="resultado-info"><span className="partido-nombre">{r.nombre}</span><span className="partido-votos">{r.votos.toLocaleString('es-AR')} ({r.porcentaje.toFixed(2)}%)</span></div><div className="progress-bar"><div className="progress-fill" style={{ width: `${r.porcentaje}%` }}></div></div></li>))}</ul> | ||||
|       <ul className="resultados-lista"> | ||||
|         {data?.resultados.map((r, index) => ( | ||||
|           <li key={`${r.nombre}-${index}`}> | ||||
|             <div className="resultado-info"> | ||||
|               <span className="partido-nombre">{r.nombre}</span> | ||||
|               <span className="partido-votos">{r.votos.toLocaleString('es-AR')} ({r.porcentaje.toFixed(2)}%)</span> | ||||
|             </div> | ||||
|             <div className="progress-bar"> | ||||
|               <div className="progress-fill" style={{ width: `${r.porcentaje}%` }}></div> | ||||
|             </div> | ||||
|           </li> | ||||
|         ))} | ||||
|       </ul> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
							
								
								
									
										156
									
								
								Elecciones-Web/frontend/src/components/ParliamentLayout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								Elecciones-Web/frontend/src/components/ParliamentLayout.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,156 @@ | ||||
| // src/components/ParliamentLayout.tsx | ||||
| import React from 'react'; | ||||
|  | ||||
| // Interfaces (sin cambios) | ||||
| interface SeatFillData { | ||||
|   color: string; | ||||
|   isEnJuego: boolean; | ||||
| } | ||||
|  | ||||
| interface ParliamentLayoutProps { | ||||
|   seatData: SeatFillData[]; | ||||
|   size?: number; | ||||
| } | ||||
|  | ||||
| export const ParliamentLayout: React.FC<ParliamentLayoutProps> = ({ | ||||
|   seatData, | ||||
|   size = 400, | ||||
| }) => { | ||||
|   const uniqueColors = [...new Set(seatData.map(d => d.color))]; | ||||
|  | ||||
|   // La plantilla de círculos estáticos | ||||
|   const seatElements = [ | ||||
|     <circle key="seat-0" id="seat-0" r="9.964" cy="160.283" cx="-255.056" transform="matrix(-1, 0, 0, 1, 0, 0)"/>, | ||||
|     <circle key="seat-1" id="seat-1" r="9.964" cy="114.844" cx="-252.766" transform="matrix(-1, 0, 0, 1, 0, 0)"/>, | ||||
|     <circle key="seat-2" id="seat-2" r="9.964" cy="68.323" cx="-249.04" transform="matrix(-1, 0, 0, 1, 0, 0)"/>, | ||||
|     <circle key="seat-3" id="seat-3" r="9.964" cy="167.585" cx="-230.037" transform="matrix(-1, 0, 0, 1, 0, 0)"/>, | ||||
|     <circle key="seat-4" id="seat-4" r="9.964" cy="121.335" cx="-226.656" transform="matrix(-1, 0, 0, 1, 0, 0)"/>, | ||||
|     <circle key="seat-5" id="seat-5" r="9.964" cy="74.003" cx="-224.493" transform="matrix(-1, 0, 0, 1, 0, 0)"/>, | ||||
|     <circle key="seat-6" id="seat-6" r="9.964" cy="22.948" cx="-223.55" transform="matrix(-1, 0, 0, 1, 0, 0)"/>, | ||||
|     <circle key="seat-7" id="seat-7" r="9.964" cy="183.347" cx="-208.602" transform="matrix(-1, 0, 0, 1, 0, 0)"/>, | ||||
|     <circle key="seat-8" id="seat-8" r="9.964" cy="132.154" cx="-203.666" transform="matrix(-1, 0, 0, 1, 0, 0)"/>, | ||||
|     <circle key="seat-9" id="seat-9" r="9.964" cy="82.117" cx="-199.609" transform="matrix(-1, 0, 0, 1, 0, 0)"/>, | ||||
|     <circle key="seat-10" id="seat-10" r="9.964" cy="30.215" cx="-197.157" transform="matrix(-1, 0, 0, 1, 0, 0)"/>, | ||||
|     <circle key="seat-11" id="seat-11" r="9.964" cy="145.407" cx="-182.84" transform="matrix(-1, 0, 0, 1, 0, 0)"/>, | ||||
|     <circle key="seat-12" id="seat-12" r="9.964" cy="93.477" cx="-175.808" transform="matrix(-1, 0, 0, 1, 0, 0)"/>, | ||||
|     <circle key="seat-13" id="seat-13" r="9.964" cy="39.969" cx="-171.147" transform="matrix(-1, 0, 0, 1, 0, 0)"/>, | ||||
|     <circle key="seat-14" id="seat-14" r="9.964" cy="163.733" cx="-163.582" transform="matrix(-1, 0, 0, 1, 0, 0)"/>, | ||||
|     <circle key="seat-15" id="seat-15" r="9.964" cy="108.082" cx="-153.63" transform="matrix(-1, 0, 0, 1, 0, 0)"/>, | ||||
|     <circle key="seat-16" id="seat-16" r="9.964" cy="51.554" cx="-147.409" transform="matrix(-1, 0, 0, 1, 0, 0)"/>, | ||||
|     <circle key="seat-17" id="seat-17" r="9.964" cy="125.122" cx="-134.967" transform="matrix(-1, 0, 0, 1, 0, 0)"/>, | ||||
|     <circle key="seat-18" id="seat-18" r="9.964" cy="66.206" cx="-124.894" transform="matrix(-1, 0, 0, 1, 0, 0)"/>, | ||||
|     <circle key="seat-19" id="seat-19" r="9.964" cy="143.999" cx="-118.288" transform="matrix(-1, 0, 0, 1, 0, 0)"/>, | ||||
|     <circle key="seat-20" id="seat-20" r="9.964" cy="82.48" cx="-104.338" transform="matrix(-1, 0, 0, 1, 0, 0)"/>, | ||||
|     <circle key="seat-21" id="seat-21" r="9.964" cy="102.267" cx="-85.471" transform="matrix(-1, 0, 0, 1, 0, 0)"/>, | ||||
|     <circle key="seat-22" id="seat-22" r="9.964" cy="123.093" cx="-70.302" transform="matrix(-1, 0, 0, 1, 0, 0)"/>, | ||||
|     <circle key="seat-23" id="seat-23" r="9.964" cy="280.15" cx="51.596" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-24" id="seat-24" r="9.964" cy="238.303" cx="63.378" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-25" id="seat-25" r="9.964" cy="187.919" cx="77.562" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-26" id="seat-26" r="9.964" cy="137.535" cx="91.746" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-27" id="seat-27" r="9.964" cy="291.519" cx="75.175" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-28" id="seat-28" r="9.964" cy="246.564" cx="88.133" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-29" id="seat-29" r="9.964" cy="197.635" cx="103.42" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-30" id="seat-30" r="9.964" cy="145.84" cx="118.145" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-31" id="seat-31" r="9.964" cy="307.801" cx="95.891" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-32" id="seat-32" r="9.964" cy="259.444" cx="110.504" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-33" id="seat-33" r="9.964" cy="209.251" cx="125.789" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-34" id="seat-34" r="9.964" cy="157.248" cx="142.356" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-35" id="seat-35" r="9.964" cy="274.606" cx="129.053" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-36" id="seat-36" r="9.964" cy="223.156" cx="147.029" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-37" id="seat-37" r="9.964" cy="169.647" cx="164.36" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-38" id="seat-38" r="9.964" cy="290.475" cx="148.465" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-39" id="seat-39" r="9.964" cy="238.165" cx="165.39" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-40" id="seat-40" r="9.964" cy="184.139" cx="184.653" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-41" id="seat-41" r="9.964" cy="253.417" cx="184.047" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-42" id="seat-42" r="9.964" cy="199.221" cx="204.812" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-43" id="seat-43" r="9.964" cy="268.306" cx="202.26" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-44" id="seat-44" r="9.964" cy="214.247" cx="223.62" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-45" id="seat-45" r="9.964" cy="308.5" cx="-275.898" transform="matrix(-1, 0, 0, 1, 0, 0)"/>, | ||||
|     <circle key="seat-46" id="seat-46" r="9.964" cy="160.283" cx="296.74"/>, | ||||
|     <circle key="seat-47" id="seat-47" r="9.964" cy="114.844" cx="299.029"/>, | ||||
|     <circle key="seat-48" id="seat-48" r="9.964" cy="68.323" cx="302.756"/>, | ||||
|     <circle key="seat-49" id="seat-49" r="9.964" cy="167.585" cx="321.759"/>, | ||||
|     <circle key="seat-50" id="seat-50" r="9.964" cy="121.335" cx="325.14"/>, | ||||
|     <circle key="seat-51" id="seat-51" r="9.964" cy="74.003" cx="327.303"/>, | ||||
|     <circle key="seat-52" id="seat-52" r="9.964" cy="22.948" cx="328.246"/>, | ||||
|     <circle key="seat-53" id="seat-53" r="9.964" cy="183.347" cx="343.194"/>, | ||||
|     <circle key="seat-54" id="seat-54" r="9.964" cy="132.154" cx="348.129"/>, | ||||
|     <circle key="seat-55" id="seat-55" r="9.964" cy="82.117" cx="352.187"/>, | ||||
|     <circle key="seat-56" id="seat-56" r="9.964" cy="30.215" cx="354.639"/>, | ||||
|     <circle key="seat-57" id="seat-57" r="9.964" cy="145.407" cx="368.956"/>, | ||||
|     <circle key="seat-58" id="seat-58" r="9.964" cy="93.477" cx="375.988"/>, | ||||
|     <circle key="seat-59" id="seat-59" r="9.964" cy="39.969" cx="380.649"/>, | ||||
|     <circle key="seat-60" id="seat-60" r="9.964" cy="163.733" cx="388.214"/>, | ||||
|     <circle key="seat-61" id="seat-61" r="9.964" cy="108.082" cx="398.166"/>, | ||||
|     <circle key="seat-62" id="seat-62" r="9.964" cy="51.554" cx="404.387"/>, | ||||
|     <circle key="seat-63" id="seat-63" r="9.964" cy="125.122" cx="416.829"/>, | ||||
|     <circle key="seat-64" id="seat-64" r="9.964" cy="66.206" cx="426.902"/>, | ||||
|     <circle key="seat-65" id="seat-65" r="9.964" cy="143.999" cx="433.508"/>, | ||||
|     <circle key="seat-66" id="seat-66" r="9.964" cy="82.48" cx="447.457"/>, | ||||
|     <circle key="seat-67" id="seat-67" r="9.964" cy="102.267" cx="466.325"/>, | ||||
|     <circle key="seat-68" id="seat-68" r="9.964" cy="123.093" cx="481.494"/>, | ||||
|     <circle key="seat-69" id="seat-69" r="9.964" cy="-147.065" cx="400.833" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-70" id="seat-70" r="9.964" cy="-188.912" cx="412.614" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-71" id="seat-71" r="9.964" cy="-239.296" cx="426.798" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-72" id="seat-72" r="9.964" cy="-289.68" cx="440.983" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-73" id="seat-73" r="9.964" cy="-135.696" cx="424.411" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-74" id="seat-74" r="9.964" cy="-180.651" cx="437.369" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-75" id="seat-75" r="9.964" cy="-229.58" cx="452.656" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-76" id="seat-76" r="9.964" cy="-281.375" cx="467.381" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-77" id="seat-77" r="9.964" cy="-119.414" cx="445.127" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-78" id="seat-78" r="9.964" cy="-167.771" cx="459.741" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-79" id="seat-79" r="9.964" cy="-217.964" cx="475.026" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-80" id="seat-80" r="9.964" cy="-269.967" cx="491.592" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-81" id="seat-81" r="9.964" cy="-152.609" cx="478.289" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-82" id="seat-82" r="9.964" cy="-204.059" cx="496.265" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-83" id="seat-83" r="9.964" cy="-257.568" cx="513.597" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-84" id="seat-84" r="9.964" cy="-136.74" cx="497.701" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-85" id="seat-85" r="9.964" cy="-189.049" cx="514.627" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-86" id="seat-86" r="9.964" cy="-243.076" cx="533.889" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-87" id="seat-87" r="9.964" cy="-173.798" cx="533.284" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-88" id="seat-88" r="9.964" cy="-227.994" cx="554.048" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-89" id="seat-89" r="9.964" cy="-158.909" cx="551.496" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-90" id="seat-90" r="9.964" cy="-212.968" cx="572.857" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>, | ||||
|     <circle key="seat-91" id="seat-91" r="9.964" cy="-197.942" cx="591.665" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>, | ||||
|   ]; | ||||
|  | ||||
|   const renderedElements = seatElements.map((child, index) => { | ||||
|     // Si no hay datos para este asiento (ej. en la cámara de senadores que tiene menos de 92 bancas) | ||||
|     // lo pintamos de gris. | ||||
|     if (index >= seatData.length) { | ||||
|       return React.cloneElement(child, { | ||||
|         fill: '#E0E0E0', | ||||
|         stroke: '#ffffff', | ||||
|         strokeWidth: 1.5, | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     const seat = seatData[index]; | ||||
|     const patternId = `stripes-${seat.color.replace('#', '')}`; | ||||
|  | ||||
|     return React.cloneElement(child, { | ||||
|       fill: seat.isEnJuego ? `url(#${patternId})` : seat.color, | ||||
|       stroke: '#ffffff', | ||||
|       strokeWidth: 1.5, | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   return ( | ||||
|     <svg viewBox="0 0 550 375" width={size} height={size * (375 / 550)} style={{ display: 'block', margin: 'auto' }}> | ||||
|       <defs> | ||||
|         {uniqueColors.map(color => { | ||||
|           const patternId = `stripes-${color.replace('#', '')}`; | ||||
|           return ( | ||||
|             <pattern key={patternId} id={patternId} patternUnits="userSpaceOnUse" width="4" height="4" patternTransform="rotate(45)"> | ||||
|               <rect width="4" height="4" fill={color}></rect> | ||||
|               <path d="M-1,1 l2,-2 M0,4 l4,-4 M3,5 l2,-2" stroke="rgba(255,255,255,0.7)" strokeWidth="3"></path> | ||||
|             </pattern> | ||||
|           ); | ||||
|         })} | ||||
|       </defs> | ||||
|       <g transform="matrix(1, 0, 0, 1, 1.166021, 0.583011)"> | ||||
|         {renderedElements} | ||||
|       </g> | ||||
|     </svg> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										108
									
								
								Elecciones-Web/frontend/src/components/SenateLayout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								Elecciones-Web/frontend/src/components/SenateLayout.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| // src/components/SenateLayout.tsx | ||||
| import React from 'react'; | ||||
|  | ||||
| // Las interfaces son las mismas que para el otro layout | ||||
| interface SeatFillData { | ||||
|   color: string; | ||||
|   isEnJuego: boolean; | ||||
| } | ||||
|  | ||||
| interface SenateLayoutProps { | ||||
|   seatData: SeatFillData[]; | ||||
|   size?: number; | ||||
| } | ||||
|  | ||||
| export const SenateLayout: React.FC<SenateLayoutProps> = ({ | ||||
|   seatData, | ||||
|   size = 400, | ||||
| }) => { | ||||
|   const uniqueColors = [...new Set(seatData.map(d => d.color).filter(Boolean))]; | ||||
|  | ||||
|   // Plantilla estática de los 46 asientos del Senado, ordenados para el llenado por columnas. | ||||
|   const seatElements = [ | ||||
|     <circle key="senate-seat-0" id="senate-seat-0" r="8" cy="354.956" cx="-121.933" transform="matrix(-1, 0, 0, 1, 0, 0)" />, | ||||
|     <circle key="senate-seat-1" id="senate-seat-1" r="8" cy="331.318" cx="-121.933" transform="matrix(-1, 0, 0, 1, 0, 0)" />, | ||||
|     <circle key="senate-seat-2" id="senate-seat-2" r="8" cy="307.598" cx="-121.933" transform="matrix(-1, 0, 0, 1, 0, 0)" />, | ||||
|     <circle key="senate-seat-3" id="senate-seat-3" r="8" cy="285.358" cx="-121.933" transform="matrix(-1, 0, 0, 1, 0, 0)" />, | ||||
|     <circle key="senate-seat-4" id="senate-seat-4" r="8" cy="261.241" cx="-121.933" transform="matrix(-1, 0, 0, 1, 0, 0)" />, | ||||
|     <circle key="senate-seat-5" id="senate-seat-5" r="8" cy="354.956" cx="-164.933" transform="matrix(-1, 0, 0, 1, 0, 0)" />, | ||||
|     <circle key="senate-seat-6" id="senate-seat-6" r="8" cy="333.318" cx="-164.933" transform="matrix(-1, 0, 0, 1, 0, 0)" />, | ||||
|     <circle key="senate-seat-7" id="senate-seat-7" r="8" cy="312.598" cx="-164.933" transform="matrix(-1, 0, 0, 1, 0, 0)" />, | ||||
|     <circle key="senate-seat-8" id="senate-seat-8" r="8" cy="292.358" cx="-164.933" transform="matrix(-1, 0, 0, 1, 0, 0)" />, | ||||
|     <circle key="senate-seat-9" id="senate-seat-9" r="8" cy="269.241" cx="-165.933" transform="matrix(-1, 0, 0, 1, 0, 0)" />, | ||||
|     <circle key="senate-seat-10" id="senate-seat-10" r="8" cy="246.521" cx="-170.309" transform="matrix(-1, 0, 0, 1, 0, 0)" />, | ||||
|     <circle key="senate-seat-11" id="senate-seat-11" r="8" cy="229.407" cx="-183.766" transform="matrix(-1, 0, 0, 1, 0, 0)" />, | ||||
|     <circle key="senate-seat-12" id="senate-seat-12" r="8" cy="217.294" cx="-201.175" transform="matrix(-1, 0, 0, 1, 0, 0)" />, | ||||
|     <circle key="senate-seat-13" id="senate-seat-13" r="8" cy="268.057" cx="-208.672" transform="matrix(-1, 0, 0, 1, 0, 0)" />, | ||||
|     <circle key="senate-seat-14" id="senate-seat-14" r="8" cy="292.358" cx="-206.672" transform="matrix(-1, 0, 0, 1, 0, 0)" />, | ||||
|     <circle key="senate-seat-15" id="senate-seat-15" r="8" cy="211.882" cx="-223.172" transform="matrix(-1, 0, 0, 1, 0, 0)" />, | ||||
|     <circle key="senate-seat-16" id="senate-seat-16" r="8" cy="252.557" cx="-227.172" transform="matrix(-1, 0, 0, 1, 0, 0)" />, | ||||
|     <circle key="senate-seat-17" id="senate-seat-17" r="8" cy="347.619" cx="243.841" />, | ||||
|     <circle key="senate-seat-18" id="senate-seat-18" r="8" cy="252.557" cx="258.729" />, | ||||
|     <circle key="senate-seat-19" id="senate-seat-19" r="8" cy="211.882" cx="262.728" />, | ||||
|     <circle key="senate-seat-20" id="senate-seat-20" r="8" cy="268.057" cx="277.228" />, | ||||
|     <circle key="senate-seat-21" id="senate-seat-21" r="8" cy="292.358" cx="279.228" />, | ||||
|     <circle key="senate-seat-22" id="senate-seat-22" r="8" cy="217.294" cx="284.726" />, | ||||
|     <circle key="senate-seat-23" id="senate-seat-23" r="8" cy="159.989" cx="295.143" transform="matrix(0.986454, -0.16404, 0.16404, 0.986454, -54.66, 61.555)" />, | ||||
|     <circle key="senate-seat-24" id="senate-seat-24" r="8" cy="229.407" cx="302.134" />, | ||||
|     <circle key="senate-seat-25" id="senate-seat-25" r="8" cy="170.401" cx="311.791" transform="matrix(0.986454, -0.16404, 0.16404, 0.986454, -49.543, 55.556)" />, | ||||
|     <circle key="senate-seat-26" id="senate-seat-26" r="8" cy="246.521" cx="315.591" />, | ||||
|     <circle key="senate-seat-27" id="senate-seat-27" r="8" cy="269.241" cx="319.968" />, | ||||
|     <circle key="senate-seat-28" id="senate-seat-28" r="8" cy="292.358" cx="320.968" />, | ||||
|     <circle key="senate-seat-29" id="senate-seat-29" r="8" cy="312.598" cx="320.968" />, | ||||
|     <circle key="senate-seat-30" id="senate-seat-30" r="8" cy="333.318" cx="320.968" />, | ||||
|     <circle key="senate-seat-31" id="senate-seat-31" r="8" cy="354.956" cx="320.968" />, | ||||
|     <circle key="senate-seat-32" id="senate-seat-32" r="8" cy="184.514" cx="327.2" transform="matrix(0.986454, -0.16404, 0.16404, 0.986454, -42.892, 52.164)" />, | ||||
|     <circle key="senate-seat-33" id="senate-seat-33" r="8" cy="184.514" cx="327.2" transform="matrix(0.986454, -0.16404, 0.16404, 0.986454, -21.946, 65.569)" />, | ||||
|     <circle key="senate-seat-34" id="senate-seat-34" r="8" cy="184.514" cx="327.2" transform="matrix(0.986454, -0.16404, 0.16404, 0.986454, -2.353, 84.436)" />, | ||||
|     <circle key="senate-seat-35" id="senate-seat-35" r="8" cy="184.514" cx="327.2" transform="matrix(0.986454, -0.16404, 0.16404, 0.986454, 8.933, 107.941)" />, | ||||
|     <circle key="senate-seat-36" id="senate-seat-36" r="8" cy="261.241" cx="363.968" />, | ||||
|     <circle key="senate-seat-37" id="senate-seat-37" r="8" cy="285.358" cx="363.968" />, | ||||
|     <circle key="senate-seat-38" id="senate-seat-38" r="8" cy="307.598" cx="363.968" />, | ||||
|     <circle key="senate-seat-39" id="senate-seat-39" r="8" cy="331.318" cx="363.968" />, | ||||
|     <circle key="senate-seat-40" id="senate-seat-40" r="8" cy="354.956" cx="363.968" />, | ||||
|     <circle key="senate-seat-41" id="senate-seat-41" r="8" cy="159.989" cx="295.143" transform="matrix(-0.986454, -0.16404, -0.16404, 0.986454, 540.544, 61.56)" />, | ||||
|     <circle key="senate-seat-42" id="senate-seat-42" r="8" cy="170.401" cx="311.791" transform="matrix(-0.986454, -0.16404, -0.16404, 0.986454, 535.427, 55.561)" />, | ||||
|     <circle key="senate-seat-43" id="senate-seat-43" r="8" cy="184.514" cx="327.2" transform="matrix(-0.986454, -0.16404, -0.16404, 0.986454, 528.776, 52.17)" />, | ||||
|     <circle key="senate-seat-44" id="senate-seat-44" r="8" cy="184.514" cx="327.2" transform="matrix(-0.986454, -0.16404, -0.16404, 0.986454, 507.83, 65.575)" />, | ||||
|     <circle key="senate-seat-45" id="senate-seat-45" r="8" cy="184.514" cx="327.2" transform="matrix(-0.986454, -0.16404, -0.16404, 0.986454, 488.237, 84.441)" />, | ||||
|   ]; | ||||
|  | ||||
|   const renderedElements = seatElements.map((child, index) => { | ||||
|     if (index >= seatData.length) { | ||||
|       return React.cloneElement(child, { | ||||
|         fill: '#E0E0E0', | ||||
|         stroke: '#ffffff', | ||||
|         strokeWidth: 1.5, | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     const seat = seatData[index]; | ||||
|     const patternId = `stripes-${seat.color.replace('#', '')}`; | ||||
|  | ||||
|     return React.cloneElement(child, { | ||||
|       fill: seat.isEnJuego ? `url(#${patternId})` : seat.color, | ||||
|       stroke: '#ffffff', | ||||
|       strokeWidth: 1.5, | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   return ( | ||||
|     <svg viewBox="73.512 141.174 338.86 280.338" width={size} height={size * (280.338 / 338.86)} style={{ display: 'block', margin: 'auto' }}> | ||||
|       <defs> | ||||
|         {uniqueColors.map(color => { | ||||
|           const patternId = `stripes-${color.replace('#', '')}`; | ||||
|           return ( | ||||
|             <pattern key={patternId} id={patternId} patternUnits="userSpaceOnUse" width="4" height="4" patternTransform="rotate(45)"> | ||||
|               <rect width="4" height="4" fill={color}></rect> | ||||
|               <path d="M-1,1 l2,-2 M0,4 l4,-4 M3,5 l2,-2" stroke="rgba(255,255,255,0.7)" strokeWidth="3"></path> | ||||
|             </pattern> | ||||
|           ); | ||||
|         })} | ||||
|       </defs> | ||||
|       <g> | ||||
|         {renderedElements} | ||||
|       </g> | ||||
|     </svg> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										151
									
								
								Elecciones-Web/frontend/src/components/TelegramaWidget.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								Elecciones-Web/frontend/src/components/TelegramaWidget.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,151 @@ | ||||
| /* src/components/TelegramaWidget.css */ | ||||
| .telegrama-container { | ||||
|   background-color: #ffffff; | ||||
|   border: 1px solid #e0e0e0; | ||||
|   box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); | ||||
|   padding: 1.5rem 2rem; | ||||
|   border-radius: 8px; | ||||
|   max-width: 800px; | ||||
|   margin: 20px auto; | ||||
|   font-family: "Public Sans", system-ui, sans-serif; | ||||
| } | ||||
|  | ||||
| .telegrama-container h4 { | ||||
|   margin-top: 0; | ||||
|   color: #212529; | ||||
|   font-size: 1.2em; | ||||
|   font-weight: 700; | ||||
|   margin-bottom: 1.5rem; | ||||
|   text-align: center; | ||||
| } | ||||
|  | ||||
| .filters-grid { | ||||
|   display: grid; | ||||
|   grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | ||||
|   gap: 1rem; | ||||
|   margin-bottom: 1.5rem; | ||||
| } | ||||
|  | ||||
| .filters-grid select { | ||||
|   width: 100%; | ||||
|   padding: 0.75rem; | ||||
|   font-size: 1em; | ||||
|   border: 1px solid #ced4da; | ||||
|   border-radius: 4px; | ||||
|   font-family: inherit; | ||||
|   background-color: #fff; | ||||
| } | ||||
|  | ||||
| .filters-grid select:disabled { | ||||
|   background-color: #e9ecef; | ||||
|   cursor: not-allowed; | ||||
| } | ||||
|  | ||||
| .search-form { | ||||
|   display: flex; | ||||
|   gap: 0.5rem; | ||||
|   margin-bottom: 1.5rem; | ||||
| } | ||||
|  | ||||
| .search-input { | ||||
|   flex-grow: 1; | ||||
|   padding: 0.75rem; | ||||
|   font-size: 1em; | ||||
|   border: 1px solid #ced4da; | ||||
|   border-radius: 4px; | ||||
|   font-family: inherit; | ||||
| } | ||||
|  | ||||
| .search-button { | ||||
|   padding: 0.75rem 1.5rem; | ||||
|   font-size: 1em; | ||||
|   font-weight: 500; | ||||
|   border: none; | ||||
|   border-radius: 4px; | ||||
|   background-color: var(--primary-accent-color); | ||||
|   color: white; | ||||
|   cursor: pointer; | ||||
|   transition: background-color 0.2s; | ||||
| } | ||||
|  | ||||
| .search-button:hover { | ||||
|   background-color: #0056b3; | ||||
| } | ||||
|  | ||||
| .search-button:disabled { | ||||
|   background-color: #6c757d; | ||||
|   cursor: not-allowed; | ||||
| } | ||||
|  | ||||
| .telegrama-viewer { | ||||
|   min-height: 400px; | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   border-top: 1px solid #e9ecef; | ||||
|   padding-top: 1.5rem; | ||||
| } | ||||
|  | ||||
| .telegrama-viewer .message { | ||||
|   color: #6c757d; | ||||
|   font-size: 1.1em; | ||||
| } | ||||
|  | ||||
| .telegrama-viewer .message.error { | ||||
|   color: #d62728; | ||||
| } | ||||
|  | ||||
| .telegrama-content { | ||||
|   display: flex; | ||||
|   gap: 2rem; | ||||
|   width: 100%; | ||||
| } | ||||
|  | ||||
| .telegrama-image-wrapper { | ||||
|   flex: 1 1 65%; | ||||
|   border: 1px solid #dee2e6; | ||||
|   border-radius: 4px; | ||||
|   overflow: hidden; | ||||
|   max-height: 500px; | ||||
|   overflow-y: auto; | ||||
| } | ||||
|  | ||||
| .telegrama-image-wrapper img { | ||||
|   width: 100%; | ||||
|   height: auto; | ||||
|   display: block; | ||||
| } | ||||
|  | ||||
| .telegrama-metadata { | ||||
|   flex: 1 1 35%; | ||||
| } | ||||
|  | ||||
| .telegrama-metadata h5 { | ||||
|   margin-top: 0; | ||||
|   font-size: 1.1em; | ||||
|   border-bottom: 1px solid #e9ecef; | ||||
|   padding-bottom: 0.5rem; | ||||
|   margin-bottom: 1rem; | ||||
| } | ||||
|  | ||||
| .meta-item { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   margin-bottom: 1rem; | ||||
|   font-size: 0.9em; | ||||
| } | ||||
|  | ||||
| .meta-item span { | ||||
|   color: #6c757d; | ||||
| } | ||||
|  | ||||
| .meta-item strong { | ||||
|   color: #212529; | ||||
|   font-size: 1.1em; | ||||
| } | ||||
|  | ||||
| @media (max-width: 768px) { | ||||
|   .telegrama-content { | ||||
|     flex-direction: column; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										127
									
								
								Elecciones-Web/frontend/src/components/TelegramaWidget.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								Elecciones-Web/frontend/src/components/TelegramaWidget.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,127 @@ | ||||
| // src/components/TelegramaWidget.tsx | ||||
| import { useState, useEffect } from 'react'; | ||||
| import {  | ||||
|     getSecciones,  | ||||
|     getMunicipiosPorSeccion,  | ||||
|     getCircuitosPorMunicipio, | ||||
|     getEstablecimientosPorCircuito, | ||||
|     getMesasPorEstablecimiento, | ||||
|     getTelegramaPorId  | ||||
| } from '../apiService'; | ||||
| import type { TelegramaData, CatalogoItem } from '../types/types'; | ||||
| import './TelegramaWidget.css'; | ||||
|  | ||||
| export const TelegramaWidget = () => { | ||||
|   // Estados para los filtros geográficos | ||||
|   const [secciones, setSecciones] = useState<CatalogoItem[]>([]); | ||||
|   const [municipios, setMunicipios] = useState<CatalogoItem[]>([]); | ||||
|   const [circuitos, setCircuitos] = useState<CatalogoItem[]>([]); | ||||
|   const [establecimientos, setEstablecimientos] = useState<CatalogoItem[]>([]); | ||||
|   const [mesas, setMesas] = useState<CatalogoItem[]>([]); | ||||
|  | ||||
|   // Estados para los valores seleccionados | ||||
|   const [selectedSeccion, setSelectedSeccion] = useState(''); | ||||
|   const [selectedMunicipio, setSelectedMunicipio] = useState(''); | ||||
|   const [selectedCircuito, setSelectedCircuito] = useState(''); | ||||
|   const [selectedEstablecimiento, setSelectedEstablecimiento] = useState(''); | ||||
|   const [selectedMesa, setSelectedMesa] = useState(''); | ||||
|    | ||||
|   // Estados para la visualización del telegrama | ||||
|   const [telegrama, setTelegrama] = useState<TelegramaData | null>(null); | ||||
|   const [loading, setLoading] = useState(false); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|  | ||||
|   // Cargar secciones iniciales | ||||
|   useEffect(() => { | ||||
|     getSecciones().then(setSecciones); | ||||
|   }, []); | ||||
|  | ||||
|   // Cargar municipios cuando cambia la sección | ||||
|   useEffect(() => { | ||||
|     if (selectedSeccion) { | ||||
|       setMunicipios([]); setCircuitos([]); setEstablecimientos([]); setMesas([]); | ||||
|       setSelectedMunicipio(''); setSelectedCircuito(''); setSelectedEstablecimiento(''); setSelectedMesa(''); | ||||
|       getMunicipiosPorSeccion(selectedSeccion).then(setMunicipios); | ||||
|     } | ||||
|   }, [selectedSeccion]); | ||||
|    | ||||
|   // Y así sucesivamente para los demás filtros... | ||||
|   useEffect(() => { | ||||
|     if (selectedMunicipio) { | ||||
|       setCircuitos([]); setEstablecimientos([]); setMesas([]); | ||||
|       setSelectedCircuito(''); setSelectedEstablecimiento(''); setSelectedMesa(''); | ||||
|       getCircuitosPorMunicipio(selectedMunicipio).then(setCircuitos); | ||||
|     } | ||||
|   }, [selectedMunicipio]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (selectedCircuito) { | ||||
|       setEstablecimientos([]); setMesas([]); | ||||
|       setSelectedEstablecimiento(''); setSelectedMesa(''); | ||||
|       getEstablecimientosPorCircuito(selectedCircuito).then(setEstablecimientos); | ||||
|     } | ||||
|   }, [selectedCircuito]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (selectedEstablecimiento) { | ||||
|       setMesas([]); | ||||
|       setSelectedMesa(''); | ||||
|       getMesasPorEstablecimiento(selectedEstablecimiento).then(setMesas); | ||||
|     } | ||||
|   }, [selectedEstablecimiento]); | ||||
|  | ||||
|   // Buscar el telegrama cuando se selecciona una mesa | ||||
|   useEffect(() => { | ||||
|     if (selectedMesa) { | ||||
|       setLoading(true); | ||||
|       setError(null); | ||||
|       setTelegrama(null); | ||||
|       getTelegramaPorId(selectedMesa) | ||||
|         .then(setTelegrama) | ||||
|         .catch(() => setError(`No se encontró el telegrama para la mesa seleccionada.`)) | ||||
|         .finally(() => setLoading(false)); | ||||
|     } | ||||
|   }, [selectedMesa]); | ||||
|  | ||||
|   return ( | ||||
|     <div className="telegrama-container"> | ||||
|       <h4>Consulta de Telegramas por Ubicación</h4> | ||||
|       <div className="filters-grid"> | ||||
|         <select value={selectedSeccion} onChange={e => setSelectedSeccion(e.target.value)}> | ||||
|           <option value="">1. Sección</option> | ||||
|           {secciones.map(s => <option key={s.id} value={s.id}>{s.nombre}</option>)} | ||||
|         </select> | ||||
|         <select value={selectedMunicipio} onChange={e => setSelectedMunicipio(e.target.value)} disabled={!municipios.length}> | ||||
|           <option value="">2. Municipio</option> | ||||
|           {municipios.map(m => <option key={m.id} value={m.id}>{m.nombre}</option>)} | ||||
|         </select> | ||||
|         <select value={selectedCircuito} onChange={e => setSelectedCircuito(e.target.value)} disabled={!circuitos.length}> | ||||
|           <option value="">3. Circuito</option> | ||||
|           {circuitos.map(c => <option key={c.id} value={c.id}>{c.nombre}</option>)} | ||||
|         </select> | ||||
|         <select value={selectedEstablecimiento} onChange={e => setSelectedEstablecimiento(e.target.value)} disabled={!establecimientos.length}> | ||||
|           <option value="">4. Establecimiento</option> | ||||
|           {establecimientos.map(e => <option key={e.id} value={e.id}>{e.nombre}</option>)} | ||||
|         </select> | ||||
|         <select value={selectedMesa} onChange={e => setSelectedMesa(e.target.value)} disabled={!mesas.length}> | ||||
|           <option value="">5. Mesa</option> | ||||
|           {mesas.map(m => <option key={m.id} value={m.id}>{m.nombre}</option>)} | ||||
|         </select> | ||||
|       </div> | ||||
|  | ||||
|       <div className="telegrama-viewer"> | ||||
|         {loading && <div className="spinner"></div>} | ||||
|         {error && <p className="message error">{error}</p>} | ||||
|         {telegrama && ( | ||||
|           <div className="telegrama-content"> | ||||
|             <div className="telegrama-image-wrapper"> | ||||
|               <img src={`data:image/jpeg;base64,${telegrama.contenidoBase64}`} alt={`Telegrama ${telegrama.id}`} /> | ||||
|             </div> | ||||
|             {/* Metadata (opcional, se puede añadir aquí si se desea) */} | ||||
|           </div> | ||||
|         )} | ||||
|         {!loading && !telegrama && !error && <p className="message">Seleccione una mesa para visualizar el telegrama.</p>} | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,44 +1,49 @@ | ||||
| /* src/components/TickerWidget.css */ | ||||
| .ticker-container { | ||||
|   background-color: #2a2a2e; | ||||
|   /* Se cambia a un fondo claro con borde y sombra sutil */ | ||||
|   background-color: #ffffff; | ||||
|   border: 1px solid #e0e0e0; | ||||
|   box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); | ||||
|   padding: 15px 20px; | ||||
|   border-radius: 8px; | ||||
|   max-width: 800px; | ||||
|   margin: 20px auto; | ||||
|   font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; | ||||
|   color: #e0e0e0; | ||||
|   font-family: "Public Sans", system-ui, Avenir, Helvetica, Arial, sans-serif; | ||||
|   color: #333333; /* Color de texto por defecto */ | ||||
| } | ||||
|  | ||||
| .ticker-container.loading, .ticker-container.error { | ||||
|     text-align: center; | ||||
|     padding: 30px; | ||||
|     font-style: italic; | ||||
|     color: #999; | ||||
|     color: #757575; /* Color de texto atenuado */ | ||||
| } | ||||
|  | ||||
| .ticker-header { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   align-items: center; | ||||
|   border-bottom: 1px solid #444; | ||||
|   border-bottom: 1px solid #e0e0e0; /* Borde más claro */ | ||||
|   padding-bottom: 10px; | ||||
|   margin-bottom: 15px; | ||||
| } | ||||
|  | ||||
| .ticker-header h3 { | ||||
|   margin: 0; | ||||
|   color: white; | ||||
|   font-size: 1.4em; | ||||
|   color: #212529; /* Color de título oscuro */ | ||||
|   font-size: 1.2em; | ||||
|   font-weight: 700; | ||||
| } | ||||
|  | ||||
| .ticker-stats { | ||||
|   display: flex; | ||||
|   gap: 20px; | ||||
|   font-size: 0.9em; | ||||
|   color: #555; | ||||
| } | ||||
|  | ||||
| .ticker-stats strong { | ||||
|   color: #a7c7e7; | ||||
|   color: #0073e6; /* Se usa el azul primario para destacar */ | ||||
|   font-size: 1.1em; | ||||
| } | ||||
|  | ||||
| @@ -64,19 +69,19 @@ | ||||
| } | ||||
|  | ||||
| .ticker-party .party-percent { | ||||
|   font-weight: bold; | ||||
|   font-weight: 700; | ||||
| } | ||||
|  | ||||
| .party-bar-background { | ||||
|   background-color: #444; | ||||
|   background-color: #e9ecef; /* Fondo de barra claro */ | ||||
|   border-radius: 4px; | ||||
|   height: 10px; | ||||
|   overflow: hidden; | ||||
| } | ||||
|  | ||||
| .party-bar-foreground { | ||||
|   background-color: #646cff; | ||||
|   height: 100%; | ||||
|   border-radius: 4px; | ||||
|   transition: width 0.5s ease-in-out; | ||||
|   /* El color de fondo se sigue aplicando desde el componente, esto es correcto */ | ||||
| } | ||||
| @@ -13,14 +13,20 @@ const COLORS = [ | ||||
| export const TickerWidget = () => { | ||||
|   const [data, setData] = useState<ResumenProvincial | null>(null); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|   // Se añade un nuevo estado para manejar errores de forma explícita | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const fetchData = async () => { | ||||
|       // Se resetea el error en cada intento de carga | ||||
|       setError(null); | ||||
|       try { | ||||
|         const result = await getResumenProvincial(); | ||||
|         setData(result); | ||||
|       } catch (error) { | ||||
|         console.error("Error cargando resumen provincial:", error); | ||||
|       } catch (err) { | ||||
|         console.error("Error cargando resumen provincial:", err); | ||||
|         // Se guarda el mensaje de error para mostrarlo en la UI | ||||
|         setError("No se pudo conectar con el servidor para obtener el resumen provincial."); | ||||
|       } finally { | ||||
|         setLoading(false); | ||||
|       } | ||||
| @@ -36,17 +42,22 @@ export const TickerWidget = () => { | ||||
|     return <div className="ticker-container loading">Cargando resultados provinciales...</div>; | ||||
|   } | ||||
|  | ||||
|   // Si hay un error, se muestra el mensaje correspondiente | ||||
|   if (error) { | ||||
|     return <div className="ticker-container error">{error}</div>; | ||||
|   } | ||||
|    | ||||
|   if (!data) { | ||||
|     return <div className="ticker-container error">No se pudieron cargar los datos.</div>; | ||||
|     return <div className="ticker-container error">No hay datos disponibles.</div>; | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <div className="ticker-container"> | ||||
|       <div className="ticker-header"> | ||||
|         <h3>TOTAL PROVINCIA {data.provinciaNombre}</h3> | ||||
|         <h3>PROVINCIA BS. AS.</h3> | ||||
|         <div className="ticker-stats"> | ||||
|           <span>Mesas Escrutadas: <strong>{formatPercent(data.porcentajeEscrutado)}</strong></span> | ||||
|           <span>Participación: <strong>{formatPercent(data.porcentajeParticipacion)}</strong></span> | ||||
|           <span>Participación Total: <strong>{formatPercent(data.porcentajeParticipacion)}</strong></span> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div className="ticker-results"> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user