Compare commits
227 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 | ||
|
|
d9f562faa4 | ||
|
|
14e362ec3f | ||
|
|
c213e13624 | ||
|
|
dae7642a8c | ||
|
|
c751f0cba4 | ||
|
|
e128c108f8 | ||
|
|
b8b76cb96c | ||
|
|
fa958cbbfe | ||
|
|
2146672916 | ||
|
|
f96460f332 | ||
|
|
04c70876d0 | ||
|
|
bc6a94eede | ||
|
|
f288b0ee22 | ||
|
|
e54692928b | ||
|
|
0fd8bcb1b1 | ||
|
|
07b18836f5 | ||
|
|
ff08d19d79 | ||
|
|
bd6f300c8d | ||
|
|
ac2fbaf6f7 | ||
|
|
f409acc7fd | ||
|
|
06dcb20b2b | ||
|
|
f4fed0db9d | ||
|
|
8430f500ef | ||
|
|
68584243f4 | ||
|
|
c8f5c3ed9e | ||
|
|
994d7e17aa | ||
|
|
fd1dd6dddd | ||
|
|
1e9b82ba1e | ||
|
|
312795618e | ||
|
|
35b5645d6f | ||
|
|
a49d845f50 | ||
|
|
9d44540ca8 | ||
|
|
cc95d3bd44 | ||
|
|
648c47cde2 | ||
|
|
17cf6e7696 | ||
|
|
7326ffbae6 | ||
|
|
b2f73c4fba | ||
|
|
6628d43e12 | ||
|
|
a064b8e07e | ||
|
|
da41668b3f | ||
|
|
df008abec9 | ||
|
|
29c99f2dd9 | ||
|
|
3033e84f45 | ||
|
|
18966476e4 |
@@ -9,13 +9,13 @@ orbs:
|
|||||||
jobs:
|
jobs:
|
||||||
imex-api-deploy:
|
imex-api-deploy:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:18.18.2
|
- image: cimg/node:22.13.1
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- eb/setup
|
- eb/setup
|
||||||
- run:
|
- run:
|
||||||
command: |
|
command: |
|
||||||
eb init imex-online-production-api -r ca-central-1 -p "Node.js 18 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
|
||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
|
|
||||||
imex-hasura-migrate:
|
imex-hasura-migrate:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:18.18.2
|
- image: cimg/node:22.13.1
|
||||||
parameters:
|
parameters:
|
||||||
secret:
|
secret:
|
||||||
type: string
|
type: string
|
||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
pipeline_number: << pipeline.number >>
|
pipeline_number: << pipeline.number >>
|
||||||
imex-app-build:
|
imex-app-build:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:18.18.2
|
- image: cimg/node:22.13.1
|
||||||
resource_class: large
|
resource_class: large
|
||||||
working_directory: ~/repo/client
|
working_directory: ~/repo/client
|
||||||
steps:
|
steps:
|
||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
|
|
||||||
imex-app-beta-build:
|
imex-app-beta-build:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:18.18.2
|
- image: cimg/node:22.13.1
|
||||||
resource_class: large
|
resource_class: large
|
||||||
working_directory: ~/repo/client
|
working_directory: ~/repo/client
|
||||||
|
|
||||||
@@ -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 18 running 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
|
||||||
@@ -126,7 +126,7 @@ jobs:
|
|||||||
pipeline_number: << pipeline.number >>
|
pipeline_number: << pipeline.number >>
|
||||||
rome-hasura-migrate:
|
rome-hasura-migrate:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:18.18.2
|
- image: cimg/node:22.13.1
|
||||||
parameters:
|
parameters:
|
||||||
secret:
|
secret:
|
||||||
type: string
|
type: string
|
||||||
@@ -150,8 +150,8 @@ jobs:
|
|||||||
pipeline_number: << pipeline.number >>
|
pipeline_number: << pipeline.number >>
|
||||||
rome-app-build:
|
rome-app-build:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:18.18.2
|
- 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
|
||||||
@@ -181,7 +181,7 @@ jobs:
|
|||||||
|
|
||||||
test-rome-hasura-migrate:
|
test-rome-hasura-migrate:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:18.18.2
|
- image: cimg/node:22.13.1
|
||||||
parameters:
|
parameters:
|
||||||
secret:
|
secret:
|
||||||
type: string
|
type: string
|
||||||
@@ -208,8 +208,8 @@ jobs:
|
|||||||
|
|
||||||
test-rome-app-build:
|
test-rome-app-build:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:18.18.2
|
- 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
|
||||||
@@ -239,7 +239,7 @@ jobs:
|
|||||||
|
|
||||||
test-hasura-migrate:
|
test-hasura-migrate:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:18.18.2
|
- image: cimg/node:22.13.1
|
||||||
parameters:
|
parameters:
|
||||||
secret:
|
secret:
|
||||||
type: string
|
type: string
|
||||||
@@ -266,7 +266,7 @@ jobs:
|
|||||||
|
|
||||||
imex-test-app-build:
|
imex-test-app-build:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:18.18.2
|
- image: cimg/node:22.13.1
|
||||||
resource_class: large
|
resource_class: large
|
||||||
working_directory: ~/repo/client
|
working_directory: ~/repo/client
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -286,7 +286,7 @@ jobs:
|
|||||||
|
|
||||||
imex-test-app-beta-build:
|
imex-test-app-beta-build:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:18.18.2
|
- image: cimg/node:22.13.1
|
||||||
resource_class: large
|
resource_class: large
|
||||||
working_directory: ~/repo/client
|
working_directory: ~/repo/client
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ FROM amazonlinux:2023
|
|||||||
|
|
||||||
# Install Git and Node.js (Amazon Linux 2023 uses the DNF package manager)
|
# Install Git and Node.js (Amazon Linux 2023 uses the DNF package manager)
|
||||||
RUN dnf install -y git \
|
RUN dnf install -y git \
|
||||||
&& curl -sL https://rpm.nodesource.com/setup_20.x | bash - \
|
&& curl -sL https://rpm.nodesource.com/setup_22.x | bash - \
|
||||||
&& dnf install -y nodejs \
|
&& dnf install -y nodejs \
|
||||||
&& dnf clean all
|
&& dnf clean all
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
9638
client/package-lock.json
generated
9638
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,77 +8,78 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"proxy": "http://localhost:4000",
|
"proxy": "http://localhost:4000",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/pro-layout": "^7.19.12",
|
"@ant-design/pro-layout": "^7.22.3",
|
||||||
"@apollo/client": "^3.11.8",
|
"@apollo/client": "^3.13.1",
|
||||||
"@emotion/is-prop-valid": "^1.3.1",
|
"@emotion/is-prop-valid": "^1.3.1",
|
||||||
"@fingerprintjs/fingerprintjs": "^4.5.0",
|
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
"@reduxjs/toolkit": "^2.2.7",
|
"@reduxjs/toolkit": "^2.6.0",
|
||||||
"@sentry/cli": "^2.36.2",
|
"@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.51",
|
"@tanem/react-nprogress": "^5.0.53",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"antd": "^5.20.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.7",
|
"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.2",
|
"dayjs-business-days2": "^1.3.0",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.7",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"firebase": "^10.13.2",
|
"firebase": "^10.13.2",
|
||||||
"graphql": "^16.9.0",
|
"graphql": "^16.10.0",
|
||||||
"i18next": "^23.15.1",
|
"i18next": "^23.15.1",
|
||||||
"i18next-browser-languagedetector": "^8.0.0",
|
"i18next-browser-languagedetector": "^8.0.4",
|
||||||
"immutability-helper": "^3.1.1",
|
"immutability-helper": "^3.1.1",
|
||||||
"libphonenumber-js": "^1.11.9",
|
"libphonenumber-js": "^1.12.4",
|
||||||
"logrocket": "^8.1.2",
|
"logrocket": "^8.1.2",
|
||||||
"markerjs2": "^2.32.2",
|
"markerjs2": "^2.32.3",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
"normalize-url": "^8.0.1",
|
"normalize-url": "^8.0.1",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"query-string": "^9.1.0",
|
"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.14.1",
|
"react-big-calendar": "^1.18.0",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-cookie": "^7.2.0",
|
"react-cookie": "^7.2.2",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-drag-listview": "^2.0.0",
|
"react-drag-listview": "^2.0.0",
|
||||||
"react-grid-gallery": "^1.0.1",
|
"react-grid-gallery": "^1.0.1",
|
||||||
"react-grid-layout": "1.3.4",
|
"react-grid-layout": "1.3.4",
|
||||||
"react-i18next": "^14.1.3",
|
"react-i18next": "^14.1.3",
|
||||||
"react-icons": "^5.3.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-image-lightbox": "^5.1.4",
|
"react-image-lightbox": "^5.1.4",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.3",
|
||||||
"react-number-format": "^5.4.2",
|
"react-number-format": "^5.4.3",
|
||||||
"react-popopo": "^2.1.9",
|
"react-popopo": "^2.1.9",
|
||||||
"react-product-fruits": "^2.2.61",
|
"react-product-fruits": "^2.2.61",
|
||||||
"react-redux": "^9.1.2",
|
"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.12.7",
|
"recharts": "^2.15.0",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"redux-actions": "^3.0.3",
|
"redux-actions": "^3.0.3",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"redux-saga": "^1.3.0",
|
"redux-saga": "^1.3.0",
|
||||||
"redux-state-sync": "^3.1.4",
|
"redux-state-sync": "^3.1.4",
|
||||||
"reselect": "^5.1.1",
|
"reselect": "^5.1.1",
|
||||||
"sass": "^1.79.3",
|
"sass": "^1.85.1",
|
||||||
"socket.io-client": "^4.8.0",
|
"socket.io-client": "^4.8.1",
|
||||||
"styled-components": "^6.1.13",
|
"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,36 +120,36 @@
|
|||||||
"@rollup/rollup-linux-x64-gnu": "4.6.1"
|
"@rollup/rollup-linux-x64-gnu": "4.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/icons": "^5.5.1",
|
"@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.24.7",
|
"@babel/preset-react": "^7.26.3",
|
||||||
"@dotenvx/dotenvx": "^1.14.1",
|
"@dotenvx/dotenvx": "^1.38.3",
|
||||||
"@emotion/babel-plugin": "^11.12.0",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
"@emotion/react": "^11.13.3",
|
"@emotion/react": "^11.14.0",
|
||||||
"@eslint/js": "^9.15.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.23.3",
|
"browserslist": "^4.24.4",
|
||||||
"browserslist-to-esbuild": "^2.1.1",
|
"browserslist-to-esbuild": "^2.1.1",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.4.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cypress": "^13.14.2",
|
"cypress": "^13.17.0",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
"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.2",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"globals": "^15.12.0",
|
"globals": "^15.15.0",
|
||||||
"memfs": "^4.12.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": "^5.4.7",
|
"vite": "^6.2.0",
|
||||||
"vite-plugin-babel": "^1.2.0",
|
"vite-plugin-babel": "^1.3.0",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-node-polyfills": "^0.22.0",
|
"vite-plugin-node-polyfills": "^0.23.0",
|
||||||
"vite-plugin-pwa": "^0.20.5",
|
"vite-plugin-pwa": "^0.21.1",
|
||||||
"vite-plugin-style-import": "^2.0.0",
|
"vite-plugin-style-import": "^2.0.0",
|
||||||
"workbox-window": "^7.1.0"
|
"workbox-window": "^7.3.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>
|
||||||
|
|||||||
@@ -5,6 +5,13 @@
|
|||||||
border-bottom: 1px solid #74695c !important;
|
border-bottom: 1px solid #74695c !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: This was added because the newest release of ant was making the text color and the background color the same on a selected header
|
||||||
|
// Tried all available tokens (https://ant.design/components/menu?locale=en-US) and even reverted all our custom styles, to no avail
|
||||||
|
// This should be kept an eye on, especially if implementing DARK MODE
|
||||||
|
.ant-menu-submenu-title {
|
||||||
|
color: rgba(255, 255, 255, 0.65) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.imex-table-header {
|
.imex-table-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -173,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"]({
|
||||||
|
|||||||
@@ -1,26 +1,23 @@
|
|||||||
import { Collapse, Form, Input, InputNumber, Select, Space, Switch } from "antd";
|
import { Collapse, Form, Input, InputNumber, Select, Space, Switch } from "antd";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
|
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 FormItemEmail from "../form-items-formatted/email-form-item.component";
|
import FormItemEmail from "../form-items-formatted/email-form-item.component";
|
||||||
import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
|
import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
|
||||||
import JobsDetailChangeEstimator from "../jobs-detail-change-estimator/jobs-detail-change-estimator.component";
|
import JobsDetailChangeEstimator from "../jobs-detail-change-estimator/jobs-detail-change-estimator.component";
|
||||||
import JobsDetailChangeFilehandler from "../jobs-detail-change-filehandler/jobs-detail-change-filehandler.component";
|
import JobsDetailChangeFilehandler from "../jobs-detail-change-filehandler/jobs-detail-change-filehandler.component";
|
||||||
import JobsDetailRatesChangeButton from "../jobs-detail-rates-change-button/jobs-detail-rates-change-button.component";
|
import JobsDetailRatesChangeButton from "../jobs-detail-rates-change-button/jobs-detail-rates-change-button.component";
|
||||||
import JobsDetailRatesParts from "../jobs-detail-rates/jobs-detail-rates.parts.component";
|
|
||||||
|
|
||||||
import JobsDetailRatesLabor from "../jobs-detail-rates/jobs-detail-rates.labor.component";
|
import JobsDetailRatesLabor from "../jobs-detail-rates/jobs-detail-rates.labor.component";
|
||||||
import JobsDetailRatesMaterials from "../jobs-detail-rates/jobs-detail-rates.materials.component";
|
import JobsDetailRatesMaterials from "../jobs-detail-rates/jobs-detail-rates.materials.component";
|
||||||
import JobsDetailRatesOther from "../jobs-detail-rates/jobs-detail-rates.other.component";
|
import JobsDetailRatesOther from "../jobs-detail-rates/jobs-detail-rates.other.component";
|
||||||
|
import JobsDetailRatesParts from "../jobs-detail-rates/jobs-detail-rates.parts.component";
|
||||||
import JobsDetailRatesTaxes from "../jobs-detail-rates/jobs-detail-rates.taxes.component";
|
import JobsDetailRatesTaxes from "../jobs-detail-rates/jobs-detail-rates.taxes.component";
|
||||||
|
|
||||||
import JobsMarkPstExempt from "../jobs-mark-pst-exempt/jobs-mark-pst-exempt.component";
|
import JobsMarkPstExempt from "../jobs-mark-pst-exempt/jobs-mark-pst-exempt.component";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
|
||||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
@@ -199,7 +196,9 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
|
|||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
<Collapse.Panel forceRender key="financial" header={t("menus.jobsdetail.financials")}>
|
<Collapse.Panel forceRender key="financial" header={t("menus.jobsdetail.financials")}>
|
||||||
<JobsDetailRatesChangeButton form={form} />
|
<JobsDetailRatesChangeButton form={form} />
|
||||||
<JobsMarkPstExempt form={form} />
|
{InstanceRenderManager({
|
||||||
|
imex: <JobsMarkPstExempt form={form} />
|
||||||
|
})}
|
||||||
<LayoutFormRow>
|
<LayoutFormRow>
|
||||||
<Form.Item label={t("jobs.fields.ded_amt")} name="ded_amt">
|
<Form.Item label={t("jobs.fields.ded_amt")} name="ded_amt">
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput min={0} />
|
||||||
@@ -246,7 +245,6 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
|
|||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<LayoutFormRow>
|
<LayoutFormRow>
|
||||||
<Form.Item label={t("jobs.fields.rate_lab")} name="rate_lab">
|
<Form.Item label={t("jobs.fields.rate_lab")} name="rate_lab">
|
||||||
<CurrencyInput />
|
<CurrencyInput />
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -32,6 +32,7 @@ import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
|||||||
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
||||||
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
|
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -129,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 {
|
||||||
@@ -201,7 +202,10 @@ export function JobsDetailHeaderActions({
|
|||||||
message: t("appointments.successes.created")
|
message: t("appointments.successes.created")
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notification.open({ type: "error", message: t("appointments.errors.saving", { error: error.message }) });
|
notification.open({
|
||||||
|
type: "error",
|
||||||
|
message: t("appointments.errors.saving", { error: error.message })
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setVisibility(false);
|
setVisibility(false);
|
||||||
@@ -838,7 +842,7 @@ export function JobsDetailHeaderActions({
|
|||||||
id: "job-actions-addtoproduction",
|
id: "job-actions-addtoproduction",
|
||||||
disabled: !job.converted,
|
disabled: !job.converted,
|
||||||
label: t("jobs.actions.addtoproduction"),
|
label: t("jobs.actions.addtoproduction"),
|
||||||
onClick: () => AddToProduction(client, job.id, refetch, notification)
|
onClick: () => AddToProduction(client, job.id, refetch, false, notification)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -971,6 +975,14 @@ export function JobsDetailHeaderActions({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (bodyshop?.md_functionality_toggles?.teams) {
|
||||||
|
menuItems.push({
|
||||||
|
key: "sharetoteams",
|
||||||
|
id: "job-actions-sharetoteams",
|
||||||
|
label: <ShareToTeamsButton noIcon={true} urlOverride={`${window.location.origin}${window.location.pathname}`} />
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
key: "exportcustdata",
|
key: "exportcustdata",
|
||||||
id: "job-actions-exportcustdata",
|
id: "job-actions-exportcustdata",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
|||||||
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
||||||
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser,
|
//currentUser: selectCurrentUser,
|
||||||
@@ -39,7 +40,7 @@ export function JobsDetailHeaderActionsToggleProduction({
|
|||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|
||||||
const [getJobDetails] = useLazyQuery(GET_JOB_BY_PK_QUICK_INTAKE, {
|
const [getJobDetails, { loading: jobDetailsLoading }] = useLazyQuery(GET_JOB_BY_PK_QUICK_INTAKE, {
|
||||||
variables: { id: job.id },
|
variables: { id: job.id },
|
||||||
onCompleted: (data) => {
|
onCompleted: (data) => {
|
||||||
if (data?.jobs_by_pk) {
|
if (data?.jobs_by_pk) {
|
||||||
@@ -109,65 +110,69 @@ export function JobsDetailHeaderActionsToggleProduction({
|
|||||||
|
|
||||||
const popMenu = (
|
const popMenu = (
|
||||||
<div onClick={(e) => e.stopPropagation()}>
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
<Form layout="vertical" form={form} onFinish={handleConvert}>
|
{jobDetailsLoading ? (
|
||||||
{scenario === "pre" && (
|
<LoadingSpinner />
|
||||||
<>
|
) : (
|
||||||
<Form.Item
|
<Form layout="vertical" form={form} onFinish={handleConvert}>
|
||||||
name={["actual_in"]}
|
{scenario === "pre" && (
|
||||||
label={t("jobs.fields.actual_in")}
|
<>
|
||||||
rules={[
|
<Form.Item
|
||||||
{
|
name={["actual_in"]}
|
||||||
required: true
|
label={t("jobs.fields.actual_in")}
|
||||||
//message: t("general.validation.required"),
|
rules={[
|
||||||
}
|
{
|
||||||
]}
|
required: true
|
||||||
>
|
//message: t("general.validation.required"),
|
||||||
<FormDateTimePickerComponent disabled={jobRO} />
|
}
|
||||||
</Form.Item>
|
]}
|
||||||
<Form.Item
|
>
|
||||||
name={["scheduled_completion"]}
|
<FormDateTimePickerComponent disabled={jobRO} />
|
||||||
label={t("jobs.fields.scheduled_completion")}
|
</Form.Item>
|
||||||
rules={[
|
<Form.Item
|
||||||
{
|
name={["scheduled_completion"]}
|
||||||
required: true
|
label={t("jobs.fields.scheduled_completion")}
|
||||||
//message: t("general.validation.required"),
|
rules={[
|
||||||
}
|
{
|
||||||
]}
|
required: true
|
||||||
>
|
//message: t("general.validation.required"),
|
||||||
<FormDateTimePickerComponent disabled={jobRO} />
|
}
|
||||||
</Form.Item>
|
]}
|
||||||
<Form.Item name={["scheduled_delivery"]} label={t("jobs.fields.scheduled_delivery")}>
|
>
|
||||||
<FormDateTimePickerComponent disabled={jobRO} />
|
<FormDateTimePickerComponent disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</>
|
<Form.Item name={["scheduled_delivery"]} label={t("jobs.fields.scheduled_delivery")}>
|
||||||
)}
|
<FormDateTimePickerComponent disabled={jobRO} />
|
||||||
{scenario === "prod" && (
|
</Form.Item>
|
||||||
<>
|
</>
|
||||||
<Form.Item
|
)}
|
||||||
name={["actual_completion"]}
|
{scenario === "prod" && (
|
||||||
label={t("jobs.fields.actual_completion")}
|
<>
|
||||||
rules={[
|
<Form.Item
|
||||||
{
|
name={["actual_completion"]}
|
||||||
required: true
|
label={t("jobs.fields.actual_completion")}
|
||||||
//message: t("general.validation.required"),
|
rules={[
|
||||||
}
|
{
|
||||||
]}
|
required: true
|
||||||
>
|
//message: t("general.validation.required"),
|
||||||
<FormDateTimePickerComponent disabled={jobRO} />
|
}
|
||||||
</Form.Item>
|
]}
|
||||||
|
>
|
||||||
|
<FormDateTimePickerComponent disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name={["actual_delivery"]} label={t("jobs.fields.actual_delivery")}>
|
<Form.Item name={["actual_delivery"]} label={t("jobs.fields.actual_delivery")}>
|
||||||
<FormDateTimePickerComponent disabled={jobRO} />
|
<FormDateTimePickerComponent disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button type="primary" onClick={() => form.submit()} loading={loading}>
|
<Button type="primary" onClick={() => form.submit()} loading={loading}>
|
||||||
{t("general.actions.save")}
|
{t("general.actions.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Form>
|
</Form>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { useApolloClient } from "@apollo/client";
|
import { useApolloClient } from "@apollo/client";
|
||||||
import { Button, Form, Popover, Space } from "antd";
|
import { Button, Form, Popover, Space } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import React, { 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 { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { GET_DOC_SIZE_BY_JOB } from "../../graphql/documents.queries";
|
import { GET_DOC_SIZE_BY_JOB } from "../../graphql/documents.queries";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import JobSearchSelect from "../job-search-select/job-search-select.component";
|
import JobSearchSelect from "../job-search-select/job-search-select.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -134,7 +134,7 @@ export function JobsDocumentsGalleryReassign({ bodyshop, galleryImages, callback
|
|||||||
]}
|
]}
|
||||||
name={"jobid"}
|
name={"jobid"}
|
||||||
>
|
>
|
||||||
<JobSearchSelect />
|
<JobSearchSelect notExported={false} notInvoiced={false} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
<Space>
|
<Space>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Button, Form, Popover, Space } from "antd";
|
import { Button, Form, Popover, Space } from "antd";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -60,7 +60,7 @@ export function JobsDocumentsLocalGalleryReassign({ bodyshop, jobid, allMedia, g
|
|||||||
]}
|
]}
|
||||||
name={"jobid"}
|
name={"jobid"}
|
||||||
>
|
>
|
||||||
<JobSearchSelect />
|
<JobSearchSelect notExported={false} notInvoiced={false}/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
<Space>
|
<Space>
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -19,6 +19,7 @@ import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
|||||||
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
|
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
|
||||||
import PrintWrapper from "../print-wrapper/print-wrapper.component";
|
import PrintWrapper from "../print-wrapper/print-wrapper.component";
|
||||||
import PartsOrderDrawer from "./parts-order-list-table-drawer.component";
|
import PartsOrderDrawer from "./parts-order-list-table-drawer.component";
|
||||||
|
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
@@ -66,19 +67,20 @@ export function PartsOrderListTableComponent({
|
|||||||
|
|
||||||
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
|
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
|
||||||
const { refetch } = billsQuery;
|
const { refetch } = billsQuery;
|
||||||
|
|
||||||
const recordActions = (record, showView = false) => (
|
const recordActions = (record, showView = false) => (
|
||||||
<Space direction="horizontal" wrap>
|
<Space direction="horizontal" wrap>
|
||||||
|
<ShareToTeamsButton
|
||||||
|
linkText={""}
|
||||||
|
urlOverride={`${window.location.origin}/manage/jobs/${job.id}?partsorderid=${record.id}&tab=partssublet `}
|
||||||
|
/>
|
||||||
{showView && (
|
{showView && (
|
||||||
<Button
|
<Button
|
||||||
|
icon={<EyeFilled />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleOnRowClick(record);
|
handleOnRowClick(record);
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<EyeFilled />
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={jobRO || record.return || record.vendor.id === bodyshop.inhousevendorid}
|
disabled={jobRO || record.return || record.vendor.id === bodyshop.inhousevendorid}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -106,6 +108,7 @@ export function PartsOrderListTableComponent({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
title={t("tasks.buttons.create")}
|
title={t("tasks.buttons.create")}
|
||||||
|
icon={<FaTasks />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTaskUpsertContext({
|
setTaskUpsertContext({
|
||||||
context: {
|
context: {
|
||||||
@@ -114,9 +117,7 @@ export function PartsOrderListTableComponent({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<FaTasks />
|
|
||||||
</Button>
|
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title={t("parts_orders.labels.confirmdelete")}
|
title={t("parts_orders.labels.confirmdelete")}
|
||||||
disabled={jobRO}
|
disabled={jobRO}
|
||||||
@@ -137,9 +138,7 @@ export function PartsOrderListTableComponent({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button disabled={jobRO}>
|
<Button disabled={jobRO} icon={<DeleteFilled />} />
|
||||||
<DeleteFilled />
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { Button, Input, Space, Spin } from "antd";
|
import { Button, Input, Space, Spin } from "antd";
|
||||||
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 { ExclamationCircleFilled, ExclamationCircleOutlined } from "@ant-design/icons";
|
import {
|
||||||
|
ExclamationCircleFilled,
|
||||||
|
ExclamationCircleOutlined,
|
||||||
|
UserDeleteOutlined,
|
||||||
|
UsergroupDeleteOutlined
|
||||||
|
} from "@ant-design/icons";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component";
|
import EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component";
|
||||||
|
|
||||||
@@ -19,12 +23,13 @@ export default connect(mapStateToProps, mapDispatchToProps)(ProductionBoardFilte
|
|||||||
|
|
||||||
export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading }) {
|
export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [alertFilter, setAlertFilter] = useState(false);
|
|
||||||
|
|
||||||
const toggleAlertFilter = () => {
|
const toggleAlertFilter = () => {
|
||||||
const newAlertFilter = !alertFilter;
|
setFilter({ ...filter, alert: !filter.alert });
|
||||||
setAlertFilter(newAlertFilter);
|
};
|
||||||
setFilter({ ...filter, alert: newAlertFilter });
|
|
||||||
|
const toggleUnassignedFilter = () => {
|
||||||
|
setFilter({ ...filter, unassigned: !filter.unassigned });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -46,12 +51,19 @@ export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading })
|
|||||||
allowClear
|
allowClear
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type={alertFilter ? "primary" : "default"}
|
type={filter?.alert ? "primary" : "default"}
|
||||||
onClick={toggleAlertFilter}
|
onClick={toggleAlertFilter}
|
||||||
icon={alertFilter ? <ExclamationCircleFilled /> : <ExclamationCircleOutlined />}
|
icon={filter?.alert ? <ExclamationCircleFilled /> : <ExclamationCircleOutlined />}
|
||||||
>
|
>
|
||||||
{t("production.labels.alerts")}
|
{t("production.labels.alerts")}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
type={filter?.unassigned ? "primary" : "default"}
|
||||||
|
onClick={toggleUnassignedFilter}
|
||||||
|
icon={filter?.unassigned ? <UserDeleteOutlined /> : <UsergroupDeleteOutlined />}
|
||||||
|
>
|
||||||
|
{t("production.labels.unassigned")}
|
||||||
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import dayjs from "../../utils/day";
|
|||||||
|
|
||||||
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
|
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
|
||||||
|
import { PiMicrosoftTeamsLogo } from "react-icons/pi";
|
||||||
|
|
||||||
const cardColor = (ssbuckets, totalHrs) => {
|
const cardColor = (ssbuckets, totalHrs) => {
|
||||||
const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > totalHrs));
|
const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > totalHrs));
|
||||||
@@ -417,9 +419,20 @@ export default function ProductionBoardCard({ technician, card, bodyshop, cardSe
|
|||||||
title={!isBodyEmpty ? headerContent : null}
|
title={!isBodyEmpty ? headerContent : null}
|
||||||
extra={
|
extra={
|
||||||
!isBodyEmpty && (
|
!isBodyEmpty && (
|
||||||
<Link to={{ search: `?selected=${card.id}` }}>
|
<Space>
|
||||||
<EyeFilled />
|
<ShareToTeamsButton
|
||||||
</Link>
|
noIcon={true}
|
||||||
|
linkText={
|
||||||
|
<div className="share-to-teams-badge">
|
||||||
|
<PiMicrosoftTeamsLogo />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
urlOverride={`${window.location.origin}/manage/jobs/${card.id}`}
|
||||||
|
/>
|
||||||
|
<Link to={{ search: `?selected=${card.id}` }}>
|
||||||
|
<EyeFilled />
|
||||||
|
</Link>
|
||||||
|
</Space>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -10,6 +10,16 @@
|
|||||||
.height-preserving-container {
|
.height-preserving-container {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.share-to-teams-badge {
|
||||||
|
background-color: #cccccc;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.react-trello-column-header {
|
.react-trello-column-header {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const sortByParentId = (arr) => {
|
|||||||
|
|
||||||
// Function to create board data based on statuses and jobs, with optional filtering
|
// Function to create board data based on statuses and jobs, with optional filtering
|
||||||
export const createBoardData = ({ statuses, data, filter, cardSettings }) => {
|
export const createBoardData = ({ statuses, data, filter, cardSettings }) => {
|
||||||
const { search, employeeId, alert } = filter;
|
const { search, employeeId, alert, unassigned } = filter;
|
||||||
|
|
||||||
const lanes = statuses.map((status) => ({
|
const lanes = statuses.map((status) => ({
|
||||||
id: status,
|
id: status,
|
||||||
@@ -40,6 +40,13 @@ export const createBoardData = ({ statuses, data, filter, cardSettings }) => {
|
|||||||
let filteredJobs =
|
let filteredJobs =
|
||||||
(search === "" || !search) && !employeeId ? data : data.filter((job) => checkFilter(search, employeeId, job));
|
(search === "" || !search) && !employeeId ? data : data.filter((job) => checkFilter(search, employeeId, job));
|
||||||
|
|
||||||
|
// Apply "Unassigned" filter
|
||||||
|
if (unassigned) {
|
||||||
|
filteredJobs = filteredJobs.filter(
|
||||||
|
(job) => !job.employee_body && !job.employee_prep && !job.employee_refinish && !job.employee_csr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Filter jobs by selectedMdInsCos if it has values
|
// Filter jobs by selectedMdInsCos if it has values
|
||||||
if (cardSettings?.selectedMdInsCos?.length > 0) {
|
if (cardSettings?.selectedMdInsCos?.length > 0) {
|
||||||
filteredJobs = filteredJobs.filter((job) => cardSettings.selectedMdInsCos.includes(job.ins_co_nm));
|
filteredJobs = filteredJobs.filter((job) => cardSettings.selectedMdInsCos.includes(job.ins_co_nm));
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Button, Card, Col, Form, Popover, Row, Tabs } from "antd";
|
import { Button, Card, Col, Form, Popover, Row, Tabs } from "antd";
|
||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { UPDATE_KANBAN_SETTINGS } from "../../../graphql/user.queries.js";
|
import { UPDATE_KANBAN_SETTINGS } from "../../../graphql/user.queries.js";
|
||||||
import { defaultKanbanSettings, mergeWithDefaults } from "./defaultKanbanSettings.js";
|
import { defaultKanbanSettings, mergeWithDefaults } from "./defaultKanbanSettings.js";
|
||||||
@@ -11,6 +11,7 @@ import FilterSettings from "./FilterSettings.jsx";
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { isFunction } from "lodash";
|
import { isFunction } from "lodash";
|
||||||
import { useNotification } from "../../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import { SettingOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bodyshop, data, onSettingsChange }) {
|
function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bodyshop, data, onSettingsChange }) {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
@@ -153,7 +154,7 @@ function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bod
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover content={overlay} open={open} placement="topRight">
|
<Popover content={overlay} open={open} placement="topRight">
|
||||||
<Button loading={loading} onClick={() => setOpen(!open)}>
|
<Button icon={<SettingOutlined />} loading={loading} onClick={() => setOpen(!open)}>
|
||||||
{t("production.settings.board_settings")}
|
{t("production.settings.board_settings")}
|
||||||
</Button>
|
</Button>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import ProductionListColumnNote from "./production-list-columns.productionnote.c
|
|||||||
import ProductionListColumnCategory from "./production-list-columns.status.category";
|
import ProductionListColumnCategory from "./production-list-columns.status.category";
|
||||||
import ProductionListColumnStatus from "./production-list-columns.status.component";
|
import ProductionListColumnStatus from "./production-list-columns.status.component";
|
||||||
import ProductionListColumnTouchTime from "./prodution-list-columns.touchtime.component";
|
import ProductionListColumnTouchTime from "./prodution-list-columns.touchtime.component";
|
||||||
|
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
|
||||||
|
|
||||||
const getEmployeeName = (employeeId, employees) => {
|
const getEmployeeName = (employeeId, employees) => {
|
||||||
const employee = employees.find((e) => e.id === employeeId);
|
const employee = employees.find((e) => e.id === employeeId);
|
||||||
@@ -41,7 +42,17 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
|
|||||||
dataIndex: "viewdetail",
|
dataIndex: "viewdetail",
|
||||||
key: "viewdetail",
|
key: "viewdetail",
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
render: (text, record) => <Link to={{ search: `?selected=${record.id}` }}>{i18n.t("general.labels.view")}</Link>
|
render: (text, record) => (
|
||||||
|
<Space>
|
||||||
|
<Link to={{ search: `?selected=${record.id}` }}>{i18n.t("general.labels.view")}</Link>
|
||||||
|
<ShareToTeamsButton
|
||||||
|
noIcon={true}
|
||||||
|
linkText={"Share"}
|
||||||
|
noIconStyle={{ color: "#1890ff" }}
|
||||||
|
urlOverride={`${window.location.origin}/manage/jobs/${record.id}`}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
...(Enhanced_Payroll.treatment === "on"
|
...(Enhanced_Payroll.treatment === "on"
|
||||||
? [
|
? [
|
||||||
|
|||||||
@@ -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 { Button, Dropdown } from "antd";
|
import { Button, Dropdown } from "antd";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||||
@@ -7,6 +7,7 @@ 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 { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import { PrinterFilled } from "@ant-design/icons";
|
||||||
|
|
||||||
const ProdTemplates = TemplateList("production");
|
const ProdTemplates = TemplateList("production");
|
||||||
const { production_by_technician_one, production_by_category_one, production_by_repair_status_one } =
|
const { production_by_technician_one, production_by_category_one, production_by_repair_status_one } =
|
||||||
@@ -123,7 +124,9 @@ export function ProductionListPrint({ bodyshop }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown trigger="click" menu={menu}>
|
<Dropdown trigger="click" menu={menu}>
|
||||||
<Button loading={loading}>{t("general.labels.print")}</Button>
|
<Button icon={<PrinterFilled />} loading={loading}>
|
||||||
|
{t("general.labels.print")}
|
||||||
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { Button } from "antd";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { PiMicrosoftTeamsLogo } from "react-icons/pi";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ShareToTeamsButton component for sharing content to Microsoft Teams via an HTTP link.
|
||||||
|
*
|
||||||
|
* This component creates a button or link that opens the Microsoft Teams share dialog with
|
||||||
|
* the provided URL, title, and message text through query parameters. The popup window is centered on the screen.
|
||||||
|
*
|
||||||
|
* @param {Object} props - The component's props.
|
||||||
|
* @param {string} [props.messageTextOverride] - Custom message text for sharing.
|
||||||
|
* @param {string} [props.urlOverride] - Custom URL to share instead of the current page's URL.
|
||||||
|
* @param {string} [props.pageTitleOverride] - Custom title for the shared page.
|
||||||
|
* @param {boolean} [props.noIcon=false] - If true, renders as a simple text link instead of a button with an icon.
|
||||||
|
* @param {Object} [props.noIconStyle={}] - Style object for the text link when noIcon is true.
|
||||||
|
* @param {Object} [props.buttonStyle={}] - Style object for the Ant Design button.
|
||||||
|
* @param {Object} [props.buttonIconStyle={}] - Style object for the icon within the button.
|
||||||
|
* @param {string} [props.linkText] - Text to display on the button or link.
|
||||||
|
* @returns {React.ReactElement} A button or text link for sharing to Microsoft Teams.
|
||||||
|
*/
|
||||||
|
const ShareToTeamsComponent = ({
|
||||||
|
bodyshop,
|
||||||
|
messageTextOverride,
|
||||||
|
urlOverride,
|
||||||
|
pageTitleOverride,
|
||||||
|
noIcon = false,
|
||||||
|
noIconStyle = {},
|
||||||
|
buttonStyle = {},
|
||||||
|
buttonIconStyle = {},
|
||||||
|
linkText
|
||||||
|
}) => {
|
||||||
|
const location = useLocation();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const currentUrl =
|
||||||
|
urlOverride ||
|
||||||
|
encodeURIComponent(`${window.location.origin}${location.pathname}${location.search}${location.hash}`);
|
||||||
|
const pageTitle =
|
||||||
|
pageTitleOverride ||
|
||||||
|
encodeURIComponent(typeof document !== "undefined" ? document.title : t("general.actions.sharetoteams"));
|
||||||
|
const messageText = messageTextOverride || encodeURIComponent(t("general.actions.sharetoteams"));
|
||||||
|
|
||||||
|
// Construct the Teams share URL with parameters
|
||||||
|
const teamsShareUrl = `https://teams.microsoft.com/share?href=${currentUrl}&preText=${messageText}&title=${pageTitle}`;
|
||||||
|
|
||||||
|
// Function to open the centered share link in a new window/tab
|
||||||
|
const handleShare = () => {
|
||||||
|
const screenWidth = window.screen.width;
|
||||||
|
const screenHeight = window.screen.height;
|
||||||
|
const windowWidth = 600;
|
||||||
|
const windowHeight = 400;
|
||||||
|
|
||||||
|
const left = screenWidth / 2 - windowWidth / 2;
|
||||||
|
const top = screenHeight / 2 - windowHeight / 2;
|
||||||
|
|
||||||
|
const windowFeatures = `width=${windowWidth},height=${windowHeight},left=${left},top=${top}`;
|
||||||
|
|
||||||
|
// noinspection JSIgnoredPromiseFromCall
|
||||||
|
window.open(teamsShareUrl, "_blank", windowFeatures);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Feature is disabled
|
||||||
|
if (!bodyshop?.md_functionality_toggles?.teams) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noIcon) {
|
||||||
|
return (
|
||||||
|
<div style={{ cursor: "pointer", ...noIconStyle }} onClick={handleShare}>
|
||||||
|
{!linkText ? t("general.actions.sharetoteams") : linkText}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#6264A7",
|
||||||
|
borderColor: "#6264A7",
|
||||||
|
color: "#FFFFFF",
|
||||||
|
...buttonStyle
|
||||||
|
}}
|
||||||
|
icon={<PiMicrosoftTeamsLogo style={{ color: "#FFFFFF", ...buttonIconStyle }} />}
|
||||||
|
onClick={handleShare}
|
||||||
|
title={linkText === null ? t("general.actions.sharetoteams") : linkText}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ShareToTeamsComponent.propTypes = {
|
||||||
|
messageTextOverride: PropTypes.string,
|
||||||
|
urlOverride: PropTypes.string,
|
||||||
|
pageTitleOverride: PropTypes.string,
|
||||||
|
noIcon: PropTypes.bool,
|
||||||
|
noIconStyle: PropTypes.object,
|
||||||
|
buttonStyle: PropTypes.object,
|
||||||
|
buttonIconStyle: PropTypes.object,
|
||||||
|
linkText: PropTypes.oneOfType([PropTypes.string, PropTypes.node])
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(ShareToTeamsComponent);
|
||||||
@@ -14,7 +14,7 @@ import FormItemEmail from "../form-items-formatted/email-form-item.component";
|
|||||||
import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
|
import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-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";
|
||||||
// TODO: Client Update, this might break
|
|
||||||
const timeZonesList = Intl.supportedValuesOf("timeZone");
|
const timeZonesList = Intl.supportedValuesOf("timeZone");
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -642,6 +642,15 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
|||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
|
<LayoutFormRow header={t("bodyshop.labels.shop_enabled_features")} id="sharing">
|
||||||
|
<Form.Item
|
||||||
|
label={t("general.actions.sharetoteams")}
|
||||||
|
valuePropName="checked"
|
||||||
|
name={["md_functionality_toggles", "teams"]}
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</LayoutFormRow>
|
||||||
<LayoutFormRow grow header={t("bodyshop.labels.messagingpresets")} id="messagingpresets">
|
<LayoutFormRow grow header={t("bodyshop.labels.messagingpresets")} id="messagingpresets">
|
||||||
<Form.List name={["md_messaging_presets"]}>
|
<Form.List name={["md_messaging_presets"]}>
|
||||||
{(fields, { add, remove, move }) => {
|
{(fields, { add, remove, move }) => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { setModalContext } from "../../redux/modals/modals.actions";
|
|||||||
import { pageLimit } from "../../utils/config";
|
import { pageLimit } from "../../utils/config";
|
||||||
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter.jsx";
|
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter.jsx";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
|
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Task List Component
|
* Task List Component
|
||||||
@@ -266,8 +267,13 @@ function TaskListComponent({
|
|||||||
width: "8%",
|
width: "8%",
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<Space direction="horizontal">
|
<Space direction="horizontal">
|
||||||
|
<ShareToTeamsButton
|
||||||
|
linkText=""
|
||||||
|
urlOverride={`${window.location.origin}/manage/tasks/alltasks?taskid=${record.id}`}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
title={t("tasks.buttons.edit")}
|
title={t("tasks.buttons.edit")}
|
||||||
|
icon={<EditFilled />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTaskUpsertContext({
|
setTaskUpsertContext({
|
||||||
context: {
|
context: {
|
||||||
@@ -276,18 +282,18 @@ function TaskListComponent({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<EditFilled />
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
title={t("tasks.buttons.complete")}
|
title={t("tasks.buttons.complete")}
|
||||||
onClick={() => toggleCompletedStatus(record.id, record.completed)}
|
onClick={() => toggleCompletedStatus(record.id, record.completed)}
|
||||||
>
|
icon={record.completed ? <CheckCircleOutlined /> : <CheckCircleFilled />}
|
||||||
{record.completed ? <CheckCircleOutlined /> : <CheckCircleFilled />}
|
/>
|
||||||
</Button>
|
|
||||||
<Button title={t("tasks.buttons.delete")} onClick={() => toggleDeletedStatus(record.id, record.deleted)}>
|
<Button
|
||||||
{record.deleted ? <DeleteOutlined /> : <DeleteFilled />}
|
title={t("tasks.buttons.delete")}
|
||||||
</Button>
|
onClick={() => toggleDeletedStatus(record.id, record.deleted)}
|
||||||
|
icon={record.deleted ? <DeleteOutlined /> : <DeleteFilled />}
|
||||||
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMutation, useQuery } from "@apollo/client";
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
import { Form, Modal } from "antd";
|
import { Form, Modal } from "antd";
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { 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";
|
||||||
@@ -35,7 +35,7 @@ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, to
|
|||||||
const [insertTask] = useMutation(MUTATION_INSERT_NEW_TASK);
|
const [insertTask] = useMutation(MUTATION_INSERT_NEW_TASK);
|
||||||
const [updateTask] = useMutation(MUTATION_UPDATE_TASK);
|
const [updateTask] = useMutation(MUTATION_UPDATE_TASK);
|
||||||
const { open, context } = taskUpsert;
|
const { open, context } = taskUpsert;
|
||||||
const { jobid, joblineid, billid, partsorderid, taskId, existingTask, query } = context;
|
const { jobid, joblineid, billid, partsorderid, taskId, existingTask, query, view } = context;
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [selectedJobId, setSelectedJobId] = useState(null);
|
const [selectedJobId, setSelectedJobId] = useState(null);
|
||||||
const [selectedJobDetails, setSelectedJobDetails] = useState(null);
|
const [selectedJobDetails, setSelectedJobDetails] = useState(null);
|
||||||
@@ -255,16 +255,17 @@ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, to
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const taskTitle = useMemo(() => {
|
|
||||||
return existingTask ? t("tasks.actions.edit") : t("tasks.actions.new");
|
|
||||||
}, [existingTask, t]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={<span id="task-upsert-modal-title">{taskTitle}</span>}
|
title={
|
||||||
|
<span id="task-upsert-modal-title">
|
||||||
|
{view ? t("tasks.actions.view") : existingTask ? t("tasks.actions.edit") : t("tasks.actions.new")}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
open={open}
|
open={open}
|
||||||
okText={t("general.actions.save")}
|
okText={t("general.actions.save")}
|
||||||
width="50%"
|
width="50%"
|
||||||
|
cancelText={!isTouched ? t("general.actions.ok") : t("general.actions.cancel")}
|
||||||
onOk={() => {
|
onOk={() => {
|
||||||
removeTaskIdFromUrl();
|
removeTaskIdFromUrl();
|
||||||
form.submit();
|
form.submit();
|
||||||
@@ -289,6 +290,7 @@ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, to
|
|||||||
loading={loading || (taskId && taskLoading)}
|
loading={loading || (taskId && taskLoading)}
|
||||||
error={error}
|
error={error}
|
||||||
data={data}
|
data={data}
|
||||||
|
view={view}
|
||||||
existingTask={existingTask || taskData?.tasks_by_pk}
|
existingTask={existingTask || taskData?.tasks_by_pk}
|
||||||
selectedJobId={selectedJobId}
|
selectedJobId={selectedJobId}
|
||||||
setSelectedJobId={setSelectedJobId}
|
setSelectedJobId={setSelectedJobId}
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { DeleteFilled } from "@ant-design/icons";
|
import { DeleteFilled, PrinterFilled } from "@ant-design/icons";
|
||||||
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
import { useApolloClient, useMutation } from "@apollo/client";
|
import { useApolloClient, useMutation } from "@apollo/client";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
@@ -16,32 +17,31 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
Typography
|
Typography
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import { PageHeader } from "@ant-design/pro-layout";
|
import { 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 { useNavigate } from 'react-router-dom';
|
// import { useNavigate } from 'react-router-dom';
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import Dinero from "dinero.js";
|
import Dinero from "dinero.js";
|
||||||
import dayjs from "../../utils/day";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import DateTimePicker from "../../components/form-date-time-picker/form-date-time-picker.component";
|
import DateTimePicker from "../../components/form-date-time-picker/form-date-time-picker.component";
|
||||||
import FormsFieldChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component";
|
import FormsFieldChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component";
|
||||||
import CurrencyInput from "../../components/form-items-formatted/currency-form-item.component";
|
import CurrencyInput from "../../components/form-items-formatted/currency-form-item.component";
|
||||||
|
import JobCloseRoGuardContainer from "../../components/job-close-ro-guard/job-close-ro-guard.container";
|
||||||
import JobsScoreboardAdd from "../../components/job-scoreboard-add-button/job-scoreboard-add-button.component";
|
import JobsScoreboardAdd from "../../components/job-scoreboard-add-button/job-scoreboard-add-button.component";
|
||||||
import JobsCloseAutoAllocate from "../../components/jobs-close-auto-allocate/jobs-close-auto-allocate.component";
|
import JobsCloseAutoAllocate from "../../components/jobs-close-auto-allocate/jobs-close-auto-allocate.component";
|
||||||
import JobsCloseLines from "../../components/jobs-close-lines/jobs-close-lines.component";
|
import JobsCloseLines from "../../components/jobs-close-lines/jobs-close-lines.component";
|
||||||
import LayoutFormRow from "../../components/layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../../components/layout-form-row/layout-form-row.component";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { generateJobLinesUpdatesForInvoicing } from "../../graphql/jobs-lines.queries";
|
import { generateJobLinesUpdatesForInvoicing } from "../../graphql/jobs-lines.queries";
|
||||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
|
import { setModalContext } from "../../redux/modals/modals.actions.js";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import JobCloseRoGuardContainer from "../../components/job-close-ro-guard/job-close-ro-guard.container";
|
import dayjs from "../../utils/day";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -49,10 +49,17 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||||
|
setPrintCenterContext: (context) =>
|
||||||
|
dispatch(
|
||||||
|
setModalContext({
|
||||||
|
context: context,
|
||||||
|
modal: "printCenter"
|
||||||
|
})
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail }) {
|
export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, setPrintCenterContext }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
@@ -171,7 +178,6 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail }) {
|
|||||||
extra={
|
extra={
|
||||||
<Space>
|
<Space>
|
||||||
<JobsCloseAutoAllocate joblines={job.joblines} form={form} disabled={!!job.date_exported || jobRO} />
|
<JobsCloseAutoAllocate joblines={job.joblines} form={form} disabled={!!job.date_exported || jobRO} />
|
||||||
|
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
onConfirm={() => form.submit()}
|
onConfirm={() => form.submit()}
|
||||||
disabled={jobRO}
|
disabled={jobRO}
|
||||||
@@ -188,6 +194,21 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail }) {
|
|||||||
<Button disabled={job.date_exported || !jobRO}>{t("jobs.actions.sendtodms")}</Button>
|
<Button disabled={job.date_exported || !jobRO}>{t("jobs.actions.sendtodms")}</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setPrintCenterContext({
|
||||||
|
context: {
|
||||||
|
id: job.id,
|
||||||
|
job: job,
|
||||||
|
type: "job"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
key="printing"
|
||||||
|
icon={<PrinterFilled />}
|
||||||
|
>
|
||||||
|
{t("jobs.actions.printCenter")}
|
||||||
|
</Button>
|
||||||
<JobsScoreboardAdd job={job} disabled={false} />
|
<JobsScoreboardAdd job={job} disabled={false} />
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ export function AllTasksPageContainer({ setBreadcrumbs, setSelectedHeader, setTa
|
|||||||
if (taskId) {
|
if (taskId) {
|
||||||
setTaskUpsertContext({
|
setTaskUpsertContext({
|
||||||
context: {
|
context: {
|
||||||
taskId
|
taskId,
|
||||||
|
view: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
urlParams.delete("taskid");
|
urlParams.delete("taskid");
|
||||||
|
|||||||
@@ -235,7 +235,12 @@ export function* signInSuccessSaga({ payload }) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
window.$crisp.push(["set", "user:nickname", [payload.displayName || payload.email]]);
|
window.$crisp.push(["set", "user:nickname", [payload.displayName || payload.email]]);
|
||||||
window.$crisp.push(["set", "session:segments", [["user"]]]);
|
const currentUserSegment = InstanceRenderManager({
|
||||||
|
imex: "imex-online-user",
|
||||||
|
rome: "rome-online-user"
|
||||||
|
});
|
||||||
|
window.$crisp.push(["set", "session:segments", [[currentUserSegment]]]);
|
||||||
|
|
||||||
InstanceRenderManager({
|
InstanceRenderManager({
|
||||||
executeFunction: true,
|
executeFunction: true,
|
||||||
args: [],
|
args: [],
|
||||||
@@ -342,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;
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import client from "../utils/GraphQLClient";
|
|||||||
import cleanAxios from "./CleanAxios";
|
import cleanAxios from "./CleanAxios";
|
||||||
import { TemplateList } from "./TemplateConstants";
|
import { TemplateList } from "./TemplateConstants";
|
||||||
import { generateTemplate } from "./graphQLmodifier";
|
import { generateTemplate } from "./graphQLmodifier";
|
||||||
import InstanceRenderManager from "./instanceRenderMgr";
|
|
||||||
|
|
||||||
const server = import.meta.env.VITE_APP_REPORTS_SERVER_URL;
|
const server = import.meta.env.VITE_APP_REPORTS_SERVER_URL;
|
||||||
|
|
||||||
@@ -39,7 +38,7 @@ export default async function RenderTemplate(
|
|||||||
jsreport.headers["Authorization"] = jsrAuth;
|
jsreport.headers["Authorization"] = jsrAuth;
|
||||||
|
|
||||||
//Query assets that match the template name. Must be in format <<templateName>>.query
|
//Query assets that match the template name. Must be in format <<templateName>>.query
|
||||||
let { contextData, useShopSpecificTemplate } = await fetchContextData(templateObject, jsrAuth);
|
let { contextData, useShopSpecificTemplate, shopSpecificFolder } = await fetchContextData(templateObject, jsrAuth);
|
||||||
|
|
||||||
const { ignoreCustomMargins } = Templates[templateObject.name];
|
const { ignoreCustomMargins } = Templates[templateObject.name];
|
||||||
|
|
||||||
@@ -74,14 +73,8 @@ export default async function RenderTemplate(
|
|||||||
...contextData,
|
...contextData,
|
||||||
...templateObject.variables,
|
...templateObject.variables,
|
||||||
...templateObject.context,
|
...templateObject.context,
|
||||||
headerpath: `/${InstanceRenderManager({
|
headerpath: shopSpecificFolder ? `/${bodyshop.imexshopid}/header.html` : `/GENERIC/header.html`,
|
||||||
imex: bodyshop.imexshopid,
|
footerpath: shopSpecificFolder ? `/${bodyshop.imexshopid}/footer.html` : `/GENERIC/footer.html`,
|
||||||
rome: bodyshop.imexshopid
|
|
||||||
})}/header.html`,
|
|
||||||
footerpath: `/${InstanceRenderManager({
|
|
||||||
imex: bodyshop.imexshopid,
|
|
||||||
rome: bodyshop.imexshopid
|
|
||||||
})}/footer.html`,
|
|
||||||
bodyshop: bodyshop,
|
bodyshop: bodyshop,
|
||||||
filters: templateObject?.filters,
|
filters: templateObject?.filters,
|
||||||
sorters: templateObject?.sorters,
|
sorters: templateObject?.sorters,
|
||||||
@@ -149,11 +142,12 @@ export async function RenderTemplates(templateObjects, bodyshop, renderAsHtml =
|
|||||||
templateObjects.forEach((template) => {
|
templateObjects.forEach((template) => {
|
||||||
proms.push(
|
proms.push(
|
||||||
(async () => {
|
(async () => {
|
||||||
let { contextData, useShopSpecificTemplate } = await fetchContextData(template, jsrAuth);
|
let { contextData, useShopSpecificTemplate, shopSpecificFolder } = await fetchContextData(template, jsrAuth);
|
||||||
unsortedTemplatesAndData.push({
|
unsortedTemplatesAndData.push({
|
||||||
templateObject: template,
|
templateObject: template,
|
||||||
contextData,
|
contextData,
|
||||||
useShopSpecificTemplate
|
useShopSpecificTemplate,
|
||||||
|
shopSpecificFolder
|
||||||
});
|
});
|
||||||
})()
|
})()
|
||||||
);
|
);
|
||||||
@@ -248,8 +242,8 @@ export async function RenderTemplates(templateObjects, bodyshop, renderAsHtml =
|
|||||||
|
|
||||||
// ...rootTemplate.templateObject.variables,
|
// ...rootTemplate.templateObject.variables,
|
||||||
// ...rootTemplate.templateObject.context,
|
// ...rootTemplate.templateObject.context,
|
||||||
headerpath: `/${bodyshop.imexshopid}/header.html`,
|
headerpath: rootTemplate.shopSpecificFolder ? `/${bodyshop.imexshopid}/header.html` : `/GENERIC/header.html`,
|
||||||
footerpath: `/${bodyshop.imexshopid}/footer.html`,
|
footerpath: rootTemplate.shopSpecificFolder ? `/${bodyshop.imexshopid}/footer.html` : `/GENERIC/footer.html`,
|
||||||
bodyshop: bodyshop,
|
bodyshop: bodyshop,
|
||||||
offset: bodyshop.timezone
|
offset: bodyshop.timezone
|
||||||
}
|
}
|
||||||
@@ -397,10 +391,10 @@ const fetchContextData = async (templateObject, jsrAuth) => {
|
|||||||
});
|
});
|
||||||
contextData = data;
|
contextData = data;
|
||||||
}
|
}
|
||||||
return { contextData, useShopSpecificTemplate };
|
return { contextData, useShopSpecificTemplate, shopSpecificFolder };
|
||||||
}
|
}
|
||||||
|
|
||||||
return await generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate);
|
return await generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate, shopSpecificFolder);
|
||||||
};
|
};
|
||||||
|
|
||||||
//export const displayTemplateInWindow = (html) => {
|
//export const displayTemplateInWindow = (html) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { gql } from "@apollo/client";
|
||||||
import { Kind, parse, print, visit } from "graphql";
|
import { Kind, parse, print, visit } from "graphql";
|
||||||
import client from "./GraphQLClient";
|
import client from "./GraphQLClient";
|
||||||
import { gql } from "@apollo/client";
|
|
||||||
|
|
||||||
/* eslint-disable no-loop-func */
|
/* eslint-disable no-loop-func */
|
||||||
|
|
||||||
@@ -114,9 +114,10 @@ export function printQuery(query) {
|
|||||||
* @param templateQueryToExecute
|
* @param templateQueryToExecute
|
||||||
* @param templateObject
|
* @param templateObject
|
||||||
* @param useShopSpecificTemplate
|
* @param useShopSpecificTemplate
|
||||||
* @returns {Promise<{contextData: {}, useShopSpecificTemplate}>}
|
* @param shopSpecificTemplate
|
||||||
|
* @returns {Promise<{contextData: {}, useShopSpecificTemplate, shopSpecificTemplate}>}
|
||||||
*/
|
*/
|
||||||
export async function generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate) {
|
export async function generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate, shopSpecificFolder) {
|
||||||
// Advanced Filtering and Sorting modifications start here
|
// Advanced Filtering and Sorting modifications start here
|
||||||
|
|
||||||
// Parse the query and apply the filters and sorters
|
// Parse the query and apply the filters and sorters
|
||||||
@@ -147,7 +148,7 @@ export async function generateTemplate(templateQueryToExecute, templateObject, u
|
|||||||
contextData = data;
|
contextData = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { contextData, useShopSpecificTemplate };
|
return { contextData, useShopSpecificTemplate, shopSpecificFolder };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
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";
|
|
||||||
//import { visualizer } from "rollup-plugin-visualizer";
|
|
||||||
|
|
||||||
|
// 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.");
|
||||||
@@ -22,7 +37,7 @@ export const logger = createLogger("info", {
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: "/",
|
base: "/",
|
||||||
plugins: [
|
plugins: [
|
||||||
//visualizer(),
|
// Ensure all plugins are Vite 6 compatible
|
||||||
ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })),
|
ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })),
|
||||||
VitePWA({
|
VitePWA({
|
||||||
injectRegister: "auto",
|
injectRegister: "auto",
|
||||||
@@ -31,14 +46,12 @@ export default defineConfig({
|
|||||||
short_name: InstanceRenderManager({
|
short_name: InstanceRenderManager({
|
||||||
instance: process.env.VITE_APP_INSTANCE,
|
instance: process.env.VITE_APP_INSTANCE,
|
||||||
imex: "ImEX Online",
|
imex: "ImEX Online",
|
||||||
rome: "Rome Online",
|
rome: "Rome Online"
|
||||||
|
|
||||||
}),
|
}),
|
||||||
name: InstanceRenderManager({
|
name: InstanceRenderManager({
|
||||||
instance: process.env.VITE_APP_INSTANCE,
|
instance: process.env.VITE_APP_INSTANCE,
|
||||||
imex: "ImEX Online",
|
imex: "ImEX Online",
|
||||||
rome: "Rome Online",
|
rome: "Rome Online"
|
||||||
|
|
||||||
}),
|
}),
|
||||||
description: "The ultimate bodyshop management system.",
|
description: "The ultimate bodyshop management system.",
|
||||||
icons: [
|
icons: [
|
||||||
@@ -46,7 +59,7 @@ export default defineConfig({
|
|||||||
src: InstanceRenderManager({
|
src: InstanceRenderManager({
|
||||||
instance: process.env.VITE_APP_INSTANCE,
|
instance: process.env.VITE_APP_INSTANCE,
|
||||||
imex: "favicon.png",
|
imex: "favicon.png",
|
||||||
rome: "ro-favicon.png",
|
rome: "ro-favicon.png"
|
||||||
}),
|
}),
|
||||||
sizes: "64x64 32x32 24x24 16x16",
|
sizes: "64x64 32x32 24x24 16x16",
|
||||||
type: "image/x-icon"
|
type: "image/x-icon"
|
||||||
@@ -55,7 +68,7 @@ export default defineConfig({
|
|||||||
src: InstanceRenderManager({
|
src: InstanceRenderManager({
|
||||||
instance: process.env.VITE_APP_INSTANCE,
|
instance: process.env.VITE_APP_INSTANCE,
|
||||||
imex: "logo192.png",
|
imex: "logo192.png",
|
||||||
rome: "logo192.png",
|
rome: "logo192.png"
|
||||||
}),
|
}),
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
sizes: "192x192"
|
sizes: "192x192"
|
||||||
@@ -64,7 +77,7 @@ export default defineConfig({
|
|||||||
src: InstanceRenderManager({
|
src: InstanceRenderManager({
|
||||||
instance: process.env.VITE_APP_INSTANCE,
|
instance: process.env.VITE_APP_INSTANCE,
|
||||||
imex: "logo512.png",
|
imex: "logo512.png",
|
||||||
rome: "ro-favicon.png",
|
rome: "ro-favicon.png"
|
||||||
}),
|
}),
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
sizes: "512x512"
|
sizes: "512x512"
|
||||||
@@ -73,17 +86,32 @@ export default defineConfig({
|
|||||||
theme_color: InstanceRenderManager({
|
theme_color: InstanceRenderManager({
|
||||||
instance: process.env.VITE_APP_INSTANCE,
|
instance: process.env.VITE_APP_INSTANCE,
|
||||||
imex: "#1890ff",
|
imex: "#1890ff",
|
||||||
rome: "#fff",
|
rome: "#fff"
|
||||||
}),
|
}),
|
||||||
background_color: "#fff",
|
background_color: "#fff",
|
||||||
gcm_sender_id: "103953800507"
|
gcm_sender_id: "103953800507"
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
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,
|
||||||
@@ -186,7 +214,9 @@ export default defineConfig({
|
|||||||
"libphonenumber-js": ["libphonenumber-js"]
|
"libphonenumber-js": ["libphonenumber-js"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
|
sourcemap: true
|
||||||
},
|
},
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
include: [
|
include: [
|
||||||
@@ -204,8 +234,10 @@ export default defineConfig({
|
|||||||
"react-redux"
|
"react-redux"
|
||||||
],
|
],
|
||||||
esbuildOptions: {
|
esbuildOptions: {
|
||||||
|
// Update for Vite 6: Use proper file extensions
|
||||||
loader: {
|
loader: {
|
||||||
".js": "jsx"
|
".jsx": "jsx",
|
||||||
|
".tsx": "tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
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,6 +31,14 @@
|
|||||||
headers:
|
headers:
|
||||||
- name: x-imex-auth
|
- name: x-imex-auth
|
||||||
value_from_env: DATAPUMP_AUTH
|
value_from_env: DATAPUMP_AUTH
|
||||||
|
- name: Rome Usage Report
|
||||||
|
webhook: '{{HASURA_API_URL}}/data/usagereport'
|
||||||
|
schedule: 0 12 * * 5
|
||||||
|
include_in_metadata: true
|
||||||
|
payload: {}
|
||||||
|
headers:
|
||||||
|
- name: x-imex-auth
|
||||||
|
value_from_env: DATAPUMP_AUTH
|
||||||
- name: Task Reminders
|
- name: Task Reminders
|
||||||
webhook: '{{HASURA_API_URL}}/tasks-remind-handler'
|
webhook: '{{HASURA_API_URL}}/tasks-remind-handler'
|
||||||
schedule: '*/15 * * * *'
|
schedule: '*/15 * * * *'
|
||||||
|
|||||||
@@ -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;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user