Se agrego la funcionalidad de buscar apartir de la imagen
This commit is contained in:
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 14 KiB |
+32
-18
@@ -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 (
|
||||
<main className="container-fluid py-4 bg-light min-h-screen">
|
||||
<div className="row">
|
||||
@@ -105,25 +124,20 @@ export default function Home() {
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 8 }}>
|
||||
<div style={{ position: 'relative', width: '100%', maxWidth: 1200 }}>
|
||||
<div
|
||||
style={{
|
||||
transform: `translate(${translate.x}px, ${translate.y}px) scale(${scale})`,
|
||||
transformOrigin: '0 0',
|
||||
transition: 'transform 0.05s linear',
|
||||
cursor: isPanning.current ? 'grabbing' : 'grab',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={organigrama}
|
||||
alt="Organigrama"
|
||||
width={organigrama.width}
|
||||
height={organigrama.height}
|
||||
style={{ width: '100%', height: 'auto', display: 'block', userSelect: 'none', pointerEvents: 'none' }}
|
||||
/>
|
||||
<div style={{ position: 'relative', width: '100%', maxWidth: 1200, overflow: 'hidden' }}>
|
||||
<div
|
||||
style={{
|
||||
transform: `translate(${translate.x}px, ${translate.y}px) scale(${scale})`,
|
||||
transformOrigin: '0 0',
|
||||
transition: 'transform 0.05s linear',
|
||||
cursor: isPanning.current ? 'grabbing' : 'grab',
|
||||
touchAction: 'none',
|
||||
}}
|
||||
>
|
||||
<OrganigramaInteractivo onAreaSelect={(id) => buscarPorArea(id)} isPanningRef={isPanning as any} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div style={{ position: 'absolute', right: 12, top: 12, display: 'flex', gap: 8 }}>
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
onAreaSelect: (areaId: number) => void;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
isPanningRef?: React.RefObject<boolean>;
|
||||
}
|
||||
|
||||
// Map display substrings (lowercase) to the area id used in BuscarFuncionario
|
||||
const areaMap: Record<string, number> = {
|
||||
'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 = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1497.56 1125.56">${
|
||||
// Inline the inner content of the SVG (kept short for brevity, we will load the full SVG below)
|
||||
''}
|
||||
</svg>`;
|
||||
|
||||
// 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 <object> fallback and then query inside it.
|
||||
|
||||
export default function OrganigramaInteractivo({ onAreaSelect, className, style, isPanningRef }: Props) {
|
||||
const containerRef = React.useRef<HTMLDivElement | null>(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 (
|
||||
<div ref={containerRef} className={className} style={style} />
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user