up
This commit is contained in:
@@ -0,0 +1,378 @@
|
||||
<?php
|
||||
/**
|
||||
* display_recepcion.php — Pantalla TV por escritorio de Recepción
|
||||
* Acceso: erp.php?m=turnero&v=display_recepcion&desk_id=X
|
||||
* Sin autenticación (pantalla pública de TV).
|
||||
*/
|
||||
|
||||
$_deskId = (int)($_GET['desk_id'] ?? 0);
|
||||
$_deskNom = 'Recepción';
|
||||
$_labNom = 'Sistema de Turnos';
|
||||
$_labLogo = '';
|
||||
$_labColor = '#1565c0';
|
||||
|
||||
try {
|
||||
$_pdo = Database::getInstance()->getConnection();
|
||||
|
||||
// Nombre del escritorio
|
||||
if ($_deskId) {
|
||||
$_row = $_pdo->prepare("SELECT nombre FROM turnero_lugares WHERE id = ? AND tipo='recepcion' AND activo=1");
|
||||
$_row->execute([$_deskId]);
|
||||
$_found = $_row->fetchColumn();
|
||||
if ($_found) $_deskNom = $_found;
|
||||
}
|
||||
|
||||
// Config visual del lab
|
||||
$_cfg = $_pdo->query(
|
||||
"SELECT clave, valor FROM lab_config WHERE clave IN ('empresa_nombre','doc_logo_base64','doc_color')"
|
||||
)->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
$_labNom = htmlspecialchars($_cfg['empresa_nombre'] ?? 'Sistema de Turnos');
|
||||
$_labLogo = $_cfg['doc_logo_base64'] ?? '';
|
||||
if (preg_match('/^#[0-9a-fA-F]{3,8}$/', $_cfg['doc_color'] ?? '')) {
|
||||
$_labColor = $_cfg['doc_color'];
|
||||
}
|
||||
} catch (\Throwable $_) {}
|
||||
|
||||
$_deskNomSafe = htmlspecialchars($_deskNom);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= $_deskNomSafe ?> — Pantalla TV</title>
|
||||
<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; }
|
||||
html, body {
|
||||
margin: 0; padding: 0; height: 100%; width: 100%;
|
||||
overflow: hidden;
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
background: #f0f4f8; color: #1e293b;
|
||||
}
|
||||
:root {
|
||||
--brand: <?= $_labColor ?>;
|
||||
--prio-color: <?= $_labColor ?>;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.pg-wrap {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.pg-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: .7rem 2rem;
|
||||
background: var(--brand); gap: 1rem; flex-shrink: 0;
|
||||
}
|
||||
.header-brand { display: flex; align-items: center; gap: .8rem; flex: 1; min-width: 0; }
|
||||
.header-brand img.logo {
|
||||
height: 44px; max-width: 120px; object-fit: contain;
|
||||
background: rgba(255,255,255,.12); border-radius: 8px; padding: 4px 6px;
|
||||
}
|
||||
.lab-nombre {
|
||||
font-size: clamp(.9rem, 2vw, 1.3rem); font-weight: 800; color: #fff;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.lab-sub {
|
||||
font-size: .68rem; color: rgba(255,255,255,.7); font-weight: 500;
|
||||
text-transform: uppercase; letter-spacing: 1.5px; display: block; margin-top: 1px;
|
||||
}
|
||||
.reloj {
|
||||
font-size: clamp(1rem, 2.5vw, 1.6rem); font-weight: 800; color: #fff;
|
||||
font-variant-numeric: tabular-nums; letter-spacing: 1px;
|
||||
}
|
||||
.btn-sonido {
|
||||
background: rgba(255,255,255,.15); border: 1px solid rgba(255,255,255,.3);
|
||||
color: #fff; border-radius: 8px; padding: 5px 12px; font-size: .78rem; cursor: pointer;
|
||||
}
|
||||
|
||||
/* Cuerpo central */
|
||||
.pg-body {
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
padding: 2rem; overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Tarjeta principal del turno */
|
||||
.turno-card {
|
||||
width: min(600px, 88vw);
|
||||
background: #fff;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 8px 40px rgba(0,0,0,.12);
|
||||
border-top: 6px solid var(--prio-color);
|
||||
padding: 3rem 3.5rem;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
transition: border-color .4s, background .4s;
|
||||
}
|
||||
.turno-card::before {
|
||||
content: '';
|
||||
position: absolute; inset: 0; border-radius: 24px;
|
||||
border: 3px solid var(--prio-color);
|
||||
opacity: .3; pointer-events: none;
|
||||
}
|
||||
|
||||
/* Etiqueta destino */
|
||||
.lbl-destino {
|
||||
font-size: clamp(.85rem, 2vw, 1.1rem);
|
||||
font-weight: 600; color: #64748b;
|
||||
text-transform: uppercase; letter-spacing: 2px;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
/* Ring de animación */
|
||||
.ring {
|
||||
position: absolute; top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 0; height: 0;
|
||||
border-radius: 50%;
|
||||
pointer-events: none; z-index: 0;
|
||||
background: transparent;
|
||||
}
|
||||
.ring.animar {
|
||||
animation: ringPulse 1.4s ease-out forwards;
|
||||
background: var(--prio-color);
|
||||
}
|
||||
@keyframes ringPulse {
|
||||
0% { width: 0; height: 0; opacity: .5; }
|
||||
100% { width: 120%; height: 120%; opacity: 0; margin: -60% 0 0 -60%; }
|
||||
}
|
||||
|
||||
/* Código grande */
|
||||
.codigo-num {
|
||||
font-size: clamp(5rem, 18vw, 12rem);
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
color: var(--prio-color);
|
||||
letter-spacing: -4px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
position: relative; z-index: 1;
|
||||
transition: color .4s;
|
||||
}
|
||||
.codigo-num.vacio { color: #cbd5e1; }
|
||||
|
||||
/* Nombre paciente */
|
||||
.pac-nombre {
|
||||
font-size: clamp(1rem, 2.5vw, 1.6rem);
|
||||
font-weight: 600; color: #334155;
|
||||
margin-top: .5rem; min-height: 1.8em;
|
||||
position: relative; z-index: 1;
|
||||
}
|
||||
|
||||
/* Chip prioridad */
|
||||
.prio-chip {
|
||||
display: inline-flex; align-items: center; gap: .4rem;
|
||||
background: #f1f5f9; color: #94a3b8;
|
||||
border-radius: 50px; padding: .4rem 1rem;
|
||||
font-size: .85rem; font-weight: 600;
|
||||
margin-top: 1.2rem; transition: background .4s, color .4s;
|
||||
position: relative; z-index: 1;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.pg-footer {
|
||||
display: flex; align-items: center; justify-content: center; gap: 2.5rem;
|
||||
padding: .6rem 2rem;
|
||||
background: #1e293b; flex-shrink: 0;
|
||||
}
|
||||
.stat-item { text-align: center; }
|
||||
.stat-item .val {
|
||||
font-size: clamp(.9rem, 2vw, 1.3rem); font-weight: 800; color: #f1f5f9;
|
||||
}
|
||||
.stat-item .lbl { font-size: .65rem; color: #94a3b8; text-transform: uppercase; letter-spacing: 1px; }
|
||||
.stat-sep { width: 1px; height: 28px; background: rgba(255,255,255,.12); }
|
||||
|
||||
/* Sin escritorio */
|
||||
.no-desk {
|
||||
text-align: center; color: #64748b; padding: 3rem;
|
||||
}
|
||||
.no-desk i { font-size: 3rem; color: #cbd5e1; margin-bottom: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body onclick="activarSonido()">
|
||||
<div class="pg-wrap">
|
||||
|
||||
<!-- Header -->
|
||||
<header class="pg-header">
|
||||
<div class="header-brand">
|
||||
<?php if ($_labLogo): ?>
|
||||
<img class="logo" src="<?= $_labLogo ?>" alt="Logo">
|
||||
<?php endif; ?>
|
||||
<div>
|
||||
<div class="lab-nombre"><?= $_labNom ?></div>
|
||||
<span class="lab-sub"><?= $_deskNomSafe ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:.75rem">
|
||||
<button class="btn-sonido" id="btn-sonido">
|
||||
<i class="fas fa-volume-mute"></i> Sonido
|
||||
</button>
|
||||
<div class="reloj" id="reloj">--:--:--</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Cuerpo -->
|
||||
<main class="pg-body">
|
||||
<?php if (!$_deskId): ?>
|
||||
<div class="no-desk">
|
||||
<i class="fas fa-exclamation-circle d-block"></i>
|
||||
<p class="fw-semibold">No se especificó un escritorio de recepción.</p>
|
||||
<small>Usa <code>?desk_id=X</code> en la URL.</small>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="turno-card" id="turno-card">
|
||||
<div class="ring" id="ring-turno"></div>
|
||||
<div class="lbl-destino">Pase a <?= $_deskNomSafe ?></div>
|
||||
<div class="codigo-num vacio" id="cod-turno">—</div>
|
||||
<div class="pac-nombre" id="pac-turno"></div>
|
||||
<div class="prio-chip" id="chip-turno">
|
||||
<i class="fas fa-hourglass-half"></i> En espera
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
|
||||
<!-- Footer stats -->
|
||||
<footer class="pg-footer">
|
||||
<div class="stat-item">
|
||||
<div class="val" id="st-espera">—</div>
|
||||
<div class="lbl">En espera</div>
|
||||
</div>
|
||||
<div class="stat-sep"></div>
|
||||
<div class="stat-item">
|
||||
<div class="val" id="st-atencion">—</div>
|
||||
<div class="lbl">En atención</div>
|
||||
</div>
|
||||
<div class="stat-sep"></div>
|
||||
<div class="stat-item">
|
||||
<div class="val" id="st-finalizados">—</div>
|
||||
<div class="lbl">Finalizados</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const BASE_API = '<?= BASE_URL ?>modules/turnero/api/';
|
||||
const DESK_ID = <?= $_deskId ?: 'null' ?>;
|
||||
const DESK_NOM = <?= json_encode($_deskNom) ?>;
|
||||
let sonidoActivo = false;
|
||||
let lastClave = null; // codigo|llamado_at
|
||||
|
||||
// ── Reloj ─────────────────────────────────────────────────────
|
||||
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);
|
||||
|
||||
// ── Activar TTS ───────────────────────────────────────────────
|
||||
function activarSonido() {
|
||||
if (sonidoActivo) return;
|
||||
sonidoActivo = true;
|
||||
document.getElementById('btn-sonido').innerHTML = '<i class="fas fa-volume-up"></i> Sonido';
|
||||
document.getElementById('btn-sonido').style.background = 'rgba(255,255,255,.35)';
|
||||
}
|
||||
document.getElementById('btn-sonido').addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
activarSonido();
|
||||
});
|
||||
|
||||
// ── TTS ───────────────────────────────────────────────────────
|
||||
function anunciarTurno(codigo) {
|
||||
if (!sonidoActivo || !('speechSynthesis' in window)) return;
|
||||
setTimeout(() => {
|
||||
window.speechSynthesis.cancel();
|
||||
const letras = codigo.split('').join(' ');
|
||||
const utt = new SpeechSynthesisUtterance(`Turno ${letras}, pase a ${DESK_NOM}`);
|
||||
utt.lang = 'es-CO'; utt.rate = 0.85; utt.pitch = 1.05; utt.volume = 1;
|
||||
window.speechSynthesis.speak(utt);
|
||||
}, 700);
|
||||
}
|
||||
|
||||
// ── Escape HTML ───────────────────────────────────────────────
|
||||
function esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.appendChild(document.createTextNode(s));
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
// ── Render ────────────────────────────────────────────────────
|
||||
function render(snap) {
|
||||
const t = snap.turno;
|
||||
const card = document.getElementById('turno-card');
|
||||
const ring = document.getElementById('ring-turno');
|
||||
const cod = document.getElementById('cod-turno');
|
||||
const pac = document.getElementById('pac-turno');
|
||||
const chip = document.getElementById('chip-turno');
|
||||
if (!card) return;
|
||||
|
||||
if (t) {
|
||||
const color = t.prioridad_color || '#1565c0';
|
||||
const clave = t.codigo + '|' + (t.llamado_at || '');
|
||||
|
||||
// Actualizar colores
|
||||
card.style.setProperty('--prio-color', color);
|
||||
card.style.background = '#fff';
|
||||
card.style.borderTopColor = color;
|
||||
chip.style.background = color + '22';
|
||||
chip.style.color = color;
|
||||
|
||||
cod.textContent = t.codigo;
|
||||
cod.className = 'codigo-num';
|
||||
cod.style.color = '';
|
||||
pac.textContent = t.paciente_nombre || '';
|
||||
chip.innerHTML = `<i class='fas fa-ticket-alt'></i> ${esc(t.prioridad_codigo)} — ${esc(t.prioridad_nombre)}`;
|
||||
|
||||
if (clave !== lastClave) {
|
||||
anunciarTurno(t.codigo);
|
||||
ring.classList.remove('animar');
|
||||
void ring.offsetWidth;
|
||||
ring.classList.add('animar');
|
||||
lastClave = clave;
|
||||
}
|
||||
} else {
|
||||
card.style.removeProperty('--prio-color');
|
||||
card.style.background = '#fff';
|
||||
card.style.borderTopColor = '#e2e8f0';
|
||||
chip.style.background = '#f1f5f9';
|
||||
chip.style.color = '#94a3b8';
|
||||
|
||||
cod.textContent = '—';
|
||||
cod.className = 'codigo-num vacio';
|
||||
pac.textContent = '';
|
||||
chip.innerHTML = '<i class="fas fa-hourglass-half"></i> En espera';
|
||||
lastClave = null;
|
||||
}
|
||||
|
||||
// Stats footer
|
||||
const s = snap.stats || {};
|
||||
const el = id => document.getElementById(id);
|
||||
el('st-espera').textContent = s.en_espera || 0;
|
||||
el('st-atencion').textContent = s.en_atencion || 0;
|
||||
el('st-finalizados').textContent= s.finalizados || 0;
|
||||
}
|
||||
|
||||
// ── Polling ───────────────────────────────────────────────────
|
||||
async function cargar() {
|
||||
if (!DESK_ID) return;
|
||||
try {
|
||||
const res = await fetch(BASE_API + 'get_display_recepcion.php?desk_id=' + DESK_ID, { cache: 'no-store' });
|
||||
const json = await res.json();
|
||||
if (json.ok) render(json);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
cargar();
|
||||
setInterval(cargar, 4000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user