Skip to main content

Mensajes programados (features/scheduled-messages)

Pantalla "mis envíos programados": lista los mensajes y notas que el propio agente programó, permite filtrar por estado y cancelar los pendientes. Es la superficie secundaria de la función; la principal vive en el chat (diálogo de programación, botón del compositor y fila de pendientes del ticket), descrita en Chat. En el backend existe ScheduledMessageController (sin página propia en esta documentación); el emisor ScheduledMessageSenderService se menciona en Message.

Ruta​

ScheduledMessagesModule se carga en shared/routes/content.routes.ts bajo scheduled-messages (PAGE_URLS.SCHEDULED_MESSAGES) con canActivate: [AuthGuard, permissionGuard] y data: { permission: 'MESSAGE_READ' }. El comentario del código lo explica: la función reutiliza los acrónimos MESSAGE_* y no define permisos propios en la versión 1. ScheduledMessagesRoutingModule repite permissionGuard con el mismo permiso.

No hay entrada de menú: nav.constant.ts no lista esta ruta (solo hay un comentario con una entrada scheduled sin usar) y ningún componente navega a ella; se llega escribiendo la URL.

Archivos​

ArchivoRol
scheduled-messages.module.ts / scheduled-messages-routing.module.tsEl módulo declara solo la página; tabla y filtro son standalone y se importan en él (más FormsModule, TranslateModule, SharedModule)
pages/scheduled-messages-page/scheduled-messages-page.component.*Contenedor: lee el store, despacha acciones y compone breadcrumb, filtro y tabla
components/scheduled-messages-filter/scheduled-messages-filter.component.*Selector de estado (standalone)
components/scheduled-messages-table/scheduled-messages-table.component.*Tabla simple presentacional (standalone)

Permisos​

Solo MESSAGE_READ (ruta). La UI de esta pantalla no añade directivas de permiso; el botón "cancelar" depende únicamente del estado del mensaje. En el backend cada endpoint exige su acrónimo MESSAGE_* (ver tabla siguiente).

Servicios y endpoints​

ScheduledMessageService (core/services/api/scheduled-message.service.ts, providedIn: 'root') usa API_URLS.SCHEDULED_MESSAGES (/scheduled-messages). No muestra toasts.

MétodoEndpointPermiso en el backendUso
getMine(filter?)GET /scheduled-messages/mineMESSAGE_READEsta pantalla y chat-list
getByTicket(ticketId)GET /scheduled-messages/by-ticket/:ticketIdMESSAGE_READChat, al abrir o cambiar de ticket
create(payload)POST /scheduled-messages/createMESSAGE_CREATECompositor del chat
update(id, payload)PATCH /scheduled-messages/update/:idMESSAGE_UPDATEFila de pendientes del chat
cancel(id)PATCH /scheduled-messages/cancel/:idMESSAGE_DELETEEsta pantalla y el chat

Los permisos se leyeron en scheduled-message.controller.ts; las rutas coinciden con SCHEDULED_MESSAGE_ROUTES. Cancelar usa PATCH, no DELETE.

La función completa se habilita por tenant: el chat lee selectScheduledMessagesEnabled, que sale de user.featureFlags.scheduledMessagesEnabled (perfil de get-me) y no del store de Configuración, porque los agentes sin SETTINGS_READ no podrían leerlo. Esta pantalla no comprueba esa bandera.

Modelos​

core/interfaces/api/scheduled-message.interface.ts:

TipoContenido
ScheduledMessageStatuspending, processing, sent, cancelled, failed
ScheduledMessageKindchat, note
ScheduledMessageCancelReasonagent, ticket_resolved, ticket_deleted, feature_disabled, owner_inactive
ScheduledMessage_id, tenantId, ticketId, contactIdSnapshot, authorUserId, authorNameSnapshot, authorSendType (user o agent), messageType, body, parentId?, mediaId?, scheduledAt (ISO UTC), timezoneSnapshot, status, cancelReason?, sentMessageId?, sentAt?, attemptCount?, lastError?, createdAt, updatedAt
CreateScheduledMessagePayloadticketId, body, scheduledAt, messageType, signature?, parentId?
UpdateScheduledMessagePayloadbody?, scheduledAt?
ScheduledMessageQueryFilterstatus?, page?, limit?
ScheduledMessageListDataresults, meta { total, page, totalPages, limit }
ScheduledMessageStateitems, loading, saving, error

Constantes en core/helpers/global/scheduled-message.constant.ts: SCHEDULED_MESSAGE_STATUS(_VALUES), _KIND, _CANCEL_REASON y SCHEDULED_MESSAGE_SOCKET_EVENTS. El comentario indica que reflejan las enumeraciones del backend y deben mantenerse sincronizadas.

Pantalla​

ScheduledMessagesPageComponent expone items$ = select(selectAllScheduledMessages). En ngOnInit despacha loadMine({}); onFilterChange despacha loadMine({ status }) y onCancel despacha cancel({ id, item }).

ScheduledMessagesFilterComponent es un select con "todos" más los cinco estados (scheduledMessages.status.<estado>); onStatusChange emite { status } (o undefined si se elige "todos").

ScheduledMessagesTableComponent es una tabla Bootstrap simple (no ngx-datatable: el comentario explica que el store guarda la lista completa sin paginar). Columnas: cuerpo, fecha programada (date: 'short'), estado (insignia) y acciones. isCancellable es verdadero solo para pending y entonces se muestra el botón "cancelar" (scheduledMessages.strip.cancel); sin elementos muestra scheduledMessages.table.empty.

Estado NgRx y sockets​

Slice scheduledMessages (core/states/scheduled-message/, registrado en app.state.ts con ScheduledMessageEffects).

  • Acciones: loadMine, loadByTicket, create, update, cancel (cada una con *Success y *Failure), las cinco de socket (sentFromSocket, cancelledFromSocket, failedFromSocket, createdFromSocket, updatedFromSocket) y reset.
  • Efectos: loadMine$ y loadByTicket$ usan switchMap; create$, update$ y cancel$ usan mergeMap. Todos los errores se reducen al mensaje scheduledMessages.toast.error.
  • Reducer: loadMine reemplaza items; loadByTicketSuccess fusiona por ticket (conserva los de otros tickets); create no inserta de forma optimista (la fila aparece en createSuccess); cancel retira el elemento de forma optimista y cancelFailure lo reinserta con el item que viaja en la acción.
  • Selectores: selectAllScheduledMessages (todos los estados, para esta pantalla), selectAllPending, selectPendingByTicket(id), selectPendingCountByTicket(id), selectScheduledMessageSaving, selectScheduledMessageLoading, selectScheduledMessageError.
  • Sockets (chat-socket.service.ts): eventos scheduled_message_sent, _cancelled, _failed, _created y _updated, emitidos en la sala user:{authorId} (solo agentes). sent y cancelled quitan el elemento del store; failed lo marca failed con lastError; created y updated hacen alta o reemplazo. Detalle en Sockets y Estado.

Flujo​

  1. Abrir scheduled-messages: el guard comprueba MESSAGE_READ y se despacha loadMine({}).
  2. Filtrar por estado: loadMine({ status }) recarga la lista desde el servidor.
  3. Cancelar un pendiente: cancel lo quita del store de inmediato y llama PATCH cancel/:id; si falla, se reinserta.
  4. Los eventos de socket mantienen la lista al día mientras la pantalla está abierta.

Pruebas​

Tres specs (Karma/Jasmine):

  • scheduled-messages-page.component.spec.ts: despacha loadMine({}) al iniciar, loadMine({ status }) al filtrar y cancel({ id, item }) al cancelar.
  • scheduled-messages-filter.component.spec.ts: emite { status: 'pending' } y { status: undefined } para "todos".
  • scheduled-messages-table.component.spec.ts: isCancellable solo para pending y emisión de cancel.

Los reducers, efectos y el servicio del slice no tienen specs en esta rama.

Gotchas e inconsistencias​

  • El store es compartido con el chat. loadMine reemplaza items completo: filtrar aquí por sent o failed descarta los pendientes de otros tickets que usan la fila y el reloj del chat, hasta que chat-list vuelva a despachar loadMine({ status: 'pending' }) o se recargue el ticket.
  • Con el filtro sent o cancelled, un evento de socket posterior retira elementos que se están mostrando, porque sentFromSocket y cancelledFromSocket siempre los eliminan.
  • El permiso de la ruta (MESSAGE_READ) es más amplio que el necesario para cancelar: el backend exige MESSAGE_DELETE y, sin él, el botón se ve pero la petición falla (el elemento reaparece por cancelFailure).
  • Sin entrada de menú y sin enlace desde otras pantallas.
  • El filtro y la tabla no paginan: getMine acepta page y limit, pero la pantalla nunca los envía.
  • ScheduledMessagesSettings (límites, ventana máxima, comportamiento al resolver) se declara en settings.interface.ts, pero no hay panel para editarlo; ver Configuración.