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
+50 -32
View File
@@ -195,42 +195,43 @@ $_hasVideo = (bool)$_tvVideo;
height: 100%;
}
.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;
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 {
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;
}
.ul-item {
display: flex; align-items: center; gap: .45rem;
padding: .5rem .75rem; border-radius: 10px;
margin-bottom: .35rem;
display: grid; grid-template-columns: 82px 1fr;
align-items: center;
padding: .38rem .45rem; border-radius: 8px;
margin-bottom: .25rem;
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 {
background: var(--brand);
border-color: var(--brand);
border-left-color: rgba(255,255,255,.4);
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 {
font-size: 1.1rem; font-weight: 700; color: #1e293b;
flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
font-size: .92rem; font-weight: 600; color: #1e293b;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ul-item.activo .ul-dest { color: #fff; }
.ul-item .ul-dest i { margin-right: 3px; }
.ul-item.activo .ul-dest { color: rgba(255,255,255,.85); }
/* ── Video reel ── */
.reel-wrap {
@@ -405,6 +406,10 @@ $_hasVideo = (bool)$_tvVideo;
<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>
@@ -565,8 +570,9 @@ function showAnnouncement({ codigo, destino, paciente, color }) {
}
/* ── State ── */
let lastSeenKeys = new Set(); // clave por turno+destino+llamado_at
let llamados = [];
let lastSeenKeys = new Set();
let llamados = [];
let _firstLoad = true;
function renderSnapshot(snap) {
const codEl = document.getElementById('codigo-activo');
@@ -584,7 +590,7 @@ function renderSnapshot(snap) {
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();
let anuncioNuevo = false;
allTurnos.forEach(t => {
@@ -592,13 +598,29 @@ function renderSnapshot(snap) {
currentKeys.add(key);
if (!lastSeenKeys.has(key)) {
const c = t.prioridad_color || '<?= $_labColor ?>';
queueAnnouncement({ _key: key, codigo: t.codigo, destino: t.destino, paciente: t.paciente_nombre, color: c });
// 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 });
}
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;
// 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();
// zona-activo: mostrar el turno llamado más recientemente
@@ -659,16 +681,12 @@ function renderLlamados() {
if (!wrap || !sc) return;
if (llamados.length === 0) { wrap.style.display = 'none'; return; }
wrap.style.display = '';
const activo = llamados[0];
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 + '"' : ''}>
<div class="ul-left">
<span class="ul-cod">${esc(l.codigo)}</span>
${l.paciente ? `<span class="ul-nom">${esc(l.paciente)}</span>` : ''}
</div>
<span class="ul-dest"><i class="fas fa-arrow-right"></i>${esc(l.destino)}</span>
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('');
}
+41 -58
View File
@@ -385,57 +385,6 @@ const API = '<?= BASE_URL ?>modules/turnero/api/';
const BASE_WA = '<?= BASE_URL ?>';
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 ──────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
@@ -564,7 +513,6 @@ async function rellamarDesdeCard(turnoId) {
});
const json = await res.json();
if (!json.ok || !json.turno) return;
anunciarTurno(json.turno.codigo);
await abrirFicha(json.turno);
}
@@ -594,7 +542,6 @@ async function _llamar(extra) {
if (!json.ok) { mostrarError(json.error); return; }
if (!json.turno) { mostrarToast('Cola vacía — no hay turnos en espera.', 'warn', 3000); return; }
mostrarLlamando(json.turno.codigo);
anunciarTurno(json.turno.codigo);
await abrirFicha(json.turno);
cargarCola();
} catch (err) {
@@ -639,10 +586,10 @@ async function cargarFichaSolicitud(turnoId) {
const json = await res.json();
if (!json.ok) return;
const sol = json.solicitud;
const pac = json.paciente;
const exams = json.examenes || [];
const consts = json.consentimientos || [];
const sol = json.solicitud;
const pac = json.paciente;
const exams = json.examenes || [];
let consts = json.consentimientos || [];
// Datos del paciente
if (pac) {
@@ -669,6 +616,42 @@ async function cargarFichaSolicitud(turnoId) {
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
renderConsentimientos(consts);
@@ -921,7 +904,7 @@ async function rellamarTurno() {
});
const json = await res.json();
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) {
mostrarError(err.message);
} finally {