Commit Graph
248 Commits
Author SHA1 Message Date
Lizandro Guarnizo 726cbdd21a temp: debug script to inspect lab_formularios schema for F-LAB-28 2026-07-08 10:07:24 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 5109db4a4b feat: borrador de comentario en toma de muestras (localStorage)
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>
2026-07-08 10:02:06 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 3062cd7e95 fix: no mostrar consentimientos de toma de muestras al guardar en recepción
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>
2026-07-08 09:23:54 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 7b4827f70f fix: filtrar por sesión del día al llamar siguiente turno
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>
2026-07-08 09:14:48 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 8aa2f35585 fix(turnero/chat): recepcionista siempre vuelve a recepción, no a dashboard
IP registrada → su desk; sin IP registrada → primer desk activo de recepción.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-08 08:56:57 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 1f2198225c fix(turnero): IP real con X-Forwarded-For; chat back solo para recepcionista
- 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>
2026-07-08 08:53:42 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 6f56734b53 fix(turnero/chat): botón Menú regresa al desk asignado por IP
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>
2026-07-08 08:48:55 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 ea1f0e30c9 feat(turnero): tomas de muestras prolongadas con temporizador y progreso
- 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>
2026-07-08 08:42:43 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 cf8d011f09 feat(lab_examenes): múltiples consentimientos por examen
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>
2026-07-08 08:19:06 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 b2bc0f950a feat(turnero): múltiples consentimientos por tipo de examen
- 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>
2026-07-08 08:10:57 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 b6179574c3 feat(turnero): recepcionista con restricción de desk por IP
- 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>
2026-07-08 08:03:48 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 b2e7130762 feat(turnero): bacteriólogo sin dashboard/historial, redirige por IP
- 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>
2026-07-08 07:54:48 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 a776c5733c fix(turnero): origen_lugar_id calculado al vuelo, sin depender de columna DB
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>
2026-07-07 23:19:46 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 bad3d42a92 feat(turnero): gestión de dispositivos/tablets en configuración
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>
2026-07-07 23:08:33 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 fb0e77523c feat: módulo lab_examenes — CRUD catálogo de exámenes, items y tarifas
- 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>
2026-07-07 23:06:54 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 229977785b fix(turnero): no mostrar consentimientos de toma de muestras en recepcion
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>
2026-07-07 23:05:51 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 333e856f38 feat(turnero): buscador y paginación en tab de examenes
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>
2026-07-07 22:53:16 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 d24b234e30 fix(display): corregir turnos que no aparecen en pantalla TV
- 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>
2026-07-07 22:47:13 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 8760b685ad feat(turnero): dispositivos por IP, firmas, formularios y muestras prolongadas
- 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>
2026-07-07 21:58:52 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 4de9c2f9b4 turnero: simplificar URLs de pantalla TV y añadir URLs de gestión por lugar
- 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>
2026-07-07 10:34:32 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 f695b04f5b feat(caja): generar lab_recepciones al confirmar solicitud en turnero
- 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>
2026-07-04 22:12:48 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 a7aac706ee feat(recepcion): motor de precios + selector empresa/convenio
- 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>
2026-07-04 22:09:26 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 b9bb4fc628 feat(lab_empresas): módulo CRUD de empresas, convenios y tarifas
- 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>
2026-07-04 22:05:14 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 5fedd233f4 feat(lis): migración completa Firebird→MySQL + tracking de muestras en turnero
- 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>
2026-07-04 21:44:22 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 86ed70db34 fix(recepcion): corregir lectura de data-consent en Tom Select
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>
2026-07-04 16:00:46 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 56b8fc98df feat(recepcion): Tom Select para selección de exámenes con badge de consentimiento
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>
2026-07-04 15:54:36 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 c566c1f33f feat(recepcion): destacar turnos en atención en la cola
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>
2026-07-04 15:19:54 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 1d285da6ae feat(recepcion): mostrar tiempo de espera en tarjetas de cola
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>
2026-07-04 15:15:34 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 53b483d51a feat(turnero): UX audit improvements — lugar & recepcion
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>
2026-07-04 13:46:32 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 69a6859b51 style(turnero): alinear ficha-acciones de recepcion con lugar
- Eliminar display:flex de ficha-acciones (rompía el apilado de filas
  accion-primary-row / accion-secondary-row, quedaban side-by-side)
- Padding: 1rem 0 .25rem → .8rem 1.4rem .9rem (igual que lugar)
- Background: #fff → rgba(255,255,255,.97) + backdrop-filter:blur(6px)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-04 13:33:44 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 fb1cd2161c style(turnero): alinear botones de recepcion con lugar
- 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>
2026-07-04 13:28:45 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 54fc986d82 style(turnero): homologar UI/UX entre lugar y recepcion
lugar.php:
- Cola: 340px→370px, border 1.5px→1px, border-radius 11px, padding .65rem .9rem,
  min-height 58px→62px (touch target)
- Cola header: font-size .72→.78rem, letter-spacing .06→.08em
- btn-llamar-next: padding .6→.65rem
- ficha-sec: padding 1rem 1.1rem→1.2rem, header .72→.78rem, .07→.08em
- consent-row: padding .55→.5rem, min-height 46px añadido
- Transición mobile: .22s→.2s
- Modal paciente: header plano→gradiente azul (#1043a0→#1565c0)

recepcion.php:
- cola-card: border-radius 10→11px
- prio-dot: border-radius 50%→9px (cuadrado redondeado, consistente con lugar)
- btn-accion-sec: font-size .8→.82rem

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-04 12:57:50 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 400547bd7a style(turnero): ajustar tamaños de fuente en display_global
- Código turno: clamp(4→3rem, 14→13vw, 10→9rem)
- Nombre lugar: clamp(3→1rem, 10→8vw, 8→6rem)
- Nombre paciente: clamp(1.15→2.65rem, 2.1→3.6vw, 1.45→2.95rem)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-04 12:23:44 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 9afefcf3e7 fix(turnero): turno de recepcion 1 aparece en recepcion 2 en display
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>
2026-07-04 12:15:05 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 4f25cd6b59 fix(turnero): corregir anuncios de sonido en display_global
- 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>
2026-07-04 12:09:51 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 5af985581c feat(turnero): UI/UX tablet improvements for recepcion and lugar views
- recepcion: wider queue column (370px), breakpoint at 680px so portrait
  tablets get 2-column layout; prominent "Llamar siguiente" button in queue
  column header; searchable exam filter; touch-friendly action bar with
  large primary/secondary button rows; larger touch targets for cards,
  checkboxes and consent items
- lugar: wider queue column (340px), breakpoint at 680px; larger action
  buttons (.8rem padding, 1rem font) and secondary buttons; consistent
  mobile breakpoint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-03 18:55:07 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 b55c7c1a19 feat: mostrar número de orden en ficha del turno (lugar)
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>
2026-07-03 18:04:16 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 99bfaa4c2f fix: convertir audio webm → ogg con ffmpeg antes de subir a WhatsApp en turnero
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>
2026-07-03 17:56:15 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 a526a29a68 feat: botón Ver paciente con historial en vista lugar
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>
2026-07-03 17:55:51 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 282c372de3 fix: corregir reproducción de audio en chat turnero
- 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>
2026-07-03 17:50:43 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 53dc492b5e feat: auto-rellenar variable {{name}} con nombre del paciente en nuevo mensaje
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>
2026-07-03 17:48:02 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 e88fb4cc86 fix: mensajes automáticos del turnero registrados en canal principal
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>
2026-07-03 17:42:39 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 1bd799a501 fix: corregir envío de plantillas con variables nombradas en chat turnero
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>
2026-07-03 17:37:33 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 34c6d2e643 fix: documentos recibidos en chat devuelven 401
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>
2026-07-03 17:30:02 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 12c790fb5c fix: mover CSS del modal nuevo-mensaje fuera del media query mobile
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>
2026-07-03 17:28:08 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 f975844d9d fix: reacciones y audio en chat turnero
- 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>
2026-07-03 17:24:35 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 ab72750410 feat(chat): botón Nuevo mensaje con búsqueda de paciente y picker de plantilla
- 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>
2026-07-03 17:19:18 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 3d6147bf0d fix(template): send parameter_name for named-variable templates
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>
2026-07-03 15:04:31 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 a8380ddc81 debug(template): expose Meta error subcode/details, add canal meta
- 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>
2026-07-03 14:54:10 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 f60df038d8 fix(chat): capture template name/lang before closing modal in sendTemplate
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>
2026-07-03 14:47:31 -05:00