Files
Lizandro GuarnizoandClaude Opus 5 6589503c57 Tablet de firma para el paciente en Recepción 4
Hasta ahora, para que el paciente firmara el consentimiento de bienvenida
había que girarle el monitor a la recepcionista o pasarle el mouse: el botón
Firmar abre el formulario en la pantalla de ella.

Se agrega una pantalla aparte (/erp.php?m=turnero&v=firma) para poner frente
al paciente. Sola, sin que la recepcionista haga nada: cuando el turno entra
a ese puesto y le falta el F-LAB-01, aparece su nombre y un botón grande de
firmar; al terminar dice gracias y vuelve a reposo.

Reutiliza lo que ya existía: el formulario de ver_formulario_enviado.php, el
aviso 'turneroFirmado' que ya emite al firmar, y el sondeo de recepción, que
seguirá poniendo el renglón en verde sin cambios.

Sobre el acceso: va en PUBLIC_ROUTES porque nadie va a iniciar sesión cada
mañana en una tablet que manipula el público. No queda abierta: se identifica
por la cookie del dispositivo, y sin ella no muestra ningún dato. El endpoint
devuelve solo el turno que está en ese puesto en ese instante —no permite
buscar, ni ver otros, ni consultar historial— y descarta turnos de días
anteriores, igual que el televisor. La tablet debe quedar en modo kiosco.

Falta registrar el dispositivo de Recepción 4 desde Configuración; hoy no
tiene ninguno.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 16:35:20 -05:00

201 lines
7.5 KiB
PHP

<?php
/**
* modules/turnero/views/firma.php
* Tablet de firma del paciente — /erp.php?m=turnero&v=firma
*
* Pantalla de cara al público: se pone frente al paciente en el mostrador y
* muestra, sola, el consentimiento de bienvenida cuando le toca firmarlo.
* Antes había que girarle el monitor a la recepcionista o pasarle el mouse.
*
* La tablet se identifica por la cookie del dispositivo, registrada desde
* Configuración. Sin ella no muestra nada: es lo que impide que cualquiera
* abra esta dirección y vea el nombre del paciente de turno.
*
* Conviene dejarla en modo kiosco, sin barra de direcciones, para que desde
* ella no se pueda navegar al resto del ERP.
*/
$_fCfg = [];
try {
$__pdo = Database::getInstance()->getConnection();
$_fCfg = $__pdo->query(
"SELECT clave, valor FROM lab_config WHERE clave IN ('empresa_nombre','doc_logo_base64','doc_color')"
)->fetchAll(PDO::FETCH_KEY_PAIR) ?: [];
} catch (\Throwable $_) {}
$_fNombre = htmlspecialchars($_fCfg['empresa_nombre'] ?? 'Laboratorio');
$_fLogo = $_fCfg['doc_logo_base64'] ?? '';
$_fColor = preg_match('/^#[0-9a-fA-F]{3,8}$/', $_fCfg['doc_color'] ?? '') ? $_fCfg['doc_color'] : '#1565c0';
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Firma · <?= $_fNombre ?></title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root { --brand: <?= $_fColor ?>; }
html, body {
height: 100%; width: 100%; overflow: hidden;
font-family: 'Segoe UI', system-ui, sans-serif;
background: #f8fafc; color: #1e293b;
-webkit-user-select: none; user-select: none;
}
.pantalla {
height: 100vh; display: flex; flex-direction: column;
align-items: center; justify-content: center;
text-align: center; padding: 4vh 5vw; gap: 2.5vh;
}
.pantalla.oculta { display: none; }
.logo { max-height: 14vh; max-width: 50vw; object-fit: contain; }
.marca { font-size: clamp(1.2rem, 3vw, 2rem); font-weight: 700; color: #64748b; }
.saludo { font-size: clamp(1.4rem, 4vw, 2.6rem); font-weight: 600; color: #94a3b8; }
.etiqueta { font-size: clamp(.9rem, 2vw, 1.2rem); font-weight: 700;
letter-spacing: 3px; text-transform: uppercase; color: #94a3b8; }
.paciente { font-size: clamp(1.8rem, 5.5vw, 3.4rem); font-weight: 800; line-height: 1.15; }
.codigo { font-size: clamp(1.1rem, 2.6vw, 1.6rem); font-weight: 700; color: var(--brand); }
/* Botón deliberadamente enorme: lo va a tocar gente mayor, de pie y de afán */
.btn-firmar {
margin-top: 2vh;
background: var(--brand); color: #fff; border: 0;
border-radius: 18px; cursor: pointer;
font-family: inherit; font-weight: 800;
font-size: clamp(1.5rem, 4.5vw, 2.6rem);
padding: clamp(1rem, 3.5vh, 2.2rem) clamp(2.5rem, 12vw, 6rem);
box-shadow: 0 10px 30px rgba(0,0,0,.18);
transition: transform .12s ease, filter .12s ease;
}
.btn-firmar:active { transform: scale(.96); filter: brightness(.92); }
.ok-icono { font-size: clamp(3.5rem, 12vw, 7rem); color: #16a34a; line-height: 1; }
.ok-txt { font-size: clamp(1.4rem, 4vw, 2.4rem); font-weight: 700; color: #166534; }
.aviso { font-size: clamp(1rem, 2.4vw, 1.4rem); color: #94a3b8; max-width: 34ch; line-height: 1.5; }
/* El formulario ocupa toda la pantalla al abrirse */
#marco { position: fixed; inset: 0; z-index: 50; background: #fff; display: none; }
#marco.abierto { display: block; }
#marco iframe { width: 100%; height: 100%; border: 0; }
</style>
</head>
<body>
<!-- En reposo: nadie a quien pedirle firma -->
<div class="pantalla" id="p-reposo">
<?php if ($_fLogo): ?><img class="logo" src="<?= htmlspecialchars($_fLogo) ?>" alt=""><?php endif; ?>
<div class="marca"><?= $_fNombre ?></div>
<div class="saludo">Bienvenido</div>
</div>
<!-- Hay alguien y le falta firmar -->
<div class="pantalla oculta" id="p-firmar">
<div class="etiqueta">Turno <span id="f-codigo"></span></div>
<div class="paciente" id="f-paciente"></div>
<div class="aviso">Por favor lea y firme el consentimiento para continuar con su atención.</div>
<button class="btn-firmar" id="btn-firmar">Firmar</button>
</div>
<!-- Ya firmó -->
<div class="pantalla oculta" id="p-gracias">
<div class="ok-icono">✓</div>
<div class="ok-txt">¡Gracias!</div>
<div class="aviso">Su consentimiento quedó registrado. Puede continuar en el mostrador.</div>
</div>
<!-- La tablet no está registrada -->
<div class="pantalla oculta" id="p-sin-registro">
<div class="marca"><?= $_fNombre ?></div>
<div class="aviso">Esta tablet todavía no está asignada a un puesto.<br>
Regístrela desde Configuración del turnero.</div>
</div>
<div id="marco"><iframe id="marco-iframe" src="about:blank"></iframe></div>
<script>
const API = '<?= defined('BASE_URL') ? BASE_URL : '/' ?>modules/turnero/api/';
let turnoEnPantalla = null; // turno que se está mostrando
let firmando = false; // con el formulario abierto no se cambia de pantalla
function mostrar(id) {
['p-reposo','p-firmar','p-gracias','p-sin-registro']
.forEach(p => document.getElementById(p).classList.toggle('oculta', p !== id));
}
function abrirFormulario(url) {
firmando = true;
document.getElementById('marco-iframe').src = url;
document.getElementById('marco').classList.add('abierto');
}
function cerrarFormulario() {
firmando = false;
document.getElementById('marco').classList.remove('abierto');
document.getElementById('marco-iframe').src = 'about:blank';
}
document.getElementById('btn-firmar').addEventListener('click', () => {
const url = document.getElementById('btn-firmar').dataset.url;
if (url) abrirFormulario(url);
});
async function revisar() {
// Con el formulario abierto no se toca la pantalla: el paciente está firmando
if (firmando) return;
let d;
try {
const r = await fetch(API + 'get_firma_pendiente.php', { cache: 'no-store' });
d = await r.json();
} catch (_) {
return; // Sin red se deja lo que haya puesto; ya volverá
}
if (!d.ok) {
if (d.motivo === 'sin_dispositivo') mostrar('p-sin-registro');
return;
}
if (d.estado === 'por_firmar') {
document.getElementById('f-codigo').textContent = d.codigo || '';
document.getElementById('f-paciente').textContent = d.paciente || '';
document.getElementById('btn-firmar').dataset.url = d.url || '';
turnoEnPantalla = d.turno_id;
mostrar('p-firmar');
return;
}
if (d.estado === 'firmado') {
// El "gracias" solo tiene sentido para quien acaba de firmar aquí.
// Si la tablet se abre con un turno ya firmado de antes, va a reposo.
mostrar(turnoEnPantalla === d.turno_id ? 'p-gracias' : 'p-reposo');
return;
}
// reposo: se olvida el turno anterior para no dejar datos de un paciente
// en pantalla mientras llega el siguiente
turnoEnPantalla = null;
mostrar('p-reposo');
}
// ver_formulario_enviado.php avisa al terminar; si no llega el aviso, el sondeo
// se encarga igual cuando el consentimiento aparezca como firmado.
window.addEventListener('message', (e) => {
const t = e.data && e.data.type;
if (t === 'turneroFirmado') {
cerrarFormulario();
mostrar('p-gracias');
setTimeout(revisar, 2500);
}
});
revisar();
setInterval(revisar, 2000);
</script>
</body>
</html>