Test Public Side
This commit is contained in:
18
Elecciones-Web/frontend-admin/Dockerfile
Normal file
18
Elecciones-Web/frontend-admin/Dockerfile
Normal file
@@ -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
|
||||
18
Elecciones-Web/frontend-admin/frontend.nginx.conf
Normal file
18
Elecciones-Web/frontend-admin/frontend.nginx.conf
Normal file
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/eldia.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<title>Elecciones 2025 - El Día</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
6
Elecciones-Web/frontend-admin/public/eldia.svg
Normal file
6
Elecciones-Web/frontend-admin/public/eldia.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="89" height="69">
|
||||
<path d="M0 0 C29.37 0 58.74 0 89 0 C89 22.77 89 45.54 89 69 C59.63 69 30.26 69 0 69 C0 46.23 0 23.46 0 0 Z " fill="#008FBD" transform="translate(0,0)"/>
|
||||
<path d="M0 0 C3.3 0 6.6 0 10 0 C13.04822999 3.04822999 12.29337257 6.08805307 12.32226562 10.25390625 C12.31904297 11.32511719 12.31582031 12.39632812 12.3125 13.5 C12.32861328 14.57121094 12.34472656 15.64242187 12.36132812 16.74609375 C12.36197266 17.76832031 12.36261719 18.79054688 12.36328125 19.84375 C12.36775269 21.25430664 12.36775269 21.25430664 12.37231445 22.69335938 C12.1880188 23.83514648 12.1880188 23.83514648 12 25 C9 27 9 27 0 27 C0 18.09 0 9.18 0 0 Z " fill="#000000" transform="translate(0,21)"/>
|
||||
<path d="M0 0 C5.61 0 11.22 0 17 0 C17 3.3 17 6.6 17 10 C25.58 10 34.16 10 43 10 C43 10.99 43 11.98 43 13 C34.42 13 25.84 13 17 13 C17 17.29 17 21.58 17 26 C11.39 26 5.78 26 0 26 C0 24.68 0 23.36 0 22 C4.62 22 9.24 22 14 22 C14 16.06 14 10.12 14 4 C9.38 4 4.76 4 0 4 C0 2.68 0 1.36 0 0 Z " fill="#000000" transform="translate(46,21)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -2,9 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/eldia.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<title>Elecciones 2025 - Dev Showcase</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
71
Elecciones-Web/frontend/public/bootstrap.js
vendored
Normal file
71
Elecciones-Web/frontend/public/bootstrap.js
vendored
Normal file
@@ -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();
|
||||
|
||||
})();
|
||||
6
Elecciones-Web/frontend/public/eldia.svg
Normal file
6
Elecciones-Web/frontend/public/eldia.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="89" height="69">
|
||||
<path d="M0 0 C29.37 0 58.74 0 89 0 C89 22.77 89 45.54 89 69 C59.63 69 30.26 69 0 69 C0 46.23 0 23.46 0 0 Z " fill="#008FBD" transform="translate(0,0)"/>
|
||||
<path d="M0 0 C3.3 0 6.6 0 10 0 C13.04822999 3.04822999 12.29337257 6.08805307 12.32226562 10.25390625 C12.31904297 11.32511719 12.31582031 12.39632812 12.3125 13.5 C12.32861328 14.57121094 12.34472656 15.64242187 12.36132812 16.74609375 C12.36197266 17.76832031 12.36261719 18.79054688 12.36328125 19.84375 C12.36775269 21.25430664 12.36775269 21.25430664 12.37231445 22.69335938 C12.1880188 23.83514648 12.1880188 23.83514648 12 25 C9 27 9 27 0 27 C0 18.09 0 9.18 0 0 Z " fill="#000000" transform="translate(0,21)"/>
|
||||
<path d="M0 0 C5.61 0 11.22 0 17 0 C17 3.3 17 6.6 17 10 C25.58 10 34.16 10 43 10 C43 10.99 43 11.98 43 13 C34.42 13 25.84 13 17 13 C17 17.29 17 21.58 17 26 C11.39 26 5.78 26 0 26 C0 24.68 0 23.36 0 22 C4.62 22 9.24 22 14 22 C14 16.06 14 10.12 14 4 C9.38 4 4.76 4 0 4 C0 2.68 0 1.36 0 0 Z " fill="#000000" transform="translate(46,21)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -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 (
|
||||
<>
|
||||
<h1>Resultados Electorales - Provincia de Buenos Aires</h1>
|
||||
<main>
|
||||
<TickerWidget />
|
||||
<main className="space-y-6">
|
||||
<ResumenGeneralWidget />
|
||||
<hr className="border-gray-300" />
|
||||
<SenadoresTickerWidget />
|
||||
<hr className="border-gray-300" />
|
||||
<DiputadosTickerWidget />
|
||||
<hr className="border-gray-300" />
|
||||
<ConcejalesTickerWidget />
|
||||
<hr className="border-gray-300" />
|
||||
<DipSenTickerWidget />
|
||||
<hr className="border-gray-300" />
|
||||
<SenadoresPorSeccionWidget />
|
||||
<hr className="border-gray-300" />
|
||||
<DiputadosPorSeccionWidget />
|
||||
<hr className="border-gray-300" />
|
||||
<ConcejalesPorSeccionWidget />
|
||||
<hr className="border-gray-300" />
|
||||
<SenadoresWidget />
|
||||
<hr className="border-gray-300" />
|
||||
<DiputadosWidget />
|
||||
<hr className="border-gray-300" />
|
||||
<ConcejalesWidget />
|
||||
<hr className="border-gray-300" />
|
||||
<CongresoWidget />
|
||||
<hr className="border-gray-300" />
|
||||
<BancasWidget />
|
||||
<hr className="border-gray-300" />
|
||||
<MapaBsAs />
|
||||
<hr className="border-gray-300" />
|
||||
<MapaBsAsSecciones />
|
||||
<hr className="border-gray-300" />
|
||||
<TelegramaWidget />
|
||||
</main>
|
||||
</>
|
||||
|
||||
@@ -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<Proyeccion
|
||||
/**
|
||||
* Obtiene la lista de Secciones Electorales desde la API.
|
||||
*/
|
||||
export const getSeccionesElectorales = async (): Promise<MunicipioSimple[]> => {
|
||||
const response = await apiClient.get('/catalogos/secciones-electorales');
|
||||
export const getSeccionesElectorales = async (categoriaId?: number): Promise<MunicipioSimple[]> => {
|
||||
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<ConfiguracionPublica> =
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getResultadosConcejales = async (seccionId: string): Promise<ResultadoTicker[]> => {
|
||||
const response = await apiClient.get(`/resultados/concejales/${seccionId}`);
|
||||
export const getResultadosPorSeccion = async (seccionId: string, categoriaId: number): Promise<ApiResponseResultadosPorSeccion> => {
|
||||
const response = await apiClient.get(`/resultados/seccion-resultados/${seccionId}?categoriaId=${categoriaId}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
|
||||
@@ -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<MunicipioSimple[]>([]);
|
||||
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<ApiResponseResultadosPorSeccion>({
|
||||
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 (
|
||||
<div className="ticker-card">
|
||||
<div className="ticker-header">
|
||||
<h3>CONCEJALES POR SECCIÓN</h3>
|
||||
<Select
|
||||
options={seccionOptions}
|
||||
value={selectedSeccion}
|
||||
onChange={(option) => setSelectedSeccion(option)}
|
||||
isLoading={secciones.length === 0}
|
||||
placeholder="Seleccionar sección..."
|
||||
styles={customSelectStyles}
|
||||
/>
|
||||
</div>
|
||||
<div className="ticker-results">
|
||||
{(isLoadingResultados && selectedSeccion) && <p>Cargando...</p>}
|
||||
{!selectedSeccion && <p style={{textAlign: 'center', color: '#666'}}>Seleccione una sección.</p>}
|
||||
{displayResults.map(partido => (
|
||||
<div key={partido.id} className="ticker-party">
|
||||
<div className="party-logo">
|
||||
<ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc="/default-avatar.png" alt={`Logo de ${partido.nombre}`} />
|
||||
</div>
|
||||
<div className="party-details">
|
||||
<div className="party-info">
|
||||
<span className="party-name">{partido.nombreCorto || partido.nombre}</span>
|
||||
<span className="party-percent">{formatPercent(partido.porcentaje)}</span>
|
||||
</div>
|
||||
<div className="party-bar-background">
|
||||
<div className="party-bar-foreground" style={{ width: `${partido.porcentaje}%`, backgroundColor: partido.color || '#888' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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<CategoriaResumen[]>({
|
||||
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 <div className="ticker-card loading"><p>Cargando...</p></div>;
|
||||
if (error || !ConcejalesData) return <div className="ticker-card error"><p>Datos de Concejales no disponibles.</p></div>;
|
||||
|
||||
// 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 (
|
||||
<div className="ticker-card">
|
||||
<div className="ticker-header">
|
||||
<h3>RESUMEN DE {ConcejalesData.categoriaNombre}</h3>
|
||||
<div className="ticker-stats">
|
||||
<span>Mesas: <strong>{formatPercent(ConcejalesData.estadoRecuento?.mesasTotalizadasPorcentaje || 0)}</strong></span>
|
||||
<span>Part: <strong>{formatPercent(ConcejalesData.estadoRecuento?.participacionPorcentaje || 0)}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ticker-results">
|
||||
{displayResults.map(partido => (
|
||||
<div key={partido.id} className="ticker-party">
|
||||
<div className="party-logo">
|
||||
<ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc="/default-avatar.png" alt={`Logo de ${partido.nombre}`} />
|
||||
</div>
|
||||
<div className="party-details">
|
||||
<div className="party-info">
|
||||
<span className="party-name">{partido.nombreCorto || partido.nombre}</span>
|
||||
<span className="party-percent">{formatPercent(partido.porcentaje)}</span>
|
||||
</div>
|
||||
<div className="party-bar-background">
|
||||
<div className="party-bar-foreground" style={{ width: `${partido.porcentaje}%`, backgroundColor: partido.color || '#888' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
46
Elecciones-Web/frontend/src/components/DevApp.tsx
Normal file
46
Elecciones-Web/frontend/src/components/DevApp.tsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<h1 style={{ textAlign: 'center', fontFamily: 'sans-serif' }}>
|
||||
Showcase de Widgets - Elecciones 2025
|
||||
</h1>
|
||||
<main>
|
||||
<DipSenTickerWidget />
|
||||
<ResumenGeneralWidget />
|
||||
<SenadoresWidget />
|
||||
<DiputadosWidget />
|
||||
<ConcejalesWidget />
|
||||
<SenadoresTickerWidget />
|
||||
<DiputadosTickerWidget />
|
||||
<ConcejalesTickerWidget />
|
||||
<DiputadosPorSeccionWidget />
|
||||
<SenadoresPorSeccionWidget />
|
||||
<ConcejalesPorSeccionWidget />
|
||||
<CongresoWidget />
|
||||
<BancasWidget />
|
||||
<MapaBsAs />
|
||||
<MapaBsAsSecciones />
|
||||
<TelegramaWidget />
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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<CategoriaResumen[]>({
|
||||
queryKey: ['resumenProvincial'],
|
||||
queryFn: getResumenProvincial,
|
||||
@@ -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<MunicipioSimple[]>({
|
||||
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<ApiResponseResultadosPorSeccion>({
|
||||
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 (
|
||||
<div className="ticker-card">
|
||||
<div className="ticker-header">
|
||||
<h3>DIPUTADOS POR SECCIÓN</h3>
|
||||
<Select
|
||||
options={seccionOptions}
|
||||
value={selectedSeccion}
|
||||
onChange={(option) => setSelectedSeccion(option)}
|
||||
isLoading={isLoadingSecciones}
|
||||
placeholder="Seleccionar sección..."
|
||||
styles={customSelectStyles}
|
||||
/>
|
||||
</div>
|
||||
<div className="ticker-results">
|
||||
{(isLoadingResultados && selectedSeccion) && <p>Cargando...</p>}
|
||||
{!selectedSeccion && !isLoadingSecciones && <p style={{textAlign: 'center', color: '#666'}}>Seleccione una sección.</p>}
|
||||
{displayResults.map(partido => (
|
||||
<div key={partido.id} className="ticker-party">
|
||||
<div className="party-logo">
|
||||
<ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc="/default-avatar.png" alt={`Logo de ${partido.nombre}`} />
|
||||
</div>
|
||||
<div className="party-details">
|
||||
<div className="party-info">
|
||||
<span className="party-name">{partido.nombreCorto || partido.nombre}</span>
|
||||
<span className="party-percent">{formatPercent(partido.porcentaje)}</span>
|
||||
</div>
|
||||
<div className="party-bar-background">
|
||||
<div className="party-bar-foreground" style={{ width: `${partido.porcentaje}%`, backgroundColor: partido.color || '#888' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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<CategoriaResumen[]>({
|
||||
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 <div className="ticker-card loading"><p>Cargando...</p></div>;
|
||||
if (error || !diputadosData) return <div className="ticker-card error"><p>Datos de Diputados no disponibles.</p></div>;
|
||||
|
||||
// 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 (
|
||||
<div className="ticker-card">
|
||||
<div className="ticker-header">
|
||||
<h3>RESUMEN DE {diputadosData.categoriaNombre}</h3>
|
||||
<div className="ticker-stats">
|
||||
<span>Mesas: <strong>{formatPercent(diputadosData.estadoRecuento?.mesasTotalizadasPorcentaje || 0)}</strong></span>
|
||||
<span>Part: <strong>{formatPercent(diputadosData.estadoRecuento?.participacionPorcentaje || 0)}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ticker-results">
|
||||
{displayResults.map(partido => (
|
||||
<div key={partido.id} className="ticker-party">
|
||||
<div className="party-logo">
|
||||
<ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc="/default-avatar.png" alt={`Logo de ${partido.nombre}`} />
|
||||
</div>
|
||||
<div className="party-details">
|
||||
<div className="party-info">
|
||||
<span className="party-name">{partido.nombreCorto || partido.nombre}</span>
|
||||
<span className="party-percent">{formatPercent(partido.porcentaje)}</span>
|
||||
</div>
|
||||
<div className="party-bar-background">
|
||||
<div className="party-bar-foreground" style={{ width: `${partido.porcentaje}%`, backgroundColor: partido.color || '#888' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
117
Elecciones-Web/frontend/src/components/ResumenGeneralWidget.tsx
Normal file
117
Elecciones-Web/frontend/src/components/ResumenGeneralWidget.tsx
Normal file
@@ -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<CategoriaResumen[]>({
|
||||
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<string, Omit<ResultadoTicker, 'porcentaje'>>();
|
||||
|
||||
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 <div className="ticker-card loading" style={{ gridColumn: '1 / -1' }}>Cargando resumen general...</div>;
|
||||
if (error || !aggregatedData) return <div className="ticker-card error" style={{ gridColumn: '1 / -1' }}>No hay datos para el resumen general.</div>;
|
||||
|
||||
// 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 (
|
||||
<div className="ticker-card" style={{ gridColumn: '1 / -1' }}>
|
||||
<div className="ticker-header">
|
||||
<h3>RESUMEN LEGISLATIVO PROVINCIAL</h3>
|
||||
<div className="ticker-stats">
|
||||
<span>Mesas (Prom.): <strong>{formatPercent(aggregatedData.estadoRecuento.mesasTotalizadasPorcentaje)}</strong></span>
|
||||
<span>Part (Prom.): <strong>{formatPercent(aggregatedData.estadoRecuento.participacionPorcentaje)}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ticker-results">
|
||||
{displayResults.map(partido => (
|
||||
<div key={partido.id} className="ticker-party">
|
||||
<div className="party-logo">
|
||||
<ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc="/default-avatar.png" alt={`Logo de ${partido.nombre}`} />
|
||||
</div>
|
||||
<div className="party-details">
|
||||
<div className="party-info">
|
||||
<span className="party-name">{partido.nombreCorto || partido.nombre}</span>
|
||||
<span className="party-percent">{formatPercent(partido.porcentaje)}</span>
|
||||
</div>
|
||||
<div className="party-bar-background">
|
||||
<div className="party-bar-foreground" style={{ width: `${partido.porcentaje}%`, backgroundColor: partido.color || '#888' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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<MunicipioSimple[]>({
|
||||
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<ApiResponseResultadosPorSeccion>({
|
||||
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 (
|
||||
<div className="ticker-card">
|
||||
<div className="ticker-header">
|
||||
<h3>SENADORES POR SECCIÓN</h3>
|
||||
<Select
|
||||
options={seccionOptions}
|
||||
value={selectedSeccion}
|
||||
onChange={(option) => setSelectedSeccion(option)}
|
||||
isLoading={isLoadingSecciones}
|
||||
placeholder="Seleccionar sección..."
|
||||
styles={customSelectStyles}
|
||||
/>
|
||||
</div>
|
||||
<div className="ticker-results">
|
||||
{(isLoadingResultados && selectedSeccion) && <p>Cargando...</p>}
|
||||
{!selectedSeccion && !isLoadingSecciones && <p style={{textAlign: 'center', color: '#666'}}>Seleccione una sección.</p>}
|
||||
{displayResults.map(partido => (
|
||||
<div key={partido.id} className="ticker-party">
|
||||
<div className="party-logo">
|
||||
<ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc="/default-avatar.png" alt={`Logo de ${partido.nombre}`} />
|
||||
</div>
|
||||
<div className="party-details">
|
||||
<div className="party-info">
|
||||
<span className="party-name">{partido.nombreCorto || partido.nombre}</span>
|
||||
<span className="party-percent">{formatPercent(partido.porcentaje)}</span>
|
||||
</div>
|
||||
<div className="party-bar-background">
|
||||
<div className="party-bar-foreground" style={{ width: `${partido.porcentaje}%`, backgroundColor: partido.color || '#888' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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<CategoriaResumen[]>({
|
||||
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 <div className="ticker-card loading"><p>Cargando...</p></div>;
|
||||
if (error || !senadoresData) return <div className="ticker-card error"><p>Datos de Senadores no disponibles.</p></div>;
|
||||
|
||||
// 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 (
|
||||
<div className="ticker-card">
|
||||
<div className="ticker-header">
|
||||
<h3>RESUMEN DE {senadoresData.categoriaNombre}</h3>
|
||||
<div className="ticker-stats">
|
||||
<span>Mesas: <strong>{formatPercent(senadoresData.estadoRecuento?.mesasTotalizadasPorcentaje || 0)}</strong></span>
|
||||
<span>Part: <strong>{formatPercent(senadoresData.estadoRecuento?.participacionPorcentaje || 0)}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ticker-results">
|
||||
{displayResults.map(partido => (
|
||||
<div key={partido.id} className="ticker-party">
|
||||
<div className="party-logo">
|
||||
<ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc="/default-avatar.png" alt={`Logo de ${partido.nombre}`} />
|
||||
</div>
|
||||
<div className="party-details">
|
||||
<div className="party-info">
|
||||
<span className="party-name">{partido.nombreCorto || partido.nombre}</span>
|
||||
<span className="party-percent">{formatPercent(partido.porcentaje)}</span>
|
||||
</div>
|
||||
<div className="party-bar-background">
|
||||
<div className="party-bar-foreground" style={{ width: `${partido.porcentaje}%`, backgroundColor: partido.color || '#888' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
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<string, React.ElementType> = {
|
||||
'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(
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
<DevApp />
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
</React.StrictMode>
|
||||
);
|
||||
} 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(
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<WidgetComponent />
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
(window as any).EleccionesWidgets = {
|
||||
render: renderWidgets
|
||||
};
|
||||
}
|
||||
@@ -106,3 +106,8 @@ export interface CatalogoItem {
|
||||
id: string;
|
||||
nombre: string;
|
||||
}
|
||||
|
||||
export interface ApiResponseResultadosPorSeccion {
|
||||
ultimaActualizacion: string;
|
||||
resultados: ResultadoTicker[];
|
||||
}
|
||||
@@ -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/, '')
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -63,21 +63,40 @@ public class CatalogosController : ControllerBase
|
||||
}
|
||||
|
||||
[HttpGet("secciones-electorales")]
|
||||
public async Task<IActionResult> GetSeccionesElectorales()
|
||||
public async Task<IActionResult> 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}
|
||||
.Where(a => a.NivelId == 20); // Nivel 20 = Sección Electoral
|
||||
|
||||
// Si NO se proporciona una categoriaId, devolvemos todas las secciones.
|
||||
if (categoriaId == null)
|
||||
{
|
||||
Id = a.SeccionProvincialId!, // El ID que usaremos es el SeccionProvincialId
|
||||
Nombre = a.Nombre
|
||||
})
|
||||
.OrderBy(s => s.Nombre)
|
||||
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
|
||||
|
||||
@@ -531,8 +531,8 @@ public class ResultadosController : ControllerBase
|
||||
return Ok(configuracionPublica);
|
||||
}
|
||||
|
||||
[HttpGet("concejales/{seccionId}")]
|
||||
public async Task<IActionResult> GetResultadosConcejalesPorSeccion(string seccionId)
|
||||
[HttpGet("seccion-resultados/{seccionId}")]
|
||||
public async Task<IActionResult> 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")]
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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":{}}
|
||||
{"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":{}}
|
||||
@@ -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":{}}
|
||||
{"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":{}}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?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>
|
||||
@@ -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")]
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
# ¡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;
|
||||
|
||||
# Manejo de peticiones pre-vuelo (preflight) OPTIONS.
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user