Feat Front Widgets Refactizados y Ajustes Backend
This commit is contained in:
42
Elecciones-Web/Restaurar/Nuevos/App.css
Normal file
42
Elecciones-Web/Restaurar/Nuevos/App.css
Normal file
@@ -0,0 +1,42 @@
|
||||
#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;
|
||||
}
|
||||
18
Elecciones-Web/Restaurar/Nuevos/App.tsx
Normal file
18
Elecciones-Web/Restaurar/Nuevos/App.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
// 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;
|
||||
58
Elecciones-Web/Restaurar/Nuevos/components/MapaBsAs.css
Normal file
58
Elecciones-Web/Restaurar/Nuevos/components/MapaBsAs.css
Normal file
@@ -0,0 +1,58 @@
|
||||
/* 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;
|
||||
}
|
||||
217
Elecciones-Web/Restaurar/Nuevos/components/MapaBsAs.tsx
Normal file
217
Elecciones-Web/Restaurar/Nuevos/components/MapaBsAs.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
// 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;
|
||||
68
Elecciones-Web/Restaurar/Nuevos/index.css
Normal file
68
Elecciones-Web/Restaurar/Nuevos/index.css
Normal file
@@ -0,0 +1,68 @@
|
||||
: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;
|
||||
}
|
||||
}
|
||||
17
Elecciones-Web/Restaurar/Nuevos/main.tsx
Normal file
17
Elecciones-Web/Restaurar/Nuevos/main.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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
12
Elecciones-Web/Restaurar/Nuevos/types/custom.d.ts
vendored
Normal file
12
Elecciones-Web/Restaurar/Nuevos/types/custom.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
// 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;
|
||||
}
|
||||
56
Elecciones-Web/Restaurar/apiService.ts
Normal file
56
Elecciones-Web/Restaurar/apiService.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
// 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;
|
||||
};
|
||||
35
Elecciones-Web/Restaurar/components/BancasWidget.css
Normal file
35
Elecciones-Web/Restaurar/components/BancasWidget.css
Normal file
@@ -0,0 +1,35 @@
|
||||
/* 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;
|
||||
}
|
||||
91
Elecciones-Web/Restaurar/components/BancasWidget.tsx
Normal file
91
Elecciones-Web/Restaurar/components/BancasWidget.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
// 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>
|
||||
);
|
||||
};
|
||||
82
Elecciones-Web/Restaurar/components/TickerWidget.css
Normal file
82
Elecciones-Web/Restaurar/components/TickerWidget.css
Normal file
@@ -0,0 +1,82 @@
|
||||
/* 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;
|
||||
}
|
||||
67
Elecciones-Web/Restaurar/components/TickerWidget.tsx
Normal file
67
Elecciones-Web/Restaurar/components/TickerWidget.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
// 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>
|
||||
);
|
||||
};
|
||||
1352
Elecciones-Web/frontend/package-lock.json
generated
1352
Elecciones-Web/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,14 +10,18 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nivo/waffle": "^0.99.0",
|
||||
"@tanstack/react-query": "^5.85.5",
|
||||
"@types/d3-geo": "^3.1.0",
|
||||
"axios": "^1.11.0",
|
||||
"d3": "^7.9.0",
|
||||
"d3-geo": "^3.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-tooltip": "^5.29.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.33.0",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"@types/react": "^19.1.10",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,145 +0,0 @@
|
||||
{
|
||||
"100": "100",
|
||||
"101": "101",
|
||||
"102": "102",
|
||||
"103": "103",
|
||||
"104": "104",
|
||||
"105": "105",
|
||||
"106": "106",
|
||||
"107": "107",
|
||||
"108": "108",
|
||||
"109": "109",
|
||||
"110": "110",
|
||||
"111": "111",
|
||||
"113": "113",
|
||||
"114": "114",
|
||||
"115": "115",
|
||||
"116": "116",
|
||||
"117": "117",
|
||||
"118": "118",
|
||||
"119": "119",
|
||||
"120": "120",
|
||||
"121": "121",
|
||||
"122": "122",
|
||||
"123": "123",
|
||||
"124": "124",
|
||||
"125": "125",
|
||||
"126": "126",
|
||||
"127": "127",
|
||||
"128": "128",
|
||||
"129": "129",
|
||||
"130": "130",
|
||||
"131": "131",
|
||||
"132": "132",
|
||||
"133": "133",
|
||||
"134": "134",
|
||||
"135": "135",
|
||||
"136": "136",
|
||||
"137": "137",
|
||||
"309": "309",
|
||||
"314": "314",
|
||||
"338": "338",
|
||||
"357": "357",
|
||||
"387": "387",
|
||||
"396": "396",
|
||||
"398": "398",
|
||||
"399": "399",
|
||||
"045": "045",
|
||||
"055": "055",
|
||||
"070": "070",
|
||||
"030": "030",
|
||||
"074": "074",
|
||||
"003": "003",
|
||||
"086": "086",
|
||||
"082": "082",
|
||||
"063": "063",
|
||||
"006": "006",
|
||||
"047": "047",
|
||||
"084": "084",
|
||||
"026": "026",
|
||||
"025": "025",
|
||||
"007": "007",
|
||||
"033": "033",
|
||||
"076": "076",
|
||||
"054": "054",
|
||||
"072": "072",
|
||||
"012": "012",
|
||||
"004": "004",
|
||||
"077": "077",
|
||||
"087": "087",
|
||||
"098": "098",
|
||||
"015": "015",
|
||||
"080": "080",
|
||||
"046": "046",
|
||||
"038": "038",
|
||||
"032": "032",
|
||||
"014": "014",
|
||||
"064": "064",
|
||||
"083": "083",
|
||||
"049": "049",
|
||||
"058": "058",
|
||||
"023": "023",
|
||||
"060": "060",
|
||||
"018": "018",
|
||||
"042": "042",
|
||||
"062": "062",
|
||||
"066": "066",
|
||||
"041": "041",
|
||||
"053": "053",
|
||||
"057": "057",
|
||||
"001": "001",
|
||||
"005": "005",
|
||||
"002": "002",
|
||||
"031": "031",
|
||||
"008": "008",
|
||||
"009": "009",
|
||||
"013": "013",
|
||||
"010": "010",
|
||||
"011": "011",
|
||||
"019": "019",
|
||||
"017": "017",
|
||||
"097": "097",
|
||||
"020": "020",
|
||||
"016": "016",
|
||||
"037": "037",
|
||||
"021": "021",
|
||||
"027": "027",
|
||||
"022": "022",
|
||||
"036": "036",
|
||||
"029": "029",
|
||||
"028": "028",
|
||||
"035": "035",
|
||||
"056": "056",
|
||||
"044": "044",
|
||||
"034": "034",
|
||||
"040": "040",
|
||||
"039": "039",
|
||||
"043": "043",
|
||||
"051": "051",
|
||||
"052": "052",
|
||||
"059": "059",
|
||||
"065": "065",
|
||||
"061": "061",
|
||||
"024": "024",
|
||||
"068": "068",
|
||||
"050": "050",
|
||||
"078": "078",
|
||||
"079": "079",
|
||||
"067": "067",
|
||||
"096": "096",
|
||||
"099": "099",
|
||||
"075": "075",
|
||||
"081": "081",
|
||||
"069": "069",
|
||||
"073": "073",
|
||||
"089": "089",
|
||||
"090": "090",
|
||||
"085": "085",
|
||||
"088": "088",
|
||||
"091": "091",
|
||||
"093": "093",
|
||||
"071": "071",
|
||||
"095": "095",
|
||||
"092": "092",
|
||||
"094": "094"
|
||||
}
|
||||
1
Elecciones-Web/frontend/public/partidos-bsas.topojson
Normal file
1
Elecciones-Web/frontend/public/partidos-bsas.topojson
Normal file
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -40,7 +40,3 @@
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
@@ -1,69 +1,20 @@
|
||||
// src/App.tsx
|
||||
import { useState } from 'react';
|
||||
import { MunicipioWidget } from './components/MunicipioWidget';
|
||||
import { MunicipioSelector } from './components/MunicipioSelector';
|
||||
import './App.css';
|
||||
import { ResumenProvincialWidget } from './components/ResumenProvincialWidget';
|
||||
import { BancasWidget } from './components/BancasWidget';
|
||||
import { TelegramasView } from './components/TelegramasView';
|
||||
import './App.css'
|
||||
//import { BancasWidget } from './components/BancasWidget'
|
||||
import MapaBsAs from './components/MapaBsAs'
|
||||
import { TickerWidget } from './components/TickerWidget'
|
||||
|
||||
function App() {
|
||||
const [selectedMunicipioId, setSelectedMunicipioId] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Elecciones 2025 - Resultados en Vivo</h1>
|
||||
<section>
|
||||
<ResumenProvincialWidget distritoId="02" />
|
||||
</section>
|
||||
|
||||
<hr />
|
||||
|
||||
|
||||
|
||||
<section>
|
||||
{/* Usamos el ID del distrito de Bs As ("02") */}
|
||||
<ResumenProvincialWidget distritoId="02" />
|
||||
</section>
|
||||
|
||||
<hr />
|
||||
|
||||
<section>
|
||||
<h2>Consulta por Municipio</h2>
|
||||
<MunicipioSelector onMunicipioChange={setSelectedMunicipioId} municipios={[]} />
|
||||
{selectedMunicipioId && (
|
||||
<div style={{ marginTop: '20px' }}>
|
||||
<MunicipioWidget municipioId={selectedMunicipioId} />
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Proyección de Bancas</h2>
|
||||
{/* Usamos el ID de la sección de La Plata ("0001") como ejemplo */}
|
||||
<BancasWidget seccionId="0001" />
|
||||
</section>
|
||||
|
||||
<hr />
|
||||
|
||||
<section>
|
||||
<h2>Consulta de Resultados por Municipio</h2>
|
||||
<MunicipioSelector onMunicipioChange={setSelectedMunicipioId} municipios={[]} />
|
||||
{selectedMunicipioId && (
|
||||
<div style={{ marginTop: '20px' }}>
|
||||
<MunicipioWidget municipioId={selectedMunicipioId} />
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<hr />
|
||||
|
||||
<section>
|
||||
<h2>Explorador de Telegramas</h2>
|
||||
<TelegramasView />
|
||||
</section>
|
||||
<h1>Resultados Electorales - Provincia de Buenos Aires</h1>
|
||||
<main>
|
||||
<TickerWidget />
|
||||
{/*<BancasWidget />*/}
|
||||
<MapaBsAs />
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default App
|
||||
56
Elecciones-Web/frontend/src/apiService.ts
Normal file
56
Elecciones-Web/frontend/src/apiService.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
// src/apiService.ts
|
||||
import axios from 'axios';
|
||||
import type { MunicipioSimple, MunicipioDetalle, ResumenProvincial, ProyeccionBancas } 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'; // 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;
|
||||
};
|
||||
35
Elecciones-Web/frontend/src/components/BancasWidget.css
Normal file
35
Elecciones-Web/frontend/src/components/BancasWidget.css
Normal file
@@ -0,0 +1,35 @@
|
||||
/* 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,52 +1,91 @@
|
||||
// src/components/BancasWidget.tsx
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getBancasPorSeccion, type ProyeccionBancas } from '../services/api';
|
||||
import { ResponsiveWaffle } from '@nivo/waffle';
|
||||
import { getBancasPorSeccion } from '../apiService';
|
||||
import type { ProyeccionBancas } from '../types/types';
|
||||
import './BancasWidget.css';
|
||||
|
||||
interface Props {
|
||||
seccionId: string;
|
||||
}
|
||||
// Paleta de colores consistente
|
||||
const NIVO_COLORS = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"];
|
||||
|
||||
export const BancasWidget = ({ seccionId }: Props) => {
|
||||
const [data, setData] = useState<ProyeccionBancas | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
// 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)' },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const proyeccion = await getBancasPorSeccion(seccionId);
|
||||
setData(proyeccion);
|
||||
} catch (err) {
|
||||
console.error("Error cargando bancas", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [seccionId]);
|
||||
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);
|
||||
|
||||
if (loading) return <div>Cargando proyección de bancas...</div>;
|
||||
if (!data) return <div>No hay datos de bancas disponibles.</div>;
|
||||
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
|
||||
|
||||
return (
|
||||
<div style={{ fontFamily: 'sans-serif', border: '1px solid #ccc', padding: '16px', borderRadius: '8px' }}>
|
||||
<h3>Proyección de Bancas - {data.seccionNombre}</h3>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign: 'left' }}>Agrupación</th>
|
||||
<th style={{ textAlign: 'right' }}>Bancas Obtenidas</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.proyeccion.map((partido) => (
|
||||
<tr key={partido.agrupacionNombre}>
|
||||
<td>{partido.agrupacionNombre}</td>
|
||||
<td style={{ textAlign: 'right' }}>{partido.bancas}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
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>
|
||||
);
|
||||
};
|
||||
125
Elecciones-Web/frontend/src/components/MapaBsAs.css
Normal file
125
Elecciones-Web/frontend/src/components/MapaBsAs.css
Normal file
@@ -0,0 +1,125 @@
|
||||
/* src/components/MapaBsAs.css */
|
||||
:root {
|
||||
--primary-accent-color: #FF5722;
|
||||
--background-panel-color: #2f2f2f;
|
||||
--border-color: #444;
|
||||
--text-color: #f0f0f0;
|
||||
--text-color-muted: #aaa;
|
||||
--progress-bar-background: #4a4a4a;
|
||||
--scrollbar-thumb-color: #666;
|
||||
--scrollbar-track-color: #333;
|
||||
--map-background-color: #242424; /* Color de fondo del mapa */
|
||||
}
|
||||
|
||||
.mapa-wrapper {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
background-color: var(--map-background-color);
|
||||
padding: 1rem;
|
||||
border-radius: 12px;
|
||||
max-width: 1600px; /* Incrementado para pantallas más grandes */
|
||||
margin: auto;
|
||||
height: 88vh; /* Ligeramente más alto */
|
||||
min-height: 650px;
|
||||
}
|
||||
|
||||
.mapa-container {
|
||||
flex: 0 0 70%;
|
||||
height: 100%;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
/* CORRECCIÓN: Se añade el color de fondo para eliminar el marco blanco */
|
||||
background-color: var(--map-background-color);
|
||||
}
|
||||
|
||||
.mapa-container .rsm-svg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.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;
|
||||
cursor: pointer;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.rsm-geography:hover {
|
||||
filter: drop-shadow(0px 0px 6px rgba(255, 255, 255, 0.6));
|
||||
transform: translateY(-1px);
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
.rsm-geography.selected {
|
||||
fill: var(--primary-accent-color); /* Rellena el partido seleccionado con el color principal */
|
||||
stroke: #ffffff; /* Añade un borde blanco para un mejor contraste */
|
||||
stroke-width: 2px; /* Un grosor de borde definido */
|
||||
filter: none; /* Elimina el efecto de sombra/resplandor */
|
||||
outline: none; /* Previene el recuadro de enfoque del navegador */
|
||||
pointer-events: none; /* Mantenemos esto para evitar interacciones no deseadas */
|
||||
}
|
||||
|
||||
.rsm-geography.faded {
|
||||
opacity: 0.15;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.info-panel {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
background-color: var(--background-panel-color);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.info-panel::-webkit-scrollbar { width: 8px; }
|
||||
.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:hover { background-color: #888; }
|
||||
.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); }
|
||||
|
||||
.reset-button-panel {
|
||||
background: none; border: 1px solid var(--primary-accent-color); color: var(--primary-accent-color); padding: 0.5rem 1rem; border-radius: 5px; cursor: pointer; transition: all 0.2s; margin-bottom: 1rem; align-self: flex-start;
|
||||
}
|
||||
.reset-button-panel:hover { background-color: var(--primary-accent-color); color: white; }
|
||||
|
||||
.detalle-placeholder { text-align: center; margin: auto; }
|
||||
.detalle-loading, .detalle-error { text-align: center; margin: auto; 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; }
|
||||
.resultados-lista { list-style: none; padding: 0; margin: 0; }
|
||||
.resultados-lista li { margin-bottom: 1rem; }
|
||||
.resultado-info { display: flex; justify-content: space-between; margin-bottom: 0.25rem; font-size: 0.9em; }
|
||||
.partido-nombre { font-weight: 500; }
|
||||
.partido-votos { font-weight: 300; color: var(--text-color-muted); }
|
||||
.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; }
|
||||
|
||||
.spinner { width: 40px; height: 40px; border: 4px solid var(--border-color); border-top-color: var(--primary-accent-color); border-radius: 50%; animation: spin 1s linear infinite; margin: 1rem auto; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.map-controls {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
z-index: 10; /* <-- AÑADIDO: Esta línea asegura que los controles estén por encima del mapa. */
|
||||
}
|
||||
.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); }
|
||||
.legend h4 { margin-top: 0; }
|
||||
.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; }
|
||||
264
Elecciones-Web/frontend/src/components/MapaBsAs.tsx
Normal file
264
Elecciones-Web/frontend/src/components/MapaBsAs.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
// src/components/MapaBsAs.tsx
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
import type { Feature, Geometry } from 'geojson';
|
||||
import { geoCentroid } from 'd3-geo';
|
||||
|
||||
import './MapaBsAs.css';
|
||||
|
||||
// --- Interfaces y Tipos ---
|
||||
type PointTuple = [number, number];
|
||||
|
||||
interface ResultadoMapa {
|
||||
ambitoId: number;
|
||||
departamentoNombre: string;
|
||||
agrupacionGanadoraId: string;
|
||||
}
|
||||
|
||||
interface ResultadoDetalladoMunicipio {
|
||||
municipioNombre: string;
|
||||
ultimaActualizacion: string;
|
||||
porcentajeEscrutado: number;
|
||||
porcentajeParticipacion: number;
|
||||
resultados: { nombre: string; votos: number; porcentaje: number }[];
|
||||
votosAdicionales: { enBlanco: number; nulos: number; recurridos: number };
|
||||
}
|
||||
|
||||
interface Agrupacion {
|
||||
id: string;
|
||||
nombre: string;
|
||||
}
|
||||
|
||||
interface PartidoProperties {
|
||||
id: string;
|
||||
departamento: string;
|
||||
cabecera: string;
|
||||
provincia: string;
|
||||
}
|
||||
|
||||
type PartidoGeography = Feature<Geometry, PartidoProperties> & { rsmKey: string };
|
||||
|
||||
// --- Constantes ---
|
||||
const API_BASE_URL = 'http://localhost:5217/api';
|
||||
const COLORES_BASE: string[] = ["#FF5733", "#33FF57", "#3357FF", "#FF33A1", "#A133FF", "#33FFA1", "#FFC300", "#C70039", "#900C3F", "#581845"];
|
||||
const MIN_ZOOM = 1;
|
||||
const MAX_ZOOM = 8;
|
||||
// Define los límites del paneo: [[x0, y0], [x1, y1]].
|
||||
// Esto evita que el mapa se "pierda" fuera de la vista.
|
||||
// Estos valores pueden necesitar ajuste fino según el tamaño final del contenedor del mapa.
|
||||
const TRANSLATE_EXTENT: [[number, number], [number, number]] = [[-100, -600], [1100, 300]];
|
||||
const INITIAL_POSITION = { center: [-60.5, -37.2] as PointTuple, zoom: MIN_ZOOM };
|
||||
|
||||
// --- Componente Principal ---
|
||||
const MapaBsAs = () => {
|
||||
const [position, setPosition] = useState(INITIAL_POSITION);
|
||||
const [selectedAmbitoId, setSelectedAmbitoId] = useState<number | null>(null);
|
||||
|
||||
const { data: resultadosData, isLoading: isLoadingResultados } = useQuery<ResultadoMapa[]>({
|
||||
queryKey: ['mapaResultados'],
|
||||
queryFn: async () => (await axios.get(`${API_BASE_URL}/Resultados/mapa`)).data,
|
||||
});
|
||||
const { data: geoData, isLoading: isLoadingGeo } = useQuery<any>({
|
||||
queryKey: ['mapaGeoData'],
|
||||
queryFn: async () => (await axios.get('/partidos-bsas.topojson')).data,
|
||||
});
|
||||
const { data: agrupacionesData, isLoading: isLoadingAgrupaciones } = useQuery<Agrupacion[]>({
|
||||
queryKey: ['catalogoAgrupaciones'],
|
||||
queryFn: async () => (await axios.get(`${API_BASE_URL}/Catalogos/agrupaciones`)).data,
|
||||
});
|
||||
|
||||
const { nombresAgrupaciones, coloresPartidos, resultadosPorDepartamento } = useMemo(() => {
|
||||
const nombresMap = new Map<string, string>();
|
||||
const coloresMap = new Map<string, string>();
|
||||
const resultadosMap = new Map<string, ResultadoMapa>();
|
||||
if (agrupacionesData) {
|
||||
agrupacionesData.forEach((agrupacion, index) => {
|
||||
nombresMap.set(agrupacion.id, agrupacion.nombre);
|
||||
coloresMap.set(agrupacion.id, COLORES_BASE[index % COLORES_BASE.length]);
|
||||
});
|
||||
}
|
||||
coloresMap.set('default', '#D6D6DA');
|
||||
if (resultadosData) {
|
||||
resultadosData.forEach(r => resultadosMap.set(r.departamentoNombre.toUpperCase(), r));
|
||||
}
|
||||
return { nombresAgrupaciones: nombresMap, coloresPartidos: coloresMap, resultadosPorDepartamento: resultadosMap };
|
||||
}, [agrupacionesData, resultadosData]);
|
||||
|
||||
const isLoading = isLoadingResultados || isLoadingAgrupaciones || isLoadingGeo;
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
setSelectedAmbitoId(null);
|
||||
setPosition(INITIAL_POSITION);
|
||||
}, []);
|
||||
|
||||
const handleGeographyClick = useCallback((geo: PartidoGeography) => {
|
||||
const departamentoNombre = geo.properties.departamento.toUpperCase();
|
||||
const resultado = resultadosPorDepartamento.get(departamentoNombre);
|
||||
if (!resultado) return;
|
||||
const ambitoIdParaSeleccionar = resultado.ambitoId;
|
||||
if (selectedAmbitoId === ambitoIdParaSeleccionar) {
|
||||
handleReset();
|
||||
} else {
|
||||
const centroid = geoCentroid(geo) as PointTuple;
|
||||
setPosition({ center: centroid, zoom: 5 });
|
||||
setSelectedAmbitoId(ambitoIdParaSeleccionar);
|
||||
}
|
||||
}, [selectedAmbitoId, handleReset, resultadosPorDepartamento]);
|
||||
|
||||
const handleMoveEnd = (newPosition: { coordinates: PointTuple; zoom: number }) => {
|
||||
// La lógica de reseteo cuando se hace zoom out completamente con la rueda del ratón se mantiene.
|
||||
// El `translateExtent` ya previene que el mapa se mueva fuera de los límites.
|
||||
if (newPosition.zoom <= MIN_ZOOM) {
|
||||
if (position.zoom > MIN_ZOOM || selectedAmbitoId !== null) {
|
||||
handleReset();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Si se está haciendo zoom out desde una vista detallada, se deselecciona el municipio
|
||||
// para volver a la vista general sin resetear completamente la posición.
|
||||
if (newPosition.zoom < position.zoom && selectedAmbitoId !== null) {
|
||||
setSelectedAmbitoId(null);
|
||||
}
|
||||
|
||||
// Actualiza el estado con la nueva posición y zoom del paneo/zoom del usuario.
|
||||
setPosition({ center: newPosition.coordinates, zoom: newPosition.zoom });
|
||||
};
|
||||
|
||||
const handleZoomIn = () => {
|
||||
if (position.zoom < MAX_ZOOM) {
|
||||
setPosition(p => ({ ...p, zoom: Math.min(p.zoom * 1.5, MAX_ZOOM) }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleZoomOut = () => {
|
||||
// Al presionar el botón de zoom out, siempre se vuelve al estado inicial.
|
||||
handleReset();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => e.key === 'Escape' && handleReset();
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [handleReset]);
|
||||
|
||||
const getPartyFillColor = (departamentoNombre: string) => {
|
||||
const resultado = resultadosPorDepartamento.get(departamentoNombre.toUpperCase());
|
||||
if (!resultado) return coloresPartidos.get('default') || '#D6D6DA';
|
||||
return coloresPartidos.get(resultado.agrupacionGanadoraId) || coloresPartidos.get('default');
|
||||
};
|
||||
|
||||
const handleMouseEnter = (e: MouseEvent<SVGPathElement>) => {
|
||||
const path = e.target as SVGPathElement;
|
||||
if (path.parentNode) {
|
||||
path.parentNode.appendChild(path);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) return <div className="loading-container">Cargando datos del mapa...</div>;
|
||||
|
||||
return (
|
||||
<div className="mapa-wrapper">
|
||||
<div className="mapa-container">
|
||||
<ComposableMap projection="geoMercator" projectionConfig={{ scale: 4700, center: [-60.5, -37.2] }} className="rsm-svg" style={{ backgroundColor: "#242424" }}>
|
||||
<ZoomableGroup
|
||||
center={position.center}
|
||||
zoom={position.zoom}
|
||||
onMoveEnd={handleMoveEnd}
|
||||
style={{ transition: "transform 400ms ease-in-out" }}
|
||||
translateExtent={TRANSLATE_EXTENT}
|
||||
minZoom={MIN_ZOOM}
|
||||
maxZoom={MAX_ZOOM}
|
||||
filterZoomEvent={(e: WheelEvent) => {
|
||||
// Detectamos si la rueda se mueve hacia atrás (zoom out)
|
||||
if (e.deltaY > 0) {
|
||||
handleReset();
|
||||
}else if (e.deltaY < 0) {
|
||||
handleZoomIn();
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
>
|
||||
{geoData && (
|
||||
<Geographies geography={geoData}>
|
||||
{({ geographies }: { geographies: PartidoGeography[] }) =>
|
||||
geographies.map((geo) => {
|
||||
const departamentoNombre = geo.properties.departamento.toUpperCase();
|
||||
const resultado = resultadosPorDepartamento.get(departamentoNombre);
|
||||
const isSelected = resultado ? selectedAmbitoId === resultado.ambitoId : false;
|
||||
const isFaded = selectedAmbitoId !== null && !isSelected;
|
||||
const nombreAgrupacionGanadora = resultado ? nombresAgrupaciones.get(resultado.agrupacionGanadoraId) : 'Sin datos';
|
||||
|
||||
return (
|
||||
<Geography
|
||||
key={geo.rsmKey}
|
||||
geography={geo}
|
||||
data-tooltip-id="partido-tooltip"
|
||||
data-tooltip-content={`${geo.properties.departamento}: ${nombreAgrupacionGanadora}`}
|
||||
className={`rsm-geography ${isSelected ? 'selected' : ''} ${isFaded ? 'faded' : ''}`}
|
||||
fill={getPartyFillColor(geo.properties.departamento)}
|
||||
stroke="#FFF"
|
||||
onClick={() => handleGeographyClick(geo)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Geographies>
|
||||
)}
|
||||
</ZoomableGroup>
|
||||
</ComposableMap>
|
||||
<Tooltip id="partido-tooltip" />
|
||||
<ControlesMapa onZoomIn={handleZoomIn} onZoomOut={handleZoomOut} onReset={handleReset} />
|
||||
</div>
|
||||
<div className="info-panel">
|
||||
<DetalleMunicipio ambitoId={selectedAmbitoId} onReset={handleReset} />
|
||||
<Legend colores={coloresPartidos} nombres={nombresAgrupaciones} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// --- Sub-componentes (sin cambios) ---
|
||||
const ControlesMapa = ({ onZoomIn, onZoomOut, onReset }: { onZoomIn: () => void; onZoomOut: () => void; onReset: () => void }) => (
|
||||
<div className="map-controls">
|
||||
<button onClick={onZoomIn}>+</button>
|
||||
<button onClick={onZoomOut}>-</button>
|
||||
<button onClick={onReset}>⌖</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const DetalleMunicipio = ({ ambitoId, onReset }: { ambitoId: number | null; onReset: () => void }) => {
|
||||
const { data, isLoading, error } = useQuery<ResultadoDetalladoMunicipio>({
|
||||
queryKey: ['municipioDetalle', ambitoId],
|
||||
queryFn: async () => (await axios.get(`${API_BASE_URL}/Resultados/municipio/${ambitoId}`)).data,
|
||||
enabled: !!ambitoId,
|
||||
});
|
||||
|
||||
if (!ambitoId) return (<div className="detalle-placeholder"><h3>Provincia de Buenos Aires</h3><p>Seleccione un municipio en el mapa para ver los resultados detallados.</p></div>);
|
||||
if (isLoading) return (<div className="detalle-loading"><div className="spinner"></div><p>Cargando resultados...</p></div>);
|
||||
if (error) return <div className="detalle-error">Error al cargar los datos del municipio.</div>;
|
||||
|
||||
return (
|
||||
<div className="detalle-content">
|
||||
<button className="reset-button-panel" onClick={onReset}>← Ver Provincia</button>
|
||||
<h3>{data?.municipioNombre}</h3>
|
||||
<div className="detalle-metricas">
|
||||
<span><strong>Escrutado:</strong> {data?.porcentajeEscrutado.toFixed(2)}%</span>
|
||||
<span><strong>Participación:</strong> {data?.porcentajeParticipacion.toFixed(2)}%</span>
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Legend = ({ colores, nombres }: { colores: Map<string, string>; nombres: Map<string, string> }) => {
|
||||
const legendItems = Array.from(colores.entries()).filter(([id]) => id !== 'default').map(([id, color]) => ({ nombre: nombres.get(id) || 'Desconocido', color: color }));
|
||||
return (<div className="legend"><h4>Leyenda de Ganadores</h4>{legendItems.map(item => (<div key={item.nombre} className="legend-item"><div className="legend-color-box" style={{ backgroundColor: item.color }} /><span>{item.nombre}</span></div>))}</div>);
|
||||
};
|
||||
|
||||
export default MapaBsAs;
|
||||
@@ -1,112 +0,0 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import * as d3 from 'd3';
|
||||
// FIX: Usamos 'import type' para los tipos y quitamos la importación de 'MunicipioSimple'
|
||||
import type { FeatureCollection } from 'geojson';
|
||||
|
||||
// --- Interfaces y Constantes ---
|
||||
interface MapaResultado {
|
||||
municipioId: string;
|
||||
agrupacionGanadoraId: string;
|
||||
}
|
||||
const COLOR_MAP: { [key: string]: string } = { "018": "#FFC107", "025": "#03A9F4", "031": "#4CAF50", "045": "#9C27B0", "default": "#E0E0E0" };
|
||||
|
||||
interface Props {
|
||||
onMunicipioClick: (municipioId: string) => void;
|
||||
}
|
||||
|
||||
export const MapaD3Widget = ({ onMunicipioClick }: Props) => {
|
||||
const svgRef = useRef<SVGSVGElement | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [tooltip, setTooltip] = useState<{ x: number; y: number; content: string } | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const svgElement = svgRef.current;
|
||||
const containerElement = containerRef.current;
|
||||
if (!svgElement || !containerElement) return;
|
||||
|
||||
const drawMap = (
|
||||
geoData: FeatureCollection, // Usamos el tipo correcto
|
||||
resultsMap: Map<string, MapaResultado>,
|
||||
idMap: Record<string, string>
|
||||
) => {
|
||||
const { width, height } = containerElement.getBoundingClientRect();
|
||||
if (width === 0 || height === 0) return;
|
||||
|
||||
const svg = d3.select(svgElement);
|
||||
svg.selectAll('*').remove();
|
||||
svg.attr('width', width).attr('height', height).attr('viewBox', `0 0 ${width} ${height}`);
|
||||
|
||||
const projection = d3.geoMercator().fitSize([width, height], geoData);
|
||||
const pathGenerator = d3.geoPath().projection(projection);
|
||||
|
||||
const features = geoData.features;
|
||||
|
||||
svg.append('g')
|
||||
.selectAll('path')
|
||||
.data(features)
|
||||
.join('path')
|
||||
.attr('d', pathGenerator as any)
|
||||
.attr('stroke', '#FFFFFF')
|
||||
.attr('stroke-width', 0.5)
|
||||
.attr('fill', (d: any) => {
|
||||
const geoJsonId = d.properties.cca;
|
||||
const apiId = idMap[geoJsonId];
|
||||
const resultado = resultsMap.get(apiId);
|
||||
return resultado ? COLOR_MAP[resultado.agrupacionGanadoraId] || COLOR_MAP.default : COLOR_MAP.default;
|
||||
})
|
||||
.style('cursor', 'pointer')
|
||||
.on('click', (_, d: any) => {
|
||||
const apiId = idMap[d.properties.cca];
|
||||
if (apiId) onMunicipioClick(apiId);
|
||||
})
|
||||
.on('mouseover', (event, d: any) => {
|
||||
d3.select(event.currentTarget).attr('stroke', 'black').attr('stroke-width', 2);
|
||||
setTooltip({ x: event.pageX, y: event.pageY, content: d.properties.nam });
|
||||
})
|
||||
.on('mouseout', (event) => {
|
||||
d3.select(event.currentTarget).attr('stroke', '#FFFFFF').attr('stroke-width', 0.5);
|
||||
setTooltip(null);
|
||||
});
|
||||
};
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const [geoData, resultsData, idMap] = await Promise.all([
|
||||
d3.json<FeatureCollection>('/buenos-aires-municipios.geojson'),
|
||||
d3.json<MapaResultado[]>('http://localhost:5217/api/resultados/mapa'),
|
||||
d3.json<Record<string, string>>('/municipioIdMap.json')
|
||||
]);
|
||||
if (geoData && resultsData && idMap) {
|
||||
const resultsMap = new Map(resultsData.map(item => [item.municipioId, item]));
|
||||
drawMap(geoData, resultsMap, idMap);
|
||||
} else {
|
||||
throw new Error("Faltan datos para renderizar el mapa.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error cargando datos para el mapa:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [onMunicipioClick]);
|
||||
|
||||
if (loading) {
|
||||
return <div ref={containerRef} style={{ width: '100%', height: '600px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>Cargando datos del mapa...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef} style={{ width: '100%', height: '600px', border: '1px solid #eee' }}>
|
||||
<svg ref={svgRef}></svg>
|
||||
{tooltip && (
|
||||
<div style={{
|
||||
position: 'fixed', top: tooltip.y + 10, left: tooltip.x + 10,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.75)', color: 'white', padding: '8px',
|
||||
borderRadius: '4px', pointerEvents: 'none', fontSize: '14px', zIndex: 1000,
|
||||
}}>
|
||||
{tooltip.content}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import { type MunicipioSimple } from '../services/api';
|
||||
|
||||
interface Props {
|
||||
municipios: MunicipioSimple[];
|
||||
onMunicipioChange: (municipioId: string) => void;
|
||||
}
|
||||
|
||||
export const MunicipioSelector = ({ municipios, onMunicipioChange }: Props) => {
|
||||
return (
|
||||
<select onChange={(e) => onMunicipioChange(e.target.value)} defaultValue="">
|
||||
<option value="" disabled>Seleccione un municipio</option>
|
||||
{municipios.map(m => (
|
||||
<option key={m.id} value={m.id}>{m.nombre}</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
// src/components/MunicipioWidget.tsx
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getResultadosPorMunicipio, type MunicipioResultados } from '../services/api';
|
||||
|
||||
interface Props {
|
||||
municipioId: string;
|
||||
}
|
||||
|
||||
export const MunicipioWidget = ({ municipioId }: Props) => {
|
||||
const [data, setData] = useState<MunicipioResultados | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const resultados = await getResultadosPorMunicipio(municipioId);
|
||||
setData(resultados);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError('No se pudieron cargar los datos.');
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Hacemos la primera llamada inmediatamente
|
||||
fetchData();
|
||||
|
||||
// Creamos un intervalo para refrescar los datos cada 10 segundos
|
||||
const intervalId = setInterval(fetchData, 10000);
|
||||
|
||||
// ¡Importante! Limpiamos el intervalo cuando el componente se desmonta
|
||||
return () => clearInterval(intervalId);
|
||||
}, [municipioId]); // El efecto se volverá a ejecutar si el municipioId cambia
|
||||
|
||||
if (loading && !data) return <div>Cargando resultados...</div>;
|
||||
if (error) return <div style={{ color: 'red' }}>{error}</div>;
|
||||
if (!data) return <div>No hay datos disponibles.</div>;
|
||||
|
||||
return (
|
||||
<div style={{ fontFamily: 'sans-serif', border: '1px solid #ccc', padding: '16px', borderRadius: '8px' }}>
|
||||
<h2>{data.municipioNombre}</h2>
|
||||
<p>Escrutado: {data.porcentajeEscrutado.toFixed(2)}% | Participación: {data.porcentajeParticipacion.toFixed(2)}%</p>
|
||||
<hr />
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign: 'left' }}>Agrupación</th>
|
||||
<th style={{ textAlign: 'right' }}>Votos</th>
|
||||
<th style={{ textAlign: 'right' }}>%</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.resultados.map((partido) => (
|
||||
<tr key={partido.nombre}>
|
||||
<td>{partido.nombre}</td>
|
||||
<td style={{ textAlign: 'right' }}>{partido.votos.toLocaleString('es-AR')}</td>
|
||||
<td style={{ textAlign: 'right' }}>{partido.porcentaje.toFixed(2)}%</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<p style={{fontSize: '0.8em', color: '#666'}}>Última actualización: {new Date(data.ultimaActualizacion).toLocaleTimeString('es-AR')}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getResumenProvincial, type ResumenProvincial } from '../services/api';
|
||||
|
||||
interface Props {
|
||||
distritoId: string;
|
||||
}
|
||||
|
||||
export const ResumenProvincialWidget = ({ distritoId }: Props) => {
|
||||
const [data, setData] = useState<ResumenProvincial | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const resumen = await getResumenProvincial(distritoId);
|
||||
setData(resumen);
|
||||
} catch (err) {
|
||||
console.error("Error cargando resumen provincial", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
const intervalId = setInterval(fetchData, 15000); // Actualizamos cada 15s
|
||||
return () => clearInterval(intervalId);
|
||||
}, [distritoId]);
|
||||
|
||||
if (loading) return <div>Cargando resumen provincial...</div>;
|
||||
if (!data) return <div>No hay datos provinciales disponibles.</div>;
|
||||
|
||||
return (
|
||||
<div style={{ fontFamily: 'sans-serif', border: '1px solid #ccc', padding: '16px', borderRadius: '8px', backgroundColor: '#f9f9f9' }}>
|
||||
<h2>Resumen Provincial - {data.provinciaNombre}</h2>
|
||||
<p><strong>Mesas Escrutadas:</strong> {data.porcentajeEscrutado.toFixed(2)}% | <strong>Participación:</strong> {data.porcentajeParticipacion.toFixed(2)}%</p>
|
||||
|
||||
{data.resultados.map((partido) => (
|
||||
<div key={partido.nombre} style={{ margin: '10px 0' }}>
|
||||
<span>{partido.nombre}</span>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{ backgroundColor: '#ddd', width: '100%', borderRadius: '4px', marginRight: '10px' }}>
|
||||
<div style={{ width: `${partido.porcentaje}%`, backgroundColor: 'royalblue', color: 'white', padding: '4px', borderRadius: '4px', textAlign: 'right' }}>
|
||||
<strong>{partido.porcentaje.toFixed(2)}%</strong>
|
||||
</div>
|
||||
</div>
|
||||
<span>{partido.votos.toLocaleString('es-AR')}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,65 +0,0 @@
|
||||
// src/components/TelegramasView.tsx
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getListaTelegramas, getTelegramaPorId, type TelegramaDetalle } from '../services/api';
|
||||
|
||||
export const TelegramasView = () => {
|
||||
const [listaIds, setListaIds] = useState<string[]>([]);
|
||||
const [selectedTelegrama, setSelectedTelegrama] = useState<TelegramaDetalle | null>(null);
|
||||
const [loadingList, setLoadingList] = useState(true);
|
||||
const [loadingDetail, setLoadingDetail] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadList = async () => {
|
||||
try {
|
||||
const ids = await getListaTelegramas();
|
||||
setListaIds(ids);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar lista de telegramas", error);
|
||||
} finally {
|
||||
setLoadingList(false);
|
||||
}
|
||||
};
|
||||
loadList();
|
||||
}, []);
|
||||
|
||||
const handleSelectTelegrama = async (mesaId: string) => {
|
||||
try {
|
||||
setLoadingDetail(true);
|
||||
const detalle = await getTelegramaPorId(mesaId);
|
||||
setSelectedTelegrama(detalle);
|
||||
} catch (error) {
|
||||
console.error(`Error al cargar telegrama ${mesaId}`, error);
|
||||
} finally {
|
||||
setLoadingDetail(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: '20px', height: '500px' }}>
|
||||
<div style={{ flex: 1, border: '1px solid #ccc', overflowY: 'auto' }}>
|
||||
<h4>Telegramas Disponibles</h4>
|
||||
{loadingList ? <p>Cargando...</p> : (
|
||||
<ul style={{ listStyle: 'none', padding: '10px' }}>
|
||||
{listaIds.map(id => (
|
||||
<li key={id} onClick={() => handleSelectTelegrama(id)} style={{ cursor: 'pointer', padding: '5px', borderBottom: '1px solid #eee' }}>
|
||||
Mesa: {id}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ flex: 3, border: '1px solid #ccc', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
{loadingDetail ? <p>Cargando telegrama...</p> :
|
||||
selectedTelegrama ? (
|
||||
<iframe
|
||||
src={`data:application/pdf;base64,${selectedTelegrama.contenidoBase64}`}
|
||||
width="100%"
|
||||
height="100%"
|
||||
title={selectedTelegrama.id}
|
||||
/>
|
||||
) : <p>Seleccione un telegrama de la lista para visualizarlo.</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
82
Elecciones-Web/frontend/src/components/TickerWidget.css
Normal file
82
Elecciones-Web/frontend/src/components/TickerWidget.css
Normal file
@@ -0,0 +1,82 @@
|
||||
/* 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;
|
||||
}
|
||||
67
Elecciones-Web/frontend/src/components/TickerWidget.tsx
Normal file
67
Elecciones-Web/frontend/src/components/TickerWidget.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
// src/components/TickerWidget.tsx
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getResumenProvincial } from '../apiService';
|
||||
import type { ResumenProvincial } from '../types/types';
|
||||
import './TickerWidget.css';
|
||||
|
||||
const formatPercent = (num: number) => `${num.toFixed(2).replace('.', ',')}%`;
|
||||
const 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>TOTAL PROVINCIA {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">
|
||||
<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:COLORS[index % COLORS.length] }}></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +1,13 @@
|
||||
/* src/index.css */
|
||||
: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;
|
||||
/* Tema Claro por defecto */
|
||||
color-scheme: light;
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
@@ -42,7 +44,8 @@ button {
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
/* Color de fondo para botones en tema claro */
|
||||
background-color: #f9f9f9;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
@@ -53,16 +56,3 @@ 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,10 +1,17 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
// src/main.tsx
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import App from './App.tsx'
|
||||
import './index.css'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
// Crear un cliente de React Query
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
@@ -1,89 +0,0 @@
|
||||
// src/services/api.ts
|
||||
import axios from 'axios';
|
||||
|
||||
// Creamos una instancia de Axios.
|
||||
// OJO: Usamos el puerto del PROXY (8600) que configuramos en docker-compose.yml
|
||||
// No usamos el puerto de la API de .NET directamente.
|
||||
const apiClient = axios.create({
|
||||
baseURL: 'http://localhost:5217/api'
|
||||
});
|
||||
|
||||
// Definimos las interfaces de TypeScript que coinciden con los DTOs de nuestra API.
|
||||
export interface AgrupacionResultado {
|
||||
nombre: string;
|
||||
votos: number;
|
||||
porcentaje: number;
|
||||
}
|
||||
|
||||
export interface VotosAdicionales {
|
||||
enBlanco: number;
|
||||
nulos: number;
|
||||
recurridos: number;
|
||||
}
|
||||
|
||||
export interface MunicipioResultados {
|
||||
municipioNombre: string;
|
||||
ultimaActualizacion: string; // La fecha viene como string
|
||||
porcentajeEscrutado: number;
|
||||
porcentajeParticipacion: number;
|
||||
resultados: AgrupacionResultado[];
|
||||
votosAdicionales: VotosAdicionales;
|
||||
}
|
||||
|
||||
export interface MunicipioSimple {
|
||||
id: string;
|
||||
nombre: string;
|
||||
}
|
||||
|
||||
export interface ResumenProvincial extends Omit<MunicipioResultados, 'municipioNombre'> {
|
||||
provinciaNombre: string;
|
||||
}
|
||||
|
||||
export interface BancaResultado {
|
||||
agrupacionNombre: string;
|
||||
bancas: number;
|
||||
}
|
||||
|
||||
export interface ProyeccionBancas {
|
||||
seccionNombre: string;
|
||||
proyeccion: BancaResultado[];
|
||||
}
|
||||
|
||||
export interface TelegramaDetalle {
|
||||
id: string;
|
||||
ambitoGeograficoId: number;
|
||||
contenidoBase64: string;
|
||||
fechaEscaneo: string;
|
||||
fechaTotalizacion: string;
|
||||
}
|
||||
|
||||
export const getBancasPorSeccion = async (seccionId: string): Promise<ProyeccionBancas> => {
|
||||
const response = await apiClient.get<ProyeccionBancas>(`/resultados/bancas/${seccionId}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getListaTelegramas = async (): Promise<string[]> => {
|
||||
const response = await apiClient.get<string[]>('/telegramas');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getTelegramaPorId = async (mesaId: string): Promise<TelegramaDetalle> => {
|
||||
const response = await apiClient.get<TelegramaDetalle>(`/telegramas/${mesaId}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getMunicipios = async (): Promise<MunicipioSimple[]> => {
|
||||
const response = await apiClient.get<MunicipioSimple[]>('/catalogos/municipios');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getResumenProvincial = async (distritoId: string): Promise<ResumenProvincial> => {
|
||||
const response = await apiClient.get<ResumenProvincial>(`/resultados/provincia/${distritoId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Función para obtener los resultados de un municipio
|
||||
export const getResultadosPorMunicipio = async (municipioId: string): Promise<MunicipioResultados> => {
|
||||
const response = await apiClient.get<MunicipioResultados>(`/resultados/municipio/${municipioId}`);
|
||||
return response.data;
|
||||
};
|
||||
11
Elecciones-Web/frontend/src/types/custom.d.ts
vendored
Normal file
11
Elecciones-Web/frontend/src/types/custom.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
// src/types/custom.d.ts
|
||||
|
||||
// Le dice a TypeScript: "Confía en mí, el módulo 'react-simple-maps' existe.
|
||||
// No te preocupes por sus tipos internos, yo me encargo."
|
||||
declare module 'react-simple-maps';
|
||||
|
||||
// Esta declaración ya debería estar aquí.
|
||||
declare module '*.topojson' {
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
62
Elecciones-Web/frontend/src/types/types.ts
Normal file
62
Elecciones-Web/frontend/src/types/types.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// src/types/types.ts
|
||||
|
||||
// Tipos para la respuesta de la API de resultados por municipio
|
||||
export interface AgrupacionResultadoDto {
|
||||
nombre: string;
|
||||
votos: number;
|
||||
porcentaje: number;
|
||||
}
|
||||
|
||||
export interface VotosAdicionalesDto {
|
||||
enBlanco: number;
|
||||
nulos: number;
|
||||
recorridos: number;
|
||||
}
|
||||
|
||||
export interface MunicipioResultadosDto {
|
||||
municipioNombre: string;
|
||||
ultimaActualizacion: string;
|
||||
porcentajeEscrutado: number;
|
||||
porcentajeParticipacion: number;
|
||||
resultados: AgrupacionResultadoDto[];
|
||||
votosAdicionales: VotosAdicionalesDto;
|
||||
}
|
||||
|
||||
// Tipo para la respuesta del endpoint del mapa
|
||||
export interface MapaDto {
|
||||
ambitoId: number;
|
||||
departamentoNombre: string;
|
||||
agrupacionGanadoraId: string;
|
||||
}
|
||||
|
||||
// Definición de tipo para los objetos de geografía de react-simple-maps
|
||||
export interface GeographyObject {
|
||||
rsmKey: string;
|
||||
properties: {
|
||||
// CORRECCIÓN: Se cambia 'nombre' por 'NAME_2' para coincidir con el archivo topojson
|
||||
NAME_2: string;
|
||||
[key: string]: any; // Permite otras propiedades que puedan venir
|
||||
};
|
||||
}
|
||||
|
||||
export interface MunicipioSimple { id: string; nombre: string; }
|
||||
export interface AgrupacionResultado { nombre: string; votos: number; porcentaje: number; }
|
||||
export interface VotosAdicionales { enBlanco: number; nulos: number; recurridos: number; }
|
||||
export interface MunicipioDetalle {
|
||||
municipioNombre: string;
|
||||
ultimaActualizacion: string;
|
||||
porcentajeEscrutado: number;
|
||||
porcentajeParticipacion: number;
|
||||
resultados: AgrupacionResultado[];
|
||||
votosAdicionales: VotosAdicionales;
|
||||
}
|
||||
export interface ResumenProvincial {
|
||||
provinciaNombre: string;
|
||||
ultimaActualizacion: string;
|
||||
porcentajeEscrutado: number;
|
||||
porcentajeParticipacion: number;
|
||||
resultados: AgrupacionResultado[];
|
||||
votosAdicionales: VotosAdicionales;
|
||||
}
|
||||
export interface Banca { agrupacionNombre: string; bancas: number; }
|
||||
export interface ProyeccionBancas { seccionNombre: string; proyeccion: Banca[]; }
|
||||
@@ -21,13 +21,13 @@ public class CatalogosController : ControllerBase
|
||||
[HttpGet("municipios")]
|
||||
public async Task<IActionResult> GetMunicipios()
|
||||
{
|
||||
// El NivelId 5 corresponde a "Municipio" según los datos que hemos visto.
|
||||
// CORRECCIÓN: Los partidos/municipios corresponden al NivelId 30 (Sección)
|
||||
var municipios = await _dbContext.AmbitosGeograficos
|
||||
.AsNoTracking()
|
||||
.Where(a => a.NivelId == 5 && a.MunicipioId != null)
|
||||
.Where(a => a.NivelId == 30 && a.SeccionId != null) // <-- NivelId 30
|
||||
.Select(a => new MunicipioSimpleDto
|
||||
{
|
||||
Id = a.MunicipioId!,
|
||||
Id = a.SeccionId!, // <-- Usamos SeccionId como el ID
|
||||
Nombre = a.Nombre
|
||||
})
|
||||
.OrderBy(m => m.Nombre)
|
||||
@@ -35,4 +35,14 @@ public class CatalogosController : ControllerBase
|
||||
|
||||
return Ok(municipios);
|
||||
}
|
||||
|
||||
[HttpGet("agrupaciones")]
|
||||
public async Task<IActionResult> GetAgrupaciones()
|
||||
{
|
||||
var agrupaciones = await _dbContext.AgrupacionesPoliticas
|
||||
.AsNoTracking()
|
||||
.Select(a => new { a.Id, a.Nombre }) // Devuelve solo lo necesario
|
||||
.ToListAsync();
|
||||
return Ok(agrupaciones);
|
||||
}
|
||||
}
|
||||
@@ -21,17 +21,18 @@ public class ResultadosController : ControllerBase
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet("municipio/{municipioId}")]
|
||||
public async Task<IActionResult> GetResultadosPorMunicipio(string municipioId)
|
||||
[HttpGet("partido/{seccionId}")]
|
||||
public async Task<IActionResult> GetResultadosPorPartido(string seccionId)
|
||||
{
|
||||
// 1. Buscamos el ámbito geográfico correspondiente al municipio
|
||||
// 1. Buscamos el ámbito geográfico correspondiente al PARTIDO (Nivel 30)
|
||||
var ambito = await _dbContext.AmbitosGeograficos
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(a => a.MunicipioId == municipioId);
|
||||
// CAMBIO CLAVE: Buscamos por SeccionId y NivelId para ser precisos
|
||||
.FirstOrDefaultAsync(a => a.SeccionId == seccionId && a.NivelId == 30);
|
||||
|
||||
if (ambito == null)
|
||||
{
|
||||
return NotFound(new { message = $"No se encontró el municipio con ID {municipioId}" });
|
||||
return NotFound(new { message = $"No se encontró el partido con ID {seccionId}" });
|
||||
}
|
||||
|
||||
// 2. Buscamos el estado del recuento para ese ámbito
|
||||
@@ -41,25 +42,34 @@ public class ResultadosController : ControllerBase
|
||||
|
||||
if (estadoRecuento == null)
|
||||
{
|
||||
return NotFound(new { message = $"No se han encontrado resultados para el municipio {ambito.Nombre}" });
|
||||
// Devolvemos una respuesta vacía pero válida para el frontend
|
||||
return Ok(new MunicipioResultadosDto
|
||||
{
|
||||
MunicipioNombre = ambito.Nombre,
|
||||
UltimaActualizacion = DateTime.UtcNow,
|
||||
PorcentajeEscrutado = 0,
|
||||
PorcentajeParticipacion = 0,
|
||||
Resultados = new List<AgrupacionResultadoDto>(),
|
||||
VotosAdicionales = new VotosAdicionalesDto()
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Buscamos todos los votos para ese ámbito, incluyendo el nombre de la agrupación
|
||||
// 3. Buscamos todos los votos para ese ámbito
|
||||
var resultadosVotos = await _dbContext.ResultadosVotos
|
||||
.AsNoTracking()
|
||||
.Include(rv => rv.AgrupacionPolitica) // ¡Crucial para obtener el nombre del partido!
|
||||
.Include(rv => rv.AgrupacionPolitica)
|
||||
.Where(rv => rv.AmbitoGeograficoId == ambito.Id)
|
||||
.ToListAsync();
|
||||
|
||||
// 4. Calculamos el total de votos positivos para el porcentaje
|
||||
// 4. Calculamos el total de votos positivos
|
||||
long totalVotosPositivos = resultadosVotos.Sum(r => r.CantidadVotos);
|
||||
|
||||
// 5. Mapeamos todo a nuestro DTO de respuesta
|
||||
// 5. Mapeamos al DTO de respuesta
|
||||
var respuestaDto = new MunicipioResultadosDto
|
||||
{
|
||||
MunicipioNombre = ambito.Nombre,
|
||||
UltimaActualizacion = estadoRecuento.FechaTotalizacion,
|
||||
PorcentajeEscrutado = estadoRecuento.MesasTotalizadas * 100.0m / (estadoRecuento.MesasEsperadas > 0 ? estadoRecuento.MesasEsperadas : 1),
|
||||
PorcentajeEscrutado = estadoRecuento.MesasTotalizadasPorcentaje,
|
||||
PorcentajeParticipacion = estadoRecuento.ParticipacionPorcentaje,
|
||||
Resultados = resultadosVotos.Select(rv => new AgrupacionResultadoDto
|
||||
{
|
||||
@@ -75,7 +85,6 @@ public class ResultadosController : ControllerBase
|
||||
}
|
||||
};
|
||||
|
||||
// Devolvemos el resultado
|
||||
return Ok(respuestaDto);
|
||||
}
|
||||
|
||||
@@ -157,8 +166,6 @@ public class ResultadosController : ControllerBase
|
||||
[HttpGet("mapa")]
|
||||
public async Task<IActionResult> GetResultadosParaMapa()
|
||||
{
|
||||
// Esta consulta es mucho más eficiente y se traduce bien a SQL.
|
||||
// Paso 1: Para cada ámbito, encontrar la cantidad máxima de votos.
|
||||
var maxVotosPorAmbito = _dbContext.ResultadosVotos
|
||||
.GroupBy(rv => rv.AmbitoGeograficoId)
|
||||
.Select(g => new
|
||||
@@ -167,24 +174,93 @@ public class ResultadosController : ControllerBase
|
||||
MaxVotos = g.Max(v => v.CantidadVotos)
|
||||
});
|
||||
|
||||
// Paso 2: Unir los resultados originales con los máximos para encontrar el registro ganador.
|
||||
// Esto nos da, para cada ámbito, el registro completo del partido que tuvo más votos.
|
||||
var resultadosGanadores = await _dbContext.ResultadosVotos
|
||||
.Join(
|
||||
maxVotosPorAmbito,
|
||||
voto => new { AmbitoId = voto.AmbitoGeograficoId, Votos = voto.CantidadVotos },
|
||||
max => new { AmbitoId = max.AmbitoId, Votos = max.MaxVotos },
|
||||
(voto, max) => voto // Nos quedamos con el objeto 'ResultadoVoto' completo
|
||||
(voto, max) => voto
|
||||
)
|
||||
.Include(rv => rv.AmbitoGeografico) // Incluimos el ámbito para obtener el MunicipioId
|
||||
.Where(rv => rv.AmbitoGeografico.MunicipioId != null)
|
||||
.Include(rv => rv.AmbitoGeografico)
|
||||
.Where(rv => rv.AmbitoGeografico.NivelId == 30) // Aseguramos que solo sean los ámbitos de nivel 30
|
||||
.Select(rv => new
|
||||
{
|
||||
MunicipioId = rv.AmbitoGeografico.MunicipioId,
|
||||
// CORRECCIÓN CLAVE: Devolvemos los campos que el frontend necesita para funcionar.
|
||||
|
||||
// 1. El ID de la BD para hacer clic y pedir detalles.
|
||||
AmbitoId = rv.AmbitoGeografico.Id,
|
||||
|
||||
// 2. El NOMBRE del departamento/municipio para encontrar y colorear el polígono.
|
||||
DepartamentoNombre = rv.AmbitoGeografico.Nombre,
|
||||
|
||||
// 3. El ID del partido ganador.
|
||||
AgrupacionGanadoraId = rv.AgrupacionPoliticaId
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(resultadosGanadores);
|
||||
}
|
||||
|
||||
[HttpGet("municipio/{ambitoId}")] // Cambiamos el nombre del parámetro de ruta
|
||||
public async Task<IActionResult> GetResultadosPorMunicipio(int ambitoId) // Cambiamos el tipo de string a int
|
||||
{
|
||||
_logger.LogInformation("Buscando resultados para AmbitoGeograficoId: {AmbitoId}", ambitoId);
|
||||
|
||||
// PASO 1: Buscar el Ámbito Geográfico directamente por su CLAVE PRIMARIA (AmbitoGeograficoId).
|
||||
var ambito = await _dbContext.AmbitosGeograficos
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(a => a.Id == ambitoId && a.NivelId == 30); // Usamos a.Id == ambitoId
|
||||
|
||||
if (ambito == null)
|
||||
{
|
||||
_logger.LogWarning("No se encontró el ámbito para el ID interno: {AmbitoId} o no es Nivel 30.", ambitoId);
|
||||
return NotFound(new { message = $"No se encontró el municipio con ID interno {ambitoId}" });
|
||||
}
|
||||
_logger.LogInformation("Ámbito encontrado: Id={AmbitoId}, Nombre={AmbitoNombre}", ambito.Id, ambito.Nombre);
|
||||
|
||||
// PASO 2: Usar la CLAVE PRIMARIA (ambito.Id) para buscar el estado del recuento.
|
||||
var estadoRecuento = await _dbContext.EstadosRecuentos
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(e => e.AmbitoGeograficoId == ambito.Id);
|
||||
|
||||
if (estadoRecuento == null)
|
||||
{
|
||||
_logger.LogWarning("No se encontró EstadoRecuento para AmbitoGeograficoId: {AmbitoId}", ambito.Id);
|
||||
return NotFound(new { message = $"No se han encontrado resultados de recuento para el municipio {ambito.Nombre}" });
|
||||
}
|
||||
|
||||
// PASO 3: Usar la CLAVE PRIMARIA (ambito.Id) para buscar los votos.
|
||||
var resultadosVotos = await _dbContext.ResultadosVotos
|
||||
.AsNoTracking()
|
||||
.Include(rv => rv.AgrupacionPolitica) // Incluimos el nombre del partido
|
||||
.Where(rv => rv.AmbitoGeograficoId == ambito.Id)
|
||||
.OrderByDescending(rv => rv.CantidadVotos)
|
||||
.ToListAsync();
|
||||
|
||||
// PASO 4: Calcular el total de votos positivos para el porcentaje.
|
||||
long totalVotosPositivos = resultadosVotos.Sum(r => r.CantidadVotos);
|
||||
|
||||
// PASO 5: Mapear todo al DTO de respuesta que el frontend espera.
|
||||
var respuestaDto = new MunicipioResultadosDto
|
||||
{
|
||||
MunicipioNombre = ambito.Nombre,
|
||||
UltimaActualizacion = estadoRecuento.FechaTotalizacion,
|
||||
PorcentajeEscrutado = estadoRecuento.MesasTotalizadasPorcentaje,
|
||||
PorcentajeParticipacion = estadoRecuento.ParticipacionPorcentaje,
|
||||
Resultados = resultadosVotos.Select(rv => new AgrupacionResultadoDto
|
||||
{
|
||||
Nombre = rv.AgrupacionPolitica.Nombre,
|
||||
Votos = rv.CantidadVotos,
|
||||
Porcentaje = totalVotosPositivos > 0 ? (rv.CantidadVotos * 100.0m / totalVotosPositivos) : 0
|
||||
}).ToList(),
|
||||
VotosAdicionales = new VotosAdicionalesDto
|
||||
{
|
||||
EnBlanco = estadoRecuento.VotosEnBlanco,
|
||||
Nulos = estadoRecuento.VotosNulos,
|
||||
Recurridos = estadoRecuento.VotosRecurridos
|
||||
}
|
||||
};
|
||||
|
||||
return Ok(respuestaDto);
|
||||
}
|
||||
}
|
||||
@@ -7,5 +7,6 @@
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
},
|
||||
"AllowedOrigins": "http://localhost:5173"
|
||||
}
|
||||
|
||||
@@ -1253,6 +1253,14 @@
|
||||
}
|
||||
},
|
||||
"System.Threading.Channels/7.0.0": {},
|
||||
"System.Threading.RateLimiting/9.0.8": {
|
||||
"runtime": {
|
||||
"lib/net9.0/System.Threading.RateLimiting.dll": {
|
||||
"assemblyVersion": "9.0.0.0",
|
||||
"fileVersion": "9.0.825.36511"
|
||||
}
|
||||
}
|
||||
},
|
||||
"System.Threading.Tasks.Extensions/4.5.4": {},
|
||||
"System.Windows.Extensions/6.0.0": {
|
||||
"dependencies": {
|
||||
@@ -1296,7 +1304,8 @@
|
||||
"dependencies": {
|
||||
"Elecciones.Core": "1.0.0",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.8",
|
||||
"Microsoft.Extensions.Http": "9.0.8"
|
||||
"Microsoft.Extensions.Http": "9.0.8",
|
||||
"System.Threading.RateLimiting": "9.0.8"
|
||||
},
|
||||
"runtime": {
|
||||
"Elecciones.Infrastructure.dll": {
|
||||
@@ -2020,6 +2029,13 @@
|
||||
"path": "system.threading.channels/7.0.0",
|
||||
"hashPath": "system.threading.channels.7.0.0.nupkg.sha512"
|
||||
},
|
||||
"System.Threading.RateLimiting/9.0.8": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-Kr2vtbLUyZSGz40YoqE1FrNlXyGj4qOvNmm9upEVxLgT8pr/yEubhDMU5xs70ruhchuWO0LrFi76YWHjYUP/SA==",
|
||||
"path": "system.threading.ratelimiting/9.0.8",
|
||||
"hashPath": "system.threading.ratelimiting.9.0.8.nupkg.sha512"
|
||||
},
|
||||
"System.Threading.Tasks.Extensions/4.5.4": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
|
||||
@@ -7,5 +7,6 @@
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
},
|
||||
"AllowedOrigins": "http://localhost:5173"
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Api")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+68dce9415e165633856e4fae9b2d71cc07b4e2ff")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+18e6e8d3c0a378a172ad8e8afd31109673460717")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Api")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Api")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
@@ -183,3 +183,4 @@ E:\Elecciones-2025\Elecciones-Web\src\Elecciones.Api\bin\Debug\net9.0\Serilog.Se
|
||||
E:\Elecciones-2025\Elecciones-Web\src\Elecciones.Api\bin\Debug\net9.0\Serilog.Sinks.Console.dll
|
||||
E:\Elecciones-2025\Elecciones-Web\src\Elecciones.Api\bin\Debug\net9.0\Serilog.Sinks.Debug.dll
|
||||
E:\Elecciones-2025\Elecciones-Web\src\Elecciones.Api\bin\Debug\net9.0\Serilog.Sinks.File.dll
|
||||
E:\Elecciones-2025\Elecciones-Web\src\Elecciones.Api\bin\Debug\net9.0\System.Threading.RateLimiting.dll
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["mhE0FuBM0BOF9SNOE0rY9setCw2ye3UUh7cEPjhgDdY=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","ywKm3DCyXg4YCbZAIx3JUlT8N4Irff3GswYUVDST\u002BjQ=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","7rMeSoKKF2\u002B9j5kLZ30FlE98meJ1tr4dywVzhYb49Qg="],"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=","42z\u002Bw0pajpMLFLNS29VoU/hUn9IzvZ/pVLNadS0rApY=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","ifGdmI/zx2hsc8PYkk8IWTP8aZ9RYmaQbfk383bAiYQ="],"CachedAssets":{},"CachedCopyCandidates":{}}
|
||||
@@ -1 +1 @@
|
||||
{"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["mhE0FuBM0BOF9SNOE0rY9setCw2ye3UUh7cEPjhgDdY=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","ywKm3DCyXg4YCbZAIx3JUlT8N4Irff3GswYUVDST\u002BjQ=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","7rMeSoKKF2\u002B9j5kLZ30FlE98meJ1tr4dywVzhYb49Qg="],"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=","42z\u002Bw0pajpMLFLNS29VoU/hUn9IzvZ/pVLNadS0rApY=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","ifGdmI/zx2hsc8PYkk8IWTP8aZ9RYmaQbfk383bAiYQ="],"CachedAssets":{},"CachedCopyCandidates":{}}
|
||||
@@ -1 +1 @@
|
||||
{"GlobalPropertiesHash":"O7YawHw32G/Fh2bs+snZgm9O7okI0WYgTQmXM931znY=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["mhE0FuBM0BOF9SNOE0rY9setCw2ye3UUh7cEPjhgDdY=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM="],"CachedAssets":{},"CachedCopyCandidates":{}}
|
||||
{"GlobalPropertiesHash":"O7YawHw32G/Fh2bs+snZgm9O7okI0WYgTQmXM931znY=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["Dji\u002Bta/0e7zUKw3oe\u002BriV3kbWxZ93FP2z2QIYsHXTl4=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM="],"CachedAssets":{},"CachedCopyCandidates":{}}
|
||||
@@ -13,7 +13,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Core")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+30f1e751b770bf730fc48b1baefb00f560694f35")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+18e6e8d3c0a378a172ad8e8afd31109673460717")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Core")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Core")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
@@ -13,7 +13,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Database")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+30f1e751b770bf730fc48b1baefb00f560694f35")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+18e6e8d3c0a378a172ad8e8afd31109673460717")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Database")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Database")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"dependencies": {
|
||||
"Elecciones.Core": "1.0.0",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.8",
|
||||
"Microsoft.Extensions.Http": "9.0.8"
|
||||
"Microsoft.Extensions.Http": "9.0.8",
|
||||
"System.Threading.RateLimiting": "9.0.8"
|
||||
},
|
||||
"runtime": {
|
||||
"Elecciones.Infrastructure.dll": {}
|
||||
@@ -169,6 +170,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"System.Threading.RateLimiting/9.0.8": {
|
||||
"runtime": {
|
||||
"lib/net9.0/System.Threading.RateLimiting.dll": {
|
||||
"assemblyVersion": "9.0.0.0",
|
||||
"fileVersion": "9.0.825.36511"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Elecciones.Core/1.0.0": {
|
||||
"runtime": {
|
||||
"Elecciones.Core.dll": {
|
||||
@@ -276,6 +285,13 @@
|
||||
"path": "microsoft.extensions.primitives/9.0.8",
|
||||
"hashPath": "microsoft.extensions.primitives.9.0.8.nupkg.sha512"
|
||||
},
|
||||
"System.Threading.RateLimiting/9.0.8": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-Kr2vtbLUyZSGz40YoqE1FrNlXyGj4qOvNmm9upEVxLgT8pr/yEubhDMU5xs70ruhchuWO0LrFi76YWHjYUP/SA==",
|
||||
"path": "system.threading.ratelimiting/9.0.8",
|
||||
"hashPath": "system.threading.ratelimiting.9.0.8.nupkg.sha512"
|
||||
},
|
||||
"Elecciones.Core/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -13,7 +13,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Infrastructure")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+19b37f73206d043982fc77f8c2359f2598889b64")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+18e6e8d3c0a378a172ad8e8afd31109673460717")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Infrastructure")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Infrastructure")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
@@ -14,7 +14,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Worker")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+19b37f73206d043982fc77f8c2359f2598889b64")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+18e6e8d3c0a378a172ad8e8afd31109673460717")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Worker")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Worker")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
Reference in New Issue
Block a user