Commit Graph
1180 Commits
Author SHA1 Message Date
Lizandro GuarnizoandClaude Opus 5 2b1f08b03f Evitar teléfonos falsos en la historia clínica, y pedir el número a quien lo oculta
El arreglo anterior dejó un defecto: obtenerOCrearDesdeWhatsapp() copiaba
users.phone_number al teléfono de la ficha, y para quien oculta su número ahí
va el BSUID. normalizarTelefono() le quitaba el punto y las letras, así que
"CO.1761088155094242" quedaba guardado como "1761088155094242": un número de
16 dígitos, falso, con apariencia de real, dentro de una historia clínica y
sin que nadie lo notara. Ahora esa ficha se crea sin teléfono, que es la
verdad: no lo tenemos.

- esBsuid() queda definido una sola vez, en config.php, que es donde lo ven
  tanto el servicio de WhatsApp como las clases del laboratorio.
- La lista y el detalle de pacientes dicen "Solo por WhatsApp" en vez de
  mostrar el identificador crudo: recepción necesita entender por qué no
  puede llamar, no ver un código.
- Al crear la ficha desde una conversación se le pide el número con el botón
  de Meta, una sola vez (users.contacto_pedido_at). El texto es editable
  desde configuración. Solo se marca como pedido si Meta aceptó el envío,
  para poder reintentar si falló.
- tests/test_bsuid.php: 30 verificaciones sobre identidad, envío, vinculación
  y creación de fichas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 20:26:31 -05:00
Lizandro GuarnizoandClaude Opus 5 26025a6399 Reconocer por BSUID a quien oculta su teléfono, y poder pedírselo
Meta no permite averiguar el teléfono a partir del BSUID: no hay endpoint de
consulta inversa, cada empresa debe llevar su propia equivalencia. Pero el
BSUID llega en TODOS los webhooks de mensaje, también en los que aún traen
teléfono, así que la equivalencia se puede ir guardando sola mientras la
persona todavía muestra su número.

- users.bsuid guarda esa equivalencia, y el webhook la anota en cada mensaje.
  Cuando alguien oculte su número, se le seguirá reconociendo y respondiendo
  a su teléfono de siempre.
- Va en columna aparte y no en phone_number porque el teléfono además cruza
  con el paciente y el turnero; mezclarlos rompería esos cruces.
- Para quien nunca escribió mostrando el número queda pedirle el contacto:
  pedirContacto() manda el botón request_contact_info, y el webhook atiende
  el mensaje `contacts` que llega si acepta.
- Al vincular puede aparecer un segundo registro de la misma persona. No se
  fusionan: una fusión mal hecha mezcla dos historias clínicas. Gana el que
  tiene el teléfono, el otro queda anotado en el log para revisarlo a mano.
- scripts/backfill_bsuid.php carga las equivalencias del histórico (6.423),
  con --simular para verlas sin escribir.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 20:07:52 -05:00
Lizandro GuarnizoandClaude Opus 5 92179af56a Atender a quien oculta su teléfono en WhatsApp (BSUID)
Meta desplegó los nombres de usuario: quien oculta su teléfono llega al
webhook sin `from` ni `wa_id`, identificado solo por su BSUID con la forma
"CO.1761088155094242". El webhook exigía teléfono y descartaba el mensaje
en silencio, sin guardarlo ni dejar rastro: 221 mensajes de 65 personas
ignorados por completo, ya el 3-6% de lo que entra cada día y subiendo.

- webhook.php lee `from_user_id` cuando no llega el teléfono, y rescata el
  nombre del contacto indexando también por `user_id`.
- WhatsAppService responde por BSUID: Meta exige el campo `recipient` en
  lugar de `to`. Se traduce en sendMessage(), el punto por donde pasan todos
  los envíos, en vez de en los diez métodos que arman payloads.
- formatPhoneNumber() devuelve el BSUID intacto; antes le quitaba el punto
  y las letras y lo dejaba en un destinatario inexistente.
- Las cuatro columnas del flujo pasan a varchar(32): los BSUID de Meta
  llegan a 23 caracteres y se truncaban en silencio en varchar(20).

De estas personas no tendremos el teléfono; si un flujo lo necesita, hay
que identificarlas por documento.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:41:10 -05:00
Lizandro GuarnizoandClaude Opus 5 381c2ca04e Rol Calidad incluye también formularios
La coordinación del SIG administra los documentos del sistema además de revisar
los tiempos del proceso, así que el rol suma lab_formularios en solo lectura.
Permite consolidar en una sola cuenta a quien antes necesitaba dos accesos.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:42:57 -05:00
Lizandro GuarnizoandClaude Opus 5 3a7b267f65 Rol de Calidad, y restricción por rol dentro del turnero
Rol nuevo para consultar turnos y tiempos de atención, de solo lectura y con
acceso únicamente a dashboard e historial. Se le permite exportar el historial,
que es la forma de analizar tiempos fuera del sistema.

Al crearlo salió que las vistas del turnero no verificaban nada por su cuenta:
el control del ERP es por módulo, así que cualquiera con acceso al turnero podía
abrir Configuración —lugares, dispositivos, plantillas— escribiendo la URL,
aunque el menú no se la mostrara. Aplicaba a todos los roles, no solo al nuevo.

Se agrega _acceso.php con una verificación de rol que usan las tres pantallas
que operan sobre la atención o cambian configuración. Verificado que cada rol
conserva lo que ya usaba: recepcionista entra a recepción, bacteriólogo a las
estaciones, supervisor y administradores a todo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 10:50:24 -05:00
Lizandro GuarnizoandClaude Opus 5 dc17c6a469 Permitir atender Pediatría y Ginecología desde equipos con estación fija
Un equipo registrado por IP o token queda atado a su estación: si pedía otra,
lugar.php lo devolvía a la suya sin mostrar nada, y parecía que el enlace estaba
roto. La restricción tiene sentido para las Tomas de Muestras, pero Pediatría y
Ginecología se atienden desde cualquier puesto.

Se agrega la marca acceso_libre en turnero_lugares en vez de codificar los
nombres, para que mañana se habilite otra estación sin tocar código. Las
marcadas así no redirigen y aparecen en el menú del equipo junto a la suya.

El bloqueo se mantiene en todo lo demás: el equipo sigue atado a su estación, el
botón de cambiar sigue oculto y no puede saltar a una Toma de Muestras ajena.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 10:47:11 -05:00
Lizandro GuarnizoandClaude Opus 5 813b712df0 Motivo al marcar ausente, y notas visibles con su origen en dashboard e historial
Marcar ausente solo pedía confirmación: el turno quedaba registrado sin ninguna
explicación, y hoy son 6 en un día. Ahora se pide el motivo y se guarda como
comentario de recepción, no como columna nueva: así aparece en todas las
pantallas que ya muestran comentarios, sin duplicar el dato ni tocar el esquema.

Dashboard e historial mostraban las notas sin indicar de dónde venían, así que no
se distinguía una observación de toma de muestras de una de recepción. Se agrega
la insignia de origen con los mismos colores que ya usa la bandeja, y el título
pasa a "Notas y observaciones" con su cantidad.

El historial además traía solo las 3 últimas por turno y en orden inverso. Con
recepción empezando a dejar notas eso habría escondido justamente el motivo de
una ausencia sin que nada indicara que faltaban; ahora trae hasta 20, en orden
cronológico.

De las 114 notas existentes, todas son de toma de muestras: recepción nunca dejó
ninguna porque no tenía dónde hacerlo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 10:37:33 -05:00
Lizandro GuarnizoandClaude Opus 5 4bc22bd00c Pantalla TV: el llamado por voz salía entrecortado y con retraso
El troceo lo causaba el keepalive del sintetizador: corría cada 10 segundos sin
verificar si estaba hablando, y pause() a mitad de frase la corta. Como un
anuncio con nombre y destino supera los 10 segundos, lo alcanzaba siempre. Ahora
solo actúa con el sintetizador en reposo, que es para lo que existe: evitar que
Chrome lo suspenda tras un rato sin uso.

El cartel se cerraba a los 5 segundos aunque la voz siguiera sonando, y al
cerrarse pasaba al siguiente de la cola, que cancelaba el anuncio en curso. Si
dos pacientes se llamaban seguido, el primero quedaba a media frase. Ahora se
cierra cuando terminó de hablar y se cumplió el mínimo visual, con una red de
seguridad por si el navegador no emite el evento de fin.

Sobre la demora: se quita la espera fija de 300 ms antes de hablar —solo se
mantiene, y reducida, cuando de verdad hay que cancelar algo en curso— y el
sondeo baja de 2 s a 1 s. El retraso desde el clic pasa de 0,3–2,3 s a 0–1 s.

La velocidad sube de 0.85 a 0.95: acorta el anuncio sin perder claridad.

Validado con una simulación del reloj y del sintetizador: reproduce el corte
con el código anterior y no ocurre con el nuevo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 10:05:56 -05:00
Lizandro GuarnizoandClaude Opus 5 c00faa2b06 Corregir la ficha del paciente desde el propio formulario
El acceso quedaba solo en la tarjeta del paciente de la estación: al ver un dato
mal mientras se llenaba el formulario había que cerrarlo, buscar el botón y
volver a abrirlo. Ahora los datos del paciente dentro del formulario llevan un
lápiz que abre el mismo modal, ya en modo edición, sin cerrar lo que se está
llenando.

El formulario va embebido, así que no puede abrir el modal por su cuenta: le
avisa al contenedor por el mismo mecanismo que ya usa para informar una firma.
El modal queda por encima del formulario (z-index 9500 contra 9000).

De paso se corrige un error del commit anterior: el refresco tras guardar
buscaba el iframe con un selector equivocado —el modal es modal-consentimiento,
no modal-consent—, así que el formulario nunca tomaba el dato corregido sin
recargar a mano. Ahora se recargan los dos iframes posibles por su id real.

El lápiz aparece una sola vez, en el primer campo vinculado, y no se imprime.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 09:55:54 -05:00
Lizandro GuarnizoandClaude Opus 5 a6bf35cb58 Corregir secciones condicionales que dejaron de aparecer, y habilitar la obstétrica
Regresión introducida hoy en 8948ea6: al ocultar con display:none los campos de
secciones cuya condición no se cumple, dejaron de aparecer al marcar la opción
que las activa. El evaluador de condiciones anima con max-height y opacity pero
nunca tocaba display, así que el campo quedaba invisible pese a "mostrarse".
Afectaba a Tipo de dolor, Tipo de cáncer, Grupo sanguíneo y Otro antecedente
familiar de F-LAB-08.

Al revisarlo apareció un problema anterior en DATOS OBSTÉTRICOS, que depende del
sexo del paciente: `genero` nunca se entregó como clave vinculable, así que la
condición no podía evaluarse ni en PHP ni en el navegador. Antes esos campos
salían siempre visibles —también para hombres— porque los campos de secciones
ocultas se renderizaban igual; al corregir eso quedaban inaccesibles.

Se expone `genero`, y tanto el evaluador de PHP como el del navegador resuelven
condiciones que dependen de un campo vinculado: en PHP leyendo la ficha del
paciente, en el navegador leyendo el valor mostrado.

Validado con node reproduciendo la falla y las siete combinaciones de las cinco
secciones condicionales, incluidas paciente mujer y hombre.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 09:10:20 -05:00
Lizandro GuarnizoandClaude Opus 5 57818b180a Estación de muestras: corregir la ficha del paciente desde el turno
Los campos del formulario F-LAB-08 son vinculados: leen de la ficha del
paciente. Si el dato está mal, no había forma de arreglarlo desde la estación
—el modal era de solo lectura— y corregirlo únicamente en el documento habría
dejado la ficha equivocada, reapareciendo en el próximo turno, orden o
consentimiento. Por eso se corrige la ficha y no el documento: el dato queda
bien en todo el sistema y el formulario, al ser automático, lo toma solo.

La edición tiene dos niveles de fricción. Teléfono, dirección y EPS se editan
directo: cambian seguido y el error es de bajo riesgo. Nombre, tipo y número
de documento y fecha de nacimiento quedan tras un botón aparte con
confirmación, porque identifican al paciente en toda su historia clínica.

Si el turno ya tiene documentos firmados se advierte que corregir la ficha no
los modifica y que hay que emitirlos de nuevo, para que nadie espere que un
PDF ya firmado se actualice solo.

Paciente::actualizar registraba en la bitácora solo el valor nuevo, lo que
impedía reconstruir el anterior si la corrección resultaba equivocada —justo
cuando hace falta consultarlo. Ahora guarda antes y después, y únicamente de
los campos que realmente cambiaron.

La validación del servidor ya cubría nombre, celular, documento y correo, y la
cédula tiene índice único, así que no hizo falta agregar validación.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 09:05:48 -05:00
Lizandro GuarnizoandClaude Opus 5 fd46a86007 Campos vinculados: entregar la dirección del paciente y completar desde datos vivos
El consentimiento F-LAB-01 tiene un campo vinculado a `direccion`, pero esa
clave no estaba entre las que el sistema entrega, así que quedaba vacía. Como
en modo vista los campos vacíos no se imprimen, el campo desaparecía del
documento en vez de salir en blanco, y por eso pasó inadvertido.

Se agrega `direccion` a las tres consultas de paciente y a las claves
vinculables. La tiene cargada el 61% de los pacientes.

Además, en los envíos de formulario los campos vinculados leían solo de
datos_prefilled, que es una copia tomada al momento de enviar: una clave
agregada después nunca aparecía en documentos ya emitidos. Ahora se completa
con los datos vivos del paciente, sin pisar lo que la copia ya traía.

Aparte, al esquema del formulario 17 le faltaba el campo del número de
documento —solo pedía el tipo—, lo que se corrigió en la definición del
formulario. Los 706 consentimientos ya firmados no se modifican.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 08:07:42 -05:00
Lizandro GuarnizoandClaude Opus 5 5d4315fae7 Recepción: el contador de exámenes no se actualizaba en las cargas silenciosas
La insignia con la cantidad de exámenes solo se refrescaba desde los callbacks
onItemAdd/onItemRemove de TomSelect, y hay cuatro rutas que manipulan la
selección por código sin dispararlos:

- Importar desde RIPS usa addItem(id, true) —silencioso a propósito, para no
  recalcular precios en cada examen—, así que la insignia quedaba en cero
  aunque se hubieran cargado varios.
- El botón "Limpiar selección", toggleSoloMuestras y resetCheckboxes usan
  clear(), que internamente quita los ítems en modo silencioso: la insignia
  conservaba el número anterior.

Al revisarlo apareció un segundo efecto en toggleSoloMuestras: limpiaba los
exámenes sin recalcular, dejando el panel de precios mostrando exámenes que
ya no estaban seleccionados.

Se agrega limpiarExamenes(), que limpia y sincroniza contador y precios, y se
usa en las tres rutas de limpieza; la carga desde RIPS actualiza el contador
una vez al terminar, conservando el modo silencioso por examen.

Validado con node sobre un doble de TomSelect que replica el comportamiento
silencioso: los cuatro escenarios pasan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 07:59:31 -05:00
Lizandro GuarnizoandClaude Opus 5 9b5e144386 Exportación de la documentación a PDF mediante vista de impresión
Nueva vista que reúne los 32 documentos en un solo entregable maquetado para
imprimir: portada con los datos del laboratorio, índice numerado, y cada
capítulo y documento empezando en página nueva.

Se resolvió como vista y no como archivo generado para que el entregable
refleje siempre el estado actual, incluidos los inventarios técnicos que se
generan del código y de la base en cada consulta. No hay paso de regeneración
que alguien pueda olvidar.

El CSS de impresión evita cortes a mitad de tabla, bloque de código o cita,
repite el encabezado en tablas que abarcan varias páginas y oculta la barra de
acciones. Permite exportar todo o una sección puntual, y respeta la
visibilidad por rol: cada usuario exporta lo que puede leer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 08:31:22 -05:00
Lizandro GuarnizoandClaude Opus 5 bc318db129 Documentación: menús y respuestas del bot, y manual de domicilios
El análisis de completitud cruzando roles contra módulos y tablas contra
documentos encontró dos huecos reales.

Manual de domicilios: recepcionista, lab_recepcion y lab_readonly tienen acceso
a la pantalla administrativa de domicilios, y la única página sobre el tema era
la del portal del enfermero, restringida a enfermeros. Son pantallas distintas
—una ve todo, la otra solo lo propio— y ahora cada una tiene su guía. Los
enfermeros pasan también a ver el manual de formularios, que su rol habilita.

Menús y respuestas automáticas del bot: había cuatro tablas en uso que ninguna
página explicaba. El bot responde solo las preguntas frecuentes mediante
autoresponses (7 configuradas) y ofrece un menú interactivo de 21 opciones en
dos niveles, todo configurable desde la base sin desplegar código. También
quedan documentados el estado de conversación, los envíos masivos y las
encuestas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 12:04:27 -05:00
Lizandro GuarnizoandClaude Opus 5 74e219e2ae LIA consulta solo el manual de usuario, y descarta coincidencias irrelevantes
El asistente está para ayudar a usar el sistema, no a mantenerlo: la
documentación técnica, de arquitectura y de operación queda fuera de su
contexto incluso para administradores, que pueden leerla directamente en el
módulo. El filtrado por rol sigue aplicándose dentro del manual.

Al restringirlo apareció que la relevancia era débil: una pregunta sobre
respaldos devolvía Recepción y Kiosko porque palabras comunes como "paciente"
suman igual en todos los documentos. Ahora cada palabra pesa según en cuántos
documentos aparece, y se descartan los resultados que no superen un umbral
absoluto ni queden cerca del mejor. Una pregunta fuera del manual devuelve
cero fuentes y LIA lo dice, en vez de responder con lo que tenga a mano.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 12:00:04 -05:00
Lizandro GuarnizoandClaude Opus 5 9c2ef19510 Documentación: órdenes médicas, formularios y reportes; cobertura por rol completa
Al cruzar los roles con sus módulos aparecieron manuales que faltaban: había
roles con acceso a pantallas que ninguna página explicaba. lab_readonly tenía
órdenes médicas, formularios_readonly tenía el diseñador de formularios y
supervisor tenía reportes, y ninguno de los tres estaba documentado.

Manual: órdenes médicas (flujo de revisión y autorización, con el ayuno
señalado como el dato de mayor consecuencia si se transcribe mal), formularios
(tipos de campo, secciones condicionales, quién firma qué) y reportes (la
diferencia entre el dashboard del turnero y el módulo de reportes, que miran
cosas distintas y no son comparables).

Técnica: órdenes médicas con sus estados y trazabilidad, reportes,
configuración del laboratorio y dashboard — los cuatro módulos de la
generación anterior que quedaban sin página.

Ahora cada rol ve solo sus páginas: readonly ve 2, los roles operativos entre
3 y 5, supervisor 10 y los administradores las 30.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 11:35:38 -05:00
Lizandro GuarnizoandClaude Opus 5 fe96e5b60d Documentación en usted, y panel de LIA con el mismo diseño del dashboard
Toda la documentación pasa de voseo a tratamiento de usted, incluidos los
diagramas y los textos de la interfaz. Los prompts de ambos asistentes lo
piden explícitamente, para que las respuestas generadas también lo respeten.

El panel de LIA en la página de documentación adopta el diseño del dashboard
del turnero: mismo botón circular, mismo panel deslizante con encabezado en
degradado y logo LIA, mismos chips de atajo, burbujas y campo de entrada.
Los atajos se arman con los documentos que ese usuario puede ver.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 10:57:28 -05:00
Lizandro GuarnizoandClaude Opus 5 71c9dfd424 Documentación: diagramas, kiosko y chat, catálogos, respaldos y endpoints documentados
Diagramas en texto dentro de bloques de código, en vez de capturas: se editan
como texto, no pesan en el repositorio y no quedan desactualizados solos. Se
agregan el recorrido de un turno, quién firma cada consentimiento, el paso de
una muestra pendiente entre visitas y la línea de tiempo de las tomas seriadas.

Páginas nuevas del manual: kiosko y pantallas de TV (las dos que funcionan sin
nadie operándolas), y chat de WhatsApp, que explica por qué el bot deja de
responder cuando un operador toma la conversación y de dónde sale el límite de
24 horas.

Páginas técnicas nuevas: registro de exámenes, y los catálogos del laboratorio
agrupados en una sola página por compartir la misma forma. Operación suma
respaldos y recuperación, incluyendo qué datos históricos no son recuperables.

Los 10 endpoints que no tenían comentario de cabecera ahora lo tienen, así que
la tabla generada por {{endpoints}} queda completa: 83 de 83.

DOCUMENTACION_LAB.md y README_LAB.md quedan como puntero al módulo;
WEBHOOK_ENDPOINTS.md se elimina por estar ya migrado a la sección técnica.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 10:49:02 -05:00
Lizandro GuarnizoandClaude Opus 5 81c007c516 LIA responde sobre la documentación, filtrada por el rol de quien pregunta
DocIndex::contextoIA() selecciona los documentos relevantes a la pregunta y
devuelve su texto. Reutiliza el filtrado por rol ya existente: al contexto que
se envía al modelo solo entra lo que ese usuario podría leer por su cuenta en
el módulo Soporte, así el asistente no puede revelar contenido restringido.
Verificado: un recepcionista preguntando por permisos recibe solo el manual
básico, sin el SQL ni los detalles internos que sí recibe un administrador.

- services/GeminiService.php concentra la llamada a la API y la contabilidad
  del presupuesto, que antes vivía dentro de ai_chat.php y ahora comparten los
  dos asistentes.
- La LIA del turnero suma la documentación a su contexto operativo, así responde
  tanto "cuánto facturamos hoy" como "cómo marco un paciente ausente".
- modules/soporte/api/ai_docs.php es el asistente de la documentación: no accede
  a datos de pacientes ni de la operación, solo a los documentos visibles. A
  diferencia del anterior no exige acceso al turnero, así que lo puede usar
  cualquier usuario autenticado desde la página de documentación.
- La respuesta cita de qué documentos salió.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 10:41:40 -05:00
Lizandro GuarnizoandClaude Opus 5 dc27184051 Documentación: visibilidad por rol en cada documento y una sola entrada de menú
Un documento puede declarar qué roles lo ven mediante una cabecera al inicio
del archivo; sin ella hereda el permiso de su sección. Los administradores ven
todo. Se aplica tanto al índice como al acceso directo por URL, y una sección
que queda sin documentos visibles deja de mostrarse.

Con esto un recepcionista ve solo el manual de recepción, un bacteriólogo el de
toma de muestras y un enfermero el suyo, en vez del manual completo.

El módulo declaraba cinco enlaces que apuntaban todos a la misma vista, así que
al abrir una sección quedaban dos entradas marcadas como activas a la vez. Queda
una sola, "Documentación": la navegación por secciones ya vive dentro de la
página.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 10:37:33 -05:00
Lizandro GuarnizoandClaude Opus 5 32c209710c Documentación completa del proyecto en el módulo Soporte
21 documentos en cuatro secciones, escritos sobre el comportamiento real del
sistema —incluidos los casos que costaron diagnosticar esta semana.

Manual de usuario (visible para todos): primeros pasos, recepción, toma de
muestras, portal del enfermero y administración. Orientado a tareas concretas,
no a describir pantallas.

Documentación técnica: índice de módulos, turnero, formularios y firma digital,
WhatsApp y bot, domicilios, webhook (migrado de WEBHOOK_ENDPOINTS.md) e
inventario de endpoints.

Arquitectura: visión general, enrutamiento y registro de módulos, roles y
permisos, modelo de datos, integración con WhatsApp, y decisiones tomadas con
su deuda técnica asociada.

Operación: runbook de incidentes ordenado por síntoma, configuraciones críticas
—incluido qué vive en Meta y no en la base— y despliegue.

Se documentan explícitamente las trampas conocidas: role/role_id que hay que
mantener sincronizados, las columnas can_* que el control de acceso no lee, las
URL de plantilla que no se cambian desde el código, y las columnas históricas
que quedaron en NULL sin forma de recuperarlas.

README_DOCS.md apunta al módulo y explica cómo agregar páginas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 10:14:32 -05:00
Lizandro GuarnizoandClaude Opus 5 018fb13332 Módulo Soporte: visor de documentación con renderizado Markdown y buscador
Nuevo módulo `soporte` con la documentación del proyecto en cuatro secciones:
manual de usuario (visible para todos), y documentación técnica, arquitectura
y operación (solo administradores).

- Markdown.php: renderizador propio del subconjunto que usa la documentación
  (encabezados, listas anidadas, tablas, código, citas). Escapa todo el texto
  antes de aplicar formato, así que los .md no pueden inyectar HTML. Se
  prefirió un archivo auditable a incorporar una dependencia externa.
- DocIndex.php: descubre los .md, arma el árbol, resuelve acceso por sección
  y construye el índice del buscador.
- Generadores.php: expande marcadores {{modulos}}, {{endpoints}}, {{tablas}},
  {{roles}} y {{servicios}} leyendo el código y la base en cada carga, para
  que los inventarios no puedan quedar desactualizados.

Se registra en SYSTEM_MODULES y se concede a los 12 roles con permission=read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 10:07:06 -05:00
Lizandro GuarnizoandClaude Opus 5 8948ea67d1 Tomas Prolongadas: ocultar los campos junto a su sección y aplicar visibilidad al abrir
Al ver F-LAB-28 desde bandeja aparecían secciones que no correspondían al
examen del paciente (Insulina · Minuto 0/30/60 con sus firmas sueltas).
Tres causas:

1. Una sección condicional que no se cumple ocultaba solo su encabezado
   mediante CSS; sus campos (hora, observaciones, firma) se renderizaban
   visibles y sin título. Ahora los campos llevan data-sep-id y heredan el
   estado oculto de su sección.

2. _mpApplyExamVisibility solo corría al cambiar el examen o al usar el panel
   de configuración, nunca al cargar. Un formulario ya configurado abierto
   desde bandeja o historial quedaba como lo dejó el PHP. Se llama ahora en
   el DOMContentLoaded existente.

3. _mpGetExamName usaba querySelector y devolvía solo el primer examen
   marcado, así que un paciente con Glicemia y Prolactina perdía las tomas
   del segundo. Se agrega _mpGetExamNames (todos) y la visibilidad acepta
   una lista; se corrige también la ruta de restauración desde sessionStorage,
   que restauraba varios exámenes pero aplicaba visibilidad con uno.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 09:51:11 -05:00
Lizandro GuarnizoandClaude Opus 5 d79d33db36 Facturación del día: finalizados vs en proceso, en vez de extrapolación lineal
La proyección estimaba el cierre del día multiplicando el ritmo promedio por
las horas que faltaban hasta un cierre fijo a las 17:00. Eso sobreestimaba
(el laboratorio concentra las tomas en la mañana por el ayuno), dependía de
una hora de cierre escrita a mano, y en fechas pasadas usaba date('G') —la
hora actual real— así que proyectaba facturación futura sobre días ya
cerrados.

Ahora son dos cifras concretas, sin estimar: Facturado = turnos finalizados;
En proceso = turnos aún activos, cuyo total_cobrado ya está registrado pero
no han cerrado. Ausentes y cancelados quedan fuera de ambos. La gráfica por
hora usa la misma separación.

Hoy: $22.779.145 en 92 finalizados + $667.000 en 2 en proceso (antes
proyectaba $28.740.490).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 15:08:28 -05:00
Lizandro GuarnizoandClaude Opus 5 d448ccc707 Registrar en migrations los cambios de esquema aplicados hoy
Los cambios del 2026-08-03 (admin_users.cedula, turnero_consentimientos.creado_por,
turnero_muestras.recibida_en_turno_id y la tabla turnero_tv_media) se aplicaron
directo en producción y no quedaron en el repo, así que un entorno nuevo no
los tendría. Se registran como migración idempotente (IF NOT EXISTS + guardas
en el UPDATE/INSERT); verificada contra producción sin efectos.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 15:05:05 -05:00
Lizandro GuarnizoandClaude Opus 5 25a099c6ef LIA: historial de conversación y prompt que no fuerza brevedad
Cada pregunta se enviaba sola, sin los intercambios previos, así que un
seguimiento del tipo "¿y de esos cuántos en la mañana?" no tenía contexto.
Ahora el dashboard acumula los últimos 4 intercambios y los envía; el
backend los valida y antepone a la pregunta actual (recortados para no
inflar el consumo).

El prompt pedía "responde de forma concisa" en todos los casos, lo que
sumado al tope de tokens producía respuestas cortadas. Ahora ajusta el
detalle a lo que pide la pregunta y sabe que tiene historial disponible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 15:03:43 -05:00
Lizandro GuarnizoandClaude Opus 5 b1a8b1abf8 LIA: subir maxOutputTokens a 2048 y avisar cuando Gemini corta la respuesta
El límite de salida estaba en 600 tokens (~450 palabras), que truncaba a
media frase las respuestas largas — listados de turnos del día, comparativas
por bacteriólogo. Gemini Flash admite hasta 8192; 2048 cubre esos casos sin
inflar el consumo del presupuesto.

Además el código nunca leía finishReason, así que una respuesta cortada por
MAX_TOKENS llegaba al usuario indistinguible de una completa. Ahora se marca
en el texto y se expone el flag `truncada` en la respuesta JSON.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:58:45 -05:00
Lizandro GuarnizoandClaude Opus 5 f6d4ee9bdd Capturar firmante de cada toma en el servidor; igualar render del modal en historial
guardar_toma.php (única ruta de firma de F-LAB-28 Tomas Prolongadas) nunca
registró quién firmaba: solo guardaba el SVG por campo. De los 111 registros
del formulario, 0 tienen identidad del firmante y no hay traza en ninguna
tabla de auditoría, así que no es recuperable retroactivamente. Ahora el
nombre/cédula se resuelven desde la sesión en el servidor (adminId()) y se
guardan por toma — cada toma puede firmarla un profesional distinto, y esta
es la única fuente confiable. El intento anterior dependía de que el
navegador enviara window._profNombre.

historial.php abría el modal sin compact=1&zoom=1.1, que sí usa bandeja;
por eso el mismo documento se veía en lista larga en vez de la grilla con
tarjetas de toma.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:55:37 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 e480e14974 Corregir fuga de tomas_detalle entre formularios del mismo turno
\$tomasDetalle solo se inicializaba dentro del if(\$esTomaProg), así que un
formulario que NO es de toma progresiva (ej. "Datos Toma de Muestras")
heredaba el arreglo del formulario anterior en el mismo turno (ej.
"Control de Tomas de Muestras Prolongadas") si ese sí lo era. Se reinicia
en cada vuelta del loop junto con tomas_total/tomas_firmadas.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-03 14:39:22 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 e8bf1febda Quitar respaldo al usuario de sesión actual al mostrar firmante de toma
El modal de documento (usado desde bandeja) mostraba el nombre de quien
estuviera viendo el documento cuando la toma no tenía guardado el firmante
real — inconsistente con historial, que ya mostraba "—" en ese caso. Ahora
ambas vistas coinciden: solo se muestra un nombre cuando quedó realmente
guardado por esa toma (desde el fix de guardar_toma.php); si no, ninguna
de las dos adivina.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-03 14:37:08 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 6254e45e8f Pantalla TV del turnero: playlist de videos e imágenes en vez de un solo video
Nueva tabla turnero_tv_media (tipo, url, orden, duracion_segundos) para
soportar múltiples videos/imágenes reproducidos en secuencia y en bucle.
El video que ya estaba cargado se migró como primer ítem de la playlist.

- save_tv_media.php / delete_tv_media.php / reorder_tv_media.php
  reemplazan a save_tv_video.php (eliminado).
- configuracion.php (tab TV): subida múltiple, lista reordenable por
  arrastre, duración editable por imagen.
- display_global.php: el <video> único se reemplaza por un reproductor
  JS que recorre la playlist — video hasta 'ended', imagen por su
  duración configurada, y vuelve a empezar al terminar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-03 14:33:05 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 2ba13add34 Corregir Tomas Prolongadas: filtrado de secciones, firmante por toma, detalle en historial
Tres bugs relacionados en el formulario "Control de Tomas de Muestras
Prolongadas" (F-LAB-28), detectados en el turno E017:

1. El modal de documentos del historial abría el documento sin &embed=1,
   por lo que el filtrado de tomas irrelevantes (según examen seleccionado)
   nunca se aplicaba — se mostraban las 16 secciones posibles en vez de
   solo las 2-3 que aplicaban, con textos "Pendiente de firma" de más.

2. El guardado de firma en tomas progresivas usa un endpoint distinto
   (_guardarFirmaMP → guardar_toma.php) que no pasaba por el fix de
   nombre/cédula del firmante aplicado antes solo a _guardarFirmaPro.
   Como además una toma progresiva puede pasar por varios profesionales
   en el transcurso de horas, ahora se guarda identidad por-toma
   (campoId + '_pro_nombre'/'_pro_cedula'), no solo global.

3. El historial no mostraba quién atendió cada toma. get_consentimientos.php
   ahora expone tomas_detalle (etiqueta, hora, firmante) por consentimiento
   de toma progresiva, y el panel de documentos del historial lo renderiza
   en una tabla debajo del pill del formulario.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-03 14:26:28 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 ec552136bf Incluir muestras rechazadas al volver, resaltar servicio prolongado en historial
Las consultas de "muestras pendientes de visita anterior" solo incluían
estado='pendiente'; ahora también 'rechazada', así una muestra rechazada
en una visita aparece igual cuando el paciente vuelve, y se puede recibir
directo (sin el paso extra de revertir a pendiente primero).

En historial.php, la columna Servicio se resalta en rojo con ícono de
alerta cuando supera 20 minutos (SERVICIO_PROLONGADO_MIN), para detectar
turnos prolongados a simple vista.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-03 14:19:35 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 538ee8ad25 Vincular turnos por muestra pendiente y precargar Datos Toma de Muestras
El turno original no se modifica (queda finalizado igual que antes) — solo
se agrega trazabilidad: turnero_muestras.recibida_en_turno_id registra en
qué turno se completó una muestra que había quedado pendiente en otra
visita. Historial, bandeja y lugar.php ahora muestran un chip/botón "Turno
vinculado" en ambos sentidos (origen ⇄ seguimiento) con acceso directo.

Además, al abrir un formulario "Datos Toma de Muestras" pendiente para una
visita de seguimiento, aparece un botón para cargar las respuestas del
último formulario firmado de ese mismo paciente (sin firma ni identidad
del profesional anterior) — evita reescribir la historia clínica; la
firma y fecha siguen siendo las de hoy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-03 13:10:59 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 8e1f46904c Registrar quién crea cada consentimiento del turnero para el pie "Enviado por"
turnero_consentimientos nunca guardó quién lo generó; el pie del documento
mostraba el nombre de quien tuviera sesión abierta al verlo, no de quien
realmente lo atendió (por eso siempre aparecía el mismo usuario). Se agrega
la columna creado_por, se llena en los 6 puntos donde se crea un registro
(recepción, envío por WhatsApp, anexar formulario extra, autocreación por
examen/lugar), y ver_formulario_enviado.php ahora prioriza ese dato real
sobre el usuario de la sesión actual. Los documentos ya firmados antes de
este cambio siguen sin ese dato y usan el fallback anterior.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-03 12:50:41 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 aacf26168b Toma de muestras: mostrar exámenes de la visita anterior en muestras pendientes
Cuando el paciente vuelve a completar una muestra pendiente, el panel de
lugar.php ahora lista los exámenes de la solicitud original debajo del
badge "visita anterior", con la misma clase visual que el motivo de
rechazo (hereda color según estado: pendiente/recibida/rechazada).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-03 12:41:28 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 fd8b83a588 Exigir Datos Toma de Muestras en solo_muestras; nombre+cédula del firmante en todos los formatos
get_consentimientos.php ya no salta la autocreación del formulario F-LAB-08
(Datos Toma de Muestras) cuando la visita es "solo entrega de muestras" —
solo se omitían los formularios por examen, los de lugar deben seguir
exigiéndose igual.

ver_formulario_enviado.php cargaba nombre/cédula del profesional firmante
solo dentro del turnero embebido; se amplía también a la firma directa
fuera de turnero (staff logueado). firmar_profesional.php (usado por los
formatos no-turnero) no recibía esos datos del frontend y se perdían;
ahora los guarda en datos_cliente igual que el flujo turnero.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-03 12:24:58 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 86aea30f62 Historial turnero: usar el formulario real de tomas prolongadas (F-LAB-28)
El desglose anterior se basaba en turnero_muestras, que no representa el
formulario "Control de Tomas de Muestras Prolongadas" que el laboratorio
ya usa para tomas seriadas (curvas de glicemia, etc.). Ahora el doc-pill
del historial muestra "X/Y tomas" usando es_toma_progresiva/tomas_firmadas
que get_consentimientos.php ya calculaba, sin duplicar lógica.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-03 11:57:27 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 0b8418ec76 Historial turnero: desglose de tomas de muestra y modal para documentos firmados
Muestra el detalle de cada toma (tipo, quien la recibió, tiempo) cuando un
turno tuvo varias, para explicar tiempos de servicio prolongados. Los
documentos firmados ahora se abren en un modal en vez de nueva pestaña,
preservando los filtros de búsqueda activos.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-03 11:44:31 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 397dc208a0 feat(roles): rol lab_recepcion tiene acceso a vista de recepcion turnero
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-31 19:38:42 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 006772f265 fix(encuesta): usar plantilla encuesta_turnero en lugar de encuesta
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 22:14:47 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 17a9f05005 fix(portal): agregar 'del Laboratorio Ximena Caicedo' en mensaje WhatsApp del enfermero
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 21:00:49 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 79a5438531 feat(domicilios): enfermero puede editar domicilios asignados desde el portal
- save_domicilio.php: permite editar si el enfermero está asignado (no solo si lo creó)
- enfermero_portal.php: botón Editar en domcard, método abrirEditar() pre-carga el modal existente

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 20:44:38 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 1efdc7bbad fix(portal): permitir superadmin ver portal enfermero y simular agenda
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 20:34:27 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 3fa92e8813 fix(LIA): cargar tokens reales desde PHP al abrir el panel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 20:21:07 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 e1738d3fa4 feat(LIA): botón minimizar visible con ícono — / ▲ en header
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 20:19:39 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 f2ce60137a fix(LIA): inicia con sonido silenciado por defecto
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 20:16:52 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 4010e6e6c6 fix(LIA): voz femenina en español y quitar emoji de saludo
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 20:12:18 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 320b47797a feat(LIA): barra de tokens 1M con bloqueo al agotarse
- ai_chat.php: cuenta tokens reales desde Gemini usageMetadata, persiste en lab_config, bloquea con HTTP 402 al agotar 1.000.000
- dashboard.php: _actualizarTokens() actualiza barra visual, _bloquearLIA() deshabilita input+mic y muestra aviso de soporte

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 20:09:18 -05:00