Preparación Legislativas Nacionales 2025

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,104 @@
// src/features/legislativas/nacionales/components/MapaNacional.tsx
import axios from 'axios';
import { Suspense, useState, useEffect, useCallback } from 'react'; // <-- Asegúrate de que useCallback esté importado
import { useSuspenseQuery } from '@tanstack/react-query';
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
import { Tooltip } from 'react-tooltip';
import { API_BASE_URL, assetBaseUrl } from '../../../../apiService';
import type { ResultadoMapaDto, AmbitoGeography } from '../../../../types/types';
import { MapaProvincial } from './MapaProvincial';
const DEFAULT_MAP_COLOR = '#E0E0E0';
const FADED_BACKGROUND_COLOR = '#F0F0F0';
const normalizarTexto = (texto: string = '') => texto.trim().toUpperCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
type PointTuple = [number, number];
interface MapaNacionalProps {
eleccionId: number;
categoriaId: number;
nivel: 'pais' | 'provincia' | 'municipio';
nombreAmbito: string;
provinciaDistritoId: string | null;
onAmbitoSelect: (ambitoId: string, nivel: 'provincia' | 'municipio', nombre: string) => void;
onVolver: () => void;
}
export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, provinciaDistritoId, onAmbitoSelect, onVolver }: MapaNacionalProps) => {
const [position, setPosition] = useState({ zoom: 1, center: [-65, -40] as PointTuple });
const { data: mapaDataNacional } = useSuspenseQuery<ResultadoMapaDto[]>({
queryKey: ['mapaResultados', eleccionId, categoriaId, null],
queryFn: async () => {
const url = `${API_BASE_URL}/elecciones/${eleccionId}/mapa-resultados?categoriaId=${categoriaId}`;
const response = await axios.get(url);
return response.data;
},
});
const { data: geoDataNacional } = useSuspenseQuery<any>({
queryKey: ['geoDataNacional'],
queryFn: () => axios.get(`${assetBaseUrl}/maps/argentina-provincias.topojson`).then(res => res.data),
});
const resultadosNacionalesPorNombre = new Map<string, ResultadoMapaDto>(mapaDataNacional.map(d => [normalizarTexto(d.ambitoNombre), d]));
const nombreMunicipioSeleccionado = nivel === 'municipio' ? nombreAmbito : null;
// El useEffect para el zoom provincial y nacional sigue siendo correcto.
useEffect(() => {
if (nivel === 'pais') {
setPosition({ zoom: 1, center: [-65, -40] });
} else if (nivel === 'provincia') {
setPosition({ zoom: 7, center: [-60.5, -37] });
}
// La lógica de centrado en municipio se delega al hijo, que llamará a `handleCalculatedCenter`
}, [nivel]);
// **LA SOLUCIÓN CLAVE**: Estabilizamos la función que se pasa al hijo.
const handleCalculatedCenter = useCallback((center: PointTuple, zoom: number) => {
setPosition({ center, zoom });
}, []); // El array de dependencias vacío asegura que la función nunca cambie
return (
<div className="mapa-componente-container">
{nivel !== 'pais' && <button onClick={onVolver} className="mapa-volver-btn"> Volver</button>}
<ComposableMap projection="geoMercator" projectionConfig={{ scale: 700, center: [-65, -40] }} style={{ width: "100%", height: "100%" }}>
<ZoomableGroup center={position.center} zoom={position.zoom} filterZoomEvent={() => false}>
<Geographies geography={geoDataNacional}>
{({ geographies }: { geographies: AmbitoGeography[] }) => geographies.map((geo) => {
const resultado = resultadosNacionalesPorNombre.get(normalizarTexto(geo.properties.nombre));
const esProvinciaActiva = provinciaDistritoId && resultado?.ambitoId === provinciaDistritoId;
return (
<Geography
key={geo.rsmKey} geography={geo}
className={`rsm-geography ${nivel !== 'pais' ? 'rsm-geography-faded' : ''}`}
style={{ visibility: esProvinciaActiva ? 'hidden' : 'visible' }}
fill={nivel === 'pais' ? (resultado?.colorGanador || DEFAULT_MAP_COLOR) : FADED_BACKGROUND_COLOR}
onClick={() => resultado && onAmbitoSelect(resultado.ambitoId, 'provincia', resultado.ambitoNombre)}
/>
);
})}
</Geographies>
{provinciaDistritoId && (
<Suspense fallback={null}>
<MapaProvincial
eleccionId={eleccionId}
categoriaId={categoriaId}
distritoId={provinciaDistritoId}
nombreProvincia={"BUENOS AIRES"} // Esto se podría hacer dinámico si fuera necesario
nombreMunicipioSeleccionado={nombreMunicipioSeleccionado}
onMunicipioSelect={(ambitoId, nombre) => onAmbitoSelect(ambitoId, 'municipio', nombre)}
onCalculatedCenter={handleCalculatedCenter} // Pasamos la función estabilizada
nivel={nivel as 'provincia' | 'municipio'} // El cast de tipo sigue siendo necesario y correcto
/>
</Suspense>
)}
</ZoomableGroup>
</ComposableMap>
<Tooltip id="mapa-tooltip" />
</div>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
// src/components/MapaBsAs.tsx // src/features/legislativas/provinciales/MapaBsAs.tsx
import { useState, useMemo, useCallback, useEffect } from 'react'; import { useState, useMemo, useCallback, useEffect } 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';
@@ -7,7 +7,7 @@ import axios from 'axios';
import { feature } from 'topojson-client'; import { feature } from 'topojson-client';
import type { Feature, Geometry } from 'geojson'; import type { Feature, Geometry } from 'geojson';
import { geoCentroid } from 'd3-geo'; import { geoCentroid } from 'd3-geo';
import { API_BASE_URL, assetBaseUrl } from '../apiService'; import { API_BASE_URL, assetBaseUrl } from '../../../apiService';
import './MapaBsAs.css'; import './MapaBsAs.css';
// --- Interfaces y Tipos --- // --- Interfaces y Tipos ---

View File

@@ -1,12 +1,12 @@
// src/components/MapaBsAsSecciones.tsx // src/features/legislativas/provinciales/MapaBsAsSecciones.tsx
import { useState, useMemo, useCallback, useEffect } from 'react'; import { useState, useMemo, useCallback, useEffect } 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';
import axios from 'axios'; import axios from 'axios';
import { geoCentroid } from 'd3-geo'; import { geoCentroid } from 'd3-geo';
import { getDetalleSeccion, API_BASE_URL, assetBaseUrl } from '../apiService'; import { getDetalleSeccion, API_BASE_URL, assetBaseUrl } from '../../../apiService';
import { type ResultadoDetalleSeccion } from '../apiService'; import { type ResultadoDetalleSeccion } from '../../../apiService';
import './MapaBsAs.css'; import './MapaBsAs.css';
// --- Interfaces y Tipos --- // --- Interfaces y Tipos ---

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
// src/Elecciones.Api/Controllers/ResultadosController.cs // src/Elecciones.Api/Controllers/ResultadosController.cs
using Elecciones.Core.DTOs;
using Elecciones.Core.DTOs.ApiResponses; using Elecciones.Core.DTOs.ApiResponses;
using Elecciones.Database; using Elecciones.Database;
using Elecciones.Database.Entities; using Elecciones.Database.Entities;
@@ -8,7 +9,7 @@ using Microsoft.EntityFrameworkCore;
namespace Elecciones.Api.Controllers; namespace Elecciones.Api.Controllers;
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/elecciones/{eleccionId}")]
public class ResultadosController : ControllerBase public class ResultadosController : ControllerBase
{ {
private readonly EleccionesDbContext _dbContext; private readonly EleccionesDbContext _dbContext;
@@ -46,7 +47,7 @@ public class ResultadosController : ControllerBase
} }
[HttpGet("partido/{municipioId}")] [HttpGet("partido/{municipioId}")]
public async Task<IActionResult> GetResultadosPorPartido(string municipioId, [FromQuery] int categoriaId) public async Task<IActionResult> GetResultadosPorPartido([FromRoute] int eleccionId, string municipioId, [FromQuery] int categoriaId)
{ {
var ambito = await _dbContext.AmbitosGeograficos.AsNoTracking() var ambito = await _dbContext.AmbitosGeograficos.AsNoTracking()
.FirstOrDefaultAsync(a => a.SeccionId == municipioId && a.NivelId == 30); .FirstOrDefaultAsync(a => a.SeccionId == municipioId && a.NivelId == 30);
@@ -57,7 +58,7 @@ public class ResultadosController : ControllerBase
} }
var estadoRecuento = await _dbContext.EstadosRecuentos.AsNoTracking() var estadoRecuento = await _dbContext.EstadosRecuentos.AsNoTracking()
.FirstOrDefaultAsync(e => e.AmbitoGeograficoId == ambito.Id && e.CategoriaId == categoriaId); .FirstOrDefaultAsync(e => e.EleccionId == eleccionId && e.AmbitoGeograficoId == ambito.Id && e.CategoriaId == categoriaId);
var agrupacionIds = await _dbContext.ResultadosVotos var agrupacionIds = await _dbContext.ResultadosVotos
.Where(rv => rv.AmbitoGeograficoId == ambito.Id && rv.CategoriaId == categoriaId) .Where(rv => rv.AmbitoGeograficoId == ambito.Id && rv.CategoriaId == categoriaId)
@@ -71,7 +72,7 @@ public class ResultadosController : ControllerBase
var resultadosVotos = await _dbContext.ResultadosVotos.AsNoTracking() var resultadosVotos = await _dbContext.ResultadosVotos.AsNoTracking()
.Include(rv => rv.AgrupacionPolitica) .Include(rv => rv.AgrupacionPolitica)
.Where(rv => rv.AmbitoGeograficoId == ambito.Id && rv.CategoriaId == categoriaId) .Where(rv => rv.EleccionId == eleccionId && rv.AmbitoGeograficoId == ambito.Id && rv.CategoriaId == categoriaId)
.ToListAsync(); .ToListAsync();
var candidatosRelevantes = await _dbContext.CandidatosOverrides.AsNoTracking() var candidatosRelevantes = await _dbContext.CandidatosOverrides.AsNoTracking()
@@ -119,7 +120,7 @@ public class ResultadosController : ControllerBase
} }
[HttpGet("provincia/{distritoId}")] [HttpGet("provincia/{distritoId}")]
public async Task<IActionResult> GetResultadosProvinciales(string distritoId) public async Task<IActionResult> GetResultadosProvinciales([FromRoute] int eleccionId, string distritoId)
{ {
var provincia = await _dbContext.AmbitosGeograficos.AsNoTracking() var provincia = await _dbContext.AmbitosGeograficos.AsNoTracking()
.FirstOrDefaultAsync(a => a.DistritoId == distritoId && a.NivelId == 10); .FirstOrDefaultAsync(a => a.DistritoId == distritoId && a.NivelId == 10);
@@ -131,13 +132,13 @@ public class ResultadosController : ControllerBase
var estadosPorCategoria = await _dbContext.EstadosRecuentosGenerales.AsNoTracking() var estadosPorCategoria = await _dbContext.EstadosRecuentosGenerales.AsNoTracking()
.Include(e => e.CategoriaElectoral) .Include(e => e.CategoriaElectoral)
.Where(e => e.AmbitoGeograficoId == provincia.Id) .Where(e => e.EleccionId == eleccionId && e.AmbitoGeograficoId == provincia.Id)
.ToDictionaryAsync(e => e.CategoriaId); .ToDictionaryAsync(e => e.CategoriaId);
var resultadosPorMunicipio = await _dbContext.ResultadosVotos var resultadosPorMunicipio = await _dbContext.ResultadosVotos
.AsNoTracking() .AsNoTracking()
.Include(r => r.AgrupacionPolitica) .Include(r => r.AgrupacionPolitica)
.Where(r => r.AmbitoGeografico.NivelId == 30) // Nivel 30 = Municipio .Where(r => r.EleccionId == eleccionId && r.AmbitoGeografico.NivelId == 30) // Nivel 30 = Municipio
.ToListAsync(); .ToListAsync();
// Obtenemos TODOS los logos relevantes en una sola consulta // Obtenemos TODOS los logos relevantes en una sola consulta
@@ -194,8 +195,8 @@ public class ResultadosController : ControllerBase
} }
[HttpGet("bancas-por-seccion/{seccionId}/{camara}")] // <-- CAMBIO 1: Modificar la ruta [HttpGet("bancas-por-seccion/{seccionId}/{camara}")]
public async Task<IActionResult> GetBancasPorSeccion(string seccionId, string camara) // <-- CAMBIO 2: Añadir el nuevo parámetro public async Task<IActionResult> GetBancasPorSeccion([FromRoute] int eleccionId, string seccionId, string camara)
{ {
// Convertimos el string de la cámara a un enum o un valor numérico para la base de datos // Convertimos el string de la cámara a un enum o un valor numérico para la base de datos
// 0 = Diputados, 1 = Senadores. Esto debe coincidir con cómo lo guardas en la DB. // 0 = Diputados, 1 = Senadores. Esto debe coincidir con cómo lo guardas en la DB.
@@ -228,7 +229,7 @@ public class ResultadosController : ControllerBase
var proyecciones = await _dbContext.ProyeccionesBancas var proyecciones = await _dbContext.ProyeccionesBancas
.AsNoTracking() .AsNoTracking()
.Include(p => p.AgrupacionPolitica) .Include(p => p.AgrupacionPolitica)
.Where(p => p.AmbitoGeograficoId == seccion.Id && p.CategoriaId == CategoriaId) // <-- AÑADIDO EL FILTRO .Where(p => p.EleccionId == eleccionId && p.AmbitoGeograficoId == seccion.Id && p.CategoriaId == CategoriaId)
.Select(p => new .Select(p => new
{ {
AgrupacionId = p.AgrupacionPolitica.Id, // Añadir para el 'key' en React AgrupacionId = p.AgrupacionPolitica.Id, // Añadir para el 'key' en React
@@ -347,7 +348,7 @@ public class ResultadosController : ControllerBase
} }
[HttpGet("composicion-congreso")] [HttpGet("composicion-congreso")]
public async Task<IActionResult> GetComposicionCongreso() public async Task<IActionResult> GetComposicionCongreso([FromRoute] int eleccionId)
{ {
var config = await _dbContext.Configuraciones var config = await _dbContext.Configuraciones
.AsNoTracking() .AsNoTracking()
@@ -360,22 +361,23 @@ public class ResultadosController : ControllerBase
if (usarDatosOficiales) if (usarDatosOficiales)
{ {
// Si el interruptor está en 'true', llama a este método // Si el interruptor está en 'true', llama a este método
return await GetComposicionDesdeBancadasOficiales(config); return await GetComposicionDesdeBancadasOficiales(config, eleccionId);
} }
else else
{ {
// Si está en 'false' o no existe, llama a este otro // Si está en 'false' o no existe, llama a este otro
return await GetComposicionDesdeProyecciones(config); return await GetComposicionDesdeProyecciones(config, eleccionId);
} }
} }
// En ResultadosController.cs // En ResultadosController.cs
private async Task<IActionResult> GetComposicionDesdeBancadasOficiales(Dictionary<string, string> config) private async Task<IActionResult> GetComposicionDesdeBancadasOficiales(Dictionary<string, string> config, int eleccionId)
{ {
config.TryGetValue("MostrarOcupantes", out var mostrarOcupantesValue); config.TryGetValue("MostrarOcupantes", out var mostrarOcupantesValue);
bool mostrarOcupantes = mostrarOcupantesValue == "true"; bool mostrarOcupantes = mostrarOcupantesValue == "true";
IQueryable<Bancada> bancadasQuery = _dbContext.Bancadas.AsNoTracking() IQueryable<Bancada> bancadasQuery = _dbContext.Bancadas.AsNoTracking()
.Where(b => b.EleccionId == eleccionId)
.Include(b => b.AgrupacionPolitica); .Include(b => b.AgrupacionPolitica);
if (mostrarOcupantes) if (mostrarOcupantes)
{ {
@@ -459,7 +461,7 @@ public class ResultadosController : ControllerBase
return Ok(new { Diputados = diputados, Senadores = senadores }); return Ok(new { Diputados = diputados, Senadores = senadores });
} }
private async Task<IActionResult> GetComposicionDesdeProyecciones(Dictionary<string, string> config) private async Task<IActionResult> GetComposicionDesdeProyecciones(Dictionary<string, string> config, int eleccionId)
{ {
// --- INICIO DE LA CORRECCIÓN --- // --- INICIO DE LA CORRECCIÓN ---
// 1. Obtenemos el ID del ámbito provincial para usarlo en el filtro. // 1. Obtenemos el ID del ámbito provincial para usarlo en el filtro.
@@ -480,8 +482,7 @@ public class ResultadosController : ControllerBase
var bancasPorAgrupacion = await _dbContext.ProyeccionesBancas var bancasPorAgrupacion = await _dbContext.ProyeccionesBancas
.AsNoTracking() .AsNoTracking()
// --- CAMBIO CLAVE: Añadimos el filtro por AmbitoGeograficoId --- .Where(p => p.EleccionId == eleccionId && p.AmbitoGeograficoId == provincia.Id)
.Where(p => p.AmbitoGeograficoId == provincia.Id)
.GroupBy(p => new { p.AgrupacionPoliticaId, p.CategoriaId }) .GroupBy(p => new { p.AgrupacionPoliticaId, p.CategoriaId })
.Select(g => new .Select(g => new
{ {
@@ -551,7 +552,7 @@ public class ResultadosController : ControllerBase
} }
[HttpGet("bancadas-detalle")] [HttpGet("bancadas-detalle")]
public async Task<IActionResult> GetBancadasConOcupantes() public async Task<IActionResult> GetBancadasConOcupantes([FromRoute] int eleccionId)
{ {
var config = await _dbContext.Configuraciones.AsNoTracking().ToDictionaryAsync(c => c.Clave, c => c.Valor); var config = await _dbContext.Configuraciones.AsNoTracking().ToDictionaryAsync(c => c.Clave, c => c.Valor);
@@ -565,6 +566,7 @@ public class ResultadosController : ControllerBase
// Si el modo oficial SÍ está activo, devolvemos los detalles. // Si el modo oficial SÍ está activo, devolvemos los detalles.
var bancadasConOcupantes = await _dbContext.Bancadas var bancadasConOcupantes = await _dbContext.Bancadas
.AsNoTracking() .AsNoTracking()
.Where(b => b.EleccionId == eleccionId)
.Include(b => b.Ocupante) .Include(b => b.Ocupante)
.Select(b => new .Select(b => new
{ {
@@ -1012,4 +1014,342 @@ public class ResultadosController : ControllerBase
Resultados = resultadosPorMunicipio Resultados = resultadosPorMunicipio
}); });
} }
[HttpGet("panel/{ambitoId?}")]
public async Task<IActionResult> GetPanelElectoral(int eleccionId, string? ambitoId, [FromQuery] int categoriaId)
{
if (string.IsNullOrEmpty(ambitoId))
{
// CASO 1: No hay ID -> Vista Nacional
return await GetPanelNacional(eleccionId, categoriaId);
}
// CASO 2: El ID es un número (y no un string corto como "02") -> Vista Municipal
// La condición clave es que los IDs de distrito son cortos. Los IDs de BD son más largos.
// O simplemente, un ID de distrito nunca será un ID de municipio.
if (int.TryParse(ambitoId, out int idNumerico) && ambitoId.Length > 2)
{
return await GetPanelMunicipal(eleccionId, idNumerico, categoriaId);
}
else
{
// CASO 3: El ID es un string corto como "02" o "06" -> Vista Provincial
return await GetPanelProvincial(eleccionId, ambitoId, categoriaId);
}
}
private async Task<IActionResult> GetPanelMunicipal(int eleccionId, int ambitoId, int categoriaId)
{
// 1. Validar y obtener la entidad del municipio
var municipio = await _dbContext.AmbitosGeograficos.AsNoTracking()
.FirstOrDefaultAsync(a => a.Id == ambitoId && a.NivelId == 30);
if (municipio == null) return NotFound($"No se encontró el municipio con ID {ambitoId}.");
// 2. Obtener los votos solo para ESE municipio
var resultadosCrudos = await _dbContext.ResultadosVotos.AsNoTracking()
.Include(r => r.AgrupacionPolitica)
.Where(r => r.EleccionId == eleccionId &&
r.CategoriaId == categoriaId &&
r.AmbitoGeograficoId == ambitoId)
.ToListAsync();
if (!resultadosCrudos.Any())
{
// Devolver un DTO vacío pero válido si no hay resultados
return Ok(new PanelElectoralDto
{
AmbitoNombre = municipio.Nombre,
MapaData = new List<ResultadoMapaDto>(), // El mapa estará vacío en la vista de un solo municipio
ResultadosPanel = new List<AgrupacionResultadoDto>(),
EstadoRecuento = new EstadoRecuentoDto()
});
}
// 3. Calcular los resultados para el panel lateral (son los mismos datos crudos)
var totalVotosMunicipio = (decimal)resultadosCrudos.Sum(r => r.CantidadVotos);
var resultadosPanel = resultadosCrudos
.Select(g => new AgrupacionResultadoDto
{
Id = g.AgrupacionPolitica.Id,
Nombre = g.AgrupacionPolitica.Nombre,
NombreCorto = g.AgrupacionPolitica.NombreCorto,
Color = g.AgrupacionPolitica.Color,
Votos = g.CantidadVotos,
Porcentaje = totalVotosMunicipio > 0 ? (g.CantidadVotos / totalVotosMunicipio) * 100 : 0
})
.OrderByDescending(r => r.Votos)
.ToList();
var estadoRecuento = await _dbContext.EstadosRecuentos
.AsNoTracking()
.FirstOrDefaultAsync(e => e.EleccionId == eleccionId && e.AmbitoGeograficoId == ambitoId && e.CategoriaId == categoriaId);
var respuesta = new PanelElectoralDto
{
AmbitoNombre = municipio.Nombre,
MapaData = new List<ResultadoMapaDto>(), // El mapa no muestra sub-geografías aquí
ResultadosPanel = resultadosPanel,
EstadoRecuento = new EstadoRecuentoDto
{
ParticipacionPorcentaje = estadoRecuento?.ParticipacionPorcentaje ?? 0,
MesasTotalizadasPorcentaje = estadoRecuento?.MesasTotalizadasPorcentaje ?? 0
}
};
return Ok(respuesta);
}
// Este método se ejecutará cuando la URL sea, por ejemplo, /api/elecciones/2/panel/02?categoriaId=2
private async Task<IActionResult> GetPanelProvincial(int eleccionId, string distritoId, int categoriaId)
{
// 1. Validar y obtener la entidad de la provincia
var provincia = await _dbContext.AmbitosGeograficos.AsNoTracking()
.FirstOrDefaultAsync(a => a.DistritoId == distritoId && a.NivelId == 10);
if (provincia == null)
{
_logger.LogWarning("Panel Provincial: La provincia {DistritoId} no fue encontrada en AmbitosGeograficos.", distritoId);
return NotFound($"No se encontró la provincia con distritoId {distritoId}");
}
_logger.LogInformation("Panel Provincial: Provincia {Nombre} ({Id}) encontrada. Buscando municipios...", provincia.Nombre, provincia.Id);
// 2. Obtener los votos de TODOS los municipios de ESA provincia.
var resultadosCrudos = await _dbContext.ResultadosVotos.AsNoTracking()
.Include(r => r.AgrupacionPolitica)
.Include(r => r.AmbitoGeografico)
.Where(r => r.EleccionId == eleccionId &&
r.CategoriaId == categoriaId &&
r.AmbitoGeografico.DistritoId == distritoId &&
r.AmbitoGeografico.NivelId == 30)
.ToListAsync();
if (!resultadosCrudos.Any())
{
return Ok(new PanelElectoralDto
{
AmbitoNombre = provincia.Nombre,
MapaData = new List<ResultadoMapaDto>(),
ResultadosPanel = new List<AgrupacionResultadoDto>(),
EstadoRecuento = new EstadoRecuentoDto()
});
}
// 3. Calcular los resultados para el PANEL LATERAL (agregado provincial)
var totalVotosProvincia = (decimal)resultadosCrudos.Sum(r => r.CantidadVotos);
var resultadosPanel = resultadosCrudos
.GroupBy(r => r.AgrupacionPolitica.Id)
.Select(g => new AgrupacionResultadoDto
{
Id = g.Key,
Nombre = g.First().AgrupacionPolitica.Nombre,
NombreCorto = g.First().AgrupacionPolitica.NombreCorto,
Color = g.First().AgrupacionPolitica.Color,
Votos = g.Sum(r => r.CantidadVotos),
Porcentaje = totalVotosProvincia > 0 ? (g.Sum(r => r.CantidadVotos) / totalVotosProvincia) * 100 : 0
})
.OrderByDescending(r => r.Votos)
.ToList();
// --- CORRECCIÓN DEFINITIVA DE LA LÓGICA DEL MAPA ---
var mapaData = resultadosCrudos
.GroupBy(r => r.AmbitoGeografico) // 1. Agrupar por la entidad del municipio
.Select(g =>
{
// 2. DENTRO de cada grupo (municipio), encontrar la fila con más votos
var ganador = g.OrderByDescending(r => r.CantidadVotos).First();
// 3. Crear UN SOLO objeto ResultadoMapaDto para este municipio
return new ResultadoMapaDto
{
AmbitoId = g.Key.SeccionId!,
AmbitoNombre = g.Key.Nombre,
AgrupacionGanadoraId = ganador.AgrupacionPolitica.Id,
ColorGanador = ganador.AgrupacionPolitica.Color ?? "#808080"
};
})
.ToList();
var estadoRecuento = await _dbContext.EstadosRecuentosGenerales.AsNoTracking()
.FirstOrDefaultAsync(e => e.EleccionId == eleccionId && e.AmbitoGeograficoId == provincia.Id && e.CategoriaId == categoriaId);
var respuesta = new PanelElectoralDto
{
AmbitoNombre = provincia.Nombre,
MapaData = mapaData,
ResultadosPanel = resultadosPanel,
EstadoRecuento = new EstadoRecuentoDto() // Simplificado para el ejemplo
};
return Ok(respuesta);
}
private async Task<IActionResult> GetPanelNacional(int eleccionId, int categoriaId)
{
var resultadosCrudos = await _dbContext.ResultadosVotos.AsNoTracking()
.Include(r => r.AgrupacionPolitica)
.Include(r => r.AmbitoGeografico)
.Where(r => r.EleccionId == eleccionId && r.CategoriaId == categoriaId)
.ToListAsync();
if (!resultadosCrudos.Any())
{
return Ok(new PanelElectoralDto
{
AmbitoNombre = "Argentina",
MapaData = new List<ResultadoMapaDto>(),
ResultadosPanel = new List<AgrupacionResultadoDto>(),
EstadoRecuento = new EstadoRecuentoDto()
});
}
// 2. Calcular los resultados para el panel
var resultadosPanel = resultadosCrudos
.GroupBy(r => r.AgrupacionPolitica.Id)
.Select(g => new AgrupacionResultadoDto
{
// g.Key ahora es el ID de la agrupación
Id = g.Key,
// Tomamos los datos de la agrupación del primer elemento del grupo
Nombre = g.First().AgrupacionPolitica.Nombre,
NombreCorto = g.First().AgrupacionPolitica.NombreCorto,
Color = g.First().AgrupacionPolitica.Color,
Votos = g.Sum(r => r.CantidadVotos),
Porcentaje = (decimal)g.Sum(r => r.CantidadVotos) * 100 / resultadosCrudos.Sum(r => r.CantidadVotos)
})
.OrderByDescending(r => r.Votos)
.ToList();
// 3. Calcular los datos para el mapa (ganador por provincia/distrito)
var mapaData = resultadosCrudos
.Where(r => !string.IsNullOrEmpty(r.AmbitoGeografico.DistritoId))
.GroupBy(r => r.AmbitoGeografico.DistritoId)
.Select(g =>
{
var ganador = g.GroupBy(r => r.AgrupacionPoliticaId)
.Select(pg => new { Votos = pg.Sum(r => r.CantidadVotos), Agrupacion = pg.First().AgrupacionPolitica })
.OrderByDescending(x => x.Votos)
.First();
var provinciaAmbito = _dbContext.AmbitosGeograficos.AsNoTracking()
.FirstOrDefault(a => a.DistritoId == g.Key && a.NivelId == 10);
return new ResultadoMapaDto
{
AmbitoId = g.Key!,
AmbitoNombre = provinciaAmbito?.Nombre ?? "Desconocido", // <-- ENVIAMOS EL NOMBRE DE LA PROVINCIA
AgrupacionGanadoraId = ganador.Agrupacion.Id,
ColorGanador = ganador.Agrupacion.Color ?? "#808080"
};
})
.ToList();
// 4. Obtener el estado del recuento general
// Asumimos que existe un registro en EstadosRecuentosGenerales para el ámbito de la elección
var estadoRecuento = await _dbContext.EstadosRecuentosGenerales
.AsNoTracking()
.FirstOrDefaultAsync(e => e.EleccionId == eleccionId);
var respuesta = new PanelElectoralDto
{
AmbitoNombre = "Argentina",
MapaData = mapaData,
ResultadosPanel = resultadosPanel,
EstadoRecuento = new EstadoRecuentoDto()
};
return Ok(respuesta);
}
[HttpGet("mapa-resultados")]
public async Task<IActionResult> GetResultadosMapaPorMunicipio(
[FromRoute] int eleccionId,
[FromQuery] int categoriaId,
[FromQuery] string? distritoId = null)
{
if (string.IsNullOrEmpty(distritoId))
{
// --- VISTA NACIONAL (Ya corregida y funcionando) ---
var votosAgregadosPorProvincia = await _dbContext.ResultadosVotos
.AsNoTracking()
.Where(r => r.EleccionId == eleccionId
&& r.CategoriaId == categoriaId
&& r.AmbitoGeografico.NivelId == 30
&& r.AmbitoGeografico.DistritoId != null)
.GroupBy(r => new { r.AmbitoGeografico.DistritoId, r.AgrupacionPoliticaId })
.Select(g => new
{
g.Key.DistritoId,
g.Key.AgrupacionPoliticaId,
TotalVotos = g.Sum(r => r.CantidadVotos)
})
.ToListAsync();
var agrupacionesInfo = await _dbContext.AgrupacionesPoliticas.AsNoTracking().ToDictionaryAsync(a => a.Id);
var provinciasInfo = await _dbContext.AmbitosGeograficos.AsNoTracking().Where(a => a.NivelId == 10).ToListAsync();
var ganadoresPorProvincia = votosAgregadosPorProvincia
.GroupBy(r => r.DistritoId)
.Select(g => g.OrderByDescending(x => x.TotalVotos).First())
.ToList();
var mapaDataNacional = ganadoresPorProvincia.Select(g => new ResultadoMapaDto
{
AmbitoId = g.DistritoId!,
AmbitoNombre = provinciasInfo.FirstOrDefault(p => p.DistritoId == g.DistritoId)?.Nombre ?? "Desconocido",
AgrupacionGanadoraId = g.AgrupacionPoliticaId,
ColorGanador = agrupacionesInfo.GetValueOrDefault(g.AgrupacionPoliticaId)?.Color ?? "#808080"
}).ToList();
return Ok(mapaDataNacional);
}
else
{
// --- VISTA PROVINCIAL (AHORA CORREGIDA CON LA MISMA LÓGICA) ---
// PASO 1: Agrupar por IDs y sumar votos en la base de datos.
var votosAgregadosPorMunicipio = await _dbContext.ResultadosVotos
.AsNoTracking()
.Where(r => r.EleccionId == eleccionId
&& r.CategoriaId == categoriaId
&& r.AmbitoGeografico.DistritoId == distritoId
&& r.AmbitoGeografico.NivelId == 30)
// Agrupamos por los IDs (int y string)
.GroupBy(r => new { r.AmbitoGeograficoId, r.AgrupacionPoliticaId })
.Select(g => new
{
g.Key.AmbitoGeograficoId,
g.Key.AgrupacionPoliticaId,
TotalVotos = g.Sum(r => r.CantidadVotos)
})
.ToListAsync();
// PASO 2: Encontrar el ganador para cada municipio en memoria.
var ganadoresPorMunicipio = votosAgregadosPorMunicipio
.GroupBy(r => r.AmbitoGeograficoId)
.Select(g => g.OrderByDescending(x => x.TotalVotos).First())
.ToList();
// PASO 3: Hidratar con los nombres y colores (muy rápido).
var idsMunicipios = ganadoresPorMunicipio.Select(g => g.AmbitoGeograficoId).ToList();
var idsAgrupaciones = ganadoresPorMunicipio.Select(g => g.AgrupacionPoliticaId).ToList();
var municipiosInfo = await _dbContext.AmbitosGeograficos.AsNoTracking()
.Where(a => idsMunicipios.Contains(a.Id)).ToDictionaryAsync(a => a.Id);
var agrupacionesInfo = await _dbContext.AgrupacionesPoliticas.AsNoTracking()
.Where(a => idsAgrupaciones.Contains(a.Id)).ToDictionaryAsync(a => a.Id);
// Mapeo final a DTO.
var mapaDataProvincial = ganadoresPorMunicipio.Select(g => new ResultadoMapaDto
{
AmbitoId = g.AmbitoGeograficoId.ToString(),
AmbitoNombre = municipiosInfo.GetValueOrDefault(g.AmbitoGeograficoId)?.Nombre ?? "Desconocido",
AgrupacionGanadoraId = g.AgrupacionPoliticaId,
ColorGanador = agrupacionesInfo.GetValueOrDefault(g.AgrupacionPoliticaId)?.Color ?? "#808080"
}).ToList();
return Ok(mapaDataProvincial);
}
}
} }

View File

@@ -215,6 +215,116 @@ using (var scope = app.Services.CreateScope())
Console.WriteLine("--> Seeded default configurations."); Console.WriteLine("--> Seeded default configurations.");
} }
// --- SEEDER FINAL Y AUTOSUFICIENTE: Resultados Nacionales de Simulación para todo el país ---
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
var context = services.GetRequiredService<EleccionesDbContext>();
var logger = services.GetRequiredService<ILogger<Program>>();
const int eleccionNacionalId = 2;
if (!context.ResultadosVotos.Any(r => r.EleccionId == eleccionNacionalId))
{
logger.LogInformation("--> No se encontraron datos para la elección nacional ID {EleccionId}. Generando datos de simulación para TODO EL PAÍS...", eleccionNacionalId);
var eleccionNacional = await context.Elecciones.FindAsync(eleccionNacionalId) ?? new Eleccion { Id = eleccionNacionalId, Nombre = "Elecciones Nacionales 2025", Nivel = "Nacional", DistritoId = "00", Fecha = new DateOnly(2025, 10, 26) };
if (!context.Elecciones.Local.Any(e => e.Id == eleccionNacionalId)) context.Elecciones.Add(eleccionNacional);
var categoriaDiputadosNac = await context.CategoriasElectorales.FindAsync(2) ?? new CategoriaElectoral { Id = 2, Nombre = "DIPUTADOS NACIONALES", Orden = 3 };
if (!context.CategoriasElectorales.Local.Any(c => c.Id == 2)) context.CategoriasElectorales.Add(categoriaDiputadosNac);
await context.SaveChangesAsync();
var provinciasMaestras = new Dictionary<string, string>
{
{ "01", "CABA" }, { "02", "BUENOS AIRES" }, { "03", "CATAMARCA" }, { "04", "CORDOBA" },
{ "05", "CORRIENTES" },{ "06", "CHACO" }, { "07", "CHUBUT" }, { "08", "ENTRE RIOS" },
{ "09", "FORMOSA" }, { "10", "JUJUY" }, { "11", "LA PAMPA" }, { "12", "LA RIOJA" },
{ "13", "MENDOZA" }, { "14", "MISIONES" }, { "15", "NEUQUEN" }, { "16", "RIO NEGRO" },
{ "17", "SALTA" }, { "18", "SAN JUAN" }, { "19", "SAN LUIS" }, { "20", "SANTA CRUZ" },
{ "21", "SANTA FE" }, { "22", "SANTIAGO DEL ESTERO" }, { "23", "TIERRA DEL FUEGO" }, { "24", "TUCUMAN" }
};
foreach (var p in provinciasMaestras)
{
if (!await context.AmbitosGeograficos.AnyAsync(a => a.NivelId == 10 && a.DistritoId == p.Key))
{
context.AmbitosGeograficos.Add(new AmbitoGeografico { Nombre = p.Value, NivelId = 10, DistritoId = p.Key });
}
}
await context.SaveChangesAsync();
logger.LogInformation("--> Verificados/creados los 24 ámbitos geográficos de Nivel 10.");
// --- INICIO DE LA LÓGICA DE CREACIÓN DE MUNICIPIOS DE EJEMPLO ---
logger.LogInformation("--> Verificando existencia de municipios (Nivel 30) para cada provincia...");
var provinciasEnDb = await context.AmbitosGeograficos.AsNoTracking().Where(a => a.NivelId == 10).ToListAsync();
foreach (var provincia in provinciasEnDb)
{
bool existenMunicipios = await context.AmbitosGeograficos.AnyAsync(a => a.NivelId == 30 && a.DistritoId == provincia.DistritoId);
if (!existenMunicipios)
{
logger.LogWarning("--> No se encontraron municipios para {Provincia}. Creando 5 municipios de ejemplo...", provincia.Nombre);
for (int i = 1; i <= 5; i++)
{
context.AmbitosGeograficos.Add(new AmbitoGeografico
{
Nombre = $"{provincia.Nombre} - Municipio de Ejemplo {i}",
NivelId = 30,
DistritoId = provincia.DistritoId
});
}
}
}
await context.SaveChangesAsync();
// --- FIN DE LA LÓGICA DE CREACIÓN DE MUNICIPIOS DE EJEMPLO ---
var todosLosPartidos = await context.AgrupacionesPoliticas.Take(5).ToListAsync();
if (!todosLosPartidos.Any()) {
logger.LogWarning("--> No hay agrupaciones políticas en la BD. No se pueden generar votos de simulación.");
return;
}
var nuevosResultados = new List<ResultadoVoto>();
var rand = new Random();
foreach (var provincia in provinciasEnDb)
{
var municipiosDeProvincia = await context.AmbitosGeograficos.AsNoTracking()
.Where(a => a.NivelId == 30 && a.DistritoId == provincia.DistritoId).ToListAsync();
if (!municipiosDeProvincia.Any()) continue;
logger.LogInformation("--> Generando votos para {Count} municipios en {Provincia}...", municipiosDeProvincia.Count, provincia.Nombre);
int partidoIndex = rand.Next(todosLosPartidos.Count);
foreach (var municipio in municipiosDeProvincia)
{
var partidoGanador = todosLosPartidos[partidoIndex % todosLosPartidos.Count];
partidoIndex++;
nuevosResultados.Add(new ResultadoVoto {
EleccionId = eleccionNacional.Id, AmbitoGeograficoId = municipio.Id, CategoriaId = categoriaDiputadosNac.Id,
AgrupacionPoliticaId = partidoGanador.Id, CantidadVotos = rand.Next(25000, 70000)
});
var otrosPartidos = todosLosPartidos.Where(p => p.Id != partidoGanador.Id).OrderBy(p => rand.Next()).Take(rand.Next(3, 6));
foreach (var competidor in otrosPartidos) {
nuevosResultados.Add(new ResultadoVoto {
EleccionId = eleccionNacional.Id, AmbitoGeograficoId = municipio.Id, CategoriaId = categoriaDiputadosNac.Id,
AgrupacionPoliticaId = competidor.Id, CantidadVotos = rand.Next(1000, 24000)
});
}
}
}
if (nuevosResultados.Any()) {
await context.ResultadosVotos.AddRangeAsync(nuevosResultados);
await context.SaveChangesAsync();
logger.LogInformation("--> Se generaron {Count} registros de votos de simulación para todo el país.", nuevosResultados.Count);
} else {
logger.LogWarning("--> No se encontraron municipios en la BD para generar votos de simulación.");
}
}
}
// Configurar el pipeline de peticiones HTTP. // Configurar el pipeline de peticiones HTTP.
// Añadimos el logging de peticiones de Serilog aquí. // Añadimos el logging de peticiones de Serilog aquí.
app.UseSerilogRequestLogging(); app.UseSerilogRequestLogging();

View File

@@ -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+843c0f725893a48eae1236473cb5eeb00ef3d91c")] [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+64dc7ef440f585cb5e7723585b6327d5387f1b32")]
[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")]

View File

@@ -1 +1 @@
{"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["TyIJk/eQMWjmB5LsDE\u002BZIJC9P9ciVxd7bnzRiTZsGt4=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","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=","GelE32odx/vTului267wqi6zL3abBnF9yvwC2Q66LoM=","TEsXImnzxFKTIq2f5fiDu7i6Ar/cbecW5MZ3z8Wb/a4=","5WogJu\u002BUPlF\u002BE5mq/ILtDXpVwqwmhHtsEB13nmT5JJk=","dcHQRkttjMjo2dvhL7hA9t4Pg\u002B7OnjZpkFmakT4QR9U=","Of8nTYw5l\u002BgiAJo7z6XYIntG2tUtCFcILzHbTiiXn\u002Bw=","PDy\u002BTiayvNAoXXBEgwC/kCojpgOOMI6RQOIoSXs3LJc=","ePXrkee3hv3wHUr8S7aYmRVvXUTxQf76zApKGv3/l3o=","DXx5dQywLo3UsY2zQaUG\u002BbW4ObiYbybxPBWxeJD2bhk=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","/f\u002B\u002BdIRysg7dipW05N4RtxXuPBXZZIhhi3aMiCZ\u002BB2w="],"CachedAssets":{},"CachedCopyCandidates":{}} {"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["TyIJk/eQMWjmB5LsDE\u002BZIJC9P9ciVxd7bnzRiTZsGt4=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","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=","GelE32odx/vTului267wqi6zL3abBnF9yvwC2Q66LoM=","TEsXImnzxFKTIq2f5fiDu7i6Ar/cbecW5MZ3z8Wb/a4=","5WogJu\u002BUPlF\u002BE5mq/ILtDXpVwqwmhHtsEB13nmT5JJk=","dcHQRkttjMjo2dvhL7hA9t4Pg\u002B7OnjZpkFmakT4QR9U=","Of8nTYw5l\u002BgiAJo7z6XYIntG2tUtCFcILzHbTiiXn\u002Bw=","PDy\u002BTiayvNAoXXBEgwC/kCojpgOOMI6RQOIoSXs3LJc=","ePXrkee3hv3wHUr8S7aYmRVvXUTxQf76zApKGv3/l3o=","DXx5dQywLo3UsY2zQaUG\u002BbW4ObiYbybxPBWxeJD2bhk=","muVh5sjH3sgdvuz4TbuTwTggX1uDnsWXgoosMKST/r4=","nrP5gSIA5vzgp8v12CAOr943QYLxU4Til6oiCcWSNI8=","yMd45U9BK07I3b3fBQ627PWTYyZ2ZjrmFc5VD\u002BQVx1Q=","xKskvcoJU0RVRN1a5dRqKRM7IP5vmmbraUaPFYjhnCc=","p7BjQw7aSZjfOCqmKm7/kPO9qegEQZBfirMjlOx/I1I=","xf7fvi/A0tUBZMxhPJDER8vuJQH2E3gyAnDWXXhOyD0=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","sfQbd729CZjIZCr20t2H\u002BLd7hI/GEf6\u002BdyvRnmo8MpU="],"CachedAssets":{},"CachedCopyCandidates":{}}

View File

@@ -1 +1 @@
{"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["TyIJk/eQMWjmB5LsDE\u002BZIJC9P9ciVxd7bnzRiTZsGt4=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","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=","GelE32odx/vTului267wqi6zL3abBnF9yvwC2Q66LoM=","TEsXImnzxFKTIq2f5fiDu7i6Ar/cbecW5MZ3z8Wb/a4=","5WogJu\u002BUPlF\u002BE5mq/ILtDXpVwqwmhHtsEB13nmT5JJk=","dcHQRkttjMjo2dvhL7hA9t4Pg\u002B7OnjZpkFmakT4QR9U=","Of8nTYw5l\u002BgiAJo7z6XYIntG2tUtCFcILzHbTiiXn\u002Bw=","PDy\u002BTiayvNAoXXBEgwC/kCojpgOOMI6RQOIoSXs3LJc=","ePXrkee3hv3wHUr8S7aYmRVvXUTxQf76zApKGv3/l3o=","DXx5dQywLo3UsY2zQaUG\u002BbW4ObiYbybxPBWxeJD2bhk=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","/f\u002B\u002BdIRysg7dipW05N4RtxXuPBXZZIhhi3aMiCZ\u002BB2w="],"CachedAssets":{},"CachedCopyCandidates":{}} {"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["TyIJk/eQMWjmB5LsDE\u002BZIJC9P9ciVxd7bnzRiTZsGt4=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","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=","GelE32odx/vTului267wqi6zL3abBnF9yvwC2Q66LoM=","TEsXImnzxFKTIq2f5fiDu7i6Ar/cbecW5MZ3z8Wb/a4=","5WogJu\u002BUPlF\u002BE5mq/ILtDXpVwqwmhHtsEB13nmT5JJk=","dcHQRkttjMjo2dvhL7hA9t4Pg\u002B7OnjZpkFmakT4QR9U=","Of8nTYw5l\u002BgiAJo7z6XYIntG2tUtCFcILzHbTiiXn\u002Bw=","PDy\u002BTiayvNAoXXBEgwC/kCojpgOOMI6RQOIoSXs3LJc=","ePXrkee3hv3wHUr8S7aYmRVvXUTxQf76zApKGv3/l3o=","DXx5dQywLo3UsY2zQaUG\u002BbW4ObiYbybxPBWxeJD2bhk=","muVh5sjH3sgdvuz4TbuTwTggX1uDnsWXgoosMKST/r4=","nrP5gSIA5vzgp8v12CAOr943QYLxU4Til6oiCcWSNI8=","yMd45U9BK07I3b3fBQ627PWTYyZ2ZjrmFc5VD\u002BQVx1Q=","xKskvcoJU0RVRN1a5dRqKRM7IP5vmmbraUaPFYjhnCc=","p7BjQw7aSZjfOCqmKm7/kPO9qegEQZBfirMjlOx/I1I=","xf7fvi/A0tUBZMxhPJDER8vuJQH2E3gyAnDWXXhOyD0=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","sfQbd729CZjIZCr20t2H\u002BLd7hI/GEf6\u002BdyvRnmo8MpU="],"CachedAssets":{},"CachedCopyCandidates":{}}

View File

@@ -0,0 +1,10 @@
using Elecciones.Core.DTOs;
using Elecciones.Core.DTOs.ApiResponses;
public class PanelElectoralDto
{
public required string AmbitoNombre { get; set; }
public required List<ResultadoMapaDto> MapaData { get; set; }
public required List<AgrupacionResultadoDto> ResultadosPanel { get; set; }
public required EstadoRecuentoDto EstadoRecuento { get; set; }
}

View File

@@ -0,0 +1,7 @@
public class ResultadoMapaDto
{
public required string AmbitoId { get; set; } // ID de la provincia o municipio
public required string AmbitoNombre { get; set; }
public required string AgrupacionGanadoraId { get; set; }
public required string ColorGanador { get; set; }
}

View File

@@ -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+843c0f725893a48eae1236473cb5eeb00ef3d91c")] [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+64dc7ef440f585cb5e7723585b6327d5387f1b32")]
[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")]

View File

@@ -21,6 +21,7 @@ public class EleccionesDbContext(DbContextOptions<EleccionesDbContext> options)
public DbSet<OcupanteBanca> OcupantesBancas { get; set; } public DbSet<OcupanteBanca> OcupantesBancas { get; set; }
public DbSet<LogoAgrupacionCategoria> LogosAgrupacionesCategorias { get; set; } public DbSet<LogoAgrupacionCategoria> LogosAgrupacionesCategorias { get; set; }
public DbSet<CandidatoOverride> CandidatosOverrides { get; set; } public DbSet<CandidatoOverride> CandidatosOverrides { get; set; }
public DbSet<Eleccion> Elecciones { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {

View File

@@ -23,4 +23,6 @@ public class Bancada
// Relación uno a uno con OcupanteBanca // Relación uno a uno con OcupanteBanca
public OcupanteBanca? Ocupante { get; set; } public OcupanteBanca? Ocupante { get; set; }
public int EleccionId { get; set; }
} }

View File

@@ -31,4 +31,5 @@ public class CandidatoOverride
[Required] [Required]
[MaxLength(255)] [MaxLength(255)]
public string NombreCandidato { get; set; } = null!; public string NombreCandidato { get; set; } = null!;
public int EleccionId { get; set; }
} }

View File

@@ -1,3 +1,4 @@
// src/Elecciones.Database/Entities/CategoriaElectoral.cs
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;

View File

@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
public class Eleccion
{
[Key]
public int Id { get; set; }
[Required]
public string Nombre { get; set; } = null!; // Ej: "Elecciones Provinciales BA 2025"
[Required]
public string Nivel { get; set; } = null!; // Ej: "Provincial" o "Nacional"
[Required]
public string DistritoId { get; set; } = null!; // Ej: "02" para PBA, "00" para Nacional
public DateOnly Fecha { get; set; }
}

View File

@@ -24,4 +24,5 @@ public class EstadoRecuento
public decimal VotosNulosPorcentaje { get; set; } public decimal VotosNulosPorcentaje { get; set; }
public decimal VotosEnBlancoPorcentaje { get; set; } public decimal VotosEnBlancoPorcentaje { get; set; }
public decimal VotosRecurridosPorcentaje { get; set; } public decimal VotosRecurridosPorcentaje { get; set; }
public int EleccionId { get; set; }
} }

View File

@@ -16,7 +16,9 @@ public class EstadoRecuentoGeneral
public int CantidadVotantes { get; set; } public int CantidadVotantes { get; set; }
public decimal ParticipacionPorcentaje { get; set; } public decimal ParticipacionPorcentaje { get; set; }
// --- Propiedades de navegación (Opcional pero recomendado) --- // --- Propiedad de navegación (Opcional pero recomendado) ---
[ForeignKey("CategoriaId")] [ForeignKey("CategoriaId")]
public CategoriaElectoral CategoriaElectoral { get; set; } = null!; public CategoriaElectoral CategoriaElectoral { get; set; } = null!;
public int EleccionId { get; set; }
} }

View File

@@ -13,4 +13,5 @@ public class LogoAgrupacionCategoria
public int CategoriaId { get; set; } public int CategoriaId { get; set; }
public string? LogoUrl { get; set; } public string? LogoUrl { get; set; }
public int? AmbitoGeograficoId { get; set; } public int? AmbitoGeograficoId { get; set; }
public int EleccionId { get; set; }
} }

View File

@@ -21,4 +21,6 @@ public class OcupanteBanca
public string? FotoUrl { get; set; } public string? FotoUrl { get; set; }
public string? Periodo { get; set; } public string? Periodo { get; set; }
public int EleccionId { get; set; }
} }

View File

@@ -24,4 +24,5 @@ public class ProyeccionBanca
// Cantidad de bancas obtenidas // Cantidad de bancas obtenidas
public int NroBancas { get; set; } public int NroBancas { get; set; }
public DateTime FechaTotalizacion { get; set; } public DateTime FechaTotalizacion { get; set; }
public int EleccionId { get; set; }
} }

View File

@@ -24,4 +24,6 @@ public class ResultadoVoto
public long CantidadVotos { get; set; } public long CantidadVotos { get; set; }
public decimal PorcentajeVotos { get; set; } public decimal PorcentajeVotos { get; set; }
public int EleccionId { get; set; }
} }

View File

@@ -24,4 +24,6 @@ public class ResumenVoto
[Required] [Required]
public decimal VotosPorcentaje { get; set; } public decimal VotosPorcentaje { get; set; }
public int EleccionId { get; set; }
} }

View File

@@ -18,4 +18,6 @@ public class Telegrama
public DateTime FechaEscaneo { get; set; } public DateTime FechaEscaneo { get; set; }
public DateTime FechaTotalizacion { get; set; } public DateTime FechaTotalizacion { get; set; }
public int EleccionId { get; set; }
} }

View File

@@ -0,0 +1,675 @@
// <auto-generated />
using System;
using Elecciones.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Elecciones.Database.Migrations
{
[DbContext(typeof(EleccionesDbContext))]
[Migration("20250911152547_AddEleccionEntities")]
partial class AddEleccionEntities
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.UseCollation("Modern_Spanish_CI_AS")
.HasAnnotation("ProductVersion", "9.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Eleccion", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("DistritoId")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateOnly>("Fecha")
.HasColumnType("date");
b.Property<string>("Nivel")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Nombre")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Elecciones");
});
modelBuilder.Entity("Elecciones.Database.Entities.AdminUser", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("PasswordSalt")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.HasKey("Id");
b.ToTable("AdminUsers");
});
modelBuilder.Entity("Elecciones.Database.Entities.AgrupacionPolitica", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<string>("Color")
.HasColumnType("nvarchar(max)");
b.Property<string>("IdTelegrama")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Nombre")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("NombreCorto")
.HasColumnType("nvarchar(max)");
b.Property<int?>("OrdenDiputados")
.HasColumnType("int");
b.Property<int?>("OrdenSenadores")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("AgrupacionesPoliticas");
});
modelBuilder.Entity("Elecciones.Database.Entities.AmbitoGeografico", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("CircuitoId")
.HasColumnType("nvarchar(max)");
b.Property<string>("DistritoId")
.HasColumnType("nvarchar(max)");
b.Property<string>("EstablecimientoId")
.HasColumnType("nvarchar(max)");
b.Property<string>("MesaId")
.HasColumnType("nvarchar(max)");
b.Property<string>("MunicipioId")
.HasColumnType("nvarchar(max)");
b.Property<int>("NivelId")
.HasColumnType("int");
b.Property<string>("Nombre")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("SeccionId")
.HasColumnType("nvarchar(max)");
b.Property<string>("SeccionProvincialId")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("AmbitosGeograficos");
});
modelBuilder.Entity("Elecciones.Database.Entities.Bancada", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("AgrupacionPoliticaId")
.HasColumnType("nvarchar(450)");
b.Property<int>("Camara")
.HasColumnType("int");
b.Property<int>("EleccionId")
.HasColumnType("int");
b.Property<int>("NumeroBanca")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("AgrupacionPoliticaId");
b.ToTable("Bancadas");
});
modelBuilder.Entity("Elecciones.Database.Entities.CandidatoOverride", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("AgrupacionPoliticaId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<int?>("AmbitoGeograficoId")
.HasColumnType("int");
b.Property<int>("CategoriaId")
.HasColumnType("int");
b.Property<int>("EleccionId")
.HasColumnType("int");
b.Property<string>("NombreCandidato")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.HasKey("Id");
b.HasIndex("AmbitoGeograficoId");
b.HasIndex("CategoriaId");
b.HasIndex("AgrupacionPoliticaId", "CategoriaId", "AmbitoGeograficoId")
.IsUnique()
.HasFilter("[AmbitoGeograficoId] IS NOT NULL");
b.ToTable("CandidatosOverrides");
});
modelBuilder.Entity("Elecciones.Database.Entities.CategoriaElectoral", b =>
{
b.Property<int>("Id")
.HasColumnType("int");
b.Property<string>("Nombre")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("Orden")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("CategoriasElectorales");
});
modelBuilder.Entity("Elecciones.Database.Entities.Configuracion", b =>
{
b.Property<string>("Clave")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Valor")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.HasKey("Clave");
b.ToTable("Configuraciones");
});
modelBuilder.Entity("Elecciones.Database.Entities.EstadoRecuento", b =>
{
b.Property<int>("AmbitoGeograficoId")
.HasColumnType("int");
b.Property<int>("CategoriaId")
.HasColumnType("int");
b.Property<int>("CantidadElectores")
.HasColumnType("int");
b.Property<int>("CantidadVotantes")
.HasColumnType("int");
b.Property<int>("EleccionId")
.HasColumnType("int");
b.Property<DateTime>("FechaTotalizacion")
.HasColumnType("datetime2");
b.Property<int>("MesasEsperadas")
.HasColumnType("int");
b.Property<int>("MesasTotalizadas")
.HasColumnType("int");
b.Property<decimal>("MesasTotalizadasPorcentaje")
.HasPrecision(5, 2)
.HasColumnType("decimal(5,2)");
b.Property<decimal>("ParticipacionPorcentaje")
.HasPrecision(5, 2)
.HasColumnType("decimal(5,2)");
b.Property<long>("VotosEnBlanco")
.HasColumnType("bigint");
b.Property<decimal>("VotosEnBlancoPorcentaje")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<long>("VotosNulos")
.HasColumnType("bigint");
b.Property<decimal>("VotosNulosPorcentaje")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<long>("VotosRecurridos")
.HasColumnType("bigint");
b.Property<decimal>("VotosRecurridosPorcentaje")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.HasKey("AmbitoGeograficoId", "CategoriaId");
b.ToTable("EstadosRecuentos");
});
modelBuilder.Entity("Elecciones.Database.Entities.EstadoRecuentoGeneral", b =>
{
b.Property<int>("AmbitoGeograficoId")
.HasColumnType("int");
b.Property<int>("CategoriaId")
.HasColumnType("int");
b.Property<int>("CantidadElectores")
.HasColumnType("int");
b.Property<int>("CantidadVotantes")
.HasColumnType("int");
b.Property<int>("EleccionId")
.HasColumnType("int");
b.Property<DateTime>("FechaTotalizacion")
.HasColumnType("datetime2");
b.Property<int>("MesasEsperadas")
.HasColumnType("int");
b.Property<int>("MesasTotalizadas")
.HasColumnType("int");
b.Property<decimal>("MesasTotalizadasPorcentaje")
.HasPrecision(5, 2)
.HasColumnType("decimal(5,2)");
b.Property<decimal>("ParticipacionPorcentaje")
.HasPrecision(5, 2)
.HasColumnType("decimal(5,2)");
b.HasKey("AmbitoGeograficoId", "CategoriaId");
b.HasIndex("CategoriaId");
b.ToTable("EstadosRecuentosGenerales");
});
modelBuilder.Entity("Elecciones.Database.Entities.LogoAgrupacionCategoria", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("AgrupacionPoliticaId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<int?>("AmbitoGeograficoId")
.HasColumnType("int");
b.Property<int>("CategoriaId")
.HasColumnType("int");
b.Property<int>("EleccionId")
.HasColumnType("int");
b.Property<string>("LogoUrl")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("AgrupacionPoliticaId", "CategoriaId", "AmbitoGeograficoId")
.IsUnique()
.HasFilter("[AmbitoGeograficoId] IS NOT NULL");
b.ToTable("LogosAgrupacionesCategorias");
});
modelBuilder.Entity("Elecciones.Database.Entities.OcupanteBanca", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("BancadaId")
.HasColumnType("int");
b.Property<int>("EleccionId")
.HasColumnType("int");
b.Property<string>("FotoUrl")
.HasColumnType("nvarchar(max)");
b.Property<string>("NombreOcupante")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Periodo")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("BancadaId")
.IsUnique();
b.ToTable("OcupantesBancas");
});
modelBuilder.Entity("Elecciones.Database.Entities.ProyeccionBanca", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("AgrupacionPoliticaId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<int>("AmbitoGeograficoId")
.HasColumnType("int");
b.Property<int>("CategoriaId")
.HasColumnType("int");
b.Property<int>("EleccionId")
.HasColumnType("int");
b.Property<DateTime>("FechaTotalizacion")
.HasColumnType("datetime2");
b.Property<int>("NroBancas")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("AgrupacionPoliticaId");
b.HasIndex("AmbitoGeograficoId", "CategoriaId", "AgrupacionPoliticaId")
.IsUnique();
b.ToTable("ProyeccionesBancas");
});
modelBuilder.Entity("Elecciones.Database.Entities.ResultadoVoto", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<string>("AgrupacionPoliticaId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<int>("AmbitoGeograficoId")
.HasColumnType("int");
b.Property<long>("CantidadVotos")
.HasColumnType("bigint");
b.Property<int>("CategoriaId")
.HasColumnType("int");
b.Property<int>("EleccionId")
.HasColumnType("int");
b.Property<decimal>("PorcentajeVotos")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.HasKey("Id");
b.HasIndex("AgrupacionPoliticaId");
b.HasIndex("AmbitoGeograficoId", "CategoriaId", "AgrupacionPoliticaId")
.IsUnique();
b.ToTable("ResultadosVotos");
});
modelBuilder.Entity("Elecciones.Database.Entities.ResumenVoto", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("AgrupacionPoliticaId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<int>("AmbitoGeograficoId")
.HasColumnType("int");
b.Property<int>("EleccionId")
.HasColumnType("int");
b.Property<long>("Votos")
.HasColumnType("bigint");
b.Property<decimal>("VotosPorcentaje")
.HasPrecision(5, 2)
.HasColumnType("decimal(5,2)");
b.HasKey("Id");
b.HasIndex("AgrupacionPoliticaId");
b.ToTable("ResumenesVotos");
});
modelBuilder.Entity("Elecciones.Database.Entities.Telegrama", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<int>("AmbitoGeograficoId")
.HasColumnType("int");
b.Property<string>("ContenidoBase64")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("EleccionId")
.HasColumnType("int");
b.Property<DateTime>("FechaEscaneo")
.HasColumnType("datetime2");
b.Property<DateTime>("FechaTotalizacion")
.HasColumnType("datetime2");
b.HasKey("Id");
b.ToTable("Telegramas");
});
modelBuilder.Entity("Elecciones.Database.Entities.Bancada", b =>
{
b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica")
.WithMany()
.HasForeignKey("AgrupacionPoliticaId");
b.Navigation("AgrupacionPolitica");
});
modelBuilder.Entity("Elecciones.Database.Entities.CandidatoOverride", b =>
{
b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica")
.WithMany()
.HasForeignKey("AgrupacionPoliticaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Elecciones.Database.Entities.AmbitoGeografico", "AmbitoGeografico")
.WithMany()
.HasForeignKey("AmbitoGeograficoId");
b.HasOne("Elecciones.Database.Entities.CategoriaElectoral", "CategoriaElectoral")
.WithMany()
.HasForeignKey("CategoriaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AgrupacionPolitica");
b.Navigation("AmbitoGeografico");
b.Navigation("CategoriaElectoral");
});
modelBuilder.Entity("Elecciones.Database.Entities.EstadoRecuento", b =>
{
b.HasOne("Elecciones.Database.Entities.AmbitoGeografico", "AmbitoGeografico")
.WithMany()
.HasForeignKey("AmbitoGeograficoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AmbitoGeografico");
});
modelBuilder.Entity("Elecciones.Database.Entities.EstadoRecuentoGeneral", b =>
{
b.HasOne("Elecciones.Database.Entities.CategoriaElectoral", "CategoriaElectoral")
.WithMany()
.HasForeignKey("CategoriaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("CategoriaElectoral");
});
modelBuilder.Entity("Elecciones.Database.Entities.OcupanteBanca", b =>
{
b.HasOne("Elecciones.Database.Entities.Bancada", "Bancada")
.WithOne("Ocupante")
.HasForeignKey("Elecciones.Database.Entities.OcupanteBanca", "BancadaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Bancada");
});
modelBuilder.Entity("Elecciones.Database.Entities.ProyeccionBanca", b =>
{
b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica")
.WithMany()
.HasForeignKey("AgrupacionPoliticaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Elecciones.Database.Entities.AmbitoGeografico", "AmbitoGeografico")
.WithMany()
.HasForeignKey("AmbitoGeograficoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AgrupacionPolitica");
b.Navigation("AmbitoGeografico");
});
modelBuilder.Entity("Elecciones.Database.Entities.ResultadoVoto", b =>
{
b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica")
.WithMany()
.HasForeignKey("AgrupacionPoliticaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Elecciones.Database.Entities.AmbitoGeografico", "AmbitoGeografico")
.WithMany()
.HasForeignKey("AmbitoGeograficoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AgrupacionPolitica");
b.Navigation("AmbitoGeografico");
});
modelBuilder.Entity("Elecciones.Database.Entities.ResumenVoto", b =>
{
b.HasOne("Elecciones.Database.Entities.AgrupacionPolitica", "AgrupacionPolitica")
.WithMany()
.HasForeignKey("AgrupacionPoliticaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AgrupacionPolitica");
});
modelBuilder.Entity("Elecciones.Database.Entities.Bancada", b =>
{
b.Navigation("Ocupante");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,148 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Elecciones.Database.Migrations
{
/// <inheritdoc />
public partial class AddEleccionEntities : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "EleccionId",
table: "Telegramas",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "EleccionId",
table: "ResumenesVotos",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "EleccionId",
table: "ResultadosVotos",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "EleccionId",
table: "ProyeccionesBancas",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "EleccionId",
table: "OcupantesBancas",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "EleccionId",
table: "LogosAgrupacionesCategorias",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "EleccionId",
table: "EstadosRecuentosGenerales",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "EleccionId",
table: "EstadosRecuentos",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "EleccionId",
table: "CandidatosOverrides",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "EleccionId",
table: "Bancadas",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateTable(
name: "Elecciones",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Nombre = table.Column<string>(type: "nvarchar(max)", nullable: false),
Nivel = table.Column<string>(type: "nvarchar(max)", nullable: false),
DistritoId = table.Column<string>(type: "nvarchar(max)", nullable: false),
Fecha = table.Column<DateOnly>(type: "date", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Elecciones", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Elecciones");
migrationBuilder.DropColumn(
name: "EleccionId",
table: "Telegramas");
migrationBuilder.DropColumn(
name: "EleccionId",
table: "ResumenesVotos");
migrationBuilder.DropColumn(
name: "EleccionId",
table: "ResultadosVotos");
migrationBuilder.DropColumn(
name: "EleccionId",
table: "ProyeccionesBancas");
migrationBuilder.DropColumn(
name: "EleccionId",
table: "OcupantesBancas");
migrationBuilder.DropColumn(
name: "EleccionId",
table: "LogosAgrupacionesCategorias");
migrationBuilder.DropColumn(
name: "EleccionId",
table: "EstadosRecuentosGenerales");
migrationBuilder.DropColumn(
name: "EleccionId",
table: "EstadosRecuentos");
migrationBuilder.DropColumn(
name: "EleccionId",
table: "CandidatosOverrides");
migrationBuilder.DropColumn(
name: "EleccionId",
table: "Bancadas");
}
}
}

View File

@@ -23,6 +23,34 @@ namespace Elecciones.Database.Migrations
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Eleccion", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("DistritoId")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateOnly>("Fecha")
.HasColumnType("date");
b.Property<string>("Nivel")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Nombre")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Elecciones");
});
modelBuilder.Entity("Elecciones.Database.Entities.AdminUser", b => modelBuilder.Entity("Elecciones.Database.Entities.AdminUser", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -134,6 +162,9 @@ namespace Elecciones.Database.Migrations
b.Property<int>("Camara") b.Property<int>("Camara")
.HasColumnType("int"); .HasColumnType("int");
b.Property<int>("EleccionId")
.HasColumnType("int");
b.Property<int>("NumeroBanca") b.Property<int>("NumeroBanca")
.HasColumnType("int"); .HasColumnType("int");
@@ -162,6 +193,9 @@ namespace Elecciones.Database.Migrations
b.Property<int>("CategoriaId") b.Property<int>("CategoriaId")
.HasColumnType("int"); .HasColumnType("int");
b.Property<int>("EleccionId")
.HasColumnType("int");
b.Property<string>("NombreCandidato") b.Property<string>("NombreCandidato")
.IsRequired() .IsRequired()
.HasMaxLength(255) .HasMaxLength(255)
@@ -227,6 +261,9 @@ namespace Elecciones.Database.Migrations
b.Property<int>("CantidadVotantes") b.Property<int>("CantidadVotantes")
.HasColumnType("int"); .HasColumnType("int");
b.Property<int>("EleccionId")
.HasColumnType("int");
b.Property<DateTime>("FechaTotalizacion") b.Property<DateTime>("FechaTotalizacion")
.HasColumnType("datetime2"); .HasColumnType("datetime2");
@@ -284,6 +321,9 @@ namespace Elecciones.Database.Migrations
b.Property<int>("CantidadVotantes") b.Property<int>("CantidadVotantes")
.HasColumnType("int"); .HasColumnType("int");
b.Property<int>("EleccionId")
.HasColumnType("int");
b.Property<DateTime>("FechaTotalizacion") b.Property<DateTime>("FechaTotalizacion")
.HasColumnType("datetime2"); .HasColumnType("datetime2");
@@ -326,6 +366,9 @@ namespace Elecciones.Database.Migrations
b.Property<int>("CategoriaId") b.Property<int>("CategoriaId")
.HasColumnType("int"); .HasColumnType("int");
b.Property<int>("EleccionId")
.HasColumnType("int");
b.Property<string>("LogoUrl") b.Property<string>("LogoUrl")
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
@@ -349,6 +392,9 @@ namespace Elecciones.Database.Migrations
b.Property<int>("BancadaId") b.Property<int>("BancadaId")
.HasColumnType("int"); .HasColumnType("int");
b.Property<int>("EleccionId")
.HasColumnType("int");
b.Property<string>("FotoUrl") b.Property<string>("FotoUrl")
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
@@ -385,6 +431,9 @@ namespace Elecciones.Database.Migrations
b.Property<int>("CategoriaId") b.Property<int>("CategoriaId")
.HasColumnType("int"); .HasColumnType("int");
b.Property<int>("EleccionId")
.HasColumnType("int");
b.Property<DateTime>("FechaTotalizacion") b.Property<DateTime>("FechaTotalizacion")
.HasColumnType("datetime2"); .HasColumnType("datetime2");
@@ -422,6 +471,9 @@ namespace Elecciones.Database.Migrations
b.Property<int>("CategoriaId") b.Property<int>("CategoriaId")
.HasColumnType("int"); .HasColumnType("int");
b.Property<int>("EleccionId")
.HasColumnType("int");
b.Property<decimal>("PorcentajeVotos") b.Property<decimal>("PorcentajeVotos")
.HasPrecision(18, 4) .HasPrecision(18, 4)
.HasColumnType("decimal(18,4)"); .HasColumnType("decimal(18,4)");
@@ -451,6 +503,9 @@ namespace Elecciones.Database.Migrations
b.Property<int>("AmbitoGeograficoId") b.Property<int>("AmbitoGeograficoId")
.HasColumnType("int"); .HasColumnType("int");
b.Property<int>("EleccionId")
.HasColumnType("int");
b.Property<long>("Votos") b.Property<long>("Votos")
.HasColumnType("bigint"); .HasColumnType("bigint");
@@ -477,6 +532,9 @@ namespace Elecciones.Database.Migrations
.IsRequired() .IsRequired()
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
b.Property<int>("EleccionId")
.HasColumnType("int");
b.Property<DateTime>("FechaEscaneo") b.Property<DateTime>("FechaEscaneo")
.HasColumnType("datetime2"); .HasColumnType("datetime2");

View File

@@ -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+843c0f725893a48eae1236473cb5eeb00ef3d91c")] [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+64dc7ef440f585cb5e7723585b6327d5387f1b32")]
[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")]

View File

@@ -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+843c0f725893a48eae1236473cb5eeb00ef3d91c")] [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+64dc7ef440f585cb5e7723585b6327d5387f1b32")]
[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")]

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="GetEFProjectMetadata">
<MSBuild Condition=" '$(TargetFramework)' == '' "
Projects="$(MSBuildProjectFile)"
Targets="GetEFProjectMetadata"
Properties="TargetFramework=$(TargetFrameworks.Split(';')[0]);EFProjectMetadataFile=$(EFProjectMetadataFile)" />
<ItemGroup Condition=" '$(TargetFramework)' != '' ">
<EFProjectMetadata Include="AssemblyName: $(AssemblyName)" />
<EFProjectMetadata Include="Language: $(Language)" />
<EFProjectMetadata Include="OutputPath: $(OutputPath)" />
<EFProjectMetadata Include="Platform: $(Platform)" />
<EFProjectMetadata Include="PlatformTarget: $(PlatformTarget)" />
<EFProjectMetadata Include="ProjectAssetsFile: $(ProjectAssetsFile)" />
<EFProjectMetadata Include="ProjectDir: $(ProjectDir)" />
<EFProjectMetadata Include="RootNamespace: $(RootNamespace)" />
<EFProjectMetadata Include="RuntimeFrameworkVersion: $(RuntimeFrameworkVersion)" />
<EFProjectMetadata Include="TargetFileName: $(TargetFileName)" />
<EFProjectMetadata Include="TargetFrameworkMoniker: $(TargetFrameworkMoniker)" />
<EFProjectMetadata Include="Nullable: $(Nullable)" />
<EFProjectMetadata Include="TargetFramework: $(TargetFramework)" />
<EFProjectMetadata Include="TargetPlatformIdentifier: $(TargetPlatformIdentifier)" />
</ItemGroup>
<WriteLinesToFile Condition=" '$(TargetFramework)' != '' "
File="$(EFProjectMetadataFile)"
Lines="@(EFProjectMetadata)" />
</Target>
</Project>