Mejoras F-LAB-28 tomas prolongadas: bugfixes + UX + datos

- guardar_toma.php: reconoce firma campo_id_svg (compat. registros históricos)
- get_consentimientos.php: mismo fix para conteo tomas_firmadas
- ver_formulario_enviado.php: $_mpMap incluye hora fija (Cortisol 3pm/4pm),
  agrupa por condición de separador para no mezclar Glicemia/Cortisol,
  muestra resumen de tomas al completar antes de cerrar el modal
- lugar.php: banner rojo + beep cuando el countdown de una toma llega a cero,
  resetea alerta al cerrar el modal; función _playBeepLugar() compartida

DB Form 15: +10 campos (firma_profesional Minuto 60 Glicemia + campo
Resultado numérico en cada toma Glicemia y Cortisol)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-09 20:26:12 -05:00
co-authored by Claude Sonnet 4.6
parent 848dd8e4d0
commit fdc04d615c
4 changed files with 169 additions and 59 deletions
+84 -52
View File
@@ -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 = '<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);
// 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
? '<table class="table table-sm mt-2 mb-0" style="font-size:.82rem"><thead><tr>'
+ '<th>Hora</th><th>Resultado</th><th></th></tr></thead><tbody>'
+ rows.map(function(r) {
return '<tr><td>' + (r.hora || '—') + '</td>'
+ '<td>' + (r.resultado || '—') + '</td>'
+ '<td style="color:#198754"><i class="fas fa-check-circle"></i></td></tr>';
}).join('') + '</tbody></table>' : '';
wrap.innerHTML = '<div class="d-flex align-items-center gap-2"><i class="fas fa-check-double fs-5"></i>'
+ '<div><strong>' + (examType || 'Examen') + ' completado.</strong><br>'
+ '<small>Todas las tomas registradas. Cerrando...</small></div></div>' + 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 = '<i class="fas fa-check-double me-2"></i>Finalizar toma · ' + (examType || 'Muestras');
btn.addEventListener('click', function() {
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>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 = '<div class="alert alert-success py-2 mb-0"><i class="fas fa-check-circle me-2"></i>Toma completada correctamente.</div>';
try { window.parent.postMessage({ type:'turneroFirmado' }, '*'); } catch(e) {}
}
})
.catch(function(){});
});
wrap.appendChild(btn);
document.querySelector('.doc-body').appendChild(wrap);
}
/* ── Canvas firma del profesional ───────────────────────────────── */