"use client"; import { useTranslations } from "next-intl"; import React, { createContext, ReactElement, useCallback, useContext, useEffect, useRef, useState } from "react"; import { SharedProvider } from "../../../contexts"; import { Modules } from "../../../core"; import { useI18nRouter } from "../../../i18n"; import { BreadcrumbItemData } from "../../../interfaces"; import { NotificationMenuItem, NotificationToast } from "../components/notifications/Notification"; import { getRoleId, isRolesConfigured } from "../../../roles"; import { RoleInterface } from "../../role"; import { useCurrentUserContext } from "../../user/contexts/CurrentUserContext"; import { NotificationInterface } from "../data"; import { NotificationService } from "../data/notification.service"; interface NotificationContextType { notifications: NotificationInterface[]; setNotifications: (notifications: NotificationInterface[]) => void; addNotification: (notification: NotificationInterface) => void; addSocketNotifications: (socketNotifications: NotificationInterface[]) => void; loadNotifications: () => Promise; generateNotification: (notification: NotificationInterface, closePopover: () => void) => ReactElement; generateToastNotification: ( notification: NotificationInterface, t: any, generateUrl: any, ) => { title: string; description: string | ReactElement; action?: { label: string; onClick: () => void; }; }; markNotificationsAsRead: (ids: string[]) => Promise; isLoading: boolean; error: string | null; lastLoaded: number; shouldRefresh: boolean; } const NotificationContext = createContext(undefined); type NotificationContextProviderProps = { children: React.ReactNode; }; export const NotificationContextProvider = ({ children }: NotificationContextProviderProps) => { const t = useTranslations(); const router = useI18nRouter(); const [notifications, setNotifications] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [lastLoaded, setLastLoaded] = useState(0); const [hasInitiallyLoaded, setHasInitiallyLoaded] = useState(false); // Tracks an in-flight loadNotifications() request so concurrent callers // (the provider's mount effect + any consumer that also loads on mount) // share a single GET /notifications instead of each firing their own. const inFlightRef = useRef | null>(null); // Access current user for initial load check const { currentUser } = useCurrentUserContext(); // Calculate shouldRefresh (5 minute cache) const shouldRefresh = Date.now() - lastLoaded > 5 * 60 * 1000; const addNotification = useCallback((notification: NotificationInterface) => { setNotifications((prev) => { // Check if notification already exists to prevent duplicates const exists = prev.some((n) => n.id === notification.id); if (exists) return prev; return [notification, ...prev]; }); }, []); const addSocketNotifications = useCallback((socketNotifications: NotificationInterface[]) => { setNotifications((prev) => { const newNotifications = socketNotifications.filter( (newNotif) => !prev.some((existingNotif) => existingNotif.id === newNotif.id), ); return [...prev, ...newNotifications]; }); }, []); const loadNotifications = useCallback(async () => { // Coalesce concurrent callers into one request. Without this, the several // mount-time loaders fire before the first fetch resolves (lastLoaded is // still 0), so each issues its own GET /notifications. if (inFlightRef.current) return inFlightRef.current; const request = (async () => { setIsLoading(true); setError(null); try { const fetchedNotifications = await NotificationService.findMany({}); setNotifications(fetchedNotifications); setLastLoaded(Date.now()); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Failed to load notifications"; setError(errorMessage); } finally { setIsLoading(false); inFlightRef.current = null; } })(); inFlightRef.current = request; return request; }, []); // Initial load - runs once per session when user is available and not admin useEffect(() => { if (hasInitiallyLoaded || !currentUser) return; // Skip for admin users if (isRolesConfigured()) { const isAdmin = currentUser.roles?.some((role: RoleInterface) => role.id === getRoleId().Administrator); if (isAdmin) { setHasInitiallyLoaded(true); return; } } loadNotifications(); setHasInitiallyLoaded(true); }, [currentUser, hasInitiallyLoaded, loadNotifications]); const markNotificationsAsRead = useCallback(async (ids: string[]) => { setIsLoading(true); setError(null); try { const data: any = { data: ids.map((id: string) => ({ type: Modules.Notification.name, id: id, attributes: { isRead: true, }, meta: {}, relationships: {}, })), }; await NotificationService.markAsRead({ data: data }); const allNotifications = await NotificationService.findMany({}); setNotifications(allNotifications); setLastLoaded(Date.now()); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Failed to mark notifications as read"; setError(errorMessage); throw error; } finally { setIsLoading(false); } }, []); const generateToastNotification = ( notification: NotificationInterface, t: any, generateUrl: any, ): { title: string; description: string | ReactElement; action?: { label: string; onClick: () => void; }; } => { return NotificationToast(notification, t, generateUrl, router); }; const generateNotification = (notification: NotificationInterface, closePopover: () => void) => { return ; }; const breadcrumb = () => { const response: BreadcrumbItemData[] = []; response.push({ name: t(`entities.notifications`, { count: 2 }), }); return response; }; const title = () => { const response: any = { type: t(`entities.notifications`, { count: 2 }), }; return response; }; return ( {children} ); }; export const useNotificationContext = (): NotificationContextType => { const context = useContext(NotificationContext); if (context === undefined) { throw new Error(`Notification.messages.errors.use_context`); } return context; };