Skip to main content

Acciones del ticket

Todo lo que un agente puede hacer sobre un ticket desde la pantalla de Chat de tickets: los 15 identificadores del menú (TicketMenuActionId) y las 54 acciones NgRx del slice tickets (TicketActions) que los respaldan. Cada acción se documenta con su efecto, endpoint, permiso (en el cliente y en el backend) y reglas. Los endpoints se verificaron contra ticket.controller.ts y ticket.service.ts del backend; ver también Ticket (su tabla de endpoints puede no listar estas rutas), Message y System message.

Flujo de una acción​

menú (tarjeta, cabecera o pop-out) -> TicketMenuActionId
-> ChatListComponent.onTicketAction (confirmaciones, guardia de bloqueo, paneles)
-> TicketActions.<accion> (NgRx)
-> TicketEffects -> TicketService (PATCH o DELETE /tickets/...)
-> <accion>Success -> reducer (quita o parchea la fila) + toast
<- eco del socket (ticket_status_changed, ticket_assigned, ticket_lock_changed, ticket_deleted, ticket_reactivated)

La lista (ChatListComponent) es quien interpreta el identificador: la tarjeta, la cabecera de la conversación y el pop-out solo emiten el TicketMenuActionId. Los errores genéricos (...Failure) los muestra actionFailure$ con el toast tickets.toast.error; el conflicto de bloqueo tiene tratamiento propio.

Archivos​

ArchivoRol
components/chat/ticket-actions-menu/ticket-actions-menu.component.*Menú desplegable (overlay CDK) con las opciones y su visibilidad
components/chat/chat-list/chat-list.component.tsonTicketAction, _resolveWithLabelGuard, _blockIfLocked, confirmaciones, paneles
components/chat/transfer-panel/transfer-panel.component.*Panel de transferencia
components/chat/postpone-dialog/postpone-dialog.component.* y .service.tsDiálogo de posposición (standalone)
core/states/ticket/ticket.actions.ts, .effects.ts, .reducers.ts, .selectors.tsAcciones, efectos, reducer y selectores
core/services/api/ticket.service.tsLlamadas HTTP
core/helpers/global/ticket.contants.tsPAGE_SIZE, TOAST, STATUS_LABELS, TICKET_ERROR_CODES

Visibilidad por contexto​

TicketActionsMenuComponent recibe ticketStatus, canTransfer, lockedBy, isOwner e isLockedByMe. Reglas del menú:

OpciónSe muestra siPermiso en el cliente
Resolver, Resolver sin despedida, DevolverEl ticket no está resolved, closed ni cancelledTICKET_UPDATE
Mantener conmigo (lock)Sin bloqueo (lockedBy vacío) y el usuario es el dueño (isOwner), además de lo anteriorTICKET_UPDATE
Liberar (unlock)Hay bloqueo y lo tiene el propio usuario (isLockedByMe), además de lo anteriorTICKET_UPDATE
TransferircanTransfer y lo anteriorTICKET_UPDATE
PosponerEstado distinto de postponed y lo anteriorTICKET_UPDATE
Cancelar posposiciónEstado postponed y lo anteriorTICKET_UPDATE
Marcar como no leídoSiempreNinguno
Motivos de contactoSiempreNinguno
Cambiar estadoEl botón siempre; los cuatro estados dentroTICKET_UPDATE sobre los estados
BorrarSiempreTICKET_DELETE

canTransfer lo calcula la lista: administrador (userType === 'admin') o dueño del ticket (assignedUserId igual al usuario). La tarjeta y el pop-out lo pasan; la cabecera de la conversación no lo enlaza y usa el valor por defecto (true), así que allí "Transferir" aparece siempre, y es el backend quien rechaza a quien no corresponda. El botón Resolver de la cabecera no tiene directiva de permiso. La vista de solo lectura no incluye menú.

Las 15 acciones del menú​

Resolver (resolve)​

  • Qué hace: cierra el ticket como resuelto y envía la despedida del canal.
  • Cliente: _resolveWithLabelGuard. Si el contacto tiene etiquetas, despacha de inmediato; si no tiene ninguna, pide confirmación ("Resolver sin etiquetas"). No comprueba bloqueo. En el pop-out cierra la ventana al despachar.
  • Acción y endpoint: resolveTicket -> PATCH /tickets/resolve/:id, cuerpo vacío.
  • Backend (TICKET_UPDATE): envía la despedida de conexión (cadena conexión, tenant, genérico), markResolved (guarda resolvedAt, que mide la ventana de reapertura), cancela los mensajes programados pendientes (motivo ticket_resolved, no bloqueante) y registra el mensaje interno "Ticket resuelto por ...". Difunde ticket_status_changed con resolved.
  • Estado local: resolveTicketSuccess quita la fila si la pestaña activa no es resolved (removeIfFilterMismatch); toast de éxito.

Resolver sin despedida (resolveNoFarewell)​

  • Cliente: siempre confirma ("El ticket se resolverá sin enviar el mensaje de despedida"). Mismo cierre de pop-out.
  • Acción y endpoint: resolveNoFarewellTicket -> PATCH /tickets/resolve-no-farewell/:id.
  • Backend (TICKET_UPDATE): igual que resolver con sendFarewell = false.
  • Estado local: igual que resolver; toast "Ticket resuelto sin despedida".

Devolver (return)​

  • Qué hace: devuelve el ticket a la cola sin asignar. No reabre un ticket resuelto.
  • Cliente: si el ticket está bloqueado, muestra el aviso "Ticket bloqueado" (_blockIfLocked) y se detiene; si no, confirma ("volverá a la cola sin asignar"). Al confirmar despacha unassignTicket, no returnTicket, y cierra la conversación si era la abierta. En el pop-out, onPopoutTicketAction cierra la ventana justo después de lanzar la acción (para return y delete, antes de que se resuelva la confirmación).
  • Endpoint: PATCH /tickets/unassign/:id.
  • Backend (TICKET_UPDATE): solo el dueño o un administrador (si no, 403); si el ticket tiene bloqueo responde 409 con el código TICKET_LOCKED; solo en estado open o inProgress (si no, 400); libera a la cola; registra "Ticket devuelto a la cola por ..."; difunde ticket_status_changed con open.
  • Estado local: unassignTicketSuccess quita la fila o la deja como open sin asignado según el filtro (removeIfUnassignedMismatch) y descuenta totalCount si la quitó; toast "Ticket devuelto a la cola". Ante el 409 TICKET_LOCKED (el bloqueo llegó entre la comprobación y la petición) se despacha ticketLockConflict y se muestra el mismo aviso, sin toast de error.

Transferir (transfer)​

  • Cliente: no disponible si canTransfer es falso; con bloqueo muestra el aviso de desbloqueo. Si no, abre TransferPanelComponent como overlay centrado con el agente y el departamento actuales preseleccionados.
  • Panel: formulario reactivo con agentId (obligatorio), departmentId (obligatorio, deshabilitado hasta elegir agente), internalNote (opcional) y keepWithUser (interruptor). Los agentes salen de GET /users/get-all (búsqueda y scroll infinito) y los departamentos de GET /departments/get-all filtrado por userId del agente elegido, de modo que solo se ofrecen los departamentos de los que es miembro. Cambiar de agente limpia y recarga el departamento. Emite el agente, el departamento (con nombre y color) y los opcionales.
  • Acción y endpoint: transferTicket -> PATCH /tickets/transfer/:id con { userId, departmentId, internalNote, keepWithUser } (los dos últimos solo si se rellenan).
  • Backend (TICKET_UPDATE): exige agente o departamento (400); permitido al dueño, a un administrador o a cualquiera si el ticket está en cola; con bloqueo responde 409 TICKET_LOCKED; si se indican ambos, el agente debe pertenecer al departamento (400); registra el mensaje interno de asignación o de transferencia a departamento, notifica al agente asignado y puede enviar el saludo del departamento. Ver gotcha sobre internalNote y keepWithUser.
  • Estado local: transferTicketSuccess actualiza assignedUser y el departamento (para repintar el color sin refetch); toast "Ticket transferido al agente".

Posponer (postpone)​

  • Cliente: PostponeDialogService.open() abre un modal pequeño y estático con selector de fecha y hora (DateTimePickerComponent, hora inicial: una hora adelante). Solo permite confirmar una fecha futura (isValidFutureSelection); devuelve un Date o null si se cancela.
  • Acción y endpoint: postponeTicket -> PATCH /tickets/postpone/:id con { postponedUntil } en ISO.
  • Backend (TICKET_UPDATE): postponedUntil debe ser ISO y estrictamente futura (@IsFutureDate, se valida al llegar la petición); dueño o administrador (403 si no); pasa el ticket a postponed, registra "Ticket pospuesto por ..." y difunde ticket_status_changed con postponed. Un job reactiva el ticket al vencer (ver ticket_reactivated en Tiempo real).
  • Estado local: postponeTicketSuccess pasa la fila a postponed con postponedUntil (o la quita si la pestaña es otra); toast warning.

Cancelar posposición (cancelPostpone)​

  • Cliente: sin confirmación; la opción reemplaza a "Posponer" cuando el ticket está postponed.
  • Acción y endpoint: cancelPostponeTicket -> PATCH /tickets/cancel-postpone/:id.
  • Backend (TICKET_UPDATE): solo si está postponed (400 si no); dueño o administrador; vuelve a open, registra "Posponer cancelado por ..." y difunde open.
  • Estado local: fila a open con postponedUntil vacío; toast informativo.

Marcar como no leído (markUnread)​

  • Cliente: despacha markUnread({ ticketId, agentUnread: true }) sin confirmación ni permiso.
  • Endpoint: PATCH /tickets/mark-unread/:id con { agentUnread }.
  • Backend: exige TICKET_UPDATE; guarda el indicador persistente agentUnread (independiente de unreadCount).
  • Estado local: markUnreadSuccess fija agentUnread; toast solo cuando es true. La tarjeta muestra un punto. Al seleccionar el ticket, selectConversation despacha markUnread con false.

Motivos de contacto (contactReasons)​

  • Cliente: no despacha ninguna acción de TicketActions ni llama a un endpoint al abrirse: abre LabelsPanelComponent (etiquetas y metadatos del contacto) como overlay. Al guardar emite updated y la lista se recarga. Detalle y endpoints en Etiquetas y datos del contacto.
  • Permisos: ninguno para abrir el panel; sus acciones internas exigen CONTACT_UPDATE y LABEL_*.

Borrar (delete)​

  • Cliente: confirma con advertencia de irreversibilidad ("se eliminarán el ticket y todos sus mensajes de forma permanente, y el contacto será notificado"). Si se cancela no hace nada. Al confirmar despacha deleteTicket y cierra la conversación si era la abierta.
  • Endpoint: DELETE /tickets/remove/:id.
  • Backend (TICKET_DELETE): difunde el cierre al contacto (motivo deleted), expulsa la sala del ticket (el fallo no es bloqueante), cancela los programados pendientes (ticket_deleted), borra todos los mensajes del ticket y por último el ticket.
  • Estado local: deleteTicketSuccess quita la fila y descuenta totalCount; el eco ticket_deleted solo descuenta si la fila seguía presente (idempotente). Toast "Ticket eliminado".

Cambiar estado: statusOpen, statusInProgress, statusClosed, statusCancelled​

Cuatro identificadores que despachan la misma acción changeTicketStatus con open, inProgress, closed y cancelled.

  • Cliente: submenú "Cambiar estado" sin confirmación; los cuatro ítems requieren TICKET_UPDATE. No se ofrecen pending, postponed ni resolved.
  • Endpoint: PATCH /tickets/status/:id con { status }.
  • Backend (TICKET_UPDATE): ChangeStatusDto acepta cualquiera de los siete valores; actualiza el estado sin comprobar propiedad, registra un mensaje interno con el estado y el agente (por ejemplo "Ticket cerrado por ...") y difunde ticket_status_changed.
  • Estado local: changeTicketStatusSuccess quita la fila si el estado no coincide con la pestaña activa; toast "Estado actualizado" con el nombre traducido del estado.

Mantener conmigo (lock)​

  • Cliente: sin confirmación. La tarjeta muestra un candado y la lista de bloqueados está en Tickets bloqueados.
  • Acción y endpoint: lockTicket -> PATCH /tickets/lock/:id.
  • Backend (TICKET_UPDATE): solo el dueño actual (403 notLockOwner), solo en open o inProgress; el cambio es atómico (409 si cambió entretanto). El controlador difunde ticket_lock_changed a todo el tenant.
  • Estado local: lockTicketSuccess guarda lockedBy; toast "Ticket bloqueado: lo mantienes contigo". Mientras esté bloqueado no se puede devolver ni transferir.

Liberar (unlock)​

  • Acción y endpoint: unlockTicket -> PATCH /tickets/unlock/:id.
  • Backend (TICKET_UPDATE): solo quien lo bloqueó (403 notLockOwner); difunde ticket_lock_changed con lockedBy vacío.
  • Estado local: unlockTicketSuccess limpia lockedBy y lockedByUser. La liberación administrativa (PATCH /tickets/force-unlock/:id, TICKET_UNLOCK_ANY, sin comprobar quién bloqueó) está fuera de este menú, en Tickets bloqueados. Resolver, cerrar o cancelar también limpia el bloqueo (ticketStatusChangedFromSocket con estado terminal).

Las 54 acciones de TicketActions​

TicketActions (objeto exportado de ticket.actions.ts) agrupa 54 acciones. Filtro activo: activeFilter del estado; "quita la fila" significa filtrar la lista si el estado resultante no coincide con la pestaña activa.

Carga (7)​

#AcciónQuién la despachaEfecto
1loadTicketsLista (dispatchLoadTickets)Reinicia lista, página 1 y banderas; guarda el filtro; efecto GET /tickets/get-all con page: 1 y limit: 10 (switchMap)
2loadTicketsSuccessEfectoGuarda tickets, totalCount, hasMore (meta.page < meta.totalPages) y fija currentPage en 2
3loadTicketsFailureEfectoGuarda el error y apaga las banderas de carga
4loadMoreTicketsLista, al llegar al fondoMarca isLoadingMore; efecto GET /tickets/get-all con la página siguiente (exhaustMap)
5loadMoreTicketsSuccessEfectoAnexa los resultados e incrementa la página
6loadMoreTicketsFailureEfectoGuarda el error
7resetTicketsNadie en el código de la rama (solo specs)Devuelve el estado inicial

Socket (6)​

Las despacha ChatSocketService desde los eventos del servidor.

#AcciónEventoEfecto en el reducer
8ticketUpdatedFromSocketticket_updatedSi el ticket está en la lista, actualiza lastMessage (id, ack, contenido, tipo, medio, vista única, remitente) y updatedAt y lo sube al primer lugar
9ticketAssignedFromSocketticket_assignedQuita la fila si el filtro es "sin asignar" u otro operador; si no, actualiza assignedUser y descarta el avatar anterior si cambió el agente
10ticketStatusChangedFromSocketticket_status_changedQuita la fila si el estado no coincide con la pestaña; con estado terminal (resolved, closed, cancelled) limpia lockedBy
11ticketReactivatedFromSocketticket_reactivatedFila a open, sin postponedUntil y con reactivated: true (bandera solo de interfaz)
12ticketDeletedFromSocketticket_deletedQuita la fila y descuenta totalCount solo si seguía presente
13contactPresenceChangedFromSocketcontact_presence_changedParchea isOnline, lastSeen y lastDisconnectReason en todas las filas del contacto

Resolver, devolver, transferir, posponer, borrar, estado, no leído (21)​

#AcciónEfecto
14resolveTicketEfecto PATCH /tickets/resolve/:id (mergeMap)
15resolveTicketSuccessQuita la fila salvo en resolved; toast
16resolveTicketFailureToast de error genérico
17resolveNoFarewellTicketPATCH /tickets/resolve-no-farewell/:id
18resolveNoFarewellTicketSuccessIgual que 15; toast propio
19resolveNoFarewellTicketFailureToast de error
20returnTicketPATCH /tickets/return/:id (reabre un ticket resuelto en el backend). Ningún componente la despacha: el botón "Devolver" usa unassignTicket
21returnTicketSuccessFila a open o quitada según el filtro; toast informativo
22returnTicketFailureToast de error
23unassignTicketPATCH /tickets/unassign/:id; un 409 TICKET_LOCKED emite ticketLockConflict en lugar del fallo
24unassignTicketSuccessVer "Devolver"; toast
25unassignTicketFailureToast de error
26transferTicketPATCH /tickets/transfer/:id; mismo tratamiento del 409
27transferTicketSuccessActualiza assignedUser y department; toast
28transferTicketFailureToast de error
29postponeTicketPATCH /tickets/postpone/:id
30postponeTicketSuccessFila a postponed con postponedUntil; toast warning
31postponeTicketFailureToast de error
32cancelPostponeTicketPATCH /tickets/cancel-postpone/:id
33cancelPostponeTicketSuccessFila a open sin postponedUntil; toast
34cancelPostponeTicketFailureToast de error
35deleteTicketDELETE /tickets/remove/:id
36deleteTicketSuccessQuita la fila y descuenta totalCount; toast
37deleteTicketFailureToast de error
38changeTicketStatusPATCH /tickets/status/:id con { status }
39changeTicketStatusSuccessQuita la fila si no coincide; toast con el nombre del estado
40changeTicketStatusFailureToast de error
41markUnreadPATCH /tickets/mark-unread/:id con { agentUnread }; también la despacha selectConversation con false
42markUnreadSuccessFija agentUnread; toast solo si es true
43markUnreadFailureToast de error

No leídos y acuses en tiempo real (3)​

#AcciónQuién la despachaEfecto
44ticketIncrementUnreadLista, al llegar un ticket_updated del contacto de un ticket no abierto (de administradores o de tickets visibles); también suena el aviso de audioSuma 1 a unreadCount
45ticketClearUnreadLista, al seleccionar un ticket con no leídos o al llegar un acuse readPone unreadCount en 0
46ticketLastMessageAckChangedLista, con cada message_ack_changedActualiza el ack del último mensaje si su id está en la lista de ids

Bloqueo (8)​

#AcciónEfecto
47lockTicketPATCH /tickets/lock/:id; toma lockedBy de la respuesta
48lockTicketSuccessGuarda lockedBy; toast
49lockTicketFailureToast de error
50unlockTicketPATCH /tickets/unlock/:id
51unlockTicketSuccesslockedBy y lockedByUser a null; toast
52unlockTicketFailureToast de error
53ticketLockChangedFromSocketEvento ticket_lock_changed (difundido a todo el tenant): fija lockedBy y lockedByUser (con username si llega el nombre) o los limpia
54ticketLockConflictSe emite en lugar del fallo cuando transferir o devolver reciben 409 TICKET_LOCKED: el efecto muestra el aviso "desbloquea primero"; no está en la lista de actionFailure$ y su reducer no hace nada

Pruebas​

SpecCubre
ticket-actions-menu.component.spec.tscanLockTicket (sin bloqueo y dueño), canUnlockTicket (bloqueado por el propio usuario; no si lo tiene otro) y emisión de lock y unlock con select()
transfer-panel.component.spec.tsDepartamento deshabilitado hasta elegir agente, carga filtrada por membresía, limpieza al cambiar de agente, bloqueo hasta tener agente y departamento, emisión con el departamento elegido
postpone-dialog.component.spec.tsFecha pasada inválida, fecha futura válida, no cierra con fecha pasada, resuelve el Date combinado, cancelar
chat-list.component.spec.tsonTicketAction para borrar (confirmación, cierre de la conversación abierta, cancelación), bloquear y desbloquear, guardia "desbloquea primero" al transferir y devolver, isTicketOwner e isLockedByCurrentUser
chat-popout.component.spec.tsEnlaces del menú de bloqueo y minimizar
core/states/ticket/ticket.actions.spec.tsCreadores de posponer, cancelar posposición, reactivación, bloquear, liberar y cambio de bloqueo
core/states/ticket/ticket.effects.spec.tsEfectos de posponer, cancelar posposición, bloquear, liberar, conflicto 409 en transferir y devolver y que ticketLockConflict no dispare el toast genérico
core/states/ticket/ticket.reducers.spec.tsPosponer, cancelar, reactivar, eliminar por socket, marcar no leído, presencia, bloqueo y su limpieza por estado terminal, devolver con filtro de operador, asignación por socket y transferir

No hay specs de efectos ni reducers para resolver, resolver sin despedida, cambiar estado, borrar (efecto) ni carga paginada, y no hay spec de ticket.service.ts.

Gotchas​

  • "Devolver" llama a unassign, no a return: return/:id en el backend reabre un ticket resuelto (y falla si el contacto ya tiene otro ticket activo o si no es el último), pero la acción returnTicket no está conectada a la interfaz.
  • El DTO de transferencia valida internalNote y keepWithUser, pero el controlador solo pasa userId y departmentId al servicio; en esta rama esos dos campos no tienen efecto en el backend.
  • El efecto de transferir lee res.data.assignedUser y, como el backend devuelve el ticket, recurre a { _id: userId }: el nombre del asignado se completa luego con el evento ticket_assigned o con la recarga.
  • "Marcar como no leído" no tiene permiso en el cliente pero el backend exige TICKET_UPDATE.
  • markUnread, Motivos de contacto y Borrar aparecen también en tickets resueltos o cerrados.
  • Los textos de la guardia de bloqueo usan voseo (Liberalo) en whaticket.ticketActions.lockedFirstMessage.