feature/IO-3096-GlobalNotifications - Check-point

This commit is contained in:
Dave Richer
2025-02-10 17:15:53 -05:00
parent b1ffbe0e12
commit 54820fe3c8
6 changed files with 72 additions and 11 deletions

View File

@@ -1,6 +1,6 @@
import { useMutation, useQuery } from "@apollo/client"; import { useMutation, useQuery } from "@apollo/client";
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Button, Card, Col, Form, Row, Switch } from "antd"; import { Button, Card, Col, Form, Row } from "antd";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -8,9 +8,34 @@ import { createStructuredSelector } from "reselect";
import { selectCurrentUser } from "../../redux/user/user.selectors"; import { selectCurrentUser } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import { QUERY_NOTIFICATION_SETTINGS, UPDATE_NOTIFICATION_SETTINGS } from "../../graphql/user.queries.js"; 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"; 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 (
<Button.Group>
{notificationChannels.map((method) => (
<Button
disabled={method === "fcm"}
key={method}
type={value[method] ? "primary" : "default"}
onClick={() => toggleMethod(method)}
>
{t(`notifications.channels.${method}`)}
</Button>
))}
</Button.Group>
);
};
function NotificationSettingsForm({ currentUser }) { function NotificationSettingsForm({ currentUser }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [form] = Form.useForm(); const [form] = Form.useForm();
@@ -31,8 +56,10 @@ function NotificationSettingsForm({ currentUser }) {
useEffect(() => { useEffect(() => {
if (data?.associations?.length > 0) { if (data?.associations?.length > 0) {
const settings = data.associations[0].notification_settings || {}; 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) => { const formattedValues = notificationScenarios.reduce((acc, scenario) => {
acc[scenario] = settings[scenario] ?? false; acc[scenario] = settings[scenario] ?? { app: false, email: false, fcm: false };
return acc; return acc;
}, {}); }, {});
@@ -45,6 +72,7 @@ function NotificationSettingsForm({ currentUser }) {
const handleSave = async (values) => { const handleSave = async (values) => {
if (data?.associations?.length > 0) { if (data?.associations?.length > 0) {
const userId = data.associations[0].id; 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 } }); await updateNotificationSettings({ variables: { id: userId, ns: values } });
setInitialValues(values); setInitialValues(values);
setIsDirty(false); setIsDirty(false);
@@ -88,8 +116,8 @@ function NotificationSettingsForm({ currentUser }) {
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
{notificationScenarios.map((scenario) => ( {notificationScenarios.map((scenario) => (
<Col xs={24} sm={12} md={8} key={scenario}> <Col xs={24} sm={12} md={8} key={scenario}>
<Form.Item name={scenario} label={t(`notifications.scenarios.${scenario}`)} valuePropName="checked"> <Form.Item name={scenario} label={t(`notifications.scenarios.${scenario}`)}>
<Switch /> <NotificationMethodButtonGroup />
</Form.Item> </Form.Item>
</Col> </Col>
))} ))}

View File

@@ -3761,7 +3761,7 @@
}, },
"notifications": { "notifications": {
"labels": { "labels": {
"notificationscenarios": "Notification Scenarios", "notificationscenarios": "Job Notification Scenarios",
"save": "Save Scenarios", "save": "Save Scenarios",
"watching-issue": "Watching", "watching-issue": "Watching",
"add-watchers": "Add Watchers", "add-watchers": "Add Watchers",
@@ -3795,6 +3795,11 @@
"job-status-change": "Job Status Changed", "job-status-change": "Job Status Changed",
"payment-collected-completed": "Payment Collected / Completed", "payment-collected-completed": "Payment Collected / Completed",
"alternate-transport-changed": "Alternate Transport Changed" "alternate-transport-changed": "Alternate Transport Changed"
},
"channels": {
"app": "App",
"email": "Email",
"fcm": "Push"
} }
} }
} }

View File

@@ -3795,6 +3795,11 @@
"job-status-change": "", "job-status-change": "",
"payment-collected-completed": "", "payment-collected-completed": "",
"alternate-transport-changed": "" "alternate-transport-changed": ""
},
"channels": {
"app": "",
"email": "",
"fcm": ""
} }
} }
} }

View File

@@ -3795,6 +3795,11 @@
"job-status-change": "", "job-status-change": "",
"payment-collected-completed": "", "payment-collected-completed": "",
"alternate-transport-changed": "" "alternate-transport-changed": ""
},
"channels": {
"app": "",
"email": "",
"fcm": ""
} }
} }
} }

View File

@@ -16,4 +16,6 @@ const notificationScenarios = [
"alternate-transport-changed" "alternate-transport-changed"
]; ];
export default notificationScenarios; const notificationChannels = ["app", "email", "fcm"];
export { notificationScenarios, notificationChannels };

View File

@@ -62,11 +62,27 @@ const scenarioParser = async (req) => {
if (isEmpty(associationsData?.associations)) return; if (isEmpty(associationsData?.associations)) return;
// Step 6: For each matching scenario, add a scenarioWatchers property // 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) => { finalScenarioData.matchingScenarios.forEach((scenario) => {
scenario.scenarioWatchers = associationsData.associations scenario.scenarioWatchers = associationsData.associations
.filter((assoc) => assoc.notification_settings && assoc.notification_settings[scenario.key] === true) .filter((assoc) => {
.map((assoc) => assoc.useremail); // 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) // Step 7: Call builder functions for each matching scenario (fire-and-forget)