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,