Appearance
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 ¬if_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 (
executeOnceretornafalse): 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 enJobActionRegistrypara eljob_typecorrespondiente. - 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
¬if_id={id}alaction_url.- Si
action_urlcomienza con'?'(ej:?loc=mmem#/movimientos/facturacion-lote): usawindow.location.href = (URL_APP ?? '') + actionUrl + suffix— full page redirect. El sufijo resultante tiene la forma?loc=mmem#/movimientos/facturacion-lote?job_id=142¬if_id=7. - Si
action_urlNO comienza con'?'(ej:#/ruta): usawindow.location.hash = base + suffix— in-SPA hash navigation.
- Si
markAsReadNO se llama en Tier 2.5: Retorna antes de llegar a esa llamada. La notificacion queda sin leer.markAsReadse llama posteriormente cuando la vista destino disparahandleNotificationAction(notifId)via su auto-dispatch reactivo (Tier 2 resuelve desde la vista montada →markAsReadsigue).- La vista destino abre el modal automaticamente (single-click): Al montar, la vista captura
notif_idde la URL, lo guarda en un ref y limpia la URL. Un efecto reactivo dependiente de[isLoading, handleNotificationAction]detecta el ref ≠ null cuandoisLoading=falsey llamahandleNotificationAction(notifId)— esto dispara Tier 2, abre el modal y llamamarkAsRead. 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
markAsReadque ocurrira automaticamente al montar la vista destino). - Si no aplica (
action_urlausente oundefined): Se pasa a Tier 3.
Tier 3 — Fallback sessionStorage
- Cuándo se activa:
action_urlausente (notificacion creada antes de esta implementacion, o job sinaction_urlconfigurado). - Acción: Escribe
pending_resultconsource: '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 usuario | Tier que resuelve | Mecanismo | Modal |
|---|---|---|---|
Usuario en FacturacionLotesView, SPA activa | Tier 1 o 2 | Callback en vivo o onResult de la vista | Se abre en el primer clic |
Usuario en otra ruta del SPA, notificacion nueva (?loc= URL) | Tier 2.5 → auto-dispatch → Tier 2 | 1er clic: window.location.href con ?loc=mmem#/ruta?job_id=X¬if_id=N. Pagina recarga. Vista monta, Step URL captura notif_id, efecto reactivo llama handleNotificationAction automaticamente | Se abre automaticamente (single-click) |
Usuario en area legacy PHP (?loc= URL) | Tier 2.5 → auto-dispatch → Tier 2 | 1er clic: window.location.href navega al area correcta. Vista monta, auto-dispatch dispara Tier 2 | Se abre automaticamente (single-click) |
Usuario cerro sesion y volvio a entrar (?loc= URL) | Tier 2.5 → auto-dispatch → Tier 2 | Notificacion en campana. 1er clic: full page redirect con notif_id. Vista monta, auto-dispatch recupera resultado del backend via Tier 2 | Se abre automaticamente (single-click) |
Notificacion antigua (sin action_url) | Tier 3 | sessionStorage, usuario navega manualmente | Se 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 ¬if_id=7 en la URL, la vista FacturacionLotesView implementa un mecanismo de dos fases en su montaje:
Fase 1 — Step URL (efecto de montaje):
- Lee
notif_idde los parametros de la URL actual. - Guarda el valor en
pendingNotifIdRef(unuseRef<number | null>). - Limpia
notif_idde la URL para evitar re-procesamiento. - 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:
- Lee el
notifIddel ref y lo resetea anull. - Llama
handleNotificationAction(notifId)— esto ejecuta Tier 2 desde la vista ya montada. - Tier 2 llama
onResult(payload), abre el modal de resultados y llamamarkAsRead(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:
- 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'
- Para vistas en area legacy PHP:
NotificationService.phplee$job->payload['action_url']y le agrega?job_id={$job->id}.- El valor resultante (ej:
'?loc=mmem#/movimientos/facturacion-lote?job_id=142') se almacena enmetadata.action_urlde la notificacion en la base de datos. - El backend no tiene un mapa de rutas: simplemente concatena el
job_idal valor que provino del frontend. Si el campo esta ausente en el payload, no se escribe en la metadata. - En Tier 2.5, el frontend append adicionalmente
¬if_id={id}(ID de la notificacion) alaction_urlalmacenado antes de navegar. Este sufijo no existe en la base de datos; se agrega en runtime porJobNotificationsContext.
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 (comoFacturacionLotesView),markAsReadse 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 detectarnotif_iden 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_idse 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 destino | Formato de action_url | Mecanismo de Tier 2.5 |
|---|---|---|
Vista React en area legacy PHP (requiere ?loc=) | ?loc={modulo}#/ruta/vista | window.location.href (full page redirect) |
| Vista React en SPA pura (solo hash) | #/ruta/vista | window.location.hash (in-SPA) |
Para habilitar auto-dispatch (single-click) en una nueva vista:
- En el servicio frontend, incluir
action_url: '?loc={modulo}#/ruta/de/la-vista'en el dispatch. - En la vista, implementar dos elementos:
- Step URL (efecto de montaje): leer
notif_idde la URL, guardarlo en unuseRef, limpiar la URL. - Reactive auto-dispatch effect con dependencias
[isLoading, handleNotificationAction]: cuandoisLoading=falsey el ref tiene valor, llamarhandleNotificationAction(notifId).
- Step URL (efecto de montaje): leer
- 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
- ADR-007: Tab-Scoped Job Bus via
window.BautistaJobBus - ADR-003: Polling HTTP → SSE (Fases)
- Spec:
openspec/specs/notification-bell/spec.md - Spec:
openspec/specs/facturacion-lotes-job/spec.md - Feature: Facturacion por Lotes