feature/IO-3225-Notifications-1.5: checkpoint
This commit is contained in:
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
//To be used as a form element only.
|
//To be used as a form element only.
|
||||||
|
|
||||||
const EmployeeSearchSelect = ({ options, ...props }) => {
|
const EmployeeSearchSelect = ({ options, showEmail, ...props }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -21,12 +21,16 @@ const EmployeeSearchSelect = ({ options, ...props }) => {
|
|||||||
{options
|
{options
|
||||||
? options.map((o) => (
|
? options.map((o) => (
|
||||||
<Option key={o.id} value={o.id} search={`${o.employee_number} ${o.first_name} ${o.last_name}`}>
|
<Option key={o.id} value={o.id} search={`${o.employee_number} ${o.first_name} ${o.last_name}`}>
|
||||||
<Space>
|
<Space size="small">
|
||||||
{`${o.employee_number ?? ""} ${o.first_name} ${o.last_name}`}
|
{`${o.employee_number ?? ""} ${o.first_name} ${o.last_name}`}
|
||||||
<Tag color="green">
|
<Tag color="green" style={{ padding: "0.1 0.1rem", marginRight: "1px", marginLeft: "1px" }}>
|
||||||
{o.flat_rate ? t("timetickets.labels.flat_rate") : t("timetickets.labels.straight_time")}
|
{o.flat_rate ? t("timetickets.labels.flat_rate") : t("timetickets.labels.straight_time")}
|
||||||
</Tag>
|
</Tag>
|
||||||
{o.user_email ? <Tag color="blue">{o.user_email}</Tag> : null}
|
{showEmail && o.user_email ? (
|
||||||
|
<Tag color="blue" style={{ padding: "0.1 0.1rem", marginRight: "1px", marginLeft: "1px" }}>
|
||||||
|
{o.user_email}
|
||||||
|
</Tag>
|
||||||
|
) : null}
|
||||||
</Space>
|
</Space>
|
||||||
</Option>
|
</Option>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ export default function JobWatcherToggleComponent({
|
|||||||
}
|
}
|
||||||
placeholder={t("notifications.labels.employee-search")}
|
placeholder={t("notifications.labels.employee-search")}
|
||||||
value={selectedWatcher}
|
value={selectedWatcher}
|
||||||
|
showEmail={true}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setSelectedWatcher(value);
|
setSelectedWatcher(value);
|
||||||
handleWatcherSelect(value);
|
handleWatcherSelect(value);
|
||||||
|
|||||||
@@ -1,27 +1,52 @@
|
|||||||
// shop-info.notifications-autoadd.component.jsx
|
|
||||||
import React from "react";
|
|
||||||
import { Form, Typography } from "antd";
|
import { Form, Typography } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component.jsx";
|
import EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component.jsx";
|
||||||
|
|
||||||
const { Text, Paragraph } = Typography;
|
const { Text, Paragraph } = Typography;
|
||||||
|
|
||||||
export default function ShopInfoNotificationsAutoadd({ form, bodyshop }) {
|
export default function ShopInfoNotificationsAutoadd({ bodyshop }) {
|
||||||
const { t } = useTranslation();
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Paragraph>{t("bodyshop.fields.notifications.description")}</Paragraph>
|
<Paragraph>{t("bodyshop.fields.notifications.description")}</Paragraph>
|
||||||
<Text type="secondary">{t("bodyshop.labels.notifications.followers")}</Text>
|
<Text type="secondary">{t("bodyshop.labels.notifications.followers")}</Text>
|
||||||
{employeeOptions.length > 0 ? (
|
{employeeOptions.length > 0 ? (
|
||||||
<Form.Item name="notification_followers" rules={[{ type: "array", message: t("general.validation.array") }]}>
|
<Form.Item
|
||||||
|
name="notification_followers"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
type: "array",
|
||||||
|
message: t("general.validation.array")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validator: async (_, value) => {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
<EmployeeSearchSelectComponent
|
<EmployeeSearchSelectComponent
|
||||||
style={{ minWidth: "100%" }}
|
style={{ minWidth: "100%" }}
|
||||||
mode="multiple"
|
mode="multiple"
|
||||||
options={employeeOptions}
|
options={employeeOptions}
|
||||||
placeholder={t("bodyshop.fields.notifications.placeholder")}
|
placeholder={t("bodyshop.fields.notifications.placeholder")}
|
||||||
|
showEmail={true}
|
||||||
|
onChange={(value) => {
|
||||||
|
// Filter out null or invalid values before passing to Form
|
||||||
|
const cleanedValue = value?.filter((id) => id != null && typeof id === "string" && id.trim() !== "");
|
||||||
|
return cleanedValue;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -651,7 +651,8 @@
|
|||||||
"zip_post": "Zip/Postal Code",
|
"zip_post": "Zip/Postal Code",
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"description": "Select employees to automatically follow new jobs and receive notifications for job updates.",
|
"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": {
|
"labels": {
|
||||||
|
|||||||
@@ -651,7 +651,8 @@
|
|||||||
"zip_post": "",
|
"zip_post": "",
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"description": "",
|
"description": "",
|
||||||
"placeholder": ""
|
"placeholder": "",
|
||||||
|
"invalid_followers": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
|
|||||||
@@ -651,7 +651,8 @@
|
|||||||
"zip_post": "",
|
"zip_post": "",
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"description": "",
|
"description": "",
|
||||||
"placeholder": ""
|
"placeholder": "",
|
||||||
|
"invalid_followers": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."bodyshops" alter column "notification_followers" set default json_build_object();
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."bodyshops" alter column "notification_followers" set default json_build_array();
|
||||||
@@ -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 = `
|
exports.GET_NOTIFICATION_ASSOCIATIONS = `
|
||||||
query GET_NOTIFICATION_ASSOCIATIONS($emails: [String!]!, $shopid: uuid!) {
|
query GET_NOTIFICATION_ASSOCIATIONS($emails: [String!]!, $shopid: uuid!) {
|
||||||
associations(where: {
|
associations(where: {
|
||||||
@@ -2802,6 +2813,7 @@ exports.GET_BODYSHOP_BY_ID = `
|
|||||||
imexshopid
|
imexshopid
|
||||||
intellipay_config
|
intellipay_config
|
||||||
state
|
state
|
||||||
|
notification_followers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -2931,10 +2943,6 @@ exports.INSERT_NEW_DOCUMENT = `
|
|||||||
|
|
||||||
exports.GET_AUTOADD_NOTIFICATION_USERS = `
|
exports.GET_AUTOADD_NOTIFICATION_USERS = `
|
||||||
query GET_AUTOADD_NOTIFICATION_USERS($shopId: uuid!) {
|
query GET_AUTOADD_NOTIFICATION_USERS($shopId: uuid!) {
|
||||||
bodyshops_by_pk(id: $shopId) {
|
|
||||||
id
|
|
||||||
notification_followers
|
|
||||||
}
|
|
||||||
associations(where: {
|
associations(where: {
|
||||||
_and: [
|
_and: [
|
||||||
{ shopid: { _eq: $shopId } },
|
{ shopid: { _eq: $shopId } },
|
||||||
|
|||||||
@@ -7,8 +7,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const { client: gqlClient } = require("../graphql-client/graphql-client");
|
const { client: gqlClient } = require("../graphql-client/graphql-client");
|
||||||
const queries = require("../graphql-client/queries");
|
|
||||||
const { isEmpty } = require("lodash");
|
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.
|
// 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";
|
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 autoAddWatchers = async (req) => {
|
||||||
const { event, trigger } = req.body;
|
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) {
|
if (trigger?.name !== "notifications_jobs_autoadd" || event.op !== "INSERT" || event.data.old) {
|
||||||
logger.log("Invalid event for auto-add watchers, skipping", "info", "notifications");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,8 +49,12 @@ const autoAddWatchers = async (req) => {
|
|||||||
const hasuraUserId = event?.session_variables?.["x-hasura-user-id"];
|
const hasuraUserId = event?.session_variables?.["x-hasura-user-id"];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch auto-add users and notification followers
|
// Fetch bodyshop data from Redis
|
||||||
const autoAddData = await gqlClient.request(queries.GET_AUTOADD_NOTIFICATION_USERS, { shopId });
|
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
|
// Get users with notifications_autoadd: true
|
||||||
const autoAddUsers =
|
const autoAddUsers =
|
||||||
@@ -53,12 +64,11 @@ const autoAddWatchers = async (req) => {
|
|||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
// Get users from notification_followers (array of employee IDs)
|
// Get users from notification_followers (array of employee IDs)
|
||||||
const notificationFollowers = autoAddData?.bodyshops_by_pk?.notification_followers || [];
|
|
||||||
let followerEmails = [];
|
let followerEmails = [];
|
||||||
if (notificationFollowers.length > 0) {
|
if (notificationFollowers.length > 0) {
|
||||||
const validFollowers = notificationFollowers.filter((id) => id); // Remove null values
|
const validFollowers = notificationFollowers.filter((id) => id); // Remove null values
|
||||||
if (validFollowers.length > 0) {
|
if (validFollowers.length > 0) {
|
||||||
const employeeData = await gqlClient.request(queries.GET_EMPLOYEE_EMAILS, {
|
const employeeData = await gqlClient.request(GET_EMPLOYEE_EMAILS, {
|
||||||
employeeIds: validFollowers,
|
employeeIds: validFollowers,
|
||||||
shopid: shopId
|
shopid: shopId
|
||||||
});
|
});
|
||||||
@@ -80,12 +90,11 @@ const autoAddWatchers = async (req) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (isEmpty(usersToAdd)) {
|
if (isEmpty(usersToAdd)) {
|
||||||
logger.log(`No users to auto-add for jobId "${jobId}" (RO: ${roNumber})`, "info", "notifications");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check existing watchers to avoid duplicates
|
// 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) || [];
|
const existingWatcherEmails = existingWatchersData?.job_watchers?.map((w) => w.user_email) || [];
|
||||||
|
|
||||||
// Filter out already existing watchers and optionally the user who created the job
|
// Filter out already existing watchers and optionally the user who created the job
|
||||||
@@ -104,23 +113,11 @@ const autoAddWatchers = async (req) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
if (isEmpty(newWatchers)) {
|
if (isEmpty(newWatchers)) {
|
||||||
logger.log(
|
|
||||||
`No new watchers to add after filtering for jobId "${jobId}" (RO: ${roNumber})`,
|
|
||||||
"info",
|
|
||||||
"notifications"
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert new watchers
|
// Insert new watchers
|
||||||
await gqlClient.request(queries.INSERT_JOB_WATCHERS, { watchers: newWatchers });
|
await gqlClient.request(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) }
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Error adding auto-add watchers", "error", "notifications", null, {
|
logger.log("Error adding auto-add watchers", "error", "notifications", null, {
|
||||||
message: error?.message,
|
message: error?.message,
|
||||||
|
|||||||
Reference in New Issue
Block a user