Panel de soporte
El panel de soporte es la interfaz principal del módulo de Chat en el frontend (/app/chat/). Funciona como hub central de operaciones, integrando chat, tickets, encuestas, usuarios, propiedades y analytics.
Sidebar principal (8 items)
La navegación lateral organiza las herramientas por frecuencia de uso diario:
Usuarios y Propiedades están promovidos desde Settings al sidebar principal para facilitar flujos diarios de soporte (buscar casa → copiar enlace encuesta → enviar por chat).
Vistas principales
Lista de canales
Ruta:/app/chat/
Panel lateral redimensionable (320px por defecto, rango 240-600px) que muestra la lista de canales con:
- Scroll virtualizado con paginación infinita (50 canales por página)
- Filtros: por propiedad, agente, tipo de canal
- Búsqueda de mensajes a través de canales
- Conteo de no leídos en tiempo real por canal
- Marcar como no leído: menú contextual (kebab, visible al pasar el cursor) en cada canal que lo devuelve al estado no leído (badge = 1), persistente tras refrescar y solo para la agente que lo marca. Usa
markUnreadnativo de Stream víaPATCH /chat/channels/:channelId/mark-unread. Solo disponible para agentes con identidad de chat propia (no para viewers, que comparten la identidadvivla-admin). - Marcar todas leídas: botón en los controles del listado (solo visible para moderadores/admin) que limpia el estado de no leído del agente en todos sus canales de una sola vez, vía
POST /chat/channels/mark-all-read. El estado de lectura en Stream es por miembro, así que solo mueve los contadores de quien lo pulsa; la bandeja del resto del equipo no se toca. Se rechaza a los viewers (comparten identidad de Stream y arrastrarían a todo el equipo), con el mismo predicadocanAutoMarkReadAsSelfque protege el auto-clear de lectura. Además, archivar un canal lo marca como leído para quien archiva (best-effort, por usuario): así un canal archivado con no leídos pendientes no descuadra el contador de la pestaña. - Pestañas para diferentes vistas de canales
- Asistente de creación de canales (wizard multi-paso)
Resolución de conversaciones (VIV-2461)
Los canales de tipo property y booking tienen un estado de resuelto (resolved_at + resolved_source) que gobierna un eje propio del listado, independiente de no-leídos u orden:
- Por defecto los canales resueltos desaparecen de la bandeja (no solo bajan al final). Los canales vacíos (sin ningún mensaje todavía) también se ocultan y reaparecen en cuanto alguien escribe.
- El control Resueltos de la cabecera (
resolvedOnly) es el espejo: muestra solo los canales resueltos (modo auditoría de CX). No se aplica cuando hay un filtro de tipo de canal explícito ni en las pestañas Archivados / Necesita respuesta / Lola.
ChannelResolutionService decide por canal a partir del último mensaje real y guarda el origen en chat_channels.resolved_source:
Un canal con un ticket abierto nunca se auto-resuelve (falla en cerrado ante cualquier duda), y los mensajes salientes clasificados como
awaits_owner/owes_owner dejan el canal abierto. Los veredictos de IA se cachean en chat_message_logs (conversation_closed, outbound_expectation) — migración 234.
Conversación
Ruta:/app/chat/$channelId
Vista principal de la conversación con:
- Lista de mensajes con scroll virtualizado
- Búsqueda en conversación con navegación entre resultados
- Sidebar de respuestas rápidas (quick replies), gestionadas en
/app/chat/settings/quick-replies(título, mensaje y slug editables — el slug es la clave que usa el disparador/, única dentro del grupo) - Modo de selección de mensajes para crear tickets o agregar a tickets existentes
- Sidebar lateral con metadata del canal (propiedad, booking, información del huésped)
- En canales asociados a una propiedad o reserva se muestra esa casa con el desplegable Información de la casa (WiFi, acceso, alarma, parking).
- En canales sin propiedad propia (novedades, feedback, incidencias), si el usuario principal tiene casas se listan todas bajo Propiedades de nombre, cada una con su mismo desplegable de información, para conservar contexto en cualquier canal.
- En canales de propiedad, la tarjeta Onboarding deja a CX generar a mano el enlace de la encuesta
onboarding-review(scopeproperty, sin auto-envío) y copiarlo para entregarlo donde haga falta. Una vez el propietario completa el onboarding de esa casa, el botón se sustituye por un tic verde y ya no se regenera; el estado sale deGET /surveys/tokens/completion({ completed }). - En canales de reserva, la sección Reservas asociadas lista todas las reservas del canal (la Principal marcada con estrella) y permite añadir o quitar reservas (rol
editor). Solo se asocian reservas del mismo cliente y misma casa que aún no tengan canal propio; la principal no se puede quitar aquí. Endpoints:GET/POST /chat/channels/:id/bookingsyDELETE /chat/channels/:id/bookings/:bookingId. La relación vive en la tablachannel_bookings(una reserva pertenece como mucho a un canal);chat_channels.booking_idsigue siendo la reserva principal que gobierna elcidde Stream, el display por defecto y los filtros de etiquetas.
- Seguimiento de estado de lectura, con acción Marcar como no leído en la cabecera (junto a Archivar): deselecciona el canal para que
stream-chat-reactno lo vuelva a marcar leído y el badge reaparezca en la lista. Los tics de entrega/lectura se pintan en todos los mensajes salientes del equipo (los de la derecha: propios, de un compañero o de la IA), no solo en los propios: informan de si el propietario recibió/leyó el mensaje, así todo CX ve que llegó. Los mensajes del propietario van a la izquierda y nunca llevan tic. - Reacciones: el selector ofrece el catálogo completo de emojis (emoji-mart) desde el menú de opciones del mensaje, no solo los seis clásicos. Cada reacción se envía con el propio glifo del emoji como
type(codificado por codepoints); las seis reacciones legacy (❤️ love, 👍 like, 😂 laugh, 😮 wow, 😢 sad, 🙏 pray) siguen resolviéndose a su emoji por alias para el histórico de Stream. Las píldoras se construyen por mensaje desde susreaction_counts(DynamicReactionsList+buildReactionOptions), así se pinta cualquier reacción que llegue —alias o emoji nativo, incluidas las enviadas desde vivla-mobile— y untypeirresoluble cae a un glifo neutro (🔘) en vez de desaparecer. El picker excluye los emojis cuyotypecodificado superaría el límite de 30 caracteres de Stream (familias ZWJ multipersona), que fallarían al enviarse. - Editar mensaje propio: los agentes con capability
update-own-messagepueden editar sus propios mensajes de texto mediante un editor inline desde el menú de opciones; los mensajes editados muestran el indicador (editado). El mismo menú incluye copiar texto, reaccionar, crear/añadir a ticket y eliminar. - Selector de emojis en el input: se abre desde el botón de emoji o escribiendo
:al inicio de una palabra; al elegir un emoji se reemplaza el token:querydel textarea. - Negrita con
Cmd/Ctrl+B: aplica el marcador**...**a la selección del input (composer y edición de mensaje comparten la misma función,applyMarkdownFormatenapp/lib/chat/message-formatting.ts). Es toggle (si ya está en negrita, la quita) y en una selección de varias líneas aplica el marcador línea a línea en vez de envolver el bloque entero (el Markdown no cruza saltos de línea) — las líneas en blanco de por medio se dejan tal cual, nunca quedan****. Sin selección, inserta****con el cursor en medio. - Selector de respuestas rápidas con
/(solo escritorio): al escribir/al inicio de una palabra se abre un popover filtrable por slug o título (app/lib/chat/slash-quick-replies.ts, espejo del disparador:del emoji). Se navega con las flechas, se confirma conEnter/Taby se cierra conEscape; al confirmar se sustituye el token/querypor el contenido de la respuesta ya interpolado ({{propertyName}},{{dateRange}}, etc. — misma resolución que la sidebar,useResolvedQuickReplies). El móvil mantiene la sidebar de respuestas rápidas existente en vez de este popover. - Salto a mensaje (jump-to-message)
- Indicador «Lola está respondiendo» (
LolaRespondingIndicator, solo panel CX): escucha el evento Stream a medidalola_respondingque emite el backend (AiAgentTriggerService/AiAgentResponderService→StreamService.sendLolaRespondingState) durante el turno vivo de Lola y muestra su estado — fasewindow(“Lola responde en ~2 min”, tras aterrizar el mensaje del propietario y abrirse el debounce de 120 s) ogenerating(“Lola está escribiendo…”, al arrancar el stream del concierge);idle(turno cerrado) lo oculta. Es efímero y auto-sanable: un TTL de cliente lo esconde aunque se pierda el eventoidle, así nunca se queda encendido. Solo lo renderiza el panel — ni la app del propietario ni la Expo de CX escuchan el evento, así que no se filtra nada al propietario.
Tickets
Ruta:/app/chat/tickets/
Panel de administración de tickets con:
- Filtros por estado, prioridad, propiedad y agente, que se intersectan (AND) entre sí
- Filtros rápidos Asignados a mí / Creados por mí (por
users.idinterno) - Búsqueda server-side (
search) sobre título, descripción y número de ticket de Zendesk (case-insensitive), con debounce - Estado persistido en la URL: los filtros, la búsqueda y la página se guardan en los search params, así que sobreviven al entrar en el detalle de un ticket y volver, y la URL es compartible
- Paginación (100 tickets por página)
- Badges de prioridad y estado
- Vista detallada por ticket (
/app/chat/tickets/$ticketId) con comentarios
Usuarios
Ruta:/app/chat/users/
Vista de usuarios promovida desde Settings al sidebar principal. Incluye lista de usuarios con vista detallada por usuario (/app/chat/users/$userId).
Propiedades
Ruta:/app/chat/properties/
Vista de propiedades promovida desde Settings al sidebar principal. Muestra un grid de tarjetas con imagen de portada, nombre, ubicación, selector de agente asignado y botón “Ver NPS” que navega a /app/chat/surveys/results?property={id}. Soporta modo batch para asignaciones masivas.
Encuestas
Ruta:/app/chat/surveys/
Hub de encuestas integrado en el panel de soporte. Incluye 5 sub-secciones con navegación propia:
- Actividad
- Administrar
- Resultados
- Planes de Acción
- Rewards
/app/chat/surveysTabla de respuestas con filtros (búsqueda, tipo encuesta, destino, rango de fechas con DateRangePicker), quick actions (generador inline de deep links, resumen de rewards) y exportación CSV.Configuración (Settings)
Todas las rutas bajo/app/chat/settings/. Reducido a 4 secciones (Usuarios y Propiedades promovidos al sidebar principal):
- Agentes
- Canales
- Turnos
- Sincronización
/app/chat/settings/agentsGestión de agentes de soporte: crear, editar, quitar y asignar a canales.- Crear / convertir: el modal permite crear un agente nuevo (con invitación por email) o convertir a un usuario existente. El buscador del picker consulta todos los usuarios por nombre o email (con debounce), sin filtrar por tipo, de modo que también aparece el personal de Vivla que solo tiene usuario
client(creado por el login de la app). Las opciones de tipoclientse marcan con un badge Cliente. Solo se ofrecen los roles Moderador y Administrador: el rolviewerse retiró de esta superficie porque elCHECKde la tablauserssolo admiteadmin/moderator/usery guardarlo fallaba. - Quitar agente: baja el rol de la persona a
user, así deja de ser asignable a canales y tickets y pierde los permisos de agente CX. Conserva su usuario y su historial de chat (no se borra el usuario en Stream ni Auth0) y se le puede volver a convertir en agente más adelante. No puedes quitarte a ti mismo, ni quitar al último administrador de chat. El diálogo incluye “Quitar también de todos los canales” (activado por defecto): al confirmarlo se le saca como moderador de todos los canales no archivados que atendía (DELETE /chat/users/agents/:id?removeFromChannels=true). Los canales donde es el agente activo o por defecto se respetan y se listan en el aviso: reasígnalos primero y vuelve a quitarlo. El toast de éxito informa de cuántos canales se limpiaron y cuántos se saltaron.
News (Notificaciones embebidas)
Sección de broadcast y notificaciones integrada en el módulo de chat, bajo/app/chat/news/:
Analytics
Ruta:/app/chat/analytics
Métricas de uso del chat: actividad por canal, por usuario y estadísticas generales. La vista se divide en dos pestañas: Resumen (insights, adopción, reservas y totales — lo que sigue) y Automatismos (ver más abajo).
Análisis de conversaciones (insights)
Endpoint:GET /chat/analytics/insights (rol viewer en tool-chat), acepta startDate/endDate ISO (por defecto últimos 30 días).
Bloque de métricas granulares calculadas en SQL sobre la capa semántica analytics.* (vistas v_chat_activity, v_chat_sessions, v_client_health) vía ChatInsightsService. Si ANALYTICS_DATABASE_URL no está configurada, degrada a available:false (todos los campos a cero) y la UI muestra un aviso en vez de fallar. La sección InsightsSection del dashboard pinta:
- Tarjetas resumen: propietarios activos (segmentados estrenando ≤90 días / veteranos por
lifecycle_effective), mensajes del período por rol (CX / IA Lola / propietario-huésped), conversaciones (sesiones abiertas vs. cerradas) y % de mensajes con adjunto. - Tiempos de respuesta de CX (
ResponseTimesCard): mediana, p90 y media recortada al p90 de los huecos propietario→CX, más el % respondido en menos de 60 min (solo huecos ≤ 24 h). - Mezcla por rol (
RoleMixChart): serie diaria apilada CX / IA / cliente. - Mapa de calor de actividad (
ActivityHeatmap): mensajes por día de la semana × hora (Europe/Madrid). - Longitud de conversación (
ThreadLengthChart): distribución de sesiones por turnos (buckets 1-2, 3-5, 6-10, 11-20, 20+) con media y mediana. - Salud de sesiones (
SessionsHealthCard): abiertas/cerradas y desglose por sentimiento y urgencia.
Las reacciones y las respuestas citadas aún no se miden: viven solo en Stream y el webhook no las persiste (requiere instrumentación).
Adopción (lente de casas activas + plan de despliegue CX)
Endpoint:GET /chat/analytics/adoption (rol viewer en tool-chat).
Complementa las métricas globales con una lente centrada en las casas activas y su avance frente al plan de lanzamiento de CX:
- Cabecera Personas / Canales (
AdoptionPeopleCard+AdoptionChannelsCard) — fila de dos tarjetas que encabeza la sección. Personas muestra el titular de owners de casa activa con chat sobre el total, más segmentos por tipo (owners, visitors, guests y todos) y una tabla al pie (equipo porchat_role, internos y total de usuarios). Los usuarios internos (user_type=internalo email de dominio corporativo) se excluyen de todos los segmentos de personas, porque hay decenas de compañeros registrados comoclient_type=ownerque inflarían las cuentas. Canales muestra activos sobre el total y un desglose por tipo (property, booking, issues, feedback, news) con partición activo/archivado allí donde el tipo tiene archivados (hoy solo booking). El backend calcula ambos bloques enadoption.service.ts(people/channelsenAdoptionResponseDto). - Casas activas con canal — casas con
status = active(no borradas) que tienen un canal de tipopropertyno archivado, sobre el total de casas activas. - Owners activados — owners de al menos una casa activa (deal
active) que tienen canalpropertypara una de sus casas, sobre el total de owners de casas activas. - Owners sin canal — owners de casas activas todavía sin canal; el desglose muestra las invitaciones de chat pendientes.
- Uso real — media de mensajes por canal
propertyde casas activas y mensajes por rol (equipo vs owners) en los últimos 30 días. - Cobertura por destino — casas con canal por destino, con detalle expandible de owners activados por casa.
- Adopción vs plan — curva “esperado” (plan) frente a “real” (activados hoy), con hitos y el delta (+ por delante / − por detrás).
apps/backend/src/chat/analytics/launch-plan.constant.ts, que replica a mano alma.vivla.ai/plan.html (datos a 9 jul 2026). No se scrapea: hay que actualizarlo manualmente cuando cambie el plan. El lado “real” se mide en vivo desde la base de datos en adoption.service.ts.
Reservas próximas (cobertura de estancias, 4 semanas ISO)
Endpoint:GET /chat/analytics/reservations (rol viewer en tool-chat).
Separa dos conceptos que antes se mezclaban y da un desglose objetivo por tipo de reserva. Horizonte rodante de 4 semanas ISO desde el lunes de la semana actual (Europe/Madrid); cada estancia se ubica por su check-in (start_day dentro de [lunes, domingo]), no por solapamiento. La fuente son las reservas de v2 Postgres (bookings/stays/guests, mismo pool que device tokens vía DEVICE_TOKENS_DATABASE_URL, solo lectura) cruzadas con los canales y usuarios de tools (Supabase).
- Propietario activado = tiene su set completo de canales (
property+issues+news+feedback), no archivados. Es un eje distinto del canal de reserva. - Canal de reserva = cada estancia necesita su canal de tipo
booking(chat_channels.booking_id == bookings.old_bid, no archivado), aunque el propietario ya esté activado. - Cruce v2 ↔ tools por
email(lowercase) otools.users.vivla_user_id == v2 users.id.
types uniforme por tipo (enjoy, rent, exchange, thirdhome), donde cada TypeBreakdown es una partición que suma a su total:
conCanal— el ocupante es usuario Vivla y el canal de reserva ya existe (listo).porCrear— el ocupante es usuario Vivla pero falta el canal de reserva (accionable).externos— el ocupante NO es usuario Vivla (no accionable). Enenjoysiempre es 0.propietarioSinActivar— soloenjoy: el propietario ni siquiera tiene el set completo de canales. En el resto es 0.
enjoy— ocupante = el propietario (bookings.owner).conCanal= owner activado (set completo) y canal de reserva existe;porCrear= owner activado sin canal;propietarioSinActivar= owner no activado.rent/exchange/thirdhome— ocupante real =guests(role='main').user. Si matchea un tools user (email ovivla_user_id) → activable:conCanalsi el canal existe, si noporCrear. Si no matchea →externos.
totalPorCrear— suma detypes.*.porCrear(el “N canales por crear” del header).propietariosSinActivar— =types.enjoy.propietarioSinActivar(el “M propietarios por activar”).gaps— lista accionable (quién / casa / tipo / motivoowner_sin_activar|falta_canal_reserva); se mantiene en el DTO aunque la UI actual no la pinte. Las cuentas test (@vivla.) se excluyen de gaps y de todos los conteos.
ReservationsWeekCard pinta por tipo una barra segmentada (emerald #10b981 con canal · amber #f59e0b por crear · grey #d4d4d8 externos · rose #f43f5e propietario sin activar) con su leyenda; enjoy/rent/exchange siempre, thirdhome solo si tiene reservas.
La aritmética vive en apps/backend/src/chat/analytics/reservations-v2.service.ts. Si el pool v2 no está configurado, el endpoint degrada a una respuesta vacía coherente (v2Configured: false, 4 semanas a cero) en vez de fallar.
Automatismos (panel de control)
Endpoint:GET /chat/analytics/automations (rol viewer en tool-chat), acepta startDate/endDate ISO (por defecto últimos 30 días).
La pestaña Automatismos pinta una tarjeta por cada automation_flow (AutomationCard), agrupadas por namespace del disparador: Tickets (ticket.*), Encuestas (survey.*), Reservas / mensajes de chat (booking.*) y Otros. Cada tarjeta muestra:
- Enviados (
sent) — dispatches con estadosenten el período, con una barra de progreso frente a los eventos del disparador. - debían salir (
triggerEventCount) — número dedomain_eventsdeltrigger_eventen el período. En un flow con condiciones se etiqueta “eventos del disparador” porque es una cota superior, no un objetivo exacto. - fallidos (
failed) y estado (Inactivo / Sin fallos / N fallidos), con badge Condicionada si el flow tiene un nodocondition. - Enlace Ver eventos de este disparador al monitor de eventos (
/app/notifications/monitor?event_type=...).
Los envíos por push/email pueden estar subcontados: el motor se salta
recordDispatch cuando no hay destinatario resuelto (no_recipient); el canal chat es fiable. El aviso viaja en caveat y se muestra en el tooltip de cada tarjeta.El acceso a las secciones está controlado por el sistema de permisos. Los agentes con rol
editor en tool-chat pueden gestionar canales, tickets y encuestas. Las operaciones de configuración avanzada (tipos de canal, sync) requieren rol admin.