diff --git a/Elecciones-Web/frontend-admin/Dockerfile b/Elecciones-Web/frontend-admin/Dockerfile
new file mode 100644
index 0000000..ee4d327
--- /dev/null
+++ b/Elecciones-Web/frontend-admin/Dockerfile
@@ -0,0 +1,18 @@
+# frontend-admin/Dockerfile
+
+# --- Etapa 1: Build (Construcción) ---
+FROM node:20-alpine AS build
+WORKDIR /app
+COPY package*.json ./
+RUN npm install
+COPY . .
+RUN npm run build
+
+# --- Etapa 2: Serve (Servir con Nginx configurado para SPA) ---
+FROM nginx:1.25-alpine
+COPY --from=build /app/dist /usr/share/nginx/html
+
+# Copiamos nuestra configuración de Nginx para manejar el enrutamiento de React
+COPY frontend.nginx.conf /etc/nginx/conf.d/default.conf
+
+EXPOSE 80
\ No newline at end of file
diff --git a/Elecciones-Web/frontend-admin/frontend.nginx.conf b/Elecciones-Web/frontend-admin/frontend.nginx.conf
new file mode 100644
index 0000000..d1f6335
--- /dev/null
+++ b/Elecciones-Web/frontend-admin/frontend.nginx.conf
@@ -0,0 +1,18 @@
+# frontend-admin/frontend.nginx.conf
+
+server {
+ listen 80;
+ server_name localhost;
+
+ root /usr/share/nginx/html;
+ index index.html;
+
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ location ~* \.(?:css|js|jpg|jpeg|gif|png|ico|svg|woff|woff2)$ {
+ expires 1y;
+ add_header Cache-Control "public";
+ }
+}
\ No newline at end of file
diff --git a/Elecciones-Web/frontend-admin/index.html b/Elecciones-Web/frontend-admin/index.html
index e4b78ea..29721e9 100644
--- a/Elecciones-Web/frontend-admin/index.html
+++ b/Elecciones-Web/frontend-admin/index.html
@@ -2,9 +2,9 @@
-
+
- Vite + React + TS
+ Elecciones 2025 - El Día
diff --git a/Elecciones-Web/frontend-admin/public/eldia.svg b/Elecciones-Web/frontend-admin/public/eldia.svg
new file mode 100644
index 0000000..91ce072
--- /dev/null
+++ b/Elecciones-Web/frontend-admin/public/eldia.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/Elecciones-Web/frontend/index.html b/Elecciones-Web/frontend/index.html
index e4b78ea..b8d4c21 100644
--- a/Elecciones-Web/frontend/index.html
+++ b/Elecciones-Web/frontend/index.html
@@ -2,12 +2,12 @@
-
+
- Vite + React + TS
+ Elecciones 2025 - Dev Showcase
-
+
\ No newline at end of file
diff --git a/Elecciones-Web/frontend/public/bootstrap.js b/Elecciones-Web/frontend/public/bootstrap.js
new file mode 100644
index 0000000..73609b1
--- /dev/null
+++ b/Elecciones-Web/frontend/public/bootstrap.js
@@ -0,0 +1,71 @@
+// public/bootstrap.js
+(function() {
+ // --- CONFIGURACIÓN ---
+ // Cambie esto por el dominio final en producción
+ const WIDGETS_HOST = 'https://elecciones2025.eldia.com';
+
+ // --- FUNCIONES AUXILIARES (sin cambios) ---
+ function loadScript(src) {
+ return new Promise((resolve, reject) => {
+ const script = document.createElement('script');
+ script.type = 'module';
+ script.src = src;
+ script.onload = resolve;
+ script.onerror = reject;
+ document.head.appendChild(script);
+ });
+ }
+
+ function loadCSS(href) {
+ const link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.href = href;
+ document.head.appendChild(link);
+ }
+
+ // --- LÓGICA PRINCIPAL ---
+ async function initWidgets() {
+ try {
+ // 1. Obtener el manifest.json
+ const response = await fetch(`${WIDGETS_HOST}/manifest.json`);
+ if (!response.ok) throw new Error('No se pudo cargar el manifest.');
+ const manifest = await response.json();
+
+ // 2. Encontrar el punto de entrada principal (nuestro main.tsx)
+ // En modo 'lib', la entrada es el propio archivo de entrada.
+ const entryKey = 'src/main.tsx';
+ const entry = manifest[entryKey];
+
+ if (!entry) throw new Error('No se encontró el punto de entrada en el manifest.');
+
+ const jsUrl = `${WIDGETS_HOST}/${entry.file}`;
+
+ // 3. Cargar el CSS asociado, si existe
+ if (entry.css && entry.css.length > 0) {
+ entry.css.forEach(cssFile => {
+ const cssUrl = `${WIDGETS_HOST}/${cssFile}`;
+ loadCSS(cssUrl);
+ });
+ }
+
+ // 4. Cargar el script principal de la librería
+ await loadScript(jsUrl);
+
+ // 5. Esperar a que la página esté completamente cargada
+ // y luego llamar a la función de renderizado.
+ window.addEventListener('load', function () {
+ if (window.EleccionesWidgets && typeof window.EleccionesWidgets.render === 'function') {
+ console.log('Elecciones Widgets listos. Renderizando...');
+ window.EleccionesWidgets.render();
+ }
+ });
+
+ } catch (error) {
+ console.error('Error al inicializar los widgets de Elecciones:', error);
+ }
+ }
+
+ // Iniciar el proceso
+ initWidgets();
+
+})();
\ No newline at end of file
diff --git a/Elecciones-Web/frontend/public/eldia.svg b/Elecciones-Web/frontend/public/eldia.svg
new file mode 100644
index 0000000..91ce072
--- /dev/null
+++ b/Elecciones-Web/frontend/public/eldia.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/Elecciones-Web/frontend/src/App.tsx b/Elecciones-Web/frontend/src/App.tsx
index dabfc74..f70ac2d 100644
--- a/Elecciones-Web/frontend/src/App.tsx
+++ b/Elecciones-Web/frontend/src/App.tsx
@@ -3,26 +3,55 @@ import './App.css'
import { BancasWidget } from './components/BancasWidget'
import { CongresoWidget } from './components/CongresoWidget'
import MapaBsAs from './components/MapaBsAs'
-import { TickerWidget } from './components/TickerWidget'
+import { DipSenTickerWidget } from './components/DipSenTickerWidget'
import { TelegramaWidget } from './components/TelegramaWidget'
import { ConcejalesWidget } from './components/ConcejalesWidget'
import MapaBsAsSecciones from './components/MapaBsAsSecciones'
import { SenadoresWidget } from './components/SenadoresWidget'
import { DiputadosWidget } from './components/DiputadosWidget'
+import { ResumenGeneralWidget } from './components/ResumenGeneralWidget'
+import { SenadoresTickerWidget } from './components/SenadoresTickerWidget'
+import { DiputadosTickerWidget } from './components/DiputadosTickerWidget'
+import { ConcejalesTickerWidget } from './components/ConcejalesTickerWidget'
+import { DiputadosPorSeccionWidget } from './components/DiputadosPorSeccionWidget'
+import { SenadoresPorSeccionWidget } from './components/SenadoresPorSeccionWidget'
+import { ConcejalesPorSeccionWidget } from './components/ConcejalesPorSeccionWidget'
function App() {
return (
<>
Resultados Electorales - Provincia de Buenos Aires
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
>
diff --git a/Elecciones-Web/frontend/src/apiService.ts b/Elecciones-Web/frontend/src/apiService.ts
index ee38df0..2ce7fdc 100644
--- a/Elecciones-Web/frontend/src/apiService.ts
+++ b/Elecciones-Web/frontend/src/apiService.ts
@@ -1,6 +1,6 @@
// src/apiService.ts
import axios from 'axios';
-import type { ProyeccionBancas, MunicipioSimple, TelegramaData, CatalogoItem, CategoriaResumen, ResultadoTicker } from './types/types';
+import type { ProyeccionBancas, MunicipioSimple, TelegramaData, CatalogoItem, CategoriaResumen, ResultadoTicker, ApiResponseResultadosPorSeccion } from './types/types';
const API_BASE_URL = 'http://localhost:5217/api';
@@ -81,8 +81,13 @@ export const getBancasPorSeccion = async (seccionId: string): Promise => {
- const response = await apiClient.get('/catalogos/secciones-electorales');
+export const getSeccionesElectorales = async (categoriaId?: number): Promise => {
+ let url = '/catalogos/secciones-electorales';
+ // Si se proporciona una categoría, la añadimos a la URL
+ if (categoriaId) {
+ url += `?categoriaId=${categoriaId}`;
+ }
+ const response = await apiClient.get(url);
return response.data;
};
@@ -134,8 +139,8 @@ export const getConfiguracionPublica = async (): Promise =
return response.data;
};
-export const getResultadosConcejales = async (seccionId: string): Promise => {
- const response = await apiClient.get(`/resultados/concejales/${seccionId}`);
+export const getResultadosPorSeccion = async (seccionId: string, categoriaId: number): Promise => {
+ const response = await apiClient.get(`/resultados/seccion-resultados/${seccionId}?categoriaId=${categoriaId}`);
return response.data;
};
diff --git a/Elecciones-Web/frontend/src/components/ConcejalesPorSeccionWidget.tsx b/Elecciones-Web/frontend/src/components/ConcejalesPorSeccionWidget.tsx
new file mode 100644
index 0000000..908768d
--- /dev/null
+++ b/Elecciones-Web/frontend/src/components/ConcejalesPorSeccionWidget.tsx
@@ -0,0 +1,124 @@
+// src/components/ConcejalesPorSeccionWidget.tsx
+import { useState, useMemo, useEffect } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import Select from 'react-select';
+import { getSeccionesElectorales, getResultadosPorSeccion, getConfiguracionPublica } from '../apiService';
+import type { MunicipioSimple, ResultadoTicker, ApiResponseResultadosPorSeccion } from '../types/types';
+import { ImageWithFallback } from './ImageWithFallback';
+import './TickerWidget.css'; // Reutilizamos los estilos del ticker
+
+const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;
+
+// Estilos personalizados para que el selector se vea bien
+const customSelectStyles = {
+ control: (base: any) => ({ ...base, minWidth: '220px', border: '1px solid #ced4da' }),
+ menu: (base: any) => ({ ...base, zIndex: 10 }), // Para que el menú se superponga
+};
+
+const CATEGORIA_ID = 7; // ID para Concejales
+
+export const ConcejalesPorSeccionWidget = () => {
+ const [secciones, setSecciones] = useState([]);
+ const [selectedSeccion, setSelectedSeccion] = useState<{ value: string; label: string } | null>(null);
+
+ const { data: configData } = useQuery({
+ queryKey: ['configuracionPublica'],
+ queryFn: getConfiguracionPublica,
+ staleTime: 0,
+ });
+
+ const cantidadAMostrar = parseInt(configData?.TickerResultadosCantidad || '5', 10);
+
+ // useEffect para obtener la lista de secciones una sola vez
+ useEffect(() => {
+ getSeccionesElectorales().then(seccionesData => {
+ if (seccionesData && seccionesData.length > 0) {
+ const orden = new Map([
+ ['Capital', 0], ['Primera', 1], ['Segunda', 2], ['Tercera', 3],
+ ['Cuarta', 4], ['Quinta', 5], ['Sexta', 6], ['Séptima', 7]
+ ]);
+ const getOrden = (nombre: string) => {
+ const match = nombre.match(/Capital|Primera|Segunda|Tercera|Cuarta|Quinta|Sexta|Séptima/);
+ return match ? orden.get(match[0]) ?? 99 : 99;
+ };
+ seccionesData.sort((a, b) => getOrden(a.nombre) - getOrden(b.nombre));
+
+ setSecciones(seccionesData);
+ // Establecemos la primera sección de la lista ordenada como la por defecto
+ if (!selectedSeccion) {
+ setSelectedSeccion({ value: seccionesData[0].id, label: seccionesData[0].nombre });
+ }
+ }
+ });
+ }, [selectedSeccion]); // Dependencia para asegurar que no se resetee la selección del usuario
+
+ // Transformamos los datos para react-select
+ const seccionOptions = useMemo(() =>
+ secciones.map(s => ({ value: s.id, label: s.nombre })),
+ [secciones]);
+
+ // Query para obtener los resultados de la sección seleccionada
+ const { data, isLoading: isLoadingResultados } = useQuery({
+ queryKey: ['resultadosPorSeccion', selectedSeccion?.value, CATEGORIA_ID],
+ queryFn: () => getResultadosPorSeccion(selectedSeccion!.value, CATEGORIA_ID),
+ enabled: !!selectedSeccion,
+ });
+
+ const resultados = data?.resultados || [];
+
+ // Lógica para "Otros"
+ let displayResults: ResultadoTicker[] = resultados;
+ if (resultados && resultados.length > cantidadAMostrar) {
+ const topParties = resultados.slice(0, cantidadAMostrar - 1);
+ const otherParties = resultados.slice(cantidadAMostrar - 1);
+ const otrosPorcentaje = otherParties.reduce((sum, party) => sum + (party.porcentaje || 0), 0);
+ const otrosEntry: ResultadoTicker = {
+ id: `otros-concejales-${selectedSeccion?.value}`,
+ nombre: 'Otros',
+ nombreCorto: 'Otros',
+ color: '#888888',
+ logoUrl: null,
+ votos: 0,
+ porcentaje: otrosPorcentaje,
+ };
+ displayResults = [...topParties, otrosEntry];
+ } else if (resultados) {
+ displayResults = resultados.slice(0, cantidadAMostrar);
+ }
+
+ return (
+
+
+
CONCEJALES POR SECCIÓN
+
+
+ {(isLoadingResultados && selectedSeccion) &&
Cargando...
}
+ {!selectedSeccion &&
Seleccione una sección.
}
+ {displayResults.map(partido => (
+
+
+
+
+
+
+ {partido.nombreCorto || partido.nombre}
+ {formatPercent(partido.porcentaje)}
+
+
+
+
+ ))}
+
+
+ );
+};
\ No newline at end of file
diff --git a/Elecciones-Web/frontend/src/components/ConcejalesTickerWidget.tsx b/Elecciones-Web/frontend/src/components/ConcejalesTickerWidget.tsx
new file mode 100644
index 0000000..8a5800d
--- /dev/null
+++ b/Elecciones-Web/frontend/src/components/ConcejalesTickerWidget.tsx
@@ -0,0 +1,84 @@
+// src/components/ConcejalesTickerWidget.tsx
+import { useMemo } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { getResumenProvincial, getConfiguracionPublica } from '../apiService';
+import type { CategoriaResumen, ResultadoTicker } from '../types/types';
+import { ImageWithFallback } from './ImageWithFallback';
+import './TickerWidget.css'; // Reutilizamos los mismos estilos
+
+const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;
+const CATEGORIA_ID = 7; // ID para Concejales
+
+export const ConcejalesTickerWidget = () => {
+ const { data: categorias, isLoading, error } = useQuery({
+ queryKey: ['resumenProvincial'],
+ queryFn: getResumenProvincial,
+ refetchInterval: 30000,
+ });
+
+ const { data: configData } = useQuery({
+ queryKey: ['configuracionPublica'],
+ queryFn: getConfiguracionPublica,
+ staleTime: 0,
+ });
+
+ const cantidadAMostrar = parseInt(configData?.TickerResultadosCantidad || '5', 10);
+
+ // Usamos useMemo para encontrar los datos específicos de Concejales
+ const ConcejalesData = useMemo(() => {
+ return categorias?.find(c => c.categoriaId === CATEGORIA_ID);
+ }, [categorias]);
+
+ if (isLoading) return ;
+ if (error || !ConcejalesData) return Datos de Concejales no disponibles.
;
+
+ // Lógica para "Otros"
+ let displayResults: ResultadoTicker[] = ConcejalesData.resultados;
+ if (ConcejalesData.resultados.length > cantidadAMostrar) {
+ const topParties = ConcejalesData.resultados.slice(0, cantidadAMostrar - 1);
+ const otherParties = ConcejalesData.resultados.slice(cantidadAMostrar - 1);
+ const otrosPorcentaje = otherParties.reduce((sum, party) => sum + party.porcentaje, 0);
+ const otrosEntry: ResultadoTicker = {
+ id: `otros-Concejales`,
+ nombre: 'Otros',
+ nombreCorto: 'Otros',
+ color: '#888888',
+ logoUrl: null,
+ votos: 0,
+ porcentaje: otrosPorcentaje,
+ };
+ displayResults = [...topParties, otrosEntry];
+ } else {
+ displayResults = ConcejalesData.resultados.slice(0, cantidadAMostrar);
+ }
+
+ return (
+
+
+
RESUMEN DE {ConcejalesData.categoriaNombre}
+
+ Mesas: {formatPercent(ConcejalesData.estadoRecuento?.mesasTotalizadasPorcentaje || 0)}
+ Part: {formatPercent(ConcejalesData.estadoRecuento?.participacionPorcentaje || 0)}
+
+
+
+ {displayResults.map(partido => (
+
+
+
+
+
+
+ {partido.nombreCorto || partido.nombre}
+ {formatPercent(partido.porcentaje)}
+
+
+
+
+ ))}
+
+
+ );
+};
\ No newline at end of file
diff --git a/Elecciones-Web/frontend/src/components/DevApp.tsx b/Elecciones-Web/frontend/src/components/DevApp.tsx
new file mode 100644
index 0000000..14267f7
--- /dev/null
+++ b/Elecciones-Web/frontend/src/components/DevApp.tsx
@@ -0,0 +1,46 @@
+// src/components/DevApp.tsx
+import { BancasWidget } from './BancasWidget'
+import { CongresoWidget } from './CongresoWidget'
+import MapaBsAs from './MapaBsAs'
+import { DipSenTickerWidget } from './DipSenTickerWidget'
+import { TelegramaWidget } from './TelegramaWidget'
+import { ConcejalesWidget } from './ConcejalesWidget'
+import MapaBsAsSecciones from './MapaBsAsSecciones'
+import { SenadoresWidget } from './SenadoresWidget'
+import { DiputadosWidget } from './DiputadosWidget'
+import { ResumenGeneralWidget } from './ResumenGeneralWidget'
+import { SenadoresTickerWidget } from './SenadoresTickerWidget'
+import { DiputadosTickerWidget } from './DiputadosTickerWidget'
+import { ConcejalesTickerWidget } from './ConcejalesTickerWidget'
+import { DiputadosPorSeccionWidget } from './DiputadosPorSeccionWidget'
+import { SenadoresPorSeccionWidget } from './SenadoresPorSeccionWidget'
+import { ConcejalesPorSeccionWidget } from './ConcejalesPorSeccionWidget'
+import '../App.css';
+
+export const DevApp = () => {
+ return (
+ <>
+
+ Showcase de Widgets - Elecciones 2025
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
\ No newline at end of file
diff --git a/Elecciones-Web/frontend/src/components/TickerWidget.tsx b/Elecciones-Web/frontend/src/components/DipSenTickerWidget.tsx
similarity index 97%
rename from Elecciones-Web/frontend/src/components/TickerWidget.tsx
rename to Elecciones-Web/frontend/src/components/DipSenTickerWidget.tsx
index d4f4033..a071989 100644
--- a/Elecciones-Web/frontend/src/components/TickerWidget.tsx
+++ b/Elecciones-Web/frontend/src/components/DipSenTickerWidget.tsx
@@ -1,4 +1,4 @@
-// src/components/TickerWidget.tsx
+// src/components/DipSenTickerWidget.tsx
import { useQuery } from '@tanstack/react-query';
import { getResumenProvincial, getConfiguracionPublica } from '../apiService';
import type { CategoriaResumen, ResultadoTicker } from '../types/types';
@@ -8,7 +8,7 @@ import { useMemo } from 'react';
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;
-export const TickerWidget = () => {
+export const DipSenTickerWidget = () => {
const { data: categorias, isLoading, error } = useQuery({
queryKey: ['resumenProvincial'],
queryFn: getResumenProvincial,
diff --git a/Elecciones-Web/frontend/src/components/DiputadosPorSeccionWidget.tsx b/Elecciones-Web/frontend/src/components/DiputadosPorSeccionWidget.tsx
new file mode 100644
index 0000000..9a7580c
--- /dev/null
+++ b/Elecciones-Web/frontend/src/components/DiputadosPorSeccionWidget.tsx
@@ -0,0 +1,127 @@
+// src/components/DiputadosPorSeccionWidget.tsx
+import { useState, useMemo, useEffect } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import Select from 'react-select';
+import { getSeccionesElectorales, getResultadosPorSeccion, getConfiguracionPublica } from '../apiService';
+import type { MunicipioSimple, ResultadoTicker, ApiResponseResultadosPorSeccion } from '../types/types';
+import { ImageWithFallback } from './ImageWithFallback';
+import './TickerWidget.css';
+
+const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;
+const customSelectStyles = {
+ control: (base: any) => ({ ...base, minWidth: '220px', border: '1px solid #ced4da' }),
+ menu: (base: any) => ({ ...base, zIndex: 10 }),
+};
+
+const CATEGORIA_ID = 6; // ID para Diputados
+
+export const DiputadosPorSeccionWidget = () => {
+ const [selectedSeccion, setSelectedSeccion] = useState<{ value: string; label: string } | null>(null);
+
+ const { data: configData } = useQuery({
+ queryKey: ['configuracionPublica'],
+ queryFn: getConfiguracionPublica,
+ staleTime: 0,
+ });
+
+ const cantidadAMostrar = parseInt(configData?.ConcejalesResultadosCantidad || '5', 10);
+
+ // Ahora usamos useQuery para obtener las secciones filtradas
+ const { data: secciones = [], isLoading: isLoadingSecciones } = useQuery({
+ queryKey: ['seccionesElectorales', CATEGORIA_ID], // Key única para la caché
+ queryFn: () => getSeccionesElectorales(CATEGORIA_ID), // Pasamos el ID de la categoría
+ });
+
+ // useEffect para establecer la primera sección por defecto
+ useEffect(() => {
+ if (secciones.length > 0 && !selectedSeccion) {
+ // Ordenamos aquí solo para la selección inicial
+ const orden = new Map([
+ ['Capital', 0], ['Primera', 1], ['Segunda', 2], ['Tercera', 3],
+ ['Cuarta', 4], ['Quinta', 5], ['Sexta', 6], ['Séptima', 7]
+ ]);
+ const getOrden = (nombre: string) => {
+ const match = nombre.match(/Capital|Primera|Segunda|Tercera|Cuarta|Quinta|Sexta|Séptima/);
+ return match ? orden.get(match[0]) ?? 99 : 99;
+ };
+ const seccionesOrdenadas = [...secciones].sort((a, b) => getOrden(a.nombre) - getOrden(b.nombre));
+
+ setSelectedSeccion({ value: seccionesOrdenadas[0].id, label: seccionesOrdenadas[0].nombre });
+ }
+ }, [secciones, selectedSeccion]);
+
+ const seccionOptions = useMemo(() =>
+ secciones
+ .map(s => ({ value: s.id, label: s.nombre }))
+ .sort((a, b) => { // Mantenemos el orden en el dropdown
+ const orden = new Map([
+ ['Sección Capital', 0], ['Sección Primera', 1], ['Sección Segunda', 2], ['Sección Tercera', 3],
+ ['Sección Cuarta', 4], ['Sección Quinta', 5], ['Sección Sexta', 6], ['Sección Séptima', 7]
+ ]);
+ return (orden.get(a.label) ?? 99) - (orden.get(b.label) ?? 99);
+ }),
+ [secciones]);
+
+ const { data, isLoading: isLoadingResultados } = useQuery({
+ queryKey: ['resultadosPorSeccion', selectedSeccion?.value, CATEGORIA_ID],
+ queryFn: () => getResultadosPorSeccion(selectedSeccion!.value, CATEGORIA_ID),
+ enabled: !!selectedSeccion,
+ });
+
+ const resultados = data?.resultados || [];
+
+ let displayResults: ResultadoTicker[] = resultados;
+ if (resultados && resultados.length > cantidadAMostrar) {
+ const topParties = resultados.slice(0, cantidadAMostrar - 1);
+ const otherParties = resultados.slice(cantidadAMostrar - 1);
+ const otrosPorcentaje = otherParties.reduce((sum, party) => sum + (party.porcentaje || 0), 0);
+ const otrosEntry: ResultadoTicker = {
+ id: `otros-diputados-${selectedSeccion?.value}`,
+ nombre: 'Otros',
+ nombreCorto: 'Otros',
+ color: '#888888',
+ logoUrl: null,
+ votos: 0,
+ porcentaje: otrosPorcentaje,
+ };
+ displayResults = [...topParties, otrosEntry];
+ } else if (resultados) {
+ displayResults = resultados.slice(0, cantidadAMostrar);
+ }
+
+ return (
+
+
+
DIPUTADOS POR SECCIÓN
+
+
+ {(isLoadingResultados && selectedSeccion) &&
Cargando...
}
+ {!selectedSeccion && !isLoadingSecciones &&
Seleccione una sección.
}
+ {displayResults.map(partido => (
+
+
+
+
+
+
+ {partido.nombreCorto || partido.nombre}
+ {formatPercent(partido.porcentaje)}
+
+
+
+
+ ))}
+
+
+ );
+};
\ No newline at end of file
diff --git a/Elecciones-Web/frontend/src/components/DiputadosTickerWidget.tsx b/Elecciones-Web/frontend/src/components/DiputadosTickerWidget.tsx
new file mode 100644
index 0000000..7160462
--- /dev/null
+++ b/Elecciones-Web/frontend/src/components/DiputadosTickerWidget.tsx
@@ -0,0 +1,84 @@
+// src/components/DiputadosTickerWidget.tsx
+import { useMemo } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { getResumenProvincial, getConfiguracionPublica } from '../apiService';
+import type { CategoriaResumen, ResultadoTicker } from '../types/types';
+import { ImageWithFallback } from './ImageWithFallback';
+import './TickerWidget.css';
+
+const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;
+const CATEGORIA_ID = 6; // ID para Diputados
+
+export const DiputadosTickerWidget = () => {
+ const { data: categorias, isLoading, error } = useQuery({
+ queryKey: ['resumenProvincial'],
+ queryFn: getResumenProvincial,
+ refetchInterval: 30000,
+ });
+
+ const { data: configData } = useQuery({
+ queryKey: ['configuracionPublica'],
+ queryFn: getConfiguracionPublica,
+ staleTime: 0,
+ });
+
+ const cantidadAMostrar = parseInt(configData?.TickerResultadosCantidad || '5', 10);
+
+ // Usamos useMemo para encontrar los datos específicos de Diputados
+ const diputadosData = useMemo(() => {
+ return categorias?.find(c => c.categoriaId === CATEGORIA_ID);
+ }, [categorias]);
+
+ if (isLoading) return ;
+ if (error || !diputadosData) return Datos de Diputados no disponibles.
;
+
+ // Lógica para "Otros"
+ let displayResults: ResultadoTicker[] = diputadosData.resultados;
+ if (diputadosData.resultados.length > cantidadAMostrar) {
+ const topParties = diputadosData.resultados.slice(0, cantidadAMostrar - 1);
+ const otherParties = diputadosData.resultados.slice(cantidadAMostrar - 1);
+ const otrosPorcentaje = otherParties.reduce((sum, party) => sum + party.porcentaje, 0);
+ const otrosEntry: ResultadoTicker = {
+ id: `otros-diputados`,
+ nombre: 'Otros',
+ nombreCorto: 'Otros',
+ color: '#888888',
+ logoUrl: null,
+ votos: 0,
+ porcentaje: otrosPorcentaje,
+ };
+ displayResults = [...topParties, otrosEntry];
+ } else {
+ displayResults = diputadosData.resultados.slice(0, cantidadAMostrar);
+ }
+
+ return (
+
+
+
RESUMEN DE {diputadosData.categoriaNombre}
+
+ Mesas: {formatPercent(diputadosData.estadoRecuento?.mesasTotalizadasPorcentaje || 0)}
+ Part: {formatPercent(diputadosData.estadoRecuento?.participacionPorcentaje || 0)}
+
+
+
+ {displayResults.map(partido => (
+
+
+
+
+
+
+ {partido.nombreCorto || partido.nombre}
+ {formatPercent(partido.porcentaje)}
+
+
+
+
+ ))}
+
+
+ );
+};
\ No newline at end of file
diff --git a/Elecciones-Web/frontend/src/components/ResumenGeneralWidget.tsx b/Elecciones-Web/frontend/src/components/ResumenGeneralWidget.tsx
new file mode 100644
index 0000000..7fb45fd
--- /dev/null
+++ b/Elecciones-Web/frontend/src/components/ResumenGeneralWidget.tsx
@@ -0,0 +1,117 @@
+// src/components/ResumenGeneralWidget.tsx
+import { useMemo } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { getResumenProvincial, getConfiguracionPublica } from '../apiService';
+import type { CategoriaResumen, ResultadoTicker } from '../types/types';
+import { ImageWithFallback } from './ImageWithFallback';
+import './TickerWidget.css';
+
+const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;
+
+export const ResumenGeneralWidget = () => {
+ const { data: categorias, isLoading, error } = useQuery({
+ queryKey: ['resumenProvincial'],
+ queryFn: getResumenProvincial,
+ refetchInterval: 30000,
+ });
+
+ const { data: configData } = useQuery({
+ queryKey: ['configuracionPublica'],
+ queryFn: getConfiguracionPublica,
+ staleTime: 0,
+ });
+
+ const cantidadAMostrar = parseInt(configData?.TickerResultadosCantidad || '5', 10);
+
+ const aggregatedData = useMemo(() => {
+ if (!categorias) return null;
+
+ const legislativeCategories = categorias.filter(c => c.categoriaId === 5 || c.categoriaId === 6);
+ if (legislativeCategories.length === 0) return null;
+
+ const partyMap = new Map>();
+
+ legislativeCategories.forEach(category => {
+ category.resultados.forEach(party => {
+ const existing = partyMap.get(party.id);
+ if (existing) {
+ existing.votos += party.votos;
+ } else {
+ // Clonamos el objeto para no modificar el original
+ partyMap.set(party.id, { ...party });
+ }
+ });
+ });
+
+ const resultsArray = Array.from(partyMap.values());
+ const grandTotalVotes = resultsArray.reduce((sum, party) => sum + party.votos, 0);
+
+ const finalResults: ResultadoTicker[] = resultsArray
+ .map(party => ({
+ ...party,
+ porcentaje: grandTotalVotes > 0 ? (party.votos * 100 / grandTotalVotes) : 0,
+ }))
+ .sort((a, b) => b.votos - a.votos);
+
+ const avgMesas = legislativeCategories.reduce((sum, cat) => sum + (cat.estadoRecuento?.mesasTotalizadasPorcentaje ?? 0), 0) / legislativeCategories.length;
+ const avgParticipacion = legislativeCategories.reduce((sum, cat) => sum + (cat.estadoRecuento?.participacionPorcentaje ?? 0), 0) / legislativeCategories.length;
+
+ return {
+ resultados: finalResults,
+ estadoRecuento: { mesasTotalizadasPorcentaje: avgMesas, participacionPorcentaje: avgParticipacion }
+ };
+ }, [categorias]);
+
+ if (isLoading) return Cargando resumen general...
;
+ if (error || !aggregatedData) return No hay datos para el resumen general.
;
+
+ // Lógica para "Otros"
+ let displayResults: ResultadoTicker[] = aggregatedData.resultados;
+ if (aggregatedData.resultados.length > cantidadAMostrar) {
+ const topParties = aggregatedData.resultados.slice(0, cantidadAMostrar - 1);
+ const otherParties = aggregatedData.resultados.slice(cantidadAMostrar - 1);
+ const otrosPorcentaje = otherParties.reduce((sum, party) => sum + party.porcentaje, 0);
+ const otrosEntry: ResultadoTicker = {
+ id: `otros-general`,
+ nombre: 'Otros',
+ nombreCorto: 'Otros',
+ color: '#888888',
+ logoUrl: null,
+ votos: 0,
+ porcentaje: otrosPorcentaje,
+ };
+ displayResults = [...topParties, otrosEntry];
+ } else {
+ displayResults = aggregatedData.resultados.slice(0, cantidadAMostrar);
+ }
+
+ return (
+
+
+
RESUMEN LEGISLATIVO PROVINCIAL
+
+ Mesas (Prom.): {formatPercent(aggregatedData.estadoRecuento.mesasTotalizadasPorcentaje)}
+ Part (Prom.): {formatPercent(aggregatedData.estadoRecuento.participacionPorcentaje)}
+
+
+
+ {displayResults.map(partido => (
+
+
+
+
+
+
+ {partido.nombreCorto || partido.nombre}
+ {formatPercent(partido.porcentaje)}
+
+
+
+
+ ))}
+
+
+ );
+};
\ No newline at end of file
diff --git a/Elecciones-Web/frontend/src/components/SenadoresPorSeccionWidget.tsx b/Elecciones-Web/frontend/src/components/SenadoresPorSeccionWidget.tsx
new file mode 100644
index 0000000..9723eab
--- /dev/null
+++ b/Elecciones-Web/frontend/src/components/SenadoresPorSeccionWidget.tsx
@@ -0,0 +1,127 @@
+// src/components/SenadoresPorSeccionWidget.tsx
+import { useState, useMemo, useEffect } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import Select from 'react-select';
+import { getSeccionesElectorales, getResultadosPorSeccion, getConfiguracionPublica } from '../apiService';
+import type { MunicipioSimple, ResultadoTicker, ApiResponseResultadosPorSeccion } from '../types/types';
+import { ImageWithFallback } from './ImageWithFallback';
+import './TickerWidget.css';
+
+const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;
+const customSelectStyles = {
+ control: (base: any) => ({ ...base, minWidth: '220px', border: '1px solid #ced4da' }),
+ menu: (base: any) => ({ ...base, zIndex: 10 }),
+};
+
+const CATEGORIA_ID = 5; // ID para Senadores
+
+export const SenadoresPorSeccionWidget = () => {
+ const [selectedSeccion, setSelectedSeccion] = useState<{ value: string; label: string } | null>(null);
+
+ const { data: configData } = useQuery({
+ queryKey: ['configuracionPublica'],
+ queryFn: getConfiguracionPublica,
+ staleTime: 0,
+ });
+
+ const cantidadAMostrar = parseInt(configData?.ConcejalesResultadosCantidad || '5', 10);
+
+ // Ahora usamos useQuery para obtener las secciones filtradas
+ const { data: secciones = [], isLoading: isLoadingSecciones } = useQuery({
+ queryKey: ['seccionesElectorales', CATEGORIA_ID], // Key única para la caché
+ queryFn: () => getSeccionesElectorales(CATEGORIA_ID), // Pasamos el ID de la categoría
+ });
+
+ // useEffect para establecer la primera sección por defecto
+ useEffect(() => {
+ if (secciones.length > 0 && !selectedSeccion) {
+ // Ordenamos aquí solo para la selección inicial
+ const orden = new Map([
+ ['Capital', 0], ['Primera', 1], ['Segunda', 2], ['Tercera', 3],
+ ['Cuarta', 4], ['Quinta', 5], ['Sexta', 6], ['Séptima', 7]
+ ]);
+ const getOrden = (nombre: string) => {
+ const match = nombre.match(/Capital|Primera|Segunda|Tercera|Cuarta|Quinta|Sexta|Séptima/);
+ return match ? orden.get(match[0]) ?? 99 : 99;
+ };
+ const seccionesOrdenadas = [...secciones].sort((a, b) => getOrden(a.nombre) - getOrden(b.nombre));
+
+ setSelectedSeccion({ value: seccionesOrdenadas[0].id, label: seccionesOrdenadas[0].nombre });
+ }
+ }, [secciones, selectedSeccion]);
+
+ const seccionOptions = useMemo(() =>
+ secciones
+ .map(s => ({ value: s.id, label: s.nombre }))
+ .sort((a, b) => { // Mantenemos el orden en el dropdown
+ const orden = new Map([
+ ['Sección Capital', 0], ['Sección Primera', 1], ['Sección Segunda', 2], ['Sección Tercera', 3],
+ ['Sección Cuarta', 4], ['Sección Quinta', 5], ['Sección Sexta', 6], ['Sección Séptima', 7]
+ ]);
+ return (orden.get(a.label) ?? 99) - (orden.get(b.label) ?? 99);
+ }),
+ [secciones]);
+
+ const { data, isLoading: isLoadingResultados } = useQuery({
+ queryKey: ['resultadosPorSeccion', selectedSeccion?.value, CATEGORIA_ID],
+ queryFn: () => getResultadosPorSeccion(selectedSeccion!.value, CATEGORIA_ID),
+ enabled: !!selectedSeccion,
+ });
+
+ const resultados = data?.resultados || [];
+
+ let displayResults: ResultadoTicker[] = resultados;
+ if (resultados && resultados.length > cantidadAMostrar) {
+ const topParties = resultados.slice(0, cantidadAMostrar - 1);
+ const otherParties = resultados.slice(cantidadAMostrar - 1);
+ const otrosPorcentaje = otherParties.reduce((sum, party) => sum + (party.porcentaje || 0), 0);
+ const otrosEntry: ResultadoTicker = {
+ id: `otros-senadores-${selectedSeccion?.value}`,
+ nombre: 'Otros',
+ nombreCorto: 'Otros',
+ color: '#888888',
+ logoUrl: null,
+ votos: 0,
+ porcentaje: otrosPorcentaje,
+ };
+ displayResults = [...topParties, otrosEntry];
+ } else if (resultados) {
+ displayResults = resultados.slice(0, cantidadAMostrar);
+ }
+
+ return (
+
+
+
SENADORES POR SECCIÓN
+
+
+ {(isLoadingResultados && selectedSeccion) &&
Cargando...
}
+ {!selectedSeccion && !isLoadingSecciones &&
Seleccione una sección.
}
+ {displayResults.map(partido => (
+
+
+
+
+
+
+ {partido.nombreCorto || partido.nombre}
+ {formatPercent(partido.porcentaje)}
+
+
+
+
+ ))}
+
+
+ );
+};
\ No newline at end of file
diff --git a/Elecciones-Web/frontend/src/components/SenadoresTickerWidget.tsx b/Elecciones-Web/frontend/src/components/SenadoresTickerWidget.tsx
new file mode 100644
index 0000000..c867480
--- /dev/null
+++ b/Elecciones-Web/frontend/src/components/SenadoresTickerWidget.tsx
@@ -0,0 +1,84 @@
+// src/components/SenadoresTickerWidget.tsx
+import { useMemo } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { getResumenProvincial, getConfiguracionPublica } from '../apiService';
+import type { CategoriaResumen, ResultadoTicker } from '../types/types';
+import { ImageWithFallback } from './ImageWithFallback';
+import './TickerWidget.css'; // Reutilizamos los mismos estilos
+
+const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;
+const CATEGORIA_ID = 5; // ID para Senadores
+
+export const SenadoresTickerWidget = () => {
+ const { data: categorias, isLoading, error } = useQuery({
+ queryKey: ['resumenProvincial'],
+ queryFn: getResumenProvincial,
+ refetchInterval: 30000,
+ });
+
+ const { data: configData } = useQuery({
+ queryKey: ['configuracionPublica'],
+ queryFn: getConfiguracionPublica,
+ staleTime: 0,
+ });
+
+ const cantidadAMostrar = parseInt(configData?.TickerResultadosCantidad || '5', 10);
+
+ // Usamos useMemo para encontrar los datos específicos de Senadores
+ const senadoresData = useMemo(() => {
+ return categorias?.find(c => c.categoriaId === CATEGORIA_ID);
+ }, [categorias]);
+
+ if (isLoading) return ;
+ if (error || !senadoresData) return Datos de Senadores no disponibles.
;
+
+ // Lógica para "Otros"
+ let displayResults: ResultadoTicker[] = senadoresData.resultados;
+ if (senadoresData.resultados.length > cantidadAMostrar) {
+ const topParties = senadoresData.resultados.slice(0, cantidadAMostrar - 1);
+ const otherParties = senadoresData.resultados.slice(cantidadAMostrar - 1);
+ const otrosPorcentaje = otherParties.reduce((sum, party) => sum + party.porcentaje, 0);
+ const otrosEntry: ResultadoTicker = {
+ id: `otros-senadores`,
+ nombre: 'Otros',
+ nombreCorto: 'Otros',
+ color: '#888888',
+ logoUrl: null,
+ votos: 0,
+ porcentaje: otrosPorcentaje,
+ };
+ displayResults = [...topParties, otrosEntry];
+ } else {
+ displayResults = senadoresData.resultados.slice(0, cantidadAMostrar);
+ }
+
+ return (
+
+
+
RESUMEN DE {senadoresData.categoriaNombre}
+
+ Mesas: {formatPercent(senadoresData.estadoRecuento?.mesasTotalizadasPorcentaje || 0)}
+ Part: {formatPercent(senadoresData.estadoRecuento?.participacionPorcentaje || 0)}
+
+
+
+ {displayResults.map(partido => (
+
+
+
+
+
+
+ {partido.nombreCorto || partido.nombre}
+ {formatPercent(partido.porcentaje)}
+
+
+
+
+ ))}
+
+
+ );
+};
\ No newline at end of file
diff --git a/Elecciones-Web/frontend/src/components/SenadoresWidget.tsx b/Elecciones-Web/frontend/src/components/SenadoresWidget.tsx
index 3f5aaa5..cbec56c 100644
--- a/Elecciones-Web/frontend/src/components/SenadoresWidget.tsx
+++ b/Elecciones-Web/frontend/src/components/SenadoresWidget.tsx
@@ -35,10 +35,10 @@ export const SenadoresWidget = () => {
queryFn: () => getMunicipios(CATEGORIA_ID), // Pasamos el ID de la categoría
});
- // useEffect para establecer "LA PLATA" por defecto
+ // useEffect para establecer "ALBERTI" por defecto
useEffect(() => {
if (municipios.length > 0 && !selectedMunicipio) {
- const laPlata = municipios.find(m => m.nombre.toUpperCase() === 'LA PLATA');
+ const laPlata = municipios.find(m => m.nombre.toUpperCase() === 'ALBERTI');
if (laPlata) {
setSelectedMunicipio({ value: laPlata.id, label: laPlata.nombre });
}
diff --git a/Elecciones-Web/frontend/src/main.tsx b/Elecciones-Web/frontend/src/main.tsx
index ab17c75..c4a5205 100644
--- a/Elecciones-Web/frontend/src/main.tsx
+++ b/Elecciones-Web/frontend/src/main.tsx
@@ -1,17 +1,83 @@
// src/main.tsx
-import React from 'react'
-import ReactDOM from 'react-dom/client'
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
-import App from './App.tsx'
-import './index.css'
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-// Crear un cliente de React Query
-const queryClient = new QueryClient()
+import { BancasWidget } from './components/BancasWidget'
+import { CongresoWidget } from './components/CongresoWidget'
+import MapaBsAs from './components/MapaBsAs'
+import { DipSenTickerWidget } from './components/DipSenTickerWidget'
+import { TelegramaWidget } from './components/TelegramaWidget'
+import { ConcejalesWidget } from './components/ConcejalesWidget'
+import MapaBsAsSecciones from './components/MapaBsAsSecciones'
+import { SenadoresWidget } from './components/SenadoresWidget'
+import { DiputadosWidget } from './components/DiputadosWidget'
+import { ResumenGeneralWidget } from './components/ResumenGeneralWidget'
+import { SenadoresTickerWidget } from './components/SenadoresTickerWidget'
+import { DiputadosTickerWidget } from './components/DiputadosTickerWidget'
+import { ConcejalesTickerWidget } from './components/ConcejalesTickerWidget'
+import { DiputadosPorSeccionWidget } from './components/DiputadosPorSeccionWidget'
+import { SenadoresPorSeccionWidget } from './components/SenadoresPorSeccionWidget'
+import { ConcejalesPorSeccionWidget } from './components/ConcejalesPorSeccionWidget'
-ReactDOM.createRoot(document.getElementById('root')!).render(
-
-
-
-
- ,
-)
\ No newline at end of file
+import './index.css';
+import { DevApp } from './components/DevApp';
+
+const queryClient = new QueryClient();
+
+// Mapeamos el nombre del widget (del atributo data) al componente de React
+const WIDGET_MAP: Record = {
+ 'resumen-senadores': SenadoresWidget,
+ 'resumen-diputados': DiputadosWidget,
+ 'resumen-concejales': ConcejalesWidget,
+ 'congreso-provincial': CongresoWidget,
+ 'distribucion-bancas': BancasWidget,
+ 'mapa-municipios': MapaBsAs,
+ 'mapa-secciones': MapaBsAsSecciones,
+ 'consulta-telegramas': TelegramaWidget,
+ 'ticker-senadores': SenadoresTickerWidget,
+ 'ticker-diputados': DiputadosTickerWidget,
+ 'ticker-concejales': ConcejalesTickerWidget,
+ 'ticker-dip-sen': DipSenTickerWidget,
+ 'resumen-general': ResumenGeneralWidget,
+ 'diputados-por-seccion': DiputadosPorSeccionWidget,
+ 'senadores-por-seccion': SenadoresPorSeccionWidget,
+ 'concejales-por-seccion': ConcejalesPorSeccionWidget,
+};
+
+// Vite establece `import.meta.env.DEV` a `true` cuando ejecutamos 'npm run dev'
+if (import.meta.env.DEV) {
+ // --- MODO DESARROLLO ---
+ // Renderizamos nuestra página de showcase en el div#root
+ ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+
+
+ );
+} else {
+ // --- MODO PRODUCCIÓN ---
+ // Exponemos la función de renderizado para el bootstrap.js
+ const renderWidgets = () => {
+ const widgetContainers = document.querySelectorAll('[data-elecciones-widget]');
+ widgetContainers.forEach(container => {
+ const widgetName = (container as HTMLElement).dataset.eleccionesWidget;
+ if (widgetName && WIDGET_MAP[widgetName]) {
+ const WidgetComponent = WIDGET_MAP[widgetName];
+ const root = ReactDOM.createRoot(container);
+ root.render(
+
+
+
+
+
+ );
+ }
+ });
+ };
+
+ (window as any).EleccionesWidgets = {
+ render: renderWidgets
+ };
+}
\ No newline at end of file
diff --git a/Elecciones-Web/frontend/src/types/types.ts b/Elecciones-Web/frontend/src/types/types.ts
index 5c17760..1e950b4 100644
--- a/Elecciones-Web/frontend/src/types/types.ts
+++ b/Elecciones-Web/frontend/src/types/types.ts
@@ -105,4 +105,9 @@ export interface TelegramaData {
export interface CatalogoItem {
id: string;
nombre: string;
+}
+
+export interface ApiResponseResultadosPorSeccion {
+ ultimaActualizacion: string;
+ resultados: ResultadoTicker[];
}
\ No newline at end of file
diff --git a/Elecciones-Web/frontend/vite.config.ts b/Elecciones-Web/frontend/vite.config.ts
index 8b0f57b..c4b9cce 100644
--- a/Elecciones-Web/frontend/vite.config.ts
+++ b/Elecciones-Web/frontend/vite.config.ts
@@ -1,7 +1,39 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
+import path from 'path';
-// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
+ build: {
+ // Mantenemos la configuración de librería para generar el UMD
+ lib: {
+ entry: path.resolve(__dirname, 'src/main.tsx'),
+ name: 'EleccionesWidgets',
+ fileName: 'elecciones-widgets', // Vite añadirá .umd.js automáticamente
+ formats: ['umd']
+ },
+ outDir: 'dist',
+ manifest: true, // Le dice a Vite que genere el manifest.json
+ rollupOptions: {
+ // Asegura que el manifest se genere incluso en modo librería
+ output: {
+ assetFileNames: "assets/[name].[hash].[ext]",
+ chunkFileNames: "assets/[name].[hash].js",
+ entryFileNames: "assets/[name].[hash].js"
+ }
+ }
+ },
+ server: {
+ proxy: {
+ // Cualquier petición que empiece con /api...
+ '/api': {
+ // ...redirígela a nuestro backend de .NET
+ target: 'http://localhost:5217',
+ // Cambia el origen de la petición para que el backend la acepte
+ changeOrigin: true,
+ // No necesitamos reescribir la ruta, ya que el backend espera /api/...
+ // rewrite: (path) => path.replace(/^\/api/, '')
+ },
+ }
+ }
})
diff --git a/Elecciones-Web/src/Elecciones.Api/Controllers/CatalogosController.cs b/Elecciones-Web/src/Elecciones.Api/Controllers/CatalogosController.cs
index 8a57a96..23e99b4 100644
--- a/Elecciones-Web/src/Elecciones.Api/Controllers/CatalogosController.cs
+++ b/Elecciones-Web/src/Elecciones.Api/Controllers/CatalogosController.cs
@@ -63,21 +63,40 @@ public class CatalogosController : ControllerBase
}
[HttpGet("secciones-electorales")]
- public async Task GetSeccionesElectorales()
+ public async Task GetSeccionesElectorales([FromQuery] int? categoriaId)
{
- var secciones = await _dbContext.AmbitosGeograficos
+ var seccionesQuery = _dbContext.AmbitosGeograficos
.AsNoTracking()
- // Buscamos los ámbitos que son Secciones Electorales (Nivel 20)
- .Where(a => a.NivelId == 20 && a.SeccionProvincialId != null)
- .Select(a => new MunicipioSimpleDto // Reutilizamos el DTO porque tiene la misma estructura {Id, Nombre}
- {
- Id = a.SeccionProvincialId!, // El ID que usaremos es el SeccionProvincialId
- Nombre = a.Nombre
- })
- .OrderBy(s => s.Nombre)
+ .Where(a => a.NivelId == 20); // Nivel 20 = Sección Electoral
+
+ // Si NO se proporciona una categoriaId, devolvemos todas las secciones.
+ if (categoriaId == null)
+ {
+ var todasLasSecciones = await seccionesQuery
+ .OrderBy(a => a.Nombre)
+ .Select(a => new { Id = a.SeccionProvincialId, a.Nombre })
+ .ToListAsync();
+ return Ok(todasLasSecciones);
+ }
+
+ // --- LÓGICA DE FILTRADO ---
+ // 1. Encontrar las Secciones Provinciales que SÍ tienen resultados para la categoría solicitada.
+ // Usamos ProyeccionesBancas como fuente de verdad, ya que es el dato más relevante.
+ var seccionesActivasIds = await _dbContext.ProyeccionesBancas
+ .AsNoTracking()
+ .Where(p => p.CategoriaId == categoriaId)
+ .Select(p => p.AmbitoGeografico.SeccionProvincialId)
+ .Distinct()
.ToListAsync();
- return Ok(secciones);
+ // 2. Filtramos la lista de secciones.
+ var seccionesFiltradas = await seccionesQuery
+ .Where(s => seccionesActivasIds.Contains(s.SeccionProvincialId))
+ .OrderBy(a => a.Nombre)
+ .Select(a => new { Id = a.SeccionProvincialId, a.Nombre })
+ .ToListAsync();
+
+ return Ok(seccionesFiltradas);
}
// Nivel 20: Sección Electoral
diff --git a/Elecciones-Web/src/Elecciones.Api/Controllers/ResultadosController.cs b/Elecciones-Web/src/Elecciones.Api/Controllers/ResultadosController.cs
index b7787b3..f5060a7 100644
--- a/Elecciones-Web/src/Elecciones.Api/Controllers/ResultadosController.cs
+++ b/Elecciones-Web/src/Elecciones.Api/Controllers/ResultadosController.cs
@@ -531,8 +531,8 @@ public class ResultadosController : ControllerBase
return Ok(configuracionPublica);
}
- [HttpGet("concejales/{seccionId}")]
- public async Task GetResultadosConcejalesPorSeccion(string seccionId)
+ [HttpGet("seccion-resultados/{seccionId}")]
+ public async Task GetResultadosAgregadosPorSeccion(string seccionId, [FromQuery] int categoriaId)
{
var municipiosDeLaSeccion = await _dbContext.AmbitosGeograficos
.AsNoTracking()
@@ -548,40 +548,51 @@ public class ResultadosController : ControllerBase
var resultadosMunicipales = await _dbContext.ResultadosVotos
.AsNoTracking()
.Include(r => r.AgrupacionPolitica)
- .Where(r => r.CategoriaId == 7 && municipiosDeLaSeccion.Contains(r.AmbitoGeograficoId))
+ // Usamos la categoriaId del parámetro
+ .Where(r => r.CategoriaId == categoriaId && municipiosDeLaSeccion.Contains(r.AmbitoGeograficoId))
.ToListAsync();
- var logosConcejales = await _dbContext.LogosAgrupacionesCategorias
+ var logos = await _dbContext.LogosAgrupacionesCategorias
.AsNoTracking()
- .Where(l => l.CategoriaId == 7)
+ // Usamos la categoriaId del parámetro
+ .Where(l => l.CategoriaId == categoriaId)
.ToDictionaryAsync(l => l.AgrupacionPoliticaId);
var totalVotosSeccion = (decimal)resultadosMunicipales.Sum(r => r.CantidadVotos);
var resultadosFinales = resultadosMunicipales
- // 1. Agrupamos por el ID del partido para evitar duplicados.
.GroupBy(r => r.AgrupacionPoliticaId)
.Select(g => new
{
- // 2. Obtenemos la entidad completa del primer elemento del grupo.
Agrupacion = g.First().AgrupacionPolitica,
Votos = g.Sum(r => r.CantidadVotos)
})
.OrderByDescending(r => r.Votos)
.Select(r => new
{
- Id = r.Agrupacion.Id, // Aseguramos que el Id esté en el objeto final
+ Id = r.Agrupacion.Id,
r.Agrupacion.Nombre,
r.Agrupacion.NombreCorto,
r.Agrupacion.Color,
- LogoUrl = logosConcejales.GetValueOrDefault(r.Agrupacion.Id)?.LogoUrl,
+ LogoUrl = logos.GetValueOrDefault(r.Agrupacion.Id)?.LogoUrl,
Votos = r.Votos,
- // 3. Usamos el nombre de propiedad correcto que el frontend espera: 'votosPorcentaje'
- VotosPorcentaje = totalVotosSeccion > 0 ? ((decimal)r.Votos * 100 / totalVotosSeccion) : 0
+ Porcentaje = totalVotosSeccion > 0 ? ((decimal)r.Votos * 100 / totalVotosSeccion) : 0
})
.ToList();
- return Ok(resultadosFinales);
+ // Devolvemos un objeto para poder añadir la fecha de actualización
+ var seccionAmbito = await _dbContext.AmbitosGeograficos.AsNoTracking()
+ .FirstOrDefaultAsync(a => a.SeccionProvincialId == seccionId && a.NivelId == 20);
+ var estadoRecuento = seccionAmbito != null
+ ? await _dbContext.EstadosRecuentos.AsNoTracking()
+ .FirstOrDefaultAsync(e => e.AmbitoGeograficoId == seccionAmbito.Id && e.CategoriaId == categoriaId)
+ : null;
+
+ return Ok(new
+ {
+ UltimaActualizacion = estadoRecuento?.FechaTotalizacion ?? DateTime.UtcNow,
+ Resultados = resultadosFinales
+ });
}
[HttpGet("mapa-por-seccion")]
diff --git a/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/Elecciones.Api.AssemblyInfo.cs b/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/Elecciones.Api.AssemblyInfo.cs
index 1cd89d0..fd71d17 100644
--- a/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/Elecciones.Api.AssemblyInfo.cs
+++ b/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/Elecciones.Api.AssemblyInfo.cs
@@ -14,7 +14,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Api")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
-[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+6732a0e826a402495269212729673ebf1ff01916")]
+[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+32e85b9b9ddc7cd6422bfa52ff3c17fc6eff930b")]
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Api")]
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Api")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
diff --git a/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json b/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json
index 470c31e..cb6ecc8 100644
--- a/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json
+++ b/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json
@@ -1 +1 @@
-{"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["h1yBBcAgq4jIQ1vINVvluRQMeuJlGA3/Zciq/j5c0AM=","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=","FktDKOD55tGexuQTZYqJXFJKcfFUYha2UUveJ7i4d\u002B0=","6CAjHexjcmVc1caYyfNvMfhJRU6qtmi57Siv1ysirg0=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","yfarYtFn36AY3Hcf8kh\u002BwBS7wI/HSaScE\u002BTOvW5GoAM="],"CachedAssets":{},"CachedCopyCandidates":{}}
\ No newline at end of file
+{"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["h1yBBcAgq4jIQ1vINVvluRQMeuJlGA3/Zciq/j5c0AM=","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=","6UbV83RDqihxiPJRSqBBce97kpcbB7parfVbqy/JDrA=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","4a8hgDNqfLAz7U5813iwPnUccUabxTlRH1USsCHCuGA="],"CachedAssets":{},"CachedCopyCandidates":{}}
\ No newline at end of file
diff --git a/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json b/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json
index d51f28f..d15c4bb 100644
--- a/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json
+++ b/Elecciones-Web/src/Elecciones.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json
@@ -1 +1 @@
-{"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["h1yBBcAgq4jIQ1vINVvluRQMeuJlGA3/Zciq/j5c0AM=","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=","FktDKOD55tGexuQTZYqJXFJKcfFUYha2UUveJ7i4d\u002B0=","6CAjHexjcmVc1caYyfNvMfhJRU6qtmi57Siv1ysirg0=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","yfarYtFn36AY3Hcf8kh\u002BwBS7wI/HSaScE\u002BTOvW5GoAM="],"CachedAssets":{},"CachedCopyCandidates":{}}
\ No newline at end of file
+{"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["h1yBBcAgq4jIQ1vINVvluRQMeuJlGA3/Zciq/j5c0AM=","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=","6UbV83RDqihxiPJRSqBBce97kpcbB7parfVbqy/JDrA=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","4a8hgDNqfLAz7U5813iwPnUccUabxTlRH1USsCHCuGA="],"CachedAssets":{},"CachedCopyCandidates":{}}
\ No newline at end of file
diff --git a/Elecciones-Web/src/Elecciones.Api/src/obj/Elecciones.Database.EntityFrameworkCore.targets b/Elecciones-Web/src/Elecciones.Api/src/obj/Elecciones.Database.EntityFrameworkCore.targets
deleted file mode 100644
index 7d6485d..0000000
--- a/Elecciones-Web/src/Elecciones.Api/src/obj/Elecciones.Database.EntityFrameworkCore.targets
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Elecciones-Web/src/Elecciones.Core/obj/Debug/net9.0/Elecciones.Core.AssemblyInfo.cs b/Elecciones-Web/src/Elecciones.Core/obj/Debug/net9.0/Elecciones.Core.AssemblyInfo.cs
index 81bac9e..de1f177 100644
--- a/Elecciones-Web/src/Elecciones.Core/obj/Debug/net9.0/Elecciones.Core.AssemblyInfo.cs
+++ b/Elecciones-Web/src/Elecciones.Core/obj/Debug/net9.0/Elecciones.Core.AssemblyInfo.cs
@@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Core")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
-[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+6732a0e826a402495269212729673ebf1ff01916")]
+[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+32e85b9b9ddc7cd6422bfa52ff3c17fc6eff930b")]
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Core")]
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Core")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
diff --git a/Elecciones-Web/src/Elecciones.Database/obj/Debug/net9.0/Elecciones.Database.AssemblyInfo.cs b/Elecciones-Web/src/Elecciones.Database/obj/Debug/net9.0/Elecciones.Database.AssemblyInfo.cs
index d1068db..d80845a 100644
--- a/Elecciones-Web/src/Elecciones.Database/obj/Debug/net9.0/Elecciones.Database.AssemblyInfo.cs
+++ b/Elecciones-Web/src/Elecciones.Database/obj/Debug/net9.0/Elecciones.Database.AssemblyInfo.cs
@@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Database")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
-[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+6732a0e826a402495269212729673ebf1ff01916")]
+[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+32e85b9b9ddc7cd6422bfa52ff3c17fc6eff930b")]
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Database")]
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Database")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
diff --git a/Elecciones-Web/src/Elecciones.Infrastructure/obj/Debug/net9.0/Elecciones.Infrastructure.AssemblyInfo.cs b/Elecciones-Web/src/Elecciones.Infrastructure/obj/Debug/net9.0/Elecciones.Infrastructure.AssemblyInfo.cs
index 6d5a89e..917a401 100644
--- a/Elecciones-Web/src/Elecciones.Infrastructure/obj/Debug/net9.0/Elecciones.Infrastructure.AssemblyInfo.cs
+++ b/Elecciones-Web/src/Elecciones.Infrastructure/obj/Debug/net9.0/Elecciones.Infrastructure.AssemblyInfo.cs
@@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Infrastructure")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
-[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+6732a0e826a402495269212729673ebf1ff01916")]
+[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+32e85b9b9ddc7cd6422bfa52ff3c17fc6eff930b")]
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Infrastructure")]
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Infrastructure")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
diff --git a/docker-compose.yml b/docker-compose.yml
index 6f081bb..c07eb25 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,46 +1,54 @@
services:
- # Servicio del Backend API
elecciones-api:
build:
context: ./Elecciones-Web
dockerfile: src/Elecciones.Api/Dockerfile
container_name: elecciones-api
restart: unless-stopped
- env_file:
- - ./.env # Lee las variables de entorno desde el archivo .env
+ env_file: ./.env
+ # .NET expone el puerto 8080 por defecto en contenedores
+ expose:
+ - "8080"
networks:
- elecciones-net
- - shared-net # Se conecta a la red compartida para hablar con la DB
- # No se exponen puertos directamente al host. El proxy se encarga.
+ - shared-net
- # Servicio del Worker para la ingesta de datos
+ # Servicio del Worker (sin cambios)
elecciones-worker:
build:
context: ./Elecciones-Web
dockerfile: src/Elecciones.Worker/Dockerfile
container_name: elecciones-worker
restart: unless-stopped
- env_file:
- - ./.env
+ env_file: ./.env
networks:
- - shared-net # Solo necesita acceso a la DB y a la API electoral (internet).
+ - shared-net
volumes:
- # Mapea el directorio /app/logs de dentro del contenedor
- # a un directorio llamado 'logs-worker' en la raíz de tu proyecto en la máquina local.
- ./logs-worker:/app/logs
- # No se exponen puertos.
- # Servicio del Frontend (servido por Nginx)
- # Lo definimos ahora, aunque lo construiremos más adelante.
+ # Servicio del Frontend Público (sin cambios)
elecciones-frontend:
build:
context: ./Elecciones-Web/frontend
dockerfile: Dockerfile
container_name: elecciones-frontend
restart: unless-stopped
+ expose:
+ - "80"
+ networks:
+ - elecciones-net
+
+ # --- Frontend Admin ---
+ elecciones-frontend-admin:
+ build:
+ context: ./Elecciones-Web/frontend-admin
+ dockerfile: Dockerfile
+ container_name: elecciones-frontend-admin
+ restart: unless-stopped
+ expose:
+ - "80"
networks:
- elecciones-net
- # No se exponen puertos.
# Proxy Inverso que expone los servicios al exterior
proxy:
@@ -48,21 +56,22 @@ services:
container_name: elecciones-proxy
restart: unless-stopped
volumes:
- # Mapea nuestro archivo de configuración local al contenedor de Nginx
+ # Mapea nuestro archivo de configuración de proxy local al contenedor
- ./proxy/nginx.conf:/etc/nginx/conf.d/default.conf
ports:
- # ÚNICO PUNTO DE ENTRADA: Expone el puerto 80 del contenedor al puerto 8600 del host.
+ # Puerto para el sitio público/widgets
- "8600:80"
+ # --- NUEVO PUERTO: Puerto para el panel de administración ---
+ - "8700:81"
networks:
- elecciones-net
depends_on:
- elecciones-api
- elecciones-frontend
+ - elecciones-frontend-admin
networks:
elecciones-net:
driver: bridge
-
- # La red 'shared-net' ya existe en Docker.
shared-net:
external: true
\ No newline at end of file
diff --git a/proxy/nginx.conf b/proxy/nginx.conf
index 5ee2078..907acbb 100644
--- a/proxy/nginx.conf
+++ b/proxy/nginx.conf
@@ -1,55 +1,67 @@
# ./proxy/nginx.conf
-# Definimos los upstreams para referirnos a nuestros servicios por su nombre.
-# Docker Compose se encargará de resolver estos nombres a las IPs de los contenedores.
+# --- Upstreams (Definiciones de nuestros servicios) ---
upstream backend_api {
- # Apunta al servicio de la API en el puerto que expone internamente.
- # Por defecto, .NET en contenedores usa el 8080.
server elecciones-api:8080;
}
-
-upstream frontend_app {
- # Apunta al contenedor que sirve los archivos estáticos del frontend.
- # El Dockerfile del frontend usará un Nginx que escucha en el puerto 80.
+upstream frontend_public {
server elecciones-frontend:80;
}
+upstream frontend_admin {
+ server elecciones-frontend-admin:80;
+}
+# --- Servidor para el Frontend Público / Widgets (Puerto 80) ---
server {
listen 80;
- server_name _; # Escucha en cualquier hostname que llegue a este proxy.
+ server_name elecciones2025.eldia.com; # Servirá para este dominio
- # --- UBICACIÓN PARA LA API ---
- # Gestiona todas las peticiones que comienzan con /api/
+ # --- RUTA PARA LA API ---
location /api/ {
proxy_pass http://backend_api;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
}
- # --- UBICACIÓN PARA EL FRONTEND (RAÍZ Y CUALQUIER OTRA RUTA) ---
- # Gestiona todas las demás peticiones.
+ # --- RUTA PARA EL FRONTEND PÚBLICO ---
location / {
- # ¡CRUCIAL! Añade la cabecera CORS para permitir que los widgets
- # se incrusten y carguen desde otros dominios como www.eldia.com.
- add_header 'Access-Control-Allow-Origin' '*' always;
-
- # Manejo de peticiones pre-vuelo (preflight) OPTIONS.
+ # ¡CRUCIAL! Permite que los widgets se incrusten en otros sitios.
+ # Esto es más seguro que un '*' genérico.
+ add_header 'Access-Control-Allow-Origin' 'https://www.eldia.com, https://extras.eldia.com, http://localhost:5173' always;
+
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
- add_header 'Access-Control-Max-Age' 1728000;
- add_header 'Content-Type' 'text/plain; charset=utf-8';
- add_header 'Content-Length' 0;
return 204;
}
- # Redirige la petición al contenedor del frontend.
- proxy_pass http://frontend_app;
+ proxy_pass http://frontend_public;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+}
+
+# --- SERVIDOR para el Frontend de Administración (Puerto 81) ---
+server {
+ listen 81;
+ server_name _; # Escucha en cualquier hostname que llegue a este puerto
+
+ # --- RUTA PARA LA API ---
+ # Es necesario repetir esto para que el panel de admin pueda hablar con la API.
+ location /api/ {
+ proxy_pass http://backend_api;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ # --- RUTA PARA LA APP DE ADMIN ---
+ location / {
+ proxy_pass http://frontend_admin;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
}
}
\ No newline at end of file