feat(turnero): panel de estado de muestras en recepción

Recepción ve en una barra inferior qué turno atiende cada estación de
toma de muestras (verde = ocupada, gris = libre), cuántos están en
espera y cuántos en espera entre muestras progresivas. Se refresca cada 7 s.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-28 11:59:18 -05:00
co-authored by Claude Sonnet 4.6
parent 7b599b28ef
commit 7d444699d3
2 changed files with 83 additions and 0 deletions
@@ -0,0 +1,51 @@
<?php
/**
* GET get_resumen_muestras.php
* Resumen de estaciones de toma de muestras para recepción.
*/
require_once __DIR__ . '/_helpers.php';
requireTurnero();
$pdo = db();
$sesionId = (int)(obtenerOCrearSesionHoy()['id'] ?? 0);
// Estaciones con el turno en servicio actual
$stmt = $pdo->prepare(
"SELECT l.id, l.nombre,
t.id AS turno_id, t.codigo, t.paciente_nombre
FROM turnero_lugares l
LEFT JOIN turnero_turnos t
ON t.lugar_destino_id = l.id
AND t.sesion_id = ?
AND t.estado = 'en_servicio'
AND t.muestra_espera_at IS NULL
AND t.fin_lugar_at IS NULL
WHERE l.tipo = 'muestras' AND l.activo = 1
ORDER BY l.sort_order"
);
$stmt->execute([$sesionId]);
$estaciones = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Cola global de muestras (en_espera_lugar)
$s = $pdo->prepare(
"SELECT COUNT(*) FROM turnero_turnos
WHERE sesion_id = ? AND estado = 'en_espera_lugar' AND muestra_espera_at IS NULL
AND lugar_destino_id IN (SELECT id FROM turnero_lugares WHERE tipo='muestras' AND activo=1)"
);
$s->execute([$sesionId]);
$enEspera = (int)$s->fetchColumn();
// Pacientes entre tomas (espera progresiva)
$s2 = $pdo->prepare(
"SELECT COUNT(*) FROM turnero_turnos
WHERE sesion_id = ? AND muestra_espera_at IS NOT NULL AND fin_lugar_at IS NULL
AND lugar_destino_id IN (SELECT id FROM turnero_lugares WHERE tipo='muestras' AND activo=1)"
);
$s2->execute([$sesionId]);
$enEsperaMuestra = (int)$s2->fetchColumn();
jsonOk([
'estaciones' => $estaciones,
'en_espera' => $enEspera,
'en_espera_muestra' => $enEsperaMuestra,
]);
+32
View File
@@ -934,6 +934,14 @@ document.addEventListener('DOMContentLoaded', function() {
</div><!-- /rec-ficha -->
</div><!-- /rec-layout -->
<!-- ══ Panel resumen toma de muestras ════════════════════════ -->
<div id="panel-muestras-resumen" style="padding:6px 12px;border-top:1px solid #e2e8f0;background:#f8fafc;display:flex;gap:8px;align-items:center;flex-wrap:wrap;min-height:38px">
<span class="text-muted small fw-semibold me-1" style="white-space:nowrap"><i class="fas fa-vial me-1"></i>Muestras:</span>
<div id="muestras-estaciones" class="d-flex gap-2 flex-wrap align-items-center"></div>
<span id="muestras-cola-badge" class="badge bg-warning text-dark ms-auto d-none" title="En espera de ser llamados"></span>
<span id="muestras-prog-badge" class="badge bg-info text-dark d-none" title="Esperando siguiente muestra (toma progresiva)"></span>
</div>
</main>
<!-- ═══ Modal firma enfermero ═══════════════════════════════════════ -->
@@ -1200,6 +1208,8 @@ document.addEventListener('DOMContentLoaded', () => {
cargarCola();
pollingColaId = setInterval(cargarCola, 3000);
_cargarResumenMuestras();
setInterval(_cargarResumenMuestras, 7000);
let _pacBusqTimer;
const _inpPac = document.getElementById('inp-buscar-pac');
_inpPac.addEventListener('input', () => {
@@ -3079,6 +3089,28 @@ async function mpacGuardar() {
// ── Toast ────────────────────────────────────────────────────
let toastTimer = null;
async function _cargarResumenMuestras() {
try {
const r = await fetch(API + 'get_resumen_muestras.php');
const j = await r.json();
if (!j.ok) return;
const cont = document.getElementById('muestras-estaciones');
if (!cont) return;
cont.innerHTML = j.estaciones.map(e => {
const libre = !e.turno_id;
return `<span class="badge ${libre ? 'bg-secondary' : 'bg-success'}" style="font-size:.75rem;font-weight:500" title="${libre ? 'Libre' : escHtml(e.paciente_nombre || '')}">
${escHtml(e.nombre)}: ${libre ? '—' : escHtml(e.codigo)}
</span>`;
}).join('');
const cb = document.getElementById('muestras-cola-badge');
cb.textContent = `${j.en_espera} en espera`;
cb.classList.toggle('d-none', j.en_espera === 0);
const pb = document.getElementById('muestras-prog-badge');
pb.textContent = `${j.en_espera_muestra} prog.`;
pb.classList.toggle('d-none', j.en_espera_muestra === 0);
} catch {}
}
function mostrarToast(msg, type = 'info', duration = 2500) {
const el = document.getElementById('rec-toast');
const ico = document.getElementById('toast-ico');