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
| Archivo | Rol |
|---|---|
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.ts | LAST_USED_SCOPE_KEY y CONTACT_DETAILS_PANEL_OPEN_KEY |
core/helpers/global/presence.helper.ts | PRESENCE_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:
- 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 ordenmine,all,unassigned. Sin ningún permiso de alcance se queda enall. - Estado por defecto: el primero concedido en el orden de pestañas (
open,inProgress,pending,postponed,resolved,closed,cancelled); sin permisos quedaopen. - Despacha
TicketActions.loadTickets({ filter }).
buildBaseFilter arma el filtro con esta precedencia:
- Parte de
panelFilters(panel de filtros) y fuerzagroupByContact = true. - Si el panel no fija estado, usa la pestaña seleccionada.
- Si hay operadores elegidos, usa
assignedUserIdsy termina (anula el alcance). - Si el alcance es
unassigned, usaunassigned = true. - Para el alcance
mine, o para cualquier usuario de tipoagent, usaassignedUserIddel usuario actual; un agente con alcanceallañadeincludeUnassigned = 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 permisoTICKET_FILTER_SCOPE_*; si no hay ninguno, el desplegable no aparece. Al elegir uno se guarda enlocalStorage. - 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.
filteredConversationsfiltra las tarjetas ya cargadas por nombre ousernamedel contacto; no se envía al backend. Si la tarjeta seleccionada deja de ser visible, se limpia la selección y el parámetrochatId. - Filtros avanzados (
ChatFilterPanelComponent, botón de filtro con indicadorhasActiveFilters): secciones de etiqueta (una), contacto (búsqueda porusername, hasta 20), estado y periodo (createdFrom,createdTo, conminen la fecha final). Emite unChatFilterSelection(labelId,contactId,status,createdFrom,createdTo) que reemplazapanelFiltersy 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:
getSanitizedLastMessagesustituye 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, aplicarenderWhatsappMarkupy sanea el HTML conSanitizationService. - Indicadores: candado si
lockedBy. Una sola insignia lateral, por prioridad: número de no leídos (unreadCount), punto siagentUnreado, solo cuandoshowCheckBadge(la lista lo calcula confromType === 'User',toType === 'Player', usuario de tipoagentystate === 0), el acuse del último mensaje (failedcon icono rojo, check simple, doble check, doble check azul siread). 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),isOwnereisLockedByMecalculados 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ña | Contenido | Datos |
|---|---|---|
info | Sección Contacto (teléfono con botón de copiar) e Información adicional (metadatos con copia por ítem) | De la propia conversación |
history | Tickets previos del contacto con su estado; clic abre ese ticket (goToHistoryTicket) | GET /tickets/get-all con contactId, página 1, límite 20 |
starred | Mensajes destacados del contacto, 20 por página, con salto al mensaje | GET /starred-messages/by-contact/:contactId; se invalida con StarredMessageService.changed$ |
notes | Notas 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 conrightOffset; reabrir una existente la trae al frente. Cada una alojaapp-chat-conversationconhideHeader, 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): alojaapp-chat-conversationconhideHeader,forceReadOnly(banner y sin compositor) y barra de etiquetas. Se mantiene sincronizada con la lista (syncViewModal).
Pruebas
| Spec | Cubre |
|---|---|
chat-list.component.spec.ts | Pestañ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.ts | Punto de presencia, candado (y su estilo), insignia del agente asignado y su permiso |
chat-filter-panel.component.spec.ts | Retirada 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.
ChatListComponentguarda_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).showCheckBadgecomparafromTypecon'User'ytoTypecon'Player', peroticketToConversationfijafromTypealsendTypedel último mensaje (contact,agentosystem) ytoTypea 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.