This commit is contained in:
Lizandro Guarnizo
2026-06-16 15:37:34 -05:00
parent 7f84c7f3ae
commit acbc1d0714
2 changed files with 91 additions and 90 deletions
+47 -29
View File
@@ -195,42 +195,43 @@ $_hasVideo = (bool)$_tvVideo;
height: 100%; height: 100%;
} }
.ultimos-llamados .ul-label { .ultimos-llamados .ul-label {
font-size: 1.2rem; font-weight: 700; text-transform: uppercase; font-size: 1rem; font-weight: 700; text-transform: uppercase;
letter-spacing: 2px; color: #94a3b8; letter-spacing: 2px; color: #94a3b8;
padding: .8rem .8rem .3rem; flex-shrink: 0; padding: .65rem .85rem .3rem; flex-shrink: 0;
}
.ul-thead {
display: grid; grid-template-columns: 82px 1fr;
padding: .25rem .85rem;
background: color-mix(in srgb, var(--brand) 12%, #f1f5f9);
border-top: 1px solid #e2e8f0; border-bottom: 2px solid #e2e8f0;
font-size: .62rem; font-weight: 800;
text-transform: uppercase; letter-spacing: .1em; color: #64748b;
flex-shrink: 0;
} }
.ultimos-llamados .ul-scroll { .ultimos-llamados .ul-scroll {
flex: 1; overflow-y: auto; padding: 0 .5rem .5rem; flex: 1; overflow-y: auto; padding: .3rem .4rem .5rem;
scrollbar-width: thin; scrollbar-color: #e2e8f0 transparent; scrollbar-width: thin; scrollbar-color: #e2e8f0 transparent;
} }
.ul-item { .ul-item {
display: flex; align-items: center; gap: .45rem; display: grid; grid-template-columns: 82px 1fr;
padding: .5rem .75rem; border-radius: 10px; align-items: center;
margin-bottom: .35rem; padding: .38rem .45rem; border-radius: 8px;
margin-bottom: .25rem;
background: #fff; border: 1px solid #e2e8f0; background: #fff; border: 1px solid #e2e8f0;
border-left: 4px solid color-mix(in srgb, var(--brand, #1565c0) 50%, transparent); border-left: 3px solid color-mix(in srgb, var(--brand, #1565c0) 55%, transparent);
} }
.ul-item.activo { .ul-item.activo {
background: var(--brand); background: var(--brand);
border-color: var(--brand); border-color: var(--brand);
border-left-color: rgba(255,255,255,.4);
color: #fff; color: #fff;
border-left-color: #fff;
}
.ul-item.activo .ul-nom,
.ul-item.activo .ul-dest { color: rgba(255,255,255,.75); }
.ul-item .ul-left { flex: 0 0 auto; min-width: 0; }
.ul-item .ul-cod { font-weight: 800; font-size: 1.55rem; display: block; }
.ul-item .ul-nom {
font-size: .8rem; font-weight: 500;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
display: block;
} }
.ul-item .ul-cod { font-weight: 800; font-size: 1.45rem; line-height: 1; }
.ul-item .ul-dest { .ul-item .ul-dest {
font-size: 1.1rem; font-weight: 700; color: #1e293b; font-size: .92rem; font-weight: 600; color: #1e293b;
flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
} }
.ul-item.activo .ul-dest { color: #fff; } .ul-item.activo .ul-dest { color: rgba(255,255,255,.85); }
.ul-item .ul-dest i { margin-right: 3px; }
/* ── Video reel ── */ /* ── Video reel ── */
.reel-wrap { .reel-wrap {
@@ -405,6 +406,10 @@ $_hasVideo = (bool)$_tvVideo;
<div class="left-area"> <div class="left-area">
<div class="ultimos-llamados" id="ultimos-llamados" style="display:none"> <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-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 class="ul-scroll" id="ul-scroll"></div>
</div> </div>
</div> </div>
@@ -565,8 +570,9 @@ function showAnnouncement({ codigo, destino, paciente, color }) {
} }
/* ── State ── */ /* ── State ── */
let lastSeenKeys = new Set(); // clave por turno+destino+llamado_at let lastSeenKeys = new Set();
let llamados = []; let llamados = [];
let _firstLoad = true;
function renderSnapshot(snap) { function renderSnapshot(snap) {
const codEl = document.getElementById('codigo-activo'); const codEl = document.getElementById('codigo-activo');
@@ -584,7 +590,7 @@ function renderSnapshot(snap) {
if (l.turno) allTurnos.push({ ...l.turno, destino: l.lugar_nombre }); if (l.turno) allTurnos.push({ ...l.turno, destino: l.lugar_nombre });
}); });
// Detectar turno nuevo en CUALQUIER estación y anunciar // Detectar turnos nuevos y anunciar
const currentKeys = new Set(); const currentKeys = new Set();
let anuncioNuevo = false; let anuncioNuevo = false;
allTurnos.forEach(t => { allTurnos.forEach(t => {
@@ -592,13 +598,29 @@ function renderSnapshot(snap) {
currentKeys.add(key); currentKeys.add(key);
if (!lastSeenKeys.has(key)) { if (!lastSeenKeys.has(key)) {
const c = t.prioridad_color || '<?= $_labColor ?>'; const c = t.prioridad_color || '<?= $_labColor ?>';
// En primera carga silenciar todos; el más reciente se anuncia abajo
if (!_firstLoad) {
queueAnnouncement({ _key: key, codigo: t.codigo, destino: t.destino, paciente: t.paciente_nombre, color: c }); queueAnnouncement({ _key: key, codigo: t.codigo, destino: t.destino, paciente: t.paciente_nombre, color: c });
}
llamados.unshift({ codigo: t.codigo, destino: t.destino, paciente: t.paciente_nombre, color: c }); llamados.unshift({ codigo: t.codigo, destino: t.destino, paciente: t.paciente_nombre, color: c });
if (llamados.length > 30) llamados.pop(); if (llamados.length > 30) llamados.pop();
anuncioNuevo = true; anuncioNuevo = true;
} }
}); });
lastSeenKeys = currentKeys; lastSeenKeys = currentKeys;
if (_firstLoad) {
_firstLoad = false;
// Anunciar solo el turno más reciente al cargar la página
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 || '');
queueAnnouncement({ _key: key, codigo: t.codigo, destino: t.destino, paciente: t.paciente_nombre, color: c });
}
}
if (anuncioNuevo) renderLlamados(); if (anuncioNuevo) renderLlamados();
// zona-activo: mostrar el turno llamado más recientemente // zona-activo: mostrar el turno llamado más recientemente
@@ -659,16 +681,12 @@ function renderLlamados() {
if (!wrap || !sc) return; if (!wrap || !sc) return;
if (llamados.length === 0) { wrap.style.display = 'none'; return; } if (llamados.length === 0) { wrap.style.display = 'none'; return; }
wrap.style.display = ''; wrap.style.display = '';
const activo = llamados[0];
sc.innerHTML = llamados.map((l, i) => { sc.innerHTML = llamados.map((l, i) => {
const isActivo = i === 0; const isActivo = i === 0;
const c = l.color || '<?= $_labColor ?>'; const c = l.color || '<?= $_labColor ?>';
return `<div class="ul-item${isActivo ? ' activo' : ''}"${isActivo ? ' style="--brand:' + c + '"' : ''}> return `<div class="ul-item${isActivo ? ' activo' : ''}"${isActivo ? ` style="--brand:${c}"` : ''}>
<div class="ul-left">
<span class="ul-cod">${esc(l.codigo)}</span> <span class="ul-cod">${esc(l.codigo)}</span>
${l.paciente ? `<span class="ul-nom">${esc(l.paciente)}</span>` : ''} <span class="ul-dest">${esc(l.destino)}</span>
</div>
<span class="ul-dest"><i class="fas fa-arrow-right"></i>${esc(l.destino)}</span>
</div>`; </div>`;
}).join(''); }).join('');
} }
+38 -55
View File
@@ -385,57 +385,6 @@ const API = '<?= BASE_URL ?>modules/turnero/api/';
const BASE_WA = '<?= BASE_URL ?>'; const BASE_WA = '<?= BASE_URL ?>';
const LUGAR_NOMBRE = document.getElementById('lbl-lugar-titulo')?.textContent || '<?= addslashes($lugarNombre) ?>'; const LUGAR_NOMBRE = document.getElementById('lbl-lugar-titulo')?.textContent || '<?= addslashes($lugarNombre) ?>';
// ── Audio / anuncio ───────────────────────────────────────────
let _audioCtx = null;
let _sonidoListo = false;
function _desbloquearAudio() {
if (_sonidoListo) return;
try {
_audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const g = _audioCtx.createGain(); g.gain.value = 0.001;
const o = _audioCtx.createOscillator();
o.connect(g); g.connect(_audioCtx.destination);
o.start(); o.stop(_audioCtx.currentTime + 0.05);
} catch (_) {}
_sonidoListo = true;
}
document.addEventListener('click', _desbloquearAudio, { once: false });
document.addEventListener('touchstart', _desbloquearAudio, { once: false });
function _playBeep() {
if (!_audioCtx) return;
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) {
_desbloquearAudio();
_playBeep();
if ('speechSynthesis' in window) {
setTimeout(() => {
window.speechSynthesis.cancel();
const letras = codigo.split('').join(' ');
const utt = new SpeechSynthesisUtterance(`Turno ${letras}, pase a ${LUGAR_NOMBRE}`);
utt.lang = 'es-CO';
utt.rate = 0.85;
utt.pitch = 1.05;
utt.volume = 1;
window.speechSynthesis.speak(utt);
}, 700);
}
}
// ── Arranque ────────────────────────────────────────────────── // ── Arranque ──────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
@@ -564,7 +513,6 @@ async function rellamarDesdeCard(turnoId) {
}); });
const json = await res.json(); const json = await res.json();
if (!json.ok || !json.turno) return; if (!json.ok || !json.turno) return;
anunciarTurno(json.turno.codigo);
await abrirFicha(json.turno); await abrirFicha(json.turno);
} }
@@ -594,7 +542,6 @@ async function _llamar(extra) {
if (!json.ok) { mostrarError(json.error); return; } if (!json.ok) { mostrarError(json.error); return; }
if (!json.turno) { mostrarToast('Cola vacía — no hay turnos en espera.', 'warn', 3000); return; } if (!json.turno) { mostrarToast('Cola vacía — no hay turnos en espera.', 'warn', 3000); return; }
mostrarLlamando(json.turno.codigo); mostrarLlamando(json.turno.codigo);
anunciarTurno(json.turno.codigo);
await abrirFicha(json.turno); await abrirFicha(json.turno);
cargarCola(); cargarCola();
} catch (err) { } catch (err) {
@@ -642,7 +589,7 @@ async function cargarFichaSolicitud(turnoId) {
const sol = json.solicitud; const sol = json.solicitud;
const pac = json.paciente; const pac = json.paciente;
const exams = json.examenes || []; const exams = json.examenes || [];
const consts = json.consentimientos || []; let consts = json.consentimientos || [];
// Datos del paciente // Datos del paciente
if (pac) { if (pac) {
@@ -669,6 +616,42 @@ async function cargarFichaSolicitud(turnoId) {
listaEx.innerHTML = '<span class="text-muted small">Sin exámenes registrados</span>'; listaEx.innerHTML = '<span class="text-muted small">Sin exámenes registrados</span>';
} }
// Auto-crear tokens para formularios del lugar que aún no tengan registro
if (lugarId) {
const resF = await fetch(`${API}get_lugar_formularios.php?lugar_id=${lugarId}`);
const jsonF = await resF.json();
const forms = (jsonF.ok && jsonF.formularios) ? jsonF.formularios : [];
const existIds = new Set(consts.map(c => parseInt(c.formulario_id)));
const faltantes = forms.filter(f => !existIds.has(parseInt(f.id)));
if (faltantes.length > 0) {
await Promise.all(faltantes.map(f =>
fetch(API + 'create_consent_token.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ turno_id: turnoId, formulario_id: f.id }),
}).catch(() => {})
));
// Recargar consents tras la creación
const res2 = await fetch(`${API}get_consentimientos.php?turno_id=${turnoId}`);
const json2 = await res2.json();
if (json2.ok) consts = json2.consentimientos || [];
// Si alguno no se pudo crear (sin paciente), mostrarlo igual como pendiente visual
const createdIds = new Set(consts.map(c => parseInt(c.formulario_id)));
faltantes.forEach(f => {
if (!createdIds.has(parseInt(f.id))) {
consts.push({
id: null, turno_id: turnoId,
formulario_id: f.id, formulario_nombre: f.nombre,
estado: 'pendiente', token: null,
});
}
});
}
}
// Consentimientos // Consentimientos
renderConsentimientos(consts); renderConsentimientos(consts);
@@ -921,7 +904,7 @@ async function rellamarTurno() {
}); });
const json = await res.json(); const json = await res.json();
if (!json.ok) mostrarError(json.error); if (!json.ok) mostrarError(json.error);
else if (json.turno) { mostrarLlamando(json.turno.codigo); anunciarTurno(json.turno.codigo); } else if (json.turno) { mostrarLlamando(json.turno.codigo); }
} catch (err) { } catch (err) {
mostrarError(err.message); mostrarError(err.message);
} finally { } finally {