diff --git a/modules/turnero/api/get_consentimientos.php b/modules/turnero/api/get_consentimientos.php
index 4cc9cce..0755aa1 100644
--- a/modules/turnero/api/get_consentimientos.php
+++ b/modules/turnero/api/get_consentimientos.php
@@ -118,7 +118,8 @@ foreach ($consentimientos as &$c) {
$tomasTotal = count($relevantIds);
foreach ($relevantIds as $fid) {
- if (!empty($dr[$fid]) && strlen($dr[$fid]) > 10) $tomasFirm++;
+ if ((!empty($dr[$fid]) && strlen($dr[$fid]) > 10)
+ || (!empty($dr[$fid.'_svg']) && strlen($dr[$fid.'_svg']) > 10)) $tomasFirm++;
}
}
diff --git a/modules/turnero/api/guardar_toma.php b/modules/turnero/api/guardar_toma.php
index 21a6aa5..df199a1 100644
--- a/modules/turnero/api/guardar_toma.php
+++ b/modules/turnero/api/guardar_toma.php
@@ -102,9 +102,11 @@ foreach ($esquema as $campo) {
}
// ── Determinar estado y siguiente_toma_at ────────────────────
-$firmados = array_filter($firmasCampos, fn($f) => isset($datosMerge[$f['id']]) && strlen($datosMerge[$f['id']]) > 10);
-$pendientes = array_filter($firmasCampos, fn($f) => !isset($datosMerge[$f['id']]) || strlen($datosMerge[$f['id']]) <= 10);
-$pendientes = array_values($pendientes);
+// Acepta firma guardada con clave campo_id O campo_id_svg (compatibilidad registros históricos)
+$hasFirma = fn($f) => (isset($datosMerge[$f['id']]) && strlen($datosMerge[$f['id']]) > 10)
+ || (isset($datosMerge[$f['id'].'_svg']) && strlen($datosMerge[$f['id'].'_svg']) > 10);
+$firmados = array_filter($firmasCampos, $hasFirma);
+$pendientes = array_values(array_filter($firmasCampos, fn($f) => !$hasFirma($f)));
$todasFirmadas = empty($pendientes);
$siguienteToma = null;
diff --git a/modules/turnero/views/lugar.php b/modules/turnero/views/lugar.php
index 6c290c2..dc45955 100644
--- a/modules/turnero/views/lugar.php
+++ b/modules/turnero/views/lugar.php
@@ -433,6 +433,27 @@ try {
.mtl-lugar { font-size:.73rem; color:#64748b; }
.mtl-eb { font-size:.62rem; padding:1px 7px; border-radius:99px; font-weight:700; display:inline-block; margin-top:2px; }
+ /* ── Banner toma alerta ── */
+ #banner-toma-alerta {
+ display: none; position: fixed; top: 52px; left: 0; right: 0; z-index: 8900;
+ background: #dc2626; color: #fff;
+ padding: .65rem 1.2rem; display: none;
+ align-items: center; gap: .75rem; font-size: .9rem; font-weight: 600;
+ box-shadow: 0 4px 20px rgba(220,38,38,.45);
+ animation: parpadeo-alerta 1s infinite;
+ }
+ #banner-toma-alerta.show { display: flex; }
+ #banner-toma-alerta .btn-banner-pac {
+ background: rgba(255,255,255,.2); border: 1.5px solid rgba(255,255,255,.5);
+ color: #fff; border-radius: 8px; padding: 3px 12px; font-size: .8rem;
+ cursor: pointer; font-weight: 700; transition: background .12s;
+ }
+ #banner-toma-alerta .btn-banner-pac:hover { background: rgba(255,255,255,.35); }
+ #banner-toma-alerta .btn-banner-x {
+ margin-left: auto; background: none; border: none; color: rgba(255,255,255,.8);
+ font-size: 1.1rem; cursor: pointer; padding: 0 4px;
+ }
+
/* ── Toast ── */
.rec-toast {
position: fixed; bottom: 2rem; right: 2rem; z-index: 9999;
@@ -519,6 +540,14 @@ require_once __DIR__ . '/../../../shared/components/sidebar.php';
+
+
+
+ ¡Hora de la siguiente muestra!
+
+
+
+
@@ -1199,11 +1228,39 @@ const CONSENT_LBL = {
};
// ── Countdown en tarjetas de cola (toma progresiva) ──────────
+const _tomaAlertados = new Set();
+let _bannerTurnoId = null;
+
+function _playBeepLugar() {
+ try {
+ const ctx = new (window.AudioContext || window.webkitAudioContext)();
+ [880, 1100, 880, 1100].forEach(function(freq, i) {
+ const osc = ctx.createOscillator(), gain = ctx.createGain();
+ osc.connect(gain); gain.connect(ctx.destination);
+ osc.frequency.value = freq;
+ const t0 = ctx.currentTime + i * 0.22;
+ gain.gain.setValueAtTime(0.7, t0);
+ gain.gain.exponentialRampToValueAtTime(0.001, t0 + 0.18);
+ osc.start(t0); osc.stop(t0 + 0.18);
+ });
+ } catch(e) {}
+}
+
+function verPacienteToma() {
+ cerrarBannerToma();
+ if (_bannerTurnoId) seleccionarSinLlamar(_bannerTurnoId);
+}
+function cerrarBannerToma() {
+ const b = document.getElementById('banner-toma-alerta');
+ if (b) b.classList.remove('show');
+}
+
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');
+ const at = chip.dataset.tomaAt;
+ const turnoId = chip.dataset.turnoId ? parseInt(chip.dataset.turnoId) : null;
+ 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');
@@ -1211,7 +1268,23 @@ setInterval(function() {
txt.textContent = '¡Ahora!';
chip.className = 'toma-prog-chip crit';
if (card) card.classList.add('crit-card');
+ // Alerta única por turno
+ if (turnoId && !_tomaAlertados.has(turnoId)) {
+ _tomaAlertados.add(turnoId);
+ _bannerTurnoId = turnoId;
+ const pacNombre = card?.querySelector('.pac')?.textContent || 'Paciente';
+ const banner = document.getElementById('banner-toma-alerta');
+ const bannerTxt = document.getElementById('banner-toma-txt');
+ if (banner && bannerTxt) {
+ bannerTxt.textContent = '¡Hora de la siguiente muestra! — ' + pacNombre;
+ banner.classList.add('show');
+ }
+ _playBeepLugar();
+ if (navigator.vibrate) navigator.vibrate([400, 150, 400]);
+ }
} else {
+ // Si el tiempo se actualizó (nueva toma programada), resetear la alerta
+ if (turnoId) _tomaAlertados.delete(turnoId);
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';
@@ -1236,7 +1309,7 @@ setInterval(() => {
row.classList.add('alerta');
if (!info.alertado) {
info.alertado = true;
- try { new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAA...').play().catch(()=>{}); } catch(_) {}
+ _playBeepLugar();
}
} else {
row.classList.remove('alerta');
@@ -1466,6 +1539,8 @@ window.addEventListener('message', function(e) {
_tomaProgresivaActiva = false;
_setBtnCerrarConsent(true);
cerrarModalConsentimiento(true);
+ cerrarBannerToma();
+ _tomaAlertados.clear();
mostrarToast('Tomas completadas ✓', 'success', 3000);
if (turnoActivo) actualizarConsentimientos(turnoActivo.id);
}
diff --git a/ver_formulario_enviado.php b/ver_formulario_enviado.php
index 5b6593c..63937b4 100644
--- a/ver_formulario_enviado.php
+++ b/ver_formulario_enviado.php
@@ -246,21 +246,46 @@ $_soloFirmaPro = ($modoTurnero && !empty($tcRow['es_toma_progresiva']))
|| (!empty(array_filter($esquema, fn($c) => ($c['tipo'] ?? '') === 'firma_profesional'))
&& empty(array_filter($esquema, fn($c) => ($c['tipo'] ?? '') === 'firma')));
-// Muestras Prolongadas: detectar secciones hora+firma_profesional con intervalos de minutos
-// Agrupa por prefijo de sección (ej. "Taukit", "Heliprobe") para calcular esperas correctas
+// Muestras Prolongadas: detectar secciones hora+firma_profesional con intervalos de minutos.
+// Agrupa por condición del separador para no mezclar Glicemia y Cortisol (ambas usan prefijo "Toma").
$_mpMap = [];
if ($modoTurnero && $embebido && $_soloFirmaPro) {
- $_mpSecs = []; $_mpLabel = null; $_mpMin = null; $_mpHora = null;
+ $_mpSecs = []; $_mpLabel = null; $_mpMin = null; $_mpHoraFija = null; $_mpHora = null;
+ $_mpCondKey = null; $_mpCondDisplay = null;
foreach ($esquema as $_c) {
$_t = $_c['tipo'] ?? '';
if ($_t === 'separador') {
- $_mpLabel = $_c['label'] ?? ''; $_mpMin = null; $_mpHora = null;
- if (preg_match('/[Mm]inuto\s+(\d+)/u', $_mpLabel, $_mx)) $_mpMin = (int)$_mx[1];
- } elseif ($_t === 'hora' && $_mpMin !== null) {
+ $_mpLabel = $_c['label'] ?? ''; $_mpMin = null; $_mpHoraFija = null; $_mpHora = null;
+ $_mpCond = $_c['condicion'] ?? null;
+ // Clave de agrupación: usa valores de condición para distinguir Glicemia vs Cortisol
+ if ($_mpCond) {
+ $_cv = $_mpCond['valores'] ?? ($_mpCond['valor'] ? [$_mpCond['valor']] : []);
+ $_mpCondKey = implode('|', $_cv);
+ $_mpCondDisplay = trim(explode('·', $_mpLabel)[0] ?? '');
+ } else {
+ $_mpCondKey = $_mpCondDisplay = trim(explode('·', $_mpLabel)[0] ?? '');
+ }
+ if (preg_match('/[Mm]inuto\s+(\d+)/u', $_mpLabel, $_mx)) {
+ $_mpMin = (int)$_mx[1];
+ } elseif (preg_match('/(\d{1,2}):(\d{2})\s*(a\.?m\.?|p\.?m\.?)/i', $_mpLabel, $_mx)) {
+ $_h = (int)$_mx[1]; $_m2 = (int)$_mx[2];
+ $_pm = strtolower(preg_replace('/[^apm]/i', '', $_mx[3])) === 'pm';
+ if ($_pm && $_h < 12) $_h += 12;
+ $_mpHoraFija = sprintf('%02d:%02d', $_h, $_m2);
+ $_mpMin = PHP_INT_MAX; // sentinel: va después de todos los Minuto X
+ }
+ } elseif ($_t === 'hora' && ($_mpMin !== null || $_mpHoraFija !== null)) {
$_mpHora = $_c['id'] ?? null;
- } elseif ($_t === 'firma_profesional' && $_mpHora !== null && $_mpMin !== null) {
- $_prefix = trim(explode('·', $_mpLabel)[0] ?? '');
- $_mpSecs[] = ['label' => $_mpLabel, 'min' => $_mpMin, 'hora' => $_mpHora, 'firma' => $_c['id'], 'tipo' => $_prefix];
+ } elseif ($_t === 'firma_profesional' && $_mpHora !== null && ($_mpMin !== null || $_mpHoraFija !== null)) {
+ $_mpSecs[] = [
+ 'label' => $_mpLabel,
+ 'min' => $_mpMin ?? PHP_INT_MAX,
+ 'hora_fija' => $_mpHoraFija,
+ 'hora' => $_mpHora,
+ 'firma' => $_c['id'],
+ 'tipo' => $_mpCondKey,
+ 'display' => $_mpCondDisplay,
+ ];
$_mpHora = null;
}
}
@@ -271,11 +296,10 @@ if ($modoTurnero && $embebido && $_soloFirmaPro) {
$_s = $_secs[$i]; $_nx = $_secs[$i + 1] ?? null;
$_mpMap[$_s['firma']] = [
'hora_campo' => $_s['hora'],
- 'esperar_min' => $_nx ? ($_nx['min'] - $_s['min']) : 0,
'next_label' => $_nx ? $_nx['label'] : null,
'next_firma_id' => $_nx ? $_nx['firma'] : null,
'is_last' => !$_nx,
- 'exam_type' => $_tipo,
+ 'exam_type' => $_s['display'],
];
}
}
@@ -1190,14 +1214,13 @@ function _guardarFirmaMP(widget, svg, msgEl, entry, campoId) {
widget.replaceWith(box);
if (data.completado) {
- // 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 = 'Toma ' + (entry.exam_type || '') + ' completada.
Cerrando...
';
- document.querySelector('.doc-body').appendChild(ok);
+ // Toma completada: mostrar resumen de tomas y cerrar modal
+ var cd = document.getElementById('mp-countdown');
+ if (cd) { clearInterval(cd._mpTick); cd.remove(); }
+ _mpMostrarResumen(entry.exam_type || 'Examen');
setTimeout(function() {
try { window.parent.postMessage({ type: 'turneroFirmado' }, '*'); } catch(e) {}
- }, 1200);
+ }, 2200);
} else if (data.siguiente_toma_at) {
var targetMs = new Date(data.siguiente_toma_at.replace(' ', 'T')).getTime();
var minsLeft = Math.max(1, Math.round((targetMs - Date.now()) / 60000));
@@ -1290,43 +1313,52 @@ function _mpIniciarCountdown(minutos, nextLabel, nextFirmaId) {
}, 1000);
}
-function _mpMostrarFinalizar(examType) {
- if (document.getElementById('mp-finalizar')) return;
+function _mpMostrarResumen(examType) {
+ if (document.getElementById('mp-resumen')) return;
+ // Recopilar horas y resultados de las tomas visibles
+ var rows = [];
+ if (window._muestrasMap) {
+ Object.keys(window._muestrasMap).forEach(function(fid) {
+ var entry = window._muestrasMap[fid];
+ if (!entry || entry.exam_type !== examType) return;
+ var horaEl = entry.hora_campo ? document.querySelector('[name="' + entry.hora_campo + '"]') : null;
+ var horaVal = horaEl ? horaEl.value : '';
+ // Buscar campo resultado adyacente (previo al firma widget)
+ var fpw = document.getElementById('fpw-' + fid);
+ var resVal = '';
+ if (fpw) {
+ var prev = fpw.previousElementSibling;
+ while (prev) {
+ var inp = prev.querySelector && prev.querySelector('input[type="number"],input[type="text"]');
+ if (inp) { resVal = inp.value; break; }
+ prev = prev.previousElementSibling;
+ }
+ }
+ // Separador label para esta firma
+ var secLabel = entry.next_label || ('Toma ' + fid);
+ // Si tiene next_label del ANTERIOR, buscamos la label de esta entrada en el DOM
+ var sepEl = document.querySelector('[data-campo-id]');
+ rows.push({ hora: horaVal, resultado: resVal, firmado: !!(window._mpSavedFirmas && window._mpSavedFirmas[fid]) });
+ });
+ }
+ var wrap = document.createElement('div');
+ wrap.id = 'mp-resumen';
+ wrap.className = 'alert alert-success mt-4';
+ var rowsHtml = rows.length
+ ? ''
+ + '| Hora | Resultado | |
'
+ + rows.map(function(r) {
+ return '| ' + (r.hora || '—') + ' | '
+ + '' + (r.resultado || '—') + ' | '
+ + ' |
';
+ }).join('') + '
' : '';
+ wrap.innerHTML = ''
+ + '
' + (examType || 'Examen') + ' completado.
'
+ + 'Todas las tomas registradas. Cerrando...
' + rowsHtml;
+ document.querySelector('.doc-body').appendChild(wrap);
+ // Ocultar countdown si queda
var cd = document.getElementById('mp-countdown');
if (cd) { clearInterval(cd._mpTick); cd.remove(); }
- var wrap = document.createElement('div');
- wrap.id = 'mp-finalizar';
- wrap.style.cssText = 'position:sticky;bottom:12px;margin:12px 0;text-align:center;z-index:50';
- var btn = document.createElement('button');
- btn.className = 'btn btn-success fw-bold px-4 py-2';
- btn.style.fontSize = '1rem';
- btn.innerHTML = 'Finalizar toma · ' + (examType || 'Muestras');
- btn.addEventListener('click', function() {
- btn.disabled = true;
- btn.innerHTML = 'Guardando...';
- 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;
- });
- Object.assign(dr, window._mpSavedFirmas || {});
- fetch(window.location.href, {
- method:'POST', headers:{'Content-Type':'application/json'},
- body: JSON.stringify({ mp_completar: true, datos_respuestas: dr })
- })
- .then(function(r){ return r.json(); })
- .then(function(data) {
- if (data.ok) {
- wrap.innerHTML = 'Toma completada correctamente.
';
- try { window.parent.postMessage({ type:'turneroFirmado' }, '*'); } catch(e) {}
- }
- })
- .catch(function(){});
- });
- wrap.appendChild(btn);
- document.querySelector('.doc-body').appendChild(wrap);
}
/* ── Canvas firma del profesional ───────────────────────────────── */