n.id),
- totalCount: notifications.length
- });
+ console.log(
+ "Rendering NotificationCenter with notifications:",
+ notifications.length,
+ notifications.map((n) => ({
+ id: n.id,
+ read: n.read,
+ created_at: n.created_at,
+ associationid: n.associationid
+ }))
+ );
return (
diff --git a/client/src/components/notification-center/notification-center.container.jsx b/client/src/components/notification-center/notification-center.container.jsx
index ff5e26d22..2085fc6c5 100644
--- a/client/src/components/notification-center/notification-center.container.jsx
+++ b/client/src/components/notification-center/notification-center.container.jsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from "react";
+import React, { useState, useEffect, useCallback } from "react";
import { useQuery, useMutation } from "@apollo/client";
import { connect } from "react-redux";
import NotificationCenterComponent from "./notification-center.component";
@@ -19,9 +19,9 @@ export function NotificationCenterContainer({ visible, onClose }) {
variables: {
limit: 20,
offset: 0,
- where: showUnreadOnly ? { read: { _is_null: true } } : {}
+ where: showUnreadOnly ? { read: { _is_null: true } } : {} // Default to all notifications
},
- fetchPolicy: "cache-and-network",
+ fetchPolicy: "cache-and-network", // Ensure reactivity to cache updates
notifyOnNetworkStatusChange: true,
onError: (err) => {
setError(err.message);
@@ -48,39 +48,69 @@ export function NotificationCenterContainer({ visible, onClose }) {
onError: (err) => setError(err.message)
});
+ // Remove refetchNotifications function and useEffect/context logic
useEffect(() => {
if (data?.notifications) {
const processedNotifications = data.notifications.map((notif) => {
let scenarioText;
+ let scenarioMeta;
try {
scenarioText =
typeof notif.scenario_text === "string" ? JSON.parse(notif.scenario_text) : notif.scenario_text || [];
+ scenarioMeta =
+ typeof notif.scenario_meta === "string" ? JSON.parse(notif.scenario_meta) : 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];
+ if (!Array.isArray(scenarioMeta)) scenarioMeta = [scenarioMeta];
+ console.log("Processed notification:", {
+ id: notif.id,
+ scenarioText,
+ scenarioMeta,
+ read: notif.read,
+ created_at: notif.created_at,
+ raw: notif // Log raw data for debugging
+ });
return {
id: notif.id,
jobid: notif.jobid,
associationid: notif.associationid,
scenarioText,
+ scenarioMeta, // Add scenarioMeta for completeness (optional for rendering)
created_at: notif.created_at,
read: notif.read,
__typename: notif.__typename
};
});
- console.log("Processed Notifications:", processedNotifications);
+ console.log(
+ "Notifications data updated:",
+ data?.notifications?.map((n) => ({
+ id: n.id,
+ read: n.read,
+ created_at: n.created_at
+ }))
+ );
+ console.log(
+ "Processed Notifications:",
+ processedNotifications.map((n) => ({
+ id: n.id,
+ read: n.read,
+ created_at: n.created_at
+ }))
+ );
console.log("Number of notifications to render:", processedNotifications.length);
setNotifications(processedNotifications);
setError(null);
} else {
console.log("No data yet or error in data:", data, queryError);
}
- }, [data]);
+ }, [data, queryError]);
useEffect(() => {
if (queryError || mutationError) {
@@ -88,7 +118,7 @@ export function NotificationCenterContainer({ visible, onClose }) {
}
}, [queryError, mutationError]);
- const loadMore = () => {
+ const loadMore = useCallback(() => {
if (!loading && data?.notifications.length) {
fetchMore({
variables: { offset: data.notifications.length },
@@ -100,7 +130,7 @@ export function NotificationCenterContainer({ visible, onClose }) {
}
}).catch((err) => setError(err.message));
}
- };
+ }, [data?.notifications?.length, fetchMore, loading]);
const handleToggleUnreadOnly = (value) => {
setShowUnreadOnly(value);
@@ -122,11 +152,20 @@ export function NotificationCenterContainer({ visible, onClose }) {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}
- }, [visible, loading, data]);
+ }, [visible, loading, data, loadMore]);
- console.log("Rendering NotificationCenter with notifications:", notifications.length, notifications);
+ console.log(
+ "Rendering NotificationCenter with notifications:",
+ notifications.length,
+ notifications.map((n) => ({
+ id: n.id,
+ read: n.read,
+ created_at: n.created_at
+ }))
+ );
return (
+ // Remove NotificationContext.Provider
{
const socketRef = useRef(null);
@@ -79,39 +80,88 @@ const useSocket = (bodyshop) => {
};
const handleNotification = (data) => {
- const { jobId, bodyShopId, notifications, notificationId } = data;
- console.log("handleNotification - Received", { jobId, bodyShopId, notificationId, notifications });
+ const { jobId, bodyShopId, notificationId, associationId, notifications } = data; // Changed to associationId (capital I)
+ console.log("handleNotification - Received", {
+ jobId,
+ bodyShopId,
+ notificationId,
+ associationId,
+ notifications
+ });
+ // Construct the notification object to match the cache/database format
const newNotification = {
+ __typename: "notifications",
id: notificationId,
jobid: jobId,
- associationid: null,
- scenario_text: notifications.map((notif) => notif.body),
+ associationid: associationId || null, // Use associationId from socket, default to null if missing
+ scenario_text: JSON.stringify(notifications.map((notif) => notif.body)), // Stringify as in the cache
fcm_text: notifications.map((notif) => notif.body).join(". ") + ".",
- scenario_meta: notifications.map((notif) => notif.variables || {}),
+ scenario_meta: JSON.stringify(notifications.map((notif) => notif.variables || {})), // Stringify as in the cache
created_at: new Date(notifications[0].timestamp).toISOString(),
- read: null,
- __typename: "notifications"
+ read: null // Assume unread unless specified otherwise
};
try {
+ // Use writeQuery to add the new notification to the cache, ensuring normalization and broadcasting
+ client.cache.writeQuery({
+ query: gql`
+ query GetNotifications {
+ notifications(order_by: { created_at: desc }) {
+ __typename
+ id
+ jobid
+ associationid
+ scenario_text
+ fcm_text
+ scenario_meta
+ created_at
+ read
+ }
+ }
+ `,
+ data: {
+ notifications: [
+ newNotification,
+ ...(
+ client.cache.readQuery({
+ query: gql`
+ query GetNotifications {
+ notifications(order_by: { created_at: desc }) {
+ __typename
+ id
+ jobid
+ associationid
+ scenario_text
+ fcm_text
+ scenario_meta
+ created_at
+ read
+ }
+ }
+ `
+ })?.notifications || []
+ ).filter((n) => n.id !== newNotification.id)
+ ].sort((a, b) => new Date(b.created_at) - new Date(a.created_at)) // Sort by created_at desc
+ },
+ broadcast: true // Notify dependent queries of the cache update
+ });
+
+ console.log("Cache updated with new notification:", newNotification);
+
+ // Update notifications_aggregate for unread count
client.cache.modify({
id: "ROOT_QUERY",
fields: {
- notifications(existing = []) {
- if (existing.some((n) => n.id === newNotification.id)) return existing;
- console.log("Adding to cache", newNotification);
- return [newNotification, ...existing];
- },
notifications_aggregate(existing = { aggregate: { count: 0 } }) {
- // Fallback if aggregate isn’t in schema yet
- if (!existing) return existing;
- console.log("Updating unread count", existing.aggregate.count + 1);
+ const isUnread = newNotification.read === null;
+ const countChange = isUnread ? 1 : 0;
+ console.log("Updating unread count from socket:", existing.aggregate.count + countChange);
return {
...existing,
aggregate: {
...existing.aggregate,
- count: existing.aggregate.count + 1
+ count: existing.aggregate.count + countChange
}
};
}
@@ -183,7 +233,7 @@ const useSocket = (bodyshop) => {
socketRef.current = null;
}
};
- }, [bodyshop, client.cache]);
+ }, [bodyshop, notification]);
return { socket: socketRef.current, clientId };
};
diff --git a/client/src/graphql/notifications.queries.js b/client/src/graphql/notifications.queries.js
index 44e0d3da2..6a782ba75 100644
--- a/client/src/graphql/notifications.queries.js
+++ b/client/src/graphql/notifications.queries.js
@@ -35,7 +35,7 @@ export const MARK_ALL_NOTIFICATIONS_READ = gql`
export const SUBSCRIBE_TO_NOTIFICATIONS = gql`
subscription SubscribeToNotifications {
- notifications(order_by: { created_at: desc }, limit: 1) {
+ notifications(where: { read: { _is_null: true } }, order_by: { created_at: desc }) {
id
jobid
associationid
diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json
index 75781d098..1f58aa777 100644
--- a/client/src/translations/en_us/common.json
+++ b/client/src/translations/en_us/common.json
@@ -3776,7 +3776,11 @@
"add-watchers": "Add Watchers",
"employee-search": "Search for an Employee",
"teams-search": "Search for a Team",
- "add-watchers-team": "Add Team Members"
+ "add-watchers-team": "Add Team Members",
+ "new-notification-title": "New Notification:",
+ "show-unread-only": "Show Unread",
+ "mark-all-read": "Mark Read",
+ "loading": "Loading Notifications..."
},
"actions": {
"remove": "remove"
diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml
index 0fb3007c5..ba6d7a12c 100644
--- a/hasura/metadata/tables.yaml
+++ b/hasura/metadata/tables.yaml
@@ -4940,6 +4940,7 @@
_eq: X-Hasura-User-Id
- active:
_eq: true
+ allow_aggregations: true
comment: ""
update_permissions:
- role: user
diff --git a/server/notifications/queues/appQueue.js b/server/notifications/queues/appQueue.js
index a600fb525..668ffd8c6 100644
--- a/server/notifications/queues/appQueue.js
+++ b/server/notifications/queues/appQueue.js
@@ -200,25 +200,20 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
// Emit notifications to users via Socket.io with notification ID
for (const [user, bodyShopData] of Object.entries(allNotifications)) {
const userMapping = await redisHelpers.getUserSocketMapping(user);
- logger.logger.debug(`User socket mapping for ${user}: ${JSON.stringify(userMapping)}`);
+ // Get all recipients for the user and extract the associationId (employeeId)
+ const userRecipients = recipients.filter((r) => r.user === user);
+ const associationId = userRecipients[0]?.employeeId;
for (const [bodyShopId, notifications] of Object.entries(bodyShopData)) {
const notificationId = notificationIdMap.get(`${user}:${bodyShopId}`);
if (userMapping && userMapping[bodyShopId]?.socketIds) {
userMapping[bodyShopId].socketIds.forEach((socketId) => {
- logger.logger.debug(
- `Emitting to socket ${socketId}: ${JSON.stringify({
- jobId,
- bodyShopId,
- notifications,
- notificationId
- })}`
- );
ioRedis.to(socketId).emit("notification", {
jobId,
bodyShopId,
notifications,
- notificationId
+ notificationId,
+ associationId // now included in the emit payload
});
});
logger.logger.info(