395 lines
17 KiB
PHP
395 lines
17 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';
|
|
|
|
// 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 de Turnos</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>
|
|
/* ── Reset fullscreen ── */
|
|
*, *::before, *::after { box-sizing: border-box; }
|
|
html, body {
|
|
margin: 0; padding: 0;
|
|
height: 100%; width: 100%;
|
|
overflow: hidden;
|
|
background: #0f172a;
|
|
color: #f8fafc;
|
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
|
touch-action: manipulation;
|
|
-webkit-tap-highlight-color: transparent;
|
|
}
|
|
|
|
/* ── Pantalla / paso ── */
|
|
.screen {
|
|
position: fixed; inset: 0;
|
|
display: flex; flex-direction: column;
|
|
align-items: center; justify-content: center;
|
|
padding: 2rem;
|
|
transition: opacity .35s ease, transform .35s ease;
|
|
}
|
|
.screen.hidden { opacity: 0; pointer-events: none; transform: scale(.97); }
|
|
.screen.visible { opacity: 1; pointer-events: all; transform: scale(1); }
|
|
|
|
/* ── Logo / cabecera ── */
|
|
.kiosko-header { text-align: center; margin-bottom: 2.5rem; }
|
|
.kiosko-header h1 { font-size: clamp(1.6rem, 4vw, 3rem); font-weight: 700; letter-spacing: -.5px; }
|
|
.kiosko-header p { font-size: clamp(.9rem, 2vw, 1.2rem); color: #94a3b8; margin: 0; }
|
|
|
|
/* ── Botones de prioridad ── */
|
|
.prioridad-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
|
|
gap: 1rem;
|
|
width: 100%;
|
|
max-width: 900px;
|
|
}
|
|
.btn-prioridad {
|
|
border: none; border-radius: 16px;
|
|
padding: 1.6rem 1.2rem;
|
|
cursor: pointer;
|
|
display: flex; flex-direction: column;
|
|
align-items: center; justify-content: center;
|
|
gap: .6rem;
|
|
transition: transform .12s, box-shadow .12s, filter .12s;
|
|
color: #fff; font-weight: 600;
|
|
box-shadow: 0 4px 20px rgba(0,0,0,.35);
|
|
-webkit-user-select: none; user-select: none;
|
|
}
|
|
.btn-prioridad:active { transform: scale(.95); filter: brightness(.88); }
|
|
.btn-prioridad .letra { font-size: clamp(2rem, 6vw, 4rem); line-height: 1; font-weight: 800; }
|
|
.btn-prioridad .nombre { font-size: clamp(.85rem, 2vw, 1.1rem); text-align: center; line-height: 1.2; }
|
|
.btn-prioridad .desc { font-size: clamp(.7rem, 1.5vw, .85rem); opacity: .8; text-align: center; }
|
|
.btn-prioridad i { font-size: clamp(1.4rem, 3.5vw, 2.2rem); }
|
|
|
|
/* ── Formulario opcional ── */
|
|
.form-kiosko { width: 100%; max-width: 520px; }
|
|
.form-kiosko label { color: #cbd5e1; font-size: 1rem; margin-bottom: .35rem; }
|
|
.form-kiosko .form-control {
|
|
background: #1e293b; border: 1.5px solid #334155;
|
|
color: #f8fafc; border-radius: 12px;
|
|
padding: .8rem 1rem; font-size: 1.1rem;
|
|
}
|
|
.form-kiosko .form-control:focus {
|
|
background: #1e293b; border-color: #60a5fa;
|
|
box-shadow: 0 0 0 3px rgba(96,165,250,.2); color: #f8fafc;
|
|
}
|
|
.form-kiosko .hint { color: #64748b; font-size: .85rem; margin-top: .4rem; }
|
|
.btn-kiosko-main {
|
|
border: none; border-radius: 14px;
|
|
padding: 1rem 2.5rem; font-size: 1.15rem; font-weight: 700;
|
|
cursor: pointer; color: #fff;
|
|
background: linear-gradient(135deg, #3b82f6, #6366f1);
|
|
box-shadow: 0 4px 16px rgba(99,102,241,.4);
|
|
transition: transform .12s, box-shadow .12s;
|
|
width: 100%; margin-top: 1rem;
|
|
}
|
|
.btn-kiosko-main:active { transform: scale(.97); }
|
|
.btn-kiosko-back {
|
|
background: transparent; border: 1.5px solid #475569;
|
|
color: #94a3b8; border-radius: 12px; padding: .7rem 1.5rem;
|
|
font-size: .95rem; cursor: pointer; margin-top: .6rem; width: 100%;
|
|
transition: background .12s;
|
|
}
|
|
.btn-kiosko-back:active { background: #1e293b; }
|
|
|
|
/* ── Pantalla de ticket ── */
|
|
.ticket-box {
|
|
background: #1e293b; border-radius: 24px;
|
|
padding: 2.5rem 3rem; text-align: center;
|
|
box-shadow: 0 8px 40px rgba(0,0,0,.5);
|
|
max-width: 480px; width: 100%;
|
|
}
|
|
.ticket-codigo {
|
|
font-size: clamp(4rem, 18vw, 9rem);
|
|
font-weight: 900; line-height: 1;
|
|
letter-spacing: -2px;
|
|
}
|
|
.ticket-subtitle { color: #94a3b8; font-size: 1rem; margin-top: .5rem; }
|
|
.ticket-prioridad { font-size: 1.05rem; margin-top: .8rem; }
|
|
.ticket-numero { font-size: 2rem; font-weight: 700; color: #e2e8f0; }
|
|
.ticket-instruc {
|
|
margin-top: 1.5rem; color: #64748b; font-size: .9rem; line-height: 1.5;
|
|
}
|
|
.btn-nuevo {
|
|
margin-top: 2rem; border: none; border-radius: 14px;
|
|
padding: .85rem 2rem; font-size: 1rem; font-weight: 600;
|
|
cursor: pointer; color: #fff;
|
|
background: #475569;
|
|
transition: background .15s;
|
|
}
|
|
.btn-nuevo:hover { background: #64748b; }
|
|
|
|
/* ── Spinner ── */
|
|
.spinner-overlay {
|
|
position: fixed; inset: 0;
|
|
background: rgba(15,23,42,.75);
|
|
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: 56px; height: 56px; border: 5px solid #334155;
|
|
border-top-color: #60a5fa; border-radius: 50%; animation: spin .8s linear infinite; }
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
|
|
/* ── Error toast ── */
|
|
.toast-error {
|
|
position: fixed; bottom: 2rem; left: 50%; transform: translateX(-50%);
|
|
background: #ef4444; color: #fff; padding: .8rem 1.6rem;
|
|
border-radius: 12px; font-size: .95rem; font-weight: 600;
|
|
box-shadow: 0 4px 20px rgba(0,0,0,.4);
|
|
opacity: 0; pointer-events: none; transition: opacity .25s;
|
|
z-index: 1000; white-space: nowrap;
|
|
}
|
|
.toast-error.show { opacity: 1; }
|
|
|
|
/* ── Responsive: pantalla grande (TV táctil) ── */
|
|
@media (min-height: 800px) {
|
|
.prioridad-grid { gap: 1.4rem; }
|
|
.btn-prioridad { padding: 2rem 1.5rem; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- ══ PASO 1: Selección de prioridad ══════════════════════════ -->
|
|
<div id="screen-prio" class="screen visible">
|
|
<div class="kiosko-header">
|
|
<i class="fas fa-ticket-alt fa-2x mb-3" style="color:#60a5fa"></i>
|
|
<h1>Solicitar Turno</h1>
|
|
<p>Seleccione su tipo de atención</p>
|
|
</div>
|
|
|
|
<div class="prioridad-grid">
|
|
<?php foreach ($prioridades as $prio): ?>
|
|
<button
|
|
class="btn-prioridad"
|
|
style="background: <?= htmlspecialchars($prio['color']) ?>;"
|
|
data-codigo="<?= htmlspecialchars($prio['codigo']) ?>"
|
|
data-nombre="<?= htmlspecialchars($prio['nombre']) ?>"
|
|
data-color="<?= htmlspecialchars($prio['color']) ?>"
|
|
onclick="seleccionarPrioridad(this)"
|
|
>
|
|
<i class="<?= htmlspecialchars($prio['icono']) ?>"></i>
|
|
<span class="letra"><?= 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 opcionales ════════════════════════════════ -->
|
|
<div id="screen-datos" class="screen hidden">
|
|
<div class="kiosko-header">
|
|
<div id="badge-prio" style="display:inline-block; padding:.5rem 1.4rem; border-radius:40px; font-size:1.2rem; font-weight:700; margin-bottom:1rem;"></div>
|
|
<h1>Datos de contacto</h1>
|
|
<p>Opcional — para notificaciones por WhatsApp</p>
|
|
</div>
|
|
|
|
<div class="form-kiosko">
|
|
<div class="mb-4">
|
|
<label for="inp-nombre">Nombre completo</label>
|
|
<input type="text" id="inp-nombre" class="form-control" placeholder="Ej: Juan Pérez" maxlength="120" autocomplete="off">
|
|
</div>
|
|
<div class="mb-4">
|
|
<label for="inp-cel">Celular (WhatsApp)</label>
|
|
<input type="tel" id="inp-cel" class="form-control" placeholder="Ej: 3001234567" maxlength="20" autocomplete="off" inputmode="numeric">
|
|
<div class="hint">Incluya código de país si es diferente a Colombia (+57)</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
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ══ PASO 3: Ticket asignado ══════════════════════════════════ -->
|
|
<div id="screen-ticket" class="screen hidden">
|
|
<div class="ticket-box">
|
|
<div class="ticket-subtitle">Su número de turno es</div>
|
|
<div id="tick-codigo" class="ticket-codigo">—</div>
|
|
<div id="tick-prioridad" class="ticket-prioridad"></div>
|
|
<div class="ticket-subtitle mt-2">Posición en cola</div>
|
|
<div id="tick-posicion" class="ticket-numero">—</div>
|
|
<div class="ticket-instruc">
|
|
Por favor espere ser llamado.<br>
|
|
Recuerde traer su documento de identidad y la orden médica.
|
|
</div>
|
|
</div>
|
|
<button class="btn-nuevo" onclick="reiniciar()">
|
|
<i class="fas fa-plus me-1"></i>Nuevo turno
|
|
</button>
|
|
</div>
|
|
|
|
<!-- ══ Spinner de carga ════════════════════════════════════════ -->
|
|
<div class="spinner-overlay" id="spinner">
|
|
<div class="spinner"></div>
|
|
</div>
|
|
|
|
<!-- ══ Toast de error ════════════════════════════════════════ -->
|
|
<div class="toast-error" id="toast-error"></div>
|
|
|
|
<script>
|
|
// ── Estado local ──────────────────────────────────────────
|
|
let prioCodigo = '';
|
|
let prioNombre = '';
|
|
let prioColor = '';
|
|
|
|
const API_URL = '<?= BASE_URL ?>modules/turnero/api/create_turno.php';
|
|
|
|
// ── Navegación entre pasos ────────────────────────────────
|
|
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;
|
|
|
|
document.getElementById('badge-prio').textContent = prioCodigo + ' — ' + prioNombre;
|
|
document.getElementById('badge-prio').style.background = prioColor;
|
|
|
|
mostrar('screen-datos');
|
|
document.getElementById('inp-nombre').focus();
|
|
}
|
|
|
|
function volverPrioridades() {
|
|
mostrar('screen-prio');
|
|
}
|
|
|
|
// ── Crear turno ───────────────────────────────────────────
|
|
async function confirmarTurno() {
|
|
const nombre = document.getElementById('inp-nombre').value.trim();
|
|
const cel = document.getElementById('inp-cel').value.trim();
|
|
|
|
// Validación mínima de celular
|
|
if (cel && !/^\+?\d{7,15}$/.test(cel.replace(/\s/g, ''))) {
|
|
mostrarError('Ingrese un número de celular válido (solo dígitos).');
|
|
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: nombre || null,
|
|
paciente_cel: cel || null,
|
|
}),
|
|
});
|
|
|
|
const json = await res.json();
|
|
|
|
if (!json.ok) {
|
|
throw new Error(json.error ?? 'Error al crear turno');
|
|
}
|
|
|
|
const t = json.turno ?? json.data?.turno;
|
|
mostrarTicket(t);
|
|
|
|
} catch (err) {
|
|
mostrarError(err.message);
|
|
} finally {
|
|
setSpinner(false);
|
|
}
|
|
}
|
|
|
|
// ── Mostrar ticket ────────────────────────────────────────
|
|
function mostrarTicket(t) {
|
|
document.getElementById('tick-codigo').textContent = t.codigo;
|
|
document.getElementById('tick-codigo').style.color = prioColor;
|
|
document.getElementById('tick-prioridad').innerHTML =
|
|
'<span style="background:' + prioColor + ';padding:.3rem .9rem;border-radius:20px;font-weight:700;">'
|
|
+ prioCodigo + ' — ' + prioNombre + '</span>';
|
|
document.getElementById('tick-posicion').textContent = '#' + t.posicion_cola;
|
|
mostrar('screen-ticket');
|
|
|
|
// Auto-reinicio tras 30 s para liberar el kiosko
|
|
clearTimeout(window._reinicioTimer);
|
|
window._reinicioTimer = setTimeout(reiniciar, 30000);
|
|
}
|
|
|
|
// ── Reinicio ──────────────────────────────────────────────
|
|
function reiniciar() {
|
|
clearTimeout(window._reinicioTimer);
|
|
document.getElementById('inp-nombre').value = '';
|
|
document.getElementById('inp-cel').value = '';
|
|
prioCodigo = prioNombre = prioColor = '';
|
|
mostrar('screen-prio');
|
|
}
|
|
|
|
// ── Utilidades ────────────────────────────────────────────
|
|
function setSpinner(on) {
|
|
document.getElementById('spinner').classList.toggle('active', on);
|
|
}
|
|
|
|
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>
|