diff --git a/client/src/App/App.jsx b/client/src/App/App.jsx index af3c906a8..1cf4a8a06 100644 --- a/client/src/App/App.jsx +++ b/client/src/App/App.jsx @@ -48,8 +48,6 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline const { t } = useTranslation(); const navigate = useNavigate(); - const scenarioNotificationsOn = client?.getTreatment("Realtime_Notifications_UI") === "on"; - useEffect(() => { if (!navigator.onLine) { setOnline(false); @@ -203,12 +201,7 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline path="/manage/*" element={ - + @@ -220,12 +213,7 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline path="/tech/*" element={ - + diff --git a/client/src/contexts/SocketIO/useSocket.jsx b/client/src/contexts/SocketIO/useSocket.jsx index ee51def17..70f52311f 100644 --- a/client/src/contexts/SocketIO/useSocket.jsx +++ b/client/src/contexts/SocketIO/useSocket.jsx @@ -14,6 +14,7 @@ import { } from "../../graphql/notifications.queries.js"; import { useMutation } from "@apollo/client"; import { useTranslation } from "react-i18next"; +import { useSplitTreatments } from "@splitsoftware/splitio-react"; const SocketContext = createContext(null); @@ -25,11 +26,10 @@ const INITIAL_NOTIFICATIONS = 10; * @param bodyshop * @param navigate * @param currentUser - * @param scenarioNotificationsOn * @returns {JSX.Element} * @constructor */ -const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNotificationsOn }) => { +const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => { const socketRef = useRef(null); const [clientId, setClientId] = useState(null); const [isConnected, setIsConnected] = useState(false); @@ -37,6 +37,14 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNot const userAssociationId = bodyshop?.associations?.[0]?.id; const { t } = useTranslation(); + const { + treatments: { Realtime_Notifications_UI } + } = useSplitTreatments({ + attributes: {}, + names: ["Realtime_Notifications_UI"], + splitKey: bodyshop?.imexshopid + }); + const [markNotificationRead] = useMutation(MARK_NOTIFICATION_READ, { update: (cache, { data: { update_notifications } }) => { const timestamp = new Date().toISOString(); @@ -209,7 +217,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNot const handleNotification = (data) => { // Scenario Notifications have been disabled, bail. - if (!scenarioNotificationsOn) { + if (Realtime_Notifications_UI?.treatment !== "on") { return; } @@ -329,7 +337,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNot const handleSyncNotificationRead = ({ notificationId, timestamp }) => { // Scenario Notifications have been disabled, bail. - if (!scenarioNotificationsOn) { + if (Realtime_Notifications_UI?.treatment !== "on") { return; } @@ -371,7 +379,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNot const handleSyncAllNotificationsRead = ({ timestamp }) => { // Scenario Notifications have been disabled, bail. - if (!scenarioNotificationsOn) { + if (Realtime_Notifications_UI?.treatment !== "on") { return; } @@ -462,7 +470,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNot markAllNotificationsRead, navigate, currentUser, - scenarioNotificationsOn, + Realtime_Notifications_UI, t ]); @@ -474,7 +482,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNot isConnected, markNotificationRead, markAllNotificationsRead, - scenarioNotificationsOn + scenarioNotificationsOn: Realtime_Notifications_UI?.treatment === "on" }} > {children} diff --git a/hasura/migrations/1741904378259_create_index_notificiations_idx_jobs/down.sql b/hasura/migrations/1741904378259_create_index_notificiations_idx_jobs/down.sql new file mode 100644 index 000000000..a4029e3f4 --- /dev/null +++ b/hasura/migrations/1741904378259_create_index_notificiations_idx_jobs/down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "public"."notificiations_idx_jobs"; diff --git a/hasura/migrations/1741904378259_create_index_notificiations_idx_jobs/up.sql b/hasura/migrations/1741904378259_create_index_notificiations_idx_jobs/up.sql new file mode 100644 index 000000000..8fd3f3f34 --- /dev/null +++ b/hasura/migrations/1741904378259_create_index_notificiations_idx_jobs/up.sql @@ -0,0 +1,2 @@ +CREATE INDEX "notificiations_idx_jobs" on + "public"."notifications" using btree ("jobid"); diff --git a/hasura/migrations/1741904395934_create_index_notifications_idx_associations/down.sql b/hasura/migrations/1741904395934_create_index_notifications_idx_associations/down.sql new file mode 100644 index 000000000..17b162d35 --- /dev/null +++ b/hasura/migrations/1741904395934_create_index_notifications_idx_associations/down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "public"."notifications_idx_associations"; diff --git a/hasura/migrations/1741904395934_create_index_notifications_idx_associations/up.sql b/hasura/migrations/1741904395934_create_index_notifications_idx_associations/up.sql new file mode 100644 index 000000000..9d131b3b2 --- /dev/null +++ b/hasura/migrations/1741904395934_create_index_notifications_idx_associations/up.sql @@ -0,0 +1,2 @@ +CREATE INDEX "notifications_idx_associations" on + "public"."notifications" using btree ("associationid"); diff --git a/hasura/migrations/1741904614090_run_sql_migration/down.sql b/hasura/migrations/1741904614090_run_sql_migration/down.sql new file mode 100644 index 000000000..90dce4b23 --- /dev/null +++ b/hasura/migrations/1741904614090_run_sql_migration/down.sql @@ -0,0 +1,3 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE INDEX idx_notifications_created_at_not_read ON notifications(created_at desc, read) where read is null; diff --git a/hasura/migrations/1741904614090_run_sql_migration/up.sql b/hasura/migrations/1741904614090_run_sql_migration/up.sql new file mode 100644 index 000000000..ecca4610e --- /dev/null +++ b/hasura/migrations/1741904614090_run_sql_migration/up.sql @@ -0,0 +1 @@ +CREATE INDEX idx_notifications_created_at_not_read ON notifications(created_at desc, read) where read is null; diff --git a/hasura/migrations/1741904805838_run_sql_migration/down.sql b/hasura/migrations/1741904805838_run_sql_migration/down.sql new file mode 100644 index 000000000..f6b9c6e6e --- /dev/null +++ b/hasura/migrations/1741904805838_run_sql_migration/down.sql @@ -0,0 +1,3 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE INDEX idx_notifications_associations_not_read ON notifications(associationid, read) where read is null; diff --git a/hasura/migrations/1741904805838_run_sql_migration/up.sql b/hasura/migrations/1741904805838_run_sql_migration/up.sql new file mode 100644 index 000000000..7950b3bd9 --- /dev/null +++ b/hasura/migrations/1741904805838_run_sql_migration/up.sql @@ -0,0 +1 @@ +CREATE INDEX idx_notifications_associations_not_read ON notifications(associationid, read) where read is null; diff --git a/server/email/generateTemplate.js b/server/email/generateTemplate.js index 9d2815851..2887b2fd2 100644 --- a/server/email/generateTemplate.js +++ b/server/email/generateTemplate.js @@ -20,6 +20,11 @@ const defaultFooter = () => { const now = () => moment().format("MM/DD/YYYY @ hh:mm a"); +/** + * Generate the email template + * @param strings + * @returns {string} + */ const generateEmailTemplate = (strings) => { return ( ` diff --git a/server/notifications/queues/emailQueue.js b/server/notifications/queues/emailQueue.js index 823dff5ea..ff6702635 100644 --- a/server/notifications/queues/emailQueue.js +++ b/server/notifications/queues/emailQueue.js @@ -5,6 +5,7 @@ const { InstanceEndpoints } = require("../../utils/instanceMgr"); const { registerCleanupTask } = require("../../utils/cleanupManager"); const getBullMQPrefix = require("../../utils/getBullMQPrefix"); const devDebugLogger = require("../../utils/devDebugLogger"); +const moment = require("moment-timezone"); const EMAIL_CONSOLIDATION_DELAY_IN_MINS = (() => { const envValue = process.env?.EMAIL_CONSOLIDATION_DELAY_IN_MINS; @@ -59,7 +60,7 @@ const loadEmailQueue = async ({ pubClient, logger }) => { emailAddWorker = new Worker( "emailAdd", async (job) => { - const { jobId, jobRoNumber, bodyShopName, body, recipients } = job.data; + const { jobId, jobRoNumber, bodyShopName, bodyShopTimezone, body, recipients } = job.data; devDebugLogger(`Adding email notifications for jobId ${jobId}`); const redisKeyPrefix = `email:${devKey}:notifications:${jobId}`; @@ -72,6 +73,7 @@ const loadEmailQueue = async ({ pubClient, logger }) => { const detailsKey = `email:${devKey}:recipientDetails:${jobId}:${user}`; await pubClient.hsetnx(detailsKey, "firstName", firstName || ""); await pubClient.hsetnx(detailsKey, "lastName", lastName || ""); + await pubClient.hsetnx(detailsKey, "bodyShopTimezone", bodyShopTimezone); await pubClient.expire(detailsKey, NOTIFICATION_EXPIRATION / 1000); await pubClient.sadd(`email:${devKey}:recipients:${jobId}`, user); devDebugLogger(`Stored message for ${user} under ${userKey}: ${body}`); @@ -82,7 +84,7 @@ const loadEmailQueue = async ({ pubClient, logger }) => { if (flagSet) { await emailConsolidateQueue.add( "consolidate-emails", - { jobId, jobRoNumber, bodyShopName }, + { jobId, jobRoNumber, bodyShopName, bodyShopTimezone }, { jobId: `consolidate:${jobId}`, delay: EMAIL_CONSOLIDATION_DELAY, @@ -125,9 +127,11 @@ const loadEmailQueue = async ({ pubClient, logger }) => { const firstName = details.firstName || "User"; const multipleUpdateString = messages.length > 1 ? "Updates" : "Update"; const subject = `${multipleUpdateString} for job ${jobRoNumber || "N/A"} at ${bodyShopName}`; + const timezone = moment.tz.zone(details?.bodyShopTimezone) ? details.bodyShopTimezone : "UTC"; const emailBody = generateEmailTemplate({ header: `${multipleUpdateString} for Job ${jobRoNumber || "N/A"}`, subHeader: `Dear ${firstName},`, + dateLine: moment().tz(timezone).format("MM/DD/YYYY hh:mm a"), body: `

There have been updates to job ${jobRoNumber || "N/A"} at ${bodyShopName}: