Spaces:
Running
Running
| // Navegación-Pestañas | |
| const tabs = document.querySelectorAll('.tab-nav'); | |
| const paneles = document.querySelectorAll('main > .panel, .col3-wrapper > .panel'); | |
| const examenesSubtabsBar = document.getElementById('examenes-subtabs-bar'); | |
| const EXAMENES_SUBTAB_PANELS = new Set(['panel-hema', 'panel-bioquim', 'panel-uri', 'panel-endo', 'panel-coag', 'panel-gas']); | |
| let panelExamenActivo = 'panel-hema'; | |
| let panelActivo = 'panel-flujo'; | |
| const SWIPE_ORDER = ['panel-flujo', 'panel-paciente', 'panel-hema', 'panel-bioquim', 'panel-uri', 'panel-endo', 'panel-coag', 'panel-gas', 'panel-imagenes', 'panel-resultados']; | |
| export function activarTab(targetId) { | |
| const esSubpanelExamenes = EXAMENES_SUBTAB_PANELS.has(targetId); | |
| const esTabExamenes = targetId === 'examenes'; | |
| const mostrarExamenes = esTabExamenes || esSubpanelExamenes; | |
| // Si se clickea la tab generica "Examenes", muestra el ultimo subtab activo; si es un subtab, lo guarda | |
| let idPanelActual; | |
| if (esTabExamenes) { | |
| idPanelActual = panelExamenActivo; | |
| } else if (esSubpanelExamenes) { | |
| panelExamenActivo = targetId; | |
| idPanelActual = targetId; | |
| } else { | |
| idPanelActual = targetId; | |
| } | |
| tabs.forEach(tab => { | |
| const estaActivo = mostrarExamenes | |
| ? tab.dataset.target === 'examenes' | |
| : tab.dataset.target === targetId; | |
| tab.classList.toggle('activo', estaActivo); | |
| tab.setAttribute('aria-current', estaActivo ? 'true' : 'false'); | |
| }); | |
| if (examenesSubtabsBar) examenesSubtabsBar.hidden = !mostrarExamenes; | |
| if (mostrarExamenes) { | |
| document.querySelectorAll('.tab-examenes').forEach(btn => { | |
| btn.classList.toggle('activo', btn.dataset.subtabTarget === panelExamenActivo); | |
| }); | |
| } | |
| paneles.forEach(panel => { | |
| panel.classList.toggle('activo', panel.id === idPanelActual); | |
| }); | |
| panelActivo = idPanelActual; | |
| if (targetId === 'panel-paciente') sincronizarPacienteMob(); | |
| } | |
| tabs.forEach(tab => { | |
| tab.addEventListener('click', () => activarTab(tab.dataset.target)); | |
| }); | |
| document.querySelectorAll('.tab-examenes').forEach(btn => { | |
| btn.addEventListener('click', () => activarTab(btn.dataset.subtabTarget)); | |
| }); | |
| // Swipe para navegar entre secciones | |
| let inicioSwipeX = 0; | |
| let inicioSwipeY = 0; | |
| document.querySelector('main').addEventListener('touchstart', e => { | |
| inicioSwipeX = e.touches[0].clientX; | |
| inicioSwipeY = e.touches[0].clientY; | |
| }, { passive: true }); | |
| document.querySelector('main').addEventListener('touchend', e => { | |
| const dx = e.changedTouches[0].clientX - inicioSwipeX; | |
| const dy = e.changedTouches[0].clientY - inicioSwipeY; | |
| // Ignora gestos cortos o verticales para no interferir con scroll | |
| if (Math.abs(dx) < 50 || Math.abs(dx) < Math.abs(dy)) return; | |
| const indice = SWIPE_ORDER.indexOf(panelActivo); | |
| const siguiente = dx < 0 ? SWIPE_ORDER[indice + 1] : SWIPE_ORDER[indice - 1]; | |
| if (siguiente) activarTab(siguiente); | |
| }, { passive: true }); | |
| // Sincronizacón de datos de pacientes en mobile | |
| const MAPA_MOB_CANON = { | |
| 'mob-pt-especie': 'pt-especie', | |
| 'mob-pt-raza': 'pt-raza', | |
| 'mob-pt-edad': 'pt-edad', | |
| 'mob-pt-edad-unidad': 'pt-edad-unidad', | |
| 'mob-pt-sexo': 'pt-sexo' | |
| }; | |
| function sincronizarPacienteMob() { | |
| Object.entries(MAPA_MOB_CANON).forEach(([mobId, canonId]) => { | |
| const mobEl = document.getElementById(mobId); | |
| const canonEl = document.getElementById(canonId); | |
| if (mobEl && canonEl) mobEl.value = canonEl.value; | |
| }); | |
| } | |
| export function inicializarSincMob(evaluar) { | |
| Object.entries(MAPA_MOB_CANON).forEach(([mobId, canonId]) => { | |
| const mobEl = document.getElementById(mobId); | |
| if (!mobEl) return; | |
| const tipoEvento = mobEl.tagName === 'SELECT' ? 'change' : 'input'; | |
| mobEl.addEventListener(tipoEvento, () => { | |
| const canonEl = document.getElementById(canonId); | |
| if (canonEl) canonEl.value = mobEl.value; | |
| evaluar(); | |
| }); | |
| }); | |
| } | |
| // Filas de grid | |
| const panelFlujo = document.getElementById('panel-flujo'); | |
| const btnColapsar = document.getElementById('btn-colapsar-flujo'); | |
| const mainEl = document.querySelector('main'); | |
| let filaColapsada = ''; | |
| let filaExpandida = ''; | |
| const esGridEscritorio = () => window.innerWidth > 1100; | |
| function inicializarFilasGrid() { | |
| if (!esGridEscritorio()) return; | |
| mainEl.style.gridTemplateRows = '1fr auto auto auto'; | |
| // Mide el panel de flujo expandido y colapsado para animar grid-template-rows con precision | |
| const alturaPanel = panelFlujo.getBoundingClientRect().height; | |
| const alturaEncabezado = panelFlujo.querySelector('.panel-cabecera').getBoundingClientRect().height; | |
| if (alturaPanel > 0) filaExpandida = `${alturaPanel}px`; | |
| if (alturaEncabezado > 0) filaColapsada = `${alturaEncabezado}px`; | |
| mainEl.style.gridTemplateRows = `1fr auto auto ${filaExpandida || 'auto'}`; | |
| } | |
| function establecerFilasGrid(colapsado, animar) { | |
| if (!esGridEscritorio()) return; | |
| if (!animar) mainEl.style.transition = 'none'; | |
| mainEl.style.gridTemplateRows = colapsado | |
| ? `1fr auto auto ${filaColapsada}` | |
| : `1fr auto auto ${filaExpandida}`; | |
| if (!animar) { | |
| mainEl.offsetHeight; | |
| mainEl.style.transition = ''; | |
| } | |
| } | |
| inicializarFilasGrid(); | |
| const inicioColapsado = localStorage.getItem('mx-flujo-collapsed') === '1'; | |
| if (inicioColapsado) { | |
| panelFlujo.classList.add('collapsed'); | |
| btnColapsar.setAttribute('aria-expanded', 'false'); | |
| establecerFilasGrid(true, false); | |
| } | |
| btnColapsar.addEventListener('click', () => { | |
| const colapsado = panelFlujo.classList.toggle('collapsed'); | |
| btnColapsar.setAttribute('aria-expanded', String(!colapsado)); | |
| establecerFilasGrid(colapsado, true); | |
| localStorage.setItem('mx-flujo-collapsed', colapsado ? '1' : '0'); | |
| if (!colapsado) { | |
| ['panel-endo', 'panel-uri', 'panel-coag', 'panel-gas'].forEach(id => { | |
| const sp = document.getElementById(id); | |
| if (sp) establecerSubpanelColapsado(sp, true); | |
| }); | |
| } | |
| }); | |
| window.addEventListener('resize', () => { | |
| if (esGridEscritorio()) { | |
| if (!panelFlujo.classList.contains('collapsed')) inicializarFilasGrid(); | |
| } else { | |
| document.querySelectorAll('.subpanel-anim').forEach(animEl => { | |
| animEl.style.height = ''; | |
| animEl.style.transition = ''; | |
| }); | |
| document.getElementById('subpanel-citologia') | |
| ?.querySelector('.subpanel-anim') | |
| ?.style.setProperty('height', ''); | |
| } | |
| }); | |
| // Paneles colapsables | |
| function establecerSubpanelColapsado(subpanel, debeColapsar) { | |
| if (!esGridEscritorio()) return; | |
| const animEl = subpanel.querySelector('.subpanel-anim'); | |
| const btn = subpanel.querySelector('.btn-colapsar-subpanel'); | |
| const esRelleno = subpanel.id === 'subpanel-citologia'; | |
| if (debeColapsar === subpanel.classList.contains('collapsed')) return; | |
| subpanel.classList.toggle('collapsed', debeColapsar); | |
| if (btn) btn.setAttribute('aria-expanded', String(!debeColapsar)); | |
| // Forzar reflujo antes de cambiar height permite que CSS transition anime correctamente | |
| if (debeColapsar) { | |
| animEl.style.height = `${animEl.offsetHeight}px`; | |
| animEl.offsetHeight; | |
| animEl.style.height = '0px'; | |
| } else { | |
| animEl.style.height = `${animEl.scrollHeight}px`; | |
| if (esRelleno) { | |
| animEl.addEventListener('transitionend', () => { | |
| animEl.style.height = ''; | |
| }, { once: true }); | |
| } | |
| } | |
| localStorage.setItem(`mx-${subpanel.id}-collapsed`, debeColapsar ? '1' : '0'); | |
| } | |
| const GRUPOS_VINCULADOS = [ | |
| ['panel-endo', 'panel-uri'], | |
| ['panel-coag', 'panel-gas'], | |
| ]; | |
| const PANELES_COLAPSADOS_POR_DEFECTO = new Set(['panel-uri', 'panel-endo', 'panel-coag', 'panel-gas']); | |
| document.querySelectorAll('.btn-colapsar-subpanel').forEach(btn => { | |
| const subpanel = btn.closest('.subpanel'); | |
| const animEl = subpanel.querySelector('.subpanel-anim'); | |
| const claveAlmacenamiento = `mx-${subpanel.id}-collapsed`; | |
| const esRelleno = subpanel.id === 'subpanel-citologia'; | |
| if (esGridEscritorio()) { | |
| if (!esRelleno) { | |
| animEl.style.transition = 'none'; | |
| animEl.style.height = `${animEl.scrollHeight}px`; | |
| } | |
| const valorGuardado = localStorage.getItem(claveAlmacenamiento); | |
| const debeColapsar = valorGuardado !== null | |
| ? valorGuardado === '1' | |
| : PANELES_COLAPSADOS_POR_DEFECTO.has(subpanel.id); | |
| if (debeColapsar) { | |
| subpanel.classList.add('collapsed'); | |
| btn.setAttribute('aria-expanded', 'false'); | |
| if (esRelleno) animEl.style.transition = 'none'; | |
| animEl.style.height = '0px'; | |
| if (esRelleno) { animEl.offsetHeight; animEl.style.transition = ''; } | |
| } | |
| if (!esRelleno) { animEl.offsetHeight; animEl.style.transition = ''; } | |
| } | |
| btn.addEventListener('click', () => { | |
| if (!esGridEscritorio()) return; | |
| const colapsado = subpanel.classList.toggle('collapsed'); | |
| btn.setAttribute('aria-expanded', String(!colapsado)); | |
| if (colapsado) { | |
| animEl.style.height = `${animEl.offsetHeight}px`; | |
| animEl.offsetHeight; | |
| animEl.style.height = '0px'; | |
| } else { | |
| animEl.style.height = `${animEl.scrollHeight}px`; | |
| if (esRelleno) { | |
| animEl.addEventListener('transitionend', () => { | |
| animEl.style.height = ''; | |
| }, { once: true }); | |
| } | |
| } | |
| localStorage.setItem(claveAlmacenamiento, colapsado ? '1' : '0'); | |
| const grupo = GRUPOS_VINCULADOS.find(g => g.includes(subpanel.id)); | |
| if (grupo) { | |
| grupo.forEach(id => { | |
| if (id !== subpanel.id) { | |
| const asociado = document.getElementById(id); | |
| if (asociado) establecerSubpanelColapsado(asociado, colapsado); | |
| } | |
| }); | |
| } | |
| }); | |
| }); | |
| // Patrones de paneles colapsables | |
| const btnColapsarPatrones = document.getElementById('btn-colapsar-patrones'); | |
| const patronesAnim = document.getElementById('patrones-anim'); | |
| export function colapsarPatrones(debeColapsar) { | |
| const estaExpandido = btnColapsarPatrones.getAttribute('aria-expanded') === 'true'; | |
| const colapsado = debeColapsar ?? estaExpandido; | |
| if (colapsado && estaExpandido) { | |
| patronesAnim.style.height = `${patronesAnim.scrollHeight}px`; | |
| patronesAnim.offsetHeight; | |
| patronesAnim.style.height = '0px'; | |
| btnColapsarPatrones.setAttribute('aria-expanded', 'false'); | |
| } else if (!colapsado && !estaExpandido) { | |
| patronesAnim.style.height = `${patronesAnim.scrollHeight}px`; | |
| patronesAnim.addEventListener('transitionend', () => { | |
| if (btnColapsarPatrones.getAttribute('aria-expanded') === 'true') { | |
| patronesAnim.style.height = ''; | |
| } | |
| }, { once: true }); | |
| btnColapsarPatrones.setAttribute('aria-expanded', 'true'); | |
| } | |
| } | |
| btnColapsarPatrones.addEventListener('click', () => colapsarPatrones()); | |
| // Imágenes | |
| export const imagenesDataUrl = [null, null]; | |
| export const capturasMicroscopio = []; | |
| const MAX_CAPTURAS_MICRO = 4; | |
| document.querySelectorAll('.zona-imagen').forEach(zona => { | |
| const indice = parseInt(zona.dataset.zona); | |
| const input = zona.querySelector('.input-zona'); | |
| const vacia = zona.querySelector('.zona-vacia'); | |
| const btnQuitar = zona.querySelector('.btn-quitar-zona'); | |
| const vistaPrevia = document.createElement('img'); | |
| vistaPrevia.className = 'zona-img-preview'; | |
| vistaPrevia.alt = `Citología ${indice + 1}`; | |
| vistaPrevia.hidden = true; | |
| btnQuitar.before(vistaPrevia); | |
| zona.addEventListener('click', e => { | |
| if (btnQuitar.contains(e.target)) return; | |
| input.click(); | |
| }); | |
| input.addEventListener('change', () => { | |
| const file = input.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = ev => { | |
| const img = new Image(); | |
| img.onload = () => { | |
| // Reduce la imagen a max 1024px en su lado mayor para no saturar la memoria ni la API | |
| const MAX_PIXELES = 1024; | |
| const scale = Math.min(MAX_PIXELES / img.width, MAX_PIXELES / img.height, 1); | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = Math.round(img.width * scale); | |
| canvas.height = Math.round(img.height * scale); | |
| canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height); | |
| const dataUrl = canvas.toDataURL('image/jpeg', 0.85); | |
| imagenesDataUrl[indice] = dataUrl; | |
| vistaPrevia.src = dataUrl; | |
| vistaPrevia.hidden = false; | |
| btnQuitar.hidden = false; | |
| vacia.hidden = true; | |
| zona.classList.add('con-imagen'); | |
| }; | |
| img.src = ev.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| }); | |
| btnQuitar.addEventListener('click', e => { | |
| e.stopPropagation(); | |
| imagenesDataUrl[indice] = null; | |
| vistaPrevia.src = ''; | |
| vistaPrevia.hidden = true; | |
| btnQuitar.hidden = true; | |
| vacia.hidden = false; | |
| zona.classList.remove('con-imagen'); | |
| input.value = ''; | |
| }); | |
| }); | |
| // Captura de microscopio | |
| (function () { | |
| const zona = document.querySelector('.zona-microscopio'); | |
| if (!zona) return; | |
| const micVacia = zona.querySelector('.micro-vacia'); | |
| const video = zona.querySelector('.micro-video'); | |
| const controles = zona.querySelector('.micro-controles'); | |
| const btnGaleria = zona.querySelector('.micro-btn-galeria'); | |
| const badge = zona.querySelector('.micro-badge'); | |
| const btnCapturar = zona.querySelector('.micro-btn-capturar'); | |
| const btnCerrar = zona.querySelector('.micro-btn-cerrar'); | |
| const galeriaEl = zona.querySelector('.micro-galeria'); | |
| let stream = null; | |
| let galeriaEsVisible = false; | |
| function detenerStream() { | |
| if (stream) { stream.getTracks().forEach(t => t.stop()); stream = null; } | |
| } | |
| function actualizarInsignia() { | |
| const n = capturasMicroscopio.length; | |
| badge.textContent = n; | |
| badge.hidden = n === 0; | |
| btnCapturar.disabled = n >= MAX_CAPTURAS_MICRO; | |
| } | |
| function renderizarGaleria() { | |
| if (capturasMicroscopio.length === 0) { | |
| galeriaEl.innerHTML = '<span class="micro-galeria-vacia">Sin capturas</span>'; | |
| return; | |
| } | |
| galeriaEl.innerHTML = capturasMicroscopio.map((src, i) => ` | |
| <div class="micro-thumb"> | |
| <img src="${src}" alt="Captura ${i + 1}"> | |
| <button class="micro-thumb-quitar" type="button" data-capture-idx="${i}" aria-label="Eliminar captura ${i + 1}"> | |
| <svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" aria-hidden="true"> | |
| <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/> | |
| </svg> | |
| </button> | |
| </div>`).join(''); | |
| galeriaEl.querySelectorAll('.micro-thumb-quitar').forEach(btn => { | |
| btn.addEventListener('click', e => { | |
| e.stopPropagation(); | |
| const i = parseInt(btn.dataset.captureIdx); | |
| capturasMicroscopio.splice(i, 1); | |
| actualizarInsignia(); | |
| renderizarGaleria(); | |
| if (capturasMicroscopio.length === 0 && galeriaEsVisible) alternarGaleria(); | |
| }); | |
| }); | |
| } | |
| function alternarGaleria() { | |
| galeriaEsVisible = !galeriaEsVisible; | |
| galeriaEl.hidden = !galeriaEsVisible; | |
| if (galeriaEsVisible) renderizarGaleria(); | |
| btnGaleria.style.color = galeriaEsVisible ? 'var(--accent)' : ''; | |
| } | |
| async function abrirCamara() { | |
| try { | |
| // Preferencia por camara trasera (microscopio o movil apuntando a la muestra) | |
| stream = await navigator.mediaDevices.getUserMedia({ | |
| video: { facingMode: { ideal: 'environment' }, width: { ideal: 1920 } } | |
| }); | |
| video.srcObject = stream; | |
| video.hidden = false; | |
| micVacia.hidden = true; | |
| controles.hidden = false; | |
| actualizarInsignia(); | |
| } catch { | |
| // Permiso denegado o camara no disponible; no se requiere fallback | |
| } | |
| } | |
| zona.addEventListener('click', e => { | |
| if (controles.contains(e.target) || galeriaEl.contains(e.target)) return; | |
| if (!stream) abrirCamara(); | |
| }); | |
| btnGaleria.addEventListener('click', e => { | |
| e.stopPropagation(); | |
| alternarGaleria(); | |
| }); | |
| btnCapturar.addEventListener('click', e => { | |
| e.stopPropagation(); | |
| if (capturasMicroscopio.length >= MAX_CAPTURAS_MICRO) return; | |
| const canvas = document.createElement('canvas'); | |
| // Escala el fotograma de video para mantener un tamano razonable antes de enviarlo al modelo | |
| const MAX_PIXELES = 1024; | |
| const scale = Math.min(MAX_PIXELES / video.videoWidth, MAX_PIXELES / video.videoHeight, 1); | |
| canvas.width = Math.round(video.videoWidth * scale); | |
| canvas.height = Math.round(video.videoHeight * scale); | |
| canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height); | |
| capturasMicroscopio.push(canvas.toDataURL('image/jpeg', 0.85)); | |
| actualizarInsignia(); | |
| if (galeriaEsVisible) renderizarGaleria(); | |
| }); | |
| btnCerrar.addEventListener('click', e => { | |
| e.stopPropagation(); | |
| detenerStream(); | |
| video.hidden = true; | |
| video.srcObject = null; | |
| controles.hidden = true; | |
| galeriaEl.hidden = true; | |
| galeriaEsVisible = false; | |
| micVacia.hidden = false; | |
| }); | |
| })(); | |