Files
whatsapp/modules/turnero/views/verificar_paciente.php
T
Lizandro GuarnizoandClaude Sonnet 4.6 cb77425ca2 feat(turnero): vista verificar_paciente dentro del ERP con Layout
- Crea modules/turnero/views/verificar_paciente.php con Layout::open/close
- Flujo completo: cédula → OTP → éxito, mismo JS que verificar.php público
- Link del módulo ahora apunta a erp.php?m=turnero&v=verificar_paciente

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-03 00:35:00 -05:00

290 lines
14 KiB
PHP

<?php
/**
* modules/turnero/views/verificar_paciente.php
* Verificación de identidad del paciente por OTP — dentro del ERP.
*/
require_once APP_ROOT . '/config/config.php';
if (!isUserLoggedIn()) { header('Location: ' . BASE_URL . 'login.php'); exit; }
Layout::open('Verificar Paciente', 'fas fa-id-card');
?>
<style>
.vp-wrap {
min-height: calc(100vh - 120px);
display: flex; align-items: center; justify-content: center;
padding: 24px 16px;
background: #f1f5f9;
}
.vp-card {
background: #fff;
border-radius: 20px;
box-shadow: 0 8px 32px rgba(0,0,0,.10);
width: 100%; max-width: 420px;
overflow: hidden;
}
.vp-header {
background: linear-gradient(135deg, var(--brand-dark, #0d47a1), var(--brand, #1565c0));
padding: 24px 28px 20px;
text-align: center;
position: relative;
}
.vp-header::after {
content: ''; position: absolute; bottom: -1px; left: 0; right: 0;
height: 20px; background: #fff; border-radius: 20px 20px 0 0;
}
.vp-header h2 { color: #fff; font-size: 1.1rem; font-weight: 700; position: relative; z-index: 1; }
.vp-header p { color: rgba(255,255,255,.75); font-size: .8rem; margin-top: 4px; position: relative; z-index: 1; }
.vp-body { padding: 24px 28px 28px; }
.step { display: none; }
.step.active { display: block; animation: fadeUp .25s ease; }
@keyframes fadeUp { from { opacity:0; transform:translateY(10px); } to { opacity:1; transform:translateY(0); } }
.step-icon { width:56px; height:56px; border-radius:50%; background:color-mix(in srgb,var(--brand,#1565c0) 12%,#fff); display:flex; align-items:center; justify-content:center; font-size:1.4rem; color:var(--brand,#1565c0); margin:0 auto 14px; }
.step-title { font-size:1rem; font-weight:700; color:#1e293b; text-align:center; margin-bottom:6px; }
.step-desc { font-size:.82rem; color:#64748b; text-align:center; line-height:1.55; margin-bottom:20px; }
.highlight { color:var(--brand,#1565c0); font-weight:600; }
.steps-indicator { display:flex; align-items:center; justify-content:center; gap:8px; margin-bottom:20px; }
.step-dot { width:8px; height:8px; border-radius:50%; background:#e2e8f0; transition:all .3s; }
.step-dot.done,.step-dot.active { background:var(--brand,#1565c0); }
.step-dot.active { transform:scale(1.4); }
.step-line { flex:1; max-width:40px; height:2px; background:#e2e8f0; border-radius:1px; transition:background .3s; }
.step-line.done { background:var(--brand,#1565c0); }
.input-wrap { position:relative; margin-bottom:14px; }
.input-wrap i { position:absolute; left:13px; top:50%; transform:translateY(-50%); color:#94a3b8; font-size:.9rem; pointer-events:none; }
.input-field { width:100%; padding:12px 12px 12px 38px; border:2px solid #e2e8f0; border-radius:10px; font-size:.93rem; color:#1e293b; background:#f8faff; outline:none; transition:border-color .2s, box-shadow .2s; }
.input-field:focus { border-color:var(--brand,#1565c0); box-shadow:0 0 0 3px color-mix(in srgb,var(--brand,#1565c0) 15%,transparent); background:#fff; }
.otp-wrap { display:flex; gap:8px; justify-content:center; margin-bottom:14px; }
.otp-digit { width:46px; height:54px; border:2px solid #e2e8f0; border-radius:10px; font-size:1.4rem; font-weight:700; text-align:center; color:var(--brand,#1565c0); background:#f8faff; outline:none; transition:border-color .2s, box-shadow .2s; }
.otp-digit:focus { border-color:var(--brand,#1565c0); box-shadow:0 0 0 3px color-mix(in srgb,var(--brand,#1565c0) 15%,transparent); background:#fff; }
.btn-primary { width:100%; padding:12px; background:linear-gradient(135deg,var(--brand-dark,#0d47a1),var(--brand,#1565c0)); color:#fff; border:none; border-radius:10px; font-size:.93rem; font-weight:700; cursor:pointer; display:flex; align-items:center; justify-content:center; gap:8px; transition:opacity .2s, transform .15s; margin-top:4px; }
.btn-primary:hover:not(:disabled) { opacity:.9; transform:translateY(-1px); }
.btn-primary:disabled { opacity:.6; cursor:not-allowed; }
.btn-link { background:none; border:none; color:var(--brand,#1565c0); font-size:.82rem; font-weight:600; cursor:pointer; text-decoration:underline; margin-top:10px; display:block; text-align:center; }
.vp-error { background:#fef2f2; border:1px solid #fca5a5; border-radius:8px; padding:10px 14px; font-size:.83rem; color:#dc2626; margin-bottom:12px; display:none; }
.timer { font-size:.78rem; color:#94a3b8; text-align:center; margin-top:8px; }
.success-icon { width:64px; height:64px; border-radius:50%; background:#dcfce7; display:flex; align-items:center; justify-content:center; font-size:1.8rem; color:#16a34a; margin:0 auto 14px; }
</style>
<div class="vp-wrap">
<div class="vp-card">
<div class="vp-header">
<h2><i class="fas fa-id-card me-2"></i>Verificar Paciente</h2>
<p>Confirmación de identidad por código SMS</p>
</div>
<div class="vp-body">
<div class="steps-indicator">
<div class="step-dot active" id="dot-1"></div>
<div class="step-line" id="line-1"></div>
<div class="step-dot" id="dot-2"></div>
<div class="step-line" id="line-2"></div>
<div class="step-dot" id="dot-3"></div>
</div>
<div class="vp-error" id="vp-error"></div>
<!-- PASO 1: Cédula -->
<div class="step active" id="step-1">
<div class="step-icon"><i class="fas fa-id-card"></i></div>
<div class="step-title">Número de documento</div>
<div class="step-desc">Ingresa el documento del paciente para enviarle un código de verificación.</div>
<div class="input-wrap">
<i class="fas fa-hashtag"></i>
<input type="text" id="input-cedula" class="input-field" placeholder="Ej: 10234567"
inputmode="numeric" pattern="\d*" maxlength="12"
onkeydown="if(event.key==='Enter') enviarOtp()">
</div>
<button class="btn-primary" id="btn-enviar" onclick="enviarOtp()">
<i class="fas fa-paper-plane"></i> Enviar código
</button>
</div>
<!-- PASO 2: OTP -->
<div class="step" id="step-2">
<div class="step-icon"><i class="fas fa-mobile-alt"></i></div>
<div class="step-title">Código de verificación</div>
<div class="step-desc" id="desc-otp">
Ingresa el código de 6 dígitos enviado al número del paciente.
</div>
<div class="otp-wrap" id="otp-wrap">
<input class="otp-digit" maxlength="1" inputmode="numeric" pattern="\d">
<input class="otp-digit" maxlength="1" inputmode="numeric" pattern="\d">
<input class="otp-digit" maxlength="1" inputmode="numeric" pattern="\d">
<input class="otp-digit" maxlength="1" inputmode="numeric" pattern="\d">
<input class="otp-digit" maxlength="1" inputmode="numeric" pattern="\d">
<input class="otp-digit" maxlength="1" inputmode="numeric" pattern="\d">
</div>
<button class="btn-primary" id="btn-verificar" onclick="verificarOtp()">
<i class="fas fa-check-circle"></i> Verificar
</button>
<div class="timer">Código válido por <span id="countdown">5:00</span></div>
<button class="btn-link" id="btn-reenviar" onclick="reenviarOtp()" disabled>Reenviar código</button>
</div>
<!-- PASO 3: Éxito -->
<div class="step" id="step-3">
<div class="success-icon"><i class="fas fa-check"></i></div>
<div class="step-title">¡Verificación exitosa!</div>
<div class="step-desc">
Hola, <span class="highlight" id="nombre-paciente"></span>. Identidad verificada correctamente.
</div>
<button class="btn-primary" onclick="reiniciar()">
<i class="fas fa-rotate-left"></i> Nueva verificación
</button>
</div>
</div>
</div>
</div>
<script>
const BASE_URL = '<?= rtrim(BASE_URL, '/') ?>';
let _cedula = '', _nombre = '', _timer = null, _reenvioTimer = null;
document.querySelectorAll('.otp-digit').forEach((inp, i, arr) => {
inp.addEventListener('input', () => {
const val = inp.value.replace(/\D/g, '');
if (val.length > 1) {
const digits = val.slice(0, 6);
arr.forEach((d, j) => { d.value = digits[j] || ''; });
const last = Math.min(digits.length - 1, arr.length - 1);
arr[last].focus();
if (otpCompleto()) verificarOtp();
return;
}
inp.value = val;
if (inp.value && i < arr.length - 1) arr[i + 1].focus();
if (otpCompleto()) verificarOtp();
});
inp.addEventListener('keydown', e => {
if (e.key === 'Backspace' && !inp.value && i > 0) arr[i - 1].focus();
});
inp.addEventListener('paste', e => {
e.preventDefault();
const txt = (e.clipboardData || window.clipboardData).getData('text').replace(/\D/g,'').slice(0,6);
arr.forEach((d, j) => { d.value = txt[j] || ''; });
const last = Math.min(txt.length - 1, arr.length - 1);
if (last >= 0) arr[last].focus();
if (txt.length === 6) verificarOtp();
});
});
function otpCompleto() { return [...document.querySelectorAll('.otp-digit')].every(d => d.value); }
function getOtp() { return [...document.querySelectorAll('.otp-digit')].map(d => d.value).join(''); }
function clearOtp() { document.querySelectorAll('.otp-digit').forEach(d => d.value = ''); document.querySelectorAll('.otp-digit')[0].focus(); }
function showError(msg) { const el = document.getElementById('vp-error'); el.textContent = msg; el.style.display = 'block'; }
function hideError() { document.getElementById('vp-error').style.display = 'none'; }
function goStep(n) {
document.querySelectorAll('.step').forEach(s => s.classList.remove('active'));
document.getElementById('step-' + n).classList.add('active');
for (let i = 1; i <= 3; i++) {
document.getElementById('dot-' + i).className = 'step-dot' + (i < n ? ' done' : i === n ? ' active' : '');
}
for (let i = 1; i <= 2; i++) {
document.getElementById('line-' + i).className = 'step-line' + (i < n ? ' done' : '');
}
hideError();
}
async function enviarOtp() {
const cedula = document.getElementById('input-cedula').value.trim();
if (!cedula) { showError('Ingresa el número de documento.'); return; }
hideError();
const btn = document.getElementById('btn-enviar');
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Enviando…';
try {
const r = await fetch(BASE_URL + '/api/public/send_otp.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cedula }),
});
const d = await r.json();
if (!d.ok) { showError(d.error || 'Error al enviar el código.'); btn.disabled = false; btn.innerHTML = '<i class="fas fa-paper-plane"></i> Enviar código'; return; }
_cedula = cedula;
_nombre = d.nombre || '';
document.getElementById('desc-otp').innerHTML = `Código enviado al número <strong>${d.telefono || '***'}</strong> del paciente.`;
goStep(2);
iniciarContador(d.expira || 300);
iniciarReenvio();
setTimeout(() => document.querySelector('.otp-digit').focus(), 100);
} catch(e) { showError('Error de conexión.'); }
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-paper-plane"></i> Enviar código';
}
async function verificarOtp() {
if (!otpCompleto()) return;
hideError();
const btn = document.getElementById('btn-verificar');
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Verificando…';
try {
const r = await fetch(BASE_URL + '/api/public/verify_otp.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cedula: _cedula, codigo: getOtp() }),
});
const d = await r.json();
if (!d.ok) { showError(d.error || 'Código incorrecto.'); clearOtp(); btn.disabled = false; btn.innerHTML = '<i class="fas fa-check-circle"></i> Verificar'; return; }
clearInterval(_timer); clearTimeout(_reenvioTimer);
document.getElementById('nombre-paciente').textContent = d.nombre || _nombre;
goStep(3);
} catch(e) { showError('Error de conexión.'); }
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-check-circle"></i> Verificar';
}
async function reenviarOtp() {
hideError();
const btn = document.getElementById('btn-reenviar');
btn.disabled = true; btn.textContent = 'Enviando…';
try {
const r = await fetch(BASE_URL + '/api/public/send_otp.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cedula: _cedula }),
});
const d = await r.json();
if (!d.ok) { showError(d.error || 'Error al reenviar.'); btn.disabled = false; btn.textContent = 'Reenviar código'; return; }
clearOtp();
iniciarContador(d.expira || 300);
iniciarReenvio();
} catch(e) { showError('Error de conexión.'); }
}
function iniciarContador(segundos) {
clearInterval(_timer);
const el = document.getElementById('countdown');
let remaining = segundos;
el.textContent = fmt(remaining);
_timer = setInterval(() => {
remaining--;
el.textContent = fmt(remaining);
if (remaining <= 0) { clearInterval(_timer); el.textContent = 'Expirado'; }
}, 1000);
}
function iniciarReenvio() {
clearTimeout(_reenvioTimer);
const btn = document.getElementById('btn-reenviar');
btn.disabled = true; btn.textContent = 'Reenviar en 60s…';
_reenvioTimer = setTimeout(() => { btn.disabled = false; btn.textContent = 'Reenviar código'; }, 60000);
}
function fmt(s) { return Math.floor(s/60) + ':' + String(s%60).padStart(2,'0'); }
function reiniciar() {
_cedula = ''; _nombre = '';
clearInterval(_timer); clearTimeout(_reenvioTimer);
document.getElementById('input-cedula').value = '';
clearOtp();
goStep(1);
}
</script>
<?php Layout::close(); ?>