Feat Widgets

- Widget de Home
- Widget Cards por Provincias
- Widget Mapa por Categorias
This commit is contained in:
2025-10-01 10:03:01 -03:00
parent 3b0eee25e6
commit a985cbfd7c
45 changed files with 1786 additions and 953 deletions

View File

@@ -21,11 +21,13 @@
"react": "^19.1.1",
"react-circular-progressbar": "^2.2.0",
"react-dom": "^19.1.1",
"react-hot-toast": "^2.6.0",
"react-icons": "^5.5.0",
"react-pdf": "^10.1.0",
"react-select": "^5.10.2",
"react-simple-maps": "github:ozimmortal/react-simple-maps#feat/react-19-support",
"react-tooltip": "^5.29.1",
"swiper": "^12.0.2",
"topojson-client": "^3.1.0",
"vite-plugin-svgr": "^4.5.0"
},
@@ -3962,6 +3964,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/goober": {
"version": "2.1.16",
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz",
"integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==",
"license": "MIT",
"peerDependencies": {
"csstype": "^3.0.10"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -4745,6 +4756,23 @@
"react": "^19.1.1"
}
},
"node_modules/react-hot-toast": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
"integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==",
"license": "MIT",
"dependencies": {
"csstype": "^3.1.3",
"goober": "^2.1.16"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/react-icons": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
@@ -5120,6 +5148,25 @@
"integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==",
"license": "MIT"
},
"node_modules/swiper": {
"version": "12.0.2",
"resolved": "https://registry.npmjs.org/swiper/-/swiper-12.0.2.tgz",
"integrity": "sha512-y8F6fDGXmTVVgwqJj6I00l4FdGuhpFJn0U/9Ucn1MwWOw3NdLV8aH88pZOjyhBgU/6PyBlUx+JuAQ5KMWz906Q==",
"funding": [
{
"type": "patreon",
"url": "https://www.patreon.com/swiperjs"
},
{
"type": "open_collective",
"url": "http://opencollective.com/swiper"
}
],
"license": "MIT",
"engines": {
"node": ">= 4.7.0"
}
},
"node_modules/terser": {
"version": "5.43.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",

View File

@@ -23,11 +23,13 @@
"react": "^19.1.1",
"react-circular-progressbar": "^2.2.0",
"react-dom": "^19.1.1",
"react-hot-toast": "^2.6.0",
"react-icons": "^5.5.0",
"react-pdf": "^10.1.0",
"react-select": "^5.10.2",
"react-simple-maps": "github:ozimmortal/react-simple-maps#feat/react-19-support",
"react-tooltip": "^5.29.1",
"swiper": "^12.0.2",
"topojson-client": "^3.1.0",
"vite-plugin-svgr": "^4.5.0"
},

View File

@@ -1,9 +1,12 @@
// src/apiService.ts
import axios from 'axios';
import type { ApiResponseRankingMunicipio, ApiResponseRankingSeccion,
ApiResponseTablaDetallada, ProyeccionBancas, MunicipioSimple,
TelegramaData, CatalogoItem, CategoriaResumen, ResultadoTicker,
ApiResponseResultadosPorSeccion, PanelElectoralDto, ResumenProvincia } from './types/types';
import type {
ApiResponseRankingMunicipio, ApiResponseRankingSeccion,
ApiResponseTablaDetallada, ProyeccionBancas, MunicipioSimple,
TelegramaData, CatalogoItem, CategoriaResumen, ResultadoTicker,
ApiResponseResultadosPorSeccion, PanelElectoralDto, ResumenProvincia,
CategoriaResumenHome
} from './types/types';
/**
* URL base para las llamadas a la API.
@@ -76,7 +79,6 @@ export interface BancadaDetalle {
export interface ConfiguracionPublica {
TickerResultadosCantidad?: string;
ConcejalesResultadosCantidad?: string;
// ... otras claves públicas que pueda añadir en el futuro
}
export interface ResultadoDetalleSeccion {
@@ -88,29 +90,35 @@ export interface ResultadoDetalleSeccion {
}
export interface PartidoComposicionNacional {
id: string;
nombre: string;
nombreCorto: string | null;
color: string | null;
bancasFijos: number;
bancasGanadas: number;
bancasTotales: number;
ordenDiputadosNacionales: number | null;
ordenSenadoresNacionales: number | null;
id: string;
nombre: string;
nombreCorto: string | null;
color: string | null;
bancasFijos: number;
bancasGanadas: number;
bancasTotales: number;
ordenDiputadosNacionales: number | null;
ordenSenadoresNacionales: number | null;
}
export interface CamaraComposicionNacional {
camaraNombre: string;
totalBancas: number;
bancasEnJuego: number;
partidos: PartidoComposicionNacional[];
presidenteBancada: { color: string | null; tipoBanca: 'ganada' | 'previa' | null } | null;
ultimaActualizacion: string;
camaraNombre: string;
totalBancas: number;
bancasEnJuego: number;
partidos: PartidoComposicionNacional[];
presidenteBancada: { color: string | null; tipoBanca: 'ganada' | 'previa' | null } | null;
ultimaActualizacion: string;
}
export interface ComposicionNacionalData {
diputados: CamaraComposicionNacional;
senadores: CamaraComposicionNacional;
diputados: CamaraComposicionNacional;
senadores: CamaraComposicionNacional;
}
export interface ResumenParams {
focoDistritoId?: string;
focoCategoriaId?: number;
cantidadResultados?: number;
}
export const getResumenProvincial = async (eleccionId: number): Promise<CategoriaResumen[]> => {
@@ -239,27 +247,55 @@ export const getEstablecimientosPorMunicipio = async (municipioId: string): Prom
};
export const getPanelElectoral = async (eleccionId: number, ambitoId: string | null, categoriaId: number): Promise<PanelElectoralDto> => {
// Construimos la URL base
let url = ambitoId
? `/elecciones/${eleccionId}/panel/${ambitoId}`
: `/elecciones/${eleccionId}/panel`;
// Añadimos categoriaId como un query parameter
url += `?categoriaId=${categoriaId}`;
const { data } = await apiClient.get(url);
return data;
// Construimos la URL base
let url = ambitoId
? `/elecciones/${eleccionId}/panel/${ambitoId}`
: `/elecciones/${eleccionId}/panel`;
// Añadimos categoriaId como un query parameter
url += `?categoriaId=${categoriaId}`;
const { data } = await apiClient.get(url);
return data;
};
export const getComposicionNacional = async (eleccionId: number): Promise<ComposicionNacionalData> => {
const { data } = await apiClient.get(`/elecciones/${eleccionId}/composicion-nacional`);
return data;
const { data } = await apiClient.get(`/elecciones/${eleccionId}/composicion-nacional`);
return data;
};
// 11. Endpoint para el widget de tarjetas nacionales
export const getResumenPorProvincia = async (eleccionId: number): Promise<ResumenProvincia[]> => {
// Usamos el cliente público ya que son datos de resultados
const { data } = await apiClient.get(`/elecciones/${eleccionId}/resumen-por-provincia`);
export const getResumenPorProvincia = async (eleccionId: number, params: ResumenParams = {}): Promise<ResumenProvincia[]> => {
// Usamos URLSearchParams para construir la query string de forma segura y limpia
const queryParams = new URLSearchParams();
if (params.focoDistritoId) {
queryParams.append('focoDistritoId', params.focoDistritoId);
}
if (params.focoCategoriaId) {
queryParams.append('focoCategoriaId', params.focoCategoriaId.toString());
}
if (params.cantidadResultados) {
queryParams.append('cantidadResultados', params.cantidadResultados.toString());
}
const queryString = queryParams.toString();
// Añadimos la query string a la URL solo si tiene contenido
const url = `/elecciones/${eleccionId}/resumen-por-provincia${queryString ? `?${queryString}` : ''}`;
const { data } = await apiClient.get(url);
return data;
};
export const getHomeResumen = async (eleccionId: number, distritoId: string, categoriaId: number): Promise<CategoriaResumenHome> => {
const queryParams = new URLSearchParams({
eleccionId: eleccionId.toString(),
distritoId: distritoId,
categoriaId: categoriaId.toString(),
});
const url = `/elecciones/home-resumen?${queryParams.toString()}`;
const { data } = await apiClient.get(url);
return data;
};

View File

@@ -38,7 +38,7 @@ export const DevApp = () => {
<DiputadosPorSeccionWidget />
<SenadoresPorSeccionWidget />
<ConcejalesPorSeccionWidget />
<CongresoWidget />
<CongresoWidget eleccionId={1} />
<BancasWidget />
<MapaBsAs />
<MapaBsAsSecciones />

View File

@@ -1,14 +1,145 @@
// src/features/legislativas/rovinciales/DevAppLegislativas.tsx
// src/features/legislativas/nacionales/DevAppLegislativas.tsx
import { useState } from 'react'; // <-- Importar useState
import { ResultadosNacionalesCardsWidget } from './nacionales/ResultadosNacionalesCardsWidget';
import { CongresoNacionalWidget } from './nacionales/CongresoNacionalWidget';
import { PanelNacionalWidget } from './nacionales/PanelNacionalWidget';
import { HomeCarouselWidget } from './nacionales/HomeCarouselWidget';
import './DevAppStyle.css'
// --- NUEVO COMPONENTE REUTILIZABLE PARA CONTENIDO COLAPSABLE ---
const CollapsibleWidgetWrapper = ({ children }: { children: React.ReactNode }) => {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div className="collapsible-container">
<div className={`collapsible-content ${isExpanded ? 'expanded' : ''}`}>
{children}
</div>
<button className="toggle-button" onClick={() => setIsExpanded(!isExpanded)}>
{isExpanded ? 'Mostrar Menos' : 'Mostrar Más'}
</button>
</div>
);
};
export const DevAppLegislativas = () => {
// Estilos para los separadores y descripciones para mejorar la legibilidad
const sectionStyle = {
border: '2px solid #007bff',
borderRadius: '8px',
padding: '1rem 2rem',
marginTop: '3rem',
marginBottom: '3rem',
backgroundColor: '#f8f9fa'
};
const descriptionStyle = {
fontFamily: 'sans-serif',
color: '#333',
lineHeight: 1.6
};
const codeStyle = {
backgroundColor: '#e9ecef',
padding: '2px 6px',
borderRadius: '4px',
fontFamily: 'Roboto'
};
return (
<div className="container">
<h1>Visor de Widgets</h1>
<ResultadosNacionalesCardsWidget eleccionId={2} />
<div style={sectionStyle}>
<h2>Widget: Carrusel de Resultados (Home)</h2>
<p style={descriptionStyle}>
Uso: <code style={codeStyle}>&lt;HomeCarouselWidget eleccionId={2} distritoId="02" categoriaId={2} titulo="Diputados - Provincia de Buenos Aires" /&gt;</code>
</p>
<HomeCarouselWidget
eleccionId={2} // Nacional
distritoId="02" // Buenos Aires
categoriaId={2} // Diputados Nacionales
titulo="Diputados - Provincia de Buenos Aires"
/>
</div>
{/* --- SECCIÓN PARA EL WIDGET DE TARJETAS CON EJEMPLOS --- */}
<div style={sectionStyle}>
<h2>Widget: Resultados por Provincia (Tarjetas)</h2>
<hr />
<h3 style={{ marginTop: '2rem' }}>1. Vista por Defecto</h3>
<p style={descriptionStyle}>
Sin parámetros adicionales. Muestra todas las provincias, con sus categorías correspondientes (Diputados para las 24, Senadores para las 8 que renuevan). Muestra los 2 principales partidos por defecto.
<br />
Uso: <code style={codeStyle}>&lt;ResultadosNacionalesCardsWidget eleccionId={2} /&gt;</code>
</p>
<CollapsibleWidgetWrapper>
<ResultadosNacionalesCardsWidget eleccionId={2} />
</CollapsibleWidgetWrapper>
<hr style={{ marginTop: '2rem' }} />
<h3 style={{ marginTop: '2rem' }}>2. Filtrado por Provincia (focoDistritoId)</h3>
<p style={descriptionStyle}>
Muestra únicamente la tarjeta de una provincia específica. Ideal para páginas de noticias locales. El ID de distrito ("02" para Bs. As., "06" para Chaco) se pasa como prop.
<br />
Ejemplo Buenos Aires: <code style={codeStyle}>&lt;ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="02" /&gt;</code>
</p>
<ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="02" />
<p style={{ ...descriptionStyle, marginTop: '2rem' }}>
Ejemplo Chaco (que también renueva Senadores): <code style={codeStyle}>&lt;ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="06" /&gt;</code>
</p>
<ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="06" />
<hr style={{ marginTop: '2rem' }} />
<h3 style={{ marginTop: '2rem' }}>3. Filtrado por Categoría (focoCategoriaId)</h3>
<p style={descriptionStyle}>
Muestra todas las provincias que votan para una categoría específica.
<br />
Ejemplo Senadores (ID 1): <code style={codeStyle}>&lt;ResultadosNacionalesCardsWidget eleccionId={2} focoCategoriaId={1} /&gt;</code>
</p>
<ResultadosNacionalesCardsWidget eleccionId={2} focoCategoriaId={1} />
<hr style={{ marginTop: '2rem' }} />
<h3 style={{ marginTop: '2rem' }}>4. Indicando Cantidad de Resultados (cantidadResultados)</h3>
<p style={descriptionStyle}>
Controla cuántos partidos se muestran en cada categoría. Por defecto son 2.
<br />
Ejemplo mostrando el TOP 3 de cada categoría: <code style={codeStyle}>&lt;ResultadosNacionalesCardsWidget eleccionId={2} cantidadResultados={3} /&gt;</code>
</p>
<CollapsibleWidgetWrapper>
<ResultadosNacionalesCardsWidget eleccionId={2} cantidadResultados={3} />
</CollapsibleWidgetWrapper>
<hr style={{ marginTop: '2rem' }} />
<h3 style={{ marginTop: '2rem' }}>5. Mostrando las Bancas (mostrarBancas)</h3>
<p style={descriptionStyle}>
Útil para contextos donde importan las bancas. La prop <code style={codeStyle}>mostrarBancas</code> se establece en <code style={codeStyle}>true</code>.
<br />
Ejemplo en Tierra del Fuego: <code style={codeStyle}>&lt;ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="23" mostrarBancas={true} /&gt;</code>
</p>
<ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="23" mostrarBancas={true} />
<hr style={{ marginTop: '2rem' }} />
<h3 style={{ marginTop: '2rem' }}>6. Combinación de Parámetros</h3>
<p style={descriptionStyle}>
Se pueden combinar todos los parámetros para vistas muy específicas.
<br />
Ejemplo: Mostrar el TOP 1 (el ganador) para la categoría de SENADORES en la provincia de RÍO NEGRO (Distrito ID "16").
<br />
Uso: <code style={codeStyle}>&lt;ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="16" focoCategoriaId={1} cantidadResultados={1} /&gt;</code>
</p>
<ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="16" focoCategoriaId={1} cantidadResultados={1} />
</div>
{/* --- OTROS WIDGETS --- */}
<CongresoNacionalWidget eleccionId={2} />
<PanelNacionalWidget eleccionId={2} />
</div>

View File

@@ -1,3 +1,50 @@
.container{
text-align: center;
}
/* --- ESTILOS PARA EL CONTENEDOR COLAPSABLE --- */
.collapsible-container {
position: relative;
padding-bottom: 50px; /* Espacio para el botón de expandir */
}
.collapsible-content {
max-height: 950px; /* Altura suficiente para 2 filas de tarjetas (aprox) */
overflow: hidden;
transition: max-height 0.7s ease-in-out;
position: relative;
}
.collapsible-content.expanded {
max-height: 100%; /* Un valor grande para asegurar que todo el contenido sea visible */
}
/* Pseudo-elemento para crear un degradado y sugerir que hay más contenido */
.collapsible-content:not(.expanded)::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 150px;
background: linear-gradient(to top, rgba(248, 249, 250, 1) 20%, rgba(248, 249, 250, 0));
pointer-events: none; /* Permite hacer clic a través del degradado */
}
.toggle-button {
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
font-size: 1rem;
font-weight: bold;
color: #fff;
background-color: #007bff;
border: none;
border-radius: 20px;
cursor: pointer;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
z-index: 2;
}

View File

@@ -0,0 +1,239 @@
/* src/features/legislativas/nacionales/HomeCarouselWidget.css */
.home-carousel-widget {
--primary-text: #212529;
--secondary-text: #6c757d;
--border-color: #dee2e6;
--background-light: #f8f9fa;
--background-white: #ffffff;
--shadow: 0 2px 8px rgba(0, 0, 0, 0.07);
--font-family-sans: "Roboto", system-ui, sans-serif;
}
.home-carousel-widget {
font-family: var(--font-family-sans);
background-color: var(--background-white);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 0.75rem;
max-width: 1200px;
margin: 2rem auto;
}
.widget-title {
font-size: 1.2rem;
font-weight: 900;
color: var(--primary-text);
margin: 0 0 0.5rem 0;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border-color);
text-align: left;
}
.top-stats-bar {
display: flex;
justify-content: space-around;
background-color: transparent;
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 0.3rem 0.5rem;
margin-bottom: 0.5rem;
}
.top-stats-bar > div {
display: flex;
align-items: baseline;
gap: 0.5rem;
border-right: 1px solid var(--border-color);
padding: 0 0.5rem;
flex-grow: 1;
justify-content: center;
}
.top-stats-bar > div:last-child { border-right: none; }
.top-stats-bar span { font-size: 0.9rem; color: var(--secondary-text); }
.top-stats-bar strong { font-size: 0.9rem; font-weight: 600; color: var(--primary-text); }
.candidate-card {
display: flex;
align-items: center;
gap: 0.75rem;
background: var(--background-white);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 0.75rem;
box-shadow: var(--shadow);
height: 100%;
border-left: 5px solid;
border-left-color: var(--candidate-color, #ccc);
position: relative;
}
.candidate-photo-wrapper {
flex-shrink: 0;
width: 60px;
height: 60px;
border-radius: 8px;
overflow: hidden;
background-color: var(--candidate-color, #e9ecef);
}
.candidate-photo {
width: 100%;
height: 100%;
object-fit: cover;
box-sizing: border-box;
}
.candidate-details {
flex-grow: 1;
display: flex;
justify-content: space-between;
align-items: center;
min-width: 0;
}
.candidate-info {
display: flex;
flex-direction: column;
justify-content: center;
align-items:flex-start;
gap: 0.1rem;
min-width: 0;
margin-right: 0.75rem;
}
.candidate-name, .party-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
width: 100%;
}
.candidate-name {
font-size: 0.95rem;
text-align: left;
font-weight: 700;
color: var(--primary-text);
}
.party-name {
font-size: 0.8rem;
text-align: left;
text-transform: uppercase;
color: var(--secondary-text);
text-transform: uppercase;
}
.candidate-results { text-align: right; flex-shrink: 0; }
.percentage {
display: block;
font-size: 1.2rem;
font-weight: 700;
color: var(--primary-text);
line-height: 1.1;
}
.votes {
font-size: 0.75rem;
color: var(--secondary-text);
white-space: nowrap;
}
.swiper-slide:not(:last-child) .candidate-card::after {
content: '';
position: absolute;
right: -8px;
top: 20%;
bottom: 20%;
width: 1px;
background-color: var(--border-color);
}
.swiper-button-prev, .swiper-button-next {
width: 30px; height: 30px;
background-color: rgba(255, 255, 255, 0.9);
border: 1px solid var(--border-color);
border-radius: 50%;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
transition: opacity 0.2s;
color: var(--secondary-text);
}
.swiper-button-prev:after, .swiper-button-next:after {
font-size: 18px;
font-weight: bold;
}
.swiper-button-prev { left: -10px; }
.swiper-button-next { right: -10px; }
.swiper-button-disabled { opacity: 0; pointer-events: none; }
.widget-footer {
text-align: right;
font-size: 0.75rem;
color: var(--secondary-text);
margin-top: 0.5rem;
}
.short-text {
display: none; /* Oculto por defecto en la vista de escritorio */
}
/* --- INICIO DE LA SECCIÓN DE ESTILOS PARA MÓVIL --- */
@media (max-width: 768px) {
.home-carousel-widget {
padding: 0.75rem;
}
/* 1. Centrar el título en móvil */
.widget-title {
text-align: center;
font-size: 1.1rem;
}
/* 2. Reestructurar la barra de estadísticas a 2x2 y usar textos cortos */
.top-stats-bar {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.2rem;
padding: 0.3rem;
}
.top-stats-bar > div {
padding: 0.25rem 0.5rem;
border-right: none; /* Quitar todos los bordes derechos */
}
.top-stats-bar > div:nth-child(odd) {
border-right: 1px solid var(--border-color); /* Restablecer borde solo para la columna izquierda */
}
/* Lógica de visibilidad de textos */
.long-text {
display: none; /* Ocultar el texto largo en móvil */
}
.short-text {
display:inline; /* Mostrar el texto corto en móvil */
}
/* Reducir fuentes para que quepan mejor */
.top-stats-bar span { font-size: 0.8rem; text-align: left; }
.top-stats-bar strong { font-size: 0.85rem; text-align: right;}
/* --- Botones del Carrusel (sin cambios) --- */
.swiper-button-prev, .swiper-button-next {
width: 32px;
height: 32px;
top: 45%;
}
.swiper-button-prev { left: 2px; }
.swiper-button-next { right: 2px; }
/* --- Ajustes en la tarjeta (sin cambios) --- */
.candidate-card { gap: 0.5rem; padding: 0.5rem; }
.candidate-photo-wrapper { width: 50px; height: 50px; }
.candidate-name { font-size: 0.9rem; }
.percentage { font-size: 1.1rem; }
.votes { font-size: 0.7rem; }
/* 3. Centrar el footer en móvil */
.widget-footer {
text-align: center;
}
}

View File

@@ -0,0 +1,135 @@
// src/features/legislativas/nacionales/HomeCarouselWidget.tsx
import { useQuery } from '@tanstack/react-query';
import { getHomeResumen } from '../../../apiService';
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
import { assetBaseUrl } from '../../../apiService';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Navigation, A11y } from 'swiper/modules';
// @ts-ignore
import 'swiper/css';
// @ts-ignore
import 'swiper/css/navigation';
import './HomeCarouselWidget.css';
interface Props {
eleccionId: number;
distritoId: string;
categoriaId: number;
titulo: string;
}
const formatPercent = (num: number | null | undefined) => `${(num || 0).toFixed(2).replace('.', ',')}%`;
const formatNumber = (num: number) => num.toLocaleString('es-AR');
// --- Lógica de formateo de fecha ---
const formatDateTime = (dateString: string | undefined | null) => {
if (!dateString) return '...';
try {
const date = new Date(dateString);
// Verificar si la fecha es válida
if (isNaN(date.getTime())) {
return dateString; // Si no se puede parsear, devolver el string original
}
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${day}/${month}/${year}, ${hours}:${minutes} hs.`;
} catch (e) {
return dateString; // En caso de cualquier error, devolver el string original
}
};
export const HomeCarouselWidget = ({ eleccionId, distritoId, categoriaId, titulo }: Props) => {
const { data, isLoading, error } = useQuery({
queryKey: ['homeResumen', eleccionId, distritoId, categoriaId],
queryFn: () => getHomeResumen(eleccionId, distritoId, categoriaId),
});
if (isLoading) return <div>Cargando widget...</div>;
if (error || !data) return <div>No se pudieron cargar los datos.</div>;
return (
<div className="home-carousel-widget">
<h2 className="widget-title">{titulo}</h2>
<div className="top-stats-bar">
<div>
<span>Participación</span>
<strong>{formatPercent(data.estadoRecuento?.participacionPorcentaje)}</strong>
</div>
<div>
<span className="long-text">Mesas escrutadas</span>
<span className="short-text">Escrutado</span>
<strong>{formatPercent(data.estadoRecuento?.mesasTotalizadasPorcentaje)}</strong>
</div>
<div>
<span className="long-text">Votos en blanco</span>
<span className="short-text">En blanco</span>
<strong>{formatPercent(data.votosEnBlancoPorcentaje)}</strong>
</div>
<div>
<span className="long-text">Votos totales</span>
<span className="short-text">Votos</span>
<strong>{formatNumber(data.votosTotales)}</strong>
</div>
</div>
<Swiper
modules={[Navigation, A11y]}
spaceBetween={16}
slidesPerView={1.15}
navigation
breakpoints={{ 640: { slidesPerView: 2 }, 1024: { slidesPerView: 3 }, 1200: { slidesPerView: 3.5 } }} // Añadir breakpoint
>
{data.resultados.map(candidato => (
<SwiperSlide key={candidato.agrupacionId}>
<div className="candidate-card" style={{ '--candidate-color': candidato.color || '#ccc' } as React.CSSProperties}>
<div className="candidate-photo-wrapper">
<ImageWithFallback
src={candidato.fotoUrl ?? undefined}
fallbackSrc={`${assetBaseUrl}/default-avatar.png`}
alt={candidato.nombreCandidato ?? ''}
className="candidate-photo"
/>
</div>
<div className="candidate-details">
<div className="candidate-info">
{candidato.nombreCandidato ? (
// CASO 1: Hay un candidato (se muestran dos líneas)
<>
<span className="candidate-name">
{candidato.nombreCandidato}
</span>
<span className="party-name">
{candidato.nombreCortoAgrupacion || candidato.nombreAgrupacion}
</span>
</>
) : (
// CASO 2: No hay candidato (se muestra solo una línea)
<span className="candidate-name">
{candidato.nombreCortoAgrupacion || candidato.nombreAgrupacion}
</span>
)}
</div>
<div className="candidate-results">
<span className="percentage">{formatPercent(candidato.porcentaje)}</span>
<span className="votes">{formatNumber(candidato.votos)} votos</span>
</div>
</div>
</div>
</SwiperSlide>
))}
</Swiper>
<div className="widget-footer">
Última actualización: {formatDateTime(data.ultimaActualizacion)}
</div>
</div>
);
};

View File

@@ -22,14 +22,9 @@
/* Contenedor para alinear título y selector */
.header-top-row {
display: flex;
justify-content: space-between;
justify-content: flex-start; /* Alinea los items al inicio */
align-items: center;
margin-bottom: 0.5rem;
}
.panel-header h1 {
font-size: 1.5rem;
margin: 0;
gap: 2rem; /* Añade un espacio de separación de 2rem entre el selector y el breadcrumb */
}
.categoria-selector {
@@ -188,6 +183,7 @@
padding: 1rem 0;
border-bottom: 1px solid #f0f0f0;
border-left: 5px solid;
border-radius: 12px;
padding-left: 1rem;
}
@@ -227,18 +223,25 @@
.partido-info-wrapper {
/* Ocupa el espacio disponible a la izquierda */
min-width: 0;
text-align: left;
}
.partido-nombre {
font-weight: 800;
font-weight: 700;
font-size: 1.05rem;
color: #212529;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.2;
}
.candidato-nombre {
font-size: 0.85rem;
color: #666;
font-size: 0.8rem;
color: #6c757d;
text-transform: uppercase;
font-weight: 500;
line-height: 1.1;
}
.partido-stats {
@@ -381,10 +384,13 @@
}
.rsm-geography:not(.selected):hover {
filter: brightness(1.25); /* Mantenemos el brillo */
stroke: #ffffff; /* Color del borde a blanco */
filter: brightness(1.25);
/* Mantenemos el brillo */
stroke: #ffffff;
/* Color del borde a blanco */
stroke-width: 0.25px;
paint-order: stroke; /* Asegura que el borde se dibuje encima del relleno */
paint-order: stroke;
/* Asegura que el borde se dibuje encima del relleno */
}
.rsm-geography.selected {
@@ -492,8 +498,10 @@
/* --- NUEVOS ESTILOS PARA EL TOGGLE MÓVIL --- */
.mobile-view-toggle {
display: none;
position: absolute; /* <-- CAMBIO: De 'fixed' a 'absolute' */
bottom: 10px; /* <-- AJUSTE: Menos espacio desde abajo */
position: absolute;
/* <-- CAMBIO: De 'fixed' a 'absolute' */
bottom: 10px;
/* <-- AJUSTE: Menos espacio desde abajo */
left: 50%;
transform: translateX(-50%);
z-index: 100;
@@ -685,6 +693,14 @@
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem 0;
border-bottom: 1px solid #f0f0f0;
border-left: 5px solid;
/* Grosor del borde */
border-radius: 12px;
/* Redondeamos las esquinas */
padding-left: 1rem;
/* Espacio a la izquierda */
}
.partido-logo {

View File

@@ -10,6 +10,7 @@ import Select from 'react-select';
import type { PanelElectoralDto } from '../../../types/types';
import { FiMap, FiList } from 'react-icons/fi';
import { useMediaQuery } from './hooks/useMediaQuery';
import { Toaster } from 'react-hot-toast';
interface PanelNacionalWidgetProps {
eleccionId: number;
@@ -79,9 +80,9 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) =>
return (
<div className="panel-nacional-container">
<Toaster containerClassName="widget-toaster-container" />
<header className="panel-header">
<div className="header-top-row">
<h1>Legislativas Argentina 2025</h1>
<Select
options={CATEGORIAS_NACIONALES}
value={selectedCategoria}
@@ -90,14 +91,14 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) =>
classNamePrefix="categoria-selector"
isSearchable={false}
/>
<Breadcrumbs
nivel={ambitoActual.nivel}
nombreAmbito={ambitoActual.nombre}
nombreProvincia={ambitoActual.provinciaNombre}
onReset={handleResetToPais}
onVolverProvincia={handleVolverAProvincia}
/>
</div>
<Breadcrumbs
nivel={ambitoActual.nivel}
nombreAmbito={ambitoActual.nombre}
nombreProvincia={ambitoActual.provinciaNombre}
onReset={handleResetToPais}
onVolverProvincia={handleVolverAProvincia}
/>
</header>
<main className={`panel-main-content ${!isPanelOpen ? 'panel-collapsed' : ''} ${isMobile ? `mobile-view-${mobileView}` : ''}`}>
<div className="mapa-column">

View File

@@ -8,7 +8,7 @@
--card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
--text-primary: #212529;
--text-secondary: #6c757d;
--font-family: "Public Sans", system-ui, sans-serif;
--font-family: "Roboto", system-ui, sans-serif;
--primary-accent-color: #007bff;
}
@@ -34,6 +34,7 @@
/* Crea columnas flexibles que se ajustan al espacio disponible */
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
gap: 1.5rem;
align-items: start;
}
/* --- Tarjeta Individual --- */
@@ -110,6 +111,9 @@
gap: 0.75rem;
padding: 0.75rem 0;
border-bottom: 1px solid #f0f0f0;
border-left: 5px solid; /* Grosor del borde */
border-radius: 12px; /* Redondeamos las esquinas para un look más suave */
padding-left: 1rem; /* Añadimos un poco de espacio a la izquierda */
}
.candidato-row:last-child {
@@ -117,9 +121,9 @@
}
.candidato-foto {
width: 45px;
height: 45px;
border-radius: 50%;
width: 60px;
height: 60px;
border-radius: 5%;
object-fit: cover;
flex-shrink: 0;
}
@@ -135,6 +139,7 @@
font-size: 0.95rem;
color: var(--text-primary);
display: block;
text-align: left;
}
.candidato-partido {
@@ -143,10 +148,11 @@
text-transform: uppercase;
display: block;
margin-bottom: 0.3rem;
text-align: left;
}
.progress-bar-container {
height: 6px;
height: 16px;
background-color: #e9ecef;
border-radius: 3px;
overflow: hidden;
@@ -249,11 +255,49 @@
}
}
/* --- NUEVOS ESTILOS PARA EL NOMBRE DEL PARTIDO CUANDO ES EL TÍTULO PRINCIPAL --- */
/* --- ESTILOS PARA EL NOMBRE DEL PARTIDO CUANDO ES EL TÍTULO PRINCIPAL --- */
.candidato-partido.main-title {
font-size: 0.95rem; /* Hacemos la fuente más grande */
font-weight: 700; /* La ponemos en negrita, como el nombre del candidato */
color: var(--text-primary); /* Usamos el color de texto principal */
text-transform: none; /* Quitamos el 'uppercase' para que se lea mejor */
margin-bottom: 0.3rem;
}
/* --- ESTILOS PARA LA ESTRUCTURA MULTI-CATEGORÍA --- */
.categoria-bloque {
width: 100%;
}
/* Añadimos un separador si hay más de una categoría en la misma tarjeta */
.categoria-bloque + .categoria-bloque {
border-top: 1px dashed var(--card-border-color);
margin-top: 1rem;
padding-top: 1rem;
}
.categoria-titulo {
font-size: 0.8rem;
color: var(--text-secondary);
text-transform: uppercase;
text-align: center;
margin: 0 0 1rem 0;
}
/* Ajuste para el footer, que ahora está dentro de cada categoría */
.categoria-bloque .card-footer {
grid-template-columns: repeat(3, 1fr);
background-color: transparent; /* Quitamos el fondo gris */
border-top: 1px solid var(--card-border-color);
padding: 0.75rem 0;
margin-top: 0.75rem; /* Espacio antes del footer */
text-align: center;
}
.categoria-bloque .card-footer div {
border-right: 1px solid var(--card-border-color);
}
.categoria-bloque .card-footer div:last-child {
border-right: none;
}

View File

@@ -4,25 +4,47 @@ import { getResumenPorProvincia } from '../../../apiService';
import { ProvinciaCard } from './components/ProvinciaCard';
import './ResultadosNacionalesCardsWidget.css';
// --- 1. AÑADIR LA PROP A LA INTERFAZ ---
interface Props {
eleccionId: number;
focoDistritoId?: string;
focoCategoriaId?: number;
cantidadResultados?: number;
mostrarBancas?: boolean; // Booleano opcional
}
export const ResultadosNacionalesCardsWidget = ({ eleccionId }: Props) => {
// --- 2. RECIBIR LA PROP Y ESTABLECER UN VALOR POR DEFECTO ---
export const ResultadosNacionalesCardsWidget = ({
eleccionId,
focoDistritoId,
focoCategoriaId,
cantidadResultados,
mostrarBancas = false // Por defecto, no se muestran las bancas
}: Props) => {
const { data, isLoading, error } = useQuery({
queryKey: ['resumenPorProvincia', eleccionId],
queryFn: () => getResumenPorProvincia(eleccionId),
queryKey: ['resumenPorProvincia', eleccionId, focoDistritoId, focoCategoriaId, cantidadResultados],
queryFn: () => getResumenPorProvincia(eleccionId, {
focoDistritoId,
focoCategoriaId,
cantidadResultados
}),
});
if (isLoading) return <div>Cargando resultados por provincia...</div>;
if (error) return <div>Error al cargar los datos.</div>;
if (!data || data.length === 0) return <div>No hay resultados para mostrar con los filtros seleccionados.</div>
return (
<section className="cards-widget-container">
<h2>Resultados elecciones nacionales 2025</h2>
<div className="cards-grid">
{data?.map(provinciaData => (
<ProvinciaCard key={provinciaData.provinciaId} data={provinciaData} />
<ProvinciaCard
key={provinciaData.provinciaId}
data={provinciaData}
mostrarBancas={mostrarBancas}
/>
))}
</div>
</section>

View File

@@ -11,6 +11,7 @@ import type { ResultadoMapaDto, AmbitoGeography } from '../../../../types/types'
import { MapaProvincial } from './MapaProvincial';
import { CabaLupa } from './CabaLupa';
import { BiZoomIn, BiZoomOut } from "react-icons/bi";
import toast from 'react-hot-toast';
const DEFAULT_MAP_COLOR = '#E0E0E0';
const FADED_BACKGROUND_COLOR = '#F0F0F0';
@@ -166,9 +167,45 @@ export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nom
};
}, [position, nivel]);
const handleZoomIn = () => setPosition(prev => ({ ...prev, zoom: Math.min(prev.zoom * 1.8, 100) }));
const panEnabled =
nivel === 'provincia' &&
initialProvincePositionRef.current !== null &&
position.zoom > initialProvincePositionRef.current.zoom &&
!nombreMunicipioSeleccionado;
// --- INICIO DE LA CORRECCIÓN ---
const handleZoomIn = () => {
// Solo mostramos la notificación si el paneo NO está ya habilitado
if (!panEnabled && initialProvincePositionRef.current) {
// Calculamos cuál será el nuevo nivel de zoom
const newZoom = position.zoom * 1.8;
// Si el nuevo zoom supera el umbral inicial, activamos la notificación
if (newZoom > initialProvincePositionRef.current.zoom) {
toast.success('Desplazamiento Habilitado', {
icon: '🖐️',
style: { background: '#32e5f1ff', color: 'white' },
duration: 1000,
});
}
}
setPosition(prev => ({ ...prev, zoom: Math.min(prev.zoom * 1.8, 100) }));
};
const handleZoomOut = () => {
// Solo mostramos la notificación si el paneo SÍ está habilitado actualmente
if (panEnabled && initialProvincePositionRef.current) {
const newZoom = position.zoom / 1.8;
// Si el nuevo zoom es igual o menor al umbral, desactivamos
if (newZoom <= initialProvincePositionRef.current.zoom) {
toast.error('Desplazamiento Deshabilitado', {
icon: '🔒',
style: { background: '#32e5f1ff', color: 'white' },
duration: 1000,
});
}
}
// La lógica para actualizar la posición no cambia
setPosition(prev => {
const newZoom = Math.max(prev.zoom / 1.8, 1);
const initialPos = initialProvincePositionRef.current;
@@ -182,12 +219,6 @@ export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nom
setIsPanning(false);
};
const panEnabled =
nivel === 'provincia' &&
initialProvincePositionRef.current !== null &&
position.zoom > initialProvincePositionRef.current.zoom &&
!nombreMunicipioSeleccionado;
const filterInteractionEvents = (event: any) => {
if (event.sourceEvent && event.sourceEvent.type === 'wheel') return false;
return panEnabled;

View File

@@ -71,15 +71,25 @@ export const PanelResultados = ({ resultados, estadoRecuento }: PanelResultadosP
<div className="panel-partidos-container">
{resultados.map(partido => (
<div key={partido.id} className="partido-fila" style={{ borderLeftColor: partido.color || '#888' }}>
<div
key={partido.id}
className="partido-fila"
style={{ borderLeftColor: partido.color || '#ccc' }}
>
<div className="partido-logo">
<ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc={`${assetBaseUrl}/default-avatar.png`} alt={partido.nombre} />
</div>
<div className="partido-main-content">
<div className="partido-top-row">
<div className="partido-info-wrapper">
<span className="partido-nombre">{partido.nombreCorto || partido.nombre}</span>
{partido.nombreCandidato && <span className="candidato-nombre">{partido.nombreCandidato}</span>}
{partido.nombreCandidato ? (
<>
<span className="candidato-nombre">{partido.nombreCandidato}</span>
<span className="partido-nombre">{partido.nombreCorto || partido.nombre}</span>
</>
) : (
<span className="partido-nombre">{partido.nombreCorto || partido.nombre}</span>
)}
</div>
<div className="partido-stats">
<span className="partido-porcentaje">

View File

@@ -1,78 +1,110 @@
// src/features/legislativas/nacionales/components/ProvinciaCard.tsx
import type { ResumenProvincia } from '../../../../types/types';
import type { ResumenProvincia, CategoriaResumen } from '../../../../types/types';
import { MiniMapaSvg } from './MiniMapaSvg';
import { ImageWithFallback } from '../../../../components/common/ImageWithFallback';
import { assetBaseUrl } from '../../../../apiService';
// --- 1. AÑADIR LA PROP A AMBAS INTERFACES ---
interface CategoriaDisplayProps {
categoria: CategoriaResumen;
mostrarBancas?: boolean;
}
interface ProvinciaCardProps {
data: ResumenProvincia;
mostrarBancas?: boolean;
}
const formatNumber = (num: number) => num.toLocaleString('es-AR');
const formatPercent = (num: number) => `${num.toFixed(2).replace('.', ',')}%`;
export const ProvinciaCard = ({ data }: ProvinciaCardProps) => {
// Determinamos el color del ganador para pasárselo al mapa.
// Si no hay ganador, usamos un color gris por defecto.
const colorGanador = data.resultados[0]?.color || '#d1d1d1';
// --- 2. RECIBIR Y USAR LA PROP EN EL SUB-COMPONENTE ---
const CategoriaDisplay = ({ categoria, mostrarBancas }: CategoriaDisplayProps) => {
return (
<div className="categoria-bloque">
<h4 className="categoria-titulo">{categoria.categoriaNombre}</h4>
{categoria.resultados.map(res => (
<div
key={res.agrupacionId}
className="candidato-row"
style={{ borderLeftColor: res.color || '#ccc' }}
>
<ImageWithFallback
src={res.fotoUrl ?? undefined}
fallbackSrc={`${assetBaseUrl}/default-avatar.png`}
alt={res.nombreCandidato ?? res.nombreAgrupacion}
className="candidato-foto"
/>
<div className="candidato-data">
{res.nombreCandidato && (
<span className="candidato-nombre">{res.nombreCandidato}</span>
)}
<span className={`candidato-partido ${!res.nombreCandidato ? 'main-title' : ''}`}>
{res.nombreAgrupacion}
</span>
<div className="progress-bar-container">
<div className="progress-bar" style={{ width: `${res.porcentaje}%`, backgroundColor: res.color || '#ccc' }} />
</div>
</div>
<div className="candidato-stats">
<span className="stats-percent">{formatPercent(res.porcentaje)}</span>
<span className="stats-votos">{formatNumber(res.votos)} votos</span>
</div>
{/* --- 3. RENDERIZADO CONDICIONAL DEL CUADRO DE BANCAS --- */}
{/* Este div solo se renderizará si mostrarBancas es true */}
{mostrarBancas && (
<div className="stats-bancas">
+{res.bancasObtenidas}
<span>Bancas</span>
</div>
)}
</div>
))}
<footer className="card-footer">
<div>
<span>Participación</span>
<strong>{formatPercent(categoria.estadoRecuento?.participacionPorcentaje ?? 0)}</strong>
</div>
<div>
<span>Mesas escrutadas</span>
<strong>{formatPercent(categoria.estadoRecuento?.mesasTotalizadasPorcentaje ?? 0)}</strong>
</div>
<div>
<span>Votos totales</span>
<strong>{formatNumber(categoria.estadoRecuento?.cantidadVotantes ?? 0)}</strong>
</div>
</footer>
</div>
);
};
// --- 4. RECIBIR Y PASAR LA PROP EN EL COMPONENTE PRINCIPAL ---
export const ProvinciaCard = ({ data, mostrarBancas }: ProvinciaCardProps) => {
const colorGanador = data.categorias[0]?.resultados[0]?.color || '#d1d1d1';
return (
<div className="provincia-card">
<header className="card-header">
<div className="header-info">
<h3 style={{ whiteSpace: 'normal' }}>{data.provinciaNombre}</h3>
<span>DIPUTADOS NACIONALES</span>
</div>
<div className="header-map">
<MiniMapaSvg provinciaNombre={data.provinciaNombre} fillColor={colorGanador} />
</div>
</header>
<div className="card-body">
{data.resultados.map(res => (
<div key={res.agrupacionId} className="candidato-row">
<ImageWithFallback src={res.fotoUrl ?? undefined} fallbackSrc={`${assetBaseUrl}/default-avatar.png`} alt={res.nombreCandidato ?? res.nombreAgrupacion} className="candidato-foto" />
<div className="candidato-data">
{res.nombreCandidato && (
<span className="candidato-nombre">{res.nombreCandidato}</span>
)}
<span className={`candidato-partido ${!res.nombreCandidato ? 'main-title' : ''}`}>
{res.nombreAgrupacion}
</span>
<div className="progress-bar-container">
<div className="progress-bar" style={{ width: `${res.porcentaje}%`, backgroundColor: res.color || '#ccc' }} />
</div>
</div>
<div className="candidato-stats">
<span className="stats-percent">{formatPercent(res.porcentaje)}</span>
<span className="stats-votos">{formatNumber(res.votos)} votos</span>
</div>
<div className="stats-bancas">
+{res.bancasObtenidas}
<span>Bancas</span>
</div>
</div>
{data.categorias.map(categoria => (
<CategoriaDisplay
key={categoria.categoriaId}
categoria={categoria}
mostrarBancas={mostrarBancas} // Pasar la prop hacia abajo
/>
))}
</div>
<footer className="card-footer">
<div>
<span>Participación</span>
{/* Usamos los datos reales del estado de recuento */}
<strong>{formatPercent(data.estadoRecuento?.participacionPorcentaje ?? 0)}</strong>
</div>
<div>
<span>Mesas escrutadas</span>
<strong>{formatPercent(data.estadoRecuento?.mesasTotalizadasPorcentaje ?? 0)}</strong>
</div>
<div>
<span>Votos totales</span>
{/* Usamos el nuevo campo cantidadVotantes */}
<strong>{formatNumber(data.estadoRecuento?.cantidadVotantes ?? 0)}</strong>
</div>
</footer>
</div>
);
};

View File

@@ -1,31 +1,37 @@
// src/types/types.ts
import type { Feature as GeoJsonFeature, Geometry } from 'geojson';
// Definimos nuestras propiedades personalizadas
// --- TIPOS GEOGRÁFICOS Y DE MAPAS ---
export interface GeoProperties {
nombre: string;
id: string;
[key: string]: any; // Permite otras propiedades
[key: string]: any;
}
export type AmbitoGeography = GeoJsonFeature<Geometry, GeoProperties> & { rsmKey: string };
export interface GeographyObject {
rsmKey: string;
properties: {
NAME_2: string;
[key: string]: any;
};
}
export interface MapaDto {
ambitoId: number;
departamentoNombre: string;
agrupacionGanadoraId: string;
}
// Nuestro tipo de geografía ahora se basa en el tipo estándar de GeoJSON
// y le añadimos la propiedad 'rsmKey' que 'react-simple-maps' añade.
export type AmbitoGeography = GeoJsonFeature<Geometry, GeoProperties> & { rsmKey: string };
// Tipos para la respuesta de la API de resultados por municipio
// --- TIPOS DE RESPUESTAS DE API ---
export interface AgrupacionResultadoDto {
nombre: string;
votos: number;
porcentaje: number;
}
export interface VotosAdicionalesDto {
enBlanco: number;
nulos: number;
recorridos: number;
}
export interface MunicipioResultadosDto {
municipioNombre: string;
ultimaActualizacion: string;
@@ -34,25 +40,7 @@ export interface MunicipioResultadosDto {
resultados: AgrupacionResultadoDto[];
votosAdicionales: VotosAdicionalesDto;
}
// Tipo para la respuesta del endpoint del mapa
export interface MapaDto {
ambitoId: number;
departamentoNombre: string;
agrupacionGanadoraId: string;
}
// Definición de tipo para los objetos de geografía de react-simple-maps
export interface GeographyObject {
rsmKey: string;
properties: {
NAME_2: string;
[key: string]: any;
};
}
export interface MunicipioSimple { id: string; nombre: string; camarasDisponibles?: ('diputados' | 'senadores')[]; }
export interface ResultadoTicker {
id: string;
nombre: string;
@@ -63,50 +51,19 @@ export interface ResultadoTicker {
porcentaje: number;
nombreCandidato?: string | null;
}
export interface EstadoRecuentoTicker {
mesasTotalizadasPorcentaje: number;
participacionPorcentaje: number;
}
export interface CategoriaResumen {
categoriaId: number;
categoriaNombre: string;
estadoRecuento: EstadoRecuentoTicker | null;
resultados: ResultadoTicker[];
}
export interface VotosAdicionales { enBlanco: number; nulos: number; recurridos: number; }
export interface MunicipioDetalle {
municipioNombre: string;
ultimaActualizacion: string;
porcentajeEscrutado: number;
porcentajeParticipacion: number;
resultados: CategoriaResumen[];
votosAdicionales: VotosAdicionales;
}
export interface ResumenProvincial {
provinciaNombre: string;
ultimaActualizacion: string;
porcentajeEscrutado: number;
porcentajeParticipacion: number;
resultados: CategoriaResumen[];
votosAdicionales: VotosAdicionales;
}
export interface Banca {
agrupacionNombre: string;
bancas: number;
[key: string]: string | number;
}
export interface ProyeccionBancas {
seccionNombre: string;
proyeccion: Banca[];
}
export interface TelegramaData {
id: string;
ambitoGeograficoId: number;
@@ -114,58 +71,22 @@ export interface TelegramaData {
fechaEscaneo: string;
fechaTotalizacion: string;
}
export interface CatalogoItem {
id: string;
nombre: string;
}
export interface ApiResponseResultadosPorSeccion {
ultimaActualizacion: string;
resultados: ResultadoTicker[];
}
export interface ResultadoTablaAgrupacion {
id: string;
nombre: string;
nombreCorto: string | null;
porcentaje: number;
}
export interface ResultadoTablaCategoria {
categoriaId: number;
categoriaNombre: string;
resultados: ResultadoTablaAgrupacion[];
}
export interface TablaDetalladaPartidoPrincipal {
id: string;
nombre: string;
}
export interface TablaDetalladaResultadoPartido {
id: string;
porcentaje: number;
}
export interface TablaDetalladaResultadoCategoria {
categoriaId: number;
partidos: TablaDetalladaResultadoPartido[];
}
export interface TablaDetalladaResultadoMunicipio {
municipioId: number;
municipioNombre: string;
resultadosPorCategoria: TablaDetalladaResultadoCategoria[];
}
// --- TIPOS PARA TABLAS DE RANKING ---
export interface PartidoPrincipalDetalle {
puesto: number;
id: string;
nombre: string;
porcentajeTotalSeccion: number;
}
export interface ApiResponseTablaDetallada {
categorias: { id: number; nombre: string }[];
partidosPorCategoria: { [catId: string]: PartidoPrincipalDetalle[] };
@@ -175,99 +96,86 @@ export interface ApiResponseTablaDetallada {
celdas: { [catId: string]: { [partidoId: string]: number } };
}[];
}
export interface TablaDetalladaPartidoPrincipal {
id: string;
nombre: string;
}
export interface TablaDetalladaResultadoPartido {
id: string;
porcentaje: number;
}
export interface TablaDetalladaResultadoCategoria {
categoriaId: number;
partidos: TablaDetalladaResultadoPartido[];
}
export interface TablaDetalladaResultadoMunicipio {
municipioId: number;
municipioNombre: string;
resultadosPorCategoria: TablaDetalladaResultadoCategoria[];
}
export interface RankingPartido {
nombreCorto: string;
porcentaje: number;
votos: number;
}
export interface RankingCategoria {
categoriaId: number;
ranking: RankingPartido[];
}
export interface ApiResponseRankingSeccion {
categorias: { id: number; nombre: string }[];
resultados: RankingMunicipio[];
}
export interface RankingPartido {
nombreCorto: string;
porcentaje: number;
}
export interface RankingCategoria {
ranking: RankingPartido[];
}
export interface RankingMunicipio {
municipioId: number;
municipioNombre: string;
resultadosPorCategoria: { [catId: string]: RankingCategoria };
resultadosPorCategoria: { [catId: string]: { ranking: { nombreCorto: string; porcentaje: number }[] } };
}
export interface ApiResponseRankingMunicipio {
categorias: { id: number; nombre: string }[];
resultados: RankingMunicipio[];
}
// --- TIPOS PARA PANEL ELECTORAL ---
export interface ResultadoMapaDto {
ambitoId: string;
ambitoNombre: string;
agrupacionGanadoraId: string;
colorGanador: string;
}
export interface PanelElectoralDto {
ambitoNombre: string;
mapaData: ResultadoMapaDto[];
resultadosPanel: ResultadoTicker[]; // Reutilizamos el tipo que ya tienes
estadoRecuento: EstadoRecuentoTicker; // Reutilizamos el tipo que ya tienes
resultadosPanel: ResultadoTicker[];
estadoRecuento: EstadoRecuentoTicker;
}
// --- TIPOS PARA EL WIDGET DE TARJETAS ---
// --- TIPOS PARA EL WIDGET DE TARJETAS NACIONALES ---
// Definición correcta y más completa. Reemplaza a EstadoRecuentoTicker.
export interface EstadoRecuentoDto {
participacionPorcentaje: number;
mesasTotalizadasPorcentaje: number;
cantidadVotantes: number;
participacionPorcentaje: number;
mesasTotalizadasPorcentaje: number;
cantidadVotantes: number;
}
// Definición para los resultados en las tarjetas.
export interface ResultadoCandidato {
agrupacionId: string;
nombreCandidato: string | null;
nombreAgrupacion: string;
fotoUrl: string | null;
color: string | null;
porcentaje: number;
votos: number;
bancasObtenidas: number;
agrupacionId: string;
nombreAgrupacion: string; // Nombre completo
nombreCortoAgrupacion: string | null; // <-- AÑADIR ESTA LÍNEA
nombreCandidato: string | null;
fotoUrl: string | null;
color: string | null;
porcentaje: number;
votos: number;
bancasObtenidas?: number;
}
// Definición para una categoría.
export interface CategoriaResumen {
categoriaId: number;
categoriaNombre: string;
estadoRecuento: EstadoRecuentoDto | null;
resultados: ResultadoCandidato[];
}
// Definición para el objeto de una provincia.
export interface ResumenProvincia {
provinciaId: string;
provinciaNombre: string;
provinciaId: string;
provinciaNombre: string;
categorias: CategoriaResumen[];
}
export interface CategoriaResumenHome {
categoriaId: number;
categoriaNombre: string;
estadoRecuento: EstadoRecuentoDto | null;
resultados: ResultadoCandidato[];
votosEnBlanco: number;
votosEnBlancoPorcentaje: number;
votosTotales: number;
ultimaActualizacion: string;
}