Feat Widgets 0209
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -6,6 +6,7 @@ import MapaBsAs from './components/MapaBsAs'
|
|||||||
import { TickerWidget } from './components/TickerWidget'
|
import { TickerWidget } from './components/TickerWidget'
|
||||||
import { TelegramaWidget } from './components/TelegramaWidget'
|
import { TelegramaWidget } from './components/TelegramaWidget'
|
||||||
import { ConcejalesWidget } from './components/ConcejalesWidget'
|
import { ConcejalesWidget } from './components/ConcejalesWidget'
|
||||||
|
import MapaBsAsSecciones from './components/MapaBsAsSecciones'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@@ -17,6 +18,7 @@ function App() {
|
|||||||
<CongresoWidget />
|
<CongresoWidget />
|
||||||
<BancasWidget />
|
<BancasWidget />
|
||||||
<MapaBsAs />
|
<MapaBsAs />
|
||||||
|
<MapaBsAsSecciones />
|
||||||
<TelegramaWidget />
|
<TelegramaWidget />
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -60,6 +60,13 @@ export interface ConfiguracionPublica {
|
|||||||
// ... otras claves públicas que pueda añadir en el futuro
|
// ... otras claves públicas que pueda añadir en el futuro
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ResultadoDetalleSeccion {
|
||||||
|
id: string; // ID de la agrupación para la key
|
||||||
|
nombre: string;
|
||||||
|
votos: number;
|
||||||
|
porcentaje: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const getResumenProvincial = async (): Promise<CategoriaResumen[]> => {
|
export const getResumenProvincial = async (): Promise<CategoriaResumen[]> => {
|
||||||
const response = await apiClient.get('/resultados/provincia/02');
|
const response = await apiClient.get('/resultados/provincia/02');
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -130,3 +137,8 @@ export const getResultadosConcejales = async (seccionId: string): Promise<Result
|
|||||||
const response = await apiClient.get(`/resultados/concejales/${seccionId}`);
|
const response = await apiClient.get(`/resultados/concejales/${seccionId}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDetalleSeccion = async (seccionId: string, categoriaId: number): Promise<ResultadoDetalleSeccion[]> => {
|
||||||
|
const response = await apiClient.get(`/resultados/seccion/${seccionId}?categoriaId=${categoriaId}`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// src/components/ConcejalesWidget.tsx
|
// src/components/ConcejalesWidget.tsx
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { getSeccionesElectorales, getResultadosConcejales, getConfiguracionPublica } from '../apiService';
|
import { getSeccionesElectorales, getResultadosConcejales, getConfiguracionPublica } from '../apiService';
|
||||||
import type { MunicipioSimple, ResultadoTicker } from '../types/types';
|
import type { MunicipioSimple, ResultadoTicker } from '../types/types';
|
||||||
@@ -20,7 +20,9 @@ export const ConcejalesWidget = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Calculamos la cantidad a mostrar desde la configuración
|
// Calculamos la cantidad a mostrar desde la configuración
|
||||||
const cantidadAMostrar = parseInt(configData?.ConcejalesResultadosCantidad || '5', 10) + 1;
|
const cantidadAMostrar = useMemo(() => {
|
||||||
|
return parseInt(configData?.TickerResultadosCantidad || '5', 10) + 1;
|
||||||
|
}, [configData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getSeccionesElectorales().then(seccionesData => {
|
getSeccionesElectorales().then(seccionesData => {
|
||||||
@@ -53,7 +55,7 @@ export const ConcejalesWidget = () => {
|
|||||||
if (resultados && resultados.length > cantidadAMostrar) {
|
if (resultados && resultados.length > cantidadAMostrar) {
|
||||||
const topParties = resultados.slice(0, cantidadAMostrar - 1);
|
const topParties = resultados.slice(0, cantidadAMostrar - 1);
|
||||||
const otherParties = resultados.slice(cantidadAMostrar - 1);
|
const otherParties = resultados.slice(cantidadAMostrar - 1);
|
||||||
const otrosPorcentaje = otherParties.reduce((sum, party) => sum + (party.votosPorcentaje || 0), 0);
|
const otrosPorcentaje = otherParties.reduce((sum, party) => sum + party.votosPorcentaje, 0);
|
||||||
|
|
||||||
const otrosEntry: ResultadoTicker = {
|
const otrosEntry: ResultadoTicker = {
|
||||||
id: `otros-concejales-${seccionActualId}`,
|
id: `otros-concejales-${seccionActualId}`,
|
||||||
@@ -73,7 +75,7 @@ export const ConcejalesWidget = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="ticker-card" style={{ gridColumn: '1 / -1' }}>
|
<div className="ticker-card" style={{ gridColumn: '1 / -1' }}>
|
||||||
<div className="ticker-header">
|
<div className="ticker-header">
|
||||||
<h3>CONCEJALES - LA PLATA</h3>
|
<h3>CONCEJALES POR SECCIÓN ELECTORAL</h3>
|
||||||
<select value={seccionActualId} onChange={e => setSeccionActualId(e.target.value)} disabled={secciones.length === 0}>
|
<select value={seccionActualId} onChange={e => setSeccionActualId(e.target.value)} disabled={secciones.length === 0}>
|
||||||
{secciones.map(s => <option key={s.id} value={s.id}>{s.nombre}</option>)}
|
{secciones.map(s => <option key={s.id} value={s.id}>{s.nombre}</option>)}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
background-color: var(--background-panel-color);
|
background-color: var(--background-panel-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 1.5rem;
|
padding: 1rem;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,3 +195,51 @@
|
|||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- ESTILOS PARA EL SELECTOR DE CATEGORÍA --- */
|
||||||
|
.mapa-categoria-selector {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapa-categoria-combobox {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
appearance: none;
|
||||||
|
background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%230073e6%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.4-12.9z%22%2F%3E%3C%2Fsvg%3E');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 1rem center;
|
||||||
|
background-size: 0.8em;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapa-categoria-combobox:hover {
|
||||||
|
border-color: var(--primary-accent-color);
|
||||||
|
background-color: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapa-categoria-combobox:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-accent-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 115, 230, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- ESTILOS PARA SECCIONES NO CLICLEABLES --- */
|
||||||
|
.rsm-geography.no-results {
|
||||||
|
pointer-events: none; /* Ignora todos los eventos del ratón (click, hover, etc.) */
|
||||||
|
cursor: default; /* Muestra el cursor por defecto en lugar de la mano */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Opcional pero recomendado: modificar la regla :hover para que no afecte a las secciones no clicleables */
|
||||||
|
.rsm-geography:not(.no-results):hover {
|
||||||
|
stroke: var(--primary-accent-color);
|
||||||
|
stroke-width: 1.5px;
|
||||||
|
filter: brightness(1.05);
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
// src/components/MapaBsAs.tsx
|
// src/components/MapaBsAs.tsx
|
||||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||||
import type { MouseEvent } from 'react';
|
|
||||||
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
|
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
|
||||||
import { Tooltip } from 'react-tooltip';
|
import { Tooltip } from 'react-tooltip';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
@@ -24,7 +23,7 @@ interface ResultadoDetalladoMunicipio {
|
|||||||
ultimaActualizacion: string;
|
ultimaActualizacion: string;
|
||||||
porcentajeEscrutado: number;
|
porcentajeEscrutado: number;
|
||||||
porcentajeParticipacion: number;
|
porcentajeParticipacion: number;
|
||||||
resultados: { nombre: string; votos: number; porcentaje: number }[];
|
resultados: { id: string; nombre: string; votos: number; porcentaje: number }[];
|
||||||
votosAdicionales: { enBlanco: number; nulos: number; recurridos: number };
|
votosAdicionales: { enBlanco: number; nulos: number; recurridos: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,11 +32,13 @@ interface Agrupacion {
|
|||||||
nombre: string;
|
nombre: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Categoria {
|
||||||
|
id: number;
|
||||||
|
nombre: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface PartidoProperties {
|
interface PartidoProperties {
|
||||||
id: string;
|
|
||||||
departamento: string;
|
departamento: string;
|
||||||
cabecera: string;
|
|
||||||
provincia: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PartidoGeography = Feature<Geometry, PartidoProperties> & { rsmKey: string };
|
type PartidoGeography = Feature<Geometry, PartidoProperties> & { rsmKey: string };
|
||||||
@@ -50,52 +51,57 @@ const TRANSLATE_EXTENT: [[number, number], [number, number]] = [[-100, -600], [1
|
|||||||
const INITIAL_POSITION = { center: [-60.5, -37.2] as PointTuple, zoom: MIN_ZOOM };
|
const INITIAL_POSITION = { center: [-60.5, -37.2] as PointTuple, zoom: MIN_ZOOM };
|
||||||
const DEFAULT_MAP_COLOR = '#E0E0E0';
|
const DEFAULT_MAP_COLOR = '#E0E0E0';
|
||||||
|
|
||||||
|
const CATEGORIAS: Categoria[] = [
|
||||||
|
{ id: 5, nombre: 'Senadores' },
|
||||||
|
{ id: 6, nombre: 'Diputados' },
|
||||||
|
{ id: 7, nombre: 'Concejales' }
|
||||||
|
];
|
||||||
|
|
||||||
// --- Componente Principal ---
|
// --- Componente Principal ---
|
||||||
const MapaBsAs = () => {
|
const MapaBsAs = () => {
|
||||||
const [position, setPosition] = useState(INITIAL_POSITION);
|
const [position, setPosition] = useState(INITIAL_POSITION);
|
||||||
const [selectedAmbitoId, setSelectedAmbitoId] = useState<number | null>(null);
|
const [selectedAmbitoId, setSelectedAmbitoId] = useState<number | null>(null);
|
||||||
|
const [selectedCategoriaId, setSelectedCategoriaId] = useState<number>(6);
|
||||||
|
const [tooltipContent, setTooltipContent] = useState('');
|
||||||
|
|
||||||
const { data: resultadosData, isLoading: isLoadingResultados } = useQuery<ResultadoMapa[]>({
|
const { data: resultadosData, isLoading: isLoadingResultados } = useQuery<ResultadoMapa[]>({
|
||||||
queryKey: ['mapaResultados'],
|
queryKey: ['mapaResultadosPorMunicipio', selectedCategoriaId],
|
||||||
queryFn: async () => (await axios.get(`${API_BASE_URL}/Resultados/mapa`)).data,
|
queryFn: async () => (await axios.get(`${API_BASE_URL}/Resultados/mapa-por-municipio?categoriaId=${selectedCategoriaId}`)).data,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: geoData, isLoading: isLoadingGeo } = useQuery<any>({
|
const { data: geoData, isLoading: isLoadingGeo } = useQuery<any>({
|
||||||
queryKey: ['mapaGeoData'],
|
queryKey: ['mapaGeoData'],
|
||||||
queryFn: async () => (await axios.get('/partidos-bsas.topojson')).data,
|
queryFn: async () => (await axios.get('/partidos-bsas.topojson')).data,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: agrupacionesData, isLoading: isLoadingAgrupaciones } = useQuery<Agrupacion[]>({
|
const { data: agrupacionesData, isLoading: isLoadingAgrupaciones } = useQuery<Agrupacion[]>({
|
||||||
queryKey: ['catalogoAgrupaciones'],
|
queryKey: ['catalogoAgrupaciones'],
|
||||||
queryFn: async () => (await axios.get(`${API_BASE_URL}/Catalogos/agrupaciones`)).data,
|
queryFn: async () => (await axios.get(`${API_BASE_URL}/Catalogos/agrupaciones`)).data,
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- SU SOLUCIÓN CORRECTA INTEGRADA ---
|
|
||||||
const { nombresAgrupaciones, resultadosPorDepartamento } = useMemo<{
|
const { nombresAgrupaciones, resultadosPorDepartamento } = useMemo<{
|
||||||
nombresAgrupaciones: Map<string, string>;
|
nombresAgrupaciones: Map<string, string>;
|
||||||
resultadosPorDepartamento: Map<string, ResultadoMapa>;
|
resultadosPorDepartamento: Map<string, ResultadoMapa>;
|
||||||
}>(() => {
|
}>(() => {
|
||||||
const nombresMap = new Map<string, string>();
|
const nombresMap = new Map<string, string>();
|
||||||
const resultadosMap = new Map<string, ResultadoMapa>();
|
const resultadosMap = new Map<string, ResultadoMapa>();
|
||||||
|
|
||||||
if (agrupacionesData) {
|
if (agrupacionesData) {
|
||||||
agrupacionesData.forEach((agrupacion) => {
|
agrupacionesData.forEach((agrupacion) => {
|
||||||
nombresMap.set(agrupacion.id, agrupacion.nombre);
|
nombresMap.set(agrupacion.id, agrupacion.nombre);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resultadosData) {
|
if (resultadosData) {
|
||||||
resultadosData.forEach(r => resultadosMap.set(r.departamentoNombre.toUpperCase(), r));
|
resultadosData.forEach(r => {
|
||||||
|
if (r.departamentoNombre) {
|
||||||
|
resultadosMap.set(r.departamentoNombre.toUpperCase(), r)
|
||||||
}
|
}
|
||||||
|
});
|
||||||
return {
|
}
|
||||||
nombresAgrupaciones: nombresMap,
|
return { nombresAgrupaciones: nombresMap, resultadosPorDepartamento: resultadosMap };
|
||||||
resultadosPorDepartamento: resultadosMap
|
|
||||||
};
|
|
||||||
}, [agrupacionesData, resultadosData]);
|
}, [agrupacionesData, resultadosData]);
|
||||||
|
|
||||||
const isLoading = isLoadingResultados || isLoadingAgrupaciones || isLoadingGeo;
|
const isLoading = isLoadingResultados || isLoadingAgrupaciones || isLoadingGeo;
|
||||||
|
|
||||||
// ... (el resto del componente no necesita cambios)
|
|
||||||
|
|
||||||
const handleReset = useCallback(() => {
|
const handleReset = useCallback(() => {
|
||||||
setSelectedAmbitoId(null);
|
setSelectedAmbitoId(null);
|
||||||
setPosition(INITIAL_POSITION);
|
setPosition(INITIAL_POSITION);
|
||||||
@@ -142,25 +148,44 @@ const MapaBsAs = () => {
|
|||||||
|
|
||||||
const getPartyFillColor = (departamentoNombre: string) => {
|
const getPartyFillColor = (departamentoNombre: string) => {
|
||||||
const resultado = resultadosPorDepartamento.get(departamentoNombre.toUpperCase());
|
const resultado = resultadosPorDepartamento.get(departamentoNombre.toUpperCase());
|
||||||
if (!resultado || !resultado.colorGanador) {
|
return resultado?.colorGanador || DEFAULT_MAP_COLOR;
|
||||||
return DEFAULT_MAP_COLOR;
|
|
||||||
}
|
|
||||||
return resultado.colorGanador;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseEnter = (e: MouseEvent<SVGPathElement>) => {
|
// --- Helper de Renderizado ---
|
||||||
const path = e.target as SVGPathElement;
|
const renderGeography = (geo: PartidoGeography, isSelectedGeo: boolean = false) => {
|
||||||
if (path.parentNode) {
|
const departamentoNombre = geo.properties.departamento.toUpperCase();
|
||||||
path.parentNode.appendChild(path);
|
const resultado = resultadosPorDepartamento.get(departamentoNombre);
|
||||||
}
|
const isClickable = !!resultado;
|
||||||
};
|
const isSelected = isSelectedGeo || (selectedAmbitoId !== null && selectedAmbitoId === resultado?.ambitoId);
|
||||||
|
const isFaded = !isSelectedGeo && selectedAmbitoId !== null && !isSelected;
|
||||||
|
const nombreAgrupacionGanadora = resultado ? nombresAgrupaciones.get(resultado.agrupacionGanadoraId) : 'Sin datos';
|
||||||
|
|
||||||
if (isLoading) return <div className="loading-container">Cargando datos del mapa...</div>;
|
return (
|
||||||
|
<Geography
|
||||||
|
key={geo.rsmKey + (isSelectedGeo ? '-selected' : '')}
|
||||||
|
geography={geo}
|
||||||
|
data-tooltip-id="partido-tooltip"
|
||||||
|
data-tooltip-content={`${geo.properties.departamento}: ${nombreAgrupacionGanadora}`}
|
||||||
|
className={`rsm-geography ${isSelected ? 'selected' : ''} ${isFaded ? 'faded' : ''} ${!isClickable ? 'no-results' : ''}`}
|
||||||
|
fill={getPartyFillColor(geo.properties.departamento)}
|
||||||
|
onClick={isClickable ? () => handleGeographyClick(geo) : undefined}
|
||||||
|
onMouseEnter={() => setTooltipContent(`${geo.properties.departamento}: ${nombreAgrupacionGanadora}`)}
|
||||||
|
onMouseLeave={() => setTooltipContent("")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mapa-wrapper">
|
<div className="mapa-wrapper">
|
||||||
<div className="mapa-container">
|
<div className="mapa-container">
|
||||||
<ComposableMap projection="geoMercator" projectionConfig={{ scale: 4400, center: [-60.5, -37.2] }} className="rsm-svg">
|
{isLoading ? <div className="spinner"></div> : (
|
||||||
|
<ComposableMap
|
||||||
|
key={selectedCategoriaId}
|
||||||
|
projection="geoMercator"
|
||||||
|
projectionConfig={{ scale: 4400, center: [-60.5, -37.2] }}
|
||||||
|
className="rsm-svg"
|
||||||
|
data-tooltip-id="partido-tooltip"
|
||||||
|
>
|
||||||
<ZoomableGroup
|
<ZoomableGroup
|
||||||
center={position.center}
|
center={position.center}
|
||||||
zoom={position.zoom}
|
zoom={position.zoom}
|
||||||
@@ -180,37 +205,47 @@ const MapaBsAs = () => {
|
|||||||
>
|
>
|
||||||
{geoData && (
|
{geoData && (
|
||||||
<Geographies geography={geoData}>
|
<Geographies geography={geoData}>
|
||||||
{({ geographies }: { geographies: PartidoGeography[] }) =>
|
{({ geographies }: { geographies: PartidoGeography[] }) => {
|
||||||
geographies.map((geo) => {
|
const selectedGeo = selectedAmbitoId
|
||||||
const departamentoNombre = geo.properties.departamento.toUpperCase();
|
? geographies.find(geo => {
|
||||||
const resultado = resultadosPorDepartamento.get(departamentoNombre);
|
const resultado = resultadosPorDepartamento.get(geo.properties.departamento.toUpperCase());
|
||||||
const isSelected = resultado ? selectedAmbitoId === resultado.ambitoId : false;
|
return resultado?.ambitoId === selectedAmbitoId;
|
||||||
const isFaded = selectedAmbitoId !== null && !isSelected;
|
})
|
||||||
const nombreAgrupacionGanadora = resultado ? nombresAgrupaciones.get(resultado.agrupacionGanadoraId) : 'Sin datos';
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Geography
|
<>
|
||||||
key={geo.rsmKey}
|
{geographies.map(geo => (!selectedGeo || geo.rsmKey !== selectedGeo.rsmKey) ? renderGeography(geo) : null)}
|
||||||
geography={geo}
|
{selectedGeo && renderGeography(selectedGeo, true)}
|
||||||
data-tooltip-id="partido-tooltip"
|
</>
|
||||||
data-tooltip-content={`${geo.properties.departamento}: ${nombreAgrupacionGanadora}`}
|
|
||||||
className={`rsm-geography ${isSelected ? 'selected' : ''} ${isFaded ? 'faded' : ''}`}
|
|
||||||
fill={getPartyFillColor(geo.properties.departamento)}
|
|
||||||
onClick={() => handleGeographyClick(geo)}
|
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
})
|
}}
|
||||||
}
|
|
||||||
</Geographies>
|
</Geographies>
|
||||||
)}
|
)}
|
||||||
</ZoomableGroup>
|
</ZoomableGroup>
|
||||||
</ComposableMap>
|
</ComposableMap>
|
||||||
<Tooltip id="partido-tooltip" variant="light" />
|
)}
|
||||||
|
<Tooltip id="partido-tooltip" content={tooltipContent} />
|
||||||
{selectedAmbitoId !== null && <ControlesMapa onReset={handleReset} />}
|
{selectedAmbitoId !== null && <ControlesMapa onReset={handleReset} />}
|
||||||
</div>
|
</div>
|
||||||
<div className="info-panel">
|
<div className="info-panel">
|
||||||
<DetalleMunicipio ambitoId={selectedAmbitoId} onReset={handleReset} />
|
<div className="mapa-categoria-selector">
|
||||||
|
<select
|
||||||
|
className="mapa-categoria-combobox"
|
||||||
|
value={selectedCategoriaId}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSelectedCategoriaId(Number(e.target.value));
|
||||||
|
handleReset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{CATEGORIAS.map(cat => (
|
||||||
|
<option key={cat.id} value={cat.id}>
|
||||||
|
{cat.nombre}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<DetalleMunicipio ambitoId={selectedAmbitoId} onReset={handleReset} categoriaId={selectedCategoriaId} />
|
||||||
<Legend resultados={resultadosPorDepartamento} nombresAgrupaciones={nombresAgrupaciones} />
|
<Legend resultados={resultadosPorDepartamento} nombresAgrupaciones={nombresAgrupaciones} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -224,10 +259,10 @@ const ControlesMapa = ({ onReset }: { onReset: () => void }) => (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const DetalleMunicipio = ({ ambitoId, onReset }: { ambitoId: number | null; onReset: () => void }) => {
|
const DetalleMunicipio = ({ ambitoId, onReset, categoriaId }: { ambitoId: number | null; onReset: () => void; categoriaId: number; }) => {
|
||||||
const { data, isLoading, error } = useQuery<ResultadoDetalladoMunicipio>({
|
const { data, isLoading, error } = useQuery<ResultadoDetalladoMunicipio>({
|
||||||
queryKey: ['municipioDetalle', ambitoId],
|
queryKey: ['municipioDetalle', ambitoId, categoriaId],
|
||||||
queryFn: async () => (await axios.get(`${API_BASE_URL}/Resultados/municipio/${ambitoId}`)).data,
|
queryFn: async () => (await axios.get(`${API_BASE_URL}/Resultados/municipio/${ambitoId}?categoriaId=${categoriaId}`)).data,
|
||||||
enabled: !!ambitoId,
|
enabled: !!ambitoId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
308
Elecciones-Web/frontend/src/components/MapaBsAsSecciones.tsx
Normal file
308
Elecciones-Web/frontend/src/components/MapaBsAsSecciones.tsx
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
// src/components/MapaBsAsSecciones.tsx
|
||||||
|
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||||
|
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
|
||||||
|
import { Tooltip } from 'react-tooltip';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { geoCentroid } from 'd3-geo';
|
||||||
|
import { getDetalleSeccion } from '../apiService';
|
||||||
|
import type { ResultadoDetalleSeccion } from '../apiService';
|
||||||
|
import './MapaBsAs.css';
|
||||||
|
|
||||||
|
// --- Interfaces y Tipos ---
|
||||||
|
type PointTuple = [number, number];
|
||||||
|
interface ResultadoMapaSeccion {
|
||||||
|
seccionId: string;
|
||||||
|
seccionNombre: string;
|
||||||
|
agrupacionGanadoraId: string | null;
|
||||||
|
colorGanador: string | null;
|
||||||
|
}
|
||||||
|
interface Agrupacion { id: string; nombre: string; }
|
||||||
|
interface Categoria { id: number; nombre: string; }
|
||||||
|
type SeccionGeography = {
|
||||||
|
rsmKey: string;
|
||||||
|
properties: { seccion: string; fna: string; };
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Constantes ---
|
||||||
|
const API_BASE_URL = 'http://localhost:5217/api';
|
||||||
|
const DEFAULT_MAP_COLOR = '#E0E0E0';
|
||||||
|
const CATEGORIAS: Categoria[] = [{ id: 5, nombre: 'Senadores' }, { id: 6, nombre: 'Diputados' }];
|
||||||
|
const SECCION_ID_TO_ROMAN: Record<string, string> = { '1': 'I', '2': 'II', '3': 'III', '4': 'IV', '5': 'V', '6': 'VI', '7': 'VII', '8': 'VIII' };
|
||||||
|
const ROMAN_TO_SECCION_ID: Record<string, string> = { 'I': '1', 'II': '2', 'III': '3', 'IV': '4', 'V': '5', 'VI': '6', 'VII': '7', 'VIII': '8' };
|
||||||
|
const MIN_ZOOM = 1;
|
||||||
|
const MAX_ZOOM = 5;
|
||||||
|
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 de Detalle ---
|
||||||
|
const DetalleSeccion = ({ seccion, categoriaId, onReset }: { seccion: SeccionGeography | null, categoriaId: number, onReset: () => void }) => {
|
||||||
|
// Obtenemos el ID numérico de la sección a partir de su número romano para llamar a la API
|
||||||
|
const seccionId = seccion ? ROMAN_TO_SECCION_ID[seccion.properties.seccion] : null;
|
||||||
|
|
||||||
|
const { data: resultadosDetalle, isLoading, error } = useQuery<ResultadoDetalleSeccion[]>({
|
||||||
|
queryKey: ['detalleSeccion', seccionId, categoriaId],
|
||||||
|
queryFn: () => getDetalleSeccion(seccionId!, categoriaId),
|
||||||
|
enabled: !!seccionId, // La query solo se ejecuta si hay una sección seleccionada
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!seccion) {
|
||||||
|
return (
|
||||||
|
<div className="detalle-placeholder">
|
||||||
|
<h3>Resultados por Sección</h3>
|
||||||
|
<p>Haga clic en una sección del mapa para ver los resultados detallados.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!seccion) return (<div className="detalle-placeholder"><h3>Resultados por Sección</h3><p>Haga clic en una sección del mapa para ver los resultados detallados.</p></div>);
|
||||||
|
if (isLoading) return (<div className="detalle-loading"><div className="spinner"></div><p>Cargando resultados de la sección...</p></div>);
|
||||||
|
if (error) return <div className="detalle-error">Error al cargar los datos de la sección.</div>;
|
||||||
|
|
||||||
|
// --- LÓGICA DE NOMBRE DE SECCIÓN ---
|
||||||
|
// Mapeo de número romano a nombre legible. Se puede mejorar en el futuro.
|
||||||
|
const NOMBRES_SECCIONES: Record<string, string> = {
|
||||||
|
'I': 'Sección Primera', 'II': 'Sección Segunda', 'III': 'Sección Tercera', 'IV': 'Sección Cuarta',
|
||||||
|
'V': 'Sección Quinta', 'VI': 'Sección Sexta', 'VII': 'Sección Séptima', 'VIII': 'Sección Capital'
|
||||||
|
};
|
||||||
|
const nombreSeccionLegible = NOMBRES_SECCIONES[seccion.properties.seccion] || "Sección Desconocida";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="detalle-content">
|
||||||
|
<button className="reset-button-panel" onClick={onReset}>← VOLVER</button>
|
||||||
|
{/* Mostramos el nombre legible de la sección */}
|
||||||
|
<h3>{nombreSeccionLegible}</h3>
|
||||||
|
|
||||||
|
<ul className="resultados-lista">
|
||||||
|
{/* Mapeamos los resultados obtenidos de la API */}
|
||||||
|
{resultadosDetalle?.map((r) => (
|
||||||
|
<li key={r.id}>
|
||||||
|
<div className="resultado-info">
|
||||||
|
<span className="partido-nombre">{r.nombre}</span>
|
||||||
|
<span className="partido-votos">{r.votos.toLocaleString('es-AR')} ({r.porcentaje.toFixed(2)}%)</span>
|
||||||
|
</div>
|
||||||
|
<div className="progress-bar">
|
||||||
|
<div className="progress-fill" style={{ width: `${r.porcentaje}%` }}></div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Componente de Controles del Mapa ---
|
||||||
|
const ControlesMapa = ({ onReset }: { onReset: () => void }) => (
|
||||||
|
<div className="map-controls">
|
||||||
|
<button onClick={onReset}>← VOLVER</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Componente Principal ---
|
||||||
|
const MapaBsAsSecciones = () => {
|
||||||
|
const [position, setPosition] = useState(INITIAL_POSITION);
|
||||||
|
const [selectedCategoriaId, setSelectedCategoriaId] = useState<number>(6);
|
||||||
|
const [clickedSeccion, setClickedSeccion] = useState<SeccionGeography | null>(null);
|
||||||
|
const [tooltipContent, setTooltipContent] = useState('');
|
||||||
|
|
||||||
|
const { data: geoData, isLoading: isLoadingGeo } = useQuery<any>({
|
||||||
|
queryKey: ['mapaGeoDataSecciones'],
|
||||||
|
queryFn: async () => (await axios.get('./secciones-electorales-pba.topojson')).data,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: resultadosData, isLoading: isLoadingResultados } = useQuery<ResultadoMapaSeccion[]>({
|
||||||
|
queryKey: ['mapaResultadosPorSeccion', selectedCategoriaId],
|
||||||
|
queryFn: async () => (await axios.get(`${API_BASE_URL}/Resultados/mapa-por-seccion?categoriaId=${selectedCategoriaId}`)).data,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: agrupacionesData, isLoading: isLoadingAgrupaciones } = useQuery<Agrupacion[]>({
|
||||||
|
queryKey: ['catalogoAgrupaciones'],
|
||||||
|
queryFn: async () => (await axios.get(`${API_BASE_URL}/Catalogos/agrupaciones`)).data,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { nombresAgrupaciones, resultadosPorSeccionRomana } = useMemo<{
|
||||||
|
nombresAgrupaciones: Map<string, string>;
|
||||||
|
resultadosPorSeccionRomana: Map<string, ResultadoMapaSeccion>;
|
||||||
|
}>(() => {
|
||||||
|
const nombresMap = new Map<string, string>();
|
||||||
|
const resultadosMap = new Map<string, ResultadoMapaSeccion>();
|
||||||
|
|
||||||
|
if (agrupacionesData) {
|
||||||
|
agrupacionesData.forEach(a => nombresMap.set(a.id, a.nombre));
|
||||||
|
}
|
||||||
|
if (resultadosData) {
|
||||||
|
resultadosData.forEach(r => {
|
||||||
|
const roman = SECCION_ID_TO_ROMAN[r.seccionId];
|
||||||
|
if (roman) resultadosMap.set(roman, r);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { nombresAgrupaciones: nombresMap, resultadosPorSeccionRomana: resultadosMap };
|
||||||
|
}, [agrupacionesData, resultadosData]);
|
||||||
|
|
||||||
|
const isLoading = isLoadingGeo || isLoadingResultados || isLoadingAgrupaciones;
|
||||||
|
|
||||||
|
const handleReset = useCallback(() => {
|
||||||
|
setClickedSeccion(null);
|
||||||
|
setPosition(INITIAL_POSITION);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleGeographyClick = useCallback((geo: SeccionGeography) => {
|
||||||
|
if (clickedSeccion?.rsmKey === geo.rsmKey) {
|
||||||
|
handleReset();
|
||||||
|
} else {
|
||||||
|
const centroid = geoCentroid(geo as any) as PointTuple;
|
||||||
|
setPosition({ center: centroid, zoom: 2 });
|
||||||
|
setClickedSeccion(geo);
|
||||||
|
}
|
||||||
|
}, [clickedSeccion, handleReset]);
|
||||||
|
|
||||||
|
const handleMoveEnd = (newPosition: { coordinates: PointTuple; zoom: number }) => {
|
||||||
|
if (newPosition.zoom <= MIN_ZOOM) {
|
||||||
|
if (position.zoom > MIN_ZOOM || clickedSeccion !== null) {
|
||||||
|
handleReset();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Si el usuario hace zoom out, deseleccionamos la sección para volver a la vista general
|
||||||
|
if (newPosition.zoom < position.zoom && clickedSeccion !== null) {
|
||||||
|
setClickedSeccion(null);
|
||||||
|
}
|
||||||
|
setPosition({ center: newPosition.coordinates, zoom: newPosition.zoom });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => e.key === 'Escape' && handleReset();
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [handleReset]);
|
||||||
|
|
||||||
|
const getSectionFillColor = (seccionRomana: string) => {
|
||||||
|
return resultadosPorSeccionRomana.get(seccionRomana)?.colorGanador || DEFAULT_MAP_COLOR;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleZoomIn = () => {
|
||||||
|
if (position.zoom < MAX_ZOOM) {
|
||||||
|
setPosition(p => ({ ...p, zoom: Math.min(p.zoom * 1.5, MAX_ZOOM) }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mapa-wrapper">
|
||||||
|
<div className="mapa-container">
|
||||||
|
{isLoading ? <div className="spinner"></div> : (
|
||||||
|
<ComposableMap
|
||||||
|
key={selectedCategoriaId}
|
||||||
|
projection="geoMercator"
|
||||||
|
projectionConfig={{ scale: 4400, center: [-60.5, -37.2] }}
|
||||||
|
className="rsm-svg"
|
||||||
|
data-tooltip-id="seccion-tooltip"
|
||||||
|
>
|
||||||
|
<ZoomableGroup
|
||||||
|
center={position.center}
|
||||||
|
zoom={position.zoom}
|
||||||
|
onMoveEnd={handleMoveEnd}
|
||||||
|
minZoom={MIN_ZOOM}
|
||||||
|
maxZoom={MAX_ZOOM}
|
||||||
|
translateExtent={TRANSLATE_EXTENT}
|
||||||
|
style={{ transition: "transform 400ms ease-in-out" }}
|
||||||
|
filterZoomEvent={(e: WheelEvent) => {
|
||||||
|
if (e.deltaY > 0) {
|
||||||
|
handleReset();
|
||||||
|
} else if (e.deltaY < 0) {
|
||||||
|
handleZoomIn();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{geoData && (
|
||||||
|
<Geographies geography={geoData}>
|
||||||
|
{({ geographies }: { geographies: SeccionGeography[] }) =>
|
||||||
|
geographies.map((geo) => {
|
||||||
|
const seccionRomana = geo.properties.seccion;
|
||||||
|
const resultado = resultadosPorSeccionRomana.get(seccionRomana);
|
||||||
|
const nombreGanador = resultado?.agrupacionGanadoraId ? nombresAgrupaciones.get(resultado.agrupacionGanadoraId) : 'Sin datos';
|
||||||
|
const isSelected = clickedSeccion?.rsmKey === geo.rsmKey;
|
||||||
|
const isFaded = clickedSeccion && !isSelected;
|
||||||
|
const isClickable = !!resultado;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Geography
|
||||||
|
key={geo.rsmKey + (isSelected ? '-selected' : '')}
|
||||||
|
geography={geo as any}
|
||||||
|
data-tooltip-id="seccion-tooltip"
|
||||||
|
data-tooltip-content={`${geo.properties.fna}: ${nombreGanador}`}
|
||||||
|
onClick={isClickable ? () => handleGeographyClick(geo) : undefined}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
if (isClickable) {
|
||||||
|
setTooltipContent(`${geo.properties.fna}: ${nombreGanador}`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => setTooltipContent("")}
|
||||||
|
className={`rsm-geography ${isSelected ? 'selected' : ''} ${isFaded ? 'faded' : ''} ${!isClickable ? 'no-results' : ''}`}
|
||||||
|
fill={getSectionFillColor(seccionRomana)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Geographies>
|
||||||
|
)}
|
||||||
|
</ZoomableGroup>
|
||||||
|
</ComposableMap>
|
||||||
|
)}
|
||||||
|
{/* El botón de volver ahora está aquí, en el componente principal */}
|
||||||
|
{clickedSeccion && <ControlesMapa onReset={handleReset} />}
|
||||||
|
<Tooltip id="seccion-tooltip" content={tooltipContent} />
|
||||||
|
</div>
|
||||||
|
<div className="info-panel">
|
||||||
|
<div className="mapa-categoria-selector">
|
||||||
|
<select
|
||||||
|
className="mapa-categoria-combobox"
|
||||||
|
value={selectedCategoriaId}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSelectedCategoriaId(Number(e.target.value));
|
||||||
|
handleReset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{CATEGORIAS.map(cat => (
|
||||||
|
<option key={cat.id} value={cat.id}>
|
||||||
|
{cat.nombre}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<DetalleSeccion seccion={clickedSeccion} categoriaId={selectedCategoriaId} onReset={handleReset} />
|
||||||
|
<LegendSecciones resultados={resultadosPorSeccionRomana} nombresAgrupaciones={nombresAgrupaciones} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Sub-componente para la Leyenda ---
|
||||||
|
const LegendSecciones = ({ resultados, nombresAgrupaciones }: { resultados: Map<string, ResultadoMapaSeccion>, nombresAgrupaciones: Map<string, string> }) => {
|
||||||
|
const legendItems = useMemo(() => {
|
||||||
|
const ganadoresUnicos = new Map<string, { nombre: string; color: string }>();
|
||||||
|
resultados.forEach(resultado => {
|
||||||
|
if (resultado.agrupacionGanadoraId && resultado.colorGanador && !ganadoresUnicos.has(resultado.agrupacionGanadoraId)) {
|
||||||
|
ganadoresUnicos.set(resultado.agrupacionGanadoraId, {
|
||||||
|
nombre: nombresAgrupaciones.get(resultado.agrupacionGanadoraId) || 'Desconocido',
|
||||||
|
color: resultado.colorGanador
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Array.from(ganadoresUnicos.values());
|
||||||
|
}, [resultados, nombresAgrupaciones]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="legend">
|
||||||
|
<h4>Ganadores por Sección</h4>
|
||||||
|
{legendItems.map(item => (
|
||||||
|
<div key={item.nombre} className="legend-item">
|
||||||
|
<div className="legend-color-box" style={{ backgroundColor: item.color }} />
|
||||||
|
<span>{item.nombre}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MapaBsAsSecciones;
|
||||||
@@ -4,6 +4,7 @@ import { getResumenProvincial, getConfiguracionPublica } from '../apiService';
|
|||||||
import type { CategoriaResumen, ResultadoTicker } from '../types/types';
|
import type { CategoriaResumen, ResultadoTicker } from '../types/types';
|
||||||
import { ImageWithFallback } from './ImageWithFallback';
|
import { ImageWithFallback } from './ImageWithFallback';
|
||||||
import './TickerWidget.css';
|
import './TickerWidget.css';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;
|
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;
|
||||||
|
|
||||||
@@ -20,7 +21,9 @@ export const TickerWidget = () => {
|
|||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const cantidadAMostrar = parseInt(configData?.TickerResultadosCantidad || '5', 10) + 1;
|
const cantidadAMostrar = useMemo(() => {
|
||||||
|
return parseInt(configData?.TickerResultadosCantidad || '5', 10) + 1;
|
||||||
|
}, [configData]);
|
||||||
|
|
||||||
if (isLoading) return <div className="ticker-wrapper loading">Cargando resumen...</div>;
|
if (isLoading) return <div className="ticker-wrapper loading">Cargando resumen...</div>;
|
||||||
if (error || !categorias) return <div className="ticker-wrapper error">No hay datos disponibles.</div>;
|
if (error || !categorias) return <div className="ticker-wrapper error">No hay datos disponibles.</div>;
|
||||||
|
|||||||
@@ -235,67 +235,36 @@ public class ResultadosController : ControllerBase
|
|||||||
return Ok(resultadosGanadores);
|
return Ok(resultadosGanadores);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("municipio/{ambitoId}")] // Cambiamos el nombre del parámetro de ruta
|
[HttpGet("municipio/{ambitoId}")]
|
||||||
public async Task<IActionResult> GetResultadosPorMunicipio(int ambitoId) // Cambiamos el tipo de string a int
|
public async Task<IActionResult> GetResultadosPorMunicipio(int ambitoId, [FromQuery] int categoriaId)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Buscando resultados para AmbitoGeograficoId: {AmbitoId}", ambitoId);
|
// Validamos que el ámbito exista
|
||||||
|
var ambito = await _dbContext.AmbitosGeograficos.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(a => a.Id == ambitoId && a.NivelId == 30);
|
||||||
|
if (ambito == null) return NotFound($"No se encontró el municipio con ID {ambitoId}");
|
||||||
|
|
||||||
// PASO 1: Buscar el Ámbito Geográfico directamente por su CLAVE PRIMARIA (AmbitoGeograficoId).
|
// Obtenemos los votos para ESE municipio y ESA categoría
|
||||||
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
|
var resultadosVotos = await _dbContext.ResultadosVotos
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(rv => rv.AgrupacionPolitica) // Incluimos el nombre del partido
|
.Include(rv => rv.AgrupacionPolitica)
|
||||||
.Where(rv => rv.AmbitoGeograficoId == ambito.Id)
|
.Where(rv => rv.AmbitoGeograficoId == ambitoId && rv.CategoriaId == categoriaId)
|
||||||
.OrderByDescending(rv => rv.CantidadVotos)
|
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
// PASO 4: Calcular el total de votos positivos para el porcentaje.
|
// Calculamos el total de votos solo para esta selección
|
||||||
long totalVotosPositivos = resultadosVotos.Sum(r => r.CantidadVotos);
|
var totalVotosPositivos = (decimal)resultadosVotos.Sum(r => r.CantidadVotos);
|
||||||
|
|
||||||
// PASO 5: Mapear todo al DTO de respuesta que el frontend espera.
|
// Mapeamos a la respuesta final que espera el frontend
|
||||||
var respuestaDto = new MunicipioResultadosDto
|
var respuesta = resultadosVotos
|
||||||
|
.OrderByDescending(r => r.CantidadVotos)
|
||||||
|
.Select(rv => new
|
||||||
{
|
{
|
||||||
MunicipioNombre = ambito.Nombre,
|
id = rv.AgrupacionPolitica.Id,
|
||||||
UltimaActualizacion = estadoRecuento.FechaTotalizacion,
|
nombre = rv.AgrupacionPolitica.NombreCorto ?? rv.AgrupacionPolitica.Nombre,
|
||||||
PorcentajeEscrutado = estadoRecuento.MesasTotalizadasPorcentaje,
|
votos = rv.CantidadVotos,
|
||||||
PorcentajeParticipacion = estadoRecuento.ParticipacionPorcentaje,
|
porcentaje = totalVotosPositivos > 0 ? (rv.CantidadVotos / totalVotosPositivos) * 100 : 0
|
||||||
Resultados = resultadosVotos.Select(rv => new AgrupacionResultadoDto
|
}).ToList();
|
||||||
{
|
|
||||||
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);
|
return Ok(respuesta);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("composicion-congreso")]
|
[HttpGet("composicion-congreso")]
|
||||||
@@ -535,13 +504,12 @@ public class ResultadosController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("concejales/{seccionId}")]
|
[HttpGet("concejales/{seccionId}")]
|
||||||
public async Task<IActionResult> GetResultadosConcejalesPorSeccion(string seccionId)
|
public async Task<IActionResult> GetResultadosConcejalesPorSeccion(string seccionId)
|
||||||
{
|
{
|
||||||
// 1. Encontrar todos los municipios (Nivel 30) que pertenecen a la sección dada (Nivel 20)
|
|
||||||
var municipiosDeLaSeccion = await _dbContext.AmbitosGeograficos
|
var municipiosDeLaSeccion = await _dbContext.AmbitosGeograficos
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(a => a.NivelId == 30 && a.SeccionProvincialId == seccionId)
|
.Where(a => a.NivelId == 30 && a.SeccionProvincialId == seccionId)
|
||||||
.Select(a => a.Id) // Solo necesitamos sus IDs
|
.Select(a => a.Id)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
if (!municipiosDeLaSeccion.Any())
|
if (!municipiosDeLaSeccion.Any())
|
||||||
@@ -549,7 +517,6 @@ public class ResultadosController : ControllerBase
|
|||||||
return Ok(new List<object>());
|
return Ok(new List<object>());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Obtener todos los resultados de la categoría Concejales (ID 7) para esos municipios
|
|
||||||
var resultadosMunicipales = await _dbContext.ResultadosVotos
|
var resultadosMunicipales = await _dbContext.ResultadosVotos
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(r => r.AgrupacionPolitica)
|
.Include(r => r.AgrupacionPolitica)
|
||||||
@@ -561,29 +528,156 @@ public class ResultadosController : ControllerBase
|
|||||||
.Where(l => l.CategoriaId == 7)
|
.Where(l => l.CategoriaId == 7)
|
||||||
.ToDictionaryAsync(l => l.AgrupacionPoliticaId);
|
.ToDictionaryAsync(l => l.AgrupacionPoliticaId);
|
||||||
|
|
||||||
// 3. Agrupar y sumar en memoria para obtener el total por partido para la sección
|
var totalVotosSeccion = (decimal)resultadosMunicipales.Sum(r => r.CantidadVotos);
|
||||||
var totalVotosSeccion = resultadosMunicipales.Sum(r => r.CantidadVotos);
|
|
||||||
|
|
||||||
var resultadosFinales = resultadosMunicipales
|
var resultadosFinales = resultadosMunicipales
|
||||||
.GroupBy(r => r.AgrupacionPolitica)
|
// 1. Agrupamos por el ID del partido para evitar duplicados.
|
||||||
|
.GroupBy(r => r.AgrupacionPoliticaId)
|
||||||
.Select(g => new
|
.Select(g => new
|
||||||
{
|
{
|
||||||
Agrupacion = g.Key,
|
// 2. Obtenemos la entidad completa del primer elemento del grupo.
|
||||||
|
Agrupacion = g.First().AgrupacionPolitica,
|
||||||
Votos = g.Sum(r => r.CantidadVotos)
|
Votos = g.Sum(r => r.CantidadVotos)
|
||||||
})
|
})
|
||||||
.OrderByDescending(r => r.Votos)
|
.OrderByDescending(r => r.Votos)
|
||||||
.Select(r => new
|
.Select(r => new
|
||||||
{
|
{
|
||||||
r.Agrupacion.Id,
|
Id = r.Agrupacion.Id, // Aseguramos que el Id esté en el objeto final
|
||||||
r.Agrupacion.Nombre,
|
r.Agrupacion.Nombre,
|
||||||
r.Agrupacion.NombreCorto,
|
r.Agrupacion.NombreCorto,
|
||||||
r.Agrupacion.Color,
|
r.Agrupacion.Color,
|
||||||
LogoUrl = logosConcejales.GetValueOrDefault(r.Agrupacion.Id)?.LogoUrl,
|
LogoUrl = logosConcejales.GetValueOrDefault(r.Agrupacion.Id)?.LogoUrl,
|
||||||
r.Votos,
|
Votos = r.Votos,
|
||||||
votosPorcentaje = totalVotosSeccion > 0 ? ((decimal)r.Votos * 100 / totalVotosSeccion) : 0
|
// --- CORRECCIÓN CLAVE ---
|
||||||
|
// 3. Usamos el nombre de propiedad correcto que el frontend espera: 'votosPorcentaje'
|
||||||
|
VotosPorcentaje = totalVotosSeccion > 0 ? ((decimal)r.Votos * 100 / totalVotosSeccion) : 0
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return Ok(resultadosFinales);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("mapa-por-seccion")]
|
||||||
|
public async Task<IActionResult> GetResultadosMapaPorSeccion([FromQuery] int categoriaId)
|
||||||
|
{
|
||||||
|
// 1. Obtenemos todos los resultados a nivel de MUNICIPIO para la categoría dada.
|
||||||
|
var resultadosMunicipales = await _dbContext.ResultadosVotos
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(r => r.AmbitoGeografico)
|
||||||
|
.Include(r => r.AgrupacionPolitica)
|
||||||
|
.Where(r => r.CategoriaId == categoriaId && r.AmbitoGeografico.NivelId == 30)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// 2. Agrupamos en memoria por Sección Electoral y sumamos los votos.
|
||||||
|
var ganadoresPorSeccion = resultadosMunicipales
|
||||||
|
.GroupBy(r => r.AmbitoGeografico.SeccionProvincialId)
|
||||||
|
.Select(g =>
|
||||||
|
{
|
||||||
|
// Para cada sección, encontramos al partido con más votos.
|
||||||
|
var ganador = g
|
||||||
|
.GroupBy(r => r.AgrupacionPolitica)
|
||||||
|
.Select(pg => new { Agrupacion = pg.Key, TotalVotos = pg.Sum(r => r.CantidadVotos) })
|
||||||
|
.OrderByDescending(x => x.TotalVotos)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
// Buscamos el nombre de la sección
|
||||||
|
var seccionInfo = _dbContext.AmbitosGeograficos
|
||||||
|
.FirstOrDefault(a => a.SeccionProvincialId == g.Key && a.NivelId == 20);
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
SeccionId = g.Key,
|
||||||
|
SeccionNombre = seccionInfo?.Nombre,
|
||||||
|
AgrupacionGanadoraId = ganador?.Agrupacion.Id,
|
||||||
|
ColorGanador = ganador?.Agrupacion.Color
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.Where(r => r.SeccionId != null) // Filtramos cualquier posible nulo
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return Ok(ganadoresPorSeccion);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("seccion/{seccionId}")]
|
||||||
|
public async Task<IActionResult> GetResultadosDetallePorSeccion(string seccionId, [FromQuery] int categoriaId)
|
||||||
|
{
|
||||||
|
var municipiosDeLaSeccion = await _dbContext.AmbitosGeograficos
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(a => a.NivelId == 30 && a.SeccionProvincialId == seccionId)
|
||||||
|
.Select(a => a.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (!municipiosDeLaSeccion.Any()) return Ok(new List<object>());
|
||||||
|
|
||||||
|
var resultadosMunicipales = await _dbContext.ResultadosVotos
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(r => r.AgrupacionPolitica)
|
||||||
|
.Where(r => r.CategoriaId == categoriaId && municipiosDeLaSeccion.Contains(r.AmbitoGeograficoId))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var totalVotosSeccion = (decimal)resultadosMunicipales.Sum(r => r.CantidadVotos);
|
||||||
|
var resultadosFinales = resultadosMunicipales
|
||||||
|
// 1. Agrupamos por el ID del partido, que es un valor único y estable
|
||||||
|
.GroupBy(r => r.AgrupacionPoliticaId)
|
||||||
|
.Select(g => new
|
||||||
|
{
|
||||||
|
// 2. Obtenemos el objeto Agrupacion del primer elemento del grupo
|
||||||
|
Agrupacion = g.First().AgrupacionPolitica,
|
||||||
|
Votos = g.Sum(r => r.CantidadVotos)
|
||||||
|
})
|
||||||
|
.OrderByDescending(r => r.Votos)
|
||||||
|
.Select(r => new
|
||||||
|
{
|
||||||
|
id = r.Agrupacion.Id,
|
||||||
|
nombre = r.Agrupacion.NombreCorto ?? r.Agrupacion.Nombre,
|
||||||
|
votos = r.Votos,
|
||||||
|
porcentaje = totalVotosSeccion > 0 ? ((decimal)r.Votos * 100 / totalVotosSeccion) : 0
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
return Ok(resultadosFinales);
|
return Ok(resultadosFinales);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("mapa-por-municipio")]
|
||||||
|
public async Task<IActionResult> GetResultadosMapaPorMunicipio([FromQuery] int categoriaId)
|
||||||
|
{
|
||||||
|
// Obtenemos los votos primero
|
||||||
|
var votosPorMunicipio = await _dbContext.ResultadosVotos
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(r => r.CategoriaId == categoriaId && r.AmbitoGeografico.NivelId == 30)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// Luego, los agrupamos en memoria
|
||||||
|
var ganadores = votosPorMunicipio
|
||||||
|
.GroupBy(r => r.AmbitoGeograficoId)
|
||||||
|
.Select(g => g.OrderByDescending(r => r.CantidadVotos).First())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Ahora, obtenemos los detalles necesarios en una sola consulta adicional
|
||||||
|
var idsAgrupacionesGanadoras = ganadores.Select(g => g.AgrupacionPoliticaId).ToList();
|
||||||
|
var idsAmbitosGanadores = ganadores.Select(g => g.AmbitoGeograficoId).ToList();
|
||||||
|
|
||||||
|
var agrupacionesInfo = await _dbContext.AgrupacionesPoliticas
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(a => idsAgrupacionesGanadoras.Contains(a.Id))
|
||||||
|
.ToDictionaryAsync(a => a.Id);
|
||||||
|
|
||||||
|
var ambitosInfo = await _dbContext.AmbitosGeograficos
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(a => idsAmbitosGanadores.Contains(a.Id))
|
||||||
|
.ToDictionaryAsync(a => a.Id);
|
||||||
|
|
||||||
|
// Finalmente, unimos todo en memoria
|
||||||
|
var resultadoFinal = ganadores.Select(g => new
|
||||||
|
{
|
||||||
|
AmbitoId = g.AmbitoGeograficoId,
|
||||||
|
DepartamentoNombre = ambitosInfo.GetValueOrDefault(g.AmbitoGeograficoId)?.Nombre,
|
||||||
|
AgrupacionGanadoraId = g.AgrupacionPoliticaId,
|
||||||
|
ColorGanador = agrupacionesInfo.GetValueOrDefault(g.AgrupacionPoliticaId)?.Color
|
||||||
|
})
|
||||||
|
.Where(r => r.DepartamentoNombre != null) // Filtramos por si acaso
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return Ok(resultadoFinal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,7 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Api")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Api")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+608ae655bedf6c59be5fec1e14fc308871d2fd62")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+12860f24067a7de29ec937e18f314f2f24059f47")]
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Api")]
|
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Api")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Api")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Api")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["BNGxWTPjjFD1Fj56FltRDUvsBzgMlQvuqV\u002BraH2IhwQ=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","ywKm3DCyXg4YCbZAIx3JUlT8N4Irff3GswYUVDST\u002BjQ=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/GfbpJthEWmsuz0uFx1QLHM7gyM1wLLeQgAIl4SzUD4=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","AvSkxVPIg0GjnB1RJ4hDNyo9p9GONrzDs8uVuixH\u002BOE=","IgT9pOgRnK37qfILj2QcjFoBZ180HMt\u002BScgje2iYOo4=","ezUlOMzNZmyKDIe1wwXqvX\u002BvhwfB992xNVts7r2zcTc=","y2BV4WpkQuLfqQhfOQBtmuzh940c3s4LAopGKfztfTE=","lHTUEsMkDu8nqXtfTwl7FRfgocyyc7RI5O/edTHN1\u002B0=","A7nz7qgOtQ1CwZZLvNnr0b5QZB3fTi3y4i6y7rBIcxQ=","znnuRi2tsk7AACuYo4WSgj7NcLriG4PKVaF4L35SvDk=","khGrM2Rl22MsVh9N6\u002B7todRrMuJ6o3ljuHxZF/aubqE=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","6xYke/2SzNspypSwIgizeNUH7b\u002Bfoz3wYfKk6z1tMsw="],"CachedAssets":{},"CachedCopyCandidates":{}}
|
{"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["BNGxWTPjjFD1Fj56FltRDUvsBzgMlQvuqV\u002BraH2IhwQ=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","ywKm3DCyXg4YCbZAIx3JUlT8N4Irff3GswYUVDST\u002BjQ=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/GfbpJthEWmsuz0uFx1QLHM7gyM1wLLeQgAIl4SzUD4=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","AvSkxVPIg0GjnB1RJ4hDNyo9p9GONrzDs8uVuixH\u002BOE=","IgT9pOgRnK37qfILj2QcjFoBZ180HMt\u002BScgje2iYOo4=","ezUlOMzNZmyKDIe1wwXqvX\u002BvhwfB992xNVts7r2zcTc=","y2BV4WpkQuLfqQhfOQBtmuzh940c3s4LAopGKfztfTE=","lHTUEsMkDu8nqXtfTwl7FRfgocyyc7RI5O/edTHN1\u002B0=","A7nz7qgOtQ1CwZZLvNnr0b5QZB3fTi3y4i6y7rBIcxQ=","znnuRi2tsk7AACuYo4WSgj7NcLriG4PKVaF4L35SvDk=","eeoSVF\u002BzWQgGKOM9hSCzmc0CNSUCyP/f6aJxLHiJ4A8=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","nt6JdXgYN0sYJ/7di0B2aSGDokBPJnFqtwyBcMkycYI="],"CachedAssets":{},"CachedCopyCandidates":{}}
|
||||||
@@ -1 +1 @@
|
|||||||
{"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["BNGxWTPjjFD1Fj56FltRDUvsBzgMlQvuqV\u002BraH2IhwQ=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","ywKm3DCyXg4YCbZAIx3JUlT8N4Irff3GswYUVDST\u002BjQ=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/GfbpJthEWmsuz0uFx1QLHM7gyM1wLLeQgAIl4SzUD4=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","AvSkxVPIg0GjnB1RJ4hDNyo9p9GONrzDs8uVuixH\u002BOE=","IgT9pOgRnK37qfILj2QcjFoBZ180HMt\u002BScgje2iYOo4=","ezUlOMzNZmyKDIe1wwXqvX\u002BvhwfB992xNVts7r2zcTc=","y2BV4WpkQuLfqQhfOQBtmuzh940c3s4LAopGKfztfTE=","lHTUEsMkDu8nqXtfTwl7FRfgocyyc7RI5O/edTHN1\u002B0=","A7nz7qgOtQ1CwZZLvNnr0b5QZB3fTi3y4i6y7rBIcxQ=","znnuRi2tsk7AACuYo4WSgj7NcLriG4PKVaF4L35SvDk=","khGrM2Rl22MsVh9N6\u002B7todRrMuJ6o3ljuHxZF/aubqE=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","6xYke/2SzNspypSwIgizeNUH7b\u002Bfoz3wYfKk6z1tMsw="],"CachedAssets":{},"CachedCopyCandidates":{}}
|
{"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["BNGxWTPjjFD1Fj56FltRDUvsBzgMlQvuqV\u002BraH2IhwQ=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","ywKm3DCyXg4YCbZAIx3JUlT8N4Irff3GswYUVDST\u002BjQ=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/GfbpJthEWmsuz0uFx1QLHM7gyM1wLLeQgAIl4SzUD4=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","AvSkxVPIg0GjnB1RJ4hDNyo9p9GONrzDs8uVuixH\u002BOE=","IgT9pOgRnK37qfILj2QcjFoBZ180HMt\u002BScgje2iYOo4=","ezUlOMzNZmyKDIe1wwXqvX\u002BvhwfB992xNVts7r2zcTc=","y2BV4WpkQuLfqQhfOQBtmuzh940c3s4LAopGKfztfTE=","lHTUEsMkDu8nqXtfTwl7FRfgocyyc7RI5O/edTHN1\u002B0=","A7nz7qgOtQ1CwZZLvNnr0b5QZB3fTi3y4i6y7rBIcxQ=","znnuRi2tsk7AACuYo4WSgj7NcLriG4PKVaF4L35SvDk=","eeoSVF\u002BzWQgGKOM9hSCzmc0CNSUCyP/f6aJxLHiJ4A8=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","nt6JdXgYN0sYJ/7di0B2aSGDokBPJnFqtwyBcMkycYI="],"CachedAssets":{},"CachedCopyCandidates":{}}
|
||||||
@@ -13,7 +13,7 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Core")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Core")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+608ae655bedf6c59be5fec1e14fc308871d2fd62")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+12860f24067a7de29ec937e18f314f2f24059f47")]
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Core")]
|
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Core")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Core")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Core")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Database")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Database")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+3b8c6bf754cff6ace486ae8fe850ed4d69233280")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+12860f24067a7de29ec937e18f314f2f24059f47")]
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Database")]
|
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Database")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Database")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Database")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Infrastructure")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Infrastructure")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+608ae655bedf6c59be5fec1e14fc308871d2fd62")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+12860f24067a7de29ec937e18f314f2f24059f47")]
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Infrastructure")]
|
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Infrastructure")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Infrastructure")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Infrastructure")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|||||||
Reference in New Issue
Block a user