Preparación Legislativas Nacionales 2025

This commit is contained in:
2025-09-17 11:31:17 -03:00
parent 64dc7ef440
commit 3a8f64bf85
94 changed files with 2471 additions and 195 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
// src/apiService.ts
import axios from 'axios';
import type { ApiResponseRankingMunicipio, ApiResponseRankingSeccion, ApiResponseTablaDetallada, ProyeccionBancas, MunicipioSimple, TelegramaData, CatalogoItem, CategoriaResumen, ResultadoTicker, ApiResponseResultadosPorSeccion } from './types/types';
import type { ApiResponseRankingMunicipio, ApiResponseRankingSeccion, ApiResponseTablaDetallada, ProyeccionBancas, MunicipioSimple, TelegramaData, CatalogoItem, CategoriaResumen, ResultadoTicker, ApiResponseResultadosPorSeccion, PanelElectoralDto } from './types/types';
/**
* URL base para las llamadas a la API.
@@ -84,14 +84,14 @@ export interface ResultadoDetalleSeccion {
color: string | null;
}
export const getResumenProvincial = async (): Promise<CategoriaResumen[]> => {
const response = await apiClient.get('/resultados/provincia/02');
export const getResumenProvincial = async (eleccionId: number): Promise<CategoriaResumen[]> => {
const response = await apiClient.get(`/elecciones/${eleccionId}/provincia/02`);
return response.data;
};
export const getBancasPorSeccion = async (seccionId: string, camara: 'diputados' | 'senadores'): Promise<ProyeccionBancas> => {
const { data } = await apiClient.get(`/resultados/bancas-por-seccion/${seccionId}/${camara}`);
return data;
export const getBancasPorSeccion = async (eleccionId: number, seccionId: string, camara: 'diputados' | 'senadores'): Promise<ProyeccionBancas> => {
const { data } = await apiClient.get(`/elecciones/${eleccionId}/bancas-por-seccion/${seccionId}/${camara}`);
return data;
};
/**
@@ -140,13 +140,13 @@ export const getMesasPorEstablecimiento = async (establecimientoId: string): Pro
return response.data;
};
export const getComposicionCongreso = async (): Promise<ComposicionData> => {
const response = await apiClient.get('/resultados/composicion-congreso');
export const getComposicionCongreso = async (eleccionId: number): Promise<ComposicionData> => {
const response = await apiClient.get(`/elecciones/${eleccionId}/composicion-congreso`);
return response.data;
};
export const getBancadasDetalle = async (): Promise<BancadaDetalle[]> => {
const response = await apiClient.get('/resultados/bancadas-detalle');
export const getBancadasDetalle = async (eleccionId: number): Promise<BancadaDetalle[]> => {
const response = await apiClient.get(`/elecciones/${eleccionId}/bancadas-detalle`);
return response.data;
};
@@ -155,24 +155,18 @@ export const getConfiguracionPublica = async (): Promise<ConfiguracionPublica> =
return response.data;
};
export const getResultadosPorSeccion = async (seccionId: string, categoriaId: number): Promise<ApiResponseResultadosPorSeccion> => {
const response = await apiClient.get(`/resultados/seccion-resultados/${seccionId}?categoriaId=${categoriaId}`);
export const getResultadosPorSeccion = async (eleccionId: number, seccionId: string, categoriaId: number): Promise<ApiResponseResultadosPorSeccion> => {
const response = await apiClient.get(`/elecciones/${eleccionId}/seccion-resultados/${seccionId}?categoriaId=${categoriaId}`);
return response.data;
};
export const getDetalleSeccion = async (seccionId: string, categoriaId: number): Promise<ResultadoDetalleSeccion[]> => {
const response = await apiClient.get(`/resultados/seccion/${seccionId}?categoriaId=${categoriaId}`);
export const getDetalleSeccion = async (eleccionId: number, seccionId: string, categoriaId: number): Promise<ResultadoDetalleSeccion[]> => {
const response = await apiClient.get(`/elecciones/${eleccionId}/seccion/${seccionId}?categoriaId=${categoriaId}`);
return response.data;
};
export const getResultadosPorMunicipioYCategoria = async (municipioId: string, categoriaId: number): Promise<ResultadoTicker[]> => {
const response = await apiClient.get(`/resultados/partido/${municipioId}?categoriaId=${categoriaId}`);
return response.data.resultados;
};
export const getResultadosPorMunicipio = async (municipioId: string, categoriaId: number): Promise<ResultadoTicker[]> => {
const response = await apiClient.get(`/resultados/partido/${municipioId}?categoriaId=${categoriaId}`);
// La respuesta es un objeto, nosotros extraemos el array de resultados
export const getResultadosPorMunicipio = async (eleccionId: number, municipioId: string, categoriaId: number): Promise<ResultadoTicker[]> => {
const response = await apiClient.get(`/elecciones/${eleccionId}/partido/${municipioId}?categoriaId=${categoriaId}`);
return response.data.resultados;
};
@@ -213,4 +207,18 @@ export const getRankingMunicipiosPorSeccion = async (seccionId: string): Promise
export const getEstablecimientosPorMunicipio = async (municipioId: string): Promise<CatalogoItem[]> => {
const response = await apiClient.get(`/catalogos/establecimientos-por-municipio/${municipioId}`);
return response.data;
};
export const getPanelElectoral = async (eleccionId: number, ambitoId: string | null, categoriaId: number): Promise<PanelElectoralDto> => {
// Construimos la URL base
let url = ambitoId
? `/elecciones/${eleccionId}/panel/${ambitoId}`
: `/elecciones/${eleccionId}/panel`;
// Añadimos categoriaId como un query parameter
url += `?categoriaId=${categoriaId}`;
const { data } = await apiClient.get(url);
return data;
};

View File

@@ -1,23 +1,23 @@
// src/components/DevApp.tsx
import { BancasWidget } from './BancasWidget'
import { CongresoWidget } from './CongresoWidget'
import MapaBsAs from './MapaBsAs'
import { DipSenTickerWidget } from './DipSenTickerWidget'
import { TelegramaWidget } from './TelegramaWidget'
import { ConcejalesWidget } from './ConcejalesWidget'
import MapaBsAsSecciones from './MapaBsAsSecciones'
import { SenadoresWidget } from './SenadoresWidget'
import { DiputadosWidget } from './DiputadosWidget'
import { ResumenGeneralWidget } from './ResumenGeneralWidget'
import { SenadoresTickerWidget } from './SenadoresTickerWidget'
import { DiputadosTickerWidget } from './DiputadosTickerWidget'
import { ConcejalesTickerWidget } from './ConcejalesTickerWidget'
import { DiputadosPorSeccionWidget } from './DiputadosPorSeccionWidget'
import { SenadoresPorSeccionWidget } from './SenadoresPorSeccionWidget'
import { ConcejalesPorSeccionWidget } from './ConcejalesPorSeccionWidget'
import { ResultadosTablaDetalladaWidget } from './ResultadosTablaDetalladaWidget'
import { ResultadosRankingMunicipioWidget } from './ResultadosRankingMunicipioWidget'
import '../App.css';
// src/components/common/DevApp.tsx
import { BancasWidget } from '../../features/legislativas/provinciales/BancasWidget'
import { CongresoWidget } from '../../features/legislativas/provinciales/CongresoWidget'
import MapaBsAs from '../../features/legislativas/provinciales/MapaBsAs'
import { DipSenTickerWidget } from '../../features/legislativas/provinciales/DipSenTickerWidget'
import { TelegramaWidget } from '../../features/legislativas/provinciales/TelegramaWidget'
import { ConcejalesWidget } from '../../features/legislativas/provinciales/ConcejalesWidget'
import MapaBsAsSecciones from '../../features/legislativas/provinciales/MapaBsAsSecciones'
import { SenadoresWidget } from '../../features/legislativas/provinciales/SenadoresWidget'
import { DiputadosWidget } from '../../features/legislativas/provinciales/DiputadosWidget'
import { ResumenGeneralWidget } from '../../features/legislativas/provinciales/ResumenGeneralWidget'
import { SenadoresTickerWidget } from '../../features/legislativas/provinciales/SenadoresTickerWidget'
import { DiputadosTickerWidget } from '../../features/legislativas/provinciales/DiputadosTickerWidget'
import { ConcejalesTickerWidget } from '../../features/legislativas/provinciales/ConcejalesTickerWidget'
import { DiputadosPorSeccionWidget } from '../../features/legislativas/provinciales/DiputadosPorSeccionWidget'
import { SenadoresPorSeccionWidget } from '../../features/legislativas/provinciales/SenadoresPorSeccionWidget'
import { ConcejalesPorSeccionWidget } from '../../features/legislativas/provinciales/ConcejalesPorSeccionWidget'
import { ResultadosTablaDetalladaWidget } from '../../features/legislativas/provinciales/ResultadosTablaDetalladaWidget'
import { ResultadosRankingMunicipioWidget } from '../../features/legislativas/provinciales/ResultadosRankingMunicipioWidget'
import '../../App.css';
export const DevApp = () => {

View File

@@ -1,4 +1,4 @@
// src/components/ImageWithFallback.tsx
// src/components/common/ImageWithFallback.tsx
import { useState, useEffect } from 'react';
interface Props extends React.ImgHTMLAttributes<HTMLImageElement> {

View File

@@ -1,6 +1,6 @@
// src/components/ParliamentLayout.tsx
// src/components/common/ParliamentLayout.tsx
import React, { useLayoutEffect } from 'react';
import { assetBaseUrl } from '../apiService';
import { assetBaseUrl } from '../../apiService';
import { handleImageFallback } from './imageFallback';
// Interfaces (no cambian)

View File

@@ -1,7 +1,7 @@
// src/components/SenateLayout.tsx
// src/components/common/SenateLayout.tsx
import React, { useLayoutEffect } from 'react';
import { handleImageFallback } from './imageFallback';
import { assetBaseUrl } from '../apiService';
import { assetBaseUrl } from '../../apiService';
// Interfaces
interface SeatFillData {

View File

@@ -1,4 +1,4 @@
// src/components/imageFallback.ts
// src/components/common/imageFallback.ts
export function handleImageFallback(selector: string, fallbackImageUrl: string) {
// Le decimos a TypeScript que el resultado será una lista de elementos de imagen HTML

View File

@@ -0,0 +1,15 @@
// src/features/legislativas/rovinciales/DevAppLegislativas.tsx
import { PanelNacionalWidget } from './nacionales/PanelNacionalWidget';
import './DevAppStyle.css'
export const DevAppLegislativas = () => {
return (
<div className="container">
<h1>Il visualizzatore di widget - Elecciones Nacionales 2025</h1>
{/* Le pasamos el ID de la elección que queremos visualizar.
Para tus datos de prueba provinciales, este ID es 1. */}
<PanelNacionalWidget eleccionId={2} />
</div>
);
};

View File

@@ -0,0 +1,3 @@
.container{
text-align: center;
}

View File

@@ -0,0 +1,327 @@
/* src/features/legislativas/nacionales/PanelNaciona.css */
.panel-nacional-container {
font-family: 'Roboto', sans-serif;
max-width: 1200px;
margin: auto;
border: 1px solid #e0e0e0;
border-radius: 8px;
}
.panel-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid #e0e0e0;
}
/* Nuevo contenedor para alinear título y selector */
.header-top-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.panel-header h1 {
font-size: 1.5rem;
margin: 0;
}
.categoria-selector {
min-width: 220px;
/* Ancho del selector */
}
.breadcrumbs {
font-size: 0.9rem;
color: #666;
}
.breadcrumb-link {
background: none;
border: none;
color: #007bff;
cursor: pointer;
padding: 0;
}
.breadcrumb-separator {
margin: 0 0.5rem;
}
.panel-main-content {
display: flex;
height: 75vh;
min-height: 500px;
transition: all 0.5s ease-in-out;
}
/* Columna del mapa */
.mapa-column {
flex: 2; /* Por defecto, ocupa 2/3 del espacio */
position: relative;
transition: flex 0.5s ease-in-out;
}
/* Columna de resultados */
.resultados-column {
flex: 1; /* Por defecto, ocupa 1/3 */
overflow-y: auto;
padding: 1.5rem;
transition: all 0.5s ease-in-out;
min-width: 320px; /* Un ancho mínimo para que no se comprima demasiado */
}
/* --- NUEVOS ESTILOS --- */
.mapa-componente-container {
width: 100%;
height: 100%;
position: relative;
}
.mapa-volver-btn {
position: absolute;
top: 10px;
left: 10px;
z-index: 10;
padding: 8px 12px;
background-color: white;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.partido-fila {
display: flex;
align-items: center;
margin-bottom: 1rem;
gap: 1rem; /* Añade un espacio entre logo, info y stats */
}
.partido-logo {
flex-shrink: 0;
width: 48px;
height: 48px;
}
.partido-logo img {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 50%;
}
.partido-info-wrapper {
flex-grow: 1; /* Permite que esta sección crezca */
flex-shrink: 1; /* Permite que se encoja si es necesario */
min-width: 0; /* <-- TRUCO CLAVE DE FLEXBOX para que text-overflow funcione */
}
.partido-nombre {
font-weight: 500;
display: block;
white-space: nowrap; /* <-- No permitir que el texto salte de línea */
overflow: hidden; /* <-- Ocultar el texto que se desborda */
text-overflow: ellipsis; /* <-- Añadir "..." al final */
}
.candidato-nombre {
font-size: 0.85rem;
color: #666;
display: block;
}
.partido-barra-background {
height: 15px;
background-color: #f0f0f0;
border-radius: 5px;
margin-top: 4px;
}
.partido-barra-foreground {
height: 100%;
border-radius: 4px;
transition: width 0.5s ease-in-out;
}
.partido-stats {
flex-shrink: 0; /* <-- MUY IMPORTANTE: Evita que este bloque se encoja */
text-align: right;
min-width: 100px; /* Asegura que siempre tenga espacio suficiente */
}
.partido-porcentaje {
font-size: 1.2rem;
font-weight: 700;
display: block;
}
.partido-votos {
font-size: 0.8rem;
color: #666;
display: block;
}
.panel-estado-recuento {
margin-top: auto;
padding-top: 1rem;
border-top: 1px solid #e0e0e0;
display: flex;
justify-content: space-around;
}
.estado-item {
text-align: center;
}
.estado-item span {
font-size: 0.8rem;
color: #666;
display: block;
}
.estado-item strong {
font-size: 1.2rem;
color: #333;
}
.rsm-zoomable-group {
transition: transform 0.75s ease-in-out;
}
* Contenedor principal del contenido */
.panel-main-content {
display: flex;
height: 70vh;
min-height: 500px;
transition: all 0.5s ease-in-out; /* Transición suave para el layout */
}
/* Columna del mapa */
.mapa-column {
flex: 2; /* Por defecto, ocupa 2/3 del espacio */
position: relative;
transition: flex 0.5s ease-in-out;
}
/* Columna de resultados */
.resultados-column {
flex: 1; /* Por defecto, ocupa 1/3 */
overflow-y: auto;
padding: 1.5rem;
transition: all 0.5s ease-in-out;
min-width: 320px; /* Un ancho mínimo para que no se comprima demasiado */
}
/* --- ESTADO COLAPSADO --- */
/* Cuando el panel principal tiene la clase 'panel-collapsed' */
.panel-main-content.panel-collapsed .mapa-column {
flex: 1 1 100%; /* El mapa ocupa todo el ancho */
}
.panel-main-content.panel-collapsed .resultados-column {
flex-basis: 0;
min-width: 0;
max-width: 0;
padding: 0;
overflow: hidden; /* Oculta el contenido para que no se desborde */
}
/* --- Estilo del botón para colapsar --- */
.panel-toggle-btn {
position: absolute;
top: 50%;
right: 10px;
transform: translateY(-50%);
z-index: 10;
width: 30px;
height: 50px;
border: 1px solid #ccc;
background-color: white;
border-radius: 4px 0 0 4px;
cursor: pointer;
font-size: 1.5rem;
font-weight: bold;
color: #555;
display: flex;
align-items: center;
justify-content: center;
box-shadow: -2px 0 5px rgba(0,0,0,0.1);
transition: background-color 0.2s;
}
.panel-toggle-btn:hover {
background-color: #f0f0f0;
}
.rsm-geography {
cursor: pointer;
stroke: #000000;
stroke-width: 0.2px;
outline: none;
transition: filter 0.2s ease-in-out, stroke 0.2s ease-in-out, stroke-width 0.2s ease-in-out;
}
/* --- ESTADO HOVER (Sutil) --- */
/* Se aplica solo si la geografía NO está seleccionada */
.rsm-geography:not(.selected):hover {
filter: brightness(1.10);
stroke: #0000ff;
stroke-width: 0.5px;
}
/* --- ESTADO SELECCIONADO (Foco) --- */
/* Clase que añadiremos desde React para el municipio en foco */
.rsm-geography.selected {
stroke: #0000ff; /* Borde negro para el seleccionado */
stroke-width: 0.5px; /* <-- Borde más grueso para destacar */
filter: none; /* Quitamos cualquier otro filtro para que se vea nítido */
pointer-events: none; /* Desactivamos eventos para que no interfiera el hover */
}
/* Reglas para los mapas atenuados (sin cambios) */
.rsm-geography-faded,
.rsm-geography-faded-municipality {
opacity: 0.3;
pointer-events: none;
}
.rsm-geography-faded:hover,
.rsm-geography-faded-municipality:hover {
filter: none;
stroke: #FFFFFF;
stroke-width: 0.5px;
}
.partido-barra-foreground {
height: 100%;
border-radius: 4px;
transition: width 0.5s ease-in-out;
}
/* Spinner para la transición entre mapas */
.transition-spinner {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.5); /* Fondo blanco semitransparente */
z-index: 20;
display: flex;
align-items: center;
justify-content: center;
}
/* Estilo del spinner en sí mismo */
.transition-spinner::after {
content: '';
width: 50px;
height: 50px;
border: 5px solid rgba(0, 0, 0, 0.2);
border-top-color: #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}

View File

@@ -0,0 +1,135 @@
// src/features/legislativas/nacionales/PanelNacionalWidget.tsx
import { useMemo, useState, Suspense } from 'react';
import { useSuspenseQuery } from '@tanstack/react-query'; // <-- CAMBIO CLAVE
import { getPanelElectoral } from '../../../apiService';
import { MapaNacional } from './components/MapaNacional';
import { PanelResultados } from './components/PanelResultados';
import { Breadcrumbs } from './components/Breadcrumbs';
import './PanelNacional.css';
import Select from 'react-select';
import type { PanelElectoralDto } from '../../../types/types';
interface PanelNacionalWidgetProps {
eleccionId: number;
}
type AmbitoState = {
id: string | null;
nivel: 'pais' | 'provincia' | 'municipio';
nombre: string;
provinciaNombre?: string;
provinciaDistritoId?: string | null;
};
const CATEGORIAS_NACIONALES = [
{ value: 2, label: 'Diputados Nacionales' },
{ value: 1, label: 'Senadores Nacionales' },
];
// Creamos un componente interno para poder usar Suspense correctamente
const PanelContenido = ({ eleccionId, ambitoActual, categoriaId }: { eleccionId: number, ambitoActual: AmbitoState, categoriaId: number }) => {
// Este hook ahora suspenderá el renderizado si los datos no están listos
const { data } = useSuspenseQuery<PanelElectoralDto>({
queryKey: ['panelElectoral', eleccionId, ambitoActual.id, categoriaId],
queryFn: () => getPanelElectoral(eleccionId, ambitoActual.id, categoriaId),
});
return (
<PanelResultados
resultados={data.resultadosPanel}
estadoRecuento={data.estadoRecuento}
/>
);
};
export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) => {
const [ambitoActual, setAmbitoActual] = useState<AmbitoState>({ id: null, nivel: 'pais', nombre: 'Argentina', provinciaDistritoId: null });
const [categoriaId, setCategoriaId] = useState<number>(2);
const [isPanelOpen, setIsPanelOpen] = useState(true);
const handleAmbitoSelect = (nuevoAmbitoId: string, nuevoNivel: 'provincia' | 'municipio', nuevoNombre: string) => {
setAmbitoActual(prev => ({
id: nuevoAmbitoId,
nivel: nuevoNivel,
nombre: nuevoNombre,
provinciaNombre: nuevoNivel === 'municipio' ? prev.nombre : (nuevoNivel === 'provincia' ? nuevoNombre : undefined),
provinciaDistritoId: nuevoNivel === 'provincia' ? nuevoAmbitoId : prev.provinciaDistritoId
}));
};
const handleResetToPais = () => {
setAmbitoActual({ id: null, nivel: 'pais', nombre: 'Argentina', provinciaDistritoId: null });
};
const handleVolverAProvincia = () => {
if (ambitoActual.provinciaDistritoId && ambitoActual.provinciaNombre) {
setAmbitoActual({
id: ambitoActual.provinciaDistritoId,
nivel: 'provincia',
nombre: ambitoActual.provinciaNombre,
provinciaDistritoId: ambitoActual.provinciaDistritoId
});
} else {
handleResetToPais();
}
};
const selectedCategoria = useMemo(() =>
CATEGORIAS_NACIONALES.find(c => c.value === categoriaId),
[categoriaId]
);
return (
<div className="panel-nacional-container">
<header className="panel-header">
<div className="header-top-row">
<h1>Resultados elecciones {ambitoActual.nombre}</h1>
<Select
options={CATEGORIAS_NACIONALES}
value={selectedCategoria}
onChange={(option) => option && setCategoriaId(option.value)}
className="categoria-selector"
/>
</div>
<Breadcrumbs
nivel={ambitoActual.nivel}
nombreAmbito={ambitoActual.nombre}
nombreProvincia={ambitoActual.provinciaNombre}
onReset={handleResetToPais}
onVolverProvincia={handleVolverAProvincia}
/>
</header>
<main className={`panel-main-content ${!isPanelOpen ? 'panel-collapsed' : ''}`}>
<div className="mapa-column">
<button
className="panel-toggle-btn"
onClick={() => setIsPanelOpen(!isPanelOpen)}
title={isPanelOpen ? "Ocultar panel" : "Mostrar panel"}
>
{isPanelOpen ? '' : ''}
</button>
<Suspense fallback={<div className="spinner" />}>
<MapaNacional
eleccionId={eleccionId}
categoriaId={categoriaId}
nivel={ambitoActual.nivel}
nombreAmbito={ambitoActual.nombre}
provinciaDistritoId={ambitoActual.provinciaDistritoId ?? null}
onAmbitoSelect={handleAmbitoSelect}
onVolver={ambitoActual.nivel === 'municipio' ? handleVolverAProvincia : handleResetToPais}
/>
</Suspense>
</div>
<div className="resultados-column">
<Suspense fallback={<div className="spinner" />}>
<PanelContenido
eleccionId={eleccionId}
ambitoActual={ambitoActual}
categoriaId={categoriaId}
/>
</Suspense>
</div>
</main>
</div>
);
};

View File

@@ -0,0 +1,12 @@
// src/features/legislativas/nacionales/components/AnimatedNumber.tsx
import { useAnimatedNumber } from '../hooks/useAnimatedNumber';
interface AnimatedNumberProps {
value: number;
formatter: (value: number) => string;
}
export const AnimatedNumber = ({ value, formatter }: AnimatedNumberProps) => {
const animatedValue = useAnimatedNumber(value);
return <span>{formatter(animatedValue)}</span>;
};

View File

@@ -0,0 +1,28 @@
// src/features/legislativas/nacionales/components/Breadcrumbs.tsx
interface BreadcrumbsProps {
nivel: 'pais' | 'provincia' | 'municipio';
nombreAmbito: string;
nombreProvincia?: string;
onReset: () => void;
onVolverProvincia: () => void;
}
export const Breadcrumbs = ({ nivel, nombreAmbito, nombreProvincia, onReset, onVolverProvincia }: BreadcrumbsProps) => {
return (
<div className="breadcrumbs">
{nivel !== 'pais' && (
<>
<button onClick={onReset} className="breadcrumb-link">Argentina</button>
<span className="breadcrumb-separator">{'>'}</span>
</>
)}
{nivel === 'municipio' && nombreProvincia && (
<>
<button onClick={onVolverProvincia} className="breadcrumb-link">{nombreProvincia}</button>
<span className="breadcrumb-separator">{'>'}</span>
</>
)}
<span className="breadcrumb-actual">{nombreAmbito}</span>
</div>
);
};

View File

@@ -0,0 +1,104 @@
// 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>
);
};

View File

@@ -0,0 +1,82 @@
// src/features/legislativas/nacionales/components/MapaProvincial.tsx
import axios from 'axios';
import { useEffect } from 'react';
import { useSuspenseQuery } from '@tanstack/react-query';
import { Geographies, Geography } from 'react-simple-maps';
import { geoCentroid } from 'd3-geo';
import { feature } from 'topojson-client';
import { API_BASE_URL, assetBaseUrl } from '../../../../apiService';
import type { ResultadoMapaDto, AmbitoGeography } from '../../../../types/types';
const DEFAULT_MAP_COLOR = '#E0E0E0';
const normalizarTexto = (texto: string = ''): string => texto.trim().toUpperCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
type PointTuple = [number, number];
interface MapaProvincialProps {
eleccionId: number;
categoriaId: number;
distritoId: string;
nombreProvincia: string;
nombreMunicipioSeleccionado: string | null;
nivel: 'provincia' | 'municipio';
onMunicipioSelect: (ambitoId: string, nombre: string) => void;
onCalculatedCenter: (center: PointTuple, zoom: number) => void;
}
export const MapaProvincial = ({ eleccionId, categoriaId, distritoId, nombreProvincia, nombreMunicipioSeleccionado, nivel, onMunicipioSelect, onCalculatedCenter }: MapaProvincialProps) => {
const { data: mapaData = [] } = useSuspenseQuery<ResultadoMapaDto[]>({
queryKey: ['mapaResultados', eleccionId, categoriaId, distritoId],
queryFn: async () => {
const url = `${API_BASE_URL}/elecciones/${eleccionId}/mapa-resultados?categoriaId=${categoriaId}&distritoId=${distritoId}`;
const response = await axios.get(url);
return response.data;
},
});
// El nombre del archivo ahora es completamente dinámico
const { data: geoData } = useSuspenseQuery<any>({
queryKey: ['geoDataProvincial', nombreProvincia],
queryFn: async () => {
const nombreNormalizado = nombreProvincia.toLowerCase().replace(/ /g, '_');
const mapFile = `departamentos-${nombreNormalizado}.topojson`;
return axios.get(`${assetBaseUrl}/maps/${mapFile}`).then(res => res.data);
},
});
// useEffect para calcular y "exportar" la posición del municipio al padre
useEffect(() => {
if (nivel === 'municipio' && geoData?.objects && nombreMunicipioSeleccionado) {
const geometries = geoData.objects[Object.keys(geoData.objects)[0]].geometries;
const municipioGeo = geometries.find((g: any) => normalizarTexto(g.properties.departamento) === normalizarTexto(nombreMunicipioSeleccionado));
if (municipioGeo) {
const municipioFeature = feature(geoData, municipioGeo);
const centroid = geoCentroid(municipioFeature);
// Usamos un zoom genérico alto para cualquier municipio
onCalculatedCenter(centroid as PointTuple, 40);
}
}
}, [nivel, nombreMunicipioSeleccionado, geoData, onCalculatedCenter]);
const resultadosPorNombre = new Map<string, ResultadoMapaDto>(mapaData.map(d => [normalizarTexto(d.ambitoNombre), d]));
return (
<Geographies geography={geoData}>
{({ geographies }: { geographies: AmbitoGeography[] }) => geographies.map((geo) => {
const resultado = resultadosPorNombre.get(normalizarTexto(geo.properties.departamento));
const esSeleccionado = nombreMunicipioSeleccionado ? normalizarTexto(geo.properties.departamento) === normalizarTexto(nombreMunicipioSeleccionado) : false;
return (
<Geography
key={geo.rsmKey}
geography={geo}
className={`rsm-geography ${esSeleccionado ? 'selected' : ''} ${nombreMunicipioSeleccionado && !esSeleccionado ? 'rsm-geography-faded-municipality' : ''}`}
fill={resultado?.colorGanador || DEFAULT_MAP_COLOR}
onClick={resultado ? () => onMunicipioSelect(resultado.ambitoId.toString(), resultado.ambitoNombre) : undefined}
data-tooltip-id="mapa-tooltip"
data-tooltip-content={geo.properties.departamento}
/>
);
})}
</Geographies>
);
};

View File

@@ -0,0 +1,55 @@
// src/features/legislativas/nacionales/components/PanelResultados.tsx
import type { ResultadoTicker, EstadoRecuentoTicker } from '../../../../types/types';
import { ImageWithFallback } from '../../../../components/common/ImageWithFallback';
import { assetBaseUrl } from '../../../../apiService';
import { AnimatedNumber } from './AnimatedNumber';
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;
const formatVotes = (num: number) => Math.round(num).toLocaleString('es-AR');
interface PanelResultadosProps {
resultados: ResultadoTicker[];
estadoRecuento: EstadoRecuentoTicker;
}
export const PanelResultados = ({ resultados, estadoRecuento }: PanelResultadosProps) => {
return (
<div className="panel-resultados">
<div className="panel-partidos-container">
{resultados.map(partido => (
<div key={partido.id} className="partido-fila">
<div className="partido-logo">
<ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc={`${assetBaseUrl}/default-avatar.png`} alt={partido.nombre} />
</div>
<div className="partido-info-wrapper">
<span className="partido-nombre">{partido.nombreCorto || partido.nombre}</span>
{partido.nombreCandidato && <span className="candidato-nombre">{partido.nombreCandidato}</span>}
<div className="partido-barra-background">
<div className="partido-barra-foreground" style={{ width: `${partido.porcentaje}%`, backgroundColor: partido.color || '#888' }} />
</div>
</div>
<div className="partido-stats">
<span className="partido-porcentaje">
<AnimatedNumber value={partido.porcentaje} formatter={formatPercent} />
</span>
<span className="partido-votos">
<AnimatedNumber value={partido.votos} formatter={formatVotes} /> votos
</span>
</div>
</div>
))}
</div>
<div className="panel-estado-recuento">
<div className="estado-item">
<span>Participación</span>
<strong><AnimatedNumber value={estadoRecuento.participacionPorcentaje} formatter={formatPercent} /></strong>
</div>
<div className="estado-item">
<span>Mesas Escrutadas</span>
<strong><AnimatedNumber value={estadoRecuento.mesasTotalizadasPorcentaje} formatter={formatPercent} /></strong>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,44 @@
// src/features/legislativas/nacionales/components/hooks/useAnimatedNumber.ts
import { useState, useEffect, useRef } from 'react';
const easeOutQuad = (t: number) => t * (2 - t);
export const useAnimatedNumber = (
endValue: number,
duration: number = 700 // Duración de la animación en milisegundos
) => {
const [currentValue, setCurrentValue] = useState(endValue);
const previousValueRef = useRef(endValue);
useEffect(() => {
const startValue = previousValueRef.current;
let animationFrameId: number;
const startTime = Date.now();
const animate = () => {
const elapsedTime = Date.now() - startTime;
const progress = Math.min(elapsedTime / duration, 1);
const easedProgress = easeOutQuad(progress);
const newAnimatedValue = startValue + (endValue - startValue) * easedProgress;
setCurrentValue(newAnimatedValue);
if (progress < 1) {
animationFrameId = requestAnimationFrame(animate);
} else {
// Asegurarse de que el valor final sea exacto
setCurrentValue(endValue);
previousValueRef.current = endValue;
}
};
animationFrameId = requestAnimationFrame(animate);
return () => {
cancelAnimationFrame(animationFrameId);
previousValueRef.current = endValue;
};
}, [endValue, duration]);
return currentValue;
};

View File

@@ -1,4 +1,4 @@
/* src/components/BancasWidget.css
/* src/features/legislativas/rovinciales/BancasWidget.css
/* Contenedor principal del widget */
.bancas-widget-container {

View File

@@ -1,9 +1,9 @@
// src/components/BancasWidget.tsx (Corregido)
// src/features/legislativas/provinciales/BancasWidget.tsx (Corregido)
import { useState, useEffect, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import Select from 'react-select'; // --- CAMBIO: Importar react-select ---
import { getBancasPorSeccion, getSeccionesElectoralesConCargos } from '../apiService';
import type { ProyeccionBancas, MunicipioSimple } from '../types/types';
import { getBancasPorSeccion, getSeccionesElectoralesConCargos } from '../../../apiService';
import type { ProyeccionBancas, MunicipioSimple } from '../../../types/types';
import { Tooltip } from 'react-tooltip';
import './BancasWidget.css';
import type { Property } from 'csstype';

View File

@@ -1,10 +1,10 @@
// src/components/ConcejalesPorSeccionWidget.tsx
// src/features/legislativas/provinciales/ConcejalesPorSeccionWidget.tsx
import { useState, useMemo, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import Select from 'react-select';
import { getSeccionesElectorales, getResultadosPorSeccion, getConfiguracionPublica, assetBaseUrl } from '../apiService';
import type { MunicipioSimple, ResultadoTicker, ApiResponseResultadosPorSeccion } from '../types/types';
import { ImageWithFallback } from './ImageWithFallback';
import { getSeccionesElectorales, getResultadosPorSeccion, getConfiguracionPublica, assetBaseUrl } from '../../../apiService';
import type { MunicipioSimple, ResultadoTicker, ApiResponseResultadosPorSeccion } from '../../../types/types';
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
import './TickerWidget.css'; // Reutilizamos los estilos del ticker
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;

View File

@@ -1,9 +1,9 @@
// src/components/ConcejalesTickerWidget.tsx
// src/features/legislativas/provinciales/ConcejalesTickerWidget.tsx
import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { getResumenProvincial, getConfiguracionPublica, assetBaseUrl } from '../apiService';
import type { CategoriaResumen, ResultadoTicker } from '../types/types';
import { ImageWithFallback } from './ImageWithFallback';
import { getResumenProvincial, getConfiguracionPublica, assetBaseUrl } from '../../../apiService';
import type { CategoriaResumen, ResultadoTicker } from '../../../types/types';
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
import './TickerWidget.css'; // Reutilizamos los mismos estilos
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;

View File

@@ -1,10 +1,10 @@
// src/components/ConcejalesWidget.tsx
// src/features/legislativas/provinciales/ConcejalesWidget.tsx
import { useState, useMemo, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import Select from 'react-select';
import { getMunicipios, getResultadosPorMunicipio, getConfiguracionPublica, assetBaseUrl } from '../apiService';
import type { MunicipioSimple, ResultadoTicker } from '../types/types';
import { ImageWithFallback } from './ImageWithFallback';
import { getMunicipios, getResultadosPorMunicipio, getConfiguracionPublica, assetBaseUrl } from '../../../apiService';
import type { MunicipioSimple, ResultadoTicker } from '../../../types/types';
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
import './TickerWidget.css';
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;

View File

@@ -1,4 +1,4 @@
/* src/components/CongresoWidget.css */
/* src/features/legislativas/provinciales/CongresoWidget.css */
.congreso-container {
display: flex;
/* Se reduce ligeramente el espacio entre el gráfico y el panel */

View File

@@ -1,28 +1,32 @@
// src/components/CongresoWidget.tsx
// src/features/legislativas/provinciales/CongresoWidget.tsx
import { useState, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { ParliamentLayout } from './ParliamentLayout';
import { SenateLayout } from './SenateLayout';
import { getComposicionCongreso, getBancadasDetalle } from '../apiService';
import type { ComposicionData, BancadaDetalle } from '../apiService';
import { ParliamentLayout } from '../../../components/common/ParliamentLayout';
import { SenateLayout } from '../../../components/common/SenateLayout';
import { getComposicionCongreso, getBancadasDetalle } from '../../../apiService';
import type { ComposicionData, BancadaDetalle } from '../../../apiService';
import { Tooltip } from 'react-tooltip';
import './CongresoWidget.css';
type CamaraType = 'diputados' | 'senadores';
const DEFAULT_COLOR = '#808080';
export const CongresoWidget = () => {
interface CongresoWidgetProps {
eleccionId: number;
}
export const CongresoWidget = ({ eleccionId }: CongresoWidgetProps) => {
const [camaraActiva, setCamaraActiva] = useState<CamaraType>('diputados');
const { data: composicionData, isLoading: isLoadingComposicion, error: errorComposicion } = useQuery<ComposicionData>({
queryKey: ['composicionCongreso'],
queryFn: getComposicionCongreso,
queryKey: ['composicionCongreso', eleccionId],
queryFn: () => getComposicionCongreso(eleccionId),
refetchInterval: 180000,
});
const { data: bancadasDetalle = [] } = useQuery<BancadaDetalle[]>({
queryKey: ['bancadasDetalle'],
queryFn: getBancadasDetalle,
queryKey: ['bancadasDetalle', eleccionId],
queryFn: () => getBancadasDetalle(eleccionId),
enabled: !!composicionData,
});

View File

@@ -1,8 +1,8 @@
// src/components/DipSenTickerWidget.tsx
// src/features/legislativas/provinciales/DipSenTickerWidget.tsx
import { useQuery } from '@tanstack/react-query';
import { getResumenProvincial, getConfiguracionPublica, assetBaseUrl } from '../apiService';
import type { CategoriaResumen, ResultadoTicker } from '../types/types';
import { ImageWithFallback } from './ImageWithFallback';
import { getResumenProvincial, getConfiguracionPublica, assetBaseUrl } from '../../../apiService';
import type { CategoriaResumen, ResultadoTicker } from '../../../types/types';
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
import './TickerWidget.css';
import { useMemo } from 'react';

View File

@@ -1,10 +1,10 @@
// src/components/DiputadosPorSeccionWidget.tsx
// src/features/legislativas/provinciales/DiputadosPorSeccionWidget.tsx
import { useState, useMemo, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import Select from 'react-select';
import { getSeccionesElectorales, getResultadosPorSeccion, getConfiguracionPublica, assetBaseUrl } from '../apiService';
import type { MunicipioSimple, ResultadoTicker, ApiResponseResultadosPorSeccion } from '../types/types';
import { ImageWithFallback } from './ImageWithFallback';
import { getSeccionesElectorales, getResultadosPorSeccion, getConfiguracionPublica, assetBaseUrl } from '../../../apiService';
import type { MunicipioSimple, ResultadoTicker, ApiResponseResultadosPorSeccion } from '../../../types/types';
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
import './TickerWidget.css';
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;

View File

@@ -1,9 +1,9 @@
// src/components/DiputadosTickerWidget.tsx
// src/features/legislativas/provinciales/DiputadosTickerWidget.tsx
import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { getResumenProvincial, getConfiguracionPublica, assetBaseUrl } from '../apiService';
import type { CategoriaResumen, ResultadoTicker } from '../types/types';
import { ImageWithFallback } from './ImageWithFallback';
import { getResumenProvincial, getConfiguracionPublica, assetBaseUrl } from '../../../apiService';
import type { CategoriaResumen, ResultadoTicker } from '../../../types/types';
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
import './TickerWidget.css';
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;

View File

@@ -1,10 +1,10 @@
// src/components/DiputadosWidget.tsx
// src/features/legislativas/provinciales/DiputadosWidget.tsx
import { useState, useMemo, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import Select from 'react-select';
import { getMunicipios, getResultadosPorMunicipio, getConfiguracionPublica, assetBaseUrl } from '../apiService';
import type { MunicipioSimple, ResultadoTicker } from '../types/types';
import { ImageWithFallback } from './ImageWithFallback';
import { getMunicipios, getResultadosPorMunicipio, getConfiguracionPublica, assetBaseUrl } from '../../../apiService';
import type { MunicipioSimple, ResultadoTicker } from '../../../types/types';
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
import './TickerWidget.css';
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;

View File

@@ -1,4 +1,4 @@
/* src/components/MapaBsAs.css */
/* src/features/legislativas/provinciales/MapaBsAs.css */
:root {
--primary-accent-color: #0073e6;
--background-panel-color: #ffffff;

View File

@@ -1,4 +1,4 @@
// src/components/MapaBsAs.tsx
// src/features/legislativas/provinciales/MapaBsAs.tsx
import { useState, useMemo, useCallback, useEffect } from 'react';
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
import { Tooltip } from 'react-tooltip';
@@ -7,7 +7,7 @@ import axios from 'axios';
import { feature } from 'topojson-client';
import type { Feature, Geometry } from 'geojson';
import { geoCentroid } from 'd3-geo';
import { API_BASE_URL, assetBaseUrl } from '../apiService';
import { API_BASE_URL, assetBaseUrl } from '../../../apiService';
import './MapaBsAs.css';
// --- Interfaces y Tipos ---

View File

@@ -1,12 +1,12 @@
// src/components/MapaBsAsSecciones.tsx
// src/features/legislativas/provinciales/MapaBsAsSecciones.tsx
import { useState, useMemo, useCallback, useEffect } from 'react';
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
import { Tooltip } from 'react-tooltip';
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
import { geoCentroid } from 'd3-geo';
import { getDetalleSeccion, API_BASE_URL, assetBaseUrl } from '../apiService';
import { type ResultadoDetalleSeccion } from '../apiService';
import { getDetalleSeccion, API_BASE_URL, assetBaseUrl } from '../../../apiService';
import { type ResultadoDetalleSeccion } from '../../../apiService';
import './MapaBsAs.css';
// --- Interfaces y Tipos ---

View File

@@ -1,8 +1,9 @@
// src/features/legislativas/provinciales/ResultaosRankingMunicipioWidget.tsx
import { useState, useMemo, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import Select from 'react-select';
import { getSeccionesElectorales, getRankingMunicipiosPorSeccion } from '../apiService';
import type { MunicipioSimple, ApiResponseRankingMunicipio, RankingPartido } from '../types/types';
import { getSeccionesElectorales, getRankingMunicipiosPorSeccion } from '../../../apiService';
import type { MunicipioSimple, ApiResponseRankingMunicipio, RankingPartido } from '../../../types/types';
import './ResultadosTablaSeccionWidget.css';
type DisplayMode = 'porcentaje' | 'votos' | 'ambos';

View File

@@ -1,8 +1,9 @@
// src/features/legislativas/provinciales/ResultadosTablaDetalladaWidget.tsx
import { useState, useMemo, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import Select from 'react-select';
import { getSeccionesElectorales, getResultadosTablaDetallada } from '../apiService';
import type { MunicipioSimple, ApiResponseTablaDetallada } from '../types/types';
import { getSeccionesElectorales, getResultadosTablaDetallada } from '../../../apiService';
import type { MunicipioSimple, ApiResponseTablaDetallada } from '../../../types/types';
import './ResultadosTablaSeccionWidget.css';
const customSelectStyles = {

View File

@@ -1,6 +1,4 @@
/* ==========================================================================
ResultadosTablaSeccionWidget.css
========================================================================== */
/* src/features/legislativas/provinciales/ResultadosTablaSeccionWidget.css */
.tabla-resultados-widget {
font-family: 'Roboto', sans-serif;

View File

@@ -1,9 +1,9 @@
// src/components/ResumenGeneralWidget.tsx
// src/features/legislativas/provinciales/ResumenGeneralWidget.tsx
import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { getResumenProvincial, getConfiguracionPublica, assetBaseUrl } from '../apiService';
import type { CategoriaResumen, ResultadoTicker } from '../types/types';
import { ImageWithFallback } from './ImageWithFallback';
import { getResumenProvincial, getConfiguracionPublica, assetBaseUrl } from '../../../apiService';
import type { CategoriaResumen, ResultadoTicker } from '../../../types/types';
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
import './TickerWidget.css';
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;

View File

@@ -1,10 +1,10 @@
// src/components/SenadoresPorSeccionWidget.tsx
// src/features/legislativas/provinciales/SenadoresPorSeccionWidget.tsx
import { useState, useMemo, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import Select from 'react-select';
import { getSeccionesElectorales, getResultadosPorSeccion, getConfiguracionPublica, assetBaseUrl } from '../apiService';
import type { MunicipioSimple, ResultadoTicker, ApiResponseResultadosPorSeccion } from '../types/types';
import { ImageWithFallback } from './ImageWithFallback';
import { getSeccionesElectorales, getResultadosPorSeccion, getConfiguracionPublica, assetBaseUrl } from '../../../apiService';
import type { MunicipioSimple, ResultadoTicker, ApiResponseResultadosPorSeccion } from '../../../types/types';
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
import './TickerWidget.css';
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;

View File

@@ -1,9 +1,9 @@
// src/components/SenadoresTickerWidget.tsx
// src/features/legislativas/provinciales/SenadoresTickerWidget.tsx
import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { getResumenProvincial, getConfiguracionPublica, assetBaseUrl } from '../apiService';
import type { CategoriaResumen, ResultadoTicker } from '../types/types';
import { ImageWithFallback } from './ImageWithFallback';
import { getResumenProvincial, getConfiguracionPublica, assetBaseUrl } from '../../../apiService';
import type { CategoriaResumen, ResultadoTicker } from '../../../types/types';
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
import './TickerWidget.css'; // Reutilizamos los mismos estilos
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;

View File

@@ -1,10 +1,10 @@
// src/components/SenadoresWidget.tsx
// src/features/legislativas/provinciales/SenadoresWidget.tsx
import { useState, useEffect, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import Select from 'react-select'; // Importamos react-select
import { getMunicipios, getResultadosPorMunicipio, getConfiguracionPublica, assetBaseUrl } from '../apiService'; // Usamos las funciones genéricas
import type { MunicipioSimple, ResultadoTicker } from '../types/types';
import { ImageWithFallback } from './ImageWithFallback';
import { getMunicipios, getResultadosPorMunicipio, getConfiguracionPublica, assetBaseUrl } from '../../../apiService'; // Usamos las funciones genéricas
import type { MunicipioSimple, ResultadoTicker } from '../../../types/types';
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
import './TickerWidget.css';
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;

View File

@@ -1,4 +1,4 @@
/* src/components/TelegramaWidget.css */
/* src/features/legislativas/provinciales/TelegramaWidget.css */
.telegrama-container {
background-color: #ffffff;
border: 1px solid #e0e0e0;

View File

@@ -1,4 +1,4 @@
// src/components/TelegramaWidget.tsx
// src/features/legislativas/provinciales/TelegramaWidget.tsx
import { useState, useEffect, useMemo } from 'react';
import Select, { type FilterOptionOption } from 'react-select'; // <-- Importar react-select
import {
@@ -8,8 +8,8 @@ import {
getMesasPorEstablecimiento,
getTelegramaPorId,
assetBaseUrl
} from '../apiService';
import type { TelegramaData, CatalogoItem } from '../types/types';
} from '../../../apiService';
import type { TelegramaData, CatalogoItem } from '../../../types/types';
import './TelegramaWidget.css';
import { pdfjs, Document, Page } from 'react-pdf';

View File

@@ -1,6 +1,4 @@
/* ==========================================================================
TickerWidget.css (Versión Mejorada y Responsiva)
========================================================================== */
/* src/features/legislativas/provinciales/TickerWidget.css */
/* --- Contenedor Principal del Widget --- */
.ticker-card {

View File

@@ -3,26 +3,27 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BancasWidget } from './components/BancasWidget'
import { CongresoWidget } from './components/CongresoWidget'
import MapaBsAs from './components/MapaBsAs'
import { DipSenTickerWidget } from './components/DipSenTickerWidget'
import { TelegramaWidget } from './components/TelegramaWidget'
import { ConcejalesWidget } from './components/ConcejalesWidget'
import MapaBsAsSecciones from './components/MapaBsAsSecciones'
import { SenadoresWidget } from './components/SenadoresWidget'
import { DiputadosWidget } from './components/DiputadosWidget'
import { ResumenGeneralWidget } from './components/ResumenGeneralWidget'
import { SenadoresTickerWidget } from './components/SenadoresTickerWidget'
import { DiputadosTickerWidget } from './components/DiputadosTickerWidget'
import { ConcejalesTickerWidget } from './components/ConcejalesTickerWidget'
import { DiputadosPorSeccionWidget } from './components/DiputadosPorSeccionWidget'
import { SenadoresPorSeccionWidget } from './components/SenadoresPorSeccionWidget'
import { ConcejalesPorSeccionWidget } from './components/ConcejalesPorSeccionWidget'
import { ResultadosTablaDetalladaWidget } from './components/ResultadosTablaDetalladaWidget';
import { ResultadosRankingMunicipioWidget } from './components/ResultadosRankingMunicipioWidget';
import { DevApp } from './components/DevApp';
import { BancasWidget } from './features/legislativas/provinciales/BancasWidget'
import { CongresoWidget } from './features/legislativas/provinciales/CongresoWidget'
import MapaBsAs from './features/legislativas/provinciales/MapaBsAs'
import { DipSenTickerWidget } from './features/legislativas/provinciales/DipSenTickerWidget'
import { TelegramaWidget } from './features/legislativas/provinciales/TelegramaWidget'
import { ConcejalesWidget } from './features/legislativas/provinciales/ConcejalesWidget'
import MapaBsAsSecciones from './features/legislativas/provinciales/MapaBsAsSecciones'
import { SenadoresWidget } from './features/legislativas/provinciales/SenadoresWidget'
import { DiputadosWidget } from './features/legislativas/provinciales/DiputadosWidget'
import { ResumenGeneralWidget } from './features/legislativas/provinciales/ResumenGeneralWidget'
import { SenadoresTickerWidget } from './features/legislativas/provinciales/SenadoresTickerWidget'
import { DiputadosTickerWidget } from './features/legislativas/provinciales/DiputadosTickerWidget'
import { ConcejalesTickerWidget } from './features/legislativas/provinciales/ConcejalesTickerWidget'
import { DiputadosPorSeccionWidget } from './features/legislativas/provinciales/DiputadosPorSeccionWidget'
import { SenadoresPorSeccionWidget } from './features/legislativas/provinciales/SenadoresPorSeccionWidget'
import { ConcejalesPorSeccionWidget } from './features/legislativas/provinciales/ConcejalesPorSeccionWidget'
import { ResultadosTablaDetalladaWidget } from './features/legislativas/provinciales/ResultadosTablaDetalladaWidget';
import { ResultadosRankingMunicipioWidget } from './features/legislativas/provinciales/ResultadosRankingMunicipioWidget';
//import { DevApp } from './components/common/DevApp';
import './index.css';
import { DevAppLegislativas } from './features/legislativas/DevAppLegislativas';
const queryClient = new QueryClient();
@@ -56,7 +57,8 @@ if (import.meta.env.DEV) {
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<DevApp />
<DevAppLegislativas />
{/* <DevApp /> */}
</QueryClientProvider>
</React.StrictMode>
);

View File

@@ -1,4 +1,17 @@
// src/types/types.ts
import type { Feature as GeoJsonFeature, Geometry } from 'geojson';
// Definimos nuestras propiedades personalizadas
export interface GeoProperties {
nombre: string;
id: string;
[key: string]: any; // Permite otras propiedades
}
// Nuestro tipo de geografía ahora se basa en el tipo estándar de GeoJSON
// y le añadimos la propiedad 'rsmKey' que 'react-simple-maps' añade.
export type AmbitoGeography = GeoJsonFeature<Geometry, GeoProperties> & { rsmKey: string };
// Tipos para la respuesta de la API de resultados por municipio
export interface AgrupacionResultadoDto {
@@ -33,13 +46,12 @@ export interface MapaDto {
export interface GeographyObject {
rsmKey: string;
properties: {
// CORRECCIÓN: Se cambia 'nombre' por 'NAME_2' para coincidir con el archivo topojson
NAME_2: string;
[key: string]: any; // Permite otras propiedades que puedan venir
[key: string]: any;
};
}
export interface MunicipioSimple { id: string; nombre: string; camarasDisponibles?: ('diputados' | 'senadores')[];}
export interface MunicipioSimple { id: string; nombre: string; camarasDisponibles?: ('diputados' | 'senadores')[]; }
export interface ResultadoTicker {
id: string;
@@ -53,8 +65,8 @@ export interface ResultadoTicker {
}
export interface EstadoRecuentoTicker {
mesasTotalizadasPorcentaje: number;
participacionPorcentaje: number;
mesasTotalizadasPorcentaje: number;
participacionPorcentaje: number;
}
export interface CategoriaResumen {
@@ -109,8 +121,8 @@ export interface CatalogoItem {
}
export interface ApiResponseResultadosPorSeccion {
ultimaActualizacion: string;
resultados: ResultadoTicker[];
ultimaActualizacion: string;
resultados: ResultadoTicker[];
}
export interface ResultadoTablaAgrupacion {
@@ -219,4 +231,18 @@ export interface RankingMunicipio {
export interface ApiResponseRankingMunicipio {
categorias: { id: number; nombre: string }[];
resultados: RankingMunicipio[];
}
export interface ResultadoMapaDto {
ambitoId: string;
ambitoNombre: string;
agrupacionGanadoraId: string;
colorGanador: string;
}
export interface PanelElectoralDto {
ambitoNombre: string;
mapaData: ResultadoMapaDto[];
resultadosPanel: ResultadoTicker[]; // Reutilizamos el tipo que ya tienes
estadoRecuento: EstadoRecuentoTicker; // Reutilizamos el tipo que ya tienes
}