getConnection(); $_myIp = trim(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? '')[0]); // ── Cargar datos para los tabs (con fallback para tablas aún no migradas) ── $lugares = []; $prioridades = []; $examTipos = []; $formulariosCons= []; $sesiones = []; $sesionHoy = false; $cfg = []; $_loadErrors = []; try { $lugares = $pdo->query( 'SELECT * FROM turnero_lugares ORDER BY sort_order ASC, id ASC' )->fetchAll(PDO::FETCH_ASSOC); // Cargar formularios de consentimiento vinculados a cada lugar $lugarFormIds = []; foreach ($lugares as $lu) { $stmtLcf = $pdo->prepare( "SELECT formulario_id FROM turnero_lugar_consentimientos WHERE lugar_id = ?" ); $stmtLcf->execute([$lu['id']]); $lugarFormIds[(int)$lu['id']] = $stmtLcf->fetchAll(PDO::FETCH_COLUMN); } } catch (\Throwable $e) { $_loadErrors[] = 'turnero_lugares: ' . $e->getMessage(); $lugarFormIds = []; } $dispositivos = []; try { $dispositivos = $pdo->query( "SELECT d.*, l.nombre AS lugar_nombre FROM turnero_dispositivos d LEFT JOIN turnero_lugares l ON l.id = d.lugar_id ORDER BY d.activo DESC, l.sort_order ASC, d.nombre ASC" )->fetchAll(PDO::FETCH_ASSOC); } catch (\Throwable $e) { $_loadErrors[] = 'turnero_dispositivos: ' . $e->getMessage(); } try { $prioridades = $pdo->query( 'SELECT * FROM turnero_prioridades ORDER BY orden_peso ASC' )->fetchAll(PDO::FETCH_ASSOC); } catch (\Throwable $e) { $_loadErrors[] = 'turnero_prioridades: ' . $e->getMessage(); } try { $examTipos = $pdo->query( 'SELECT et.*, GROUP_CONCAT(etc.formulario_id) AS form_ids FROM exam_tipos et LEFT JOIN exam_tipo_consentimientos etc ON etc.exam_tipo_id = et.id GROUP BY et.id ORDER BY et.categoria ASC, et.nombre ASC' )->fetchAll(PDO::FETCH_ASSOC); } catch (\Throwable $e) { $_loadErrors[] = 'exam_tipos: ' . $e->getMessage(); } try { $formulariosCons = $pdo->query( "SELECT id, nombre FROM lab_formularios WHERE is_active=1 ORDER BY nombre ASC" )->fetchAll(PDO::FETCH_ASSOC); } catch (\Throwable $e) { $_loadErrors[] = 'lab_formularios: ' . $e->getMessage(); } try { $sesiones = $pdo->query( 'SELECT s.*, a1.full_name AS abierto_nombre, a2.full_name AS cerrado_nombre FROM turnero_sesiones s LEFT JOIN admin_users a1 ON a1.id = s.abierto_por LEFT JOIN admin_users a2 ON a2.id = s.cerrado_por ORDER BY s.fecha DESC LIMIT 30' )->fetchAll(PDO::FETCH_ASSOC); } catch (\Throwable $e) { $_loadErrors[] = 'turnero_sesiones: ' . $e->getMessage(); } try { $stmtHoy = $pdo->prepare( "SELECT s.*, (SELECT COUNT(*) FROM turnero_turnos WHERE sesion_id = s.id) AS total_turnos FROM turnero_sesiones s WHERE s.fecha = CURDATE()" ); $stmtHoy->execute(); $sesionHoy = $stmtHoy->fetch(PDO::FETCH_ASSOC); } catch (\Throwable $e) { $_loadErrors[] = 'sesion_hoy: ' . $e->getMessage(); } try { $cfgRows = $pdo->query("SELECT clave, valor FROM lab_config WHERE clave LIKE 'turnero_%'")->fetchAll(PDO::FETCH_ASSOC); foreach ($cfgRows as $r) { $cfg[$r['clave']] = $r['valor']; } } catch (\Throwable $e) { $_loadErrors[] = 'lab_config: ' . $e->getMessage(); } $tvMedia = []; try { $tvMedia = $pdo->query( "SELECT id, tipo, url, orden, duracion_segundos FROM turnero_tv_media ORDER BY orden ASC" )->fetchAll(PDO::FETCH_ASSOC); } catch (\Throwable $e) { $_loadErrors[] = 'turnero_tv_media: ' . $e->getMessage(); } $plantillasAprobadas = []; $plantillasHabilitadas = []; try { $plantillasAprobadas = $pdo->query( "SELECT id, name, template_name, language_code, body_text FROM message_templates WHERE status='approved' ORDER BY name ASC" )->fetchAll(PDO::FETCH_ASSOC); $pdo->exec("CREATE TABLE IF NOT EXISTS turnero_chat_plantillas ( template_id INT NOT NULL, PRIMARY KEY (template_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); $plantillasHabilitadas = array_column( $pdo->query("SELECT template_id FROM turnero_chat_plantillas")->fetchAll(PDO::FETCH_ASSOC), 'template_id' ); } catch (\Throwable $e) { $_loadErrors[] = 'plantillas_chat: ' . $e->getMessage(); } $tab = $_GET['tab'] ?? 'lugares'; ?> Configuración Turnero
Algunas tablas aún no existen (ejecuta las migraciones pendientes):
($l['tipo'] ?? 'muestras') === 'recepcion')); $lugaresMues = array_values(array_filter($lugares, fn($l) => ($l['tipo'] ?? 'muestras') === 'muestras')); ?>

Escritorios de Recepción

Cada escritorio es una ventanilla donde un recepcionista atiende pacientes. Aparecerá en la columna izquierda de la pantalla TV global.

No hay escritorios de recepción configurados

Agregar escritorio de recepción


Estaciones de Toma de Muestras

Cada estación es donde se toman muestras o se realizan pruebas. Aparecerá en la columna derecha de la pantalla TV global.

No hay estaciones de muestras configuradas

Agregar estación de toma de muestras


URLs de pantalla TV

Pantalla Global (sala de espera — recomendada)

Muestra TODOS los escritorios de recepción y estaciones de muestras en una sola pantalla.

Kiosko de turnos (tablet entrada)
Gestión — Escritorios de Recepción
Gestión — Estaciones de Toma de Muestras

Dispositivos / Tablets autorizadas

Cada equipo queda bloqueado a su lugar asignado por token de navegador. Si el equipo no aparece aquí, verá todos los lugares disponibles.

Token / ID equipo Nombre / descripción Lugar asignado Estado
No hay dispositivos registrados
Token de este equipo: cargando…

Agregar dispositivo

Tipos de examen y sus formularios de consentimiento

$items): ?>
Inactivo

No hay tipos de examen configurados

Agregar tipo de examen

Sin formularios activos

Vista inversa: por consentimiento

Sin exámenes vinculados' : implode(', ', array_map(fn($e)=>''.$e['codigo'].'', $exsVinculados)) ?>

Códigos de prioridad (menor orden_peso = mayor prioridad)

Orden:

Los códigos de prioridad no se pueden eliminar (están vinculados a turnos). Solo puede editarlos.

Sesión de hoy

Fecha: • Turnos: • Inicio: • Cierre:

No hay sesión creada para hoy.

Historial de sesiones (últimas 30)

Sin historial

Fecha Inicio Cierre Abierto por Estado

Notificaciones WhatsApp

>
>
Consulta, toma de muestra, etc. — incluye link de firma
Solo notifica turno, sin consentimiento
La plantilla con consentimiento debe contener una variable de URL ({{1}}) para el enlace de firma. El sistema usará texto plano como fallback si la plantilla falla.

Perfil de WhatsApp Business

Plantillas habilitadas en el chat

Marca las plantillas aprobadas disponibles en el chat del turnero.

No hay plantillas aprobadas. Sincroniza primero desde WhatsApp Business.

Playlist de la Pantalla TV

Videos e imágenes se reproducen en bucle, uno detrás de otro, sin sonido, como fondo en la pantalla TV del turnero. Formatos: MP4, WebM, JPG, PNG, WEBP — máximo 500 MB por archivo.

seg
Sin videos ni imágenes en la playlist.