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 91aa22746..5264c886a 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
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
const { Option } = Select;
//To be used as a form element only.
-const EmployeeSearchSelect = ({ options, ...props }) => {
+const EmployeeSearchSelect = ({ options, showEmail, ...props }) => {
const { t } = useTranslation();
return (
@@ -21,12 +21,16 @@ const EmployeeSearchSelect = ({ options, ...props }) => {
{options
? options.map((o) => (
))
diff --git a/client/src/components/job-watcher-toggle/job-watcher-toggle.component.jsx b/client/src/components/job-watcher-toggle/job-watcher-toggle.component.jsx
index 879b8c1f7..8e21fbc44 100644
--- a/client/src/components/job-watcher-toggle/job-watcher-toggle.component.jsx
+++ b/client/src/components/job-watcher-toggle/job-watcher-toggle.component.jsx
@@ -104,6 +104,7 @@ export default function JobWatcherToggleComponent({
}
placeholder={t("notifications.labels.employee-search")}
value={selectedWatcher}
+ showEmail={true}
onChange={(value) => {
setSelectedWatcher(value);
handleWatcherSelect(value);
diff --git a/client/src/components/shop-info/shop-info.notifications-autoadd.component.jsx b/client/src/components/shop-info/shop-info.notifications-autoadd.component.jsx
index 33fa8f121..e34729452 100644
--- a/client/src/components/shop-info/shop-info.notifications-autoadd.component.jsx
+++ b/client/src/components/shop-info/shop-info.notifications-autoadd.component.jsx
@@ -1,27 +1,52 @@
-// shop-info.notifications-autoadd.component.jsx
-import React from "react";
import { Form, Typography } from "antd";
import { useTranslation } from "react-i18next";
import EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component.jsx";
const { Text, Paragraph } = Typography;
-export default function ShopInfoNotificationsAutoadd({ form, bodyshop }) {
+export default function ShopInfoNotificationsAutoadd({ bodyshop }) {
const { t } = useTranslation();
- const employeeOptions = bodyshop?.employees?.filter((e) => e.active && e.id) || [];
+ // Filter employee options to ensure active employees with valid IDs
+ const employeeOptions = bodyshop?.employees?.filter((e) => e.active && e.id && typeof e.id === "string") || [];
return (
{t("bodyshop.fields.notifications.description")}
{t("bodyshop.labels.notifications.followers")}
{employeeOptions.length > 0 ? (
-
+ {
+ if (!value || value.length === 0) {
+ return Promise.resolve(); // Allow empty array
+ }
+ const hasInvalid = value.some((id) => id == null || typeof id !== "string" || id.trim() === "");
+ if (hasInvalid) {
+ return Promise.reject(new Error(t("bodyshop.fields.notifications.invalid_followers")));
+ }
+ return Promise.resolve();
+ }
+ }
+ ]}
+ >
{
+ // Filter out null or invalid values before passing to Form
+ const cleanedValue = value?.filter((id) => id != null && typeof id === "string" && id.trim() !== "");
+ return cleanedValue;
+ }}
/>
) : (
diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json
index d17edbf4c..546e48b6d 100644
--- a/client/src/translations/en_us/common.json
+++ b/client/src/translations/en_us/common.json
@@ -651,7 +651,8 @@
"zip_post": "Zip/Postal Code",
"notifications": {
"description": "Select employees to automatically follow new jobs and receive notifications for job updates.",
- "placeholder": "Search for employees"
+ "placeholder": "Search for employees",
+ "invalid_followers": "Invalid followers. Please select valid employees."
}
},
"labels": {
diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json
index b2e85a563..4889ec3ec 100644
--- a/client/src/translations/es/common.json
+++ b/client/src/translations/es/common.json
@@ -651,7 +651,8 @@
"zip_post": "",
"notifications": {
"description": "",
- "placeholder": ""
+ "placeholder": "",
+ "invalid_followers": ""
}
},
"labels": {
diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json
index 9269d21c7..11382844d 100644
--- a/client/src/translations/fr/common.json
+++ b/client/src/translations/fr/common.json
@@ -651,7 +651,8 @@
"zip_post": "",
"notifications": {
"description": "",
- "placeholder": ""
+ "placeholder": "",
+ "invalid_followers": ""
}
},
"labels": {
diff --git a/hasura/migrations/1746551798223_alter_table_public_bodyshops_alter_column_notification_followers/down.sql b/hasura/migrations/1746551798223_alter_table_public_bodyshops_alter_column_notification_followers/down.sql
new file mode 100644
index 000000000..4b478644b
--- /dev/null
+++ b/hasura/migrations/1746551798223_alter_table_public_bodyshops_alter_column_notification_followers/down.sql
@@ -0,0 +1 @@
+alter table "public"."bodyshops" alter column "notification_followers" set default json_build_object();
diff --git a/hasura/migrations/1746551798223_alter_table_public_bodyshops_alter_column_notification_followers/up.sql b/hasura/migrations/1746551798223_alter_table_public_bodyshops_alter_column_notification_followers/up.sql
new file mode 100644
index 000000000..2f8457ba3
--- /dev/null
+++ b/hasura/migrations/1746551798223_alter_table_public_bodyshops_alter_column_notification_followers/up.sql
@@ -0,0 +1 @@
+alter table "public"."bodyshops" alter column "notification_followers" set default json_build_array();
diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js
index 8e760a5c3..56909d9b0 100644
--- a/server/graphql-client/queries.js
+++ b/server/graphql-client/queries.js
@@ -2765,6 +2765,17 @@ query GET_JOB_WATCHERS($jobid: uuid!) {
}
`;
+exports.GET_JOB_WATCHERS_MINIMAL = `
+ query GET_JOB_WATCHERS_MINIMAL($jobid: uuid!) {
+ job_watchers(where: { jobid: { _eq: $jobid } }) {
+ user_email
+ user {
+ authid
+ }
+ }
+ }
+`;
+
exports.GET_NOTIFICATION_ASSOCIATIONS = `
query GET_NOTIFICATION_ASSOCIATIONS($emails: [String!]!, $shopid: uuid!) {
associations(where: {
@@ -2802,6 +2813,7 @@ exports.GET_BODYSHOP_BY_ID = `
imexshopid
intellipay_config
state
+ notification_followers
}
}
`;
@@ -2931,10 +2943,6 @@ exports.INSERT_NEW_DOCUMENT = `
exports.GET_AUTOADD_NOTIFICATION_USERS = `
query GET_AUTOADD_NOTIFICATION_USERS($shopId: uuid!) {
- bodyshops_by_pk(id: $shopId) {
- id
- notification_followers
- }
associations(where: {
_and: [
{ shopid: { _eq: $shopId } },
diff --git a/server/notifications/autoAddWatchers.js b/server/notifications/autoAddWatchers.js
index c29b0388d..e302610ea 100644
--- a/server/notifications/autoAddWatchers.js
+++ b/server/notifications/autoAddWatchers.js
@@ -7,8 +7,13 @@
*/
const { client: gqlClient } = require("../graphql-client/graphql-client");
-const queries = require("../graphql-client/queries");
const { isEmpty } = require("lodash");
+const {
+ GET_JOB_WATCHERS_MINIMAL,
+ GET_AUTOADD_NOTIFICATION_USERS,
+ GET_EMPLOYEE_EMAILS,
+ INSERT_JOB_WATCHERS
+} = require("../graphql-client/queries");
// If true, the user who commits the action will NOT receive notifications; if false, they will.
const FILTER_SELF_FROM_WATCHERS = process.env?.FILTER_SELF_FROM_WATCHERS !== "false";
@@ -22,11 +27,13 @@ const FILTER_SELF_FROM_WATCHERS = process.env?.FILTER_SELF_FROM_WATCHERS !== "fa
*/
const autoAddWatchers = async (req) => {
const { event, trigger } = req.body;
- const { logger } = req;
+ const {
+ logger,
+ sessionUtils: { getBodyshopFromRedis }
+ } = req;
- // Validate that this is an INSERT event
+ // Validate that this is an INSERT event, bail
if (trigger?.name !== "notifications_jobs_autoadd" || event.op !== "INSERT" || event.data.old) {
- logger.log("Invalid event for auto-add watchers, skipping", "info", "notifications");
return;
}
@@ -42,8 +49,12 @@ const autoAddWatchers = async (req) => {
const hasuraUserId = event?.session_variables?.["x-hasura-user-id"];
try {
- // Fetch auto-add users and notification followers
- const autoAddData = await gqlClient.request(queries.GET_AUTOADD_NOTIFICATION_USERS, { shopId });
+ // Fetch bodyshop data from Redis
+ const bodyshopData = await getBodyshopFromRedis(shopId);
+ const notificationFollowers = bodyshopData?.notification_followers || [];
+
+ // Fetch auto-add users from associations
+ const autoAddData = await gqlClient.request(GET_AUTOADD_NOTIFICATION_USERS, { shopId });
// Get users with notifications_autoadd: true
const autoAddUsers =
@@ -53,12 +64,11 @@ const autoAddWatchers = async (req) => {
})) || [];
// Get users from notification_followers (array of employee IDs)
- const notificationFollowers = autoAddData?.bodyshops_by_pk?.notification_followers || [];
let followerEmails = [];
if (notificationFollowers.length > 0) {
const validFollowers = notificationFollowers.filter((id) => id); // Remove null values
if (validFollowers.length > 0) {
- const employeeData = await gqlClient.request(queries.GET_EMPLOYEE_EMAILS, {
+ const employeeData = await gqlClient.request(GET_EMPLOYEE_EMAILS, {
employeeIds: validFollowers,
shopid: shopId
});
@@ -80,12 +90,11 @@ const autoAddWatchers = async (req) => {
}, []);
if (isEmpty(usersToAdd)) {
- logger.log(`No users to auto-add for jobId "${jobId}" (RO: ${roNumber})`, "info", "notifications");
return;
}
// Check existing watchers to avoid duplicates
- const existingWatchersData = await gqlClient.request(queries.GET_JOB_WATCHERS, { jobid: jobId });
+ const existingWatchersData = await gqlClient.request(GET_JOB_WATCHERS_MINIMAL, { jobid: jobId });
const existingWatcherEmails = existingWatchersData?.job_watchers?.map((w) => w.user_email) || [];
// Filter out already existing watchers and optionally the user who created the job
@@ -104,23 +113,11 @@ const autoAddWatchers = async (req) => {
}));
if (isEmpty(newWatchers)) {
- logger.log(
- `No new watchers to add after filtering for jobId "${jobId}" (RO: ${roNumber})`,
- "info",
- "notifications"
- );
return;
}
// Insert new watchers
- await gqlClient.request(queries.INSERT_JOB_WATCHERS, { watchers: newWatchers });
- logger.log(
- `Added ${newWatchers.length} auto-add watchers for jobId "${jobId}" (RO: ${roNumber})`,
- "info",
- "notifications",
- null,
- { addedEmails: newWatchers.map((w) => w.user_email) }
- );
+ await gqlClient.request(INSERT_JOB_WATCHERS, { watchers: newWatchers });
} catch (error) {
logger.log("Error adding auto-add watchers", "error", "notifications", null, {
message: error?.message,