- Estado: Aceptada. Segundo bloque de la conversación y el centro de notificaciones. El lugar del perfil lo reemplaza el ADR-0079: ahora es un círculo con las iniciales en el dock.
- Fecha: 2026-10-31
Contexto
Con la conversación de cada incidente, quien escribía una pregunta no tenía cómo enterarse de la respuesta sin volver a abrir el incidente. Lo mismo pasaba con lo demás: el aviso “Te asignaron…” solo aparecía si la persona estaba conectada en ese momento y desaparecía a los pocos segundos. Cambios de estado y respuestas a una reapertura no avisaban nada. Hacía falta un lugar que guardara esos avisos y los mostrara hasta que la persona los viera.
Alternativas evaluadas
Para guardar los avisos:
- Tabla propia
notificationscon claves foráneas al mensaje o al registro del historial que la originó, más un catálogonotification_types. - La tabla de notificaciones de Laravel (
notifiable_type/notifiable_idy el contenido en JSON). - Calcularlos al vuelo desde el historial y la conversación, sin guardar nada.
Para mostrarlos:
- Un panel (
AppSheet): desde la derecha en portátil y monitor y desde abajo en celular y tablet, como la conversación. - Un menú desplegable bajo la campana.
- Una pantalla propia (“Notificaciones”) en el menú lateral.
Para los mensajes de una conversación:
- Un aviso por incidente que se agrupa mientras no se lea (“3 mensajes nuevos”).
- Un aviso por mensaje.
Decisión
- Campana a la derecha del encabezado en las cuatro vistas, con las sin leer (hasta “99+”). Abre un panel con dos pestañas:
- Conversaciones: mensajes nuevos en los incidentes en los que la persona participa. Llevan al incidente con la conversación abierta.
- Actividad: “te asignaron”, cambio de estado, reabierto y reapertura rechazada. Llevan al incidente.
- A quién se avisa (
Recipients, dominio). Nunca a quien hizo el cambio.- Mensaje: a quien creó el incidente, a quien lo tiene asignado y a quienes ya escribieron.
- Asignado: a quien lo recibe.
- Cambio de estado: a quien lo creó y a quien lo tiene asignado.
- Reabierto: a ellos dos y a quien pidió la reapertura.
- Reapertura rechazada: a quien la pidió.
- Datos:
notification_types(código, nombre y pestaña) ynotifications, con claves foráneas a la persona, al incidente y al mensaje o al registro del historial. No copian textos: el nombre de quien actuó, el mensaje o la nota salen de su origen. Sin relaciones polimórficas. - Mensajes agrupados: mientras un aviso de mensajes no se lea, el siguiente mensaje del mismo incidente lo reemplaza por uno más reciente que los cuenta (
occurrences). Lo garantiza un índice único sobre una columna calculada, como en las reaperturas pendientes. - Cómo nacen: los mensajes (
IncidentCommented) y el historial (IncidentActivityRecorded, nuevo) emiten eventos al confirmarse. Un oyente en la cola crea los avisos y mandanotifications.changedpor el canal personal. Crear avisos nunca demora ni hace fallar el cambio que los originó. - Leer: abrir un aviso lo marca como leído; “Marcar todo como leído” por pestaña; tener abierta la conversación de un incidente da por leídos sus mensajes, también los que lleguen mientras tanto.
- Lo que ya no se ve: si la persona perdió acceso al incidente, el aviso queda sin código, título ni mensaje (“Ya no tienes acceso a este incidente”). Es la misma regla de visibilidad del listado.
- Retención: 90 días, leídas o no (
NOTIFICATIONS_RETENTION_DAYS, limpieza diarianotifications:prune). - Perfil al pie del menú lateral: iniciales, nombre y rol, y al lado dos botones de solo ícono (Mi cuenta y Cerrar sesión, con su nombre al pasar el cursor). Cerrar sesión pide confirmación, por si se tocó sin querer. En tablet y con el menú plegado, en columna: iniciales y los dos botones. En celular, al final del menú hamburguesa. Así el encabezado queda con el logo y la campana. Se descartó un menú desplegable desde el perfil: un toque más para lo mismo y un menú flotando sobre el contenido.
- Paneles de borde a borde: desde la derecha, de arriba abajo y sin esquinas redondeadas, como el menú lateral; desde abajo, a todo lo ancho. Vale para todos los
AppSheet(también la conversación).
Alternativas descartadas
- Tabla de Laravel: es polimórfica (
notifiable_type) y guarda el aviso como JSON. Rompe la regla de claves foráneas reales y repite datos que ya existen en el historial. - Calcular al vuelo: “leído” no existe en el historial, y cada apertura de la campana cruzaría historial y conversación de todos los incidentes de la persona.
- Menú desplegable: en celular queda estrecho y no cabe una lista que se desplaza. El panel es el mismo patrón de la conversación, así que la persona ya lo conoce.
- Pantalla propia: obliga a salir de lo que se está haciendo para ver un aviso.
- Un aviso por mensaje: una conversación activa llenaría la campana de avisos del mismo incidente.
Consecuencias
- Nada se pierde por no estar conectado: los avisos esperan en la campana.
- El aviso emergente “Te asignaron…” se mantiene para quien está conectado; la campana lo guarda además.
- Los avisos de coordinación (creado sin asignar, crítico creado, reapertura pendiente y vencido) se sumaron a Actividad en el ADR-0085.
- Medido con 60.000 avisos (6.700 de una persona): la página de 20 tarda 0,8 ms porque filtra por los ids de los tipos y recorre
(user_id, id)hacia atrás; uniendo el catálogo tardaba 37 ms. - Quedan como mejoras: avisos por correo o push y preferencias por tipo de aviso.
Dónde está en el código
- Reglas:
Recipients.phpyNotificationType.php. - Casos de uso:
NotifyParticipants.phpyPersonalNotifications.php. - Datos:
DatabaseNotificationStore.php,DatabaseIncidentParticipants.phpy las migraciones000041y000042. - Front:
useNotificationsStore.ts,NotificationBell.vue,NotificationCenter.vueyAppUserMenu.vue. - Pruebas:
NotificationsTest.php,RecipientsTest.php,notifications.spec.tsy la E2Enotifications.spec.ts.