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
| Archivo | Rol |
|---|---|
scheduled-messages.module.ts / scheduled-messages-routing.module.ts | El 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étodo | Endpoint | Permiso en el backend | Uso |
|---|---|---|---|
getMine(filter?) | GET /scheduled-messages/mine | MESSAGE_READ | Esta pantalla y chat-list |
getByTicket(ticketId) | GET /scheduled-messages/by-ticket/:ticketId | MESSAGE_READ | Chat, al abrir o cambiar de ticket |
create(payload) | POST /scheduled-messages/create | MESSAGE_CREATE | Compositor del chat |
update(id, payload) | PATCH /scheduled-messages/update/:id | MESSAGE_UPDATE | Fila de pendientes del chat |
cancel(id) | PATCH /scheduled-messages/cancel/:id | MESSAGE_DELETE | Esta 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:
| Tipo | Contenido |
|---|---|
ScheduledMessageStatus | pending, processing, sent, cancelled, failed |
ScheduledMessageKind | chat, note |
ScheduledMessageCancelReason | agent, 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 |
CreateScheduledMessagePayload | ticketId, body, scheduledAt, messageType, signature?, parentId? |
UpdateScheduledMessagePayload | body?, scheduledAt? |
ScheduledMessageQueryFilter | status?, page?, limit? |
ScheduledMessageListData | results, meta { total, page, totalPages, limit } |
ScheduledMessageState | items, 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*Successy*Failure), las cinco de socket (sentFromSocket,cancelledFromSocket,failedFromSocket,createdFromSocket,updatedFromSocket) yreset. - Efectos:
loadMine$yloadByTicket$usanswitchMap;create$,update$ycancel$usanmergeMap. Todos los errores se reducen al mensajescheduledMessages.toast.error. - Reducer:
loadMinereemplazaitems;loadByTicketSuccessfusiona por ticket (conserva los de otros tickets);createno inserta de forma optimista (la fila aparece encreateSuccess);cancelretira el elemento de forma optimista ycancelFailurelo reinserta con elitemque 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): eventosscheduled_message_sent,_cancelled,_failed,_createdy_updated, emitidos en la salauser:{authorId}(solo agentes).sentycancelledquitan el elemento del store;failedlo marcafailedconlastError;createdyupdatedhacen alta o reemplazo. Detalle en Sockets y Estado.
Flujo
- Abrir
scheduled-messages: el guard compruebaMESSAGE_READy se despachaloadMine({}). - Filtrar por estado:
loadMine({ status })recarga la lista desde el servidor. - Cancelar un pendiente:
cancello quita del store de inmediato y llamaPATCH cancel/:id; si falla, se reinserta. - 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: despachaloadMine({})al iniciar,loadMine({ status })al filtrar ycancel({ id, item })al cancelar.scheduled-messages-filter.component.spec.ts: emite{ status: 'pending' }y{ status: undefined }para "todos".scheduled-messages-table.component.spec.ts:isCancellablesolo parapendingy emisión decancel.
Los reducers, efectos y el servicio del slice no tienen specs en esta rama.
Gotchas e inconsistencias
- El store es compartido con el chat.
loadMinereemplazaitemscompleto: filtrar aquí porsentofaileddescarta los pendientes de otros tickets que usan la fila y el reloj del chat, hasta quechat-listvuelva a despacharloadMine({ status: 'pending' })o se recargue el ticket. - Con el filtro
sentocancelled, un evento de socket posterior retira elementos que se están mostrando, porquesentFromSocketycancelledFromSocketsiempre los eliminan. - El permiso de la ruta (
MESSAGE_READ) es más amplio que el necesario para cancelar: el backend exigeMESSAGE_DELETEy, sin él, el botón se ve pero la petición falla (el elemento reaparece porcancelFailure). - Sin entrada de menú y sin enlace desde otras pantallas.
- El filtro y la tabla no paginan:
getMineaceptapageylimit, pero la pantalla nunca los envía. ScheduledMessagesSettings(límites, ventana máxima, comportamiento al resolver) se declara ensettings.interface.ts, pero no hay panel para editarlo; ver Configuración.