frontend_nyla/src/hooks/useNotifications.ts
2026-01-26 01:29:24 +01:00

277 lines
7.1 KiB
TypeScript

/**
* 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<UserNotification[]>([]);
const [unreadCount, setUnreadCount] = useState<number>(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Polling interval ref
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
/**
* Fetch all notifications for the current user
*/
const fetchNotifications = useCallback(async (
options?: { status?: string; type?: string; limit?: number }
): Promise<UserNotification[]> => {
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<number> => {
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<boolean> => {
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<boolean> => {
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<NotificationActionResult | null> => {
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<boolean> => {
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;