104 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			104 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | // src/features/legislativas/nacionales/components/MapaNacional.tsx
 | ||
|  | import axios from 'axios'; | ||
|  | import { Suspense, useState, useEffect, useCallback } from 'react'; // <-- Asegúrate de que useCallback esté importado
 | ||
|  | import { useSuspenseQuery } from '@tanstack/react-query'; | ||
|  | import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps'; | ||
|  | import { Tooltip } from 'react-tooltip'; | ||
|  | import { API_BASE_URL, assetBaseUrl } from '../../../../apiService'; | ||
|  | import type { ResultadoMapaDto, AmbitoGeography } from '../../../../types/types'; | ||
|  | import { MapaProvincial } from './MapaProvincial'; | ||
|  | 
 | ||
|  | const DEFAULT_MAP_COLOR = '#E0E0E0'; | ||
|  | const FADED_BACKGROUND_COLOR = '#F0F0F0'; | ||
|  | const normalizarTexto = (texto: string = '') => texto.trim().toUpperCase().normalize("NFD").replace(/[\u0300-\u036f]/g, ""); | ||
|  | 
 | ||
|  | type PointTuple = [number, number]; | ||
|  | 
 | ||
|  | interface MapaNacionalProps { | ||
|  |   eleccionId: number; | ||
|  |   categoriaId: number; | ||
|  |   nivel: 'pais' | 'provincia' | 'municipio'; | ||
|  |   nombreAmbito: string; | ||
|  |   provinciaDistritoId: string | null; | ||
|  |   onAmbitoSelect: (ambitoId: string, nivel: 'provincia' | 'municipio', nombre: string) => void; | ||
|  |   onVolver: () => void; | ||
|  | } | ||
|  | 
 | ||
|  | export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, provinciaDistritoId, onAmbitoSelect, onVolver }: MapaNacionalProps) => { | ||
|  |   const [position, setPosition] = useState({ zoom: 1, center: [-65, -40] as PointTuple }); | ||
|  | 
 | ||
|  |   const { data: mapaDataNacional } = useSuspenseQuery<ResultadoMapaDto[]>({ | ||
|  |     queryKey: ['mapaResultados', eleccionId, categoriaId, null], | ||
|  |     queryFn: async () => { | ||
|  |       const url = `${API_BASE_URL}/elecciones/${eleccionId}/mapa-resultados?categoriaId=${categoriaId}`; | ||
|  |       const response = await axios.get(url); | ||
|  |       return response.data; | ||
|  |     }, | ||
|  |   }); | ||
|  | 
 | ||
|  |   const { data: geoDataNacional } = useSuspenseQuery<any>({ | ||
|  |     queryKey: ['geoDataNacional'], | ||
|  |     queryFn: () => axios.get(`${assetBaseUrl}/maps/argentina-provincias.topojson`).then(res => res.data), | ||
|  |   }); | ||
|  | 
 | ||
|  |   const resultadosNacionalesPorNombre = new Map<string, ResultadoMapaDto>(mapaDataNacional.map(d => [normalizarTexto(d.ambitoNombre), d])); | ||
|  |    | ||
|  |   const nombreMunicipioSeleccionado = nivel === 'municipio' ? nombreAmbito : null; | ||
|  | 
 | ||
|  |   // El useEffect para el zoom provincial y nacional sigue siendo correcto.
 | ||
|  |   useEffect(() => { | ||
|  |     if (nivel === 'pais') { | ||
|  |       setPosition({ zoom: 1, center: [-65, -40] }); | ||
|  |     } else if (nivel === 'provincia') { | ||
|  |       setPosition({ zoom: 7, center: [-60.5, -37] }); | ||
|  |     } | ||
|  |     // La lógica de centrado en municipio se delega al hijo, que llamará a `handleCalculatedCenter`
 | ||
|  |   }, [nivel]); | ||
|  | 
 | ||
|  |   // **LA SOLUCIÓN CLAVE**: Estabilizamos la función que se pasa al hijo.
 | ||
|  |   const handleCalculatedCenter = useCallback((center: PointTuple, zoom: number) => { | ||
|  |     setPosition({ center, zoom }); | ||
|  |   }, []); // El array de dependencias vacío asegura que la función nunca cambie
 | ||
|  | 
 | ||
|  |   return ( | ||
|  |     <div className="mapa-componente-container"> | ||
|  |       {nivel !== 'pais' && <button onClick={onVolver} className="mapa-volver-btn">← Volver</button>} | ||
|  |       <ComposableMap projection="geoMercator" projectionConfig={{ scale: 700, center: [-65, -40] }} style={{ width: "100%", height: "100%" }}> | ||
|  |         <ZoomableGroup center={position.center} zoom={position.zoom} filterZoomEvent={() => false}> | ||
|  |           <Geographies geography={geoDataNacional}> | ||
|  |             {({ geographies }: { geographies: AmbitoGeography[] }) => geographies.map((geo) => { | ||
|  |               const resultado = resultadosNacionalesPorNombre.get(normalizarTexto(geo.properties.nombre)); | ||
|  |               const esProvinciaActiva = provinciaDistritoId && resultado?.ambitoId === provinciaDistritoId; | ||
|  | 
 | ||
|  |               return ( | ||
|  |                 <Geography | ||
|  |                   key={geo.rsmKey} geography={geo} | ||
|  |                   className={`rsm-geography ${nivel !== 'pais' ? 'rsm-geography-faded' : ''}`} | ||
|  |                   style={{ visibility: esProvinciaActiva ? 'hidden' : 'visible' }} | ||
|  |                   fill={nivel === 'pais' ? (resultado?.colorGanador || DEFAULT_MAP_COLOR) : FADED_BACKGROUND_COLOR} | ||
|  |                   onClick={() => resultado && onAmbitoSelect(resultado.ambitoId, 'provincia', resultado.ambitoNombre)} | ||
|  |                 /> | ||
|  |               ); | ||
|  |             })} | ||
|  |           </Geographies> | ||
|  | 
 | ||
|  |           {provinciaDistritoId && ( | ||
|  |             <Suspense fallback={null}> | ||
|  |               <MapaProvincial | ||
|  |                 eleccionId={eleccionId} | ||
|  |                 categoriaId={categoriaId} | ||
|  |                 distritoId={provinciaDistritoId} | ||
|  |                 nombreProvincia={"BUENOS AIRES"} // Esto se podría hacer dinámico si fuera necesario
 | ||
|  |                 nombreMunicipioSeleccionado={nombreMunicipioSeleccionado} | ||
|  |                 onMunicipioSelect={(ambitoId, nombre) => onAmbitoSelect(ambitoId, 'municipio', nombre)} | ||
|  |                 onCalculatedCenter={handleCalculatedCenter} // Pasamos la función estabilizada
 | ||
|  |                 nivel={nivel as 'provincia' | 'municipio'} // El cast de tipo sigue siendo necesario y correcto
 | ||
|  |               /> | ||
|  |             </Suspense> | ||
|  |           )} | ||
|  |         </ZoomableGroup> | ||
|  |       </ComposableMap> | ||
|  |       <Tooltip id="mapa-tooltip" /> | ||
|  |     </div> | ||
|  |   ); | ||
|  | }; |