Files
Elecciones-2025/Elecciones-Web/frontend/src/components/ResultadosRankingMunicipioWidget.tsx
2025-09-08 15:23:18 -03:00

175 lines
8.6 KiB
TypeScript

import { useState, useMemo, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import Select from 'react-select';
import { getSeccionesElectorales, getRankingMunicipiosPorSeccion } from '../apiService';
import type { MunicipioSimple, ApiResponseRankingMunicipio, RankingPartido } from '../types/types';
import './ResultadosTablaSeccionWidget.css';
type DisplayMode = 'porcentaje' | 'votos' | 'ambos';
type DisplayOption = {
value: DisplayMode;
label: string;
};
const displayModeOptions: readonly DisplayOption[] = [
{ value: 'porcentaje', label: 'Ver Porcentajes' },
{ value: 'votos', label: 'Ver Votos' },
{ value: 'ambos', label: 'Ver Ambos' },
];
const customSelectStyles = {
control: (base: any) => ({ ...base, minWidth: '200px', border: '1px solid #ced4da', boxShadow: 'none', '&:hover': { borderColor: '#86b7fe' } }),
menu: (base: any) => ({ ...base, zIndex: 10 }),
};
const formatPercent = (porcentaje: number) => `${porcentaje.toFixed(2).replace('.', ',')}%`;
// Nueva función para formatear votos con separador de miles
const formatVotos = (votos: number) => votos.toLocaleString('es-AR');
// --- NUEVO COMPONENTE HELPER PARA RENDERIZAR CELDAS ---
const CellRenderer = ({ partido, mode }: { partido?: RankingPartido, mode: DisplayMode }) => {
if (!partido) {
return <span>-</span>;
}
switch (mode) {
case 'votos':
return <span>{formatVotos(partido.votos)}</span>;
case 'ambos':
return (
<div className="cell-ambos">
<span>{formatVotos(partido.votos)}</span>
<small>{formatPercent(partido.porcentaje)}</small>
</div>
);
case 'porcentaje':
default:
return <span>{formatPercent(partido.porcentaje)}</span>;
}
};
export const ResultadosRankingMunicipioWidget = () => {
const [secciones, setSecciones] = useState<MunicipioSimple[]>([]);
const [selectedSeccion, setSelectedSeccion] = useState<{ value: string; label: string } | null>(null);
const [displayMode, setDisplayMode] = useState<DisplayOption>(displayModeOptions[0]);
useEffect(() => {
const fetchSecciones = async () => {
const seccionesData = await getSeccionesElectorales();
if (seccionesData && seccionesData.length > 0) {
const orden = new Map([
['Capital', 0], ['Primera', 1], ['Segunda', 2], ['Tercera', 3],
['Cuarta', 4], ['Quinta', 5], ['Sexta', 6], ['Séptima', 7]
]);
const getOrden = (nombre: string) => {
const match = nombre.match(/Capital|Primera|Segunda|Tercera|Cuarta|Quinta|Sexta|Séptima/);
return match ? orden.get(match[0]) ?? 99 : 99;
};
seccionesData.sort((a, b) => getOrden(a.nombre) - getOrden(b.nombre));
setSecciones(seccionesData);
if (!selectedSeccion) {
setSelectedSeccion({ value: seccionesData[0].id, label: seccionesData[0].nombre });
}
}
};
fetchSecciones();
}, [selectedSeccion]);
const seccionOptions = useMemo(() => secciones.map(s => ({ value: s.id, label: s.nombre })), [secciones]);
const { data: rankingData, isLoading } = useQuery<ApiResponseRankingMunicipio>({
queryKey: ['rankingMunicipiosPorSeccion', selectedSeccion?.value],
queryFn: () => getRankingMunicipiosPorSeccion(selectedSeccion!.value),
enabled: !!selectedSeccion,
});
return (
<div className="tabla-resultados-widget">
<div className="tabla-header">
<h3>Resultados por Municipio</h3>
<div className="header-filters">
<Select
options={displayModeOptions}
value={displayMode}
onChange={(option) => setDisplayMode(option as DisplayOption)}
styles={customSelectStyles}
isSearchable={false}
/>
<Select
options={seccionOptions}
value={selectedSeccion}
onChange={(option) => setSelectedSeccion(option)}
isLoading={secciones.length === 0}
styles={customSelectStyles}
isSearchable={false}
/>
</div>
</div>
<div className="tabla-container">
{isLoading ? <p>Cargando...</p> : !rankingData || rankingData.categorias.length === 0 ? <p>No hay datos.</p> : (
<table>
<thead>
{/* --- Fila 1: Nombres de Categorías --- */}
<tr>
<th rowSpan={3} className="sticky-col municipio-header">Municipio</th>
{rankingData.categorias.map(cat => (
<th key={cat.id} colSpan={4} className="categoria-header">
{cat.nombre}
</th>
))}
</tr>
{/* --- Fila 2: Puestos --- */}
<tr>
{rankingData.categorias.flatMap(cat => [
<th key={`${cat.id}-p1`} colSpan={2} className="puesto-header">1° Puesto</th>,
<th key={`${cat.id}-p2`} colSpan={2} className="puesto-header category-divider-header">2° Puesto</th>
])}
</tr>
{/* --- Fila 3: Sub-cabeceras (Partido y %) --- */}
<tr>
{rankingData.categorias.flatMap(cat => [
<th key={`${cat.id}-p1-partido`} className="sub-header">Partido</th>,
<th key={`${cat.id}-p1-porc`} className="sub-header">%</th>,
<th key={`${cat.id}-p2-partido`} className="sub-header category-divider-header">Partido</th>,
<th key={`${cat.id}-p2-porc`} className="sub-header">%</th>
])}
</tr>
</thead>
<tbody>
{rankingData.resultados.map(municipio => (
<tr key={municipio.municipioId}>
<td className="sticky-col">{municipio.municipioNombre}</td>
{rankingData.categorias.flatMap(cat => {
const resCategoria = municipio.resultadosPorCategoria[cat.id];
const primerPuesto = resCategoria?.ranking[0];
const segundoPuesto = resCategoria?.ranking[1];
return [
// --- Celdas para el 1° Puesto ---
<td key={`${municipio.municipioId}-${cat.id}-p1-partido`} className="cell-partido">
{primerPuesto?.nombreCorto || '-'}
</td>,
<td key={`${municipio.municipioId}-${cat.id}-p1-porc`} className="cell-porcentaje">
{primerPuesto ? <CellRenderer partido={primerPuesto} mode={displayMode.value} /> : '-'}
</td>,
// --- Celdas para el 2° Puesto ---
<td key={`${municipio.municipioId}-${cat.id}-p2-partido`} className="cell-partido category-divider">
{segundoPuesto?.nombreCorto || '-'}
</td>,
<td key={`${municipio.municipioId}-${cat.id}-p2-porc`} className="cell-porcentaje">
{segundoPuesto ? <CellRenderer partido={segundoPuesto} mode={displayMode.value} /> : '-'}
</td>
];
})}
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
);
};