diff --git a/modules/turnero/api/get_turno.php b/modules/turnero/api/get_turno.php
new file mode 100644
index 0000000..9d80cdd
--- /dev/null
+++ b/modules/turnero/api/get_turno.php
@@ -0,0 +1,34 @@
+prepare(
+ "SELECT t.*,
+ p.codigo AS prioridad_codigo,
+ p.nombre AS prioridad_nombre,
+ p.color AS prioridad_color,
+ p.orden_peso
+ FROM turnero_turnos t
+ JOIN turnero_prioridades p ON p.id = t.prioridad_id
+ WHERE t.id = ?
+ LIMIT 1"
+);
+$stmt->execute([$turnoId]);
+$turno = $stmt->fetch(PDO::FETCH_ASSOC);
+
+if (!$turno) {
+ jsonError('Turno no encontrado', 404);
+}
+
+jsonOk(['turno' => $turno]);
diff --git a/modules/turnero/views/display.php b/modules/turnero/views/display.php
index 7ec841b..a9b6348 100644
--- a/modules/turnero/views/display.php
+++ b/modules/turnero/views/display.php
@@ -187,6 +187,12 @@
Conectando…
+
--:--:--
@@ -259,28 +265,77 @@ function actualizarReloj() {
actualizarReloj();
setInterval(actualizarReloj, 1000);
-// ── Audio (Web Audio API — sin archivos externos) ─────────────
-let audioCtx = null;
-function playBeep() {
+// ── Audio ─────────────────────────────────────────────────────
+let audioCtx = null;
+let sonidoActivo = false;
+
+function activarSonido() {
try {
- if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
- const osc = audioCtx.createOscillator();
- const gain = audioCtx.createGain();
- osc.connect(gain);
- gain.connect(audioCtx.destination);
- osc.type = 'sine';
- osc.frequency.setValueAtTime(880, audioCtx.currentTime);
- osc.frequency.setValueAtTime(660, audioCtx.currentTime + 0.12);
- gain.gain.setValueAtTime(0.25, audioCtx.currentTime);
- gain.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime + 0.5);
- osc.start(audioCtx.currentTime);
- osc.stop(audioCtx.currentTime + 0.5);
- } catch (_) { /* Sin audio si el contexto no está disponible */ }
+ audioCtx = new (window.AudioContext || window.webkitAudioContext)();
+ // Beep silencioso para desbloquear el contexto en el mismo gesto
+ const g = audioCtx.createGain();
+ g.gain.setValueAtTime(0.001, audioCtx.currentTime);
+ const o = audioCtx.createOscillator();
+ o.connect(g); g.connect(audioCtx.destination);
+ o.start(); o.stop(audioCtx.currentTime + 0.05);
+ } catch (_) {}
+ sonidoActivo = true;
+ const btn = document.getElementById('btn-activar-sonido');
+ if (btn) {
+ btn.innerHTML = ' Sonido ON';
+ btn.style.background = 'rgba(34,197,94,0.2)';
+ btn.style.borderColor = 'rgba(34,197,94,0.5)';
+ btn.style.color = '#86efac';
+ btn.onclick = null;
+ btn.style.cursor = 'default';
+ // Reproducir beep de prueba al activar
+ setTimeout(playBeep, 100);
+ }
+}
+
+// Desbloquear también si el usuario toca/clica cualquier parte de la pantalla
+document.addEventListener('click', () => { if (!sonidoActivo) activarSonido(); }, { once: true });
+document.addEventListener('touchstart', () => { if (!sonidoActivo) activarSonido(); }, { once: true });
+
+function playBeep() {
+ if (sonidoActivo && audioCtx) {
+ 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, nombre) {
+ // 1. Beep electrónico
+ playBeep();
+ // 2. Voz sintética — funciona sin interacción previa en la mayoría de navegadores
+ if ('speechSynthesis' in window) {
+ // Esperar a que el beep termine antes de hablar
+ setTimeout(() => {
+ window.speechSynthesis.cancel();
+ const letras = codigo.split('').join(' '); // "A 0 1" para mejor pronunciación
+ let texto = `Turno ${letras}.`;
+ if (nombre) texto += ` ${nombre}, por favor pase a recepción.`;
+ const utt = new SpeechSynthesisUtterance(texto);
+ utt.lang = 'es-CO';
+ utt.rate = 0.88;
+ utt.pitch = 1.05;
+ utt.volume = 1;
+ window.speechSynthesis.speak(utt);
+ }, 700);
+ }
}
-// Desbloquear AudioContext ante primer tap/clic (requerido por navegadores)
-document.addEventListener('click', () => {
- if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
-}, { once: true });
// ── Estado previo ─────────────────────────────────────────────
let lastCodigoActivo = null;
@@ -302,7 +357,7 @@ function renderSnapshot(snap) {
// ¿Cambió el turno? → animar + sonido
if (activo.codigo !== lastCodigoActivo) {
- playBeep();
+ anunciarTurno(activo.codigo, activo.paciente_nombre || '');
elRing.style.color = color;
elRing.classList.remove('animar');
// Forzar reflow para reiniciar animación
diff --git a/modules/turnero/views/recepcion.php b/modules/turnero/views/recepcion.php
index bcd3f45..d0f2e19 100644
--- a/modules/turnero/views/recepcion.php
+++ b/modules/turnero/views/recepcion.php
@@ -434,8 +434,16 @@ async function llamarSiguiente() {
// ── Seleccionar turno de la cola (para ver ficha de uno ya llamado) ─
async function seleccionarTurno(turnoId) {
- // No abrir si ya hay uno en recepción (confuso)
+ // No abrir si ya hay uno activo en recepción
if (turnoActivo && turnoActivo.estado === 'en_recepcion') return;
+ try {
+ const res = await fetch(API + 'get_turno.php?id=' + turnoId);
+ const json = await res.json();
+ if (!json.ok) { mostrarError(json.error || 'No se pudo cargar el turno'); return; }
+ abrirFicha(json.turno ?? json.data?.turno);
+ } catch (err) {
+ mostrarError('Error al cargar turno: ' + err.message);
+ }
}
// ── Abrir ficha ───────────────────────────────────────────────
@@ -566,8 +574,8 @@ async function guardarSolicitud() {
const json = await res.json();
if (!json.ok) { mostrarError(json.error); return; }
- solicitudActiva = json.data.solicitud;
- consentimientos = json.data.consentimientos_requeridos || [];
+ solicitudActiva = json.solicitud ?? json.data?.solicitud;
+ consentimientos = json.consentimientos_requeridos ?? json.data?.consentimientos_requeridos ?? [];
btn.innerHTML = 'Guardado';
@@ -619,7 +627,7 @@ async function enviarConsentimientos() {
const json = await res.json();
if (!json.ok) { mostrarError(json.error); return; }
- consentimientos = json.data.consentimientos || consentimientos;
+ consentimientos = json.consentimientos ?? json.data?.consentimientos ?? consentimientos;
renderConsentimientos(consentimientos);
btn.innerHTML = 'Enviado';
} catch (err) {