Files
Elecciones-2025/Elecciones-Web/frontend/src/components/TelegramaWidget.tsx

189 lines
7.5 KiB
TypeScript
Raw Normal View History

// src/components/TelegramaWidget.tsx
2025-09-08 12:17:22 -03:00
import { useState, useEffect, useMemo } from 'react';
import Select, { type FilterOptionOption } from 'react-select'; // <-- Importar react-select
2025-08-25 15:04:09 -03:00
import {
getSecciones,
getMunicipiosPorSeccion,
2025-09-08 12:17:22 -03:00
getEstablecimientosPorMunicipio, // <-- Nueva función
2025-08-25 15:04:09 -03:00
getMesasPorEstablecimiento,
2025-09-08 12:17:22 -03:00
getTelegramaPorId,
assetBaseUrl
} from '../apiService';
import type { TelegramaData, CatalogoItem } from '../types/types';
import './TelegramaWidget.css';
2025-08-25 15:04:09 -03:00
import { pdfjs, Document, Page } from 'react-pdf';
import 'react-pdf/dist/Page/AnnotationLayer.css';
import 'react-pdf/dist/Page/TextLayer.css';
2025-09-08 12:17:22 -03:00
pdfjs.GlobalWorkerOptions.workerSrc = `${assetBaseUrl}/pdf.worker.min.mjs`;
// Estilos para los selectores
const customSelectStyles = {
control: (base: any) => ({ ...base, marginBottom: '1rem' }),
menu: (base: any) => ({ ...base, zIndex: 10 }),
};
// --- FUNCIÓN DE FILTRO "SMART SEARCH" ---
const smartSearchFilter = (
option: FilterOptionOption<{ label: string; value: string }>,
inputValue: string
) => {
// 1. Si no hay entrada de búsqueda, muestra todas las opciones.
if (!inputValue) {
return true;
}
// 2. Normalizamos tanto la etiqueta de la opción como la entrada del usuario:
// - a minúsculas
// - quitamos los acentos (si fuera necesario, aunque aquí no tanto)
const normalizedLabel = option.label.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
const normalizedInput = inputValue.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
// 3. Dividimos la entrada del usuario en palabras individuales.
const searchTerms = normalizedInput.split(' ').filter(term => term.length > 0);
// 4. La opción es válida si CADA TÉRMINO de búsqueda está incluido en la etiqueta.
return searchTerms.every(term => normalizedLabel.includes(term));
};
2025-08-25 15:04:09 -03:00
export const TelegramaWidget = () => {
2025-09-08 12:17:22 -03:00
// Estados para los datos de los dropdowns
const [secciones, setSecciones] = useState<CatalogoItem[]>([]);
const [municipios, setMunicipios] = useState<CatalogoItem[]>([]);
const [establecimientos, setEstablecimientos] = useState<CatalogoItem[]>([]);
const [mesas, setMesas] = useState<CatalogoItem[]>([]);
2025-09-08 12:17:22 -03:00
// Estados para los valores seleccionados (adaptados para react-select)
const [selectedSeccion, setSelectedSeccion] = useState<{ value: string; label: string } | null>(null);
const [selectedMunicipio, setSelectedMunicipio] = useState<{ value: string; label: string } | null>(null);
const [selectedEstablecimiento, setSelectedEstablecimiento] = useState<{ value: string; label: string } | null>(null);
const [selectedMesa, setSelectedMesa] = useState<{ value: string; label: string } | null>(null);
2025-08-25 15:04:09 -03:00
2025-09-08 12:17:22 -03:00
// Estados para la visualización del telegrama (sin cambios)
const [telegrama, setTelegrama] = useState<TelegramaData | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
2025-09-08 12:17:22 -03:00
// Cargar secciones iniciales y aplicar orden
useEffect(() => {
2025-09-08 12:17:22 -03:00
getSecciones().then(seccionesData => {
const orden = new Map([
['Capital', 0], ['Primera', 1], ['Segunda', 2], ['Tercera', 3],
['Cuarta', 4], ['Quinta', 5], ['Sexta', 6], ['Séptima', 7]
]);
const getOrden = (nombre: string) => {
const match = nombre.match(/Capital|Primera|Segunda|Tercera|Cuarta|Quinta|Sexta|Séptima/);
return match ? orden.get(match[0]) ?? 99 : 99;
};
seccionesData.sort((a, b) => getOrden(a.nombre) - getOrden(b.nombre));
setSecciones(seccionesData);
});
}, []);
// Cargar municipios cuando cambia la sección
useEffect(() => {
if (selectedSeccion) {
2025-09-08 12:17:22 -03:00
setMunicipios([]); setEstablecimientos([]); setMesas([]);
setSelectedMunicipio(null); setSelectedEstablecimiento(null); setSelectedMesa(null);
getMunicipiosPorSeccion(selectedSeccion.value).then(setMunicipios);
}
}, [selectedSeccion]);
2025-08-25 15:04:09 -03:00
2025-09-08 12:17:22 -03:00
// Cargar establecimientos cuando cambia el municipio (SIN pasar por circuito)
useEffect(() => {
if (selectedMunicipio) {
setEstablecimientos([]); setMesas([]);
2025-09-08 12:17:22 -03:00
setSelectedEstablecimiento(null); setSelectedMesa(null);
getEstablecimientosPorMunicipio(selectedMunicipio.value).then(setEstablecimientos);
}
2025-09-08 12:17:22 -03:00
}, [selectedMunicipio]);
2025-09-08 12:17:22 -03:00
// Cargar mesas cuando cambia el establecimiento
useEffect(() => {
if (selectedEstablecimiento) {
setMesas([]);
2025-09-08 12:17:22 -03:00
setSelectedMesa(null);
getMesasPorEstablecimiento(selectedEstablecimiento.value).then(setMesas);
}
}, [selectedEstablecimiento]);
// Buscar el telegrama cuando se selecciona una mesa
useEffect(() => {
if (selectedMesa) {
2025-09-08 12:17:22 -03:00
setLoading(true); setError(null); setTelegrama(null);
getTelegramaPorId(selectedMesa.value)
.then(setTelegrama)
2025-09-02 20:34:49 -03:00
.catch(() => setError(`El telegrama para la mesa seleccionada, aún no se cargó en el sistema.`))
.finally(() => setLoading(false));
}
}, [selectedMesa]);
2025-09-08 12:17:22 -03:00
// Formateo de opciones para react-select
const seccionOptions = useMemo(() => secciones.map(s => ({ value: s.id, label: s.nombre })), [secciones]);
const municipioOptions = useMemo(() => municipios.map(m => ({ value: m.id, label: m.nombre })), [municipios]);
const establecimientoOptions = useMemo(() => establecimientos.map(e => ({ value: e.id, label: e.nombre })), [establecimientos]);
const mesaOptions = useMemo(() => mesas.map(m => ({ value: m.id, label: m.nombre })), [mesas]);
return (
<div className="telegrama-container">
<h4>Consulta de Telegramas por Ubicación</h4>
<div className="filters-grid">
2025-09-08 12:17:22 -03:00
<Select
options={seccionOptions}
value={selectedSeccion}
onChange={setSelectedSeccion}
placeholder="1. Sección..."
styles={customSelectStyles}
/>
<Select
options={municipioOptions}
value={selectedMunicipio}
onChange={setSelectedMunicipio}
placeholder="2. Municipio..."
isDisabled={!selectedSeccion}
styles={customSelectStyles}
/>
<Select
options={establecimientoOptions}
value={selectedEstablecimiento}
onChange={setSelectedEstablecimiento}
placeholder="3. Establecimiento..."
isDisabled={!selectedMunicipio}
styles={customSelectStyles}
filterOption={smartSearchFilter}
isSearchable // Habilitamos la búsqueda
/>
<Select
options={mesaOptions}
value={selectedMesa}
onChange={setSelectedMesa}
placeholder="4. Mesa..."
isDisabled={!selectedEstablecimiento}
styles={customSelectStyles}
/>
</div>
<div className="telegrama-viewer">
{loading && <div className="spinner"></div>}
{error && <p className="message error">{error}</p>}
2025-08-25 15:04:09 -03:00
{telegrama && (
<div className="telegrama-content">
2025-08-25 15:04:09 -03:00
<div className="telegrama-pdf-viewer">
<Document
file={`data:application/pdf;base64,${telegrama.contenidoBase64}`}
onLoadError={(error) => setError(`Error al cargar el PDF: ${error.message}`)}
loading={<div className="spinner"></div>}
>
<Page pageNumber={1} renderTextLayer={false} renderAnnotationLayer={false} />
</Document>
</div>
</div>
)}
2025-08-25 12:43:28 -03:00
{!loading && !telegrama && !error && <p className="message">Seleccione una mesa para visualizar el telegrama.</p>}
</div>
</div>
);
};