From 90618213477a1bd3afe738e173e3f27e4b7b4b08 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 11 Mar 2025 11:00:42 -0400 Subject: [PATCH 1/4] IO-3166-Global-Notifications-Part-2: Fixed unread notifications not vanishing once marked as read in unread only --- .../notification-center.container.jsx | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/client/src/components/notification-center/notification-center.container.jsx b/client/src/components/notification-center/notification-center.container.jsx index b73c12688..ae12552f3 100644 --- a/client/src/components/notification-center/notification-center.container.jsx +++ b/client/src/components/notification-center/notification-center.container.jsx @@ -132,15 +132,22 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount } markAllNotificationsRead() .then(() => { const timestamp = new Date().toISOString(); - setNotifications((prev) => - prev.map((notif) => - notif.read === null && notif.associationid === userAssociationId ? { ...notif, read: timestamp } : notif - ) - ); + 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]); + }, [markAllNotificationsRead, userAssociationId, showUnreadOnly]); const handleNotificationClick = useCallback( (notificationId) => { @@ -148,14 +155,18 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount } markNotificationRead({ variables: { id: notificationId } }) .then(() => { const timestamp = new Date().toISOString(); - setNotifications((prev) => - prev.map((notif) => (notif.id === notificationId && !notif.read ? { ...notif, read: timestamp } : notif)) - ); + 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] + [markNotificationRead, showUnreadOnly] ); useEffect(() => { From 8d36ad35891598cbf25740af0d9c15536dc53457 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 11 Mar 2025 11:38:56 -0400 Subject: [PATCH 2/4] IO-3166-Global-Notifications-Part-2: checkpoint --- hasura/metadata/tables.yaml | 4 +-- server/notifications/scenarioBuilders.js | 31 ++++++++++++++++++++---- server/notifications/scenarioParser.js | 5 ++-- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index 7b3f2a333..1be15efad 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -3303,7 +3303,7 @@ request_transform: body: action: transform - template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body?.event?.session_variables?.x-hasura-user-id ?? \"Internal\"}}\r\n }, \r\n \"op\": \"UPDATE\",\r\n \"data\": {\r\n \"old\": {\r\n \"id\": {{$body.event.data.old.id}},\r\n \"jobid\": {{$body.event.data.old.jobid}},\r\n \"critical\": {{$body.event.data.old.critical}},\r\n \"status\": {{$body.event.data.old.status}},\r\n \"line_desc\": {{$body.event.data.old.line_desc}}\r\n },\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"jobid\": {{$body.event.data.new.jobid}},\r\n \"critical\": {{$body.event.data.new.critical}},\r\n \"status\": {{$body.event.data.new.status}},\r\n \"line_desc\": {{$body.event.data.new.line_desc}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_joblines\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"joblines\"\r\n }\r\n}\r\n" + template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body?.event?.session_variables?.x-hasura-user-id ?? \"Internal\"}},\r\n \"x-hasura-role\": {{$body?.event?.session_variables?.x-hasura-role ?? \"Internal\"}}\r\n }, \r\n \"op\": \"UPDATE\",\r\n \"data\": {\r\n \"old\": {\r\n \"id\": {{$body.event.data.old.id}},\r\n \"jobid\": {{$body.event.data.old.jobid}},\r\n \"critical\": {{$body.event.data.old.critical}},\r\n \"status\": {{$body.event.data.old.status}},\r\n \"line_desc\": {{$body.event.data.old.line_desc}}\r\n },\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"jobid\": {{$body.event.data.new.jobid}},\r\n \"critical\": {{$body.event.data.new.critical}},\r\n \"status\": {{$body.event.data.new.status}},\r\n \"line_desc\": {{$body.event.data.new.line_desc}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_joblines\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"joblines\"\r\n }\r\n}\r\n" method: POST query_params: {} template_engine: Kriti @@ -4573,7 +4573,7 @@ request_transform: body: action: transform - template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body?.event?.session_variables?.x-hasura-user-id ?? \"Internal\"}}\r\n }, \r\n \"op\": \"UPDATE\",\r\n \"data\": {\r\n \"old\": {\r\n \"id\": {{$body.event.data.old.id}},\r\n \"ro_number\": {{$body.event.data.old.ro_number}},\r\n \"queued_for_parts\": {{$body.event.data.old.queued_for_parts}},\r\n \"employee_prep\": {{$body.event.data.old.employee_prep}},\r\n \"clm_total\": {{$body.event.data.old.clm_total}},\r\n \"towin\": {{$body.event.data.old.towin}},\r\n \"employee_body\": {{$body.event.data.old.employee_body}},\r\n \"converted\": {{$body.event.data.old.converted}},\r\n \"scheduled_in\": {{$body.event.data.old.scheduled_in}},\r\n \"scheduled_completion\": {{$body.event.data.old.scheduled_completion}},\r\n \"scheduled_delivery\": {{$body.event.data.old.scheduled_delivery}},\r\n \"actual_delivery\": {{$body.event.data.old.actual_delivery}},\r\n \"actual_completion\": {{$body.event.data.old.actual_completion}},\r\n \"alt_transport\": {{$body.event.data.old.alt_transport}},\r\n \"date_exported\": {{$body.event.data.old.date_exported}},\r\n \"status\": {{$body.event.data.old.status}},\r\n \"employee_csr\": {{$body.event.data.old.employee_csr}},\r\n \"actual_in\": {{$body.event.data.old.actual_in}},\r\n \"deliverchecklist\": {{$body.event.data.old.deliverchecklist}},\r\n \"comment\": {{$body.event.data.old.comment}},\r\n \"employee_refinish\": {{$body.event.data.old.employee_refinish}},\r\n \"inproduction\": {{$body.event.data.old.inproduction}},\r\n \"production_vars\": {{$body.event.data.old.production_vars}},\r\n \"intakechecklist\": {{$body.event.data.old.intakechecklist}},\r\n \"cieca_ttl\": {{$body.event.data.old.cieca_ttl}},\r\n \"date_invoiced\": {{$body.event.data.old.date_invoiced}}\r\n },\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"ro_number\": {{$body.event.data.old.ro_number}},\r\n \"queued_for_parts\": {{$body.event.data.new.queued_for_parts}},\r\n \"employee_prep\": {{$body.event.data.new.employee_prep}},\r\n \"clm_total\": {{$body.event.data.new.clm_total}},\r\n \"towin\": {{$body.event.data.new.towin}},\r\n \"employee_body\": {{$body.event.data.new.employee_body}},\r\n \"converted\": {{$body.event.data.new.converted}},\r\n \"scheduled_in\": {{$body.event.data.new.scheduled_in}},\r\n \"scheduled_completion\": {{$body.event.data.new.scheduled_completion}},\r\n \"scheduled_delivery\": {{$body.event.data.new.scheduled_delivery}},\r\n \"actual_delivery\": {{$body.event.data.new.actual_delivery}},\r\n \"actual_completion\": {{$body.event.data.new.actual_completion}},\r\n \"alt_transport\": {{$body.event.data.new.alt_transport}},\r\n \"date_exported\": {{$body.event.data.new.date_exported}},\r\n \"status\": {{$body.event.data.new.status}},\r\n \"employee_csr\": {{$body.event.data.new.employee_csr}},\r\n \"actual_in\": {{$body.event.data.new.actual_in}},\r\n \"deliverchecklist\": {{$body.event.data.new.deliverchecklist}},\r\n \"comment\": {{$body.event.data.new.comment}},\r\n \"employee_refinish\": {{$body.event.data.new.employee_refinish}},\r\n \"inproduction\": {{$body.event.data.new.inproduction}},\r\n \"production_vars\": {{$body.event.data.new.production_vars}},\r\n \"intakechecklist\": {{$body.event.data.new.intakechecklist}},\r\n \"cieca_ttl\": {{$body.event.data.new.cieca_ttl}},\r\n \"date_invoiced\": {{$body.event.data.new.date_invoiced}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_jobs\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"jobs\"\r\n }\r\n}\r\n" + template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body?.event?.session_variables?.x-hasura-user-id ?? \"Internal\"}},\r\n \"x-hasura-role\": {{$body?.event?.session_variables?.x-hasura-role ?? \"Internal\"}}\r\n }, \r\n \"op\": \"UPDATE\",\r\n \"data\": {\r\n \"old\": {\r\n \"id\": {{$body.event.data.old.id}},\r\n \"ro_number\": {{$body.event.data.old.ro_number}},\r\n \"queued_for_parts\": {{$body.event.data.old.queued_for_parts}},\r\n \"employee_prep\": {{$body.event.data.old.employee_prep}},\r\n \"clm_total\": {{$body.event.data.old.clm_total}},\r\n \"towin\": {{$body.event.data.old.towin}},\r\n \"employee_body\": {{$body.event.data.old.employee_body}},\r\n \"converted\": {{$body.event.data.old.converted}},\r\n \"scheduled_in\": {{$body.event.data.old.scheduled_in}},\r\n \"scheduled_completion\": {{$body.event.data.old.scheduled_completion}},\r\n \"scheduled_delivery\": {{$body.event.data.old.scheduled_delivery}},\r\n \"actual_delivery\": {{$body.event.data.old.actual_delivery}},\r\n \"actual_completion\": {{$body.event.data.old.actual_completion}},\r\n \"alt_transport\": {{$body.event.data.old.alt_transport}},\r\n \"date_exported\": {{$body.event.data.old.date_exported}},\r\n \"status\": {{$body.event.data.old.status}},\r\n \"employee_csr\": {{$body.event.data.old.employee_csr}},\r\n \"actual_in\": {{$body.event.data.old.actual_in}},\r\n \"deliverchecklist\": {{$body.event.data.old.deliverchecklist}},\r\n \"comment\": {{$body.event.data.old.comment}},\r\n \"employee_refinish\": {{$body.event.data.old.employee_refinish}},\r\n \"inproduction\": {{$body.event.data.old.inproduction}},\r\n \"production_vars\": {{$body.event.data.old.production_vars}},\r\n \"intakechecklist\": {{$body.event.data.old.intakechecklist}},\r\n \"cieca_ttl\": {{$body.event.data.old.cieca_ttl}},\r\n \"date_invoiced\": {{$body.event.data.old.date_invoiced}}\r\n },\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"ro_number\": {{$body.event.data.old.ro_number}},\r\n \"queued_for_parts\": {{$body.event.data.new.queued_for_parts}},\r\n \"employee_prep\": {{$body.event.data.new.employee_prep}},\r\n \"clm_total\": {{$body.event.data.new.clm_total}},\r\n \"towin\": {{$body.event.data.new.towin}},\r\n \"employee_body\": {{$body.event.data.new.employee_body}},\r\n \"converted\": {{$body.event.data.new.converted}},\r\n \"scheduled_in\": {{$body.event.data.new.scheduled_in}},\r\n \"scheduled_completion\": {{$body.event.data.new.scheduled_completion}},\r\n \"scheduled_delivery\": {{$body.event.data.new.scheduled_delivery}},\r\n \"actual_delivery\": {{$body.event.data.new.actual_delivery}},\r\n \"actual_completion\": {{$body.event.data.new.actual_completion}},\r\n \"alt_transport\": {{$body.event.data.new.alt_transport}},\r\n \"date_exported\": {{$body.event.data.new.date_exported}},\r\n \"status\": {{$body.event.data.new.status}},\r\n \"employee_csr\": {{$body.event.data.new.employee_csr}},\r\n \"actual_in\": {{$body.event.data.new.actual_in}},\r\n \"deliverchecklist\": {{$body.event.data.new.deliverchecklist}},\r\n \"comment\": {{$body.event.data.new.comment}},\r\n \"employee_refinish\": {{$body.event.data.new.employee_refinish}},\r\n \"inproduction\": {{$body.event.data.new.inproduction}},\r\n \"production_vars\": {{$body.event.data.new.production_vars}},\r\n \"intakechecklist\": {{$body.event.data.new.intakechecklist}},\r\n \"cieca_ttl\": {{$body.event.data.new.cieca_ttl}},\r\n \"date_invoiced\": {{$body.event.data.new.date_invoiced}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_jobs\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"jobs\"\r\n }\r\n}\r\n" method: POST query_params: {} template_engine: Kriti diff --git a/server/notifications/scenarioBuilders.js b/server/notifications/scenarioBuilders.js index 4adf6eede..3e63510c1 100644 --- a/server/notifications/scenarioBuilders.js +++ b/server/notifications/scenarioBuilders.js @@ -452,7 +452,6 @@ const paymentCollectedCompletedBuilder = (data) => { * Builds notification data for changes to scheduled dates. */ const scheduledDatesChangedBuilder = (data) => { - const momentFormat = "MM/DD/YYYY hh:mm a"; const changedFields = data.changedFields; // Define field configurations @@ -462,16 +461,38 @@ const scheduledDatesChangedBuilder = (data) => { scheduled_delivery: "Scheduled Delivery" }; + // Helper function to format date and time with "at" + const formatDateTime = (date) => { + if (!date) return "unset"; + const formatted = moment(date).tz(data.bodyShopTimezone); + const datePart = formatted.format("MM/DD/YYYY"); + const timePart = formatted.format("hh:mm a"); + return `${datePart} at ${timePart}`; + }; + // Build field messages dynamically const fieldMessages = Object.entries(fieldConfigs) .filter(([field]) => changedFields[field]) // Only include changed fields .map(([field, label]) => { const { old, new: newValue } = changedFields[field]; - const formatDate = (date) => (date ? moment(date).tz(data.bodyShopTimezone).format(momentFormat) : "unset"); - return `${label} changed from ${formatDate(old)} to ${formatDate(newValue)}`; - }); - const body = fieldMessages.length > 0 ? fieldMessages.join(", ") + "." : "Scheduled dates have been updated."; + // Case 1: Scheduled date cancelled (from value to null) + if (old && !newValue) { + return `${label} was cancelled (previously ${formatDateTime(old)}).`; + } + // Case 2: Scheduled date set (from null to value) + else if (!old && newValue) { + return `${label} was set to ${formatDateTime(newValue)}.`; + } + // Case 3: Scheduled date changed (from value to value) + else if (old && newValue) { + return `${label} changed from ${formatDateTime(old)} to ${formatDateTime(newValue)}.`; + } + return ""; // Fallback, though this shouldn't happen with the filter + }) + .filter(Boolean); // Remove any empty strings + + const body = fieldMessages.length > 0 ? fieldMessages.join(" ") : "Scheduled dates have been updated."; const result = { app: { diff --git a/server/notifications/scenarioParser.js b/server/notifications/scenarioParser.js index ac1db6aeb..d5c9069a0 100644 --- a/server/notifications/scenarioParser.js +++ b/server/notifications/scenarioParser.js @@ -37,10 +37,11 @@ const scenarioParser = async (req, jobIdField) => { // Step 1: Validate we know what user committed the action that fired the parser // console.log("Step 1"); + const hasuraUserRole = event?.session_variables?.["x-hasura-role"]; const hasuraUserId = event?.session_variables?.["x-hasura-user-id"]; // Bail if we don't know who started the scenario - if (!hasuraUserId) { + if (hasuraUserRole === "user" && !hasuraUserId) { logger.log("No Hasura user ID found, skipping notification parsing", "info", "notifications"); return; } @@ -84,7 +85,7 @@ const scenarioParser = async (req, jobIdField) => { authId: watcher?.user?.authid })); - if (FILTER_SELF_FROM_WATCHERS) { + if (FILTER_SELF_FROM_WATCHERS && hasuraUserRole === "user") { jobWatchers = jobWatchers.filter((watcher) => watcher.authId !== hasuraUserId); } From d6df5af1a4176c208e1e56caac7163711f04df9b Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 11 Mar 2025 11:57:16 -0400 Subject: [PATCH 3/4] IO-3166-Global-Notifications-Part-2: checkpoint --- server/notifications/scenarioBuilders.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/server/notifications/scenarioBuilders.js b/server/notifications/scenarioBuilders.js index 3e63510c1..4c1083415 100644 --- a/server/notifications/scenarioBuilders.js +++ b/server/notifications/scenarioBuilders.js @@ -258,8 +258,17 @@ const jobStatusChangeBuilder = (data) => { const newMediaAddedReassignedBuilder = (data) => { // Determine if it's an image or document const mediaType = data?.data?.type?.startsWith("image") ? "Image" : "Document"; - // Determine if it's added or updated - const action = data.isNew ? "added" : "updated"; + + // Determine the action + let action; + if (data.isNew) { + action = "added"; // New media + } else if (data.changedFields?.jobid && data.changedFields.jobid.old !== data.changedFields.jobid.new) { + action = "reassigned to Job"; + } else { + action = "updated"; + } + // Construct the body string const body = `An ${mediaType} has been ${action}.`; From 8de7db60e6cfbfaef9dd5b2eaca6d6a9524a41f5 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 11 Mar 2025 13:16:47 -0400 Subject: [PATCH 4/4] IO-3166-Global-Notifications-Part-2: checkpoint --- server/notifications/eventHandlers.js | 60 +++++++++++++++++++++++- server/notifications/scenarioBuilders.js | 9 ++-- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/server/notifications/eventHandlers.js b/server/notifications/eventHandlers.js index 45de5dc3e..315cfe705 100644 --- a/server/notifications/eventHandlers.js +++ b/server/notifications/eventHandlers.js @@ -50,13 +50,69 @@ const handleBillsChange = async (req, res) => /** * Handle documents change notifications. + * Processes both old and new job IDs if the document was moved between jobs. * * @param {Object} req - Express request object. * @param {Object} res - Express response object. * @returns {Promise} JSON response with a success message. */ -const handleDocumentsChange = async (req, res) => - processNotificationEvent(req, res, "req.body.event.new.jobid", "Documents Change Notifications Event Handled."); +const handleDocumentsChange = async (req, res) => { + const { logger } = req; + const newJobId = req.body?.event?.data?.new?.jobid; + const oldJobId = req.body?.event?.data?.old?.jobid; + + // If jobid changed (document moved between jobs), we need to notify both jobs + if (oldJobId && newJobId && oldJobId !== newJobId) { + // Process notification for new job ID + scenarioParser(req, "req.body.event.new.jobid").catch((error) => { + logger.log("notifications-error", "error", "notifications", null, { + message: error?.message, + stack: error?.stack + }); + }); + + // Create a modified request for old job ID + const oldJobReq = { + body: { + ...req.body, + event: { + ...req.body.event, + data: { + new: { + ...req.body.event.data.old, + // Add a flag to indicate this document was moved away + _documentMoved: true, + _movedToJob: newJobId + }, + old: null + } + } + }, + logger, + sessionUtils: req.sessionUtils + }; + + // Process notification for old job ID using the modified request + scenarioParser(oldJobReq, "req.body.event.new.jobid").catch((error) => { + logger.log("notifications-error", "error", "notifications", null, { + message: error?.message, + stack: error?.stack + }); + }); + + return res.status(200).json({ message: "Documents Change Notifications Event Handled for both jobs." }); + } + + // Otherwise just process the new job ID + scenarioParser(req, "req.body.event.new.jobid").catch((error) => { + logger.log("notifications-error", "error", "notifications", null, { + message: error?.message, + stack: error?.stack + }); + }); + + return res.status(200).json({ message: "Documents Change Notifications Event Handled." }); +}; /** * Handle job lines change notifications. diff --git a/server/notifications/scenarioBuilders.js b/server/notifications/scenarioBuilders.js index 4c1083415..276cff882 100644 --- a/server/notifications/scenarioBuilders.js +++ b/server/notifications/scenarioBuilders.js @@ -261,10 +261,12 @@ const newMediaAddedReassignedBuilder = (data) => { // Determine the action let action; - if (data.isNew) { + if (data?.data?._documentMoved) { + action = "moved to another Job"; // Special case for document moved from this job + } else if (data.isNew) { action = "added"; // New media } else if (data.changedFields?.jobid && data.changedFields.jobid.old !== data.changedFields.jobid.new) { - action = "reassigned to Job"; + action = "moved to this Job"; } else { action = "updated"; } @@ -281,7 +283,8 @@ const newMediaAddedReassignedBuilder = (data) => { body, variables: { mediaType, - action + action, + movedToJob: data?.data?._movedToJob }, recipients: [] },