- wa_profile.php: GET carga perfil desde Graph API, POST guardar campos, POST foto sube imagen
- configuracion.php tab sesión: UI con foto de perfil editable, campos del negocio y botón guardar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Al crear turno busca el formulario configurado en el desk de recepción
- Crea token de consentimiento y lo marca como 'enviado'
- Envía plantilla consentimiento_turno con nombre real, nombre formulario y link
- Fallback texto plano incluye el link si está disponible
- Tipo F (solo muestra) sigue usando plantilla sin consentimiento
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Turnero messages were triggering the SSE event that conversations.php listens to,
causing the contact to flash briefly at the top of the list before disappearing
on the next poll (since get_conversations.php filters them out by canal).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
get_user_messages.php and get_recent_messages.php now exclude canal='turnero'
so the main bot chat only shows bot messages even for users who also have
turnero conversations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All queries in get_conversations.php now filter (canal IS NULL OR canal='bot')
so the main chat only shows bot conversations and the turnero chat stays isolated.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
saveOutgoingMessage() now reads __app_meta.canal and stores it in conversations.
sendMediaById() accepts a $meta param and forwards it to saveOutgoingMessage.
chat_upload_media.php passes $extra (canal=turnero) to sendMediaById.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- WhatsAppService($canal): when 'turnero', swaps phoneNumberId to whatsapp_phone_number_id_turnero
so outgoing messages actually go from the turnero number (not the main bot)
- webhook.php: reads metadata.phone_number_id from the payload and compares it to
whatsapp_phone_number_id_turnero; if it matches, saves canal='turnero' and skips bot processing
- Previously all incoming messages had canal=NULL/bot so Chat Turnero never saw them,
and all outgoing messages from Chat Turnero were sent via the wrong (main bot) number
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- save_formulario.php: manejo de {clonar:true} → crea copia con "Copia de [nombre]"
- lab_formularios.php: botón icono copy en cada tarjeta + función clonarFormulario()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Capture activeUserId at the start of each async function (fetchMessages,
loadMoreMessages, pollMessages) and discard the response if the user changed
contacts while the request was in-flight.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- create_solicitud.php: evitar SQL `IN ()` vacío cuando solo_muestras=1 sin exámenes
- recepcion.php: mover embarazada+médico después del historial del paciente
- recepcion.php: actualizar label kiosko con nombre real al vincular paciente
- recepcion.php: botón "Duplicar" en historial para copiar exámenes y pago de última visita
- recepcion.php: omitir validación de consentimientos pendientes cuando es solo_muestras
- recepcion.php: agregar alias esc() = escHtml() usado en buscarMedico
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- chat_upload_media.php: multipart upload to WhatsApp Media API, sends image/video/audio/document
- chat_react.php: sends emoji reaction to a message via WhatsApp, updates reaction_emoji in DB
- chat_send_message.php: extended to support type=template with lang+params
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Attach button with file preview (image thumb for images, name+size for others)
- Audio recording via MediaRecorder API with live timer; sends as voice note
- Emoji panel with 35 common emojis (medical/comms focused)
- Reaction picker on message hover — sends via chat_react.php API
- Template modal (name, lang, JSON params) — sends via chat_send_message.php type=template
- buildBubble() now renders: inline images, <video> player, <audio> player, doc links with type icon
- Shows reaction_emoji below bubble and status tick on outgoing messages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Crea modules/turnero/views/verificar_paciente.php con Layout::open/close
- Flujo completo: cédula → OTP → éxito, mismo JS que verificar.php público
- Link del módulo ahora apunta a erp.php?m=turnero&v=verificar_paciente
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- create_turno.php: tipo F usa turnero_wa_template_muestra, resto usa turnero_wa_template
- styles.css: has-active ya incluye font-size 0.82rem (verificado)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- nav-link.active: color-mix tintado, font-size 0.93rem, border 4px
- sidebar-section: 0.72rem base, 0.82rem cuando tiene subitem activo
- sb-group: fondo y borde tintados con --cat-color via color-mix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- verificar.php: BASE_URL absoluta en fetch, fix paste multi-dígito
- send_otp.php: mensaje SMS correcto sin dos puntos ni expiración
- module.php: Chat Turnero y Verificar Paciente para recepcionistas
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Guarda scrollTop en localStorage al hacer clic en un nav-link y lo
restaura en DOMContentLoaded, evitando que el menú lateral salte al
inicio en cada navegación.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Al vincular un paciente al turno, actualiza paciente_nombre con el
nombre_completo real de lab_pacientes. El siguiente poll de la cola
muestra el nombre en lugar de la cédula del kiosko.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
callKey usaba id+llamado_at: si llamado_at llegaba null en el primer
poll y con valor en el segundo, se generaban dos claves distintas para
el mismo evento y el turno se anunciaba dos veces.
Cambia callKey a String(id) — cada turno se anuncia una sola vez
por sesión de pantalla, sin importar cambios en el timestamp.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El array 'llamados' acumulaba una entrada por cada lugar al que pasaba
un turno (recepción + toma de muestras). Al insertar un nuevo llamado
se elimina la entrada previa del mismo turno, dejando solo el último lugar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Agrega botón campana por cada card en cola (solo turnos en_espera_lugar)
- Click en card abre ficha sin llamar; campana llama ese turno específico
- Llamar siguiente ahora también llama de verdad (antes solo abría ficha)
- Cards en_servicio mantienen indicador de estetoscopio sin botón
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reemplaza window.open a lab_pacientes.php por modal Bootstrap inline
- Modal incluye campos completos: datos personales, contacto (con verificador WA), ubicación y EPS
- Botón Ver/Editar en paciente vinculado carga datos desde API y abre modal en modo edición
- Botón Cambiar desvincula y enfoca el buscador
- Al crear: seleccionarPaciente() auto-vincula al turno y cierra modal
- Al editar: actualiza labels en pantalla sin perder el turno activo
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BD: home_page asignado a roles que lo tenían NULL:
- recepcionista → /erp.php?m=turnero&v=recepcion
- supervisor → /lab_dashboard.php
- operador_bot → /index.php
- readonly → /lab_ordenes.php
login.php: fallback mejorado por módulos cuando home_page no aplica
(turnero → recepcion, lab_dashboard → dashboard, etc.)
index.php: redirige recepcionistas y roles sin whatsapp a su página
correcta si por algún motivo llegan al panel de WhatsApp.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Las 8 estaciones de muestras ya comparten grupo_id=1, así que se
agrupan en una sola opción 'Toma de Muestras'. Pediatría y Ginecología
tienen grupo_id=NULL y aparecen como destinos independientes.
Cualquier estación del grupo ve los turnos asignados a él.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El dropdown solo mostraba el primer lugar tipo muestras (LIMIT 1).
Ahora carga todos los lugares activos tipo muestras, incluyendo
Pediatría y Ginecología que ya estaban en la BD.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cuando la recepcionista seleccionaba un turno de la cola sin llamarlo
formalmente (via seleccionarTurno), el turno quedaba en estado espera
en BD. Al intentar pasar a lugar, espera→en_espera_lugar no es válido.
Fix: en create_solicitud.php, si el turno está en espera al guardar
la solicitud, se promueve a en_recepcion en el mismo transaction,
preservando timestamps existentes con COALESCE.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Al mover el canvas al esquema se eliminó la variable widget del JS,
pero el success handler seguía haciendo widget.outerHTML → ReferenceError
→ caía al catch y mostraba error de conexión aunque la firma se guardó.
Fix: envolver el canvas en div#turneroFirmaWidget y restaurar widget
en el JS para que el reemplazo al éxito funcione correctamente.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Agregado selector de escritorio al abrir recepcion.php sin desk_id.
Si hay múltiples escritorios muestra un modal para elegir; si hay uno
solo se asigna automáticamente. Así recepcion_desk_id siempre queda
registrado en el turno y aparece correctamente en el historial.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
La firma global (firma_svg) se aplicaba a todos los campos tipo=firma.
Ahora la firma global solo se asigna al primer campo firma del esquema.
Los campos firma adicionales (ej. desistimiento) solo muestran su propia
firma por campo; si no tienen, se omiten sin mostrar el canvas extra.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El JS buscaba #turneroFirmaWidget para arrancar; como ese div ya no
se renderiza cuando el esquema tiene campo firma propio, el script
salía sin enlazar eventos al canvas. Ahora busca el canvas directamente.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
La firma del profesional se maneja desde el modal de recepción.
El canvas no debe aparecer cuando el paciente abre el consentimiento.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El campo tipo=firma se saltaba cuando no había firma existente, dejando
el canvas al fondo de la página fuera de lugar. Ahora en modoTurnero
pendiente el canvas aparece donde el esquema lo define. El widget del
fondo es solo fallback cuando el esquema no tiene campo firma propio.
Los campos siguen siendo editables como corresponde.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
En modo turnero el documento debe ser de solo lectura; solo la firma
del paciente es interactiva. Los campos editables (inputs, selects,
checkboxes) solo aplican para envíos normales sin modo turnero.
También inicializa $_renderedFirmaPaciente que se usaba sin definir,
evitando que la firma aparezca duplicada al pie del documento.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El llamados.unshift no estaba protegido por callKey, así que el mismo
turno se agregaba una vez por cada escritorio de recepción. Ahora tanto
el anuncio como la entrada en la lista usan el mismo guard callKey.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cuando recepcion_desk_id es NULL, el mismo turno aparecía en los 3
escritorios de recepción con claves distintas (incluían el nombre del
escritorio). Eso generaba 3 entradas nuevas en lastSeenKeys → 3 anuncios.
Fix: añadir announcedCalls Set con clave id|llamado_at (sin destino).
El sonido/anuncio solo se encola una vez por evento de llamada,
independientemente de cuántos escritorios devuelvan el mismo turno.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dos causas:
1. sse_turno.php inicializaba \$lastPing = null, haciendo que la primera
iteración del loop (2s después del emit inicial) siempre emitiera
un snapshot extra aunque no hubiera cambios reales.
Fix: inicializar \$lastPing con el sse_ping_at actual de la BD.
2. display.php comparaba activo.codigo para detectar nueva llamada.
Si el poll y el SSE llegaban casi simultáneamente antes de que
lastCodigoActivo se actualizara, ambos disparaban el sonido.
Fix: comparar llamado_at en vez del código — el timestamp es
único por llamada, por lo que sonidos duplicados son imposibles.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
verificar.php:
- Pública, sin autenticación ni menú lateral
- Diseño moderno: card glassmorphism sobre gradiente brand
- Flujo en 3 pasos con indicador animado
- Paso 1: ingresa cédula → busca paciente
- Paso 2: inputs OTP de 6 dígitos con navegación automática
y pegado desde portapapeles; cuenta regresiva + reenvío a los 60s
- Paso 3: pantalla de éxito con animación
api/public/send_otp.php:
- Busca paciente por cédula, genera OTP aleatorio de 6 dígitos
- Guarda en lab_otp_tokens con expiración 5 min
- Envía SMS via WEBSms usando config de lab_config
- Retorna teléfono enmascarado (3**1234**67)
api/public/verify_otp.php:
- Valida cedula+codigo contra lab_otp_tokens activos
- Marca OTP como usado y paciente como verificado
(telefono_verificado=1, verificado_at=NOW())
DB:
- lab_pacientes: +telefono_verificado, +verificado_at
- lab_otp_tokens: tabla nueva para almacenar OTPs temporales
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El filtro brightness(0) invert(1) hacía invisible logos con fondo claro.
Ahora el logo se muestra sobre una píldora blanca semitransparente,
visible independientemente de los colores del logo.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- item-row con flex-wrap para que los tags pasen a la siguiente línea
- consent-tag con word-break y max-width para textos largos
- Botones de acción agrupados en .item-actions con margin-left:auto
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Logo:
- Carga doc_logo_base64 y empresa_nombre desde lab_config
- Header muestra logo (filtro invert para fondo oscuro) o ícono fallback
- Nombre del laboratorio siempre visible debajo, nunca cambia por página
- Ícono de configuración reposicionado en esquina superior derecha
Colapso de módulos:
- Bug: transición CSS se disparaba durante init causando estado inconsistente
- Fix: inyecta <style> temporal con transition:none antes de aplicar estados,
lo remueve en el siguiente frame para que las animaciones funcionen luego
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Header con avatar de iniciales + gradiente brand + subtítulo
- Secciones agrupadas en cards con fondo sutil y borde brand
- Todos los campos usan form-floating (labels animados)
- Footer con botones mejor definidos y gradiente brand
- Avatar se actualiza en tiempo real al escribir el nombre
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Builder:
- Picker de valor ahora es <select multiple> — Ctrl+clic para varios
- Al re-editar un separador, los valores guardados quedan preseleccionados
- aplicarCampo() guarda siempre como condicion.valores (array)
- Badge en canvas muestra todos los valores separados por coma
form_cliente.php / iniciarCondiciones():
- Soporta tanto .valor (string legacy) como .valores (array nuevo)
- Evaluación: activo si CUALQUIER valor de la lista está marcado
BD (ya aplicado):
- Formulario id=15 actualizado con condiciones en los separadores
- Taukit → valor único; Heliprobe → valor único
- Glicemia/Prolactina/etc. → valores[] con todos los prolongados
- Cortisol AM/PM → valores[] para sección de tomas hora fija
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Nueva sección WEBSms en lab_configuracion.php con:
- Switch activo/inactivo (sms_activo)
- Campo URL base de la API (sms_url)
- Campo API key / Bearer token (sms_api_key)
- Campos de prueba: número + mensaje + botón Enviar
- api/lab/test_sms.php: proxy al endpoint POST /api/sms/send
con Authorization: Bearer, valida respuesta ok/error
- save_config.php: permite guardar sms_url, sms_api_key, sms_activo
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Builder:
- Separadores tienen nuevo panel "Visible solo si [campo] = [valor]"
- Selector de campo (checkbox/radio/select del mismo form)
- Selector de valor se actualiza dinámicamente al elegir el campo
- Badge visual en el canvas muestra la condición configurada
- Condición se guarda en campo.condicion { campo_id, valor }
form_cliente.php:
- iniciarCondiciones() evalúa condiciones al cargar y al cambiar campos
- Secciones sin la opción marcada se ocultan (max-height:0, pointer-events:none)
- Se muestran con transición suave al marcar la opción correspondiente
- Cada campo tiene data-campo-id para que la lógica pueda localizarlo
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Toggle activo/inactivo en configuración → guarda ia_activa en lab_config
- Botón "Probar" llama api/lab/test_gemini.php con la clave actual
- test_gemini.php envía pregunta mínima a Gemini y confirma respuesta
- Dashboard oculta botón y panel IA cuando ia_activa != '1'
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Campo N.° Orden editable en la ficha, antes de seleccionar paciente
- Se carga automáticamente al abrir un turno (YYYYMMDD-001, 002…)
- Botón para regenerar el siguiente consecutivo
- Columna numero_orden en turnero_solicitudes (VARCHAR 20)
- Nuevo endpoint get_consecutivo.php calcula el siguiente del día
- create_solicitud.php guarda el numero_orden junto con la solicitud
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Icono de engranaje (⚙) en cabecera del sidebar → lab_configuracion.php
- Buscador de texto que filtra ítems del menú en tiempo real
- Al borrar la búsqueda restaura el estado colapsado guardado
- CSS integrado en el mismo componente
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Columnas añadidas: Recepción (desk + recepcionista), Lugar/Bacteriólogo, Exámenes
- Elimina columnas Lugar y Entrada que eran menos informativas
- colspan actualizado a 10
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>