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'; ?>
= htmlspecialchars($le) ?>Escritorios de Recepción
= count($lugaresRec) ?>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
= count($lugaresMues) ?>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
Muestra TODOS los escritorios de recepción y estaciones de muestras en una sola pantalla.
Dispositivos / Tablets autorizadas
= count($dispositivos) ?>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 | |
|---|---|---|---|---|
= htmlspecialchars(substr($dv['token'], 0, 8)) ?>…
= htmlspecialchars($dv['ip'] ?: '—') ?>
|
= htmlspecialchars($dv['nombre']) ?> | = htmlspecialchars($dv['lugar_nombre'] ?? '—') ?> | = $dv['activo'] ? 'Activo' : 'Inactivo' ?> | |
| No hay dispositivos registrados | ||||
cargando…
Agregar dispositivo
Tipos de examen y sus formularios de consentimiento
No hay tipos de examen configurados
Agregar tipo de examen
Vista inversa: por consentimiento
'.$e['codigo'].'', $exsVinculados))
?>
Códigos de prioridad (menor orden_peso = mayor prioridad)
Los códigos de prioridad no se pueden eliminar (están vinculados a turnos). Solo puede editarlos.
Sesión de hoy
No hay sesión creada para hoy.
Historial de sesiones (últimas 30)
Sin historial
| Fecha | Inicio | Cierre | Abierto por | Estado |
|---|---|---|---|---|
| = htmlspecialchars($s['fecha']) ?> | = $s['inicio_at'] ? date('H:i', strtotime($s['inicio_at'])) : '—' ?> | = $s['fin_at'] ? date('H:i', strtotime($s['fin_at'])) : '—' ?> | = htmlspecialchars($s['abierto_nombre'] ?? '—') ?> | = $s['fin_at'] ? 'Cerrada' : 'Abierta' ?> |
Notificaciones WhatsApp
{{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.
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.