Compare commits
69 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 | ||
|
|
a6c863f67d | ||
|
|
5fa7377121 | ||
|
|
169b5265c3 | ||
|
|
d56d1f369c | ||
|
|
72ee621303 | ||
|
|
478e5fb569 | ||
|
|
6b047418cc | ||
|
|
87a55028e1 | ||
|
|
8045c228d6 | ||
|
|
b97bc0df8e | ||
|
|
0d80854196 | ||
|
|
cf86430aa9 | ||
|
|
212fc4a7cc | ||
|
|
aa6fc78aa0 | ||
|
|
77e4d72a54 | ||
|
|
1fad3968bb | ||
|
|
9a5a2c7497 | ||
|
|
a492909ad7 | ||
|
|
774f1fea68 | ||
|
|
6e6cabbd63 | ||
|
|
57930005b2 | ||
|
|
e7bbb96dc3 | ||
|
|
ffadd31a5f | ||
|
|
af6139dcaf | ||
|
|
ef22ba3d2c | ||
|
|
f120116e52 | ||
|
|
71dd138f2f | ||
|
|
46af401e9b | ||
|
|
3cbcbb92eb | ||
|
|
1c1f0a16e2 | ||
|
|
ef695776cd | ||
|
|
8b98206e63 | ||
|
|
9b545d6c8c | ||
|
|
14cffd3ad4 | ||
|
|
b4a3960eac | ||
|
|
9567cd88b1 | ||
|
|
e40e0bbb8f | ||
|
|
8fdd07827e | ||
|
|
ac2bb42124 | ||
|
|
b149f70b6f | ||
|
|
7bbbf5934a | ||
|
|
03863ce838 |
@@ -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_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
||||
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_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_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
||||
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_COUNTRY=USA
|
||||
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_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
||||
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_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||
VITE_APP_INSTANCE=IMEX
|
||||
|
||||
@@ -48,8 +48,6 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const scenarioNotificationsOn = client?.getTreatment("Realtime_Notifications_UI") === "on";
|
||||
|
||||
useEffect(() => {
|
||||
if (!navigator.onLine) {
|
||||
setOnline(false);
|
||||
@@ -203,12 +201,7 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
||||
path="/manage/*"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<SocketProvider
|
||||
bodyshop={bodyshop}
|
||||
navigate={navigate}
|
||||
currentUser={currentUser}
|
||||
scenarioNotificationsOn={scenarioNotificationsOn}
|
||||
>
|
||||
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}>
|
||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||
</SocketProvider>
|
||||
</ErrorBoundary>
|
||||
@@ -220,12 +213,7 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
||||
path="/tech/*"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<SocketProvider
|
||||
bodyshop={bodyshop}
|
||||
navigate={navigate}
|
||||
currentUser={currentUser}
|
||||
scenarioNotificationsOn={scenarioNotificationsOn}
|
||||
>
|
||||
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}>
|
||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||
</SocketProvider>
|
||||
</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 {
|
||||
BankFilled,
|
||||
BarChartOutlined,
|
||||
@@ -38,19 +26,31 @@ import {
|
||||
UnorderedListOutlined,
|
||||
UserOutlined
|
||||
} 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 { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar, FaTasks } from "react-icons/fa";
|
||||
import { FiLogOut } from "react-icons/fi";
|
||||
import { GiPayMoney, GiPlayerTime, GiSettingsKnobs } from "react-icons/gi";
|
||||
import { IoBusinessOutline } from "react-icons/io5";
|
||||
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 { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { signOutStart } from "../../redux/user/user.actions";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
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
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -208,25 +208,14 @@ function Header({
|
||||
key: "allpayments",
|
||||
id: "header-accounting-allpayments",
|
||||
icon: <BankFilled />,
|
||||
label: (
|
||||
<Link to="/manage/payments">
|
||||
<LockWrapper featureName="payments" bodyshop={bodyshop}>
|
||||
{t("menus.header.allpayments")}
|
||||
</LockWrapper>
|
||||
</Link>
|
||||
)
|
||||
label: <Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
|
||||
},
|
||||
{
|
||||
key: "enterpayments",
|
||||
id: "header-accounting-enterpayments",
|
||||
icon: <FaCreditCard />,
|
||||
label: (
|
||||
<LockWrapper featureName="payments" bodyshop={bodyshop}>
|
||||
{t("menus.header.enterpayment")}
|
||||
</LockWrapper>
|
||||
),
|
||||
label: t("menus.header.enterpayment"),
|
||||
onClick: () =>
|
||||
HasFeatureAccess({ featureName: "payments", bodyshop }) &&
|
||||
setPaymentContext({
|
||||
actions: {},
|
||||
context: null
|
||||
|
||||
@@ -28,11 +28,11 @@ import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||
import LockerWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
|
||||
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
||||
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
||||
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -775,15 +775,14 @@ export function JobsDetailHeaderActions({
|
||||
key: "enterpayments",
|
||||
id: "job-actions-enterpayments",
|
||||
disabled: !job.converted,
|
||||
label: <LockerWrapperComponent featureName="payments">{t("menus.header.enterpayment")}</LockerWrapperComponent>,
|
||||
label: t("menus.header.enterpayment"),
|
||||
onClick: () => {
|
||||
logImEXEvent("job_header_enter_payment");
|
||||
|
||||
HasFeatureAccess({ featureName: "payments", bodyshop }) &&
|
||||
setPaymentContext({
|
||||
actions: {},
|
||||
context: { jobid: job.id }
|
||||
});
|
||||
setPaymentContext({
|
||||
actions: {},
|
||||
context: { jobid: job.id }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.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 CurrencyInput from "../form-items-formatted/currency-form-item.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 JobsDetailRatesOther from "./jobs-detail-rates.other.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 InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import JobsDetailRatesTaxes from "./jobs-detail-rates.taxes.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
@@ -66,14 +66,48 @@ export function JobsDetailRates({ jobRO, form, job, bodyshop }) {
|
||||
</Space>
|
||||
)}
|
||||
<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 noStyle shouldUpdate={(prev, cur) => prev.auto_add_ats !== cur.auto_add_ats}>
|
||||
{() => {
|
||||
if (form.getFieldValue("auto_add_ats"))
|
||||
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} />
|
||||
</Form.Item>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { Button, Form, Input } from "antd";
|
||||
import React from "react";
|
||||
import { Button, Form, Input, Space } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.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();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form.List name={["md_labor_rates"]}>
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item key={field.key}>
|
||||
<LayoutFormRow>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.labor_rate_desc")}
|
||||
key={`${index}rate_label`}
|
||||
name={[field.name, "rate_label"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_laa")}
|
||||
key={`${index}rate_laa`}
|
||||
name={[field.name, "rate_laa"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_lab")}
|
||||
key={`${index}rate_lab`}
|
||||
name={[field.name, "rate_lab"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_lad")}
|
||||
key={`${index}rate_lad`}
|
||||
name={[field.name, "rate_lad"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_lae")}
|
||||
key={`${index}rate_lae`}
|
||||
name={[field.name, "rate_lae"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_laf")}
|
||||
key={`${index}rate_laf`}
|
||||
name={[field.name, "rate_laf"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_lag")}
|
||||
key={`${index}rate_lag`}
|
||||
name={[field.name, "rate_lag"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_lam")}
|
||||
key={`${index}rate_lam`}
|
||||
name={[field.name, "rate_lam"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_lar")}
|
||||
key={`${index}rate_lar`}
|
||||
name={[field.name, "rate_lar"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_las")}
|
||||
key={`${index}rate_las`}
|
||||
name={[field.name, "rate_las"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_la1")}
|
||||
key={`${index}rate_la1`}
|
||||
name={[field.name, "rate_la1"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_la2")}
|
||||
key={`${index}rate_la2`}
|
||||
name={[field.name, "rate_la2"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_la3")}
|
||||
key={`${index}rate_la3`}
|
||||
name={[field.name, "rate_la3"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_la4")}
|
||||
key={`${index}rate_la4`}
|
||||
name={[field.name, "rate_la4"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_mash")}
|
||||
key={`${index}rate_mash`}
|
||||
name={[field.name, "rate_mash"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_mapa")}
|
||||
key={`${index}rate_mapa`}
|
||||
name={[field.name, "rate_mapa"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_ma2s")}
|
||||
key={`${index}rate_ma2s`}
|
||||
name={[field.name, "rate_ma2s"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_ma3s")}
|
||||
key={`${index}rate_ma3s`}
|
||||
name={[field.name, "rate_ma3s"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
{
|
||||
// <Form.Item
|
||||
// label={t("jobs.fields.rate_mabl")}
|
||||
// key={`${index}rate_mabl`}
|
||||
// name={[field.name, "rate_mabl"]}
|
||||
// rules={[
|
||||
// {
|
||||
// required: true,
|
||||
// //message: t("general.validation.required"),
|
||||
// },
|
||||
// ]}
|
||||
// >
|
||||
// <CurrencyInput min={0} />
|
||||
// </Form.Item>
|
||||
// <Form.Item
|
||||
// label={t("jobs.fields.rate_macs")}
|
||||
// key={`${index}rate_macs`}
|
||||
// name={[field.name, "rate_macs"]}
|
||||
// rules={[
|
||||
// {
|
||||
// required: true,
|
||||
// //message: t("general.validation.required"),
|
||||
// },
|
||||
// ]}
|
||||
// >
|
||||
// <CurrencyInput min={0} />
|
||||
// </Form.Item>
|
||||
}
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_matd")}
|
||||
key={`${index}rate_matd`}
|
||||
name={[field.name, "rate_matd"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_mahw")}
|
||||
key={`${index}rate_mahw`}
|
||||
name={[field.name, "rate_mahw"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<DeleteFilled
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
<FormListMoveArrows move={move} index={index} total={fields.rate_length} />
|
||||
</LayoutFormRow>
|
||||
<>
|
||||
<LayoutFormRow header={t("bodyshop.labels.shoprates")}>
|
||||
<Form.Item label={t("jobs.fields.rate_ats")} name={["shoprates", "rate_ats"]}>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.rate_ats_flat")} name={["shoprates", "rate_ats_flat"]}>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("bodyshop.labels.laborrates")}>
|
||||
<Form.List name={["md_labor_rates"]}>
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item key={field.key}>
|
||||
<LayoutFormRow noDivider={index === 0}>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.labor_rate_desc")}
|
||||
key={`${index}rate_label`}
|
||||
name={[field.name, "rate_label"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_laa")}
|
||||
key={`${index}rate_laa`}
|
||||
name={[field.name, "rate_laa"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_lab")}
|
||||
key={`${index}rate_lab`}
|
||||
name={[field.name, "rate_lab"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_lad")}
|
||||
key={`${index}rate_lad`}
|
||||
name={[field.name, "rate_lad"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_lae")}
|
||||
key={`${index}rate_lae`}
|
||||
name={[field.name, "rate_lae"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_laf")}
|
||||
key={`${index}rate_laf`}
|
||||
name={[field.name, "rate_laf"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_lag")}
|
||||
key={`${index}rate_lag`}
|
||||
name={[field.name, "rate_lag"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_lam")}
|
||||
key={`${index}rate_lam`}
|
||||
name={[field.name, "rate_lam"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_lar")}
|
||||
key={`${index}rate_lar`}
|
||||
name={[field.name, "rate_lar"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_las")}
|
||||
key={`${index}rate_las`}
|
||||
name={[field.name, "rate_las"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_la1")}
|
||||
key={`${index}rate_la1`}
|
||||
name={[field.name, "rate_la1"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_la2")}
|
||||
key={`${index}rate_la2`}
|
||||
name={[field.name, "rate_la2"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_la3")}
|
||||
key={`${index}rate_la3`}
|
||||
name={[field.name, "rate_la3"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_la4")}
|
||||
key={`${index}rate_la4`}
|
||||
name={[field.name, "rate_la4"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_mash")}
|
||||
key={`${index}rate_mash`}
|
||||
name={[field.name, "rate_mash"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_mapa")}
|
||||
key={`${index}rate_mapa`}
|
||||
name={[field.name, "rate_mapa"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_ma2s")}
|
||||
key={`${index}rate_ma2s`}
|
||||
name={[field.name, "rate_ma2s"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_ma3s")}
|
||||
key={`${index}rate_ma3s`}
|
||||
name={[field.name, "rate_ma3s"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
{
|
||||
// <Form.Item
|
||||
// label={t("jobs.fields.rate_mabl")}
|
||||
// key={`${index}rate_mabl`}
|
||||
// name={[field.name, "rate_mabl"]}
|
||||
// rules={[
|
||||
// {
|
||||
// required: true,
|
||||
// //message: t("general.validation.required"),
|
||||
// },
|
||||
// ]}
|
||||
// >
|
||||
// <CurrencyInput min={0} />
|
||||
// </Form.Item>
|
||||
// <Form.Item
|
||||
// label={t("jobs.fields.rate_macs")}
|
||||
// key={`${index}rate_macs`}
|
||||
// name={[field.name, "rate_macs"]}
|
||||
// rules={[
|
||||
// {
|
||||
// required: true,
|
||||
// //message: t("general.validation.required"),
|
||||
// },
|
||||
// ]}
|
||||
// >
|
||||
// <CurrencyInput min={0} />
|
||||
// </Form.Item>
|
||||
}
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_matd")}
|
||||
key={`${index}rate_matd`}
|
||||
name={[field.name, "rate_matd"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.rate_mahw")}
|
||||
key={`${index}rate_mahw`}
|
||||
name={[field.name, "rate_mahw"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<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>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => {
|
||||
add();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
{t("bodyshop.actions.newlaborrate")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
</LayoutFormRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Alert, Form, Switch } from "antd";
|
||||
import React from "react";
|
||||
import { Alert, Form, Select, Switch } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -16,17 +15,17 @@ const mapDispatchToProps = () => ({
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoIntellipay);
|
||||
|
||||
// noinspection JSUnusedLocalSymbols
|
||||
export function ShopInfoIntellipay({bodyshop, form}) {
|
||||
const {t} = useTranslation();
|
||||
export function ShopInfoIntellipay({ bodyshop, form }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item dependencies={[["intellipay_config", "enable_cash_discount"]]}>
|
||||
{() => {
|
||||
const {intellipay_config} = form.getFieldsValue();
|
||||
const { intellipay_config } = form.getFieldsValue();
|
||||
|
||||
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>
|
||||
|
||||
@@ -36,7 +35,93 @@ export function ShopInfoIntellipay({bodyshop, form}) {
|
||||
valuePropName="checked"
|
||||
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>
|
||||
</LayoutFormRow>
|
||||
</>
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "../../graphql/notifications.queries.js";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
|
||||
const SocketContext = createContext(null);
|
||||
|
||||
@@ -25,11 +26,10 @@ const INITIAL_NOTIFICATIONS = 10;
|
||||
* @param bodyshop
|
||||
* @param navigate
|
||||
* @param currentUser
|
||||
* @param scenarioNotificationsOn
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNotificationsOn }) => {
|
||||
const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
||||
const socketRef = useRef(null);
|
||||
const [clientId, setClientId] = useState(null);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
@@ -37,6 +37,14 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNot
|
||||
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
treatments: { Realtime_Notifications_UI }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Realtime_Notifications_UI"],
|
||||
splitKey: bodyshop?.imexshopid
|
||||
});
|
||||
|
||||
const [markNotificationRead] = useMutation(MARK_NOTIFICATION_READ, {
|
||||
update: (cache, { data: { update_notifications } }) => {
|
||||
const timestamp = new Date().toISOString();
|
||||
@@ -209,7 +217,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNot
|
||||
|
||||
const handleNotification = (data) => {
|
||||
// Scenario Notifications have been disabled, bail.
|
||||
if (!scenarioNotificationsOn) {
|
||||
if (Realtime_Notifications_UI?.treatment !== "on") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -329,7 +337,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNot
|
||||
|
||||
const handleSyncNotificationRead = ({ notificationId, timestamp }) => {
|
||||
// Scenario Notifications have been disabled, bail.
|
||||
if (!scenarioNotificationsOn) {
|
||||
if (Realtime_Notifications_UI?.treatment !== "on") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -371,7 +379,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNot
|
||||
|
||||
const handleSyncAllNotificationsRead = ({ timestamp }) => {
|
||||
// Scenario Notifications have been disabled, bail.
|
||||
if (!scenarioNotificationsOn) {
|
||||
if (Realtime_Notifications_UI?.treatment !== "on") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -462,7 +470,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNot
|
||||
markAllNotificationsRead,
|
||||
navigate,
|
||||
currentUser,
|
||||
scenarioNotificationsOn,
|
||||
Realtime_Notifications_UI,
|
||||
t
|
||||
]);
|
||||
|
||||
@@ -474,7 +482,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNot
|
||||
isConnected,
|
||||
markNotificationRead,
|
||||
markAllNotificationsRead,
|
||||
scenarioNotificationsOn
|
||||
scenarioNotificationsOn: Realtime_Notifications_UI?.treatment === "on"
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -509,6 +509,7 @@ export const GET_JOB_BY_PK = gql`
|
||||
est_ct_ln
|
||||
est_ea
|
||||
est_ph1
|
||||
flat_rate_ats
|
||||
federal_tax_rate
|
||||
id
|
||||
inproduction
|
||||
@@ -649,6 +650,7 @@ export const GET_JOB_BY_PK = gql`
|
||||
policy_no
|
||||
production_vars
|
||||
rate_ats
|
||||
rate_ats_flat
|
||||
rate_la1
|
||||
rate_la2
|
||||
rate_la3
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import queryString from "query-string";
|
||||
import React, { useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation } from "react-router-dom";
|
||||
@@ -10,23 +10,17 @@ import PaymentsListPaginated from "../../components/payments-list-paginated/paym
|
||||
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||
import { QUERY_ALL_PAYMENTS_PAGINATED } from "../../graphql/payments.queries";
|
||||
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import FeatureWrapperComponent from "../../components/feature-wrapper/feature-wrapper.component";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import UpsellComponent, { upsellEnum } from "../../components/upsell/upsell.component";
|
||||
import { Card } from "antd";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapStateToProps = createStructuredSelector({});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||
setSelectedHeader: (key) => dispatch(setSelectedHeader(key))
|
||||
});
|
||||
|
||||
export function AllJobs({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
|
||||
export function AllJobs({ setBreadcrumbs, setSelectedHeader }) {
|
||||
const searchParams = queryString.parse(useLocation().search);
|
||||
const { page, sortcolumn, sortorder, searchObj } = searchParams;
|
||||
|
||||
@@ -60,25 +54,15 @@ export function AllJobs({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
return (
|
||||
<FeatureWrapperComponent
|
||||
featureName="payments"
|
||||
noauth={
|
||||
<Card>
|
||||
<UpsellComponent upsell={upsellEnum().payments.general} />
|
||||
</Card>
|
||||
}
|
||||
z
|
||||
>
|
||||
<RbacWrapper action="payments:list">
|
||||
<PaymentsListPaginated
|
||||
refetch={refetch}
|
||||
loading={loading}
|
||||
searchParams={searchParams}
|
||||
total={data ? data.payments_aggregate.aggregate.count : 0}
|
||||
payments={data ? data.payments : []}
|
||||
/>
|
||||
</RbacWrapper>
|
||||
</FeatureWrapperComponent>
|
||||
<RbacWrapper action="payments:list">
|
||||
<PaymentsListPaginated
|
||||
refetch={refetch}
|
||||
loading={loading}
|
||||
searchParams={searchParams}
|
||||
total={data ? data.payments_aggregate.aggregate.count : 0}
|
||||
payments={data ? data.payments : []}
|
||||
/>
|
||||
</RbacWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
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
@@ -3695,6 +3695,7 @@
|
||||
- est_st
|
||||
- est_zip
|
||||
- federal_tax_rate
|
||||
- flat_rate_ats
|
||||
- g_bett_amt
|
||||
- id
|
||||
- inproduction
|
||||
@@ -3789,6 +3790,7 @@
|
||||
- qb_multiple_payers
|
||||
- queued_for_parts
|
||||
- rate_ats
|
||||
- rate_ats_flat
|
||||
- rate_la1
|
||||
- rate_la2
|
||||
- rate_la3
|
||||
@@ -3965,6 +3967,7 @@
|
||||
- est_st
|
||||
- est_zip
|
||||
- federal_tax_rate
|
||||
- flat_rate_ats
|
||||
- g_bett_amt
|
||||
- id
|
||||
- inproduction
|
||||
@@ -4060,6 +4063,7 @@
|
||||
- qb_multiple_payers
|
||||
- queued_for_parts
|
||||
- rate_ats
|
||||
- rate_ats_flat
|
||||
- rate_la1
|
||||
- rate_la2
|
||||
- rate_la3
|
||||
@@ -4247,6 +4251,7 @@
|
||||
- est_st
|
||||
- est_zip
|
||||
- federal_tax_rate
|
||||
- flat_rate_ats
|
||||
- g_bett_amt
|
||||
- id
|
||||
- inproduction
|
||||
@@ -4342,6 +4347,7 @@
|
||||
- qb_multiple_payers
|
||||
- queued_for_parts
|
||||
- rate_ats
|
||||
- rate_ats_flat
|
||||
- rate_la1
|
||||
- rate_la2
|
||||
- rate_la3
|
||||
|
||||
@@ -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 { createAdapter } = require("@socket.io/redis-adapter");
|
||||
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 { applyRedisHelpers } = require("./server/utils/redisHelpers");
|
||||
@@ -393,7 +393,9 @@ const main = async () => {
|
||||
|
||||
const StatusReporter = StartStatusReporter();
|
||||
registerCleanupTask(async () => {
|
||||
StatusReporter.end();
|
||||
if (isFunction(StatusReporter?.end)) {
|
||||
StatusReporter.end();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
@@ -20,6 +20,11 @@ const defaultFooter = () => {
|
||||
|
||||
const now = () => moment().format("MM/DD/YYYY @ hh:mm a");
|
||||
|
||||
/**
|
||||
* Generate the email template
|
||||
* @param strings
|
||||
* @returns {string}
|
||||
*/
|
||||
const generateEmailTemplate = (strings) => {
|
||||
return (
|
||||
`
|
||||
|
||||
@@ -1485,6 +1485,8 @@ exports.GET_JOB_BY_PK = `query GET_JOB_BY_PK($id: uuid!) {
|
||||
materials
|
||||
auto_add_ats
|
||||
rate_ats
|
||||
flat_rate_ats
|
||||
rate_ats_flat
|
||||
joblines(where: { removed: { _eq: false } }){
|
||||
id
|
||||
line_no
|
||||
|
||||
@@ -371,6 +371,7 @@ exports.postback = async (req, res) => {
|
||||
iprequest: values,
|
||||
decodedComment
|
||||
};
|
||||
const ipMapping = req.body?.bodyshop?.intellipay_config?.payment_map;
|
||||
|
||||
logger.log("intellipay-postback-received", "DEBUG", req.user?.email, null, logResponseMeta);
|
||||
|
||||
@@ -417,7 +418,7 @@ exports.postback = async (req, res) => {
|
||||
amount: p.amount,
|
||||
transactionid: values.authcode,
|
||||
payer: "Customer",
|
||||
type: values.cardtype,
|
||||
type: ipMapping[(values.cardtype || "").toLowerCase()] || values.cardtype,
|
||||
jobid: p.jobid,
|
||||
date: moment(Date.now()),
|
||||
payment_responses: {
|
||||
@@ -481,7 +482,7 @@ exports.postback = async (req, res) => {
|
||||
amount: values.total,
|
||||
transactionid: values.authcode,
|
||||
payer: "Customer",
|
||||
type: values.cardtype,
|
||||
type: ipMapping[(values.cardtype || "").toLowerCase()] || values.cardtype,
|
||||
jobid: values.invoice,
|
||||
date: moment(Date.now())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const Dinero = require("dinero.js");
|
||||
const queries = require("../graphql-client/queries");
|
||||
const adminClient = require("../graphql-client/graphql-client").client;
|
||||
const _ = require("lodash");
|
||||
// const adminClient = require("../graphql-client/graphql-client").client;
|
||||
// const _ = require("lodash");
|
||||
const logger = require("../utils/logger");
|
||||
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) {
|
||||
logger.log("job-totals-ssu-USA-error", "ERROR", req?.user?.email, id, {
|
||||
jobid: id,
|
||||
@@ -59,7 +61,7 @@ exports.totalsSsu = async function (req, res) {
|
||||
//IMPORTANT*** These two functions MUST be mirrrored.
|
||||
async function TotalsServerSide(req, res) {
|
||||
const { job, client } = req.body;
|
||||
await AutoAddAtsIfRequired({ job: job, client: client });
|
||||
await AtsAdjustmentsIfRequired({ job: job, client: client, user: req?.user });
|
||||
|
||||
try {
|
||||
let ret = {
|
||||
@@ -138,10 +140,11 @@ async function Totals(req, res) {
|
||||
const client = req.userGraphQLClient;
|
||||
|
||||
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 {
|
||||
let ret = {
|
||||
@@ -153,7 +156,7 @@ async function Totals(req, res) {
|
||||
|
||||
res.status(200).json(ret);
|
||||
} 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,
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
@@ -162,40 +165,45 @@ async function Totals(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
async function AutoAddAtsIfRequired({ job, client }) {
|
||||
//Check if ATS should be automatically added.
|
||||
if (job.auto_add_ats) {
|
||||
//Get the total sum of hours that should be the ATS amount.
|
||||
//Check to see if an ATS line exists.
|
||||
async function AtsAdjustmentsIfRequired({ job, client, user }) {
|
||||
if (job.auto_add_ats || job.flat_rate_ats) {
|
||||
let atsAmount = 0;
|
||||
let atsLineIndex = null;
|
||||
const atsHours = job.joblines.reduce((acc, val, index) => {
|
||||
if (val.line_desc && val.line_desc.toLowerCase() === "ats amount") {
|
||||
atsLineIndex = index;
|
||||
}
|
||||
|
||||
if (
|
||||
val.mod_lbr_ty !== "LA1" &&
|
||||
val.mod_lbr_ty !== "LA2" &&
|
||||
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;
|
||||
}
|
||||
//Check if ATS should be automatically added.
|
||||
if (job.auto_add_ats) {
|
||||
const excludedLaborTypes = new Set(["LAA", "LAG", "LAS", "LAU", "LA1", "LA2", "LA3", "LA4"]);
|
||||
|
||||
return acc;
|
||||
}, 0);
|
||||
//Get the total sum of hours that should be the ATS amount.
|
||||
//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 it does, update it in place, and make sure it is updated for local calculations.
|
||||
if (!excludedLaborTypes.has(val.mod_lbr_ty)) {
|
||||
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) {
|
||||
const newAtsLine = {
|
||||
jobid: job.id,
|
||||
alt_partm: null,
|
||||
line_no: 35,
|
||||
unq_seq: 0,
|
||||
line_ind: "E",
|
||||
line_desc: "ATS Amount",
|
||||
@@ -220,19 +228,42 @@ async function AutoAddAtsIfRequired({ job, client }) {
|
||||
prt_dsmk_m: 0.0
|
||||
};
|
||||
|
||||
const result = await client.request(queries.INSERT_NEW_JOB_LINE, {
|
||||
lineInput: [newAtsLine]
|
||||
});
|
||||
try {
|
||||
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 {
|
||||
const result = await client.request(queries.UPDATE_JOB_LINE, {
|
||||
line: { act_price: atsAmount },
|
||||
lineId: job.joblines[atsLineIndex].id
|
||||
});
|
||||
job.joblines[atsLineIndex].act_price = atsAmount;
|
||||
try {
|
||||
const result = await client.request(queries.UPDATE_JOB_LINE, {
|
||||
line: { act_price: atsAmount },
|
||||
lineId: job.joblines[atsLineIndex].id
|
||||
});
|
||||
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 hasMahwLine = false;
|
||||
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);
|
||||
|
||||
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;
|
||||
|
||||
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 = {};
|
||||
//Track all adjustments that need to be made.
|
||||
|
||||
const linesToAdjustForDiscount = [];
|
||||
//const linesToAdjustForDiscount = [];
|
||||
Object.keys(parts_tax_rates).forEach((key) => {
|
||||
//Check if there's a discount or a mark up.
|
||||
let disc = Dinero(),
|
||||
@@ -1019,7 +1051,9 @@ function CalculateTaxesTotals(job, otherTotals) {
|
||||
}
|
||||
} catch (error) {
|
||||
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;
|
||||
|
||||
//eslint-disable-next-line no-unused-vars
|
||||
function DiscountNotAlreadyCounted(jobline, joblines) {
|
||||
return false;
|
||||
}
|
||||
@@ -1172,27 +1207,35 @@ function IsTrueOrYes(value) {
|
||||
return value === true || value === "Y" || value === "y";
|
||||
}
|
||||
|
||||
async function UpdateJobLines(joblinesToUpdate) {
|
||||
if (joblinesToUpdate.length === 0) return;
|
||||
const updateQueries = joblinesToUpdate.map((line, index) =>
|
||||
generateUpdateQuery(_.pick(line, ["id", "prt_dsmk_m", "prt_dsmk_p"]), index)
|
||||
);
|
||||
const query = `
|
||||
mutation UPDATE_EST_LINES{
|
||||
${updateQueries}
|
||||
}
|
||||
`;
|
||||
// Function not in use from RO to IO Merger 02/05/2024
|
||||
// async function UpdateJobLines(joblinesToUpdate) {
|
||||
// if (joblinesToUpdate.length === 0) return;
|
||||
// const updateQueries = joblinesToUpdate.map((line, index) =>
|
||||
// generateUpdateQuery(_.pick(line, ["id", "prt_dsmk_m", "prt_dsmk_p"]), index)
|
||||
// );
|
||||
// const query = `
|
||||
// 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: "${
|
||||
lineToUpdate.id
|
||||
}" } }, _set: ${JSON.stringify(lineToUpdate).replace(/"(\w+)"\s*:/g, "$1:")}) {
|
||||
returning {
|
||||
id
|
||||
}
|
||||
}`;
|
||||
};
|
||||
// const generateUpdateQuery = (lineToUpdate, index) => {
|
||||
// return `
|
||||
// update_joblines${index}: update_joblines(where: { id: { _eq: "${
|
||||
// lineToUpdate.id
|
||||
// }" } }, _set: ${JSON.stringify(lineToUpdate).replace(/"(\w+)"\s*:/g, "$1:")}) {
|
||||
// returning {
|
||||
// id
|
||||
// }
|
||||
// }`;
|
||||
// };
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
const Dinero = require("dinero.js");
|
||||
const queries = require("../graphql-client/queries");
|
||||
const adminClient = require("../graphql-client/graphql-client").client;
|
||||
const _ = require("lodash");
|
||||
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();
|
||||
} catch (error) {
|
||||
logger.log("job-totals-ssu-error", "ERROR", req.user.email, id, {
|
||||
jobid: id,
|
||||
error
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
res.status(503).send();
|
||||
}
|
||||
@@ -57,7 +60,7 @@ exports.totalsSsu = async function (req, res) {
|
||||
//IMPORTANT*** These two functions MUST be mirrrored.
|
||||
async function TotalsServerSide(req, res) {
|
||||
const { job, client } = req.body;
|
||||
await AutoAddAtsIfRequired({ job: job, client: client });
|
||||
await AtsAdjustmentsIfRequired({ job: job, client: client, user: req?.user });
|
||||
|
||||
try {
|
||||
let ret = {
|
||||
@@ -71,7 +74,8 @@ async function TotalsServerSide(req, res) {
|
||||
} catch (error) {
|
||||
logger.log("job-totals-ssu-error", "ERROR", req?.user?.email, job.id, {
|
||||
jobid: job.id,
|
||||
error
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
res.status(400).send(JSON.stringify(error));
|
||||
}
|
||||
@@ -83,13 +87,12 @@ async function Totals(req, res) {
|
||||
const logger = req.logger;
|
||||
const client = req.userGraphQLClient;
|
||||
|
||||
logger.log("job-totals", "DEBUG", req.user.email, job.id, {
|
||||
jobid: job.id
|
||||
logger.log("job-totals-ssu", "DEBUG", req.user.email, job.id, {
|
||||
jobid: job.id,
|
||||
id: id
|
||||
});
|
||||
|
||||
logger.log("job-totals-ssu", "DEBUG", req.user.email, id, null);
|
||||
|
||||
await AutoAddAtsIfRequired({ job, client });
|
||||
await AtsAdjustmentsIfRequired({ job, client, user: req.user });
|
||||
|
||||
try {
|
||||
let ret = {
|
||||
@@ -101,48 +104,54 @@ async function Totals(req, res) {
|
||||
|
||||
res.status(200).json(ret);
|
||||
} 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,
|
||||
error
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
res.status(400).send(JSON.stringify(error));
|
||||
}
|
||||
}
|
||||
|
||||
async function AutoAddAtsIfRequired({ job, client }) {
|
||||
//Check if ATS should be automatically added.
|
||||
if (job.auto_add_ats) {
|
||||
//Get the total sum of hours that should be the ATS amount.
|
||||
//Check to see if an ATS line exists.
|
||||
async function AtsAdjustmentsIfRequired({ job, client, user }) {
|
||||
if (job.auto_add_ats || job.flat_rate_ats) {
|
||||
let atsAmount = 0;
|
||||
let atsLineIndex = null;
|
||||
const atsHours = job.joblines.reduce((acc, val, index) => {
|
||||
if (val.line_desc && val.line_desc.toLowerCase() === "ats amount") {
|
||||
atsLineIndex = index;
|
||||
}
|
||||
|
||||
if (
|
||||
val.mod_lbr_ty !== "LA1" &&
|
||||
val.mod_lbr_ty !== "LA2" &&
|
||||
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;
|
||||
}
|
||||
//Check if ATS should be automatically added.
|
||||
if (job.auto_add_ats) {
|
||||
const excludedLaborTypes = new Set(["LAA", "LAG", "LAS", "LAU", "LA1", "LA2", "LA3", "LA4"]);
|
||||
|
||||
return acc;
|
||||
}, 0);
|
||||
//Get the total sum of hours that should be the ATS amount.
|
||||
//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 it does, update it in place, and make sure it is updated for local calculations.
|
||||
if (!excludedLaborTypes.has(val.mod_lbr_ty)) {
|
||||
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) {
|
||||
const newAtsLine = {
|
||||
jobid: job.id,
|
||||
alt_partm: null,
|
||||
line_no: 35,
|
||||
unq_seq: 0,
|
||||
line_ind: "E",
|
||||
line_desc: "ATS Amount",
|
||||
@@ -167,22 +176,43 @@ async function AutoAddAtsIfRequired({ job, client }) {
|
||||
prt_dsmk_m: 0.0
|
||||
};
|
||||
|
||||
const result = await client.request(queries.INSERT_NEW_JOB_LINE, {
|
||||
lineInput: [newAtsLine]
|
||||
});
|
||||
try {
|
||||
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 {
|
||||
const result = await client.request(queries.UPDATE_JOB_LINE, {
|
||||
line: { act_price: atsAmount },
|
||||
lineId: job.joblines[atsLineIndex].id
|
||||
});
|
||||
job.joblines[atsLineIndex].act_price = atsAmount;
|
||||
try {
|
||||
const result = await client.request(queries.UPDATE_JOB_LINE, {
|
||||
line: { act_price: atsAmount },
|
||||
lineId: job.joblines[atsLineIndex].id
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ const { Queue, Worker } = require("bullmq");
|
||||
const { INSERT_NOTIFICATIONS_MUTATION } = require("../../graphql-client/queries");
|
||||
const { registerCleanupTask } = require("../../utils/cleanupManager");
|
||||
const getBullMQPrefix = require("../../utils/getBullMQPrefix");
|
||||
const devDebugLogger = require("../../utils/devDebugLogger");
|
||||
const graphQLClient = require("../../graphql-client/graphql-client").client;
|
||||
|
||||
// Base time-related constant in minutes, sourced from environment variable or defaulting to 1
|
||||
@@ -49,7 +50,7 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
||||
const prefix = getBullMQPrefix();
|
||||
const devKey = process.env?.NODE_ENV === "production" ? "prod" : "dev";
|
||||
|
||||
logger.logger.debug(`Initializing Notifications Queues with prefix: ${prefix}`);
|
||||
devDebugLogger(`Initializing Notifications Queues with prefix: ${prefix}`);
|
||||
|
||||
addQueue = new Queue("notificationsAdd", {
|
||||
prefix,
|
||||
@@ -67,7 +68,7 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
||||
"notificationsAdd",
|
||||
async (job) => {
|
||||
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:${devKey}:notifications:${jobId}`;
|
||||
const notification = { key, variables, body, jobRoNumber, timestamp: Date.now() };
|
||||
@@ -79,12 +80,12 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
||||
const notifications = existingNotifications ? JSON.parse(existingNotifications) : [];
|
||||
notifications.push(notification);
|
||||
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:${devKey}:consolidate:${jobId}`;
|
||||
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) {
|
||||
await consolidateQueue.add(
|
||||
@@ -97,10 +98,10 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
||||
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);
|
||||
} else {
|
||||
logger.logger.debug(`Consolidation already scheduled for jobId ${jobId}`);
|
||||
devDebugLogger(`Consolidation already scheduled for jobId ${jobId}`);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -114,24 +115,24 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
||||
"notificationsConsolidate",
|
||||
async (job) => {
|
||||
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 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) {
|
||||
try {
|
||||
const allNotifications = {};
|
||||
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) {
|
||||
const userKey = `${redisKeyPrefix}:${user}`;
|
||||
const notifications = await pubClient.get(userKey);
|
||||
logger.logger.debug(`Retrieved notifications for ${user}: ${notifications}`);
|
||||
devDebugLogger(`Retrieved notifications for ${user}: ${notifications}`);
|
||||
|
||||
if (notifications) {
|
||||
const parsedNotifications = JSON.parse(notifications);
|
||||
@@ -141,13 +142,13 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
||||
allNotifications[user][bodyShopId] = parsedNotifications;
|
||||
}
|
||||
await pubClient.del(userKey);
|
||||
logger.logger.debug(`Deleted Redis key ${userKey}`);
|
||||
devDebugLogger(`Deleted Redis key ${userKey}`);
|
||||
} 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
|
||||
const notificationInserts = [];
|
||||
@@ -174,7 +175,7 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
||||
const insertResponse = await graphQLClient.request(INSERT_NOTIFICATIONS_MUTATION, {
|
||||
objects: notificationInserts
|
||||
});
|
||||
logger.logger.debug(
|
||||
devDebugLogger(
|
||||
`Inserted ${insertResponse.insert_notifications.affected_rows} notifications for jobId ${jobId}`
|
||||
);
|
||||
|
||||
@@ -208,11 +209,11 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
||||
associationId
|
||||
});
|
||||
});
|
||||
logger.logger.debug(
|
||||
devDebugLogger(
|
||||
`Sent ${notifications.length} consolidated notifications to ${user} for jobId ${jobId} with notificationId ${notificationId}`
|
||||
);
|
||||
} else {
|
||||
logger.logger.debug(`No socket IDs found for ${user} in bodyShopId ${bodyShopId}`);
|
||||
devDebugLogger(`No socket IDs found for ${user} in bodyShopId ${bodyShopId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,7 +229,7 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
||||
await pubClient.del(lockKey);
|
||||
}
|
||||
} 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`);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -239,8 +240,9 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
||||
}
|
||||
);
|
||||
|
||||
addWorker.on("completed", (job) => logger.logger.debug(`Add job ${job.id} completed`));
|
||||
consolidateWorker.on("completed", (job) => logger.logger.debug(`Consolidate job ${job.id} completed`));
|
||||
addWorker.on("completed", (job) => devDebugLogger(`Add job ${job.id} completed`));
|
||||
consolidateWorker.on("completed", (job) => devDebugLogger(`Consolidate job ${job.id} completed`));
|
||||
|
||||
addWorker.on("failed", (job, err) =>
|
||||
logger.log(`app-queue-notification-error`, "ERROR", "notifications", "api", {
|
||||
message: err?.message,
|
||||
@@ -256,9 +258,9 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
||||
|
||||
// Register cleanup task instead of direct process listeners
|
||||
const shutdown = async () => {
|
||||
logger.logger.debug("Closing app queue workers...");
|
||||
devDebugLogger("Closing app queue workers...");
|
||||
await Promise.all([addWorker.close(), consolidateWorker.close()]);
|
||||
logger.logger.debug("App queue workers closed");
|
||||
devDebugLogger("App queue workers closed");
|
||||
};
|
||||
|
||||
registerCleanupTask(shutdown);
|
||||
@@ -288,7 +290,7 @@ const dispatchAppsToQueue = async ({ appsToDispatch, logger }) => {
|
||||
{ jobId, bodyShopId, key, variables, recipients, body, jobRoNumber },
|
||||
{ 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`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ const generateEmailTemplate = require("../../email/generateTemplate");
|
||||
const { InstanceEndpoints } = require("../../utils/instanceMgr");
|
||||
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 envValue = process.env?.EMAIL_CONSOLIDATION_DELAY_IN_MINS;
|
||||
@@ -38,7 +40,7 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
||||
const prefix = getBullMQPrefix();
|
||||
const devKey = process.env?.NODE_ENV === "production" ? "prod" : "dev";
|
||||
|
||||
logger.logger.debug(`Initializing Email Notification Queues with prefix: ${prefix}`);
|
||||
devDebugLogger(`Initializing Email Notification Queues with prefix: ${prefix}`);
|
||||
|
||||
// Queue for adding email notifications
|
||||
emailAddQueue = new Queue("emailAdd", {
|
||||
@@ -58,8 +60,8 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
||||
emailAddWorker = new Worker(
|
||||
"emailAdd",
|
||||
async (job) => {
|
||||
const { jobId, jobRoNumber, bodyShopName, body, recipients } = job.data;
|
||||
logger.logger.debug(`Adding email notifications for jobId ${jobId}`);
|
||||
const { jobId, jobRoNumber, bodyShopName, bodyShopTimezone, body, recipients } = job.data;
|
||||
devDebugLogger(`Adding email notifications for jobId ${jobId}`);
|
||||
|
||||
const redisKeyPrefix = `email:${devKey}:notifications:${jobId}`;
|
||||
|
||||
@@ -71,9 +73,10 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
||||
const detailsKey = `email:${devKey}:recipientDetails:${jobId}:${user}`;
|
||||
await pubClient.hsetnx(detailsKey, "firstName", firstName || "");
|
||||
await pubClient.hsetnx(detailsKey, "lastName", lastName || "");
|
||||
await pubClient.hsetnx(detailsKey, "bodyShopTimezone", bodyShopTimezone);
|
||||
await pubClient.expire(detailsKey, NOTIFICATION_EXPIRATION / 1000);
|
||||
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:${devKey}:consolidate:${jobId}`;
|
||||
@@ -81,7 +84,7 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
||||
if (flagSet) {
|
||||
await emailConsolidateQueue.add(
|
||||
"consolidate-emails",
|
||||
{ jobId, jobRoNumber, bodyShopName },
|
||||
{ jobId, jobRoNumber, bodyShopName, bodyShopTimezone },
|
||||
{
|
||||
jobId: `consolidate:${jobId}`,
|
||||
delay: EMAIL_CONSOLIDATION_DELAY,
|
||||
@@ -89,10 +92,10 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
||||
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);
|
||||
} else {
|
||||
logger.logger.debug(`Email consolidation already scheduled for jobId ${jobId}`);
|
||||
devDebugLogger(`Email consolidation already scheduled for jobId ${jobId}`);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -107,7 +110,7 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
||||
"emailConsolidate",
|
||||
async (job) => {
|
||||
const { jobId, jobRoNumber, bodyShopName } = job.data;
|
||||
logger.logger.debug(`Consolidating emails for jobId ${jobId}`);
|
||||
devDebugLogger(`Consolidating emails for jobId ${jobId}`);
|
||||
|
||||
const lockKey = `lock:${devKey}:emailConsolidate:${jobId}`;
|
||||
const lockAcquired = await pubClient.set(lockKey, "locked", "NX", "EX", LOCK_EXPIRATION / 1000);
|
||||
@@ -124,9 +127,11 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
||||
const firstName = details.firstName || "User";
|
||||
const multipleUpdateString = messages.length > 1 ? "Updates" : "Update";
|
||||
const subject = `${multipleUpdateString} for job ${jobRoNumber || "N/A"} at ${bodyShopName}`;
|
||||
const timezone = moment.tz.zone(details?.bodyShopTimezone) ? details.bodyShopTimezone : "UTC";
|
||||
const emailBody = generateEmailTemplate({
|
||||
header: `${multipleUpdateString} for Job ${jobRoNumber || "N/A"}`,
|
||||
subHeader: `Dear ${firstName},`,
|
||||
dateLine: moment().tz(timezone).format("MM/DD/YYYY hh:mm a"),
|
||||
body: `
|
||||
<p>There have been updates to job ${jobRoNumber || "N/A"} at ${bodyShopName}:</p><br/>
|
||||
<ul>
|
||||
@@ -141,7 +146,7 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
||||
type: "html",
|
||||
html: emailBody
|
||||
});
|
||||
logger.logger.debug(
|
||||
devDebugLogger(
|
||||
`Sent consolidated email to ${recipient} for jobId ${jobId} with ${messages.length} updates`
|
||||
);
|
||||
await pubClient.del(userKey);
|
||||
@@ -160,7 +165,7 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
||||
await pubClient.del(lockKey);
|
||||
}
|
||||
} 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`);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -172,8 +177,9 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
||||
);
|
||||
|
||||
// Event handlers for workers
|
||||
emailAddWorker.on("completed", (job) => logger.logger.debug(`Email add job ${job.id} completed`));
|
||||
emailConsolidateWorker.on("completed", (job) => logger.logger.debug(`Email consolidate job ${job.id} completed`));
|
||||
emailAddWorker.on("completed", (job) => devDebugLogger(`Email add job ${job.id} completed`));
|
||||
emailConsolidateWorker.on("completed", (job) => devDebugLogger(`Email consolidate job ${job.id} completed`));
|
||||
|
||||
emailAddWorker.on("failed", (job, err) =>
|
||||
logger.log(`add-email-queue-failed`, "ERROR", "notifications", "api", {
|
||||
message: err?.message,
|
||||
@@ -189,9 +195,9 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
||||
|
||||
// Register cleanup task instead of direct process listeners
|
||||
const shutdown = async () => {
|
||||
logger.logger.debug("Closing email queue workers...");
|
||||
devDebugLogger("Closing email queue workers...");
|
||||
await Promise.all([emailAddWorker.close(), emailConsolidateWorker.close()]);
|
||||
logger.logger.debug("Email queue workers closed");
|
||||
devDebugLogger("Email queue workers closed");
|
||||
};
|
||||
registerCleanupTask(shutdown);
|
||||
}
|
||||
@@ -224,10 +230,10 @@ const dispatchEmailsToQueue = async ({ emailsToDispatch, logger }) => {
|
||||
const emailAddQueue = getQueue();
|
||||
|
||||
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) {
|
||||
logger.logger.warn(
|
||||
devDebugLogger(
|
||||
`Skipping email dispatch for jobId ${jobId} due to missing data: ` +
|
||||
`jobRoNumber=${jobRoNumber || "N/A"}, bodyShopName=${bodyShopName}, body=${body}, recipients=${recipients.length}`
|
||||
);
|
||||
@@ -236,10 +242,10 @@ const dispatchEmailsToQueue = async ({ emailsToDispatch, logger }) => {
|
||||
|
||||
await emailAddQueue.add(
|
||||
"add-email-notification",
|
||||
{ jobId, jobRoNumber, bodyShopName, body, recipients },
|
||||
{ jobId, jobRoNumber, bodyShopName, bodyShopTimezone, body, recipients },
|
||||
{ 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`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ const buildNotification = (data, key, body, variables = {}) => {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopName: data.bodyShopName,
|
||||
bodyShopTimezone: data.bodyShopTimezone,
|
||||
body,
|
||||
recipients: []
|
||||
},
|
||||
|
||||
@@ -248,7 +248,8 @@ async function OpenSearchSearchHandler(req, res) {
|
||||
"*ownr_fn^8",
|
||||
"*ownr_co_nm^8",
|
||||
"*ownr_ph1^8",
|
||||
"*ownr_ph2^8"
|
||||
"*ownr_ph2^8",
|
||||
"*comment^6"
|
||||
// "*"
|
||||
]
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -1,4 +1,5 @@
|
||||
const { GET_BODYSHOP_BY_ID } = require("../graphql-client/queries");
|
||||
const devDebugLogger = require("./devDebugLogger");
|
||||
const client = require("../graphql-client/graphql-client").client;
|
||||
|
||||
const BODYSHOP_CACHE_TTL = 3600; // 1 hour
|
||||
@@ -87,7 +88,7 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
||||
const addUserSocketMapping = async (email, socketId, bodyshopId) => {
|
||||
const socketMappingKey = getUserSocketMappingKey(email);
|
||||
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
|
||||
await pubClient.hset(socketMappingKey, socketId, bodyshopId);
|
||||
// Set TTL (24 hours) for the mapping hash
|
||||
@@ -109,7 +110,7 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
||||
const exists = await pubClient.exists(socketMappingKey);
|
||||
if (exists) {
|
||||
await pubClient.expire(socketMappingKey, 86400);
|
||||
logger.log(`Refreshed TTL for ${email} socket mapping`, "debug", "redis");
|
||||
devDebugLogger(`Refreshed TTL for ${email} socket mapping`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log(`Error refreshing TTL for ${email}: ${error}`, "ERROR", "redis");
|
||||
@@ -126,20 +127,16 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
||||
const socketMappingKey = getUserSocketMappingKey(email);
|
||||
|
||||
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
|
||||
const bodyshopId = await pubClient.hget(socketMappingKey, socketId);
|
||||
if (!bodyshopId) {
|
||||
logger.log(`Socket ${socketId} not found for user ${email}`, "DEBUG", "redis");
|
||||
devDebugLogger(`Socket ${socketId} not found for user ${email}`);
|
||||
return;
|
||||
}
|
||||
// Remove the socket mapping
|
||||
await pubClient.hdel(socketMappingKey, socketId);
|
||||
logger.log(
|
||||
`Removed socket ${socketId} (associated with bodyshop ${bodyshopId}) for user ${email}`,
|
||||
"DEBUG",
|
||||
"redis"
|
||||
);
|
||||
devDebugLogger(`Removed socket ${socketId} (associated with bodyshop ${bodyshopId}) for user ${email}`);
|
||||
|
||||
// Refresh TTL if any socket mappings remain
|
||||
const remainingSockets = await pubClient.hlen(socketMappingKey);
|
||||
@@ -227,7 +224,7 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
||||
await pubClient.set(key, jsonData);
|
||||
await pubClient.expire(key, BODYSHOP_CACHE_TTL);
|
||||
|
||||
logger.log("bodyshop-cache-miss", "DEBUG", "redis", null, {
|
||||
devDebugLogger("bodyshop-cache-miss", {
|
||||
bodyshopId,
|
||||
action: "Fetched from DB and cached"
|
||||
});
|
||||
@@ -254,7 +251,7 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
||||
if (!values) {
|
||||
// Invalidate cache by deleting the key
|
||||
await pubClient.del(key);
|
||||
logger.log("bodyshop-cache-invalidate", "DEBUG", "api", "redis", {
|
||||
devDebugLogger("bodyshop-cache-invalidate", {
|
||||
bodyshopId,
|
||||
action: "Cache invalidated"
|
||||
});
|
||||
@@ -263,7 +260,7 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
||||
const jsonData = JSON.stringify(values);
|
||||
await pubClient.set(key, jsonData);
|
||||
await pubClient.expire(key, BODYSHOP_CACHE_TTL);
|
||||
logger.log("bodyshop-cache-update", "DEBUG", "api", "redis", {
|
||||
devDebugLogger("bodyshop-cache-update", {
|
||||
bodyshopId,
|
||||
action: "Cache updated",
|
||||
values
|
||||
|
||||
Reference in New Issue
Block a user