diff --git a/modules/turnero/views/display_global.php b/modules/turnero/views/display_global.php index c1b3b0d..d7b2d7f 100644 --- a/modules/turnero/views/display_global.php +++ b/modules/turnero/views/display_global.php @@ -195,42 +195,43 @@ $_hasVideo = (bool)$_tvVideo; height: 100%; } .ultimos-llamados .ul-label { - font-size: 1.2rem; font-weight: 700; text-transform: uppercase; + font-size: 1rem; font-weight: 700; text-transform: uppercase; letter-spacing: 2px; color: #94a3b8; - padding: .8rem .8rem .3rem; flex-shrink: 0; + padding: .65rem .85rem .3rem; flex-shrink: 0; + } + .ul-thead { + display: grid; grid-template-columns: 82px 1fr; + padding: .25rem .85rem; + background: color-mix(in srgb, var(--brand) 12%, #f1f5f9); + border-top: 1px solid #e2e8f0; border-bottom: 2px solid #e2e8f0; + font-size: .62rem; font-weight: 800; + text-transform: uppercase; letter-spacing: .1em; color: #64748b; + flex-shrink: 0; } .ultimos-llamados .ul-scroll { - flex: 1; overflow-y: auto; padding: 0 .5rem .5rem; + flex: 1; overflow-y: auto; padding: .3rem .4rem .5rem; scrollbar-width: thin; scrollbar-color: #e2e8f0 transparent; } .ul-item { - display: flex; align-items: center; gap: .45rem; - padding: .5rem .75rem; border-radius: 10px; - margin-bottom: .35rem; + display: grid; grid-template-columns: 82px 1fr; + align-items: center; + padding: .38rem .45rem; border-radius: 8px; + margin-bottom: .25rem; background: #fff; border: 1px solid #e2e8f0; - border-left: 4px solid color-mix(in srgb, var(--brand, #1565c0) 50%, transparent); + border-left: 3px solid color-mix(in srgb, var(--brand, #1565c0) 55%, transparent); } .ul-item.activo { background: var(--brand); border-color: var(--brand); + border-left-color: rgba(255,255,255,.4); color: #fff; - border-left-color: #fff; - } - .ul-item.activo .ul-nom, - .ul-item.activo .ul-dest { color: rgba(255,255,255,.75); } - .ul-item .ul-left { flex: 0 0 auto; min-width: 0; } - .ul-item .ul-cod { font-weight: 800; font-size: 1.55rem; display: block; } - .ul-item .ul-nom { - font-size: .8rem; font-weight: 500; - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - display: block; } + .ul-item .ul-cod { font-weight: 800; font-size: 1.45rem; line-height: 1; } .ul-item .ul-dest { - font-size: 1.1rem; font-weight: 700; color: #1e293b; - flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + font-size: .92rem; font-weight: 600; color: #1e293b; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } - .ul-item.activo .ul-dest { color: #fff; } - .ul-item .ul-dest i { margin-right: 3px; } + .ul-item.activo .ul-dest { color: rgba(255,255,255,.85); } /* ── Video reel ── */ .reel-wrap { @@ -405,6 +406,10 @@ $_hasVideo = (bool)$_tvVideo;
@@ -565,8 +570,9 @@ function showAnnouncement({ codigo, destino, paciente, color }) { } /* ── State ── */ -let lastSeenKeys = new Set(); // clave por turno+destino+llamado_at -let llamados = []; +let lastSeenKeys = new Set(); +let llamados = []; +let _firstLoad = true; function renderSnapshot(snap) { const codEl = document.getElementById('codigo-activo'); @@ -584,7 +590,7 @@ function renderSnapshot(snap) { if (l.turno) allTurnos.push({ ...l.turno, destino: l.lugar_nombre }); }); - // Detectar turno nuevo en CUALQUIER estación y anunciar + // Detectar turnos nuevos y anunciar const currentKeys = new Set(); let anuncioNuevo = false; allTurnos.forEach(t => { @@ -592,13 +598,29 @@ function renderSnapshot(snap) { currentKeys.add(key); if (!lastSeenKeys.has(key)) { const c = t.prioridad_color || ''; - queueAnnouncement({ _key: key, codigo: t.codigo, destino: t.destino, paciente: t.paciente_nombre, color: c }); + // En primera carga silenciar todos; el más reciente se anuncia abajo + if (!_firstLoad) { + queueAnnouncement({ _key: key, codigo: t.codigo, destino: t.destino, paciente: t.paciente_nombre, color: c }); + } llamados.unshift({ codigo: t.codigo, destino: t.destino, paciente: t.paciente_nombre, color: c }); if (llamados.length > 30) llamados.pop(); anuncioNuevo = true; } }); lastSeenKeys = currentKeys; + + if (_firstLoad) { + _firstLoad = false; + // Anunciar solo el turno más reciente al cargar la página + const sorted = allTurnos.slice().sort((a, b) => new Date(b.llamado_at || 0) - new Date(a.llamado_at || 0)); + if (sorted.length > 0) { + const t = sorted[0]; + const c = t.prioridad_color || ''; + const key = t.codigo + '|' + t.destino + '|' + (t.llamado_at || ''); + queueAnnouncement({ _key: key, codigo: t.codigo, destino: t.destino, paciente: t.paciente_nombre, color: c }); + } + } + if (anuncioNuevo) renderLlamados(); // zona-activo: mostrar el turno llamado más recientemente @@ -659,16 +681,12 @@ function renderLlamados() { if (!wrap || !sc) return; if (llamados.length === 0) { wrap.style.display = 'none'; return; } wrap.style.display = ''; - const activo = llamados[0]; sc.innerHTML = llamados.map((l, i) => { const isActivo = i === 0; const c = l.color || ''; - return `
-
- ${esc(l.codigo)} - ${l.paciente ? `${esc(l.paciente)}` : ''} -
- ${esc(l.destino)} + return `
+ ${esc(l.codigo)} + ${esc(l.destino)}
`; }).join(''); } diff --git a/modules/turnero/views/lugar.php b/modules/turnero/views/lugar.php index d509f53..f4e7b4a 100644 --- a/modules/turnero/views/lugar.php +++ b/modules/turnero/views/lugar.php @@ -385,57 +385,6 @@ const API = 'modules/turnero/api/'; const BASE_WA = ''; const LUGAR_NOMBRE = document.getElementById('lbl-lugar-titulo')?.textContent || ''; -// ── Audio / anuncio ─────────────────────────────────────────── -let _audioCtx = null; -let _sonidoListo = false; - -function _desbloquearAudio() { - if (_sonidoListo) return; - try { - _audioCtx = new (window.AudioContext || window.webkitAudioContext)(); - const g = _audioCtx.createGain(); g.gain.value = 0.001; - const o = _audioCtx.createOscillator(); - o.connect(g); g.connect(_audioCtx.destination); - o.start(); o.stop(_audioCtx.currentTime + 0.05); - } catch (_) {} - _sonidoListo = true; -} -document.addEventListener('click', _desbloquearAudio, { once: false }); -document.addEventListener('touchstart', _desbloquearAudio, { once: false }); - -function _playBeep() { - if (!_audioCtx) return; - try { - const osc = _audioCtx.createOscillator(); - const gain = _audioCtx.createGain(); - osc.type = 'sine'; - osc.connect(gain); gain.connect(_audioCtx.destination); - osc.frequency.setValueAtTime(880, _audioCtx.currentTime); - osc.frequency.setValueAtTime(1100, _audioCtx.currentTime + 0.1); - osc.frequency.setValueAtTime(880, _audioCtx.currentTime + 0.2); - gain.gain.setValueAtTime(0.35, _audioCtx.currentTime); - gain.gain.exponentialRampToValueAtTime(0.0001, _audioCtx.currentTime + 0.65); - osc.start(_audioCtx.currentTime); - osc.stop(_audioCtx.currentTime + 0.65); - } catch (_) {} -} - -function anunciarTurno(codigo) { - _desbloquearAudio(); - _playBeep(); - if ('speechSynthesis' in window) { - setTimeout(() => { - window.speechSynthesis.cancel(); - const letras = codigo.split('').join(' '); - const utt = new SpeechSynthesisUtterance(`Turno ${letras}, pase a ${LUGAR_NOMBRE}`); - utt.lang = 'es-CO'; - utt.rate = 0.85; - utt.pitch = 1.05; - utt.volume = 1; - window.speechSynthesis.speak(utt); - }, 700); - } -} // ── Arranque ────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { @@ -564,7 +513,6 @@ async function rellamarDesdeCard(turnoId) { }); const json = await res.json(); if (!json.ok || !json.turno) return; - anunciarTurno(json.turno.codigo); await abrirFicha(json.turno); } @@ -594,7 +542,6 @@ async function _llamar(extra) { if (!json.ok) { mostrarError(json.error); return; } if (!json.turno) { mostrarToast('Cola vacía — no hay turnos en espera.', 'warn', 3000); return; } mostrarLlamando(json.turno.codigo); - anunciarTurno(json.turno.codigo); await abrirFicha(json.turno); cargarCola(); } catch (err) { @@ -639,10 +586,10 @@ async function cargarFichaSolicitud(turnoId) { const json = await res.json(); if (!json.ok) return; - const sol = json.solicitud; - const pac = json.paciente; - const exams = json.examenes || []; - const consts = json.consentimientos || []; + const sol = json.solicitud; + const pac = json.paciente; + const exams = json.examenes || []; + let consts = json.consentimientos || []; // Datos del paciente if (pac) { @@ -669,6 +616,42 @@ async function cargarFichaSolicitud(turnoId) { listaEx.innerHTML = 'Sin exámenes registrados'; } + // Auto-crear tokens para formularios del lugar que aún no tengan registro + if (lugarId) { + const resF = await fetch(`${API}get_lugar_formularios.php?lugar_id=${lugarId}`); + const jsonF = await resF.json(); + const forms = (jsonF.ok && jsonF.formularios) ? jsonF.formularios : []; + + const existIds = new Set(consts.map(c => parseInt(c.formulario_id))); + const faltantes = forms.filter(f => !existIds.has(parseInt(f.id))); + + if (faltantes.length > 0) { + await Promise.all(faltantes.map(f => + fetch(API + 'create_consent_token.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ turno_id: turnoId, formulario_id: f.id }), + }).catch(() => {}) + )); + // Recargar consents tras la creación + const res2 = await fetch(`${API}get_consentimientos.php?turno_id=${turnoId}`); + const json2 = await res2.json(); + if (json2.ok) consts = json2.consentimientos || []; + + // Si alguno no se pudo crear (sin paciente), mostrarlo igual como pendiente visual + const createdIds = new Set(consts.map(c => parseInt(c.formulario_id))); + faltantes.forEach(f => { + if (!createdIds.has(parseInt(f.id))) { + consts.push({ + id: null, turno_id: turnoId, + formulario_id: f.id, formulario_nombre: f.nombre, + estado: 'pendiente', token: null, + }); + } + }); + } + } + // Consentimientos renderConsentimientos(consts); @@ -921,7 +904,7 @@ async function rellamarTurno() { }); const json = await res.json(); if (!json.ok) mostrarError(json.error); - else if (json.turno) { mostrarLlamando(json.turno.codigo); anunciarTurno(json.turno.codigo); } + else if (json.turno) { mostrarLlamando(json.turno.codigo); } } catch (err) { mostrarError(err.message); } finally {