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; }