Files
whatsapp/modules/turnero/views/display_global.php
T
Lizandro GuarnizoandClaude Sonnet 4.6 449b492eea fix: turno aparecía 3 veces en lista de llamados en display_global
El llamados.unshift no estaba protegido por callKey, así que el mismo
turno se agregaba una vez por cada escritorio de recepción. Ahora tanto
el anuncio como la entrada en la lista usan el mismo guard callKey.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 01:00:26 -05:00

741 lines
28 KiB
PHP

<?php
$_dispCfg = [];
try {
$__pdo = Database::getInstance()->getConnection();
$__rows = $__pdo->query(
"SELECT clave, valor FROM lab_config WHERE clave IN ('empresa_nombre','doc_logo_base64','doc_color','turnero_tv_video')"
)->fetchAll(PDO::FETCH_KEY_PAIR);
$_dispCfg = $__rows ?: [];
} catch (\Throwable $_) {}
$_labNombre = htmlspecialchars($_dispCfg['empresa_nombre'] ?? 'Sistema de Turnos');
$_labLogo = $_dispCfg['doc_logo_base64'] ?? '';
$_labColor = preg_match('/^#[0-9a-fA-F]{3,8}$/', $_dispCfg['doc_color'] ?? '') ? $_dispCfg['doc_color'] : '#1565c0';
$_tvVideo = $_dispCfg['turnero_tv_video'] ?? '';
if ($_tvVideo && !preg_match('#^https?://#', $_tvVideo)) { $_tvVideo = ''; }
$_hasVideo = (bool)$_tvVideo;
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pantalla Global de Turnos</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%; width: 100%;
overflow: hidden;
background: #f8fafc;
font-family: 'Segoe UI', system-ui, sans-serif;
color: #1e293b;
}
:root {
--brand: <?= $_labColor ?>;
--brand-glow: color-mix(in srgb, <?= $_labColor ?> 70%, transparent);
}
.pg-wrap {
display: grid;
grid-template-rows: auto 1fr auto;
height: 100vh;
overflow: hidden;
}
/* ── Header ── */
.pg-header {
display: flex; align-items: center; gap: 1rem;
padding: .7rem 1.8rem;
background: #fff;
border-bottom: 3px solid var(--brand);
flex-shrink: 0;
}
.hd-brand { display: flex; align-items: center; gap: .7rem; flex: 1; min-width: 0; }
.hd-brand .logo {
height: 44px; max-width: 110px; object-fit: contain;
background: #f1f5f9; border-radius: 8px; padding: 4px 7px;
}
.lab-nombre {
font-size: clamp(1.5rem, 2.6vw, 1.85rem);
font-weight: 700; color: #1e293b;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.hd-right { display: flex; align-items: center; gap: .8rem; flex-shrink: 0; }
.reloj {
font-size: clamp(1.7rem, 3.1vw, 2.15rem);
font-weight: 700; color: #64748b;
font-variant-numeric: tabular-nums; letter-spacing: .5px;
}
.live-dot {
width: 8px; height: 8px; border-radius: 50%;
background: #4ade80; box-shadow: 0 0 8px #4ade80;
animation: pdot 2s ease-in-out infinite;
}
@keyframes pdot { 0%,100%{opacity:1} 50%{opacity:.3} }
.btn-fs {
background: transparent; border: 1px solid #e2e8f0;
color: #64748b; border-radius: 8px; padding: 6px 10px;
font-size: 1.1rem; cursor: pointer; font-family: inherit;
transition: background .15s;
}
.btn-fs:hover { background: #f1f5f9; }
.btn-fs.on { background: #dcfce7; border-color: #86efac; color: #15803d; }
/* ── Background decoration ── */
.bg-deco {
position: fixed; inset: 0;
pointer-events: none; z-index: 0;
overflow: hidden;
}
.bg-deco .dot {
position: absolute;
border-radius: 50%;
background: var(--brand);
opacity: .04;
}
.bg-deco .dot:nth-child(1) { width: 300px; height: 300px; top: -80px; left: -60px; }
.bg-deco .dot:nth-child(2) { width: 180px; height: 180px; bottom: 10%; left: 15%; }
.bg-deco .dot:nth-child(3) { width: 120px; height: 120px; top: 30%; left: 60%; }
.bg-deco .dot:nth-child(4) { width: 240px; height: 240px; bottom: -60px; right: 30%; }
.bg-deco .dot:nth-child(5) { width: 80px; height: 80px; top: 15%; right: 25%; }
/* ── Main body ── */
.pg-body {
display: grid;
grid-template-columns: 1fr;
overflow: hidden;
position: relative;
}
.pg-body.has-video {
grid-template-columns: 25% 46% 29%;
}
/* ── Turno activo (centro) ── */
.turno-activo {
display: flex; flex-direction: column;
align-items: center; justify-content: center;
text-align: center;
padding: 1rem;
position: relative;
overflow: hidden;
flex: 1;
}
.turno-activo .label {
font-size: clamp(1.1rem, 1.7vw, 1.3rem);
font-weight: 700; text-transform: uppercase; letter-spacing: 4px;
color: #94a3b8;
margin-bottom: .2rem;
}
.turno-activo .codigo {
font-size: clamp(4rem, 14vw, 10rem);
font-weight: 900; line-height: 1;
letter-spacing: -4px;
color: var(--brand);
transition: color .3s;
font-variant-numeric: tabular-nums;
position: relative;
z-index: 1;
}
.pulse-ring {
position: absolute;
top: 50%; left: 50%;
width: 90vmin; height: 90vmin;
transform: translate(-50%, -50%);
border-radius: 50%;
border: 3px solid var(--brand);
opacity: 0;
pointer-events: none;
z-index: 0;
transition: opacity .6s ease;
}
.pulse-ring.active {
opacity: 1;
animation: pulse-ring 3s ease-in-out infinite;
}
@keyframes pulse-ring {
0% { transform: translate(-50%, -50%) scale(.75); opacity: .15; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: .08; }
100% { transform: translate(-50%, -50%) scale(.75); opacity: .15; }
}
.turno-activo .lugar-label {
font-size: clamp(1.5rem, 3vw, 2.5rem);
font-weight: 600;
color: #94a3b8;
margin-top: .3rem;
}
.turno-activo .lugar-nombre {
font-size: clamp(3rem, 10vw, 8rem);
font-weight: 800;
color: #1e293b;
margin-top: .1rem;
}
.turno-activo .pac-nombre {
font-size: clamp(1.15rem, 2.1vw, 1.45rem);
color: #64748b;
margin-top: .3rem;
}
.turno-activo .sin-turno {
font-size: clamp(1.45rem, 2.8vw, 2.05rem);
color: #cbd5e1;
}
/* ── Últimos llamados (columna izquierda) ── */
.left-area {
display: flex; flex-direction: column;
overflow: hidden;
position: relative;
background: color-mix(in srgb, var(--brand) 6%, #f8fafc);
border-right: 1px solid #e2e8f0;
}
.ultimos-llamados {
display: flex; flex-direction: column;
overflow: hidden;
height: 100%;
}
.ultimos-llamados .ul-label {
font-size: 1rem; font-weight: 700; text-transform: uppercase;
letter-spacing: 2px; color: #94a3b8;
padding: .65rem .85rem .3rem; flex-shrink: 0;
}
.ul-thead {
display: grid; grid-template-columns: 160px 1fr;
padding: .35rem 1rem;
background: color-mix(in srgb, var(--brand) 12%, #f1f5f9);
border-top: 1px solid #e2e8f0; border-bottom: 2px solid #e2e8f0;
font-size: 1.75rem; font-weight: 800;
text-transform: uppercase; letter-spacing: .1em; color: #64748b;
flex-shrink: 0;
}
.ultimos-llamados .ul-scroll {
flex: 1; overflow-y: auto; padding: .35rem .5rem .5rem;
scrollbar-width: thin; scrollbar-color: #e2e8f0 transparent;
}
.ul-item {
display: grid; grid-template-columns: 160px 1fr;
align-items: center;
padding: .5rem .65rem; border-radius: 10px;
margin-bottom: .35rem;
background: #fff; border: 1px solid #e2e8f0;
border-left: 4px 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;
}
.ul-item .ul-cod { font-weight: 800; font-size: 2.9rem; line-height: 1; }
.ul-item .ul-dest {
font-size: 2.68rem; font-weight: 700; color: #1e293b;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
line-height: 1.1;
}
.ul-item.activo .ul-dest { color: rgba(255,255,255,.85); }
/* ── Video reel ── */
.reel-wrap {
display: none;
height: 100%;
overflow: hidden;
padding: 8px 8px 8px 0;
}
.reel-wrap.has-video { display: block; }
.reel-wrap .reel-inner {
width: 100%; height: 100%;
border-radius: 16px;
overflow: hidden;
box-shadow: -8px 0 30px rgba(0,0,0,.08);
}
.reel-wrap video {
width: 100%; height: 100%;
object-fit: cover;
display: block;
}
/* ── Waiting queue ── */
.pg-cola {
background: #fff;
border-top: 1px solid #e2e8f0;
padding: .35rem 1rem;
display: flex;
align-items: center;
gap: .5rem;
overflow-x: auto;
flex-shrink: 0;
scrollbar-width: none;
min-height: 44px;
}
.pg-cola::-webkit-scrollbar { display: none; }
.cola-lbl {
font-size: .85rem; font-weight: 700;
text-transform: uppercase; letter-spacing: 1.5px;
color: #94a3b8; white-space: nowrap; flex-shrink: 0;
}
.cola-sep { width: 1px; height: 18px; background: #e2e8f0; flex-shrink: 0; }
.cola-chip {
display: inline-flex; align-items: center; gap: .25rem;
padding: .2rem .6rem; border-radius: 99px;
font-size: .97rem; font-weight: 700;
border: 1px solid #e2e8f0;
background: #f8fafc;
color: #475569;
white-space: nowrap; flex-shrink: 0;
}
.cola-chip .cdot { width: 5px; height: 5px; border-radius: 50%; display: inline-block; }
.cola-empty { font-size: .9rem; color: #cbd5e1; }
/* ── Announcement overlay ── */
#ann-overlay {
position: fixed; inset: 0; z-index: 9999;
display: flex; align-items: center; justify-content: center;
opacity: 0; pointer-events: none; transition: opacity .35s ease;
}
#ann-overlay.visible { opacity: 1; pointer-events: all; }
.ann-bg {
position: absolute; inset: 0;
background: rgba(4,8,20,.94);
backdrop-filter: blur(20px);
}
.ann-card {
position: relative; z-index: 1;
text-align: center;
transform: scale(.85) translateY(20px);
transition: transform .45s cubic-bezier(.34,1.5,.64,1);
}
#ann-overlay.visible .ann-card { transform: scale(1) translateY(0); }
.ann-label {
font-size: clamp(1.05rem, 1.8vw, 1.25rem); font-weight: 700;
text-transform: uppercase; letter-spacing: 4px;
color: rgba(255,255,255,.3); margin-bottom: .2rem;
}
.ann-code {
font-size: clamp(7rem, 22vw, 16rem); font-weight: 900;
line-height: 1; letter-spacing: -6px;
font-variant-numeric: tabular-nums;
color: var(--ac, var(--brand));
text-shadow: 0 0 80px var(--ac, var(--brand))66;
}
.ann-destino {
font-size: clamp(1.75rem, 4.3vw, 3.25rem); font-weight: 700;
color: #fff; margin-top: .2rem;
}
.ann-pac {
font-size: clamp(1.05rem, 1.8vw, 1.35rem);
color: rgba(255,255,255,.45); margin-top: .3rem;
}
.ann-progress {
width: 200px; height: 3px;
background: rgba(255,255,255,.06);
border-radius: 99px; margin: 1.5rem auto 0; overflow: hidden;
}
.ann-bar {
height: 100%; width: 100%; border-radius: 99px;
background: var(--ac, var(--brand));
box-shadow: 0 0 10px var(--ac, var(--brand));
}
/* ── Mini stat en footer ── */
.pg-footer {
position: fixed;
bottom: 0; left: 0; right: 0;
display: flex; justify-content: center; align-items: center;
gap: 1.5rem;
padding: .25rem 1rem;
background: #fff;
border-top: 1px solid #e2e8f0;
flex-shrink: 0;
z-index: 50;
}
.stat-item { text-align: center; }
.stat-item .val {
font-size: clamp(.95rem, 1.5vw, 1.1rem);
font-weight: 700; color: #475569;
font-variant-numeric: tabular-nums;
}
.stat-item .lbl {
font-size: .75rem; color: #94a3b8;
text-transform: uppercase; letter-spacing: 1px;
}
.stat-sep { width: 1px; height: 18px; background: #e2e8f0; }
</style>
</head>
<body>
<div class="bg-deco">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
<div id="ann-overlay">
<div class="ann-bg"></div>
<div class="ann-card">
<div class="ann-label">Pase a</div>
<div class="ann-code" id="ann-code">A01</div>
<div class="ann-destino" id="ann-destino">Recepción 1</div>
<div class="ann-pac" id="ann-pac"></div>
<div class="ann-progress"><div class="ann-bar" id="ann-bar"></div></div>
</div>
</div>
<div class="pg-wrap">
<header class="pg-header">
<div class="hd-brand">
<?php if ($_labLogo): ?>
<img class="logo" src="<?= htmlspecialchars($_labLogo) ?>" alt="Logo">
<?php endif; ?>
<div class="lab-nombre"><?= $_labNombre ?></div>
</div>
<div class="hd-right">
<div class="live-dot"></div>
<div class="reloj" id="reloj">--:--:--</div>
<button class="btn-fs" id="btn-sonido" onclick="activarSonido()" title="Activar sonido">
<i class="fas fa-volume-mute"></i>
</button>
<button class="btn-fs" onclick="toggleFullscreen()" title="Pantalla completa">
<i class="fas fa-expand" id="fs-icon"></i>
</button>
</div>
</header>
<div class="pg-body <?= $_hasVideo ? 'has-video' : '' ?>">
<div class="left-area">
<div class="ultimos-llamados" id="ultimos-llamados" style="display:none">
<div class="ul-label"><i class="fas fa-history me-1"></i>Llamados</div>
<div class="ul-thead">
<span>Turno</span>
<span>Lugar</span>
</div>
<div class="ul-scroll" id="ul-scroll"></div>
</div>
</div>
<div class="turno-activo" id="zona-activo">
<div class="pulse-ring" id="pulse-ring"></div>
<div class="label">Siguiente turno</div>
<div class="codigo" id="codigo-activo">—</div>
<div class="lugar-label" id="lugar-label">Llamado a</div>
<div class="lugar-nombre" id="lugar-nombre">—</div>
<div class="pac-nombre" id="pac-nombre"></div>
</div>
<div class="reel-wrap <?= $_hasVideo ? 'has-video' : '' ?>" id="reel-wrap">
<div class="reel-inner">
<video autoplay muted loop playsinline>
<source src="<?= htmlspecialchars($_tvVideo) ?>">
</video>
</div>
</div>
</div>
<div class="pg-cola" id="pg-cola">
<span class="cola-lbl">EN ESPERA</span>
<div class="cola-sep"></div>
<span class="cola-empty" id="cola-vacia">Cola vacía</span>
</div>
</div>
<footer class="pg-footer">
<div class="stat-item"><div class="val" id="st-espera">—</div><div class="lbl">Espera</div></div>
<div class="stat-sep"></div>
<div class="stat-item"><div class="val" id="st-atencion">—</div><div class="lbl">Atención</div></div>
<div class="stat-sep"></div>
<div class="stat-item"><div class="val" id="st-finalizados">—</div><div class="lbl">Atendidos</div></div>
<div class="stat-sep"></div>
<div class="stat-item"><div class="val" id="st-total">—</div><div class="lbl">Total día</div></div>
</footer>
<script>
const BASE_API = '<?= BASE_URL ?>modules/turnero/api/';
function tick() {
const d = new Date();
document.getElementById('reloj').textContent =
String(d.getHours()).padStart(2,'0') + ':' +
String(d.getMinutes()).padStart(2,'0') + ':' +
String(d.getSeconds()).padStart(2,'0');
}
tick(); setInterval(tick, 1000);
/* ── Audio ── */
let audioCtx = null, sonidoActivo = false;
function activarSonido() {
try {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
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-sonido');
if (btn) { btn.innerHTML = '<i class="fas fa-volume-up"></i>'; btn.classList.add('on'); }
setTimeout(playBeep, 100);
}
document.addEventListener('click', () => { if (!sonidoActivo) activarSonido(); }, { once: true });
document.addEventListener('touchstart', () => { if (!sonidoActivo) activarSonido(); }, { once: true });
function playBeep() {
if (!sonidoActivo || !audioCtx) return;
try {
const osc = audioCtx.createOscillator();
const g = audioCtx.createGain();
osc.type = 'sine';
osc.connect(g); g.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);
g.gain.setValueAtTime(0.35, audioCtx.currentTime);
g.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime + 0.65);
osc.start(audioCtx.currentTime);
osc.stop(audioCtx.currentTime + 0.65);
} catch (_) {}
}
// Selección de voz española — cargada una vez, reutilizada en cada anuncio
let _vozES = null;
function getVozES() {
if (_vozES) return _vozES;
const voices = window.speechSynthesis.getVoices();
// Preferencia: es-CO → es-419 → es-MX → es-US → es-ES → cualquier es-*
for (const lang of ['es-CO','es-419','es-MX','es-US','es-ES']) {
const v = voices.find(v => v.lang === lang);
if (v) { _vozES = v; return v; }
}
const v = voices.find(v => v.lang.startsWith('es'));
if (v) { _vozES = v; }
return _vozES;
}
if ('speechSynthesis' in window) {
window.speechSynthesis.onvoiceschanged = () => { _vozES = null; getVozES(); };
}
function anunciarTurno(codigo, destino, paciente) {
playBeep();
if ('speechSynthesis' in window) {
setTimeout(() => {
window.speechSynthesis.cancel();
const letras = codigo.split('').join(' ');
let texto = `Turno ${letras}`;
if (paciente) texto += `, ${paciente}`;
texto += `, pase a ${destino}`;
const utt = new SpeechSynthesisUtterance(texto);
utt.lang = 'es-CO';
const voz = getVozES();
if (voz) utt.voice = voz;
utt.rate = 0.85; utt.pitch = 1.05; utt.volume = 1;
window.speechSynthesis.speak(utt);
}, 800);
}
}
/* ── Fullscreen ── */
function toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(() => {});
} else {
document.exitFullscreen().catch(() => {});
}
}
document.addEventListener('fullscreenchange', () => {
const ico = document.getElementById('fs-icon');
if (ico) ico.className = document.fullscreenElement ? 'fas fa-compress' : 'fas fa-expand';
});
/* ── Announcement ── */
const annQueue = [];
let isAnnouncing = false;
function queueAnnouncement(item) {
if (annQueue.some(q => q._key === item._key)) return;
annQueue.push(item);
if (!isAnnouncing) processQueue();
}
function processQueue() {
if (annQueue.length === 0) { isAnnouncing = false; return; }
isAnnouncing = true;
showAnnouncement(annQueue.shift());
}
function showAnnouncement({ codigo, destino, paciente, color }) {
const ov = document.getElementById('ann-overlay');
const cd = document.getElementById('ann-code');
const dst = document.getElementById('ann-destino');
const pac = document.getElementById('ann-pac');
const bar = document.getElementById('ann-bar');
const c = color || '<?= $_labColor ?>';
cd.textContent = codigo;
dst.textContent = destino;
pac.textContent = paciente || '';
ov.style.setProperty('--ac', c);
ov.classList.add('visible');
anunciarTurno(codigo, destino, paciente);
bar.style.transition = 'none';
bar.style.width = '100%';
requestAnimationFrame(() => requestAnimationFrame(() => {
bar.style.transition = 'width 2s linear';
bar.style.width = '0%';
}));
setTimeout(() => {
ov.classList.remove('visible');
setTimeout(processQueue, 370);
}, 2300);
}
/* ── State ── */
let lastSeenKeys = new Set();
let announcedCalls = new Set(); // dedup por id+llamado_at, evita anunciar mismo turno desde varios escritorios
let llamados = [];
let _firstLoad = true;
function renderSnapshot(snap) {
const codEl = document.getElementById('codigo-activo');
const lugEl = document.getElementById('lugar-nombre');
const pacEl = document.getElementById('pac-nombre');
const lblEl = document.getElementById('lugar-label');
const ringEl = document.getElementById('pulse-ring');
// Recopilar TODOS los turnos activos: recepción + lugares
const allTurnos = [];
(snap.activos_recepcion || []).forEach(d => {
if (d.turno) allTurnos.push({ ...d.turno, destino: d.desk_nombre });
});
(snap.lugares || []).forEach(l => {
if (l.turno) allTurnos.push({ ...l.turno, destino: l.lugar_nombre });
});
// Detectar turnos nuevos y anunciar
const currentKeys = new Set();
let anuncioNuevo = false;
allTurnos.forEach(t => {
const key = t.codigo + '|' + t.destino + '|' + (t.llamado_at || '');
// Clave única por evento de llamada (id+llamado_at), independiente del escritorio
const callKey = (t.id || t.codigo) + '|' + (t.llamado_at || '');
currentKeys.add(key);
if (!lastSeenKeys.has(key)) {
const c = t.prioridad_color || '<?= $_labColor ?>';
const nomVoz = t.paciente_id ? t.paciente_nombre : null;
if (!announcedCalls.has(callKey)) {
announcedCalls.add(callKey);
if (!_firstLoad) {
queueAnnouncement({ _key: key, codigo: t.codigo, destino: t.destino, paciente: nomVoz, 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;
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 || '<?= $_labColor ?>';
const key = t.codigo + '|' + t.destino + '|' + (t.llamado_at || '');
const callKey = (t.id || t.codigo) + '|' + (t.llamado_at || '');
const nomVoz = t.paciente_id ? t.paciente_nombre : null;
announcedCalls.add(callKey);
queueAnnouncement({ _key: key, codigo: t.codigo, destino: t.destino, paciente: nomVoz, color: c });
}
}
if (anuncioNuevo) renderLlamados();
// zona-activo: mostrar el turno llamado más recientemente
const sorted = allTurnos.slice().sort((a, b) => {
return new Date(b.llamado_at || 0) - new Date(a.llamado_at || 0);
});
if (sorted.length > 0) {
const t = sorted[0];
codEl.textContent = t.codigo;
lugEl.textContent = t.destino;
pacEl.textContent = t.paciente_nombre || '';
lblEl.textContent = 'Llamado a';
ringEl.classList.add('active');
const c = t.prioridad_color || '<?= $_labColor ?>';
codEl.style.color = c;
document.getElementById('zona-activo').style.setProperty('--brand', c);
} else {
codEl.textContent = '—';
lugEl.textContent = '—';
pacEl.textContent = '';
lblEl.textContent = 'Llamado a';
codEl.style.color = '<?= $_labColor ?>';
ringEl.classList.remove('active');
}
// Queue
const cola = snap.cola || [];
const pgCola = document.getElementById('pg-cola');
pgCola.querySelectorAll('.cola-chip').forEach(c => c.remove());
const vaciaMg = document.getElementById('cola-vacia');
if (cola.length === 0) {
vaciaMg.style.display = '';
} else {
vaciaMg.style.display = 'none';
cola.forEach(t => {
const chip = document.createElement('span');
const c = t.prioridad_color || '<?= $_labColor ?>';
chip.className = 'cola-chip';
chip.innerHTML = '<span class="cdot" style="background:' + c + ';box-shadow:0 0 4px ' + c + '88"></span>' + esc(t.codigo);
pgCola.appendChild(chip);
});
}
renderLlamados();
// Stats
const s = snap.stats || {};
document.getElementById('st-espera').textContent = s.en_espera || 0;
document.getElementById('st-atencion').textContent = s.en_atencion || 0;
document.getElementById('st-finalizados').textContent = s.finalizados || 0;
document.getElementById('st-total').textContent = s.total || 0;
}
function renderLlamados() {
const wrap = document.getElementById('ultimos-llamados');
const sc = document.getElementById('ul-scroll');
if (!wrap || !sc) return;
if (llamados.length === 0) { wrap.style.display = 'none'; return; }
wrap.style.display = '';
sc.innerHTML = llamados.map((l, i) => {
const isActivo = i === 0;
const c = l.color || '<?= $_labColor ?>';
return `<div class="ul-item${isActivo ? ' activo' : ''}"${isActivo ? ` style="--brand:${c}"` : ''}>
<span class="ul-cod">${esc(l.codigo)}</span>
<span class="ul-dest">${esc(l.destino)}</span>
</div>`;
}).join('');
}
function esc(s) {
const d = document.createElement('div');
d.appendChild(document.createTextNode(String(s ?? '')));
return d.innerHTML;
}
async function cargarSnapshot() {
try {
const res = await fetch(BASE_API + 'get_display_global.php', { cache: 'no-store' });
const json = await res.json();
if (json.ok) renderSnapshot(json);
} catch(_) {}
}
cargarSnapshot();
setInterval(cargarSnapshot, 2000);
</script>
</body>
</html>