Files
bodyshop/client/src/components/notification-center/notification-center.container.jsx
2025-03-07 10:58:01 -05:00

192 lines
6.7 KiB
JavaScript

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 { INITIAL_NOTIFICATIONS, useSocket } from "../../contexts/SocketIO/useSocket.jsx";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors.js";
import day from "../../utils/day.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
* @returns {JSX.Element}
* @constructor
*/
const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }) => {
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 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,
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) {
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);
}
}, [data]);
const loadMore = useCallback(() => {
if (!queryLoading && data?.notifications.length) {
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]);
const handleToggleUnreadOnly = (value) => {
setShowUnreadOnly(value);
};
const handleMarkAllRead = useCallback(() => {
setIsLoading(true);
markAllNotificationsRead()
.then(() => {
const timestamp = new Date().toISOString();
setNotifications((prev) =>
prev.map((notif) =>
notif.read === null && notif.associationid === userAssociationId ? { ...notif, read: timestamp } : notif
)
);
})
.catch((e) => console.error(`Error marking all notifications read: ${e?.message || ""}`))
.finally(() => setIsLoading(false));
}, [markAllNotificationsRead, userAssociationId]);
const handleNotificationClick = useCallback(
(notificationId) => {
setIsLoading(true);
markNotificationRead({ variables: { id: notificationId } })
.then(() => {
const timestamp = new Date().toISOString();
setNotifications((prev) =>
prev.map((notif) => (notif.id === notificationId && !notif.read ? { ...notif, read: timestamp } : notif))
);
})
.catch((e) => console.error(`Error marking notification read: ${e?.message || ""}`))
.finally(() => setIsLoading(false));
},
[markNotificationRead]
);
useEffect(() => {
if (visible && !isConnected) {
setIsLoading(true);
refetch()
.catch((err) => console.error(`Error re-fetching notifications: ${err?.message || ""}`))
.finally(() => setIsLoading(false));
}
}, [visible, isConnected, refetch]);
return (
<NotificationCenterComponent
ref={notificationRef}
visible={visible}
onClose={onClose}
notifications={notifications}
loading={isLoading}
showUnreadOnly={showUnreadOnly}
toggleUnreadOnly={handleToggleUnreadOnly}
markAllRead={handleMarkAllRead}
loadMore={loadMore}
onNotificationClick={handleNotificationClick}
unreadCount={unreadCount}
/>
);
};
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
export default connect(mapStateToProps, null)(NotificationCenterContainer);