Commit Graph
1196 Commits
Author SHA1 Message Date
Lizandro Guarnizo 95f83444a0 fix: system_config toma prioridad sobre terms_versions para mensajes T&C 2026-03-13 23:17:15 -05:00
Lizandro Guarnizo abfab22c35 fix: advisory lock por usuario en processMessage para eliminar race condition
- GET_LOCK('bot_user_ID', 10) serializa el procesamiento por usuario
- Solo un proceso a la vez puede ejecutar processMessage para el mismo user
- Recarga datos frescos del usuario tras adquirir el lock
- El lock se libera siempre en el bloque finally, incluso si hay excepciones
- Resuelve definitivamente el doble envío de bienvenida + términos
2026-03-13 19:33:31 -05:00
Lizandro Guarnizo bcfaf62293 fix: deduplicar webhooks duplicados con INSERT IGNORE + UNIQUE index en message_id
- conversations.message_id ahora tiene UNIQUE index (aplicado en BD prod)
- saveMessage() usa INSERT IGNORE: si el mismo wamid llega dos veces en
  paralelo solo una llamada procesa el mensaje y llama al bot
- La segunda llamada recibe rowCount=0 y hace continue sin pasar al bot
- Elimina race condition que enviaba bienvenida + términos simultáneamente
2026-03-13 19:22:34 -05:00
Lizandro Guarnizo 80e9cb43db fix: normalizar http/https al comparar URL del PDF para evitar duplicado 2026-03-13 19:16:10 -05:00
Lizandro Guarnizo 0e9c6935d8 fix: evitar doble saludo y URL duplicada en mensaje de términos
- sendTermsMessage: solo agrega PDF al final si la URL no está ya en el mensaje
- sendTermsMessage: marca welcome_sent_at=now() junto con terms_pending=1
  para evitar race condition donde un segundo webhook concurrente cae en
  isNewUser() y envía la bienvenida mientras los términos ya fueron enviados
2026-03-13 19:10:43 -05:00
Lizandro Guarnizo 46a9d97a07 fix: eliminar polling get_last_webhook_time (innecesario en index.php) 2026-03-13 18:47:21 -05:00
Lizandro Guarnizo 8df007d3de fix: actualizar APP_URL por defecto a bot.laboratorioximenacaicedo.com 2026-03-13 16:09:20 -05:00
Lizandro Guarnizo 730e1e9636 fix: accion ENUM en lab_autorizaciones al crear orden
El metodo registrarCambioEstado usaba estadoNuevo ('pendiente')
como valor de accion, pero pendiente no es un valor valido del
ENUM de lab_autorizaciones.accion.

Se pasa 'creada' explicitamente al crear una orden nueva, que si
es un valor valido del ENUM ('creada','en_revision','autorizada',
'rechazada','en_domicilio','completada','editada').
2026-03-13 15:53:42 -05:00
Lizandro Guarnizo 5211d83e8b fix: eliminar polling get_notifications en index.php + debug wamid en broadcast
- index.php: NotificationManager simplificado a stub vacío.
  Los elementos notification-bell / global-notification-toasts no existen en el
  markup → el setInterval cada 7s solo generaba tráfico inútil a get_notifications.php
  Las notificaciones ya se manejan vía SSE en conversations.php

- send_broadcast.php: exponer debug_wamids en respuesta (wamid, wa_status, wa_contact)
  para saber exactamente qué devuelve WhatsApp por cada destinatario.
  Agregar dryRun log del payload completo al error_log para diagnóstico de
  mensajes que WhatsApp acepta (200+wamid) pero no entrega.
2026-03-13 15:26:14 -05:00
Lizandro Guarnizo 6cdc7bd6c4 fix: corregir verificación de éxito en send_broadcast.php
La API de WhatsApp NO devuelve {success:true} - devuelve
{messaging_product, messages:[{id:wamid.xxx}], contacts:[...]}.
La comprobación anterior siempre fallaba → sent_count=0, error_count=N.
Ahora verifica presencia de messages/contacts en la respuesta.
2026-03-13 13:36:50 -05:00
Lizandro Guarnizo e081dc739a feat: soporte de imagen dinámica en plantillas WhatsApp
- Detecta header_type=IMAGE en get_template_details.php
- send_message (individual): muestra input de imagen si la plantilla lo requiere,
  sube a upload_media.php y envía header_image_url al backend
- broadcast (masivo): igual, con upload previo al envío
- api/send_message.php: acepta header_image_url y lo pasa como headerParameters
  [type:image, image:{link:URL}] a WhatsAppService::sendTemplateMessage()
- api/send_broadcast.php: igual para cada destinatario del masivo
- Preview en tiempo real de la imagen seleccionada en ambos formularios
2026-03-13 13:24:22 -05:00
Lizandro Guarnizo 027c8a6f26 fix: tipo_servicio hardcodeado como 'domicilio' en portal enfermero 2026-03-13 13:15:03 -05:00
Lizandro Guarnizo 7cc29fac1e fix: quitar campo Tipo de servicio del portal enfermero 2026-03-13 13:12:00 -05:00
Lizandro Guarnizo 4bcfd585cd fix: enf-nombre muestra nombre real desde sesión PHP (no queda en Cargando…) 2026-03-13 09:03:31 -05:00
Lizandro Guarnizo b8b4bffde2 feat: sesiones minimizadas múltiples — píldoras con nombre de chat y navegación 2026-03-13 08:57:10 -05:00
Lizandro Guarnizo e9c8cf7d9b feat: crear nuevo paciente desde modal de agendamiento (enfermero portal) 2026-03-12 23:18:31 -05:00
Lizandro Guarnizo 2b46057ac1 feat: modal agendar domicilio es minimizable y arrastrable 2026-03-12 23:16:14 -05:00
Lizandro Guarnizo 5c4ddd1cd8 fix: portal enfermero — agendamiento siempre particular, sin campo seguro 2026-03-12 23:04:34 -05:00
Lizandro Guarnizo 702323f852 fix: corrección portal enfermero — pagos y agendamiento
- fix: portal.renderAgenda → portal._renderLista() (error JS al registrar pago)
- feat: modal nueva agenda añade tipo_cliente (particular/seguro), seguro_nombre, autorizacion, valor_domicilio, valor_copago
- feat: toggleSeguro() muestra/oculta campo EPS según tipo seleccionado
- fix: abrir() resetea todos los campos nuevos en cada apertura del modal
- datos de cobro se envían al guardar y se auto-asigna la enfermera creadora
2026-03-12 23:02:48 -05:00
Lizandro Guarnizo eaebda9fe7 feat: edición completa de domicilio desde panel detalle
- Modal expandido: tipo_cliente, seguro, autorizacion, valor_domicilio, valor_copago, copago_laboratorio, indicaciones, fecha, hora, tipo_servicio, notas
- Botón Editar en header del panel detalle (aparece al seleccionar un domicilio)
- editarDomicilio(id): carga y pre-rellena todos los campos del modal
- toggleSeguro(): muestra/oculta campo seguro_nombre según tipo_cliente
- Tras guardar, el detalle se refresca automáticamente
2026-03-12 23:00:25 -05:00
Lizandro Guarnizo beb1bbe1d4 feat: sistema de pagos domicilios — cobro enfermero y estado pago admin
- database/07_add_pago_domicilio.sql: 6 columnas nuevas en lab_domicilios
  (pago_estado ENUM, pago_modo, pago_monto, pago_fecha, pago_notas, pago_registrado_por)
- Domicilio::filtrarCampos(): permite guardar campos de pago vía actualizar()
- Enfermera::agenda(): incluye valor_domicilio, valor_copago, pago_estado, pago_modo, pago_monto, pago_notas
- api/lab/registrar_pago.php: endpoint POST para registrar/actualizar pago del domicilio
- enfermero_portal.php: bloque cobro en card, modal registrar pago (efectivo/transferencia/exento), objeto pagoModal JS
- lab_domicilios.php: badge pago //🔵 en fila tabla, sección pago detallada en panel lateral
2026-03-12 22:53:24 -05:00
Lizandro Guarnizo c42e0976d7 fix: enfermero_portal - mostrar imagen de la orden aunque no esté vinculada al domicilio
Si lab_domicilios.orden_id es NULL (domicilio creado antes del fix
o desde otro flujo), usa subquery para encontrar la orden más reciente
del mismo paciente que tenga local_file.

Así el enfermero siempre ve la imagen de la orden del paciente
independientemente de cómo se agendó el domicilio.
2026-03-12 22:38:48 -05:00
Lizandro Guarnizo 8956de8eaa fix: ruta doble en local_file al mostrar imagen de orden médica
La BD guarda local_file con 'uploads/media/...' incluido.
El código le añadía otro 'uploads/media/' dando ruta doble.

- lab_ordenes.php: helper _resolveImg() detecta si el path
  ya empieza con 'uploads/' y no añade el prefijo de nuevo
- enfermero_portal.php: nueva función resImg() con misma lógica,
  usada en _cardHTML() al renderizar la imagen de la orden
2026-03-12 22:23:15 -05:00
Lizandro Guarnizo d362598a70 feat: relacionar imagen de WhatsApp con orden médica al agendar domicilio
Al abrir el modal desde una imagen en el chat:
- guardar() crea primero la OrdenMedica via crear_desde_whatsapp.php
  y pasa el orden_id resultante a save_domicilio.php
- Enfermera::agenda() hace LEFT JOIN a lab_ordenes_medicas para traer
  local_file de la orden asociada (as orden_local_file)
- enfermero_portal._cardHTML() muestra la imagen de la orden médica
  con link para ampliar
2026-03-12 22:14:04 -05:00
Lizandro Guarnizo 12e7b411d7 feat: enfermero_portal - mostrar todos los datos del paciente en tarjeta de domicilio
La tarjeta ahora muestra: documento, fecha de nacimiento, EPS,
teléfono (clickeable), correo (clickeable), exámenes solicitados
y tipo de cliente (particular/seguro con nombre del seguro).

Cambios:
- Enfermera::agenda() amplía SELECT con email, fecha_nacimiento,
  numero_documento, tipo_documento, eps, examenes_solicitados,
  tipo_cliente, seguro_nombre, ciudad, indicaciones_dir, notas_admin
- my_agenda.php simplificado: ya no re-consulta lab_domicilios por
  los campos que ahora vienen directo en la query
- enfermero_portal.php _cardHTML() muestra todos los campos nuevos
2026-03-12 22:05:47 -05:00
Lizandro Guarnizo bce9a8c6b5 feat: modalAgendarDomicilio - campos fecha nacimiento y correo con auto-relleno 2026-03-12 19:58:47 -05:00
Lizandro Guarnizo 4bbb31dc30 fix: lab_usuarios.php — tab-content oculto por CSS custom vs Bootstrap 5
styles.css define .tab-content { display:none } para un sistema de tabs
propio. Bootstrap 5 agrega 'active' al .tab-pane, no al .tab-content,
por lo que el contenedor nunca se mostraba (display:none permanente).

Solución: style='display:block;padding:0' en el div .tab-content para
que Bootstrap controle la visibilidad de los panes internos.
2026-03-12 18:57:39 -05:00
Lizandro Guarnizo 3399d3cf41 fix: docker-compose.prod.yml usar red coolify externa (Coolify usa este archivo, no docker-compose.yml)
Coolify usa /artifacts/.../docker-compose.prod.yml en el deploy.
Ese archivo tenía 'whatsapp-network' local bridge — el contenedor nunca
se conectaba a la red 'coolify' donde está el Redis → SERVFAIL en DNS.

Cambios:
- networks: whatsapp-network → coolify (external: true)
- container_name: removido (Coolify asigna el suyo)
- healthcheck: timeout 5s→10s, start_period 30s→60s
2026-03-12 18:48:58 -05:00
Lizandro Guarnizo 729474b138 fix: PHP sessions → Redis correcto (session.save_path con host real)
php.ini NO expande variables de entorno, estaba hardcodeado 'tcp://redis:6379'.
PHP intentaba conectar al host inexistente 'redis' en cada request →
timeout 10s en session_start() → todos los scripts tardaban 10-13s.

Solución:
- entrypoint.sh reescribe session.save_path con el host/password real al inicio
- php.ini actualizado con placeholder (sobreescrito por entrypoint de todas formas)
2026-03-12 17:13:11 -05:00
Lizandro Guarnizo d6d312f72f fix: health.php respuesta instantánea — sin DB ni Redis
Versión anterior intentaba conectar a DB (g1286...) y Redis (zqmfom0...)
que colgaban esperando conexión → todos los workers PHP-FPM bloqueados →
nginx acepta TCP pero no responde HTTP → healthcheck timeout → unhealthy.
2026-03-12 17:04:40 -05:00
Lizandro Guarnizo 6807da8a2b fix: conectar app a red 'coolify' donde vive Redis de Coolify
El contenedor Redis (zqmfom0vw6nj3ae66mkwxek1) vive en la red 'coolify'.
La app estaba solo en la red nj05x5... → DNS SERVFAIL → health.php tardaba
10-12s intentando conectar → healthcheck timeout → unhealthy → 'no available server'.

Solución: declarar red coolify como external y conectar el servicio app.
Fix inmediato aplicado manualmente con: docker network connect coolify <container>
2026-03-12 16:58:44 -05:00
Lizandro Guarnizo a497dc4b31 fix: migración SQL + red Docker para Coolify
06_master_sync.sql:
- Comentario tenía ';' (La columna role_id ya existe; la FK) →
  el parser de migrate.php divide por ; y el texto 'la FK' quedaba
  como sentencia SQL → error SQLSTATE[42000]

docker-compose.yml:
- Eliminar whatsapp-network (red bridge propia)
- Con 1 solo servicio no se necesita red interna
- Coolify añade automáticamente su red nj05x5... (donde está Traefik)
- La red bridge extra podría interferir con el routing de Traefik
2026-03-12 16:42:14 -05:00
Lizandro Guarnizo 2537bc8cfc fix: healthcheck nunca retorna 503 — evita 'no available server'
health.php:
- DB fallo → 'warning' (no 'unhealthy', no cierra tráfico Traefik)
- Redis ya era warning desde commit anterior

docker-compose.yml:
- timeout: 5s → 10s (DB externa puede tardar más)
- start_period: 40s → 60s (tiempo suficiente para that migrations corran)
- fallback: si health.php falla, intenta / raíz antes de marcar unhealthy
2026-03-12 16:35:37 -05:00
Lizandro Guarnizo 3b85c4ac93 fix: docker-compose.yml limpiar volúmenes y container_name para Coolify
- Eliminar container_name (Coolify asigna su propio nombre, conflicto evitado)
- Eliminar ./:/var/www/html (bind mount sobreescribía código del contenedor en prod)
- Eliminar ./logs y ./uploads bind mounts (reemplazados por named volumes)
- Eliminar php-socket volume (ya no se usa)
- Usar named volumes app-logs/app-uploads (persistencia correcta en Coolify)
2026-03-12 16:32:46 -05:00
Lizandro Guarnizo 85fb463708 fix: 06_master_sync.sql corregir errores SQL para MariaDB
1. ADD CONSTRAINT IF NOT EXISTS no es estándar en MariaDB < 10.5 —
   eliminado el bloque FK de admin_users.role_id (la integridad la
   gestiona la aplicación).
2. AFTER `pdf_path` fallaba porque la columna no existe en lab_form_envios —
   eliminado el AFTER, la columna se agrega al final de la tabla.

Esto evita que la migración falle en cada deploy.
2026-03-12 16:30:03 -05:00
Lizandro Guarnizo ec2135442d fix: eliminar wait de Redis en entrypoint.sh
Redis es externo (Coolify managed) — esperar con redis-cli causaba
30 segundos de delay en cada arranque porque:
1. redis-cli no pasaba -a $REDIS_PASSWORD (auth fallaba)
2. Redis no es local, no tiene sentido hacer ping desde dentro del contenedor
La app conecta a Redis cuando necesita (RedisQueue, ConversationState, etc.)
2026-03-12 16:26:46 -05:00
Lizandro Guarnizo 9571b2677a fix: health.php Redis fallo no marca app como unhealthy
Redis es infraestructura opcional — si falla la conexión, retorna 'warning'
en lugar de 'unhealthy', evitando que Traefik muestre 'no available server'.
Solo DB down debe marcar el app como unhealthy (es crítico).
2026-03-12 16:22:43 -05:00
Lizandro Guarnizo 2a50a5e1d4 fix: crear .installation_completed (con punto) en Dockerfile
isInstallationCompleted() en config.php busca '.installation_completed' (con punto)
pero el Dockerfile solo creaba 'installation_completed' (sin punto).
Ahora se crean ambas variantes para cubrir todos los checks del código.
2026-03-12 16:09:23 -05:00
Lizandro Guarnizo 30e38720e2 fix: eliminar REDIS_HOST hardcodeado 'redis' en todos los archivos
- docker-compose.prod.yml: REDIS_HOST=redis → ${REDIS_HOST:-127.0.0.1}
  eliminados servicios redis/worker/maintenance (usan Coolify/Supervisor)
- .env, .env.docker: REDIS_HOST=redis → 127.0.0.1 (fallback seguro)
- .env.example: actualizado con comentario de Coolify
- health.php: fallback 'redis' → '127.0.0.1'
- PHP files (RedisQueue, ConversationState, etc.) ya usaban getenv() correctamente
2026-03-12 13:48:01 -05:00
Lizandro Guarnizo d50abb3ff3 fix: eliminar Redis y worker locales del docker-compose.yml
- Redis ahora es un Database Resource externo en Coolify
  Configurar: REDIS_HOST, REDIS_PORT, REDIS_PASSWORD en env vars
- Worker corre dentro del contenedor app via Supervisor (no servicio aparte)
- Eliminados: servicio redis, worker, redis-commander, volumenes db-data/redis-data
- Solo queda: servicio app + volumen php-socket
2026-03-12 13:44:41 -05:00
Lizandro Guarnizo c8a4a399fc fix: eliminar bloqueo infinito en espera de Redis al arrancar
- entrypoint.sh: Redis wait tenía loop infinito sin timeout → el contenedor
  se quedaba colgado para siempre si Redis tardaba, causando 504 Gateway Timeout
  Ahora tiene MAX_RETRIES=15 (30s) y continúa aunque Redis no esté disponible
- entrypoint.sh: removido set -e para que errores no-críticos (permisos,
  SSL, etc.) no maten el arranque del contenedor
- docker-compose.yml: removido depends_on redis con condition:service_healthy
  Docker bloqueaba el inicio de app hasta que Redis estuviera healthy,
  lo que podía exceder el timeout de Traefik
2026-03-12 13:31:57 -05:00
Lizandro Guarnizo 937a02e5ac debug: agregar try/catch y console.log en lab_usuarios.php para diagnosticar render 2026-03-12 13:24:41 -05:00
Lizandro Guarnizo 8a862e914a fix: crear installation_completed en build → activa modo producción PHP
Sin este archivo, config.php ejecuta con display_errors=1 (modo dev).
Los PHP warnings se inyectaban dentro del bloque <script> de las páginas,
rompiendo el JavaScript y causando que el contenido no se renderizara.

El archivo se crea en el RUN del Dockerfile así sobrevive cada deploy.
2026-03-12 10:40:37 -05:00
Lizandro Guarnizo 81bdda2ca3 feat(migrate): agregar migración de datos seed para roles y módulos
20260304_roles_seed_data.sql — inicializa datos en roles/role_modules
y vincula admin_users.role_id. Todos los statements son idempotentes
(UPDATE + INSERT IGNORE), por lo que se pueden re-ejecutar sin problemas.

Esto asegura que en cualquier deploy los módulos de rol y los colores
estén correctos, aunque la migración DDL ya haya sido registrada.
2026-03-12 10:24:52 -05:00
Lizandro Guarnizo 33dfc0cf71 fix(migrate): detectar tablas faltantes aunque migración esté registrada
Si una migración está en la tabla migrations pero la tabla que crea ya
no existe en la BD (ej. Coolify recreó la DB), el sistema la re-ejecuta
automáticamente. Esto evita el error anterior donde 10 lab migrations
estaban registradas como ejecutadas pero las tablas no existían.
2026-03-12 09:46:07 -05:00
Lizandro Guarnizo 01ea867229 fix: usar expose en lugar de ports en docker-compose.yml para Coolify/Traefik
El archivo docker-compose.yml es el que usa Coolify por defecto.
Con 'ports: 8080:80' Traefik no sabía a qué puerto rutear en cada
redeploy. Con 'expose: 80' Traefik detecta el puerto 80 del contenedor
directamente (igual que docker-compose.prod.yml).

Para desarrollo local usar: docker-compose -f docker-compose.dev.yml up
2026-03-12 09:36:27 -05:00
Lizandro Guarnizo 7000464d36 fix(migrate): robustecer parser SQL y no registrar migración si hubo errores
- Eliminar comentarios de bloque /* */ antes de parsear
- Limpiar líneas vacías y comentarios -- sentencia por sentencia
- Agregar 'Duplicate key name' y 'Multiple primary key defined' a errores ignorables
- Solo registrar en tabla migrations si no hubo errores reales
- Si una sentencia falla, continuar con las demás pero marcar had_error=true
2026-03-12 09:27:46 -05:00
Lizandro Guarnizo 28307f44d1 fix: remove labels from compose (conflicts with Coolify), EXPOSE only port 80 2026-03-11 22:38:36 -05:00
Lizandro Guarnizo 0b3b499c3f fix: migrate.php also scans migrations/ folder for lab SQL files 2026-03-11 22:29:00 -05:00
Lizandro Guarnizo ef6c4277a6 fix: add explicit Traefik loadbalancer port label (port 80) 2026-03-11 22:01:58 -05:00