Files
Lizandro GuarnizoandClaude Sonnet 4.6 b1d1c0896f turnero: kiosko — niños placeholder+edad, muestras desc, numpad +5%
- Prioridad A (Niños): placeholder cedula → 'Tarjeta de Identidad del menor (si tiene)'
- Prioridad A: age check via check_paciente.php → toast aviso si paciente ≥ 10 años
- Prioridad F: descripción → 'Muestras de días anteriores' (BD + fallback PHP)
- Numpad: padding .6rem → .63rem .6rem (+5% alto)
- Nuevo: check_paciente.php (endpoint público, solo devuelve fecha_nacimiento)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-27 22:18:38 -05:00

865 lines
38 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Kiosko de turnos — pantalla táctil pública
* Ruta: /modules/turnero/views/kiosko.php
* Sin login requerido. NUNCA expone datos de otros pacientes.
*/
require_once __DIR__ . '/../../../config/config.php';
// ── Configuración visual del laboratorio ──────────────────────
$_kioskoCfg = [];
try {
$_kpdo = Database::getInstance()->getConnection();
$_krows = $_kpdo->query(
"SELECT clave, valor FROM lab_config
WHERE clave IN ('empresa_nombre','doc_logo_base64','doc_color')"
)->fetchAll(PDO::FETCH_KEY_PAIR);
$_kioskoCfg = $_krows ?: [];
} catch (\Throwable $_) {}
$_kNombre = htmlspecialchars($_kioskoCfg['empresa_nombre'] ?? 'Laboratorio Clínico');
$_kLogo = $_kioskoCfg['doc_logo_base64'] ?? '';
$_kColor = preg_match('/^#[0-9a-fA-F]{3,8}$/', $_kioskoCfg['doc_color'] ?? '')
? $_kioskoCfg['doc_color'] : '#1565c0';
// ── Cargar prioridades activas desde BD ───────────────────────
$prioridades = [];
try {
$_pdo = Database::getInstance()->getConnection();
$stmt = $_pdo->query(
"SELECT id, codigo, nombre, color, icono, descripcion, orden_peso
FROM turnero_prioridades
WHERE activo = 1
ORDER BY orden_peso ASC"
);
$prioridades = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (\Throwable $e) {
// Fallback con prioridades por defecto si la BD no está disponible
$prioridades = [
['codigo'=>'A','nombre'=>'Niños', 'color'=>'#ef4444','icono'=>'fas fa-child', 'descripcion'=>'Pacientes menores de edad'],
['codigo'=>'B','nombre'=>'Embarazadas', 'color'=>'#f97316','icono'=>'fas fa-heart', 'descripcion'=>'Mujeres en estado de embarazo'],
['codigo'=>'C','nombre'=>'Adulto mayor', 'color'=>'#eab308','icono'=>'fas fa-person-cane', 'descripcion'=>'Mayores de 60 años'],
['codigo'=>'D','nombre'=>'Discapacidad', 'color'=>'#8b5cf6','icono'=>'fas fa-wheelchair', 'descripcion'=>'Personas con discapacidad'],
['codigo'=>'E','nombre'=>'Paciente general', 'color'=>'#3b82f6','icono'=>'fas fa-user', 'descripcion'=>'Atención general'],
['codigo'=>'F','nombre'=>'Muestra pendiente','color'=>'#6b7280','icono'=>'fas fa-vial', 'descripcion'=>'Muestras de días anteriores'],
];
}
// Iconos por defecto por código (si la BD no tiene el campo icono populado)
$iconosDefecto = [
'A' => 'fas fa-child',
'B' => 'fas fa-heart',
'C' => 'fas fa-person-cane',
'D' => 'fas fa-wheelchair',
'E' => 'fas fa-user',
'F' => 'fas fa-vial',
];
foreach ($prioridades as &$p) {
if (empty($p['icono'])) {
$p['icono'] = $iconosDefecto[$p['codigo']] ?? 'fas fa-ticket-alt';
}
}
unset($p);
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Kiosko <?= $_kNombre ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
:root {
--brand: <?= $_kColor ?>;
--brand-dark: color-mix(in srgb, <?= $_kColor ?> 75%, #000);
--brand-light: color-mix(in srgb, <?= $_kColor ?> 10%, #fff);
--brand-mid: color-mix(in srgb, <?= $_kColor ?> 22%, #fff);
}
/* ── Reset fullscreen ── */
*, *::before, *::after { box-sizing: border-box; }
html, body {
margin: 0; padding: 0;
height: 100%; width: 100%;
overflow: hidden;
background: var(--brand-light);
color: #1e293b;
font-family: 'Segoe UI', system-ui, sans-serif;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
/* ── Layout general ── */
.kiosko-wrap {
display: flex;
flex-direction: column;
height: calc(100vh / 0.80); /* ponytail: compensa el zoom:0.80 → visualmente 100vh */
overflow: hidden;
position: relative;
zoom: 0.80;
}
/* ── Header con logo ── */
.kiosko-topbar {
background: var(--brand);
padding: 14px 28px;
display: flex;
align-items: center;
gap: 14px;
flex-shrink: 0;
box-shadow: 0 2px 12px rgba(0,0,0,.18);
}
.kiosko-topbar .logo-img {
height: 48px;
max-width: 140px;
object-fit: contain;
background: #fff;
border-radius: 8px;
padding: 4px 6px;
}
.kiosko-topbar .logo-fallback {
width: 46px; height: 46px;
background: rgba(255,255,255,.2);
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
color: #fff; font-size: 1.3rem;
}
.kiosko-topbar .lab-nombre {
color: #fff;
font-size: clamp(1.15rem, 2.8vw, 1.55rem);
font-weight: 700;
letter-spacing: -.3px;
flex: 1;
}
.kiosko-topbar .turno-badge {
background: rgba(255,255,255,.15);
border: 1.5px solid rgba(255,255,255,.3);
color: #fff;
border-radius: 8px;
padding: 4px 12px;
font-size: .95rem;
font-weight: 600;
}
.btn-fullscreen {
background: rgba(255,255,255,.15);
border: 1.5px solid rgba(255,255,255,.3);
color: #fff;
border-radius: 8px;
padding: 6px 10px;
font-size: 1.15rem;
cursor: pointer;
transition: background .15s;
flex-shrink: 0;
line-height: 1;
touch-action: manipulation;
-ms-touch-action: manipulation;
}
.btn-fullscreen:hover { background: rgba(255,255,255,.28); }
/* ── Pantalla / paso ── */
.screen {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem 1.5rem;
transition: opacity .3s ease, transform .3s ease;
overflow: hidden;
}
.screen.hidden { opacity: 0; pointer-events: none; transform: scale(.98); position: absolute; inset: 0; }
.screen.visible { opacity: 1; pointer-events: all; transform: scale(1); position: relative; }
/* ── Instrucción central ── */
.kiosko-titulo {
font-size: clamp(1.55rem, 3.8vw, 2.35rem);
font-weight: 700;
color: var(--brand-dark);
text-align: center;
margin-bottom: .4rem;
}
.kiosko-subtitulo {
font-size: clamp(1rem, 2.1vw, 1.2rem);
color: #64748b;
text-align: center;
margin-bottom: 2rem;
}
/* ── Grid de prioridades ── */
.prioridad-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
width: 100%;
max-width: 860px;
}
.btn-prioridad {
border: 2px solid transparent;
border-radius: 18px;
padding: 1.6rem 1.2rem;
cursor: pointer;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
gap: .5rem;
transition: transform .12s, box-shadow .12s, filter .12s;
font-weight: 600;
box-shadow: 0 2px 12px rgba(0,0,0,.10);
-webkit-user-select: none; user-select: none;
background: #fff;
touch-action: manipulation;
-ms-touch-action: manipulation;
}
.btn-prioridad:active { transform: scale(.95); filter: brightness(.93); }
.btn-prioridad:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0,0,0,.13); }
.btn-prioridad .icon-wrap {
width: 90px; height: 90px;
border-radius: 16px;
display: flex; align-items: center; justify-content: center;
font-size: 3rem;
color: #fff;
margin-bottom: .2rem;
}
.btn-prioridad .letra { font-size: clamp(1.15rem, 2.8vw, 1.55rem); font-weight: 800; color: #1e293b; }
.btn-prioridad .nombre { font-size: clamp(.97rem, 2.1vw, 1.15rem); color: #334155; text-align: center; }
.btn-prioridad .desc { font-size: clamp(.83rem, 1.6vw, .95rem); color: #94a3b8; text-align: center; }
/* ── Formulario de datos ── */
.form-kiosko { width: 100%; max-width: 480px; }
.badge-prio-mini {
display: inline-flex; align-items: center; gap: 8px;
padding: .45rem 1.2rem; border-radius: 99px;
font-size: 1.15rem; font-weight: 700; color: #fff;
margin-bottom: 1.5rem;
}
.form-kiosko label { color: #475569; font-size: 1.1rem; margin-bottom: .35rem; font-weight: 500; }
.form-kiosko .form-control {
background: #fff;
border: 1.5px solid #e2e8f0;
color: #1e293b;
border-radius: 14px;
padding: .85rem 1.1rem;
font-size: 1.2rem;
box-shadow: 0 1px 4px rgba(0,0,0,.05);
transition: border-color .2s, box-shadow .2s;
touch-action: manipulation;
-ms-touch-action: manipulation;
}
.form-kiosko .form-control:focus {
border-color: var(--brand);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 20%, transparent);
outline: none;
}
.form-kiosko .hint { color: #94a3b8; font-size: .97rem; margin-top: .35rem; }
.btn-kiosko-main {
border: none; border-radius: 14px;
padding: 1rem 2rem; font-size: 1.2rem; font-weight: 700;
cursor: pointer; color: #fff;
background: var(--brand);
box-shadow: 0 4px 16px color-mix(in srgb, var(--brand) 40%, transparent);
transition: transform .12s, box-shadow .12s, filter .12s;
width: 100%; margin-top: 1rem;
touch-action: manipulation;
-ms-touch-action: manipulation;
}
.btn-kiosko-main:active { transform: scale(.97); filter: brightness(.9); }
.btn-kiosko-back {
background: transparent;
border: 1.5px solid #e2e8f0;
color: #94a3b8; border-radius: 12px; padding: .7rem 1.5rem;
font-size: 1.07rem; cursor: pointer; margin-top: .6rem; width: 100%;
transition: background .12s, border-color .12s;
touch-action: manipulation;
-ms-touch-action: manipulation;
}
.btn-kiosko-back:hover { background: var(--brand-mid); border-color: var(--brand); color: var(--brand-dark); }
/* ── Print: ticket para impresora térmica SRP-B300 ── */
@media print {
@page { margin: 1mm 2mm; size: 80mm auto; }
body { background: #fff !important; font-family: Arial, Helvetica, sans-serif !important; -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; -webkit-font-smoothing: antialiased !important; }
.kiosko-topbar, .kiosko-footer, .spinner-overlay, .toast-error,
.btn-nuevo, #screen-prio, #screen-datos { display: none !important; }
#screen-ticket { display: block !important; opacity: 1 !important; position: static !important; }
.kiosko-wrap { height: auto !important; }
.screen { padding: 0 !important; min-height: 0 !important; }
.ticket-box {
width: 72mm !important; max-width: 72mm !important;
padding: 0 !important; margin: 0 auto !important;
box-shadow: none !important; border: none !important;
border-top: none !important; border-radius: 0 !important;
text-align: center !important;
}
.ticket-label { font-size: 10pt !important; font-weight: 600 !important; letter-spacing: 0.03em !important; margin-bottom: 0 !important; padding-top: 2pt !important; }
.ticket-codigo { font-size: 22pt !important; font-weight: 700 !important; letter-spacing: 0 !important; line-height: 1 !important; padding: 0 !important; }
.ticket-prio-badge { font-size: 10pt !important; font-weight: 600 !important; padding: 0 !important; margin: 0 !important; background: transparent !important; color: #000 !important; }
.ticket-pos { margin: 2pt 0 !important; padding: 0 !important; border-radius: 0 !important; background: transparent !important; }
.ticket-pos .lbl { font-size: 9pt !important; font-weight: 600 !important; letter-spacing: 0.03em !important; }
.ticket-pos .num { font-size: 12pt !important; font-weight: 700 !important; color: #000 !important; }
.ticket-instruc { font-size: 9pt !important; font-weight: 500 !important; margin: 2pt 0 !important; line-height: 1.3 !important; padding-bottom: 2pt !important; }
.ticket-codigo, .ticket-label, .ticket-prio-badge,
.ticket-pos, .ticket-pos .lbl, .ticket-pos .num,
.ticket-instruc { color: #000 !important; }
}
/* ── Ticket ── */
.ticket-box {
background: #fff;
border-radius: 24px;
padding: 2.5rem 3rem;
text-align: center;
box-shadow: 0 4px 30px rgba(0,0,0,.10);
max-width: 460px; width: 100%;
border-top: 6px solid var(--brand);
}
.ticket-label { color: #94a3b8; font-size: 1rem; text-transform: uppercase;
letter-spacing: .08em; margin-bottom: .3rem; }
.ticket-codigo {
font-size: clamp(4rem, 18vw, 9rem);
font-weight: 900; line-height: 1;
letter-spacing: -2px;
color: var(--brand);
}
.ticket-prio-badge {
display: inline-block; padding: .3rem 1.1rem;
border-radius: 99px; font-weight: 700; color: #fff;
font-size: 1.1rem; margin-top: .8rem;
}
.ticket-pos {
margin-top: 1.2rem;
background: var(--brand-light);
border-radius: 12px;
padding: .8rem;
}
.ticket-pos .lbl { font-size: .93rem; color: #64748b; text-transform: uppercase; letter-spacing: .06em; }
.ticket-pos .num { font-size: 1.95rem; font-weight: 800; color: var(--brand-dark); }
.ticket-instruc { margin-top: 1.4rem; color: #94a3b8; font-size: 1.03rem; line-height: 1.5; }
.btn-nuevo {
margin-top: 1.8rem; border: 2px solid var(--brand-mid);
border-radius: 12px; padding: .75rem 2rem;
font-size: 1.1rem; font-weight: 600; cursor: pointer;
color: var(--brand-dark); background: #fff;
transition: background .15s, border-color .15s;
touch-action: manipulation;
-ms-touch-action: manipulation;
}
.btn-nuevo:hover { background: var(--brand-light); border-color: var(--brand); }
/* ── Spinner ── */
.spinner-overlay {
position: fixed; inset: 0;
background: rgba(248,250,252,.8);
display: flex; align-items: center; justify-content: center;
z-index: 999; opacity: 0; pointer-events: none;
transition: opacity .2s;
}
.spinner-overlay.active { opacity: 1; pointer-events: all; }
.spinner {
width: 52px; height: 52px;
border: 4px solid var(--brand-mid);
border-top-color: var(--brand);
border-radius: 50%;
animation: spin .75s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ── Toast de error ── */
.toast-error {
position: fixed; bottom: 2rem; left: 50%; transform: translateX(-50%);
background: #ef4444; color: #fff; padding: .75rem 1.5rem;
border-radius: 12px; font-size: 1.07rem; font-weight: 600;
box-shadow: 0 4px 20px rgba(0,0,0,.2);
opacity: 0; pointer-events: none; transition: opacity .25s;
z-index: 1000; white-space: nowrap;
}
.toast-error.show { opacity: 1; }
/* ── Footer discreto ── */
.kiosko-footer {
text-align: center;
padding: 8px;
font-size: .85rem;
color: color-mix(in srgb, var(--brand) 55%, #fff);
background: var(--brand-mid);
flex-shrink: 0;
}
/* ── Numpad táctil ── */
.numpad {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: .5rem;
margin: .6rem 0 1rem;
}
.np-btn {
padding: .63rem .6rem;
font-size: 1.2rem;
font-weight: 700;
border: 1.5px solid #e2e8f0;
border-radius: 10px;
background: #fff;
color: #1e293b;
cursor: pointer;
touch-action: manipulation;
-ms-touch-action: manipulation;
transition: background .1s, transform .08s;
user-select: none;
-webkit-user-select: none;
}
.np-btn:active { background: var(--brand-light); transform: scale(.94); }
.np-clear { color: #ef4444; border-color: #fca5a5; }
.np-back { color: #64748b; }
.np-ok { background: var(--brand); color: #fff; border-color: var(--brand);
font-size: .88rem; padding: .75rem; }
/* ── Responsive ── */
@media (min-height: 800px) {
.prioridad-grid { gap: 1.3rem; }
.btn-prioridad { padding: 1.9rem 1.4rem; }
}
@media (max-width: 480px) {
.kiosko-topbar { padding: 10px 16px; }
.ticket-box { padding: 1.8rem 1.5rem; }
}
</style>
</head>
<body>
<div class="kiosko-wrap">
<!-- ══ Top bar con logo ════════════════════════════════════════ -->
<header class="kiosko-topbar">
<?php if ($_kLogo): ?>
<img src="<?= htmlspecialchars($_kLogo) ?>" alt="Logo" class="logo-img" id="kiosko-logo-tap">
<?php else: ?>
<div class="logo-fallback" id="kiosko-logo-tap"><i class="fas fa-flask"></i></div>
<?php endif; ?>
<div class="lab-nombre" id="kiosko-logo-tap2"><?= $_kNombre ?></div>
<div class="turno-badge"><i class="fas fa-ticket-alt me-1"></i>Turnero</div>
<button class="btn-fullscreen" id="btn-fs" onclick="toggleFullscreen()" title="Pantalla completa">
<i class="fas fa-expand" id="fs-icon"></i>
</button>
</header>
<!-- ══ Overlay salida admin (toque logo 7×) ═══════════════════════ -->
<div id="overlay-salir" style="display:none;position:fixed;inset:0;z-index:9999;
background:rgba(0,0,0,.7);align-items:center;justify-content:center">
<div style="background:#fff;border-radius:20px;padding:2.5rem 3rem;text-align:center;
box-shadow:0 8px 40px rgba(0,0,0,.4);min-width:280px">
<div style="font-size:1.4rem;font-weight:700;margin-bottom:.5rem">Salir del kiosko</div>
<div style="color:#64748b;font-size:.95rem;margin-bottom:1.5rem">¿Qué desea hacer?</div>
<button onclick="window.close()"
style="width:100%;padding:.9rem;background:#2563eb;color:#fff;border:none;
border-radius:12px;font-size:1.1rem;font-weight:700;cursor:pointer;margin-bottom:.7rem">
<i class="fas fa-times-circle me-2"></i>Cerrar kiosko
</button>
<button onclick="location.reload()"
style="width:100%;padding:.9rem;background:#f1f5f9;color:#334155;border:none;
border-radius:12px;font-size:1.1rem;font-weight:700;cursor:pointer;margin-bottom:.7rem">
<i class="fas fa-sync me-2"></i>Recargar kiosko
</button>
<button onclick="document.getElementById('overlay-salir').style.display='none'"
style="width:100%;padding:.7rem;background:transparent;color:#94a3b8;border:1.5px solid #e2e8f0;
border-radius:12px;font-size:1rem;cursor:pointer">
Cancelar
</button>
</div>
</div>
<!-- ══ PASO 1: Selección de prioridad ══════════════════════════ -->
<div id="screen-prio" class="screen visible">
<div class="kiosko-titulo">Seleccione su tipo de atención</div>
<div class="kiosko-subtitulo">Toque la categoría que le corresponde para obtener su turno</div>
<div class="prioridad-grid">
<?php foreach ($prioridades as $prio): ?>
<button
class="btn-prioridad"
style="border-color:<?= htmlspecialchars($prio['color']) ?>22"
data-codigo="<?= htmlspecialchars($prio['codigo']) ?>"
data-nombre="<?= htmlspecialchars($prio['nombre']) ?>"
data-color="<?= htmlspecialchars($prio['color']) ?>"
onclick="seleccionarPrioridad(this)"
>
<div class="icon-wrap" style="background:<?= htmlspecialchars($prio['color']) ?>">
<i class="<?= htmlspecialchars($prio['icono']) ?>"></i>
</div>
<span class="letra" style="color:<?= htmlspecialchars($prio['color']) ?>"><?= htmlspecialchars($prio['codigo']) ?></span>
<span class="nombre"><?= htmlspecialchars($prio['nombre']) ?></span>
<?php if (!empty($prio['descripcion'])): ?>
<span class="desc"><?= htmlspecialchars($prio['descripcion']) ?></span>
<?php endif; ?>
</button>
<?php endforeach; ?>
</div>
</div>
<!-- ══ PASO 2: Datos del paciente ═══════════════════════════════ -->
<div id="screen-datos" class="screen hidden">
<div style="display:flex;align-items:center;gap:.8rem;margin-bottom:.5rem;width:100%;max-width:480px">
<button onclick="volverPrioridades()"
style="background:none;border:1.5px solid #e2e8f0;border-radius:10px;
padding:.45rem 1rem;font-size:.95rem;font-weight:600;color:#475569;
cursor:pointer;display:flex;align-items:center;gap:.4rem;
touch-action:manipulation;white-space:nowrap">
<i class="fas fa-arrow-left"></i> Volver
</button>
<div id="badge-prio" class="badge-prio-mini" style="margin:0"></div>
</div>
<div class="kiosko-titulo">Ingrese su número de cédula</div>
<div class="kiosko-subtitulo">Le ayudará a identificar su turno correctamente</div>
<div class="form-kiosko">
<div class="mb-3">
<label for="inp-cedula">Número de cédula <span style="color:#ef4444">*</span></label>
<input type="text" id="inp-cedula" class="form-control"
placeholder="Ej: 1234567890" maxlength="20"
autocomplete="off" inputmode="none">
</div>
<div class="mb-3">
<label for="inp-cel">WhatsApp
<span style="color:#94a3b8;font-size:.82rem;font-weight:400"> (opcional)</span>
</label>
<input type="tel" id="inp-cel" class="form-control"
placeholder="Ej: 3001234567" maxlength="20"
autocomplete="off" inputmode="none">
<div class="hint"><i class="fas fa-bell me-1"></i>Para recibir notificaciones de su turno</div>
</div>
<!-- Teclado numérico táctil -->
<div class="numpad">
<button class="np-btn" data-k="1">1</button>
<button class="np-btn" data-k="2">2</button>
<button class="np-btn" data-k="3">3</button>
<button class="np-btn" data-k="4">4</button>
<button class="np-btn" data-k="5">5</button>
<button class="np-btn" data-k="6">6</button>
<button class="np-btn" data-k="7">7</button>
<button class="np-btn" data-k="8">8</button>
<button class="np-btn" data-k="9">9</button>
<button class="np-btn np-clear" data-k="C">C</button>
<button class="np-btn" data-k="0">0</button>
<button class="np-btn np-back" data-k="⌫"><i class="fas fa-backspace"></i></button>
<button class="np-btn np-ok" data-k="OK" style="grid-column:1/-1">
<i class="fas fa-ticket-alt me-2"></i>Obtener mi turno
</button>
</div>
</div>
</div>
<!-- ══ PASO 3: Ticket asignado ══════════════════════════════════ -->
<div id="screen-ticket" class="screen hidden">
<div class="ticket-box">
<div class="ticket-label">Su número de turno es</div>
<div id="tick-codigo" class="ticket-codigo">—</div>
<div id="tick-prio-badge"></div>
<div id="tick-nombre" style="font-size:0.95rem;margin:4pt 0 2pt;font-weight:600;"></div>
<div class="ticket-instruc">
Por favor espere a ser llamado.<br>
Recuerde traer su documento de identidad y la orden médica.
</div>
</div>
<button class="btn-nuevo" onclick="seleccionarOtraPrioridad()">
<i class="fas fa-plus me-1"></i>Nuevo turno
</button>
</div>
<!-- ══ Footer ═════════════════════════════════════════════════ -->
<footer class="kiosko-footer"><?= $_kNombre ?> · Sistema de Turnos</footer>
</div><!-- /kiosko-wrap -->
<!-- ══ Spinner ════════════════════════════════════════════════ -->
<div class="spinner-overlay" id="spinner"><div class="spinner"></div></div>
<!-- ══ Toast de error ════════════════════════════════════════ -->
<div class="toast-error" id="toast-error"></div>
<script>
let prioCodigo = '';
let prioNombre = '';
let prioColor = '';
const API_URL = '<?= BASE_URL ?>modules/turnero/api/create_turno.php';
function mostrar(id) {
document.querySelectorAll('.screen').forEach(s => {
s.classList.toggle('visible', s.id === id);
s.classList.toggle('hidden', s.id !== id);
});
}
// ── Detector de escáner ──────────────────────────────────────────
// Escribir a mano NUNCA dispara el turno (la persona oprime el botón).
// Solo el lector de cédula lo dispara: mete los dígitos en ráfaga
// (<35 ms entre cada uno, imposible a mano) y/o termina con Enter.
let _keyTimes = []; // timestamps de cada pulsación
let _timerRafaga = null;
let _timerInactividad = null;
const _inpCedula = document.getElementById('inp-cedula');
// Solo es ráfaga de escáner si TODAS las pulsaciones (≥5) llegaron <35 ms
function esRafagaScanner() {
if (_keyTimes.length < 5) return false;
for (let i = 1; i < _keyTimes.length; i++) {
if (_keyTimes[i] - _keyTimes[i - 1] > 35) return false;
}
return true;
}
_inpCedula.addEventListener('input', () => {
// El escáner envía cedula + nombre + apellidos + …
// La cédula es solo dígitos: cortar en el primer carácter no numérico.
const sp = _inpCedula.value.search(/\D/);
if (sp > 0) _inpCedula.value = _inpCedula.value.slice(0, sp);
// Algunos barcodes rellenan con ceros al inicio (ej. 001092356892 → 1092356892)
_inpCedula.value = _inpCedula.value.replace(/^0+(\d)/, '$1');
resetInactividad();
_keyTimes.push(Date.now());
if (_keyTimes.length > 25) _keyTimes.shift();
// Tras una breve pausa revisamos: si fue ráfaga → escáner → disparar.
// Si fue escritura a mano (gaps grandes) esRafagaScanner() = false → no dispara.
clearTimeout(_timerRafaga);
_timerRafaga = setTimeout(() => {
const val = _inpCedula.value.trim();
if (/^\d{5,15}$/.test(val) && esRafagaScanner()) confirmarTurno();
}, 120);
});
// El lector casi siempre envía Enter al final → disparar de inmediato.
// Tab entre campos del código de barras: prevenir foco y BLOQUEAR el campo
// (el Tab indica fin de la cédula — todo lo que sigue es apellido/fecha/etc).
let _scannerLocked = false;
_inpCedula.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
e.preventDefault();
// Si ya tenemos dígitos válidos, bloquear el campo aquí mismo
if (/^\d{5,15}$/.test(_inpCedula.value.trim())) _scannerLocked = true;
return;
}
if (_scannerLocked && e.key.length === 1) {
e.preventDefault(); // ignorar todo lo que llega después del primer Tab
return;
}
if (e.key === 'Enter') {
e.preventDefault();
_scannerLocked = false;
const val = _inpCedula.value.trim();
if (/^\d{5,15}$/.test(val)) confirmarTurno();
}
});
// Si el Tab escapó y algo llegó al campo WhatsApp, limpiarlo.
document.getElementById('inp-cel').addEventListener('keydown', (e) => {
if (e.key === 'Tab') e.preventDefault();
});
document.getElementById('inp-cel').addEventListener('input', () => {
const v = document.getElementById('inp-cel').value;
// Si contiene letras → vino del escáner, limpiar y devolver foco a cédula
if (/[a-zA-Z]/.test(v)) {
document.getElementById('inp-cel').value = '';
_inpCedula.focus();
}
});
// ── Teclado táctil Windows ────────────────────────────────────────
// Si el campo ya está enfocado (por código), un toque no dispara focus
// y Windows no muestra el teclado. Solución: blur + focus en pointerup.
function _forzarTecladoTactil(inp) {
inp.addEventListener('pointerup', () => {
if (document.activeElement === inp) {
inp.blur();
setTimeout(() => inp.focus(), 30);
}
});
}
_forzarTecladoTactil(_inpCedula);
_forzarTecladoTactil(document.getElementById('inp-cel'));
function resetInactividad() {
clearTimeout(_timerInactividad);
_timerInactividad = setTimeout(() => {
seleccionarOtraPrioridad();
}, 90000); // 90 s sin actividad → vuelve al inicio
}
function seleccionarPrioridad(btn) {
prioCodigo = btn.dataset.codigo;
prioNombre = btn.dataset.nombre;
prioColor = btn.dataset.color;
const badge = document.getElementById('badge-prio');
badge.textContent = prioCodigo + ' — ' + prioNombre;
badge.style.background = prioColor;
mostrar('screen-datos');
const inpCedula = document.getElementById('inp-cedula');
inpCedula.placeholder = prioCodigo === 'A'
? 'Tarjeta de Identidad del menor (si tiene)'
: 'Ej: 1234567890';
inpCedula.focus();
inpCedula.value = '';
_scannerLocked = false;
_keyTimes = [];
clearTimeout(_timerRafaga);
resetInactividad();
}
function volverPrioridades() { clearTimeout(_timerInactividad); document.activeElement?.blur(); mostrar('screen-prio'); }
let _enviando = false;
async function confirmarTurno() {
if (_enviando) return; // evita doble disparo (ráfaga + Enter)
clearTimeout(_timerRafaga);
clearTimeout(_timerInactividad);
const cedula = document.getElementById('inp-cedula').value.trim().replace(/^0+(\d)/, '$1');
const cel = document.getElementById('inp-cel').value.trim();
if (!cedula) { mostrarError('Por favor ingrese su número de cédula.'); return; }
if (!/^\d{5,15}$/.test(cedula)) { mostrarError('La cédula debe tener solo dígitos (mínimo 5).'); return; }
if (cel && !/^\+?\d{7,15}$/.test(cel.replace(/\s/g,''))) { mostrarError('Ingrese un número de WhatsApp válido.'); return; }
_enviando = true;
setSpinner(true);
// Verificar edad solo para prioridad Niños
if (prioCodigo === 'A') {
try {
const ar = await fetch(`<?= BASE_URL ?>modules/turnero/api/check_paciente.php?doc=${encodeURIComponent(cedula)}`);
const aj = await ar.json();
if (aj.ok && aj.fecha_nacimiento) {
const dob = new Date(aj.fecha_nacimiento);
const today = new Date();
let age = today.getFullYear() - dob.getFullYear();
if (today.getMonth() < dob.getMonth() ||
(today.getMonth() === dob.getMonth() && today.getDate() < dob.getDate())) age--;
if (age >= 10) mostrarError('Este turno es para menores de 10 años, verifique la prioridad.');
}
} catch {}
}
try {
const res = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prioridad_codigo: prioCodigo, paciente_nombre: cedula, paciente_cel: cel || null }),
});
const json = await res.json();
if (!json.ok) throw new Error(json.error ?? 'Error al crear turno');
mostrarTicket(json.turno ?? json.data?.turno);
} catch (err) {
mostrarError(err.message);
setSpinner(false);
} finally {
_enviando = false;
}
}
function mostrarTicket(t) {
setSpinner(false);
document.activeElement?.blur();
document.getElementById('tick-codigo').textContent = t.codigo;
document.getElementById('tick-codigo').style.color = prioColor;
document.getElementById('tick-nombre').textContent = t.paciente_nombre || '';
document.getElementById('tick-prio-badge').innerHTML =
`<span class="ticket-prio-badge" style="background:${prioColor}">${prioCodigo} — ${prioNombre}</span>`;
mostrar('screen-ticket');
// Imprimir automáticamente
setTimeout(() => window.print(), 300);
clearTimeout(window._timer);
window._timer = setTimeout(seleccionarOtraPrioridad, 5000);
}
function seleccionarOtraPrioridad() {
clearTimeout(window._timer);
clearTimeout(_timerInactividad);
document.activeElement?.blur();
document.getElementById('inp-cedula').value = '';
document.getElementById('inp-cel').value = '';
prioCodigo = prioNombre = prioColor = '';
mostrar('screen-prio');
}
function setSpinner(on) { document.getElementById('spinner').classList.toggle('active', on); }
// ── Numpad táctil ─────────────────────────────────────────────────
const _inpCel = document.getElementById('inp-cel');
let _npTarget = _inpCedula; // campo activo para el numpad
[_inpCedula, _inpCel].forEach(inp => {
inp.addEventListener('focus', () => { _npTarget = inp; });
});
document.querySelectorAll('.np-btn').forEach(btn => {
// pointerdown con preventDefault evita que el input pierda el foco
btn.addEventListener('pointerdown', e => {
e.preventDefault();
const k = btn.dataset.k;
const inp = _npTarget;
if (k === 'OK') {
confirmarTurno();
return;
} else if (k === 'C') {
_scannerLocked = false;
inp.value = '';
} else if (k === '⌫') {
inp.value = inp.value.slice(0, -1);
} else if (inp.value.length < 20) {
inp.value += k;
}
// Notificar al detector de escáner/inactividad
inp.dispatchEvent(new Event('input', { bubbles: true }));
});
});
// ── Escape admin: tocar logo/nombre 7 veces en 4 s ───────────────
(function() {
let _taps = 0, _timer = null;
function _tap() {
_taps++;
clearTimeout(_timer);
if (_taps >= 7) {
_taps = 0;
document.getElementById('overlay-salir').style.display = 'flex';
} else {
_timer = setTimeout(() => { _taps = 0; }, 4000);
}
}
['kiosko-logo-tap','kiosko-logo-tap2'].forEach(id => {
const el = document.getElementById(id);
if (el) el.addEventListener('pointerup', _tap);
});
})();
function toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(() => {});
} else {
document.exitFullscreen().catch(() => {});
}
}
document.addEventListener('fullscreenchange', () => {
const icon = document.getElementById('fs-icon');
if (document.fullscreenElement) {
icon.classList.replace('fa-expand', 'fa-compress');
} else {
icon.classList.replace('fa-compress', 'fa-expand');
}
});
function mostrarError(msg) {
const el = document.getElementById('toast-error');
el.textContent = msg;
el.classList.add('show');
setTimeout(() => el.classList.remove('show'), 4000);
}
</script>
</body>
</html>