Feat Telegramas Busquedas
This commit is contained in:
@@ -208,4 +208,9 @@ export const getRankingResultadosPorSeccion = async (seccionId: string): Promise
|
||||
export const getRankingMunicipiosPorSeccion = async (seccionId: string): Promise<ApiResponseRankingMunicipio> => {
|
||||
const { data } = await apiClient.get(`/resultados/ranking-municipios-por-seccion/${seccionId}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getEstablecimientosPorMunicipio = async (municipioId: string): Promise<CatalogoItem[]> => {
|
||||
const response = await apiClient.get(`/catalogos/establecimientos-por-municipio/${municipioId}`);
|
||||
return response.data;
|
||||
};
|
||||
@@ -1,12 +1,13 @@
|
||||
// src/components/TelegramaWidget.tsx
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import Select, { type FilterOptionOption } from 'react-select'; // <-- Importar react-select
|
||||
import {
|
||||
getSecciones,
|
||||
getMunicipiosPorSeccion,
|
||||
getCircuitosPorMunicipio,
|
||||
getEstablecimientosPorCircuito,
|
||||
getEstablecimientosPorMunicipio, // <-- Nueva función
|
||||
getMesasPorEstablecimiento,
|
||||
getTelegramaPorId
|
||||
getTelegramaPorId,
|
||||
assetBaseUrl
|
||||
} from '../apiService';
|
||||
import type { TelegramaData, CatalogoItem } from '../types/types';
|
||||
import './TelegramaWidget.css';
|
||||
@@ -15,103 +16,152 @@ import { pdfjs, Document, Page } from 'react-pdf';
|
||||
import 'react-pdf/dist/Page/AnnotationLayer.css';
|
||||
import 'react-pdf/dist/Page/TextLayer.css';
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.mjs';
|
||||
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));
|
||||
};
|
||||
|
||||
export const TelegramaWidget = () => {
|
||||
// Estados para los filtros geográficos
|
||||
// Estados para los datos de los dropdowns
|
||||
const [secciones, setSecciones] = useState<CatalogoItem[]>([]);
|
||||
const [municipios, setMunicipios] = useState<CatalogoItem[]>([]);
|
||||
const [circuitos, setCircuitos] = useState<CatalogoItem[]>([]);
|
||||
const [establecimientos, setEstablecimientos] = useState<CatalogoItem[]>([]);
|
||||
const [mesas, setMesas] = useState<CatalogoItem[]>([]);
|
||||
|
||||
// Estados para los valores seleccionados
|
||||
const [selectedSeccion, setSelectedSeccion] = useState('');
|
||||
const [selectedMunicipio, setSelectedMunicipio] = useState('');
|
||||
const [selectedCircuito, setSelectedCircuito] = useState('');
|
||||
const [selectedEstablecimiento, setSelectedEstablecimiento] = useState('');
|
||||
const [selectedMesa, setSelectedMesa] = useState('');
|
||||
// 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);
|
||||
|
||||
// Estados para la visualización del telegrama
|
||||
// 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);
|
||||
|
||||
// Cargar secciones iniciales
|
||||
// Cargar secciones iniciales y aplicar orden
|
||||
useEffect(() => {
|
||||
getSecciones().then(setSecciones);
|
||||
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) {
|
||||
setMunicipios([]); setCircuitos([]); setEstablecimientos([]); setMesas([]);
|
||||
setSelectedMunicipio(''); setSelectedCircuito(''); setSelectedEstablecimiento(''); setSelectedMesa('');
|
||||
getMunicipiosPorSeccion(selectedSeccion).then(setMunicipios);
|
||||
setMunicipios([]); setEstablecimientos([]); setMesas([]);
|
||||
setSelectedMunicipio(null); setSelectedEstablecimiento(null); setSelectedMesa(null);
|
||||
getMunicipiosPorSeccion(selectedSeccion.value).then(setMunicipios);
|
||||
}
|
||||
}, [selectedSeccion]);
|
||||
|
||||
// Cargar establecimientos cuando cambia el municipio (SIN pasar por circuito)
|
||||
useEffect(() => {
|
||||
if (selectedMunicipio) {
|
||||
setCircuitos([]); setEstablecimientos([]); setMesas([]);
|
||||
setSelectedCircuito(''); setSelectedEstablecimiento(''); setSelectedMesa('');
|
||||
getCircuitosPorMunicipio(selectedMunicipio).then(setCircuitos);
|
||||
setEstablecimientos([]); setMesas([]);
|
||||
setSelectedEstablecimiento(null); setSelectedMesa(null);
|
||||
getEstablecimientosPorMunicipio(selectedMunicipio.value).then(setEstablecimientos);
|
||||
}
|
||||
}, [selectedMunicipio]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedCircuito) {
|
||||
setEstablecimientos([]); setMesas([]);
|
||||
setSelectedEstablecimiento(''); setSelectedMesa('');
|
||||
getEstablecimientosPorCircuito(selectedCircuito).then(setEstablecimientos);
|
||||
}
|
||||
}, [selectedCircuito]);
|
||||
|
||||
// Cargar mesas cuando cambia el establecimiento
|
||||
useEffect(() => {
|
||||
if (selectedEstablecimiento) {
|
||||
setMesas([]);
|
||||
setSelectedMesa('');
|
||||
getMesasPorEstablecimiento(selectedEstablecimiento).then(setMesas);
|
||||
setSelectedMesa(null);
|
||||
getMesasPorEstablecimiento(selectedEstablecimiento.value).then(setMesas);
|
||||
}
|
||||
}, [selectedEstablecimiento]);
|
||||
|
||||
// Buscar el telegrama cuando se selecciona una mesa
|
||||
useEffect(() => {
|
||||
if (selectedMesa) {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setTelegrama(null);
|
||||
getTelegramaPorId(selectedMesa)
|
||||
setLoading(true); setError(null); setTelegrama(null);
|
||||
getTelegramaPorId(selectedMesa.value)
|
||||
.then(setTelegrama)
|
||||
.catch(() => setError(`El telegrama para la mesa seleccionada, aún no se cargó en el sistema.`))
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
}, [selectedMesa]);
|
||||
|
||||
|
||||
// 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">
|
||||
<select value={selectedSeccion} onChange={e => setSelectedSeccion(e.target.value)}>
|
||||
<option value="">1. Sección</option>
|
||||
{secciones.map(s => <option key={s.id} value={s.id}>{s.nombre}</option>)}
|
||||
</select>
|
||||
<select value={selectedMunicipio} onChange={e => setSelectedMunicipio(e.target.value)} disabled={!municipios.length}>
|
||||
<option value="">2. Municipio</option>
|
||||
{municipios.map(m => <option key={m.id} value={m.id}>{m.nombre}</option>)}
|
||||
</select>
|
||||
<select value={selectedCircuito} onChange={e => setSelectedCircuito(e.target.value)} disabled={!circuitos.length}>
|
||||
<option value="">3. Circuito</option>
|
||||
{circuitos.map(c => <option key={c.id} value={c.id}>{c.nombre}</option>)}
|
||||
</select>
|
||||
<select value={selectedEstablecimiento} onChange={e => setSelectedEstablecimiento(e.target.value)} disabled={!establecimientos.length}>
|
||||
<option value="">4. Establecimiento</option>
|
||||
{establecimientos.map(e => <option key={e.id} value={e.id}>{e.nombre}</option>)}
|
||||
</select>
|
||||
<select value={selectedMesa} onChange={e => setSelectedMesa(e.target.value)} disabled={!mesas.length}>
|
||||
<option value="">5. Mesa</option>
|
||||
{mesas.map(m => <option key={m.id} value={m.id}>{m.nombre}</option>)}
|
||||
</select>
|
||||
<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">
|
||||
|
||||
Reference in New Issue
Block a user