Skip to content

Frontend: Cadena de Despacho de Notificaciones de Jobs

Tipo: Arquitectural (Frontend) Alcance: Sistema transversal — todos los jobs que usan JobNotificationsContextEstado: Implementado Fecha: 2026-02-26 Ultima revision: 2026-02-26 — change job-notification-action-url: Tier 2.5 actualizado a full-page redirect con notif_id; FacturacionLotesView implementa auto-dispatch reactivo (single-click)


Descripcion

Cuando el usuario hace clic en "Ver Resultados" en la campana de notificaciones (JobNotificationBell), el sistema ejecuta una cadena de despacho ordenada por prioridad para entregar el resultado del job al usuario. Cada nivel ("Tier") se intenta en orden; si un Tier maneja el despacho, los siguientes no se ejecutan.

El punto de entrada es handleNotificationAction en JobNotificationsContext.


Cadena de Despacho

Tier 1  →  globalJobCallbackRegistry.executeOnce(jobId, ...)
Tier 2  →  JobActionRegistry.get(jobType)?.onResult(payload)
Tier 2.5 → metadata.action_url → navegacion + suffix &notif_id={id}
             si action_url empieza con '?': window.location.href (full page redirect)
             si no:                         window.location.hash (in-SPA hash navigation)
Tier 3  →  sessionStorage: escribir pending_result con source='bell_action'

En cualquier Tier que resuelva el despacho, markAsRead(id) se llama antes de retornar — excepto en Tier 2.5, que navega y retorna sin marcar la notificacion como leida. La vista destino es responsable de llamar handleNotificationAction(notifId) via auto-dispatch reactivo, lo que desencadena Tier 2 y consecuentemente markAsRead (ver detalles en la seccion del Tier 2.5 y en Step URL).


Tiers

Tier 1 — Callback en vivo (globalJobCallbackRegistry)

  • Cuándo se activa: El componente React que despachó el job registró un callback directo via globalJobCallbackRegistry. El callback sigue vivo en memoria (el componente no fue desmontado).
  • Acción: Ejecuta el callback registrado y retorna el resultado directamente al componente.
  • Si resuelve: El despacho termina. No se escribe sessionStorage ni se navega.
  • Si no resuelve (executeOnce retorna false): Se pasa a Tier 2.

Tier 2 — Accion registrada en vista (JobActionRegistry)

  • Cuándo se activa: La vista destino del job (ej: FacturacionLotesView) esta montada y registró una accion en JobActionRegistry para el job_type correspondiente.
  • Acción: Llama a onResult(payload) de la entrada registrada — típicamente abre el modal de resultados directamente en la vista activa.
  • Si resuelve: El despacho termina. No se escribe sessionStorage ni se navega.
  • Si no resuelve (no hay entrada para el job_type): Se pasa a Tier 2.5.

Tier 2.5 — Navegacion por URL (metadata.action_url)

  • Cuándo se activa: Tiers 1 y 2 no resolvieron, y la notificacion contiene metadata.action_url (campo no vacío, presente en notificaciones creadas desde esta implementacion).
  • Acción: Navega a la URL de la notificacion y retorna inmediatamente. Antes de navegar, append &notif_id={id} al action_url.
    • Si action_url comienza con '?' (ej: ?loc=mmem#/movimientos/facturacion-lote): usa window.location.href = (URL_APP ?? '') + actionUrl + suffix — full page redirect. El sufijo resultante tiene la forma ?loc=mmem#/movimientos/facturacion-lote?job_id=142&notif_id=7.
    • Si action_url NO comienza con '?' (ej: #/ruta): usa window.location.hash = base + suffix — in-SPA hash navigation.
  • markAsRead NO se llama en Tier 2.5: Retorna antes de llegar a esa llamada. La notificacion queda sin leer. markAsRead se llama posteriormente cuando la vista destino dispara handleNotificationAction(notifId) via su auto-dispatch reactivo (Tier 2 resuelve desde la vista montada → markAsRead sigue).
  • La vista destino abre el modal automaticamente (single-click): Al montar, la vista captura notif_id de la URL, lo guarda en un ref y limpia la URL. Un efecto reactivo dependiente de [isLoading, handleNotificationAction] detecta el ref ≠ null cuando isLoading=false y llama handleNotificationAction(notifId) — esto dispara Tier 2, abre el modal y llama markAsRead. No se requiere un segundo clic del usuario.
  • Restriccion: No escribe sessionStorage. La vista destino recupera el resultado directamente desde el backend cuando Tier 2 dispara via el auto-dispatch reactivo.
  • Si resuelve: El despacho termina (navegacion completada, notificacion pendiente de markAsRead que ocurrira automaticamente al montar la vista destino).
  • Si no aplica (action_url ausente o undefined): Se pasa a Tier 3.

Tier 3 — Fallback sessionStorage

  • Cuándo se activa: action_url ausente (notificacion creada antes de esta implementacion, o job sin action_url configurado).
  • Acción: Escribe pending_result con source: 'bell_action' en sessionStorage (user-scoped). La vista destino, cuando el usuario navegue hasta ella manualmente, lee el pending result en su efecto de montaje (Step A) y abre el modal.
  • Limitaciones: No funciona tras cerrar sesion (sessionStorage se limpia) ni cuando el usuario esta en el area legacy fuera del SPA.

Escenarios de Activacion

Situacion del usuarioTier que resuelveMecanismoModal
Usuario en FacturacionLotesView, SPA activaTier 1 o 2Callback en vivo o onResult de la vistaSe abre en el primer clic
Usuario en otra ruta del SPA, notificacion nueva (?loc= URL)Tier 2.5 → auto-dispatch → Tier 21er clic: window.location.href con ?loc=mmem#/ruta?job_id=X&notif_id=N. Pagina recarga. Vista monta, Step URL captura notif_id, efecto reactivo llama handleNotificationAction automaticamenteSe abre automaticamente (single-click)
Usuario en area legacy PHP (?loc= URL)Tier 2.5 → auto-dispatch → Tier 21er clic: window.location.href navega al area correcta. Vista monta, auto-dispatch dispara Tier 2Se abre automaticamente (single-click)
Usuario cerro sesion y volvio a entrar (?loc= URL)Tier 2.5 → auto-dispatch → Tier 2Notificacion en campana. 1er clic: full page redirect con notif_id. Vista monta, auto-dispatch recupera resultado del backend via Tier 2Se abre automaticamente (single-click)
Notificacion antigua (sin action_url)Tier 3sessionStorage, usuario navega manualmenteSe abre via Tier 2 cuando la vista esta montada

Recuperacion de Resultado via URL-Param (Step URL) y Step A

Step URL — Captura de notif_id y auto-dispatch reactivo

Cuando Tier 2.5 navega con &notif_id=7 en la URL, la vista FacturacionLotesView implementa un mecanismo de dos fases en su montaje:

Fase 1 — Step URL (efecto de montaje):

  1. Lee notif_id de los parametros de la URL actual.
  2. Guarda el valor en pendingNotifIdRef (un useRef<number | null>).
  3. Limpia notif_id de la URL para evitar re-procesamiento.
  4. Retorna sin abrir el modal ni llamar al backend.

Fase 2 — Reactive auto-dispatch effect ([isLoading, handleNotificationAction]):

Cuando isLoading (del hook useJobNotifications) transiciona a false y pendingNotifIdRef.current !== null:

  1. Lee el notifId del ref y lo resetea a null.
  2. Llama handleNotificationAction(notifId) — esto ejecuta Tier 2 desde la vista ya montada.
  3. Tier 2 llama onResult(payload), abre el modal de resultados y llama markAsRead(notifId).

El efecto es que el modal se abre automaticamente al montar la vista, sin requerir un segundo clic del usuario. La espera por isLoading=false garantiza que handleNotificationAction este disponible y estable antes de disparar el despacho.

Parametro transportado: notif_id (ID de la notificacion), no job_id. El job_id puede estar presente en la URL como parte de action_url original, pero el auto-dispatch usa notif_id para llamar a handleNotificationAction.

Step A — sessionStorage (Tier 3 fallback)

Cuando Tier 3 escribe un pending_result en sessionStorage y el usuario navega manualmente a la vista, Step A lee el pending result y limpia la sesion. En la implementacion actual, Step A solo llama a clearSession() y retorna — no abre el modal directamente. El modal se abre igualmente via Tier 2 cuando el usuario llega a la vista a traves de la campana.

Compatibilidad entre Step URL y Step A: Son mutuamente excluyentes para una misma notificacion: si action_url esta presente, Tier 3 no escribe sessionStorage (Step A es no-op). Si action_url esta ausente, Step URL es no-op (no hay notif_id en la URL).

Manejo de errores HTTP: Si el backend retorna 403 o 404 cuando Tier 2 intenta cargar el resultado, se muestra un toast de error y el modal no se abre.


Generacion de action_url en el Backend

El campo action_url se propaga de la siguiente manera:

  1. El frontend incluye action_url: '<ruta-base>' en el cuerpo del request de despacho. El formato depende del contexto de navegacion de la vista destino:
    • Para vistas en area legacy PHP: '?loc={modulo}#/ruta/vista' (ej: '?loc=mmem#/movimientos/facturacion-lote')
    • Para vistas en SPA pura: '#/ruta/vista'
  2. NotificationService.php lee $job->payload['action_url'] y le agrega ?job_id={$job->id}.
  3. El valor resultante (ej: '?loc=mmem#/movimientos/facturacion-lote?job_id=142') se almacena en metadata.action_url de la notificacion en la base de datos.
  4. El backend no tiene un mapa de rutas: simplemente concatena el job_id al valor que provino del frontend. Si el campo esta ausente en el payload, no se escribe en la metadata.
  5. En Tier 2.5, el frontend append adicionalmente &notif_id={id} (ID de la notificacion) al action_url almacenado antes de navegar. Este sufijo no existe en la base de datos; se agrega en runtime por JobNotificationsContext.

Invariantes

  • markAsRead(id) se llama despues de cualquier Tier que resuelva el despacho, excepto Tier 2.5, que retorna antes de llegar a esa llamada. Para vistas con auto-dispatch reactivo (como FacturacionLotesView), markAsRead se llama automaticamente cuando el efecto reactivo dispara Tier 2 al montar la vista. Para vistas sin auto-dispatch, la notificacion queda sin leer hasta un segundo clic explicito del usuario.
  • El modal se abre via Tier 2 (path JobActionRegistry.onResult). Puede dispararse por: (a) clic explicito del usuario en "Ver Resultados" cuando la vista ya esta montada, o (b) auto-dispatch reactivo de la vista al detectar notif_id en la URL (single-click).
  • Tier 2.5 solo aplica a notificaciones con type = 'success'. Las de tipo 'error' no desencadenan navegacion ni escritura en sessionStorage.
  • El parametro notif_id se elimina de la URL inmediatamente en Step URL al montar la vista, independientemente de si el auto-dispatch llega a completarse.

Extensibilidad

El patron de action_url puede replicarse en otros modulos que usen background jobs.

Formato de action_url segun contexto de navegacion

Contexto de la vista destinoFormato de action_urlMecanismo de Tier 2.5
Vista React en area legacy PHP (requiere ?loc=)?loc={modulo}#/ruta/vistawindow.location.href (full page redirect)
Vista React en SPA pura (solo hash)#/ruta/vistawindow.location.hash (in-SPA)

Para habilitar auto-dispatch (single-click) en una nueva vista:

  1. En el servicio frontend, incluir action_url: '?loc={modulo}#/ruta/de/la-vista' en el dispatch.
  2. En la vista, implementar dos elementos:
    • Step URL (efecto de montaje): leer notif_id de la URL, guardarlo en un useRef, limpiar la URL.
    • Reactive auto-dispatch effect con dependencias [isLoading, handleNotificationAction]: cuando isLoading=false y el ref tiene valor, llamar handleNotificationAction(notifId).
  3. El Tier 2.5 y el backend funcionan sin cambios adicionales.

El job_id puede incluirse en action_url si la vista lo necesita para pre-cargar datos antes del auto-dispatch, pero el mecanismo de despacho usa exclusivamente notif_id.


Referencias