Feat Selector Modo Tabla
This commit is contained in:
@@ -2,9 +2,20 @@ 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 } from '../types/types';
|
||||
import './ResultadosTablaSeccionWidget.css'; // Reutilizamos el mismo estilo
|
||||
import React from 'react';
|
||||
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' } }),
|
||||
@@ -12,10 +23,36 @@ const customSelectStyles = {
|
||||
};
|
||||
|
||||
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 () => {
|
||||
@@ -48,91 +85,79 @@ export const ResultadosRankingMunicipioWidget = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="tabla-resultados-widget">
|
||||
<div className="tabla-header">
|
||||
<h3>Resultados por Municipio</h3>
|
||||
<Select
|
||||
options={seccionOptions}
|
||||
value={selectedSeccion}
|
||||
onChange={(option) => setSelectedSeccion(option)}
|
||||
isLoading={secciones.length === 0}
|
||||
styles={customSelectStyles}
|
||||
isSearchable={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="tabla-resultados-widget">
|
||||
<div className="tabla-header">
|
||||
<h3>Resultados por Municipio</h3>
|
||||
<div className="header-filters">
|
||||
<Select
|
||||
options={displayModeOptions}
|
||||
value={displayMode}
|
||||
|
||||
// --- CORRECCIÓN EN ONCHANGE ---
|
||||
// 'option' ahora es del tipo correcto, por lo que no necesitamos aserción.
|
||||
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={2} className="sticky-col municipio-header">Municipio</th>
|
||||
{rankingData.categorias.map(cat => (
|
||||
// Cada categoría ahora ocupa 4 columnas (1° Partido, 1° %, 2° Partido, 2° %)
|
||||
<th key={cat.id} colSpan={4} className="categoria-header">
|
||||
{cat.nombre}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
{/* --- Fila 2: Puestos y % --- */}
|
||||
<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, %) */}
|
||||
<tr>
|
||||
<th className="sub-header-init" />
|
||||
{
|
||||
// Usamos un bucle map simple en lugar de flatMap.
|
||||
// React manejará la creación de un array de nodos sin problemas.
|
||||
rankingData.categorias.map(cat => (
|
||||
// Usamos un Fragmento (<>) para agrupar los 4 <th> de cada categoría
|
||||
// sin añadir un nodo extra al DOM.
|
||||
<React.Fragment key={`subheaders-${cat.id}`}>
|
||||
<th className="sub-header">Partido</th>
|
||||
<th className="sub-header">%</th>
|
||||
<th className="sub-header category-divider-header">Partido</th>
|
||||
<th className="sub-header">%</th>
|
||||
</React.Fragment>
|
||||
))
|
||||
}
|
||||
</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];
|
||||
<div className="tabla-container">
|
||||
{isLoading ? <p>Cargando...</p> : !rankingData || rankingData.categorias.length === 0 ? <p>No hay datos.</p> : (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowSpan={2} className="sticky-col municipio-header">Municipio</th>
|
||||
{rankingData.categorias.map(cat => (
|
||||
<th key={cat.id} colSpan={2} className="categoria-header">{cat.nombre}</th>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
{rankingData.categorias.flatMap(cat => [
|
||||
<th key={`${cat.id}-1`} className="partido-header">1° Puesto</th>,
|
||||
<th key={`${cat.id}-2`} className="partido-header category-divider-header">2° Puesto</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 ? formatPercent(primerPuesto.porcentaje) : '-'}
|
||||
</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 ? formatPercent(segundoPuesto.porcentaje) : '-'}
|
||||
</td>
|
||||
];
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return [
|
||||
<td key={`${municipio.municipioId}-${cat.id}-1`} className="category-divider">
|
||||
<div className="cell-content">
|
||||
<span className="cell-partido-nombre">{primerPuesto?.nombreCorto || '-'}</span>
|
||||
<CellRenderer partido={primerPuesto} mode={displayMode.value} />
|
||||
</div>
|
||||
</td>,
|
||||
<td key={`${municipio.municipioId}-${cat.id}-2`}>
|
||||
<div className="cell-content">
|
||||
<span className="cell-partido-nombre">{segundoPuesto?.nombreCorto || '-'}</span>
|
||||
<CellRenderer partido={segundoPuesto} mode={displayMode.value} />
|
||||
</div>
|
||||
</td>
|
||||
];
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -23,6 +23,11 @@
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.header-filters {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.tabla-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
@@ -173,4 +178,32 @@
|
||||
}
|
||||
.tabla-container tbody tr:nth-of-type(even) td.sticky-col {
|
||||
background-color: #f8f9fa; /* Para que coincida con el fondo de la fila */
|
||||
}
|
||||
|
||||
/* Contenedor principal de la celda para alinear contenido */
|
||||
.cell-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
/* Nombre del partido dentro de la celda */
|
||||
.cell-partido-nombre {
|
||||
text-align: left;
|
||||
font-size: 0.85rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
/* Contenedor para la vista "ambos" (votos y porcentaje) */
|
||||
.cell-ambos {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
text-align: right;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.cell-ambos small {
|
||||
font-size: 0.8em;
|
||||
color: #6c757d;
|
||||
}
|
||||
@@ -188,6 +188,7 @@ export interface TablaDetalladaResultadoMunicipio {
|
||||
export interface RankingPartido {
|
||||
nombreCorto: string;
|
||||
porcentaje: number;
|
||||
votos: number;
|
||||
}
|
||||
|
||||
export interface RankingCategoria {
|
||||
|
||||
Reference in New Issue
Block a user