Compare commits

..

1 Commits

Author SHA1 Message Date
11d9417ef5 Pre Merge 2025-10-01 10:28:49 -03:00
161 changed files with 1077 additions and 9988 deletions

View File

@@ -36,23 +36,6 @@ td button {
margin-right: 5px;
}
.table-container {
max-height: 500px; /* Altura máxima antes de que aparezca el scroll */
overflow-y: auto; /* Habilita el scroll vertical cuando es necesario */
border: 1px solid #ddd;
border-radius: 4px;
position: relative; /* Necesario para que 'sticky' funcione correctamente */
}
/* Hacemos que la cabecera de la tabla se quede fija en la parte superior */
.table-container thead th {
position: sticky;
top: 0;
z-index: 1;
/* El color de fondo es crucial para que no se vea el contenido que pasa por debajo */
background-color: #f2f2f2;
}
.sortable-list-horizontal {
list-style: none;
padding: 8px;

View File

@@ -1,144 +1,171 @@
// EN: src/components/AgrupacionesManager.tsx
// src/components/AgrupacionesManager.tsx
import { useState, useEffect } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import Select from 'react-select';
import { getAgrupaciones, updateAgrupacion, getLogos, updateLogos } from '../services/apiService';
import type { AgrupacionPolitica, LogoAgrupacionCategoria, UpdateAgrupacionData } from '../types';
import type { AgrupacionPolitica, LogoAgrupacionCategoria } from '../types';
import './AgrupacionesManager.css';
const GLOBAL_ELECTION_ID = 0;
const ELECCION_OPTIONS = [
{ value: GLOBAL_ELECTION_ID, label: 'Global (Logo por Defecto)' },
{ value: 2, label: 'Elecciones Nacionales (Override General)' },
{ value: 1, label: 'Elecciones Provinciales (Override General)' }
];
const SENADORES_ID = 5;
const DIPUTADOS_ID = 6;
const CONCEJALES_ID = 7;
// Esta función limpia cualquier carácter no válido de un string de color.
const sanitizeColor = (color: string | null | undefined): string => {
if (!color) return '#000000';
return color.startsWith('#') ? color : `#${color}`;
if (!color) return '#000000'; // Devuelve un color válido por defecto si es nulo
// Usa una expresión regular para eliminar todo lo que no sea un '#' o un carácter hexadecimal
const sanitized = color.replace(/[^#0-9a-fA-F]/g, '');
return sanitized.startsWith('#') ? sanitized : `#${sanitized}`;
};
export const AgrupacionesManager = () => {
const queryClient = useQueryClient();
const [selectedEleccion, setSelectedEleccion] = useState(ELECCION_OPTIONS[0]);
const [editedAgrupaciones, setEditedAgrupaciones] = useState<Record<string, { nombreCorto: string | null; color: string | null; }>>({});
const [editedLogos, setEditedLogos] = useState<Record<string, string | null>>({});
const [editedAgrupaciones, setEditedAgrupaciones] = useState<Record<string, Partial<AgrupacionPolitica>>>({});
const [editedLogos, setEditedLogos] = useState<LogoAgrupacionCategoria[]>([]);
// Query 1: Obtener agrupaciones
const { data: agrupaciones = [], isLoading: isLoadingAgrupaciones } = useQuery<AgrupacionPolitica[]>({
queryKey: ['agrupaciones'], queryFn: getAgrupaciones,
queryKey: ['agrupaciones'],
queryFn: getAgrupaciones,
});
// Query 2: Obtener logos
const { data: logos = [], isLoading: isLoadingLogos } = useQuery<LogoAgrupacionCategoria[]>({
queryKey: ['allLogos'],
queryFn: () => Promise.all([getLogos(0), getLogos(1), getLogos(2)]).then(res => res.flat()),
queryKey: ['logos'],
queryFn: getLogos,
});
useEffect(() => {
if (agrupaciones.length > 0) {
const initialEdits = Object.fromEntries(
agrupaciones.map(a => [a.id, { nombreCorto: a.nombreCorto, color: a.color }])
);
setEditedAgrupaciones(initialEdits);
// Solo procedemos si los datos de agrupaciones están disponibles
if (agrupaciones && agrupaciones.length > 0) {
// Inicializamos el estado de 'editedAgrupaciones' una sola vez.
// Usamos una función en setState para asegurarnos de que solo se ejecute
// si el estado está vacío, evitando sobreescribir ediciones del usuario.
setEditedAgrupaciones(prev => {
if (Object.keys(prev).length === 0) {
return Object.fromEntries(agrupaciones.map(a => [a.id, {}]));
}
return prev;
});
}
}, [agrupaciones]);
useEffect(() => {
if (logos) {
const logoMap = Object.fromEntries(
logos
// --- CORRECCIÓN CLAVE 1: Comprobar contra `null` en lugar de `0` ---
.filter(l => l.categoriaId === 0 && l.ambitoGeograficoId === null)
.map(l => [`${l.agrupacionPoliticaId}-${l.eleccionId}`, l.logoUrl])
);
setEditedLogos(logoMap);
// Hacemos lo mismo para los logos
if (logos && logos.length > 0) {
setEditedLogos(prev => {
if (prev.length === 0) {
// Creamos una copia profunda para evitar mutaciones accidentales
return JSON.parse(JSON.stringify(logos));
}
return prev;
});
}
}, [logos]);
// La dependencia ahora es el estado de carga. El hook se ejecutará cuando
// isLoadingAgrupaciones o isLoadingLogos cambien de true a false.
}, [agrupaciones, logos]);
const handleInputChange = (id: string, field: 'nombreCorto' | 'color', value: string | null) => {
setEditedAgrupaciones(prev => ({ ...prev, [id]: { ...prev[id], [field]: value } }));
const handleInputChange = (id: string, field: 'nombreCorto' | 'color', value: string) => {
setEditedAgrupaciones(prev => ({
...prev,
[id]: { ...prev[id], [field]: value }
}));
};
const handleLogoInputChange = (agrupacionId: string, value: string | null) => {
const key = `${agrupacionId}-${selectedEleccion.value}`;
setEditedLogos(prev => ({ ...prev, [key]: value }));
const handleLogoChange = (agrupacionId: string, categoriaId: number, value: string) => {
setEditedLogos(prev => {
const newLogos = [...prev];
const existing = newLogos.find(l =>
l.agrupacionPoliticaId === agrupacionId &&
l.categoriaId === categoriaId &&
l.ambitoGeograficoId == null
);
if (existing) {
existing.logoUrl = value;
} else {
newLogos.push({
id: 0,
agrupacionPoliticaId: agrupacionId,
categoriaId,
logoUrl: value,
ambitoGeograficoId: null
});
}
return newLogos;
});
};
const handleSaveAll = async () => {
try {
const agrupacionPromises = agrupaciones.map(agrupacion => {
const changes = editedAgrupaciones[agrupacion.id] || {};
const payload: UpdateAgrupacionData = {
nombreCorto: changes.nombreCorto ?? agrupacion.nombreCorto,
color: changes.color ?? agrupacion.color,
};
return updateAgrupacion(agrupacion.id, payload);
const agrupacionPromises = Object.entries(editedAgrupaciones).map(([id, changes]) => {
if (Object.keys(changes).length > 0) {
const original = agrupaciones.find(a => a.id === id);
if (original) { // Chequeo de seguridad
return updateAgrupacion(id, { ...original, ...changes });
}
}
return Promise.resolve();
});
// --- CORRECCIÓN CLAVE 2: Enviar `null` a la API en lugar de `0` ---
const logosPayload = Object.entries(editedLogos)
.map(([key, logoUrl]) => {
const [agrupacionPoliticaId, eleccionIdStr] = key.split('-');
return { id: 0, eleccionId: parseInt(eleccionIdStr), agrupacionPoliticaId, categoriaId: 0, logoUrl: logoUrl || null, ambitoGeograficoId: null };
});
const logoPromise = updateLogos(logosPayload);
const logoPromise = updateLogos(editedLogos);
await Promise.all([...agrupacionPromises, logoPromise]);
await queryClient.invalidateQueries({ queryKey: ['agrupaciones'] });
await queryClient.invalidateQueries({ queryKey: ['allLogos'] });
queryClient.invalidateQueries({ queryKey: ['agrupaciones'] });
queryClient.invalidateQueries({ queryKey: ['logos'] });
alert('¡Todos los cambios han sido guardados!');
} catch (err) { console.error("Error al guardar todo:", err); alert("Ocurrió un error."); }
};
const getLogoValue = (agrupacionId: string): string => {
const key = `${agrupacionId}-${selectedEleccion.value}`;
return editedLogos[key] ?? '';
} catch (err) {
console.error("Error al guardar todo:", err);
alert("Ocurrió un error al guardar los cambios.");
}
};
const isLoading = isLoadingAgrupaciones || isLoadingLogos;
const getLogoUrl = (agrupacionId: string, categoriaId: number) => {
return editedLogos.find(l =>
l.agrupacionPoliticaId === agrupacionId &&
l.categoriaId === categoriaId &&
l.ambitoGeograficoId == null
)?.logoUrl || '';
};
return (
<div className="admin-module">
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
<h3>Gestión de Agrupaciones y Logos</h3>
<div style={{width: '350px', zIndex: 100 }}>
<Select options={ELECCION_OPTIONS} value={selectedEleccion} onChange={(opt) => setSelectedEleccion(opt!)} />
</div>
</div>
<h3>Gestión de Agrupaciones y Logos</h3>
{isLoading ? <p>Cargando...</p> : (
<>
<div className="table-container">
<table>
<thead>
<tr>
<th>Nombre</th>
<th>Nombre Corto</th>
<th>Color</th>
<th>Logo</th>
<table>
<thead>
<tr>
<th>Nombre</th>
<th>Nombre Corto</th>
<th>Color</th>
<th>Logo Senadores</th>
<th>Logo Diputados</th>
<th>Logo Concejales</th>
</tr>
</thead>
<tbody>
{agrupaciones.map(agrupacion => (
<tr key={agrupacion.id}>
<td>{agrupacion.nombre}</td>
<td><input type="text" value={editedAgrupaciones[agrupacion.id]?.nombreCorto ?? agrupacion.nombreCorto ?? ''} onChange={(e) => handleInputChange(agrupacion.id, 'nombreCorto', e.target.value)} /></td>
<td>
<input
type="color"
// Usamos la función sanitizeColor para asegurarnos de que el valor sea siempre válido
value={sanitizeColor(editedAgrupaciones[agrupacion.id]?.color ?? agrupacion.color)}
onChange={(e) => handleInputChange(agrupacion.id, 'color', e.target.value)}
/>
</td>
<td><input type="text" placeholder="URL de la imagen" value={getLogoUrl(agrupacion.id, SENADORES_ID)} onChange={(e) => handleLogoChange(agrupacion.id, SENADORES_ID, e.target.value)} /></td>
<td><input type="text" placeholder="URL de la imagen" value={getLogoUrl(agrupacion.id, DIPUTADOS_ID)} onChange={(e) => handleLogoChange(agrupacion.id, DIPUTADOS_ID, e.target.value)} /></td>
<td><input type="text" placeholder="URL de la imagen" value={getLogoUrl(agrupacion.id, CONCEJALES_ID)} onChange={(e) => handleLogoChange(agrupacion.id, CONCEJALES_ID, e.target.value)} /></td>
</tr>
</thead>
<tbody>
{agrupaciones.map(agrupacion => (
<tr key={agrupacion.id}>
<td>{agrupacion.nombre}</td>
<td><input type="text" value={editedAgrupaciones[agrupacion.id]?.nombreCorto ?? ''} onChange={(e) => handleInputChange(agrupacion.id, 'nombreCorto', e.target.value)} /></td>
<td><input type="color" value={sanitizeColor(editedAgrupaciones[agrupacion.id]?.color)} onChange={(e) => handleInputChange(agrupacion.id, 'color', e.target.value)} /></td>
<td>
<input
type="text"
placeholder="URL..."
value={getLogoValue(agrupacion.id)}
onChange={(e) => handleLogoInputChange(agrupacion.id, e.target.value)}
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
))}
</tbody>
</table>
<button onClick={handleSaveAll} style={{ marginTop: '1rem' }}>
Guardar Todos los Cambios
</button>

View File

@@ -1,4 +1,4 @@
// src/components/BancasProvincialesManager.tsx
// src/components/BancasManager.tsx
import { useState } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { getBancadas, getAgrupaciones, updateBancada, type UpdateBancadaData } from '../services/apiService';
@@ -6,10 +6,9 @@ import type { Bancada, AgrupacionPolitica } from '../types';
import { OcupantesModal } from './OcupantesModal';
import './AgrupacionesManager.css';
const ELECCION_ID_PROVINCIAL = 1;
const camaras = ['diputados', 'senadores'] as const;
export const BancasProvincialesManager = () => {
export const BancasManager = () => {
const [activeTab, setActiveTab] = useState<'diputados' | 'senadores'>('diputados');
const [modalVisible, setModalVisible] = useState(false);
const [bancadaSeleccionada, setBancadaSeleccionada] = useState<Bancada | null>(null);
@@ -20,18 +19,16 @@ export const BancasProvincialesManager = () => {
queryFn: getAgrupaciones
});
// --- CORRECCIÓN CLAVE ---
// 1. La queryKey ahora incluye el eleccionId para ser única.
// 2. La función queryFn ahora pasa el ELECCION_ID_PROVINCIAL a getBancadas.
const { data: bancadas = [], isLoading, error } = useQuery<Bancada[]>({
queryKey: ['bancadas', activeTab, ELECCION_ID_PROVINCIAL],
queryFn: () => getBancadas(activeTab, ELECCION_ID_PROVINCIAL),
queryKey: ['bancadas', activeTab],
queryFn: () => getBancadas(activeTab),
});
const handleAgrupacionChange = async (bancadaId: number, nuevaAgrupacionId: string | null) => {
const bancadaActual = bancadas.find(b => b.id === bancadaId);
if (!bancadaActual) return;
// Si se desasigna el partido (vacante), también se limpia el ocupante
const payload: UpdateBancadaData = {
agrupacionPoliticaId: nuevaAgrupacionId,
nombreOcupante: nuevaAgrupacionId ? (bancadaActual.ocupante?.nombreOcupante ?? null) : null,
@@ -41,7 +38,7 @@ export const BancasProvincialesManager = () => {
try {
await updateBancada(bancadaId, payload);
queryClient.invalidateQueries({ queryKey: ['bancadas', activeTab, ELECCION_ID_PROVINCIAL] });
queryClient.invalidateQueries({ queryKey: ['bancadas', activeTab] });
} catch (err) {
alert("Error al guardar el cambio de agrupación.");
}
@@ -52,12 +49,12 @@ export const BancasProvincialesManager = () => {
setModalVisible(true);
};
if (error) return <p style={{ color: 'red' }}>Error al cargar las bancas provinciales.</p>;
if (error) return <p style={{ color: 'red' }}>Error al cargar las bancas.</p>;
return (
<div className="admin-module">
<h3>Gestión de Bancas (Provinciales)</h3>
<p>Asigne partidos y ocupantes a las bancas de la legislatura provincial.</p>
<h2>Gestión de Ocupación de Bancas</h2>
<p>Asigne a cada banca física un partido político y, opcionalmente, los datos de la persona que la ocupa.</p>
<div className="chamber-tabs">
{camaras.map(camara => (
@@ -66,7 +63,7 @@ export const BancasProvincialesManager = () => {
className={activeTab === camara ? 'active' : ''}
onClick={() => setActiveTab(camara)}
>
{camara === 'diputados' ? 'Diputados Provinciales (92)' : 'Senadores Provinciales (46)'}
{camara === 'diputados' ? 'Cámara de Diputados' : 'Cámara de Senadores'}
</button>
))}
</div>
@@ -84,7 +81,16 @@ export const BancasProvincialesManager = () => {
<tbody>
{bancadas.map((bancada) => (
<tr key={bancada.id}>
<td>{`${activeTab.charAt(0).toUpperCase()}-${bancada.numeroBanca}`}</td>
{/* Usamos el NumeroBanca para la etiqueta visual */}
<td>
{`${activeTab.charAt(0).toUpperCase()}-${bancada.numeroBanca}`}
{((activeTab === 'diputados' && bancada.numeroBanca === 92) ||
(activeTab === 'senadores' && bancada.numeroBanca === 46)) && (
<span style={{ marginLeft: '8px', fontSize: '0.8em', color: '#666', fontStyle: 'italic' }}>
(Presidencia)
</span>
)}
</td>
<td>
<select
value={bancada.agrupacionPoliticaId || ''}
@@ -96,7 +102,11 @@ export const BancasProvincialesManager = () => {
</td>
<td>{bancada.ocupante?.nombreOcupante || 'Sin asignar'}</td>
<td>
<button disabled={!bancada.agrupacionPoliticaId} onClick={() => handleOpenModal(bancada)}>
<button
// El botón se habilita solo si hay un partido asignado a la banca
disabled={!bancada.agrupacionPoliticaId}
onClick={() => handleOpenModal(bancada)}
>
Editar Ocupante
</button>
</td>

View File

@@ -1,117 +0,0 @@
// src/components/BancasNacionalesManager.tsx
import { useState } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { getBancadas, getAgrupaciones, updateBancada, type UpdateBancadaData } from '../services/apiService';
import type { Bancada, AgrupacionPolitica } from '../types';
import { OcupantesModal } from './OcupantesModal';
import './AgrupacionesManager.css';
const ELECCION_ID_NACIONAL = 2;
const camaras = ['diputados', 'senadores'] as const;
export const BancasNacionalesManager = () => {
const [activeTab, setActiveTab] = useState<'diputados' | 'senadores'>('diputados');
const [modalVisible, setModalVisible] = useState(false);
const [bancadaSeleccionada, setBancadaSeleccionada] = useState<Bancada | null>(null);
const queryClient = useQueryClient();
const { data: agrupaciones = [] } = useQuery<AgrupacionPolitica[]>({
queryKey: ['agrupaciones'],
queryFn: getAgrupaciones
});
const { data: bancadas = [], isLoading, error } = useQuery<Bancada[]>({
queryKey: ['bancadas', activeTab, ELECCION_ID_NACIONAL],
queryFn: () => getBancadas(activeTab, ELECCION_ID_NACIONAL),
});
const handleAgrupacionChange = async (bancadaId: number, nuevaAgrupacionId: string | null) => {
const bancadaActual = bancadas.find(b => b.id === bancadaId);
if (!bancadaActual) return;
const payload: UpdateBancadaData = {
agrupacionPoliticaId: nuevaAgrupacionId,
nombreOcupante: nuevaAgrupacionId ? (bancadaActual.ocupante?.nombreOcupante ?? null) : null,
fotoUrl: nuevaAgrupacionId ? (bancadaActual.ocupante?.fotoUrl ?? null) : null,
periodo: nuevaAgrupacionId ? (bancadaActual.ocupante?.periodo ?? null) : null,
};
try {
await updateBancada(bancadaId, payload);
queryClient.invalidateQueries({ queryKey: ['bancadas', activeTab, ELECCION_ID_NACIONAL] });
} catch (err) {
alert("Error al guardar el cambio de agrupación.");
}
};
const handleOpenModal = (bancada: Bancada) => {
setBancadaSeleccionada(bancada);
setModalVisible(true);
};
if (error) return <p style={{ color: 'red' }}>Error al cargar las bancas nacionales.</p>;
return (
<div className="admin-module">
<h3>Gestión de Bancas (Nacionales)</h3>
<p>Asigne partidos y ocupantes a las bancas del Congreso de la Nación.</p>
<div className="chamber-tabs">
{camaras.map(camara => (
<button
key={camara}
className={activeTab === camara ? 'active' : ''}
onClick={() => setActiveTab(camara)}
>
{camara === 'diputados' ? 'Diputados Nacionales (257)' : 'Senadores Nacionales (72)'}
</button>
))}
</div>
{isLoading ? <p>Cargando bancas...</p> : (
<div className="table-container">
<table>
<thead>
<tr>
<th style={{ width: '15%' }}>Banca #</th>
<th style={{ width: '35%' }}>Partido Asignado</th>
<th style={{ width: '30%' }}>Ocupante Actual</th>
<th style={{ width: '20%' }}>Acciones</th>
</tr>
</thead>
<tbody>
{bancadas.map((bancada) => (
<tr key={bancada.id}>
<td>{`${activeTab.charAt(0).toUpperCase()}-${bancada.numeroBanca}`}</td>
<td>
<select
value={bancada.agrupacionPoliticaId || ''}
onChange={(e) => handleAgrupacionChange(bancada.id, e.target.value || null)}
>
<option value="">-- Vacante --</option>
{agrupaciones.map(a => <option key={a.id} value={a.id}>{a.nombre}</option>)}
</select>
</td>
<td>{bancada.ocupante?.nombreOcupante || 'Sin asignar'}</td>
<td>
<button disabled={!bancada.agrupacionPoliticaId} onClick={() => handleOpenModal(bancada)}>
Editar Ocupante
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{modalVisible && bancadaSeleccionada && (
<OcupantesModal
bancada={bancadaSeleccionada}
onClose={() => setModalVisible(false)}
activeTab={activeTab}
/>
)}
</div>
);
};

View File

@@ -1,125 +0,0 @@
// src/components/BancasPreviasManager.tsx
import { useState, useEffect } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { getBancasPrevias, updateBancasPrevias, getAgrupaciones } from '../services/apiService';
import type { BancaPrevia, AgrupacionPolitica } from '../types';
import { TipoCamara } from '../types';
const ELECCION_ID_NACIONAL = 2;
export const BancasPreviasManager = () => {
const queryClient = useQueryClient();
const [editedBancas, setEditedBancas] = useState<Record<string, Partial<BancaPrevia>>>({});
const { data: agrupaciones = [], isLoading: isLoadingAgrupaciones } = useQuery<AgrupacionPolitica[]>({
queryKey: ['agrupaciones'],
queryFn: getAgrupaciones,
});
const { data: bancasPrevias = [], isLoading: isLoadingBancas } = useQuery<BancaPrevia[]>({
queryKey: ['bancasPrevias', ELECCION_ID_NACIONAL],
queryFn: () => getBancasPrevias(ELECCION_ID_NACIONAL),
});
useEffect(() => {
if (agrupaciones.length > 0) {
const initialData: Record<string, Partial<BancaPrevia>> = {};
agrupaciones.forEach(agrupacion => {
// Para Diputados
const keyDip = `${agrupacion.id}-${TipoCamara.Diputados}`;
const existingDip = bancasPrevias.find(b => b.agrupacionPoliticaId === agrupacion.id && b.camara === TipoCamara.Diputados);
initialData[keyDip] = { cantidad: existingDip?.cantidad || 0 };
// Para Senadores
const keySen = `${agrupacion.id}-${TipoCamara.Senadores}`;
const existingSen = bancasPrevias.find(b => b.agrupacionPoliticaId === agrupacion.id && b.camara === TipoCamara.Senadores);
initialData[keySen] = { cantidad: existingSen?.cantidad || 0 };
});
setEditedBancas(initialData);
}
}, [agrupaciones, bancasPrevias]);
const handleInputChange = (agrupacionId: string, camara: typeof TipoCamara.Diputados | typeof TipoCamara.Senadores, value: string) => {
const key = `${agrupacionId}-${camara}`;
const cantidad = parseInt(value, 10);
setEditedBancas(prev => ({
...prev,
[key]: { ...prev[key], cantidad: isNaN(cantidad) ? 0 : cantidad }
}));
};
const handleSave = async () => {
const payload: BancaPrevia[] = Object.entries(editedBancas)
.map(([key, value]) => {
const [agrupacionPoliticaId, camara] = key.split('-');
return {
id: 0,
eleccionId: ELECCION_ID_NACIONAL,
agrupacionPoliticaId,
camara: parseInt(camara) as typeof TipoCamara.Diputados | typeof TipoCamara.Senadores,
cantidad: value.cantidad || 0,
};
})
.filter(b => b.cantidad > 0);
try {
await updateBancasPrevias(ELECCION_ID_NACIONAL, payload);
queryClient.invalidateQueries({ queryKey: ['bancasPrevias', ELECCION_ID_NACIONAL] });
alert('Bancas previas guardadas con éxito.');
} catch (error) {
console.error(error);
alert('Error al guardar las bancas previas.');
}
};
const totalDiputados = Object.entries(editedBancas).reduce((sum, [key, value]) => key.endsWith(`-${TipoCamara.Diputados}`) ? sum + (value.cantidad || 0) : sum, 0);
const totalSenadores = Object.entries(editedBancas).reduce((sum, [key, value]) => key.endsWith(`-${TipoCamara.Senadores}`) ? sum + (value.cantidad || 0) : sum, 0);
const isLoading = isLoadingAgrupaciones || isLoadingBancas;
return (
<div className="admin-module">
<h3>Gestión de Bancas Previas (Composición Nacional)</h3>
<p>Define cuántas bancas retiene cada partido antes de la elección. Estos son los escaños que **no** están en juego.</p>
{isLoading ? <p>Cargando...</p> : (
<>
<div className="table-container">
<table>
<thead>
<tr>
<th>Agrupación Política</th>
<th>Bancas Previas Diputados (Total: {totalDiputados} / 130)</th>
<th>Bancas Previas Senadores (Total: {totalSenadores} / 48)</th>
</tr>
</thead>
<tbody>
{agrupaciones.map(agrupacion => (
<tr key={agrupacion.id}>
<td>{agrupacion.nombre}</td>
<td>
<input
type="number"
min="0"
value={editedBancas[`${agrupacion.id}-${TipoCamara.Diputados}`]?.cantidad || 0}
onChange={e => handleInputChange(agrupacion.id, TipoCamara.Diputados, e.target.value)}
/>
</td>
<td>
<input
type="number"
min="0"
value={editedBancas[`${agrupacion.id}-${TipoCamara.Senadores}`]?.cantidad || 0}
onChange={e => handleInputChange(agrupacion.id, TipoCamara.Senadores, e.target.value)}
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
<button onClick={handleSave} style={{ marginTop: '1rem' }}>Guardar Bancas Previas</button>
</>
)}
</div>
);
};

View File

@@ -1,101 +1,96 @@
// src/components/CandidatoOverridesManager.tsx
import { useState, useMemo, useEffect } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import Select from 'react-select';
import { getProvinciasForAdmin, getMunicipiosForAdmin, getAgrupaciones, getCandidatos, updateCandidatos } from '../services/apiService';
import type { MunicipioSimple, AgrupacionPolitica, CandidatoOverride, ProvinciaSimple } from '../types';
import { CATEGORIAS_NACIONALES_OPTIONS, CATEGORIAS_PROVINCIALES_OPTIONS } from '../constants/categorias';
import { getMunicipiosForAdmin, getAgrupaciones, getCandidatos, updateCandidatos } from '../services/apiService';
import type { MunicipioSimple, AgrupacionPolitica, CandidatoOverride } from '../types';
const ELECCION_OPTIONS = [
{ value: 2, label: 'Elecciones Nacionales' },
{ value: 1, label: 'Elecciones Provinciales' }
];
const AMBITO_LEVEL_OPTIONS = [
{ value: 'general', label: 'General (Toda la elección)' },
{ value: 'provincia', label: 'Por Provincia' },
{ value: 'municipio', label: 'Por Municipio' }
const CATEGORIAS_OPTIONS = [
{ value: 5, label: 'Senadores' },
{ value: 6, label: 'Diputados' },
{ value: 7, label: 'Concejales' }
];
export const CandidatoOverridesManager = () => {
const queryClient = useQueryClient();
const [selectedEleccion, setSelectedEleccion] = useState(ELECCION_OPTIONS[0]);
const [selectedAmbitoLevel, setSelectedAmbitoLevel] = useState(AMBITO_LEVEL_OPTIONS[0]);
const [selectedProvincia, setSelectedProvincia] = useState<ProvinciaSimple | null>(null);
const [selectedMunicipio, setSelectedMunicipio] = useState<MunicipioSimple | null>(null);
const [selectedCategoria, setSelectedCategoria] = useState<{ value: number; label: string } | null>(null);
const [selectedAgrupacion, setSelectedAgrupacion] = useState<AgrupacionPolitica | null>(null);
const [nombreCandidato, setNombreCandidato] = useState('');
const { data: provincias = [] } = useQuery<ProvinciaSimple[]>({ queryKey: ['provinciasForAdmin'], queryFn: getProvinciasForAdmin });
const { data: municipios = [] } = useQuery<MunicipioSimple[]>({ queryKey: ['municipiosForAdmin'], queryFn: getMunicipiosForAdmin });
const { data: agrupaciones = [] } = useQuery<AgrupacionPolitica[]>({ queryKey: ['agrupaciones'], queryFn: getAgrupaciones });
const { data: candidatos = [] } = useQuery<CandidatoOverride[]>({
queryKey: ['candidatos', selectedEleccion.value],
queryFn: () => getCandidatos(selectedEleccion.value),
});
const { data: candidatos = [] } = useQuery<CandidatoOverride[]>({ queryKey: ['candidatos'], queryFn: getCandidatos });
const categoriaOptions = selectedEleccion.value === 2 ? CATEGORIAS_NACIONALES_OPTIONS : CATEGORIAS_PROVINCIALES_OPTIONS;
const [selectedCategoria, setSelectedCategoria] = useState<{ value: number; label: string } | null>(null);
const [selectedMunicipio, setSelectedMunicipio] = useState<{ value: string; label: string } | null>(null);
const [selectedAgrupacion, setSelectedAgrupacion] = useState<{ value: string; label: string } | null>(null);
const [nombreCandidato, setNombreCandidato] = useState('');
const getAmbitoId = () => {
if (selectedAmbitoLevel.value === 'municipio' && selectedMunicipio) return parseInt(selectedMunicipio.id);
if (selectedAmbitoLevel.value === 'provincia' && selectedProvincia) return parseInt(selectedProvincia.id);
return null;
};
const municipioOptions = useMemo(() =>
// Añadimos la opción "General" que representará un ámbito nulo
[{ value: 'general', label: 'General (Todos los Municipios)' }, ...municipios.map(m => ({ value: m.id, label: m.nombre }))]
, [municipios]);
const agrupacionOptions = useMemo(() => agrupaciones.map(a => ({ value: a.id, label: a.nombre })), [agrupaciones]);
const currentCandidato = useMemo(() => {
if (!selectedAgrupacion || !selectedCategoria) return '';
const ambitoId = getAmbitoId();
return candidatos.find(c =>
c.ambitoGeograficoId === ambitoId &&
c.agrupacionPoliticaId === selectedAgrupacion.id &&
// Determina si estamos buscando un override general (null) o específico (ID numérico)
const ambitoIdBuscado = selectedMunicipio?.value === 'general' ? null : (selectedMunicipio ? parseInt(selectedMunicipio.value) : undefined);
// Si no se ha seleccionado un municipio, no buscamos nada
if (ambitoIdBuscado === undefined) return '';
return candidatos.find(c =>
c.ambitoGeograficoId === ambitoIdBuscado &&
c.agrupacionPoliticaId === selectedAgrupacion.value &&
c.categoriaId === selectedCategoria.value
)?.nombreCandidato || '';
}, [candidatos, selectedAmbitoLevel, selectedProvincia, selectedMunicipio, selectedAgrupacion, selectedCategoria]);
useEffect(() => { setNombreCandidato(currentCandidato || ''); }, [currentCandidato]);
}, [candidatos, selectedMunicipio, selectedAgrupacion, selectedCategoria]);
useEffect(() => { setNombreCandidato(currentCandidato) }, [currentCandidato]);
const handleSave = async () => {
if (!selectedAgrupacion || !selectedCategoria) return;
if (!selectedMunicipio || !selectedAgrupacion || !selectedCategoria) return;
const ambitoIdParaEnviar = selectedMunicipio.value === 'general'
? null
: parseInt(selectedMunicipio.value);
const newCandidatoEntry: CandidatoOverride = {
id: 0,
eleccionId: selectedEleccion.value,
agrupacionPoliticaId: selectedAgrupacion.id,
id: 0, // El backend no lo necesita para el upsert
agrupacionPoliticaId: selectedAgrupacion.value,
categoriaId: selectedCategoria.value,
ambitoGeograficoId: getAmbitoId(),
ambitoGeograficoId: ambitoIdParaEnviar,
nombreCandidato: nombreCandidato || null
};
try {
await updateCandidatos([newCandidatoEntry]);
queryClient.invalidateQueries({ queryKey: ['candidatos', selectedEleccion.value] });
queryClient.invalidateQueries({ queryKey: ['candidatos'] });
alert('Override de candidato guardado.');
} catch (error) {
console.error(error);
alert('Error al guardar el override del candidato.');
}
};
return (
<div className="admin-module">
<h3>Overrides de Nombres de Candidatos</h3>
<p>Configure un nombre de candidato específico para un partido en un contexto determinado.</p>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem', alignItems: 'flex-end' }}>
<Select options={ELECCION_OPTIONS} value={selectedEleccion} onChange={(opt) => { setSelectedEleccion(opt!); setSelectedCategoria(null); }} />
<Select options={categoriaOptions} value={selectedCategoria} onChange={setSelectedCategoria} placeholder="Seleccione Categoría..." isDisabled={!selectedEleccion} />
<Select options={agrupaciones.map(a => ({ value: a.id, label: a.nombre, ...a }))} getOptionValue={opt => opt.id} getOptionLabel={opt => opt.nombre} value={selectedAgrupacion} onChange={setSelectedAgrupacion} placeholder="Seleccione Agrupación..." />
<Select options={AMBITO_LEVEL_OPTIONS} value={selectedAmbitoLevel} onChange={(opt) => { setSelectedAmbitoLevel(opt!); setSelectedProvincia(null); setSelectedMunicipio(null); }} />
{selectedAmbitoLevel.value === 'provincia' || selectedAmbitoLevel.value === 'municipio' ? (
<Select options={provincias.map(p => ({ value: p.id, label: p.nombre, ...p }))} getOptionValue={opt => opt.id} getOptionLabel={opt => opt.nombre} value={selectedProvincia} onChange={setSelectedProvincia} placeholder="Seleccione Provincia..." />
) : <div />}
{selectedAmbitoLevel.value === 'municipio' ? (
<Select options={municipios.map(m => ({ value: m.id, label: m.nombre, ...m }))} getOptionValue={opt => opt.id} getOptionLabel={opt => opt.nombre} value={selectedMunicipio} onChange={setSelectedMunicipio} placeholder="Seleccione Municipio..." isDisabled={!selectedProvincia} />
) : <div />}
</div>
<div style={{ display: 'flex', gap: '1rem', alignItems: 'flex-end', marginTop: '1rem' }}>
<p>Configure un nombre de candidato específico para un partido, categoría y municipio (o general).</p>
<div style={{ display: 'flex', gap: '1rem', alignItems: 'flex-end', flexWrap: 'wrap' }}>
<div style={{ flex: 1 }}>
<label>Categoría</label>
<Select options={CATEGORIAS_OPTIONS} value={selectedCategoria} onChange={setSelectedCategoria} isClearable placeholder="Seleccione..."/>
</div>
<div style={{ flex: 1 }}>
<label>Municipio (Opcional)</label>
<Select options={municipioOptions} value={selectedMunicipio} onChange={setSelectedMunicipio} isClearable placeholder="General..."/>
</div>
<div style={{ flex: 1 }}>
<label>Agrupación</label>
<Select options={agrupacionOptions} value={selectedAgrupacion} onChange={setSelectedAgrupacion} isClearable placeholder="Seleccione..."/>
</div>
<div style={{ flex: 2 }}>
<label>Nombre del Candidato</label>
<input type="text" value={nombreCandidato} onChange={e => setNombreCandidato(e.target.value)} style={{ width: '100%' }} disabled={!selectedAgrupacion || !selectedCategoria} />
</div>

View File

@@ -1,110 +0,0 @@
// src/components/ConfiguracionNacional.tsx
import { useState, useEffect } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { getAgrupaciones, getConfiguracion, updateConfiguracion } from '../services/apiService';
import type { AgrupacionPolitica } from '../types';
import './AgrupacionesManager.css';
export const ConfiguracionNacional = () => {
const queryClient = useQueryClient();
const [agrupaciones, setAgrupaciones] = useState<AgrupacionPolitica[]>([]);
const [loading, setLoading] = useState(true);
const [presidenciaDiputadosId, setPresidenciaDiputadosId] = useState<string>('');
const [presidenciaSenadoId, setPresidenciaSenadoId] = useState<string>('');
const [modoOficialActivo, setModoOficialActivo] = useState(false);
const [diputadosTipoBanca, setDiputadosTipoBanca] = useState<'ganada' | 'previa'>('ganada');
// El estado para el tipo de banca del senado ya no es necesario para la UI,
// pero lo mantenemos para no romper el handleSave.
const [senadoTipoBanca, setSenadoTipoBanca] = useState<'ganada' | 'previa'>('ganada');
useEffect(() => {
const loadInitialData = async () => {
try {
setLoading(true);
const [agrupacionesData, configData] = await Promise.all([getAgrupaciones(), getConfiguracion()]);
setAgrupaciones(agrupacionesData);
setPresidenciaDiputadosId(configData.PresidenciaDiputadosNacional || '');
setPresidenciaSenadoId(configData.PresidenciaSenadoNacional || '');
setModoOficialActivo(configData.UsarDatosOficialesNacionales === 'true');
setDiputadosTipoBanca(configData.PresidenciaDiputadosNacional_TipoBanca === 'previa' ? 'previa' : 'ganada');
setSenadoTipoBanca(configData.PresidenciaSenadoNacional_TipoBanca === 'previa' ? 'previa' : 'ganada');
} catch (err) { console.error("Error al cargar datos de configuración nacional:", err); }
finally { setLoading(false); }
};
loadInitialData();
}, []);
const handleSave = async () => {
try {
await updateConfiguracion({
"PresidenciaDiputadosNacional": presidenciaDiputadosId,
"PresidenciaSenadoNacional": presidenciaSenadoId,
"UsarDatosOficialesNacionales": modoOficialActivo.toString(),
"PresidenciaDiputadosNacional_TipoBanca": diputadosTipoBanca,
// Aunque no se muestre, guardamos el valor para consistencia
"PresidenciaSenadoNacional_TipoBanca": senadoTipoBanca,
});
queryClient.invalidateQueries({ queryKey: ['composicionNacional'] });
alert('Configuración nacional guardada.');
} catch { alert('Error al guardar.'); }
};
if (loading) return <div className="admin-module"><p>Cargando...</p></div>;
return (
<div className="admin-module">
<h3>Configuración de Widgets Nacionales</h3>
{/*<div className="form-group">
<label>
<input type="checkbox" checked={modoOficialActivo} onChange={e => setModoOficialActivo(e.target.checked)} />
**Activar Modo "Resultados Oficiales" para Widgets Nacionales**
</label>
<p style={{ fontSize: '0.8rem', color: '#666' }}>
Si está activo, los widgets nacionales usarán la composición manual de bancas. Si no, usarán la proyección en tiempo real.
</p>
</div>*/}
<div style={{ display: 'flex', gap: '2rem', marginTop: '1rem' }}>
{/* Columna Diputados */}
<div style={{ flex: 1, borderRight: '1px solid #ccc', paddingRight: '1rem' }}>
<label htmlFor="presidencia-diputados-nacional" style={{ display: 'block', fontWeight: 'bold', marginBottom: '0.5rem' }}>
Presidencia Cámara de Diputados
</label>
<p style={{ fontSize: '0.8rem', color: '#666', margin: '0.5rem 0 0 0' }}>
Este escaño es parte de los 257 diputados y se descuenta del total del partido.
</p>
<select id="presidencia-diputados-nacional" value={presidenciaDiputadosId} onChange={e => setPresidenciaDiputadosId(e.target.value)} style={{ width: '100%', padding: '8px', marginBottom: '0.5rem' }}>
<option value="">-- No Asignado --</option>
{agrupaciones.map(a => (<option key={a.id} value={a.id}>{a.nombre}</option>))}
</select>
{presidenciaDiputadosId && (
<div>
<label><input type="radio" value="ganada" checked={diputadosTipoBanca === 'ganada'} onChange={() => setDiputadosTipoBanca('ganada')} /> Descontar de Banca Ganada</label>
<label style={{marginLeft: '1rem'}}><input type="radio" value="previa" checked={diputadosTipoBanca === 'previa'} onChange={() => setDiputadosTipoBanca('previa')} /> Descontar de Banca Previa</label>
</div>
)}
</div>
{/* Columna Senadores */}
<div style={{ flex: 1 }}>
<label htmlFor="presidencia-senado-nacional" style={{ display: 'block', fontWeight: 'bold', marginBottom: '0.5rem' }}>
Presidencia Senado (Vicepresidente)
</label>
<p style={{ fontSize: '0.8rem', color: '#666', margin: '0.5rem 0 0 0' }}>
Este escaño es adicional a los 72 senadores y no se descuenta del total del partido.
</p>
<select id="presidencia-senado-nacional" value={presidenciaSenadoId} onChange={e => setPresidenciaSenadoId(e.target.value)} style={{ width: '100%', padding: '8px' }}>
<option value="">-- No Asignado --</option>
{agrupaciones.map(a => (<option key={a.id} value={a.id}>{a.nombre}</option>))}
</select>
</div>
</div>
<button onClick={handleSave} style={{ marginTop: '1.5rem' }}>
Guardar Configuración
</button>
</div>
);
};

View File

@@ -1,89 +1,43 @@
// src/components/DashboardPage.tsx
import { useAuth } from '../context/AuthContext';
import { AgrupacionesManager } from './AgrupacionesManager';
//import { OrdenDiputadosManager } from './OrdenDiputadosManager';
//import { OrdenSenadoresManager } from './OrdenSenadoresManager';
//import { ConfiguracionGeneral } from './ConfiguracionGeneral';
import { OrdenDiputadosManager } from './OrdenDiputadosManager';
import { OrdenSenadoresManager } from './OrdenSenadoresManager';
import { ConfiguracionGeneral } from './ConfiguracionGeneral';
import { BancasManager } from './BancasManager';
import { LogoOverridesManager } from './LogoOverridesManager';
import { CandidatoOverridesManager } from './CandidatoOverridesManager';
import { WorkerManager } from './WorkerManager';
import { ConfiguracionNacional } from './ConfiguracionNacional';
import { BancasPreviasManager } from './BancasPreviasManager';
import { OrdenDiputadosNacionalesManager } from './OrdenDiputadosNacionalesManager';
import { OrdenSenadoresNacionalesManager } from './OrdenSenadoresNacionalesManager';
//import { BancasProvincialesManager } from './BancasProvincialesManager';
//import { BancasNacionalesManager } from './BancasNacionalesManager';
export const DashboardPage = () => {
const { logout } = useAuth();
const sectionStyle = {
border: '1px solid #dee2e6',
borderRadius: '8px',
padding: '1.5rem',
marginBottom: '2rem',
backgroundColor: '#f8f9fa'
};
const sectionTitleStyle = {
marginTop: 0,
borderBottom: '2px solid #007bff',
paddingBottom: '0.5rem',
marginBottom: '1.5rem',
color: '#007bff'
};
return (
<div style={{ padding: '1rem 2rem' }}>
<header style={{ /* ... */ }}>
<header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '2px solid #eee', paddingBottom: '1rem' }}>
<h1>Panel de Administración Electoral</h1>
<button onClick={logout}>Cerrar Sesión</button>
</header>
<main style={{ marginTop: '2rem' }}>
<div style={sectionStyle}>
<h2 style={sectionTitleStyle}>Configuración Global</h2>
<AgrupacionesManager />
<LogoOverridesManager />
<CandidatoOverridesManager />
</div>
<div style={sectionStyle}>
<h2 style={sectionTitleStyle}>Gestión de Elecciones Nacionales</h2>
<ConfiguracionNacional />
<BancasPreviasManager />
<div style={{ display: 'flex', gap: '2rem', flexWrap: 'wrap', marginTop: '2rem' }}>
<div style={{ flex: '1 1 400px' }}>
<OrdenDiputadosNacionalesManager />
</div>
<div style={{ flex: '1 1 400px' }}>
<OrdenSenadoresNacionalesManager />
</div>
</div>
{/* <BancasNacionalesManager /> */}
</div>
{/*
<div style={sectionStyle}>
<h2 style={sectionTitleStyle}>Gestión de Elecciones Provinciales</h2>
<ConfiguracionGeneral />
<BancasProvincialesManager />
<div style={{ display: 'flex', gap: '2rem', flexWrap: 'wrap', marginTop: '2rem' }}>
<div style={{ flex: '1 1 400px' }}>
<OrdenDiputadosManager />
</div>
<div style={{ flex: '1 1 400px' }}>
<OrdenSenadoresManager />
</div>
<AgrupacionesManager />
<div style={{ flex: '1 1 800px' }}>
<LogoOverridesManager />
</div>
<div style={{ flex: '1 1 800px' }}>
<CandidatoOverridesManager />
</div>
<div style={{ display: 'flex', gap: '2rem', flexWrap: 'wrap', marginTop: '2rem' }}>
<div style={{ flex: '1 1 400px' }}>
<OrdenDiputadosManager />
</div>
<div style={{ flex: '1 1 400px' }}>
<OrdenSenadoresManager />
</div>
</div>*/}
<div style={sectionStyle}>
<h2 style={sectionTitleStyle}>Gestión de Workers y Sistema</h2>
<WorkerManager />
</div>
<ConfiguracionGeneral />
<BancasManager />
<hr style={{ margin: '2rem 0' }}/>
<WorkerManager />
</main>
</div>
);

View File

@@ -2,105 +2,83 @@
import { useState, useMemo, useEffect } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import Select from 'react-select';
import { getProvinciasForAdmin, getMunicipiosForAdmin, getAgrupaciones, getLogos, updateLogos } from '../services/apiService';
import type { MunicipioSimple, AgrupacionPolitica, LogoAgrupacionCategoria, ProvinciaSimple } from '../types';
import { CATEGORIAS_NACIONALES_OPTIONS, CATEGORIAS_PROVINCIALES_OPTIONS } from '../constants/categorias';
import { getMunicipiosForAdmin, getAgrupaciones, getLogos, updateLogos } from '../services/apiService';
import type { MunicipioSimple, AgrupacionPolitica, LogoAgrupacionCategoria } from '../types';
const ELECCION_OPTIONS = [
{ value: 0, label: 'General (Toda la elección)' },
{ value: 2, label: 'Elecciones Nacionales' },
{ value: 1, label: 'Elecciones Provinciales' }
];
const AMBITO_LEVEL_OPTIONS = [
{ value: 'general', label: 'General (Toda la elección)' },
{ value: 'provincia', label: 'Por Provincia' },
{ value: 'municipio', label: 'Por Municipio' }
// --- AÑADIMOS LAS CATEGORÍAS PARA EL SELECTOR ---
const CATEGORIAS_OPTIONS = [
{ value: 5, label: 'Senadores' },
{ value: 6, label: 'Diputados' },
{ value: 7, label: 'Concejales' }
];
export const LogoOverridesManager = () => {
const queryClient = useQueryClient();
// --- ESTADOS ---
const [selectedEleccion, setSelectedEleccion] = useState(ELECCION_OPTIONS[0]);
const [selectedAmbitoLevel, setSelectedAmbitoLevel] = useState(AMBITO_LEVEL_OPTIONS[0]);
const [selectedProvincia, setSelectedProvincia] = useState<ProvinciaSimple | null>(null);
const [selectedMunicipio, setSelectedMunicipio] = useState<MunicipioSimple | null>(null);
const [selectedCategoria, setSelectedCategoria] = useState<{ value: number; label: string } | null>(null);
const [selectedAgrupacion, setSelectedAgrupacion] = useState<AgrupacionPolitica | null>(null);
const [logoUrl, setLogoUrl] = useState('');
// --- QUERIES ---
const { data: provincias = [] } = useQuery<ProvinciaSimple[]>({ queryKey: ['provinciasForAdmin'], queryFn: getProvinciasForAdmin });
const { data: municipios = [] } = useQuery<MunicipioSimple[]>({ queryKey: ['municipiosForAdmin'], queryFn: getMunicipiosForAdmin });
const { data: agrupaciones = [] } = useQuery<AgrupacionPolitica[]>({ queryKey: ['agrupaciones'], queryFn: getAgrupaciones });
const { data: logos = [] } = useQuery<LogoAgrupacionCategoria[]>({
queryKey: ['logos', selectedEleccion.value],
queryFn: () => getLogos(selectedEleccion.value)
});
const { data: logos = [] } = useQuery<LogoAgrupacionCategoria[]>({ queryKey: ['logos'], queryFn: getLogos });
// --- LÓGICA DE SELECTORES DINÁMICOS ---
const categoriaOptions = selectedEleccion.value === 2 ? CATEGORIAS_NACIONALES_OPTIONS : CATEGORIAS_PROVINCIALES_OPTIONS;
// --- NUEVO ESTADO PARA LA CATEGORÍA ---
const [selectedCategoria, setSelectedCategoria] = useState<{ value: number; label: string } | null>(null);
const [selectedMunicipio, setSelectedMunicipio] = useState<{ value: string; label: string } | null>(null);
const [selectedAgrupacion, setSelectedAgrupacion] = useState<{ value: string; label: string } | null>(null);
const [logoUrl, setLogoUrl] = useState('');
const getAmbitoId = () => {
if (selectedAmbitoLevel.value === 'municipio' && selectedMunicipio) return parseInt(selectedMunicipio.id);
if (selectedAmbitoLevel.value === 'provincia' && selectedProvincia) return parseInt(selectedProvincia.id);
return 0;
};
const municipioOptions = useMemo(() =>
[{ value: 'general', label: 'General (Todas las secciones)' }, ...municipios.map(m => ({ value: m.id, label: m.nombre }))]
, [municipios]);
const agrupacionOptions = useMemo(() => agrupaciones.map(a => ({ value: a.id, label: a.nombre })), [agrupaciones]);
const currentLogo = useMemo(() => {
if (!selectedAgrupacion || !selectedCategoria) return '';
const ambitoId = getAmbitoId();
return logos.find(l =>
l.ambitoGeograficoId === ambitoId &&
l.agrupacionPoliticaId === selectedAgrupacion.id &&
// La búsqueda ahora depende de los 3 selectores
if (!selectedMunicipio || !selectedAgrupacion || !selectedCategoria) return '';
return logos.find(l =>
l.ambitoGeograficoId === parseInt(selectedMunicipio.value) &&
l.agrupacionPoliticaId === selectedAgrupacion.value &&
l.categoriaId === selectedCategoria.value
)?.logoUrl || '';
}, [logos, selectedAmbitoLevel, selectedProvincia, selectedMunicipio, selectedAgrupacion, selectedCategoria]);
useEffect(() => { setLogoUrl(currentLogo || ''); }, [currentLogo]);
}, [logos, selectedMunicipio, selectedAgrupacion, selectedCategoria]);
useEffect(() => { setLogoUrl(currentLogo) }, [currentLogo]);
const handleSave = async () => {
if (!selectedAgrupacion || !selectedCategoria) return;
if (!selectedMunicipio || !selectedAgrupacion || !selectedCategoria) return;
const newLogoEntry: LogoAgrupacionCategoria = {
id: 0,
eleccionId: selectedEleccion.value,
agrupacionPoliticaId: selectedAgrupacion.id,
agrupacionPoliticaId: selectedAgrupacion.value,
categoriaId: selectedCategoria.value,
ambitoGeograficoId: getAmbitoId(),
ambitoGeograficoId: parseInt(selectedMunicipio.value),
logoUrl: logoUrl || null
};
try {
await updateLogos([newLogoEntry]);
queryClient.invalidateQueries({ queryKey: ['logos', selectedEleccion.value] });
queryClient.invalidateQueries({ queryKey: ['logos'] });
alert('Override de logo guardado.');
} catch { alert('Error al guardar.'); }
};
return (
<div className="admin-module">
<h3>Overrides de Logos</h3>
<p>Configure una imagen específica para un partido en un contexto determinado.</p>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem', alignItems: 'flex-end' }}>
<Select options={ELECCION_OPTIONS} value={selectedEleccion} onChange={(opt) => { setSelectedEleccion(opt!); setSelectedCategoria(null); }} />
<Select options={categoriaOptions} value={selectedCategoria} onChange={setSelectedCategoria} placeholder="Seleccione Categoría..." isDisabled={!selectedEleccion} />
<Select options={agrupaciones.map(a => ({ value: a.id, label: a.nombre, ...a }))} getOptionValue={opt => opt.id} getOptionLabel={opt => opt.nombre} value={selectedAgrupacion} onChange={setSelectedAgrupacion} placeholder="Seleccione Agrupación..." />
<Select options={AMBITO_LEVEL_OPTIONS} value={selectedAmbitoLevel} onChange={(opt) => { setSelectedAmbitoLevel(opt!); setSelectedProvincia(null); setSelectedMunicipio(null); }} />
{selectedAmbitoLevel.value === 'provincia' || selectedAmbitoLevel.value === 'municipio' ? (
<Select options={provincias.map(p => ({ value: p.id, label: p.nombre, ...p }))} getOptionValue={opt => opt.id} getOptionLabel={opt => opt.nombre} value={selectedProvincia} onChange={setSelectedProvincia} placeholder="Seleccione Provincia..." />
) : <div />}
{selectedAmbitoLevel.value === 'municipio' ? (
<Select options={municipios.map(m => ({ value: m.id, label: m.nombre, ...m }))} getOptionValue={opt => opt.id} getOptionLabel={opt => opt.nombre} value={selectedMunicipio} onChange={setSelectedMunicipio} placeholder="Seleccione Municipio..." isDisabled={!selectedProvincia} />
) : <div />}
</div>
<div style={{ display: 'flex', gap: '1rem', alignItems: 'flex-end', marginTop: '1rem' }}>
<h3>Overrides de Logos por Municipio y Categoría</h3>
<p>Configure una imagen específica para un partido en un municipio y categoría determinados.</p>
<div style={{ display: 'flex', gap: '1rem', alignItems: 'flex-end' }}>
<div style={{ flex: 1 }}>
<label>URL del Logo Específico</label>
<input type="text" value={logoUrl} onChange={e => setLogoUrl(e.target.value)} style={{ width: '100%' }} disabled={!selectedAgrupacion || !selectedCategoria} />
<label>Categoría</label>
<Select options={CATEGORIAS_OPTIONS} value={selectedCategoria} onChange={setSelectedCategoria} isClearable placeholder="Seleccione..."/>
</div>
<button onClick={handleSave} disabled={!selectedAgrupacion || !selectedCategoria}>Guardar</button>
<div style={{ flex: 1 }}>
<label>Municipio</label>
<Select options={municipioOptions} value={selectedMunicipio} onChange={setSelectedMunicipio} isClearable placeholder="Seleccione..."/>
</div>
<div style={{ flex: 1 }}>
<label>Agrupación</label>
<Select options={agrupacionOptions} value={selectedAgrupacion} onChange={setSelectedAgrupacion} isClearable placeholder="Seleccione..."/>
</div>
<div style={{ flex: 2 }}>
<label>URL del Logo Específico</label>
<input type="text" value={logoUrl} onChange={e => setLogoUrl(e.target.value)} style={{ width: '100%' }} disabled={!selectedMunicipio || !selectedAgrupacion || !selectedCategoria} />
</div>
<button onClick={handleSave} disabled={!selectedMunicipio || !selectedAgrupacion || !selectedCategoria}>Guardar</button>
</div>
</div>
);

View File

@@ -1,102 +0,0 @@
// src/components/OrdenDiputadosNacionalesManager.tsx
import { useState, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core';
import { arrayMove, SortableContext, sortableKeyboardCoordinates, horizontalListSortingStrategy } from '@dnd-kit/sortable';
import { getAgrupaciones, updateOrden, getComposicionNacional } from '../services/apiService';
import type { AgrupacionPolitica } from '../types';
import { SortableItem } from './SortableItem';
import './AgrupacionesManager.css';
const ELECCION_ID_NACIONAL = 2;
export const OrdenDiputadosNacionalesManager = () => {
// Estado para la lista que el usuario puede ordenar
const [agrupacionesOrdenadas, setAgrupacionesOrdenadas] = useState<AgrupacionPolitica[]>([]);
// Query 1: Obtener TODAS las agrupaciones para tener sus datos completos (nombre, etc.)
const { data: todasAgrupaciones = [], isLoading: isLoadingAgrupaciones } = useQuery<AgrupacionPolitica[]>({
queryKey: ['agrupaciones'],
queryFn: getAgrupaciones,
});
// Query 2: Obtener los datos de composición para saber qué partidos tienen bancas
const { data: composicionData, isLoading: isLoadingComposicion } = useQuery({
queryKey: ['composicionNacional', ELECCION_ID_NACIONAL],
queryFn: () => getComposicionNacional(ELECCION_ID_NACIONAL),
});
// Este efecto se ejecuta cuando los datos de las queries estén disponibles
useEffect(() => {
// No hacemos nada hasta que ambas queries hayan cargado sus datos
if (!composicionData || !todasAgrupaciones || todasAgrupaciones.length === 0) {
return;
}
// Creamos un Set con los IDs de los partidos que tienen al menos una banca de diputado
const partidosConBancasIds = new Set(
composicionData.diputados.partidos
.filter(p => p.bancasTotales > 0)
.map(p => p.id)
);
// Filtramos la lista completa de agrupaciones, quedándonos solo con las relevantes
const agrupacionesFiltradas = todasAgrupaciones.filter(a => partidosConBancasIds.has(a.id));
// Ordenamos la lista filtrada según el orden guardado en la BD
agrupacionesFiltradas.sort((a, b) => (a.ordenDiputadosNacionales || 999) - (b.ordenDiputadosNacionales || 999));
// Actualizamos el estado que se renderiza y que el usuario puede ordenar
setAgrupacionesOrdenadas(agrupacionesFiltradas);
}, [todasAgrupaciones, composicionData]); // Dependencias: se re-ejecuta si los datos cambian
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
);
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (over && active.id !== over.id) {
setAgrupacionesOrdenadas((items) => {
const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex((item) => item.id === over.id);
return arrayMove(items, oldIndex, newIndex);
});
}
};
const handleSaveOrder = async () => {
const idsOrdenados = agrupacionesOrdenadas.map(a => a.id);
try {
await updateOrden('diputados-nacionales', idsOrdenados);
alert('Orden de Diputados Nacionales guardado con éxito!');
} catch (error) {
alert('Error al guardar el orden de Diputados Nacionales.');
}
};
const isLoading = isLoadingAgrupaciones || isLoadingComposicion;
if (isLoading) return <p>Cargando orden de Diputados Nacionales...</p>;
return (
<div className="admin-module">
<h3>Ordenar Agrupaciones (Diputados Nacionales)</h3>
<p>Arrastre para reordenar. Solo se muestran los partidos con bancas.</p>
<p>Ancla izquierda. Prioridad de izquierda a derecha y de arriba abajo.</p>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={agrupacionesOrdenadas.map(a => a.id)} strategy={horizontalListSortingStrategy}>
<ul className="sortable-list-horizontal">
{agrupacionesOrdenadas.map(agrupacion => (
<SortableItem key={agrupacion.id} id={agrupacion.id}>
{agrupacion.nombreCorto || agrupacion.nombre}
</SortableItem>
))}
</ul>
</SortableContext>
</DndContext>
<button onClick={handleSaveOrder} style={{ marginTop: '1rem' }}>Guardar Orden</button>
</div>
);
};

View File

@@ -1,94 +0,0 @@
// src/components/OrdenSenadoresNacionalesManager.tsx
import { useState, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core';
import { arrayMove, SortableContext, sortableKeyboardCoordinates, horizontalListSortingStrategy } from '@dnd-kit/sortable';
import { getAgrupaciones, updateOrden, getComposicionNacional } from '../services/apiService';
import type { AgrupacionPolitica } from '../types';
import { SortableItem } from './SortableItem';
import './AgrupacionesManager.css';
const ELECCION_ID_NACIONAL = 2;
export const OrdenSenadoresNacionalesManager = () => {
const [agrupacionesOrdenadas, setAgrupacionesOrdenadas] = useState<AgrupacionPolitica[]>([]);
const { data: todasAgrupaciones = [], isLoading: isLoadingAgrupaciones } = useQuery<AgrupacionPolitica[]>({
queryKey: ['agrupaciones'],
queryFn: getAgrupaciones,
});
const { data: composicionData, isLoading: isLoadingComposicion } = useQuery({
queryKey: ['composicionNacional', ELECCION_ID_NACIONAL],
queryFn: () => getComposicionNacional(ELECCION_ID_NACIONAL),
});
useEffect(() => {
if (!composicionData || !todasAgrupaciones || todasAgrupaciones.length === 0) {
return;
}
// Creamos un Set con los IDs de los partidos que tienen al menos una banca de senador
const partidosConBancasIds = new Set(
composicionData.senadores.partidos
.filter(p => p.bancasTotales > 0)
.map(p => p.id)
);
const agrupacionesFiltradas = todasAgrupaciones.filter(a => partidosConBancasIds.has(a.id));
agrupacionesFiltradas.sort((a, b) => (a.ordenSenadoresNacionales || 999) - (b.ordenSenadoresNacionales || 999));
setAgrupacionesOrdenadas(agrupacionesFiltradas);
}, [todasAgrupaciones, composicionData]);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
);
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (over && active.id !== over.id) {
setAgrupacionesOrdenadas((items) => {
const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex((item) => item.id === over.id);
return arrayMove(items, oldIndex, newIndex);
});
}
};
const handleSaveOrder = async () => {
const idsOrdenados = agrupacionesOrdenadas.map(a => a.id);
try {
await updateOrden('senadores-nacionales', idsOrdenados);
alert('Orden de Senadores Nacionales guardado con éxito!');
} catch (error) {
alert('Error al guardar el orden de Senadores Nacionales.');
}
};
const isLoading = isLoadingAgrupaciones || isLoadingComposicion;
if (isLoading) return <p>Cargando orden de Senadores Nacionales...</p>;
return (
<div className="admin-module">
<h3>Ordenar Agrupaciones (Senado de la Nación)</h3>
<p>Arrastre para reordenar. Solo se muestran los partidos con bancas.</p>
<p>Ancla izquierda. Prioridad de izquierda a derecha y de arriba abajo.</p>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={agrupacionesOrdenadas.map(a => a.id)} strategy={horizontalListSortingStrategy}>
<ul className="sortable-list-horizontal">
{agrupacionesOrdenadas.map(agrupacion => (
<SortableItem key={agrupacion.id} id={agrupacion.id}>
{agrupacion.nombreCorto || agrupacion.nombre}
</SortableItem>
))}
</ul>
</SortableContext>
</DndContext>
<button onClick={handleSaveOrder} style={{ marginTop: '1rem' }}>Guardar Orden</button>
</div>
);
};

View File

@@ -1,23 +0,0 @@
// src/constants/categorias.ts
// Opciones para los selectores en el panel de administración
export const CATEGORIAS_ADMIN_OPTIONS = [
// Nacionales
{ value: 1, label: 'Senadores Nacionales' },
{ value: 2, label: 'Diputados Nacionales' },
// Provinciales
{ value: 5, label: 'Senadores Provinciales' },
{ value: 6, label: 'Diputados Provinciales' },
{ value: 7, label: 'Concejales' },
];
export const CATEGORIAS_NACIONALES_OPTIONS = [
{ value: 1, label: 'Senadores Nacionales' },
{ value: 2, label: 'Diputados Nacionales' },
];
export const CATEGORIAS_PROVINCIALES_OPTIONS = [
{ value: 5, label: 'Senadores Provinciales' },
{ value: 6, label: 'Diputados Provinciales' },
{ value: 7, label: 'Concejales' },
];

View File

@@ -1,12 +1,11 @@
// src/services/apiService.ts
import axios from 'axios';
import { triggerLogout } from '../context/authUtils';
import type { CandidatoOverride, AgrupacionPolitica,
UpdateAgrupacionData, Bancada, LogoAgrupacionCategoria,
MunicipioSimple, BancaPrevia, ProvinciaSimple } from '../types';
import type { CandidatoOverride, AgrupacionPolitica, UpdateAgrupacionData, Bancada, LogoAgrupacionCategoria, MunicipioSimple } from '../types';
/**
* URL base para las llamadas a la API.
* Se usa para construir las URLs más específicas.
*/
const API_URL_BASE = import.meta.env.DEV
? 'http://localhost:5217/api'
@@ -22,19 +21,13 @@ export const AUTH_API_URL = `${API_URL_BASE}/auth`;
*/
export const ADMIN_API_URL = `${API_URL_BASE}/admin`;
// Cliente de API para endpoints de administración (requiere token)
const adminApiClient = axios.create({
baseURL: ADMIN_API_URL,
});
// Cliente de API para endpoints públicos (no envía token)
const apiClient = axios.create({
baseURL: API_URL_BASE,
headers: { 'Content-Type': 'application/json' },
});
// --- INTERCEPTORES ---
// --- INTERCEPTORES (Solo para el cliente de admin) ---
// Interceptor de Peticiones: Añade el token JWT a cada llamada
adminApiClient.interceptors.request.use(
(config) => {
const token = localStorage.getItem('admin-jwt-token');
@@ -46,6 +39,7 @@ adminApiClient.interceptors.request.use(
(error) => Promise.reject(error)
);
// Interceptor de Respuestas: Maneja la expiración del token (error 401)
adminApiClient.interceptors.response.use(
(response) => response,
(error) => {
@@ -57,32 +51,6 @@ adminApiClient.interceptors.response.use(
}
);
// --- INTERFACES PARA COMPOSICIÓN NACIONAL (NECESARIAS PARA EL NUEVO MÉTODO) ---
export interface PartidoComposicionNacional {
id: string;
nombre: string;
nombreCorto: string | null;
color: string | null;
bancasFijos: number;
bancasGanadas: number;
bancasTotales: number;
ordenDiputadosNacionales: number | null;
ordenSenadoresNacionales: number | null;
}
export interface CamaraComposicionNacional {
camaraNombre: string;
totalBancas: number;
bancasEnJuego: number;
partidos: PartidoComposicionNacional[];
presidenteBancada: { color: string | null; tipoBanca: 'ganada' | 'previa' | null } | null;
}
export interface ComposicionNacionalData {
diputados: CamaraComposicionNacional;
senadores: CamaraComposicionNacional;
}
// --- SERVICIOS DE API ---
// 1. Autenticación
@@ -98,7 +66,7 @@ export const loginUser = async (credentials: LoginCredentials): Promise<string |
}
};
// 2. Agrupaciones
// 2. Agrupaciones Políticas
export const getAgrupaciones = async (): Promise<AgrupacionPolitica[]> => {
const response = await adminApiClient.get('/agrupaciones');
return response.data;
@@ -109,14 +77,14 @@ export const updateAgrupacion = async (id: string, data: UpdateAgrupacionData):
};
// 3. Ordenamiento de Agrupaciones
export const updateOrden = async (camara: 'diputados' | 'senadores' | 'diputados-nacionales' | 'senadores-nacionales', ids: string[]): Promise<void> => {
export const updateOrden = async (camara: 'diputados' | 'senadores', ids: string[]): Promise<void> => {
await adminApiClient.put(`/agrupaciones/orden-${camara}`, ids);
};
// 4. Gestión de Bancas
export const getBancadas = async (camara: 'diputados' | 'senadores', eleccionId: number): Promise<Bancada[]> => {
const camaraId = (camara === 'diputados') ? 0 : 1;
const response = await adminApiClient.get(`/bancadas/${camaraId}?eleccionId=${eleccionId}`);
// 4. Gestión de Bancas y Ocupantes
export const getBancadas = async (camara: 'diputados' | 'senadores'): Promise<Bancada[]> => {
const camaraId = camara === 'diputados' ? 0 : 1;
const response = await adminApiClient.get(`/bancadas/${camaraId}`);
return response.data;
};
@@ -143,52 +111,38 @@ export const updateConfiguracion = async (data: Record<string, string>): Promise
await adminApiClient.put('/configuracion', data);
};
// 6. Logos y Candidatos
export const getLogos = async (eleccionId: number): Promise<LogoAgrupacionCategoria[]> => {
const response = await adminApiClient.get(`/logos?eleccionId=${eleccionId}`);
export const getLogos = async (): Promise<LogoAgrupacionCategoria[]> => {
const response = await adminApiClient.get('/logos');
return response.data;
};
export const updateLogos = async (data: LogoAgrupacionCategoria[]): Promise<void> => {
await adminApiClient.put('/logos', data);
};
export const getCandidatos = async (eleccionId: number): Promise<CandidatoOverride[]> => {
const response = await adminApiClient.get(`/candidatos?eleccionId=${eleccionId}`);
return response.data;
};
export const updateCandidatos = async (data: CandidatoOverride[]): Promise<void> => {
await adminApiClient.put('/candidatos', data);
};
// 7. Catálogos
export const getMunicipiosForAdmin = async (): Promise<MunicipioSimple[]> => {
// Ahora usa adminApiClient, que apunta a /api/admin/
// La URL final será /api/admin/catalogos/municipios
const response = await adminApiClient.get('/catalogos/municipios');
return response.data;
};
// 8. Logging
export interface UpdateLoggingLevelData { level: string; }
// 6. Overrides de Candidatos
export const getCandidatos = async (): Promise<CandidatoOverride[]> => {
const response = await adminApiClient.get('/candidatos');
return response.data;
};
export const updateCandidatos = async (data: CandidatoOverride[]): Promise<void> => {
await adminApiClient.put('/candidatos', data);
};
// 7. Gestión de Logging
export interface UpdateLoggingLevelData {
level: string;
}
export const updateLoggingLevel = async (data: UpdateLoggingLevelData): Promise<void> => {
// Este endpoint es específico, no es parte de la configuración general
await adminApiClient.put(`/logging-level`, data);
};
// 9. Bancas Previas
export const getBancasPrevias = async (eleccionId: number): Promise<BancaPrevia[]> => {
const response = await adminApiClient.get(`/bancas-previas/${eleccionId}`);
return response.data;
};
export const updateBancasPrevias = async (eleccionId: number, data: BancaPrevia[]): Promise<void> => {
await adminApiClient.put(`/bancas-previas/${eleccionId}`, data);
};
// 10. Obtener Composición Nacional (Endpoint Público)
export const getComposicionNacional = async (eleccionId: number): Promise<ComposicionNacionalData> => {
// Este es un endpoint público, por lo que usamos el cliente sin token de admin.
const response = await apiClient.get(`/elecciones/${eleccionId}/composicion-nacional`);
return response.data;
};
// Obtenemos las provincias para el selector de ámbito
export const getProvinciasForAdmin = async (): Promise<ProvinciaSimple[]> => {
const response = await adminApiClient.get('/catalogos/provincias');
return response.data;
};

View File

@@ -8,8 +8,6 @@ export interface AgrupacionPolitica {
color: string | null;
ordenDiputados: number | null;
ordenSenadores: number | null;
ordenDiputadosNacionales: number | null;
ordenSenadoresNacionales: number | null;
}
export interface UpdateAgrupacionData {
@@ -32,9 +30,9 @@ export interface OcupanteBanca {
periodo: string | null;
}
// Nueva interfaz para la Bancada
export interface Bancada {
id: number;
eleccionId: number; // Clave para diferenciar provinciales de nacionales
camara: TipoCamaraValue;
numeroBanca: number;
agrupacionPoliticaId: string | null;
@@ -42,33 +40,18 @@ export interface Bancada {
ocupante: OcupanteBanca | null;
}
// Nueva interfaz para Bancas Previas
export interface BancaPrevia {
id: number;
eleccionId: number;
camara: TipoCamaraValue;
agrupacionPoliticaId: string;
agrupacionPolitica?: AgrupacionPolitica; // Opcional para la UI
cantidad: number;
}
export interface LogoAgrupacionCategoria {
id: number;
eleccionId: number; // Clave para diferenciar
agrupacionPoliticaId: string;
categoriaId: number | null;
categoriaId: number;
logoUrl: string | null;
ambitoGeograficoId: number | null;
}
export interface MunicipioSimple { id: string; nombre: string; }
export interface ProvinciaSimple { id: string; nombre: string; }
export interface CandidatoOverride {
id: number;
eleccionId: number; // Clave para diferenciar
agrupacionPoliticaId: string;
categoriaId: number;
ambitoGeograficoId: number | null;

File diff suppressed because it is too large Load Diff

View File

@@ -18,20 +18,13 @@
"axios": "^1.11.0",
"d3-geo": "^3.1.1",
"d3-shape": "^3.2.0",
"highcharts": "^12.4.0",
"highcharts-react-official": "^3.2.2",
"react": "^19.1.1",
"react-circular-progressbar": "^2.2.0",
"react-dom": "^19.1.1",
"react-hot-toast": "^2.6.0",
"react-icons": "^5.5.0",
"react-pdf": "^10.1.0",
"react-select": "^5.10.2",
"react-simple-maps": "github:ozimmortal/react-simple-maps#feat/react-19-support",
"react-tooltip": "^5.29.1",
"swiper": "^12.0.2",
"topojson-client": "^3.1.0",
"vite-plugin-svgr": "^4.5.0"
"topojson-client": "^3.1.0"
},
"devDependencies": {
"@eslint/js": "^9.33.0",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.8 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.7 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.0 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="1.3493201mm"
height="1.6933239mm"
viewBox="0 0 1.3493201 1.6933238"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1"
transform="translate(-103.9813,-147.63758)">
<path
d="m 105.33062,148.53708 -0.0264,0.0794 -0.1323,0.13229 -0.1852,0.0265 -0.15875,0.15875 -0.21167,0.39687 -0.52917,-0.44979 -0.10583,-0.37042 0.13229,-0.58208 0.34396,-0.29104 0.13229,0.0794 0.10583,0.0529 0.10584,0.0794 0.18521,0.13229 0.0794,0.0794 0.10583,0.15875 0.0794,0.15875 z"
id="ARC"
name="Ciudad de Buenos Aires"
style="stroke-width:0.264583" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 821 B

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="22.092972mm"
height="36.143562mm"
viewBox="0 0 22.092972 36.143562"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1"
transform="translate(-93.662753,-130.4396)">
<path
d="m 95.646872,134.9375 1.190625,-0.3175 0.264583,-0.15875 0.47625,-0.50271 0.582083,-0.66146 0.05292,-0.10583 0.02646,-0.13229 -0.15875,-1.45521 0.05292,-0.21167 0.370417,-0.13229 1.5875,-0.39687 1.03188,-0.29105 0.60854,-0.13229 h 0.21167 l 1.08479,0.26459 0.0529,0.0264 v 0.0265 l 0.0265,0.0794 -0.0265,0.15875 v 0.0794 l 0.0265,0.0529 0.0794,0.0529 0.39687,0.23812 0.0265,0.0265 0.0529,0.0794 0.0529,0.0529 0.0265,0.0265 0.34396,0.0529 0.0529,0.0265 0.0265,0.0265 0.0794,0.0529 0.0529,0.0529 0.0529,0.0265 h 0.0529 l 0.3175,0.0265 h 0.10583 l 0.635,-0.10583 0.0794,0.0265 0.39687,0.10583 0.0529,0.0265 0.21167,-0.0529 h 0.0529 l 0.0265,0.0265 0.0265,0.0265 v 0.0265 0.0265 0.0265 l -0.0265,0.18521 v 0.0265 l 0.0265,0.0529 h 0.0529 l 0.1852,0.0265 0.0529,0.0264 0.0529,0.0265 v 0.0265 0.0529 0.10583 l 0.0265,0.0265 0.0264,0.0529 0.1323,0.0264 h 3.12208 l 1.85208,-0.0264 0.89959,0.0529 0.26458,0.15875 0.84667,2.2225 -0.26459,1.66688 0.0265,0.15875 0.0265,0.10583 1.21708,1.34938 0.26459,0.37041 -0.0529,0.37042 -1.40229,5.26521 -0.10584,0.21166 -0.23812,0.21167 -0.21167,0.21167 -0.0794,0.0794 v 0.0794 0.0265 l 0.0794,0.15875 0.0265,0.0265 v 0.0265 0.0529 0.0794 l -0.0265,0.29105 v 0.0794 l 0.0265,0.39688 0.0265,0.0264 v 0.0265 0.0265 l 0.0794,0.13229 0.0529,0.0529 0.0265,0.0265 v 0.0529 0.0529 l -0.0794,0.18521 -0.0529,0.10583 -0.0265,0.10584 -0.0265,0.15875 v 0.52916 l 0.13229,0.15875 0.13229,0.10584 0.15875,0.21166 0.0529,0.0265 0.15875,0.0794 0.10583,0.0794 0.0265,0.0265 0.0265,0.0529 0.15875,0.55562 0.0265,0.10583 0.13229,0.21167 0.39688,0.29104 0.0529,0.0794 0.0794,0.0794 0.0265,0.0529 0.0264,0.13229 0.0529,0.21167 v 0.21166 l 0.0265,0.0265 0.0265,0.0529 0.0265,0.0265 0.0529,0.0529 0.0265,0.0265 0.0265,0.0529 v 0.0794 l -0.0265,0.0529 v 0.0265 0.0529 l -0.0265,0.0529 -0.0265,0.0265 -0.0794,0.10583 v 0.0265 0.0529 l 0.0265,0.0265 h 0.0529 l 0.15875,0.0265 0.0265,0.0265 H 115.2 l 0.0265,0.0265 v 0.0265 l 0.15875,0.29104 0.10583,0.21167 v 0.0529 l 0.0265,0.0265 0.15875,0.10584 0.0265,0.0265 v 0.0529 l 0.0529,0.0794 v 0.0529 0.0529 0.0529 l -0.0264,0.10583 -0.0794,0.15875 -0.0265,0.13229 v 0.39688 0.15875 l -0.0265,0.13229 -0.0265,0.0265 -0.0794,0.15875 -0.18521,0.21166 -0.0794,0.0794 -0.0794,0.0265 -0.0265,0.0265 h -0.0264 l -0.0794,0.0264 h -0.0529 l -0.0794,-0.0264 -0.13229,0.21166 -1.24354,2.01084 -1.66688,2.7252 -2.2225,3.6248 -2.67229,-0.0265 -0.18521,0.23812 -0.0265,1.08479 -0.0265,2.83105 h -3.12208 -2.91041 -3.28084 v -0.0265 -6.87917 l -0.264584,-4.97417 0.02646,-0.0529 0.02646,-0.0794 0.15875,-0.29104 0.02646,-0.0265 v -0.0265 l 0.02646,-0.0264 h 0.05292 l 0.132292,-0.0265 0.02646,-0.0265 0.05292,-0.0265 0.02646,-0.0529 v -0.0264 -0.0529 l -0.05292,-0.13229 v -0.10584 l 0.02646,-0.0794 0.132292,-0.29104 0.02646,-0.0794 v -0.0529 l -0.02646,-0.10584 -0.02646,-0.0794 0.02646,-0.0529 v -0.0529 l 0.211666,-0.39687 0.02646,-0.0529 v -0.0529 -0.23813 l 0.02646,-0.0529 v -0.0529 l 0.07937,-0.13229 0.02646,-0.0529 v -0.0265 -0.10583 -0.0794 -0.0265 l 0.02646,-0.0529 0.07938,-0.0794 0.02646,-0.0265 0.02646,-0.0529 0.02646,-0.0529 0.02646,-0.26458 0.132292,-0.29104 0.02646,-0.18521 0.02646,-0.21167 v -0.0529 l -0.02646,-0.0529 -0.02646,-0.13229 -0.02646,-0.10584 -0.02646,-0.13229 -0.132292,-0.23812 -0.02646,-0.0794 v -0.0529 -0.15875 l 0.02646,-0.15875 v -0.1852 -0.18521 l -0.02646,-0.0794 v -0.0794 h -0.02646 l -0.02646,-0.0265 -0.05292,-0.0265 h -0.02646 -0.02646 l -0.105833,0.0265 -0.555625,0.15875 -0.582084,0.0794 -0.07937,-0.0265 -0.02646,-0.0265 -0.02646,-0.0265 v -0.0529 -0.0529 -0.26459 l -0.02646,-0.0794 v -0.0265 l -0.07937,-0.13229 -0.05292,-0.10584 -0.05292,-0.13229 v -0.0794 -0.0794 -0.10583 -0.0265 l -0.02646,-0.10583 -0.02646,-0.0529 -0.02646,-0.0529 -0.15875,-0.13229 -0.15875,-0.18521 -1.534583,-0.9525 -0.264584,-0.13229 -0.185208,-0.0529 h -0.47625 v -1.69334 l -0.02646,-2.24896 v -1.11125 l 0.05292,-0.39687 0.370417,-1.08479 0.07937,-0.21167 0.105833,-0.3175 0.449792,-1.34937 v -0.10584 l 0.05292,-0.18521 0.132291,-0.44979 0.07937,-0.23812 0.02646,-0.0794 0.105833,-0.18521 0.07937,-0.1852 z"
id="ARX"
name="Córdoba"
style="stroke-width:0.264583" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="26.908112mm"
height="29.739168mm"
viewBox="0 0 26.908112 29.739168"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1"
transform="translate(-91.281242,-133.61455)">
<path
d="m 91.545834,150.99771 v -5.00063 l -0.05292,-3.78354 -0.15875,-0.82021 -0.02646,-0.0794 -0.02646,-0.10584 0.02646,-0.29104 0.05292,-0.44979 0.07937,-0.0794 0.105834,-0.0529 h 6.085416 0.423334 2.434162 3.01625 0.21167 4.89479 0.21167 0.0264 v -1.05833 l 0.0265,-5.66209 h 3.28084 2.91041 3.12208 v 1.82563 1.50812 l -0.0264,3.04271 v 0.23813 2.83104 1.53458 1.53458 2.9898 l 0.0264,3.09562 -0.0264,0.3175 0.0264,3.41313 v 3.88937 l -0.0264,3.51896 -0.47625,-0.0529 -0.21167,-0.0794 -0.15875,-0.15875 -0.71437,-0.50271 -0.18521,-0.0794 -0.10584,-0.13229 -0.13229,-0.23812 -0.0794,-0.0794 -0.0794,-0.0529 -0.21166,-0.1323 -0.37042,-0.0794 -0.10583,-0.0529 -0.47625,-0.58208 -0.0794,-0.0794 -0.26458,-0.0265 -0.10584,-0.0529 -0.39687,-0.26458 -1.77271,-0.68792 h -0.39687 l -1.50813,-0.29104 -0.10583,0.0529 -0.18521,-0.0529 -0.39688,0.0529 -0.13229,-0.10583 h -0.0529 -0.74084 l -0.82021,0.15875 -0.26458,-0.0265 -0.23812,0.10584 h -0.0794 l -0.0794,-0.0265 -0.21167,-0.15875 -0.47625,-0.18521 -0.29104,-0.0529 h -0.15875 l -0.10584,0.0794 -0.0794,0.0794 -0.13229,0.0794 -0.10584,0.0529 h -0.10583 l -0.0529,-0.0265 -0.0529,-0.0529 -0.0529,-0.0529 -0.0794,-0.0265 h -0.23813 -0.0529 l -0.10583,-0.10583 -0.0529,-0.0265 -0.0794,-0.0265 -0.21167,-0.0265 -0.18521,-0.0794 -0.15875,-0.0265 -0.10583,-0.0794 h -0.0529 l -1.40229,-0.18521 -0.74083,0.13229 h -0.23813 l -0.635,-0.18521 h -0.15875 l -0.13229,-0.0529 -0.0529,-0.10584 -0.0529,-0.34396 -0.0529,-0.21166 -0.13229,-0.18521 -0.18521,-0.15875 -0.18521,-0.10583 -0.238117,-0.0529 -0.05292,-0.0265 -0.105833,-0.0794 -0.02646,-0.0265 -0.05292,-0.0265 -0.211667,-0.13229 -0.264583,-0.0265 -0.105834,-0.0529 -0.185208,-0.13229 -0.238125,-0.0529 -0.555625,-0.29104 h -0.05292 l -0.02646,-0.0529 -0.07937,-0.10584 -0.07937,-0.0794 -0.211667,-0.29104 -0.132291,-0.37042 v -0.0794 -0.15875 l -0.02646,-0.10583 -0.05292,-0.10583 -0.132292,-0.0529 h -0.105833 l -0.47625,0.0794 -0.238125,0.15875 -0.15875,0.0529 -0.396875,-0.0529 -0.15875,0.0794 -0.396875,0.0265 -0.211667,-0.10583 -0.185208,-0.18521 -0.15875,-0.21167 -0.291042,-0.635 -0.15875,-0.15875 -0.211667,-0.10583 h -0.05292 l -0.15875,0.0529 h -0.05292 l -0.07937,-0.0794 h -0.05292 l -0.05292,-0.0529 -0.05292,-0.15875 -0.02646,-0.13229 -0.02646,-0.13229 0.02646,-0.34396 0.132291,-0.23812 0.15875,-0.15875 0.370417,-0.26459 0.132292,-0.18521 0.07937,-0.26458 v -0.3175 l -0.07937,-0.21167 -0.132292,-0.23812 -0.15875,-0.21167 -0.15875,-0.13229 -0.238125,-0.13229 z"
id="ARL"
name="La Pampa"
style="stroke-width:0.264583" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.7 KiB

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="13.838294mm"
height="27.438555mm"
viewBox="0 0 13.838294 27.438555"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1"
transform="translate(-97.895573,-134.6725)">
<path
d="m 101.54708,134.85812 0.37042,0.0794 0.13229,0.0529 0.15875,0.0794 0.0529,0.0265 h 0.0265 0.0529 0.10584 l 0.10583,-0.0529 0.0529,-0.0265 0.0265,0.0265 0.0529,0.0265 0.13229,0.0794 h 0.0265 l 0.29104,-0.0529 h 0.0529 l 0.0529,0.0265 0.0794,0.0265 0.13229,0.0794 h 0.0529 0.0529 l 0.1852,-0.0529 h 0.0794 l 0.26458,0.0265 0.26459,-0.0265 0.29104,-0.10583 0.21166,-0.13229 0.1323,-0.0529 0.26458,-0.0529 h 0.21167 l 0.26458,0.0529 h 0.0529 l 0.0264,0.0265 0.0794,0.0529 0.0529,0.0265 h 0.0264 l 0.10584,0.0265 h 0.26458 l 0.52917,-0.10583 h 0.47625 l 0.18521,0.0529 0.26458,0.13229 1.53458,0.9525 0.15875,0.18521 0.15875,0.13229 0.0265,0.0529 0.0265,0.0529 0.0265,0.10584 v 0.0264 0.10584 0.0794 0.0794 l 0.0529,0.13229 0.0529,0.10583 0.0794,0.13229 v 0.0265 l 0.0264,0.0794 v 0.26458 0.0529 0.0529 l 0.0265,0.0265 0.0265,0.0265 0.0794,0.0265 0.58208,-0.0794 0.55562,-0.15875 0.10584,-0.0265 h 0.0265 0.0264 l 0.0529,0.0265 0.0265,0.0265 h 0.0265 v 0.0794 l 0.0265,0.0794 v 0.18521 0.18521 l -0.0265,0.15875 v 0.15875 0.0529 l 0.0265,0.0794 0.13229,0.23813 0.0265,0.13229 0.0265,0.10583 0.0265,0.13229 0.0265,0.0529 v 0.0529 l -0.0265,0.21166 -0.0265,0.18521 -0.13229,0.29104 -0.0265,0.26459 -0.0265,0.0529 -0.0265,0.0529 -0.0265,0.0265 -0.0794,0.0794 -0.0264,0.0529 v 0.0265 0.0794 0.10584 0.0264 l -0.0265,0.0529 -0.0794,0.13229 v 0.0529 l -0.0265,0.0529 v 0.23812 0.0529 l -0.0264,0.0529 -0.21167,0.39688 v 0.0529 l -0.0265,0.0529 0.0265,0.0794 0.0265,0.10583 v 0.0529 l -0.0265,0.0794 -0.13229,0.29104 -0.0265,0.0794 v 0.10583 l 0.0529,0.13229 v 0.0529 0.0265 l -0.0265,0.0529 -0.0529,0.0265 -0.0265,0.0265 -0.13229,0.0265 h -0.0529 l -0.0265,0.0265 v 0.0265 l -0.0265,0.0265 -0.15875,0.29104 -0.0265,0.0794 -0.0265,0.0529 0.26458,4.97417 v 6.87916 0.0265 l -0.0265,5.66208 v 1.05834 h -0.0265 -0.21167 -4.89479 -0.21167 -3.01625 l -0.0529,-0.0265 -0.0265,-0.21167 v -0.10583 l 0.0794,-0.3175 0.0265,-0.50271 0.23813,-1.05833 0.0529,-0.68792 0.18521,-0.37042 0.0265,-0.13229 v -0.60854 l 0.0794,-0.42333 v -0.10584 l -0.0794,-0.42333 0.0265,-0.0265 0.0265,-0.0529 0.0265,-0.0794 v -0.0794 l -0.0265,-0.0529 -0.0265,-0.0529 -0.0529,-0.0794 0.10583,-0.39688 V 156.21 l -0.0265,-0.10583 -0.10584,-0.21167 -0.0265,-0.0794 -0.0265,-0.10583 -0.0794,-0.26458 v -0.0794 l 0.0529,-0.34396 -0.0265,-0.13229 -0.13229,-0.50271 -0.66146,-1.29645 -0.23813,-0.29105 -0.0529,-0.10583 -0.0265,-0.13229 -0.13229,-0.42333 -0.0265,-0.58209 -0.0529,-0.13229 -0.0794,-0.0529 -0.0529,-0.10583 0.0794,-0.13229 -0.0794,-0.13229 V 150.495 l -0.0529,-0.15875 v -0.0794 l 0.0794,-0.13229 0.0265,-0.10583 0.0265,-0.0265 h 0.0264 l 0.0794,0.0529 h 0.0265 l 0.0794,-0.10584 0.0265,-0.13229 0.0265,-0.34396 0.0265,-0.21166 v -0.10584 l -0.10584,-0.15875 -0.0264,-0.21166 -0.0529,-0.10584 -0.0794,-0.0529 -0.0794,-0.0529 -0.0794,-0.10583 -0.0529,-0.29104 -0.0529,-0.1323 -0.10583,-0.0794 -0.0794,-0.0794 -0.18521,-0.13229 -0.10583,-0.0794 -0.0529,-0.0794 v -0.13229 l -0.13229,-0.44979 -0.13229,-0.21167 -0.0794,-0.29104 -0.0265,-0.0794 -0.07938,-0.21167 -0.02646,-0.0794 -0.105833,-0.10583 -0.529167,-0.89958 -0.05292,-0.15875 v -0.13229 -0.29105 -0.10583 l -0.132291,-0.3175 v -0.23812 l -0.05292,-0.0529 -0.02646,-0.18521 0.02646,-0.50271 -0.02646,-0.60854 -0.05292,-0.13229 -0.02646,-0.0529 v -0.0794 l 0.02646,-0.13229 v -0.0794 l -0.105833,-0.26458 -0.05292,-0.18521 0.02646,-0.0794 0.105834,-0.0794 v -0.13229 l -0.07937,-0.29104 0.02646,-0.0529 0.02646,-0.18521 0.105834,-0.18521 -0.02646,-0.0794 -0.05292,-0.0794 -0.02646,-0.10583 -0.02646,-0.10583 -0.05292,-0.0529 -0.05292,-0.0529 -0.05292,-0.0794 -0.05292,-0.0529 -0.02646,-0.10583 -0.105833,-0.52917 -0.02646,-0.0794 v -0.10584 l -0.07937,-0.18521 -0.02646,-0.1852 -0.02646,-0.0529 -0.02646,-0.0529 -0.02646,-0.0529 -0.02646,-0.0265 v -0.10584 -0.0529 l -0.05292,-0.0794 -0.05292,-0.0529 -0.132291,-0.10584 -0.132292,-0.58208 -0.02646,-0.29104 v -0.42334 -0.21166 -0.39688 -0.10583 l -0.02646,-0.13229 0.02646,-0.0529 v -0.0794 l 0.07937,-0.29105 0.02646,-0.0265 v -0.0529 l -0.02646,-0.0794 v -0.0529 l -0.02646,-0.0265 v -0.0265 l 0.02646,-0.10583 -0.02646,-0.0529 -0.02646,-0.15875 v -0.0794 -0.0265 -0.0529 l 0.02646,-0.0529 0.02646,-0.0529 0.05292,-0.0529 0.132292,-0.0794 0.238125,0.0529 0.15875,0.0529 h 0.07937 l 0.132292,0.0265 0.449791,-0.0529 0.185209,-0.0529 h 0.05292 0.07937 l 0.105834,0.0265 0.07937,0.0265 0.05292,0.0265 0.05292,0.0794 h 0.02646 l 0.02646,0.0265 h 0.05292 l 0.185209,0.0529 z"
id="ARD"
name="San Luis"
style="stroke-width:0.264583" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.9 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="19.023661mm"
height="28.363354mm"
viewBox="0 0 19.023661 28.363354"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1"
transform="translate(-95.24991,-134.40833)">
<path
d="m 99.059995,138.29771 0.02646,-0.21167 0.02646,-0.0265 0.185208,-0.39687 0.05292,-0.10584 v -0.0529 -0.21167 l 0.02646,-0.0529 0.02646,-0.10583 1.164167,-2.59292 0.10583,-0.13229 0.21167,0.0265 1.05833,0.34396 0.18521,0.0529 2.88396,0.0529 h 9.02229 l 0.15875,0.50271 0.0794,2.46063 v 0.68791 3.28084 6.27062 1.16417 l -0.3175,2.2225 -0.52917,3.4925 -0.47625,3.04271 -0.26458,1.66687 -0.47625,3.09563 -0.84667,-2.2225 -0.26458,-0.15875 -0.89959,-0.0529 -1.85208,0.0265 h -3.12208 l -0.1323,-0.0265 -0.0264,-0.0529 -0.0265,-0.0265 v -0.10583 -0.0529 -0.0264 l -0.0529,-0.0265 -0.0529,-0.0265 -0.1852,-0.0265 h -0.0529 l -0.0265,-0.0529 v -0.0264 l 0.0265,-0.18521 v -0.0265 -0.0265 -0.0265 l -0.0265,-0.0265 -0.0265,-0.0265 h -0.0529 l -0.21167,0.0529 -0.0529,-0.0265 -0.39687,-0.10583 -0.0794,-0.0265 -0.635,0.10584 h -0.10583 l -0.3175,-0.0265 h -0.0529 l -0.0529,-0.0265 -0.0529,-0.0529 -0.0794,-0.0529 -0.0265,-0.0265 -0.0529,-0.0265 -0.34396,-0.0529 -0.0265,-0.0265 -0.0529,-0.0529 -0.0529,-0.0794 -0.0265,-0.0265 -0.39687,-0.23812 -0.0794,-0.0529 -0.0265,-0.0529 v -0.0794 l 0.0265,-0.15875 -0.0265,-0.0794 v -0.0265 l -0.0529,-0.0265 -1.08479,-0.26458 h -0.21167 l -0.608543,0.13229 -1.031875,0.29104 -1.5875,0.39687 -0.132292,-0.15875 -0.05292,-0.13229 V 158.67 l -0.05292,-0.13229 -0.370417,-0.635 -0.238125,-0.58208 -0.238125,-1.82563 0.02646,-0.52916 -0.105833,-1.21709 v -0.10583 l -0.02646,-0.0265 -0.02646,-0.0265 -0.105833,-0.10583 -0.211667,-0.13229 h -0.05292 l -0.02646,-0.0265 v -0.0265 l -0.02646,-0.0265 v -0.21166 -0.0529 l 0.02646,-0.0265 0.105834,-0.0529 0.02646,-0.0265 0.05292,-0.13229 0.07937,-0.1323 0.132291,-0.13229 0.07937,-0.10583 0.05292,-0.15875 0.05292,-0.34396 -0.05292,-0.92604 -0.47625,-2.27542 0.05292,-0.0529 0.07937,-0.0265 h 0.02646 0.05292 l 0.07937,0.0265 h 0.05292 0.07937 l 0.05292,-0.0265 0.05292,-0.0529 0.05292,-0.10583 0.15875,-0.50271 0.05292,-0.0529 0.02646,-0.0265 h 0.07937 0.02646 l -0.02646,-0.0794 -0.132291,-0.3175 -0.238125,-0.55563 v -0.13229 l 0.05292,-0.18521 h 0.05292 l 0.02646,-0.0264 h 0.105834 0.05292 0.02646 0.02646 l 0.02646,-0.0265 0.05292,-0.0265 0.02646,-0.0265 0.02646,-0.0529 0.05292,-0.10583 0.02646,-0.0529 v -0.0529 h -0.02646 l -0.105833,-0.0794 -0.105833,-0.0529 h -0.07937 l -0.105834,-0.0265 -0.07937,-0.0529 -0.02646,-0.0265 0.343958,-0.39687 0.07937,-0.0794 0.264584,-0.3175 0.07937,-0.0794 0.05292,-0.0265 0.132292,-0.0265 0.02646,-0.0265 0.02646,-0.0529 0.132291,-0.37042 0.02646,-0.0265 0.105833,-0.13229 0.15875,-0.44979 0.05292,-0.26459 0.07937,-0.34395 0.02646,-0.0529 0.02646,-0.0529 0.05292,-0.0529 0.02646,-0.0529 h 0.05292 l 0.02646,-0.0265 0.07937,-0.15875 0.185209,-0.76729 0.07937,-0.26458 0.02646,-0.10584 H 98.081 l 0.02646,-0.0265 h 0.07937 0.07937 0.02646 l 0.02646,-0.0529 0.02646,-0.0529 0.15875,-0.58208 0.02646,-0.0529 0.07937,-0.0265 0.343959,0.0529 0.02646,-0.0265 v -0.10583 -0.23813 l -0.02646,-0.44979 -0.105834,-0.635 v -0.0529 l 0.02646,-0.0529 0.185208,-0.66146 0.02646,-0.18521 z"
id="ARG"
name="Santiago del Estero"
style="stroke-width:0.264583" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="9.3935156mm"
height="12.197932mm"
viewBox="0 0 9.3935156 12.197932"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1"
transform="translate(-100.01234,-142.34548)">
<path
d="m 100.77979,143.45708 1.61396,0.26459 0.1852,0.0265 0.0529,-0.0265 0.0265,-0.0265 0.0265,-0.0265 0.0265,-0.0265 0.0264,-0.0529 v -0.0265 -0.0265 -0.0529 -0.21167 -0.0794 l 0.0265,-0.13229 0.0529,-0.10584 v -0.13229 -0.23812 -0.0794 l 0.0265,-0.0794 v -0.0265 l 0.0265,-0.0265 0.0529,-0.0265 h 0.0265 l 0.0265,0.0265 0.13229,0.0529 0.0265,0.0265 0.26458,0.0794 0.13229,-0.0265 h 0.0794 l 0.52917,0.13229 h 0.0794 0.0265 l 0.0265,-0.0264 0.29104,-0.15875 0.15875,-0.0529 0.0529,-0.0265 h 0.0529 l 0.0265,0.0265 h 0.0265 l 0.0265,0.0265 0.0794,0.15875 0.0265,0.15875 0.0529,0.0794 0.0265,0.0794 0.0794,0.0529 1.05833,0.42334 0.0794,-0.0265 h 0.0529 l 0.10583,0.0265 0.10583,0.0794 0.18521,0.0794 0.10583,0.0265 0.0794,0.0265 0.0265,-0.0265 h 0.0794 l 0.0529,-0.0265 h 0.0265 l 0.10583,-0.0794 0.10583,-0.10583 0.0529,-0.0265 0.26458,-0.10583 0.18521,-0.0265 0.29104,0.0794 h 0.0794 l 1.16417,-0.0265 0.0264,0.42333 -0.0264,0.18521 -0.18521,0.66146 -0.0265,0.0529 v 0.0529 l 0.10583,0.635 0.0265,0.44979 v 0.23813 0.10583 l -0.0265,0.0265 -0.34396,-0.0529 -0.0794,0.0265 -0.0265,0.0529 -0.15875,0.58208 -0.0265,0.0529 -0.0265,0.0529 h -0.0265 -0.0794 -0.0794 l -0.0265,0.0265 h -0.0529 l -0.0265,0.10583 -0.0794,0.26459 -0.18521,0.76729 -0.0794,0.15875 -0.0264,0.0265 h -0.0529 l -0.0265,0.0529 -0.0529,0.0529 -0.0264,0.0529 -0.0265,0.0529 -0.0794,0.34396 -0.0529,0.26459 -0.15875,0.44979 -0.10584,0.13229 -0.0265,0.0265 -0.13229,0.37041 -0.0265,0.0529 -0.0264,0.0265 -0.1323,0.0265 -0.0529,0.0265 -0.0794,0.0794 -0.26458,0.3175 -0.0794,0.0794 -0.34395,0.39687 0.0264,0.0265 0.0794,0.0529 0.10583,0.0265 h 0.0794 l 0.10583,0.0529 0.10583,0.0794 h 0.0265 v 0.0529 l -0.0265,0.0529 -0.0529,0.10583 -0.0265,0.0529 -0.0265,0.0265 -0.0529,0.0265 -0.0265,0.0265 h -0.0265 -0.0265 -0.0529 -0.10583 l -0.0265,0.0265 h -0.0529 l -0.0529,0.18521 v 0.13229 l 0.23812,0.55563 0.13229,0.3175 0.0265,0.0794 h -0.0265 -0.0794 l -0.0265,0.0265 -0.0529,0.0529 -0.15875,0.50271 -0.0529,0.10583 -0.0529,0.0529 -0.0529,0.0265 h -0.0794 -0.0529 l -0.0794,-0.0265 h -0.0529 -0.0265 l -0.0794,0.0265 -0.0529,0.0529 -0.15875,0.0794 h -0.10583 -0.0794 l -0.0794,-0.0265 -0.10584,-0.0529 -0.10583,-0.0794 -0.10583,-0.10584 -0.0794,-0.0529 -0.0794,-0.0529 h -0.0529 -0.0529 l -0.44979,0.29104 -0.15875,0.18521 -0.18521,0.13229 -0.0529,0.0265 -0.13229,0.21167 -0.1323,0.29104 -0.0265,0.0529 -0.0794,-0.0264 -0.44979,-0.58209 -0.0529,-0.0794 -0.0794,-0.23813 -0.0529,-0.3175 -0.0529,-0.18521 -0.0264,-0.0265 -0.0529,-0.0529 -0.23813,-0.13229 h -0.10583 l -0.0529,0.0265 -0.0529,0.0265 -0.13229,0.0529 -0.10584,-0.0794 -0.0265,-0.0265 -0.0529,-0.0529 -0.0265,-0.0794 v -0.1323 l -0.0794,-0.23812 -0.0264,-0.0794 -0.0265,-0.0264 -0.0794,-0.0529 -0.0529,-0.0265 -0.0794,-0.0794 -0.0265,-0.0265 -0.0265,-0.0529 -0.0264,-0.0529 -0.0265,-0.26458 -0.0794,-0.15875 -0.18521,-0.89958 0.0529,-0.15875 v -0.0265 l -0.0265,-0.0529 h -0.0265 -0.0264 l -0.15875,-0.0265 -0.39688,-0.23812 -0.0794,-0.0265 -0.18521,-0.0265 -0.10584,-0.0265 -0.10583,-0.0264 -0.0529,-0.0529 -0.0264,-0.0265 v -0.0265 l 0.0264,-0.0265 0.1323,-0.21166 0.10583,-0.23813 0.15875,-0.18521 0.13229,-0.10583 0.10583,-0.13229 0.0265,-0.0529 0.0265,-0.0265 0.0265,-0.0529 0.0265,-0.0794 0.10583,-0.18521 0.13229,-0.1852 0.18521,-0.18521 0.37042,-0.39688 0.15875,-0.23812 0.0529,-0.15875 0.0265,-0.0794 v -0.0794 -0.34396 -0.0794 l 0.0794,-0.37041 v -0.0529 -0.0794 -0.0529 -0.0529 l -0.0265,-0.0265 v -0.0264 l -0.0265,-0.0529 h -0.0265 l -0.0265,-0.0265 -0.23812,-0.13229 -0.37042,-0.29104 -0.18521,-0.18521 -0.0794,-0.0529 -0.13229,-0.0529 -0.13229,-0.0529 h -0.10584 l -0.13229,-0.0529 -0.0794,-0.0529 -0.0794,-0.1323 -0.0265,-0.0794 v -0.0529 -0.10583 l 0.10584,-0.29104 0.13229,-0.26459 0.0265,-0.15875 0.0264,-0.13229 v -0.10583 l 0.0265,-0.13229 0.0265,-0.0794 0.0794,-0.0794 0.0529,-0.0794 0.0529,-0.0265 z"
id="ART"
name="Tucumán"
style="stroke-width:0.264583" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -1,12 +1,6 @@
// src/apiService.ts
import axios from 'axios';
import type {
ApiResponseRankingMunicipio, ApiResponseRankingSeccion,
ApiResponseTablaDetallada, ProyeccionBancas, MunicipioSimple,
TelegramaData, CatalogoItem, CategoriaResumen, ResultadoTicker,
ApiResponseResultadosPorSeccion, PanelElectoralDto, ResumenProvincia,
CategoriaResumenHome
} from './types/types';
import type { ApiResponseRankingMunicipio, ApiResponseRankingSeccion, ApiResponseTablaDetallada, ProyeccionBancas, MunicipioSimple, TelegramaData, CatalogoItem, CategoriaResumen, ResultadoTicker, ApiResponseResultadosPorSeccion } from './types/types';
/**
* URL base para las llamadas a la API.
@@ -79,6 +73,7 @@ export interface BancadaDetalle {
export interface ConfiguracionPublica {
TickerResultadosCantidad?: string;
ConcejalesResultadosCantidad?: string;
// ... otras claves públicas que pueda añadir en el futuro
}
export interface ResultadoDetalleSeccion {
@@ -89,46 +84,14 @@ export interface ResultadoDetalleSeccion {
color: string | null;
}
export interface PartidoComposicionNacional {
id: string;
nombre: string;
nombreCorto: string | null;
color: string | null;
bancasFijos: number;
bancasGanadas: number;
bancasTotales: number;
ordenDiputadosNacionales: number | null;
ordenSenadoresNacionales: number | null;
}
export interface CamaraComposicionNacional {
camaraNombre: string;
totalBancas: number;
bancasEnJuego: number;
partidos: PartidoComposicionNacional[];
presidenteBancada: { color: string | null; tipoBanca: 'ganada' | 'previa' | null } | null;
ultimaActualizacion: string;
}
export interface ComposicionNacionalData {
diputados: CamaraComposicionNacional;
senadores: CamaraComposicionNacional;
}
export interface ResumenParams {
focoDistritoId?: string;
focoCategoriaId?: number;
cantidadResultados?: number;
}
export const getResumenProvincial = async (eleccionId: number): Promise<CategoriaResumen[]> => {
const response = await apiClient.get(`/elecciones/${eleccionId}/provincia/02`);
export const getResumenProvincial = async (): Promise<CategoriaResumen[]> => {
const response = await apiClient.get('/resultados/provincia/02');
return response.data;
};
export const getBancasPorSeccion = async (eleccionId: number, seccionId: string, camara: 'diputados' | 'senadores'): Promise<ProyeccionBancas> => {
const { data } = await apiClient.get(`/elecciones/${eleccionId}/bancas-por-seccion/${seccionId}/${camara}`);
return data;
export const getBancasPorSeccion = async (seccionId: string, camara: 'diputados' | 'senadores'): Promise<ProyeccionBancas> => {
const { data } = await apiClient.get(`/resultados/bancas-por-seccion/${seccionId}/${camara}`);
return data;
};
/**
@@ -177,13 +140,13 @@ export const getMesasPorEstablecimiento = async (establecimientoId: string): Pro
return response.data;
};
export const getComposicionCongreso = async (eleccionId: number): Promise<ComposicionData> => {
const response = await apiClient.get(`/elecciones/${eleccionId}/composicion-congreso`);
export const getComposicionCongreso = async (): Promise<ComposicionData> => {
const response = await apiClient.get('/resultados/composicion-congreso');
return response.data;
};
export const getBancadasDetalle = async (eleccionId: number): Promise<BancadaDetalle[]> => {
const response = await apiClient.get(`/elecciones/${eleccionId}/bancadas-detalle`);
export const getBancadasDetalle = async (): Promise<BancadaDetalle[]> => {
const response = await apiClient.get('/resultados/bancadas-detalle');
return response.data;
};
@@ -192,18 +155,24 @@ export const getConfiguracionPublica = async (): Promise<ConfiguracionPublica> =
return response.data;
};
export const getResultadosPorSeccion = async (eleccionId: number, seccionId: string, categoriaId: number): Promise<ApiResponseResultadosPorSeccion> => {
const response = await apiClient.get(`/elecciones/${eleccionId}/seccion-resultados/${seccionId}?categoriaId=${categoriaId}`);
export const getResultadosPorSeccion = async (seccionId: string, categoriaId: number): Promise<ApiResponseResultadosPorSeccion> => {
const response = await apiClient.get(`/resultados/seccion-resultados/${seccionId}?categoriaId=${categoriaId}`);
return response.data;
};
export const getDetalleSeccion = async (eleccionId: number, seccionId: string, categoriaId: number): Promise<ResultadoDetalleSeccion[]> => {
const response = await apiClient.get(`/elecciones/${eleccionId}/seccion/${seccionId}?categoriaId=${categoriaId}`);
export const getDetalleSeccion = async (seccionId: string, categoriaId: number): Promise<ResultadoDetalleSeccion[]> => {
const response = await apiClient.get(`/resultados/seccion/${seccionId}?categoriaId=${categoriaId}`);
return response.data;
};
export const getResultadosPorMunicipio = async (eleccionId: number, municipioId: string, categoriaId: number): Promise<ResultadoTicker[]> => {
const response = await apiClient.get(`/elecciones/${eleccionId}/partido/${municipioId}?categoriaId=${categoriaId}`);
export const getResultadosPorMunicipioYCategoria = async (municipioId: string, categoriaId: number): Promise<ResultadoTicker[]> => {
const response = await apiClient.get(`/resultados/partido/${municipioId}?categoriaId=${categoriaId}`);
return response.data.resultados;
};
export const getResultadosPorMunicipio = async (municipioId: string, categoriaId: number): Promise<ResultadoTicker[]> => {
const response = await apiClient.get(`/resultados/partido/${municipioId}?categoriaId=${categoriaId}`);
// La respuesta es un objeto, nosotros extraemos el array de resultados
return response.data.resultados;
};
@@ -244,58 +213,4 @@ export const getRankingMunicipiosPorSeccion = async (seccionId: string): Promise
export const getEstablecimientosPorMunicipio = async (municipioId: string): Promise<CatalogoItem[]> => {
const response = await apiClient.get(`/catalogos/establecimientos-por-municipio/${municipioId}`);
return response.data;
};
export const getPanelElectoral = async (eleccionId: number, ambitoId: string | null, categoriaId: number): Promise<PanelElectoralDto> => {
// Construimos la URL base
let url = ambitoId
? `/elecciones/${eleccionId}/panel/${ambitoId}`
: `/elecciones/${eleccionId}/panel`;
// Añadimos categoriaId como un query parameter
url += `?categoriaId=${categoriaId}`;
const { data } = await apiClient.get(url);
return data;
};
export const getComposicionNacional = async (eleccionId: number): Promise<ComposicionNacionalData> => {
const { data } = await apiClient.get(`/elecciones/${eleccionId}/composicion-nacional`);
return data;
};
// 11. Endpoint para el widget de tarjetas nacionales
export const getResumenPorProvincia = async (eleccionId: number, params: ResumenParams = {}): Promise<ResumenProvincia[]> => {
// Usamos URLSearchParams para construir la query string de forma segura y limpia
const queryParams = new URLSearchParams();
if (params.focoDistritoId) {
queryParams.append('focoDistritoId', params.focoDistritoId);
}
if (params.focoCategoriaId) {
queryParams.append('focoCategoriaId', params.focoCategoriaId.toString());
}
if (params.cantidadResultados) {
queryParams.append('cantidadResultados', params.cantidadResultados.toString());
}
const queryString = queryParams.toString();
// Añadimos la query string a la URL solo si tiene contenido
const url = `/elecciones/${eleccionId}/resumen-por-provincia${queryString ? `?${queryString}` : ''}`;
const { data } = await apiClient.get(url);
return data;
};
export const getHomeResumen = async (eleccionId: number, distritoId: string, categoriaId: number): Promise<CategoriaResumenHome> => {
const queryParams = new URLSearchParams({
eleccionId: eleccionId.toString(),
distritoId: distritoId,
categoriaId: categoriaId.toString(),
});
const url = `/elecciones/home-resumen?${queryParams.toString()}`;
const { data } = await apiClient.get(url);
return data;
};

View File

@@ -1,4 +1,4 @@
/* src/features/legislativas/rovinciales/BancasWidget.css
/* src/components/BancasWidget.css
/* Contenedor principal del widget */
.bancas-widget-container {

View File

@@ -1,9 +1,9 @@
// src/features/legislativas/provinciales/BancasWidget.tsx (Corregido)
// src/components/BancasWidget.tsx (Corregido)
import { useState, useEffect, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import Select from 'react-select'; // --- CAMBIO: Importar react-select ---
import { getBancasPorSeccion, getSeccionesElectoralesConCargos } from '../../../apiService';
import type { ProyeccionBancas, MunicipioSimple } from '../../../types/types';
import { getBancasPorSeccion, getSeccionesElectoralesConCargos } from '../apiService';
import type { ProyeccionBancas, MunicipioSimple } from '../types/types';
import { Tooltip } from 'react-tooltip';
import './BancasWidget.css';
import type { Property } from 'csstype';

View File

@@ -1,10 +1,10 @@
// src/features/legislativas/provinciales/ConcejalesPorSeccionWidget.tsx
// src/components/ConcejalesPorSeccionWidget.tsx
import { useState, useMemo, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import Select from 'react-select';
import { getSeccionesElectorales, getResultadosPorSeccion, getConfiguracionPublica, assetBaseUrl } from '../../../apiService';
import type { MunicipioSimple, ResultadoTicker, ApiResponseResultadosPorSeccion } from '../../../types/types';
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
import { getSeccionesElectorales, getResultadosPorSeccion, getConfiguracionPublica, assetBaseUrl } from '../apiService';
import type { MunicipioSimple, ResultadoTicker, ApiResponseResultadosPorSeccion } from '../types/types';
import { ImageWithFallback } from './ImageWithFallback';
import './TickerWidget.css'; // Reutilizamos los estilos del ticker
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;

View File

@@ -1,9 +1,9 @@
// src/features/legislativas/provinciales/ConcejalesTickerWidget.tsx
// src/components/ConcejalesTickerWidget.tsx
import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { getResumenProvincial, getConfiguracionPublica, assetBaseUrl } from '../../../apiService';
import type { CategoriaResumen, ResultadoTicker } from '../../../types/types';
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
import { getResumenProvincial, getConfiguracionPublica, assetBaseUrl } from '../apiService';
import type { CategoriaResumen, ResultadoTicker } from '../types/types';
import { ImageWithFallback } from './ImageWithFallback';
import './TickerWidget.css'; // Reutilizamos los mismos estilos
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;

View File

@@ -1,10 +1,10 @@
// src/features/legislativas/provinciales/ConcejalesWidget.tsx
// src/components/ConcejalesWidget.tsx
import { useState, useMemo, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import Select from 'react-select';
import { getMunicipios, getResultadosPorMunicipio, getConfiguracionPublica, assetBaseUrl } from '../../../apiService';
import type { MunicipioSimple, ResultadoTicker } from '../../../types/types';
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
import { getMunicipios, getResultadosPorMunicipio, getConfiguracionPublica, assetBaseUrl } from '../apiService';
import type { MunicipioSimple, ResultadoTicker } from '../types/types';
import { ImageWithFallback } from './ImageWithFallback';
import './TickerWidget.css';
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;

View File

@@ -0,0 +1,209 @@
/* src/components/CongresoWidget.css */
.congreso-container {
display: flex;
/* Se reduce ligeramente el espacio entre el gráfico y el panel */
gap: 1rem;
background-color: #ffffff;
border: 1px solid #e0e0e0;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
padding: 1rem;
border-radius: 8px;
max-width: 800px;
margin: 20px auto;
font-family: "Public Sans", system-ui, sans-serif;
color: #333333;
align-items: center;
}
.congreso-grafico {
/* --- CAMBIO PRINCIPAL: Se aumenta la proporción del gráfico --- */
flex: 1 1 65%;
min-width: 300px;
display: flex;
align-items: center;
justify-content: center;
}
.congreso-grafico svg {
width: 100%;
height: auto;
animation: fadeIn 0.8s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.congreso-summary {
/* --- CAMBIO PRINCIPAL: Se reduce la proporción del panel de datos --- */
flex: 1 1 35%;
border-left: 1px solid #e0e0e0;
/* Se reduce el padding para dar aún más espacio al gráfico */
padding-left: 1rem;
}
.congreso-summary h3 {
margin-top: 0;
font-size: 1.4em;
color: #212529;
}
.chamber-tabs {
display: flex;
margin-bottom: 1.5rem;
border: 1px solid #dee2e6;
border-radius: 6px;
overflow: hidden;
}
.chamber-tabs button {
flex: 1;
padding: 0.75rem 0.5rem;
border: none;
background-color: #f8f9fa;
color: #6c757d;
font-family: inherit;
font-size: 1em;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.chamber-tabs button:first-child {
border-right: 1px solid #dee2e6;
}
.chamber-tabs button:hover {
background-color: #e9ecef;
}
.chamber-tabs button.active {
background-color: var(--primary-accent-color);
color: #ffffff;
}
.summary-metric {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 0.5rem;
font-size: 1.1em;
}
.summary-metric strong {
font-size: 1.5em;
font-weight: 700;
color: var(--primary-accent-color);
}
.congreso-summary hr {
border: none;
border-top: 1px solid #e0e0e0;
margin: 1.5rem 0;
}
.partido-lista {
list-style: none;
padding: 0;
margin: 0;
}
.partido-lista li {
display: flex;
align-items: center;
margin-bottom: 0.75rem;
}
.partido-color-box {
width: 14px;
height: 14px;
border-radius: 3px;
margin-right: 10px;
flex-shrink: 0;
}
.partido-nombre {
flex-grow: 1;
}
.partido-bancas {
font-weight: 700;
font-size: 1.1em;
}
/* --- Media Query para Responsividad Móvil --- */
@media (max-width: 768px) {
.congreso-container {
flex-direction: column;
padding: 1.5rem;
}
.congreso-summary {
border-left: none;
padding-left: 0;
margin-top: 2rem;
border-top: 1px solid #e0e0e0;
padding-top: 1.5rem;
}
}
.seat-tooltip {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
padding: 5px;
background-color: white;
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.seat-tooltip img {
width: 60px;
height: 60px;
border-radius: 50%;
object-fit: cover;
border: 2px solid #ccc;
}
.seat-tooltip p {
margin: 0;
font-size: 12px;
font-weight: bold;
color: #333;
}
.seat-tooltip {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
padding: 8px;
background-color: white;
}
.seat-tooltip img {
width: 60px;
height: 60px;
border-radius: 50%;
object-fit: cover;
border: 2px solid #ccc;
}
.seat-tooltip p {
margin: 0;
font-size: 12px;
font-weight: bold;
color: #333;
}
#seat-tooltip.react-tooltip {
opacity: 1 !important;
background-color: white; /* Opcional: asegura un fondo sólido */
}

View File

@@ -1,32 +1,28 @@
// src/features/legislativas/provinciales/CongresoWidget.tsx
// src/components/CongresoWidget.tsx
import { useState, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { ParliamentLayout } from '../../../components/common/ParliamentLayout';
import { SenateLayout } from '../../../components/common/SenateLayout';
import { getComposicionCongreso, getBancadasDetalle } from '../../../apiService';
import type { ComposicionData, BancadaDetalle } from '../../../apiService';
import { ParliamentLayout } from './ParliamentLayout';
import { SenateLayout } from './SenateLayout';
import { getComposicionCongreso, getBancadasDetalle } from '../apiService';
import type { ComposicionData, BancadaDetalle } from '../apiService';
import { Tooltip } from 'react-tooltip';
import './CongresoWidget.css';
type CamaraType = 'diputados' | 'senadores';
const DEFAULT_COLOR = '#808080';
interface CongresoWidgetProps {
eleccionId: number;
}
export const CongresoWidget = ({ eleccionId }: CongresoWidgetProps) => {
export const CongresoWidget = () => {
const [camaraActiva, setCamaraActiva] = useState<CamaraType>('diputados');
const { data: composicionData, isLoading: isLoadingComposicion, error: errorComposicion } = useQuery<ComposicionData>({
queryKey: ['composicionCongreso', eleccionId],
queryFn: () => getComposicionCongreso(eleccionId),
queryKey: ['composicionCongreso'],
queryFn: getComposicionCongreso,
refetchInterval: 180000,
});
const { data: bancadasDetalle = [] } = useQuery<BancadaDetalle[]>({
queryKey: ['bancadasDetalle', eleccionId],
queryFn: () => getBancadasDetalle(eleccionId),
queryKey: ['bancadasDetalle'],
queryFn: getBancadasDetalle,
enabled: !!composicionData,
});

View File

@@ -1,23 +1,23 @@
// src/components/common/DevApp.tsx
import { BancasWidget } from '../../features/legislativas/provinciales/BancasWidget'
import { CongresoWidget } from '../../features/legislativas/provinciales/CongresoWidget'
import MapaBsAs from '../../features/legislativas/provinciales/MapaBsAs'
import { DipSenTickerWidget } from '../../features/legislativas/provinciales/DipSenTickerWidget'
import { TelegramaWidget } from '../../features/legislativas/provinciales/TelegramaWidget'
import { ConcejalesWidget } from '../../features/legislativas/provinciales/ConcejalesWidget'
import MapaBsAsSecciones from '../../features/legislativas/provinciales/MapaBsAsSecciones'
import { SenadoresWidget } from '../../features/legislativas/provinciales/SenadoresWidget'
import { DiputadosWidget } from '../../features/legislativas/provinciales/DiputadosWidget'
import { ResumenGeneralWidget } from '../../features/legislativas/provinciales/ResumenGeneralWidget'
import { SenadoresTickerWidget } from '../../features/legislativas/provinciales/SenadoresTickerWidget'
import { DiputadosTickerWidget } from '../../features/legislativas/provinciales/DiputadosTickerWidget'
import { ConcejalesTickerWidget } from '../../features/legislativas/provinciales/ConcejalesTickerWidget'
import { DiputadosPorSeccionWidget } from '../../features/legislativas/provinciales/DiputadosPorSeccionWidget'
import { SenadoresPorSeccionWidget } from '../../features/legislativas/provinciales/SenadoresPorSeccionWidget'
import { ConcejalesPorSeccionWidget } from '../../features/legislativas/provinciales/ConcejalesPorSeccionWidget'
import { ResultadosTablaDetalladaWidget } from '../../features/legislativas/provinciales/ResultadosTablaDetalladaWidget'
import { ResultadosRankingMunicipioWidget } from '../../features/legislativas/provinciales/ResultadosRankingMunicipioWidget'
import '../../App.css';
// src/components/DevApp.tsx
import { BancasWidget } from './BancasWidget'
import { CongresoWidget } from './CongresoWidget'
import MapaBsAs from './MapaBsAs'
import { DipSenTickerWidget } from './DipSenTickerWidget'
import { TelegramaWidget } from './TelegramaWidget'
import { ConcejalesWidget } from './ConcejalesWidget'
import MapaBsAsSecciones from './MapaBsAsSecciones'
import { SenadoresWidget } from './SenadoresWidget'
import { DiputadosWidget } from './DiputadosWidget'
import { ResumenGeneralWidget } from './ResumenGeneralWidget'
import { SenadoresTickerWidget } from './SenadoresTickerWidget'
import { DiputadosTickerWidget } from './DiputadosTickerWidget'
import { ConcejalesTickerWidget } from './ConcejalesTickerWidget'
import { DiputadosPorSeccionWidget } from './DiputadosPorSeccionWidget'
import { SenadoresPorSeccionWidget } from './SenadoresPorSeccionWidget'
import { ConcejalesPorSeccionWidget } from './ConcejalesPorSeccionWidget'
import { ResultadosTablaDetalladaWidget } from './ResultadosTablaDetalladaWidget'
import { ResultadosRankingMunicipioWidget } from './ResultadosRankingMunicipioWidget'
import '../App.css';
export const DevApp = () => {
@@ -38,7 +38,7 @@ export const DevApp = () => {
<DiputadosPorSeccionWidget />
<SenadoresPorSeccionWidget />
<ConcejalesPorSeccionWidget />
<CongresoWidget eleccionId={1} />
<CongresoWidget />
<BancasWidget />
<MapaBsAs />
<MapaBsAsSecciones />

View File

@@ -1,8 +1,8 @@
// src/features/legislativas/provinciales/DipSenTickerWidget.tsx
// src/components/DipSenTickerWidget.tsx
import { useQuery } from '@tanstack/react-query';
import { getResumenProvincial, getConfiguracionPublica, assetBaseUrl } from '../../../apiService';
import type { CategoriaResumen, ResultadoTicker } from '../../../types/types';
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
import { getResumenProvincial, getConfiguracionPublica, assetBaseUrl } from '../apiService';
import type { CategoriaResumen, ResultadoTicker } from '../types/types';
import { ImageWithFallback } from './ImageWithFallback';
import './TickerWidget.css';
import { useMemo } from 'react';

View File

@@ -1,10 +1,10 @@
// src/features/legislativas/provinciales/DiputadosPorSeccionWidget.tsx
// src/components/DiputadosPorSeccionWidget.tsx
import { useState, useMemo, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import Select from 'react-select';
import { getSeccionesElectorales, getResultadosPorSeccion, getConfiguracionPublica, assetBaseUrl } from '../../../apiService';
import type { MunicipioSimple, ResultadoTicker, ApiResponseResultadosPorSeccion } from '../../../types/types';
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
import { getSeccionesElectorales, getResultadosPorSeccion, getConfiguracionPublica, assetBaseUrl } from '../apiService';
import type { MunicipioSimple, ResultadoTicker, ApiResponseResultadosPorSeccion } from '../types/types';
import { ImageWithFallback } from './ImageWithFallback';
import './TickerWidget.css';
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;

View File

@@ -1,9 +1,9 @@
// src/features/legislativas/provinciales/DiputadosTickerWidget.tsx
// src/components/DiputadosTickerWidget.tsx
import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { getResumenProvincial, getConfiguracionPublica, assetBaseUrl } from '../../../apiService';
import type { CategoriaResumen, ResultadoTicker } from '../../../types/types';
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
import { getResumenProvincial, getConfiguracionPublica, assetBaseUrl } from '../apiService';
import type { CategoriaResumen, ResultadoTicker } from '../types/types';
import { ImageWithFallback } from './ImageWithFallback';
import './TickerWidget.css';
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;

View File

@@ -1,10 +1,10 @@
// src/features/legislativas/provinciales/DiputadosWidget.tsx
// src/components/DiputadosWidget.tsx
import { useState, useMemo, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import Select from 'react-select';
import { getMunicipios, getResultadosPorMunicipio, getConfiguracionPublica, assetBaseUrl } from '../../../apiService';
import type { MunicipioSimple, ResultadoTicker } from '../../../types/types';
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
import { getMunicipios, getResultadosPorMunicipio, getConfiguracionPublica, assetBaseUrl } from '../apiService';
import type { MunicipioSimple, ResultadoTicker } from '../types/types';
import { ImageWithFallback } from './ImageWithFallback';
import './TickerWidget.css';
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;

View File

@@ -1,4 +1,4 @@
// src/components/common/ImageWithFallback.tsx
// src/components/ImageWithFallback.tsx
import { useState, useEffect } from 'react';
interface Props extends React.ImgHTMLAttributes<HTMLImageElement> {

View File

@@ -1,4 +1,4 @@
/* src/features/legislativas/provinciales/MapaBsAs.css */
/* src/components/MapaBsAs.css */
:root {
--primary-accent-color: #0073e6;
--background-panel-color: #ffffff;

View File

@@ -1,4 +1,4 @@
// src/features/legislativas/provinciales/MapaBsAs.tsx
// src/components/MapaBsAs.tsx
import { useState, useMemo, useCallback, useEffect } from 'react';
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
import { Tooltip } from 'react-tooltip';
@@ -7,7 +7,7 @@ import axios from 'axios';
import { feature } from 'topojson-client';
import type { Feature, Geometry } from 'geojson';
import { geoCentroid } from 'd3-geo';
import { API_BASE_URL, assetBaseUrl } from '../../../apiService';
import { API_BASE_URL, assetBaseUrl } from '../apiService';
import './MapaBsAs.css';
// --- Interfaces y Tipos ---

View File

@@ -1,12 +1,12 @@
// src/features/legislativas/provinciales/MapaBsAsSecciones.tsx
// src/components/MapaBsAsSecciones.tsx
import { useState, useMemo, useCallback, useEffect } from 'react';
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
import { Tooltip } from 'react-tooltip';
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
import { geoCentroid } from 'd3-geo';
import { getDetalleSeccion, API_BASE_URL, assetBaseUrl } from '../../../apiService';
import { type ResultadoDetalleSeccion } from '../../../apiService';
import { getDetalleSeccion, API_BASE_URL, assetBaseUrl } from '../apiService';
import { type ResultadoDetalleSeccion } from '../apiService';
import './MapaBsAs.css';
// --- Interfaces y Tipos ---

View File

@@ -1,6 +1,6 @@
// src/components/common/ParliamentLayout.tsx
// src/components/ParliamentLayout.tsx
import React, { useLayoutEffect } from 'react';
import { assetBaseUrl } from '../../apiService';
import { assetBaseUrl } from '../apiService';
import { handleImageFallback } from './imageFallback';
// Interfaces (no cambian)

View File

@@ -1,9 +1,8 @@
// src/features/legislativas/provinciales/ResultaosRankingMunicipioWidget.tsx
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 { getSeccionesElectorales, getRankingMunicipiosPorSeccion } from '../apiService';
import type { MunicipioSimple, ApiResponseRankingMunicipio, RankingPartido } from '../types/types';
import './ResultadosTablaSeccionWidget.css';
type DisplayMode = 'porcentaje' | 'votos' | 'ambos';

View File

@@ -1,9 +1,8 @@
// src/features/legislativas/provinciales/ResultadosTablaDetalladaWidget.tsx
import { useState, useMemo, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import Select from 'react-select';
import { getSeccionesElectorales, getResultadosTablaDetallada } from '../../../apiService';
import type { MunicipioSimple, ApiResponseTablaDetallada } from '../../../types/types';
import { getSeccionesElectorales, getResultadosTablaDetallada } from '../apiService';
import type { MunicipioSimple, ApiResponseTablaDetallada } from '../types/types';
import './ResultadosTablaSeccionWidget.css';
const customSelectStyles = {

View File

@@ -1,4 +1,6 @@
/* src/features/legislativas/provinciales/ResultadosTablaSeccionWidget.css */
/* ==========================================================================
ResultadosTablaSeccionWidget.css
========================================================================== */
.tabla-resultados-widget {
font-family: 'Roboto', sans-serif;

View File

@@ -1,9 +1,9 @@
// src/features/legislativas/provinciales/ResumenGeneralWidget.tsx
// src/components/ResumenGeneralWidget.tsx
import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { getResumenProvincial, getConfiguracionPublica, assetBaseUrl } from '../../../apiService';
import type { CategoriaResumen, ResultadoTicker } from '../../../types/types';
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
import { getResumenProvincial, getConfiguracionPublica, assetBaseUrl } from '../apiService';
import type { CategoriaResumen, ResultadoTicker } from '../types/types';
import { ImageWithFallback } from './ImageWithFallback';
import './TickerWidget.css';
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;

View File

@@ -1,10 +1,10 @@
// src/features/legislativas/provinciales/SenadoresPorSeccionWidget.tsx
// src/components/SenadoresPorSeccionWidget.tsx
import { useState, useMemo, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import Select from 'react-select';
import { getSeccionesElectorales, getResultadosPorSeccion, getConfiguracionPublica, assetBaseUrl } from '../../../apiService';
import type { MunicipioSimple, ResultadoTicker, ApiResponseResultadosPorSeccion } from '../../../types/types';
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
import { getSeccionesElectorales, getResultadosPorSeccion, getConfiguracionPublica, assetBaseUrl } from '../apiService';
import type { MunicipioSimple, ResultadoTicker, ApiResponseResultadosPorSeccion } from '../types/types';
import { ImageWithFallback } from './ImageWithFallback';
import './TickerWidget.css';
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;

View File

@@ -1,9 +1,9 @@
// src/features/legislativas/provinciales/SenadoresTickerWidget.tsx
// src/components/SenadoresTickerWidget.tsx
import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { getResumenProvincial, getConfiguracionPublica, assetBaseUrl } from '../../../apiService';
import type { CategoriaResumen, ResultadoTicker } from '../../../types/types';
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
import { getResumenProvincial, getConfiguracionPublica, assetBaseUrl } from '../apiService';
import type { CategoriaResumen, ResultadoTicker } from '../types/types';
import { ImageWithFallback } from './ImageWithFallback';
import './TickerWidget.css'; // Reutilizamos los mismos estilos
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;

View File

@@ -1,10 +1,10 @@
// src/features/legislativas/provinciales/SenadoresWidget.tsx
// src/components/SenadoresWidget.tsx
import { useState, useEffect, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import Select from 'react-select'; // Importamos react-select
import { getMunicipios, getResultadosPorMunicipio, getConfiguracionPublica, assetBaseUrl } from '../../../apiService'; // Usamos las funciones genéricas
import type { MunicipioSimple, ResultadoTicker } from '../../../types/types';
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
import { getMunicipios, getResultadosPorMunicipio, getConfiguracionPublica, assetBaseUrl } from '../apiService'; // Usamos las funciones genéricas
import type { MunicipioSimple, ResultadoTicker } from '../types/types';
import { ImageWithFallback } from './ImageWithFallback';
import './TickerWidget.css';
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;

View File

@@ -1,7 +1,7 @@
// src/components/common/SenateLayout.tsx
// src/components/SenateLayout.tsx
import React, { useLayoutEffect } from 'react';
import { handleImageFallback } from './imageFallback';
import { assetBaseUrl } from '../../apiService';
import { assetBaseUrl } from '../apiService';
// Interfaces
interface SeatFillData {

View File

@@ -1,4 +1,4 @@
/* src/features/legislativas/provinciales/TelegramaWidget.css */
/* src/components/TelegramaWidget.css */
.telegrama-container {
background-color: #ffffff;
border: 1px solid #e0e0e0;

View File

@@ -1,4 +1,4 @@
// src/features/legislativas/provinciales/TelegramaWidget.tsx
// src/components/TelegramaWidget.tsx
import { useState, useEffect, useMemo } from 'react';
import Select, { type FilterOptionOption } from 'react-select'; // <-- Importar react-select
import {
@@ -8,8 +8,8 @@ import {
getMesasPorEstablecimiento,
getTelegramaPorId,
assetBaseUrl
} from '../../../apiService';
import type { TelegramaData, CatalogoItem } from '../../../types/types';
} from '../apiService';
import type { TelegramaData, CatalogoItem } from '../types/types';
import './TelegramaWidget.css';
import { pdfjs, Document, Page } from 'react-pdf';

View File

@@ -1,4 +1,6 @@
/* src/features/legislativas/provinciales/TickerWidget.css */
/* ==========================================================================
TickerWidget.css (Versión Mejorada y Responsiva)
========================================================================== */
/* --- Contenedor Principal del Widget --- */
.ticker-card {

View File

@@ -1,339 +0,0 @@
// src/components/common/DiputadosNacionalesLayout.tsx
import React from 'react';
import type { PartidoComposicionNacional } from '../../apiService';
// --- Interfaces Actualizadas ---
interface DiputadosNacionalesLayoutProps {
partyData: PartidoComposicionNacional[];
size?: number;
presidenteBancada?: { color: string | null } | null; // <-- Nueva Prop
}
const PRESIDENTE_SEAT_INDEX = 0; // El escaño 'seat-0' es el del presidente
export const DiputadosNacionalesLayout: React.FC<DiputadosNacionalesLayoutProps> = ({
partyData,
size = 800,
presidenteBancada, // <-- Recibimos la nueva prop
}) => {
// --- ARRAY DE 257 ELEMENTOS <circle> ORDENADOS POR ID DE "seat-X" ---
const seatElements = [
<circle key="seat-0" id="seat-0" r="15.7" cy="639.5" cx="595.3" />,
<circle key="seat-1" id="seat-1" r="15.7" cy="673.1" cx="109.3" />,
<circle key="seat-2" id="seat-2" r="15.7" cy="673.1" cx="161.7" />,
<circle key="seat-3" id="seat-3" r="15.7" cy="673.5" cx="214.3" />,
<circle key="seat-4" id="seat-4" r="15.7" cy="673.2" cx="266.5" />,
<circle key="seat-5" id="seat-5" r="15.7" cy="669.5" cx="319.4" />,
<circle key="seat-6" id="seat-6" r="15.7" cy="660" cx="370.8" />,
<circle key="seat-7" id="seat-7" transform="rotate(-88.1)" r="15.7" cy="77.69" cx="-634.1" />,
<circle key="seat-8" id="seat-8" r="15.7" cy="639" cx="109.3" />,
<circle key="seat-9" id="seat-9" r="15.7" cy="639" cx="161.7" />,
<circle key="seat-10" id="seat-10" r="15.7" cy="639.2" cx="214.3" />,
<circle key="seat-11" id="seat-11" r="15.7" cy="638.8" cx="266.7" />,
<circle key="seat-12" id="seat-12" r="15.7" cy="635.1" cx="319.4" />,
<circle key="seat-13" id="seat-13" r="15.7" cy="625.7" cx="371.7" />,
<circle key="seat-14" id="seat-14" r="15.7" cy="639" cx="424.2" />,
<circle key="seat-15" id="seat-15" transform="rotate(-88.1)" r="15.7" cy="77" cx="-600.18" />,
<circle key="seat-16" id="seat-16" r="15.7" cy="600.9" cx="109.5" />,
<circle key="seat-17" id="seat-17" r="15.7" cy="603.7" cx="162.1" />,
<circle key="seat-18" id="seat-18" r="15.7" cy="598.6" cx="215" />,
<circle key="seat-19" id="seat-19" r="15.7" cy="602.6" cx="267.1" />,
<circle key="seat-20" id="seat-20" transform="rotate(-88.1)" r="15.7" cy="76.57" cx="-562.57" />,
<circle key="seat-21" id="seat-21" r="15.7" cy="566.7" cx="112.2" />,
<circle key="seat-22" id="seat-22" r="15.7" cy="570" cx="164.7" />,
<circle key="seat-23" id="seat-23" r="15.7" cy="564.5" cx="218.2" />,
<circle key="seat-24" id="seat-24" r="15.7" cy="568.6" cx="270.9" />,
<circle key="seat-25" id="seat-25" r="15.7" cy="588" cx="321.1" />,
<circle key="seat-26" id="seat-26" transform="rotate(-88.1)" r="15.7" cy="79.88" cx="-524.51" />,
<circle key="seat-27" id="seat-27" transform="rotate(-5.7)" r="15.7" cy="539.19" cx="65.05" />,
<circle key="seat-28" id="seat-28" r="15.7" cy="535.9" cx="170" />,
<circle key="seat-29" id="seat-29" transform="rotate(-88.1)" r="15.7" cy="86.87" cx="-488.2" />,
<circle key="seat-30" id="seat-30" r="15.7" cy="497.2" cx="125.2" />,
<circle key="seat-31" id="seat-31" r="15.7" cy="502.8" cx="178.2" />,
<circle key="seat-32" id="seat-32" r="15.7" cy="525.1" cx="226.3" />,
<circle key="seat-33" id="seat-33" r="15.7" cy="533.1" cx="278.4" />,
<circle key="seat-34" id="seat-34" r="15.7" cy="554.6" cx="327.1" />,
<circle key="seat-35" id="seat-35" r="15.7" cy="567.9" cx="377.9" />,
<circle key="seat-36" id="seat-36" r="15.7" cy="596.7" cx="426" />,
<circle key="seat-37" id="seat-37" r="15.7" cy="453.8" cx="79.7" />,
<circle key="seat-38" id="seat-38" r="15.7" cy="462" cx="135.7" />,
<circle key="seat-39" id="seat-39" r="15.7" cy="469.3" cx="188.9" />,
<circle key="seat-40" id="seat-40" r="15.7" cy="492.6" cx="236.4" />,
<circle key="seat-41" id="seat-41" r="15.7" cy="500.6" cx="289.8" />,
<circle key="seat-42" id="seat-42" r="15.7" cy="511.6" cx="341.5" />,
<circle key="seat-43" id="seat-43" r="15.7" cy="535" cx="388.9" />,
<circle key="seat-44" id="seat-44" r="15.7" cy="555" cx="437.3" />,
<circle key="seat-45" id="seat-45" r="15.7" cy="419.3" cx="92.8" />,
<circle key="seat-46" id="seat-46" r="15.7" cy="429.8" cx="148.1" />,
<circle key="seat-47" id="seat-47" r="15.7" cy="387.4" cx="106.8" />,
<circle key="seat-48" id="seat-48" transform="rotate(-5.7)" r="15.7" cy="364.72" cx="89.86" />,
<circle key="seat-49" id="seat-49" r="15.7" cy="395.5" cx="164.4" />,
<circle key="seat-50" id="seat-50" r="15.7" cy="437.3" cx="202.4" />,
<circle key="seat-51" id="seat-51" r="15.7" cy="455.4" cx="252.1" />,
<circle key="seat-52" id="seat-52" r="15.7" cy="325.1" cx="144.9" />,
<circle key="seat-53" id="seat-53" r="15.7" cy="365.7" cx="181.3" />,
<circle key="seat-54" id="seat-54" r="15.7" cy="405.1" cx="218.8" />,
<circle key="seat-55" id="seat-55" r="15.7" cy="425.6" cx="267.7" />,
<circle key="seat-56" id="seat-56" r="15.7" cy="464.9" cx="306.5" />,
<circle key="seat-57" id="seat-57" r="15.7" cy="292.1" cx="168.7" />,
<circle key="seat-58" id="seat-58" r="15.7" cy="334.6" cx="202.3" />,
<circle key="seat-59" id="seat-59" r="15.7" cy="376.9" cx="236.7" />,
<circle key="seat-60" id="seat-60" r="15.7" cy="265.1" cx="190.8" />,
<circle key="seat-61" id="seat-61" r="15.7" cy="307.2" cx="224" />,
<circle key="seat-62" id="seat-62" r="15.7" cy="346.9" cx="259.3" />,
<circle key="seat-63" id="seat-63" r="15.7" cy="393" cx="289.6" />,
<circle key="seat-64" id="seat-64" r="15.7" cy="435.9" cx="323.7" />,
<circle key="seat-65" id="seat-65" r="15.7" cy="480.8" cx="357.3" />,
<circle key="seat-66" id="seat-66" r="15.7" cy="236.2" cx="218.1" />,
<circle key="seat-67" id="seat-67" r="15.7" cy="278.6" cx="250" />,
<circle key="seat-68" id="seat-68" r="15.7" cy="320.2" cx="283" />,
<circle key="seat-69" id="seat-69" r="15.7" cy="362" cx="315.5" />,
<circle key="seat-70" id="seat-70" r="15.7" cy="403.8" cx="348.7" />,
<circle key="seat-71" id="seat-71" r="15.7" cy="445.9" cx="381.6" />,
<circle key="seat-72" id="seat-72" r="15.7" cy="489" cx="415.1" />,
<circle key="seat-73" id="seat-73" r="15.7" cy="515.6" cx="460.7" />,
<circle key="seat-74" id="seat-74" r="15.7" cy="485.2" cx="491" />,
<circle key="seat-75" id="seat-75" r="15.7" cy="213.6" cx="243.2" />,
<circle key="seat-76" id="seat-76" r="15.7" cy="254.9" cx="275.3" />,
<circle key="seat-77" id="seat-77" r="15.7" cy="296.4" cx="307.8" />,
<circle key="seat-78" id="seat-78" r="15.7" cy="337.6" cx="339.9" />,
<circle key="seat-79" id="seat-79" r="15.7" cy="379" cx="372.5" />,
<circle key="seat-80" id="seat-80" r="15.7" cy="420.8" cx="405.1" />,
<circle key="seat-81" id="seat-81" r="15.7" cy="462.7" cx="437.2" />,
<circle key="seat-82" id="seat-82" r="15.5" cy="181.8" cx="283.1" />,
<circle key="seat-83" id="seat-83" r="15.5" cy="223.6" cx="315.4" />,
<circle key="seat-84" id="seat-84" r="15.7" cy="262.6" cx="351" />,
<circle key="seat-85" id="seat-85" r="15.5" cy="304.5" cx="382.7" />,
<circle key="seat-86" id="seat-86" r="15.7" cy="339.1" cx="425.3" />,
<circle key="seat-87" id="seat-87" r="15.7" cy="379" cx="461" />,
<circle key="seat-88" id="seat-88" r="15.7" cy="420.4" cx="495.9" />,
<circle key="seat-89" id="seat-89" r="15.7" cy="463.5" cx="528.1" />,
<circle key="seat-90" id="seat-90" r="15.5" cy="160.4" cx="315.7" />,
<circle key="seat-91" id="seat-91" r="15.5" cy="206.2" cx="342.9" />,
<circle key="seat-92" id="seat-92" r="15.7" cy="245.1" cx="379" />,
<circle key="seat-93" id="seat-93" r="15.5" cy="287.4" cx="410.5" />,
<circle key="seat-94" id="seat-94" r="15.7" cy="323.4" cx="455.9" />,
<circle key="seat-95" id="seat-95" transform="rotate(-80.8)" r="15.7" cy="555.93" cx="-274.27" />,
<circle key="seat-96" id="seat-96" r="15.7" cy="407.6" cx="527.7" />,
<circle key="seat-97" id="seat-97" r="15.5" cy="142.7" cx="345.9" />,
<circle key="seat-98" id="seat-98" r="15.5" cy="186.8" cx="375.8" />,
<circle key="seat-99" id="seat-99" r="15.5" cy="125.9" cx="377.8" />,
<circle key="seat-100" id="seat-100" r="15.5" cy="173.7" cx="405.1" />,
<circle key="seat-101" id="seat-101" r="15.7" cy="223" cx="422.9" />,
<circle key="seat-102" id="seat-102" r="15.5" cy="270.9" cx="444.3" />,
<circle key="seat-103" id="seat-103" r="15.5" cy="112" cx="409.4" />,
<circle key="seat-104" id="seat-104" r="15.5" cy="157.7" cx="438.1" />,
<circle key="seat-105" id="seat-105" r="15.7" cy="209" cx="453.9" />,
<circle key="seat-106" id="seat-106" r="15.5" cy="259.6" cx="474.2" />,
<circle key="seat-107" id="seat-107" r="15.7" cy="306.3" cx="499.3" />,
<circle key="seat-108" id="seat-108" r="15.5" cy="100.1" cx="443.4" />,
<circle key="seat-109" id="seat-109" r="15.5" cy="146.7" cx="472.7" />,
<circle key="seat-110" id="seat-110" r="15.7" cy="197.9" cx="497" />,
<circle key="seat-111" id="seat-111" r="15.5" cy="249" cx="508.8" />,
<circle key="seat-112" id="seat-112" r="15.7" cy="298.4" cx="532.7" />,
<circle key="seat-113" id="seat-113" r="15.7" cy="350.8" cx="538.1" />,
<circle key="seat-114" id="seat-114" r="15.5" cy="92.2" cx="477" />,
<circle key="seat-115" id="seat-115" r="15.5" cy="84.4" cx="510" />,
<circle key="seat-116" id="seat-116" transform="rotate(-80.8)" r="15.5" cy="523.04" cx="-55.62" />,
<circle key="seat-117" id="seat-117" r="15.7" cy="190.1" cx="531.6" />,
<circle key="seat-118" id="seat-118" r="15.5" cy="243.4" cx="542.3" />,
<circle key="seat-119" id="seat-119" r="15.5" cy="80.7" cx="544.3" />,
<circle key="seat-120" id="seat-120" r="15.5" cy="136.1" cx="541.9" />,
<circle key="seat-121" id="seat-121" r="15.5" cy="78.5" cx="579" />,
<circle key="seat-122" id="seat-122" r="15.5" cy="135" cx="578.2" />,
<circle key="seat-123" id="seat-123" r="15.7" cy="187.6" cx="577.9" />,
<circle key="seat-124" id="seat-124" r="15.5" cy="240" cx="579" />,
<circle key="seat-125" id="seat-125" r="15.7" cy="292.6" cx="578" />,
<circle key="seat-126" id="seat-126" r="15.7" cy="345.3" cx="578" />,
<circle key="seat-127" id="seat-127" r="15.7" cy="398" cx="577.8" />,
<circle key="seat-128" id="seat-128" r="15.7" cy="451.2" cx="572.2" />,
<circle key="seat-129" id="seat-129" r="15.5" cy="78.5" cx="613.5" />,
<circle key="seat-130" id="seat-130" r="15.5" cy="135" cx="612.3" />,
<circle key="seat-131" id="seat-131" r="15.7" cy="187.6" cx="612.6" />,
<circle key="seat-132" id="seat-132" r="15.5" cy="240" cx="611.5" />,
<circle key="seat-133" id="seat-133" r="15.7" cy="292.6" cx="612.5" />,
<circle key="seat-134" id="seat-134" r="15.7" cy="345.3" cx="612.5" />,
<circle key="seat-135" id="seat-135" r="15.7" cy="398" cx="612.7" />,
<circle key="seat-136" id="seat-136" r="15.7" cy="451.2" cx="618.3" />,
<circle key="seat-137" id="seat-137" r="15.5" cy="82.6" cx="646.3" />,
<circle key="seat-138" id="seat-138" r="15.5" cy="86.4" cx="680.5" />,
<circle key="seat-139" id="seat-139" r="15.5" cy="138.4" cx="650.6" />,
<circle key="seat-140" id="seat-140" r="15.5" cy="94.2" cx="715.6" />,
<circle key="seat-141" id="seat-141" r="15.5" cy="142.6" cx="685.4" />,
<circle key="seat-142" id="seat-142" r="15.7" cy="190.1" cx="657" />,
<circle key="seat-143" id="seat-143" r="15.5" cy="243.4" cx="648.3" />,
<circle key="seat-144" id="seat-144" r="15.5" cy="104.1" cx="747.1" />,
<circle key="seat-145" id="seat-145" r="15.5" cy="150.7" cx="719.9" />,
<circle key="seat-146" id="seat-146" r="15.7" cy="197.9" cx="691.5" />,
<circle key="seat-147" id="seat-147" r="15.5" cy="248.5" cx="679.8" />,
<circle key="seat-148" id="seat-148" r="15.7" cy="298.4" cx="657.8" />,
<circle key="seat-149" id="seat-149" r="15.7" cy="350.8" cx="652.4" />,
<circle key="seat-150" id="seat-150" r="15.5" cy="116" cx="783.1" />,
<circle key="seat-151" id="seat-151" r="15.5" cy="159.7" cx="750.4" />,
<circle key="seat-152" id="seat-152" r="15.7" cy="211" cx="736.6" />,
<circle key="seat-153" id="seat-153" r="15.5" cy="259.6" cx="716.4" />,
<circle key="seat-154" id="seat-154" r="15.7" cy="306.3" cx="691.2" />,
<circle key="seat-155" id="seat-155" r="15.5" cy="127.9" cx="812.8" />,
<circle key="seat-156" id="seat-156" r="15.5" cy="173.7" cx="785.5" />,
<circle key="seat-157" id="seat-157" r="15.7" cy="223" cx="767.7" />,
<circle key="seat-158" id="seat-158" r="15.5" cy="270.9" cx="746.3" />,
<circle key="seat-159" id="seat-159" r="15.5" cy="144.7" cx="846.6" />,
<circle key="seat-160" id="seat-160" r="15.5" cy="186.8" cx="814.8" />,
<circle key="seat-161" id="seat-161" r="15.5" cy="160.4" cx="874.8" />,
<circle key="seat-162" id="seat-162" r="15.5" cy="206.2" cx="847.6" />,
<circle key="seat-163" id="seat-163" r="15.7" cy="245.1" cx="811.5" />,
<circle key="seat-164" id="seat-164" r="15.5" cy="287.4" cx="780.1" />,
<circle key="seat-165" id="seat-165" r="15.7" cy="323.4" cx="734.6" />,
<circle key="seat-166" id="seat-166" r="15.7" cy="357.8" cx="687.4" />,
<circle key="seat-167" id="seat-167" r="15.7" cy="407.6" cx="662.8" />,
<circle key="seat-168" id="seat-168" r="15.5" cy="181.8" cx="907.5" />,
<circle key="seat-169" id="seat-169" r="15.5" cy="223.6" cx="875.2" />,
<circle key="seat-170" id="seat-170" r="15.7" cy="262.6" cx="839.5" />,
<circle key="seat-171" id="seat-171" r="15.5" cy="304.3" cx="807.8" />,
<circle key="seat-172" id="seat-172" r="15.7" cy="339.1" cx="765.3" />,
<circle key="seat-173" id="seat-173" r="15.7" cy="379" cx="729.6" />,
<circle key="seat-174" id="seat-174" r="15.7" cy="420.4" cx="694.6" />,
<circle key="seat-175" id="seat-175" r="15.7" cy="463.5" cx="662.5" />,
<circle key="seat-176" id="seat-176" r="15.7" cy="485.4" cx="699.5" />,
<circle key="seat-177" id="seat-177" r="15.7" cy="213.6" cx="947.4" />,
<circle key="seat-178" id="seat-178" r="15.7" cy="254.9" cx="915.2" />,
<circle key="seat-179" id="seat-179" r="15.7" cy="296.4" cx="882.7" />,
<circle key="seat-180" id="seat-180" r="15.7" cy="337.6" cx="850.7" />,
<circle key="seat-181" id="seat-181" r="15.7" cy="379" cx="818.1" />,
<circle key="seat-182" id="seat-182" r="15.7" cy="420.8" cx="785.4" />,
<circle key="seat-183" id="seat-183" r="15.7" cy="462.7" cx="753.4" />,
<circle key="seat-184" id="seat-184" r="15.7" cy="515.4" cx="730.1" />,
<circle key="seat-185" id="seat-185" r="15.7" cy="236.2" cx="972.4" />,
<circle key="seat-186" id="seat-186" r="15.7" cy="278.6" cx="940.5" />,
<circle key="seat-187" id="seat-187" r="15.7" cy="320.2" cx="907.5" />,
<circle key="seat-188" id="seat-188" r="15.7" cy="362" cx="875.1" />,
<circle key="seat-189" id="seat-189" r="15.7" cy="403.8" cx="841.8" />,
<circle key="seat-190" id="seat-190" r="15.7" cy="445.9" cx="808.9" />,
<circle key="seat-191" id="seat-191" r="15.7" cy="489" cx="775.5" />,
<circle key="seat-192" id="seat-192" r="15.7" cy="265.1" cx="999.7" />,
<circle key="seat-193" id="seat-193" r="15.7" cy="307.2" cx="966.6" />,
<circle key="seat-194" id="seat-194" r="15.7" cy="346.9" cx="931.2" />,
<circle key="seat-195" id="seat-195" r="15.7" cy="393" cx="901" />,
<circle key="seat-196" id="seat-196" r="15.7" cy="435.9" cx="866.9" />,
<circle key="seat-197" id="seat-197" r="15.7" cy="480.8" cx="833.2" />,
<circle key="seat-198" id="seat-198" transform="rotate(-80.8)" r="15.7" cy="1055.16" cx="-124.85" />,
<circle key="seat-199" id="seat-199" r="15.7" cy="334.6" cx="988.2" />,
<circle key="seat-200" id="seat-200" r="15.7" cy="376.9" cx="953.8" />,
<circle key="seat-201" id="seat-201" r="15.7" cy="425.6" cx="922.8" />,
<circle key="seat-202" id="seat-202" r="15.7" cy="464.9" cx="884" />,
<circle key="seat-203" id="seat-203" r="15.7" cy="325.1" cx="1045.7" />,
<circle key="seat-204" id="seat-204" r="15.7" cy="365.7" cx="1009.2" />,
<circle key="seat-205" id="seat-205" r="15.7" cy="405.1" cx="971.7" />,
<circle key="seat-206" id="seat-206" r="15.7" cy="354.1" cx="1063.2" />,
<circle key="seat-207" id="seat-207" transform="rotate(-80.8)" r="15.7" cy="1075.78" cx="-226.25" />,
<circle key="seat-208" id="seat-208" r="15.7" cy="387.4" cx="1081.8" />,
<circle key="seat-209" id="seat-209" r="15.7" cy="421.3" cx="1095.7" />,
<circle key="seat-210" id="seat-210" r="15.7" cy="429.8" cx="1042.5" />,
<circle key="seat-211" id="seat-211" r="15.7" cy="437.3" cx="988.2" />,
<circle key="seat-212" id="seat-212" r="15.7" cy="455.4" cx="938.5" />,
<circle key="seat-213" id="seat-213" r="15.7" cy="455.8" cx="1108.8" />,
<circle key="seat-214" id="seat-214" r="15.7" cy="462" cx="1054.9" />,
<circle key="seat-215" id="seat-215" r="15.7" cy="469.3" cx="1001.6" />,
<circle key="seat-216" id="seat-216" r="15.7" cy="492.6" cx="954.1" />,
<circle key="seat-217" id="seat-217" r="15.7" cy="500.6" cx="900.8" />,
<circle key="seat-218" id="seat-218" r="15.7" cy="511.6" cx="849" />,
<circle key="seat-219" id="seat-219" r="15.7" cy="535" cx="801.6" />,
<circle key="seat-220" id="seat-220" r="15.7" cy="554.8" cx="753.3" />,
<circle key="seat-221" id="seat-221" r="15.7" cy="490.9" cx="1118" />,
<circle key="seat-222" id="seat-222" r="15.7" cy="497.2" cx="1065.3" />,
<circle key="seat-223" id="seat-223" r="15.7" cy="502.8" cx="1012.3" />,
<circle key="seat-224" id="seat-224" r="15.7" cy="525.1" cx="964.2" />,
<circle key="seat-225" id="seat-225" r="15.7" cy="533.1" cx="912.2" />,
<circle key="seat-226" id="seat-226" r="15.7" cy="554.6" cx="863.4" />,
<circle key="seat-227" id="seat-227" r="15.7" cy="567.9" cx="812.7" />,
<circle key="seat-228" id="seat-228" r="15.7" cy="596.7" cx="764.8" />,
<circle key="seat-229" id="seat-229" r="15.7" cy="528.9" cx="1126.1" />,
<circle key="seat-230" id="seat-230" r="15.7" cy="530.2" cx="1072.7" />,
<circle key="seat-231" id="seat-231" transform="rotate(-80.8)" r="15.7" cy="1092.81" cx="-365.69" />,
<circle key="seat-232" id="seat-232" r="15.7" cy="562.9" cx="1130.6" />,
<circle key="seat-233" id="seat-233" r="15.7" cy="566.7" cx="1078.3" />,
<circle key="seat-234" id="seat-234" transform="rotate(-80.8)" r="15.7" cy="1103.39" cx="-398.54" />,
<circle key="seat-235" id="seat-235" r="15.7" cy="564.5" cx="972.4" />,
<circle key="seat-236" id="seat-236" r="15.7" cy="568.6" cx="919.7" />,
<circle key="seat-237" id="seat-237" r="15.7" cy="588" cx="869.4" />,
<circle key="seat-238" id="seat-238" r="15.7" cy="602.5" cx="1133.5" />,
<circle key="seat-239" id="seat-239" r="15.7" cy="600.9" cx="1081" />,
<circle key="seat-240" id="seat-240" transform="rotate(-80.8)" r="15.7" cy="1111.41" cx="-431.3" />,
<circle key="seat-241" id="seat-241" r="15.7" cy="598.6" cx="975.6" />,
<circle key="seat-242" id="seat-242" r="15.7" cy="602.6" cx="923.4" />,
<circle key="seat-243" id="seat-243" r="15.7" cy="636.4" cx="1133.9" />,
<circle key="seat-244" id="seat-244" r="15.7" cy="639" cx="1081.3" />,
<circle key="seat-245" id="seat-245" transform="rotate(-80.8)" r="15.7" cy="1117.48" cx="-466.13" />,
<circle key="seat-246" id="seat-246" r="15.7" cy="639.2" cx="976.3" />,
<circle key="seat-247" id="seat-247" r="15.7" cy="638.8" cx="923.9" />,
<circle key="seat-248" id="seat-248" r="15.7" cy="635.1" cx="871.2" />,
<circle key="seat-249" id="seat-249" r="15.7" cy="625.7" cx="818.8" />,
<circle key="seat-250" id="seat-250" r="15.7" cy="639" cx="766.3" />,
<circle key="seat-251" id="seat-251" r="15.7" cy="673.1" cx="1081.3" />,
<circle key="seat-252" id="seat-252" transform="rotate(-80.8)" r="15.7" cy="1122.99" cx="-499.74" />,
<circle key="seat-253" id="seat-253" r="15.7" cy="673.5" cx="976.3" />,
<circle key="seat-254" id="seat-254" r="15.7" cy="673.2" cx="924" />,
<circle key="seat-255" id="seat-255" r="15.7" cy="669.5" cx="871.2" />,
<circle key="seat-256" id="seat-256" r="15.7" cy="660" cx="819.7" />,
];
let seatIndex = 1; // Empezamos a contar desde 1, ya que el 0 es presidencial
return (
<svg viewBox="0 0 1190.6 772.2" width={size} height={size * (772.2 / 1190.6)} style={{ display: 'block', margin: 'auto' }}>
<g>
{/* Renderizamos el escaño presidencial primero y por separado */}
{presidenteBancada && React.cloneElement(seatElements[PRESIDENTE_SEAT_INDEX], {
fill: presidenteBancada.color || '#A9A9A9',
strokeWidth: 0.5,
})}
{partyData.map(partido => {
// Por cada partido, creamos un array combinado de sus escaños
const partySeats = [
...Array(partido.bancasFijos).fill({ isNew: false }),
...Array(partido.bancasGanadas).fill({ isNew: true })
];
return (
// Envolvemos todos los escaños de un partido en un <g>
<g
key={partido.id}
className="party-block"
data-tooltip-id="party-tooltip"
data-tooltip-content={`${partido.nombreCorto || partido.nombre}: ${partido.bancasTotales} bancas`}
>
{partySeats.map((seatInfo, i) => {
// Si ya no hay más plantillas de escaños, no renderizamos nada
if (seatIndex >= seatElements.length) return null;
const template = seatElements[seatIndex];
seatIndex++; // Incrementamos el contador para el siguiente escaño
// Clonamos la plantilla con el estilo apropiado
return React.cloneElement(template, {
key: `${partido.id}-${i}`,
className: 'seat-circle',
fill: partido.color || '#808080',
fillOpacity: seatInfo.isNew ? 1 : 0.3, // Opacidad para bancas previas
stroke: partido.color || '#808080',
strokeWidth: 0.5,
});
})}
</g>
);
})}
{/* Renderizamos los escaños vacíos sobrantes */}
{seatIndex < seatElements.length &&
seatElements.slice(seatIndex).map((template, i) =>
React.cloneElement(template, {
key: `empty-${i}`,
fill: '#E0E0E0',
stroke: '#ffffff',
strokeWidth: 0.5
})
)
}
</g>
</svg>
);
};

View File

@@ -1,154 +0,0 @@
// src/components/common/SenadoresNacionalesLayout.tsx
import React from 'react';
import type { PartidoComposicionNacional } from '../../apiService';
// Interfaces
interface SenadoresNacionalesLayoutProps {
partyData: PartidoComposicionNacional[];
size?: number;
presidenteBancada?: { color: string | null } | null;
}
const PRESIDENTE_SEAT_INDEX = 0;
export const SenadoresNacionalesLayout: React.FC<SenadoresNacionalesLayoutProps> = ({
partyData,
size = 800,
presidenteBancada,
}) => {
// --- ARRAY DE 73 ELEMENTOS <circle> ORDENADOS POR ID DE "seat-X" ---
// El asiento 0 es el presidencial, los 72 restantes son los senadores.
const seatElements = [
<circle key="seat-0" id="seat-0" r="7.1" cy="187" cx="168.6" />,
<circle key="seat-1" id="seat-1" r="7.1" cy="166" cx="21.8" />,
<circle key="seat-2" id="seat-2" r="7.1" cy="172" cx="51.5" />,
<circle key="seat-3" id="seat-3" r="7.1" cy="174.5" cx="82.7" />,
<circle key="seat-4" id="seat-4" r="7.1" cy="147.4" cx="21.5" />,
<circle key="seat-5" id="seat-5" r="7.1" cy="155.2" cx="51.8" />,
<circle key="seat-6" id="seat-6" r="7.1" cy="156.3" cx="83.4" />,
<circle key="seat-7" id="seat-7" r="7.1" cy="169.9" cx="120.9" />,
<circle key="seat-8" id="seat-8" r="7.1" cy="128.4" cx="22.8" />,
<circle key="seat-9" id="seat-9" r="7.1" cy="137.9" cx="53.2" />,
<circle key="seat-10" id="seat-10" r="7.1" cy="138.8" cx="85.5" />,
<circle key="seat-11" id="seat-11" r="7.1" cy="151.9" cx="120.9" />,
<circle key="seat-12" id="seat-12" r="7.1" cy="109" cx="25.6" />,
<circle key="seat-13" id="seat-13" r="7.1" cy="121.3" cx="57.2" />,
<circle key="seat-14" id="seat-14" r="7.1" cy="91.5" cx="34.2" />,
<circle key="seat-15" id="seat-15" r="7.1" cy="105.7" cx="64.8" />,
<circle key="seat-16" id="seat-16" r="7.1" cy="122.5" cx="92.9" />,
<circle key="seat-17" id="seat-17" r="7.1" cy="136.2" cx="128.2" />,
<circle key="seat-18" id="seat-18" r="7.1" cy="75.5" cx="45.3" />,
<circle key="seat-19" id="seat-19" r="7.1" cy="91.3" cx="75.7" />,
<circle key="seat-20" id="seat-20" r="7.1" cy="106.5" cx="106.3" />,
<circle key="seat-21" id="seat-21" r="7.1" cy="59.8" cx="57.9" />,
<circle key="seat-22" id="seat-22" r="7.1" cy="78.6" cx="89.5" />,
<circle key="seat-23" id="seat-23" r="7.1" cy="45.3" cx="73.2" />,
<circle key="seat-24" id="seat-24" r="7.1" cy="67.2" cx="104.6" />,
<circle key="seat-25" id="seat-25" r="7.1" cy="94.3" cx="121.6" />,
<circle key="seat-26" id="seat-26" r="7.1" cy="124.3" cx="141.1" />,
<circle key="seat-27" id="seat-27" r="7.1" cy="32.7" cx="90.8" />,
<circle key="seat-28" id="seat-28" r="7.1" cy="58.3" cx="120.9" />,
<circle key="seat-29" id="seat-29" r="7.1" cy="84.9" cx="139.1" />,
<circle key="seat-30" id="seat-30" r="7.1" cy="116.4" cx="157.2" />,
<circle key="seat-31" id="seat-31" r="7.1" cy="24.6" cx="109.5" />,
<circle key="seat-32" id="seat-32" r="7.1" cy="52.2" cx="138.6" />,
<circle key="seat-33" id="seat-33" r="7.1" cy="79.5" cx="157.8" />,
<circle key="seat-34" id="seat-34" r="7.1" cy="17.9" cx="128.8" />,
<circle key="seat-35" id="seat-35" r="7.1" cy="15.2" cx="147.7" />,
<circle key="seat-36" id="seat-36" r="7.1" cy="48.3" cx="156.9" />,
<circle key="seat-37" id="seat-37" r="7.1" cy="15.2" cx="192.5" />,
<circle key="seat-38" id="seat-38" r="7.1" cy="48.3" cx="183.3" />,
<circle key="seat-39" id="seat-39" r="7.1" cy="79.5" cx="182.4" />,
<circle key="seat-40" id="seat-40" r="7.1" cy="115.8" cx="182.2" />,
<circle key="seat-41" id="seat-41" r="7.1" cy="17.9" cx="211.4" />,
<circle key="seat-42" id="seat-42" r="7.1" cy="52.2" cx="201.6" />,
<circle key="seat-43" id="seat-43" r="7.1" cy="24.6" cx="230.7" />,
<circle key="seat-44" id="seat-44" r="7.1" cy="58.3" cx="219.3" />,
<circle key="seat-45" id="seat-45" r="7.1" cy="84.9" cx="201.1" />,
<circle key="seat-46" id="seat-46" r="7.1" cy="32.7" cx="249.4" />,
<circle key="seat-47" id="seat-47" r="7.1" cy="67.2" cx="235.6" />,
<circle key="seat-48" id="seat-48" r="7.1" cy="94.3" cx="218.6" />,
<circle key="seat-49" id="seat-49" r="7.1" cy="124.3" cx="199.1" />,
<circle key="seat-50" id="seat-50" r="7.1" cy="45.3" cx="267" />,
<circle key="seat-51" id="seat-51" r="7.1" cy="59.8" cx="282.3" />,
<circle key="seat-52" id="seat-52" r="7.1" cy="78.6" cx="250.7" />,
<circle key="seat-53" id="seat-53" r="7.1" cy="106.5" cx="234" />,
<circle key="seat-54" id="seat-54" r="7.1" cy="136.2" cx="212" />,
<circle key="seat-55" id="seat-55" r="7.1" cy="75.5" cx="294.9" />,
<circle key="seat-56" id="seat-56" r="7.1" cy="91.3" cx="264.5" />,
<circle key="seat-57" id="seat-57" r="7.1" cy="91.5" cx="306" />,
<circle key="seat-58" id="seat-58" r="7.1" cy="105.7" cx="275.4" />,
<circle key="seat-59" id="seat-59" r="7.1" cy="122.5" cx="247.3" />,
<circle key="seat-60" id="seat-60" r="7.1" cy="109" cx="313.5" />,
<circle key="seat-61" id="seat-61" r="7.1" cy="121.3" cx="283" />,
<circle key="seat-62" id="seat-62" r="7.1" cy="138.8" cx="254.7" />,
<circle key="seat-63" id="seat-63" r="7.1" cy="151.9" cx="219.3" />,
<circle key="seat-64" id="seat-64" r="7.1" cy="128.4" cx="317.4" />,
<circle key="seat-65" id="seat-65" r="7.1" cy="137.9" cx="287" />,
<circle key="seat-66" id="seat-66" r="7.1" cy="156.3" cx="256.8" />,
<circle key="seat-67" id="seat-67" r="7.1" cy="169.9" cx="219.3" />,
<circle key="seat-68" id="seat-68" r="7.1" cy="147.4" cx="318.7" />,
<circle key="seat-69" id="seat-69" r="7.1" cy="155.2" cx="288.4" />,
<circle key="seat-70" id="seat-70" r="7.1" cy="166" cx="318.4" />,
<circle key="seat-71" id="seat-71" r="7.1" cy="172" cx="288.7" />,
<circle key="seat-72" id="seat-72" r="7.1" cy="174.5" cx="257.5" />,
];
let seatIndex = 1; // Empezamos desde 1 porque el 0 es para el presidente
return (
<svg viewBox="0 0 340.2 220.5" width={size} height={size * (220.5 / 340.2)} style={{ display: 'block', margin: 'auto' }}>
<g>
{/* Renderizamos primero el escaño del presidente por separado */}
{presidenteBancada && React.cloneElement(seatElements[PRESIDENTE_SEAT_INDEX], {
fill: presidenteBancada.color || '#A9A9A9',
strokeWidth: 0.5,
})}
{/* Mapeamos los partidos para crear los bloques */}
{partyData.map(partido => {
const partySeats = [
...Array(partido.bancasFijos).fill({ isNew: false }),
...Array(partido.bancasGanadas).fill({ isNew: true })
];
return (
<g
key={partido.id}
className="party-block"
data-tooltip-id="party-tooltip"
data-tooltip-content={`${partido.nombreCorto || partido.nombre}: ${partido.bancasTotales} bancas`}
>
{partySeats.map((seatInfo, i) => {
if (seatIndex >= seatElements.length) return null;
const template = seatElements[seatIndex];
seatIndex++;
return React.cloneElement(template, {
key: `${partido.id}-${i}`,
className: 'seat-circle',
fill: partido.color || '#808080',
fillOpacity: seatInfo.isNew ? 1 : 0.3,
stroke: partido.color || '#808080',
strokeWidth: 0.5,
});
})}
</g>
);
})}
{/* Renderizamos escaños vacíos si sobran */}
{seatIndex < seatElements.length &&
seatElements.slice(seatIndex).map((template, i) =>
React.cloneElement(template, {
key: `empty-${i}`,
fill: '#E0E0E0',
stroke: '#ffffff',
strokeWidth: 0.5
})
)
}
</g>
</svg>
);
};

View File

@@ -1,4 +1,4 @@
// src/components/common/imageFallback.ts
// src/components/imageFallback.ts
export function handleImageFallback(selector: string, fallbackImageUrl: string) {
// Le decimos a TypeScript que el resultado será una lista de elementos de imagen HTML

View File

@@ -1,147 +0,0 @@
// src/features/legislativas/nacionales/DevAppLegislativas.tsx
import { useState } from 'react'; // <-- Importar useState
import { ResultadosNacionalesCardsWidget } from './nacionales/ResultadosNacionalesCardsWidget';
import { CongresoNacionalWidget } from './nacionales/CongresoNacionalWidget';
import { PanelNacionalWidget } from './nacionales/PanelNacionalWidget';
import { HomeCarouselWidget } from './nacionales/HomeCarouselWidget';
import './DevAppStyle.css'
// --- NUEVO COMPONENTE REUTILIZABLE PARA CONTENIDO COLAPSABLE ---
const CollapsibleWidgetWrapper = ({ children }: { children: React.ReactNode }) => {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div className="collapsible-container">
<div className={`collapsible-content ${isExpanded ? 'expanded' : ''}`}>
{children}
</div>
<button className="toggle-button" onClick={() => setIsExpanded(!isExpanded)}>
{isExpanded ? 'Mostrar Menos' : 'Mostrar Más'}
</button>
</div>
);
};
export const DevAppLegislativas = () => {
// Estilos para los separadores y descripciones para mejorar la legibilidad
const sectionStyle = {
border: '2px solid #007bff',
borderRadius: '8px',
padding: '1rem 2rem',
marginTop: '3rem',
marginBottom: '3rem',
backgroundColor: '#f8f9fa'
};
const descriptionStyle = {
fontFamily: 'sans-serif',
color: '#333',
lineHeight: 1.6
};
const codeStyle = {
backgroundColor: '#e9ecef',
padding: '2px 6px',
borderRadius: '4px',
fontFamily: 'Roboto'
};
return (
<div className="container">
<h1>Visor de Widgets</h1>
<div style={sectionStyle}>
<h2>Widget: Carrusel de Resultados (Home)</h2>
<p style={descriptionStyle}>
Uso: <code style={codeStyle}>&lt;HomeCarouselWidget eleccionId={2} distritoId="02" categoriaId={2} titulo="Diputados - Provincia de Buenos Aires" /&gt;</code>
</p>
<HomeCarouselWidget
eleccionId={2} // Nacional
distritoId="02" // Buenos Aires
categoriaId={2} // Diputados Nacionales
titulo="Diputados - Provincia de Buenos Aires"
/>
</div>
{/* --- SECCIÓN PARA EL WIDGET DE TARJETAS CON EJEMPLOS --- */}
<div style={sectionStyle}>
<h2>Widget: Resultados por Provincia (Tarjetas)</h2>
<hr />
<h3 style={{ marginTop: '2rem' }}>1. Vista por Defecto</h3>
<p style={descriptionStyle}>
Sin parámetros adicionales. Muestra todas las provincias, con sus categorías correspondientes (Diputados para las 24, Senadores para las 8 que renuevan). Muestra los 2 principales partidos por defecto.
<br />
Uso: <code style={codeStyle}>&lt;ResultadosNacionalesCardsWidget eleccionId={2} /&gt;</code>
</p>
<CollapsibleWidgetWrapper>
<ResultadosNacionalesCardsWidget eleccionId={2} />
</CollapsibleWidgetWrapper>
<hr style={{ marginTop: '2rem' }} />
<h3 style={{ marginTop: '2rem' }}>2. Filtrado por Provincia (focoDistritoId)</h3>
<p style={descriptionStyle}>
Muestra únicamente la tarjeta de una provincia específica. Ideal para páginas de noticias locales. El ID de distrito ("02" para Bs. As., "06" para Chaco) se pasa como prop.
<br />
Ejemplo Buenos Aires: <code style={codeStyle}>&lt;ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="02" /&gt;</code>
</p>
<ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="02" />
<p style={{ ...descriptionStyle, marginTop: '2rem' }}>
Ejemplo Chaco (que también renueva Senadores): <code style={codeStyle}>&lt;ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="06" /&gt;</code>
</p>
<ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="06" />
<hr style={{ marginTop: '2rem' }} />
<h3 style={{ marginTop: '2rem' }}>3. Filtrado por Categoría (focoCategoriaId)</h3>
<p style={descriptionStyle}>
Muestra todas las provincias que votan para una categoría específica.
<br />
Ejemplo Senadores (ID 1): <code style={codeStyle}>&lt;ResultadosNacionalesCardsWidget eleccionId={2} focoCategoriaId={1} /&gt;</code>
</p>
<ResultadosNacionalesCardsWidget eleccionId={2} focoCategoriaId={1} />
<hr style={{ marginTop: '2rem' }} />
<h3 style={{ marginTop: '2rem' }}>4. Indicando Cantidad de Resultados (cantidadResultados)</h3>
<p style={descriptionStyle}>
Controla cuántos partidos se muestran en cada categoría. Por defecto son 2.
<br />
Ejemplo mostrando el TOP 3 de cada categoría: <code style={codeStyle}>&lt;ResultadosNacionalesCardsWidget eleccionId={2} cantidadResultados={3} /&gt;</code>
</p>
<CollapsibleWidgetWrapper>
<ResultadosNacionalesCardsWidget eleccionId={2} cantidadResultados={3} />
</CollapsibleWidgetWrapper>
<hr style={{ marginTop: '2rem' }} />
<h3 style={{ marginTop: '2rem' }}>5. Mostrando las Bancas (mostrarBancas)</h3>
<p style={descriptionStyle}>
Útil para contextos donde importan las bancas. La prop <code style={codeStyle}>mostrarBancas</code> se establece en <code style={codeStyle}>true</code>.
<br />
Ejemplo en Tierra del Fuego: <code style={codeStyle}>&lt;ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="23" mostrarBancas={true} /&gt;</code>
</p>
<ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="23" mostrarBancas={true} />
<hr style={{ marginTop: '2rem' }} />
<h3 style={{ marginTop: '2rem' }}>6. Combinación de Parámetros</h3>
<p style={descriptionStyle}>
Se pueden combinar todos los parámetros para vistas muy específicas.
<br />
Ejemplo: Mostrar el TOP 1 (el ganador) para la categoría de SENADORES en la provincia de RÍO NEGRO (Distrito ID "16").
<br />
Uso: <code style={codeStyle}>&lt;ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="16" focoCategoriaId={1} cantidadResultados={1} /&gt;</code>
</p>
<ResultadosNacionalesCardsWidget eleccionId={2} focoDistritoId="16" focoCategoriaId={1} cantidadResultados={1} />
</div>
{/* --- OTROS WIDGETS --- */}
<CongresoNacionalWidget eleccionId={2} />
<PanelNacionalWidget eleccionId={2} />
</div>
);
};

View File

@@ -1,50 +0,0 @@
.container{
text-align: center;
}
/* --- ESTILOS PARA EL CONTENEDOR COLAPSABLE --- */
.collapsible-container {
position: relative;
padding-bottom: 50px; /* Espacio para el botón de expandir */
}
.collapsible-content {
max-height: 950px; /* Altura suficiente para 2 filas de tarjetas (aprox) */
overflow: hidden;
transition: max-height 0.7s ease-in-out;
position: relative;
}
.collapsible-content.expanded {
max-height: 100%; /* Un valor grande para asegurar que todo el contenido sea visible */
}
/* Pseudo-elemento para crear un degradado y sugerir que hay más contenido */
.collapsible-content:not(.expanded)::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 150px;
background: linear-gradient(to top, rgba(248, 249, 250, 1) 20%, rgba(248, 249, 250, 0));
pointer-events: none; /* Permite hacer clic a través del degradado */
}
.toggle-button {
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
font-size: 1rem;
font-weight: bold;
color: #fff;
background-color: #007bff;
border: none;
border-radius: 20px;
cursor: pointer;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
z-index: 2;
}

View File

@@ -1,162 +0,0 @@
// src/features/legislativas/nacionales/CongresoNacionalWidget.tsx
import { useState, Suspense, useMemo } from 'react';
import { useSuspenseQuery } from '@tanstack/react-query';
import { Tooltip } from 'react-tooltip';
import { DiputadosNacionalesLayout } from '../../../components/common/DiputadosNacionalesLayout';
import { SenadoresNacionalesLayout } from '../../../components/common/SenadoresNacionalesLayout';
import { getComposicionNacional, type ComposicionNacionalData, type PartidoComposicionNacional } from '../../../apiService';
import '../provinciales/CongresoWidget.css';
interface CongresoNacionalWidgetProps {
eleccionId: number;
}
const formatTimestamp = (dateString: string) => {
if (!dateString) return '...';
const date = new Date(dateString);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${day}/${month}/${year} ${hours}:${minutes}`;
};
const WidgetContent = ({ eleccionId }: CongresoNacionalWidgetProps) => {
const [camaraActiva, setCamaraActiva] = useState<'diputados' | 'senadores'>('diputados');
const [isHovering, setIsHovering] = useState(false);
const { data } = useSuspenseQuery<ComposicionNacionalData>({
queryKey: ['composicionNacional', eleccionId],
queryFn: () => getComposicionNacional(eleccionId),
refetchInterval: 30000,
});
const datosCamaraActual = data[camaraActiva];
const partidosOrdenados = useMemo(() => {
if (!datosCamaraActual?.partidos) return [];
const partidosACopiar = [...datosCamaraActual.partidos];
partidosACopiar.sort((a, b) => {
const ordenA = camaraActiva === 'diputados' ? a.ordenDiputadosNacionales : a.ordenSenadoresNacionales;
const ordenB = camaraActiva === 'diputados' ? b.ordenDiputadosNacionales : b.ordenSenadoresNacionales;
return (ordenA ?? 999) - (ordenB ?? 999);
});
return partidosACopiar;
}, [datosCamaraActual, camaraActiva]);
const partyDataParaLayout = useMemo(() => {
if (camaraActiva === 'senadores') return partidosOrdenados;
if (!partidosOrdenados || !datosCamaraActual.presidenteBancada?.color) return partidosOrdenados;
const partidoPresidente = partidosOrdenados.find(p => p.color === datosCamaraActual.presidenteBancada!.color);
if (!partidoPresidente) return partidosOrdenados;
const adjustedPartyData = JSON.parse(JSON.stringify(partidosOrdenados));
const partidoAjustar = adjustedPartyData.find((p: PartidoComposicionNacional) => p.id === partidoPresidente.id);
if (partidoAjustar) {
const tipoBanca = datosCamaraActual.presidenteBancada.tipoBanca;
if (tipoBanca === 'ganada' && partidoAjustar.bancasGanadas > 0) {
partidoAjustar.bancasGanadas -= 1;
} else if (tipoBanca === 'previa' && partidoAjustar.bancasFijos > 0) {
partidoAjustar.bancasFijos -= 1;
} else {
if (partidoAjustar.bancasGanadas > 0) {
partidoAjustar.bancasGanadas -= 1;
} else if (partidoAjustar.bancasFijos > 0) {
partidoAjustar.bancasFijos -= 1;
}
}
}
return adjustedPartyData;
}, [partidosOrdenados, datosCamaraActual.presidenteBancada, camaraActiva]);
return (
<div className="congreso-container">
<div className="congreso-grafico">
<div
className={`congreso-hemiciclo-wrapper ${isHovering ? 'is-hovering' : ''}`}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
{camaraActiva === 'diputados' ?
<DiputadosNacionalesLayout
partyData={partyDataParaLayout}
presidenteBancada={datosCamaraActual.presidenteBancada || null}
size={700}
/> :
<SenadoresNacionalesLayout
partyData={partyDataParaLayout}
presidenteBancada={datosCamaraActual.presidenteBancada || null}
size={700}
/>
}
</div>
<div className="congreso-footer">
<div className="footer-legend">
<div className="footer-legend-item">
{/* Usamos la nueva clase CSS para el círculo sólido */}
<span className="legend-icon legend-icon--solid"></span>
<span>Bancas en juego</span>
</div>
<div className="footer-legend-item">
{/* Reemplazamos el SVG por un span con la nueva clase para el anillo */}
<span className="legend-icon legend-icon--ring"></span>
<span>Bancas previas</span>
</div>
</div>
<div className="footer-timestamp">
Última Actualización: {formatTimestamp(datosCamaraActual.ultimaActualizacion)}
</div>
</div>
</div>
<div className="congreso-summary">
<div className="chamber-tabs">
<button className={camaraActiva === 'diputados' ? 'active' : ''} onClick={() => setCamaraActiva('diputados')}>
Diputados
</button>
<button className={camaraActiva === 'senadores' ? 'active' : ''} onClick={() => setCamaraActiva('senadores')}>
Senadores
</button>
</div>
<h3>{datosCamaraActual.camaraNombre}</h3>
<div className="summary-metric">
<span>Total de Bancas</span>
<strong>{datosCamaraActual.totalBancas}</strong>
</div>
<div className="summary-metric">
<span>Bancas en Juego</span>
<strong>{datosCamaraActual.bancasEnJuego}</strong>
</div>
<hr />
<div className="partido-lista-container">
<ul className="partido-lista">
{partidosOrdenados
.filter(p => p.bancasTotales > 0)
.map((partido: PartidoComposicionNacional) => (
<li key={partido.id}>
<span className="partido-color-box" style={{ backgroundColor: partido.color || '#808080' }}></span>
<span className="partido-nombre">{partido.nombreCorto || partido.nombre}</span>
<strong
className="partido-bancas"
title={`${partido.bancasFijos} bancas previas + ${partido.bancasGanadas} ganadas`}
>
{partido.bancasTotales}
</strong>
</li>
))}
</ul>
</div>
</div>
<Tooltip id="party-tooltip" />
</div>
);
};
export const CongresoNacionalWidget = ({ eleccionId }: CongresoNacionalWidgetProps) => {
return (
<Suspense fallback={<div className="congreso-container loading" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '400px' }}>Cargando composición del congreso...</div>}>
<WidgetContent eleccionId={eleccionId} />
</Suspense>
);
};

Some files were not shown because too many files have changed in this diff Show More