verificar.php: - Pública, sin autenticación ni menú lateral - Diseño moderno: card glassmorphism sobre gradiente brand - Flujo en 3 pasos con indicador animado - Paso 1: ingresa cédula → busca paciente - Paso 2: inputs OTP de 6 dígitos con navegación automática y pegado desde portapapeles; cuenta regresiva + reenvío a los 60s - Paso 3: pantalla de éxito con animación api/public/send_otp.php: - Busca paciente por cédula, genera OTP aleatorio de 6 dígitos - Guarda en lab_otp_tokens con expiración 5 min - Envía SMS via WEBSms usando config de lab_config - Retorna teléfono enmascarado (3**1234**67) api/public/verify_otp.php: - Valida cedula+codigo contra lab_otp_tokens activos - Marca OTP como usado y paciente como verificado (telefono_verificado=1, verificado_at=NOW()) DB: - lab_pacientes: +telefono_verificado, +verificado_at - lab_otp_tokens: tabla nueva para almacenar OTPs temporales Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
607 lines
20 KiB
PHP
607 lines
20 KiB
PHP
<?php
|
|
/**
|
|
* /verificar.php — Validación de paciente por OTP. Página pública, sin autenticación.
|
|
*/
|
|
require_once __DIR__ . '/config/config.php';
|
|
|
|
// Leer brand desde DB
|
|
$brandColor = '#1565c0';
|
|
$brandDark = '#0d47a1';
|
|
$logoBase64 = '';
|
|
$labNombre = 'Laboratorio';
|
|
try {
|
|
$cfgs = Database::getInstance()->fetchAll(
|
|
"SELECT clave, valor FROM lab_config WHERE clave IN ('doc_color','doc_logo_base64','empresa_nombre')"
|
|
);
|
|
foreach ($cfgs as $r) {
|
|
if ($r['clave'] === 'doc_color' && preg_match('/^#[0-9a-fA-F]{3,8}$/', $r['valor'])) {
|
|
$brandColor = $r['valor'];
|
|
} elseif ($r['clave'] === 'doc_logo_base64') {
|
|
$logoBase64 = $r['valor'];
|
|
} elseif ($r['clave'] === 'empresa_nombre') {
|
|
$labNombre = $r['valor'];
|
|
}
|
|
}
|
|
$hex = ltrim($brandColor, '#');
|
|
$brandDark = sprintf('#%02x%02x%02x',
|
|
max(0, hexdec(substr($hex,0,2)) - 40),
|
|
max(0, hexdec(substr($hex,2,2)) - 40),
|
|
max(0, hexdec(substr($hex,4,2)) - 40)
|
|
);
|
|
} catch (\Throwable $_) {}
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Verificación de Paciente — <?= htmlspecialchars($labNombre) ?></title>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
:root {
|
|
--brand: <?= $brandColor ?>;
|
|
--brand-dark: <?= $brandDark ?>;
|
|
}
|
|
|
|
body {
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: linear-gradient(135deg, var(--brand-dark) 0%, var(--brand) 50%, color-mix(in srgb, var(--brand) 70%, #7c3aed) 100%);
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
padding: 20px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Decoración de fondo */
|
|
body::before, body::after {
|
|
content: '';
|
|
position: fixed;
|
|
border-radius: 50%;
|
|
background: rgba(255,255,255,.06);
|
|
pointer-events: none;
|
|
}
|
|
body::before { width: 500px; height: 500px; top: -120px; right: -120px; }
|
|
body::after { width: 400px; height: 400px; bottom: -100px; left: -100px; }
|
|
|
|
/* Card principal */
|
|
.card {
|
|
background: rgba(255,255,255,.97);
|
|
backdrop-filter: blur(20px);
|
|
border-radius: 24px;
|
|
box-shadow: 0 32px 80px rgba(0,0,0,.25), 0 0 0 1px rgba(255,255,255,.2);
|
|
width: 100%;
|
|
max-width: 420px;
|
|
overflow: hidden;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* Header de la card */
|
|
.card-header {
|
|
background: linear-gradient(135deg, var(--brand-dark), var(--brand));
|
|
padding: 32px 28px 24px;
|
|
text-align: center;
|
|
position: relative;
|
|
}
|
|
.card-header::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: -1px; left: 0; right: 0;
|
|
height: 24px;
|
|
background: rgba(255,255,255,.97);
|
|
border-radius: 24px 24px 0 0;
|
|
}
|
|
.logo-wrap {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(255,255,255,.92);
|
|
border-radius: 16px;
|
|
padding: 10px 16px;
|
|
margin-bottom: 14px;
|
|
}
|
|
.logo-wrap img { max-height: 52px; max-width: 160px; object-fit: contain; }
|
|
.logo-wrap .logo-fallback { font-size: 2rem; color: var(--brand); }
|
|
.card-header h1 {
|
|
color: #fff;
|
|
font-size: 1.1rem;
|
|
font-weight: 700;
|
|
letter-spacing: -.01em;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
.card-header p {
|
|
color: rgba(255,255,255,.75);
|
|
font-size: .8rem;
|
|
margin-top: 4px;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* Cuerpo */
|
|
.card-body { padding: 28px 28px 32px; }
|
|
|
|
/* Steps */
|
|
.step { display: none; }
|
|
.step.active { display: block; animation: fadeUp .3s ease; }
|
|
@keyframes fadeUp {
|
|
from { opacity: 0; transform: translateY(12px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
/* Step indicator */
|
|
.steps-indicator {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
margin-bottom: 24px;
|
|
}
|
|
.step-dot {
|
|
width: 8px; height: 8px;
|
|
border-radius: 50%;
|
|
background: #e2e8f0;
|
|
transition: all .3s ease;
|
|
}
|
|
.step-dot.done { background: var(--brand); transform: scale(1); }
|
|
.step-dot.active { background: var(--brand); 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); }
|
|
|
|
/* Iconos de paso */
|
|
.step-icon {
|
|
width: 64px; height: 64px;
|
|
border-radius: 50%;
|
|
background: color-mix(in srgb, var(--brand) 12%, #fff);
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 1.6rem;
|
|
color: var(--brand);
|
|
margin: 0 auto 16px;
|
|
}
|
|
.step-title {
|
|
font-size: 1.05rem;
|
|
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: 22px;
|
|
}
|
|
.highlight { color: var(--brand); font-weight: 600; }
|
|
|
|
/* Inputs */
|
|
.input-wrap { position: relative; margin-bottom: 14px; }
|
|
.input-wrap i {
|
|
position: absolute;
|
|
left: 14px; top: 50%; transform: translateY(-50%);
|
|
color: #94a3b8; font-size: .9rem;
|
|
pointer-events: none;
|
|
}
|
|
.input-field {
|
|
width: 100%;
|
|
padding: 14px 14px 14px 40px;
|
|
border: 2px solid #e2e8f0;
|
|
border-radius: 12px;
|
|
font-size: .95rem;
|
|
color: #1e293b;
|
|
background: #f8faff;
|
|
outline: none;
|
|
transition: border-color .2s, box-shadow .2s;
|
|
}
|
|
.input-field:focus {
|
|
border-color: var(--brand);
|
|
box-shadow: 0 0 0 4px color-mix(in srgb, var(--brand) 15%, transparent);
|
|
background: #fff;
|
|
}
|
|
|
|
/* OTP input especial */
|
|
.otp-wrap {
|
|
display: flex;
|
|
gap: 8px;
|
|
justify-content: center;
|
|
margin-bottom: 14px;
|
|
}
|
|
.otp-digit {
|
|
width: 48px; height: 58px;
|
|
border: 2px solid #e2e8f0;
|
|
border-radius: 12px;
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
text-align: center;
|
|
color: var(--brand);
|
|
background: #f8faff;
|
|
outline: none;
|
|
transition: border-color .2s, box-shadow .2s, transform .1s;
|
|
}
|
|
.otp-digit:focus {
|
|
border-color: var(--brand);
|
|
box-shadow: 0 0 0 4px color-mix(in srgb, var(--brand) 15%, transparent);
|
|
background: #fff;
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
/* Botón principal */
|
|
.btn-primary {
|
|
width: 100%;
|
|
padding: 14px;
|
|
background: linear-gradient(135deg, var(--brand-dark), var(--brand));
|
|
color: #fff;
|
|
border: none;
|
|
border-radius: 12px;
|
|
font-size: .95rem;
|
|
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:active:not(:disabled) { transform: translateY(0); }
|
|
.btn-primary:disabled { opacity: .6; cursor: not-allowed; }
|
|
|
|
/* Botón secundario */
|
|
.btn-link {
|
|
background: none; border: none;
|
|
color: var(--brand); font-size: .82rem;
|
|
font-weight: 600; cursor: pointer;
|
|
text-decoration: underline;
|
|
margin-top: 12px;
|
|
display: block;
|
|
text-align: center;
|
|
}
|
|
.btn-link:disabled { color: #94a3b8; text-decoration: none; cursor: default; }
|
|
|
|
/* Timer */
|
|
.timer {
|
|
text-align: center;
|
|
font-size: .8rem;
|
|
color: #94a3b8;
|
|
margin-top: 8px;
|
|
}
|
|
.timer span { color: var(--brand); font-weight: 700; }
|
|
|
|
/* Error */
|
|
.error-msg {
|
|
background: #fef2f2;
|
|
border: 1px solid #fecaca;
|
|
border-radius: 10px;
|
|
padding: 10px 14px;
|
|
font-size: .82rem;
|
|
color: #dc2626;
|
|
margin-bottom: 12px;
|
|
display: none;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.error-msg.show { display: flex; }
|
|
|
|
/* Éxito */
|
|
.success-icon {
|
|
width: 80px; height: 80px;
|
|
border-radius: 50%;
|
|
background: #dcfce7;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 2.2rem;
|
|
color: #16a34a;
|
|
margin: 0 auto 20px;
|
|
animation: pop .4s cubic-bezier(.175,.885,.32,1.275);
|
|
}
|
|
@keyframes pop {
|
|
from { transform: scale(0); opacity: 0; }
|
|
to { transform: scale(1); opacity: 1; }
|
|
}
|
|
|
|
/* Spinner */
|
|
.spinner { animation: spin .7s linear infinite; display: inline-block; }
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
|
|
/* Footer */
|
|
.card-footer {
|
|
text-align: center;
|
|
padding: 0 28px 20px;
|
|
font-size: .75rem;
|
|
color: #94a3b8;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="card">
|
|
<!-- Header -->
|
|
<div class="card-header">
|
|
<div class="logo-wrap">
|
|
<?php if ($logoBase64): ?>
|
|
<img src="<?= htmlspecialchars($logoBase64) ?>" alt="Logo">
|
|
<?php else: ?>
|
|
<span class="logo-fallback"><i class="fas fa-flask"></i></span>
|
|
<?php endif; ?>
|
|
</div>
|
|
<h1><?= htmlspecialchars($labNombre) ?></h1>
|
|
<p>Portal de verificación de pacientes</p>
|
|
</div>
|
|
|
|
<!-- Body -->
|
|
<div class="card-body">
|
|
|
|
<!-- Indicador de pasos -->
|
|
<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>
|
|
|
|
<!-- Error global -->
|
|
<div class="error-msg" id="error-msg">
|
|
<i class="fas fa-exclamation-circle"></i>
|
|
<span id="error-text"></span>
|
|
</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">Ingresa tu documento</div>
|
|
<div class="step-desc">Escribe tu número de cédula para buscar tu registro y enviarte un código de verificación.</div>
|
|
<div class="input-wrap">
|
|
<i class="fas fa-id-badge"></i>
|
|
<input type="text" class="input-field" id="inp-cedula"
|
|
placeholder="Ej: 12345678" maxlength="12"
|
|
inputmode="numeric" pattern="\d+"
|
|
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">
|
|
Enviamos un código de 6 dígitos al número <span class="highlight" id="tel-masked">***</span> registrado en tu ficha.
|
|
</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>. Tu identidad ha sido verificada correctamente.
|
|
Puedes proceder con tu atención.
|
|
</div>
|
|
<button class="btn-primary" onclick="reiniciar()" style="margin-top:8px">
|
|
<i class="fas fa-rotate-left"></i> Nueva verificación
|
|
</button>
|
|
</div>
|
|
|
|
</div><!-- /card-body -->
|
|
|
|
<div class="card-footer">
|
|
Laboratorio Ximena Caicedo © <?= date('Y') ?>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let _cedula = '';
|
|
let _nombre = '';
|
|
let _timer = null;
|
|
let _reenvioTimer = null;
|
|
|
|
// ── OTP input navegación automática ──────────────────────────────────
|
|
document.querySelectorAll('.otp-digit').forEach((inp, i, arr) => {
|
|
inp.addEventListener('input', () => {
|
|
inp.value = inp.value.replace(/\D/g, '').slice(-1);
|
|
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] || ''; });
|
|
if (txt.length === 6) { arr[5].focus(); 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();
|
|
}
|
|
|
|
// ── Mostrar paso ──────────────────────────────────────────────────────
|
|
function irAPaso(n) {
|
|
[1,2,3].forEach(i => document.getElementById('step-'+i).classList.toggle('active', i===n));
|
|
[1,2,3].forEach(i => {
|
|
document.getElementById('dot-'+i).classList.toggle('active', i===n);
|
|
document.getElementById('dot-'+i).classList.toggle('done', i<n);
|
|
});
|
|
[1,2].forEach(i => document.getElementById('line-'+i).classList.toggle('done', i<n));
|
|
hideError();
|
|
}
|
|
|
|
// ── Error ─────────────────────────────────────────────────────────────
|
|
function showError(msg) {
|
|
const el = document.getElementById('error-msg');
|
|
document.getElementById('error-text').textContent = msg;
|
|
el.classList.add('show');
|
|
}
|
|
function hideError() { document.getElementById('error-msg').classList.remove('show'); }
|
|
|
|
// ── Enviar OTP ────────────────────────────────────────────────────────
|
|
async function enviarOtp() {
|
|
const cedula = document.getElementById('inp-cedula').value.trim();
|
|
if (!cedula || !/^\d{4,12}$/.test(cedula)) { showError('Ingresa un número de documento válido.'); return; }
|
|
|
|
const btn = document.getElementById('btn-enviar');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fas fa-circle-notch spinner"></i> Enviando…';
|
|
hideError();
|
|
|
|
try {
|
|
const r = await fetch('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.'); return; }
|
|
|
|
_cedula = cedula;
|
|
_nombre = d.nombre || '';
|
|
document.getElementById('tel-masked').textContent = d.telefono || '***';
|
|
irAPaso(2);
|
|
iniciarTimer(d.expira || 300);
|
|
clearOtp();
|
|
iniciarReenvioTimer();
|
|
} catch(_) {
|
|
showError('No se pudo conectar. Intenta de nuevo.');
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="fas fa-paper-plane"></i> Enviar código';
|
|
}
|
|
}
|
|
|
|
// ── Verificar OTP ─────────────────────────────────────────────────────
|
|
async function verificarOtp() {
|
|
if (!otpCompleto()) return;
|
|
|
|
const btn = document.getElementById('btn-verificar');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fas fa-circle-notch spinner"></i> Verificando…';
|
|
hideError();
|
|
|
|
try {
|
|
const r = await fetch('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();
|
|
return;
|
|
}
|
|
|
|
clearInterval(_timer);
|
|
clearTimeout(_reenvioTimer);
|
|
document.getElementById('nombre-paciente').textContent = d.nombre || _nombre;
|
|
irAPaso(3);
|
|
} catch(_) {
|
|
showError('No se pudo conectar. Intenta de nuevo.');
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="fas fa-check-circle"></i> Verificar';
|
|
}
|
|
}
|
|
|
|
// ── Reenviar ──────────────────────────────────────────────────────────
|
|
async function reenviarOtp() {
|
|
document.getElementById('btn-reenviar').disabled = true;
|
|
hideError();
|
|
clearOtp();
|
|
|
|
try {
|
|
const r = await fetch('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.'); return; }
|
|
iniciarTimer(d.expira || 300);
|
|
iniciarReenvioTimer();
|
|
} catch(_) {
|
|
showError('No se pudo reenviar. Intenta de nuevo.');
|
|
}
|
|
}
|
|
|
|
// ── Timer cuenta regresiva ────────────────────────────────────────────
|
|
function iniciarTimer(segundos) {
|
|
clearInterval(_timer);
|
|
let s = segundos;
|
|
const el = document.getElementById('countdown');
|
|
function tick() {
|
|
const m = Math.floor(s / 60);
|
|
const ss = s % 60;
|
|
el.textContent = m + ':' + String(ss).padStart(2,'0');
|
|
if (s <= 0) { clearInterval(_timer); el.textContent = 'expirado'; }
|
|
s--;
|
|
}
|
|
tick();
|
|
_timer = setInterval(tick, 1000);
|
|
}
|
|
|
|
function iniciarReenvioTimer() {
|
|
const btn = document.getElementById('btn-reenviar');
|
|
btn.disabled = true;
|
|
let s = 60;
|
|
clearTimeout(_reenvioTimer);
|
|
function tick() {
|
|
btn.textContent = `Reenviar código (${s}s)`;
|
|
if (s <= 0) { btn.textContent = 'Reenviar código'; btn.disabled = false; return; }
|
|
s--;
|
|
_reenvioTimer = setTimeout(tick, 1000);
|
|
}
|
|
tick();
|
|
}
|
|
|
|
// ── Reiniciar ─────────────────────────────────────────────────────────
|
|
function reiniciar() {
|
|
_cedula = '';
|
|
_nombre = '';
|
|
clearInterval(_timer);
|
|
clearTimeout(_reenvioTimer);
|
|
document.getElementById('inp-cedula').value = '';
|
|
irAPaso(1);
|
|
}
|
|
|
|
// Focus inicial
|
|
document.getElementById('inp-cedula').focus();
|
|
</script>
|
|
</body>
|
|
</html>
|