From 05ec62595d8dc4e0056cbe7a4a4962e3c78012c1 Mon Sep 17 00:00:00 2001 From: alma Date: Fri, 16 Jan 2026 00:27:07 +0100 Subject: [PATCH] refactor Notifications --- app/api/notifications/mark-all-read/route.ts | 56 ++++++++++++++++++++ components/notification-badge-enhanced.tsx | 15 ++++-- hooks/use-notifications.ts | 35 ++++++++++++ 3 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 app/api/notifications/mark-all-read/route.ts diff --git a/app/api/notifications/mark-all-read/route.ts b/app/api/notifications/mark-all-read/route.ts new file mode 100644 index 0000000..6e5ec4b --- /dev/null +++ b/app/api/notifications/mark-all-read/route.ts @@ -0,0 +1,56 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from "@/app/api/auth/options"; +import { NotificationRegistry } from '@/lib/services/notifications/notification-registry'; +import { logger } from '@/lib/logger'; + +// POST /api/notifications/mark-all-read +export async function POST(request: Request) { + try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); + } + + const registry = NotificationRegistry.getInstance(); + + // Reset all counts to 0 for this user + try { + const redis = await import('@/lib/redis').then(m => m.getRedisClient()); + const countKey = `notifications:count:${session.user.id}`; + + // Set all counts to 0 + const emptyCount = { + total: 0, + unread: 0, + sources: { + email: { total: 0, unread: 0 }, + rocketchat: { total: 0, unread: 0 }, + leantime: { total: 0, unread: 0 }, + calendar: { total: 0, unread: 0 }, + }, + }; + + await redis.set(countKey, JSON.stringify(emptyCount), 'EX', 30); + + logger.debug('[NOTIFICATIONS_MARK_ALL_READ] All notifications marked as read', { + userId: session.user.id, + }); + } catch (error) { + logger.error('[NOTIFICATIONS_MARK_ALL_READ] Error marking all as read', { + userId: session.user.id, + error: error instanceof Error ? error.message : String(error), + }); + } + + return NextResponse.json({ success: true }); + } catch (error: any) { + logger.error('[NOTIFICATIONS_MARK_ALL_READ] Error', { + error: error instanceof Error ? error.message : String(error), + }); + return NextResponse.json( + { error: "Internal server error", message: error.message }, + { status: 500 } + ); + } +} diff --git a/components/notification-badge-enhanced.tsx b/components/notification-badge-enhanced.tsx index 4e8b266..5062111 100644 --- a/components/notification-badge-enhanced.tsx +++ b/components/notification-badge-enhanced.tsx @@ -30,19 +30,28 @@ type FilterOption = 'all' | 'email' | 'rocketchat' | 'leantime' | 'calendar'; // Use React.memo to prevent unnecessary re-renders export const NotificationBadge = memo(function NotificationBadge({ className }: NotificationBadgeProps) { const { data: session, status } = useSession(); - const { notifications, notificationCount, fetchNotifications, markAsRead, loading, error } = useNotifications(); + const { notifications, notificationCount, fetchNotifications, markAsRead, markAllAsRead, loading, error } = useNotifications(); const hasUnread = notificationCount.unread > 0; const [isOpen, setIsOpen] = useState(false); const [sortBy, setSortBy] = useState('newest'); const [filterBy, setFilterBy] = useState('all'); const [displayLimit, setDisplayLimit] = useState(10); + const [hasMarkedAsRead, setHasMarkedAsRead] = useState(false); - // Fetch notifications when dropdown opens + // When dropdown opens, mark all as read and fetch notifications useEffect(() => { if (isOpen && status === 'authenticated') { + // Mark all as read when opening (only once per open) + if (!hasMarkedAsRead) { + markAllAsRead(); + setHasMarkedAsRead(true); + } fetchNotifications(1, 50, filterBy === 'all' ? undefined : filterBy); + } else if (!isOpen) { + // Reset flag when dropdown closes + setHasMarkedAsRead(false); } - }, [isOpen, status, filterBy, fetchNotifications]); + }, [isOpen, status, filterBy, fetchNotifications, markAllAsRead, hasMarkedAsRead]); // Sort and filter notifications const sortedAndFilteredNotifications = useMemo(() => { diff --git a/hooks/use-notifications.ts b/hooks/use-notifications.ts index 7f14473..7278e5e 100644 --- a/hooks/use-notifications.ts +++ b/hooks/use-notifications.ts @@ -69,6 +69,40 @@ export function useNotifications() { } }, [session?.user]); + // Mark all notifications as read (when dropdown is opened) + const markAllAsRead = useCallback(async () => { + if (!session?.user || !isMountedRef.current) return false; + + try { + const response = await fetch('/api/notifications/mark-all-read', { + method: 'POST', + credentials: 'include', + }); + + if (!response.ok) { + throw new Error('Failed to mark all notifications as read'); + } + + // Update local state - reset count to 0 + setNotificationCount({ + total: 0, + unread: 0, + sources: { + email: { total: 0, unread: 0 }, + rocketchat: { total: 0, unread: 0 }, + leantime: { total: 0, unread: 0 }, + calendar: { total: 0, unread: 0 }, + }, + }); + + return true; + } catch (err: any) { + console.error('Error marking all notifications as read:', err); + setError(err.message || 'Failed to mark all notifications as read'); + return false; + } + }, [session?.user]); + // Mark notification as read const markAsRead = useCallback(async (notificationId: string) => { if (!session?.user || !isMountedRef.current) return false; @@ -208,5 +242,6 @@ export function useNotifications() { fetchNotifications, fetchNotificationCount: () => fetchNotificationCount(true), markAsRead, + markAllAsRead, }; } \ No newline at end of file