Compare commits
188 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 | ||
|
|
01b18a4a02 | ||
|
|
17c4e2fd0e | ||
|
|
eb51085055 | ||
|
|
abd530b8b2 | ||
|
|
e4d437018d | ||
|
|
0767e290f4 | ||
|
|
b86309e74b | ||
|
|
7e2bd128e8 | ||
|
|
7f547c90c2 | ||
|
|
fa39e2b97e | ||
|
|
c5d00f7641 | ||
|
|
08b7f0e59c | ||
|
|
f0af12bc2c | ||
|
|
ace9ec792d | ||
|
|
015f4cc5bd | ||
|
|
4f1c0b9996 | ||
|
|
b395839b37 | ||
|
|
0f067fc503 | ||
|
|
a5cf81bd28 | ||
|
|
e892e4cab1 | ||
|
|
ef4bb75ce7 | ||
|
|
459af4f537 | ||
|
|
f860931eab | ||
|
|
0bf9f932b7 | ||
|
|
a077cf0820 | ||
|
|
c1abe98b89 | ||
|
|
0f32e6ffc7 | ||
|
|
eca7ff4a42 | ||
|
|
7d6b95d344 | ||
|
|
9e44ee2a26 | ||
|
|
5d0500582e | ||
|
|
f53fcc345e | ||
|
|
1b7cb7c852 | ||
|
|
c82cfb3ec2 | ||
|
|
cc5fea9410 | ||
|
|
29f7144e72 | ||
|
|
1384616d66 | ||
|
|
366f7b9c4a | ||
|
|
67e904e121 | ||
|
|
83ea51157d | ||
|
|
9f207f0946 | ||
|
|
2a81517104 | ||
|
|
00005c881e | ||
|
|
c1ea8e8a3d | ||
|
|
adb15a4748 | ||
|
|
c214ed1dfb | ||
|
|
c02c36c548 | ||
|
|
a15f86cc4e | ||
|
|
8a88a241d6 | ||
|
|
df13f257db | ||
|
|
5cfadf7929 | ||
|
|
4a46870327 | ||
|
|
4684bada1e | ||
|
|
163354f4b4 | ||
|
|
3d225c9f92 | ||
|
|
f3b2edea1c | ||
|
|
01e103fd0e | ||
|
|
1fc21e49a0 | ||
|
|
19d608e2b0 | ||
|
|
4b184d1d42 | ||
|
|
3f75041ad9 | ||
|
|
8c541dad05 | ||
|
|
921cca86c1 | ||
|
|
841312ebcd | ||
|
|
5ed00eaffe | ||
|
|
994ea8bb20 | ||
|
|
580641bae6 | ||
|
|
024b4fe21b | ||
|
|
40aca91c76 | ||
|
|
72305f91d8 | ||
|
|
abe4f4fb3d | ||
|
|
142617bc3d | ||
|
|
2ee582bfa2 | ||
|
|
35a3726cf0 | ||
|
|
54820fe3c8 | ||
|
|
b1ffbe0e12 | ||
|
|
ba2d03176f | ||
|
|
95a592fb9a | ||
|
|
6d343e9b7f | ||
|
|
c27b1d802f | ||
|
|
f11d9dd804 | ||
|
|
996f5b3c71 | ||
|
|
9bb7f647a7 | ||
|
|
760f2ac7f9 | ||
|
|
872e36a61a | ||
|
|
779f608506 | ||
|
|
14e362ec3f | ||
|
|
c213e13624 | ||
|
|
dae7642a8c | ||
|
|
c751f0cba4 | ||
|
|
e128c108f8 |
@@ -15,7 +15,7 @@ jobs:
|
|||||||
- eb/setup
|
- eb/setup
|
||||||
- run:
|
- run:
|
||||||
command: |
|
command: |
|
||||||
eb init imex-online-production-api -r ca-central-1 -p "Node.js 22 running on 64bit Amazon Linux 2"
|
eb init imex-online-production-api -r ca-central-1 -p "Node.js 22 running on 64bit Amazon Linux 2023"
|
||||||
eb status --verbose
|
eb status --verbose
|
||||||
eb deploy
|
eb deploy
|
||||||
eb status
|
eb status
|
||||||
@@ -88,7 +88,7 @@ jobs:
|
|||||||
name: Install Dependencies
|
name: Install Dependencies
|
||||||
command: npm i
|
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-cli/setup:
|
||||||
aws_access_key_id: AWS_ACCESS_KEY_ID
|
aws_access_key_id: AWS_ACCESS_KEY_ID
|
||||||
@@ -114,7 +114,7 @@ jobs:
|
|||||||
- eb/setup
|
- eb/setup
|
||||||
- run:
|
- run:
|
||||||
command: |
|
command: |
|
||||||
eb init romeonline-productionapi -r us-east-2 -p "Node.js 22 on 64bit Amazon Linux 2"
|
eb init romeonline-productionapi -r us-east-2 -p "Node.js 22 running on 64bit Amazon Linux 2023"
|
||||||
eb status --verbose
|
eb status --verbose
|
||||||
eb deploy
|
eb deploy
|
||||||
eb status
|
eb status
|
||||||
@@ -151,7 +151,7 @@ jobs:
|
|||||||
rome-app-build:
|
rome-app-build:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:22.13.1
|
- image: cimg/node:22.13.1
|
||||||
|
resource_class: large
|
||||||
working_directory: ~/repo/client
|
working_directory: ~/repo/client
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -161,7 +161,7 @@ jobs:
|
|||||||
name: Install Dependencies
|
name: Install Dependencies
|
||||||
command: npm i
|
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-cli/setup:
|
||||||
aws_access_key_id: AWS_ACCESS_KEY_ID
|
aws_access_key_id: AWS_ACCESS_KEY_ID
|
||||||
@@ -209,7 +209,7 @@ jobs:
|
|||||||
test-rome-app-build:
|
test-rome-app-build:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:22.13.1
|
- image: cimg/node:22.13.1
|
||||||
|
resource_class: large
|
||||||
working_directory: ~/repo/client
|
working_directory: ~/repo/client
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -219,7 +219,7 @@ jobs:
|
|||||||
name: Install Dependencies
|
name: Install Dependencies
|
||||||
command: npm i
|
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-cli/setup:
|
||||||
aws_access_key_id: AWS_ACCESS_KEY_ID
|
aws_access_key_id: AWS_ACCESS_KEY_ID
|
||||||
@@ -277,7 +277,7 @@ jobs:
|
|||||||
name: Install Dependencies
|
name: Install Dependencies
|
||||||
command: npm i
|
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:
|
- aws-s3/sync:
|
||||||
from: build
|
from: build
|
||||||
@@ -298,7 +298,7 @@ jobs:
|
|||||||
name: Install Dependencies
|
name: Install Dependencies
|
||||||
command: npm i
|
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-cli/setup:
|
||||||
aws_access_key_id: AWS_ACCESS_KEY_ID
|
aws_access_key_id: AWS_ACCESS_KEY_ID
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<babeledit_project version="1.2" be_version="2.7.1">
|
<babeledit_project be_version="2.7.1" version="1.2">
|
||||||
<!--
|
<!--
|
||||||
|
|
||||||
BabelEdit project file
|
BabelEdit project file
|
||||||
@@ -6453,6 +6453,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>mark_critical</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>operation</name>
|
<name>operation</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -6474,6 +6495,48 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>update_field</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>update_value</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>value</name>
|
<name>value</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -11943,6 +12006,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>shop_enabled_features</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>shopinfo</name>
|
<name>shopinfo</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -12312,6 +12396,37 @@
|
|||||||
</concept_node>
|
</concept_node>
|
||||||
</children>
|
</children>
|
||||||
</folder_node>
|
</folder_node>
|
||||||
|
<folder_node>
|
||||||
|
<name>tooltips</name>
|
||||||
|
<children>
|
||||||
|
<folder_node>
|
||||||
|
<name>md_parts_scan</name>
|
||||||
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>update_value_tooltip</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
<folder_node>
|
<folder_node>
|
||||||
<name>validation</name>
|
<name>validation</name>
|
||||||
<children>
|
<children>
|
||||||
@@ -19091,6 +19206,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>ok</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>previous</name>
|
<name>previous</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -19385,6 +19521,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>sharetoteams</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>submit</name>
|
<name>submit</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -43090,6 +43247,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>parts_returns</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>print</name>
|
<name>print</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -48557,6 +48735,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>unassigned</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>vertical</name>
|
<name>vertical</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -52732,6 +52931,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>purchases_by_date_excel</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>purchases_by_date_range_detail</name>
|
<name>purchases_by_date_range_detail</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -54483,6 +54703,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>view</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
</children>
|
</children>
|
||||||
</folder_node>
|
</folder_node>
|
||||||
<folder_node>
|
<folder_node>
|
||||||
|
|||||||
1062
client/package-lock.json
generated
1062
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,26 +8,27 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"proxy": "http://localhost:4000",
|
"proxy": "http://localhost:4000",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/pro-layout": "^7.22.0",
|
"@ant-design/pro-layout": "^7.22.3",
|
||||||
"@apollo/client": "^3.12.6",
|
"@apollo/client": "^3.13.1",
|
||||||
"@emotion/is-prop-valid": "^1.3.1",
|
"@emotion/is-prop-valid": "^1.3.1",
|
||||||
"@fingerprintjs/fingerprintjs": "^4.5.1",
|
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
"@reduxjs/toolkit": "^2.5.0",
|
"@reduxjs/toolkit": "^2.6.0",
|
||||||
"@sentry/cli": "^2.40.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",
|
"@splitsoftware/splitio-react": "^1.13.0",
|
||||||
"@tanem/react-nprogress": "^5.0.53",
|
"@tanem/react-nprogress": "^5.0.53",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"antd": "^5.23.1",
|
"antd": "^5.24.2",
|
||||||
"apollo-link-logger": "^2.0.1",
|
"apollo-link-logger": "^2.0.1",
|
||||||
"apollo-link-sentry": "^3.3.0",
|
"apollo-link-sentry": "^4.1.0",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.8.1",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"css-box-model": "^1.2.1",
|
"css-box-model": "^1.2.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"dayjs-business-days2": "^1.2.3",
|
"dayjs-business-days2": "^1.3.0",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
@@ -35,9 +36,9 @@
|
|||||||
"firebase": "^10.13.2",
|
"firebase": "^10.13.2",
|
||||||
"graphql": "^16.10.0",
|
"graphql": "^16.10.0",
|
||||||
"i18next": "^23.15.1",
|
"i18next": "^23.15.1",
|
||||||
"i18next-browser-languagedetector": "^8.0.2",
|
"i18next-browser-languagedetector": "^8.0.4",
|
||||||
"immutability-helper": "^3.1.1",
|
"immutability-helper": "^3.1.1",
|
||||||
"libphonenumber-js": "^1.11.18",
|
"libphonenumber-js": "^1.12.4",
|
||||||
"logrocket": "^8.1.2",
|
"logrocket": "^8.1.2",
|
||||||
"markerjs2": "^2.32.3",
|
"markerjs2": "^2.32.3",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
@@ -47,7 +48,7 @@
|
|||||||
"query-string": "^9.1.1",
|
"query-string": "^9.1.1",
|
||||||
"raf-schd": "^4.0.3",
|
"raf-schd": "^4.0.3",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-big-calendar": "^1.17.1",
|
"react-big-calendar": "^1.18.0",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-cookie": "^7.2.2",
|
"react-cookie": "^7.2.2",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
@@ -55,7 +56,7 @@
|
|||||||
"react-grid-gallery": "^1.0.1",
|
"react-grid-gallery": "^1.0.1",
|
||||||
"react-grid-layout": "1.3.4",
|
"react-grid-layout": "1.3.4",
|
||||||
"react-i18next": "^14.1.3",
|
"react-i18next": "^14.1.3",
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-image-lightbox": "^5.1.4",
|
"react-image-lightbox": "^5.1.4",
|
||||||
"react-markdown": "^9.0.3",
|
"react-markdown": "^9.0.3",
|
||||||
"react-number-format": "^5.4.3",
|
"react-number-format": "^5.4.3",
|
||||||
@@ -63,9 +64,9 @@
|
|||||||
"react-product-fruits": "^2.2.61",
|
"react-product-fruits": "^2.2.61",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"react-resizable": "^3.0.5",
|
"react-resizable": "^3.0.5",
|
||||||
"react-router-dom": "^6.26.2",
|
"react-router-dom": "^6.30.0",
|
||||||
"react-sticky": "^6.0.3",
|
"react-sticky": "^6.0.3",
|
||||||
"react-virtuoso": "^4.10.4",
|
"react-virtuoso": "^4.12.5",
|
||||||
"recharts": "^2.15.0",
|
"recharts": "^2.15.0",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"redux-actions": "^3.0.3",
|
"redux-actions": "^3.0.3",
|
||||||
@@ -73,12 +74,12 @@
|
|||||||
"redux-saga": "^1.3.0",
|
"redux-saga": "^1.3.0",
|
||||||
"redux-state-sync": "^3.1.4",
|
"redux-state-sync": "^3.1.4",
|
||||||
"reselect": "^5.1.1",
|
"reselect": "^5.1.1",
|
||||||
"sass": "^1.83.4",
|
"sass": "^1.85.1",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"styled-components": "^6.1.14",
|
"styled-components": "^6.1.15",
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
"subscriptions-transport-ws": "^0.11.0",
|
||||||
"use-memo-one": "^1.1.3",
|
"use-memo-one": "^1.1.3",
|
||||||
"userpilot": "^1.3.6",
|
"userpilot": "^1.3.8",
|
||||||
"vite-plugin-ejs": "^1.7.0",
|
"vite-plugin-ejs": "^1.7.0",
|
||||||
"web-vitals": "^3.5.2"
|
"web-vitals": "^3.5.2"
|
||||||
},
|
},
|
||||||
@@ -98,8 +99,7 @@
|
|||||||
"test": "cypress open",
|
"test": "cypress open",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .",
|
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .",
|
||||||
"eulaize": "node src/utils/eulaize.js",
|
"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"
|
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
@@ -120,14 +120,14 @@
|
|||||||
"@rollup/rollup-linux-x64-gnu": "4.6.1"
|
"@rollup/rollup-linux-x64-gnu": "4.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/icons": "^5.5.2",
|
"@ant-design/icons": "^5.6.1",
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-react": "^7.26.3",
|
"@babel/preset-react": "^7.26.3",
|
||||||
"@dotenvx/dotenvx": "^1.33.0",
|
"@dotenvx/dotenvx": "^1.38.3",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@eslint/js": "^9.18.0",
|
"@eslint/js": "^9.21.0",
|
||||||
"@sentry/webpack-plugin": "^2.22.4",
|
"@sentry/webpack-plugin": "^3.2.2",
|
||||||
"@testing-library/cypress": "^10.0.2",
|
"@testing-library/cypress": "^10.0.2",
|
||||||
"browserslist": "^4.24.4",
|
"browserslist": "^4.24.4",
|
||||||
"browserslist-to-esbuild": "^2.1.1",
|
"browserslist-to-esbuild": "^2.1.1",
|
||||||
@@ -138,13 +138,13 @@
|
|||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"eslint-plugin-cypress": "^2.15.1",
|
"eslint-plugin-cypress": "^2.15.1",
|
||||||
"eslint-plugin-react": "^7.37.4",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"globals": "^15.14.0",
|
"globals": "^15.15.0",
|
||||||
"memfs": "^4.17.0",
|
"memfs": "^4.17.0",
|
||||||
"os-browserify": "^0.3.0",
|
"os-browserify": "^0.3.0",
|
||||||
"react-error-overlay": "6.0.11",
|
"react-error-overlay": "^6.1.0",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"source-map-explorer": "^2.5.3",
|
"source-map-explorer": "^2.5.3",
|
||||||
"vite": "^6.0.7",
|
"vite": "^6.2.0",
|
||||||
"vite-plugin-babel": "^1.3.0",
|
"vite-plugin-babel": "^1.3.0",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-node-polyfills": "^0.23.0",
|
"vite-plugin-node-polyfills": "^0.23.0",
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useSplitClient } from "@splitsoftware/splitio-react";
|
import { useSplitClient } from "@splitsoftware/splitio-react";
|
||||||
import { Button, Result } from "antd";
|
import { Button, Result } from "antd";
|
||||||
import LogRocket from "logrocket";
|
import LogRocket from "logrocket";
|
||||||
import React, { lazy, Suspense, useEffect, useState } from "react";
|
import { lazy, Suspense, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Route, Routes } from "react-router-dom";
|
import { Route, Routes, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import DocumentEditorContainer from "../components/document-editor/document-editor.container";
|
import DocumentEditorContainer from "../components/document-editor/document-editor.container";
|
||||||
import ErrorBoundary from "../components/error-boundary/error-boundary.component"; // Component Imports
|
import ErrorBoundary from "../components/error-boundary/error-boundary.component"; // Component Imports
|
||||||
@@ -21,7 +21,7 @@ import "./App.styles.scss";
|
|||||||
import Eula from "../components/eula/eula.component";
|
import Eula from "../components/eula/eula.component";
|
||||||
import InstanceRenderMgr from "../utils/instanceRenderMgr";
|
import InstanceRenderMgr from "../utils/instanceRenderMgr";
|
||||||
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
|
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
|
||||||
import { SocketProvider } from "../contexts/SocketIO/socketContext.jsx";
|
import { SocketProvider } from "../contexts/SocketIO/useSocket.jsx";
|
||||||
import { NotificationProvider } from "../contexts/Notifications/notificationContext.jsx";
|
import { NotificationProvider } from "../contexts/Notifications/notificationContext.jsx";
|
||||||
|
|
||||||
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
|
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
|
||||||
@@ -46,6 +46,7 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
|||||||
const client = useSplitClient().client;
|
const client = useSplitClient().client;
|
||||||
const [listenersAdded, setListenersAdded] = useState(false);
|
const [listenersAdded, setListenersAdded] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!navigator.onLine) {
|
if (!navigator.onLine) {
|
||||||
@@ -200,7 +201,7 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
|||||||
path="/manage/*"
|
path="/manage/*"
|
||||||
element={
|
element={
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<SocketProvider bodyshop={bodyshop}>
|
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}>
|
||||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||||
</SocketProvider>
|
</SocketProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
@@ -212,7 +213,7 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
|||||||
path="/tech/*"
|
path="/tech/*"
|
||||||
element={
|
element={
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<SocketProvider bodyshop={bodyshop}>
|
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}>
|
||||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||||
</SocketProvider>
|
</SocketProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|||||||
@@ -180,3 +180,13 @@
|
|||||||
.muted-button:hover {
|
.muted-button:hover {
|
||||||
color: darkgrey;
|
color: darkgrey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-alert-unordered-list {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
.notification-alert-unordered-list-item {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useApolloClient } from "@apollo/client";
|
import { useApolloClient } from "@apollo/client";
|
||||||
import { getToken } from "@firebase/messaging";
|
import { getToken } from "@firebase/messaging";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import React, { useContext, useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import SocketContext from "../../contexts/SocketIO/socketContext";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||||
import { messaging, requestForToken } from "../../firebase/firebase.utils";
|
import { messaging, requestForToken } from "../../firebase/firebase.utils";
|
||||||
import ChatPopupComponent from "../chat-popup/chat-popup.component";
|
import ChatPopupComponent from "../chat-popup/chat-popup.component";
|
||||||
import "./chat-affix.styles.scss";
|
import "./chat-affix.styles.scss";
|
||||||
@@ -12,7 +12,7 @@ import { registerMessagingHandlers, unregisterMessagingHandlers } from "./regist
|
|||||||
export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
const { socket } = useContext(SocketContext);
|
const { socket } = useSocket();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!bodyshop || !bodyshop.messagingservicesid) return;
|
if (!bodyshop || !bodyshop.messagingservicesid) return;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Button } from "antd";
|
import { Button } from "antd";
|
||||||
import React, { useContext, useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { TOGGLE_CONVERSATION_ARCHIVE } from "../../graphql/conversations.queries";
|
import { TOGGLE_CONVERSATION_ARCHIVE } from "../../graphql/conversations.queries";
|
||||||
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -18,7 +18,7 @@ export function ChatArchiveButton({ conversation, bodyshop }) {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [updateConversation] = useMutation(TOGGLE_CONVERSATION_ARCHIVE);
|
const [updateConversation] = useMutation(TOGGLE_CONVERSATION_ARCHIVE);
|
||||||
const { socket } = useContext(SocketContext);
|
const { socket } = useSocket();
|
||||||
|
|
||||||
const handleToggleArchive = async () => {
|
const handleToggleArchive = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Tag } from "antd";
|
import { Tag } from "antd";
|
||||||
import React, { useContext } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -18,7 +17,7 @@ const mapDispatchToProps = () => ({});
|
|||||||
|
|
||||||
export function ChatConversationTitleTags({ jobConversations, bodyshop }) {
|
export function ChatConversationTitleTags({ jobConversations, bodyshop }) {
|
||||||
const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG);
|
const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG);
|
||||||
const { socket } = useContext(SocketContext);
|
const { socket } = useSocket();
|
||||||
|
|
||||||
const handleRemoveTag = async (jobId) => {
|
const handleRemoveTag = async (jobId) => {
|
||||||
const convId = jobConversations[0].conversationid;
|
const convId = jobConversations[0].conversationid;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { gql, useApolloClient, useQuery, useSubscription } from "@apollo/client";
|
import { gql, useApolloClient, useQuery, useSubscription } from "@apollo/client";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import React, { useCallback, useContext, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import SocketContext from "../../contexts/SocketIO/socketContext";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||||
import { GET_CONVERSATION_DETAILS, CONVERSATION_SUBSCRIPTION_BY_PK } from "../../graphql/conversations.queries";
|
import { CONVERSATION_SUBSCRIPTION_BY_PK, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries";
|
||||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import ChatConversationComponent from "./chat-conversation.component";
|
import ChatConversationComponent from "./chat-conversation.component";
|
||||||
@@ -16,7 +16,7 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
|
|
||||||
function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
const { socket } = useContext(SocketContext);
|
const { socket } = useSocket();
|
||||||
const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false);
|
const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false);
|
||||||
|
|
||||||
// Fetch conversation details
|
// Fetch conversation details
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { PlusOutlined } from "@ant-design/icons";
|
import { PlusOutlined } from "@ant-design/icons";
|
||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Input, Spin, Tag, Tooltip } from "antd";
|
import { Input, Spin, Tag, Tooltip } from "antd";
|
||||||
import React, { useContext, useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { UPDATE_CONVERSATION_LABEL } from "../../graphql/conversations.queries";
|
import { UPDATE_CONVERSATION_LABEL } from "../../graphql/conversations.queries";
|
||||||
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -20,7 +20,7 @@ export function ChatLabel({ conversation, bodyshop }) {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [value, setValue] = useState(conversation.label);
|
const [value, setValue] = useState(conversation.label);
|
||||||
const { socket } = useContext(SocketContext);
|
const { socket } = useSocket();
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { PlusCircleFilled } from "@ant-design/icons";
|
import { PlusCircleFilled } from "@ant-design/icons";
|
||||||
import { Button, Form, Popover } from "antd";
|
import { Button, Form, Popover } from "antd";
|
||||||
import React, { useContext } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
||||||
import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
|
import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
|
||||||
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
@@ -18,7 +17,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
export function ChatNewConversation({ openChatByPhone }) {
|
export function ChatNewConversation({ openChatByPhone }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const { socket } = useContext(SocketContext);
|
const { socket } = useSocket();
|
||||||
|
|
||||||
const handleFinish = (values) => {
|
const handleFinish = (values) => {
|
||||||
openChatByPhone({ phone_num: values.phoneNumber, socket });
|
openChatByPhone({ phone_num: values.phoneNumber, socket });
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import parsePhoneNumber from "libphonenumber-js";
|
import parsePhoneNumber from "libphonenumber-js";
|
||||||
import React, { useContext } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
||||||
@@ -8,7 +7,7 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
|||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { searchingForConversation } from "../../redux/messaging/messaging.selectors";
|
import { searchingForConversation } from "../../redux/messaging/messaging.selectors";
|
||||||
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -22,7 +21,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
|
|
||||||
export function ChatOpenButton({ bodyshop, searchingForConversation, phone, jobid, openChatByPhone }) {
|
export function ChatOpenButton({ bodyshop, searchingForConversation, phone, jobid, openChatByPhone }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { socket } = useContext(SocketContext);
|
const { socket } = useSocket();
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|
||||||
if (!phone) return <></>;
|
if (!phone) return <></>;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { InfoCircleOutlined, MessageOutlined, ShrinkOutlined, SyncOutlined } from "@ant-design/icons";
|
import { InfoCircleOutlined, MessageOutlined, ShrinkOutlined, SyncOutlined } from "@ant-design/icons";
|
||||||
import { useApolloClient, useLazyQuery, useQuery } from "@apollo/client";
|
import { useApolloClient, useLazyQuery, useQuery } from "@apollo/client";
|
||||||
import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd";
|
import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd";
|
||||||
import React, { useContext, useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -12,8 +12,9 @@ import ChatConversationListComponent from "../chat-conversation-list/chat-conver
|
|||||||
import ChatConversationContainer from "../chat-conversation/chat-conversation.container";
|
import ChatConversationContainer from "../chat-conversation/chat-conversation.container";
|
||||||
import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component";
|
import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
|
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||||
|
|
||||||
import "./chat-popup.styles.scss";
|
import "./chat-popup.styles.scss";
|
||||||
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
selectedConversation: selectSelectedConversation,
|
selectedConversation: selectSelectedConversation,
|
||||||
@@ -27,7 +28,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
export function ChatPopupComponent({ chatVisible, selectedConversation, toggleChatVisible }) {
|
export function ChatPopupComponent({ chatVisible, selectedConversation, toggleChatVisible }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [pollInterval, setPollInterval] = useState(0);
|
const [pollInterval, setPollInterval] = useState(0);
|
||||||
const { socket } = useContext(SocketContext);
|
const { socket } = useSocket();
|
||||||
const client = useApolloClient(); // Apollo Client instance for cache operations
|
const client = useApolloClient(); // Apollo Client instance for cache operations
|
||||||
|
|
||||||
// Lazy query for conversations
|
// Lazy query for conversations
|
||||||
@@ -42,8 +43,7 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh
|
|||||||
const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, {
|
const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
skip: chatVisible, // Skip when chat is visible
|
pollInterval: 60 * 1000 // TODO: This is a fix for now, should be coming from sockets
|
||||||
...(pollInterval > 0 ? { pollInterval } : {})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Socket connection status
|
// Socket connection status
|
||||||
@@ -85,29 +85,25 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh
|
|||||||
|
|
||||||
// Get unread count from the cache
|
// Get unread count from the cache
|
||||||
const unreadCount = (() => {
|
const unreadCount = (() => {
|
||||||
if (chatVisible) {
|
try {
|
||||||
try {
|
const cachedData = client.readQuery({
|
||||||
const cachedData = client.readQuery({
|
query: CONVERSATION_LIST_QUERY,
|
||||||
query: CONVERSATION_LIST_QUERY,
|
variables: { offset: 0 }
|
||||||
variables: { offset: 0 }
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (!cachedData?.conversations) return 0;
|
if (!cachedData?.conversations) {
|
||||||
|
return unreadData?.messages_aggregate?.aggregate?.count;
|
||||||
// Aggregate unread message count
|
|
||||||
return cachedData.conversations.reduce((total, conversation) => {
|
|
||||||
const unread = conversation.messages_aggregate?.aggregate?.count || 0;
|
|
||||||
return total + unread;
|
|
||||||
}, 0);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("Unread count not found in cache:", error);
|
|
||||||
return 0; // Fallback if not in cache
|
|
||||||
}
|
}
|
||||||
} else if (unreadData?.messages_aggregate?.aggregate?.count) {
|
|
||||||
// Use the unread count from the query result
|
// Aggregate unread message count
|
||||||
return unreadData.messages_aggregate.aggregate.count;
|
return cachedData.conversations.reduce((total, conversation) => {
|
||||||
|
const unread = conversation.messages_aggregate?.aggregate?.count || 0;
|
||||||
|
return total + unread;
|
||||||
|
}, 0);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Unread count not found in cache:", error);
|
||||||
|
return 0; // Fallback if not in cache
|
||||||
}
|
}
|
||||||
return 0;
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { PlusOutlined } from "@ant-design/icons";
|
|||||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||||
import { Tag } from "antd";
|
import { Tag } from "antd";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React, { useContext, useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||||
import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries";
|
import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries";
|
||||||
import ChatTagRo from "./chat-tag-ro.component";
|
import ChatTagRo from "./chat-tag-ro.component";
|
||||||
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -22,7 +22,7 @@ const mapDispatchToProps = () => ({});
|
|||||||
export function ChatTagRoContainer({ conversation, bodyshop }) {
|
export function ChatTagRoContainer({ conversation, bodyshop }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const { socket } = useContext(SocketContext);
|
const { socket } = useSocket();
|
||||||
|
|
||||||
const [loadRo, { loading, data }] = useLazyQuery(SEARCH_FOR_JOBS);
|
const [loadRo, { loading, data }] = useLazyQuery(SEARCH_FOR_JOBS);
|
||||||
|
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ class ErrorBoundary extends React.Component {
|
|||||||
<Row>
|
<Row>
|
||||||
<Col offset={6} span={12}>
|
<Col offset={6} span={12}>
|
||||||
<Collapse bordered={false}>
|
<Collapse bordered={false}>
|
||||||
<Collapse.Panel header={t("general.labels.errors")}>
|
<Collapse.Panel key="errors-panel" header={t("general.labels.errors")}>
|
||||||
<div>
|
<div>
|
||||||
<strong>{this.state.error.message}</strong>
|
<strong>{this.state.error.message}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -78,9 +78,7 @@ const Eula = ({ currentEula, currentUser, acceptEula }) => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
notification.error({
|
notification.error({
|
||||||
message: t("eula.errors.acceptance.message"),
|
message: t("eula.errors.acceptance.message"),
|
||||||
description: t("eula.errors.acceptance.description"),
|
description: t("eula.errors.acceptance.description")
|
||||||
placement: "bottomRight",
|
|
||||||
duration: 5000
|
|
||||||
});
|
});
|
||||||
console.log(`${t("eula.errors.acceptance.message")}`);
|
console.log(`${t("eula.errors.acceptance.message")}`);
|
||||||
console.dir({
|
console.dir({
|
||||||
|
|||||||
@@ -1,6 +1,19 @@
|
|||||||
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,
|
BankFilled,
|
||||||
BarChartOutlined,
|
BarChartOutlined,
|
||||||
|
BellFilled,
|
||||||
CarFilled,
|
CarFilled,
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
ClockCircleFilled,
|
ClockCircleFilled,
|
||||||
@@ -25,26 +38,21 @@ import Icon, {
|
|||||||
UnorderedListOutlined,
|
UnorderedListOutlined,
|
||||||
UserOutlined
|
UserOutlined
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
|
||||||
import { Layout, Menu, Space } from "antd";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { BsKanban } from "react-icons/bs";
|
import { BsKanban } from "react-icons/bs";
|
||||||
import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar, FaTasks } from "react-icons/fa";
|
import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar, FaTasks } from "react-icons/fa";
|
||||||
import { FiLogOut } from "react-icons/fi";
|
import { FiLogOut } from "react-icons/fi";
|
||||||
import { GiPayMoney, GiPlayerTime, GiSettingsKnobs } from "react-icons/gi";
|
import { GiPayMoney, GiPlayerTime, GiSettingsKnobs } from "react-icons/gi";
|
||||||
import { IoBusinessOutline } from "react-icons/io5";
|
import { IoBusinessOutline } from "react-icons/io5";
|
||||||
import { RiSurveyLine } from "react-icons/ri";
|
import { RiSurveyLine } from "react-icons/ri";
|
||||||
import { connect } from "react-redux";
|
import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js";
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors";
|
import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { signOutStart } from "../../redux/user/user.actions";
|
import { signOutStart } from "../../redux/user/user.actions";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
import day from "../../utils/day.js";
|
||||||
import LockWrapper from "../lock-wrapper/lock-wrapper.component";
|
|
||||||
|
|
||||||
|
// Redux mappings
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
recentItems: selectRecentItems,
|
recentItems: selectRecentItems,
|
||||||
@@ -53,43 +61,13 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setBillEnterContext: (context) =>
|
setBillEnterContext: (context) => dispatch(setModalContext({ context, modal: "billEnter" })),
|
||||||
dispatch(
|
setTimeTicketContext: (context) => dispatch(setModalContext({ context, modal: "timeTicket" })),
|
||||||
setModalContext({
|
setPaymentContext: (context) => dispatch(setModalContext({ context, modal: "payment" })),
|
||||||
context: context,
|
setReportCenterContext: (context) => dispatch(setModalContext({ context, modal: "reportCenter" })),
|
||||||
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"
|
|
||||||
})
|
|
||||||
),
|
|
||||||
signOutStart: () => dispatch(signOutStart()),
|
signOutStart: () => dispatch(signOutStart()),
|
||||||
setCardPaymentContext: (context) =>
|
setCardPaymentContext: (context) => dispatch(setModalContext({ context, modal: "cardPayment" })),
|
||||||
dispatch(
|
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
||||||
setModalContext({
|
|
||||||
context: context,
|
|
||||||
modal: "cardPayment"
|
|
||||||
})
|
|
||||||
),
|
|
||||||
setTaskUpsertContext: (context) =>
|
|
||||||
dispatch(
|
|
||||||
setModalContext({
|
|
||||||
context: context,
|
|
||||||
modal: "taskUpsert"
|
|
||||||
})
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function Header({
|
function Header({
|
||||||
@@ -115,24 +93,81 @@ function Header({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { isConnected, scenarioNotificationsOn } = useSocket();
|
||||||
|
const [notificationVisible, setNotificationVisible] = useState(false);
|
||||||
|
const baseTitleRef = useRef(document.title || "");
|
||||||
|
const lastSetTitleRef = useRef("");
|
||||||
|
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
||||||
|
|
||||||
// const deleteBetaCookie = () => {
|
const {
|
||||||
// const cookieExists = document.cookie.split("; ").some((row) => row.startsWith(`betaSwitchImex=`));
|
data: unreadData,
|
||||||
// if (cookieExists) {
|
refetch: refetchUnread,
|
||||||
// const domain = window.location.hostname.split(".").slice(-2).join(".");
|
loading: unreadLoading
|
||||||
// document.cookie = `betaSwitchImex=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${domain}`;
|
} = useQuery(GET_UNREAD_COUNT, {
|
||||||
// }
|
variables: { associationid: userAssociationId },
|
||||||
// };
|
fetchPolicy: "network-only",
|
||||||
//
|
pollInterval: isConnected ? 0 : day.duration(60, "seconds").asMilliseconds(),
|
||||||
// deleteBetaCookie();
|
skip: !userAssociationId || !scenarioNotificationsOn
|
||||||
|
});
|
||||||
|
|
||||||
const accountingChildren = [];
|
const unreadCount = unreadData?.notifications_aggregate?.aggregate?.count ?? 0;
|
||||||
|
|
||||||
accountingChildren.push(
|
useEffect(() => {
|
||||||
|
if (userAssociationId) {
|
||||||
|
refetchUnread().catch((e) => console.error(`Error fetching unread notifications: ${e?.message}`));
|
||||||
|
}
|
||||||
|
}, [refetchUnread, userAssociationId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isConnected && !unreadLoading && userAssociationId) {
|
||||||
|
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 accountingChildren = [
|
||||||
{
|
{
|
||||||
key: "bills",
|
key: "bills",
|
||||||
id: "header-accounting-bills",
|
id: "header-accounting-bills",
|
||||||
icon: <Icon component={FaFileInvoiceDollar} />,
|
icon: <FaFileInvoiceDollar />,
|
||||||
label: (
|
label: (
|
||||||
<Link to="/manage/bills">
|
<Link to="/manage/bills">
|
||||||
<LockWrapper featureName="bills" bodyshop={bodyshop}>
|
<LockWrapper featureName="bills" bodyshop={bodyshop}>
|
||||||
@@ -144,42 +179,31 @@ function Header({
|
|||||||
{
|
{
|
||||||
key: "enterbills",
|
key: "enterbills",
|
||||||
id: "header-accounting-enterbills",
|
id: "header-accounting-enterbills",
|
||||||
icon: <Icon component={GiPayMoney} />,
|
icon: <GiPayMoney />,
|
||||||
label: (
|
label: (
|
||||||
<Space>
|
<LockWrapper featureName="bills" bodyshop={bodyshop}>
|
||||||
<LockWrapper featureName="bills" bodyshop={bodyshop}>
|
{t("menus.header.enterbills")}
|
||||||
{t("menus.header.enterbills")}
|
</LockWrapper>
|
||||||
</LockWrapper>
|
|
||||||
</Space>
|
|
||||||
),
|
),
|
||||||
onClick: () => {
|
onClick: () =>
|
||||||
HasFeatureAccess({ featureName: "bills", bodyshop }) &&
|
HasFeatureAccess({ featureName: "bills", bodyshop }) &&
|
||||||
setBillEnterContext({
|
setBillEnterContext({
|
||||||
actions: {},
|
actions: {},
|
||||||
context: {}
|
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"
|
|
||||||
},
|
},
|
||||||
|
...(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",
|
key: "allpayments",
|
||||||
id: "header-accounting-allpayments",
|
id: "header-accounting-allpayments",
|
||||||
@@ -195,41 +219,31 @@ function Header({
|
|||||||
{
|
{
|
||||||
key: "enterpayments",
|
key: "enterpayments",
|
||||||
id: "header-accounting-enterpayments",
|
id: "header-accounting-enterpayments",
|
||||||
icon: <Icon component={FaCreditCard} />,
|
icon: <FaCreditCard />,
|
||||||
label: (
|
label: (
|
||||||
<LockWrapper featureName="payments" bodyshop={bodyshop}>
|
<LockWrapper featureName="payments" bodyshop={bodyshop}>
|
||||||
{t("menus.header.enterpayment")}
|
{t("menus.header.enterpayment")}
|
||||||
</LockWrapper>
|
</LockWrapper>
|
||||||
),
|
),
|
||||||
onClick: () => {
|
onClick: () =>
|
||||||
HasFeatureAccess({ featureName: "payments", bodyshop }) &&
|
HasFeatureAccess({ featureName: "payments", bodyshop }) &&
|
||||||
setPaymentContext({
|
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({
|
|
||||||
actions: {},
|
actions: {},
|
||||||
context: {}
|
context: null
|
||||||
});
|
})
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
accountingChildren.push(
|
|
||||||
{
|
|
||||||
type: "divider"
|
|
||||||
},
|
},
|
||||||
|
...(ImEXPay.treatment === "on"
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: "entercardpayments",
|
||||||
|
id: "header-accounting-entercardpayments",
|
||||||
|
icon: <FaCreditCard />,
|
||||||
|
label: t("menus.header.entercardpayment"),
|
||||||
|
onClick: () => setCardPaymentContext({ actions: {}, context: {} })
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{ type: "divider" },
|
||||||
{
|
{
|
||||||
key: "timetickets",
|
key: "timetickets",
|
||||||
id: "header-accounting-timetickets",
|
id: "header-accounting-timetickets",
|
||||||
@@ -241,132 +255,124 @@ function Header({
|
|||||||
</LockWrapper>
|
</LockWrapper>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
);
|
...(bodyshop?.md_tasks_presets?.use_approvals
|
||||||
|
? [
|
||||||
if (bodyshop?.md_tasks_presets?.use_approvals) {
|
{
|
||||||
accountingChildren.push({
|
key: "ttapprovals",
|
||||||
key: "ttapprovals",
|
id: "header-accounting-ttapprovals",
|
||||||
id: "header-accounting-ttapprovals",
|
icon: <FieldTimeOutlined />,
|
||||||
icon: <FieldTimeOutlined />,
|
label: <Link to="/manage/ttapprovals">{t("menus.header.ttapprovals")}</Link>
|
||||||
label: <Link to="/manage/ttapprovals">{t("menus.header.ttapprovals")}</Link>
|
}
|
||||||
});
|
]
|
||||||
}
|
: []),
|
||||||
accountingChildren.push(
|
|
||||||
{
|
{
|
||||||
key: "entertimetickets",
|
key: "entertimetickets",
|
||||||
icon: <Icon component={GiPlayerTime} />,
|
id: "header-accounting-entertimetickets",
|
||||||
|
icon: <GiPlayerTime />,
|
||||||
label: (
|
label: (
|
||||||
<LockWrapper featureName="timetickets" bodyshop={bodyshop}>
|
<LockWrapper featureName="timetickets" bodyshop={bodyshop}>
|
||||||
{t("menus.header.entertimeticket")}
|
{t("menus.header.entertimeticket")}
|
||||||
</LockWrapper>
|
</LockWrapper>
|
||||||
),
|
),
|
||||||
id: "header-accounting-entertimetickets",
|
onClick: () =>
|
||||||
onClick: () => {
|
|
||||||
HasFeatureAccess({ featureName: "timetickets", bodyshop }) &&
|
HasFeatureAccess({ featureName: "timetickets", bodyshop }) &&
|
||||||
setTimeTicketContext({
|
setTimeTicketContext({
|
||||||
actions: {},
|
actions: {},
|
||||||
context: {
|
context: {
|
||||||
created_by: currentUser.displayName
|
created_by: currentUser.displayName
|
||||||
? currentUser.email.concat(" | ", currentUser.displayName)
|
? `${currentUser.email} | ${currentUser.displayName}`
|
||||||
: currentUser.email
|
: currentUser.email
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
{ type: "divider" },
|
||||||
{
|
{
|
||||||
type: "divider"
|
key: "accountingexport",
|
||||||
}
|
id: "header-accounting-export",
|
||||||
);
|
icon: <ExportOutlined />,
|
||||||
|
|
||||||
const accountingExportChildren = [
|
|
||||||
{
|
|
||||||
key: "receivables",
|
|
||||||
id: "header-accounting-receivables",
|
|
||||||
label: (
|
label: (
|
||||||
<Link to="/manage/accounting/receivables">
|
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||||
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
{t("menus.header.export")}
|
||||||
{t("menus.header.accounting-receivables")}
|
</LockWrapper>
|
||||||
</LockWrapper>
|
),
|
||||||
</Link>
|
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") {
|
// Left menu items (includes original navigation items)
|
||||||
accountingExportChildren.push({
|
const leftMenuItems = [
|
||||||
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
|
|
||||||
});
|
|
||||||
|
|
||||||
const menuItems = [
|
|
||||||
{
|
{
|
||||||
key: "home",
|
key: "home",
|
||||||
icon: <HomeFilled />,
|
|
||||||
id: "header-home",
|
id: "header-home",
|
||||||
|
icon: <HomeFilled />,
|
||||||
label: <Link to="/manage/">{t("menus.header.home")}</Link>
|
label: <Link to="/manage/">{t("menus.header.home")}</Link>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "schedule",
|
key: "schedule",
|
||||||
id: "header-schedule",
|
id: "header-schedule",
|
||||||
icon: <Icon component={FaCalendarAlt} />,
|
icon: <FaCalendarAlt />,
|
||||||
label: <Link to="/manage/schedule">{t("menus.header.schedule")}</Link>
|
label: <Link to="/manage/schedule">{t("menus.header.schedule")}</Link>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "jobssubmenu",
|
key: "jobssubmenu",
|
||||||
id: "header-jobs",
|
id: "header-jobs",
|
||||||
icon: <Icon component={FaCarCrash} />,
|
icon: <FaCarCrash />,
|
||||||
label: t("menus.header.jobs"),
|
label: t("menus.header.jobs"),
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
@@ -399,31 +405,24 @@ function Header({
|
|||||||
icon: <FileAddOutlined />,
|
icon: <FileAddOutlined />,
|
||||||
label: <Link to="/manage/jobs/new">{t("menus.header.newjob")}</Link>
|
label: <Link to="/manage/jobs/new">{t("menus.header.newjob")}</Link>
|
||||||
},
|
},
|
||||||
{
|
{ type: "divider" },
|
||||||
type: "divider",
|
|
||||||
id: "header-jobs-divider"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: "alljobs",
|
key: "alljobs",
|
||||||
id: "header-all-jobs",
|
id: "header-all-jobs",
|
||||||
icon: <UnorderedListOutlined />,
|
icon: <UnorderedListOutlined />,
|
||||||
label: <Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link>
|
label: <Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link>
|
||||||
},
|
},
|
||||||
{
|
{ type: "divider" },
|
||||||
type: "divider",
|
|
||||||
id: "header-jobs-divider2"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: "productionlist",
|
key: "productionlist",
|
||||||
id: "header-production-list",
|
id: "header-production-list",
|
||||||
icon: <ScheduleOutlined />,
|
icon: <ScheduleOutlined />,
|
||||||
label: <Link to="/manage/production/list">{t("menus.header.productionlist")}</Link>
|
label: <Link to="/manage/production/list">{t("menus.header.productionlist")}</Link>
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
key: "productionboard",
|
key: "productionboard",
|
||||||
id: "header-production-board",
|
id: "header-production-board",
|
||||||
icon: <Icon component={BsKanban} />,
|
icon: <BsKanban />,
|
||||||
label: (
|
label: (
|
||||||
<Link to="/manage/production/board">
|
<Link to="/manage/production/board">
|
||||||
<LockWrapper featureName="visualboard" bodyshop={bodyshop}>
|
<LockWrapper featureName="visualboard" bodyshop={bodyshop}>
|
||||||
@@ -432,11 +431,7 @@ function Header({
|
|||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{ type: "divider" },
|
||||||
{
|
|
||||||
type: "divider",
|
|
||||||
id: "header-jobs-divider3"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: "scoreboard",
|
key: "scoreboard",
|
||||||
id: "header-scoreboard",
|
id: "header-scoreboard",
|
||||||
@@ -453,8 +448,8 @@ function Header({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "customers",
|
key: "customers",
|
||||||
icon: <UserOutlined />,
|
|
||||||
id: "header-customers",
|
id: "header-customers",
|
||||||
|
icon: <UserOutlined />,
|
||||||
label: t("menus.header.customers"),
|
label: t("menus.header.customers"),
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
@@ -519,7 +514,6 @@ function Header({
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
...(accountingChildren.length > 0
|
...(accountingChildren.length > 0
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@@ -537,7 +531,6 @@ function Header({
|
|||||||
icon: <PhoneOutlined />,
|
icon: <PhoneOutlined />,
|
||||||
label: <Link to="/manage/phonebook">{t("menus.header.phonebook")}</Link>
|
label: <Link to="/manage/phonebook">{t("menus.header.phonebook")}</Link>
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
key: "temporarydocs",
|
key: "temporarydocs",
|
||||||
id: "header-temporarydocs",
|
id: "header-temporarydocs",
|
||||||
@@ -550,7 +543,6 @@ function Header({
|
|||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
key: "tasks",
|
key: "tasks",
|
||||||
id: "tasks",
|
id: "tasks",
|
||||||
@@ -562,12 +554,7 @@ function Header({
|
|||||||
id: "header-create-task",
|
id: "header-create-task",
|
||||||
icon: <PlusCircleOutlined />,
|
icon: <PlusCircleOutlined />,
|
||||||
label: t("menus.header.create_task"),
|
label: t("menus.header.create_task"),
|
||||||
onClick: () => {
|
onClick: () => setTaskUpsertContext({ actions: {}, context: {} })
|
||||||
setTaskUpsertContext({
|
|
||||||
actions: {},
|
|
||||||
context: {}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "mytasks",
|
key: "mytasks",
|
||||||
@@ -592,7 +579,7 @@ function Header({
|
|||||||
{
|
{
|
||||||
key: "shop",
|
key: "shop",
|
||||||
id: "header-shop",
|
id: "header-shop",
|
||||||
icon: <Icon component={GiSettingsKnobs} />,
|
icon: <GiSettingsKnobs />,
|
||||||
label: <Link to="/manage/shop?tab=info">{t("menus.header.shop_config")}</Link>
|
label: <Link to="/manage/shop?tab=info">{t("menus.header.shop_config")}</Link>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -610,24 +597,18 @@ function Header({
|
|||||||
id: "header-reportcenter",
|
id: "header-reportcenter",
|
||||||
icon: <BarChartOutlined />,
|
icon: <BarChartOutlined />,
|
||||||
label: t("menus.header.reportcenter"),
|
label: t("menus.header.reportcenter"),
|
||||||
onClick: () => {
|
onClick: () => setReportCenterContext({ actions: {}, context: {} })
|
||||||
setReportCenterContext({
|
|
||||||
actions: {},
|
|
||||||
context: {}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "shop-vendors",
|
key: "shop-vendors",
|
||||||
id: "header-shop-vendors",
|
id: "header-shop-vendors",
|
||||||
icon: <Icon component={IoBusinessOutline} />,
|
icon: <IoBusinessOutline />,
|
||||||
label: <Link to="/manage/shop/vendors">{t("menus.header.shop_vendors")}</Link>
|
label: <Link to="/manage/shop/vendors">{t("menus.header.shop_vendors")}</Link>
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
key: "shop-csi",
|
key: "shop-csi",
|
||||||
id: "header-shop-csi",
|
id: "header-shop-csi",
|
||||||
icon: <Icon component={RiSurveyLine} />,
|
icon: <RiSurveyLine />,
|
||||||
label: (
|
label: (
|
||||||
<Link to="/manage/shop/csi">
|
<Link to="/manage/shop/csi">
|
||||||
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||||
@@ -638,14 +619,27 @@ function Header({
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "recent",
|
||||||
|
id: "header-recent",
|
||||||
|
icon: <ClockCircleFilled />,
|
||||||
|
label: t("menus.header.recent"),
|
||||||
|
children: recentItems.map((i, idx) => ({
|
||||||
|
key: idx,
|
||||||
|
id: `header-recent-${idx}`,
|
||||||
|
label: <Link to={i.url}>{i.label}</Link>
|
||||||
|
}))
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "user",
|
key: "user",
|
||||||
label: currentUser.displayName || currentUser.email || t("general.labels.unknown"),
|
id: "header-user",
|
||||||
|
icon: <UserOutlined />,
|
||||||
|
label: t("menus.currentuser.profile"),
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
key: "signout",
|
key: "signout",
|
||||||
id: "header-signout",
|
id: "header-signout",
|
||||||
icon: <Icon component={FiLogOut} />,
|
icon: <FiLogOut />,
|
||||||
danger: true,
|
danger: true,
|
||||||
label: t("user.actions.signout"),
|
label: t("user.actions.signout"),
|
||||||
onClick: () => signOutStart()
|
onClick: () => signOutStart()
|
||||||
@@ -653,33 +647,25 @@ function Header({
|
|||||||
{
|
{
|
||||||
key: "help",
|
key: "help",
|
||||||
id: "header-help",
|
id: "header-help",
|
||||||
icon: <Icon component={QuestionCircleFilled} />,
|
icon: <QuestionCircleFilled />,
|
||||||
label: t("menus.header.help"),
|
label: t("menus.header.help"),
|
||||||
onClick: () => {
|
onClick: () => window.open("https://help.imex.online/", "_blank")
|
||||||
window.open("https://help.imex.online/", "_blank");
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
...(InstanceRenderManager({
|
...(InstanceRenderManager({ imex: true, rome: false })
|
||||||
imex: true,
|
|
||||||
rome: false
|
|
||||||
})
|
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
key: "rescue",
|
key: "rescue",
|
||||||
id: "header-rescue",
|
id: "header-rescue",
|
||||||
icon: <Icon component={CarFilled} />,
|
icon: <CarFilled />,
|
||||||
label: t("menus.header.rescueme"),
|
label: t("menus.header.rescueme"),
|
||||||
onClick: () => {
|
onClick: () => window.open("https://imexrescue.com/", "_blank")
|
||||||
window.open("https://imexrescue.com/", "_blank");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
|
||||||
{
|
{
|
||||||
key: "shiftclock",
|
key: "shiftclock",
|
||||||
id: "header-shiftclock",
|
id: "header-shiftclock",
|
||||||
icon: <Icon component={GiPlayerTime} />,
|
icon: <GiPlayerTime />,
|
||||||
label: (
|
label: (
|
||||||
<Link to="/manage/shiftclock">
|
<Link to="/manage/shiftclock">
|
||||||
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
<LockWrapper featureName="export" bodyshop={bodyshop}>
|
||||||
@@ -688,64 +674,79 @@ function Header({
|
|||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
key: "profile",
|
key: "profile",
|
||||||
id: "header-profile",
|
id: "header-profile",
|
||||||
icon: <UserOutlined />,
|
icon: <UserOutlined />,
|
||||||
label: <Link to="/manage/profile">{t("menus.currentuser.profile")}</Link>
|
label: <Link to="/manage/profile">{t("menus.currentuser.profile")}</Link>
|
||||||
}
|
}
|
||||||
// {
|
|
||||||
// key: 'langselecter',
|
|
||||||
// label: t("menus.currentuser.languageselector"),
|
|
||||||
// children: [
|
|
||||||
// {
|
|
||||||
// key: 'en-US',
|
|
||||||
// label: t("general.languages.english"),
|
|
||||||
// onClick: () => {
|
|
||||||
// window.location.href = "/?lang=en-US";
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// key: 'fr-CA',
|
|
||||||
// label: t("general.languages.french"),
|
|
||||||
// onClick: () => {
|
|
||||||
// window.location.href = "/?lang=fr-CA";
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// key: 'es-MX',
|
|
||||||
// label: t("general.languages.spanish"),
|
|
||||||
// onClick: () => {
|
|
||||||
// window.location.href = "/?lang=es-MX";
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// ]
|
|
||||||
// },
|
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "recent",
|
|
||||||
icon: <ClockCircleFilled />,
|
|
||||||
id: "header-recent",
|
|
||||||
children: recentItems.map((i, idx) => ({
|
|
||||||
key: idx,
|
|
||||||
id: `header-recent-${idx}`,
|
|
||||||
label: <Link to={i.url}>{i.label}</Link>
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 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 (
|
return (
|
||||||
<Layout.Header>
|
<Layout.Header style={{ padding: 0, background: "#001529" }}>
|
||||||
<Menu
|
<div
|
||||||
mode="horizontal"
|
style={{
|
||||||
theme={"dark"}
|
display: "flex",
|
||||||
selectedKeys={[selectedHeader]}
|
justifyContent: "space-between",
|
||||||
onClick={handleMenuClick}
|
alignItems: "center",
|
||||||
subMenuCloseDelay={0.3}
|
height: "100%",
|
||||||
items={menuItems}
|
overflow: "hidden"
|
||||||
/>
|
}}
|
||||||
|
>
|
||||||
|
<Menu
|
||||||
|
mode="horizontal"
|
||||||
|
theme="dark"
|
||||||
|
selectedKeys={[selectedHeader]}
|
||||||
|
onClick={handleMenuClick}
|
||||||
|
subMenuCloseDelay={0.3}
|
||||||
|
items={leftMenuItems}
|
||||||
|
style={{
|
||||||
|
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={notificationItem}
|
||||||
|
style={{ flex: "0 0 auto", minWidth: 0, borderBottom: "none", background: "transparent" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{scenarioNotificationsOn && (
|
||||||
|
<NotificationCenterContainer
|
||||||
|
visible={notificationVisible}
|
||||||
|
onClose={() => setNotificationVisible(false)}
|
||||||
|
unreadCount={unreadCount}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Layout.Header>
|
</Layout.Header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,8 @@
|
|||||||
import i18next from "i18next";
|
|
||||||
import React from "react";
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { setUserLanguage } from "../../redux/user/user.actions";
|
|
||||||
import HeaderComponent from "./header.component";
|
import HeaderComponent from "./header.component";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
export function HeaderContainer() {
|
||||||
setUserLanguage: (language) => dispatch(setUserLanguage(language))
|
return <HeaderComponent />;
|
||||||
});
|
|
||||||
|
|
||||||
export function HeaderContainer({ setUserLanguage }) {
|
|
||||||
const handleMenuClick = (e) => {
|
|
||||||
if (e.item.props.actiontype === "lang-select") {
|
|
||||||
i18next.changeLanguage(e.key, (err, t) => {
|
|
||||||
if (err) {
|
|
||||||
logImEXEvent("language_change_error", { error: err });
|
|
||||||
|
|
||||||
return console.log("Error encountered when changing languages.", err);
|
|
||||||
}
|
|
||||||
logImEXEvent("language_change", { language: e.key });
|
|
||||||
|
|
||||||
setUserLanguage(e.key);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return <HeaderComponent handleMenuClick={handleMenuClick} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(null, mapDispatchToProps)(HeaderContainer);
|
export default connect(null, null)(HeaderContainer);
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { useMutation } from "@apollo/client";
|
|||||||
import { Button, Divider, Dropdown, Form, Input, Popover, Select, Space } from "antd";
|
import { Button, Divider, Dropdown, Form, Input, Popover, Select, Space } from "antd";
|
||||||
import parsePhoneNumber from "libphonenumber-js";
|
import parsePhoneNumber from "libphonenumber-js";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useContext, useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||||
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
|
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
|
||||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
@@ -51,7 +51,7 @@ export function ScheduleEventComponent({
|
|||||||
const searchParams = queryString.parse(useLocation().search);
|
const searchParams = queryString.parse(useLocation().search);
|
||||||
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
||||||
const [title, setTitle] = useState(event.title);
|
const [title, setTitle] = useState(event.title);
|
||||||
const { socket } = useContext(SocketContext);
|
const { socket } = useSocket();
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|
||||||
const blockContent = (
|
const blockContent = (
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ export function JobCloseRoGuardContainer({ job, jobRO, bodyshop, form }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
|
|
||||||
<Collapse.Panel header={t("jobs.labels.performance")}>
|
<Collapse.Panel key="job-performance" header={t("jobs.labels.performance")}>
|
||||||
<Row gutter={[32, 32]}>
|
<Row gutter={[32, 32]}>
|
||||||
<Col className="ro-guard-col" span={24}>
|
<Col className="ro-guard-col" span={24}>
|
||||||
<JobCloseRoGuardTtLifecycle job={job} />
|
<JobCloseRoGuardTtLifecycle job={job} />
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import JobDetailCardsInsuranceComponent from "./job-detail-cards.insurance.compo
|
|||||||
import JobDetailCardsNotesComponent from "./job-detail-cards.notes.component";
|
import JobDetailCardsNotesComponent from "./job-detail-cards.notes.component";
|
||||||
import JobDetailCardsPartsComponent from "./job-detail-cards.parts.component";
|
import JobDetailCardsPartsComponent from "./job-detail-cards.parts.component";
|
||||||
import JobDetailCardsTotalsComponent from "./job-detail-cards.totals.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({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -37,6 +39,7 @@ const span = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function JobDetailCards({ bodyshop, setPrintCenterContext }) {
|
export function JobDetailCards({ bodyshop, setPrintCenterContext }) {
|
||||||
|
const { scenarioNotificationsOn } = useSocket();
|
||||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||||
.filter((screen) => !!screen[1])
|
.filter((screen) => !!screen[1])
|
||||||
.slice(-1)[0];
|
.slice(-1)[0];
|
||||||
@@ -78,7 +81,12 @@ export function JobDetailCards({ bodyshop, setPrintCenterContext }) {
|
|||||||
{data ? (
|
{data ? (
|
||||||
<Card
|
<Card
|
||||||
title={
|
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={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
@@ -122,7 +130,11 @@ export function JobDetailCards({ bodyshop, setPrintCenterContext }) {
|
|||||||
</Col>
|
</Col>
|
||||||
{!bodyshop.uselocalmediaserver && (
|
{!bodyshop.uselocalmediaserver && (
|
||||||
<Col {...span}>
|
<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>
|
||||||
)}
|
)}
|
||||||
<Col {...span}>
|
<Col {...span}>
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo
|
|||||||
refetchQueries: ["GET_LINE_TICKET_BY_PK"]
|
refetchQueries: ["GET_LINE_TICKET_BY_PK"]
|
||||||
});
|
});
|
||||||
if (!r.errors) {
|
if (!r.errors) {
|
||||||
|
if (CriticalPartsScanning.treatment === "on") {
|
||||||
|
await CriticalPartsScan(jobLineEditModal.context.jobid, notification);
|
||||||
|
}
|
||||||
await Axios.post("/job/totalsssu", {
|
await Axios.post("/job/totalsssu", {
|
||||||
id: jobLineEditModal.context.jobid
|
id: jobLineEditModal.context.jobid
|
||||||
});
|
});
|
||||||
@@ -107,7 +110,9 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (CriticalPartsScanning.treatment === "on") {
|
||||||
|
await CriticalPartsScan(jobLineEditModal.context.jobid, notification);
|
||||||
|
}
|
||||||
if (jobLineEditModal.actions.submit) {
|
if (jobLineEditModal.actions.submit) {
|
||||||
jobLineEditModal.actions.submit();
|
jobLineEditModal.actions.submit();
|
||||||
} else {
|
} else {
|
||||||
@@ -115,9 +120,7 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo
|
|||||||
}
|
}
|
||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
}
|
}
|
||||||
if (CriticalPartsScanning.treatment === "on") {
|
|
||||||
CriticalPartsScan(jobLineEditModal.context.jobid, notification);
|
|
||||||
}
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export function JobsTotalsTableComponent({ jobRO, currentUser, job }) {
|
|||||||
<Card title="DEVELOPMENT USE ONLY">
|
<Card title="DEVELOPMENT USE ONLY">
|
||||||
<JobCalculateTotals job={job} disabled={jobRO} />
|
<JobCalculateTotals job={job} disabled={jobRO} />
|
||||||
<Collapse>
|
<Collapse>
|
||||||
<Collapse.Panel header="JSON Tree Totals">
|
<Collapse.Panel key="json-totals" header="JSON Tree Totals">
|
||||||
<div>
|
<div>
|
||||||
<pre>
|
<pre>
|
||||||
{JSON.stringify(
|
{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);
|
||||||
@@ -172,13 +172,13 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
|
|||||||
job: newJob
|
job: newJob
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (CriticalPartsScanning.treatment === "on") {
|
||||||
|
await CriticalPartsScan(r.data.insert_jobs.returning[0].id, notification);
|
||||||
|
}
|
||||||
await Axios.post("/job/totalsssu", {
|
await Axios.post("/job/totalsssu", {
|
||||||
id: r.data.insert_jobs.returning[0].id
|
id: r.data.insert_jobs.returning[0].id
|
||||||
});
|
});
|
||||||
|
|
||||||
if (CriticalPartsScanning.treatment === "on") {
|
|
||||||
CriticalPartsScan(r.data.insert_jobs.returning[0].id, notification);
|
|
||||||
}
|
|
||||||
notification["success"]({
|
notification["success"]({
|
||||||
message: t("jobs.successes.created"),
|
message: t("jobs.successes.created"),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
@@ -281,6 +281,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
|
|||||||
if (CriticalPartsScanning.treatment === "on") {
|
if (CriticalPartsScanning.treatment === "on") {
|
||||||
CriticalPartsScan(updateResult.data.update_jobs.returning[0].id, notification);
|
CriticalPartsScan(updateResult.data.update_jobs.returning[0].id, notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updateResult.errors) {
|
if (updateResult.errors) {
|
||||||
//error while inserting
|
//error while inserting
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
|||||||
import { Button, Card, Dropdown, Form, Input, Modal, Popconfirm, Popover, Select, Space } from "antd";
|
import { Button, Card, Dropdown, Form, Input, Modal, Popconfirm, Popover, Select, Space } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import parsePhoneNumber from "libphonenumber-js";
|
import parsePhoneNumber from "libphonenumber-js";
|
||||||
import { useContext, useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||||
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
|
import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
|
||||||
import { GET_CURRENT_QUESTIONSET_ID, INSERT_CSI } from "../../graphql/csi.queries";
|
import { GET_CURRENT_QUESTIONSET_ID, INSERT_CSI } from "../../graphql/csi.queries";
|
||||||
@@ -130,7 +130,7 @@ export function JobsDetailHeaderActions({
|
|||||||
const [updateJob] = useMutation(UPDATE_JOB);
|
const [updateJob] = useMutation(UPDATE_JOB);
|
||||||
const [voidJob] = useMutation(VOID_JOB);
|
const [voidJob] = useMutation(VOID_JOB);
|
||||||
const [cancelAllAppointments] = useMutation(CANCEL_APPOINTMENTS_BY_JOB_ID);
|
const [cancelAllAppointments] = useMutation(CANCEL_APPOINTMENTS_BY_JOB_ID);
|
||||||
const { socket } = useContext(SocketContext);
|
const { socket } = useSocket();
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
|||||||
<DataLabel label={t("jobs.labels.contracts")}>
|
<DataLabel label={t("jobs.labels.contracts")}>
|
||||||
{job.cccontracts.map((c, index) => (
|
{job.cccontracts.map((c, index) => (
|
||||||
<Space key={c.id} wrap>
|
<Space key={c.id} wrap>
|
||||||
<Link to={`/manage/courtesycars/contracts/${c.id}`}>
|
<Link to={`/manage/courtesycars/contracts/${c.id}`}>
|
||||||
{`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`}
|
{`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`}
|
||||||
{index !== job.cccontracts.length - 1 ? "," : null}
|
{index !== job.cccontracts.length - 1 ? "," : null}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { Virtuoso } from "react-virtuoso";
|
||||||
|
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 day from "../../utils/day.js";
|
||||||
|
import { forwardRef, useRef, useEffect } from "react";
|
||||||
|
import { DateTimeFormat } from "../../utils/DateFormatter.jsx";
|
||||||
|
|
||||||
|
const { Text, Title } = Typography;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
|
||||||
|
// Scroll to top when showUnreadOnly changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (virtuosoRef.current) {
|
||||||
|
virtuosoRef.current.scrollToIndex({ index: 0, behavior: "smooth" });
|
||||||
|
}
|
||||||
|
}, [showUnreadOnly]);
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default NotificationCenterComponent;
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
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/useSocket.jsx";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||||
|
import day from "../../utils/day.js";
|
||||||
|
|
||||||
|
// 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 [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { isConnected, markNotificationRead, markAllNotificationsRead } = useSocket();
|
||||||
|
const notificationRef = useRef(null);
|
||||||
|
|
||||||
|
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
||||||
|
|
||||||
|
const baseWhereClause = useMemo(() => {
|
||||||
|
return { associationid: { _eq: userAssociationId } };
|
||||||
|
}, [userAssociationId]);
|
||||||
|
|
||||||
|
const whereClause = useMemo(() => {
|
||||||
|
return showUnreadOnly ? { ...baseWhereClause, read: { _is_null: true } } : baseWhereClause;
|
||||||
|
}, [baseWhereClause, showUnreadOnly]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
fetchMore,
|
||||||
|
loading: queryLoading,
|
||||||
|
refetch
|
||||||
|
} = useQuery(GET_NOTIFICATIONS, {
|
||||||
|
variables: {
|
||||||
|
limit: INITIAL_NOTIFICATIONS,
|
||||||
|
offset: 0,
|
||||||
|
where: whereClause
|
||||||
|
},
|
||||||
|
fetchPolicy: "cache-and-network",
|
||||||
|
notifyOnNetworkStatusChange: true,
|
||||||
|
pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(),
|
||||||
|
skip: !userAssociationId,
|
||||||
|
onError: (err) => {
|
||||||
|
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
|
||||||
|
.map((notif) => {
|
||||||
|
let scenarioText;
|
||||||
|
let scenarioMeta;
|
||||||
|
try {
|
||||||
|
scenarioText = notif.scenario_text ? JSON.parse(notif.scenario_text) : [];
|
||||||
|
scenarioMeta = notif.scenario_meta ? JSON.parse(notif.scenario_meta) : {};
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error parsing JSON for notification:", notif.id, e);
|
||||||
|
scenarioText = [notif.fcm_text || "Invalid notification data"];
|
||||||
|
scenarioMeta = {};
|
||||||
|
}
|
||||||
|
if (!Array.isArray(scenarioText)) scenarioText = [scenarioText];
|
||||||
|
const roNumber = notif.job.ro_number;
|
||||||
|
if (!Array.isArray(scenarioMeta)) scenarioMeta = [scenarioMeta];
|
||||||
|
return {
|
||||||
|
id: notif.id,
|
||||||
|
jobid: notif.jobid,
|
||||||
|
associationid: notif.associationid,
|
||||||
|
scenarioText,
|
||||||
|
scenarioMeta,
|
||||||
|
roNumber,
|
||||||
|
created_at: notif.created_at,
|
||||||
|
read: notif.read,
|
||||||
|
__typename: notif.__typename
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||||
|
setNotifications(processedNotifications);
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const loadMore = useCallback(() => {
|
||||||
|
if (!queryLoading && data?.notifications.length) {
|
||||||
|
setIsLoading(true); // Show spinner during fetchMore
|
||||||
|
fetchMore({
|
||||||
|
variables: { offset: data.notifications.length, where: whereClause },
|
||||||
|
updateQuery: (prev, { fetchMoreResult }) => {
|
||||||
|
if (!fetchMoreResult) return prev;
|
||||||
|
return {
|
||||||
|
notifications: [...prev.notifications, ...fetchMoreResult.notifications]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Fetch more error:", err);
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false)); // Hide spinner when done
|
||||||
|
}
|
||||||
|
}, [data?.notifications?.length, fetchMore, queryLoading, whereClause]);
|
||||||
|
|
||||||
|
const handleToggleUnreadOnly = (value) => {
|
||||||
|
setShowUnreadOnly(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMarkAllRead = useCallback(() => {
|
||||||
|
setIsLoading(true);
|
||||||
|
markAllNotificationsRead()
|
||||||
|
.then(() => {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
setNotifications((prev) => {
|
||||||
|
const updatedNotifications = prev.map((notif) =>
|
||||||
|
notif.read === null && notif.associationid === userAssociationId
|
||||||
|
? {
|
||||||
|
...notif,
|
||||||
|
read: timestamp
|
||||||
|
}
|
||||||
|
: notif
|
||||||
|
);
|
||||||
|
// 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 || ""}`))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
}, [markAllNotificationsRead, userAssociationId, showUnreadOnly]);
|
||||||
|
|
||||||
|
const handleNotificationClick = useCallback(
|
||||||
|
(notificationId) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
markNotificationRead({ variables: { id: notificationId } })
|
||||||
|
.then(() => {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
setNotifications((prev) => {
|
||||||
|
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 || ""}`))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
},
|
||||||
|
[markNotificationRead, showUnreadOnly]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible && !isConnected) {
|
||||||
|
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={isLoading}
|
||||||
|
showUnreadOnly={showUnreadOnly}
|
||||||
|
toggleUnreadOnly={handleToggleUnreadOnly}
|
||||||
|
markAllRead={handleMarkAllRead}
|
||||||
|
loadMore={loadMore}
|
||||||
|
onNotificationClick={handleNotificationClick}
|
||||||
|
unreadCount={unreadCount}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, null)(NotificationCenterContainer);
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
.notification-center {
|
||||||
|
position: absolute;
|
||||||
|
top: 64px;
|
||||||
|
right: 0;
|
||||||
|
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: 4px 16px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: #fafafa;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
// Styles for the eye icon and switch (custom classes)
|
||||||
|
.notification-toggle {
|
||||||
|
align-items: center; // Ensure vertical alignment
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: #0958d9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-read {
|
||||||
|
background: #fff;
|
||||||
|
color: rgba(0, 0, 0, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-unread {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-item {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
display: block;
|
||||||
|
overflow: visible;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-title {
|
||||||
|
margin: 0;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
border: 1px solid #ffa39e;
|
||||||
|
|
||||||
|
.ant-alert-message {
|
||||||
|
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;
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
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";
|
||||||
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
import { QUERY_NOTIFICATION_SETTINGS, UPDATE_NOTIFICATION_SETTINGS } from "../../graphql/user.queries.js";
|
||||||
|
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";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, {
|
||||||
|
fetchPolicy: "network-only",
|
||||||
|
nextFetchPolicy: "network-only",
|
||||||
|
variables: { email: currentUser.email },
|
||||||
|
skip: !currentUser
|
||||||
|
});
|
||||||
|
|
||||||
|
const [updateNotificationSettings, { loading: saving }] = useMutation(UPDATE_NOTIFICATION_SETTINGS);
|
||||||
|
|
||||||
|
// Populate form with fetched data.
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.associations?.length > 0) {
|
||||||
|
const settings = data.associations[0].notification_settings || {};
|
||||||
|
// Ensure each scenario has an object with { app, email, fcm }.
|
||||||
|
const formattedValues = notificationScenarios.reduce((acc, scenario) => {
|
||||||
|
acc[scenario] = settings[scenario] ?? { app: false, email: false, fcm: false };
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
setInitialValues(formattedValues);
|
||||||
|
form.setFieldsValue(formattedValues);
|
||||||
|
setIsDirty(false); // Reset dirty state when new data loads.
|
||||||
|
}
|
||||||
|
}, [data, form]);
|
||||||
|
|
||||||
|
const handleSave = async (values) => {
|
||||||
|
if (data?.associations?.length > 0) {
|
||||||
|
const userId = data.associations[0].id;
|
||||||
|
// Save the updated notification settings.
|
||||||
|
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") });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mark the form as dirty on any manual change.
|
||||||
|
const handleFormChange = () => {
|
||||||
|
setIsDirty(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
form.setFieldsValue(initialValues);
|
||||||
|
setIsDirty(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) return <AlertComponent type="error" message={error.message} />;
|
||||||
|
if (loading) return <LoadingSpinner />;
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("notifications.labels.scenario"),
|
||||||
|
dataIndex: "scenarioLabel",
|
||||||
|
key: "scenario",
|
||||||
|
render: (_, record) => t(`notifications.scenarios.${record.key}`),
|
||||||
|
width: "90%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: <ColumnHeaderCheckbox channel="app" form={form} onHeaderChange={() => setIsDirty(true)} />,
|
||||||
|
dataIndex: "app",
|
||||||
|
key: "app",
|
||||||
|
align: "center",
|
||||||
|
render: (_, record) => (
|
||||||
|
<Form.Item name={[record.key, "app"]} valuePropName="checked" noStyle>
|
||||||
|
<Checkbox />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: <ColumnHeaderCheckbox channel="email" form={form} onHeaderChange={() => setIsDirty(true)} />,
|
||||||
|
dataIndex: "email",
|
||||||
|
key: "email",
|
||||||
|
align: "center",
|
||||||
|
render: (_, record) => (
|
||||||
|
<Form.Item name={[record.key, "email"]} valuePropName="checked" noStyle>
|
||||||
|
<Checkbox />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// TODO: Disabled for now until FCM is implemented.
|
||||||
|
// {
|
||||||
|
// title: <ColumnHeaderCheckbox channel="fcm" form={form} disabled onHeaderChange={() => setIsDirty(true)} />,
|
||||||
|
// dataIndex: "fcm",
|
||||||
|
// key: "fcm",
|
||||||
|
// align: "center",
|
||||||
|
// render: (_, record) => (
|
||||||
|
// <Form.Item name={[record.key, "fcm"]} valuePropName="checked" noStyle>
|
||||||
|
// <Checkbox disabled />
|
||||||
|
// </Form.Item>
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
];
|
||||||
|
|
||||||
|
const dataSource = notificationScenarios.map((scenario) => ({ key: scenario }));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
onFinish={handleSave}
|
||||||
|
onValuesChange={handleFormChange}
|
||||||
|
initialValues={initialValues}
|
||||||
|
autoComplete="off"
|
||||||
|
layout="vertical"
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
title={t("notifications.labels.notificationscenarios")}
|
||||||
|
extra={
|
||||||
|
<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({
|
||||||
|
email: PropTypes.string.isRequired
|
||||||
|
}).isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
currentUser: selectCurrentUser
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(NotificationSettingsForm);
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
import { DeleteFilled, DownOutlined, WarningFilled } from "@ant-design/icons";
|
import { DeleteFilled, DownOutlined, WarningFilled } from "@ant-design/icons";
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { Checkbox, Divider, Dropdown, Form, Input, InputNumber, Radio, Select, Space, Tag } from "antd";
|
import { Checkbox, Divider, Dropdown, Form, Input, InputNumber, Radio, Select, Space, Tag } from "antd";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
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 CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
||||||
import PartsOrderModalPriceChange from "./parts-order-modal-price-change.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({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -33,7 +32,7 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState,
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const handleClick = ({ item, key, keyPath }) => {
|
const handleClick = ({ item }) => {
|
||||||
form.setFieldsValue({ comments: item.props.value });
|
form.setFieldsValue({ comments: item.props.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,17 +97,18 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState,
|
|||||||
<Checkbox />
|
<Checkbox />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
{!isReturn && (
|
||||||
<Form.Item name="order_type" initialValue="parts_order" label={t("parts_orders.labels.order_type")}>
|
<Form.Item name="order_type" initialValue="parts_order" label={t("parts_orders.labels.order_type")}>
|
||||||
<Radio.Group disabled={sendType === "oec"}>
|
<Radio.Group disabled={sendType === "oec"}>
|
||||||
<Radio value={"parts_order"}>{t("parts_orders.labels.parts_order")}</Radio>
|
<Radio value={"parts_order"}>{t("parts_orders.labels.parts_order")}</Radio>
|
||||||
<Radio value={"sublet"}>{t("parts_orders.labels.sublet_order")}</Radio>
|
<Radio value={"sublet"}>{t("parts_orders.labels.sublet_order")}</Radio>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
)}
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<Divider orientation="left">{t("parts_orders.labels.inthisorder")}</Divider>
|
<Divider orientation="left">{t("parts_orders.labels.inthisorder")}</Divider>
|
||||||
<Form.List name={["parts_order_lines", "data"]}>
|
<Form.List name={["parts_order_lines", "data"]}>
|
||||||
{(fields, { add, remove, move }) => {
|
{(fields, { remove, move }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ import { CopyFilled } from "@ant-design/icons";
|
|||||||
import { Button, Form, message, Popover, Space } from "antd";
|
import { Button, Form, message, Popover, Space } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import Dinero from "dinero.js";
|
import Dinero from "dinero.js";
|
||||||
import { parsePhoneNumber } from "libphonenumber-js";
|
import { parsePhoneNumberWithError, ParseError } from "libphonenumber-js";
|
||||||
import React, { useContext, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
||||||
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -29,22 +29,34 @@ export function PaymentsGenerateLink({ bodyshop, currentUser, callback, job, ope
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [paymentLink, setPaymentLink] = useState(null);
|
const [paymentLink, setPaymentLink] = useState(null);
|
||||||
const { socket } = useContext(SocketContext);
|
const { socket } = useSocket();
|
||||||
|
|
||||||
const handleFinish = async ({ amount }) => {
|
const handleFinish = async ({ amount }) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
let p;
|
let p;
|
||||||
try {
|
try {
|
||||||
p = parsePhoneNumber(job.ownr_ph1 || "", "CA");
|
// Updated to use parsePhoneNumberWithError
|
||||||
|
p = parsePhoneNumberWithError(job.ownr_ph1 || "", "CA");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Unable to parse phone number");
|
if (error instanceof ParseError) {
|
||||||
|
// Handle specific parsing errors
|
||||||
|
console.log(`Phone number parsing failed: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
// Handle other unexpected errors
|
||||||
|
console.log("Unexpected error while parsing phone number:", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await axios.post("/intellipay/generate_payment_url", {
|
const response = await axios.post("/intellipay/generate_payment_url", {
|
||||||
bodyshop,
|
bodyshop,
|
||||||
amount: amount,
|
amount: amount,
|
||||||
account: job.ro_number,
|
account: job.ro_number,
|
||||||
comment: btoa(JSON.stringify({ payments: [{ jobid: job.id, amount }], userEmail: currentUser.email }))
|
comment: btoa(
|
||||||
|
JSON.stringify({
|
||||||
|
payments: [{ jobid: job.id, amount }],
|
||||||
|
userEmail: currentUser.email
|
||||||
|
})
|
||||||
|
)
|
||||||
});
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setPaymentLink(response.data.shorUrl);
|
setPaymentLink(response.data.shorUrl);
|
||||||
@@ -106,7 +118,20 @@ export function PaymentsGenerateLink({ bodyshop, currentUser, callback, job, ope
|
|||||||
</Space>
|
</Space>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const p = parsePhoneNumber(job.ownr_ph1, "CA");
|
let p;
|
||||||
|
try {
|
||||||
|
// Updated second instance of phone parsing
|
||||||
|
p = parsePhoneNumberWithError(job.ownr_ph1, "CA");
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ParseError) {
|
||||||
|
// Handle specific parsing errors
|
||||||
|
console.log(`Phone number parsing failed: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
// Handle other unexpected errors
|
||||||
|
console.log("Unexpected error while parsing phone number:", error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
openChatByPhone({
|
openChatByPhone({
|
||||||
phone_num: p.formatInternational(),
|
phone_num: p.formatInternational(),
|
||||||
jobid: job.id,
|
jobid: job.id,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useApolloClient } from "@apollo/client";
|
|||||||
import { Button, Skeleton, Space } from "antd";
|
import { Button, Skeleton, Space } from "antd";
|
||||||
import cloneDeep from "lodash/cloneDeep";
|
import cloneDeep from "lodash/cloneDeep";
|
||||||
import isEqual from "lodash/isEqual";
|
import isEqual from "lodash/isEqual";
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useContext, useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import { useApolloClient, useQuery, useSubscription } from "@apollo/client";
|
import { useApolloClient, useQuery, useSubscription } from "@apollo/client";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -12,7 +12,7 @@ import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
|
|||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import ProductionBoardKanbanComponent from "./production-board-kanban.component";
|
import ProductionBoardKanbanComponent from "./production-board-kanban.component";
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -22,7 +22,7 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionType = "direct" }) {
|
function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionType = "direct" }) {
|
||||||
const fired = useRef(false);
|
const fired = useRef(false);
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
const { socket } = useContext(SocketContext); // Get the socket from context
|
const { socket } = useSocket();
|
||||||
const reconnectTimeout = useRef(null); // To store the reconnect timeout
|
const reconnectTimeout = useRef(null); // To store the reconnect timeout
|
||||||
const disconnectTime = useRef(null); // To track disconnection time
|
const disconnectTime = useRef(null); // To track disconnection time
|
||||||
const acceptableReconnectTime = 2000; // 2 seconds threshold
|
const acceptableReconnectTime = 2000; // 2 seconds threshold
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import ScoreboardAddButton from "../job-scoreboard-add-button/job-scoreboard-add
|
|||||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
import ProductionRemoveButton from "../production-remove-button/production-remove-button.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({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -41,6 +43,7 @@ export function ProductionListDetail({ bodyshop, jobs, setPrintCenterContext, te
|
|||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
const { selected } = search;
|
const { selected } = search;
|
||||||
|
const { scenarioNotificationsOn } = useSocket();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const theJob = jobs.find((j) => j.id === selected) || {};
|
const theJob = jobs.find((j) => j.id === selected) || {};
|
||||||
@@ -60,7 +63,12 @@ export function ProductionListDetail({ bodyshop, jobs, setPrintCenterContext, te
|
|||||||
<Drawer
|
<Drawer
|
||||||
title={
|
title={
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={theJob.ro_number}
|
title={
|
||||||
|
<Space>
|
||||||
|
{!technician && scenarioNotificationsOn && <JobWatcherToggleContainer job={theJob} />}
|
||||||
|
{theJob.ro_number}
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{!technician ? <ProductionRemoveButton jobId={theJob.id} /> : null}
|
{!technician ? <ProductionRemoveButton jobId={theJob.id} /> : null}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useApolloClient, useQuery, useSubscription } from "@apollo/client";
|
import { useApolloClient, useQuery, useSubscription } from "@apollo/client";
|
||||||
import React, { useContext, useEffect, useState, useRef } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
QUERY_EXACT_JOB_IN_PRODUCTION,
|
QUERY_EXACT_JOB_IN_PRODUCTION,
|
||||||
QUERY_EXACT_JOBS_IN_PRODUCTION,
|
QUERY_EXACT_JOBS_IN_PRODUCTION,
|
||||||
@@ -10,11 +10,11 @@ import {
|
|||||||
import ProductionListTable from "./production-list-table.component";
|
import ProductionListTable from "./production-list-table.component";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||||
|
|
||||||
export default function ProductionListTableContainer({ bodyshop, subscriptionType = "direct" }) {
|
export default function ProductionListTableContainer({ bodyshop, subscriptionType = "direct" }) {
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
const { socket } = useContext(SocketContext);
|
const { socket } = useSocket();
|
||||||
const [joblist, setJoblist] = useState([]);
|
const [joblist, setJoblist] = useState([]);
|
||||||
const reconnectTimeout = useRef(null); // To store the reconnect timeout
|
const reconnectTimeout = useRef(null); // To store the reconnect timeout
|
||||||
const disconnectTime = useRef(null); // To store the time of disconnection
|
const disconnectTime = useRef(null); // To store the time of disconnection
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export default function ProductionRemoveButton({ jobId }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button loading={loading} onClick={handleRemoveFromProd} type={"danger"}>
|
<Button loading={loading} onClick={handleRemoveFromProd} type="default" danger>
|
||||||
{t("production.actions.remove")}
|
{t("production.actions.remove")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Button, Card, Col, Form, Input } from "antd";
|
import { Button, Card, Col, Form, Input } from "antd";
|
||||||
import { LockOutlined } from "@ant-design/icons";
|
import { LockOutlined } from "@ant-design/icons";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -9,6 +8,8 @@ import { selectCurrentUser } from "../../redux/user/user.selectors";
|
|||||||
import { logImEXEvent, updateCurrentPassword } from "../../firebase/firebase.utils";
|
import { logImEXEvent, updateCurrentPassword } from "../../firebase/firebase.utils";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||||
|
import NotificationSettingsForm from "../notification-settings/notification-settings-form.component.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser
|
currentUser: selectCurrentUser
|
||||||
@@ -22,6 +23,7 @@ export default connect(
|
|||||||
)(function ProfileMyComponent({ currentUser, updateUserDetails }) {
|
)(function ProfileMyComponent({ currentUser, updateUserDetails }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
const { scenarioNotificationsOn } = useSocket();
|
||||||
|
|
||||||
const handleFinish = (values) => {
|
const handleFinish = (values) => {
|
||||||
logImEXEvent("profile_update");
|
logImEXEvent("profile_update");
|
||||||
@@ -117,6 +119,11 @@ export default connect(
|
|||||||
</Card>
|
</Card>
|
||||||
</Form>
|
</Form>
|
||||||
</Col>
|
</Col>
|
||||||
|
{scenarioNotificationsOn && (
|
||||||
|
<Col span={24}>
|
||||||
|
<NotificationSettingsForm />
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,27 @@
|
|||||||
import {DeleteFilled} from "@ant-design/icons";
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
import {Button, Col, Form, Input, Row, Select, Space, Switch} from "antd";
|
import { Button, Col, Form, Input, Row, Select, Space, Switch } from "antd";
|
||||||
import React, {useMemo} from "react";
|
import { useMemo } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
import i18n from "i18next";
|
||||||
|
|
||||||
const predefinedPartTypes = [
|
const predefinedPartTypes = ["PAN", "PAC", "PAR", "PAL", "PAA", "PAM", "PAP", "PAS", "PASL", "PAG"];
|
||||||
"PAN", "PAC", "PAR", "PAL", "PAA", "PAM", "PAP", "PAS", "PASL", "PAG"
|
|
||||||
];
|
|
||||||
const predefinedModLbrTypes = [
|
const predefinedModLbrTypes = [
|
||||||
"LAA", "LAB", "LAD", "LAE", "LAF", "LAG", "LAM", "LAR", "LAS", "LAU",
|
"LAA",
|
||||||
"LA1", "LA2", "LA3", "LA4"
|
"LAB",
|
||||||
|
"LAD",
|
||||||
|
"LAE",
|
||||||
|
"LAF",
|
||||||
|
"LAG",
|
||||||
|
"LAM",
|
||||||
|
"LAR",
|
||||||
|
"LAS",
|
||||||
|
"LAU",
|
||||||
|
"LA1",
|
||||||
|
"LA2",
|
||||||
|
"LA3",
|
||||||
|
"LA4"
|
||||||
];
|
];
|
||||||
|
|
||||||
const getFieldType = (field) => {
|
const getFieldType = (field) => {
|
||||||
@@ -20,30 +31,46 @@ const getFieldType = (field) => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ShopInfoPartsScan({form}) {
|
const fieldSelectOptions = [
|
||||||
const {t} = useTranslation();
|
{ label: i18n.t("joblines.fields.line_desc"), value: "line_desc" },
|
||||||
|
{ label: i18n.t("joblines.fields.part_type"), value: "part_type" },
|
||||||
|
{ label: i18n.t("joblines.fields.act_price"), value: "act_price" },
|
||||||
|
{ label: i18n.t("joblines.fields.part_qty"), value: "part_qty" },
|
||||||
|
{ label: i18n.t("joblines.fields.mod_lbr_ty"), value: "mod_lbr_ty" },
|
||||||
|
|
||||||
|
{
|
||||||
|
label: `${i18n.t("joblines.fields.oem_partno")} / ${i18n.t("joblines.fields.alt_partno")}`,
|
||||||
|
value: "part_number"
|
||||||
|
},
|
||||||
|
{ label: i18n.t("joblines.fields.op_code_desc"), value: "op_code_desc" }
|
||||||
|
];
|
||||||
|
export default function ShopInfoPartsScan({ form }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const watchedFields = Form.useWatch("md_parts_scan", form);
|
const watchedFields = Form.useWatch("md_parts_scan", form);
|
||||||
|
|
||||||
const operationOptions = useMemo(() => ({
|
const operationOptions = useMemo(
|
||||||
string: [
|
() => ({
|
||||||
{label: t("bodyshop.operations.contains"), value: "contains"},
|
string: [
|
||||||
{label: t("bodyshop.operations.equals"), value: "equals"},
|
{ label: t("bodyshop.operations.contains"), value: "contains" },
|
||||||
{label: t("bodyshop.operations.starts_with"), value: "startsWith"},
|
{ label: t("bodyshop.operations.equals"), value: "equals" },
|
||||||
{label: t("bodyshop.operations.ends_with"), value: "endsWith"},
|
{ label: t("bodyshop.operations.starts_with"), value: "startsWith" },
|
||||||
],
|
{ label: t("bodyshop.operations.ends_with"), value: "endsWith" }
|
||||||
number: [
|
],
|
||||||
{label: t("bodyshop.operations.equals"), value: "="},
|
number: [
|
||||||
{label: t("bodyshop.operations.greater_than"), value: ">"},
|
{ label: t("bodyshop.operations.equals"), value: "=" },
|
||||||
{label: t("bodyshop.operations.less_than"), value: "<"},
|
{ label: t("bodyshop.operations.greater_than"), value: ">" },
|
||||||
],
|
{ label: t("bodyshop.operations.less_than"), value: "<" }
|
||||||
}), [t]);
|
]
|
||||||
|
}),
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<LayoutFormRow header={t("bodyshop.labels.md_parts_scan")}>
|
<LayoutFormRow header={t("bodyshop.labels.md_parts_scan")}>
|
||||||
<Form.List name={["md_parts_scan"]}>
|
<Form.List name={["md_parts_scan"]}>
|
||||||
{(fields, {add, remove, move}) => (
|
{(fields, { add, remove, move }) => (
|
||||||
<div>
|
<div>
|
||||||
{fields.map((field, index) => {
|
{fields.map((field, index) => {
|
||||||
const selectedField = watchedFields?.[index]?.field || "line_desc";
|
const selectedField = watchedFields?.[index]?.field || "line_desc";
|
||||||
@@ -61,28 +88,17 @@ export default function ShopInfoPartsScan({form}) {
|
|||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required", {
|
message: t("general.validation.required", {
|
||||||
label: t("bodyshop.fields.md_parts_scan.field"),
|
label: t("bodyshop.fields.md_parts_scan.field")
|
||||||
}),
|
})
|
||||||
},
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
options={[
|
options={fieldSelectOptions}
|
||||||
{label: t("joblines.fields.line_desc"), value: "line_desc"},
|
|
||||||
{label: t("joblines.fields.part_type"), value: "part_type"},
|
|
||||||
{label: t("joblines.fields.act_price"), value: "act_price"},
|
|
||||||
{label: t("joblines.fields.part_qty"), value: "part_qty"},
|
|
||||||
{label: t("joblines.fields.mod_lbr_ty"), value: "mod_lbr_ty"},
|
|
||||||
{label: t("joblines.fields.mod_lb_hrs"), value: "mod_lb_hrs"},
|
|
||||||
{
|
|
||||||
label: `${t("joblines.fields.oem_partno")} / ${t("joblines.fields.alt_partno")}`,
|
|
||||||
value: "part_number"
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
form.setFields([
|
form.setFields([
|
||||||
{name: ["md_parts_scan", index, "operation"], value: "contains"},
|
{ name: ["md_parts_scan", index, "operation"], value: "contains" },
|
||||||
{name: ["md_parts_scan", index, "value"], value: undefined},
|
{ name: ["md_parts_scan", index, "value"], value: undefined }
|
||||||
]);
|
]);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -99,12 +115,12 @@ export default function ShopInfoPartsScan({form}) {
|
|||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required", {
|
message: t("general.validation.required", {
|
||||||
label: t("bodyshop.fields.md_parts_scan.operation"),
|
label: t("bodyshop.fields.md_parts_scan.operation")
|
||||||
}),
|
})
|
||||||
},
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Select options={operationOptions[fieldType]}/>
|
<Select options={operationOptions[fieldType]} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
@@ -119,9 +135,9 @@ export default function ShopInfoPartsScan({form}) {
|
|||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required", {
|
message: t("general.validation.required", {
|
||||||
label: t("bodyshop.fields.md_parts_scan.value"),
|
label: t("bodyshop.fields.md_parts_scan.value")
|
||||||
}),
|
})
|
||||||
},
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{fieldType === "predefined" ? (
|
{fieldType === "predefined" ? (
|
||||||
@@ -129,17 +145,17 @@ export default function ShopInfoPartsScan({form}) {
|
|||||||
options={
|
options={
|
||||||
selectedField === "part_type"
|
selectedField === "part_type"
|
||||||
? predefinedPartTypes.map((type) => ({
|
? predefinedPartTypes.map((type) => ({
|
||||||
label: type,
|
label: type,
|
||||||
value: type
|
value: type
|
||||||
}))
|
}))
|
||||||
: predefinedModLbrTypes.map((type) => ({
|
: predefinedModLbrTypes.map((type) => ({
|
||||||
label: type,
|
label: type,
|
||||||
value: type
|
value: type
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Input/>
|
<Input />
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -152,19 +168,70 @@ export default function ShopInfoPartsScan({form}) {
|
|||||||
label={t("bodyshop.fields.md_parts_scan.caseInsensitive")}
|
label={t("bodyshop.fields.md_parts_scan.caseInsensitive")}
|
||||||
name={[field.name, "caseInsensitive"]}
|
name={[field.name, "caseInsensitive"]}
|
||||||
valuePropName="checked"
|
valuePropName="checked"
|
||||||
labelCol={{span: 14}}
|
initialValue={true}
|
||||||
wrapperCol={{span: 10}}
|
labelCol={{ span: 14 }}
|
||||||
|
wrapperCol={{ span: 10 }}
|
||||||
>
|
>
|
||||||
<Switch defaultChecked={true}/>
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Mark Line as Critical */}
|
||||||
|
<Col span={4}>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.md_parts_scan.mark_critical")}
|
||||||
|
name={[field.name, "mark_critical"]}
|
||||||
|
valuePropName="checked"
|
||||||
|
initialValue={true}
|
||||||
|
labelCol={{ span: 14 }}
|
||||||
|
wrapperCol={{ span: 10 }}
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{/* Update Field */}
|
||||||
|
<Col span={4}>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.md_parts_scan.update_field")}
|
||||||
|
name={[field.name, "update_field"]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={fieldSelectOptions}
|
||||||
|
allowClear
|
||||||
|
onClear={() =>
|
||||||
|
form.setFields([{ name: ["md_parts_scan", index, "update_field"], value: null }])
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{/* Update Field */}
|
||||||
|
<Col span={4}>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.md_parts_scan.update_value")}
|
||||||
|
name={[field.name, "update_value"]}
|
||||||
|
dependencies={[["md_parts_scan", index, "update_field"]]}
|
||||||
|
tooltip={t("bodyshop.tooltips.md_parts_scan.update_value_tooltip")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: form.getFieldValue(["md_parts_scan", index, "update_field"]),
|
||||||
|
message: t("general.validation.required", {
|
||||||
|
label: t("bodyshop.fields.md_parts_scan.update_value")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<Col span={2}>
|
<Col span={2}>
|
||||||
<Space>
|
<Space>
|
||||||
<DeleteFilled onClick={() => remove(field.name)}/>
|
<DeleteFilled onClick={() => remove(field.name)} />
|
||||||
<FormListMoveArrows move={move} index={index} total={fields.length}/>
|
<FormListMoveArrows move={move} index={index} total={fields.length} />
|
||||||
</Space>
|
</Space>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -175,8 +242,8 @@ export default function ShopInfoPartsScan({form}) {
|
|||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Button
|
<Button
|
||||||
type="dashed"
|
type="dashed"
|
||||||
onClick={() => add({field: "line_desc", operation: "contains"})}
|
onClick={() => add({ field: "line_desc", operation: "contains" })}
|
||||||
style={{width: "100%"}}
|
style={{ width: "100%" }}
|
||||||
>
|
>
|
||||||
{t("bodyshop.actions.addpartsrule")}
|
{t("bodyshop.actions.addpartsrule")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { AlertOutlined } from "@ant-design/icons";
|
import { AlertOutlined } from "@ant-design/icons";
|
||||||
import { Alert, Button, Col, Row, Space } from "antd";
|
import { Alert, Button, Col, Row, Space } from "antd";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -81,8 +81,7 @@ export function UpdateAlert({ updateAvailable }) {
|
|||||||
imex: "$t(titles.imexonline)",
|
imex: "$t(titles.imexonline)",
|
||||||
rome: "$t(titles.romeonline)"
|
rome: "$t(titles.romeonline)"
|
||||||
})
|
})
|
||||||
}),
|
})
|
||||||
placement: "bottomRight"
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (needRefresh && timerStarted && timeLeft <= 0) {
|
if (needRefresh && timerStarted && timeLeft <= 0) {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// NotificationProvider.jsx
|
import { createContext, useContext } from "react";
|
||||||
import React, { createContext, useContext } from "react";
|
|
||||||
import { notification } from "antd";
|
import { notification } from "antd";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,7 +21,11 @@ export const useNotification = () => {
|
|||||||
* - Provide `api` via the NotificationContext.
|
* - Provide `api` via the NotificationContext.
|
||||||
*/
|
*/
|
||||||
export const NotificationProvider = ({ children }) => {
|
export const NotificationProvider = ({ children }) => {
|
||||||
const [api, contextHolder] = notification.useNotification();
|
const [api, contextHolder] = notification.useNotification({
|
||||||
|
placement: "bottomRight",
|
||||||
|
bottom: 70,
|
||||||
|
showProgress: true
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NotificationContext.Provider value={api}>
|
<NotificationContext.Provider value={api}>
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
import React, { createContext } from "react";
|
|
||||||
import useSocket from "./useSocket"; // Import the custom hook
|
|
||||||
|
|
||||||
// Create the SocketContext
|
|
||||||
const SocketContext = createContext(null);
|
|
||||||
|
|
||||||
export const SocketProvider = ({ children, bodyshop }) => {
|
|
||||||
const { socket, clientId } = useSocket(bodyshop);
|
|
||||||
|
|
||||||
return <SocketContext.Provider value={{ socket, clientId }}> {children}</SocketContext.Provider>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SocketContext;
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
import { 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";
|
|
||||||
|
|
||||||
const useSocket = (bodyshop) => {
|
|
||||||
const socketRef = useRef(null);
|
|
||||||
const [clientId, setClientId] = useState(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const initializeSocket = async (token) => {
|
|
||||||
if (!bodyshop || !bodyshop.id) 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 },
|
|
||||||
reconnectionAttempts: Infinity,
|
|
||||||
reconnectionDelay: 2000,
|
|
||||||
reconnectionDelayMax: 10000
|
|
||||||
});
|
|
||||||
|
|
||||||
socketRef.current = socketInstance;
|
|
||||||
|
|
||||||
// Handle socket events
|
|
||||||
const handleBodyshopMessage = (message) => {
|
|
||||||
if (!message || !message.type) return;
|
|
||||||
|
|
||||||
switch (message.type) {
|
|
||||||
case "alert-update":
|
|
||||||
store.dispatch(addAlerts(message.payload));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!import.meta.env.DEV) return;
|
|
||||||
console.log(`Received message for bodyshop ${bodyshop.id}:`, message);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConnect = () => {
|
|
||||||
socketInstance.emit("join-bodyshop-room", bodyshop.id);
|
|
||||||
setClientId(socketInstance.id);
|
|
||||||
store.dispatch(setWssStatus("connected"));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReconnect = () => {
|
|
||||||
store.dispatch(setWssStatus("connected"));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConnectionError = (err) => {
|
|
||||||
console.error("Socket connection error:", err);
|
|
||||||
|
|
||||||
// Handle token expiration
|
|
||||||
if (err.message.includes("auth/id-token-expired")) {
|
|
||||||
console.warn("Token expired, refreshing...");
|
|
||||||
auth.currentUser?.getIdToken(true).then((newToken) => {
|
|
||||||
socketInstance.auth = { token: newToken }; // Update socket auth
|
|
||||||
socketInstance.connect(); // Retry connection
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
store.dispatch(setWssStatus("error"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDisconnect = (reason) => {
|
|
||||||
console.warn("Socket disconnected:", reason);
|
|
||||||
store.dispatch(setWssStatus("disconnected"));
|
|
||||||
|
|
||||||
// Manually trigger reconnection if necessary
|
|
||||||
if (!socketInstance.connected && reason !== "io server disconnect") {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (socketInstance.disconnected) {
|
|
||||||
console.log("Manually triggering reconnection...");
|
|
||||||
socketInstance.connect();
|
|
||||||
}
|
|
||||||
}, 2000); // Retry after 2 seconds
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Register event handlers
|
|
||||||
socketInstance.on("connect", handleConnect);
|
|
||||||
socketInstance.on("reconnect", handleReconnect);
|
|
||||||
socketInstance.on("connect_error", handleConnectionError);
|
|
||||||
socketInstance.on("disconnect", handleDisconnect);
|
|
||||||
socketInstance.on("bodyshop-message", handleBodyshopMessage);
|
|
||||||
};
|
|
||||||
|
|
||||||
const unsubscribe = auth.onIdTokenChanged(async (user) => {
|
|
||||||
if (user) {
|
|
||||||
const token = await user.getIdToken();
|
|
||||||
|
|
||||||
if (socketRef.current) {
|
|
||||||
// Update token if socket exists
|
|
||||||
socketRef.current.emit("update-token", token);
|
|
||||||
} else {
|
|
||||||
// Initialize socket if not already connected
|
|
||||||
initializeSocket(token);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// User is not authenticated
|
|
||||||
if (socketRef.current) {
|
|
||||||
socketRef.current.disconnect();
|
|
||||||
socketRef.current = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clean up on unmount
|
|
||||||
return () => {
|
|
||||||
unsubscribe();
|
|
||||||
if (socketRef.current) {
|
|
||||||
socketRef.current.disconnect();
|
|
||||||
socketRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [bodyshop]);
|
|
||||||
|
|
||||||
return { socket: socketRef.current, clientId };
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useSocket;
|
|
||||||
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";
|
import { onError } from "@apollo/client/link/error";
|
||||||
//https://stackoverflow.com/questions/57163454/refreshing-a-token-with-apollo-client-firebase-auth
|
//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 }) => {
|
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
|
||||||
if (graphQLErrors) {
|
if (graphQLErrors) {
|
||||||
graphQLErrors.forEach(({ message, locations, path }) => {
|
graphQLErrors.forEach(({ message, locations, path }) => {
|
||||||
console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${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)}`);
|
if (networkError) console.log(`[Network error]: ${JSON.stringify(networkError)}`);
|
||||||
|
|||||||
@@ -349,3 +349,13 @@ export const QUERY_STRIPE_ID = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const GET_ACTIVE_EMPLOYEES_IN_SHOP = gql`
|
||||||
|
query GetActiveEmployeesInShop($shopid: uuid!) {
|
||||||
|
associations(where: { shopid: { _eq: $shopid } }) {
|
||||||
|
id
|
||||||
|
useremail
|
||||||
|
shopid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -524,6 +524,10 @@ export const GET_JOB_BY_PK = gql`
|
|||||||
invoice_final_note
|
invoice_final_note
|
||||||
iouparent
|
iouparent
|
||||||
job_totals
|
job_totals
|
||||||
|
job_watchers {
|
||||||
|
id
|
||||||
|
user_email
|
||||||
|
}
|
||||||
joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) {
|
joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) {
|
||||||
act_price
|
act_price
|
||||||
act_price_before_ppc
|
act_price_before_ppc
|
||||||
@@ -1890,6 +1894,7 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
|
|||||||
kmout
|
kmout
|
||||||
qb_multiple_payers
|
qb_multiple_payers
|
||||||
lbr_adjustments
|
lbr_adjustments
|
||||||
|
ownr_ea
|
||||||
payments {
|
payments {
|
||||||
amount
|
amount
|
||||||
created_at
|
created_at
|
||||||
@@ -2566,3 +2571,34 @@ export const GET_JOB_BY_PK_QUICK_INTAKE = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const GET_JOB_WATCHERS = gql`
|
||||||
|
query GET_JOB_WATCHERS($jobid: uuid!) {
|
||||||
|
job_watchers(where: { jobid: { _eq: $jobid } }) {
|
||||||
|
id
|
||||||
|
user_email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ADD_JOB_WATCHER = gql`
|
||||||
|
mutation ADD_JOB_WATCHER($jobid: uuid!, $userEmail: String!) {
|
||||||
|
insert_job_watchers_one(object: { jobid: $jobid, user_email: $userEmail }) {
|
||||||
|
id
|
||||||
|
jobid
|
||||||
|
user_email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
58
client/src/graphql/notifications.queries.js
Normal file
58
client/src/graphql/notifications.queries.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { gql } from "@apollo/client";
|
||||||
|
|
||||||
|
export const GET_NOTIFICATIONS = gql`
|
||||||
|
query GetNotifications($limit: Int!, $offset: Int!, $where: notifications_bool_exp) {
|
||||||
|
notifications(limit: $limit, offset: $offset, order_by: { created_at: desc }, where: $where) {
|
||||||
|
id
|
||||||
|
jobid
|
||||||
|
associationid
|
||||||
|
scenario_text
|
||||||
|
fcm_text
|
||||||
|
scenario_meta
|
||||||
|
created_at
|
||||||
|
read
|
||||||
|
job {
|
||||||
|
id
|
||||||
|
ro_number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GET_UNREAD_COUNT = gql`
|
||||||
|
query GetUnreadCount($associationid: uuid!) {
|
||||||
|
notifications_aggregate(where: { read: { _is_null: true }, associationid: { _eq: $associationid } }) {
|
||||||
|
aggregate {
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const MARK_ALL_NOTIFICATIONS_READ = gql`
|
||||||
|
mutation MarkAllNotificationsRead($associationid: uuid!) {
|
||||||
|
update_notifications(
|
||||||
|
where: { read: { _is_null: true }, associationid: { _eq: $associationid } }
|
||||||
|
_set: { read: "now()" }
|
||||||
|
) {
|
||||||
|
affected_rows
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const MARK_NOTIFICATION_READ = gql`
|
||||||
|
mutation MarkNotificationRead($id: uuid!) {
|
||||||
|
update_notifications(where: { id: { _eq: $id } }, _set: { read: "now()" }) {
|
||||||
|
returning {
|
||||||
|
id
|
||||||
|
read
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const UPDATE_NOTIFICATIONS_READ_FRAGMENT = gql`
|
||||||
|
fragment UpdateNotificationRead on notifications {
|
||||||
|
read
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -85,3 +85,21 @@ export const UPDATE_KANBAN_SETTINGS = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const QUERY_NOTIFICATION_SETTINGS = gql`
|
||||||
|
query QUERY_NOTIFICATION_SETTINGS($email: String!) {
|
||||||
|
associations(where: { _and: { useremail: { _eq: $email }, active: { _eq: true } } }) {
|
||||||
|
id
|
||||||
|
notification_settings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const UPDATE_NOTIFICATION_SETTINGS = gql`
|
||||||
|
mutation UPDATE_NOTIFICATION_SETTINGS($id: uuid!, $ns: jsonb) {
|
||||||
|
update_associations_by_pk(pk_columns: { id: $id }, _set: { notification_settings: $ns }) {
|
||||||
|
id
|
||||||
|
notification_settings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
import "./utils/sentry"; //Must be first.
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
|
import { ConfigProvider } from "antd";
|
||||||
import Dinero from "dinero.js";
|
import Dinero from "dinero.js";
|
||||||
import React from "react";
|
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import { createBrowserRouter, createRoutesFromElements, Route, RouterProvider } from "react-router-dom";
|
import { createBrowserRouter, createRoutesFromElements, Route, RouterProvider } from "react-router-dom";
|
||||||
import { PersistGate } from "redux-persist/integration/react";
|
import { PersistGate } from "redux-persist/integration/react";
|
||||||
|
import { registerSW } from "virtual:pwa-register";
|
||||||
import AppContainer from "./App/App.container";
|
import AppContainer from "./App/App.container";
|
||||||
import LoadingSpinner from "./components/loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "./components/loading-spinner/loading-spinner.component";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
@@ -12,56 +14,18 @@ import { persistor, store } from "./redux/store";
|
|||||||
import reportWebVitals from "./reportWebVitals";
|
import reportWebVitals from "./reportWebVitals";
|
||||||
import "./translations/i18n";
|
import "./translations/i18n";
|
||||||
import "./utils/CleanAxios";
|
import "./utils/CleanAxios";
|
||||||
import { ConfigProvider } from "antd";
|
|
||||||
import InstanceRenderManager from "./utils/instanceRenderMgr";
|
|
||||||
import { registerSW } from "virtual:pwa-register";
|
|
||||||
|
|
||||||
window.global ||= window;
|
window.global ||= window;
|
||||||
|
|
||||||
registerSW({ immediate: true });
|
registerSW({ immediate: true });
|
||||||
//import { BrowserTracing } from "@sentry/tracing";
|
|
||||||
//import "antd/dist/antd.css";
|
|
||||||
// import "antd/dist/antd.less";
|
|
||||||
|
|
||||||
// Dinero.defaultCurrency = "CAD";
|
// Dinero.defaultCurrency = "CAD";
|
||||||
// Dinero.globalLocale = "en-CA";
|
// Dinero.globalLocale = "en-CA";
|
||||||
Dinero.globalRoundingMode = "HALF_EVEN";
|
Dinero.globalRoundingMode = "HALF_EVEN";
|
||||||
|
|
||||||
if (import.meta.env.PROD) {
|
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV6(createBrowserRouter);
|
||||||
Sentry.init({
|
|
||||||
dsn: InstanceRenderManager({
|
|
||||||
imex: "https://fd7e89369b6b4bdc9c6c4c9f22fa4ee4@o492140.ingest.sentry.io/5651027",
|
|
||||||
rome: "https://a6acc91c073e414196014b8484627a61@o492140.ingest.sentry.io/4504561071161344"
|
|
||||||
}),
|
|
||||||
|
|
||||||
ignoreErrors: [
|
const router = sentryCreateBrowserRouter(createRoutesFromElements(<Route path="*" element={<AppContainer />} />));
|
||||||
"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 />} />));
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
let styles =
|
let styles =
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ import { DateTimeFormat } from "../../utils/DateFormatter";
|
|||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import UndefinedToNull from "../../utils/undefinedtonull";
|
import UndefinedToNull from "../../utils/undefinedtonull";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||||
|
import JobWatcherToggleContainer from "../../components/job-watcher-toggle/job-watcher-toggle.container.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -102,6 +104,7 @@ export function JobsDetailPage({
|
|||||||
nextFetchPolicy: "network-only"
|
nextFetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
const { scenarioNotificationsOn } = useSocket();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
//form.setFieldsValue(transormJobToForm(job));
|
//form.setFieldsValue(transormJobToForm(job));
|
||||||
@@ -319,7 +322,13 @@ export function JobsDetailPage({
|
|||||||
>
|
>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
// onBack={() => window.history.back()}
|
// onBack={() => window.history.back()}
|
||||||
title={job.ro_number || t("general.labels.na")}
|
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
{scenarioNotificationsOn && <JobWatcherToggleContainer job={job} />}
|
||||||
|
{job.ro_number || t("general.labels.na")}
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
extra={menuExtra}
|
extra={menuExtra}
|
||||||
/>
|
/>
|
||||||
<JobsDetailHeader job={job} />
|
<JobsDetailHeader job={job} />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FloatButton, Layout, Spin } from "antd";
|
import { FloatButton, Layout, Spin } from "antd";
|
||||||
|
|
||||||
// import preval from "preval.macro";
|
// import preval from "preval.macro";
|
||||||
import React, { lazy, Suspense, useContext, useEffect, useState } from "react";
|
import React, { lazy, Suspense, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, Route, Routes } from "react-router-dom";
|
import { Link, Route, Routes } from "react-router-dom";
|
||||||
@@ -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 PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container";
|
||||||
import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component";
|
import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component";
|
||||||
import { requestForToken } from "../../firebase/firebase.utils";
|
import { requestForToken } from "../../firebase/firebase.utils";
|
||||||
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||||
import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors";
|
||||||
import UpdateAlert from "../../components/update-alert/update-alert.component";
|
import UpdateAlert from "../../components/update-alert/update-alert.component";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
||||||
@@ -29,6 +29,7 @@ import WssStatusDisplayComponent from "../../components/wss-status-display/wss-s
|
|||||||
import { selectAlerts } from "../../redux/application/application.selectors.js";
|
import { selectAlerts } from "../../redux/application/application.selectors.js";
|
||||||
import { addAlerts } from "../../redux/application/application.actions.js";
|
import { addAlerts } from "../../redux/application/application.actions.js";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
|
||||||
const JobsPage = lazy(() => import("../jobs/jobs.page"));
|
const JobsPage = lazy(() => import("../jobs/jobs.page"));
|
||||||
|
|
||||||
const CardPaymentModalContainer = lazy(
|
const CardPaymentModalContainer = lazy(
|
||||||
@@ -122,7 +123,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [chatVisible] = useState(false);
|
const [chatVisible] = useState(false);
|
||||||
const { socket, clientId } = useContext(SocketContext);
|
const { socket, clientId } = useSocket();
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|
||||||
// State to track displayed alerts
|
// State to track displayed alerts
|
||||||
@@ -142,11 +143,11 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
|||||||
const fetchedAlerts = await response.json();
|
const fetchedAlerts = await response.json();
|
||||||
setAlerts(fetchedAlerts);
|
setAlerts(fetchedAlerts);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching alerts:", error);
|
console.warn("Error fetching alerts:", error.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchAlerts();
|
fetchAlerts().catch((err) => `Error fetching Bodyshop Alerts: ${err?.message || ""}`);
|
||||||
}, [setAlerts]);
|
}, [setAlerts]);
|
||||||
|
|
||||||
// Use useEffect to watch for new alerts
|
// Use useEffect to watch for new alerts
|
||||||
@@ -166,7 +167,6 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
|||||||
description: alert.description,
|
description: alert.description,
|
||||||
type: alert.type || "info",
|
type: alert.type || "info",
|
||||||
duration: 0,
|
duration: 0,
|
||||||
placement: "bottomRight",
|
|
||||||
closable: true,
|
closable: true,
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
// When the notification is closed, update displayed alerts state and localStorage
|
// When the notification is closed, update displayed alerts state and localStorage
|
||||||
|
|||||||
@@ -347,8 +347,11 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
|||||||
window.$zoho.salesiq.visitor.info({ "Shop Name": payload.shopname });
|
window.$zoho.salesiq.visitor.info({ "Shop Name": payload.shopname });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
payload.features?.allAccess === true
|
||||||
|
? window.$crisp.push(["set", "session:segments", [["allAccess"]]])
|
||||||
|
: window.$crisp.push(["set", "session:segments", [["basic"]]]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Couldnt find $crisp.");
|
console.warn("Couldnt find $crisp.", error.message);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
yield put(signInFailure(error.message));
|
yield put(signInFailure(error.message));
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,9 +7,9 @@ import { getMainDefinition } from "@apollo/client/utilities";
|
|||||||
//import { split } from "apollo-link";
|
//import { split } from "apollo-link";
|
||||||
import apolloLogger from "apollo-link-logger";
|
import apolloLogger from "apollo-link-logger";
|
||||||
//import axios from "axios";
|
//import axios from "axios";
|
||||||
|
import { SentryLink } from "apollo-link-sentry";
|
||||||
import { auth } from "../firebase/firebase.utils";
|
import { auth } from "../firebase/firebase.utils";
|
||||||
import errorLink from "../graphql/apollo-error-handling";
|
import errorLink from "../graphql/apollo-error-handling";
|
||||||
import { SentryLink } from "apollo-link-sentry";
|
|
||||||
|
|
||||||
//import { store } from "../redux/store";
|
//import { store } from "../redux/store";
|
||||||
const httpLink = new HttpLink({
|
const httpLink = new HttpLink({
|
||||||
@@ -143,7 +143,41 @@ middlewares.push(
|
|||||||
new SentryLink().concat(roundTripLink.concat(retryLink.concat(errorLink.concat(authLink.concat(link)))))
|
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({
|
const client = new ApolloClient({
|
||||||
link: ApolloLink.from(middlewares),
|
link: ApolloLink.from(middlewares),
|
||||||
cache,
|
cache,
|
||||||
@@ -163,4 +197,5 @@ const client = new ApolloClient({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default client;
|
export default client;
|
||||||
|
|||||||
23
client/src/utils/jobNotificationScenarios.js
Normal file
23
client/src/utils/jobNotificationScenarios.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/** 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",
|
||||||
|
"schedule-dates-changed",
|
||||||
|
"tasks-updated-created",
|
||||||
|
"new-media-added-reassigned",
|
||||||
|
"new-time-ticket-posted",
|
||||||
|
"intake-delivery-checklist-completed",
|
||||||
|
"job-added-to-production",
|
||||||
|
"job-status-change",
|
||||||
|
"payment-collected-completed",
|
||||||
|
"alternate-transport-changed"
|
||||||
|
// "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 react from "@vitejs/plugin-react";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import * as child from "child_process";
|
||||||
import { promises as fsPromises } from "fs";
|
import { promises as fsPromises } from "fs";
|
||||||
import { createLogger, defineConfig } from "vite";
|
import { createLogger, defineConfig } from "vite";
|
||||||
import { ViteEjsPlugin } from "vite-plugin-ejs";
|
import { ViteEjsPlugin } from "vite-plugin-ejs";
|
||||||
import eslint from "vite-plugin-eslint";
|
import eslint from "vite-plugin-eslint";
|
||||||
import { VitePWA } from "vite-plugin-pwa";
|
import { VitePWA } from "vite-plugin-pwa";
|
||||||
import InstanceRenderManager from "./src/utils/instanceRenderMgr";
|
import InstanceRenderManager from "./src/utils/instanceRenderMgr";
|
||||||
import chalk from "chalk";
|
|
||||||
|
|
||||||
// Ensure your environment variables are set correctly for Vite 6
|
// Ensure your environment variables are set correctly for Vite 6
|
||||||
process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", {
|
process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", {
|
||||||
timeZone: "America/Los_Angeles"
|
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 = () =>
|
const getFormattedTimestamp = () =>
|
||||||
new Date().toLocaleTimeString("en-US", { hour12: true }).replace("AM", "a.m.").replace("PM", "p.m.");
|
new Date().toLocaleTimeString("en-US", { hour12: true }).replace("AM", "a.m.").replace("PM", "p.m.");
|
||||||
@@ -78,10 +93,25 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
react(),
|
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: {
|
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: {
|
server: {
|
||||||
host: true,
|
host: true,
|
||||||
@@ -184,7 +214,9 @@ export default defineConfig({
|
|||||||
"libphonenumber-js": ["libphonenumber-js"]
|
"libphonenumber-js": ["libphonenumber-js"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
|
sourcemap: true
|
||||||
},
|
},
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
include: [
|
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:
|
||||||
@@ -31,14 +31,6 @@
|
|||||||
headers:
|
headers:
|
||||||
- name: x-imex-auth
|
- name: x-imex-auth
|
||||||
value_from_env: DATAPUMP_AUTH
|
value_from_env: DATAPUMP_AUTH
|
||||||
- name: Task Reminders
|
|
||||||
webhook: '{{HASURA_API_URL}}/tasks-remind-handler'
|
|
||||||
schedule: '*/15 * * * *'
|
|
||||||
include_in_metadata: true
|
|
||||||
payload: {}
|
|
||||||
headers:
|
|
||||||
- name: event-secret
|
|
||||||
value_from_env: EVENT_SECRET
|
|
||||||
- name: Rome Usage Report
|
- name: Rome Usage Report
|
||||||
webhook: '{{HASURA_API_URL}}/data/usagereport'
|
webhook: '{{HASURA_API_URL}}/data/usagereport'
|
||||||
schedule: 0 12 * * 5
|
schedule: 0 12 * * 5
|
||||||
@@ -47,3 +39,11 @@
|
|||||||
headers:
|
headers:
|
||||||
- name: x-imex-auth
|
- name: x-imex-auth
|
||||||
value_from_env: DATAPUMP_AUTH
|
value_from_env: DATAPUMP_AUTH
|
||||||
|
- name: Task Reminders
|
||||||
|
webhook: '{{HASURA_API_URL}}/tasks-remind-handler'
|
||||||
|
schedule: '*/15 * * * *'
|
||||||
|
include_in_metadata: true
|
||||||
|
payload: {}
|
||||||
|
headers:
|
||||||
|
- name: event-secret
|
||||||
|
value_from_env: EVENT_SECRET
|
||||||
|
|||||||
@@ -198,6 +198,14 @@
|
|||||||
- name: user
|
- name: user
|
||||||
using:
|
using:
|
||||||
foreign_key_constraint_on: useremail
|
foreign_key_constraint_on: useremail
|
||||||
|
array_relationships:
|
||||||
|
- name: notifications
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on:
|
||||||
|
column: associationid
|
||||||
|
table:
|
||||||
|
name: notifications
|
||||||
|
schema: public
|
||||||
select_permissions:
|
select_permissions:
|
||||||
- role: user
|
- role: user
|
||||||
permission:
|
permission:
|
||||||
@@ -697,12 +705,6 @@
|
|||||||
- name: event-secret
|
- name: event-secret
|
||||||
value_from_env: EVENT_SECRET
|
value_from_env: EVENT_SECRET
|
||||||
request_transform:
|
request_transform:
|
||||||
body:
|
|
||||||
action: transform
|
|
||||||
template: |-
|
|
||||||
{
|
|
||||||
"success": true
|
|
||||||
}
|
|
||||||
method: POST
|
method: POST
|
||||||
query_params: {}
|
query_params: {}
|
||||||
template_engine: Kriti
|
template_engine: Kriti
|
||||||
@@ -1133,6 +1135,46 @@
|
|||||||
- active:
|
- active:
|
||||||
_eq: true
|
_eq: true
|
||||||
check: null
|
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:
|
- table:
|
||||||
name: cccontracts
|
name: cccontracts
|
||||||
schema: public
|
schema: public
|
||||||
@@ -1958,6 +2000,29 @@
|
|||||||
_eq: X-Hasura-User-Id
|
_eq: X-Hasura-User-Id
|
||||||
- active:
|
- active:
|
||||||
_eq: true
|
_eq: true
|
||||||
|
event_triggers:
|
||||||
|
- name: notifications_documents
|
||||||
|
definition:
|
||||||
|
enable_manual: false
|
||||||
|
insert:
|
||||||
|
columns: '*'
|
||||||
|
update:
|
||||||
|
columns:
|
||||||
|
- jobid
|
||||||
|
retry_conf:
|
||||||
|
interval_sec: 10
|
||||||
|
num_retries: 0
|
||||||
|
timeout_sec: 60
|
||||||
|
webhook_from_env: HASURA_API_URL
|
||||||
|
headers:
|
||||||
|
- name: event-secret
|
||||||
|
value_from_env: EVENT_SECRET
|
||||||
|
request_transform:
|
||||||
|
method: POST
|
||||||
|
query_params: {}
|
||||||
|
template_engine: Kriti
|
||||||
|
url: '{{$base_url}}/notifications/events/handleDocumentsChange'
|
||||||
|
version: 2
|
||||||
- table:
|
- table:
|
||||||
name: email_audit_trail
|
name: email_audit_trail
|
||||||
schema: public
|
schema: public
|
||||||
@@ -2846,13 +2911,12 @@
|
|||||||
- role: user
|
- role: user
|
||||||
permission:
|
permission:
|
||||||
check:
|
check:
|
||||||
user:
|
job:
|
||||||
_and:
|
bodyshop:
|
||||||
- associations:
|
associations:
|
||||||
active:
|
user:
|
||||||
_eq: true
|
authid:
|
||||||
- authid:
|
_eq: X-Hasura-User-Id
|
||||||
_eq: X-Hasura-User-Id
|
|
||||||
columns:
|
columns:
|
||||||
- user_email
|
- user_email
|
||||||
- created_at
|
- created_at
|
||||||
@@ -2868,13 +2932,12 @@
|
|||||||
- id
|
- id
|
||||||
- jobid
|
- jobid
|
||||||
filter:
|
filter:
|
||||||
user:
|
job:
|
||||||
_and:
|
bodyshop:
|
||||||
- associations:
|
associations:
|
||||||
active:
|
user:
|
||||||
_eq: true
|
authid:
|
||||||
- authid:
|
_eq: X-Hasura-User-Id
|
||||||
_eq: X-Hasura-User-Id
|
|
||||||
comment: ""
|
comment: ""
|
||||||
update_permissions:
|
update_permissions:
|
||||||
- role: user
|
- role: user
|
||||||
@@ -2885,26 +2948,24 @@
|
|||||||
- id
|
- id
|
||||||
- jobid
|
- jobid
|
||||||
filter:
|
filter:
|
||||||
user:
|
job:
|
||||||
_and:
|
bodyshop:
|
||||||
- associations:
|
associations:
|
||||||
active:
|
user:
|
||||||
_eq: true
|
authid:
|
||||||
- authid:
|
_eq: X-Hasura-User-Id
|
||||||
_eq: X-Hasura-User-Id
|
|
||||||
check: null
|
check: null
|
||||||
comment: ""
|
comment: ""
|
||||||
delete_permissions:
|
delete_permissions:
|
||||||
- role: user
|
- role: user
|
||||||
permission:
|
permission:
|
||||||
filter:
|
filter:
|
||||||
user:
|
job:
|
||||||
_and:
|
bodyshop:
|
||||||
- associations:
|
associations:
|
||||||
active:
|
user:
|
||||||
_eq: true
|
authid:
|
||||||
- authid:
|
_eq: X-Hasura-User-Id
|
||||||
_eq: X-Hasura-User-Id
|
|
||||||
comment: ""
|
comment: ""
|
||||||
- table:
|
- table:
|
||||||
name: joblines
|
name: joblines
|
||||||
@@ -3223,6 +3284,31 @@
|
|||||||
_eq: X-Hasura-User-Id
|
_eq: X-Hasura-User-Id
|
||||||
- active:
|
- active:
|
||||||
_eq: true
|
_eq: true
|
||||||
|
event_triggers:
|
||||||
|
- name: notifications_joblines
|
||||||
|
definition:
|
||||||
|
enable_manual: false
|
||||||
|
update:
|
||||||
|
columns:
|
||||||
|
- critical
|
||||||
|
- status
|
||||||
|
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: "{\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: 2
|
||||||
- table:
|
- table:
|
||||||
name: joblines_status
|
name: joblines_status
|
||||||
schema: public
|
schema: public
|
||||||
@@ -3369,6 +3455,13 @@
|
|||||||
table:
|
table:
|
||||||
name: job_conversations
|
name: job_conversations
|
||||||
schema: public
|
schema: public
|
||||||
|
- name: job_watchers
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on:
|
||||||
|
column: jobid
|
||||||
|
table:
|
||||||
|
name: job_watchers
|
||||||
|
schema: public
|
||||||
- name: joblines
|
- name: joblines
|
||||||
using:
|
using:
|
||||||
foreign_key_constraint_on:
|
foreign_key_constraint_on:
|
||||||
@@ -3399,6 +3492,13 @@
|
|||||||
table:
|
table:
|
||||||
name: notes
|
name: notes
|
||||||
schema: public
|
schema: public
|
||||||
|
- name: notifications
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on:
|
||||||
|
column: jobid
|
||||||
|
table:
|
||||||
|
name: notifications
|
||||||
|
schema: public
|
||||||
- name: parts_dispatches
|
- name: parts_dispatches
|
||||||
using:
|
using:
|
||||||
foreign_key_constraint_on:
|
foreign_key_constraint_on:
|
||||||
@@ -4473,10 +4573,7 @@
|
|||||||
request_transform:
|
request_transform:
|
||||||
body:
|
body:
|
||||||
action: transform
|
action: transform
|
||||||
template: |-
|
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"
|
||||||
{
|
|
||||||
"success": true
|
|
||||||
}
|
|
||||||
method: POST
|
method: POST
|
||||||
query_params: {}
|
query_params: {}
|
||||||
template_engine: Kriti
|
template_engine: Kriti
|
||||||
@@ -4825,6 +4922,26 @@
|
|||||||
_eq: X-Hasura-User-Id
|
_eq: X-Hasura-User-Id
|
||||||
- active:
|
- active:
|
||||||
_eq: true
|
_eq: true
|
||||||
|
event_triggers:
|
||||||
|
- name: notifications_notes
|
||||||
|
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:
|
||||||
|
method: POST
|
||||||
|
query_params: {}
|
||||||
|
template_engine: Kriti
|
||||||
|
url: '{{$base_url}}/notifications/events/handleNotesChange'
|
||||||
|
version: 2
|
||||||
- table:
|
- table:
|
||||||
name: notifications
|
name: notifications
|
||||||
schema: public
|
schema: public
|
||||||
@@ -4835,46 +4952,79 @@
|
|||||||
- name: job
|
- name: job
|
||||||
using:
|
using:
|
||||||
foreign_key_constraint_on: jobid
|
foreign_key_constraint_on: jobid
|
||||||
|
insert_permissions:
|
||||||
|
- role: user
|
||||||
|
permission:
|
||||||
|
check:
|
||||||
|
job:
|
||||||
|
bodyshop:
|
||||||
|
associations:
|
||||||
|
_and:
|
||||||
|
- user:
|
||||||
|
authid:
|
||||||
|
_eq: X-Hasura-User-Id
|
||||||
|
- active:
|
||||||
|
_eq: true
|
||||||
|
columns:
|
||||||
|
- scenario_meta
|
||||||
|
- scenario_text
|
||||||
|
- fcm_text
|
||||||
|
- created_at
|
||||||
|
- read
|
||||||
|
- updated_at
|
||||||
|
- associationid
|
||||||
|
- id
|
||||||
|
- jobid
|
||||||
|
comment: ""
|
||||||
select_permissions:
|
select_permissions:
|
||||||
- role: user
|
- role: user
|
||||||
permission:
|
permission:
|
||||||
columns:
|
columns:
|
||||||
- associationid
|
- scenario_meta
|
||||||
|
- scenario_text
|
||||||
|
- fcm_text
|
||||||
- created_at
|
- created_at
|
||||||
- fcm_data
|
- read
|
||||||
- fcm_message
|
- updated_at
|
||||||
- fcm_title
|
- associationid
|
||||||
- id
|
- id
|
||||||
- jobid
|
- jobid
|
||||||
- meta
|
|
||||||
- read
|
|
||||||
- ui_translation_meta
|
|
||||||
- ui_translation_string
|
|
||||||
- updated_at
|
|
||||||
filter:
|
filter:
|
||||||
association:
|
job:
|
||||||
_and:
|
bodyshop:
|
||||||
- active:
|
associations:
|
||||||
_eq: true
|
_and:
|
||||||
- user:
|
- user:
|
||||||
authid:
|
authid:
|
||||||
_eq: X-Hasura-User-Id
|
_eq: X-Hasura-User-Id
|
||||||
|
- active:
|
||||||
|
_eq: true
|
||||||
|
allow_aggregations: true
|
||||||
comment: ""
|
comment: ""
|
||||||
update_permissions:
|
update_permissions:
|
||||||
- role: user
|
- role: user
|
||||||
permission:
|
permission:
|
||||||
columns:
|
columns:
|
||||||
- meta
|
- scenario_meta
|
||||||
|
- scenario_text
|
||||||
|
- fcm_text
|
||||||
|
- created_at
|
||||||
- read
|
- read
|
||||||
filter:
|
- updated_at
|
||||||
association:
|
- associationid
|
||||||
_and:
|
- id
|
||||||
- active:
|
- jobid
|
||||||
_eq: true
|
filter: {}
|
||||||
- user:
|
check:
|
||||||
authid:
|
job:
|
||||||
_eq: X-Hasura-User-Id
|
bodyshop:
|
||||||
check: null
|
associations:
|
||||||
|
_and:
|
||||||
|
- user:
|
||||||
|
authid:
|
||||||
|
_eq: X-Hasura-User-Id
|
||||||
|
- active:
|
||||||
|
_eq: true
|
||||||
comment: ""
|
comment: ""
|
||||||
- table:
|
- table:
|
||||||
name: owners
|
name: owners
|
||||||
@@ -5116,32 +5266,6 @@
|
|||||||
- active:
|
- active:
|
||||||
_eq: true
|
_eq: true
|
||||||
check: null
|
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:
|
- table:
|
||||||
name: parts_dispatch_lines
|
name: parts_dispatch_lines
|
||||||
schema: public
|
schema: public
|
||||||
@@ -5648,6 +5772,25 @@
|
|||||||
- active:
|
- active:
|
||||||
_eq: true
|
_eq: true
|
||||||
event_triggers:
|
event_triggers:
|
||||||
|
- name: notifications_payments
|
||||||
|
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:
|
||||||
|
method: POST
|
||||||
|
query_params: {}
|
||||||
|
template_engine: Kriti
|
||||||
|
url: '{{$base_url}}/notifications/events/handlePaymentsChange'
|
||||||
|
version: 2
|
||||||
- name: os_payments
|
- name: os_payments
|
||||||
definition:
|
definition:
|
||||||
delete:
|
delete:
|
||||||
@@ -6119,9 +6262,15 @@
|
|||||||
columns: '*'
|
columns: '*'
|
||||||
update:
|
update:
|
||||||
columns:
|
columns:
|
||||||
|
- joblineid
|
||||||
- assigned_to
|
- assigned_to
|
||||||
|
- due_date
|
||||||
|
- partsorderid
|
||||||
- completed
|
- completed
|
||||||
- description
|
- description
|
||||||
|
- billid
|
||||||
|
- title
|
||||||
|
- priority
|
||||||
retry_conf:
|
retry_conf:
|
||||||
interval_sec: 10
|
interval_sec: 10
|
||||||
num_retries: 0
|
num_retries: 0
|
||||||
@@ -6131,12 +6280,6 @@
|
|||||||
- name: event-secret
|
- name: event-secret
|
||||||
value_from_env: EVENT_SECRET
|
value_from_env: EVENT_SECRET
|
||||||
request_transform:
|
request_transform:
|
||||||
body:
|
|
||||||
action: transform
|
|
||||||
template: |-
|
|
||||||
{
|
|
||||||
"success": true
|
|
||||||
}
|
|
||||||
method: POST
|
method: POST
|
||||||
query_params: {}
|
query_params: {}
|
||||||
template_engine: Kriti
|
template_engine: Kriti
|
||||||
@@ -6313,12 +6456,6 @@
|
|||||||
- name: event-secret
|
- name: event-secret
|
||||||
value_from_env: EVENT_SECRET
|
value_from_env: EVENT_SECRET
|
||||||
request_transform:
|
request_transform:
|
||||||
body:
|
|
||||||
action: transform
|
|
||||||
template: |-
|
|
||||||
{
|
|
||||||
"success": true
|
|
||||||
}
|
|
||||||
method: POST
|
method: POST
|
||||||
query_params: {}
|
query_params: {}
|
||||||
template_engine: Kriti
|
template_engine: Kriti
|
||||||
@@ -6586,6 +6723,13 @@
|
|||||||
table:
|
table:
|
||||||
name: ioevents
|
name: ioevents
|
||||||
schema: public
|
schema: public
|
||||||
|
- name: job_watchers
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on:
|
||||||
|
column: user_email
|
||||||
|
table:
|
||||||
|
name: job_watchers
|
||||||
|
schema: public
|
||||||
- name: messages
|
- name: messages
|
||||||
using:
|
using:
|
||||||
foreign_key_constraint_on:
|
foreign_key_constraint_on:
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."notifications" add column "html_body" text
|
||||||
|
-- not null;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."notifications" add column "html_body" text
|
||||||
|
not null;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."notifications" alter column "fcm_title" set not null;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."notifications" alter column "fcm_title" drop not null;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."notifications" alter column "fcm_message" set not null;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."notifications" alter column "fcm_message" drop not null;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
comment on column "public"."notifications"."html_body" is E'Real Time Notifications System';
|
||||||
|
alter table "public"."notifications" alter column "html_body" drop not null;
|
||||||
|
alter table "public"."notifications" add column "html_body" text;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."notifications" drop column "html_body" cascade;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
comment on column "public"."notifications"."fcm_data" is E'Real Time Notifications System';
|
||||||
|
alter table "public"."notifications" alter column "fcm_data" set default jsonb_build_object();
|
||||||
|
alter table "public"."notifications" alter column "fcm_data" drop not null;
|
||||||
|
alter table "public"."notifications" add column "fcm_data" jsonb;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."notifications" drop column "fcm_data" cascade;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
comment on column "public"."notifications"."fcm_message" is E'Real Time Notifications System';
|
||||||
|
alter table "public"."notifications" alter column "fcm_message" drop not null;
|
||||||
|
alter table "public"."notifications" add column "fcm_message" text;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."notifications" drop column "fcm_message" cascade;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
comment on column "public"."notifications"."ui_translation_string" is E'Real Time Notifications System';
|
||||||
|
alter table "public"."notifications" alter column "ui_translation_string" drop not null;
|
||||||
|
alter table "public"."notifications" add column "ui_translation_string" text;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."notifications" drop column "ui_translation_string" cascade;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."notifications" rename column "fcm_text" to "fcm_title";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."notifications" rename column "fcm_title" to "fcm_text";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."notifications" rename column "scenario_text" to "ui_translation_meta";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."notifications" rename column "ui_translation_meta" to "scenario_text";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."notifications" rename column "scenario_meta" to "meta";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."notifications" rename column "meta" to "scenario_meta";
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1320
package-lock.json
generated
1320
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
42
package.json
42
package.json
@@ -19,39 +19,41 @@
|
|||||||
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\""
|
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-cloudwatch-logs": "^3.738.0",
|
"@aws-sdk/client-cloudwatch-logs": "^3.758.0",
|
||||||
"@aws-sdk/client-elasticache": "^3.738.0",
|
"@aws-sdk/client-elasticache": "^3.758.0",
|
||||||
"@aws-sdk/client-s3": "^3.738.0",
|
"@aws-sdk/client-s3": "^3.758.0",
|
||||||
"@aws-sdk/client-secrets-manager": "^3.738.0",
|
"@aws-sdk/client-secrets-manager": "^3.758.0",
|
||||||
"@aws-sdk/client-ses": "^3.738.0",
|
"@aws-sdk/client-ses": "^3.758.0",
|
||||||
"@aws-sdk/credential-provider-node": "^3.738.0",
|
"@aws-sdk/credential-provider-node": "^3.758.0",
|
||||||
"@opensearch-project/opensearch": "^2.13.0",
|
"@opensearch-project/opensearch": "^2.13.0",
|
||||||
"@socket.io/admin-ui": "^0.5.1",
|
"@socket.io/admin-ui": "^0.5.1",
|
||||||
"@socket.io/redis-adapter": "^8.3.0",
|
"@socket.io/redis-adapter": "^8.3.0",
|
||||||
"aws4": "^1.13.2",
|
"aws4": "^1.13.2",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.8.1",
|
||||||
|
"bee-queue": "^1.7.1",
|
||||||
"better-queue": "^3.8.12",
|
"better-queue": "^3.8.12",
|
||||||
"bluebird": "^3.7.2",
|
"bluebird": "^3.7.2",
|
||||||
"body-parser": "^1.20.3",
|
"body-parser": "^1.20.3",
|
||||||
"chart.js": "^4.4.6",
|
"bullmq": "^5.41.7",
|
||||||
|
"chart.js": "^4.4.8",
|
||||||
"cloudinary": "^2.5.1",
|
"cloudinary": "^2.5.1",
|
||||||
"compression": "^1.7.5",
|
"compression": "^1.8.0",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"crisp-status-reporter": "^1.2.2",
|
"crisp-status-reporter": "^1.2.2",
|
||||||
"csrf": "^3.1.0",
|
"csrf": "^3.1.0",
|
||||||
"dd-trace": "^5.33.1",
|
"dd-trace": "^5.40.0",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
"firebase-admin": "^13.0.2",
|
"firebase-admin": "^13.1.0",
|
||||||
"graphql": "^16.10.0",
|
"graphql": "^16.10.0",
|
||||||
"graphql-request": "^6.1.0",
|
"graphql-request": "^6.1.0",
|
||||||
"inline-css": "^4.0.3",
|
"inline-css": "^4.0.3",
|
||||||
"intuit-oauth": "^4.1.3",
|
"intuit-oauth": "^4.2.0",
|
||||||
"ioredis": "^5.4.2",
|
"ioredis": "^5.5.0",
|
||||||
"json-2-csv": "^5.5.8",
|
"json-2-csv": "^5.5.8",
|
||||||
"juice": "^11.0.0",
|
"juice": "^11.0.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"moment-timezone": "^0.5.47",
|
"moment-timezone": "^0.5.47",
|
||||||
@@ -64,7 +66,7 @@
|
|||||||
"redis": "^4.7.0",
|
"redis": "^4.7.0",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"skia-canvas": "^2.0.2",
|
"skia-canvas": "^2.0.2",
|
||||||
"soap": "^1.1.7",
|
"soap": "^1.1.9",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"socket.io-adapter": "^2.5.5",
|
"socket.io-adapter": "^2.5.5",
|
||||||
"ssh2-sftp-client": "^11.0.0",
|
"ssh2-sftp-client": "^11.0.0",
|
||||||
@@ -76,14 +78,14 @@
|
|||||||
"xmlbuilder2": "^3.1.1"
|
"xmlbuilder2": "^3.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.19.0",
|
"@eslint/js": "^9.21.0",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
"eslint": "^9.19.0",
|
"eslint": "^9.21.0",
|
||||||
"eslint-plugin-react": "^7.37.4",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"globals": "^15.14.0",
|
"globals": "^15.15.0",
|
||||||
"p-limit": "^3.1.0",
|
"p-limit": "^3.1.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.5.3",
|
||||||
"source-map-explorer": "^2.5.2"
|
"source-map-explorer": "^2.5.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user