- 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>
290 lines
14 KiB
PHP
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(); ?>
|