From ab8b5ae620929161bfa17cda108ba8a7eae96f34 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:44:39 -0500 Subject: [PATCH] up --- DOCUMENTACION_TURNERO.html | 1022 ++++++++++++++++++++++ WEBHOOK_ENDPOINTS.md | 163 ++++ config/config.php | 55 +- modules/turnero/api/_helpers.php | 19 + modules/turnero/api/create_turno.php | 6 +- modules/turnero/api/get_dashboard.php | 16 +- modules/turnero/views/dashboard.php | 8 +- modules/turnero/views/display_global.php | 620 +++++++------ modules/turnero/views/historial.php | 4 +- 9 files changed, 1590 insertions(+), 323 deletions(-) create mode 100644 DOCUMENTACION_TURNERO.html create mode 100644 WEBHOOK_ENDPOINTS.md diff --git a/DOCUMENTACION_TURNERO.html b/DOCUMENTACION_TURNERO.html new file mode 100644 index 0000000..cf2a692 --- /dev/null +++ b/DOCUMENTACION_TURNERO.html @@ -0,0 +1,1022 @@ + + + + + +Flujo Completo del Turnero — Laboratorio Clínico + + + + + + + +
+

🏥 Sistema Turnero Inteligente

+
Flujo completo del proceso — Desde la llegada del paciente hasta la toma de muestras
+
+ 📋 9 Tablas en BD + ⚡ 22 APIs REST + 🖥️ 9 Vistas + 📡 SSE en Tiempo Real + 📱 WhatsApp Integration +
+
+ + + + + + +
+ + + + +
+
+
👥
+
+

Actores del Sistema

+ Quiénes intervienen +
+
+ +
+
+
🧑‍🦽
+

Paciente

+

Llega al laboratorio, usa el kiosko táctil para obtener su turno, firma consentimientos desde su celular vía WhatsApp.

+
+ kiosko.php + ver_formulario_enviado.php +
+
+
+
👩‍💼
+

Recepcionista

+

Llama turnos, vincula paciente, selecciona exámenes, registra pago, envía consentimientos y pasa al paciente al área de muestras.

+
+ recepcion.php + display_recepcion.php +
+
+
+
🧑‍⚕️
+

Auxiliar de Muestras

+

Llama turnos de su estación, verifica consentimientos firmados, inicia atención, toma la muestra y finaliza el turno.

+
+ lugar.php + display (por lugar) +
+
+
+
⚙️
+

Administrador

+

Configura lugares, exámenes, prioridades, plantillas WhatsApp. Monitorea dashboard e historial. Exporta reportes CSV.

+
+ configuracion.php + dashboard.php + historial.php +
+
+
+
📺
+

Pantallas TV

+

Muestran en tiempo real el turno en atención y la cola de espera. Se actualizan vía SSE (Server-Sent Events).

+
+ display_global.php + display.php +
+
+
+
+ + + + +
+
+
🎫
+
+

Sistema de Prioridades

+ Motor de cola +
+
+ +

+ El motor de cola ordena los turnos por peso de prioridad (menor = más urgente) y luego por orden de llegada (FIFO). Las prioridades son configurables desde el panel de administración. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CódigoPrioridadColorIconoDescripciónPeso
ANiños#ef4444👶 fa-childPacientes menores de edad1 (mayor prioridad)
BEmbarazadas#f97316💗 fa-heartMujeres en estado de embarazo2
CAdulto mayor#eab308🧓 fa-person-caneMayores de 60 años3
DDiscapacidad#8b5cf6♿ fa-wheelchairPersonas con discapacidad4
EPaciente general#3b82f6🧑 fa-userAtención general5
FMuestra pendiente#6b7280🧪 fa-vialEntrega de muestra tomada previamente6 (menor prioridad)
+ +
+

💡 Ejemplo de correlativos independientes y ordenamiento

+

Cada prioridad tiene su propia secuencia:
+ A-001 (niño, llegó 8:00) → E-001 (general, llegó 8:05) → A-002 (niño, llegó 8:10) → E-002 (general, llegó 8:15)

+ El motor de cola ordena por peso de prioridad y luego por número correlativo: primero llama a A-001 (prioridad A, peso 1), luego A-002, después E-001, y finalmente E-002.

+
+
+ + + + +
+
+
🔄
+
+

Máquina de Estados del Turno

+ Lifecycle +
+
+ +
+

Estados posibles de un turno

+
+
+ espera +
+
+ 🛎️en_recepcion +
+
+ 🚶en_espera_lugar +
+
+ 🔬en_servicio +
+
+ finalizado +
+
+ 🚫ausente +
+
+ cancelado +
+
+ +

Transiciones permitidas

+
+
+ espera + + en_recepcion + (recepcionista llama) +
+
+ en_recepcion + + en_espera_lugar + (pasa a estación) +
+
+ en_espera_lugar + + en_servicio + (⚠️ requiere consentimientos OK) +
+
+ en_servicio + + finalizado + (muestra tomada) +
+
+ en_servicio + + en_espera_lugar + (regresar a cola) +
+
+ cualquier estado + + ausente / cancelado +
+
+
+ +
+

🔒 Bloqueo de consentimientos

+

La transición en_espera_lugar → en_servicio está bloqueada si el turno tiene consentimientos en estado pendiente, enviado o visto. Todos deben estar en firmado o rechazado antes de poder iniciar la atención. Esto se valida en cambiar_estado.php.

+
+
+ + + + +
+
+
📋
+
+

Flujo Completo — Paso a Paso

+ Proceso end-to-end +
+
+ +
+ + +
+
+
1
+
+
+
+
+

🖥️ Kiosko — El paciente obtiene su turno

+

El paciente llega al laboratorio y se acerca al kiosko táctil (tablet o pantalla touch). No requiere login — es una pantalla pública siempre activa.

+ +
+
+
Paso 1
+
Selecciona su tipo de atención (A-F) tocando el botón correspondiente
+
+
+
Paso 2
+
Ingresa su número de cédula y opcionalmente su WhatsApp
+
+
+
Paso 3
+
El sistema genera un código con correlativo independiente por prioridad (ej: "A-001", "A-002", "B-001", "E-001")
+
+
+
Resultado
+
Turno en estado espera — El paciente se sienta a esperar
+
+
+ +
+ 📄 kiosko.php + ⚡ create_turno.php +
+ +
+

🔐 Seguridad del kiosko

+

El kiosko nunca expone datos de otros pacientes. Solo muestra el código del turno recién creado y su posición en cola. El campo paciente_nombre almacena la cédula digitada (no el nombre real). El nombre real se vincula después en recepción.

+
+
+
+
+ + +
+
+
2
+
+
+
+
+

🛎️ Recepción — Vinculación y solicitud

+

El recepcionista ve la cola de turnos en espera en su pantalla. Cuando decide atender un turno, lo llama y se abre la ficha de atención.

+ +
+
+
Llamar turno
+
Clic en "Llamar siguiente" o en la campana de un turno específico de la cola
+
+
+
Estado cambia a
+
en_recepcion — El turno aparece en la pantalla TV de recepción
+
+
+
Buscar paciente
+
Busca por nombre o documento en la base de datos de pacientes (reutiliza lab_pacientes)
+
+
+
Seleccionar exámenes
+
Marca los exámenes solicitados de un catálogo agrupado por categoría
+
+
+
Lugar destino
+
Selecciona a qué estación de toma de muestras enviar al paciente
+
+
+
Registro de pago
+
Monto + método (efectivo, transferencia, tarjeta, EPS, cortesía)
+
+
+ +
+ 📄 recepcion.php + ⚡ llamar_turno.php + ⚡ create_solicitud.php + ⚡ get_cola.php +
+ +
+

📝 Guardar solicitud

+

Al hacer clic en "Guardar solicitud", se crea la turnero_solicitud con los exámenes (turnero_examen_items), el pago y el lugar destino. El sistema calcula automáticamente qué consentimientos informados se requieren basándose en los exámenes seleccionados y el lugar de destino.

+
+
+
+
+ + +
+
+
3
+
+
+
+
+

📱 Consentimientos Informados — Firma digital

+

Una vez guardada la solicitud, aparecen los consentimientos requeridos. El recepcionista puede enviarlos por WhatsApp o hacer que el paciente firme presencialmente en una tablet.

+ +
+
+
Cálculo automático
+
El sistema cruza exam_tipo_consentimientos + turnero_lugar_consentimientos para determinar qué formularios aplica
+
+
+
Token UUID
+
Cada consentimiento genera un token único (ej: "a3f2-4b1c-...") para acceso seguro sin login
+
+
+
Envío por WhatsApp
+
Se envía mensaje con template "consentimiento_turno" + enlace de firma al celular del paciente
+
+
+
Firma presencial
+
Botón "Firmar" abre modal con iframe del formulario — el paciente firma en la tablet del puesto
+
+
+ +
+ ⚡ send_consentimiento.php + ⚡ get_consentimientos.php + 📄 ver_formulario_enviado.php +
+ +
+

📊 Estados del consentimiento

+

+ ● pendiente → creado pero no enviado  |  + ● enviado → WhatsApp enviado  |  + ● visto → paciente abrió el enlace  |  + ● firmado → paciente firmó  |  + ● rechazado → paciente rechazó +

+
+ +
+

🔄 Deduplicación inteligente

+

Si varios exámenes requieren el mismo formulario de consentimiento, el sistema lo detecta y envía un solo enlace de firma. También si el paciente ya firmó ese consentimiento en una visita anterior, se reutiliza el token existente.

+
+
+
+
+ + +
+
+
4
+
+
+
+
+

➡️ Pasar a Lugar — El paciente se desplaza

+

El recepcionista hace clic en "Pasar a lugar". El turno cambia de estado y aparece en la cola de la estación de toma de muestras asignada.

+ +
+
+
Estado cambia a
+
en_espera_lugar
+
+
+
Se asigna
+
lugar_destino_id = la estación seleccionada (ej: "Toma de Muestras 1")
+
+
+
Pantalla TV
+
El display global muestra el anuncio: "A-001, pase a Toma de Muestras 1"
+
+
+
Recepción
+
La ficha se cierra y el recepcionista puede llamar al siguiente turno
+
+
+ +
+ ⚡ cambiar_estado.php +
+
+
+
+ + +
+
+
5
+
+
+
+
+

🔬 Toma de Muestras — Proceso completo

+

El auxiliar de la estación de muestras ve en su pantalla la cola de pacientes asignados a su lugar. Este es el proceso detallado de toma de muestras:

+ + +
+
+
5a
+
+

Llamar paciente

+

El auxiliar hace clic en "Llamar siguiente". El motor de cola selecciona el turno con mayor prioridad (menor peso) y mayor tiempo de espera. El turno pasa a estado en_servicio.

+

⚠️ Bloqueo: Si hay consentimientos sin firmar, el botón "Iniciar atención" está deshabilitado y se muestra un banner rojo indicando cuántos consentimientos faltan.

+
+
+ +
+
5b
+
+

Verificar ficha del paciente

+

Se abre la ficha con toda la información:

+
    +
  • Datos del paciente: nombre, documento, fecha de nacimiento, celular
  • +
  • Exámenes solicitados: lista con código y nombre de cada examen
  • +
  • Consentimientos: estado de cada uno (firmado/rechazado/pendiente)
  • +
+
+
+ +
+
5c
+
+

Resolver consentimientos pendientes (si los hay)

+

Si algún consentimiento aún no está firmado, el auxiliar puede:

+
    +
  • "Firmar aquí": Abre un modal con el formulario — el paciente firma en la tablet del puesto
  • +
  • "WA": Reenvía el enlace de firma por WhatsApp al celular del paciente
  • +
  • Polling automático: Cada 5 segundos el sistema verifica si el paciente firmó desde su celular
  • +
+
+
+ +
+
5d
+
+

Iniciar atención

+

Cuando todos los consentimientos están OK (firmados o rechazados), el botón "Iniciar atención" se habilita. El auxiliar hace clic y el estado cambia a en_servicio. Se registra inicio_lugar_at.

+

🧪 En este momento el auxiliar toma la muestra de sangre/orina/etc. al paciente.

+
+
+ +
+
5e
+
+

Finalizar atención

+

El auxiliar hace clic en "Finalizar". El turno pasa a estado finalizado. Se registra fin_lugar_at. La ficha se cierra y el auxiliar puede llamar al siguiente paciente.

+
+
+ +
+
⚠️
+
+

Casos especiales

+

+ "Ausente": El paciente no se presentó → estado ausente
+ "Regresar a cola": El paciente necesita esperar (ej: ayuno insuficiente) → vuelve a en_espera_lugar +

+
+
+
+ +
+ 📄 lugar.php + ⚡ llamar_turno.php + ⚡ cambiar_estado.php + ⚡ get_consentimientos.php +
+
+
+
+ + +
+
+
6
+
+
+
+
+

📺 Pantallas TV — Información en tiempo real

+

Durante todo el proceso, las pantallas TV muestran el estado actualizado del sistema. Hay 3 tipos de display:

+ +
+
+
Display Global
+
Vista general con todas las áreas: recepción + todas las estaciones de muestras. Tema oscuro. Anuncio animado cuando se llama un turno. Video de fondo configurable.
+
+
+
Display Recepción
+
Muestra el turno activo en un escritorio de recepción específico + cola de espera. Se actualiza vía SSE.
+
+
+
Display por Lugar
+
Muestra el turno activo en una estación de muestras específica + cola de ese lugar.
+
+
+
Actualización
+
SSE (Server-Sent Events) cada 2s si hay cambios + polling de respaldo cada 4s. Beep + voz sintética al llamar turno.
+
+
+ +
+ 📄 display_global.php + 📄 display.php + 📄 display_recepcion.php + ⚡ sse_turno.php + ⚡ get_cola.php + ⚡ get_display_global.php +
+
+
+
+ + +
+
+
7
+
+
+
+
+

⚙️ Administración — Monitoreo y configuración

+

El administrador tiene acceso a vistas de control y configuración del sistema completo.

+ +
+
+
Dashboard
+
KPIs del día: total turnos, atendidos, en espera, ausentes, tiempo promedio de espera y servicio. Desglose por prioridad y lugar. Auto-refresh cada 30s.
+
+
+
Historial
+
Consulta multi-día con filtros por estado, lugar, prioridad y texto libre. Paginación. Detalle expandible con todos los timestamps.
+
+
+
Configuración
+
CRUD de lugares (recepción/muestras), tipos de examen, prioridades, sesiones, WhatsApp (template Meta), video TV.
+
+
+
Exportar
+
CSV con todos los turnos, timestamps, exámenes, pagos y estado de consentimientos.
+
+
+ +
+ 📄 dashboard.php + 📄 historial.php + 📄 configuracion.php + ⚡ get_dashboard.php + ⚡ export_csv.php + ⚡ get_historial.php + ⚡ export_historial.php +
+
+
+
+ +
+
+ + + + +
+
+
📡
+
+

Diagrama de Secuencia — APIs y Datos

+ Comunicaciones +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#Actor / Vista→ APIAcción en BDResultadoNotifica
1KioskoPOST create_turno.phpINSERT turnero_turnos (estado=espera, correlativo por prioridad)Código "A-001" + posición colaSSE notify
2RecepciónPOST llamar_turno.phpUPDATE estado=en_recepcion + timestampsTurno con datos completosSSE notify
3RecepciónPOST create_solicitud.phpINSERT turnero_solicitudes + turnero_examen_itemsSolicitud + consentimientos requeridosSSE notify
4RecepciónPOST send_consentimiento.phpINSERT/UPDATE turnero_consentimientos + tokens UUIDEstado → enviado
5Paciente (celular)GET ver_formulario_enviado.php?token=...UPDATE turnero_consentimientos estado=firmadoFormulario firmado + evidenciapostMessage
6RecepciónPOST cambiar_estado.php → en_espera_lugarUPDATE estado + lugar_destino_idTurno pasa a cola del lugarSSE notify
7Lugar (auxiliar)POST llamar_turno.php (area=lugar)UPDATE estado=en_servicio + timestampsTurno con ficha completaSSE notify
8Lugar (auxiliar)GET get_consentimientos.php (polling 5s)SELECT turnero_consentimientos + paciente + exámenesVerifica si todos están firmados
9Lugar (auxiliar)POST cambiar_estado.php → finalizadoUPDATE estado + fin_lugar_atTurno completadoSSE notify
10Pantallas TVGET sse_turno.php (conexión persistente)SELECT cola + activo (cada 2s o por cambio)Render en tiempo real + audio + voz
+
+
+ + + + +
+
+
🗄️
+
+

Tablas de Base de Datos

+ 9 tablas del turnero +
+
+ +
+
+

📋 turnero_turnos

+

Tabla principal. Cada fila = un turno. Almacena: código, número, prioridad, estado, timestamps de cada fase (creado, llamado recepción, inicio/fin recepción, llamado lugar, inicio/fin lugar), paciente_nombre (cédula del kiosko), paciente_cel.

+
+
+

📅 turnero_sesiones

+

Una por día. Controla apertura/cierre del sistema. Almacena: fecha, abierto_por, inicio_at, fin_at, sse_ping_at (para notificaciones SSE).

+
+
+

📝 turnero_solicitudes

+

Vincula un turno con un paciente real, un lugar destino y los datos de pago. Se crea en recepción después de llamar el turno.

+
+
+

🧪 turnero_examen_items

+

Relación N:M entre solicitud y tipos de examen. Cada fila = un examen solicitado para esa solicitud.

+
+
+

✍️ turnero_consentimientos

+

Cada consentimiento requerido por turno. Almacena: token UUID, estado (pendiente/enviado/visto/firmado/rechazado), timestamps.

+
+
+

📍 turnero_lugares

+

Catálogo de estaciones: recepción, toma de muestras 1, 2, etc. Campos: nombre, tipo (recepcion/muestras), descripción, sort_order, activo.

+
+
+

🎫 turnero_prioridades

+

Código (A-F), nombre, color, icono, descripción, orden_peso. Configurable desde el panel admin.

+
+
+

🧪 exam_tipos

+

Catálogo de tipos de examen del laboratorio. Código, nombre, categoría. Se usa en la ficha de recepción.

+
+
+

🔗 Tablas de relación

+

+ exam_tipo_consentimientos: qué formularios requiere cada tipo de examen.
+ turnero_lugar_consentimientos: qué formularios requiere cada lugar/servicio. +

+
+
+
+ + + + +
+
+
🗺️
+
+

Resumen Visual del Flujo

+ Big picture +
+
+ +
+
+ +
+
🖥️
+
KIOSKO
+
Paciente obtiene
turno A-001
+
estado: espera
(correlativo por prioridad)
+
+
+
+
🛎️
+
RECEPCIÓN
+
Vincula paciente
exámenes + pago
+
estado: en_recepcion
+
+
+
+
📱
+
CONSENTIM.
+
Envío WhatsApp
firma digital
+
tokens UUID
+
+
+
+
🚶
+
TRÁNSITO
+
Paciente se
desplaza
+
estado: en_espera_lugar
+
+
+
+
🔬
+
MUESTRAS
+
Verifica + toma
muestra
+
estado: en_servicio
+
+
+
+
+
FINALIZADO
+
Muestra tomada
turno completo
+
estado: finalizado
+
+
+ + +
+ 📡 SSE (Server-Sent Events) + — Cada cambio de estado notifica a todas las pantallas TV en tiempo real (beep + voz sintética + animación) +
+
+
+ +
+ + + + + + + + diff --git a/WEBHOOK_ENDPOINTS.md b/WEBHOOK_ENDPOINTS.md new file mode 100644 index 0000000..4e3cf4e --- /dev/null +++ b/WEBHOOK_ENDPOINTS.md @@ -0,0 +1,163 @@ +# Webhook WhatsApp — Endpoints y Características + +## Endpoint principal + +``` +URL: /api/webhook.php +``` + +--- + +## GET — Verificación de webhook + +``` +GET /api/webhook.php?hub.mode=subscribe&hub.verify_token=TOKEN&hub.challenge=CHALLENGE +``` + +### Parámetros que envía Meta +| Parámetro | Valor esperado | +|---|---| +| `hub.mode` | `subscribe` | +| `hub.verify_token` | El token configurado en `system_config.webhook_verify_token` | +| `hub.challenge` | Número aleatorio que debe devolverse tal cual | + +### ⚠️ Bug conocido +El código lee `$_GET['hub_verify_token']` (con guión bajo), pero PHP convierte los puntos a guiones bajos automáticamente al parsear `$_GET`, por lo que **funciona correctamente**. + +### Respuesta exitosa +``` +HTTP 200 +Body: {challenge} +``` + +### Respuesta fallida +``` +HTTP 403 +Body: {"error":"Token de verificación inválido"} +``` + +--- + +## POST — Recepción de eventos + +``` +POST /api/webhook.php +Content-Type: application/json +``` + +### Estructura del payload esperado (Meta Cloud API) + +```json +{ + "object": "whatsapp_business_account", + "entry": [{ + "id": "WABA_ID", + "changes": [{ + "field": "messages", + "value": { + "messaging_product": "whatsapp", + "metadata": { + "phone_number_id": "PHONE_NUMBER_ID" + }, + "contacts": [{ + "wa_id": "573001234567", + "profile": { "name": "Nombre Contacto" } + }], + "messages": [{ + "from": "573001234567", + "id": "wamid.XXX", + "timestamp": "1234567890", + "type": "text", + "text": { "body": "Hola" } + }] + } + }] + }] +} +``` + +### Tipos de mensaje soportados +| `type` | Descripción | +|---|---| +| `text` | Texto plano | +| `image` | Imagen (con caption opcional) | +| `audio` | Audio / nota de voz | +| `video` | Video | +| `document` | Documento / PDF | +| `sticker` | Sticker | +| `reaction` | Reacción emoji a otro mensaje | +| `interactive` | Respuesta de lista o botón | + +### El campo `field` del change puede ser +- `messages` → mensajes entrantes y estados +- `conversations` → alias aceptado también + +### Eventos de estado (statuses) +```json +"statuses": [{ + "id": "wamid.XXX", + "status": "sent|delivered|read|failed", + "recipient_id": "573001234567" +}] +``` + +### Respuesta exitosa +``` +HTTP 200 +Body: {"status":"success"} +``` + +--- + +## Configuración necesaria en `system_config` (BD) + +| config_key | Descripción | +|---|---| +| `whatsapp_token` | Access Token de Meta | +| `whatsapp_phone_number_id` | Phone Number ID de la línea | +| `webhook_verify_token` | Token de verificación del webhook | +| `whatsapp_api_url` | `https://graph.facebook.com/v22.0/` | + +--- + +## Variables de entorno equivalentes (`.env`) + +```env +WHATSAPP_TOKEN= +WHATSAPP_PHONE_NUMBER_ID= +WEBHOOK_VERIFY_TOKEN= +WHATSAPP_API_URL=https://graph.facebook.com/v22.0/ +DB_HOST= +DB_PORT=3306 +DB_NAME= +DB_USER= +DB_PASS= +``` + +--- + +## Tablas BD que usa el webhook + +| Tabla | Uso | +|---|---| +| `users` | Crea o busca usuario por `phone_number` | +| `conversations` | Guarda cada mensaje (deduplicado por `message_id`) | +| `webhook_logs` | Registra el payload crudo de cada POST | +| `notifications` | Crea aviso de nuevo mensaje entrante | +| `media_queue` | Encola media que no pudo descargarse en el momento | +| `system_config` | Lee tokens y configuración | + +--- + +## Seguridad — pendiente de implementar + +- No valida la firma `X-Hub-Signature-256` en el POST. +- Se recomienda agregar antes de procesar: +```php +$signature = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? ''; +$expected = 'sha256=' . hash_hmac('sha256', $input, APP_SECRET); +if (!hash_equals($expected, $signature)) { + http_response_code(401); + exit; +} +``` diff --git a/config/config.php b/config/config.php index 0729db9..1139331 100644 --- a/config/config.php +++ b/config/config.php @@ -5,6 +5,32 @@ * Fecha: 4 de enero de 2026 - Sistema de autenticación unificado */ +// ── Cargar .env ANTES de definir cualquier constante ────────────────────────── +// Si no se hace aquí, getenv() devuelve vacío y los define() usan el fallback +// 'mysql' (nombre del contenedor Docker) en lugar de la IP real de la BD. +if (!function_exists('loadEnvFile')) { +function loadEnvFile($path) { + if (!file_exists($path)) { + return false; + } + $lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + foreach ($lines as $line) { + if (strpos($line, '#') === 0 || strpos($line, '=') === false) { + continue; + } + [$name, $value] = explode('=', $line, 2); + $name = trim($name); + $value = trim($value, " \t\n\r\0\x0B\"'"); + if (!array_key_exists($name, $_ENV)) { + $_ENV[$name] = $value; + putenv("$name=$value"); + } + } + return true; +} +} +loadEnvFile(__DIR__ . '/../.env'); + // Configuración de la base de datos - 100% desde variables de entorno (o .env) define('DB_HOST', getenv('DB_HOST') ?: 'mysql'); define('DB_PORT', getenv('DB_PORT') ?: '3306'); @@ -46,34 +72,7 @@ define('SYSTEM_MODULES', [ 'resultados' => 'Resultados', ]); -// Función para cargar archivo .env -if (!function_exists('loadEnvFile')) { -function loadEnvFile($path) { - if (!file_exists($path)) { - return false; - } - - $lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - foreach ($lines as $line) { - if (strpos($line, '#') === 0 || strpos($line, '=') === false) { - continue; - } - - [$name, $value] = explode('=', $line, 2); - $name = trim($name); - $value = trim($value, " \t\n\r\0\x0B\"'"); - - if (!array_key_exists($name, $_ENV)) { - $_ENV[$name] = $value; - putenv("$name=$value"); - } - } - return true; -} -} - -// Cargar .env si existe -loadEnvFile(__DIR__ . '/../.env'); +// loadEnvFile ya fue definida y ejecutada al inicio del archivo. // Cargar autoloader de Composer si existe (solo una vez) $composerAutoload = __DIR__ . '/../vendor/autoload.php'; diff --git a/modules/turnero/api/_helpers.php b/modules/turnero/api/_helpers.php index 4d341df..4c06d88 100644 --- a/modules/turnero/api/_helpers.php +++ b/modules/turnero/api/_helpers.php @@ -171,6 +171,25 @@ function siguienteNumero(int $sesionId): int return (int) $stmt->fetchColumn(); } +/** + * Genera el siguiente número correlativo de turno POR PRIORIDAD para una sesión. + * Cada prioridad (A, B, C, D, E, F) tiene su propia secuencia independiente. + * Usa bloqueo a nivel de fila para evitar duplicados en concurrencia. + */ +function siguienteNumeroPorPrioridad(int $sesionId, int $prioridadId): int +{ + $pdo = db(); + $stmt = $pdo->prepare( + 'SELECT COALESCE(MAX(numero), 0) + 1 AS siguiente + FROM turnero_turnos + WHERE sesion_id = ? + AND prioridad_id = ? + FOR UPDATE' + ); + $stmt->execute([$sesionId, $prioridadId]); + return (int) $stmt->fetchColumn(); +} + /** * Selecciona el siguiente turno a llamar según el motor de prioridades. * diff --git a/modules/turnero/api/create_turno.php b/modules/turnero/api/create_turno.php index 4c677c4..13eba54 100644 --- a/modules/turnero/api/create_turno.php +++ b/modules/turnero/api/create_turno.php @@ -34,7 +34,6 @@ $pdo->beginTransaction(); try { $sesionId = obtenerOCrearSesionHoy(); - $numero = siguienteNumero($sesionId); // Buscar prioridad_id $stmt = $pdo->prepare( @@ -50,7 +49,10 @@ try { $prioridadId = (int) $prioridad['id']; - // Código visual: "A001", "E042", etc. + // Correlativo independiente por prioridad (A-001, A-002... B-001, B-002... etc.) + $numero = siguienteNumeroPorPrioridad($sesionId, $prioridadId); + + // Código visual: "A001", "B001", "E001", etc. $codigo = $prioCodigo . str_pad($numero, 3, '0', STR_PAD_LEFT); $stmt = $pdo->prepare( diff --git a/modules/turnero/api/get_dashboard.php b/modules/turnero/api/get_dashboard.php index 9293fa7..f5ec09a 100644 --- a/modules/turnero/api/get_dashboard.php +++ b/modules/turnero/api/get_dashboard.php @@ -30,12 +30,20 @@ $sesion = $stmtSesion->fetch(PDO::FETCH_ASSOC); if (!$sesion) { jsonOk([ - 'fecha' => $fecha, - 'sesion' => null, - 'resumen' => ['total' => 0, 'atendidos' => 0, 'en_espera' => 0, - 'ausentes' => 0, 'cancelados' => 0, 'tiempo_promedio_min' => null], + 'fecha' => $fecha, + 'sesion' => null, + 'resumen' => [ + 'total' => 0, + 'atendidos' => 0, + 'en_espera' => 0, + 'ausentes' => 0, + 'cancelados' => 0, + 'tiempo_espera_promedio_min' => null, + 'tiempo_servicio_promedio_min' => null, + ], 'por_prioridad' => [], 'por_lugar' => [], + 'consent_stats' => [], 'turnos' => [], ]); } diff --git a/modules/turnero/views/dashboard.php b/modules/turnero/views/dashboard.php index 36dd73b..b6ba974 100644 --- a/modules/turnero/views/dashboard.php +++ b/modules/turnero/views/dashboard.php @@ -68,7 +68,7 @@ Layout::open('Dashboard Turnero', 'fas fa-chart-bar'); @@ -151,7 +151,7 @@ Layout::open('Dashboard Turnero', 'fas fa-chart-bar'); /* ═══════════════════════════════════════════════════════════ Dashboard Turnero ═══════════════════════════════════════════════════════════ */ -const API = '/modules/turnero/api/'; +const API = 'modules/turnero/api/'; const BASE_WA = ''; let _turnos = []; // cache para búsqueda local let _autoReloadId = null; @@ -194,7 +194,7 @@ function mostrarError(msg) { // ── Render ─────────────────────────────────────────────────── function renderDashboard(json) { - const { sesion, resumen, por_prioridad, por_lugar, consent_stats, turnos } = json; + const { sesion, resumen, por_prioridad, por_lugar, consent_stats = {}, turnos } = json; // ── Sesión info ──────────────────────────────────────── const si = document.getElementById('sesionInfo'); diff --git a/modules/turnero/views/display_global.php b/modules/turnero/views/display_global.php index c51e981..2cc00c5 100644 --- a/modules/turnero/views/display_global.php +++ b/modules/turnero/views/display_global.php @@ -12,8 +12,8 @@ $_labNombre = htmlspecialchars($_dispCfg['empresa_nombre'] ?? 'Sistema de Turnos $_labLogo = $_dispCfg['doc_logo_base64'] ?? ''; $_labColor = preg_match('/^#[0-9a-fA-F]{3,8}$/', $_dispCfg['doc_color'] ?? '') ? $_dispCfg['doc_color'] : '#1565c0'; $_tvVideo = $_dispCfg['turnero_tv_video'] ?? ''; -// Validar que sea una URL del mismo origen (solo rutas relativas o mismo dominio) if ($_tvVideo && !preg_match('#^https?://#', $_tvVideo)) { $_tvVideo = ''; } +$_hasVideo = (bool)$_tvVideo; ?> @@ -24,336 +24,369 @@ if ($_tvVideo && !preg_match('#^https?://#', $_tvVideo)) { $_tvVideo = ''; } - - +
@@ -370,7 +403,7 @@ if ($_tvVideo && !preg_match('#^https?://#', $_tvVideo)) { $_tvVideo = ''; }
-
+
@@ -379,33 +412,52 @@ if ($_tvVideo && !preg_match('#^https?://#', $_tvVideo)) { $_tvVideo = ''; } Sistema de Turnos — Vista General
-
+
-
--:--:--
-
- - +
+ + + +
+
+
Recepción
+
+
+
+
Toma de Muestras
+
+
+
+
+ +
+ + + +
+
+
Recepción
+
+
+
+
+
Toma de Muestras
+
+
-
-
Recepción
-
-
-
-
-
Toma de Muestras
-
-
+
@@ -431,7 +483,7 @@ if ($_tvVideo && !preg_match('#^https?://#', $_tvVideo)) { $_tvVideo = ''; }