Compare commits
179 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
0a68d2791d | ||
|
|
11928d9a7e | ||
|
|
c169bb5d5d | ||
|
|
3cc4f1c63e | ||
|
|
5237b1d535 | ||
|
|
cd56c50cf9 | ||
|
|
a18ce18d72 | ||
|
|
3691d32aaa | ||
|
|
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 | ||
|
|
7e741e4af9 | ||
|
|
24d47ae1c5 | ||
|
|
f556d59ad7 | ||
|
|
09c4662436 | ||
|
|
9bf6ba9cf0 | ||
|
|
7843ca9b1a | ||
|
|
c78b9866a3 | ||
|
|
c8701aba63 | ||
|
|
09c1a8ae35 | ||
|
|
0ef2814de3 | ||
|
|
8e105f0b36 | ||
|
|
ba4da3e35c | ||
|
|
1b8be56c15 | ||
|
|
f6e65f82e5 | ||
|
|
8b7bb099f3 | ||
|
|
2b26db78eb | ||
|
|
663d91b648 | ||
|
|
2a7686ec75 | ||
|
|
549cb56cdf | ||
|
|
146bb6c5c0 | ||
|
|
67b6da7c31 | ||
|
|
c2d96922c8 | ||
|
|
624894621b | ||
|
|
3fba215266 | ||
|
|
bbf291e8f3 | ||
|
|
70b4ec7948 | ||
|
|
341fc09c22 | ||
|
|
a3ec364034 | ||
|
|
fb30529808 | ||
|
|
e1728b275b | ||
|
|
46999145fc | ||
|
|
9d1f810af2 | ||
|
|
10d55df461 | ||
|
|
b9693aae95 | ||
|
|
02f5f1985c | ||
|
|
37edceee84 | ||
|
|
1fd63012b0 | ||
|
|
cf084fa168 | ||
|
|
96af289640 | ||
|
|
f8df351de6 | ||
|
|
b8c096f4ff | ||
|
|
93ad23b615 | ||
|
|
0a918535bb | ||
|
|
4863b16b5f | ||
|
|
a27f5e2153 | ||
|
|
3ffea50072 | ||
|
|
34af7d3880 | ||
|
|
4432721c27 | ||
|
|
65ad4d9426 | ||
|
|
18924b4f08 | ||
|
|
c524f5f0e0 | ||
|
|
2fbac78eec | ||
|
|
4734971d48 | ||
|
|
fc1055c644 | ||
|
|
24798390b5 | ||
|
|
a992dead04 | ||
|
|
f039cd8d0d | ||
|
|
494e691230 | ||
|
|
4cc7366290 | ||
|
|
fd9d660a61 | ||
|
|
0b5bd4f718 | ||
|
|
7511b42bd4 | ||
|
|
26f94c4d5b | ||
|
|
aa55f4840b | ||
|
|
2810428d19 | ||
|
|
83da64f96b | ||
|
|
1f8d027f97 | ||
|
|
2f8ba20a5b | ||
|
|
b525f920e0 | ||
|
|
91fe6745fe | ||
|
|
b9073fe3f5 | ||
|
|
2c95b49ae1 | ||
|
|
9bde06e110 | ||
|
|
30449ca113 | ||
|
|
0405d19f98 | ||
|
|
2c5310403b | ||
|
|
e2ef4f1caf | ||
|
|
b32a2d4d86 | ||
|
|
7c92484ae0 | ||
|
|
67cada5d8e | ||
|
|
4bf68b637f | ||
|
|
b40c433865 | ||
|
|
55ed499ab5 | ||
|
|
353bc3bc05 | ||
|
|
df5c96345c | ||
|
|
2c7c187c45 | ||
|
|
3a5a78d60a | ||
|
|
6dd2871c07 | ||
|
|
ef36ab9da0 | ||
|
|
a917f6bcdf | ||
|
|
c5d6457146 | ||
|
|
f3831e934f |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -121,3 +121,12 @@ logs/oAuthClient-log.log
|
||||
/*.env.*
|
||||
.idea/*
|
||||
.idea
|
||||
|
||||
# Vitest
|
||||
vitest-report*/
|
||||
vitest-coverage/
|
||||
*.vitest.log
|
||||
test-output.txt
|
||||
server/job/test/fixtures
|
||||
|
||||
.github
|
||||
|
||||
@@ -56,4 +56,5 @@ COPY . .
|
||||
EXPOSE 4000 9229
|
||||
|
||||
# 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",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"express": "^4.21.1",
|
||||
"mailparser": "^3.7.1",
|
||||
"express": "^5.1.0",
|
||||
"mailparser": "^3.7.2",
|
||||
"node-fetch": "^3.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,3 +12,5 @@ VITE_APP_AXIOS_BASE_API_URL=/api/
|
||||
VITE_APP_REPORTS_SERVER_URL=https://reports.test.imex.online
|
||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||
VITE_APP_INSTANCE=IMEX
|
||||
TEST_USERNAME="test@imex.dev"
|
||||
TEST_PASSWORD="test123"
|
||||
|
||||
@@ -14,3 +14,5 @@ VITE_APP_REPORTS_SERVER_URL=https://reports.test.romeonline.io
|
||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||
VITE_APP_COUNTRY=USA
|
||||
VITE_APP_INSTANCE=ROME
|
||||
TEST_USERNAME="test@imex.dev"
|
||||
TEST_PASSWORD="test123"
|
||||
|
||||
11
client/.gitignore
vendored
11
client/.gitignore
vendored
@@ -1,3 +1,14 @@
|
||||
# Vitest
|
||||
vitest-report*/
|
||||
vitest-coverage/
|
||||
*.vitest.log
|
||||
test-output.txt
|
||||
|
||||
# Playwright
|
||||
playwright-report/
|
||||
test-results/
|
||||
playwright/.cache/
|
||||
*.playwright.log
|
||||
|
||||
# Sentry Config File
|
||||
.sentryclirc
|
||||
|
||||
2674
client/package-lock.json
generated
2674
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,24 +8,23 @@
|
||||
"private": true,
|
||||
"proxy": "http://localhost:4000",
|
||||
"dependencies": {
|
||||
"@ant-design/pro-layout": "^7.22.3",
|
||||
"@apollo/client": "^3.13.5",
|
||||
"@ant-design/pro-layout": "^7.22.4",
|
||||
"@apollo/client": "^3.13.6",
|
||||
"@emotion/is-prop-valid": "^1.3.1",
|
||||
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
||||
"@firebase/analytics": "^0.10.12",
|
||||
"@firebase/app": "^0.11.3",
|
||||
"@firebase/auth": "^1.9.1",
|
||||
"@firebase/app": "^0.11.4",
|
||||
"@firebase/auth": "^1.10.0",
|
||||
"@firebase/firestore": "^4.7.10",
|
||||
"@firebase/messaging": "^0.12.17",
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.6.1",
|
||||
"@sentry/cli": "^2.42.4",
|
||||
"@sentry/react": "^9.9.0",
|
||||
"@sentry/vite-plugin": "^3.2.2",
|
||||
"@splitsoftware/splitio-react": "^2.0.1",
|
||||
"@sentry/cli": "^2.43.0",
|
||||
"@sentry/react": "^9.11.0",
|
||||
"@sentry/vite-plugin": "^3.3.1",
|
||||
"@splitsoftware/splitio-react": "^2.1.1",
|
||||
"@tanem/react-nprogress": "^5.0.53",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"antd": "^5.24.5",
|
||||
"antd": "^5.24.6",
|
||||
"apollo-link-logger": "^2.0.1",
|
||||
"apollo-link-sentry": "^4.2.0",
|
||||
"autosize": "^6.0.1",
|
||||
@@ -58,7 +57,7 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-drag-listview": "^2.0.0",
|
||||
"react-grid-gallery": "^1.0.1",
|
||||
"react-grid-layout": "1.3.4",
|
||||
"react-grid-layout": "^1.3.4",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-image-lightbox": "^5.1.4",
|
||||
@@ -71,16 +70,16 @@
|
||||
"react-router-dom": "^6.30.0",
|
||||
"react-sticky": "^6.0.3",
|
||||
"react-virtuoso": "^4.12.5",
|
||||
"recharts": "^2.15.0",
|
||||
"recharts": "^2.15.2",
|
||||
"redux": "^5.0.1",
|
||||
"redux-actions": "^3.0.3",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-saga": "^1.3.0",
|
||||
"redux-state-sync": "^3.1.4",
|
||||
"reselect": "^5.1.1",
|
||||
"sass": "^1.86.0",
|
||||
"sass": "^1.86.3",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"styled-components": "^6.1.16",
|
||||
"styled-components": "^6.1.17",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
"use-memo-one": "^1.1.3",
|
||||
"vite-plugin-ejs": "^1.7.0",
|
||||
@@ -100,7 +99,14 @@
|
||||
"build:production:imex": "env-cmd -f .env.production.imex npm run build",
|
||||
"build:production:rome": "env-cmd -f .env.production.rome npm run build",
|
||||
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .",
|
||||
"eulaize": "node src/utils/eulaize.js"
|
||||
"eulaize": "node src/utils/eulaize.js",
|
||||
"test:unit": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:e2e:imex": "playwright test --config playwright.config.js",
|
||||
"test:e2e:rome": "playwright test --config playwright.rome.config.js",
|
||||
"test:e2e:imex:headed": "playwright test --config playwright.config.js --headed",
|
||||
"test:e2e:rome:headed": "playwright test --config playwright.rome.config.js --headed",
|
||||
"test:e2e:report": "playwright show-report"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -124,29 +130,37 @@
|
||||
"@ant-design/icons": "^6.0.0",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
"@dotenvx/dotenvx": "^1.39.0",
|
||||
"@dotenvx/dotenvx": "^1.39.1",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@eslint/js": "^9.23.0",
|
||||
"@sentry/webpack-plugin": "^3.2.2",
|
||||
"@eslint/js": "^9.24.0",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@sentry/webpack-plugin": "^3.3.1",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"browserslist": "^4.24.4",
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
"chalk": "^5.4.1",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^15.15.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"memfs": "^4.17.0",
|
||||
"os-browserify": "^0.3.0",
|
||||
"playwright": "^1.51.1",
|
||||
"react-error-overlay": "^6.1.0",
|
||||
"redux-logger": "^3.0.6",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"vite": "^6.2.3",
|
||||
"vite": "^6.2.5",
|
||||
"vite-plugin-babel": "^1.3.0",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"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",
|
||||
"vitest": "^3.1.1",
|
||||
"workbox-window": "^7.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
25
client/playwright.config.js
Normal file
25
client/playwright.config.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineConfig } from "@playwright/test";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config({
|
||||
path: "./.env.development.imex",
|
||||
prefix: "TEST_"
|
||||
});
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests/e2e",
|
||||
testMatch: "*.e2e.js",
|
||||
timeout: 60 * 1000,
|
||||
reporter: [["list"], ["html"]],
|
||||
use: {
|
||||
baseURL: "https://localhost:3000",
|
||||
browser: "chromium",
|
||||
ignoreHTTPSErrors: true
|
||||
},
|
||||
webServer: {
|
||||
command: "npm run start:imex",
|
||||
ignoreHTTPSErrors: true,
|
||||
url: "https://localhost:3000/health", // Health check endpoint will tell us when the server is ready
|
||||
reuseExistingServer: !process.env.CI // Reuse server locally, not in CI
|
||||
}
|
||||
});
|
||||
25
client/playwright.rome.config.js
Normal file
25
client/playwright.rome.config.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineConfig } from "@playwright/test";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config({
|
||||
path: "./.env.development.rome",
|
||||
prefix: "TEST_"
|
||||
});
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests/e2e",
|
||||
testMatch: "*.e2e.js",
|
||||
timeout: 60 * 1000,
|
||||
reporter: [["list"], ["html"]],
|
||||
use: {
|
||||
baseURL: "https://localhost:3000",
|
||||
browser: "chromium",
|
||||
ignoreHTTPSErrors: true
|
||||
},
|
||||
webServer: {
|
||||
command: "npm run start:rome",
|
||||
ignoreHTTPSErrors: true,
|
||||
url: "https://localhost:3000/health", // Health check endpoint will tell us when the server is ready
|
||||
reuseExistingServer: !process.env.CI // Reuse server locally, not in CI
|
||||
}
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import client from "../utils/GraphQLClient";
|
||||
import App from "./App";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import themeProvider from "./themeProvider";
|
||||
import { CookiesProvider } from "react-cookie";
|
||||
|
||||
// Base Split configuration
|
||||
const config = {
|
||||
@@ -38,26 +39,28 @@ function AppContainer() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ApolloProvider client={client}>
|
||||
<ConfigProvider
|
||||
input={{ autoComplete: "new-password" }}
|
||||
locale={enLocale}
|
||||
theme={themeProvider}
|
||||
form={{
|
||||
validateMessages: {
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
required: t("general.validation.required", { label: "${label}" })
|
||||
}
|
||||
}}
|
||||
>
|
||||
<GlobalLoadingBar />
|
||||
<SplitFactoryProvider config={config}>
|
||||
<SplitClientProvider>
|
||||
<App />
|
||||
</SplitClientProvider>
|
||||
</SplitFactoryProvider>
|
||||
</ConfigProvider>
|
||||
</ApolloProvider>
|
||||
<CookiesProvider>
|
||||
<ApolloProvider client={client}>
|
||||
<ConfigProvider
|
||||
input={{ autoComplete: "new-password" }}
|
||||
locale={enLocale}
|
||||
theme={themeProvider}
|
||||
form={{
|
||||
validateMessages: {
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
required: t("general.validation.required", { label: "${label}" })
|
||||
}
|
||||
}}
|
||||
>
|
||||
<GlobalLoadingBar />
|
||||
<SplitFactoryProvider config={config}>
|
||||
<SplitClientProvider>
|
||||
<App />
|
||||
</SplitClientProvider>
|
||||
</SplitFactoryProvider>
|
||||
</ConfigProvider>
|
||||
</ApolloProvider>
|
||||
</CookiesProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ import "./App.styles.scss";
|
||||
import Eula from "../components/eula/eula.component";
|
||||
import InstanceRenderMgr from "../utils/instanceRenderMgr";
|
||||
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
|
||||
import { SocketProvider } from "../contexts/SocketIO/useSocket.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 ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
|
||||
@@ -142,11 +142,10 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
||||
>
|
||||
<ProductFruitsWrapper
|
||||
currentUser={currentUser}
|
||||
workspaceCode={InstanceRenderMgr({
|
||||
imex: null,
|
||||
rome: "9BkbEseqNqxw8jUH"
|
||||
})}
|
||||
bodyshop={bodyshop}
|
||||
workspaceCode={bodyshop?.tours_enabled ? "9BkbEseqNqxw8jUH" : ""}
|
||||
/>
|
||||
|
||||
<NotificationProvider>
|
||||
<Routes>
|
||||
<Route
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import React from "react";
|
||||
import { ProductFruits } from "react-product-fruits";
|
||||
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 (
|
||||
workspaceCode &&
|
||||
currentUser?.authorized === true &&
|
||||
@@ -14,7 +22,8 @@ const ProductFruitsWrapper = React.memo(({ currentUser, workspaceCode }) => {
|
||||
language="en"
|
||||
user={{
|
||||
email: currentUser.email,
|
||||
username: currentUser.email
|
||||
username: currentUser.email,
|
||||
props: featureProps
|
||||
}}
|
||||
/>
|
||||
)
|
||||
@@ -28,5 +37,6 @@ ProductFruitsWrapper.propTypes = {
|
||||
authorized: PropTypes.bool,
|
||||
email: PropTypes.string
|
||||
}),
|
||||
workspaceCode: PropTypes.string
|
||||
workspaceCode: PropTypes.string,
|
||||
bodyshop: PropTypes.object
|
||||
};
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Alert component should render Alert component 1`] = `ShallowWrapper {}`;
|
||||
@@ -1,19 +0,0 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import Alert from "./alert.component";
|
||||
|
||||
describe("Alert component", () => {
|
||||
let wrapper;
|
||||
beforeEach(() => {
|
||||
const mockProps = {
|
||||
type: "error",
|
||||
message: "Test error message."
|
||||
};
|
||||
|
||||
wrapper = shallow(<Alert {...mockProps} />);
|
||||
});
|
||||
|
||||
it("should render Alert component", () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
31
client/src/components/alert/alert.component.test.jsx
Normal file
31
client/src/components/alert/alert.component.test.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import AlertComponent from "./alert.component";
|
||||
|
||||
describe("AlertComponent", () => {
|
||||
it("renders with default props", () => {
|
||||
render(<AlertComponent message="Default Alert" />);
|
||||
expect(screen.getByText("Default Alert")).toBeInTheDocument();
|
||||
expect(screen.getByRole("alert")).toHaveClass("ant-alert");
|
||||
});
|
||||
|
||||
it("applies type prop correctly", () => {
|
||||
render(<AlertComponent message="Success Alert" type="success" />);
|
||||
const alert = screen.getByRole("alert");
|
||||
expect(screen.getByText("Success Alert")).toBeInTheDocument();
|
||||
expect(alert).toHaveClass("ant-alert-success");
|
||||
});
|
||||
|
||||
it("displays description when provided", () => {
|
||||
render(<AlertComponent message="Error Alert" description="Something went wrong" type="error" />);
|
||||
expect(screen.getByText("Error Alert")).toBeInTheDocument();
|
||||
expect(screen.getByText("Something went wrong")).toBeInTheDocument();
|
||||
expect(screen.getByRole("alert")).toHaveClass("ant-alert-error");
|
||||
});
|
||||
|
||||
it("is closable and shows icon when props are set", () => {
|
||||
render(<AlertComponent message="Warning Alert" type="warning" showIcon closable />);
|
||||
expect(screen.getByText("Warning Alert")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /close/i })).toBeInTheDocument(); // Close button
|
||||
});
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AllocationsAssignmentComponent component should create an allocation on save 1`] = `ReactWrapper {}`;
|
||||
|
||||
exports[`AllocationsAssignmentComponent component should render AllocationsAssignmentComponent component 1`] = `ReactWrapper {}`;
|
||||
@@ -1,35 +0,0 @@
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import { MockBodyshop } from "../../utils/TestingHelpers";
|
||||
import { AllocationsAssignmentComponent } from "./allocations-assignment.component";
|
||||
|
||||
describe("AllocationsAssignmentComponent component", () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockProps = {
|
||||
bodyshop: MockBodyshop,
|
||||
handleAssignment: jest.fn(),
|
||||
assignment: {},
|
||||
setAssignment: jest.fn(),
|
||||
visibilityState: [false, jest.fn()],
|
||||
maxHours: 4
|
||||
};
|
||||
|
||||
wrapper = mount(<AllocationsAssignmentComponent {...mockProps} />);
|
||||
});
|
||||
|
||||
it("should render AllocationsAssignmentComponent component", () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render a list of employees", () => {
|
||||
const empList = wrapper.find("#employeeSelector");
|
||||
expect(empList.children()).to.have.lengthOf(2);
|
||||
});
|
||||
|
||||
it("should create an allocation on save", () => {
|
||||
wrapper.find("Button").simulate("click");
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -3,11 +3,11 @@ import { getToken } from "@firebase/messaging";
|
||||
import axios from "axios";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { messaging, requestForToken } from "../../firebase/firebase.utils";
|
||||
import ChatPopupComponent from "../chat-popup/chat-popup.component";
|
||||
import "./chat-affix.styles.scss";
|
||||
import { registerMessagingHandlers, unregisterMessagingHandlers } from "./registerMessagingSocketHandlers";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -3,10 +3,10 @@ import { Button } from "antd";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TOGGLE_CONVERSATION_ARCHIVE } from "../../graphql/conversations.queries";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||
import { connect } from "react-redux";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
|
||||
@@ -4,10 +4,10 @@ import { Link } from "react-router-dom";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||
import { connect } from "react-redux";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
|
||||
@@ -3,11 +3,11 @@ import axios from "axios";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { CONVERSATION_SUBSCRIPTION_BY_PK, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries";
|
||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import ChatConversationComponent from "./chat-conversation.component";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
selectedConversation: selectSelectedConversation,
|
||||
|
||||
@@ -4,11 +4,11 @@ import { Input, Spin, Tag, Tooltip } from "antd";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UPDATE_CONVERSATION_LABEL } from "../../graphql/conversations.queries";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||
import { connect } from "react-redux";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
|
||||
@@ -5,7 +5,7 @@ import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
||||
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({
|
||||
//currentUser: selectCurrentUser
|
||||
|
||||
@@ -7,7 +7,7 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.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";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
|
||||
@@ -12,9 +12,9 @@ import ChatConversationListComponent from "../chat-conversation-list/chat-conver
|
||||
import ChatConversationContainer from "../chat-conversation/chat-conversation.container";
|
||||
import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
|
||||
import "./chat-popup.styles.scss";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
selectedConversation: selectSelectedConversation,
|
||||
|
||||
@@ -8,10 +8,10 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||
import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries";
|
||||
import ChatTagRo from "./chat-tag-ro.component";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||
import { connect } from "react-redux";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
|
||||
@@ -20,6 +20,7 @@ function FeatureWrapper({
|
||||
children,
|
||||
upsellComponent,
|
||||
bypass,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
...restProps
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
@@ -78,7 +79,11 @@ export function HasFeatureAccess({ featureName, bodyshop, bypass, debug = false
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return bodyshop?.features?.allAccess || dayjs(bodyshop?.features[featureName]).isAfter(dayjs());
|
||||
return (
|
||||
bodyshop?.features?.allAccess ||
|
||||
bodyshop?.features?.[featureName] ||
|
||||
dayjs(bodyshop?.features[featureName]).isAfter(dayjs())
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(FeatureWrapper);
|
||||
|
||||
@@ -40,7 +40,6 @@ import { RiSurveyLine } from "react-icons/ri";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js";
|
||||
import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
@@ -51,6 +50,7 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import LockWrapper from "../lock-wrapper/lock-wrapper.component";
|
||||
import NotificationCenterContainer from "../notification-center/notification-center.container.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
// Redux mappings
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 parsePhoneNumber from "libphonenumber-js";
|
||||
import queryString from "query-string";
|
||||
@@ -8,24 +8,30 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
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 { 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 { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
||||
import dayjs from "../../utils/day";
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import ChatOpenButton from "../chat-open-button/chat-open-button.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 ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.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 ScheduleEventColor from "./schedule-event.color.component";
|
||||
import ScheduleEventNote from "./schedule-event.note.component";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -33,7 +39,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setScheduleContext: (context) => dispatch(setModalContext({ context: context, modal: "schedule" })),
|
||||
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({
|
||||
@@ -43,16 +50,41 @@ export function ScheduleEventComponent({
|
||||
event,
|
||||
refetch,
|
||||
handleCancel,
|
||||
setScheduleContext
|
||||
setScheduleContext,
|
||||
insertAuditTrail
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const history = useNavigate();
|
||||
const searchParams = queryString.parse(useLocation().search);
|
||||
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
||||
const [mutationUpdateJob] = useMutation(JOB_PRODUCTION_TOGGLE);
|
||||
const [title, setTitle] = useState(event.title);
|
||||
const { socket } = useSocket();
|
||||
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 = (
|
||||
<Space direction="vertical" wrap>
|
||||
@@ -89,6 +121,74 @@ export function ScheduleEventComponent({
|
||||
</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 = (
|
||||
<div style={{ maxWidth: "40vw" }}>
|
||||
{!event.isintake ? (
|
||||
@@ -294,7 +394,7 @@ export function ScheduleEventComponent({
|
||||
) : (
|
||||
<ScheduleManualEvent event={event} />
|
||||
)}
|
||||
{event.isintake ? (
|
||||
{event.isintake && HasFeatureAccess({ featureName: "checklist", bodyshop }) ? (
|
||||
<Link
|
||||
to={{
|
||||
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>
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,8 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
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 { QUERY_JOB_CARD_DETAILS, UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions.js";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
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 JobReconciliationPartsTable from "../job-reconciliation-parts-table/job-reconciliation-parts-table.component";
|
||||
import JobReconciliationTotals from "../job-reconciliation-totals/job-reconciliation-totals.component";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
|
||||
export default function JobReconciliationModalComponent({ job, bills }) {
|
||||
const jobLineState = useState([]);
|
||||
@@ -20,7 +20,7 @@ export default function JobReconciliationModalComponent({ job, bills }) {
|
||||
|
||||
const filterFunction = InstanceRenderManager({
|
||||
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.db_ref === "936004", //ADD SHIPPING LINE.
|
||||
rome: (j) =>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
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 { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
|
||||
import { GET_CURRENT_QUESTIONSET_ID, INSERT_CSI } from "../../graphql/csi.queries";
|
||||
@@ -133,6 +133,16 @@ export function JobsDetailHeaderActions({
|
||||
const { socket } = useSocket();
|
||||
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 {
|
||||
treatments: { ImEXPay }
|
||||
} = useSplitTreatments({
|
||||
@@ -171,7 +181,7 @@ export function JobsDetailHeaderActions({
|
||||
{ defaultOpenStatus: bodyshop.md_ro_statuses.default_imported },
|
||||
(newJobId) => {
|
||||
history(`/manage/jobs/${newJobId}`);
|
||||
notification["success"]({
|
||||
notification.success({
|
||||
message: t("jobs.successes.duplicated")
|
||||
});
|
||||
},
|
||||
@@ -181,7 +191,7 @@ export function JobsDetailHeaderActions({
|
||||
const handleDuplicateConfirm = () =>
|
||||
DuplicateJob(client, job.id, { defaultOpenStatus: bodyshop.md_ro_statuses.default_imported }, (newJobId) => {
|
||||
history(`/manage/jobs/${newJobId}`);
|
||||
notification["success"]({
|
||||
notification.success({
|
||||
message: t("jobs.successes.duplicated")
|
||||
});
|
||||
});
|
||||
@@ -217,13 +227,13 @@ export function JobsDetailHeaderActions({
|
||||
const result = await deleteJob({ variables: { id: job.id } });
|
||||
|
||||
if (!result.errors) {
|
||||
notification["success"]({
|
||||
notification.success({
|
||||
message: t("jobs.successes.delete")
|
||||
});
|
||||
//go back to jobs list.
|
||||
history(`/manage/`);
|
||||
} else {
|
||||
notification["error"]({
|
||||
notification.error({
|
||||
message: t("jobs.errors.deleted", {
|
||||
error: JSON.stringify(result.errors)
|
||||
})
|
||||
@@ -275,9 +285,9 @@ export function JobsDetailHeaderActions({
|
||||
});
|
||||
|
||||
if (!result.errors) {
|
||||
notification["success"]({ message: t("csi.successes.created") });
|
||||
notification.success({ message: t("csi.successes.created") });
|
||||
} else {
|
||||
notification["error"]({
|
||||
notification.error({
|
||||
message: t("csi.errors.creating", {
|
||||
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}`
|
||||
);
|
||||
} else {
|
||||
notification["error"]({
|
||||
notification.error({
|
||||
message: t("messaging.error.invalidphone")
|
||||
});
|
||||
}
|
||||
@@ -328,7 +338,7 @@ export function JobsDetailHeaderActions({
|
||||
);
|
||||
}
|
||||
} else {
|
||||
notification["error"]({
|
||||
notification.error({
|
||||
message: t("csi.errors.notconfigured")
|
||||
});
|
||||
}
|
||||
@@ -358,7 +368,7 @@ export function JobsDetailHeaderActions({
|
||||
});
|
||||
setMessage(`${window.location.protocol}//${window.location.host}/csi/${job.csiinvites[0].id}`);
|
||||
} else {
|
||||
notification["error"]({
|
||||
notification.error({
|
||||
message: t("messaging.error.invalidphone")
|
||||
});
|
||||
}
|
||||
@@ -398,7 +408,7 @@ export function JobsDetailHeaderActions({
|
||||
});
|
||||
|
||||
if (!result.errors) {
|
||||
notification["success"]({
|
||||
notification.success({
|
||||
message: t("jobs.successes.voided")
|
||||
});
|
||||
insertAuditTrail({
|
||||
@@ -409,7 +419,7 @@ export function JobsDetailHeaderActions({
|
||||
//go back to jobs list.
|
||||
history(`/manage/`);
|
||||
} else {
|
||||
notification["error"]({
|
||||
notification.error({
|
||||
message: t("jobs.errors.voiding", {
|
||||
error: JSON.stringify(result.errors)
|
||||
})
|
||||
@@ -442,7 +452,7 @@ export function JobsDetailHeaderActions({
|
||||
console.log("handle -> XML", QbXmlResponse);
|
||||
} catch (error) {
|
||||
console.log("Error getting QBXML from Server.", error);
|
||||
notification["error"]({
|
||||
notification.error({
|
||||
message: t("jobs.errors.exporting", {
|
||||
error: "Unable to retrieve QBXML. " + JSON.stringify(error.message)
|
||||
})
|
||||
@@ -460,7 +470,7 @@ export function JobsDetailHeaderActions({
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error connecting to quickbooks or partner.", error);
|
||||
notification["error"]({
|
||||
notification.error({
|
||||
message: t("jobs.errors.exporting-partner")
|
||||
});
|
||||
|
||||
@@ -556,7 +566,7 @@ export function JobsDetailHeaderActions({
|
||||
}
|
||||
});
|
||||
if (!jobUpdate.errors) {
|
||||
notification["success"]({
|
||||
notification.success({
|
||||
message: t("appointments.successes.canceled")
|
||||
});
|
||||
insertAuditTrail({
|
||||
@@ -931,11 +941,11 @@ export function JobsDetailHeaderActions({
|
||||
});
|
||||
|
||||
if (!result.errors) {
|
||||
notification["success"]({
|
||||
notification.success({
|
||||
message: t("jobs.successes.partsqueue")
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
notification.error({
|
||||
message: t("jobs.errors.saving", {
|
||||
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 = {
|
||||
items: menuItems,
|
||||
key: "popovermenu"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
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 { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
@@ -12,7 +13,6 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
||||
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";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -44,9 +44,16 @@ export function JobsDetailHeaderActionsToggleProduction({
|
||||
variables: { id: 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,
|
||||
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,
|
||||
scheduled_delivery: data.jobs_by_pk.scheduled_delivery,
|
||||
actual_delivery: data.jobs_by_pk.actual_delivery
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { Button, Space } from "antd";
|
||||
import axios from "axios";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import cleanAxios from "../../utils/CleanAxios";
|
||||
import formatBytes from "../../utils/formatbytes";
|
||||
//import yauzl from "yauzl";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
@@ -28,7 +26,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyDownloadButton);
|
||||
|
||||
export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, identifier }) {
|
||||
export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, identifier, jobId }) {
|
||||
const { t } = useTranslation();
|
||||
const [download, setDownload] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -46,6 +44,7 @@ export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, i
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function standardMediaDownload(bufferData) {
|
||||
const a = document.createElement("a");
|
||||
const url = window.URL.createObjectURL(new Blob([bufferData]));
|
||||
@@ -53,13 +52,14 @@ export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, i
|
||||
a.download = `${identifier || "documents"}.zip`;
|
||||
a.click();
|
||||
}
|
||||
|
||||
const handleDownload = async () => {
|
||||
logImEXEvent("jobs_documents_download");
|
||||
setLoading(true);
|
||||
const zipUrl = await axios({
|
||||
url: "/media/imgproxy/download",
|
||||
method: "POST",
|
||||
data: { documentids: imagesToDownload.map((_) => _.id) }
|
||||
data: { jobId, documentids: imagesToDownload.map((_) => _.id) }
|
||||
});
|
||||
|
||||
const theDownloadedZip = await cleanAxios({
|
||||
|
||||
@@ -75,7 +75,7 @@ function JobsDocumentsImgproxyComponent({
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
<JobsDocumentsGallerySelectAllComponent galleryImages={galleryImages} setGalleryImages={setGalleryImages} />
|
||||
<JobsDocumentsDownloadButton galleryImages={galleryImages} identifier={downloadIdentifier} />
|
||||
<JobsDocumentsDownloadButton galleryImages={galleryImages} identifier={downloadIdentifier} jobId={jobId} />
|
||||
<JobsDocumentsDeleteButton
|
||||
galleryImages={galleryImages}
|
||||
deletionCallback={billsCallback || fetchThumbnails || refetch}
|
||||
|
||||
@@ -3,10 +3,10 @@ import { useQuery } from "@apollo/client";
|
||||
import { connect } from "react-redux";
|
||||
import NotificationCenterComponent from "./notification-center.component";
|
||||
import { GET_NOTIFICATIONS } from "../../graphql/notifications.queries";
|
||||
import { INITIAL_NOTIFICATIONS, useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors.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
|
||||
const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
|
||||
|
||||
@@ -10,7 +10,7 @@ import { createStructuredSelector } from "reselect";
|
||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
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({
|
||||
bodyshop: selectBodyshop,
|
||||
|
||||
@@ -12,7 +12,7 @@ import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import ProductionBoardKanbanComponent from "./production-board-kanban.component";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
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 { QUERY_JOB_CARD_DETAILS, UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions.js";
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import ProductionListTable from "./production-list-table.component";
|
||||
import _ from "lodash";
|
||||
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" }) {
|
||||
const client = useApolloClient();
|
||||
|
||||
@@ -8,7 +8,7 @@ import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import { logImEXEvent, updateCurrentPassword } from "../../firebase/firebase.utils";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
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";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
|
||||
@@ -54,6 +54,9 @@ export function ProfileShopsContainer({ bodyshop, currentUser }) {
|
||||
|
||||
//Force window refresh.
|
||||
|
||||
//Ping the new partner to refresh.
|
||||
axios.post("http://localhost:1337/refresh");
|
||||
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Button, Col, Form, Input, Row, Select, Space, Switch, Typography } from "antd";
|
||||
import axios from "axios";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -8,16 +8,16 @@ import { calculateScheduleLoad } from "../../redux/application/application.actio
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import dayjs from "../../utils/day";
|
||||
import BlurWrapper from "../feature-wrapper/blur-wrapper.component";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
|
||||
import EmailInput from "../form-items-formatted/email-form-item.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 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 "./schedule-job-modal.scss";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -60,10 +60,12 @@ export function ScheduleJobModalComponent({
|
||||
const totalHours =
|
||||
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)
|
||||
form.setFieldsValue({
|
||||
scheduled_completion: dayjs(values.start).businessDaysAdd(totalHours / bodyshop.target_touchtime, "day")
|
||||
});
|
||||
if (values.start && !values.scheduled_completion) {
|
||||
const addDays = bodyshop.ss_configuration.nobusinessdays
|
||||
? 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();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
id="insurancecos-add-button"
|
||||
>
|
||||
{t("general.actions.add")}
|
||||
</Button>
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { Button, Divider, Form, Input, InputNumber, Select, Space, Switch, TimePicker } from "antd";
|
||||
import React from "react";
|
||||
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 ColorpickerFormItemComponent from "../form-items-formatted/colorpicker-form-item.component";
|
||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.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({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
@@ -78,6 +77,13 @@ export function ShopInfoSchedulingComponent({ form, bodyshop }) {
|
||||
>
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["ss_configuration", "nobusinessdays"]}
|
||||
label={t("bodyshop.fields.ss_configuration.nobusinessdays")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_lost_sale_reasons"]}
|
||||
label={t("bodyshop.fields.md_lost_sale_reasons")}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useLazyQuery } from "@apollo/client";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Form, Input, InputNumber, Select, Switch } from "antd";
|
||||
import React from "react";
|
||||
import { Card, Form, Input, InputNumber, Select, Space, Switch } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
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 { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import TimeTicketList from "../time-ticket-list/time-ticket-list.component";
|
||||
import JobEmployeeAssignmentsContainer from "./../job-employee-assignments/job-employee-assignments.container";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -319,10 +319,15 @@ export function TimeTicketModalComponent({
|
||||
}
|
||||
|
||||
export function LaborAllocationContainer({ jobid, loading, lineTicketData, hideTimeTickets = false }) {
|
||||
const { t } = useTranslation();
|
||||
if (loading) return <LoadingSkeleton />;
|
||||
if (!lineTicketData) return null;
|
||||
if (!jobid) return null;
|
||||
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
|
||||
jobId={jobid}
|
||||
joblines={lineTicketData.joblines}
|
||||
@@ -332,6 +337,6 @@ export function LaborAllocationContainer({ jobid, loading, lineTicketData, hideT
|
||||
{!hideTimeTickets && (
|
||||
<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 { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Button, Form, Modal, Space } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { QUERY_ACTIVE_EMPLOYEES } from "../../graphql/employees.queries";
|
||||
import { INSERT_NEW_TIME_TICKET, UPDATE_TIME_TICKET } from "../../graphql/timetickets.queries";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
@@ -14,7 +15,6 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import dayjs from "../../utils/day";
|
||||
import TimeTicketsCommitToggleComponent from "../time-tickets-commit-toggle/time-tickets-commit-toggle.component";
|
||||
import TimeTicketModalComponent from "./time-ticket-modal.component";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
timeTicketModal: selectTimeTicket,
|
||||
@@ -81,7 +81,7 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible,
|
||||
}
|
||||
};
|
||||
|
||||
const handleMutationSuccess = (response) => {
|
||||
const handleMutationSuccess = () => {
|
||||
notification["success"]({
|
||||
message: t("timetickets.successes.created")
|
||||
});
|
||||
@@ -123,7 +123,7 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible,
|
||||
if (timeTicketModal.open) form.resetFields();
|
||||
}, [timeTicketModal.open, form]);
|
||||
|
||||
const handleFieldsChange = (changedFields, allFields) => {
|
||||
const handleFieldsChange = (changedFields) => {
|
||||
if (!!changedFields.employeeid && !!EmployeeAutoCompleteData) {
|
||||
const emps = EmployeeAutoCompleteData.employees.filter((e) => e.id === changedFields.employeeid);
|
||||
form.setFieldsValue({
|
||||
@@ -182,6 +182,7 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible,
|
||||
</Space>
|
||||
}
|
||||
destroyOnClose
|
||||
id="time-ticket-modal"
|
||||
>
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
|
||||
@@ -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 { auth } from "../../firebase/firebase.utils";
|
||||
import { store } from "../../redux/store";
|
||||
@@ -15,10 +16,7 @@ import {
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
|
||||
const SocketContext = createContext(null);
|
||||
|
||||
const INITIAL_NOTIFICATIONS = 10;
|
||||
import { SocketContext, INITIAL_NOTIFICATIONS } from "./useSocket.js";
|
||||
|
||||
/**
|
||||
* Socket Provider - Scenario Notifications / Web Socket related items
|
||||
@@ -216,7 +214,6 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
||||
};
|
||||
|
||||
const handleNotification = (data) => {
|
||||
// Scenario Notifications have been disabled, bail.
|
||||
if (Realtime_Notifications_UI?.treatment !== "on") {
|
||||
return;
|
||||
}
|
||||
@@ -336,7 +333,6 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
||||
};
|
||||
|
||||
const handleSyncNotificationRead = ({ notificationId, timestamp }) => {
|
||||
// Scenario Notifications have been disabled, bail.
|
||||
if (Realtime_Notifications_UI?.treatment !== "on") {
|
||||
return;
|
||||
}
|
||||
@@ -378,7 +374,6 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
||||
};
|
||||
|
||||
const handleSyncAllNotificationsRead = ({ timestamp }) => {
|
||||
// Scenario Notifications have been disabled, bail.
|
||||
if (Realtime_Notifications_UI?.treatment !== "on") {
|
||||
return;
|
||||
}
|
||||
@@ -490,11 +485,4 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const useSocket = () => {
|
||||
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 };
|
||||
export default SocketProvider;
|
||||
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
|
||||
md_ro_statuses
|
||||
md_order_statuses
|
||||
tours_enabled
|
||||
md_functionality_toggles
|
||||
shopname
|
||||
state
|
||||
@@ -186,6 +187,7 @@ export const UPDATE_SHOP = gql`
|
||||
phone
|
||||
federal_tax_id
|
||||
id
|
||||
tours_enabled
|
||||
insurance_vendor_id
|
||||
logo_img_path
|
||||
md_ro_statuses
|
||||
|
||||
@@ -35,6 +35,30 @@ export const GET_LINE_TICKET_BY_PK = gql`
|
||||
lbr_adjustments
|
||||
converted
|
||||
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 } }) {
|
||||
id
|
||||
|
||||
@@ -2570,6 +2570,20 @@ export const GET_JOB_BY_PK_QUICK_INTAKE = gql`
|
||||
actual_completion
|
||||
scheduled_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
|
||||
clm_no
|
||||
cieca_stl
|
||||
cieca_ttl
|
||||
cieca_pfl
|
||||
asgn_no
|
||||
asgn_date
|
||||
state_tax_rate
|
||||
@@ -164,6 +167,7 @@ export const QUERY_PARTS_ORDER_OEC = gql`
|
||||
loss_desc
|
||||
loss_of_use
|
||||
loss_type
|
||||
materials
|
||||
ownr_addr1
|
||||
ownr_addr2
|
||||
ownr_city
|
||||
|
||||
@@ -56,7 +56,7 @@ import { DateTimeFormat } from "../../utils/DateFormatter";
|
||||
import dayjs from "../../utils/day";
|
||||
import UndefinedToNull from "../../utils/undefinedtonull";
|
||||
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";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
|
||||
@@ -20,7 +20,6 @@ import PartnerPingComponent from "../../components/partner-ping/partner-ping.com
|
||||
import PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container";
|
||||
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 UpdateAlert from "../../components/update-alert/update-alert.component";
|
||||
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 { addAlerts } from "../../redux/application/application.actions.js";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
const JobsPage = lazy(() => import("../jobs/jobs.page"));
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
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 {
|
||||
checkActionCode,
|
||||
@@ -12,6 +9,9 @@ import {
|
||||
} from "@firebase/auth";
|
||||
import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "@firebase/firestore";
|
||||
import { getToken } from "@firebase/messaging";
|
||||
import * as Sentry from "@sentry/browser";
|
||||
import { notification } from "antd";
|
||||
import axios from "axios";
|
||||
import i18next from "i18next";
|
||||
import LogRocket from "logrocket";
|
||||
import { all, call, delay, put, select, takeLatest } from "redux-saga/effects";
|
||||
@@ -340,6 +340,7 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
||||
args: [],
|
||||
imex: () => {
|
||||
window.$crisp.push(["set", "user:company", [payload.shopname]]);
|
||||
window.$crisp.push(["set", "session:segments", [[`region:${payload.region_config}`]]]);
|
||||
if (authRecord[0] && authRecord[0].user.validemail) {
|
||||
window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]);
|
||||
}
|
||||
@@ -350,7 +351,14 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
||||
});
|
||||
payload.features?.allAccess === true
|
||||
? 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) {
|
||||
console.warn("Couldnt find $crisp.", error.message);
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import { configure } from "enzyme";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
|
||||
configure({ adapter: new Adapter() });
|
||||
@@ -601,7 +601,8 @@
|
||||
"templates": "Templates"
|
||||
},
|
||||
"ss_configuration": {
|
||||
"dailyhrslimit": "Daily Incoming Hours Limit"
|
||||
"dailyhrslimit": "Daily Incoming Hours Limit",
|
||||
"nobusinessdays": "Include Weekends"
|
||||
},
|
||||
"ssbuckets": {
|
||||
"color": "Job Color",
|
||||
@@ -1220,7 +1221,8 @@
|
||||
"errors": {
|
||||
"fcm": "You must allow notification permissions to have real time messaging. Click to try again.",
|
||||
"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": {
|
||||
"contract": "CC Contract",
|
||||
@@ -1322,7 +1324,8 @@
|
||||
"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.",
|
||||
"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?",
|
||||
"submit-for-testing": "Submitted Job for testing successfully."
|
||||
},
|
||||
"validation": {
|
||||
"dateRangeExceeded": "The date range has been exceeded.",
|
||||
@@ -2314,7 +2317,8 @@
|
||||
"duplicate": "Duplicate this Job",
|
||||
"duplicatenolines": "Duplicate this Job without Repair Data",
|
||||
"newcccontract": "Create Courtesy Car Contract",
|
||||
"void": "Void Job"
|
||||
"void": "Void Job",
|
||||
"submit-for-testing": "Submit for Testing"
|
||||
},
|
||||
"jobsdetail": {
|
||||
"claimdetail": "Claim Details",
|
||||
@@ -3679,7 +3683,8 @@
|
||||
"signinerror": {
|
||||
"auth/user-disabled": "User account disabled. ",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -601,7 +601,8 @@
|
||||
"templates": ""
|
||||
},
|
||||
"ss_configuration": {
|
||||
"dailyhrslimit": ""
|
||||
"dailyhrslimit": "",
|
||||
"nobusinessdays": ""
|
||||
},
|
||||
"ssbuckets": {
|
||||
"color": "",
|
||||
@@ -1220,7 +1221,8 @@
|
||||
"errors": {
|
||||
"fcm": "",
|
||||
"notfound": "",
|
||||
"sizelimit": ""
|
||||
"sizelimit": "",
|
||||
"submit-for-testing": ""
|
||||
},
|
||||
"itemtypes": {
|
||||
"contract": "",
|
||||
@@ -1322,7 +1324,8 @@
|
||||
"partnernotrunning": "",
|
||||
"rbacunauth": "",
|
||||
"unsavedchanges": "Usted tiene cambios no guardados.",
|
||||
"unsavedchangespopup": ""
|
||||
"unsavedchangespopup": "",
|
||||
"submit-for-testing": ""
|
||||
},
|
||||
"validation": {
|
||||
"dateRangeExceeded": "",
|
||||
@@ -2314,7 +2317,8 @@
|
||||
"duplicate": "",
|
||||
"duplicatenolines": "",
|
||||
"newcccontract": "",
|
||||
"void": ""
|
||||
"void": "",
|
||||
"submit-for-testing": ""
|
||||
},
|
||||
"jobsdetail": {
|
||||
"claimdetail": "Detalles de la reclamación",
|
||||
@@ -3679,7 +3683,8 @@
|
||||
"signinerror": {
|
||||
"auth/user-disabled": "",
|
||||
"auth/user-not-found": "",
|
||||
"auth/wrong-password": ""
|
||||
"auth/wrong-password": "",
|
||||
"auth/invalid-email": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -601,7 +601,8 @@
|
||||
"templates": ""
|
||||
},
|
||||
"ss_configuration": {
|
||||
"dailyhrslimit": ""
|
||||
"dailyhrslimit": "",
|
||||
"nobusinessdays": ""
|
||||
},
|
||||
"ssbuckets": {
|
||||
"color": "",
|
||||
@@ -1220,7 +1221,8 @@
|
||||
"errors": {
|
||||
"fcm": "",
|
||||
"notfound": "",
|
||||
"sizelimit": ""
|
||||
"sizelimit": "",
|
||||
"submit-for-testing": ""
|
||||
},
|
||||
"itemtypes": {
|
||||
"contract": "",
|
||||
@@ -1322,7 +1324,9 @@
|
||||
"partnernotrunning": "",
|
||||
"rbacunauth": "",
|
||||
"unsavedchanges": "Vous avez des changements non enregistrés.",
|
||||
"unsavedchangespopup": ""
|
||||
"unsavedchangespopup": "",
|
||||
"submit-for-testing": ""
|
||||
|
||||
},
|
||||
"validation": {
|
||||
"dateRangeExceeded": "",
|
||||
@@ -2314,7 +2318,8 @@
|
||||
"duplicate": "",
|
||||
"duplicatenolines": "",
|
||||
"newcccontract": "",
|
||||
"void": ""
|
||||
"void": "",
|
||||
"submit-for-testing": ""
|
||||
},
|
||||
"jobsdetail": {
|
||||
"claimdetail": "Détails de la réclamation",
|
||||
@@ -3679,7 +3684,8 @@
|
||||
"signinerror": {
|
||||
"auth/user-disabled": "",
|
||||
"auth/user-not-found": "",
|
||||
"auth/wrong-password": ""
|
||||
"auth/wrong-password": "",
|
||||
"auth/invalid-email": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -15,8 +15,8 @@ const AuditTrailMapping = {
|
||||
jobchecklist: (type, inproduction, status) =>
|
||||
i18n.t("audit_trail.messages.jobchecklist", { type, inproduction, status }),
|
||||
jobconverted: (ro_number) => i18n.t("audit_trail.messages.jobconverted", { ro_number }),
|
||||
jobintake: (status, email, scheduled_completion) =>
|
||||
i18n.t("audit_trail.messages.jobintake", { status, email, scheduled_completion }),
|
||||
jobintake: (status, scheduled_completion) =>
|
||||
i18n.t("audit_trail.messages.jobintake", { status, scheduled_completion }),
|
||||
jobdelivery: (status, email, actual_completion) =>
|
||||
i18n.t("audit_trail.messages.jobdelivery", { status, email, actual_completion }),
|
||||
jobexported: () => i18n.t("audit_trail.messages.jobexported"),
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
export const MockBodyshop = {
|
||||
address1: "123 Fake St",
|
||||
address2: "Unit #100",
|
||||
city: "Vancouver",
|
||||
country: "Canada",
|
||||
created_at: "2019-12-10T20:03:06.420853+00:00",
|
||||
email: "snaptsoft@gmail.com",
|
||||
federal_tax_id: "GST10150492",
|
||||
id: "52b7357c-0edd-4c95-85c3-dfdbcdfad9ac",
|
||||
insurance_vendor_id: "F123456",
|
||||
logo_img_path: "https://www.snapt.ca/assets/logo-placeholder.png",
|
||||
md_ro_statuses: {
|
||||
statuses: [
|
||||
"Open",
|
||||
"Scheduled",
|
||||
"Arrived",
|
||||
"Repair Plan",
|
||||
"Parts",
|
||||
"Body",
|
||||
"Prep",
|
||||
"Paint",
|
||||
"Reassembly",
|
||||
"Sublet",
|
||||
"Detail",
|
||||
"Completed",
|
||||
"Delivered",
|
||||
"Invoiced",
|
||||
"Exported"
|
||||
],
|
||||
open_statuses: ["Open", "Scheduled", "Arrived", "Repair Plan", "Parts", "Body", "Prep", "Paint"],
|
||||
default_arrived: "Arrived",
|
||||
default_exported: "Exported",
|
||||
default_imported: "Open",
|
||||
default_invoiced: "Invoiced",
|
||||
default_completed: "Completed",
|
||||
default_delivered: "Delivered",
|
||||
default_scheduled: "Scheduled"
|
||||
},
|
||||
md_order_statuses: {
|
||||
statuses: ["Ordered", "Received", "Canceled", "Backordered"],
|
||||
default_bo: "Backordered",
|
||||
default_ordered: "Ordered",
|
||||
default_canceled: "Canceled",
|
||||
default_received: "Received"
|
||||
},
|
||||
shopname: "Testing Collision",
|
||||
state: "BC",
|
||||
state_tax_id: "PST1000-2991",
|
||||
updated_at: "2020-03-23T22:06:03.509544+00:00",
|
||||
zip_post: "V6B 1M9",
|
||||
region_config: "CA_BC",
|
||||
md_responsibility_centers: {
|
||||
costs: [
|
||||
"Aftermarket",
|
||||
"ATS",
|
||||
"Body",
|
||||
"Detail",
|
||||
"Daignostic",
|
||||
"Electrical",
|
||||
"Chrome",
|
||||
"Frame",
|
||||
"Mechanical",
|
||||
"Refinish",
|
||||
"Structural",
|
||||
"Existing",
|
||||
"Glass",
|
||||
"LKQ",
|
||||
"OEM",
|
||||
"OEM Partial",
|
||||
"Re-cored",
|
||||
"Remanufactured",
|
||||
"Other",
|
||||
"Sublet",
|
||||
"Towing"
|
||||
],
|
||||
profits: [
|
||||
"Aftermarket",
|
||||
"ATS",
|
||||
"Body",
|
||||
"Detail",
|
||||
"Daignostic",
|
||||
"Electrical",
|
||||
"Chrome",
|
||||
"Frame",
|
||||
"Mechanical",
|
||||
"Refinish",
|
||||
"Structural",
|
||||
"Existing",
|
||||
"Glass",
|
||||
"LKQ",
|
||||
"OEM",
|
||||
"OEM Partial",
|
||||
"Re-cored",
|
||||
"Remanufactured",
|
||||
"Other",
|
||||
"Sublet",
|
||||
"Towing"
|
||||
],
|
||||
defaults: {
|
||||
ATS: "ATS",
|
||||
LAB: "Body",
|
||||
LAD: "Diagnostic",
|
||||
LAE: "Electrical",
|
||||
LAF: "Frame",
|
||||
LAG: "Glass",
|
||||
LAM: "Mechanical",
|
||||
LAR: "Refinish",
|
||||
LAS: "Structural",
|
||||
LAU: "Detail",
|
||||
PAA: "Aftermarket",
|
||||
PAC: "Chrome",
|
||||
PAL: "LKQ",
|
||||
PAM: "Remanufactured",
|
||||
PAN: "OEM",
|
||||
PAO: "Other",
|
||||
PAP: "OEM Partial",
|
||||
PAR: "16",
|
||||
TOW: "Towing"
|
||||
}
|
||||
},
|
||||
employees: [
|
||||
{
|
||||
id: "075b744c-8919-49ca-abb2-ccd51040326d",
|
||||
first_name: "Patrick",
|
||||
last_name: "BODY123",
|
||||
employee_number: "101",
|
||||
cost_center: "Body",
|
||||
__typename: "employees"
|
||||
},
|
||||
{
|
||||
id: "8cc787d3-1cfe-49d3-8a15-8469cd5c2e41",
|
||||
first_name: "Patrick",
|
||||
last_name: "Painter",
|
||||
employee_number: "10211",
|
||||
cost_center: "REFINISH",
|
||||
__typename: "employees"
|
||||
}
|
||||
]
|
||||
};
|
||||
14
client/tests/Button.test.jsx
Normal file
14
client/tests/Button.test.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
// Button.test.jsx
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { Button } from "antd";
|
||||
import "antd/dist/reset.css"; // Optional: include if needed for styling reset
|
||||
|
||||
describe("AntD Button", () => {
|
||||
it("renders with correct text", () => {
|
||||
render(<Button>Click me</Button>);
|
||||
const button = screen.getByRole("button", { name: /click me/i });
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
6
client/tests/e2e/homepage.e2e.js
Normal file
6
client/tests/e2e/homepage.e2e.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("homepage loads correctly", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page.locator("h1")).toContainText("ImEX Online");
|
||||
});
|
||||
28
client/tests/e2e/signin.e2e.js
Normal file
28
client/tests/e2e/signin.e2e.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { login } from "./utils/login";
|
||||
|
||||
test.describe("SignInComponent", () => {
|
||||
test("successfully logs in with valid credentials", async ({ page }) => {
|
||||
const email = process.env.TEST_USERNAME;
|
||||
const password = process.env.TEST_PASSWORD;
|
||||
|
||||
await login(page, { email, password });
|
||||
|
||||
// Additional assertions after login (optional)
|
||||
await expect(page).toHaveURL(/\/manage\//);
|
||||
});
|
||||
|
||||
test("displays error on invalid credentials", async ({ page }) => {
|
||||
await page.goto("/"); // Adjust if login route differs
|
||||
|
||||
// Fill form with invalid credentials
|
||||
await page.fill('input[placeholder="Username"]', "wronguser@example.com");
|
||||
await page.fill('input[placeholder="Password"]', "wrongpassword");
|
||||
await page.click("button.login-btn");
|
||||
|
||||
// Check for error alert
|
||||
const alert = page.locator(".ant-alert-error");
|
||||
await expect(alert).toBeVisible();
|
||||
await expect(alert).toContainText("A user with this email does not exist.");
|
||||
});
|
||||
});
|
||||
21
client/tests/e2e/utils/login.js
Normal file
21
client/tests/e2e/utils/login.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
export async function login(page, { email, password }) {
|
||||
// Navigate to the login page
|
||||
await page.goto("/"); // Adjust if your login route differs (e.g., '/login')
|
||||
|
||||
// Fill email field
|
||||
await page.fill('input[placeholder="Username"]', email); // Matches Ant Design Input placeholder
|
||||
|
||||
// Fill password field
|
||||
await page.fill('input[placeholder="Password"]', password);
|
||||
|
||||
// Click login button
|
||||
await page.click("button.login-btn");
|
||||
|
||||
// Wait for navigation or success indicator (e.g., redirect to /manage/)
|
||||
await page.waitForURL(/\/manage\//, { timeout: 10000 }); // Adjust based on redirect
|
||||
|
||||
// Verify successful login (e.g., check for a dashboard element)
|
||||
await expect(page.locator("text=Manage")).toBeVisible(); // Adjust to your app’s post-login UI
|
||||
}
|
||||
5
client/tests/setup.js
Normal file
5
client/tests/setup.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import { afterEach } from "vitest";
|
||||
import { cleanup } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom";
|
||||
|
||||
afterEach(() => cleanup());
|
||||
27
client/tests/setupI18n.js
Normal file
27
client/tests/setupI18n.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import en_Translation from "../src/translations/en_us/common.json";
|
||||
import es_Translation from "../src/translations/es/common.json";
|
||||
import fr_Translation from "../src/translations/fr/common.json";
|
||||
|
||||
const resources = {
|
||||
"en-US": en_Translation,
|
||||
"fr-CA": fr_Translation,
|
||||
"es-MX": es_Translation
|
||||
};
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources,
|
||||
lng: "en-US", // Default to en-US for tests (no LanguageDetector)
|
||||
fallbackLng: "en-US",
|
||||
debug: false, // Disable debug in tests
|
||||
react: {
|
||||
useSuspense: false // Disable Suspense for Vitest
|
||||
},
|
||||
interpolation: {
|
||||
escapeValue: false, // React handles XSS
|
||||
skipOnVariables: false
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -191,11 +191,13 @@ export default defineConfig({
|
||||
"@sentry/react": ["@sentry/react"],
|
||||
"@splitsoftware/splitio-react": ["@splitsoftware/splitio-react"],
|
||||
logrocket: ["logrocket"],
|
||||
"@firebase/analytics": ["@firebase/analytics"],
|
||||
"@firebase/app": ["@firebase/app"],
|
||||
"@firebase/firestore": ["@firebase/firestore"],
|
||||
"@firebase/auth": ["@firebase/auth"],
|
||||
"@firebase/messaging": ["@firebase/messaging"],
|
||||
firebase: [
|
||||
"@firebase/analytics",
|
||||
"@firebase/app",
|
||||
"@firebase/firestore",
|
||||
"@firebase/auth",
|
||||
"@firebase/messaging"
|
||||
],
|
||||
markerjs2: ["markerjs2"],
|
||||
"@apollo/client": ["@apollo/client"],
|
||||
"libphonenumber-js": ["libphonenumber-js"]
|
||||
@@ -218,7 +220,13 @@ export default defineConfig({
|
||||
"react-router-dom",
|
||||
"dayjs",
|
||||
"redux",
|
||||
"react-redux"
|
||||
"react-redux",
|
||||
"@firebase/app",
|
||||
"@firebase/analytics",
|
||||
"@firebase/firestore",
|
||||
"@firebase/auth",
|
||||
"@firebase/messaging",
|
||||
"@firebase/util"
|
||||
],
|
||||
esbuildOptions: {
|
||||
// Update for Vite 6: Use proper file extensions
|
||||
|
||||
19
client/vitest.config.js
Normal file
19
client/vitest.config.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
globals: true,
|
||||
setupFiles: "./tests/setup.js",
|
||||
testTimeout: 10000 // 10 seconds (in milliseconds)
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
silenceDeprecations: ["import"] // Suppress @import warnings
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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 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-job-totals --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
"
|
||||
# Node App: The Main IMEX API
|
||||
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:
|
||||
- name: x-imex-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
|
||||
webhook: '{{HASURA_API_URL}}/data/usagereport'
|
||||
schedule: 0 12 * * 5
|
||||
|
||||
@@ -965,6 +965,7 @@
|
||||
- insurance_vendor_id
|
||||
- intakechecklist
|
||||
- intellipay_config
|
||||
- intellipay_merchant_id
|
||||
- jc_hourly_rates
|
||||
- jobsizelimit
|
||||
- last_name_first
|
||||
@@ -1004,6 +1005,7 @@
|
||||
- pbs_configuration
|
||||
- pbs_serialnumber
|
||||
- phone
|
||||
- podiumid
|
||||
- prodtargethrs
|
||||
- production_config
|
||||
- region_config
|
||||
@@ -1023,6 +1025,7 @@
|
||||
- template_header
|
||||
- textid
|
||||
- timezone
|
||||
- tours_enabled
|
||||
- tt_allow_post_to_invoiced
|
||||
- tt_enforce_hours_for_tech_console
|
||||
- updated_at
|
||||
|
||||
@@ -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;
|
||||
2863
package-lock.json
generated
2863
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
39
package.json
39
package.json
@@ -10,17 +10,20 @@
|
||||
"setup": "rm -rf node_modules && npm i && cd client && rm -rf node_modules && npm i",
|
||||
"setup:win": "rimraf node_modules && npm i && cd client && rimraf node_modules && npm i",
|
||||
"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:watch": "vitest",
|
||||
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-cloudwatch-logs": "^3.772.0",
|
||||
"@aws-sdk/client-elasticache": "^3.772.0",
|
||||
"@aws-sdk/client-s3": "^3.772.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.772.0",
|
||||
"@aws-sdk/client-ses": "^3.772.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.772.0",
|
||||
"@aws-sdk/lib-storage": "^3.743.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.731.1",
|
||||
"@aws-sdk/client-cloudwatch-logs": "^3.782.0",
|
||||
"@aws-sdk/client-elasticache": "^3.782.0",
|
||||
"@aws-sdk/client-s3": "^3.782.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.782.0",
|
||||
"@aws-sdk/client-ses": "^3.782.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.782.0",
|
||||
"@aws-sdk/lib-storage": "^3.782.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.782.0",
|
||||
"@opensearch-project/opensearch": "^2.13.0",
|
||||
"@socket.io/admin-ui": "^0.5.1",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
@@ -30,8 +33,7 @@
|
||||
"bee-queue": "^1.7.1",
|
||||
"better-queue": "^3.8.12",
|
||||
"bluebird": "^3.7.2",
|
||||
"body-parser": "^1.20.3",
|
||||
"bullmq": "^5.44.4",
|
||||
"bullmq": "^5.48.0",
|
||||
"chart.js": "^4.4.8",
|
||||
"cloudinary": "^2.6.0",
|
||||
"compression": "^1.8.0",
|
||||
@@ -39,7 +41,7 @@
|
||||
"cors": "2.8.5",
|
||||
"crisp-status-reporter": "^1.2.2",
|
||||
"csrf": "^3.1.0",
|
||||
"dd-trace": "^5.43.0",
|
||||
"dd-trace": "^5.45.0",
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.1",
|
||||
@@ -67,7 +69,7 @@
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-adapter": "^2.5.5",
|
||||
"ssh2-sftp-client": "^11.0.0",
|
||||
"twilio": "^5.5.1",
|
||||
"twilio": "^5.5.2",
|
||||
"uuid": "^11.1.0",
|
||||
"winston": "^3.17.0",
|
||||
"winston-cloudwatch": "^6.3.0",
|
||||
@@ -75,13 +77,16 @@
|
||||
"xmlbuilder2": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.23.0",
|
||||
"@eslint/js": "^9.24.0",
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||
"eslint": "^9.23.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^15.15.0",
|
||||
"mock-require": "^3.0.3",
|
||||
"p-limit": "^3.1.0",
|
||||
"prettier": "^3.5.3",
|
||||
"source-map-explorer": "^2.5.2"
|
||||
"source-map-explorer": "^2.5.2",
|
||||
"supertest": "^7.1.0",
|
||||
"vitest": "^3.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ const cors = require("cors");
|
||||
const http = require("http");
|
||||
const Redis = require("ioredis");
|
||||
const express = require("express");
|
||||
const bodyParser = require("body-parser");
|
||||
const compression = require("compression");
|
||||
const cookieParser = require("cookie-parser");
|
||||
const { Server } = require("socket.io");
|
||||
@@ -84,8 +83,8 @@ const SOCKETIO_CORS_ORIGIN_DEV = ["http://localhost:3333", "https://localhost:33
|
||||
const applyMiddleware = ({ app }) => {
|
||||
app.use(compression());
|
||||
app.use(cookieParser());
|
||||
app.use(bodyParser.json({ limit: "50mb" }));
|
||||
app.use(bodyParser.urlencoded({ limit: "50mb", extended: true }));
|
||||
app.use(express.json({ limit: "50mb" }));
|
||||
app.use(express.urlencoded({ limit: "50mb", extended: true }));
|
||||
app.use(cors({ credentials: true, exposedHeaders: ["set-cookie"] }));
|
||||
|
||||
// Helper middleware
|
||||
@@ -118,6 +117,7 @@ const applyRoutes = ({ app }) => {
|
||||
app.use("/cdk", require("./server/routes/cdkRoutes"));
|
||||
app.use("/csi", require("./server/routes/csiRoutes"));
|
||||
app.use("/payroll", require("./server/routes/payrollRoutes"));
|
||||
app.use("/integrations", require("./server/routes/intergrationRoutes"));
|
||||
|
||||
// Default route for forbidden access
|
||||
app.get("/", (req, res) => {
|
||||
|
||||
@@ -217,7 +217,7 @@ exports.PbsExportAp = async function (socket, { billids, txEnvelope }) {
|
||||
|
||||
socket.emit("ap-export-success", billid);
|
||||
} else {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Export was not succesful.`);
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Export was not successful.`);
|
||||
socket.emit("ap-export-failure", {
|
||||
billid,
|
||||
error: AccountPostingChange.Message
|
||||
|
||||
@@ -105,14 +105,14 @@ exports.PbsSelectedCustomer = async function PbsSelectedCustomer(socket, selecte
|
||||
|
||||
socket.emit("export-success", socket.JobData.id);
|
||||
} else {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Export was not succesful.`);
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Export was not successful.`);
|
||||
}
|
||||
} catch (error) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in CdkSelectedCustomer. ${error}`);
|
||||
await InsertFailedExportLog(socket, error);
|
||||
}
|
||||
};
|
||||
|
||||
// Was Successful
|
||||
async function CheckForErrors(socket, response) {
|
||||
if (response.WasSuccessful === undefined || response.WasSuccessful === true) {
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `Successful response from DMS. ${response.Message || ""}`);
|
||||
|
||||
@@ -39,12 +39,14 @@ exports.createShop = async (req, res) => {
|
||||
|
||||
try {
|
||||
const result = await client.request(
|
||||
`mutation INSERT_BODYSHOPS($bs: bodyshops_insert_input!){
|
||||
insert_bodyshops_one(object:$bs){
|
||||
id
|
||||
|
||||
}
|
||||
}`,
|
||||
`mutation INSERT_BODYSHOPS($bs: bodyshops_insert_input!) {
|
||||
insert_bodyshops_one(object: $bs) {
|
||||
id
|
||||
vendors {
|
||||
id
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{
|
||||
bs: {
|
||||
...bodyshop,
|
||||
@@ -54,12 +56,39 @@ exports.createShop = async (req, res) => {
|
||||
{ countertype: "ihbnum", count: 1 },
|
||||
{ countertype: "paymentnum", count: 1 }
|
||||
]
|
||||
},
|
||||
vendors: {
|
||||
data: [{ name: "In-House" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
res.json(result);
|
||||
const bodyshopId = result.insert_bodyshops_one.id;
|
||||
const vendorId = result.insert_bodyshops_one.vendors[0].id;
|
||||
|
||||
if (!bodyshopId || !vendorId) {
|
||||
throw new Error("Failed to create bodyshop or vendor");
|
||||
}
|
||||
|
||||
const updateBodyshop = await client.request(
|
||||
`mutation UPDATE_BODYSHOP($id: uuid!, $inhousevendorid: uuid!) {
|
||||
update_bodyshops_by_pk(pk_columns: { id: $id }, _set: { inhousevendorid: $inhousevendorid }) {
|
||||
id
|
||||
}
|
||||
}`,
|
||||
{
|
||||
id: bodyshopId,
|
||||
inhousevendorid: vendorId
|
||||
}
|
||||
);
|
||||
res.status(200).json(updateBodyshop);
|
||||
} catch (error) {
|
||||
logger.log("admin-create-shop-error", "error", req.user.email, null, {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
request: req.body,
|
||||
ioadmin: true
|
||||
});
|
||||
res.status(500).json(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@ const path = require("path");
|
||||
const queries = require("../graphql-client/queries");
|
||||
const moment = require("moment-timezone");
|
||||
const converter = require("json-2-csv");
|
||||
const _ = require("lodash");
|
||||
const logger = require("../utils/logger");
|
||||
const fs = require("fs");
|
||||
const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");
|
||||
|
||||
@@ -3,4 +3,5 @@ exports.autohouse = require("./autohouse").default;
|
||||
exports.chatter = require("./chatter").default;
|
||||
exports.claimscorp = require("./claimscorp").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: "" });
|
||||
emailer
|
||||
.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")}`,
|
||||
text: `
|
||||
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");
|
||||
|
||||
// Required Strings
|
||||
@@ -7,19 +5,6 @@ const { header, end, start } = require("./html");
|
||||
// - subHeader - The subheader 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
|
||||
* @param strings
|
||||
@@ -32,81 +17,48 @@ const generateEmailTemplate = (strings) => {
|
||||
header +
|
||||
start +
|
||||
`
|
||||
<table class="row">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th class="small-12 large-12 columns first last">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h6 style="text-align:left"><strong>${strings.header}</strong></h6>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p style="font-size:90%">${strings.subHeader}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Report Title -->
|
||||
${
|
||||
strings.header &&
|
||||
`
|
||||
<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: 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;">
|
||||
<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>
|
||||
</td></tr>
|
||||
</tbody></table></th>
|
||||
</tr></tbody></table>
|
||||
`
|
||||
}
|
||||
${
|
||||
strings.subHeader &&
|
||||
`
|
||||
<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: 95%;">${strings.subHeader}</p>
|
||||
</td></tr>
|
||||
</tbody></table></th>
|
||||
</tr></tbody></table>
|
||||
`
|
||||
}
|
||||
<!-- End Report Title -->
|
||||
<!-- Task Detail -->
|
||||
<table class="row">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th class="small-12 large-12 columns first last">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>${strings.body}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- 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
|
||||
${
|
||||
strings.body &&
|
||||
`
|
||||
<!-- Report Detail -->
|
||||
<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;">
|
||||
${strings.body}
|
||||
</td></tr>
|
||||
</tbody></table></th>
|
||||
</tr></tbody></table>
|
||||
<!-- End Report Detail -->
|
||||
`
|
||||
}
|
||||
` +
|
||||
end(strings.dateLine)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
2765
server/email/html.js
2765
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,
|
||||
cc: req?.body?.cc,
|
||||
subject: req?.body?.subject,
|
||||
email
|
||||
email,
|
||||
errorMessage: error?.message,
|
||||
errorStack: error?.stack
|
||||
// info,
|
||||
});
|
||||
}
|
||||
@@ -68,6 +70,7 @@ const sendServerEmail = async ({ subject, text }) => {
|
||||
]
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
(err, info) => {
|
||||
logger.log("server-email-failure", err ? "error" : "debug", null, null, {
|
||||
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 }) => {
|
||||
try {
|
||||
mailer.sendMail(
|
||||
@@ -93,6 +198,7 @@ const sendTaskEmail = async ({ to, subject, type = "text", html, text, attachmen
|
||||
...(type === "text" ? { text } : { html }),
|
||||
attachments: attachments || null
|
||||
},
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
(err, info) => {
|
||||
// (message, type, user, record, meta
|
||||
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,
|
||||
cc: req.body.cc,
|
||||
subject: req.body.subject,
|
||||
attachments:
|
||||
[
|
||||
...((req.body.attachments &&
|
||||
req.body.attachments.map((a) => {
|
||||
return {
|
||||
filename: a.filename,
|
||||
path: a.path
|
||||
};
|
||||
})) ||
|
||||
[]),
|
||||
...downloadedMedia.map((a) => {
|
||||
attachments: [
|
||||
...(req.body.attachments &&
|
||||
req.body.attachments.map((a) => {
|
||||
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,
|
||||
ses: {
|
||||
// optional extra arguments for SendRawEmail
|
||||
@@ -273,6 +377,7 @@ ${body.bounce?.bouncedRecipients.map(
|
||||
)}
|
||||
`
|
||||
},
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
(err, info) => {
|
||||
logger.log("sns-error", err ? "error" : "debug", "api", null, {
|
||||
errorMessage: err?.message,
|
||||
@@ -294,5 +399,6 @@ module.exports = {
|
||||
sendEmail,
|
||||
sendServerEmail,
|
||||
sendTaskEmail,
|
||||
emailBounce
|
||||
emailBounce,
|
||||
sendWelcomeEmail
|
||||
};
|
||||
|
||||
@@ -17,11 +17,13 @@ const { formatTaskPriority } = require("../notifications/stringHelpers");
|
||||
const tasksEmailQueue = taskEmailQueue();
|
||||
|
||||
// Cleanup function for the Tasks Email Queue
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const tasksEmailQueueCleanup = async () => {
|
||||
try {
|
||||
// Example async operation
|
||||
// console.log("Performing Tasks Email Reminder process cleanup...");
|
||||
await new Promise((resolve) => tasksEmailQueue.destroy(() => resolve()));
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
} catch (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`,
|
||||
subHeader: `Please click on the Tasks below to view the Task.`,
|
||||
dateLine,
|
||||
body: `<ul>
|
||||
body: `
|
||||
<ul style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; margin: 1%; padding-left: 30px;">
|
||||
${allTasks
|
||||
.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("")}
|
||||
</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 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({
|
||||
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 = {
|
||||
admin,
|
||||
createUser,
|
||||
@@ -208,23 +292,7 @@ module.exports = {
|
||||
getUser,
|
||||
sendNotification,
|
||||
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 path = require("path");
|
||||
require("dotenv").config({
|
||||
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
||||
});
|
||||
|
||||
//New bug introduced with Graphql Request.
|
||||
// https://github.com/prisma-labs/graphql-request/issues/206
|
||||
// const { Headers } = require("cross-fetch");
|
||||
// global.Headers = global.Headers || Headers;
|
||||
|
||||
exports.client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
|
||||
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
|
||||
headers: {
|
||||
"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 = `
|
||||
mutation UPDATE_JOB($jobId: uuid!, $job: jobs_set_input!) {
|
||||
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{
|
||||
delete_dms_vehicles(where: {}) {
|
||||
affected_rows
|
||||
@@ -2768,6 +2799,9 @@ exports.GET_BODYSHOP_BY_ID = `
|
||||
id
|
||||
md_order_statuses
|
||||
shopname
|
||||
imexshopid
|
||||
intellipay_config
|
||||
state
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -2829,3 +2863,68 @@ exports.GET_DOCUMENTS_BY_IDS = `
|
||||
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;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user