El campo WhatsApp generaba fricción innecesaria. Ahora el numpad tiene un botón "Obtener mi turno" de ancho completo como última fila. WhatsApp queda como hidden vacío para no romper la API. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
811 lines
35 KiB
PHP
811 lines
35 KiB
PHP
<?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'=>'Entrega de muestra tomada'],
|
||
];
|
||
}
|
||
|
||
// 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: 100vh;
|
||
overflow: hidden;
|
||
position: relative;
|
||
}
|
||
|
||
/* ── 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: 1rem;
|
||
font-size: 1.6rem;
|
||
font-weight: 700;
|
||
border: 1.5px solid #e2e8f0;
|
||
border-radius: 12px;
|
||
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: 1.15rem; padding: 1.1rem; }
|
||
|
||
/* ── 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 id="badge-prio" class="badge-prio-mini"></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>
|
||
<!-- 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>
|
||
|
||
<!-- WhatsApp oculto — se envía vacío en kiosko -->
|
||
<input type="hidden" id="inp-cel" value="">
|
||
|
||
<button class="btn-kiosko-back" onclick="volverPrioridades()">
|
||
<i class="fas fa-arrow-left me-1"></i>Cambiar tipo de atención
|
||
</button>
|
||
</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);
|
||
|
||
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 que el browser cambie el foco.
|
||
_inpCedula.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Tab') {
|
||
e.preventDefault();
|
||
return;
|
||
}
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
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.focus();
|
||
inpCedula.value = '';
|
||
_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();
|
||
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);
|
||
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 ─────────────────────────────────────────────────
|
||
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 = _inpCedula;
|
||
if (k === 'OK') {
|
||
confirmarTurno();
|
||
return;
|
||
} else if (k === 'C') {
|
||
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>
|
||
|