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