Files
analiticaNucleo/nuxt4-app/app/composables/useNotifications.ts
josedario87 b6dc08e599
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 48s
Feat: Implementar sistema de notificaciones con historial por usuario
- Crear composable useNotifications con gestión por localStorage
  - Almacenamiento separado por usuario usando UID de Authentik
  - Auto-limpieza de notificaciones mayores a 30 días
  - Sincronización automática entre pestañas
  - Filtrado por tipo, búsqueda y gestión completa

- Crear wrapper useToast para guardar toasts automáticamente
  - Intercepta todos los toasts de la aplicación
  - Guarda historial sin afectar funcionalidad existente

- Implementar endpoints de API para notificaciones del backend
  - POST /api/notifications/send para enviar notificaciones
  - GET /api/notifications/list para obtener pendientes

- Actualizar página de notificaciones con funcionalidad real
  - Búsqueda y filtros por tipo (info, warning, success, error)
  - Eliminar individual o todas las notificaciones
  - Marcar como leídas individual o todas
  - Badges de origen (toast, backend, manual)
  - Estados vacíos con mensajes informativos

- Actualizar badge del sidebar con contador dinámico
  - Muestra número real de notificaciones no leídas
  - Se oculta cuando no hay notificaciones

- Inicializar sistema en app.vue
  - Auto-inicialización al montar la app
  - Limpieza automática de notificaciones antiguas
2025-10-30 18:03:37 -06:00

268 lines
6.5 KiB
TypeScript

import { ref, computed, watch } from 'vue'
import type { Ref } from 'vue'
export interface Notification {
id: string
type: 'info' | 'warning' | 'success' | 'error'
title: string
message: string
timestamp: number
read: boolean
origin: 'toast' | 'backend' | 'manual'
metadata?: Record<string, any>
}
const notifications: Ref<Notification[]> = ref([])
const unreadCount = computed(() => notifications.value.filter(n => !n.read).length)
// Días para retención de notificaciones
const RETENTION_DAYS = 30
const STORAGE_KEY_PREFIX = 'notifications-'
let currentUserId: string | null = null
let isInitialized = false
/**
* Obtiene la clave de localStorage para el usuario actual
*/
function getStorageKey(userId: string): string {
return `${STORAGE_KEY_PREFIX}${userId}`
}
/**
* Carga las notificaciones desde localStorage
*/
function loadNotifications(userId: string): Notification[] {
if (process.client) {
const key = getStorageKey(userId)
const stored = localStorage.getItem(key)
if (stored) {
try {
const parsed = JSON.parse(stored) as Notification[]
// Limpiar notificaciones antiguas (> RETENTION_DAYS días)
const cutoffTime = Date.now() - (RETENTION_DAYS * 24 * 60 * 60 * 1000)
return parsed.filter(n => n.timestamp > cutoffTime)
} catch (error) {
console.error('Error al parsear notificaciones:', error)
return []
}
}
}
return []
}
/**
* Guarda las notificaciones en localStorage
*/
function saveNotifications(userId: string, notifs: Notification[]): void {
if (process.client) {
const key = getStorageKey(userId)
localStorage.setItem(key, JSON.stringify(notifs))
}
}
/**
* Limpia las notificaciones del usuario actual del localStorage
*/
function clearStorageForUser(userId: string): void {
if (process.client) {
const key = getStorageKey(userId)
localStorage.removeItem(key)
}
}
/**
* Sincronización entre pestañas
*/
function setupStorageSync(userId: string): void {
if (process.client) {
const key = getStorageKey(userId)
const handleStorageChange = (event: StorageEvent) => {
if (event.key === key && event.newValue) {
try {
notifications.value = JSON.parse(event.newValue)
} catch (error) {
console.error('Error al sincronizar notificaciones:', error)
}
}
}
window.addEventListener('storage', handleStorageChange)
// Cleanup
return () => {
window.removeEventListener('storage', handleStorageChange)
}
}
}
/**
* Composable principal para gestionar notificaciones
*/
export function useNotifications() {
const { user } = useAuthentik()
/**
* Inicializa el sistema de notificaciones para el usuario actual
*/
function initialize(): void {
if (!user.value || !user.value.uid) {
console.warn('No se puede inicializar notificaciones sin usuario autenticado')
notifications.value = []
currentUserId = null
isInitialized = false
return
}
const userId = user.value.uid
// Si cambia el usuario, limpiar y recargar
if (currentUserId !== userId) {
currentUserId = userId
notifications.value = loadNotifications(userId)
// Guardar automáticamente cuando cambien las notificaciones
watch(notifications, (newNotifications) => {
if (currentUserId) {
saveNotifications(currentUserId, newNotifications)
}
}, { deep: true })
// Configurar sincronización entre pestañas
setupStorageSync(userId)
isInitialized = true
}
}
/**
* Agrega una nueva notificación
*/
function addNotification(
notification: Omit<Notification, 'id' | 'timestamp' | 'read'>
): void {
if (!isInitialized) {
initialize()
}
const newNotification: Notification = {
...notification,
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
timestamp: Date.now(),
read: false
}
notifications.value.unshift(newNotification)
}
/**
* Marca una notificación como leída
*/
function markAsRead(notificationId: string): void {
const notification = notifications.value.find(n => n.id === notificationId)
if (notification) {
notification.read = true
}
}
/**
* Marca todas las notificaciones como leídas
*/
function markAllAsRead(): void {
notifications.value.forEach(n => n.read = true)
}
/**
* Elimina una notificación específica
*/
function removeNotification(notificationId: string): void {
const index = notifications.value.findIndex(n => n.id === notificationId)
if (index !== -1) {
notifications.value.splice(index, 1)
}
}
/**
* Elimina todas las notificaciones
*/
function clearAll(): void {
notifications.value = []
if (currentUserId) {
clearStorageForUser(currentUserId)
}
}
/**
* Elimina notificaciones antiguas (> RETENTION_DAYS días)
*/
function cleanupOldNotifications(): void {
const cutoffTime = Date.now() - (RETENTION_DAYS * 24 * 60 * 60 * 1000)
notifications.value = notifications.value.filter(n => n.timestamp > cutoffTime)
}
/**
* Obtiene notificaciones filtradas
*/
function getFilteredNotifications(options?: {
type?: Notification['type'][]
read?: boolean
search?: string
limit?: number
}): Notification[] {
let filtered = [...notifications.value]
if (options?.type && options.type.length > 0) {
filtered = filtered.filter(n => options.type!.includes(n.type))
}
if (options?.read !== undefined) {
filtered = filtered.filter(n => n.read === options.read)
}
if (options?.search) {
const searchLower = options.search.toLowerCase()
filtered = filtered.filter(n =>
n.title.toLowerCase().includes(searchLower) ||
n.message.toLowerCase().includes(searchLower)
)
}
if (options?.limit) {
filtered = filtered.slice(0, options.limit)
}
return filtered
}
// Auto-inicializar cuando haya usuario
if (user.value && user.value.uid) {
initialize()
}
// Re-inicializar si cambia el usuario
watch(() => user.value?.uid, (newUid) => {
if (newUid) {
initialize()
} else {
notifications.value = []
currentUserId = null
isInitialized = false
}
})
return {
notifications: computed(() => notifications.value),
unreadCount,
addNotification,
markAsRead,
markAllAsRead,
removeNotification,
clearAll,
cleanupOldNotifications,
getFilteredNotifications,
initialize
}
}