Feat Front Widgets Refactizados y Ajustes Backend

This commit is contained in:
2025-08-22 21:55:03 -03:00
parent 18e6e8d3c0
commit 5de9d6729c
54 changed files with 2443 additions and 1680 deletions

View File

@@ -0,0 +1,35 @@
/* src/components/BancasWidget.css */
.bancas-widget-container {
background-color: #2a2a2e;
padding: 15px 20px;
border-radius: 8px;
max-width: 800px;
margin: 20px auto;
color: #e0e0e0;
}
.bancas-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.bancas-header h4 {
margin: 0;
color: white;
font-size: 1.2em;
}
.bancas-header select {
background-color: #3a3a3a;
color: white;
border: 1px solid #555;
border-radius: 4px;
padding: 5px 10px;
}
.waffle-chart-container {
height: 300px;
font-family: system-ui, sans-serif;
}

View File

@@ -0,0 +1,91 @@
// src/components/BancasWidget.tsx
import { useState, useEffect } from 'react';
import { ResponsiveWaffle } from '@nivo/waffle';
import { getBancasPorSeccion } from '../apiService';
import type { ProyeccionBancas } from '../types';
import './BancasWidget.css';
// Paleta de colores consistente
const NIVO_COLORS = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"];
// Las Secciones Electorales de la Provincia (esto podría venir de la API en el futuro)
const secciones = [
{ id: '1', nombre: 'Primera Sección' },
{ id: '2', nombre: 'Segunda Sección' },
{ id: '3', nombre: 'Tercera Sección' },
{ id: '4', nombre: 'Cuarta Sección' },
{ id: '5', nombre: 'Quinta Sección' },
{ id: '6', nombre: 'Sexta Sección' },
{ id: '7', nombre: 'Séptima Sección' },
{ id: '8', nombre: 'Octava Sección (Capital)' },
];
export const BancasWidget = () => {
const [seccionActual, setSeccionActual] = useState('1'); // Empezamos con la Primera Sección
const [data, setData] = useState<ProyeccionBancas | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const result = await getBancasPorSeccion(seccionActual);
setData(result);
} catch (error) {
console.error(`Error cargando datos de bancas para sección ${seccionActual}:`, error);
setData(null); // Limpiar datos en caso de error
} finally {
setLoading(false);
}
};
fetchData();
}, [seccionActual]); // Se ejecuta cada vez que cambia la sección
const waffleData = data?.proyeccion.map(p => ({
id: p.agrupacionNombre,
label: p.agrupacionNombre,
value: p.bancas,
})) || [];
const totalBancas = waffleData.reduce((sum, current) => sum + current.value, 0);
return (
<div className="bancas-widget-container">
<div className="bancas-header">
<h4>Distribución de Bancas</h4>
<select value={seccionActual} onChange={e => setSeccionActual(e.target.value)}>
{secciones.map(s => <option key={s.id} value={s.id}>{s.nombre}</option>)}
</select>
</div>
<div className="waffle-chart-container">
{loading ? <p>Cargando...</p> : !data ? <p>No hay datos disponibles para esta sección.</p> :
<ResponsiveWaffle
data={waffleData}
total={totalBancas}
rows={8}
columns={10}
fillDirection="bottom"
padding={3}
colors={NIVO_COLORS}
borderColor={{ from: 'color', modifiers: [['darker', 0.3]] }}
animate={true}
legends={[
{
anchor: 'bottom',
direction: 'row',
justify: false,
translateX: 0,
translateY: 40,
itemsSpacing: 4,
itemWidth: 100,
itemHeight: 20,
itemTextColor: '#999',
itemDirection: 'left-to-right',
symbolSize: 20,
},
]}
/>}
</div>
</div>
);
};

View File

@@ -0,0 +1,82 @@
/* src/components/TickerWidget.css */
.ticker-container {
background-color: #2a2a2e;
padding: 15px 20px;
border-radius: 8px;
max-width: 800px;
margin: 20px auto;
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
color: #e0e0e0;
}
.ticker-container.loading, .ticker-container.error {
text-align: center;
padding: 30px;
font-style: italic;
color: #999;
}
.ticker-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #444;
padding-bottom: 10px;
margin-bottom: 15px;
}
.ticker-header h3 {
margin: 0;
color: white;
font-size: 1.4em;
}
.ticker-stats {
display: flex;
gap: 20px;
font-size: 0.9em;
}
.ticker-stats strong {
color: #a7c7e7;
font-size: 1.1em;
}
.ticker-results {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.ticker-party .party-info {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
font-size: 0.9em;
}
.ticker-party .party-name {
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 10px;
}
.ticker-party .party-percent {
font-weight: bold;
}
.party-bar-background {
background-color: #444;
border-radius: 4px;
height: 10px;
overflow: hidden;
}
.party-bar-foreground {
background-color: #646cff;
height: 100%;
border-radius: 4px;
transition: width 0.5s ease-in-out;
}

View File

@@ -0,0 +1,67 @@
// src/components/TickerWidget.tsx
import { useState, useEffect } from 'react';
import { getResumenProvincial } from '../apiService';
import type { ResumenProvincial } from '../types';
import './TickerWidget.css';
const formatPercent = (num: number) => `${num.toFixed(2).replace('.', ',')}%`;
const NIVO_COLORS = [
"#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd",
"#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"
];
export const TickerWidget = () => {
const [data, setData] = useState<ResumenProvincial | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const result = await getResumenProvincial();
setData(result);
} catch (error) {
console.error("Error cargando resumen provincial:", error);
} finally {
setLoading(false);
}
};
fetchData(); // Carga inicial
const intervalId = setInterval(fetchData, 30000); // Actualiza cada 30 segundos
return () => clearInterval(intervalId); // Limpia el intervalo al desmontar el componente
}, []);
if (loading) {
return <div className="ticker-container loading">Cargando resultados provinciales...</div>;
}
if (!data) {
return <div className="ticker-container error">No se pudieron cargar los datos.</div>;
}
return (
<div className="ticker-container">
<div className="ticker-header">
<h3>{data.provinciaNombre}</h3>
<div className="ticker-stats">
<span>Mesas Escrutadas: <strong>{formatPercent(data.porcentajeEscrutado)}</strong></span>
<span>Participación: <strong>{formatPercent(data.porcentajeParticipacion)}</strong></span>
</div>
</div>
<div className="ticker-results">
{data.resultados.slice(0, 3).map((partido, index) => (
<div key={`${partido.nombre}-${index}`} className="ticker-party"> {/* <-- CAMBIO AQUÍ */}
<div className="party-info">
<span className="party-name">{partido.nombre}</span>
<span className="party-percent">{formatPercent(partido.porcentaje)}</span>
</div>
<div className="party-bar-background">
<div className="party-bar-foreground" style={{ width: `${partido.porcentaje}%`, backgroundColor: NIVO_COLORS[index % NIVO_COLORS.length] }}></div>
</div>
</div>
))}
</div>
</div>
);
};