This commit is contained in:
Lizandro Guarnizo
2026-04-21 16:03:42 -05:00
parent 8165f66ac0
commit abd28b730a
4 changed files with 75 additions and 46 deletions
+8 -9
View File
@@ -107,16 +107,15 @@ try {
$stmtItem->execute([$solicitudId, $examId]);
}
// ── Calcular consentimientos necesarios (sin crearlos todavía) ──
// Deduplica: varios exámenes que apuntan al mismo formulario → 1 solo ítem.
$in = implode(',', array_fill(0, count($examIds), '?'));
$stmt = $pdo->prepare(
"SELECT DISTINCT etc.formulario_id, f.nombre AS formulario_nombre
FROM exam_tipo_consentimientos etc
JOIN lab_formularios f ON f.id = etc.formulario_id
WHERE etc.exam_tipo_id IN ({$in})"
// ── Consentimientos: todos los formularios activos del laboratorio ──
// (no solo los vinculados a exámenes — el recepcionista elige cuáles aplican)
$stmt = $pdo->prepare(
"SELECT id AS formulario_id, nombre AS formulario_nombre
FROM lab_formularios
WHERE is_active = 1
ORDER BY nombre ASC"
);
$stmt->execute($examIds);
$stmt->execute();
$consentimientosRequeridos = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Agregar estado actual de cada consentimiento (si ya fue enviado antes)
+35 -16
View File
@@ -245,9 +245,7 @@ $_labColor = preg_match('/^#[0-9a-fA-F]{3,8}$/', $_dispCfg['doc_color'] ?? '')
</div>
</div>
<div class="header-right">
<span id="chip-estado" class="estado-chip conectando">
<i class="fas fa-circle-notch fa-spin"></i>Conectando…
</span>
<span id="chip-estado" class="estado-chip" style="display:none"></span>
<button id="btn-activar-sonido" onclick="activarSonido()" title="Activar sonido">
<i class="fas fa-volume-mute"></i> Sonido
</button>
@@ -309,7 +307,7 @@ $_labColor = preg_match('/^#[0-9a-fA-F]{3,8}$/', $_dispCfg['doc_color'] ?? '')
</div>
<script>
// ── Configuración desde URL ───────────────────────────────────
// ── Config desde URL ─────────────────────────────────────────
const params = new URLSearchParams(location.search);
const area = params.get('display') || 'recepcion';
const lugarId = params.get('lugar_id') || '';
@@ -319,7 +317,29 @@ const BASE_API = '<?= BASE_URL ?>modules/turnero/api/';
document.getElementById('lbl-area').textContent =
area === 'lugar' && lugarId ? `Lugar #${lugarId} — Turnos` : 'Recepción — Turnos';
// ── Reloj ─────────────────────────────────────────────────────
// ── Video publicitario desde URL ?video=URL ───────────────────
(function initVideo() {
const videoUrl = params.get('video');
if (!videoUrl) return;
const panel = document.getElementById('video-panel');
const src = document.getElementById('video-src');
const vid = document.getElementById('video-pub');
const body = document.getElementById('display-body');
src.src = videoUrl;
vid.load();
panel.style.display = 'block';
// 3 columnas: turno | video | cola
body.style.gridTemplateColumns = '1fr 320px 280px';
})();
// ── Helper: oscurecer color hex ───────────────────────────────
function shadeColor(hex, pct) {
const num = parseInt(hex.replace('#',''), 16);
const r = Math.max(0, Math.min(255, (num >> 16) + pct));
const g = Math.max(0, Math.min(255, ((num >> 8) & 0xff) + pct));
const b = Math.max(0, Math.min(255, (num & 0xff) + pct));
return `rgb(${r},${g},${b})`;
}
function actualizarReloj() {
document.getElementById('reloj').textContent =
new Date().toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
@@ -381,17 +401,14 @@ function playBeep() {
function anunciarTurno(codigo, nombre) {
// 1. Beep electrónico
playBeep();
// 2. Voz sintética — funciona sin interacción previa en la mayoría de navegadores
// 2. Voz sintética — solo el número del turno, sin el nombre
if ('speechSynthesis' in window) {
// Esperar a que el beep termine antes de hablar
setTimeout(() => {
window.speechSynthesis.cancel();
const letras = codigo.split('').join(' '); // "A 0 1" para mejor pronunciación
let texto = `Turno ${letras}.`;
if (nombre) texto += ` ${nombre}, por favor pase a recepción.`;
const utt = new SpeechSynthesisUtterance(texto);
const utt = new SpeechSynthesisUtterance(`Turno ${letras}`);
utt.lang = 'es-CO';
utt.rate = 0.88;
utt.rate = 0.85;
utt.pitch = 1.05;
utt.volume = 1;
window.speechSynthesis.speak(utt);
@@ -425,7 +442,9 @@ function renderSnapshot(snap) {
}
elCodigo.textContent = activo.codigo;
elCodigo.style.color = activo.prioridad_color || '';
elCodigo.style.color = activo.prioridad_color
? shadeColor(activo.prioridad_color, -30)
: 'var(--brand-dark)';
elNombre.textContent = activo.paciente_nombre || '';
elPrio.style.background = (activo.prioridad_color || '#1565c0') + '22';
elPrio.style.color = activo.prioridad_color || '#1565c0';
@@ -520,8 +539,8 @@ function conectarSSE() {
esSource.addEventListener('open', () => {
reconnectDelay = 2000;
sseViva = true;
chipEstado.className = 'estado-chip conectado';
chipEstado.innerHTML = '<i class="fas fa-circle me-1" style="font-size:.5rem"></i>En vivo';
// SSE conectado → ocultar chip (el polling garantiza actualización)
chipEstado.style.display = 'none';
});
esSource.addEventListener('cola_update', (e) => {
@@ -533,8 +552,8 @@ function conectarSSE() {
esSource.addEventListener('error', () => {
sseViva = false;
chipEstado.className = 'estado-chip error';
chipEstado.innerHTML = '<i class="fas fa-exclamation-circle me-1"></i>Reconectando…';
// Solo mostramos chip cuando SSE falla (el polling sigue funcionando)
chipEstado.style.display = 'none'; // silencioso — polling cubre
esSource.close();
setTimeout(conectarSSE, reconnectDelay);
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
+28 -15
View File
@@ -229,23 +229,29 @@ unset($p);
</div>
</div>
<!-- ══ PASO 2: Datos opcionales ════════════════════════════════ -->
<!-- ══ PASO 2: Datos del paciente ═══════════════════════════════ -->
<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>
<h1>Ingresa tu cédula</h1>
<p>Escribe tu número de documento de identidad</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">
<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-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>
<label for="inp-cel">WhatsApp
<span style="color:#64748b;font-size:.85rem;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">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
@@ -307,7 +313,7 @@ unset($p);
document.getElementById('badge-prio').style.background = prioColor;
mostrar('screen-datos');
document.getElementById('inp-nombre').focus();
document.getElementById('inp-cedula').focus();
}
function volverPrioridades() {
@@ -316,12 +322,19 @@ unset($p);
// ── Crear turno ───────────────────────────────────────────
async function confirmarTurno() {
const nombre = document.getElementById('inp-nombre').value.trim();
const cedula = document.getElementById('inp-cedula').value.trim();
const cel = document.getElementById('inp-cel').value.trim();
// Validación mínima de celular
if (!cedula) {
mostrarError('Por favor ingresa tu número de cédula.');
return;
}
if (!/^\d{5,15}$/.test(cedula)) {
mostrarError('La cédula debe contener solo dígitos (mínimo 5).');
return;
}
if (cel && !/^\+?\d{7,15}$/.test(cel.replace(/\s/g, ''))) {
mostrarError('Ingrese un número de celular válido (solo dígitos).');
mostrarError('Ingresa un número de WhatsApp válido.');
return;
}
@@ -333,8 +346,8 @@ unset($p);
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prioridad_codigo: prioCodigo,
paciente_nombre: nombre || null,
paciente_cel: cel || null,
paciente_nombre: cedula, // se guarda como referencia de identificación
paciente_cel: cel || null,
}),
});
@@ -372,7 +385,7 @@ unset($p);
// ── Reinicio ──────────────────────────────────────────────
function reiniciar() {
clearTimeout(window._reinicioTimer);
document.getElementById('inp-nombre').value = '';
document.getElementById('inp-cedula').value = '';
document.getElementById('inp-cel').value = '';
prioCodigo = prioNombre = prioColor = '';
mostrar('screen-prio');
+4 -6
View File
@@ -602,12 +602,10 @@ async function guardarSolicitud() {
btn.innerHTML = '<i class="fas fa-check me-1"></i>Guardado';
// Mostrar sección de consentimientos si aplica
if (consentimientos.length > 0) {
renderConsentimientos(consentimientos);
document.getElementById('sec-consentimientos').style.display = '';
document.getElementById('btn-enviar-consent').classList.remove('d-none');
}
// Mostrar consentimientos (siempre — son todos los formularios del lab)
renderConsentimientos(consentimientos);
document.getElementById('sec-consentimientos').style.display = '';
document.getElementById('btn-enviar-consent').classList.remove('d-none');
document.getElementById('btn-pasar-lugar').classList.remove('d-none');
document.getElementById('btn-guardar').classList.add('d-none');