Antes había que apuntarle a una flecha de pocos píxeles al final de la fila.
Ahora sirve toda la fila, como ya funciona el listado de pacientes.
Con dos resguardos, porque la fila lleva controles y datos que se copian:
no abre si el clic cayó sobre un botón o un enlace —el de cambiar estado
tiene lo suyo que hacer— ni si el usuario venía seleccionando texto, que en
esta pantalla es frecuente para copiar un nombre o un código.
La flecha se conserva: sigue siendo la señal visual de que la fila se abre.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La celda estaba limitada a 140px y llevaba una etiqueta por examen. Con 9,5
exámenes de promedio por turno —y 981 de 1.722 turnos por encima de seis, uno
con 94— cada fila ocupaba cinco renglones o más y la tabla quedaba ilegible.
Ahora la celda dice «9 exámenes» y la lista completa aparece al pasar el
cursor. Los nombres ya estaban en el detalle desplegable, así que no se pierde
información: se deja de repetir en un espacio donde no cabe.
Las exportaciones a Excel y CSV arman su propia lista y no cambian.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Las etiquetas eran tan cortas que se prestaban para entender otra cosa.
«Servicio prom. en lugar» y «Total prom. puerta a puerta» parecían medir lo
mismo, y no cuadraban entre sí: 16,3 de espera más 7,4 de servicio no dan los
36,6 del total.
Sí cuadran, pero el recorrido tiene cuatro tramos y el tablero solo mostraba
dos. Los otros dos —6,4 min dentro de recepción y 8,4 esperando el puesto—
suman casi quince minutos que no aparecían por ningún lado.
Cada casilla lleva ahora una línea que explica de dónde sale el número, y el
total dice explícitamente por qué es mayor que la suma de las anteriores. Las
etiquetas pasan a lenguaje llano: «Espera para recepción» en vez de «Espera
prom.», «No se presentaron» en vez de «Ausentes».
En la de tiempo en el puesto se aclara que los protocolos prolongados cuentan
hasta la primera toma, para que nadie lea ese número como si midiera lo que
dura una curva completa.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Una curva de glucosa dura dos horas, pero de esas el puesto solo trabaja unos
minutos al principio y otros al final: el resto el paciente está esperando el
examen. El promedio contaba esas horas como tiempo de atención.
Ahora, cuando el turno tiene un formulario de tomas prolongadas, el servicio
se corta en la primera toma (toma_inicio_at). Sin protocolo, nada cambia.
El efecto es grande y explica por qué los números no cuadraban:
promedio general 21,9 min → 7,4 min
Omaira Limas 50,9 min → 22,6 min
Luisa Gil 32,0 min → 7,9 min
Yesica Morales 23,5 min → 6,5 min
Lo importante no es que el número baje sino que era injusto: quien atendía
protocolos prolongados aparecía como la más lenta cuando estaba haciendo el
mismo trabajo que las demás. El promedio corregido coincide con el de los
turnos que nunca tuvieron protocolo (8,2 min), que es la comprobación de que
el cálculo ahora mide lo que dice medir.
Se aplica a los tres promedios: general, por puesto y por profesional. El
detalle de cada turno sigue mostrando el tiempo real transcurrido.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tres correcciones sobre lo reportado en E042.
1. La bitácora daba por hecho que firmado_at era del paciente, y lo anunciaba
así en documentos que el paciente nunca firma: el F-LAB-08 solo lo firma el
profesional y el F-LAB-28 lleva una firma por toma. Ahora mira qué firma hay
realmente guardada; si no hay ninguna dice «completado» o «protocolo
cerrado», según el caso.
2. Los protocolos prolongados desglosan cada toma con su hora y su firmante,
que es el dato que interesa: en E042 se ve que el minuto 0 lo firmó Yesica y
el minuto 150 Omaira. La firma general no distinguía eso.
3. firmar_profesional_consentimiento.php aceptaba el nombre del firmante que
mandaba el navegador, y ese valor se carga al abrir la página: si el
personal cambiaba de turno sin recargar, la firma final quedaba a nombre de
quien abrió el formulario. Ahora se resuelve desde la sesión en el servidor,
igual que ya hacía guardar_toma.php para cada toma.
Además, llamar y rellamar no dejaban rastro —el turno solo guarda la hora del
último llamado, que el rellamado sobrescribe—, así que la bitácora mostraba
«Llamado a X» sin autor. Ahora se auditan ambos.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La bitácora del turnero ya registra en lab_actividad_admin, pero los filtros
de esa pantalla solo listaban los módulos viejos: para ver un cambio de estado
o el retiro de un formulario había que buscarlo entre todo lo demás.
Se agregan los módulos turnero y turnero_config, y las cinco acciones nuevas.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
El F-LAB-08 muestra «Sexo» tomándolo de la ficha, pero el modal de edición no
tenía ese campo: si estaba mal, desde el puesto no había forma de corregirlo
pese a que el endpoint ya aceptaba el dato.
Va en la parte sin fricción, junto a EPS. Corregir un sexo mal digitado es
frecuente y no cambia de quién es la historia clínica, que es lo que protege
el bloqueo de identidad.
Las tres opciones corresponden al enum('M','F','O') de la columna.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
El botón sí traía las respuestas —comprobado con C028, que encuentra su visita
C014 y devuelve 12 campos—, pero los asignaba a mano con el.value y el.checked.
Eso no dispara ningún evento, y las secciones condicionales del F-LAB-08 (tipo
de dolor, tipo de cáncer, grupo sanguíneo) se despliegan escuchando 'change' e
'input'. Los datos entraban al formulario y las secciones seguían ocultas, así
que desde afuera parecía que el botón no hacía nada.
Ahora se disparan input y change en cada campo tocado, y el mensaje dice
cuántos datos se cargaron en vez de un texto fijo: si son cero, lo dice, en
lugar de afirmar que cargó.
Aparte: el botón «Corregir datos» del formulario quedó demasiado grande en el
cambio anterior. Vuelve a ser discreto —enlace subrayado de 10px— pero con
texto, que era lo que le faltaba para encontrarlo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
El botón existía pero era un lápiz gris de 11px sin texto ni contorno: en
siete días ningún bacteriólogo editó una ficha, lo que sugiere que no lo
encontraban. Ahora lleva la etiqueta «Corregir datos» y contorno, junto al
primer dato traído de la ficha —que en el F-LAB-08 es el nombre, al inicio
del bloque de datos del paciente.
Sigue apareciendo una sola vez y solo mientras el formulario no esté firmado:
no se debe alterar la ficha desde un documento ya firmado.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La bitácora del turno enlaza al chat del paciente con ?user_id=N, pero la
vista no leía ese parámetro: abría el chat en la lista general y había que
buscar la conversación a mano, que es justo lo que el enlace debía evitar.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Para saber quién retiró un formulario de un turno había que consultar la base
a mano. La información existía, pero repartida en cinco tablas y sin ninguna
pantalla que la juntara.
get_traza_turno.php arma una sola línea de tiempo con el recorrido del turno
(con quién atendió en cada punto), los documentos enviados y firmados, los
comentarios del personal —donde caen las ausencias con su motivo—, las
muestras recibidas y rechazadas, y las acciones administrativas auditadas.
Se muestra en el detalle desplegable de Historial y en el panel de Bandeja.
Del chat de WhatsApp solo se indica cuántos mensajes hubo ese día, con enlace
a la conversación. Volcar los mensajes sería esparcir datos personales del
paciente por una pantalla de consulta.
Y la parte que faltaba: cinco acciones no dejaban ningún rastro. Ahora se
auditan cambiar_estado, resetear_consentimiento, resetear_toma,
vincular_paciente y cancelar_toma_pendiente — las que borran una firma,
descartan datos de un protocolo o mandan la atención a otra historia clínica.
La auditoría va en try/catch: si falla, no tumba la operación.
La respuesta declara en `sinRastro` lo que el sistema todavía no registra,
para que la ausencia de un evento no se lea como prueba de que no ocurrió.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La miniatura era de 90x60 con recorte, así que el material cuadrado se veía
achatado en Configuración y no coincidía con lo que después salía al aire.
Ahora es cuadrada, igual que la pantalla.
La carga en sí no necesitaba cambios: no valida proporciones ni dimensiones,
solo formato y tamaño, así que un video cuadrado siempre entró bien.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
El cambio anterior hizo cuadrada la caja, pero la dejó dentro de la columna
fija del 29% —que es proporción de reel—, así que el cuadrado salía diminuto
y con medio espacio vertical vacío.
Ahora la columna se dimensiona sola (grid `auto`) y el cuadrado toma todo el
alto del cuerpo, deduciendo el ancho de ahí. Queda tan grande como quepa, con
tope de media pantalla para no ahogar la columna de llamados.
El aspect-ratio va en el elemento interno y no en el contenedor: con
box-sizing border-box, el padding asimétrico del contenedor deformaba la
proporción y el "cuadrado" no quedaba cuadrado.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La página sirve para averiguar qué voces tiene ESE equipo, así que hay que
abrirla en el televisor —donde no hay sesión iniciada, porque es un kiosco—.
Tal como quedó, redirigía al login y era inservible justo donde se necesita.
No expone nada: lista las voces que reporta el navegador y lee una frase de
ejemplo inventada. Ningún dato de paciente.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
El material del reel es cuadrado, pero la caja ocupaba todo el alto de una
columna angosta. Con object-fit:cover eso significa recortar: del cuadrado
solo quedaba visible una franja vertical del centro, y se perdían los bordes
—que es justo donde suele ir el logo o el texto de una pieza publicitaria.
Ahora la caja es cuadrada (aspect-ratio 1/1) y se centra en su columna, así
que la pieza entra completa. cover se conserva: sobre una caja cuadrada con
material cuadrado no recorta nada, y si algún día suben algo de otra
proporción, lo encuadra en vez de deformarlo.
Mismo arreglo en display.php, que tiene su propio panel de video por ?video=
con el mismo defecto.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Hasta ahora, para que el paciente firmara el consentimiento de bienvenida
había que girarle el monitor a la recepcionista o pasarle el mouse: el botón
Firmar abre el formulario en la pantalla de ella.
Se agrega una pantalla aparte (/erp.php?m=turnero&v=firma) para poner frente
al paciente. Sola, sin que la recepcionista haga nada: cuando el turno entra
a ese puesto y le falta el F-LAB-01, aparece su nombre y un botón grande de
firmar; al terminar dice gracias y vuelve a reposo.
Reutiliza lo que ya existía: el formulario de ver_formulario_enviado.php, el
aviso 'turneroFirmado' que ya emite al firmar, y el sondeo de recepción, que
seguirá poniendo el renglón en verde sin cambios.
Sobre el acceso: va en PUBLIC_ROUTES porque nadie va a iniciar sesión cada
mañana en una tablet que manipula el público. No queda abierta: se identifica
por la cookie del dispositivo, y sin ella no muestra ningún dato. El endpoint
devuelve solo el turno que está en ese puesto en ese instante —no permite
buscar, ni ver otros, ni consultar historial— y descarta turnos de días
anteriores, igual que el televisor. La tablet debe quedar en modo kiosco.
Falta registrar el dispositivo de Recepción 4 desde Configuración; hoy no
tiene ninguno.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reportan que a veces llama a otro paciente. Sí ocurre, y hay 26 turnos sin
cerrar en estado "en servicio" desde el 28 de julio, varios de ellos dos y
tres en el mismo puesto a la vez.
La pantalla pregunta por cada puesto cuál turno está en servicio y se queda
con el más reciente. Mientras hay uno real todo va bien, pero al cerrarse
ese, el puesto queda con el viejo como único en servicio y sube al panel como
si lo estuvieran llamando. Peor: al cargar, la pantalla anunciaba POR VOZ el
turno que encontrara arriba, así que cada recarga —y se recarga sola— podía
gritar el nombre de alguien que se fue hace días.
- Al abrir ya no se anuncia nada: los turnos presentes se dan por vistos y
solo se anuncia lo que pase de ahí en adelante. Mostrar el estado actual es
correcto; declamarlo como si fuera un llamado nuevo, no.
- Un turno vale hasta la medianoche de su día, tanto en recepción como en los
puestos. De los 26 atascados, 25 dejan de aparecer; queda el único de hoy.
El filtro es una red, no la solución: esos 26 turnos hay que cerrarlos, y
mientras existan siguen inflando el "en proceso" del dashboard.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cuando recepción marca "solo entrega de muestras", el puesto de toma no
mostraba ningún formulario. No era que no se generara: el backend ya lo
creaba por la vía del lugar —los ocho puestos de toma, Pediatría y
Ginecología tienen vinculado el formulario 16— y era la interfaz la que lo
escondía. En 30 días: 59 turnos de solo entrega, 34 con el F-LAB-08 ya
creado, 0 firmados.
Eran tres bloqueos, no uno:
- la sección de formularios se ocultaba por solo_muestras, y como los puestos
de toma van en modo embebido, esa sección es la única vía para abrirlo;
- no se cargaba el formulario embebido;
- ni se refrescaba la lista cada 5 s, así que al firmar no se habría visto
el cambio.
Se conserva la exención del bloqueo para finalizar (línea 1685): el
formulario aparece y se puede llenar, pero un turno de solo entrega no queda
atrapado si no se firma. Cerrar la cola por esto sería peor que el problema
que resuelve.
Estos turnos no traen exámenes, así que la lista solo puede contener los
formularios del puesto: no hay riesgo de que aparezcan consentimientos de
exámenes que no aplican.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dos defectos del mismo cambio de hoy, que explican lo que se oía.
La detección de "el navegador bloqueó el audio" daba 1,9 s de plazo y, si en
ese tiempo no había empezado a hablar, cancelaba y repetía con otra voz. Pero
una voz remota tarda en arrancar porque se baja de internet: el plazo la
declaraba muerta cuando iba a hablar, le cortaba la frase y la repetía con la
voz del navegador. De ahí que se oyera media frase y luego otra distinta, a
veces de hombre y a veces de mujer. Ahora se le pregunta al sintetizador si
está trabajando antes de darla por fallida, y el plazo sube a 3,2 s.
Y la voz se elegía al llegar el turno, no al hablar. La lista de voces la
carga el navegador de forma asíncrona, así que en los primeros llamados tras
abrir la página venía vacía y se hablaba con la voz por defecto. Ahora se
resuelve en el momento de hablar, cuando ya está cargada.
Comprobado: con voz instantánea, lenta (2 s) y muy lenta (4 s) ya no hay
corte y siempre habla Sabina; con el audio bloqueado se sigue avisando para
que el cartel no quede pegado.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Regresión del cambio anterior. Al arrancar con sonidoActivo=true,
anunciarTurno() devolvía el objeto de voz aunque el navegador estuviera
bloqueando el audio; el cartel esperaba el evento 'end', que en ese caso no
llega nunca, y quedaba fijo hasta la red de seguridad de 25 segundos. Antes
devolvía null y cerraba a los 5.
Ahora avisa por callback tanto al terminar de hablar como al quedar claro que
no va a hablar, detectado porque 'start' no ocurre. speak() no lanza nada ni
emite 'error' cuando lo bloquean: el silencio es la única señal.
Además, si la voz elegida no arranca —el equipo la lista pero no la puede
usar— se reintenta una vez con la del navegador en vez de quedarse mudo.
Comprobado con tres escenarios: equipo normal (cierra a 4,7 s), audio
bloqueado (3,1 s) y voz que no suena (5,9 s, hablando en el reintento).
Aparte: la expresión de mayúsculas usaba \w, que no cuenta letras acentuadas
y trataba la tilde como separador. "maría fernanda gómez" se pronunciaba
"MaríA Fernanda GóMez".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La voz es-CO de Chrome es de Google y se baja de internet en cada llamado:
con wifi flojo se corta. Una colombiana local no existe, así que se prefieren
las mexicanas del propio equipo —Sabina en Windows, Paulina en macOS—, que
para leer letras, números y un nombre propio se oyen naturales aquí.
El orden queda: las dos por nombre, luego cualquier local latinoamericana,
luego cualquier local aunque sea de España (peor acento, pero no se corta), y
la remota solo si el equipo no tiene ninguna instalada.
Comprobado contra cinco inventarios de voces: Windows con Sabina, macOS con
Paulina, solo España local, sin ninguna local, y sin voces en español.
voces.php replica el mismo orden para señalar cuál está sonando.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sonidoActivo empezaba en false y solo un clic lo encendía, así que cualquier
recarga —corte de red, reinicio, refresco— dejaba el televisor mudo hasta que
una persona fuera físicamente a tocarlo. Peor: con esa lógica ni siquiera
poniendo --autoplay-policy=no-user-gesture-required habría sonado, porque la
página se bloqueaba a sí misma antes de que el navegador opinara.
Ahora arranca encendido e intenta abrir el audio al cargar. Si el navegador
lo permite, habla sin que nadie intervenga. Si lo bloquea, se muestra el
letrero para que alguien toque una vez, que es el único camino que dejan las
políticas de reproducción automática.
El pito de confirmación queda solo para el clic humano: al arrancar sola, la
pantalla no tiene por qué pitar cada vez que se recarga. Y el letrero ya no
depende de localStorage sino de si el audio quedó realmente suspendido,
atado a onstatechange porque resume() es asíncrono y antes parpadeaba.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
El llamado se oía "entrecortado" por dos motivos distintos.
playBeep() y speak() salían en el mismo instante, y el pito dura 0,65 s con
volumen 0,35: "Turno A-01" se pronunciaba por debajo del tono y desde lejos
parecía que el anuncio empezaba ya empezado. Se vuelve a esperar los 700 ms
que el código tenía antes de que se quitaran.
El otro motivo es la voz. getVozES() pide es-CO, que en Chrome es la de
Google: remota. Cada llamado la baja de internet, así que con wifi flojo se
corta o no suena. Las voces disponibles las decide el televisor, no el ERP,
así que en vez de adivinar se agrega /erp.php?m=turnero&v=voces: lista las
voces del equipo, marca cuáles son locales y cuáles se bajan de internet,
señala la que está sonando hoy y deja oír cada una con el texto real de un
llamado. Hay que abrirla en el televisor.
Queda descartado el eco entre pantallas: hay un solo televisor con sonido.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
57 números aparecen con dos identificadores (línea reciclada, o la persona
se registró de nuevo). El recorrido iba por BSUID, así que en esos casos los
dos escribían sobre el mismo usuario y el que quedaba dependía del orden:
cada ejecución dejaba un valor distinto y ninguna era más correcta. De ahí
los "110 reemplazos", que no eran 110 personas sino 57 escribiéndose dos
veces.
Ahora el empate se resuelve en memoria antes de tocar la base —gana el visto
más tarde, que es el vigente— y se escribe una sola vez por teléfono. La
corrida siguiente da 0 cambios y 0 conflictos, que es como debe comportarse
algo idempotente.
Estado final: 5.607 usuarios con su BSUID, sin duplicados, y ningún registro
con el identificador guardado en el campo de teléfono.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La ejecución real falló con "MySQL server has gone away" tras escribir 4.522
de 5.663: leer los 228.887 registros tarda más de media hora, y para cuando
empezaba a escribir el servidor ya había cerrado esa conexión. El singleton
de Database no sabe reconectar.
Ahora la lectura guarda el mapa en disco y termina; la segunda ejecución lo
toma de la caché en un segundo y escribe con la conexión sana. Reintentar
sale gratis, que era el otro problema: cada intento costaba media hora.
También informa cuántos teléfonos tienen más de un BSUID. La simulación
decía cero conflictos, pero no podía detectarlos: como no escribe, el bsuid
del usuario siempre estaba vacío y la comparación nunca daba positivo. Al
aplicar de verdad aparecieron. Ahora se cuentan al leer, sin depender de
lo ya escrito.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Quedó fuera del commit anterior porque tests/ está en .gitignore, pese a
que el mensaje la mencionaba. Va en scripts/, que sí se versiona, junto al
backfill al que acompaña.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
\$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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
- 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>
- LIA: saludo de voz al abrir (1x/día), auto-escucha 3s, olas animadas
- Gráfica de barras (Chart.js) con facturación real + proyección lineal
- Exportar a Excel (.xls con HTML table, se abre directo en Excel)
- Botón "Informe WA": envía resumen del día por WhatsApp a número elegido
- Diseño accesible: texto más grande, botones claros para usuario adulto
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
API: agrega por_prioridad con último código llamado a muestras por letra.
Vista: badges con color de prioridad mostrando "A: A-023", "B: B-011", etc.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- API: agrega tipo_documento y numero_documento al SELECT
- Vista: línea 1 = nombre BD + CC/doc, línea 2 gris = Kiosko: nombre
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- APIs: total_min = creado_at → fin_lugar_at (solo finalizados)
- Dashboard: KPI card 'Total prom. puerta a puerta' + columna Total en tabla
- Historial: columna Total en tabla
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Usaba ['id'] sobre un int → sesionId=0 → sin resultados.
Cambiado a sesionHoy() que sí retorna array con 'id'.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- get_bandeja.php: contar estado='rechazada' (no 'pendiente')
- chip renombrado a "Muestras rechaz." con ícono fa-flask
- card muestra badge rojo con ícono y cantidad cuando hay rechazadas
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Elimina el lookup innecesario en users/$userId que nunca se usaba.
Ahora sigue el mismo patrón que recepción: BD telefono → paciente_cel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El paciente siempre tiene celular registrado; el error ocurría porque se
requería match en users. Ahora usa paciente_cel como fallback final.
Revierte la guarda del frontend del commit anterior.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Si turnoActivo.paciente_cel está vacío, _ofrecerEncuesta retorna sin
mostrar la barra, evitando el error de WA al intentar enviar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Si el nombre registrado en BD difiere del ingresado en kiosko, muestra
ambos: el BD como principal y el kiosko en gris debajo.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- esc() ahora escapa backticks (`) para evitar que nombres de paciente
con backticks rompan el template literal
- onclick simplificado: eliminado .replace(/'/g,\\'\\\\\\'\\'') que era JS
inválido (backslash fuera de string literal dentro de ${}); esc() ya
convierte comillas simples a '
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Al olvidar una toma los tiempos del ciclo anterior quedaban en la DB,
haciendo que al re-firmar la toma reseteada las demás aparecieran
inmediatamente (sin countdown) y el profesional tuviera que firmar 3
veces en vez de 1.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
bin2hex(random_bytes(16)) genera hex sin guiones que no pasa la validación
de ver_formulario_enviado.php — el formulario adicional daba token inválido
al intentar firmar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El timer de 1s solo escaneaba chips en la cola principal. Los pacientes
en espera entre extracciones (muestra_espera_at IS NOT NULL) viven en el
panel de espera sin chip, por lo que nunca sonaba. Se añade un chip oculto
con data-toma-at en cada fila del panel; el timer existente lo detecta y
dispara banner + sonido en todas las tablets del grupo.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Chip "Muestras pend." y nuevo "Prog. en espera" son toggles de filtro
- Se resaltan con borde al activarse; combinables entre sí y con el dropdown
- API retorna muestras_pendientes (turnos con ≥1 muestra pendiente) y
prolongadas_pendientes (toma progresiva en espera entre extracciones)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Inputs desde/hasta pasan al API y filtran por rango de fechas
- Buscador cubre nombre, número de orden, código y examen
- API usa fecha cuando se pasa, sesión actual como fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Añade conteo de muestras_pendientes por turno en la lista y opción
de filtro en el selector de estados de la bandeja del día.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Recepción ve en una barra inferior qué turno atiende cada estación de
toma de muestras (verde = ocupada, gris = libre), cuántos están en
espera y cuántos en espera entre muestras progresivas. Se refresca cada 7 s.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Al llamar un paciente de toma progresiva desde la espera, ahora se
actualiza lugar_destino_id al puesto que llama, se pone llamado_lugar_at
para que el display anuncie al paciente, y se notifica por SSE. El JS
abre la ficha y muestra el toast igual que un llamar normal.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Badge junto al título "Exámenes solicitados" que muestra cuántos
exámenes lleva el turno actual. Se actualiza en tiempo real al
agregar o quitar ítems del TomSelect.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
login.php y index.php enviaban a v=recepcion a cualquier usuario con el
módulo turnero que no fuera bacteriólogo, incluyendo superadmin, admin
y supervisor. Esto causaba que al cerrar sesión y volver a entrar, el
admin aterrizaba en recepción en vez del dashboard.
Ahora:
- bacteriologo → lugar_id=1
- superadmin/admin/supervisor → v=dashboard
- recepcionista/otros → v=recepcion (comportamiento previo correcto)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- llamar_turno.php: ambos SELECT de refresco hacen LEFT JOIN con
turnero_solicitudes para devolver COALESCE(ts.solo_muestras,0).
Sin este JOIN, JS recibía undefined y _esSoloEntrega() retornaba
false → polling de consentimientos arrancaba → hayPendientes=true
→ finalizar bloqueado (caso turno E047 ALVARO TIRADO).
- lugar.php renderConsentimientos: hayPendientes se fuerza a false
cuando _esSoloEntrega(turnoActivo) es true, para que un
consentimiento huérfano previo no bloquee la finalización en
turnos de solo entrega de muestras.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Las 3 subqueries por turno (examenes, consentimientos, comentarios)
× 50 turnos = 150 round-trips al servidor remoto (~83ms c/u) sumaban
12.5s de espera, causando timeouts o página sin carga.
Ahora se agrupan en 3 queries con IN(?) sobre todos los IDs del lote.
Tiempo total: ~357ms (35x más rápido).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. App::bacteDefaultUrl() ahora busca también por token cookie (antes
solo buscaba por IP), consistente con login.php y lugar.php.
2. cambiarEstadoTurno(): cuando el servidor devuelve 422 (transición
inválida porque otro operador cambió el estado), muestra mensaje
claro y refresca la UI en lugar de mostrar el error técnico bruto
"Transición 'en_recepcion' → 'finalizado' no está permitida".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
La detección de tablet fija solo buscaba por IP; tablets con token
configurado no hacían match y caían al redirect de rol (recepcion).
Ahora busca primero por cookie turnero_token y como fallback por IP.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El rol bacteriologo iba a v=dashboard al hacer login. dashboard.php solo
redirige automáticamente si la IP está registrada como dispositivo; sin
IP registrada, la bacterióloga se quedaba en el dashboard sin usar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Permite al operador agregar un consentimiento informado extra (fuera de
los asignados por defecto) mientras el turno está en servicio.
- PHP: consulta lab_formularios activos al cargar lugar.php
- JS: modal de selección con lista de formularios
- Botón visible solo cuando turno está en_servicio, oculto en resetFicha
- API: modules/turnero/api/anexar_formulario.php crea el consentimiento
y notifica SSE para que el dispositivo lo vea inmediatamente
- Fix: _olvidarConsentimiento usaba cargarConsentimientos() inexistente;
corregido a actualizarConsentimientos(turnoActivo.id)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- feat(turnero): Botón "Olvidar" en lugar.php y recepcion.php para descartar firma de consentimiento completo (nuevo endpoint resetear_consentimiento.php)
- feat(turnero): Botón "Olvidar" por toma individual en prolongadas para borrar firma+hora de una toma específica (nuevo endpoint resetear_toma.php)
- feat(turnero): Editar tiempos en tomas en_progreso: hora_campo signed muestra readonly con botón olvidar, permite re-editar tras olvidar
- feat(turnero): Guardar tiempos (draft save) también en estado en_progreso
- feat(turnero): Observaciones de tomas prolongadas se muestran en header de tarjeta firmada (campo obs_campo en _mpMap)
- fix(turnero): Token del equipo se sobreescribía en tabla — ip permitía solo un registro con ip='' (UNIQUE); ahora ip es nullable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- getVozES() prefiere voces locales (offline) sobre Microsoft Online que
fallan silenciosamente sin internet o con latencia
- cancel() antes de speak() para limpiar cola atascada
- keepalive sin check !speaking, cada 10s
- sonidoActivo persiste en localStorage; banner rojo si no activado
- AudioContext se resume si suspended antes de reproducir
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cuando se usaban rawComponents, el parámetro $meta no se agregaba
al payload, causando que los mensajes del turnero se guardaran con
canal='bot' y aparecieran en conversations.php.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bootstrap d-none usa display:none !important que gana sobre
modal.style.display = 'flex' del JS. Se quita la clase d-none;
el display:none del style inline es suficiente.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PHP 8.1+ throws TypeError when using an array as array key.
Old flat-format loop iterated nested { examName: { group: [fids] } }
and tried $allowed[$arrayValue] = true, causing a fatal error and
returning ok:false ("Error al guardar") on every toma signing attempt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- JS evaluar(): añade margin:0; padding:0 al ocultar elementos con maxHeight:0
para que .campo-edit no deje margen residual
- PHP render: baked-in separator oculto incluye margin:0;padding:0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DB: añade estado 'cerrado_anticipado' al enum de turnero_consentimientos
- ver_formulario_enviado.php: POST handler mp_cerrar_anticipado guarda motivo
en _cierre_anticipado y cambia estado; botón "Cerrar anticipadamente" con modal
textarea; bloque display para estado cerrado_anticipado mostrando el motivo;
modoEditar y check de estado actualizados para excluir cerrado_anticipado
- get_consentimientos.php: expone cierre_anticipado en respuesta; mueve parse
de datos_respuestas fuera del if(esTomaProg) para ambos loops
- lugar.php: CONSENT_IC/LBL incluye cerrado_anticipado; ya[] lo incluye;
hayPendientes lo excluye; fila muestra motivo en banner amarillo
- recepcion.php: CONSENT_META incluye cerrado_anticipado; ya[] lo incluye;
motivo visible en la tarjeta del consentimiento
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- configurar_tomas.php: almacena _tomas_config como { examName: { groupKey: [fids] } }
en vez de flat, detecta y resetea formato antiguo, valida exam_type requerido
- ver_formulario_enviado.php: rendering aplana nested→flat para filtrar secciones;
JS lee _mpTomasConfig[examName][gk] en inSaved y _mpRenderBadges; auto-open
verifica per-examen; _mpActiveConfigExam evita race entre exámenes simultáneos
- get_consentimientos.php: flatten de nested o flat para calcular allowedFids
en ambos loops de tomas progresivas
- F-LAB-08 (id=16): añade 'Grupo sanguíneo' a opciones de _m5n865g;
agrega campos condicionales '¿Qué tipo de cáncer?' y 'Especifique grupo sanguíneo'
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Al guardar config de un examen se sobreescribía _c8j2g16 con solo ese examen,
perdiendo los demás en el reload. Ahora se envía exam_types (array completo),
se guarda como array en datos_respuestas y sessionStorage restaura todos los
checkboxes marcados al recargar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- every en lugar de some: examen configurado solo cuando TODOS sus grupos
tienen config (no solo alguno compartido)
- inSaved sin requisito isModify: grupos compartidos muestran selección
previa al abrir panel de otro examen, evitando sobreescribir config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El fallback que abría el primer examen en modo editar cuando todos estaban
configurados causaba un loop infinito — el panel nunca se cerraba y el
usuario no podía llegar a firmar. Ahora solo se auto-abre si hay un examen
pendiente de configuración.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Antes solo aparecían pacientes con inicio_lugar_at IS NOT NULL (ya llamados).
Ahora incluye estado IN ('en_espera_lugar','en_servicio') para mostrar
pacientes enviados a ginecología/pediatría antes de ser llamados.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- configurar_tomas.php: merge config por examen en lugar de overwrite
- ver_formulario_enviado.php: auto-abrir panel de tiempos al cargar; soporte formularios solo_profesional con botón Guardar y completar
- get_consentimientos.php: leer campo solo_profesional de lab_formularios
- recepcion.php: formularios sin firma paciente muestran badge "Completa en laboratorio"
- lugar.php: botón "Completar" para formularios solo_profesional
DB: ALTER TABLE lab_formularios ADD solo_profesional; UPDATE Rubeola id=8; UPDATE descripcion Niños kiosko
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DB: columna muestra_espera_at en turnero_turnos
- Panel 'Esperando siguiente muestra' en la cola de lugar.php
· Muestra pacientes en espera con countdown a siguiente_toma_at
· Botón 'Llamar' los devuelve a la cola activa
- Botón 'Mandar a espera' en la ficha (visible cuando toma progresiva activa)
· Desaparece de la cola principal, queda en sección de espera
- get_cola.php: excluye muestra_espera_at IS NOT NULL de cola y activo
· Agrega array en_espera_muestra en la respuesta
- Nuevas APIs: poner_en_espera_muestra.php, llamar_desde_espera.php
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Sección en la parte superior de lugar.php muestra pacientes esperando
en Ginecología y Pediatría (inicio_lugar_at IS NOT NULL, fin_lugar_at IS NULL)
- Chips de acceso rápido (botones) al tope de la cola para navegar directo
- Contadores de espera actualizados cada 30s
- Clicking en un paciente o "Ir →" navega a esa vista completa para atenderlo
- Solo aparece si existen esos lugares en DB y hay pacientes esperando
- Nuevo: get_especialidades.php para consultar pacientes de lugares especiales
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Muestra que el paciente trae después no se muestra como rechazo.
Colores rojo → naranja, botón/etiqueta/íconos actualizados en lugar.php y bandeja.php.
DB/API internos sin cambio (enum 'rechazada' se mantiene).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Consentimientos abren en modal iframe (ver_formulario_enviado.php?embed=1)
- Igual que en toma de muestras: spinner de carga, click fuera para cerrar
- Badges de comentarios (Recep./Toma/Gral.) y estado (Finalizado/En servicio)
confirmados visibles en tarjeta
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Indicadores por tipo: Recep. (azul) / Toma (verde) / Gral. (amarillo)
- Label de estado del servicio: En servicio / En espera / Finalizado / etc.
- Query agrega SUM por tipo de comentario sin subquery adicional
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reemplaza SELECT MAX() (vulnerable a concurrencia) por UPDATE atómico sobre
turnero_consecutivos. Dos recepcionistas simultáneas nunca pueden recibir
el mismo número de orden. Sin UNIQUE constraint, sin riesgo en datos históricos.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reemplaza activos/cerrados por pendientes (por revisar) / vistos
- Columna bandeja_visto_at en turnero_turnos; API marcar_visto.php
- Botón "Marcar como visto" → mueve paciente a sección inferior
- Filtro en tiempo real por nombre y por código de examen (GROUP_CONCAT)
- URL de consentimientos: relativa (consent.php?token=...)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Vista split: lista ordenada por numero_orden (activos arriba, cerrados abajo),
stats bar con totales, panel de detalle con exámenes por categoría, muestras,
consentimientos, comentarios, PDF via window.print() y acción de cerrar turno.
Auto-refresh cada 30s. Visible para bacteriólogos y admins.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Agrega botón de cambio de estado en cada fila del historial de turnos.
Admins (sin role_id) pueden reabrir turnos finalizados/ausentes/cancelados.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
zoom en el elemento hijo encogía 100vh dejando espacio vacío abajo.
En html el browser recalcula vh correctamente y el layout llena la pantalla.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Los nombres en MAYÚSCULAS son deletreados por el motor de voz.
Convertir a Title Case para que se lean como palabras normales.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Elimina speechSynthesis.cancel() para que los anuncios no se interrumpan.
El overlay espera a que el TTS termine (onend) antes de pasar al siguiente,
con un mínimo visual de 5 segundos.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El parser de PHP interpreta <? en HTML mode cuando short_open_tag=On.
Se rompe la cadena para que no aparezca el literal <? en el archivo.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Abre directamente en Excel con columnas formateadas, sin asistente
de importación ni apertura en VS Code.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Muestra "Hoy", "Ayer" o la fecha entre grupos de mensajes de días distintos.
El timestamp del bubble ahora solo muestra la hora.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sonido más fuerte (gain 1.0, 3 pulsos grave-agudo), se repite cada
30 s mientras el banner no se cierre con click.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Permite usar campos linked (texto) como controladores de secciones
condicionales; necesario para ocultar datos obstétricos según género.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El Tab es la señal de fin de cédula en el barcode. Al recibirlo se
activa _scannerLocked y se ignoran todos los caracteres siguientes
(apellidos, fecha, sangre) hasta que llegue Enter o se limpie el campo.
También se redujo el numpad ~10% para que entre en pantalla.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Restaura campo WhatsApp (inputmode=none), mueve el numpad debajo.
El numpad detecta cuál input tiene foco y escribe en ese campo.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El campo WhatsApp generaba fricción innecesaria. Ahora el numpad
tiene un botón "Obtener mi turno" de ancho completo como última fila.
WhatsApp queda como hidden vacío para no romper la API.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- inputmode=none en inp-cedula suprime el teclado Windows
- Grid 3×4 con dígitos 0-9, C (borrar todo) y ⌫ (borrar uno)
- pointerdown + preventDefault mantiene el foco en el input
- El escáner sigue funcionando (escribe por keydown, no por IME)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Navegar al ERP dentro del kiosk no sirve porque --kiosk es un flag
del proceso. window.close() cierra Chrome y devuelve el escritorio
Windows táctil, desde donde pueden abrir el ERP normalmente.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Toca el logo o el nombre del laboratorio 7 veces en 4 s para abrir
el overlay de salida: ir al ERP, recargar kiosko o cancelar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- blur() en mostrarTicket, seleccionarOtraPrioridad y volverPrioridades
para que ningún campo quede enfocado al cambiar de pantalla
- pointerup en cada input: si ya estaba enfocado, blur+focus(30ms)
fuerza a Windows a reinvocar el teclado táctil
- El focus programático de seleccionarPrioridad se conserva (escáner)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El Tab se bloquea pero las letras seguían escribiéndose en el campo.
Ahora cualquier carácter no numérico trunca el valor inmediatamente.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El lector de cédula envía los campos con Tab entre ellos. Se previene
el cambio de foco en keydown. Si algo llega igualmente a inp-cel
(letras) se limpia y regresa el foco a la cédula.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El escáner envía cedula+nombre+apellidos+… en una sola ráfaga.
Se corta en el primer espacio/tab para quedarse solo con los dígitos.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- cambiar_estado: permite en_espera_lugar→en_recepcion y en_servicio→en_recepcion
- Al volver de un lugar limpia lugar_destino_id, llamado_lugar_at, inicio_lugar_at,
fin_recepcion_at y atendido_lugar_por para que quede reasignable
- lugar.php: botón "A recepción" visible en en_espera_lugar y en_servicio
- resetFicha oculta el botón nuevo al limpiar la ficha
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- touch-action: manipulation directo en cada botón e input (no solo en body)
- -ms-touch-action para compatibilidad Edge legacy
- position: relative en .kiosko-wrap para contener screens absolutas
- inset: 0 en .screen.hidden para que no se salga del contenedor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Formularios con 2 campos firma (consentimiento + disentimiento)
mostraban la firma del campo 2 también en el campo 1 porque
firma_svg se usaba como fallback del primer campo. Ahora si
datos_respuestas ya tiene algún _svg, se confía solo en los datos
por-campo y se ignora el fallback global.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Usar GetSigImageB64 en el poll para actualizar el canvas del overlay
cada vez que cambia el número de puntos. SigImage/0 (SigWebRefresh)
solo devuelve el trazo actual; SigImage/1 acumula toda la firma.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Agregar <canvas id="topaz-canvas"> al overlay de ambos archivos
- SetTabletState(1, canvasCtx, 50) con contexto 2D (sin esto los trazos no se dibujan)
- Guardar timer retornado y pasarlo a SetTabletState(0, tmr) al cerrar
- NumberOfTabletPoints() en vez de GetSigTotalPoints() (función inexistente en v1.0.4)
- GetSigImageB64(callback) async en vez de llamada síncrona sin valor de retorno
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mismo fix que recepcion.php: servir el JS desde assets/js/ en vez
de localhost:47289 que no sirve archivos estáticos.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copiado de C:\Users\ZAIDA\AppData\Local\TopazSigWeb\SigWebTablet.js
en SOPORTE-3 (39 KB, versión instalada localmente).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El JS no es servido por SigREST sino instalado en AppData del usuario.
Se agrega /assets/js/SigWebTablet.js como primera URL; los puertos
localhost quedan como fallback.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Chrome bloquea scripts HTTP desde páginas HTTPS; SigWeb >=1.6.4 expone
el SDK en https://localhost:47290 para este caso. Fallback a http://47289
para instalaciones antiguas.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- cancelar_toma_pendiente: añade JOIN por paciente_id para prevenir IDOR
- cancelar_toma_pendiente: requiere turno_id para validar propiedad
- get_tomas_pendientes: porta filtros condicionales y _tomas_config de
guardar_toma.php para mostrar conteo de firmas correcto
- lugar.php: refresca sección tomas previas al completar via turneroFirmado
- lugar.php: muestra error en catch de cargarTomasPrevias en vez de silenciar
- lugar.php: limpia _tomaAlertados al cancelar toma previa
- lugar.php: pasa turno_id en POST de cancelar para ownership check
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cuando el paciente regresa con un turno nuevo, toma de muestras muestra
las tomas en_progreso de visitas anteriores con botón Continuar o Cancelar.
Cancelar marca estado='rechazado' (campo ya existía en el enum).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gesta/Partos/Cesáreas/Abortos ahora caben en una fila usando CSS :has(.campo-cuarto)
que activa grid de 4 columnas solo cuando el campo tiene clase campo-cuarto.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a patient selects 'Muestra pendiente' (prioridad F) at the kiosk,
the turn is immediately set to en_espera_lugar pointing to the first
active muestras location. Recepcion never sees it; toma de muestras
picks it up from its queue with no consent forms required.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_mpAutoFillHora was gated behind hayFirmaIniciada, so it never ran
on a fresh form where no signatures exist yet. Now always fills the
hora field of the first visible firma widget; scroll only when at
least one signature has already been saved.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Was sending 'consentimiento_turno' (plain text fallback) while the
configured template is 'consentimiento_turno_v2'. Now reads
turnero_wa_template and turnero_wa_lang from lab_config, matching
the same logic used in create_turno.php (kiosk).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add turno_id column to rips_examenes_pendientes (already migrated)
- Filter turno_id IS NULL in get_examenes_rips.php query
- New marcar_rips_usado.php marks record as consumed when loaded
- cargarExamenesRips() calls marker after loading exams into the turn
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If the kiosk captured a numeric document number as paciente_nombre,
fire consultarExamenesRips immediately on turn open without waiting
for the receptionist to manually search the patient.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the history section required a manual click to expand and load.
Now it opens and fetches automatically when a patient is selected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
--disable-print-preview conflicted with --kiosk-printing, routing to
the OS print dialog instead of printing silently. Also added AHK
fallback script for edge cases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Genera par de llaves RSA (cert en config/qz/)
- api/qz_sign.php firma los print jobs con SHA512
- assets/js/qz-print.js conecta QZ Tray y envía trabajo HTML
- ver_formulario_enviado.php carga QZ cuando ?autoprint=1
- Fallback a window.print() si QZ no está instalado
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Detecta si el pad físico responde (GetTabletState), si falla instanciar
SigWebTablet (posible conflicto con SigPlus), y guía paso a paso.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Detecta SigWeb al cargar; muestra badge verde solo si localhost:47289
responde. Si no hay pad, no aparece nada.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Intenta localhost:47289 primero; si falla prueba localhost:8080.
Funciona para instalaciones con puerto no estándar sin cambiar Oracle.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Detecta al cargar la página si localhost:47289 responde; si no,
oculta todos los .btn-topaz para evitar el error al usuario.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El alert() bloqueaba la UI. Ahora muestra un banner flotante de 5s
que no interrumpe el flujo y sugiere usar firma manual como alternativa.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El iframe pasa targetMs al padre via postMessage. El padre corre su propio
intervalo (5s) que dispara la notificación si el modal está cerrado.
Si el iframe alcanza el 0 primero, cancela el del padre para no duplicar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Usa Web Notifications API via postMessage iframe→parent para alertar
incluso cuando el tab está en segundo plano o la pantalla ocupada.
Pide permiso al iniciar el protocolo (primer gesto del usuario).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
La alerta y el countdown mostraban "Toma · Otro" sin el minuto.
Ahora usan _mpEffectiveLabel para mostrar ej: "Toma · Otro - 2".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sin el sort, Otro (2 min) aparecía después de Minuto 15 en schema order,
haciendo que el countdown arrancara con 15 min en vez de 2.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Agrega _mpEffectiveLabel() que añade " - N" al label cuando la sección
es Otro y _mpOtroHora tiene valor, mostrando ej: "Toma · Otro - 2".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El render iteraba $esquema en orden original del JSON — Otro siempre
quedaba de último. Ahora se re-ordena $esquema agrupando las secciones
de toma por su minuto (usando $_mpSepOrder construido del usort ya
existente) antes de entrar al foreach de render.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- sel-lugar movido a última sección (antes de acciones) para que el
usuario lo seleccione justo antes de guardar/pasar
- resetCheckboxes() ahora también limpia sel-lugar entre turnos
- pasarALugar() lee sel-lugar directamente como primera opción,
evitando que un solicitudActiva.lugar_id desactualizado cause errores
- Botón refresh (fa-sync-alt) en cabecera de consentimientos llama
refrescarConsentimientosLugar() para ver estado sin recargar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Cada chip envuelto en .mp-chip-grp con CSS order = minuto
- Otro arranca en order:9999 (último); al digitar el valor se mueve
en tiempo real al lugar cronológico correcto
- Si ya tiene valor guardado (_mpOtroHora), abre posicionado
- Click handler usa .mp-chip-grp para encontrar input/unit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- fecha usa COALESCE(firmado_at, enviado_at, t.creado_at) porque
turnero siempre deja enviado_at en NULL
- filtro por fecha también usa ese COALESCE para no excluir registros
- total_envios y total_completados suman turnero_consentimientos
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Chip "0 min" queda marcado por defecto al abrir configurar tiempos
- Si se selecciona "Otro", bloquea guardar hasta que se ingrese la hora
- La hora de "Otro" se guarda en datos_respuestas._otro_hora
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Garantiza que la cadena de tomas siempre sea 0→15→30→... sin importar
el orden en que se guardaron en _tomas_config.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reemplaza JSON.stringify en atributo HTML por data-atributos + event
delegation, evitando que las comillas dobles rompan el handler.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DB: columna genero_permitido en exam_tipos (M/F/NULL)
GX,FV → solo femenino | PSA,FPSA → solo masculino
- recepcion.php: TomSelect muestra badge rojo y deshabilita opciones
incompatibles al seleccionar paciente; elimina los ya agregados
- create_solicitud.php: validación backend 422 si hay conflicto género/examen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Schema F-LAB-28: reemplaza 9 secciones hardcodeadas por 17 puntos de
tiempo (0,15,30,...,195 min, 3pm, 4pm, Otro)
- PHP: detecta sección "Otro" como toma válida (sentinel PHP_INT_MAX)
- JS/CSS: reemplaza checkboxes por chips clicables en el panel de
configuración; chip "Otro" muestra input de hora libre
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Al guardar una solicitud en recepción, escribe los exámenes en la bandeja
temporal WhatsApp (rips_examenes_pendientes) con DELETE+INSERT del día.
Al volver a seleccionar el mismo paciente, get_examenes_rips los detecta
como fuente=cache y los auto-carga sin confirmación.
También fix en get_examenes_rips: exámenes con codigo_legacy=NULL ahora
se buscan por codigo como fallback (COALESCE).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Si _llamar no actualiza turnoActivo a en_servicio (fallo silencioso del
cliente), recuperarTurnoActivo() vuelve a consultar el servidor y carga
el turno correctamente, mostrando el botón rellamar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- buscar_cie10.php: busca por código o descripción, límite 12 resultados
- inp-diag: dropdown estilo empresa, Enter selecciona primero
- selectOnTab:false en TomSelect de exámenes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Añade flag $_mpSkipSec para secciones excluidas por _tomas_config; la
condición anterior dependía de !$modoEditar y nunca ocultaba nada
cuando el consentimiento está en_progreso.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PHP's PDO in silent mode returns false on prepare() when table doesn't
exist; calling ->execute() on false throws TypeError (Error, not
Exception). Changed catch to \Throwable and added explicit checks so
the real error message is returned as JSON instead of HTTP 500 empty.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El badge 1/9 ignoraba la configuración guardada e incluía todas las tomas
sin condición. Ahora filtra relevantIds por los firma-ids en _tomas_config,
igual que lo hace guardar_toma.php y el render PHP del formulario.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- migrations/20260717_cie10_diagnosticos.sql: CREATE TABLE cie10_diagnosticos
- api/lab/ingest_diagnosticos.php: POST endpoint that receives batches of
{cod, concepto} rows and upserts into cie10_diagnosticos (ON DUPLICATE KEY)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Al crear un paciente nuevo desde el modal, el objeto no incluía genero
por lo que el bloque embarazada no se ocultaba para masculinos. Al editar
también se actualiza genero y se re-evalúa la visibilidad del bloque.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PHP: restaura auto-apply de _tomas_config para mostrar tomas correctas en recarga
- Badge: agrega botón 'Modificar' cuando el examen ya está configurado
- Panel: en modo modificar pre-marca las tomas guardadas y deshabilita
las ya firmadas (con ícono ✓ y opacity reducida)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PHP: elimina auto-aplicación de _tomas_config guardada; las tomas
siempre parten limpias hasta que el profesional configure manualmente
- JS: elimina auto-guardado cuando hay 1 sola toma por grupo
- JS: restaura _mpGetGroupsForExam a mostrar todos los grupos disponibles
(condicionados del examen + genéricos sin condición)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_mpGetGroupsForExam ahora solo incluye los grupos genéricos ('Toma')
cuando el examen no tiene secciones propias condicionadas.
Para Taukit/Heliprobe/etc. aparecen únicamente sus tomas.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Texto: "Marque las tomas que va a realizar."
- Título de grupo solo aparece cuando hay más de un grupo;
con un solo grupo (caso habitual) los checkboxes salen directamente
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El profesional elige manualmente cuáles tomas aplicar antes
de iniciar el protocolo, en vez de tener que desmarcar las que no usa.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Al vincular un paciente con genero='M' se oculta el bloque y
se desmarca el check. Al desvincular/resetear vuelve a mostrarse.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Grid pasa de 2 a 3 columnas en modo compact; campo-full abarca las 3
- texto/textarea siguen en fila completa (campo-full)
- numero, edad, fecha, hora ocupan 1/3 → caben 3 por fila
- form-check-input 1.35em, min-height 2.2rem para área táctil cómoda en tablet
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- create_turno.php: reemplaza http:// por https:// en el link enviado
por WhatsApp, evitando que Android CCT bloquee páginas HTTP
- ver_formulario_enviado.php: agrega botón "Abrir en navegador" en la
barra de acción cuando el modo es turnero, como fallback para Android
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Modal con select de prioridad (cargado desde BD) y campo opcional
de cédula/nombre. Llama a create_turno.php que ya maneja sesión,
duplicados, SSE y notificación WhatsApp.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- get_cola.php: quita orden por orden_peso, deja solo creado_at ASC
- recepcion.php: cards con prioridad distinta a E (general) muestran
borde izquierdo y fondo tintado con el color de la prioridad
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Campo inp-recibo aparece/desaparece al seleccionar 'Tarjeta' en forma de pago
- Se incluye en el payload como numero_recibo y se guarda en turnero_solicitudes
- Migración: ADD COLUMN numero_recibo VARCHAR(40) en turnero_solicitudes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Si eps o ciudad del paciente no existe en el select, se agrega como opción
temporal para no sobrescribir con vacío al guardar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Tabla lab_ciudades creada y poblada con 86 ciudades existentes
- Módulo lab_ciudades: listado, crear, editar, activar/desactivar, eliminar
- API /api/lab/ciudades.php: GET list, POST save/toggle/delete
- Modal paciente: campo ciudad cambiado a select, cargado junto con EPS
en un solo Promise.all al iniciar; Cúcuta preseleccionada por defecto
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Tabla lab_eps creada y poblada con valores existentes de lab_pacientes
- Módulo lab_eps: listado, crear, editar, activar/desactivar, eliminar
- API /api/lab/eps.php: GET list, POST save/toggle/delete
- Modal paciente en recepción: campo EPS cambiado de texto libre a select
cargado desde lab_eps al iniciar la página
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El kiosko guarda la cédula en paciente_nombre del turno. Al abrir el
modal de nuevo paciente, si el valor es numérico se pone en el campo
documento (no en nombre). El foco cae en nombre para que el recepcionista
complete los datos restantes. Campo editable normalmente.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Sin empresa: usa precio_base de exam_tipos (tarifa particular)
- Panel de precios se muestra con solo seleccionar exámenes (no requiere convenio)
- Total se aplica automáticamente al campo de cobro sin clic manual
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Con video activo: layout pasa de 3 columnas (37/39/24%) a 2 (1fr/29%).
El panel central (turno-activo) se oculta via CSS; left-area (listado)
absorbe el espacio liberado y el video sube de 24% a 29%.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Búsqueda automática con debounce 300ms al escribir (sin necesidad de lupa)
- Enter: si ya hay resultados selecciona el primero, si no dispara la búsqueda
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
JSON.stringify(e) dentro de onmousedown="..." producía comillas dobles
que el parser HTML interpretaba como cierre del atributo, dejando la
función con un objeto incompleto. Ahora los resultados se guardan en
_empresaResultados[] y el handler pasa solo el índice numérico.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Items seleccionados se muestran en filas verticales con chip de CUPS/código
- Búsqueda por CUPS funciona (código CUPS incluido en texto de <option>)
- Dropdown muestra chip de CUPS junto al nombre del examen
- PHP SELECT incluye columna cups de exam_tipos
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El banner de alerta solo se cerraba vía 'Ver paciente'. Si el profesional
hacía clic en 'Siguiente toma' directamente, el banner seguía parpadeando
y sonando aunque el formulario ya estuviera abierto.
- abrirTomaProgresiva() ahora llama cerrarBannerToma() + _tomaAlertados.delete()
- renderConsentimientos() preserva alertado:true si la siguiente_toma_at no
cambió, evitando beep doble al re-sincronizar después de firmar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PHP saltaba el render de separadores y campos hijos cuando _saltarSeccion=true,
incluso en modoEditar, así que el JS evaluar() nunca encontraba los elementos.
En modoEditar ahora se renderizan con estilo oculto inicial; JS gestiona la
visibilidad cuando el usuario marca/desmarca la condición.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Cada bloque separador+hora+observaciones+firma se envuelve en una
toma-card con 3 estados visuales: signed (verde colapsable), active
(color primario pulsante) y locked (gris opaco).
- Barra de progreso con puntos y porcentaje encima de las tarjetas.
- Al firmar: tarjeta activa pasa a signed (muestra hora anotada y
miniatura colapsable), siguiente pasa a active con scroll automático.
- Countdown de espera aparece en el header de la tarjeta bloqueada
además del banner global.
- Fix: variable 'minutos' indefinida en pct del countdown → 0.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A) Fix bug: grupos fuera de _tomas_config contaminaban $_mpMap y bloqueaban
firmas configuradas. Ahora se vacían y sus secciones se agregan a
$_mpSkippedSepIds para que PHP no las renderice.
B) Quitar campo Resultado del esquema (14 campos eliminados del form 15 en
BD). save_tomas_config.php ya no genera campo Resultado en exámenes nuevos.
Patrón queda: separador → hora → observaciones → firma_profesional.
C) Condicionamiento implícito: con el fix de A, solo aparecen las secciones
del grupo configurado en _tomas_config, sin cambios en el esquema DB.
D) UX: reemplaza popup automático por badges bajo el campo _c8j2g16.
Cada examen seleccionado muestra badge con estado (pendiente/configurado)
y botón "Configurar tiempos" que abre el panel solo al hacer clic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Auto-select tras crear examen: buscaba input[type=radio] pero el campo
_c8j2g16 es checkbox → nunca encontraba el elemento. Usa _mpSetExamChecked.
- save_tomas_config.php: añade campo Observaciones (texto) entre Resultado
y firma_profesional, igual que las secciones Toma existentes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El panel mp-cfg-overlay no tenía forma de cerrarse. Añade botón × en
el encabezado que oculta el panel, desmarca el tipo de examen y resetea
la visibilidad de secciones.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El campo tipo examen es checkbox → HTML renderiza name="_c8j2g16[]".
Todos los querySelector y el listener comparaban name="_c8j2g16" exacto
→ nunca matcheaban → visibilidad de secciones y panel de config nunca
se activaban. Añade _mpGetExamName()/_mpSetExamChecked() que aceptan
ambas variantes y los usa en todos los puntos de uso.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tras guardar la config de tomas (_mpGuardarConfig), location.reload() borraba
el checkbox seleccionado → _mpApplyExamVisibility(null) ocultaba toda la UI.
Ahora se guarda el examen en sessionStorage antes del reload y se restaura en
DOMContentLoaded, reactivando la visibilidad de firmas y tiempos.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix 1: countdown se recupera al reabrir el modal (lee siguiente_toma_at de BD)
- Fix 2: _mpIniciarCountdown recibe timestamp exacto en vez de minutos redondeados
- Fix 3: window._fpwPreFirma se emite una sola vez (no N veces por campo firma)
- Fix 4: postMessage('turneroFirmado') unificado a 2000ms en todos los paths
- Fix 5: resumen de tomas usa hora cacheada al guardar; elimina traversal DOM frágil
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El listener bloqueaba todo con el guard de _mpModoTurnero antes de llamar
a _mpApplyExamVisibility. Ahora la visibilidad siempre aplica al seleccionar
examen; el panel de config sigue siendo solo para modo turnero.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Los campos están en div.campos-grid (hijo de .doc-body), no directamente en .doc-body.
Elimina todos los console.log de debug y simplifica _mpApplyExamVisibility.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Los div.section-title ("Firma del profesional") no tienen data-campo-id y eran
ignorados por querySelectorAll. Ahora se itera docBody.children en orden para
que inHidden aplique a todos los hijos incluyendo los que no tienen atributo.
Agrega console.log [tomas] para diagnóstico del estado de _mpAllGroups.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lógica anterior usaba sepToExam (vacío cuando labels son genéricos "Toma · X").
Nueva lógica: reúne TODOS los sep_ids de _mpAllGroups como allTomaSeps,
luego muestra solo los del grupo que coincide con el examen seleccionado
(vía _mpGetGroupsForExam). Si el examen no tiene grupos o no hay selección,
todas las secciones de toma quedan ocultas.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- _mpApplyExamVisibility(): oculta/muestra secciones en el DOM usando
sep_id de _mpAllGroups, sin depender de condicion en el esquema
- Funciona aunque la BD no tenga condiciones restauradas aún
- _restore_tomas_conditions.php: migración para restaurar condiciones
en separadores usando prefijo del label vs opciones de _c8j2g16
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Panel ya no se renderiza server-side, sino que JS lo muestra al instante
cuando el usuario selecciona un tipo de examen del selector _c8j2g16
- _mpGetGroupsForExam() filtra los grupos correspondientes al examen elegido
- Si solo hay 1 toma por grupo, auto-guarda sin mostrar panel
- Si al cargar ya hay examen seleccionado y sin _tomas_config, muestra panel
- configurar_tomas.php: guarda también _c8j2g16 (exam_type) en datos_respuestas
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Botón "Añadir tipo de examen…" abre wizard directo en el formulario
- Paso 1: nombre del examen + cantidad de tomas (1-12) con +/-
- Cada toma: label, tipo (minutos / hora fija), valor
- Al guardar llama api/lab/save_tomas_config.php y recarga la página
- Auto-selecciona el nuevo examen tras recargar (sessionStorage)
- Restaura filtrado de $_gruposConfig por examen seleccionado
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Botón "Añadir tipo de examen…" debajo del selector _c8j2g16
- Al escribir y guardar, añade la opción vía API y la selecciona al instante
- api/lab/add_exam_option.php: guarda la nueva opción en el esquema del formulario
- Migración actualizada: solo elimina condiciones de separadores (no toca el selector)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- _c8j2g16 pasa a texto libre ("Tipo de examen"), sin opciones predefinidas
- Separadores de tomas sin condición → siempre visibles
- Eliminar lab_tomas_config.php (ya no necesario)
- $_gruposConfig incluye todos los grupos directamente
- Script _migrate_tomas_simplify.php para actualizar esquema en BD
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Panel de protocolo ahora tiene botón "Usar todas" para cerrar sin configurar
- Si todos los grupos ya tienen una sola toma, se guarda config silenciosamente y se recarga sin mostrar panel
- Lógica de guardado extraída a _mpGuardarConfig() compartida entre ambos botones
- Botón de acceso a lab_tomas_config.php en tarjeta del formulario es_toma_progresiva
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Feature 1 — Ciclos configurables por paciente:
- ver_formulario_enviado.php: pantalla de configuración aparece la primera
vez que el profesional abre el formulario (antes de firmar); permite
seleccionar qué tomas realizar para cada protocolo. $_mpSkippedSepIds
oculta las secciones no seleccionadas en render. $_mpMap se reconstruye
desde los grupos filtrados para que next_firma_id salte tomas omitidas.
- configurar_tomas.php (nuevo): guarda _tomas_config en datos_respuestas
del consentimiento; valida que cada grupo tenga al menos una toma.
- guardar_toma.php: aplica el filtro _tomas_config al array firmasCampos
para que el conteo de tomas firmadas/pendientes sea correcto.
Feature 2 — Gestión de tipos de examen (admin):
- lab_tomas_config.php (nuevo): página admin que muestra los exámenes
actuales con sus tomas y permite añadir nuevos tipos con su esquema
completo (separador + hora + resultado + firma_profesional por toma).
- api/lab/save_tomas_config.php (nuevo): modifica el esquema JSON del
formulario id=15; add_exam agrega opción al selector y los campos de
cada toma; delete_exam elimina la opción y los separadores exclusivos.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Al activarse cada sección de toma (carga inicial, toma inmediata,
fin de countdown), se rellena automáticamente el campo hora con la
hora actual del dispositivo. Solo actúa si el campo está vacío, por
lo que no sobreescribe valores ya ingresados manualmente.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Agrega script inline en <head> de Layout.php, lugar.php y recepcion.php
que detecta prefers-color-scheme y activa data-bs-theme="dark" en <html>
antes del primer paint (evita flash de contenido blanco)
- sidebar.php: script de fallback para páginas con head propio no actualizado
- Layout.php: corrige background hardcodeado #f8f9fa en .main-content y
reemplaza bg-white por bg-body-tertiary en barra de título
- styles.css: elimina bloque @media duplicado y contradictorio; nuevo bloque
unificado cubre @media (fallback sin JS) + [data-bs-theme="dark"] para
elementos custom (sidebar, chat); Bootstrap maneja el resto automáticamente
- Bump styles.css a v=15 en los tres templates
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- guardar_toma.php: reconoce firma campo_id_svg (compat. registros históricos)
- get_consentimientos.php: mismo fix para conteo tomas_firmadas
- ver_formulario_enviado.php: $_mpMap incluye hora fija (Cortisol 3pm/4pm),
agrupa por condición de separador para no mezclar Glicemia/Cortisol,
muestra resumen de tomas al completar antes de cerrar el modal
- lugar.php: banner rojo + beep cuando el countdown de una toma llega a cero,
resetea alerta al cerrar el modal; función _playBeepLugar() compartida
DB Form 15: +10 campos (firma_profesional Minuto 60 Glicemia + campo
Resultado numérico en cada toma Glicemia y Cortisol)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- guardar_toma.php: filtrar firmas por condición del examen seleccionado; corrige
countdown espurio y detección de 'completado' cuando hay múltiples tipos de examen.
También descarta siguiente_toma_at si ya es pasado (minuto 0).
- ver_formulario_enviado.php: auto-scroll a primera firma pendiente al cargar el modal;
si el examen se selecciona y la firma no está en DOM, guarda y recarga el iframe.
Al completar tomas, cierra el modal directamente sin botón "Finalizar".
- get_cola.php: incluye siguiente_toma_at y en_toma_progresiva por turno desde
turnero_consentimientos para mostrar cuenta regresiva en tarjetas.
- lugar.php: muestra chip de cuenta regresiva en tarjetas de cola para pacientes
en toma progresiva; color cambia a warn (<2 min) y crit (¡Ahora!).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- get_consentimientos: tomas_total ahora filtra por examen elegido en
datos_respuestas (no cuenta todos los 13 campos globales); badge
se oculta (total=0) hasta que se seleccione un examen.
- ver_formulario_enviado: agrega f.es_toma_progresiva al SELECT y lo
usa para forzar _soloFirmaPro=true, evitando que aparezca el canvas
de firma del paciente cuando la DB indica toma progresiva.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ver_formulario_enviado.php:
- $_mpPrimeraPendiente, $_mpBloqueado y $fSvg buscaban la clave campoId_svg
pero guardar_toma.php guarda con clave campoId (sin sufijo). Se añade
fallback a ambas claves para compatibilidad hacia adelante y atrás.
lugar.php:
- new Date('2026-07-09 15:30:00') no parsea en Safari; se reemplaza el
espacio por 'T' para formato ISO antes de construir el timestamp.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Formularios con ambos tipos de firma (paciente + profesional) en modo embebido
quedaban bloqueados porque $_mostrarCanvasPro exigía $_soloFirmaPro.
Flujo corregido:
- Canvas profesional ahora se muestra en cualquier form con firma_profesional
cuando el operador está logueado en modo turnero embebido.
- El botón "Confirmar" del paciente detecta si hay canvas del profesional:
si hay, guarda como borrador (sin marcar firmado) y hace scroll al canvas pro.
- El profesional firma último con solo_profesional=true, marcando firmado y
cerrando el modal.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- _guardarFirmaMP ahora llama guardar_toma.php en vez del draft POST,
guardando la firma con clave campoId (sin _svg) que es la que lee
get_consentimientos para el contador X/13.
guardar_toma.php también actualiza estado→en_progreso y siguiente_toma_at,
lo que activa el countdown en lugar.php via polling SSE.
- Al completar la última toma envía postMessage turneroFirmado para cerrar
el modal automáticamente.
- Botón cerrar del modal siempre habilitado (el operador puede cerrar
en cualquier momento durante el proceso).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Agrega medico_nombre al __paciente prefilled de crear_envio_presencial.php
haciendo JOIN con medicos vía turnero_solicitudes.medico_id, de modo que
el campo linked 'Médico solicitante' del formulario id=15 (tomas prolongadas)
se auto-cargue con el médico registrado en recepción.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- get_consentimientos: skip auto-create of consentimientos when solo_muestras=1
- get_consentimientos: add paciente_id to turno query; fallback muestras fetch
when no solicitud exists but turno has paciente_id (F-turns without reception)
- lugar: badge "Solo entrega de muestras" in ficha header for prioridad F
- lugar: hide sec-consent and sec-form-embebido when solo_muestras
- lugar: skip consent polling and form embedding for F-turns
- lugar: restore sec-consent visibility on resetFicha
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add data-campo-id attributes to all editable field wrappers and separators,
then run iniciarCondiciones-equivalent JS on page load so conditional sections
(e.g. Tipo de dolor when Dolores is checked) appear/disappear without needing
to close and reopen the modal.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add fecha_nacimiento to __paciente in modoTurnero so linked fields render it
- Format fecha_nacimiento as dd/mm/YYYY in static completed view
- Evaluate separator conditions in static view to skip hidden sections
- DB: add linked fields (numero_documento, tipo_documento, fecha_nacimiento, eps, telefono) to form id=16
- DB: add conditional "Tipo de dolor" field that appears only when Dolores is checked
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Before creating a new turn, check if the patient (identified by
paciente_id) already has a turn in today's session that is not
finalizado/ausente/cancelado. Returns 409 with the existing turn
code so the receptionist or kiosko can show a clear message.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a patient with a cedula is selected, the system queries RIPS
Manager for exams registered in the last 5 minutes. A green banner
shows the count with a "Cargar" button that pre-fills TomSelect.
Unmapped exam codes shown as a warning. Banner clears on deselect/reset.
- config.php: RIPS_MANAGER_URL constant from env var
- api/lab/get_examenes_rips.php: proxy + codigo_legacy mapper
- recepcion.php: banner HTML + consultarExamenesRips/cargarExamenesRips JS
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Toggling the checkbox hides the doctor field and clears any selection.
Unchecking or resetting the form restores the field to visible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New Bono field in the combined payment panel — included in the
running total sum, reset, and pagos_detalle payload on submit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
For recepcionista and bacteriologo roles, module.php now checks the
turnero_token cookie first — same order as recepcion.php — so token-
registered devices show only their assigned desk/lugar in the menu.
IP fallback now requires token IS NULL to avoid ambiguous matches.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After pressing Enter to add an exam, the typed text is cleared
and the dropdown closes so the next code can be typed immediately.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
onType highlights the first visible option on each keystroke so pressing
Enter immediately selects it — no need to arrow-down manually.
Also adds selectOnTab for the same effect with Tab key.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Table now uses full col-12 width — no more horizontal scroll.
Detail panel slides in from right as a fixed overlay with backdrop.
Also renames stats card from "Importados RIPS" to "Importados Lab".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Valor de BD cambia de 'rips' a 'lab' (más genérico)
- ingest_paciente: en UPDATE nunca pisa origen (conserva manual/whatsapp/lab)
- Badge muestra 'Resultados Lab' con ícono matraz; retrocompat con valor 'rips'
- Tabla agrega: teléfono+WA, email, EPS, género en nombre
- filtro dropdown usa 'lab' en lugar de 'rips'
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Migration: columna 'origen' (manual|rips|whatsapp) en lab_pacientes
- ingest_paciente.php: guarda origen='rips' en cada paciente importado
- Paciente::listar(): filtro por origen + orden por created_at DESC
- Paciente::stats(): conteo total/importados/con_wa/manuales
- get_pacientes.php: expone ?origen= y ?stats=1
- lab_pacientes.php: cards de stats clicables, badge origen por fila,
columna ciudad, columna fecha registro, dropdown filtro por origen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Nuevo campo 'modo' en el body:
- "insertar": si la cédula ya existe devuelve skipped sin tocar la BD
- "upsert" (default): crea o actualiza como antes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Incluye Access-Control-Allow-Headers con X-Lab-Key para que el
navegador pueda hacer el preflight desde RIPS Manager.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Token UUID generado en localStorage del navegador, persistido como cookie
- turnero_dispositivos.token: nueva columna para identificar equipos
- recepcion.php y lugar.php: chequean token (cookie) primero, IP como fallback
- configuracion.php: banner con token del equipo actual + botón "Registrar este equipo"
- save_dispositivo.php: acepta campo token al crear/editar dispositivo
- Migración: 20260709_dispositivos_token.sql
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Agrega /api/lab/ingest_paciente.php con autenticación por X-Lab-Key
(LAB_SYNC_KEY en config). Hace UPSERT por numero_documento sin requerir
sesión de admin — permite que RIPS Manager sincronice pacientes directo
a lab_pacientes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
En lugar de confiar solo en la sesión (seteada al login), recepcion.php
y lugar.php ahora consultan turnero_dispositivos por IP en cada request.
Esto cubre sesiones preexistentes y evita que aparezca el selector cuando
la IP ya tiene un lugar asignado.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- authenticateUser incluye turnero_lugar_id en la sesión
- login.php redirige con desk_id/lugar_id si el usuario tiene lugar fijo (sin necesitar IP)
- recepcion.php redirige con desk_id si la sesión tiene turnero_lugar_id, evitando el modal de selección
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ver_formulario_enviado: auto-fill médico (nombres/especialidad/código) en datos_prefilled desde solicitud
- ver_formulario_enviado: canvases de toma progresiva bloqueados secuencialmente — solo el turno activo es firmable, los siguientes aparecen al expirar el countdown
- ver_formulario_enviado: countdown prominente con reloj grande, color dinámico, sonido (Web Audio) y vibración al llegar la hora
- lugar: bloquear cierre del modal durante toma progresiva activa (botón X deshabilitado hasta mp_completar)
- lugar: bloquear Finalizar atención si hay muestras pendientes sin decisión (recibida/rechazada)
- lugar: _syncBtnFinalizar combina consentimientos + muestras para el aviso y estado del botón
- lugar: restricción de usuario a lugar específico via turnero_lugar_id (admin_users) + bloqueo JS en confirmarLugar()
- migrations: turnero_lugar_id en admin_users para restringir bacteriólogos por usuario
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cuando la solicitud tiene solo_muestras=1, get_consentimientos.php
busca turnero_muestras pendientes de otras solicitudes del mismo
paciente y las antepone en el array muestras con es_pendiente_anterior=1.
lugar.php renderiza un tag 'visita anterior' en cada muestra previa
para que el bacteriólogo identifique el origen.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El campo observaciones de la solicitud ya venía en el JSON de
get_consentimientos.php pero nunca se renderizaba en lugar.php.
Se agrega un bloque amber visible solo cuando sol.observaciones
tiene contenido, y se limpia al hacer resetFicha.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
llamarTurnoEspecifico usaba cambiar_estado.php sin pasar desk_id, por
lo que recepcion_desk_id quedaba NULL. El display TV filtra por
recepcion_desk_id = ?, así el turno llamado no aparecía en pantalla.
También se corrige llamado_recepcion_at: era COALESCE (no actualizaba
si ya tenía valor), ahora es NOW() para que el display reaccione al
re-llamar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El else branch de 3062cd7 marcaba todos los formularios de lugar con
origen_lugar_id=-1 cuando lugar_destino_id es NULL, ocultándolos en
recepción. Como create_solicitud ya no crea consentimientos de lugar,
el else es innecesario — en recepción fidsDeLugar queda vacío y los
consentimientos de examen pasan el filtro !c.origen_lugar_id.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Al escribir en el textarea de comentarios se guarda automáticamente
en localStorage con clave toma_draft_<turno_id>. Al abrir la ficha
del mismo turno el texto se restaura con borde amarillo de aviso.
Al guardar exitosamente o al hacer resetFicha se borra el draft.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
create_solicitud.php: cambia de crear consents por lugar (toma de muestras)
a crear consents por examen (exam_tipo_consentimientos). Los de lugar se
crean cuando el paciente llega a la estación vía get_consentimientos.php.
get_consentimientos.php: cuando lugar_destino_id es NULL (turno en recepción),
busca TODOS los formularios de turnero_lugar_consentimientos y los marca con
origen_lugar_id = -1 (truthy), para que el filtro !c.origen_lugar_id de
recepcion.php los excluya correctamente aunque no haya destino asignado aún.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
siguienteTurnoEnCola ahora acepta sesion_id opcional. llamar_turno.php
pasa la sesión de hoy para que nunca agarre turnos viejos de días
anteriores que quedaron en estado 'espera'.
Sin el filtro, el primer llamado del día podía agarrar un turno de ayer,
la ficha abría en recepción pero la pantalla TV (get_display_recepcion.php)
filtra por sesion_id=hoy y no lo encontraba → el número no salía en display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- App.php: clientIp() público resuelve X-Forwarded-For/X-Real-IP
- module.php: usa el mismo IP resolution en sidebar de recepcionista
- chat.php: botón Menú regresa al desk solo si rol=recepcionista con IP
registrada; bacteriólogo no aplica lógica de IP en este flujo
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Si la IP está registrada en turnero_dispositivos para una recepción,
el botón volver apunta a ese desk en lugar de al dashboard.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Los checkboxes del campo tipo checkbox quedaban apilados verticalmente.
Se envuelven en d-flex flex-wrap gap-2 para quedar lado a lado.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DB: ALTER TABLE turnero_consentimientos ADD siguiente_toma_at, toma_inicio_at;
MODIFY estado para incluir 'en_progreso'
- guardar_toma.php (nuevo): guarda firma parcial por campo_id, parsea el
esquema para calcular siguiente_toma_at desde 'Minuto X' o hora fija,
marca 'en_progreso' o 'firmado' según tomas restantes
- get_consentimientos.php: expone siguiente_toma_at, es_toma_progresiva,
tomas_total, tomas_firmadas, campos_firma_pro en cada consentimiento
- lugar.php: estado en_progreso con badge X/Y, countdown en tiempo real,
animación de alerta cuando el tiempo llega, botón 'Siguiente toma' que
abre el modal embebido en modo toma_progresiva
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mismo patrón que turnero/configuracion: form_ids[] en lugar de
formulario_id único, checkboxes en la vista, INSERT múltiple en save.php
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- save_ex_tipo.php: GET devuelve form_ids[], POST acepta form_ids[] y
hace DELETE+INSERT por cada uno (la tabla ya soportaba muchos a muchos)
- configuracion.php: reemplaza select único por checkboxes en formulario
Agregar y modal Editar; JS lee los checks y envía form_ids[]
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- module.php: si la IP está registrada en turnero_dispositivos para un
lugar de tipo recepción, el sidebar muestra solo ese desk; resto de
vistas (chat, verificar, TV) siempre visibles
- App.php: guard que redirige al recepcionista a su desk si intenta
acceder manualmente a otro desk_id; IP libre no tiene restricción
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Auth: agrega isBacteriologo()
- App: guard que bloquea dashboard e historial para bacteriólogo y
redirige según IP (turnero_dispositivos); IP registrada → ese lugar,
IP libre → primer lugar de muestras
- module.php: sidebar bacteriólogo sin Dashboard/Historial; IP
registrada muestra solo ese lugar, IP libre muestra TV + todos los
lugares muestras
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Elimina dependencia de tc.origen_lugar_id en SELECT; calcula el flag
dinámicamente consultando turnero_lugar_consentimientos contra el lugar
destino del turno. Funciona para registros históricos y sin migración.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Botón firma en tabla de usuarios (verde si ya tiene, gris si no).
Modal con canvas para dibujar o subir imagen, vista previa de firma
actual y opción de eliminar. APIs get/save_firma_usuario.php para admins.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sección nueva en tab Lugares con tabla CRUD de turnero_dispositivos:
IP, nombre, lugar asignado, estado activo/inactivo.
API save_dispositivo.php para crear/editar/eliminar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Nuevo módulo lab_examenes con lista paginada, buscador y filtro por categoría
- Vista de detalle/edición con campos completos de exam_tipos (CUPS, protocolo,
tipo muestra, nivel, abreviatura, seremite, ayuno, instrucciones)
- Sub-tabla inline de items de resultado (lab_items_resultado) con edición por fila
- Sub-tabla de precios por tarifa (lab_tarifas) con modal de edición
- APIs: list, get, save, save_item, save_tarifa, get_tarifas
- Script ETL Firebird→MySQL (scripts/etl_examenes.php) para la migración inicial
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Agrega origen_lugar_id a turnero_consentimientos para distinguir consents
creados por examen (null) vs por estación (= lugar_id destino).
Recepcion filtra los de origen_lugar_id != null; lugar.php muestra
solo los suyos + los de examen.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Filtrado en tiempo real + paginación client-side (15 por página).
Categorías vacías se ocultan automáticamente al buscar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cuando el esquema tiene campos tipo linked que ya muestran nombre, documento,
etc., el bloque top Datos del paciente queda oculto para no repetir la info.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- sse_turno.php: sleep(2)→sleep(1) para detectar notificarSSE en máx 1s
- display*.php: visibilitychange + focus → fetch inmediato al despertar pantalla
- display*.php: Wake Lock API para evitar throttling del browser en TVs
- display.php: SSE open → cargarSnapshot() inmediato al reconectar
Causa raíz: browsers throttlean setInterval a >1min en tabs sin foco;
las TVs inactivas dejaban de recibir actualizaciones aunque el servidor
las enviara correctamente.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- login.php: detectar dispositivo por IP al autenticar y redirigir al lugar asignado
- dashboard.php / lugar.php / recepcion.php: bloquear acceso por dispositivo (IP→lugar)
- lugar.php: modal firma profesional pre-guardada; compact 2-col en iframe; botón WA
solo cuando el formulario requiere firma del paciente
- ver_formulario_enviado.php: modo compact tablet, auto-save borrador (merge), firma
profesional en 1 clic, Muestras Prolongadas (countdown + Finalizar), auto_profesional
pre-fill desde sesión, fecha→fecha_hoy en campos de toma/recepción
- get_consentimientos.php: añadir requiere_firma_paciente para controlar botón WA
- save_firma_profesional.php: API para guardar firma pre-configurada del profesional
- migrations: turnero_dispositivos, admin_users.firma_svg, clon VIH F-LAB-05 para turnero
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Quita pantallas individuales por estación de muestras y kiosko del bloque anterior
- Deja solo Pantalla Global y Kiosko como accesos de visualización
- Añade bloque de URLs de gestión por escritorio de Recepción (recepcion&desk_id=X)
- Añade bloque de URLs de gestión por estación de Muestras (lugar&lugar_id=X)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
La columna se usaba en ver_formulario_enviado.php pero no existía
en la BD, causando error 500.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- create_solicitud.php crea lab_recepciones (es_historico=0) dentro de
la transacción: num_factura auto-secuencial para registros nuevos,
campos empresa/subgrupo/autorización/diagnóstico tomados de la solicitud
- Inserta lab_relaciones por cada examen con precio del snapshot JSON
- Inserta lab_pagos + lab_pagos_det si hay metodo_pago registrado
- Bloque try/catch: si migración 11 aún no corrió solo se loguea,
no se revierte la solicitud principal
- recepcion.php muestra toast "Recepción #N generada" al guardar
- Migración 11: ALTER lab_recepciones.id → AUTO_INCREMENT,
ADD solicitud_id, ALTER lab_pagos.numcaja_legacy → nullable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Nueva sección empresa/convenio en recepcion.php: búsqueda live por
nombre/NIT, selector de subgrupo, campo autorización (cuando req_autoriza),
diagnóstico CIE-10
- Tabla de precios calculados en tiempo real: resolución por tarifa de la
empresa o subgrupo, descuento empresa aplicado, botón "Aplicar al cobro"
- Al agregar/quitar exámenes o cambiar empresa/subgrupo se recalcula automáticamente
- Nuevo API get_precios_examenes.php: resuelve precios desde lab_tarifas
con soporte de tarifas derivadas por porcentaje
- create_solicitud.php guarda nit_empresa, subgrupo_id, autorizacion,
diag_ppal, items_precio (snapshot JSON al momento de la recepción)
- Migración 10: ALTER turnero_solicitudes añade los 5 campos nuevos
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Nuevo módulo lab_empresas: lista paginada, modal create/edit empresa,
gestión de subgrupos inline y CRUD de catálogo lab_tarifas_id
- APIs: empresas.php (list/get/save/toggle/delete),
empresa_subgrupos.php, tarifas_id.php
- Migración 09 registra el módulo en system_modules y asigna permiso admin
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Migraciones LIS 01-08: schema completo del nuevo LIS (secciones, protocolos,
ítems de resultado, perfiles, tarifas, empresas, histórico transaccional)
- ETL Firebird→MySQL: script CLI con conversión WIN1252→UTF-8, batches de 500,
resolución de FKs y deduplicación de pacientes
- turnero_muestras: tracking pendiente/recibida/rechazada por tipo de tubo
- lugar.php: widget de recepción de muestras (solo tipo=muestras)
- update_muestra_estado.php: API para marcar estado de muestra
- create_solicitud.php: auto-crea muestras al guardar solicitud
- get_consentimientos.php: incluye muestras[] en el response
- 6 vistas SQL: v_muestras_hoy, v_recepcion_completa, v_examen_precio, etc.
- numero_orden en encabezado del formulario firmado (D-/F- color diferenciado)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tom Select expone los data-* de los <option> como data.consent (sin prefijo),
no como data.element.dataset.consent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reemplaza la lista de checkboxes por Tom Select (sin jQuery):
- Búsqueda integrada mientras se escribe
- Exámenes agrupados por categoría (optgroup)
- Cada examen con consentimiento asociado muestra badge ⚠ Consentimiento
en el dropdown y marcador visual en el tag seleccionado
- Solo entrega de muestras deshabilita el select automáticamente
- Remove_button plugin para quitar ítems individualmente
- Elimina filtrarExamenes / toggleCategoria / actualizarCatChk (ya innecesarios)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Los turnos ya tomados (en_recepcion) muestran fondo azul pálido,
borde izquierdo azul grueso y badge "EN ATENCIÓN" en lugar del
chip de tiempo, para que el recepcionista identifique de un vistazo
qué turno ya está siendo atendido y no llame otro.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cada turno en la lista muestra cuánto lleva el paciente esperando,
igual que en toma de muestras — verde < 10 min, amarillo < 25 min,
rojo ≥ 25 min.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
lugar.php:
- Bell button enlarged to 44×44px touch target
- Fecha nac. / Celular fields hidden from bacteriologist ficha
- Cancel button added to station-selector overlay; shows only when changing
- ESC key closes overlay when a station is already selected
- visibilitychange pauses polling when tab hidden, resumes on return
recepcion.php:
- Consent action buttons: padding 2px 8px → 5px 12px, font-size .72→.82rem
- Exam list: per-category select-all checkbox with indeterminate state support
- guardarSolicitud: auto-calls pasarALugar() when no consents required
- abrirModalPaciente: nome field auto-focused for both new and edit cases
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- btn-accion-primary: padding .8rem 1rem→.8rem, font-size .97→1rem, opacity .55→.5
- touch target: eliminar override de padding en cola-card que contradecía el base
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
get_display_global.php: la query de cada escritorio tenía la condición
OR t.recepcion_desk_id IS NULL
lo que hacía que un turno llamado sin desk_id (NULL) apareciera como activo
en TODOS los escritorios simultáneamente, anunciando el destino incorrecto.
Se eliminó esa condición para usar match exacto por escritorio.
llamar_turno.php: cambiar COALESCE(?, recepcion_desk_id) por ? directo.
El COALESCE era innecesario porque el UPDATE solo aplica sobre estado='espera'
(primer llamado). Ahora siempre escribe el valor recibido.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Bug 1: announcedCalls usaba el ID del turno como clave, silenciando todas
las re-llamadas. Ahora usa key=codigo|destino|llamado_at, igual que
lastSeenKeys, para que cada re-llamada anuncie correctamente.
- Bug 2: anunciarTurno no verificaba sonidoActivo antes de llamar a
speechSynthesis.speak(); agregado guard al inicio de la función.
- Bug 3: Chrome congela speechSynthesis tras ~15 min sin interacción;
agregado keepalive de 14s con pause()+resume() para mantener el motor activo.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
get_consentimientos.php ahora incluye s.numero_orden en el SELECT.
En lugar.php el número de orden aparece como badge de solo lectura
en el header de la ficha, junto al código del turno, cuando la
solicitud tiene número de orden asignado.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Después de firmar (y en carga cuando ya está firmado) no había ninguna
acción disponible — solo texto "Puede cerrar esta ventana" sin botón.
Ahora aparecen dos botones:
- Descargar PDF → window.print() (permite guardar como PDF)
- Cerrar → window.close() (solo cuando no está en iframe)
En modo embebido (iframe desde lugar.php) solo aparece Descargar PDF;
el cierre lo maneja el postMessage ya existente al padre.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El archivo grabado por el browser es WebM (no soportado por WhatsApp).
conversations.php usa ffmpeg para convertirlo; chat_upload_media.php solo
cambiaba el string del MIME pero enviaba el archivo WebM real, causando que
el audio no llegara al destinatario. Ahora se convierte con los mismos
parámetros que upload_media.php (libopus 32k mono 48kHz).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Agrega botón "Ver" en la sección Paciente de la ficha del turno.
Abre modal con datos completos del paciente (nombre, doc, fecha nac,
celular, médico, embarazo) y timeline de historial de visitas anterior
(misma lógica que recepción), cargado desde get_historial.php.
El botón solo aparece cuando hay un paciente vinculado al turno.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- URL absoluta para local_file y media_url_external (igual que conversations.php)
- Múltiples <source> con MIME types (ogg, mpeg, wav, webm) para compatibilidad
- onerror handler cuando el archivo no está disponible
- Fallback visible cuando mediaSrc es null ("Audio pendiente de descarga")
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Al seleccionar un paciente de la lista, si la plantilla tiene una variable
llamada 'name', se pre-rellena automáticamente con su nombre. Si se ingresa
un número directo, el campo queda vacío para llenarlo manualmente.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
create_turno.php y send_consentimiento.php enviaban por WhatsAppService('turnero')
(número correcto) pero sin pasar meta canal='turnero', por lo que saveOutgoingMessage
defaulteaba a canal='bot' y quedaban registrados en las conversaciones del principal.
Agregado $waMeta = ['canal'=>'turnero'] en ambos archivos para sendTemplateMessage
y sendTextMessage (fallback).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El parámetro_name enviado a WhatsApp era el índice numérico ("1") en vez
del nombre real de la variable ("name", "fecha", etc.), causando el error
#100 Invalid parameter. Ahora se detecta si las variables son nombradas o
numéricas y se construye el payload correctamente, igual que conversations.php.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Las URLs de CDN de WhatsApp (lookaside.fbsbx.com) requieren Authorization Bearer.
chat_get_messages.php las exponía directamente al browser, que no puede añadir
ese header, resultando en 401 "Authentication Error".
Ahora siempre se enrutan por /api/version/media-url.php (proxy existente) que:
- Sirve el archivo local si existe en disco
- Hace fetch con el token de WA si no está en local
- No requiere sesión para peticiones GET del browser
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Los estilos de #nuevo-modal y .nv-* estaban dentro de @media (max-width: 680px),
por lo que el modal no tenía diseño en desktop.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- chat_react: guardaba en tabla `messages` en lugar de `conversations` (tabla real del chat)
- chat_react: columna era `whatsapp_message_id`; la correcta es `message_id`
- chat.php: sendReactionEmoji actualiza la burbuja DOM inmediatamente sin esperar poll
- chat_upload_media: mime_content_type() devuelve video/webm en Linux para audio grabado;
ahora cualquier mime con "webm" se remapea a audio/ogg antes de determinar mediaType
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- chat_buscar_paciente.php: GET ?q= busca en lab_pacientes por nombre,
documento o teléfono (mín. 2 chars, máx. 20 resultados)
- chat_start_conversation.php: POST {phone, template_name, lang, params}
— normaliza teléfono, busca/crea usuario en users, envía plantilla
vía WhatsAppService('turnero'), devuelve {ok, user_id}
- chat.php: botón "+ Nuevo" en sidebar header; modal 3 pasos:
1) buscar paciente (debounce 350ms) o ingresar número libre
2) lista de plantillas habilitadas
3) variables de la plantilla + Enviar
Al enviar recarga el sidebar y abre la conversación automáticamente
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CSS/HTML: mismo overlay #topaz-overlay con modal de estado
- JS: módulo topaz con target={canvas, onAccept} para canvas
del profesional (.fpw-canvas) y del paciente turnero (.turnero-cv)
- Botón "Tableta" en canvas del paciente (.turnero-topaz) y del
profesional (.fpw-topaz); el usuario sigue confirmando manualmente
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CSS: overlay #topaz-overlay con modal centrado, estado has-sig
- HTML: modal con área de estado, contador de trazos, botones
Cancelar / Limpiar / Aceptar
- JS: módulo topaz — carga SigWebTablet.js desde localhost:47289,
compatible con SigWeb v1 (globals+ctx) y v2 (métodos en objeto),
soporta fid='__global' para canvas principal e inline (fw-*)
- Botón "Tableta" agregado en canvas area global y en renderFirmaInline
para campos tipo firma/firma_profesional
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WhatsApp API returns "(#100) Invalid parameter — Parameter name is
missing or empty" for templates that use named variables.
- chat.php sendTemplate(): collect params as [{name, value}] objects
using data-var-index as the parameter name
- WhatsAppService::sendTemplateMessage(): handle {name, value} objects
in the indexed-array path by emitting {type, parameter_name, text}
(backward-compatible — plain strings still work as positional params)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- WhatsAppService::makeRequest: include error_subcode and error_data.details
in the thrown exception message to surface the real reason for #100 errors
- chat_send_message: log template name/lang/params before sending;
pass canal+operator_id meta so outgoing templates are tagged correctly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
closeTemplatePicker() sets _tplSelected = null, so accessing
_tplSelected.template_name after the call threw a TypeError caught
as "Error de red al enviar plantilla".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- chat_get_plantillas.php: GET — returns enabled templates from
turnero_chat_plantillas JOIN message_templates; auto-creates table
- chat_save_plantillas.php: POST {ids:[...]} — replaces all rows in
turnero_chat_plantillas using DELETE + INSERT IGNORE in a transaction
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
uploadMedia() returns the full API response array, not just the id string.
chat_upload_media.php was passing the whole array as $mediaId, producing
{"image": {"id": {"id": "...", "messaging_product": "..."}}} which
violated WhatsApp JSON schema for the media id field.
Also remap audio/webm (browser MediaRecorder default) to audio/ogg at
upload time since WhatsApp's media API rejects webm but accepts ogg/opus.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
list.php now accepts page/limit params and returns total count.
View shows X-Y de N label, page buttons with ellipsis for large ranges,
and prev/next arrows. Search resets to page 1; save/delete stay on
current page.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bloque 1 - Cola:
- Separate sections for "en servicio" vs "en espera" with headers
- Time-in-queue badges (green <10min, amber <25min, red 25min+) from creado_at
- Prominent EN SERVICIO pill (solid green) instead of subtle border
- Larger touch targets on queue cards, bigger bell button
- Count chips show espera + servicio counts separately
Bloque 2 - Ficha header + action bar:
- Patient name is now the dominant element (1.35rem bold) with skeleton
loader animation while async data loads; turno code becomes a secondary chip
- Action bar restructured: full-width primary button (blue "Anunciar turno" /
green "Finalizar atención") + smaller secondary row (Re-llamar, Regresar, Ausente)
- Consent warning strip added inside action bar showing pending count;
blocks Finalizar visually and functionally
Bloque 3 - Sections + mobile:
- Section headers use consistent .ficha-sec-hdr style with icon
- Mobile: fixed sticky "← Cola" top bar inside ficha panel (always visible,
no hunting for back button)
- ficha-turno uses flex-column so action bar sticks to bottom without scroll
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-03 14:21:42 -05:00
236 changed files with 27763 additions and 2293 deletions
- [Tipos de campos del Builder](#tipos-de-campos-del-builder)
- [Firma digital](#firma-digital)
- [Enlace público y vigencia](#enlace-público-y-vigencia)
- [PDF y visualización del documento](#pdf-y-visualización-del-documento)
- [Sello de integridad SHA-256](#sello-de-integridad-sha-256)
- [Firma del profesional](#firma-del-profesional)
- [API Endpoints](#api-endpoints-1)
---
## 1. Módulo de Agendamiento (Domicilios)
### ¿Qué es?
El módulo de agendamiento gestiona los **servicios de toma de muestras a domicilio**. Permite crear, asignar, seguir y completar visitas médicas domiciliarias. Los administradores gestionan la agenda desde `lab_domicilios.php`; los enfermeros gestionan su propia agenda desde `enfermero_portal.php`.
---
### Archivos involucrados
| Archivo | Descripción |
|---|---|
| `lab_domicilios.php` | Vista principal del admin — tabla con filtros, detalle del domicilio, formularios recibidos, exportar CSV |
| `enfermero_portal.php` | Portal exclusivo del enfermero — su agenda personal del día, ordenada por hora, con tarjetas colapsables |
| `lab_enfermeras.php` | CRUD del personal de enfermería |
| **Admin** | Crear, editar y ver todos los domicilios. Asignar/reasignar enfermeros. Registrar pagos. Exportar Excel. Ver informe completo con notas. Ver agenda de cualquier enfermero usando `?eid=X`. |
| **Enfermero** | Solo ve su propia agenda (`enfermero_portal.php`). Avanza el estado de sus domicilios asignados. Agrega servicios extra. Registra notas clínicas y libres. Visualiza órdenes médicas adjuntas. |
> **Redirección automática**: si el usuario autenticado tiene rol `enfermero`, `lab_domicilios.php` lo redirige inmediatamente a `enfermero_portal.php`.
Los roles se definen en `admin_users.role` (ENUM `admin` / `enfermero`) y `admin_users.enfermera_id` (FK a `lab_enfermeras`).
---
### Flujo de estados
```
[programado]
│
│ El enfermero confirma que realizará la visita
▼
[confirmado]
│
│ El enfermero sale hacia el domicilio
▼
[en_camino]
│
│ El enfermero llega → hora_llegada se registra automáticamente
▼
[en_domicilio]
│
│ El enfermero finaliza → hora_salida se registra automáticamente
▼
[completado]
Desde cualquier estado:
→ [cancelado] (requiere motivo_cancelacion como campo obligatorio)
→ [reprogramado] (requiere fecha_reprogramada)
```
**Transiciones permitidas al enfermero** (validadas en `update_domicilio_enfermero.php`):
| Estado actual | Estados posibles |
|---|---|
| `programado` | `confirmado` |
| `confirmado` | `en_camino`, `cancelado` |
| `en_camino` | `en_domicilio`, `cancelado` |
| `en_domicilio` | `completado`, `cancelado` |
El **admin** puede cambiar a cualquier estado directamente, incluyendo cancelar desde cualquier punto.
---
### Funcionalidades
- **Filtros**: por fecha, estado, enfermero asignado. Botón "Hoy" para filtro rápido.
- **Resumen del día**: conteo de domicilios por estado en la parte superior.
- **Panel de detalle**: al hacer clic en un domicilio se abre el panel lateral con toda la información, notas clínicas, notas libres e informe imprimible.
- **Asignar / Reasignar enfermero**: modal con lista del personal disponible.
- **Sin asignar**: badge con el conteo de domicilios que aún no tienen enfermero.
- **Registrar pago**: modal para marcar el cobro con modalidad y monto.
- **Servicios extra**: el enfermero los agrega desde su portal durante la visita.
- **Notas del enfermero**: ficha clínica con antecedentes, medicamentos, acudiente (si menor) y notas libres con imagen adjunta.
- **Informe de domicilio**: vista imprimible del domicilio con datos del paciente, ficha clínica y notas del enfermero.
- **Exportar CSV (Excel)**: exporta todos los domicilios filtrados, incluyendo las columnas de notas del enfermero (antecedentes, medicamentos, acudiente, notas libres).
- **Formularios recibidos**: pestaña dentro de `lab_domicilios.php` que muestra formularios enviados con filtro por plantilla y estado.
- **Portal del enfermero**: tarjetas colapsables ordenadas por hora, separadas en "Activos" y "Finalizados". Permite avanzar estados, agregar notas y ver órdenes.
---
### API Endpoints
| Endpoint | Método | Descripción |
|---|---|---|
| `api/lab/get_domicilios.php` | GET | Lista con filtros. `?id=X` para uno solo con detalle completo. |
| `api/lab/save_domicilio.php` | POST | Crear, editar o cambiar estado. `?solo_estado=true` para solo cambiar estado. |
| `api/lab/update_domicilio_enfermero.php` | POST | El enfermero avanza el estado de su domicilio. |
| `api/lab/my_agenda.php` | GET | Agenda del enfermero autenticado con servicios extra. |
| `api/lab/save_asignacion.php` | POST | Asignar o reasignar enfermero a domicilio. |
| `api/lab/get_asignaciones.php` | GET | Asignaciones por fecha. |
| `api/lab/registrar_pago.php` | POST | Registrar pago con monto y modalidad. |
| `api/lab/save_servicio_extra.php` | POST | Agregar servicio realizado durante la visita. |
| `api/lab/get_notas_domicilio.php` | GET | Notas del domicilio (clínicas y libres). |
| `api/lab/upload_nota_imagen.php` | POST | Subir imagen adjunta a una nota libre. |
| `api/lab/crear_desde_whatsapp.php` | GET/POST | Crear paciente u orden desde una conversación de WhatsApp activa. |
---
## 2. Módulo de Formularios
### ¿Qué es?
El módulo de formularios permite crear **plantillas de documentos** (consentimientos, historias clínicas, autorizaciones, encuestas) mediante un builder visual, enviarlas a los pacientes por WhatsApp y recopilar sus respuestas con firma digital. El documento firmado genera un **sello de integridad SHA-256** que puede verificarse públicamente.
---
### Archivos involucrados
| Archivo | Descripción |
|---|---|
| `lab_formularios.php` | Vista principal — lista de plantillas y registro de envíos |
| `lab_formulario_builder.php` | Editor visual drag & drop (ventana separada, solo admin) |
| `form_cliente.php` | Página pública — el paciente llena y firma sin iniciar sesión |
| `ver_formulario_enviado.php` | Vista del documento firmado — acceso por ID (admin/enfermero) o token (cliente) |
| `verificar_formulario.php` | Verificación pública de autenticidad por hash SHA-256 |
| `expira_en` | DATETIME NULL | Siempre `NULL` — el enlace no expira |
| `completado_en` | DATETIME | Fecha y hora en que el cliente completó el formulario |
| `hash_verificacion` | CHAR(64) | Sello de integridad SHA-256 del documento |
---
### Roles y permisos
| Rol | Acceso |
|---|---|
| **Admin** | Crear, editar y eliminar plantillas desde el builder. Enviar formularios a cualquier paciente. Ver todos los envíos. Ver y descargar el PDF de cualquier formulario. |
| **Enfermero** | Enviar formularios existentes a sus pacientes. Ver solo sus propios envíos (`enviado_por = su user_id`). No puede crear ni editar plantillas. Puede firmar como profesional en los formularios que él mismo envió. |
| **Cliente (público)** | Accede a `form_cliente.php?t=TOKEN` sin ninguna autenticación. Llena y firma el formulario. Puede volver al mismo enlace en cualquier momento para ver el documento firmado y descargarlo como PDF. |
---
### Flujo completo
```
1. ADMIN crea la plantilla
├─ Abre lab_formulario_builder.php (se abre en ventana nueva)
├─ Arrastra campos al canvas y los configura
├─ Configura el diseño del documento (logo, color, encabezado, pie de página)
└─ Guarda → POST api/lab/save_formulario.php → lab_formularios
2. ADMIN o ENFERMERO envía el formulario
├─ lab_formularios.php → botón "Enviar" → modal
├─ Busca y selecciona el paciente
├─ Previsualiza los campos que llegarán pre-llenados
├─ Selecciona el canal: WhatsApp o "solo link"
└─ POST api/lab/send_formulario.php
├─ Genera token de 64 hex chars: bin2hex(random_bytes(32))
├─ Crea fila en lab_form_envios (estado=pendiente, expira_en=NULL)
└─ Devuelve URL pública y mensaje preformateado para WhatsApp
3. CLIENTE recibe el enlace (por WhatsApp u otro medio)
├─ Abre form_cliente.php?t=TOKEN
├─ GET api/lab/submit_formulario.php?t=TOKEN → carga datos del formulario
└─ Si ya fue firmado antes → muestra pantalla de solo lectura con link al PDF
4. CLIENTE llena el formulario
├─ Campos "linked" llegan pre-llenados con datos del paciente (readonly si tienen valor)
├─ Campos vacíos linked son editables para que el cliente los complete
├─ Campos firma_profesional muestran aviso "uso exclusivo del profesional"
└─ Dibuja su firma (canvas) o adjunta una foto de firma
5. CLIENTE envía
├─ POST api/lab/submit_formulario.php
├─ Se genera hash SHA-256 (contenido + firma + ID + token + timestamp)
├─ Estado → "firmado" (si hay firma) o "completado" (sin firma)
└─ Pantalla de éxito con hash visible y botón para descargar el PDF
6. PROFESIONAL firma (si el formulario lo requiere)
├─ Admin/Enfermero abre ver_formulario_enviado.php?id=X con sesión activa
├─ Aparece canvas de firma en la posición del campo firma_profesional
├─ Dibuja su firma y hace clic en "Guardar firma"
└─ POST api/lab/firmar_profesional.php → guarda campo_id_svg en datos_cliente
└─ ver_formulario_enviado.php?id=X → documento HTML imprimible
8. VERIFICACIÓN pública de integridad
└─ verificar_formulario.php?h=HASH_SHA256
├─ Busca en lab_form_envios.hash_verificacion
└─ Muestra: nombre del formulario, paciente, fecha, estado y si el sello es válido
```
---
### Tipos de campos del Builder
#### Campos de entrada
| Tipo | Descripción |
|---|---|
| `texto` | Campo de texto corto de una sola línea |
| `textarea` | Área de texto largo (varias líneas) |
| `numero` | Campo numérico |
| `fecha` | Selector de fecha |
| `hora` | Selector de hora |
| `select` | Lista desplegable con opciones configurables |
| `radio` | Selección única con opciones configurables |
| `checkbox` | Selección múltiple con opciones configurables |
| `lista_marcable` | Lista de ítems numerados con checkboxes |
#### Campos de firma
| Tipo | Descripción |
|---|---|
| `firma` | Firma del **paciente** — visible y editable en `form_cliente.php` |
| `firma_profesional` | Firma del **profesional** — bloqueada para el cliente; solo editable desde el panel admin/enfermero |
#### Campos de contenido
| Tipo | Descripción |
|---|---|
| `separador` | Separador visual o título de sección |
| `parrafo` | Bloque de texto estático (pre-formatado o flujo libre) |
| `parrafo_inline` | Párrafo con marcadores `{nombre_completo}`, `{telefono}`, etc. que se convierten en espacios editables si el valor está vacío |
#### Campos vinculados al paciente (`tipo: linked`)
Se auto-rellenan con los datos del paciente al momento de enviar. Si el valor existe → campo de solo lectura. Si está vacío → el cliente puede completarlo.
| `linked_key` | Dato que extrae |
|---|---|
| `nombre_completo` | Nombre completo del paciente |
| `numero_documento` | Número de documento |
| `tipo_documento` | Tipo de documento |
| `fecha_nacimiento` | Fecha de nacimiento |
| `telefono` | Teléfono |
| `email` | Correo electrónico |
| `eps` | EPS o aseguradora |
| `direccion` | Dirección |
---
### Firma digital
**Modos disponibles** (configurados en la plantilla mediante `firma_modos`):
| Modo | Funcionamiento |
|---|---|
| `canvas` | El cliente dibuja su firma con el dedo o el mouse. Se captura con `canvas.toDataURL('image/png')`. |
| `foto` | El cliente sube una imagen desde su cámara o galería (`<input accept="image/*" capture="environment">`). Se convierte a base64 con `FileReader`. |
Ambos modos pueden estar activos simultáneamente en la misma plantilla.
**Firma global vs. firma por campo:**
- Si el esquema **no incluye** campos tipo `firma`, se muestra una sección de firma global al pie del formulario.
- Si el esquema **incluye** campos `firma`, cada uno tiene su propio widget canvas independiente en la posición configurada dentro del formulario.
---
### Enlace público y vigencia
- **URL pública**: `form_cliente.php?t=TOKEN`
- **TOKEN**: 64 caracteres hexadecimales generados con `bin2hex(random_bytes(32))`.
- **Sin sesión**: el cliente no necesita crear cuenta ni iniciar sesión.
- **Sin vencimiento**: la columna `expira_en` existe en la tabla pero siempre es `NULL`. El enlace es permanente.
- **Bloqueo por estado**: si el formulario ya fue completado o firmado, el enlace muestra la pantalla de solo lectura. No permite modificar la respuesta.
- **Idempotencia**: si el cliente reintenta enviar (por error de red, por ejemplo), el sistema devuelve éxito con los datos ya guardados en lugar de crear un duplicado.
---
### PDF y visualización del documento
No se usa ninguna librería de generación de PDF en el backend. El documento es la página `ver_formulario_enviado.php` con estilos `@media print`. El usuario puede imprimirla o guardarla como PDF directamente desde el navegador.
**Contenido del documento impreso:**
- Encabezado con logo, nombre, subtítulo, datos de contacto y color corporativo
- Datos del paciente (nombre, documento, fecha de nacimiento, teléfono, EPS)
- Respuestas del formulario campo por campo, en el orden del esquema
- Imagen de la firma del paciente
- Firma del profesional (si fue completada)
- Sello SHA-256 con link para verificar autenticidad
- Pie de página con fecha de generación e ID del documento
**Formas de acceder al documento:**
| URL | Quién puede acceder |
|---|---|
| `ver_formulario_enviado.php?id=X` | Admin (cualquier formulario) o Enfermero (solo los que él envió). Requiere sesión. |
| `ver_formulario_enviado.php?t=TOKEN` | Cliente u cualquier persona con el enlace. Sin sesión. Solo si el estado es `firmado` o `completado`. |
---
### Sello de integridad SHA-256
Al guardar la respuesta del cliente, el sistema genera un hash SHA-256 que vincula de forma única el contenido del formulario con la firma y el momento en que se completó.
**Construcción del hash** (en `Formulario::guardarRespuesta()`):
```
SHA-256 de:
JSON de los datos del cliente
+ firma SVG/PNG del paciente
+ ID interno del envío
+ token del enlace
+ timestamp del momento de registro
```
**¿Para qué sirve?** Cualquier persona con el hash puede ir a `verificar_formulario.php?h=HASH` para confirmar que:
- El documento existe en la base de datos.
- El nombre del formulario y del paciente.
- La fecha en que fue completado.
- El estado actual (firmado / completado).
Si el documento fue alterado, el hash no coincidirá y la verificación fallará.
---
### Firma del profesional
Algunos formularios requieren que un profesional de salud también firme el documento, además del paciente.
**Flujo:**
1. Al diseñar la plantilla en el builder se agrega un campo `tipo: firma_profesional` en la posición deseada.
2. Cuando el cliente llena el formulario en `form_cliente.php`, ese campo muestra solo un aviso: *"Uso exclusivo del profesional de salud"*. El cliente no puede interactuar con él.
3. Una vez que el cliente ha completado y enviado el formulario, el admin o enfermero abre `ver_formulario_enviado.php?id=X` con sesión activa y verá el canvas de firma en esa posición.
4. El profesional dibuja su firma y hace clic en "Guardar firma".
5. La firma se guarda mediante POST a `api/lab/firmar_profesional.php`.
**Validaciones en el servidor:**
- Requiere sesión activa (`isUserLoggedIn()`).
- Si el usuario es enfermero, solo puede firmar en formularios que él mismo envió.
- Valida que el `campo_id` corresponde a un campo `tipo: firma_profesional` en el esquema del formulario.
- Valida que la imagen enviada sea un data URI de imagen válido.
> Si el formulario se accede via `?t=TOKEN` (cliente público), el canvas **no aparece**. En su lugar se muestra un aviso *"Pendiente de firma del profesional"* (solo visible en pantalla, no en el PDF impreso).
En `conversations.php`, los mensajes de imagen entrantes tienen un botón **<i class="fas fa-flask"></i>** (verde) en las acciones del mensaje. Al hacer click:
1. Se abre un modal con la imagen adjunta
2. El operador busca o selecciona un paciente (o usa el contacto de la conversación)
3. Completa datos opcionales (médico, exámenes, ayuno)
4. Se crea la orden en estado `pendiente`
---
## Exportaciones CSV
Disponibles desde `lab_reportes.php`:
- **Órdenes médicas** del período — incluye estado, médico, exámenes
- **Domicilios** del período — incluye enfermera asignada, dirección, estado
- **Pacientes** — catálogo completo con total de órdenes
---
## Requisitos
- PHP 8.2+
- MariaDB 10.11+ (o MySQL 8+)
- Bootstrap 5.3 (ya incluido en el sistema)
- Font Awesome 6.4 (ya incluido en el sistema)
-`uploads/media/` con permisos de escritura (755/775)
Ver `README_DOCS.md` para saber cómo se organiza y cómo agregar páginas.
"ALTER TABLE lab_pacientes ADD COLUMN IF NOT EXISTS origen VARCHAR(20) NOT NULL DEFAULT 'manual' COMMENT 'Origen: manual | lab | whatsapp' AFTER notas_admin",
"UPDATE lab_pacientes SET origen = 'whatsapp' WHERE user_id IS NOT NULL AND origen = 'manual'",
"CREATE INDEX IF NOT EXISTS idx_pac_origen ON lab_pacientes (origen)",
"contenido": "Autorización voluntaria para realizar la prueba presuntiva para VIH (Decreto 1543/97 del Ministerio de Protección Social por el cual se reglamentan los mecanismos de prevención, diagnóstico, manejo y reporte epidemiológico de la infección por VIH)",
"flujoLibre": false
},
{
"id": "_z7pv612",
"tipo": "fecha_hoy",
"label": "Fecha",
"required": false
},
{
"id": "_afsycwa",
"tipo": "parrafo",
"contenido": "Qué es el síndrome de inmunodeficiencia adquirida (SIDA)? \nEs una enfermedad producida por un virus conocido como el virus de inmunodeficiencia Humana (VIH), el cual infecta y destruye las células del sistema inmune, originando una falla progresiva y grave en los sistemas de defensa del organismo el cual queda expuesto a la infección y ciertos tipos de tumores. \n\n¿Cómo se adquiere la enfermedad? \nLa enfermedad se adquiere principalmente por contacto sexual con personas infectadas con el VIH. Por exposición a la sangre y a ciertos productos derivados de la misma contaminados con el virus. Además, durante el embarazo, las madres infectadas con el virus de inmunodeficiencia humana pueden transmitir la infección al feto a través de la placenta. \n\n¿Cómo se hace el diagnóstico de la infección? El diagnóstico se hace mediante una prueba de sangre que busca anticuerpos producidos por el organismo contra el virus. Existen dos clases de pruebas de laboratorio. Presuntivas, que pueden indicar una posible infección, y las pruebas confirmatorias, las cuales se hacen únicamente en caso de que la prueba presuntiva de positiva. \n\n¿Cuál es el procedimiento que el laboratorio debe realizar en el análisis de la prueba? El laboratorio procesa la muestra y en caso de un resultado presuntamente positivo o inconcluyente, realiza un segundo examen con otra muestra. En caso de que la segunda muestra también arroje un resultado presuntamente positivo, el resultado de la prueba se reporta como reactivo. En caso de que la segunda muestra no confirme los resultados de la primera muestra, las dos muestras se envían a un tercer laboratorio, con el fin de que sea procesada en él. Los resultados de este tercer laboratorio se toman como definitivos para decidir el reporte como Reactivo o Negativo. En todo caso, los costos por estas pruebas son asumidos por cuenta del laboratorio. \n\n¿Cómo se debe interpretar el resultado de la prueba? La prueba inicial, como ya se anotó, es apenas una prueba presuntiva, y por lo tanto, el hecho de salir reactiva no implica que usted tenga SIDA, o esté infectado por el virus. Este resultado debe ser confirmado mediante una prueba llamada Western Blot, con el fin de eliminar posibles falsos positivos de la prueba presuntiva. Para este segundo examen es importante tomar una nueva muestra y procesarla asumiendo usted los costos. Aún si esta segunda prueba confirma la presencia de anticuerpos para el VIH, esto no significa que usted tiene SIDA, pues existe un período de la enfermedad, controlable, en el cual los pacientes tienen anticuerpos contra el VIH, pero no tienen síntomas de la enfermedad, y pueden, inclusive, no desarrollar jamás la enfermedad. Lo que es muy urgente, es consultar con un médico para que se determine el estado de su enfermedad, y se inicie el tratamiento apropiado. \n\nEl resultado será entregado personalmente, previa identificación. ",
"flujoLibre": false
},
{
"id": "_wsqqoiu",
"tipo": "parrafo_inline",
"contenido": "Yo, {nombre_completo}, con N.º de identificación {numero_documento}, declaro que fui informado (a) sobre el examen de anticuerpos contra el VIH que me será practicado el día de hoy, he recibido asesoría, me han explicado en que consiste; el procedimiento y sus implicaciones en mi vida, y la confidencialidad con la que se manejara las información que he dado y que se obtendrá. YO COMPRENDO Y AUTORIZO LA REALIZACIÓN DE LA PRUEBA DE FORMA LIBRE Y ESPONTÁNEA, en el Laboratorio Clínico Ximena Caicedo Empresa Unipersonal."
},
{
"id": "_vihsep1",
"tipo": "separador",
"label": "AUTORIZACIÓN Y FIRMA"
},
{
"id": "_vihresp",
"tipo": "texto",
"label": "En caso de menor o incapacitado: nombre y parentesco del responsable",
"required": false
},
{
"id": "_vihfpac",
"tipo": "firma",
"label": "Firma del paciente / responsable",
"required": true
},
{
"id": "_vihsep2",
"tipo": "separador",
"label": "USO EXCLUSIVO DEL PROFESIONAL"
},
{
"id": "_vihfpro",
"tipo": "firma_profesional",
"label": "Firma del profesional de salud"
}
]';
INSERTINTOlab_formularios(
nombre,
descripcion,
categoria,
esquema,
permite_firma,
requiere_firma,
doc_encabezado,
doc_subtitulo,
doc_color,
tipo,
is_principal,
is_active,
version,
creado_por
)VALUES(
'CONSENTIMIENTO INFORMADO VIH F-LAB-05 V.5',
'Autorización voluntaria para prueba presuntiva de VIH — uso turnero',
'consentimiento',
@esquema_vih,
1,
1,
'XIMENA CAICEDO G. E.U',
'Laboratorio Hematológico',
'#a0a59c',
'consentimiento',
0,
1,
5,
1
);
-- Asignar a todos los puestos de toma de muestras activos
-- Segunda parte del cambio de identidad de WhatsApp (ver 20260811_bsuid_identidad_whatsapp.sql).
--
-- Meta manda el BSUID en TODOS los webhooks de mensaje, también en los que aún
-- traen teléfono. Eso permite guardar la equivalencia BSUID↔teléfono mientras
-- la persona todavía muestra su número, de modo que el día que lo oculte
-- siga siendo reconocible: ya sabemos quién es.
--
-- El histórico de webhook_logs tiene 6.423 equivalencias que se cargan con
-- scripts/backfill_bsuid.php.
--
-- Se guarda en una columna aparte y no en phone_number porque son dos cosas
-- distintas: el BSUID identifica, el teléfono además sirve para cruzar con el
-- paciente y el turnero. Mezclarlos rompería esos cruces.
ALTERTABLEusersADDCOLUMNbsuidVARCHAR(32)NULLDEFAULTNULLCOMMENT'Identificador de usuario por empresa (Meta). Presente aunque la persona oculte su teléfono.'AFTERphone_number;
-- Único: un BSUID identifica a una sola persona dentro del portafolio.
-- Admite varios NULL, que es el caso de todos los usuarios ya existentes.
$stmt=$fb->query('SELECT CODPROTOCOLO,NOMITEM,TIPOSEXO,TIPO,MEDIDA,ABREVITEM,VMINREF,VMAXREF,ORDEN,FORMULA,CUPS FROM ITEM ORDER BY CODPROTOCOLO, ORDEN');
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.