Merged in feature/IO-3225-Notifications-1.5 (pull request #2300)
Feature/IO-3225 Notifications 1.5 into Release/2025-05-09
This commit is contained in:
739
client/package-lock.json
generated
739
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -19,12 +19,12 @@
|
|||||||
"@firebase/messaging": "^0.12.17",
|
"@firebase/messaging": "^0.12.17",
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
"@reduxjs/toolkit": "^2.6.1",
|
"@reduxjs/toolkit": "^2.6.1",
|
||||||
"@sentry/cli": "^2.43.0",
|
"@sentry/cli": "^2.44.0",
|
||||||
"@sentry/react": "^9.11.0",
|
"@sentry/react": "^9.15.0",
|
||||||
"@sentry/vite-plugin": "^3.3.1",
|
"@sentry/vite-plugin": "^3.4.0",
|
||||||
"@splitsoftware/splitio-react": "^2.1.1",
|
"@splitsoftware/splitio-react": "^2.1.1",
|
||||||
"@tanem/react-nprogress": "^5.0.53",
|
"@tanem/react-nprogress": "^5.0.53",
|
||||||
"antd": "^5.24.6",
|
"antd": "^5.24.9",
|
||||||
"apollo-link-logger": "^2.0.1",
|
"apollo-link-logger": "^2.0.1",
|
||||||
"apollo-link-sentry": "^4.2.0",
|
"apollo-link-sentry": "^4.2.0",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
@@ -37,9 +37,9 @@
|
|||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"graphql": "^16.10.0",
|
"graphql": "^16.11.0",
|
||||||
"i18next": "^24.2.3",
|
"i18next": "^24.2.3",
|
||||||
"i18next-browser-languagedetector": "^8.0.4",
|
"i18next-browser-languagedetector": "^8.1.0",
|
||||||
"immutability-helper": "^3.1.1",
|
"immutability-helper": "^3.1.1",
|
||||||
"libphonenumber-js": "^1.12.6",
|
"libphonenumber-js": "^1.12.6",
|
||||||
"logrocket": "^9.0.2",
|
"logrocket": "^9.0.2",
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
"normalize-url": "^8.0.1",
|
"normalize-url": "^8.0.1",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"query-string": "^9.1.1",
|
"query-string": "^9.1.2",
|
||||||
"raf-schd": "^4.0.3",
|
"raf-schd": "^4.0.3",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-big-calendar": "^1.18.0",
|
"react-big-calendar": "^1.18.0",
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
"react-resizable": "^3.0.5",
|
"react-resizable": "^3.0.5",
|
||||||
"react-router-dom": "^6.30.0",
|
"react-router-dom": "^6.30.0",
|
||||||
"react-sticky": "^6.0.3",
|
"react-sticky": "^6.0.3",
|
||||||
"react-virtuoso": "^4.12.6",
|
"react-virtuoso": "^4.12.7",
|
||||||
"recharts": "^2.15.2",
|
"recharts": "^2.15.2",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"redux-actions": "^3.0.3",
|
"redux-actions": "^3.0.3",
|
||||||
@@ -129,18 +129,18 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/icons": "^6.0.0",
|
"@ant-design/icons": "^6.0.0",
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-react": "^7.26.3",
|
"@babel/preset-react": "^7.27.1",
|
||||||
"@dotenvx/dotenvx": "^1.39.1",
|
"@dotenvx/dotenvx": "^1.43.0",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@eslint/js": "^9.24.0",
|
"@eslint/js": "^9.26.0",
|
||||||
"@playwright/test": "^1.51.1",
|
"@playwright/test": "^1.51.1",
|
||||||
"@sentry/webpack-plugin": "^3.3.1",
|
"@sentry/webpack-plugin": "^3.4.0",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"browserslist": "^4.24.4",
|
"browserslist": "^4.24.5",
|
||||||
"browserslist-to-esbuild": "^2.1.1",
|
"browserslist-to-esbuild": "^2.1.1",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
@@ -148,19 +148,19 @@
|
|||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
"memfs": "^4.17.0",
|
"memfs": "^4.17.1",
|
||||||
"os-browserify": "^0.3.0",
|
"os-browserify": "^0.3.0",
|
||||||
"playwright": "^1.51.1",
|
"playwright": "^1.51.1",
|
||||||
"react-error-overlay": "^6.1.0",
|
"react-error-overlay": "^6.1.0",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"source-map-explorer": "^2.5.3",
|
"source-map-explorer": "^2.5.3",
|
||||||
"vite": "^6.2.5",
|
"vite": "^6.3.5",
|
||||||
"vite-plugin-babel": "^1.3.0",
|
"vite-plugin-babel": "^1.3.1",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-node-polyfills": "^0.23.0",
|
"vite-plugin-node-polyfills": "^0.23.0",
|
||||||
"vite-plugin-pwa": "^1.0.0",
|
"vite-plugin-pwa": "^1.0.0",
|
||||||
"vite-plugin-style-import": "^2.0.0",
|
"vite-plugin-style-import": "^2.0.0",
|
||||||
"vitest": "^3.1.1",
|
"vitest": "^3.1.3",
|
||||||
"workbox-window": "^7.3.0"
|
"workbox-window": "^7.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" style={{ padding: "0.1 0.1rem", marginRight: "1px", marginLeft: "1px" }}>
|
||||||
<Tag color="green">
|
|
||||||
{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>
|
||||||
|
{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,12 +1,16 @@
|
|||||||
import { useMutation, useQuery } from "@apollo/client";
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button, Card, Checkbox, Form, Space, Table } from "antd";
|
import { Button, Card, Checkbox, Divider, Form, Space, Switch, Table, Typography } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import { QUERY_NOTIFICATION_SETTINGS, UPDATE_NOTIFICATION_SETTINGS } from "../../graphql/user.queries.js";
|
import {
|
||||||
|
QUERY_NOTIFICATION_SETTINGS,
|
||||||
|
UPDATE_NOTIFICATION_SETTINGS,
|
||||||
|
UPDATE_NOTIFICATIONS_AUTOADD
|
||||||
|
} from "../../graphql/user.queries.js";
|
||||||
import { notificationScenarios } from "../../utils/jobNotificationScenarios.js";
|
import { notificationScenarios } from "../../utils/jobNotificationScenarios.js";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
@@ -24,9 +28,11 @@ const NotificationSettingsForm = ({ currentUser }) => {
|
|||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [initialValues, setInitialValues] = useState({});
|
const [initialValues, setInitialValues] = useState({});
|
||||||
const [isDirty, setIsDirty] = useState(false);
|
const [isDirty, setIsDirty] = useState(false);
|
||||||
|
const [autoAddEnabled, setAutoAddEnabled] = useState(false);
|
||||||
|
const [initialAutoAdd, setInitialAutoAdd] = useState(false);
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|
||||||
// Fetch notification settings.
|
// Fetch notification settings and notifications_autoadd
|
||||||
const { loading, error, data } = useQuery(QUERY_NOTIFICATION_SETTINGS, {
|
const { loading, error, data } = useQuery(QUERY_NOTIFICATION_SETTINGS, {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
@@ -34,13 +40,16 @@ const NotificationSettingsForm = ({ currentUser }) => {
|
|||||||
skip: !currentUser
|
skip: !currentUser
|
||||||
});
|
});
|
||||||
|
|
||||||
const [updateNotificationSettings, { loading: saving }] = useMutation(UPDATE_NOTIFICATION_SETTINGS);
|
const [updateNotificationSettings, { loading: savingSettings }] = useMutation(UPDATE_NOTIFICATION_SETTINGS);
|
||||||
|
const [updateNotificationsAutoAdd, { loading: savingAutoAdd }] = useMutation(UPDATE_NOTIFICATIONS_AUTOADD);
|
||||||
|
|
||||||
// Populate form with fetched data.
|
// Populate form with fetched data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.associations?.length > 0) {
|
if (data?.associations?.length > 0) {
|
||||||
const settings = data.associations[0].notification_settings || {};
|
const settings = data.associations[0].notification_settings || {};
|
||||||
// Ensure each scenario has an object with { app, email, fcm }.
|
const autoAdd = data.associations[0].notifications_autoadd ?? false;
|
||||||
|
|
||||||
|
// Ensure each scenario has an object with { app, email, fcm }
|
||||||
const formattedValues = notificationScenarios.reduce((acc, scenario) => {
|
const formattedValues = notificationScenarios.reduce((acc, scenario) => {
|
||||||
acc[scenario] = settings[scenario] ?? { app: false, email: false, fcm: false };
|
acc[scenario] = settings[scenario] ?? { app: false, email: false, fcm: false };
|
||||||
return acc;
|
return acc;
|
||||||
@@ -48,32 +57,66 @@ const NotificationSettingsForm = ({ currentUser }) => {
|
|||||||
|
|
||||||
setInitialValues(formattedValues);
|
setInitialValues(formattedValues);
|
||||||
form.setFieldsValue(formattedValues);
|
form.setFieldsValue(formattedValues);
|
||||||
setIsDirty(false); // Reset dirty state when new data loads.
|
setAutoAddEnabled(autoAdd);
|
||||||
|
setInitialAutoAdd(autoAdd);
|
||||||
|
setIsDirty(false); // Reset dirty state when new data loads
|
||||||
}
|
}
|
||||||
}, [data, form]);
|
}, [data, form]);
|
||||||
|
|
||||||
|
// Handle toggle of notifications_autoadd
|
||||||
|
const handleAutoAddToggle = async (checked) => {
|
||||||
|
if (data?.associations?.length > 0) {
|
||||||
|
const userId = data.associations[0].id;
|
||||||
|
try {
|
||||||
|
const result = await updateNotificationsAutoAdd({
|
||||||
|
variables: { id: userId, autoadd: checked }
|
||||||
|
});
|
||||||
|
if (!result?.errors) {
|
||||||
|
setAutoAddEnabled(checked);
|
||||||
|
setInitialAutoAdd(checked);
|
||||||
|
notification.success({ message: t("notifications.labels.auto-add-success") });
|
||||||
|
setIsDirty(false); // Reset dirty state if only auto-add was changed
|
||||||
|
} else {
|
||||||
|
throw new Error("Failed to update auto-add setting");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setAutoAddEnabled(!checked); // Revert on error
|
||||||
|
notification.error({ message: t("notifications.labels.auto-add-failure") });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle save of notification settings
|
||||||
const handleSave = async (values) => {
|
const handleSave = async (values) => {
|
||||||
if (data?.associations?.length > 0) {
|
if (data?.associations?.length > 0) {
|
||||||
const userId = data.associations[0].id;
|
const userId = data.associations[0].id;
|
||||||
// Save the updated notification settings.
|
try {
|
||||||
const result = await updateNotificationSettings({ variables: { id: userId, ns: values } });
|
const result = await updateNotificationSettings({ variables: { id: userId, ns: values } });
|
||||||
if (!result?.errors) {
|
if (!result?.errors) {
|
||||||
notification.success({ message: t("notifications.labels.notification-settings-success") });
|
notification.success({ message: t("notifications.labels.notification-settings-success") });
|
||||||
setInitialValues(values);
|
setInitialValues(values);
|
||||||
setIsDirty(false);
|
setIsDirty(false);
|
||||||
} else {
|
} else {
|
||||||
|
throw new Error("Failed to update notification settings");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
notification.error({ message: t("notifications.labels.notification-settings-failure") });
|
notification.error({ message: t("notifications.labels.notification-settings-failure") });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mark the form as dirty on any manual change.
|
// Mark the form as dirty on any manual change
|
||||||
const handleFormChange = () => {
|
const handleFormChange = () => {
|
||||||
setIsDirty(true);
|
setIsDirty(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check if auto-add has changed
|
||||||
|
const isAutoAddDirty = autoAddEnabled !== initialAutoAdd;
|
||||||
|
|
||||||
|
// Handle reset of form and auto-add
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
form.setFieldsValue(initialValues);
|
form.setFieldsValue(initialValues);
|
||||||
|
setAutoAddEnabled(initialAutoAdd);
|
||||||
setIsDirty(false);
|
setIsDirty(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -139,17 +182,25 @@ const NotificationSettingsForm = ({ currentUser }) => {
|
|||||||
title={t("notifications.labels.notificationscenarios")}
|
title={t("notifications.labels.notificationscenarios")}
|
||||||
extra={
|
extra={
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="default" onClick={handleReset} disabled={!isDirty}>
|
<Typography.Text type="secondary">{t("notifications.labels.auto-add")}</Typography.Text>
|
||||||
|
<Switch
|
||||||
|
checked={autoAddEnabled}
|
||||||
|
onChange={handleAutoAddToggle}
|
||||||
|
loading={savingAutoAdd}
|
||||||
|
// checkedChildren={t("notifications.labels.auto-add-on")}
|
||||||
|
// unCheckedChildren={t("notifications.labels.auto-add-off")}
|
||||||
|
/>
|
||||||
|
<Button type="default" onClick={handleReset} disabled={!isDirty && !isAutoAddDirty}>
|
||||||
{t("general.actions.clear")}
|
{t("general.actions.clear")}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button type="primary" htmlType="submit" disabled={!isDirty} loading={savingSettings}>
|
||||||
<Button type="primary" htmlType="submit" disabled={!isDirty} loading={saving}>
|
|
||||||
{t("notifications.labels.save")}
|
{t("notifications.labels.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Table dataSource={dataSource} columns={columns} pagination={false} bordered rowKey="key" />
|
<Table dataSource={dataSource} columns={columns} pagination={false} bordered rowKey="key" />
|
||||||
|
<Divider />
|
||||||
</Card>
|
</Card>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { Button, Card, Tabs } from "antd";
|
import { Button, Card, Tabs } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@@ -24,6 +23,8 @@ import ShopInfoRoGuard from "./shop-info.roguard.component";
|
|||||||
import ShopInfoIntellipay from "./shop-intellipay-config.component";
|
import ShopInfoIntellipay from "./shop-intellipay-config.component";
|
||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
import LockWrapperComponent from "../lock-wrapper/lock-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({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -41,6 +42,7 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
|
|||||||
names: ["CriticalPartsScanning", "Enhanced_Payroll"],
|
names: ["CriticalPartsScanning", "Enhanced_Payroll"],
|
||||||
splitKey: bodyshop.imexshopid
|
splitKey: bodyshop.imexshopid
|
||||||
});
|
});
|
||||||
|
const { scenarioNotificationsOn } = useSocket();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
@@ -137,9 +139,21 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
|
|||||||
|
|
||||||
{
|
{
|
||||||
key: "intellipay",
|
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: <ShopInfoIntellipay form={form} />
|
children: <ShopInfoIntellipay form={form} />
|
||||||
}
|
},
|
||||||
|
...(scenarioNotificationsOn
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: "notifications_autoadd",
|
||||||
|
label: t("bodyshop.labels.notifications.followers"),
|
||||||
|
children: <ShopInfoNotificationsAutoadd form={form} bodyshop={bodyshop} />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: [])
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
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({ bodyshop }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<div>
|
||||||
|
<Paragraph>{t("bodyshop.fields.notifications.description")}</Paragraph>
|
||||||
|
<Text type="secondary">{t("bodyshop.labels.notifications.followers")}</Text>
|
||||||
|
{employeeOptions.length > 0 ? (
|
||||||
|
<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
|
||||||
|
style={{ minWidth: "100%" }}
|
||||||
|
mode="multiple"
|
||||||
|
options={employeeOptions}
|
||||||
|
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>
|
||||||
|
) : (
|
||||||
|
<Text type="secondary">{t("bodyshop.fields.no_employees_available")}</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -25,23 +25,6 @@ export function ShopTemplateTestRender({ bodyshop, query, emailEditorRef, style
|
|||||||
|
|
||||||
emailEditorRef.current.exportHtml(async (data) => {
|
emailEditorRef.current.exportHtml(async (data) => {
|
||||||
try {
|
try {
|
||||||
// const inlineHtml = await axios.post("/render/inlinecss", {
|
|
||||||
// html: data.html,
|
|
||||||
// url: `${window.location.protocol}://${window.location.host}/`,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const { data: contextData } = await client.query({
|
|
||||||
// query: gql(query),
|
|
||||||
// variables: variables,
|
|
||||||
//
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const renderResponse = await axios.post("/render", {
|
|
||||||
// view: inlineHtml.data,
|
|
||||||
// context: { ...contextData, bodyshop: bodyshop },
|
|
||||||
// });
|
|
||||||
// displayTemplateInWindowNoprint(renderResponse.data);
|
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ export const QUERY_BODYSHOP = gql`
|
|||||||
use_paint_scale_data
|
use_paint_scale_data
|
||||||
intellipay_config
|
intellipay_config
|
||||||
md_ro_guard
|
md_ro_guard
|
||||||
|
notification_followers
|
||||||
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
|
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
@@ -271,6 +272,7 @@ export const UPDATE_SHOP = gql`
|
|||||||
md_tasks_presets
|
md_tasks_presets
|
||||||
intellipay_config
|
intellipay_config
|
||||||
md_ro_guard
|
md_ro_guard
|
||||||
|
notification_followers
|
||||||
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
|
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export const QUERY_NOTIFICATION_SETTINGS = gql`
|
|||||||
associations(where: { _and: { useremail: { _eq: $email }, active: { _eq: true } } }) {
|
associations(where: { _and: { useremail: { _eq: $email }, active: { _eq: true } } }) {
|
||||||
id
|
id
|
||||||
notification_settings
|
notification_settings
|
||||||
|
notifications_autoadd
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -103,3 +104,12 @@ export const UPDATE_NOTIFICATION_SETTINGS = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const UPDATE_NOTIFICATIONS_AUTOADD = gql`
|
||||||
|
mutation UPDATE_NOTIFICATIONS_AUTOADD($id: uuid!, $autoadd: Boolean!) {
|
||||||
|
update_associations_by_pk(pk_columns: { id: $id }, _set: { notifications_autoadd: $autoadd }) {
|
||||||
|
id
|
||||||
|
notifications_autoadd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -648,7 +648,12 @@
|
|||||||
"use_paint_scale_data": "Use Paint Scale Data for Job Costing?",
|
"use_paint_scale_data": "Use Paint Scale Data for Job Costing?",
|
||||||
"uselocalmediaserver": "Use Local Media Server?",
|
"uselocalmediaserver": "Use Local Media Server?",
|
||||||
"website": "Website",
|
"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",
|
||||||
|
"invalid_followers": "Invalid selection. Please select valid employees."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"2tiername": "Name => RO",
|
"2tiername": "Name => RO",
|
||||||
@@ -728,7 +733,10 @@
|
|||||||
"ssbuckets": "Job Size Definitions",
|
"ssbuckets": "Job Size Definitions",
|
||||||
"systemsettings": "System Settings",
|
"systemsettings": "System Settings",
|
||||||
"task-presets": "Task Presets",
|
"task-presets": "Task Presets",
|
||||||
"workingdays": "Working Days"
|
"workingdays": "Working Days",
|
||||||
|
"notifications": {
|
||||||
|
"followers": "Notifications"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"operations": {
|
"operations": {
|
||||||
"contains": "Contains",
|
"contains": "Contains",
|
||||||
@@ -2441,6 +2449,9 @@
|
|||||||
"fcm": "Push"
|
"fcm": "Push"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
|
"auto-add": "Automatically watch Jobs I import",
|
||||||
|
"auto-add-success": "Auto watcher status successfully changed.",
|
||||||
|
"auto-add-failure": "Something went wrong updating your auto watcher status.",
|
||||||
"add-watchers": "Add Watchers",
|
"add-watchers": "Add Watchers",
|
||||||
"add-watchers-team": "Add Team Members",
|
"add-watchers-team": "Add Team Members",
|
||||||
"employee-search": "Search for an Employee",
|
"employee-search": "Search for an Employee",
|
||||||
|
|||||||
@@ -648,7 +648,12 @@
|
|||||||
"use_paint_scale_data": "",
|
"use_paint_scale_data": "",
|
||||||
"uselocalmediaserver": "",
|
"uselocalmediaserver": "",
|
||||||
"website": "",
|
"website": "",
|
||||||
"zip_post": ""
|
"zip_post": "",
|
||||||
|
"notifications": {
|
||||||
|
"description": "",
|
||||||
|
"placeholder": "",
|
||||||
|
"invalid_followers": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"2tiername": "",
|
"2tiername": "",
|
||||||
@@ -728,7 +733,10 @@
|
|||||||
"ssbuckets": "",
|
"ssbuckets": "",
|
||||||
"systemsettings": "",
|
"systemsettings": "",
|
||||||
"task-presets": "",
|
"task-presets": "",
|
||||||
"workingdays": ""
|
"workingdays": "",
|
||||||
|
"notifications": {
|
||||||
|
"followers": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"operations": {
|
"operations": {
|
||||||
"contains": "",
|
"contains": "",
|
||||||
@@ -2441,6 +2449,11 @@
|
|||||||
"fcm": ""
|
"fcm": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
|
"auto-add-on": "",
|
||||||
|
"auto-add-off": "",
|
||||||
|
"auto-add-success": "",
|
||||||
|
"auto-add-failure": "",
|
||||||
|
"auto-add-description": "",
|
||||||
"add-watchers": "",
|
"add-watchers": "",
|
||||||
"add-watchers-team": "",
|
"add-watchers-team": "",
|
||||||
"employee-search": "",
|
"employee-search": "",
|
||||||
|
|||||||
@@ -648,7 +648,12 @@
|
|||||||
"use_paint_scale_data": "",
|
"use_paint_scale_data": "",
|
||||||
"uselocalmediaserver": "",
|
"uselocalmediaserver": "",
|
||||||
"website": "",
|
"website": "",
|
||||||
"zip_post": ""
|
"zip_post": "",
|
||||||
|
"notifications": {
|
||||||
|
"description": "",
|
||||||
|
"placeholder": "",
|
||||||
|
"invalid_followers": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"2tiername": "",
|
"2tiername": "",
|
||||||
@@ -728,7 +733,10 @@
|
|||||||
"ssbuckets": "",
|
"ssbuckets": "",
|
||||||
"systemsettings": "",
|
"systemsettings": "",
|
||||||
"task-presets": "",
|
"task-presets": "",
|
||||||
"workingdays": ""
|
"workingdays": "",
|
||||||
|
"notifications": {
|
||||||
|
"followers": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"operations": {
|
"operations": {
|
||||||
"contains": "",
|
"contains": "",
|
||||||
@@ -2441,6 +2449,11 @@
|
|||||||
"fcm": ""
|
"fcm": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
|
"auto-add-on": "",
|
||||||
|
"auto-add-off": "",
|
||||||
|
"auto-add-success": "",
|
||||||
|
"auto-add-failure": "",
|
||||||
|
"auto-add-description": "",
|
||||||
"add-watchers": "",
|
"add-watchers": "",
|
||||||
"add-watchers-team": "",
|
"add-watchers-team": "",
|
||||||
"employee-search": "",
|
"employee-search": "",
|
||||||
|
|||||||
@@ -216,6 +216,7 @@
|
|||||||
- id
|
- id
|
||||||
- kanban_settings
|
- kanban_settings
|
||||||
- notification_settings
|
- notification_settings
|
||||||
|
- notifications_autoadd
|
||||||
- qbo_realmId
|
- qbo_realmId
|
||||||
- shopid
|
- shopid
|
||||||
- useremail
|
- useremail
|
||||||
@@ -232,6 +233,7 @@
|
|||||||
- default_prod_list_view
|
- default_prod_list_view
|
||||||
- kanban_settings
|
- kanban_settings
|
||||||
- notification_settings
|
- notification_settings
|
||||||
|
- notifications_autoadd
|
||||||
- qbo_realmId
|
- qbo_realmId
|
||||||
filter:
|
filter:
|
||||||
user:
|
user:
|
||||||
@@ -1002,6 +1004,7 @@
|
|||||||
- md_tasks_presets
|
- md_tasks_presets
|
||||||
- md_to_emails
|
- md_to_emails
|
||||||
- messagingservicesid
|
- messagingservicesid
|
||||||
|
- notification_followers
|
||||||
- pbs_configuration
|
- pbs_configuration
|
||||||
- pbs_serialnumber
|
- pbs_serialnumber
|
||||||
- phone
|
- phone
|
||||||
@@ -1104,6 +1107,7 @@
|
|||||||
- md_ro_statuses
|
- md_ro_statuses
|
||||||
- md_tasks_presets
|
- md_tasks_presets
|
||||||
- md_to_emails
|
- md_to_emails
|
||||||
|
- notification_followers
|
||||||
- pbs_configuration
|
- pbs_configuration
|
||||||
- phone
|
- phone
|
||||||
- prodtargethrs
|
- prodtargethrs
|
||||||
@@ -4588,12 +4592,34 @@
|
|||||||
request_transform:
|
request_transform:
|
||||||
body:
|
body:
|
||||||
action: transform
|
action: transform
|
||||||
template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body?.event?.session_variables?.x-hasura-user-id ?? \"Internal\"}},\r\n \"x-hasura-role\": {{$body?.event?.session_variables?.x-hasura-role ?? \"Internal\"}}\r\n }, \r\n \"op\": \"UPDATE\",\r\n \"data\": {\r\n \"old\": {\r\n \"id\": {{$body.event.data.old.id}},\r\n \"ro_number\": {{$body.event.data.old.ro_number}},\r\n \"queued_for_parts\": {{$body.event.data.old.queued_for_parts}},\r\n \"employee_prep\": {{$body.event.data.old.employee_prep}},\r\n \"clm_total\": {{$body.event.data.old.clm_total}},\r\n \"towin\": {{$body.event.data.old.towin}},\r\n \"employee_body\": {{$body.event.data.old.employee_body}},\r\n \"converted\": {{$body.event.data.old.converted}},\r\n \"scheduled_in\": {{$body.event.data.old.scheduled_in}},\r\n \"scheduled_completion\": {{$body.event.data.old.scheduled_completion}},\r\n \"scheduled_delivery\": {{$body.event.data.old.scheduled_delivery}},\r\n \"actual_delivery\": {{$body.event.data.old.actual_delivery}},\r\n \"actual_completion\": {{$body.event.data.old.actual_completion}},\r\n \"alt_transport\": {{$body.event.data.old.alt_transport}},\r\n \"date_exported\": {{$body.event.data.old.date_exported}},\r\n \"status\": {{$body.event.data.old.status}},\r\n \"employee_csr\": {{$body.event.data.old.employee_csr}},\r\n \"actual_in\": {{$body.event.data.old.actual_in}},\r\n \"deliverchecklist\": {{$body.event.data.old.deliverchecklist}},\r\n \"comment\": {{$body.event.data.old.comment}},\r\n \"employee_refinish\": {{$body.event.data.old.employee_refinish}},\r\n \"inproduction\": {{$body.event.data.old.inproduction}},\r\n \"production_vars\": {{$body.event.data.old.production_vars}},\r\n \"intakechecklist\": {{$body.event.data.old.intakechecklist}},\r\n \"cieca_ttl\": {{$body.event.data.old.cieca_ttl}},\r\n \"date_invoiced\": {{$body.event.data.old.date_invoiced}}\r\n },\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"ro_number\": {{$body.event.data.old.ro_number}},\r\n \"queued_for_parts\": {{$body.event.data.new.queued_for_parts}},\r\n \"employee_prep\": {{$body.event.data.new.employee_prep}},\r\n \"clm_total\": {{$body.event.data.new.clm_total}},\r\n \"towin\": {{$body.event.data.new.towin}},\r\n \"employee_body\": {{$body.event.data.new.employee_body}},\r\n \"converted\": {{$body.event.data.new.converted}},\r\n \"scheduled_in\": {{$body.event.data.new.scheduled_in}},\r\n \"scheduled_completion\": {{$body.event.data.new.scheduled_completion}},\r\n \"scheduled_delivery\": {{$body.event.data.new.scheduled_delivery}},\r\n \"actual_delivery\": {{$body.event.data.new.actual_delivery}},\r\n \"actual_completion\": {{$body.event.data.new.actual_completion}},\r\n \"alt_transport\": {{$body.event.data.new.alt_transport}},\r\n \"date_exported\": {{$body.event.data.new.date_exported}},\r\n \"status\": {{$body.event.data.new.status}},\r\n \"employee_csr\": {{$body.event.data.new.employee_csr}},\r\n \"actual_in\": {{$body.event.data.new.actual_in}},\r\n \"deliverchecklist\": {{$body.event.data.new.deliverchecklist}},\r\n \"comment\": {{$body.event.data.new.comment}},\r\n \"employee_refinish\": {{$body.event.data.new.employee_refinish}},\r\n \"inproduction\": {{$body.event.data.new.inproduction}},\r\n \"production_vars\": {{$body.event.data.new.production_vars}},\r\n \"intakechecklist\": {{$body.event.data.new.intakechecklist}},\r\n \"cieca_ttl\": {{$body.event.data.new.cieca_ttl}},\r\n \"date_invoiced\": {{$body.event.data.new.date_invoiced}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_jobs\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"jobs\"\r\n }\r\n}\r\n"
|
template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body?.event?.session_variables?.x-hasura-user-id ?? \"Internal\"}},\r\n \"x-hasura-role\": {{$body?.event?.session_variables?.x-hasura-role ?? \"Internal\"}}\r\n }, \r\n \"op\": {{$body.event.op}},\r\n \"data\": {\r\n \"old\": {\r\n \"id\": {{$body.event.data.old.id}},\r\n \"ro_number\": {{$body.event.data.old.ro_number}},\r\n \"queued_for_parts\": {{$body.event.data.old.queued_for_parts}},\r\n \"employee_prep\": {{$body.event.data.old.employee_prep}},\r\n \"clm_total\": {{$body.event.data.old.clm_total}},\r\n \"towin\": {{$body.event.data.old.towin}},\r\n \"employee_body\": {{$body.event.data.old.employee_body}},\r\n \"converted\": {{$body.event.data.old.converted}},\r\n \"scheduled_in\": {{$body.event.data.old.scheduled_in}},\r\n \"scheduled_completion\": {{$body.event.data.old.scheduled_completion}},\r\n \"scheduled_delivery\": {{$body.event.data.old.scheduled_delivery}},\r\n \"actual_delivery\": {{$body.event.data.old.actual_delivery}},\r\n \"actual_completion\": {{$body.event.data.old.actual_completion}},\r\n \"alt_transport\": {{$body.event.data.old.alt_transport}},\r\n \"date_exported\": {{$body.event.data.old.date_exported}},\r\n \"status\": {{$body.event.data.old.status}},\r\n \"employee_csr\": {{$body.event.data.old.employee_csr}},\r\n \"actual_in\": {{$body.event.data.old.actual_in}},\r\n \"deliverchecklist\": {{$body.event.data.old.deliverchecklist}},\r\n \"comment\": {{$body.event.data.old.comment}},\r\n \"employee_refinish\": {{$body.event.data.old.employee_refinish}},\r\n \"inproduction\": {{$body.event.data.old.inproduction}},\r\n \"production_vars\": {{$body.event.data.old.production_vars}},\r\n \"intakechecklist\": {{$body.event.data.old.intakechecklist}},\r\n \"cieca_ttl\": {{$body.event.data.old.cieca_ttl}},\r\n \"date_invoiced\": {{$body.event.data.old.date_invoiced}}\r\n },\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"ro_number\": {{$body.event.data.old.ro_number}},\r\n \"queued_for_parts\": {{$body.event.data.new.queued_for_parts}},\r\n \"employee_prep\": {{$body.event.data.new.employee_prep}},\r\n \"clm_total\": {{$body.event.data.new.clm_total}},\r\n \"towin\": {{$body.event.data.new.towin}},\r\n \"employee_body\": {{$body.event.data.new.employee_body}},\r\n \"converted\": {{$body.event.data.new.converted}},\r\n \"scheduled_in\": {{$body.event.data.new.scheduled_in}},\r\n \"scheduled_completion\": {{$body.event.data.new.scheduled_completion}},\r\n \"scheduled_delivery\": {{$body.event.data.new.scheduled_delivery}},\r\n \"actual_delivery\": {{$body.event.data.new.actual_delivery}},\r\n \"actual_completion\": {{$body.event.data.new.actual_completion}},\r\n \"alt_transport\": {{$body.event.data.new.alt_transport}},\r\n \"date_exported\": {{$body.event.data.new.date_exported}},\r\n \"status\": {{$body.event.data.new.status}},\r\n \"employee_csr\": {{$body.event.data.new.employee_csr}},\r\n \"actual_in\": {{$body.event.data.new.actual_in}},\r\n \"deliverchecklist\": {{$body.event.data.new.deliverchecklist}},\r\n \"comment\": {{$body.event.data.new.comment}},\r\n \"employee_refinish\": {{$body.event.data.new.employee_refinish}},\r\n \"inproduction\": {{$body.event.data.new.inproduction}},\r\n \"production_vars\": {{$body.event.data.new.production_vars}},\r\n \"intakechecklist\": {{$body.event.data.new.intakechecklist}},\r\n \"cieca_ttl\": {{$body.event.data.new.cieca_ttl}},\r\n \"date_invoiced\": {{$body.event.data.new.date_invoiced}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_jobs\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"jobs\"\r\n }\r\n}\r\n"
|
||||||
method: POST
|
method: POST
|
||||||
query_params: {}
|
query_params: {}
|
||||||
template_engine: Kriti
|
template_engine: Kriti
|
||||||
url: '{{$base_url}}/notifications/events/handleJobsChange'
|
url: '{{$base_url}}/notifications/events/handleJobsChange'
|
||||||
version: 2
|
version: 2
|
||||||
|
- name: notifications_jobs_autoadd
|
||||||
|
definition:
|
||||||
|
enable_manual: false
|
||||||
|
insert:
|
||||||
|
columns: '*'
|
||||||
|
retry_conf:
|
||||||
|
interval_sec: 10
|
||||||
|
num_retries: 0
|
||||||
|
timeout_sec: 60
|
||||||
|
webhook_from_env: HASURA_API_URL
|
||||||
|
headers:
|
||||||
|
- name: event-secret
|
||||||
|
value_from_env: EVENT_SECRET
|
||||||
|
request_transform:
|
||||||
|
body:
|
||||||
|
action: transform
|
||||||
|
template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body?.event?.session_variables?.x-hasura-user-id ?? \"Internal\"}},\r\n \"x-hasura-role\": {{$body?.event?.session_variables?.x-hasura-role ?? \"Internal\"}}\r\n }, \r\n \"op\": {{$body.event.op}},\r\n \"data\": {\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"shopid\": {{$body.event.data.new?.shopid}},\r\n \"ro_number\": {{$body.event.data.new?.ro_number}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_jobs_autoadd\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"jobs\"\r\n }\r\n}\r\n"
|
||||||
|
method: POST
|
||||||
|
query_params: {}
|
||||||
|
template_engine: Kriti
|
||||||
|
url: '{{$base_url}}/notifications/events/handleAutoAdd'
|
||||||
|
version: 2
|
||||||
- name: os_jobs
|
- name: os_jobs
|
||||||
definition:
|
definition:
|
||||||
delete:
|
delete:
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."bodyshops" add column "notification_followers" json
|
||||||
|
-- null default json_build_object();
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."bodyshops" add column "notification_followers" json
|
||||||
|
null default json_build_object();
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."associations" add column "notifications_autoadd" boolean
|
||||||
|
-- null default 'false';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."associations" add column "notifications_autoadd" boolean
|
||||||
|
null default 'false';
|
||||||
@@ -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();
|
||||||
2641
package-lock.json
generated
2641
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
38
package.json
38
package.json
@@ -16,39 +16,34 @@
|
|||||||
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
|
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-cloudwatch-logs": "^3.800.0",
|
"@aws-sdk/client-cloudwatch-logs": "^3.803.0",
|
||||||
"@aws-sdk/client-elasticache": "^3.799.0",
|
"@aws-sdk/client-elasticache": "^3.803.0",
|
||||||
"@aws-sdk/client-s3": "^3.800.0",
|
"@aws-sdk/client-s3": "^3.803.0",
|
||||||
"@aws-sdk/client-secrets-manager": "^3.799.0",
|
"@aws-sdk/client-secrets-manager": "^3.803.0",
|
||||||
"@aws-sdk/client-ses": "^3.799.0",
|
"@aws-sdk/client-ses": "^3.803.0",
|
||||||
"@aws-sdk/credential-provider-node": "^3.799.0",
|
"@aws-sdk/credential-provider-node": "^3.803.0",
|
||||||
"@aws-sdk/lib-storage": "^3.800.0",
|
"@aws-sdk/lib-storage": "^3.803.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.800.0",
|
"@aws-sdk/s3-request-presigner": "^3.803.0",
|
||||||
"@opensearch-project/opensearch": "^2.13.0",
|
"@opensearch-project/opensearch": "^2.13.0",
|
||||||
"@socket.io/admin-ui": "^0.5.1",
|
"@socket.io/admin-ui": "^0.5.1",
|
||||||
"@socket.io/redis-adapter": "^8.3.0",
|
"@socket.io/redis-adapter": "^8.3.0",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"aws4": "^1.13.2",
|
"aws4": "^1.13.2",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"bee-queue": "^1.7.1",
|
|
||||||
"better-queue": "^3.8.12",
|
"better-queue": "^3.8.12",
|
||||||
"bluebird": "^3.7.2",
|
|
||||||
"bullmq": "^5.52.1",
|
"bullmq": "^5.52.1",
|
||||||
"chart.js": "^4.4.8",
|
"chart.js": "^4.4.8",
|
||||||
"cloudinary": "^2.6.0",
|
"cloudinary": "^2.6.1",
|
||||||
"compression": "^1.8.0",
|
"compression": "^1.8.0",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"crisp-status-reporter": "^1.2.2",
|
"crisp-status-reporter": "^1.2.2",
|
||||||
"csrf": "^3.1.0",
|
"dd-trace": "^5.51.0",
|
||||||
"dd-trace": "^5.50.0",
|
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
"firebase-admin": "^13.2.0",
|
"firebase-admin": "^13.2.0",
|
||||||
"graphql": "^16.11.0",
|
|
||||||
"graphql-request": "^6.1.0",
|
"graphql-request": "^6.1.0",
|
||||||
"inline-css": "^4.0.3",
|
|
||||||
"intuit-oauth": "^4.2.0",
|
"intuit-oauth": "^4.2.0",
|
||||||
"ioredis": "^5.6.0",
|
"ioredis": "^5.6.0",
|
||||||
"json-2-csv": "^5.5.9",
|
"json-2-csv": "^5.5.9",
|
||||||
@@ -58,19 +53,18 @@
|
|||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"moment-timezone": "^0.5.48",
|
"moment-timezone": "^0.5.48",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"node-mailjet": "^6.0.8",
|
|
||||||
"node-persist": "^4.0.4",
|
"node-persist": "^4.0.4",
|
||||||
"nodemailer": "^6.10.0",
|
"nodemailer": "^6.10.0",
|
||||||
"phone": "^3.1.58",
|
"phone": "^3.1.58",
|
||||||
|
"query-string": "^9.1.2",
|
||||||
"recursive-diff": "^1.0.9",
|
"recursive-diff": "^1.0.9",
|
||||||
"redis": "^4.7.0",
|
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"skia-canvas": "^2.0.2",
|
"skia-canvas": "^2.0.2",
|
||||||
"soap": "^1.1.10",
|
"soap": "^1.1.10",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"socket.io-adapter": "^2.5.5",
|
"socket.io-adapter": "^2.5.5",
|
||||||
"ssh2-sftp-client": "^11.0.0",
|
"ssh2-sftp-client": "^11.0.0",
|
||||||
"twilio": "^5.5.2",
|
"twilio": "^5.6.0",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
"winston-cloudwatch": "^6.3.0",
|
"winston-cloudwatch": "^6.3.0",
|
||||||
@@ -78,16 +72,14 @@
|
|||||||
"xmlbuilder2": "^3.1.1"
|
"xmlbuilder2": "^3.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.24.0",
|
"@eslint/js": "^9.26.0",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
"eslint": "^9.26.0",
|
||||||
"eslint": "^9.24.0",
|
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"mock-require": "^3.0.3",
|
"mock-require": "^3.0.3",
|
||||||
"p-limit": "^3.1.0",
|
"p-limit": "^3.1.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"source-map-explorer": "^2.5.2",
|
|
||||||
"supertest": "^7.1.0",
|
"supertest": "^7.1.0",
|
||||||
"vitest": "^3.1.1"
|
"vitest": "^3.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ query FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID($mssid: String!, $phone: String!) {
|
|||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
// Unused
|
||||||
exports.GET_JOB_BY_RO_NUMBER = `
|
exports.GET_JOB_BY_RO_NUMBER = `
|
||||||
query GET_JOB_BY_RO_NUMBER($ro_number: String!) {
|
query GET_JOB_BY_RO_NUMBER($ro_number: String!) {
|
||||||
jobs(where:{ro_number:{_eq:$ro_number}}) {
|
jobs(where:{ro_number:{_eq:$ro_number}}) {
|
||||||
@@ -1773,6 +1774,7 @@ exports.QUERY_JOB_COSTING_DETAILS_MULTI = ` query QUERY_JOB_COSTING_DETAILS_MULT
|
|||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
// Exists in Commented out Query
|
||||||
exports.INSERT_IOEVENT = ` mutation INSERT_IOEVENT($event: ioevents_insert_input!) {
|
exports.INSERT_IOEVENT = ` mutation INSERT_IOEVENT($event: ioevents_insert_input!) {
|
||||||
insert_ioevents_one(object: $event) {
|
insert_ioevents_one(object: $event) {
|
||||||
id
|
id
|
||||||
@@ -2802,6 +2804,7 @@ exports.GET_BODYSHOP_BY_ID = `
|
|||||||
imexshopid
|
imexshopid
|
||||||
intellipay_config
|
intellipay_config
|
||||||
state
|
state
|
||||||
|
notification_followers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -2928,3 +2931,40 @@ exports.INSERT_NEW_DOCUMENT = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports.INSERT_JOB_WATCHERS = `
|
||||||
|
mutation INSERT_JOB_WATCHERS($watchers: [job_watchers_insert_input!]!) {
|
||||||
|
insert_job_watchers(objects: $watchers, on_conflict: { constraint: job_watchers_pkey, update_columns: [] }) {
|
||||||
|
affected_rows
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports.GET_NOTIFICATION_WATCHERS = `
|
||||||
|
query GET_NOTIFICATION_WATCHERS($shopId: uuid!, $employeeIds: [uuid!]!) {
|
||||||
|
associations(where: {
|
||||||
|
_and: [
|
||||||
|
{ shopid: { _eq: $shopId } },
|
||||||
|
{ active: { _eq: true } },
|
||||||
|
{ notifications_autoadd: { _eq: true } }
|
||||||
|
]
|
||||||
|
}) {
|
||||||
|
id
|
||||||
|
useremail
|
||||||
|
}
|
||||||
|
employees(where: { id: { _in: $employeeIds }, shopid: { _eq: $shopId }, active: { _eq: true } }) {
|
||||||
|
user_email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports.GET_JOB_WATCHERS_MINIMAL = `
|
||||||
|
query GET_JOB_WATCHERS_MINIMAL($jobid: uuid!) {
|
||||||
|
job_watchers(where: { jobid: { _eq: $jobid } }) {
|
||||||
|
user_email
|
||||||
|
user {
|
||||||
|
authid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
127
server/notifications/autoAddWatchers.js
Normal file
127
server/notifications/autoAddWatchers.js
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
/**
|
||||||
|
* @module autoAddWatchers
|
||||||
|
* @description
|
||||||
|
* This module handles automatically adding watchers to new jobs based on the notifications_autoadd
|
||||||
|
* boolean field in the associations table and the notification_followers JSON field in the bodyshops table.
|
||||||
|
* It ensures users are not added twice and logs the process.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { client: gqlClient } = require("../graphql-client/graphql-client");
|
||||||
|
const { isEmpty } = require("lodash");
|
||||||
|
const {
|
||||||
|
GET_JOB_WATCHERS_MINIMAL,
|
||||||
|
GET_NOTIFICATION_WATCHERS,
|
||||||
|
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";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds watchers to a new job based on notifications_autoadd and notification_followers.
|
||||||
|
*
|
||||||
|
* @param {Object} req - The request object containing event data and logger.
|
||||||
|
* @returns {Promise<void>} Resolves when watchers are added or if no action is needed.
|
||||||
|
* @throws {Error} If critical data (e.g., jobId, shopId) is missing.
|
||||||
|
*/
|
||||||
|
const autoAddWatchers = async (req) => {
|
||||||
|
const { event, trigger } = req.body;
|
||||||
|
const {
|
||||||
|
logger,
|
||||||
|
sessionUtils: { getBodyshopFromRedis }
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
// Validate that this is an INSERT event, bail
|
||||||
|
if (trigger?.name !== "notifications_jobs_autoadd" || event.op !== "INSERT" || event.data.old) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jobId = event?.data?.new?.id;
|
||||||
|
const shopId = event?.data?.new?.shopid;
|
||||||
|
const roNumber = event?.data?.new?.ro_number || "unknown";
|
||||||
|
|
||||||
|
if (!jobId || !shopId) {
|
||||||
|
throw new Error(`Missing jobId (${jobId}) or shopId (${shopId}) for auto-add watchers`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasuraUserRole = event?.session_variables?.["x-hasura-role"];
|
||||||
|
const hasuraUserId = event?.session_variables?.["x-hasura-user-id"];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch bodyshop data from Redis
|
||||||
|
const bodyshopData = await getBodyshopFromRedis(shopId);
|
||||||
|
const notificationFollowers = bodyshopData?.notification_followers || [];
|
||||||
|
|
||||||
|
// Execute queries in parallel
|
||||||
|
const [notificationData, existingWatchersData] = await Promise.all([
|
||||||
|
gqlClient.request(GET_NOTIFICATION_WATCHERS, {
|
||||||
|
shopId,
|
||||||
|
employeeIds: notificationFollowers.filter((id) => id)
|
||||||
|
}),
|
||||||
|
gqlClient.request(GET_JOB_WATCHERS_MINIMAL, { jobid: jobId })
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Get users with notifications_autoadd: true
|
||||||
|
const autoAddUsers =
|
||||||
|
notificationData?.associations?.map((assoc) => ({
|
||||||
|
email: assoc.useremail,
|
||||||
|
associationId: assoc.id
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
// Get users from notification_followers
|
||||||
|
const followerEmails =
|
||||||
|
notificationData?.employees
|
||||||
|
?.filter((e) => e.user_email)
|
||||||
|
?.map((e) => ({
|
||||||
|
email: e.user_email,
|
||||||
|
associationId: null
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
// Combine and deduplicate emails (use email as the unique key)
|
||||||
|
const usersToAdd = [...autoAddUsers, ...followerEmails].reduce((acc, user) => {
|
||||||
|
if (!acc.some((u) => u.email === user.email)) {
|
||||||
|
acc.push(user);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (isEmpty(usersToAdd)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check existing watchers to avoid duplicates
|
||||||
|
const existingWatcherEmails = existingWatchersData?.job_watchers?.map((w) => w.user_email) || [];
|
||||||
|
|
||||||
|
// Filter out already existing watchers and optionally the user who created the job
|
||||||
|
const newWatchers = usersToAdd
|
||||||
|
.filter((user) => !existingWatcherEmails.includes(user.email))
|
||||||
|
.filter((user) => {
|
||||||
|
if (FILTER_SELF_FROM_WATCHERS && hasuraUserRole === "user") {
|
||||||
|
const userData = existingWatchersData?.job_watchers?.find((w) => w.user?.authid === hasuraUserId);
|
||||||
|
return userData ? user.email !== userData.user_email : true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((user) => ({
|
||||||
|
jobid: jobId,
|
||||||
|
user_email: user.email
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (isEmpty(newWatchers)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert new watchers
|
||||||
|
await gqlClient.request(INSERT_JOB_WATCHERS, { watchers: newWatchers });
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("Error adding auto-add watchers", "error", "notifications", null, {
|
||||||
|
message: error?.message,
|
||||||
|
stack: error?.stack,
|
||||||
|
jobId,
|
||||||
|
roNumber
|
||||||
|
});
|
||||||
|
throw error; // Re-throw to ensure the error is logged in the handler
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { autoAddWatchers };
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const scenarioParser = require("./scenarioParser");
|
const scenarioParser = require("./scenarioParser");
|
||||||
|
const { autoAddWatchers } = require("./autoAddWatchers"); // New module
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes a notification event by invoking the scenario parser.
|
* Processes a notification event by invoking the scenario parser.
|
||||||
@@ -185,6 +186,27 @@ const handlePartsDispatchChange = (req, res) => res.status(200).json({ message:
|
|||||||
*/
|
*/
|
||||||
const handlePartsOrderChange = (req, res) => res.status(200).json({ message: "Parts Order change handled." });
|
const handlePartsOrderChange = (req, res) => res.status(200).json({ message: "Parts Order change handled." });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle auto-add watchers for new jobs.
|
||||||
|
*
|
||||||
|
* @param {Object} req - Express request object.
|
||||||
|
* @param {Object} res - Express response object.
|
||||||
|
* @returns {Promise<Object>} JSON response with a success message.
|
||||||
|
*/
|
||||||
|
const handleAutoAddWatchers = async (req, res) => {
|
||||||
|
const { logger } = req;
|
||||||
|
|
||||||
|
// Call autoAddWatchers but don't await it; log any error that occurs.
|
||||||
|
autoAddWatchers(req).catch((error) => {
|
||||||
|
logger.log("auto-add-watchers-error", "error", "notifications", null, {
|
||||||
|
message: error?.message,
|
||||||
|
stack: error?.stack
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({ message: "Auto-Add Watchers Event Handled." });
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
handleJobsChange,
|
handleJobsChange,
|
||||||
handleBillsChange,
|
handleBillsChange,
|
||||||
@@ -195,5 +217,6 @@ module.exports = {
|
|||||||
handlePartsOrderChange,
|
handlePartsOrderChange,
|
||||||
handlePaymentsChange,
|
handlePaymentsChange,
|
||||||
handleTasksChange,
|
handleTasksChange,
|
||||||
handleTimeTicketsChange
|
handleTimeTicketsChange,
|
||||||
|
handleAutoAddWatchers
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
const path = require("path");
|
|
||||||
require("dotenv").config({
|
|
||||||
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
|
||||||
});
|
|
||||||
const logger = require("../utils/logger");
|
|
||||||
//const inlineCssTool = require("inline-css");
|
|
||||||
const juice = require("juice");
|
const juice = require("juice");
|
||||||
|
|
||||||
exports.inlinecss = async (req, res) => {
|
exports.inlineCSS = async (req, res) => {
|
||||||
//Perform request validation
|
const { logger } = req;
|
||||||
|
const { html } = req.body;
|
||||||
|
|
||||||
logger.log("email-inline-css", "DEBUG", req.user.email, null, null);
|
logger.log("email-inline-css", "DEBUG", req.user.email, null, null);
|
||||||
|
|
||||||
const { html, url } = req.body;
|
|
||||||
try {
|
try {
|
||||||
const inlinedHtml = juice(html, {
|
const inlinedHtml = juice(html, {
|
||||||
applyAttributesTableElements: false,
|
applyAttributesTableElements: false,
|
||||||
@@ -24,15 +19,4 @@ exports.inlinecss = async (req, res) => {
|
|||||||
});
|
});
|
||||||
res.send(error.message);
|
res.send(error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// inlineCssTool(html, { url: url })
|
|
||||||
// .then((inlinedHtml) => {
|
|
||||||
// res.send(inlinedHtml);
|
|
||||||
// })
|
|
||||||
// .catch((error) => {
|
|
||||||
// logger.log("email-inline-css-error", "ERROR", req.user.email, null, {
|
|
||||||
// error
|
|
||||||
// });
|
|
||||||
|
|
||||||
// });
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ const {
|
|||||||
handleNotesChange,
|
handleNotesChange,
|
||||||
handlePaymentsChange,
|
handlePaymentsChange,
|
||||||
handleDocumentsChange,
|
handleDocumentsChange,
|
||||||
handleJobLinesChange
|
handleJobLinesChange,
|
||||||
|
handleAutoAddWatchers
|
||||||
} = require("../notifications/eventHandlers");
|
} = require("../notifications/eventHandlers");
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -33,5 +34,6 @@ router.post("/events/handleNotesChange", eventAuthorizationMiddleware, handleNot
|
|||||||
router.post("/events/handlePaymentsChange", eventAuthorizationMiddleware, handlePaymentsChange);
|
router.post("/events/handlePaymentsChange", eventAuthorizationMiddleware, handlePaymentsChange);
|
||||||
router.post("/events/handleDocumentsChange", eventAuthorizationMiddleware, handleDocumentsChange);
|
router.post("/events/handleDocumentsChange", eventAuthorizationMiddleware, handleDocumentsChange);
|
||||||
router.post("/events/handleJobLinesChange", eventAuthorizationMiddleware, handleJobLinesChange);
|
router.post("/events/handleJobLinesChange", eventAuthorizationMiddleware, handleJobLinesChange);
|
||||||
|
router.post("/events/handleAutoAdd", eventAuthorizationMiddleware, handleAutoAddWatchers);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { inlinecss } = require("../render/inlinecss");
|
const { inlineCSS } = require("../render/inlinecss");
|
||||||
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
||||||
const { canvas } = require("../render/canvas-handler");
|
const { canvas } = require("../render/canvas-handler");
|
||||||
const validateCanvasInputMiddleware = require("../middleware/validateCanvasInputMiddleware");
|
const validateCanvasInputMiddleware = require("../middleware/validateCanvasInputMiddleware");
|
||||||
|
|
||||||
// Define the route for inline CSS rendering
|
// Define the route for inline CSS rendering
|
||||||
router.post("/inlinecss", validateFirebaseIdTokenMiddleware, inlinecss);
|
router.post("/inlinecss", validateFirebaseIdTokenMiddleware, inlineCSS);
|
||||||
router.post("/canvas-skia", validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware, canvas);
|
router.post("/canvas-skia", validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware, canvas);
|
||||||
router.post("/canvas", validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware, canvas);
|
router.post("/canvas", validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware, canvas);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user