Compare commits
97 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c0eab9366 | ||
|
|
b831d8ca8a | ||
|
|
69da6bccf7 | ||
|
|
e3d7ebd7d8 | ||
|
|
5f0b63a192 | ||
|
|
7a5ac739ab | ||
|
|
e2297be0af | ||
|
|
73c4983342 | ||
|
|
166e1e4030 | ||
|
|
5fa7377121 | ||
|
|
f21ba8e087 | ||
|
|
d56d1f369c | ||
|
|
360a1954f4 | ||
|
|
6b047418cc | ||
|
|
87db292e5d | ||
|
|
9ef8440e64 | ||
|
|
8ae3b28cb6 | ||
|
|
0d80854196 | ||
|
|
029fb58f48 | ||
|
|
85929b0bb1 | ||
|
|
dc234e4d72 | ||
|
|
212fc4a7cc | ||
|
|
8de7db60e6 | ||
|
|
d6df5af1a4 | ||
|
|
8d36ad3589 | ||
|
|
9061821347 | ||
|
|
1fad3968bb | ||
|
|
1d84dd1a83 | ||
|
|
a492909ad7 | ||
|
|
14a885b443 | ||
|
|
d5bd9d9b59 | ||
|
|
6e6cabbd63 | ||
|
|
480838b1dc | ||
|
|
ffadd31a5f | ||
|
|
235527140c | ||
|
|
ef22ba3d2c | ||
|
|
11ff8e91c7 | ||
|
|
71dd138f2f | ||
|
|
36f4cc8cb8 | ||
|
|
d2944ff902 | ||
|
|
3cbcbb92eb | ||
|
|
02e6c6007c | ||
|
|
2cee5f1944 | ||
|
|
ef695776cd | ||
|
|
53580fbc78 | ||
|
|
21335d4e8c | ||
|
|
9b545d6c8c | ||
|
|
fbe674a2e5 | ||
|
|
2a65cb5025 | ||
|
|
b4a3960eac | ||
|
|
358503f9ef | ||
|
|
25a9e6cea1 | ||
|
|
e40e0bbb8f | ||
|
|
8fdd07827e | ||
|
|
059067bc61 | ||
|
|
f8ae6dc5af | ||
|
|
ac2bb42124 | ||
|
|
b149f70b6f | ||
|
|
ec8a413ed1 | ||
|
|
76ec755d07 | ||
|
|
07faa5eec2 | ||
|
|
7bbbf5934a | ||
|
|
fd7850b551 | ||
|
|
2b76f8a12d | ||
|
|
aa073cfd68 | ||
|
|
03863ce838 | ||
|
|
1b22697429 | ||
|
|
4fc3fbdcc0 | ||
|
|
163978930f | ||
|
|
c75e27e018 | ||
|
|
555bedbb6c | ||
|
|
a57abec81b | ||
|
|
b9df4c2587 | ||
|
|
15686bdab8 | ||
|
|
175e2097fa | ||
|
|
359c4c75a1 | ||
|
|
86aa5bf5e7 | ||
|
|
35b92570e5 | ||
|
|
b5c03b8cf0 | ||
|
|
3c45519457 | ||
|
|
dc60b8d18e | ||
|
|
ea75ac49aa | ||
|
|
f3c6c7f004 | ||
|
|
65fb73ae82 | ||
|
|
617e39eb17 | ||
|
|
f4a3b75a86 | ||
|
|
c0ffda27cf | ||
|
|
f51fa08961 | ||
|
|
ba63e8054f | ||
|
|
32813032e6 | ||
|
|
a5904f55aa | ||
|
|
f6acc1107c | ||
|
|
9b871149ac | ||
|
|
9a71779cfe | ||
|
|
5bd6f0453d | ||
|
|
f6328d10f7 | ||
|
|
3766c3d938 |
@@ -88,7 +88,7 @@ jobs:
|
||||
name: Install Dependencies
|
||||
command: npm i
|
||||
|
||||
- run: npm run build:production:imex
|
||||
- run: NODE_OPTIONS=--max-old-space-size=8192 npm run build:production:imex
|
||||
|
||||
- aws-cli/setup:
|
||||
aws_access_key_id: AWS_ACCESS_KEY_ID
|
||||
@@ -151,7 +151,7 @@ jobs:
|
||||
rome-app-build:
|
||||
docker:
|
||||
- image: cimg/node:22.13.1
|
||||
|
||||
resource_class: large
|
||||
working_directory: ~/repo/client
|
||||
|
||||
steps:
|
||||
@@ -161,7 +161,7 @@ jobs:
|
||||
name: Install Dependencies
|
||||
command: npm i
|
||||
|
||||
- run: npm run build:production:rome
|
||||
- run: NODE_OPTIONS=--max-old-space-size=8192 npm run build:production:rome
|
||||
|
||||
- aws-cli/setup:
|
||||
aws_access_key_id: AWS_ACCESS_KEY_ID
|
||||
@@ -209,7 +209,7 @@ jobs:
|
||||
test-rome-app-build:
|
||||
docker:
|
||||
- image: cimg/node:22.13.1
|
||||
|
||||
resource_class: large
|
||||
working_directory: ~/repo/client
|
||||
|
||||
steps:
|
||||
@@ -219,7 +219,7 @@ jobs:
|
||||
name: Install Dependencies
|
||||
command: npm i
|
||||
|
||||
- run: npm run build:test:rome
|
||||
- run: NODE_OPTIONS=--max-old-space-size=8192 npm run build:test:rome
|
||||
|
||||
- aws-cli/setup:
|
||||
aws_access_key_id: AWS_ACCESS_KEY_ID
|
||||
@@ -277,7 +277,7 @@ jobs:
|
||||
name: Install Dependencies
|
||||
command: npm i
|
||||
|
||||
- run: npm run build:test:imex
|
||||
- run: NODE_OPTIONS=--max-old-space-size=8192 npm run build:test:imex
|
||||
|
||||
- aws-s3/sync:
|
||||
from: build
|
||||
@@ -298,7 +298,7 @@ jobs:
|
||||
name: Install Dependencies
|
||||
command: npm i
|
||||
|
||||
- run: npm run build:test:imex
|
||||
- run: NODE_OPTIONS=--max-old-space-size=8192 npm run build:test:imex
|
||||
|
||||
- aws-cli/setup:
|
||||
aws_access_key_id: AWS_ACCESS_KEY_ID
|
||||
|
||||
442
client/package-lock.json
generated
442
client/package-lock.json
generated
@@ -16,13 +16,14 @@
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.6.0",
|
||||
"@sentry/cli": "^2.42.2",
|
||||
"@sentry/react": "^7.114.0",
|
||||
"@sentry/react": "^9.3.0",
|
||||
"@sentry/vite-plugin": "^3.2.2",
|
||||
"@splitsoftware/splitio-react": "^1.13.0",
|
||||
"@tanem/react-nprogress": "^5.0.53",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"antd": "^5.24.2",
|
||||
"apollo-link-logger": "^2.0.1",
|
||||
"apollo-link-sentry": "^3.3.0",
|
||||
"apollo-link-sentry": "^4.1.0",
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.8.1",
|
||||
"classnames": "^2.5.1",
|
||||
@@ -91,7 +92,7 @@
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@sentry/webpack-plugin": "^2.22.4",
|
||||
"@sentry/webpack-plugin": "^3.2.2",
|
||||
"@testing-library/cypress": "^10.0.2",
|
||||
"browserslist": "^4.24.4",
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
@@ -5259,88 +5260,90 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sentry-internal/feedback": {
|
||||
"version": "7.120.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.120.3.tgz",
|
||||
"integrity": "sha512-ewJJIQ0mbsOX6jfiVFvqMjokxNtgP3dNwUv+4nenN+iJJPQsM6a0ocro3iscxwVdbkjw5hY3BUV2ICI5Q0UWoA==",
|
||||
"node_modules/@sentry-internal/browser-utils": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.3.0.tgz",
|
||||
"integrity": "sha512-G3z4HCUyb5nJe03EPUhWjnaHqMDt4mOTFJDNha3DGoB51lMYojpQI1Qo1u6bY4qkWVSO1c+HqOU0RVsXoAchtQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "7.120.3",
|
||||
"@sentry/types": "7.120.3",
|
||||
"@sentry/utils": "7.120.3"
|
||||
"@sentry/core": "9.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/feedback": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.3.0.tgz",
|
||||
"integrity": "sha512-LQmIbQaATlN5QEwCD2Xt+7VKfwfR5W3dbn0jdF1x4hQFE/srdnOj60xMz/mj3tP5BxV552xJniGsyZ8lXHDb2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "9.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/replay": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.3.0.tgz",
|
||||
"integrity": "sha512-ZkH+Gahn89JygpuiFn26ZgAqJXHtnr+HjfQ2ONOFoWQHNH6X5wk75UTma55aYk1d8VcBPFoU6WjFhZoQ55SV1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry-internal/browser-utils": "9.3.0",
|
||||
"@sentry/core": "9.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/replay-canvas": {
|
||||
"version": "7.120.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.120.3.tgz",
|
||||
"integrity": "sha512-s5xy+bVL1eDZchM6gmaOiXvTqpAsUfO7122DxVdEDMtwVq3e22bS2aiGa8CUgOiJkulZ+09q73nufM77kOmT/A==",
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.3.0.tgz",
|
||||
"integrity": "sha512-MhDMJeRGa55a0D541+OzTFMWwbabthhDGbAL90/NpappfyeBbAiktmCNl0BFTZuRbCGrC2m1LLCqHegCVKW4fQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "7.120.3",
|
||||
"@sentry/replay": "7.120.3",
|
||||
"@sentry/types": "7.120.3",
|
||||
"@sentry/utils": "7.120.3"
|
||||
"@sentry-internal/replay": "9.3.0",
|
||||
"@sentry/core": "9.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/tracing": {
|
||||
"version": "7.120.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.120.3.tgz",
|
||||
"integrity": "sha512-Ausx+Jw1pAMbIBHStoQ6ZqDZR60PsCByvHdw/jdH9AqPrNE9xlBSf9EwcycvmrzwyKspSLaB52grlje2cRIUMg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "7.120.3",
|
||||
"@sentry/types": "7.120.3",
|
||||
"@sentry/utils": "7.120.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/babel-plugin-component-annotate": {
|
||||
"version": "2.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.23.0.tgz",
|
||||
"integrity": "sha512-+uLqaCKeYmH/W2YUV1XHkFEtpHdx/aFjCQahPVsvXyqg13dfkR6jaygPL4DB5DJtUSmPFCUE3MEk9ZO5JlhJYg==",
|
||||
"dev": true,
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.2.2.tgz",
|
||||
"integrity": "sha512-D+SKQ266ra/wo87s9+UI/rKQi3qhGPCR8eSCDe0VJudhjHsqyNU+JJ5lnIGCgmZaWFTXgdBP/gdr1Iz1zqGs4Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/browser": {
|
||||
"version": "7.120.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.120.3.tgz",
|
||||
"integrity": "sha512-i9vGcK9N8zZ/JQo1TCEfHHYZ2miidOvgOABRUc9zQKhYdcYQB2/LU1kqlj77Pxdxf4wOa9137d6rPrSn9iiBxg==",
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.3.0.tgz",
|
||||
"integrity": "sha512-yPwWWQo/hpN63p0NGmk/Dd1Fx5CQRWNMfuV7dtfPBtg3vRjDecA9OLyK29AqK5h3Fl8FuJOyOqB87CvtXUqh5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry-internal/feedback": "7.120.3",
|
||||
"@sentry-internal/replay-canvas": "7.120.3",
|
||||
"@sentry-internal/tracing": "7.120.3",
|
||||
"@sentry/core": "7.120.3",
|
||||
"@sentry/integrations": "7.120.3",
|
||||
"@sentry/replay": "7.120.3",
|
||||
"@sentry/types": "7.120.3",
|
||||
"@sentry/utils": "7.120.3"
|
||||
"@sentry-internal/browser-utils": "9.3.0",
|
||||
"@sentry-internal/feedback": "9.3.0",
|
||||
"@sentry-internal/replay": "9.3.0",
|
||||
"@sentry-internal/replay-canvas": "9.3.0",
|
||||
"@sentry/core": "9.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/bundler-plugin-core": {
|
||||
"version": "2.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.23.0.tgz",
|
||||
"integrity": "sha512-Qbw+jZFK63w+V193l0eCFKLzGba2Iu93Fx8kCRzZ3uqjky002H8U3pu4mKgcc11J+u8QTjfNZGUyXsxz0jv2mg==",
|
||||
"dev": true,
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.2.2.tgz",
|
||||
"integrity": "sha512-YGrtmqQ2jMixccX2slVG/Lw7pCGJL3DGB3clmY9mO8QBEBIN3/gEANiHJVWwRidpUOS/0b7yVVGAdwZ87oPwTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.18.5",
|
||||
"@sentry/babel-plugin-component-annotate": "2.23.0",
|
||||
"@sentry/cli": "2.39.1",
|
||||
"@sentry/babel-plugin-component-annotate": "3.2.2",
|
||||
"@sentry/cli": "2.42.2",
|
||||
"dotenv": "^16.3.1",
|
||||
"find-up": "^5.0.0",
|
||||
"glob": "^9.3.2",
|
||||
@@ -5351,181 +5354,6 @@
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/cli": {
|
||||
"version": "2.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.39.1.tgz",
|
||||
"integrity": "sha512-JIb3e9vh0+OmQ0KxmexMXg9oZsR/G7HMwxt5BUIKAXZ9m17Xll4ETXTRnRUBT3sf7EpNGAmlQk1xEmVN9pYZYQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"progress": "^2.0.3",
|
||||
"proxy-from-env": "^1.1.0",
|
||||
"which": "^2.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"sentry-cli": "bin/sentry-cli"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@sentry/cli-darwin": "2.39.1",
|
||||
"@sentry/cli-linux-arm": "2.39.1",
|
||||
"@sentry/cli-linux-arm64": "2.39.1",
|
||||
"@sentry/cli-linux-i686": "2.39.1",
|
||||
"@sentry/cli-linux-x64": "2.39.1",
|
||||
"@sentry/cli-win32-i686": "2.39.1",
|
||||
"@sentry/cli-win32-x64": "2.39.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/cli-darwin": {
|
||||
"version": "2.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.39.1.tgz",
|
||||
"integrity": "sha512-kiNGNSAkg46LNGatfNH5tfsmI/kCAaPA62KQuFZloZiemTNzhy9/6NJP8HZ/GxGs8GDMxic6wNrV9CkVEgFLJQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/cli-linux-arm": {
|
||||
"version": "2.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.39.1.tgz",
|
||||
"integrity": "sha512-DkENbxyRxUrfLnJLXTA4s5UL/GoctU5Cm4ER1eB7XN7p9WsamFJd/yf2KpltkjEyiTuplv0yAbdjl1KX3vKmEQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux",
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/cli-linux-arm64": {
|
||||
"version": "2.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.39.1.tgz",
|
||||
"integrity": "sha512-5VbVJDatolDrWOgaffsEM7znjs0cR8bHt9Bq0mStM3tBolgAeSDHE89NgHggfZR+DJ2VWOy4vgCwkObrUD6NQw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux",
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/cli-linux-i686": {
|
||||
"version": "2.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.39.1.tgz",
|
||||
"integrity": "sha512-pXWVoKXCRrY7N8vc9H7mETiV9ZCz+zSnX65JQCzZxgYrayQPJTc+NPRnZTdYdk5RlAupXaFicBI2GwOCRqVRkg==",
|
||||
"cpu": [
|
||||
"x86",
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux",
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/cli-linux-x64": {
|
||||
"version": "2.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.39.1.tgz",
|
||||
"integrity": "sha512-IwayNZy+it7FWG4M9LayyUmG1a/8kT9+/IEm67sT5+7dkMIMcpmHDqL8rWcPojOXuTKaOBBjkVdNMBTXy0mXlA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux",
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/cli-win32-i686": {
|
||||
"version": "2.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.39.1.tgz",
|
||||
"integrity": "sha512-NglnNoqHSmE+Dz/wHeIVRnV2bLMx7tIn3IQ8vXGO5HWA2f8zYJGktbkLq1Lg23PaQmeZLPGlja3gBQfZYSG10Q==",
|
||||
"cpu": [
|
||||
"x86",
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/cli-win32-x64": {
|
||||
"version": "2.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.39.1.tgz",
|
||||
"integrity": "sha512-xv0R2CMf/X1Fte3cMWie1NXuHmUyQPDBfCyIt6k6RPFPxAYUgcqgMPznYwVMwWEA1W43PaOkSn3d8ZylsDaETw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/bundler-plugin-core/node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@sentry/bundler-plugin-core/node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/cli": {
|
||||
"version": "2.42.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.42.2.tgz",
|
||||
@@ -5692,96 +5520,52 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/core": {
|
||||
"version": "7.120.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.120.3.tgz",
|
||||
"integrity": "sha512-vyy11fCGpkGK3qI5DSXOjgIboBZTriw0YDx/0KyX5CjIjDDNgp5AGgpgFkfZyiYiaU2Ww3iFuKo4wHmBusz1uA==",
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.3.0.tgz",
|
||||
"integrity": "sha512-SxQ4z7wTkfguvYb2ctNEMU9kVAbhl9ymfjhLnrvtygTwL5soLqAKdco/lX/4P9K9Osgb2Dl6urQWRl+AhzKVbQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/types": "7.120.3",
|
||||
"@sentry/utils": "7.120.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/integrations": {
|
||||
"version": "7.120.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.120.3.tgz",
|
||||
"integrity": "sha512-6i/lYp0BubHPDTg91/uxHvNui427df9r17SsIEXa2eKDwQ9gW2qRx5IWgvnxs2GV/GfSbwcx4swUB3RfEWrXrQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "7.120.3",
|
||||
"@sentry/types": "7.120.3",
|
||||
"@sentry/utils": "7.120.3",
|
||||
"localforage": "^1.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/react": {
|
||||
"version": "7.120.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.120.3.tgz",
|
||||
"integrity": "sha512-BcpoK9dwblfb20xwjn/1DRtplvPEXFc3XCRkYSnTfnfZNU8yPOcVX4X2X0I8R+/gsg+MWiFOdEtXJ3FqpJiJ4Q==",
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-9.3.0.tgz",
|
||||
"integrity": "sha512-/ruDHBHLDXmZoEHNCSjdekZr9+0pbOC5+BY1oABGoDXRISGyoenOBtAsX8TsaC9oJYhr16yKDFlYxzzQRhxDyg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/browser": "7.120.3",
|
||||
"@sentry/core": "7.120.3",
|
||||
"@sentry/types": "7.120.3",
|
||||
"@sentry/utils": "7.120.3",
|
||||
"@sentry/browser": "9.3.0",
|
||||
"@sentry/core": "9.3.0",
|
||||
"hoist-non-react-statics": "^3.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "15.x || 16.x || 17.x || 18.x"
|
||||
"react": "^16.14.0 || 17.x || 18.x || 19.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/replay": {
|
||||
"version": "7.120.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.120.3.tgz",
|
||||
"integrity": "sha512-CjVq1fP6bpDiX8VQxudD5MPWwatfXk8EJ2jQhJTcWu/4bCSOQmHxnnmBM+GVn5acKUBCodWHBN+IUZgnJheZSg==",
|
||||
"node_modules/@sentry/vite-plugin": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-3.2.2.tgz",
|
||||
"integrity": "sha512-WSkHOhZszMrIE9zmx2l4JhMnMlZmN/yAoHyf59pwFLIMctuZak6lNPbTbIFkFHDzIJ9Nut5RAVsw1qjmWc1PTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry-internal/tracing": "7.120.3",
|
||||
"@sentry/core": "7.120.3",
|
||||
"@sentry/types": "7.120.3",
|
||||
"@sentry/utils": "7.120.3"
|
||||
"@sentry/bundler-plugin-core": "3.2.2",
|
||||
"unplugin": "1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/types": {
|
||||
"version": "7.120.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.120.3.tgz",
|
||||
"integrity": "sha512-C4z+3kGWNFJ303FC+FxAd4KkHvxpNFYAFN8iMIgBwJdpIl25KZ8Q/VdGn0MLLUEHNLvjob0+wvwlcRBBNLXOow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/utils": {
|
||||
"version": "7.120.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.120.3.tgz",
|
||||
"integrity": "sha512-UDAOQJtJDxZHQ5Nm1olycBIsz2wdGX8SdzyGVHmD8EOQYAeDZQyIlQYohDe9nazdIOQLZCIc3fU0G9gqVLkaGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/types": "7.120.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/webpack-plugin": {
|
||||
"version": "2.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-2.23.0.tgz",
|
||||
"integrity": "sha512-WxYUbTt/tNfeDm9apeUDXXKs6bEuuVrgYJeCDPDzjGQdmLTsZnbLxcX/b+zr4pceyzZNFEJujk60rRWCjFZY3w==",
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-3.2.2.tgz",
|
||||
"integrity": "sha512-6OkVKNOjKk8P9j7oh6svZ+kEP1i9YIHBC2aGWL2XsgeZTIrMBxJAXtOf+qSrfMAxEtibSroGVOMQc/y3WJTQtg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/bundler-plugin-core": "2.23.0",
|
||||
"@sentry/bundler-plugin-core": "3.2.2",
|
||||
"unplugin": "1.0.1",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
@@ -6665,7 +6449,6 @@
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
@@ -6844,7 +6627,6 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
@@ -6858,7 +6640,6 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@@ -6881,9 +6662,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/apollo-link-sentry": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/apollo-link-sentry/-/apollo-link-sentry-3.3.0.tgz",
|
||||
"integrity": "sha512-wLffWmo5sRw3rHN1Ck6azM0oxObvtaBBf3AC8cLX4SxhyjmkRIagGDji6CFkyAhxupPz0b9/H1u4Ocx+63lNug==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/apollo-link-sentry/-/apollo-link-sentry-4.1.0.tgz",
|
||||
"integrity": "sha512-wHiJXZ9OzmRbV3famykszz0E0SZyCBpa3Rt0EVrdOKDN9qQQaE63xQB2lFa3mUkZb95GMc+6ugPt8bVIkwsRPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"deepmerge": "^4.2.2",
|
||||
@@ -6893,7 +6674,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apollo/client": "^3.2.3",
|
||||
"@sentry/browser": "^7.41.0",
|
||||
"@sentry/core": "^8.33.0",
|
||||
"graphql": "15 - 16"
|
||||
}
|
||||
},
|
||||
@@ -7457,7 +7238,6 @@
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -7520,7 +7300,6 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
@@ -10678,7 +10457,6 @@
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -10710,7 +10488,6 @@
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^6.0.0",
|
||||
@@ -10867,7 +10644,6 @@
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -11038,7 +10814,6 @@
|
||||
"version": "9.3.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz",
|
||||
"integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
@@ -11070,7 +10845,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
@@ -11080,7 +10854,6 @@
|
||||
"version": "8.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz",
|
||||
"integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
@@ -11570,12 +11343,6 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
|
||||
@@ -11842,7 +11609,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
@@ -11985,7 +11751,6 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -12039,7 +11804,6 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
@@ -12116,7 +11880,6 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
@@ -12728,15 +12491,6 @@
|
||||
"integrity": "sha512-vLmhg7Gan7idyAKfc6pvCtNzvar4/eIzrVVk3hjNFH5+fGqyjD0gQRovdTrDl20wsmZhBtmZpcsR0tOfquwb8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
|
||||
"integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lines-and-columns": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
@@ -12788,20 +12542,10 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/localforage": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
|
||||
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"lie": "3.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^5.0.0"
|
||||
@@ -13091,7 +12835,6 @@
|
||||
"version": "0.30.8",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
|
||||
"integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||
@@ -13910,7 +13653,6 @@
|
||||
"version": "4.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz",
|
||||
"integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -14091,7 +13833,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -14399,7 +14140,6 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"yocto-queue": "^0.1.0"
|
||||
@@ -14415,7 +14155,6 @@
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
|
||||
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^3.0.2"
|
||||
@@ -14562,7 +14301,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -14597,7 +14335,6 @@
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
||||
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^10.2.0",
|
||||
@@ -14614,14 +14351,12 @@
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/path-scurry/node_modules/minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
@@ -18119,7 +17854,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
@@ -18655,7 +18389,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.0.1.tgz",
|
||||
"integrity": "sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.8.1",
|
||||
@@ -18668,7 +18401,6 @@
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
@@ -18693,7 +18425,6 @@
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
@@ -18706,7 +18437,6 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@@ -18719,7 +18449,6 @@
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
@@ -19263,7 +18992,6 @@
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
|
||||
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
@@ -19273,7 +19001,6 @@
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz",
|
||||
"integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/websocket-driver": {
|
||||
@@ -19887,7 +19614,6 @@
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
|
||||
@@ -15,13 +15,14 @@
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.6.0",
|
||||
"@sentry/cli": "^2.42.2",
|
||||
"@sentry/react": "^7.114.0",
|
||||
"@sentry/react": "^9.3.0",
|
||||
"@sentry/vite-plugin": "^3.2.2",
|
||||
"@splitsoftware/splitio-react": "^1.13.0",
|
||||
"@tanem/react-nprogress": "^5.0.53",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"antd": "^5.24.2",
|
||||
"apollo-link-logger": "^2.0.1",
|
||||
"apollo-link-sentry": "^3.3.0",
|
||||
"apollo-link-sentry": "^4.1.0",
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.8.1",
|
||||
"classnames": "^2.5.1",
|
||||
@@ -98,8 +99,7 @@
|
||||
"test": "cypress open",
|
||||
"eject": "react-scripts eject",
|
||||
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .",
|
||||
"eulaize": "node src/utils/eulaize.js",
|
||||
"sentry:sourcemaps:imex": "sentry-cli sourcemaps inject --org imex --project imexonline ./build && sentry-cli sourcemaps upload --org imex --project imexonline ./build"
|
||||
"eulaize": "node src/utils/eulaize.js"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -127,7 +127,7 @@
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@sentry/webpack-plugin": "^2.22.4",
|
||||
"@sentry/webpack-plugin": "^3.2.2",
|
||||
"@testing-library/cypress": "^10.0.2",
|
||||
"browserslist": "^4.24.4",
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
|
||||
@@ -21,7 +21,7 @@ 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";
|
||||
import { SocketProvider } from "../contexts/SocketIO/useSocket.jsx";
|
||||
import { NotificationProvider } from "../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
|
||||
@@ -201,7 +201,7 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
||||
path="/manage/*"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<SocketProvider bodyshop={bodyshop} navigate={navigate}>
|
||||
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}>
|
||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||
</SocketProvider>
|
||||
</ErrorBoundary>
|
||||
@@ -213,7 +213,7 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
||||
path="/tech/*"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<SocketProvider bodyshop={bodyshop} navigate={navigate}>
|
||||
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}>
|
||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||
</SocketProvider>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -180,3 +180,13 @@
|
||||
.muted-button:hover {
|
||||
color: darkgrey;
|
||||
}
|
||||
|
||||
.notification-alert-unordered-list {
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
.notification-alert-unordered-list-item {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getToken } from "@firebase/messaging";
|
||||
import axios from "axios";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSocket } from "../../contexts/SocketIO/socketContext";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { messaging, requestForToken } from "../../firebase/firebase.utils";
|
||||
import ChatPopupComponent from "../chat-popup/chat-popup.component";
|
||||
import "./chat-affix.styles.scss";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Button } from "antd";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TOGGLE_CONVERSATION_ARCHIVE } from "../../graphql/conversations.queries";
|
||||
import { useSocket } from "../../contexts/SocketIO/socketContext.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Link } from "react-router-dom";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import { useSocket } from "../../contexts/SocketIO/socketContext.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
@@ -3,7 +3,7 @@ import axios from "axios";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useSocket } from "../../contexts/SocketIO/socketContext";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { CONVERSATION_SUBSCRIPTION_BY_PK, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries";
|
||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Input, Spin, Tag, Tooltip } from "antd";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UPDATE_CONVERSATION_LABEL } from "../../graphql/conversations.queries";
|
||||
import { useSocket } from "../../contexts/SocketIO/socketContext.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
||||
import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
|
||||
import { useSocket } from "../../contexts/SocketIO/socketContext.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
|
||||
@@ -7,7 +7,7 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { searchingForConversation } from "../../redux/messaging/messaging.selectors";
|
||||
import { useSocket } from "../../contexts/SocketIO/socketContext.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
|
||||
@@ -12,7 +12,7 @@ import ChatConversationListComponent from "../chat-conversation-list/chat-conver
|
||||
import ChatConversationContainer from "../chat-conversation/chat-conversation.container";
|
||||
import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import { useSocket } from "../../contexts/SocketIO/socketContext.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
|
||||
import "./chat-popup.styles.scss";
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||
import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries";
|
||||
import ChatTagRo from "./chat-tag-ro.component";
|
||||
import { useSocket } from "../../contexts/SocketIO/socketContext.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
@@ -123,7 +123,7 @@ class ErrorBoundary extends React.Component {
|
||||
<Row>
|
||||
<Col offset={6} span={12}>
|
||||
<Collapse bordered={false}>
|
||||
<Collapse.Panel header={t("general.labels.errors")}>
|
||||
<Collapse.Panel key="errors-panel" header={t("general.labels.errors")}>
|
||||
<div>
|
||||
<strong>{this.state.error.message}</strong>
|
||||
</div>
|
||||
|
||||
@@ -78,9 +78,7 @@ const Eula = ({ currentEula, currentUser, acceptEula }) => {
|
||||
} catch (err) {
|
||||
notification.error({
|
||||
message: t("eula.errors.acceptance.message"),
|
||||
description: t("eula.errors.acceptance.description"),
|
||||
placement: "bottomRight",
|
||||
duration: 5000
|
||||
description: t("eula.errors.acceptance.description")
|
||||
});
|
||||
console.log(`${t("eula.errors.acceptance.message")}`);
|
||||
console.dir({
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
import Icon, {
|
||||
import { Badge, Layout, Menu, Spin } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import NotificationCenterContainer from "../notification-center/notification-center.container.jsx";
|
||||
import LockWrapper from "../lock-wrapper/lock-wrapper.component";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import {
|
||||
BankFilled,
|
||||
BarChartOutlined,
|
||||
BellFilled,
|
||||
@@ -26,35 +38,21 @@ import Icon, {
|
||||
UnorderedListOutlined,
|
||||
UserOutlined
|
||||
} from "@ant-design/icons";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Badge, Layout, Menu, Space, Spin } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BsKanban } from "react-icons/bs";
|
||||
import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar, FaTasks } from "react-icons/fa";
|
||||
import { FiLogOut } from "react-icons/fi";
|
||||
import { GiPayMoney, GiPlayerTime, GiSettingsKnobs } from "react-icons/gi";
|
||||
import { IoBusinessOutline } from "react-icons/io5";
|
||||
import { RiSurveyLine } from "react-icons/ri";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js";
|
||||
import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { signOutStart } from "../../redux/user/user.actions";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import LockWrapper from "../lock-wrapper/lock-wrapper.component";
|
||||
import { useState, useEffect } from "react";
|
||||
import { debounce } from "lodash";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js";
|
||||
import NotificationCenterContainer from "../notification-center/notification-center.container.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/socketContext.jsx";
|
||||
|
||||
// Used to Determine if the Header is in Mobile Mode, and to toggle the multiple menus
|
||||
const HEADER_MOBILE_BREAKPOINT = 576;
|
||||
import day from "../../utils/day.js";
|
||||
|
||||
// Redux mappings
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
recentItems: selectRecentItems,
|
||||
@@ -63,43 +61,13 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBillEnterContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "billEnter"
|
||||
})
|
||||
),
|
||||
setTimeTicketContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "timeTicket"
|
||||
})
|
||||
),
|
||||
setPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "payment" })),
|
||||
setReportCenterContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "reportCenter"
|
||||
})
|
||||
),
|
||||
setBillEnterContext: (context) => dispatch(setModalContext({ context, modal: "billEnter" })),
|
||||
setTimeTicketContext: (context) => dispatch(setModalContext({ context, modal: "timeTicket" })),
|
||||
setPaymentContext: (context) => dispatch(setModalContext({ context, modal: "payment" })),
|
||||
setReportCenterContext: (context) => dispatch(setModalContext({ context, modal: "reportCenter" })),
|
||||
signOutStart: () => dispatch(signOutStart()),
|
||||
setCardPaymentContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "cardPayment"
|
||||
})
|
||||
),
|
||||
setTaskUpsertContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "taskUpsert"
|
||||
})
|
||||
)
|
||||
setCardPaymentContext: (context) => dispatch(setModalContext({ context, modal: "cardPayment" })),
|
||||
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
||||
});
|
||||
|
||||
function Header({
|
||||
@@ -125,10 +93,10 @@ function Header({
|
||||
});
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { isConnected } = useSocket();
|
||||
const { isConnected, scenarioNotificationsOn } = useSocket();
|
||||
const [notificationVisible, setNotificationVisible] = useState(false);
|
||||
|
||||
const baseTitleRef = useRef(document.title || "");
|
||||
const lastSetTitleRef = useRef("");
|
||||
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
||||
|
||||
const {
|
||||
@@ -138,57 +106,68 @@ function Header({
|
||||
} = useQuery(GET_UNREAD_COUNT, {
|
||||
variables: { associationid: userAssociationId },
|
||||
fetchPolicy: "network-only",
|
||||
pollInterval: isConnected ? 0 : 30000, // Poll only if socket is down
|
||||
skip: !userAssociationId // Skip query if no userAssociationId
|
||||
pollInterval: isConnected ? 0 : day.duration(60, "seconds").asMilliseconds(),
|
||||
skip: !userAssociationId || !scenarioNotificationsOn
|
||||
});
|
||||
|
||||
const unreadCount = unreadData?.notifications_aggregate?.aggregate?.count ?? 0;
|
||||
|
||||
// Initial fetch and socket status handling
|
||||
useEffect(() => {
|
||||
if (userAssociationId) {
|
||||
refetchUnread().catch((e) => console.error(`Something went wrong fetching unread notifications: ${e?.message}`));
|
||||
refetchUnread().catch((e) => console.error(`Error fetching unread notifications: ${e?.message}`));
|
||||
}
|
||||
}, [refetchUnread, userAssociationId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isConnected && !unreadLoading && userAssociationId) {
|
||||
refetchUnread().catch((e) => console.error(`Something went wrong fetching unread notifications: ${e?.message}`));
|
||||
refetchUnread().catch((e) => console.error(`Error fetching unread notifications: ${e?.message}`));
|
||||
}
|
||||
}, [isConnected, unreadLoading, refetchUnread, userAssociationId]);
|
||||
|
||||
// Keep The unread count in the title.
|
||||
useEffect(() => {
|
||||
const updateTitle = () => {
|
||||
const currentTitle = document.title;
|
||||
// Check if the current title differs from what we last set
|
||||
if (currentTitle !== lastSetTitleRef.current) {
|
||||
// Extract base title by removing any unread count prefix
|
||||
const baseTitleMatch = currentTitle.match(/^\(\d+\)\s*(.*)$/);
|
||||
baseTitleRef.current = baseTitleMatch ? baseTitleMatch[1] : currentTitle;
|
||||
}
|
||||
|
||||
// Apply unread count to the base title
|
||||
const newTitle = unreadCount > 0 ? `(${unreadCount}) ${baseTitleRef.current}` : baseTitleRef.current;
|
||||
|
||||
// Only update if the title has changed to avoid unnecessary DOM writes
|
||||
if (document.title !== newTitle) {
|
||||
document.title = newTitle;
|
||||
lastSetTitleRef.current = newTitle; // Store what we set
|
||||
}
|
||||
};
|
||||
|
||||
// Initial update
|
||||
updateTitle();
|
||||
|
||||
// Poll every 100ms to catch child component changes
|
||||
const interval = setInterval(updateTitle, 100);
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
document.title = baseTitleRef.current; // Reset to base title on unmount
|
||||
};
|
||||
}, [unreadCount]); // Re-run when unreadCount changes
|
||||
|
||||
const handleNotificationClick = (e) => {
|
||||
setNotificationVisible(!notificationVisible);
|
||||
if (handleMenuClick) handleMenuClick(e);
|
||||
};
|
||||
|
||||
const [isMobile, setIsMobile] = useState(() => {
|
||||
const effectiveWidth = window.innerWidth / (window.devicePixelRatio || 1);
|
||||
return effectiveWidth <= HEADER_MOBILE_BREAKPOINT;
|
||||
});
|
||||
|
||||
const handleResize = debounce(() => {
|
||||
const effectiveWidth = window.innerWidth / (window.devicePixelRatio || 1);
|
||||
setIsMobile(effectiveWidth <= HEADER_MOBILE_BREAKPOINT);
|
||||
}, 200);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("resize", handleResize);
|
||||
window.addEventListener("orientationchange", handleResize);
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
window.removeEventListener("orientationchange", handleResize);
|
||||
handleResize.cancel(); // Cancel any pending debounced calls on cleanup
|
||||
};
|
||||
}, [handleResize]);
|
||||
|
||||
// Accounting children setup (unchanged)
|
||||
const accountingChildren = [];
|
||||
accountingChildren.push(
|
||||
const accountingChildren = [
|
||||
{
|
||||
key: "bills",
|
||||
id: "header-accounting-bills",
|
||||
icon: <Icon component={FaFileInvoiceDollar} />,
|
||||
icon: <FaFileInvoiceDollar />,
|
||||
label: (
|
||||
<Link to="/manage/bills">
|
||||
<LockWrapper featureName="bills" bodyshop={bodyshop}>
|
||||
@@ -200,42 +179,31 @@ function Header({
|
||||
{
|
||||
key: "enterbills",
|
||||
id: "header-accounting-enterbills",
|
||||
icon: <Icon component={GiPayMoney} />,
|
||||
icon: <GiPayMoney />,
|
||||
label: (
|
||||
<Space>
|
||||
<LockWrapper featureName="bills" bodyshop={bodyshop}>
|
||||
{t("menus.header.enterbills")}
|
||||
</LockWrapper>
|
||||
</Space>
|
||||
<LockWrapper featureName="bills" bodyshop={bodyshop}>
|
||||
{t("menus.header.enterbills")}
|
||||
</LockWrapper>
|
||||
),
|
||||
onClick: () => {
|
||||
onClick: () =>
|
||||
HasFeatureAccess({ featureName: "bills", bodyshop }) &&
|
||||
setBillEnterContext({
|
||||
actions: {},
|
||||
context: {}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (Simple_Inventory.treatment === "on") {
|
||||
accountingChildren.push(
|
||||
{
|
||||
type: "divider"
|
||||
},
|
||||
{
|
||||
key: "inventory",
|
||||
id: "header-accounting-inventory",
|
||||
icon: <Icon component={FaFileInvoiceDollar} />,
|
||||
label: <Link to="/manage/inventory">{t("menus.header.inventory")}</Link>
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
accountingChildren.push(
|
||||
{
|
||||
type: "divider"
|
||||
setBillEnterContext({
|
||||
actions: {},
|
||||
context: {}
|
||||
})
|
||||
},
|
||||
...(Simple_Inventory.treatment === "on"
|
||||
? [
|
||||
{ type: "divider" },
|
||||
{
|
||||
key: "inventory",
|
||||
id: "header-accounting-inventory",
|
||||
icon: <FaFileInvoiceDollar />,
|
||||
label: <Link to="/manage/inventory">{t("menus.header.inventory")}</Link>
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{ type: "divider" },
|
||||
{
|
||||
key: "allpayments",
|
||||
id: "header-accounting-allpayments",
|
||||
@@ -251,41 +219,31 @@ function Header({
|
||||
{
|
||||
key: "enterpayments",
|
||||
id: "header-accounting-enterpayments",
|
||||
icon: <Icon component={FaCreditCard} />,
|
||||
icon: <FaCreditCard />,
|
||||
label: (
|
||||
<LockWrapper featureName="payments" bodyshop={bodyshop}>
|
||||
{t("menus.header.enterpayment")}
|
||||
</LockWrapper>
|
||||
),
|
||||
onClick: () => {
|
||||
onClick: () =>
|
||||
HasFeatureAccess({ featureName: "payments", bodyshop }) &&
|
||||
setPaymentContext({
|
||||
actions: {},
|
||||
context: null
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (ImEXPay.treatment === "on") {
|
||||
accountingChildren.push({
|
||||
key: "entercardpayments",
|
||||
id: "header-accounting-entercardpayments",
|
||||
icon: <Icon component={FaCreditCard} />,
|
||||
label: t("menus.header.entercardpayment"),
|
||||
onClick: () => {
|
||||
setCardPaymentContext({
|
||||
setPaymentContext({
|
||||
actions: {},
|
||||
context: {}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
accountingChildren.push(
|
||||
{
|
||||
type: "divider"
|
||||
context: null
|
||||
})
|
||||
},
|
||||
...(ImEXPay.treatment === "on"
|
||||
? [
|
||||
{
|
||||
key: "entercardpayments",
|
||||
id: "header-accounting-entercardpayments",
|
||||
icon: <FaCreditCard />,
|
||||
label: t("menus.header.entercardpayment"),
|
||||
onClick: () => setCardPaymentContext({ actions: {}, context: {} })
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{ type: "divider" },
|
||||
{
|
||||
key: "timetickets",
|
||||
id: "header-accounting-timetickets",
|
||||
@@ -297,133 +255,124 @@ function Header({
|
||||
</LockWrapper>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
if (bodyshop?.md_tasks_presets?.use_approvals) {
|
||||
accountingChildren.push({
|
||||
key: "ttapprovals",
|
||||
id: "header-accounting-ttapprovals",
|
||||
icon: <FieldTimeOutlined />,
|
||||
label: <Link to="/manage/ttapprovals">{t("menus.header.ttapprovals")}</Link>
|
||||
});
|
||||
}
|
||||
accountingChildren.push(
|
||||
},
|
||||
...(bodyshop?.md_tasks_presets?.use_approvals
|
||||
? [
|
||||
{
|
||||
key: "ttapprovals",
|
||||
id: "header-accounting-ttapprovals",
|
||||
icon: <FieldTimeOutlined />,
|
||||
label: <Link to="/manage/ttapprovals">{t("menus.header.ttapprovals")}</Link>
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: "entertimetickets",
|
||||
icon: <Icon component={GiPlayerTime} />,
|
||||
id: "header-accounting-entertimetickets",
|
||||
icon: <GiPlayerTime />,
|
||||
label: (
|
||||
<LockWrapper featureName="timetickets" bodyshop={bodyshop}>
|
||||
{t("menus.header.entertimeticket")}
|
||||
</LockWrapper>
|
||||
),
|
||||
id: "header-accounting-entertimetickets",
|
||||
onClick: () => {
|
||||
onClick: () =>
|
||||
HasFeatureAccess({ featureName: "timetickets", bodyshop }) &&
|
||||
setTimeTicketContext({
|
||||
actions: {},
|
||||
context: {
|
||||
created_by: currentUser.displayName
|
||||
? currentUser.email.concat(" | ", currentUser.displayName)
|
||||
: currentUser.email
|
||||
}
|
||||
});
|
||||
}
|
||||
setTimeTicketContext({
|
||||
actions: {},
|
||||
context: {
|
||||
created_by: currentUser.displayName
|
||||
? `${currentUser.email} | ${currentUser.displayName}`
|
||||
: currentUser.email
|
||||
}
|
||||
})
|
||||
},
|
||||
{ type: "divider" },
|
||||
{
|
||||
type: "divider"
|
||||
}
|
||||
);
|
||||
|
||||
const accountingExportChildren = [
|
||||
{
|
||||
key: "receivables",
|
||||
id: "header-accounting-receivables",
|
||||
key: "accountingexport",
|
||||
id: "header-accounting-export",
|
||||
icon: <ExportOutlined />,
|
||||
label: (
|
||||
<Link to="/manage/accounting/receivables">
|
||||
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||
{t("menus.header.accounting-receivables")}
|
||||
</LockWrapper>
|
||||
</Link>
|
||||
)
|
||||
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||
{t("menus.header.export")}
|
||||
</LockWrapper>
|
||||
),
|
||||
children: [
|
||||
{
|
||||
key: "receivables",
|
||||
id: "header-accounting-receivables",
|
||||
label: (
|
||||
<Link to="/manage/accounting/receivables">
|
||||
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||
{t("menus.header.accounting-receivables")}
|
||||
</LockWrapper>
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
...(!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) ||
|
||||
DmsAp.treatment === "on"
|
||||
? [
|
||||
{
|
||||
key: "payables",
|
||||
id: "header-accounting-payables",
|
||||
label: (
|
||||
<Link to="/manage/accounting/payables">
|
||||
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||
{t("menus.header.accounting-payables")}
|
||||
</LockWrapper>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber))
|
||||
? [
|
||||
{
|
||||
key: "payments",
|
||||
id: "header-accounting-payments",
|
||||
label: (
|
||||
<Link to="/manage/accounting/payments">
|
||||
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||
{t("menus.header.accounting-payments")}
|
||||
</LockWrapper>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{ type: "divider" },
|
||||
{
|
||||
key: "exportlogs",
|
||||
id: "header-accounting-exportlogs",
|
||||
label: (
|
||||
<Link to="/manage/accounting/exportlogs">
|
||||
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||
{t("menus.header.export-logs")}
|
||||
</LockWrapper>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
if (!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) || DmsAp.treatment === "on") {
|
||||
accountingExportChildren.push({
|
||||
key: "payables",
|
||||
id: "header-accounting-payables",
|
||||
label: (
|
||||
<Link to="/manage/accounting/payables">
|
||||
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||
{t("menus.header.accounting-payables")}
|
||||
</LockWrapper>
|
||||
</Link>
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
if (!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber))) {
|
||||
accountingExportChildren.push({
|
||||
key: "payments",
|
||||
id: "header-accounting-payments",
|
||||
label: (
|
||||
<Link to="/manage/accounting/payments">
|
||||
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||
{t("menus.header.accounting-payments")}
|
||||
</LockWrapper>
|
||||
</Link>
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
accountingExportChildren.push(
|
||||
{
|
||||
type: "divider"
|
||||
},
|
||||
{
|
||||
key: "exportlogs",
|
||||
id: "header-accounting-exportlogs",
|
||||
label: (
|
||||
<Link to="/manage/accounting/exportlogs">
|
||||
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||
{t("menus.header.export-logs")}
|
||||
</LockWrapper>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
accountingChildren.push({
|
||||
key: "accountingexport",
|
||||
id: "header-accounting-export",
|
||||
icon: <ExportOutlined />,
|
||||
label: (
|
||||
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||
{t("menus.header.export")}
|
||||
</LockWrapper>
|
||||
),
|
||||
children: accountingExportChildren
|
||||
});
|
||||
|
||||
// Define all menu items
|
||||
const menuItems = [
|
||||
// Left menu items (includes original navigation items)
|
||||
const leftMenuItems = [
|
||||
{
|
||||
key: "home",
|
||||
icon: <HomeFilled />,
|
||||
id: "header-home",
|
||||
icon: <HomeFilled />,
|
||||
label: <Link to="/manage/">{t("menus.header.home")}</Link>
|
||||
},
|
||||
{
|
||||
key: "schedule",
|
||||
id: "header-schedule",
|
||||
icon: <Icon component={FaCalendarAlt} />,
|
||||
icon: <FaCalendarAlt />,
|
||||
label: <Link to="/manage/schedule">{t("menus.header.schedule")}</Link>
|
||||
},
|
||||
{
|
||||
key: "jobssubmenu",
|
||||
id: "header-jobs",
|
||||
icon: <Icon component={FaCarCrash} />,
|
||||
icon: <FaCarCrash />,
|
||||
label: t("menus.header.jobs"),
|
||||
children: [
|
||||
{
|
||||
@@ -456,20 +405,14 @@ function Header({
|
||||
icon: <FileAddOutlined />,
|
||||
label: <Link to="/manage/jobs/new">{t("menus.header.newjob")}</Link>
|
||||
},
|
||||
{
|
||||
type: "divider",
|
||||
id: "header-jobs-divider"
|
||||
},
|
||||
{ type: "divider" },
|
||||
{
|
||||
key: "alljobs",
|
||||
id: "header-all-jobs",
|
||||
icon: <UnorderedListOutlined />,
|
||||
label: <Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link>
|
||||
},
|
||||
{
|
||||
type: "divider",
|
||||
id: "header-jobs-divider2"
|
||||
},
|
||||
{ type: "divider" },
|
||||
{
|
||||
key: "productionlist",
|
||||
id: "header-production-list",
|
||||
@@ -479,7 +422,7 @@ function Header({
|
||||
{
|
||||
key: "productionboard",
|
||||
id: "header-production-board",
|
||||
icon: <Icon component={BsKanban} />,
|
||||
icon: <BsKanban />,
|
||||
label: (
|
||||
<Link to="/manage/production/board">
|
||||
<LockWrapper featureName="visualboard" bodyshop={bodyshop}>
|
||||
@@ -488,10 +431,7 @@ function Header({
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
{
|
||||
type: "divider",
|
||||
id: "header-jobs-divider3"
|
||||
},
|
||||
{ type: "divider" },
|
||||
{
|
||||
key: "scoreboard",
|
||||
id: "header-scoreboard",
|
||||
@@ -508,8 +448,8 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "customers",
|
||||
icon: <UserOutlined />,
|
||||
id: "header-customers",
|
||||
icon: <UserOutlined />,
|
||||
label: t("menus.header.customers"),
|
||||
children: [
|
||||
{
|
||||
@@ -614,12 +554,7 @@ function Header({
|
||||
id: "header-create-task",
|
||||
icon: <PlusCircleOutlined />,
|
||||
label: t("menus.header.create_task"),
|
||||
onClick: () => {
|
||||
setTaskUpsertContext({
|
||||
actions: {},
|
||||
context: {}
|
||||
});
|
||||
}
|
||||
onClick: () => setTaskUpsertContext({ actions: {}, context: {} })
|
||||
},
|
||||
{
|
||||
key: "mytasks",
|
||||
@@ -644,7 +579,7 @@ function Header({
|
||||
{
|
||||
key: "shop",
|
||||
id: "header-shop",
|
||||
icon: <Icon component={GiSettingsKnobs} />,
|
||||
icon: <GiSettingsKnobs />,
|
||||
label: <Link to="/manage/shop?tab=info">{t("menus.header.shop_config")}</Link>
|
||||
},
|
||||
{
|
||||
@@ -662,23 +597,18 @@ function Header({
|
||||
id: "header-reportcenter",
|
||||
icon: <BarChartOutlined />,
|
||||
label: t("menus.header.reportcenter"),
|
||||
onClick: () => {
|
||||
setReportCenterContext({
|
||||
actions: {},
|
||||
context: {}
|
||||
});
|
||||
}
|
||||
onClick: () => setReportCenterContext({ actions: {}, context: {} })
|
||||
},
|
||||
{
|
||||
key: "shop-vendors",
|
||||
id: "header-shop-vendors",
|
||||
icon: <Icon component={IoBusinessOutline} />,
|
||||
icon: <IoBusinessOutline />,
|
||||
label: <Link to="/manage/shop/vendors">{t("menus.header.shop_vendors")}</Link>
|
||||
},
|
||||
{
|
||||
key: "shop-csi",
|
||||
id: "header-shop-csi",
|
||||
icon: <Icon component={RiSurveyLine} />,
|
||||
icon: <RiSurveyLine />,
|
||||
label: (
|
||||
<Link to="/manage/shop/csi">
|
||||
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||
@@ -689,23 +619,11 @@ function Header({
|
||||
}
|
||||
]
|
||||
},
|
||||
// Right-aligned items on desktop, merged on mobile
|
||||
{
|
||||
key: "notifications",
|
||||
icon: unreadLoading ? (
|
||||
<Spin size="small" />
|
||||
) : (
|
||||
<Badge count={unreadCount}>
|
||||
<BellFilled />
|
||||
</Badge>
|
||||
),
|
||||
id: "header-notifications",
|
||||
onClick: handleNotificationClick
|
||||
},
|
||||
{
|
||||
key: "recent",
|
||||
icon: <ClockCircleFilled />,
|
||||
id: "header-recent",
|
||||
icon: <ClockCircleFilled />,
|
||||
label: t("menus.header.recent"),
|
||||
children: recentItems.map((i, idx) => ({
|
||||
key: idx,
|
||||
id: `header-recent-${idx}`,
|
||||
@@ -714,13 +632,14 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "user",
|
||||
id: "header-user",
|
||||
icon: <UserOutlined />,
|
||||
// label: currentUser.displayName || currentUser.email || t("general.labels.unknown"),
|
||||
label: t("menus.currentuser.profile"),
|
||||
children: [
|
||||
{
|
||||
key: "signout",
|
||||
id: "header-signout",
|
||||
icon: <Icon component={FiLogOut} />,
|
||||
icon: <FiLogOut />,
|
||||
danger: true,
|
||||
label: t("user.actions.signout"),
|
||||
onClick: () => signOutStart()
|
||||
@@ -728,32 +647,25 @@ function Header({
|
||||
{
|
||||
key: "help",
|
||||
id: "header-help",
|
||||
icon: <Icon component={QuestionCircleFilled} />,
|
||||
icon: <QuestionCircleFilled />,
|
||||
label: t("menus.header.help"),
|
||||
onClick: () => {
|
||||
window.open("https://help.imex.online/", "_blank");
|
||||
}
|
||||
onClick: () => window.open("https://help.imex.online/", "_blank")
|
||||
},
|
||||
...(InstanceRenderManager({
|
||||
imex: true,
|
||||
rome: false
|
||||
})
|
||||
...(InstanceRenderManager({ imex: true, rome: false })
|
||||
? [
|
||||
{
|
||||
key: "rescue",
|
||||
id: "header-rescue",
|
||||
icon: <Icon component={CarFilled} />,
|
||||
icon: <CarFilled />,
|
||||
label: t("menus.header.rescueme"),
|
||||
onClick: () => {
|
||||
window.open("https://imexrescue.com/", "_blank");
|
||||
}
|
||||
onClick: () => window.open("https://imexrescue.com/", "_blank")
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: "shiftclock",
|
||||
id: "header-shiftclock",
|
||||
icon: <Icon component={GiPlayerTime} />,
|
||||
icon: <GiPlayerTime />,
|
||||
label: (
|
||||
<Link to="/manage/shiftclock">
|
||||
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||
@@ -772,57 +684,68 @@ function Header({
|
||||
}
|
||||
];
|
||||
|
||||
// Notifications item (always on the right)
|
||||
const notificationItem = scenarioNotificationsOn
|
||||
? [
|
||||
{
|
||||
key: "notifications",
|
||||
id: "header-notifications",
|
||||
icon: unreadLoading ? (
|
||||
<Spin size="small" />
|
||||
) : (
|
||||
<Badge offset={[8, 0]} size="small" count={unreadCount}>
|
||||
<BellFilled />
|
||||
</Badge>
|
||||
),
|
||||
onClick: handleNotificationClick
|
||||
}
|
||||
]
|
||||
: [];
|
||||
|
||||
return (
|
||||
<Layout.Header style={{ padding: 0 }}>
|
||||
{isMobile ? (
|
||||
<Layout.Header style={{ padding: 0, background: "#001529" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
mode="horizontal"
|
||||
theme="dark"
|
||||
selectedKeys={[selectedHeader]}
|
||||
onClick={handleMenuClick}
|
||||
subMenuCloseDelay={0.3}
|
||||
items={menuItems}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
items={leftMenuItems}
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
width: "100%"
|
||||
flex: "1 1 auto",
|
||||
minWidth: 0,
|
||||
overflowX: "auto",
|
||||
borderBottom: "none",
|
||||
background: "transparent"
|
||||
}}
|
||||
>
|
||||
/>
|
||||
{scenarioNotificationsOn && (
|
||||
<Menu
|
||||
mode="horizontal"
|
||||
theme="dark"
|
||||
selectedKeys={[selectedHeader]}
|
||||
onClick={handleMenuClick}
|
||||
subMenuCloseDelay={0.3}
|
||||
items={menuItems.slice(0, -3)}
|
||||
style={{
|
||||
flex: "0 1 auto",
|
||||
justifyContent: "flex-start",
|
||||
minWidth: 0,
|
||||
overflow: "visible"
|
||||
}}
|
||||
items={notificationItem}
|
||||
style={{ flex: "0 0 auto", minWidth: 0, borderBottom: "none", background: "transparent" }}
|
||||
/>
|
||||
<div style={{ flex: "1 0 0" }} />
|
||||
<Menu
|
||||
mode="horizontal"
|
||||
theme="dark"
|
||||
selectedKeys={[selectedHeader]}
|
||||
onClick={handleMenuClick}
|
||||
subMenuCloseDelay={0.3}
|
||||
items={menuItems.slice(-3)}
|
||||
style={{
|
||||
flex: "0 0 auto",
|
||||
justifyContent: "flex-end",
|
||||
overflow: "visible"
|
||||
}}
|
||||
/>
|
||||
<NotificationCenterContainer visible={notificationVisible} onClose={() => setNotificationVisible(false)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{scenarioNotificationsOn && (
|
||||
<NotificationCenterContainer
|
||||
visible={notificationVisible}
|
||||
onClose={() => setNotificationVisible(false)}
|
||||
unreadCount={unreadCount}
|
||||
/>
|
||||
)}
|
||||
</Layout.Header>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useSocket } from "../../contexts/SocketIO/socketContext.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
|
||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
|
||||
@@ -216,7 +216,7 @@ export function JobCloseRoGuardContainer({ job, jobRO, bodyshop, form }) {
|
||||
</Form.Item>
|
||||
</Collapse.Panel>
|
||||
|
||||
<Collapse.Panel header={t("jobs.labels.performance")}>
|
||||
<Collapse.Panel key="job-performance" header={t("jobs.labels.performance")}>
|
||||
<Row gutter={[32, 32]}>
|
||||
<Col className="ro-guard-col" span={24}>
|
||||
<JobCloseRoGuardTtLifecycle job={job} />
|
||||
|
||||
@@ -21,6 +21,8 @@ import JobDetailCardsInsuranceComponent from "./job-detail-cards.insurance.compo
|
||||
import JobDetailCardsNotesComponent from "./job-detail-cards.notes.component";
|
||||
import JobDetailCardsPartsComponent from "./job-detail-cards.parts.component";
|
||||
import JobDetailCardsTotalsComponent from "./job-detail-cards.totals.component";
|
||||
import JobWatcherToggleContainer from "../job-watcher-toggle/job-watcher-toggle.container.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -37,6 +39,7 @@ const span = {
|
||||
};
|
||||
|
||||
export function JobDetailCards({ bodyshop, setPrintCenterContext }) {
|
||||
const { scenarioNotificationsOn } = useSocket();
|
||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||
.filter((screen) => !!screen[1])
|
||||
.slice(-1)[0];
|
||||
@@ -78,7 +81,12 @@ export function JobDetailCards({ bodyshop, setPrintCenterContext }) {
|
||||
{data ? (
|
||||
<Card
|
||||
title={
|
||||
<Link to={`/manage/jobs/${data.jobs_by_pk.id}`}>{data.jobs_by_pk.ro_number || t("general.labels.na")}</Link>
|
||||
<Space>
|
||||
{scenarioNotificationsOn && <JobWatcherToggleContainer job={data.jobs_by_pk} />}
|
||||
<Link to={`/manage/jobs/${data.jobs_by_pk.id}`}>
|
||||
{data.jobs_by_pk.ro_number || t("general.labels.na")}
|
||||
</Link>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Space wrap>
|
||||
@@ -122,7 +130,11 @@ export function JobDetailCards({ bodyshop, setPrintCenterContext }) {
|
||||
</Col>
|
||||
{!bodyshop.uselocalmediaserver && (
|
||||
<Col {...span}>
|
||||
<JobDetailCardsDocumentsComponent loading={loading} data={data ? data.jobs_by_pk : null} bodyshop={bodyshop} />
|
||||
<JobDetailCardsDocumentsComponent
|
||||
loading={loading}
|
||||
data={data ? data.jobs_by_pk : null}
|
||||
bodyshop={bodyshop}
|
||||
/>
|
||||
</Col>
|
||||
)}
|
||||
<Col {...span}>
|
||||
|
||||
@@ -69,7 +69,7 @@ export function JobsTotalsTableComponent({ jobRO, currentUser, job }) {
|
||||
<Card title="DEVELOPMENT USE ONLY">
|
||||
<JobCalculateTotals job={job} disabled={jobRO} />
|
||||
<Collapse>
|
||||
<Collapse.Panel header="JSON Tree Totals">
|
||||
<Collapse.Panel key="json-totals" header="JSON Tree Totals">
|
||||
<div>
|
||||
<pre>
|
||||
{JSON.stringify(
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import React from "react";
|
||||
import { EyeFilled, EyeOutlined, UserOutlined } from "@ant-design/icons";
|
||||
import { Avatar, Button, Divider, List, Popover, Select, Tooltip, Typography } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import EmployeeSearchSelectComponent from "../../components/employee-search-select/employee-search-select.component.jsx";
|
||||
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component.jsx";
|
||||
import { BiSolidTrash } from "react-icons/bi";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export default function JobWatcherToggleComponent({
|
||||
jobWatchers,
|
||||
isWatching,
|
||||
watcherLoading,
|
||||
adding,
|
||||
removing,
|
||||
open,
|
||||
setOpen,
|
||||
selectedWatcher,
|
||||
setSelectedWatcher,
|
||||
selectedTeam,
|
||||
bodyshop,
|
||||
Enhanced_Payroll,
|
||||
handleToggleSelf,
|
||||
handleRemoveWatcher,
|
||||
handleWatcherSelect,
|
||||
handleTeamSelect
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleRenderItem = (watcher) => {
|
||||
// Check if watcher is defined and has user_email
|
||||
if (!watcher || !watcher.user_email) {
|
||||
return null; // Skip rendering invalid watchers
|
||||
}
|
||||
|
||||
const employee = bodyshop?.employees?.find((e) => e.user_email === watcher.user_email);
|
||||
const displayName = employee ? `${employee.first_name} ${employee.last_name}` : watcher.user_email;
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Button
|
||||
type="default"
|
||||
danger
|
||||
size="medium"
|
||||
icon={<BiSolidTrash />}
|
||||
onClick={() => handleRemoveWatcher(watcher.user_email)}
|
||||
disabled={adding || removing} // Optional: Disable button during mutations
|
||||
>
|
||||
{t("notifications.actions.remove")}
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
avatar={<Avatar icon={<UserOutlined />} />}
|
||||
title={<Text>{displayName}</Text>}
|
||||
description={watcher.user_email}
|
||||
/>
|
||||
</List.Item>
|
||||
);
|
||||
};
|
||||
|
||||
const popoverContent = (
|
||||
<div style={{ width: "30em" }}>
|
||||
<List>
|
||||
<List.Item
|
||||
actions={[
|
||||
<Button
|
||||
type={isWatching ? "primary" : "default"}
|
||||
danger={!isWatching}
|
||||
icon={isWatching ? <EyeOutlined /> : <EyeFilled />}
|
||||
size="medium"
|
||||
onClick={handleToggleSelf}
|
||||
loading={adding || removing}
|
||||
>
|
||||
{isWatching ? t("notifications.labels.unwatch") : t("notifications.labels.watch")}
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta>
|
||||
<Text type="secondary" style={{ marginBottom: 8, display: "block" }}>
|
||||
{t("notifications.labels.watching-issue")}
|
||||
</Text>
|
||||
</List.Item.Meta>
|
||||
</List.Item>
|
||||
</List>
|
||||
{watcherLoading ? (
|
||||
<LoadingSpinner />
|
||||
) : jobWatchers && jobWatchers.length > 0 ? (
|
||||
<List dataSource={jobWatchers} renderItem={handleRenderItem} />
|
||||
) : (
|
||||
<Text type="secondary">{t("notifications.labels.no-watchers")}</Text>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
<Text type="secondary">{t("notifications.labels.add-watchers")}</Text>
|
||||
<EmployeeSearchSelectComponent
|
||||
style={{ minWidth: "100%" }}
|
||||
options={
|
||||
bodyshop?.employees?.filter((e) =>
|
||||
jobWatchers.every((w) => w.user_email !== e.user_email && e.active && e.user_email)
|
||||
) || []
|
||||
}
|
||||
placeholder={t("notifications.labels.employee-search")}
|
||||
value={selectedWatcher}
|
||||
onChange={(value) => {
|
||||
setSelectedWatcher(value);
|
||||
handleWatcherSelect(value);
|
||||
}}
|
||||
/>
|
||||
{Enhanced_Payroll && bodyshop?.employee_teams?.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<Text type="secondary">{t("notifications.labels.add-watchers-team")}</Text>
|
||||
<Select
|
||||
showSearch
|
||||
style={{ minWidth: "100%" }}
|
||||
placeholder={t("notifications.labels.teams-search")}
|
||||
value={selectedTeam}
|
||||
onChange={handleTeamSelect}
|
||||
options={
|
||||
bodyshop?.employee_teams?.map((team) => {
|
||||
const teamMembers = team.employee_team_members
|
||||
.map((member) => {
|
||||
const employee = bodyshop?.employees?.find((e) => e.id === member.employeeid);
|
||||
return employee?.user_email && employee?.active ? employee.user_email : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
return {
|
||||
value: JSON.stringify(teamMembers),
|
||||
label: team.name
|
||||
};
|
||||
}) || []
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover placement="rightTop" content={popoverContent} trigger="click" open={open} onOpenChange={setOpen}>
|
||||
<Tooltip title={t("notifications.tooltips.job-watchers")}>
|
||||
<Button
|
||||
shape="circle"
|
||||
type={isWatching ? "primary" : "default"}
|
||||
icon={isWatching ? <EyeFilled /> : <EyeOutlined />}
|
||||
loading={watcherLoading}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { ADD_JOB_WATCHER, GET_JOB_WATCHERS, REMOVE_JOB_WATCHER } from "../../graphql/jobs.queries.js";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import JobWatcherToggleComponent from "./job-watcher-toggle.component.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
|
||||
function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
||||
const {
|
||||
treatments: { Enhanced_Payroll }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Enhanced_Payroll"],
|
||||
splitKey: bodyshop && bodyshop.imexshopid
|
||||
});
|
||||
|
||||
const userEmail = currentUser.email;
|
||||
const jobid = job.id;
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedWatcher, setSelectedWatcher] = useState(null);
|
||||
const [selectedTeam, setSelectedTeam] = useState(null);
|
||||
|
||||
// Fetch current watchers with refetch capability
|
||||
const {
|
||||
data: watcherData,
|
||||
loading: watcherLoading,
|
||||
refetch
|
||||
} = useQuery(GET_JOB_WATCHERS, {
|
||||
variables: { jobid },
|
||||
fetchPolicy: "cache-and-network" // Ensure fresh data from server
|
||||
});
|
||||
|
||||
// Refetch jobWatchers when the popover opens (open changes to true)
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
refetch().catch((err) =>
|
||||
console.error(`Something went wrong fetching Notification Watchers on popover open: ${err?.message}`, {
|
||||
stack: err?.stack
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [open, refetch]);
|
||||
|
||||
const jobWatchers = useMemo(() => (watcherData?.job_watchers ? [...watcherData.job_watchers] : []), [watcherData]);
|
||||
const isWatching = jobWatchers.some((w) => w.user_email === userEmail);
|
||||
|
||||
const [addWatcher, { loading: adding }] = useMutation(ADD_JOB_WATCHER, {
|
||||
onCompleted: () =>
|
||||
refetch().catch((err) =>
|
||||
console.error(`Something went wrong fetching Notification Watchers after add: ${err?.message}`, {
|
||||
stack: err?.stack
|
||||
})
|
||||
),
|
||||
onError: (err) => {
|
||||
if (err.graphQLErrors && err.graphQLErrors.length > 0) {
|
||||
const errorMessage = err.graphQLErrors[0].message;
|
||||
if (
|
||||
errorMessage.includes("Uniqueness violation") ||
|
||||
errorMessage.includes("idx_job_watchers_jobid_user_email_unique")
|
||||
) {
|
||||
console.warn("Watcher already exists for this job and user.");
|
||||
refetch().catch((err) =>
|
||||
console.error(
|
||||
`Something went wrong fetching Notification Watchers after uniqueness violation: ${err?.message}`,
|
||||
{ stack: err?.stack }
|
||||
)
|
||||
); // Sync with server to ensure UI reflects actual state
|
||||
} else {
|
||||
console.error(`Error adding job watcher: ${errorMessage}`);
|
||||
}
|
||||
} else {
|
||||
console.error(`Unexpected error adding job watcher: ${err.message || JSON.stringify(err)}`);
|
||||
}
|
||||
},
|
||||
update(cache, { data }) {
|
||||
if (!data || !data.insert_job_watchers_one) {
|
||||
console.warn("No data or insert_job_watchers_one returned from mutation, skipping cache update.");
|
||||
refetch().catch((err) =>
|
||||
console.error(`Something went wrong updating Notification Watchers after add: ${err?.message}`, {
|
||||
stack: err?.stack
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const insert_job_watchers_one = data.insert_job_watchers_one;
|
||||
const existingData = cache.readQuery({
|
||||
query: GET_JOB_WATCHERS,
|
||||
variables: { jobid }
|
||||
});
|
||||
|
||||
cache.writeQuery({
|
||||
query: GET_JOB_WATCHERS,
|
||||
variables: { jobid },
|
||||
data: {
|
||||
...existingData,
|
||||
job_watchers: [...(existingData?.job_watchers || []), insert_job_watchers_one]
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const [removeWatcher, { loading: removing }] = useMutation(REMOVE_JOB_WATCHER, {
|
||||
onCompleted: () =>
|
||||
refetch().catch((err) =>
|
||||
console.error(`Something went wrong fetching Notification Watchers after remove: ${err?.message}`, {
|
||||
stack: err?.stack
|
||||
})
|
||||
), // Refetch to sync with server after success
|
||||
onError: (err) => console.error(`Error removing job watcher: ${err.message}`),
|
||||
update(cache, { data: { delete_job_watchers } }) {
|
||||
const existingData = cache.readQuery({
|
||||
query: GET_JOB_WATCHERS,
|
||||
variables: { jobid }
|
||||
});
|
||||
|
||||
const deletedWatcher = delete_job_watchers.returning[0];
|
||||
const updatedWatchers = deletedWatcher
|
||||
? (existingData?.job_watchers || []).filter((watcher) => watcher.user_email !== deletedWatcher.user_email)
|
||||
: existingData?.job_watchers || [];
|
||||
|
||||
cache.writeQuery({
|
||||
query: GET_JOB_WATCHERS,
|
||||
variables: { jobid },
|
||||
data: {
|
||||
...existingData,
|
||||
job_watchers: updatedWatchers
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const handleToggleSelf = useCallback(async () => {
|
||||
if (adding || removing) return;
|
||||
if (isWatching) {
|
||||
await removeWatcher({ variables: { jobid, userEmail } });
|
||||
} else {
|
||||
await addWatcher({ variables: { jobid, userEmail } });
|
||||
}
|
||||
}, [isWatching, addWatcher, removeWatcher, jobid, userEmail, adding, removing]);
|
||||
|
||||
const handleRemoveWatcher = useCallback(
|
||||
async (email) => {
|
||||
if (removing) return;
|
||||
await removeWatcher({ variables: { jobid, userEmail: email } });
|
||||
},
|
||||
[removeWatcher, jobid, removing]
|
||||
);
|
||||
|
||||
const handleWatcherSelect = useCallback(
|
||||
async (selectedUser) => {
|
||||
if (adding || removing) return;
|
||||
const employee = bodyshop.employees.find((e) => e.id === selectedUser);
|
||||
if (!employee) return;
|
||||
|
||||
const email = employee.user_email;
|
||||
const isAlreadyWatching = jobWatchers.some((w) => w.user_email === email);
|
||||
|
||||
if (isAlreadyWatching) {
|
||||
await handleRemoveWatcher(email);
|
||||
} else {
|
||||
await addWatcher({ variables: { jobid, userEmail: email } });
|
||||
}
|
||||
setSelectedWatcher(null);
|
||||
},
|
||||
[jobWatchers, addWatcher, handleRemoveWatcher, jobid, bodyshop, adding, removing]
|
||||
);
|
||||
|
||||
const handleTeamSelect = useCallback(
|
||||
async (team) => {
|
||||
if (adding) return;
|
||||
const selectedTeamMembers = JSON.parse(team);
|
||||
const newWatchers = selectedTeamMembers.filter(
|
||||
(email) => !jobWatchers.some((watcher) => watcher.user_email === email)
|
||||
);
|
||||
|
||||
if (newWatchers.length === 0) {
|
||||
console.warn("All selected team members are already watchers.");
|
||||
setSelectedTeam(null);
|
||||
return;
|
||||
}
|
||||
await Promise.all(newWatchers.map((email) => addWatcher({ variables: { jobid, userEmail: email } })));
|
||||
},
|
||||
[jobWatchers, addWatcher, jobid, adding]
|
||||
);
|
||||
|
||||
return (
|
||||
<JobWatcherToggleComponent
|
||||
jobWatchers={jobWatchers}
|
||||
isWatching={isWatching}
|
||||
watcherLoading={watcherLoading}
|
||||
adding={adding}
|
||||
removing={removing}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
selectedWatcher={selectedWatcher}
|
||||
setSelectedWatcher={setSelectedWatcher}
|
||||
selectedTeam={selectedTeam}
|
||||
setSelectedTeam={setSelectedTeam}
|
||||
bodyshop={bodyshop}
|
||||
Enhanced_Payroll={Enhanced_Payroll}
|
||||
handleToggleSelf={handleToggleSelf}
|
||||
handleRemoveWatcher={handleRemoveWatcher}
|
||||
handleWatcherSelect={handleWatcherSelect}
|
||||
handleTeamSelect={handleTeamSelect}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(JobWatcherToggleContainer);
|
||||
@@ -9,7 +9,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useSocket } from "../../contexts/SocketIO/socketContext.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
|
||||
import { GET_CURRENT_QUESTIONSET_ID, INSERT_CSI } from "../../graphql/csi.queries";
|
||||
|
||||
@@ -1,94 +1,122 @@
|
||||
// notification-center.component.jsx
|
||||
import React from "react";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { Button, Checkbox, List, Badge, Typography, Alert } from "antd";
|
||||
import { Badge, Button, Space, Spin, Switch, Tooltip, Typography } from "antd";
|
||||
import { CheckCircleFilled, CheckCircleOutlined, EyeFilled, EyeOutlined } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import "./notification-center.styles.scss";
|
||||
import { Link } from "react-router-dom";
|
||||
import day from "../../utils/day.js";
|
||||
import { forwardRef, useRef, useEffect } from "react";
|
||||
import { DateTimeFormat } from "../../utils/DateFormatter.jsx";
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
const NotificationCenterComponent = ({
|
||||
visible,
|
||||
onClose,
|
||||
notifications,
|
||||
loading,
|
||||
error,
|
||||
showUnreadOnly,
|
||||
toggleUnreadOnly,
|
||||
markAllRead,
|
||||
loadMore,
|
||||
onNotificationClick
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
/**
|
||||
* Notification Center Component
|
||||
* @type {React.ForwardRefExoticComponent<React.PropsWithoutRef<{readonly visible?: *, readonly onClose?: *, readonly notifications?: *, readonly loading?: *, readonly showUnreadOnly?: *, readonly toggleUnreadOnly?: *, readonly markAllRead?: *, readonly loadMore?: *, readonly onNotificationClick?: *, readonly unreadCount?: *}> & React.RefAttributes<unknown>>}
|
||||
*/
|
||||
const NotificationCenterComponent = forwardRef(
|
||||
(
|
||||
{
|
||||
visible,
|
||||
onClose,
|
||||
notifications,
|
||||
loading,
|
||||
showUnreadOnly,
|
||||
toggleUnreadOnly,
|
||||
markAllRead,
|
||||
loadMore,
|
||||
onNotificationClick,
|
||||
unreadCount
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const virtuosoRef = useRef(null);
|
||||
|
||||
const renderNotification = (index, notification) => {
|
||||
return (
|
||||
<List.Item
|
||||
key={`${notification.id}-${index}`}
|
||||
className={notification.read ? "notification-read" : "notification-unread"}
|
||||
onClick={() => !notification.read && onNotificationClick(notification.id)}
|
||||
>
|
||||
<Badge dot={!notification.read}>
|
||||
<div>
|
||||
<Title
|
||||
level={5}
|
||||
style={{
|
||||
margin: "0 0 8px 0",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
to={`/manage/jobs/${notification.jobid}`}
|
||||
target="_blank"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // Prevent List.Item click handler from firing
|
||||
!notification.read && onNotificationClick(notification.id); // Mark as read when link clicked
|
||||
}}
|
||||
>
|
||||
RO #{notification.roNumber}
|
||||
</Link>
|
||||
<Text type="secondary">{new Date(notification.created_at).toLocaleString()}</Text>
|
||||
</Title>
|
||||
<Text strong={!notification.read}>
|
||||
<ul>
|
||||
{notification.scenarioText.map((text, idx) => (
|
||||
<li key={`${notification.id}-${idx}`}>{text}</li>
|
||||
))}
|
||||
</ul>
|
||||
</Text>
|
||||
</div>
|
||||
</Badge>
|
||||
</List.Item>
|
||||
);
|
||||
};
|
||||
// Scroll to top when showUnreadOnly changes
|
||||
useEffect(() => {
|
||||
if (virtuosoRef.current) {
|
||||
virtuosoRef.current.scrollToIndex({ index: 0, behavior: "smooth" });
|
||||
}
|
||||
}, [showUnreadOnly]);
|
||||
|
||||
return (
|
||||
<div className={`notification-center ${visible ? "visible" : ""}`}>
|
||||
<div className="notification-header">
|
||||
<h3>{t("notifications.labels.notification-center")}</h3>
|
||||
<div className="notification-controls">
|
||||
<Checkbox checked={showUnreadOnly} onChange={(e) => toggleUnreadOnly(e.target.checked)}>
|
||||
{t("notifications.labels.show-unread-only")}
|
||||
</Checkbox>
|
||||
<Button type="link" onClick={markAllRead} disabled={!notifications.some((n) => !n.read)}>
|
||||
{t("notifications.labels.mark-all-read")}
|
||||
</Button>
|
||||
const renderNotification = (index, notification) => {
|
||||
const handleClick = () => {
|
||||
if (!notification.read) {
|
||||
onNotificationClick(notification.id);
|
||||
}
|
||||
navigate(`/manage/jobs/${notification.jobid}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${notification.id}-${index}`}
|
||||
className={`notification-item ${notification.read ? "notification-read" : "notification-unread"}`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Badge dot={!notification.read}>
|
||||
<div className="notification-content">
|
||||
<Title level={5} className="notification-title">
|
||||
<span className="ro-number">
|
||||
{t("notifications.labels.ro-number", { ro_number: notification.roNumber || t("general.labels.na") })}
|
||||
</span>
|
||||
<Text type="secondary" className="relative-time" title={DateTimeFormat(notification.created_at)}>
|
||||
{day(notification.created_at).fromNow()}
|
||||
</Text>
|
||||
</Title>
|
||||
<Text strong={!notification.read} className="notification-body">
|
||||
<ul>
|
||||
{notification.scenarioText.map((text, idx) => (
|
||||
<li key={`${notification.id}-${idx}`}>{text}</li>
|
||||
))}
|
||||
</ul>
|
||||
</Text>
|
||||
</div>
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`notification-center ${visible ? "visible" : ""}`} ref={ref}>
|
||||
<div className="notification-header">
|
||||
<Space direction="horizontal">
|
||||
<h3>{t("notifications.labels.notification-center")}</h3>
|
||||
{loading && <Spin spinning={loading} size="small"></Spin>}
|
||||
</Space>
|
||||
<div className="notification-controls">
|
||||
<Tooltip title={t("notifications.labels.show-unread-only")}>
|
||||
<Space size={4} align="center" className="notification-toggle">
|
||||
{showUnreadOnly ? (
|
||||
<EyeFilled className="notification-toggle-icon" />
|
||||
) : (
|
||||
<EyeOutlined className="notification-toggle-icon" />
|
||||
)}
|
||||
<Switch checked={showUnreadOnly} onChange={(checked) => toggleUnreadOnly(checked)} size="small" />
|
||||
</Space>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("notifications.labels.mark-all-read")}>
|
||||
<Button
|
||||
type="link"
|
||||
icon={!unreadCount ? <CheckCircleFilled /> : <CheckCircleOutlined />}
|
||||
onClick={markAllRead}
|
||||
disabled={!unreadCount}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
style={{ height: "400px", width: "100%" }}
|
||||
data={notifications}
|
||||
totalCount={notifications.length}
|
||||
endReached={loadMore}
|
||||
itemContent={renderNotification}
|
||||
/>
|
||||
</div>
|
||||
{error && <Alert message="Error" description={error} type="error" closable onClose={() => onClose()} />}
|
||||
<Virtuoso
|
||||
style={{ height: "400px", width: "100%" }}
|
||||
data={notifications}
|
||||
totalCount={notifications.length}
|
||||
endReached={loadMore}
|
||||
itemContent={renderNotification}
|
||||
/>
|
||||
{loading && !error && <div>{t("notifications.labels.loading")}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default NotificationCenterComponent;
|
||||
|
||||
@@ -1,17 +1,31 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { connect } from "react-redux";
|
||||
import NotificationCenterComponent from "./notification-center.component";
|
||||
import { GET_NOTIFICATIONS } from "../../graphql/notifications.queries";
|
||||
import { INITIAL_NOTIFICATIONS, useSocket } from "../../contexts/SocketIO/socketContext.jsx";
|
||||
import { INITIAL_NOTIFICATIONS, useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||
import day from "../../utils/day.js";
|
||||
|
||||
export function NotificationCenterContainer({ visible, onClose, bodyshop }) {
|
||||
// This will be used to poll for notifications when the socket is disconnected
|
||||
const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
|
||||
|
||||
/**
|
||||
* Notification Center Container
|
||||
* @param visible
|
||||
* @param onClose
|
||||
* @param bodyshop
|
||||
* @param unreadCount
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }) => {
|
||||
const [showUnreadOnly, setShowUnreadOnly] = useState(false);
|
||||
const [notifications, setNotifications] = useState([]);
|
||||
const [error, setError] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { isConnected, markNotificationRead, markAllNotificationsRead } = useSocket();
|
||||
const notificationRef = useRef(null);
|
||||
|
||||
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
||||
|
||||
@@ -26,8 +40,7 @@ export function NotificationCenterContainer({ visible, onClose, bodyshop }) {
|
||||
const {
|
||||
data,
|
||||
fetchMore,
|
||||
loading,
|
||||
error: queryError,
|
||||
loading: queryLoading,
|
||||
refetch
|
||||
} = useQuery(GET_NOTIFICATIONS, {
|
||||
variables: {
|
||||
@@ -37,15 +50,26 @@ export function NotificationCenterContainer({ visible, onClose, bodyshop }) {
|
||||
},
|
||||
fetchPolicy: "cache-and-network",
|
||||
notifyOnNetworkStatusChange: true,
|
||||
pollInterval: isConnected ? 0 : 30000,
|
||||
pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(),
|
||||
skip: !userAssociationId,
|
||||
onError: (err) => {
|
||||
setError(err.message);
|
||||
console.error("GET_NOTIFICATIONS error:", err);
|
||||
setTimeout(() => refetch(), 2000);
|
||||
console.error(`Error polling Notifications: ${err?.message || ""}`);
|
||||
setTimeout(() => refetch(), day.duration(2, "seconds").asMilliseconds());
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
// Prevent open + close behavior from the header
|
||||
if (event.target.closest("#header-notifications")) return;
|
||||
if (visible && notificationRef.current && !notificationRef.current.contains(event.target)) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, [visible, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.notifications) {
|
||||
const processedNotifications = data.notifications
|
||||
@@ -77,18 +101,12 @@ export function NotificationCenterContainer({ visible, onClose, bodyshop }) {
|
||||
})
|
||||
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||
setNotifications(processedNotifications);
|
||||
setError(null);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (queryError) {
|
||||
setError(queryError.message);
|
||||
}
|
||||
}, [queryError]);
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
if (!loading && data?.notifications.length) {
|
||||
if (!queryLoading && data?.notifications.length) {
|
||||
setIsLoading(true); // Show spinner during fetchMore
|
||||
fetchMore({
|
||||
variables: { offset: data.notifications.length, where: whereClause },
|
||||
updateQuery: (prev, { fetchMoreResult }) => {
|
||||
@@ -97,18 +115,20 @@ export function NotificationCenterContainer({ visible, onClose, bodyshop }) {
|
||||
notifications: [...prev.notifications, ...fetchMoreResult.notifications]
|
||||
};
|
||||
}
|
||||
}).catch((err) => {
|
||||
setError(err.message);
|
||||
console.error("Fetch more error:", err);
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Fetch more error:", err);
|
||||
})
|
||||
.finally(() => setIsLoading(false)); // Hide spinner when done
|
||||
}
|
||||
}, [data?.notifications?.length, fetchMore, loading, whereClause]);
|
||||
}, [data?.notifications?.length, fetchMore, queryLoading, whereClause]);
|
||||
|
||||
const handleToggleUnreadOnly = (value) => {
|
||||
setShowUnreadOnly(value);
|
||||
};
|
||||
|
||||
const handleMarkAllRead = useCallback(() => {
|
||||
setIsLoading(true);
|
||||
markAllNotificationsRead()
|
||||
.then(() => {
|
||||
const timestamp = new Date().toISOString();
|
||||
@@ -121,53 +141,59 @@ export function NotificationCenterContainer({ visible, onClose, bodyshop }) {
|
||||
}
|
||||
: notif
|
||||
);
|
||||
return [...updatedNotifications];
|
||||
// Filter out read notifications if in unread only mode
|
||||
return showUnreadOnly ? updatedNotifications.filter((notif) => !notif.read) : updatedNotifications;
|
||||
});
|
||||
})
|
||||
.catch((e) => console.error(`Error marking all notifications read: ${e?.message || ""}`));
|
||||
}, [markAllNotificationsRead, userAssociationId]);
|
||||
.catch((e) => console.error(`Error marking all notifications read: ${e?.message || ""}`))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [markAllNotificationsRead, userAssociationId, showUnreadOnly]);
|
||||
|
||||
const handleNotificationClick = useCallback(
|
||||
(notificationId) => {
|
||||
markNotificationRead({
|
||||
variables: { id: notificationId }
|
||||
})
|
||||
setIsLoading(true);
|
||||
markNotificationRead({ variables: { id: notificationId } })
|
||||
.then(() => {
|
||||
const timestamp = new Date().toISOString();
|
||||
setNotifications((prev) => {
|
||||
return prev.map((notif) =>
|
||||
const updatedNotifications = prev.map((notif) =>
|
||||
notif.id === notificationId && !notif.read ? { ...notif, read: timestamp } : notif
|
||||
);
|
||||
// Filter out the read notification if in unread only mode
|
||||
return showUnreadOnly ? updatedNotifications.filter((notif) => !notif.read) : updatedNotifications;
|
||||
});
|
||||
})
|
||||
.catch((e) => console.error(`Error marking notification read: ${e?.message || ""}`));
|
||||
.catch((e) => console.error(`Error marking notification read: ${e?.message || ""}`))
|
||||
.finally(() => setIsLoading(false));
|
||||
},
|
||||
[markNotificationRead]
|
||||
[markNotificationRead, showUnreadOnly]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && !isConnected) {
|
||||
refetch().catch(
|
||||
(err) => `Something went wrong re-fetching notifications in the notification-center: ${err?.message || ""}`
|
||||
);
|
||||
setIsLoading(true);
|
||||
refetch()
|
||||
.catch((err) => console.error(`Error re-fetching notifications: ${err?.message || ""}`))
|
||||
.finally(() => setIsLoading(false));
|
||||
}
|
||||
}, [visible, isConnected, refetch]);
|
||||
|
||||
return (
|
||||
<NotificationCenterComponent
|
||||
ref={notificationRef}
|
||||
visible={visible}
|
||||
onClose={onClose}
|
||||
notifications={notifications}
|
||||
loading={loading}
|
||||
error={error}
|
||||
loading={isLoading}
|
||||
showUnreadOnly={showUnreadOnly}
|
||||
toggleUnreadOnly={handleToggleUnreadOnly}
|
||||
markAllRead={handleMarkAllRead}
|
||||
loadMore={loadMore}
|
||||
onNotificationClick={handleNotificationClick}
|
||||
unreadCount={unreadCount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
|
||||
@@ -2,112 +2,174 @@
|
||||
position: absolute;
|
||||
top: 64px;
|
||||
right: 0;
|
||||
//width: 600px;
|
||||
background: #fff; /* White background, Ant’s default */
|
||||
color: rgba(0, 0, 0, 0.85); /* Primary text color in Ant 5 */
|
||||
border: 1px solid #d9d9d9; /* Neutral gray border */
|
||||
border-radius: 6px; /* Slightly larger radius per Ant 5 */
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08), 0 3px 6px rgba(0, 0, 0, 0.06); /* Subtle Ant 5 shadow */
|
||||
width: 400px;
|
||||
max-width: 400px;
|
||||
background: #fff;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08), 0 3px 6px rgba(0, 0, 0, 0.06);
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
overflow-x: hidden;
|
||||
|
||||
&.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.notification-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f0f0f0; /* Light gray border from Ant 5 */
|
||||
padding: 4px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #fafafa; /* Light gray background for header */
|
||||
background: #fafafa;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: rgba(0, 0, 0, 0.85); /* Primary text color */
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.notification-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
gap: 8px;
|
||||
|
||||
.ant-checkbox-wrapper {
|
||||
color: rgba(0, 0, 0, 0.85); /* Match Ant’s text color */
|
||||
// Styles for the eye icon and switch (custom classes)
|
||||
.notification-toggle {
|
||||
align-items: center; // Ensure vertical alignment
|
||||
}
|
||||
|
||||
.ant-btn-link {
|
||||
color: #1677ff; /* Ant 5 primary blue */
|
||||
&:hover {
|
||||
color: #69b1ff; /* Lighter blue on hover */
|
||||
.notification-toggle-icon {
|
||||
font-size: 14px;
|
||||
color: #1677ff;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ant-switch {
|
||||
&.ant-switch-small {
|
||||
min-width: 28px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
|
||||
.ant-switch-handle {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
&.ant-switch-checked {
|
||||
background-color: #1677ff;
|
||||
.ant-switch-handle {
|
||||
left: calc(100% - 14px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Styles for the "Mark All Read" button (restore original link button style)
|
||||
.ant-btn-link {
|
||||
padding: 0;
|
||||
color: #1677ff;
|
||||
|
||||
&:hover {
|
||||
color: #69b1ff;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: rgba(0, 0, 0, 0.25); /* Disabled text color from Ant 5 */
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #0958d9;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notification-read {
|
||||
background: #fff; /* White background for read items */
|
||||
color: rgba(0, 0, 0, 0.65); /* Secondary text color */
|
||||
background: #fff;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.notification-unread {
|
||||
background: #f5f5f5; /* Very light gray for unread items */
|
||||
color: rgba(0, 0, 0, 0.85); /* Primary text color */
|
||||
background: #f5f5f5;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.ant-list {
|
||||
overflow: auto; /* Match Virtuoso’s default scrolling behavior */
|
||||
max-height: 100%; /* Allow full height, let Virtuoso handle virtualization */
|
||||
}
|
||||
.notification-item {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: block;
|
||||
overflow: visible;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
|
||||
.ant-list-item {
|
||||
padding: 2px 16px;
|
||||
border-bottom: 1px solid #f0f0f0; /* Light gray border */
|
||||
display: block; /* Ensure visibility */
|
||||
overflow: visible; /* Prevent clipping within items */
|
||||
min-height: 80px; /* Minimum height for multi-line content */
|
||||
|
||||
.ant-typography {
|
||||
color: inherit; /* Inherit from parent (read/unread) */
|
||||
&:hover {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.ant-typography-secondary {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45); /* Ant 5 secondary text color */
|
||||
.notification-content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ant-badge-dot {
|
||||
background: #ff4d4f; /* Keep red dot for unread, consistent with Ant */
|
||||
}
|
||||
|
||||
ul {
|
||||
.notification-title {
|
||||
margin: 0;
|
||||
padding-left: 20px; /* Standard list padding */
|
||||
list-style-type: disc; /* Ensure bullet points */
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.ro-number {
|
||||
margin: 0;
|
||||
color: #1677ff;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.relative-time {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 4px; /* Space between list items */
|
||||
.notification-body {
|
||||
margin-top: 4px;
|
||||
|
||||
.ant-typography {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-badge {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ant-alert {
|
||||
margin: 8px;
|
||||
background: #fff1f0; /* Light red background for error per Ant 5 */
|
||||
background: #fff1f0;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
border: 1px solid #ffa39e; /* Light red border */
|
||||
border: 1px solid #ffa39e;
|
||||
|
||||
.ant-alert-message {
|
||||
color: #ff4d4f; /* Red text for message */
|
||||
}
|
||||
|
||||
.ant-alert-description {
|
||||
color: rgba(0, 0, 0, 0.65); /* Slightly muted description */
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { notificationScenarios } from "../../utils/jobNotificationScenarios.js";
|
||||
import { Checkbox, Form } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* ColumnHeaderCheckbox
|
||||
* @param channel
|
||||
* @param form
|
||||
* @param disabled
|
||||
* @param onHeaderChange
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const ColumnHeaderCheckbox = ({ channel, form, disabled = false, onHeaderChange }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Subscribe to all form values so that this component re-renders on changes.
|
||||
const formValues = Form.useWatch([], form) || {};
|
||||
|
||||
// Determine if all scenarios for this channel are checked.
|
||||
const allChecked =
|
||||
notificationScenarios.length > 0 && notificationScenarios.every((scenario) => formValues[scenario]?.[channel]);
|
||||
|
||||
const onChange = (e) => {
|
||||
const checked = e.target.checked;
|
||||
// Get current form values.
|
||||
const currentValues = form.getFieldsValue();
|
||||
// Update each scenario for this channel.
|
||||
const newValues = { ...currentValues };
|
||||
notificationScenarios.forEach((scenario) => {
|
||||
newValues[scenario] = { ...newValues[scenario], [channel]: checked };
|
||||
});
|
||||
// Update form values.
|
||||
form.setFieldsValue(newValues);
|
||||
// Manually mark the form as dirty.
|
||||
if (onHeaderChange) {
|
||||
onHeaderChange();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Checkbox onChange={onChange} checked={allChecked} disabled={disabled}>
|
||||
{t(`notifications.channels.${channel}`)}
|
||||
</Checkbox>
|
||||
);
|
||||
};
|
||||
|
||||
ColumnHeaderCheckbox.propTypes = {
|
||||
channel: PropTypes.oneOf(["app", "email", "fcm"]).isRequired,
|
||||
form: PropTypes.object.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
onHeaderChange: PropTypes.func
|
||||
};
|
||||
|
||||
export default ColumnHeaderCheckbox;
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, Card, Checkbox, Form, Table } from "antd";
|
||||
import { Button, Card, Checkbox, Form, Space, Table } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
@@ -11,52 +10,21 @@ import { QUERY_NOTIFICATION_SETTINGS, UPDATE_NOTIFICATION_SETTINGS } from "../..
|
||||
import { notificationScenarios } from "../../utils/jobNotificationScenarios.js";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
|
||||
import PropTypes from "prop-types";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import ColumnHeaderCheckbox from "../notification-settings/column-header-checkbox.component.jsx";
|
||||
|
||||
const ColumnHeaderCheckbox = ({ channel, form, disabled = false, onHeaderChange }) => {
|
||||
const { t } = useTranslation();
|
||||
// Subscribe to all form values so that this component re-renders on changes.
|
||||
const formValues = Form.useWatch([], form) || {};
|
||||
|
||||
// Determine if all scenarios for this channel are checked.
|
||||
const allChecked =
|
||||
notificationScenarios.length > 0 && notificationScenarios.every((scenario) => formValues[scenario]?.[channel]);
|
||||
|
||||
const onChange = (e) => {
|
||||
const checked = e.target.checked;
|
||||
// Get current form values.
|
||||
const currentValues = form.getFieldsValue();
|
||||
// Update each scenario for this channel.
|
||||
const newValues = { ...currentValues };
|
||||
notificationScenarios.forEach((scenario) => {
|
||||
newValues[scenario] = { ...newValues[scenario], [channel]: checked };
|
||||
});
|
||||
// Update form values.
|
||||
form.setFieldsValue(newValues);
|
||||
// Manually mark the form as dirty.
|
||||
if (onHeaderChange) {
|
||||
onHeaderChange();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Checkbox onChange={onChange} checked={allChecked} disabled={disabled}>
|
||||
{t(`notifications.channels.${channel}`)}
|
||||
</Checkbox>
|
||||
);
|
||||
};
|
||||
|
||||
ColumnHeaderCheckbox.propTypes = {
|
||||
channel: PropTypes.oneOf(["app", "email", "fcm"]).isRequired,
|
||||
form: PropTypes.object.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
onHeaderChange: PropTypes.func
|
||||
};
|
||||
|
||||
function NotificationSettingsForm({ currentUser }) {
|
||||
/**
|
||||
* Notifications Settings Form
|
||||
* @param currentUser
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const NotificationSettingsForm = ({ currentUser }) => {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const [initialValues, setInitialValues] = useState({});
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
const notification = useNotification();
|
||||
|
||||
// Fetch notification settings.
|
||||
const { loading, error, data } = useQuery(QUERY_NOTIFICATION_SETTINGS, {
|
||||
@@ -88,9 +56,14 @@ function NotificationSettingsForm({ currentUser }) {
|
||||
if (data?.associations?.length > 0) {
|
||||
const userId = data.associations[0].id;
|
||||
// Save the updated notification settings.
|
||||
await updateNotificationSettings({ variables: { id: userId, ns: values } });
|
||||
setInitialValues(values);
|
||||
setIsDirty(false);
|
||||
const result = await updateNotificationSettings({ variables: { id: userId, ns: values } });
|
||||
if (!result?.errors) {
|
||||
notification.success({ message: t("notifications.labels.notification-settings-success") });
|
||||
setInitialValues(values);
|
||||
setIsDirty(false);
|
||||
} else {
|
||||
notification.error({ message: t("notifications.labels.notification-settings-failure") });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -165,21 +138,22 @@ function NotificationSettingsForm({ currentUser }) {
|
||||
<Card
|
||||
title={t("notifications.labels.notificationscenarios")}
|
||||
extra={
|
||||
<>
|
||||
<Button type="default" onClick={handleReset} disabled={!isDirty} style={{ marginRight: 8 }}>
|
||||
<Space>
|
||||
<Button type="default" onClick={handleReset} disabled={!isDirty}>
|
||||
{t("general.actions.clear")}
|
||||
</Button>
|
||||
|
||||
<Button type="primary" htmlType="submit" disabled={!isDirty} loading={saving}>
|
||||
{t("notifications.labels.save")}
|
||||
</Button>
|
||||
</>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Table dataSource={dataSource} columns={columns} pagination={false} bordered rowKey="key" />
|
||||
</Card>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
NotificationSettingsForm.propTypes = {
|
||||
currentUser: PropTypes.shape({
|
||||
@@ -1,17 +1,16 @@
|
||||
import { DeleteFilled, DownOutlined, WarningFilled } from "@ant-design/icons";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Checkbox, Divider, Dropdown, Form, Input, InputNumber, Radio, Select, Space, Tag } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
||||
import PartsOrderModalPriceChange from "./parts-order-modal-price-change.component";
|
||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -33,7 +32,7 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState,
|
||||
});
|
||||
|
||||
const { t } = useTranslation();
|
||||
const handleClick = ({ item, key, keyPath }) => {
|
||||
const handleClick = ({ item }) => {
|
||||
form.setFieldsValue({ comments: item.props.value });
|
||||
};
|
||||
|
||||
@@ -98,17 +97,18 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState,
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item name="order_type" initialValue="parts_order" label={t("parts_orders.labels.order_type")}>
|
||||
<Radio.Group disabled={sendType === "oec"}>
|
||||
<Radio value={"parts_order"}>{t("parts_orders.labels.parts_order")}</Radio>
|
||||
<Radio value={"sublet"}>{t("parts_orders.labels.sublet_order")}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
{!isReturn && (
|
||||
<Form.Item name="order_type" initialValue="parts_order" label={t("parts_orders.labels.order_type")}>
|
||||
<Radio.Group disabled={sendType === "oec"}>
|
||||
<Radio value={"parts_order"}>{t("parts_orders.labels.parts_order")}</Radio>
|
||||
<Radio value={"sublet"}>{t("parts_orders.labels.sublet_order")}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
<Divider orientation="left">{t("parts_orders.labels.inthisorder")}</Divider>
|
||||
<Form.List name={["parts_order_lines", "data"]}>
|
||||
{(fields, { add, remove, move }) => {
|
||||
{(fields, { remove, move }) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
|
||||
@@ -10,7 +10,7 @@ import { createStructuredSelector } from "reselect";
|
||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
||||
import { useSocket } from "../../contexts/SocketIO/socketContext.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
|
||||
@@ -12,7 +12,7 @@ import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import ProductionBoardKanbanComponent from "./production-board-kanban.component";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { useSocket } from "../../contexts/SocketIO/socketContext.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
|
||||
@@ -27,6 +27,8 @@ import ScoreboardAddButton from "../job-scoreboard-add-button/job-scoreboard-add
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import ProductionRemoveButton from "../production-remove-button/production-remove-button.component";
|
||||
import JobWatcherToggleContainer from "../job-watcher-toggle/job-watcher-toggle.container.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -41,6 +43,7 @@ export function ProductionListDetail({ bodyshop, jobs, setPrintCenterContext, te
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const history = useNavigate();
|
||||
const { selected } = search;
|
||||
const { scenarioNotificationsOn } = useSocket();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const theJob = jobs.find((j) => j.id === selected) || {};
|
||||
@@ -60,7 +63,12 @@ export function ProductionListDetail({ bodyshop, jobs, setPrintCenterContext, te
|
||||
<Drawer
|
||||
title={
|
||||
<PageHeader
|
||||
title={theJob.ro_number}
|
||||
title={
|
||||
<Space>
|
||||
{!technician && scenarioNotificationsOn && <JobWatcherToggleContainer job={theJob} />}
|
||||
{theJob.ro_number}
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Space wrap>
|
||||
{!technician ? <ProductionRemoveButton jobId={theJob.id} /> : null}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import ProductionListTable from "./production-list-table.component";
|
||||
import _ from "lodash";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { useSocket } from "../../contexts/SocketIO/socketContext.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
|
||||
export default function ProductionListTableContainer({ bodyshop, subscriptionType = "direct" }) {
|
||||
const client = useApolloClient();
|
||||
|
||||
@@ -39,7 +39,7 @@ export default function ProductionRemoveButton({ jobId }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Button loading={loading} onClick={handleRemoveFromProd} type={"danger"}>
|
||||
<Button loading={loading} onClick={handleRemoveFromProd} type="default" danger>
|
||||
{t("production.actions.remove")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,8 @@ import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import { logImEXEvent, updateCurrentPassword } from "../../firebase/firebase.utils";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import NotificationSettingsForm from "./notification-settings.component.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import NotificationSettingsForm from "../notification-settings/notification-settings-form.component.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser
|
||||
@@ -22,6 +23,7 @@ export default connect(
|
||||
)(function ProfileMyComponent({ currentUser, updateUserDetails }) {
|
||||
const { t } = useTranslation();
|
||||
const notification = useNotification();
|
||||
const { scenarioNotificationsOn } = useSocket();
|
||||
|
||||
const handleFinish = (values) => {
|
||||
logImEXEvent("profile_update");
|
||||
@@ -117,9 +119,11 @@ export default connect(
|
||||
</Card>
|
||||
</Form>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<NotificationSettingsForm />
|
||||
</Col>
|
||||
{scenarioNotificationsOn && (
|
||||
<Col span={24}>
|
||||
<NotificationSettingsForm />
|
||||
</Col>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AlertOutlined } from "@ant-design/icons";
|
||||
import { Alert, Button, Col, Row, Space } from "antd";
|
||||
import i18n from "i18next";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -81,8 +81,7 @@ export function UpdateAlert({ updateAvailable }) {
|
||||
imex: "$t(titles.imexonline)",
|
||||
rome: "$t(titles.romeonline)"
|
||||
})
|
||||
}),
|
||||
placement: "bottomRight"
|
||||
})
|
||||
});
|
||||
}
|
||||
if (needRefresh && timerStarted && timeLeft <= 0) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// NotificationProvider.jsx
|
||||
import React, { createContext, useContext } from "react";
|
||||
import { createContext, useContext } from "react";
|
||||
import { notification } from "antd";
|
||||
|
||||
/**
|
||||
@@ -22,7 +21,11 @@ export const useNotification = () => {
|
||||
* - Provide `api` via the NotificationContext.
|
||||
*/
|
||||
export const NotificationProvider = ({ children }) => {
|
||||
const [api, contextHolder] = notification.useNotification();
|
||||
const [api, contextHolder] = notification.useNotification({
|
||||
placement: "bottomRight",
|
||||
bottom: 70,
|
||||
showProgress: true
|
||||
});
|
||||
|
||||
return (
|
||||
<NotificationContext.Provider value={api}>
|
||||
|
||||
@@ -1,384 +0,0 @@
|
||||
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
||||
import SocketIO from "socket.io-client";
|
||||
import { auth } from "../../firebase/firebase.utils";
|
||||
import { store } from "../../redux/store";
|
||||
import { addAlerts, setWssStatus } from "../../redux/application/application.actions";
|
||||
import client from "../../utils/GraphQLClient";
|
||||
import { useNotification } from "../Notifications/notificationContext.jsx";
|
||||
import {
|
||||
GET_NOTIFICATIONS,
|
||||
GET_UNREAD_COUNT,
|
||||
MARK_ALL_NOTIFICATIONS_READ,
|
||||
MARK_NOTIFICATION_READ
|
||||
} from "../../graphql/notifications.queries.js";
|
||||
import { useMutation } from "@apollo/client";
|
||||
|
||||
const SocketContext = createContext(null);
|
||||
|
||||
// This is how many notifications the database will populate on load, and the increment for load more
|
||||
export const INITIAL_NOTIFICATIONS = 10;
|
||||
|
||||
export const SCENARIO_NOTIFICATION_LOCATION = "bottomRight";
|
||||
export const SCENARIO_NOTIFICATION_DURATION = 15; // Seconds
|
||||
|
||||
export const SocketProvider = ({ children, bodyshop, navigate }) => {
|
||||
const socketRef = useRef(null);
|
||||
const [clientId, setClientId] = useState(null);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const notification = useNotification();
|
||||
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
||||
|
||||
const [markNotificationRead] = useMutation(MARK_NOTIFICATION_READ, {
|
||||
update: (cache, { data: { update_notifications } }) => {
|
||||
const timestamp = new Date().toISOString();
|
||||
const updatedNotification = update_notifications.returning[0];
|
||||
|
||||
// Update the notifications list
|
||||
cache.modify({
|
||||
fields: {
|
||||
notifications(existing = [], { readField }) {
|
||||
return existing.map((notif) => {
|
||||
if (readField("id", notif) === updatedNotification.id) {
|
||||
return { ...notif, read: timestamp };
|
||||
}
|
||||
return notif;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update the unread count in notifications_aggregate
|
||||
const unreadCountQuery = cache.readQuery({
|
||||
query: GET_UNREAD_COUNT,
|
||||
variables: { associationid: userAssociationId }
|
||||
});
|
||||
|
||||
if (unreadCountQuery?.notifications_aggregate?.aggregate?.count > 0) {
|
||||
cache.writeQuery({
|
||||
query: GET_UNREAD_COUNT,
|
||||
variables: { associationid: userAssociationId },
|
||||
data: {
|
||||
notifications_aggregate: {
|
||||
...unreadCountQuery.notifications_aggregate,
|
||||
aggregate: {
|
||||
...unreadCountQuery.notifications_aggregate.aggregate,
|
||||
count: unreadCountQuery.notifications_aggregate.aggregate.count - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error("MARK_NOTIFICATION_READ error in SocketProvider:", err);
|
||||
}
|
||||
});
|
||||
|
||||
const [markAllNotificationsRead] = useMutation(MARK_ALL_NOTIFICATIONS_READ, {
|
||||
variables: { associationid: userAssociationId },
|
||||
update: (cache) => {
|
||||
const timestamp = new Date().toISOString();
|
||||
cache.modify({
|
||||
fields: {
|
||||
notifications(existing = [], { readField }) {
|
||||
return existing.map((notif) => {
|
||||
if (readField("read", notif) === null && readField("associationid", notif) === userAssociationId) {
|
||||
return { ...notif, read: timestamp };
|
||||
}
|
||||
return notif;
|
||||
});
|
||||
},
|
||||
notifications_aggregate() {
|
||||
return { aggregate: { count: 0, __typename: "notifications_aggregate_fields" } };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const baseWhereClause = { associationid: { _eq: userAssociationId } };
|
||||
const cachedNotifications = cache.readQuery({
|
||||
query: GET_NOTIFICATIONS,
|
||||
variables: {
|
||||
limit: INITIAL_NOTIFICATIONS,
|
||||
offset: 0,
|
||||
where: baseWhereClause
|
||||
}
|
||||
});
|
||||
|
||||
if (cachedNotifications?.notifications) {
|
||||
cache.writeQuery({
|
||||
query: GET_NOTIFICATIONS,
|
||||
variables: {
|
||||
limit: INITIAL_NOTIFICATIONS,
|
||||
offset: 0,
|
||||
where: baseWhereClause
|
||||
},
|
||||
data: {
|
||||
notifications: cachedNotifications.notifications.map((notif) =>
|
||||
notif.read === null ? { ...notif, read: timestamp } : notif
|
||||
)
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error("MARK_ALL_NOTIFICATIONS_READ error in SocketProvider:", err);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const initializeSocket = async (token) => {
|
||||
if (!bodyshop || !bodyshop.id || socketRef.current) return;
|
||||
|
||||
const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "";
|
||||
const socketInstance = SocketIO(endpoint, {
|
||||
path: "/wss",
|
||||
withCredentials: true,
|
||||
auth: { token, bodyshopId: bodyshop.id },
|
||||
reconnectionAttempts: Infinity,
|
||||
reconnectionDelay: 2000,
|
||||
reconnectionDelayMax: 10000
|
||||
});
|
||||
|
||||
socketRef.current = socketInstance;
|
||||
|
||||
const handleBodyshopMessage = (message) => {
|
||||
if (!message || !message.type) return;
|
||||
switch (message.type) {
|
||||
case "alert-update":
|
||||
store.dispatch(addAlerts(message.payload));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleConnect = () => {
|
||||
socketInstance.emit("join-bodyshop-room", bodyshop.id);
|
||||
setClientId(socketInstance.id);
|
||||
setIsConnected(true);
|
||||
store.dispatch(setWssStatus("connected"));
|
||||
};
|
||||
|
||||
const handleReconnect = () => {
|
||||
setIsConnected(true);
|
||||
store.dispatch(setWssStatus("connected"));
|
||||
};
|
||||
|
||||
const handleConnectionError = (err) => {
|
||||
console.error("Socket connection error:", err);
|
||||
setIsConnected(false);
|
||||
if (err.message.includes("auth/id-token-expired")) {
|
||||
console.warn("Token expired, refreshing...");
|
||||
auth.currentUser?.getIdToken(true).then((newToken) => {
|
||||
socketInstance.auth = { token: newToken };
|
||||
socketInstance.connect();
|
||||
});
|
||||
} else {
|
||||
store.dispatch(setWssStatus("error"));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisconnect = (reason) => {
|
||||
console.warn("Socket disconnected:", reason);
|
||||
setIsConnected(false);
|
||||
store.dispatch(setWssStatus("disconnected"));
|
||||
if (!socketInstance.connected && reason !== "io server disconnect") {
|
||||
setTimeout(() => {
|
||||
if (socketInstance.disconnected) {
|
||||
console.log("Manually triggering reconnection...");
|
||||
socketInstance.connect();
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNotification = (data) => {
|
||||
const { jobId, jobRoNumber, notificationId, associationId, notifications } = data;
|
||||
|
||||
if (associationId !== userAssociationId) return;
|
||||
|
||||
const newNotification = {
|
||||
__typename: "notifications",
|
||||
id: notificationId,
|
||||
jobid: jobId,
|
||||
associationid: associationId,
|
||||
scenario_text: JSON.stringify(notifications.map((notif) => notif.body)),
|
||||
fcm_text: notifications.map((notif) => notif.body).join(". ") + ".",
|
||||
scenario_meta: JSON.stringify(notifications.map((notif) => notif.variables || {})),
|
||||
created_at: new Date(notifications[0].timestamp).toISOString(),
|
||||
read: null,
|
||||
job: {
|
||||
ro_number: jobRoNumber
|
||||
}
|
||||
};
|
||||
|
||||
const baseVariables = {
|
||||
limit: INITIAL_NOTIFICATIONS,
|
||||
offset: 0,
|
||||
where: { associationid: { _eq: userAssociationId } }
|
||||
};
|
||||
|
||||
try {
|
||||
const existingNotifications =
|
||||
client.cache.readQuery({
|
||||
query: GET_NOTIFICATIONS,
|
||||
variables: baseVariables
|
||||
})?.notifications || [];
|
||||
|
||||
if (existingNotifications.some((n) => n.id === newNotification.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.cache.writeQuery({
|
||||
query: GET_NOTIFICATIONS,
|
||||
variables: baseVariables,
|
||||
data: {
|
||||
notifications: [newNotification, ...existingNotifications].sort(
|
||||
(a, b) => new Date(b.created_at) - new Date(a.created_at)
|
||||
)
|
||||
},
|
||||
broadcast: true
|
||||
});
|
||||
|
||||
const unreadVariables = {
|
||||
...baseVariables,
|
||||
where: { ...baseVariables.where, read: { _is_null: true } }
|
||||
};
|
||||
const unreadNotifications =
|
||||
client.cache.readQuery({
|
||||
query: GET_NOTIFICATIONS,
|
||||
variables: unreadVariables
|
||||
})?.notifications || [];
|
||||
|
||||
if (newNotification.read === null && !unreadNotifications.some((n) => n.id === newNotification.id)) {
|
||||
client.cache.writeQuery({
|
||||
query: GET_NOTIFICATIONS,
|
||||
variables: unreadVariables,
|
||||
data: {
|
||||
notifications: [newNotification, ...unreadNotifications].sort(
|
||||
(a, b) => new Date(b.created_at) - new Date(a.created_at)
|
||||
)
|
||||
},
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
|
||||
client.cache.modify({
|
||||
id: "ROOT_QUERY",
|
||||
fields: {
|
||||
notifications_aggregate(existing = { aggregate: { count: 0 } }) {
|
||||
const isUnread = newNotification.read === null;
|
||||
const countChange = isUnread ? 1 : 0;
|
||||
return {
|
||||
...existing,
|
||||
aggregate: {
|
||||
...existing.aggregate,
|
||||
count: existing.aggregate.count + countChange
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
notification.info({
|
||||
message: `Changes for ${jobRoNumber}:`,
|
||||
description: (
|
||||
<ul
|
||||
className="notification-alert-unorderd-list"
|
||||
onClick={() => {
|
||||
markNotificationRead({ variables: { id: notificationId } })
|
||||
.then(() => navigate(`/manage/jobs/${jobId}`))
|
||||
.catch((e) => console.error(`Error marking notification read from info: ${e?.message || ""}`));
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{notifications.map((notif, index) => (
|
||||
<li className="notification-alert-unorderd-list-item" key={index}>
|
||||
{notif.body}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
),
|
||||
placement: SCENARIO_NOTIFICATION_LOCATION,
|
||||
duration: SCENARIO_NOTIFICATION_DURATION
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Something went wrong handling a new notification: ${error?.message || ""}`);
|
||||
}
|
||||
};
|
||||
|
||||
socketInstance.on("connect", handleConnect);
|
||||
socketInstance.on("reconnect", handleReconnect);
|
||||
socketInstance.on("connect_error", handleConnectionError);
|
||||
socketInstance.on("disconnect", handleDisconnect);
|
||||
socketInstance.on("bodyshop-message", handleBodyshopMessage);
|
||||
socketInstance.on("message", (message) => {
|
||||
try {
|
||||
if (typeof message === "string" && message.startsWith("42")) {
|
||||
const parsedMessage = JSON.parse(message.slice(2));
|
||||
const [event, data] = parsedMessage;
|
||||
if (event === "notification") handleNotification(data);
|
||||
} else if (Array.isArray(message)) {
|
||||
const [event, data] = message;
|
||||
if (event === "notification") handleNotification(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing socket message:", error);
|
||||
}
|
||||
});
|
||||
socketInstance.on("notification", handleNotification);
|
||||
};
|
||||
|
||||
const unsubscribe = auth.onIdTokenChanged(async (user) => {
|
||||
if (user) {
|
||||
const token = await user.getIdToken();
|
||||
if (socketRef.current) {
|
||||
socketRef.current.emit("update-token", { token, bodyshopId: bodyshop.id });
|
||||
} else {
|
||||
initializeSocket(token).catch((err) =>
|
||||
console.error(`Something went wrong Initializing Sockets: ${err?.message || ""}`)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (socketRef.current) {
|
||||
socketRef.current.disconnect();
|
||||
socketRef.current = null;
|
||||
setIsConnected(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
if (socketRef.current) {
|
||||
socketRef.current.disconnect();
|
||||
socketRef.current = null;
|
||||
setIsConnected(false);
|
||||
}
|
||||
};
|
||||
}, [bodyshop, notification, userAssociationId, markNotificationRead, markAllNotificationsRead, navigate]);
|
||||
|
||||
return (
|
||||
<SocketContext.Provider
|
||||
value={{
|
||||
socket: socketRef.current,
|
||||
clientId,
|
||||
isConnected,
|
||||
markNotificationRead,
|
||||
markAllNotificationsRead
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SocketContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useSocket = () => {
|
||||
const context = useContext(SocketContext);
|
||||
if (!context) {
|
||||
throw new Error("useSocket must be used within a SocketProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export default SocketContext;
|
||||
500
client/src/contexts/SocketIO/useSocket.jsx
Normal file
500
client/src/contexts/SocketIO/useSocket.jsx
Normal file
@@ -0,0 +1,500 @@
|
||||
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
||||
import SocketIO from "socket.io-client";
|
||||
import { auth } from "../../firebase/firebase.utils";
|
||||
import { store } from "../../redux/store";
|
||||
import { addAlerts, setWssStatus } from "../../redux/application/application.actions";
|
||||
import client from "../../utils/GraphQLClient";
|
||||
import { useNotification } from "../Notifications/notificationContext.jsx";
|
||||
import {
|
||||
GET_NOTIFICATIONS,
|
||||
GET_UNREAD_COUNT,
|
||||
MARK_ALL_NOTIFICATIONS_READ,
|
||||
MARK_NOTIFICATION_READ,
|
||||
UPDATE_NOTIFICATIONS_READ_FRAGMENT
|
||||
} from "../../graphql/notifications.queries.js";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
|
||||
const SocketContext = createContext(null);
|
||||
|
||||
const INITIAL_NOTIFICATIONS = 10;
|
||||
|
||||
/**
|
||||
* Socket Provider - Scenario Notifications / Web Socket related items
|
||||
* @param children
|
||||
* @param bodyshop
|
||||
* @param navigate
|
||||
* @param currentUser
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
||||
const socketRef = useRef(null);
|
||||
const [clientId, setClientId] = useState(null);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const notification = useNotification();
|
||||
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
treatments: { Realtime_Notifications_UI }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Realtime_Notifications_UI"],
|
||||
splitKey: bodyshop?.imexshopid
|
||||
});
|
||||
|
||||
const [markNotificationRead] = useMutation(MARK_NOTIFICATION_READ, {
|
||||
update: (cache, { data: { update_notifications } }) => {
|
||||
const timestamp = new Date().toISOString();
|
||||
const updatedNotification = update_notifications.returning[0];
|
||||
|
||||
cache.modify({
|
||||
fields: {
|
||||
notifications(existing = [], { readField }) {
|
||||
return existing.map((notif) =>
|
||||
readField("id", notif) === updatedNotification.id
|
||||
? {
|
||||
...notif,
|
||||
read: timestamp
|
||||
}
|
||||
: notif
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const unreadCountQuery = cache.readQuery({
|
||||
query: GET_UNREAD_COUNT,
|
||||
variables: { associationid: userAssociationId }
|
||||
});
|
||||
|
||||
if (unreadCountQuery?.notifications_aggregate?.aggregate?.count > 0) {
|
||||
cache.writeQuery({
|
||||
query: GET_UNREAD_COUNT,
|
||||
variables: { associationid: userAssociationId },
|
||||
data: {
|
||||
notifications_aggregate: {
|
||||
...unreadCountQuery.notifications_aggregate,
|
||||
aggregate: {
|
||||
...unreadCountQuery.notifications_aggregate.aggregate,
|
||||
count: unreadCountQuery.notifications_aggregate.aggregate.count - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (socketRef.current && isConnected) {
|
||||
socketRef.current.emit("sync-notification-read", {
|
||||
email: currentUser?.email,
|
||||
bodyshopId: bodyshop.id,
|
||||
notificationId: updatedNotification.id
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (err) =>
|
||||
console.error("MARK_NOTIFICATION_READ error:", {
|
||||
message: err?.message,
|
||||
stack: err?.stack
|
||||
})
|
||||
});
|
||||
|
||||
const [markAllNotificationsRead] = useMutation(MARK_ALL_NOTIFICATIONS_READ, {
|
||||
variables: { associationid: userAssociationId },
|
||||
update: (cache) => {
|
||||
const timestamp = new Date().toISOString();
|
||||
cache.modify({
|
||||
fields: {
|
||||
notifications(existing = [], { readField }) {
|
||||
return existing.map((notif) =>
|
||||
readField("read", notif) === null && readField("associationid", notif) === userAssociationId
|
||||
? { ...notif, read: timestamp }
|
||||
: notif
|
||||
);
|
||||
},
|
||||
notifications_aggregate() {
|
||||
return { aggregate: { count: 0, __typename: "notifications_aggregate_fields" } };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const baseWhereClause = { associationid: { _eq: userAssociationId } };
|
||||
const cachedNotifications = cache.readQuery({
|
||||
query: GET_NOTIFICATIONS,
|
||||
variables: { limit: INITIAL_NOTIFICATIONS, offset: 0, where: baseWhereClause }
|
||||
});
|
||||
|
||||
if (cachedNotifications?.notifications) {
|
||||
cache.writeQuery({
|
||||
query: GET_NOTIFICATIONS,
|
||||
variables: { limit: INITIAL_NOTIFICATIONS, offset: 0, where: baseWhereClause },
|
||||
data: {
|
||||
notifications: cachedNotifications.notifications.map((notif) =>
|
||||
notif.read === null ? { ...notif, read: timestamp } : notif
|
||||
)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (socketRef.current && isConnected) {
|
||||
socketRef.current.emit("sync-all-notifications-read", {
|
||||
email: currentUser?.email,
|
||||
bodyshopId: bodyshop.id
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (err) => console.error("MARK_ALL_NOTIFICATIONS_READ error:", err)
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const initializeSocket = async (token) => {
|
||||
if (!bodyshop || !bodyshop.id || socketRef.current) return;
|
||||
|
||||
const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "";
|
||||
const socketInstance = SocketIO(endpoint, {
|
||||
path: "/wss",
|
||||
withCredentials: true,
|
||||
auth: { token, bodyshopId: bodyshop.id },
|
||||
reconnectionAttempts: Infinity,
|
||||
reconnectionDelay: 2000,
|
||||
reconnectionDelayMax: 10000
|
||||
});
|
||||
|
||||
socketRef.current = socketInstance;
|
||||
|
||||
const handleBodyshopMessage = (message) => {
|
||||
if (!message || !message.type) return;
|
||||
switch (message.type) {
|
||||
case "alert-update":
|
||||
store.dispatch(addAlerts(message.payload));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleConnect = () => {
|
||||
socketInstance.emit("join-bodyshop-room", bodyshop.id);
|
||||
setClientId(socketInstance.id);
|
||||
setIsConnected(true);
|
||||
store.dispatch(setWssStatus("connected"));
|
||||
};
|
||||
|
||||
const handleReconnect = () => {
|
||||
setIsConnected(true);
|
||||
store.dispatch(setWssStatus("connected"));
|
||||
};
|
||||
|
||||
const handleConnectionError = (err) => {
|
||||
console.error("Socket connection error:", err);
|
||||
setIsConnected(false);
|
||||
if (err.message.includes("auth/id-token-expired")) {
|
||||
console.warn("Token expired, refreshing...");
|
||||
auth.currentUser?.getIdToken(true).then((newToken) => {
|
||||
socketInstance.auth = { token: newToken };
|
||||
socketInstance.connect();
|
||||
});
|
||||
} else {
|
||||
store.dispatch(setWssStatus("error"));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisconnect = (reason) => {
|
||||
console.warn("Socket disconnected:", reason);
|
||||
setIsConnected(false);
|
||||
store.dispatch(setWssStatus("disconnected"));
|
||||
if (!socketInstance.connected && reason !== "io server disconnect") {
|
||||
setTimeout(() => {
|
||||
if (socketInstance.disconnected) {
|
||||
console.log("Manually triggering reconnection...");
|
||||
socketInstance.connect();
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNotification = (data) => {
|
||||
// Scenario Notifications have been disabled, bail.
|
||||
if (Realtime_Notifications_UI?.treatment !== "on") {
|
||||
return;
|
||||
}
|
||||
|
||||
const { jobId, jobRoNumber, notificationId, associationId, notifications } = data;
|
||||
if (associationId !== userAssociationId) return;
|
||||
|
||||
const newNotification = {
|
||||
__typename: "notifications",
|
||||
id: notificationId,
|
||||
jobid: jobId,
|
||||
associationid: associationId,
|
||||
scenario_text: JSON.stringify(notifications.map((notif) => notif.body)),
|
||||
fcm_text: notifications.map((notif) => notif.body).join(". ") + ".",
|
||||
scenario_meta: JSON.stringify(notifications.map((notif) => notif.variables || {})),
|
||||
created_at: new Date(notifications[0].timestamp).toISOString(),
|
||||
read: null,
|
||||
job: { ro_number: jobRoNumber }
|
||||
};
|
||||
|
||||
const baseVariables = {
|
||||
limit: INITIAL_NOTIFICATIONS,
|
||||
offset: 0,
|
||||
where: { associationid: { _eq: userAssociationId } }
|
||||
};
|
||||
|
||||
try {
|
||||
const existingNotifications =
|
||||
client.cache.readQuery({
|
||||
query: GET_NOTIFICATIONS,
|
||||
variables: baseVariables
|
||||
})?.notifications || [];
|
||||
if (!existingNotifications.some((n) => n.id === newNotification.id)) {
|
||||
client.cache.writeQuery({
|
||||
query: GET_NOTIFICATIONS,
|
||||
variables: baseVariables,
|
||||
data: {
|
||||
notifications: [newNotification, ...existingNotifications].sort(
|
||||
(a, b) => new Date(b.created_at) - new Date(a.created_at)
|
||||
)
|
||||
},
|
||||
broadcast: true
|
||||
});
|
||||
|
||||
const unreadVariables = {
|
||||
...baseVariables,
|
||||
where: { ...baseVariables.where, read: { _is_null: true } }
|
||||
};
|
||||
const unreadNotifications =
|
||||
client.cache.readQuery({
|
||||
query: GET_NOTIFICATIONS,
|
||||
variables: unreadVariables
|
||||
})?.notifications || [];
|
||||
if (newNotification.read === null && !unreadNotifications.some((n) => n.id === newNotification.id)) {
|
||||
client.cache.writeQuery({
|
||||
query: GET_NOTIFICATIONS,
|
||||
variables: unreadVariables,
|
||||
data: {
|
||||
notifications: [newNotification, ...unreadNotifications].sort(
|
||||
(a, b) => new Date(b.created_at) - new Date(a.created_at)
|
||||
)
|
||||
},
|
||||
broadcast: true
|
||||
});
|
||||
}
|
||||
|
||||
client.cache.modify({
|
||||
id: "ROOT_QUERY",
|
||||
fields: {
|
||||
notifications_aggregate(existing = { aggregate: { count: 0 } }) {
|
||||
return {
|
||||
...existing,
|
||||
aggregate: {
|
||||
...existing.aggregate,
|
||||
count: existing.aggregate.count + (newNotification.read === null ? 1 : 0)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
notification.info({
|
||||
message: (
|
||||
<div
|
||||
onClick={() => {
|
||||
markNotificationRead({ variables: { id: notificationId } })
|
||||
.then(() => navigate(`/manage/jobs/${jobId}`))
|
||||
.catch((e) => console.error(`Error marking notification read: ${e?.message || ""}`));
|
||||
}}
|
||||
>
|
||||
{t("notifications.labels.notification-popup-title", {
|
||||
ro_number: jobRoNumber || t("general.labels.na")
|
||||
})}
|
||||
</div>
|
||||
),
|
||||
description: (
|
||||
<ul
|
||||
className="notification-alert-unordered-list"
|
||||
onClick={() => {
|
||||
markNotificationRead({ variables: { id: notificationId } })
|
||||
.then(() => navigate(`/manage/jobs/${jobId}`))
|
||||
.catch((e) => console.error(`Error marking notification read: ${e?.message || ""}`));
|
||||
}}
|
||||
>
|
||||
{notifications.map((notif, index) => (
|
||||
<li className="notification-alert-unordered-list-item" key={index}>
|
||||
{notif.body}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error handling new notification: ${error?.message || ""}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSyncNotificationRead = ({ notificationId, timestamp }) => {
|
||||
// Scenario Notifications have been disabled, bail.
|
||||
if (Realtime_Notifications_UI?.treatment !== "on") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const notificationRef = client.cache.identify({
|
||||
__typename: "notifications",
|
||||
id: notificationId
|
||||
});
|
||||
client.cache.writeFragment({
|
||||
id: notificationRef,
|
||||
fragment: UPDATE_NOTIFICATIONS_READ_FRAGMENT,
|
||||
data: { read: timestamp }
|
||||
});
|
||||
|
||||
const unreadCountData = client.cache.readQuery({
|
||||
query: GET_UNREAD_COUNT,
|
||||
variables: { associationid: userAssociationId }
|
||||
});
|
||||
if (unreadCountData?.notifications_aggregate?.aggregate?.count > 0) {
|
||||
const newCount = Math.max(unreadCountData.notifications_aggregate.aggregate.count - 1, 0);
|
||||
client.cache.writeQuery({
|
||||
query: GET_UNREAD_COUNT,
|
||||
variables: { associationid: userAssociationId },
|
||||
data: {
|
||||
notifications_aggregate: {
|
||||
__typename: "notifications_aggregate",
|
||||
aggregate: {
|
||||
__typename: "notifications_aggregate_fields",
|
||||
count: newCount
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in handleSyncNotificationRead:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSyncAllNotificationsRead = ({ timestamp }) => {
|
||||
// Scenario Notifications have been disabled, bail.
|
||||
if (Realtime_Notifications_UI?.treatment !== "on") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const queryVars = {
|
||||
limit: INITIAL_NOTIFICATIONS,
|
||||
offset: 0,
|
||||
where: { associationid: { _eq: userAssociationId } }
|
||||
};
|
||||
const cachedData = client.cache.readQuery({
|
||||
query: GET_NOTIFICATIONS,
|
||||
variables: queryVars
|
||||
});
|
||||
|
||||
if (cachedData?.notifications) {
|
||||
cachedData.notifications.forEach((notif) => {
|
||||
if (!notif.read) {
|
||||
const notifRef = client.cache.identify({ __typename: "notifications", id: notif.id });
|
||||
client.cache.writeFragment({
|
||||
id: notifRef,
|
||||
fragment: UPDATE_NOTIFICATIONS_READ_FRAGMENT,
|
||||
data: { read: timestamp }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
client.cache.writeQuery({
|
||||
query: GET_UNREAD_COUNT,
|
||||
variables: { associationid: userAssociationId },
|
||||
data: {
|
||||
notifications_aggregate: {
|
||||
__typename: "notifications_aggregate",
|
||||
aggregate: {
|
||||
__typename: "notifications_aggregate_fields",
|
||||
count: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error In HandleSyncAllNotificationsRead: ${error?.message || ""}`);
|
||||
}
|
||||
};
|
||||
|
||||
socketInstance.on("connect", handleConnect);
|
||||
socketInstance.on("reconnect", handleReconnect);
|
||||
socketInstance.on("connect_error", handleConnectionError);
|
||||
socketInstance.on("disconnect", handleDisconnect);
|
||||
socketInstance.on("bodyshop-message", handleBodyshopMessage);
|
||||
socketInstance.on("notification", handleNotification);
|
||||
socketInstance.on("sync-notification-read", handleSyncNotificationRead);
|
||||
socketInstance.on("sync-all-notifications-read", handleSyncAllNotificationsRead);
|
||||
};
|
||||
|
||||
const unsubscribe = auth.onIdTokenChanged(async (user) => {
|
||||
if (user) {
|
||||
const token = await user.getIdToken();
|
||||
if (socketRef.current) {
|
||||
socketRef.current.emit("update-token", { token, bodyshopId: bodyshop.id });
|
||||
} else {
|
||||
initializeSocket(token).catch((err) =>
|
||||
console.error(`Something went wrong Initializing Sockets: ${err?.message || ""}`)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (socketRef.current) {
|
||||
socketRef.current.disconnect();
|
||||
socketRef.current = null;
|
||||
setIsConnected(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
if (socketRef.current) {
|
||||
socketRef.current.disconnect();
|
||||
socketRef.current = null;
|
||||
setIsConnected(false);
|
||||
}
|
||||
};
|
||||
}, [
|
||||
bodyshop,
|
||||
notification,
|
||||
userAssociationId,
|
||||
markNotificationRead,
|
||||
markAllNotificationsRead,
|
||||
navigate,
|
||||
currentUser,
|
||||
Realtime_Notifications_UI,
|
||||
t
|
||||
]);
|
||||
|
||||
return (
|
||||
<SocketContext.Provider
|
||||
value={{
|
||||
socket: socketRef.current,
|
||||
clientId,
|
||||
isConnected,
|
||||
markNotificationRead,
|
||||
markAllNotificationsRead,
|
||||
scenarioNotificationsOn: Realtime_Notifications_UI?.treatment === "on"
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SocketContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useSocket = () => {
|
||||
const context = useContext(SocketContext);
|
||||
// NOTE: Not sure if we absolutely require this, does cause slipups on dev env
|
||||
if (!context) throw new Error("useSocket must be used within a SocketProvider");
|
||||
return context;
|
||||
};
|
||||
|
||||
export { SocketContext, SocketProvider, INITIAL_NOTIFICATIONS, useSocket };
|
||||
@@ -1,12 +1,10 @@
|
||||
import { onError } from "@apollo/client/link/error";
|
||||
//https://stackoverflow.com/questions/57163454/refreshing-a-token-with-apollo-client-firebase-auth
|
||||
import * as Sentry from "@sentry/react";
|
||||
|
||||
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
|
||||
if (graphQLErrors) {
|
||||
graphQLErrors.forEach(({ message, locations, path }) => {
|
||||
console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`);
|
||||
Sentry.captureException({ message, locations, path });
|
||||
});
|
||||
}
|
||||
if (networkError) console.log(`[Network error]: ${JSON.stringify(networkError)}`);
|
||||
|
||||
@@ -525,6 +525,7 @@ export const GET_JOB_BY_PK = gql`
|
||||
iouparent
|
||||
job_totals
|
||||
job_watchers {
|
||||
id
|
||||
user_email
|
||||
}
|
||||
joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) {
|
||||
@@ -2594,6 +2595,10 @@ export const REMOVE_JOB_WATCHER = gql`
|
||||
mutation REMOVE_JOB_WATCHER($jobid: uuid!, $userEmail: String!) {
|
||||
delete_job_watchers(where: { jobid: { _eq: $jobid }, user_email: { _eq: $userEmail } }) {
|
||||
affected_rows
|
||||
returning {
|
||||
id
|
||||
user_email
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -12,6 +12,7 @@ export const GET_NOTIFICATIONS = gql`
|
||||
created_at
|
||||
read
|
||||
job {
|
||||
id
|
||||
ro_number
|
||||
}
|
||||
}
|
||||
@@ -49,3 +50,9 @@ export const MARK_NOTIFICATION_READ = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_NOTIFICATIONS_READ_FRAGMENT = gql`
|
||||
fragment UpdateNotificationRead on notifications {
|
||||
read
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import "./utils/sentry"; //Must be first.
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { ConfigProvider } from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { Provider } from "react-redux";
|
||||
import { createBrowserRouter, createRoutesFromElements, Route, RouterProvider } from "react-router-dom";
|
||||
import { PersistGate } from "redux-persist/integration/react";
|
||||
import { registerSW } from "virtual:pwa-register";
|
||||
import AppContainer from "./App/App.container";
|
||||
import LoadingSpinner from "./components/loading-spinner/loading-spinner.component";
|
||||
import "./index.css";
|
||||
@@ -12,56 +14,18 @@ import { persistor, store } from "./redux/store";
|
||||
import reportWebVitals from "./reportWebVitals";
|
||||
import "./translations/i18n";
|
||||
import "./utils/CleanAxios";
|
||||
import { ConfigProvider } from "antd";
|
||||
import InstanceRenderManager from "./utils/instanceRenderMgr";
|
||||
import { registerSW } from "virtual:pwa-register";
|
||||
|
||||
window.global ||= window;
|
||||
|
||||
registerSW({ immediate: true });
|
||||
//import { BrowserTracing } from "@sentry/tracing";
|
||||
//import "antd/dist/antd.css";
|
||||
// import "antd/dist/antd.less";
|
||||
|
||||
// Dinero.defaultCurrency = "CAD";
|
||||
// Dinero.globalLocale = "en-CA";
|
||||
Dinero.globalRoundingMode = "HALF_EVEN";
|
||||
|
||||
if (import.meta.env.PROD) {
|
||||
Sentry.init({
|
||||
dsn: InstanceRenderManager({
|
||||
imex: "https://fd7e89369b6b4bdc9c6c4c9f22fa4ee4@o492140.ingest.sentry.io/5651027",
|
||||
rome: "https://a6acc91c073e414196014b8484627a61@o492140.ingest.sentry.io/4504561071161344"
|
||||
}),
|
||||
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV6(createBrowserRouter);
|
||||
|
||||
ignoreErrors: [
|
||||
"ResizeObserver loop",
|
||||
"ResizeObserver loop limit exceeded",
|
||||
"Module specifier, 'fs' does not start",
|
||||
"Module specifier, 'zlib' does not start with"
|
||||
],
|
||||
integrations: [
|
||||
Sentry.replayIntegration({
|
||||
maskAllText: false,
|
||||
blockAllMedia: true
|
||||
}),
|
||||
Sentry.browserTracingIntegration()
|
||||
],
|
||||
tracePropagationTargets: [
|
||||
"api.imex.online",
|
||||
"api.test.imex.online",
|
||||
"db.imex.online",
|
||||
"api.romeonline.io",
|
||||
"api.test.romeonline.io",
|
||||
"db.romeonline.io"
|
||||
],
|
||||
tracesSampleRate: 1.0,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
environment: import.meta.env.MODE
|
||||
});
|
||||
}
|
||||
|
||||
const router = createBrowserRouter(createRoutesFromElements(<Route path="*" element={<AppContainer />} />));
|
||||
const router = sentryCreateBrowserRouter(createRoutesFromElements(<Route path="*" element={<AppContainer />} />));
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
let styles =
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { EyeFilled, EyeOutlined, UserOutlined } from "@ant-design/icons";
|
||||
import { ADD_JOB_WATCHER, GET_JOB_WATCHERS, REMOVE_JOB_WATCHER } from "../../graphql/jobs.queries.js";
|
||||
import { Avatar, Button, Divider, List, Popover, Select, Tooltip, Typography } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
|
||||
import EmployeeSearchSelectComponent from "../../components/employee-search-select/employee-search-select.component.jsx";
|
||||
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component.jsx";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
|
||||
const JobWatcherToggle = ({ job, currentUser, bodyshop }) => {
|
||||
const { t } = useTranslation();
|
||||
const userEmail = currentUser.email;
|
||||
const jobid = job.id;
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedWatcher, setSelectedWatcher] = useState(null); // New state for selected value
|
||||
const [selectedTeam, setSelectedTeam] = useState(null); // New state to track selected team
|
||||
|
||||
// Fetch current watchers
|
||||
const { data: watcherData, loading: watcherLoading } = useQuery(GET_JOB_WATCHERS, { variables: { jobid } });
|
||||
|
||||
// Extract watchers list
|
||||
const jobWatchers = useMemo(() => watcherData?.job_watchers || [], [watcherData]);
|
||||
const isWatching = useMemo(() => jobWatchers.some((w) => w.user_email === userEmail), [jobWatchers, userEmail]);
|
||||
|
||||
// Add watcher mutation
|
||||
const [addWatcher, { loading: adding }] = useMutation(ADD_JOB_WATCHER, {
|
||||
refetchQueries: [{ query: GET_JOB_WATCHERS, variables: { jobid } }]
|
||||
});
|
||||
|
||||
// Remove watcher mutation
|
||||
const [removeWatcher, { loading: removing }] = useMutation(REMOVE_JOB_WATCHER, {
|
||||
refetchQueries: [{ query: GET_JOB_WATCHERS, variables: { jobid } }]
|
||||
});
|
||||
|
||||
// Toggle watcher for self
|
||||
const handleToggleSelf = useCallback(() => {
|
||||
(isWatching
|
||||
? removeWatcher({ variables: { jobid, userEmail } })
|
||||
: addWatcher({ variables: { jobid, userEmail } })
|
||||
).catch((err) => console.error(`Error updating job watcher: ${err.message}`));
|
||||
}, [isWatching, addWatcher, removeWatcher, jobid, userEmail]);
|
||||
|
||||
// Handle removing a watcher
|
||||
const handleRemoveWatcher = (userEmail) => {
|
||||
removeWatcher({ variables: { jobid, userEmail } }).catch((err) =>
|
||||
console.error(`Error removing job watcher: ${err.message}`)
|
||||
);
|
||||
};
|
||||
|
||||
const handleWatcherSelect = (selectedUser) => {
|
||||
const employee = bodyshop.employees.find((e) => e.id === selectedUser);
|
||||
if (!employee) return;
|
||||
|
||||
const isAlreadyWatching = jobWatchers.some((w) => w.user_email === employee.user_email);
|
||||
|
||||
if (isAlreadyWatching) {
|
||||
handleRemoveWatcher(employee.user_email);
|
||||
} else {
|
||||
addWatcher({ variables: { jobid, userEmail: employee.user_email } }).catch((err) =>
|
||||
console.error(`Error adding job watcher: ${err.message}`)
|
||||
);
|
||||
}
|
||||
|
||||
// Clear selection
|
||||
setSelectedWatcher(null);
|
||||
};
|
||||
|
||||
const handleTeamSelect = (team) => {
|
||||
const selectedTeamMembers = JSON.parse(team); // Parse the array of emails
|
||||
|
||||
const newWatchers = selectedTeamMembers.filter(
|
||||
(email) => !jobWatchers.some((watcher) => watcher.user_email === email)
|
||||
);
|
||||
|
||||
// Add each new watcher
|
||||
newWatchers.forEach((email) => {
|
||||
addWatcher({ variables: { jobid, userEmail: email } }).catch((err) =>
|
||||
console.error(`Error adding job watcher: ${err.message}`)
|
||||
);
|
||||
});
|
||||
|
||||
// Clear selection
|
||||
setSelectedTeam(null);
|
||||
};
|
||||
|
||||
const handleRenderItem = (watcher) => {
|
||||
const employee = bodyshop.employees.find((e) => e.user_email === watcher.user_email);
|
||||
const displayName = employee ? `${employee.first_name} ${employee.last_name}` : watcher.user_email;
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Button type="link" danger size="small" onClick={() => handleRemoveWatcher(watcher.user_email)}>
|
||||
{t("notifications.actions.remove")}
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
avatar={<Avatar icon={<UserOutlined />} />}
|
||||
title={<Text>{displayName}</Text>}
|
||||
description={watcher.user_email} // Keep the email for reference
|
||||
/>
|
||||
</List.Item>
|
||||
);
|
||||
};
|
||||
|
||||
// Popover content
|
||||
const popoverContent = (
|
||||
<div style={{ width: 600 }}>
|
||||
{/* Self-toggle Button */}
|
||||
<Button
|
||||
block
|
||||
type="text"
|
||||
icon={isWatching ? <EyeOutlined /> : <EyeFilled />}
|
||||
onClick={handleToggleSelf}
|
||||
loading={adding || removing}
|
||||
>
|
||||
{isWatching ? t("notifications.tooltips.unwatch") : t("notifications.tooltips.watch")}
|
||||
</Button>
|
||||
{/* List of Watchers */}
|
||||
<Text type="secondary" style={{ marginBottom: 8, display: "block" }}>
|
||||
{t("notifications.labels.watching-issue")}
|
||||
</Text>
|
||||
{watcherLoading ? <LoadingSpinner /> : <List dataSource={jobWatchers} renderItem={handleRenderItem} />}
|
||||
{/* Employee Search Select (for adding watchers) */}
|
||||
<Divider />
|
||||
|
||||
<Text type="secondary">{t("notifications.labels.add-watchers")}</Text>
|
||||
<EmployeeSearchSelectComponent
|
||||
style={{ minWidth: "100%" }}
|
||||
options={bodyshop.employees.filter((e) => jobWatchers.every((w) => w.user_email !== e.user_email))}
|
||||
placeholder={t("notifications.labels.employee-search")}
|
||||
value={selectedWatcher} // Controlled value
|
||||
onChange={(value) => {
|
||||
setSelectedWatcher(value); // Update selected state
|
||||
handleWatcherSelect(value); // Add watcher logic
|
||||
}}
|
||||
/>
|
||||
{/* Divider for UI separation */}
|
||||
{/* Only show team selection if there are available teams */}
|
||||
{bodyshop?.employee_teams?.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<Text type="secondary">{t("notifications.labels.add-watchers-team")}</Text>
|
||||
|
||||
<Select
|
||||
showSearch
|
||||
style={{ minWidth: "100%" }}
|
||||
placeholder={t("notifications.labels.teams-search")}
|
||||
value={selectedTeam} // Controlled value
|
||||
onChange={handleTeamSelect}
|
||||
options={bodyshop.employee_teams.map((team) => {
|
||||
const teamMembers = team.employee_team_members
|
||||
.map((member) => {
|
||||
const employee = bodyshop.employees.find((e) => e.id === member.employeeid);
|
||||
return employee ? employee.user_email : null;
|
||||
})
|
||||
.filter(Boolean); // Remove nulls
|
||||
|
||||
return {
|
||||
value: JSON.stringify(teamMembers), // Store array as string
|
||||
label: team.name // Use team name as label
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={popoverContent} trigger="click" open={open} onOpenChange={setOpen}>
|
||||
<Tooltip title={isWatching ? t("notifications.tooltips.unwatch") : t("notifications.tooltips.watch")}>
|
||||
<Button
|
||||
shape="circle"
|
||||
type={isWatching ? "primary" : "default"}
|
||||
icon={isWatching ? <EyeFilled /> : <EyeOutlined />}
|
||||
loading={watcherLoading}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(JobWatcherToggle);
|
||||
@@ -56,7 +56,8 @@ import { DateTimeFormat } from "../../utils/DateFormatter";
|
||||
import dayjs from "../../utils/day";
|
||||
import UndefinedToNull from "../../utils/undefinedtonull";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import JobWatcherToggle from "./job-watcher-toggle.component.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import JobWatcherToggleContainer from "../../components/job-watcher-toggle/job-watcher-toggle.container.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -103,6 +104,7 @@ export function JobsDetailPage({
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
const notification = useNotification();
|
||||
const { scenarioNotificationsOn } = useSocket();
|
||||
|
||||
useEffect(() => {
|
||||
//form.setFieldsValue(transormJobToForm(job));
|
||||
@@ -323,7 +325,7 @@ export function JobsDetailPage({
|
||||
|
||||
title={
|
||||
<Space>
|
||||
<JobWatcherToggle job={job} />
|
||||
{scenarioNotificationsOn && <JobWatcherToggleContainer job={job} />}
|
||||
{job.ro_number || t("general.labels.na")}
|
||||
</Space>
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import PartnerPingComponent from "../../components/partner-ping/partner-ping.com
|
||||
import PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container";
|
||||
import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component";
|
||||
import { requestForToken } from "../../firebase/firebase.utils";
|
||||
import { useSocket } from "../../contexts/SocketIO/socketContext.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||
import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors";
|
||||
import UpdateAlert from "../../components/update-alert/update-alert.component";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
||||
@@ -143,7 +143,7 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
||||
const fetchedAlerts = await response.json();
|
||||
setAlerts(fetchedAlerts);
|
||||
} catch (error) {
|
||||
console.error("Error fetching alerts:", error);
|
||||
console.warn("Error fetching alerts:", error.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -167,7 +167,6 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
||||
description: alert.description,
|
||||
type: alert.type || "info",
|
||||
duration: 0,
|
||||
placement: "bottomRight",
|
||||
closable: true,
|
||||
onClose: () => {
|
||||
// When the notification is closed, update displayed alerts state and localStorage
|
||||
|
||||
@@ -351,7 +351,7 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
||||
? window.$crisp.push(["set", "session:segments", [["allAccess"]]])
|
||||
: window.$crisp.push(["set", "session:segments", [["basic"]]]);
|
||||
} catch (error) {
|
||||
console.error("Couldnt find $crisp.");
|
||||
console.warn("Couldnt find $crisp.", error.message);
|
||||
}
|
||||
} catch (error) {
|
||||
yield put(signInFailure(error.message));
|
||||
|
||||
@@ -3779,19 +3779,24 @@
|
||||
"teams-search": "Search for a Team",
|
||||
"add-watchers-team": "Add Team Members",
|
||||
"new-notification-title": "New Notification:",
|
||||
"show-unread-only": "Show Unread",
|
||||
"mark-all-read": "Mark Read",
|
||||
"loading": "Loading Notifications..."
|
||||
"show-unread-only": "Show Unread Only",
|
||||
"mark-all-read": "Mark All Read",
|
||||
"notification-popup-title": "Changes for Job #{{ro_number}}",
|
||||
"ro-number": "RO #{{ro_number}}",
|
||||
"no-watchers": "No Watchers",
|
||||
"notification-settings-success": "Notification Settings saved successfully.",
|
||||
"notification-settings-failure": "Error saving Notification Settings. {{error}}",
|
||||
"watch": "Watch",
|
||||
"unwatch": "Unwatch"
|
||||
},
|
||||
"actions": {
|
||||
"remove": "remove"
|
||||
"remove": "Remove"
|
||||
},
|
||||
"aria": {
|
||||
"toggle": "Toggle Watching Job"
|
||||
},
|
||||
"tooltips": {
|
||||
"watch": "Watch Job",
|
||||
"unwatch": "Unwatch Job"
|
||||
"job-watchers": "Job Watchers"
|
||||
},
|
||||
"scenarios": {
|
||||
"job-assigned-to-me": "Job Assigned to Me",
|
||||
|
||||
@@ -3769,6 +3769,7 @@
|
||||
},
|
||||
"notifications": {
|
||||
"labels": {
|
||||
"notification-center": "",
|
||||
"scenario": "",
|
||||
"notificationscenarios": "",
|
||||
"save": "",
|
||||
@@ -3776,7 +3777,17 @@
|
||||
"add-watchers": "",
|
||||
"employee-search": "",
|
||||
"teams-search": "",
|
||||
"add-watchers-team": ""
|
||||
"add-watchers-team": "",
|
||||
"new-notification-title": "",
|
||||
"show-unread-only": "",
|
||||
"mark-all-read": "",
|
||||
"notification-popup-title": "",
|
||||
"ro-number": "",
|
||||
"no-watchers": "",
|
||||
"notification-settings-success": "",
|
||||
"notification-settings-failure": "",
|
||||
"watch": "",
|
||||
"unwatch": ""
|
||||
},
|
||||
"actions": {
|
||||
"remove": ""
|
||||
@@ -3785,8 +3796,7 @@
|
||||
"toggle": ""
|
||||
},
|
||||
"tooltips": {
|
||||
"watch": "",
|
||||
"unwatch": ""
|
||||
"job-watchers": ""
|
||||
},
|
||||
"scenarios": {
|
||||
"job-assigned-to-me": "",
|
||||
|
||||
@@ -3769,6 +3769,7 @@
|
||||
},
|
||||
"notifications": {
|
||||
"labels": {
|
||||
"notification-center": "",
|
||||
"scenario": "",
|
||||
"notificationscenarios": "",
|
||||
"save": "",
|
||||
@@ -3776,7 +3777,17 @@
|
||||
"add-watchers": "",
|
||||
"employee-search": "",
|
||||
"teams-search": "",
|
||||
"add-watchers-team": ""
|
||||
"add-watchers-team": "",
|
||||
"new-notification-title": "",
|
||||
"show-unread-only": "",
|
||||
"mark-all-read": "",
|
||||
"notification-popup-title": "",
|
||||
"ro-number": "",
|
||||
"no-watchers": "",
|
||||
"notification-settings-success": "",
|
||||
"notification-settings-failure": "",
|
||||
"watch": "",
|
||||
"unwatch": ""
|
||||
},
|
||||
"actions": {
|
||||
"remove": ""
|
||||
@@ -3785,8 +3796,7 @@
|
||||
"toggle": ""
|
||||
},
|
||||
"tooltips": {
|
||||
"watch": "",
|
||||
"unwatch": ""
|
||||
"job-watchers": ""
|
||||
},
|
||||
"scenarios": {
|
||||
"job-assigned-to-me": "",
|
||||
|
||||
@@ -7,9 +7,9 @@ import { getMainDefinition } from "@apollo/client/utilities";
|
||||
//import { split } from "apollo-link";
|
||||
import apolloLogger from "apollo-link-logger";
|
||||
//import axios from "axios";
|
||||
import { SentryLink } from "apollo-link-sentry";
|
||||
import { auth } from "../firebase/firebase.utils";
|
||||
import errorLink from "../graphql/apollo-error-handling";
|
||||
import { SentryLink } from "apollo-link-sentry";
|
||||
|
||||
//import { store } from "../redux/store";
|
||||
const httpLink = new HttpLink({
|
||||
@@ -143,7 +143,41 @@ middlewares.push(
|
||||
new SentryLink().concat(roundTripLink.concat(retryLink.concat(errorLink.concat(authLink.concat(link)))))
|
||||
);
|
||||
|
||||
const cache = new InMemoryCache({});
|
||||
const cache = new InMemoryCache({
|
||||
typePolicies: {
|
||||
Query: {
|
||||
fields: {
|
||||
// Note: This is required because we switch from a read to an unread state with a toggle,
|
||||
notifications: {
|
||||
merge(existing = [], incoming = [], { readField }) {
|
||||
// Create a map to deduplicate by __ref
|
||||
const merged = new Map();
|
||||
|
||||
// Add existing items to retain cached data
|
||||
existing.forEach((item) => {
|
||||
const ref = readField("__ref", item);
|
||||
if (ref) {
|
||||
merged.set(ref, item);
|
||||
}
|
||||
});
|
||||
|
||||
// Add incoming items, overwriting duplicates
|
||||
incoming.forEach((item) => {
|
||||
const ref = readField("__ref", item);
|
||||
if (ref) {
|
||||
merged.set(ref, item);
|
||||
}
|
||||
});
|
||||
|
||||
// Return incoming to respect the current query’s filter (e.g., unread-only or all)
|
||||
return incoming;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const client = new ApolloClient({
|
||||
link: ApolloLink.from(middlewares),
|
||||
cache,
|
||||
@@ -163,4 +197,5 @@ const client = new ApolloClient({
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default client;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
/** Notification Scenarios
|
||||
* @description This file contains the scenarios for job notifications.
|
||||
* @type {string[]}
|
||||
*/
|
||||
const notificationScenarios = [
|
||||
"job-assigned-to-me",
|
||||
"bill-posted",
|
||||
"critical-parts-status-changed",
|
||||
"part-marked-back-ordered",
|
||||
"new-note-added",
|
||||
"supplement-imported",
|
||||
"schedule-dates-changed",
|
||||
"tasks-updated-created",
|
||||
"new-media-added-reassigned",
|
||||
@@ -14,6 +17,7 @@ const notificationScenarios = [
|
||||
"job-status-change",
|
||||
"payment-collected-completed",
|
||||
"alternate-transport-changed"
|
||||
// "supplement-imported", // Disabled for now
|
||||
];
|
||||
|
||||
export { notificationScenarios };
|
||||
|
||||
63
client/src/utils/sentry.js
Normal file
63
client/src/utils/sentry.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { excludeGraphQLFetch } from "apollo-link-sentry";
|
||||
import { useEffect } from "react";
|
||||
import { createRoutesFromChildren, matchRoutes, useLocation, useNavigationType } from "react-router-dom";
|
||||
import InstanceRenderManager from "./instanceRenderMgr";
|
||||
|
||||
const currentDatePST = new Date()
|
||||
.toLocaleDateString("en-US", {
|
||||
timeZone: "America/Los_Angeles",
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit"
|
||||
})
|
||||
.split("/")
|
||||
.reverse()
|
||||
.join("-");
|
||||
const sentryRelease =
|
||||
`${import.meta.env.VITE_APP_IS_TEST ? "test" : "production"}-${currentDatePST}-${process.env.VITE_GIT_COMMIT_HASH}`.trim();
|
||||
|
||||
if (!import.meta.env.DEV) {
|
||||
Sentry.init({
|
||||
dsn: InstanceRenderManager({
|
||||
imex: "https://fd7e89369b6b4bdc9c6c4c9f22fa4ee4@o492140.ingest.sentry.io/5651027",
|
||||
rome: "https://a6acc91c073e414196014b8484627a61@o492140.ingest.sentry.io/4504561071161344"
|
||||
}),
|
||||
release: sentryRelease,
|
||||
|
||||
ignoreErrors: [
|
||||
"ResizeObserver loop",
|
||||
"ResizeObserver loop limit exceeded",
|
||||
"Module specifier, 'fs' does not start",
|
||||
"Module specifier, 'zlib' does not start with",
|
||||
"Messaging: This browser doesn't support the API's required to use the Firebase SDK.",
|
||||
"Failed to update a ServiceWorker for scope"
|
||||
],
|
||||
integrations: [
|
||||
// See docs for support of different versions of variation of react router
|
||||
// https://docs.sentry.io/platforms/javascript/guides/react/configuration/integrations/react-router/
|
||||
Sentry.reactRouterV6BrowserTracingIntegration({
|
||||
useEffect,
|
||||
useLocation,
|
||||
useNavigationType,
|
||||
createRoutesFromChildren,
|
||||
matchRoutes
|
||||
}),
|
||||
Sentry.replayIntegration(),
|
||||
Sentry.browserProfilingIntegration()
|
||||
],
|
||||
|
||||
tracePropagationTargets: [
|
||||
"api.imex.online",
|
||||
"api.test.imex.online",
|
||||
"db.imex.online",
|
||||
"api.romeonline.io",
|
||||
"api.test.romeonline.io",
|
||||
"db.romeonline.io"
|
||||
],
|
||||
tracesSampleRate: 1.0,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
environment: import.meta.env.MODE,
|
||||
beforeBreadcrumb: excludeGraphQLFetch
|
||||
});
|
||||
}
|
||||
@@ -1,16 +1,31 @@
|
||||
import { sentryVitePlugin } from "@sentry/vite-plugin";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import chalk from "chalk";
|
||||
import * as child from "child_process";
|
||||
import { promises as fsPromises } from "fs";
|
||||
import { createLogger, 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";
|
||||
|
||||
// Ensure your environment variables are set correctly for Vite 6
|
||||
process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", {
|
||||
timeZone: "America/Los_Angeles"
|
||||
});
|
||||
const commitHash = child.execSync("git rev-parse HEAD").toString().trimEnd();
|
||||
process.env.VITE_GIT_COMMIT_HASH = commitHash;
|
||||
|
||||
const currentDatePST = new Date()
|
||||
.toLocaleDateString("en-US", {
|
||||
timeZone: "America/Los_Angeles",
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit"
|
||||
})
|
||||
.split("/")
|
||||
.reverse()
|
||||
.join("-");
|
||||
|
||||
const getFormattedTimestamp = () =>
|
||||
new Date().toLocaleTimeString("en-US", { hour12: true }).replace("AM", "a.m.").replace("PM", "p.m.");
|
||||
@@ -78,10 +93,25 @@ export default defineConfig({
|
||||
}
|
||||
}),
|
||||
react(),
|
||||
eslint()
|
||||
eslint(),
|
||||
sentryVitePlugin({
|
||||
org: "imex",
|
||||
reactComponentAnnotation: {
|
||||
enabled: true
|
||||
},
|
||||
release: {
|
||||
name: `${process.env.VITE_APP_IS_TEST ? "test" : "production"}-${currentDatePST}-${commitHash}`.trim()
|
||||
},
|
||||
project: InstanceRenderManager({
|
||||
instance: process.env.VITE_APP_INSTANCE,
|
||||
imex: "imexonline",
|
||||
rome: "rome-online"
|
||||
})
|
||||
})
|
||||
],
|
||||
define: {
|
||||
APP_VERSION: JSON.stringify(process.env.npm_package_version)
|
||||
APP_VERSION: JSON.stringify(process.env.npm_package_version),
|
||||
__COMMIT_HASH__: JSON.stringify(commitHash)
|
||||
},
|
||||
server: {
|
||||
host: true,
|
||||
@@ -184,7 +214,9 @@ export default defineConfig({
|
||||
"libphonenumber-js": ["libphonenumber-js"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
sourcemap: true
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
|
||||
221
docker-compose-cluster.yml
Normal file
221
docker-compose-cluster.yml
Normal file
@@ -0,0 +1,221 @@
|
||||
services:
|
||||
# Load Balancer (NGINX) with WebSocket support and session persistence
|
||||
load-balancer:
|
||||
image: nginx:latest
|
||||
container_name: load-balancer
|
||||
ports:
|
||||
- "4000:80" # External port 4000 maps to NGINX's port 80
|
||||
volumes:
|
||||
- ./nginx-websocket.conf:/etc/nginx/nginx.conf:ro # Mount NGINX configuration
|
||||
networks:
|
||||
- redis-cluster-net
|
||||
depends_on:
|
||||
- node-app-1
|
||||
- node-app-2
|
||||
- node-app-3
|
||||
healthcheck:
|
||||
test: [ "CMD", "curl", "-f", "http://localhost/health" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Node App Instance 1
|
||||
node-app-1:
|
||||
build:
|
||||
context: .
|
||||
container_name: node-app-1
|
||||
hostname: node-app-1
|
||||
networks:
|
||||
- redis-cluster-net
|
||||
env_file:
|
||||
- .env.development
|
||||
depends_on:
|
||||
redis-node-1:
|
||||
condition: service_healthy
|
||||
redis-node-2:
|
||||
condition: service_healthy
|
||||
redis-node-3:
|
||||
condition: service_healthy
|
||||
localstack:
|
||||
condition: service_healthy
|
||||
aws-cli:
|
||||
condition: service_completed_successfully
|
||||
ports:
|
||||
- "4001:4000" # Different external port for local access
|
||||
volumes:
|
||||
- .:/app
|
||||
- node-app-npm-cache:/app/node_modules
|
||||
|
||||
# Node App Instance 2
|
||||
node-app-2:
|
||||
build:
|
||||
context: .
|
||||
container_name: node-app-2
|
||||
hostname: node-app-2
|
||||
networks:
|
||||
- redis-cluster-net
|
||||
env_file:
|
||||
- .env.development
|
||||
depends_on:
|
||||
redis-node-1:
|
||||
condition: service_healthy
|
||||
redis-node-2:
|
||||
condition: service_healthy
|
||||
redis-node-3:
|
||||
condition: service_healthy
|
||||
localstack:
|
||||
condition: service_healthy
|
||||
aws-cli:
|
||||
condition: service_completed_successfully
|
||||
ports:
|
||||
- "4002:4000" # Different external port for local access
|
||||
volumes:
|
||||
- .:/app
|
||||
- node-app-npm-cache:/app/node_modules
|
||||
|
||||
# Node App Instance 3
|
||||
node-app-3:
|
||||
build:
|
||||
context: .
|
||||
container_name: node-app-3
|
||||
hostname: node-app-3
|
||||
networks:
|
||||
- redis-cluster-net
|
||||
env_file:
|
||||
- .env.development
|
||||
depends_on:
|
||||
redis-node-1:
|
||||
condition: service_healthy
|
||||
redis-node-2:
|
||||
condition: service_healthy
|
||||
redis-node-3:
|
||||
condition: service_healthy
|
||||
localstack:
|
||||
condition: service_healthy
|
||||
aws-cli:
|
||||
condition: service_completed_successfully
|
||||
ports:
|
||||
- "4003:4000" # Different external port for local access
|
||||
volumes:
|
||||
- .:/app
|
||||
- node-app-npm-cache:/app/node_modules
|
||||
|
||||
# Redis Node 1
|
||||
redis-node-1:
|
||||
build:
|
||||
context: ./redis
|
||||
container_name: redis-node-1
|
||||
hostname: redis-node-1
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- redis-cluster-net
|
||||
volumes:
|
||||
- redis-node-1-data:/data
|
||||
- redis-lock:/redis-lock
|
||||
healthcheck:
|
||||
test: [ "CMD", "redis-cli", "ping" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
# Redis Node 2
|
||||
redis-node-2:
|
||||
build:
|
||||
context: ./redis
|
||||
container_name: redis-node-2
|
||||
hostname: redis-node-2
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- redis-cluster-net
|
||||
volumes:
|
||||
- redis-node-2-data:/data
|
||||
- redis-lock:/redis-lock
|
||||
healthcheck:
|
||||
test: [ "CMD", "redis-cli", "ping" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
# Redis Node 3
|
||||
redis-node-3:
|
||||
build:
|
||||
context: ./redis
|
||||
container_name: redis-node-3
|
||||
hostname: redis-node-3
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- redis-cluster-net
|
||||
volumes:
|
||||
- redis-node-3-data:/data
|
||||
- redis-lock:/redis-lock
|
||||
healthcheck:
|
||||
test: [ "CMD", "redis-cli", "ping" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
# LocalStack
|
||||
localstack:
|
||||
image: localstack/localstack
|
||||
container_name: localstack
|
||||
hostname: localstack
|
||||
networks:
|
||||
- redis-cluster-net
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
- SERVICES=s3,ses,secretsmanager,cloudwatch,logs
|
||||
- DEBUG=0
|
||||
- AWS_ACCESS_KEY_ID=test
|
||||
- AWS_SECRET_ACCESS_KEY=test
|
||||
- AWS_DEFAULT_REGION=ca-central-1
|
||||
- EXTRA_CORS_ALLOWED_HEADERS=Authorization,Content-Type
|
||||
- EXTRA_CORS_ALLOWED_ORIGINS=*
|
||||
- EXTRA_CORS_EXPOSE_HEADERS=Authorization,Content-Type
|
||||
ports:
|
||||
- "4566:4566"
|
||||
healthcheck:
|
||||
test: [ "CMD", "curl", "-f", "http://localhost:4566/_localstack/health" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
|
||||
# AWS-CLI
|
||||
aws-cli:
|
||||
image: amazon/aws-cli
|
||||
container_name: aws-cli
|
||||
hostname: aws-cli
|
||||
networks:
|
||||
- redis-cluster-net
|
||||
depends_on:
|
||||
localstack:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- './localstack:/tmp/localstack'
|
||||
- './certs:/tmp/certs'
|
||||
environment:
|
||||
- AWS_ACCESS_KEY_ID=test
|
||||
- AWS_SECRET_ACCESS_KEY=test
|
||||
- AWS_DEFAULT_REGION=ca-central-1
|
||||
entrypoint: /bin/sh -c
|
||||
command: >
|
||||
"
|
||||
aws --endpoint-url=http://localstack:4566 ses verify-domain-identity --domain imex.online --region ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 ses verify-email-identity --email-address noreply@imex.online --region ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 secretsmanager create-secret --name CHATTER_PRIVATE_KEY --secret-string file:///tmp/certs/io-ftp-test.key
|
||||
aws --endpoint-url=http://localstack:4566 logs create-log-group --log-group-name development --region ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-large-log --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
"
|
||||
|
||||
networks:
|
||||
redis-cluster-net:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
node-app-npm-cache:
|
||||
redis-node-1-data:
|
||||
redis-node-2-data:
|
||||
redis-node-3-data:
|
||||
redis-lock:
|
||||
@@ -198,6 +198,14 @@
|
||||
- name: user
|
||||
using:
|
||||
foreign_key_constraint_on: useremail
|
||||
array_relationships:
|
||||
- name: notifications
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
column: associationid
|
||||
table:
|
||||
name: notifications
|
||||
schema: public
|
||||
select_permissions:
|
||||
- role: user
|
||||
permission:
|
||||
@@ -1127,6 +1135,46 @@
|
||||
- active:
|
||||
_eq: true
|
||||
check: null
|
||||
event_triggers:
|
||||
- name: cache_bodyshop
|
||||
definition:
|
||||
enable_manual: false
|
||||
update:
|
||||
columns:
|
||||
- shopname
|
||||
- md_order_statuses
|
||||
retry_conf:
|
||||
interval_sec: 10
|
||||
num_retries: 0
|
||||
timeout_sec: 60
|
||||
webhook_from_env: HASURA_API_URL
|
||||
headers:
|
||||
- name: event-secret
|
||||
value_from_env: EVENT_SECRET
|
||||
request_transform:
|
||||
body:
|
||||
action: transform
|
||||
template: |-
|
||||
{
|
||||
"created_at": {{$body.created_at}},
|
||||
"delivery_info": {{$body.delivery_info}},
|
||||
"event": {
|
||||
"data": {
|
||||
"new": {
|
||||
"id": {{$body.event.data.new.id}},
|
||||
"shopname": {{$body.event.data.new.shopname}},
|
||||
"md_order_statuses": {{$body.event.data.new.md_order_statuses}}
|
||||
}
|
||||
},
|
||||
"op": {{$body.event.op}},
|
||||
"session_variables": {{$body.event.session_variables}}
|
||||
}
|
||||
}
|
||||
method: POST
|
||||
query_params: {}
|
||||
template_engine: Kriti
|
||||
url: '{{$base_url}}/bodyshop-cache'
|
||||
version: 2
|
||||
- table:
|
||||
name: cccontracts
|
||||
schema: public
|
||||
@@ -1953,9 +2001,11 @@
|
||||
- active:
|
||||
_eq: true
|
||||
event_triggers:
|
||||
- name: notifications_docuemtns
|
||||
- name: notifications_documents
|
||||
definition:
|
||||
enable_manual: false
|
||||
insert:
|
||||
columns: '*'
|
||||
update:
|
||||
columns:
|
||||
- jobid
|
||||
@@ -3238,11 +3288,10 @@
|
||||
- name: notifications_joblines
|
||||
definition:
|
||||
enable_manual: false
|
||||
insert:
|
||||
columns: '*'
|
||||
update:
|
||||
columns:
|
||||
- critical
|
||||
- status
|
||||
retry_conf:
|
||||
interval_sec: 10
|
||||
num_retries: 0
|
||||
@@ -3252,11 +3301,14 @@
|
||||
- name: event-secret
|
||||
value_from_env: EVENT_SECRET
|
||||
request_transform:
|
||||
body:
|
||||
action: transform
|
||||
template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body?.event?.session_variables?.x-hasura-user-id ?? \"Internal\"}},\r\n \"x-hasura-role\": {{$body?.event?.session_variables?.x-hasura-role ?? \"Internal\"}}\r\n }, \r\n \"op\": \"UPDATE\",\r\n \"data\": {\r\n \"old\": {\r\n \"id\": {{$body.event.data.old.id}},\r\n \"jobid\": {{$body.event.data.old.jobid}},\r\n \"critical\": {{$body.event.data.old.critical}},\r\n \"status\": {{$body.event.data.old.status}},\r\n \"line_desc\": {{$body.event.data.old.line_desc}}\r\n },\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"jobid\": {{$body.event.data.new.jobid}},\r\n \"critical\": {{$body.event.data.new.critical}},\r\n \"status\": {{$body.event.data.new.status}},\r\n \"line_desc\": {{$body.event.data.new.line_desc}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_joblines\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"joblines\"\r\n }\r\n}\r\n"
|
||||
method: POST
|
||||
query_params: {}
|
||||
template_engine: Kriti
|
||||
url: '{{$base_url}}/notifications/events/handleJobLinesChange'
|
||||
version: 1
|
||||
version: 2
|
||||
- table:
|
||||
name: joblines_status
|
||||
schema: public
|
||||
@@ -3440,6 +3492,13 @@
|
||||
table:
|
||||
name: notes
|
||||
schema: public
|
||||
- name: notifications
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
column: jobid
|
||||
table:
|
||||
name: notifications
|
||||
schema: public
|
||||
- name: parts_dispatches
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
@@ -4514,7 +4573,7 @@
|
||||
request_transform:
|
||||
body:
|
||||
action: transform
|
||||
template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body.event.session_variables.x-hasura-user-id}}\r\n }, \r\n \"op\": \"UPDATE\",\r\n \"data\": {\r\n \"old\": {\r\n \"id\": {{$body.event.data.old.id}},\r\n \"ro_number\": {{$body.event.data.old.ro_number}},\r\n \"queued_for_parts\": {{$body.event.data.old.queued_for_parts}},\r\n \"employee_prep\": {{$body.event.data.old.employee_prep}},\r\n \"clm_total\": {{$body.event.data.old.clm_total}},\r\n \"towin\": {{$body.event.data.old.towin}},\r\n \"employee_body\": {{$body.event.data.old.employee_body}},\r\n \"converted\": {{$body.event.data.old.converted}},\r\n \"scheduled_in\": {{$body.event.data.old.scheduled_in}},\r\n \"scheduled_completion\": {{$body.event.data.old.scheduled_completion}},\r\n \"scheduled_delivery\": {{$body.event.data.old.scheduled_delivery}},\r\n \"actual_delivery\": {{$body.event.data.old.actual_delivery}},\r\n \"actual_completion\": {{$body.event.data.old.actual_completion}},\r\n \"alt_transport\": {{$body.event.data.old.alt_transport}},\r\n \"date_exported\": {{$body.event.data.old.date_exported}},\r\n \"status\": {{$body.event.data.old.status}},\r\n \"employee_csr\": {{$body.event.data.old.employee_csr}},\r\n \"actual_in\": {{$body.event.data.old.actual_in}},\r\n \"deliverchecklist\": {{$body.event.data.old.deliverchecklist}},\r\n \"comment\": {{$body.event.data.old.comment}},\r\n \"employee_refinish\": {{$body.event.data.old.employee_refinish}},\r\n \"inproduction\": {{$body.event.data.old.inproduction}},\r\n \"production_vars\": {{$body.event.data.old.production_vars}},\r\n \"intakechecklist\": {{$body.event.data.old.intakechecklist}},\r\n \"cieca_ttl\": {{$body.event.data.old.cieca_ttl}},\r\n \"date_invoiced\": {{$body.event.data.old.date_invoiced}}\r\n },\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"ro_number\": {{$body.event.data.old.ro_number}},\r\n \"queued_for_parts\": {{$body.event.data.new.queued_for_parts}},\r\n \"employee_prep\": {{$body.event.data.new.employee_prep}},\r\n \"clm_total\": {{$body.event.data.new.clm_total}},\r\n \"towin\": {{$body.event.data.new.towin}},\r\n \"employee_body\": {{$body.event.data.new.employee_body}},\r\n \"converted\": {{$body.event.data.new.converted}},\r\n \"scheduled_in\": {{$body.event.data.new.scheduled_in}},\r\n \"scheduled_completion\": {{$body.event.data.new.scheduled_completion}},\r\n \"scheduled_delivery\": {{$body.event.data.new.scheduled_delivery}},\r\n \"actual_delivery\": {{$body.event.data.new.actual_delivery}},\r\n \"actual_completion\": {{$body.event.data.new.actual_completion}},\r\n \"alt_transport\": {{$body.event.data.new.alt_transport}},\r\n \"date_exported\": {{$body.event.data.new.date_exported}},\r\n \"status\": {{$body.event.data.new.status}},\r\n \"employee_csr\": {{$body.event.data.new.employee_csr}},\r\n \"actual_in\": {{$body.event.data.new.actual_in}},\r\n \"deliverchecklist\": {{$body.event.data.new.deliverchecklist}},\r\n \"comment\": {{$body.event.data.new.comment}},\r\n \"employee_refinish\": {{$body.event.data.new.employee_refinish}},\r\n \"inproduction\": {{$body.event.data.new.inproduction}},\r\n \"production_vars\": {{$body.event.data.new.production_vars}},\r\n \"intakechecklist\": {{$body.event.data.new.intakechecklist}},\r\n \"cieca_ttl\": {{$body.event.data.new.cieca_ttl}},\r\n \"date_invoiced\": {{$body.event.data.new.date_invoiced}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_jobs\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"jobs\"\r\n }\r\n}\r\n"
|
||||
template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body?.event?.session_variables?.x-hasura-user-id ?? \"Internal\"}},\r\n \"x-hasura-role\": {{$body?.event?.session_variables?.x-hasura-role ?? \"Internal\"}}\r\n }, \r\n \"op\": \"UPDATE\",\r\n \"data\": {\r\n \"old\": {\r\n \"id\": {{$body.event.data.old.id}},\r\n \"ro_number\": {{$body.event.data.old.ro_number}},\r\n \"queued_for_parts\": {{$body.event.data.old.queued_for_parts}},\r\n \"employee_prep\": {{$body.event.data.old.employee_prep}},\r\n \"clm_total\": {{$body.event.data.old.clm_total}},\r\n \"towin\": {{$body.event.data.old.towin}},\r\n \"employee_body\": {{$body.event.data.old.employee_body}},\r\n \"converted\": {{$body.event.data.old.converted}},\r\n \"scheduled_in\": {{$body.event.data.old.scheduled_in}},\r\n \"scheduled_completion\": {{$body.event.data.old.scheduled_completion}},\r\n \"scheduled_delivery\": {{$body.event.data.old.scheduled_delivery}},\r\n \"actual_delivery\": {{$body.event.data.old.actual_delivery}},\r\n \"actual_completion\": {{$body.event.data.old.actual_completion}},\r\n \"alt_transport\": {{$body.event.data.old.alt_transport}},\r\n \"date_exported\": {{$body.event.data.old.date_exported}},\r\n \"status\": {{$body.event.data.old.status}},\r\n \"employee_csr\": {{$body.event.data.old.employee_csr}},\r\n \"actual_in\": {{$body.event.data.old.actual_in}},\r\n \"deliverchecklist\": {{$body.event.data.old.deliverchecklist}},\r\n \"comment\": {{$body.event.data.old.comment}},\r\n \"employee_refinish\": {{$body.event.data.old.employee_refinish}},\r\n \"inproduction\": {{$body.event.data.old.inproduction}},\r\n \"production_vars\": {{$body.event.data.old.production_vars}},\r\n \"intakechecklist\": {{$body.event.data.old.intakechecklist}},\r\n \"cieca_ttl\": {{$body.event.data.old.cieca_ttl}},\r\n \"date_invoiced\": {{$body.event.data.old.date_invoiced}}\r\n },\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"ro_number\": {{$body.event.data.old.ro_number}},\r\n \"queued_for_parts\": {{$body.event.data.new.queued_for_parts}},\r\n \"employee_prep\": {{$body.event.data.new.employee_prep}},\r\n \"clm_total\": {{$body.event.data.new.clm_total}},\r\n \"towin\": {{$body.event.data.new.towin}},\r\n \"employee_body\": {{$body.event.data.new.employee_body}},\r\n \"converted\": {{$body.event.data.new.converted}},\r\n \"scheduled_in\": {{$body.event.data.new.scheduled_in}},\r\n \"scheduled_completion\": {{$body.event.data.new.scheduled_completion}},\r\n \"scheduled_delivery\": {{$body.event.data.new.scheduled_delivery}},\r\n \"actual_delivery\": {{$body.event.data.new.actual_delivery}},\r\n \"actual_completion\": {{$body.event.data.new.actual_completion}},\r\n \"alt_transport\": {{$body.event.data.new.alt_transport}},\r\n \"date_exported\": {{$body.event.data.new.date_exported}},\r\n \"status\": {{$body.event.data.new.status}},\r\n \"employee_csr\": {{$body.event.data.new.employee_csr}},\r\n \"actual_in\": {{$body.event.data.new.actual_in}},\r\n \"deliverchecklist\": {{$body.event.data.new.deliverchecklist}},\r\n \"comment\": {{$body.event.data.new.comment}},\r\n \"employee_refinish\": {{$body.event.data.new.employee_refinish}},\r\n \"inproduction\": {{$body.event.data.new.inproduction}},\r\n \"production_vars\": {{$body.event.data.new.production_vars}},\r\n \"intakechecklist\": {{$body.event.data.new.intakechecklist}},\r\n \"cieca_ttl\": {{$body.event.data.new.cieca_ttl}},\r\n \"date_invoiced\": {{$body.event.data.new.date_invoiced}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_jobs\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"jobs\"\r\n }\r\n}\r\n"
|
||||
method: POST
|
||||
query_params: {}
|
||||
template_engine: Kriti
|
||||
@@ -5207,32 +5266,6 @@
|
||||
- active:
|
||||
_eq: true
|
||||
check: null
|
||||
event_triggers:
|
||||
- name: notifications_parts_dispatch
|
||||
definition:
|
||||
enable_manual: false
|
||||
insert:
|
||||
columns: '*'
|
||||
retry_conf:
|
||||
interval_sec: 10
|
||||
num_retries: 0
|
||||
timeout_sec: 60
|
||||
webhook_from_env: HASURA_API_URL
|
||||
headers:
|
||||
- name: event-secret
|
||||
value_from_env: EVENT_SECRET
|
||||
request_transform:
|
||||
body:
|
||||
action: transform
|
||||
template: |-
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
method: POST
|
||||
query_params: {}
|
||||
template_engine: Kriti
|
||||
url: '{{$base_url}}/notifications/events/handlePartsDispatchChange'
|
||||
version: 2
|
||||
- table:
|
||||
name: parts_dispatch_lines
|
||||
schema: public
|
||||
@@ -6231,10 +6264,12 @@
|
||||
columns:
|
||||
- joblineid
|
||||
- assigned_to
|
||||
- due_date
|
||||
- partsorderid
|
||||
- completed
|
||||
- description
|
||||
- billid
|
||||
- title
|
||||
- priority
|
||||
retry_conf:
|
||||
interval_sec: 10
|
||||
@@ -6688,6 +6723,13 @@
|
||||
table:
|
||||
name: ioevents
|
||||
schema: public
|
||||
- name: job_watchers
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
column: user_email
|
||||
table:
|
||||
name: job_watchers
|
||||
schema: public
|
||||
- name: messages
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS "public"."idx_job_watchers_jobid_user_email_unique";
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE UNIQUE INDEX "idx_job_watchers_jobid_user_email_unique" on
|
||||
"public"."job_watchers" using btree ("jobid", "user_email");
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS "public"."notificiations_idx_jobs";
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE INDEX "notificiations_idx_jobs" on
|
||||
"public"."notifications" using btree ("jobid");
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS "public"."notifications_idx_associations";
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE INDEX "notifications_idx_associations" on
|
||||
"public"."notifications" using btree ("associationid");
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- CREATE INDEX idx_notifications_created_at_not_read ON notifications(created_at desc, read) where read is null;
|
||||
1
hasura/migrations/1741904614090_run_sql_migration/up.sql
Normal file
1
hasura/migrations/1741904614090_run_sql_migration/up.sql
Normal file
@@ -0,0 +1 @@
|
||||
CREATE INDEX idx_notifications_created_at_not_read ON notifications(created_at desc, read) where read is null;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- CREATE INDEX idx_notifications_associations_not_read ON notifications(associationid, read) where read is null;
|
||||
1
hasura/migrations/1741904805838_run_sql_migration/up.sql
Normal file
1
hasura/migrations/1741904805838_run_sql_migration/up.sql
Normal file
@@ -0,0 +1 @@
|
||||
CREATE INDEX idx_notifications_associations_not_read ON notifications(associationid, read) where read is null;
|
||||
45
nginx-websocket.conf
Normal file
45
nginx-websocket.conf
Normal file
@@ -0,0 +1,45 @@
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
upstream node_app {
|
||||
ip_hash; # Enables session persistence based on client IP
|
||||
server node-app-1:4000;
|
||||
server node-app-2:4000;
|
||||
server node-app-3:4000;
|
||||
}
|
||||
|
||||
# WebSocket upgrade configuration
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
location / {
|
||||
proxy_pass http://node_app;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# WebSocket headers
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_read_timeout 86400; # Keep WebSocket connections alive (24 hours)
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
proxy_pass http://node_app;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
469
package-lock.json
generated
469
package-lock.json
generated
@@ -9,12 +9,12 @@
|
||||
"version": "0.2.0",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-cloudwatch-logs": "^3.750.0",
|
||||
"@aws-sdk/client-elasticache": "^3.755.0",
|
||||
"@aws-sdk/client-s3": "^3.750.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.750.0",
|
||||
"@aws-sdk/client-ses": "^3.750.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.750.0",
|
||||
"@aws-sdk/client-cloudwatch-logs": "^3.758.0",
|
||||
"@aws-sdk/client-elasticache": "^3.758.0",
|
||||
"@aws-sdk/client-s3": "^3.758.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.758.0",
|
||||
"@aws-sdk/client-ses": "^3.758.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.758.0",
|
||||
"@opensearch-project/opensearch": "^2.13.0",
|
||||
"@socket.io/admin-ui": "^0.5.1",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
@@ -32,7 +32,7 @@
|
||||
"cors": "2.8.5",
|
||||
"crisp-status-reporter": "^1.2.2",
|
||||
"csrf": "^3.1.0",
|
||||
"dd-trace": "^5.39.0",
|
||||
"dd-trace": "^5.40.0",
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.1",
|
||||
@@ -43,7 +43,7 @@
|
||||
"intuit-oauth": "^4.2.0",
|
||||
"ioredis": "^5.5.0",
|
||||
"json-2-csv": "^5.5.8",
|
||||
"juice": "^11.0.0",
|
||||
"juice": "^11.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.47",
|
||||
@@ -56,7 +56,7 @@
|
||||
"redis": "^4.7.0",
|
||||
"rimraf": "^6.0.1",
|
||||
"skia-canvas": "^2.0.2",
|
||||
"soap": "^1.1.8",
|
||||
"soap": "^1.1.9",
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-adapter": "^2.5.5",
|
||||
"ssh2-sftp-client": "^11.0.0",
|
||||
@@ -75,7 +75,7 @@
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"globals": "^15.15.0",
|
||||
"p-limit": "^3.1.0",
|
||||
"prettier": "^3.5.2",
|
||||
"prettier": "^3.5.3",
|
||||
"source-map-explorer": "^2.5.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -286,26 +286,26 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/client-cloudwatch-logs": {
|
||||
"version": "3.750.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.750.0.tgz",
|
||||
"integrity": "sha512-nXzQ+x6tPKSzXE9eo4IMuxQr/Cc+R53CFBqnmq3WKJEUao7cXxLKmxC/6NiJg89Vif9QEeuP4T0hTjyIHsYezg==",
|
||||
"version": "3.758.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.758.0.tgz",
|
||||
"integrity": "sha512-IlEIm5h4vfeoZyY8Op4W6lX1lqcEYE3DRKl+fMKRTFttvJ+AJfuZlAgFlMh9OPFQ0ZMLe8etoxHwKN50YCLivw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-browser": "5.2.0",
|
||||
"@aws-crypto/sha256-js": "5.2.0",
|
||||
"@aws-sdk/core": "3.750.0",
|
||||
"@aws-sdk/credential-provider-node": "3.750.0",
|
||||
"@aws-sdk/core": "3.758.0",
|
||||
"@aws-sdk/credential-provider-node": "3.758.0",
|
||||
"@aws-sdk/middleware-host-header": "3.734.0",
|
||||
"@aws-sdk/middleware-logger": "3.734.0",
|
||||
"@aws-sdk/middleware-recursion-detection": "3.734.0",
|
||||
"@aws-sdk/middleware-user-agent": "3.750.0",
|
||||
"@aws-sdk/middleware-user-agent": "3.758.0",
|
||||
"@aws-sdk/region-config-resolver": "3.734.0",
|
||||
"@aws-sdk/types": "3.734.0",
|
||||
"@aws-sdk/util-endpoints": "3.743.0",
|
||||
"@aws-sdk/util-user-agent-browser": "3.734.0",
|
||||
"@aws-sdk/util-user-agent-node": "3.750.0",
|
||||
"@aws-sdk/util-user-agent-node": "3.758.0",
|
||||
"@smithy/config-resolver": "^4.0.1",
|
||||
"@smithy/core": "^3.1.4",
|
||||
"@smithy/core": "^3.1.5",
|
||||
"@smithy/eventstream-serde-browser": "^4.0.1",
|
||||
"@smithy/eventstream-serde-config-resolver": "^4.0.1",
|
||||
"@smithy/eventstream-serde-node": "^4.0.1",
|
||||
@@ -313,21 +313,21 @@
|
||||
"@smithy/hash-node": "^4.0.1",
|
||||
"@smithy/invalid-dependency": "^4.0.1",
|
||||
"@smithy/middleware-content-length": "^4.0.1",
|
||||
"@smithy/middleware-endpoint": "^4.0.5",
|
||||
"@smithy/middleware-retry": "^4.0.6",
|
||||
"@smithy/middleware-endpoint": "^4.0.6",
|
||||
"@smithy/middleware-retry": "^4.0.7",
|
||||
"@smithy/middleware-serde": "^4.0.2",
|
||||
"@smithy/middleware-stack": "^4.0.1",
|
||||
"@smithy/node-config-provider": "^4.0.1",
|
||||
"@smithy/node-http-handler": "^4.0.2",
|
||||
"@smithy/node-http-handler": "^4.0.3",
|
||||
"@smithy/protocol-http": "^5.0.1",
|
||||
"@smithy/smithy-client": "^4.1.5",
|
||||
"@smithy/smithy-client": "^4.1.6",
|
||||
"@smithy/types": "^4.1.0",
|
||||
"@smithy/url-parser": "^4.0.1",
|
||||
"@smithy/util-base64": "^4.0.0",
|
||||
"@smithy/util-body-length-browser": "^4.0.0",
|
||||
"@smithy/util-body-length-node": "^4.0.0",
|
||||
"@smithy/util-defaults-mode-browser": "^4.0.6",
|
||||
"@smithy/util-defaults-mode-node": "^4.0.6",
|
||||
"@smithy/util-defaults-mode-browser": "^4.0.7",
|
||||
"@smithy/util-defaults-mode-node": "^4.0.7",
|
||||
"@smithy/util-endpoints": "^3.0.1",
|
||||
"@smithy/util-middleware": "^4.0.1",
|
||||
"@smithy/util-retry": "^4.0.1",
|
||||
@@ -354,45 +354,45 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/client-elasticache": {
|
||||
"version": "3.755.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-elasticache/-/client-elasticache-3.755.0.tgz",
|
||||
"integrity": "sha512-8BQb92HtloPR8b0EilHexTCXL5ASk+E4fZn2RLsEtjY/JjSAezvv2RT0QJ7j/h/fQ6CBzq/oi87IPqMBZ0qfYw==",
|
||||
"version": "3.758.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-elasticache/-/client-elasticache-3.758.0.tgz",
|
||||
"integrity": "sha512-qmDOTHhB0hUm/Ifypi6+zjUR4dl7H576oM4/p2RUgkjyz2RgJaLJhyX32TDDzcX2maevNHJ3TijXOkGxoGDeog==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-browser": "5.2.0",
|
||||
"@aws-crypto/sha256-js": "5.2.0",
|
||||
"@aws-sdk/core": "3.750.0",
|
||||
"@aws-sdk/credential-provider-node": "3.750.0",
|
||||
"@aws-sdk/core": "3.758.0",
|
||||
"@aws-sdk/credential-provider-node": "3.758.0",
|
||||
"@aws-sdk/middleware-host-header": "3.734.0",
|
||||
"@aws-sdk/middleware-logger": "3.734.0",
|
||||
"@aws-sdk/middleware-recursion-detection": "3.734.0",
|
||||
"@aws-sdk/middleware-user-agent": "3.750.0",
|
||||
"@aws-sdk/middleware-user-agent": "3.758.0",
|
||||
"@aws-sdk/region-config-resolver": "3.734.0",
|
||||
"@aws-sdk/types": "3.734.0",
|
||||
"@aws-sdk/util-endpoints": "3.743.0",
|
||||
"@aws-sdk/util-user-agent-browser": "3.734.0",
|
||||
"@aws-sdk/util-user-agent-node": "3.750.0",
|
||||
"@aws-sdk/util-user-agent-node": "3.758.0",
|
||||
"@smithy/config-resolver": "^4.0.1",
|
||||
"@smithy/core": "^3.1.4",
|
||||
"@smithy/core": "^3.1.5",
|
||||
"@smithy/fetch-http-handler": "^5.0.1",
|
||||
"@smithy/hash-node": "^4.0.1",
|
||||
"@smithy/invalid-dependency": "^4.0.1",
|
||||
"@smithy/middleware-content-length": "^4.0.1",
|
||||
"@smithy/middleware-endpoint": "^4.0.5",
|
||||
"@smithy/middleware-retry": "^4.0.6",
|
||||
"@smithy/middleware-endpoint": "^4.0.6",
|
||||
"@smithy/middleware-retry": "^4.0.7",
|
||||
"@smithy/middleware-serde": "^4.0.2",
|
||||
"@smithy/middleware-stack": "^4.0.1",
|
||||
"@smithy/node-config-provider": "^4.0.1",
|
||||
"@smithy/node-http-handler": "^4.0.2",
|
||||
"@smithy/node-http-handler": "^4.0.3",
|
||||
"@smithy/protocol-http": "^5.0.1",
|
||||
"@smithy/smithy-client": "^4.1.5",
|
||||
"@smithy/smithy-client": "^4.1.6",
|
||||
"@smithy/types": "^4.1.0",
|
||||
"@smithy/url-parser": "^4.0.1",
|
||||
"@smithy/util-base64": "^4.0.0",
|
||||
"@smithy/util-body-length-browser": "^4.0.0",
|
||||
"@smithy/util-body-length-node": "^4.0.0",
|
||||
"@smithy/util-defaults-mode-browser": "^4.0.6",
|
||||
"@smithy/util-defaults-mode-node": "^4.0.6",
|
||||
"@smithy/util-defaults-mode-browser": "^4.0.7",
|
||||
"@smithy/util-defaults-mode-node": "^4.0.7",
|
||||
"@smithy/util-endpoints": "^3.0.1",
|
||||
"@smithy/util-middleware": "^4.0.1",
|
||||
"@smithy/util-retry": "^4.0.1",
|
||||
@@ -405,35 +405,35 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/client-s3": {
|
||||
"version": "3.750.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.750.0.tgz",
|
||||
"integrity": "sha512-S9G9noCeBxchoMVkHYrRi1A1xW/VOTP2W7X34lP+Y7Wpl32yMA7IJo0fAGAuTc0q1Nu6/pXDm+oDG7rhTCA1tg==",
|
||||
"version": "3.758.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.758.0.tgz",
|
||||
"integrity": "sha512-f8SlhU9/93OC/WEI6xVJf/x/GoQFj9a/xXK6QCtr5fvCjfSLgMVFmKTiIl/tgtDRzxUDc8YS6EGtbHjJ3Y/atg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha1-browser": "5.2.0",
|
||||
"@aws-crypto/sha256-browser": "5.2.0",
|
||||
"@aws-crypto/sha256-js": "5.2.0",
|
||||
"@aws-sdk/core": "3.750.0",
|
||||
"@aws-sdk/credential-provider-node": "3.750.0",
|
||||
"@aws-sdk/core": "3.758.0",
|
||||
"@aws-sdk/credential-provider-node": "3.758.0",
|
||||
"@aws-sdk/middleware-bucket-endpoint": "3.734.0",
|
||||
"@aws-sdk/middleware-expect-continue": "3.734.0",
|
||||
"@aws-sdk/middleware-flexible-checksums": "3.750.0",
|
||||
"@aws-sdk/middleware-flexible-checksums": "3.758.0",
|
||||
"@aws-sdk/middleware-host-header": "3.734.0",
|
||||
"@aws-sdk/middleware-location-constraint": "3.734.0",
|
||||
"@aws-sdk/middleware-logger": "3.734.0",
|
||||
"@aws-sdk/middleware-recursion-detection": "3.734.0",
|
||||
"@aws-sdk/middleware-sdk-s3": "3.750.0",
|
||||
"@aws-sdk/middleware-sdk-s3": "3.758.0",
|
||||
"@aws-sdk/middleware-ssec": "3.734.0",
|
||||
"@aws-sdk/middleware-user-agent": "3.750.0",
|
||||
"@aws-sdk/middleware-user-agent": "3.758.0",
|
||||
"@aws-sdk/region-config-resolver": "3.734.0",
|
||||
"@aws-sdk/signature-v4-multi-region": "3.750.0",
|
||||
"@aws-sdk/signature-v4-multi-region": "3.758.0",
|
||||
"@aws-sdk/types": "3.734.0",
|
||||
"@aws-sdk/util-endpoints": "3.743.0",
|
||||
"@aws-sdk/util-user-agent-browser": "3.734.0",
|
||||
"@aws-sdk/util-user-agent-node": "3.750.0",
|
||||
"@aws-sdk/util-user-agent-node": "3.758.0",
|
||||
"@aws-sdk/xml-builder": "3.734.0",
|
||||
"@smithy/config-resolver": "^4.0.1",
|
||||
"@smithy/core": "^3.1.4",
|
||||
"@smithy/core": "^3.1.5",
|
||||
"@smithy/eventstream-serde-browser": "^4.0.1",
|
||||
"@smithy/eventstream-serde-config-resolver": "^4.0.1",
|
||||
"@smithy/eventstream-serde-node": "^4.0.1",
|
||||
@@ -444,25 +444,25 @@
|
||||
"@smithy/invalid-dependency": "^4.0.1",
|
||||
"@smithy/md5-js": "^4.0.1",
|
||||
"@smithy/middleware-content-length": "^4.0.1",
|
||||
"@smithy/middleware-endpoint": "^4.0.5",
|
||||
"@smithy/middleware-retry": "^4.0.6",
|
||||
"@smithy/middleware-endpoint": "^4.0.6",
|
||||
"@smithy/middleware-retry": "^4.0.7",
|
||||
"@smithy/middleware-serde": "^4.0.2",
|
||||
"@smithy/middleware-stack": "^4.0.1",
|
||||
"@smithy/node-config-provider": "^4.0.1",
|
||||
"@smithy/node-http-handler": "^4.0.2",
|
||||
"@smithy/node-http-handler": "^4.0.3",
|
||||
"@smithy/protocol-http": "^5.0.1",
|
||||
"@smithy/smithy-client": "^4.1.5",
|
||||
"@smithy/smithy-client": "^4.1.6",
|
||||
"@smithy/types": "^4.1.0",
|
||||
"@smithy/url-parser": "^4.0.1",
|
||||
"@smithy/util-base64": "^4.0.0",
|
||||
"@smithy/util-body-length-browser": "^4.0.0",
|
||||
"@smithy/util-body-length-node": "^4.0.0",
|
||||
"@smithy/util-defaults-mode-browser": "^4.0.6",
|
||||
"@smithy/util-defaults-mode-node": "^4.0.6",
|
||||
"@smithy/util-defaults-mode-browser": "^4.0.7",
|
||||
"@smithy/util-defaults-mode-node": "^4.0.7",
|
||||
"@smithy/util-endpoints": "^3.0.1",
|
||||
"@smithy/util-middleware": "^4.0.1",
|
||||
"@smithy/util-retry": "^4.0.1",
|
||||
"@smithy/util-stream": "^4.1.1",
|
||||
"@smithy/util-stream": "^4.1.2",
|
||||
"@smithy/util-utf8": "^4.0.0",
|
||||
"@smithy/util-waiter": "^4.0.2",
|
||||
"tslib": "^2.6.2"
|
||||
@@ -472,45 +472,45 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/client-secrets-manager": {
|
||||
"version": "3.750.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.750.0.tgz",
|
||||
"integrity": "sha512-5JrrOQECJtcUFodKqBNKTk82WycIu/4cVFYf6QXsZQ/0bJ8zlp3vDyTeAjLriZXRXrb8HZlWqOsPCPT3wEBYdg==",
|
||||
"version": "3.758.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.758.0.tgz",
|
||||
"integrity": "sha512-Vi4cdCim0jQx3rrU5R1W4v3czoWL0ajBtoI15oSSt7cwLjzNA0xq4nXSa6rahjTgtZWlLeBprbquvxNzY3qg5Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-browser": "5.2.0",
|
||||
"@aws-crypto/sha256-js": "5.2.0",
|
||||
"@aws-sdk/core": "3.750.0",
|
||||
"@aws-sdk/credential-provider-node": "3.750.0",
|
||||
"@aws-sdk/core": "3.758.0",
|
||||
"@aws-sdk/credential-provider-node": "3.758.0",
|
||||
"@aws-sdk/middleware-host-header": "3.734.0",
|
||||
"@aws-sdk/middleware-logger": "3.734.0",
|
||||
"@aws-sdk/middleware-recursion-detection": "3.734.0",
|
||||
"@aws-sdk/middleware-user-agent": "3.750.0",
|
||||
"@aws-sdk/middleware-user-agent": "3.758.0",
|
||||
"@aws-sdk/region-config-resolver": "3.734.0",
|
||||
"@aws-sdk/types": "3.734.0",
|
||||
"@aws-sdk/util-endpoints": "3.743.0",
|
||||
"@aws-sdk/util-user-agent-browser": "3.734.0",
|
||||
"@aws-sdk/util-user-agent-node": "3.750.0",
|
||||
"@aws-sdk/util-user-agent-node": "3.758.0",
|
||||
"@smithy/config-resolver": "^4.0.1",
|
||||
"@smithy/core": "^3.1.4",
|
||||
"@smithy/core": "^3.1.5",
|
||||
"@smithy/fetch-http-handler": "^5.0.1",
|
||||
"@smithy/hash-node": "^4.0.1",
|
||||
"@smithy/invalid-dependency": "^4.0.1",
|
||||
"@smithy/middleware-content-length": "^4.0.1",
|
||||
"@smithy/middleware-endpoint": "^4.0.5",
|
||||
"@smithy/middleware-retry": "^4.0.6",
|
||||
"@smithy/middleware-endpoint": "^4.0.6",
|
||||
"@smithy/middleware-retry": "^4.0.7",
|
||||
"@smithy/middleware-serde": "^4.0.2",
|
||||
"@smithy/middleware-stack": "^4.0.1",
|
||||
"@smithy/node-config-provider": "^4.0.1",
|
||||
"@smithy/node-http-handler": "^4.0.2",
|
||||
"@smithy/node-http-handler": "^4.0.3",
|
||||
"@smithy/protocol-http": "^5.0.1",
|
||||
"@smithy/smithy-client": "^4.1.5",
|
||||
"@smithy/smithy-client": "^4.1.6",
|
||||
"@smithy/types": "^4.1.0",
|
||||
"@smithy/url-parser": "^4.0.1",
|
||||
"@smithy/util-base64": "^4.0.0",
|
||||
"@smithy/util-body-length-browser": "^4.0.0",
|
||||
"@smithy/util-body-length-node": "^4.0.0",
|
||||
"@smithy/util-defaults-mode-browser": "^4.0.6",
|
||||
"@smithy/util-defaults-mode-node": "^4.0.6",
|
||||
"@smithy/util-defaults-mode-browser": "^4.0.7",
|
||||
"@smithy/util-defaults-mode-node": "^4.0.7",
|
||||
"@smithy/util-endpoints": "^3.0.1",
|
||||
"@smithy/util-middleware": "^4.0.1",
|
||||
"@smithy/util-retry": "^4.0.1",
|
||||
@@ -537,45 +537,45 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/client-ses": {
|
||||
"version": "3.750.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.750.0.tgz",
|
||||
"integrity": "sha512-0apX2PEzT/09XiO42jNHjkszz/k2RLcIiaLbl1ngcKY1lWzMzIiGIqXw7Emei8iye2o6EsWuBG1p3k30iSyjhg==",
|
||||
"version": "3.758.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.758.0.tgz",
|
||||
"integrity": "sha512-cWBjZqY7SsFdTTSw3726DEPy3d7FfQ8qrw21RCukM/p3Ty42NWauHkqgxOmRygeiSY3ygHmWexc32B+4RXXqTw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-browser": "5.2.0",
|
||||
"@aws-crypto/sha256-js": "5.2.0",
|
||||
"@aws-sdk/core": "3.750.0",
|
||||
"@aws-sdk/credential-provider-node": "3.750.0",
|
||||
"@aws-sdk/core": "3.758.0",
|
||||
"@aws-sdk/credential-provider-node": "3.758.0",
|
||||
"@aws-sdk/middleware-host-header": "3.734.0",
|
||||
"@aws-sdk/middleware-logger": "3.734.0",
|
||||
"@aws-sdk/middleware-recursion-detection": "3.734.0",
|
||||
"@aws-sdk/middleware-user-agent": "3.750.0",
|
||||
"@aws-sdk/middleware-user-agent": "3.758.0",
|
||||
"@aws-sdk/region-config-resolver": "3.734.0",
|
||||
"@aws-sdk/types": "3.734.0",
|
||||
"@aws-sdk/util-endpoints": "3.743.0",
|
||||
"@aws-sdk/util-user-agent-browser": "3.734.0",
|
||||
"@aws-sdk/util-user-agent-node": "3.750.0",
|
||||
"@aws-sdk/util-user-agent-node": "3.758.0",
|
||||
"@smithy/config-resolver": "^4.0.1",
|
||||
"@smithy/core": "^3.1.4",
|
||||
"@smithy/core": "^3.1.5",
|
||||
"@smithy/fetch-http-handler": "^5.0.1",
|
||||
"@smithy/hash-node": "^4.0.1",
|
||||
"@smithy/invalid-dependency": "^4.0.1",
|
||||
"@smithy/middleware-content-length": "^4.0.1",
|
||||
"@smithy/middleware-endpoint": "^4.0.5",
|
||||
"@smithy/middleware-retry": "^4.0.6",
|
||||
"@smithy/middleware-endpoint": "^4.0.6",
|
||||
"@smithy/middleware-retry": "^4.0.7",
|
||||
"@smithy/middleware-serde": "^4.0.2",
|
||||
"@smithy/middleware-stack": "^4.0.1",
|
||||
"@smithy/node-config-provider": "^4.0.1",
|
||||
"@smithy/node-http-handler": "^4.0.2",
|
||||
"@smithy/node-http-handler": "^4.0.3",
|
||||
"@smithy/protocol-http": "^5.0.1",
|
||||
"@smithy/smithy-client": "^4.1.5",
|
||||
"@smithy/smithy-client": "^4.1.6",
|
||||
"@smithy/types": "^4.1.0",
|
||||
"@smithy/url-parser": "^4.0.1",
|
||||
"@smithy/util-base64": "^4.0.0",
|
||||
"@smithy/util-body-length-browser": "^4.0.0",
|
||||
"@smithy/util-body-length-node": "^4.0.0",
|
||||
"@smithy/util-defaults-mode-browser": "^4.0.6",
|
||||
"@smithy/util-defaults-mode-node": "^4.0.6",
|
||||
"@smithy/util-defaults-mode-browser": "^4.0.7",
|
||||
"@smithy/util-defaults-mode-node": "^4.0.7",
|
||||
"@smithy/util-endpoints": "^3.0.1",
|
||||
"@smithy/util-middleware": "^4.0.1",
|
||||
"@smithy/util-retry": "^4.0.1",
|
||||
@@ -588,44 +588,44 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/client-sso": {
|
||||
"version": "3.750.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.750.0.tgz",
|
||||
"integrity": "sha512-y0Rx6pTQXw0E61CaptpZF65qNggjqOgymq/RYZU5vWba5DGQ+iqGt8Yq8s+jfBoBBNXshxq8l8Dl5Uq/JTY1wg==",
|
||||
"version": "3.758.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.758.0.tgz",
|
||||
"integrity": "sha512-BoGO6IIWrLyLxQG6txJw6RT2urmbtlwfggapNCrNPyYjlXpzTSJhBYjndg7TpDATFd0SXL0zm8y/tXsUXNkdYQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-browser": "5.2.0",
|
||||
"@aws-crypto/sha256-js": "5.2.0",
|
||||
"@aws-sdk/core": "3.750.0",
|
||||
"@aws-sdk/core": "3.758.0",
|
||||
"@aws-sdk/middleware-host-header": "3.734.0",
|
||||
"@aws-sdk/middleware-logger": "3.734.0",
|
||||
"@aws-sdk/middleware-recursion-detection": "3.734.0",
|
||||
"@aws-sdk/middleware-user-agent": "3.750.0",
|
||||
"@aws-sdk/middleware-user-agent": "3.758.0",
|
||||
"@aws-sdk/region-config-resolver": "3.734.0",
|
||||
"@aws-sdk/types": "3.734.0",
|
||||
"@aws-sdk/util-endpoints": "3.743.0",
|
||||
"@aws-sdk/util-user-agent-browser": "3.734.0",
|
||||
"@aws-sdk/util-user-agent-node": "3.750.0",
|
||||
"@aws-sdk/util-user-agent-node": "3.758.0",
|
||||
"@smithy/config-resolver": "^4.0.1",
|
||||
"@smithy/core": "^3.1.4",
|
||||
"@smithy/core": "^3.1.5",
|
||||
"@smithy/fetch-http-handler": "^5.0.1",
|
||||
"@smithy/hash-node": "^4.0.1",
|
||||
"@smithy/invalid-dependency": "^4.0.1",
|
||||
"@smithy/middleware-content-length": "^4.0.1",
|
||||
"@smithy/middleware-endpoint": "^4.0.5",
|
||||
"@smithy/middleware-retry": "^4.0.6",
|
||||
"@smithy/middleware-endpoint": "^4.0.6",
|
||||
"@smithy/middleware-retry": "^4.0.7",
|
||||
"@smithy/middleware-serde": "^4.0.2",
|
||||
"@smithy/middleware-stack": "^4.0.1",
|
||||
"@smithy/node-config-provider": "^4.0.1",
|
||||
"@smithy/node-http-handler": "^4.0.2",
|
||||
"@smithy/node-http-handler": "^4.0.3",
|
||||
"@smithy/protocol-http": "^5.0.1",
|
||||
"@smithy/smithy-client": "^4.1.5",
|
||||
"@smithy/smithy-client": "^4.1.6",
|
||||
"@smithy/types": "^4.1.0",
|
||||
"@smithy/url-parser": "^4.0.1",
|
||||
"@smithy/util-base64": "^4.0.0",
|
||||
"@smithy/util-body-length-browser": "^4.0.0",
|
||||
"@smithy/util-body-length-node": "^4.0.0",
|
||||
"@smithy/util-defaults-mode-browser": "^4.0.6",
|
||||
"@smithy/util-defaults-mode-node": "^4.0.6",
|
||||
"@smithy/util-defaults-mode-browser": "^4.0.7",
|
||||
"@smithy/util-defaults-mode-node": "^4.0.7",
|
||||
"@smithy/util-endpoints": "^3.0.1",
|
||||
"@smithy/util-middleware": "^4.0.1",
|
||||
"@smithy/util-retry": "^4.0.1",
|
||||
@@ -637,18 +637,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/core": {
|
||||
"version": "3.750.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.750.0.tgz",
|
||||
"integrity": "sha512-bZ5K7N5L4+Pa2epbVpUQqd1XLG2uU8BGs/Sd+2nbgTf+lNQJyIxAg/Qsrjz9MzmY8zzQIeRQEkNmR6yVAfCmmQ==",
|
||||
"version": "3.758.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.758.0.tgz",
|
||||
"integrity": "sha512-0RswbdR9jt/XKemaLNuxi2gGr4xGlHyGxkTdhSQzCyUe9A9OPCoLl3rIESRguQEech+oJnbHk/wuiwHqTuP9sg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/types": "3.734.0",
|
||||
"@smithy/core": "^3.1.4",
|
||||
"@smithy/core": "^3.1.5",
|
||||
"@smithy/node-config-provider": "^4.0.1",
|
||||
"@smithy/property-provider": "^4.0.1",
|
||||
"@smithy/protocol-http": "^5.0.1",
|
||||
"@smithy/signature-v4": "^5.0.1",
|
||||
"@smithy/smithy-client": "^4.1.5",
|
||||
"@smithy/smithy-client": "^4.1.6",
|
||||
"@smithy/types": "^4.1.0",
|
||||
"@smithy/util-middleware": "^4.0.1",
|
||||
"fast-xml-parser": "4.4.1",
|
||||
@@ -659,12 +659,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-env": {
|
||||
"version": "3.750.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.750.0.tgz",
|
||||
"integrity": "sha512-In6bsG0p/P31HcH4DBRKBbcDS/3SHvEPjfXV8ODPWZO/l3/p7IRoYBdQ07C9R+VMZU2D0+/Sc/DWK/TUNDk1+Q==",
|
||||
"version": "3.758.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.758.0.tgz",
|
||||
"integrity": "sha512-N27eFoRrO6MeUNumtNHDW9WOiwfd59LPXPqDrIa3kWL/s+fOKFHb9xIcF++bAwtcZnAxKkgpDCUP+INNZskE+w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "3.750.0",
|
||||
"@aws-sdk/core": "3.758.0",
|
||||
"@aws-sdk/types": "3.734.0",
|
||||
"@smithy/property-provider": "^4.0.1",
|
||||
"@smithy/types": "^4.1.0",
|
||||
@@ -675,20 +675,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-http": {
|
||||
"version": "3.750.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.750.0.tgz",
|
||||
"integrity": "sha512-wFB9qqfa20AB0dElsQz5ZlZT5o+a+XzpEpmg0erylmGYqEOvh8NQWfDUVpRmQuGq9VbvW/8cIbxPoNqEbPtuWQ==",
|
||||
"version": "3.758.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.758.0.tgz",
|
||||
"integrity": "sha512-Xt9/U8qUCiw1hihztWkNeIR+arg6P+yda10OuCHX6kFVx3auTlU7+hCqs3UxqniGU4dguHuftf3mRpi5/GJ33Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "3.750.0",
|
||||
"@aws-sdk/core": "3.758.0",
|
||||
"@aws-sdk/types": "3.734.0",
|
||||
"@smithy/fetch-http-handler": "^5.0.1",
|
||||
"@smithy/node-http-handler": "^4.0.2",
|
||||
"@smithy/node-http-handler": "^4.0.3",
|
||||
"@smithy/property-provider": "^4.0.1",
|
||||
"@smithy/protocol-http": "^5.0.1",
|
||||
"@smithy/smithy-client": "^4.1.5",
|
||||
"@smithy/smithy-client": "^4.1.6",
|
||||
"@smithy/types": "^4.1.0",
|
||||
"@smithy/util-stream": "^4.1.1",
|
||||
"@smithy/util-stream": "^4.1.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -696,18 +696,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-ini": {
|
||||
"version": "3.750.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.750.0.tgz",
|
||||
"integrity": "sha512-2YIZmyEr5RUd3uxXpxOLD9G67Bibm4I/65M6vKFP17jVMUT+R1nL7mKqmhEVO2p+BoeV+bwMyJ/jpTYG368PCg==",
|
||||
"version": "3.758.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.758.0.tgz",
|
||||
"integrity": "sha512-cymSKMcP5d+OsgetoIZ5QCe1wnp2Q/tq+uIxVdh9MbfdBBEnl9Ecq6dH6VlYS89sp4QKuxHxkWXVnbXU3Q19Aw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "3.750.0",
|
||||
"@aws-sdk/credential-provider-env": "3.750.0",
|
||||
"@aws-sdk/credential-provider-http": "3.750.0",
|
||||
"@aws-sdk/credential-provider-process": "3.750.0",
|
||||
"@aws-sdk/credential-provider-sso": "3.750.0",
|
||||
"@aws-sdk/credential-provider-web-identity": "3.750.0",
|
||||
"@aws-sdk/nested-clients": "3.750.0",
|
||||
"@aws-sdk/core": "3.758.0",
|
||||
"@aws-sdk/credential-provider-env": "3.758.0",
|
||||
"@aws-sdk/credential-provider-http": "3.758.0",
|
||||
"@aws-sdk/credential-provider-process": "3.758.0",
|
||||
"@aws-sdk/credential-provider-sso": "3.758.0",
|
||||
"@aws-sdk/credential-provider-web-identity": "3.758.0",
|
||||
"@aws-sdk/nested-clients": "3.758.0",
|
||||
"@aws-sdk/types": "3.734.0",
|
||||
"@smithy/credential-provider-imds": "^4.0.1",
|
||||
"@smithy/property-provider": "^4.0.1",
|
||||
@@ -720,17 +720,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-node": {
|
||||
"version": "3.750.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.750.0.tgz",
|
||||
"integrity": "sha512-THWHHAceLwsOiowPEmKyhWVDlEUxH07GHSw5AQFDvNQtGKOQl0HSIFO1mKObT2Q2Vqzji9Bq8H58SO5BFtNPRw==",
|
||||
"version": "3.758.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.758.0.tgz",
|
||||
"integrity": "sha512-+DaMv63wiq7pJrhIQzZYMn4hSarKiizDoJRvyR7WGhnn0oQ/getX9Z0VNCV3i7lIFoLNTb7WMmQ9k7+z/uD5EQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-provider-env": "3.750.0",
|
||||
"@aws-sdk/credential-provider-http": "3.750.0",
|
||||
"@aws-sdk/credential-provider-ini": "3.750.0",
|
||||
"@aws-sdk/credential-provider-process": "3.750.0",
|
||||
"@aws-sdk/credential-provider-sso": "3.750.0",
|
||||
"@aws-sdk/credential-provider-web-identity": "3.750.0",
|
||||
"@aws-sdk/credential-provider-env": "3.758.0",
|
||||
"@aws-sdk/credential-provider-http": "3.758.0",
|
||||
"@aws-sdk/credential-provider-ini": "3.758.0",
|
||||
"@aws-sdk/credential-provider-process": "3.758.0",
|
||||
"@aws-sdk/credential-provider-sso": "3.758.0",
|
||||
"@aws-sdk/credential-provider-web-identity": "3.758.0",
|
||||
"@aws-sdk/types": "3.734.0",
|
||||
"@smithy/credential-provider-imds": "^4.0.1",
|
||||
"@smithy/property-provider": "^4.0.1",
|
||||
@@ -743,12 +743,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-process": {
|
||||
"version": "3.750.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.750.0.tgz",
|
||||
"integrity": "sha512-Q78SCH1n0m7tpu36sJwfrUSxI8l611OyysjQeMiIOliVfZICEoHcLHLcLkiR+tnIpZ3rk7d2EQ6R1jwlXnalMQ==",
|
||||
"version": "3.758.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.758.0.tgz",
|
||||
"integrity": "sha512-AzcY74QTPqcbXWVgjpPZ3HOmxQZYPROIBz2YINF0OQk0MhezDWV/O7Xec+K1+MPGQO3qS6EDrUUlnPLjsqieHA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "3.750.0",
|
||||
"@aws-sdk/core": "3.758.0",
|
||||
"@aws-sdk/types": "3.734.0",
|
||||
"@smithy/property-provider": "^4.0.1",
|
||||
"@smithy/shared-ini-file-loader": "^4.0.1",
|
||||
@@ -760,14 +760,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-sso": {
|
||||
"version": "3.750.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.750.0.tgz",
|
||||
"integrity": "sha512-FGYrDjXN/FOQVi/t8fHSv8zCk+NEvtFnuc4cZUj5OIbM4vrfFc5VaPyn41Uza3iv6Qq9rZg0QOwWnqK8lNrqUw==",
|
||||
"version": "3.758.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.758.0.tgz",
|
||||
"integrity": "sha512-x0FYJqcOLUCv8GLLFDYMXRAQKGjoM+L0BG4BiHYZRDf24yQWFCAZsCQAYKo6XZYh2qznbsW6f//qpyJ5b0QVKQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sso": "3.750.0",
|
||||
"@aws-sdk/core": "3.750.0",
|
||||
"@aws-sdk/token-providers": "3.750.0",
|
||||
"@aws-sdk/client-sso": "3.758.0",
|
||||
"@aws-sdk/core": "3.758.0",
|
||||
"@aws-sdk/token-providers": "3.758.0",
|
||||
"@aws-sdk/types": "3.734.0",
|
||||
"@smithy/property-provider": "^4.0.1",
|
||||
"@smithy/shared-ini-file-loader": "^4.0.1",
|
||||
@@ -779,13 +779,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-web-identity": {
|
||||
"version": "3.750.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.750.0.tgz",
|
||||
"integrity": "sha512-Nz8zs3YJ+GOTSrq+LyzbbC1Ffpt7pK38gcOyNZv76pP5MswKTUKNYBJehqwa+i7FcFQHsCk3TdhR8MT1ZR23uA==",
|
||||
"version": "3.758.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.758.0.tgz",
|
||||
"integrity": "sha512-XGguXhBqiCXMXRxcfCAVPlMbm3VyJTou79r/3mxWddHWF0XbhaQiBIbUz6vobVTD25YQRbWSmSch7VA8kI5Lrw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "3.750.0",
|
||||
"@aws-sdk/nested-clients": "3.750.0",
|
||||
"@aws-sdk/core": "3.758.0",
|
||||
"@aws-sdk/nested-clients": "3.758.0",
|
||||
"@aws-sdk/types": "3.734.0",
|
||||
"@smithy/property-provider": "^4.0.1",
|
||||
"@smithy/types": "^4.1.0",
|
||||
@@ -829,22 +829,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/middleware-flexible-checksums": {
|
||||
"version": "3.750.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.750.0.tgz",
|
||||
"integrity": "sha512-ach0d2buDnX2TUausUbiXXFWFo3IegLnCrA+Rw8I9AYVpLN9lTaRwAYJwYC6zEuW9Golff8MwkYsp/OaC5tKMw==",
|
||||
"version": "3.758.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.758.0.tgz",
|
||||
"integrity": "sha512-o8Rk71S08YTKLoSobucjnbj97OCGaXgpEDNKXpXaavUM5xLNoHCLSUPRCiEN86Ivqxg1n17Y2nSRhfbsveOXXA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-crypto/crc32": "5.2.0",
|
||||
"@aws-crypto/crc32c": "5.2.0",
|
||||
"@aws-crypto/util": "5.2.0",
|
||||
"@aws-sdk/core": "3.750.0",
|
||||
"@aws-sdk/core": "3.758.0",
|
||||
"@aws-sdk/types": "3.734.0",
|
||||
"@smithy/is-array-buffer": "^4.0.0",
|
||||
"@smithy/node-config-provider": "^4.0.1",
|
||||
"@smithy/protocol-http": "^5.0.1",
|
||||
"@smithy/types": "^4.1.0",
|
||||
"@smithy/util-middleware": "^4.0.1",
|
||||
"@smithy/util-stream": "^4.1.1",
|
||||
"@smithy/util-stream": "^4.1.2",
|
||||
"@smithy/util-utf8": "^4.0.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
@@ -911,23 +911,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/middleware-sdk-s3": {
|
||||
"version": "3.750.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.750.0.tgz",
|
||||
"integrity": "sha512-3H6Z46cmAQCHQ0z8mm7/cftY5ifiLfCjbObrbyyp2fhQs9zk6gCKzIX8Zjhw0RMd93FZi3ebRuKJWmMglf4Itw==",
|
||||
"version": "3.758.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.758.0.tgz",
|
||||
"integrity": "sha512-6mJ2zyyHPYSV6bAcaFpsdoXZJeQlR1QgBnZZ6juY/+dcYiuyWCdyLUbGzSZSE7GTfx6i+9+QWFeoIMlWKgU63A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "3.750.0",
|
||||
"@aws-sdk/core": "3.758.0",
|
||||
"@aws-sdk/types": "3.734.0",
|
||||
"@aws-sdk/util-arn-parser": "3.723.0",
|
||||
"@smithy/core": "^3.1.4",
|
||||
"@smithy/core": "^3.1.5",
|
||||
"@smithy/node-config-provider": "^4.0.1",
|
||||
"@smithy/protocol-http": "^5.0.1",
|
||||
"@smithy/signature-v4": "^5.0.1",
|
||||
"@smithy/smithy-client": "^4.1.5",
|
||||
"@smithy/smithy-client": "^4.1.6",
|
||||
"@smithy/types": "^4.1.0",
|
||||
"@smithy/util-config-provider": "^4.0.0",
|
||||
"@smithy/util-middleware": "^4.0.1",
|
||||
"@smithy/util-stream": "^4.1.1",
|
||||
"@smithy/util-stream": "^4.1.2",
|
||||
"@smithy/util-utf8": "^4.0.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
@@ -950,15 +950,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/middleware-user-agent": {
|
||||
"version": "3.750.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.750.0.tgz",
|
||||
"integrity": "sha512-YYcslDsP5+2NZoN3UwuhZGkhAHPSli7HlJHBafBrvjGV/I9f8FuOO1d1ebxGdEP4HyRXUGyh+7Ur4q+Psk0ryw==",
|
||||
"version": "3.758.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.758.0.tgz",
|
||||
"integrity": "sha512-iNyehQXtQlj69JCgfaOssgZD4HeYGOwxcaKeG6F+40cwBjTAi0+Ph1yfDwqk2qiBPIRWJ/9l2LodZbxiBqgrwg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "3.750.0",
|
||||
"@aws-sdk/core": "3.758.0",
|
||||
"@aws-sdk/types": "3.734.0",
|
||||
"@aws-sdk/util-endpoints": "3.743.0",
|
||||
"@smithy/core": "^3.1.4",
|
||||
"@smithy/core": "^3.1.5",
|
||||
"@smithy/protocol-http": "^5.0.1",
|
||||
"@smithy/types": "^4.1.0",
|
||||
"tslib": "^2.6.2"
|
||||
@@ -968,44 +968,44 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/nested-clients": {
|
||||
"version": "3.750.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.750.0.tgz",
|
||||
"integrity": "sha512-OH68BRF0rt9nDloq4zsfeHI0G21lj11a66qosaljtEP66PWm7tQ06feKbFkXHT5E1K3QhJW3nVyK8v2fEBY5fg==",
|
||||
"version": "3.758.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.758.0.tgz",
|
||||
"integrity": "sha512-YZ5s7PSvyF3Mt2h1EQulCG93uybprNGbBkPmVuy/HMMfbFTt4iL3SbKjxqvOZelm86epFfj7pvK7FliI2WOEcg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-browser": "5.2.0",
|
||||
"@aws-crypto/sha256-js": "5.2.0",
|
||||
"@aws-sdk/core": "3.750.0",
|
||||
"@aws-sdk/core": "3.758.0",
|
||||
"@aws-sdk/middleware-host-header": "3.734.0",
|
||||
"@aws-sdk/middleware-logger": "3.734.0",
|
||||
"@aws-sdk/middleware-recursion-detection": "3.734.0",
|
||||
"@aws-sdk/middleware-user-agent": "3.750.0",
|
||||
"@aws-sdk/middleware-user-agent": "3.758.0",
|
||||
"@aws-sdk/region-config-resolver": "3.734.0",
|
||||
"@aws-sdk/types": "3.734.0",
|
||||
"@aws-sdk/util-endpoints": "3.743.0",
|
||||
"@aws-sdk/util-user-agent-browser": "3.734.0",
|
||||
"@aws-sdk/util-user-agent-node": "3.750.0",
|
||||
"@aws-sdk/util-user-agent-node": "3.758.0",
|
||||
"@smithy/config-resolver": "^4.0.1",
|
||||
"@smithy/core": "^3.1.4",
|
||||
"@smithy/core": "^3.1.5",
|
||||
"@smithy/fetch-http-handler": "^5.0.1",
|
||||
"@smithy/hash-node": "^4.0.1",
|
||||
"@smithy/invalid-dependency": "^4.0.1",
|
||||
"@smithy/middleware-content-length": "^4.0.1",
|
||||
"@smithy/middleware-endpoint": "^4.0.5",
|
||||
"@smithy/middleware-retry": "^4.0.6",
|
||||
"@smithy/middleware-endpoint": "^4.0.6",
|
||||
"@smithy/middleware-retry": "^4.0.7",
|
||||
"@smithy/middleware-serde": "^4.0.2",
|
||||
"@smithy/middleware-stack": "^4.0.1",
|
||||
"@smithy/node-config-provider": "^4.0.1",
|
||||
"@smithy/node-http-handler": "^4.0.2",
|
||||
"@smithy/node-http-handler": "^4.0.3",
|
||||
"@smithy/protocol-http": "^5.0.1",
|
||||
"@smithy/smithy-client": "^4.1.5",
|
||||
"@smithy/smithy-client": "^4.1.6",
|
||||
"@smithy/types": "^4.1.0",
|
||||
"@smithy/url-parser": "^4.0.1",
|
||||
"@smithy/util-base64": "^4.0.0",
|
||||
"@smithy/util-body-length-browser": "^4.0.0",
|
||||
"@smithy/util-body-length-node": "^4.0.0",
|
||||
"@smithy/util-defaults-mode-browser": "^4.0.6",
|
||||
"@smithy/util-defaults-mode-node": "^4.0.6",
|
||||
"@smithy/util-defaults-mode-browser": "^4.0.7",
|
||||
"@smithy/util-defaults-mode-node": "^4.0.7",
|
||||
"@smithy/util-endpoints": "^3.0.1",
|
||||
"@smithy/util-middleware": "^4.0.1",
|
||||
"@smithy/util-retry": "^4.0.1",
|
||||
@@ -1034,12 +1034,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/signature-v4-multi-region": {
|
||||
"version": "3.750.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.750.0.tgz",
|
||||
"integrity": "sha512-RA9hv1Irro/CrdPcOEXKwJ0DJYJwYCsauGEdRXihrRfy8MNSR9E+mD5/Fr5Rxjaq5AHM05DYnN3mg/DU6VwzSw==",
|
||||
"version": "3.758.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.758.0.tgz",
|
||||
"integrity": "sha512-0RPCo8fYJcrenJ6bRtiUbFOSgQ1CX/GpvwtLU2Fam1tS9h2klKK8d74caeV6A1mIUvBU7bhyQ0wMGlwMtn3EYw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/middleware-sdk-s3": "3.750.0",
|
||||
"@aws-sdk/middleware-sdk-s3": "3.758.0",
|
||||
"@aws-sdk/types": "3.734.0",
|
||||
"@smithy/protocol-http": "^5.0.1",
|
||||
"@smithy/signature-v4": "^5.0.1",
|
||||
@@ -1051,12 +1051,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/token-providers": {
|
||||
"version": "3.750.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.750.0.tgz",
|
||||
"integrity": "sha512-X/KzqZw41iWolwNdc8e3RMcNSMR364viHv78u6AefXOO5eRM40c4/LuST1jDzq35/LpnqRhL7/MuixOetw+sFw==",
|
||||
"version": "3.758.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.758.0.tgz",
|
||||
"integrity": "sha512-ckptN1tNrIfQUaGWm/ayW1ddG+imbKN7HHhjFdS4VfItsP0QQOB0+Ov+tpgb4MoNR4JaUghMIVStjIeHN2ks1w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/nested-clients": "3.750.0",
|
||||
"@aws-sdk/nested-clients": "3.758.0",
|
||||
"@aws-sdk/types": "3.734.0",
|
||||
"@smithy/property-provider": "^4.0.1",
|
||||
"@smithy/shared-ini-file-loader": "^4.0.1",
|
||||
@@ -1132,12 +1132,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/util-user-agent-node": {
|
||||
"version": "3.750.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.750.0.tgz",
|
||||
"integrity": "sha512-84HJj9G9zbrHX2opLk9eHfDceB+UIHVrmflMzWHpsmo9fDuro/flIBqaVDlE021Osj6qIM0SJJcnL6s23j7JEw==",
|
||||
"version": "3.758.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.758.0.tgz",
|
||||
"integrity": "sha512-A5EZw85V6WhoKMV2hbuFRvb9NPlxEErb4HPO6/SPXYY4QrjprIzScHxikqcWv1w4J3apB1wto9LPU3IMsYtfrw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/middleware-user-agent": "3.750.0",
|
||||
"@aws-sdk/middleware-user-agent": "3.758.0",
|
||||
"@aws-sdk/types": "3.734.0",
|
||||
"@smithy/node-config-provider": "^4.0.1",
|
||||
"@smithy/types": "^4.1.0",
|
||||
@@ -2438,9 +2438,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/core": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.1.4.tgz",
|
||||
"integrity": "sha512-wFExFGK+7r2wYriOqe7RRIBNpvxwiS95ih09+GSLRBdoyK/O1uZA7K7pKesj5CBvwJuSBeXwLyR88WwIAY+DGA==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.1.5.tgz",
|
||||
"integrity": "sha512-HLclGWPkCsekQgsyzxLhCQLa8THWXtB5PxyYN+2O6nkyLt550KQKTlbV2D1/j5dNIQapAZM1+qFnpBFxZQkgCA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/middleware-serde": "^4.0.2",
|
||||
@@ -2448,7 +2448,7 @@
|
||||
"@smithy/types": "^4.1.0",
|
||||
"@smithy/util-body-length-browser": "^4.0.0",
|
||||
"@smithy/util-middleware": "^4.0.1",
|
||||
"@smithy/util-stream": "^4.1.1",
|
||||
"@smithy/util-stream": "^4.1.2",
|
||||
"@smithy/util-utf8": "^4.0.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
@@ -2656,12 +2656,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/middleware-endpoint": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.0.5.tgz",
|
||||
"integrity": "sha512-cPzGZV7qStHwboFrm6GfrzQE+YDiCzWcTh4+7wKrP/ZQ4gkw+r7qDjV8GjM4N0UYsuUyLfpzLGg5hxsYTU11WA==",
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.0.6.tgz",
|
||||
"integrity": "sha512-ftpmkTHIFqgaFugcjzLZv3kzPEFsBFSnq1JsIkr2mwFzCraZVhQk2gqN51OOeRxqhbPTkRFj39Qd2V91E/mQxg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.1.4",
|
||||
"@smithy/core": "^3.1.5",
|
||||
"@smithy/middleware-serde": "^4.0.2",
|
||||
"@smithy/node-config-provider": "^4.0.1",
|
||||
"@smithy/shared-ini-file-loader": "^4.0.1",
|
||||
@@ -2675,15 +2675,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/middleware-retry": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.0.6.tgz",
|
||||
"integrity": "sha512-s8QzuOQnbdvRymD9Gt9c9zMq10wUQAHQ3z72uirrBHCwZcLTrL5iCOuVTMdka2IXOYhQE890WD5t6G24+F+Qcg==",
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.0.7.tgz",
|
||||
"integrity": "sha512-58j9XbUPLkqAcV1kHzVX/kAR16GT+j7DUZJqwzsxh1jtz7G82caZiGyyFgUvogVfNTg3TeAOIJepGc8TXF4AVQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/node-config-provider": "^4.0.1",
|
||||
"@smithy/protocol-http": "^5.0.1",
|
||||
"@smithy/service-error-classification": "^4.0.1",
|
||||
"@smithy/smithy-client": "^4.1.5",
|
||||
"@smithy/smithy-client": "^4.1.6",
|
||||
"@smithy/types": "^4.1.0",
|
||||
"@smithy/util-middleware": "^4.0.1",
|
||||
"@smithy/util-retry": "^4.0.1",
|
||||
@@ -2749,9 +2749,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/node-http-handler": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.2.tgz",
|
||||
"integrity": "sha512-X66H9aah9hisLLSnGuzRYba6vckuFtGE+a5DcHLliI/YlqKrGoxhisD5XbX44KyoeRzoNlGr94eTsMVHFAzPOw==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.3.tgz",
|
||||
"integrity": "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/abort-controller": "^4.0.1",
|
||||
@@ -2862,17 +2862,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/smithy-client": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.1.5.tgz",
|
||||
"integrity": "sha512-DMXYoYeL4QkElr216n1yodTFeATbfb4jwYM9gKn71Rw/FNA1/Sm36tkTSCsZEs7mgpG3OINmkxL9vgVFzyGPaw==",
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.1.6.tgz",
|
||||
"integrity": "sha512-UYDolNg6h2O0L+cJjtgSyKKvEKCOa/8FHYJnBobyeoeWDmNpXjwOAtw16ezyeu1ETuuLEOZbrynK0ZY1Lx9Jbw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.1.4",
|
||||
"@smithy/middleware-endpoint": "^4.0.5",
|
||||
"@smithy/core": "^3.1.5",
|
||||
"@smithy/middleware-endpoint": "^4.0.6",
|
||||
"@smithy/middleware-stack": "^4.0.1",
|
||||
"@smithy/protocol-http": "^5.0.1",
|
||||
"@smithy/types": "^4.1.0",
|
||||
"@smithy/util-stream": "^4.1.1",
|
||||
"@smithy/util-stream": "^4.1.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2969,13 +2969,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/util-defaults-mode-browser": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.6.tgz",
|
||||
"integrity": "sha512-N8+VCt+piupH1A7DgSVDNrVHqRLz8r6DvBkpS7EWHiIxsUk4jqGuQLjqC/gnCzmwGkVBdNruHoYAzzaSQ8e80w==",
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.7.tgz",
|
||||
"integrity": "sha512-CZgDDrYHLv0RUElOsmZtAnp1pIjwDVCSuZWOPhIOBvG36RDfX1Q9+6lS61xBf+qqvHoqRjHxgINeQz47cYFC2Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/property-provider": "^4.0.1",
|
||||
"@smithy/smithy-client": "^4.1.5",
|
||||
"@smithy/smithy-client": "^4.1.6",
|
||||
"@smithy/types": "^4.1.0",
|
||||
"bowser": "^2.11.0",
|
||||
"tslib": "^2.6.2"
|
||||
@@ -2985,16 +2985,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/util-defaults-mode-node": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.6.tgz",
|
||||
"integrity": "sha512-9zhx1shd1VwSSVvLZB8CM3qQ3RPD3le7A3h/UPuyh/PC7g4OaWDi2xUNzamsVoSmCGtmUBONl56lM2EU6LcH7A==",
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.7.tgz",
|
||||
"integrity": "sha512-79fQW3hnfCdrfIi1soPbK3zmooRFnLpSx3Vxi6nUlqaaQeC5dm8plt4OTNDNqEEEDkvKghZSaoti684dQFVrGQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/config-resolver": "^4.0.1",
|
||||
"@smithy/credential-provider-imds": "^4.0.1",
|
||||
"@smithy/node-config-provider": "^4.0.1",
|
||||
"@smithy/property-provider": "^4.0.1",
|
||||
"@smithy/smithy-client": "^4.1.5",
|
||||
"@smithy/smithy-client": "^4.1.6",
|
||||
"@smithy/types": "^4.1.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
@@ -3056,13 +3056,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/util-stream": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.1.1.tgz",
|
||||
"integrity": "sha512-+Xvh8nhy0Wjv1y71rBVyV3eJU3356XsFQNI8dEZVNrQju7Eib8G31GWtO+zMa9kTCGd41Mflu+ZKfmQL/o2XzQ==",
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.1.2.tgz",
|
||||
"integrity": "sha512-44PKEqQ303d3rlQuiDpcCcu//hV8sn+u2JBo84dWCE0rvgeiVl0IlLMagbU++o0jCWhYCsHaAt9wZuZqNe05Hw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/fetch-http-handler": "^5.0.1",
|
||||
"@smithy/node-http-handler": "^4.0.2",
|
||||
"@smithy/node-http-handler": "^4.0.3",
|
||||
"@smithy/types": "^4.1.0",
|
||||
"@smithy/util-base64": "^4.0.0",
|
||||
"@smithy/util-buffer-from": "^4.0.0",
|
||||
@@ -4861,9 +4861,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dd-trace": {
|
||||
"version": "5.39.0",
|
||||
"resolved": "https://registry.npmjs.org/dd-trace/-/dd-trace-5.39.0.tgz",
|
||||
"integrity": "sha512-vNC25L2ScHxGl9DdmmbwgrApd2gDlp4vA84N/hLq0MFWpBkia7BbhfA2GGtJ8+4vUChFdoefLMZhMKd7WAM7AA==",
|
||||
"version": "5.40.0",
|
||||
"resolved": "https://registry.npmjs.org/dd-trace/-/dd-trace-5.40.0.tgz",
|
||||
"integrity": "sha512-/UYVCcgpZ9LnnUvIJcNfd1Hj51i8HhqLOn9PCj5gK3wJUn6MY/ie/5da2ZaFtoK2DKQ9OZmFBITLV3+KDl4pjA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "(Apache-2.0 OR BSD-3-Clause)",
|
||||
"dependencies": {
|
||||
@@ -7738,13 +7738,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/juice": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/juice/-/juice-11.0.0.tgz",
|
||||
"integrity": "sha512-sGF8hPz9/Wg+YXbaNDqc1Iuoaw+J/P9lBHNQKXAGc9pPNjCd4fyPai0Zxj7MRtdjMr0lcgk5PjEIkP2b8R9F3w==",
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/juice/-/juice-11.0.1.tgz",
|
||||
"integrity": "sha512-R3KLud4l/sN9AMmFZs0QY7cugGSiKvPhGyIsufCV5nJ0MjSlngUE7k80TmFeK9I62wOXrjWBtYA1knVs2OkF8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cheerio": "^1.0.0",
|
||||
"commander": "^12.1.0",
|
||||
"entities": "^4.5.0",
|
||||
"mensch": "^0.3.4",
|
||||
"slick": "^1.12.2",
|
||||
"web-resource-inliner": "^7.0.0"
|
||||
@@ -8975,9 +8976,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz",
|
||||
"integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==",
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -9919,9 +9920,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/soap": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/soap/-/soap-1.1.8.tgz",
|
||||
"integrity": "sha512-fDNGyGsPkQP3bZX/366Ud5Kpjo9mCMh7ZKYIc3uipBEPPM2ZqCNkv1Z2/w0qpzpYFLL7do8WWwVUAjAwuUe1AQ==",
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/soap/-/soap-1.1.9.tgz",
|
||||
"integrity": "sha512-x6wMhwIwGFnMQiV0tLIygERELwpV/EkidUvzjcCPRx0D16YngNL8z7j5+nFad0Fl5irisXbfY2FKzvF9SEjMog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.7.9",
|
||||
|
||||
20
package.json
20
package.json
@@ -19,12 +19,12 @@
|
||||
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-cloudwatch-logs": "^3.750.0",
|
||||
"@aws-sdk/client-elasticache": "^3.755.0",
|
||||
"@aws-sdk/client-s3": "^3.750.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.750.0",
|
||||
"@aws-sdk/client-ses": "^3.750.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.750.0",
|
||||
"@aws-sdk/client-cloudwatch-logs": "^3.758.0",
|
||||
"@aws-sdk/client-elasticache": "^3.758.0",
|
||||
"@aws-sdk/client-s3": "^3.758.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.758.0",
|
||||
"@aws-sdk/client-ses": "^3.758.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.758.0",
|
||||
"@opensearch-project/opensearch": "^2.13.0",
|
||||
"@socket.io/admin-ui": "^0.5.1",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
@@ -42,7 +42,7 @@
|
||||
"cors": "2.8.5",
|
||||
"crisp-status-reporter": "^1.2.2",
|
||||
"csrf": "^3.1.0",
|
||||
"dd-trace": "^5.39.0",
|
||||
"dd-trace": "^5.40.0",
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.1",
|
||||
@@ -53,7 +53,7 @@
|
||||
"intuit-oauth": "^4.2.0",
|
||||
"ioredis": "^5.5.0",
|
||||
"json-2-csv": "^5.5.8",
|
||||
"juice": "^11.0.0",
|
||||
"juice": "^11.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.47",
|
||||
@@ -66,7 +66,7 @@
|
||||
"redis": "^4.7.0",
|
||||
"rimraf": "^6.0.1",
|
||||
"skia-canvas": "^2.0.2",
|
||||
"soap": "^1.1.8",
|
||||
"soap": "^1.1.9",
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-adapter": "^2.5.5",
|
||||
"ssh2-sftp-client": "^11.0.0",
|
||||
@@ -85,7 +85,7 @@
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"globals": "^15.15.0",
|
||||
"p-limit": "^3.1.0",
|
||||
"prettier": "^3.5.2",
|
||||
"prettier": "^3.5.3",
|
||||
"source-map-explorer": "^2.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
131
server.js
131
server.js
@@ -22,20 +22,24 @@ const cookieParser = require("cookie-parser");
|
||||
const { Server } = require("socket.io");
|
||||
const { createAdapter } = require("@socket.io/redis-adapter");
|
||||
const { instrument } = require("@socket.io/admin-ui");
|
||||
const { isString, isEmpty } = require("lodash");
|
||||
const { isString, isEmpty, isFunction } = require("lodash");
|
||||
|
||||
const logger = require("./server/utils/logger");
|
||||
const { applyRedisHelpers } = require("./server/utils/redisHelpers");
|
||||
const { applyIOHelpers } = require("./server/utils/ioHelpers");
|
||||
const { redisSocketEvents } = require("./server/web-sockets/redisSocketEvents");
|
||||
const { ElastiCacheClient, DescribeCacheClustersCommand } = require("@aws-sdk/client-elasticache");
|
||||
const {
|
||||
ElastiCacheClient,
|
||||
DescribeCacheClustersCommand,
|
||||
DescribeReplicationGroupsCommand
|
||||
} = require("@aws-sdk/client-elasticache");
|
||||
const { InstanceRegion } = require("./server/utils/instanceMgr");
|
||||
const StartStatusReporter = require("./server/utils/statusReporter");
|
||||
const { registerCleanupTask, initializeCleanupManager } = require("./server/utils/cleanupManager");
|
||||
|
||||
const { loadEmailQueue } = require("./server/notifications/queues/emailQueue");
|
||||
const { loadAppQueue } = require("./server/notifications/queues/appQueue");
|
||||
|
||||
const cleanupTasks = [];
|
||||
let isShuttingDown = false;
|
||||
const CLUSTER_RETRY_BASE_DELAY = 100;
|
||||
const CLUSTER_RETRY_MAX_DELAY = 5000;
|
||||
const CLUSTER_RETRY_JITTER = 100;
|
||||
@@ -126,26 +130,48 @@ const applyRoutes = ({ app }) => {
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
const getRedisNodesFromAWS = async () => {
|
||||
const client = new ElastiCacheClient({
|
||||
region: InstanceRegion()
|
||||
});
|
||||
|
||||
const params = {
|
||||
ReplicationGroupId: process.env.REDIS_CLUSTER_ID,
|
||||
ShowCacheNodeInfo: true
|
||||
};
|
||||
const client = new ElastiCacheClient({ region: InstanceRegion() });
|
||||
|
||||
try {
|
||||
// Fetch the cache clusters associated with the replication group
|
||||
const command = new DescribeCacheClustersCommand(params);
|
||||
const response = await client.send(command);
|
||||
const cacheClusters = response.CacheClusters;
|
||||
const describeReplicationGroupCommand = new DescribeReplicationGroupsCommand({
|
||||
ReplicationGroupId: process.env.REDIS_CLUSTER_ID
|
||||
});
|
||||
const describeReplicationGroupResponse = await client.send(describeReplicationGroupCommand);
|
||||
|
||||
return cacheClusters.flatMap((cluster) =>
|
||||
cluster.CacheNodes.map((node) => `${node.Endpoint.Address}:${node.Endpoint.Port}`)
|
||||
);
|
||||
//TODO: add checking to make sure there's only 1.
|
||||
const cacheClusterIds = describeReplicationGroupResponse.ReplicationGroups[0].MemberClusters;
|
||||
|
||||
// Ensure cacheClusters exists and is an array
|
||||
if (!cacheClusterIds || !Array.isArray(cacheClusterIds) || cacheClusterIds.length === 0) {
|
||||
logger.log(`No cache clusters found for cluster id ${process.env.REDIS_CLUSTER_ID}`, "ERROR", "redis", "api");
|
||||
return [];
|
||||
}
|
||||
|
||||
const nodeEndpointAddresses = [];
|
||||
|
||||
for (const cluster of cacheClusterIds) {
|
||||
const params = { CacheClusterId: cluster, ShowCacheNodeInfo: true };
|
||||
const command = new DescribeCacheClustersCommand(params);
|
||||
const response = await client.send(command);
|
||||
|
||||
if (response.CacheClusters && Array.isArray(response.CacheClusters)) {
|
||||
// Map nodes to address strings
|
||||
//TODO: What happens if we have more shards?
|
||||
const nodeAddress = `${response.CacheClusters[0].CacheNodes[0].Endpoint.Address}:${response.CacheClusters[0].CacheNodes[0].Endpoint.Port}`;
|
||||
// Debug log node addresses
|
||||
logger.log(`Cluster node addresses: ${nodeAddress}`, "DEBUG", "redis", "api");
|
||||
// Return only those addresses that start with the current cluster id
|
||||
nodeEndpointAddresses.push(nodeAddress);
|
||||
}
|
||||
}
|
||||
|
||||
return nodeEndpointAddresses;
|
||||
// Process each cluster
|
||||
} catch (err) {
|
||||
logger.log(`Error fetching Redis nodes from AWS: ${err.message}`, "ERROR", "redis", "api");
|
||||
logger.log(`Error fetching Redis nodes from AWS:`, "ERROR", "redis", "api", {
|
||||
message: err?.message,
|
||||
stack: err?.stack
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
@@ -169,7 +195,10 @@ const connectToRedisCluster = async () => {
|
||||
try {
|
||||
redisServers = JSON.parse(process.env.REDIS_URL);
|
||||
} catch (error) {
|
||||
logger.log(`Failed to parse REDIS_URL: ${error.message}. Exiting...`, "ERROR", "redis", "api");
|
||||
logger.log(`Failed to parse REDIS_URL: ${error.message}. Exiting...`, "ERROR", "redis", "api", {
|
||||
message: error?.message,
|
||||
stack: error?.stack
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -207,7 +236,10 @@ const connectToRedisCluster = async () => {
|
||||
});
|
||||
|
||||
redisCluster.on("error", (err) => {
|
||||
logger.log(`Redis cluster connection failed: ${err.message}`, "ERROR", "redis", "api");
|
||||
logger.log(`Redis cluster connection failed:`, "ERROR", "redis", "api", {
|
||||
message: err?.message,
|
||||
stack: err?.stack
|
||||
});
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
@@ -229,8 +261,18 @@ const applySocketIO = async ({ server, app }) => {
|
||||
const pubClient = redisCluster;
|
||||
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"));
|
||||
pubClient.on("error", (err) =>
|
||||
logger.log(`Redis pubClient error: ${err}`, "ERROR", "redis", "api", {
|
||||
message: err?.message,
|
||||
stack: err?.stack
|
||||
})
|
||||
);
|
||||
subClient.on("error", (err) =>
|
||||
logger.log(`Redis subClient error: ${err}`, "ERROR", "redis", "api", {
|
||||
message: err?.message,
|
||||
stack: err?.stack
|
||||
})
|
||||
);
|
||||
|
||||
// Register Redis cleanup
|
||||
registerCleanupTask(async () => {
|
||||
@@ -332,6 +374,9 @@ const main = async () => {
|
||||
|
||||
const server = http.createServer(app);
|
||||
|
||||
// Initialize cleanup manager with signal handlers
|
||||
initializeCleanupManager();
|
||||
|
||||
const { pubClient, ioRedis } = await applySocketIO({ server, app });
|
||||
const redisHelpers = applyRedisHelpers({ pubClient, app, logger });
|
||||
const ioHelpers = applyIOHelpers({ app, redisHelpers, ioRedis, logger });
|
||||
@@ -348,13 +393,11 @@ const main = async () => {
|
||||
|
||||
const StatusReporter = StartStatusReporter();
|
||||
registerCleanupTask(async () => {
|
||||
StatusReporter.end();
|
||||
if (isFunction(StatusReporter?.end)) {
|
||||
StatusReporter.end();
|
||||
}
|
||||
});
|
||||
|
||||
// Add SIGTERM signal handler
|
||||
process.on("SIGTERM", handleSigterm);
|
||||
process.on("SIGINT", handleSigterm); // Optional: Handle Ctrl+C
|
||||
|
||||
try {
|
||||
await server.listen(port);
|
||||
logger.log(`Server started on port ${port}`, "INFO", "api");
|
||||
@@ -373,33 +416,3 @@ main().catch((error) => {
|
||||
// Note: If we want the app to crash on all uncaught async operations, we would
|
||||
// need to put a `process.exit(1);` here
|
||||
});
|
||||
|
||||
// Register a cleanup task
|
||||
function registerCleanupTask(task) {
|
||||
cleanupTasks.push(task);
|
||||
}
|
||||
|
||||
// SIGTERM handler
|
||||
async function handleSigterm() {
|
||||
if (isShuttingDown) {
|
||||
logger.log("sigterm-api", "WARN", null, null, { message: "Shutdown already in progress, ignoring signal." });
|
||||
return;
|
||||
}
|
||||
|
||||
isShuttingDown = true;
|
||||
|
||||
logger.log("sigterm-api", "WARN", null, null, { message: "SIGTERM Received. Starting graceful shutdown." });
|
||||
|
||||
try {
|
||||
for (const task of cleanupTasks) {
|
||||
logger.log("sigterm-api", "WARN", null, null, { message: `Running cleanup task: ${task.name}` });
|
||||
|
||||
await task();
|
||||
}
|
||||
logger.log("sigterm-api", "WARN", null, null, { message: `All cleanup tasks completed.` });
|
||||
} catch (error) {
|
||||
logger.log("sigterm-api-error", "ERROR", null, null, { message: error.message, stack: error.stack });
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,11 @@ const defaultFooter = () => {
|
||||
|
||||
const now = () => moment().format("MM/DD/YYYY @ hh:mm a");
|
||||
|
||||
/**
|
||||
* Generate the email template
|
||||
* @param strings
|
||||
* @returns {string}
|
||||
*/
|
||||
const generateEmailTemplate = (strings) => {
|
||||
return (
|
||||
`
|
||||
|
||||
@@ -69,11 +69,14 @@ const sendServerEmail = async ({ subject, text }) => {
|
||||
}
|
||||
},
|
||||
(err, info) => {
|
||||
logger.log("server-email-failure", err ? "error" : "debug", null, null, { message: err?.message });
|
||||
logger.log("server-email-failure", err ? "error" : "debug", null, null, {
|
||||
message: err?.message,
|
||||
stack: err?.stack
|
||||
});
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
logger.log("server-email-failure", "error", null, null, { message: error?.message });
|
||||
logger.log("server-email-failure", "error", null, null, { message: error?.message, stack: error?.stack });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -92,11 +95,11 @@ const sendTaskEmail = async ({ to, subject, type = "text", html, text, attachmen
|
||||
},
|
||||
(err, info) => {
|
||||
// (message, type, user, record, meta
|
||||
logger.log("server-email", err ? "error" : "debug", null, null, { message: err?.message });
|
||||
logger.log("server-email", err ? "error" : "debug", null, null, { message: err?.message, stack: err?.stack });
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
logger.log("server-email-failure", "error", null, null, { message: error?.message });
|
||||
logger.log("server-email-failure", "error", null, null, { message: error?.message, stack: error?.stack });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -125,7 +128,8 @@ const sendEmail = async (req, res) => {
|
||||
cc: req.body.cc,
|
||||
subject: req.body.subject,
|
||||
templateStrings: req.body.templateStrings,
|
||||
errorMessage: error?.message
|
||||
errorMessage: error?.message,
|
||||
errorStack: error?.stack
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -194,7 +198,8 @@ const sendEmail = async (req, res) => {
|
||||
cc: req.body.cc,
|
||||
subject: req.body.subject,
|
||||
templateStrings: req.body.templateStrings,
|
||||
errorMessage: err?.message
|
||||
errorMessage: err?.message,
|
||||
errorStack: err?.stack
|
||||
});
|
||||
logEmail(req, {
|
||||
to: req.body.to,
|
||||
@@ -202,7 +207,7 @@ const sendEmail = async (req, res) => {
|
||||
subject: req.body.subject,
|
||||
bodyshopid: req.body.bodyshopid
|
||||
});
|
||||
res.status(500).json({ success: false, errorMessage: err?.message });
|
||||
res.status(500).json({ success: false, errorMessage: err?.message, stack: err?.stack });
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -270,14 +275,16 @@ ${body.bounce?.bouncedRecipients.map(
|
||||
},
|
||||
(err, info) => {
|
||||
logger.log("sns-error", err ? "error" : "debug", "api", null, {
|
||||
errorMessage: err?.message
|
||||
errorMessage: err?.message,
|
||||
errorStack: err?.stack
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log("sns-error", "ERROR", "api", null, {
|
||||
errorMessage: error?.message
|
||||
errorMessage: error?.message,
|
||||
errorStack: error?.stack
|
||||
});
|
||||
}
|
||||
res.sendStatus(200);
|
||||
|
||||
@@ -11,6 +11,7 @@ const moment = require("moment-timezone");
|
||||
const { taskEmailQueue } = require("./tasksEmailsQueue");
|
||||
const mailer = require("./mailer");
|
||||
const { InstanceEndpoints } = require("../utils/instanceMgr");
|
||||
const { formatTaskPriority } = require("../notifications/stringHelpers");
|
||||
|
||||
// Initialize the Tasks Email Queue
|
||||
const tasksEmailQueue = taskEmailQueue();
|
||||
@@ -62,16 +63,6 @@ const formatDate = (date) => {
|
||||
return date ? `| Due on: ${moment(date).format("MM/DD/YYYY")}` : "";
|
||||
};
|
||||
|
||||
const formatPriority = (priority) => {
|
||||
if (priority === 1) {
|
||||
return "High";
|
||||
} else if (priority === 3) {
|
||||
return "Low";
|
||||
} else {
|
||||
return "Medium";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate the email template arguments.
|
||||
* @param title
|
||||
@@ -88,7 +79,7 @@ const generateTemplateArgs = (title, priority, description, dueDate, bodyshop, j
|
||||
const endPoints = InstanceEndpoints();
|
||||
return {
|
||||
header: title,
|
||||
subHeader: `Body Shop: ${bodyshop.shopname} | Priority: ${formatPriority(priority)} ${formatDate(dueDate)} | Created By: ${createdBy || "N/A"}`,
|
||||
subHeader: `Body Shop: ${bodyshop.shopname} | Priority: ${formatTaskPriority(priority)} ${formatDate(dueDate)} | Created By: ${createdBy || "N/A"}`,
|
||||
body: `Reference: ${job.ro_number || "N/A"} | ${job.ownr_co_nm ? job.ownr_co_nm : `${job.ownr_fn || ""} ${job.ownr_ln || ""}`.trim()} | ${`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""}`.trim()}<br>${description ? description.concat("<br>") : ""}<a href="${endPoints}/manage/tasks/alltasks?taskid=${taskId}">View this task.</a>`,
|
||||
dateLine
|
||||
};
|
||||
@@ -155,7 +146,7 @@ const taskAssignedEmail = async (req, res) => {
|
||||
sendMail(
|
||||
"assigned",
|
||||
tasks_by_pk.assigned_to_employee.user_email,
|
||||
`A ${formatPriority(newTask.priority)} priority task has been ${dirty ? "reassigned to" : "created for"} you - ${newTask.title}`,
|
||||
`A ${formatTaskPriority(newTask.priority)} priority task has been ${dirty ? "reassigned to" : "created for"} you - ${newTask.title}`,
|
||||
generateEmailTemplate(
|
||||
generateTemplateArgs(
|
||||
newTask.title,
|
||||
@@ -239,7 +230,7 @@ const tasksRemindEmail = async (req, res) => {
|
||||
const onlyTask = groupedTasks[recipient.email][0];
|
||||
|
||||
emailData.subject =
|
||||
`New ${formatPriority(onlyTask.priority)} Priority Task Reminder - ${onlyTask.title} ${onlyTask.due_date ? `- ${formatDate(onlyTask.due_date)}` : ""}`.trim();
|
||||
`New ${formatTaskPriority(onlyTask.priority)} Priority Task Reminder - ${onlyTask.title} ${onlyTask.due_date ? `- ${formatDate(onlyTask.due_date)}` : ""}`.trim();
|
||||
|
||||
emailData.html = generateEmailTemplate(
|
||||
generateTemplateArgs(
|
||||
@@ -266,7 +257,7 @@ const tasksRemindEmail = async (req, res) => {
|
||||
body: `<ul>
|
||||
${allTasks
|
||||
.map((task) =>
|
||||
`<li><a href="${InstanceEndpoints()}/manage/tasks/alltasks?taskid=${task.id}">${task.title} - Priority: ${formatPriority(task.priority)} ${task.due_date ? `${formatDate(task.due_date)}` : ""} | Bodyshop: ${task.bodyshop.shopname}</a></li>`.trim()
|
||||
`<li><a href="${InstanceEndpoints()}/manage/tasks/alltasks?taskid=${task.id}">${task.title} - Priority: ${formatTaskPriority(task.priority)} ${task.due_date ? `${formatDate(task.due_date)}` : ""} | Bodyshop: ${task.bodyshop.shopname}</a></li>`.trim()
|
||||
)
|
||||
.join("")}
|
||||
</ul>`
|
||||
|
||||
@@ -2708,16 +2708,14 @@ exports.INSERT_AUDIT_TRAIL = `
|
||||
|
||||
exports.GET_JOB_WATCHERS = `
|
||||
query GET_JOB_WATCHERS($jobid: uuid!) {
|
||||
job_watchers_aggregate(where: { jobid: { _eq: $jobid } }) {
|
||||
nodes {
|
||||
user_email
|
||||
user {
|
||||
authid
|
||||
employee {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
}
|
||||
job_watchers(where: { jobid: { _eq: $jobid } }) {
|
||||
user_email
|
||||
user {
|
||||
authid
|
||||
employee {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2728,6 +2726,7 @@ query GET_JOB_WATCHERS($jobid: uuid!) {
|
||||
bodyshop {
|
||||
id
|
||||
shopname
|
||||
timezone
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2759,3 +2758,14 @@ exports.INSERT_NOTIFICATIONS_MUTATION = ` mutation INSERT_NOTIFICATIONS($object
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
// REMEMBER: Update the cache_bodyshop event in hasura to include any added fields
|
||||
exports.GET_BODYSHOP_BY_ID = `
|
||||
query GET_BODYSHOP_BY_ID($id: uuid!) {
|
||||
bodyshops_by_pk(id: $id) {
|
||||
id
|
||||
md_order_statuses
|
||||
shopname
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -22,7 +22,7 @@ async function processNotificationEvent(req, res, parserPath, successMessage) {
|
||||
|
||||
// Call scenarioParser but don't await it; log any error that occurs.
|
||||
scenarioParser(req, parserPath).catch((error) => {
|
||||
logger.log("notifications-error", "error", "notifications", null, { error: error?.message });
|
||||
logger.log("notifications-error", "error", "notifications", null, { message: error?.message, stack: error?.stack });
|
||||
});
|
||||
|
||||
return res.status(200).json({ message: successMessage });
|
||||
@@ -50,13 +50,69 @@ const handleBillsChange = async (req, res) =>
|
||||
|
||||
/**
|
||||
* Handle documents change notifications.
|
||||
* Processes both old and new job IDs if the document was moved between jobs.
|
||||
*
|
||||
* @param {Object} req - Express request object.
|
||||
* @param {Object} res - Express response object.
|
||||
* @returns {Promise<Object>} JSON response with a success message.
|
||||
*/
|
||||
const handleDocumentsChange = async (req, res) =>
|
||||
processNotificationEvent(req, res, "req.body.event.new.jobid", "Documents Change Notifications Event Handled.");
|
||||
const handleDocumentsChange = async (req, res) => {
|
||||
const { logger } = req;
|
||||
const newJobId = req.body?.event?.data?.new?.jobid;
|
||||
const oldJobId = req.body?.event?.data?.old?.jobid;
|
||||
|
||||
// If jobid changed (document moved between jobs), we need to notify both jobs
|
||||
if (oldJobId && newJobId && oldJobId !== newJobId) {
|
||||
// Process notification for new job ID
|
||||
scenarioParser(req, "req.body.event.new.jobid").catch((error) => {
|
||||
logger.log("notifications-error", "error", "notifications", null, {
|
||||
message: error?.message,
|
||||
stack: error?.stack
|
||||
});
|
||||
});
|
||||
|
||||
// Create a modified request for old job ID
|
||||
const oldJobReq = {
|
||||
body: {
|
||||
...req.body,
|
||||
event: {
|
||||
...req.body.event,
|
||||
data: {
|
||||
new: {
|
||||
...req.body.event.data.old,
|
||||
// Add a flag to indicate this document was moved away
|
||||
_documentMoved: true,
|
||||
_movedToJob: newJobId
|
||||
},
|
||||
old: null
|
||||
}
|
||||
}
|
||||
},
|
||||
logger,
|
||||
sessionUtils: req.sessionUtils
|
||||
};
|
||||
|
||||
// Process notification for old job ID using the modified request
|
||||
scenarioParser(oldJobReq, "req.body.event.new.jobid").catch((error) => {
|
||||
logger.log("notifications-error", "error", "notifications", null, {
|
||||
message: error?.message,
|
||||
stack: error?.stack
|
||||
});
|
||||
});
|
||||
|
||||
return res.status(200).json({ message: "Documents Change Notifications Event Handled for both jobs." });
|
||||
}
|
||||
|
||||
// Otherwise just process the new job ID
|
||||
scenarioParser(req, "req.body.event.new.jobid").catch((error) => {
|
||||
logger.log("notifications-error", "error", "notifications", null, {
|
||||
message: error?.message,
|
||||
stack: error?.stack
|
||||
});
|
||||
});
|
||||
|
||||
return res.status(200).json({ message: "Documents Change Notifications Event Handled." });
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle job lines change notifications.
|
||||
@@ -78,24 +134,6 @@ const handleJobLinesChange = async (req, res) =>
|
||||
const handleNotesChange = async (req, res) =>
|
||||
processNotificationEvent(req, res, "req.body.event.new.jobid", "Notes Changed Notification Event Handled.");
|
||||
|
||||
/**
|
||||
* Handle parts dispatch change notifications.
|
||||
*
|
||||
* @param {Object} req - Express request object.
|
||||
* @param {Object} res - Express response object.
|
||||
* @returns {Object} JSON response with a success message.
|
||||
*/
|
||||
const handlePartsDispatchChange = (req, res) => res.status(200).json({ message: "Parts Dispatch change handled." });
|
||||
|
||||
/**
|
||||
* Handle parts order change notifications.
|
||||
*
|
||||
* @param {Object} req - Express request object.
|
||||
* @param {Object} res - Express response object.
|
||||
* @returns {Object} JSON response with a success message.
|
||||
*/
|
||||
const handlePartsOrderChange = (req, res) => res.status(200).json({ message: "Parts Order change handled." });
|
||||
|
||||
/**
|
||||
* Handle payments change notifications.
|
||||
*
|
||||
@@ -126,6 +164,27 @@ const handleTasksChange = async (req, res) =>
|
||||
const handleTimeTicketsChange = async (req, res) =>
|
||||
processNotificationEvent(req, res, "req.body.event.new.jobid", "Time Tickets Changed Notification Event Handled.");
|
||||
|
||||
/**
|
||||
* Handle parts dispatch change notifications.
|
||||
* Note: Placeholder
|
||||
*
|
||||
* @param {Object} req - Express request object.
|
||||
* @param {Object} res - Express response object.
|
||||
* @returns {Object} JSON response with a success message.
|
||||
*
|
||||
*/
|
||||
const handlePartsDispatchChange = (req, res) => res.status(200).json({ message: "Parts Dispatch change handled." });
|
||||
|
||||
/**
|
||||
* Handle parts order change notifications.
|
||||
* Note: Placeholder
|
||||
*
|
||||
* @param {Object} req - Express request object.
|
||||
* @param {Object} res - Express response object.
|
||||
* @returns {Object} JSON response with a success message.
|
||||
*/
|
||||
const handlePartsOrderChange = (req, res) => res.status(200).json({ message: "Parts Order change handled." });
|
||||
|
||||
module.exports = {
|
||||
handleJobsChange,
|
||||
handleBillsChange,
|
||||
|
||||
@@ -3,15 +3,14 @@
|
||||
*
|
||||
* This function analyzes the differences between previous (`oldData`) and current (`newData`)
|
||||
* data states to identify changed fields. It determines if the event is a new entry or an update
|
||||
* and optionally extracts a `jobId` based on a specified field. The result includes details
|
||||
* about changed fields, the event type, and associated metadata.
|
||||
* and returns details about changed fields, the event type, and associated metadata.
|
||||
*
|
||||
* @param {Object} options - Configuration options for parsing the event.
|
||||
* @param {Object} [options.oldData] - The previous state of the data (undefined for new entries).
|
||||
* @param {Object} options.newData - The current state of the data.
|
||||
* @param {string} options.trigger - The type of event trigger (e.g., 'INSERT', 'UPDATE').
|
||||
* @param {string} options.table - The name of the table associated with the event.
|
||||
* @param {string} [options.jobIdField] - The field name used to extract the jobId (optional).
|
||||
* @param {string} [options.jobId] - The job ID, if already extracted by the caller (optional).
|
||||
* @returns {Object} An object containing the parsed event details:
|
||||
* - {Array<string>} changedFieldNames - List of field names that have changed.
|
||||
* - {Object} changedFields - Map of changed fields with their old and new values.
|
||||
@@ -19,9 +18,9 @@
|
||||
* - {Object} data - The current data state (`newData`).
|
||||
* - {string} trigger - The event trigger type.
|
||||
* - {string} table - The table name.
|
||||
* - {string|null} jobId - The extracted jobId or null if not applicable.
|
||||
* - {string|null} jobId - The provided jobId or null if not provided.
|
||||
*/
|
||||
const eventParser = async ({ oldData, newData, trigger, table, jobIdField }) => {
|
||||
const eventParser = async ({ oldData, newData, trigger, table, jobId = null }) => {
|
||||
const isNew = !oldData; // True if no old data exists, indicating a new entry
|
||||
let changedFields = {};
|
||||
let changedFieldNames = [];
|
||||
@@ -61,19 +60,6 @@ const eventParser = async ({ oldData, newData, trigger, table, jobIdField }) =>
|
||||
}
|
||||
}
|
||||
|
||||
// Extract jobId if jobIdField is provided
|
||||
let jobId = null;
|
||||
if (jobIdField) {
|
||||
let keyName = jobIdField;
|
||||
const prefix = "req.body.event.new.";
|
||||
// Strip prefix if present to isolate the actual field name
|
||||
if (keyName.startsWith(prefix)) {
|
||||
keyName = keyName.slice(prefix.length);
|
||||
}
|
||||
// Look for jobId in newData first, then fallback to oldData if necessary
|
||||
jobId = newData[keyName] || (oldData && oldData[keyName]) || null;
|
||||
}
|
||||
|
||||
return {
|
||||
changedFieldNames, // Array of fields that changed
|
||||
changedFields, // Object with old/new values for changed fields
|
||||
@@ -81,7 +67,7 @@ const eventParser = async ({ oldData, newData, trigger, table, jobIdField }) =>
|
||||
data: newData, // Current data state
|
||||
trigger, // Event trigger (e.g., 'INSERT', 'UPDATE')
|
||||
table, // Associated table name
|
||||
jobId // Extracted jobId or null
|
||||
jobId // Provided jobId or null
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
const { Queue, Worker } = require("bullmq");
|
||||
const { INSERT_NOTIFICATIONS_MUTATION } = require("../../graphql-client/queries");
|
||||
const { registerCleanupTask } = require("../../utils/cleanupManager");
|
||||
const getBullMQPrefix = require("../../utils/getBullMQPrefix");
|
||||
const devDebugLogger = require("../../utils/devDebugLogger");
|
||||
const graphQLClient = require("../../graphql-client/graphql-client").client;
|
||||
|
||||
// Base time-related constant in minutes, sourced from environment variable or defaulting to 1
|
||||
const APP_CONSOLIDATION_DELAY_IN_MINS = (() => {
|
||||
const envValue = process.env?.APP_CONSOLIDATION_DELAY_IN_MINS;
|
||||
const parsedValue = envValue ? parseInt(envValue, 10) : NaN;
|
||||
return isNaN(parsedValue) ? 1 : Math.max(1, parsedValue); // Default to 1, ensure at least 1
|
||||
return isNaN(parsedValue) ? 3 : Math.max(1, parsedValue); // Default to 3, ensure at least 1
|
||||
})();
|
||||
|
||||
// Base time-related constant (in milliseconds) / DO NOT TOUCH
|
||||
@@ -44,17 +47,20 @@ const buildNotificationContent = (notifications) => {
|
||||
*/
|
||||
const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
||||
if (!addQueue || !consolidateQueue) {
|
||||
logger.logger.info("Initializing Notifications Queues");
|
||||
const prefix = getBullMQPrefix();
|
||||
const devKey = process.env?.NODE_ENV === "production" ? "prod" : "dev";
|
||||
|
||||
devDebugLogger(`Initializing Notifications Queues with prefix: ${prefix}`);
|
||||
|
||||
addQueue = new Queue("notificationsAdd", {
|
||||
prefix,
|
||||
connection: pubClient,
|
||||
prefix: "{BULLMQ}",
|
||||
defaultJobOptions: { removeOnComplete: true, removeOnFail: true }
|
||||
});
|
||||
|
||||
consolidateQueue = new Queue("notificationsConsolidate", {
|
||||
prefix,
|
||||
connection: pubClient,
|
||||
prefix: "{BULLMQ}",
|
||||
defaultJobOptions: { removeOnComplete: true, removeOnFail: true }
|
||||
});
|
||||
|
||||
@@ -62,9 +68,9 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
||||
"notificationsAdd",
|
||||
async (job) => {
|
||||
const { jobId, key, variables, recipients, body, jobRoNumber } = job.data;
|
||||
logger.logger.info(`Adding notifications for jobId ${jobId}`);
|
||||
devDebugLogger(`Adding notifications for jobId ${jobId}`);
|
||||
|
||||
const redisKeyPrefix = `app:notifications:${jobId}`;
|
||||
const redisKeyPrefix = `app:${devKey}:notifications:${jobId}`;
|
||||
const notification = { key, variables, body, jobRoNumber, timestamp: Date.now() };
|
||||
|
||||
for (const recipient of recipients) {
|
||||
@@ -74,12 +80,12 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
||||
const notifications = existingNotifications ? JSON.parse(existingNotifications) : [];
|
||||
notifications.push(notification);
|
||||
await pubClient.set(userKey, JSON.stringify(notifications), "EX", NOTIFICATION_STORAGE_EXPIRATION / 1000);
|
||||
logger.logger.debug(`Stored notification for ${user} under ${userKey}: ${JSON.stringify(notifications)}`);
|
||||
devDebugLogger(`Stored notification for ${user} under ${userKey}: ${JSON.stringify(notifications)}`);
|
||||
}
|
||||
|
||||
const consolidateKey = `app:consolidate:${jobId}`;
|
||||
const consolidateKey = `app:${devKey}:consolidate:${jobId}`;
|
||||
const flagSet = await pubClient.setnx(consolidateKey, "pending");
|
||||
logger.logger.debug(`Consolidation flag set for jobId ${jobId}: ${flagSet}`);
|
||||
devDebugLogger(`Consolidation flag set for jobId ${jobId}: ${flagSet}`);
|
||||
|
||||
if (flagSet) {
|
||||
await consolidateQueue.add(
|
||||
@@ -92,15 +98,15 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
||||
backoff: LOCK_EXPIRATION
|
||||
}
|
||||
);
|
||||
logger.logger.info(`Scheduled consolidation for jobId ${jobId}`);
|
||||
devDebugLogger(`Scheduled consolidation for jobId ${jobId}`);
|
||||
await pubClient.expire(consolidateKey, CONSOLIDATION_FLAG_EXPIRATION / 1000);
|
||||
} else {
|
||||
logger.logger.debug(`Consolidation already scheduled for jobId ${jobId}`);
|
||||
devDebugLogger(`Consolidation already scheduled for jobId ${jobId}`);
|
||||
}
|
||||
},
|
||||
{
|
||||
prefix,
|
||||
connection: pubClient,
|
||||
prefix: "{BULLMQ}",
|
||||
concurrency: 5
|
||||
}
|
||||
);
|
||||
@@ -109,23 +115,24 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
||||
"notificationsConsolidate",
|
||||
async (job) => {
|
||||
const { jobId, recipients } = job.data;
|
||||
logger.logger.info(`Consolidating notifications for jobId ${jobId}`);
|
||||
devDebugLogger(`Consolidating notifications for jobId ${jobId}`);
|
||||
|
||||
const redisKeyPrefix = `app:${devKey}:notifications:${jobId}`;
|
||||
const lockKey = `lock:${devKey}:consolidate:${jobId}`;
|
||||
|
||||
const redisKeyPrefix = `app:notifications:${jobId}`;
|
||||
const lockKey = `lock:consolidate:${jobId}`;
|
||||
const lockAcquired = await pubClient.set(lockKey, "locked", "NX", "EX", LOCK_EXPIRATION / 1000);
|
||||
logger.logger.debug(`Lock acquisition for jobId ${jobId}: ${lockAcquired}`);
|
||||
devDebugLogger(`Lock acquisition for jobId ${jobId}: ${lockAcquired}`);
|
||||
|
||||
if (lockAcquired) {
|
||||
try {
|
||||
const allNotifications = {};
|
||||
const uniqueUsers = [...new Set(recipients.map((r) => r.user))];
|
||||
logger.logger.debug(`Unique users for jobId ${jobId}: ${uniqueUsers}`);
|
||||
devDebugLogger(`Unique users for jobId ${jobId}: ${uniqueUsers}`);
|
||||
|
||||
for (const user of uniqueUsers) {
|
||||
const userKey = `${redisKeyPrefix}:${user}`;
|
||||
const notifications = await pubClient.get(userKey);
|
||||
logger.logger.debug(`Retrieved notifications for ${user}: ${notifications}`);
|
||||
devDebugLogger(`Retrieved notifications for ${user}: ${notifications}`);
|
||||
|
||||
if (notifications) {
|
||||
const parsedNotifications = JSON.parse(notifications);
|
||||
@@ -135,13 +142,13 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
||||
allNotifications[user][bodyShopId] = parsedNotifications;
|
||||
}
|
||||
await pubClient.del(userKey);
|
||||
logger.logger.debug(`Deleted Redis key ${userKey}`);
|
||||
devDebugLogger(`Deleted Redis key ${userKey}`);
|
||||
} else {
|
||||
logger.logger.warn(`No notifications found for ${user} under ${userKey}`);
|
||||
devDebugLogger(`No notifications found for ${user} under ${userKey}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.logger.debug(`Consolidated notifications: ${JSON.stringify(allNotifications)}`);
|
||||
devDebugLogger(`Consolidated notifications: ${JSON.stringify(allNotifications)}`);
|
||||
|
||||
// Insert notifications into the database and collect IDs
|
||||
const notificationInserts = [];
|
||||
@@ -168,7 +175,7 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
||||
const insertResponse = await graphQLClient.request(INSERT_NOTIFICATIONS_MUTATION, {
|
||||
objects: notificationInserts
|
||||
});
|
||||
logger.logger.info(
|
||||
devDebugLogger(
|
||||
`Inserted ${insertResponse.insert_notifications.affected_rows} notifications for jobId ${jobId}`
|
||||
);
|
||||
|
||||
@@ -202,51 +209,61 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
||||
associationId
|
||||
});
|
||||
});
|
||||
logger.logger.info(
|
||||
devDebugLogger(
|
||||
`Sent ${notifications.length} consolidated notifications to ${user} for jobId ${jobId} with notificationId ${notificationId}`
|
||||
);
|
||||
} else {
|
||||
logger.logger.warn(`No socket IDs found for ${user} in bodyShopId ${bodyShopId}`);
|
||||
devDebugLogger(`No socket IDs found for ${user} in bodyShopId ${bodyShopId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await pubClient.del(`app:consolidate:${jobId}`);
|
||||
await pubClient.del(`app:${devKey}:consolidate:${jobId}`);
|
||||
} catch (err) {
|
||||
logger.logger.error(`Consolidation error for jobId ${jobId}: ${err.message}`, { error: err });
|
||||
logger.log(`app-queue-consolidation-error`, "ERROR", "notifications", "api", {
|
||||
message: err?.message,
|
||||
stack: err?.stack
|
||||
});
|
||||
throw err;
|
||||
} finally {
|
||||
await pubClient.del(lockKey);
|
||||
}
|
||||
} else {
|
||||
logger.logger.info(`Skipped consolidation for jobId ${jobId} - lock held by another worker`);
|
||||
devDebugLogger(`Skipped consolidation for jobId ${jobId} - lock held by another worker`);
|
||||
}
|
||||
},
|
||||
{
|
||||
prefix,
|
||||
connection: pubClient,
|
||||
prefix: "{BULLMQ}",
|
||||
concurrency: 1,
|
||||
limiter: { max: 1, duration: RATE_LIMITER_DURATION }
|
||||
}
|
||||
);
|
||||
|
||||
addWorker.on("completed", (job) => logger.logger.info(`Add job ${job.id} completed`));
|
||||
consolidateWorker.on("completed", (job) => logger.logger.info(`Consolidate job ${job.id} completed`));
|
||||
addWorker.on("completed", (job) => devDebugLogger(`Add job ${job.id} completed`));
|
||||
consolidateWorker.on("completed", (job) => devDebugLogger(`Consolidate job ${job.id} completed`));
|
||||
|
||||
addWorker.on("failed", (job, err) =>
|
||||
logger.logger.error(`Add job ${job.id} failed: ${err.message}`, { error: err })
|
||||
logger.log(`app-queue-notification-error`, "ERROR", "notifications", "api", {
|
||||
message: err?.message,
|
||||
stack: err?.stack
|
||||
})
|
||||
);
|
||||
consolidateWorker.on("failed", (job, err) =>
|
||||
logger.logger.error(`Consolidate job ${job.id} failed: ${err.message}`, { error: err })
|
||||
logger.log(`app-queue-consolidation-failed:`, "ERROR", "notifications", "api", {
|
||||
message: err?.message,
|
||||
stack: err?.stack
|
||||
})
|
||||
);
|
||||
|
||||
// Register cleanup task instead of direct process listeners
|
||||
const shutdown = async () => {
|
||||
logger.logger.info("Closing app queue workers...");
|
||||
devDebugLogger("Closing app queue workers...");
|
||||
await Promise.all([addWorker.close(), consolidateWorker.close()]);
|
||||
logger.logger.info("App queue workers closed");
|
||||
devDebugLogger("App queue workers closed");
|
||||
};
|
||||
|
||||
process.on("SIGTERM", shutdown);
|
||||
process.on("SIGINT", shutdown);
|
||||
registerCleanupTask(shutdown);
|
||||
}
|
||||
|
||||
return addQueue;
|
||||
@@ -273,7 +290,7 @@ const dispatchAppsToQueue = async ({ appsToDispatch, logger }) => {
|
||||
{ jobId, bodyShopId, key, variables, recipients, body, jobRoNumber },
|
||||
{ jobId: `${jobId}:${Date.now()}` }
|
||||
);
|
||||
logger.logger.info(`Added notification to queue for jobId ${jobId} with ${recipients.length} recipients`);
|
||||
devDebugLogger(`Added notification to queue for jobId ${jobId} with ${recipients.length} recipients`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,11 +2,15 @@ const { Queue, Worker } = require("bullmq");
|
||||
const { sendTaskEmail } = require("../../email/sendemail");
|
||||
const generateEmailTemplate = require("../../email/generateTemplate");
|
||||
const { InstanceEndpoints } = require("../../utils/instanceMgr");
|
||||
const { registerCleanupTask } = require("../../utils/cleanupManager");
|
||||
const getBullMQPrefix = require("../../utils/getBullMQPrefix");
|
||||
const devDebugLogger = require("../../utils/devDebugLogger");
|
||||
const moment = require("moment-timezone");
|
||||
|
||||
const EMAIL_CONSOLIDATION_DELAY_IN_MINS = (() => {
|
||||
const envValue = process.env?.APP_CONSOLIDATION_DELAY_IN_MINS;
|
||||
const envValue = process.env?.EMAIL_CONSOLIDATION_DELAY_IN_MINS;
|
||||
const parsedValue = envValue ? parseInt(envValue, 10) : NaN;
|
||||
return isNaN(parsedValue) ? 1 : Math.max(1, parsedValue); // Default to 1, ensure at least 1
|
||||
return isNaN(parsedValue) ? 3 : Math.max(1, parsedValue); // Default to 3, ensure at least 1
|
||||
})();
|
||||
|
||||
// Base time-related constant (in milliseconds) / DO NOT TOUCH
|
||||
@@ -33,19 +37,22 @@ let emailConsolidateWorker;
|
||||
*/
|
||||
const loadEmailQueue = async ({ pubClient, logger }) => {
|
||||
if (!emailAddQueue || !emailConsolidateQueue) {
|
||||
logger.logger.info("Initializing Email Notification Queues");
|
||||
const prefix = getBullMQPrefix();
|
||||
const devKey = process.env?.NODE_ENV === "production" ? "prod" : "dev";
|
||||
|
||||
devDebugLogger(`Initializing Email Notification Queues with prefix: ${prefix}`);
|
||||
|
||||
// Queue for adding email notifications
|
||||
emailAddQueue = new Queue("emailAdd", {
|
||||
prefix,
|
||||
connection: pubClient,
|
||||
prefix: "{BULLMQ}",
|
||||
defaultJobOptions: { removeOnComplete: true, removeOnFail: true }
|
||||
});
|
||||
|
||||
// Queue for consolidating and sending emails
|
||||
emailConsolidateQueue = new Queue("emailConsolidate", {
|
||||
prefix,
|
||||
connection: pubClient,
|
||||
prefix: "{BULLMQ}",
|
||||
defaultJobOptions: { removeOnComplete: true, removeOnFail: true }
|
||||
});
|
||||
|
||||
@@ -53,45 +60,47 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
||||
emailAddWorker = new Worker(
|
||||
"emailAdd",
|
||||
async (job) => {
|
||||
const { jobId, jobRoNumber, bodyShopName, body, recipients } = job.data;
|
||||
logger.logger.info(`Adding email notifications for jobId ${jobId}`);
|
||||
const { jobId, jobRoNumber, bodyShopName, bodyShopTimezone, body, recipients } = job.data;
|
||||
devDebugLogger(`Adding email notifications for jobId ${jobId}`);
|
||||
|
||||
const redisKeyPrefix = `email:${devKey}:notifications:${jobId}`;
|
||||
|
||||
const redisKeyPrefix = `email:notifications:${jobId}`;
|
||||
for (const recipient of recipients) {
|
||||
const { user, firstName, lastName } = recipient;
|
||||
const userKey = `${redisKeyPrefix}:${user}`;
|
||||
await pubClient.rpush(userKey, body);
|
||||
await pubClient.expire(userKey, NOTIFICATION_EXPIRATION / 1000); // Set expiration
|
||||
const detailsKey = `email:recipientDetails:${jobId}:${user}`;
|
||||
await pubClient.expire(userKey, NOTIFICATION_EXPIRATION / 1000);
|
||||
const detailsKey = `email:${devKey}:recipientDetails:${jobId}:${user}`;
|
||||
await pubClient.hsetnx(detailsKey, "firstName", firstName || "");
|
||||
await pubClient.hsetnx(detailsKey, "lastName", lastName || "");
|
||||
await pubClient.expire(detailsKey, NOTIFICATION_EXPIRATION / 1000); // Set expiration
|
||||
await pubClient.sadd(`email:recipients:${jobId}`, user);
|
||||
logger.logger.debug(`Stored message for ${user} under ${userKey}: ${body}`);
|
||||
await pubClient.hsetnx(detailsKey, "bodyShopTimezone", bodyShopTimezone);
|
||||
await pubClient.expire(detailsKey, NOTIFICATION_EXPIRATION / 1000);
|
||||
await pubClient.sadd(`email:${devKey}:recipients:${jobId}`, user);
|
||||
devDebugLogger(`Stored message for ${user} under ${userKey}: ${body}`);
|
||||
}
|
||||
|
||||
const consolidateKey = `email:consolidate:${jobId}`;
|
||||
const consolidateKey = `email:${devKey}:consolidate:${jobId}`;
|
||||
const flagSet = await pubClient.setnx(consolidateKey, "pending");
|
||||
if (flagSet) {
|
||||
await emailConsolidateQueue.add(
|
||||
"consolidate-emails",
|
||||
{ jobId, jobRoNumber, bodyShopName },
|
||||
{ jobId, jobRoNumber, bodyShopName, bodyShopTimezone },
|
||||
{
|
||||
jobId: `consolidate:${jobId}`,
|
||||
delay: EMAIL_CONSOLIDATION_DELAY,
|
||||
attempts: 3, // Retry up to 3 times
|
||||
backoff: LOCK_EXPIRATION // Retry delay matches lock expiration (15s)
|
||||
attempts: 3,
|
||||
backoff: LOCK_EXPIRATION
|
||||
}
|
||||
);
|
||||
logger.logger.info(`Scheduled email consolidation for jobId ${jobId}`);
|
||||
await pubClient.expire(consolidateKey, CONSOLIDATION_KEY_EXPIRATION / 1000); // Convert to seconds
|
||||
devDebugLogger(`Scheduled email consolidation for jobId ${jobId}`);
|
||||
await pubClient.expire(consolidateKey, CONSOLIDATION_KEY_EXPIRATION / 1000);
|
||||
} else {
|
||||
logger.logger.debug(`Email consolidation already scheduled for jobId ${jobId}`);
|
||||
devDebugLogger(`Email consolidation already scheduled for jobId ${jobId}`);
|
||||
}
|
||||
},
|
||||
{
|
||||
prefix,
|
||||
connection: pubClient,
|
||||
prefix: "{BULLMQ}",
|
||||
concurrency: 5
|
||||
}
|
||||
);
|
||||
@@ -101,29 +110,30 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
||||
"emailConsolidate",
|
||||
async (job) => {
|
||||
const { jobId, jobRoNumber, bodyShopName } = job.data;
|
||||
logger.logger.info(`Consolidating emails for jobId ${jobId}`);
|
||||
devDebugLogger(`Consolidating emails for jobId ${jobId}`);
|
||||
|
||||
const lockKey = `lock:emailConsolidate:${jobId}`;
|
||||
const lockAcquired = await pubClient.set(lockKey, "locked", "NX", "EX", LOCK_EXPIRATION / 1000); // Convert to seconds
|
||||
const lockKey = `lock:${devKey}:emailConsolidate:${jobId}`;
|
||||
const lockAcquired = await pubClient.set(lockKey, "locked", "NX", "EX", LOCK_EXPIRATION / 1000);
|
||||
if (lockAcquired) {
|
||||
try {
|
||||
const recipientsSet = `email:recipients:${jobId}`;
|
||||
const recipientsSet = `email:${devKey}:recipients:${jobId}`;
|
||||
const recipients = await pubClient.smembers(recipientsSet);
|
||||
for (const recipient of recipients) {
|
||||
const userKey = `email:notifications:${jobId}:${recipient}`;
|
||||
const detailsKey = `email:recipientDetails:${jobId}:${recipient}`;
|
||||
const userKey = `email:${devKey}:notifications:${jobId}:${recipient}`;
|
||||
const detailsKey = `email:${devKey}:recipientDetails:${jobId}:${recipient}`;
|
||||
const messages = await pubClient.lrange(userKey, 0, -1);
|
||||
if (messages.length > 0) {
|
||||
const details = await pubClient.hgetall(detailsKey);
|
||||
const firstName = details.firstName || "User";
|
||||
const multipleUpdateString = messages.length > 1 ? "Updates" : "Update";
|
||||
const subject = `${multipleUpdateString} for job ${jobRoNumber} at ${bodyShopName}`;
|
||||
// Use the template instead of inline HTML
|
||||
const subject = `${multipleUpdateString} for job ${jobRoNumber || "N/A"} at ${bodyShopName}`;
|
||||
const timezone = moment.tz.zone(details?.bodyShopTimezone) ? details.bodyShopTimezone : "UTC";
|
||||
const emailBody = generateEmailTemplate({
|
||||
header: `${multipleUpdateString} for Job ${jobRoNumber}`,
|
||||
header: `${multipleUpdateString} for Job ${jobRoNumber || "N/A"}`,
|
||||
subHeader: `Dear ${firstName},`,
|
||||
dateLine: moment().tz(timezone).format("MM/DD/YYYY hh:mm a"),
|
||||
body: `
|
||||
<p>There have been updates to job ${jobRoNumber} at ${bodyShopName}:</p><br/>
|
||||
<p>There have been updates to job ${jobRoNumber || "N/A"} at ${bodyShopName}:</p><br/>
|
||||
<ul>
|
||||
${messages.map((msg) => `<li>${msg}</li>`).join("")}
|
||||
</ul><br/><br/>
|
||||
@@ -136,7 +146,7 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
||||
type: "html",
|
||||
html: emailBody
|
||||
});
|
||||
logger.logger.info(
|
||||
devDebugLogger(
|
||||
`Sent consolidated email to ${recipient} for jobId ${jobId} with ${messages.length} updates`
|
||||
);
|
||||
await pubClient.del(userKey);
|
||||
@@ -144,43 +154,52 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
||||
}
|
||||
}
|
||||
await pubClient.del(recipientsSet);
|
||||
await pubClient.del(`email:consolidate:${jobId}`);
|
||||
await pubClient.del(`email:${devKey}:consolidate:${jobId}`);
|
||||
} catch (err) {
|
||||
logger.logger.error(`Email consolidation error for jobId ${jobId}: ${err.message}`, { error: err });
|
||||
throw err; // Trigger retry if attempts remain
|
||||
logger.log(`email-queue-consolidation-error`, "ERROR", "notifications", "api", {
|
||||
message: err?.message,
|
||||
stack: err?.stack
|
||||
});
|
||||
throw err;
|
||||
} finally {
|
||||
await pubClient.del(lockKey);
|
||||
}
|
||||
} else {
|
||||
logger.logger.info(`Skipped email consolidation for jobId ${jobId} - lock held by another worker`);
|
||||
devDebugLogger(`Skipped email consolidation for jobId ${jobId} - lock held by another worker`);
|
||||
}
|
||||
},
|
||||
{
|
||||
prefix,
|
||||
connection: pubClient,
|
||||
prefix: "{BULLMQ}",
|
||||
concurrency: 1,
|
||||
limiter: { max: 1, duration: RATE_LIMITER_DURATION }
|
||||
}
|
||||
);
|
||||
|
||||
// Event handlers for workers
|
||||
emailAddWorker.on("completed", (job) => logger.logger.info(`Email add job ${job.id} completed`));
|
||||
emailConsolidateWorker.on("completed", (job) => logger.logger.info(`Email consolidate job ${job.id} completed`));
|
||||
emailAddWorker.on("completed", (job) => devDebugLogger(`Email add job ${job.id} completed`));
|
||||
emailConsolidateWorker.on("completed", (job) => devDebugLogger(`Email consolidate job ${job.id} completed`));
|
||||
|
||||
emailAddWorker.on("failed", (job, err) =>
|
||||
logger.logger.error(`Email add job ${job.id} failed: ${err.message}`, { error: err })
|
||||
logger.log(`add-email-queue-failed`, "ERROR", "notifications", "api", {
|
||||
message: err?.message,
|
||||
stack: err?.stack
|
||||
})
|
||||
);
|
||||
emailConsolidateWorker.on("failed", (job, err) =>
|
||||
logger.logger.error(`Email consolidate job ${job.id} failed: ${err.message}`, { error: err })
|
||||
logger.log(`email-consolidation-job-failed`, "ERROR", "notifications", "api", {
|
||||
message: err?.message,
|
||||
stack: err?.stack
|
||||
})
|
||||
);
|
||||
|
||||
// Graceful shutdown
|
||||
// Register cleanup task instead of direct process listeners
|
||||
const shutdown = async () => {
|
||||
logger.logger.info("Closing email queue workers...");
|
||||
devDebugLogger("Closing email queue workers...");
|
||||
await Promise.all([emailAddWorker.close(), emailConsolidateWorker.close()]);
|
||||
logger.logger.info("Email queue workers closed");
|
||||
devDebugLogger("Email queue workers closed");
|
||||
};
|
||||
process.on("SIGTERM", shutdown);
|
||||
process.on("SIGINT", shutdown);
|
||||
registerCleanupTask(shutdown);
|
||||
}
|
||||
|
||||
return emailAddQueue;
|
||||
@@ -211,22 +230,22 @@ const dispatchEmailsToQueue = async ({ emailsToDispatch, logger }) => {
|
||||
const emailAddQueue = getQueue();
|
||||
|
||||
for (const email of emailsToDispatch) {
|
||||
const { jobId, jobRoNumber, bodyShopName, body, recipients } = email;
|
||||
const { jobId, jobRoNumber, bodyShopName, bodyShopTimezone, body, recipients } = email;
|
||||
|
||||
if (!jobId || !jobRoNumber || !bodyShopName || !body || !recipients.length) {
|
||||
logger.logger.warn(
|
||||
devDebugLogger(
|
||||
`Skipping email dispatch for jobId ${jobId} due to missing data: ` +
|
||||
`jobRoNumber=${jobRoNumber}, bodyShopName=${bodyShopName}, body=${body}, recipients=${recipients.length}`
|
||||
`jobRoNumber=${jobRoNumber || "N/A"}, bodyShopName=${bodyShopName}, body=${body}, recipients=${recipients.length}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
await emailAddQueue.add(
|
||||
"add-email-notification",
|
||||
{ jobId, jobRoNumber, bodyShopName, body, recipients },
|
||||
{ jobId, jobRoNumber, bodyShopName, bodyShopTimezone, body, recipients },
|
||||
{ jobId: `${jobId}:${Date.now()}` }
|
||||
);
|
||||
logger.logger.info(`Added email notification to queue for jobId ${jobId} with ${recipients.length} recipients`);
|
||||
devDebugLogger(`Added email notification to queue for jobId ${jobId} with ${recipients.length} recipients`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,494 +1,403 @@
|
||||
const { getJobAssignmentType } = require("./stringHelpers");
|
||||
const { getJobAssignmentType, formatTaskPriority } = require("./stringHelpers");
|
||||
const moment = require("moment-timezone");
|
||||
const { startCase } = require("lodash");
|
||||
const Dinero = require("dinero.js");
|
||||
|
||||
Dinero.globalRoundingMode = "HALF_EVEN";
|
||||
|
||||
/**
|
||||
* Populates the recipients for app, email, and FCM notifications based on scenario watchers.
|
||||
*
|
||||
* @param {Object} data - The data object containing scenarioWatchers and bodyShopId.
|
||||
* @param {Object} result - The result object to populate with recipients for app, email, and FCM notifications.
|
||||
* Creates a standard notification object with app, email, and FCM properties and populates recipients.
|
||||
* @param {Object} data - Input data containing jobId, jobRoNumber, bodyShopId, bodyShopName, and scenarioWatchers
|
||||
* @param {string} key - Notification key for the app
|
||||
* @param {string} body - Notification body text
|
||||
* @param {Object} [variables={}] - Variables for the app notification
|
||||
* @returns {Object} Notification object with populated recipients
|
||||
*/
|
||||
const populateWatchers = (data, result) => {
|
||||
const buildNotification = (data, key, body, variables = {}) => {
|
||||
const result = {
|
||||
app: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopId: data.bodyShopId,
|
||||
key,
|
||||
body,
|
||||
variables,
|
||||
recipients: []
|
||||
},
|
||||
email: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopName: data.bodyShopName,
|
||||
bodyShopTimezone: data.bodyShopTimezone,
|
||||
body,
|
||||
recipients: []
|
||||
},
|
||||
fcm: { recipients: [] }
|
||||
};
|
||||
|
||||
// Populate recipients from scenarioWatchers
|
||||
data.scenarioWatchers.forEach((recipients) => {
|
||||
const { user, app, fcm, email, firstName, lastName, employeeId, associationId } = recipients;
|
||||
if (app === true) result.app.recipients.push({ user, bodyShopId: data.bodyShopId, employeeId, associationId });
|
||||
if (app === true)
|
||||
result.app.recipients.push({
|
||||
user,
|
||||
bodyShopId: data.bodyShopId,
|
||||
employeeId,
|
||||
associationId
|
||||
});
|
||||
if (fcm === true) result.fcm.recipients.push(user);
|
||||
if (email === true) result.email.recipients.push({ user, firstName, lastName });
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a notification for when the alternate transport is changed.
|
||||
* @param data
|
||||
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||
*/
|
||||
const alternateTransportChangedBuilder = (data) => {
|
||||
const oldTransport = data?.changedFields?.alt_transport?.old;
|
||||
const newTransport = data?.changedFields?.alt_transport?.new;
|
||||
let body;
|
||||
|
||||
if (oldTransport && newTransport)
|
||||
body = `The alternate transportation has been changed from ${oldTransport} to ${newTransport}.`;
|
||||
else if (!oldTransport && newTransport) body = `The alternate transportation has been set to ${newTransport}.`;
|
||||
else if (oldTransport && !newTransport)
|
||||
body = `The alternate transportation has been canceled (previously ${oldTransport}).`;
|
||||
else body = `The alternate transportation has been updated.`;
|
||||
|
||||
return buildNotification(data, "notifications.job.alternateTransportChanged", body, {
|
||||
alternateTransport: newTransport,
|
||||
oldAlternateTransport: oldTransport
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds notification data for changes to alternate transport.
|
||||
* Creates a notification for when a bill is posted.
|
||||
* @param data
|
||||
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||
*/
|
||||
const alternateTransportChangedBuilder = (data) => {
|
||||
const body = `The alternate transport status has been updated from ${data?.changedFields?.altTransport?.old}.`;
|
||||
const result = {
|
||||
app: {
|
||||
jobId: data.jobId,
|
||||
bodyShopId: data.bodyShopId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
key: "notifications.job.alternateTransportChanged",
|
||||
body, // Same as email body
|
||||
variables: {
|
||||
alternateTransport: data.changedFields.alt_transport?.new,
|
||||
oldAlternateTransport: data.changedFields.alt_transport?.old
|
||||
},
|
||||
recipients: []
|
||||
},
|
||||
email: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopName: data.bodyShopName,
|
||||
body,
|
||||
recipients: []
|
||||
},
|
||||
fcm: { recipients: [] }
|
||||
};
|
||||
const billPostedBuilder = (data) => {
|
||||
const facing = data?.data?.isinhouse ? "in-house" : "vendor";
|
||||
const body = `An ${facing} ${data?.data?.is_credit_memo ? "credit memo" : "bill"} has been posted.`.trim();
|
||||
|
||||
populateWatchers(data, result);
|
||||
return result;
|
||||
return buildNotification(data, "notifications.job.billPosted", body, {
|
||||
isInHouse: data?.data?.isinhouse,
|
||||
isCreditMemo: data?.data?.is_credit_memo
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds notification data for bill posted events.
|
||||
*/
|
||||
const billPostedHandler = (data) => {
|
||||
const body = `A bill of $${data.data.clm_total} has been posted.`;
|
||||
const result = {
|
||||
app: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopId: data.bodyShopId,
|
||||
key: "notifications.job.billPosted",
|
||||
body,
|
||||
variables: {
|
||||
clmTotal: data.data.clm_total
|
||||
},
|
||||
recipients: []
|
||||
},
|
||||
email: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopName: data.bodyShopName,
|
||||
body,
|
||||
recipients: []
|
||||
},
|
||||
fcm: { recipients: [] }
|
||||
};
|
||||
|
||||
populateWatchers(data, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds notification data for changes to critical parts status.
|
||||
* Creates a notification for when the status of critical parts changes.
|
||||
* @param data
|
||||
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||
*/
|
||||
const criticalPartsStatusChangedBuilder = (data) => {
|
||||
const body = `The critical parts status has changed to ${data.data.queued_for_parts ? "queued" : "not queued"}.`;
|
||||
const result = {
|
||||
app: {
|
||||
jobId: data.jobId,
|
||||
bodyShopId: data.bodyShopId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
key: "notifications.job.criticalPartsStatusChanged",
|
||||
body,
|
||||
variables: {
|
||||
queuedForParts: data.data.queued_for_parts,
|
||||
oldQueuedForParts: data.changedFields.queued_for_parts?.old
|
||||
},
|
||||
recipients: []
|
||||
},
|
||||
email: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopName: data.bodyShopName,
|
||||
body,
|
||||
recipients: []
|
||||
},
|
||||
fcm: { recipients: [] }
|
||||
};
|
||||
const lineDesc = data?.data?.line_desc;
|
||||
const status = data?.data?.status;
|
||||
const body = status
|
||||
? `The status on a critical part line (${lineDesc}) has been set to ${status}.`
|
||||
: `The status on a critical part line (${lineDesc}) has been cleared.`;
|
||||
|
||||
populateWatchers(data, result);
|
||||
return result;
|
||||
return buildNotification(data, "notifications.job.criticalPartsStatusChanged", body, {
|
||||
joblineId: data?.data?.id,
|
||||
status: data?.data?.status,
|
||||
line_desc: lineDesc
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds notification data for completed intake or delivery checklists.
|
||||
* Creates a notification for when the intake or delivery checklist is completed.
|
||||
* @param data
|
||||
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||
*/
|
||||
const intakeDeliveryChecklistCompletedBuilder = (data) => {
|
||||
const checklistType = data.changedFields.intakechecklist ? "intake" : "delivery";
|
||||
const checklistType = data?.changedFields?.intakechecklist ? "intake" : "delivery";
|
||||
const body = `The ${checklistType.charAt(0).toUpperCase() + checklistType.slice(1)} checklist has been completed.`;
|
||||
const result = {
|
||||
app: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopId: data.bodyShopId,
|
||||
key: "notifications.job.checklistCompleted",
|
||||
body,
|
||||
variables: {
|
||||
checklistType,
|
||||
completed: true
|
||||
},
|
||||
recipients: []
|
||||
},
|
||||
email: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopName: data.bodyShopName,
|
||||
body,
|
||||
recipients: []
|
||||
},
|
||||
fcm: { recipients: [] }
|
||||
};
|
||||
|
||||
populateWatchers(data, result);
|
||||
return result;
|
||||
return buildNotification(data, "notifications.job.checklistCompleted", body, {
|
||||
checklistType,
|
||||
completed: true
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds notification data for job assignment events.
|
||||
* Creates a notification for when a job is assigned to the user.
|
||||
* @param data
|
||||
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||
*/
|
||||
const jobAssignedToMeBuilder = (data) => {
|
||||
const body = `You have been assigned to [${getJobAssignmentType(data.scenarioFields?.[0])}]`;
|
||||
const result = {
|
||||
app: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopId: data.bodyShopId,
|
||||
key: "notifications.job.assigned",
|
||||
body,
|
||||
variables: {
|
||||
type: data.scenarioFields?.[0]
|
||||
},
|
||||
recipients: []
|
||||
},
|
||||
email: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopName: data.bodyShopName,
|
||||
body,
|
||||
recipients: []
|
||||
},
|
||||
fcm: { recipients: [] }
|
||||
};
|
||||
const body = `You have been assigned to ${getJobAssignmentType(data.scenarioFields?.[0])}.`;
|
||||
|
||||
populateWatchers(data, result);
|
||||
return result;
|
||||
return buildNotification(data, "notifications.job.assigned", body, {
|
||||
type: data.scenarioFields?.[0]
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds notification data for jobs added to production.
|
||||
* Creates a notification for when jobs are added to production.
|
||||
* @param data
|
||||
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||
*/
|
||||
const jobsAddedToProductionBuilder = (data) => {
|
||||
const body = `Job has been added to production.`;
|
||||
const result = {
|
||||
app: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopId: data.bodyShopId,
|
||||
key: "notifications.job.addedToProduction",
|
||||
body,
|
||||
variables: {},
|
||||
recipients: []
|
||||
},
|
||||
email: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopName: data.bodyShopName,
|
||||
body,
|
||||
recipients: []
|
||||
},
|
||||
fcm: { recipients: [] }
|
||||
};
|
||||
|
||||
populateWatchers(data, result);
|
||||
return result;
|
||||
const body = `Job is now in production.`;
|
||||
return buildNotification(data, "notifications.job.addedToProduction", body);
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds notification data for job status changes.
|
||||
* Creates a notification for when the job status changes.
|
||||
* @param data
|
||||
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||
*/
|
||||
const jobStatusChangeBuilder = (data) => {
|
||||
const body = `The status has changed from ${data.changedFields.status.old} to ${data.changedFields.status.new}`;
|
||||
const result = {
|
||||
app: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopId: data.bodyShopId,
|
||||
key: "notifications.job.statusChanged",
|
||||
body,
|
||||
variables: {
|
||||
status: data.changedFields.status.new,
|
||||
oldStatus: data.changedFields.status.old
|
||||
},
|
||||
recipients: []
|
||||
},
|
||||
email: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopName: data.bodyShopName,
|
||||
body,
|
||||
recipients: []
|
||||
},
|
||||
fcm: { recipients: [] }
|
||||
};
|
||||
const oldStatus = data?.changedFields?.status?.old;
|
||||
const newStatus = data?.changedFields?.status?.new;
|
||||
let body;
|
||||
|
||||
populateWatchers(data, result);
|
||||
return result;
|
||||
if (oldStatus && newStatus) body = `The status has been changed from ${oldStatus} to ${newStatus}.`;
|
||||
else if (!oldStatus && newStatus) body = `The status has been set to ${newStatus}.`;
|
||||
else if (oldStatus && !newStatus) body = `The status has been cleared (previously ${oldStatus}).`;
|
||||
else body = `The status has been updated.`;
|
||||
|
||||
return buildNotification(data, "notifications.job.statusChanged", body, {
|
||||
status: newStatus,
|
||||
oldStatus: oldStatus
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds notification data for new media added or reassigned events.
|
||||
* Creates a notification for when new media is added or reassigned.
|
||||
* @param data
|
||||
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||
*/
|
||||
const newMediaAddedReassignedBuilder = (data) => {
|
||||
const body = `New media has been added.`;
|
||||
const result = {
|
||||
app: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopId: data.bodyShopId,
|
||||
key: "notifications.job.newMediaAdded",
|
||||
body,
|
||||
variables: {},
|
||||
recipients: []
|
||||
},
|
||||
email: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopName: data.bodyShopName,
|
||||
body,
|
||||
recipients: []
|
||||
},
|
||||
fcm: { recipients: [] }
|
||||
};
|
||||
const mediaType = data?.data?.type?.startsWith("image") ? "Image" : "Document";
|
||||
const action = data?.data?._documentMoved
|
||||
? "moved to another job"
|
||||
: data.isNew
|
||||
? "added"
|
||||
: data.changedFields?.jobid && data.changedFields.jobid.old !== data.changedFields.jobid.new
|
||||
? "moved to this job"
|
||||
: "updated";
|
||||
const body = `An ${mediaType} has been ${action}.`;
|
||||
|
||||
populateWatchers(data, result);
|
||||
return result;
|
||||
return buildNotification(data, "notifications.job.newMediaAdded", body, {
|
||||
mediaType,
|
||||
action,
|
||||
movedToJob: data?.data?._movedToJob
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds notification data for new notes added to a job.
|
||||
* Creates a notification for when a new note is added.
|
||||
* @param data
|
||||
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||
*/
|
||||
const newNoteAddedBuilder = (data) => {
|
||||
const body = `A new note has been added: "${data.data.text}"`;
|
||||
const result = {
|
||||
app: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopId: data.bodyShopId,
|
||||
key: "notifications.job.newNoteAdded",
|
||||
body,
|
||||
variables: {
|
||||
text: data.data.text
|
||||
},
|
||||
recipients: []
|
||||
},
|
||||
email: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopName: data.bodyShopName,
|
||||
body,
|
||||
recipients: []
|
||||
},
|
||||
fcm: { recipients: [] }
|
||||
};
|
||||
const body = [
|
||||
"A",
|
||||
data?.data?.critical && "critical",
|
||||
data?.data?.private && "private",
|
||||
data?.data?.type,
|
||||
"note has been added by",
|
||||
`${data.data.created_by}`
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
populateWatchers(data, result);
|
||||
return result;
|
||||
return buildNotification(data, "notifications.job.newNoteAdded", body, {
|
||||
createdBy: data?.data?.created_by,
|
||||
critical: data?.data?.critical,
|
||||
type: data?.data?.type,
|
||||
private: data?.data?.private
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds notification data for new time tickets posted.
|
||||
* Creates a notification for when a new time ticket is posted.
|
||||
* @param data
|
||||
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||
*/
|
||||
const newTimeTicketPostedBuilder = (data) => {
|
||||
const body = `A new time ticket has been posted.`;
|
||||
const result = {
|
||||
app: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopId: data.bodyShopId,
|
||||
key: "notifications.job.newTimeTicketPosted",
|
||||
body,
|
||||
variables: {},
|
||||
recipients: []
|
||||
},
|
||||
email: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopName: data.bodyShopName,
|
||||
body,
|
||||
recipients: []
|
||||
},
|
||||
fcm: { recipients: [] }
|
||||
};
|
||||
const type = data?.data?.cost_center;
|
||||
const body = `A ${startCase(type.toLowerCase())} time ticket for ${data?.data?.date} has been posted.`.trim();
|
||||
|
||||
populateWatchers(data, result);
|
||||
return result;
|
||||
return buildNotification(data, "notifications.job.newTimeTicketPosted", body, {
|
||||
type,
|
||||
date: data?.data?.date
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds notification data for parts marked as back-ordered.
|
||||
* Creates a notification for when a part is marked as back-ordered.
|
||||
* @param data
|
||||
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||
*/
|
||||
const partMarkedBackOrderedBuilder = (data) => {
|
||||
const body = `A part has been marked as back-ordered.`;
|
||||
const result = {
|
||||
app: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopId: data.bodyShopId,
|
||||
key: "notifications.job.partBackOrdered",
|
||||
body,
|
||||
variables: {
|
||||
queuedForParts: data.changedFields.queued_for_parts?.new,
|
||||
oldQueuedForParts: data.changedFields.queued_for_parts?.old
|
||||
},
|
||||
recipients: []
|
||||
},
|
||||
email: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopName: data.bodyShopName,
|
||||
body,
|
||||
recipients: []
|
||||
},
|
||||
fcm: { recipients: [] }
|
||||
};
|
||||
const body = `A part ${data?.data?.line_desc} has been marked as back-ordered.`;
|
||||
|
||||
populateWatchers(data, result);
|
||||
return result;
|
||||
return buildNotification(data, "notifications.job.partBackOrdered", body, {
|
||||
line_desc: data?.data?.line_desc
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds notification data for payment collection events.
|
||||
* Creates a notification for when payment is collected or completed.
|
||||
* @param data
|
||||
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||
*/
|
||||
const paymentCollectedCompletedBuilder = (data) => {
|
||||
const body = `Payment of $${data.data.clm_total} has been collected.`;
|
||||
const result = {
|
||||
app: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopId: data.bodyShopId,
|
||||
key: "notifications.job.paymentCollected",
|
||||
body,
|
||||
variables: {
|
||||
clmTotal: data.data.clm_total
|
||||
},
|
||||
recipients: []
|
||||
},
|
||||
email: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopName: data.bodyShopName,
|
||||
body,
|
||||
recipients: []
|
||||
},
|
||||
fcm: { recipients: [] }
|
||||
};
|
||||
const momentFormat = "MM/DD/YYYY";
|
||||
const amountDinero = Dinero({ amount: Math.round((data.data.amount || 0) * 100) });
|
||||
const amountFormatted = amountDinero.toFormat();
|
||||
const payer = data.data.payer;
|
||||
const paymentType = data.data.type;
|
||||
const paymentDate = moment(data.data.date).format(momentFormat);
|
||||
const body = `Payment of ${amountFormatted} has been collected from ${payer} via ${paymentType} on ${paymentDate}`;
|
||||
|
||||
populateWatchers(data, result);
|
||||
return result;
|
||||
return buildNotification(data, "notifications.job.paymentCollected", body, {
|
||||
amount: data.data.amount,
|
||||
payer: data.data.payer,
|
||||
type: data.data.type,
|
||||
date: data.data.date
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds notification data for changes to scheduled dates.
|
||||
* Creates a notification for when scheduled dates are changed.
|
||||
* @param data
|
||||
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||
*/
|
||||
const scheduledDatesChangedBuilder = (data) => {
|
||||
const body = `Scheduled dates have been updated.`;
|
||||
const result = {
|
||||
app: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopId: data.bodyShopId,
|
||||
key: "notifications.job.scheduledDatesChanged",
|
||||
body,
|
||||
variables: {
|
||||
scheduledIn: data.changedFields.scheduled_in?.new,
|
||||
oldScheduledIn: data.changedFields.scheduled_in?.old,
|
||||
scheduledCompletion: data.changedFields.scheduled_completion?.new,
|
||||
oldScheduledCompletion: data.changedFields.scheduled_completion?.old,
|
||||
scheduledDelivery: data.changedFields.scheduled_delivery?.new,
|
||||
oldScheduledDelivery: data.changedFields.scheduled_delivery?.old
|
||||
},
|
||||
recipients: []
|
||||
},
|
||||
email: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopName: data.bodyShopName,
|
||||
body,
|
||||
recipients: []
|
||||
},
|
||||
fcm: { recipients: [] }
|
||||
const changedFields = data.changedFields;
|
||||
const fieldConfigs = {
|
||||
scheduled_in: "Scheduled In",
|
||||
scheduled_completion: "Scheduled Completion",
|
||||
scheduled_delivery: "Scheduled Delivery"
|
||||
};
|
||||
const formatDateTime = (date) => {
|
||||
if (!date) return "(no date set)";
|
||||
const formatted = moment(date).tz(data.bodyShopTimezone);
|
||||
return `${formatted.format("MM/DD/YYYY")} at ${formatted.format("hh:mm a")}`;
|
||||
};
|
||||
|
||||
populateWatchers(data, result);
|
||||
return result;
|
||||
const fieldMessages = Object.entries(fieldConfigs)
|
||||
.filter(([field]) => changedFields[field])
|
||||
.map(([field, label]) => {
|
||||
const { old, new: newValue } = changedFields[field];
|
||||
if (old && !newValue) return `${label} was cancelled (previously ${formatDateTime(old)}).`;
|
||||
else if (!old && newValue) return `${label} was set to ${formatDateTime(newValue)}.`;
|
||||
else if (old && newValue) return `${label} changed from ${formatDateTime(old)} to ${formatDateTime(newValue)}.`;
|
||||
return "";
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const body = fieldMessages.length > 0 ? fieldMessages.join(" ") : "Scheduled dates have been updated.";
|
||||
|
||||
return buildNotification(data, "notifications.job.scheduledDatesChanged", body, {
|
||||
scheduledIn: changedFields.scheduled_in?.new,
|
||||
oldScheduledIn: changedFields.scheduled_in?.old,
|
||||
scheduledCompletion: changedFields.scheduled_completion?.new,
|
||||
oldScheduledCompletion: changedFields.scheduled_completion?.old,
|
||||
scheduledDelivery: changedFields.scheduled_delivery?.new,
|
||||
oldScheduledDelivery: changedFields.scheduled_delivery?.old
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds notification data for supplement imported events.
|
||||
*/
|
||||
const supplementImportedBuilder = (data) => {
|
||||
const body = `A supplement of $${data.data.cieca_ttl?.data?.supp_amt || 0} has been imported.`;
|
||||
const result = {
|
||||
app: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopId: data.bodyShopId,
|
||||
key: "notifications.job.supplementImported",
|
||||
body,
|
||||
variables: {
|
||||
suppAmt: data.data.cieca_ttl?.data?.supp_amt
|
||||
},
|
||||
recipients: []
|
||||
},
|
||||
email: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopName: data.bodyShopName,
|
||||
body,
|
||||
recipients: []
|
||||
},
|
||||
fcm: { recipients: [] }
|
||||
};
|
||||
|
||||
populateWatchers(data, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds notification data for tasks updated or created.
|
||||
* Creates a notification for when tasks are updated or created.
|
||||
* @param data
|
||||
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||
*/
|
||||
const tasksUpdatedCreatedBuilder = (data) => {
|
||||
const body = `Tasks have been ${data.isNew ? "created" : "updated"}.`;
|
||||
const result = {
|
||||
app: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopId: data.bodyShopId,
|
||||
key: data.isNew ? "notifications.job.taskCreated" : "notifications.job.taskUpdated",
|
||||
body,
|
||||
variables: {
|
||||
isNew: data.isNew,
|
||||
roNumber: data.jobRoNumber
|
||||
},
|
||||
recipients: []
|
||||
},
|
||||
email: {
|
||||
jobId: data.jobId,
|
||||
jobRoNumber: data.jobRoNumber,
|
||||
bodyShopName: data.bodyShopName,
|
||||
body,
|
||||
recipients: []
|
||||
},
|
||||
fcm: { recipients: [] }
|
||||
};
|
||||
const momentFormat = "MM/DD/YYYY hh:mm a";
|
||||
const timezone = data.bodyShopTimezone;
|
||||
const taskTitle = data?.data?.title ? `"${data.data.title}"` : "Unnamed Task";
|
||||
|
||||
populateWatchers(data, result);
|
||||
return result;
|
||||
let body, variables;
|
||||
if (data.isNew) {
|
||||
const priority = formatTaskPriority(data?.data?.priority);
|
||||
const createdBy = data?.data?.created_by || "Unknown";
|
||||
const dueDate = data.data.due_date ? ` due on ${moment(data.data.due_date).tz(timezone).format(momentFormat)}` : "";
|
||||
const completedOnCreation = data.data.completed === true;
|
||||
body = `A ${priority} task ${taskTitle} has been created${completedOnCreation ? " and marked completed" : ""} by ${createdBy}${dueDate}.`;
|
||||
variables = {
|
||||
isNew: data.isNew,
|
||||
roNumber: data.jobRoNumber,
|
||||
title: data?.data?.title,
|
||||
priority: data?.data?.priority,
|
||||
createdBy: data?.data?.created_by,
|
||||
dueDate: data?.data?.due_date,
|
||||
completed: completedOnCreation ? data?.data?.completed : undefined
|
||||
};
|
||||
} else {
|
||||
const changedFields = data.changedFields;
|
||||
const fieldNames = Object.keys(changedFields);
|
||||
const oldTitle = changedFields.title ? `"${changedFields.title.old || "Unnamed Task"}"` : taskTitle;
|
||||
|
||||
if (fieldNames.length === 1 && changedFields.completed) {
|
||||
body = `Task ${oldTitle} was marked ${changedFields.completed.new ? "complete" : "incomplete"}`;
|
||||
variables = {
|
||||
isNew: data.isNew,
|
||||
roNumber: data.jobRoNumber,
|
||||
title: data?.data?.title,
|
||||
changedCompleted: changedFields.completed.new
|
||||
};
|
||||
} else {
|
||||
const fieldMessages = [];
|
||||
if (changedFields.title)
|
||||
fieldMessages.push(`Task ${oldTitle} changed title to "${changedFields.title.new || "unnamed task"}".`);
|
||||
if (changedFields.description) fieldMessages.push("Description updated.");
|
||||
if (changedFields.priority)
|
||||
fieldMessages.push(`Priority changed to ${formatTaskPriority(changedFields.priority.new)}.`);
|
||||
if (changedFields.due_date)
|
||||
fieldMessages.push(`Due date set to ${moment(changedFields.due_date.new).tz(timezone).format(momentFormat)}.`);
|
||||
if (changedFields.completed)
|
||||
fieldMessages.push(`Status changed to ${changedFields.completed.new ? "complete" : "incomplete"}.`);
|
||||
|
||||
body =
|
||||
fieldMessages.length > 0
|
||||
? fieldMessages.length === 1 && changedFields.title
|
||||
? fieldMessages[0]
|
||||
: `Task ${oldTitle} updated: ${fieldMessages.join(", ")}`
|
||||
: `Task ${oldTitle} has been updated.`;
|
||||
variables = {
|
||||
isNew: data.isNew,
|
||||
roNumber: data.jobRoNumber,
|
||||
title: data?.data?.title,
|
||||
changedTitleOld: changedFields.title?.old,
|
||||
changedTitleNew: changedFields.title?.new,
|
||||
changedPriority: changedFields.priority?.new,
|
||||
changedDueDate: changedFields.due_date?.new,
|
||||
changedCompleted: changedFields.completed?.new
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return buildNotification(
|
||||
data,
|
||||
data.isNew ? "notifications.job.taskCreated" : "notifications.job.taskUpdated",
|
||||
body,
|
||||
variables
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a notification for when a supplement is imported.
|
||||
* @param data
|
||||
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
||||
*/
|
||||
const supplementImportedBuilder = (data) => {
|
||||
const body = `A supplement has been imported.`;
|
||||
return buildNotification(data, "notifications.job.supplementImported", body);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
alternateTransportChangedBuilder,
|
||||
billPostedHandler,
|
||||
billPostedBuilder,
|
||||
criticalPartsStatusChangedBuilder,
|
||||
intakeDeliveryChecklistCompletedBuilder,
|
||||
jobAssignedToMeBuilder,
|
||||
|
||||
257
server/notifications/scenarioMapper.js
Normal file
257
server/notifications/scenarioMapper.js
Normal file
@@ -0,0 +1,257 @@
|
||||
const {
|
||||
jobAssignedToMeBuilder,
|
||||
billPostedBuilder,
|
||||
newNoteAddedBuilder,
|
||||
scheduledDatesChangedBuilder,
|
||||
tasksUpdatedCreatedBuilder,
|
||||
jobStatusChangeBuilder,
|
||||
jobsAddedToProductionBuilder,
|
||||
alternateTransportChangedBuilder,
|
||||
newTimeTicketPostedBuilder,
|
||||
intakeDeliveryChecklistCompletedBuilder,
|
||||
paymentCollectedCompletedBuilder,
|
||||
newMediaAddedReassignedBuilder,
|
||||
criticalPartsStatusChangedBuilder,
|
||||
supplementImportedBuilder,
|
||||
partMarkedBackOrderedBuilder
|
||||
} = require("./scenarioBuilders");
|
||||
const logger = require("../utils/logger");
|
||||
const { isFunction } = require("lodash");
|
||||
|
||||
/**
|
||||
* An array of notification scenario definitions.
|
||||
*
|
||||
* Each scenario object can include the following properties:
|
||||
* - key {string}: The unique scenario name.
|
||||
* - table {string}: The table name to check for changes.
|
||||
* - fields {Array<string>}: Fields to check for changes.
|
||||
* - matchToUserFields {Array<string>}: Fields used to match scenarios to user data.
|
||||
* - onNew {boolean|Array<boolean>}: Indicates whether the scenario should be triggered on new data.
|
||||
* - builder {Function}: A function to handle the scenario.
|
||||
* - onlyTruthyValues {boolean|Array<string>}: Specifies fields that must have truthy values for the scenario to match.
|
||||
* - filterCallback {Function}: Optional callback (sync or async) to further filter the scenario based on event data (returns boolean).
|
||||
* - enabled {boolean}: If true, the scenario is active; if false or omitted, the scenario is skipped.
|
||||
*/
|
||||
const notificationScenarios = [
|
||||
{
|
||||
key: "job-assigned-to-me",
|
||||
enabled: true,
|
||||
table: "jobs",
|
||||
fields: ["employee_prep", "employee_body", "employee_csr", "employee_refinish"],
|
||||
matchToUserFields: ["employee_prep", "employee_body", "employee_csr", "employee_refinish"],
|
||||
builder: jobAssignedToMeBuilder
|
||||
},
|
||||
{
|
||||
key: "bill-posted",
|
||||
enabled: true,
|
||||
table: "bills",
|
||||
builder: billPostedBuilder,
|
||||
onNew: true
|
||||
},
|
||||
{
|
||||
key: "new-note-added",
|
||||
enabled: true,
|
||||
table: "notes",
|
||||
builder: newNoteAddedBuilder,
|
||||
onNew: true
|
||||
},
|
||||
{
|
||||
key: "schedule-dates-changed",
|
||||
enabled: true,
|
||||
table: "jobs",
|
||||
fields: ["scheduled_in", "scheduled_completion", "scheduled_delivery"],
|
||||
builder: scheduledDatesChangedBuilder
|
||||
},
|
||||
{
|
||||
key: "tasks-updated-created",
|
||||
enabled: true,
|
||||
table: "tasks",
|
||||
fields: ["updated_at"],
|
||||
// onNew: true,
|
||||
builder: tasksUpdatedCreatedBuilder
|
||||
},
|
||||
{
|
||||
key: "job-status-change",
|
||||
enabled: true,
|
||||
table: "jobs",
|
||||
fields: ["status"],
|
||||
builder: jobStatusChangeBuilder
|
||||
},
|
||||
{
|
||||
key: "job-added-to-production",
|
||||
enabled: true,
|
||||
table: "jobs",
|
||||
fields: ["inproduction"],
|
||||
onlyTruthyValues: ["inproduction"],
|
||||
builder: jobsAddedToProductionBuilder
|
||||
},
|
||||
{
|
||||
key: "alternate-transport-changed",
|
||||
enabled: true,
|
||||
table: "jobs",
|
||||
fields: ["alt_transport"],
|
||||
builder: alternateTransportChangedBuilder
|
||||
},
|
||||
{
|
||||
key: "new-time-ticket-posted",
|
||||
enabled: true,
|
||||
table: "timetickets",
|
||||
builder: newTimeTicketPostedBuilder
|
||||
},
|
||||
{
|
||||
key: "intake-delivery-checklist-completed",
|
||||
enabled: true,
|
||||
table: "jobs",
|
||||
fields: ["intakechecklist", "deliverchecklist"],
|
||||
builder: intakeDeliveryChecklistCompletedBuilder
|
||||
},
|
||||
{
|
||||
key: "payment-collected-completed",
|
||||
enabled: true,
|
||||
table: "payments",
|
||||
onNew: true,
|
||||
builder: paymentCollectedCompletedBuilder
|
||||
},
|
||||
{
|
||||
// Only works on a non LMS ENV
|
||||
key: "new-media-added-reassigned",
|
||||
enabled: true,
|
||||
table: "documents",
|
||||
fields: ["jobid"],
|
||||
builder: newMediaAddedReassignedBuilder
|
||||
},
|
||||
{
|
||||
key: "critical-parts-status-changed",
|
||||
enabled: true,
|
||||
table: "joblines",
|
||||
fields: ["status"],
|
||||
onlyTruthyValues: ["status"],
|
||||
builder: criticalPartsStatusChangedBuilder,
|
||||
filterCallback: ({ eventData }) => !eventData?.data?.critical
|
||||
},
|
||||
{
|
||||
key: "part-marked-back-ordered",
|
||||
enabled: true,
|
||||
table: "joblines",
|
||||
fields: ["status"],
|
||||
builder: partMarkedBackOrderedBuilder,
|
||||
filterCallback: async ({ eventData, getBodyshopFromRedis }) => {
|
||||
try {
|
||||
const bodyshop = await getBodyshopFromRedis(eventData.bodyShopId);
|
||||
return eventData?.data?.status !== bodyshop?.md_order_statuses?.default_bo;
|
||||
} catch (err) {
|
||||
logger.log("notifications-error-parts-marked-back-ordered", "error", "notifications", "mapper", {
|
||||
message: err?.message,
|
||||
stack: err?.stack
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
// Holding off on this one for now, spans multiple tables
|
||||
{
|
||||
key: "supplement-imported",
|
||||
enabled: false,
|
||||
builder: supplementImportedBuilder
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns an array of scenarios that match the given event data.
|
||||
* Supports asynchronous callbacks for additional filtering.
|
||||
*
|
||||
* @param {Object} eventData - The parsed event data.
|
||||
* Expected properties:
|
||||
* - table: an object with a `name` property (e.g. { name: "tasks", schema: "public" })
|
||||
* - changedFieldNames: an array of changed field names (e.g. [ "description", "updated_at" ])
|
||||
* - isNew: boolean indicating whether the record is new or updated
|
||||
* - data: the new data object (used to check field values)
|
||||
* - (other properties may be added such as jobWatchers, bodyShopId, etc.)
|
||||
* @param {Function} getBodyshopFromRedis - Function to retrieve bodyshop data from Redis.
|
||||
* @returns {Promise<Array<Object>>} A promise resolving to an array of matching scenario objects.
|
||||
*/
|
||||
const getMatchingScenarios = async (eventData, getBodyshopFromRedis) => {
|
||||
const matches = [];
|
||||
for (const scenario of notificationScenarios) {
|
||||
// Check if the scenario is enabled; skip if not explicitly true
|
||||
if (scenario.enabled !== true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If eventData has a table, then only scenarios with a table property that matches should be considered.
|
||||
if (eventData.table) {
|
||||
if (!scenario.table || eventData.table.name !== scenario.table) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Check the onNew flag.
|
||||
// Allow onNew to be either a boolean or an array of booleans.
|
||||
if (Object.prototype.hasOwnProperty.call(scenario, "onNew")) {
|
||||
if (Array.isArray(scenario.onNew)) {
|
||||
if (!scenario.onNew.includes(eventData.isNew)) continue;
|
||||
} else {
|
||||
if (eventData.isNew !== scenario.onNew) continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If the scenario defines fields, ensure at least one of them is present in changedFieldNames.
|
||||
if (scenario.fields && scenario.fields.length > 0) {
|
||||
const hasMatchingField = scenario.fields.some((field) => eventData.changedFieldNames.includes(field));
|
||||
if (!hasMatchingField) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// OnlyTruthyValues logic:
|
||||
// If onlyTruthyValues is defined, check that the new values of specified fields (or all changed fields if true)
|
||||
// are truthy. If an array, only check the listed fields, which must be in scenario.fields.
|
||||
if (Object.prototype.hasOwnProperty.call(scenario, "onlyTruthyValues")) {
|
||||
let fieldsToCheck;
|
||||
|
||||
if (scenario.onlyTruthyValues === true) {
|
||||
// If true, check all fields in the scenario that changed
|
||||
fieldsToCheck = scenario.fields.filter((field) => eventData.changedFieldNames.includes(field));
|
||||
} else if (Array.isArray(scenario.onlyTruthyValues) && scenario.onlyTruthyValues.length > 0) {
|
||||
// If an array, check only the specified fields, ensuring they are in scenario.fields
|
||||
fieldsToCheck = scenario.onlyTruthyValues.filter(
|
||||
(field) => scenario.fields.includes(field) && eventData.changedFieldNames.includes(field)
|
||||
);
|
||||
// If no fields in onlyTruthyValues match the scenario’s fields or changed fields, skip this scenario
|
||||
if (fieldsToCheck.length === 0) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Invalid onlyTruthyValues (not true or a non-empty array), skip this scenario
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure all fields to check have truthy new values
|
||||
const allTruthy = fieldsToCheck.every((field) => Boolean(eventData.data[field]));
|
||||
if (!allTruthy) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the callback if defined, supporting both sync and async, and filter based on its return value
|
||||
if (isFunction(scenario?.filterCallback)) {
|
||||
const shouldFilter = await Promise.resolve(
|
||||
scenario.filterCallback({
|
||||
eventData,
|
||||
getBodyshopFromRedis
|
||||
})
|
||||
);
|
||||
if (shouldFilter) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
matches.push(scenario);
|
||||
}
|
||||
return matches;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
notificationScenarios,
|
||||
getMatchingScenarios
|
||||
};
|
||||
@@ -1,192 +0,0 @@
|
||||
const {
|
||||
jobAssignedToMeBuilder,
|
||||
billPostedHandler,
|
||||
newNoteAddedBuilder,
|
||||
scheduledDatesChangedBuilder,
|
||||
tasksUpdatedCreatedBuilder,
|
||||
jobStatusChangeBuilder,
|
||||
jobsAddedToProductionBuilder,
|
||||
alternateTransportChangedBuilder,
|
||||
newTimeTicketPostedBuilder,
|
||||
intakeDeliveryChecklistCompletedBuilder,
|
||||
paymentCollectedCompletedBuilder,
|
||||
newMediaAddedReassignedBuilder,
|
||||
criticalPartsStatusChangedBuilder,
|
||||
supplementImportedBuilder,
|
||||
partMarkedBackOrderedBuilder
|
||||
} = require("./scenarioBuilders");
|
||||
|
||||
/**
|
||||
* An array of notification scenario definitions.
|
||||
*
|
||||
* Each scenario object can include the following properties:
|
||||
* - key {string}: The unique scenario name.
|
||||
* - table {string}: The table name to check for changes.
|
||||
* - fields {Array<string>}: Fields to check for changes.
|
||||
* - matchToUserFields {Array<string>}: Fields used to match scenarios to user data.
|
||||
* - onNew {boolean|Array<boolean>}: Indicates whether the scenario should be triggered on new data.
|
||||
* - onlyTrue {Array<string>}: Specifies fields that must be true for the scenario to match.
|
||||
* - builder {Function}: A function to handle the scenario.
|
||||
*/
|
||||
const notificationScenarios = [
|
||||
{
|
||||
key: "job-assigned-to-me",
|
||||
table: "jobs",
|
||||
fields: ["employee_pre", "employee_body", "employee_csr", "employee_refinish"],
|
||||
matchToUserFields: ["employee_pre", "employee_body", "employee_csr", "employee_refinish"],
|
||||
builder: jobAssignedToMeBuilder
|
||||
},
|
||||
{
|
||||
key: "bill-posted",
|
||||
table: "bills",
|
||||
builder: billPostedHandler,
|
||||
onNew: true
|
||||
},
|
||||
{
|
||||
key: "new-note-added",
|
||||
table: "notes",
|
||||
builder: newNoteAddedBuilder,
|
||||
onNew: true
|
||||
},
|
||||
{
|
||||
key: "schedule-dates-changed",
|
||||
table: "jobs",
|
||||
fields: ["scheduled_in", "scheduled_completion", "scheduled_delivery"],
|
||||
builder: scheduledDatesChangedBuilder
|
||||
},
|
||||
{
|
||||
key: "tasks-updated-created",
|
||||
table: "tasks",
|
||||
fields: ["updated_at"],
|
||||
// onNew: true,
|
||||
builder: tasksUpdatedCreatedBuilder
|
||||
},
|
||||
{
|
||||
key: "job-status-change",
|
||||
table: "jobs",
|
||||
fields: ["status"],
|
||||
builder: jobStatusChangeBuilder
|
||||
},
|
||||
{
|
||||
key: "job-added-to-production",
|
||||
table: "jobs",
|
||||
fields: ["inproduction"],
|
||||
builder: jobsAddedToProductionBuilder
|
||||
},
|
||||
{
|
||||
key: "alternate-transport-changed",
|
||||
table: "jobs",
|
||||
fields: ["alt_transport"],
|
||||
builder: alternateTransportChangedBuilder
|
||||
},
|
||||
{
|
||||
key: "new-time-ticket-posted",
|
||||
table: "timetickets",
|
||||
builder: newTimeTicketPostedBuilder
|
||||
},
|
||||
{
|
||||
// Good test for batching as this will hit multiple scenarios
|
||||
key: "intake-delivery-checklist-completed",
|
||||
table: "jobs",
|
||||
fields: ["intakechecklist"],
|
||||
builder: intakeDeliveryChecklistCompletedBuilder
|
||||
},
|
||||
{
|
||||
key: "payment-collected-completed",
|
||||
table: "payments",
|
||||
onNew: true,
|
||||
builder: paymentCollectedCompletedBuilder
|
||||
},
|
||||
{
|
||||
// MAKE SURE YOU ARE NOT ON A LMS ENVIRONMENT
|
||||
// Potential Callbacks / Save for last
|
||||
// Not question mark for Non LMS Scenario
|
||||
key: "new-media-added-reassigned",
|
||||
table: "documents",
|
||||
fields: ["jobid"],
|
||||
builder: newMediaAddedReassignedBuilder
|
||||
},
|
||||
{
|
||||
key: "critical-parts-status-changed",
|
||||
table: "joblines",
|
||||
fields: ["critical"],
|
||||
onlyTrue: ["critical"],
|
||||
builder: criticalPartsStatusChangedBuilder
|
||||
},
|
||||
// -------------- Difficult ---------------
|
||||
// Holding off on this one for now
|
||||
{
|
||||
key: "supplement-imported",
|
||||
builder: supplementImportedBuilder
|
||||
// spans multiple tables,
|
||||
},
|
||||
// This one may be tricky as the jobid is not directly in the event data (this is probably wrong)
|
||||
// (should otherwise)
|
||||
// Status needs to mark meta data 'md_backorderd' for example
|
||||
// Double check Jobid
|
||||
{
|
||||
key: "part-marked-back-ordered",
|
||||
table: "joblines",
|
||||
builder: partMarkedBackOrderedBuilder
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns an array of scenarios that match the given event data.
|
||||
*
|
||||
* @param {Object} eventData - The parsed event data.
|
||||
* Expected properties:
|
||||
* - table: an object with a `name` property (e.g. { name: "tasks", schema: "public" })
|
||||
* - changedFieldNames: an array of changed field names (e.g. [ "description", "updated_at" ])
|
||||
* - isNew: boolean indicating whether the record is new or updated
|
||||
* - data: the new data object (used to check field values)
|
||||
* - (other properties may be added such as jobWatchers, bodyShopId, etc.)
|
||||
*
|
||||
* @returns {Array<Object>} An array of matching scenario objects.
|
||||
*/
|
||||
const getMatchingScenarios = (eventData) =>
|
||||
notificationScenarios.filter((scenario) => {
|
||||
// If eventData has a table, then only scenarios with a table property that matches should be considered.
|
||||
if (eventData.table) {
|
||||
if (!scenario.table || eventData.table.name !== scenario.table) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check the onNew flag.
|
||||
// Allow onNew to be either a boolean or an array of booleans.
|
||||
if (Object.prototype.hasOwnProperty.call(scenario, "onNew")) {
|
||||
if (Array.isArray(scenario.onNew)) {
|
||||
if (!scenario.onNew.includes(eventData.isNew)) return false;
|
||||
} else {
|
||||
if (eventData.isNew !== scenario.onNew) return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If the scenario defines fields, ensure at least one of them is present in changedFieldNames.
|
||||
if (scenario.fields && scenario.fields.length > 0) {
|
||||
const hasMatchingField = scenario.fields.some((field) => eventData.changedFieldNames.includes(field));
|
||||
if (!hasMatchingField) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// OnlyTrue logic:
|
||||
// If a scenario defines an onlyTrue array, then at least one of those fields must have changed
|
||||
// and its new value (from eventData.data) must be non-falsey.
|
||||
if (scenario.onlyTrue && Array.isArray(scenario.onlyTrue) && scenario.onlyTrue.length > 0) {
|
||||
const hasTruthyChange = scenario.onlyTrue.some(
|
||||
(field) => eventData.changedFieldNames.includes(field) && Boolean(eventData.data[field])
|
||||
);
|
||||
if (!hasTruthyChange) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
notificationScenarios,
|
||||
getMatchingScenarios
|
||||
};
|
||||
@@ -11,31 +11,37 @@ const eventParser = require("./eventParser");
|
||||
const { client: gqlClient } = require("../graphql-client/graphql-client");
|
||||
const queries = require("../graphql-client/queries");
|
||||
const { isEmpty, isFunction } = require("lodash");
|
||||
const { getMatchingScenarios } = require("./scenarioMapperr");
|
||||
const { getMatchingScenarios } = require("./scenarioMapper");
|
||||
const { dispatchEmailsToQueue } = require("./queues/emailQueue");
|
||||
const { dispatchAppsToQueue } = require("./queues/appQueue");
|
||||
|
||||
// If true, the user who commits the action will NOT receive notifications; if false, they will.
|
||||
const FILTER_SELF_FROM_WATCHERS = (() => process.env.NODE_ENV === "production")();
|
||||
const FILTER_SELF_FROM_WATCHERS = process.env?.FILTER_SELF_FROM_WATCHERS !== "false";
|
||||
|
||||
/**
|
||||
* Parses an event and determines matching scenarios for notifications.
|
||||
* Queries job watchers and notification settings before triggering scenario builders.
|
||||
*
|
||||
* @param {Object} req - The request object containing event data, trigger, table, and logger.
|
||||
* @param {string} jobIdField - The field name used to extract the job ID from the event data.
|
||||
* @param {string} jobIdField - The field path (e.g., "req.body.event.new.id") to extract the job ID.
|
||||
* @returns {Promise<void>} Resolves when the parsing and notification dispatching process is complete.
|
||||
* @throws {Error} If required request fields (event data, trigger, or table) or body shop data are missing.
|
||||
*/
|
||||
const scenarioParser = async (req, jobIdField) => {
|
||||
const { event, trigger, table } = req.body;
|
||||
const { logger } = req;
|
||||
const {
|
||||
logger,
|
||||
sessionUtils: { getBodyshopFromRedis }
|
||||
} = req;
|
||||
|
||||
// Validate we know what user committed the action that fired the parser
|
||||
// Step 1: Validate we know what user committed the action that fired the parser
|
||||
|
||||
const hasuraUserRole = event?.session_variables?.["x-hasura-role"];
|
||||
const hasuraUserId = event?.session_variables?.["x-hasura-user-id"];
|
||||
|
||||
// Bail if we don't know
|
||||
if (!hasuraUserId) {
|
||||
// Bail if we don't know who started the scenario
|
||||
if (hasuraUserRole === "user" && !hasuraUserId) {
|
||||
logger.log("No Hasura user ID found, skipping notification parsing", "info", "notifications");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -44,22 +50,31 @@ const scenarioParser = async (req, jobIdField) => {
|
||||
throw new Error("Missing required request fields: event data, trigger, or table.");
|
||||
}
|
||||
|
||||
// Step 1: Parse the event data to extract details like job ID and changed fields
|
||||
const eventData = await eventParser({
|
||||
newData: event.data.new,
|
||||
oldData: event.data.old,
|
||||
trigger,
|
||||
table,
|
||||
jobIdField
|
||||
});
|
||||
// Step 2: Extract just the jobId using the provided jobIdField
|
||||
|
||||
let jobId = null;
|
||||
if (jobIdField) {
|
||||
let keyName = jobIdField;
|
||||
const prefix = "req.body.event.new.";
|
||||
if (keyName.startsWith(prefix)) {
|
||||
keyName = keyName.slice(prefix.length);
|
||||
}
|
||||
jobId = event.data.new[keyName] || (event.data.old && event.data.old[keyName]) || null;
|
||||
}
|
||||
|
||||
if (!jobId) {
|
||||
logger.log(`No jobId found using path "${jobIdField}", skipping notification parsing`, "info", "notifications");
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 3: Query job watchers associated with the job ID using GraphQL
|
||||
|
||||
// Step 2: Query job watchers associated with the job ID using GraphQL
|
||||
const watcherData = await gqlClient.request(queries.GET_JOB_WATCHERS, {
|
||||
jobid: eventData.jobId
|
||||
jobid: jobId
|
||||
});
|
||||
|
||||
// Transform watcher data into a simplified format with email and employee details
|
||||
let jobWatchers = watcherData?.job_watchers_aggregate?.nodes?.map((watcher) => ({
|
||||
let jobWatchers = watcherData?.job_watchers?.map((watcher) => ({
|
||||
email: watcher.user_email,
|
||||
firstName: watcher?.user?.employee?.first_name,
|
||||
lastName: watcher?.user?.employee?.last_name,
|
||||
@@ -67,18 +82,32 @@ const scenarioParser = async (req, jobIdField) => {
|
||||
authId: watcher?.user?.authid
|
||||
}));
|
||||
|
||||
if (FILTER_SELF_FROM_WATCHERS) {
|
||||
if (FILTER_SELF_FROM_WATCHERS && hasuraUserRole === "user") {
|
||||
jobWatchers = jobWatchers.filter((watcher) => watcher.authId !== hasuraUserId);
|
||||
}
|
||||
|
||||
// Exit early if no job watchers are found for this job
|
||||
if (isEmpty(jobWatchers)) {
|
||||
logger.log(`No watchers found for jobId "${jobId}", skipping notification parsing`, "info", "notifications");
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 3: Extract body shop information from the job data
|
||||
// Step 5: Perform the full event diff now that we know there are watchers
|
||||
|
||||
const eventData = await eventParser({
|
||||
newData: event.data.new,
|
||||
oldData: event.data.old,
|
||||
trigger,
|
||||
table,
|
||||
jobId
|
||||
});
|
||||
|
||||
// Step 6: Extract body shop information from the job data
|
||||
|
||||
const bodyShopId = watcherData?.job?.bodyshop?.id;
|
||||
const bodyShopName = watcherData?.job?.bodyshop?.shopname;
|
||||
const bodyShopTimezone = watcherData?.job?.bodyshop?.timezone;
|
||||
|
||||
const jobRoNumber = watcherData?.job?.ro_number;
|
||||
const jobClaimNumber = watcherData?.job?.clm_no;
|
||||
|
||||
@@ -87,16 +116,25 @@ const scenarioParser = async (req, jobIdField) => {
|
||||
throw new Error("No bodyshop data found for this job.");
|
||||
}
|
||||
|
||||
// Step 4: Identify scenarios that match the event data and job context
|
||||
const matchingScenarios = getMatchingScenarios({
|
||||
...eventData,
|
||||
jobWatchers,
|
||||
bodyShopId,
|
||||
bodyShopName
|
||||
});
|
||||
// Step 7: Identify scenarios that match the event data and job context
|
||||
|
||||
const matchingScenarios = await getMatchingScenarios(
|
||||
{
|
||||
...eventData,
|
||||
jobWatchers,
|
||||
bodyShopId,
|
||||
bodyShopName
|
||||
},
|
||||
getBodyshopFromRedis
|
||||
);
|
||||
|
||||
// Exit early if no matching scenarios are identified
|
||||
if (isEmpty(matchingScenarios)) {
|
||||
logger.log(
|
||||
`No matching scenarios found for jobId "${jobId}", skipping notification dispatch`,
|
||||
"info",
|
||||
"notifications"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -106,10 +144,12 @@ const scenarioParser = async (req, jobIdField) => {
|
||||
jobWatchers,
|
||||
bodyShopId,
|
||||
bodyShopName,
|
||||
bodyShopTimezone,
|
||||
matchingScenarios
|
||||
};
|
||||
|
||||
// Step 5: Query notification settings for the job watchers
|
||||
// Step 8: Query notification settings for the job watchers
|
||||
|
||||
const associationsData = await gqlClient.request(queries.GET_NOTIFICATION_ASSOCIATIONS, {
|
||||
emails: jobWatchers.map((x) => x.email),
|
||||
shopid: bodyShopId
|
||||
@@ -117,10 +157,16 @@ const scenarioParser = async (req, jobIdField) => {
|
||||
|
||||
// Exit early if no notification associations are found
|
||||
if (isEmpty(associationsData?.associations)) {
|
||||
logger.log(
|
||||
`No notification associations found for jobId "${jobId}", skipping notification dispatch`,
|
||||
"info",
|
||||
"notifications"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 6: Filter scenario watchers based on their enabled notification methods
|
||||
// Step 9: Filter scenario watchers based on their enabled notification methods
|
||||
|
||||
finalScenarioData.matchingScenarios = finalScenarioData.matchingScenarios.map((scenario) => ({
|
||||
...scenario,
|
||||
scenarioWatchers: associationsData.associations
|
||||
@@ -150,10 +196,16 @@ const scenarioParser = async (req, jobIdField) => {
|
||||
|
||||
// Exit early if no scenarios have eligible watchers after filtering
|
||||
if (isEmpty(finalScenarioData?.matchingScenarios)) {
|
||||
logger.log(
|
||||
`No eligible watchers after filtering for jobId "${jobId}", skipping notification dispatch`,
|
||||
"info",
|
||||
"notifications"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 7: Build and collect scenarios to dispatch notifications for
|
||||
// Step 10: Build and collect scenarios to dispatch notifications for
|
||||
|
||||
const scenariosToDispatch = [];
|
||||
|
||||
for (const scenario of finalScenarioData.matchingScenarios) {
|
||||
@@ -178,7 +230,8 @@ const scenarioParser = async (req, jobIdField) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Step 8: Filter scenario fields to include only those that changed
|
||||
// Step 11: Filter scenario fields to include only those that changed
|
||||
|
||||
const filteredScenarioFields =
|
||||
scenario.fields?.filter((field) => eventData.changedFieldNames.includes(field)) || [];
|
||||
|
||||
@@ -188,6 +241,7 @@ const scenarioParser = async (req, jobIdField) => {
|
||||
trigger: finalScenarioData.trigger.name,
|
||||
bodyShopId: finalScenarioData.bodyShopId,
|
||||
bodyShopName: finalScenarioData.bodyShopName,
|
||||
bodyShopTimezone: finalScenarioData.bodyShopTimezone,
|
||||
scenarioKey: scenario.key,
|
||||
scenarioTable: scenario.table,
|
||||
scenarioFields: filteredScenarioFields,
|
||||
@@ -204,35 +258,31 @@ const scenarioParser = async (req, jobIdField) => {
|
||||
);
|
||||
}
|
||||
|
||||
// Exit early if no scenarios are ready to dispatch
|
||||
if (isEmpty(scenariosToDispatch)) {
|
||||
logger.log(`No scenarios to dispatch for jobId "${jobId}" after building`, "info", "notifications");
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 9: Dispatch email notifications to the email queue
|
||||
// Step 12: Dispatch email notifications to the email queue
|
||||
|
||||
const emailsToDispatch = scenariosToDispatch.map((scenario) => scenario?.email);
|
||||
if (!isEmpty(emailsToDispatch)) {
|
||||
dispatchEmailsToQueue({
|
||||
emailsToDispatch,
|
||||
logger
|
||||
}).catch((e) =>
|
||||
// Log any errors encountered during email dispatching
|
||||
dispatchEmailsToQueue({ emailsToDispatch, logger }).catch((e) =>
|
||||
logger.log("Something went wrong dispatching emails to the Email Notification Queue", "error", "queue", null, {
|
||||
message: e?.message
|
||||
message: e?.message,
|
||||
stack: e?.stack
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Step 10: Dispatch app notifications to the app queue
|
||||
// Step 13: Dispatch app notifications to the app queue
|
||||
|
||||
const appsToDispatch = scenariosToDispatch.map((scenario) => scenario?.app);
|
||||
if (!isEmpty(appsToDispatch)) {
|
||||
dispatchAppsToQueue({
|
||||
appsToDispatch,
|
||||
logger
|
||||
}).catch((e) =>
|
||||
// Log any errors encountered during app notification dispatching
|
||||
dispatchAppsToQueue({ appsToDispatch, logger }).catch((e) =>
|
||||
logger.log("Something went wrong dispatching apps to the App Notification Queue", "error", "queue", null, {
|
||||
message: e?.message
|
||||
message: e?.message,
|
||||
stack: e?.stack
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
*/
|
||||
const getJobAssignmentType = (data) => {
|
||||
switch (data) {
|
||||
case "employee_pre":
|
||||
case "employee_prep":
|
||||
return "Prep";
|
||||
case "employee_body":
|
||||
return "Body";
|
||||
@@ -26,6 +26,17 @@ const getJobAssignmentType = (data) => {
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getJobAssignmentType
|
||||
const formatTaskPriority = (priority) => {
|
||||
if (priority === 1) {
|
||||
return "High";
|
||||
} else if (priority === 3) {
|
||||
return "Low";
|
||||
} else {
|
||||
return "Medium";
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getJobAssignmentType,
|
||||
formatTaskPriority
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLCl
|
||||
const { taskAssignedEmail, tasksRemindEmail } = require("../email/tasksEmails");
|
||||
const { canvastest } = require("../render/canvas-handler");
|
||||
const { alertCheck } = require("../alerts/alertcheck");
|
||||
const updateBodyshopCache = require("../web-sockets/updateBodyshopCache");
|
||||
const uuid = require("uuid").v4;
|
||||
|
||||
//Test route to ensure Express is responding.
|
||||
@@ -58,6 +59,7 @@ router.get("/test-logs", eventAuthorizationMiddleware, (req, res) => {
|
||||
|
||||
return res.status(500).send("Logs tested.");
|
||||
});
|
||||
|
||||
router.get("/wstest", eventAuthorizationMiddleware, (req, res) => {
|
||||
const { ioRedis } = req;
|
||||
ioRedis.to(`bodyshop-broadcast-room:bfec8c8c-b7f1-49e0-be4c-524455f4e582`).emit("new-message-summary", {
|
||||
@@ -137,4 +139,20 @@ router.post("/canvastest", validateFirebaseIdTokenMiddleware, canvastest);
|
||||
// Alert Check
|
||||
router.post("/alertcheck", eventAuthorizationMiddleware, alertCheck);
|
||||
|
||||
// Redis Cache Routes
|
||||
router.post("/bodyshop-cache", eventAuthorizationMiddleware, updateBodyshopCache);
|
||||
|
||||
// Health Check for docker-compose-cluster load balancer, only available in development
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
router.get("/health", (req, res) => {
|
||||
const healthStatus = {
|
||||
status: "healthy",
|
||||
timestamp: new Date().toISOString(),
|
||||
environment: process.env.NODE_ENV || "unknown",
|
||||
uptime: process.uptime()
|
||||
};
|
||||
res.status(200).json(healthStatus);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
|
||||
52
server/utils/cleanupManager.js
Normal file
52
server/utils/cleanupManager.js
Normal file
@@ -0,0 +1,52 @@
|
||||
// server/utils/cleanupManager.js
|
||||
const logger = require("./logger");
|
||||
|
||||
let cleanupTasks = [];
|
||||
let isShuttingDown = false;
|
||||
|
||||
/**
|
||||
* Register a cleanup task to be executed during shutdown
|
||||
* @param {Function} task - The cleanup task to register
|
||||
*/
|
||||
function registerCleanupTask(task) {
|
||||
cleanupTasks.push(task);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle SIGTERM signal for graceful shutdown
|
||||
*/
|
||||
async function handleSigterm() {
|
||||
if (isShuttingDown) {
|
||||
logger.log("sigterm-api", "WARN", null, null, { message: "Shutdown already in progress, ignoring signal." });
|
||||
return;
|
||||
}
|
||||
|
||||
isShuttingDown = true;
|
||||
|
||||
logger.log("sigterm-api", "WARN", null, null, { message: "SIGTERM Received. Starting graceful shutdown." });
|
||||
|
||||
try {
|
||||
for (const task of cleanupTasks) {
|
||||
logger.log("sigterm-api", "WARN", null, null, { message: `Running cleanup task: ${task.name}` });
|
||||
await task();
|
||||
}
|
||||
logger.log("sigterm-api", "WARN", null, null, { message: `All cleanup tasks completed.` });
|
||||
} catch (error) {
|
||||
logger.log("sigterm-api-error", "ERROR", null, null, { message: error.message, stack: error.stack });
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize cleanup manager with process event listeners
|
||||
*/
|
||||
function initializeCleanupManager() {
|
||||
process.on("SIGTERM", handleSigterm);
|
||||
process.on("SIGINT", handleSigterm); // Handle Ctrl+C
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
registerCleanupTask,
|
||||
initializeCleanupManager
|
||||
};
|
||||
10
server/utils/devDebugLogger.js
Normal file
10
server/utils/devDebugLogger.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const logger = require("./logger");
|
||||
|
||||
const devDebugLogger = (message, meta) => {
|
||||
if (process.env?.NODE_ENV === "production") {
|
||||
return;
|
||||
}
|
||||
logger.logger.debug(message, meta);
|
||||
};
|
||||
|
||||
module.exports = devDebugLogger;
|
||||
3
server/utils/getBullMQPrefix.js
Normal file
3
server/utils/getBullMQPrefix.js
Normal file
@@ -0,0 +1,3 @@
|
||||
const getBullMQPrefix = () => (process.env?.NODE_ENV === "production" ? "{PROD-BULLMQ}" : "{DEV-BULLMQ}");
|
||||
|
||||
module.exports = getBullMQPrefix;
|
||||
@@ -1,7 +1,9 @@
|
||||
const applyIOHelpers = ({ app, api, io, logger }) => {
|
||||
const getBodyshopRoom = (bodyshopID) => `bodyshop-broadcast-room:${bodyshopID}`;
|
||||
// Global Bodyshop Room
|
||||
const getBodyshopRoom = (bodyshopId) => `bodyshop-broadcast-room:${bodyshopId}`;
|
||||
|
||||
// Messaging - conversation specific room to handle detailed messages when the user has a conversation open.
|
||||
const getBodyshopConversationRoom = ({bodyshopId, conversationId}) =>
|
||||
const getBodyshopConversationRoom = ({ bodyshopId, conversationId }) =>
|
||||
`bodyshop-conversation-room:${bodyshopId}:${conversationId}`;
|
||||
|
||||
const ioHelpersAPI = {
|
||||
|
||||
@@ -1,3 +1,48 @@
|
||||
const { GET_BODYSHOP_BY_ID } = require("../graphql-client/queries");
|
||||
const devDebugLogger = require("./devDebugLogger");
|
||||
const client = require("../graphql-client/graphql-client").client;
|
||||
|
||||
const BODYSHOP_CACHE_TTL = 3600; // 1 hour
|
||||
|
||||
/**
|
||||
* Generate a cache key for a bodyshop
|
||||
* @param bodyshopId
|
||||
* @returns {`bodyshop-cache:${string}`}
|
||||
*/
|
||||
const getBodyshopCacheKey = (bodyshopId) => `bodyshop-cache:${bodyshopId}`;
|
||||
|
||||
/**
|
||||
* Generate a cache key for a user socket mapping
|
||||
* @param email
|
||||
* @returns {`user:${string}:${string}:socketMapping`}
|
||||
*/
|
||||
const getUserSocketMappingKey = (email) =>
|
||||
`user:${process.env?.NODE_ENV === "production" ? "prod" : "dev"}:${email}:socketMapping`;
|
||||
|
||||
/**
|
||||
* Fetch bodyshop data from the database
|
||||
* @param bodyshopId
|
||||
* @param logger
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
const fetchBodyshopFromDB = async (bodyshopId, logger) => {
|
||||
try {
|
||||
const response = await client.request(GET_BODYSHOP_BY_ID, { id: bodyshopId });
|
||||
const bodyshop = response.bodyshops_by_pk;
|
||||
if (!bodyshop) {
|
||||
throw new Error(`Bodyshop with ID ${bodyshopId} not found`);
|
||||
}
|
||||
return bodyshop; // Return the full object as-is
|
||||
} catch (error) {
|
||||
logger.log("fetch-bodyshop-from-db", "ERROR", "redis", null, {
|
||||
bodyshopId,
|
||||
error: error?.message,
|
||||
stack: error?.stack
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply Redis helper functions
|
||||
* @param pubClient
|
||||
@@ -33,112 +78,17 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Store multiple session data in Redis
|
||||
const setMultipleSessionData = async (socketId, keyValues) => {
|
||||
try {
|
||||
// keyValues is expected to be an object { key1: value1, key2: value2, ... }
|
||||
const entries = Object.entries(keyValues).map(([key, value]) => [key, JSON.stringify(value)]);
|
||||
await pubClient.hset(`socket:${socketId}`, ...entries.flat());
|
||||
} catch (error) {
|
||||
logger.log(`Error Setting Multiple Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||
}
|
||||
};
|
||||
|
||||
// Retrieve multiple session data from Redis
|
||||
const getMultipleSessionData = async (socketId, keys) => {
|
||||
try {
|
||||
const data = await pubClient.hmget(`socket:${socketId}`, keys);
|
||||
// Redis returns an object with null values for missing keys, so we parse the non-null ones
|
||||
return Object.fromEntries(keys.map((key, index) => [key, data[index] ? JSON.parse(data[index]) : null]));
|
||||
} catch (error) {
|
||||
logger.log(`Error Getting Multiple Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||
}
|
||||
};
|
||||
|
||||
const setMultipleFromArraySessionData = async (socketId, keyValueArray) => {
|
||||
try {
|
||||
// Use Redis multi/pipeline to batch the commands
|
||||
const multi = pubClient.multi();
|
||||
keyValueArray.forEach(([key, value]) => {
|
||||
multi.hset(`socket:${socketId}`, key, JSON.stringify(value));
|
||||
});
|
||||
await multi.exec(); // Execute all queued commands
|
||||
} catch (error) {
|
||||
logger.log(`Error Setting Multiple Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to add an item to the end of the Redis list
|
||||
const addItemToEndOfList = async (socketId, key, newItem) => {
|
||||
try {
|
||||
await pubClient.rpush(`socket:${socketId}:${key}`, JSON.stringify(newItem));
|
||||
} catch (error) {
|
||||
let userEmail = "unknown";
|
||||
let socketMappings = {};
|
||||
try {
|
||||
const userData = await getSessionData(socketId, "user");
|
||||
if (userData && userData.email) {
|
||||
userEmail = userData.email;
|
||||
socketMappings = await getUserSocketMapping(userEmail);
|
||||
}
|
||||
} catch (sessionError) {
|
||||
logger.log(`Failed to fetch session data for socket ${socketId}: ${sessionError}`, "ERROR", "redis");
|
||||
}
|
||||
const mappingString = JSON.stringify(socketMappings, null, 2);
|
||||
const errorMessage = `Error adding item to the end of the list for socket ${socketId}: ${error}. User: ${userEmail}, Socket Mappings: ${mappingString}`;
|
||||
logger.log(errorMessage, "ERROR", "redis");
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to add an item to the beginning of the Redis list
|
||||
const addItemToBeginningOfList = async (socketId, key, newItem) => {
|
||||
try {
|
||||
await pubClient.lpush(`socket:${socketId}:${key}`, JSON.stringify(newItem));
|
||||
} catch (error) {
|
||||
logger.log(`Error adding item to the beginning of the list for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to clear a list in Redis
|
||||
const clearList = async (socketId, key) => {
|
||||
try {
|
||||
await pubClient.del(`socket:${socketId}:${key}`);
|
||||
} catch (error) {
|
||||
logger.log(`Error clearing list for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||
}
|
||||
};
|
||||
|
||||
// Add methods to manage room users
|
||||
const addUserToRoom = async (room, user) => {
|
||||
try {
|
||||
await pubClient.sadd(room, JSON.stringify(user));
|
||||
} catch (error) {
|
||||
logger.log(`Error adding user to room ${room}: ${error}`, "ERROR", "redis");
|
||||
}
|
||||
};
|
||||
|
||||
const removeUserFromRoom = async (room, user) => {
|
||||
try {
|
||||
await pubClient.srem(room, JSON.stringify(user));
|
||||
} catch (error) {
|
||||
logger.log(`Error removing user to room ${room}: ${error}`, "ERROR", "redis");
|
||||
}
|
||||
};
|
||||
|
||||
const getUsersInRoom = async (room) => {
|
||||
try {
|
||||
const users = await pubClient.smembers(room);
|
||||
return users.map((user) => JSON.parse(user));
|
||||
} catch (error) {
|
||||
logger.log(`Error getting users in room ${room}: ${error}`, "ERROR", "redis");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a socket mapping for a user
|
||||
* @param email
|
||||
* @param socketId
|
||||
* @param bodyshopId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const addUserSocketMapping = async (email, socketId, bodyshopId) => {
|
||||
const userKey = `user:${email}`;
|
||||
const socketMappingKey = `${userKey}:socketMapping`;
|
||||
const socketMappingKey = getUserSocketMappingKey(email);
|
||||
try {
|
||||
logger.log(`Adding socket ${socketId} to user ${email} for bodyshop ${bodyshopId}`, "debug", "redis");
|
||||
devDebugLogger(`Adding socket ${socketId} to user ${email} for bodyshop ${bodyshopId}`);
|
||||
// Save the mapping: socketId -> bodyshopId
|
||||
await pubClient.hset(socketMappingKey, socketId, bodyshopId);
|
||||
// Set TTL (24 hours) for the mapping hash
|
||||
@@ -148,38 +98,45 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Refresh the TTL for a user's socket mapping
|
||||
* @param email
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const refreshUserSocketTTL = async (email) => {
|
||||
const userKey = `user:${email}`;
|
||||
const socketMappingKey = `${userKey}:socketMapping`;
|
||||
const socketMappingKey = getUserSocketMappingKey(email);
|
||||
|
||||
try {
|
||||
const exists = await pubClient.exists(socketMappingKey);
|
||||
if (exists) {
|
||||
await pubClient.expire(socketMappingKey, 86400);
|
||||
logger.log(`Refreshed TTL for ${email} socket mapping`, "debug", "redis");
|
||||
devDebugLogger(`Refreshed TTL for ${email} socket mapping`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log(`Error refreshing TTL for ${email}: ${error}`, "ERROR", "redis");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a socket mapping for a user
|
||||
* @param email
|
||||
* @param socketId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const removeUserSocketMapping = async (email, socketId) => {
|
||||
const userKey = `user:${email}`;
|
||||
const socketMappingKey = `${userKey}:socketMapping`;
|
||||
const socketMappingKey = getUserSocketMappingKey(email);
|
||||
|
||||
try {
|
||||
logger.log(`Removing socket ${socketId} mapping for user ${email}`, "DEBUG", "redis");
|
||||
devDebugLogger(`Removing socket ${socketId} mapping for user ${email}`);
|
||||
// Look up the bodyshopId associated with this socket
|
||||
const bodyshopId = await pubClient.hget(socketMappingKey, socketId);
|
||||
if (!bodyshopId) {
|
||||
logger.log(`Socket ${socketId} not found for user ${email}`, "DEBUG", "redis");
|
||||
devDebugLogger(`Socket ${socketId} not found for user ${email}`);
|
||||
return;
|
||||
}
|
||||
// Remove the socket mapping
|
||||
await pubClient.hdel(socketMappingKey, socketId);
|
||||
logger.log(
|
||||
`Removed socket ${socketId} (associated with bodyshop ${bodyshopId}) for user ${email}`,
|
||||
"DEBUG",
|
||||
"redis"
|
||||
);
|
||||
devDebugLogger(`Removed socket ${socketId} (associated with bodyshop ${bodyshopId}) for user ${email}`);
|
||||
|
||||
// Refresh TTL if any socket mappings remain
|
||||
const remainingSockets = await pubClient.hlen(socketMappingKey);
|
||||
@@ -191,9 +148,14 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all socket mappings for a user
|
||||
* @param email
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
const getUserSocketMapping = async (email) => {
|
||||
const userKey = `user:${email}`;
|
||||
const socketMappingKey = `${userKey}:socketMapping`;
|
||||
const socketMappingKey = getUserSocketMappingKey(email);
|
||||
|
||||
try {
|
||||
// Retrieve all socket mappings for the user
|
||||
const mapping = await pubClient.hgetall(socketMappingKey);
|
||||
@@ -213,23 +175,235 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get socket IDs for a user by bodyshopId
|
||||
* @param email
|
||||
* @param bodyshopId
|
||||
* @returns {Promise<{socketIds: [string, string], ttl: *}>}
|
||||
*/
|
||||
const getUserSocketMappingByBodyshop = async (email, bodyshopId) => {
|
||||
const socketMappingKey = getUserSocketMappingKey(email);
|
||||
|
||||
try {
|
||||
// Retrieve all socket mappings for the user
|
||||
const mapping = await pubClient.hgetall(socketMappingKey);
|
||||
const ttl = await pubClient.ttl(socketMappingKey);
|
||||
// Filter socket IDs for the provided bodyshopId
|
||||
const socketIds = Object.entries(mapping).reduce((acc, [socketId, bId]) => {
|
||||
if (bId === bodyshopId) {
|
||||
acc.push(socketId);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
return { socketIds, ttl };
|
||||
} catch (error) {
|
||||
logger.log(`Error retrieving socket mappings for ${email} by bodyshop ${bodyshopId}: ${error}`, "ERROR", "redis");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get bodyshop data from Redis
|
||||
* @param bodyshopId
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
const getBodyshopFromRedis = async (bodyshopId) => {
|
||||
const key = getBodyshopCacheKey(bodyshopId);
|
||||
try {
|
||||
// Check if data exists in Redis
|
||||
const cachedData = await pubClient.get(key);
|
||||
if (cachedData) {
|
||||
return JSON.parse(cachedData); // Parse and return the full object
|
||||
}
|
||||
|
||||
// Cache miss: fetch from DB
|
||||
const bodyshopData = await fetchBodyshopFromDB(bodyshopId, logger);
|
||||
|
||||
// Store in Redis as a single JSON string
|
||||
const jsonData = JSON.stringify(bodyshopData);
|
||||
await pubClient.set(key, jsonData);
|
||||
await pubClient.expire(key, BODYSHOP_CACHE_TTL);
|
||||
|
||||
devDebugLogger("bodyshop-cache-miss", {
|
||||
bodyshopId,
|
||||
action: "Fetched from DB and cached"
|
||||
});
|
||||
|
||||
return bodyshopData; // Return the full object
|
||||
} catch (error) {
|
||||
logger.log("get-bodyshop-from-redis", "ERROR", "redis", null, {
|
||||
bodyshopId,
|
||||
error: error.message
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update or invalidate bodyshop data in Redis
|
||||
* @param bodyshopId
|
||||
* @param values
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const updateOrInvalidateBodyshopFromRedis = async (bodyshopId, values = null) => {
|
||||
const key = getBodyshopCacheKey(bodyshopId);
|
||||
try {
|
||||
if (!values) {
|
||||
// Invalidate cache by deleting the key
|
||||
await pubClient.del(key);
|
||||
devDebugLogger("bodyshop-cache-invalidate", {
|
||||
bodyshopId,
|
||||
action: "Cache invalidated"
|
||||
});
|
||||
} else {
|
||||
// Update cache with the full provided values
|
||||
const jsonData = JSON.stringify(values);
|
||||
await pubClient.set(key, jsonData);
|
||||
await pubClient.expire(key, BODYSHOP_CACHE_TTL);
|
||||
devDebugLogger("bodyshop-cache-update", {
|
||||
bodyshopId,
|
||||
action: "Cache updated",
|
||||
values
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log("update-or-invalidate-bodyshop-from-redis", "ERROR", "api", "redis", {
|
||||
bodyshopId,
|
||||
values,
|
||||
error: error.message
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// NOTE: The following code was written for an abandoned branch and things have changes since the,
|
||||
// Leaving it here for demonstration purposes, commenting it out so it does not get used
|
||||
|
||||
// Store multiple session data in Redis
|
||||
// const setMultipleSessionData = async (socketId, keyValues) => {
|
||||
// try {
|
||||
// // keyValues is expected to be an object { key1: value1, key2: value2, ... }
|
||||
// const entries = Object.entries(keyValues).map(([key, value]) => [key, JSON.stringify(value)]);
|
||||
// await pubClient.hset(`socket:${socketId}`, ...entries.flat());
|
||||
// } catch (error) {
|
||||
// logger.log(`Error Setting Multiple Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||
// }
|
||||
// };
|
||||
|
||||
// Retrieve multiple session data from Redis
|
||||
// const getMultipleSessionData = async (socketId, keys) => {
|
||||
// try {
|
||||
// const data = await pubClient.hmget(`socket:${socketId}`, keys);
|
||||
// // Redis returns an object with null values for missing keys, so we parse the non-null ones
|
||||
// return Object.fromEntries(keys.map((key, index) => [key, data[index] ? JSON.parse(data[index]) : null]));
|
||||
// } catch (error) {
|
||||
// logger.log(`Error Getting Multiple Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||
// }
|
||||
// };
|
||||
|
||||
// const setMultipleFromArraySessionData = async (socketId, keyValueArray) => {
|
||||
// try {
|
||||
// // Use Redis multi/pipeline to batch the commands
|
||||
// const multi = pubClient.multi();
|
||||
// keyValueArray.forEach(([key, value]) => {
|
||||
// multi.hset(`socket:${socketId}`, key, JSON.stringify(value));
|
||||
// });
|
||||
// await multi.exec(); // Execute all queued commands
|
||||
// } catch (error) {
|
||||
// logger.log(`Error Setting Multiple Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||
// }
|
||||
// };
|
||||
|
||||
// Helper function to add an item to the end of the Redis list
|
||||
// const addItemToEndOfList = async (socketId, key, newItem) => {
|
||||
// try {
|
||||
// await pubClient.rpush(`socket:${socketId}:${key}`, JSON.stringify(newItem));
|
||||
// } catch (error) {
|
||||
// let userEmail = "unknown";
|
||||
// let socketMappings = {};
|
||||
// try {
|
||||
// const userData = await getSessionData(socketId, "user");
|
||||
// if (userData && userData.email) {
|
||||
// userEmail = userData.email;
|
||||
// socketMappings = await getUserSocketMapping(userEmail);
|
||||
// }
|
||||
// } catch (sessionError) {
|
||||
// logger.log(`Failed to fetch session data for socket ${socketId}: ${sessionError}`, "ERROR", "redis");
|
||||
// }
|
||||
// const mappingString = JSON.stringify(socketMappings, null, 2);
|
||||
// const errorMessage = `Error adding item to the end of the list for socket ${socketId}: ${error}. User: ${userEmail}, Socket Mappings: ${mappingString}`;
|
||||
// logger.log(errorMessage, "ERROR", "redis");
|
||||
// }
|
||||
// };
|
||||
|
||||
// Helper function to add an item to the beginning of the Redis list
|
||||
// const addItemToBeginningOfList = async (socketId, key, newItem) => {
|
||||
// try {
|
||||
// await pubClient.lpush(`socket:${socketId}:${key}`, JSON.stringify(newItem));
|
||||
// } catch (error) {
|
||||
// logger.log(`Error adding item to the beginning of the list for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||
// }
|
||||
// };
|
||||
|
||||
// Helper function to clear a list in Redis
|
||||
// const clearList = async (socketId, key) => {
|
||||
// try {
|
||||
// await pubClient.del(`socket:${socketId}:${key}`);
|
||||
// } catch (error) {
|
||||
// logger.log(`Error clearing list for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||
// }
|
||||
// };
|
||||
|
||||
// Add methods to manage room users
|
||||
// const addUserToRoom = async (room, user) => {
|
||||
// try {
|
||||
// await pubClient.sadd(room, JSON.stringify(user));
|
||||
// } catch (error) {
|
||||
// logger.log(`Error adding user to room ${room}: ${error}`, "ERROR", "redis");
|
||||
// }
|
||||
// };
|
||||
|
||||
// Remove users from room
|
||||
// const removeUserFromRoom = async (room, user) => {
|
||||
// try {
|
||||
// await pubClient.srem(room, JSON.stringify(user));
|
||||
// } catch (error) {
|
||||
// logger.log(`Error removing user to room ${room}: ${error}`, "ERROR", "redis");
|
||||
// }
|
||||
// };
|
||||
|
||||
// Get Users in room
|
||||
// const getUsersInRoom = async (room) => {
|
||||
// try {
|
||||
// const users = await pubClient.smembers(room);
|
||||
// return users.map((user) => JSON.parse(user));
|
||||
// } catch (error) {
|
||||
// logger.log(`Error getting users in room ${room}: ${error}`, "ERROR", "redis");
|
||||
// }
|
||||
// };
|
||||
|
||||
const api = {
|
||||
getUserSocketMappingKey,
|
||||
getBodyshopCacheKey,
|
||||
setSessionData,
|
||||
getSessionData,
|
||||
clearSessionData,
|
||||
setMultipleSessionData,
|
||||
getMultipleSessionData,
|
||||
setMultipleFromArraySessionData,
|
||||
addItemToEndOfList,
|
||||
addItemToBeginningOfList,
|
||||
clearList,
|
||||
addUserToRoom,
|
||||
removeUserFromRoom,
|
||||
getUsersInRoom,
|
||||
addUserSocketMapping,
|
||||
removeUserSocketMapping,
|
||||
getUserSocketMappingByBodyshop,
|
||||
getUserSocketMapping,
|
||||
refreshUserSocketTTL
|
||||
refreshUserSocketTTL,
|
||||
getBodyshopFromRedis,
|
||||
updateOrInvalidateBodyshopFromRedis
|
||||
// setMultipleSessionData,
|
||||
// getMultipleSessionData,
|
||||
// setMultipleFromArraySessionData,
|
||||
// addItemToEndOfList,
|
||||
// addItemToBeginningOfList,
|
||||
// clearList,
|
||||
// addUserToRoom,
|
||||
// removeUserFromRoom,
|
||||
// getUsersInRoom,
|
||||
};
|
||||
|
||||
Object.assign(module.exports, api);
|
||||
|
||||
@@ -2,7 +2,7 @@ const { admin } = require("../firebase/firebase-handler");
|
||||
|
||||
const redisSocketEvents = ({
|
||||
io,
|
||||
redisHelpers: { addUserSocketMapping, removeUserSocketMapping, refreshUserSocketTTL },
|
||||
redisHelpers: { addUserSocketMapping, removeUserSocketMapping, refreshUserSocketTTL, getUserSocketMappingByBodyshop },
|
||||
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom },
|
||||
logger
|
||||
}) => {
|
||||
@@ -14,12 +14,15 @@ const redisSocketEvents = ({
|
||||
// Socket Auth Middleware
|
||||
const authMiddleware = async (socket, next) => {
|
||||
const { token, bodyshopId } = socket.handshake.auth;
|
||||
|
||||
if (!token) {
|
||||
return next(new Error("Authentication error - no authorization token."));
|
||||
}
|
||||
|
||||
if (!bodyshopId) {
|
||||
return next(new Error("Authentication error - no bodyshopId provided."));
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await admin.auth().verifyIdToken(token);
|
||||
socket.user = user;
|
||||
@@ -182,11 +185,58 @@ const redisSocketEvents = ({
|
||||
socket.on("leave-bodyshop-conversation", leaveConversationRoom);
|
||||
};
|
||||
|
||||
// Sync Notification Read Events
|
||||
const registerSyncEvents = (socket) => {
|
||||
socket.on("sync-notification-read", async ({ email, bodyshopId, notificationId }) => {
|
||||
try {
|
||||
const userEmail = socket.user.email;
|
||||
const socketMapping = await getUserSocketMappingByBodyshop(email, bodyshopId);
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
if (socketMapping?.socketIds) {
|
||||
socketMapping?.socketIds.forEach((socketId) => {
|
||||
if (socketId !== socket.id) {
|
||||
// Avoid sending back to the originating socket
|
||||
io.to(socketId).emit("sync-notification-read", { notificationId, timestamp });
|
||||
}
|
||||
});
|
||||
createLogEvent(
|
||||
socket,
|
||||
"debug",
|
||||
`Synced notification ${notificationId} read for ${userEmail} in bodyshop ${bodyshopId}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
createLogEvent(socket, "error", `Error syncing notification read: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("sync-all-notifications-read", async ({ email, bodyshopId }) => {
|
||||
try {
|
||||
const socketMapping = await getUserSocketMappingByBodyshop(email, bodyshopId);
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
if (socketMapping?.socketIds) {
|
||||
socketMapping?.socketIds.forEach((socketId) => {
|
||||
if (socketId !== socket.id) {
|
||||
// Avoid sending back to the originating socket
|
||||
io.to(socketId).emit("sync-all-notifications-read", { timestamp });
|
||||
}
|
||||
});
|
||||
createLogEvent(socket, "debug", `Synced all notifications read for ${email} in bodyshop ${bodyshopId}`);
|
||||
}
|
||||
} catch (error) {
|
||||
createLogEvent(socket, "error", `Error syncing all notifications read: ${error.message}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Call Handlers
|
||||
registerRoomAndBroadcastEvents(socket);
|
||||
registerUpdateEvents(socket);
|
||||
registerMessagingEvents(socket);
|
||||
registerDisconnectEvents(socket);
|
||||
registerSyncEvents(socket);
|
||||
};
|
||||
|
||||
// Associate Middleware and Handlers
|
||||
|
||||
36
server/web-sockets/updateBodyshopCache.js
Normal file
36
server/web-sockets/updateBodyshopCache.js
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Update or invalidate bodyshop cache
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const updateBodyshopCache = async (req, res) => {
|
||||
const {
|
||||
sessionUtils: { updateOrInvalidateBodyshopFromRedis },
|
||||
logger
|
||||
} = req;
|
||||
|
||||
const { event } = req.body;
|
||||
const { new: newData } = event.data;
|
||||
|
||||
try {
|
||||
if (newData && newData.id) {
|
||||
// Update cache with the full new data object
|
||||
await updateOrInvalidateBodyshopFromRedis(newData.id, newData);
|
||||
logger.logger.debug("Bodyshop cache updated successfully.");
|
||||
} else {
|
||||
// Invalidate cache if no valid data provided
|
||||
await updateOrInvalidateBodyshopFromRedis(newData.id);
|
||||
logger.logger.debug("Bodyshop cache invalidated successfully.");
|
||||
}
|
||||
res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
logger.log("bodyshop-cache-update-error", "ERROR", "api", "redis", {
|
||||
message: error?.message,
|
||||
stack: error?.stack
|
||||
});
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = updateBodyshopCache;
|
||||
Reference in New Issue
Block a user