- Kiosko: detector scanner (gap < 80ms), iconos 90px, fa-person-pregnant, ticket Arial - Recepción: vincular_paciente.php para persistir paciente_id antes de firmar consentimiento - Historial y dashboard: recepción desk, empleado recepción/muestras, exámenes y comentarios - display_global: turno más pequeño, lugar-nombre más grande - lab_pacientes: modal compacta con form-sm - Configuración: icono embarazada fa-person-pregnant en prioridades - ver_formulario_enviado: firma por campo (fix doble firma) - docker-compose.local.yml: override BD producción para dev local Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
614 lines
26 KiB
PHP
614 lines
26 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;
|
||
}
|
||
|
||
/* ── 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;
|
||
}
|
||
.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; }
|
||
.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;
|
||
}
|
||
.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;
|
||
}
|
||
.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;
|
||
}
|
||
.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;
|
||
}
|
||
.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;
|
||
}
|
||
.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;
|
||
}
|
||
|
||
/* ── 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">
|
||
<?php else: ?>
|
||
<div class="logo-fallback"><i class="fas fa-flask"></i></div>
|
||
<?php endif; ?>
|
||
<div class="lab-nombre"><?= $_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>
|
||
|
||
|
||
<!-- ══ 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="numeric">
|
||
</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="numeric">
|
||
<div class="hint"><i class="fas fa-bell me-1"></i>Para recibir notificaciones de su turno</div>
|
||
</div>
|
||
<button class="btn-kiosko-main" onclick="confirmarTurno()">
|
||
<i class="fas fa-ticket-alt me-2"></i>Obtener mi turno
|
||
</button>
|
||
<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 class="ticket-pos">
|
||
<div class="lbl">Posición en cola</div>
|
||
<div class="num" id="tick-posicion">—</div>
|
||
</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);
|
||
});
|
||
}
|
||
|
||
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 = '';
|
||
|
||
// Detector de scanner: auto-submit solo si los dígitos llegan rápido (< 80ms entre cada uno)
|
||
let _timerAutoSubmit = null;
|
||
let _lastKeyAt = 0;
|
||
inpCedula.addEventListener('input', () => {
|
||
clearTimeout(_timerAutoSubmit);
|
||
const now = Date.now();
|
||
const gap = now - _lastKeyAt;
|
||
_lastKeyAt = now;
|
||
const val = inpCedula.value.trim();
|
||
// Scanner envía dígitos en ráfaga; teclado humano siempre > 80ms entre teclas
|
||
if (/^\d{5,15}$/.test(val) && gap < 80) {
|
||
_timerAutoSubmit = setTimeout(() => confirmarTurno(), 300);
|
||
}
|
||
});
|
||
}
|
||
|
||
function volverPrioridades() { mostrar('screen-prio'); }
|
||
|
||
async function confirmarTurno() {
|
||
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; }
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
function mostrarTicket(t) {
|
||
setSpinner(false);
|
||
document.getElementById('tick-codigo').textContent = t.codigo;
|
||
document.getElementById('tick-codigo').style.color = prioColor;
|
||
document.getElementById('tick-posicion').textContent = '#' + t.posicion_cola;
|
||
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);
|
||
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); }
|
||
|
||
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>
|
||
|