Compare commits
3 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c0eab9366 | ||
|
|
b831d8ca8a | ||
|
|
69da6bccf7 |
@@ -48,8 +48,6 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const scenarioNotificationsOn = client?.getTreatment("Realtime_Notifications_UI") === "on";
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!navigator.onLine) {
|
if (!navigator.onLine) {
|
||||||
setOnline(false);
|
setOnline(false);
|
||||||
@@ -203,12 +201,7 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
|||||||
path="/manage/*"
|
path="/manage/*"
|
||||||
element={
|
element={
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<SocketProvider
|
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}>
|
||||||
bodyshop={bodyshop}
|
|
||||||
navigate={navigate}
|
|
||||||
currentUser={currentUser}
|
|
||||||
scenarioNotificationsOn={scenarioNotificationsOn}
|
|
||||||
>
|
|
||||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||||
</SocketProvider>
|
</SocketProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
@@ -220,12 +213,7 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
|||||||
path="/tech/*"
|
path="/tech/*"
|
||||||
element={
|
element={
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<SocketProvider
|
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}>
|
||||||
bodyshop={bodyshop}
|
|
||||||
navigate={navigate}
|
|
||||||
currentUser={currentUser}
|
|
||||||
scenarioNotificationsOn={scenarioNotificationsOn}
|
|
||||||
>
|
|
||||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||||
</SocketProvider>
|
</SocketProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
import { Badge, Layout, Menu, Spin } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { useQuery } from "@apollo/client";
|
||||||
|
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||||
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
|
import NotificationCenterContainer from "../notification-center/notification-center.container.jsx";
|
||||||
|
import LockWrapper from "../lock-wrapper/lock-wrapper.component";
|
||||||
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
import {
|
import {
|
||||||
BankFilled,
|
BankFilled,
|
||||||
BarChartOutlined,
|
BarChartOutlined,
|
||||||
@@ -26,31 +38,19 @@ import {
|
|||||||
UnorderedListOutlined,
|
UnorderedListOutlined,
|
||||||
UserOutlined
|
UserOutlined
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { useQuery } from "@apollo/client";
|
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
|
||||||
import { Badge, Layout, Menu, Spin } from "antd";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { BsKanban } from "react-icons/bs";
|
import { BsKanban } from "react-icons/bs";
|
||||||
import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar, FaTasks } from "react-icons/fa";
|
import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar, FaTasks } from "react-icons/fa";
|
||||||
import { FiLogOut } from "react-icons/fi";
|
import { FiLogOut } from "react-icons/fi";
|
||||||
import { GiPayMoney, GiPlayerTime, GiSettingsKnobs } from "react-icons/gi";
|
import { GiPayMoney, GiPlayerTime, GiSettingsKnobs } from "react-icons/gi";
|
||||||
import { IoBusinessOutline } from "react-icons/io5";
|
import { IoBusinessOutline } from "react-icons/io5";
|
||||||
import { RiSurveyLine } from "react-icons/ri";
|
import { RiSurveyLine } from "react-icons/ri";
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
|
||||||
import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js";
|
import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js";
|
||||||
import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors";
|
import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { signOutStart } from "../../redux/user/user.actions";
|
import { signOutStart } from "../../redux/user/user.actions";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import day from "../../utils/day.js";
|
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
import day from "../../utils/day.js";
|
||||||
import LockWrapper from "../lock-wrapper/lock-wrapper.component";
|
|
||||||
import NotificationCenterContainer from "../notification-center/notification-center.container.jsx";
|
|
||||||
|
|
||||||
// Redux mappings
|
// Redux mappings
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -208,14 +208,25 @@ function Header({
|
|||||||
key: "allpayments",
|
key: "allpayments",
|
||||||
id: "header-accounting-allpayments",
|
id: "header-accounting-allpayments",
|
||||||
icon: <BankFilled />,
|
icon: <BankFilled />,
|
||||||
label: <Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
|
label: (
|
||||||
|
<Link to="/manage/payments">
|
||||||
|
<LockWrapper featureName="payments" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.allpayments")}
|
||||||
|
</LockWrapper>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "enterpayments",
|
key: "enterpayments",
|
||||||
id: "header-accounting-enterpayments",
|
id: "header-accounting-enterpayments",
|
||||||
icon: <FaCreditCard />,
|
icon: <FaCreditCard />,
|
||||||
label: t("menus.header.enterpayment"),
|
label: (
|
||||||
|
<LockWrapper featureName="payments" bodyshop={bodyshop}>
|
||||||
|
{t("menus.header.enterpayment")}
|
||||||
|
</LockWrapper>
|
||||||
|
),
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
|
HasFeatureAccess({ featureName: "payments", bodyshop }) &&
|
||||||
setPaymentContext({
|
setPaymentContext({
|
||||||
actions: {},
|
actions: {},
|
||||||
context: null
|
context: null
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
|||||||
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||||
import LockerWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
import LockerWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
||||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||||
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
|
|
||||||
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
||||||
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
||||||
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
|
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -775,14 +775,15 @@ export function JobsDetailHeaderActions({
|
|||||||
key: "enterpayments",
|
key: "enterpayments",
|
||||||
id: "job-actions-enterpayments",
|
id: "job-actions-enterpayments",
|
||||||
disabled: !job.converted,
|
disabled: !job.converted,
|
||||||
label: t("menus.header.enterpayment"),
|
label: <LockerWrapperComponent featureName="payments">{t("menus.header.enterpayment")}</LockerWrapperComponent>,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
logImEXEvent("job_header_enter_payment");
|
logImEXEvent("job_header_enter_payment");
|
||||||
|
|
||||||
setPaymentContext({
|
HasFeatureAccess({ featureName: "payments", bodyshop }) &&
|
||||||
actions: {},
|
setPaymentContext({
|
||||||
context: { jobid: job.id }
|
actions: {},
|
||||||
});
|
context: { jobid: job.id }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from "../../graphql/notifications.queries.js";
|
} from "../../graphql/notifications.queries.js";
|
||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
|
|
||||||
const SocketContext = createContext(null);
|
const SocketContext = createContext(null);
|
||||||
|
|
||||||
@@ -25,11 +26,10 @@ const INITIAL_NOTIFICATIONS = 10;
|
|||||||
* @param bodyshop
|
* @param bodyshop
|
||||||
* @param navigate
|
* @param navigate
|
||||||
* @param currentUser
|
* @param currentUser
|
||||||
* @param scenarioNotificationsOn
|
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNotificationsOn }) => {
|
const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
||||||
const socketRef = useRef(null);
|
const socketRef = useRef(null);
|
||||||
const [clientId, setClientId] = useState(null);
|
const [clientId, setClientId] = useState(null);
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
@@ -37,6 +37,14 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNot
|
|||||||
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const {
|
||||||
|
treatments: { Realtime_Notifications_UI }
|
||||||
|
} = useSplitTreatments({
|
||||||
|
attributes: {},
|
||||||
|
names: ["Realtime_Notifications_UI"],
|
||||||
|
splitKey: bodyshop?.imexshopid
|
||||||
|
});
|
||||||
|
|
||||||
const [markNotificationRead] = useMutation(MARK_NOTIFICATION_READ, {
|
const [markNotificationRead] = useMutation(MARK_NOTIFICATION_READ, {
|
||||||
update: (cache, { data: { update_notifications } }) => {
|
update: (cache, { data: { update_notifications } }) => {
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
@@ -209,7 +217,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNot
|
|||||||
|
|
||||||
const handleNotification = (data) => {
|
const handleNotification = (data) => {
|
||||||
// Scenario Notifications have been disabled, bail.
|
// Scenario Notifications have been disabled, bail.
|
||||||
if (!scenarioNotificationsOn) {
|
if (Realtime_Notifications_UI?.treatment !== "on") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,7 +337,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNot
|
|||||||
|
|
||||||
const handleSyncNotificationRead = ({ notificationId, timestamp }) => {
|
const handleSyncNotificationRead = ({ notificationId, timestamp }) => {
|
||||||
// Scenario Notifications have been disabled, bail.
|
// Scenario Notifications have been disabled, bail.
|
||||||
if (!scenarioNotificationsOn) {
|
if (Realtime_Notifications_UI?.treatment !== "on") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,7 +379,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNot
|
|||||||
|
|
||||||
const handleSyncAllNotificationsRead = ({ timestamp }) => {
|
const handleSyncAllNotificationsRead = ({ timestamp }) => {
|
||||||
// Scenario Notifications have been disabled, bail.
|
// Scenario Notifications have been disabled, bail.
|
||||||
if (!scenarioNotificationsOn) {
|
if (Realtime_Notifications_UI?.treatment !== "on") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,7 +470,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNot
|
|||||||
markAllNotificationsRead,
|
markAllNotificationsRead,
|
||||||
navigate,
|
navigate,
|
||||||
currentUser,
|
currentUser,
|
||||||
scenarioNotificationsOn,
|
Realtime_Notifications_UI,
|
||||||
t
|
t
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -474,7 +482,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNot
|
|||||||
isConnected,
|
isConnected,
|
||||||
markNotificationRead,
|
markNotificationRead,
|
||||||
markAllNotificationsRead,
|
markAllNotificationsRead,
|
||||||
scenarioNotificationsOn
|
scenarioNotificationsOn: Realtime_Notifications_UI?.treatment === "on"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
@@ -10,17 +10,23 @@ import PaymentsListPaginated from "../../components/payments-list-paginated/paym
|
|||||||
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||||
import { QUERY_ALL_PAYMENTS_PAGINATED } from "../../graphql/payments.queries";
|
import { QUERY_ALL_PAYMENTS_PAGINATED } from "../../graphql/payments.queries";
|
||||||
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { pageLimit } from "../../utils/config";
|
import { pageLimit } from "../../utils/config";
|
||||||
|
import FeatureWrapperComponent from "../../components/feature-wrapper/feature-wrapper.component";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
|
import UpsellComponent, { upsellEnum } from "../../components/upsell/upsell.component";
|
||||||
|
import { Card } from "antd";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({});
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||||
setSelectedHeader: (key) => dispatch(setSelectedHeader(key))
|
setSelectedHeader: (key) => dispatch(setSelectedHeader(key))
|
||||||
});
|
});
|
||||||
|
|
||||||
export function AllJobs({ setBreadcrumbs, setSelectedHeader }) {
|
export function AllJobs({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
|
||||||
const searchParams = queryString.parse(useLocation().search);
|
const searchParams = queryString.parse(useLocation().search);
|
||||||
const { page, sortcolumn, sortorder, searchObj } = searchParams;
|
const { page, sortcolumn, sortorder, searchObj } = searchParams;
|
||||||
|
|
||||||
@@ -54,15 +60,25 @@ export function AllJobs({ setBreadcrumbs, setSelectedHeader }) {
|
|||||||
|
|
||||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
return (
|
return (
|
||||||
<RbacWrapper action="payments:list">
|
<FeatureWrapperComponent
|
||||||
<PaymentsListPaginated
|
featureName="payments"
|
||||||
refetch={refetch}
|
noauth={
|
||||||
loading={loading}
|
<Card>
|
||||||
searchParams={searchParams}
|
<UpsellComponent upsell={upsellEnum().payments.general} />
|
||||||
total={data ? data.payments_aggregate.aggregate.count : 0}
|
</Card>
|
||||||
payments={data ? data.payments : []}
|
}
|
||||||
/>
|
z
|
||||||
</RbacWrapper>
|
>
|
||||||
|
<RbacWrapper action="payments:list">
|
||||||
|
<PaymentsListPaginated
|
||||||
|
refetch={refetch}
|
||||||
|
loading={loading}
|
||||||
|
searchParams={searchParams}
|
||||||
|
total={data ? data.payments_aggregate.aggregate.count : 0}
|
||||||
|
payments={data ? data.payments : []}
|
||||||
|
/>
|
||||||
|
</RbacWrapper>
|
||||||
|
</FeatureWrapperComponent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP INDEX IF EXISTS "public"."notificiations_idx_jobs";
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
CREATE INDEX "notificiations_idx_jobs" on
|
||||||
|
"public"."notifications" using btree ("jobid");
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP INDEX IF EXISTS "public"."notifications_idx_associations";
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
CREATE INDEX "notifications_idx_associations" on
|
||||||
|
"public"."notifications" using btree ("associationid");
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- CREATE INDEX idx_notifications_created_at_not_read ON notifications(created_at desc, read) where read is null;
|
||||||
1
hasura/migrations/1741904614090_run_sql_migration/up.sql
Normal file
1
hasura/migrations/1741904614090_run_sql_migration/up.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
CREATE INDEX idx_notifications_created_at_not_read ON notifications(created_at desc, read) where read is null;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- CREATE INDEX idx_notifications_associations_not_read ON notifications(associationid, read) where read is null;
|
||||||
1
hasura/migrations/1741904805838_run_sql_migration/up.sql
Normal file
1
hasura/migrations/1741904805838_run_sql_migration/up.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
CREATE INDEX idx_notifications_associations_not_read ON notifications(associationid, read) where read is null;
|
||||||
@@ -20,6 +20,11 @@ const defaultFooter = () => {
|
|||||||
|
|
||||||
const now = () => moment().format("MM/DD/YYYY @ hh:mm a");
|
const now = () => moment().format("MM/DD/YYYY @ hh:mm a");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the email template
|
||||||
|
* @param strings
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
const generateEmailTemplate = (strings) => {
|
const generateEmailTemplate = (strings) => {
|
||||||
return (
|
return (
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const { InstanceEndpoints } = require("../../utils/instanceMgr");
|
|||||||
const { registerCleanupTask } = require("../../utils/cleanupManager");
|
const { registerCleanupTask } = require("../../utils/cleanupManager");
|
||||||
const getBullMQPrefix = require("../../utils/getBullMQPrefix");
|
const getBullMQPrefix = require("../../utils/getBullMQPrefix");
|
||||||
const devDebugLogger = require("../../utils/devDebugLogger");
|
const devDebugLogger = require("../../utils/devDebugLogger");
|
||||||
|
const moment = require("moment-timezone");
|
||||||
|
|
||||||
const EMAIL_CONSOLIDATION_DELAY_IN_MINS = (() => {
|
const EMAIL_CONSOLIDATION_DELAY_IN_MINS = (() => {
|
||||||
const envValue = process.env?.EMAIL_CONSOLIDATION_DELAY_IN_MINS;
|
const envValue = process.env?.EMAIL_CONSOLIDATION_DELAY_IN_MINS;
|
||||||
@@ -59,7 +60,7 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
|||||||
emailAddWorker = new Worker(
|
emailAddWorker = new Worker(
|
||||||
"emailAdd",
|
"emailAdd",
|
||||||
async (job) => {
|
async (job) => {
|
||||||
const { jobId, jobRoNumber, bodyShopName, body, recipients } = job.data;
|
const { jobId, jobRoNumber, bodyShopName, bodyShopTimezone, body, recipients } = job.data;
|
||||||
devDebugLogger(`Adding email notifications for jobId ${jobId}`);
|
devDebugLogger(`Adding email notifications for jobId ${jobId}`);
|
||||||
|
|
||||||
const redisKeyPrefix = `email:${devKey}:notifications:${jobId}`;
|
const redisKeyPrefix = `email:${devKey}:notifications:${jobId}`;
|
||||||
@@ -72,6 +73,7 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
|||||||
const detailsKey = `email:${devKey}:recipientDetails:${jobId}:${user}`;
|
const detailsKey = `email:${devKey}:recipientDetails:${jobId}:${user}`;
|
||||||
await pubClient.hsetnx(detailsKey, "firstName", firstName || "");
|
await pubClient.hsetnx(detailsKey, "firstName", firstName || "");
|
||||||
await pubClient.hsetnx(detailsKey, "lastName", lastName || "");
|
await pubClient.hsetnx(detailsKey, "lastName", lastName || "");
|
||||||
|
await pubClient.hsetnx(detailsKey, "bodyShopTimezone", bodyShopTimezone);
|
||||||
await pubClient.expire(detailsKey, NOTIFICATION_EXPIRATION / 1000);
|
await pubClient.expire(detailsKey, NOTIFICATION_EXPIRATION / 1000);
|
||||||
await pubClient.sadd(`email:${devKey}:recipients:${jobId}`, user);
|
await pubClient.sadd(`email:${devKey}:recipients:${jobId}`, user);
|
||||||
devDebugLogger(`Stored message for ${user} under ${userKey}: ${body}`);
|
devDebugLogger(`Stored message for ${user} under ${userKey}: ${body}`);
|
||||||
@@ -82,7 +84,7 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
|||||||
if (flagSet) {
|
if (flagSet) {
|
||||||
await emailConsolidateQueue.add(
|
await emailConsolidateQueue.add(
|
||||||
"consolidate-emails",
|
"consolidate-emails",
|
||||||
{ jobId, jobRoNumber, bodyShopName },
|
{ jobId, jobRoNumber, bodyShopName, bodyShopTimezone },
|
||||||
{
|
{
|
||||||
jobId: `consolidate:${jobId}`,
|
jobId: `consolidate:${jobId}`,
|
||||||
delay: EMAIL_CONSOLIDATION_DELAY,
|
delay: EMAIL_CONSOLIDATION_DELAY,
|
||||||
@@ -125,9 +127,11 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
|||||||
const firstName = details.firstName || "User";
|
const firstName = details.firstName || "User";
|
||||||
const multipleUpdateString = messages.length > 1 ? "Updates" : "Update";
|
const multipleUpdateString = messages.length > 1 ? "Updates" : "Update";
|
||||||
const subject = `${multipleUpdateString} for job ${jobRoNumber || "N/A"} at ${bodyShopName}`;
|
const subject = `${multipleUpdateString} for job ${jobRoNumber || "N/A"} at ${bodyShopName}`;
|
||||||
|
const timezone = moment.tz.zone(details?.bodyShopTimezone) ? details.bodyShopTimezone : "UTC";
|
||||||
const emailBody = generateEmailTemplate({
|
const emailBody = generateEmailTemplate({
|
||||||
header: `${multipleUpdateString} for Job ${jobRoNumber || "N/A"}`,
|
header: `${multipleUpdateString} for Job ${jobRoNumber || "N/A"}`,
|
||||||
subHeader: `Dear ${firstName},`,
|
subHeader: `Dear ${firstName},`,
|
||||||
|
dateLine: moment().tz(timezone).format("MM/DD/YYYY hh:mm a"),
|
||||||
body: `
|
body: `
|
||||||
<p>There have been updates to job ${jobRoNumber || "N/A"} at ${bodyShopName}:</p><br/>
|
<p>There have been updates to job ${jobRoNumber || "N/A"} at ${bodyShopName}:</p><br/>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -226,10 +230,10 @@ const dispatchEmailsToQueue = async ({ emailsToDispatch, logger }) => {
|
|||||||
const emailAddQueue = getQueue();
|
const emailAddQueue = getQueue();
|
||||||
|
|
||||||
for (const email of emailsToDispatch) {
|
for (const email of emailsToDispatch) {
|
||||||
const { jobId, jobRoNumber, bodyShopName, body, recipients } = email;
|
const { jobId, jobRoNumber, bodyShopName, bodyShopTimezone, body, recipients } = email;
|
||||||
|
|
||||||
if (!jobId || !jobRoNumber || !bodyShopName || !body || !recipients.length) {
|
if (!jobId || !jobRoNumber || !bodyShopName || !body || !recipients.length) {
|
||||||
logger.logger.warn(
|
devDebugLogger(
|
||||||
`Skipping email dispatch for jobId ${jobId} due to missing data: ` +
|
`Skipping email dispatch for jobId ${jobId} due to missing data: ` +
|
||||||
`jobRoNumber=${jobRoNumber || "N/A"}, bodyShopName=${bodyShopName}, body=${body}, recipients=${recipients.length}`
|
`jobRoNumber=${jobRoNumber || "N/A"}, bodyShopName=${bodyShopName}, body=${body}, recipients=${recipients.length}`
|
||||||
);
|
);
|
||||||
@@ -238,7 +242,7 @@ const dispatchEmailsToQueue = async ({ emailsToDispatch, logger }) => {
|
|||||||
|
|
||||||
await emailAddQueue.add(
|
await emailAddQueue.add(
|
||||||
"add-email-notification",
|
"add-email-notification",
|
||||||
{ jobId, jobRoNumber, bodyShopName, body, recipients },
|
{ jobId, jobRoNumber, bodyShopName, bodyShopTimezone, body, recipients },
|
||||||
{ jobId: `${jobId}:${Date.now()}` }
|
{ jobId: `${jobId}:${Date.now()}` }
|
||||||
);
|
);
|
||||||
devDebugLogger(`Added email notification to queue for jobId ${jobId} with ${recipients.length} recipients`);
|
devDebugLogger(`Added email notification to queue for jobId ${jobId} with ${recipients.length} recipients`);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const buildNotification = (data, key, body, variables = {}) => {
|
|||||||
jobId: data.jobId,
|
jobId: data.jobId,
|
||||||
jobRoNumber: data.jobRoNumber,
|
jobRoNumber: data.jobRoNumber,
|
||||||
bodyShopName: data.bodyShopName,
|
bodyShopName: data.bodyShopName,
|
||||||
|
bodyShopTimezone: data.bodyShopTimezone,
|
||||||
body,
|
body,
|
||||||
recipients: []
|
recipients: []
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -248,8 +248,7 @@ async function OpenSearchSearchHandler(req, res) {
|
|||||||
"*ownr_fn^8",
|
"*ownr_fn^8",
|
||||||
"*ownr_co_nm^8",
|
"*ownr_co_nm^8",
|
||||||
"*ownr_ph1^8",
|
"*ownr_ph1^8",
|
||||||
"*ownr_ph2^8",
|
"*ownr_ph2^8"
|
||||||
"*comment^6"
|
|
||||||
// "*"
|
// "*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user