Feat Widgets
- Widget de Home - Widget Cards por Provincias - Widget Mapa por Categorias
This commit is contained in:
47
Elecciones-Web/frontend/package-lock.json
generated
47
Elecciones-Web/frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -38,7 +38,7 @@ export const DevApp = () => {
|
||||
<DiputadosPorSeccionWidget />
|
||||
<SenadoresPorSeccionWidget />
|
||||
<ConcejalesPorSeccionWidget />
|
||||
<CongresoWidget />
|
||||
<CongresoWidget eleccionId={1} />
|
||||
<BancasWidget />
|
||||
<MapaBsAs />
|
||||
<MapaBsAsSecciones />
|
||||
|
||||
@@ -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}><HomeCarouselWidget eleccionId={2} distritoId="02" categoriaId={2} titulo="Diputados - Provincia de Buenos Aires" /></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}><ResultadosNacionalesCardsWidget eleccionId={2} /></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}><ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="02" /></code>
|
||||
</p>
|
||||
<ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="02" />
|
||||
|
||||
<p style={{ ...descriptionStyle, marginTop: '2rem' }}>
|
||||
Ejemplo Chaco (que también renueva Senadores): <code style={codeStyle}><ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="06" /></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}><ResultadosNacionalesCardsWidget eleccionId={2} focoCategoriaId={1} /></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}><ResultadosNacionalesCardsWidget eleccionId={2} cantidadResultados={3} /></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}><ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="23" mostrarBancas={true} /></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}><ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="16" focoCategoriaId={1} cantidadResultados={1} /></code>
|
||||
</p>
|
||||
<ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="16" focoCategoriaId={1} cantidadResultados={1} />
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{/* --- OTROS WIDGETS --- */}
|
||||
<CongresoNacionalWidget eleccionId={2} />
|
||||
<PanelNacionalWidget eleccionId={2} />
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user