Feat Telegramas Busquedas
This commit is contained in:
		| @@ -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