diff --git a/public/organigrama_FES102025.svg b/public/organigrama_FES102025.svg new file mode 100644 index 0000000..c96cb35 --- /dev/null +++ b/public/organigrama_FES102025.svg @@ -0,0 +1 @@ +DIRECCIÓNDEPARTAMENTO DE LA OFICINA DE PRENSACOORDINACIÓN DE COMUNICACIÓN SOCIALCOORDINACIÓN DE GESTIÓNSECRETARÍA GENERALDIVISIÓN DE MATEMÁTICAS E INGENIERÍADIVISIÓN DE DISEÑO Y EDIFICACIÓNDIVISIÓN DE HUMANIDADESDIVISIÓN DE CIENCIAS Y SOCIOECONÓMICASDIVISIÓN DE CIENCIAS JURÍDICASDIVISIÓN DE SISTEMA DE SUAYEDDIVISIÓN DE SISTEMA DE SUAYEDSECRETARÍA AUXILIAR DE LA SECRETARÍA GENERALSECRETARÍA DE POSGRADO E INVESTIGACIÓNCOORDINACIÓN DE ESTUDIOS DE POSGRADOUNIDAD DE ADMINISTRACIÓN ESCOLARCOORDINACIÓN DEL CENTRO DE ENSEÑANZA DE IDIOMASUNIDAD DEL CENTRO DE INFORMACIÓN Y DOCUMENTACIÓNUNIDAD DEL CENTRO DE DESARROLLO TECNOLÓGICOUNIDAD DE SERVICIOS EDITORIALESUNIDAD DE TALLERES, LABORATORIOS Y AUDIOVISUALESCOORDINACIÓN DEL CENTRO TECNOLÓGICO PARA LA EDUCACIÓN A DISTANCIACOORDINACIÓN DEL CENTRO DE ESTUDIOS MUNICIPALES Y METROPOLITANOSCOORDINACIÓN DEL CENTRO DE DIFUSIÓN CULTURALCOORDINACIÓN DE ACTIVIDADES DEPORTIVAS Y RECREATIVASSECRETARÍA AUXILIAR DE EXTENSIÓN UNIVERSITARIA Y VINCULACIÓN INSTITUCIONALSECRETARÍA TÉCNICA DE EXTENSIÓN UNIVERSITARIA Y VINCULACIÓN INSTITUCIONALSECRETARÍA AUXILIAR DE LA SECRETARÍA DE ASUNTOS ACADÉMICOS ESTUDIANTILESDEPARTAMENTO DE MOVILIDAD ESTUDIANTIL, INTERCAMBIO, BECAS Y SERVICIOS DE SALUDSECRETARÍA AUXILIAR DE POSGRADO E INVESTIGACIÓNCOORDINACIÓN DE LA UNIDAD DE INVESTIGACIÓN MULTIDISCIPLINARIA APLICADASECRETARÍA DE ASUNTOS ACADÉMICOS ESTUDIANTILESSECRETARÍA DE EXTENSIÓN UNIVERSITARIA Y VINCULACIÓN INSTITUCIONALCOORDINACIÓN DE SERVICIOS ACADÉMICOSUNIDAD DE LA OFICINA JURÍDICAUNIDAD DE PLANEACIÓNCOORDINACIÓN DE CUERPOS COLEGIADOSSECRETARÍA ADMINISTRATIVAUNIDAD DE ADMINISTRACIÓN DE RECURSOSSUPERINTENDENCIA DE OBRAS Y MANTENIMIENTOCOORDINACIÓN DE PLANTA DE TRATAMIENTO DE AGUAS RESIDUALESUNIDAD DE SERVICIOS A LA COMUNIDADDEPARTAMENTO DE PROYECTOS ADMINISTRATIVOS \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index cbb8ec3..cbb4a4f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,8 +1,9 @@ 'use client' import React, { useState, useRef, useCallback } from 'react'; -import Image from 'next/image'; import BuscarFuncionario from '@/components/BuscarFuncionario'; +import OrganigramaInteractivo from '@/components/OrganigramaInteractivo'; +import axios from 'axios'; import { Funcionario } from '@/types/Funcionarios'; import organigrama from '@/public/organigrama_FES102025.png'; // Asegúrate de la ruta @@ -72,6 +73,24 @@ export default function Home() { setTranslate({ x: 0, y: 0 }); }; + // Buscar por área: realiza la misma petición que BuscarFuncionario cuando se selecciona un área + const buscarPorArea = async (areaId: number) => { + try { + const url = process.env.NEXT_PUBLIC_API_FUNCIONARIOS; + if (!url) throw new Error('Url no definida en las variables de entorno'); + const payload: any = { tipoBusqueda: 'area', IdUnidadResponsable: areaId }; + const response = await axios.post(url, payload); + if (Array.isArray(response.data) && response.data.length > 0) { + setResultados(response.data as Funcionario[]); + } else { + setResultados([]); + } + } catch (err) { + console.error('Error buscando por área:', err); + setResultados([]); + } + }; + return (
@@ -105,25 +124,20 @@ export default function Home() { }} >
-
-
- Organigrama +
+
+ buscarPorArea(id)} isPanningRef={isPanning as any} /> +
-
{/* Controls */}
diff --git a/src/components/OrganigramaInteractivo.tsx b/src/components/OrganigramaInteractivo.tsx new file mode 100644 index 0000000..10cc3e6 --- /dev/null +++ b/src/components/OrganigramaInteractivo.tsx @@ -0,0 +1,164 @@ +import React from 'react'; + +interface Props { + onAreaSelect: (areaId: number) => void; + className?: string; + style?: React.CSSProperties; + isPanningRef?: React.RefObject; +} + +// Map display substrings (lowercase) to the area id used in BuscarFuncionario +const areaMap: Record = { + 'coordinación de actividades deportivas y recreativas': 128, + 'coordinación de comunicación social': 122, + 'coordinación de cuerpos colegiados': 9, + 'coordinación de derecho del sistema de universidad abierta y educación a distancia': 126, + 'coordinación de especializaciones': 206, + 'coordinación de estudios de posgrado': 120, + 'coordinación de la unidad de investigación multidisciplinaria aplicada': 182, + 'coordinación de licel sistema de universidad abierta y educación a distancia': 174, + 'coordinación de maestrías y doctorados': 221, + 'coordinación de relaciones internacionales del sistema de universidad abierta y educación a distancia': 152, + 'coordinación de servicios académicos': 121, + 'coordinación del centro de difusión cultural': 125, + 'coordinación del centro de educación continua': 124, + 'coordinación del centro de enseñanza de idiomas': 129, + 'coordinación del centro de estudios municipales y metropolitanos': 214, + 'coordinación del centro tecnológico para la educación a distancia': 176, + 'dirección': 72, + 'división de ciencias jurídicas': 80, + 'división de ciencias socioeconómicas': 79, + 'división de diseño y edificación': 78, + 'división de humanidades': 77, + 'división de matemáticas e ingeniería': 76, + 'secretaría de asuntos académicos estudiantiles': 181, + 'secretaría de extensión universitaria y vinculación institucional': 177, + 'secretaría de posgrado e investigación': 119, + 'secretaría general': 8, + 'unidad de administración escolar': 6, + 'unidad de servicios editoriales': 2, + 'unidad de talleres, laboratorios y audiovisuales': 1, + 'unidad del centro de desarrollo tecnológico': 130, + 'unidad del centro de información y documentación': 123, + // add more mappings as needed +}; + +const svgContent = ` +${ + // Inline the inner content of the SVG (kept short for brevity, we will load the full SVG below) +''} +`; + +// Instead of inlining the full SVG string here (long), we'll import it via require at runtime by fetching the file relative to the app. +// But to keep this component self-contained, we'll render an fallback and then query inside it. + +export default function OrganigramaInteractivo({ onAreaSelect, className, style, isPanningRef }: Props) { + const containerRef = React.useRef(null); + + const normalize = (s: string) => + s + .normalize('NFD') + .replace(/\p{Diacritic}/gu, '') + .toLowerCase() + .replace(/\s+/g, ' ') + .trim(); + + const normalizedAreaEntries = React.useMemo(() => { + return Object.keys(areaMap).map((k) => ({ key: normalize(k), id: areaMap[k] })); + }, []); + + React.useEffect(() => { + const container = containerRef.current; + if (!container) return; + + // Fetch the SVG file from the public path. The SVG is located at /organigrama_FES102025.svg (if placed in public/) + // There's a copy in src/public; to be served statically, ensure the SVG is available at '/organigrama_FES102025.svg'. + let mounted = true; + fetch('/organigrama_FES102025.svg') + .then((r) => r.text()) + .then((text) => { + if (!mounted) return; + container.innerHTML = text; + + const svg = container.querySelector('svg'); + if (!svg) return; + + const onClick = (e: MouseEvent) => { + // if parent reports it's panning/dragging, ignore clicks + if (isPanningRef && isPanningRef.current) return; + const target = e.target as Element; + const clientX = (e as MouseEvent).clientX; + const clientY = (e as MouseEvent).clientY; + + // Try to find a text element under the click + const texts = Array.from(svg.querySelectorAll('text')) as Element[]; + + // First check which text elements have bounding rect containing the click + const hit = texts.find((t) => { + const r = (t as Element).getBoundingClientRect(); + return clientX >= r.left && clientX <= r.right && clientY >= r.top && clientY <= r.bottom; + }); + + let textContent = ''; + if (hit) { + textContent = hit.textContent || ''; + } else { + // If no direct hit, find nearest text within 120px + let nearest: { el: Element | null; dist: number } = { el: null, dist: Infinity }; + texts.forEach((t) => { + const r = (t as Element).getBoundingClientRect(); + const cx = (r.left + r.right) / 2; + const cy = (r.top + r.bottom) / 2; + const d = Math.hypot(clientX - cx, clientY - cy); + if (d < nearest.dist) { + nearest = { el: t, dist: d }; + } + }); + if (nearest.el && nearest.dist < 140) { + textContent = nearest.el.textContent || ''; + } + } + + if (!textContent) return; + + const normalized = normalize(textContent); + // Try to match any normalized key in areaMap + for (const entry of normalizedAreaEntries) { + if (normalized.includes(entry.key) || entry.key.includes(normalized)) { + onAreaSelect(entry.id); + return; + } + } + + // If target element directly has a title or id + if (target) { + const el = target as HTMLElement; + const title = (el.getAttribute('title') || el.getAttribute('id') || '').toString(); + const n = normalize(title); + for (const entry of normalizedAreaEntries) { + if (n.includes(entry.key) || entry.key.includes(n)) { + onAreaSelect(entry.id); + return; + } + } + } + }; + + svg.addEventListener('click', onClick); + + // allow pointer events + svg.style.pointerEvents = 'auto'; + + return () => svg.removeEventListener('click', onClick); + }) + .catch((err) => console.error('No se pudo cargar SVG:', err)); + + return () => { + mounted = false; + }; + }, [onAreaSelect]); + + return ( +
+ ); +}