2025-10-03 13:26:20 -03:00
|
|
|
|
import { useMemo, useState, Suspense, useEffect } from 'react';
|
2025-09-19 17:19:10 -03:00
|
|
|
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
2025-09-17 11:31:17 -03:00
|
|
|
|
import { getPanelElectoral } from '../../../apiService';
|
|
|
|
|
|
import { MapaNacional } from './components/MapaNacional';
|
|
|
|
|
|
import { PanelResultados } from './components/PanelResultados';
|
|
|
|
|
|
import { Breadcrumbs } from './components/Breadcrumbs';
|
2025-10-03 13:26:20 -03:00
|
|
|
|
import { MunicipioSearch } from './components/MunicipioSearch';
|
2025-09-17 11:31:17 -03:00
|
|
|
|
import './PanelNacional.css';
|
|
|
|
|
|
import Select from 'react-select';
|
2025-10-03 13:26:20 -03:00
|
|
|
|
import type { PanelElectoralDto, ResultadoTicker } from '../../../types/types';
|
|
|
|
|
|
import { FiMap, FiList, FiChevronDown, FiChevronUp } from 'react-icons/fi';
|
2025-09-20 22:31:11 -03:00
|
|
|
|
import { useMediaQuery } from './hooks/useMediaQuery';
|
2025-10-01 10:03:01 -03:00
|
|
|
|
import { Toaster } from 'react-hot-toast';
|
2025-10-03 13:26:20 -03:00
|
|
|
|
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
|
|
|
|
|
|
import { assetBaseUrl } from '../../../apiService';
|
2025-09-17 11:31:17 -03:00
|
|
|
|
|
2025-10-03 13:26:20 -03:00
|
|
|
|
// --- COMPONENTE INTERNO PARA LA TARJETA DE RESULTADOS EN MÓVIL ---
|
|
|
|
|
|
interface MobileResultsCardProps {
|
|
|
|
|
|
eleccionId: number;
|
|
|
|
|
|
ambitoId: string | null;
|
|
|
|
|
|
categoriaId: number;
|
|
|
|
|
|
ambitoNombre: string;
|
|
|
|
|
|
ambitoNivel: 'pais' | 'provincia' | 'municipio';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;
|
|
|
|
|
|
|
|
|
|
|
|
// --- SUB-COMPONENTE PARA UNA FILA DE RESULTADO ---
|
|
|
|
|
|
const ResultRow = ({ partido }: { partido: ResultadoTicker }) => (
|
|
|
|
|
|
<div className="mobile-result-row" style={{ borderLeftColor: partido.color || '#ccc' }}>
|
|
|
|
|
|
<div className="mobile-result-logo" style={{ backgroundColor: partido.color || '#e9ecef' }}>
|
|
|
|
|
|
<ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc={`${assetBaseUrl}/default-avatar.png`} alt={partido.nombre} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="mobile-result-info">
|
|
|
|
|
|
{partido.nombreCandidato ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<span className="mobile-result-party-name">{partido.nombreCandidato}</span>
|
|
|
|
|
|
<span className="mobile-result-candidate-name">{partido.nombreCorto || partido.nombre}</span>
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<span className="mobile-result-party-name">{partido.nombreCorto || partido.nombre}</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="mobile-result-stats">
|
|
|
|
|
|
<strong>{formatPercent(partido.porcentaje)}</strong>
|
|
|
|
|
|
<span>{partido.votos.toLocaleString('es-AR')}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// --- COMPONENTE REFACTORIZADO PARA LA TARJETA MÓVIL ---
|
|
|
|
|
|
interface MobileResultsCardProps {
|
|
|
|
|
|
eleccionId: number;
|
|
|
|
|
|
ambitoId: string | null;
|
|
|
|
|
|
categoriaId: number;
|
|
|
|
|
|
ambitoNombre: string;
|
|
|
|
|
|
ambitoNivel: 'pais' | 'provincia' | 'municipio';
|
|
|
|
|
|
mobileView: 'mapa' | 'resultados';
|
|
|
|
|
|
setMobileView: (view: 'mapa' | 'resultados') => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const MobileResultsCard = ({
|
|
|
|
|
|
eleccionId, ambitoId, categoriaId, ambitoNombre, ambitoNivel, mobileView, setMobileView
|
|
|
|
|
|
}: MobileResultsCardProps) => {
|
|
|
|
|
|
|
|
|
|
|
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
const { data } = useSuspenseQuery<PanelElectoralDto>({
|
|
|
|
|
|
queryKey: ['panelElectoral', eleccionId, ambitoId, categoriaId],
|
|
|
|
|
|
queryFn: () => getPanelElectoral(eleccionId, ambitoId, categoriaId),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
setIsExpanded(ambitoNivel === 'municipio');
|
|
|
|
|
|
}, [ambitoNivel]);
|
|
|
|
|
|
|
|
|
|
|
|
const topResults = data.resultadosPanel.slice(0, 4);
|
|
|
|
|
|
|
|
|
|
|
|
if (topResults.length === 0 && ambitoNivel === 'pais') {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className={`mobile-results-card-container ${isExpanded ? 'expanded' : ''} view-${mobileView}`}>
|
|
|
|
|
|
{/* Sección Colapsable con Resultados */}
|
|
|
|
|
|
<div className="collapsible-section">
|
|
|
|
|
|
<div className="mobile-results-header" onClick={() => setIsExpanded(!isExpanded)}>
|
|
|
|
|
|
<div className="header-info">
|
|
|
|
|
|
<h4>{ambitoNombre}</h4>
|
|
|
|
|
|
{/* Se añade una clase para estilizar este texto específicamente */}
|
|
|
|
|
|
<span className="header-action-text">{isExpanded ? 'Ocultar resultados' : 'Ver top 4'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="header-toggle-icon">
|
|
|
|
|
|
{isExpanded ? <FiChevronUp /> : <FiChevronDown />}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="mobile-results-content">
|
|
|
|
|
|
{topResults.length > 0 ? (
|
|
|
|
|
|
topResults.map(partido => <ResultRow key={partido.id} partido={partido} />)
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<p className="no-results-text">No hay resultados para esta selección.</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Footer Fijo con Botones de Navegación */}
|
|
|
|
|
|
<div className="mobile-card-view-toggle">
|
|
|
|
|
|
<button
|
|
|
|
|
|
className={`toggle-btn ${mobileView === 'mapa' ? 'active' : ''}`}
|
|
|
|
|
|
onClick={() => setMobileView('mapa')}
|
|
|
|
|
|
>
|
|
|
|
|
|
<FiMap />
|
|
|
|
|
|
<span>Mapa</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
className={`toggle-btn ${mobileView === 'resultados' ? 'active' : ''}`}
|
|
|
|
|
|
onClick={() => setMobileView('resultados')}
|
|
|
|
|
|
>
|
|
|
|
|
|
<FiList />
|
|
|
|
|
|
<span>Resultados</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// --- WIDGET PRINCIPAL ---
|
2025-09-17 11:31:17 -03:00
|
|
|
|
interface PanelNacionalWidgetProps {
|
|
|
|
|
|
eleccionId: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type AmbitoState = {
|
|
|
|
|
|
id: string | null;
|
|
|
|
|
|
nivel: 'pais' | 'provincia' | 'municipio';
|
|
|
|
|
|
nombre: string;
|
|
|
|
|
|
provinciaNombre?: string;
|
|
|
|
|
|
provinciaDistritoId?: string | null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const CATEGORIAS_NACIONALES = [
|
|
|
|
|
|
{ value: 2, label: 'Diputados Nacionales' },
|
|
|
|
|
|
{ value: 1, label: 'Senadores Nacionales' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const PanelContenido = ({ eleccionId, ambitoActual, categoriaId }: { eleccionId: number, ambitoActual: AmbitoState, categoriaId: number }) => {
|
|
|
|
|
|
const { data } = useSuspenseQuery<PanelElectoralDto>({
|
|
|
|
|
|
queryKey: ['panelElectoral', eleccionId, ambitoActual.id, categoriaId],
|
|
|
|
|
|
queryFn: () => getPanelElectoral(eleccionId, ambitoActual.id, categoriaId),
|
|
|
|
|
|
});
|
2025-09-19 17:19:10 -03:00
|
|
|
|
return <PanelResultados resultados={data.resultadosPanel} estadoRecuento={data.estadoRecuento} />;
|
2025-09-17 11:31:17 -03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) => {
|
|
|
|
|
|
const [ambitoActual, setAmbitoActual] = useState<AmbitoState>({ id: null, nivel: 'pais', nombre: 'Argentina', provinciaDistritoId: null });
|
|
|
|
|
|
const [categoriaId, setCategoriaId] = useState<number>(2);
|
|
|
|
|
|
const [isPanelOpen, setIsPanelOpen] = useState(true);
|
2025-09-20 22:31:11 -03:00
|
|
|
|
const [mobileView, setMobileView] = useState<'mapa' | 'resultados'>('mapa');
|
|
|
|
|
|
const isMobile = useMediaQuery('(max-width: 800px)');
|
2025-09-17 11:31:17 -03:00
|
|
|
|
|
|
|
|
|
|
const handleAmbitoSelect = (nuevoAmbitoId: string, nuevoNivel: 'provincia' | 'municipio', nuevoNombre: string) => {
|
|
|
|
|
|
setAmbitoActual(prev => ({
|
|
|
|
|
|
id: nuevoAmbitoId,
|
|
|
|
|
|
nivel: nuevoNivel,
|
|
|
|
|
|
nombre: nuevoNombre,
|
2025-09-19 17:19:10 -03:00
|
|
|
|
provinciaNombre: nuevoNivel === 'municipio' ? prev.provinciaNombre : (nuevoNivel === 'provincia' ? nuevoNombre : undefined),
|
2025-09-17 11:31:17 -03:00
|
|
|
|
provinciaDistritoId: nuevoNivel === 'provincia' ? nuevoAmbitoId : prev.provinciaDistritoId
|
|
|
|
|
|
}));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleResetToPais = () => {
|
|
|
|
|
|
setAmbitoActual({ id: null, nivel: 'pais', nombre: 'Argentina', provinciaDistritoId: null });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleVolverAProvincia = () => {
|
|
|
|
|
|
if (ambitoActual.provinciaDistritoId && ambitoActual.provinciaNombre) {
|
|
|
|
|
|
setAmbitoActual({
|
|
|
|
|
|
id: ambitoActual.provinciaDistritoId,
|
|
|
|
|
|
nivel: 'provincia',
|
|
|
|
|
|
nombre: ambitoActual.provinciaNombre,
|
2025-09-19 17:19:10 -03:00
|
|
|
|
provinciaDistritoId: ambitoActual.provinciaDistritoId,
|
|
|
|
|
|
provinciaNombre: ambitoActual.provinciaNombre,
|
2025-09-17 11:31:17 -03:00
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
handleResetToPais();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const selectedCategoria = useMemo(() =>
|
|
|
|
|
|
CATEGORIAS_NACIONALES.find(c => c.value === categoriaId),
|
|
|
|
|
|
[categoriaId]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="panel-nacional-container">
|
2025-10-01 10:03:01 -03:00
|
|
|
|
<Toaster containerClassName="widget-toaster-container" />
|
2025-09-17 11:31:17 -03:00
|
|
|
|
<header className="panel-header">
|
|
|
|
|
|
<div className="header-top-row">
|
|
|
|
|
|
<Select
|
|
|
|
|
|
options={CATEGORIAS_NACIONALES}
|
|
|
|
|
|
value={selectedCategoria}
|
|
|
|
|
|
onChange={(option) => option && setCategoriaId(option.value)}
|
|
|
|
|
|
className="categoria-selector"
|
2025-09-20 22:31:11 -03:00
|
|
|
|
classNamePrefix="categoria-selector"
|
|
|
|
|
|
isSearchable={false}
|
2025-09-17 11:31:17 -03:00
|
|
|
|
/>
|
2025-10-03 13:26:20 -03:00
|
|
|
|
</div>
|
|
|
|
|
|
{/* --- 2. NUEVO CONTENEDOR PARA BREADCRUMBS Y BUSCADOR --- */}
|
|
|
|
|
|
<div className="header-bottom-row">
|
|
|
|
|
|
<Breadcrumbs
|
|
|
|
|
|
nivel={ambitoActual.nivel}
|
|
|
|
|
|
nombreAmbito={ambitoActual.nombre}
|
|
|
|
|
|
nombreProvincia={ambitoActual.provinciaNombre}
|
|
|
|
|
|
onReset={handleResetToPais}
|
|
|
|
|
|
onVolverProvincia={handleVolverAProvincia}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* --- 3. RENDERIZADO CONDICIONAL DEL BUSCADOR --- */}
|
|
|
|
|
|
{ambitoActual.nivel === 'provincia' && ambitoActual.provinciaDistritoId && (
|
|
|
|
|
|
<MunicipioSearch
|
|
|
|
|
|
distritoId={ambitoActual.provinciaDistritoId}
|
|
|
|
|
|
onMunicipioSelect={(municipioId, municipioNombre) =>
|
|
|
|
|
|
handleAmbitoSelect(municipioId, 'municipio', municipioNombre)
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2025-09-17 11:31:17 -03:00
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
2025-09-20 22:31:11 -03:00
|
|
|
|
<main className={`panel-main-content ${!isPanelOpen ? 'panel-collapsed' : ''} ${isMobile ? `mobile-view-${mobileView}` : ''}`}>
|
2025-09-17 11:31:17 -03:00
|
|
|
|
<div className="mapa-column">
|
2025-10-03 13:26:20 -03:00
|
|
|
|
<button className="panel-toggle-btn" onClick={() => setIsPanelOpen(!isPanelOpen)} title={isPanelOpen ? "Ocultar panel" : "Mostrar panel"}> {isPanelOpen ? '›' : '‹'} </button>
|
|
|
|
|
|
|
2025-09-17 11:31:17 -03:00
|
|
|
|
<Suspense fallback={<div className="spinner" />}>
|
2025-10-03 13:26:20 -03:00
|
|
|
|
<MapaNacional eleccionId={eleccionId} categoriaId={categoriaId} nivel={ambitoActual.nivel} nombreAmbito={ambitoActual.nombre} nombreProvinciaActiva={ambitoActual.provinciaNombre} provinciaDistritoId={ambitoActual.provinciaDistritoId ?? null} onAmbitoSelect={handleAmbitoSelect} onVolver={ambitoActual.nivel === 'municipio' ? handleVolverAProvincia : handleResetToPais} isMobileView={isMobile} />
|
2025-09-17 11:31:17 -03:00
|
|
|
|
</Suspense>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="resultados-column">
|
|
|
|
|
|
<Suspense fallback={<div className="spinner" />}>
|
2025-10-03 13:26:20 -03:00
|
|
|
|
<PanelContenido eleccionId={eleccionId} ambitoActual={ambitoActual} categoriaId={categoriaId} />
|
2025-09-17 11:31:17 -03:00
|
|
|
|
</Suspense>
|
|
|
|
|
|
</div>
|
2025-09-20 22:31:11 -03:00
|
|
|
|
|
2025-10-03 13:26:20 -03:00
|
|
|
|
<Suspense fallback={null}>
|
|
|
|
|
|
{isMobile && (
|
|
|
|
|
|
<MobileResultsCard
|
|
|
|
|
|
eleccionId={eleccionId}
|
|
|
|
|
|
ambitoId={ambitoActual.id}
|
|
|
|
|
|
categoriaId={categoriaId}
|
|
|
|
|
|
ambitoNombre={ambitoActual.nombre}
|
|
|
|
|
|
ambitoNivel={ambitoActual.nivel}
|
|
|
|
|
|
mobileView={mobileView}
|
|
|
|
|
|
setMobileView={setMobileView}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Suspense>
|
|
|
|
|
|
</main>
|
2025-09-17 11:31:17 -03:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|