|
|
|
|
@@ -30,61 +30,59 @@ const DEFAULT_MAP_COLOR = '#E0E0E0';
|
|
|
|
|
const CATEGORIAS: Categoria[] = [{ id: 5, nombre: 'Senadores' }, { id: 6, nombre: 'Diputados' }];
|
|
|
|
|
const SECCION_ID_TO_ROMAN: Record<string, string> = { '1': 'I', '2': 'II', '3': 'III', '4': 'IV', '5': 'V', '6': 'VI', '7': 'VII', '8': 'VIII' };
|
|
|
|
|
const ROMAN_TO_SECCION_ID: Record<string, string> = { 'I': '1', 'II': '2', 'III': '3', 'IV': '4', 'V': '5', 'VI': '6', 'VII': '7', 'VIII': '8' };
|
|
|
|
|
// --- CORRECCIÓN 1: Mover NOMBRES_SECCIONES aquí para que sea global al archivo ---
|
|
|
|
|
const NOMBRES_SECCIONES: Record<string, string> = {
|
|
|
|
|
'I': 'Sección Primera', 'II': 'Sección Segunda', 'III': 'Sección Tercera', 'IV': 'Sección Cuarta',
|
|
|
|
|
'V': 'Sección Quinta', 'VI': 'Sección Sexta', 'VII': 'Sección Séptima', 'VIII': 'Sección Capital'
|
|
|
|
|
};
|
|
|
|
|
const MIN_ZOOM = 1;
|
|
|
|
|
const MAX_ZOOM = 5;
|
|
|
|
|
const TRANSLATE_EXTENT: [[number, number], [number, number]] = [[-100, -600], [1100, 300]];
|
|
|
|
|
const TRANSLATE_EXTENT: [[number, number], [number, number]] = [[-100, -1000], [1100, 800]];
|
|
|
|
|
const INITIAL_POSITION = { center: [-60.5, -37.2] as PointTuple, zoom: MIN_ZOOM };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// --- Componente de Detalle ---
|
|
|
|
|
const DetalleSeccion = ({ seccion, categoriaId, onReset }: { seccion: SeccionGeography | null, categoriaId: number, onReset: () => void }) => {
|
|
|
|
|
const seccionId = seccion ? ROMAN_TO_SECCION_ID[seccion.properties.seccion] : null;
|
|
|
|
|
const seccionId = seccion ? ROMAN_TO_SECCION_ID[seccion.properties.seccion] : null;
|
|
|
|
|
|
|
|
|
|
const { data: resultadosDetalle, isLoading, error } = useQuery<ResultadoDetalleSeccion[]>({
|
|
|
|
|
queryKey: ['detalleSeccion', seccionId, categoriaId],
|
|
|
|
|
queryFn: () => getDetalleSeccion(seccionId!, categoriaId),
|
|
|
|
|
enabled: !!seccionId,
|
|
|
|
|
});
|
|
|
|
|
const { data: resultadosDetalle, isLoading, error } = useQuery<ResultadoDetalleSeccion[]>({
|
|
|
|
|
queryKey: ['detalleSeccion', seccionId, categoriaId],
|
|
|
|
|
queryFn: () => getDetalleSeccion(seccionId!, categoriaId),
|
|
|
|
|
enabled: !!seccionId,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!seccion) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="detalle-placeholder">
|
|
|
|
|
<h3>Resultados por Sección</h3>
|
|
|
|
|
<p>Haga clic en una sección del mapa para ver los resultados detallados.</p>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isLoading) return (<div className="detalle-loading"><div className="spinner"></div><p>Cargando resultados de la sección...</p></div>);
|
|
|
|
|
if (error) return <div className="detalle-error">Error al cargar los datos de la sección.</div>;
|
|
|
|
|
|
|
|
|
|
const nombreSeccionLegible = NOMBRES_SECCIONES[seccion.properties.seccion] || "Sección Desconocida";
|
|
|
|
|
|
|
|
|
|
if (!seccion) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="detalle-placeholder">
|
|
|
|
|
<h3>Resultados por Sección</h3>
|
|
|
|
|
<p>Haga clic en una sección del mapa para ver los resultados detallados.</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="detalle-content">
|
|
|
|
|
<button className="reset-button-panel" onClick={onReset}>← VOLVER</button>
|
|
|
|
|
<h3>{nombreSeccionLegible}</h3>
|
|
|
|
|
<ul className="resultados-lista">
|
|
|
|
|
{resultadosDetalle?.map((r) => (
|
|
|
|
|
<li key={r.id}>
|
|
|
|
|
<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}%`, backgroundColor: r.color || DEFAULT_MAP_COLOR }}></div>
|
|
|
|
|
</div>
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isLoading) return (<div className="detalle-loading"><div className="spinner"></div><p>Cargando resultados de la sección...</p></div>);
|
|
|
|
|
if (error) return <div className="detalle-error">Error al cargar los datos de la sección.</div>;
|
|
|
|
|
|
|
|
|
|
const nombreSeccionLegible = NOMBRES_SECCIONES[seccion.properties.seccion] || "Sección Desconocida";
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="detalle-content">
|
|
|
|
|
<button className="reset-button-panel" onClick={onReset}>← VOLVER</button>
|
|
|
|
|
<h3>{nombreSeccionLegible}</h3>
|
|
|
|
|
<ul className="resultados-lista">
|
|
|
|
|
{resultadosDetalle?.map((r) => (
|
|
|
|
|
<li key={r.id}>
|
|
|
|
|
<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">
|
|
|
|
|
{/* --- CORRECCIÓN 2: Usar el color de la API --- */}
|
|
|
|
|
<div className="progress-fill" style={{ width: `${r.porcentaje}%`, backgroundColor: r.color || DEFAULT_MAP_COLOR }}></div>
|
|
|
|
|
</div>
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// --- Componente de Controles del Mapa ---
|
|
|
|
|
@@ -100,86 +98,89 @@ const MapaBsAsSecciones = () => {
|
|
|
|
|
const [selectedCategoriaId, setSelectedCategoriaId] = useState<number>(6);
|
|
|
|
|
const [clickedSeccion, setClickedSeccion] = useState<SeccionGeography | null>(null);
|
|
|
|
|
const [tooltipContent, setTooltipContent] = useState('');
|
|
|
|
|
const [isPanning, setIsPanning] = useState(false);
|
|
|
|
|
|
|
|
|
|
const { data: geoData, isLoading: isLoadingGeo } = useQuery<any>({
|
|
|
|
|
queryKey: ['mapaGeoDataSecciones'],
|
|
|
|
|
queryFn: async () => (await axios.get('./secciones-electorales-pba.topojson')).data,
|
|
|
|
|
});
|
|
|
|
|
const { data: geoData, isLoading: isLoadingGeo } = useQuery<any>({
|
|
|
|
|
queryKey: ['mapaGeoDataSecciones'],
|
|
|
|
|
queryFn: async () => (await axios.get('./secciones-electorales-pba.topojson')).data,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const { data: resultadosData, isLoading: isLoadingResultados } = useQuery<ResultadoMapaSeccion[]>({
|
|
|
|
|
queryKey: ['mapaResultadosPorSeccion', selectedCategoriaId],
|
|
|
|
|
queryFn: async () => (await axios.get(`${API_BASE_URL}/Resultados/mapa-por-seccion?categoriaId=${selectedCategoriaId}`)).data,
|
|
|
|
|
});
|
|
|
|
|
const { data: resultadosData, isLoading: isLoadingResultados } = useQuery<ResultadoMapaSeccion[]>({
|
|
|
|
|
queryKey: ['mapaResultadosPorSeccion', selectedCategoriaId],
|
|
|
|
|
queryFn: async () => (await axios.get(`${API_BASE_URL}/Resultados/mapa-por-seccion?categoriaId=${selectedCategoriaId}`)).data,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const { data: agrupacionesData, isLoading: isLoadingAgrupaciones } = useQuery<Agrupacion[]>({
|
|
|
|
|
queryKey: ['catalogoAgrupaciones'],
|
|
|
|
|
queryFn: async () => (await axios.get(`${API_BASE_URL}/Catalogos/agrupaciones`)).data,
|
|
|
|
|
});
|
|
|
|
|
const { data: agrupacionesData, isLoading: isLoadingAgrupaciones } = useQuery<Agrupacion[]>({
|
|
|
|
|
queryKey: ['catalogoAgrupaciones'],
|
|
|
|
|
queryFn: async () => (await axios.get(`${API_BASE_URL}/Catalogos/agrupaciones`)).data,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const { nombresAgrupaciones, resultadosPorSeccionRomana } = useMemo<{
|
|
|
|
|
nombresAgrupaciones: Map<string, string>;
|
|
|
|
|
resultadosPorSeccionRomana: Map<string, ResultadoMapaSeccion>;
|
|
|
|
|
}>(() => {
|
|
|
|
|
const nombresMap = new Map<string, string>();
|
|
|
|
|
const resultadosMap = new Map<string, ResultadoMapaSeccion>();
|
|
|
|
|
const { nombresAgrupaciones, resultadosPorSeccionRomana } = useMemo<{
|
|
|
|
|
nombresAgrupaciones: Map<string, string>;
|
|
|
|
|
resultadosPorSeccionRomana: Map<string, ResultadoMapaSeccion>;
|
|
|
|
|
}>((
|
|
|
|
|
) => {
|
|
|
|
|
const nombresMap = new Map<string, string>();
|
|
|
|
|
const resultadosMap = new Map<string, ResultadoMapaSeccion>();
|
|
|
|
|
|
|
|
|
|
if (agrupacionesData) {
|
|
|
|
|
agrupacionesData.forEach(a => nombresMap.set(a.id, a.nombre));
|
|
|
|
|
}
|
|
|
|
|
if (resultadosData) {
|
|
|
|
|
resultadosData.forEach(r => {
|
|
|
|
|
const roman = SECCION_ID_TO_ROMAN[r.seccionId];
|
|
|
|
|
if (roman) resultadosMap.set(roman, r);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return { nombresAgrupaciones: nombresMap, resultadosPorSeccionRomana: resultadosMap };
|
|
|
|
|
}, [agrupacionesData, resultadosData]);
|
|
|
|
|
if (agrupacionesData) {
|
|
|
|
|
agrupacionesData.forEach(a => nombresMap.set(a.id, a.nombre));
|
|
|
|
|
}
|
|
|
|
|
if (resultadosData) {
|
|
|
|
|
resultadosData.forEach(r => {
|
|
|
|
|
const roman = SECCION_ID_TO_ROMAN[r.seccionId];
|
|
|
|
|
if (roman) resultadosMap.set(roman, r);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return { nombresAgrupaciones: nombresMap, resultadosPorSeccionRomana: resultadosMap };
|
|
|
|
|
}, [agrupacionesData, resultadosData]);
|
|
|
|
|
|
|
|
|
|
const isLoading = isLoadingGeo || isLoadingResultados || isLoadingAgrupaciones;
|
|
|
|
|
|
|
|
|
|
const handleReset = useCallback(() => {
|
|
|
|
|
setClickedSeccion(null);
|
|
|
|
|
setPosition(INITIAL_POSITION);
|
|
|
|
|
}, []);
|
|
|
|
|
const handleReset = useCallback(() => {
|
|
|
|
|
setClickedSeccion(null);
|
|
|
|
|
setPosition(INITIAL_POSITION);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleGeographyClick = useCallback((geo: SeccionGeography) => {
|
|
|
|
|
if (clickedSeccion?.rsmKey === geo.rsmKey) {
|
|
|
|
|
handleReset();
|
|
|
|
|
} else {
|
|
|
|
|
const centroid = geoCentroid(geo as any) as PointTuple;
|
|
|
|
|
setPosition({ center: centroid, zoom: 2 });
|
|
|
|
|
setClickedSeccion(geo);
|
|
|
|
|
}
|
|
|
|
|
}, [clickedSeccion, handleReset]);
|
|
|
|
|
|
|
|
|
|
const handleMoveEnd = (newPosition: { coordinates: PointTuple; zoom: number }) => {
|
|
|
|
|
if (newPosition.zoom <= MIN_ZOOM) {
|
|
|
|
|
if (position.zoom > MIN_ZOOM || clickedSeccion !== null) {
|
|
|
|
|
const handleGeographyClick = useCallback((geo: SeccionGeography) => {
|
|
|
|
|
if (clickedSeccion?.rsmKey === geo.rsmKey) {
|
|
|
|
|
handleReset();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (newPosition.zoom < position.zoom && clickedSeccion !== null) {
|
|
|
|
|
setClickedSeccion(null);
|
|
|
|
|
}
|
|
|
|
|
setPosition({ center: newPosition.coordinates, zoom: newPosition.zoom });
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
const centroid = geoCentroid(geo as any) as PointTuple;
|
|
|
|
|
setPosition({ center: centroid, zoom: 2 });
|
|
|
|
|
setClickedSeccion(geo);
|
|
|
|
|
}
|
|
|
|
|
}, [clickedSeccion, handleReset]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => e.key === 'Escape' && handleReset();
|
|
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
|
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
|
|
|
}, [handleReset]);
|
|
|
|
|
const handleMoveEnd = (newPosition: { coordinates: PointTuple; zoom: number }) => {
|
|
|
|
|
if (newPosition.zoom <= MIN_ZOOM) {
|
|
|
|
|
if (position.zoom > MIN_ZOOM || clickedSeccion !== null) {
|
|
|
|
|
handleReset();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (newPosition.zoom < position.zoom && clickedSeccion !== null) {
|
|
|
|
|
setClickedSeccion(null);
|
|
|
|
|
}
|
|
|
|
|
setPosition({ center: newPosition.coordinates, zoom: newPosition.zoom });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getSectionFillColor = (seccionRomana: string) => {
|
|
|
|
|
return resultadosPorSeccionRomana.get(seccionRomana)?.colorGanador || DEFAULT_MAP_COLOR;
|
|
|
|
|
};
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => e.key === 'Escape' && handleReset();
|
|
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
|
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
|
|
|
}, [handleReset]);
|
|
|
|
|
|
|
|
|
|
const getSectionFillColor = (seccionRomana: string) => {
|
|
|
|
|
return resultadosPorSeccionRomana.get(seccionRomana)?.colorGanador || DEFAULT_MAP_COLOR;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleZoomIn = () => {
|
|
|
|
|
if (position.zoom < MAX_ZOOM) {
|
|
|
|
|
setPosition(p => ({ ...p, zoom: Math.min(p.zoom * 1.5, MAX_ZOOM) }));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleZoomIn = () => {
|
|
|
|
|
if (position.zoom < MAX_ZOOM) {
|
|
|
|
|
setPosition(p => ({ ...p, zoom: Math.min(p.zoom * 1.5, MAX_ZOOM) }));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="mapa-wrapper">
|
|
|
|
|
@@ -195,11 +196,15 @@ const MapaBsAsSecciones = () => {
|
|
|
|
|
<ZoomableGroup
|
|
|
|
|
center={position.center}
|
|
|
|
|
zoom={position.zoom}
|
|
|
|
|
onMoveEnd={handleMoveEnd}
|
|
|
|
|
onMoveEnd={(newPosition: { coordinates: PointTuple; zoom: number }) => {
|
|
|
|
|
setIsPanning(false);
|
|
|
|
|
handleMoveEnd(newPosition);
|
|
|
|
|
}}
|
|
|
|
|
minZoom={MIN_ZOOM}
|
|
|
|
|
maxZoom={MAX_ZOOM}
|
|
|
|
|
translateExtent={TRANSLATE_EXTENT}
|
|
|
|
|
style={{ transition: "transform 400ms ease-in-out" }}
|
|
|
|
|
className={isPanning ? 'panning' : ''}
|
|
|
|
|
onMoveStart={() => setIsPanning(true)}
|
|
|
|
|
filterZoomEvent={(e: WheelEvent) => {
|
|
|
|
|
if (e.deltaY > 0) {
|
|
|
|
|
handleReset();
|
|
|
|
|
@@ -228,7 +233,6 @@ const MapaBsAsSecciones = () => {
|
|
|
|
|
onClick={isClickable ? () => handleGeographyClick(geo) : undefined}
|
|
|
|
|
onMouseEnter={() => {
|
|
|
|
|
if (isClickable) {
|
|
|
|
|
// --- CORRECCIÓN 3: Tooltip con nombre de sección ---
|
|
|
|
|
const nombreSeccionLegible = NOMBRES_SECCIONES[geo.properties.seccion] || "Sección Desconocida";
|
|
|
|
|
setTooltipContent(`${nombreSeccionLegible}: ${nombreGanador}`);
|
|
|
|
|
}
|
|
|
|
|
@@ -272,32 +276,32 @@ const MapaBsAsSecciones = () => {
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// --- Sub-componente para la Leyenda ---
|
|
|
|
|
// --- Sub-componente para la Leyenda (sin cambios) ---
|
|
|
|
|
const LegendSecciones = ({ resultados, nombresAgrupaciones }: { resultados: Map<string, ResultadoMapaSeccion>, nombresAgrupaciones: Map<string, string> }) => {
|
|
|
|
|
const legendItems = useMemo(() => {
|
|
|
|
|
const ganadoresUnicos = new Map<string, { nombre: string; color: string }>();
|
|
|
|
|
resultados.forEach(resultado => {
|
|
|
|
|
if (resultado.agrupacionGanadoraId && resultado.colorGanador && !ganadoresUnicos.has(resultado.agrupacionGanadoraId)) {
|
|
|
|
|
ganadoresUnicos.set(resultado.agrupacionGanadoraId, {
|
|
|
|
|
nombre: nombresAgrupaciones.get(resultado.agrupacionGanadoraId) || 'Desconocido',
|
|
|
|
|
color: resultado.colorGanador
|
|
|
|
|
const legendItems = useMemo(() => {
|
|
|
|
|
const ganadoresUnicos = new Map<string, { nombre: string; color: string }>();
|
|
|
|
|
resultados.forEach(resultado => {
|
|
|
|
|
if (resultado.agrupacionGanadoraId && resultado.colorGanador && !ganadoresUnicos.has(resultado.agrupacionGanadoraId)) {
|
|
|
|
|
ganadoresUnicos.set(resultado.agrupacionGanadoraId, {
|
|
|
|
|
nombre: nombresAgrupaciones.get(resultado.agrupacionGanadoraId) || 'Desconocido',
|
|
|
|
|
color: resultado.colorGanador
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return Array.from(ganadoresUnicos.values());
|
|
|
|
|
}, [resultados, nombresAgrupaciones]);
|
|
|
|
|
return Array.from(ganadoresUnicos.values());
|
|
|
|
|
}, [resultados, nombresAgrupaciones]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="legend">
|
|
|
|
|
<h4>Ganadores por Sección</h4>
|
|
|
|
|
{legendItems.map(item => (
|
|
|
|
|
<div key={item.nombre} className="legend-item">
|
|
|
|
|
<div className="legend-color-box" style={{ backgroundColor: item.color }} />
|
|
|
|
|
<span>{item.nombre}</span>
|
|
|
|
|
return (
|
|
|
|
|
<div className="legend">
|
|
|
|
|
<h4>Ganadores por Sección</h4>
|
|
|
|
|
{legendItems.map(item => (
|
|
|
|
|
<div key={item.nombre} className="legend-item">
|
|
|
|
|
<div className="legend-color-box" style={{ backgroundColor: item.color }} />
|
|
|
|
|
<span>{item.nombre}</span>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default MapaBsAsSecciones;
|