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>
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>
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>
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>
- 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>