Skip to main content

Lista de tickets (ChatListComponent)

Columna izquierda de la pantalla de Chat de tickets y contenedor de todo lo demás: hospeda la conversación activa, el panel de datos del contacto, las ventanas flotantes, y despacha las acciones del ticket. Es el componente más grande de la feature (chat-list.component.ts, unas 1800 líneas) y se apoya en el slice NgRx tickets (ver Tiempo real y estado). Los permisos y endpoints generales están en el índice; aquí solo se detalla lo propio de la lista.

Archivos​

ArchivoRol
components/chat/chat-list/chat-list.component.*Lista, filtros, selección, panel de datos del contacto, orquestación de acciones
components/chat/chat-list/chat-list-item/chat-list-item.component.*Tarjeta de un ticket
components/chat/chat-filter-panel/chat-filter-panel.component.*Panel de filtros avanzados
components/chat/chat-popout/chat-popout.component.*Ventana flotante de una conversación
components/chat/chat-view-modal/chat-view-modal.component.*Vista modal de solo lectura
core/helpers/ui/chat-panel.contant.tsLAST_USED_SCOPE_KEY y CONTACT_DETAILS_PANEL_OPEN_KEY
core/helpers/global/presence.helper.tsPRESENCE_STALE_MS (90 s) e isPresenceFresh

Carga y filtros​

ngOnInit conecta el socket (initSocketConnection), se suscribe al estado de tickets y a los eventos de socket, y llama a applyPermissionDefaultsAndLoadTickets, que espera a conocer los permisos del usuario antes de la primera carga:

  1. Alcance por defecto: el último usado guardado en localStorage (chat_last_used_scope) si el usuario aún tiene el permiso; si no, el primero concedido en el orden mine, all, unassigned. Sin ningún permiso de alcance se queda en all.
  2. Estado por defecto: el primero concedido en el orden de pestañas (open, inProgress, pending, postponed, resolved, closed, cancelled); sin permisos queda open.
  3. Despacha TicketActions.loadTickets({ filter }).

buildBaseFilter arma el filtro con esta precedencia:

  1. Parte de panelFilters (panel de filtros) y fuerza groupByContact = true.
  2. Si el panel no fija estado, usa la pestaña seleccionada.
  3. Si hay operadores elegidos, usa assignedUserIds y termina (anula el alcance).
  4. Si el alcance es unassigned, usa unassigned = true.
  5. Para el alcance mine, o para cualquier usuario de tipo agent, usa assignedUserId del usuario actual; un agente con alcance all añade includeUnassigned = true (ve su cola y los sin asignar).

La paginación es incremental: al llegar al fondo del contenedor (margen de 50 px) y si hay más páginas, despacha loadMoreTickets. Mientras carga por primera vez se muestran 6 tarjetas esqueleto. PAGE_SIZE (10 tickets por página) viene de core/helpers/global/ticket.contants.ts.

Pestañas de estado​

statusTabs define siete pestañas, de las que solo las dos primeras (VISIBLE_TABS = 2: Abiertos y En progreso) se muestran en la fila; el resto cae en un desplegable "+N". Elegir una del desplegable la intercambia con la última visible. Cada pestaña se gobierna con su acrónimo TICKET_FILTER_STATUS_* (getGatedTabPermission); la fila de pestañas y el selector de alcance solo se pintan cuando se ha resuelto la comprobación de permisos (chatTagRowReady$, con esqueleto mientras tanto), y el contador "+N" cuenta solo las pestañas permitidas.

Alcance, operador y búsqueda​

  • Alcance (mine, all, unassigned): desplegable con una opción por permiso TICKET_FILTER_SCOPE_*; si no hay ninguno, el desplegable no aparece. Al elegir uno se guarda en localStorage.
  • Operador (TICKET_FILTER_BY_AGENT): selector múltiple cuyo roster se pide una sola vez y de forma diferida (loadOperatorsOnce, GET /tickets/filter-agents). Anula el alcance y se mantiene al cambiar de pestaña.
  • Búsqueda: es solo local. filteredConversations filtra las tarjetas ya cargadas por nombre o username del contacto; no se envía al backend. Si la tarjeta seleccionada deja de ser visible, se limpia la selección y el parámetro chatId.
  • Filtros avanzados (ChatFilterPanelComponent, botón de filtro con indicador hasActiveFilters): secciones de etiqueta (una), contacto (búsqueda por username, hasta 20), estado y periodo (createdFrom, createdTo, con min en la fecha final). Emite un ChatFilterSelection (labelId, contactId, status, createdFrom, createdTo) que reemplaza panelFilters y recarga. Un estado elegido en el panel prevalece sobre la pestaña. La sección de usuarios asignados se eliminó a favor del filtro por operador.
  • Modo etiquetas (botón de la barra): muestra las etiquetas del contacto en cada tarjeta en lugar del último mensaje.
  • Refrescar limpia el texto de búsqueda y recarga.

Tarjeta (ChatListItemComponent)​

Muestra: nombre y avatar (o iniciales), punto de presencia (isOnline), hora relativa (getTimeAgo), departamento con su color (o "sin departamento"), icono del canal con tooltip del nombre de la conexión, vista previa del último mensaje y etiquetas.

  • Vista previa: getSanitizedLastMessage sustituye el cuerpo por un texto fijo para vista única, ubicación y contacto; para medios muestra un texto por tipo (mediaPreview.*). Antepone el autor (el contacto, "Tú" o el nombre del agente), quita la firma *Nombre*: del cuerpo, aplica renderWhatsappMarkup y sanea el HTML con SanitizationService.
  • Indicadores: candado si lockedBy. Una sola insignia lateral, por prioridad: número de no leídos (unreadCount), punto si agentUnread o, solo cuando showCheckBadge (la lista lo calcula con fromType === 'User', toType === 'Player', usuario de tipo agent y state === 0), el acuse del último mensaje (failed con icono rojo, check simple, doble check, doble check azul si read). Aparte, un reloj con el número de envíos programados pendientes del propio agente (scheduledMessages.badge).
  • Insignia del agente asignado: avatar o iniciales, solo con TICKET_VIEW_AGENT_AVATAR; se muestra en todos los estados del ticket.
  • Menús: botones (y menú "más") para abrir en ventana flotante y en vista de solo lectura, y la flecha desplegable que abre el menú de acciones con canTransfer (administrador o dueño del ticket), isOwner e isLockedByMe calculados por la lista.

Selección y ruta​

selectConversation fija la conversación, reinicia el panel de datos, avisa al NotificationCenterService (setActiveTicket), limpia los no leídos (ticketClearUnread) y, si estaba marcado como no leído por el agente, lo desmarca (markUnread con agentUnread: false). Después escribe ?chatId=<ticketId> en la URL sin recargar. Al iniciar, initializeRoutedConversation lee chatId: si la tarjeta no está en la lista pide el ticket con GET /tickets/get-one/:id (loadSpecificConversation). Si además llega highlightMessageId, _scheduleDeepLinkJump espera hasta 20 intentos de 100 ms a que exista la conversación montada y llama a jumpToMessage (falla en silencio si no aparece). En móvil (menos de 992 px) la conversación ocupa la pantalla y el botón de volver llama a closeConversation.

ticketToConversation convierte un Ticket en ChatConversation: userId pasa a ser el _id del ticket, otherParticipantId el del contacto, la presencia se evalúa con isPresenceFresh y se copian canal, departamento, asignado (con avatar), lockedBy y agentUnread. La lista se rehidrata cada 30 s (PRESENCE_REEVAL_MS) para que un contacto pase a desconectado sin esperar un evento.

Panel de datos del contacto​

Se abre desde la conversación (openDetailsPanel) y su estado abierto o cerrado se guarda en localStorage (chat_contact_details_panel_open); se restaura al abrir un ticket por ruta. Cuatro pestañas, cada una con su carga diferida y su caché por contacto (que se invalida al cambiar de contacto):

PestañaContenidoDatos
infoSección Contacto (teléfono con botón de copiar) e Información adicional (metadatos con copia por ítem)De la propia conversación
historyTickets previos del contacto con su estado; clic abre ese ticket (goToHistoryTicket)GET /tickets/get-all con contactId, página 1, límite 20
starredMensajes destacados del contacto, 20 por página, con salto al mensajeGET /starred-messages/by-contact/:contactId; se invalida con StarredMessageService.changed$
notesNotas internas, 10 por página (invertidas a orden cronológico)GET /messages/:contactId con messageType=note; se invalida con newNote$ del socket

El botón de editar (CONTACT_UPDATE) abre ContactEditModalComponent (de la feature de Contactos) con BootstrapModalService; al guardar llama a updateContact y assignLabels y recarga la lista. El teléfono se normaliza anteponiendo +.

Ventanas flotantes y vista de solo lectura​

  • Pop-out (ChatPopoutComponent): hasta 4 ventanas (MAX_POPOUTS) apiladas con rightOffset; reabrir una existente la trae al frente. Cada una aloja app-chat-conversation con hideHeader, su propio menú de acciones y botones de minimizar, abrir en pantalla completa (popoutToFull) y cerrar. Para resolver usa _resolveWithLabelGuard; devolver y eliminar cierran la ventana.
  • Vista de solo lectura (ChatViewModalComponent, overlay centrado con fondo oscuro): aloja app-chat-conversation con hideHeader, forceReadOnly (banner y sin compositor) y barra de etiquetas. Se mantiene sincronizada con la lista (syncViewModal).

Pruebas​

SpecCubre
chat-list.component.spec.tsPestaña de destacados (carga, paginación, vista previa, salto), eliminar ticket con confirmación, bloquear y desbloquear, guardia "desbloquea primero" al transferir o devolver, isTicketOwner e isLockedByCurrentUser, mapeo de lockedBy y avatar, pestaña de notas (incluida invalidación por socket), persistencia del panel de datos, tick de presencia, resincronización tras reconexión, hidratación de pendientes programados, syncPopouts, estructura de la pestaña de información, visibilidad del filtro por operador, permisos de pestañas y de alcance, precedencia de buildBaseFilter, valores por defecto según permisos y persistencia del alcance
chat-list-item.component.spec.tsPunto de presencia, candado (y su estilo), insignia del agente asignado y su permiso
chat-filter-panel.component.spec.tsRetirada del selector de usuarios y emisión del resto de la selección

No hay specs de chat-popout más allá del enlace con el menú de bloqueo (ver Acciones del ticket) ni de chat-view-modal.

Gotchas​

  • La búsqueda de la lista no consulta al backend: solo filtra lo ya cargado.
  • Los acrónimos de pestañas y alcance son solo un interruptor de pantalla; no restringen lo que el backend devuelve.
  • ChatListComponent guarda _handleResolveWithContactReason, que abre el modal de motivo de contacto según el departamento, pero ningún flujo lo invoca; resolver pasa por _resolveWithLabelGuard (ver Acciones del ticket).
  • showCheckBadge compara fromType con 'User' y toType con 'Player', pero ticketToConversation fija fromType al sendType del último mensaje (contact, agent o system) y toType a una cadena vacía; con ese mapeo la condición no se cumple y el acuse de la tarjeta no llega a pintarse en esta rama.
  • La etiqueta del alcance seleccionado (getSelectedScopeLabel) está escrita en inglés fijo (Mine, Unassigned, All) y los toasts de asignación automática en español fijo, sin pasar por i18n.