From 54820fe3c8211fc11eb052d7ad8adc5a5ec3300a Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 10 Feb 2025 17:15:53 -0500 Subject: [PATCH] feature/IO-3096-GlobalNotifications - Check-point --- .../notification-settings.component.jsx | 40 ++++++++++++++++--- client/src/translations/en_us/common.json | 7 +++- client/src/translations/es/common.json | 5 +++ client/src/translations/fr/common.json | 5 +++ client/src/utils/jobNotificationScenarios.js | 4 +- server/notifications/utils/scenarioParser.js | 22 ++++++++-- 6 files changed, 72 insertions(+), 11 deletions(-) diff --git a/client/src/components/profile-my/notification-settings.component.jsx b/client/src/components/profile-my/notification-settings.component.jsx index 328c85cf1..f1a2a1e73 100644 --- a/client/src/components/profile-my/notification-settings.component.jsx +++ b/client/src/components/profile-my/notification-settings.component.jsx @@ -1,6 +1,6 @@ import { useMutation, useQuery } from "@apollo/client"; -import React, { useEffect, useState } from "react"; -import { Button, Card, Col, Form, Row, Switch } from "antd"; +import { useEffect, useState } from "react"; +import { Button, Card, Col, Form, Row } from "antd"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; @@ -8,9 +8,34 @@ import { createStructuredSelector } from "reselect"; import { selectCurrentUser } from "../../redux/user/user.selectors"; import AlertComponent from "../alert/alert.component"; import { QUERY_NOTIFICATION_SETTINGS, UPDATE_NOTIFICATION_SETTINGS } from "../../graphql/user.queries.js"; -import notificationScenarios from "../../utils/jobNotificationScenarios.js"; +import { notificationScenarios, notificationChannels } from "../../utils/jobNotificationScenarios.js"; import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx"; +const NotificationMethodButtonGroup = ({ value = {}, onChange }) => { + const { t } = useTranslation(); + const toggleMethod = (method) => { + const newValue = { ...value, [method]: !value[method] }; + if (onChange) { + onChange(newValue); + } + }; + + return ( + + {notificationChannels.map((method) => ( + + ))} + + ); +}; + function NotificationSettingsForm({ currentUser }) { const { t } = useTranslation(); const [form] = Form.useForm(); @@ -31,8 +56,10 @@ function NotificationSettingsForm({ currentUser }) { useEffect(() => { if (data?.associations?.length > 0) { const settings = data.associations[0].notification_settings || {}; + // For each scenario, expect an object with keys { app, email, fcm }. + // If not present in the fetched data, default to all false. const formattedValues = notificationScenarios.reduce((acc, scenario) => { - acc[scenario] = settings[scenario] ?? false; + acc[scenario] = settings[scenario] ?? { app: false, email: false, fcm: false }; return acc; }, {}); @@ -45,6 +72,7 @@ function NotificationSettingsForm({ currentUser }) { const handleSave = async (values) => { if (data?.associations?.length > 0) { const userId = data.associations[0].id; + // `values` now contains, for each scenario, an object with keys { app, email, fcm } await updateNotificationSettings({ variables: { id: userId, ns: values } }); setInitialValues(values); setIsDirty(false); @@ -88,8 +116,8 @@ function NotificationSettingsForm({ currentUser }) { {notificationScenarios.map((scenario) => ( - - + + ))} diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index afb2f9b4f..f4a3a9f2b 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -3761,7 +3761,7 @@ }, "notifications": { "labels": { - "notificationscenarios": "Notification Scenarios", + "notificationscenarios": "Job Notification Scenarios", "save": "Save Scenarios", "watching-issue": "Watching", "add-watchers": "Add Watchers", @@ -3795,6 +3795,11 @@ "job-status-change": "Job Status Changed", "payment-collected-completed": "Payment Collected / Completed", "alternate-transport-changed": "Alternate Transport Changed" + }, + "channels": { + "app": "App", + "email": "Email", + "fcm": "Push" } } } diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index f072864c9..18fde0bbc 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -3795,6 +3795,11 @@ "job-status-change": "", "payment-collected-completed": "", "alternate-transport-changed": "" + }, + "channels": { + "app": "", + "email": "", + "fcm": "" } } } diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 1fb719619..5c0b4396a 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -3795,6 +3795,11 @@ "job-status-change": "", "payment-collected-completed": "", "alternate-transport-changed": "" + }, + "channels": { + "app": "", + "email": "", + "fcm": "" } } } diff --git a/client/src/utils/jobNotificationScenarios.js b/client/src/utils/jobNotificationScenarios.js index 0ccf6d4df..1f5fddfab 100644 --- a/client/src/utils/jobNotificationScenarios.js +++ b/client/src/utils/jobNotificationScenarios.js @@ -16,4 +16,6 @@ const notificationScenarios = [ "alternate-transport-changed" ]; -export default notificationScenarios; +const notificationChannels = ["app", "email", "fcm"]; + +export { notificationScenarios, notificationChannels }; diff --git a/server/notifications/utils/scenarioParser.js b/server/notifications/utils/scenarioParser.js index 11fdaef30..9ad0c47f3 100644 --- a/server/notifications/utils/scenarioParser.js +++ b/server/notifications/utils/scenarioParser.js @@ -62,11 +62,27 @@ const scenarioParser = async (req) => { if (isEmpty(associationsData?.associations)) return; // Step 6: For each matching scenario, add a scenarioWatchers property - // that includes only the jobWatchers with the notification setting enabled + // that includes only the jobWatchers with at least one notification method enabled. + // Each watcher object is formatted as: { user, email, app, fcm } finalScenarioData.matchingScenarios.forEach((scenario) => { scenario.scenarioWatchers = associationsData.associations - .filter((assoc) => assoc.notification_settings && assoc.notification_settings[scenario.key] === true) - .map((assoc) => assoc.useremail); + .filter((assoc) => { + // Retrieve the settings object for this scenario (it now contains app, email, and fcm) + const settings = assoc.notification_settings && assoc.notification_settings[scenario.key]; + // Only include this association if at least one notification channel is enabled + return settings && (settings.app || settings.email || settings.fcm); + }) + .map((assoc) => { + const settings = assoc.notification_settings[scenario.key]; + return { + // Use assoc.user if available, otherwise fallback to assoc.useremail as the identifier + user: assoc.user || assoc.useremail, + // The email field here is the user's email notification setting (boolean) + email: settings.email, + app: settings.app, + fcm: settings.fcm + }; + }); }); // Step 7: Call builder functions for each matching scenario (fire-and-forget)