import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useQuery } from "@apollo/client"; import { connect } from "react-redux"; import NotificationCenterComponent from "./notification-center.component"; import { GET_NOTIFICATIONS } from "../../graphql/notifications.queries"; import { createStructuredSelector } from "reselect"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js"; import day from "../../utils/day.js"; import { INITIAL_NOTIFICATIONS, useSocket } from "../../contexts/SocketIO/useSocket.js"; import { useIsEmployee } from "../../utils/useIsEmployee.js"; // This will be used to poll for notifications when the socket is disconnected const NOTIFICATION_POLL_INTERVAL_SECONDS = 60; /** * Notification Center Container * @param visible * @param onClose * @param bodyshop * @param unreadCount * @param currentUser * @returns {JSX.Element} * @constructor */ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount, currentUser }) => { const [showUnreadOnly, setShowUnreadOnly] = useState(false); const [notifications, setNotifications] = useState([]); const [isLoading, setIsLoading] = useState(false); const { isConnected, markNotificationRead, markAllNotificationsRead } = useSocket(); const notificationRef = useRef(null); const userAssociationId = bodyshop?.associations?.[0]?.id; const isEmployee = useIsEmployee(bodyshop, currentUser); const baseWhereClause = useMemo(() => { return { associationid: { _eq: userAssociationId } }; }, [userAssociationId]); const whereClause = useMemo(() => { return showUnreadOnly ? { ...baseWhereClause, read: { _is_null: true } } : baseWhereClause; }, [baseWhereClause, showUnreadOnly]); const { data, fetchMore, loading: queryLoading, refetch } = useQuery(GET_NOTIFICATIONS, { variables: { limit: INITIAL_NOTIFICATIONS, offset: 0, where: whereClause }, fetchPolicy: "cache-and-network", notifyOnNetworkStatusChange: true, pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(), skip: !userAssociationId || !isEmployee, onError: (err) => { console.error(`Error polling Notifications: ${err?.message || ""}`); setTimeout(() => refetch(), day.duration(2, "seconds").asMilliseconds()); } }); useEffect(() => { const handleClickOutside = (event) => { // Prevent open + close behavior from the header if (event.target.closest("#header-notifications")) return; if (visible && notificationRef.current && !notificationRef.current.contains(event.target)) { onClose(); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [visible, onClose]); useEffect(() => { if (data?.notifications && isEmployee) { const processedNotifications = data.notifications .map((notif) => { let scenarioText; let scenarioMeta; try { scenarioText = notif.scenario_text ? JSON.parse(notif.scenario_text) : []; scenarioMeta = notif.scenario_meta ? JSON.parse(notif.scenario_meta) : {}; } catch (e) { console.error("Error parsing JSON for notification:", notif.id, e); scenarioText = [notif.fcm_text || "Invalid notification data"]; scenarioMeta = {}; } if (!Array.isArray(scenarioText)) scenarioText = [scenarioText]; const roNumber = notif.job.ro_number; if (!Array.isArray(scenarioMeta)) scenarioMeta = [scenarioMeta]; return { id: notif.id, jobid: notif.jobid, associationid: notif.associationid, scenarioText, scenarioMeta, roNumber, created_at: notif.created_at, read: notif.read, __typename: notif.__typename }; }) .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); setNotifications(processedNotifications); } else if (!isEmployee) { setNotifications([]); // Clear notifications if not an employee } }, [data, isEmployee]); const loadMore = useCallback(() => { if (!queryLoading && data?.notifications.length && isEmployee) { setIsLoading(true); // Show spinner during fetchMore fetchMore({ variables: { offset: data.notifications.length, where: whereClause }, updateQuery: (prev, { fetchMoreResult }) => { if (!fetchMoreResult) return prev; return { notifications: [...prev.notifications, ...fetchMoreResult.notifications] }; } }) .catch((err) => { console.error("Fetch more error:", err); }) .finally(() => setIsLoading(false)); // Hide spinner when done } }, [data?.notifications?.length, fetchMore, queryLoading, whereClause, isEmployee]); const handleToggleUnreadOnly = (value) => { setShowUnreadOnly(value); }; const handleMarkAllRead = useCallback(() => { if (!isEmployee) return; // Do nothing if not an employee setIsLoading(true); markAllNotificationsRead() .then(() => { const timestamp = new Date().toISOString(); setNotifications((prev) => { const updatedNotifications = prev.map((notif) => notif.read === null && notif.associationid === userAssociationId ? { ...notif, read: timestamp } : notif ); // Filter out read notifications if in unread only mode return showUnreadOnly ? updatedNotifications.filter((notif) => !notif.read) : updatedNotifications; }); }) .catch((e) => console.error(`Error marking all notifications read: ${e?.message || ""}`)) .finally(() => setIsLoading(false)); }, [markAllNotificationsRead, userAssociationId, showUnreadOnly, isEmployee]); const handleNotificationClick = useCallback( (notificationId) => { setIsLoading(true); markNotificationRead({ variables: { id: notificationId } }) .then(() => { const timestamp = new Date().toISOString(); setNotifications((prev) => { const updatedNotifications = prev.map((notif) => notif.id === notificationId && !notif.read ? { ...notif, read: timestamp } : notif ); // Filter out the read notification if in unread only mode return showUnreadOnly ? updatedNotifications.filter((notif) => !notif.read) : updatedNotifications; }); }) .catch((e) => console.error(`Error marking notification read: ${e?.message || ""}`)) .finally(() => setIsLoading(false)); }, [markNotificationRead, showUnreadOnly] ); useEffect(() => { if (visible && !isConnected && isEmployee) { setIsLoading(true); refetch() .catch((err) => console.error(`Error re-fetching notifications: ${err?.message || ""}`)) .finally(() => setIsLoading(false)); } }, [visible, isConnected, refetch, isEmployee]); return ( ); }; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, currentUser: selectCurrentUser }); export default connect(mapStateToProps, null)(NotificationCenterContainer);