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>
- Cada sección del menú se puede abrir/cerrar con clic
- Estado persiste en localStorage por categoría
- La sección con el link activo siempre se auto-expande
- Chevron rota al colapsar con transición suave
- CSS en styles.css actualizado para que sidebar-section sea clickeable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- sidebar.php inyecta --brand/--brand-dark desde DB si no fueron definidos ya
por Layout.php, solucionando que lab_*.php mostraran el sidebar azul (fallback)
mientras las páginas ERP mostraban verde (#a0a59c del doc_color)
- Gradiente del sidebar invertido: ahora va de más oscuro (top) a brand (bottom)
en lugar de terminar con mezcla blanca que perdía las letras
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Si llega con desk_id redirige a la URL canónica erp.php?m=turnero&v=recepcion&desk_id=N
- Si llega sin desk_id muestra lista de turnero_lugares activos para elegir
- Elimina acceso directo sin contexto de desk
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ai_chat.php: contexto completo (tiempos, facturación, franjas, turnos detallados)
- Preguntas soportadas: demoras por recepcionista, total facturado, horas pico,
tiempo total por paciente, exámenes, búsqueda por cédula
- Panel flotante de chat en dashboard (botón robot azul)
- Búsqueda de tabla ahora filtra por cédula/documento
- Ícono de fila gira al expandir detalle (flecha → rotada)
- lab_configuracion.php: nueva sección IA con campo token Gemini
- save_config.php: permite guardar gemini_api_key
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Columnas nuevas en tabla principal:
- Recepción: módulo de desk + nombre del recepcionista
- Lugar / Bacteriólogo: lugar destino + quien atendió toma de muestras
- Exámenes: badges con código de cada examen solicitado
Los datos ya venían del API (get_historial.php), solo se exponen en la tabla.
Se elimina columna Teléfono y Entrada de la vista principal (siguen en detalle).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- table-hover, form-control:focus, btn-primary, menu-item hover
- Usar var(--brand, fallback) para que funcione sin Layout.php
- Contextos chat/WhatsApp mantienen su verde propio
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Eliminar comentarios {{-- --}} de Blade que se renderizaban como texto visible
- Inyectar --brand/--brand-dark desde doc_color de BD (igual que Layout.php)
- Modal header con gradiente brand igual que login/sidebar
- Secciones de formulario con separadores visuales (Datos, Contacto, Ubicación)
- Panel detalle con header brand en lugar de card-header blanco
- Actualizar version CSS a v=13
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Layout.php inyecta --brand y --brand-dark desde doc_color de BD
- Sidebar usa el mismo gradiente azul del login (brand-dark → brand)
- Círculos decorativos animados igual que el login
- Items activos: fondo blanco con texto brand (inverso al fondo)
- Hover: overlay blanco sutil + translateX(3px)
- Ancho unificado en --sidebar-width (270px)
- Breakpoint responsive consistente en 991.98px
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Eliminar .env del tracking de git (credenciales no deben ir en repo)
- Agregar .env y .env.* al .dockerignore (no hornear credenciales en imagen)
- Quitar extension=redis.so duplicada en php.ini
- Activar cache 7d para assets estáticos en nginx
- Corregir 5 migraciones: eliminar INSERT INTO migrations con columna incorrecta
y remover semicolons en comentarios -- que partian el parser SQL
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El servidor Redis vive fuera (recurso gestionado por Coolify); el app se
conecta vía REDIS_HOST. El servicio redis local era un contenedor muerto.
La extensión PHP phpredis se sigue compilando en la imagen (es el cliente).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
validate_timestamps=0 hacía que PHP-FPM sirviera código viejo para siempre;
el opcache_reset del entrypoint corre en CLI (OPcache separado) y no limpiaba
el de FPM. Ahora valida mtimes cada 2s — costo despreciable para un ERP.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dos despliegues que comparten el mismo Redis pisaban las sesiones.
session.save_path ahora incluye &database=${REDIS_DB} (default 0).
Despliegue secundario usa REDIS_DB=1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
create_consent_token.php ahora acepta paciente_id y lo vincula al turno
si aún no lo tiene, eliminando la carrera con vincular_paciente.php
(fire-and-forget). Ya no hace falta refrescar para firmar el consentimiento.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Escribir manualmente ya no dispara el turno: la persona oprime el botón.
El lector de cédula sí lo dispara porque mete los dígitos en ráfaga
(<35ms entre cada uno) o termina con Enter. Guard contra doble envío.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
La firma se guardaba en datos_cliente[campo_svg] pero el display
solo leía firma_profesional_svg (columna inexistente en lab_form_envios).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Kiosko: detector scanner (gap < 80ms), iconos 90px, fa-person-pregnant, ticket Arial
- Recepción: vincular_paciente.php para persistir paciente_id antes de firmar consentimiento
- Historial y dashboard: recepción desk, empleado recepción/muestras, exámenes y comentarios
- display_global: turno más pequeño, lugar-nombre más grande
- lab_pacientes: modal compacta con form-sm
- Configuración: icono embarazada fa-person-pregnant en prioridades
- ver_formulario_enviado: firma por campo (fix doble firma)
- docker-compose.local.yml: override BD producción para dev local
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>