feat(turnero): toma prolongada secuencial + bloqueo muestras/cierre
- ver_formulario_enviado: auto-fill médico (nombres/especialidad/código) en datos_prefilled desde solicitud - ver_formulario_enviado: canvases de toma progresiva bloqueados secuencialmente — solo el turno activo es firmable, los siguientes aparecen al expirar el countdown - ver_formulario_enviado: countdown prominente con reloj grande, color dinámico, sonido (Web Audio) y vibración al llegar la hora - lugar: bloquear cierre del modal durante toma progresiva activa (botón X deshabilitado hasta mp_completar) - lugar: bloquear Finalizar atención si hay muestras pendientes sin decisión (recibida/rechazada) - lugar: _syncBtnFinalizar combina consentimientos + muestras para el aviso y estado del botón - lugar: restricción de usuario a lugar específico via turnero_lugar_id (admin_users) + bloqueo JS en confirmarLugar() - migrations: turnero_lugar_id en admin_users para restringir bacteriólogos por usuario Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
0e116359f8
commit
f4da864db7
+72
-17
@@ -37,12 +37,16 @@ if ($modoTurnero) {
|
||||
p.numero_documento, p.tipo_documento,
|
||||
p.fecha_nacimiento, p.telefono AS paciente_telefono, p.eps,
|
||||
t.sesion_id,
|
||||
ts.numero_orden
|
||||
ts.numero_orden,
|
||||
med.nombres AS medico_nombres, med.apellidos AS medico_apellidos,
|
||||
med.cod_especialidad AS medico_especialidad,
|
||||
med.codigo AS medico_codigo, med.docidmedico AS medico_docid
|
||||
FROM turnero_consentimientos tc
|
||||
JOIN lab_formularios f ON f.id = tc.formulario_id
|
||||
JOIN turnero_turnos t ON t.id = tc.turno_id
|
||||
LEFT JOIN turnero_solicitudes ts ON ts.turno_id = tc.turno_id
|
||||
LEFT JOIN lab_pacientes p ON p.id = COALESCE(ts.paciente_id, t.paciente_id)
|
||||
LEFT JOIN medicos med ON med.id = ts.medico_id
|
||||
WHERE tc.token = ?",
|
||||
[$tokenTurnero]
|
||||
);
|
||||
@@ -124,11 +128,15 @@ if ($modoTurnero) {
|
||||
'firma_profesional_svg' => $tcRow['firma_profesional_svg'] ?? null,
|
||||
'datos_cliente' => $tcRow['datos_respuestas'] ?: '{}',
|
||||
'datos_prefilled' => json_encode(['__paciente' => [
|
||||
'nombre_completo' => $tcRow['paciente_nombre'] ?? '',
|
||||
'numero_documento' => $tcRow['numero_documento'] ?? '',
|
||||
'tipo_documento' => $tcRow['tipo_documento'] ?? '',
|
||||
'telefono' => $tcRow['paciente_telefono'] ?? '',
|
||||
'eps' => $tcRow['eps'] ?? '',
|
||||
'nombre_completo' => $tcRow['paciente_nombre'] ?? '',
|
||||
'numero_documento' => $tcRow['numero_documento'] ?? '',
|
||||
'tipo_documento' => $tcRow['tipo_documento'] ?? '',
|
||||
'telefono' => $tcRow['paciente_telefono'] ?? '',
|
||||
'eps' => $tcRow['eps'] ?? '',
|
||||
'medico_nombre' => trim(($tcRow['medico_nombres'] ?? '') . ' ' . ($tcRow['medico_apellidos'] ?? '')),
|
||||
'medico_especialidad'=> $tcRow['medico_especialidad'] ?? '',
|
||||
'medico_codigo' => $tcRow['medico_codigo'] ?? '',
|
||||
'medico_docid' => $tcRow['medico_docid'] ?? '',
|
||||
]]),
|
||||
'created_at' => $tcRow['enviado_at'] ?? date('Y-m-d H:i:s'),
|
||||
'completado_en' => $tcRow['firmado_at'],
|
||||
@@ -271,6 +279,17 @@ if ($modoTurnero && $embebido && $_soloFirmaPro) {
|
||||
}
|
||||
}
|
||||
|
||||
// Primera firma pendiente en toma progresiva (las demás se ocultan hasta su turno)
|
||||
$_mpPrimeraPendiente = null;
|
||||
if (!empty($_mpMap)) {
|
||||
foreach ($_mpMap as $_fid => $_) {
|
||||
if (empty($datosCliente[$_fid . '_svg'])) {
|
||||
$_mpPrimeraPendiente = $_fid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mapa id → label
|
||||
$labelMap = [];
|
||||
foreach ($esquema as $c) {
|
||||
@@ -704,11 +723,18 @@ function esc2(mixed $v): string {
|
||||
<script>window._fpwPreFirma = <?= json_encode($firmaProfPreguardada) ?>;</script>
|
||||
<?php endif; ?>
|
||||
<!-- Canvas del profesional -->
|
||||
<?php
|
||||
$_mpBloqueado = !empty($_mpMap)
|
||||
&& $_mpPrimeraPendiente !== null
|
||||
&& $cid !== $_mpPrimeraPendiente
|
||||
&& empty($datosCliente[$cid . '_svg']);
|
||||
?>
|
||||
<div class="firma-pro-widget no-print" id="fpw-<?= htmlspecialchars($cid) ?>"
|
||||
data-envio="<?= (int)$envio['id'] ?>" data-campo="<?= htmlspecialchars($cid) ?>"
|
||||
data-solo-pro="<?= ($modoTurnero && $_soloFirmaPro) ? '1' : '0' ?>"
|
||||
data-turno="<?= isset($tcRow) ? (int)$tcRow['turno_id'] : '' ?>"
|
||||
data-formulario="<?= isset($tcRow) ? (int)$tcRow['formulario_id'] : '' ?>">
|
||||
data-formulario="<?= isset($tcRow) ? (int)$tcRow['formulario_id'] : '' ?>"
|
||||
<?= $_mpBloqueado ? 'style="display:none"' : '' ?>>
|
||||
<?php if ($firmaProfPreguardada): ?>
|
||||
<div class="fpw-oneclic">
|
||||
<button class="btn btn-success fpw-oneclic-btn w-100" style="font-size:1rem;padding:.55rem 1rem">
|
||||
@@ -1139,39 +1165,68 @@ function _guardarFirmaMP(widget, svg, msgEl, entry, campoId) {
|
||||
});
|
||||
}
|
||||
|
||||
function _mpPlayBeep() {
|
||||
try {
|
||||
var ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
[880, 1100, 880, 1100].forEach(function(freq, i) {
|
||||
var osc = ctx.createOscillator(), gain = ctx.createGain();
|
||||
osc.connect(gain); gain.connect(ctx.destination);
|
||||
osc.frequency.value = freq;
|
||||
var 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 _mpIniciarCountdown(minutos, nextLabel, nextFirmaId) {
|
||||
var targetMs = Date.now() + minutos * 60000;
|
||||
var box = document.getElementById('mp-countdown');
|
||||
if (!box) {
|
||||
box = document.createElement('div');
|
||||
box.id = 'mp-countdown';
|
||||
box.style.cssText = 'position:sticky;bottom:12px;background:#1565c0;color:#fff;padding:10px 16px;border-radius:8px;margin:12px 0;text-align:center;font-size:.9rem;z-index:50';
|
||||
box.style.cssText = 'position:sticky;bottom:0;left:0;right:0;background:#1565c0;color:#fff;'
|
||||
+ 'padding:14px 20px;margin:16px -14px -12px;text-align:center;z-index:60;'
|
||||
+ 'box-shadow:0 -4px 20px rgba(21,101,192,.35)';
|
||||
document.querySelector('.doc-body').appendChild(box);
|
||||
}
|
||||
if (box._mpTick) clearInterval(box._mpTick);
|
||||
|
||||
// Notificar al padre que bloquee el cierre
|
||||
try { window.parent.postMessage({ type: 'tomaProgresivaIniciada' }, '*'); } catch(e) {}
|
||||
|
||||
box._mpTick = setInterval(function() {
|
||||
var left = targetMs - Date.now();
|
||||
if (left <= 0) {
|
||||
clearInterval(box._mpTick);
|
||||
box.style.background = '#dc3545';
|
||||
box.style.animation = 'mp-pulse 1s ease-in-out infinite';
|
||||
box.innerHTML = '<strong><i class="fas fa-bell me-2"></i>¡Hora de la siguiente muestra!</strong>'
|
||||
+ (nextLabel ? '<br><small>' + nextLabel + '</small>' : '');
|
||||
if (navigator.vibrate) navigator.vibrate([400,200,400,200,400]);
|
||||
var nw = nextFirmaId ? document.getElementById('fpw-' + nextFirmaId) : document.querySelector('.firma-pro-widget');
|
||||
box.innerHTML = '<div style="font-size:1.1rem;font-weight:700"><i class="fas fa-bell me-2"></i>¡Hora de la siguiente muestra!</div>'
|
||||
+ (nextLabel ? '<div style="font-size:.85rem;opacity:.9;margin-top:4px">' + nextLabel + '</div>' : '');
|
||||
if (navigator.vibrate) navigator.vibrate([500,150,500,150,500]);
|
||||
_mpPlayBeep();
|
||||
// Desbloquear y mostrar siguiente canvas
|
||||
var nw = nextFirmaId ? document.getElementById('fpw-' + nextFirmaId) : null;
|
||||
if (!nw) nw = document.querySelector('.firma-pro-widget[style*="display:none"]');
|
||||
if (nw) {
|
||||
nw.scrollIntoView({ behavior:'smooth', block:'center' });
|
||||
nw.style.display = '';
|
||||
nw.style.outline = '3px solid #dc3545';
|
||||
nw.style.borderRadius = '6px';
|
||||
nw.style.borderRadius = '8px';
|
||||
setTimeout(function() { nw.scrollIntoView({ behavior:'smooth', block:'center' }); }, 120);
|
||||
}
|
||||
return;
|
||||
}
|
||||
var m = Math.floor(left / 60000);
|
||||
var s = Math.floor((left % 60000) / 1000);
|
||||
box.innerHTML = '⏱ Próxima muestra'
|
||||
+ (nextLabel ? ' <small>(' + nextLabel + ')</small>' : '')
|
||||
+ ' en <strong>' + String(m).padStart(2,'0') + ':' + String(s).padStart(2,'0') + '</strong>';
|
||||
var pct = Math.max(0, left / (minutos * 60000)) * 100;
|
||||
box.style.background = pct < 15 ? '#f59e0b' : '#1565c0';
|
||||
box.style.animation = '';
|
||||
box.innerHTML = '<div style="font-size:.75rem;opacity:.85;margin-bottom:4px;text-transform:uppercase;letter-spacing:.05em">'
|
||||
+ '<i class="fas fa-clock me-1"></i>Próxima muestra' + (nextLabel ? ' · ' + nextLabel : '') + '</div>'
|
||||
+ '<div style="font-size:2.4rem;font-weight:900;font-family:monospace;line-height:1;letter-spacing:.04em">'
|
||||
+ String(m).padStart(2,'0') + '<span style="opacity:.6;animation:mp-pulse .8s infinite">:</span>' + String(s).padStart(2,'0')
|
||||
+ '</div>';
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user