Test Docker
This commit is contained in:
		
							
								
								
									
										52
									
								
								Elecciones-Web/frontend/src/components/BancasWidget.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								Elecciones-Web/frontend/src/components/BancasWidget.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| // src/components/BancasWidget.tsx | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { getBancasPorSeccion, type ProyeccionBancas } from '../services/api'; | ||||
|  | ||||
| interface Props { | ||||
|   seccionId: string; | ||||
| } | ||||
|  | ||||
| export const BancasWidget = ({ seccionId }: Props) => { | ||||
|   const [data, setData] = useState<ProyeccionBancas | null>(null); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const fetchData = async () => { | ||||
|       try { | ||||
|         setLoading(true); | ||||
|         const proyeccion = await getBancasPorSeccion(seccionId); | ||||
|         setData(proyeccion); | ||||
|       } catch (err) { | ||||
|         console.error("Error cargando bancas", err); | ||||
|       } finally { | ||||
|         setLoading(false); | ||||
|       } | ||||
|     }; | ||||
|     fetchData(); | ||||
|   }, [seccionId]); | ||||
|  | ||||
|   if (loading) return <div>Cargando proyección de bancas...</div>; | ||||
|   if (!data) return <div>No hay datos de bancas disponibles.</div>; | ||||
|  | ||||
|   return ( | ||||
|     <div style={{ fontFamily: 'sans-serif', border: '1px solid #ccc', padding: '16px', borderRadius: '8px' }}> | ||||
|       <h3>Proyección de Bancas - {data.seccionNombre}</h3> | ||||
|       <table style={{ width: '100%', borderCollapse: 'collapse' }}> | ||||
|         <thead> | ||||
|           <tr> | ||||
|             <th style={{ textAlign: 'left' }}>Agrupación</th> | ||||
|             <th style={{ textAlign: 'right' }}>Bancas Obtenidas</th> | ||||
|           </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|           {data.proyeccion.map((partido) => ( | ||||
|             <tr key={partido.agrupacionNombre}> | ||||
|               <td>{partido.agrupacionNombre}</td> | ||||
|               <td style={{ textAlign: 'right' }}>{partido.bancas}</td> | ||||
|             </tr> | ||||
|           ))} | ||||
|         </tbody> | ||||
|       </table> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										112
									
								
								Elecciones-Web/frontend/src/components/MapaD3Widget.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								Elecciones-Web/frontend/src/components/MapaD3Widget.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| import { useEffect, useRef, useState } from 'react'; | ||||
| import * as d3 from 'd3'; | ||||
| // FIX: Usamos 'import type' para los tipos y quitamos la importación de 'MunicipioSimple' | ||||
| import type { FeatureCollection } from 'geojson'; | ||||
|  | ||||
| // --- Interfaces y Constantes --- | ||||
| interface MapaResultado { | ||||
|   municipioId: string; | ||||
|   agrupacionGanadoraId: string; | ||||
| } | ||||
| const COLOR_MAP: { [key: string]: string } = { "018": "#FFC107", "025": "#03A9F4", "031": "#4CAF50", "045": "#9C27B0", "default": "#E0E0E0" }; | ||||
|  | ||||
| interface Props { | ||||
|   onMunicipioClick: (municipioId: string) => void; | ||||
| } | ||||
|  | ||||
| export const MapaD3Widget = ({ onMunicipioClick }: Props) => { | ||||
|   const svgRef = useRef<SVGSVGElement | null>(null); | ||||
|   const containerRef = useRef<HTMLDivElement | null>(null); | ||||
|   const [tooltip, setTooltip] = useState<{ x: number; y: number; content: string } | null>(null); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const svgElement = svgRef.current; | ||||
|     const containerElement = containerRef.current; | ||||
|     if (!svgElement || !containerElement) return; | ||||
|  | ||||
|     const drawMap = ( | ||||
|       geoData: FeatureCollection, // Usamos el tipo correcto | ||||
|       resultsMap: Map<string, MapaResultado>, | ||||
|       idMap: Record<string, string> | ||||
|     ) => { | ||||
|       const { width, height } = containerElement.getBoundingClientRect(); | ||||
|       if (width === 0 || height === 0) return; | ||||
|  | ||||
|       const svg = d3.select(svgElement); | ||||
|       svg.selectAll('*').remove(); | ||||
|       svg.attr('width', width).attr('height', height).attr('viewBox', `0 0 ${width} ${height}`); | ||||
|  | ||||
|       const projection = d3.geoMercator().fitSize([width, height], geoData); | ||||
|       const pathGenerator = d3.geoPath().projection(projection); | ||||
|  | ||||
|       const features = geoData.features; | ||||
|  | ||||
|       svg.append('g') | ||||
|         .selectAll('path') | ||||
|         .data(features) | ||||
|         .join('path') | ||||
|           .attr('d', pathGenerator as any) | ||||
|           .attr('stroke', '#FFFFFF') | ||||
|           .attr('stroke-width', 0.5) | ||||
|           .attr('fill', (d: any) => { | ||||
|             const geoJsonId = d.properties.cca; | ||||
|             const apiId = idMap[geoJsonId]; | ||||
|             const resultado = resultsMap.get(apiId); | ||||
|             return resultado ? COLOR_MAP[resultado.agrupacionGanadoraId] || COLOR_MAP.default : COLOR_MAP.default; | ||||
|           }) | ||||
|           .style('cursor', 'pointer') | ||||
|           .on('click', (_, d: any) => { | ||||
|             const apiId = idMap[d.properties.cca]; | ||||
|             if (apiId) onMunicipioClick(apiId); | ||||
|           }) | ||||
|           .on('mouseover', (event, d: any) => { | ||||
|             d3.select(event.currentTarget).attr('stroke', 'black').attr('stroke-width', 2); | ||||
|             setTooltip({ x: event.pageX, y: event.pageY, content: d.properties.nam }); | ||||
|           }) | ||||
|           .on('mouseout', (event) => { | ||||
|             d3.select(event.currentTarget).attr('stroke', '#FFFFFF').attr('stroke-width', 0.5); | ||||
|             setTooltip(null); | ||||
|           }); | ||||
|     }; | ||||
|  | ||||
|     (async () => { | ||||
|       try { | ||||
|         const [geoData, resultsData, idMap] = await Promise.all([ | ||||
|           d3.json<FeatureCollection>('/buenos-aires-municipios.geojson'), | ||||
|           d3.json<MapaResultado[]>('http://localhost:5217/api/resultados/mapa'), | ||||
|           d3.json<Record<string, string>>('/municipioIdMap.json') | ||||
|         ]); | ||||
|         if (geoData && resultsData && idMap) { | ||||
|           const resultsMap = new Map(resultsData.map(item => [item.municipioId, item])); | ||||
|           drawMap(geoData, resultsMap, idMap); | ||||
|         } else { | ||||
|           throw new Error("Faltan datos para renderizar el mapa."); | ||||
|         } | ||||
|       } catch (err) { | ||||
|         console.error("Error cargando datos para el mapa:", err); | ||||
|       } finally { | ||||
|         setLoading(false); | ||||
|       } | ||||
|     })(); | ||||
|   }, [onMunicipioClick]); | ||||
|  | ||||
|   if (loading) { | ||||
|     return <div ref={containerRef} style={{ width: '100%', height: '600px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>Cargando datos del mapa...</div>; | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <div ref={containerRef} style={{ width: '100%', height: '600px', border: '1px solid #eee' }}> | ||||
|       <svg ref={svgRef}></svg> | ||||
|       {tooltip && ( | ||||
|           <div style={{ | ||||
|               position: 'fixed', top: tooltip.y + 10, left: tooltip.x + 10, | ||||
|               backgroundColor: 'rgba(0, 0, 0, 0.75)', color: 'white', padding: '8px', | ||||
|               borderRadius: '4px', pointerEvents: 'none', fontSize: '14px', zIndex: 1000, | ||||
|           }}> | ||||
|               {tooltip.content} | ||||
|           </div> | ||||
|       )} | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,31 +1,11 @@ | ||||
| // src/components/MunicipioSelector.tsx | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { getMunicipios, type MunicipioSimple } from '../services/api'; | ||||
| import { type MunicipioSimple } from '../services/api'; | ||||
|  | ||||
| interface Props { | ||||
|   municipios: MunicipioSimple[]; | ||||
|   onMunicipioChange: (municipioId: string) => void; | ||||
| } | ||||
|  | ||||
| export const MunicipioSelector = ({ onMunicipioChange }: Props) => { | ||||
|   const [municipios, setMunicipios] = useState<MunicipioSimple[]>([]); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const loadMunicipios = async () => { | ||||
|       try { | ||||
|         const data = await getMunicipios(); | ||||
|         setMunicipios(data); | ||||
|       } catch (error) { | ||||
|         console.error("Error al cargar municipios", error); | ||||
|       } finally { | ||||
|         setLoading(false); | ||||
|       } | ||||
|     }; | ||||
|     loadMunicipios(); | ||||
|   }, []); | ||||
|  | ||||
|   if (loading) return <p>Cargando municipios...</p>; | ||||
|  | ||||
| export const MunicipioSelector = ({ municipios, onMunicipioChange }: Props) => { | ||||
|   return ( | ||||
|     <select onChange={(e) => onMunicipioChange(e.target.value)} defaultValue=""> | ||||
|       <option value="" disabled>Seleccione un municipio</option> | ||||
|   | ||||
| @@ -0,0 +1,52 @@ | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { getResumenProvincial, type ResumenProvincial } from '../services/api'; | ||||
|  | ||||
| interface Props { | ||||
|   distritoId: string; | ||||
| } | ||||
|  | ||||
| export const ResumenProvincialWidget = ({ distritoId }: Props) => { | ||||
|   const [data, setData] = useState<ResumenProvincial | null>(null); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const fetchData = async () => { | ||||
|       try { | ||||
|         const resumen = await getResumenProvincial(distritoId); | ||||
|         setData(resumen); | ||||
|       } catch (err) { | ||||
|         console.error("Error cargando resumen provincial", err); | ||||
|       } finally { | ||||
|         setLoading(false); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     fetchData(); | ||||
|     const intervalId = setInterval(fetchData, 15000); // Actualizamos cada 15s | ||||
|     return () => clearInterval(intervalId); | ||||
|   }, [distritoId]); | ||||
|  | ||||
|   if (loading) return <div>Cargando resumen provincial...</div>; | ||||
|   if (!data) return <div>No hay datos provinciales disponibles.</div>; | ||||
|  | ||||
|   return ( | ||||
|     <div style={{ fontFamily: 'sans-serif', border: '1px solid #ccc', padding: '16px', borderRadius: '8px', backgroundColor: '#f9f9f9' }}> | ||||
|       <h2>Resumen Provincial - {data.provinciaNombre}</h2> | ||||
|       <p><strong>Mesas Escrutadas:</strong> {data.porcentajeEscrutado.toFixed(2)}% | <strong>Participación:</strong> {data.porcentajeParticipacion.toFixed(2)}%</p> | ||||
|        | ||||
|       {data.resultados.map((partido) => ( | ||||
|         <div key={partido.nombre} style={{ margin: '10px 0' }}> | ||||
|           <span>{partido.nombre}</span> | ||||
|           <div style={{ display: 'flex', alignItems: 'center' }}> | ||||
|             <div style={{ backgroundColor: '#ddd', width: '100%', borderRadius: '4px', marginRight: '10px' }}> | ||||
|               <div style={{ width: `${partido.porcentaje}%`, backgroundColor: 'royalblue', color: 'white', padding: '4px', borderRadius: '4px', textAlign: 'right' }}> | ||||
|                 <strong>{partido.porcentaje.toFixed(2)}%</strong> | ||||
|               </div> | ||||
|             </div> | ||||
|             <span>{partido.votos.toLocaleString('es-AR')}</span> | ||||
|           </div> | ||||
|         </div> | ||||
|       ))} | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										65
									
								
								Elecciones-Web/frontend/src/components/TelegramasView.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								Elecciones-Web/frontend/src/components/TelegramasView.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| // src/components/TelegramasView.tsx | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { getListaTelegramas, getTelegramaPorId, type TelegramaDetalle } from '../services/api'; | ||||
|  | ||||
| export const TelegramasView = () => { | ||||
|   const [listaIds, setListaIds] = useState<string[]>([]); | ||||
|   const [selectedTelegrama, setSelectedTelegrama] = useState<TelegramaDetalle | null>(null); | ||||
|   const [loadingList, setLoadingList] = useState(true); | ||||
|   const [loadingDetail, setLoadingDetail] = useState(false); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const loadList = async () => { | ||||
|       try { | ||||
|         const ids = await getListaTelegramas(); | ||||
|         setListaIds(ids); | ||||
|       } catch (error) { | ||||
|         console.error("Error al cargar lista de telegramas", error); | ||||
|       } finally { | ||||
|         setLoadingList(false); | ||||
|       } | ||||
|     }; | ||||
|     loadList(); | ||||
|   }, []); | ||||
|  | ||||
|   const handleSelectTelegrama = async (mesaId: string) => { | ||||
|     try { | ||||
|         setLoadingDetail(true); | ||||
|         const detalle = await getTelegramaPorId(mesaId); | ||||
|         setSelectedTelegrama(detalle); | ||||
|     } catch (error) { | ||||
|         console.error(`Error al cargar telegrama ${mesaId}`, error); | ||||
|     } finally { | ||||
|         setLoadingDetail(false); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <div style={{ display: 'flex', gap: '20px', height: '500px' }}> | ||||
|       <div style={{ flex: 1, border: '1px solid #ccc', overflowY: 'auto' }}> | ||||
|         <h4>Telegramas Disponibles</h4> | ||||
|         {loadingList ? <p>Cargando...</p> : ( | ||||
|             <ul style={{ listStyle: 'none', padding: '10px' }}> | ||||
|                 {listaIds.map(id => ( | ||||
|                     <li key={id} onClick={() => handleSelectTelegrama(id)} style={{ cursor: 'pointer', padding: '5px', borderBottom: '1px solid #eee' }}> | ||||
|                         Mesa: {id} | ||||
|                     </li> | ||||
|                 ))} | ||||
|             </ul> | ||||
|         )} | ||||
|       </div> | ||||
|       <div style={{ flex: 3, border: '1px solid #ccc', display: 'flex', justifyContent: 'center', alignItems: 'center' }}> | ||||
|         {loadingDetail ? <p>Cargando telegrama...</p> :  | ||||
|          selectedTelegrama ? ( | ||||
|             <iframe  | ||||
|                 src={`data:application/pdf;base64,${selectedTelegrama.contenidoBase64}`}  | ||||
|                 width="100%"  | ||||
|                 height="100%"  | ||||
|                 title={selectedTelegrama.id} | ||||
|             /> | ||||
|          ) : <p>Seleccione un telegrama de la lista para visualizarlo.</p> | ||||
|         } | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
		Reference in New Issue
	
	Block a user