Feat Widgets

- Widget de Home
- Widget Cards por Provincias
- Widget Mapa por Categorias
This commit is contained in:
2025-10-01 10:03:01 -03:00
parent 3b0eee25e6
commit a985cbfd7c
45 changed files with 1786 additions and 953 deletions

View File

@@ -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;

View File

@@ -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">

View File

@@ -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>
);
};