Compare commits

..

4 Commits

Author SHA1 Message Date
Patrick Fic
b3303e3c38 IO-2733 Add loading state and further delay reload. 2024-09-13 11:22:38 -07:00
Patrick Fic
73ec8b8a70 IO-2733 Resolve notification showing incorrect time. 2024-09-13 10:51:04 -07:00
Patrick Fic
954504de8d IO-2733 Add Timer Started check to prevent auto refresh early. 2024-09-13 09:58:46 -07:00
Patrick Fic
3bfa556b02 IO-2733 Added countdown timer to PWA Refresh & cache busting meta. 2024-09-10 15:54:15 -07:00
46 changed files with 15789 additions and 15018 deletions

View File

@@ -226,7 +226,9 @@ jobs:
command: |
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
hasura migrate apply --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >>
sleep 5
hasura metadata apply --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >>
sleep 10
hasura metadata reload --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >>
- jira/notify:
environment: Test (Rome) - Hasura
@@ -313,7 +315,9 @@ jobs:
command: |
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
hasura migrate apply --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
sleep 15
hasura metadata apply --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
sleep 30
hasura metadata reload --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
- jira/notify:
environment: Test (ImEX) - Hasura
@@ -423,7 +427,7 @@ workflows:
secret: ${HASURA_PROD_SECRET}
filters:
branches:
only: master
only: master-AIO
- rome-api-deploy:
filters:
branches:
@@ -433,7 +437,7 @@ workflows:
branches:
only: master-AIO
- rome-hasura-migrate:
secret: ${HASURA_PROD_SECRET}
secret: ${HASURA_ROME_PROD_SECRET}
filters:
branches:
only: master-AIO

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDWzCCAkOgAwIBAgIUD/QBSAXy/AlJ/cS4DaPWJLpChxgwDQYJKoZIhvcNAQEL
BQAwPTELMAkGA1UEBhMCQ0ExCzAJBgNVBAgMAk9OMSEwHwYDVQQKDBhJbnRlcm5l
dCBXaWRnaXRzIFB0eSBMdGQwHhcNMjQwOTA5MTU0MjA1WhcNMjUwOTA5MTU0MjA1
WjA9MQswCQYDVQQGEwJDQTELMAkGA1UECAwCT04xITAfBgNVBAoMGEludGVybmV0
IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AKSd0l7NJCNBwvtPU+dVPQkteg0AfC3sGqRnZMQteCRVa2oIgC4NoF3A9BK/yHbF
ZF25OnXTck5vzc8yb3v73ndfTD9ASKNoiaZE84/GFBsxqlKR8cs0qVwzuAsdijMv
vlMPNlMRyE1Rb7nR6HXGkPXNyxgMko03NXPkvIje9zRudm0Lf8L4q/hPyPkS7Mrm
/uQfAAJe+xFcupkEX2XY7r0x1C+z6E8lA1UcuhK3SHdW7CWYqp1vU5/dnnUiXwCa
GiC6Y1bCJB0pDAVISzy3JUDdINZdiqGR+y8ho3pstChf2mp/76s3N9eG9KA/qaFK
BrGk2PvCoZ8/Aj1aMsRYFHECAwEAAaNTMFEwHQYDVR0OBBYEFDLJ2fbWP4VUJgOp
PSs+NGHcVgRmMB8GA1UdIwQYMBaAFDLJ2fbWP4VUJgOpPSs+NGHcVgRmMA8GA1Ud
EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBABfv5ut/y03atq0NMB0jeDY4
AvW4ukk0k1svyqxFZCw9o7m2lHb/IjmVrZG1Sj4JWrrSv0s02ccb26/t6vazNa5L
Powe3eyfHgfjTZJmgs8hyeMwKS0wWk/SPuu9JDhIJakiquqD+UVBGkHpP+XYvhDv
vhS2XRlW+aEjpUmr1oCyyrc6WbzrYRNadqEsn/AxwcMyUbht3Ugjkg+OpidcTIQp
5lv63waKo6I1vQofzBQ3L7JYsKo8kC0vAP7wkLxvzBii335uZJzzpFYFVOyVNezi
dJdazPbRYbXz4LjltdEn/SNfRuKX8ZRiN2OSo7OfSrZaMTS87SfCSFJGgQM8Yrk=
-----END CERTIFICATE-----

View File

@@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCkndJezSQjQcL7
T1PnVT0JLXoNAHwt7BqkZ2TELXgkVWtqCIAuDaBdwPQSv8h2xWRduTp103JOb83P
Mm97+953X0w/QEijaImmRPOPxhQbMapSkfHLNKlcM7gLHYozL75TDzZTEchNUW+5
0eh1xpD1zcsYDJKNNzVz5LyI3vc0bnZtC3/C+Kv4T8j5EuzK5v7kHwACXvsRXLqZ
BF9l2O69MdQvs+hPJQNVHLoSt0h3VuwlmKqdb1Of3Z51Il8AmhogumNWwiQdKQwF
SEs8tyVA3SDWXYqhkfsvIaN6bLQoX9pqf++rNzfXhvSgP6mhSgaxpNj7wqGfPwI9
WjLEWBRxAgMBAAECggEAUNpHYlLFxh9dokujPUMreF+Cy/IKDBAkQc2au5RNpyLh
YDIOqw/8TTAhcTgLQPLQygvZP9f8E7RsVLFD+pSJ/v2qmIJ9au1Edor1Sg+S/oxV
SLrwFMunx2aLpcH7iAqSI3+cQg7A3+D4zD7iOz6tIl3Su9wo+v073tFhHKTOrEwv
Qgr9Jf3viIiKV1ym+uQEVQndocfsj46FnFpXTQ2qs7kAF6FgAOLDGfQQwzkiqEBD
NsqsDmbYIx6foZL+DEz1ZVO2M5B+xxpbNK82KwuQilVpimW8ui4LZHCe+RIFzt9+
BK6KGlLpSEwTFliivI3nahy18JzskZsfyah0CPZlQQKBgQDVv+A0qIPGvOP3Sx+9
HyeQCV23SkvvSvw8p8pMB0gvwv63YdJ7N/rJzBGS6YUHFWWZZgEeTgkJ6VJvoe0r
8JL1el9uSUa7f0eayjmFBOGuzpktNVdIn2Tg7A9MWA4JqPNNC69RMOh86ewGD4J3
a8Hz2a1bHxAmy/AZt2ukypY6eQKBgQDFJ7kqeOPkRBz9WbALRgVIXo8YWf5di0sQ
r0HC03GAISHQ725A2IFBPHJWeqj0jaMiIZD0y+Obgp7KAskrJaLfsd7Ug775kFfw
oUI9UAl6kRuPKvm3BaVAm46SQm+56VsgxTi73YN0NUp75THHZgAJjepF9zSpVJxq
VY9DjEGruQKBgQCQCpGIatcCol/tUg69X7VFd0pULhkl1J5OMbQ9r9qRdRI5eg5h
QsQaIQ7mtb8TmvOwf/DY/zVQHI+U8sXlCmW+TwzoQTENQSR7xzMj1LpRFqBaustr
AR72A537kItFLzll/i3SxOam5uxK2UDOQSuerF4KPdCglGXkrpo3nt3F4QKBgQCa
RArPAOjQo7tLQfJN3+wiRFsTYtd1uphx5bA/EdOtvj8HjVFnzADXWsTchf3N3UXY
XwtdgGwIMpys1KEz8a8P+c2x26SDAj7NOmDqOMYx8Xju/WGHpBM6Cn30U6e4gK+d
ZLSPyzQgqdIuP5hDvbwpvbGiLVw3Ys1BJtGCuSxpgQJ/eHnRiuSi5Zq5jGg+GpA+
FEEc9NCy772rR+4uzEOqyIwqewffqzSuVWuIsY/8MP3fh+NDxl/mU6cB5QVeD54Z
JZUKwmpM26muiM6WvVnM4ExPdSGA2+l4pZjby/KKd6F/w0tgZ1jb9Pb2/0vN3qVA
Y4U4XNTMt2fxUACqiL4SHA==
-----END PRIVATE KEY-----

View File

@@ -8,7 +8,7 @@ VITE_APP_CLOUDINARY_API_KEY=957865933348715
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
VITE_APP_AXIOS_BASE_API_URL=/api/
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_INSTANCE=IMEX

View File

@@ -8,7 +8,7 @@ VITE_APP_CLOUDINARY_API_KEY=957865933348715
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
VITE_APP_AXIOS_BASE_API_URL=/api/
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_INSTANCE=PROMANAGER

View File

@@ -9,7 +9,7 @@ VITE_APP_CLOUDINARY_API_KEY=957865933348715
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BP1B7ZTYpn-KMt6nOxlld6aS8Skt3Q7ZLEqP0hAvGHxG4UojPYiXZ6kPlzZkUC5jH-EcWXomTLtmadAIxurfcHo'
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
VITE_APP_AXIOS_BASE_API_URL=/api/
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_COUNTRY=USA

1
client/.gitignore vendored
View File

@@ -1,4 +1,3 @@
# Sentry Config File
.sentryclirc
/dev-dist

View File

@@ -12,6 +12,6 @@ module.exports = defineConfig({
setupNodeEvents(on, config) {
return require("./cypress/plugins/index.js")(on, config);
},
baseUrl: "https://localhost:3000"
baseUrl: "http://localhost:3000"
}
});

View File

@@ -2,6 +2,9 @@
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
<link rel="icon" href="/favicon.png"/>
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>

2650
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,22 +8,22 @@
"private": true,
"proxy": "http://localhost:4000",
"dependencies": {
"@ant-design/pro-layout": "^7.20.0",
"@apollo/client": "^3.11.8",
"@ant-design/pro-layout": "^7.19.12",
"@apollo/client": "^3.11.4",
"@emotion/is-prop-valid": "^1.3.0",
"@fingerprintjs/fingerprintjs": "^4.5.0",
"@fingerprintjs/fingerprintjs": "^4.4.3",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.2.7",
"@sentry/cli": "^2.36.1",
"@sentry/cli": "^2.33.1",
"@sentry/react": "^7.114.0",
"@splitsoftware/splitio-react": "^1.13.0",
"@splitsoftware/splitio-react": "^1.12.1",
"@tanem/react-nprogress": "^5.0.51",
"@vitejs/plugin-react": "^4.3.1",
"antd": "^5.20.6",
"antd": "^5.20.1",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0",
"autosize": "^6.0.1",
"axios": "^1.7.7",
"axios": "^1.7.4",
"classnames": "^2.5.1",
"css-box-model": "^1.2.1",
"dayjs": "^1.11.12",
@@ -32,12 +32,12 @@
"dotenv": "^16.4.5",
"env-cmd": "^10.1.0",
"exifr": "^7.1.3",
"firebase": "^10.13.1",
"firebase": "^10.12.5",
"graphql": "^16.9.0",
"i18next": "^23.15.1",
"i18next": "^23.12.3",
"i18next-browser-languagedetector": "^8.0.0",
"immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.11.8",
"libphonenumber-js": "^1.11.5",
"logrocket": "^8.1.2",
"markerjs2": "^2.32.1",
"memoize-one": "^6.0.0",
@@ -47,7 +47,7 @@
"query-string": "^9.1.0",
"raf-schd": "^4.0.3",
"react": "^18.3.1",
"react-big-calendar": "^1.13.4",
"react-big-calendar": "^1.13.2",
"react-color": "^2.19.3",
"react-cookie": "^7.2.0",
"react-dom": "^18.3.1",
@@ -58,15 +58,15 @@
"react-icons": "^5.3.0",
"react-image-lightbox": "^5.1.4",
"react-markdown": "^9.0.1",
"react-number-format": "^5.4.2",
"react-number-format": "^5.4.0",
"react-popopo": "^2.1.9",
"react-product-fruits": "^2.2.61",
"react-product-fruits": "^2.2.6",
"react-redux": "^9.1.2",
"react-resizable": "^3.0.5",
"react-router-dom": "^6.26.2",
"react-router-dom": "^6.26.0",
"react-sticky": "^6.0.3",
"react-virtualized": "^9.22.5",
"react-virtuoso": "^4.10.3",
"react-virtuoso": "^4.10.1",
"recharts": "^2.12.7",
"redux": "^5.0.1",
"redux-actions": "^3.0.3",
@@ -74,9 +74,9 @@
"redux-saga": "^1.3.0",
"redux-state-sync": "^3.1.4",
"reselect": "^5.1.1",
"sass": "^1.78.0",
"sass": "^1.77.8",
"socket.io-client": "^4.7.5",
"styled-components": "^6.1.13",
"styled-components": "^6.1.12",
"subscriptions-transport-ws": "^0.11.0",
"use-memo-one": "^1.1.3",
"userpilot": "^1.3.5",
@@ -90,9 +90,6 @@
"start:imex": "dotenvx run --env-file=.env.development.imex -- vite",
"start:rome": "dotenvx run --env-file=.env.development.rome -- vite",
"start:promanager": "dotenvx run --env-file=.env.development.promanager -- vite",
"preview:imex": "dotenvx run --env-file=.env.development.imex -- vite preview",
"preview:rome": "dotenvx run --env-file=.env.development.rome -- vite preview",
"preview:promanager": "dotenvx run --env-file=.env.development.promanager -- vite preview",
"build:test:imex": "env-cmd -f .env.test.imex npm run build",
"build:test:rome": "env-cmd -f .env.test.rome npm run build",
"build:test:promanager": "env-cmd -f .env.test.promanager npm run build",
@@ -133,16 +130,15 @@
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-react": "^7.24.7",
"@dotenvx/dotenvx": "^1.14.0",
"@dotenvx/dotenvx": "^1.7.0",
"@emotion/babel-plugin": "^11.12.0",
"@emotion/react": "^11.13.3",
"@sentry/webpack-plugin": "^2.22.4",
"@emotion/react": "^11.13.0",
"@sentry/webpack-plugin": "^2.22.2",
"@testing-library/cypress": "^10.0.2",
"browserslist": "^4.23.3",
"browserslist-to-esbuild": "^2.1.1",
"chalk": "^5.3.0",
"cross-env": "^7.0.3",
"cypress": "^13.14.2",
"cypress": "^13.13.3",
"eslint": "^8.57.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-cypress": "^2.15.1",
@@ -151,9 +147,10 @@
"react-error-overlay": "6.0.11",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.3",
"vite": "^5.4.3",
"vite": "^5.4.0",
"vite-plugin-babel": "^1.2.0",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-legacy": "^2.1.0",
"vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-pwa": "^0.20.1",
"vite-plugin-style-import": "^2.0.0",

View File

@@ -21,7 +21,6 @@ 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/socketContext.jsx";
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
@@ -202,9 +201,7 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
path="/manage/*"
element={
<ErrorBoundary>
<SocketProvider bodyshop={bodyshop}>
<PrivateRoute isAuthorized={currentUser.authorized} />
</SocketProvider>
<PrivateRoute isAuthorized={currentUser.authorized} />
</ErrorBoundary>
}
>
@@ -214,9 +211,7 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
path="/tech/*"
element={
<ErrorBoundary>
<SocketProvider bodyshop={bodyshop}>
<PrivateRoute isAuthorized={currentUser.authorized} />
</SocketProvider>
<PrivateRoute isAuthorized={currentUser.authorized} />
</ErrorBoundary>
}
>

View File

@@ -1,77 +1,62 @@
import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Form, Input, Table } from "antd";
import React, { useEffect, useState, useContext } from "react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { pageLimit } from "../../utils/config";
import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; // Import SocketContext
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop
});
export default connect(mapStateToProps)(DmsAllocationsSummaryAp);
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function DmsAllocationsSummaryAp({ bodyshop, billids, title }) {
export default connect(mapStateToProps, mapDispatchToProps)(DmsAllocationsSummaryAp);
export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
const { t } = useTranslation();
const [allocationsSummary, setAllocationsSummary] = useState([]);
const { socket } = useContext(SocketContext);
useEffect(() => {
if (!socket) return;
const handleSuccess = async (billid) => {
socket.on("ap-export-success", (billid) => {
setAllocationsSummary((allocationsSummary) =>
allocationsSummary.map((a) => {
if (a.billid !== billid) return a;
return { ...a, status: "Successful" };
})
);
});
socket.on("ap-export-failure", ({ billid, error }) => {
allocationsSummary.map((a) => {
if (a.billid !== billid) return a;
return { ...a, status: error };
});
});
try {
await new Promise((resolve, reject) => {
socket.emit("clear-dms-session", (response) => {
if (response && response.status === "ok") {
resolve();
} else {
reject(new Error("Failed to clear DMS session"));
}
});
});
} catch (error) {
console.error("Failed to clear DMS session", error);
}
};
const handleFailure = ({ billid, error }) => {
setAllocationsSummary((allocationsSummary) =>
allocationsSummary.map((a) => {
if (a.billid !== billid) return a;
return { ...a, status: error };
})
);
};
socket.on("ap-export-success", handleSuccess);
socket.on("ap-export-failure", handleFailure);
if (socket.disconnected) socket.connect();
return () => {
socket.off("ap-export-success", handleSuccess);
socket.off("ap-export-failure", handleFailure);
socket.removeListener("ap-export-success");
socket.removeListener("ap-export-failure");
//socket.disconnect();
};
}, [socket]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (socket && socket.connected) {
if (socket.connected) {
socket.emit("pbs-calculate-allocations-ap", billids, (ack) => {
setAllocationsSummary(ack);
socket.allocationsSummary = ack;
});
}
}, [socket, socket.connected, billids]);
console.log(allocationsSummary);
const columns = [
{
title: t("general.labels.status"),
@@ -83,40 +68,35 @@ export function DmsAllocationsSummaryAp({ bodyshop, billids, title }) {
dataIndex: ["Posting", "Reference"],
key: "reference"
},
{
title: t("jobs.fields.dms.lines"),
dataIndex: "Lines",
key: "Lines",
render: (text, record) => (
<table style={{ tableLayout: "auto", width: "100%" }}>
<thead>
<tr>
<th>{t("bills.fields.invoice_number")}</th>
<th>{t("bodyshop.fields.dms.dms_acctnumber")}</th>
<th>{t("jobs.fields.dms.amount")}</th>
<tr>
<th>{t("bills.fields.invoice_number")}</th>
<th>{t("bodyshop.fields.dms.dms_acctnumber")}</th>
<th>{t("jobs.fields.dms.amount")}</th>
</tr>
{record.Posting.Lines.map((l, idx) => (
<tr key={idx}>
<td>{l.InvoiceNumber}</td>
<td>{l.Account}</td>
<td>{l.Amount}</td>
</tr>
</thead>
<tbody>
{record.Posting.Lines.map((l, idx) => (
<tr key={idx}>
<td>{l.InvoiceNumber}</td>
<td>{l.Account}</td>
<td>{l.Amount}</td>
</tr>
))}
</tbody>
))}
</table>
)
}
];
const handleFinish = async (values) => {
if (socket) {
socket.emit("pbs-export-ap", {
billids,
txEnvelope: values
});
}
socket.emit(`pbs-export-ap`, {
billids,
txEnvelope: values
});
};
return (
@@ -125,9 +105,7 @@ export function DmsAllocationsSummaryAp({ bodyshop, billids, title }) {
extra={
<Button
onClick={() => {
if (socket) {
socket.emit("pbs-calculate-allocations-ap", billids, (ack) => setAllocationsSummary(ack));
}
socket.emit("pbs-calculate-allocations-ap", billids, (ack) => setAllocationsSummary(ack));
}}
>
<SyncOutlined />
@@ -146,7 +124,12 @@ export function DmsAllocationsSummaryAp({ bodyshop, billids, title }) {
name="journal"
label={t("jobs.fields.dms.journal")}
initialValue={bodyshop.cdk_configuration && bodyshop.cdk_configuration.default_journal}
rules={[{ required: true }]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Input />
</Form.Item>

View File

@@ -1,51 +1,37 @@
import { Button, Checkbox, Col, Table } from "antd";
import React, { useContext, useEffect, useState } from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { socket } from "../../pages/dms/dms.container";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { alphaSort } from "../../utils/sorters";
import SocketContext from "../../contexts/SocketIO/socketContext"; // Import Socket Context
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(DmsCustomerSelector);
export function DmsCustomerSelector({ bodyshop }) {
const { t } = useTranslation();
const { socket } = useContext(SocketContext); // Use Socket Context
const [customerList, setCustomerList] = useState([]);
const [customerList, setcustomerList] = useState([]);
const [open, setOpen] = useState(false);
const [selectedCustomer, setSelectedCustomer] = useState(null);
const [dmsType, setDmsType] = useState("cdk");
useEffect(() => {
if (socket) {
const handleCdkSelectCustomer = (customerList) => {
setOpen(true);
setDmsType("cdk");
setCustomerList(customerList);
};
const handlePbsSelectCustomer = (customerList) => {
setOpen(true);
setDmsType("pbs");
setCustomerList(customerList);
};
socket.on("cdk-select-customer", handleCdkSelectCustomer);
socket.on("pbs-select-customer", handlePbsSelectCustomer);
// Clean up listeners on unmount
return () => {
socket.off("cdk-select-customer", handleCdkSelectCustomer);
socket.off("pbs-select-customer", handlePbsSelectCustomer);
};
}
}, [socket]);
socket.on("cdk-select-customer", (customerList, callback) => {
setOpen(true);
setDmsType("cdk");
setcustomerList(customerList);
});
socket.on("pbs-select-customer", (customerList, callback) => {
setOpen(true);
setDmsType("pbs");
setcustomerList(customerList);
});
const onUseSelected = () => {
setOpen(false);
@@ -83,11 +69,17 @@ export function DmsCustomerSelector({ bodyshop }) {
key: "name1",
sorter: (a, b) => alphaSort(a.name1 && a.name1.fullName, b.name1 && b.name1.fullName)
},
{
title: t("jobs.fields.dms.address"),
//dataIndex: ["name2", "fullName"],
key: "address",
render: (record) =>
`${record.address?.addressLine?.[0]}, ${record.address?.city} ${record.address?.stateOrProvince} ${record.address?.postalCode}`
render: (record, value) =>
`${
record.address && record.address.addressLine && record.address.addressLine[0]
}, ${record.address && record.address.city} ${
record.address && record.address.stateOrProvince
} ${record.address && record.address.postalCode}`
}
];
@@ -103,15 +95,15 @@ export function DmsCustomerSelector({ bodyshop }) {
sorter: (a, b) => alphaSort(a.LastName, b.LastName),
render: (text, record) => `${record.FirstName || ""} ${record.LastName || ""}`
},
{
title: t("jobs.fields.dms.address"),
key: "address",
render: (record) => `${record.Address}, ${record.City} ${record.State} ${record.ZipCode}`
render: (record, value) => `${record.Address}, ${record.City} ${record.State} ${record.ZipCode}`
}
];
if (!open) return null;
return (
<Col span={24}>
<Table
@@ -133,6 +125,7 @@ export function DmsCustomerSelector({ bodyshop }) {
columns={dmsType === "cdk" ? cdkColumns : pbsColumns}
rowKey={(record) => (dmsType === "cdk" ? record.id.value : record.ContactId)}
dataSource={customerList}
//onChange={handleTableChange}
rowSelection={{
onSelect: (record) => {
setSelectedCustomer(dmsType === "cdk" ? record.id.value : record.ContactId);

View File

@@ -4,8 +4,11 @@ import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({});
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
@@ -14,7 +17,7 @@ const mapDispatchToProps = (dispatch) => ({
export default connect(mapStateToProps, mapDispatchToProps)(DmsLogEvents);
export function DmsLogEvents({ logs }) {
export function DmsLogEvents({ socket, logs, bodyshop }) {
return (
<Timeline
pending

View File

@@ -1,10 +1,9 @@
import dayjs from "../../utils/day";
import { SyncOutlined } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client";
import Board from "./trello-board/index";
import { Button, notification, Skeleton, Space } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import React, { useCallback, useContext, useEffect, useMemo, useState } from "react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -24,7 +23,6 @@ import cloneDeep from "lodash/cloneDeep";
import isEqual from "lodash/isEqual";
import { defaultFilters, mergeWithDefaults } from "./settings/defaultKanbanSettings.js";
import NoteUpsertModal from "../../components/note-upsert-modal/note-upsert-modal.container";
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -47,136 +45,10 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
const [loading, setLoading] = useState(true);
const [isMoving, setIsMoving] = useState(false);
const [orientation, setOrientation] = useState("vertical");
const { socket } = useContext(SocketContext); // Access socket from the context
const { t } = useTranslation();
const client = useApolloClient();
const handleJobUpdated = useCallback(
(updatedJob) => {
setBoardLanes((prevBoardLanes) => {
const updatedLanes = cloneDeep(prevBoardLanes.lanes);
// Find the lane containing the card with the updated job ID
let sourceLane = updatedLanes.find((lane) => lane.cards.some((card) => card.id === updatedJob.id));
if (!sourceLane) {
console.log("Card not found in any lane. Checking for valid status to add it.");
// Find the target lane based on the new status if the card does not exist
const targetLane = updatedLanes.find((lane) => lane.id === updatedJob.status);
if (targetLane && updatedJob.isInProduction) {
// Check if job is in production and status is valid
console.log(`Adding card to lane ${targetLane.title}`);
// Add the new card to the target lane
const newCard = {
id: updatedJob.id,
metadata: { ...updatedJob }
};
targetLane.cards.push(newCard);
// Update the lane title with the new card count
targetLane.title = `${targetLane.title.split(" ")[0]} (${targetLane.cards.length})`;
return { lanes: updatedLanes }; // Return early since the card is added
} else {
console.error("No valid lane or status to add the job to.");
return prevBoardLanes; // Return the previous state if no valid status or lane
}
}
// If the card exists, find it in the source lane
const cardIndex = sourceLane.cards.findIndex((card) => card.id === updatedJob.id);
const currentCard = sourceLane.cards[cardIndex];
// If we somehow can't find the card, return
if (!currentCard) {
console.error("Card not found for the updated job.");
return prevBoardLanes; // Return the previous state if the card is not found
}
// Iterate through the properties of updatedJob and update the corresponding values in currentCard.metadata
Object.keys(updatedJob).forEach((key) => {
// Normalize date fields by comparing their ISO strings or timestamps
if (key === "updated_at") {
const currentCardDate = dayjs(currentCard.metadata[key]).toISOString();
const updatedJobDate = dayjs(updatedJob[key]).toISOString();
if (currentCardDate !== updatedJobDate) {
console.log(`Updating ${key} from ${currentCardDate} to ${updatedJobDate}`);
currentCard.metadata[key] = updatedJob[key]; // Assign the new value if different
}
} else if (key in currentCard.metadata && currentCard.metadata[key] !== updatedJob[key]) {
console.log(`Updating ${key} from ${currentCard.metadata[key]} to ${updatedJob[key]}`);
currentCard.metadata[key] = updatedJob[key];
}
});
// Mark that data has been changed if any field was updated
const isDataChanged = !isEqual(currentCard.metadata, updatedJob);
// Check if the lane (status) has changed
const isLaneChanged = updatedJob.status !== sourceLane.id;
// Case 1: Both data and lane have changed
if (isDataChanged && isLaneChanged) {
console.log("Case 1: Data and Lane Changed");
// Remove the card from the source lane
const [cardToMove] = sourceLane.cards.splice(cardIndex, 1);
// Find the target lane based on the new status
const targetLane = updatedLanes.find((lane) => lane.id === updatedJob.status);
if (targetLane) {
targetLane.cards.push({ ...cardToMove, metadata: { ...currentCard.metadata } });
sourceLane.title = `${sourceLane.title.split(" ")[0]} (${sourceLane.cards.length})`;
targetLane.title = `${targetLane.title.split(" ")[0]} (${targetLane.cards.length})`;
} else {
console.error("Target lane not found for the updated job.");
}
}
// Case 2: Only data has changed
else if (isDataChanged && !isLaneChanged) {
console.log("Case 2: Only Data Changed");
sourceLane.cards[cardIndex] = { ...currentCard, metadata: { ...currentCard.metadata } };
}
// Case 3: Only the lane has changed
else if (!isDataChanged && isLaneChanged) {
console.log("Case 3: Only Lane Changed");
// Remove the card from the source lane
const [cardToMove] = sourceLane.cards.splice(cardIndex, 1);
// Find the target lane based on the new status
const targetLane = updatedLanes.find((lane) => lane.id === updatedJob.status);
if (targetLane) {
targetLane.cards.push(cardToMove);
sourceLane.title = `${sourceLane.title.split(" ")[0]} (${sourceLane.cards.length})`;
targetLane.title = `${targetLane.title.split(" ")[0]} (${targetLane.cards.length})`;
} else {
console.error("Target lane not found for the updated job.");
}
}
return { lanes: updatedLanes };
});
},
[setBoardLanes]
);
useEffect(() => {
// Listen for the job-updated event from the socket
if (socket) {
socket.on("job-updated", handleJobUpdated);
return () => {
socket.off("job-updated", handleJobUpdated);
};
}
}, [socket, handleJobUpdated]);
useEffect(() => {
if (associationSettings) {
setLoading(true);

View File

@@ -28,23 +28,22 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser }) {
onError: (error) => console.error(`Error fetching jobs in production: ${error.message}`)
});
// const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION, {
// onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
// });
const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION, {
onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
});
const { loading: associationSettingsLoading, data: associationSettings } = useQuery(QUERY_KANBAN_SETTINGS, {
variables: { email: currentUser.email },
onError: (error) => console.error(`Error fetching Kanban settings: ${error.message}`)
});
// This provides us the current version of the Lanes from the Redux store
// const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
// useEffect(() => {
// if (updatedJobs && data) {
// refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`));
// }
// }, [updatedJobs, data, refetch]);
useEffect(() => {
if (updatedJobs && data) {
refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`));
}
}, [updatedJobs, data, refetch]);
const filteredAssociationSettings = useMemo(() => {
return associationSettings?.associations[0] || null;

View File

@@ -1,13 +1,14 @@
import { AlertOutlined } from "@ant-design/icons";
import { Alert, Button, Col, Row, Space } from "antd";
import { Alert, Button, Col, notification, Row, Space } from "antd";
import i18n from "i18next";
import React, { useEffect } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectUpdateAvailable } from "../../redux/application/application.selectors";
import { useRegisterSW } from "virtual:pwa-register/react";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import useCountDown from "../../utils/countdownHook";
const mapStateToProps = createStructuredSelector({
updateAvailable: selectUpdateAvailable
@@ -19,6 +20,15 @@ const mapDispatchToProps = (dispatch) => ({
export function UpdateAlert({ updateAvailable }) {
const { t } = useTranslation();
const [timerStarted, setTimerStarted] = useState(false);
const [loading, setLoading] = useState(false);
const [
timeLeft,
{
start //pause, resume, reset
}
] = useCountDown(180000, 1000);
const {
offlineReady: [offlineReady],
needRefresh: [needRefresh],
@@ -40,11 +50,43 @@ export function UpdateAlert({ updateAvailable }) {
}
});
const ReloadNewVersion = useCallback(() => {
setLoading(true);
updateServiceWorker(true);
setTimeout(() => {
window.location.reload(true);
}, 5000);
}, [updateServiceWorker]);
useEffect(() => {
if (import.meta.env.DEV) {
console.log(`SW Status => Refresh? ${needRefresh} - offlineReady? ${offlineReady}`);
if (needRefresh) {
start();
setTimerStarted(true);
}
}, [needRefresh, offlineReady]);
}, [start, needRefresh, offlineReady]);
useEffect(() => {
if (needRefresh && timerStarted && timeLeft < 60000) {
notification.open({
type: "warning",
closable: false,
duration: 65000,
key: "autoupdate",
message: t("general.actions.autoupdate", {
time: (timeLeft / 1000).toFixed(0),
app: InstanceRenderManager({
imex: "$t(titles.imexonline)",
rome: "$t(titles.romeonline)",
promanager: "$t(titles.promanager)"
})
}),
placement: "bottomRight"
});
}
if (needRefresh && timerStarted && timeLeft <= 0) {
ReloadNewVersion();
}
}, [timeLeft, t, needRefresh, ReloadNewVersion, timerStarted]);
if (!needRefresh) return null;
@@ -75,9 +117,10 @@ export function UpdateAlert({ updateAvailable }) {
<Button onClick={() => window.open("https://imex-online.noticeable.news/", "_blank")}>
{i18n.t("general.actions.viewreleasenotes")}
</Button>
<Button type="primary" onClick={() => updateServiceWorker(true)}>
{i18n.t("general.actions.refresh")}
<Button loading={loading} type="primary" onClick={() => ReloadNewVersion()}>
{i18n.t("general.actions.refresh")} {`(${(timeLeft / 1000).toFixed(0)} s)`}
</Button>
<Button onClick={() => start(300000)}>{i18n.t("general.actions.delay")}</Button>
</Space>
</Col>
</Row>

View File

@@ -1,13 +0,0 @@
import React, { createContext } from "react";
import useSocket from "./useSocket"; // Import the custom hook
// Create the SocketContext
const SocketContext = createContext(null);
export const SocketProvider = ({ children, bodyshop }) => {
const { socket, clientId } = useSocket(bodyshop);
return <SocketContext.Provider value={{ socket, clientId }}> {children}</SocketContext.Provider>;
};
export default SocketContext;

View File

@@ -1,75 +0,0 @@
import { useEffect, useState } from "react";
import SocketIO from "socket.io-client";
import { auth } from "../../firebase/firebase.utils";
const useSocket = (bodyshop) => {
const [socket, setSocket] = useState(null);
const [clientId, setClientId] = useState(null); // State to store unique identifier
useEffect(() => {
const handleBodyshopMessage = (message) => {
console.log(`Received message for bodyshop ${bodyshop.id}:`, message);
};
if (bodyshop && bodyshop.id) {
const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "https://localhost:3000";
const socketInstance = SocketIO(endpoint, {
path: "/ws", // Ensure this matches the Vite proxy and backend path
withCredentials: true,
auth: async (callback) => {
const token = auth.currentUser && (await auth.currentUser.getIdToken());
callback({ token });
},
reconnectionAttempts: Infinity, // Try reconnecting forever
reconnectionDelay: 2000, // How long to wait between reconnection attempts
reconnectionDelayMax: 10000 // Maximum delay between attempts
});
setSocket(socketInstance);
// When the socket connects or reconnects, join the bodyshop room
const joinRoomOnConnect = () => {
console.log("Socket connected:", socketInstance.id);
setClientId(socketInstance.id);
if (bodyshop.id) {
socketInstance.emit("join-bodyshop-room", bodyshop.id);
console.log(`Joined bodyshop room: ${bodyshop.id}`);
}
};
// Set up the necessary socket event handlers
socketInstance.on("connect", joinRoomOnConnect);
socketInstance.on("reconnect", (attempt) => {
console.log(`Socket reconnected after ${attempt} attempts`);
});
socketInstance.on("bodyshop-message", handleBodyshopMessage);
socketInstance.on("connect_error", (err) => {
console.error("Socket connection error:", err);
});
socketInstance.on("disconnect", () => {
console.log("Socket disconnected");
});
// Clean up on component unmount or when bodyshop changes
return () => {
if (bodyshop?.id) {
socketInstance.emit("leave-bodyshop-room", bodyshop.id);
}
socketInstance.off("connect", joinRoomOnConnect);
socketInstance.off("bodyshop-message", handleBodyshopMessage);
socketInstance.disconnect();
};
}
}, [bodyshop]);
// Return both socket and clientId
return { socket, clientId };
};
export default useSocket;

View File

@@ -1,16 +1,20 @@
import { Button, Card, Col, notification, Row, Select, Space } from "antd";
import React, { useContext, useEffect, useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import SocketIO from "socket.io-client";
import DmsAllocationsSummaryApComponent from "../../components/dms-allocations-summary-ap/dms-allocations-summary-ap.component";
import DmsLogEvents from "../../components/dms-log-events/dms-log-events.component";
import { auth } from "../../firebase/firebase.utils";
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import SocketContext from "../../contexts/SocketIO/socketContext";
const mapStateToProps = createStructuredSelector({});
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
@@ -19,9 +23,20 @@ const mapDispatchToProps = (dispatch) => ({
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
export function DmsContainer({ setBreadcrumbs, setSelectedHeader }) {
export const socket = SocketIO(
import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : window.location.origin,
{
path: "/ws",
withCredentials: true,
auth: async (callback) => {
const token = auth.currentUser && (await auth.currentUser.getIdToken());
callback({ token });
}
}
);
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
const { t } = useTranslation();
const { socket } = useContext(SocketContext);
const [logLevel, setLogLevel] = useState("DEBUG");
const history = useNavigate();
const [logs, setLogs] = useState([]);
@@ -52,43 +67,40 @@ export function DmsContainer({ setBreadcrumbs, setSelectedHeader }) {
}, [t, setBreadcrumbs, setSelectedHeader]);
useEffect(() => {
if (socket) {
const handleConnect = () => socket.emit("set-log-level", logLevel);
const handleReconnect = () => {
setLogs((logs) => [
socket.on("connect", () => socket.emit("set-log-level", logLevel));
socket.on("reconnect", () => {
setLogs((logs) => {
return [
...logs,
{
timestamp: new Date(),
level: "WARNING",
message: "Reconnected to CDK Export Service"
}
]);
};
const handleLogEvent = (payload) => {
setLogs((logs) => [...logs, payload]);
};
const handleExportComplete = () => {
notification.open({
type: "success",
message: t("jobs.labels.dms.apexported")
});
};
];
});
});
socket.on("connect", handleConnect);
socket.on("reconnect", handleReconnect);
socket.on("log-event", handleLogEvent);
socket.on("ap-export-complete", handleExportComplete);
socket.on("log-event", (payload) => {
setLogs((logs) => {
return [...logs, payload];
});
});
if (socket.disconnected) socket.connect();
socket.on("ap-export-complete", (payload) => {
notification.open({
type: "success",
message: t("jobs.labels.dms.apexported")
});
});
return () => {
socket.off("connect", handleConnect);
socket.off("reconnect", handleReconnect);
socket.off("log-event", handleLogEvent);
socket.off("ap-export-complete", handleExportComplete);
};
}
}, [socket, logLevel, t]);
if (socket.disconnected) socket.connect();
return () => {
socket.removeAllListeners();
socket.disconnect();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!state?.billids) {
history(`/manage/accounting/payables`);
@@ -124,20 +136,27 @@ export function DmsContainer({ setBreadcrumbs, setSelectedHeader }) {
<Button
onClick={() => {
setLogs([]);
if (socket) {
socket.emit("clear-dms-session");
}
socket.disconnect();
socket.connect();
}}
>
Clear Session
Reconnect
</Button>
</Space>
}
>
<DmsLogEvents logs={logs} />
<DmsLogEvents socket={socket} logs={logs} />
</Card>
</div>
</Col>
</Row>
);
}
export const determineDmsType = (bodyshop) => {
if (bodyshop.cdk_dealerid) return "cdk";
else {
return "pbs";
}
};

View File

@@ -1,11 +1,12 @@
import { useQuery } from "@apollo/client";
import { Button, Card, Col, notification, Result, Row, Select, Space } from "antd";
import queryString from "query-string";
import React, { useContext, useEffect, useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import SocketIO from "socket.io-client";
import AlertComponent from "../../components/alert/alert.component";
import DmsAllocationsSummary from "../../components/dms-allocations-summary/dms-allocations-summary.component";
import DmsCustomerSelector from "../../components/dms-customer-selector/dms-customer-selector.component";
@@ -13,12 +14,12 @@ import DmsLogEvents from "../../components/dms-log-events/dms-log-events.compone
import DmsPostForm from "../../components/dms-post-form/dms-post-form.component";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import { OwnerNameDisplayFunction } from "../../components/owner-name-display/owner-name-display.component";
import { auth } from "../../firebase/firebase.utils";
import { QUERY_JOB_EXPORT_DMS } from "../../graphql/jobs.queries";
import { insertAuditTrail, setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import SocketContext from "../../contexts/SocketIO/socketContext";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -27,21 +28,25 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(
insertAuditTrail({
jobid,
operation,
type
})
)
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
});
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
export const socket = SocketIO(
import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "http://localhost:4000", // for dev testing,
{
path: "/ws",
withCredentials: true,
auth: async (callback) => {
const token = auth.currentUser && (await auth.currentUser.getIdToken());
callback({ token });
}
}
);
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, insertAuditTrail }) {
const { t } = useTranslation();
const { socket } = useContext(SocketContext);
const [logLevel, setLogLevel] = useState("DEBUG");
const history = useNavigate();
const [logs, setLogs] = useState([]);
@@ -54,7 +59,6 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
fetchPolicy: "network-only",
nextFetchPolicy: "network-only"
});
const logsRef = useRef(null);
useEffect(() => {
@@ -79,73 +83,47 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
}, [t, setBreadcrumbs, setSelectedHeader]);
useEffect(() => {
if (socket) {
const handleConnect = () => {
socket.emit("set-log-level", logLevel);
};
const handleReconnect = () => {
setLogs((logs) => [
socket.on("connect", () => socket.emit("set-log-level", logLevel));
socket.on("reconnect", () => {
setLogs((logs) => {
return [
...logs,
{
timestamp: new Date(),
level: "WARNING",
message: "Reconnected to CDK Export Service"
}
]);
};
];
});
});
socket.on("connect_error", (err) => {
console.log(`connect_error due to ${err}`, err);
notification.error({ message: err.message });
});
socket.on("log-event", (payload) => {
setLogs((logs) => {
return [...logs, payload];
});
});
socket.on("export-success", (payload) => {
notification.success({
message: t("jobs.successes.exported")
});
insertAuditTrail({
jobid: payload,
operation: AuditTrailMapping.jobexported(),
type: "jobexported"
});
history("/manage/accounting/receivables");
});
const handleConnectError = (err) => {
console.log(`connect_error due to ${err}`, err);
notification.error({ message: err.message });
};
const handleLogEvent = (payload) => {
setLogs((logs) => [...logs, payload]);
};
const handleExportSuccess = async (payload) => {
notification.success({
message: t("jobs.successes.exported")
});
insertAuditTrail({
jobid: payload,
operation: AuditTrailMapping.jobexported(),
type: "jobexported"
});
try {
await new Promise((resolve, reject) => {
socket.emit("clear-dms-session", (response) => {
if (response && response.status === "ok") {
resolve();
} else {
reject(new Error("Failed to clear DMS session"));
}
});
});
} catch (error) {
console.error("Failed to clear DMS session", error);
}
history("/manage/accounting/receivables");
};
socket.on("connect", handleConnect);
socket.on("reconnect", handleReconnect);
socket.on("connect_error", handleConnectError);
socket.on("log-event", handleLogEvent);
socket.on("export-success", handleExportSuccess);
return () => {
socket.off("connect", handleConnect);
socket.off("reconnect", handleReconnect);
socket.off("connect_error", handleConnectError);
socket.off("log-event", handleLogEvent);
socket.off("export-success", handleExportSuccess);
};
}
}, [socket, logLevel, t, insertAuditTrail, history]);
if (socket.disconnected) socket.connect();
return () => {
socket.removeAllListeners();
socket.disconnect();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type="error" />;
@@ -201,21 +179,20 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
<Select.Option key="WARNING">WARNING</Select.Option>
<Select.Option key="ERROR">ERROR</Select.Option>
</Select>
4<Button onClick={() => setLogs([])}>Clear Logs</Button>
<Button onClick={() => setLogs([])}>Clear Logs</Button>
<Button
onClick={() => {
setLogs([]);
if (socket) {
socket.emit("clear-dms-session");
}
socket.disconnect();
socket.connect();
}}
>
Clear Session
Reconnect
</Button>
</Space>
}
>
<DmsLogEvents logs={logs} />
<DmsLogEvents socket={socket} logs={logs} />
</Card>
</div>
</Col>

View File

@@ -1,6 +1,6 @@
import { FloatButton, Layout, Spin } from "antd";
// import preval from "preval.macro";
import React, { lazy, Suspense, useContext, useEffect, useState } from "react";
import React, { lazy, Suspense, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, Route, Routes } from "react-router-dom";
@@ -18,12 +18,12 @@ import LoadingSpinner from "../../components/loading-spinner/loading-spinner.com
import PartnerPingComponent from "../../components/partner-ping/partner-ping.component";
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 { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors";
import UpdateAlert from "../../components/update-alert/update-alert.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
import "./manage.page.styles.scss";
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
const JobsPage = lazy(() => import("../jobs/jobs.page"));
@@ -110,7 +110,17 @@ const mapDispatchToProps = (dispatch) => ({});
export function Manage({ conflict, bodyshop }) {
const { t } = useTranslation();
const [chatVisible] = useState(false);
const { socket, clientId } = useContext(SocketContext);
useEffect(() => {
const widgetId = InstanceRenderManager({
imex: "IABVNO4scRKY11XBQkNr",
rome: "mQdqARMzkZRUVugJ6TdS"
});
window.noticeable.render("widget", widgetId);
requestForToken().catch((error) => {
console.error(`Unable to request for token.`, error);
});
}, []);
useEffect(() => {
document.title = InstanceRenderManager({
@@ -119,7 +129,6 @@ export function Manage({ conflict, bodyshop }) {
promanager: t("titles.promanager")
});
}, [t]);
const AppRouteTable = (
<Suspense
fallback={
@@ -560,13 +569,6 @@ export function Manage({ conflict, bodyshop }) {
else if (bodyshop && bodyshop.sub_status !== "active") PageContent = <ShopSubStatusComponent />;
else PageContent = AppRouteTable;
const broadcastMessage = () => {
if (socket && bodyshop && bodyshop.id) {
console.log(`Broadcasting message to bodyshop ${bodyshop.id}:`);
socket.emit("broadcast-to-bodyshop", bodyshop.id, `Hello from ${clientId}`);
}
};
return (
<>
{import.meta.env.PROD && <ChatAffixContainer bodyshop={bodyshop} chatVisible={chatVisible} />}
@@ -601,8 +603,6 @@ export function Manage({ conflict, bodyshop }) {
</div>
<div id="noticeable-widget" style={{ marginLeft: "1rem" }} />
</div>
<button onClick={broadcastMessage}>Broadcast Message</button>
<Link to="/disclaimer" target="_blank" style={{ color: "#ccc" }}>
Disclaimer & Notices
</Link>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,9 +2,9 @@ import axios from "axios";
import { auth } from "../firebase/firebase.utils";
import InstanceRenderManager from "./instanceRenderMgr";
axios.defaults.baseURL = import.meta.env.DEV
? "/api/"
: import.meta.env.VITE_APP_AXIOS_BASE_API_URL || "https://api.imex.online/";
axios.defaults.baseURL =
import.meta.env.VITE_APP_AXIOS_BASE_API_URL ||
(import.meta.env.MODE === "production" ? "https://api.imex.online/" : "http://localhost:4000/");
export const axiosAuthInterceptorId = axios.interceptors.request.use(
async (config) => {

View File

@@ -0,0 +1,84 @@
import React from "react";
const useCountDown = (timeToCount = 60 * 1000, interval = 1000) => {
const [timeLeft, setTimeLeft] = React.useState(0);
const timer = React.useRef({});
const run = (ts) => {
if (!timer.current.started) {
timer.current.started = ts;
timer.current.lastInterval = ts;
}
const localInterval = Math.min(interval, timer.current.timeLeft || Infinity);
if (ts - timer.current.lastInterval >= localInterval) {
timer.current.lastInterval += localInterval;
setTimeLeft((timeLeft) => {
timer.current.timeLeft = timeLeft - localInterval;
return timer.current.timeLeft;
});
}
if (ts - timer.current.started < timer.current.timeToCount) {
timer.current.requestId = window.requestAnimationFrame(run);
} else {
timer.current = {};
setTimeLeft(0);
}
};
const start = React.useCallback(
(ttc) => {
window.cancelAnimationFrame(timer.current.requestId);
const newTimeToCount = ttc !== undefined ? ttc : timeToCount;
timer.current.started = null;
timer.current.lastInterval = null;
timer.current.timeToCount = newTimeToCount;
timer.current.requestId = window.requestAnimationFrame(run);
setTimeLeft(newTimeToCount);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const pause = React.useCallback(() => {
window.cancelAnimationFrame(timer.current.requestId);
timer.current.started = null;
timer.current.lastInterval = null;
timer.current.timeToCount = timer.current.timeLeft;
}, []);
const resume = React.useCallback(
() => {
if (!timer.current.started && timer.current.timeLeft > 0) {
window.cancelAnimationFrame(timer.current.requestId);
timer.current.requestId = window.requestAnimationFrame(run);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const reset = React.useCallback(() => {
if (timer.current.timeLeft) {
window.cancelAnimationFrame(timer.current.requestId);
timer.current = {};
setTimeLeft(0);
}
}, []);
const actions = React.useMemo(
() => ({ start, pause, resume, reset }), // eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
React.useEffect(() => {
return () => window.cancelAnimationFrame(timer.current.requestId);
}, []);
return [timeLeft, actions];
};
export default useCountDown;

View File

@@ -3,21 +3,16 @@ import { promises as fsPromises } from "fs";
import { createRequire } from "module";
import * as path from "path";
import * as url from "url";
import { createLogger, defineConfig } from "vite";
import { defineConfig } from "vite";
import { ViteEjsPlugin } from "vite-plugin-ejs";
import eslint from "vite-plugin-eslint";
import { VitePWA } from "vite-plugin-pwa";
import InstanceRenderManager from "./src/utils/instanceRenderMgr";
import chalk from "chalk";
process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", {
timeZone: "America/Los_Angeles"
});
const getFormattedTimestamp = () =>
new Date().toLocaleTimeString("en-US", { hour12: true }).replace("AM", "a.m.").replace("PM", "p.m.");
/** This is a hack around react-virtualized, should be removed when switching to react-virtuoso */
const WRONG_CODE = `import { bpfrpt_proptype_WindowScroller } from "../WindowScroller.js";`;
function reactVirtualizedFix() {
@@ -37,11 +32,6 @@ function reactVirtualizedFix() {
}
};
}
/** End of hack */
export const logger = createLogger("info", {
allowClearScreen: false
});
export default defineConfig({
base: "/",
@@ -109,6 +99,7 @@ export default defineConfig({
reactVirtualizedFix(),
react(),
eslint()
// CompressionPlugin(), //Cloudfront already compresses assets, so not needed.
],
define: {
APP_VERSION: JSON.stringify(process.env.npm_package_version)
@@ -116,57 +107,7 @@ export default defineConfig({
server: {
host: true,
port: 3000,
open: true,
proxy: {
"/ws": {
target: "ws://localhost:4000",
rewriteWsOrigin: true,
secure: false,
ws: true
},
"/api": {
target: "http://localhost:4000",
changeOrigin: true,
secure: false,
ws: false,
rewrite: (path) => {
const replacedValue = path.replace(/^\/api/, "");
logger.info(
`${chalk.grey.bold(getFormattedTimestamp())} ${chalk.cyan.bold("[vite]")} ${chalk.green.bold("[API]")} ${chalk.blue(replacedValue)}`
);
return replacedValue;
}
}
},
https: {
key: await fsPromises.readFile("../certs/key.pem"),
cert: await fsPromises.readFile("../certs/cert.pem"),
allowHTTP1: false // Force HTTP/2
}
},
preview: {
port: 6000,
host: true,
open: true,
https: {
key: await fsPromises.readFile("../certs/key.pem"),
cert: await fsPromises.readFile("../certs/cert.pem"),
allowHTTP1: false // Force HTTP/2
},
proxy: {
"/ws": {
target: "ws://localhost:4000",
rewriteWsOrigin: true,
secure: false,
ws: true
},
"/api": {
target: "http://localhost:4000",
changeOrigin: true,
secure: false,
ws: false
}
}
open: true
},
build: {
rollupOptions: {
@@ -180,18 +121,7 @@ export default defineConfig({
}
},
optimizeDeps: {
include: [
"react",
"react-dom",
"antd",
"@apollo/client",
"@reduxjs/toolkit",
"axios",
"react-router-dom",
"dayjs",
"redux",
"react-redux"
],
include: ["react", "react-dom", "antd", "@apollo/client", "@reduxjs/toolkit", "axios"],
esbuildOptions: {
loader: {
".js": "jsx"

View File

@@ -1,4 +1,4 @@
Must set the environment variables using:
firebase functions:config:set auth.graphql_endpoint="https://db.dev.imex.online/v1/graphql"
firebase functions:config:set auth.graphql_endpoint="https://db.dev.bodyshop.app/v1/graphql"
auth.hasura_secret_admin_key="Dev-BodyShopApp!"

View File

@@ -4240,63 +4240,6 @@
- active:
_eq: true
event_triggers:
- name: job_modified
definition:
enable_manual: false
update:
columns:
- clm_no
- v_make_desc
- date_next_contact
- status
- employee_csr
- employee_prep
- clm_total
- suspended
- employee_body
- ro_number
- actual_in
- ownr_co_nm
- v_model_yr
- comment
- job_totals
- v_vin
- ownr_fn
- scheduled_completion
- special_coverage_policy
- v_color
- ca_gst_registrant
- scheduled_delivery
- actual_delivery
- actual_completion
- kanbanparent
- est_ct_fn
- employee_refinish
- ownr_ph1
- date_last_contacted
- alt_transport
- inproduction
- est_ct_ln
- production_vars
- category
- v_model_desc
- date_invoiced
- est_co_nm
- ownr_ln
retry_conf:
interval_sec: 10
num_retries: 0
timeout_sec: 60
webhook_from_env: HASURA_API_URL
headers:
- name: event-secret
value_from_env: EVENT_SECRET
request_transform:
method: POST
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/job/job-updated'
version: 2
- name: job_status_transition
definition:
enable_manual: true

1033
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,18 +19,17 @@
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\""
},
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.645.0",
"@aws-sdk/client-ses": "^3.645.0",
"@aws-sdk/credential-provider-node": "^3.645.0",
"@opensearch-project/opensearch": "^2.12.0",
"@socket.io/redis-adapter": "^8.3.0",
"aws4": "^1.13.2",
"axios": "^1.7.7",
"@aws-sdk/client-secrets-manager": "^3.629.0",
"@aws-sdk/client-ses": "^3.629.0",
"@aws-sdk/credential-provider-node": "^3.629.0",
"@opensearch-project/opensearch": "^2.11.0",
"aws4": "^1.13.1",
"axios": "^1.7.4",
"better-queue": "^3.8.12",
"bluebird": "^3.7.2",
"body-parser": "^1.20.3",
"body-parser": "^1.20.2",
"canvas": "^2.11.2",
"chart.js": "^4.4.4",
"chart.js": "^4.4.3",
"cloudinary": "^2.4.0",
"compression": "^1.7.4",
"cookie-parser": "^1.4.6",
@@ -38,8 +37,8 @@
"csrf": "^3.1.0",
"dinero.js": "^1.9.1",
"dotenv": "^16.4.5",
"express": "^4.20.0",
"firebase-admin": "^12.4.0",
"express": "^4.19.2",
"firebase-admin": "^12.3.1",
"graphql": "^16.9.0",
"graphql-request": "^6.1.0",
"graylog2": "^0.2.1",
@@ -50,16 +49,14 @@
"moment": "^2.30.1",
"moment-timezone": "^0.5.45",
"multer": "^1.4.5-lts.1",
"node-mailjet": "^6.0.6",
"node-mailjet": "^6.0.5",
"node-persist": "^4.0.3",
"nodemailer": "^6.9.15",
"phone": "^3.1.50",
"nodemailer": "^6.9.14",
"phone": "^3.1.49",
"recursive-diff": "^1.0.9",
"redis": "^4.7.0",
"rimraf": "^6.0.1",
"soap": "^1.1.3",
"soap": "^1.1.1",
"socket.io": "^4.7.5",
"socket.io-adapter": "^2.5.5",
"ssh2-sftp-client": "^10.0.3",
"twilio": "^4.23.0",
"uuid": "^10.0.0",

370
server.js
View File

@@ -1,3 +1,4 @@
// Import core modules
const express = require("express");
const cors = require("cors");
const bodyParser = require("body-parser");
@@ -6,307 +7,104 @@ const compression = require("compression");
const cookieParser = require("cookie-parser");
const http = require("http");
const { Server } = require("socket.io");
const { createClient } = require("redis");
const { createAdapter } = require("@socket.io/redis-adapter");
const logger = require("./server/utils/logger");
// Load environment variables
require("dotenv").config({
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
});
/**
* CORS Origin for Socket.IO
* @type {string[][]}
*/
const SOCKETIO_CORS_ORIGIN = [
"https://test.imex.online",
"https://www.test.imex.online",
"http://localhost:3000",
"https://imex.online",
"https://www.imex.online",
"https://romeonline.io",
"https://www.romeonline.io",
"https://beta.test.romeonline.io",
"https://www.beta.test.romeonline.io",
"https://beta.romeonline.io",
"https://www.beta.romeonline.io",
"https://beta.test.imex.online",
"https://www.beta.test.imex.online",
"https://beta.imex.online",
"https://www.beta.imex.online",
"https://www.test.promanager.web-est.com",
"https://test.promanager.web-est.com",
"https://www.promanager.web-est.com",
"https://www.promanager.web-est.com"
];
// Import custom utilities and handlers
const logger = require("./server/utils/logger");
/**
* Middleware for Express app
* @param app
*/
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(cors({ credentials: true, exposedHeaders: ["set-cookie"] }));
// Helper middleware
app.use((req, res, next) => {
req.logger = logger;
next();
});
};
/**
* Route groupings for Express app
* @param app
*/
const applyRoutes = (app) => {
app.use("/", require("./server/routes/miscellaneousRoutes"));
app.use("/notifications", require("./server/routes/notificationsRoutes"));
app.use("/render", require("./server/routes/renderRoutes"));
app.use("/mixdata", require("./server/routes/mixDataRoutes"));
app.use("/accounting", require("./server/routes/accountingRoutes"));
app.use("/qbo", require("./server/routes/qboRoutes"));
app.use("/media", require("./server/routes/mediaRoutes"));
app.use("/sms", require("./server/routes/smsRoutes"));
app.use("/job", require("./server/routes/jobRoutes"));
app.use("/scheduling", require("./server/routes/schedulingRoutes"));
app.use("/utils", require("./server/routes/utilRoutes"));
app.use("/data", require("./server/routes/dataRoutes"));
app.use("/adm", require("./server/routes/adminRoutes"));
app.use("/tech", require("./server/routes/techRoutes"));
app.use("/intellipay", require("./server/routes/intellipayRoutes"));
app.use("/cdk", require("./server/routes/cdkRoutes"));
app.use("/csi", require("./server/routes/csiRoutes"));
app.use("/payroll", require("./server/routes/payrollRoutes"));
// Default route for forbidden access
app.get("/", (req, res) => {
res.status(200).send("Access Forbidden.");
});
};
/**
* Apply Redis to the server
* @param server
* @param app
*/
const applySocketIO = async (server, app) => {
// Redis client setup for Pub/Sub and Key-Value Store
const pubClient = createClient({ url: process.env.REDIS_URL || "redis://localhost:6379" });
const subClient = pubClient.duplicate();
pubClient.on("error", (err) => logger.log(`Redis pubClient error: ${err}`, "ERROR", "redis"));
subClient.on("error", (err) => logger.log(`Redis subClient error: ${err}`, "ERROR", "redis"));
try {
await Promise.all([pubClient.connect(), subClient.connect()]);
logger.log(`[${process.env.NODE_ENV}] Connected to Redis`, "INFO", "redis", "api");
} catch (redisError) {
logger.log("Failed to connect to Redis", "ERROR", "redis", redisError);
// Express app and server setup
const app = express();
const port = process.env.PORT || 5000;
const server = http.createServer(app);
const io = new Server(server, {
path: "/ws",
cors: {
origin: [
"https://test.imex.online",
"https://www.test.imex.online",
"http://localhost:3000",
"https://imex.online",
"https://www.imex.online",
"https://romeonline.io", //Added in all RO and PM routes to simplyify setup.
"https://www.romeonline.io",
"https://beta.test.romeonline.io",
"https://www.beta.test.romeonline.io",
"https://beta.romeonline.io",
"https://www.beta.romeonline.io",
"https://beta.test.imex.online",
"https://www.beta.test.imex.online",
"https://beta.imex.online",
"https://www.beta.imex.online",
"https://www.test.promanager.web-est.com",
"https://test.promanager.web-est.com",
"https://www.promanager.web-est.com",
"https://www.promanager.web-est.com"
],
methods: ["GET", "POST"],
credentials: true,
exposedHeaders: ["set-cookie"]
}
});
exports.io = io;
process.on("SIGINT", async () => {
logger.log("Closing Redis connections...", "INFO", "redis", "api");
await Promise.all([pubClient.disconnect(), subClient.disconnect()]);
process.exit(0);
});
require("./server/web-sockets/web-socket");
const io = new Server(server, {
path: "/ws",
adapter: createAdapter(pubClient, subClient),
cors: {
origin: SOCKETIO_CORS_ORIGIN,
methods: ["GET", "POST"],
credentials: true,
exposedHeaders: ["set-cookie"]
}
});
// Middleware
app.use(compression());
app.use(cookieParser());
app.use(bodyParser.json({ limit: "50mb" }));
app.use(bodyParser.urlencoded({ limit: "50mb", extended: true }));
app.use(cors({ credentials: true, exposedHeaders: ["set-cookie"] }));
app.use((req, res, next) => {
req.pubClient = pubClient;
req.io = io;
next();
});
// Helper middleware
app.use((req, res, next) => {
req.logger = logger;
next();
});
Object.assign(module.exports, { io, pubClient });
// Route groupings
app.use("/", require("./server/routes/miscellaneousRoutes"));
app.use("/notifications", require("./server/routes/notificationsRoutes"));
app.use("/render", require("./server/routes/renderRoutes"));
app.use("/mixdata", require("./server/routes/mixDataRoutes"));
app.use("/accounting", require("./server/routes/accountingRoutes"));
app.use("/qbo", require("./server/routes/qboRoutes"));
app.use("/media", require("./server/routes/mediaRoutes"));
app.use("/sms", require("./server/routes/smsRoutes"));
app.use("/job", require("./server/routes/jobRoutes"));
app.use("/scheduling", require("./server/routes/schedulingRoutes"));
app.use("/utils", require("./server/routes/utilRoutes"));
app.use("/data", require("./server/routes/dataRoutes"));
app.use("/adm", require("./server/routes/adminRoutes"));
app.use("/tech", require("./server/routes/techRoutes"));
app.use("/intellipay", require("./server/routes/intellipayRoutes"));
app.use("/cdk", require("./server/routes/cdkRoutes"));
app.use("/csi", require("./server/routes/csiRoutes"));
app.use("/payroll", require("./server/routes/payrollRoutes"));
return { pubClient, io };
};
// Default route for forbidden access
app.get("/", (req, res) => {
res.status(200).send("Access Forbidden.");
});
/**
* Apply Redis helper functions
* @param pubClient
* @param app
*/
const applyRedisHelpers = (pubClient, app) => {
// Store session data in Redis
const setSessionData = async (socketId, key, value) => {
await pubClient.hSet(`socket:${socketId}`, key, JSON.stringify(value)); // Use Redis pubClient
};
// Retrieve session data from Redis
const getSessionData = async (socketId, key) => {
const data = await pubClient.hGet(`socket:${socketId}`, key);
return data ? JSON.parse(data) : null;
};
// Clear session data from Redis
const clearSessionData = async (socketId) => {
await pubClient.del(`socket:${socketId}`);
};
// Store multiple session data in Redis
const setMultipleSessionData = async (socketId, keyValues) => {
// keyValues is expected to be an object { key1: value1, key2: value2, ... }
const entries = Object.entries(keyValues).map(([key, value]) => [key, JSON.stringify(value)]);
await pubClient.hSet(`socket:${socketId}`, ...entries.flat());
};
// Retrieve multiple session data from Redis
const getMultipleSessionData = async (socketId, keys) => {
const data = await pubClient.hmGet(`socket:${socketId}`, keys);
// Redis returns an object with null values for missing keys, so we parse the non-null ones
return Object.fromEntries(keys.map((key, index) => [key, data[index] ? JSON.parse(data[index]) : null]));
};
const setMultipleFromArraySessionData = async (socketId, keyValueArray) => {
// Use Redis multi/pipeline to batch the commands
const multi = pubClient.multi();
keyValueArray.forEach(([key, value]) => {
multi.hSet(`socket:${socketId}`, key, JSON.stringify(value));
});
await multi.exec(); // Execute all queued commands
};
// Helper function to add an item to the end of the Redis list
const addItemToEndOfList = async (socketId, key, newItem) => {
try {
await pubClient.rPush(`socket:${socketId}:${key}`, JSON.stringify(newItem));
} catch (error) {
console.error(`Error adding item to the end of the list for socket ${socketId}:`, error);
}
};
// Helper function to add an item to the beginning of the Redis list
const addItemToBeginningOfList = async (socketId, key, newItem) => {
try {
await pubClient.lPush(`socket:${socketId}:${key}`, JSON.stringify(newItem));
} catch (error) {
console.error(`Error adding item to the beginning of the list for socket ${socketId}:`, error);
}
};
Object.assign(module.exports, {
setSessionData,
getSessionData,
clearSessionData,
setMultipleSessionData,
getMultipleSessionData,
setMultipleFromArraySessionData,
addItemToEndOfList,
addItemToBeginningOfList,
pubClient
});
app.use((req, res, next) => {
req.sessionUtils = {
setSessionData,
getSessionData,
clearSessionData,
setMultipleSessionData,
getMultipleSessionData,
setMultipleFromArraySessionData,
addItemToEndOfList,
addItemToBeginningOfList,
pubClient
};
next();
});
// // Demo to show how all the helper functions work
// const demoSessionData = async () => {
// const socketId = "testSocketId";
//
// // Store session data using setSessionData
// await exports.setSessionData(socketId, "field1", "Hello, Redis!");
//
// // Retrieve session data using getSessionData
// const field1Value = await exports.getSessionData(socketId, "field1");
// console.log("Retrieved single field value:", field1Value);
//
// // Store multiple session data using setMultipleSessionData
// await exports.setMultipleSessionData(socketId, { field2: "Second Value", field3: "Third Value" });
//
// // Retrieve multiple session data using getMultipleSessionData
// const multipleFields = await exports.getMultipleSessionData(socketId, ["field2", "field3"]);
// console.log("Retrieved multiple field values:", multipleFields);
//
// // Store multiple session data using setMultipleFromArraySessionData
// await exports.setMultipleFromArraySessionData(socketId, [
// ["field4", "Fourth Value"],
// ["field5", "Fifth Value"]
// ]);
//
// // Retrieve and log all fields
// const allFields = await exports.getMultipleSessionData(socketId, [
// "field1",
// "field2",
// "field3",
// "field4",
// "field5"
// ]);
// console.log("Retrieved all field values:", allFields);
//
// // Add item to the end of a Redis list
// await exports.addItemToEndOfList(socketId, "logEvents", { event: "Log Event 1", timestamp: new Date() });
// await exports.addItemToEndOfList(socketId, "logEvents", { event: "Log Event 2", timestamp: new Date() });
//
// // Add item to the beginning of a Redis list
// await exports.addItemToBeginningOfList(socketId, "logEvents", { event: "First Log Event", timestamp: new Date() });
//
// // Retrieve the entire list (using lRange)
// const logEvents = await pubClient.lRange(`socket:${socketId}:logEvents`, 0, -1);
// console.log("Log Events List:", logEvents.map(JSON.parse));
//
// // Clear session data
// await exports.clearSessionData(socketId);
// console.log("Session data cleared.");
// };
//
// if (process.env.NODE_ENV === "development") {
// demoSessionData();
// }
};
/**
* Main function to start the server
* @returns {Promise<void>}
*/
const main = async () => {
const app = express();
const port = process.env.PORT || 5000;
const server = http.createServer(app);
const { pubClient } = await applySocketIO(server, app);
applyRedisHelpers(pubClient, app);
require("./server/web-sockets/web-socket");
applyMiddleware(app);
applyRoutes(app);
try {
await server.listen(port);
logger.log(`[${process.env.NODE_ENV}] Server started on port ${port}`, "INFO", "api");
} catch (error) {
logger.log(`[${process.env.NODE_ENV}] Server failed to start on port ${port}`, "ERROR", "api", error);
}
await server.listen(port);
};
// Start server
main();
main()
.then(() => {
logger.log(`[${process.env.NODE_ENV || "DEVELOPMENT"}] Server started on port ${port}`, "INFO", "api");
})
.catch((error) => {
logger.log(
`[${process.env.NODE_ENV || "DEVELOPMENT"}] Server failed to start on port ${port}`,
"ERROR",
"api",
error
);
});

View File

@@ -12,92 +12,67 @@ const AxiosLib = require("axios").default;
const axios = AxiosLib.create();
const { PBS_ENDPOINTS, PBS_CREDENTIALS } = require("./pbs-constants");
const { CheckForErrors } = require("./pbs-job-export");
const { getSessionData, getMultipleSessionData, setMultipleSessionData } = require("../../../server");
const uuid = require("uuid").v4;
axios.interceptors.request.use((x) => {
const socket = x.socket;
axios.interceptors.request.use(
async (x) => {
const socket = x.socket;
const headers = {
...x.headers.common,
...x.headers[x.method],
...x.headers
};
const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${
x.url
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
console.log(printable);
const headers = {
...x.headers.common,
...x.headers[x.method],
...x.headers
};
CdkBase.createJsonEvent(socket, "TRACE", `Raw Request: ${printable}`, x.data);
const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${
x.url
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
return x;
});
console.log(printable);
axios.interceptors.response.use((x) => {
const socket = x.config.socket;
// Use await properly here for the async operation
await CdkBase.createJsonEvent(socket, "TRACE", `Raw Request: ${printable}`, x.data);
const printable = `${new Date()} | Response: ${x.status} | ${JSON.stringify(x.data)}`;
console.log(printable);
CdkBase.createJsonEvent(socket, "TRACE", `Raw Response: ${printable}`, x.data);
return x; // Return the modified request
},
(error) => {
return Promise.reject(error); // Proper error handling
}
);
axios.interceptors.response.use(
async (x) => {
const socket = x.config.socket;
const printable = `${new Date()} | Response: ${x.status} | ${JSON.stringify(x.data)}`;
console.log(printable);
// Use await properly here for the async operation
await CdkBase.createJsonEvent(socket, "TRACE", `Raw Response: ${printable}`, x.data);
return x; // Return the modified response
},
(error) => {
return Promise.reject(error); // Proper error handling
}
);
return x;
});
async function PbsCalculateAllocationsAp(socket, billids) {
try {
await CdkBase.createLogEvent(socket, "DEBUG", `Received request to calculate allocations for ${billids}`);
CdkBase.createLogEvent(socket, "DEBUG", `Received request to calculate allocations for ${billids}`);
const { bills, bodyshops } = await QueryBillData(socket, billids);
const bodyshop = bodyshops[0];
await setMultipleSessionData(socket.id, {
bills,
bodyshop
});
const txEnvelope = await getSessionData(socket.id, "txEnvelope");
socket.bodyshop = bodyshop;
socket.bills = bills;
//Each bill will enter it's own top level transaction.
const transactionlist = [];
if (bills.length === 0) {
await CdkBase.createLogEvent(
CdkBase.createLogEvent(
socket,
"ERROR",
`No bills found for export. Ensure they have not already been exported and try again.`
);
}
bills.forEach((bill) => {
//Keep the allocations at the bill level.
const transactionObject = {
SerialNumber: bodyshop.pbs_serialnumber,
SerialNumber: socket.bodyshop.pbs_serialnumber,
billid: bill.id,
Posting: {
Reference: bill.invoice_number,
JournalCode: socket.txEnvelope ? socket.txEnvelope.journal : null,
TransactionDate: moment().tz(socket.bodyshop.timezone).toISOString(), //"0001-01-01T00:00:00.0000000Z",
//Description: "Bulk AP posting.",
//AdditionalInfo: "String",
Reference: bill.invoice_number,
JournalCode: txEnvelope?.journal,
TransactionDate: moment().tz(bodyshop.timezone).toISOString(),
Source: "ImEX Online", // TODO: Resolve this for Rome Online.
Lines: [] // Will be populated with allocation data,
Source: "ImEX Online", //TODO:AIO Resolve this for rome online.
Lines: [] //socket.apAllocations,
}
};
@@ -142,13 +117,13 @@ async function PbsCalculateAllocationsAp(socket, billids) {
};
}
// Add the line amount.
//Add the line amount.
billHash[cc.name] = {
...billHash[cc.name],
Amount: billHash[cc.name].Amount.add(lineDinero)
};
// Does the line have taxes?
//Does the line have taxes?
if (bl.applicable_taxes.federal) {
billHash[bodyshop.md_responsibility_centers.taxes.federal_itc.name] = {
...billHash[bodyshop.md_responsibility_centers.taxes.federal_itc.name],
@@ -170,7 +145,7 @@ async function PbsCalculateAllocationsAp(socket, billids) {
let APAmount = Dinero();
Object.keys(billHash).map((key) => {
if (billHash[key].Amount.getAmount() !== 0) {
if (billHash[key].Amount.getAmount() > 0 || billHash[key].Amount.getAmount() < 0) {
transactionObject.Posting.Lines.push({
...billHash[key],
Amount: billHash[key].Amount.toFormat("0.00")
@@ -194,19 +169,19 @@ async function PbsCalculateAllocationsAp(socket, billids) {
return transactionlist;
} catch (error) {
await CdkBase.createLogEvent(socket, "ERROR", `Error encountered in PbsCalculateAllocationsAp. ${error}`);
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in PbsCalculateAllocationsAp. ${error}`);
}
}
exports.PbsCalculateAllocationsAp = PbsCalculateAllocationsAp;
async function QueryBillData(socket, billids) {
await CdkBase.createLogEvent(socket, "DEBUG", `Querying bill data for id(s) ${billids}`);
CdkBase.createLogEvent(socket, "DEBUG", `Querying bill data for id(s) ${billids}`);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
const result = await client
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.GET_PBS_AP_ALLOCATIONS, { billids: billids });
await CdkBase.createLogEvent(socket, "TRACE", `Bill data query result ${JSON.stringify(result, null, 2)}`);
CdkBase.createLogEvent(socket, "TRACE", `Bill data query result ${JSON.stringify(result, null, 2)}`);
return result;
}
@@ -221,49 +196,40 @@ function getCostAccount(billline, respcenters) {
}
exports.PbsExportAp = async function (socket, { billids, txEnvelope }) {
await CdkBase.createLogEvent(socket, "DEBUG", `Exporting selected AP.`);
CdkBase.createLogEvent(socket, "DEBUG", `Exporting selected AP.`);
const apAllocations = await PbsCalculateAllocationsAp(socket, billids);
await setMultipleSessionData(socket.id, {
apAllocations,
txEnvelope
});
for (const allocation of apAllocations) {
//apAllocations has the same shap as the lines key for the accounting posting to PBS.
socket.apAllocations = await PbsCalculateAllocationsAp(socket, billids);
socket.txEnvelope = txEnvelope;
for (const allocation of socket.apAllocations) {
const { billid, ...restAllocation } = allocation;
const { data: AccountPostingChange } = await axios.post(PBS_ENDPOINTS.AccountingPostingChange, restAllocation, {
auth: PBS_CREDENTIALS,
socket
});
CheckForErrors(socket, AccountPostingChange).catch((err) =>
console.error(`Error running CheckingForErrors in pbs-ap-allocations`)
);
CheckForErrors(socket, AccountPostingChange);
if (AccountPostingChange.WasSuccessful) {
await CdkBase.createLogEvent(socket, "DEBUG", `Marking bill as exported.`);
CdkBase.createLogEvent(socket, "DEBUG", `Marking bill as exported.`);
await MarkApExported(socket, [billid]);
socket.emit("ap-export-success", billid);
} else {
await CdkBase.createLogEvent(socket, "ERROR", `Export was not successful.`);
CdkBase.createLogEvent(socket, "ERROR", `Export was not succesful.`);
socket.emit("ap-export-failure", {
billid,
error: AccountPostingChange.Message
});
}
}
socket.emit("ap-export-complete");
};
async function MarkApExported(socket, billids) {
const { bills, bodyshop } = await getMultipleSessionData(socket.id, ["bills", "bodyshop"]);
await CdkBase.createLogEvent(socket, "DEBUG", `Marking bills as exported for id ${billids}`);
CdkBase.createLogEvent(socket, "DEBUG", `Marking bills as exported for id ${billids}`);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
return await client
const result = await client
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.MARK_BILLS_EXPORTED, {
billids,
@@ -271,11 +237,13 @@ async function MarkApExported(socket, billids) {
exported: true,
exported_at: new Date()
},
logs: bills.map((bill) => ({
bodyshopid: bodyshop.id,
logs: socket.bills.map((bill) => ({
bodyshopid: socket.bodyshop.id,
billid: bill.id,
successful: true,
useremail: socket.user.email
}))
});
return result;
}

View File

@@ -12,169 +12,136 @@ const CalculateAllocations = require("../../cdk/cdk-calculate-allocations").defa
const CdkBase = require("../../web-sockets/web-socket");
const moment = require("moment-timezone");
const Dinero = require("dinero.js");
const { setSessionData, getSessionData, getMultipleSessionData, setMultipleSessionData } = require("../../../server");
const InstanceManager = require("../../utils/instanceMgr").default;
const axios = AxiosLib.create();
axios.interceptors.request.use(
async (x) => {
const socket = x.socket;
axios.interceptors.request.use((x) => {
const socket = x.socket;
const headers = {
...x.headers.common,
...x.headers[x.method],
...x.headers
};
const headers = {
...x.headers.common,
...x.headers[x.method],
...x.headers
};
const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${
x.url
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
console.log(printable);
const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${
x.url
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
CdkBase.createJsonEvent(socket, "TRACE", `Raw Request: ${printable}`, x.data);
console.log(printable);
return x;
});
await CdkBase.createJsonEvent(socket, "TRACE", `Raw Request: ${printable}`, x.data);
axios.interceptors.response.use((x) => {
const socket = x.config.socket;
return x; // Make sure to return the request object
},
(error) => {
return Promise.reject(error);
}
);
const printable = `${new Date()} | Response: ${x.status} | ${JSON.stringify(x.data)}`;
console.log(printable);
CdkBase.createJsonEvent(socket, "TRACE", `Raw Response: ${printable}`, x.data);
axios.interceptors.response.use(
async (x) => {
const socket = x.config.socket;
const printable = `${new Date()} | Response: ${x.status} | ${JSON.stringify(x.data)}`;
console.log(printable);
await CdkBase.createJsonEvent(socket, "TRACE", `Raw Response: ${printable}`, x.data);
return x; // Make sure to return the response object
},
(error) => {
return Promise.reject(error);
}
);
return x;
});
exports.default = async function (socket, { txEnvelope, jobid }) {
socket.logEvents = [];
socket.recordid = jobid;
socket.txEnvelope = txEnvelope;
try {
await setMultipleSessionData(socket.id, {
recordid: jobid,
txEnvelope
});
await CdkBase.createLogEvent(socket, "DEBUG", `Received Job export request for id ${jobid}`);
CdkBase.createLogEvent(socket, "DEBUG", `Received Job export request for id ${jobid}`);
const JobData = await QueryJobData(socket, jobid);
await setSessionData(socket.id, "JobData", JobData);
await CdkBase.createLogEvent(socket, "DEBUG", `Querying the DMS for the Vehicle Record.`);
// Query for the Vehicle record to get the associated customer
const DmsVeh = await QueryVehicleFromDms(socket);
await setSessionData(socket.id, "DmsVeh", DmsVeh);
let DMSVehCustomer;
socket.JobData = JobData;
CdkBase.createLogEvent(socket, "DEBUG", `Querying the DMS for the Vehicle Record.`);
//Query for the Vehicle record to get the associated customer.
socket.DmsVeh = await QueryVehicleFromDms(socket);
//Todo: Need to validate the lines and methods below.
if (DmsVeh?.CustomerRef) {
// Get the associated customer from the Vehicle Record
DMSVehCustomer = await QueryCustomerBycodeFromDms(socket, DmsVeh.CustomerRef);
await setSessionData(socket.id, "DMSVehCustomer", DMSVehCustomer);
if (socket.DmsVeh && socket.DmsVeh.CustomerRef) {
//Get the associated customer from the Vehicle Record.
socket.DMSVehCustomer = await QueryCustomerBycodeFromDms(socket, socket.DmsVeh.CustomerRef);
}
const DMSCustList = await QueryCustomersFromDms(socket);
await setSessionData(socket.id, "DMSCustList", DMSCustList);
socket.DMSCustList = await QueryCustomersFromDms(socket);
socket.emit("pbs-select-customer", [
...(DMSVehCustomer ? [{ ...DMSVehCustomer, vinOwner: true }] : []),
...DMSCustList
...(socket.DMSVehCustomer ? [{ ...socket.DMSVehCustomer, vinOwner: true }] : []),
...socket.DMSCustList
]);
} catch (error) {
await CdkBase.createLogEvent(socket, "ERROR", `Error encountered in PbsJobExport. ${error}`);
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in PbsJobExport. ${error}`);
}
};
exports.PbsSelectedCustomer = async function PbsSelectedCustomer(socket, selectedCustomerId) {
try {
const JobData = await getSessionData(socket.id, "JobData");
if (socket.JobData.bodyshop.pbs_configuration.disablecontactvehicle === false) {
CdkBase.createLogEvent(socket, "DEBUG", `User selected customer ${selectedCustomerId || "NEW"}`);
if (JobData.bodyshop.pbs_configuration.disablecontactvehicle === false) {
await CdkBase.createLogEvent(socket, "DEBUG", `User selected customer ${selectedCustomerId || "NEW"}`);
// Upsert the contact information as per Wafaa's Email
await CdkBase.createLogEvent(
//Upsert the contact information as per Wafaa's Email.
CdkBase.createLogEvent(
socket,
"DEBUG",
`Upserting contact information to DMS for ${
JobData.ownr_fn || ""
} ${JobData.ownr_ln || ""} ${JobData.ownr_co_nm || ""}`
socket.JobData.ownr_fn || ""
} ${socket.JobData.ownr_ln || ""} ${socket.JobData.ownr_co_nm || ""}`
);
const ownerRef = await UpsertContactData(socket, selectedCustomerId);
await CdkBase.createLogEvent(socket, "DEBUG", `Upserting vehicle information to DMS for ${JobData.v_vin}`);
CdkBase.createLogEvent(socket, "DEBUG", `Upserting vehicle information to DMS for ${socket.JobData.v_vin}`);
await UpsertVehicleData(socket, ownerRef.ReferenceId);
} else {
await CdkBase.createLogEvent(
CdkBase.createLogEvent(
socket,
"DEBUG",
`Contact and Vehicle updates disabled. Skipping to accounting data insert.`
);
}
await CdkBase.createLogEvent(socket, "DEBUG", `Inserting account data.`);
await CdkBase.createLogEvent(socket, "DEBUG", `Inserting accounting posting data..`);
CdkBase.createLogEvent(socket, "DEBUG", `Inserting account data.`);
CdkBase.createLogEvent(socket, "DEBUG", `Inserting accounting posting data..`);
const insertResponse = await InsertAccountPostingData(socket);
// TODO: Insert Clear session
if (insertResponse.WasSuccessful) {
await CdkBase.createLogEvent(socket, "DEBUG", `Marking job as exported.`);
await MarkJobExported(socket, JobData.id);
CdkBase.createLogEvent(socket, "DEBUG", `Marking job as exported.`);
await MarkJobExported(socket, socket.JobData.id);
socket.emit("export-success", JobData.id);
socket.emit("export-success", socket.JobData.id);
} else {
await CdkBase.createLogEvent(socket, "ERROR", `Export was not successful.`);
CdkBase.createLogEvent(socket, "ERROR", `Export was not succesful.`);
}
} catch (error) {
await CdkBase.createLogEvent(socket, "ERROR", `Error encountered in PbsSelectedCustomer. ${error}`);
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in CdkSelectedCustomer. ${error}`);
await InsertFailedExportLog(socket, error);
}
};
async function CheckForErrors(socket, response) {
if (response.WasSuccessful === undefined || response.WasSuccessful === true) {
await CdkBase.createLogEvent(socket, "DEBUG", `Successful response from DMS. ${response.Message || ""}`);
CdkBase.createLogEvent(socket, "DEBUG", `Successful response from DMS. ${response.Message || ""}`);
} else {
await CdkBase.createLogEvent(socket, "ERROR", `Error received from DMS: ${response.Message}`);
await CdkBase.createLogEvent(socket, "TRACE", `Error received from DMS: ${JSON.stringify(response)}`);
CdkBase.createLogEvent(socket, "ERROR", `Error received from DMS: ${response.Message}`);
CdkBase.createLogEvent(socket, "TRACE", `Error received from DMS: ${JSON.stringify(response)}`);
}
}
exports.CheckForErrors = CheckForErrors;
async function QueryJobData(socket, jobid) {
await CdkBase.createLogEvent(socket, "DEBUG", `Querying job data for id ${jobid}`);
CdkBase.createLogEvent(socket, "DEBUG", `Querying job data for id ${jobid}`);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
const result = await client
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.QUERY_JOBS_FOR_PBS_EXPORT, { id: jobid });
await CdkBase.createLogEvent(socket, "TRACE", `Job data query result ${JSON.stringify(result, null, 2)}`);
CdkBase.createLogEvent(socket, "TRACE", `Job data query result ${JSON.stringify(result, null, 2)}`);
return result.jobs_by_pk;
}
async function QueryVehicleFromDms(socket) {
try {
const JobData = await getSessionData(socket.id, "JobData");
if (!JobData.v_vin) return null;
if (!socket.JobData.v_vin) return null;
const { data: VehicleGetResponse, request } = await axios.post(
PBS_ENDPOINTS.VehicleGet,
{
SerialNumber: JobData.bodyshop.pbs_serialnumber,
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
// VehicleId: "00000000000000000000000000000000",
// Year: "String",
// Make: "String",
@@ -182,7 +149,7 @@ async function QueryVehicleFromDms(socket) {
// Trim: "String",
// ModelNumber: "String",
// StockNumber: "String",
VIN: JobData.v_vin
VIN: socket.JobData.v_vin
// LicenseNumber: "String",
// Lot: "String",
// Status: "String",
@@ -199,31 +166,26 @@ async function QueryVehicleFromDms(socket) {
{ auth: PBS_CREDENTIALS, socket }
);
await CheckForErrors(socket, VehicleGetResponse);
CheckForErrors(socket, VehicleGetResponse);
return VehicleGetResponse;
} catch (error) {
await CdkBase.createLogEvent(socket, "ERROR", `Error in QueryVehicleFromDms - ${error}`);
CdkBase.createLogEvent(socket, "ERROR", `Error in QueryVehicleFromDms - ${error}`);
throw new Error(error);
}
}
async function QueryCustomersFromDms(socket) {
try {
// Retrieve JobData from session storage
const JobData = await getSessionData(socket.id, "JobData");
// Make an API call to PBS to query customer details
const { data: CustomerGetResponse } = await axios.post(
PBS_ENDPOINTS.ContactGet,
{
SerialNumber: JobData.bodyshop.pbs_serialnumber,
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
//ContactId: "00000000000000000000000000000000",
// ContactCode: JobData.owner.accountingid,
FirstName: JobData.ownr_fn,
LastName: JobData.ownr_co_nm ? JobData.ownr_co_nm : JobData.ownr_ln,
PhoneNumber: JobData.ownr_ph1,
EmailAddress: JobData.ownr_ea
// ContactCode: socket.JobData.owner.accountingid,
FirstName: socket.JobData.ownr_fn,
LastName: socket.JobData.ownr_co_nm ? socket.JobData.ownr_co_nm : socket.JobData.ownr_ln,
PhoneNumber: socket.JobData.ownr_ph1,
EmailAddress: socket.JobData.ownr_ea
// ModifiedSince: "0001-01-01T00:00:00.0000000Z",
// ModifiedUntil: "0001-01-01T00:00:00.0000000Z",
// ContactIdList: ["00000000000000000000000000000000"],
@@ -235,36 +197,27 @@ async function QueryCustomersFromDms(socket) {
},
{ auth: PBS_CREDENTIALS, socket }
);
// Check for errors in the PBS response
await CheckForErrors(socket, CustomerGetResponse);
// Return the list of contacts from the PBS response
CheckForErrors(socket, CustomerGetResponse);
return CustomerGetResponse && CustomerGetResponse.Contacts;
} catch (error) {
// Log any errors encountered during the API call
await CdkBase.createLogEvent(socket, "ERROR", `Error in QueryCustomersFromDms - ${error}`);
CdkBase.createLogEvent(socket, "ERROR", `Error in QueryCustomersFromDms - ${error}`);
throw new Error(error);
}
}
async function QueryCustomerBycodeFromDms(socket, CustomerRef) {
try {
// Retrieve JobData from session storage
const JobData = await getSessionData(socket.id, "JobData");
// Make an API call to PBS to query customer by ContactId
const { data: CustomerGetResponse } = await axios.post(
PBS_ENDPOINTS.ContactGet,
{
SerialNumber: JobData.bodyshop.pbs_serialnumber,
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
ContactId: CustomerRef
//ContactCode: JobData.owner.accountingid,
//FirstName: JobData.ownr_co_nm
// ? JobData.ownr_co_nm
// : JobData.ownr_fn,
//LastName: JobData.ownr_ln,
//PhoneNumber: JobData.ownr_ph1,
//ContactCode: socket.JobData.owner.accountingid,
//FirstName: socket.JobData.ownr_co_nm
// ? socket.JobData.ownr_co_nm
// : socket.JobData.ownr_fn,
//LastName: socket.JobData.ownr_ln,
//PhoneNumber: socket.JobData.ownr_ph1,
// EmailAddress: "String",
// ModifiedSince: "0001-01-01T00:00:00.0000000Z",
// ModifiedUntil: "0001-01-01T00:00:00.0000000Z",
@@ -277,42 +230,33 @@ async function QueryCustomerBycodeFromDms(socket, CustomerRef) {
},
{ auth: PBS_CREDENTIALS, socket }
);
// Check for errors in the PBS response
await CheckForErrors(socket, CustomerGetResponse);
// Return the list of contacts from the PBS response
CheckForErrors(socket, CustomerGetResponse);
return CustomerGetResponse && CustomerGetResponse.Contacts;
} catch (error) {
// Log any errors encountered during the API call
await CdkBase.createLogEvent(socket, "ERROR", `Error in QueryCustomerBycodeFromDms - ${error}`);
CdkBase.createLogEvent(socket, "ERROR", `Error in QueryCustomersFromDms - ${error}`);
throw new Error(error);
}
}
async function UpsertContactData(socket, selectedCustomerId) {
try {
// Retrieve JobData from session storage
const JobData = await getSessionData(socket.id, "JobData");
// Make an API call to PBS to upsert contact data
const { data: ContactChangeResponse } = await axios.post(
PBS_ENDPOINTS.ContactChange,
{
ContactInfo: {
// Id: JobData.owner.id,
// Id: socket.JobData.owner.id,
...(selectedCustomerId ? { ContactId: selectedCustomerId } : {}),
SerialNumber: JobData.bodyshop.pbs_serialnumber,
Code: JobData.owner.accountingid,
...(JobData.ownr_co_nm
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
Code: socket.JobData.owner.accountingid,
...(socket.JobData.ownr_co_nm
? {
//LastName: JobData.ownr_ln,
FirstName: JobData.ownr_co_nm,
//LastName: socket.JobData.ownr_ln,
FirstName: socket.JobData.ownr_co_nm,
IsBusiness: true
}
: {
LastName: JobData.ownr_ln,
FirstName: JobData.ownr_fn,
LastName: socket.JobData.ownr_ln,
FirstName: socket.JobData.ownr_fn,
IsBusiness: false
}),
@@ -322,20 +266,20 @@ async function UpsertContactData(socket, selectedCustomerId) {
IsInactive: false,
//ApartmentNumber: "String",
Address: JobData.ownr_addr1,
City: JobData.ownr_city,
//County: JobData.ownr_addr1,
State: JobData.ownr_st,
ZipCode: JobData.ownr_zip,
Address: socket.JobData.ownr_addr1,
City: socket.JobData.ownr_city,
//County: socket.JobData.ownr_addr1,
State: socket.JobData.ownr_st,
ZipCode: socket.JobData.ownr_zip,
//BusinessPhone: "String",
//BusinessPhoneExt: "String",
HomePhone: JobData.ownr_ph2,
CellPhone: JobData.ownr_ph1,
HomePhone: socket.JobData.ownr_ph2,
CellPhone: socket.JobData.ownr_ph1,
//BusinessPhoneRawReverse: "String",
//HomePhoneRawReverse: "String",
//CellPhoneRawReverse: "String",
//FaxNumber: "String",
EmailAddress: JobData.ownr_ea
EmailAddress: socket.JobData.ownr_ea
//Notes: "String",
//CriticalMemo: "String",
//BirthDate: "0001-01-01T00:00:00.0000000Z",
@@ -368,43 +312,39 @@ async function UpsertContactData(socket, selectedCustomerId) {
},
{ auth: PBS_CREDENTIALS, socket }
);
await CheckForErrors(socket, ContactChangeResponse);
CheckForErrors(socket, ContactChangeResponse);
return ContactChangeResponse;
} catch (error) {
await CdkBase.createLogEvent(socket, "ERROR", `Error in UpsertContactData - ${error}`);
CdkBase.createLogEvent(socket, "ERROR", `Error in UpsertContactData - ${error}`);
throw new Error(error);
}
}
async function UpsertVehicleData(socket, ownerRef) {
try {
const JobData = await getSessionData(socket.id, "JobData");
const { data: VehicleChangeResponse } = await axios.post(
PBS_ENDPOINTS.VehicleChange,
{
VehicleInfo: {
//Id: "string/00000000-0000-0000-0000-000000000000",
//VehicleId: "00000000000000000000000000000000",
SerialNumber: JobData.bodyshop.pbs_serialnumber,
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
//StockNumber: "String",
VIN: JobData.v_vin,
LicenseNumber: JobData.plate_no,
VIN: socket.JobData.v_vin,
LicenseNumber: socket.JobData.plate_no,
//FleetNumber: "String",
//Status: "String",
OwnerRef: ownerRef, // "00000000000000000000000000000000",
ModelNumber: JobData.vehicle && JobData.vehicle.v_makecode,
Make: JobData.v_make_desc,
Model: JobData.v_model_desc,
Trim: JobData.vehicle && JobData.vehicle.v_trimcode,
ModelNumber: socket.JobData.vehicle && socket.JobData.vehicle.v_makecode,
Make: socket.JobData.v_make_desc,
Model: socket.JobData.v_model_desc,
Trim: socket.JobData.vehicle && socket.JobData.vehicle.v_trimcode,
//VehicleType: "String",
Year: JobData.v_model_yr,
Odometer: JobData.kmout,
Year: socket.JobData.v_model_yr,
Odometer: socket.JobData.kmout,
ExteriorColor: {
Code: JobData.v_color,
Description: JobData.v_color
Code: socket.JobData.v_color,
Description: socket.JobData.v_color
}
// InteriorColor: { Code: "String", Description: "String" },
//Engine: "String",
@@ -525,112 +465,100 @@ async function UpsertVehicleData(socket, ownerRef) {
},
{ auth: PBS_CREDENTIALS, socket }
);
await CheckForErrors(socket, VehicleChangeResponse);
CheckForErrors(socket, VehicleChangeResponse);
return VehicleChangeResponse;
} catch (error) {
await CdkBase.createLogEvent(socket, "ERROR", `Error in UpsertVehicleData - ${error}`);
CdkBase.createLogEvent(socket, "ERROR", `Error in UpsertVehicleData - ${error}`);
throw new Error(error);
}
}
async function InsertAccountPostingData(socket) {
try {
const { JobData, txEnvelope } = await getMultipleSessionData(socket.id, ["JobData", "txEnvelope"]);
const allocations = await CalculateAllocations(socket, JobData.id);
const allocations = await CalculateAllocations(socket, socket.JobData.id);
const wips = [];
allocations.forEach((alloc) => {
// Add the sale item from each allocation if the amount is greater than 0 and not a tax
//Add the sale item from each allocation.
if (alloc.sale.getAmount() > 0 && !alloc.tax) {
const item = {
Account: alloc.profitCenter.dms_acctnumber,
ControlNumber: JobData.ro_number,
ControlNumber: socket.JobData.ro_number,
Amount: alloc.sale.multiply(-1).toFormat("0.00"),
// Comment: "String",
// AdditionalInfo: "String",
InvoiceNumber: JobData.ro_number,
InvoiceDate: moment(JobData.date_invoiced).tz(JobData.bodyshop.timezone).toISOString()
//Comment: "String",
//AdditionalInfo: "String",
InvoiceNumber: socket.JobData.ro_number,
InvoiceDate: moment(socket.JobData.date_invoiced).tz(socket.JobData.bodyshop.timezone).toISOString()
};
wips.push(item);
}
// Add the cost item if the cost amount is greater than 0 and not a tax
//Add the cost Item.
if (alloc.cost.getAmount() > 0 && !alloc.tax) {
const item = {
Account: alloc.costCenter.dms_acctnumber,
ControlNumber: JobData.ro_number,
ControlNumber: socket.JobData.ro_number,
Amount: alloc.cost.toFormat("0.00"),
// Comment: "String",
// AdditionalInfo: "String",
InvoiceNumber: JobData.ro_number,
InvoiceDate: moment(JobData.date_invoiced).tz(JobData.bodyshop.timezone).toISOString()
//Comment: "String",
//AdditionalInfo: "String",
InvoiceNumber: socket.JobData.ro_number,
InvoiceDate: moment(socket.JobData.date_invoiced).tz(socket.JobData.bodyshop.timezone).toISOString()
};
wips.push(item);
const itemWip = {
Account: alloc.costCenter.dms_wip_acctnumber,
ControlNumber: JobData.ro_number,
ControlNumber: socket.JobData.ro_number,
Amount: alloc.cost.multiply(-1).toFormat("0.00"),
// Comment: "String",
// AdditionalInfo: "String",
InvoiceNumber: JobData.ro_number,
InvoiceDate: moment(JobData.date_invoiced).tz(JobData.bodyshop.timezone).toISOString()
//Comment: "String",
//AdditionalInfo: "String",
InvoiceNumber: socket.JobData.ro_number,
InvoiceDate: moment(socket.JobData.date_invoiced).tz(socket.JobData.bodyshop.timezone).toISOString()
};
wips.push(itemWip);
// Add to the WIP account.
//Add to the WIP account.
}
// Add tax-related entries if applicable
if (alloc.tax) {
if (alloc.sale.getAmount() > 0) {
const item2 = {
Account: alloc.profitCenter.dms_acctnumber,
ControlNumber: JobData.ro_number,
ControlNumber: socket.JobData.ro_number,
Amount: alloc.sale.multiply(-1).toFormat("0.00"),
// Comment: "String",
// AdditionalInfo: "String",
InvoiceNumber: JobData.ro_number,
InvoiceDate: moment(JobData.date_invoiced).tz(JobData.bodyshop.timezone).toISOString()
//Comment: "String",
//AdditionalInfo: "String",
InvoiceNumber: socket.JobData.ro_number,
InvoiceDate: moment(socket.JobData.date_invoiced).tz(socket.JobData.bodyshop.timezone).toISOString()
};
wips.push(item2);
}
}
});
// Add payer information
txEnvelope.payers.forEach((payer) => {
socket.txEnvelope.payers.forEach((payer) => {
const item = {
Account: payer.dms_acctnumber,
ControlNumber: payer.controlnumber,
Amount: Dinero({ amount: Math.round(payer.amount * 100) }).toFormat("0.0"),
// Comment: "String",
// AdditionalInfo: "String",
InvoiceNumber: JobData.ro_number,
InvoiceDate: moment(JobData.date_invoiced).tz(JobData.bodyshop.timezone).toISOString()
//Comment: "String",
//AdditionalInfo: "String",
InvoiceNumber: socket.JobData.ro_number,
InvoiceDate: moment(socket.JobData.date_invoiced).tz(socket.JobData.bodyshop.timezone).toISOString()
};
wips.push(item);
});
await setSessionData(socket.id, "transWips", wips);
socket.transWips = wips;
const { data: AccountPostingChange } = await axios.post(
PBS_ENDPOINTS.AccountingPostingChange,
{
SerialNumber: JobData.bodyshop.pbs_serialnumber,
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
Posting: {
Reference: JobData.ro_number,
JournalCode: txEnvelope.journal,
TransactionDate: moment(JobData.date_invoiced).tz(JobData.bodyshop.timezone).toISOString(), // "0001-01-01T00:00:00.0000000Z",
Description: txEnvelope.story,
// AdditionalInfo: "String",
Reference: socket.JobData.ro_number,
JournalCode: socket.txEnvelope.journal,
TransactionDate: moment(socket.JobData.date_invoiced).tz(socket.JobData.bodyshop.timezone).toISOString(), //"0001-01-01T00:00:00.0000000Z",
Description: socket.txEnvelope.story,
//AdditionalInfo: "String",
Source: InstanceManager({ imex: "ImEX Online", rome: "Rome Online" }),
Lines: wips
}
@@ -638,56 +566,54 @@ async function InsertAccountPostingData(socket) {
{ auth: PBS_CREDENTIALS, socket }
);
await CheckForErrors(socket, AccountPostingChange);
CheckForErrors(socket, AccountPostingChange);
return AccountPostingChange;
} catch (error) {
await CdkBase.createLogEvent(socket, "ERROR", `Error in InsertAccountPostingData - ${error}`);
CdkBase.createLogEvent(socket, "ERROR", `Error in InsertAccountPostingData - ${error}`);
throw new Error(error);
}
}
async function MarkJobExported(socket, jobid) {
await CdkBase.createLogEvent(socket, "DEBUG", `Marking job as exported for id ${jobid}`);
CdkBase.createLogEvent(socket, "DEBUG", `Marking job as exported for id ${jobid}`);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
const { JobData, transWips } = await getMultipleSessionData(socket.id, ["JobData", "transWips"]);
return await client
const result = await client
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.MARK_JOB_EXPORTED, {
jobId: jobid,
job: {
status: JobData.bodyshop.md_ro_statuses.default_exported || "Exported*",
status: socket.JobData.bodyshop.md_ro_statuses.default_exported || "Exported*",
date_exported: new Date()
},
log: {
bodyshopid: JobData.bodyshop.id,
bodyshopid: socket.JobData.bodyshop.id,
jobid: jobid,
successful: true,
useremail: socket.user.email,
metadata: transWips
metadata: socket.transWips
},
bill: {
exported: true,
exported_at: new Date()
}
});
return result;
}
async function InsertFailedExportLog(socket, error) {
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
const JobData = await getSessionData(socket.id, "JobData");
return await client
const result = await client
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.INSERT_EXPORT_LOG, {
log: {
bodyshopid: JobData.bodyshop.id,
jobid: JobData.id,
bodyshopid: socket.JobData.bodyshop.id,
jobid: socket.JobData.id,
successful: false,
message: [error],
useremail: socket.user.email
}
});
return result;
}

View File

@@ -18,12 +18,12 @@ const { DiscountNotAlreadyCounted } = InstanceManager({
exports.defaultRoute = async function (req, res) {
try {
await CdkBase.createLogEvent(req, "DEBUG", `Received request to calculate allocations for ${req.body.jobid}`);
CdkBase.createLogEvent(req, "DEBUG", `Received request to calculate allocations for ${req.body.jobid}`);
const jobData = await QueryJobData(req, req.BearerToken, req.body.jobid);
return res.status(200).json({ data: await calculateAllocations(req, jobData) });
return res.status(200).json({ data: calculateAllocations(req, jobData) });
} catch (error) {
console.log(error);
await CdkBase.createLogEvent(req, "ERROR", `Error encountered in CdkCalculateAllocations. ${error}`);
CdkBase.createLogEvent(req, "ERROR", `Error encountered in CdkCalculateAllocations. ${error}`);
res.status(500).json({ error: `Error encountered in CdkCalculateAllocations. ${error}` });
}
};
@@ -31,22 +31,22 @@ exports.defaultRoute = async function (req, res) {
exports.default = async function (socket, jobid) {
try {
const jobData = await QueryJobData(socket, "Bearer " + socket.handshake.auth.token, jobid);
return await calculateAllocations(socket, jobData);
return calculateAllocations(socket, jobData);
} catch (error) {
console.log(error);
await CdkBase.createLogEvent(socket, "ERROR", `Error encountered in CdkCalculateAllocations. ${error}`);
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in CdkCalculateAllocations. ${error}`);
}
};
async function QueryJobData(connectionData, token, jobid) {
await CdkBase.createLogEvent(connectionData, "DEBUG", `Querying job data for id ${jobid}`);
CdkBase.createLogEvent(connectionData, "DEBUG", `Querying job data for id ${jobid}`);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
const result = await client.setHeaders({ Authorization: token }).request(queries.GET_CDK_ALLOCATIONS, { id: jobid });
await CdkBase.createLogEvent(connectionData, "TRACE", `Job data query result ${JSON.stringify(result, null, 2)}`);
CdkBase.createLogEvent(connectionData, "TRACE", `Job data query result ${JSON.stringify(result, null, 2)}`);
return result.jobs_by_pk;
}
async function calculateAllocations(connectionData, job) {
function calculateAllocations(connectionData, job) {
const { bodyshop } = job;
const taxAllocations = InstanceManager({
@@ -171,7 +171,7 @@ async function calculateAllocations(connectionData, job) {
const selectedDmsAllocationConfig = bodyshop.md_responsibility_centers.dms_defaults.find(
(d) => d.name === job.dms_allocation
);
await CdkBase.createLogEvent(
CdkBase.createLogEvent(
connectionData,
"DEBUG",
`Using DMS Allocation ${selectedDmsAllocationConfig && selectedDmsAllocationConfig.name} for cost export.`
@@ -361,10 +361,9 @@ async function calculateAllocations(connectionData, job) {
}
if (InstanceManager({ rome: true })) {
//profile level adjustments for parts
for (const key of Object.keys(job.job_totals.parts.adjustments)) {
Object.keys(job.job_totals.parts.adjustments).forEach((key) => {
const accountName = selectedDmsAllocationConfig.profits[key];
const otherAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === accountName);
if (otherAccount) {
if (!profitCenterHash[accountName]) profitCenterHash[accountName] = Dinero();
@@ -372,41 +371,31 @@ async function calculateAllocations(connectionData, job) {
Dinero(job.job_totals.parts.adjustments[key])
);
} else {
// Use await correctly here
await CdkBase.createLogEvent(
CdkBase.createLogEvent(
connectionData,
"ERROR",
`Error encountered in CdkCalculateAllocations. Unable to find adjustment account for key ${key}.`
`Error encountered in CdkCalculateAllocations. Unable to find adjustment account. ${error}`
);
}
}
});
//profile level adjustments for labor and materials
for (const key of Object.keys(job.job_totals.rates)) {
if (
job.job_totals.rates[key] &&
job.job_totals.rates[key].adjustment &&
Dinero(job.job_totals.rates[key].adjustment).isZero() === false
) {
Object.keys(job.job_totals.rates).forEach((key) => {
if (job.job_totals.rates[key] && job.job_totals.rates[key].adjustment && Dinero(job.job_totals.rates[key].adjustment).isZero() === false) {
const accountName = selectedDmsAllocationConfig.profits[key.toUpperCase()];
const otherAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === accountName);
if (otherAccount) {
if (!profitCenterHash[accountName]) profitCenterHash[accountName] = Dinero();
profitCenterHash[accountName] = profitCenterHash[accountName].add(
Dinero(job.job_totals.rates[key].adjustments)
);
profitCenterHash[accountName] = profitCenterHash[accountName].add(Dinero(job.job_totals.rates[key].adjustments));
} else {
// Await the log event creation here
await CdkBase.createLogEvent(
CdkBase.createLogEvent(
connectionData,
"ERROR",
`Error encountered in CdkCalculateAllocations. Unable to find adjustment account for key ${key}.`
`Error encountered in CdkCalculateAllocations. Unable to find adjustment account. ${error}`
);
}
}
}
});
}
const jobAllocations = _.union(Object.keys(profitCenterHash), Object.keys(costCenterHash)).map((key) => {

View File

@@ -86,7 +86,7 @@ async function GetCdkMakes(req, cdk_dealerid) {
{}
);
await CheckCdkResponseForError(null, soapResponseVehicleSearch);
CheckCdkResponseForError(null, soapResponseVehicleSearch);
const [result, rawResponse, , rawRequest] = soapResponseVehicleSearch;
logger.log("cdk-replace-makes-models-request", "ERROR", req.user.email, null, {
cdk_dealerid,

File diff suppressed because it is too large Load Diff

View File

@@ -15,10 +15,10 @@ exports.CDK_CREDENTIALS = CDK_CREDENTIALS;
const cdkDomain =
process.env.NODE_ENV === "production" ? "https://3pa.dmotorworks.com" : "https://uat-3pa.dmotorworks.com";
async function CheckCdkResponseForError(socket, soapResponse) {
function CheckCdkResponseForError(socket, soapResponse) {
if (!soapResponse[0]) {
//The response was null, this might be ok, it might not.
await CdkBase.createLogEvent(
CdkBase.createLogEvent(
socket,
"WARNING",
`Warning detected in CDK Response - it appears to be null. Stack: ${new Error().stack}`
@@ -31,22 +31,23 @@ async function CheckCdkResponseForError(socket, soapResponse) {
if (Array.isArray(ResultToCheck)) {
ResultToCheck.forEach((result) => checkIndividualResult(socket, result));
} else {
await checkIndividualResult(socket, ResultToCheck);
checkIndividualResult(socket, ResultToCheck);
}
}
exports.CheckCdkResponseForError = CheckCdkResponseForError;
async function checkIndividualResult(socket, ResultToCheck) {
function checkIndividualResult(socket, ResultToCheck) {
if (
ResultToCheck.errorLevel === 0 ||
ResultToCheck.errorLevel === "0" ||
ResultToCheck.code === "success" ||
(!ResultToCheck.code && !ResultToCheck.errorLevel)
) {
)
//TODO: Verify that this is the best way to detect errors.
} else {
await CdkBase.createLogEvent(
return;
else {
CdkBase.createLogEvent(
socket,
"ERROR",
`Error detected in CDK Response - ${JSON.stringify(ResultToCheck, null, 2)}`

View File

@@ -41,11 +41,9 @@ const tasksEmailQueueCleanup = async () => {
}
};
// TODO: Consolidate into a group that can be run (multiple events with the same name)
if (process.env.NODE_ENV !== "development") {
// Handling SIGINT (e.g., Ctrl+C)
process.on("SIGINT", async () => {
logger.log("Clearing Tasks Email Queue...", "INFO", "redis", "api");
await tasksEmailQueueCleanup();
process.exit(0);
});

View File

@@ -1,461 +0,0 @@
const { isObject } = require("lodash");
const jobUpdated = async (req, res) => {
const { io, logger } = req;
console.dir(req.body, { depth: null });
if (!req?.body?.event?.data?.new || !isObject(req?.body?.event?.data?.new)) {
logger.log("job-update-error", "ERROR", req.user?.email, null, {
message: `Malformed Job Update request sent from Hasura`,
body: req?.body
});
return res.json({
status: "error",
message: `Malformed Job Update request sent from Hasura`
});
}
logger.log("job-update", "INFO", req.user?.email, null, {
message: `Job updated event received from Hasura`,
jobid: req?.body?.event?.data?.new?.id
});
const updatedJob = req.body.event.data.new;
const bodyshopID = updatedJob.shopid;
// Emit the job-updated event only to the room corresponding to the bodyshop
io.to(bodyshopID).emit("job-updated", updatedJob);
return res.json({ message: "Job updated and event emitted" });
};
module.exports = jobUpdated;
// {
// actual_completion: null,
// actual_delivery: null,
// actual_in: '2024-09-05T23:04:31.439+00:00',
// adj_g_disc: 0,
// adj_strdis: 0,
// adj_towdis: 0,
// adjustment_bottom_line: null,
// agt_addr1: null,
// agt_addr2: null,
// agt_city: null,
// agt_co_id: null,
// agt_co_nm: null,
// agt_ct_fn: null,
// agt_ct_ln: null,
// agt_ct_ph: null,
// agt_ct_phx: null,
// agt_ctry: null,
// agt_ea: null,
// agt_fax: null,
// agt_faxx: null,
// agt_lic_no: null,
// agt_ph1: null,
// agt_ph1x: null,
// agt_ph2: null,
// agt_ph2x: null,
// agt_st: null,
// agt_zip: null,
// alt_transport: 'No car',
// area_of_damage: { impact1: '19', impact2: '0102' },
// asgn_date: '2024-08-06',
// asgn_no: '72361166-99',
// asgn_type: null,
// auto_add_ats: false,
// ca_bc_pvrt: null,
// ca_customer_gst: 500,
// ca_gst_registrant: true,
// cat_no: null,
// category: null,
// cieca_pfl: {},
// cieca_pfo: {},
// cieca_pft: {},
// cieca_stl: {
// data: [
// [Object], [Object], [Object],
// [Object], [Object], [Object],
// [Object], [Object], [Object],
// [Object], [Object], [Object],
// [Object], [Object], [Object],
// [Object], [Object], [Object],
// [Object], [Object], [Object],
// [Object]
// ]
// },
// cieca_ttl: {
// data: {
// g_aa_amt: 0,
// g_bett_amt: 0,
// g_cust_amt: 0,
// g_ded_amt: 0,
// g_rpd_amt: 0,
// g_tax: 854.37,
// g_ttl_amt: 13721.35,
// g_ttl_disc: 0,
// g_upd_amt: 0,
// gst_amt: 612.72,
// n_ttl_amt: 13721.35,
// prev_net: 0,
// supp_amt: 958.53
// }
// },
// ciecaid: '2273515',
// class: null,
// clm_addr1: null,
// clm_addr2: null,
// clm_city: null,
// clm_ct_fn: null,
// clm_ct_ln: null,
// clm_ct_ph: null,
// clm_ct_phx: null,
// clm_ctry: null,
// clm_ea: null,
// clm_fax: null,
// clm_faxx: null,
// clm_no: '72361166-99',
// clm_ofc_id: null,
// clm_ofc_nm: null,
// clm_ph1: null,
// clm_ph1x: null,
// clm_ph2: null,
// clm_ph2x: null,
// clm_st: null,
// clm_title: null,
// clm_total: 13721.3,
// clm_zip: null,
// comment: null,
// completed_tasks: [],
// converted: true,
// created_at: '2024-09-05T22:50:11.37571+00:00',
// cust_pr: 'C',
// date_estimated: null,
// date_exported: null,
// date_invoiced: null,
// date_last_contacted: null,
// date_lost_sale: null,
// date_next_contact: '2024-09-07T23:04:31.439+00:00',
// date_open: '2024-09-05T22:50:12.083+00:00',
// date_rentalresp: null,
// date_repairstarted: null,
// date_scheduled: '2024-09-05T23:04:12.182+00:00',
// date_towin: null,
// date_void: null,
// ded_amt: 0,
// ded_note: null,
// ded_status: 'Y',
// deliverchecklist: null,
// depreciation_taxes: 0,
// dms_allocation: null,
// driveable: true,
// employee_body: null,
// employee_csr: null,
// employee_prep: null,
// employee_refinish: null,
// est_addr1: null,
// est_addr2: null,
// est_city: null,
// est_co_nm: null,
// est_ct_fn: 'Monique',
// est_ct_ln: 'Bruneau',
// est_ctry: null,
// est_ea: 'MONIQUE@STCAUTO.COM',
// est_ph1: null,
// est_st: null,
// est_zip: null,
// federal_tax_rate: 0.05,
// g_bett_amt: 0,
// id: '344fe1e0-4e0c-4f7b-9728-659c850d8192',
// inproduction: true,
// ins_addr1: null,
// ins_addr2: null,
// ins_city: null,
// ins_co_id: null,
// ins_co_nm: 'ICBC',
// ins_ct_fn: null,
// ins_ct_ln: null,
// ins_ct_ph: null,
// ins_ct_phx: null,
// ins_ctry: null,
// ins_ea: null,
// ins_fax: null,
// ins_faxx: null,
// ins_memo: null,
// ins_ph1: null,
// ins_ph1x: null,
// ins_ph2: null,
// ins_ph2x: null,
// ins_st: null,
// ins_title: null,
// ins_zip: null,
// insd_addr1: null,
// insd_addr2: null,
// insd_city: null,
// insd_co_nm: null,
// insd_ctry: null,
// insd_ea: null,
// insd_fax: null,
// insd_faxx: null,
// insd_fn: null,
// insd_ln: null,
// insd_ph1: null,
// insd_ph1x: null,
// insd_ph2: null,
// insd_ph2x: null,
// insd_st: null,
// insd_title: null,
// insd_zip: null,
// intakechecklist: {
// addToProduction: true,
// allow_text_message: false,
// completed_at: '2024-09-05T23:04:31.439Z',
// completed_by: 'allan@imex.dev',
// form: [ [Object], [Object], [Object], [Object], [Object], [Object] ],
// production_vars: {},
// scheduled_completion: '2024-09-25T23:03:55.285Z',
// scheduled_delivery: null
// },
// invoice_allocation: null,
// invoice_date: null,
// invoice_final_note: null,
// iouparent: null,
// job_totals: {
// additional: {
// additionalCostItems: [Array],
// additionalCosts: [Object],
// adjustments: [Object],
// pvrt: [Object],
// shipping: [Object],
// storage: [Object],
// total: [Object],
// towing: [Object]
// },
// parts: { parts: [Object], sublets: [Object] },
// rates: {
// la1: [Object],
// la2: [Object],
// la3: [Object],
// la4: [Object],
// laa: [Object],
// lab: [Object],
// lad: [Object],
// lae: [Object],
// laf: [Object],
// lag: [Object],
// lam: [Object],
// lar: [Object],
// las: [Object],
// lau: [Object],
// mapa: [Object],
// mash: [Object],
// rates_subtotal: [Object],
// subtotal: [Object]
// },
// totals: {
// custPayable: [Object],
// federal_tax: [Object],
// local_tax: [Object],
// net_repairs: [Object],
// statePartsTax: [Object],
// state_tax: [Object],
// subtotal: [Object],
// total_repairs: [Object]
// }
// },
// kanbanparent: '-1',
// kmin: null,
// kmout: null,
// labor_rate_desc: 'EST',
// labor_rate_id: null,
// lbr_adjustments: {},
// local_tax_rate: null,
// loss_cat: 'U',
// loss_date: '2024-06-19',
// loss_desc: 'Animal',
// loss_of_use: null,
// loss_type: 'A',
// lost_sale_reason: null,
// materials: {
// mapa: { cal_maxdlr: 9999.99, cal_opcode: 'OP13' },
// mash: { cal_maxdlr: 9999.99, cal_opcode: 'OP13' }
// },
// other_amount_payable: null,
// owner_owing: 500,
// ownerid: 'f392b24f-e828-47fa-bd17-2e5af8493147',
// ownr_addr1: null,
// ownr_addr2: null,
// ownr_city: null,
// ownr_co_nm: null,
// ownr_ctry: null,
// ownr_ea: null,
// ownr_fax: null,
// ownr_faxx: null,
// ownr_fn: 'Neil',
// ownr_ln: 'Leslie',
// ownr_ph1: null,
// ownr_ph1x: null,
// ownr_ph2: null,
// ownr_ph2x: null,
// ownr_st: null,
// ownr_title: null,
// ownr_zip: null,
// parts_tax_rates: {
// CCC: {},
// CCD: {},
// CCDR: {},
// CCF: {},
// CCM: {},
// PAA: {
// prt_discp: 0,
// prt_mktyp: false,
// prt_mkupp: 0,
// prt_tax_in: true,
// prt_tax_rt: 0.07,
// prt_type: 'PAA'
// },
// PAC: {
// prt_discp: 0,
// prt_mktyp: false,
// prt_mkupp: 0,
// prt_tax_in: true,
// prt_tax_rt: 0.07,
// prt_type: 'PAC'
// },
// PAG: {},
// PAL: {
// prt_discp: 0,
// prt_mktyp: false,
// prt_mkupp: 0,
// prt_tax_in: true,
// prt_tax_rt: 0.07,
// prt_type: 'PAL'
// },
// PAM: {
// prt_discp: 0,
// prt_mktyp: false,
// prt_mkupp: 0,
// prt_tax_in: true,
// prt_tax_rt: 0.07,
// prt_type: 'PAM'
// },
// PAN: {
// prt_discp: 0,
// prt_mktyp: false,
// prt_mkupp: 0,
// prt_tax_in: true,
// prt_tax_rt: 0.07,
// prt_type: 'PAN'
// },
// PAO: {},
// PAP: {},
// PAR: {
// prt_discp: 0,
// prt_mktyp: false,
// prt_mkupp: 0,
// prt_tax_in: true,
// prt_tax_rt: 0.07,
// prt_type: 'PAR'
// },
// PAS: {
// prt_discp: 0,
// prt_mktyp: false,
// prt_mkupp: 0,
// prt_tax_in: true,
// prt_tax_rt: 0.07,
// prt_type: 'PAS'
// },
// PASL: {}
// },
// pay_amt: 0,
// pay_chknm: '0',
// pay_date: null,
// pay_type: null,
// payee_nms: null,
// plate_no: 'B15717',
// plate_st: 'MB',
// po_number: null,
// policy_no: '18292147',
// production_vars: { note: 'testtsdsadsasdasdsadsdsadsd' },
// qb_multiple_payers: null,
// queued_for_parts: false,
// rate_ats: null,
// rate_la1: 0,
// rate_la2: 0,
// rate_la3: null,
// rate_la4: 0,
// rate_laa: 0,
// rate_lab: 88.43,
// rate_lad: null,
// rate_lae: null,
// rate_laf: 97.06,
// rate_lag: 88.43,
// rate_lam: 103.5,
// rate_lar: 88.43,
// rate_las: 88.43,
// rate_lau: 0,
// rate_ma2s: 0,
// rate_ma2t: 0,
// rate_ma3s: 0,
// rate_mabl: null,
// rate_macs: 0,
// rate_mahw: 49.7,
// rate_mapa: 56.67,
// rate_mash: 7.23,
// rate_matd: null,
// referral_source: null,
// referral_source_extra: null,
// regie_number: null,
// remove_from_ar: false,
// ro_number: '10002',
// scheduled_completion: '2024-09-25T23:03:55.285+00:00',
// scheduled_delivery: null,
// scheduled_in: '2024-09-05T22:00:55.2+00:00',
// selling_dealer: null,
// selling_dealer_contact: null,
// servicing_dealer: null,
// servicing_dealer_contact: null,
// shopid: 'bfec8c8c-b7f1-49e0-be4c-524455f4e582',
// special_coverage_policy: false,
// state_tax_rate: null,
// status: 'Repair Plan',
// storage_payable: null,
// suspended: false,
// tax_lbr_rt: 0.07,
// tax_levies_rt: 0.07,
// tax_paint_mat_rt: 0.07,
// tax_predis: 0,
// tax_prethr: 0.07,
// tax_pstthr: 0,
// tax_registration_number: null,
// tax_shop_mat_rt: 0.07,
// tax_str_rt: 0.07,
// tax_sub_rt: 0.07,
// tax_thramt: 0,
// tax_tow_rt: 0,
// theft_ind: false,
// tlos_ind: false,
// towin: false,
// towing_payable: null,
// unit_number: null,
// updated_at: '2024-09-12T19:25:48.492299+00:00',
// v_color: 'Polished Metal Metal',
// v_make_desc: 'Honda',
// v_model_desc: 'Civic',
// v_model_yr: '17',
// v_vin: 'SHHFK7H20HU306419',
// vehicleid: '811ba420-e9db-430b-b341-22e197c8dd0e',
// voided: false
// }
// I have been hit, wewet
// job-transition-update-result {
// type: 'DEBUG',
// env: 'development',
// user: null,
// record: '344fe1e0-4e0c-4f7b-9728-659c850d8192',
// insert_transitions_one: { id: '88e73841-247d-4e31-b31c-f1b2ca8364d9' },
// update_transitions: { affected_rows: 1 }
// }

View File

@@ -14,4 +14,3 @@ exports.costing = require("./job-costing").JobCosting;
exports.costingmulti = require("./job-costing").JobCostingMulti;
exports.statustransition = require("./job-status-transition").statustransition;
exports.lifecycle = require("./job-lifecycle");
exports.jobUpdated = require("./job-updated");

View File

@@ -1,10 +1,11 @@
const express = require("express");
const router = express.Router();
const job = require("../job/job");
const ppc = require("../ccc/partspricechange");
const { partsScan } = require("../parts-scan/parts-scan");
const eventAuthorizationMiddleware = require("../middleware/eventAuthorizationMIddleware");
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const { totals, statustransition, totalsSsu, costing, lifecycle, costingmulti, jobUpdated } = require("../job/job");
const { totals, statustransition, totalsSsu, costing, lifecycle, costingmulti } = require("../job/job");
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
router.post("/totals", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, totals);
@@ -15,5 +16,5 @@ router.post("/lifecycle", validateFirebaseIdTokenMiddleware, withUserGraphQLClie
router.post("/costingmulti", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, costingmulti);
router.post("/partsscan", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, partsScan);
router.post("/ppc", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, ppc.generatePpc);
router.post("/job-updated", eventAuthorizationMiddleware, jobUpdated);
module.exports = router;

View File

@@ -3,79 +3,64 @@ require("dotenv").config({
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
});
const {
io,
setSessionData,
clearSessionData,
getMultipleSessionData,
addItemToEndOfList,
pubClient
} = require("../../server");
const { io } = require("../../server");
const { admin } = require("../firebase/firebase-handler");
const logger = require("../utils/logger");
const { default: CdkJobExport, CdkSelectedCustomer } = require("../cdk/cdk-job-export");
const CdkGetMakes = require("../cdk/cdk-get-makes").default;
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
const { isArray } = require("lodash");
const logger = require("../utils/logger");
const { default: PbsExportJob, PbsSelectedCustomer } = require("../accounting/pbs/pbs-job-export");
const { PbsCalculateAllocationsAp, PbsExportAp } = require("../accounting/pbs/pbs-ap-allocations");
// Middleware for verifying tokens via Firebase authentication
function socketAuthMiddleware(socket, next) {
const token = socket.handshake.auth.token;
if (!token) return next(new Error("Authentication error - no authorization token."));
admin
.auth()
.verifyIdToken(token)
.then((user) => {
socket.user = user;
next();
})
.catch((error) => {
next(new Error("Authentication error", JSON.stringify(error)));
io.use(function (socket, next) {
try {
if (socket.handshake.auth.token) {
admin
.auth()
.verifyIdToken(socket.handshake.auth.token)
.then((user) => {
socket.user = user;
next();
})
.catch((error) => {
next(new Error("Authentication error", JSON.stringify(error)));
});
} else {
next(new Error("Authentication error - no authorization token."));
}
} catch (error) {
console.log("Uncaught connection error:::", error);
logger.log("websocket-connection-error", "error", null, null, {
token: socket.handshake.auth.token,
...error
});
}
next(new Error(`Authentication error ${error}`));
}
});
// Register all socket events for a given socket connection
async function registerSocketEvents(socket) {
await setSessionData(socket.id, "log_level", "TRACE");
await createLogEvent(socket, "DEBUG", `Connected and Authenticated.`);
io.on("connection", (socket) => {
socket.log_level = "TRACE";
createLogEvent(socket, "DEBUG", `Connected and Authenticated.`);
// Register CDK-related socket events
registerCdkEvents(socket);
// Register PBS AR-related socket events
registerPbsArEvents(socket);
// Register PBS AP-related socket events
registerPbsApEvents(socket);
// Register room and broadcasting events
registerRoomAndBroadcastEvents(socket);
// Register event to clear DMS session
registerDmsClearSessionEvent(socket);
// Register Production Board events
registerProductionBoardEvents(socket);
// Handle socket disconnection
socket.on("disconnect", async () => {
await createLogEvent(socket, "DEBUG", `User disconnected.`);
socket.on("set-log-level", (level) => {
socket.log_level = level;
socket.emit("log-event", {
timestamp: new Date(),
level: "INFO",
message: `Updated log level to ${level}`
});
});
}
// CDK-specific socket events
function registerCdkEvents(socket) {
socket.on("cdk-export-job", (jobid) => CdkJobExport(socket, jobid));
socket.on("cdk-selected-customer", async (selectedCustomerId) => {
await createLogEvent(socket, "DEBUG", `User selected customer ID ${selectedCustomerId}`);
CdkSelectedCustomer(socket, selectedCustomerId).catch((err) =>
console.error(`Error in cdk-selected-customer: ${err}`)
);
await setSessionData(socket.id, "selectedCustomer", selectedCustomerId);
///CDK
socket.on("cdk-export-job", (jobid) => {
CdkJobExport(socket, jobid);
});
socket.on("cdk-selected-customer", (selectedCustomerId) => {
createLogEvent(socket, "DEBUG", `User selected customer ID ${selectedCustomerId}`);
socket.selectedCustomerId = selectedCustomerId;
CdkSelectedCustomer(socket, selectedCustomerId);
});
socket.on("cdk-get-makes", async (cdk_dealerid, callback) => {
@@ -83,187 +68,159 @@ function registerCdkEvents(socket) {
const makes = await CdkGetMakes(socket, cdk_dealerid);
callback(makes);
} catch (error) {
await createLogEvent(socket, "ERROR", `Error in cdk-get-makes WS call. ${JSON.stringify(error)}`);
createLogEvent(socket, "ERROR", `Error in cdk-get-makes WS call. ${JSON.stringify(error, null, 2)}`);
}
});
socket.on("cdk-calculate-allocations", async (jobid, callback) => {
const allocations = await CdkCalculateAllocations(socket, jobid);
await createLogEvent(socket, "DEBUG", `Allocations calculated.`);
await createLogEvent(socket, "TRACE", `Allocations details: ${JSON.stringify(allocations)}`);
callback(allocations);
await setSessionData(socket.id, "cdk_allocations", allocations);
});
}
createLogEvent(socket, "DEBUG", `Allocations calculated.`);
createLogEvent(socket, "TRACE", `Allocations calculated. ${JSON.stringify(allocations, null, 2)}`);
// PBS AR-specific socket events
function registerPbsArEvents(socket) {
callback(allocations);
});
//END CDK
//PBS AR
socket.on("pbs-calculate-allocations", async (jobid, callback) => {
const allocations = await CdkCalculateAllocations(socket, jobid);
await createLogEvent(socket, "DEBUG", `PBS AR allocations calculated.`);
await createLogEvent(socket, "TRACE", `Allocations details: ${JSON.stringify(allocations)}`);
createLogEvent(socket, "DEBUG", `Allocations calculated.`);
createLogEvent(socket, "TRACE", `Allocations calculated. ${JSON.stringify(allocations, null, 2)}`);
callback(allocations);
await setSessionData(socket.id, "pbs_allocations", allocations);
});
socket.on("pbs-export-job", (jobid) => PbsExportJob(socket, jobid));
socket.on("pbs-selected-customer", async (selectedCustomerId) => {
await createLogEvent(socket, "DEBUG", `PBS AR selected customer ID ${selectedCustomerId}`);
PbsSelectedCustomer(socket, selectedCustomerId).catch((err) =>
console.error(`Error in pbs-selected-customer: ${err}`)
);
await setSessionData(socket.id, "selectedCustomer", selectedCustomerId);
socket.on("pbs-export-job", (jobid) => {
PbsExportJob(socket, jobid);
});
}
socket.on("pbs-selected-customer", (selectedCustomerId) => {
createLogEvent(socket, "DEBUG", `User selected customer ID ${selectedCustomerId}`);
socket.selectedCustomerId = selectedCustomerId;
PbsSelectedCustomer(socket, selectedCustomerId);
});
//End PBS AR
function registerProductionBoardEvents(socket) {}
// PBS AP-specific socket events
function registerPbsApEvents(socket) {
//PBS AP
socket.on("pbs-calculate-allocations-ap", async (billids, callback) => {
const allocations = await PbsCalculateAllocationsAp(socket, billids);
await createLogEvent(socket, "DEBUG", `PBS AP allocations calculated.`);
await createLogEvent(socket, "TRACE", `Allocations details: ${JSON.stringify(allocations)}`);
createLogEvent(socket, "DEBUG", `AP Allocations calculated.`);
createLogEvent(socket, "TRACE", `Allocations calculated. ${JSON.stringify(allocations, null, 2)}`);
socket.apAllocations = allocations;
callback(allocations);
await setSessionData(socket.id, "pbs_ap_allocations", allocations);
});
socket.on("pbs-export-ap", async ({ billids, txEnvelope }) => {
await setSessionData(socket.id, "pbs_txEnvelope", txEnvelope);
PbsExportAp(socket, {
billids,
txEnvelope
}).catch((err) => console.error(`Error in pbs-export-ap: ${err}`));
});
}
const getRedisKeyForSocket = (socketId) => `socket:${socketId}:rooms`;
// Room management and broadcasting events
function registerRoomAndBroadcastEvents(socket) {
// Rejoin rooms on reconnect
pubClient.lRange(getRedisKeyForSocket(socket.id), 0, -1, (err, rooms) => {
if (rooms && rooms.length > 0) {
rooms.forEach((room) => {
socket.join(room);
createLogEvent(socket, "DEBUG", `Client rejoined bodyshop room: ${room}`);
});
}
socket.on("pbs-export-ap", ({ billids, txEnvelope }) => {
socket.txEnvelope = txEnvelope;
PbsExportAp(socket, { billids, txEnvelope });
});
socket.on("join-bodyshop-room", async (bodyshopUUID) => {
socket.join(bodyshopUUID);
// Store room in Redis
pubClient.rPush(getRedisKeyForSocket(socket.id), bodyshopUUID);
await createLogEvent(socket, "DEBUG", `Client joined bodyshop room: ${bodyshopUUID}`);
});
socket.on("leave-bodyshop-room", async (bodyshopUUID) => {
socket.leave(bodyshopUUID);
// Remove room from Redis
pubClient.lRem(getRedisKeyForSocket(socket.id), 0, bodyshopUUID);
await createLogEvent(socket, "DEBUG", `Client left bodyshop room: ${bodyshopUUID}`);
});
socket.on("broadcast-to-bodyshop", async (bodyshopUUID, message) => {
io.to(bodyshopUUID).emit("bodyshop-message", message);
await createLogEvent(socket, "INFO", `Broadcast message to bodyshop ${bodyshopUUID}`);
});
//END PBS AP
socket.on("disconnect", () => {
// Optional: Cleanup Redis entry on disconnect if needed
createLogEvent(socket, "DEBUG", `Client disconnected: ${socket.id}`);
createLogEvent(socket, "DEBUG", `User disconnected.`);
});
}
// DMS session clearing event
function registerDmsClearSessionEvent(socket) {
socket.on("clear-dms-session", async () => {
await clearSessionData(socket.id); // Clear all session data in Redis
await createLogEvent(socket, "INFO", `DMS session data cleared for socket ${socket.id}`);
});
}
});
// Logging helper functions
async function createLogEvent(socket, level, message) {
const { logLevel, recordid } = await getMultipleSessionData(socket.id, ["log_level", "recordid"]);
if (LogLevelHierarchy(logLevel) >= LogLevelHierarchy(level)) {
const logMessage = { timestamp: new Date(), level, message };
// Log the message to the console and emit it via the socket
console.log(`[WS LOG EVENT] ${level} - ${logMessage.timestamp} - ${socket.user.email} - ${socket.id} - ${message}`);
socket.emit("log-event", logMessage);
// Log the event via the logger
logger.log("ws-log-event", level, socket.user.email, recordid, { wsmessage: message });
// Add the log message to the Redis list using the helper function
await addItemToEndOfList(socket.id, "logEvents", logMessage);
}
}
async function createJsonEvent(socket, level, message, json) {
const { logLevel, recordid } = await getMultipleSessionData(socket.id, ["log_level", "recordid"]);
if (LogLevelHierarchy(logLevel) >= LogLevelHierarchy(level)) {
const logMessage = { timestamp: new Date(), level, message };
// Emit the log message via the socket
socket.emit("log-event", logMessage);
// Log the JSON event via the logger
logger.log("ws-log-event-json", level, socket.user.email, recordid, {
wsmessage: message,
json
function createLogEvent(socket, level, message) {
if (LogLevelHierarchy(socket.log_level) >= LogLevelHierarchy(level)) {
console.log(`[WS LOG EVENT] ${level} - ${new Date()} - ${socket.user.email} - ${socket.id} - ${message}`);
socket.emit("log-event", {
timestamp: new Date(),
level,
message
});
// Use the helper function to append the log event to the Redis list
await addItemToEndOfList(socket.id, "logEvents", logMessage);
logger.log("ws-log-event", level, socket.user.email, socket.recordid, {
wsmessage: message
});
if (socket.logEvents && isArray(socket.logEvents)) {
socket.logEvents.push({
timestamp: new Date(),
level,
message
});
}
// if (level === "ERROR") {
// throw new Error(message);
// }
}
}
async function createXmlEvent(socket, xml, message, isError = false) {
const { logLevel, recordid } = await getMultipleSessionData(socket.id, ["log_level", "recordid"]);
function createJsonEvent(socket, level, message, json) {
if (LogLevelHierarchy(socket.log_level) >= LogLevelHierarchy(level)) {
console.log(`[WS LOG EVENT] ${level} - ${new Date()} - ${socket.user.email} - ${socket.id} - ${message}`);
socket.emit("log-event", {
timestamp: new Date(),
level,
message
});
}
logger.log("ws-log-event-json", level, socket.user.email, socket.recordid, {
wsmessage: message,
json
});
if (LogLevelHierarchy(logLevel) >= LogLevelHierarchy("TRACE")) {
const logMessage = {
if (socket.logEvents && isArray(socket.logEvents)) {
socket.logEvents.push({
timestamp: new Date(),
level,
message
});
}
// if (level === "ERROR") {
// throw new Error(message);
// }
}
function createXmlEvent(socket, xml, message, isError = false) {
if (LogLevelHierarchy(socket.log_level) >= LogLevelHierarchy("TRACE")) {
socket.emit("log-event", {
timestamp: new Date(),
level: isError ? "ERROR" : "TRACE",
message: `${message}: ${xml}`
};
});
}
// Emit the log message via the socket
socket.emit("log-event", logMessage);
logger.log(
isError ? "ws-log-event-xml-error" : "ws-log-event-xml",
isError ? "ERROR" : "TRACE",
socket.user.email,
socket.recordid,
{
wsmessage: message,
xml
}
);
// Log the XML event via the logger
logger.log(
isError ? "ws-log-event-xml-error" : "ws-log-event-xml",
isError ? "ERROR" : "TRACE",
socket.user.email,
recordid,
{ wsmessage: message, xml }
);
// Use the helper function to append the log event to the Redis list
await addItemToEndOfList(socket.id, "logEvents", { ...logMessage, xml });
if (socket.logEvents && isArray(socket.logEvents)) {
socket.logEvents.push({
timestamp: new Date(),
level: isError ? "ERROR" : "TRACE",
message,
xml
});
}
}
// Log level hierarchy
function LogLevelHierarchy(level) {
const levels = { XML: 5, TRACE: 5, DEBUG: 4, INFO: 3, WARNING: 2, ERROR: 1 };
return levels[level] || 3;
switch (level) {
case "XML":
return 5;
case "TRACE":
return 5;
case "DEBUG":
return 4;
case "INFO":
return 3;
case "WARNING":
return 2;
case "ERROR":
return 1;
default:
return 3;
}
}
// Socket.IO Middleware and Connection
io.use(socketAuthMiddleware);
io.on("connection", registerSocketEvents);
// Export logging helpers
exports.createLogEvent = createLogEvent;
exports.createXmlEvent = createXmlEvent;
exports.createJsonEvent = createJsonEvent;