turnero: sala de espera entre muestras (toma progresiva)
- DB: columna muestra_espera_at en turnero_turnos - Panel 'Esperando siguiente muestra' en la cola de lugar.php · Muestra pacientes en espera con countdown a siguiente_toma_at · Botón 'Llamar' los devuelve a la cola activa - Botón 'Mandar a espera' en la ficha (visible cuando toma progresiva activa) · Desaparece de la cola principal, queda en sección de espera - get_cola.php: excluye muestra_espera_at IS NOT NULL de cola y activo · Agrega array en_espera_muestra en la respuesta - Nuevas APIs: poner_en_espera_muestra.php, llamar_desde_espera.php Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
e634eb8932
commit
4012ae97be
@@ -83,7 +83,8 @@ if ($area === 'recepcion') {
|
||||
LEFT JOIN turnero_solicitudes s ON s.turno_id = t.id
|
||||
WHERE t.sesion_id = ?
|
||||
AND t.estado = 'en_espera_lugar'
|
||||
AND t.lugar_destino_id IN ($inLugares))
|
||||
AND t.lugar_destino_id IN ($inLugares)
|
||||
AND t.muestra_espera_at IS NULL)
|
||||
UNION ALL
|
||||
(SELECT $cols
|
||||
FROM turnero_turnos t
|
||||
@@ -91,7 +92,8 @@ if ($area === 'recepcion') {
|
||||
LEFT JOIN turnero_solicitudes s ON s.turno_id = t.id
|
||||
WHERE t.sesion_id = ?
|
||||
AND t.estado = 'en_servicio'
|
||||
AND t.lugar_destino_id = ?)
|
||||
AND t.lugar_destino_id = ?
|
||||
AND t.muestra_espera_at IS NULL)
|
||||
) AS cola_union
|
||||
ORDER BY solo_muestras DESC, orden_peso ASC, creado_at ASC"
|
||||
);
|
||||
@@ -126,7 +128,7 @@ if (!empty($cola)) {
|
||||
$campoLlamado = $area === 'recepcion' ? 'llamado_recepcion_at' : 'llamado_lugar_at';
|
||||
$estadoActivo = $area === 'recepcion' ? "'en_recepcion'" : "'en_servicio'";
|
||||
// Activo: para lugar filtrar SOLO por esta estación (ya fue asignada al llamar)
|
||||
$filtroActivo = $area === 'lugar' ? 'AND t.lugar_destino_id = ?' : '';
|
||||
$filtroActivo = $area === 'lugar' ? 'AND t.lugar_destino_id = ? AND t.muestra_espera_at IS NULL' : '';
|
||||
$bindActivo = $area === 'lugar' ? [$sesionId, $lugarId] : [$sesionId];
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
@@ -173,12 +175,33 @@ $stmt = $pdo->prepare(
|
||||
$stmt->execute([$sesionId]);
|
||||
$stats = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// ── Pacientes en espera entre muestras (toma progresiva) ─────
|
||||
$enEsperaMuestra = [];
|
||||
if ($area === 'lugar') {
|
||||
$stmtEsp = $pdo->prepare("
|
||||
SELECT t.id, t.paciente_nombre, ts.numero_orden, t.muestra_espera_at,
|
||||
(SELECT tc.siguiente_toma_at
|
||||
FROM turnero_consentimientos tc
|
||||
JOIN lab_formularios f ON f.id = tc.formulario_id AND f.es_toma_progresiva = 1
|
||||
WHERE tc.turno_id = t.id AND tc.estado = 'en_progreso'
|
||||
ORDER BY tc.siguiente_toma_at ASC LIMIT 1) AS siguiente_toma_at
|
||||
FROM turnero_turnos t
|
||||
JOIN turnero_solicitudes ts ON ts.turno_id = t.id
|
||||
WHERE t.sesion_id = ? AND t.lugar_destino_id IN ($inLugares)
|
||||
AND t.muestra_espera_at IS NOT NULL AND t.fin_lugar_at IS NULL
|
||||
ORDER BY t.muestra_espera_at ASC
|
||||
");
|
||||
$stmtEsp->execute([$sesionId]);
|
||||
$enEsperaMuestra = $stmtEsp->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
jsonOk([
|
||||
'sesion_id' => $sesionId,
|
||||
'area' => $area,
|
||||
'lugar_id' => $lugarId,
|
||||
'activo' => $activo,
|
||||
'cola' => $cola,
|
||||
'stats' => $stats,
|
||||
'timestamp' => date('c'),
|
||||
'sesion_id' => $sesionId,
|
||||
'area' => $area,
|
||||
'lugar_id' => $lugarId,
|
||||
'activo' => $activo,
|
||||
'cola' => $cola,
|
||||
'en_espera_muestra' => $enEsperaMuestra,
|
||||
'stats' => $stats,
|
||||
'timestamp' => date('c'),
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
/**
|
||||
* POST llamar_desde_espera.php
|
||||
* {turno_id} → devuelve el paciente a la cola activa para la siguiente muestra.
|
||||
* SET muestra_espera_at = NULL.
|
||||
*/
|
||||
require_once __DIR__ . '/_helpers.php';
|
||||
requireTurnero();
|
||||
requireMethod('POST');
|
||||
|
||||
$input = inputJson();
|
||||
$turnoId = (int)($input['turno_id'] ?? 0);
|
||||
if (!$turnoId) jsonError('turno_id requerido');
|
||||
|
||||
db()->prepare(
|
||||
"UPDATE turnero_turnos SET muestra_espera_at = NULL WHERE id = ? AND muestra_espera_at IS NOT NULL"
|
||||
)->execute([$turnoId]);
|
||||
|
||||
jsonOk([], 'Paciente de vuelta en cola');
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
/**
|
||||
* POST poner_en_espera_muestra.php
|
||||
* {turno_id} → mueve el paciente a la sala de espera entre muestras.
|
||||
* SET muestra_espera_at = NOW(). El turno queda en_servicio pero sale de la cola activa.
|
||||
*/
|
||||
require_once __DIR__ . '/_helpers.php';
|
||||
requireTurnero();
|
||||
requireMethod('POST');
|
||||
|
||||
$input = inputJson();
|
||||
$turnoId = (int)($input['turno_id'] ?? 0);
|
||||
if (!$turnoId) jsonError('turno_id requerido');
|
||||
|
||||
db()->prepare(
|
||||
"UPDATE turnero_turnos SET muestra_espera_at = NOW()
|
||||
WHERE id = ? AND estado = 'en_servicio' AND muestra_espera_at IS NULL"
|
||||
)->execute([$turnoId]);
|
||||
|
||||
jsonOk([], 'Paciente enviado a sala de espera');
|
||||
@@ -201,6 +201,33 @@ foreach ($lugares as $_el) {
|
||||
.esp-pac-orden { font-weight: 700; font-family: monospace; color: var(--esp-gc); font-size: .7rem; flex-shrink: 0; }
|
||||
.esp-pac-hora { margin-left: auto; font-size: .68rem; color: #94a3b8; flex-shrink: 0; }
|
||||
|
||||
/* ── Sala de espera entre muestras ── */
|
||||
#espera-muestras-panel { flex-shrink: 0; border-bottom: 2px solid #fde68a; background: #fffbeb; }
|
||||
.espera-m-hdr {
|
||||
display: flex; align-items: center; gap: .4rem;
|
||||
padding: .4rem .75rem .25rem; font-size: .72rem; font-weight: 700; color: #92400e;
|
||||
}
|
||||
.espera-m-count {
|
||||
background: #f97316; color: #fff; border-radius: 99px; padding: 0 6px; font-size: .65rem;
|
||||
}
|
||||
.espera-m-row {
|
||||
display: flex; align-items: center; gap: .5rem;
|
||||
padding: .22rem .75rem; font-size: .78rem; color: #1e293b;
|
||||
border-top: 1px solid #fde68a;
|
||||
}
|
||||
.espera-m-orden { font-weight: 700; font-family: monospace; color: #f97316; font-size: .7rem; flex-shrink: 0; }
|
||||
.espera-m-cd { font-size: .71rem; font-weight: 700; flex-shrink: 0; white-space: nowrap; }
|
||||
.espera-m-cd.ok { color: #16a34a; }
|
||||
.espera-m-cd.warn { color: #d97706; }
|
||||
.espera-m-cd.crit { color: #dc2626; animation: parpadeo-alerta .8s infinite; }
|
||||
.btn-llamar-espera {
|
||||
margin-left: auto; flex-shrink: 0;
|
||||
padding: .18rem .55rem; font-size: .72rem; font-weight: 700;
|
||||
border: 1.5px solid #22c55e; color: #16a34a; background: #fff;
|
||||
border-radius: 7px; cursor: pointer; transition: background .1s;
|
||||
}
|
||||
.btn-llamar-espera:hover { background: #f0fdf4; }
|
||||
|
||||
.cola-section-sep {
|
||||
font-size: .65rem; text-transform: uppercase; letter-spacing: .06em; font-weight: 700;
|
||||
color: #94a3b8; padding: .3rem .4rem .15rem; margin-top: .25rem;
|
||||
@@ -676,6 +703,8 @@ require_once __DIR__ . '/../../../shared/components/sidebar.php';
|
||||
<div id="esp-panel" style="display:none"></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div id="espera-muestras-panel" style="display:none"></div>
|
||||
|
||||
<div class="cola-items" id="cola-items">
|
||||
<div class="cola-vacia">
|
||||
<i class="fas fa-hourglass-start fa-2x mb-2 d-block"></i>
|
||||
@@ -868,6 +897,10 @@ require_once __DIR__ . '/../../../shared/components/sidebar.php';
|
||||
<button class="btn-accion-sec warning d-none" id="btn-devolver" onclick="devolverRecepcion()">
|
||||
<i class="fas fa-reply"></i>A recepción
|
||||
</button>
|
||||
<button class="btn-accion-sec d-none" id="btn-espera" onclick="_ponerEnEspera()"
|
||||
style="color:#c2410c;border-color:#fed7aa">
|
||||
<i class="fas fa-hourglass-half"></i>Mandar a espera
|
||||
</button>
|
||||
<button class="btn-accion-sec danger" id="btn-ausente" onclick="marcarAusente()">
|
||||
<i class="fas fa-user-slash"></i>Ausente
|
||||
</button>
|
||||
@@ -1087,6 +1120,7 @@ function tiempoEspera(iso) {
|
||||
}
|
||||
|
||||
function renderCola(snap) {
|
||||
_renderEsperaMuestras(snap.en_espera_muestra || []);
|
||||
const items = snap.cola || [];
|
||||
const enEspera = items.filter(t => t.estado === 'en_espera_lugar');
|
||||
const enServ = items.filter(t => t.estado === 'en_servicio');
|
||||
@@ -1532,6 +1566,12 @@ function renderConsentimientos(lista) {
|
||||
};
|
||||
}
|
||||
});
|
||||
// Mostrar botón "Mandar a espera" si hay toma progresiva activa y estado en_servicio
|
||||
const btnEsp = document.getElementById('btn-espera');
|
||||
if (btnEsp) {
|
||||
const hasProg = Object.keys(_tomaTimers).length > 0;
|
||||
btnEsp.classList.toggle('d-none', !(hasProg && turnoActivo?.estado === 'en_servicio'));
|
||||
}
|
||||
|
||||
listEl.innerHTML = lista.map(c => {
|
||||
const ya = ['firmado','rechazado'].includes(c.estado);
|
||||
@@ -2028,18 +2068,22 @@ function actualizarEstadoBadge(estado) {
|
||||
const btnRellamar = document.getElementById('btn-rellamar');
|
||||
const btnDev = document.getElementById('btn-devolver');
|
||||
|
||||
const btnEsp = document.getElementById('btn-espera');
|
||||
if (estado === 'en_espera_lugar') {
|
||||
btnIni.classList.remove('d-none'); btnIni.disabled = false;
|
||||
btnFin.classList.add('d-none');
|
||||
btnReg.classList.add('d-none');
|
||||
btnRellamar.classList.add('d-none');
|
||||
btnDev.classList.remove('d-none');
|
||||
if (btnEsp) btnEsp.classList.add('d-none');
|
||||
} else if (estado === 'en_servicio') {
|
||||
btnIni.classList.add('d-none');
|
||||
btnFin.classList.remove('d-none');
|
||||
btnReg.classList.remove('d-none');
|
||||
btnRellamar.classList.remove('d-none');
|
||||
btnDev.classList.remove('d-none');
|
||||
// btn-espera: solo si hay toma progresiva activa (sincronizado desde renderConsentimientos)
|
||||
if (btnEsp) btnEsp.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2396,6 +2440,71 @@ function mostrarToast(msg, type = 'info', duration = 2500) {
|
||||
function mostrarError(msg) { mostrarToast(msg, 'error', 4000); }
|
||||
function mostrarLlamando(cod) { mostrarToast('Llamando turno ' + cod + '…', 'info', 2000); }
|
||||
|
||||
function _renderEsperaMuestras(items) {
|
||||
const panel = document.getElementById('espera-muestras-panel');
|
||||
if (!panel) return;
|
||||
if (!items.length) { panel.style.display = 'none'; panel.innerHTML = ''; return; }
|
||||
const ahora = Date.now();
|
||||
let html = `<div class="espera-m-hdr">
|
||||
<i class="fas fa-hourglass-half" style="color:#f97316"></i>
|
||||
Esperando siguiente muestra
|
||||
<span class="espera-m-count">${items.length}</span>
|
||||
</div>`;
|
||||
for (const p of items) {
|
||||
let cdHtml = '';
|
||||
if (p.siguiente_toma_at) {
|
||||
const ms = new Date(p.siguiente_toma_at.replace(' ', 'T')).getTime() - ahora;
|
||||
const mins = Math.ceil(ms / 60000);
|
||||
if (ms <= 0) {
|
||||
cdHtml = `<span class="espera-m-cd crit">¡Lista ya!</span>`;
|
||||
} else if (mins <= 5) {
|
||||
cdHtml = `<span class="espera-m-cd warn">en ${mins} min</span>`;
|
||||
} else {
|
||||
cdHtml = `<span class="espera-m-cd ok">en ${mins} min</span>`;
|
||||
}
|
||||
}
|
||||
html += `<div class="espera-m-row">
|
||||
<span class="espera-m-orden">#${escHtml(String(p.numero_orden))}</span>
|
||||
<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(p.paciente_nombre)}</span>
|
||||
${cdHtml}
|
||||
<button class="btn-llamar-espera" onclick="_llamarDesdeEspera(${(int = p.id, int)})">
|
||||
<i class="fas fa-bell fa-xs me-1"></i>Llamar
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
panel.innerHTML = html;
|
||||
panel.style.display = '';
|
||||
}
|
||||
|
||||
async function _ponerEnEspera() {
|
||||
if (!turnoActivo) return;
|
||||
const btn = document.getElementById('btn-espera');
|
||||
if (btn) btn.disabled = true;
|
||||
try {
|
||||
const res = await fetch(API + 'poner_en_espera_muestra.php', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ turno_id: turnoActivo.id }),
|
||||
});
|
||||
const j = await res.json();
|
||||
if (!j.ok) { mostrarError(j.error); return; }
|
||||
resetFicha();
|
||||
cargarCola();
|
||||
} catch (e) { mostrarError(e.message); }
|
||||
finally { if (btn) btn.disabled = false; }
|
||||
}
|
||||
|
||||
async function _llamarDesdeEspera(turnoId) {
|
||||
try {
|
||||
const res = await fetch(API + 'llamar_desde_espera.php', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ turno_id: turnoId }),
|
||||
});
|
||||
const j = await res.json();
|
||||
if (!j.ok) mostrarError(j.error);
|
||||
else cargarCola();
|
||||
} catch (e) { mostrarError(e.message); }
|
||||
}
|
||||
|
||||
async function _cargarEspecialidades() {
|
||||
if (!ESP_LUGARES.length) return;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user