diff --git a/client/src/components/notification-center/notification-center.component.jsx b/client/src/components/notification-center/notification-center.component.jsx index 856a3570b..07a478893 100644 --- a/client/src/components/notification-center/notification-center.component.jsx +++ b/client/src/components/notification-center/notification-center.component.jsx @@ -17,7 +17,8 @@ const NotificationCenterComponent = ({ showUnreadOnly, toggleUnreadOnly, markAllRead, - loadMore + loadMore, + onNotificationClick }) => { const { t } = useTranslation(); @@ -26,10 +27,10 @@ const NotificationCenterComponent = ({ !notification.read && onNotificationClick(notification.id)} >
- {/* RO number as title/link */} - <Link to={`/manage/jobs/${notification.jobid}`} target="_blank"> + <Link + to={`/manage/jobs/${notification.jobid}`} + target="_blank" + onClick={(e) => { + e.stopPropagation(); // Prevent List.Item click handler from firing + !notification.read && onNotificationClick(notification.id); // Mark as read when link clicked + }} + > RO #{notification.roNumber} </Link> <Text type="secondary">{new Date(notification.created_at).toLocaleString()}</Text> @@ -75,7 +83,6 @@ const NotificationCenterComponent = ({ style={{ height: "400px", width: "100%" }} data={notifications} totalCount={notifications.length} - overscan={200} endReached={loadMore} itemContent={renderNotification} /> diff --git a/client/src/components/notification-center/notification-center.container.jsx b/client/src/components/notification-center/notification-center.container.jsx index 5bb5f1cf0..895ba03f5 100644 --- a/client/src/components/notification-center/notification-center.container.jsx +++ b/client/src/components/notification-center/notification-center.container.jsx @@ -2,7 +2,12 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { useMutation, useQuery } from "@apollo/client"; import { connect } from "react-redux"; import NotificationCenterComponent from "./notification-center.component"; -import { GET_NOTIFICATIONS, MARK_ALL_NOTIFICATIONS_READ } from "../../graphql/notifications.queries"; +import { + GET_NOTIFICATIONS, + MARK_ALL_NOTIFICATIONS_READ, + MARK_NOTIFICATION_READ, + GET_UNREAD_COUNT +} from "../../graphql/notifications.queries"; import { useSocket } from "../../contexts/SocketIO/socketContext.jsx"; import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../redux/user/user.selectors.js"; @@ -38,7 +43,7 @@ export function NotificationCenterContainer({ visible, onClose, bodyshop }) { fetchPolicy: "cache-and-network", notifyOnNetworkStatusChange: true, pollInterval: isConnected ? 0 : 30000, - skip: !userAssociationId, // Skip query if no userAssociationId + skip: !userAssociationId, onError: (err) => { setError(err.message); console.error("GET_NOTIFICATIONS error:", err); @@ -99,6 +104,53 @@ export function NotificationCenterContainer({ visible, onClose, bodyshop }) { } }); + const [markNotificationRead] = useMutation(MARK_NOTIFICATION_READ, { + update: (cache, { data: { update_notifications } }) => { + const timestamp = new Date().toISOString(); + const updatedNotification = update_notifications.returning[0]; + + // Update the notifications list + cache.modify({ + fields: { + notifications(existing = [], { readField }) { + return existing.map((notif) => { + if (readField("id", notif) === updatedNotification.id) { + return { ...notif, read: timestamp }; + } + return notif; + }); + } + } + }); + + // Update the unread count in notifications_aggregate + const unreadCountQuery = cache.readQuery({ + query: GET_UNREAD_COUNT, + variables: { associationid: userAssociationId } + }); + + if (unreadCountQuery?.notifications_aggregate?.aggregate?.count > 0) { + cache.writeQuery({ + query: GET_UNREAD_COUNT, + variables: { associationid: userAssociationId }, + data: { + notifications_aggregate: { + ...unreadCountQuery.notifications_aggregate, + aggregate: { + ...unreadCountQuery.notifications_aggregate.aggregate, + count: unreadCountQuery.notifications_aggregate.aggregate.count - 1 + } + } + } + }); + } + }, + onError: (err) => { + setError(err.message); + console.error("MARK_NOTIFICATION_READ error:", err); + } + }); + useEffect(() => { if (data?.notifications) { const processedNotifications = data.notifications @@ -167,12 +219,7 @@ export function NotificationCenterContainer({ visible, onClose, bodyshop }) { const timestamp = new Date().toISOString(); setNotifications((prev) => { const updatedNotifications = prev.map((notif) => - notif.read === null && notif.associationid === userAssociationId - ? { - ...notif, - read: timestamp - } - : notif + notif.read === null && notif.associationid === userAssociationId ? { ...notif, read: timestamp } : notif ); return [...updatedNotifications]; }); @@ -180,6 +227,24 @@ export function NotificationCenterContainer({ visible, onClose, bodyshop }) { .catch((e) => console.error(`Error marking all notifications read: ${e?.message || ""}`)); }; + const handleNotificationClick = useCallback( + (notificationId) => { + markNotificationRead({ + variables: { id: notificationId } + }) + .then(() => { + const timestamp = new Date().toISOString(); + setNotifications((prev) => { + return prev.map((notif) => + notif.id === notificationId && !notif.read ? { ...notif, read: timestamp } : notif + ); + }); + }) + .catch((e) => console.error(`Error marking notification read: ${e?.message || ""}`)); + }, + [markNotificationRead] + ); + useEffect(() => { if (visible && !isConnected) { refetch(); @@ -197,6 +262,7 @@ export function NotificationCenterContainer({ visible, onClose, bodyshop }) { toggleUnreadOnly={handleToggleUnreadOnly} markAllRead={handleMarkAllRead} loadMore={loadMore} + onNotificationClick={handleNotificationClick} /> ); } diff --git a/client/src/graphql/notifications.queries.js b/client/src/graphql/notifications.queries.js index ef8527e77..f2f9fa932 100644 --- a/client/src/graphql/notifications.queries.js +++ b/client/src/graphql/notifications.queries.js @@ -38,3 +38,14 @@ export const MARK_ALL_NOTIFICATIONS_READ = gql` } } `; + +export const MARK_NOTIFICATION_READ = gql` + mutation MarkNotificationRead($id: uuid!) { + update_notifications(where: { id: { _eq: $id } }, _set: { read: "now()" }) { + returning { + id + read + } + } + } +`; diff --git a/server/notifications/scenarioMapperr.js b/server/notifications/scenarioMapperr.js index 671223541..fb96a2a99 100644 --- a/server/notifications/scenarioMapperr.js +++ b/server/notifications/scenarioMapperr.js @@ -118,10 +118,12 @@ const notificationScenarios = [ { key: "supplement-imported", builder: supplementImportedBuilder + // spans multiple tables, }, // This one may be tricky as the jobid is not directly in the event data (this is probably wrong) // (should otherwise) // Status needs to mark meta data 'md_backorderd' for example + // Double check Jobid { key: "part-marked-back-ordered", table: "joblines",