Feat Prototipos Widgets y Fix Worker Telegramas
This commit is contained in:
@@ -1,42 +0,0 @@
|
|||||||
#root {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: 6em;
|
|
||||||
padding: 1.5em;
|
|
||||||
will-change: filter;
|
|
||||||
transition: filter 300ms;
|
|
||||||
}
|
|
||||||
.logo:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #646cffaa);
|
|
||||||
}
|
|
||||||
.logo.react:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes logo-spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
a:nth-of-type(2) .logo {
|
|
||||||
animation: logo-spin infinite 20s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.read-the-docs {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
// src/App.tsx
|
|
||||||
import 'react-tooltip/dist/react-tooltip.css';
|
|
||||||
import MapaBsAs from './components/MapaBsAs';
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
return (
|
|
||||||
<div style={{ width: '50%', margin: '2rem auto' }}>
|
|
||||||
<h1>Resultados Electorales - Provincia de Buenos Aires</h1>
|
|
||||||
<p>Pasa el ratón sobre un partido para ver el ganador. Haz clic para más detalles.</p>
|
|
||||||
|
|
||||||
<MapaBsAs /> {/* Ya no necesita un div contenedor aquí */}
|
|
||||||
|
|
||||||
{/* Próximo paso: Crear una Leyenda aquí */}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
/* src/components/MapaBsAs.css */
|
|
||||||
.mapa-wrapper {
|
|
||||||
display: flex;
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mapa-container {
|
|
||||||
flex: 3; /* El mapa ocupa 3/4 del espacio */
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 8px;
|
|
||||||
position: relative;
|
|
||||||
/* Proporción aproximada para la provincia de Bs As */
|
|
||||||
padding-top: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mapa-container .rsm-svg {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Efecto "lift" al pasar el ratón */
|
|
||||||
.rsm-geography {
|
|
||||||
transition: transform 0.2s ease-in-out, fill 0.2s ease-in-out;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.rsm-geography:hover {
|
|
||||||
transform: scale(1.03);
|
|
||||||
transform-origin: center center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-panel {
|
|
||||||
flex: 1; /* El panel ocupa 1/4 del espacio */
|
|
||||||
padding: 1rem;
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
.info-panel h3 {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legend {
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
}
|
|
||||||
.legend-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
.legend-color-box {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
margin-right: 10px;
|
|
||||||
border: 1px solid #fff;
|
|
||||||
outline: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
// src/components/MapaBsAs.tsx
|
|
||||||
import { useState, useMemo } from 'react';
|
|
||||||
import { ComposableMap, Geographies, Geography } from 'react-simple-maps';
|
|
||||||
import { Tooltip } from 'react-tooltip';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import axios from 'axios';
|
|
||||||
import type { Feature, Geometry } from 'geojson';
|
|
||||||
import { geoCentroid } from 'd3-geo'; // Para calcular el centro de cada partido
|
|
||||||
import { useSpring, animated } from 'react-spring'; // Para animar el zoom
|
|
||||||
|
|
||||||
//import geoUrl from '/partidos-bsas.topojson';
|
|
||||||
import './MapaBsAs.css';
|
|
||||||
|
|
||||||
// --- Interfaces y Tipos ---
|
|
||||||
interface ResultadoMapa {
|
|
||||||
partidoId: string;
|
|
||||||
agrupacionGanadoraId: string;
|
|
||||||
porcentajeGanador: number; // Nueva propiedad desde el backend
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Agrupacion {
|
|
||||||
id: string;
|
|
||||||
nombre: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PartidoProperties {
|
|
||||||
id: number;
|
|
||||||
departamento: string;
|
|
||||||
cabecera: string; // Asegúrate de que coincida con tu topojson
|
|
||||||
provincia: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type PartidoGeography = Feature<Geometry, PartidoProperties> & { rsmKey: string };
|
|
||||||
|
|
||||||
const PALETA_COLORES: { [key: string]: [number, number, number] } = {
|
|
||||||
'default': [214, 214, 218] // RGB para el color por defecto
|
|
||||||
};
|
|
||||||
|
|
||||||
const INITIAL_PROJECTION = {
|
|
||||||
center: [-59.8, -37.0] as [number, number],
|
|
||||||
scale: 5400,
|
|
||||||
};
|
|
||||||
|
|
||||||
const MapaBsAs = () => {
|
|
||||||
const [selectedPartido, setSelectedPartido] = useState<PartidoGeography | null>(null);
|
|
||||||
const [projectionConfig, setProjectionConfig] = useState(INITIAL_PROJECTION);
|
|
||||||
|
|
||||||
// --- Carga de Datos ---
|
|
||||||
const { data: resultadosData, isLoading: isLoadingResultados } = useQuery<ResultadoMapa[]>({
|
|
||||||
queryKey: ['mapaResultados'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await axios.get('http://localhost:5217/api/Resultados/mapa');
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: geoData, isLoading: isLoadingGeo } = useQuery({
|
|
||||||
queryKey: ['mapaGeoData'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await axios.get('/partidos-bsas.topojson');
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: agrupacionesData, isLoading: isLoadingAgrupaciones } = useQuery<Agrupacion[]>({
|
|
||||||
queryKey: ['catalogoAgrupaciones'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await axios.get('http://localhost:5217/api/Catalogos/agrupaciones');
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { nombresAgrupaciones, coloresPartidos } = useMemo(() => {
|
|
||||||
if (!agrupacionesData) return { nombresAgrupaciones: {}, coloresPartidos: {} };
|
|
||||||
|
|
||||||
const nombres = agrupacionesData.reduce((acc, agrupacion) => {
|
|
||||||
acc[agrupacion.id] = agrupacion.nombre;
|
|
||||||
return acc;
|
|
||||||
}, {} as { [key: string]: string });
|
|
||||||
|
|
||||||
const colores = agrupacionesData.reduce((acc, agrupacion, index) => {
|
|
||||||
const baseColor = [255, 87, 51, 51, 255, 87, 51, 87, 255, 255, 51, 161, 161, 51, 255, 255, 195, 0, 199, 0, 57, 144, 12, 63, 88, 24, 69];
|
|
||||||
acc[agrupacion.nombre] = [baseColor[index*3], baseColor[index*3+1], baseColor[index*3+2]];
|
|
||||||
return acc;
|
|
||||||
}, {} as { [key: string]: [number, number, number] });
|
|
||||||
|
|
||||||
colores['default'] = [214, 214, 218];
|
|
||||||
return { nombresAgrupaciones: nombres, coloresPartidos: colores };
|
|
||||||
}, [agrupacionesData]);
|
|
||||||
|
|
||||||
const animatedProps = useSpring({
|
|
||||||
to: { scale: projectionConfig.scale, cx: projectionConfig.center[0], cy: projectionConfig.center[1] },
|
|
||||||
config: { tension: 170, friction: 26 },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isLoadingResultados || isLoadingGeo || isLoadingAgrupaciones) {
|
|
||||||
return <div>Cargando datos del mapa...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPartyStyle = (partidoIdGeo: string) => {
|
|
||||||
const resultado = resultadosData?.find(r => r.partidoId === partidoIdGeo);
|
|
||||||
if (!resultado) {
|
|
||||||
return { fill: `rgb(${PALETA_COLORES.default.join(',')})` };
|
|
||||||
}
|
|
||||||
const nombreAgrupacion = nombresAgrupaciones[resultado.agrupacionGanadoraId] || 'Otro';
|
|
||||||
const baseColor = coloresPartidos[nombreAgrupacion] || PALETA_COLORES.default;
|
|
||||||
|
|
||||||
// Calcula la opacidad basada en el porcentaje. 0.4 (débil) a 1.0 (fuerte)
|
|
||||||
const opacity = 0.4 + (resultado.porcentajeGanador / 100) * 0.6;
|
|
||||||
|
|
||||||
return { fill: `rgba(${baseColor.join(',')}, ${opacity})` };
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGeographyClick = (geo: PartidoGeography) => {
|
|
||||||
const centroid = geoCentroid(geo);
|
|
||||||
setSelectedPartido(geo);
|
|
||||||
setProjectionConfig({
|
|
||||||
center: centroid,
|
|
||||||
scale: 18000, // Zoom más cercano
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
setSelectedPartido(null);
|
|
||||||
setProjectionConfig(INITIAL_PROJECTION);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mapa-wrapper">
|
|
||||||
<div className="mapa-container">
|
|
||||||
<animated.div style={{
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
transform: animatedProps.scale.to(s => `scale(${s / INITIAL_PROJECTION.scale})`),
|
|
||||||
}}>
|
|
||||||
<ComposableMap
|
|
||||||
projection="geoMercator"
|
|
||||||
projectionConfig={{
|
|
||||||
scale: animatedProps.scale,
|
|
||||||
center: [animatedProps.cx, animatedProps.cy],
|
|
||||||
}}
|
|
||||||
className="rsm-svg"
|
|
||||||
>
|
|
||||||
<Geographies geography={geoData}>
|
|
||||||
{({ geographies }: { geographies: PartidoGeography[] }) =>
|
|
||||||
geographies.map((geo) => {
|
|
||||||
const partidoId = String(geo.properties.id);
|
|
||||||
const partidoNombre = geo.properties.departamento;
|
|
||||||
const resultado = resultadosData?.find(r => r.partidoId === partidoId);
|
|
||||||
const agrupacionNombre = resultado ? (nombresAgrupaciones[resultado.agrupacionGanadoraId] || 'Desconocido') : 'Sin datos';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Geography
|
|
||||||
key={geo.rsmKey}
|
|
||||||
geography={geo}
|
|
||||||
data-tooltip-id="partido-tooltip"
|
|
||||||
data-tooltip-content={`${partidoNombre}: ${agrupacionNombre}`}
|
|
||||||
fill={getPartyStyle(partidoId).fill}
|
|
||||||
stroke="#FFF"
|
|
||||||
className="rsm-geography"
|
|
||||||
style={{
|
|
||||||
default: { outline: 'none' },
|
|
||||||
hover: { outline: 'none', stroke: '#FF5722', strokeWidth: 2, fill: getPartyStyle(partidoId).fill },
|
|
||||||
pressed: { outline: 'none' },
|
|
||||||
}}
|
|
||||||
onClick={() => handleGeographyClick(geo)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</Geographies>
|
|
||||||
</ComposableMap>
|
|
||||||
</animated.div>
|
|
||||||
<Tooltip id="partido-tooltip" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="info-panel">
|
|
||||||
<button onClick={handleReset}>Resetear Vista</button>
|
|
||||||
{selectedPartido ? (
|
|
||||||
<div>
|
|
||||||
<h3>{selectedPartido.properties.departamento}</h3>
|
|
||||||
<p><strong>Cabecera:</strong> {selectedPartido.properties.cabecera}</p>
|
|
||||||
<p><strong>ID:</strong> {selectedPartido.properties.id}</p>
|
|
||||||
{/* Aquí mostrarías más datos del partido seleccionado */}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
<h3>Provincia de Buenos Aires</h3>
|
|
||||||
<p>Selecciona un partido para ver más detalles.</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Legend colores={coloresPartidos} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// Componente de Leyenda separado
|
|
||||||
const Legend = ({ colores }: { colores: { [key: string]: [number, number, number] } }) => {
|
|
||||||
return (
|
|
||||||
<div className="legend">
|
|
||||||
<h4>Leyenda</h4>
|
|
||||||
{Object.entries(colores).map(([nombre, color]) => {
|
|
||||||
if (nombre === 'default') return null;
|
|
||||||
return (
|
|
||||||
<div key={nombre} className="legend-item">
|
|
||||||
<div className="legend-color-box" style={{ backgroundColor: `rgb(${color.join(',')})` }} />
|
|
||||||
<span>{nombre}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MapaBsAs;
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
:root {
|
|
||||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
color-scheme: light dark;
|
|
||||||
color: rgba(255, 255, 255, 0.87);
|
|
||||||
background-color: #242424;
|
|
||||||
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #646cff;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
min-width: 320px;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 3.2em;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
border-color: #646cff;
|
|
||||||
}
|
|
||||||
button:focus,
|
|
||||||
button:focus-visible {
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
:root {
|
|
||||||
color: #213547;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #747bff;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
// src/main.tsx
|
|
||||||
import React from 'react'
|
|
||||||
import ReactDOM from 'react-dom/client'
|
|
||||||
import App from './App.tsx'
|
|
||||||
import './index.css'
|
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
||||||
|
|
||||||
// Crea un cliente
|
|
||||||
const queryClient = new QueryClient()
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<App />
|
|
||||||
</QueryClientProvider>
|
|
||||||
</React.StrictMode>,
|
|
||||||
)
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,12 +0,0 @@
|
|||||||
// src/types/custom.d.ts
|
|
||||||
|
|
||||||
// Solución para el problema 1: Le dice a TypeScript que el módulo 'react-simple-maps' existe.
|
|
||||||
// Esto evita el error de "módulo no encontrado" y trata sus componentes como 'any'.
|
|
||||||
declare module 'react-simple-maps';
|
|
||||||
|
|
||||||
// Solución para el problema 2: Le dice a TypeScript cómo manejar la importación de archivos .topojson.
|
|
||||||
// Define que cuando importemos un archivo con esa extensión, el contenido será de tipo 'any'.
|
|
||||||
declare module '*.topojson' {
|
|
||||||
const value: any;
|
|
||||||
export default value;
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
// src/apiService.ts
|
|
||||||
import axios from 'axios';
|
|
||||||
import type { MunicipioSimple, MunicipioDetalle, ResumenProvincial, ProyeccionBancas } from './types';
|
|
||||||
|
|
||||||
// La URL base de tu API. En un proyecto real, esto iría en un archivo .env
|
|
||||||
const API_BASE_URL = 'http://localhost:5217/api'; // Ajusta el puerto si es necesario
|
|
||||||
|
|
||||||
const apiClient = axios.create({
|
|
||||||
baseURL: API_BASE_URL,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtiene los resultados para colorear el mapa inicial.
|
|
||||||
*/
|
|
||||||
export const getResultadosParaMapa = async (): Promise<any[]> => { // Usamos any[] temporalmente
|
|
||||||
const response = await apiClient.get('/resultados/mapa');
|
|
||||||
// Mapeamos la respuesta para que coincida con lo que el frontend espera
|
|
||||||
return response.data.map((item: any) => ({
|
|
||||||
municipioId: item.partidoId, // La propiedad en el frontend se llama municipioId
|
|
||||||
agrupacionGanadoraId: item.agrupacionGanadoraId,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtiene la lista de todos los municipios con sus IDs y nombres.
|
|
||||||
*/
|
|
||||||
export const getMunicipios = async (): Promise<MunicipioSimple[]> => {
|
|
||||||
const response = await apiClient.get('/catalogos/municipios');
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtiene el detalle completo de los resultados para un municipio específico.
|
|
||||||
*/
|
|
||||||
export const getDetallePorMunicipio = async (partidoId: string): Promise<MunicipioDetalle> => {
|
|
||||||
const response = await apiClient.get(`/resultados/partido/${partidoId}`);
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtiene el resumen de resultados a nivel provincial.
|
|
||||||
* El distritoId para la PBA es "02" según la estructura de la API.
|
|
||||||
*/
|
|
||||||
export const getResumenProvincial = async (): Promise<ResumenProvincial> => {
|
|
||||||
// Hardcodeamos el distritoId '02' para Buenos Aires
|
|
||||||
const response = await apiClient.get('/resultados/provincia/02');
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBancasPorSeccion = async (seccionId: string): Promise<ProyeccionBancas> => {
|
|
||||||
const response = await apiClient.get(`/resultados/bancas/${seccionId}`);
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
/* src/components/BancasWidget.css */
|
|
||||||
.bancas-widget-container {
|
|
||||||
background-color: #2a2a2e;
|
|
||||||
padding: 15px 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 20px auto;
|
|
||||||
color: #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bancas-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bancas-header h4 {
|
|
||||||
margin: 0;
|
|
||||||
color: white;
|
|
||||||
font-size: 1.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bancas-header select {
|
|
||||||
background-color: #3a3a3a;
|
|
||||||
color: white;
|
|
||||||
border: 1px solid #555;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 5px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.waffle-chart-container {
|
|
||||||
height: 300px;
|
|
||||||
font-family: system-ui, sans-serif;
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
// src/components/BancasWidget.tsx
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { ResponsiveWaffle } from '@nivo/waffle';
|
|
||||||
import { getBancasPorSeccion } from '../apiService';
|
|
||||||
import type { ProyeccionBancas } from '../types';
|
|
||||||
import './BancasWidget.css';
|
|
||||||
|
|
||||||
// Paleta de colores consistente
|
|
||||||
const NIVO_COLORS = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"];
|
|
||||||
|
|
||||||
// Las Secciones Electorales de la Provincia (esto podría venir de la API en el futuro)
|
|
||||||
const secciones = [
|
|
||||||
{ id: '1', nombre: 'Primera Sección' },
|
|
||||||
{ id: '2', nombre: 'Segunda Sección' },
|
|
||||||
{ id: '3', nombre: 'Tercera Sección' },
|
|
||||||
{ id: '4', nombre: 'Cuarta Sección' },
|
|
||||||
{ id: '5', nombre: 'Quinta Sección' },
|
|
||||||
{ id: '6', nombre: 'Sexta Sección' },
|
|
||||||
{ id: '7', nombre: 'Séptima Sección' },
|
|
||||||
{ id: '8', nombre: 'Octava Sección (Capital)' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const BancasWidget = () => {
|
|
||||||
const [seccionActual, setSeccionActual] = useState('1'); // Empezamos con la Primera Sección
|
|
||||||
const [data, setData] = useState<ProyeccionBancas | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const result = await getBancasPorSeccion(seccionActual);
|
|
||||||
setData(result);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error cargando datos de bancas para sección ${seccionActual}:`, error);
|
|
||||||
setData(null); // Limpiar datos en caso de error
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchData();
|
|
||||||
}, [seccionActual]); // Se ejecuta cada vez que cambia la sección
|
|
||||||
|
|
||||||
const waffleData = data?.proyeccion.map(p => ({
|
|
||||||
id: p.agrupacionNombre,
|
|
||||||
label: p.agrupacionNombre,
|
|
||||||
value: p.bancas,
|
|
||||||
})) || [];
|
|
||||||
|
|
||||||
const totalBancas = waffleData.reduce((sum, current) => sum + current.value, 0);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bancas-widget-container">
|
|
||||||
<div className="bancas-header">
|
|
||||||
<h4>Distribución de Bancas</h4>
|
|
||||||
<select value={seccionActual} onChange={e => setSeccionActual(e.target.value)}>
|
|
||||||
{secciones.map(s => <option key={s.id} value={s.id}>{s.nombre}</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="waffle-chart-container">
|
|
||||||
{loading ? <p>Cargando...</p> : !data ? <p>No hay datos disponibles para esta sección.</p> :
|
|
||||||
<ResponsiveWaffle
|
|
||||||
data={waffleData}
|
|
||||||
total={totalBancas}
|
|
||||||
rows={8}
|
|
||||||
columns={10}
|
|
||||||
fillDirection="bottom"
|
|
||||||
padding={3}
|
|
||||||
colors={NIVO_COLORS}
|
|
||||||
borderColor={{ from: 'color', modifiers: [['darker', 0.3]] }}
|
|
||||||
animate={true}
|
|
||||||
legends={[
|
|
||||||
{
|
|
||||||
anchor: 'bottom',
|
|
||||||
direction: 'row',
|
|
||||||
justify: false,
|
|
||||||
translateX: 0,
|
|
||||||
translateY: 40,
|
|
||||||
itemsSpacing: 4,
|
|
||||||
itemWidth: 100,
|
|
||||||
itemHeight: 20,
|
|
||||||
itemTextColor: '#999',
|
|
||||||
itemDirection: 'left-to-right',
|
|
||||||
symbolSize: 20,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
/* src/components/TickerWidget.css */
|
|
||||||
.ticker-container {
|
|
||||||
background-color: #2a2a2e;
|
|
||||||
padding: 15px 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 20px auto;
|
|
||||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
color: #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ticker-container.loading, .ticker-container.error {
|
|
||||||
text-align: center;
|
|
||||||
padding: 30px;
|
|
||||||
font-style: italic;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ticker-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: 1px solid #444;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ticker-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
color: white;
|
|
||||||
font-size: 1.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ticker-stats {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ticker-stats strong {
|
|
||||||
color: #a7c7e7;
|
|
||||||
font-size: 1.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ticker-results {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ticker-party .party-info {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ticker-party .party-name {
|
|
||||||
font-weight: 500;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
padding-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ticker-party .party-percent {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.party-bar-background {
|
|
||||||
background-color: #444;
|
|
||||||
border-radius: 4px;
|
|
||||||
height: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.party-bar-foreground {
|
|
||||||
background-color: #646cff;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: width 0.5s ease-in-out;
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
// src/components/TickerWidget.tsx
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { getResumenProvincial } from '../apiService';
|
|
||||||
import type { ResumenProvincial } from '../types';
|
|
||||||
import './TickerWidget.css';
|
|
||||||
|
|
||||||
const formatPercent = (num: number) => `${num.toFixed(2).replace('.', ',')}%`;
|
|
||||||
const NIVO_COLORS = [
|
|
||||||
"#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd",
|
|
||||||
"#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"
|
|
||||||
];
|
|
||||||
|
|
||||||
export const TickerWidget = () => {
|
|
||||||
const [data, setData] = useState<ResumenProvincial | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const result = await getResumenProvincial();
|
|
||||||
setData(result);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error cargando resumen provincial:", error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData(); // Carga inicial
|
|
||||||
const intervalId = setInterval(fetchData, 30000); // Actualiza cada 30 segundos
|
|
||||||
|
|
||||||
return () => clearInterval(intervalId); // Limpia el intervalo al desmontar el componente
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <div className="ticker-container loading">Cargando resultados provinciales...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return <div className="ticker-container error">No se pudieron cargar los datos.</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="ticker-container">
|
|
||||||
<div className="ticker-header">
|
|
||||||
<h3>{data.provinciaNombre}</h3>
|
|
||||||
<div className="ticker-stats">
|
|
||||||
<span>Mesas Escrutadas: <strong>{formatPercent(data.porcentajeEscrutado)}</strong></span>
|
|
||||||
<span>Participación: <strong>{formatPercent(data.porcentajeParticipacion)}</strong></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="ticker-results">
|
|
||||||
{data.resultados.slice(0, 3).map((partido, index) => (
|
|
||||||
<div key={`${partido.nombre}-${index}`} className="ticker-party"> {/* <-- CAMBIO AQUÍ */}
|
|
||||||
<div className="party-info">
|
|
||||||
<span className="party-name">{partido.nombre}</span>
|
|
||||||
<span className="party-percent">{formatPercent(partido.porcentaje)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="party-bar-background">
|
|
||||||
<div className="party-bar-foreground" style={{ width: `${partido.porcentaje}%`, backgroundColor: NIVO_COLORS[index % NIVO_COLORS.length] }}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
194
Elecciones-Web/frontend/package-lock.json
generated
194
Elecciones-Web/frontend/package-lock.json
generated
@@ -8,11 +8,13 @@
|
|||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nivo/waffle": "^0.99.0",
|
"@nivo/bar": "^0.99.0",
|
||||||
"@tanstack/react-query": "^5.85.5",
|
"@tanstack/react-query": "^5.85.5",
|
||||||
"@types/d3-geo": "^3.1.0",
|
"@types/d3-geo": "^3.1.0",
|
||||||
|
"@types/d3-shape": "^3.1.7",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"d3-geo": "^3.1.1",
|
"d3-geo": "^3.1.1",
|
||||||
|
"d3-shape": "^3.2.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-simple-maps": "github:ozimmortal/react-simple-maps#feat/react-19-support",
|
"react-simple-maps": "github:ozimmortal/react-simple-maps#feat/react-19-support",
|
||||||
@@ -1068,6 +1070,69 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nivo/annotations": {
|
||||||
|
"version": "0.99.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nivo/annotations/-/annotations-0.99.0.tgz",
|
||||||
|
"integrity": "sha512-jCuuXPbvpaqaz4xF7k5dv0OT2ubn5Nt0gWryuTe/8oVsC/9bzSuK8bM9vBty60m9tfO+X8vUYliuaCDwGksC2g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@nivo/colors": "0.99.0",
|
||||||
|
"@nivo/core": "0.99.0",
|
||||||
|
"@nivo/theming": "0.99.0",
|
||||||
|
"@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0",
|
||||||
|
"lodash": "^4.17.21"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.14 || ^17.0 || ^18.0 || ^19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nivo/axes": {
|
||||||
|
"version": "0.99.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nivo/axes/-/axes-0.99.0.tgz",
|
||||||
|
"integrity": "sha512-3KschnmEL0acRoa7INSSOSEFwJLm54aZwSev7/r8XxXlkgRBriu6ReZy/FG0wfN+ljZ4GMvx+XyIIf6kxzvrZg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@nivo/core": "0.99.0",
|
||||||
|
"@nivo/scales": "0.99.0",
|
||||||
|
"@nivo/text": "0.99.0",
|
||||||
|
"@nivo/theming": "0.99.0",
|
||||||
|
"@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0",
|
||||||
|
"@types/d3-format": "^1.4.1",
|
||||||
|
"@types/d3-time-format": "^2.3.1",
|
||||||
|
"d3-format": "^1.4.4",
|
||||||
|
"d3-time-format": "^3.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.14 || ^17.0 || ^18.0 || ^19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nivo/bar": {
|
||||||
|
"version": "0.99.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nivo/bar/-/bar-0.99.0.tgz",
|
||||||
|
"integrity": "sha512-9yfMn7H6UF/TqtCwVZ/vihVAXUff9wWvSaeF2Z1DCfgr5S07qs31Qb2p0LZA+YgCWpaU7zqkeb3VZ4WCpZbrDA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@nivo/annotations": "0.99.0",
|
||||||
|
"@nivo/axes": "0.99.0",
|
||||||
|
"@nivo/canvas": "0.99.0",
|
||||||
|
"@nivo/colors": "0.99.0",
|
||||||
|
"@nivo/core": "0.99.0",
|
||||||
|
"@nivo/legends": "0.99.0",
|
||||||
|
"@nivo/scales": "0.99.0",
|
||||||
|
"@nivo/text": "0.99.0",
|
||||||
|
"@nivo/theming": "0.99.0",
|
||||||
|
"@nivo/tooltip": "0.99.0",
|
||||||
|
"@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0",
|
||||||
|
"@types/d3-scale": "^4.0.8",
|
||||||
|
"@types/d3-shape": "^3.1.6",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
|
"d3-shape": "^3.2.0",
|
||||||
|
"lodash": "^4.17.21"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.14 || ^17.0 || ^18.0 || ^19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nivo/canvas": {
|
"node_modules/@nivo/canvas": {
|
||||||
"version": "0.99.0",
|
"version": "0.99.0",
|
||||||
"resolved": "https://registry.npmjs.org/@nivo/canvas/-/canvas-0.99.0.tgz",
|
"resolved": "https://registry.npmjs.org/@nivo/canvas/-/canvas-0.99.0.tgz",
|
||||||
@@ -1123,12 +1188,6 @@
|
|||||||
"react": "^16.14 || ^17.0 || ^18.0 || ^19.0"
|
"react": "^16.14 || ^17.0 || ^18.0 || ^19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nivo/grid": {
|
|
||||||
"version": "0.99.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@nivo/grid/-/grid-0.99.0.tgz",
|
|
||||||
"integrity": "sha512-mZiCa8PJ5j8Z7I9lwQvtBvSy++Oyaa/1ztYWQeTaoLxuoUmrpFpc4WK38lETLPzU32EwukZKFNkFp700itmaWw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@nivo/legends": {
|
"node_modules/@nivo/legends": {
|
||||||
"version": "0.99.0",
|
"version": "0.99.0",
|
||||||
"resolved": "https://registry.npmjs.org/@nivo/legends/-/legends-0.99.0.tgz",
|
"resolved": "https://registry.npmjs.org/@nivo/legends/-/legends-0.99.0.tgz",
|
||||||
@@ -1146,6 +1205,29 @@
|
|||||||
"react": "^16.14 || ^17.0 || ^18.0 || ^19.0"
|
"react": "^16.14 || ^17.0 || ^18.0 || ^19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nivo/scales": {
|
||||||
|
"version": "0.99.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nivo/scales/-/scales-0.99.0.tgz",
|
||||||
|
"integrity": "sha512-g/2K4L6L8si6E2BWAHtFVGahtDKbUcO6xHJtlIZMwdzaJc7yB16EpWLK8AfI/A42KadLhJSJqBK3mty+c7YZ+w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-interpolate": "^3.0.4",
|
||||||
|
"@types/d3-scale": "^4.0.8",
|
||||||
|
"@types/d3-time": "^1.1.1",
|
||||||
|
"@types/d3-time-format": "^3.0.0",
|
||||||
|
"d3-interpolate": "^3.0.1",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
|
"d3-time": "^1.0.11",
|
||||||
|
"d3-time-format": "^3.0.0",
|
||||||
|
"lodash": "^4.17.21"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nivo/scales/node_modules/@types/d3-time-format": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-or9DiDnYI1h38J9hxKEsw513+KVuFbEVhl7qdxcaudoiqWWepapUen+2vAriFGexr6W5+P4l9+HJrB39GG+oRg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@nivo/text": {
|
"node_modules/@nivo/text": {
|
||||||
"version": "0.99.0",
|
"version": "0.99.0",
|
||||||
"resolved": "https://registry.npmjs.org/@nivo/text/-/text-0.99.0.tgz",
|
"resolved": "https://registry.npmjs.org/@nivo/text/-/text-0.99.0.tgz",
|
||||||
@@ -1186,27 +1268,6 @@
|
|||||||
"react": "^16.14 || ^17.0 || ^18.0 || ^19.0"
|
"react": "^16.14 || ^17.0 || ^18.0 || ^19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nivo/waffle": {
|
|
||||||
"version": "0.99.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@nivo/waffle/-/waffle-0.99.0.tgz",
|
|
||||||
"integrity": "sha512-3JyC0O/dLjLVqYCigsCfreAgAqFkEFEdSq4MGVdSRnw2Huha6kx8/U3XTF6fRQemvTYnNpXybHd8rlddjtVE/Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@nivo/canvas": "0.99.0",
|
|
||||||
"@nivo/colors": "0.99.0",
|
|
||||||
"@nivo/core": "0.99.0",
|
|
||||||
"@nivo/grid": "0.99.0",
|
|
||||||
"@nivo/legends": "0.99.0",
|
|
||||||
"@nivo/theming": "0.99.0",
|
|
||||||
"@nivo/tooltip": "0.99.0",
|
|
||||||
"@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0",
|
|
||||||
"@types/d3-shape": "^3.1.6",
|
|
||||||
"d3-shape": "^3.2.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.14 || ^17.0 || ^18.0 || ^19.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@@ -1681,6 +1742,12 @@
|
|||||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3-format": {
|
||||||
|
"version": "1.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.5.tgz",
|
||||||
|
"integrity": "sha512-mLxrC1MSWupOSncXN/HOlWUAAIffAEBaI4+PKy2uMPsKe4FNZlk7qrbTjmzJXITQQqBHivaks4Td18azgqnotA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/d3-geo": {
|
"node_modules/@types/d3-geo": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
|
||||||
@@ -1690,6 +1757,15 @@
|
|||||||
"@types/geojson": "*"
|
"@types/geojson": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3-interpolate": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-color": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/d3-path": {
|
"node_modules/@types/d3-path": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||||
@@ -1721,9 +1797,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/d3-time": {
|
"node_modules/@types/d3-time": {
|
||||||
"version": "3.0.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-1.1.4.tgz",
|
||||||
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
"integrity": "sha512-JIvy2HjRInE+TXOmIGN5LCmeO0hkFZx5f9FZ7kiN+D+YTcc8pptsiLiuHsvwxwC7VVKmJ2ExHUgNlAiV7vQM9g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-time-format": {
|
||||||
|
"version": "2.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.3.4.tgz",
|
||||||
|
"integrity": "sha512-xdDXbpVO74EvadI3UDxjxTdR6QIxm1FKzEA/+F8tL4GWWUg/hgvBqf6chql64U5A9ZUGWo7pEu4eNlyLwbKdhg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
@@ -2478,6 +2560,18 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-scale/node_modules/d3-time": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/d3-selection": {
|
"node_modules/d3-selection": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||||
@@ -2500,16 +2594,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/d3-time": {
|
"node_modules/d3-time": {
|
||||||
"version": "3.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz",
|
||||||
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
"integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==",
|
||||||
"license": "ISC",
|
"license": "BSD-3-Clause"
|
||||||
"dependencies": {
|
|
||||||
"d3-array": "2 - 3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/d3-time-format": {
|
"node_modules/d3-time-format": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
@@ -2520,30 +2608,6 @@
|
|||||||
"d3-time": "1 - 2"
|
"d3-time": "1 - 2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/d3-time-format/node_modules/d3-array": {
|
|
||||||
"version": "2.12.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
|
|
||||||
"integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"internmap": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/d3-time-format/node_modules/d3-time": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"d3-array": "2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/d3-time-format/node_modules/internmap": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==",
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/d3-timer": {
|
"node_modules/d3-timer": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||||
|
|||||||
@@ -10,11 +10,13 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nivo/waffle": "^0.99.0",
|
"@nivo/bar": "^0.99.0",
|
||||||
"@tanstack/react-query": "^5.85.5",
|
"@tanstack/react-query": "^5.85.5",
|
||||||
"@types/d3-geo": "^3.1.0",
|
"@types/d3-geo": "^3.1.0",
|
||||||
|
"@types/d3-shape": "^3.1.7",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"d3-geo": "^3.1.1",
|
"d3-geo": "^3.1.1",
|
||||||
|
"d3-shape": "^3.2.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-simple-maps": "github:ozimmortal/react-simple-maps#feat/react-19-support",
|
"react-simple-maps": "github:ozimmortal/react-simple-maps#feat/react-19-support",
|
||||||
|
|||||||
96
Elecciones-Web/frontend/public/parliament-layout.svg
Normal file
96
Elecciones-Web/frontend/public/parliament-layout.svg
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<svg viewBox="0 0 550 375" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g transform="matrix(1, 0, 0, 1, 1.166021, 0.583011)">
|
||||||
|
<circle id="seat-0" r="9.964" cy="160.283" cx="-255.056" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||||
|
<circle id="seat-1" r="9.964" cy="114.844" cx="-252.766" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||||
|
<circle id="seat-2" r="9.964" cy="68.323" cx="-249.04" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||||
|
<circle id="seat-3" r="9.964" cy="167.585" cx="-230.037" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||||
|
<circle id="seat-4" r="9.964" cy="121.335" cx="-226.656" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||||
|
<circle id="seat-5" r="9.964" cy="74.003" cx="-224.493" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||||
|
<circle id="seat-6" r="9.964" cy="22.948" cx="-223.55" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||||
|
<circle id="seat-7" r="9.964" cy="183.347" cx="-208.602" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||||
|
<circle id="seat-8" r="9.964" cy="132.154" cx="-203.666" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||||
|
<circle id="seat-9" r="9.964" cy="82.117" cx="-199.609" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||||
|
<circle id="seat-10" r="9.964" cy="30.215" cx="-197.157" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||||
|
<circle id="seat-11" r="9.964" cy="145.407" cx="-182.84" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||||
|
<circle id="seat-12" r="9.964" cy="93.477" cx="-175.808" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||||
|
<circle id="seat-13" r="9.964" cy="39.969" cx="-171.147" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||||
|
<circle id="seat-14" r="9.964" cy="163.733" cx="-163.582" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||||
|
<circle id="seat-15" r="9.964" cy="108.082" cx="-153.63" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||||
|
<circle id="seat-16" r="9.964" cy="51.554" cx="-147.409" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||||
|
<circle id="seat-17" r="9.964" cy="125.122" cx="-134.967" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||||
|
<circle id="seat-18" r="9.964" cy="66.206" cx="-124.894" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||||
|
<circle id="seat-19" r="9.964" cy="143.999" cx="-118.288" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||||
|
<circle id="seat-20" r="9.964" cy="82.48" cx="-104.338" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||||
|
<circle id="seat-21" r="9.964" cy="102.267" cx="-85.471" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||||
|
<circle id="seat-22" r="9.964" cy="123.093" cx="-70.302" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||||
|
<circle id="seat-23" r="9.964" cy="280.15" cx="51.596" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-24" r="9.964" cy="238.303" cx="63.378" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-25" r="9.964" cy="187.919" cx="77.562" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-26" r="9.964" cy="137.535" cx="91.746" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-27" r="9.964" cy="291.519" cx="75.175" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-28" r="9.964" cy="246.564" cx="88.133" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-29" r="9.964" cy="197.635" cx="103.42" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-30" r="9.964" cy="145.84" cx="118.145" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-31" r="9.964" cy="307.801" cx="95.891" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-32" r="9.964" cy="259.444" cx="110.504" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-33" r="9.964" cy="209.251" cx="125.789" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-34" r="9.964" cy="157.248" cx="142.356" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-35" r="9.964" cy="274.606" cx="129.053" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-36" r="9.964" cy="223.156" cx="147.029" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-37" r="9.964" cy="169.647" cx="164.36" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-38" r="9.964" cy="290.475" cx="148.465" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-39" r="9.964" cy="238.165" cx="165.39" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-40" r="9.964" cy="184.139" cx="184.653" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-41" r="9.964" cy="253.417" cx="184.047" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-42" r="9.964" cy="199.221" cx="204.812" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-43" r="9.964" cy="268.306" cx="202.26" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-44" r="9.964" cy="214.247" cx="223.62" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-45" r="9.964" cy="308.5" cx="-275.898" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||||
|
<circle id="seat-46" r="9.964" cy="160.283" cx="296.74"/>
|
||||||
|
<circle id="seat-47" r="9.964" cy="114.844" cx="299.029"/>
|
||||||
|
<circle id="seat-48" r="9.964" cy="68.323" cx="302.756"/>
|
||||||
|
<circle id="seat-49" r="9.964" cy="167.585" cx="321.759"/>
|
||||||
|
<circle id="seat-50" r="9.964" cy="121.335" cx="325.14"/>
|
||||||
|
<circle id="seat-51" r="9.964" cy="74.003" cx="327.303"/>
|
||||||
|
<circle id="seat-52" r="9.964" cy="22.948" cx="328.246"/>
|
||||||
|
<circle id="seat-53" r="9.964" cy="183.347" cx="343.194"/>
|
||||||
|
<circle id="seat-54" r="9.964" cy="132.154" cx="348.129"/>
|
||||||
|
<circle id="seat-55" r="9.964" cy="82.117" cx="352.187"/>
|
||||||
|
<circle id="seat-56" r="9.964" cy="30.215" cx="354.639"/>
|
||||||
|
<circle id="seat-57" r="9.964" cy="145.407" cx="368.956"/>
|
||||||
|
<circle id="seat-58" r="9.964" cy="93.477" cx="375.988"/>
|
||||||
|
<circle id="seat-59" r="9.964" cy="39.969" cx="380.649"/>
|
||||||
|
<circle id="seat-60" r="9.964" cy="163.733" cx="388.214"/>
|
||||||
|
<circle id="seat-61" r="9.964" cy="108.082" cx="398.166"/>
|
||||||
|
<circle id="seat-62" r="9.964" cy="51.554" cx="404.387"/>
|
||||||
|
<circle id="seat-63" r="9.964" cy="125.122" cx="416.829"/>
|
||||||
|
<circle id="seat-64" r="9.964" cy="66.206" cx="426.902"/>
|
||||||
|
<circle id="seat-65" r="9.964" cy="143.999" cx="433.508"/>
|
||||||
|
<circle id="seat-66" r="9.964" cy="82.48" cx="447.457"/>
|
||||||
|
<circle id="seat-67" r="9.964" cy="102.267" cx="466.325"/>
|
||||||
|
<circle id="seat-68" r="9.964" cy="123.093" cx="481.494"/>
|
||||||
|
<circle id="seat-69" r="9.964" cy="-147.065" cx="400.833" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-70" r="9.964" cy="-188.912" cx="412.614" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-71" r="9.964" cy="-239.296" cx="426.798" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-72" r="9.964" cy="-289.68" cx="440.983" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-73" r="9.964" cy="-135.696" cx="424.411" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-74" r="9.964" cy="-180.651" cx="437.369" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-75" r="9.964" cy="-229.58" cx="452.656" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-76" r="9.964" cy="-281.375" cx="467.381" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-77" r="9.964" cy="-119.414" cx="445.127" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-78" r="9.964" cy="-167.771" cx="459.741" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-79" r="9.964" cy="-217.964" cx="475.026" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-80" r="9.964" cy="-269.967" cx="491.592" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-81" r="9.964" cy="-152.609" cx="478.289" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-82" r="9.964" cy="-204.059" cx="496.265" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-83" r="9.964" cy="-257.568" cx="513.597" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-84" r="9.964" cy="-136.74" cx="497.701" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-85" r="9.964" cy="-189.049" cx="514.627" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-86" r="9.964" cy="-243.076" cx="533.889" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-87" r="9.964" cy="-173.798" cx="533.284" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-88" r="9.964" cy="-227.994" cx="554.048" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-89" r="9.964" cy="-158.909" cx="551.496" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-90" r="9.964" cy="-212.968" cx="572.857" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>
|
||||||
|
<circle id="seat-91" r="9.964" cy="-197.942" cx="591.665" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 9.6 KiB |
@@ -1,8 +1,10 @@
|
|||||||
// src/App.tsx
|
// src/App.tsx
|
||||||
import './App.css'
|
import './App.css'
|
||||||
//import { BancasWidget } from './components/BancasWidget'
|
import { BancasWidget } from './components/BancasWidget'
|
||||||
|
import { CongresoWidget } from './components/CongresoWidget'
|
||||||
import MapaBsAs from './components/MapaBsAs'
|
import MapaBsAs from './components/MapaBsAs'
|
||||||
import { TickerWidget } from './components/TickerWidget'
|
import { TickerWidget } from './components/TickerWidget'
|
||||||
|
import { TelegramaWidget } from './components/TelegramaWidget'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@@ -10,8 +12,10 @@ function App() {
|
|||||||
<h1>Resultados Electorales - Provincia de Buenos Aires</h1>
|
<h1>Resultados Electorales - Provincia de Buenos Aires</h1>
|
||||||
<main>
|
<main>
|
||||||
<TickerWidget />
|
<TickerWidget />
|
||||||
{/*<BancasWidget />*/}
|
<CongresoWidget />
|
||||||
|
<BancasWidget />
|
||||||
<MapaBsAs />
|
<MapaBsAs />
|
||||||
|
<TelegramaWidget />
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,51 +1,15 @@
|
|||||||
// src/apiService.ts
|
// src/apiService.ts
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type { MunicipioSimple, MunicipioDetalle, ResumenProvincial, ProyeccionBancas } from './types/types';
|
import type { ResumenProvincial, ProyeccionBancas, MunicipioSimple, TelegramaData, CatalogoItem } from './types/types';
|
||||||
|
|
||||||
// La URL base de tu API. En un proyecto real, esto iría en un archivo .env
|
const API_BASE_URL = 'http://localhost:5217/api';
|
||||||
const API_BASE_URL = 'http://localhost:5217/api'; // Ajusta el puerto si es necesario
|
|
||||||
|
|
||||||
const apiClient = axios.create({
|
const apiClient = axios.create({
|
||||||
baseURL: API_BASE_URL,
|
baseURL: API_BASE_URL,
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtiene los resultados para colorear el mapa inicial.
|
|
||||||
*/
|
|
||||||
export const getResultadosParaMapa = async (): Promise<any[]> => { // Usamos any[] temporalmente
|
|
||||||
const response = await apiClient.get('/resultados/mapa');
|
|
||||||
// Mapeamos la respuesta para que coincida con lo que el frontend espera
|
|
||||||
return response.data.map((item: any) => ({
|
|
||||||
municipioId: item.partidoId, // La propiedad en el frontend se llama municipioId
|
|
||||||
agrupacionGanadoraId: item.agrupacionGanadoraId,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtiene la lista de todos los municipios con sus IDs y nombres.
|
|
||||||
*/
|
|
||||||
export const getMunicipios = async (): Promise<MunicipioSimple[]> => {
|
|
||||||
const response = await apiClient.get('/catalogos/municipios');
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtiene el detalle completo de los resultados para un municipio específico.
|
|
||||||
*/
|
|
||||||
export const getDetallePorMunicipio = async (partidoId: string): Promise<MunicipioDetalle> => {
|
|
||||||
const response = await apiClient.get(`/resultados/partido/${partidoId}`);
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtiene el resumen de resultados a nivel provincial.
|
|
||||||
* El distritoId para la PBA es "02" según la estructura de la API.
|
|
||||||
*/
|
|
||||||
export const getResumenProvincial = async (): Promise<ResumenProvincial> => {
|
export const getResumenProvincial = async (): Promise<ResumenProvincial> => {
|
||||||
// Hardcodeamos el distritoId '02' para Buenos Aires
|
|
||||||
const response = await apiClient.get('/resultados/provincia/02');
|
const response = await apiClient.get('/resultados/provincia/02');
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
@@ -54,3 +18,44 @@ export const getBancasPorSeccion = async (seccionId: string): Promise<Proyeccion
|
|||||||
const response = await apiClient.get(`/resultados/bancas/${seccionId}`);
|
const response = await apiClient.get(`/resultados/bancas/${seccionId}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene la lista de Secciones Electorales desde la API.
|
||||||
|
*/
|
||||||
|
export const getSeccionesElectorales = async (): Promise<MunicipioSimple[]> => {
|
||||||
|
const response = await apiClient.get('/catalogos/secciones-electorales');
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene los datos completos de un telegrama por su ID de mesa.
|
||||||
|
*/
|
||||||
|
export const getTelegramaPorId = async (mesaId: string): Promise<TelegramaData> => {
|
||||||
|
const response = await apiClient.get(`/telegramas/${mesaId}`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSecciones = async (): Promise<CatalogoItem[]> => {
|
||||||
|
const response = await apiClient.get('/catalogos/secciones');
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMunicipiosPorSeccion = async (seccionId: string): Promise<CatalogoItem[]> => {
|
||||||
|
const response = await apiClient.get(`/catalogos/municipios/${seccionId}`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCircuitosPorMunicipio = async (municipioId: string): Promise<CatalogoItem[]> => {
|
||||||
|
const response = await apiClient.get(`/catalogos/circuitos/${municipioId}`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEstablecimientosPorCircuito = async (circuitoId: string): Promise<CatalogoItem[]> => {
|
||||||
|
const response = await apiClient.get(`/catalogos/establecimientos/${circuitoId}`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMesasPorEstablecimiento = async (establecimientoId: string): Promise<CatalogoItem[]> => {
|
||||||
|
const response = await apiClient.get(`/catalogos/mesas/${establecimientoId}`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
/* src/components/BancasWidget.css */
|
/* src/components/BancasWidget.css */
|
||||||
.bancas-widget-container {
|
.bancas-widget-container {
|
||||||
background-color: #2a2a2e;
|
/* Mismo estilo de tarjeta que el Ticker */
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
|
||||||
padding: 15px 20px;
|
padding: 15px 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: 20px auto;
|
margin: 20px auto;
|
||||||
color: #e0e0e0;
|
color: #333333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bancas-header {
|
.bancas-header {
|
||||||
@@ -17,19 +20,22 @@
|
|||||||
|
|
||||||
.bancas-header h4 {
|
.bancas-header h4 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: white;
|
color: #212529;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bancas-header select {
|
.bancas-header select {
|
||||||
background-color: #3a3a3a;
|
background-color: #ffffff;
|
||||||
color: white;
|
color: #333333;
|
||||||
border: 1px solid #555;
|
border: 1px solid #ced4da; /* Borde estándar para inputs */
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.waffle-chart-container {
|
.waffle-chart-container {
|
||||||
height: 300px;
|
height: 300px;
|
||||||
font-family: system-ui, sans-serif;
|
font-family: "Public Sans", system-ui, sans-serif;
|
||||||
}
|
}
|
||||||
@@ -1,89 +1,125 @@
|
|||||||
// src/components/BancasWidget.tsx
|
// src/components/BancasWidget.tsx
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { ResponsiveWaffle } from '@nivo/waffle';
|
// Se cambia la importación: de ResponsiveWaffle a ResponsiveBar
|
||||||
import { getBancasPorSeccion } from '../apiService';
|
import { ResponsiveBar } from '@nivo/bar';
|
||||||
import type { ProyeccionBancas } from '../types/types';
|
import { getBancasPorSeccion, getSeccionesElectorales } from '../apiService';
|
||||||
|
import type { ProyeccionBancas, MunicipioSimple } from '../types/types';
|
||||||
import './BancasWidget.css';
|
import './BancasWidget.css';
|
||||||
|
|
||||||
// Paleta de colores consistente
|
// La paleta de colores se mantiene para consistencia visual
|
||||||
const NIVO_COLORS = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"];
|
const NIVO_COLORS = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"];
|
||||||
|
|
||||||
// Las Secciones Electorales de la Provincia (esto podría venir de la API en el futuro)
|
|
||||||
const secciones = [
|
|
||||||
{ id: '1', nombre: 'Primera Sección' },
|
|
||||||
{ id: '2', nombre: 'Segunda Sección' },
|
|
||||||
{ id: '3', nombre: 'Tercera Sección' },
|
|
||||||
{ id: '4', nombre: 'Cuarta Sección' },
|
|
||||||
{ id: '5', nombre: 'Quinta Sección' },
|
|
||||||
{ id: '6', nombre: 'Sexta Sección' },
|
|
||||||
{ id: '7', nombre: 'Séptima Sección' },
|
|
||||||
{ id: '8', nombre: 'Octava Sección (Capital)' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const BancasWidget = () => {
|
export const BancasWidget = () => {
|
||||||
const [seccionActual, setSeccionActual] = useState('1'); // Empezamos con la Primera Sección
|
const [secciones, setSecciones] = useState<MunicipioSimple[]>([]);
|
||||||
|
const [seccionActual, setSeccionActual] = useState<string>('');
|
||||||
const [data, setData] = useState<ProyeccionBancas | null>(null);
|
const [data, setData] = useState<ProyeccionBancas | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// useEffect para cargar la lista de secciones una sola vez
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const fetchSecciones = async () => {
|
||||||
|
try {
|
||||||
|
const seccionesData = await getSeccionesElectorales();
|
||||||
|
if (seccionesData && seccionesData.length > 0) {
|
||||||
|
|
||||||
|
// --- INICIO DE LA LÓGICA DE ORDENAMIENTO ---
|
||||||
|
const orden = new Map([
|
||||||
|
['Capital', 0], ['Primera', 1], ['Segunda', 2], ['Tercera', 3],
|
||||||
|
['Cuarta', 4], ['Quinta', 5], ['Sexta', 6], ['Séptima', 7]
|
||||||
|
]);
|
||||||
|
|
||||||
|
const getOrden = (nombre: string) => {
|
||||||
|
const match = nombre.match(/Capital|Primera|Segunda|Tercera|Cuarta|Quinta|Sexta|Séptima/);
|
||||||
|
return match ? orden.get(match[0]) ?? 99 : 99;
|
||||||
|
};
|
||||||
|
|
||||||
|
seccionesData.sort((a, b) => getOrden(a.nombre) - getOrden(b.nombre));
|
||||||
|
// --- FIN DE LA LÓGICA DE ORDENAMIENTO ---
|
||||||
|
|
||||||
|
setSecciones(seccionesData);
|
||||||
|
setSeccionActual(seccionesData[0].id);
|
||||||
|
} else {
|
||||||
|
setError("No se encontraron secciones electorales.");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error cargando secciones electorales:", err);
|
||||||
|
setError("No se pudo cargar la lista de secciones.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchSecciones();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// useEffect para cargar los datos de bancas cuando cambia la sección
|
||||||
|
useEffect(() => {
|
||||||
|
if (!seccionActual) return;
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const result = await getBancasPorSeccion(seccionActual);
|
const result = await getBancasPorSeccion(seccionActual);
|
||||||
setData(result);
|
setData(result);
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error(`Error cargando datos de bancas para sección ${seccionActual}:`, error);
|
console.error(`Error cargando datos de bancas para sección ${seccionActual}:`, err);
|
||||||
setData(null); // Limpiar datos en caso de error
|
setData(null);
|
||||||
|
setError("No hay datos de bancas disponibles para esta sección.");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [seccionActual]); // Se ejecuta cada vez que cambia la sección
|
}, [seccionActual]);
|
||||||
|
|
||||||
const waffleData = data?.proyeccion.map(p => ({
|
// Se preparan los datos para el gráfico de barras.
|
||||||
id: p.agrupacionNombre,
|
// Se invierte el array para que el partido con más bancas aparezca arriba.
|
||||||
label: p.agrupacionNombre,
|
const barData = data ? [...data.proyeccion].reverse() : [];
|
||||||
value: p.bancas,
|
|
||||||
})) || [];
|
|
||||||
|
|
||||||
const totalBancas = waffleData.reduce((sum, current) => sum + current.value, 0);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bancas-widget-container">
|
<div className="bancas-widget-container">
|
||||||
<div className="bancas-header">
|
<div className="bancas-header">
|
||||||
<h4>Distribución de Bancas</h4>
|
<h4>Distribución de Bancas</h4>
|
||||||
<select value={seccionActual} onChange={e => setSeccionActual(e.target.value)}>
|
<select value={seccionActual} onChange={e => setSeccionActual(e.target.value)} disabled={secciones.length === 0}>
|
||||||
{secciones.map(s => <option key={s.id} value={s.id}>{s.nombre}</option>)}
|
{secciones.map(s => <option key={s.id} value={s.id}>{s.nombre}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="waffle-chart-container">
|
<div className="waffle-chart-container">
|
||||||
{loading ? <p>Cargando...</p> : !data ? <p>No hay datos disponibles para esta sección.</p> :
|
{loading ? <p>Cargando...</p> : error ? <p>{error}</p> :
|
||||||
<ResponsiveWaffle
|
// --- SE REEMPLAZA EL GRÁFICO WAFFLE POR EL GRÁFICO DE BARRAS ---
|
||||||
data={waffleData}
|
<ResponsiveBar
|
||||||
total={totalBancas}
|
data={barData}
|
||||||
rows={8}
|
keys={['bancas']}
|
||||||
columns={10}
|
indexBy="agrupacionNombre"
|
||||||
fillDirection="bottom"
|
layout="horizontal"
|
||||||
padding={3}
|
margin={{ top: 10, right: 30, bottom: 25, left: 160 }}
|
||||||
colors={NIVO_COLORS}
|
padding={0.3}
|
||||||
borderColor={{ from: 'color', modifiers: [['darker', 0.3]] }}
|
valueScale={{ type: 'linear' }}
|
||||||
|
indexScale={{ type: 'band', round: true }}
|
||||||
|
colors={({ index }) => NIVO_COLORS[index % NIVO_COLORS.length]}
|
||||||
|
borderColor={{ from: 'color', modifiers: [['darker', 1.6]] }}
|
||||||
|
axisTop={null}
|
||||||
|
axisRight={null}
|
||||||
|
axisBottom={{
|
||||||
|
tickSize: 5,
|
||||||
|
tickPadding: 5,
|
||||||
|
tickRotation: 0,
|
||||||
|
legend: 'Cantidad de Bancas',
|
||||||
|
legendPosition: 'middle',
|
||||||
|
legendOffset: 20,
|
||||||
|
// Asegura que los ticks del eje sean números enteros
|
||||||
|
format: (value) => Math.floor(value) === value ? value : ''
|
||||||
|
}}
|
||||||
|
axisLeft={{
|
||||||
|
tickSize: 5,
|
||||||
|
tickPadding: 5,
|
||||||
|
tickRotation: 0,
|
||||||
|
}}
|
||||||
|
labelSkipWidth={12}
|
||||||
|
labelSkipHeight={12}
|
||||||
|
labelTextColor={{ from: 'color', modifiers: [['darker', 3]] }}
|
||||||
animate={true}
|
animate={true}
|
||||||
legends={[
|
// Se elimina la leyenda, ya que las etiquetas en el eje son suficientes
|
||||||
{
|
legends={[]}
|
||||||
anchor: 'bottom',
|
|
||||||
direction: 'row',
|
|
||||||
justify: false,
|
|
||||||
translateX: 0,
|
|
||||||
translateY: 40,
|
|
||||||
itemsSpacing: 4,
|
|
||||||
itemWidth: 100,
|
|
||||||
itemHeight: 20,
|
|
||||||
itemTextColor: '#999',
|
|
||||||
itemDirection: 'left-to-right',
|
|
||||||
symbolSize: 20,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>}
|
/>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
148
Elecciones-Web/frontend/src/components/CongresoWidget.css
Normal file
148
Elecciones-Web/frontend/src/components/CongresoWidget.css
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
/* src/components/CongresoWidget.css */
|
||||||
|
.congreso-container {
|
||||||
|
display: flex;
|
||||||
|
/* Se reduce ligeramente el espacio entre el gráfico y el panel */
|
||||||
|
gap: 1rem;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 20px auto;
|
||||||
|
font-family: "Public Sans", system-ui, sans-serif;
|
||||||
|
color: #333333;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.congreso-grafico {
|
||||||
|
/* --- CAMBIO PRINCIPAL: Se aumenta la proporción del gráfico --- */
|
||||||
|
flex: 1 1 65%;
|
||||||
|
min-width: 300px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.congreso-grafico svg {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
animation: fadeIn 0.8s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: scale(0.9); }
|
||||||
|
to { opacity: 1; transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.congreso-summary {
|
||||||
|
/* --- CAMBIO PRINCIPAL: Se reduce la proporción del panel de datos --- */
|
||||||
|
flex: 1 1 35%;
|
||||||
|
border-left: 1px solid #e0e0e0;
|
||||||
|
/* Se reduce el padding para dar aún más espacio al gráfico */
|
||||||
|
padding-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.congreso-summary h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 1.4em;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chamber-tabs {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chamber-tabs button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.75rem 0.5rem;
|
||||||
|
border: none;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #6c757d;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chamber-tabs button:first-child {
|
||||||
|
border-right: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chamber-tabs button:hover {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chamber-tabs button.active {
|
||||||
|
background-color: var(--primary-accent-color);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-metric {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-metric strong {
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--primary-accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.congreso-summary hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partido-lista {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partido-lista li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partido-color-box {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-right: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partido-nombre {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partido-bancas {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Media Query para Responsividad Móvil --- */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.congreso-container {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
.congreso-summary {
|
||||||
|
border-left: none;
|
||||||
|
padding-left: 0;
|
||||||
|
margin-top: 2rem;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
125
Elecciones-Web/frontend/src/components/CongresoWidget.tsx
Normal file
125
Elecciones-Web/frontend/src/components/CongresoWidget.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
// src/components/CongresoWidget.tsx
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
// 1. Importar ambos layouts
|
||||||
|
import { ParliamentLayout } from './ParliamentLayout';
|
||||||
|
import { SenateLayout } from './SenateLayout';
|
||||||
|
import './CongresoWidget.css';
|
||||||
|
|
||||||
|
// ... (Interfaces sin cambios)
|
||||||
|
type CamaraType = 'diputados' | 'senadores';
|
||||||
|
|
||||||
|
interface PartidoData {
|
||||||
|
id: string;
|
||||||
|
nombre: string;
|
||||||
|
bancasTotales: number;
|
||||||
|
bancasEnJuego: number;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CamaraData {
|
||||||
|
camaraNombre: string;
|
||||||
|
totalBancas: number;
|
||||||
|
bancasEnJuego: number;
|
||||||
|
partidos: PartidoData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ComposicionData {
|
||||||
|
diputados: CamaraData;
|
||||||
|
senadores: CamaraData;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const CongresoWidget = () => {
|
||||||
|
const [data, setData] = useState<ComposicionData | null>(null);
|
||||||
|
const [camaraActiva, setCamaraActiva] = useState<CamaraType>('diputados');
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// ... (fetchData sin cambios)
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost:5217/api/resultados/composicion-congreso');
|
||||||
|
if (!response.ok) throw new Error('La respuesta de la red no fue exitosa');
|
||||||
|
const result: ComposicionData = await response.json();
|
||||||
|
setData(result);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error cargando datos de composición:", err);
|
||||||
|
setError("No se pudieron cargar los datos de composición del congreso.");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) return <div className="congreso-container loading">Cargando...</div>;
|
||||||
|
if (error) return <div className="congreso-container error">{error}</div>;
|
||||||
|
|
||||||
|
const datosCamaraActual = data ? data[camaraActiva] : null;
|
||||||
|
|
||||||
|
if (!datosCamaraActual) {
|
||||||
|
return <div className="congreso-container error">Datos no disponibles para la cámara seleccionada.</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const partidosOrdenados = datosCamaraActual.partidos;
|
||||||
|
|
||||||
|
const seatFillData = partidosOrdenados.flatMap(party => {
|
||||||
|
const retainedSeats = party.bancasTotales - party.bancasEnJuego;
|
||||||
|
const inPlaySeats = party.bancasEnJuego;
|
||||||
|
return [
|
||||||
|
...Array(retainedSeats).fill({ color: party.color, isEnJuego: false }),
|
||||||
|
...Array(inPlaySeats).fill({ color: party.color, isEnJuego: true }),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="congreso-container">
|
||||||
|
<div className="congreso-grafico">
|
||||||
|
{/* --- INICIO DE LA MODIFICACIÓN: RENDERIZADO CONDICIONAL --- */}
|
||||||
|
{camaraActiva === 'diputados' ? (
|
||||||
|
<ParliamentLayout seatData={seatFillData} />
|
||||||
|
) : (
|
||||||
|
<SenateLayout seatData={seatFillData} />
|
||||||
|
)}
|
||||||
|
{/* --- FIN DE LA MODIFICACIÓN --- */}
|
||||||
|
</div>
|
||||||
|
<div className="congreso-summary">
|
||||||
|
<div className="chamber-tabs">
|
||||||
|
<button
|
||||||
|
className={camaraActiva === 'diputados' ? 'active' : ''}
|
||||||
|
onClick={() => setCamaraActiva('diputados')}
|
||||||
|
>
|
||||||
|
Diputados
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={camaraActiva === 'senadores' ? 'active' : ''}
|
||||||
|
onClick={() => setCamaraActiva('senadores')}
|
||||||
|
>
|
||||||
|
Senadores
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>{datosCamaraActual.camaraNombre}</h3>
|
||||||
|
<div className="summary-metric">
|
||||||
|
<span>Total de Bancas</span>
|
||||||
|
<strong>{datosCamaraActual.totalBancas}</strong>
|
||||||
|
</div>
|
||||||
|
<div className="summary-metric">
|
||||||
|
<span>Bancas en juego</span>
|
||||||
|
<strong>{datosCamaraActual.bancasEnJuego}</strong>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<ul className="partido-lista">
|
||||||
|
{partidosOrdenados.map(partido => (
|
||||||
|
<li key={partido.id}>
|
||||||
|
<span className="partido-color-box" style={{ backgroundColor: partido.color }}></span>
|
||||||
|
<span className="partido-nombre">{partido.nombre}</span>
|
||||||
|
<strong className="partido-bancas">{partido.bancasTotales}</strong>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,25 +1,27 @@
|
|||||||
/* src/components/MapaBsAs.css */
|
/* src/components/MapaBsAs.css */
|
||||||
:root {
|
:root {
|
||||||
--primary-accent-color: #FF5722;
|
--primary-accent-color: #0073e6;
|
||||||
--background-panel-color: #2f2f2f;
|
--background-panel-color: #ffffff;
|
||||||
--border-color: #444;
|
--border-color: #dee2e6;
|
||||||
--text-color: #f0f0f0;
|
--text-color: #212529;
|
||||||
--text-color-muted: #aaa;
|
--text-color-muted: #6c757d;
|
||||||
--progress-bar-background: #4a4a4a;
|
--progress-bar-background: #e9ecef;
|
||||||
--scrollbar-thumb-color: #666;
|
--scrollbar-thumb-color: #ced4da;
|
||||||
--scrollbar-track-color: #333;
|
--scrollbar-track-color: #f1f1f1;
|
||||||
--map-background-color: #242424; /* Color de fondo del mapa */
|
--map-background-color: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapa-wrapper {
|
.mapa-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
background-color: var(--map-background-color);
|
background-color: #ffffff;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
max-width: 1600px; /* Incrementado para pantallas más grandes */
|
max-width: 1600px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
height: 88vh; /* Ligeramente más alto */
|
height: 88vh;
|
||||||
min-height: 650px;
|
min-height: 650px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +32,6 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
/* CORRECCIÓN: Se añade el color de fondo para eliminar el marco blanco */
|
|
||||||
background-color: var(--map-background-color);
|
background-color: var(--map-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,46 +44,47 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rsm-geography {
|
.rsm-geography {
|
||||||
transition: opacity 0.3s ease-in-out, transform 0.2s ease-in-out, filter 0.2s ease-in-out, fill 0.3s ease;
|
transition: opacity 0.3s ease, transform 0.2s ease, filter 0.2s ease, fill 0.3s ease;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
stroke-width: 1px;
|
stroke: #b0b0b0;
|
||||||
|
stroke-width: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rsm-geography:hover {
|
.rsm-geography:hover {
|
||||||
filter: drop-shadow(0px 0px 6px rgba(255, 255, 255, 0.6));
|
stroke: var(--primary-accent-color);
|
||||||
transform: translateY(-1px);
|
|
||||||
stroke-width: 1.5px;
|
stroke-width: 1.5px;
|
||||||
|
filter: brightness(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rsm-geography.selected {
|
.rsm-geography.selected {
|
||||||
fill: var(--primary-accent-color); /* Rellena el partido seleccionado con el color principal */
|
stroke: #333;
|
||||||
stroke: #ffffff; /* Añade un borde blanco para un mejor contraste */
|
stroke-width: 2px;
|
||||||
stroke-width: 2px; /* Un grosor de borde definido */
|
filter: none;
|
||||||
filter: none; /* Elimina el efecto de sombra/resplandor */
|
pointer-events: none;
|
||||||
outline: none; /* Previene el recuadro de enfoque del navegador */
|
|
||||||
pointer-events: none; /* Mantenemos esto para evitar interacciones no deseadas */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rsm-geography.faded {
|
.rsm-geography.faded {
|
||||||
opacity: 0.15;
|
opacity: 0.25;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-panel {
|
.info-panel {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 100%;
|
|
||||||
background-color: var(--background-panel-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
min-height: 0;
|
||||||
|
background-color: var(--background-panel-color);
|
||||||
|
border-radius: 8px;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-panel::-webkit-scrollbar { width: 8px; }
|
.info-panel::-webkit-scrollbar { width: 8px; }
|
||||||
.info-panel::-webkit-scrollbar-track { background: var(--scrollbar-track-color); border-radius: 4px; }
|
.info-panel::-webkit-scrollbar-track { background: var(--scrollbar-track-color); border-radius: 4px; }
|
||||||
.info-panel::-webkit-scrollbar-thumb { background-color: var(--scrollbar-thumb-color); border-radius: 4px; border: 2px solid var(--scrollbar-track-color); }
|
.info-panel::-webkit-scrollbar-thumb { background-color: var(--scrollbar-thumb-color); border-radius: 4px; border: 2px solid var(--scrollbar-track-color); }
|
||||||
.info-panel::-webkit-scrollbar-thumb:hover { background-color: #888; }
|
.info-panel::-webkit-scrollbar-thumb:hover { background-color: #adb5bd; }
|
||||||
|
|
||||||
.info-panel h3 { margin-top: 0; color: var(--primary-accent-color); border-bottom: 2px solid var(--border-color); padding-bottom: 0.5rem; }
|
.info-panel h3 { margin-top: 0; color: var(--primary-accent-color); border-bottom: 2px solid var(--border-color); padding-bottom: 0.5rem; }
|
||||||
.info-panel p { color: var(--text-color-muted); }
|
.info-panel p { color: var(--text-color-muted); }
|
||||||
|
|
||||||
@@ -91,14 +93,17 @@
|
|||||||
}
|
}
|
||||||
.reset-button-panel:hover { background-color: var(--primary-accent-color); color: white; }
|
.reset-button-panel:hover { background-color: var(--primary-accent-color); color: white; }
|
||||||
|
|
||||||
.detalle-placeholder { text-align: center; margin: auto; }
|
.detalle-placeholder {
|
||||||
.detalle-loading, .detalle-error { text-align: center; margin: auto; color: var(--text-color-muted); }
|
text-align: center;
|
||||||
|
margin: auto 0;
|
||||||
|
}
|
||||||
|
.detalle-loading, .detalle-error { text-align: center; margin: auto 0; color: var(--text-color-muted); }
|
||||||
.detalle-metricas { display: flex; justify-content: space-between; font-size: 0.9em; padding-bottom: 1rem; border-bottom: 1px solid var(--border-color); margin-bottom: 1rem; }
|
.detalle-metricas { display: flex; justify-content: space-between; font-size: 0.9em; padding-bottom: 1rem; border-bottom: 1px solid var(--border-color); margin-bottom: 1rem; }
|
||||||
.resultados-lista { list-style: none; padding: 0; margin: 0; }
|
.resultados-lista { list-style: none; padding: 0; margin: 0; }
|
||||||
.resultados-lista li { margin-bottom: 1rem; }
|
.resultados-lista li { margin-bottom: 1rem; }
|
||||||
.resultado-info { display: flex; justify-content: space-between; margin-bottom: 0.25rem; font-size: 0.9em; }
|
.resultado-info { display: flex; justify-content: space-between; margin-bottom: 0.25rem; font-size: 0.9em; }
|
||||||
.partido-nombre { font-weight: 500; }
|
.partido-nombre { font-weight: 500; }
|
||||||
.partido-votos { font-weight: 300; color: var(--text-color-muted); }
|
.partido-votos { font-weight: 400; color: var(--text-color-muted); }
|
||||||
.progress-bar { height: 8px; background-color: var(--progress-bar-background); border-radius: 4px; overflow: hidden; }
|
.progress-bar { height: 8px; background-color: var(--progress-bar-background); border-radius: 4px; overflow: hidden; }
|
||||||
.progress-fill { height: 100%; background-color: var(--primary-accent-color); border-radius: 4px; transition: width 0.5s ease-out; }
|
.progress-fill { height: 100%; background-color: var(--primary-accent-color); border-radius: 4px; transition: width 0.5s ease-out; }
|
||||||
|
|
||||||
@@ -112,14 +117,81 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
z-index: 10; /* <-- AÑADIDO: Esta línea asegura que los controles estén por encima del mapa. */
|
z-index: 10;
|
||||||
}
|
}
|
||||||
.map-controls button {
|
|
||||||
width: 32px; height: 32px; font-size: 1.2rem; font-weight: bold; background-color: rgba(0, 0, 0, 0.7); color: white; border: 1px solid var(--border-color); border-radius: 4px; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; justify-content: center; padding: 0; line-height: 1;
|
|
||||||
}
|
|
||||||
.map-controls button:hover { background-color: rgba(0, 0, 0, 0.9); border-color: var(--primary-accent-color); }
|
|
||||||
|
|
||||||
.legend { margin-top: auto; padding-top: 1rem; border-top: 1px solid var(--border-color); }
|
/* --- ESTILOS PARA EL BOTÓN DE "VOLVER" (VISTA DESKTOP) --- */
|
||||||
.legend h4 { margin-top: 0; }
|
.map-controls button {
|
||||||
|
/* Se elimina el ancho y alto fijos para que el botón se ajuste al texto. */
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
/* Se define un padding para dar espacio interno al texto. */
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
/* Se ajusta el tamaño de fuente para el texto. */
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #333;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
/* Se eliminan las propiedades de centrado de íconos que ya no son necesarias. */
|
||||||
|
}
|
||||||
|
.map-controls button:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-color: var(--primary-accent-color);
|
||||||
|
color: var(--primary-accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
.legend h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
.legend-item { display: flex; align-items: center; margin-bottom: 0.5rem; font-size: 0.85em; }
|
.legend-item { display: flex; align-items: center; margin-bottom: 0.5rem; font-size: 0.85em; }
|
||||||
.legend-color-box { width: 16px; height: 16px; margin-right: 8px; border-radius: 3px; }
|
.legend-color-box { width: 16px; height: 16px; margin-right: 8px; border-radius: 3px; border: 1px solid #ccc; }
|
||||||
|
|
||||||
|
/* --- ESTILOS PARA RESPONSIVIDAD MÓVIL --- */
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.mapa-wrapper {
|
||||||
|
flex-direction: column;
|
||||||
|
height: auto;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapa-container {
|
||||||
|
flex-basis: auto;
|
||||||
|
width: 100%;
|
||||||
|
height: 50vh;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel {
|
||||||
|
flex-basis: auto;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
overflow-y: visible;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-controls {
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- ESTILOS PARA EL BOTÓN DE "VOLVER" (VISTA MÓVIL) --- */
|
||||||
|
.map-controls button {
|
||||||
|
/* Se elimina el ancho y alto fijos para que el botón se ajuste al texto. */
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
/* Se ajusta el padding para que sea un buen objetivo táctil (tappable). */
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
/* Un tamaño de fuente legible en móviles. */
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,7 +81,8 @@ const MapaBsAs = () => {
|
|||||||
coloresMap.set(agrupacion.id, COLORES_BASE[index % COLORES_BASE.length]);
|
coloresMap.set(agrupacion.id, COLORES_BASE[index % COLORES_BASE.length]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
coloresMap.set('default', '#D6D6DA');
|
// Se cambia el color por defecto a uno más apropiado para fondo claro
|
||||||
|
coloresMap.set('default', '#E0E0E0');
|
||||||
if (resultadosData) {
|
if (resultadosData) {
|
||||||
resultadosData.forEach(r => resultadosMap.set(r.departamentoNombre.toUpperCase(), r));
|
resultadosData.forEach(r => resultadosMap.set(r.departamentoNombre.toUpperCase(), r));
|
||||||
}
|
}
|
||||||
@@ -135,11 +136,6 @@ const MapaBsAs = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleZoomOut = () => {
|
|
||||||
// Al presionar el botón de zoom out, siempre se vuelve al estado inicial.
|
|
||||||
handleReset();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => e.key === 'Escape' && handleReset();
|
const handleKeyDown = (e: KeyboardEvent) => e.key === 'Escape' && handleReset();
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
@@ -164,7 +160,8 @@ const MapaBsAs = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="mapa-wrapper">
|
<div className="mapa-wrapper">
|
||||||
<div className="mapa-container">
|
<div className="mapa-container">
|
||||||
<ComposableMap projection="geoMercator" projectionConfig={{ scale: 4700, center: [-60.5, -37.2] }} className="rsm-svg" style={{ backgroundColor: "#242424" }}>
|
{/* Se elimina el 'style' con el backgroundColor para que lo controle el CSS */}
|
||||||
|
<ComposableMap projection="geoMercator" projectionConfig={{ scale: 4400, center: [-60.5, -37.2] }} className="rsm-svg">
|
||||||
<ZoomableGroup
|
<ZoomableGroup
|
||||||
center={position.center}
|
center={position.center}
|
||||||
zoom={position.zoom}
|
zoom={position.zoom}
|
||||||
@@ -177,7 +174,7 @@ const MapaBsAs = () => {
|
|||||||
// Detectamos si la rueda se mueve hacia atrás (zoom out)
|
// Detectamos si la rueda se mueve hacia atrás (zoom out)
|
||||||
if (e.deltaY > 0) {
|
if (e.deltaY > 0) {
|
||||||
handleReset();
|
handleReset();
|
||||||
}else if (e.deltaY < 0) {
|
} else if (e.deltaY < 0) {
|
||||||
handleZoomIn();
|
handleZoomIn();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -201,7 +198,7 @@ const MapaBsAs = () => {
|
|||||||
data-tooltip-content={`${geo.properties.departamento}: ${nombreAgrupacionGanadora}`}
|
data-tooltip-content={`${geo.properties.departamento}: ${nombreAgrupacionGanadora}`}
|
||||||
className={`rsm-geography ${isSelected ? 'selected' : ''} ${isFaded ? 'faded' : ''}`}
|
className={`rsm-geography ${isSelected ? 'selected' : ''} ${isFaded ? 'faded' : ''}`}
|
||||||
fill={getPartyFillColor(geo.properties.departamento)}
|
fill={getPartyFillColor(geo.properties.departamento)}
|
||||||
stroke="#FFF"
|
// Se elimina la prop 'stroke' para que la controle el CSS
|
||||||
onClick={() => handleGeographyClick(geo)}
|
onClick={() => handleGeographyClick(geo)}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
/>
|
/>
|
||||||
@@ -212,8 +209,9 @@ const MapaBsAs = () => {
|
|||||||
)}
|
)}
|
||||||
</ZoomableGroup>
|
</ZoomableGroup>
|
||||||
</ComposableMap>
|
</ComposableMap>
|
||||||
<Tooltip id="partido-tooltip" />
|
{/* Se añade una variante 'light' al Tooltip para que combine mejor */}
|
||||||
<ControlesMapa onZoomIn={handleZoomIn} onZoomOut={handleZoomOut} onReset={handleReset} />
|
<Tooltip id="partido-tooltip" variant="light" />
|
||||||
|
{selectedAmbitoId !== null && <ControlesMapa onReset={handleReset} />}
|
||||||
</div>
|
</div>
|
||||||
<div className="info-panel">
|
<div className="info-panel">
|
||||||
<DetalleMunicipio ambitoId={selectedAmbitoId} onReset={handleReset} />
|
<DetalleMunicipio ambitoId={selectedAmbitoId} onReset={handleReset} />
|
||||||
@@ -223,12 +221,10 @@ const MapaBsAs = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Sub-componentes (sin cambios) ---
|
// --- Sub-componentes ---
|
||||||
const ControlesMapa = ({ onZoomIn, onZoomOut, onReset }: { onZoomIn: () => void; onZoomOut: () => void; onReset: () => void }) => (
|
const ControlesMapa = ({ onReset }: { onReset: () => void }) => (
|
||||||
<div className="map-controls">
|
<div className="map-controls">
|
||||||
<button onClick={onZoomIn}>+</button>
|
<button onClick={onReset}>← VOLVER</button>
|
||||||
<button onClick={onZoomOut}>-</button>
|
|
||||||
<button onClick={onReset}>⌖</button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -245,13 +241,25 @@ const DetalleMunicipio = ({ ambitoId, onReset }: { ambitoId: number | null; onRe
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="detalle-content">
|
<div className="detalle-content">
|
||||||
<button className="reset-button-panel" onClick={onReset}>← Ver Provincia</button>
|
<button className="reset-button-panel" onClick={onReset}>← VOLVER</button>
|
||||||
<h3>{data?.municipioNombre}</h3>
|
<h3>{data?.municipioNombre}</h3>
|
||||||
<div className="detalle-metricas">
|
<div className="detalle-metricas">
|
||||||
<span><strong>Escrutado:</strong> {data?.porcentajeEscrutado.toFixed(2)}%</span>
|
<span><strong>Escrutado:</strong> {data?.porcentajeEscrutado.toFixed(2)}%</span>
|
||||||
<span><strong>Participación:</strong> {data?.porcentajeParticipacion.toFixed(2)}%</span>
|
<span><strong>Participación:</strong> {data?.porcentajeParticipacion.toFixed(2)}%</span>
|
||||||
</div>
|
</div>
|
||||||
<ul className="resultados-lista">{data?.resultados.map(r => (<li key={r.nombre}><div className="resultado-info"><span className="partido-nombre">{r.nombre}</span><span className="partido-votos">{r.votos.toLocaleString('es-AR')} ({r.porcentaje.toFixed(2)}%)</span></div><div className="progress-bar"><div className="progress-fill" style={{ width: `${r.porcentaje}%` }}></div></div></li>))}</ul>
|
<ul className="resultados-lista">
|
||||||
|
{data?.resultados.map((r, index) => (
|
||||||
|
<li key={`${r.nombre}-${index}`}>
|
||||||
|
<div className="resultado-info">
|
||||||
|
<span className="partido-nombre">{r.nombre}</span>
|
||||||
|
<span className="partido-votos">{r.votos.toLocaleString('es-AR')} ({r.porcentaje.toFixed(2)}%)</span>
|
||||||
|
</div>
|
||||||
|
<div className="progress-bar">
|
||||||
|
<div className="progress-fill" style={{ width: `${r.porcentaje}%` }}></div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
156
Elecciones-Web/frontend/src/components/ParliamentLayout.tsx
Normal file
156
Elecciones-Web/frontend/src/components/ParliamentLayout.tsx
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
// src/components/ParliamentLayout.tsx
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
// Interfaces (sin cambios)
|
||||||
|
interface SeatFillData {
|
||||||
|
color: string;
|
||||||
|
isEnJuego: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParliamentLayoutProps {
|
||||||
|
seatData: SeatFillData[];
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ParliamentLayout: React.FC<ParliamentLayoutProps> = ({
|
||||||
|
seatData,
|
||||||
|
size = 400,
|
||||||
|
}) => {
|
||||||
|
const uniqueColors = [...new Set(seatData.map(d => d.color))];
|
||||||
|
|
||||||
|
// La plantilla de círculos estáticos
|
||||||
|
const seatElements = [
|
||||||
|
<circle key="seat-0" id="seat-0" r="9.964" cy="160.283" cx="-255.056" transform="matrix(-1, 0, 0, 1, 0, 0)"/>,
|
||||||
|
<circle key="seat-1" id="seat-1" r="9.964" cy="114.844" cx="-252.766" transform="matrix(-1, 0, 0, 1, 0, 0)"/>,
|
||||||
|
<circle key="seat-2" id="seat-2" r="9.964" cy="68.323" cx="-249.04" transform="matrix(-1, 0, 0, 1, 0, 0)"/>,
|
||||||
|
<circle key="seat-3" id="seat-3" r="9.964" cy="167.585" cx="-230.037" transform="matrix(-1, 0, 0, 1, 0, 0)"/>,
|
||||||
|
<circle key="seat-4" id="seat-4" r="9.964" cy="121.335" cx="-226.656" transform="matrix(-1, 0, 0, 1, 0, 0)"/>,
|
||||||
|
<circle key="seat-5" id="seat-5" r="9.964" cy="74.003" cx="-224.493" transform="matrix(-1, 0, 0, 1, 0, 0)"/>,
|
||||||
|
<circle key="seat-6" id="seat-6" r="9.964" cy="22.948" cx="-223.55" transform="matrix(-1, 0, 0, 1, 0, 0)"/>,
|
||||||
|
<circle key="seat-7" id="seat-7" r="9.964" cy="183.347" cx="-208.602" transform="matrix(-1, 0, 0, 1, 0, 0)"/>,
|
||||||
|
<circle key="seat-8" id="seat-8" r="9.964" cy="132.154" cx="-203.666" transform="matrix(-1, 0, 0, 1, 0, 0)"/>,
|
||||||
|
<circle key="seat-9" id="seat-9" r="9.964" cy="82.117" cx="-199.609" transform="matrix(-1, 0, 0, 1, 0, 0)"/>,
|
||||||
|
<circle key="seat-10" id="seat-10" r="9.964" cy="30.215" cx="-197.157" transform="matrix(-1, 0, 0, 1, 0, 0)"/>,
|
||||||
|
<circle key="seat-11" id="seat-11" r="9.964" cy="145.407" cx="-182.84" transform="matrix(-1, 0, 0, 1, 0, 0)"/>,
|
||||||
|
<circle key="seat-12" id="seat-12" r="9.964" cy="93.477" cx="-175.808" transform="matrix(-1, 0, 0, 1, 0, 0)"/>,
|
||||||
|
<circle key="seat-13" id="seat-13" r="9.964" cy="39.969" cx="-171.147" transform="matrix(-1, 0, 0, 1, 0, 0)"/>,
|
||||||
|
<circle key="seat-14" id="seat-14" r="9.964" cy="163.733" cx="-163.582" transform="matrix(-1, 0, 0, 1, 0, 0)"/>,
|
||||||
|
<circle key="seat-15" id="seat-15" r="9.964" cy="108.082" cx="-153.63" transform="matrix(-1, 0, 0, 1, 0, 0)"/>,
|
||||||
|
<circle key="seat-16" id="seat-16" r="9.964" cy="51.554" cx="-147.409" transform="matrix(-1, 0, 0, 1, 0, 0)"/>,
|
||||||
|
<circle key="seat-17" id="seat-17" r="9.964" cy="125.122" cx="-134.967" transform="matrix(-1, 0, 0, 1, 0, 0)"/>,
|
||||||
|
<circle key="seat-18" id="seat-18" r="9.964" cy="66.206" cx="-124.894" transform="matrix(-1, 0, 0, 1, 0, 0)"/>,
|
||||||
|
<circle key="seat-19" id="seat-19" r="9.964" cy="143.999" cx="-118.288" transform="matrix(-1, 0, 0, 1, 0, 0)"/>,
|
||||||
|
<circle key="seat-20" id="seat-20" r="9.964" cy="82.48" cx="-104.338" transform="matrix(-1, 0, 0, 1, 0, 0)"/>,
|
||||||
|
<circle key="seat-21" id="seat-21" r="9.964" cy="102.267" cx="-85.471" transform="matrix(-1, 0, 0, 1, 0, 0)"/>,
|
||||||
|
<circle key="seat-22" id="seat-22" r="9.964" cy="123.093" cx="-70.302" transform="matrix(-1, 0, 0, 1, 0, 0)"/>,
|
||||||
|
<circle key="seat-23" id="seat-23" r="9.964" cy="280.15" cx="51.596" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-24" id="seat-24" r="9.964" cy="238.303" cx="63.378" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-25" id="seat-25" r="9.964" cy="187.919" cx="77.562" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-26" id="seat-26" r="9.964" cy="137.535" cx="91.746" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-27" id="seat-27" r="9.964" cy="291.519" cx="75.175" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-28" id="seat-28" r="9.964" cy="246.564" cx="88.133" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-29" id="seat-29" r="9.964" cy="197.635" cx="103.42" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-30" id="seat-30" r="9.964" cy="145.84" cx="118.145" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-31" id="seat-31" r="9.964" cy="307.801" cx="95.891" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-32" id="seat-32" r="9.964" cy="259.444" cx="110.504" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-33" id="seat-33" r="9.964" cy="209.251" cx="125.789" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-34" id="seat-34" r="9.964" cy="157.248" cx="142.356" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-35" id="seat-35" r="9.964" cy="274.606" cx="129.053" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-36" id="seat-36" r="9.964" cy="223.156" cx="147.029" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-37" id="seat-37" r="9.964" cy="169.647" cx="164.36" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-38" id="seat-38" r="9.964" cy="290.475" cx="148.465" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-39" id="seat-39" r="9.964" cy="238.165" cx="165.39" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-40" id="seat-40" r="9.964" cy="184.139" cx="184.653" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-41" id="seat-41" r="9.964" cy="253.417" cx="184.047" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-42" id="seat-42" r="9.964" cy="199.221" cx="204.812" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-43" id="seat-43" r="9.964" cy="268.306" cx="202.26" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-44" id="seat-44" r="9.964" cy="214.247" cx="223.62" transform="matrix(-0.632908, 0.774227, 0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-45" id="seat-45" r="9.964" cy="308.5" cx="-275.898" transform="matrix(-1, 0, 0, 1, 0, 0)"/>,
|
||||||
|
<circle key="seat-46" id="seat-46" r="9.964" cy="160.283" cx="296.74"/>,
|
||||||
|
<circle key="seat-47" id="seat-47" r="9.964" cy="114.844" cx="299.029"/>,
|
||||||
|
<circle key="seat-48" id="seat-48" r="9.964" cy="68.323" cx="302.756"/>,
|
||||||
|
<circle key="seat-49" id="seat-49" r="9.964" cy="167.585" cx="321.759"/>,
|
||||||
|
<circle key="seat-50" id="seat-50" r="9.964" cy="121.335" cx="325.14"/>,
|
||||||
|
<circle key="seat-51" id="seat-51" r="9.964" cy="74.003" cx="327.303"/>,
|
||||||
|
<circle key="seat-52" id="seat-52" r="9.964" cy="22.948" cx="328.246"/>,
|
||||||
|
<circle key="seat-53" id="seat-53" r="9.964" cy="183.347" cx="343.194"/>,
|
||||||
|
<circle key="seat-54" id="seat-54" r="9.964" cy="132.154" cx="348.129"/>,
|
||||||
|
<circle key="seat-55" id="seat-55" r="9.964" cy="82.117" cx="352.187"/>,
|
||||||
|
<circle key="seat-56" id="seat-56" r="9.964" cy="30.215" cx="354.639"/>,
|
||||||
|
<circle key="seat-57" id="seat-57" r="9.964" cy="145.407" cx="368.956"/>,
|
||||||
|
<circle key="seat-58" id="seat-58" r="9.964" cy="93.477" cx="375.988"/>,
|
||||||
|
<circle key="seat-59" id="seat-59" r="9.964" cy="39.969" cx="380.649"/>,
|
||||||
|
<circle key="seat-60" id="seat-60" r="9.964" cy="163.733" cx="388.214"/>,
|
||||||
|
<circle key="seat-61" id="seat-61" r="9.964" cy="108.082" cx="398.166"/>,
|
||||||
|
<circle key="seat-62" id="seat-62" r="9.964" cy="51.554" cx="404.387"/>,
|
||||||
|
<circle key="seat-63" id="seat-63" r="9.964" cy="125.122" cx="416.829"/>,
|
||||||
|
<circle key="seat-64" id="seat-64" r="9.964" cy="66.206" cx="426.902"/>,
|
||||||
|
<circle key="seat-65" id="seat-65" r="9.964" cy="143.999" cx="433.508"/>,
|
||||||
|
<circle key="seat-66" id="seat-66" r="9.964" cy="82.48" cx="447.457"/>,
|
||||||
|
<circle key="seat-67" id="seat-67" r="9.964" cy="102.267" cx="466.325"/>,
|
||||||
|
<circle key="seat-68" id="seat-68" r="9.964" cy="123.093" cx="481.494"/>,
|
||||||
|
<circle key="seat-69" id="seat-69" r="9.964" cy="-147.065" cx="400.833" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-70" id="seat-70" r="9.964" cy="-188.912" cx="412.614" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-71" id="seat-71" r="9.964" cy="-239.296" cx="426.798" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-72" id="seat-72" r="9.964" cy="-289.68" cx="440.983" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-73" id="seat-73" r="9.964" cy="-135.696" cx="424.411" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-74" id="seat-74" r="9.964" cy="-180.651" cx="437.369" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-75" id="seat-75" r="9.964" cy="-229.58" cx="452.656" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-76" id="seat-76" r="9.964" cy="-281.375" cx="467.381" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-77" id="seat-77" r="9.964" cy="-119.414" cx="445.127" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-78" id="seat-78" r="9.964" cy="-167.771" cx="459.741" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-79" id="seat-79" r="9.964" cy="-217.964" cx="475.026" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-80" id="seat-80" r="9.964" cy="-269.967" cx="491.592" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-81" id="seat-81" r="9.964" cy="-152.609" cx="478.289" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-82" id="seat-82" r="9.964" cy="-204.059" cx="496.265" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-83" id="seat-83" r="9.964" cy="-257.568" cx="513.597" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-84" id="seat-84" r="9.964" cy="-136.74" cx="497.701" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-85" id="seat-85" r="9.964" cy="-189.049" cx="514.627" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-86" id="seat-86" r="9.964" cy="-243.076" cx="533.889" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-87" id="seat-87" r="9.964" cy="-173.798" cx="533.284" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-88" id="seat-88" r="9.964" cy="-227.994" cx="554.048" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-89" id="seat-89" r="9.964" cy="-158.909" cx="551.496" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-90" id="seat-90" r="9.964" cy="-212.968" cx="572.857" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
<circle key="seat-91" id="seat-91" r="9.964" cy="-197.942" cx="591.665" transform="matrix(0.632908, 0.774227, -0.774227, 0.632908, 0, 0)"/>,
|
||||||
|
];
|
||||||
|
|
||||||
|
const renderedElements = seatElements.map((child, index) => {
|
||||||
|
// Si no hay datos para este asiento (ej. en la cámara de senadores que tiene menos de 92 bancas)
|
||||||
|
// lo pintamos de gris.
|
||||||
|
if (index >= seatData.length) {
|
||||||
|
return React.cloneElement(child, {
|
||||||
|
fill: '#E0E0E0',
|
||||||
|
stroke: '#ffffff',
|
||||||
|
strokeWidth: 1.5,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const seat = seatData[index];
|
||||||
|
const patternId = `stripes-${seat.color.replace('#', '')}`;
|
||||||
|
|
||||||
|
return React.cloneElement(child, {
|
||||||
|
fill: seat.isEnJuego ? `url(#${patternId})` : seat.color,
|
||||||
|
stroke: '#ffffff',
|
||||||
|
strokeWidth: 1.5,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 550 375" width={size} height={size * (375 / 550)} style={{ display: 'block', margin: 'auto' }}>
|
||||||
|
<defs>
|
||||||
|
{uniqueColors.map(color => {
|
||||||
|
const patternId = `stripes-${color.replace('#', '')}`;
|
||||||
|
return (
|
||||||
|
<pattern key={patternId} id={patternId} patternUnits="userSpaceOnUse" width="4" height="4" patternTransform="rotate(45)">
|
||||||
|
<rect width="4" height="4" fill={color}></rect>
|
||||||
|
<path d="M-1,1 l2,-2 M0,4 l4,-4 M3,5 l2,-2" stroke="rgba(255,255,255,0.7)" strokeWidth="3"></path>
|
||||||
|
</pattern>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</defs>
|
||||||
|
<g transform="matrix(1, 0, 0, 1, 1.166021, 0.583011)">
|
||||||
|
{renderedElements}
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
108
Elecciones-Web/frontend/src/components/SenateLayout.tsx
Normal file
108
Elecciones-Web/frontend/src/components/SenateLayout.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
// src/components/SenateLayout.tsx
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
// Las interfaces son las mismas que para el otro layout
|
||||||
|
interface SeatFillData {
|
||||||
|
color: string;
|
||||||
|
isEnJuego: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SenateLayoutProps {
|
||||||
|
seatData: SeatFillData[];
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SenateLayout: React.FC<SenateLayoutProps> = ({
|
||||||
|
seatData,
|
||||||
|
size = 400,
|
||||||
|
}) => {
|
||||||
|
const uniqueColors = [...new Set(seatData.map(d => d.color).filter(Boolean))];
|
||||||
|
|
||||||
|
// Plantilla estática de los 46 asientos del Senado, ordenados para el llenado por columnas.
|
||||||
|
const seatElements = [
|
||||||
|
<circle key="senate-seat-0" id="senate-seat-0" r="8" cy="354.956" cx="-121.933" transform="matrix(-1, 0, 0, 1, 0, 0)" />,
|
||||||
|
<circle key="senate-seat-1" id="senate-seat-1" r="8" cy="331.318" cx="-121.933" transform="matrix(-1, 0, 0, 1, 0, 0)" />,
|
||||||
|
<circle key="senate-seat-2" id="senate-seat-2" r="8" cy="307.598" cx="-121.933" transform="matrix(-1, 0, 0, 1, 0, 0)" />,
|
||||||
|
<circle key="senate-seat-3" id="senate-seat-3" r="8" cy="285.358" cx="-121.933" transform="matrix(-1, 0, 0, 1, 0, 0)" />,
|
||||||
|
<circle key="senate-seat-4" id="senate-seat-4" r="8" cy="261.241" cx="-121.933" transform="matrix(-1, 0, 0, 1, 0, 0)" />,
|
||||||
|
<circle key="senate-seat-5" id="senate-seat-5" r="8" cy="354.956" cx="-164.933" transform="matrix(-1, 0, 0, 1, 0, 0)" />,
|
||||||
|
<circle key="senate-seat-6" id="senate-seat-6" r="8" cy="333.318" cx="-164.933" transform="matrix(-1, 0, 0, 1, 0, 0)" />,
|
||||||
|
<circle key="senate-seat-7" id="senate-seat-7" r="8" cy="312.598" cx="-164.933" transform="matrix(-1, 0, 0, 1, 0, 0)" />,
|
||||||
|
<circle key="senate-seat-8" id="senate-seat-8" r="8" cy="292.358" cx="-164.933" transform="matrix(-1, 0, 0, 1, 0, 0)" />,
|
||||||
|
<circle key="senate-seat-9" id="senate-seat-9" r="8" cy="269.241" cx="-165.933" transform="matrix(-1, 0, 0, 1, 0, 0)" />,
|
||||||
|
<circle key="senate-seat-10" id="senate-seat-10" r="8" cy="246.521" cx="-170.309" transform="matrix(-1, 0, 0, 1, 0, 0)" />,
|
||||||
|
<circle key="senate-seat-11" id="senate-seat-11" r="8" cy="229.407" cx="-183.766" transform="matrix(-1, 0, 0, 1, 0, 0)" />,
|
||||||
|
<circle key="senate-seat-12" id="senate-seat-12" r="8" cy="217.294" cx="-201.175" transform="matrix(-1, 0, 0, 1, 0, 0)" />,
|
||||||
|
<circle key="senate-seat-13" id="senate-seat-13" r="8" cy="268.057" cx="-208.672" transform="matrix(-1, 0, 0, 1, 0, 0)" />,
|
||||||
|
<circle key="senate-seat-14" id="senate-seat-14" r="8" cy="292.358" cx="-206.672" transform="matrix(-1, 0, 0, 1, 0, 0)" />,
|
||||||
|
<circle key="senate-seat-15" id="senate-seat-15" r="8" cy="211.882" cx="-223.172" transform="matrix(-1, 0, 0, 1, 0, 0)" />,
|
||||||
|
<circle key="senate-seat-16" id="senate-seat-16" r="8" cy="252.557" cx="-227.172" transform="matrix(-1, 0, 0, 1, 0, 0)" />,
|
||||||
|
<circle key="senate-seat-17" id="senate-seat-17" r="8" cy="347.619" cx="243.841" />,
|
||||||
|
<circle key="senate-seat-18" id="senate-seat-18" r="8" cy="252.557" cx="258.729" />,
|
||||||
|
<circle key="senate-seat-19" id="senate-seat-19" r="8" cy="211.882" cx="262.728" />,
|
||||||
|
<circle key="senate-seat-20" id="senate-seat-20" r="8" cy="268.057" cx="277.228" />,
|
||||||
|
<circle key="senate-seat-21" id="senate-seat-21" r="8" cy="292.358" cx="279.228" />,
|
||||||
|
<circle key="senate-seat-22" id="senate-seat-22" r="8" cy="217.294" cx="284.726" />,
|
||||||
|
<circle key="senate-seat-23" id="senate-seat-23" r="8" cy="159.989" cx="295.143" transform="matrix(0.986454, -0.16404, 0.16404, 0.986454, -54.66, 61.555)" />,
|
||||||
|
<circle key="senate-seat-24" id="senate-seat-24" r="8" cy="229.407" cx="302.134" />,
|
||||||
|
<circle key="senate-seat-25" id="senate-seat-25" r="8" cy="170.401" cx="311.791" transform="matrix(0.986454, -0.16404, 0.16404, 0.986454, -49.543, 55.556)" />,
|
||||||
|
<circle key="senate-seat-26" id="senate-seat-26" r="8" cy="246.521" cx="315.591" />,
|
||||||
|
<circle key="senate-seat-27" id="senate-seat-27" r="8" cy="269.241" cx="319.968" />,
|
||||||
|
<circle key="senate-seat-28" id="senate-seat-28" r="8" cy="292.358" cx="320.968" />,
|
||||||
|
<circle key="senate-seat-29" id="senate-seat-29" r="8" cy="312.598" cx="320.968" />,
|
||||||
|
<circle key="senate-seat-30" id="senate-seat-30" r="8" cy="333.318" cx="320.968" />,
|
||||||
|
<circle key="senate-seat-31" id="senate-seat-31" r="8" cy="354.956" cx="320.968" />,
|
||||||
|
<circle key="senate-seat-32" id="senate-seat-32" r="8" cy="184.514" cx="327.2" transform="matrix(0.986454, -0.16404, 0.16404, 0.986454, -42.892, 52.164)" />,
|
||||||
|
<circle key="senate-seat-33" id="senate-seat-33" r="8" cy="184.514" cx="327.2" transform="matrix(0.986454, -0.16404, 0.16404, 0.986454, -21.946, 65.569)" />,
|
||||||
|
<circle key="senate-seat-34" id="senate-seat-34" r="8" cy="184.514" cx="327.2" transform="matrix(0.986454, -0.16404, 0.16404, 0.986454, -2.353, 84.436)" />,
|
||||||
|
<circle key="senate-seat-35" id="senate-seat-35" r="8" cy="184.514" cx="327.2" transform="matrix(0.986454, -0.16404, 0.16404, 0.986454, 8.933, 107.941)" />,
|
||||||
|
<circle key="senate-seat-36" id="senate-seat-36" r="8" cy="261.241" cx="363.968" />,
|
||||||
|
<circle key="senate-seat-37" id="senate-seat-37" r="8" cy="285.358" cx="363.968" />,
|
||||||
|
<circle key="senate-seat-38" id="senate-seat-38" r="8" cy="307.598" cx="363.968" />,
|
||||||
|
<circle key="senate-seat-39" id="senate-seat-39" r="8" cy="331.318" cx="363.968" />,
|
||||||
|
<circle key="senate-seat-40" id="senate-seat-40" r="8" cy="354.956" cx="363.968" />,
|
||||||
|
<circle key="senate-seat-41" id="senate-seat-41" r="8" cy="159.989" cx="295.143" transform="matrix(-0.986454, -0.16404, -0.16404, 0.986454, 540.544, 61.56)" />,
|
||||||
|
<circle key="senate-seat-42" id="senate-seat-42" r="8" cy="170.401" cx="311.791" transform="matrix(-0.986454, -0.16404, -0.16404, 0.986454, 535.427, 55.561)" />,
|
||||||
|
<circle key="senate-seat-43" id="senate-seat-43" r="8" cy="184.514" cx="327.2" transform="matrix(-0.986454, -0.16404, -0.16404, 0.986454, 528.776, 52.17)" />,
|
||||||
|
<circle key="senate-seat-44" id="senate-seat-44" r="8" cy="184.514" cx="327.2" transform="matrix(-0.986454, -0.16404, -0.16404, 0.986454, 507.83, 65.575)" />,
|
||||||
|
<circle key="senate-seat-45" id="senate-seat-45" r="8" cy="184.514" cx="327.2" transform="matrix(-0.986454, -0.16404, -0.16404, 0.986454, 488.237, 84.441)" />,
|
||||||
|
];
|
||||||
|
|
||||||
|
const renderedElements = seatElements.map((child, index) => {
|
||||||
|
if (index >= seatData.length) {
|
||||||
|
return React.cloneElement(child, {
|
||||||
|
fill: '#E0E0E0',
|
||||||
|
stroke: '#ffffff',
|
||||||
|
strokeWidth: 1.5,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const seat = seatData[index];
|
||||||
|
const patternId = `stripes-${seat.color.replace('#', '')}`;
|
||||||
|
|
||||||
|
return React.cloneElement(child, {
|
||||||
|
fill: seat.isEnJuego ? `url(#${patternId})` : seat.color,
|
||||||
|
stroke: '#ffffff',
|
||||||
|
strokeWidth: 1.5,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg viewBox="73.512 141.174 338.86 280.338" width={size} height={size * (280.338 / 338.86)} style={{ display: 'block', margin: 'auto' }}>
|
||||||
|
<defs>
|
||||||
|
{uniqueColors.map(color => {
|
||||||
|
const patternId = `stripes-${color.replace('#', '')}`;
|
||||||
|
return (
|
||||||
|
<pattern key={patternId} id={patternId} patternUnits="userSpaceOnUse" width="4" height="4" patternTransform="rotate(45)">
|
||||||
|
<rect width="4" height="4" fill={color}></rect>
|
||||||
|
<path d="M-1,1 l2,-2 M0,4 l4,-4 M3,5 l2,-2" stroke="rgba(255,255,255,0.7)" strokeWidth="3"></path>
|
||||||
|
</pattern>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
{renderedElements}
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
151
Elecciones-Web/frontend/src/components/TelegramaWidget.css
Normal file
151
Elecciones-Web/frontend/src/components/TelegramaWidget.css
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
/* src/components/TelegramaWidget.css */
|
||||||
|
.telegrama-container {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 20px auto;
|
||||||
|
font-family: "Public Sans", system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.telegrama-container h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #212529;
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-grid select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
font-size: 1em;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-grid select:disabled {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 0.75rem;
|
||||||
|
font-size: 1em;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--primary-accent-color);
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button:disabled {
|
||||||
|
background-color: #6c757d;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.telegrama-viewer {
|
||||||
|
min-height: 400px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.telegrama-viewer .message {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.telegrama-viewer .message.error {
|
||||||
|
color: #d62728;
|
||||||
|
}
|
||||||
|
|
||||||
|
.telegrama-content {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.telegrama-image-wrapper {
|
||||||
|
flex: 1 1 65%;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.telegrama-image-wrapper img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.telegrama-metadata {
|
||||||
|
flex: 1 1 35%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.telegrama-metadata h5 {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 1.1em;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item span {
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item strong {
|
||||||
|
color: #212529;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.telegrama-content {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
127
Elecciones-Web/frontend/src/components/TelegramaWidget.tsx
Normal file
127
Elecciones-Web/frontend/src/components/TelegramaWidget.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
// src/components/TelegramaWidget.tsx
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
getSecciones,
|
||||||
|
getMunicipiosPorSeccion,
|
||||||
|
getCircuitosPorMunicipio,
|
||||||
|
getEstablecimientosPorCircuito,
|
||||||
|
getMesasPorEstablecimiento,
|
||||||
|
getTelegramaPorId
|
||||||
|
} from '../apiService';
|
||||||
|
import type { TelegramaData, CatalogoItem } from '../types/types';
|
||||||
|
import './TelegramaWidget.css';
|
||||||
|
|
||||||
|
export const TelegramaWidget = () => {
|
||||||
|
// Estados para los filtros geográficos
|
||||||
|
const [secciones, setSecciones] = useState<CatalogoItem[]>([]);
|
||||||
|
const [municipios, setMunicipios] = useState<CatalogoItem[]>([]);
|
||||||
|
const [circuitos, setCircuitos] = useState<CatalogoItem[]>([]);
|
||||||
|
const [establecimientos, setEstablecimientos] = useState<CatalogoItem[]>([]);
|
||||||
|
const [mesas, setMesas] = useState<CatalogoItem[]>([]);
|
||||||
|
|
||||||
|
// Estados para los valores seleccionados
|
||||||
|
const [selectedSeccion, setSelectedSeccion] = useState('');
|
||||||
|
const [selectedMunicipio, setSelectedMunicipio] = useState('');
|
||||||
|
const [selectedCircuito, setSelectedCircuito] = useState('');
|
||||||
|
const [selectedEstablecimiento, setSelectedEstablecimiento] = useState('');
|
||||||
|
const [selectedMesa, setSelectedMesa] = useState('');
|
||||||
|
|
||||||
|
// Estados para la visualización del telegrama
|
||||||
|
const [telegrama, setTelegrama] = useState<TelegramaData | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Cargar secciones iniciales
|
||||||
|
useEffect(() => {
|
||||||
|
getSecciones().then(setSecciones);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Cargar municipios cuando cambia la sección
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedSeccion) {
|
||||||
|
setMunicipios([]); setCircuitos([]); setEstablecimientos([]); setMesas([]);
|
||||||
|
setSelectedMunicipio(''); setSelectedCircuito(''); setSelectedEstablecimiento(''); setSelectedMesa('');
|
||||||
|
getMunicipiosPorSeccion(selectedSeccion).then(setMunicipios);
|
||||||
|
}
|
||||||
|
}, [selectedSeccion]);
|
||||||
|
|
||||||
|
// Y así sucesivamente para los demás filtros...
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedMunicipio) {
|
||||||
|
setCircuitos([]); setEstablecimientos([]); setMesas([]);
|
||||||
|
setSelectedCircuito(''); setSelectedEstablecimiento(''); setSelectedMesa('');
|
||||||
|
getCircuitosPorMunicipio(selectedMunicipio).then(setCircuitos);
|
||||||
|
}
|
||||||
|
}, [selectedMunicipio]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedCircuito) {
|
||||||
|
setEstablecimientos([]); setMesas([]);
|
||||||
|
setSelectedEstablecimiento(''); setSelectedMesa('');
|
||||||
|
getEstablecimientosPorCircuito(selectedCircuito).then(setEstablecimientos);
|
||||||
|
}
|
||||||
|
}, [selectedCircuito]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedEstablecimiento) {
|
||||||
|
setMesas([]);
|
||||||
|
setSelectedMesa('');
|
||||||
|
getMesasPorEstablecimiento(selectedEstablecimiento).then(setMesas);
|
||||||
|
}
|
||||||
|
}, [selectedEstablecimiento]);
|
||||||
|
|
||||||
|
// Buscar el telegrama cuando se selecciona una mesa
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedMesa) {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setTelegrama(null);
|
||||||
|
getTelegramaPorId(selectedMesa)
|
||||||
|
.then(setTelegrama)
|
||||||
|
.catch(() => setError(`No se encontró el telegrama para la mesa seleccionada.`))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}
|
||||||
|
}, [selectedMesa]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="telegrama-container">
|
||||||
|
<h4>Consulta de Telegramas por Ubicación</h4>
|
||||||
|
<div className="filters-grid">
|
||||||
|
<select value={selectedSeccion} onChange={e => setSelectedSeccion(e.target.value)}>
|
||||||
|
<option value="">1. Sección</option>
|
||||||
|
{secciones.map(s => <option key={s.id} value={s.id}>{s.nombre}</option>)}
|
||||||
|
</select>
|
||||||
|
<select value={selectedMunicipio} onChange={e => setSelectedMunicipio(e.target.value)} disabled={!municipios.length}>
|
||||||
|
<option value="">2. Municipio</option>
|
||||||
|
{municipios.map(m => <option key={m.id} value={m.id}>{m.nombre}</option>)}
|
||||||
|
</select>
|
||||||
|
<select value={selectedCircuito} onChange={e => setSelectedCircuito(e.target.value)} disabled={!circuitos.length}>
|
||||||
|
<option value="">3. Circuito</option>
|
||||||
|
{circuitos.map(c => <option key={c.id} value={c.id}>{c.nombre}</option>)}
|
||||||
|
</select>
|
||||||
|
<select value={selectedEstablecimiento} onChange={e => setSelectedEstablecimiento(e.target.value)} disabled={!establecimientos.length}>
|
||||||
|
<option value="">4. Establecimiento</option>
|
||||||
|
{establecimientos.map(e => <option key={e.id} value={e.id}>{e.nombre}</option>)}
|
||||||
|
</select>
|
||||||
|
<select value={selectedMesa} onChange={e => setSelectedMesa(e.target.value)} disabled={!mesas.length}>
|
||||||
|
<option value="">5. Mesa</option>
|
||||||
|
{mesas.map(m => <option key={m.id} value={m.id}>{m.nombre}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="telegrama-viewer">
|
||||||
|
{loading && <div className="spinner"></div>}
|
||||||
|
{error && <p className="message error">{error}</p>}
|
||||||
|
{telegrama && (
|
||||||
|
<div className="telegrama-content">
|
||||||
|
<div className="telegrama-image-wrapper">
|
||||||
|
<img src={`data:image/jpeg;base64,${telegrama.contenidoBase64}`} alt={`Telegrama ${telegrama.id}`} />
|
||||||
|
</div>
|
||||||
|
{/* Metadata (opcional, se puede añadir aquí si se desea) */}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!loading && !telegrama && !error && <p className="message">Seleccione una mesa para visualizar el telegrama.</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,44 +1,49 @@
|
|||||||
/* src/components/TickerWidget.css */
|
/* src/components/TickerWidget.css */
|
||||||
.ticker-container {
|
.ticker-container {
|
||||||
background-color: #2a2a2e;
|
/* Se cambia a un fondo claro con borde y sombra sutil */
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
|
||||||
padding: 15px 20px;
|
padding: 15px 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: 20px auto;
|
margin: 20px auto;
|
||||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
font-family: "Public Sans", system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
color: #e0e0e0;
|
color: #333333; /* Color de texto por defecto */
|
||||||
}
|
}
|
||||||
|
|
||||||
.ticker-container.loading, .ticker-container.error {
|
.ticker-container.loading, .ticker-container.error {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: #999;
|
color: #757575; /* Color de texto atenuado */
|
||||||
}
|
}
|
||||||
|
|
||||||
.ticker-header {
|
.ticker-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-bottom: 1px solid #444;
|
border-bottom: 1px solid #e0e0e0; /* Borde más claro */
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ticker-header h3 {
|
.ticker-header h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: white;
|
color: #212529; /* Color de título oscuro */
|
||||||
font-size: 1.4em;
|
font-size: 1.2em;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ticker-stats {
|
.ticker-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ticker-stats strong {
|
.ticker-stats strong {
|
||||||
color: #a7c7e7;
|
color: #0073e6; /* Se usa el azul primario para destacar */
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,19 +69,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ticker-party .party-percent {
|
.ticker-party .party-percent {
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.party-bar-background {
|
.party-bar-background {
|
||||||
background-color: #444;
|
background-color: #e9ecef; /* Fondo de barra claro */
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.party-bar-foreground {
|
.party-bar-foreground {
|
||||||
background-color: #646cff;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: width 0.5s ease-in-out;
|
transition: width 0.5s ease-in-out;
|
||||||
|
/* El color de fondo se sigue aplicando desde el componente, esto es correcto */
|
||||||
}
|
}
|
||||||
@@ -13,14 +13,20 @@ const COLORS = [
|
|||||||
export const TickerWidget = () => {
|
export const TickerWidget = () => {
|
||||||
const [data, setData] = useState<ResumenProvincial | null>(null);
|
const [data, setData] = useState<ResumenProvincial | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
// Se añade un nuevo estado para manejar errores de forma explícita
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
|
// Se resetea el error en cada intento de carga
|
||||||
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const result = await getResumenProvincial();
|
const result = await getResumenProvincial();
|
||||||
setData(result);
|
setData(result);
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error("Error cargando resumen provincial:", error);
|
console.error("Error cargando resumen provincial:", err);
|
||||||
|
// Se guarda el mensaje de error para mostrarlo en la UI
|
||||||
|
setError("No se pudo conectar con el servidor para obtener el resumen provincial.");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -36,17 +42,22 @@ export const TickerWidget = () => {
|
|||||||
return <div className="ticker-container loading">Cargando resultados provinciales...</div>;
|
return <div className="ticker-container loading">Cargando resultados provinciales...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Si hay un error, se muestra el mensaje correspondiente
|
||||||
|
if (error) {
|
||||||
|
return <div className="ticker-container error">{error}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return <div className="ticker-container error">No se pudieron cargar los datos.</div>;
|
return <div className="ticker-container error">No hay datos disponibles.</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ticker-container">
|
<div className="ticker-container">
|
||||||
<div className="ticker-header">
|
<div className="ticker-header">
|
||||||
<h3>TOTAL PROVINCIA {data.provinciaNombre}</h3>
|
<h3>PROVINCIA BS. AS.</h3>
|
||||||
<div className="ticker-stats">
|
<div className="ticker-stats">
|
||||||
<span>Mesas Escrutadas: <strong>{formatPercent(data.porcentajeEscrutado)}</strong></span>
|
<span>Mesas Escrutadas: <strong>{formatPercent(data.porcentajeEscrutado)}</strong></span>
|
||||||
<span>Participación: <strong>{formatPercent(data.porcentajeParticipacion)}</strong></span>
|
<span>Participación Total: <strong>{formatPercent(data.porcentajeParticipacion)}</strong></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ticker-results">
|
<div className="ticker-results">
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
/* src/index.css */
|
/* src/index.css */
|
||||||
|
|
||||||
|
/* Se importa la tipografía "Public Sans" desde Google Fonts, la misma que usa eldia.com */
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Public+Sans:wght@400;500;700&display=swap');
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
/* Se establece la nueva fuente principal y las de respaldo */
|
||||||
|
font-family: "Public Sans", system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
/* Tema Claro por defecto */
|
/* Tema Claro inspirado en El Día */
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
color: #213547;
|
color: #333333; /* Texto principal más oscuro para mejor legibilidad */
|
||||||
background-color: #ffffff;
|
background-color: #ffffff; /* Fondo blanco */
|
||||||
|
|
||||||
font-synthesis: none;
|
font-synthesis: none;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
@@ -17,42 +22,39 @@
|
|||||||
|
|
||||||
a {
|
a {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #646cff;
|
color: #0073e6; /* Azul corporativo de El Día */
|
||||||
text-decoration: inherit;
|
text-decoration: none; /* Se quita el subrayado por defecto, como en el sitio */
|
||||||
}
|
}
|
||||||
a:hover {
|
a:hover {
|
||||||
color: #535bf2;
|
text-decoration: underline; /* Se añade subrayado en hover para claridad */
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
/* Se elimina el centrado vertical para un layout de página más tradicional */
|
||||||
place-items: center;
|
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 3.2em;
|
font-size: 2.5em; /* Ligeramente más pequeño para un look más de noticia */
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
|
color: #212529;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
border-radius: 8px;
|
border-radius: 4px; /* Bordes menos redondeados */
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
padding: 0.6em 1.2em;
|
padding: 0.6em 1.2em;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
/* Color de fondo para botones en tema claro */
|
background-color: #f8f9fa; /* Un gris muy claro para botones */
|
||||||
background-color: #f9f9f9;
|
border-color: #dee2e6; /* Borde sutil */
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color 0.25s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
button:hover {
|
button:hover {
|
||||||
border-color: #646cff;
|
border-color: #0073e6;
|
||||||
}
|
background-color: #eef7ff;
|
||||||
button:focus,
|
|
||||||
button:focus-visible {
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
}
|
||||||
@@ -58,5 +58,27 @@ export interface ResumenProvincial {
|
|||||||
resultados: AgrupacionResultado[];
|
resultados: AgrupacionResultado[];
|
||||||
votosAdicionales: VotosAdicionales;
|
votosAdicionales: VotosAdicionales;
|
||||||
}
|
}
|
||||||
export interface Banca { agrupacionNombre: string; bancas: number; }
|
|
||||||
export interface ProyeccionBancas { seccionNombre: string; proyeccion: Banca[]; }
|
export interface Banca {
|
||||||
|
agrupacionNombre: string;
|
||||||
|
bancas: number;
|
||||||
|
[key: string]: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProyeccionBancas {
|
||||||
|
seccionNombre: string;
|
||||||
|
proyeccion: Banca[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TelegramaData {
|
||||||
|
id: string;
|
||||||
|
ambitoGeograficoId: number;
|
||||||
|
contenidoBase64: string;
|
||||||
|
fechaEscaneo: string;
|
||||||
|
fechaTotalizacion: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CatalogoItem {
|
||||||
|
id: string;
|
||||||
|
nombre: string;
|
||||||
|
}
|
||||||
@@ -21,13 +21,12 @@ public class CatalogosController : ControllerBase
|
|||||||
[HttpGet("municipios")]
|
[HttpGet("municipios")]
|
||||||
public async Task<IActionResult> GetMunicipios()
|
public async Task<IActionResult> GetMunicipios()
|
||||||
{
|
{
|
||||||
// CORRECCIÓN: Los partidos/municipios corresponden al NivelId 30 (Sección)
|
|
||||||
var municipios = await _dbContext.AmbitosGeograficos
|
var municipios = await _dbContext.AmbitosGeograficos
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(a => a.NivelId == 30 && a.SeccionId != null) // <-- NivelId 30
|
.Where(a => a.NivelId == 30 && a.SeccionId != null)
|
||||||
.Select(a => new MunicipioSimpleDto
|
.Select(a => new MunicipioSimpleDto
|
||||||
{
|
{
|
||||||
Id = a.SeccionId!, // <-- Usamos SeccionId como el ID
|
Id = a.SeccionId!,
|
||||||
Nombre = a.Nombre
|
Nombre = a.Nombre
|
||||||
})
|
})
|
||||||
.OrderBy(m => m.Nombre)
|
.OrderBy(m => m.Nombre)
|
||||||
@@ -41,8 +40,82 @@ public class CatalogosController : ControllerBase
|
|||||||
{
|
{
|
||||||
var agrupaciones = await _dbContext.AgrupacionesPoliticas
|
var agrupaciones = await _dbContext.AgrupacionesPoliticas
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Select(a => new { a.Id, a.Nombre }) // Devuelve solo lo necesario
|
.Select(a => new { a.Id, a.Nombre })
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
return Ok(agrupaciones);
|
return Ok(agrupaciones);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("secciones-electorales")]
|
||||||
|
public async Task<IActionResult> GetSeccionesElectorales()
|
||||||
|
{
|
||||||
|
var secciones = await _dbContext.AmbitosGeograficos
|
||||||
|
.AsNoTracking()
|
||||||
|
// Buscamos los ámbitos que son Secciones Electorales (Nivel 20)
|
||||||
|
.Where(a => a.NivelId == 20 && a.SeccionProvincialId != null)
|
||||||
|
.Select(a => new MunicipioSimpleDto // Reutilizamos el DTO porque tiene la misma estructura {Id, Nombre}
|
||||||
|
{
|
||||||
|
Id = a.SeccionProvincialId!, // El ID que usaremos es el SeccionProvincialId
|
||||||
|
Nombre = a.Nombre
|
||||||
|
})
|
||||||
|
.OrderBy(s => s.Nombre)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return Ok(secciones);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nivel 20: Sección Electoral
|
||||||
|
[HttpGet("secciones")]
|
||||||
|
public async Task<IActionResult> GetSecciones()
|
||||||
|
{
|
||||||
|
var secciones = await _dbContext.AmbitosGeograficos.AsNoTracking()
|
||||||
|
.Where(a => a.NivelId == 20 && !string.IsNullOrEmpty(a.SeccionProvincialId))
|
||||||
|
.Select(a => new { Id = a.SeccionProvincialId, a.Nombre })
|
||||||
|
.Distinct().OrderBy(s => s.Nombre).ToListAsync();
|
||||||
|
return Ok(secciones);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nivel 30: Municipio (llamado "Sección" por la API)
|
||||||
|
[HttpGet("municipios/{seccionProvincialId}")]
|
||||||
|
public async Task<IActionResult> GetMunicipiosPorSeccion(string seccionProvincialId)
|
||||||
|
{
|
||||||
|
var municipios = await _dbContext.AmbitosGeograficos.AsNoTracking()
|
||||||
|
.Where(a => a.NivelId == 30 && a.SeccionProvincialId == seccionProvincialId && !string.IsNullOrEmpty(a.SeccionId))
|
||||||
|
.Select(a => new { Id = a.SeccionId, a.Nombre }) // El ID del Municipio es SeccionId
|
||||||
|
.Distinct().OrderBy(m => m.Nombre).ToListAsync();
|
||||||
|
return Ok(municipios);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nivel 50: Circuito (llamado "Municipio" por la API)
|
||||||
|
[HttpGet("circuitos/{municipioId}")]
|
||||||
|
public async Task<IActionResult> GetCircuitosPorMunicipio(string municipioId)
|
||||||
|
{
|
||||||
|
var circuitos = await _dbContext.AmbitosGeograficos.AsNoTracking()
|
||||||
|
// Un Circuito (Nivel 50) pertenece a un Municipio (Nivel 30) a través del campo 'SeccionId'
|
||||||
|
.Where(a => a.NivelId == 50 && a.SeccionId == municipioId && !string.IsNullOrEmpty(a.CircuitoId))
|
||||||
|
.Select(a => new { Id = a.CircuitoId, a.Nombre })
|
||||||
|
.Distinct().OrderBy(c => c.Nombre).ToListAsync();
|
||||||
|
return Ok(circuitos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nivel 60: Establecimiento (llamado "Colegio" por la API)
|
||||||
|
[HttpGet("establecimientos/{circuitoId}")]
|
||||||
|
public async Task<IActionResult> GetEstablecimientosPorCircuito(string circuitoId)
|
||||||
|
{
|
||||||
|
var establecimientos = await _dbContext.AmbitosGeograficos.AsNoTracking()
|
||||||
|
.Where(a => a.NivelId == 60 && a.CircuitoId == circuitoId && !string.IsNullOrEmpty(a.EstablecimientoId))
|
||||||
|
.Select(a => new { Id = a.EstablecimientoId, a.Nombre })
|
||||||
|
.Distinct().OrderBy(e => e.Nombre).ToListAsync();
|
||||||
|
return Ok(establecimientos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nivel 70: Mesa
|
||||||
|
[HttpGet("mesas/{establecimientoId}")]
|
||||||
|
public async Task<IActionResult> GetMesasPorEstablecimiento(string establecimientoId)
|
||||||
|
{
|
||||||
|
var mesas = await _dbContext.AmbitosGeograficos.AsNoTracking()
|
||||||
|
.Where(a => a.NivelId == 70 && a.EstablecimientoId == establecimientoId && !string.IsNullOrEmpty(a.MesaId))
|
||||||
|
.Select(a => new { Id = a.MesaId, a.Nombre })
|
||||||
|
.Distinct().OrderBy(m => m.Nombre).ToListAsync();
|
||||||
|
return Ok(mesas);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,8 @@ using Elecciones.Core.DTOs.ApiResponses;
|
|||||||
using Elecciones.Database;
|
using Elecciones.Database;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@@ -91,59 +93,85 @@ public class ResultadosController : ControllerBase
|
|||||||
[HttpGet("provincia/{distritoId}")]
|
[HttpGet("provincia/{distritoId}")]
|
||||||
public async Task<IActionResult> GetResultadosProvinciales(string distritoId)
|
public async Task<IActionResult> GetResultadosProvinciales(string distritoId)
|
||||||
{
|
{
|
||||||
// TODO: Esta lógica debe ser reemplazada para leer datos reales de la BD
|
_logger.LogInformation("Solicitud de resultados para la provincia con distritoId: {DistritoId}", distritoId);
|
||||||
// cuando el worker comience a ingestar los totales a nivel provincial.
|
|
||||||
// Por ahora, devolvemos datos simulados para permitir el desarrollo del frontend.
|
|
||||||
|
|
||||||
var ambito = await _dbContext.AmbitosGeograficos.AsNoTracking()
|
// PASO 1: Encontrar el ámbito geográfico de la provincia.
|
||||||
|
var provincia = await _dbContext.AmbitosGeograficos.AsNoTracking()
|
||||||
.FirstOrDefaultAsync(a => a.DistritoId == distritoId && a.NivelId == 10);
|
.FirstOrDefaultAsync(a => a.DistritoId == distritoId && a.NivelId == 10);
|
||||||
|
|
||||||
if (ambito == null)
|
if (provincia == null)
|
||||||
{
|
{
|
||||||
return NotFound(new { message = "No se encontró la provincia" });
|
_logger.LogWarning("No se encontró la provincia con distritoId: {DistritoId}", distritoId);
|
||||||
|
return NotFound(new { message = $"No se encontró la provincia con distritoId {distritoId}" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simulación
|
// PASO 2: Obtener el estado general del recuento para la provincia.
|
||||||
var random = new Random();
|
// Como las estadísticas generales (mesas, participación) son las mismas para todas las categorías,
|
||||||
var respuestaSimulada = new ResumenProvincialDto
|
// simplemente tomamos la primera que encontremos para este ámbito.
|
||||||
|
var estadoGeneral = await _dbContext.EstadosRecuentosGenerales.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(e => e.AmbitoGeograficoId == provincia.Id);
|
||||||
|
|
||||||
|
// PASO 3: Obtener el resumen de votos por agrupación para la provincia.
|
||||||
|
// Hacemos un JOIN manual entre ResumenesVotos y AgrupacionesPoliticas para obtener los nombres.
|
||||||
|
var resultados = await _dbContext.ResumenesVotos
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(r => r.AmbitoGeograficoId == provincia.Id)
|
||||||
|
.Join(
|
||||||
|
_dbContext.AgrupacionesPoliticas.AsNoTracking(),
|
||||||
|
resumen => resumen.AgrupacionPoliticaId,
|
||||||
|
agrupacion => agrupacion.Id,
|
||||||
|
(resumen, agrupacion) => new AgrupacionResultadoDto
|
||||||
{
|
{
|
||||||
ProvinciaNombre = ambito.Nombre,
|
Nombre = agrupacion.Nombre,
|
||||||
UltimaActualizacion = DateTime.UtcNow,
|
Votos = resumen.Votos,
|
||||||
PorcentajeEscrutado = 78.45m,
|
Porcentaje = resumen.VotosPorcentaje
|
||||||
PorcentajeParticipacion = 65.12m,
|
})
|
||||||
Resultados =
|
.OrderByDescending(r => r.Votos)
|
||||||
[
|
.ToListAsync();
|
||||||
new() { Nombre = "ALIANZA POR EL FUTURO", Votos = 2500000 + random.Next(1, 1000), Porcentaje = 45.12m },
|
|
||||||
new() { Nombre = "FRENTE DE AVANZADA", Votos = 2100000 + random.Next(1, 1000), Porcentaje = 38.78m },
|
// PASO 4: Construir el objeto de respuesta (DTO).
|
||||||
new() { Nombre = "UNION POPULAR", Votos = 800000 + random.Next(1, 1000), Porcentaje = 14.10m },
|
// Si no hay datos de recuento aún, usamos valores por defecto para evitar errores en el frontend.
|
||||||
],
|
var respuestaDto = new ResumenProvincialDto
|
||||||
VotosAdicionales = new VotosAdicionalesDto { EnBlanco = 150000, Nulos = 80000, Recurridos = 1200 }
|
{
|
||||||
|
ProvinciaNombre = provincia.Nombre,
|
||||||
|
UltimaActualizacion = estadoGeneral?.FechaTotalizacion ?? DateTime.UtcNow,
|
||||||
|
PorcentajeEscrutado = estadoGeneral?.MesasTotalizadasPorcentaje ?? 0,
|
||||||
|
PorcentajeParticipacion = estadoGeneral?.ParticipacionPorcentaje ?? 0,
|
||||||
|
Resultados = resultados,
|
||||||
|
// NOTA: Los votos adicionales (nulos, en blanco) no están en la tabla de resumen provincial.
|
||||||
|
// Esto es una mejora pendiente en el Worker. Por ahora, devolvemos 0.
|
||||||
|
VotosAdicionales = new VotosAdicionalesDto { EnBlanco = 0, Nulos = 0, Recurridos = 0 }
|
||||||
};
|
};
|
||||||
|
|
||||||
return Ok(await Task.FromResult(respuestaSimulada));
|
_logger.LogInformation("Devolviendo {NumResultados} resultados de agrupaciones para la provincia.", respuestaDto.Resultados.Count);
|
||||||
|
|
||||||
|
return Ok(respuestaDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpGet("bancas/{seccionId}")]
|
[HttpGet("bancas/{seccionId}")]
|
||||||
public async Task<IActionResult> GetBancasPorSeccion(string seccionId)
|
public async Task<IActionResult> GetBancasPorSeccion(string seccionId)
|
||||||
{
|
{
|
||||||
// 1. Buscamos el ámbito de la sección electoral
|
// 1. Buscamos el ámbito usando 'SeccionProvincialId'.
|
||||||
|
// La API oficial usa este campo para las secciones electorales.
|
||||||
|
// Además, el worker guarda estas secciones con NivelId = 20, por lo que lo usamos aquí para consistencia.
|
||||||
var seccion = await _dbContext.AmbitosGeograficos
|
var seccion = await _dbContext.AmbitosGeograficos
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.FirstOrDefaultAsync(a => a.SeccionId == seccionId && a.NivelId == 4); // Nivel 4 = Sección Electoral
|
.FirstOrDefaultAsync(a => a.SeccionProvincialId == seccionId && a.NivelId == 20); // Nivel 20 = Sección Electoral Provincial
|
||||||
|
|
||||||
if (seccion == null)
|
if (seccion == null)
|
||||||
{
|
{
|
||||||
|
_logger.LogWarning("No se encontró la sección electoral con SeccionProvincialId: {SeccionId}", seccionId);
|
||||||
return NotFound(new { message = $"No se encontró la sección electoral con ID {seccionId}" });
|
return NotFound(new { message = $"No se encontró la sección electoral con ID {seccionId}" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Buscamos todas las proyecciones para ese ámbito, incluyendo el nombre de la agrupación
|
// 2. Buscamos todas las proyecciones para ese ámbito (usando su clave primaria 'Id')
|
||||||
var proyecciones = await _dbContext.ProyeccionesBancas
|
var proyecciones = await _dbContext.ProyeccionesBancas
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(p => p.AgrupacionPolitica) // Incluimos el nombre del partido
|
.Include(p => p.AgrupacionPolitica)
|
||||||
.Where(p => p.AmbitoGeograficoId == seccion.Id)
|
.Where(p => p.AmbitoGeograficoId == seccion.Id)
|
||||||
.Select(p => new
|
.Select(p => new
|
||||||
{
|
{
|
||||||
// Creamos un objeto anónimo para la respuesta, más limpio que un DTO para este caso simple
|
|
||||||
AgrupacionNombre = p.AgrupacionPolitica.Nombre,
|
AgrupacionNombre = p.AgrupacionPolitica.Nombre,
|
||||||
Bancas = p.NroBancas
|
Bancas = p.NroBancas
|
||||||
})
|
})
|
||||||
@@ -152,10 +180,12 @@ public class ResultadosController : ControllerBase
|
|||||||
|
|
||||||
if (!proyecciones.Any())
|
if (!proyecciones.Any())
|
||||||
{
|
{
|
||||||
|
// Este caso es posible si aún no hay proyecciones calculadas para esta sección.
|
||||||
|
_logger.LogWarning("No se encontraron proyecciones de bancas para la sección: {SeccionNombre}", seccion.Nombre);
|
||||||
return NotFound(new { message = $"No se han encontrado proyecciones de bancas para la sección {seccion.Nombre}" });
|
return NotFound(new { message = $"No se han encontrado proyecciones de bancas para la sección {seccion.Nombre}" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Devolvemos un objeto que contiene el nombre de la sección y la lista de resultados
|
// 3. Devolvemos la respuesta
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
SeccionNombre = seccion.Nombre,
|
SeccionNombre = seccion.Nombre,
|
||||||
@@ -263,4 +293,40 @@ public class ResultadosController : ControllerBase
|
|||||||
|
|
||||||
return Ok(respuestaDto);
|
return Ok(respuestaDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("composicion-congreso")]
|
||||||
|
public IActionResult GetComposicionCongreso()
|
||||||
|
{
|
||||||
|
var diputados = new
|
||||||
|
{
|
||||||
|
CamaraNombre = "Cámara de Diputados",
|
||||||
|
TotalBancas = 92,
|
||||||
|
BancasEnJuego = 46,
|
||||||
|
Partidos = new[]
|
||||||
|
{
|
||||||
|
// --- DATOS ACTUALIZADOS CON 'BANCASENJUEGO' ---
|
||||||
|
new { Id = "501", Nombre = "CANDIDATURA 501", BancasTotales = 2, BancasEnJuego = 1, Color = "#d62728" },
|
||||||
|
new { Id = "513", Nombre = "CANDIDATURA 513", BancasTotales = 37, BancasEnJuego = 19, Color = "#1f77b4" },
|
||||||
|
new { Id = "516", Nombre = "CANDIDATURA 516", BancasTotales = 18, BancasEnJuego = 8, Color = "#2ca02c" },
|
||||||
|
new { Id = "511", Nombre = "CANDIDATURA 511", BancasTotales = 22, BancasEnJuego = 12, Color = "#ff7f0e" },
|
||||||
|
new { Id = "507", Nombre = "CANDIDATURA 507", BancasTotales = 13, BancasEnJuego = 6, Color = "#9467bd" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var senadores = new
|
||||||
|
{
|
||||||
|
CamaraNombre = "Cámara de Senadores",
|
||||||
|
TotalBancas = 46,
|
||||||
|
BancasEnJuego = 23,
|
||||||
|
Partidos = new[]
|
||||||
|
{
|
||||||
|
new { Id = "513_S", Nombre = "CANDIDATURA 513", BancasTotales = 21, BancasEnJuego = 10, Color = "#1f77b4" },
|
||||||
|
new { Id = "516_S", Nombre = "CANDIDATURA 516", BancasTotales = 9, BancasEnJuego = 5, Color = "#2ca02c" },
|
||||||
|
new { Id = "511_S", Nombre = "CANDIDATURA 511", BancasTotales = 11, BancasEnJuego = 6, Color = "#ff7f0e" },
|
||||||
|
new { Id = "507_S", Nombre = "CANDIDATURA 507", BancasTotales = 5, BancasEnJuego = 2, Color = "#9467bd" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(new { Diputados = diputados, Senadores = senadores });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -40,16 +40,31 @@ public class TelegramasController : ControllerBase
|
|||||||
[HttpGet("{mesaId}")]
|
[HttpGet("{mesaId}")]
|
||||||
public async Task<IActionResult> GetTelegramaPorId(string mesaId)
|
public async Task<IActionResult> GetTelegramaPorId(string mesaId)
|
||||||
{
|
{
|
||||||
|
// PASO 1: Buscar el ámbito geográfico que corresponde a esta MesaId.
|
||||||
|
// Esto nos dará el ID interno (clave primaria) del ámbito.
|
||||||
|
var ambitoMesa = await _dbContext.AmbitosGeograficos
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(a => a.MesaId == mesaId && a.NivelId == 70);
|
||||||
|
|
||||||
|
if (ambitoMesa == null)
|
||||||
|
{
|
||||||
|
// Si no encontramos el ámbito, significa que el MesaId no es válido.
|
||||||
|
return NotFound(new { message = $"No se encontró un ámbito geográfico para la mesa con ID {mesaId}" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// PASO 2: Usar el ID interno del ámbito para buscar el telegrama.
|
||||||
|
// Esta es la relación correcta entre las tablas.
|
||||||
var telegrama = await _dbContext.Telegramas
|
var telegrama = await _dbContext.Telegramas
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.FirstOrDefaultAsync(t => t.Id == mesaId);
|
.FirstOrDefaultAsync(t => t.AmbitoGeograficoId == ambitoMesa.Id);
|
||||||
|
|
||||||
if (telegrama == null)
|
if (telegrama == null)
|
||||||
{
|
{
|
||||||
return NotFound(new { message = $"No se encontró el telegrama con ID {mesaId}" });
|
// Si encontramos el ámbito pero no el telegrama, significa que aún no ha sido descargado.
|
||||||
|
return NotFound(new { message = $"Se encontró la mesa, pero su telegrama aún no ha sido procesado." });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Devolvemos todos los datos del telegrama
|
// Devolvemos los datos del telegrama encontrado.
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
telegrama.Id,
|
telegrama.Id,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Api")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Api")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+13c6accd156385dfc057e9dd765349535f494139")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+8192185bc567f6103d3457235798b1cefca61239")]
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Api")]
|
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Api")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Api")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Api")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["Dji\u002Bta/0e7zUKw3oe\u002BriV3kbWxZ93FP2z2QIYsHXTl4=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","ywKm3DCyXg4YCbZAIx3JUlT8N4Irff3GswYUVDST\u002BjQ=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/dERIyc1JOhwFtrVKNy7mb/2h9NWmiwO1FwPtFm4Im0=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","yTq/Ml6GIPmYajoAWVY9apLuQ9\u002B9//ZN/AKmDBmwvBg="],"CachedAssets":{},"CachedCopyCandidates":{}}
|
{"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["Dji\u002Bta/0e7zUKw3oe\u002BriV3kbWxZ93FP2z2QIYsHXTl4=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","ywKm3DCyXg4YCbZAIx3JUlT8N4Irff3GswYUVDST\u002BjQ=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/GfbpJthEWmsuz0uFx1QLHM7gyM1wLLeQgAIl4SzUD4=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","pJWMHkD90O0wJmytQzhndfB28\u002Bh41dgWfu2x4/0cWSQ=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","b188IfzQfT2K5xJ83wmDTA9\u002B6jiF27JwsgOAO2dXlcY="],"CachedAssets":{},"CachedCopyCandidates":{}}
|
||||||
@@ -1 +1 @@
|
|||||||
{"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["Dji\u002Bta/0e7zUKw3oe\u002BriV3kbWxZ93FP2z2QIYsHXTl4=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","ywKm3DCyXg4YCbZAIx3JUlT8N4Irff3GswYUVDST\u002BjQ=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/dERIyc1JOhwFtrVKNy7mb/2h9NWmiwO1FwPtFm4Im0=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","yTq/Ml6GIPmYajoAWVY9apLuQ9\u002B9//ZN/AKmDBmwvBg="],"CachedAssets":{},"CachedCopyCandidates":{}}
|
{"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["Dji\u002Bta/0e7zUKw3oe\u002BriV3kbWxZ93FP2z2QIYsHXTl4=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","ywKm3DCyXg4YCbZAIx3JUlT8N4Irff3GswYUVDST\u002BjQ=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/GfbpJthEWmsuz0uFx1QLHM7gyM1wLLeQgAIl4SzUD4=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","pJWMHkD90O0wJmytQzhndfB28\u002Bh41dgWfu2x4/0cWSQ=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","b188IfzQfT2K5xJ83wmDTA9\u002B6jiF27JwsgOAO2dXlcY="],"CachedAssets":{},"CachedCopyCandidates":{}}
|
||||||
@@ -13,7 +13,7 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Core")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Core")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+13c6accd156385dfc057e9dd765349535f494139")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+8192185bc567f6103d3457235798b1cefca61239")]
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Core")]
|
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Core")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Core")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Core")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Database")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Database")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+13c6accd156385dfc057e9dd765349535f494139")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+8192185bc567f6103d3457235798b1cefca61239")]
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Database")]
|
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Database")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Database")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Database")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Infrastructure")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Infrastructure")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+13c6accd156385dfc057e9dd765349535f494139")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+8192185bc567f6103d3457235798b1cefca61239")]
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Infrastructure")]
|
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Infrastructure")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Infrastructure")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Infrastructure")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|||||||
@@ -237,8 +237,6 @@ public class LowPriorityDataWorker : BackgroundService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sondea la proyección de bancas a nivel Provincial y por Sección Electoral.
|
/// Sondea la proyección de bancas a nivel Provincial y por Sección Electoral.
|
||||||
/// Esta versión es completamente robusta: maneja respuestas de API vacías o con fechas mal formadas,
|
/// Esta versión es completamente robusta: maneja respuestas de API vacías o con fechas mal formadas,
|
||||||
@@ -410,21 +408,16 @@ public class LowPriorityDataWorker : BackgroundService
|
|||||||
|
|
||||||
if (!partidos.Any() || !categorias.Any()) return;
|
if (!partidos.Any() || !categorias.Any()) return;
|
||||||
|
|
||||||
// --- LÓGICA DE GOTEO LENTO ---
|
|
||||||
// Procesamos una combinación (partido/categoría) a la vez.
|
|
||||||
foreach (var partido in partidos)
|
foreach (var partido in partidos)
|
||||||
{
|
{
|
||||||
foreach (var categoria in categorias)
|
foreach (var categoria in categorias)
|
||||||
{
|
{
|
||||||
// Si la aplicación se apaga, salimos inmediatamente.
|
|
||||||
if (stoppingToken.IsCancellationRequested) return;
|
if (stoppingToken.IsCancellationRequested) return;
|
||||||
|
|
||||||
// Obtenemos la lista de IDs.
|
|
||||||
var listaTelegramasApi = await _apiService.GetTelegramasTotalizadosAsync(authToken, partido.DistritoId!, partido.SeccionId!, categoria.Id);
|
var listaTelegramasApi = await _apiService.GetTelegramasTotalizadosAsync(authToken, partido.DistritoId!, partido.SeccionId!, categoria.Id);
|
||||||
|
|
||||||
if (listaTelegramasApi is { Count: > 0 })
|
if (listaTelegramasApi is { Count: > 0 })
|
||||||
{
|
{
|
||||||
// Usamos un DbContext propio para este bloque para asegurar que los cambios se guarden.
|
|
||||||
using var innerScope = _serviceProvider.CreateScope();
|
using var innerScope = _serviceProvider.CreateScope();
|
||||||
var innerDbContext = innerScope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
var innerDbContext = innerScope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
||||||
|
|
||||||
@@ -439,32 +432,44 @@ public class LowPriorityDataWorker : BackgroundService
|
|||||||
{
|
{
|
||||||
_logger.LogInformation("Se encontraron {count} telegramas nuevos en '{partido}' para '{cat}'. Descargando...", nuevosTelegramasIds.Count, partido.Nombre, categoria.Nombre);
|
_logger.LogInformation("Se encontraron {count} telegramas nuevos en '{partido}' para '{cat}'. Descargando...", nuevosTelegramasIds.Count, partido.Nombre, categoria.Nombre);
|
||||||
|
|
||||||
// Descargamos los archivos de uno en uno, con una pausa entre cada uno.
|
|
||||||
foreach (var mesaId in nuevosTelegramasIds)
|
foreach (var mesaId in nuevosTelegramasIds)
|
||||||
{
|
{
|
||||||
if (stoppingToken.IsCancellationRequested) return;
|
if (stoppingToken.IsCancellationRequested) return;
|
||||||
|
|
||||||
var telegramaFile = await _apiService.GetTelegramaFileAsync(authToken, mesaId);
|
var telegramaFile = await _apiService.GetTelegramaFileAsync(authToken, mesaId);
|
||||||
if (telegramaFile != null)
|
if (telegramaFile != null)
|
||||||
|
{
|
||||||
|
// --- INICIO DE LA CORRECCIÓN ---
|
||||||
|
// 1. Buscamos el AmbitoGeografico específico de la MESA que estamos procesando.
|
||||||
|
var ambitoMesa = await innerDbContext.AmbitosGeograficos
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(a => a.MesaId == mesaId, stoppingToken);
|
||||||
|
|
||||||
|
// 2. Solo guardamos el telegrama si encontramos su ámbito de mesa correspondiente.
|
||||||
|
if (ambitoMesa != null)
|
||||||
{
|
{
|
||||||
var nuevoTelegrama = new Telegrama
|
var nuevoTelegrama = new Telegrama
|
||||||
{
|
{
|
||||||
Id = telegramaFile.NombreArchivo,
|
Id = telegramaFile.NombreArchivo,
|
||||||
AmbitoGeograficoId = partido.Id,
|
// 3. Usamos el ID del ÁMBITO DE LA MESA, no el del municipio.
|
||||||
|
AmbitoGeograficoId = ambitoMesa.Id,
|
||||||
ContenidoBase64 = telegramaFile.Imagen,
|
ContenidoBase64 = telegramaFile.Imagen,
|
||||||
FechaEscaneo = DateTime.Parse(telegramaFile.FechaEscaneo).ToUniversalTime(),
|
FechaEscaneo = DateTime.Parse(telegramaFile.FechaEscaneo).ToUniversalTime(),
|
||||||
FechaTotalizacion = DateTime.Parse(telegramaFile.FechaTotalizacion).ToUniversalTime()
|
FechaTotalizacion = DateTime.Parse(telegramaFile.FechaTotalizacion).ToUniversalTime()
|
||||||
};
|
};
|
||||||
await innerDbContext.Telegramas.AddAsync(nuevoTelegrama, stoppingToken);
|
await innerDbContext.Telegramas.AddAsync(nuevoTelegrama, stoppingToken);
|
||||||
}
|
}
|
||||||
// PAUSA DELIBERADA: Esperamos un poco para no parecer un bot.
|
else
|
||||||
await Task.Delay(250, stoppingToken); // 250ms de espera = 4 peticiones/segundo máximo.
|
{
|
||||||
|
_logger.LogWarning("No se encontró un ámbito geográfico para la mesa con MesaId {MesaId}. El telegrama no será guardado.", mesaId);
|
||||||
|
}
|
||||||
|
// --- FIN DE LA CORRECCIÓN ---
|
||||||
|
}
|
||||||
|
await Task.Delay(250, stoppingToken);
|
||||||
}
|
}
|
||||||
await innerDbContext.SaveChangesAsync(stoppingToken);
|
await innerDbContext.SaveChangesAsync(stoppingToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PAUSA DELIBERADA: Esperamos un poco entre cada consulta de lista de telegramas.
|
|
||||||
await Task.Delay(100, stoppingToken);
|
await Task.Delay(100, stoppingToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user