UP
This commit is contained in:
@@ -115,7 +115,7 @@ foreach ($lugares as $lugar) {
|
||||
FROM turnero_turnos t
|
||||
JOIN turnero_prioridades p ON p.id = t.prioridad_id
|
||||
WHERE t.sesion_id = ?
|
||||
AND t.estado IN ('en_espera_lugar', 'en_servicio')
|
||||
AND t.estado = 'en_servicio'
|
||||
AND t.lugar_destino_id = ?
|
||||
ORDER BY t.llamado_lugar_at DESC
|
||||
LIMIT 1"
|
||||
|
||||
@@ -566,7 +566,7 @@ function showAnnouncement({ codigo, destino, paciente, color }) {
|
||||
}
|
||||
|
||||
/* ── State ── */
|
||||
let lastTurnoKey = null;
|
||||
let lastSeenKeys = new Set(); // clave por turno+destino+llamado_at
|
||||
let llamados = [];
|
||||
|
||||
function renderSnapshot(snap) {
|
||||
@@ -576,48 +576,53 @@ function renderSnapshot(snap) {
|
||||
const lblEl = document.getElementById('lugar-label');
|
||||
const ringEl = document.getElementById('pulse-ring');
|
||||
|
||||
const turnos = [];
|
||||
|
||||
// Recopilar TODOS los turnos activos: recepción + lugares
|
||||
const allTurnos = [];
|
||||
(snap.activos_recepcion || []).forEach(d => {
|
||||
if (d.turno) turnos.push({ ...d.turno, destino: d.desk_nombre, tipo: 'rec' });
|
||||
if (d.turno) allTurnos.push({ ...d.turno, destino: d.desk_nombre });
|
||||
});
|
||||
(snap.lugares || []).forEach(l => {
|
||||
if (l.turno) turnos.push({ ...l.turno, destino: l.lugar_nombre, tipo: 'mue' });
|
||||
if (l.turno) allTurnos.push({ ...l.turno, destino: l.lugar_nombre });
|
||||
});
|
||||
|
||||
if (turnos.length > 0) {
|
||||
const t = turnos[0];
|
||||
// Detectar turno nuevo en CUALQUIER estación y anunciar
|
||||
const currentKeys = new Set();
|
||||
let anuncioNuevo = false;
|
||||
allTurnos.forEach(t => {
|
||||
const key = t.codigo + '|' + t.destino + '|' + (t.llamado_at || '');
|
||||
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 });
|
||||
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 (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);
|
||||
|
||||
const key = t.codigo + '|' + t.destino + '|' + (t.llamado_at || '');
|
||||
if (key !== lastTurnoKey) {
|
||||
queueAnnouncement({
|
||||
_key: key,
|
||||
codigo: t.codigo,
|
||||
destino: t.destino,
|
||||
paciente: t.paciente_nombre,
|
||||
color: c
|
||||
});
|
||||
lastTurnoKey = key;
|
||||
llamados.unshift({ codigo: t.codigo, destino: t.destino, paciente: t.paciente_nombre, color: c });
|
||||
if (llamados.length > 30) llamados.pop();
|
||||
renderLlamados();
|
||||
}
|
||||
} else {
|
||||
codEl.textContent = '—';
|
||||
lugEl.textContent = '—';
|
||||
pacEl.textContent = '';
|
||||
lblEl.textContent = 'Llamado a';
|
||||
codEl.style.color = '<?= $_labColor ?>';
|
||||
lastTurnoKey = null;
|
||||
ringEl.classList.remove('active');
|
||||
}
|
||||
|
||||
|
||||
+111
-76
@@ -176,6 +176,29 @@ $adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['
|
||||
}
|
||||
.selector-lugar-box h2 { font-size: 1.4rem; font-weight: 700; margin-bottom: 1.5rem; }
|
||||
|
||||
/* ── 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; }
|
||||
|
||||
/* ── Indicador de bloqueo ── */
|
||||
.bloqueo-banner {
|
||||
background: #fef2f2; border: 1px solid #fca5a5; border-radius: 10px;
|
||||
@@ -313,8 +336,8 @@ require_once __DIR__ . '/../../../shared/components/sidebar.php';
|
||||
|
||||
<!-- ── Barra de acciones ── -->
|
||||
<div class="ficha-acciones">
|
||||
<button class="btn btn-success" id="btn-iniciar" onclick="iniciarAtencion()">
|
||||
<i class="fas fa-play me-1"></i>Iniciar atención
|
||||
<button class="btn btn-primary" id="btn-iniciar" onclick="iniciarAtencion()">
|
||||
<i class="fas fa-bell me-1"></i>Llamar
|
||||
</button>
|
||||
<button class="btn btn-primary d-none" id="btn-finalizar" onclick="finalizarAtencion()">
|
||||
<i class="fas fa-flag-checkered me-1"></i>Finalizar
|
||||
@@ -442,7 +465,7 @@ async function recuperarTurnoActivo() {
|
||||
function confirmarLugar() {
|
||||
const sel = document.getElementById('sel-lugar-init');
|
||||
const id = parseInt(sel.value);
|
||||
if (!id) return alert('Seleccione un lugar.');
|
||||
if (!id) return mostrarError('Seleccione un lugar.');
|
||||
lugarId = id;
|
||||
const txt = sel.options[sel.selectedIndex].text;
|
||||
document.getElementById('lbl-lugar-titulo').textContent = txt;
|
||||
@@ -470,11 +493,16 @@ async function cargarCola() {
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
let _colaMap = new Map(); // id → turno object
|
||||
|
||||
function renderCola(snap) {
|
||||
const items = snap.cola || [];
|
||||
const espera = items.filter(t => t.estado === 'en_espera_lugar');
|
||||
document.getElementById('badge-count').textContent = espera.length;
|
||||
|
||||
// Guardar mapa para selección sin llamar
|
||||
_colaMap = new Map(items.map(t => [t.id, t]));
|
||||
|
||||
const lista = document.getElementById('cola-items');
|
||||
if (!items.length) {
|
||||
lista.innerHTML = `<div class="cola-vacia">
|
||||
@@ -484,39 +512,50 @@ function renderCola(snap) {
|
||||
lista.innerHTML = items.map(t => {
|
||||
const enServicio = t.estado === 'en_servicio';
|
||||
const esActivo = turnoActivo?.id == t.id;
|
||||
if (enServicio) {
|
||||
return `
|
||||
<div class="cola-card en-servicio-card ${esActivo ? 'activo' : ''}"
|
||||
style="cursor:pointer" onclick="rellamarDesdeCard(${t.id})"
|
||||
title="Re-llamar turno ${escHtml(t.codigo)}">
|
||||
<div class="prio-dot" style="background:${t.prioridad_color}">${t.prioridad_codigo}</div>
|
||||
<div class="turno-info">
|
||||
<div class="cod">${escHtml(t.codigo)}</div>
|
||||
<div class="pac">${escHtml(t.paciente_nombre || 'Paciente')}</div>
|
||||
</div>
|
||||
<div class="ms-auto d-flex flex-column align-items-center" style="gap:1px">
|
||||
<i class="fas fa-bell text-success" style="font-size:.85rem"></i>
|
||||
<span style="font-size:.58rem;color:#16a34a;font-weight:700">RE-LLAMAR</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
const onclick = enServicio
|
||||
? `seleccionarSinLlamar(${t.id})`
|
||||
: `seleccionarSinLlamar(${t.id})`;
|
||||
const indicador = enServicio
|
||||
? `<div class="ms-auto d-flex flex-column align-items-center" style="gap:1px">
|
||||
<i class="fas fa-stethoscope text-success" style="font-size:.85rem"></i>
|
||||
<span style="font-size:.58rem;color:#16a34a;font-weight:700">EN SERVICIO</span>
|
||||
</div>`
|
||||
: `<i class="fas fa-chevron-right text-muted ms-auto" style="font-size:.7rem"></i>`;
|
||||
return `
|
||||
<div class="cola-card ${esActivo ? 'activo' : ''}"
|
||||
style="cursor:pointer" onclick="llamarTurnoEspecifico(${t.id})"
|
||||
title="Llamar turno ${escHtml(t.codigo)}">
|
||||
<div class="cola-card ${enServicio ? 'en-servicio-card' : ''} ${esActivo ? 'activo' : ''}"
|
||||
style="cursor:pointer" onclick="${onclick}"
|
||||
title="${enServicio ? 'Ver turno en servicio' : `Ver turno ${escHtml(t.codigo)}`}">
|
||||
<div class="prio-dot" style="background:${t.prioridad_color}">${t.prioridad_codigo}</div>
|
||||
<div class="turno-info">
|
||||
<div class="cod">${escHtml(t.codigo)}</div>
|
||||
<div class="pac">${escHtml(t.paciente_nombre || 'Paciente')}</div>
|
||||
</div>
|
||||
<div class="ms-auto d-flex flex-column align-items-center" style="gap:1px">
|
||||
<i class="fas fa-bell text-primary" style="font-size:.85rem"></i>
|
||||
<span style="font-size:.58rem;color:#1d4ed8;font-weight:700">LLAMAR</span>
|
||||
</div>
|
||||
${indicador}
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Abre la ficha SIN llamar ni cambiar estado
|
||||
async function seleccionarSinLlamar(turnoId) {
|
||||
const t = _colaMap.get(turnoId);
|
||||
if (!t) return;
|
||||
turnoActivo = t;
|
||||
document.getElementById('ficha-placeholder').classList.add('d-none');
|
||||
document.getElementById('ficha-turno').classList.remove('d-none');
|
||||
document.getElementById('ficha-codigo').textContent = t.codigo;
|
||||
document.getElementById('ficha-prio-nombre').textContent = t.prioridad_nombre || '';
|
||||
const dot = document.getElementById('ficha-prio-dot');
|
||||
dot.textContent = t.prioridad_codigo || '—';
|
||||
dot.style.background = t.prioridad_color || '#6366f1';
|
||||
const badgeCod = document.getElementById('badge-turno-activo');
|
||||
badgeCod.textContent = t.codigo;
|
||||
badgeCod.classList.remove('d-none');
|
||||
actualizarEstadoBadge(t.estado);
|
||||
await cargarFichaSolicitud(t.id);
|
||||
clearInterval(pollingConsentId);
|
||||
pollingConsentId = setInterval(() => actualizarConsentimientos(turnoActivo?.id), 5000);
|
||||
}
|
||||
|
||||
async function rellamarDesdeCard(turnoId) {
|
||||
const res = await fetch(API + 'llamar_turno.php', {
|
||||
method: 'POST',
|
||||
@@ -529,24 +568,22 @@ async function rellamarDesdeCard(turnoId) {
|
||||
await abrirFicha(json.turno);
|
||||
}
|
||||
|
||||
// ── Llamar siguiente ──────────────────────────────────────────
|
||||
// ── Llamar siguiente: selecciona el primero en espera sin llamarlo ─
|
||||
async function llamarSiguiente() {
|
||||
await _llamar({});
|
||||
const primero = [..._colaMap.values()].find(t => t.estado === 'en_espera_lugar');
|
||||
if (!primero) { mostrarToast('Cola vacía — no hay turnos en espera.', 'warn', 3000); return; }
|
||||
await seleccionarSinLlamar(primero.id);
|
||||
}
|
||||
|
||||
async function llamarTurnoEspecifico(turnoId) {
|
||||
if (turnoActivo?.id == turnoId) {
|
||||
// Ya está activo — solo mostrar la ficha sin re-llamar API
|
||||
await abrirFicha(turnoActivo);
|
||||
return;
|
||||
}
|
||||
await _llamar({ turno_id: turnoId });
|
||||
await seleccionarSinLlamar(turnoId);
|
||||
}
|
||||
|
||||
async function _llamar(extra) {
|
||||
if (!lugarId) { alert('Primero seleccione un lugar.'); return; }
|
||||
if (!lugarId) { mostrarError('Primero seleccione un lugar.'); return; }
|
||||
const btn = document.getElementById('btn-llamar');
|
||||
btn.disabled = true;
|
||||
mostrarToast('Consultando siguiente turno…', 'info', 1500);
|
||||
try {
|
||||
const res = await fetch(API + 'llamar_turno.php', {
|
||||
method: 'POST',
|
||||
@@ -554,13 +591,14 @@ async function _llamar(extra) {
|
||||
body: JSON.stringify({ area: 'lugar', lugar_id: lugarId, ...extra }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.ok) { alert('Error: ' + json.error); return; }
|
||||
if (!json.turno) { alert('Cola del lugar vacía.'); return; }
|
||||
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) {
|
||||
alert('Error: ' + err.message);
|
||||
mostrarError(err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
@@ -722,10 +760,10 @@ async function reenviarConsentimiento(turnoId) {
|
||||
body: JSON.stringify({ turno_id: turnoId }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.ok) { alert('Error: ' + json.error); return; }
|
||||
if (!json.ok) { mostrarError(json.error); return; }
|
||||
await actualizarConsentimientos(turnoId);
|
||||
} catch (err) {
|
||||
alert('Error: ' + err.message);
|
||||
mostrarError(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -763,35 +801,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// ── Acciones del turno ────────────────────────────────────────
|
||||
async function iniciarAtencion() {
|
||||
if (!turnoActivo) return;
|
||||
if (hayPendientes) return; // botón ya deshabilitado, doble seguro
|
||||
|
||||
const btn = document.getElementById('btn-iniciar');
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch(API + 'cambiar_estado.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ turno_id: turnoActivo.id, nuevo_estado: 'en_servicio' }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.ok) {
|
||||
alert('Error: ' + json.error);
|
||||
btn.disabled = hayPendientes;
|
||||
return;
|
||||
}
|
||||
turnoActivo = json.turno;
|
||||
actualizarEstadoBadge('en_servicio');
|
||||
|
||||
// Mostrar "Finalizar" y "Regresar", ocultar "Iniciar"
|
||||
btn.classList.add('d-none');
|
||||
document.getElementById('btn-finalizar').classList.remove('d-none');
|
||||
document.getElementById('btn-regresar').classList.remove('d-none');
|
||||
cargarCola();
|
||||
} catch (err) {
|
||||
alert('Error: ' + err.message);
|
||||
btn.disabled = false;
|
||||
}
|
||||
if (hayPendientes) return;
|
||||
// Llamar via llamar_turno.php para que suene la campana y notifique el display
|
||||
await _llamar({ turno_id: turnoActivo.id });
|
||||
}
|
||||
|
||||
async function finalizarAtencion() {
|
||||
@@ -824,11 +836,11 @@ async function regresarCola() {
|
||||
}),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.ok) { alert('Error: ' + json.error); return; }
|
||||
if (!json.ok) { mostrarError(json.error); return; }
|
||||
resetFicha();
|
||||
cargarCola();
|
||||
} catch (err) {
|
||||
alert('Error: ' + err.message);
|
||||
mostrarError(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -840,11 +852,11 @@ async function cambiarEstadoTurno(nuevoEstado) {
|
||||
body: JSON.stringify({ turno_id: turnoActivo.id, nuevo_estado: nuevoEstado }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.ok) { alert('Error: ' + json.error); return; }
|
||||
if (!json.ok) { mostrarError(json.error); return; }
|
||||
resetFicha();
|
||||
cargarCola();
|
||||
} catch (err) {
|
||||
alert('Error: ' + err.message);
|
||||
mostrarError(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -908,10 +920,10 @@ async function rellamarTurno() {
|
||||
body: JSON.stringify({ area: 'lugar', lugar_id: lugarId, turno_id: turnoActivo.id }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.ok) alert('Error: ' + json.error);
|
||||
else if (json.turno) anunciarTurno(json.turno.codigo);
|
||||
if (!json.ok) mostrarError(json.error);
|
||||
else if (json.turno) { mostrarLlamando(json.turno.codigo); anunciarTurno(json.turno.codigo); }
|
||||
} catch (err) {
|
||||
alert('Error: ' + err.message);
|
||||
mostrarError(err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
@@ -922,6 +934,29 @@ function escHtml(str) {
|
||||
d.appendChild(document.createTextNode(String(str ?? '')));
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
// ── Toast (igual que recepción) ───────────────────────────────
|
||||
let _toastTimer = null;
|
||||
function mostrarToast(msg, type = 'info', duration = 2500) {
|
||||
const el = document.getElementById('lug-toast');
|
||||
const ico = document.getElementById('lug-toast-ico');
|
||||
const txt = document.getElementById('lug-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);
|
||||
}
|
||||
function mostrarError(msg) { mostrarToast(msg, 'error', 4000); }
|
||||
function mostrarLlamando(cod) { mostrarToast('Llamando turno ' + cod + '…', 'info', 2000); }
|
||||
</script>
|
||||
|
||||
<div class="rec-toast" id="lug-toast">
|
||||
<span class="ico" id="lug-toast-ico"><i class="fas fa-info-circle"></i></span>
|
||||
<span id="lug-toast-msg"></span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user