diff --git a/modules/turnero/api/get_cola.php b/modules/turnero/api/get_cola.php index 7a0cb8c..933d877 100644 --- a/modules/turnero/api/get_cola.php +++ b/modules/turnero/api/get_cola.php @@ -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'), ]); diff --git a/modules/turnero/api/llamar_desde_espera.php b/modules/turnero/api/llamar_desde_espera.php new file mode 100644 index 0000000..36a404c --- /dev/null +++ b/modules/turnero/api/llamar_desde_espera.php @@ -0,0 +1,19 @@ +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'); diff --git a/modules/turnero/api/poner_en_espera_muestra.php b/modules/turnero/api/poner_en_espera_muestra.php new file mode 100644 index 0000000..bcbb39d --- /dev/null +++ b/modules/turnero/api/poner_en_espera_muestra.php @@ -0,0 +1,20 @@ +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'); diff --git a/modules/turnero/views/lugar.php b/modules/turnero/views/lugar.php index 713cf46..9dafcc2 100644 --- a/modules/turnero/views/lugar.php +++ b/modules/turnero/views/lugar.php @@ -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'; + +
@@ -868,6 +897,10 @@ require_once __DIR__ . '/../../../shared/components/sidebar.php'; + @@ -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 = `
+ + Esperando siguiente muestra + ${items.length} +
`; + 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 = `¡Lista ya!`; + } else if (mins <= 5) { + cdHtml = `en ${mins} min`; + } else { + cdHtml = `en ${mins} min`; + } + } + html += `
+ #${escHtml(String(p.numero_orden))} + ${escHtml(p.paciente_nombre)} + ${cdHtml} + +
`; + } + 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 {