up
This commit is contained in:
@@ -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]);
|
||||||
@@ -187,6 +187,12 @@
|
|||||||
<span id="chip-estado" class="estado-chip conectando">
|
<span id="chip-estado" class="estado-chip conectando">
|
||||||
<i class="fas fa-circle-notch fa-spin me-1"></i>Conectando…
|
<i class="fas fa-circle-notch fa-spin me-1"></i>Conectando…
|
||||||
</span>
|
</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>
|
<div class="reloj" id="reloj">--:--:--</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -259,28 +265,77 @@ function actualizarReloj() {
|
|||||||
actualizarReloj();
|
actualizarReloj();
|
||||||
setInterval(actualizarReloj, 1000);
|
setInterval(actualizarReloj, 1000);
|
||||||
|
|
||||||
// ── Audio (Web Audio API — sin archivos externos) ─────────────
|
// ── Audio ─────────────────────────────────────────────────────
|
||||||
let audioCtx = null;
|
let audioCtx = null;
|
||||||
function playBeep() {
|
let sonidoActivo = false;
|
||||||
|
|
||||||
|
function activarSonido() {
|
||||||
try {
|
try {
|
||||||
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
const osc = audioCtx.createOscillator();
|
// Beep silencioso para desbloquear el contexto en el mismo gesto
|
||||||
const gain = audioCtx.createGain();
|
const g = audioCtx.createGain();
|
||||||
osc.connect(gain);
|
g.gain.setValueAtTime(0.001, audioCtx.currentTime);
|
||||||
gain.connect(audioCtx.destination);
|
const o = audioCtx.createOscillator();
|
||||||
osc.type = 'sine';
|
o.connect(g); g.connect(audioCtx.destination);
|
||||||
osc.frequency.setValueAtTime(880, audioCtx.currentTime);
|
o.start(); o.stop(audioCtx.currentTime + 0.05);
|
||||||
osc.frequency.setValueAtTime(660, audioCtx.currentTime + 0.12);
|
} catch (_) {}
|
||||||
gain.gain.setValueAtTime(0.25, audioCtx.currentTime);
|
sonidoActivo = true;
|
||||||
gain.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime + 0.5);
|
const btn = document.getElementById('btn-activar-sonido');
|
||||||
osc.start(audioCtx.currentTime);
|
if (btn) {
|
||||||
osc.stop(audioCtx.currentTime + 0.5);
|
btn.innerHTML = '<i class="fas fa-volume-up"></i> Sonido ON';
|
||||||
} catch (_) { /* Sin audio si el contexto no está disponible */ }
|
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 ─────────────────────────────────────────────
|
// ── Estado previo ─────────────────────────────────────────────
|
||||||
let lastCodigoActivo = null;
|
let lastCodigoActivo = null;
|
||||||
@@ -302,7 +357,7 @@ function renderSnapshot(snap) {
|
|||||||
|
|
||||||
// ¿Cambió el turno? → animar + sonido
|
// ¿Cambió el turno? → animar + sonido
|
||||||
if (activo.codigo !== lastCodigoActivo) {
|
if (activo.codigo !== lastCodigoActivo) {
|
||||||
playBeep();
|
anunciarTurno(activo.codigo, activo.paciente_nombre || '');
|
||||||
elRing.style.color = color;
|
elRing.style.color = color;
|
||||||
elRing.classList.remove('animar');
|
elRing.classList.remove('animar');
|
||||||
// Forzar reflow para reiniciar animación
|
// Forzar reflow para reiniciar animación
|
||||||
|
|||||||
@@ -434,8 +434,16 @@ async function llamarSiguiente() {
|
|||||||
|
|
||||||
// ── Seleccionar turno de la cola (para ver ficha de uno ya llamado) ─
|
// ── Seleccionar turno de la cola (para ver ficha de uno ya llamado) ─
|
||||||
async function seleccionarTurno(turnoId) {
|
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;
|
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 ───────────────────────────────────────────────
|
// ── Abrir ficha ───────────────────────────────────────────────
|
||||||
@@ -566,8 +574,8 @@ async function guardarSolicitud() {
|
|||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
if (!json.ok) { mostrarError(json.error); return; }
|
if (!json.ok) { mostrarError(json.error); return; }
|
||||||
|
|
||||||
solicitudActiva = json.data.solicitud;
|
solicitudActiva = json.solicitud ?? json.data?.solicitud;
|
||||||
consentimientos = json.data.consentimientos_requeridos || [];
|
consentimientos = json.consentimientos_requeridos ?? json.data?.consentimientos_requeridos ?? [];
|
||||||
|
|
||||||
btn.innerHTML = '<i class="fas fa-check me-1"></i>Guardado';
|
btn.innerHTML = '<i class="fas fa-check me-1"></i>Guardado';
|
||||||
|
|
||||||
@@ -619,7 +627,7 @@ async function enviarConsentimientos() {
|
|||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
if (!json.ok) { mostrarError(json.error); return; }
|
if (!json.ok) { mostrarError(json.error); return; }
|
||||||
|
|
||||||
consentimientos = json.data.consentimientos || consentimientos;
|
consentimientos = json.consentimientos ?? json.data?.consentimientos ?? consentimientos;
|
||||||
renderConsentimientos(consentimientos);
|
renderConsentimientos(consentimientos);
|
||||||
btn.innerHTML = '<i class="fas fa-check me-1"></i>Enviado';
|
btn.innerHTML = '<i class="fas fa-check me-1"></i>Enviado';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user