Toma progresiva: auto-scroll, cierre automático, countdown en cola y fix filtro por examen

- guardar_toma.php: filtrar firmas por condición del examen seleccionado; corrige
  countdown espurio y detección de 'completado' cuando hay múltiples tipos de examen.
  También descarta siguiente_toma_at si ya es pasado (minuto 0).
- ver_formulario_enviado.php: auto-scroll a primera firma pendiente al cargar el modal;
  si el examen se selecciona y la firma no está en DOM, guarda y recarga el iframe.
  Al completar tomas, cierra el modal directamente sin botón "Finalizar".
- get_cola.php: incluye siguiente_toma_at y en_toma_progresiva por turno desde
  turnero_consentimientos para mostrar cuenta regresiva en tarjetas.
- lugar.php: muestra chip de cuenta regresiva en tarjetas de cola para pacientes
  en toma progresiva; color cambia a warn (<2 min) y crit (¡Ahora!).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-09 20:05:56 -05:00
co-authored by Claude Sonnet 4.6
parent c2605fb274
commit 848dd8e4d0
4 changed files with 149 additions and 32 deletions
+23
View File
@@ -99,6 +99,29 @@ if ($area === 'recepcion') {
}
$cola = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Enriquecer con datos de toma progresiva (cuenta regresiva en tarjetas de cola)
if (!empty($cola)) {
$turnoIds = implode(',', array_map('intval', array_column($cola, 'id')));
$tomaRows = $pdo->query(
"SELECT tc.turno_id, 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 IN ($turnoIds) AND tc.estado = 'en_progreso'
ORDER BY tc.siguiente_toma_at ASC"
)->fetchAll(PDO::FETCH_ASSOC);
$tomaInfo = [];
foreach ($tomaRows as $row) {
if (!isset($tomaInfo[$row['turno_id']])) {
$tomaInfo[$row['turno_id']] = $row['siguiente_toma_at'];
}
}
foreach ($cola as &$t) {
$t['en_toma_progresiva'] = isset($tomaInfo[$t['id']]) ? 1 : 0;
$t['siguiente_toma_at'] = $tomaInfo[$t['id']] ?? null;
}
unset($t);
}
// ── Turno actualmente en pantalla (último llamado) ────────────
$campoLlamado = $area === 'recepcion' ? 'llamado_recepcion_at' : 'llamado_lugar_at';
$estadoActivo = $area === 'recepcion' ? "'en_recepcion'" : "'en_servicio'";
+27 -14
View File
@@ -59,28 +59,40 @@ $datosInput[$campoId] = $svg;
$datosMerge = array_merge($datosActuales, $datosInput);
// ── Analizar esquema: campos firma_profesional en orden ──────
// Construir mapa: campo_id → minuto (o hora fija) leyendo el separador previo
$firmasCampos = []; // [ [id, minutos|null, hora_fija|null], ... ] en orden
// Solo incluye firmas cuya sección condicional coincide con el examen seleccionado.
$firmasCampos = [];
$separadorLabel = null;
$curCondField = null;
$curCondVals = [];
foreach ($esquema as $campo) {
$tipo = $campo['tipo'] ?? '';
if ($tipo === 'separador') {
$separadorLabel = $campo['label'] ?? '';
$cond = $campo['condicion'] ?? null;
if ($cond) {
$curCondField = $cond['campo_id'] ?? null;
$curCondVals = $cond['valores'] ?? ($cond['valor'] ? [$cond['valor']] : []);
} else {
$curCondField = null;
$curCondVals = [];
}
}
if ($tipo === 'firma_profesional') {
$minutos = null;
$horaFija = null;
// Filtrar por condición del separador padre
if ($curCondField !== null && !empty($curCondVals)) {
$ctrlVal = $datosMerge[$curCondField] ?? null;
$ctrlArr = is_array($ctrlVal) ? $ctrlVal : ($ctrlVal !== null ? [$ctrlVal] : []);
if (empty(array_intersect($curCondVals, $ctrlArr))) continue;
}
$minutos = null;
$horaFija = null;
if ($separadorLabel !== null) {
// "Taukit · Minuto 10" → 10; "Toma · Minuto 60" → 60
if (preg_match('/Minuto\s+(\d+)/i', $separadorLabel, $m)) {
$minutos = (int)$m[1];
}
// "3:00 p.m." / "4:00 p.m." → hora fija
elseif (preg_match('/(\d+):(\d+)\s*(a\.?m\.?|p\.?m\.?)/i', $separadorLabel, $m)) {
$h = (int)$m[1];
$min = (int)$m[2];
$pm = strtolower(preg_replace('/[^apm]/i', '', $m[3])) === 'pm';
} elseif (preg_match('/(\d+):(\d+)\s*(a\.?m\.?|p\.?m\.?)/i', $separadorLabel, $m)) {
$h = (int)$m[1]; $min = (int)$m[2];
$pm = strtolower(preg_replace('/[^apm]/i', '', $m[3])) === 'pm';
if ($pm && $h < 12) $h += 12;
$horaFija = sprintf('%02d:%02d:00', $h, $min);
}
@@ -106,15 +118,16 @@ if (empty($tc['toma_inicio_at'])) {
if ($todasFirmadas) {
$nuevoEstado = 'firmado';
} else {
// Calcular cuándo es la siguiente toma
$next = $pendientes[0];
if ($next['hora_fija'] !== null) {
// Hora fija en el día actual
$siguienteToma = date('Y-m-d') . ' ' . $next['hora_fija'];
} elseif ($next['minutos'] !== null && $tomaInicioAt) {
// Minuto relativo desde el inicio de la toma
$siguienteToma = date('Y-m-d H:i:s', strtotime($tomaInicioAt) + $next['minutos'] * 60);
}
// Si la siguiente toma ya pasó o es ahora (minuto 0), no enviar countdown
if ($siguienteToma !== null && $siguienteToma <= date('Y-m-d H:i:s')) {
$siguienteToma = null;
}
}
// ── Persistir ────────────────────────────────────────────────
+48 -8
View File
@@ -188,6 +188,16 @@ try {
font-size: .6rem; font-weight: 700; color: #ea580c;
background: #fff7ed; border-radius: 99px; padding: 0 6px; line-height: 1.7;
}
.toma-prog-chip {
font-size: .6rem; font-weight: 700; border-radius: 99px;
padding: 0 6px; line-height: 1.7; white-space: nowrap;
}
.toma-prog-chip.ok { background: #eff6ff; color: #1d4ed8; }
.toma-prog-chip.warn { background: #fffbeb; color: #92400e; }
.toma-prog-chip.crit { background: #fef2f2; color: #991b1b; animation: parpadeo-alerta 1s infinite; }
@keyframes parpadeo-alerta { 0%,100%{opacity:1} 50%{opacity:.55} }
.cola-card.toma-prog { border-color: #f97316 !important; }
.cola-card.toma-prog.crit-card { border-color: #dc2626 !important; background: #fef2f2 !important; }
.card-en-servicio-pill {
flex-shrink: 0; display: flex; flex-direction: column; align-items: center;
@@ -945,9 +955,11 @@ function renderCola(snap) {
}
function renderColaCard(t) {
const enServicio = t.estado === 'en_servicio';
const esActivo = turnoActivo?.id == t.id;
const esMuestra = t.solo_muestras == 1;
const enServicio = t.estado === 'en_servicio';
const esActivo = turnoActivo?.id == t.id;
const esMuestra = t.solo_muestras == 1;
const enTomaProg = t.en_toma_progresiva == 1;
const sigTomaAt = t.siguiente_toma_at || null;
const tiempo = tiempoEspera(t.creado_at);
const tiempoBadge = tiempo
@@ -956,8 +968,13 @@ function renderColaCard(t) {
const muestraBadge = esMuestra
? `<span class="muestra-chip"><i class="fas fa-plus me-1"></i>MUESTRAS</span>`
: '';
const badgeRow = (tiempoBadge || muestraBadge)
? `<div class="t-badge-row">${tiempoBadge}${muestraBadge}</div>`
const tomaBadge = enTomaProg
? `<span class="toma-prog-chip ok" data-toma-at="${escHtml(sigTomaAt || '')}" data-turno-id="${t.id}">
<i class="fas fa-syringe me-1"></i><span class="toma-cd-txt">${sigTomaAt ? '...' : '¡Ahora!'}</span>
</span>`
: '';
const badgeRow = (tiempoBadge || muestraBadge || tomaBadge)
? `<div class="t-badge-row">${tiempoBadge}${muestraBadge}${tomaBadge}</div>`
: '';
const rightEl = enServicio
@@ -967,11 +984,12 @@ function renderColaCard(t) {
<i class="fas fa-bell"></i>
</button>`;
const bg = esMuestra ? 'border:2px solid #ea580c!important;background:#fff7ed!important' : '';
const prioBg = esMuestra ? '#ea580c' : (t.prioridad_color || '#6366f1');
let bg = esMuestra ? 'border:2px solid #ea580c!important;background:#fff7ed!important' : '';
const prioBg = esMuestra ? '#ea580c' : (t.prioridad_color || '#6366f1');
const prioLbl = esMuestra ? '<i class="fas fa-plus"></i>' : escHtml(t.prioridad_codigo || '?');
const tomaClass = enTomaProg ? ' toma-prog' : '';
return `<div class="cola-card ${enServicio ? 'en-servicio-card' : ''} ${esActivo ? 'activo' : ''}"
return `<div class="cola-card ${enServicio ? 'en-servicio-card' : ''} ${esActivo ? 'activo' : ''}${tomaClass}"
style="${bg}" onclick="seleccionarSinLlamar(${t.id})">
<div class="prio-dot" style="background:${prioBg}">${prioLbl}</div>
<div class="turno-info">
@@ -1180,6 +1198,28 @@ const CONSENT_LBL = {
en_progreso:'En progreso',
};
// ── Countdown en tarjetas de cola (toma progresiva) ──────────
setInterval(function() {
const ahora = Date.now();
document.querySelectorAll('.toma-prog-chip[data-toma-at]').forEach(function(chip) {
const at = chip.dataset.tomaAt;
const txt = chip.querySelector('.toma-cd-txt');
if (!at || !txt) return;
const diff = Math.floor((new Date(at.replace(' ','T')).getTime() - ahora) / 1000);
const card = chip.closest('.cola-card');
if (diff <= 0) {
txt.textContent = '¡Ahora!';
chip.className = 'toma-prog-chip crit';
if (card) card.classList.add('crit-card');
} else {
const m = Math.floor(diff / 60), s = diff % 60;
txt.textContent = m + ':' + String(s).padStart(2,'0');
chip.className = diff < 120 ? 'toma-prog-chip warn' : 'toma-prog-chip ok';
if (card) card.classList.remove('crit-card');
}
});
}, 1000);
// ── Tomas progresivas: actualiza countdowns cada segundo ──────
let _tomaTimers = {}; // consent_id → { siguiente_toma_at, es_alerta }
setInterval(() => {
+51 -10
View File
@@ -1073,6 +1073,13 @@ function esc2(mixed $v): string {
<?php if (!empty($_mpMap)): ?>
window._muestrasMap = <?= json_encode($_mpMap, JSON_UNESCAPED_UNICODE) ?>;
window._mpSavedFirmas = {};
// Auto-scroll a la primera firma pendiente al cargar (examen ya seleccionado)
document.addEventListener('DOMContentLoaded', function() {
setTimeout(function() {
var fw = document.querySelector('.firma-pro-widget:not([style*="display:none"])');
if (fw) fw.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 350);
});
<?php endif; ?>
/* ── Firma del profesional: función compartida canvas/1-clic ────── */
function _guardarFirmaPro(widget, svg, msgEl) {
@@ -1183,24 +1190,34 @@ function _guardarFirmaMP(widget, svg, msgEl, entry, campoId) {
widget.replaceWith(box);
if (data.completado) {
_mpMostrarFinalizar(entry.exam_type);
// Cerrar modal en lugar.php y refrescar la lista
// Toma completada: mostrar mensaje y cerrar modal automáticamente
var ok = document.createElement('div');
ok.className = 'alert alert-success mt-4 d-flex align-items-center gap-2';
ok.innerHTML = '<i class="fas fa-check-circle fs-5"></i><div><strong>Toma ' + (entry.exam_type || '') + ' completada.</strong><br><small>Cerrando...</small></div>';
document.querySelector('.doc-body').appendChild(ok);
setTimeout(function() {
try { window.parent.postMessage({ type: 'turneroFirmado' }, '*'); } catch(e) {}
}, 1200);
} else if (data.siguiente_toma_at) {
// Countdown usando tiempo absoluto del servidor
var targetMs = new Date(data.siguiente_toma_at.replace(' ', 'T')).getTime();
var minsLeft = Math.max(1, Math.round((targetMs - Date.now()) / 60000));
_mpIniciarCountdown(minsLeft, entry.next_label, entry.next_firma_id);
} else if (!entry.is_last) {
var nw = entry.next_firma_id ? document.getElementById('fpw-' + entry.next_firma_id) : document.querySelector('.firma-pro-widget');
if (nw) nw.scrollIntoView({ behavior:'smooth', block:'center' });
var nw = entry.next_firma_id ? document.getElementById('fpw-' + entry.next_firma_id) : null;
if (!nw) nw = document.querySelector('.firma-pro-widget:not([style*="display:none"])');
if (nw) { nw.style.display = ''; nw.scrollIntoView({ behavior:'smooth', block:'center' }); }
} else {
_mpMostrarFinalizar(entry.exam_type);
setTimeout(function() {
try { window.parent.postMessage({ type: 'turneroFirmado' }, '*'); } catch(e) {}
}, 1200);
// Último firma del grupo pero hay más exámenes pendientes; desbloquear siguiente
var nw = document.querySelector('.firma-pro-widget[style*="display:none"]');
if (nw) {
nw.style.display = '';
nw.scrollIntoView({ behavior:'smooth', block:'center' });
} else {
// Sin más firmas pendientes → cerrar
setTimeout(function() {
try { window.parent.postMessage({ type: 'turneroFirmado' }, '*'); } catch(e) {}
}, 1200);
}
}
})
.catch(function() {
@@ -1681,7 +1698,7 @@ const topaz = (() => {
var condicionales = secciones.filter(function(s) { return s.condicion; });
if (!condicionales.length) return;
function evaluar() {
function evaluar(evt) {
condicionales.forEach(function(sec) {
var cond = sec.condicion;
var valoresCond = cond.valores ? cond.valores : (cond.valor ? [cond.valor] : []);
@@ -1708,6 +1725,30 @@ const topaz = (() => {
el.style.pointerEvents = activo ? '' : 'none';
});
});
// Toma progresiva: scroll a la primera firma pendiente
if (window._muestrasMap) {
setTimeout(function() {
var fw = document.querySelector('.firma-pro-widget:not([style*="display:none"])');
if (fw) {
fw.scrollIntoView({ behavior: 'smooth', block: 'start' });
} else if (evt) {
// Firma no está en el DOM (examen recién seleccionado) → guardar y recargar
var dr = {};
document.querySelectorAll('[name]').forEach(function(el) {
var n = el.name.endsWith('[]') ? el.name.slice(0,-2) : el.name;
if (el.type==='checkbox'){ if(el.checked){ if(!Array.isArray(dr[n]))dr[n]=[]; dr[n].push(el.value); } }
else if (el.type==='radio'){ if(el.checked) dr[n]=el.value; }
else if (el.value!=='') dr[n]=el.value;
});
fetch(window.location.href, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ datos_respuestas: dr })
}).then(function() { window.location.reload(); })
.catch(function() { window.location.reload(); });
}
}, 400);
}
}
var ctrlIds = [...new Set(condicionales.map(function(s) { return s.condicion.campo_id; }))];