diff --git a/client/src/components/employee-search-select/employee-search-select.component.jsx b/client/src/components/employee-search-select/employee-search-select.component.jsx
index 150e4b426..91aa22746 100644
--- a/client/src/components/employee-search-select/employee-search-select.component.jsx
+++ b/client/src/components/employee-search-select/employee-search-select.component.jsx
@@ -22,11 +22,11 @@ const EmployeeSearchSelect = ({ options, ...props }) => {
? options.map((o) => (
))
diff --git a/client/src/components/notification-settings/notification-settings-form.component.jsx b/client/src/components/notification-settings/notification-settings-form.component.jsx
index f3a307071..c80cde0e6 100644
--- a/client/src/components/notification-settings/notification-settings-form.component.jsx
+++ b/client/src/components/notification-settings/notification-settings-form.component.jsx
@@ -1,6 +1,6 @@
import { useMutation, useQuery } from "@apollo/client";
import { useEffect, useState } from "react";
-import { Button, Card, Checkbox, Form, Space, Switch, Table } from "antd";
+import { Button, Card, Checkbox, Divider, Form, Space, Switch, Table, Typography } from "antd";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -199,6 +199,8 @@ const NotificationSettingsForm = ({ currentUser }) => {
}
>
+
+ {t("notifications.labels.auto-add-description")}
);
diff --git a/client/src/components/shop-info/shop-info.component.jsx b/client/src/components/shop-info/shop-info.component.jsx
index 4ab800915..c0e743528 100644
--- a/client/src/components/shop-info/shop-info.component.jsx
+++ b/client/src/components/shop-info/shop-info.component.jsx
@@ -1,4 +1,3 @@
-
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Button, Card, Tabs } from "antd";
import React from "react";
@@ -24,6 +23,8 @@ import ShopInfoRoGuard from "./shop-info.roguard.component";
import ShopInfoIntellipay from "./shop-intellipay-config.component";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
+import { useSocket } from "../../contexts/SocketIO/useSocket.js";
+import ShopInfoNotificationsAutoadd from "./shop-info.notifications-autoadd.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -41,6 +42,7 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
names: ["CriticalPartsScanning", "Enhanced_Payroll"],
splitKey: bodyshop.imexshopid
});
+ const { scenarioNotificationsOn } = useSocket();
const { t } = useTranslation();
const history = useNavigate();
@@ -137,9 +139,21 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
{
key: "intellipay",
- label: InstanceRenderManager({ rome: t("bodyshop.labels.romepay"), imex: t("bodyshop.labels.imexpay") }),
+ label: InstanceRenderManager({
+ rome: t("bodyshop.labels.romepay"),
+ imex: t("bodyshop.labels.imexpay")
+ }),
children:
- }
+ },
+ ...(scenarioNotificationsOn
+ ? [
+ {
+ key: "notifications_autoadd",
+ label: t("bodyshop.labels.notifications.followers"),
+ children:
+ }
+ ]
+ : [])
];
return (
e.active && e.id) || [];
+
+ return (
+
+
{t("bodyshop.fields.notifications.description")}
+
{t("bodyshop.labels.notifications.followers")}
+ {employeeOptions.length > 0 ? (
+
+
+
+ ) : (
+
{t("bodyshop.fields.no_employees_available")}
+ )}
+
+ );
+}
diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json
index bb855a72f..d17edbf4c 100644
--- a/client/src/translations/en_us/common.json
+++ b/client/src/translations/en_us/common.json
@@ -648,7 +648,11 @@
"use_paint_scale_data": "Use Paint Scale Data for Job Costing?",
"uselocalmediaserver": "Use Local Media Server?",
"website": "Website",
- "zip_post": "Zip/Postal Code"
+ "zip_post": "Zip/Postal Code",
+ "notifications": {
+ "description": "Select employees to automatically follow new jobs and receive notifications for job updates.",
+ "placeholder": "Search for employees"
+ }
},
"labels": {
"2tiername": "Name => RO",
@@ -728,7 +732,10 @@
"ssbuckets": "Job Size Definitions",
"systemsettings": "System Settings",
"task-presets": "Task Presets",
- "workingdays": "Working Days"
+ "workingdays": "Working Days",
+ "notifications": {
+ "followers": "Notification Followers"
+ }
},
"operations": {
"contains": "Contains",
@@ -2441,6 +2448,11 @@
"fcm": "Push"
},
"labels": {
+ "auto-add-on": "Unfollow",
+ "auto-add-off": "Follow",
+ "auto-add-success": "Follow status successfully changed.",
+ "auto-add-failure": "Something went wrong updating your Follow status.",
+ "auto-add-description": "When enabled, the Follow setting automatically adds you as a watcher to new jobs, ensuring you receive notifications for job updates.",
"add-watchers": "Add Watchers",
"add-watchers-team": "Add Team Members",
"employee-search": "Search for an Employee",
diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json
index 1701d8732..b2e85a563 100644
--- a/client/src/translations/es/common.json
+++ b/client/src/translations/es/common.json
@@ -648,7 +648,11 @@
"use_paint_scale_data": "",
"uselocalmediaserver": "",
"website": "",
- "zip_post": ""
+ "zip_post": "",
+ "notifications": {
+ "description": "",
+ "placeholder": ""
+ }
},
"labels": {
"2tiername": "",
@@ -728,7 +732,10 @@
"ssbuckets": "",
"systemsettings": "",
"task-presets": "",
- "workingdays": ""
+ "workingdays": "",
+ "notifications": {
+ "followers": ""
+ }
},
"operations": {
"contains": "",
@@ -2441,6 +2448,11 @@
"fcm": ""
},
"labels": {
+ "auto-add-on": "",
+ "auto-add-off": "",
+ "auto-add-success": "",
+ "auto-add-failure": "",
+ "auto-add-description": "",
"add-watchers": "",
"add-watchers-team": "",
"employee-search": "",
diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json
index 4b63295f4..9269d21c7 100644
--- a/client/src/translations/fr/common.json
+++ b/client/src/translations/fr/common.json
@@ -648,7 +648,11 @@
"use_paint_scale_data": "",
"uselocalmediaserver": "",
"website": "",
- "zip_post": ""
+ "zip_post": "",
+ "notifications": {
+ "description": "",
+ "placeholder": ""
+ }
},
"labels": {
"2tiername": "",
@@ -728,7 +732,10 @@
"ssbuckets": "",
"systemsettings": "",
"task-presets": "",
- "workingdays": ""
+ "workingdays": "",
+ "notifications": {
+ "followers": ""
+ }
},
"operations": {
"contains": "",
@@ -2441,6 +2448,11 @@
"fcm": ""
},
"labels": {
+ "auto-add-on": "",
+ "auto-add-off": "",
+ "auto-add-success": "",
+ "auto-add-failure": "",
+ "auto-add-description": "",
"add-watchers": "",
"add-watchers-team": "",
"employee-search": "",
diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js
index 027467b10..8e760a5c3 100644
--- a/server/graphql-client/queries.js
+++ b/server/graphql-client/queries.js
@@ -2958,3 +2958,30 @@ exports.INSERT_JOB_WATCHERS = `
}
}
`;
+
+exports.GET_NOTIFICATION_ASSOCIATIONS_BY_IDS = `
+ query GET_NOTIFICATION_ASSOCIATIONS_BY_IDS($associationIds: [uuid!]!, $shopid: uuid!) {
+ associations(where: { id: { _in: $associationIds }, shopid: { _eq: $shopid }, active: { _eq: true } }) {
+ id
+ useremail
+ }
+ }
+`;
+
+exports.GET_EMPLOYEE_EMAILS = `
+ query GET_EMPLOYEE_EMAILS($employeeIds: [uuid!]!, $shopid: uuid!) {
+ employees(where: { id: { _in: $employeeIds }, shopid: { _eq: $shopid }, active: { _eq: true } }) {
+ id
+ user_email
+ }
+ }
+`;
+
+exports.GET_NOTIFICATION_ASSOCIATIONS_BY_EMAILS = `
+ query GET_NOTIFICATION_ASSOCIATIONS_BY_EMAILS($emails: [String!]!, $shopid: uuid!) {
+ associations(where: { useremail: { _in: $emails }, shopid: { _eq: $shopid }, active: { _eq: true } }) {
+ id
+ useremail
+ }
+ }
+`;
diff --git a/server/notifications/autoAddWatchers.js b/server/notifications/autoAddWatchers.js
index aebf42f49..c29b0388d 100644
--- a/server/notifications/autoAddWatchers.js
+++ b/server/notifications/autoAddWatchers.js
@@ -52,21 +52,23 @@ const autoAddWatchers = async (req) => {
associationId: assoc.id
})) || [];
- // Get users from notification_followers (array of association IDs)
+ // Get users from notification_followers (array of employee IDs)
const notificationFollowers = autoAddData?.bodyshops_by_pk?.notification_followers || [];
let followerEmails = [];
if (notificationFollowers.length > 0) {
- // Fetch associations for notification_followers
- const followerAssociations = await gqlClient.request(queries.GET_NOTIFICATION_ASSOCIATIONS, {
- emails: [], // Filter by association IDs
- shopid: shopId
- });
- followerEmails = followerAssociations.associations
- .filter((assoc) => notificationFollowers.includes(assoc.id))
- .map((assoc) => ({
- email: assoc.useremail,
- associationId: assoc.id
- }));
+ const validFollowers = notificationFollowers.filter((id) => id); // Remove null values
+ if (validFollowers.length > 0) {
+ const employeeData = await gqlClient.request(queries.GET_EMPLOYEE_EMAILS, {
+ employeeIds: validFollowers,
+ shopid: shopId
+ });
+ followerEmails = employeeData.employees
+ .filter((e) => e.user_email)
+ .map((e) => ({
+ email: e.user_email,
+ associationId: null
+ }));
+ }
}
// Combine and deduplicate emails (use email as the unique key)
@@ -91,7 +93,6 @@ const autoAddWatchers = async (req) => {
.filter((user) => !existingWatcherEmails.includes(user.email))
.filter((user) => {
if (FILTER_SELF_FROM_WATCHERS && hasuraUserRole === "user") {
- // Fetch user email for hasuraUserId to compare
const userData = existingWatchersData?.job_watchers?.find((w) => w.user?.authid === hasuraUserId);
return userData ? user.email !== userData.user_email : true;
}