Feat CarouselNacional y Fix Workers

This commit is contained in:
2025-10-17 15:49:15 -03:00
parent 4bc257df43
commit ae846f2d48
12 changed files with 430 additions and 148 deletions

View File

@@ -317,4 +317,15 @@ export const getHomeResumen = async (eleccionId: number, distritoId: string, cat
const url = `/elecciones/home-resumen?${queryParams.toString()}`;
const { data } = await apiClient.get(url);
return data;
};
export const getHomeResumenNacional = async (eleccionId: number, categoriaId: number): Promise<CategoriaResumenHome> => {
const queryParams = new URLSearchParams({
eleccionId: eleccionId.toString(),
categoriaId: categoriaId.toString(),
});
// Apunta al nuevo endpoint que creamos
const url = `/elecciones/home-resumen-nacional?${queryParams.toString()}`;
const { data } = await apiClient.get(url);
return data;
};

View File

@@ -5,6 +5,7 @@ import { CongresoNacionalWidget } from './nacionales/CongresoNacionalWidget';
import { PanelNacionalWidget } from './nacionales/PanelNacionalWidget';
import { HomeCarouselWidget } from './nacionales/HomeCarouselWidget';
import './DevAppStyle.css'
import { HomeCarouselNacionalWidget } from './nacionales/HomeCarouselNacionalWidget';
// --- NUEVO COMPONENTE REUTILIZABLE PARA CONTENIDO COLAPSABLE ---
const CollapsibleWidgetWrapper = ({ children }: { children: React.ReactNode }) => {
@@ -52,22 +53,34 @@ export const DevAppLegislativas = () => {
<PanelNacionalWidget eleccionId={2} />
<div style={sectionStyle}>
<h2>Widget: Carrusel de Resultados (Home)</h2>
<h2>Widget: Carrusel de Resultados Provincias (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>
Uso: <code style={codeStyle}>&lt;HomeCarouselWidget eleccionId={2} distritoId="02" categoriaId={3} titulo="Diputados - Provincia de Buenos Aires" /&gt;</code>
</p>
<HomeCarouselWidget
eleccionId={2} // Nacional
distritoId="02" // Buenos Aires
categoriaId={2} // Diputados Nacionales
categoriaId={3} // Diputados Nacionales
titulo="Diputados - Provincia de Buenos Aires"
/>
</div>
<div style={sectionStyle}>
<h2>Widget: Carrusel de Resultados Nación (Home)</h2>
<p style={descriptionStyle}>
Uso: <code style={codeStyle}>&lt;HomeCarouselNacionalWidget eleccionId={2} categoriaId={3} titulo="Diputados - Argentina" /&gt;</code>
</p>
<HomeCarouselNacionalWidget
eleccionId={2}
categoriaId={3} // 3 para Diputados, 2 para Senadores
titulo="Diputados - Total País"
/>
</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>
@@ -89,7 +102,7 @@ export const DevAppLegislativas = () => {
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>
@@ -101,10 +114,10 @@ export const DevAppLegislativas = () => {
<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>
Ejemplo Senadores (ID 2): <code style={codeStyle}>&lt;ResultadosNacionalesCardsWidget eleccionId={2} focoCategoriaId={2} /&gt;</code>
</p>
<ResultadosNacionalesCardsWidget eleccionId={2} focoCategoriaId={1} />
<ResultadosNacionalesCardsWidget eleccionId={2} focoCategoriaId={2} />
<hr style={{ marginTop: '2rem' }} />
<h3 style={{ marginTop: '2rem' }}>4. Indicando Cantidad de Resultados (cantidadResultados)</h3>
@@ -135,9 +148,9 @@ export const DevAppLegislativas = () => {
<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>
Uso: <code style={codeStyle}>&lt;ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="16" focoCategoriaId={2} cantidadResultados={1} /&gt;</code>
</p>
<ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="16" focoCategoriaId={1} cantidadResultados={1} />
<ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="16" focoCategoriaId={2} cantidadResultados={1} />
</div>
</div>

View File

@@ -0,0 +1,146 @@
// src/features/legislativas/nacionales/HomeCarouselNacionalWidget.tsx
import { useQuery } from '@tanstack/react-query';
import { getHomeResumenNacional } 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 styles from './HomeCarouselWidget.module.css';
const formatPercent = (num: number | null | undefined) => `${(num || 0).toFixed(2).replace('.', ',')}%`;
const formatNumber = (num: number) => num.toLocaleString('es-AR');
const formatDateTime = (dateString: string | undefined | null) => {
if (!dateString) return '...';
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) {
return dateString;
}
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;
}
};
// Las props ya no incluyen distritoId
interface Props {
eleccionId: number;
categoriaId: number;
titulo: string;
}
export const HomeCarouselNacionalWidget = ({ eleccionId, categoriaId, titulo }: Props) => {
const { data, isLoading, error } = useQuery({
// La queryKey ahora no necesita distritoId
queryKey: ['homeResumenNacional', eleccionId, categoriaId],
// Llama a la nueva función de la API
queryFn: () => getHomeResumenNacional(eleccionId, categoriaId),
});
if (isLoading) return <div>Cargando widget...</div>;
if (error || !data) return <div>No se pudieron cargar los datos.</div>;
return (
<div className={styles.homeCarouselWidget}>
<h2 className={styles.widgetTitle}>{titulo}</h2>
<div className={styles.topStatsBar}>
<div>
<span>Participación</span>
<strong>{formatPercent(data.estadoRecuento?.participacionPorcentaje)}</strong>
</div>
<div>
<span className={styles.longText}>Mesas escrutadas</span>
<span className={styles.shortText}>Escrutado</span>
<strong>{formatPercent(data.estadoRecuento?.mesasTotalizadasPorcentaje)}</strong>
</div>
<div>
<span className={styles.longText}>Votos en blanco</span>
<span className={styles.shortText}>En blanco</span>
<strong>{formatPercent(data.votosEnBlancoPorcentaje)}</strong>
</div>
<div>
<span className={styles.longText}>Votos totales</span>
<span className={styles.shortText}>Votos</span>
<strong>{formatNumber(data.votosTotales)}</strong>
</div>
</div>
<div className={styles.carouselContainer}>
<Swiper
modules={[Navigation, A11y]}
spaceBetween={16}
slidesPerView={1.3}
navigation={{
prevEl: `.${styles.navButtonPrev}`,
nextEl: `.${styles.navButtonNext}`,
}}
breakpoints={{
320: { slidesPerView: 1.25, spaceBetween: 10 },
430: { slidesPerView: 1.4, spaceBetween: 12 },
640: { slidesPerView: 2.5 },
1024: { slidesPerView: 3 },
1200: { slidesPerView: 3.5 }
}}
>
{data.resultados.map(candidato => (
<SwiperSlide key={candidato.agrupacionId}>
<div className={styles.candidateCard} style={{ '--candidate-color': candidato.color || '#ccc' } as React.CSSProperties}>
<div className={styles.candidatePhotoWrapper}>
<ImageWithFallback
src={candidato.fotoUrl ?? undefined}
fallbackSrc={`${assetBaseUrl}/default-avatar.png`}
alt={candidato.nombreCandidato ?? ''}
className={styles.candidatePhoto}
/>
</div>
<div className={styles.candidateDetails}>
<div className={styles.candidateInfo}>
{candidato.nombreCandidato ? (
<>
<span className={styles.candidateName}>
{candidato.nombreCandidato}
</span>
<span className={styles.partyName}>
{candidato.nombreCortoAgrupacion || candidato.nombreAgrupacion}
</span>
</>
) : (
<span className={styles.candidateName}>
{candidato.nombreCortoAgrupacion || candidato.nombreAgrupacion}
</span>
)}
</div>
<div className={styles.candidateResults}>
<span className={styles.percentage}>{formatPercent(candidato.porcentaje)}</span>
<span className={styles.votes}>{formatNumber(candidato.votos)} votos</span>
</div>
</div>
</div>
</SwiperSlide>
))}
</Swiper>
<div className={`${styles.navButton} ${styles.navButtonPrev}`}></div>
<div className={`${styles.navButton} ${styles.navButtonNext}`}></div>
</div>
<div className={styles.widgetFooter}>
Última actualización: {formatDateTime(data.ultimaActualizacion)}
</div>
</div>
);
};