This commit is contained in:
Lizandro Guarnizo
2026-04-21 13:27:37 -05:00
parent 0767dea05e
commit 51f98dd2fb
3 changed files with 122 additions and 25 deletions
+34
View File
@@ -0,0 +1,34 @@
<?php
/**
* GET /modules/turnero/api/get_turno.php?id=TURNO_ID
* Devuelve los datos completos de un turno para abrir su ficha en recepción.
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('GET');
$turnoId = isset($_GET['id']) ? (int) $_GET['id'] : 0;
if (!$turnoId) {
jsonError('id es obligatorio', 400);
}
$pdo = db();
$stmt = $pdo->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]);
+76 -21
View File
@@ -187,6 +187,12 @@
<span id="chip-estado" class="estado-chip conectando">
<i class="fas fa-circle-notch fa-spin me-1"></i>Conectando…
</span>
<button id="btn-activar-sonido" onclick="activarSonido()" title="Toca para activar el sonido del turnero"
style="background:rgba(59,130,246,0.15);border:1px solid rgba(59,130,246,0.4);color:#93c5fd;
border-radius:8px;padding:6px 14px;font-size:.82rem;cursor:pointer;
display:flex;align-items:center;gap:6px;white-space:nowrap">
<i class="fas fa-volume-mute"></i> Activar sonido
</button>
<div class="reloj" id="reloj">--:--:--</div>
</header>
@@ -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 = '<i class="fas fa-volume-up"></i> 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
+12 -4
View File
@@ -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 = '<i class="fas fa-check me-1"></i>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 = '<i class="fas fa-check me-1"></i>Enviado';
} catch (err) {