All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 48s
- 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
268 lines
6.5 KiB
TypeScript
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
|
|
}
|
|
}
|