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
@@ -1,23 +1,40 @@
|
||||
<?php
|
||||
/**
|
||||
* GET modules/turnero/api/get_consecutivo.php
|
||||
* Devuelve el siguiente número de orden del día (YYYYMMDD-NNN).
|
||||
* Devuelve el siguiente número de orden del día.
|
||||
*
|
||||
* Parámetros:
|
||||
* tipo string 'F' = Físico/turnero (default) | 'D' = Domicilio
|
||||
*
|
||||
* Formato resultante:
|
||||
* F-YYYYMMDD-NNN para tipo=F
|
||||
* D-YYYYMMDD-NNN para tipo=D
|
||||
*/
|
||||
require_once __DIR__ . '/_helpers.php';
|
||||
requireTurnero();
|
||||
requireMethod('GET');
|
||||
|
||||
$pdo = db();
|
||||
$prefix = date('Ymd') . '-';
|
||||
$tipo = strtoupper(trim($_GET['tipo'] ?? 'F'));
|
||||
if (!in_array($tipo, ['F', 'D'], true)) $tipo = 'F';
|
||||
|
||||
$row = $pdo->prepare(
|
||||
"SELECT MAX(CAST(SUBSTRING_INDEX(numero_orden,'-',-1) AS UNSIGNED)) AS ultimo
|
||||
FROM turnero_solicitudes
|
||||
WHERE numero_orden LIKE ?"
|
||||
);
|
||||
$row->execute([$prefix . '%']);
|
||||
$ultimo = (int)($row->fetch(PDO::FETCH_ASSOC)['ultimo'] ?? 0);
|
||||
$pdo = db();
|
||||
$prefix = $tipo . '-' . date('Ymd') . '-';
|
||||
|
||||
if ($tipo === 'D') {
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT MAX(CAST(SUBSTRING_INDEX(numero_orden, '-', -1) AS UNSIGNED)) AS ultimo
|
||||
FROM lab_domicilios
|
||||
WHERE numero_orden LIKE ?"
|
||||
);
|
||||
} else {
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT MAX(CAST(SUBSTRING_INDEX(numero_orden, '-', -1) AS UNSIGNED)) AS ultimo
|
||||
FROM turnero_solicitudes
|
||||
WHERE numero_orden LIKE ?"
|
||||
);
|
||||
}
|
||||
$stmt->execute([$prefix . '%']);
|
||||
$ultimo = (int)($stmt->fetch(PDO::FETCH_ASSOC)['ultimo'] ?? 0);
|
||||
|
||||
$siguiente = $prefix . str_pad($ultimo + 1, 3, '0', STR_PAD_LEFT);
|
||||
|
||||
jsonOk(['consecutivo' => $siguiente]);
|
||||
|
||||
@@ -10,6 +10,7 @@ if (!isUserLoggedIn()) {
|
||||
}
|
||||
|
||||
// Tablet asignada → forzar su lugar, bloquear cualquier otro
|
||||
$lugarForzado = 0;
|
||||
if (!empty($_SESSION['turnero_dispositivo'])) {
|
||||
$_d = $_SESSION['turnero_dispositivo'];
|
||||
$_forzado = (int)$_d['lugar_id'];
|
||||
@@ -19,6 +20,17 @@ if (!empty($_SESSION['turnero_dispositivo'])) {
|
||||
if ((int)($_GET['lugar_id'] ?? 0) !== $_forzado) {
|
||||
header('Location: ' . BASE_URL . 'erp.php?m=turnero&v=lugar&lugar_id=' . $_forzado); exit;
|
||||
}
|
||||
$lugarForzado = $_forzado;
|
||||
}
|
||||
// Restricción a nivel de usuario (turnero_lugar_id en admin_users)
|
||||
if (!$lugarForzado) {
|
||||
$_userLugar = (int)($_SESSION['admin_user']['turnero_lugar_id'] ?? 0);
|
||||
if ($_userLugar) {
|
||||
if ((int)($_GET['lugar_id'] ?? 0) !== $_userLugar) {
|
||||
header('Location: ' . BASE_URL . 'erp.php?m=turnero&v=lugar&lugar_id=' . $_userLugar); exit;
|
||||
}
|
||||
$lugarForzado = $_userLugar;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -495,9 +507,11 @@ require_once __DIR__ . '/../../../shared/components/sidebar.php';
|
||||
onclick="abrirModalMiFirma()" title="<?= $adminFirmaSvg ? 'Ver / cambiar mi firma' : 'Configurar mi firma' ?>">
|
||||
<i class="fas fa-signature me-1"></i><?= $adminFirmaSvg ? 'Mi firma' : 'Configurar firma' ?>
|
||||
</button>
|
||||
<?php if (!$lugarForzado): ?>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="cambiarLugar()">
|
||||
<i class="fas fa-exchange-alt me-1"></i>Cambiar
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -751,8 +765,9 @@ require_once __DIR__ . '/../../../shared/components/sidebar.php';
|
||||
<span style="font-weight:700;font-size:.95rem;color:#1e293b">
|
||||
<i class="fas fa-file-signature me-2 text-primary"></i>Formulario de consentimiento
|
||||
</span>
|
||||
<button onclick="cerrarModalConsentimiento()"
|
||||
style="background:none;border:none;font-size:1.2rem;color:#94a3b8;cursor:pointer;padding:2px 6px">
|
||||
<button id="btn-cerrar-modal-consent" onclick="cerrarModalConsentimiento()"
|
||||
style="background:none;border:none;font-size:1.2rem;color:#94a3b8;cursor:pointer;padding:2px 6px"
|
||||
title="Cerrar">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -769,6 +784,7 @@ require_once __DIR__ . '/../../../shared/components/sidebar.php';
|
||||
|
||||
<script>
|
||||
// ── Estado global ─────────────────────────────────────────────
|
||||
const LUGAR_FORZADO = <?= $lugarForzado ?: 0 ?>;
|
||||
let lugarId = <?= $lugarIdParam ?: 0 ?>;
|
||||
let turnoActivo = null;
|
||||
let tieneConsent = false;
|
||||
@@ -777,6 +793,8 @@ let pollingColaId = null;
|
||||
let pollingConsentId = null;
|
||||
let _consentTokenCache = null;
|
||||
let _pacienteActivo = null;
|
||||
let _tomaProgresivaActiva = false;
|
||||
let hayMuestrasPendientes = false;
|
||||
let _solicitudActiva = null;
|
||||
let _muestrasActivas = [];
|
||||
|
||||
@@ -828,6 +846,7 @@ function confirmarLugar() {
|
||||
const sel = document.getElementById('sel-lugar-init');
|
||||
const id = parseInt(sel.value);
|
||||
if (!id) return mostrarError('Seleccione un lugar.');
|
||||
if (LUGAR_FORZADO && id !== LUGAR_FORZADO) return mostrarError('No tiene acceso a esa estación.');
|
||||
lugarId = id;
|
||||
const txt = sel.options[sel.selectedIndex].text;
|
||||
document.getElementById('lbl-lugar-titulo').textContent = txt;
|
||||
@@ -1146,6 +1165,24 @@ setInterval(() => {
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
function _syncBtnFinalizar() {
|
||||
const btnFin = document.getElementById('btn-finalizar');
|
||||
const warnStrip = document.getElementById('accion-consent-warn');
|
||||
const warnTxt = document.getElementById('accion-warn-txt');
|
||||
const bloqueado = hayPendientes || hayMuestrasPendientes;
|
||||
if (btnFin) btnFin.disabled = bloqueado;
|
||||
if (!warnStrip || !warnTxt) return;
|
||||
if (bloqueado) {
|
||||
const msgs = [];
|
||||
if (hayPendientes) msgs.push('consentimientos pendientes');
|
||||
if (hayMuestrasPendientes) msgs.push('muestras sin decisión');
|
||||
warnTxt.textContent = msgs.join(' · ');
|
||||
warnStrip.classList.remove('d-none');
|
||||
} else {
|
||||
warnStrip.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
function renderConsentimientos(lista) {
|
||||
// Mostrar consentimientos de examen (origen null) + los de esta estación
|
||||
lista = lista.filter(c => !c.origen_lugar_id || c.origen_lugar_id == lugarId);
|
||||
@@ -1159,29 +1196,17 @@ function renderConsentimientos(lista) {
|
||||
const banner = document.getElementById('bloqueo-banner');
|
||||
const btnFin = document.getElementById('btn-finalizar');
|
||||
|
||||
// Aviso en barra de acciones
|
||||
const warnStrip = document.getElementById('accion-consent-warn');
|
||||
const warnTxt = document.getElementById('accion-warn-txt');
|
||||
if (warnStrip) {
|
||||
if (hayPendientes) {
|
||||
warnStrip.classList.remove('d-none');
|
||||
warnTxt.textContent = pendCount + ' consentimiento' + (pendCount > 1 ? 's' : '') + ' pendiente' + (pendCount > 1 ? 's' : '');
|
||||
} else {
|
||||
warnStrip.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
_syncBtnFinalizar();
|
||||
|
||||
if (!tieneConsent) {
|
||||
listEl.innerHTML = '';
|
||||
sinEl.classList.remove('d-none');
|
||||
banner.classList.add('d-none');
|
||||
if (btnFin) btnFin.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
sinEl.classList.add('d-none');
|
||||
banner.classList.toggle('d-none', !hayPendientes);
|
||||
if (btnFin) btnFin.disabled = hayPendientes;
|
||||
|
||||
// Actualizar mapa de timers para tomas progresivas
|
||||
_tomaTimers = {};
|
||||
@@ -1324,14 +1349,25 @@ function abrirTomaProgresiva(token, turnoId, formularioId) {
|
||||
iframe.src = '';
|
||||
load.style.display = '';
|
||||
modal.style.display = 'flex';
|
||||
// Pasa modo=toma_progresiva para que el renderer solo muestre
|
||||
// los campos del siguiente tiempo pendiente y el botón de guardar parcial
|
||||
_tomaProgresivaActiva = true;
|
||||
_setBtnCerrarConsent(false);
|
||||
setTimeout(() => {
|
||||
iframe.src = `${BASE_WA}ver_formulario_enviado.php?token=${encodeURIComponent(token)}&embed=1&compact=1&modo=toma_progresiva`;
|
||||
}, 80);
|
||||
}
|
||||
|
||||
function cerrarModalConsentimiento() {
|
||||
function _setBtnCerrarConsent(habilitado) {
|
||||
const btn = document.getElementById('btn-cerrar-modal-consent');
|
||||
if (!btn) return;
|
||||
btn.disabled = !habilitado;
|
||||
btn.style.opacity = habilitado ? '' : '.25';
|
||||
btn.title = habilitado ? 'Cerrar' : 'No puede cerrar durante una toma en progreso';
|
||||
}
|
||||
|
||||
function cerrarModalConsentimiento(forzar) {
|
||||
if (!forzar && _tomaProgresivaActiva) return; // bloqueado hasta finalizar toma
|
||||
_tomaProgresivaActiva = false;
|
||||
_setBtnCerrarConsent(true);
|
||||
const modal = document.getElementById('modal-consentimiento');
|
||||
const iframe = document.getElementById('modal-consent-iframe');
|
||||
modal.style.display = 'none';
|
||||
@@ -1342,8 +1378,14 @@ function cerrarModalConsentimiento() {
|
||||
}
|
||||
|
||||
window.addEventListener('message', function(e) {
|
||||
if (e.data && e.data.type === 'tomaProgresivaIniciada') {
|
||||
_tomaProgresivaActiva = true;
|
||||
_setBtnCerrarConsent(false);
|
||||
}
|
||||
if (e.data && e.data.type === 'turneroFirmado') {
|
||||
cerrarModalConsentimiento();
|
||||
_tomaProgresivaActiva = false;
|
||||
_setBtnCerrarConsent(true);
|
||||
cerrarModalConsentimiento(true);
|
||||
mostrarToast('Consentimiento firmado ✓', 'success', 3000);
|
||||
if (turnoActivo) actualizarConsentimientos(turnoActivo.id);
|
||||
}
|
||||
@@ -1380,7 +1422,8 @@ async function iniciarAtencion() {
|
||||
|
||||
async function finalizarAtencion() {
|
||||
if (!turnoActivo) return;
|
||||
if (hayPendientes) { mostrarError('Debe firmar todos los consentimientos antes de finalizar.'); return; }
|
||||
if (hayPendientes) { mostrarError('Debe firmar todos los consentimientos antes de finalizar.'); return; }
|
||||
if (hayMuestrasPendientes) { mostrarError('Debe marcar cada muestra como recibida o rechazada antes de finalizar.'); return; }
|
||||
if (!confirm(`¿Finalizar atención del turno ${turnoActivo.codigo}?`)) return;
|
||||
await cambiarEstadoTurno('finalizado');
|
||||
}
|
||||
@@ -1429,12 +1472,13 @@ async function cambiarEstadoTurno(nuevoEstado) {
|
||||
function resetFicha() {
|
||||
clearInterval(pollingConsentId);
|
||||
turnoActivo = null;
|
||||
tieneConsent = false;
|
||||
hayPendientes = false;
|
||||
_consentTokenCache = null;
|
||||
_pacienteActivo = null;
|
||||
_solicitudActiva = null;
|
||||
_muestrasActivas = [];
|
||||
tieneConsent = false;
|
||||
hayPendientes = false;
|
||||
hayMuestrasPendientes = false;
|
||||
_consentTokenCache = null;
|
||||
_pacienteActivo = null;
|
||||
_solicitudActiva = null;
|
||||
_muestrasActivas = [];
|
||||
const secMuestras = document.getElementById('sec-muestras');
|
||||
if (secMuestras) secMuestras.classList.add('d-none');
|
||||
document.getElementById('ficha-orden').classList.add('d-none');
|
||||
@@ -1750,6 +1794,9 @@ function renderMuestras(lista) {
|
||||
const nRec = lista.filter(m => m.estado === 'recibida').length;
|
||||
const nRech = lista.filter(m => m.estado === 'rechazada').length;
|
||||
|
||||
hayMuestrasPendientes = nPend > 0;
|
||||
_syncBtnFinalizar();
|
||||
|
||||
let chips = '';
|
||||
if (nPend) chips += `<span class="mc pend">${nPend} pendiente${nPend > 1 ? 's' : ''}</span>`;
|
||||
if (nRec) chips += `<span class="mc rec">${nRec} recibida${nRec > 1 ? 's' : ''}</span>`;
|
||||
|
||||
Reference in New Issue
Block a user