Compare commits
114 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f50b198c21 | ||
|
|
3495326de3 | ||
|
|
b5973085e7 | ||
|
|
8687214420 | ||
|
|
d61b89a1e5 | ||
|
|
468b42abd2 | ||
|
|
fc03e5f983 | ||
|
|
c4742e38ea | ||
|
|
99e1adbe13 | ||
|
|
eb5c797a43 | ||
|
|
0595c5545e | ||
|
|
55944257aa | ||
|
|
03241778fa | ||
|
|
555b81fb14 | ||
|
|
a56b720e09 | ||
|
|
b89eede164 | ||
|
|
c21cc8d6b9 | ||
|
|
d02a6bc197 | ||
|
|
360c1ce82d | ||
|
|
a7ef02976c | ||
|
|
6a9e36ea4d | ||
|
|
5ebca3ff06 | ||
|
|
37d4c0a40f | ||
|
|
1969a92226 | ||
|
|
8840ffc9ba | ||
|
|
19e42ef397 | ||
|
|
c7eb026986 | ||
|
|
b0dcd3618e | ||
|
|
5f23f135f2 | ||
|
|
159ee7364d | ||
|
|
aa6ad109c9 | ||
|
|
f2a896d568 | ||
|
|
546ebba0bd | ||
|
|
0e75f54d6e | ||
|
|
30f34a17ea | ||
|
|
6035d94404 | ||
|
|
0b7a23d555 | ||
|
|
91fe1f4af9 | ||
|
|
f09cb7b247 | ||
|
|
35a7222f5e | ||
|
|
d444821cf7 | ||
|
|
b5cb520944 | ||
|
|
6814a3bc33 | ||
|
|
19c2b19abc | ||
|
|
22b011139d | ||
|
|
5b30daefe5 | ||
|
|
e015d3574a | ||
|
|
60140902d4 | ||
|
|
84f41b2c11 | ||
|
|
e8b9fcbc6e | ||
|
|
5adf591670 | ||
|
|
f55764e859 | ||
|
|
282fa787a9 | ||
|
|
037efff81c | ||
|
|
e26eb17d09 | ||
|
|
fbea9fde27 | ||
|
|
ce7cf6bdbe | ||
|
|
2c47e5d852 | ||
|
|
a6f809b20a | ||
|
|
2bcad68351 | ||
|
|
6b1b393804 | ||
|
|
c5181d1c5d | ||
|
|
e33ff2a45d | ||
|
|
9eb77964db | ||
|
|
11928d9a7e | ||
|
|
c169bb5d5d | ||
|
|
3cc4f1c63e | ||
|
|
5237b1d535 | ||
|
|
cd56c50cf9 | ||
|
|
a18ce18d72 | ||
|
|
5f66488410 | ||
|
|
d1be7f6e09 | ||
|
|
44f02f28a6 | ||
|
|
6d33622b4e | ||
|
|
f8b8e23ef4 | ||
|
|
db09d09428 | ||
|
|
451820a67c | ||
|
|
ba0ce5027e | ||
|
|
f777d26cc1 | ||
|
|
1463037878 | ||
|
|
7ddec0bb0f | ||
|
|
51c2d3351a | ||
|
|
8323fa6696 | ||
|
|
27a3932c08 | ||
|
|
add88659a4 | ||
|
|
320ad065d0 | ||
|
|
a9bc51949a | ||
|
|
39d1397221 | ||
|
|
b44b71072f | ||
|
|
f3e2a83bab | ||
|
|
0ef030bb89 | ||
|
|
3e9e6baf32 | ||
|
|
c03d45b3fc | ||
|
|
0a9b583c4b | ||
|
|
54ac0c84a7 | ||
|
|
4d59798d8d | ||
|
|
f95dab544d | ||
|
|
41e43dda96 | ||
|
|
cec60db78c | ||
|
|
24d47ae1c5 | ||
|
|
09c4662436 | ||
|
|
9bf6ba9cf0 | ||
|
|
c78b9866a3 | ||
|
|
09c1a8ae35 | ||
|
|
0ef2814de3 | ||
|
|
8e105f0b36 | ||
|
|
ba4da3e35c | ||
|
|
1b8be56c15 | ||
|
|
2b26db78eb | ||
|
|
c2d96922c8 | ||
|
|
70b4ec7948 | ||
|
|
a3ec364034 | ||
|
|
e1728b275b | ||
|
|
10d55df461 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -127,4 +127,6 @@ vitest-report*/
|
|||||||
vitest-coverage/
|
vitest-coverage/
|
||||||
*.vitest.log
|
*.vitest.log
|
||||||
test-output.txt
|
test-output.txt
|
||||||
|
server/job/test/fixtures
|
||||||
|
|
||||||
|
.github
|
||||||
|
|||||||
@@ -56,4 +56,5 @@ COPY . .
|
|||||||
EXPOSE 4000 9229
|
EXPOSE 4000 9229
|
||||||
|
|
||||||
# Start the application
|
# Start the application
|
||||||
CMD ["nodemon", "--legacy-watch", "--inspect=0.0.0.0:9229", "server.js"]
|
RUN echo "Starting the application..."
|
||||||
|
CMD ["nodemon", "--ignore", "./server/job/test/fixtures", "--legacy-watch", "--inspect=0.0.0.0:9229", "server.js"]
|
||||||
|
|||||||
764
_reference/localEmailViewer/package-lock.json
generated
764
_reference/localEmailViewer/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,8 +11,8 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.21.1",
|
"express": "^5.1.0",
|
||||||
"mailparser": "^3.7.1",
|
"mailparser": "^3.7.2",
|
||||||
"node-fetch": "^3.3.2"
|
"node-fetch": "^3.3.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -74,50 +74,8 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<% } %>
|
<% } %>
|
||||||
<script>
|
<script>!function(w,d,i,s){function l(){if(!d.getElementById(i)){var f=d.getElementsByTagName(s)[0],e=d.createElement(s);e.type="text/javascript",e.async=!0,e.src="https://canny.io/sdk.js",f.parentNode.insertBefore(e,f)}}if("function"!=typeof w.Canny){var c=function(){c.q.push(arguments)};c.q=[],w.Canny=c,"complete"===d.readyState?l():w.attachEvent?w.attachEvent("onload",l):w.addEventListener("load",l,!1)}}(window,document,"canny-jssdk","script");</script>
|
||||||
!(function () {
|
|
||||||
"use strict";
|
|
||||||
var e = [
|
|
||||||
"debug",
|
|
||||||
"destroy",
|
|
||||||
"do",
|
|
||||||
"help",
|
|
||||||
"identify",
|
|
||||||
"is",
|
|
||||||
"off",
|
|
||||||
"on",
|
|
||||||
"ready",
|
|
||||||
"render",
|
|
||||||
"reset",
|
|
||||||
"safe",
|
|
||||||
"set"
|
|
||||||
];
|
|
||||||
if (window.noticeable) console.warn("Noticeable SDK code snippet loaded more than once");
|
|
||||||
else {
|
|
||||||
var n = (window.noticeable = window.noticeable || []);
|
|
||||||
|
|
||||||
function t(e) {
|
|
||||||
return function () {
|
|
||||||
var t = Array.prototype.slice.call(arguments);
|
|
||||||
return t.unshift(e), n.push(t), n;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
!(function () {
|
|
||||||
for (var o = 0; o < e.length; o++) {
|
|
||||||
var r = e[o];
|
|
||||||
n[r] = t(r);
|
|
||||||
}
|
|
||||||
})(),
|
|
||||||
(function () {
|
|
||||||
var e = document.createElement("script");
|
|
||||||
(e.async = !0), (e.src = "https://sdk.noticeable.io/l.js");
|
|
||||||
var n = document.head;
|
|
||||||
n.insertBefore(e, n.firstChild);
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
|||||||
3319
client/package-lock.json
generated
3319
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,23 +8,23 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"proxy": "http://localhost:4000",
|
"proxy": "http://localhost:4000",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/pro-layout": "^7.22.3",
|
"@ant-design/pro-layout": "^7.22.4",
|
||||||
"@apollo/client": "^3.13.5",
|
"@apollo/client": "^3.13.6",
|
||||||
"@emotion/is-prop-valid": "^1.3.1",
|
"@emotion/is-prop-valid": "^1.3.1",
|
||||||
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
||||||
"@firebase/analytics": "^0.10.12",
|
"@firebase/analytics": "^0.10.12",
|
||||||
"@firebase/app": "^0.11.3",
|
"@firebase/app": "^0.11.4",
|
||||||
"@firebase/auth": "^1.9.1",
|
"@firebase/auth": "^1.10.0",
|
||||||
"@firebase/firestore": "^4.7.10",
|
"@firebase/firestore": "^4.7.10",
|
||||||
"@firebase/messaging": "^0.12.17",
|
"@firebase/messaging": "^0.12.17",
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
"@reduxjs/toolkit": "^2.6.1",
|
"@reduxjs/toolkit": "^2.6.1",
|
||||||
"@sentry/cli": "^2.42.4",
|
"@sentry/cli": "^2.43.0",
|
||||||
"@sentry/react": "^9.9.0",
|
"@sentry/react": "^9.11.0",
|
||||||
"@sentry/vite-plugin": "^3.2.2",
|
"@sentry/vite-plugin": "^3.3.1",
|
||||||
"@splitsoftware/splitio-react": "^2.0.1",
|
"@splitsoftware/splitio-react": "^2.1.1",
|
||||||
"@tanem/react-nprogress": "^5.0.53",
|
"@tanem/react-nprogress": "^5.0.53",
|
||||||
"antd": "^5.24.5",
|
"antd": "^5.24.6",
|
||||||
"apollo-link-logger": "^2.0.1",
|
"apollo-link-logger": "^2.0.1",
|
||||||
"apollo-link-sentry": "^4.2.0",
|
"apollo-link-sentry": "^4.2.0",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-drag-listview": "^2.0.0",
|
"react-drag-listview": "^2.0.0",
|
||||||
"react-grid-gallery": "^1.0.1",
|
"react-grid-gallery": "^1.0.1",
|
||||||
"react-grid-layout": "1.3.4",
|
"react-grid-layout": "^1.3.4",
|
||||||
"react-i18next": "^15.4.1",
|
"react-i18next": "^15.4.1",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-image-lightbox": "^5.1.4",
|
"react-image-lightbox": "^5.1.4",
|
||||||
@@ -70,16 +70,16 @@
|
|||||||
"react-router-dom": "^6.30.0",
|
"react-router-dom": "^6.30.0",
|
||||||
"react-sticky": "^6.0.3",
|
"react-sticky": "^6.0.3",
|
||||||
"react-virtuoso": "^4.12.5",
|
"react-virtuoso": "^4.12.5",
|
||||||
"recharts": "^2.15.0",
|
"recharts": "^2.15.2",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"redux-actions": "^3.0.3",
|
"redux-actions": "^3.0.3",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"redux-saga": "^1.3.0",
|
"redux-saga": "^1.3.0",
|
||||||
"redux-state-sync": "^3.1.4",
|
"redux-state-sync": "^3.1.4",
|
||||||
"reselect": "^5.1.1",
|
"reselect": "^5.1.1",
|
||||||
"sass": "^1.86.0",
|
"sass": "^1.86.3",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"styled-components": "^6.1.16",
|
"styled-components": "^6.1.17",
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
"subscriptions-transport-ws": "^0.11.0",
|
||||||
"use-memo-one": "^1.1.3",
|
"use-memo-one": "^1.1.3",
|
||||||
"vite-plugin-ejs": "^1.7.0",
|
"vite-plugin-ejs": "^1.7.0",
|
||||||
@@ -130,22 +130,22 @@
|
|||||||
"@ant-design/icons": "^6.0.0",
|
"@ant-design/icons": "^6.0.0",
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-react": "^7.26.3",
|
"@babel/preset-react": "^7.26.3",
|
||||||
"@dotenvx/dotenvx": "^1.39.0",
|
"@dotenvx/dotenvx": "^1.39.1",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@eslint/js": "^9.23.0",
|
"@eslint/js": "^9.24.0",
|
||||||
"@playwright/test": "^1.51.1",
|
"@playwright/test": "^1.51.1",
|
||||||
"@sentry/webpack-plugin": "^3.2.2",
|
"@sentry/webpack-plugin": "^3.3.1",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.2.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"browserslist": "^4.24.4",
|
"browserslist": "^4.24.4",
|
||||||
"browserslist-to-esbuild": "^2.1.1",
|
"browserslist-to-esbuild": "^2.1.1",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"eslint-plugin-react": "^7.37.4",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
"memfs": "^4.17.0",
|
"memfs": "^4.17.0",
|
||||||
@@ -154,13 +154,13 @@
|
|||||||
"react-error-overlay": "^6.1.0",
|
"react-error-overlay": "^6.1.0",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"source-map-explorer": "^2.5.3",
|
"source-map-explorer": "^2.5.3",
|
||||||
"vite": "^6.2.3",
|
"vite": "^6.2.5",
|
||||||
"vite-plugin-babel": "^1.3.0",
|
"vite-plugin-babel": "^1.3.0",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-node-polyfills": "^0.23.0",
|
"vite-plugin-node-polyfills": "^0.23.0",
|
||||||
"vite-plugin-pwa": "^0.21.2",
|
"vite-plugin-pwa": "^1.0.0",
|
||||||
"vite-plugin-style-import": "^2.0.0",
|
"vite-plugin-style-import": "^2.0.0",
|
||||||
"vitest": "^3.0.9",
|
"vitest": "^3.1.1",
|
||||||
"workbox-window": "^7.3.0"
|
"workbox-window": "^7.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ import "./App.styles.scss";
|
|||||||
import Eula from "../components/eula/eula.component";
|
import Eula from "../components/eula/eula.component";
|
||||||
import InstanceRenderMgr from "../utils/instanceRenderMgr";
|
import InstanceRenderMgr from "../utils/instanceRenderMgr";
|
||||||
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
|
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
|
||||||
import { SocketProvider } from "../contexts/SocketIO/useSocket.jsx";
|
|
||||||
import { NotificationProvider } from "../contexts/Notifications/notificationContext.jsx";
|
import { NotificationProvider } from "../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import SocketProvider from "../contexts/SocketIO/socketProvider.jsx";
|
||||||
|
|
||||||
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
|
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
|
||||||
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
|
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
|
||||||
@@ -142,11 +142,10 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
|||||||
>
|
>
|
||||||
<ProductFruitsWrapper
|
<ProductFruitsWrapper
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
workspaceCode={InstanceRenderMgr({
|
bodyshop={bodyshop}
|
||||||
imex: null,
|
workspaceCode={bodyshop?.tours_enabled ? "9BkbEseqNqxw8jUH" : ""}
|
||||||
rome: "9BkbEseqNqxw8jUH"
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { ProductFruits } from "react-product-fruits";
|
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
import { ProductFruits } from "react-product-fruits";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
const ProductFruitsWrapper = React.memo(({ currentUser, bodyshop, workspaceCode }) => {
|
||||||
|
const featureProps = bodyshop?.features
|
||||||
|
? Object.entries(bodyshop.features).reduce((acc, [key, value]) => {
|
||||||
|
acc[key] = value === true || (typeof value === "string" && dayjs(value).isAfter(dayjs()));
|
||||||
|
return acc;
|
||||||
|
}, {})
|
||||||
|
: {};
|
||||||
|
|
||||||
const ProductFruitsWrapper = React.memo(({ currentUser, workspaceCode }) => {
|
|
||||||
return (
|
return (
|
||||||
workspaceCode &&
|
workspaceCode &&
|
||||||
currentUser?.authorized === true &&
|
currentUser?.authorized === true &&
|
||||||
@@ -14,7 +22,8 @@ const ProductFruitsWrapper = React.memo(({ currentUser, workspaceCode }) => {
|
|||||||
language="en"
|
language="en"
|
||||||
user={{
|
user={{
|
||||||
email: currentUser.email,
|
email: currentUser.email,
|
||||||
username: currentUser.email
|
username: currentUser.email,
|
||||||
|
props: featureProps
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -28,5 +37,6 @@ ProductFruitsWrapper.propTypes = {
|
|||||||
authorized: PropTypes.bool,
|
authorized: PropTypes.bool,
|
||||||
email: PropTypes.string
|
email: PropTypes.string
|
||||||
}),
|
}),
|
||||||
workspaceCode: PropTypes.string
|
workspaceCode: PropTypes.string,
|
||||||
|
bodyshop: PropTypes.object
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { getToken } from "@firebase/messaging";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
|
||||||
import { messaging, requestForToken } from "../../firebase/firebase.utils";
|
import { messaging, requestForToken } from "../../firebase/firebase.utils";
|
||||||
import ChatPopupComponent from "../chat-popup/chat-popup.component";
|
import ChatPopupComponent from "../chat-popup/chat-popup.component";
|
||||||
import "./chat-affix.styles.scss";
|
import "./chat-affix.styles.scss";
|
||||||
import { registerMessagingHandlers, unregisterMessagingHandlers } from "./registerMessagingSocketHandlers";
|
import { registerMessagingHandlers, unregisterMessagingHandlers } from "./registerMessagingSocketHandlers";
|
||||||
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
|
||||||
export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import { Button } from "antd";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { TOGGLE_CONVERSATION_ARCHIVE } from "../../graphql/conversations.queries";
|
import { TOGGLE_CONVERSATION_ARCHIVE } from "../../graphql/conversations.queries";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import { Link } from "react-router-dom";
|
|||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import axios from "axios";
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
|
||||||
import { CONVERSATION_SUBSCRIPTION_BY_PK, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries";
|
import { CONVERSATION_SUBSCRIPTION_BY_PK, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries";
|
||||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import ChatConversationComponent from "./chat-conversation.component";
|
import ChatConversationComponent from "./chat-conversation.component";
|
||||||
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
selectedConversation: selectSelectedConversation,
|
selectedConversation: selectSelectedConversation,
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import { Input, Spin, Tag, Tooltip } from "antd";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { UPDATE_CONVERSATION_LABEL } from "../../graphql/conversations.queries";
|
import { UPDATE_CONVERSATION_LABEL } from "../../graphql/conversations.queries";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { connect } from "react-redux";
|
|||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
||||||
import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
|
import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
|||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { searchingForConversation } from "../../redux/messaging/messaging.selectors";
|
import { searchingForConversation } from "../../redux/messaging/messaging.selectors";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import ChatConversationListComponent from "../chat-conversation-list/chat-conver
|
|||||||
import ChatConversationContainer from "../chat-conversation/chat-conversation.container";
|
import ChatConversationContainer from "../chat-conversation/chat-conversation.container";
|
||||||
import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component";
|
import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
|
||||||
|
|
||||||
import "./chat-popup.styles.scss";
|
import "./chat-popup.styles.scss";
|
||||||
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
selectedConversation: selectSelectedConversation,
|
selectedConversation: selectSelectedConversation,
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
|||||||
import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||||
import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries";
|
import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries";
|
||||||
import ChatTagRo from "./chat-tag-ro.component";
|
import ChatTagRo from "./chat-tag-ro.component";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ import { RiSurveyLine } from "react-icons/ri";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
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";
|
||||||
@@ -51,6 +50,7 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
|||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
import LockWrapper from "../lock-wrapper/lock-wrapper.component";
|
import LockWrapper from "../lock-wrapper/lock-wrapper.component";
|
||||||
import NotificationCenterContainer from "../notification-center/notification-center.container.jsx";
|
import NotificationCenterContainer from "../notification-center/notification-center.container.jsx";
|
||||||
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
|
||||||
// Redux mappings
|
// Redux mappings
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AlertFilled } from "@ant-design/icons";
|
import { AlertFilled } from "@ant-design/icons";
|
||||||
import { useMutation } from "@apollo/client";
|
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||||
import { Button, Divider, Dropdown, Form, Input, Popover, Select, Space } from "antd";
|
import { Button, Divider, Dropdown, Form, Input, Popover, Select, Space } from "antd";
|
||||||
import parsePhoneNumber from "libphonenumber-js";
|
import parsePhoneNumber from "libphonenumber-js";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
@@ -8,24 +8,30 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
|
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
|
||||||
|
import { GET_JOB_BY_PK_QUICK_INTAKE, JOB_PRODUCTION_TOGGLE } from "../../graphql/jobs.queries";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
|
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||||
import DataLabel from "../data-label/data-label.component";
|
import DataLabel from "../data-label/data-label.component";
|
||||||
|
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
|
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
|
||||||
import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component";
|
import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component";
|
||||||
|
import { HasFeatureAccess } from "./../feature-wrapper/feature-wrapper.component";
|
||||||
import ScheduleAtChange from "./job-at-change.component";
|
import ScheduleAtChange from "./job-at-change.component";
|
||||||
import ScheduleEventColor from "./schedule-event.color.component";
|
import ScheduleEventColor from "./schedule-event.color.component";
|
||||||
import ScheduleEventNote from "./schedule-event.note.component";
|
import ScheduleEventNote from "./schedule-event.note.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -33,7 +39,8 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setScheduleContext: (context) => dispatch(setModalContext({ context: context, modal: "schedule" })),
|
setScheduleContext: (context) => dispatch(setModalContext({ context: context, modal: "schedule" })),
|
||||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
||||||
setMessage: (text) => dispatch(setMessage(text))
|
setMessage: (text) => dispatch(setMessage(text)),
|
||||||
|
insertAuditTrail: ({ jobid, operation }) => dispatch(insertAuditTrail({ jobid, operation }))
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ScheduleEventComponent({
|
export function ScheduleEventComponent({
|
||||||
@@ -43,16 +50,41 @@ export function ScheduleEventComponent({
|
|||||||
event,
|
event,
|
||||||
refetch,
|
refetch,
|
||||||
handleCancel,
|
handleCancel,
|
||||||
setScheduleContext
|
setScheduleContext,
|
||||||
|
insertAuditTrail
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
const searchParams = queryString.parse(useLocation().search);
|
const searchParams = queryString.parse(useLocation().search);
|
||||||
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
||||||
|
const [mutationUpdateJob] = useMutation(JOB_PRODUCTION_TOGGLE);
|
||||||
const [title, setTitle] = useState(event.title);
|
const [title, setTitle] = useState(event.title);
|
||||||
const { socket } = useSocket();
|
const { socket } = useSocket();
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [popOverVisible, setPopOverVisible] = useState(false);
|
||||||
|
|
||||||
|
const [getJobDetails] = useLazyQuery(GET_JOB_BY_PK_QUICK_INTAKE, {
|
||||||
|
variables: { id: event.job.id },
|
||||||
|
onCompleted: (data) => {
|
||||||
|
if (data?.jobs_by_pk) {
|
||||||
|
const totalHours =
|
||||||
|
(data.jobs_by_pk.labhrs?.aggregate?.sum?.mod_lb_hrs || 0) +
|
||||||
|
(data.jobs_by_pk.larhrs?.aggregate?.sum?.mod_lb_hrs || 0);
|
||||||
|
form.setFieldsValue({
|
||||||
|
actual_in: data.jobs_by_pk.actual_in ? data.jobs_by_pk.actual_in : dayjs(),
|
||||||
|
scheduled_completion: data.jobs_by_pk.scheduled_completion
|
||||||
|
? data.jobs_by_pk.scheduled_completion
|
||||||
|
: totalHours && bodyshop.ss_configuration.nobusinessdays
|
||||||
|
? dayjs().businessDaysAdd(totalHours / (bodyshop.target_touchtime || 1), "day")
|
||||||
|
: dayjs().add(totalHours / (bodyshop.target_touchtime || 1), "day"),
|
||||||
|
scheduled_delivery: data.jobs_by_pk.scheduled_delivery
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fetchPolicy: "network-only"
|
||||||
|
});
|
||||||
|
|
||||||
const blockContent = (
|
const blockContent = (
|
||||||
<Space direction="vertical" wrap>
|
<Space direction="vertical" wrap>
|
||||||
@@ -89,6 +121,74 @@ export function ScheduleEventComponent({
|
|||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleConvert = async (values) => {
|
||||||
|
const res = await mutationUpdateJob({
|
||||||
|
variables: {
|
||||||
|
jobId: event.job.id,
|
||||||
|
job: {
|
||||||
|
...values,
|
||||||
|
status: bodyshop.md_ro_statuses.default_arrived,
|
||||||
|
inproduction: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.errors) {
|
||||||
|
notification["success"]({
|
||||||
|
message: t("jobs.successes.converted")
|
||||||
|
});
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: event.job.id,
|
||||||
|
operation: AuditTrailMapping.jobintake(
|
||||||
|
res.data.update_jobs.returning[0].status,
|
||||||
|
DateTimeFormatterFunction(values.scheduled_completion)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
setPopOverVisible(false);
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const popMenu = (
|
||||||
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
|
<Form layout="vertical" form={form} onFinish={handleConvert}>
|
||||||
|
<Form.Item
|
||||||
|
name={["actual_in"]}
|
||||||
|
label={t("jobs.fields.actual_in")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<FormDateTimePickerComponent disabled={event.ro_number} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={["scheduled_completion"]}
|
||||||
|
label={t("jobs.fields.scheduled_completion")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<FormDateTimePickerComponent disabled={event.ro_number} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name={["scheduled_delivery"]} label={t("jobs.fields.scheduled_delivery")}>
|
||||||
|
<FormDateTimePickerComponent disabled={event.ro_number} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Space wrap>
|
||||||
|
<Button type="primary" onClick={() => form.submit()}>
|
||||||
|
{t("general.actions.save")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const popoverContent = (
|
const popoverContent = (
|
||||||
<div style={{ maxWidth: "40vw" }}>
|
<div style={{ maxWidth: "40vw" }}>
|
||||||
{!event.isintake ? (
|
{!event.isintake ? (
|
||||||
@@ -294,7 +394,7 @@ export function ScheduleEventComponent({
|
|||||||
) : (
|
) : (
|
||||||
<ScheduleManualEvent event={event} />
|
<ScheduleManualEvent event={event} />
|
||||||
)}
|
)}
|
||||||
{event.isintake ? (
|
{event.isintake && HasFeatureAccess({ featureName: "checklist", bodyshop }) ? (
|
||||||
<Link
|
<Link
|
||||||
to={{
|
to={{
|
||||||
pathname: `/manage/jobs/${event.job && event.job.id}/intake`,
|
pathname: `/manage/jobs/${event.job && event.job.id}/intake`,
|
||||||
@@ -303,7 +403,21 @@ export function ScheduleEventComponent({
|
|||||||
>
|
>
|
||||||
<Button disabled={event.arrived}>{t("appointments.actions.intake")}</Button>
|
<Button disabled={event.arrived}>{t("appointments.actions.intake")}</Button>
|
||||||
</Link>
|
</Link>
|
||||||
) : null}
|
) : (
|
||||||
|
<Popover //open={open}
|
||||||
|
content={popMenu}
|
||||||
|
open={popOverVisible}
|
||||||
|
onOpenChange={setPopOverVisible}
|
||||||
|
onClick={(e) => {
|
||||||
|
getJobDetails();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
getPopupContainer={(trigger) => trigger.parentNode}
|
||||||
|
trigger="click"
|
||||||
|
>
|
||||||
|
<Button disabled={event.arrived}>{t("jobs.actions.intake_quick")}</Button>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { QUERY_JOB_CARD_DETAILS, UPDATE_JOB } from "../../graphql/jobs.queries";
|
import { QUERY_JOB_CARD_DETAILS, UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions.js";
|
import { insertAuditTrail } from "../../redux/application/application.actions.js";
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Col, Row } from "antd";
|
import { Col, Row } from "antd";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
import JobReconciliationBillsTable from "../job-reconciliation-bills-table/job-reconciliation-bills-table.component";
|
import JobReconciliationBillsTable from "../job-reconciliation-bills-table/job-reconciliation-bills-table.component";
|
||||||
import JobReconciliationPartsTable from "../job-reconciliation-parts-table/job-reconciliation-parts-table.component";
|
import JobReconciliationPartsTable from "../job-reconciliation-parts-table/job-reconciliation-parts-table.component";
|
||||||
import JobReconciliationTotals from "../job-reconciliation-totals/job-reconciliation-totals.component";
|
import JobReconciliationTotals from "../job-reconciliation-totals/job-reconciliation-totals.component";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
|
||||||
|
|
||||||
export default function JobReconciliationModalComponent({ job, bills }) {
|
export default function JobReconciliationModalComponent({ job, bills }) {
|
||||||
const jobLineState = useState([]);
|
const jobLineState = useState([]);
|
||||||
@@ -20,7 +20,7 @@ export default function JobReconciliationModalComponent({ job, bills }) {
|
|||||||
|
|
||||||
const filterFunction = InstanceRenderManager({
|
const filterFunction = InstanceRenderManager({
|
||||||
imex: (j) =>
|
imex: (j) =>
|
||||||
(j.part_type !== null && j.part_type !== "PAE") ||
|
(j.part_type !== null && j.part_type !== "PAE" && j.act_price !== 0 && j.part_qty !== 0) ||
|
||||||
(j.line_desc && j.line_desc.toLowerCase().includes("towing") && j.lbr_op === "OP13") ||
|
(j.line_desc && j.line_desc.toLowerCase().includes("towing") && j.lbr_op === "OP13") ||
|
||||||
j.db_ref === "936004", //ADD SHIPPING LINE.
|
j.db_ref === "936004", //ADD SHIPPING LINE.
|
||||||
rome: (j) =>
|
rome: (j) =>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd";
|
import { Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -188,6 +187,12 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
|
|||||||
<Form.Item label={t("jobs.fields.tlos_ind")} name="tlos_ind" valuePropName="checked">
|
<Form.Item label={t("jobs.fields.tlos_ind")} name="tlos_ind" valuePropName="checked">
|
||||||
<Switch disabled={jobRO} />
|
<Switch disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.hit_and_run")} name="hit_and_run" valuePropName="checked">
|
||||||
|
<Switch disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.acv_amount")} name="acv_amount">
|
||||||
|
<CurrencyInput disabled={jobRO} min={0} />
|
||||||
|
</Form.Item>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
</Col>
|
</Col>
|
||||||
<Col {...lossColDamage}>
|
<Col {...lossColDamage}>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
|
import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
|
||||||
import { GET_CURRENT_QUESTIONSET_ID, INSERT_CSI } from "../../graphql/csi.queries";
|
import { GET_CURRENT_QUESTIONSET_ID, INSERT_CSI } from "../../graphql/csi.queries";
|
||||||
@@ -133,6 +133,16 @@ export function JobsDetailHeaderActions({
|
|||||||
const { socket } = useSocket();
|
const { socket } = useSocket();
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|
||||||
|
const isDevEnv = import.meta.env.DEV;
|
||||||
|
const isProdEnv = import.meta.env.PROD;
|
||||||
|
const userEmail = currentUser?.email || "";
|
||||||
|
|
||||||
|
const devEmails = ["imex.dev", "rome.dev"];
|
||||||
|
const prodEmails = ["imex.prod", "rome.prod", "imex.test", "rome.test"];
|
||||||
|
|
||||||
|
const hasValidEmail = (emails) => emails.some((email) => userEmail.endsWith(email));
|
||||||
|
const canSubmitForTesting = (isDevEnv && hasValidEmail(devEmails)) || (isProdEnv && hasValidEmail(prodEmails));
|
||||||
|
|
||||||
const {
|
const {
|
||||||
treatments: { ImEXPay }
|
treatments: { ImEXPay }
|
||||||
} = useSplitTreatments({
|
} = useSplitTreatments({
|
||||||
@@ -171,7 +181,7 @@ export function JobsDetailHeaderActions({
|
|||||||
{ defaultOpenStatus: bodyshop.md_ro_statuses.default_imported },
|
{ defaultOpenStatus: bodyshop.md_ro_statuses.default_imported },
|
||||||
(newJobId) => {
|
(newJobId) => {
|
||||||
history(`/manage/jobs/${newJobId}`);
|
history(`/manage/jobs/${newJobId}`);
|
||||||
notification["success"]({
|
notification.success({
|
||||||
message: t("jobs.successes.duplicated")
|
message: t("jobs.successes.duplicated")
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -181,7 +191,7 @@ export function JobsDetailHeaderActions({
|
|||||||
const handleDuplicateConfirm = () =>
|
const handleDuplicateConfirm = () =>
|
||||||
DuplicateJob(client, job.id, { defaultOpenStatus: bodyshop.md_ro_statuses.default_imported }, (newJobId) => {
|
DuplicateJob(client, job.id, { defaultOpenStatus: bodyshop.md_ro_statuses.default_imported }, (newJobId) => {
|
||||||
history(`/manage/jobs/${newJobId}`);
|
history(`/manage/jobs/${newJobId}`);
|
||||||
notification["success"]({
|
notification.success({
|
||||||
message: t("jobs.successes.duplicated")
|
message: t("jobs.successes.duplicated")
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -217,13 +227,13 @@ export function JobsDetailHeaderActions({
|
|||||||
const result = await deleteJob({ variables: { id: job.id } });
|
const result = await deleteJob({ variables: { id: job.id } });
|
||||||
|
|
||||||
if (!result.errors) {
|
if (!result.errors) {
|
||||||
notification["success"]({
|
notification.success({
|
||||||
message: t("jobs.successes.delete")
|
message: t("jobs.successes.delete")
|
||||||
});
|
});
|
||||||
//go back to jobs list.
|
//go back to jobs list.
|
||||||
history(`/manage/`);
|
history(`/manage/`);
|
||||||
} else {
|
} else {
|
||||||
notification["error"]({
|
notification.error({
|
||||||
message: t("jobs.errors.deleted", {
|
message: t("jobs.errors.deleted", {
|
||||||
error: JSON.stringify(result.errors)
|
error: JSON.stringify(result.errors)
|
||||||
})
|
})
|
||||||
@@ -275,9 +285,9 @@ export function JobsDetailHeaderActions({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!result.errors) {
|
if (!result.errors) {
|
||||||
notification["success"]({ message: t("csi.successes.created") });
|
notification.success({ message: t("csi.successes.created") });
|
||||||
} else {
|
} else {
|
||||||
notification["error"]({
|
notification.error({
|
||||||
message: t("csi.errors.creating", {
|
message: t("csi.errors.creating", {
|
||||||
message: JSON.stringify(result.errors)
|
message: JSON.stringify(result.errors)
|
||||||
})
|
})
|
||||||
@@ -316,7 +326,7 @@ export function JobsDetailHeaderActions({
|
|||||||
`${window.location.protocol}//${window.location.host}/csi/${result.data.insert_csi.returning[0].id}`
|
`${window.location.protocol}//${window.location.host}/csi/${result.data.insert_csi.returning[0].id}`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
notification["error"]({
|
notification.error({
|
||||||
message: t("messaging.error.invalidphone")
|
message: t("messaging.error.invalidphone")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -328,7 +338,7 @@ export function JobsDetailHeaderActions({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
notification["error"]({
|
notification.error({
|
||||||
message: t("csi.errors.notconfigured")
|
message: t("csi.errors.notconfigured")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -358,7 +368,7 @@ export function JobsDetailHeaderActions({
|
|||||||
});
|
});
|
||||||
setMessage(`${window.location.protocol}//${window.location.host}/csi/${job.csiinvites[0].id}`);
|
setMessage(`${window.location.protocol}//${window.location.host}/csi/${job.csiinvites[0].id}`);
|
||||||
} else {
|
} else {
|
||||||
notification["error"]({
|
notification.error({
|
||||||
message: t("messaging.error.invalidphone")
|
message: t("messaging.error.invalidphone")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -398,7 +408,7 @@ export function JobsDetailHeaderActions({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!result.errors) {
|
if (!result.errors) {
|
||||||
notification["success"]({
|
notification.success({
|
||||||
message: t("jobs.successes.voided")
|
message: t("jobs.successes.voided")
|
||||||
});
|
});
|
||||||
insertAuditTrail({
|
insertAuditTrail({
|
||||||
@@ -409,7 +419,7 @@ export function JobsDetailHeaderActions({
|
|||||||
//go back to jobs list.
|
//go back to jobs list.
|
||||||
history(`/manage/`);
|
history(`/manage/`);
|
||||||
} else {
|
} else {
|
||||||
notification["error"]({
|
notification.error({
|
||||||
message: t("jobs.errors.voiding", {
|
message: t("jobs.errors.voiding", {
|
||||||
error: JSON.stringify(result.errors)
|
error: JSON.stringify(result.errors)
|
||||||
})
|
})
|
||||||
@@ -442,7 +452,7 @@ export function JobsDetailHeaderActions({
|
|||||||
console.log("handle -> XML", QbXmlResponse);
|
console.log("handle -> XML", QbXmlResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error getting QBXML from Server.", error);
|
console.log("Error getting QBXML from Server.", error);
|
||||||
notification["error"]({
|
notification.error({
|
||||||
message: t("jobs.errors.exporting", {
|
message: t("jobs.errors.exporting", {
|
||||||
error: "Unable to retrieve QBXML. " + JSON.stringify(error.message)
|
error: "Unable to retrieve QBXML. " + JSON.stringify(error.message)
|
||||||
})
|
})
|
||||||
@@ -460,7 +470,7 @@ export function JobsDetailHeaderActions({
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error connecting to quickbooks or partner.", error);
|
console.log("Error connecting to quickbooks or partner.", error);
|
||||||
notification["error"]({
|
notification.error({
|
||||||
message: t("jobs.errors.exporting-partner")
|
message: t("jobs.errors.exporting-partner")
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -556,7 +566,7 @@ export function JobsDetailHeaderActions({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!jobUpdate.errors) {
|
if (!jobUpdate.errors) {
|
||||||
notification["success"]({
|
notification.success({
|
||||||
message: t("appointments.successes.canceled")
|
message: t("appointments.successes.canceled")
|
||||||
});
|
});
|
||||||
insertAuditTrail({
|
insertAuditTrail({
|
||||||
@@ -931,11 +941,11 @@ export function JobsDetailHeaderActions({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!result.errors) {
|
if (!result.errors) {
|
||||||
notification["success"]({
|
notification.success({
|
||||||
message: t("jobs.successes.partsqueue")
|
message: t("jobs.successes.partsqueue")
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
notification["error"]({
|
notification.error({
|
||||||
message: t("jobs.errors.saving", {
|
message: t("jobs.errors.saving", {
|
||||||
error: JSON.stringify(result.errors)
|
error: JSON.stringify(result.errors)
|
||||||
})
|
})
|
||||||
@@ -1111,6 +1121,27 @@ export function JobsDetailHeaderActions({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (canSubmitForTesting) {
|
||||||
|
menuItems.push({
|
||||||
|
key: "submitfortesting",
|
||||||
|
id: "job-actions-submitfortesting",
|
||||||
|
label: t("menus.jobsactions.submit-for-testing"),
|
||||||
|
onClick: async () => {
|
||||||
|
try {
|
||||||
|
await axios.post("/job/totals-recorder", { id: job.id });
|
||||||
|
notification.success({
|
||||||
|
message: t("general.messages.submit-for-testing")
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error submitting job for testing: ${err?.message}`);
|
||||||
|
notification.error({
|
||||||
|
message: t("general.errors.submit-for-testing-error")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const menu = {
|
const menu = {
|
||||||
items: menuItems,
|
items: menuItems,
|
||||||
key: "popovermenu"
|
key: "popovermenu"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { GET_JOB_BY_PK_QUICK_INTAKE, JOB_PRODUCTION_TOGGLE } from "../../graphql/jobs.queries";
|
import { GET_JOB_BY_PK_QUICK_INTAKE, JOB_PRODUCTION_TOGGLE } from "../../graphql/jobs.queries";
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
@@ -12,7 +13,6 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
||||||
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -44,9 +44,16 @@ export function JobsDetailHeaderActionsToggleProduction({
|
|||||||
variables: { id: job.id },
|
variables: { id: job.id },
|
||||||
onCompleted: (data) => {
|
onCompleted: (data) => {
|
||||||
if (data?.jobs_by_pk) {
|
if (data?.jobs_by_pk) {
|
||||||
|
const totalHours =
|
||||||
|
(data.jobs_by_pk.labhrs?.aggregate?.sum?.mod_lb_hrs || 0) +
|
||||||
|
(data.jobs_by_pk.larhrs?.aggregate?.sum?.mod_lb_hrs || 0);
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
actual_in: data.jobs_by_pk.actual_in ? data.jobs_by_pk.actual_in : dayjs(),
|
actual_in: data.jobs_by_pk.actual_in ? data.jobs_by_pk.actual_in : dayjs(),
|
||||||
scheduled_completion: data.jobs_by_pk.scheduled_completion,
|
scheduled_completion: data.jobs_by_pk.scheduled_completion
|
||||||
|
? data.jobs_by_pk.scheduled_completion
|
||||||
|
: totalHours && bodyshop.ss_configuration.nobusinessdays
|
||||||
|
? dayjs().businessDaysAdd(totalHours / (bodyshop.target_touchtime || 1), "day")
|
||||||
|
: dayjs().add(totalHours / (bodyshop.target_touchtime || 1), "day"),
|
||||||
actual_completion: data.jobs_by_pk.actual_completion,
|
actual_completion: data.jobs_by_pk.actual_completion,
|
||||||
scheduled_delivery: data.jobs_by_pk.scheduled_delivery,
|
scheduled_delivery: data.jobs_by_pk.scheduled_delivery,
|
||||||
actual_delivery: data.jobs_by_pk.actual_delivery
|
actual_delivery: data.jobs_by_pk.actual_delivery
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { setModalContext } from "../../redux/modals/modals.actions";
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
|
import dayjs from "../../utils/day";
|
||||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||||
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||||
import DataLabel from "../data-label/data-label.component";
|
import DataLabel from "../data-label/data-label.component";
|
||||||
@@ -21,7 +22,6 @@ import ProductionListColumnComment from "../production-list-columns/production-l
|
|||||||
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
||||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||||
import "./jobs-detail-header.styles.scss";
|
import "./jobs-detail-header.styles.scss";
|
||||||
import dayjs from "../../utils/day";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
@@ -149,6 +149,14 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
|||||||
</Space>
|
</Space>
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
|
{job.hit_and_run && (
|
||||||
|
<Tag color="green">
|
||||||
|
<Space>
|
||||||
|
<WarningFilled />
|
||||||
|
<span>{t("jobs.fields.hit_and_run")}</span>
|
||||||
|
</Space>
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { Button, Space } from "antd";
|
import { Button, Space } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import cleanAxios from "../../utils/CleanAxios";
|
import cleanAxios from "../../utils/CleanAxios";
|
||||||
import formatBytes from "../../utils/formatbytes";
|
import formatBytes from "../../utils/formatbytes";
|
||||||
//import yauzl from "yauzl";
|
|
||||||
|
|
||||||
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";
|
||||||
@@ -28,7 +26,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyDownloadButton);
|
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyDownloadButton);
|
||||||
|
|
||||||
export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, identifier }) {
|
export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, identifier, jobId }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [download, setDownload] = useState(null);
|
const [download, setDownload] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -46,6 +44,7 @@ export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, i
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function standardMediaDownload(bufferData) {
|
function standardMediaDownload(bufferData) {
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
const url = window.URL.createObjectURL(new Blob([bufferData]));
|
const url = window.URL.createObjectURL(new Blob([bufferData]));
|
||||||
@@ -53,13 +52,14 @@ export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, i
|
|||||||
a.download = `${identifier || "documents"}.zip`;
|
a.download = `${identifier || "documents"}.zip`;
|
||||||
a.click();
|
a.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = async () => {
|
||||||
logImEXEvent("jobs_documents_download");
|
logImEXEvent("jobs_documents_download");
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const zipUrl = await axios({
|
const zipUrl = await axios({
|
||||||
url: "/media/imgproxy/download",
|
url: "/media/imgproxy/download",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
data: { documentids: imagesToDownload.map((_) => _.id) }
|
data: { jobId, documentids: imagesToDownload.map((_) => _.id) }
|
||||||
});
|
});
|
||||||
|
|
||||||
const theDownloadedZip = await cleanAxios({
|
const theDownloadedZip = await cleanAxios({
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ function JobsDocumentsImgproxyComponent({
|
|||||||
<SyncOutlined />
|
<SyncOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
<JobsDocumentsGallerySelectAllComponent galleryImages={galleryImages} setGalleryImages={setGalleryImages} />
|
<JobsDocumentsGallerySelectAllComponent galleryImages={galleryImages} setGalleryImages={setGalleryImages} />
|
||||||
<JobsDocumentsDownloadButton galleryImages={galleryImages} identifier={downloadIdentifier} />
|
<JobsDocumentsDownloadButton galleryImages={galleryImages} identifier={downloadIdentifier} jobId={jobId} />
|
||||||
<JobsDocumentsDeleteButton
|
<JobsDocumentsDeleteButton
|
||||||
galleryImages={galleryImages}
|
galleryImages={galleryImages}
|
||||||
deletionCallback={billsCallback || fetchThumbnails || refetch}
|
deletionCallback={billsCallback || fetchThumbnails || refetch}
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import { useQuery } from "@apollo/client";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import NotificationCenterComponent from "./notification-center.component";
|
import NotificationCenterComponent from "./notification-center.component";
|
||||||
import { GET_NOTIFICATIONS } from "../../graphql/notifications.queries";
|
import { GET_NOTIFICATIONS } from "../../graphql/notifications.queries";
|
||||||
import { INITIAL_NOTIFICATIONS, useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||||
import day from "../../utils/day.js";
|
import day from "../../utils/day.js";
|
||||||
|
import { INITIAL_NOTIFICATIONS, useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
|
||||||
// This will be used to poll for notifications when the socket is disconnected
|
// This will be used to poll for notifications when the socket is disconnected
|
||||||
const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
|
const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
|
|||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import ProductionBoardKanbanComponent from "./production-board-kanban.component";
|
import ProductionBoardKanbanComponent from "./production-board-kanban.component";
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React from "react";
|
|
||||||
import { Card, Form, Select } from "antd";
|
import { Card, Form, Select } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const FilterSettings = ({
|
const FilterSettings = ({
|
||||||
selectedMdInsCos,
|
selectedMdInsCos,
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { Card, Checkbox, Col, Form, Row } from "antd";
|
import { Card, Checkbox, Col, Form, Row } from "antd";
|
||||||
import React from "react";
|
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
const InformationSettings = ({ t }) => (
|
const InformationSettings = ({ t }) => (
|
||||||
<Card title={t("production.settings.information")}>
|
<Card title={t("production.settings.information")} style={{ maxWidth: "100%", overflowX: "auto" }}>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]} wrap>
|
||||||
{[
|
{[
|
||||||
"model_info",
|
"model_info",
|
||||||
"ownr_nm",
|
"ownr_nm",
|
||||||
@@ -21,7 +20,7 @@ const InformationSettings = ({ t }) => (
|
|||||||
"subtotal",
|
"subtotal",
|
||||||
"tasks"
|
"tasks"
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
<Col span={4} key={item}>
|
<Col xs={24} sm={12} md={8} lg={6} key={item}>
|
||||||
<Form.Item name={item} valuePropName="checked">
|
<Form.Item name={item} valuePropName="checked">
|
||||||
<Checkbox>{t(`production.labels.${item}`)}</Checkbox>
|
<Checkbox>{t(`production.labels.${item}`)}</Checkbox>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { Card, Col, Form, Radio, Row } from "antd";
|
import { Card, Col, Form, Radio, Row } from "antd";
|
||||||
import React from "react";
|
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
const LayoutSettings = ({ t }) => (
|
const LayoutSettings = ({ t }) => (
|
||||||
<Card title={t("production.settings.layout")}>
|
<Card title={t("production.settings.layout")} style={{ maxWidth: "100%", overflowX: "auto" }}>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
@@ -48,9 +47,9 @@ const LayoutSettings = ({ t }) => (
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
].map(({ name, label, options }) => (
|
].map(({ name, label, options }) => (
|
||||||
<Col span={4} key={name}>
|
<Col xs={24} sm={16} md={10} lg={8} key={name}>
|
||||||
<Form.Item name={name} label={label}>
|
<Form.Item name={name} label={label}>
|
||||||
<Radio.Group>
|
<Radio.Group style={{ display: "flex", flexWrap: "nowrap" }}>
|
||||||
{options.map((option) => (
|
{options.map((option) => (
|
||||||
<Radio.Button key={option.value.toString()} value={option.value}>
|
<Radio.Button key={option.value.toString()} value={option.value}>
|
||||||
{option.label}
|
{option.label}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
|
import { Card, Checkbox, Form } from "antd";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
import { DragDropContext, Draggable, Droppable } from "../trello-board/dnd/lib/index.js";
|
import { DragDropContext, Draggable, Droppable } from "../trello-board/dnd/lib/index.js";
|
||||||
import { statisticsItems } from "./defaultKanbanSettings.js";
|
import { statisticsItems } from "./defaultKanbanSettings.js";
|
||||||
import { Card, Checkbox, Form } from "antd";
|
|
||||||
import React from "react";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
|
|
||||||
const StatisticsSettings = ({ t, statisticsOrder, setStatisticsOrder, setHasChanges }) => {
|
const StatisticsSettings = ({ t, statisticsOrder, setStatisticsOrder, setHasChanges }) => {
|
||||||
const onDragEnd = (result) => {
|
const onDragEnd = (result) => {
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
|
import { SettingOutlined } from "@ant-design/icons";
|
||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Button, Card, Col, Form, Popover, Row, Tabs } from "antd";
|
import { Button, Card, Col, Form, Popover, Row, Tabs } from "antd";
|
||||||
|
import { isFunction } from "lodash";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useNotification } from "../../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { UPDATE_KANBAN_SETTINGS } from "../../../graphql/user.queries.js";
|
import { UPDATE_KANBAN_SETTINGS } from "../../../graphql/user.queries.js";
|
||||||
import { defaultKanbanSettings, mergeWithDefaults } from "./defaultKanbanSettings.js";
|
import { defaultKanbanSettings, mergeWithDefaults } from "./defaultKanbanSettings.js";
|
||||||
import LayoutSettings from "./LayoutSettings.jsx";
|
|
||||||
import InformationSettings from "./InformationSettings.jsx";
|
|
||||||
import StatisticsSettings from "./StatisticsSettings.jsx";
|
|
||||||
import FilterSettings from "./FilterSettings.jsx";
|
import FilterSettings from "./FilterSettings.jsx";
|
||||||
import PropTypes from "prop-types";
|
import InformationSettings from "./InformationSettings.jsx";
|
||||||
import { isFunction } from "lodash";
|
import LayoutSettings from "./LayoutSettings.jsx";
|
||||||
import { useNotification } from "../../../contexts/Notifications/notificationContext.jsx";
|
import StatisticsSettings from "./StatisticsSettings.jsx";
|
||||||
import { SettingOutlined } from "@ant-design/icons";
|
|
||||||
|
|
||||||
function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bodyshop, data, onSettingsChange }) {
|
function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bodyshop, data, onSettingsChange }) {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
@@ -87,7 +87,7 @@ function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bod
|
|||||||
};
|
};
|
||||||
|
|
||||||
const overlay = (
|
const overlay = (
|
||||||
<Card style={{ minWidth: "80vw" }}>
|
<Card style={{ maxWidth: "80vw", width: "100%"}}>
|
||||||
<Form form={form} onFinish={handleFinish} layout="vertical" onValuesChange={handleValuesChange}>
|
<Form form={form} onFinish={handleFinish} layout="vertical" onValuesChange={handleValuesChange}>
|
||||||
<Tabs
|
<Tabs
|
||||||
defaultActiveKey="1"
|
defaultActiveKey="1"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils.js";
|
import { logImEXEvent } from "../../firebase/firebase.utils.js";
|
||||||
import { QUERY_JOB_CARD_DETAILS, UPDATE_JOB } from "../../graphql/jobs.queries";
|
import { QUERY_JOB_CARD_DETAILS, UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions.js";
|
import { insertAuditTrail } from "../../redux/application/application.actions.js";
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
import ProductionListTable from "./production-list-table.component";
|
import ProductionListTable from "./production-list-table.component";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
|
||||||
export default function ProductionListTableContainer({ bodyshop, subscriptionType = "direct" }) {
|
export default function ProductionListTableContainer({ bodyshop, subscriptionType = "direct" }) {
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { selectCurrentUser } from "../../redux/user/user.selectors";
|
|||||||
import { logImEXEvent, updateCurrentPassword } from "../../firebase/firebase.utils";
|
import { logImEXEvent, updateCurrentPassword } from "../../firebase/firebase.utils";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
import NotificationSettingsForm from "../notification-settings/notification-settings-form.component.jsx";
|
import NotificationSettingsForm from "../notification-settings/notification-settings-form.component.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ export function ProfileShopsContainer({ bodyshop, currentUser }) {
|
|||||||
|
|
||||||
//Force window refresh.
|
//Force window refresh.
|
||||||
|
|
||||||
|
//Ping the new partner to refresh.
|
||||||
|
axios.post("http://localhost:1337/refresh");
|
||||||
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Button, Col, Form, Input, Row, Select, Space, Switch, Typography } from "antd";
|
import { Button, Col, Form, Input, Row, Select, Space, Switch, Typography } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -8,16 +8,16 @@ import { calculateScheduleLoad } from "../../redux/application/application.actio
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
|
import BlurWrapper from "../feature-wrapper/blur-wrapper.component";
|
||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
|
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
|
||||||
import EmailInput from "../form-items-formatted/email-form-item.component";
|
import EmailInput from "../form-items-formatted/email-form-item.component";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
||||||
import ScheduleDayViewContainer from "../schedule-day-view/schedule-day-view.container";
|
import ScheduleDayViewContainer from "../schedule-day-view/schedule-day-view.container";
|
||||||
import ScheduleExistingAppointmentsList from "../schedule-existing-appointments-list/schedule-existing-appointments-list.component";
|
import ScheduleExistingAppointmentsList from "../schedule-existing-appointments-list/schedule-existing-appointments-list.component";
|
||||||
import "./schedule-job-modal.scss";
|
|
||||||
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
|
||||||
import BlurWrapper from "../feature-wrapper/blur-wrapper.component";
|
|
||||||
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
||||||
|
import "./schedule-job-modal.scss";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -60,10 +60,12 @@ export function ScheduleJobModalComponent({
|
|||||||
const totalHours =
|
const totalHours =
|
||||||
lbrHrsData.jobs_by_pk.labhrs.aggregate.sum.mod_lb_hrs + lbrHrsData.jobs_by_pk.larhrs.aggregate.sum.mod_lb_hrs;
|
lbrHrsData.jobs_by_pk.labhrs.aggregate.sum.mod_lb_hrs + lbrHrsData.jobs_by_pk.larhrs.aggregate.sum.mod_lb_hrs;
|
||||||
|
|
||||||
if (values.start && !values.scheduled_completion)
|
if (values.start && !values.scheduled_completion) {
|
||||||
form.setFieldsValue({
|
const addDays = bodyshop.ss_configuration.nobusinessdays
|
||||||
scheduled_completion: dayjs(values.start).businessDaysAdd(totalHours / bodyshop.target_touchtime, "day")
|
? dayjs(values.start).add(totalHours / (bodyshop.target_touchtime || 1), "day")
|
||||||
});
|
: dayjs(values.start).businessDaysAdd(totalHours / (bodyshop.target_touchtime || 1), "day");
|
||||||
|
form.setFieldsValue({ scheduled_completion: addDays });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -906,6 +906,7 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
|||||||
add();
|
add();
|
||||||
}}
|
}}
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
|
id="insurancecos-add-button"
|
||||||
>
|
>
|
||||||
{t("general.actions.add")}
|
{t("general.actions.add")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { DeleteFilled } from "@ant-design/icons";
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
import { Button, Divider, Form, Input, InputNumber, Select, Space, Switch, TimePicker } from "antd";
|
import { Button, Divider, Form, Input, InputNumber, Select, Space, Switch, TimePicker } from "antd";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
import ColorpickerFormItemComponent from "../form-items-formatted/colorpicker-form-item.component";
|
import ColorpickerFormItemComponent from "../form-items-formatted/colorpicker-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";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import { ColorPicker } from "./shop-info.rostatus.component";
|
import { ColorPicker } from "./shop-info.rostatus.component";
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
@@ -78,6 +77,13 @@ export function ShopInfoSchedulingComponent({ form, bodyshop }) {
|
|||||||
>
|
>
|
||||||
<InputNumber min={0} />
|
<InputNumber min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={["ss_configuration", "nobusinessdays"]}
|
||||||
|
label={t("bodyshop.fields.ss_configuration.nobusinessdays")}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={["md_lost_sale_reasons"]}
|
name={["md_lost_sale_reasons"]}
|
||||||
label={t("bodyshop.fields.md_lost_sale_reasons")}
|
label={t("bodyshop.fields.md_lost_sale_reasons")}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useLazyQuery } from "@apollo/client";
|
import { useLazyQuery } from "@apollo/client";
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { Form, Input, InputNumber, Select, Switch } from "antd";
|
import { Card, Form, Input, InputNumber, Select, Space, Switch } from "antd";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -19,6 +18,7 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
|||||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||||
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
|
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
|
||||||
import TimeTicketList from "../time-ticket-list/time-ticket-list.component";
|
import TimeTicketList from "../time-ticket-list/time-ticket-list.component";
|
||||||
|
import JobEmployeeAssignmentsContainer from "./../job-employee-assignments/job-employee-assignments.container";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -319,10 +319,15 @@ export function TimeTicketModalComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LaborAllocationContainer({ jobid, loading, lineTicketData, hideTimeTickets = false }) {
|
export function LaborAllocationContainer({ jobid, loading, lineTicketData, hideTimeTickets = false }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
if (loading) return <LoadingSkeleton />;
|
if (loading) return <LoadingSkeleton />;
|
||||||
if (!lineTicketData) return null;
|
if (!lineTicketData) return null;
|
||||||
|
if (!jobid) return null;
|
||||||
return (
|
return (
|
||||||
<div>
|
<Space direction="vertical" style={{ width: "100%" }}>
|
||||||
|
<Card style={{ height: "100%" }} title={t("jobs.labels.employeeassignments")}>
|
||||||
|
<JobEmployeeAssignmentsContainer job={lineTicketData.jobs_by_pk} />
|
||||||
|
</Card>
|
||||||
<LaborAllocationsTable
|
<LaborAllocationsTable
|
||||||
jobId={jobid}
|
jobId={jobid}
|
||||||
joblines={lineTicketData.joblines}
|
joblines={lineTicketData.joblines}
|
||||||
@@ -332,6 +337,6 @@ export function LaborAllocationContainer({ jobid, loading, lineTicketData, hideT
|
|||||||
{!hideTimeTickets && (
|
{!hideTimeTickets && (
|
||||||
<TimeTicketList loading={loading} timetickets={jobid ? lineTicketData.timetickets : []} techConsole />
|
<TimeTicketList loading={loading} timetickets={jobid ? lineTicketData.timetickets : []} techConsole />
|
||||||
)}
|
)}
|
||||||
</div>
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import { PageHeader } from "@ant-design/pro-layout";
|
|||||||
import { useMutation, useQuery } from "@apollo/client";
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { Button, Form, Modal, Space } from "antd";
|
import { Button, Form, Modal, Space } from "antd";
|
||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { QUERY_ACTIVE_EMPLOYEES } from "../../graphql/employees.queries";
|
import { QUERY_ACTIVE_EMPLOYEES } from "../../graphql/employees.queries";
|
||||||
import { INSERT_NEW_TIME_TICKET, UPDATE_TIME_TICKET } from "../../graphql/timetickets.queries";
|
import { INSERT_NEW_TIME_TICKET, UPDATE_TIME_TICKET } from "../../graphql/timetickets.queries";
|
||||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
@@ -14,7 +15,6 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import TimeTicketsCommitToggleComponent from "../time-tickets-commit-toggle/time-tickets-commit-toggle.component";
|
import TimeTicketsCommitToggleComponent from "../time-tickets-commit-toggle/time-tickets-commit-toggle.component";
|
||||||
import TimeTicketModalComponent from "./time-ticket-modal.component";
|
import TimeTicketModalComponent from "./time-ticket-modal.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
timeTicketModal: selectTimeTicket,
|
timeTicketModal: selectTimeTicket,
|
||||||
@@ -81,7 +81,7 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible,
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMutationSuccess = (response) => {
|
const handleMutationSuccess = () => {
|
||||||
notification["success"]({
|
notification["success"]({
|
||||||
message: t("timetickets.successes.created")
|
message: t("timetickets.successes.created")
|
||||||
});
|
});
|
||||||
@@ -123,7 +123,7 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible,
|
|||||||
if (timeTicketModal.open) form.resetFields();
|
if (timeTicketModal.open) form.resetFields();
|
||||||
}, [timeTicketModal.open, form]);
|
}, [timeTicketModal.open, form]);
|
||||||
|
|
||||||
const handleFieldsChange = (changedFields, allFields) => {
|
const handleFieldsChange = (changedFields) => {
|
||||||
if (!!changedFields.employeeid && !!EmployeeAutoCompleteData) {
|
if (!!changedFields.employeeid && !!EmployeeAutoCompleteData) {
|
||||||
const emps = EmployeeAutoCompleteData.employees.filter((e) => e.id === changedFields.employeeid);
|
const emps = EmployeeAutoCompleteData.employees.filter((e) => e.id === changedFields.employeeid);
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
@@ -182,6 +182,7 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible,
|
|||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
|
id="time-ticket-modal"
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
onFinish={handleFinish}
|
onFinish={handleFinish}
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export function UpdateAlert({ updateAvailable }) {
|
|||||||
</Col>
|
</Col>
|
||||||
<Col sm={24} md={8} lg={6}>
|
<Col sm={24} md={8} lg={6}>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button onClick={() => window.open("https://imex-online.noticeable.news/", "_blank")}>
|
<Button onClick={() => window.open("https://shopmanagement.canny.io/changelog", "_blank")}>
|
||||||
{i18n.t("general.actions.viewreleasenotes")}
|
{i18n.t("general.actions.viewreleasenotes")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button loading={loading} type="primary" onClick={() => ReloadNewVersion()}>
|
<Button loading={loading} type="primary" onClick={() => ReloadNewVersion()}>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
// SocketProvider.js
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
import SocketIO from "socket.io-client";
|
import SocketIO from "socket.io-client";
|
||||||
import { auth } from "../../firebase/firebase.utils";
|
import { auth } from "../../firebase/firebase.utils";
|
||||||
import { store } from "../../redux/store";
|
import { store } from "../../redux/store";
|
||||||
@@ -15,10 +16,7 @@ import {
|
|||||||
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";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
|
import { SocketContext, INITIAL_NOTIFICATIONS } from "./useSocket.js";
|
||||||
const SocketContext = createContext(null);
|
|
||||||
|
|
||||||
const INITIAL_NOTIFICATIONS = 10;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Socket Provider - Scenario Notifications / Web Socket related items
|
* Socket Provider - Scenario Notifications / Web Socket related items
|
||||||
@@ -216,7 +214,6 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleNotification = (data) => {
|
const handleNotification = (data) => {
|
||||||
// Scenario Notifications have been disabled, bail.
|
|
||||||
if (Realtime_Notifications_UI?.treatment !== "on") {
|
if (Realtime_Notifications_UI?.treatment !== "on") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -336,7 +333,6 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSyncNotificationRead = ({ notificationId, timestamp }) => {
|
const handleSyncNotificationRead = ({ notificationId, timestamp }) => {
|
||||||
// Scenario Notifications have been disabled, bail.
|
|
||||||
if (Realtime_Notifications_UI?.treatment !== "on") {
|
if (Realtime_Notifications_UI?.treatment !== "on") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -378,7 +374,6 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSyncAllNotificationsRead = ({ timestamp }) => {
|
const handleSyncAllNotificationsRead = ({ timestamp }) => {
|
||||||
// Scenario Notifications have been disabled, bail.
|
|
||||||
if (Realtime_Notifications_UI?.treatment !== "on") {
|
if (Realtime_Notifications_UI?.treatment !== "on") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -490,11 +485,4 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const useSocket = () => {
|
export default SocketProvider;
|
||||||
const context = useContext(SocketContext);
|
|
||||||
// NOTE: Not sure if we absolutely require this, does cause slipups on dev env
|
|
||||||
if (!context) throw new Error("useSocket must be used within a SocketProvider");
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
export { SocketContext, SocketProvider, INITIAL_NOTIFICATIONS, useSocket };
|
|
||||||
13
client/src/contexts/SocketIO/useSocket.js
Normal file
13
client/src/contexts/SocketIO/useSocket.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { createContext, useContext } from "react";
|
||||||
|
|
||||||
|
const SocketContext = createContext(null);
|
||||||
|
|
||||||
|
const INITIAL_NOTIFICATIONS = 10;
|
||||||
|
|
||||||
|
const useSocket = () => {
|
||||||
|
const context = useContext(SocketContext);
|
||||||
|
if (!context) throw new Error("useSocket must be used within a SocketProvider");
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { SocketContext, INITIAL_NOTIFICATIONS, useSocket };
|
||||||
@@ -57,6 +57,7 @@ export const QUERY_BODYSHOP = gql`
|
|||||||
logo_img_path
|
logo_img_path
|
||||||
md_ro_statuses
|
md_ro_statuses
|
||||||
md_order_statuses
|
md_order_statuses
|
||||||
|
tours_enabled
|
||||||
md_functionality_toggles
|
md_functionality_toggles
|
||||||
shopname
|
shopname
|
||||||
state
|
state
|
||||||
@@ -186,6 +187,7 @@ export const UPDATE_SHOP = gql`
|
|||||||
phone
|
phone
|
||||||
federal_tax_id
|
federal_tax_id
|
||||||
id
|
id
|
||||||
|
tours_enabled
|
||||||
insurance_vendor_id
|
insurance_vendor_id
|
||||||
logo_img_path
|
logo_img_path
|
||||||
md_ro_statuses
|
md_ro_statuses
|
||||||
|
|||||||
@@ -35,6 +35,30 @@ export const GET_LINE_TICKET_BY_PK = gql`
|
|||||||
lbr_adjustments
|
lbr_adjustments
|
||||||
converted
|
converted
|
||||||
status
|
status
|
||||||
|
employee_body
|
||||||
|
employee_body_rel {
|
||||||
|
id
|
||||||
|
first_name
|
||||||
|
last_name
|
||||||
|
}
|
||||||
|
employee_csr
|
||||||
|
employee_csr_rel {
|
||||||
|
id
|
||||||
|
first_name
|
||||||
|
last_name
|
||||||
|
}
|
||||||
|
employee_prep
|
||||||
|
employee_prep_rel {
|
||||||
|
id
|
||||||
|
first_name
|
||||||
|
last_name
|
||||||
|
}
|
||||||
|
employee_refinish
|
||||||
|
employee_refinish_rel {
|
||||||
|
id
|
||||||
|
first_name
|
||||||
|
last_name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
joblines(where: { jobid: { _eq: $id }, removed: { _eq: false } }) {
|
joblines(where: { jobid: { _eq: $id }, removed: { _eq: false } }) {
|
||||||
id
|
id
|
||||||
|
|||||||
@@ -423,6 +423,7 @@ export const GET_JOB_BY_PK = gql`
|
|||||||
actual_completion
|
actual_completion
|
||||||
actual_delivery
|
actual_delivery
|
||||||
actual_in
|
actual_in
|
||||||
|
acv_amount
|
||||||
adjustment_bottom_line
|
adjustment_bottom_line
|
||||||
alt_transport
|
alt_transport
|
||||||
area_of_damage
|
area_of_damage
|
||||||
@@ -511,6 +512,7 @@ export const GET_JOB_BY_PK = gql`
|
|||||||
est_ph1
|
est_ph1
|
||||||
flat_rate_ats
|
flat_rate_ats
|
||||||
federal_tax_rate
|
federal_tax_rate
|
||||||
|
hit_and_run
|
||||||
id
|
id
|
||||||
inproduction
|
inproduction
|
||||||
ins_addr1
|
ins_addr1
|
||||||
@@ -2570,6 +2572,20 @@ export const GET_JOB_BY_PK_QUICK_INTAKE = gql`
|
|||||||
actual_completion
|
actual_completion
|
||||||
scheduled_delivery
|
scheduled_delivery
|
||||||
actual_delivery
|
actual_delivery
|
||||||
|
labhrs: joblines_aggregate(where: { _and: [{ mod_lbr_ty: { _neq: "LAR" } }, { removed: { _eq: false } }] }) {
|
||||||
|
aggregate {
|
||||||
|
sum {
|
||||||
|
mod_lb_hrs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
larhrs: joblines_aggregate(where: { _and: [{ mod_lbr_ty: { _eq: "LAR" } }, { removed: { _eq: false } }] }) {
|
||||||
|
aggregate {
|
||||||
|
sum {
|
||||||
|
mod_lb_hrs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ export const QUERY_PARTS_ORDER_OEC = gql`
|
|||||||
}
|
}
|
||||||
ro_number
|
ro_number
|
||||||
clm_no
|
clm_no
|
||||||
|
cieca_stl
|
||||||
|
cieca_ttl
|
||||||
|
cieca_pfl
|
||||||
asgn_no
|
asgn_no
|
||||||
asgn_date
|
asgn_date
|
||||||
state_tax_rate
|
state_tax_rate
|
||||||
@@ -164,6 +167,7 @@ export const QUERY_PARTS_ORDER_OEC = gql`
|
|||||||
loss_desc
|
loss_desc
|
||||||
loss_of_use
|
loss_of_use
|
||||||
loss_type
|
loss_type
|
||||||
|
materials
|
||||||
ownr_addr1
|
ownr_addr1
|
||||||
ownr_addr2
|
ownr_addr2
|
||||||
ownr_city
|
ownr_city
|
||||||
|
|||||||
42
client/src/pages/feature-request/feature-request.page.jsx
Normal file
42
client/src/pages/feature-request/feature-request.page.jsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
||||||
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||||
|
setSelectedHeader: (key) => dispatch(setSelectedHeader(key))
|
||||||
|
});
|
||||||
|
|
||||||
|
export function FeedbackPage({ setBreadcrumbs, setSelectedHeader }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = t("titles.feature-request", {
|
||||||
|
app: InstanceRenderManager({
|
||||||
|
imex: "$t(titles.imexonline)",
|
||||||
|
rome: "$t(titles.romeonline)"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
setBreadcrumbs([{ link: "/manage/feature-request", label: t("titles.bc.feature-request") }]);
|
||||||
|
}, [t, setBreadcrumbs, setSelectedHeader]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function RenderCanny() {
|
||||||
|
const ssoToken = await axios.post("/sso/canny");
|
||||||
|
window.Canny("render", {
|
||||||
|
boardToken: "bba97b06-70db-0334-dee7-8108d73ef614",
|
||||||
|
basePath: `/manage/feature-request`, // See step 2
|
||||||
|
ssoToken: ssoToken.data, // See step 3,
|
||||||
|
theme: "light" // options: light [default], dark, auto
|
||||||
|
});
|
||||||
|
}
|
||||||
|
RenderCanny();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <div data-canny />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(FeedbackPage);
|
||||||
@@ -56,7 +56,7 @@ import { DateTimeFormat } from "../../utils/DateFormatter";
|
|||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import UndefinedToNull from "../../utils/undefinedtonull";
|
import UndefinedToNull from "../../utils/undefinedtonull";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
import JobWatcherToggleContainer from "../../components/job-watcher-toggle/job-watcher-toggle.container.jsx";
|
import JobWatcherToggleContainer from "../../components/job-watcher-toggle/job-watcher-toggle.container.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { FloatButton, Layout, Spin } from "antd";
|
import { Button, FloatButton, Layout, Space, Spin } from "antd";
|
||||||
|
import { AlertOutlined, BulbOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
// import preval from "preval.macro";
|
// import preval from "preval.macro";
|
||||||
import React, { lazy, Suspense, useEffect, useState } from "react";
|
import React, { lazy, Suspense, useEffect, useState } from "react";
|
||||||
@@ -19,8 +20,6 @@ import LoadingSpinner from "../../components/loading-spinner/loading-spinner.com
|
|||||||
import PartnerPingComponent from "../../components/partner-ping/partner-ping.component";
|
import PartnerPingComponent from "../../components/partner-ping/partner-ping.component";
|
||||||
import PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container";
|
import PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container";
|
||||||
import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component";
|
import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component";
|
||||||
import { requestForToken } from "../../firebase/firebase.utils";
|
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
|
||||||
import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors";
|
||||||
import UpdateAlert from "../../components/update-alert/update-alert.component";
|
import UpdateAlert from "../../components/update-alert/update-alert.component";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
||||||
@@ -29,6 +28,7 @@ import WssStatusDisplayComponent from "../../components/wss-status-display/wss-s
|
|||||||
import { selectAlerts } from "../../redux/application/application.selectors.js";
|
import { selectAlerts } from "../../redux/application/application.selectors.js";
|
||||||
import { addAlerts } from "../../redux/application/application.actions.js";
|
import { addAlerts } from "../../redux/application/application.actions.js";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
|
||||||
const JobsPage = lazy(() => import("../jobs/jobs.page"));
|
const JobsPage = lazy(() => import("../jobs/jobs.page"));
|
||||||
|
|
||||||
@@ -56,6 +56,7 @@ const ContractCreatePage = lazy(() => import("../contract-create/contract-create
|
|||||||
const ContractDetailPage = lazy(() => import("../contract-detail/contract-detail.page.container"));
|
const ContractDetailPage = lazy(() => import("../contract-detail/contract-detail.page.container"));
|
||||||
const ContractsList = lazy(() => import("../contracts/contracts.page.container"));
|
const ContractsList = lazy(() => import("../contracts/contracts.page.container"));
|
||||||
const BillsListPage = lazy(() => import("../bills/bills.page.container"));
|
const BillsListPage = lazy(() => import("../bills/bills.page.container"));
|
||||||
|
const FeatureRequestPage = lazy(() => import("../feature-request/feature-request.page.jsx"));
|
||||||
|
|
||||||
const JobCostingModal = lazy(() => import("../../components/job-costing-modal/job-costing-modal.container"));
|
const JobCostingModal = lazy(() => import("../../components/job-costing-modal/job-costing-modal.container"));
|
||||||
const ReportCenterModal = lazy(() => import("../../components/report-center-modal/report-center-modal.container"));
|
const ReportCenterModal = lazy(() => import("../../components/report-center-modal/report-center-modal.container"));
|
||||||
@@ -180,15 +181,12 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [alerts, displayedAlertIds, notification]);
|
}, [alerts, displayedAlertIds, notification]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const widgetId = InstanceRenderManager({
|
window.Canny("initChangelog", {
|
||||||
imex: "IABVNO4scRKY11XBQkNr",
|
appID: "680bd2c7ee501290377f6686",
|
||||||
rome: "mQdqARMzkZRUVugJ6TdS"
|
position: "top",
|
||||||
});
|
align: "left",
|
||||||
window.noticeable.render("widget", widgetId);
|
theme: "light" // options: light [default], dark, auto
|
||||||
requestForToken().catch((error) => {
|
|
||||||
console.error(`Unable to request for token.`, error);
|
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -480,6 +478,8 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
|||||||
// element={<ShopTemplates />}
|
// element={<ShopTemplates />}
|
||||||
// />
|
// />
|
||||||
}
|
}
|
||||||
|
<Route path="/feature-request/*" index element={<FeatureRequestPage />} />
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/shop/vendors"
|
path="/shop/vendors"
|
||||||
element={
|
element={
|
||||||
@@ -669,7 +669,12 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
|||||||
margin: "1rem 0rem"
|
margin: "1rem 0rem"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex" }}>
|
<Link to="/manage/feature-request">
|
||||||
|
<Button icon={<BulbOutlined />} type="text">
|
||||||
|
{t("general.labels.feature-request")}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Space>
|
||||||
<WssStatusDisplayComponent />
|
<WssStatusDisplayComponent />
|
||||||
<div onClick={broadcastMessage}>
|
<div onClick={broadcastMessage}>
|
||||||
{`${InstanceRenderManager({
|
{`${InstanceRenderManager({
|
||||||
@@ -677,8 +682,10 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
|||||||
rome: t("titles.romeonline")
|
rome: t("titles.romeonline")
|
||||||
})} - ${import.meta.env.VITE_APP_GIT_SHA_DATE}`}
|
})} - ${import.meta.env.VITE_APP_GIT_SHA_DATE}`}
|
||||||
</div>
|
</div>
|
||||||
<div id="noticeable-widget" style={{ marginLeft: "1rem" }} />
|
<Button icon={<AlertOutlined />} data-canny-changelog type="text">
|
||||||
</div>
|
{t("general.labels.changelog")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
<Link to="/disclaimer" target="_blank" style={{ color: "#ccc" }}>
|
<Link to="/disclaimer" target="_blank" style={{ color: "#ccc" }}>
|
||||||
Disclaimer & Notices
|
Disclaimer & Notices
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import FingerprintJS from "@fingerprintjs/fingerprintjs";
|
import FingerprintJS from "@fingerprintjs/fingerprintjs";
|
||||||
import * as Sentry from "@sentry/browser";
|
|
||||||
import { notification } from "antd";
|
|
||||||
import axios from "axios";
|
|
||||||
import { setUserId, setUserProperties } from "@firebase/analytics";
|
import { setUserId, setUserProperties } from "@firebase/analytics";
|
||||||
import {
|
import {
|
||||||
checkActionCode,
|
checkActionCode,
|
||||||
@@ -12,6 +9,9 @@ import {
|
|||||||
} from "@firebase/auth";
|
} from "@firebase/auth";
|
||||||
import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "@firebase/firestore";
|
import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "@firebase/firestore";
|
||||||
import { getToken } from "@firebase/messaging";
|
import { getToken } from "@firebase/messaging";
|
||||||
|
import * as Sentry from "@sentry/browser";
|
||||||
|
import { notification } from "antd";
|
||||||
|
import axios from "axios";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import LogRocket from "logrocket";
|
import LogRocket from "logrocket";
|
||||||
import { all, call, delay, put, select, takeLatest } from "redux-saga/effects";
|
import { all, call, delay, put, select, takeLatest } from "redux-saga/effects";
|
||||||
@@ -351,7 +351,14 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
|||||||
});
|
});
|
||||||
payload.features?.allAccess === true
|
payload.features?.allAccess === true
|
||||||
? window.$crisp.push(["set", "session:segments", [["allAccess"]]])
|
? window.$crisp.push(["set", "session:segments", [["allAccess"]]])
|
||||||
: window.$crisp.push(["set", "session:segments", [["basic"]]]);
|
: (() => {
|
||||||
|
const featureKeys = Object.keys(payload.features).filter(
|
||||||
|
(key) =>
|
||||||
|
payload.features[key] === true ||
|
||||||
|
(typeof payload.features[key] === "string" && !isNaN(Date.parse(payload.features[key])))
|
||||||
|
);
|
||||||
|
window.$crisp.push(["set", "session:segments", [["basic", ...featureKeys]]]);
|
||||||
|
})();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Couldnt find $crisp.", error.message);
|
console.warn("Couldnt find $crisp.", error.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,7 +335,6 @@
|
|||||||
"intellipay_config": {
|
"intellipay_config": {
|
||||||
"cash_discount_percentage": "Cash Discount %",
|
"cash_discount_percentage": "Cash Discount %",
|
||||||
"enable_cash_discount": "Enable Cash Discounting",
|
"enable_cash_discount": "Enable Cash Discounting",
|
||||||
"payment_type": "Payment Type Map",
|
|
||||||
"payment_map": {
|
"payment_map": {
|
||||||
"amex": "American Express",
|
"amex": "American Express",
|
||||||
"disc": "Discover",
|
"disc": "Discover",
|
||||||
@@ -344,7 +343,8 @@
|
|||||||
"jcb": "JCB",
|
"jcb": "JCB",
|
||||||
"mast": "MasterCard",
|
"mast": "MasterCard",
|
||||||
"visa": "Visa"
|
"visa": "Visa"
|
||||||
}
|
},
|
||||||
|
"payment_type": "Payment Type Map"
|
||||||
},
|
},
|
||||||
"invoice_federal_tax_rate": "Invoices - Federal Tax Rate",
|
"invoice_federal_tax_rate": "Invoices - Federal Tax Rate",
|
||||||
"invoice_local_tax_rate": "Invoices - Local Tax Rate",
|
"invoice_local_tax_rate": "Invoices - Local Tax Rate",
|
||||||
@@ -601,7 +601,8 @@
|
|||||||
"templates": "Templates"
|
"templates": "Templates"
|
||||||
},
|
},
|
||||||
"ss_configuration": {
|
"ss_configuration": {
|
||||||
"dailyhrslimit": "Daily Incoming Hours Limit"
|
"dailyhrslimit": "Daily Incoming Hours Limit",
|
||||||
|
"nobusinessdays": "Include Weekends"
|
||||||
},
|
},
|
||||||
"ssbuckets": {
|
"ssbuckets": {
|
||||||
"color": "Job Color",
|
"color": "Job Color",
|
||||||
@@ -1220,7 +1221,8 @@
|
|||||||
"errors": {
|
"errors": {
|
||||||
"fcm": "You must allow notification permissions to have real time messaging. Click to try again.",
|
"fcm": "You must allow notification permissions to have real time messaging. Click to try again.",
|
||||||
"notfound": "No record was found.",
|
"notfound": "No record was found.",
|
||||||
"sizelimit": "The selected items exceed the size limit."
|
"sizelimit": "The selected items exceed the size limit.",
|
||||||
|
"submit-for-testing": "Error submitting Job for testing."
|
||||||
},
|
},
|
||||||
"itemtypes": {
|
"itemtypes": {
|
||||||
"contract": "CC Contract",
|
"contract": "CC Contract",
|
||||||
@@ -1234,6 +1236,7 @@
|
|||||||
"areyousure": "Are you sure?",
|
"areyousure": "Are you sure?",
|
||||||
"barcode": "Barcode",
|
"barcode": "Barcode",
|
||||||
"cancel": "Are you sure you want to cancel? Your changes will not be saved.",
|
"cancel": "Are you sure you want to cancel? Your changes will not be saved.",
|
||||||
|
"changelog": "Change Log",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"confirmpassword": "Confirm Password",
|
"confirmpassword": "Confirm Password",
|
||||||
"created_at": "Created At",
|
"created_at": "Created At",
|
||||||
@@ -1243,6 +1246,7 @@
|
|||||||
"errors": "Errors",
|
"errors": "Errors",
|
||||||
"excel": "Excel",
|
"excel": "Excel",
|
||||||
"exceptiontitle": "An error has occurred.",
|
"exceptiontitle": "An error has occurred.",
|
||||||
|
"feature-request": "Have a feature request?",
|
||||||
"friday": "Friday",
|
"friday": "Friday",
|
||||||
"globalsearch": "Global Search",
|
"globalsearch": "Global Search",
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
@@ -1321,6 +1325,7 @@
|
|||||||
"notfoundtitle": "We couldn't find what you're looking for...",
|
"notfoundtitle": "We couldn't find what you're looking for...",
|
||||||
"partnernotrunning": "{{app}} has detected that the partner is not running. Please ensure it is running to enable full functionality.",
|
"partnernotrunning": "{{app}} has detected that the partner is not running. Please ensure it is running to enable full functionality.",
|
||||||
"rbacunauth": "You are not authorized to view this content. Please reach out to your shop manager to change your access level.",
|
"rbacunauth": "You are not authorized to view this content. Please reach out to your shop manager to change your access level.",
|
||||||
|
"submit-for-testing": "Submitted Job for testing successfully.",
|
||||||
"unsavedchanges": "You have unsaved changes.",
|
"unsavedchanges": "You have unsaved changes.",
|
||||||
"unsavedchangespopup": "You have unsaved changes. Are you sure you want to leave?"
|
"unsavedchangespopup": "You have unsaved changes. Are you sure you want to leave?"
|
||||||
},
|
},
|
||||||
@@ -1633,6 +1638,7 @@
|
|||||||
"actual_completion": "Actual Completion",
|
"actual_completion": "Actual Completion",
|
||||||
"actual_delivery": "Actual Delivery",
|
"actual_delivery": "Actual Delivery",
|
||||||
"actual_in": "Actual In",
|
"actual_in": "Actual In",
|
||||||
|
"acv_amount": "ACV Amount",
|
||||||
"adjustment_bottom_line": "Adjustments",
|
"adjustment_bottom_line": "Adjustments",
|
||||||
"adjustmenthours": "Adjustment Hours",
|
"adjustmenthours": "Adjustment Hours",
|
||||||
"alt_transport": "Alt. Trans.",
|
"alt_transport": "Alt. Trans.",
|
||||||
@@ -1758,9 +1764,10 @@
|
|||||||
"est_ct_ln": "Estimator Last Name",
|
"est_ct_ln": "Estimator Last Name",
|
||||||
"est_ea": "Estimator Email",
|
"est_ea": "Estimator Email",
|
||||||
"est_ph1": "Estimator Phone #",
|
"est_ph1": "Estimator Phone #",
|
||||||
"flat_rate_ats": "Flat Rate ATS?",
|
|
||||||
"federal_tax_payable": "Federal Tax Payable",
|
"federal_tax_payable": "Federal Tax Payable",
|
||||||
"federal_tax_rate": "Federal Tax Rate",
|
"federal_tax_rate": "Federal Tax Rate",
|
||||||
|
"flat_rate_ats": "Flat Rate ATS?",
|
||||||
|
"hit_and_run": "Hit and Run",
|
||||||
"ins_addr1": "Insurance Co. Address",
|
"ins_addr1": "Insurance Co. Address",
|
||||||
"ins_city": "Insurance Co. City",
|
"ins_city": "Insurance Co. City",
|
||||||
"ins_co_id": "Insurance Co. ID",
|
"ins_co_id": "Insurance Co. ID",
|
||||||
@@ -2314,6 +2321,7 @@
|
|||||||
"duplicate": "Duplicate this Job",
|
"duplicate": "Duplicate this Job",
|
||||||
"duplicatenolines": "Duplicate this Job without Repair Data",
|
"duplicatenolines": "Duplicate this Job without Repair Data",
|
||||||
"newcccontract": "Create Courtesy Car Contract",
|
"newcccontract": "Create Courtesy Car Contract",
|
||||||
|
"submit-for-testing": "Submit for Testing",
|
||||||
"void": "Void Job"
|
"void": "Void Job"
|
||||||
},
|
},
|
||||||
"jobsdetail": {
|
"jobsdetail": {
|
||||||
@@ -2420,6 +2428,60 @@
|
|||||||
"updated": "Note updated successfully."
|
"updated": "Note updated successfully."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"notifications": {
|
||||||
|
"actions": {
|
||||||
|
"remove": "Remove"
|
||||||
|
},
|
||||||
|
"aria": {
|
||||||
|
"toggle": "Toggle Watching Job"
|
||||||
|
},
|
||||||
|
"channels": {
|
||||||
|
"app": "App",
|
||||||
|
"email": "Email",
|
||||||
|
"fcm": "Push"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"add-watchers": "Add Watchers",
|
||||||
|
"add-watchers-team": "Add Team Members",
|
||||||
|
"employee-search": "Search for an Employee",
|
||||||
|
"mark-all-read": "Mark All Read",
|
||||||
|
"new-notification-title": "New Notification:",
|
||||||
|
"no-watchers": "No Watchers",
|
||||||
|
"notification-center": "Notification Center",
|
||||||
|
"notification-popup-title": "Changes for Job #{{ro_number}}",
|
||||||
|
"notification-settings-failure": "Error saving Notification Settings. {{error}}",
|
||||||
|
"notification-settings-success": "Notification Settings saved successfully.",
|
||||||
|
"notificationscenarios": "Job Notification Scenarios",
|
||||||
|
"ro-number": "RO #{{ro_number}}",
|
||||||
|
"save": "Save Scenarios",
|
||||||
|
"scenario": "Scenario",
|
||||||
|
"show-unread-only": "Show Unread Only",
|
||||||
|
"teams-search": "Search for a Team",
|
||||||
|
"unwatch": "Unwatch",
|
||||||
|
"watch": "Watch",
|
||||||
|
"watching-issue": "Watching"
|
||||||
|
},
|
||||||
|
"scenarios": {
|
||||||
|
"alternate-transport-changed": "Alternate Transport Changed",
|
||||||
|
"bill-posted": "Bill Posted",
|
||||||
|
"critical-parts-status-changed": "Critical Parts Status Changed",
|
||||||
|
"intake-delivery-checklist-completed": "Intake or Delivery Checklist Completed",
|
||||||
|
"job-added-to-production": "Job Added to Production",
|
||||||
|
"job-assigned-to-me": "Job Assigned to Me",
|
||||||
|
"job-status-change": "Job Status Changed",
|
||||||
|
"new-media-added-reassigned": "New Media Added or Reassigned",
|
||||||
|
"new-note-added": "New Note Added",
|
||||||
|
"new-time-ticket-posted": "New Time Ticket Posted",
|
||||||
|
"part-marked-back-ordered": "Part Marked Back Ordered",
|
||||||
|
"payment-collected-completed": "Payment Collected / Completed",
|
||||||
|
"schedule-dates-changed": "Schedule Dates Changed",
|
||||||
|
"supplement-imported": "Supplement Imported",
|
||||||
|
"tasks-updated-created": "Tasks Updated / Created"
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"job-watchers": "Job Watchers"
|
||||||
|
}
|
||||||
|
},
|
||||||
"owner": {
|
"owner": {
|
||||||
"labels": {
|
"labels": {
|
||||||
"noownerinfo": "No owner information."
|
"noownerinfo": "No owner information."
|
||||||
@@ -3416,6 +3478,7 @@
|
|||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
"dms": "DMS Export",
|
"dms": "DMS Export",
|
||||||
"export-logs": "Export Logs",
|
"export-logs": "Export Logs",
|
||||||
|
"feature-request": "Feature Requet",
|
||||||
"inventory": "Inventory",
|
"inventory": "Inventory",
|
||||||
"jobs": "Jobs",
|
"jobs": "Jobs",
|
||||||
"jobs-active": "Active Jobs",
|
"jobs-active": "Active Jobs",
|
||||||
@@ -3460,6 +3523,7 @@
|
|||||||
"dashboard": "Dashboard | {{app}}",
|
"dashboard": "Dashboard | {{app}}",
|
||||||
"dms": "DMS Export | {{app}}",
|
"dms": "DMS Export | {{app}}",
|
||||||
"export-logs": "Export Logs | {{app}}",
|
"export-logs": "Export Logs | {{app}}",
|
||||||
|
"feature-request": "Feature Request | {{app}}",
|
||||||
"imexonline": "ImEX Online",
|
"imexonline": "ImEX Online",
|
||||||
"inventory": "Inventory | {{app}}",
|
"inventory": "Inventory | {{app}}",
|
||||||
"jobs": "Active Jobs | {{app}}",
|
"jobs": "Active Jobs | {{app}}",
|
||||||
@@ -3677,10 +3741,10 @@
|
|||||||
"users": {
|
"users": {
|
||||||
"errors": {
|
"errors": {
|
||||||
"signinerror": {
|
"signinerror": {
|
||||||
|
"auth/invalid-email": "A user with this email does not exist.",
|
||||||
"auth/user-disabled": "User account disabled. ",
|
"auth/user-disabled": "User account disabled. ",
|
||||||
"auth/user-not-found": "A user with this email does not exist.",
|
"auth/user-not-found": "A user with this email does not exist.",
|
||||||
"auth/wrong-password": "The email and password combination you provided is incorrect.",
|
"auth/wrong-password": "The email and password combination you provided is incorrect."
|
||||||
"auth/invalid-email": "A user with this email does not exist."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3780,60 +3844,6 @@
|
|||||||
"validation": {
|
"validation": {
|
||||||
"unique_vendor_name": "You must enter a unique vendor name."
|
"unique_vendor_name": "You must enter a unique vendor name."
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"labels": {
|
|
||||||
"notification-center": "Notification Center",
|
|
||||||
"scenario": "Scenario",
|
|
||||||
"notificationscenarios": "Job Notification Scenarios",
|
|
||||||
"save": "Save Scenarios",
|
|
||||||
"watching-issue": "Watching",
|
|
||||||
"add-watchers": "Add Watchers",
|
|
||||||
"employee-search": "Search for an Employee",
|
|
||||||
"teams-search": "Search for a Team",
|
|
||||||
"add-watchers-team": "Add Team Members",
|
|
||||||
"new-notification-title": "New Notification:",
|
|
||||||
"show-unread-only": "Show Unread Only",
|
|
||||||
"mark-all-read": "Mark All Read",
|
|
||||||
"notification-popup-title": "Changes for Job #{{ro_number}}",
|
|
||||||
"ro-number": "RO #{{ro_number}}",
|
|
||||||
"no-watchers": "No Watchers",
|
|
||||||
"notification-settings-success": "Notification Settings saved successfully.",
|
|
||||||
"notification-settings-failure": "Error saving Notification Settings. {{error}}",
|
|
||||||
"watch": "Watch",
|
|
||||||
"unwatch": "Unwatch"
|
|
||||||
},
|
|
||||||
"actions": {
|
|
||||||
"remove": "Remove"
|
|
||||||
},
|
|
||||||
"aria": {
|
|
||||||
"toggle": "Toggle Watching Job"
|
|
||||||
},
|
|
||||||
"tooltips": {
|
|
||||||
"job-watchers": "Job Watchers"
|
|
||||||
},
|
|
||||||
"scenarios": {
|
|
||||||
"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": "Supplement Imported",
|
|
||||||
"schedule-dates-changed": "Schedule Dates Changed",
|
|
||||||
"tasks-updated-created": "Tasks Updated / Created",
|
|
||||||
"new-media-added-reassigned": "New Media Added or Reassigned",
|
|
||||||
"new-time-ticket-posted": "New Time Ticket Posted",
|
|
||||||
"intake-delivery-checklist-completed": "Intake or Delivery Checklist Completed",
|
|
||||||
"job-added-to-production": "Job Added to Production",
|
|
||||||
"job-status-change": "Job Status Changed",
|
|
||||||
"payment-collected-completed": "Payment Collected / Completed",
|
|
||||||
"alternate-transport-changed": "Alternate Transport Changed"
|
|
||||||
},
|
|
||||||
"channels": {
|
|
||||||
"app": "App",
|
|
||||||
"email": "Email",
|
|
||||||
"fcm": "Push"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,7 +335,6 @@
|
|||||||
"intellipay_config": {
|
"intellipay_config": {
|
||||||
"cash_discount_percentage": "",
|
"cash_discount_percentage": "",
|
||||||
"enable_cash_discount": "",
|
"enable_cash_discount": "",
|
||||||
"payment_type": "",
|
|
||||||
"payment_map": {
|
"payment_map": {
|
||||||
"amex": "American Express",
|
"amex": "American Express",
|
||||||
"disc": "Discover",
|
"disc": "Discover",
|
||||||
@@ -344,7 +343,8 @@
|
|||||||
"jcb": "JCB",
|
"jcb": "JCB",
|
||||||
"mast": "MasterCard",
|
"mast": "MasterCard",
|
||||||
"visa": "Visa"
|
"visa": "Visa"
|
||||||
}
|
},
|
||||||
|
"payment_type": ""
|
||||||
},
|
},
|
||||||
"invoice_federal_tax_rate": "",
|
"invoice_federal_tax_rate": "",
|
||||||
"invoice_local_tax_rate": "",
|
"invoice_local_tax_rate": "",
|
||||||
@@ -601,7 +601,8 @@
|
|||||||
"templates": ""
|
"templates": ""
|
||||||
},
|
},
|
||||||
"ss_configuration": {
|
"ss_configuration": {
|
||||||
"dailyhrslimit": ""
|
"dailyhrslimit": "",
|
||||||
|
"nobusinessdays": ""
|
||||||
},
|
},
|
||||||
"ssbuckets": {
|
"ssbuckets": {
|
||||||
"color": "",
|
"color": "",
|
||||||
@@ -1220,7 +1221,8 @@
|
|||||||
"errors": {
|
"errors": {
|
||||||
"fcm": "",
|
"fcm": "",
|
||||||
"notfound": "",
|
"notfound": "",
|
||||||
"sizelimit": ""
|
"sizelimit": "",
|
||||||
|
"submit-for-testing": ""
|
||||||
},
|
},
|
||||||
"itemtypes": {
|
"itemtypes": {
|
||||||
"contract": "",
|
"contract": "",
|
||||||
@@ -1234,6 +1236,7 @@
|
|||||||
"areyousure": "",
|
"areyousure": "",
|
||||||
"barcode": "código de barras",
|
"barcode": "código de barras",
|
||||||
"cancel": "",
|
"cancel": "",
|
||||||
|
"changelog": "",
|
||||||
"clear": "",
|
"clear": "",
|
||||||
"confirmpassword": "",
|
"confirmpassword": "",
|
||||||
"created_at": "",
|
"created_at": "",
|
||||||
@@ -1243,6 +1246,7 @@
|
|||||||
"errors": "",
|
"errors": "",
|
||||||
"excel": "",
|
"excel": "",
|
||||||
"exceptiontitle": "",
|
"exceptiontitle": "",
|
||||||
|
"feature-request": "",
|
||||||
"friday": "",
|
"friday": "",
|
||||||
"globalsearch": "",
|
"globalsearch": "",
|
||||||
"help": "",
|
"help": "",
|
||||||
@@ -1321,6 +1325,7 @@
|
|||||||
"notfoundtitle": "",
|
"notfoundtitle": "",
|
||||||
"partnernotrunning": "",
|
"partnernotrunning": "",
|
||||||
"rbacunauth": "",
|
"rbacunauth": "",
|
||||||
|
"submit-for-testing": "",
|
||||||
"unsavedchanges": "Usted tiene cambios no guardados.",
|
"unsavedchanges": "Usted tiene cambios no guardados.",
|
||||||
"unsavedchangespopup": ""
|
"unsavedchangespopup": ""
|
||||||
},
|
},
|
||||||
@@ -1633,6 +1638,7 @@
|
|||||||
"actual_completion": "Realización real",
|
"actual_completion": "Realización real",
|
||||||
"actual_delivery": "Entrega real",
|
"actual_delivery": "Entrega real",
|
||||||
"actual_in": "Real en",
|
"actual_in": "Real en",
|
||||||
|
"acv_amount": "",
|
||||||
"adjustment_bottom_line": "Ajustes",
|
"adjustment_bottom_line": "Ajustes",
|
||||||
"adjustmenthours": "",
|
"adjustmenthours": "",
|
||||||
"alt_transport": "",
|
"alt_transport": "",
|
||||||
@@ -1758,9 +1764,10 @@
|
|||||||
"est_ct_ln": "Apellido del tasador",
|
"est_ct_ln": "Apellido del tasador",
|
||||||
"est_ea": "Correo electrónico del tasador",
|
"est_ea": "Correo electrónico del tasador",
|
||||||
"est_ph1": "Número de teléfono del tasador",
|
"est_ph1": "Número de teléfono del tasador",
|
||||||
"flat_rate_ats": "",
|
|
||||||
"federal_tax_payable": "Impuesto federal por pagar",
|
"federal_tax_payable": "Impuesto federal por pagar",
|
||||||
"federal_tax_rate": "",
|
"federal_tax_rate": "",
|
||||||
|
"flat_rate_ats": "",
|
||||||
|
"hit_and_run": "",
|
||||||
"ins_addr1": "Dirección de Insurance Co.",
|
"ins_addr1": "Dirección de Insurance Co.",
|
||||||
"ins_city": "Ciudad de seguros",
|
"ins_city": "Ciudad de seguros",
|
||||||
"ins_co_id": "ID de la compañía de seguros",
|
"ins_co_id": "ID de la compañía de seguros",
|
||||||
@@ -2314,6 +2321,7 @@
|
|||||||
"duplicate": "",
|
"duplicate": "",
|
||||||
"duplicatenolines": "",
|
"duplicatenolines": "",
|
||||||
"newcccontract": "",
|
"newcccontract": "",
|
||||||
|
"submit-for-testing": "",
|
||||||
"void": ""
|
"void": ""
|
||||||
},
|
},
|
||||||
"jobsdetail": {
|
"jobsdetail": {
|
||||||
@@ -2420,6 +2428,60 @@
|
|||||||
"updated": "Nota actualizada con éxito."
|
"updated": "Nota actualizada con éxito."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"notifications": {
|
||||||
|
"actions": {
|
||||||
|
"remove": ""
|
||||||
|
},
|
||||||
|
"aria": {
|
||||||
|
"toggle": ""
|
||||||
|
},
|
||||||
|
"channels": {
|
||||||
|
"app": "",
|
||||||
|
"email": "",
|
||||||
|
"fcm": ""
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"add-watchers": "",
|
||||||
|
"add-watchers-team": "",
|
||||||
|
"employee-search": "",
|
||||||
|
"mark-all-read": "",
|
||||||
|
"new-notification-title": "",
|
||||||
|
"no-watchers": "",
|
||||||
|
"notification-center": "",
|
||||||
|
"notification-popup-title": "",
|
||||||
|
"notification-settings-failure": "",
|
||||||
|
"notification-settings-success": "",
|
||||||
|
"notificationscenarios": "",
|
||||||
|
"ro-number": "",
|
||||||
|
"save": "",
|
||||||
|
"scenario": "",
|
||||||
|
"show-unread-only": "",
|
||||||
|
"teams-search": "",
|
||||||
|
"unwatch": "",
|
||||||
|
"watch": "",
|
||||||
|
"watching-issue": ""
|
||||||
|
},
|
||||||
|
"scenarios": {
|
||||||
|
"alternate-transport-changed": "",
|
||||||
|
"bill-posted": "",
|
||||||
|
"critical-parts-status-changed": "",
|
||||||
|
"intake-delivery-checklist-completed": "",
|
||||||
|
"job-added-to-production": "",
|
||||||
|
"job-assigned-to-me": "",
|
||||||
|
"job-status-change": "",
|
||||||
|
"new-media-added-reassigned": "",
|
||||||
|
"new-note-added": "",
|
||||||
|
"new-time-ticket-posted": "",
|
||||||
|
"part-marked-back-ordered": "",
|
||||||
|
"payment-collected-completed": "",
|
||||||
|
"schedule-dates-changed": "",
|
||||||
|
"supplement-imported": "",
|
||||||
|
"tasks-updated-created": ""
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"job-watchers": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"owner": {
|
"owner": {
|
||||||
"labels": {
|
"labels": {
|
||||||
"noownerinfo": ""
|
"noownerinfo": ""
|
||||||
@@ -3416,6 +3478,7 @@
|
|||||||
"dashboard": "",
|
"dashboard": "",
|
||||||
"dms": "",
|
"dms": "",
|
||||||
"export-logs": "",
|
"export-logs": "",
|
||||||
|
"feature-request": "",
|
||||||
"inventory": "",
|
"inventory": "",
|
||||||
"jobs": "",
|
"jobs": "",
|
||||||
"jobs-active": "",
|
"jobs-active": "",
|
||||||
@@ -3460,6 +3523,7 @@
|
|||||||
"dashboard": "",
|
"dashboard": "",
|
||||||
"dms": "",
|
"dms": "",
|
||||||
"export-logs": "",
|
"export-logs": "",
|
||||||
|
"feature-request": "",
|
||||||
"imexonline": "",
|
"imexonline": "",
|
||||||
"inventory": "",
|
"inventory": "",
|
||||||
"jobs": "Todos los trabajos | {{app}}",
|
"jobs": "Todos los trabajos | {{app}}",
|
||||||
@@ -3677,10 +3741,10 @@
|
|||||||
"users": {
|
"users": {
|
||||||
"errors": {
|
"errors": {
|
||||||
"signinerror": {
|
"signinerror": {
|
||||||
|
"auth/invalid-email": "",
|
||||||
"auth/user-disabled": "",
|
"auth/user-disabled": "",
|
||||||
"auth/user-not-found": "",
|
"auth/user-not-found": "",
|
||||||
"auth/wrong-password": "",
|
"auth/wrong-password": ""
|
||||||
"auth/invalid-email": ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3780,60 +3844,6 @@
|
|||||||
"validation": {
|
"validation": {
|
||||||
"unique_vendor_name": ""
|
"unique_vendor_name": ""
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"labels": {
|
|
||||||
"notification-center": "",
|
|
||||||
"scenario": "",
|
|
||||||
"notificationscenarios": "",
|
|
||||||
"save": "",
|
|
||||||
"watching-issue": "",
|
|
||||||
"add-watchers": "",
|
|
||||||
"employee-search": "",
|
|
||||||
"teams-search": "",
|
|
||||||
"add-watchers-team": "",
|
|
||||||
"new-notification-title": "",
|
|
||||||
"show-unread-only": "",
|
|
||||||
"mark-all-read": "",
|
|
||||||
"notification-popup-title": "",
|
|
||||||
"ro-number": "",
|
|
||||||
"no-watchers": "",
|
|
||||||
"notification-settings-success": "",
|
|
||||||
"notification-settings-failure": "",
|
|
||||||
"watch": "",
|
|
||||||
"unwatch": ""
|
|
||||||
},
|
|
||||||
"actions": {
|
|
||||||
"remove": ""
|
|
||||||
},
|
|
||||||
"aria": {
|
|
||||||
"toggle": ""
|
|
||||||
},
|
|
||||||
"tooltips": {
|
|
||||||
"job-watchers": ""
|
|
||||||
},
|
|
||||||
"scenarios": {
|
|
||||||
"job-assigned-to-me": "",
|
|
||||||
"bill-posted": "",
|
|
||||||
"critical-parts-status-changed": "",
|
|
||||||
"part-marked-back-ordered": "",
|
|
||||||
"new-note-added": "",
|
|
||||||
"supplement-imported": "",
|
|
||||||
"schedule-dates-changed": "",
|
|
||||||
"tasks-updated-created": "",
|
|
||||||
"new-media-added-reassigned": "",
|
|
||||||
"new-time-ticket-posted": "",
|
|
||||||
"intake-delivery-checklist-completed": "",
|
|
||||||
"job-added-to-production": "",
|
|
||||||
"job-status-change": "",
|
|
||||||
"payment-collected-completed": "",
|
|
||||||
"alternate-transport-changed": ""
|
|
||||||
},
|
|
||||||
"channels": {
|
|
||||||
"app": "",
|
|
||||||
"email": "",
|
|
||||||
"fcm": ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,7 +335,6 @@
|
|||||||
"intellipay_config": {
|
"intellipay_config": {
|
||||||
"cash_discount_percentage": "",
|
"cash_discount_percentage": "",
|
||||||
"enable_cash_discount": "",
|
"enable_cash_discount": "",
|
||||||
"payment_type": "",
|
|
||||||
"payment_map": {
|
"payment_map": {
|
||||||
"amex": "American Express",
|
"amex": "American Express",
|
||||||
"disc": "Discover",
|
"disc": "Discover",
|
||||||
@@ -344,7 +343,8 @@
|
|||||||
"jcb": "JCB",
|
"jcb": "JCB",
|
||||||
"mast": "MasterCard",
|
"mast": "MasterCard",
|
||||||
"visa": "Visa"
|
"visa": "Visa"
|
||||||
}
|
},
|
||||||
|
"payment_type": ""
|
||||||
},
|
},
|
||||||
"invoice_federal_tax_rate": "",
|
"invoice_federal_tax_rate": "",
|
||||||
"invoice_local_tax_rate": "",
|
"invoice_local_tax_rate": "",
|
||||||
@@ -601,7 +601,8 @@
|
|||||||
"templates": ""
|
"templates": ""
|
||||||
},
|
},
|
||||||
"ss_configuration": {
|
"ss_configuration": {
|
||||||
"dailyhrslimit": ""
|
"dailyhrslimit": "",
|
||||||
|
"nobusinessdays": ""
|
||||||
},
|
},
|
||||||
"ssbuckets": {
|
"ssbuckets": {
|
||||||
"color": "",
|
"color": "",
|
||||||
@@ -1220,7 +1221,8 @@
|
|||||||
"errors": {
|
"errors": {
|
||||||
"fcm": "",
|
"fcm": "",
|
||||||
"notfound": "",
|
"notfound": "",
|
||||||
"sizelimit": ""
|
"sizelimit": "",
|
||||||
|
"submit-for-testing": ""
|
||||||
},
|
},
|
||||||
"itemtypes": {
|
"itemtypes": {
|
||||||
"contract": "",
|
"contract": "",
|
||||||
@@ -1234,6 +1236,7 @@
|
|||||||
"areyousure": "",
|
"areyousure": "",
|
||||||
"barcode": "code à barre",
|
"barcode": "code à barre",
|
||||||
"cancel": "",
|
"cancel": "",
|
||||||
|
"changelog": "",
|
||||||
"clear": "",
|
"clear": "",
|
||||||
"confirmpassword": "",
|
"confirmpassword": "",
|
||||||
"created_at": "",
|
"created_at": "",
|
||||||
@@ -1243,6 +1246,7 @@
|
|||||||
"errors": "",
|
"errors": "",
|
||||||
"excel": "",
|
"excel": "",
|
||||||
"exceptiontitle": "",
|
"exceptiontitle": "",
|
||||||
|
"feature-request": "",
|
||||||
"friday": "",
|
"friday": "",
|
||||||
"globalsearch": "",
|
"globalsearch": "",
|
||||||
"help": "",
|
"help": "",
|
||||||
@@ -1321,6 +1325,7 @@
|
|||||||
"notfoundtitle": "",
|
"notfoundtitle": "",
|
||||||
"partnernotrunning": "",
|
"partnernotrunning": "",
|
||||||
"rbacunauth": "",
|
"rbacunauth": "",
|
||||||
|
"submit-for-testing": "",
|
||||||
"unsavedchanges": "Vous avez des changements non enregistrés.",
|
"unsavedchanges": "Vous avez des changements non enregistrés.",
|
||||||
"unsavedchangespopup": ""
|
"unsavedchangespopup": ""
|
||||||
},
|
},
|
||||||
@@ -1633,6 +1638,7 @@
|
|||||||
"actual_completion": "Achèvement réel",
|
"actual_completion": "Achèvement réel",
|
||||||
"actual_delivery": "Livraison réelle",
|
"actual_delivery": "Livraison réelle",
|
||||||
"actual_in": "En réel",
|
"actual_in": "En réel",
|
||||||
|
"acv_amount": "",
|
||||||
"adjustment_bottom_line": "Ajustements",
|
"adjustment_bottom_line": "Ajustements",
|
||||||
"adjustmenthours": "",
|
"adjustmenthours": "",
|
||||||
"alt_transport": "",
|
"alt_transport": "",
|
||||||
@@ -1758,9 +1764,10 @@
|
|||||||
"est_ct_ln": "Nom de l'évaluateur",
|
"est_ct_ln": "Nom de l'évaluateur",
|
||||||
"est_ea": "Courriel de l'évaluateur",
|
"est_ea": "Courriel de l'évaluateur",
|
||||||
"est_ph1": "Numéro de téléphone de l'évaluateur",
|
"est_ph1": "Numéro de téléphone de l'évaluateur",
|
||||||
"flat_rate_ats": "",
|
|
||||||
"federal_tax_payable": "Impôt fédéral à payer",
|
"federal_tax_payable": "Impôt fédéral à payer",
|
||||||
"federal_tax_rate": "",
|
"federal_tax_rate": "",
|
||||||
|
"flat_rate_ats": "",
|
||||||
|
"hit_and_run": "",
|
||||||
"ins_addr1": "Adresse Insurance Co.",
|
"ins_addr1": "Adresse Insurance Co.",
|
||||||
"ins_city": "Insurance City",
|
"ins_city": "Insurance City",
|
||||||
"ins_co_id": "ID de la compagnie d'assurance",
|
"ins_co_id": "ID de la compagnie d'assurance",
|
||||||
@@ -2314,6 +2321,7 @@
|
|||||||
"duplicate": "",
|
"duplicate": "",
|
||||||
"duplicatenolines": "",
|
"duplicatenolines": "",
|
||||||
"newcccontract": "",
|
"newcccontract": "",
|
||||||
|
"submit-for-testing": "",
|
||||||
"void": ""
|
"void": ""
|
||||||
},
|
},
|
||||||
"jobsdetail": {
|
"jobsdetail": {
|
||||||
@@ -2420,6 +2428,60 @@
|
|||||||
"updated": "Remarque mise à jour avec succès."
|
"updated": "Remarque mise à jour avec succès."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"notifications": {
|
||||||
|
"actions": {
|
||||||
|
"remove": ""
|
||||||
|
},
|
||||||
|
"aria": {
|
||||||
|
"toggle": ""
|
||||||
|
},
|
||||||
|
"channels": {
|
||||||
|
"app": "",
|
||||||
|
"email": "",
|
||||||
|
"fcm": ""
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"add-watchers": "",
|
||||||
|
"add-watchers-team": "",
|
||||||
|
"employee-search": "",
|
||||||
|
"mark-all-read": "",
|
||||||
|
"new-notification-title": "",
|
||||||
|
"no-watchers": "",
|
||||||
|
"notification-center": "",
|
||||||
|
"notification-popup-title": "",
|
||||||
|
"notification-settings-failure": "",
|
||||||
|
"notification-settings-success": "",
|
||||||
|
"notificationscenarios": "",
|
||||||
|
"ro-number": "",
|
||||||
|
"save": "",
|
||||||
|
"scenario": "",
|
||||||
|
"show-unread-only": "",
|
||||||
|
"teams-search": "",
|
||||||
|
"unwatch": "",
|
||||||
|
"watch": "",
|
||||||
|
"watching-issue": ""
|
||||||
|
},
|
||||||
|
"scenarios": {
|
||||||
|
"alternate-transport-changed": "",
|
||||||
|
"bill-posted": "",
|
||||||
|
"critical-parts-status-changed": "",
|
||||||
|
"intake-delivery-checklist-completed": "",
|
||||||
|
"job-added-to-production": "",
|
||||||
|
"job-assigned-to-me": "",
|
||||||
|
"job-status-change": "",
|
||||||
|
"new-media-added-reassigned": "",
|
||||||
|
"new-note-added": "",
|
||||||
|
"new-time-ticket-posted": "",
|
||||||
|
"part-marked-back-ordered": "",
|
||||||
|
"payment-collected-completed": "",
|
||||||
|
"schedule-dates-changed": "",
|
||||||
|
"supplement-imported": "",
|
||||||
|
"tasks-updated-created": ""
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"job-watchers": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"owner": {
|
"owner": {
|
||||||
"labels": {
|
"labels": {
|
||||||
"noownerinfo": ""
|
"noownerinfo": ""
|
||||||
@@ -3416,6 +3478,7 @@
|
|||||||
"dashboard": "",
|
"dashboard": "",
|
||||||
"dms": "",
|
"dms": "",
|
||||||
"export-logs": "",
|
"export-logs": "",
|
||||||
|
"feature-request": "",
|
||||||
"inventory": "",
|
"inventory": "",
|
||||||
"jobs": "",
|
"jobs": "",
|
||||||
"jobs-active": "",
|
"jobs-active": "",
|
||||||
@@ -3460,6 +3523,7 @@
|
|||||||
"dashboard": "",
|
"dashboard": "",
|
||||||
"dms": "",
|
"dms": "",
|
||||||
"export-logs": "",
|
"export-logs": "",
|
||||||
|
"feature-request": "",
|
||||||
"imexonline": "",
|
"imexonline": "",
|
||||||
"inventory": "",
|
"inventory": "",
|
||||||
"jobs": "Tous les emplois | {{app}}",
|
"jobs": "Tous les emplois | {{app}}",
|
||||||
@@ -3677,10 +3741,10 @@
|
|||||||
"users": {
|
"users": {
|
||||||
"errors": {
|
"errors": {
|
||||||
"signinerror": {
|
"signinerror": {
|
||||||
|
"auth/invalid-email": "",
|
||||||
"auth/user-disabled": "",
|
"auth/user-disabled": "",
|
||||||
"auth/user-not-found": "",
|
"auth/user-not-found": "",
|
||||||
"auth/wrong-password": "",
|
"auth/wrong-password": ""
|
||||||
"auth/invalid-email": ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3780,60 +3844,6 @@
|
|||||||
"validation": {
|
"validation": {
|
||||||
"unique_vendor_name": ""
|
"unique_vendor_name": ""
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"labels": {
|
|
||||||
"notification-center": "",
|
|
||||||
"scenario": "",
|
|
||||||
"notificationscenarios": "",
|
|
||||||
"save": "",
|
|
||||||
"watching-issue": "",
|
|
||||||
"add-watchers": "",
|
|
||||||
"employee-search": "",
|
|
||||||
"teams-search": "",
|
|
||||||
"add-watchers-team": "",
|
|
||||||
"new-notification-title": "",
|
|
||||||
"show-unread-only": "",
|
|
||||||
"mark-all-read": "",
|
|
||||||
"notification-popup-title": "",
|
|
||||||
"ro-number": "",
|
|
||||||
"no-watchers": "",
|
|
||||||
"notification-settings-success": "",
|
|
||||||
"notification-settings-failure": "",
|
|
||||||
"watch": "",
|
|
||||||
"unwatch": ""
|
|
||||||
},
|
|
||||||
"actions": {
|
|
||||||
"remove": ""
|
|
||||||
},
|
|
||||||
"aria": {
|
|
||||||
"toggle": ""
|
|
||||||
},
|
|
||||||
"tooltips": {
|
|
||||||
"job-watchers": ""
|
|
||||||
},
|
|
||||||
"scenarios": {
|
|
||||||
"job-assigned-to-me": "",
|
|
||||||
"bill-posted": "",
|
|
||||||
"critical-parts-status-changed": "",
|
|
||||||
"part-marked-back-ordered": "",
|
|
||||||
"new-note-added": "",
|
|
||||||
"supplement-imported": "",
|
|
||||||
"schedule-dates-changed": "",
|
|
||||||
"tasks-updated-created": "",
|
|
||||||
"new-media-added-reassigned": "",
|
|
||||||
"new-time-ticket-posted": "",
|
|
||||||
"intake-delivery-checklist-completed": "",
|
|
||||||
"job-added-to-production": "",
|
|
||||||
"job-status-change": "",
|
|
||||||
"payment-collected-completed": "",
|
|
||||||
"alternate-transport-changed": ""
|
|
||||||
},
|
|
||||||
"channels": {
|
|
||||||
"app": "",
|
|
||||||
"email": "",
|
|
||||||
"fcm": ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ const AuditTrailMapping = {
|
|||||||
jobchecklist: (type, inproduction, status) =>
|
jobchecklist: (type, inproduction, status) =>
|
||||||
i18n.t("audit_trail.messages.jobchecklist", { type, inproduction, status }),
|
i18n.t("audit_trail.messages.jobchecklist", { type, inproduction, status }),
|
||||||
jobconverted: (ro_number) => i18n.t("audit_trail.messages.jobconverted", { ro_number }),
|
jobconverted: (ro_number) => i18n.t("audit_trail.messages.jobconverted", { ro_number }),
|
||||||
jobintake: (status, email, scheduled_completion) =>
|
jobintake: (status, scheduled_completion) =>
|
||||||
i18n.t("audit_trail.messages.jobintake", { status, email, scheduled_completion }),
|
i18n.t("audit_trail.messages.jobintake", { status, scheduled_completion }),
|
||||||
jobdelivery: (status, email, actual_completion) =>
|
jobdelivery: (status, email, actual_completion) =>
|
||||||
i18n.t("audit_trail.messages.jobdelivery", { status, email, actual_completion }),
|
i18n.t("audit_trail.messages.jobdelivery", { status, email, actual_completion }),
|
||||||
jobexported: () => i18n.t("audit_trail.messages.jobexported"),
|
jobexported: () => i18n.t("audit_trail.messages.jobexported"),
|
||||||
|
|||||||
@@ -14,10 +14,7 @@ const onServiceWorkerUpdate = (registration) => {
|
|||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
window.open(
|
window.open(
|
||||||
InstanceRenderManager({
|
`https://shopmanagement.canny.io/changelog`,
|
||||||
imex: "https://imex-online.noticeable.news/",
|
|
||||||
rome: "https://rome-online.noticeable.news/"
|
|
||||||
}),
|
|
||||||
"_blank"
|
"_blank"
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const currentDatePST = new Date()
|
|||||||
.reverse()
|
.reverse()
|
||||||
.join("-");
|
.join("-");
|
||||||
const sentryRelease =
|
const sentryRelease =
|
||||||
`${import.meta.env.VITE_APP_IS_TEST ? "test" : "production"}-${currentDatePST}-${process.env.VITE_GIT_COMMIT_HASH}`.trim();
|
`${import.meta.env.VITE_APP_IS_TEST ? "test" : "production"}-${currentDatePST}`.trim();
|
||||||
|
|
||||||
if (!import.meta.env.DEV) {
|
if (!import.meta.env.DEV) {
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ services:
|
|||||||
aws --endpoint-url=http://localstack:4566 secretsmanager create-secret --name CHATTER_PRIVATE_KEY --secret-string file:///tmp/certs/io-ftp-test.key
|
aws --endpoint-url=http://localstack:4566 secretsmanager create-secret --name CHATTER_PRIVATE_KEY --secret-string file:///tmp/certs/io-ftp-test.key
|
||||||
aws --endpoint-url=http://localstack:4566 logs create-log-group --log-group-name development --region ca-central-1
|
aws --endpoint-url=http://localstack:4566 logs create-log-group --log-group-name development --region ca-central-1
|
||||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-large-log --create-bucket-configuration LocationConstraint=ca-central-1
|
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-large-log --create-bucket-configuration LocationConstraint=ca-central-1
|
||||||
|
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-job-totals --create-bucket-configuration LocationConstraint=ca-central-1
|
||||||
"
|
"
|
||||||
# Node App: The Main IMEX API
|
# Node App: The Main IMEX API
|
||||||
node-app:
|
node-app:
|
||||||
|
|||||||
77
download-job-totals-fixtures.js
Normal file
77
download-job-totals-fixtures.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const logger = require("./server/utils/logger"); // Assuming same logger utility
|
||||||
|
const s3Client = require("./server/utils/s3"); // Using the S3 client utilities with LocalStack support
|
||||||
|
|
||||||
|
// Set bucket name for development with LocalStack
|
||||||
|
const S3_BUCKET_NAME = "imex-job-totals";
|
||||||
|
|
||||||
|
// Set fixtures directory path
|
||||||
|
const FIXTURES_DIR = path.join(__dirname, "server", "job", "test", "fixtures", "job-totals");
|
||||||
|
|
||||||
|
const ensureFixturesDirectory = () => {
|
||||||
|
if (!fs.existsSync(FIXTURES_DIR)) {
|
||||||
|
fs.mkdirSync(FIXTURES_DIR, { recursive: true });
|
||||||
|
logger.log(`Created fixtures directory: ${FIXTURES_DIR}`, "info");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadJsonFiles = async (userInfo = { email: "system" }) => {
|
||||||
|
logger.log(`Starting download of JSON files from bucket: ${S3_BUCKET_NAME}`, "debug", userInfo.email);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ensureFixturesDirectory();
|
||||||
|
const contents = await s3Client.listFilesInS3Bucket(S3_BUCKET_NAME);
|
||||||
|
|
||||||
|
if (!contents.length) {
|
||||||
|
logger.log("No files found in bucket", "info", userInfo.email);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log(`Found ${contents.length} files in bucket`, "info", userInfo.email);
|
||||||
|
|
||||||
|
for (const item of contents) {
|
||||||
|
if (!item.Key.endsWith(".json")) {
|
||||||
|
logger.log(`Skipping non-JSON file: ${item.Key}`, "debug", userInfo.email);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log(`Downloading: ${item.Key}`, "debug", userInfo.email);
|
||||||
|
const fileData = await s3Client.downloadFileFromS3({
|
||||||
|
bucketName: S3_BUCKET_NAME,
|
||||||
|
key: item.Key
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileContent = await fileData.transformToString();
|
||||||
|
const fileName = path.basename(item.Key);
|
||||||
|
const filePath = path.join(FIXTURES_DIR, fileName);
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, fileContent);
|
||||||
|
logger.log(`Saved: ${filePath}`, "info", userInfo.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log("Download completed successfully", "info", userInfo.email);
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("Failed to download JSON files", "error", userInfo.email, null, {
|
||||||
|
error: error?.message,
|
||||||
|
stack: error?.stack
|
||||||
|
});
|
||||||
|
throw error; // Re-throw to trigger process exit with error code
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run the download if script is executed directly
|
||||||
|
if (require.main === module) {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
await downloadJsonFiles();
|
||||||
|
console.log("Script completed successfully");
|
||||||
|
process.exit(0); // Explicitly exit with success code
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fatal error downloading files:", error);
|
||||||
|
process.exit(1); // Explicitly exit with error code
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = downloadJsonFiles;
|
||||||
@@ -31,6 +31,15 @@
|
|||||||
headers:
|
headers:
|
||||||
- name: x-imex-auth
|
- name: x-imex-auth
|
||||||
value_from_env: DATAPUMP_AUTH
|
value_from_env: DATAPUMP_AUTH
|
||||||
|
- name: Podium Data Pump
|
||||||
|
webhook: '{{HASURA_API_URL}}/data/podium'
|
||||||
|
schedule: 15 5 * * *
|
||||||
|
include_in_metadata: true
|
||||||
|
payload: {}
|
||||||
|
headers:
|
||||||
|
- name: x-imex-auth
|
||||||
|
value_from_env: DATAPUMP_AUTH
|
||||||
|
comment: ""
|
||||||
- name: Rome Usage Report
|
- name: Rome Usage Report
|
||||||
webhook: '{{HASURA_API_URL}}/data/usagereport'
|
webhook: '{{HASURA_API_URL}}/data/usagereport'
|
||||||
schedule: 0 12 * * 5
|
schedule: 0 12 * * 5
|
||||||
|
|||||||
@@ -965,6 +965,7 @@
|
|||||||
- insurance_vendor_id
|
- insurance_vendor_id
|
||||||
- intakechecklist
|
- intakechecklist
|
||||||
- intellipay_config
|
- intellipay_config
|
||||||
|
- intellipay_merchant_id
|
||||||
- jc_hourly_rates
|
- jc_hourly_rates
|
||||||
- jobsizelimit
|
- jobsizelimit
|
||||||
- last_name_first
|
- last_name_first
|
||||||
@@ -1004,6 +1005,7 @@
|
|||||||
- pbs_configuration
|
- pbs_configuration
|
||||||
- pbs_serialnumber
|
- pbs_serialnumber
|
||||||
- phone
|
- phone
|
||||||
|
- podiumid
|
||||||
- prodtargethrs
|
- prodtargethrs
|
||||||
- production_config
|
- production_config
|
||||||
- region_config
|
- region_config
|
||||||
@@ -1023,6 +1025,7 @@
|
|||||||
- template_header
|
- template_header
|
||||||
- textid
|
- textid
|
||||||
- timezone
|
- timezone
|
||||||
|
- tours_enabled
|
||||||
- tt_allow_post_to_invoiced
|
- tt_allow_post_to_invoiced
|
||||||
- tt_enforce_hours_for_tech_console
|
- tt_enforce_hours_for_tech_console
|
||||||
- updated_at
|
- updated_at
|
||||||
@@ -3592,6 +3595,7 @@
|
|||||||
- actual_completion
|
- actual_completion
|
||||||
- actual_delivery
|
- actual_delivery
|
||||||
- actual_in
|
- actual_in
|
||||||
|
- acv_amount
|
||||||
- adj_g_disc
|
- adj_g_disc
|
||||||
- adj_strdis
|
- adj_strdis
|
||||||
- adj_towdis
|
- adj_towdis
|
||||||
@@ -3697,6 +3701,7 @@
|
|||||||
- federal_tax_rate
|
- federal_tax_rate
|
||||||
- flat_rate_ats
|
- flat_rate_ats
|
||||||
- g_bett_amt
|
- g_bett_amt
|
||||||
|
- hit_and_run
|
||||||
- id
|
- id
|
||||||
- inproduction
|
- inproduction
|
||||||
- ins_addr1
|
- ins_addr1
|
||||||
@@ -3863,6 +3868,7 @@
|
|||||||
- actual_completion
|
- actual_completion
|
||||||
- actual_delivery
|
- actual_delivery
|
||||||
- actual_in
|
- actual_in
|
||||||
|
- acv_amount
|
||||||
- adj_g_disc
|
- adj_g_disc
|
||||||
- adj_strdis
|
- adj_strdis
|
||||||
- adj_towdis
|
- adj_towdis
|
||||||
@@ -3969,6 +3975,7 @@
|
|||||||
- federal_tax_rate
|
- federal_tax_rate
|
||||||
- flat_rate_ats
|
- flat_rate_ats
|
||||||
- g_bett_amt
|
- g_bett_amt
|
||||||
|
- hit_and_run
|
||||||
- id
|
- id
|
||||||
- inproduction
|
- inproduction
|
||||||
- ins_addr1
|
- ins_addr1
|
||||||
@@ -4147,6 +4154,7 @@
|
|||||||
- actual_completion
|
- actual_completion
|
||||||
- actual_delivery
|
- actual_delivery
|
||||||
- actual_in
|
- actual_in
|
||||||
|
- acv_amount
|
||||||
- adj_g_disc
|
- adj_g_disc
|
||||||
- adj_strdis
|
- adj_strdis
|
||||||
- adj_towdis
|
- adj_towdis
|
||||||
@@ -4253,6 +4261,7 @@
|
|||||||
- federal_tax_rate
|
- federal_tax_rate
|
||||||
- flat_rate_ats
|
- flat_rate_ats
|
||||||
- g_bett_amt
|
- g_bett_amt
|
||||||
|
- hit_and_run
|
||||||
- id
|
- id
|
||||||
- inproduction
|
- inproduction
|
||||||
- ins_addr1
|
- ins_addr1
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."bodyshops" add column "intellipay_merchant_id" text
|
||||||
|
-- null unique;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."bodyshops" add column "intellipay_merchant_id" text
|
||||||
|
null unique;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."bodyshops" add column "tours_enabled" boolean
|
||||||
|
-- not null default 'true';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."bodyshops" add column "tours_enabled" boolean
|
||||||
|
not null default 'true';
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."bodyshops" add column "podiumid" text
|
||||||
|
-- null;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."bodyshops" add column "podiumid" text
|
||||||
|
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 "hit_and_run" boolean
|
||||||
|
-- null default 'false';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."jobs" add column "hit_and_run" 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 "acv_amount" numeric
|
||||||
|
-- null;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."jobs" add column "acv_amount" numeric
|
||||||
|
null;
|
||||||
3897
package-lock.json
generated
3897
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
36
package.json
36
package.json
@@ -12,17 +12,18 @@
|
|||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\"",
|
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\"",
|
||||||
"test:unit": "vitest run",
|
"test:unit": "vitest run",
|
||||||
"test:watch": "vitest"
|
"test:watch": "vitest",
|
||||||
|
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-cloudwatch-logs": "^3.772.0",
|
"@aws-sdk/client-cloudwatch-logs": "^3.782.0",
|
||||||
"@aws-sdk/client-elasticache": "^3.772.0",
|
"@aws-sdk/client-elasticache": "^3.782.0",
|
||||||
"@aws-sdk/client-s3": "^3.772.0",
|
"@aws-sdk/client-s3": "^3.782.0",
|
||||||
"@aws-sdk/client-secrets-manager": "^3.772.0",
|
"@aws-sdk/client-secrets-manager": "^3.782.0",
|
||||||
"@aws-sdk/client-ses": "^3.772.0",
|
"@aws-sdk/client-ses": "^3.782.0",
|
||||||
"@aws-sdk/credential-provider-node": "^3.772.0",
|
"@aws-sdk/credential-provider-node": "^3.782.0",
|
||||||
"@aws-sdk/lib-storage": "^3.774.0",
|
"@aws-sdk/lib-storage": "^3.782.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.774.0",
|
"@aws-sdk/s3-request-presigner": "^3.782.0",
|
||||||
"@opensearch-project/opensearch": "^2.13.0",
|
"@opensearch-project/opensearch": "^2.13.0",
|
||||||
"@socket.io/admin-ui": "^0.5.1",
|
"@socket.io/admin-ui": "^0.5.1",
|
||||||
"@socket.io/redis-adapter": "^8.3.0",
|
"@socket.io/redis-adapter": "^8.3.0",
|
||||||
@@ -32,8 +33,7 @@
|
|||||||
"bee-queue": "^1.7.1",
|
"bee-queue": "^1.7.1",
|
||||||
"better-queue": "^3.8.12",
|
"better-queue": "^3.8.12",
|
||||||
"bluebird": "^3.7.2",
|
"bluebird": "^3.7.2",
|
||||||
"body-parser": "^1.20.3",
|
"bullmq": "^5.48.0",
|
||||||
"bullmq": "^5.44.4",
|
|
||||||
"chart.js": "^4.4.8",
|
"chart.js": "^4.4.8",
|
||||||
"cloudinary": "^2.6.0",
|
"cloudinary": "^2.6.0",
|
||||||
"compression": "^1.8.0",
|
"compression": "^1.8.0",
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"crisp-status-reporter": "^1.2.2",
|
"crisp-status-reporter": "^1.2.2",
|
||||||
"csrf": "^3.1.0",
|
"csrf": "^3.1.0",
|
||||||
"dd-trace": "^5.43.0",
|
"dd-trace": "^5.45.0",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
@@ -52,6 +52,7 @@
|
|||||||
"intuit-oauth": "^4.2.0",
|
"intuit-oauth": "^4.2.0",
|
||||||
"ioredis": "^5.6.0",
|
"ioredis": "^5.6.0",
|
||||||
"json-2-csv": "^5.5.9",
|
"json-2-csv": "^5.5.9",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"juice": "^11.0.1",
|
"juice": "^11.0.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
@@ -69,7 +70,7 @@
|
|||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"socket.io-adapter": "^2.5.5",
|
"socket.io-adapter": "^2.5.5",
|
||||||
"ssh2-sftp-client": "^11.0.0",
|
"ssh2-sftp-client": "^11.0.0",
|
||||||
"twilio": "^5.5.1",
|
"twilio": "^5.5.2",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
"winston-cloudwatch": "^6.3.0",
|
"winston-cloudwatch": "^6.3.0",
|
||||||
@@ -77,15 +78,16 @@
|
|||||||
"xmlbuilder2": "^3.1.1"
|
"xmlbuilder2": "^3.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.23.0",
|
"@eslint/js": "^9.24.0",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.24.0",
|
||||||
"eslint-plugin-react": "^7.37.4",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
|
"mock-require": "^3.0.3",
|
||||||
"p-limit": "^3.1.0",
|
"p-limit": "^3.1.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"source-map-explorer": "^2.5.2",
|
"source-map-explorer": "^2.5.2",
|
||||||
"supertest": "^7.1.0",
|
"supertest": "^7.1.0",
|
||||||
"vitest": "^3.0.9"
|
"vitest": "^3.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ const cors = require("cors");
|
|||||||
const http = require("http");
|
const http = require("http");
|
||||||
const Redis = require("ioredis");
|
const Redis = require("ioredis");
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
const bodyParser = require("body-parser");
|
|
||||||
const compression = require("compression");
|
const compression = require("compression");
|
||||||
const cookieParser = require("cookie-parser");
|
const cookieParser = require("cookie-parser");
|
||||||
const { Server } = require("socket.io");
|
const { Server } = require("socket.io");
|
||||||
@@ -84,8 +83,8 @@ const SOCKETIO_CORS_ORIGIN_DEV = ["http://localhost:3333", "https://localhost:33
|
|||||||
const applyMiddleware = ({ app }) => {
|
const applyMiddleware = ({ app }) => {
|
||||||
app.use(compression());
|
app.use(compression());
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use(bodyParser.json({ limit: "50mb" }));
|
app.use(express.json({ limit: "50mb" }));
|
||||||
app.use(bodyParser.urlencoded({ limit: "50mb", extended: true }));
|
app.use(express.urlencoded({ limit: "50mb", extended: true }));
|
||||||
app.use(cors({ credentials: true, exposedHeaders: ["set-cookie"] }));
|
app.use(cors({ credentials: true, exposedHeaders: ["set-cookie"] }));
|
||||||
|
|
||||||
// Helper middleware
|
// Helper middleware
|
||||||
@@ -118,6 +117,8 @@ const applyRoutes = ({ app }) => {
|
|||||||
app.use("/cdk", require("./server/routes/cdkRoutes"));
|
app.use("/cdk", require("./server/routes/cdkRoutes"));
|
||||||
app.use("/csi", require("./server/routes/csiRoutes"));
|
app.use("/csi", require("./server/routes/csiRoutes"));
|
||||||
app.use("/payroll", require("./server/routes/payrollRoutes"));
|
app.use("/payroll", require("./server/routes/payrollRoutes"));
|
||||||
|
app.use("/sso", require("./server/routes/ssoRoutes"));
|
||||||
|
app.use("/integrations", require("./server/routes/intergrationRoutes"));
|
||||||
|
|
||||||
// Default route for forbidden access
|
// Default route for forbidden access
|
||||||
app.get("/", (req, res) => {
|
app.get("/", (req, res) => {
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ exports.PbsExportAp = async function (socket, { billids, txEnvelope }) {
|
|||||||
|
|
||||||
socket.emit("ap-export-success", billid);
|
socket.emit("ap-export-success", billid);
|
||||||
} else {
|
} else {
|
||||||
CdkBase.createLogEvent(socket, "ERROR", `Export was not succesful.`);
|
CdkBase.createLogEvent(socket, "ERROR", `Export was not successful.`);
|
||||||
socket.emit("ap-export-failure", {
|
socket.emit("ap-export-failure", {
|
||||||
billid,
|
billid,
|
||||||
error: AccountPostingChange.Message
|
error: AccountPostingChange.Message
|
||||||
|
|||||||
@@ -105,14 +105,14 @@ exports.PbsSelectedCustomer = async function PbsSelectedCustomer(socket, selecte
|
|||||||
|
|
||||||
socket.emit("export-success", socket.JobData.id);
|
socket.emit("export-success", socket.JobData.id);
|
||||||
} else {
|
} else {
|
||||||
CdkBase.createLogEvent(socket, "ERROR", `Export was not succesful.`);
|
CdkBase.createLogEvent(socket, "ERROR", `Export was not successful.`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in CdkSelectedCustomer. ${error}`);
|
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in CdkSelectedCustomer. ${error}`);
|
||||||
await InsertFailedExportLog(socket, error);
|
await InsertFailedExportLog(socket, error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// Was Successful
|
||||||
async function CheckForErrors(socket, response) {
|
async function CheckForErrors(socket, response) {
|
||||||
if (response.WasSuccessful === undefined || response.WasSuccessful === true) {
|
if (response.WasSuccessful === undefined || response.WasSuccessful === true) {
|
||||||
CdkBase.createLogEvent(socket, "DEBUG", `Successful response from DMS. ${response.Message || ""}`);
|
CdkBase.createLogEvent(socket, "DEBUG", `Successful response from DMS. ${response.Message || ""}`);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ const path = require("path");
|
|||||||
const queries = require("../graphql-client/queries");
|
const queries = require("../graphql-client/queries");
|
||||||
const moment = require("moment-timezone");
|
const moment = require("moment-timezone");
|
||||||
const converter = require("json-2-csv");
|
const converter = require("json-2-csv");
|
||||||
const _ = require("lodash");
|
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");
|
const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ exports.chatter = require("./chatter").default;
|
|||||||
exports.claimscorp = require("./claimscorp").default;
|
exports.claimscorp = require("./claimscorp").default;
|
||||||
exports.kaizen = require("./kaizen").default;
|
exports.kaizen = require("./kaizen").default;
|
||||||
exports.usageReport = require("./usageReport").default;
|
exports.usageReport = require("./usageReport").default;
|
||||||
|
exports.podium = require("./podium").default;
|
||||||
211
server/data/podium.js
Normal file
211
server/data/podium.js
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const queries = require("../graphql-client/queries");
|
||||||
|
const moment = require("moment-timezone");
|
||||||
|
const converter = require("json-2-csv");
|
||||||
|
const logger = require("../utils/logger");
|
||||||
|
const fs = require("fs");
|
||||||
|
require("dotenv").config({
|
||||||
|
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
||||||
|
});
|
||||||
|
let Client = require("ssh2-sftp-client");
|
||||||
|
|
||||||
|
const client = require("../graphql-client/graphql-client").client;
|
||||||
|
const { sendServerEmail } = require("../email/sendemail");
|
||||||
|
|
||||||
|
const ftpSetup = {
|
||||||
|
host: process.env.PODIUM_HOST,
|
||||||
|
port: process.env.PODIUM_PORT,
|
||||||
|
username: process.env.PODIUM_USER,
|
||||||
|
password: process.env.PODIUM_PASSWORD,
|
||||||
|
debug:
|
||||||
|
process.env.NODE_ENV !== "production"
|
||||||
|
? (message, ...data) => logger.log(message, "DEBUG", "api", null, data)
|
||||||
|
: () => {},
|
||||||
|
algorithms: {
|
||||||
|
serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.default = async (req, res) => {
|
||||||
|
// Only process if in production environment.
|
||||||
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
res.sendStatus(403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Only process if the appropriate token is provided.
|
||||||
|
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
|
||||||
|
res.sendStatus(401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send immediate response and continue processing.
|
||||||
|
res.status(202).json({
|
||||||
|
success: true,
|
||||||
|
message: "Processing request ...",
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.log("podium-start", "DEBUG", "api", null, null);
|
||||||
|
const allCSVResults = [];
|
||||||
|
const allErrors = [];
|
||||||
|
|
||||||
|
const { bodyshops } = await client.request(queries.GET_PODIUM_SHOPS); //Query for the List of Bodyshop Clients.
|
||||||
|
const specificShopIds = req.body.bodyshopIds; // ['uuid];
|
||||||
|
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
|
||||||
|
|
||||||
|
const shopsToProcess =
|
||||||
|
specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
|
||||||
|
logger.log("podium-shopsToProcess-generated", "DEBUG", "api", null, null);
|
||||||
|
|
||||||
|
if (shopsToProcess.length === 0) {
|
||||||
|
logger.log("podium-shopsToProcess-empty", "DEBUG", "api", null, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await processShopData(shopsToProcess, start, end, skipUpload, allCSVResults, allErrors);
|
||||||
|
|
||||||
|
await sendServerEmail({
|
||||||
|
subject: `Podium Report ${moment().format("MM-DD-YY")}`,
|
||||||
|
text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
|
||||||
|
allCSVResults.map((x) => ({
|
||||||
|
imexshopid: x.imexshopid,
|
||||||
|
filename: x.filename,
|
||||||
|
count: x.count,
|
||||||
|
result: x.result
|
||||||
|
})),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}`
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.log("podium-end", "DEBUG", "api", null, null);
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("podium-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function processShopData(shopsToProcess, start, end, skipUpload, allCSVResults, allErrors) {
|
||||||
|
for (const bodyshop of shopsToProcess) {
|
||||||
|
const erroredJobs = [];
|
||||||
|
try {
|
||||||
|
logger.log("podium-start-shop-extract", "DEBUG", "api", bodyshop.id, {
|
||||||
|
shopname: bodyshop.shopname
|
||||||
|
});
|
||||||
|
|
||||||
|
const { jobs, bodyshops_by_pk } = await client.request(queries.PODIUM_QUERY, {
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
start: start ? moment(start).startOf("day") : moment().subtract(2, "days").startOf("day"),
|
||||||
|
...(end && { end: moment(end).endOf("day") })
|
||||||
|
});
|
||||||
|
|
||||||
|
const podiumObject = jobs.map((j) => {
|
||||||
|
return {
|
||||||
|
"Podium Account ID": bodyshops_by_pk.podiumid,
|
||||||
|
"First Name": j.ownr_co_nm ? null : j.ownr_fn,
|
||||||
|
"Last Name": j.ownr_co_nm ? j.ownr_co_nm : j.ownr_ln,
|
||||||
|
"SMS Number": null,
|
||||||
|
"Phone 1": j.ownr_ph1,
|
||||||
|
"Phone 2": j.ownr_ph2,
|
||||||
|
Email: j.ownr_ea,
|
||||||
|
"Delivered Date":
|
||||||
|
(j.actual_delivery && moment(j.actual_delivery).tz(bodyshop.timezone).format("MM/DD/YYYY")) || ""
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (erroredJobs.length > 0) {
|
||||||
|
logger.log("podium-failed-jobs", "ERROR", "api", bodyshop.id, {
|
||||||
|
count: erroredJobs.length,
|
||||||
|
jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const csvObj = {
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
imexshopid: bodyshop.imexshopid,
|
||||||
|
csv: converter.json2csv(podiumObject, { emptyFieldValue: "" }),
|
||||||
|
filename: `${bodyshop.podiumid}-${moment().format("YYYYMMDDTHHMMss")}.csv`,
|
||||||
|
count: podiumObject.length
|
||||||
|
};
|
||||||
|
|
||||||
|
if (skipUpload) {
|
||||||
|
fs.writeFileSync(`./logs/${csvObj.filename}`, csvObj.csv);
|
||||||
|
} else {
|
||||||
|
await uploadViaSFTP(csvObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
allCSVResults.push({
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
imexshopid: bodyshop.imexshopid,
|
||||||
|
podiumid: bodyshop.podiumid,
|
||||||
|
count: csvObj.count,
|
||||||
|
filename: csvObj.filename,
|
||||||
|
result: csvObj.result
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.log("podium-end-shop-extract", "DEBUG", "api", bodyshop.id, {
|
||||||
|
shopname: bodyshop.shopname
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
//Error at the shop level.
|
||||||
|
logger.log("podium-error-shop", "ERROR", "api", bodyshop.id, { error: error.message, stack: error.stack });
|
||||||
|
|
||||||
|
allErrors.push({
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
imexshopid: bodyshop.imexshopid,
|
||||||
|
podiumid: bodyshop.podiumid,
|
||||||
|
fatal: true,
|
||||||
|
errors: [error.toString()]
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
allErrors.push({
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
imexshopid: bodyshop.imexshopid,
|
||||||
|
podiumid: bodyshop.podiumid,
|
||||||
|
errors: erroredJobs.map((ej) => ({
|
||||||
|
ro_number: ej.job?.ro_number,
|
||||||
|
jobid: ej.job?.id,
|
||||||
|
error: ej.error
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadViaSFTP(csvObj) {
|
||||||
|
const sftp = new Client();
|
||||||
|
sftp.on("error", (errors) =>
|
||||||
|
logger.log("podium-sftp-connection-error", "ERROR", "api", csvObj.bodyshopid, {
|
||||||
|
error: errors.message,
|
||||||
|
stack: errors.stack
|
||||||
|
})
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
//Connect to the FTP and upload all.
|
||||||
|
await sftp.connect(ftpSetup);
|
||||||
|
|
||||||
|
try {
|
||||||
|
csvObj.result = await sftp.put(Buffer.from(csvObj.xml), `${csvObj.filename}`);
|
||||||
|
logger.log("podium-sftp-upload", "DEBUG", "api", csvObj.bodyshopid, {
|
||||||
|
imexshopid: csvObj.imexshopid,
|
||||||
|
filename: csvObj.filename,
|
||||||
|
result: csvObj.result
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("podium-sftp-upload-error", "ERROR", "api", csvObj.bodyshopid, {
|
||||||
|
filename: csvObj.filename,
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("podium-sftp-error", "ERROR", "api", csvObj.bodyshopid, {
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
sftp.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,7 +55,12 @@ exports.default = async (req, res) => {
|
|||||||
const csv = converter.json2csv(shopList, { emptyFieldValue: "" });
|
const csv = converter.json2csv(shopList, { emptyFieldValue: "" });
|
||||||
emailer
|
emailer
|
||||||
.sendTaskEmail({
|
.sendTaskEmail({
|
||||||
to: ["patrick.fic@convenient-brands.com", "bradley.rhoades@convenient-brands.com", "jrome@rometech.com"],
|
to: [
|
||||||
|
"patrick.fic@convenient-brands.com",
|
||||||
|
"bradley.rhoades@convenient-brands.com",
|
||||||
|
"jrome@rometech.com",
|
||||||
|
"ivana@imexsystems.ca"
|
||||||
|
],
|
||||||
subject: `RO Usage Report - ${moment().format("MM/DD/YYYY")}`,
|
subject: `RO Usage Report - ${moment().format("MM/DD/YYYY")}`,
|
||||||
text: `
|
text: `
|
||||||
Usage Report for ${moment().format("MM/DD/YYYY")} for Rome Online Customers.
|
Usage Report for ${moment().format("MM/DD/YYYY")} for Rome Online Customers.
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
const moment = require("moment");
|
|
||||||
const { default: RenderInstanceManager } = require("../utils/instanceMgr");
|
|
||||||
const { header, end, start } = require("./html");
|
const { header, end, start } = require("./html");
|
||||||
|
|
||||||
// Required Strings
|
// Required Strings
|
||||||
@@ -7,19 +5,6 @@ const { header, end, start } = require("./html");
|
|||||||
// - subHeader - The subheader of the email
|
// - subHeader - The subheader of the email
|
||||||
// - body - The body of the email
|
// - body - The body of the email
|
||||||
|
|
||||||
// Optional Strings (Have default values)
|
|
||||||
// - footer - The footer of the email
|
|
||||||
// - dateLine - The date line of the email
|
|
||||||
|
|
||||||
const defaultFooter = () => {
|
|
||||||
return RenderInstanceManager({
|
|
||||||
imex: "ImEX Online Collision Repair Management System",
|
|
||||||
rome: "Rome Technologies"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const now = () => moment().format("MM/DD/YYYY @ hh:mm a");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate the email template
|
* Generate the email template
|
||||||
* @param strings
|
* @param strings
|
||||||
@@ -32,81 +17,48 @@ const generateEmailTemplate = (strings) => {
|
|||||||
header +
|
header +
|
||||||
start +
|
start +
|
||||||
`
|
`
|
||||||
<table class="row">
|
<!-- Report Title -->
|
||||||
<tbody>
|
${
|
||||||
<tr>
|
strings.header &&
|
||||||
<th class="small-12 large-12 columns first last">
|
`
|
||||||
<table>
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
<tbody>
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 8px; width: 734px; padding-left: 8px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;">
|
||||||
<tr>
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
<td>
|
<h6 style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; color: inherit; word-wrap: normal; font-weight: normal; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 23px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; text-align: center;"><strong style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">${strings.header}</strong></h6>
|
||||||
<h6 style="text-align:left"><strong>${strings.header}</strong></h6>
|
</td></tr>
|
||||||
</td>
|
</tbody></table></th>
|
||||||
</tr>
|
</tr></tbody></table>
|
||||||
<tr>
|
`
|
||||||
<td>
|
}
|
||||||
<p style="font-size:90%">${strings.subHeader}</p>
|
${
|
||||||
</td>
|
strings.subHeader &&
|
||||||
</tr>
|
`
|
||||||
</tbody>
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
</table>
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 16px; width: 734px; padding-left: 8px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;">
|
||||||
</th>
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
</tr>
|
<p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 95%;">${strings.subHeader}</p>
|
||||||
</tbody>
|
</td></tr>
|
||||||
</table>
|
</tbody></table></th>
|
||||||
|
</tr></tbody></table>
|
||||||
|
`
|
||||||
|
}
|
||||||
<!-- End Report Title -->
|
<!-- End Report Title -->
|
||||||
<!-- Task Detail -->
|
${
|
||||||
<table class="row">
|
strings.body &&
|
||||||
<tbody>
|
`
|
||||||
<tr>
|
<!-- Report Detail -->
|
||||||
<th class="small-12 large-12 columns first last">
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
<table>
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 16px; width: 734px; padding-left: 8px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;">
|
||||||
<tbody>
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
<tr>
|
${strings.body}
|
||||||
<td>${strings.body}</td>
|
</td></tr>
|
||||||
</tr>
|
</tbody></table></th>
|
||||||
</tbody>
|
</tr></tbody></table>
|
||||||
</table>
|
<!-- End Report Detail -->
|
||||||
</th>
|
`
|
||||||
</tr>
|
}
|
||||||
</tbody>
|
` +
|
||||||
</table>
|
end(strings.dateLine)
|
||||||
<!-- End Task Detail -->
|
|
||||||
<!-- Footer -->
|
|
||||||
<table class="row collapsed footer" id="non-printable">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th class="small-3 large-3 columns first">
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td><p style="font-size:70%; padding-right:10px">${strings?.dateLine || now()}</p></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</th>
|
|
||||||
<th class="small-6 large-6 columns">
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td><p style="font-size:70%; text-align:center">${strings?.footer || defaultFooter()}</p></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</th>
|
|
||||||
<th class="small-3 large-3 columns last">
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td><p style="font-size:70%"> </p></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>` +
|
|
||||||
end
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
2763
server/email/html.js
2763
server/email/html.js
File diff suppressed because it is too large
Load Diff
@@ -40,7 +40,9 @@ const logEmail = async (req, email) => {
|
|||||||
to: req?.body?.to,
|
to: req?.body?.to,
|
||||||
cc: req?.body?.cc,
|
cc: req?.body?.cc,
|
||||||
subject: req?.body?.subject,
|
subject: req?.body?.subject,
|
||||||
email
|
email,
|
||||||
|
errorMessage: error?.message,
|
||||||
|
errorStack: error?.stack
|
||||||
// info,
|
// info,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -68,6 +70,7 @@ const sendServerEmail = async ({ subject, text }) => {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
(err, info) => {
|
(err, info) => {
|
||||||
logger.log("server-email-failure", err ? "error" : "debug", null, null, {
|
logger.log("server-email-failure", err ? "error" : "debug", null, null, {
|
||||||
message: err?.message,
|
message: err?.message,
|
||||||
@@ -80,6 +83,108 @@ const sendServerEmail = async ({ subject, text }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sendWelcomeEmail = async ({ to, resetLink, dateLine, features, bcc }) => {
|
||||||
|
try {
|
||||||
|
await mailer.sendMail({
|
||||||
|
from: InstanceManager({
|
||||||
|
imex: `ImEX Online <noreply@imex.online>`,
|
||||||
|
rome: `Rome Online <noreply@romeonline.io>`
|
||||||
|
}),
|
||||||
|
to,
|
||||||
|
bcc,
|
||||||
|
subject: InstanceManager({
|
||||||
|
imex: "Welcome to the ImEX Online platform.",
|
||||||
|
rome: "Welcome to the Rome Online platform."
|
||||||
|
}),
|
||||||
|
html: generateEmailTemplate({
|
||||||
|
header: InstanceManager({
|
||||||
|
imex: "Welcome to the ImEX Online platform.",
|
||||||
|
rome: "Welcome to the Rome Online platform."
|
||||||
|
}),
|
||||||
|
subHeader: `Your ${InstanceManager({imex: features?.allAccess ? "ImEX Online": "ImEX Lite", rome: features?.allAccess ? "RO Manager" : "RO Basic"})} shop setup has been completed, and this email will include all the information you need to begin.`,
|
||||||
|
body: `
|
||||||
|
<p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 90%;">To finish setting up your account, visit this link and enter your desired password. <a href=${resetLink} style="color: #2199e8; text-decoration: none; font-weight: normal; padding: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; line-height: 1.2;">Reset Password</a></p>
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table></th>
|
||||||
|
</tr></tbody></table>
|
||||||
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 16px; width: 734px; padding-left: 8px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;">
|
||||||
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
|
<p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 90%;">To access your ${InstanceManager({imex: features.allAccess ? "ImEX Online": "ImEX Lite", rome: features.allAccess ? "RO Manager" : "RO Basic"})} shop, visit <a href=${InstanceManager({imex: "https://imex.online/", rome: "https://romeonline.io/"})} style="color: #2199e8; text-decoration: none; font-weight: normal; padding: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; line-height: 1.2;">${InstanceManager({imex: "imex.online", rome: "romeonline.io"})}</a>. Your username is your email, and your password is what you previously set up. Contact support for additional logins.</p>
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table></th>
|
||||||
|
</tr></tbody></table>
|
||||||
|
${InstanceManager({
|
||||||
|
rome: `
|
||||||
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 16px; width: 734px; padding-left: 8px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;">
|
||||||
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
|
<p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 90%;">To push estimates over from your estimating system, you must download the Web-Est EMS Unzipper & Rome Online Partner (Computers using Windows only). Here are some steps to help you get started.</p>
|
||||||
|
</td><tr>
|
||||||
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
|
<ul style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 1%; padding-left: 30px;">
|
||||||
|
<li style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%;">Download and set up the Web-Est EMS Unzipper - <a href="https://help.imex.online/en/article/how-to-set-up-the-ems-unzip-downloader-on-web-est-n9hbcv/" style="color: #2199e8; text-decoration: none; font-weight: normal; padding: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; line-height: 1.2;">How to setup the EMS Unzip Downloader on Web-Est</a></li>
|
||||||
|
<li style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%;">Download and set up Rome Online Partner - <a href="https://help.imex.online/en/article/setting-up-the-rome-online-partner-1xsw8tb/" style="color: #2199e8; text-decoration: none; font-weight: normal; padding: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; line-height: 1.2;">Setting up the Rome Online Partner</a></li>
|
||||||
|
</ul>
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table></th>
|
||||||
|
</tr></tbody></table>
|
||||||
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 16px; width: 734px; padding-left: 8px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;">
|
||||||
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
|
<p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 90%;">Once you successfully set up the partner, now it's time to do some initial in-product items: Please note, <b>an estimate must be exported from the estimating platform to use tours.</b></p>
|
||||||
|
</td><tr>
|
||||||
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
|
<ul style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 1%; padding-left: 30px;">
|
||||||
|
<li style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%;">Send estimate from Web-Est to RO Basic - <a href="https://help.imex.online/en/article/how-to-send-estimates-from-web-est-to-the-management-system-ox0h9a/" style="color: #2199e8; text-decoration: none; font-weight: normal; padding: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; line-height: 1.2;">How to setup the EMS Unzip Downloader on Web-Est</a></li>
|
||||||
|
<li style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%;">Once completed, learn how to use RO Basic by accessing the tours at the bottom middle of the screen (labeled “Training Tours”). These walkthroughs will show you how to navigate from creating an RO to closing an RO - <a href="https://www.youtube.com/watch?v=gcbSe5med0I" style="color: #2199e8; text-decoration: none; font-weight: normal; padding: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; line-height: 1.2;">ROME Collision Management Youtube Training Videos</a></li>
|
||||||
|
</ul>
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table></th>
|
||||||
|
</tr></tbody></table>
|
||||||
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 16px; width: 734px; padding-left: 8px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;">
|
||||||
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
|
<p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 90%;">If you need any assistance with setting up the programs, or if you want a dedicated Q&A session with one of our customer success specialists, schedule by clicking this link - <a href="https://rometech.zohobookings.com/#/PSAT" style="color: #2199e8; text-decoration: none; font-weight: normal; padding: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; line-height: 1.2;">Rome Basic Training Booking</a></p>
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table></th>
|
||||||
|
</tr></tbody></table>
|
||||||
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 16px; width: 734px; padding-left: 8px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;">
|
||||||
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
|
<p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 90%;">If you have additional questions or need any support, feel free to use the RO Basic support chat (blue chat box located in the bottom right corner) or give us a call at <a href="tel:14103576700" style="color: #2199e8; text-decoration: none; font-weight: normal; padding: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; line-height: 1.2;">(410) 357-6700</a>. We are here to help make your experience seamless!</p>
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table></th>
|
||||||
|
</tr></tbody></table>
|
||||||
|
`
|
||||||
|
})}
|
||||||
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 16px; width: 734px; padding-left: 8px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;">
|
||||||
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
|
<p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 90%;">In addition to the training tour, you can also book a live one-on-one demo to see exactly how our system can help streamline the repair process at your shop, schedule by clicking this link - <a href="https://outlook.office.com/bookwithme/user/0aa3ae2c6d59497d9f93fb72479848dc@imexsystems.ca/meetingtype/Qy7CsXl5MkuUJ0NRD7B1AA2?anonymous&ep=mlink" style="color: #2199e8; text-decoration: none; font-weight: normal; padding: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; line-height: 1.2;">${InstanceManager({imex: "ImEX Lite", rome: "Rome Basic"})} Demo Booking</a></p>
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table></th>
|
||||||
|
</tr></tbody></table>
|
||||||
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 8px; width: 734px; padding-left: 0px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;">
|
||||||
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
|
<p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 90%;">Thanks,</p>
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table></th>
|
||||||
|
</tr></tbody></table>
|
||||||
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 8px; width: 734px; padding-left: 0px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;">
|
||||||
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
|
<p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 90%;">The ${InstanceManager({imex: "ImEX Online", rome: "Rome Online"})} Team</p>
|
||||||
|
`,
|
||||||
|
dateLine
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("server-email-failure", "error", null, null, { error });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const sendTaskEmail = async ({ to, subject, type = "text", html, text, attachments }) => {
|
const sendTaskEmail = async ({ to, subject, type = "text", html, text, attachments }) => {
|
||||||
try {
|
try {
|
||||||
mailer.sendMail(
|
mailer.sendMail(
|
||||||
@@ -93,6 +198,7 @@ const sendTaskEmail = async ({ to, subject, type = "text", html, text, attachmen
|
|||||||
...(type === "text" ? { text } : { html }),
|
...(type === "text" ? { text } : { html }),
|
||||||
attachments: attachments || null
|
attachments: attachments || null
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
(err, info) => {
|
(err, info) => {
|
||||||
// (message, type, user, record, meta
|
// (message, type, user, record, meta
|
||||||
logger.log("server-email", err ? "error" : "debug", null, null, { message: err?.message, stack: err?.stack });
|
logger.log("server-email", err ? "error" : "debug", null, null, { message: err?.message, stack: err?.stack });
|
||||||
@@ -143,22 +249,20 @@ const sendEmail = async (req, res) => {
|
|||||||
to: req.body.to,
|
to: req.body.to,
|
||||||
cc: req.body.cc,
|
cc: req.body.cc,
|
||||||
subject: req.body.subject,
|
subject: req.body.subject,
|
||||||
attachments:
|
attachments: [
|
||||||
[
|
...(req.body.attachments &&
|
||||||
...((req.body.attachments &&
|
req.body.attachments.map((a) => {
|
||||||
req.body.attachments.map((a) => {
|
|
||||||
return {
|
|
||||||
filename: a.filename,
|
|
||||||
path: a.path
|
|
||||||
};
|
|
||||||
})) ||
|
|
||||||
[]),
|
|
||||||
...downloadedMedia.map((a) => {
|
|
||||||
return {
|
return {
|
||||||
path: a
|
filename: a.filename,
|
||||||
|
path: a.path
|
||||||
};
|
};
|
||||||
})
|
})),
|
||||||
] || null,
|
...downloadedMedia.map((a) => {
|
||||||
|
return {
|
||||||
|
path: a
|
||||||
|
};
|
||||||
|
})
|
||||||
|
],
|
||||||
html: isObject(req.body?.templateStrings) ? generateEmailTemplate(req.body.templateStrings) : req.body.html,
|
html: isObject(req.body?.templateStrings) ? generateEmailTemplate(req.body.templateStrings) : req.body.html,
|
||||||
ses: {
|
ses: {
|
||||||
// optional extra arguments for SendRawEmail
|
// optional extra arguments for SendRawEmail
|
||||||
@@ -273,6 +377,7 @@ ${body.bounce?.bouncedRecipients.map(
|
|||||||
)}
|
)}
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
(err, info) => {
|
(err, info) => {
|
||||||
logger.log("sns-error", err ? "error" : "debug", "api", null, {
|
logger.log("sns-error", err ? "error" : "debug", "api", null, {
|
||||||
errorMessage: err?.message,
|
errorMessage: err?.message,
|
||||||
@@ -294,5 +399,6 @@ module.exports = {
|
|||||||
sendEmail,
|
sendEmail,
|
||||||
sendServerEmail,
|
sendServerEmail,
|
||||||
sendTaskEmail,
|
sendTaskEmail,
|
||||||
emailBounce
|
emailBounce,
|
||||||
|
sendWelcomeEmail
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ const { formatTaskPriority } = require("../notifications/stringHelpers");
|
|||||||
const tasksEmailQueue = taskEmailQueue();
|
const tasksEmailQueue = taskEmailQueue();
|
||||||
|
|
||||||
// Cleanup function for the Tasks Email Queue
|
// Cleanup function for the Tasks Email Queue
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
const tasksEmailQueueCleanup = async () => {
|
const tasksEmailQueueCleanup = async () => {
|
||||||
try {
|
try {
|
||||||
// Example async operation
|
// Example async operation
|
||||||
// console.log("Performing Tasks Email Reminder process cleanup...");
|
// console.log("Performing Tasks Email Reminder process cleanup...");
|
||||||
await new Promise((resolve) => tasksEmailQueue.destroy(() => resolve()));
|
await new Promise((resolve) => tasksEmailQueue.destroy(() => resolve()));
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// console.error("Tasks Email Reminder process cleanup failed:", err);
|
// console.error("Tasks Email Reminder process cleanup failed:", err);
|
||||||
}
|
}
|
||||||
@@ -254,10 +256,15 @@ const tasksRemindEmail = async (req, res) => {
|
|||||||
header: `${allTasks.length} Tasks require your attention`,
|
header: `${allTasks.length} Tasks require your attention`,
|
||||||
subHeader: `Please click on the Tasks below to view the Task.`,
|
subHeader: `Please click on the Tasks below to view the Task.`,
|
||||||
dateLine,
|
dateLine,
|
||||||
body: `<ul>
|
body: `
|
||||||
|
<ul style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; margin: 1%; padding-left: 30px;">
|
||||||
${allTasks
|
${allTasks
|
||||||
.map((task) =>
|
.map((task) =>
|
||||||
`<li><a href="${InstanceEndpoints()}/manage/tasks/alltasks?taskid=${task.id}">${task.title} - Priority: ${formatTaskPriority(task.priority)} ${task.due_date ? `${formatDate(task.due_date)}` : ""} | Bodyshop: ${task.bodyshop.shopname}</a></li>`.trim()
|
`
|
||||||
|
<li style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%;">
|
||||||
|
<a href="${InstanceEndpoints()}/manage/tasks/alltasks?taskid=${task.id}" style="color: #2199e8; text-decoration: none; font-weight: normal; padding: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; line-height: 1.2;">${task.title} - Priority: ${formatTaskPriority(task.priority)} ${task.due_date ? `${formatDate(task.due_date)}` : ""} | Bodyshop: ${task.bodyshop.shopname}</a>
|
||||||
|
</li>
|
||||||
|
`.trim()
|
||||||
)
|
)
|
||||||
.join("")}
|
.join("")}
|
||||||
</ul>`
|
</ul>`
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
const path = require("path");
|
|
||||||
require("dotenv").config({
|
|
||||||
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
|
||||||
});
|
|
||||||
|
|
||||||
const admin = require("firebase-admin");
|
|
||||||
const logger = require("../utils/logger");
|
|
||||||
//const { sendProManagerWelcomeEmail } = require("../email/sendemail");
|
|
||||||
const client = require("../graphql-client/graphql-client").client;
|
|
||||||
const serviceAccount = require(process.env.FIREBASE_ADMINSDK_JSON);
|
const serviceAccount = require(process.env.FIREBASE_ADMINSDK_JSON);
|
||||||
//const generateEmailTemplate = require("../email/generateTemplate");
|
const admin = require("firebase-admin");
|
||||||
|
const moment = require("moment-timezone");
|
||||||
|
const logger = require("../utils/logger");
|
||||||
|
const client = require("../graphql-client/graphql-client").client;
|
||||||
|
const { sendWelcomeEmail } = require("../email/sendemail");
|
||||||
|
const { GET_USER_BY_EMAIL } = require("../graphql-client/queries");
|
||||||
|
|
||||||
admin.initializeApp({
|
admin.initializeApp({
|
||||||
credential: admin.credential.cert(serviceAccount),
|
credential: admin.credential.cert(serviceAccount),
|
||||||
@@ -201,6 +197,94 @@ const unsubscribe = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getWelcomeEmail = async (req, res) => {
|
||||||
|
const { authid, email, bcc } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch user from Firebase
|
||||||
|
const userRecord = await admin.auth().getUser(authid);
|
||||||
|
if (!userRecord) {
|
||||||
|
throw { status: 404, message: "User not found in Firebase." };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch user data from the database using GraphQL
|
||||||
|
const dbUserResult = await client.request(GET_USER_BY_EMAIL, { email: email.toLowerCase() });
|
||||||
|
|
||||||
|
const dbUser = dbUserResult?.users?.[0];
|
||||||
|
if (!dbUser) {
|
||||||
|
throw { status: 404, message: "User not found in database." };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate email before proceeding
|
||||||
|
if (!dbUser.validemail) {
|
||||||
|
logger.log("admin-send-welcome-email-skip", "debug", req.user.email, null, {
|
||||||
|
message: "User email is not valid, skipping email.",
|
||||||
|
email
|
||||||
|
});
|
||||||
|
return res.status(200).json({ message: "User email is not valid, email not sent." });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate password reset link
|
||||||
|
const resetLink = await admin.auth().generatePasswordResetLink(dbUser.email);
|
||||||
|
|
||||||
|
// Send welcome email
|
||||||
|
await sendWelcomeEmail({
|
||||||
|
to: dbUser.email,
|
||||||
|
resetLink,
|
||||||
|
dateLine: moment().tz(dbUser.associations?.[0]?.bodyshop?.timezone).format("MM/DD/YYYY @ hh:mm a"),
|
||||||
|
features: dbUser.associations?.[0]?.bodyshop?.features,
|
||||||
|
bcc
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log success and return response
|
||||||
|
logger.log("admin-send-welcome-email", "debug", req.user.email, null, {
|
||||||
|
request: req.body,
|
||||||
|
ioadmin: true,
|
||||||
|
emailSentTo: email
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({ message: "Welcome email sent successfully." });
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("admin-send-welcome-email-error", "ERROR", req.user.email, null, { error });
|
||||||
|
|
||||||
|
if (!res.headersSent) {
|
||||||
|
return res.status(error.status || 500).json({
|
||||||
|
message: error.message || "Error sending welcome email.",
|
||||||
|
error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getResetLink = async (req, res) => {
|
||||||
|
const { authid, email } = req.body;
|
||||||
|
logger.log("admin-reset-link", "debug", req.user.email, null, { authid, email });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch user from Firebase
|
||||||
|
const userRecord = await admin.auth().getUser(authid);
|
||||||
|
if (!userRecord) {
|
||||||
|
throw { status: 404, message: "User not found in Firebase." };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate password reset link
|
||||||
|
const resetLink = await admin.auth().generatePasswordResetLink(email);
|
||||||
|
|
||||||
|
// Log success and return response
|
||||||
|
logger.log("admin-reset-link-success", "debug", req.user.email, null, {
|
||||||
|
request: req.body,
|
||||||
|
ioadmin: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({ message: "Reset link generated successfully.", resetLink });
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(error.status || 500).json({
|
||||||
|
message: error.message || "Error generating reset link.",
|
||||||
|
error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
admin,
|
admin,
|
||||||
createUser,
|
createUser,
|
||||||
@@ -208,23 +292,7 @@ module.exports = {
|
|||||||
getUser,
|
getUser,
|
||||||
sendNotification,
|
sendNotification,
|
||||||
subscribe,
|
subscribe,
|
||||||
unsubscribe
|
unsubscribe,
|
||||||
|
getWelcomeEmail,
|
||||||
|
getResetLink
|
||||||
};
|
};
|
||||||
|
|
||||||
//Admin claims code.
|
|
||||||
// const uid = "JEqqYlsadwPEXIiyRBR55fflfko1";
|
|
||||||
|
|
||||||
// admin
|
|
||||||
// .auth()
|
|
||||||
// .getUser(uid)
|
|
||||||
// .then((user) => {
|
|
||||||
// console.log(user);
|
|
||||||
// admin.auth().setCustomUserClaims(uid, {
|
|
||||||
// ioadmin: true,
|
|
||||||
// "https://hasura.io/jwt/claims": {
|
|
||||||
// "x-hasura-default-role": "debug",
|
|
||||||
// "x-hasura-allowed-roles": ["admin"],
|
|
||||||
// "x-hasura-user-id": uid,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
const GraphQLClient = require("graphql-request").GraphQLClient;
|
const GraphQLClient = require("graphql-request").GraphQLClient;
|
||||||
const path = require("path");
|
|
||||||
require("dotenv").config({
|
|
||||||
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
|
||||||
});
|
|
||||||
//New bug introduced with Graphql Request.
|
//New bug introduced with Graphql Request.
|
||||||
// https://github.com/prisma-labs/graphql-request/issues/206
|
// https://github.com/prisma-labs/graphql-request/issues/206
|
||||||
// const { Headers } = require("cross-fetch");
|
// const { Headers } = require("cross-fetch");
|
||||||
// global.Headers = global.Headers || Headers;
|
// global.Headers = global.Headers || Headers;
|
||||||
|
|
||||||
exports.client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
|
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
|
||||||
headers: {
|
headers: {
|
||||||
"x-hasura-admin-secret": process.env.HASURA_ADMIN_SECRET
|
"x-hasura-admin-secret": process.env.HASURA_ADMIN_SECRET
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
exports.unauthclient = new GraphQLClient(process.env.GRAPHQL_ENDPOINT);
|
const unauthorizedClient = new GraphQLClient(process.env.GRAPHQL_ENDPOINT);
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
client,
|
||||||
|
unauthorizedClient
|
||||||
|
};
|
||||||
|
|||||||
@@ -1323,6 +1323,27 @@ exports.KAIZEN_QUERY = `query KAIZEN_EXPORT($start: timestamptz, $bodyshopid: uu
|
|||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
exports.PODIUM_QUERY = `query PODIUM_EXPORT($start: timestamptz, $bodyshopid: uuid!, $end: timestamptz) {
|
||||||
|
bodyshops_by_pk(id: $bodyshopid){
|
||||||
|
id
|
||||||
|
shopname
|
||||||
|
podiumid
|
||||||
|
timezone
|
||||||
|
}
|
||||||
|
jobs(where: {_and: [{converted: {_eq: true}}, {actual_delivery: {_gt: $start}}, {actual_delivery: {_lte: $end}}, {shopid: {_eq: $bodyshopid}}, {_or: [{ownr_ph1: {_is_null: false}}, {ownr_ea: {_is_null: false}}]}]}) {
|
||||||
|
actual_delivery
|
||||||
|
id
|
||||||
|
created_at
|
||||||
|
ro_number
|
||||||
|
ownr_fn
|
||||||
|
ownr_ln
|
||||||
|
ownr_co_nm
|
||||||
|
ownr_ph1
|
||||||
|
ownr_ph2
|
||||||
|
ownr_ea
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
exports.UPDATE_JOB = `
|
exports.UPDATE_JOB = `
|
||||||
mutation UPDATE_JOB($jobId: uuid!, $job: jobs_set_input!) {
|
mutation UPDATE_JOB($jobId: uuid!, $job: jobs_set_input!) {
|
||||||
update_jobs(where: { id: { _eq: $jobId } }, _set: $job) {
|
update_jobs(where: { id: { _eq: $jobId } }, _set: $job) {
|
||||||
@@ -1848,6 +1869,16 @@ exports.GET_KAIZEN_SHOPS = `query GET_KAIZEN_SHOPS($imexshopid: [String]) {
|
|||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
exports.GET_PODIUM_SHOPS = `query GET_PODIUM_SHOPS {
|
||||||
|
bodyshops(where: {podiumid: {_is_null: false}, _or: {podiumid: {_neq: ""}}}){
|
||||||
|
id
|
||||||
|
shopname
|
||||||
|
podiumid
|
||||||
|
imexshopid
|
||||||
|
timezone
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
exports.DELETE_ALL_DMS_VEHICLES = `mutation DELETE_ALL_DMS_VEHICLES{
|
exports.DELETE_ALL_DMS_VEHICLES = `mutation DELETE_ALL_DMS_VEHICLES{
|
||||||
delete_dms_vehicles(where: {}) {
|
delete_dms_vehicles(where: {}) {
|
||||||
affected_rows
|
affected_rows
|
||||||
@@ -2832,3 +2863,68 @@ exports.GET_DOCUMENTS_BY_IDS = `
|
|||||||
takenat
|
takenat
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
exports.GET_JOBID_BY_MERCHANTID_RONUMBER = `
|
||||||
|
query GET_JOBID_BY_MERCHANTID_RONUMBER($merchantID: String!, $roNumber: String!) {
|
||||||
|
jobs(where: {ro_number: {_eq: $roNumber}, bodyshop: {intellipay_merchant_id: {_eq: $merchantID}}}) {
|
||||||
|
id
|
||||||
|
shopid
|
||||||
|
bodyshop {
|
||||||
|
id
|
||||||
|
intellipay_config
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
exports.GET_BODYSHOP_BY_MERCHANT_ID = `
|
||||||
|
query GET_BODYSHOP_BY_MERCHANTID($merchantID: String!) {
|
||||||
|
bodyshops(where: {intellipay_merchant_id: {_eq: $merchantID}}) {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
exports.GET_USER_BY_EMAIL = `
|
||||||
|
query GET_USER_BY_EMAIL($email: String!) {
|
||||||
|
users(where: {email: {_eq: $email}}) {
|
||||||
|
email
|
||||||
|
validemail
|
||||||
|
associations {
|
||||||
|
id
|
||||||
|
shopid
|
||||||
|
bodyshop {
|
||||||
|
id
|
||||||
|
convenient_company
|
||||||
|
features
|
||||||
|
timezone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
// Define the GraphQL query to get a job by RO number and shop ID
|
||||||
|
exports.GET_JOB_BY_RO_NUMBER_AND_SHOP_ID = `
|
||||||
|
query GET_JOB_BY_RO_NUMBER_AND_SHOP_ID($roNumber: String!, $shopId: uuid!) {
|
||||||
|
jobs(where: {ro_number: {_eq: $roNumber}, shopid: {_eq: $shopId}}, limit: 1) {
|
||||||
|
id
|
||||||
|
shopid
|
||||||
|
bodyshop {
|
||||||
|
timezone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Define the mutation to insert a new document
|
||||||
|
exports.INSERT_NEW_DOCUMENT = `
|
||||||
|
mutation INSERT_NEW_DOCUMENT($docInput: [documents_insert_input!]!) {
|
||||||
|
insert_documents(objects: $docInput) {
|
||||||
|
returning {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
143
server/integrations/VSSTA/vsstaIntegrationRoute.js
Normal file
143
server/integrations/VSSTA/vsstaIntegrationRoute.js
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
// Notes: At the moment we take in RO Number, and ShopID. This is not very good considering the RO number can often be null, need
|
||||||
|
// to ask if it is possible that we just send the Job ID itself, this way we don't need to really care about the bodyshop, and we
|
||||||
|
// don't risk getting a null
|
||||||
|
|
||||||
|
const axios = require("axios");
|
||||||
|
const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3");
|
||||||
|
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
|
||||||
|
const { GET_JOB_BY_RO_NUMBER_AND_SHOP_ID, INSERT_NEW_DOCUMENT } = require("../../graphql-client/queries");
|
||||||
|
const { InstanceRegion } = require("../../utils/instanceMgr");
|
||||||
|
const moment = require("moment/moment");
|
||||||
|
const client = require("../../graphql-client/graphql-client").client;
|
||||||
|
|
||||||
|
const S3_BUCKET = process.env?.IMGPROXY_DESTINATION_BUCKET;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description VSSTA integration route
|
||||||
|
* @type {string[]}
|
||||||
|
*/
|
||||||
|
const requiredParams = [
|
||||||
|
"shop_id",
|
||||||
|
"ro_nbr",
|
||||||
|
"pdf_download_link",
|
||||||
|
"company_api_key",
|
||||||
|
"scan_type",
|
||||||
|
"scan_time",
|
||||||
|
"technician",
|
||||||
|
"year",
|
||||||
|
"make",
|
||||||
|
"model"
|
||||||
|
];
|
||||||
|
|
||||||
|
const vsstaIntegrationRoute = async (req, res) => {
|
||||||
|
const { logger } = req;
|
||||||
|
|
||||||
|
if (!S3_BUCKET) {
|
||||||
|
logger.log("vssta-integration-missing-bucket", "error", "api", "vssta");
|
||||||
|
return res.status(500).json({ error: "Improper configuration" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const missingParams = requiredParams.filter((param) => !req.body[param]);
|
||||||
|
|
||||||
|
if (missingParams.length > 0) {
|
||||||
|
logger.log(`vssta-integration-missing-param`, "error", "api", "vssta", {
|
||||||
|
params: missingParams
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "Missing required parameters",
|
||||||
|
missingParams
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// technician, year, make, model, is also available.
|
||||||
|
const { shop_id, ro_nbr, pdf_download_link, scan_type, scan_time, company_api_key } = req.body;
|
||||||
|
|
||||||
|
// 1. Get the job record by ro_number and shop_id
|
||||||
|
const jobResult = await client.request(GET_JOB_BY_RO_NUMBER_AND_SHOP_ID, {
|
||||||
|
roNumber: ro_nbr,
|
||||||
|
shopId: shop_id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!jobResult.jobs || jobResult.jobs.length === 0) {
|
||||||
|
logger.log(`vssta-integration-missing-ro`, "error", "api", "vssta");
|
||||||
|
|
||||||
|
return res.status(404).json({ error: "Job not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const job = jobResult.jobs[0];
|
||||||
|
|
||||||
|
// 2. Download the base64-encoded PDF string from the provided link
|
||||||
|
const pdfResponse = await axios.get(pdf_download_link, {
|
||||||
|
responseType: "text", // Expect base64 string
|
||||||
|
headers: {
|
||||||
|
"auth-token": company_api_key
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Decode the base64 string to a PDF buffer
|
||||||
|
const base64String = pdfResponse.data.replace(/^data:application\/pdf;base64,/, "");
|
||||||
|
const pdfBuffer = Buffer.from(base64String, "base64");
|
||||||
|
|
||||||
|
// 4. Generate key for S3
|
||||||
|
const timestamp = moment(scan_time).tz(job.bodyshop.timezone).format("YYYYMMDD-HHmmss");
|
||||||
|
const fileName = `${timestamp}_VSSTA_${scan_type}`;
|
||||||
|
const s3Key = `${job.shopid}/${job.id}/${fileName.replace(/[^A-Z0-9]+/gi, "_")}.pdf`;
|
||||||
|
|
||||||
|
// 5. Generate presigned URL for S3 upload
|
||||||
|
const s3Client = new S3Client({ region: InstanceRegion() });
|
||||||
|
|
||||||
|
const putCommand = new PutObjectCommand({
|
||||||
|
Bucket: S3_BUCKET,
|
||||||
|
Key: s3Key,
|
||||||
|
ContentType: "application/pdf",
|
||||||
|
StorageClass: "INTELLIGENT_TIERING"
|
||||||
|
});
|
||||||
|
|
||||||
|
const presignedUrl = await getSignedUrl(s3Client, putCommand, { expiresIn: 360 });
|
||||||
|
|
||||||
|
// 6. Upload the decoded PDF to S3
|
||||||
|
await axios.put(presignedUrl, pdfBuffer, {
|
||||||
|
headers: { "Content-Type": "application/pdf" }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 7. Create document record in database
|
||||||
|
const documentMeta = {
|
||||||
|
jobid: job.id,
|
||||||
|
uploaded_by: "VSSTA Integration",
|
||||||
|
name: fileName,
|
||||||
|
key: s3Key,
|
||||||
|
type: "application/pdf",
|
||||||
|
extension: "pdf",
|
||||||
|
bodyshopid: job.shopid,
|
||||||
|
size: pdfBuffer.length,
|
||||||
|
takenat: scan_time
|
||||||
|
};
|
||||||
|
|
||||||
|
const documentInsert = await client.request(INSERT_NEW_DOCUMENT, {
|
||||||
|
docInput: [documentMeta]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!documentInsert.insert_documents?.returning?.length) {
|
||||||
|
logger.log(`vssta-integration-failed-to-create-document-record`, "error", "api", "vssta", {
|
||||||
|
params: missingParams
|
||||||
|
});
|
||||||
|
return res.status(500).json({ error: "Failed to create document record" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
message: "VSSTA integration successful",
|
||||||
|
documentId: documentInsert.insert_documents.returning[0].id
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(`vssta-integration-general`, "error", "api", "vssta", {
|
||||||
|
error: error?.message,
|
||||||
|
stack: error?.stack
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = vsstaIntegrationRoute;
|
||||||
@@ -1,64 +1,22 @@
|
|||||||
const path = require("path");
|
|
||||||
require("dotenv").config({
|
|
||||||
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
|
||||||
});
|
|
||||||
const queries = require("../graphql-client/queries");
|
|
||||||
const Dinero = require("dinero.js");
|
const Dinero = require("dinero.js");
|
||||||
const qs = require("query-string");
|
const qs = require("query-string");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const moment = require("moment");
|
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
const { sendTaskEmail } = require("../email/sendemail");
|
const { isEmpty, isNumber } = require("lodash");
|
||||||
const generateEmailTemplate = require("../email/generateTemplate");
|
const handleCommentBasedPayment = require("./lib/handleCommentBasedPayment");
|
||||||
|
const handleInvoiceBasedPayment = require("./lib/handleInvoiceBasedPayment");
|
||||||
|
const logValidationError = require("./lib/handlePaymentValidationError");
|
||||||
|
const getCptellerUrl = require("./lib/getCptellerUrl");
|
||||||
|
const getShopCredentials = require("./lib/getShopCredentials");
|
||||||
|
const decodeComment = require("./lib/decodeComment");
|
||||||
|
|
||||||
const domain = process.env.NODE_ENV ? "secure" : "test";
|
/**
|
||||||
|
* @description Get lightbox credentials for the shop
|
||||||
const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");
|
* @param req
|
||||||
const { InstanceRegion, InstanceEndpoints } = require("../utils/instanceMgr");
|
* @param res
|
||||||
|
* @returns {Promise<void>}
|
||||||
const client = new SecretsManagerClient({
|
*/
|
||||||
region: InstanceRegion()
|
const lightboxCredentials = async (req, res) => {
|
||||||
});
|
|
||||||
|
|
||||||
const gqlClient = require("../graphql-client/graphql-client").client;
|
|
||||||
|
|
||||||
const getShopCredentials = async (bodyshop) => {
|
|
||||||
// Development only
|
|
||||||
if (process.env.NODE_ENV === undefined) {
|
|
||||||
return {
|
|
||||||
merchantkey: process.env.INTELLIPAY_MERCHANTKEY,
|
|
||||||
apikey: process.env.INTELLIPAY_APIKEY
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Production code
|
|
||||||
if (bodyshop?.imexshopid) {
|
|
||||||
try {
|
|
||||||
const secret = await client.send(
|
|
||||||
new GetSecretValueCommand({
|
|
||||||
SecretId: `intellipay-credentials-${bodyshop.imexshopid}`,
|
|
||||||
VersionStage: "AWSCURRENT" // VersionStage defaults to AWSCURRENT if unspecified
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return JSON.parse(secret.SecretString);
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
error: error.message
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const decodeComment = (comment) => {
|
|
||||||
try {
|
|
||||||
return comment ? JSON.parse(Buffer.from(comment, "base64").toString()) : null;
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
} catch (error) {
|
|
||||||
return null; // Handle malformed base64 string gracefully
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.lightbox_credentials = async (req, res) => {
|
|
||||||
const decodedComment = decodeComment(req.body?.comment);
|
const decodedComment = decodeComment(req.body?.comment);
|
||||||
const logMeta = {
|
const logMeta = {
|
||||||
iPayData: req.body?.iPayData,
|
iPayData: req.body?.iPayData,
|
||||||
@@ -74,17 +32,17 @@ exports.lightbox_credentials = async (req, res) => {
|
|||||||
|
|
||||||
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
||||||
|
|
||||||
if (shopCredentials.error) {
|
if (shopCredentials?.error) {
|
||||||
logger.log("intellipay-credentials-error", "ERROR", req.user?.email, null, {
|
logger.log("intellipay-credentials-error", "ERROR", req.user?.email, null, {
|
||||||
message: shopCredentials.error?.message,
|
message: shopCredentials.error?.message,
|
||||||
...logMeta
|
...logMeta
|
||||||
});
|
});
|
||||||
res.json({
|
|
||||||
|
return res.json({
|
||||||
message: shopCredentials.error?.message,
|
message: shopCredentials.error?.message,
|
||||||
type: "intellipay-credentials-error",
|
type: "intellipay-credentials-error",
|
||||||
...logMeta
|
...logMeta
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -95,7 +53,10 @@ exports.lightbox_credentials = async (req, res) => {
|
|||||||
...shopCredentials,
|
...shopCredentials,
|
||||||
operatingenv: "businessattended"
|
operatingenv: "businessattended"
|
||||||
}),
|
}),
|
||||||
url: `https://${domain}.cpteller.com/api/custapi.cfc?method=autoterminal${req.body.refresh ? "_refresh" : ""}` //autoterminal_refresh
|
url: getCptellerUrl({
|
||||||
|
apiType: "custapi",
|
||||||
|
params: { method: `autoterminal${req.body.refresh ? "_refresh" : ""}` }
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await axios(options);
|
const response = await axios(options);
|
||||||
@@ -105,13 +66,14 @@ exports.lightbox_credentials = async (req, res) => {
|
|||||||
...logMeta
|
...logMeta
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send(response.data);
|
return res.send(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("intellipay-lightbox-error", "ERROR", req.user?.email, null, {
|
logger.log("intellipay-lightbox-error", "ERROR", req.user?.email, null, {
|
||||||
message: error?.message,
|
message: error?.message,
|
||||||
...logMeta
|
...logMeta
|
||||||
});
|
});
|
||||||
res.json({
|
|
||||||
|
return res.json({
|
||||||
message: error?.message,
|
message: error?.message,
|
||||||
type: "intellipay-lightbox-error",
|
type: "intellipay-lightbox-error",
|
||||||
...logMeta
|
...logMeta
|
||||||
@@ -119,7 +81,13 @@ exports.lightbox_credentials = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.payment_refund = async (req, res) => {
|
/**
|
||||||
|
* @description Process payment refund
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
const paymentRefund = async (req, res) => {
|
||||||
const decodedComment = decodeComment(req.body.iPayData?.comment);
|
const decodedComment = decodeComment(req.body.iPayData?.comment);
|
||||||
const logResponseMeta = {
|
const logResponseMeta = {
|
||||||
iPayData: req.body?.iPayData,
|
iPayData: req.body?.iPayData,
|
||||||
@@ -137,18 +105,17 @@ exports.payment_refund = async (req, res) => {
|
|||||||
|
|
||||||
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
||||||
|
|
||||||
if (shopCredentials.error) {
|
if (shopCredentials?.error) {
|
||||||
logger.log("intellipay-refund-credentials-error", "ERROR", req.user?.email, null, {
|
logger.log("intellipay-refund-credentials-error", "ERROR", req.user?.email, null, {
|
||||||
credentialsError: shopCredentials.error,
|
credentialsError: shopCredentials.error,
|
||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(400).json({
|
return res.status(400).json({
|
||||||
credentialsError: shopCredentials.error,
|
credentialsError: shopCredentials.error,
|
||||||
type: "intellipay-refund-credentials-error",
|
type: "intellipay-refund-credentials-error",
|
||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -161,7 +128,11 @@ exports.payment_refund = async (req, res) => {
|
|||||||
paymentid: req.body.paymentid,
|
paymentid: req.body.paymentid,
|
||||||
amount: req.body.amount
|
amount: req.body.amount
|
||||||
}),
|
}),
|
||||||
url: `https://${domain}.cpteller.com/api/26/webapi.cfc?method=payment_refund`
|
url: getCptellerUrl({
|
||||||
|
apiType: "webapi",
|
||||||
|
version: "26",
|
||||||
|
params: { method: "payment_refund" }
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.log("intellipay-refund-options-prepared", "DEBUG", req.user?.email, null, {
|
logger.log("intellipay-refund-options-prepared", "DEBUG", req.user?.email, null, {
|
||||||
@@ -176,13 +147,14 @@ exports.payment_refund = async (req, res) => {
|
|||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send(response.data);
|
return res.send(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("intellipay-refund-error", "ERROR", req.user?.email, null, {
|
logger.log("intellipay-refund-error", "ERROR", req.user?.email, null, {
|
||||||
message: error?.message,
|
message: error?.message,
|
||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
res.status(500).json({
|
|
||||||
|
return res.status(500).json({
|
||||||
message: error?.message,
|
message: error?.message,
|
||||||
type: "intellipay-refund-error",
|
type: "intellipay-refund-error",
|
||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
@@ -190,7 +162,13 @@ exports.payment_refund = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.generate_payment_url = async (req, res) => {
|
/**
|
||||||
|
* @description Generate payment URL for the shop
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
const generatePaymentUrl = async (req, res) => {
|
||||||
const decodedComment = decodeComment(req.body.comment);
|
const decodedComment = decodeComment(req.body.comment);
|
||||||
const logResponseMeta = {
|
const logResponseMeta = {
|
||||||
iPayData: req.body?.iPayData,
|
iPayData: req.body?.iPayData,
|
||||||
@@ -210,17 +188,17 @@ exports.generate_payment_url = async (req, res) => {
|
|||||||
|
|
||||||
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
||||||
|
|
||||||
if (shopCredentials.error) {
|
if (shopCredentials?.error) {
|
||||||
logger.log("intellipay-generate-payment-url-credentials-error", "ERROR", req.user?.email, null, {
|
logger.log("intellipay-generate-payment-url-credentials-error", "ERROR", req.user?.email, null, {
|
||||||
message: shopCredentials.error?.message,
|
message: shopCredentials.error?.message,
|
||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
res.status(400).json({
|
|
||||||
|
return res.status(400).json({
|
||||||
message: shopCredentials.error?.message,
|
message: shopCredentials.error?.message,
|
||||||
type: "intellipay-generate-payment-url-credentials-error",
|
type: "intellipay-generate-payment-url-credentials-error",
|
||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -235,7 +213,10 @@ exports.generate_payment_url = async (req, res) => {
|
|||||||
invoice: req.body.invoice,
|
invoice: req.body.invoice,
|
||||||
createshorturl: true
|
createshorturl: true
|
||||||
}),
|
}),
|
||||||
url: `https://${domain}.cpteller.com/api/custapi.cfc?method=generate_lightbox_url`
|
url: getCptellerUrl({
|
||||||
|
apiType: "custapi",
|
||||||
|
params: { method: "generate_lightbox_url" }
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.log("intellipay-generate-payment-url-options-prepared", "DEBUG", req.user?.email, null, {
|
logger.log("intellipay-generate-payment-url-options-prepared", "DEBUG", req.user?.email, null, {
|
||||||
@@ -251,18 +232,25 @@ exports.generate_payment_url = async (req, res) => {
|
|||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send(response.data);
|
return res.send(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("intellipay-generate-payment-url-error", "ERROR", req.user?.email, null, {
|
logger.log("intellipay-generate-payment-url-error", "ERROR", req.user?.email, null, {
|
||||||
message: error?.message,
|
message: error?.message,
|
||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
res.status(500).json({ message: error?.message, ...logResponseMeta });
|
|
||||||
|
return res.status(500).json({ message: error?.message, ...logResponseMeta });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
//Reference: https://intellipay.com/dist/webapi26.html#operation/fee
|
/**
|
||||||
exports.checkfee = async (req, res) => {
|
* @description Check the fee for a given amount
|
||||||
|
* Reference: https://intellipay.com/dist/webapi26.html#operation/fee
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
const checkFee = async (req, res) => {
|
||||||
const logResponseMeta = {
|
const logResponseMeta = {
|
||||||
bodyshop: {
|
bodyshop: {
|
||||||
id: req.body?.bodyshop?.id,
|
id: req.body?.bodyshop?.id,
|
||||||
@@ -275,24 +263,24 @@ exports.checkfee = async (req, res) => {
|
|||||||
|
|
||||||
logger.log("intellipay-checkfee-request-received", "DEBUG", req.user?.email, null, logResponseMeta);
|
logger.log("intellipay-checkfee-request-received", "DEBUG", req.user?.email, null, logResponseMeta);
|
||||||
|
|
||||||
if (!req.body.amount || req.body.amount <= 0) {
|
if (!isNumber(req.body?.amount) || req.body?.amount <= 0) {
|
||||||
logger.log("intellipay-checkfee-skip", "DEBUG", req.user?.email, null, {
|
logger.log("intellipay-checkfee-skip", "DEBUG", req.user?.email, null, {
|
||||||
message: "Amount is zero or undefined, skipping fee check.",
|
message: "Amount is zero or undefined, skipping fee check.",
|
||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
res.json({ fee: 0 });
|
|
||||||
return;
|
return res.json({ fee: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
||||||
|
|
||||||
if (shopCredentials.error) {
|
if (shopCredentials?.error) {
|
||||||
logger.log("intellipay-checkfee-credentials-error", "ERROR", req.user?.email, null, {
|
logger.log("intellipay-checkfee-credentials-error", "ERROR", req.user?.email, null, {
|
||||||
message: shopCredentials.error?.message,
|
message: shopCredentials.error?.message,
|
||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
res.status(400).json({ error: shopCredentials.error?.message, ...logResponseMeta });
|
|
||||||
return;
|
return res.status(400).json({ error: shopCredentials.error?.message, ...logResponseMeta });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -313,7 +301,7 @@ exports.checkfee = async (req, res) => {
|
|||||||
},
|
},
|
||||||
{ sort: false } // Ensure query string order is preserved
|
{ sort: false } // Ensure query string order is preserved
|
||||||
),
|
),
|
||||||
url: `https://${domain}.cpteller.com/api/26/webapi.cfc`
|
url: getCptellerUrl({ apiType: "webapi", version: "26" })
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.log("intellipay-checkfee-options-prepared", "DEBUG", req.user?.email, null, {
|
logger.log("intellipay-checkfee-options-prepared", "DEBUG", req.user?.email, null, {
|
||||||
@@ -328,200 +316,92 @@ exports.checkfee = async (req, res) => {
|
|||||||
message: response.data?.error,
|
message: response.data?.error,
|
||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
res.status(400).json({
|
|
||||||
|
return res.status(400).json({
|
||||||
error: response.data?.error,
|
error: response.data?.error,
|
||||||
type: "intellipay-checkfee-api-error",
|
type: "intellipay-checkfee-api-error",
|
||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
} else if (response.data < 0) {
|
}
|
||||||
|
|
||||||
|
if (response.data < 0) {
|
||||||
logger.log("intellipay-checkfee-negative-fee", "ERROR", req.user?.email, null, {
|
logger.log("intellipay-checkfee-negative-fee", "ERROR", req.user?.email, null, {
|
||||||
message: "Fee amount returned is negative.",
|
message: "Fee amount returned is negative.",
|
||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
res.json({
|
|
||||||
|
return res.json({
|
||||||
error: "Fee amount negative. Check API credentials & account configuration.",
|
error: "Fee amount negative. Check API credentials & account configuration.",
|
||||||
...logResponseMeta,
|
...logResponseMeta,
|
||||||
type: "intellipay-checkfee-negative-fee"
|
type: "intellipay-checkfee-negative-fee"
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
logger.log("intellipay-checkfee-success", "DEBUG", req.user?.email, null, {
|
|
||||||
fee: response.data,
|
|
||||||
...logResponseMeta
|
|
||||||
});
|
|
||||||
res.json({ fee: response.data, ...logResponseMeta });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.log("intellipay-checkfee-success", "DEBUG", req.user?.email, null, {
|
||||||
|
fee: response.data,
|
||||||
|
...logResponseMeta
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({ fee: response.data, ...logResponseMeta });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("intellipay-checkfee-error", "ERROR", req.user?.email, null, {
|
logger.log("intellipay-checkfee-error", "ERROR", req.user?.email, null, {
|
||||||
message: error?.message,
|
message: error?.message,
|
||||||
...logResponseMeta
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
res.status(500).json({ error: error?.message, logResponseMeta });
|
|
||||||
|
return res.status(500).json({ error: error?.message, logResponseMeta });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.postback = async (req, res) => {
|
/**
|
||||||
|
* @description Handle the postback from Intellipay
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Handle the postback from Intellipay payment system
|
||||||
|
*/
|
||||||
|
const postBack = async (req, res) => {
|
||||||
const { body: values } = req;
|
const { body: values } = req;
|
||||||
const decodedComment = decodeComment(values?.comment);
|
const decodedComment = decodeComment(values?.comment);
|
||||||
|
const logMeta = { iprequest: values, decodedComment };
|
||||||
|
|
||||||
const logResponseMeta = {
|
logger.log("intellipay-postback-received", "DEBUG", "api", null, logMeta);
|
||||||
iprequest: values,
|
|
||||||
decodedComment
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.log("intellipay-postback-received", "DEBUG", "api", null, logResponseMeta);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ((!values.invoice || values.invoice === "") && !decodedComment) {
|
// Handle empty/invalid requests
|
||||||
//invoice is specified through the pay link. Comment by IO.
|
if (isEmpty(values?.invoice) && !decodedComment) {
|
||||||
logger.log("intellipay-postback-ignored", "DEBUG", "api", null, {
|
logger.log("intellipay-postback-ignored", "DEBUG", "api", null, {
|
||||||
message: "No invoice or comment provided",
|
message: "No invoice or comment provided",
|
||||||
...logResponseMeta
|
...logMeta
|
||||||
});
|
});
|
||||||
res.sendStatus(200);
|
return res.sendStatus(200);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process payment based on data type
|
||||||
if (decodedComment) {
|
if (decodedComment) {
|
||||||
//Shifted the order to have this first to retain backwards compatibility for the old style of short link.
|
return await handleCommentBasedPayment(values, decodedComment, logger, logMeta, res);
|
||||||
//This has been triggered by IO and may have multiple jobs.
|
} else if (values?.invoice) {
|
||||||
const parsedComment = decodedComment;
|
return await handleInvoiceBasedPayment(values, logger, logMeta, res);
|
||||||
|
} else {
|
||||||
logger.log("intellipay-postback-parsed-comment", "DEBUG", "api", null, {
|
// This should be caught by first validation, but as a safeguard
|
||||||
parsedComment,
|
logValidationError("intellipay-postback-invalid", "No valid invoice or comment provided", logMeta);
|
||||||
...logResponseMeta
|
return res.status(400).send("Bad Request: No valid invoice or comment provided");
|
||||||
});
|
|
||||||
|
|
||||||
//Adding in the user email to the short pay email.
|
|
||||||
//Need to check this to ensure backwards compatibility for clients that don't update.
|
|
||||||
|
|
||||||
const partialPayments = Array.isArray(parsedComment) ? parsedComment : parsedComment.payments;
|
|
||||||
|
|
||||||
// Fetch jobs by job IDs
|
|
||||||
const jobs = await gqlClient.request(queries.GET_JOBS_BY_PKS, {
|
|
||||||
ids: partialPayments.map((p) => p.jobid)
|
|
||||||
});
|
|
||||||
const bodyshop = await gqlClient.request(queries.GET_BODYSHOP_BY_ID, {
|
|
||||||
id: jobs.jobs[0].shopid
|
|
||||||
});
|
|
||||||
const ipMapping = bodyshop.bodyshops_by_pk.intellipay_config?.payment_map;
|
|
||||||
|
|
||||||
logger.log("intellipay-postback-jobs-fetched", "DEBUG", "api", null, {
|
|
||||||
jobs,
|
|
||||||
parsedComment,
|
|
||||||
...logResponseMeta
|
|
||||||
});
|
|
||||||
|
|
||||||
// Insert new payments
|
|
||||||
const paymentResult = await gqlClient.request(queries.INSERT_NEW_PAYMENT, {
|
|
||||||
paymentInput: partialPayments.map((p) => ({
|
|
||||||
amount: p.amount,
|
|
||||||
transactionid: values.authcode,
|
|
||||||
payer: "Customer",
|
|
||||||
type: ipMapping ? ipMapping[(values.cardtype || "").toLowerCase()] || values.cardtype : values.cardtype,
|
|
||||||
jobid: p.jobid,
|
|
||||||
date: moment(Date.now()),
|
|
||||||
payment_responses: {
|
|
||||||
data: {
|
|
||||||
amount: values.total,
|
|
||||||
bodyshopid: bodyshop.bodyshops_by_pk.id,
|
|
||||||
jobid: p.jobid,
|
|
||||||
declinereason: "Approved",
|
|
||||||
ext_paymentid: values.paymentid,
|
|
||||||
successful: true,
|
|
||||||
response: values
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.log("intellipay-postback-payment-success", "DEBUG", "api", null, {
|
|
||||||
paymentResult,
|
|
||||||
jobs,
|
|
||||||
parsedComment,
|
|
||||||
...logResponseMeta
|
|
||||||
});
|
|
||||||
|
|
||||||
if (values.origin === "OneLink" && parsedComment.userEmail) {
|
|
||||||
sendTaskEmail({
|
|
||||||
to: parsedComment.userEmail,
|
|
||||||
subject: `New Payment(s) Received - RO ${jobs.jobs.map((j) => j.ro_number).join(", ")}`,
|
|
||||||
type: "html",
|
|
||||||
html: generateEmailTemplate({
|
|
||||||
header: "New Payment(s) Received",
|
|
||||||
subHeader: "",
|
|
||||||
body: jobs.jobs
|
|
||||||
.map(
|
|
||||||
(job) =>
|
|
||||||
`Reference: <a href="${InstanceEndpoints()}/manage/jobs/${job.id}">${job.ro_number || "N/A"}</a> | ${job.ownr_co_nm ? job.ownr_co_nm : `${job.ownr_fn || ""} ${job.ownr_ln || ""}`.trim()} | ${`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""}`.trim()} | $${partialPayments.find((p) => p.jobid === job.id).amount}`
|
|
||||||
)
|
|
||||||
.join("<br/>")
|
|
||||||
})
|
|
||||||
}).catch((error) => {
|
|
||||||
logger.log("intellipay-postback-email-error", "ERROR", "api", null, {
|
|
||||||
message: error.message,
|
|
||||||
jobs,
|
|
||||||
paymentResult,
|
|
||||||
...logResponseMeta
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
res.sendStatus(200);
|
|
||||||
} else if (values.invoice) {
|
|
||||||
const job = await gqlClient.request(queries.GET_JOB_BY_PK, {
|
|
||||||
id: values.invoice
|
|
||||||
});
|
|
||||||
|
|
||||||
const bodyshop = await gqlClient.request(queries.GET_BODYSHOP_BY_ID, {
|
|
||||||
id: job.jobs_by_pk.shopid
|
|
||||||
});
|
|
||||||
const ipMapping = bodyshop.bodyshops_by_pk.intellipay_config?.payment_map;
|
|
||||||
|
|
||||||
logger.log("intellipay-postback-invoice-job-fetched", "DEBUG", "api", null, {
|
|
||||||
job,
|
|
||||||
bodyshop,
|
|
||||||
...logResponseMeta
|
|
||||||
});
|
|
||||||
|
|
||||||
const paymentResult = await gqlClient.request(queries.INSERT_NEW_PAYMENT, {
|
|
||||||
paymentInput: {
|
|
||||||
amount: values.total,
|
|
||||||
transactionid: values.authcode,
|
|
||||||
payer: "Customer",
|
|
||||||
type: ipMapping ? ipMapping[(values.cardtype || "").toLowerCase()] || values.cardtype : values.cardtype,
|
|
||||||
jobid: values.invoice,
|
|
||||||
date: moment(Date.now())
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.log("intellipay-postback-invoice-payment-success", "DEBUG", "api", null, {
|
|
||||||
paymentResult,
|
|
||||||
...logResponseMeta
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseResults = await gqlClient.request(queries.INSERT_PAYMENT_RESPONSE, {
|
|
||||||
paymentResponse: {
|
|
||||||
amount: values.total,
|
|
||||||
bodyshopid: bodyshop.bodyshops_by_pk.id,
|
|
||||||
paymentid: paymentResult.id,
|
|
||||||
jobid: values.invoice,
|
|
||||||
declinereason: "Approved",
|
|
||||||
ext_paymentid: values.paymentid,
|
|
||||||
successful: true,
|
|
||||||
response: values
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.log("intellipay-postback-invoice-response-success", "DEBUG", "api", null, {
|
|
||||||
responseResults,
|
|
||||||
...logResponseMeta
|
|
||||||
});
|
|
||||||
res.sendStatus(200);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("intellipay-postback-error", "ERROR", "api", null, {
|
logger.log("intellipay-postback-error", "ERROR", "api", null, {
|
||||||
message: error?.message,
|
message: error?.message,
|
||||||
...logResponseMeta
|
...logMeta
|
||||||
});
|
});
|
||||||
res.status(400).json({ successful: false, error: error.message, ...logResponseMeta });
|
return res.status(400).json({ successful: false, error: error.message, ...logMeta });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
lightboxCredentials,
|
||||||
|
paymentRefund,
|
||||||
|
generatePaymentUrl,
|
||||||
|
checkFee,
|
||||||
|
postBack
|
||||||
|
};
|
||||||
|
|||||||
14
server/intellipay/lib/decodeComment.js
Normal file
14
server/intellipay/lib/decodeComment.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* @description Decode the comment from base64
|
||||||
|
* @param comment
|
||||||
|
* @returns {any|null}
|
||||||
|
*/
|
||||||
|
const decodeComment = (comment) => {
|
||||||
|
try {
|
||||||
|
return comment ? JSON.parse(Buffer.from(comment, "base64").toString()) : null;
|
||||||
|
} catch (error) {
|
||||||
|
return null; // Handle malformed base64 string gracefully
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = decodeComment;
|
||||||
34
server/intellipay/lib/getCptellerUrl.js
Normal file
34
server/intellipay/lib/getCptellerUrl.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Generates a properly formatted Cpteller API URL
|
||||||
|
* @param {Object} options - URL configuration options
|
||||||
|
* @param {string} options.apiType - 'webapi' or 'custapi'
|
||||||
|
* @param {string} [options.version] - API version (e.g., '26' for webapi)
|
||||||
|
* @param {Object} [options.params] - URL query parameters
|
||||||
|
* @returns {string} - The formatted Cpteller URL
|
||||||
|
*/
|
||||||
|
const getCptellerUrl = (options) => {
|
||||||
|
const domain = process.env?.NODE_ENV === "production" ? "secure" : "test";
|
||||||
|
|
||||||
|
const { apiType = "webapi", version, params = {} } = options;
|
||||||
|
|
||||||
|
// Base URL construction
|
||||||
|
let url = `https://${domain}.cpteller.com/api/`;
|
||||||
|
|
||||||
|
// Add version if specified for webapi
|
||||||
|
if (apiType === "webapi" && version) {
|
||||||
|
url += `${version}/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the API endpoint
|
||||||
|
url += `${apiType}.cfc`;
|
||||||
|
|
||||||
|
// Add query parameters if any exist
|
||||||
|
const queryParams = new URLSearchParams(params).toString();
|
||||||
|
if (queryParams) {
|
||||||
|
url += `?${queryParams}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = getCptellerUrl;
|
||||||
12
server/intellipay/lib/getPaymentType.js
Normal file
12
server/intellipay/lib/getPaymentType.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* @description Get payment type based on IP mapping
|
||||||
|
* @param ipMapping
|
||||||
|
* @param cardType
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
const getPaymentType = (ipMapping, cardType) => {
|
||||||
|
const normalizedCardType = (cardType || "").toLowerCase();
|
||||||
|
return ipMapping ? ipMapping[normalizedCardType] || cardType : cardType;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = getPaymentType;
|
||||||
40
server/intellipay/lib/getShopCredentials.js
Normal file
40
server/intellipay/lib/getShopCredentials.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");
|
||||||
|
const { InstanceRegion } = require("../../utils/instanceMgr");
|
||||||
|
|
||||||
|
const client = new SecretsManagerClient({
|
||||||
|
region: InstanceRegion()
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Get shop credentials from AWS Secrets Manager
|
||||||
|
* @param bodyshop
|
||||||
|
* @returns {Promise<{error}|{merchantkey: *, apikey: *}|any>}
|
||||||
|
*/
|
||||||
|
const getShopCredentials = async (bodyshop) => {
|
||||||
|
// In Dev/Testing we will use the environment variables
|
||||||
|
if (process.env?.NODE_ENV !== "production") {
|
||||||
|
return {
|
||||||
|
merchantkey: process.env.INTELLIPAY_MERCHANTKEY,
|
||||||
|
apikey: process.env.INTELLIPAY_APIKEY
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// In Production, we will use the AWS Secrets Manager
|
||||||
|
if (bodyshop?.imexshopid) {
|
||||||
|
try {
|
||||||
|
const secret = await client.send(
|
||||||
|
new GetSecretValueCommand({
|
||||||
|
SecretId: `intellipay-credentials-${bodyshop.imexshopid}`,
|
||||||
|
VersionStage: "AWSCURRENT" // VersionStage defaults to AWSCURRENT if unspecified
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return JSON.parse(secret.SecretString);
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = getShopCredentials;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user