- 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>
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>
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>
- ver_formulario_enviado.php: pre-scan $_soloFirmaPro; muestra canvas pro cuando modoTurnero+embebido+sesión activa+sin firma paciente
- fpw-save JS: recopila campos del form y envía solo_profesional=true; al guardar muestra éxito y postMessage turneroFirmado
- firmar_profesional_consentimiento.php: si solo_profesional=true guarda datos_respuestas y marca estado=firmado
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Eliminar sec-form-embebido como sección independiente
- En modo embebido: fila de consentimiento muestra botón "Firmar" que abre modal directamente por token
- En modo link: botones "Firmar presencial" + "WA" como antes
- abrirModalConsentimientoPorToken(token) recibe token directo sin cache
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ver_formulario_enviado.php: usar opciones??options??items como fallback de claves del schema
- ver_formulario_enviado.php: marcar _esquemaTieneFirmaPaciente en cualquier campo firma (no solo sin SVG), evita mostrar firma paciente cuando schema solo tiene firma_profesional
- ver_formulario_enviado.php: modo embebido (?embed=1) con layout compacto para iframe
- lugar.php: pasar &embed=1 al iframe src
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- recepcion.php: quitar guard !solicitudActiva en enviarConsentimientosTodos (bloqueaba silenciosamente antes de guardar solicitud)
- send_consentimiento.php: eliminar requisito de solicitud y exámenes para poder reenviar; usar WhatsAppService('turnero') con rawComponents correctos (1 body param=codigo, button param=token); JOIN lab_pacientes también via t.paciente_id como fallback de teléfono
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Only display the 'Falta firma enfermero' row and button when the assigned
consent form actually contains a firma_profesional field in its esquema.
get_consentimientos.php now computes requiere_firma_profesional from the
JSON schema and recepcion.php gates the row on that flag.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Nueva columna turnero_lugares.formulario_modo (link|embebido)
- Config: radio en modal de edición para elegir el modo
- lugar.php: muestra iframe con ver_formulario_enviado al activar turno
- API get_consent_token.php: crea/obtiene token para turno+lugar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- wa_profile.php: GET carga perfil desde Graph API, POST guardar campos, POST foto sube imagen
- configuracion.php tab sesión: UI con foto de perfil editable, campos del negocio y botón guardar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Al crear turno busca el formulario configurado en el desk de recepción
- Crea token de consentimiento y lo marca como 'enviado'
- Envía plantilla consentimiento_turno con nombre real, nombre formulario y link
- Fallback texto plano incluye el link si está disponible
- Tipo F (solo muestra) sigue usando plantilla sin consentimiento
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
saveOutgoingMessage() now reads __app_meta.canal and stores it in conversations.
sendMediaById() accepts a $meta param and forwards it to saveOutgoingMessage.
chat_upload_media.php passes $extra (canal=turnero) to sendMediaById.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Capture activeUserId at the start of each async function (fetchMessages,
loadMoreMessages, pollMessages) and discard the response if the user changed
contacts while the request was in-flight.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>