Compare commits
40 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f485951a4c | ||
|
|
8bb86b9caa | ||
|
|
4c6d28f612 | ||
|
|
38119f7f1f | ||
|
|
869fe78d8e | ||
|
|
4a9b0cae69 | ||
|
|
de3f1972a6 | ||
|
|
02a9274f98 | ||
|
|
2c0eab9366 | ||
|
|
b831d8ca8a | ||
|
|
87a57e057d | ||
|
|
69da6bccf7 | ||
|
|
f2e399f0df | ||
|
|
9a1f0e1e42 | ||
|
|
0675f84386 | ||
|
|
6994e44bd3 | ||
|
|
0d6d8e9d7c | ||
|
|
f7c01d5b35 | ||
|
|
e3d7ebd7d8 | ||
|
|
acea8d2fee | ||
|
|
5f0b63a192 | ||
|
|
1d0b4386d1 | ||
|
|
a36db7cee7 | ||
|
|
7a5ac739ab | ||
|
|
e2297be0af | ||
|
|
a3c0e25407 | ||
|
|
73c4983342 | ||
|
|
166e1e4030 | ||
|
|
a6c863f67d | ||
|
|
5fa7377121 | ||
|
|
f21ba8e087 | ||
|
|
169b5265c3 | ||
|
|
d56d1f369c | ||
|
|
360a1954f4 | ||
|
|
72ee621303 | ||
|
|
478e5fb569 | ||
|
|
6b047418cc | ||
|
|
87db292e5d | ||
|
|
9ef8440e64 | ||
|
|
8ae3b28cb6 |
@@ -9,6 +9,6 @@ VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
|||||||
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
|
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
|
||||||
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
||||||
VITE_APP_AXIOS_BASE_API_URL=/api/
|
VITE_APP_AXIOS_BASE_API_URL=/api/
|
||||||
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
|
VITE_APP_REPORTS_SERVER_URL=https://reports.test.imex.online
|
||||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||||
VITE_APP_INSTANCE=IMEX
|
VITE_APP_INSTANCE=IMEX
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
|||||||
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BP1B7ZTYpn-KMt6nOxlld6aS8Skt3Q7ZLEqP0hAvGHxG4UojPYiXZ6kPlzZkUC5jH-EcWXomTLtmadAIxurfcHo'
|
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BP1B7ZTYpn-KMt6nOxlld6aS8Skt3Q7ZLEqP0hAvGHxG4UojPYiXZ6kPlzZkUC5jH-EcWXomTLtmadAIxurfcHo'
|
||||||
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
||||||
VITE_APP_AXIOS_BASE_API_URL=/api/
|
VITE_APP_AXIOS_BASE_API_URL=/api/
|
||||||
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
|
VITE_APP_REPORTS_SERVER_URL=https://reports.test.romeonline.io
|
||||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||||
VITE_APP_COUNTRY=USA
|
VITE_APP_COUNTRY=USA
|
||||||
VITE_APP_INSTANCE=ROME
|
VITE_APP_INSTANCE=ROME
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
|||||||
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BN2GcDPjipR5MTEosO5dT4CfQ3cmrdBIsI4juoOQrRijn_5aRiHlwj1mlq0W145mOusx6xynEKl_tvYJhpCc9lo'
|
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BN2GcDPjipR5MTEosO5dT4CfQ3cmrdBIsI4juoOQrRijn_5aRiHlwj1mlq0W145mOusx6xynEKl_tvYJhpCc9lo'
|
||||||
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
||||||
VITE_APP_AXIOS_BASE_API_URL=https://api.test.imex.online/
|
VITE_APP_AXIOS_BASE_API_URL=https://api.test.imex.online/
|
||||||
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
|
VITE_APP_REPORTS_SERVER_URL=https://reports.test.imex.online
|
||||||
VITE_APP_IS_TEST=true
|
VITE_APP_IS_TEST=true
|
||||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||||
VITE_APP_INSTANCE=IMEX
|
VITE_APP_INSTANCE=IMEX
|
||||||
|
|||||||
@@ -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,15 +1,3 @@
|
|||||||
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,
|
||||||
@@ -38,19 +26,31 @@ 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 InstanceRenderManager from "../../utils/instanceRenderMgr";
|
|
||||||
import day from "../../utils/day.js";
|
import day from "../../utils/day.js";
|
||||||
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
|
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({
|
||||||
@@ -213,27 +213,12 @@ function Header({
|
|||||||
{
|
{
|
||||||
key: "enterpayments",
|
key: "enterpayments",
|
||||||
id: "header-accounting-enterpayments",
|
id: "header-accounting-enterpayments",
|
||||||
icon: <Icon component={FaCreditCard} />,
|
icon: <FaCreditCard />,
|
||||||
label: t("menus.header.enterpayment"),
|
label: t("menus.header.enterpayment"),
|
||||||
onClick: () => {
|
onClick: () =>
|
||||||
setPaymentContext({
|
setPaymentContext({
|
||||||
actions: {},
|
actions: {},
|
||||||
context: null
|
context: null
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (ImEXPay.treatment === "on") {
|
|
||||||
accountingChildren.push({
|
|
||||||
key: "entercardpayments",
|
|
||||||
id: "header-accounting-entercardpayments",
|
|
||||||
icon: <Icon component={FaCreditCard} />,
|
|
||||||
label: t("menus.header.entercardpayment"),
|
|
||||||
onClick: () => {
|
|
||||||
setCardPaymentContext({
|
|
||||||
actions: {},
|
|
||||||
context: null
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
...(ImEXPay.treatment === "on"
|
...(ImEXPay.treatment === "on"
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ 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";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { connect } from "react-redux";
|
|||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
import CABCpvrtCalculator from "../ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component";
|
import CABCpvrtCalculator from "../ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component";
|
||||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
import JobsDetailRatesChangeButton from "../jobs-detail-rates-change-button/jobs-detail-rates-change-button.component";
|
import JobsDetailRatesChangeButton from "../jobs-detail-rates-change-button/jobs-detail-rates-change-button.component";
|
||||||
@@ -14,9 +15,8 @@ import JobsDetailRatesLabor from "./jobs-detail-rates.labor.component";
|
|||||||
import JobsDetailRatesMaterials from "./jobs-detail-rates.materials.component";
|
import JobsDetailRatesMaterials from "./jobs-detail-rates.materials.component";
|
||||||
import JobsDetailRatesOther from "./jobs-detail-rates.other.component";
|
import JobsDetailRatesOther from "./jobs-detail-rates.other.component";
|
||||||
import JobsDetailRatesParts from "./jobs-detail-rates.parts.component";
|
import JobsDetailRatesParts from "./jobs-detail-rates.parts.component";
|
||||||
import JobsDetailRatesTaxes from "./jobs-detail-rates.taxes.component";
|
|
||||||
import JobsDetailRatesProfileOVerride from "./jobs-detail-rates.profile-override.component";
|
import JobsDetailRatesProfileOVerride from "./jobs-detail-rates.profile-override.component";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
import JobsDetailRatesTaxes from "./jobs-detail-rates.taxes.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
@@ -66,14 +66,48 @@ export function JobsDetailRates({ jobRO, form, job, bodyshop }) {
|
|||||||
</Space>
|
</Space>
|
||||||
)}
|
)}
|
||||||
<Form.Item label={t("jobs.fields.auto_add_ats")} name="auto_add_ats" valuePropName="checked">
|
<Form.Item label={t("jobs.fields.auto_add_ats")} name="auto_add_ats" valuePropName="checked">
|
||||||
<Switch disabled={jobRO} />
|
<Switch
|
||||||
|
disabled={jobRO}
|
||||||
|
onChange={(checked) => {
|
||||||
|
if (checked) {
|
||||||
|
form.setFieldsValue({ flat_rate_ats: false });
|
||||||
|
form.setFieldsValue({ rate_ats: form.getFieldValue('rate_ats') || bodyshop.shoprates.rate_ats });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.auto_add_ats !== cur.auto_add_ats}>
|
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.auto_add_ats !== cur.auto_add_ats}>
|
||||||
{() => {
|
{() => {
|
||||||
if (form.getFieldValue("auto_add_ats"))
|
if (form.getFieldValue("auto_add_ats"))
|
||||||
return (
|
return (
|
||||||
<Form.Item label={t("jobs.fields.rate_ats")} name="rate_ats" initialValue={bodyshop.shoprates.rate_atp}>
|
<Form.Item label={t("jobs.fields.rate_ats")} name="rate_ats">
|
||||||
|
<CurrencyInput disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.flat_rate_ats")} name="flat_rate_ats" valuePropName="checked">
|
||||||
|
<Switch
|
||||||
|
disabled={jobRO}
|
||||||
|
onChange={(checked) => {
|
||||||
|
if (checked) {
|
||||||
|
form.setFieldsValue({ auto_add_ats: false });
|
||||||
|
form.setFieldsValue({ rate_ats_flat: form.getFieldValue('rate_ats_flat') || bodyshop.shoprates.rate_ats_flat });
|
||||||
|
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.flat_rate_ats !== cur.flat_rate_ats}>
|
||||||
|
{() => {
|
||||||
|
if (form.getFieldValue("flat_rate_ats"))
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.rate_ats_flat")}
|
||||||
|
name="rate_ats_flat"
|
||||||
|
>
|
||||||
<CurrencyInput disabled={jobRO} />
|
<CurrencyInput disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { DeleteFilled } from "@ant-design/icons";
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
import { Button, Form, Input } from "antd";
|
import { Button, Form, Input, Space } from "antd";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
@@ -10,326 +9,338 @@ export default function ShopInfoLaborRates({ form }) {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<Form.List name={["md_labor_rates"]}>
|
<LayoutFormRow header={t("bodyshop.labels.shoprates")}>
|
||||||
{(fields, { add, remove, move }) => {
|
<Form.Item label={t("jobs.fields.rate_ats")} name={["shoprates", "rate_ats"]}>
|
||||||
return (
|
<CurrencyInput min={0} />
|
||||||
<div>
|
</Form.Item>
|
||||||
{fields.map((field, index) => (
|
<Form.Item label={t("jobs.fields.rate_ats_flat")} name={["shoprates", "rate_ats_flat"]}>
|
||||||
<Form.Item key={field.key}>
|
<CurrencyInput min={0} />
|
||||||
<LayoutFormRow>
|
</Form.Item>
|
||||||
<Form.Item
|
</LayoutFormRow>
|
||||||
label={t("jobs.fields.labor_rate_desc")}
|
<LayoutFormRow header={t("bodyshop.labels.laborrates")}>
|
||||||
key={`${index}rate_label`}
|
<Form.List name={["md_labor_rates"]}>
|
||||||
name={[field.name, "rate_label"]}
|
{(fields, { add, remove, move }) => {
|
||||||
rules={[
|
return (
|
||||||
{
|
<div>
|
||||||
required: true
|
{fields.map((field, index) => (
|
||||||
//message: t("general.validation.required"),
|
<Form.Item key={field.key}>
|
||||||
}
|
<LayoutFormRow noDivider={index === 0}>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.labor_rate_desc")}
|
||||||
<Input />
|
key={`${index}rate_label`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_label"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_laa")}
|
{
|
||||||
key={`${index}rate_laa`}
|
required: true
|
||||||
name={[field.name, "rate_laa"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<Input />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_laa")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_laa`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_laa"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_lab")}
|
{
|
||||||
key={`${index}rate_lab`}
|
required: true
|
||||||
name={[field.name, "rate_lab"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_lab")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_lab`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_lab"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_lad")}
|
{
|
||||||
key={`${index}rate_lad`}
|
required: true
|
||||||
name={[field.name, "rate_lad"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_lad")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_lad`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_lad"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_lae")}
|
{
|
||||||
key={`${index}rate_lae`}
|
required: true
|
||||||
name={[field.name, "rate_lae"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_lae")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_lae`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_lae"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_laf")}
|
{
|
||||||
key={`${index}rate_laf`}
|
required: true
|
||||||
name={[field.name, "rate_laf"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_laf")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_laf`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_laf"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_lag")}
|
{
|
||||||
key={`${index}rate_lag`}
|
required: true
|
||||||
name={[field.name, "rate_lag"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_lag")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_lag`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_lag"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_lam")}
|
{
|
||||||
key={`${index}rate_lam`}
|
required: true
|
||||||
name={[field.name, "rate_lam"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_lam")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_lam`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_lam"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_lar")}
|
{
|
||||||
key={`${index}rate_lar`}
|
required: true
|
||||||
name={[field.name, "rate_lar"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_lar")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_lar`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_lar"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_las")}
|
{
|
||||||
key={`${index}rate_las`}
|
required: true
|
||||||
name={[field.name, "rate_las"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_las")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_las`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_las"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_la1")}
|
{
|
||||||
key={`${index}rate_la1`}
|
required: true
|
||||||
name={[field.name, "rate_la1"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_la1")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_la1`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_la1"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_la2")}
|
{
|
||||||
key={`${index}rate_la2`}
|
required: true
|
||||||
name={[field.name, "rate_la2"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_la2")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_la2`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_la2"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_la3")}
|
{
|
||||||
key={`${index}rate_la3`}
|
required: true
|
||||||
name={[field.name, "rate_la3"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_la3")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_la3`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_la3"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_la4")}
|
{
|
||||||
key={`${index}rate_la4`}
|
required: true
|
||||||
name={[field.name, "rate_la4"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_la4")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_la4`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_la4"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_mash")}
|
{
|
||||||
key={`${index}rate_mash`}
|
required: true
|
||||||
name={[field.name, "rate_mash"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_mash")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_mash`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_mash"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_mapa")}
|
{
|
||||||
key={`${index}rate_mapa`}
|
required: true
|
||||||
name={[field.name, "rate_mapa"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_mapa")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_mapa`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_mapa"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_ma2s")}
|
{
|
||||||
key={`${index}rate_ma2s`}
|
required: true
|
||||||
name={[field.name, "rate_ma2s"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_ma2s")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_ma2s`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_ma2s"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_ma3s")}
|
{
|
||||||
key={`${index}rate_ma3s`}
|
required: true
|
||||||
name={[field.name, "rate_ma3s"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_ma3s")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_ma3s`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_ma3s"]}
|
||||||
{
|
rules={[
|
||||||
// <Form.Item
|
{
|
||||||
// label={t("jobs.fields.rate_mabl")}
|
required: true
|
||||||
// key={`${index}rate_mabl`}
|
//message: t("general.validation.required"),
|
||||||
// name={[field.name, "rate_mabl"]}
|
}
|
||||||
// rules={[
|
]}
|
||||||
// {
|
>
|
||||||
// required: true,
|
<CurrencyInput min={0} />
|
||||||
// //message: t("general.validation.required"),
|
</Form.Item>
|
||||||
// },
|
{
|
||||||
// ]}
|
// <Form.Item
|
||||||
// >
|
// label={t("jobs.fields.rate_mabl")}
|
||||||
// <CurrencyInput min={0} />
|
// key={`${index}rate_mabl`}
|
||||||
// </Form.Item>
|
// name={[field.name, "rate_mabl"]}
|
||||||
// <Form.Item
|
// rules={[
|
||||||
// label={t("jobs.fields.rate_macs")}
|
// {
|
||||||
// key={`${index}rate_macs`}
|
// required: true,
|
||||||
// name={[field.name, "rate_macs"]}
|
// //message: t("general.validation.required"),
|
||||||
// rules={[
|
// },
|
||||||
// {
|
// ]}
|
||||||
// required: true,
|
// >
|
||||||
// //message: t("general.validation.required"),
|
// <CurrencyInput min={0} />
|
||||||
// },
|
// </Form.Item>
|
||||||
// ]}
|
// <Form.Item
|
||||||
// >
|
// label={t("jobs.fields.rate_macs")}
|
||||||
// <CurrencyInput min={0} />
|
// key={`${index}rate_macs`}
|
||||||
// </Form.Item>
|
// name={[field.name, "rate_macs"]}
|
||||||
}
|
// rules={[
|
||||||
<Form.Item
|
// {
|
||||||
label={t("jobs.fields.rate_matd")}
|
// required: true,
|
||||||
key={`${index}rate_matd`}
|
// //message: t("general.validation.required"),
|
||||||
name={[field.name, "rate_matd"]}
|
// },
|
||||||
rules={[
|
// ]}
|
||||||
{
|
// >
|
||||||
required: true
|
// <CurrencyInput min={0} />
|
||||||
//message: t("general.validation.required"),
|
// </Form.Item>
|
||||||
}
|
}
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_matd")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_matd`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_matd"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_mahw")}
|
{
|
||||||
key={`${index}rate_mahw`}
|
required: true
|
||||||
name={[field.name, "rate_mahw"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_mahw")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_mahw`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_mahw"]}
|
||||||
<DeleteFilled
|
rules={[
|
||||||
onClick={() => {
|
{
|
||||||
remove(field.name);
|
required: true
|
||||||
}}
|
//message: t("general.validation.required"),
|
||||||
/>
|
}
|
||||||
<FormListMoveArrows move={move} index={index} total={fields.rate_length} />
|
]}
|
||||||
</LayoutFormRow>
|
>
|
||||||
|
<CurrencyInput min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
<Space>
|
||||||
|
<DeleteFilled
|
||||||
|
onClick={() => {
|
||||||
|
remove(field.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormListMoveArrows move={move} index={index} total={fields.rate_length} />
|
||||||
|
</Space>
|
||||||
|
</LayoutFormRow>
|
||||||
|
</Form.Item>
|
||||||
|
))}
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
onClick={() => {
|
||||||
|
add();
|
||||||
|
}}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
>
|
||||||
|
{t("bodyshop.actions.newlaborrate")}
|
||||||
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
))}
|
</div>
|
||||||
<Form.Item>
|
);
|
||||||
<Button
|
}}
|
||||||
type="dashed"
|
</Form.List>
|
||||||
onClick={() => {
|
</LayoutFormRow>
|
||||||
add();
|
</>
|
||||||
}}
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
>
|
|
||||||
{t("bodyshop.actions.newlaborrate")}
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.List>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { Alert, Form, Switch } from "antd";
|
import { Alert, Form, Select, Switch } from "antd";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -16,17 +15,17 @@ const mapDispatchToProps = () => ({
|
|||||||
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoIntellipay);
|
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoIntellipay);
|
||||||
|
|
||||||
// noinspection JSUnusedLocalSymbols
|
// noinspection JSUnusedLocalSymbols
|
||||||
export function ShopInfoIntellipay({bodyshop, form}) {
|
export function ShopInfoIntellipay({ bodyshop, form }) {
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form.Item dependencies={[["intellipay_config", "enable_cash_discount"]]}>
|
<Form.Item dependencies={[["intellipay_config", "enable_cash_discount"]]}>
|
||||||
{() => {
|
{() => {
|
||||||
const {intellipay_config} = form.getFieldsValue();
|
const { intellipay_config } = form.getFieldsValue();
|
||||||
|
|
||||||
if (intellipay_config?.enable_cash_discount)
|
if (intellipay_config?.enable_cash_discount)
|
||||||
return <Alert message={t("bodyshop.labels.intellipay_cash_discount")}/>;
|
return <Alert message={t("bodyshop.labels.intellipay_cash_discount")} />;
|
||||||
}}
|
}}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
@@ -36,7 +35,93 @@ export function ShopInfoIntellipay({bodyshop, form}) {
|
|||||||
valuePropName="checked"
|
valuePropName="checked"
|
||||||
name={["intellipay_config", "enable_cash_discount"]}
|
name={["intellipay_config", "enable_cash_discount"]}
|
||||||
>
|
>
|
||||||
<Switch/>
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</LayoutFormRow>
|
||||||
|
<LayoutFormRow header={t("bodyshop.fields.intellipay_config.payment_type")}>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.intellipay_config.payment_map.visa")}
|
||||||
|
name={["intellipay_config", "payment_map", "visa"]}
|
||||||
|
>
|
||||||
|
<Select showSearch>
|
||||||
|
{bodyshop.md_payment_types.map((item, idx) => (
|
||||||
|
<Select.Option key={idx} value={item}>
|
||||||
|
{item}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.intellipay_config.payment_map.mast")}
|
||||||
|
name={["intellipay_config", "payment_map", "mast"]}
|
||||||
|
>
|
||||||
|
<Select showSearch>
|
||||||
|
{bodyshop.md_payment_types.map((item, idx) => (
|
||||||
|
<Select.Option key={idx} value={item}>
|
||||||
|
{item}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.intellipay_config.payment_map.amex")}
|
||||||
|
name={["intellipay_config", "payment_map", "amex"]}
|
||||||
|
>
|
||||||
|
<Select showSearch>
|
||||||
|
{bodyshop.md_payment_types.map((item, idx) => (
|
||||||
|
<Select.Option key={idx} value={item}>
|
||||||
|
{item}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.intellipay_config.payment_map.disc")}
|
||||||
|
name={["intellipay_config", "payment_map", "disc"]}
|
||||||
|
>
|
||||||
|
<Select showSearch>
|
||||||
|
{bodyshop.md_payment_types.map((item, idx) => (
|
||||||
|
<Select.Option key={idx} value={item}>
|
||||||
|
{item}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.intellipay_config.payment_map.dnrs")}
|
||||||
|
name={["intellipay_config", "payment_map", "dnrs"]}
|
||||||
|
>
|
||||||
|
<Select showSearch>
|
||||||
|
{bodyshop.md_payment_types.map((item, idx) => (
|
||||||
|
<Select.Option key={idx} value={item}>
|
||||||
|
{item}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.intellipay_config.payment_map.jcb")}
|
||||||
|
name={["intellipay_config", "payment_map", "jcb"]}
|
||||||
|
>
|
||||||
|
<Select showSearch>
|
||||||
|
{bodyshop.md_payment_types.map((item, idx) => (
|
||||||
|
<Select.Option key={idx} value={item}>
|
||||||
|
{item}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.intellipay_config.payment_map.intr")}
|
||||||
|
name={["intellipay_config", "payment_map", "intr"]}
|
||||||
|
>
|
||||||
|
<Select showSearch>
|
||||||
|
{bodyshop.md_payment_types.map((item, idx) => (
|
||||||
|
<Select.Option key={idx} value={item}>
|
||||||
|
{item}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -509,6 +509,7 @@ export const GET_JOB_BY_PK = gql`
|
|||||||
est_ct_ln
|
est_ct_ln
|
||||||
est_ea
|
est_ea
|
||||||
est_ph1
|
est_ph1
|
||||||
|
flat_rate_ats
|
||||||
federal_tax_rate
|
federal_tax_rate
|
||||||
id
|
id
|
||||||
inproduction
|
inproduction
|
||||||
@@ -649,6 +650,7 @@ export const GET_JOB_BY_PK = gql`
|
|||||||
policy_no
|
policy_no
|
||||||
production_vars
|
production_vars
|
||||||
rate_ats
|
rate_ats
|
||||||
|
rate_ats_flat
|
||||||
rate_la1
|
rate_la1
|
||||||
rate_la2
|
rate_la2
|
||||||
rate_la3
|
rate_la3
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,13 @@
|
|||||||
|
/** Notification Scenarios
|
||||||
|
* @description This file contains the scenarios for job notifications.
|
||||||
|
* @type {string[]}
|
||||||
|
*/
|
||||||
const notificationScenarios = [
|
const notificationScenarios = [
|
||||||
"job-assigned-to-me",
|
"job-assigned-to-me",
|
||||||
"bill-posted",
|
"bill-posted",
|
||||||
"critical-parts-status-changed",
|
"critical-parts-status-changed",
|
||||||
"part-marked-back-ordered",
|
"part-marked-back-ordered",
|
||||||
"new-note-added",
|
"new-note-added",
|
||||||
"supplement-imported",
|
|
||||||
"schedule-dates-changed",
|
"schedule-dates-changed",
|
||||||
"tasks-updated-created",
|
"tasks-updated-created",
|
||||||
"new-media-added-reassigned",
|
"new-media-added-reassigned",
|
||||||
@@ -14,6 +17,7 @@ const notificationScenarios = [
|
|||||||
"job-status-change",
|
"job-status-change",
|
||||||
"payment-collected-completed",
|
"payment-collected-completed",
|
||||||
"alternate-transport-changed"
|
"alternate-transport-changed"
|
||||||
|
// "supplement-imported", // Disabled for now
|
||||||
];
|
];
|
||||||
|
|
||||||
export { notificationScenarios };
|
export { notificationScenarios };
|
||||||
|
|||||||
@@ -3695,6 +3695,7 @@
|
|||||||
- est_st
|
- est_st
|
||||||
- est_zip
|
- est_zip
|
||||||
- federal_tax_rate
|
- federal_tax_rate
|
||||||
|
- flat_rate_ats
|
||||||
- g_bett_amt
|
- g_bett_amt
|
||||||
- id
|
- id
|
||||||
- inproduction
|
- inproduction
|
||||||
@@ -3789,6 +3790,7 @@
|
|||||||
- qb_multiple_payers
|
- qb_multiple_payers
|
||||||
- queued_for_parts
|
- queued_for_parts
|
||||||
- rate_ats
|
- rate_ats
|
||||||
|
- rate_ats_flat
|
||||||
- rate_la1
|
- rate_la1
|
||||||
- rate_la2
|
- rate_la2
|
||||||
- rate_la3
|
- rate_la3
|
||||||
@@ -3965,6 +3967,7 @@
|
|||||||
- est_st
|
- est_st
|
||||||
- est_zip
|
- est_zip
|
||||||
- federal_tax_rate
|
- federal_tax_rate
|
||||||
|
- flat_rate_ats
|
||||||
- g_bett_amt
|
- g_bett_amt
|
||||||
- id
|
- id
|
||||||
- inproduction
|
- inproduction
|
||||||
@@ -4060,6 +4063,7 @@
|
|||||||
- qb_multiple_payers
|
- qb_multiple_payers
|
||||||
- queued_for_parts
|
- queued_for_parts
|
||||||
- rate_ats
|
- rate_ats
|
||||||
|
- rate_ats_flat
|
||||||
- rate_la1
|
- rate_la1
|
||||||
- rate_la2
|
- rate_la2
|
||||||
- rate_la3
|
- rate_la3
|
||||||
@@ -4247,6 +4251,7 @@
|
|||||||
- est_st
|
- est_st
|
||||||
- est_zip
|
- est_zip
|
||||||
- federal_tax_rate
|
- federal_tax_rate
|
||||||
|
- flat_rate_ats
|
||||||
- g_bett_amt
|
- g_bett_amt
|
||||||
- id
|
- id
|
||||||
- inproduction
|
- inproduction
|
||||||
@@ -4342,6 +4347,7 @@
|
|||||||
- qb_multiple_payers
|
- qb_multiple_payers
|
||||||
- queued_for_parts
|
- queued_for_parts
|
||||||
- rate_ats
|
- rate_ats
|
||||||
|
- rate_ats_flat
|
||||||
- rate_la1
|
- rate_la1
|
||||||
- rate_la2
|
- rate_la2
|
||||||
- rate_la3
|
- rate_la3
|
||||||
@@ -5266,32 +5272,6 @@
|
|||||||
- active:
|
- active:
|
||||||
_eq: true
|
_eq: true
|
||||||
check: null
|
check: null
|
||||||
event_triggers:
|
|
||||||
- name: notifications_parts_dispatch
|
|
||||||
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: |-
|
|
||||||
{
|
|
||||||
"success": true
|
|
||||||
}
|
|
||||||
method: POST
|
|
||||||
query_params: {}
|
|
||||||
template_engine: Kriti
|
|
||||||
url: '{{$base_url}}/notifications/events/handlePartsDispatchChange'
|
|
||||||
version: 2
|
|
||||||
- table:
|
- table:
|
||||||
name: parts_dispatch_lines
|
name: parts_dispatch_lines
|
||||||
schema: public
|
schema: public
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."jobs" add column "flat_rate_ats" boolean
|
||||||
|
-- null default 'false';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."jobs" add column "flat_rate_ats" boolean
|
||||||
|
null default 'false';
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."jobs" add column "rate_ats_flat" numeric
|
||||||
|
-- null;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."jobs" add column "rate_ats_flat" numeric
|
||||||
|
null;
|
||||||
@@ -22,7 +22,7 @@ const cookieParser = require("cookie-parser");
|
|||||||
const { Server } = require("socket.io");
|
const { Server } = require("socket.io");
|
||||||
const { createAdapter } = require("@socket.io/redis-adapter");
|
const { createAdapter } = require("@socket.io/redis-adapter");
|
||||||
const { instrument } = require("@socket.io/admin-ui");
|
const { instrument } = require("@socket.io/admin-ui");
|
||||||
const { isString, isEmpty } = require("lodash");
|
const { isString, isEmpty, isFunction } = require("lodash");
|
||||||
|
|
||||||
const logger = require("./server/utils/logger");
|
const logger = require("./server/utils/logger");
|
||||||
const { applyRedisHelpers } = require("./server/utils/redisHelpers");
|
const { applyRedisHelpers } = require("./server/utils/redisHelpers");
|
||||||
@@ -393,7 +393,9 @@ const main = async () => {
|
|||||||
|
|
||||||
const StatusReporter = StartStatusReporter();
|
const StatusReporter = StartStatusReporter();
|
||||||
registerCleanupTask(async () => {
|
registerCleanupTask(async () => {
|
||||||
StatusReporter.end();
|
if (isFunction(StatusReporter?.end)) {
|
||||||
|
StatusReporter.end();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -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 (
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -1485,6 +1485,8 @@ exports.GET_JOB_BY_PK = `query GET_JOB_BY_PK($id: uuid!) {
|
|||||||
materials
|
materials
|
||||||
auto_add_ats
|
auto_add_ats
|
||||||
rate_ats
|
rate_ats
|
||||||
|
flat_rate_ats
|
||||||
|
rate_ats_flat
|
||||||
joblines(where: { removed: { _eq: false } }){
|
joblines(where: { removed: { _eq: false } }){
|
||||||
id
|
id
|
||||||
line_no
|
line_no
|
||||||
|
|||||||
@@ -371,6 +371,7 @@ exports.postback = async (req, res) => {
|
|||||||
iprequest: values,
|
iprequest: values,
|
||||||
decodedComment
|
decodedComment
|
||||||
};
|
};
|
||||||
|
const ipMapping = req.body?.bodyshop?.intellipay_config?.payment_map;
|
||||||
|
|
||||||
logger.log("intellipay-postback-received", "DEBUG", req.user?.email, null, logResponseMeta);
|
logger.log("intellipay-postback-received", "DEBUG", req.user?.email, null, logResponseMeta);
|
||||||
|
|
||||||
@@ -417,7 +418,7 @@ exports.postback = async (req, res) => {
|
|||||||
amount: p.amount,
|
amount: p.amount,
|
||||||
transactionid: values.authcode,
|
transactionid: values.authcode,
|
||||||
payer: "Customer",
|
payer: "Customer",
|
||||||
type: values.cardtype,
|
type: ipMapping[(values.cardtype || "").toLowerCase()] || values.cardtype,
|
||||||
jobid: p.jobid,
|
jobid: p.jobid,
|
||||||
date: moment(Date.now()),
|
date: moment(Date.now()),
|
||||||
payment_responses: {
|
payment_responses: {
|
||||||
@@ -481,7 +482,7 @@ exports.postback = async (req, res) => {
|
|||||||
amount: values.total,
|
amount: values.total,
|
||||||
transactionid: values.authcode,
|
transactionid: values.authcode,
|
||||||
payer: "Customer",
|
payer: "Customer",
|
||||||
type: values.cardtype,
|
type: ipMapping[(values.cardtype || "").toLowerCase()] || values.cardtype,
|
||||||
jobid: values.invoice,
|
jobid: values.invoice,
|
||||||
date: moment(Date.now())
|
date: moment(Date.now())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const Dinero = require("dinero.js");
|
const Dinero = require("dinero.js");
|
||||||
const queries = require("../graphql-client/queries");
|
const queries = require("../graphql-client/queries");
|
||||||
const adminClient = require("../graphql-client/graphql-client").client;
|
// const adminClient = require("../graphql-client/graphql-client").client;
|
||||||
const _ = require("lodash");
|
// const _ = require("lodash");
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
const InstanceMgr = require("../utils/instanceMgr").default;
|
const InstanceMgr = require("../utils/instanceMgr").default;
|
||||||
|
|
||||||
@@ -45,7 +45,9 @@ exports.totalsSsu = async function (req, res) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(200).send();
|
if (result) {
|
||||||
|
res.status(200).send();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("job-totals-ssu-USA-error", "ERROR", req?.user?.email, id, {
|
logger.log("job-totals-ssu-USA-error", "ERROR", req?.user?.email, id, {
|
||||||
jobid: id,
|
jobid: id,
|
||||||
@@ -59,7 +61,7 @@ exports.totalsSsu = async function (req, res) {
|
|||||||
//IMPORTANT*** These two functions MUST be mirrrored.
|
//IMPORTANT*** These two functions MUST be mirrrored.
|
||||||
async function TotalsServerSide(req, res) {
|
async function TotalsServerSide(req, res) {
|
||||||
const { job, client } = req.body;
|
const { job, client } = req.body;
|
||||||
await AutoAddAtsIfRequired({ job: job, client: client });
|
await AtsAdjustmentsIfRequired({ job: job, client: client, user: req?.user });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let ret = {
|
let ret = {
|
||||||
@@ -138,10 +140,11 @@ async function Totals(req, res) {
|
|||||||
const client = req.userGraphQLClient;
|
const client = req.userGraphQLClient;
|
||||||
|
|
||||||
logger.log("job-totals-ssu-USA", "DEBUG", req.user.email, job.id, {
|
logger.log("job-totals-ssu-USA", "DEBUG", req.user.email, job.id, {
|
||||||
jobid: job.id
|
jobid: job.id,
|
||||||
|
id: id
|
||||||
});
|
});
|
||||||
|
|
||||||
await AutoAddAtsIfRequired({ job, client });
|
await AtsAdjustmentsIfRequired({ job, client, user: req.user });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let ret = {
|
let ret = {
|
||||||
@@ -153,7 +156,7 @@ async function Totals(req, res) {
|
|||||||
|
|
||||||
res.status(200).json(ret);
|
res.status(200).json(ret);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("job-totals-USA-error", "ERROR", req.user.email, job.id, {
|
logger.log("job-totals-ssu-USA-error", "ERROR", req.user.email, job.id, {
|
||||||
jobid: job.id,
|
jobid: job.id,
|
||||||
error: error.message,
|
error: error.message,
|
||||||
stack: error.stack
|
stack: error.stack
|
||||||
@@ -162,40 +165,45 @@ async function Totals(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function AutoAddAtsIfRequired({ job, client }) {
|
async function AtsAdjustmentsIfRequired({ job, client, user }) {
|
||||||
//Check if ATS should be automatically added.
|
if (job.auto_add_ats || job.flat_rate_ats) {
|
||||||
if (job.auto_add_ats) {
|
let atsAmount = 0;
|
||||||
//Get the total sum of hours that should be the ATS amount.
|
|
||||||
//Check to see if an ATS line exists.
|
|
||||||
let atsLineIndex = null;
|
let atsLineIndex = null;
|
||||||
const atsHours = job.joblines.reduce((acc, val, index) => {
|
|
||||||
if (val.line_desc && val.line_desc.toLowerCase() === "ats amount") {
|
|
||||||
atsLineIndex = index;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
//Check if ATS should be automatically added.
|
||||||
val.mod_lbr_ty !== "LA1" &&
|
if (job.auto_add_ats) {
|
||||||
val.mod_lbr_ty !== "LA2" &&
|
const excludedLaborTypes = new Set(["LAA", "LAG", "LAS", "LAU", "LA1", "LA2", "LA3", "LA4"]);
|
||||||
val.mod_lbr_ty !== "LA3" &&
|
|
||||||
val.mod_lbr_ty !== "LA4" &&
|
|
||||||
val.mod_lbr_ty !== "LAU" &&
|
|
||||||
val.mod_lbr_ty !== "LAG" &&
|
|
||||||
val.mod_lbr_ty !== "LAS" &&
|
|
||||||
val.mod_lbr_ty !== "LAA"
|
|
||||||
) {
|
|
||||||
acc = acc + val.mod_lb_hrs;
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
//Get the total sum of hours that should be the ATS amount.
|
||||||
}, 0);
|
//Check to see if an ATS line exists.
|
||||||
|
const atsHours = job.joblines.reduce((acc, val, index) => {
|
||||||
|
if (val.line_desc?.toLowerCase() === "ats amount") {
|
||||||
|
atsLineIndex = index;
|
||||||
|
}
|
||||||
|
|
||||||
const atsAmount = atsHours * (job.rate_ats || 0);
|
if (!excludedLaborTypes.has(val.mod_lbr_ty)) {
|
||||||
//If it does, update it in place, and make sure it is updated for local calculations.
|
acc = acc + val.mod_lb_hrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
atsAmount = atsHours * (job.rate_ats || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if a Flat Rate ATS should be added.
|
||||||
|
if (job.flat_rate_ats) {
|
||||||
|
atsLineIndex = ((i) => (i === -1 ? null : i))(
|
||||||
|
job.joblines.findIndex((line) => line.line_desc?.toLowerCase() === "ats amount")
|
||||||
|
);
|
||||||
|
atsAmount = job.rate_ats_flat || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
//If it does not, create one for local calculations and insert it.
|
||||||
if (atsLineIndex === null) {
|
if (atsLineIndex === null) {
|
||||||
const newAtsLine = {
|
const newAtsLine = {
|
||||||
jobid: job.id,
|
jobid: job.id,
|
||||||
alt_partm: null,
|
alt_partm: null,
|
||||||
line_no: 35,
|
|
||||||
unq_seq: 0,
|
unq_seq: 0,
|
||||||
line_ind: "E",
|
line_ind: "E",
|
||||||
line_desc: "ATS Amount",
|
line_desc: "ATS Amount",
|
||||||
@@ -220,19 +228,42 @@ async function AutoAddAtsIfRequired({ job, client }) {
|
|||||||
prt_dsmk_m: 0.0
|
prt_dsmk_m: 0.0
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await client.request(queries.INSERT_NEW_JOB_LINE, {
|
try {
|
||||||
lineInput: [newAtsLine]
|
const result = await client.request(queries.INSERT_NEW_JOB_LINE, {
|
||||||
});
|
lineInput: [newAtsLine]
|
||||||
|
});
|
||||||
|
|
||||||
job.joblines.push(newAtsLine);
|
if (result) {
|
||||||
|
job.joblines.push(newAtsLine);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("job-totals-ssu-ats-error", "ERROR", user?.email, job.id, {
|
||||||
|
jobid: job.id,
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//If it does not, create one for local calculations and insert it.
|
//If it does, update it in place, and make sure it is updated for local calculations.
|
||||||
else {
|
else {
|
||||||
const result = await client.request(queries.UPDATE_JOB_LINE, {
|
try {
|
||||||
line: { act_price: atsAmount },
|
const result = await client.request(queries.UPDATE_JOB_LINE, {
|
||||||
lineId: job.joblines[atsLineIndex].id
|
line: { act_price: atsAmount },
|
||||||
});
|
lineId: job.joblines[atsLineIndex].id
|
||||||
job.joblines[atsLineIndex].act_price = atsAmount;
|
});
|
||||||
|
if (result) {
|
||||||
|
job.joblines[atsLineIndex].act_price = atsAmount;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("job-totals-ssu-ats-error", "ERROR", user?.email, job.id, {
|
||||||
|
jobid: job.id,
|
||||||
|
atsLineIndex: atsLineIndex,
|
||||||
|
atsAmount: atsAmount,
|
||||||
|
jobline: job.joblines[atsLineIndex],
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -314,7 +345,7 @@ async function CalculateRatesTotals({ job, client }) {
|
|||||||
let hasMashLine = false;
|
let hasMashLine = false;
|
||||||
let hasMahwLine = false;
|
let hasMahwLine = false;
|
||||||
let hasCustomMahwLine;
|
let hasCustomMahwLine;
|
||||||
let mapaOpCodes = ParseCalopCode(job.materials["MAPA"]?.cal_opcode);
|
// let mapaOpCodes = ParseCalopCode(job.materials["MAPA"]?.cal_opcode);
|
||||||
let mashOpCodes = ParseCalopCode(job.materials["MASH"]?.cal_opcode);
|
let mashOpCodes = ParseCalopCode(job.materials["MASH"]?.cal_opcode);
|
||||||
|
|
||||||
jobLines.forEach((item) => {
|
jobLines.forEach((item) => {
|
||||||
@@ -564,7 +595,7 @@ function CalculatePartsTotals(jobLines, parts_tax_rates, job) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
default:
|
default: {
|
||||||
if (!value.part_type && value.db_ref !== "900510" && value.db_ref !== "900511") return acc;
|
if (!value.part_type && value.db_ref !== "900510" && value.db_ref !== "900511") return acc;
|
||||||
|
|
||||||
const discountAmount =
|
const discountAmount =
|
||||||
@@ -631,6 +662,7 @@ function CalculatePartsTotals(jobLines, parts_tax_rates, job) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -652,7 +684,7 @@ function CalculatePartsTotals(jobLines, parts_tax_rates, job) {
|
|||||||
let adjustments = {};
|
let adjustments = {};
|
||||||
//Track all adjustments that need to be made.
|
//Track all adjustments that need to be made.
|
||||||
|
|
||||||
const linesToAdjustForDiscount = [];
|
//const linesToAdjustForDiscount = [];
|
||||||
Object.keys(parts_tax_rates).forEach((key) => {
|
Object.keys(parts_tax_rates).forEach((key) => {
|
||||||
//Check if there's a discount or a mark up.
|
//Check if there's a discount or a mark up.
|
||||||
let disc = Dinero(),
|
let disc = Dinero(),
|
||||||
@@ -1019,7 +1051,9 @@ function CalculateTaxesTotals(job, otherTotals) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("job-totals-USA Key with issue", "error", null, job.id, {
|
logger.log("job-totals-USA Key with issue", "error", null, job.id, {
|
||||||
key
|
key: key,
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1157,6 +1191,7 @@ function CalculateTaxesTotals(job, otherTotals) {
|
|||||||
|
|
||||||
exports.default = Totals;
|
exports.default = Totals;
|
||||||
|
|
||||||
|
//eslint-disable-next-line no-unused-vars
|
||||||
function DiscountNotAlreadyCounted(jobline, joblines) {
|
function DiscountNotAlreadyCounted(jobline, joblines) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1172,27 +1207,35 @@ function IsTrueOrYes(value) {
|
|||||||
return value === true || value === "Y" || value === "y";
|
return value === true || value === "Y" || value === "y";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function UpdateJobLines(joblinesToUpdate) {
|
// Function not in use from RO to IO Merger 02/05/2024
|
||||||
if (joblinesToUpdate.length === 0) return;
|
// async function UpdateJobLines(joblinesToUpdate) {
|
||||||
const updateQueries = joblinesToUpdate.map((line, index) =>
|
// if (joblinesToUpdate.length === 0) return;
|
||||||
generateUpdateQuery(_.pick(line, ["id", "prt_dsmk_m", "prt_dsmk_p"]), index)
|
// const updateQueries = joblinesToUpdate.map((line, index) =>
|
||||||
);
|
// generateUpdateQuery(_.pick(line, ["id", "prt_dsmk_m", "prt_dsmk_p"]), index)
|
||||||
const query = `
|
// );
|
||||||
mutation UPDATE_EST_LINES{
|
// const query = `
|
||||||
${updateQueries}
|
// mutation UPDATE_EST_LINES{
|
||||||
}
|
// ${updateQueries}
|
||||||
`;
|
// }
|
||||||
|
// `;
|
||||||
|
// try {
|
||||||
|
// const result = await adminClient.request(query);
|
||||||
|
// void result;
|
||||||
|
// } catch (error) {
|
||||||
|
// logger.log("update-job-lines", "error", null, null, {
|
||||||
|
// error: error.message,
|
||||||
|
// stack: error.stack
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
const result = await adminClient.request(query);
|
// const generateUpdateQuery = (lineToUpdate, index) => {
|
||||||
}
|
// return `
|
||||||
|
// update_joblines${index}: update_joblines(where: { id: { _eq: "${
|
||||||
const generateUpdateQuery = (lineToUpdate, index) => {
|
// lineToUpdate.id
|
||||||
return `
|
// }" } }, _set: ${JSON.stringify(lineToUpdate).replace(/"(\w+)"\s*:/g, "$1:")}) {
|
||||||
update_joblines${index}: update_joblines(where: { id: { _eq: "${
|
// returning {
|
||||||
lineToUpdate.id
|
// id
|
||||||
}" } }, _set: ${JSON.stringify(lineToUpdate).replace(/"(\w+)"\s*:/g, "$1:")}) {
|
// }
|
||||||
returning {
|
// }`;
|
||||||
id
|
// };
|
||||||
}
|
|
||||||
}`;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
const Dinero = require("dinero.js");
|
const Dinero = require("dinero.js");
|
||||||
const queries = require("../graphql-client/queries");
|
const queries = require("../graphql-client/queries");
|
||||||
const adminClient = require("../graphql-client/graphql-client").client;
|
|
||||||
const _ = require("lodash");
|
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
|
|
||||||
//****************************************************** */
|
//****************************************************** */
|
||||||
@@ -44,11 +42,16 @@ exports.totalsSsu = async function (req, res) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new Error("Failed to update job totals");
|
||||||
|
}
|
||||||
|
|
||||||
res.status(200).send();
|
res.status(200).send();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("job-totals-ssu-error", "ERROR", req.user.email, id, {
|
logger.log("job-totals-ssu-error", "ERROR", req.user.email, id, {
|
||||||
jobid: id,
|
jobid: id,
|
||||||
error
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
});
|
});
|
||||||
res.status(503).send();
|
res.status(503).send();
|
||||||
}
|
}
|
||||||
@@ -57,7 +60,7 @@ exports.totalsSsu = async function (req, res) {
|
|||||||
//IMPORTANT*** These two functions MUST be mirrrored.
|
//IMPORTANT*** These two functions MUST be mirrrored.
|
||||||
async function TotalsServerSide(req, res) {
|
async function TotalsServerSide(req, res) {
|
||||||
const { job, client } = req.body;
|
const { job, client } = req.body;
|
||||||
await AutoAddAtsIfRequired({ job: job, client: client });
|
await AtsAdjustmentsIfRequired({ job: job, client: client, user: req?.user });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let ret = {
|
let ret = {
|
||||||
@@ -71,7 +74,8 @@ async function TotalsServerSide(req, res) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("job-totals-ssu-error", "ERROR", req?.user?.email, job.id, {
|
logger.log("job-totals-ssu-error", "ERROR", req?.user?.email, job.id, {
|
||||||
jobid: job.id,
|
jobid: job.id,
|
||||||
error
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
});
|
});
|
||||||
res.status(400).send(JSON.stringify(error));
|
res.status(400).send(JSON.stringify(error));
|
||||||
}
|
}
|
||||||
@@ -83,13 +87,12 @@ async function Totals(req, res) {
|
|||||||
const logger = req.logger;
|
const logger = req.logger;
|
||||||
const client = req.userGraphQLClient;
|
const client = req.userGraphQLClient;
|
||||||
|
|
||||||
logger.log("job-totals", "DEBUG", req.user.email, job.id, {
|
logger.log("job-totals-ssu", "DEBUG", req.user.email, job.id, {
|
||||||
jobid: job.id
|
jobid: job.id,
|
||||||
|
id: id
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.log("job-totals-ssu", "DEBUG", req.user.email, id, null);
|
await AtsAdjustmentsIfRequired({ job, client, user: req.user });
|
||||||
|
|
||||||
await AutoAddAtsIfRequired({ job, client });
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let ret = {
|
let ret = {
|
||||||
@@ -101,48 +104,54 @@ async function Totals(req, res) {
|
|||||||
|
|
||||||
res.status(200).json(ret);
|
res.status(200).json(ret);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("job-totals-error", "ERROR", req.user.email, job.id, {
|
logger.log("job-totals-ssu-error", "ERROR", req.user.email, job.id, {
|
||||||
jobid: job.id,
|
jobid: job.id,
|
||||||
error
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
});
|
});
|
||||||
res.status(400).send(JSON.stringify(error));
|
res.status(400).send(JSON.stringify(error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function AutoAddAtsIfRequired({ job, client }) {
|
async function AtsAdjustmentsIfRequired({ job, client, user }) {
|
||||||
//Check if ATS should be automatically added.
|
if (job.auto_add_ats || job.flat_rate_ats) {
|
||||||
if (job.auto_add_ats) {
|
let atsAmount = 0;
|
||||||
//Get the total sum of hours that should be the ATS amount.
|
|
||||||
//Check to see if an ATS line exists.
|
|
||||||
let atsLineIndex = null;
|
let atsLineIndex = null;
|
||||||
const atsHours = job.joblines.reduce((acc, val, index) => {
|
|
||||||
if (val.line_desc && val.line_desc.toLowerCase() === "ats amount") {
|
|
||||||
atsLineIndex = index;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
//Check if ATS should be automatically added.
|
||||||
val.mod_lbr_ty !== "LA1" &&
|
if (job.auto_add_ats) {
|
||||||
val.mod_lbr_ty !== "LA2" &&
|
const excludedLaborTypes = new Set(["LAA", "LAG", "LAS", "LAU", "LA1", "LA2", "LA3", "LA4"]);
|
||||||
val.mod_lbr_ty !== "LA3" &&
|
|
||||||
val.mod_lbr_ty !== "LA4" &&
|
|
||||||
val.mod_lbr_ty !== "LAU" &&
|
|
||||||
val.mod_lbr_ty !== "LAG" &&
|
|
||||||
val.mod_lbr_ty !== "LAS" &&
|
|
||||||
val.mod_lbr_ty !== "LAA"
|
|
||||||
) {
|
|
||||||
acc = acc + val.mod_lb_hrs;
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
//Get the total sum of hours that should be the ATS amount.
|
||||||
}, 0);
|
//Check to see if an ATS line exists.
|
||||||
|
const atsHours = job.joblines.reduce((acc, val, index) => {
|
||||||
|
if (val.line_desc?.toLowerCase() === "ats amount") {
|
||||||
|
atsLineIndex = index;
|
||||||
|
}
|
||||||
|
|
||||||
const atsAmount = atsHours * (job.rate_ats || 0);
|
if (!excludedLaborTypes.has(val.mod_lbr_ty)) {
|
||||||
//If it does, update it in place, and make sure it is updated for local calculations.
|
acc = acc + val.mod_lb_hrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
atsAmount = atsHours * (job.rate_ats || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if a Flat Rate ATS should be added.
|
||||||
|
if (job.flat_rate_ats) {
|
||||||
|
atsLineIndex = ((i) => (i === -1 ? null : i))(
|
||||||
|
job.joblines.findIndex((line) => line.line_desc?.toLowerCase() === "ats amount")
|
||||||
|
);
|
||||||
|
atsAmount = job.rate_ats_flat || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
//If it does not, create one for local calculations and insert it.
|
||||||
if (atsLineIndex === null) {
|
if (atsLineIndex === null) {
|
||||||
const newAtsLine = {
|
const newAtsLine = {
|
||||||
jobid: job.id,
|
jobid: job.id,
|
||||||
alt_partm: null,
|
alt_partm: null,
|
||||||
line_no: 35,
|
|
||||||
unq_seq: 0,
|
unq_seq: 0,
|
||||||
line_ind: "E",
|
line_ind: "E",
|
||||||
line_desc: "ATS Amount",
|
line_desc: "ATS Amount",
|
||||||
@@ -167,22 +176,43 @@ async function AutoAddAtsIfRequired({ job, client }) {
|
|||||||
prt_dsmk_m: 0.0
|
prt_dsmk_m: 0.0
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await client.request(queries.INSERT_NEW_JOB_LINE, {
|
try {
|
||||||
lineInput: [newAtsLine]
|
const result = await client.request(queries.INSERT_NEW_JOB_LINE, {
|
||||||
});
|
lineInput: [newAtsLine]
|
||||||
|
});
|
||||||
|
|
||||||
job.joblines.push(newAtsLine);
|
if (result) {
|
||||||
|
job.joblines.push(newAtsLine);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("job-totals-ssu-ats-error", "ERROR", user?.email, job.id, {
|
||||||
|
jobid: job.id,
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//If it does not, create one for local calculations and insert it.
|
//If it does, update it in place, and make sure it is updated for local calculations.
|
||||||
else {
|
else {
|
||||||
const result = await client.request(queries.UPDATE_JOB_LINE, {
|
try {
|
||||||
line: { act_price: atsAmount },
|
const result = await client.request(queries.UPDATE_JOB_LINE, {
|
||||||
lineId: job.joblines[atsLineIndex].id
|
line: { act_price: atsAmount },
|
||||||
});
|
lineId: job.joblines[atsLineIndex].id
|
||||||
job.joblines[atsLineIndex].act_price = atsAmount;
|
});
|
||||||
|
if (result) {
|
||||||
|
job.joblines[atsLineIndex].act_price = atsAmount;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("job-totals-ssu-ats-error", "ERROR", user?.email, job.id, {
|
||||||
|
jobid: job.id,
|
||||||
|
atsLineIndex: atsLineIndex,
|
||||||
|
atsAmount: atsAmount,
|
||||||
|
jobline: job.joblines[atsLineIndex],
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//console.log(job.jobLines);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -134,24 +134,6 @@ const handleJobLinesChange = async (req, res) =>
|
|||||||
const handleNotesChange = async (req, res) =>
|
const handleNotesChange = async (req, res) =>
|
||||||
processNotificationEvent(req, res, "req.body.event.new.jobid", "Notes Changed Notification Event Handled.");
|
processNotificationEvent(req, res, "req.body.event.new.jobid", "Notes Changed Notification Event Handled.");
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle parts dispatch change notifications.
|
|
||||||
*
|
|
||||||
* @param {Object} req - Express request object.
|
|
||||||
* @param {Object} res - Express response object.
|
|
||||||
* @returns {Object} JSON response with a success message.
|
|
||||||
*/
|
|
||||||
const handlePartsDispatchChange = (req, res) => res.status(200).json({ message: "Parts Dispatch change handled." });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle parts order change notifications.
|
|
||||||
*
|
|
||||||
* @param {Object} req - Express request object.
|
|
||||||
* @param {Object} res - Express response object.
|
|
||||||
* @returns {Object} JSON response with a success message.
|
|
||||||
*/
|
|
||||||
const handlePartsOrderChange = (req, res) => res.status(200).json({ message: "Parts Order change handled." });
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle payments change notifications.
|
* Handle payments change notifications.
|
||||||
*
|
*
|
||||||
@@ -182,6 +164,27 @@ const handleTasksChange = async (req, res) =>
|
|||||||
const handleTimeTicketsChange = async (req, res) =>
|
const handleTimeTicketsChange = async (req, res) =>
|
||||||
processNotificationEvent(req, res, "req.body.event.new.jobid", "Time Tickets Changed Notification Event Handled.");
|
processNotificationEvent(req, res, "req.body.event.new.jobid", "Time Tickets Changed Notification Event Handled.");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle parts dispatch change notifications.
|
||||||
|
* Note: Placeholder
|
||||||
|
*
|
||||||
|
* @param {Object} req - Express request object.
|
||||||
|
* @param {Object} res - Express response object.
|
||||||
|
* @returns {Object} JSON response with a success message.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const handlePartsDispatchChange = (req, res) => res.status(200).json({ message: "Parts Dispatch change handled." });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle parts order change notifications.
|
||||||
|
* Note: Placeholder
|
||||||
|
*
|
||||||
|
* @param {Object} req - Express request object.
|
||||||
|
* @param {Object} res - Express response object.
|
||||||
|
* @returns {Object} JSON response with a success message.
|
||||||
|
*/
|
||||||
|
const handlePartsOrderChange = (req, res) => res.status(200).json({ message: "Parts Order change handled." });
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
handleJobsChange,
|
handleJobsChange,
|
||||||
handleBillsChange,
|
handleBillsChange,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
const { Queue, Worker } = require("bullmq");
|
const { Queue, Worker } = require("bullmq");
|
||||||
const { INSERT_NOTIFICATIONS_MUTATION } = require("../../graphql-client/queries");
|
const { INSERT_NOTIFICATIONS_MUTATION } = require("../../graphql-client/queries");
|
||||||
const { registerCleanupTask } = require("../../utils/cleanupManager");
|
const { registerCleanupTask } = require("../../utils/cleanupManager");
|
||||||
|
const getBullMQPrefix = require("../../utils/getBullMQPrefix");
|
||||||
|
const devDebugLogger = require("../../utils/devDebugLogger");
|
||||||
const graphQLClient = require("../../graphql-client/graphql-client").client;
|
const graphQLClient = require("../../graphql-client/graphql-client").client;
|
||||||
|
|
||||||
// Base time-related constant in minutes, sourced from environment variable or defaulting to 1
|
// Base time-related constant in minutes, sourced from environment variable or defaulting to 1
|
||||||
@@ -45,17 +47,20 @@ const buildNotificationContent = (notifications) => {
|
|||||||
*/
|
*/
|
||||||
const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
||||||
if (!addQueue || !consolidateQueue) {
|
if (!addQueue || !consolidateQueue) {
|
||||||
logger.logger.debug("Initializing Notifications Queues");
|
const prefix = getBullMQPrefix();
|
||||||
|
const devKey = process.env?.NODE_ENV === "production" ? "prod" : "dev";
|
||||||
|
|
||||||
|
devDebugLogger(`Initializing Notifications Queues with prefix: ${prefix}`);
|
||||||
|
|
||||||
addQueue = new Queue("notificationsAdd", {
|
addQueue = new Queue("notificationsAdd", {
|
||||||
|
prefix,
|
||||||
connection: pubClient,
|
connection: pubClient,
|
||||||
prefix: "{BULLMQ}",
|
|
||||||
defaultJobOptions: { removeOnComplete: true, removeOnFail: true }
|
defaultJobOptions: { removeOnComplete: true, removeOnFail: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
consolidateQueue = new Queue("notificationsConsolidate", {
|
consolidateQueue = new Queue("notificationsConsolidate", {
|
||||||
|
prefix,
|
||||||
connection: pubClient,
|
connection: pubClient,
|
||||||
prefix: "{BULLMQ}",
|
|
||||||
defaultJobOptions: { removeOnComplete: true, removeOnFail: true }
|
defaultJobOptions: { removeOnComplete: true, removeOnFail: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -63,9 +68,9 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
|||||||
"notificationsAdd",
|
"notificationsAdd",
|
||||||
async (job) => {
|
async (job) => {
|
||||||
const { jobId, key, variables, recipients, body, jobRoNumber } = job.data;
|
const { jobId, key, variables, recipients, body, jobRoNumber } = job.data;
|
||||||
logger.logger.debug(`Adding notifications for jobId ${jobId}`);
|
devDebugLogger(`Adding notifications for jobId ${jobId}`);
|
||||||
|
|
||||||
const redisKeyPrefix = `app:notifications:${jobId}`;
|
const redisKeyPrefix = `app:${devKey}:notifications:${jobId}`;
|
||||||
const notification = { key, variables, body, jobRoNumber, timestamp: Date.now() };
|
const notification = { key, variables, body, jobRoNumber, timestamp: Date.now() };
|
||||||
|
|
||||||
for (const recipient of recipients) {
|
for (const recipient of recipients) {
|
||||||
@@ -75,12 +80,12 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
|||||||
const notifications = existingNotifications ? JSON.parse(existingNotifications) : [];
|
const notifications = existingNotifications ? JSON.parse(existingNotifications) : [];
|
||||||
notifications.push(notification);
|
notifications.push(notification);
|
||||||
await pubClient.set(userKey, JSON.stringify(notifications), "EX", NOTIFICATION_STORAGE_EXPIRATION / 1000);
|
await pubClient.set(userKey, JSON.stringify(notifications), "EX", NOTIFICATION_STORAGE_EXPIRATION / 1000);
|
||||||
logger.logger.debug(`Stored notification for ${user} under ${userKey}: ${JSON.stringify(notifications)}`);
|
devDebugLogger(`Stored notification for ${user} under ${userKey}: ${JSON.stringify(notifications)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const consolidateKey = `app:consolidate:${jobId}`;
|
const consolidateKey = `app:${devKey}:consolidate:${jobId}`;
|
||||||
const flagSet = await pubClient.setnx(consolidateKey, "pending");
|
const flagSet = await pubClient.setnx(consolidateKey, "pending");
|
||||||
logger.logger.debug(`Consolidation flag set for jobId ${jobId}: ${flagSet}`);
|
devDebugLogger(`Consolidation flag set for jobId ${jobId}: ${flagSet}`);
|
||||||
|
|
||||||
if (flagSet) {
|
if (flagSet) {
|
||||||
await consolidateQueue.add(
|
await consolidateQueue.add(
|
||||||
@@ -93,15 +98,15 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
|||||||
backoff: LOCK_EXPIRATION
|
backoff: LOCK_EXPIRATION
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
logger.logger.debug(`Scheduled consolidation for jobId ${jobId}`);
|
devDebugLogger(`Scheduled consolidation for jobId ${jobId}`);
|
||||||
await pubClient.expire(consolidateKey, CONSOLIDATION_FLAG_EXPIRATION / 1000);
|
await pubClient.expire(consolidateKey, CONSOLIDATION_FLAG_EXPIRATION / 1000);
|
||||||
} else {
|
} else {
|
||||||
logger.logger.debug(`Consolidation already scheduled for jobId ${jobId}`);
|
devDebugLogger(`Consolidation already scheduled for jobId ${jobId}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
prefix,
|
||||||
connection: pubClient,
|
connection: pubClient,
|
||||||
prefix: "{BULLMQ}",
|
|
||||||
concurrency: 5
|
concurrency: 5
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -110,23 +115,24 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
|||||||
"notificationsConsolidate",
|
"notificationsConsolidate",
|
||||||
async (job) => {
|
async (job) => {
|
||||||
const { jobId, recipients } = job.data;
|
const { jobId, recipients } = job.data;
|
||||||
logger.logger.debug(`Consolidating notifications for jobId ${jobId}`);
|
devDebugLogger(`Consolidating notifications for jobId ${jobId}`);
|
||||||
|
|
||||||
|
const redisKeyPrefix = `app:${devKey}:notifications:${jobId}`;
|
||||||
|
const lockKey = `lock:${devKey}:consolidate:${jobId}`;
|
||||||
|
|
||||||
const redisKeyPrefix = `app:notifications:${jobId}`;
|
|
||||||
const lockKey = `lock:consolidate:${jobId}`;
|
|
||||||
const lockAcquired = await pubClient.set(lockKey, "locked", "NX", "EX", LOCK_EXPIRATION / 1000);
|
const lockAcquired = await pubClient.set(lockKey, "locked", "NX", "EX", LOCK_EXPIRATION / 1000);
|
||||||
logger.logger.debug(`Lock acquisition for jobId ${jobId}: ${lockAcquired}`);
|
devDebugLogger(`Lock acquisition for jobId ${jobId}: ${lockAcquired}`);
|
||||||
|
|
||||||
if (lockAcquired) {
|
if (lockAcquired) {
|
||||||
try {
|
try {
|
||||||
const allNotifications = {};
|
const allNotifications = {};
|
||||||
const uniqueUsers = [...new Set(recipients.map((r) => r.user))];
|
const uniqueUsers = [...new Set(recipients.map((r) => r.user))];
|
||||||
logger.logger.debug(`Unique users for jobId ${jobId}: ${uniqueUsers}`);
|
devDebugLogger(`Unique users for jobId ${jobId}: ${uniqueUsers}`);
|
||||||
|
|
||||||
for (const user of uniqueUsers) {
|
for (const user of uniqueUsers) {
|
||||||
const userKey = `${redisKeyPrefix}:${user}`;
|
const userKey = `${redisKeyPrefix}:${user}`;
|
||||||
const notifications = await pubClient.get(userKey);
|
const notifications = await pubClient.get(userKey);
|
||||||
logger.logger.debug(`Retrieved notifications for ${user}: ${notifications}`);
|
devDebugLogger(`Retrieved notifications for ${user}: ${notifications}`);
|
||||||
|
|
||||||
if (notifications) {
|
if (notifications) {
|
||||||
const parsedNotifications = JSON.parse(notifications);
|
const parsedNotifications = JSON.parse(notifications);
|
||||||
@@ -136,13 +142,13 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
|||||||
allNotifications[user][bodyShopId] = parsedNotifications;
|
allNotifications[user][bodyShopId] = parsedNotifications;
|
||||||
}
|
}
|
||||||
await pubClient.del(userKey);
|
await pubClient.del(userKey);
|
||||||
logger.logger.debug(`Deleted Redis key ${userKey}`);
|
devDebugLogger(`Deleted Redis key ${userKey}`);
|
||||||
} else {
|
} else {
|
||||||
logger.logger.debug(`No notifications found for ${user} under ${userKey}`);
|
devDebugLogger(`No notifications found for ${user} under ${userKey}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.logger.debug(`Consolidated notifications: ${JSON.stringify(allNotifications)}`);
|
devDebugLogger(`Consolidated notifications: ${JSON.stringify(allNotifications)}`);
|
||||||
|
|
||||||
// Insert notifications into the database and collect IDs
|
// Insert notifications into the database and collect IDs
|
||||||
const notificationInserts = [];
|
const notificationInserts = [];
|
||||||
@@ -169,7 +175,7 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
|||||||
const insertResponse = await graphQLClient.request(INSERT_NOTIFICATIONS_MUTATION, {
|
const insertResponse = await graphQLClient.request(INSERT_NOTIFICATIONS_MUTATION, {
|
||||||
objects: notificationInserts
|
objects: notificationInserts
|
||||||
});
|
});
|
||||||
logger.logger.debug(
|
devDebugLogger(
|
||||||
`Inserted ${insertResponse.insert_notifications.affected_rows} notifications for jobId ${jobId}`
|
`Inserted ${insertResponse.insert_notifications.affected_rows} notifications for jobId ${jobId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -203,16 +209,16 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
|||||||
associationId
|
associationId
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
logger.logger.debug(
|
devDebugLogger(
|
||||||
`Sent ${notifications.length} consolidated notifications to ${user} for jobId ${jobId} with notificationId ${notificationId}`
|
`Sent ${notifications.length} consolidated notifications to ${user} for jobId ${jobId} with notificationId ${notificationId}`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
logger.logger.debug(`No socket IDs found for ${user} in bodyShopId ${bodyShopId}`);
|
devDebugLogger(`No socket IDs found for ${user} in bodyShopId ${bodyShopId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await pubClient.del(`app:consolidate:${jobId}`);
|
await pubClient.del(`app:${devKey}:consolidate:${jobId}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.log(`app-queue-consolidation-error`, "ERROR", "notifications", "api", {
|
logger.log(`app-queue-consolidation-error`, "ERROR", "notifications", "api", {
|
||||||
message: err?.message,
|
message: err?.message,
|
||||||
@@ -223,19 +229,20 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
|||||||
await pubClient.del(lockKey);
|
await pubClient.del(lockKey);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.logger.debug(`Skipped consolidation for jobId ${jobId} - lock held by another worker`);
|
devDebugLogger(`Skipped consolidation for jobId ${jobId} - lock held by another worker`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
prefix,
|
||||||
connection: pubClient,
|
connection: pubClient,
|
||||||
prefix: "{BULLMQ}",
|
|
||||||
concurrency: 1,
|
concurrency: 1,
|
||||||
limiter: { max: 1, duration: RATE_LIMITER_DURATION }
|
limiter: { max: 1, duration: RATE_LIMITER_DURATION }
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
addWorker.on("completed", (job) => logger.logger.debug(`Add job ${job.id} completed`));
|
addWorker.on("completed", (job) => devDebugLogger(`Add job ${job.id} completed`));
|
||||||
consolidateWorker.on("completed", (job) => logger.logger.debug(`Consolidate job ${job.id} completed`));
|
consolidateWorker.on("completed", (job) => devDebugLogger(`Consolidate job ${job.id} completed`));
|
||||||
|
|
||||||
addWorker.on("failed", (job, err) =>
|
addWorker.on("failed", (job, err) =>
|
||||||
logger.log(`app-queue-notification-error`, "ERROR", "notifications", "api", {
|
logger.log(`app-queue-notification-error`, "ERROR", "notifications", "api", {
|
||||||
message: err?.message,
|
message: err?.message,
|
||||||
@@ -251,9 +258,9 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
|||||||
|
|
||||||
// Register cleanup task instead of direct process listeners
|
// Register cleanup task instead of direct process listeners
|
||||||
const shutdown = async () => {
|
const shutdown = async () => {
|
||||||
logger.logger.debug("Closing app queue workers...");
|
devDebugLogger("Closing app queue workers...");
|
||||||
await Promise.all([addWorker.close(), consolidateWorker.close()]);
|
await Promise.all([addWorker.close(), consolidateWorker.close()]);
|
||||||
logger.logger.debug("App queue workers closed");
|
devDebugLogger("App queue workers closed");
|
||||||
};
|
};
|
||||||
|
|
||||||
registerCleanupTask(shutdown);
|
registerCleanupTask(shutdown);
|
||||||
@@ -283,7 +290,7 @@ const dispatchAppsToQueue = async ({ appsToDispatch, logger }) => {
|
|||||||
{ jobId, bodyShopId, key, variables, recipients, body, jobRoNumber },
|
{ jobId, bodyShopId, key, variables, recipients, body, jobRoNumber },
|
||||||
{ jobId: `${jobId}:${Date.now()}` }
|
{ jobId: `${jobId}:${Date.now()}` }
|
||||||
);
|
);
|
||||||
logger.logger.debug(`Added notification to queue for jobId ${jobId} with ${recipients.length} recipients`);
|
devDebugLogger(`Added notification to queue for jobId ${jobId} with ${recipients.length} recipients`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ const { sendTaskEmail } = require("../../email/sendemail");
|
|||||||
const generateEmailTemplate = require("../../email/generateTemplate");
|
const generateEmailTemplate = require("../../email/generateTemplate");
|
||||||
const { InstanceEndpoints } = require("../../utils/instanceMgr");
|
const { InstanceEndpoints } = require("../../utils/instanceMgr");
|
||||||
const { registerCleanupTask } = require("../../utils/cleanupManager");
|
const { registerCleanupTask } = require("../../utils/cleanupManager");
|
||||||
|
const getBullMQPrefix = require("../../utils/getBullMQPrefix");
|
||||||
|
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;
|
||||||
@@ -34,19 +37,22 @@ let emailConsolidateWorker;
|
|||||||
*/
|
*/
|
||||||
const loadEmailQueue = async ({ pubClient, logger }) => {
|
const loadEmailQueue = async ({ pubClient, logger }) => {
|
||||||
if (!emailAddQueue || !emailConsolidateQueue) {
|
if (!emailAddQueue || !emailConsolidateQueue) {
|
||||||
logger.logger.debug("Initializing Email Notification Queues");
|
const prefix = getBullMQPrefix();
|
||||||
|
const devKey = process.env?.NODE_ENV === "production" ? "prod" : "dev";
|
||||||
|
|
||||||
|
devDebugLogger(`Initializing Email Notification Queues with prefix: ${prefix}`);
|
||||||
|
|
||||||
// Queue for adding email notifications
|
// Queue for adding email notifications
|
||||||
emailAddQueue = new Queue("emailAdd", {
|
emailAddQueue = new Queue("emailAdd", {
|
||||||
|
prefix,
|
||||||
connection: pubClient,
|
connection: pubClient,
|
||||||
prefix: "{BULLMQ}",
|
|
||||||
defaultJobOptions: { removeOnComplete: true, removeOnFail: true }
|
defaultJobOptions: { removeOnComplete: true, removeOnFail: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Queue for consolidating and sending emails
|
// Queue for consolidating and sending emails
|
||||||
emailConsolidateQueue = new Queue("emailConsolidate", {
|
emailConsolidateQueue = new Queue("emailConsolidate", {
|
||||||
|
prefix,
|
||||||
connection: pubClient,
|
connection: pubClient,
|
||||||
prefix: "{BULLMQ}",
|
|
||||||
defaultJobOptions: { removeOnComplete: true, removeOnFail: true }
|
defaultJobOptions: { removeOnComplete: true, removeOnFail: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,29 +60,31 @@ 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;
|
||||||
logger.logger.debug(`Adding email notifications for jobId ${jobId}`);
|
devDebugLogger(`Adding email notifications for jobId ${jobId}`);
|
||||||
|
|
||||||
|
const redisKeyPrefix = `email:${devKey}:notifications:${jobId}`;
|
||||||
|
|
||||||
const redisKeyPrefix = `email:notifications:${jobId}`;
|
|
||||||
for (const recipient of recipients) {
|
for (const recipient of recipients) {
|
||||||
const { user, firstName, lastName } = recipient;
|
const { user, firstName, lastName } = recipient;
|
||||||
const userKey = `${redisKeyPrefix}:${user}`;
|
const userKey = `${redisKeyPrefix}:${user}`;
|
||||||
await pubClient.rpush(userKey, body);
|
await pubClient.rpush(userKey, body);
|
||||||
await pubClient.expire(userKey, NOTIFICATION_EXPIRATION / 1000);
|
await pubClient.expire(userKey, NOTIFICATION_EXPIRATION / 1000);
|
||||||
const detailsKey = `email: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:recipients:${jobId}`, user);
|
await pubClient.sadd(`email:${devKey}:recipients:${jobId}`, user);
|
||||||
logger.logger.debug(`Stored message for ${user} under ${userKey}: ${body}`);
|
devDebugLogger(`Stored message for ${user} under ${userKey}: ${body}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const consolidateKey = `email:consolidate:${jobId}`;
|
const consolidateKey = `email:${devKey}:consolidate:${jobId}`;
|
||||||
const flagSet = await pubClient.setnx(consolidateKey, "pending");
|
const flagSet = await pubClient.setnx(consolidateKey, "pending");
|
||||||
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,
|
||||||
@@ -84,15 +92,15 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
|||||||
backoff: LOCK_EXPIRATION
|
backoff: LOCK_EXPIRATION
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
logger.logger.debug(`Scheduled email consolidation for jobId ${jobId}`);
|
devDebugLogger(`Scheduled email consolidation for jobId ${jobId}`);
|
||||||
await pubClient.expire(consolidateKey, CONSOLIDATION_KEY_EXPIRATION / 1000);
|
await pubClient.expire(consolidateKey, CONSOLIDATION_KEY_EXPIRATION / 1000);
|
||||||
} else {
|
} else {
|
||||||
logger.logger.debug(`Email consolidation already scheduled for jobId ${jobId}`);
|
devDebugLogger(`Email consolidation already scheduled for jobId ${jobId}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
prefix,
|
||||||
connection: pubClient,
|
connection: pubClient,
|
||||||
prefix: "{BULLMQ}",
|
|
||||||
concurrency: 5
|
concurrency: 5
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -102,26 +110,28 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
|||||||
"emailConsolidate",
|
"emailConsolidate",
|
||||||
async (job) => {
|
async (job) => {
|
||||||
const { jobId, jobRoNumber, bodyShopName } = job.data;
|
const { jobId, jobRoNumber, bodyShopName } = job.data;
|
||||||
logger.logger.debug(`Consolidating emails for jobId ${jobId}`);
|
devDebugLogger(`Consolidating emails for jobId ${jobId}`);
|
||||||
|
|
||||||
const lockKey = `lock:emailConsolidate:${jobId}`;
|
const lockKey = `lock:${devKey}:emailConsolidate:${jobId}`;
|
||||||
const lockAcquired = await pubClient.set(lockKey, "locked", "NX", "EX", LOCK_EXPIRATION / 1000);
|
const lockAcquired = await pubClient.set(lockKey, "locked", "NX", "EX", LOCK_EXPIRATION / 1000);
|
||||||
if (lockAcquired) {
|
if (lockAcquired) {
|
||||||
try {
|
try {
|
||||||
const recipientsSet = `email:recipients:${jobId}`;
|
const recipientsSet = `email:${devKey}:recipients:${jobId}`;
|
||||||
const recipients = await pubClient.smembers(recipientsSet);
|
const recipients = await pubClient.smembers(recipientsSet);
|
||||||
for (const recipient of recipients) {
|
for (const recipient of recipients) {
|
||||||
const userKey = `email:notifications:${jobId}:${recipient}`;
|
const userKey = `email:${devKey}:notifications:${jobId}:${recipient}`;
|
||||||
const detailsKey = `email:recipientDetails:${jobId}:${recipient}`;
|
const detailsKey = `email:${devKey}:recipientDetails:${jobId}:${recipient}`;
|
||||||
const messages = await pubClient.lrange(userKey, 0, -1);
|
const messages = await pubClient.lrange(userKey, 0, -1);
|
||||||
if (messages.length > 0) {
|
if (messages.length > 0) {
|
||||||
const details = await pubClient.hgetall(detailsKey);
|
const details = await pubClient.hgetall(detailsKey);
|
||||||
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>
|
||||||
@@ -136,7 +146,7 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
|||||||
type: "html",
|
type: "html",
|
||||||
html: emailBody
|
html: emailBody
|
||||||
});
|
});
|
||||||
logger.logger.debug(
|
devDebugLogger(
|
||||||
`Sent consolidated email to ${recipient} for jobId ${jobId} with ${messages.length} updates`
|
`Sent consolidated email to ${recipient} for jobId ${jobId} with ${messages.length} updates`
|
||||||
);
|
);
|
||||||
await pubClient.del(userKey);
|
await pubClient.del(userKey);
|
||||||
@@ -144,7 +154,7 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await pubClient.del(recipientsSet);
|
await pubClient.del(recipientsSet);
|
||||||
await pubClient.del(`email:consolidate:${jobId}`);
|
await pubClient.del(`email:${devKey}:consolidate:${jobId}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.log(`email-queue-consolidation-error`, "ERROR", "notifications", "api", {
|
logger.log(`email-queue-consolidation-error`, "ERROR", "notifications", "api", {
|
||||||
message: err?.message,
|
message: err?.message,
|
||||||
@@ -155,20 +165,21 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
|||||||
await pubClient.del(lockKey);
|
await pubClient.del(lockKey);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.logger.debug(`Skipped email consolidation for jobId ${jobId} - lock held by another worker`);
|
devDebugLogger(`Skipped email consolidation for jobId ${jobId} - lock held by another worker`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
prefix,
|
||||||
connection: pubClient,
|
connection: pubClient,
|
||||||
prefix: "{BULLMQ}",
|
|
||||||
concurrency: 1,
|
concurrency: 1,
|
||||||
limiter: { max: 1, duration: RATE_LIMITER_DURATION }
|
limiter: { max: 1, duration: RATE_LIMITER_DURATION }
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Event handlers for workers
|
// Event handlers for workers
|
||||||
emailAddWorker.on("completed", (job) => logger.logger.debug(`Email add job ${job.id} completed`));
|
emailAddWorker.on("completed", (job) => devDebugLogger(`Email add job ${job.id} completed`));
|
||||||
emailConsolidateWorker.on("completed", (job) => logger.logger.debug(`Email consolidate job ${job.id} completed`));
|
emailConsolidateWorker.on("completed", (job) => devDebugLogger(`Email consolidate job ${job.id} completed`));
|
||||||
|
|
||||||
emailAddWorker.on("failed", (job, err) =>
|
emailAddWorker.on("failed", (job, err) =>
|
||||||
logger.log(`add-email-queue-failed`, "ERROR", "notifications", "api", {
|
logger.log(`add-email-queue-failed`, "ERROR", "notifications", "api", {
|
||||||
message: err?.message,
|
message: err?.message,
|
||||||
@@ -184,9 +195,9 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
|||||||
|
|
||||||
// Register cleanup task instead of direct process listeners
|
// Register cleanup task instead of direct process listeners
|
||||||
const shutdown = async () => {
|
const shutdown = async () => {
|
||||||
logger.logger.debug("Closing email queue workers...");
|
devDebugLogger("Closing email queue workers...");
|
||||||
await Promise.all([emailAddWorker.close(), emailConsolidateWorker.close()]);
|
await Promise.all([emailAddWorker.close(), emailConsolidateWorker.close()]);
|
||||||
logger.logger.debug("Email queue workers closed");
|
devDebugLogger("Email queue workers closed");
|
||||||
};
|
};
|
||||||
registerCleanupTask(shutdown);
|
registerCleanupTask(shutdown);
|
||||||
}
|
}
|
||||||
@@ -219,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}`
|
||||||
);
|
);
|
||||||
@@ -231,10 +242,10 @@ 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()}` }
|
||||||
);
|
);
|
||||||
logger.logger.debug(`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`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,36 @@ const Dinero = require("dinero.js");
|
|||||||
Dinero.globalRoundingMode = "HALF_EVEN";
|
Dinero.globalRoundingMode = "HALF_EVEN";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Populates the recipients for app, email, and FCM notifications based on scenario watchers.
|
* Creates a standard notification object with app, email, and FCM properties and populates recipients.
|
||||||
*
|
* @param {Object} data - Input data containing jobId, jobRoNumber, bodyShopId, bodyShopName, and scenarioWatchers
|
||||||
* @param {Object} data - The data object containing scenarioWatchers and bodyShopId.
|
* @param {string} key - Notification key for the app
|
||||||
* @param {Object} result - The result object to populate with recipients for app, email, and FCM notifications.
|
* @param {string} body - Notification body text
|
||||||
|
* @param {Object} [variables={}] - Variables for the app notification
|
||||||
|
* @returns {Object} Notification object with populated recipients
|
||||||
*/
|
*/
|
||||||
const populateWatchers = (data, result) => {
|
const buildNotification = (data, key, body, variables = {}) => {
|
||||||
|
const result = {
|
||||||
|
app: {
|
||||||
|
jobId: data.jobId,
|
||||||
|
jobRoNumber: data.jobRoNumber,
|
||||||
|
bodyShopId: data.bodyShopId,
|
||||||
|
key,
|
||||||
|
body,
|
||||||
|
variables,
|
||||||
|
recipients: []
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
jobId: data.jobId,
|
||||||
|
jobRoNumber: data.jobRoNumber,
|
||||||
|
bodyShopName: data.bodyShopName,
|
||||||
|
bodyShopTimezone: data.bodyShopTimezone,
|
||||||
|
body,
|
||||||
|
recipients: []
|
||||||
|
},
|
||||||
|
fcm: { recipients: [] }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Populate recipients from scenarioWatchers
|
||||||
data.scenarioWatchers.forEach((recipients) => {
|
data.scenarioWatchers.forEach((recipients) => {
|
||||||
const { user, app, fcm, email, firstName, lastName, employeeId, associationId } = recipients;
|
const { user, app, fcm, email, firstName, lastName, employeeId, associationId } = recipients;
|
||||||
if (app === true)
|
if (app === true)
|
||||||
@@ -24,287 +48,153 @@ const populateWatchers = (data, result) => {
|
|||||||
if (fcm === true) result.fcm.recipients.push(user);
|
if (fcm === true) result.fcm.recipients.push(user);
|
||||||
if (email === true) result.email.recipients.push({ user, firstName, lastName });
|
if (email === true) result.email.recipients.push({ user, firstName, lastName });
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds notification data for changes to alternate transport.
|
|
||||||
*/
|
|
||||||
const alternateTransportChangedBuilder = (data) => {
|
|
||||||
const body = `The alternate transportation has been changed from ${data.changedFields.alt_transport?.old || "unset"} to ${data?.changedFields?.alt_transport?.new || "unset"}.`;
|
|
||||||
const result = {
|
|
||||||
app: {
|
|
||||||
jobId: data.jobId,
|
|
||||||
bodyShopId: data.bodyShopId,
|
|
||||||
jobRoNumber: data.jobRoNumber,
|
|
||||||
key: "notifications.job.alternateTransportChanged",
|
|
||||||
body,
|
|
||||||
variables: {
|
|
||||||
alternateTransport: data?.changedFields?.alt_transport?.new,
|
|
||||||
oldAlternateTransport: data?.changedFields?.alt_transport?.old
|
|
||||||
},
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
jobId: data.jobId,
|
|
||||||
jobRoNumber: data.jobRoNumber,
|
|
||||||
bodyShopName: data.bodyShopName,
|
|
||||||
body,
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
fcm: { recipients: [] }
|
|
||||||
};
|
|
||||||
|
|
||||||
populateWatchers(data, result);
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds notification data for bill posted events.
|
* Creates a notification for when the alternate transport is changed.
|
||||||
|
* @param data
|
||||||
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||||
*/
|
*/
|
||||||
const billPostedHandler = (data) => {
|
const alternateTransportChangedBuilder = (data) => {
|
||||||
|
const oldTransport = data?.changedFields?.alt_transport?.old;
|
||||||
|
const newTransport = data?.changedFields?.alt_transport?.new;
|
||||||
|
let body;
|
||||||
|
|
||||||
|
if (oldTransport && newTransport)
|
||||||
|
body = `The alternate transportation has been changed from ${oldTransport} to ${newTransport}.`;
|
||||||
|
else if (!oldTransport && newTransport) body = `The alternate transportation has been set to ${newTransport}.`;
|
||||||
|
else if (oldTransport && !newTransport)
|
||||||
|
body = `The alternate transportation has been canceled (previously ${oldTransport}).`;
|
||||||
|
else body = `The alternate transportation has been updated.`;
|
||||||
|
|
||||||
|
return buildNotification(data, "notifications.job.alternateTransportChanged", body, {
|
||||||
|
alternateTransport: newTransport,
|
||||||
|
oldAlternateTransport: oldTransport
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a notification for when a bill is posted.
|
||||||
|
* @param data
|
||||||
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||||
|
*/
|
||||||
|
const billPostedBuilder = (data) => {
|
||||||
const facing = data?.data?.isinhouse ? "in-house" : "vendor";
|
const facing = data?.data?.isinhouse ? "in-house" : "vendor";
|
||||||
const body = `An ${facing} ${data?.data?.is_credit_memo ? "credit memo" : "bill"} has been posted.`.trim();
|
const body = `An ${facing} ${data?.data?.is_credit_memo ? "credit memo" : "bill"} has been posted.`.trim();
|
||||||
|
|
||||||
const result = {
|
return buildNotification(data, "notifications.job.billPosted", body, {
|
||||||
app: {
|
isInHouse: data?.data?.isinhouse,
|
||||||
jobId: data.jobId,
|
isCreditMemo: data?.data?.is_credit_memo
|
||||||
jobRoNumber: data.jobRoNumber,
|
});
|
||||||
bodyShopId: data.bodyShopId,
|
|
||||||
key: "notifications.job.billPosted",
|
|
||||||
body,
|
|
||||||
variables: {
|
|
||||||
isInHouse: data?.data?.isinhouse,
|
|
||||||
isCreditMemo: data?.data?.is_credit_memo
|
|
||||||
},
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
jobId: data.jobId,
|
|
||||||
jobRoNumber: data.jobRoNumber,
|
|
||||||
bodyShopName: data.bodyShopName,
|
|
||||||
body,
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
fcm: { recipients: [] }
|
|
||||||
};
|
|
||||||
|
|
||||||
populateWatchers(data, result);
|
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds notification data for changes to critical parts status.
|
* Creates a notification for when the status of critical parts changes.
|
||||||
|
* @param data
|
||||||
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||||
*/
|
*/
|
||||||
//
|
|
||||||
const criticalPartsStatusChangedBuilder = (data) => {
|
const criticalPartsStatusChangedBuilder = (data) => {
|
||||||
const body = `The status on a critical part line (${data?.data?.line_desc}) has changed to ${data?.data?.status || "unset"}.`;
|
const lineDesc = data?.data?.line_desc;
|
||||||
|
const status = data?.data?.status;
|
||||||
|
const body = status
|
||||||
|
? `The status on a critical part line (${lineDesc}) has been set to ${status}.`
|
||||||
|
: `The status on a critical part line (${lineDesc}) has been cleared.`;
|
||||||
|
|
||||||
const result = {
|
return buildNotification(data, "notifications.job.criticalPartsStatusChanged", body, {
|
||||||
app: {
|
joblineId: data?.data?.id,
|
||||||
jobId: data.jobId,
|
status: data?.data?.status,
|
||||||
bodyShopId: data.bodyShopId,
|
line_desc: lineDesc
|
||||||
jobRoNumber: data.jobRoNumber,
|
});
|
||||||
key: "notifications.job.criticalPartsStatusChanged",
|
|
||||||
body,
|
|
||||||
variables: {
|
|
||||||
joblineId: data?.data?.id, // If we want to deeplink to the jobline
|
|
||||||
status: data?.data?.status,
|
|
||||||
line_desc: data?.data?.line_desc
|
|
||||||
},
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
jobId: data.jobId,
|
|
||||||
jobRoNumber: data.jobRoNumber,
|
|
||||||
bodyShopName: data.bodyShopName,
|
|
||||||
body,
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
fcm: { recipients: [] }
|
|
||||||
};
|
|
||||||
|
|
||||||
populateWatchers(data, result);
|
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds notification data for completed intake or delivery checklists.
|
* Creates a notification for when the intake or delivery checklist is completed.
|
||||||
|
* @param data
|
||||||
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||||
*/
|
*/
|
||||||
const intakeDeliveryChecklistCompletedBuilder = (data) => {
|
const intakeDeliveryChecklistCompletedBuilder = (data) => {
|
||||||
const checklistType = data?.changedFields?.intakechecklist ? "intake" : "delivery";
|
const checklistType = data?.changedFields?.intakechecklist ? "intake" : "delivery";
|
||||||
const body = `The ${checklistType.charAt(0).toUpperCase() + checklistType.slice(1)} checklist has been completed.`;
|
const body = `The ${checklistType.charAt(0).toUpperCase() + checklistType.slice(1)} checklist has been completed.`;
|
||||||
const result = {
|
|
||||||
app: {
|
|
||||||
jobId: data.jobId,
|
|
||||||
jobRoNumber: data.jobRoNumber,
|
|
||||||
bodyShopId: data.bodyShopId,
|
|
||||||
key: "notifications.job.checklistCompleted",
|
|
||||||
body,
|
|
||||||
variables: {
|
|
||||||
checklistType,
|
|
||||||
completed: true
|
|
||||||
},
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
jobId: data.jobId,
|
|
||||||
jobRoNumber: data.jobRoNumber,
|
|
||||||
bodyShopName: data.bodyShopName,
|
|
||||||
body,
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
fcm: { recipients: [] }
|
|
||||||
};
|
|
||||||
|
|
||||||
populateWatchers(data, result);
|
return buildNotification(data, "notifications.job.checklistCompleted", body, {
|
||||||
return result;
|
checklistType,
|
||||||
|
completed: true
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds notification data for job assignment events.
|
* Creates a notification for when a job is assigned to the user.
|
||||||
|
* @param data
|
||||||
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||||
*/
|
*/
|
||||||
const jobAssignedToMeBuilder = (data) => {
|
const jobAssignedToMeBuilder = (data) => {
|
||||||
const body = `You have been assigned to ${getJobAssignmentType(data.scenarioFields?.[0])}.`;
|
const body = `You have been assigned to ${getJobAssignmentType(data.scenarioFields?.[0])}.`;
|
||||||
const result = {
|
|
||||||
app: {
|
|
||||||
jobId: data.jobId,
|
|
||||||
jobRoNumber: data.jobRoNumber,
|
|
||||||
bodyShopId: data.bodyShopId,
|
|
||||||
key: "notifications.job.assigned",
|
|
||||||
body,
|
|
||||||
variables: {
|
|
||||||
type: data.scenarioFields?.[0]
|
|
||||||
},
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
jobId: data.jobId,
|
|
||||||
jobRoNumber: data.jobRoNumber,
|
|
||||||
bodyShopName: data.bodyShopName,
|
|
||||||
body,
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
fcm: { recipients: [] }
|
|
||||||
};
|
|
||||||
|
|
||||||
populateWatchers(data, result);
|
return buildNotification(data, "notifications.job.assigned", body, {
|
||||||
return result;
|
type: data.scenarioFields?.[0]
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds notification data for jobs added to production.
|
* Creates a notification for when jobs are added to production.
|
||||||
|
* @param data
|
||||||
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||||
*/
|
*/
|
||||||
const jobsAddedToProductionBuilder = (data) => {
|
const jobsAddedToProductionBuilder = (data) => {
|
||||||
const body = `Job is now in production.`;
|
const body = `Job is now in production.`;
|
||||||
const result = {
|
return buildNotification(data, "notifications.job.addedToProduction", body);
|
||||||
app: {
|
|
||||||
jobId: data.jobId,
|
|
||||||
jobRoNumber: data.jobRoNumber,
|
|
||||||
bodyShopId: data.bodyShopId,
|
|
||||||
key: "notifications.job.addedToProduction",
|
|
||||||
body,
|
|
||||||
variables: {},
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
jobId: data.jobId,
|
|
||||||
jobRoNumber: data.jobRoNumber,
|
|
||||||
bodyShopName: data.bodyShopName,
|
|
||||||
body,
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
fcm: { recipients: [] }
|
|
||||||
};
|
|
||||||
|
|
||||||
populateWatchers(data, result);
|
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds notification data for job status changes.
|
* Creates a notification for when the job status changes.
|
||||||
|
* @param data
|
||||||
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||||
*/
|
*/
|
||||||
const jobStatusChangeBuilder = (data) => {
|
const jobStatusChangeBuilder = (data) => {
|
||||||
const body = `The status has changed from ${data?.changedFields?.status?.old || "unset"} to ${data?.changedFields?.status?.new || "unset"}`;
|
const oldStatus = data?.changedFields?.status?.old;
|
||||||
const result = {
|
const newStatus = data?.changedFields?.status?.new;
|
||||||
app: {
|
let body;
|
||||||
jobId: data.jobId,
|
|
||||||
jobRoNumber: data.jobRoNumber,
|
|
||||||
bodyShopId: data.bodyShopId,
|
|
||||||
key: "notifications.job.statusChanged",
|
|
||||||
body,
|
|
||||||
variables: {
|
|
||||||
status: data.changedFields.status.new,
|
|
||||||
oldStatus: data.changedFields.status.old
|
|
||||||
},
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
jobId: data.jobId,
|
|
||||||
jobRoNumber: data.jobRoNumber,
|
|
||||||
bodyShopName: data.bodyShopName,
|
|
||||||
body,
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
fcm: { recipients: [] }
|
|
||||||
};
|
|
||||||
|
|
||||||
populateWatchers(data, result);
|
if (oldStatus && newStatus) body = `The status has been changed from ${oldStatus} to ${newStatus}.`;
|
||||||
return result;
|
else if (!oldStatus && newStatus) body = `The status has been set to ${newStatus}.`;
|
||||||
|
else if (oldStatus && !newStatus) body = `The status has been cleared (previously ${oldStatus}).`;
|
||||||
|
else body = `The status has been updated.`;
|
||||||
|
|
||||||
|
return buildNotification(data, "notifications.job.statusChanged", body, {
|
||||||
|
status: newStatus,
|
||||||
|
oldStatus: oldStatus
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds notification data for new media added or reassigned events.
|
* Creates a notification for when new media is added or reassigned.
|
||||||
|
* @param data
|
||||||
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||||
*/
|
*/
|
||||||
const newMediaAddedReassignedBuilder = (data) => {
|
const newMediaAddedReassignedBuilder = (data) => {
|
||||||
// Determine if it's an image or document
|
|
||||||
const mediaType = data?.data?.type?.startsWith("image") ? "Image" : "Document";
|
const mediaType = data?.data?.type?.startsWith("image") ? "Image" : "Document";
|
||||||
|
const action = data?.data?._documentMoved
|
||||||
// Determine the action
|
? "moved to another job"
|
||||||
let action;
|
: data.isNew
|
||||||
|
? "added"
|
||||||
if (data?.data?._documentMoved) {
|
: data.changedFields?.jobid && data.changedFields.jobid.old !== data.changedFields.jobid.new
|
||||||
action = "moved to another job"; // Special case for document moved from this job
|
? "moved to this job"
|
||||||
} else if (data.isNew) {
|
: "updated";
|
||||||
action = "added"; // New media
|
|
||||||
} else if (data.changedFields?.jobid && data.changedFields.jobid.old !== data.changedFields.jobid.new) {
|
|
||||||
action = "moved to this job";
|
|
||||||
} else {
|
|
||||||
action = "updated";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct the body string
|
|
||||||
const body = `An ${mediaType} has been ${action}.`;
|
const body = `An ${mediaType} has been ${action}.`;
|
||||||
|
|
||||||
const result = {
|
return buildNotification(data, "notifications.job.newMediaAdded", body, {
|
||||||
app: {
|
mediaType,
|
||||||
jobId: data.jobId,
|
action,
|
||||||
jobRoNumber: data.jobRoNumber,
|
movedToJob: data?.data?._movedToJob
|
||||||
bodyShopId: data.bodyShopId,
|
});
|
||||||
key: "notifications.job.newMediaAdded",
|
|
||||||
body,
|
|
||||||
variables: {
|
|
||||||
mediaType,
|
|
||||||
action,
|
|
||||||
movedToJob: data?.data?._movedToJob
|
|
||||||
},
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
jobId: data.jobId,
|
|
||||||
jobRoNumber: data.jobRoNumber,
|
|
||||||
bodyShopName: data.bodyShopName,
|
|
||||||
body,
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
fcm: { recipients: [] }
|
|
||||||
};
|
|
||||||
|
|
||||||
populateWatchers(data, result);
|
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds notification data for new notes added to a job.
|
* Creates a notification for when a new note is added.
|
||||||
|
* @param data
|
||||||
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||||
*/
|
*/
|
||||||
const newNoteAddedBuilder = (data) => {
|
const newNoteAddedBuilder = (data) => {
|
||||||
const body = [
|
const body = [
|
||||||
@@ -318,240 +208,119 @@ const newNoteAddedBuilder = (data) => {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
const result = {
|
return buildNotification(data, "notifications.job.newNoteAdded", body, {
|
||||||
app: {
|
createdBy: data?.data?.created_by,
|
||||||
jobId: data.jobId,
|
critical: data?.data?.critical,
|
||||||
jobRoNumber: data.jobRoNumber,
|
type: data?.data?.type,
|
||||||
bodyShopId: data.bodyShopId,
|
private: data?.data?.private
|
||||||
key: "notifications.job.newNoteAdded",
|
});
|
||||||
body,
|
|
||||||
variables: {
|
|
||||||
createdBy: data?.data?.created_by,
|
|
||||||
critical: data?.data?.critical,
|
|
||||||
type: data?.data?.type,
|
|
||||||
private: data?.data?.private
|
|
||||||
},
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
jobId: data.jobId,
|
|
||||||
jobRoNumber: data.jobRoNumber,
|
|
||||||
bodyShopName: data.bodyShopName,
|
|
||||||
body,
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
fcm: { recipients: [] }
|
|
||||||
};
|
|
||||||
|
|
||||||
populateWatchers(data, result);
|
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds notification data for new time tickets posted.
|
* Creates a notification for when a new time ticket is posted.
|
||||||
|
* @param data
|
||||||
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||||
*/
|
*/
|
||||||
const newTimeTicketPostedBuilder = (data) => {
|
const newTimeTicketPostedBuilder = (data) => {
|
||||||
const type = data?.data?.cost_center;
|
const type = data?.data?.cost_center;
|
||||||
const body = `A ${startCase(type.toLowerCase())} time ticket for ${data?.data?.date} has been posted.`.trim();
|
const body = `A ${startCase(type.toLowerCase())} time ticket for ${data?.data?.date} has been posted.`.trim();
|
||||||
|
|
||||||
const result = {
|
return buildNotification(data, "notifications.job.newTimeTicketPosted", body, {
|
||||||
app: {
|
type,
|
||||||
jobId: data.jobId,
|
date: data?.data?.date
|
||||||
jobRoNumber: data.jobRoNumber,
|
});
|
||||||
bodyShopId: data.bodyShopId,
|
|
||||||
key: "notifications.job.newTimeTicketPosted",
|
|
||||||
body,
|
|
||||||
variables: {
|
|
||||||
type,
|
|
||||||
date: data?.data?.date
|
|
||||||
},
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
jobId: data.jobId,
|
|
||||||
jobRoNumber: data.jobRoNumber,
|
|
||||||
bodyShopName: data.bodyShopName,
|
|
||||||
body,
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
fcm: { recipients: [] }
|
|
||||||
};
|
|
||||||
|
|
||||||
populateWatchers(data, result);
|
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds notification data for parts marked as back-ordered.
|
* Creates a notification for when a part is marked as back-ordered.
|
||||||
|
* @param data
|
||||||
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||||
*/
|
*/
|
||||||
const partMarkedBackOrderedBuilder = (data) => {
|
const partMarkedBackOrderedBuilder = (data) => {
|
||||||
const body = `A part ${data?.data?.line_desc} has been marked as back-ordered.`;
|
const body = `A part ${data?.data?.line_desc} has been marked as back-ordered.`;
|
||||||
const result = {
|
|
||||||
app: {
|
|
||||||
jobId: data.jobId,
|
|
||||||
jobRoNumber: data.jobRoNumber,
|
|
||||||
bodyShopId: data.bodyShopId,
|
|
||||||
key: "notifications.job.partBackOrdered",
|
|
||||||
body,
|
|
||||||
variables: {
|
|
||||||
line_desc: data?.data?.line_desc
|
|
||||||
},
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
jobId: data.jobId,
|
|
||||||
jobRoNumber: data.jobRoNumber,
|
|
||||||
bodyShopName: data.bodyShopName,
|
|
||||||
body,
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
fcm: { recipients: [] }
|
|
||||||
};
|
|
||||||
|
|
||||||
populateWatchers(data, result);
|
return buildNotification(data, "notifications.job.partBackOrdered", body, {
|
||||||
return result;
|
line_desc: data?.data?.line_desc
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds notification data for payment collection events.
|
* Creates a notification for when payment is collected or completed.
|
||||||
|
* @param data
|
||||||
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||||
*/
|
*/
|
||||||
const paymentCollectedCompletedBuilder = (data) => {
|
const paymentCollectedCompletedBuilder = (data) => {
|
||||||
const momentFormat = "MM/DD/YYYY";
|
const momentFormat = "MM/DD/YYYY";
|
||||||
|
const amountDinero = Dinero({ amount: Math.round((data.data.amount || 0) * 100) });
|
||||||
// Format amount using Dinero.js
|
|
||||||
const amountDinero = Dinero({
|
|
||||||
amount: Math.round((data.data.amount || 0) * 100) // Convert to cents, default to 0 if missing
|
|
||||||
});
|
|
||||||
|
|
||||||
const amountFormatted = amountDinero.toFormat();
|
const amountFormatted = amountDinero.toFormat();
|
||||||
|
|
||||||
const payer = data.data.payer;
|
const payer = data.data.payer;
|
||||||
const paymentType = data.data.type;
|
const paymentType = data.data.type;
|
||||||
const paymentDate = moment(data.data.date).format(momentFormat);
|
const paymentDate = moment(data.data.date).format(momentFormat);
|
||||||
|
|
||||||
const body = `Payment of ${amountFormatted} has been collected from ${payer} via ${paymentType} on ${paymentDate}`;
|
const body = `Payment of ${amountFormatted} has been collected from ${payer} via ${paymentType} on ${paymentDate}`;
|
||||||
|
|
||||||
const result = {
|
return buildNotification(data, "notifications.job.paymentCollected", body, {
|
||||||
app: {
|
amount: data.data.amount,
|
||||||
jobId: data.jobId,
|
payer: data.data.payer,
|
||||||
jobRoNumber: data.jobRoNumber,
|
type: data.data.type,
|
||||||
bodyShopId: data.bodyShopId,
|
date: data.data.date
|
||||||
key: "notifications.job.paymentCollected",
|
});
|
||||||
body,
|
|
||||||
variables: {
|
|
||||||
amount: data.data.amount,
|
|
||||||
payer: data.data.payer,
|
|
||||||
type: data.data.type,
|
|
||||||
date: data.data.date
|
|
||||||
},
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
jobId: data.jobId,
|
|
||||||
jobRoNumber: data.jobRoNumber,
|
|
||||||
bodyShopName: data.bodyShopName,
|
|
||||||
body,
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
fcm: { recipients: [] }
|
|
||||||
};
|
|
||||||
|
|
||||||
populateWatchers(data, result);
|
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds notification data for changes to scheduled dates.
|
* Creates a notification for when scheduled dates are changed.
|
||||||
|
* @param data
|
||||||
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||||
*/
|
*/
|
||||||
const scheduledDatesChangedBuilder = (data) => {
|
const scheduledDatesChangedBuilder = (data) => {
|
||||||
const changedFields = data.changedFields;
|
const changedFields = data.changedFields;
|
||||||
|
|
||||||
// Define field configurations
|
|
||||||
const fieldConfigs = {
|
const fieldConfigs = {
|
||||||
scheduled_in: "Scheduled In",
|
scheduled_in: "Scheduled In",
|
||||||
scheduled_completion: "Scheduled Completion",
|
scheduled_completion: "Scheduled Completion",
|
||||||
scheduled_delivery: "Scheduled Delivery"
|
scheduled_delivery: "Scheduled Delivery"
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to format date and time with "at"
|
|
||||||
const formatDateTime = (date) => {
|
const formatDateTime = (date) => {
|
||||||
if (!date) return "unset";
|
if (!date) return "(no date set)";
|
||||||
const formatted = moment(date).tz(data.bodyShopTimezone);
|
const formatted = moment(date).tz(data.bodyShopTimezone);
|
||||||
const datePart = formatted.format("MM/DD/YYYY");
|
return `${formatted.format("MM/DD/YYYY")} at ${formatted.format("hh:mm a")}`;
|
||||||
const timePart = formatted.format("hh:mm a");
|
|
||||||
return `${datePart} at ${timePart}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build field messages dynamically
|
|
||||||
const fieldMessages = Object.entries(fieldConfigs)
|
const fieldMessages = Object.entries(fieldConfigs)
|
||||||
.filter(([field]) => changedFields[field]) // Only include changed fields
|
.filter(([field]) => changedFields[field])
|
||||||
.map(([field, label]) => {
|
.map(([field, label]) => {
|
||||||
const { old, new: newValue } = changedFields[field];
|
const { old, new: newValue } = changedFields[field];
|
||||||
|
if (old && !newValue) return `${label} was cancelled (previously ${formatDateTime(old)}).`;
|
||||||
// Case 1: Scheduled date cancelled (from value to null)
|
else if (!old && newValue) return `${label} was set to ${formatDateTime(newValue)}.`;
|
||||||
if (old && !newValue) {
|
else if (old && newValue) return `${label} changed from ${formatDateTime(old)} to ${formatDateTime(newValue)}.`;
|
||||||
return `${label} was cancelled (previously ${formatDateTime(old)}).`;
|
return "";
|
||||||
}
|
|
||||||
// Case 2: Scheduled date set (from null to value)
|
|
||||||
else if (!old && newValue) {
|
|
||||||
return `${label} was set to ${formatDateTime(newValue)}.`;
|
|
||||||
}
|
|
||||||
// Case 3: Scheduled date changed (from value to value)
|
|
||||||
else if (old && newValue) {
|
|
||||||
return `${label} changed from ${formatDateTime(old)} to ${formatDateTime(newValue)}.`;
|
|
||||||
}
|
|
||||||
return ""; // Fallback, though this shouldn't happen with the filter
|
|
||||||
})
|
})
|
||||||
.filter(Boolean); // Remove any empty strings
|
.filter(Boolean);
|
||||||
|
|
||||||
const body = fieldMessages.length > 0 ? fieldMessages.join(" ") : "Scheduled dates have been updated.";
|
const body = fieldMessages.length > 0 ? fieldMessages.join(" ") : "Scheduled dates have been updated.";
|
||||||
|
|
||||||
const result = {
|
return buildNotification(data, "notifications.job.scheduledDatesChanged", body, {
|
||||||
app: {
|
scheduledIn: changedFields.scheduled_in?.new,
|
||||||
jobId: data.jobId,
|
oldScheduledIn: changedFields.scheduled_in?.old,
|
||||||
jobRoNumber: data.jobRoNumber,
|
scheduledCompletion: changedFields.scheduled_completion?.new,
|
||||||
bodyShopId: data.bodyShopId,
|
oldScheduledCompletion: changedFields.scheduled_completion?.old,
|
||||||
key: "notifications.job.scheduledDatesChanged",
|
scheduledDelivery: changedFields.scheduled_delivery?.new,
|
||||||
body,
|
oldScheduledDelivery: changedFields.scheduled_delivery?.old
|
||||||
variables: {
|
});
|
||||||
scheduledIn: changedFields.scheduled_in?.new,
|
|
||||||
oldScheduledIn: changedFields.scheduled_in?.old,
|
|
||||||
scheduledCompletion: changedFields.scheduled_completion?.new,
|
|
||||||
oldScheduledCompletion: changedFields.scheduled_completion?.old,
|
|
||||||
scheduledDelivery: changedFields.scheduled_delivery?.new,
|
|
||||||
oldScheduledDelivery: changedFields.scheduled_delivery?.old
|
|
||||||
},
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
jobId: data.jobId,
|
|
||||||
jobRoNumber: data.jobRoNumber,
|
|
||||||
bodyShopName: data.bodyShopName,
|
|
||||||
body,
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
fcm: { recipients: [] }
|
|
||||||
};
|
|
||||||
|
|
||||||
populateWatchers(data, result);
|
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds notification data for tasks updated or created.
|
* Creates a notification for when tasks are updated or created.
|
||||||
|
* @param data
|
||||||
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||||
*/
|
*/
|
||||||
const tasksUpdatedCreatedBuilder = (data) => {
|
const tasksUpdatedCreatedBuilder = (data) => {
|
||||||
const momentFormat = "MM/DD/YYYY hh:mm a";
|
const momentFormat = "MM/DD/YYYY hh:mm a";
|
||||||
const timezone = data.bodyShopTimezone;
|
const timezone = data.bodyShopTimezone;
|
||||||
const taskTitle = data?.data?.title ? `"${data.data.title}"` : "Unnamed Task";
|
const taskTitle = data?.data?.title ? `"${data.data.title}"` : "Unnamed Task";
|
||||||
|
|
||||||
let body;
|
let body, variables;
|
||||||
let variables;
|
|
||||||
|
|
||||||
if (data.isNew) {
|
if (data.isNew) {
|
||||||
// Created case
|
|
||||||
const priority = formatTaskPriority(data?.data?.priority);
|
const priority = formatTaskPriority(data?.data?.priority);
|
||||||
const createdBy = data?.data?.created_by || "Unknown"; // Fallback for undefined created_by
|
const createdBy = data?.data?.created_by || "Unknown";
|
||||||
const dueDate = data.data.due_date ? ` due on ${moment(data.data.due_date).tz(timezone).format(momentFormat)}` : "";
|
const dueDate = data.data.due_date ? ` due on ${moment(data.data.due_date).tz(timezone).format(momentFormat)}` : "";
|
||||||
const completedOnCreation = data.data.completed === true;
|
const completedOnCreation = data.data.completed === true;
|
||||||
body = `A ${priority} task ${taskTitle} has been created${completedOnCreation ? " and marked completed" : ""} by ${createdBy}${dueDate}.`;
|
body = `A ${priority} task ${taskTitle} has been created${completedOnCreation ? " and marked completed" : ""} by ${createdBy}${dueDate}.`;
|
||||||
@@ -562,119 +331,73 @@ const tasksUpdatedCreatedBuilder = (data) => {
|
|||||||
priority: data?.data?.priority,
|
priority: data?.data?.priority,
|
||||||
createdBy: data?.data?.created_by,
|
createdBy: data?.data?.created_by,
|
||||||
dueDate: data?.data?.due_date,
|
dueDate: data?.data?.due_date,
|
||||||
completed: completedOnCreation ? data?.data?.completed : undefined // Only include if true
|
completed: completedOnCreation ? data?.data?.completed : undefined
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Updated case
|
|
||||||
const changedFields = data.changedFields;
|
const changedFields = data.changedFields;
|
||||||
const fieldNames = Object.keys(changedFields);
|
const fieldNames = Object.keys(changedFields);
|
||||||
const oldTitle = changedFields.title ? `"${changedFields.title.old || "Unnamed Task"}"` : taskTitle;
|
const oldTitle = changedFields.title ? `"${changedFields.title.old || "Unnamed Task"}"` : taskTitle;
|
||||||
|
|
||||||
// Special case: Only 'completed' changed
|
|
||||||
if (fieldNames.length === 1 && changedFields.completed) {
|
if (fieldNames.length === 1 && changedFields.completed) {
|
||||||
body = `Task ${oldTitle} was marked ${changedFields.completed.new ? "complete" : "incomplete"}`;
|
body = `Task ${oldTitle} was marked ${changedFields.completed.new ? "complete" : "incomplete"}`;
|
||||||
variables = {
|
variables = {
|
||||||
isNew: data.isNew,
|
isNew: data.isNew,
|
||||||
roNumber: data.jobRoNumber,
|
roNumber: data.jobRoNumber,
|
||||||
title: data?.data?.title,
|
title: data?.data?.title,
|
||||||
changedCompleted: data?.changedFields?.completed?.new
|
changedCompleted: changedFields.completed.new
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// General update case
|
|
||||||
const fieldMessages = [];
|
const fieldMessages = [];
|
||||||
|
if (changedFields.title)
|
||||||
if (changedFields.title) {
|
|
||||||
fieldMessages.push(`Task ${oldTitle} changed title to "${changedFields.title.new || "unnamed task"}".`);
|
fieldMessages.push(`Task ${oldTitle} changed title to "${changedFields.title.new || "unnamed task"}".`);
|
||||||
}
|
if (changedFields.description) fieldMessages.push("Description updated.");
|
||||||
if (changedFields.description) {
|
if (changedFields.priority)
|
||||||
fieldMessages.push("Description updated.");
|
|
||||||
}
|
|
||||||
if (changedFields.priority) {
|
|
||||||
fieldMessages.push(`Priority changed to ${formatTaskPriority(changedFields.priority.new)}.`);
|
fieldMessages.push(`Priority changed to ${formatTaskPriority(changedFields.priority.new)}.`);
|
||||||
}
|
if (changedFields.due_date)
|
||||||
if (changedFields.due_date) {
|
|
||||||
fieldMessages.push(`Due date set to ${moment(changedFields.due_date.new).tz(timezone).format(momentFormat)}.`);
|
fieldMessages.push(`Due date set to ${moment(changedFields.due_date.new).tz(timezone).format(momentFormat)}.`);
|
||||||
}
|
if (changedFields.completed)
|
||||||
if (changedFields.completed) {
|
|
||||||
fieldMessages.push(`Status changed to ${changedFields.completed.new ? "complete" : "incomplete"}.`);
|
fieldMessages.push(`Status changed to ${changedFields.completed.new ? "complete" : "incomplete"}.`);
|
||||||
}
|
|
||||||
|
|
||||||
body =
|
body =
|
||||||
fieldMessages.length > 0
|
fieldMessages.length > 0
|
||||||
? fieldMessages.length === 1 && changedFields.title
|
? fieldMessages.length === 1 && changedFields.title
|
||||||
? fieldMessages[0] // If only title changed, use it standalone
|
? fieldMessages[0]
|
||||||
: `Task ${oldTitle} updated: ${fieldMessages.join(", ")}`
|
: `Task ${oldTitle} updated: ${fieldMessages.join(", ")}`
|
||||||
: `Task ${oldTitle} has been updated.`;
|
: `Task ${oldTitle} has been updated.`;
|
||||||
variables = {
|
variables = {
|
||||||
isNew: data.isNew,
|
isNew: data.isNew,
|
||||||
roNumber: data.jobRoNumber,
|
roNumber: data.jobRoNumber,
|
||||||
title: data?.data?.title,
|
title: data?.data?.title,
|
||||||
changedTitleOld: data?.changedFields?.title?.old,
|
changedTitleOld: changedFields.title?.old,
|
||||||
changedTitleNew: data?.changedFields?.title?.new,
|
changedTitleNew: changedFields.title?.new,
|
||||||
changedPriority: data?.changedFields?.priority?.new,
|
changedPriority: changedFields.priority?.new,
|
||||||
changedDueDate: data?.changedFields?.due_date?.new,
|
changedDueDate: changedFields.due_date?.new,
|
||||||
changedCompleted: data?.changedFields?.completed?.new
|
changedCompleted: changedFields.completed?.new
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = {
|
return buildNotification(
|
||||||
app: {
|
data,
|
||||||
jobId: data.jobId,
|
data.isNew ? "notifications.job.taskCreated" : "notifications.job.taskUpdated",
|
||||||
jobRoNumber: data.jobRoNumber,
|
body,
|
||||||
bodyShopId: data.bodyShopId,
|
variables
|
||||||
key: data.isNew ? "notifications.job.taskCreated" : "notifications.job.taskUpdated",
|
);
|
||||||
body,
|
|
||||||
variables,
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
jobId: data.jobId,
|
|
||||||
jobRoNumber: data.jobRoNumber,
|
|
||||||
bodyShopName: data.bodyShopName,
|
|
||||||
body,
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
fcm: { recipients: [] }
|
|
||||||
};
|
|
||||||
|
|
||||||
populateWatchers(data, result);
|
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds notification data for supplement imported events.
|
* Creates a notification for when a supplement is imported.
|
||||||
* TODO: This is an advanced case and will be done later
|
* @param data
|
||||||
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||||
*/
|
*/
|
||||||
const supplementImportedBuilder = (data) => {
|
const supplementImportedBuilder = (data) => {
|
||||||
const body = `A supplement has been imported.`;
|
const body = `A supplement has been imported.`;
|
||||||
const result = {
|
return buildNotification(data, "notifications.job.supplementImported", body);
|
||||||
app: {
|
|
||||||
jobId: data.jobId,
|
|
||||||
jobRoNumber: data.jobRoNumber,
|
|
||||||
bodyShopId: data.bodyShopId,
|
|
||||||
key: "notifications.job.supplementImported",
|
|
||||||
body,
|
|
||||||
variables: {},
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
jobId: data.jobId,
|
|
||||||
jobRoNumber: data.jobRoNumber,
|
|
||||||
bodyShopName: data.bodyShopName,
|
|
||||||
body,
|
|
||||||
recipients: []
|
|
||||||
},
|
|
||||||
fcm: { recipients: [] }
|
|
||||||
};
|
|
||||||
|
|
||||||
populateWatchers(data, result);
|
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
alternateTransportChangedBuilder,
|
alternateTransportChangedBuilder,
|
||||||
billPostedHandler,
|
billPostedBuilder,
|
||||||
criticalPartsStatusChangedBuilder,
|
criticalPartsStatusChangedBuilder,
|
||||||
intakeDeliveryChecklistCompletedBuilder,
|
intakeDeliveryChecklistCompletedBuilder,
|
||||||
jobAssignedToMeBuilder,
|
jobAssignedToMeBuilder,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const {
|
const {
|
||||||
jobAssignedToMeBuilder,
|
jobAssignedToMeBuilder,
|
||||||
billPostedHandler,
|
billPostedBuilder,
|
||||||
newNoteAddedBuilder,
|
newNoteAddedBuilder,
|
||||||
scheduledDatesChangedBuilder,
|
scheduledDatesChangedBuilder,
|
||||||
tasksUpdatedCreatedBuilder,
|
tasksUpdatedCreatedBuilder,
|
||||||
@@ -30,10 +30,12 @@ const { isFunction } = require("lodash");
|
|||||||
* - builder {Function}: A function to handle the scenario.
|
* - builder {Function}: A function to handle the scenario.
|
||||||
* - onlyTruthyValues {boolean|Array<string>}: Specifies fields that must have truthy values for the scenario to match.
|
* - onlyTruthyValues {boolean|Array<string>}: Specifies fields that must have truthy values for the scenario to match.
|
||||||
* - filterCallback {Function}: Optional callback (sync or async) to further filter the scenario based on event data (returns boolean).
|
* - filterCallback {Function}: Optional callback (sync or async) to further filter the scenario based on event data (returns boolean).
|
||||||
|
* - enabled {boolean}: If true, the scenario is active; if false or omitted, the scenario is skipped.
|
||||||
*/
|
*/
|
||||||
const notificationScenarios = [
|
const notificationScenarios = [
|
||||||
{
|
{
|
||||||
key: "job-assigned-to-me",
|
key: "job-assigned-to-me",
|
||||||
|
enabled: true,
|
||||||
table: "jobs",
|
table: "jobs",
|
||||||
fields: ["employee_prep", "employee_body", "employee_csr", "employee_refinish"],
|
fields: ["employee_prep", "employee_body", "employee_csr", "employee_refinish"],
|
||||||
matchToUserFields: ["employee_prep", "employee_body", "employee_csr", "employee_refinish"],
|
matchToUserFields: ["employee_prep", "employee_body", "employee_csr", "employee_refinish"],
|
||||||
@@ -41,24 +43,28 @@ const notificationScenarios = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "bill-posted",
|
key: "bill-posted",
|
||||||
|
enabled: true,
|
||||||
table: "bills",
|
table: "bills",
|
||||||
builder: billPostedHandler,
|
builder: billPostedBuilder,
|
||||||
onNew: true
|
onNew: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "new-note-added",
|
key: "new-note-added",
|
||||||
|
enabled: true,
|
||||||
table: "notes",
|
table: "notes",
|
||||||
builder: newNoteAddedBuilder,
|
builder: newNoteAddedBuilder,
|
||||||
onNew: true
|
onNew: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "schedule-dates-changed",
|
key: "schedule-dates-changed",
|
||||||
|
enabled: true,
|
||||||
table: "jobs",
|
table: "jobs",
|
||||||
fields: ["scheduled_in", "scheduled_completion", "scheduled_delivery"],
|
fields: ["scheduled_in", "scheduled_completion", "scheduled_delivery"],
|
||||||
builder: scheduledDatesChangedBuilder
|
builder: scheduledDatesChangedBuilder
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "tasks-updated-created",
|
key: "tasks-updated-created",
|
||||||
|
enabled: true,
|
||||||
table: "tasks",
|
table: "tasks",
|
||||||
fields: ["updated_at"],
|
fields: ["updated_at"],
|
||||||
// onNew: true,
|
// onNew: true,
|
||||||
@@ -66,12 +72,14 @@ const notificationScenarios = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "job-status-change",
|
key: "job-status-change",
|
||||||
|
enabled: true,
|
||||||
table: "jobs",
|
table: "jobs",
|
||||||
fields: ["status"],
|
fields: ["status"],
|
||||||
builder: jobStatusChangeBuilder
|
builder: jobStatusChangeBuilder
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "job-added-to-production",
|
key: "job-added-to-production",
|
||||||
|
enabled: true,
|
||||||
table: "jobs",
|
table: "jobs",
|
||||||
fields: ["inproduction"],
|
fields: ["inproduction"],
|
||||||
onlyTruthyValues: ["inproduction"],
|
onlyTruthyValues: ["inproduction"],
|
||||||
@@ -79,36 +87,42 @@ const notificationScenarios = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "alternate-transport-changed",
|
key: "alternate-transport-changed",
|
||||||
|
enabled: true,
|
||||||
table: "jobs",
|
table: "jobs",
|
||||||
fields: ["alt_transport"],
|
fields: ["alt_transport"],
|
||||||
builder: alternateTransportChangedBuilder
|
builder: alternateTransportChangedBuilder
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "new-time-ticket-posted",
|
key: "new-time-ticket-posted",
|
||||||
|
enabled: true,
|
||||||
table: "timetickets",
|
table: "timetickets",
|
||||||
builder: newTimeTicketPostedBuilder
|
builder: newTimeTicketPostedBuilder
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "intake-delivery-checklist-completed",
|
key: "intake-delivery-checklist-completed",
|
||||||
|
enabled: true,
|
||||||
table: "jobs",
|
table: "jobs",
|
||||||
fields: ["intakechecklist", "deliverchecklist"],
|
fields: ["intakechecklist", "deliverchecklist"],
|
||||||
builder: intakeDeliveryChecklistCompletedBuilder
|
builder: intakeDeliveryChecklistCompletedBuilder
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "payment-collected-completed",
|
key: "payment-collected-completed",
|
||||||
|
enabled: true,
|
||||||
table: "payments",
|
table: "payments",
|
||||||
onNew: true,
|
onNew: true,
|
||||||
builder: paymentCollectedCompletedBuilder
|
builder: paymentCollectedCompletedBuilder
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// MAKE SURE YOU ARE NOT ON A LMS ENVIRONMENT
|
// Only works on a non LMS ENV
|
||||||
key: "new-media-added-reassigned",
|
key: "new-media-added-reassigned",
|
||||||
|
enabled: true,
|
||||||
table: "documents",
|
table: "documents",
|
||||||
fields: ["jobid"],
|
fields: ["jobid"],
|
||||||
builder: newMediaAddedReassignedBuilder
|
builder: newMediaAddedReassignedBuilder
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "critical-parts-status-changed",
|
key: "critical-parts-status-changed",
|
||||||
|
enabled: true,
|
||||||
table: "joblines",
|
table: "joblines",
|
||||||
fields: ["status"],
|
fields: ["status"],
|
||||||
onlyTruthyValues: ["status"],
|
onlyTruthyValues: ["status"],
|
||||||
@@ -117,6 +131,7 @@ const notificationScenarios = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "part-marked-back-ordered",
|
key: "part-marked-back-ordered",
|
||||||
|
enabled: true,
|
||||||
table: "joblines",
|
table: "joblines",
|
||||||
fields: ["status"],
|
fields: ["status"],
|
||||||
builder: partMarkedBackOrderedBuilder,
|
builder: partMarkedBackOrderedBuilder,
|
||||||
@@ -133,12 +148,11 @@ const notificationScenarios = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// -------------- Difficult ---------------
|
// Holding off on this one for now, spans multiple tables
|
||||||
// Holding off on this one for now
|
|
||||||
{
|
{
|
||||||
key: "supplement-imported",
|
key: "supplement-imported",
|
||||||
|
enabled: false,
|
||||||
builder: supplementImportedBuilder
|
builder: supplementImportedBuilder
|
||||||
// spans multiple tables,
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -159,6 +173,11 @@ const notificationScenarios = [
|
|||||||
const getMatchingScenarios = async (eventData, getBodyshopFromRedis) => {
|
const getMatchingScenarios = async (eventData, getBodyshopFromRedis) => {
|
||||||
const matches = [];
|
const matches = [];
|
||||||
for (const scenario of notificationScenarios) {
|
for (const scenario of notificationScenarios) {
|
||||||
|
// Check if the scenario is enabled; skip if not explicitly true
|
||||||
|
if (scenario.enabled !== true) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// If eventData has a table, then only scenarios with a table property that matches should be considered.
|
// If eventData has a table, then only scenarios with a table property that matches should be considered.
|
||||||
if (eventData.table) {
|
if (eventData.table) {
|
||||||
if (!scenario.table || eventData.table.name !== scenario.table) {
|
if (!scenario.table || eventData.table.name !== scenario.table) {
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ const scenarioParser = async (req, jobIdField) => {
|
|||||||
} = req;
|
} = req;
|
||||||
|
|
||||||
// Step 1: Validate we know what user committed the action that fired the parser
|
// Step 1: Validate we know what user committed the action that fired the parser
|
||||||
// console.log("Step 1");
|
|
||||||
|
|
||||||
const hasuraUserRole = event?.session_variables?.["x-hasura-role"];
|
const hasuraUserRole = event?.session_variables?.["x-hasura-role"];
|
||||||
const hasuraUserId = event?.session_variables?.["x-hasura-user-id"];
|
const hasuraUserId = event?.session_variables?.["x-hasura-user-id"];
|
||||||
@@ -52,7 +51,6 @@ const scenarioParser = async (req, jobIdField) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Extract just the jobId using the provided jobIdField
|
// Step 2: Extract just the jobId using the provided jobIdField
|
||||||
// console.log("Step 2");
|
|
||||||
|
|
||||||
let jobId = null;
|
let jobId = null;
|
||||||
if (jobIdField) {
|
if (jobIdField) {
|
||||||
@@ -70,7 +68,6 @@ const scenarioParser = async (req, jobIdField) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Query job watchers associated with the job ID using GraphQL
|
// Step 3: Query job watchers associated with the job ID using GraphQL
|
||||||
// console.log("Step 3");
|
|
||||||
|
|
||||||
const watcherData = await gqlClient.request(queries.GET_JOB_WATCHERS, {
|
const watcherData = await gqlClient.request(queries.GET_JOB_WATCHERS, {
|
||||||
jobid: jobId
|
jobid: jobId
|
||||||
@@ -96,7 +93,6 @@ const scenarioParser = async (req, jobIdField) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 5: Perform the full event diff now that we know there are watchers
|
// Step 5: Perform the full event diff now that we know there are watchers
|
||||||
// console.log("Step 5");
|
|
||||||
|
|
||||||
const eventData = await eventParser({
|
const eventData = await eventParser({
|
||||||
newData: event.data.new,
|
newData: event.data.new,
|
||||||
@@ -107,7 +103,6 @@ const scenarioParser = async (req, jobIdField) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Step 6: Extract body shop information from the job data
|
// Step 6: Extract body shop information from the job data
|
||||||
// console.log("Step 6");
|
|
||||||
|
|
||||||
const bodyShopId = watcherData?.job?.bodyshop?.id;
|
const bodyShopId = watcherData?.job?.bodyshop?.id;
|
||||||
const bodyShopName = watcherData?.job?.bodyshop?.shopname;
|
const bodyShopName = watcherData?.job?.bodyshop?.shopname;
|
||||||
@@ -122,7 +117,6 @@ const scenarioParser = async (req, jobIdField) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 7: Identify scenarios that match the event data and job context
|
// Step 7: Identify scenarios that match the event data and job context
|
||||||
// console.log("Step 7");
|
|
||||||
|
|
||||||
const matchingScenarios = await getMatchingScenarios(
|
const matchingScenarios = await getMatchingScenarios(
|
||||||
{
|
{
|
||||||
@@ -155,7 +149,6 @@ const scenarioParser = async (req, jobIdField) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Step 8: Query notification settings for the job watchers
|
// Step 8: Query notification settings for the job watchers
|
||||||
// console.log("Step 8");
|
|
||||||
|
|
||||||
const associationsData = await gqlClient.request(queries.GET_NOTIFICATION_ASSOCIATIONS, {
|
const associationsData = await gqlClient.request(queries.GET_NOTIFICATION_ASSOCIATIONS, {
|
||||||
emails: jobWatchers.map((x) => x.email),
|
emails: jobWatchers.map((x) => x.email),
|
||||||
@@ -173,7 +166,6 @@ const scenarioParser = async (req, jobIdField) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 9: Filter scenario watchers based on their enabled notification methods
|
// Step 9: Filter scenario watchers based on their enabled notification methods
|
||||||
// console.log("Step 9");
|
|
||||||
|
|
||||||
finalScenarioData.matchingScenarios = finalScenarioData.matchingScenarios.map((scenario) => ({
|
finalScenarioData.matchingScenarios = finalScenarioData.matchingScenarios.map((scenario) => ({
|
||||||
...scenario,
|
...scenario,
|
||||||
@@ -213,7 +205,6 @@ const scenarioParser = async (req, jobIdField) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 10: Build and collect scenarios to dispatch notifications for
|
// Step 10: Build and collect scenarios to dispatch notifications for
|
||||||
// console.log("Step 10");
|
|
||||||
|
|
||||||
const scenariosToDispatch = [];
|
const scenariosToDispatch = [];
|
||||||
|
|
||||||
@@ -240,7 +231,6 @@ const scenarioParser = async (req, jobIdField) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 11: Filter scenario fields to include only those that changed
|
// Step 11: Filter scenario fields to include only those that changed
|
||||||
// console.log("Step 11");
|
|
||||||
|
|
||||||
const filteredScenarioFields =
|
const filteredScenarioFields =
|
||||||
scenario.fields?.filter((field) => eventData.changedFieldNames.includes(field)) || [];
|
scenario.fields?.filter((field) => eventData.changedFieldNames.includes(field)) || [];
|
||||||
@@ -274,7 +264,6 @@ const scenarioParser = async (req, jobIdField) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 12: Dispatch email notifications to the email queue
|
// Step 12: Dispatch email notifications to the email queue
|
||||||
// console.log("Step 12");
|
|
||||||
|
|
||||||
const emailsToDispatch = scenariosToDispatch.map((scenario) => scenario?.email);
|
const emailsToDispatch = scenariosToDispatch.map((scenario) => scenario?.email);
|
||||||
if (!isEmpty(emailsToDispatch)) {
|
if (!isEmpty(emailsToDispatch)) {
|
||||||
@@ -287,7 +276,6 @@ const scenarioParser = async (req, jobIdField) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 13: Dispatch app notifications to the app queue
|
// Step 13: Dispatch app notifications to the app queue
|
||||||
// console.log("Step 13");
|
|
||||||
|
|
||||||
const appsToDispatch = scenariosToDispatch.map((scenario) => scenario?.app);
|
const appsToDispatch = scenariosToDispatch.map((scenario) => scenario?.app);
|
||||||
if (!isEmpty(appsToDispatch)) {
|
if (!isEmpty(appsToDispatch)) {
|
||||||
|
|||||||
10
server/utils/devDebugLogger.js
Normal file
10
server/utils/devDebugLogger.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const logger = require("./logger");
|
||||||
|
|
||||||
|
const devDebugLogger = (message, meta) => {
|
||||||
|
if (process.env?.NODE_ENV === "production") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.logger.debug(message, meta);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = devDebugLogger;
|
||||||
3
server/utils/getBullMQPrefix.js
Normal file
3
server/utils/getBullMQPrefix.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
const getBullMQPrefix = () => (process.env?.NODE_ENV === "production" ? "{PROD-BULLMQ}" : "{DEV-BULLMQ}");
|
||||||
|
|
||||||
|
module.exports = getBullMQPrefix;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
const { GET_BODYSHOP_BY_ID } = require("../graphql-client/queries");
|
const { GET_BODYSHOP_BY_ID } = require("../graphql-client/queries");
|
||||||
|
const devDebugLogger = require("./devDebugLogger");
|
||||||
const client = require("../graphql-client/graphql-client").client;
|
const client = require("../graphql-client/graphql-client").client;
|
||||||
|
|
||||||
const BODYSHOP_CACHE_TTL = 3600; // 1 hour
|
const BODYSHOP_CACHE_TTL = 3600; // 1 hour
|
||||||
@@ -10,6 +11,14 @@ const BODYSHOP_CACHE_TTL = 3600; // 1 hour
|
|||||||
*/
|
*/
|
||||||
const getBodyshopCacheKey = (bodyshopId) => `bodyshop-cache:${bodyshopId}`;
|
const getBodyshopCacheKey = (bodyshopId) => `bodyshop-cache:${bodyshopId}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a cache key for a user socket mapping
|
||||||
|
* @param email
|
||||||
|
* @returns {`user:${string}:${string}:socketMapping`}
|
||||||
|
*/
|
||||||
|
const getUserSocketMappingKey = (email) =>
|
||||||
|
`user:${process.env?.NODE_ENV === "production" ? "prod" : "dev"}:${email}:socketMapping`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch bodyshop data from the database
|
* Fetch bodyshop data from the database
|
||||||
* @param bodyshopId
|
* @param bodyshopId
|
||||||
@@ -69,112 +78,17 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store multiple session data in Redis
|
/**
|
||||||
const setMultipleSessionData = async (socketId, keyValues) => {
|
* Add a socket mapping for a user
|
||||||
try {
|
* @param email
|
||||||
// keyValues is expected to be an object { key1: value1, key2: value2, ... }
|
* @param socketId
|
||||||
const entries = Object.entries(keyValues).map(([key, value]) => [key, JSON.stringify(value)]);
|
* @param bodyshopId
|
||||||
await pubClient.hset(`socket:${socketId}`, ...entries.flat());
|
* @returns {Promise<void>}
|
||||||
} catch (error) {
|
*/
|
||||||
logger.log(`Error Setting Multiple Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Retrieve multiple session data from Redis
|
|
||||||
const getMultipleSessionData = async (socketId, keys) => {
|
|
||||||
try {
|
|
||||||
const data = await pubClient.hmget(`socket:${socketId}`, keys);
|
|
||||||
// Redis returns an object with null values for missing keys, so we parse the non-null ones
|
|
||||||
return Object.fromEntries(keys.map((key, index) => [key, data[index] ? JSON.parse(data[index]) : null]));
|
|
||||||
} catch (error) {
|
|
||||||
logger.log(`Error Getting Multiple Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setMultipleFromArraySessionData = async (socketId, keyValueArray) => {
|
|
||||||
try {
|
|
||||||
// Use Redis multi/pipeline to batch the commands
|
|
||||||
const multi = pubClient.multi();
|
|
||||||
keyValueArray.forEach(([key, value]) => {
|
|
||||||
multi.hset(`socket:${socketId}`, key, JSON.stringify(value));
|
|
||||||
});
|
|
||||||
await multi.exec(); // Execute all queued commands
|
|
||||||
} catch (error) {
|
|
||||||
logger.log(`Error Setting Multiple Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to add an item to the end of the Redis list
|
|
||||||
const addItemToEndOfList = async (socketId, key, newItem) => {
|
|
||||||
try {
|
|
||||||
await pubClient.rpush(`socket:${socketId}:${key}`, JSON.stringify(newItem));
|
|
||||||
} catch (error) {
|
|
||||||
let userEmail = "unknown";
|
|
||||||
let socketMappings = {};
|
|
||||||
try {
|
|
||||||
const userData = await getSessionData(socketId, "user");
|
|
||||||
if (userData && userData.email) {
|
|
||||||
userEmail = userData.email;
|
|
||||||
socketMappings = await getUserSocketMapping(userEmail);
|
|
||||||
}
|
|
||||||
} catch (sessionError) {
|
|
||||||
logger.log(`Failed to fetch session data for socket ${socketId}: ${sessionError}`, "ERROR", "redis");
|
|
||||||
}
|
|
||||||
const mappingString = JSON.stringify(socketMappings, null, 2);
|
|
||||||
const errorMessage = `Error adding item to the end of the list for socket ${socketId}: ${error}. User: ${userEmail}, Socket Mappings: ${mappingString}`;
|
|
||||||
logger.log(errorMessage, "ERROR", "redis");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to add an item to the beginning of the Redis list
|
|
||||||
const addItemToBeginningOfList = async (socketId, key, newItem) => {
|
|
||||||
try {
|
|
||||||
await pubClient.lpush(`socket:${socketId}:${key}`, JSON.stringify(newItem));
|
|
||||||
} catch (error) {
|
|
||||||
logger.log(`Error adding item to the beginning of the list for socket ${socketId}: ${error}`, "ERROR", "redis");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to clear a list in Redis
|
|
||||||
const clearList = async (socketId, key) => {
|
|
||||||
try {
|
|
||||||
await pubClient.del(`socket:${socketId}:${key}`);
|
|
||||||
} catch (error) {
|
|
||||||
logger.log(`Error clearing list for socket ${socketId}: ${error}`, "ERROR", "redis");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add methods to manage room users
|
|
||||||
const addUserToRoom = async (room, user) => {
|
|
||||||
try {
|
|
||||||
await pubClient.sadd(room, JSON.stringify(user));
|
|
||||||
} catch (error) {
|
|
||||||
logger.log(`Error adding user to room ${room}: ${error}`, "ERROR", "redis");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeUserFromRoom = async (room, user) => {
|
|
||||||
try {
|
|
||||||
await pubClient.srem(room, JSON.stringify(user));
|
|
||||||
} catch (error) {
|
|
||||||
logger.log(`Error removing user to room ${room}: ${error}`, "ERROR", "redis");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getUsersInRoom = async (room) => {
|
|
||||||
try {
|
|
||||||
const users = await pubClient.smembers(room);
|
|
||||||
return users.map((user) => JSON.parse(user));
|
|
||||||
} catch (error) {
|
|
||||||
logger.log(`Error getting users in room ${room}: ${error}`, "ERROR", "redis");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addUserSocketMapping = async (email, socketId, bodyshopId) => {
|
const addUserSocketMapping = async (email, socketId, bodyshopId) => {
|
||||||
const userKey = `user:${email}`;
|
const socketMappingKey = getUserSocketMappingKey(email);
|
||||||
const socketMappingKey = `${userKey}:socketMapping`;
|
|
||||||
try {
|
try {
|
||||||
logger.log(`Adding socket ${socketId} to user ${email} for bodyshop ${bodyshopId}`, "debug", "redis");
|
devDebugLogger(`Adding socket ${socketId} to user ${email} for bodyshop ${bodyshopId}`);
|
||||||
// Save the mapping: socketId -> bodyshopId
|
// Save the mapping: socketId -> bodyshopId
|
||||||
await pubClient.hset(socketMappingKey, socketId, bodyshopId);
|
await pubClient.hset(socketMappingKey, socketId, bodyshopId);
|
||||||
// Set TTL (24 hours) for the mapping hash
|
// Set TTL (24 hours) for the mapping hash
|
||||||
@@ -184,38 +98,45 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the TTL for a user's socket mapping
|
||||||
|
* @param email
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
const refreshUserSocketTTL = async (email) => {
|
const refreshUserSocketTTL = async (email) => {
|
||||||
const userKey = `user:${email}`;
|
const socketMappingKey = getUserSocketMappingKey(email);
|
||||||
const socketMappingKey = `${userKey}:socketMapping`;
|
|
||||||
try {
|
try {
|
||||||
const exists = await pubClient.exists(socketMappingKey);
|
const exists = await pubClient.exists(socketMappingKey);
|
||||||
if (exists) {
|
if (exists) {
|
||||||
await pubClient.expire(socketMappingKey, 86400);
|
await pubClient.expire(socketMappingKey, 86400);
|
||||||
logger.log(`Refreshed TTL for ${email} socket mapping`, "debug", "redis");
|
devDebugLogger(`Refreshed TTL for ${email} socket mapping`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log(`Error refreshing TTL for ${email}: ${error}`, "ERROR", "redis");
|
logger.log(`Error refreshing TTL for ${email}: ${error}`, "ERROR", "redis");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a socket mapping for a user
|
||||||
|
* @param email
|
||||||
|
* @param socketId
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
const removeUserSocketMapping = async (email, socketId) => {
|
const removeUserSocketMapping = async (email, socketId) => {
|
||||||
const userKey = `user:${email}`;
|
const socketMappingKey = getUserSocketMappingKey(email);
|
||||||
const socketMappingKey = `${userKey}:socketMapping`;
|
|
||||||
try {
|
try {
|
||||||
logger.log(`Removing socket ${socketId} mapping for user ${email}`, "DEBUG", "redis");
|
devDebugLogger(`Removing socket ${socketId} mapping for user ${email}`);
|
||||||
// Look up the bodyshopId associated with this socket
|
// Look up the bodyshopId associated with this socket
|
||||||
const bodyshopId = await pubClient.hget(socketMappingKey, socketId);
|
const bodyshopId = await pubClient.hget(socketMappingKey, socketId);
|
||||||
if (!bodyshopId) {
|
if (!bodyshopId) {
|
||||||
logger.log(`Socket ${socketId} not found for user ${email}`, "DEBUG", "redis");
|
devDebugLogger(`Socket ${socketId} not found for user ${email}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Remove the socket mapping
|
// Remove the socket mapping
|
||||||
await pubClient.hdel(socketMappingKey, socketId);
|
await pubClient.hdel(socketMappingKey, socketId);
|
||||||
logger.log(
|
devDebugLogger(`Removed socket ${socketId} (associated with bodyshop ${bodyshopId}) for user ${email}`);
|
||||||
`Removed socket ${socketId} (associated with bodyshop ${bodyshopId}) for user ${email}`,
|
|
||||||
"DEBUG",
|
|
||||||
"redis"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Refresh TTL if any socket mappings remain
|
// Refresh TTL if any socket mappings remain
|
||||||
const remainingSockets = await pubClient.hlen(socketMappingKey);
|
const remainingSockets = await pubClient.hlen(socketMappingKey);
|
||||||
@@ -227,9 +148,14 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all socket mappings for a user
|
||||||
|
* @param email
|
||||||
|
* @returns {Promise<{}>}
|
||||||
|
*/
|
||||||
const getUserSocketMapping = async (email) => {
|
const getUserSocketMapping = async (email) => {
|
||||||
const userKey = `user:${email}`;
|
const socketMappingKey = getUserSocketMappingKey(email);
|
||||||
const socketMappingKey = `${userKey}:socketMapping`;
|
|
||||||
try {
|
try {
|
||||||
// Retrieve all socket mappings for the user
|
// Retrieve all socket mappings for the user
|
||||||
const mapping = await pubClient.hgetall(socketMappingKey);
|
const mapping = await pubClient.hgetall(socketMappingKey);
|
||||||
@@ -249,9 +175,15 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get socket IDs for a user by bodyshopId
|
||||||
|
* @param email
|
||||||
|
* @param bodyshopId
|
||||||
|
* @returns {Promise<{socketIds: [string, string], ttl: *}>}
|
||||||
|
*/
|
||||||
const getUserSocketMappingByBodyshop = async (email, bodyshopId) => {
|
const getUserSocketMappingByBodyshop = async (email, bodyshopId) => {
|
||||||
const userKey = `user:${email}`;
|
const socketMappingKey = getUserSocketMappingKey(email);
|
||||||
const socketMappingKey = `${userKey}:socketMapping`;
|
|
||||||
try {
|
try {
|
||||||
// Retrieve all socket mappings for the user
|
// Retrieve all socket mappings for the user
|
||||||
const mapping = await pubClient.hgetall(socketMappingKey);
|
const mapping = await pubClient.hgetall(socketMappingKey);
|
||||||
@@ -270,7 +202,11 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get bodyshop data from Redis or fetch from DB if missing
|
/**
|
||||||
|
* Get bodyshop data from Redis
|
||||||
|
* @param bodyshopId
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
const getBodyshopFromRedis = async (bodyshopId) => {
|
const getBodyshopFromRedis = async (bodyshopId) => {
|
||||||
const key = getBodyshopCacheKey(bodyshopId);
|
const key = getBodyshopCacheKey(bodyshopId);
|
||||||
try {
|
try {
|
||||||
@@ -288,7 +224,7 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
|||||||
await pubClient.set(key, jsonData);
|
await pubClient.set(key, jsonData);
|
||||||
await pubClient.expire(key, BODYSHOP_CACHE_TTL);
|
await pubClient.expire(key, BODYSHOP_CACHE_TTL);
|
||||||
|
|
||||||
logger.log("bodyshop-cache-miss", "DEBUG", "redis", null, {
|
devDebugLogger("bodyshop-cache-miss", {
|
||||||
bodyshopId,
|
bodyshopId,
|
||||||
action: "Fetched from DB and cached"
|
action: "Fetched from DB and cached"
|
||||||
});
|
});
|
||||||
@@ -303,14 +239,19 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update or invalidate bodyshop data in Redis
|
/**
|
||||||
|
* Update or invalidate bodyshop data in Redis
|
||||||
|
* @param bodyshopId
|
||||||
|
* @param values
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
const updateOrInvalidateBodyshopFromRedis = async (bodyshopId, values = null) => {
|
const updateOrInvalidateBodyshopFromRedis = async (bodyshopId, values = null) => {
|
||||||
const key = getBodyshopCacheKey(bodyshopId);
|
const key = getBodyshopCacheKey(bodyshopId);
|
||||||
try {
|
try {
|
||||||
if (!values) {
|
if (!values) {
|
||||||
// Invalidate cache by deleting the key
|
// Invalidate cache by deleting the key
|
||||||
await pubClient.del(key);
|
await pubClient.del(key);
|
||||||
logger.log("bodyshop-cache-invalidate", "DEBUG", "api", "redis", {
|
devDebugLogger("bodyshop-cache-invalidate", {
|
||||||
bodyshopId,
|
bodyshopId,
|
||||||
action: "Cache invalidated"
|
action: "Cache invalidated"
|
||||||
});
|
});
|
||||||
@@ -319,7 +260,7 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
|||||||
const jsonData = JSON.stringify(values);
|
const jsonData = JSON.stringify(values);
|
||||||
await pubClient.set(key, jsonData);
|
await pubClient.set(key, jsonData);
|
||||||
await pubClient.expire(key, BODYSHOP_CACHE_TTL);
|
await pubClient.expire(key, BODYSHOP_CACHE_TTL);
|
||||||
logger.log("bodyshop-cache-update", "DEBUG", "api", "redis", {
|
devDebugLogger("bodyshop-cache-update", {
|
||||||
bodyshopId,
|
bodyshopId,
|
||||||
action: "Cache updated",
|
action: "Cache updated",
|
||||||
values
|
values
|
||||||
@@ -335,19 +276,118 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// NOTE: The following code was written for an abandoned branch and things have changes since the,
|
||||||
|
// Leaving it here for demonstration purposes, commenting it out so it does not get used
|
||||||
|
|
||||||
|
// Store multiple session data in Redis
|
||||||
|
// const setMultipleSessionData = async (socketId, keyValues) => {
|
||||||
|
// try {
|
||||||
|
// // keyValues is expected to be an object { key1: value1, key2: value2, ... }
|
||||||
|
// const entries = Object.entries(keyValues).map(([key, value]) => [key, JSON.stringify(value)]);
|
||||||
|
// await pubClient.hset(`socket:${socketId}`, ...entries.flat());
|
||||||
|
// } catch (error) {
|
||||||
|
// logger.log(`Error Setting Multiple Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// Retrieve multiple session data from Redis
|
||||||
|
// const getMultipleSessionData = async (socketId, keys) => {
|
||||||
|
// try {
|
||||||
|
// const data = await pubClient.hmget(`socket:${socketId}`, keys);
|
||||||
|
// // Redis returns an object with null values for missing keys, so we parse the non-null ones
|
||||||
|
// return Object.fromEntries(keys.map((key, index) => [key, data[index] ? JSON.parse(data[index]) : null]));
|
||||||
|
// } catch (error) {
|
||||||
|
// logger.log(`Error Getting Multiple Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const setMultipleFromArraySessionData = async (socketId, keyValueArray) => {
|
||||||
|
// try {
|
||||||
|
// // Use Redis multi/pipeline to batch the commands
|
||||||
|
// const multi = pubClient.multi();
|
||||||
|
// keyValueArray.forEach(([key, value]) => {
|
||||||
|
// multi.hset(`socket:${socketId}`, key, JSON.stringify(value));
|
||||||
|
// });
|
||||||
|
// await multi.exec(); // Execute all queued commands
|
||||||
|
// } catch (error) {
|
||||||
|
// logger.log(`Error Setting Multiple Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// Helper function to add an item to the end of the Redis list
|
||||||
|
// const addItemToEndOfList = async (socketId, key, newItem) => {
|
||||||
|
// try {
|
||||||
|
// await pubClient.rpush(`socket:${socketId}:${key}`, JSON.stringify(newItem));
|
||||||
|
// } catch (error) {
|
||||||
|
// let userEmail = "unknown";
|
||||||
|
// let socketMappings = {};
|
||||||
|
// try {
|
||||||
|
// const userData = await getSessionData(socketId, "user");
|
||||||
|
// if (userData && userData.email) {
|
||||||
|
// userEmail = userData.email;
|
||||||
|
// socketMappings = await getUserSocketMapping(userEmail);
|
||||||
|
// }
|
||||||
|
// } catch (sessionError) {
|
||||||
|
// logger.log(`Failed to fetch session data for socket ${socketId}: ${sessionError}`, "ERROR", "redis");
|
||||||
|
// }
|
||||||
|
// const mappingString = JSON.stringify(socketMappings, null, 2);
|
||||||
|
// const errorMessage = `Error adding item to the end of the list for socket ${socketId}: ${error}. User: ${userEmail}, Socket Mappings: ${mappingString}`;
|
||||||
|
// logger.log(errorMessage, "ERROR", "redis");
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// Helper function to add an item to the beginning of the Redis list
|
||||||
|
// const addItemToBeginningOfList = async (socketId, key, newItem) => {
|
||||||
|
// try {
|
||||||
|
// await pubClient.lpush(`socket:${socketId}:${key}`, JSON.stringify(newItem));
|
||||||
|
// } catch (error) {
|
||||||
|
// logger.log(`Error adding item to the beginning of the list for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// Helper function to clear a list in Redis
|
||||||
|
// const clearList = async (socketId, key) => {
|
||||||
|
// try {
|
||||||
|
// await pubClient.del(`socket:${socketId}:${key}`);
|
||||||
|
// } catch (error) {
|
||||||
|
// logger.log(`Error clearing list for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// Add methods to manage room users
|
||||||
|
// const addUserToRoom = async (room, user) => {
|
||||||
|
// try {
|
||||||
|
// await pubClient.sadd(room, JSON.stringify(user));
|
||||||
|
// } catch (error) {
|
||||||
|
// logger.log(`Error adding user to room ${room}: ${error}`, "ERROR", "redis");
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// Remove users from room
|
||||||
|
// const removeUserFromRoom = async (room, user) => {
|
||||||
|
// try {
|
||||||
|
// await pubClient.srem(room, JSON.stringify(user));
|
||||||
|
// } catch (error) {
|
||||||
|
// logger.log(`Error removing user to room ${room}: ${error}`, "ERROR", "redis");
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// Get Users in room
|
||||||
|
// const getUsersInRoom = async (room) => {
|
||||||
|
// try {
|
||||||
|
// const users = await pubClient.smembers(room);
|
||||||
|
// return users.map((user) => JSON.parse(user));
|
||||||
|
// } catch (error) {
|
||||||
|
// logger.log(`Error getting users in room ${room}: ${error}`, "ERROR", "redis");
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
|
getUserSocketMappingKey,
|
||||||
|
getBodyshopCacheKey,
|
||||||
setSessionData,
|
setSessionData,
|
||||||
getSessionData,
|
getSessionData,
|
||||||
clearSessionData,
|
clearSessionData,
|
||||||
setMultipleSessionData,
|
|
||||||
getMultipleSessionData,
|
|
||||||
setMultipleFromArraySessionData,
|
|
||||||
addItemToEndOfList,
|
|
||||||
addItemToBeginningOfList,
|
|
||||||
clearList,
|
|
||||||
addUserToRoom,
|
|
||||||
removeUserFromRoom,
|
|
||||||
getUsersInRoom,
|
|
||||||
addUserSocketMapping,
|
addUserSocketMapping,
|
||||||
removeUserSocketMapping,
|
removeUserSocketMapping,
|
||||||
getUserSocketMappingByBodyshop,
|
getUserSocketMappingByBodyshop,
|
||||||
@@ -355,6 +395,15 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
|||||||
refreshUserSocketTTL,
|
refreshUserSocketTTL,
|
||||||
getBodyshopFromRedis,
|
getBodyshopFromRedis,
|
||||||
updateOrInvalidateBodyshopFromRedis
|
updateOrInvalidateBodyshopFromRedis
|
||||||
|
// setMultipleSessionData,
|
||||||
|
// getMultipleSessionData,
|
||||||
|
// setMultipleFromArraySessionData,
|
||||||
|
// addItemToEndOfList,
|
||||||
|
// addItemToBeginningOfList,
|
||||||
|
// clearList,
|
||||||
|
// addUserToRoom,
|
||||||
|
// removeUserFromRoom,
|
||||||
|
// getUsersInRoom,
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(module.exports, api);
|
Object.assign(module.exports, api);
|
||||||
|
|||||||
Reference in New Issue
Block a user