/** * useNotifications Hook * * Hook for managing in-app notifications. * Supports fetching, marking as read, and executing actions. */ import { useState, useCallback, useEffect, useRef } from 'react'; import api from '../api'; // Types export interface NotificationAction { actionId: string; label: string; style: 'primary' | 'danger' | 'default'; } export interface UserNotification { id: string; userId: string; type: 'invitation' | 'system' | 'workflow' | 'mention'; status: 'unread' | 'read' | 'actioned' | 'dismissed'; title: string; message: string; icon?: string; referenceType?: string; referenceId?: string; actions?: NotificationAction[]; actionTaken?: string; actionResult?: string; createdAt: number; readAt?: number; actionedAt?: number; expiresAt?: number; } export interface NotificationActionResult { message: string; action: string; notificationId: string; } /** * Hook for managing notifications */ export function useNotifications() { const [notifications, setNotifications] = useState([]); const [unreadCount, setUnreadCount] = useState(0); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // Polling interval ref const pollingIntervalRef = useRef(null); /** * Fetch all notifications for the current user */ const fetchNotifications = useCallback(async ( options?: { status?: string; type?: string; limit?: number } ): Promise => { setLoading(true); setError(null); try { const params = new URLSearchParams(); if (options?.status) params.append('status', options.status); if (options?.type) params.append('type', options.type); if (options?.limit) params.append('limit', options.limit.toString()); const queryString = params.toString(); const url = `/api/notifications${queryString ? `?${queryString}` : ''}`; const response = await api.get(url); const data = response.data as UserNotification[]; setNotifications(data); return data; } catch (err: any) { const errorMessage = err.response?.data?.detail || 'Fehler beim Laden der Benachrichtigungen'; setError(errorMessage); return []; } finally { setLoading(false); } }, []); /** * Fetch unread count */ const fetchUnreadCount = useCallback(async (): Promise => { try { const response = await api.get('/api/notifications/unread-count'); const count = response.data.count; setUnreadCount(count); return count; } catch (err: any) { console.error('Failed to fetch unread count:', err); return 0; } }, []); /** * Mark a notification as read */ const markAsRead = useCallback(async (notificationId: string): Promise => { try { await api.put(`/api/notifications/${notificationId}/read`); // Update local state setNotifications(prev => prev.map(n => n.id === notificationId ? { ...n, status: 'read' as const, readAt: Date.now() / 1000 } : n ) ); // Update unread count setUnreadCount(prev => Math.max(0, prev - 1)); return true; } catch (err: any) { console.error('Failed to mark notification as read:', err); return false; } }, []); /** * Mark all notifications as read */ const markAllAsRead = useCallback(async (): Promise => { try { await api.put('/api/notifications/mark-all-read'); // Update local state setNotifications(prev => prev.map(n => n.status === 'unread' ? { ...n, status: 'read' as const, readAt: Date.now() / 1000 } : n ) ); setUnreadCount(0); return true; } catch (err: any) { console.error('Failed to mark all notifications as read:', err); return false; } }, []); /** * Execute an action on a notification */ const executeAction = useCallback(async ( notificationId: string, actionId: string ): Promise => { setLoading(true); setError(null); try { const response = await api.post(`/api/notifications/${notificationId}/action`, { actionId }); const result = response.data as NotificationActionResult; // Update local state setNotifications(prev => prev.map(n => n.id === notificationId ? { ...n, status: 'actioned' as const, actionTaken: actionId, actionResult: result.message, actionedAt: Date.now() / 1000 } : n ) ); // Update unread count if it was unread const notification = notifications.find(n => n.id === notificationId); if (notification?.status === 'unread') { setUnreadCount(prev => Math.max(0, prev - 1)); } return result; } catch (err: any) { const errorMessage = err.response?.data?.detail || 'Fehler bei der Ausführung der Aktion'; setError(errorMessage); return null; } finally { setLoading(false); } }, [notifications]); /** * Dismiss/delete a notification */ const dismissNotification = useCallback(async (notificationId: string): Promise => { try { await api.delete(`/api/notifications/${notificationId}`); // Update local state const notification = notifications.find(n => n.id === notificationId); setNotifications(prev => prev.filter(n => n.id !== notificationId)); // Update unread count if it was unread if (notification?.status === 'unread') { setUnreadCount(prev => Math.max(0, prev - 1)); } return true; } catch (err: any) { console.error('Failed to dismiss notification:', err); return false; } }, [notifications]); /** * Start polling for new notifications */ const startPolling = useCallback((intervalMs: number = 30000) => { // Clear any existing interval if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); } // Fetch immediately fetchUnreadCount(); // Set up polling pollingIntervalRef.current = setInterval(() => { fetchUnreadCount(); }, intervalMs); }, [fetchUnreadCount]); /** * Stop polling */ const stopPolling = useCallback(() => { if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } }, []); // Cleanup on unmount useEffect(() => { return () => { if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); } }; }, []); return { notifications, unreadCount, loading, error, fetchNotifications, fetchUnreadCount, markAsRead, markAllAsRead, executeAction, dismissNotification, startPolling, stopPolling }; } export default useNotifications;