This commit is contained in:
Lizandro Guarnizo
2026-06-16 07:02:04 -05:00
parent 64f21b083a
commit 35b13468b6
5 changed files with 81 additions and 22 deletions
+4
View File
@@ -31,6 +31,10 @@ if ($stmt->rowCount() === 0) {
jsonError('El turno no está en estado en_recepcion o no existe.', 404); jsonError('El turno no está en estado en_recepcion o no existe.', 404);
} }
// Notificar SSE para que las pantallas se actualicen inmediatamente
$stmtS = $pdo->prepare('UPDATE turnero_sesiones SET sse_ping_at = NOW() WHERE id = (SELECT sesion_id FROM turnero_turnos WHERE id = ?)');
$stmtS->execute([$turnoId]);
// Devolver el turno actualizado para que recepcion.php abra la ficha // Devolver el turno actualizado para que recepcion.php abra la ficha
$stmtT = $pdo->prepare( $stmtT = $pdo->prepare(
"SELECT t.*, "SELECT t.*,
+1 -1
View File
@@ -537,7 +537,7 @@ async function cargarSnapshot() {
// Arrancar polling cada 4 s (funciona aunque SSE falle por proxy) // Arrancar polling cada 4 s (funciona aunque SSE falle por proxy)
function iniciarPolling() { function iniciarPolling() {
cargarSnapshot(); cargarSnapshot();
pollTimer = setInterval(cargarSnapshot, 4000); pollTimer = setInterval(cargarSnapshot, 2000);
} }
// ── SSE ─────────────────────────────────────────────────────── // ── SSE ───────────────────────────────────────────────────────
+1 -1
View File
@@ -684,7 +684,7 @@ async function cargarSnapshot() {
} }
cargarSnapshot(); cargarSnapshot();
setInterval(cargarSnapshot, 4000); setInterval(cargarSnapshot, 2000);
</script> </script>
</body> </body>
</html> </html>
+1 -1
View File
@@ -372,7 +372,7 @@ async function cargar() {
} }
cargar(); cargar();
setInterval(cargar, 4000); setInterval(cargar, 2000);
</script> </script>
</body> </body>
</html> </html>
+74 -19
View File
@@ -197,6 +197,31 @@ $adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['
border: 1px solid #bfdbfe; border-radius: 8px; border: 1px solid #bfdbfe; border-radius: 8px;
padding: .25rem .75rem; font-size: .82rem; font-weight: 700; padding: .25rem .75rem; font-size: .82rem; font-weight: 700;
} }
/* ── Toast ── */
.rec-toast {
position: fixed; bottom: 2rem; right: 2rem; z-index: 9999;
display: flex; align-items: center; gap: .7rem;
padding: .8rem 1.2rem; border-radius: 12px;
background: #fff; border: 1px solid #e2e8f0;
box-shadow: 0 8px 30px rgba(0,0,0,.12);
font-size: .9rem; font-weight: 500;
transform: translateY(120%); opacity: 0;
transition: transform .3s ease, opacity .3s ease;
pointer-events: none;
}
.rec-toast.show {
transform: translateY(0); opacity: 1;
}
.rec-toast.success { border-left: 4px solid #22c55e; }
.rec-toast.info { border-left: 4px solid #3b82f6; }
.rec-toast.warn { border-left: 4px solid #f59e0b; }
.rec-toast.error { border-left: 4px solid #ef4444; }
.rec-toast .ico { font-size: 1.2rem; }
.rec-toast.success .ico { color: #22c55e; }
.rec-toast.info .ico { color: #3b82f6; }
.rec-toast.warn .ico { color: #f59e0b; }
.rec-toast.error .ico { color: #ef4444; }
</style> </style>
</head> </head>
<body> <body>
@@ -404,6 +429,11 @@ require_once __DIR__ . '/../../../shared/components/sidebar.php';
</div><!-- /rec-layout --> </div><!-- /rec-layout -->
</main> </main>
<div class="rec-toast" id="rec-toast">
<span class="ico" id="toast-ico"><i class="fas fa-check-circle"></i></span>
<span id="toast-msg"></span>
</div>
<!-- ══ Modal firma presencial ═════════════════════════════════ --> <!-- ══ Modal firma presencial ═════════════════════════════════ -->
<div class="modal-firma-backdrop" id="modal-firma-rec"> <div class="modal-firma-backdrop" id="modal-firma-rec">
<div class="modal-firma-box"> <div class="modal-firma-box">
@@ -432,7 +462,7 @@ const DESK_ID = <?= $deskId ?: 'null' ?>;
// ── Arranque ────────────────────────────────────────────────── // ── Arranque ──────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
cargarCola(); cargarCola();
pollingColaId = setInterval(cargarCola, 8000); pollingColaId = setInterval(cargarCola, 3000);
document.getElementById('inp-buscar-pac') document.getElementById('inp-buscar-pac')
.addEventListener('keydown', e => { if (e.key === 'Enter') buscarPaciente(); }); .addEventListener('keydown', e => { if (e.key === 'Enter') buscarPaciente(); });
}); });
@@ -460,7 +490,7 @@ function renderCola(snap) {
return; return;
} }
lista.innerHTML = espera.map(t => ` lista.innerHTML = espera.map(t => `
<div class="cola-card ${turnoActivo?.id === t.id ? 'activo' : ''}"> <div class="cola-card ${turnoActivo?.id === t.id ? 'activo' : ''}" data-id="${t.id}">
<div class="prio-dot" style="background:${t.prioridad_color};cursor:pointer" onclick="seleccionarTurno(${t.id})">${t.prioridad_codigo}</div> <div class="prio-dot" style="background:${t.prioridad_color};cursor:pointer" onclick="seleccionarTurno(${t.id})">${t.prioridad_codigo}</div>
<div class="turno-info" style="cursor:pointer" onclick="seleccionarTurno(${t.id})"> <div class="turno-info" style="cursor:pointer" onclick="seleccionarTurno(${t.id})">
<div class="cod">${escHtml(t.codigo)}</div> <div class="cod">${escHtml(t.codigo)}</div>
@@ -479,6 +509,7 @@ function renderCola(snap) {
async function llamarSiguiente() { async function llamarSiguiente() {
const btn = document.getElementById('btn-llamar'); const btn = document.getElementById('btn-llamar');
btn.disabled = true; btn.disabled = true;
mostrarToast('Consultando siguiente turno…', 'info', 1500);
try { try {
const res = await fetch(API + 'llamar_turno.php', { const res = await fetch(API + 'llamar_turno.php', {
method: 'POST', method: 'POST',
@@ -491,8 +522,10 @@ async function llamarSiguiente() {
const turnoLlamado = json.turno ?? json.data?.turno; const turnoLlamado = json.turno ?? json.data?.turno;
if (!turnoLlamado) { if (!turnoLlamado) {
mostrarAviso('Cola vacía', 'No hay turnos en espera.'); mostrarAviso('Cola vacía', 'No hay turnos en espera.');
resetPlaceholder();
return; return;
} }
mostrarLlamando(turnoLlamado.codigo);
abrirFicha(turnoLlamado); abrirFicha(turnoLlamado);
cargarCola(); cargarCola();
} catch (err) { } catch (err) {
@@ -516,6 +549,12 @@ async function seleccionarTurno(turnoId) {
// ── Llamar turno específico (cambia estado → en_recepcion) ───────── // ── Llamar turno específico (cambia estado → en_recepcion) ─────────
async function llamarTurnoEspecifico(turnoId, estadoActual) { async function llamarTurnoEspecifico(turnoId, estadoActual) {
// Mostrar feedback inmediato
const card = document.querySelector(`.cola-card[data-id="${turnoId}"]`);
if (card) {
const cod = card.querySelector('.cod')?.textContent || '';
mostrarLlamando(cod);
}
// Si ya está en recepción: re-llamar (actualiza llamado_at para que la pantalla TV reactive) // Si ya está en recepción: re-llamar (actualiza llamado_at para que la pantalla TV reactive)
if (estadoActual === 'en_recepcion') { if (estadoActual === 'en_recepcion') {
try { try {
@@ -860,22 +899,20 @@ async function pasarALugar() {
solicitudActiva = null; solicitudActiva = null;
document.getElementById('ficha-turno').classList.add('d-none'); document.getElementById('ficha-turno').classList.add('d-none');
document.getElementById('badge-turno-activo').classList.add('d-none'); document.getElementById('badge-turno-activo').classList.add('d-none');
cargarCola();
// Mostrar aviso con link al lugar mostrarToast('Turno pasado al lugar. Llamando siguiente…', 'success', 2000);
const ph = document.getElementById('ficha-placeholder');
ph.classList.remove('d-none'); // Llamar siguiente automáticamente después de 1.5 s
ph.innerHTML = ` setTimeout(() => {
<i class="fas fa-check-circle fa-3x mb-3" style="color:#22c55e"></i> cargarCola();
<p class="fw-semibold mb-1">Turno pasado al lugar correctamente</p> resetPlaceholder();
<small class="text-muted d-block mb-3">El paciente aparece en la cola del puesto de toma de muestras</small> // Auto-llamar siguiente turno si hay cola
<a href="<?= BASE_URL ?>erp.php?m=turnero&v=lugar" target="_blank" document.getElementById('ficha-placeholder').classList.remove('d-none');
class="btn btn-primary btn-sm"> document.getElementById('ficha-placeholder').innerHTML = `
<i class="fas fa-flask me-1"></i>Ir a Vista Lugar <i class="fas fa-hourglass-half fa-3x mb-3" style="color:#3b82f6"></i>
</a> <p class="fw-semibold mb-0">Llamando siguiente turno…</p>`;
<button class="btn btn-outline-secondary btn-sm ms-2" onclick="resetPlaceholder()"> llamarSiguiente();
Siguiente turno }, 1500);
</button>`;
} catch (err) { } catch (err) {
mostrarError(err.message); mostrarError(err.message);
@@ -925,10 +962,28 @@ function resetPlaceholder() {
} }
function mostrarError(msg) { function mostrarError(msg) {
alert('Error: ' + msg); mostrarToast(msg, 'error', 4000);
} }
function mostrarAviso(titulo, msg) { function mostrarAviso(titulo, msg) {
alert(titulo + '\n' + msg); mostrarToast(titulo + ': ' + msg, 'warn', 3500);
}
function mostrarLlamando(codigo) {
mostrarToast('Llamando turno ' + codigo + '…', 'info', 2000);
}
// ── Toast ────────────────────────────────────────────────────
let toastTimer = null;
function mostrarToast(msg, type = 'info', duration = 2500) {
const el = document.getElementById('rec-toast');
const ico = document.getElementById('toast-ico');
const txt = document.getElementById('toast-msg');
const icons = { success: 'fa-check-circle', info: 'fa-info-circle', warn: 'fa-exclamation-triangle', error: 'fa-times-circle' };
el.className = 'rec-toast ' + type;
ico.innerHTML = '<i class="fas ' + (icons[type] || icons.info) + '"></i>';
txt.textContent = msg;
el.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => el.classList.remove('show'), duration);
} }
</script> </script>
</body> </body>