Compare commits
250 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b479684fe4 | ||
|
|
5b81912bd3 | ||
|
|
3c98a94c38 | ||
|
|
1d98de6d4d | ||
|
|
0ce5d9063a | ||
|
|
3b84e1d6ec | ||
|
|
d62f6e2116 | ||
|
|
71a26cc4ac | ||
|
|
32441e9406 | ||
|
|
e6dade1206 | ||
|
|
43d34cae07 | ||
|
|
a72a7948fe | ||
|
|
a24f6639a1 | ||
|
|
b2a0af32e9 | ||
|
|
cc58d14d32 | ||
|
|
9ce419b949 | ||
|
|
5053816be7 | ||
|
|
30ca34ea93 | ||
|
|
68d1a404b3 | ||
|
|
85e82b85ea | ||
|
|
23467280b4 | ||
|
|
aedad1c48f | ||
|
|
05cc4dd188 | ||
|
|
ea6351ea06 | ||
|
|
87d3ceb408 | ||
|
|
d08dd2b506 | ||
|
|
8a047d14a1 | ||
|
|
e103772aa4 | ||
|
|
c332699dc8 | ||
|
|
25e6e61d10 | ||
|
|
cdcd6b636a | ||
|
|
7879591bcf | ||
|
|
7fc6556866 | ||
|
|
3f5489ce7e | ||
|
|
5a90854861 | ||
|
|
8347a8c098 | ||
|
|
2bf074d85a | ||
|
|
50d47cd679 | ||
|
|
3a4e06eaa2 | ||
|
|
4be71726d4 | ||
|
|
c78db7eb08 | ||
|
|
e4dc711481 | ||
|
|
5114138c67 | ||
|
|
68b8743002 | ||
|
|
8f312bfffb | ||
|
|
7e7e109cfe | ||
|
|
05e5545466 | ||
|
|
ddb0990645 | ||
|
|
04dec6d91c | ||
|
|
a883b817b0 | ||
|
|
b7423aebf6 | ||
|
|
ee70aeb952 | ||
|
|
74d95e7cbb | ||
|
|
f6f6fab5ba | ||
|
|
699ffc822a | ||
|
|
4e35f5402c | ||
|
|
9b997d0924 | ||
|
|
d705f8211e | ||
|
|
03761bbb2a | ||
|
|
4d0794e90e | ||
|
|
e615c4a55b | ||
|
|
51eb3423f3 | ||
|
|
f6318666d9 | ||
|
|
544d4b8136 | ||
|
|
edf4846d55 | ||
|
|
f3754de843 | ||
|
|
3d920ad151 | ||
|
|
575f056360 | ||
|
|
716d9affb5 | ||
|
|
b01dd52da2 | ||
|
|
c75fddc2c0 | ||
|
|
db0c16f31d | ||
|
|
b286ab2439 | ||
|
|
fa57828ebd | ||
|
|
8052767002 | ||
|
|
932f572fb5 | ||
|
|
328a64eb90 | ||
|
|
c661fce8f1 | ||
|
|
60d1396011 | ||
|
|
3b647dfd37 | ||
|
|
50fe588949 | ||
|
|
0ced053d21 | ||
|
|
b8cf4a4d75 | ||
|
|
ff72657a82 | ||
|
|
92a96fdae6 | ||
|
|
b1a96d55ad | ||
|
|
49657816c6 | ||
|
|
7094b6ffbf | ||
|
|
ed7c2574eb | ||
|
|
45a9e37342 | ||
|
|
9e6a458203 | ||
|
|
55a279a700 | ||
|
|
82e2e332cf | ||
|
|
103d7c2bb2 | ||
|
|
f5f0b75617 | ||
|
|
c163554c3f | ||
|
|
bd75f593c2 | ||
|
|
fbc1866363 | ||
|
|
6480f7f2aa | ||
|
|
4cb3a79429 | ||
|
|
ca521eaeba | ||
|
|
f287ba2dac | ||
|
|
ff46bbbb3f | ||
|
|
cafca35500 | ||
|
|
9803841617 | ||
|
|
4dffbfe6fa | ||
|
|
6e61159608 | ||
|
|
3c85de3e34 | ||
|
|
1b5cddd371 | ||
|
|
6a7005299a | ||
|
|
1a4c9faab1 | ||
|
|
bfbf34e11d | ||
|
|
646754732d | ||
|
|
439d9e7b74 | ||
|
|
464f7044f0 | ||
|
|
7cde2f64af | ||
|
|
f674fff930 | ||
|
|
efc1157653 | ||
|
|
0677712d6e | ||
|
|
2e106a5d07 | ||
|
|
da7b97042e | ||
|
|
f018a2b2a6 | ||
|
|
c3f7d7bad2 | ||
|
|
70d857bfec | ||
|
|
f3265901b6 | ||
|
|
7c8ac50426 | ||
|
|
8ad39fe855 | ||
|
|
13b6218c43 | ||
|
|
bece3278f4 | ||
|
|
4c0a1960ad | ||
|
|
47324422a6 | ||
|
|
3b1da6901d | ||
|
|
fc6ec54233 | ||
|
|
64928d0849 | ||
|
|
56a580b1e7 | ||
|
|
f7af3b407b | ||
|
|
9a0674f5d7 | ||
|
|
cc30ea658e | ||
|
|
59869def31 | ||
|
|
a5d3f2caf1 | ||
|
|
4ad87a522c | ||
|
|
453812222b | ||
|
|
145cf7cc93 | ||
|
|
c09e22ed96 | ||
|
|
cdb2d4d2d6 | ||
|
|
29f0031c1e | ||
|
|
e8099e130a | ||
|
|
1cbca1ddf0 | ||
|
|
eeed004fe2 | ||
|
|
5a180b86fb | ||
|
|
e3059b41ae | ||
|
|
1a5c71048c | ||
|
|
fc4e97c9b5 | ||
|
|
3cd3d7414d | ||
|
|
1bb2212e4a | ||
|
|
a088f27f1d | ||
|
|
0e9ad1258d | ||
|
|
2a33f462a3 | ||
|
|
cbc164dbeb | ||
|
|
0bfc7033a9 | ||
|
|
2ec0d90a58 | ||
|
|
6382fdf19c | ||
|
|
9287e6608d | ||
|
|
0fcee5b25e | ||
|
|
d221763064 | ||
|
|
b39a5b755e | ||
|
|
30cb4ef562 | ||
|
|
449330441a | ||
|
|
fcab5e6ef2 | ||
|
|
0212b837ea | ||
|
|
e7438a099e | ||
|
|
b3303e3c38 | ||
|
|
c69c86d193 | ||
|
|
73ec8b8a70 | ||
|
|
af09796df8 | ||
|
|
954504de8d | ||
|
|
0aba040338 | ||
|
|
c3bfe87674 | ||
|
|
9aa1279144 | ||
|
|
4e6c45b195 | ||
|
|
4fdb939bd2 | ||
|
|
062a1dcc72 | ||
|
|
7b420b1855 | ||
|
|
40f61bbc8f | ||
|
|
f5d821c394 | ||
|
|
3958ec9189 | ||
|
|
1e4f52e541 | ||
|
|
5cc5cb444e | ||
|
|
4acf0c59ca | ||
|
|
2858a5e871 | ||
|
|
24496d3ee1 | ||
|
|
0a5df69b12 | ||
|
|
80efea02c6 | ||
|
|
9f5c282b41 | ||
|
|
b2602c3385 | ||
|
|
0e584af424 | ||
|
|
cdc3de2a33 | ||
|
|
3bfa556b02 | ||
|
|
44cb7577e2 | ||
|
|
46d2b08477 | ||
|
|
0193ff9e65 | ||
|
|
fd9a51209f | ||
|
|
d0a7b87e04 | ||
|
|
799b24c90e | ||
|
|
3e1a8c87d1 | ||
|
|
c886d874de | ||
|
|
4dfb020089 | ||
|
|
bc6f05acbc | ||
|
|
2701bbd501 | ||
|
|
1f2040d97c | ||
|
|
43963a3e91 | ||
|
|
4287311adb | ||
|
|
d0e8589a76 | ||
|
|
c4bab72947 | ||
|
|
aa4b4998fa | ||
|
|
ed4566e00f | ||
|
|
5c2cdfe16c | ||
|
|
12c75357b5 | ||
|
|
d40f3ee45a | ||
|
|
96a0def846 | ||
|
|
1fd595d0de | ||
|
|
52cf4f3d1f | ||
|
|
4d9be1d232 | ||
|
|
fb2bc20b4f | ||
|
|
744593e96a | ||
|
|
1e9308be9b | ||
|
|
411605e121 | ||
|
|
1da8d6abb3 | ||
|
|
cdcef798df | ||
|
|
f7207a9f3f | ||
|
|
7a54b55bd4 | ||
|
|
991dfc2ad5 | ||
|
|
718c8291a8 | ||
|
|
f1e84c348b | ||
|
|
5f513a8bef | ||
|
|
4b96d5a707 | ||
|
|
220f3d4410 | ||
|
|
841f62bd84 | ||
|
|
91e2e7931b | ||
|
|
3c6faf8473 | ||
|
|
c994eaaa8e | ||
|
|
517d8f4163 | ||
|
|
9deb2964a5 | ||
|
|
9cf9f8b844 | ||
|
|
ad46ea74c0 | ||
|
|
2a28855e4b | ||
|
|
8d25f60097 | ||
|
|
982a51f16e | ||
|
|
68d02648d7 | ||
|
|
89d5b1cfe4 |
@@ -5,6 +5,7 @@ orbs:
|
||||
aws-s3: circleci/aws-s3@4.0.0
|
||||
aws-cli: circleci/aws-cli@4.0
|
||||
eb: circleci/aws-elastic-beanstalk@2.0.1
|
||||
jira: circleci/jira@2.1.0
|
||||
jobs:
|
||||
imex-api-deploy:
|
||||
docker:
|
||||
@@ -18,6 +19,12 @@ jobs:
|
||||
eb status --verbose
|
||||
eb deploy
|
||||
eb status
|
||||
- jira/notify:
|
||||
environment: Production (ImEX) - API
|
||||
environment_type: production
|
||||
job_type: deployment
|
||||
pipeline_id: << pipeline.id >>
|
||||
pipeline_number: << pipeline.number >>
|
||||
|
||||
imex-hasura-migrate:
|
||||
docker:
|
||||
@@ -33,11 +40,16 @@ jobs:
|
||||
- run:
|
||||
name: Execute migration
|
||||
command: |
|
||||
npm install hasura-cli -g
|
||||
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
|
||||
hasura migrate apply --endpoint https://db.imex.online/ --admin-secret << parameters.secret >>
|
||||
hasura metadata apply --endpoint https://db.imex.online/ --admin-secret << parameters.secret >>
|
||||
hasura metadata reload --endpoint https://db.imex.online/ --admin-secret << parameters.secret >>
|
||||
|
||||
- jira/notify:
|
||||
environment: Production (ImEX) - Hasura
|
||||
environment_type: production
|
||||
pipeline_id: << pipeline.id >>
|
||||
job_type: deployment
|
||||
pipeline_number: << pipeline.number >>
|
||||
imex-app-build:
|
||||
docker:
|
||||
- image: cimg/node:18.18.2
|
||||
@@ -62,6 +74,7 @@ jobs:
|
||||
to: "s3://imex-online-production/"
|
||||
arguments: "--exclude '*.map'"
|
||||
|
||||
|
||||
imex-app-beta-build:
|
||||
docker:
|
||||
- image: cimg/node:18.18.2
|
||||
@@ -86,6 +99,12 @@ jobs:
|
||||
from: dist
|
||||
to: "s3://imex-online-beta/"
|
||||
arguments: "--exclude '*.map'"
|
||||
- jira/notify:
|
||||
environment: Production (ImEX) - Front End
|
||||
environment_type: production
|
||||
pipeline_id: << pipeline.id >>
|
||||
job_type: deployment
|
||||
pipeline_number: << pipeline.number >>
|
||||
|
||||
rome-api-deploy:
|
||||
docker:
|
||||
@@ -99,7 +118,12 @@ jobs:
|
||||
eb status --verbose
|
||||
eb deploy
|
||||
eb status
|
||||
|
||||
- jira/notify:
|
||||
environment: Production (Rome) - API
|
||||
environment_type: production
|
||||
pipeline_id: << pipeline.id >>
|
||||
job_type: deployment
|
||||
pipeline_number: << pipeline.number >>
|
||||
rome-hasura-migrate:
|
||||
docker:
|
||||
- image: cimg/node:18.18.2
|
||||
@@ -114,11 +138,16 @@ jobs:
|
||||
- run:
|
||||
name: Execute migration
|
||||
command: |
|
||||
npm install hasura-cli -g
|
||||
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
|
||||
hasura migrate apply --endpoint https://db.romeonline.io/ --admin-secret << parameters.secret >>
|
||||
hasura metadata apply --endpoint https://db.romeonline.io/ --admin-secret << parameters.secret >>
|
||||
hasura metadata reload --endpoint https://db.romeonline.io/ --admin-secret << parameters.secret >>
|
||||
|
||||
- jira/notify:
|
||||
environment: Production (Rome) - Hasura
|
||||
environment_type: production
|
||||
pipeline_id: << pipeline.id >>
|
||||
job_type: deployment
|
||||
pipeline_number: << pipeline.number >>
|
||||
rome-app-build:
|
||||
docker:
|
||||
- image: cimg/node:18.18.2
|
||||
@@ -143,6 +172,12 @@ jobs:
|
||||
from: dist
|
||||
to: "s3://rome-online-production/"
|
||||
arguments: "--exclude '*.map'"
|
||||
- jira/notify:
|
||||
environment: Production (Rome) - Front End
|
||||
environment_type: production
|
||||
pipeline_id: << pipeline.id >>
|
||||
job_type: deployment
|
||||
pipeline_number: << pipeline.number >>
|
||||
|
||||
promanager-app-build:
|
||||
docker:
|
||||
@@ -168,6 +203,12 @@ jobs:
|
||||
from: dist
|
||||
to: "s3://promanager-production/"
|
||||
arguments: "--exclude '*.map'"
|
||||
- jira/notify:
|
||||
environment: Production (ProManager) - Front End
|
||||
environment_type: production
|
||||
pipeline_id: << pipeline.id >>
|
||||
job_type: deployment
|
||||
pipeline_number: << pipeline.number >>
|
||||
|
||||
test-rome-hasura-migrate:
|
||||
docker:
|
||||
@@ -183,10 +224,18 @@ jobs:
|
||||
- run:
|
||||
name: Execute migration
|
||||
command: |
|
||||
npm install hasura-cli -g
|
||||
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
|
||||
hasura migrate apply --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >>
|
||||
sleep 5
|
||||
hasura metadata apply --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >>
|
||||
sleep 10
|
||||
hasura metadata reload --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >>
|
||||
- jira/notify:
|
||||
environment: Test (Rome) - Hasura
|
||||
environment_type: testing
|
||||
pipeline_id: << pipeline.id >>
|
||||
job_type: deployment
|
||||
pipeline_number: << pipeline.number >>
|
||||
|
||||
test-rome-app-build:
|
||||
docker:
|
||||
@@ -212,6 +261,12 @@ jobs:
|
||||
from: dist
|
||||
to: "s3://rome-online-test/"
|
||||
arguments: "--exclude '*.map'"
|
||||
- jira/notify:
|
||||
environment: Test (Rome) - Front End
|
||||
environment_type: testing
|
||||
pipeline_id: << pipeline.id >>
|
||||
job_type: deployment
|
||||
pipeline_number: << pipeline.number >>
|
||||
|
||||
test-promanager-app-build:
|
||||
docker:
|
||||
@@ -237,6 +292,12 @@ jobs:
|
||||
from: dist
|
||||
to: "s3://promanager-testing/"
|
||||
arguments: "--exclude '*.map'"
|
||||
- jira/notify:
|
||||
environment: Test (ProManager) - Front End
|
||||
environment_type: testing
|
||||
pipeline_id: << pipeline.id >>
|
||||
job_type: deployment
|
||||
pipeline_number: << pipeline.number >>
|
||||
|
||||
test-hasura-migrate:
|
||||
docker:
|
||||
@@ -252,10 +313,18 @@ jobs:
|
||||
- run:
|
||||
name: Execute migration
|
||||
command: |
|
||||
npm install hasura-cli -g
|
||||
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
|
||||
hasura migrate apply --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
|
||||
sleep 15
|
||||
hasura metadata apply --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
|
||||
sleep 30
|
||||
hasura metadata reload --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
|
||||
- jira/notify:
|
||||
environment: Test (ImEX) - Hasura
|
||||
environment_type: testing
|
||||
pipeline_id: << pipeline.id >>
|
||||
job_type: deployment
|
||||
pipeline_number: << pipeline.number >>
|
||||
|
||||
imex-test-app-build:
|
||||
docker:
|
||||
@@ -302,7 +371,12 @@ jobs:
|
||||
from: dist
|
||||
to: "s3://imex-online-test-beta/"
|
||||
arguments: "--exclude '*.map'"
|
||||
|
||||
- jira/notify:
|
||||
environment: Test (ImEX) - Front End
|
||||
environment_type: testing
|
||||
pipeline_id: << pipeline.id >>
|
||||
job_type: deployment
|
||||
pipeline_number: << pipeline.number >>
|
||||
|
||||
admin-app-build:
|
||||
docker:
|
||||
@@ -353,7 +427,7 @@ workflows:
|
||||
secret: ${HASURA_PROD_SECRET}
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
only: master-AIO
|
||||
- rome-api-deploy:
|
||||
filters:
|
||||
branches:
|
||||
@@ -363,7 +437,7 @@ workflows:
|
||||
branches:
|
||||
only: master-AIO
|
||||
- rome-hasura-migrate:
|
||||
secret: ${HASURA_PROD_SECRET}
|
||||
secret: ${HASURA_ROME_PROD_SECRET}
|
||||
filters:
|
||||
branches:
|
||||
only: master-AIO
|
||||
|
||||
24
.dockerignore
Normal file
24
.dockerignore
Normal file
@@ -0,0 +1,24 @@
|
||||
# Directories to exclude
|
||||
.circleci
|
||||
.idea
|
||||
.platform
|
||||
.vscode
|
||||
_reference
|
||||
client
|
||||
redis/dockerdata
|
||||
hasura
|
||||
node_modules
|
||||
# Files to exclude
|
||||
.ebignore
|
||||
.editorconfig
|
||||
.eslintrc.json
|
||||
.gitignore
|
||||
.prettierrc.js
|
||||
Dockerfile
|
||||
README.MD
|
||||
bodyshop_translations.babel
|
||||
docker-compose.yml
|
||||
ecosystem.config.js
|
||||
|
||||
# Optional: Exclude logs and temporary files
|
||||
*.log
|
||||
0
.localstack/.gitkeep
Normal file
0
.localstack/.gitkeep
Normal file
15
.vscode/launch.json
vendored
15
.vscode/launch.json
vendored
@@ -14,6 +14,21 @@
|
||||
"request": "launch",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceRoot}/client/src"
|
||||
},
|
||||
{
|
||||
"name": "Attach to Node.js in Docker",
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"address": "localhost",
|
||||
"port": 9229,
|
||||
"localRoot": "${workspaceFolder}",
|
||||
"remoteRoot": "/app",
|
||||
"protocol": "inspector",
|
||||
"restart": true,
|
||||
"sourceMaps": true,
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
47
Dockerfile
Normal file
47
Dockerfile
Normal file
@@ -0,0 +1,47 @@
|
||||
# Use Amazon Linux 2023 as the base image
|
||||
FROM amazonlinux:2023
|
||||
|
||||
# Install Git and Node.js (Amazon Linux 2023 uses the DNF package manager)
|
||||
RUN dnf install -y git \
|
||||
&& curl -sL https://rpm.nodesource.com/setup_20.x | bash - \
|
||||
&& dnf install -y nodejs \
|
||||
&& dnf clean all
|
||||
|
||||
|
||||
# Install dependencies required by node-canvas
|
||||
RUN dnf install -y \
|
||||
gcc \
|
||||
gcc-c++ \
|
||||
cairo-devel \
|
||||
pango-devel \
|
||||
libjpeg-turbo-devel \
|
||||
giflib-devel \
|
||||
libpng-devel \
|
||||
make \
|
||||
python3 \
|
||||
python3-pip \
|
||||
&& dnf clean all
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# This is because our test route uses a git commit hash
|
||||
RUN git config --global --add safe.directory /app
|
||||
|
||||
# Copy package.json and package-lock.json
|
||||
COPY package.json ./
|
||||
|
||||
# Install Nodemon
|
||||
RUN npm install -g nodemon
|
||||
|
||||
# Install dependencies
|
||||
RUN npm i --no-package-lock
|
||||
|
||||
# Copy the rest of your application code
|
||||
COPY . .
|
||||
|
||||
# Expose the port your app runs on (adjust if necessary)
|
||||
EXPOSE 4000 9229
|
||||
|
||||
# Start the application
|
||||
CMD ["nodemon", "--legacy-watch", "--inspect=0.0.0.0:9229", "server.js"]
|
||||
64
_reference/dockerreadme.md
Normal file
64
_reference/dockerreadme.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Setting up External Networking and Static IP for WSL2 using Hyper-V
|
||||
|
||||
This guide will walk you through the steps to configure your WSL2 (Windows Subsystem for Linux) instance to use an external Hyper-V virtual switch, enabling it to connect directly to your local network. Additionally, you'll learn how to assign a static IP address to your WSL2 instance.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Windows 11**
|
||||
2. **Docker Desktop For Windows (Latest Version)
|
||||
|
||||
# Docker Setup
|
||||
Inside the root of the project exists the `docker-compose.yaml` file, you can simply run
|
||||
`docker-compose up` to launch the backend.
|
||||
|
||||
Things to note:
|
||||
- When installing NPM packages, you will need to rebuild the `node-app` container
|
||||
- Making changes to the server files will restart the `node-app`
|
||||
|
||||
# Local Stack
|
||||
- LocalStack Front end (Optional) - https://apps.microsoft.com/detail/9ntrnft9zws2?hl=en-us&gl=US
|
||||
- http://localhost:4566/_aws/ses will allow you to see emails sent
|
||||
|
||||
# Docker Commands
|
||||
|
||||
## General `docker-compose` Commands:
|
||||
1. Bring up the services, force a rebuild of all services, and do not use the cache: `docker-compose up --build --no-cache`
|
||||
2. Start Containers in Detached Mode: This will run the containers in the background (detached mode): `docker-compose up -d`
|
||||
3. Stop and Remove Containers: Stops and removes the containers gracefully: `docker-compose down`
|
||||
4. Stop containers without removing them: `docker-compose stop`
|
||||
5. Remove Containers, Volumes, and Networks: `docker-compose down --volumes`
|
||||
6. Force rebuild of containers: `docker-compose build --no-cache`
|
||||
7. View running Containers: `docker-compose ps`
|
||||
8. View a specific containers logs: `docker-compose logs <container-name>`
|
||||
9. Scale services (multiple instances of a service): `docker-compose up --scale <container-name>=<instances number> -d`
|
||||
10. Watch a specific containers logs in realtime with timestamps: `docker-compose logs -f --timestamps <container-name>`
|
||||
|
||||
## Volume Management Commands
|
||||
1. List Docker volumes: `docker volume ls`
|
||||
2. Remove Unused volumes `docker volume prune`
|
||||
3. Remove specific volumes `docker volume rm <volume-name>`
|
||||
4. Inspect a volume: `docker volume inspect <volume-name>`
|
||||
|
||||
## Container Image Management Commands:
|
||||
1. List running containers: `docker ps`
|
||||
2. List all containers: `docker os -a`
|
||||
3. Remove Stopped containers: `docker container prune`
|
||||
4. Remove a specific container: `docker container rm <container-name>`
|
||||
5. Remove a specific image: `docker rmi <image-name>:<version>`
|
||||
6. Remove all unused images: `docker image prune -a`
|
||||
|
||||
## Network Management Commands:
|
||||
1. List networks: `docker network ls`
|
||||
2. Inspect a specific network: `docker network inspect <network-name>`
|
||||
3. Remove a specific network: `docker network rm <network-name>`
|
||||
4. Remove unused networks: `docker network prune`
|
||||
|
||||
## Debugging and maintenance:
|
||||
1. Enter a Running container: `docker exec -it <container name> /bin/bash` (could also be `/bin/sh` or for example `redis-cli` on a redis node)
|
||||
2. View container resource usage: `docker stats`
|
||||
3. Check Disk space used by Docker: `docker system df`
|
||||
4. Remove all unused Data (Nuclear option): `docker system prune`
|
||||
|
||||
## Specific examples
|
||||
1. To simulate a Clean state, one should run `docker system prune` followed by `docker volume prune -a`
|
||||
2. You can run `docker-compose up` without the `-d` option, and you will get what is identical to the experience you were used to, this includes being able to control-c and bring the entire stack down
|
||||
59
_reference/prHelper.html
Normal file
59
_reference/prHelper.html
Normal file
@@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>IMEX IO Extractor</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
.output-box {
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
background-color: #f9f9f9;
|
||||
min-height: 40px;
|
||||
}
|
||||
.copy-button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>IMEX IO Extractor</h1>
|
||||
<textarea id="inputText" placeholder="Paste your text here..."></textarea>
|
||||
<br>
|
||||
<button onclick="extractIO()">Extract</button>
|
||||
|
||||
<div class="output-box" id="outputBox" contenteditable="true"></div>
|
||||
<button class="copy-button" onclick="copyToClipboard()">Copy to Clipboard</button>
|
||||
|
||||
<script>
|
||||
function extractIO() {
|
||||
const inputText = document.getElementById('inputText').value;
|
||||
const ioNumbers = [...new Set(inputText.match(/IO-\d{4}/g))] // Extract unique IO-#### matches
|
||||
.map(io => ({ io, num: parseInt(io.split('-')[1]) })) // Extract number part for sorting
|
||||
.sort((a, b) => a.num - b.num) // Sort by the number
|
||||
.map(item => item.io); // Extract sorted IO-####
|
||||
|
||||
document.getElementById('outputBox').innerText = ioNumbers.join(', '); // Display horizontally
|
||||
}
|
||||
|
||||
function copyToClipboard() {
|
||||
const outputBox = document.getElementById('outputBox');
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(outputBox);
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
document.execCommand('copy');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
20
certs/cert.pem
Normal file
20
certs/cert.pem
Normal file
@@ -0,0 +1,20 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDWzCCAkOgAwIBAgIUD/QBSAXy/AlJ/cS4DaPWJLpChxgwDQYJKoZIhvcNAQEL
|
||||
BQAwPTELMAkGA1UEBhMCQ0ExCzAJBgNVBAgMAk9OMSEwHwYDVQQKDBhJbnRlcm5l
|
||||
dCBXaWRnaXRzIFB0eSBMdGQwHhcNMjQwOTA5MTU0MjA1WhcNMjUwOTA5MTU0MjA1
|
||||
WjA9MQswCQYDVQQGEwJDQTELMAkGA1UECAwCT04xITAfBgNVBAoMGEludGVybmV0
|
||||
IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
|
||||
AKSd0l7NJCNBwvtPU+dVPQkteg0AfC3sGqRnZMQteCRVa2oIgC4NoF3A9BK/yHbF
|
||||
ZF25OnXTck5vzc8yb3v73ndfTD9ASKNoiaZE84/GFBsxqlKR8cs0qVwzuAsdijMv
|
||||
vlMPNlMRyE1Rb7nR6HXGkPXNyxgMko03NXPkvIje9zRudm0Lf8L4q/hPyPkS7Mrm
|
||||
/uQfAAJe+xFcupkEX2XY7r0x1C+z6E8lA1UcuhK3SHdW7CWYqp1vU5/dnnUiXwCa
|
||||
GiC6Y1bCJB0pDAVISzy3JUDdINZdiqGR+y8ho3pstChf2mp/76s3N9eG9KA/qaFK
|
||||
BrGk2PvCoZ8/Aj1aMsRYFHECAwEAAaNTMFEwHQYDVR0OBBYEFDLJ2fbWP4VUJgOp
|
||||
PSs+NGHcVgRmMB8GA1UdIwQYMBaAFDLJ2fbWP4VUJgOpPSs+NGHcVgRmMA8GA1Ud
|
||||
EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBABfv5ut/y03atq0NMB0jeDY4
|
||||
AvW4ukk0k1svyqxFZCw9o7m2lHb/IjmVrZG1Sj4JWrrSv0s02ccb26/t6vazNa5L
|
||||
Powe3eyfHgfjTZJmgs8hyeMwKS0wWk/SPuu9JDhIJakiquqD+UVBGkHpP+XYvhDv
|
||||
vhS2XRlW+aEjpUmr1oCyyrc6WbzrYRNadqEsn/AxwcMyUbht3Ugjkg+OpidcTIQp
|
||||
5lv63waKo6I1vQofzBQ3L7JYsKo8kC0vAP7wkLxvzBii335uZJzzpFYFVOyVNezi
|
||||
dJdazPbRYbXz4LjltdEn/SNfRuKX8ZRiN2OSo7OfSrZaMTS87SfCSFJGgQM8Yrk=
|
||||
-----END CERTIFICATE-----
|
||||
27
certs/id_rsa
Normal file
27
certs/id_rsa
Normal file
@@ -0,0 +1,27 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
|
||||
NhAAAAAwEAAQAAAQEAvNl5fuVmLNv72BZNxnTqX5CHf5Xi8UxjYaYxHITSCx7blnhpVYLd
|
||||
qXvcOWXzbsfjch/den73QiW4n2FYz75oGMhUGlOYzdWKA9I9Sj09Qy1R06RhwDiZGd5qaM
|
||||
swEeXpkNmi2u4Qd2kJeDfUQUigjC09V81O/vrniGtQAJScfiG/itdm+Ufn09Z4MYk0HWjq
|
||||
iDokNEskoEPsibYIrb+Q6vdtuPkZO+wU/smXhPtgw5ST6oQdmm/gVNsRg5XNzxrire+z1G
|
||||
WatnnVL3hPnnfpnf8W589dyms7GGJwhPerSGTN1bn0T4+9C69Cd7LBJtxiuFdRmdlGLLLP
|
||||
RR48Rur71wAAA9AEfVsdBH1bHQAAAAdzc2gtcnNhAAABAQC82Xl+5WYs2/vYFk3GdOpfkI
|
||||
d/leLxTGNhpjEchNILHtuWeGlVgt2pe9w5ZfNux+NyH916fvdCJbifYVjPvmgYyFQaU5jN
|
||||
1YoD0j1KPT1DLVHTpGHAOJkZ3mpoyzAR5emQ2aLa7hB3aQl4N9RBSKCMLT1XzU7++ueIa1
|
||||
AAlJx+Ib+K12b5R+fT1ngxiTQdaOqIOiQ0SySgQ+yJtgitv5Dq9224+Rk77BT+yZeE+2DD
|
||||
lJPqhB2ab+BU2xGDlc3PGuKt77PUZZq2edUveE+ed+md/xbnz13KazsYYnCE96tIZM3Vuf
|
||||
RPj70Lr0J3ssEm3GK4V1GZ2UYsss9FHjxG6vvXAAAAAwEAAQAAAQAQTosSLQbMmtY9S3e9
|
||||
yjyusdExcCTfhyQRu4MEHmfws+JsNMuLqbgwOVTD1AzYJQR7x0qdmDcLjCxL/uDnV16vvS
|
||||
Sd/Vf1dhnryIyoS29tzI0DRG94ZKq7tBvmHp1w/jRT4KcSVnovhW9e5Rs74+SRFhr06PKI
|
||||
S+wQOIv48Nwue9+QUMsMCpWgKXHx7SHNTHvnAfqdhi9O29SWlMA+v+mELZ5Cl+HU0UTt2I
|
||||
A1BxOe1N8FjN7KE2viJexsl3is1PuqMkpLl/wyHBJTVzUadl6DRALJQIm7/YO5goE72YOV
|
||||
Lpo27do3zjhC87dlKdATvZUzfKV0LuUVdxq/PNDZMUbBAAAAgQDShAqDZiDrdTUaGXfUVm
|
||||
QzcnVNbh2/KgZh4uux9QNHST562W6cnN7qxoRwVrM4BCOk1Kl73QQZW4nDvXX3PVC5j038
|
||||
8AXkcBHS9j9f4h72ue7D2jqlbHFa7aGU9zYgk9mbBF+GX3tDntkAIQjLtwOLfj1iiJ/clX
|
||||
mHFUAY1V4L8AAAAIEA3E4t/v0yU5D9AOI0r17UNYqfeyDoKAEDR4QbbFjO1l0kLnEJy7Zx
|
||||
Mhj18GilYg2y0P0v8dSM/oWXS8Hua2t5i9Exlv6gHhGlQ80mwYcVGIxewZ/pPeCPw0U+kt
|
||||
EKUjt09m9Oe7+6xHQsTBj9hY8/vqPmQwRalZFcLdhHiDiVKTcAAACBANtykaPXdVzEFx7D
|
||||
UOlsjVL7zM0EVOFXf9JJQ6BhazhmsEI2PYt3IpgGMo8cXkoUofAOIYjf421AabN1BqSO5J
|
||||
XTMxM0ZV3JmLLi804Mu9h1iFrVTBdLYOMJdc2VCo1EwHWpo9SXOyjxce/znvcIOU04aZhu
|
||||
TaPg816X+E+gw5JhAAAAFGRhdmVARGF2ZVJpY2hlci1JTUVYAQIDBAUG
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
certs/id_rsa.pub
Normal file
1
certs/id_rsa.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC82Xl+5WYs2/vYFk3GdOpfkId/leLxTGNhpjEchNILHtuWeGlVgt2pe9w5ZfNux+NyH916fvdCJbifYVjPvmgYyFQaU5jN1YoD0j1KPT1DLVHTpGHAOJkZ3mpoyzAR5emQ2aLa7hB3aQl4N9RBSKCMLT1XzU7++ueIa1AAlJx+Ib+K12b5R+fT1ngxiTQdaOqIOiQ0SySgQ+yJtgitv5Dq9224+Rk77BT+yZeE+2DDlJPqhB2ab+BU2xGDlc3PGuKt77PUZZq2edUveE+ed+md/xbnz13KazsYYnCE96tIZM3VufRPj70Lr0J3ssEm3GK4V1GZ2UYsss9FHjxG6vvX dave@DaveRicher-IMEX
|
||||
28
certs/key.pem
Normal file
28
certs/key.pem
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCkndJezSQjQcL7
|
||||
T1PnVT0JLXoNAHwt7BqkZ2TELXgkVWtqCIAuDaBdwPQSv8h2xWRduTp103JOb83P
|
||||
Mm97+953X0w/QEijaImmRPOPxhQbMapSkfHLNKlcM7gLHYozL75TDzZTEchNUW+5
|
||||
0eh1xpD1zcsYDJKNNzVz5LyI3vc0bnZtC3/C+Kv4T8j5EuzK5v7kHwACXvsRXLqZ
|
||||
BF9l2O69MdQvs+hPJQNVHLoSt0h3VuwlmKqdb1Of3Z51Il8AmhogumNWwiQdKQwF
|
||||
SEs8tyVA3SDWXYqhkfsvIaN6bLQoX9pqf++rNzfXhvSgP6mhSgaxpNj7wqGfPwI9
|
||||
WjLEWBRxAgMBAAECggEAUNpHYlLFxh9dokujPUMreF+Cy/IKDBAkQc2au5RNpyLh
|
||||
YDIOqw/8TTAhcTgLQPLQygvZP9f8E7RsVLFD+pSJ/v2qmIJ9au1Edor1Sg+S/oxV
|
||||
SLrwFMunx2aLpcH7iAqSI3+cQg7A3+D4zD7iOz6tIl3Su9wo+v073tFhHKTOrEwv
|
||||
Qgr9Jf3viIiKV1ym+uQEVQndocfsj46FnFpXTQ2qs7kAF6FgAOLDGfQQwzkiqEBD
|
||||
NsqsDmbYIx6foZL+DEz1ZVO2M5B+xxpbNK82KwuQilVpimW8ui4LZHCe+RIFzt9+
|
||||
BK6KGlLpSEwTFliivI3nahy18JzskZsfyah0CPZlQQKBgQDVv+A0qIPGvOP3Sx+9
|
||||
HyeQCV23SkvvSvw8p8pMB0gvwv63YdJ7N/rJzBGS6YUHFWWZZgEeTgkJ6VJvoe0r
|
||||
8JL1el9uSUa7f0eayjmFBOGuzpktNVdIn2Tg7A9MWA4JqPNNC69RMOh86ewGD4J3
|
||||
a8Hz2a1bHxAmy/AZt2ukypY6eQKBgQDFJ7kqeOPkRBz9WbALRgVIXo8YWf5di0sQ
|
||||
r0HC03GAISHQ725A2IFBPHJWeqj0jaMiIZD0y+Obgp7KAskrJaLfsd7Ug775kFfw
|
||||
oUI9UAl6kRuPKvm3BaVAm46SQm+56VsgxTi73YN0NUp75THHZgAJjepF9zSpVJxq
|
||||
VY9DjEGruQKBgQCQCpGIatcCol/tUg69X7VFd0pULhkl1J5OMbQ9r9qRdRI5eg5h
|
||||
QsQaIQ7mtb8TmvOwf/DY/zVQHI+U8sXlCmW+TwzoQTENQSR7xzMj1LpRFqBaustr
|
||||
AR72A537kItFLzll/i3SxOam5uxK2UDOQSuerF4KPdCglGXkrpo3nt3F4QKBgQCa
|
||||
RArPAOjQo7tLQfJN3+wiRFsTYtd1uphx5bA/EdOtvj8HjVFnzADXWsTchf3N3UXY
|
||||
XwtdgGwIMpys1KEz8a8P+c2x26SDAj7NOmDqOMYx8Xju/WGHpBM6Cn30U6e4gK+d
|
||||
ZLSPyzQgqdIuP5hDvbwpvbGiLVw3Ys1BJtGCuSxpgQJ/eHnRiuSi5Zq5jGg+GpA+
|
||||
FEEc9NCy772rR+4uzEOqyIwqewffqzSuVWuIsY/8MP3fh+NDxl/mU6cB5QVeD54Z
|
||||
JZUKwmpM26muiM6WvVnM4ExPdSGA2+l4pZjby/KKd6F/w0tgZ1jb9Pb2/0vN3qVA
|
||||
Y4U4XNTMt2fxUACqiL4SHA==
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -1,5 +1,5 @@
|
||||
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
|
||||
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
|
||||
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.imex.online/v1/graphql
|
||||
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.imex.online/v1/graphql
|
||||
VITE_APP_GA_CODE=231099835
|
||||
VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
|
||||
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
|
||||
@@ -8,7 +8,7 @@ VITE_APP_CLOUDINARY_API_KEY=957865933348715
|
||||
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
||||
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
|
||||
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
||||
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000
|
||||
VITE_APP_AXIOS_BASE_API_URL=/api/
|
||||
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
|
||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||
VITE_APP_INSTANCE=IMEX
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
|
||||
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
|
||||
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.imex.online/v1/graphql
|
||||
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.imex.online/v1/graphql
|
||||
VITE_APP_GA_CODE=231099835
|
||||
VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
|
||||
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
|
||||
@@ -8,7 +8,7 @@ VITE_APP_CLOUDINARY_API_KEY=957865933348715
|
||||
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
||||
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
|
||||
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
||||
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000
|
||||
VITE_APP_AXIOS_BASE_API_URL=/api/
|
||||
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
|
||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||
VITE_APP_INSTANCE=PROMANAGER
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
|
||||
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
|
||||
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.imex.online/v1/graphql
|
||||
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.imex.online/v1/graphql
|
||||
VITE_APP_GA_CODE=231099835
|
||||
VITE_APP_FIREBASE_CONFIG={ "apiKey": "AIzaSyAuLQR9SV5LsVxjU8wh9hvFLdhcAHU6cxE", "authDomain": "rome-prod-1.firebaseapp.com", "projectId": "rome-prod-1", "storageBucket": "rome-prod-1.appspot.com", "messagingSenderId": "147786367145", "appId": "1:147786367145:web:9d4cba68071c3f29a8a9b8", "measurementId": "G-G8Z9DRHTZS"}
|
||||
# VITE_APP_FIREBASE_CONFIG={ "apiKey": "AIzaSyAuLQR9SV5LsVxjU8wh9hvFLdhcAHU6cxE", "authDomain": "rome-prod-1.firebaseapp.com", "projectId": "rome-prod-1", "storageBucket": "rome-prod-1.appspot.com", "messagingSenderId": "147786367145", "appId": "1:147786367145:web:9d4cba68071c3f29a8a9b8", "measurementId": "G-G8Z9DRHTZS"}
|
||||
VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
|
||||
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
|
||||
VITE_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test
|
||||
VITE_APP_CLOUDINARY_API_KEY=957865933348715
|
||||
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
||||
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BP1B7ZTYpn-KMt6nOxlld6aS8Skt3Q7ZLEqP0hAvGHxG4UojPYiXZ6kPlzZkUC5jH-EcWXomTLtmadAIxurfcHo'
|
||||
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
||||
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000
|
||||
VITE_APP_AXIOS_BASE_API_URL=/api/
|
||||
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
|
||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||
VITE_APP_COUNTRY=USA
|
||||
|
||||
1
client/.gitignore
vendored
1
client/.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
|
||||
# Sentry Config File
|
||||
.sentryclirc
|
||||
/dev-dist
|
||||
|
||||
@@ -12,6 +12,6 @@ module.exports = defineConfig({
|
||||
setupNodeEvents(on, config) {
|
||||
return require("./cypress/plugins/index.js")(on, config);
|
||||
},
|
||||
baseUrl: "http://localhost:3000"
|
||||
baseUrl: "https://localhost:3000"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||
<meta http-equiv="Pragma" content="no-cache">
|
||||
<meta http-equiv="Expires" content="0">
|
||||
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
|
||||
<link rel="icon" href="/favicon.png"/>
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
|
||||
@@ -14,7 +17,7 @@
|
||||
<meta name="theme-color" content="#1690ff"/>
|
||||
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
|
||||
<!-- TODO:AIo Update the individual logos for each.-->
|
||||
<link rel="apple-touch-icon" href="public/logo192.png"/>
|
||||
<link rel="apple-touch-icon" href="/logo192.png"/>
|
||||
<link rel="mask-icon" href="/mask-icon.svg" color="#FFFFFF">
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
@@ -46,77 +49,23 @@
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
|
||||
<meta name="description" content="Rome Online"/>
|
||||
<title>Rome Online</title>
|
||||
|
||||
<!--Use the below code snippet to provide real time updates to the live chat plugin without the need of copying and paste each time to your website when changes are made via PBX-->
|
||||
|
||||
<call-us-selector phonesystem-url=https://rometech.east.3cx.us:5001
|
||||
party="LiveChat528346"></call-us-selector>
|
||||
|
||||
<!--Incase you don't want real time updates to the live chat plugin when options are changed, use the below code snippet. Please note that each time you change the settings you will need to copy and paste the snippet code to your website-->
|
||||
|
||||
<!--<call-us
|
||||
|
||||
phonesystem-url=https://rometech.east.3cx.us:5001
|
||||
|
||||
style="position:fixed;font-size:16px;line-height:17px;z-index: 99999;right: 20px; bottom: 20px;"
|
||||
|
||||
id="wp-live-chat-by-3CX"
|
||||
|
||||
minimized="true"
|
||||
|
||||
animation-style="noanimation"
|
||||
|
||||
party="LiveChat528346"
|
||||
|
||||
minimized-style="bubbleright"
|
||||
|
||||
allow-call="true"
|
||||
|
||||
allow-video="false"
|
||||
|
||||
allow-soundnotifications="true"
|
||||
|
||||
enable-mute="true"
|
||||
|
||||
enable-onmobile="true"
|
||||
|
||||
offline-enabled="true"
|
||||
|
||||
enable="true"
|
||||
|
||||
ignore-queueownership="false"
|
||||
|
||||
authentication="both"
|
||||
|
||||
show-operator-actual-name="true"
|
||||
|
||||
aknowledge-received="true"
|
||||
|
||||
gdpr-enabled="false"
|
||||
|
||||
message-userinfo-format="name"
|
||||
|
||||
message-dateformat="both"
|
||||
|
||||
lang="browser"
|
||||
|
||||
button-icon-type="default"
|
||||
|
||||
greeting-visibility="none"
|
||||
|
||||
greeting-offline-visibility="none"
|
||||
|
||||
chat-delay="2000"
|
||||
|
||||
enable-direct-call="true"
|
||||
|
||||
enable-ga="false"
|
||||
|
||||
></call-us>-->
|
||||
|
||||
<script defer src=https://downloads-global.3cx.com/downloads/livechatandtalk/v1/callus.js
|
||||
id="tcx-callus-js" charset="utf-8"></script>
|
||||
|
||||
<script type="text/javascript" id="zsiqchat">
|
||||
var $zoho = $zoho || {};
|
||||
$zoho.salesiq = $zoho.salesiq || {
|
||||
widgetcode: "siq01bb8ac617280bdacddfeb528f07734dadc64ef3f05efef9f769c1ec171af666",
|
||||
values: {},
|
||||
ready: function () {
|
||||
}
|
||||
};
|
||||
var d = document;
|
||||
s = d.createElement("script");
|
||||
s.type = "text/javascript";
|
||||
s.id = "zsiqscript";
|
||||
s.defer = true;
|
||||
s.src = "https://salesiq.zohopublic.com/widget";
|
||||
t = d.getElementsByTagName("script")[0];
|
||||
t.parentNode.insertBefore(s, t);
|
||||
</script>
|
||||
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %>
|
||||
<title>ProManager</title>
|
||||
|
||||
2128
client/package-lock.json
generated
2128
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,37 +9,37 @@
|
||||
"proxy": "http://localhost:4000",
|
||||
"dependencies": {
|
||||
"@ant-design/pro-layout": "^7.19.12",
|
||||
"@apollo/client": "^3.11.4",
|
||||
"@emotion/is-prop-valid": "^1.3.0",
|
||||
"@fingerprintjs/fingerprintjs": "^4.4.3",
|
||||
"@apollo/client": "^3.11.8",
|
||||
"@emotion/is-prop-valid": "^1.3.1",
|
||||
"@fingerprintjs/fingerprintjs": "^4.5.0",
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.2.7",
|
||||
"@sentry/cli": "^2.33.1",
|
||||
"@sentry/cli": "^2.36.2",
|
||||
"@sentry/react": "^7.114.0",
|
||||
"@splitsoftware/splitio-react": "^1.12.1",
|
||||
"@splitsoftware/splitio-react": "^1.13.0",
|
||||
"@tanem/react-nprogress": "^5.0.51",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"antd": "^5.20.1",
|
||||
"apollo-link-logger": "^2.0.1",
|
||||
"apollo-link-sentry": "^3.3.0",
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.7.4",
|
||||
"axios": "^1.7.7",
|
||||
"classnames": "^2.5.1",
|
||||
"css-box-model": "^1.2.1",
|
||||
"dayjs": "^1.11.12",
|
||||
"dayjs": "^1.11.13",
|
||||
"dayjs-business-days2": "^1.2.2",
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"env-cmd": "^10.1.0",
|
||||
"exifr": "^7.1.3",
|
||||
"firebase": "^10.12.5",
|
||||
"firebase": "^10.13.2",
|
||||
"graphql": "^16.9.0",
|
||||
"i18next": "^23.12.3",
|
||||
"i18next": "^23.15.1",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"libphonenumber-js": "^1.11.5",
|
||||
"libphonenumber-js": "^1.11.9",
|
||||
"logrocket": "^8.1.2",
|
||||
"markerjs2": "^2.32.1",
|
||||
"markerjs2": "^2.32.2",
|
||||
"memoize-one": "^6.0.0",
|
||||
"normalize-url": "^8.0.1",
|
||||
"object-hash": "^3.0.0",
|
||||
@@ -47,7 +47,7 @@
|
||||
"query-string": "^9.1.0",
|
||||
"raf-schd": "^4.0.3",
|
||||
"react": "^18.3.1",
|
||||
"react-big-calendar": "^1.13.2",
|
||||
"react-big-calendar": "^1.14.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-cookie": "^7.2.0",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -58,15 +58,15 @@
|
||||
"react-icons": "^5.3.0",
|
||||
"react-image-lightbox": "^5.1.4",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-number-format": "^5.4.0",
|
||||
"react-number-format": "^5.4.2",
|
||||
"react-popopo": "^2.1.9",
|
||||
"react-product-fruits": "^2.2.6",
|
||||
"react-product-fruits": "^2.2.61",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-resizable": "^3.0.5",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"react-router-dom": "^6.26.2",
|
||||
"react-sticky": "^6.0.3",
|
||||
"react-virtualized": "^9.22.5",
|
||||
"react-virtuoso": "^4.10.1",
|
||||
"react-virtuoso": "^4.10.4",
|
||||
"recharts": "^2.12.7",
|
||||
"redux": "^5.0.1",
|
||||
"redux-actions": "^3.0.3",
|
||||
@@ -74,22 +74,26 @@
|
||||
"redux-saga": "^1.3.0",
|
||||
"redux-state-sync": "^3.1.4",
|
||||
"reselect": "^5.1.1",
|
||||
"sass": "^1.77.8",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"styled-components": "^6.1.12",
|
||||
"sass": "^1.79.3",
|
||||
"socket.io-client": "^4.8.0",
|
||||
"styled-components": "^6.1.13",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
"use-memo-one": "^1.1.3",
|
||||
"userpilot": "^1.3.5",
|
||||
"userpilot": "^1.3.6",
|
||||
"vite-plugin-ejs": "^1.7.0",
|
||||
"web-vitals": "^3.5.2"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "echo 'when updating react-big-calendar, remember to check to localizer in the calendar wrapper'",
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||
"start": "vite",
|
||||
"build": "dotenvx run --env-file=.env.development.imex -- vite build",
|
||||
"start:imex": "dotenvx run --env-file=.env.development.imex -- vite",
|
||||
"start:rome": "dotenvx run --env-file=.env.development.rome -- vite",
|
||||
"start:promanager": "dotenvx run --env-file=.env.development.promanager -- vite",
|
||||
"preview:imex": "dotenvx run --env-file=.env.development.imex -- vite preview",
|
||||
"preview:rome": "dotenvx run --env-file=.env.development.rome -- vite preview",
|
||||
"preview:promanager": "dotenvx run --env-file=.env.development.promanager -- vite preview",
|
||||
"build:test:imex": "env-cmd -f .env.test.imex npm run build",
|
||||
"build:test:rome": "env-cmd -f .env.test.rome npm run build",
|
||||
"build:test:promanager": "env-cmd -f .env.test.promanager npm run build",
|
||||
@@ -130,29 +134,30 @@
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-react": "^7.24.7",
|
||||
"@dotenvx/dotenvx": "^1.7.0",
|
||||
"@dotenvx/dotenvx": "^1.14.1",
|
||||
"@emotion/babel-plugin": "^11.12.0",
|
||||
"@emotion/react": "^11.13.0",
|
||||
"@sentry/webpack-plugin": "^2.22.2",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@sentry/webpack-plugin": "^2.22.4",
|
||||
"@testing-library/cypress": "^10.0.2",
|
||||
"browserslist": "^4.23.3",
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
"chalk": "^5.3.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "^13.13.3",
|
||||
"cypress": "^13.14.2",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-cypress": "^2.15.1",
|
||||
"memfs": "^4.11.1",
|
||||
"memfs": "^4.12.0",
|
||||
"os-browserify": "^0.3.0",
|
||||
"react-error-overlay": "6.0.11",
|
||||
"redux-logger": "^3.0.6",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"vite": "^5.4.0",
|
||||
"vite": "^5.4.7",
|
||||
"vite-plugin-babel": "^1.2.0",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-legacy": "^2.1.0",
|
||||
"vite-plugin-node-polyfills": "^0.22.0",
|
||||
"vite-plugin-pwa": "^0.20.1",
|
||||
"vite-plugin-style-import": "^2.0.0"
|
||||
"vite-plugin-pwa": "^0.20.5",
|
||||
"vite-plugin-style-import": "^2.0.0",
|
||||
"workbox-window": "^7.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,10 @@ import { checkUserSession } from "../redux/user/user.actions";
|
||||
import { selectBodyshop, selectCurrentEula, selectCurrentUser } from "../redux/user/user.selectors";
|
||||
import PrivateRoute from "../components/PrivateRoute";
|
||||
import "./App.styles.scss";
|
||||
import handleBeta from "../utils/handleBeta";
|
||||
import Eula from "../components/eula/eula.component";
|
||||
import InstanceRenderMgr from "../utils/instanceRenderMgr";
|
||||
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
|
||||
import { SocketProvider } from "../contexts/SocketIO/socketContext.jsx";
|
||||
|
||||
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
|
||||
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
|
||||
@@ -108,8 +108,6 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
||||
return <LoadingSpinner message={t("general.labels.loggingin")} />;
|
||||
}
|
||||
|
||||
handleBeta();
|
||||
|
||||
if (!online) {
|
||||
return (
|
||||
<Result
|
||||
@@ -204,7 +202,9 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
||||
path="/manage/*"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||
<SocketProvider bodyshop={bodyshop}>
|
||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||
</SocketProvider>
|
||||
</ErrorBoundary>
|
||||
}
|
||||
>
|
||||
@@ -214,7 +214,9 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
||||
path="/tech/*"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||
<SocketProvider bodyshop={bodyshop}>
|
||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||
</SocketProvider>
|
||||
</ErrorBoundary>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { exportPageLimit } from "../../utils/config";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
|
||||
@@ -175,7 +175,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
|
||||
<Table
|
||||
loading={loading}
|
||||
dataSource={dataSource}
|
||||
pagination={{ position: "top", pageSize: pageLimit }}
|
||||
pagination={{ position: "top", pageSize: exportPageLimit }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
onChange={handleTableChange}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { exportPageLimit } from "../../utils/config";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
@@ -177,7 +177,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, payments,
|
||||
<Table
|
||||
loading={loading}
|
||||
dataSource={dataSource}
|
||||
pagination={{ position: "top", pageSize: pageLimit }}
|
||||
pagination={{ position: "top", pageSize: exportPageLimit }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
onChange={handleTableChange}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Button, Card, Input, Space, Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { exportPageLimit } from "../../utils/config";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
||||
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import JobMarkSelectedExported from "../jobs-mark-selected-exported/jobs-mark-selected-exported";
|
||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
@@ -201,7 +201,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
|
||||
<Table
|
||||
loading={loading}
|
||||
dataSource={dataSource}
|
||||
pagination={{ position: "top" }}
|
||||
pagination={{ position: "top", pageSize: exportPageLimit }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
onChange={handleTableChange}
|
||||
|
||||
@@ -98,7 +98,7 @@ export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail
|
||||
});
|
||||
|
||||
billlines.forEach((billline) => {
|
||||
const { deductedfromlbr, inventories, jobline, ...il } = billline;
|
||||
const { deductedfromlbr, inventories, jobline, original_actual_price, create_ppc, ...il } = billline;
|
||||
delete il.__typename;
|
||||
|
||||
if (il.id) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { DeleteFilled, CopyFilled } from "@ant-design/icons";
|
||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||
import { Button, Card, Col, Form, Input, Row, Space, Spin, Statistic, notification } from "antd";
|
||||
import { Button, Card, Col, Form, Input, Row, Space, Spin, Statistic, message, notification } from "antd";
|
||||
import axios from "axios";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -14,10 +14,12 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
||||
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
|
||||
import { getCurrentUser } from "../../firebase/firebase.utils";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
cardPaymentModal: selectCardPayment,
|
||||
bodyshop: selectBodyshop
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: getCurrentUser
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
@@ -25,11 +27,17 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment"))
|
||||
});
|
||||
|
||||
const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisible, insertAuditTrail }) => {
|
||||
const CardPaymentModalComponent = ({
|
||||
bodyshop,
|
||||
currentUser,
|
||||
cardPaymentModal,
|
||||
toggleModalVisible,
|
||||
insertAuditTrail
|
||||
}) => {
|
||||
const { context, actions } = cardPaymentModal;
|
||||
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [paymentLink, setPaymentLink] = useState();
|
||||
const [loading, setLoading] = useState(false);
|
||||
// const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
||||
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
|
||||
@@ -37,7 +45,7 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
|
||||
const [, { data, refetch, queryLoading }] = useLazyQuery(QUERY_RO_AND_OWNER_BY_JOB_PKS, {
|
||||
variables: { jobids: [context.jobid] },
|
||||
skip: true
|
||||
skip: !context?.jobid
|
||||
});
|
||||
|
||||
//Initialize the intellipay window.
|
||||
@@ -51,8 +59,7 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
//2024-04-25: Nothing is going to happen here anymore. We'll completely rely on the callback.
|
||||
//Add a slight delay to allow the refetch to properly get the data.
|
||||
setTimeout(() => {
|
||||
if (actions && actions.refetch && typeof actions.refetch === "function")
|
||||
actions.refetch();
|
||||
if (actions && actions.refetch && typeof actions.refetch === "function") actions.refetch();
|
||||
setLoading(false);
|
||||
toggleModalVisible();
|
||||
}, 750);
|
||||
@@ -86,7 +93,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const handleIntelliPayCharge = async () => {
|
||||
setLoading(true);
|
||||
//Validate
|
||||
@@ -101,7 +107,7 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
const response = await axios.post("/intellipay/lightbox_credentials", {
|
||||
bodyshop,
|
||||
refresh: !!window.intellipay,
|
||||
paymentSplitMeta: form.getFieldsValue(),
|
||||
paymentSplitMeta: form.getFieldsValue()
|
||||
});
|
||||
|
||||
if (window.intellipay) {
|
||||
@@ -126,6 +132,42 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
}
|
||||
};
|
||||
|
||||
const handleIntelliPayChargeShortLink = async () => {
|
||||
setLoading(true);
|
||||
//Validate
|
||||
try {
|
||||
await form.validateFields();
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { payments } = form.getFieldsValue();
|
||||
const response = await axios.post("/intellipay/generate_payment_url", {
|
||||
bodyshop,
|
||||
amount: payments?.reduce((acc, val) => {
|
||||
return acc + (val?.amount || 0);
|
||||
}, 0),
|
||||
account: payments && data && data.jobs.length > 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null,
|
||||
comment: btoa(JSON.stringify({ payments, userEmail: currentUser.email })),
|
||||
paymentSplitMeta: form.getFieldsValue()
|
||||
});
|
||||
if (response.data) {
|
||||
setPaymentLink(response.data?.shorUrl);
|
||||
navigator.clipboard.writeText(response.data?.shorUrl);
|
||||
message.success(t("general.actions.copied"));
|
||||
}
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
notification.open({
|
||||
type: "error",
|
||||
message: t("job_payments.notifications.error.openingip")
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title="Card Payment">
|
||||
<Spin spinning={loading}>
|
||||
@@ -202,16 +244,14 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
|
||||
<Form.Item
|
||||
shouldUpdate={(prevValues, curValues) =>
|
||||
prevValues.payments?.map((p) => p?.jobid).join() !== curValues.payments?.map((p) => p?.jobid).join()
|
||||
prevValues.payments?.map((p) => p?.jobid + p?.amount).join() !==
|
||||
curValues.payments?.map((p) => p?.jobid + p?.amount).join()
|
||||
}
|
||||
>
|
||||
{() => {
|
||||
//If all of the job ids have been fileld in, then query and update the IP field.
|
||||
const { payments } = form.getFieldsValue();
|
||||
if (
|
||||
payments?.length > 0 &&
|
||||
payments?.filter((p) => p?.jobid).length === payments?.length
|
||||
) {
|
||||
if (payments?.length > 0 && payments?.filter((p) => p?.jobid).length === payments?.length) {
|
||||
refetch({ jobids: payments.map((p) => p.jobid) });
|
||||
}
|
||||
return (
|
||||
@@ -246,7 +286,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
const totalAmountToCharge = payments?.reduce((acc, val) => {
|
||||
return acc + (val?.amount || 0);
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<Space style={{ float: "right" }}>
|
||||
<Statistic title="Amount To Charge" value={totalAmountToCharge} precision={2} />
|
||||
@@ -273,11 +312,36 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
>
|
||||
{t("job_payments.buttons.proceedtopayment")}
|
||||
</Button>
|
||||
<Space direction="vertical" align="center">
|
||||
<Button
|
||||
type="primary"
|
||||
// data-ipayname="submit"
|
||||
className="ipayfield"
|
||||
loading={queryLoading || loading}
|
||||
disabled={!(totalAmountToCharge > 0)}
|
||||
onClick={handleIntelliPayChargeShortLink}
|
||||
>
|
||||
{t("job_payments.buttons.create_short_link")}
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
{paymentLink && (
|
||||
<Space
|
||||
style={{ cursor: "pointer", float: "right" }}
|
||||
align="end"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(paymentLink);
|
||||
message.success(t("general.actions.copied"));
|
||||
}}
|
||||
>
|
||||
<div>{paymentLink}</div>
|
||||
<CopyFilled />
|
||||
</Space>
|
||||
)}
|
||||
</Spin>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -3,13 +3,15 @@ import axios from "axios";
|
||||
import _ from "lodash";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||
|
||||
export default function GlobalSearchOs() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState(false);
|
||||
|
||||
@@ -177,7 +179,18 @@ export default function GlobalSearchOs() {
|
||||
};
|
||||
|
||||
return (
|
||||
<AutoComplete options={data} onSearch={handleSearch} defaultActiveFirstOption onClear={() => setData([])}>
|
||||
<AutoComplete
|
||||
options={data}
|
||||
onSearch={handleSearch}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key !== "Enter") return;
|
||||
const firstUrlForSearch = data?.[0]?.options?.[0]?.label?.props?.to;
|
||||
if (!firstUrlForSearch) return;
|
||||
navigate(firstUrlForSearch);
|
||||
}}
|
||||
defaultActiveFirstOption
|
||||
onClear={() => setData([])}
|
||||
>
|
||||
<Input.Search
|
||||
size="large"
|
||||
placeholder={t("general.labels.globalsearch")}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AutoComplete, Divider, Input, Space } from "antd";
|
||||
import _ from "lodash";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries";
|
||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
@@ -13,6 +13,7 @@ import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.compon
|
||||
export default function GlobalSearch() {
|
||||
const { t } = useTranslation();
|
||||
const [callSearch, { loading, error, data }] = useLazyQuery(GLOBAL_SEARCH_QUERY);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const executeSearch = (v) => {
|
||||
if (v && v.variables.search && v.variables.search !== "" && v.variables.search.length >= 3) callSearch(v);
|
||||
@@ -20,7 +21,6 @@ export default function GlobalSearch() {
|
||||
const debouncedExecuteSearch = _.debounce(executeSearch, 750);
|
||||
|
||||
const handleSearch = (value) => {
|
||||
console.log("Handle Search");
|
||||
debouncedExecuteSearch({ variables: { search: value } });
|
||||
};
|
||||
|
||||
@@ -156,7 +156,17 @@ export default function GlobalSearch() {
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
return (
|
||||
<AutoComplete options={options} onSearch={handleSearch} defaultActiveFirstOption>
|
||||
<AutoComplete
|
||||
options={options}
|
||||
onSearch={handleSearch}
|
||||
defaultActiveFirstOption
|
||||
onKeyDown={(e) => {
|
||||
if (e.key !== "Enter") return;
|
||||
const firstUrlForSearch = options?.[0]?.options?.[0]?.label?.props?.to;
|
||||
if (!firstUrlForSearch) return;
|
||||
navigate(firstUrlForSearch);
|
||||
}}
|
||||
>
|
||||
<Input.Search
|
||||
size="large"
|
||||
placeholder={t("general.labels.globalsearch")}
|
||||
|
||||
@@ -13,7 +13,6 @@ import Icon, {
|
||||
FileFilled,
|
||||
HomeFilled,
|
||||
ImportOutlined,
|
||||
InfoCircleOutlined,
|
||||
LineChartOutlined,
|
||||
PaperClipOutlined,
|
||||
PhoneOutlined,
|
||||
@@ -27,8 +26,8 @@ import Icon, {
|
||||
UserOutlined
|
||||
} from "@ant-design/icons";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Layout, Menu, Switch, Tooltip } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Layout, Menu } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BsKanban } from "react-icons/bs";
|
||||
import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar, FaTasks } from "react-icons/fa";
|
||||
@@ -43,7 +42,6 @@ import { selectRecentItems, selectSelectedHeader } from "../../redux/application
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { signOutStart } from "../../redux/user/user.actions";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import { checkBeta, handleBeta, setBeta } from "../../utils/handleBeta";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
|
||||
@@ -115,20 +113,22 @@ function Header({
|
||||
names: ["ImEXPay", "DmsAp", "Simple_Inventory"],
|
||||
splitKey: bodyshop && bodyshop.imexshopid
|
||||
});
|
||||
const [betaSwitch, setBetaSwitch] = useState(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
const isBeta = checkBeta();
|
||||
setBetaSwitch(isBeta);
|
||||
}, []);
|
||||
|
||||
const betaSwitchChange = (checked) => {
|
||||
setBeta(checked);
|
||||
setBetaSwitch(checked);
|
||||
handleBeta();
|
||||
const deleteBetaCookie = () => {
|
||||
const cookieExists = document.cookie.split("; ").some((row) => row.startsWith(`betaSwitchImex=`));
|
||||
if (cookieExists) {
|
||||
const domain = window.location.hostname.split(".").slice(-2).join(".");
|
||||
document.cookie = `betaSwitchImex=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${domain}`;
|
||||
console.log(`betaSwitchImex cookie deleted`);
|
||||
} else {
|
||||
console.log(`betaSwitchImex cookie does not exist`);
|
||||
}
|
||||
};
|
||||
|
||||
deleteBetaCookie();
|
||||
|
||||
const accountingChildren = [];
|
||||
|
||||
if (
|
||||
@@ -695,31 +695,6 @@ function Header({
|
||||
}
|
||||
];
|
||||
|
||||
InstanceRenderManager({
|
||||
executeFunction: true,
|
||||
args: [],
|
||||
imex: () => {
|
||||
menuItems.push({
|
||||
key: "beta-switch",
|
||||
id: "header-beta-switch",
|
||||
style: { marginLeft: "auto" },
|
||||
label: (
|
||||
<Tooltip
|
||||
title={`A more modern ${InstanceRenderManager({
|
||||
imex: t("titles.imexonline"),
|
||||
rome: t("titles.romeonline"),
|
||||
promanager: t("titles.promanager")
|
||||
})} is ready for you to try! You can switch back at any time.`}
|
||||
>
|
||||
<InfoCircleOutlined />
|
||||
<span style={{ marginRight: 8 }}>Try the new app</span>
|
||||
<Switch checked={betaSwitch} onChange={betaSwitchChange} />
|
||||
</Tooltip>
|
||||
)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Layout.Header>
|
||||
<Menu
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { notification } from "antd";
|
||||
import Axios from "axios";
|
||||
import Dinero from "dinero.js";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -7,13 +10,10 @@ import { createStructuredSelector } from "reselect";
|
||||
import { INSERT_NEW_JOB_LINE, UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectJobLineEditModal } from "../../redux/modals/modals.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CriticalPartsScan from "../../utils/criticalPartsScan";
|
||||
import UndefinedToNull from "../../utils/undefinedtonull";
|
||||
import JobLinesUpdsertModal from "./job-lines-upsert-modal.component";
|
||||
import Axios from "axios";
|
||||
import Dinero from "dinero.js";
|
||||
import CriticalPartsScan from "../../utils/criticalPartsScan";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobLineEditModal: selectJobLineEditModal,
|
||||
@@ -82,13 +82,15 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo
|
||||
variables: {
|
||||
lineId: jobLineEditModal.context.id,
|
||||
line: {
|
||||
...values,
|
||||
prt_dsmk_m: Dinero({
|
||||
amount: Math.round(values.act_price * 100)
|
||||
...UndefinedToNull({
|
||||
...values,
|
||||
prt_dsmk_m: Dinero({
|
||||
amount: Math.round(values.act_price * 100)
|
||||
})
|
||||
.percentage(Math.abs(values.prt_dsmk_p || 0))
|
||||
.multiply(values.prt_dsmk_p >= 0 ? 1 : -1)
|
||||
.toFormat(0.0)
|
||||
})
|
||||
.percentage(Math.abs(values.prt_dsmk_p || 0))
|
||||
.multiply(values.prt_dsmk_p >= 0 ? 1 : -1)
|
||||
.toFormat(0.0)
|
||||
}
|
||||
},
|
||||
refetchQueries: ["GET_LINE_TICKET_BY_PK"]
|
||||
|
||||
@@ -141,10 +141,14 @@ export function JobTotalsTableTotals({ bodyshop, job }) {
|
||||
key: t("jobs.fields.ded_amt"),
|
||||
total: job.job_totals.totals.custPayable.deductible
|
||||
},
|
||||
// {
|
||||
// key: t("jobs.fields.federal_tax_payable"),
|
||||
// total: job.job_totals.totals.custPayable.federal_tax,
|
||||
// },
|
||||
...(InstanceRenderManager({
|
||||
imex: [{
|
||||
key: t("jobs.fields.federal_tax_payable"),
|
||||
total: job.job_totals.totals.custPayable.federal_tax
|
||||
}],
|
||||
rome: [],
|
||||
promanager: "USE_ROME"
|
||||
})),
|
||||
{
|
||||
key: t("jobs.fields.other_amount_payable"),
|
||||
total: job.job_totals.totals.custPayable.other_customer_amount
|
||||
|
||||
@@ -219,7 +219,7 @@ export function JobsExportAllButton({
|
||||
};
|
||||
|
||||
return (
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled || jobIds?.length > 10}>
|
||||
{t("jobs.actions.exportselected")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -250,8 +250,8 @@ export function JobsList({ bodyshop }) {
|
||||
},
|
||||
{
|
||||
title: t("jobs.labels.estimator"),
|
||||
dataIndex: "jobs.labels.estimator",
|
||||
key: "jobs.labels.estimator",
|
||||
dataIndex: "estimator",
|
||||
key: "estimator",
|
||||
ellipsis: true,
|
||||
responsive: ["xl"],
|
||||
sorter: (a, b) =>
|
||||
|
||||
@@ -242,7 +242,8 @@ export function PartsOrderListTableComponent({
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
render: (text, record) => recordActions(record, true)
|
||||
render: (text, record) => recordActions(record, true),
|
||||
id: "parts-order-list-table-actions"
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -27,6 +27,10 @@ export default function PartsOrderModalPriceChange({ form, field }) {
|
||||
key: "25",
|
||||
label: t("parts_orders.labels.discount", { percent: "25%" })
|
||||
},
|
||||
{
|
||||
key: "40",
|
||||
label: t("parts_orders.labels.discount", { percent: "40%" })
|
||||
},
|
||||
{
|
||||
key: "custom",
|
||||
label: (
|
||||
|
||||
@@ -200,7 +200,7 @@ export function PayableExportAll({
|
||||
);
|
||||
|
||||
return (
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled || billids?.length > 10}>
|
||||
{t("jobs.actions.exportselected")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -180,7 +180,7 @@ export function PaymentsExportAllButton({
|
||||
};
|
||||
|
||||
return (
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled || paymentIds?.length > 10}>
|
||||
{t("jobs.actions.exportselected")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -8,11 +8,12 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
||||
@@ -20,7 +21,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PaymentsGenerateLink);
|
||||
|
||||
export function PaymentsGenerateLink({ bodyshop, callback, job, openChatByPhone, setMessage }) {
|
||||
export function PaymentsGenerateLink({ bodyshop, currentUser, callback, job, openChatByPhone, setMessage }) {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
@@ -30,29 +31,35 @@ export function PaymentsGenerateLink({ bodyshop, callback, job, openChatByPhone,
|
||||
|
||||
const handleFinish = async ({ amount }) => {
|
||||
setLoading(true);
|
||||
|
||||
const p = parsePhoneNumber(job.ownr_ph1, "CA");
|
||||
let p;
|
||||
try {
|
||||
p = parsePhoneNumber(job.ownr_ph1 || "", "CA");
|
||||
} catch (error) {
|
||||
console.log("Unable to parse phone number");
|
||||
}
|
||||
setLoading(true);
|
||||
const response = await axios.post("/intellipay/generate_payment_url", {
|
||||
bodyshop,
|
||||
amount: amount,
|
||||
account: job.ro_number,
|
||||
invoice: job.id
|
||||
comment: btoa(JSON.stringify({ payments: [{ jobid: job.id, amount }], userEmail: currentUser.email }))
|
||||
});
|
||||
setLoading(false);
|
||||
setPaymentLink(response.data.shorUrl);
|
||||
|
||||
openChatByPhone({
|
||||
phone_num: p.formatInternational(),
|
||||
jobid: job.id
|
||||
});
|
||||
setMessage(
|
||||
t("payments.labels.smspaymentreminder", {
|
||||
shopname: bodyshop.shopname,
|
||||
amount: amount,
|
||||
payment_link: response.data.shorUrl
|
||||
})
|
||||
);
|
||||
if (p) {
|
||||
openChatByPhone({
|
||||
phone_num: p.formatInternational(),
|
||||
jobid: job.id
|
||||
});
|
||||
setMessage(
|
||||
t("payments.labels.smspaymentreminder", {
|
||||
shopname: bodyshop.shopname,
|
||||
amount: amount,
|
||||
payment_link: response.data.shorUrl
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
//Add in confirmation & errors.
|
||||
if (callback) callback();
|
||||
|
||||
@@ -6,11 +6,11 @@ import {
|
||||
PauseCircleOutlined
|
||||
} from "@ant-design/icons";
|
||||
import { Card, Col, Row, Space, Tooltip } from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import React, { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import Dinero from "dinero.js";
|
||||
|
||||
import ProductionAlert from "../production-list-columns/production-list-columns.alert.component";
|
||||
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
||||
@@ -18,8 +18,8 @@ import ProductionSubletsManageComponent from "../production-sublets-manage/produ
|
||||
|
||||
import dayjs from "../../utils/day";
|
||||
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
|
||||
const cardColor = (ssbuckets, totalHrs) => {
|
||||
const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > totalHrs));
|
||||
@@ -213,21 +213,13 @@ const EstimatorToolTip = ({ metadata, cardSettings }) => {
|
||||
};
|
||||
|
||||
const SubtotalTooltip = ({ metadata, cardSettings, t }) => {
|
||||
const amount = metadata?.job_totals?.totals?.subtotal?.amount;
|
||||
const dineroAmount = amount ? Dinero({ amount: parseInt(amount * 100) }).toFormat("0,0.00") : null;
|
||||
const dineroAmount = Dinero(metadata?.job_totals?.totals?.subtotal ?? Dinero()).toFormat();
|
||||
|
||||
return (
|
||||
cardSettings?.subtotal && (
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip
|
||||
title={!!amount ? `${t("production.statistics.currency_symbol")}${dineroAmount}` : null}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{!!amount ? (
|
||||
<span>{`${t("production.statistics.currency_symbol")}${dineroAmount}`}</span>
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
<EllipsesToolTip title={`${dineroAmount}`} kiosk={cardSettings.kiosk}>
|
||||
{dineroAmount}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
)
|
||||
|
||||
@@ -185,7 +185,7 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
|
||||
const cardSettings = useMemo(() => {
|
||||
const kanbanSettings = associationSettings?.kanban_settings;
|
||||
return mergeWithDefaults(kanbanSettings);
|
||||
}, [associationSettings]);
|
||||
}, [associationSettings?.kanban_settings]);
|
||||
|
||||
const handleSettingsChange = () => {
|
||||
setFilter(defaultFilters);
|
||||
|
||||
@@ -1,18 +1,40 @@
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import { useQuery, useSubscription } from "@apollo/client";
|
||||
import React, { useContext, useEffect, useMemo, useRef } from "react";
|
||||
import { useApolloClient, useQuery, useSubscription } from "@apollo/client";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { QUERY_JOBS_IN_PRODUCTION, SUBSCRIPTION_JOBS_IN_PRODUCTION } from "../../graphql/jobs.queries";
|
||||
import {
|
||||
QUERY_EXACT_JOB_IN_PRODUCTION,
|
||||
QUERY_JOBS_IN_PRODUCTION,
|
||||
SUBSCRIPTION_JOBS_IN_PRODUCTION,
|
||||
SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW
|
||||
} from "../../graphql/jobs.queries";
|
||||
import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import ProductionBoardKanbanComponent from "./production-board-kanban.component";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
|
||||
function ProductionBoardKanbanContainer({ bodyshop, currentUser }) {
|
||||
function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionType = "direct" }) {
|
||||
const fired = useRef(false);
|
||||
const client = useApolloClient();
|
||||
const { socket } = useContext(SocketContext); // Get the socket from context
|
||||
const reconnectTimeout = useRef(null); // To store the reconnect timeout
|
||||
const disconnectTime = useRef(null); // To track disconnection time
|
||||
const acceptableReconnectTime = 2000; // 2 seconds threshold
|
||||
|
||||
const {
|
||||
treatments: { Websocket_Production }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Websocket_Production"],
|
||||
splitKey: bodyshop && bodyshop.imexshopid
|
||||
});
|
||||
|
||||
const combinedStatuses = useMemo(
|
||||
() => [
|
||||
...bodyshop.md_ro_statuses.production_statuses,
|
||||
@@ -28,26 +50,127 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser }) {
|
||||
onError: (error) => console.error(`Error fetching jobs in production: ${error.message}`)
|
||||
});
|
||||
|
||||
const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION, {
|
||||
onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
|
||||
});
|
||||
const subscriptionEnabled = Websocket_Production?.treatment === "off";
|
||||
|
||||
const { data: updatedJobs } = useSubscription(
|
||||
subscriptionType === "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION,
|
||||
{
|
||||
skip: !subscriptionEnabled,
|
||||
onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
|
||||
}
|
||||
);
|
||||
|
||||
const { loading: associationSettingsLoading, data: associationSettings } = useQuery(QUERY_KANBAN_SETTINGS, {
|
||||
variables: { email: currentUser.email },
|
||||
onError: (error) => console.error(`Error fetching Kanban settings: ${error.message}`)
|
||||
});
|
||||
|
||||
// const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
|
||||
|
||||
useEffect(() => {
|
||||
if (updatedJobs && data) {
|
||||
if (subscriptionEnabled) {
|
||||
if (!updatedJobs) {
|
||||
return;
|
||||
}
|
||||
if (!fired.current) {
|
||||
fired.current = true;
|
||||
return;
|
||||
}
|
||||
refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`));
|
||||
}
|
||||
}, [updatedJobs, data, refetch]);
|
||||
}, [updatedJobs, refetch, subscriptionEnabled]);
|
||||
|
||||
// Socket.IO implementation for users with Split treatment "off"
|
||||
useEffect(() => {
|
||||
if (subscriptionEnabled || !socket || !bodyshop || !bodyshop.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleJobUpdates = async (jobChangedData) => {
|
||||
const jobId = jobChangedData.id;
|
||||
|
||||
// Access the existing cache for QUERY_JOBS_IN_PRODUCTION
|
||||
const existingJobsCache = client.readQuery({
|
||||
query: QUERY_JOBS_IN_PRODUCTION
|
||||
});
|
||||
|
||||
const existingJobs = existingJobsCache?.jobs || [];
|
||||
|
||||
// Check if the job already exists in the cached jobs
|
||||
const existingJob = existingJobs.find((job) => job.id === jobId);
|
||||
|
||||
if (existingJob) {
|
||||
// If the job exists, we update the cache without making any additional queries
|
||||
client.writeQuery({
|
||||
query: QUERY_JOBS_IN_PRODUCTION,
|
||||
data: {
|
||||
jobs: existingJobs.map((job) =>
|
||||
job.id === jobId ? { ...existingJob, ...jobChangedData, __typename: "jobs" } : job
|
||||
)
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// If the job doesn't exist, fetch it from the server and then add it to the cache
|
||||
try {
|
||||
const { data: jobData } = await client.query({
|
||||
query: QUERY_EXACT_JOB_IN_PRODUCTION,
|
||||
variables: { id: jobId },
|
||||
fetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
// Add the job to the existing cached jobs
|
||||
client.writeQuery({
|
||||
query: QUERY_JOBS_IN_PRODUCTION,
|
||||
data: {
|
||||
jobs: [...existingJobs, { ...jobData.job, __typename: "jobs" }]
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error fetching job ${jobId}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisconnect = () => {
|
||||
// Capture the disconnection time
|
||||
disconnectTime.current = Date.now();
|
||||
};
|
||||
|
||||
const handleReconnect = () => {
|
||||
const reconnectTime = Date.now();
|
||||
const disconnectionDuration = reconnectTime - disconnectTime.current;
|
||||
|
||||
// Only refetch if disconnection was longer than the acceptable reconnect time
|
||||
if (disconnectionDuration >= acceptableReconnectTime) {
|
||||
if (!reconnectTimeout.current) {
|
||||
reconnectTimeout.current = setTimeout(() => {
|
||||
const randomDelay = Math.floor(Math.random() * (30000 - 10000 + 1)) + 10000; // Random delay between 10 and 30 seconds
|
||||
setTimeout(() => {
|
||||
if (refetch) refetch().catch((err) => console.error(`Issue `));
|
||||
reconnectTimeout.current = null; // Clear the timeout reference after refetch
|
||||
}, randomDelay);
|
||||
}, acceptableReconnectTime);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for 'job-changed', 'disconnect', and 'connect' events
|
||||
socket.on("production-job-updated", handleJobUpdates);
|
||||
socket.on("disconnect", handleDisconnect);
|
||||
socket.on("connect", handleReconnect);
|
||||
|
||||
// Clean up on unmount or when dependencies change
|
||||
return () => {
|
||||
socket.off("production-job-updated", handleJobUpdates);
|
||||
socket.off("disconnect", handleDisconnect);
|
||||
socket.off("connect", handleReconnect);
|
||||
if (reconnectTimeout.current) {
|
||||
clearTimeout(reconnectTimeout.current);
|
||||
}
|
||||
};
|
||||
}, [subscriptionEnabled, socket, bodyshop, client, refetch]);
|
||||
|
||||
const filteredAssociationSettings = useMemo(() => {
|
||||
return associationSettings?.associations[0] || null;
|
||||
}, [associationSettings]);
|
||||
}, [associationSettings?.associations]);
|
||||
|
||||
return (
|
||||
<ProductionBoardKanbanComponent
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Card, Statistic } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PropTypes from "prop-types";
|
||||
import { defaultKanbanSettings, statisticsItems } from "./settings/defaultKanbanSettings.js";
|
||||
import Dinero from "dinero.js";
|
||||
|
||||
export const StatisticType = {
|
||||
HOURS: "hours",
|
||||
@@ -32,7 +33,21 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
|
||||
};
|
||||
|
||||
const calculateTotalAmount = (items, key) => {
|
||||
return items.reduce((acc, item) => acc + (item[key]?.totals?.subtotal?.amount || 0), 0);
|
||||
return items.reduce((acc, item) => acc.add(Dinero(item[key]?.totals?.subtotal ?? Dinero())), Dinero({ amount: 0 }));
|
||||
};
|
||||
|
||||
const calculateReducerTotalAmount = (lanes, key) => {
|
||||
return lanes.reduce(
|
||||
(acc, lane) => {
|
||||
return acc.add(
|
||||
lane.cards.reduce(
|
||||
(laneAcc, card) => laneAcc.add(Dinero(card.metadata[key]?.totals?.subtotal ?? Dinero())),
|
||||
Dinero({ amount: 0 })
|
||||
)
|
||||
);
|
||||
},
|
||||
Dinero({ amount: 0 })
|
||||
);
|
||||
};
|
||||
|
||||
const calculateReducerTotal = (lanes, key, subKey) => {
|
||||
@@ -43,14 +58,6 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const calculateReducerTotalAmount = (lanes, key) => {
|
||||
return lanes.reduce((acc, lane) => {
|
||||
return (
|
||||
acc + lane.cards.reduce((laneAcc, card) => laneAcc + (card.metadata[key]?.totals?.subtotal?.amount || 0), 0)
|
||||
);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const formatValue = (value, type) => {
|
||||
if (type === StatisticType.JOBS) {
|
||||
return value.toFixed(0);
|
||||
@@ -87,9 +94,15 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
|
||||
const totalAmountInProduction = useMemo(() => {
|
||||
if (!cardSettings.totalAmountInProduction) return null;
|
||||
const total = calculateTotalAmount(data, "job_totals");
|
||||
return parseFloat(total.toFixed(2));
|
||||
return total.toFormat("$0,0.00");
|
||||
}, [data, cardSettings.totalAmountInProduction]);
|
||||
|
||||
const totalAmountOnBoard = useMemo(() => {
|
||||
if (!reducerData || !cardSettings.totalAmountOnBoard) return null;
|
||||
const total = calculateReducerTotalAmount(reducerData.lanes, "job_totals");
|
||||
return total.toFormat("$0,0.00");
|
||||
}, [reducerData, cardSettings.totalAmountOnBoard]);
|
||||
|
||||
const totalHrsOnBoard = useMemo(() => {
|
||||
if (!reducerData || !cardSettings.totalHrsOnBoard) return null;
|
||||
const total =
|
||||
@@ -118,12 +131,6 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
|
||||
[reducerData, cardSettings.jobsOnBoard]
|
||||
);
|
||||
|
||||
const totalAmountOnBoard = useMemo(() => {
|
||||
if (!reducerData || !cardSettings.totalAmountOnBoard) return null;
|
||||
const total = calculateReducerTotalAmount(reducerData.lanes, "job_totals");
|
||||
return parseFloat(total.toFixed(2));
|
||||
}, [reducerData, cardSettings.totalAmountOnBoard]);
|
||||
|
||||
const tasksInProduction = useMemo(() => {
|
||||
if (!data || !cardSettings.tasksInProduction) return null;
|
||||
return data.reduce((acc, item) => acc + (item.tasks_aggregate?.aggregate?.count || 0), 0);
|
||||
@@ -191,7 +198,6 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
|
||||
<Statistic
|
||||
title={t(`production.statistics.${stat.label}`)}
|
||||
value={formatValue(stat.value, stat.type)}
|
||||
prefix={stat.type === StatisticType.AMOUNT ? t("production.statistics.currency_symbol") : undefined}
|
||||
suffix={
|
||||
stat.type === StatisticType.HOURS
|
||||
? t("production.statistics.hours")
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
|
||||
.production-alert {
|
||||
background: transparent;
|
||||
border: none;
|
||||
@@ -70,3 +69,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.clone.is-dragging .ant-card {
|
||||
border: #1890ff 2px solid !important;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,66 @@
|
||||
import InstanceRenderManager from "../../../utils/instanceRenderMgr.js";
|
||||
|
||||
const statisticsItems = [
|
||||
{ id: 0, name: "totalHrs", label: "total_hours_in_production" },
|
||||
{ id: 1, name: "totalAmountInProduction", label: "total_amount_in_production" },
|
||||
{ id: 2, name: "totalLAB", label: "total_lab_in_production" },
|
||||
{ id: 3, name: "totalLAR", label: "total_lar_in_production" },
|
||||
{ id: 4, name: "jobsInProduction", label: "jobs_in_production" },
|
||||
{ id: 5, name: "totalHrsOnBoard", label: "total_hours_on_board" },
|
||||
{ id: 6, name: "totalAmountOnBoard", label: "total_amount_on_board" },
|
||||
{ id: 7, name: "totalLABOnBoard", label: "total_lab_on_board" },
|
||||
{ id: 8, name: "totalLAROnBoard", label: "total_lar_on_board" },
|
||||
{ id: 9, name: "jobsOnBoard", label: "total_jobs_on_board" },
|
||||
{ id: 10, name: "tasksOnBoard", label: "tasks_on_board" },
|
||||
|
||||
{
|
||||
id: 5,
|
||||
name: "totalHrsOnBoard",
|
||||
label: InstanceRenderManager({
|
||||
imex: "total_hours_in_view",
|
||||
rome: "total_hours_on_board",
|
||||
promanager: "total_hours_on_board"
|
||||
})
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "totalAmountOnBoard",
|
||||
label: InstanceRenderManager({
|
||||
imex: "total_amount_in_view",
|
||||
rome: "total_amount_on_board",
|
||||
promanager: "total_amount_on_board"
|
||||
})
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "totalLABOnBoard",
|
||||
label: InstanceRenderManager({
|
||||
imex: "total_lab_in_view",
|
||||
rome: "total_lab_on_board",
|
||||
promanager: "total_lab_on_board"
|
||||
})
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "totalLAROnBoard",
|
||||
label: InstanceRenderManager({
|
||||
imex: "total_lar_in_view",
|
||||
rome: "total_lar_on_board",
|
||||
promanager: "total_lar_on_board"
|
||||
})
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "jobsOnBoard",
|
||||
label: InstanceRenderManager({
|
||||
imex: "total_jobs_in_view",
|
||||
rome: "total_jobs_on_board",
|
||||
promanager: "total_jobs_on_board"
|
||||
})
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "tasksOnBoard",
|
||||
label: InstanceRenderManager({
|
||||
imex: "tasks_in_view",
|
||||
rome: "tasks_on_board",
|
||||
promanager: "tasks_on_board"
|
||||
})
|
||||
},
|
||||
{ id: 11, name: "tasksInProduction", label: "tasks_in_production" }
|
||||
];
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ function getFurthestAway({ pageBorderBox, draggable, candidates }) {
|
||||
const axis = candidate.axis;
|
||||
const target = patch(
|
||||
candidate.axis.line,
|
||||
// use the current center of the dragging item on the main axis
|
||||
pageBorderBox.center[axis.line],
|
||||
// use the center of the list on the main axis
|
||||
candidate.page.borderBox.center[axis.line],
|
||||
// use the center of the list on the cross axis
|
||||
candidate.page.borderBox.center[axis.crossAxisLine]
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import getBodyElement from "../get-body-element";
|
||||
const isEqual = (base) => (value) => base === value;
|
||||
const isScroll = isEqual("scroll");
|
||||
const isAuto = isEqual("auto");
|
||||
const isOverlay = isEqual("overlay");
|
||||
const isVisible = isEqual("visible");
|
||||
const isEither = (overflow, fn) => fn(overflow.overflowX) || fn(overflow.overflowY);
|
||||
const isBoth = (overflow, fn) => fn(overflow.overflowX) && fn(overflow.overflowY);
|
||||
@@ -14,7 +15,7 @@ const isElementScrollable = (el) => {
|
||||
overflowX: style.overflowX,
|
||||
overflowY: style.overflowY
|
||||
};
|
||||
return isEither(overflow, isScroll) || isEither(overflow, isAuto);
|
||||
return isEither(overflow, isScroll) || isEither(overflow, isAuto) || isEither(overflow, isOverlay);
|
||||
};
|
||||
|
||||
// Special case for a body element
|
||||
|
||||
@@ -8,7 +8,7 @@ function getSelector(contextId) {
|
||||
return `[${attributes.dragHandle.contextId}="${contextId}"]`;
|
||||
}
|
||||
|
||||
function findClosestDragHandleFromEvent(contextId, event) {
|
||||
export function findClosestDragHandleFromEvent(contextId, event) {
|
||||
const target = event.target;
|
||||
if (!isElement(target)) {
|
||||
warning("event.target must be a Element");
|
||||
|
||||
@@ -240,11 +240,14 @@ export default function useTouchSensor(api) {
|
||||
y: clientY
|
||||
};
|
||||
|
||||
const handle = api.findClosestDragHandle(event);
|
||||
invariant(handle, "Touch sensor unable to find drag handle");
|
||||
|
||||
// unbind this event handler
|
||||
unbindEventsRef.current();
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
startPendingDrag(actions, point);
|
||||
startPendingDrag(actions, point, handle);
|
||||
}
|
||||
}),
|
||||
// not including stop or startPendingDrag as it is not defined initially
|
||||
@@ -288,7 +291,7 @@ export default function useTouchSensor(api) {
|
||||
}
|
||||
}, [stop]);
|
||||
const bindCapturingEvents = useCallback(
|
||||
function bindCapturingEvents() {
|
||||
function bindCapturingEvents(target) {
|
||||
const options = {
|
||||
capture: true,
|
||||
passive: false
|
||||
@@ -307,7 +310,7 @@ export default function useTouchSensor(api) {
|
||||
// Old behaviour:
|
||||
// https://gist.github.com/parris/dda613e3ae78f14eb2dc9fa0f4bfce3d
|
||||
// https://stackoverflow.com/questions/33298828/touch-move-event-dont-fire-after-touch-start-target-is-removed
|
||||
const unbindTarget = bindEvents(window, getHandleBindings(args), options);
|
||||
const unbindTarget = bindEvents(target, getHandleBindings(args), options);
|
||||
const unbindWindow = bindEvents(window, getWindowBindings(args), options);
|
||||
unbindEventsRef.current = function unbindAll() {
|
||||
unbindTarget();
|
||||
@@ -330,7 +333,7 @@ export default function useTouchSensor(api) {
|
||||
[getPhase, setPhase]
|
||||
);
|
||||
const startPendingDrag = useCallback(
|
||||
function startPendingDrag(actions, point) {
|
||||
function startPendingDrag(actions, point, target) {
|
||||
invariant(getPhase().type === "IDLE", "Expected to move from IDLE to PENDING drag");
|
||||
const longPressTimerId = setTimeout(startDragging, timeForLongPress);
|
||||
setPhase({
|
||||
@@ -339,7 +342,7 @@ export default function useTouchSensor(api) {
|
||||
actions,
|
||||
longPressTimerId
|
||||
});
|
||||
bindCapturingEvents();
|
||||
bindCapturingEvents(target);
|
||||
},
|
||||
[bindCapturingEvents, getPhase, setPhase, startDragging]
|
||||
);
|
||||
|
||||
@@ -23,7 +23,9 @@ import getBorderBoxCenterPosition from "../get-border-box-center-position";
|
||||
import { warning } from "../../dev-warning";
|
||||
import useLayoutEffect from "../use-isomorphic-layout-effect";
|
||||
import { noop } from "../../empty";
|
||||
import findClosestDraggableIdFromEvent from "./find-closest-draggable-id-from-event";
|
||||
import findClosestDraggableIdFromEvent, {
|
||||
findClosestDragHandleFromEvent
|
||||
} from "./find-closest-draggable-id-from-event";
|
||||
import findDraggable from "../get-elements/find-draggable";
|
||||
import bindEvents from "../event-bindings/bind-events";
|
||||
|
||||
@@ -339,6 +341,9 @@ export default function useSensorMarshal({ contextId, store, registry, customSen
|
||||
}),
|
||||
[contextId, lockAPI, registry, store]
|
||||
);
|
||||
|
||||
const findClosestDragHandle = useCallback((event) => findClosestDragHandleFromEvent(contextId, event), [contextId]);
|
||||
|
||||
const findClosestDraggableId = useCallback((event) => findClosestDraggableIdFromEvent(contextId, event), [contextId]);
|
||||
const findOptionsForDraggable = useCallback(
|
||||
(id) => {
|
||||
@@ -370,9 +375,18 @@ export default function useSensorMarshal({ contextId, store, registry, customSen
|
||||
findClosestDraggableId,
|
||||
findOptionsForDraggable,
|
||||
tryReleaseLock,
|
||||
isLockClaimed
|
||||
isLockClaimed,
|
||||
findClosestDragHandle
|
||||
}),
|
||||
[canGetLock, tryGetLock, findClosestDraggableId, findOptionsForDraggable, tryReleaseLock, isLockClaimed]
|
||||
[
|
||||
canGetLock,
|
||||
tryGetLock,
|
||||
findClosestDraggableId,
|
||||
findOptionsForDraggable,
|
||||
tryReleaseLock,
|
||||
isLockClaimed,
|
||||
findClosestDragHandle
|
||||
]
|
||||
);
|
||||
|
||||
// Bad ass
|
||||
|
||||
@@ -83,7 +83,13 @@ const getFinalStyles = (contextId) => {
|
||||
return {
|
||||
selector: getSelector(attributes.draggable.contextId),
|
||||
styles: {
|
||||
dragging: transition,
|
||||
dragging: `
|
||||
${transition}
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
`,
|
||||
dropAnimating: transition,
|
||||
userCancel: transition
|
||||
}
|
||||
|
||||
@@ -67,7 +67,9 @@ export default function useStyleMarshal(contextId, nonce) {
|
||||
const remove = (ref) => {
|
||||
const current = ref.current;
|
||||
invariant(current, "Cannot unmount ref as it is not set");
|
||||
getHead().removeChild(current);
|
||||
if (getHead().contains(current)) {
|
||||
getHead().removeChild(current);
|
||||
}
|
||||
ref.current = null;
|
||||
};
|
||||
remove(alwaysRef);
|
||||
|
||||
@@ -298,6 +298,16 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => statusSort(a.status, b.status, activeStatuses),
|
||||
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
filters:
|
||||
activeStatuses
|
||||
?.map((s) => {
|
||||
return {
|
||||
text: s || "No Status*",
|
||||
value: [s]
|
||||
};
|
||||
})
|
||||
.sort((a, b) => statusSort(a.text, b.text, activeStatuses)) || [],
|
||||
onFilter: (value, record) => value.includes(record.status),
|
||||
render: (text, record) => <ProductionListColumnStatus record={record} />
|
||||
},
|
||||
{
|
||||
|
||||
@@ -21,25 +21,26 @@ export function ProductionListColumnStatus({ record, bodyshop, insertAuditTrail
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSetStatus = async (e) => {
|
||||
logImEXEvent("production_change_status");
|
||||
// e.stopPropagation();
|
||||
setLoading(true);
|
||||
const { key } = e;
|
||||
await updateJob({
|
||||
variables: {
|
||||
jobId: record.id,
|
||||
job: {
|
||||
status: key
|
||||
if (bodyshop.md_ro_statuses.production_statuses.includes(record.status) && !bodyshop.md_ro_statuses.post_production_statuses.includes(record.status)) {
|
||||
logImEXEvent("production_change_status");
|
||||
// e.stopPropagation();
|
||||
setLoading(true);
|
||||
const { key } = e;
|
||||
await updateJob({
|
||||
variables: {
|
||||
jobId: record.id,
|
||||
job: {
|
||||
status: key
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: record.id,
|
||||
operation: AuditTrailMapping.jobstatuschange(key),
|
||||
type: "jobstatuschange"
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: record.id,
|
||||
operation: AuditTrailMapping.jobstatuschange(key),
|
||||
type: "jobstatuschange"
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const menu = {
|
||||
|
||||
@@ -457,41 +457,42 @@ export function ProductionListConfigManager({
|
||||
value={activeView}
|
||||
disabled={open || isAddingNewProfile} // Disable the Select box when the popover is open or adding a new profile
|
||||
>
|
||||
{bodyshop.production_config
|
||||
.slice()
|
||||
.sort((a, b) =>
|
||||
a.name === t("production.constants.main_profile")
|
||||
? -1
|
||||
: b.name === t("production.constants.main_profile")
|
||||
? 1
|
||||
: 0
|
||||
) //
|
||||
.map((config) => (
|
||||
<Select.Option key={config.name} label={config.name}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<span
|
||||
style={{
|
||||
flex: 1,
|
||||
maxWidth: "80%",
|
||||
marginRight: "1rem",
|
||||
textOverflow: "ellipsis"
|
||||
}}
|
||||
>
|
||||
{config.name}
|
||||
</span>
|
||||
{config.name !== t("production.constants.main_profile") && (
|
||||
<Popconfirm
|
||||
placement="right"
|
||||
title={t("general.labels.areyousure")}
|
||||
onConfirm={() => handleTrash(config.name)}
|
||||
onCancel={(e) => e.stopPropagation()}
|
||||
{bodyshop?.production_config &&
|
||||
bodyshop.production_config
|
||||
.slice()
|
||||
.sort((a, b) =>
|
||||
a.name === t("production.constants.main_profile")
|
||||
? -1
|
||||
: b.name === t("production.constants.main_profile")
|
||||
? 1
|
||||
: 0
|
||||
) //
|
||||
.map((config) => (
|
||||
<Select.Option key={config.name} label={config.name}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<span
|
||||
style={{
|
||||
flex: 1,
|
||||
maxWidth: "80%",
|
||||
marginRight: "1rem",
|
||||
textOverflow: "ellipsis"
|
||||
}}
|
||||
>
|
||||
<DeleteOutlined onClick={(e) => e.stopPropagation()} />
|
||||
</Popconfirm>
|
||||
)}
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
{config.name}
|
||||
</span>
|
||||
{config.name !== t("production.constants.main_profile") && (
|
||||
<Popconfirm
|
||||
placement="right"
|
||||
title={t("general.labels.areyousure")}
|
||||
onConfirm={() => handleTrash(config.name)}
|
||||
onCancel={(e) => e.stopPropagation()}
|
||||
>
|
||||
<DeleteOutlined onClick={(e) => e.stopPropagation()} />
|
||||
</Popconfirm>
|
||||
)}
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
<Select.Option key="add_new" label={t("production.labels.addnewprofile")}>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<PlusOutlined style={{ marginRight: "0.5rem" }} />
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Button, Dropdown, Input, Space, Statistic, Table } from "antd";
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Button, Dropdown, Input, Space, Statistic, Table } from "antd";
|
||||
import _ from "lodash";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import ReactDragListView from "react-drag-listview";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import Prompt from "../../utils/prompt.js";
|
||||
import AlertComponent from "../alert/alert.component.jsx";
|
||||
import ProductionListColumnsAdd from "../production-list-columns/production-list-columns.add.component";
|
||||
import ProductionListColumns from "../production-list-columns/production-list-columns.data";
|
||||
import ProductionListDetail from "../production-list-detail/production-list-detail.component";
|
||||
import { ProductionListConfigManager } from "./production-list-config-manager.component.jsx";
|
||||
import ProductionListPrint from "./production-list-print.component";
|
||||
import ResizeableTitle from "./production-list-table.resizeable.component";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import Prompt from "../../utils/prompt.js";
|
||||
import _ from "lodash";
|
||||
import AlertComponent from "../alert/alert.component.jsx";
|
||||
import { ProductionListConfigManager } from "./production-list-config-manager.component.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -43,7 +43,7 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
||||
const initialStateRef = useRef(
|
||||
(bodyshop.production_config &&
|
||||
bodyshop.production_config.find((p) => p.name === defaultView)?.columns.tableState) ||
|
||||
bodyshop.production_config[0]?.columns.tableState || {
|
||||
(bodyshop.production_config && bodyshop.production_config[0]?.columns.tableState) || {
|
||||
sortedInfo: {},
|
||||
filteredInfo: { text: "" }
|
||||
}
|
||||
@@ -51,8 +51,8 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
||||
|
||||
const initialColumnsRef = useRef(
|
||||
(initialStateRef.current &&
|
||||
bodyshop.production_config
|
||||
.find((p) => p.name === defaultView)
|
||||
bodyshop?.production_config
|
||||
?.find((p) => p.name === defaultView)
|
||||
?.columns.columnKeys.map((k) => {
|
||||
return {
|
||||
...ProductionListColumns({
|
||||
@@ -76,8 +76,8 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
||||
const { t } = useTranslation();
|
||||
|
||||
const matchingColumnConfig = useMemo(() => {
|
||||
return bodyshop.production_config.find((p) => p.name === defaultView);
|
||||
}, [bodyshop.production_config, defaultView]);
|
||||
return bodyshop?.production_config?.find((p) => p.name === defaultView);
|
||||
}, [bodyshop.production_config]);
|
||||
|
||||
useEffect(() => {
|
||||
const newColumns =
|
||||
|
||||
@@ -1,24 +1,54 @@
|
||||
import { useApolloClient, useQuery, useSubscription } from "@apollo/client";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useContext, useEffect, useState, useRef } from "react";
|
||||
import {
|
||||
QUERY_EXACT_JOB_IN_PRODUCTION,
|
||||
QUERY_EXACT_JOBS_IN_PRODUCTION,
|
||||
QUERY_JOBS_IN_PRODUCTION,
|
||||
SUBSCRIPTION_JOBS_IN_PRODUCTION
|
||||
SUBSCRIPTION_JOBS_IN_PRODUCTION,
|
||||
SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW
|
||||
} from "../../graphql/jobs.queries";
|
||||
import ProductionListTable from "./production-list-table.component";
|
||||
import _ from "lodash";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||
|
||||
export default function ProductionListTableContainer() {
|
||||
export default function ProductionListTableContainer({ bodyshop, subscriptionType = "direct" }) {
|
||||
const client = useApolloClient();
|
||||
const { socket } = useContext(SocketContext);
|
||||
const [joblist, setJoblist] = useState([]);
|
||||
const reconnectTimeout = useRef(null); // To store the reconnect timeout
|
||||
const disconnectTime = useRef(null); // To store the time of disconnection
|
||||
|
||||
const acceptableReconnectTime = 2000; // 2 seconds threshold
|
||||
|
||||
// Get Split treatment
|
||||
const {
|
||||
treatments: { Websocket_Production }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Websocket_Production"],
|
||||
splitKey: bodyshop && bodyshop.imexshopid
|
||||
});
|
||||
|
||||
// Determine if subscription is enabled
|
||||
const subscriptionEnabled = Websocket_Production?.treatment === "off";
|
||||
|
||||
// Use GraphQL query
|
||||
const { refetch, loading, data } = useQuery(QUERY_JOBS_IN_PRODUCTION, {
|
||||
pollInterval: 3600000,
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
const client = useApolloClient();
|
||||
const [joblist, setJoblist] = useState([]);
|
||||
const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION);
|
||||
|
||||
// Use GraphQL subscription when subscription is enabled
|
||||
const { data: updatedJobs } = useSubscription(
|
||||
subscriptionType === "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION,
|
||||
{
|
||||
skip: !subscriptionEnabled
|
||||
}
|
||||
);
|
||||
|
||||
// Update joblist when data changes
|
||||
useEffect(() => {
|
||||
if (!(data && data.jobs)) return;
|
||||
setJoblist(
|
||||
@@ -28,34 +58,134 @@ export default function ProductionListTableContainer() {
|
||||
);
|
||||
}, [data]);
|
||||
|
||||
// Handle updates from GraphQL subscription
|
||||
useEffect(() => {
|
||||
if (!updatedJobs || joblist.length === 0) return;
|
||||
if (subscriptionEnabled) {
|
||||
if (!updatedJobs || joblist.length === 0) return;
|
||||
|
||||
const jobDiff = _.differenceWith(
|
||||
joblist,
|
||||
updatedJobs.jobs,
|
||||
(a, b) => a.id === b.id && a.updated_at === b.updated_at
|
||||
);
|
||||
const jobDiff = _.differenceWith(
|
||||
joblist,
|
||||
updatedJobs.jobs,
|
||||
(a, b) => a.id === b.id && a.updated_at === b.updated_at
|
||||
);
|
||||
|
||||
if (jobDiff.length > 1) {
|
||||
getUpdatedJobsData(jobDiff.map((j) => j.id));
|
||||
} else if (jobDiff.length === 1) {
|
||||
jobDiff.forEach((job) => {
|
||||
getUpdatedJobData(job.id);
|
||||
});
|
||||
if (jobDiff.length > 1) {
|
||||
getUpdatedJobsData(jobDiff.map((j) => j.id));
|
||||
} else if (jobDiff.length === 1) {
|
||||
jobDiff.forEach((job) => {
|
||||
getUpdatedJobData(job.id);
|
||||
});
|
||||
}
|
||||
|
||||
setJoblist(updatedJobs.jobs);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [updatedJobs, subscriptionEnabled]);
|
||||
|
||||
// Handle updates from Socket.IO when subscription is disabled
|
||||
useEffect(() => {
|
||||
if (subscriptionEnabled || !socket || !bodyshop || !bodyshop.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
setJoblist(updatedJobs.jobs);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [updatedJobs]);
|
||||
const handleJobUpdates = async (jobChangedData) => {
|
||||
const jobId = jobChangedData.id;
|
||||
|
||||
// Access the existing cache for QUERY_JOBS_IN_PRODUCTION
|
||||
const existingJobsCache = client.readQuery({
|
||||
query: QUERY_JOBS_IN_PRODUCTION
|
||||
});
|
||||
|
||||
const existingJobs = existingJobsCache?.jobs || [];
|
||||
|
||||
// Check if the job already exists in the cached jobs
|
||||
const existingJob = existingJobs.find((job) => job.id === jobId);
|
||||
|
||||
if (existingJob) {
|
||||
// If the job exists, we update the cache without making any additional queries
|
||||
client.writeQuery({
|
||||
query: QUERY_JOBS_IN_PRODUCTION,
|
||||
data: {
|
||||
jobs: existingJobs.map((job) =>
|
||||
job.id === jobId ? { ...existingJob, ...jobChangedData, __typename: "jobs" } : job
|
||||
)
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// If the job doesn't exist, fetch it from the server and then add it to the cache
|
||||
try {
|
||||
const { data: jobData } = await client.query({
|
||||
query: QUERY_EXACT_JOB_IN_PRODUCTION,
|
||||
variables: { id: jobId },
|
||||
fetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
// Add the job to the existing cached jobs
|
||||
client.writeQuery({
|
||||
query: QUERY_JOBS_IN_PRODUCTION,
|
||||
data: {
|
||||
jobs: [...existingJobs, { ...jobData.job, __typename: "jobs" }]
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error fetching job ${jobId}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisconnect = () => {
|
||||
// Capture the time when the disconnection happens
|
||||
disconnectTime.current = Date.now();
|
||||
};
|
||||
|
||||
const handleReconnect = () => {
|
||||
// Calculate how long the disconnection lasted
|
||||
const reconnectTime = Date.now();
|
||||
const disconnectionDuration = reconnectTime - disconnectTime.current;
|
||||
|
||||
// If disconnection lasted less than acceptable reconnect time, do nothing
|
||||
if (disconnectionDuration < acceptableReconnectTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule a refetch with a random delay between 10 and 30 seconds
|
||||
if (!reconnectTimeout.current) {
|
||||
reconnectTimeout.current = setTimeout(() => {
|
||||
const randomDelay = Math.floor(Math.random() * (30000 - 10000 + 1)) + 10000; // Random delay between 10 and 30 seconds
|
||||
setTimeout(() => {
|
||||
if (refetch) refetch();
|
||||
reconnectTimeout.current = null; // Clear the timeout reference after refetch
|
||||
}, randomDelay);
|
||||
}, acceptableReconnectTime);
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for 'production-job-updated', 'disconnect', and 'connect' events
|
||||
socket.on("production-job-updated", handleJobUpdates);
|
||||
socket.on("disconnect", handleDisconnect);
|
||||
socket.on("connect", handleReconnect);
|
||||
|
||||
// Clean up on unmount or when dependencies change
|
||||
return () => {
|
||||
socket.off("production-job-updated", handleJobUpdates);
|
||||
socket.off("disconnect", handleDisconnect);
|
||||
socket.off("connect", handleReconnect);
|
||||
if (reconnectTimeout.current) {
|
||||
clearTimeout(reconnectTimeout.current);
|
||||
}
|
||||
};
|
||||
}, [subscriptionEnabled, socket, bodyshop, client, refetch]);
|
||||
|
||||
// Functions to fetch updated job data
|
||||
const getUpdatedJobData = async (jobId) => {
|
||||
client.query({
|
||||
await client.query({
|
||||
query: QUERY_EXACT_JOB_IN_PRODUCTION,
|
||||
variables: { id: jobId }
|
||||
variables: { id: jobId },
|
||||
fetchPolicy: "network-only"
|
||||
});
|
||||
};
|
||||
const getUpdatedJobsData = async (jobIds) => {
|
||||
|
||||
const getUpdatedJobsData = (jobIds) => {
|
||||
client.query({
|
||||
query: QUERY_EXACT_JOBS_IN_PRODUCTION,
|
||||
variables: { ids: jobIds }
|
||||
|
||||
@@ -34,28 +34,34 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
|
||||
const [form] = Form.useForm();
|
||||
const [search, setSearch] = useState("");
|
||||
const {
|
||||
treatments: { Enhanced_Payroll }
|
||||
treatments: { Enhanced_Payroll, ADPPayroll }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Enhanced_Payroll"],
|
||||
names: ["Enhanced_Payroll", "ADPPayroll"],
|
||||
splitKey: bodyshop.imexshopid
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const Templates = TemplateList("report_center");
|
||||
const ReportsList =
|
||||
Enhanced_Payroll.treatment === "on"
|
||||
? Object.keys(Templates)
|
||||
.map((key) => {
|
||||
return Templates[key];
|
||||
})
|
||||
.filter((temp) => temp.enhanced_payroll === undefined || temp.enhanced_payroll === true)
|
||||
: Object.keys(Templates)
|
||||
.map((key) => {
|
||||
return Templates[key];
|
||||
})
|
||||
.filter((temp) => temp.enhanced_payroll === undefined || temp.enhanced_payroll === false);
|
||||
const ReportsList = Object.keys(Templates)
|
||||
.map((key) => Templates[key])
|
||||
.filter((temp) => {
|
||||
const enhancedPayrollOn = Enhanced_Payroll.treatment === "on";
|
||||
const adpPayrollOn = ADPPayroll.treatment === "on";
|
||||
|
||||
if (enhancedPayrollOn && adpPayrollOn) {
|
||||
return temp.enhanced_payroll !== false || temp.adp_payroll !== false;
|
||||
}
|
||||
if (enhancedPayrollOn) {
|
||||
return temp.enhanced_payroll !== false && temp.adp_payroll !== true;
|
||||
}
|
||||
if (adpPayrollOn) {
|
||||
return temp.adp_payroll !== false && temp.enhanced_payroll !== true;
|
||||
}
|
||||
|
||||
return temp.enhanced_payroll !== true && temp.adp_payroll !== true;
|
||||
});
|
||||
|
||||
const { open } = reportCenterModal;
|
||||
|
||||
@@ -104,7 +110,7 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
|
||||
to: values.to,
|
||||
subject: Templates[values.key]?.subject
|
||||
},
|
||||
values.sendbyexcel === "excel" ? "x" : values.sendby === "email" ? "e" : "p",
|
||||
values.sendbytext === "text" ? values.sendbytext : values.sendbyexcel === "excel" ? "x" : values.sendby === "email" ? "e" : "p",
|
||||
id
|
||||
);
|
||||
setLoading(false);
|
||||
@@ -291,7 +297,15 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
);
|
||||
if (reporttype !== "excel")
|
||||
if (reporttype === "text")
|
||||
return (
|
||||
<Form.Item label={t("general.labels.sendby")} name="sendbytext" initialValue="text">
|
||||
<Radio.Group>
|
||||
<Radio value="text">{t("general.labels.text")}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
);
|
||||
if (reporttype !== "excel" || reporttype !== "text")
|
||||
return (
|
||||
<Form.Item label={t("general.labels.sendby")} name="sendby" initialValue="print">
|
||||
<Radio.Group>
|
||||
|
||||
505
client/src/components/schedule-calendar-wrapper/localizer.js
Normal file
505
client/src/components/schedule-calendar-wrapper/localizer.js
Normal file
@@ -0,0 +1,505 @@
|
||||
import isBetween from "dayjs/plugin/isBetween";
|
||||
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
|
||||
import isSameOrBefore from "dayjs/plugin/isSameOrBefore";
|
||||
import localeData from "dayjs/plugin/localeData";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import minMax from "dayjs/plugin/minMax";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import { DateLocalizer } from "react-big-calendar";
|
||||
|
||||
function arrayWithHoles(arr) {
|
||||
if (Array.isArray(arr)) return arr;
|
||||
}
|
||||
|
||||
function iterableToArrayLimit(arr, i) {
|
||||
if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return;
|
||||
var _arr = [];
|
||||
var _n = true;
|
||||
var _d = false;
|
||||
var _e = undefined;
|
||||
try {
|
||||
for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
|
||||
_arr.push(_s.value);
|
||||
if (i && _arr.length === i) break;
|
||||
}
|
||||
} catch (err) {
|
||||
_d = true;
|
||||
_e = err;
|
||||
} finally {
|
||||
try {
|
||||
if (!_n && _i["return"] != null) _i["return"]();
|
||||
} finally {
|
||||
if (_d) throw _e;
|
||||
}
|
||||
}
|
||||
return _arr;
|
||||
}
|
||||
|
||||
function unsupportedIterableToArray(o, minLen) {
|
||||
if (!o) return;
|
||||
if (typeof o === "string") return arrayLikeToArray(o, minLen);
|
||||
var n = Object.prototype.toString.call(o).slice(8, -1);
|
||||
if (n === "Object" && o.constructor) n = o.constructor.name;
|
||||
if (n === "Map" || n === "Set") return Array.from(o);
|
||||
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return arrayLikeToArray(o, minLen);
|
||||
}
|
||||
|
||||
function arrayLikeToArray(arr, len) {
|
||||
if (len == null || len > arr.length) len = arr.length;
|
||||
for (var i = 0, arr2 = new Array(len); i < len; i++) {
|
||||
arr2[i] = arr[i];
|
||||
}
|
||||
return arr2;
|
||||
}
|
||||
|
||||
function nonIterableRest() {
|
||||
throw new TypeError(
|
||||
"Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."
|
||||
);
|
||||
}
|
||||
|
||||
function _slicedToArray(arr, i) {
|
||||
return arrayWithHoles(arr) || iterableToArrayLimit(arr, i) || unsupportedIterableToArray(arr, i) || nonIterableRest();
|
||||
}
|
||||
|
||||
function fixUnit(unit) {
|
||||
var datePart = unit ? unit.toLowerCase() : unit;
|
||||
if (datePart === "FullYear") {
|
||||
datePart = "year";
|
||||
} else if (!datePart) {
|
||||
datePart = undefined;
|
||||
}
|
||||
return datePart;
|
||||
}
|
||||
|
||||
var timeRangeFormat = function timeRangeFormat(_ref3, culture, local) {
|
||||
var start = _ref3.start,
|
||||
end = _ref3.end;
|
||||
return local.format(start, "LT", culture) + " – " + local.format(end, "LT", culture);
|
||||
};
|
||||
var timeRangeStartFormat = function timeRangeStartFormat(_ref4, culture, local) {
|
||||
var start = _ref4.start;
|
||||
return local.format(start, "LT", culture) + " – ";
|
||||
};
|
||||
var timeRangeEndFormat = function timeRangeEndFormat(_ref5, culture, local) {
|
||||
var end = _ref5.end;
|
||||
return " – " + local.format(end, "LT", culture);
|
||||
};
|
||||
var weekRangeFormat = function weekRangeFormat(_ref, culture, local) {
|
||||
var start = _ref.start,
|
||||
end = _ref.end;
|
||||
return (
|
||||
local.format(start, "MMMM DD", culture) +
|
||||
" – " +
|
||||
// updated to use this localizer 'eq()' method
|
||||
local.format(end, local.eq(start, end, "month") ? "DD" : "MMMM DD", culture)
|
||||
);
|
||||
};
|
||||
var dateRangeFormat = function dateRangeFormat(_ref2, culture, local) {
|
||||
var start = _ref2.start,
|
||||
end = _ref2.end;
|
||||
return local.format(start, "L", culture) + " – " + local.format(end, "L", culture);
|
||||
};
|
||||
|
||||
var formats = {
|
||||
dateFormat: "DD",
|
||||
dayFormat: "DD ddd",
|
||||
weekdayFormat: "ddd",
|
||||
selectRangeFormat: timeRangeFormat,
|
||||
eventTimeRangeFormat: timeRangeFormat,
|
||||
eventTimeRangeStartFormat: timeRangeStartFormat,
|
||||
eventTimeRangeEndFormat: timeRangeEndFormat,
|
||||
timeGutterFormat: "LT",
|
||||
monthHeaderFormat: "MMMM YYYY",
|
||||
dayHeaderFormat: "dddd MMM DD",
|
||||
dayRangeHeaderFormat: weekRangeFormat,
|
||||
agendaHeaderFormat: dateRangeFormat,
|
||||
agendaDateFormat: "ddd MMM DD",
|
||||
agendaTimeFormat: "LT",
|
||||
agendaTimeRangeFormat: timeRangeFormat
|
||||
};
|
||||
|
||||
const localizer = (dayjsLib) => {
|
||||
// load dayjs plugins
|
||||
dayjsLib.extend(isBetween);
|
||||
dayjsLib.extend(isSameOrAfter);
|
||||
dayjsLib.extend(isSameOrBefore);
|
||||
dayjsLib.extend(localeData);
|
||||
dayjsLib.extend(localizedFormat);
|
||||
dayjsLib.extend(minMax);
|
||||
dayjsLib.extend(utc);
|
||||
var locale = function locale(dj, c) {
|
||||
return c ? dj.locale(c) : dj;
|
||||
};
|
||||
|
||||
// if the timezone plugin is loaded,
|
||||
// then use the timezone aware version
|
||||
|
||||
//TODO This was the issue entirely...
|
||||
// var dayjs = dayjsLib.tz ? dayjsLib.tz : dayjsLib;
|
||||
var dayjs = dayjsLib;
|
||||
|
||||
function getTimezoneOffset(date) {
|
||||
// ensures this gets cast to timezone
|
||||
return dayjs(date).toDate().getTimezoneOffset();
|
||||
}
|
||||
|
||||
function getDstOffset(start, end) {
|
||||
var _st$tz$$x$$timezone;
|
||||
// convert to dayjs, in case
|
||||
var st = dayjs(start);
|
||||
var ed = dayjs(end);
|
||||
// if not using the dayjs timezone plugin
|
||||
if (!dayjs.tz) {
|
||||
return st.toDate().getTimezoneOffset() - ed.toDate().getTimezoneOffset();
|
||||
}
|
||||
/**
|
||||
* If a default timezone has been applied, then
|
||||
* use this to get the proper timezone offset, otherwise default
|
||||
* the timezone to the browser local
|
||||
*/
|
||||
var tzName =
|
||||
(_st$tz$$x$$timezone = st.tz().$x.$timezone) !== null && _st$tz$$x$$timezone !== void 0
|
||||
? _st$tz$$x$$timezone
|
||||
: dayjsLib.tz.guess();
|
||||
// invert offsets to be inline with moment.js
|
||||
var startOffset = -dayjs.tz(+st, tzName).utcOffset();
|
||||
var endOffset = -dayjs.tz(+ed, tzName).utcOffset();
|
||||
return startOffset - endOffset;
|
||||
}
|
||||
|
||||
function getDayStartDstOffset(start) {
|
||||
var dayStart = dayjs(start).startOf("day");
|
||||
return getDstOffset(dayStart, start);
|
||||
}
|
||||
|
||||
/*** BEGIN localized date arithmetic methods with dayjs ***/
|
||||
function defineComparators(a, b, unit) {
|
||||
var datePart = fixUnit(unit);
|
||||
var dtA = datePart ? dayjs(a).startOf(datePart) : dayjs(a);
|
||||
var dtB = datePart ? dayjs(b).startOf(datePart) : dayjs(b);
|
||||
return [dtA, dtB, datePart];
|
||||
}
|
||||
|
||||
function startOf() {
|
||||
var date = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
|
||||
var unit = arguments.length > 1 ? arguments[1] : undefined;
|
||||
var datePart = fixUnit(unit);
|
||||
if (datePart) {
|
||||
return dayjs(date).startOf(datePart).toDate();
|
||||
}
|
||||
return dayjs(date).toDate();
|
||||
}
|
||||
|
||||
function endOf() {
|
||||
var date = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
|
||||
var unit = arguments.length > 1 ? arguments[1] : undefined;
|
||||
var datePart = fixUnit(unit);
|
||||
if (datePart) {
|
||||
return dayjs(date).endOf(datePart).toDate();
|
||||
}
|
||||
return dayjs(date).toDate();
|
||||
}
|
||||
|
||||
// dayjs comparison operations *always* convert both sides to dayjs objects
|
||||
// prior to running the comparisons
|
||||
function eq(a, b, unit) {
|
||||
var _defineComparators = defineComparators(a, b, unit),
|
||||
_defineComparators2 = _slicedToArray(_defineComparators, 3),
|
||||
dtA = _defineComparators2[0],
|
||||
dtB = _defineComparators2[1],
|
||||
datePart = _defineComparators2[2];
|
||||
return dtA.isSame(dtB, datePart);
|
||||
}
|
||||
|
||||
function neq(a, b, unit) {
|
||||
return !eq(a, b, unit);
|
||||
}
|
||||
|
||||
function gt(a, b, unit) {
|
||||
var _defineComparators3 = defineComparators(a, b, unit),
|
||||
_defineComparators4 = _slicedToArray(_defineComparators3, 3),
|
||||
dtA = _defineComparators4[0],
|
||||
dtB = _defineComparators4[1],
|
||||
datePart = _defineComparators4[2];
|
||||
return dtA.isAfter(dtB, datePart);
|
||||
}
|
||||
|
||||
function lt(a, b, unit) {
|
||||
var _defineComparators5 = defineComparators(a, b, unit),
|
||||
_defineComparators6 = _slicedToArray(_defineComparators5, 3),
|
||||
dtA = _defineComparators6[0],
|
||||
dtB = _defineComparators6[1],
|
||||
datePart = _defineComparators6[2];
|
||||
return dtA.isBefore(dtB, datePart);
|
||||
}
|
||||
|
||||
function gte(a, b, unit) {
|
||||
var _defineComparators7 = defineComparators(a, b, unit),
|
||||
_defineComparators8 = _slicedToArray(_defineComparators7, 3),
|
||||
dtA = _defineComparators8[0],
|
||||
dtB = _defineComparators8[1],
|
||||
datePart = _defineComparators8[2];
|
||||
return dtA.isSameOrBefore(dtB, datePart);
|
||||
}
|
||||
|
||||
function lte(a, b, unit) {
|
||||
var _defineComparators9 = defineComparators(a, b, unit),
|
||||
_defineComparators10 = _slicedToArray(_defineComparators9, 3),
|
||||
dtA = _defineComparators10[0],
|
||||
dtB = _defineComparators10[1],
|
||||
datePart = _defineComparators10[2];
|
||||
return dtA.isSameOrBefore(dtB, datePart);
|
||||
}
|
||||
|
||||
function inRange(day, min, max) {
|
||||
var unit = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : "day";
|
||||
var datePart = fixUnit(unit);
|
||||
var djDay = dayjs(day);
|
||||
var djMin = dayjs(min);
|
||||
var djMax = dayjs(max);
|
||||
return djDay.isBetween(djMin, djMax, datePart, "[]");
|
||||
}
|
||||
|
||||
function min(dateA, dateB) {
|
||||
var dtA = dayjs(dateA);
|
||||
var dtB = dayjs(dateB);
|
||||
var minDt = dayjsLib.min(dtA, dtB);
|
||||
return minDt.toDate();
|
||||
}
|
||||
|
||||
function max(dateA, dateB) {
|
||||
var dtA = dayjs(dateA);
|
||||
var dtB = dayjs(dateB);
|
||||
var maxDt = dayjsLib.max(dtA, dtB);
|
||||
return maxDt.toDate();
|
||||
}
|
||||
|
||||
function merge(date, time) {
|
||||
if (!date && !time) return null;
|
||||
var tm = dayjs(time).format("HH:mm:ss");
|
||||
var dt = dayjs(date).startOf("day").format("MM/DD/YYYY");
|
||||
// We do it this way to avoid issues when timezone switching
|
||||
return dayjsLib("".concat(dt, " ").concat(tm), "MM/DD/YYYY HH:mm:ss").toDate();
|
||||
}
|
||||
|
||||
function add(date, adder, unit) {
|
||||
var datePart = fixUnit(unit);
|
||||
return dayjs(date).add(adder, datePart).toDate();
|
||||
}
|
||||
|
||||
function range(start, end) {
|
||||
var unit = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "day";
|
||||
var datePart = fixUnit(unit);
|
||||
// because the add method will put these in tz, we have to start that way
|
||||
var current = dayjs(start).toDate();
|
||||
var days = [];
|
||||
while (lte(current, end)) {
|
||||
days.push(current);
|
||||
current = add(current, 1, datePart);
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
function ceil(date, unit) {
|
||||
var datePart = fixUnit(unit);
|
||||
var floor = startOf(date, datePart);
|
||||
return eq(floor, date) ? floor : add(floor, 1, datePart);
|
||||
}
|
||||
|
||||
function diff(a, b) {
|
||||
var unit = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "day";
|
||||
var datePart = fixUnit(unit);
|
||||
// don't use 'defineComparators' here, as we don't want to mutate the values
|
||||
var dtA = dayjs(a);
|
||||
var dtB = dayjs(b);
|
||||
return dtB.diff(dtA, datePart);
|
||||
}
|
||||
|
||||
function minutes(date) {
|
||||
var dt = dayjs(date);
|
||||
return dt.minutes();
|
||||
}
|
||||
|
||||
function firstOfWeek(culture) {
|
||||
var data = culture ? dayjsLib.localeData(culture) : dayjsLib.localeData();
|
||||
return data ? data.firstDayOfWeek() : 0;
|
||||
}
|
||||
|
||||
function firstVisibleDay(date) {
|
||||
return dayjs(date).startOf("month").startOf("week").toDate();
|
||||
}
|
||||
|
||||
function lastVisibleDay(date) {
|
||||
return dayjs(date).endOf("month").endOf("week").toDate();
|
||||
}
|
||||
|
||||
function visibleDays(date) {
|
||||
var current = firstVisibleDay(date);
|
||||
var last = lastVisibleDay(date);
|
||||
var days = [];
|
||||
while (lte(current, last)) {
|
||||
days.push(current);
|
||||
current = add(current, 1, "d");
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
/*** END localized date arithmetic methods with dayjs ***/
|
||||
|
||||
/**
|
||||
* Moved from TimeSlots.js, this method overrides the method of the same name
|
||||
* in the localizer.js, using dayjs to construct the js Date
|
||||
* @param {Date} dt - date to start with
|
||||
* @param {Number} minutesFromMidnight
|
||||
* @param {Number} offset
|
||||
* @returns {Date}
|
||||
*/
|
||||
function getSlotDate(dt, minutesFromMidnight, offset) {
|
||||
return dayjs(dt)
|
||||
.startOf("day")
|
||||
.minute(minutesFromMidnight + offset)
|
||||
.toDate();
|
||||
}
|
||||
|
||||
// dayjs will automatically handle DST differences in it's calculations
|
||||
function getTotalMin(start, end) {
|
||||
return diff(start, end, "minutes");
|
||||
}
|
||||
|
||||
function getMinutesFromMidnight(start) {
|
||||
var dayStart = dayjs(start).startOf("day");
|
||||
var day = dayjs(start);
|
||||
return day.diff(dayStart, "minutes") + getDayStartDstOffset(start);
|
||||
}
|
||||
|
||||
// These two are used by DateSlotMetrics
|
||||
function continuesPrior(start, first) {
|
||||
var djStart = dayjs(start);
|
||||
var djFirst = dayjs(first);
|
||||
return djStart.isBefore(djFirst, "day");
|
||||
}
|
||||
|
||||
function continuesAfter(start, end, last) {
|
||||
var djEnd = dayjs(end);
|
||||
var djLast = dayjs(last);
|
||||
return djEnd.isSameOrAfter(djLast, "minutes");
|
||||
}
|
||||
|
||||
function daySpan(start, end) {
|
||||
var startDay = dayjs(start);
|
||||
var endDay = dayjs(end);
|
||||
return endDay.diff(startDay, "day");
|
||||
}
|
||||
|
||||
// These two are used by eventLevels
|
||||
function sortEvents(_ref6) {
|
||||
var _ref6$evtA = _ref6.evtA,
|
||||
aStart = _ref6$evtA.start,
|
||||
aEnd = _ref6$evtA.end,
|
||||
aAllDay = _ref6$evtA.allDay,
|
||||
_ref6$evtB = _ref6.evtB,
|
||||
bStart = _ref6$evtB.start,
|
||||
bEnd = _ref6$evtB.end,
|
||||
bAllDay = _ref6$evtB.allDay;
|
||||
var startSort = +startOf(aStart, "day") - +startOf(bStart, "day");
|
||||
var durA = daySpan(aStart, aEnd);
|
||||
var durB = daySpan(bStart, bEnd);
|
||||
return (
|
||||
startSort ||
|
||||
// sort by start Day first
|
||||
durB - durA ||
|
||||
// events spanning multiple days go first
|
||||
!!bAllDay - !!aAllDay ||
|
||||
// then allDay single day events
|
||||
+aStart - +bStart ||
|
||||
// then sort by start time *don't need dayjs conversion here
|
||||
+aEnd - +bEnd // then sort by end time *don't need dayjs conversion here either
|
||||
);
|
||||
}
|
||||
|
||||
function inEventRange(_ref7) {
|
||||
var _ref7$event = _ref7.event,
|
||||
start = _ref7$event.start,
|
||||
end = _ref7$event.end,
|
||||
_ref7$range = _ref7.range,
|
||||
rangeStart = _ref7$range.start,
|
||||
rangeEnd = _ref7$range.end;
|
||||
var startOfDay = dayjs(start).startOf("day");
|
||||
var eEnd = dayjs(end);
|
||||
var rStart = dayjs(rangeStart);
|
||||
var rEnd = dayjs(rangeEnd);
|
||||
var startsBeforeEnd = startOfDay.isSameOrBefore(rEnd, "day");
|
||||
// when the event is zero duration we need to handle a bit differently
|
||||
var sameMin = !startOfDay.isSame(eEnd, "minutes");
|
||||
var endsAfterStart = sameMin ? eEnd.isAfter(rStart, "minutes") : eEnd.isSameOrAfter(rStart, "minutes");
|
||||
return startsBeforeEnd && endsAfterStart;
|
||||
}
|
||||
|
||||
function isSameDate(date1, date2) {
|
||||
var dt = dayjs(date1);
|
||||
var dt2 = dayjs(date2);
|
||||
return dt.isSame(dt2, "day");
|
||||
}
|
||||
|
||||
/**
|
||||
* This method, called once in the localizer constructor, is used by eventLevels
|
||||
* 'eventSegments()' to assist in determining the 'span' of the event in the display,
|
||||
* specifically when using a timezone that is greater than the browser native timezone.
|
||||
* @returns number
|
||||
*/
|
||||
function browserTZOffset() {
|
||||
/**
|
||||
* Date.prototype.getTimezoneOffset horrifically flips the positive/negative from
|
||||
* what you see in it's string, so we have to jump through some hoops to get a value
|
||||
* we can actually compare.
|
||||
*/
|
||||
var dt = new Date();
|
||||
var neg = /-/.test(dt.toString()) ? "-" : "";
|
||||
var dtOffset = dt.getTimezoneOffset();
|
||||
var comparator = Number("".concat(neg).concat(Math.abs(dtOffset)));
|
||||
// dayjs correctly provides positive/negative offset, as expected
|
||||
var mtOffset = dayjs().utcOffset();
|
||||
return mtOffset > comparator ? 1 : 0;
|
||||
}
|
||||
|
||||
return new DateLocalizer({
|
||||
formats: formats,
|
||||
firstOfWeek: firstOfWeek,
|
||||
firstVisibleDay: firstVisibleDay,
|
||||
lastVisibleDay: lastVisibleDay,
|
||||
visibleDays: visibleDays,
|
||||
format: function format(value, _format, culture) {
|
||||
return locale(dayjs(value), culture).format(_format);
|
||||
},
|
||||
lt: lt,
|
||||
lte: lte,
|
||||
gt: gt,
|
||||
gte: gte,
|
||||
eq: eq,
|
||||
neq: neq,
|
||||
merge: merge,
|
||||
inRange: inRange,
|
||||
startOf: startOf,
|
||||
endOf: endOf,
|
||||
range: range,
|
||||
add: add,
|
||||
diff: diff,
|
||||
ceil: ceil,
|
||||
min: min,
|
||||
max: max,
|
||||
minutes: minutes,
|
||||
getSlotDate: getSlotDate,
|
||||
getTimezoneOffset: getTimezoneOffset,
|
||||
getDstOffset: getDstOffset,
|
||||
getTotalMin: getTotalMin,
|
||||
getMinutesFromMidnight: getMinutesFromMidnight,
|
||||
continuesPrior: continuesPrior,
|
||||
continuesAfter: continuesAfter,
|
||||
sortEvents: sortEvents,
|
||||
inEventRange: inEventRange,
|
||||
isSameDate: isSameDate,
|
||||
browserTZOffset: browserTZOffset
|
||||
});
|
||||
};
|
||||
export default localizer;
|
||||
@@ -1,7 +1,7 @@
|
||||
import dayjs from "../../utils/day";
|
||||
import queryString from "query-string";
|
||||
import React from "react";
|
||||
import { Calendar, dayjsLocalizer } from "react-big-calendar";
|
||||
import { Calendar } from "react-big-calendar";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -14,12 +14,13 @@ import { selectProblemJobs } from "../../redux/application/application.selectors
|
||||
import { Alert, Collapse, Space } from "antd";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import local from "./localizer";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
problemJobs: selectProblemJobs
|
||||
});
|
||||
const localizer = dayjsLocalizer(dayjs);
|
||||
const localizer = local(dayjs);
|
||||
|
||||
export function ScheduleCalendarWrapperComponent({
|
||||
bodyshop,
|
||||
|
||||
@@ -4,7 +4,7 @@ export const CalculateWorkingDaysThisMonth = () => dayjs().endOf("month").busine
|
||||
|
||||
export const CalculateWorkingDaysInPeriod = (start, end) => dayjs(end).businessDiff(dayjs(start));
|
||||
|
||||
export const CalculateWorkingDaysAsOfToday = () => dayjs().businessDaysInMonth().length;
|
||||
export const CalculateWorkingDaysAsOfToday = () => dayjs().endOf("day").businessDiff(dayjs().startOf("month"));
|
||||
|
||||
export const CalculateWorkingDaysLastMonth = () =>
|
||||
dayjs().subtract(1, "month").endOf("month").businessDaysInMonth().length;
|
||||
|
||||
@@ -20,6 +20,7 @@ import ShopInfoTaskPresets from "./shop-info.task-presets.component";
|
||||
import queryString from "query-string";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import ShopInfoRoGuard from "./shop-info.roguard.component";
|
||||
import ShopInfoIntellipay from "./shop-intellipay-config.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -135,6 +136,17 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
|
||||
],
|
||||
rome: "USE_IMEX",
|
||||
promanager: []
|
||||
}),
|
||||
...InstanceRenderManager({
|
||||
imex: [],
|
||||
rome: [
|
||||
{
|
||||
key: "intellipay",
|
||||
label: t("bodyshop.labels.intellipay"),
|
||||
children: <ShopInfoIntellipay form={form} />
|
||||
}
|
||||
],
|
||||
promanager: []
|
||||
})
|
||||
];
|
||||
return (
|
||||
|
||||
@@ -7,13 +7,13 @@ import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import DatePickerRanges from "../../utils/DatePickerRanges";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import FeatureWrapper, { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import FormItemEmail from "../form-items-formatted/email-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 LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import FeatureWrapper, { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
// TODO: Client Update, this might break
|
||||
const timeZonesList = Intl.supportedValuesOf("timeZone");
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -28,10 +28,10 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
treatments: { ClosingPeriod }
|
||||
treatments: { ClosingPeriod, ADPPayroll }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["ClosingPeriod"],
|
||||
names: ["ClosingPeriod", "ADPPayroll"],
|
||||
splitKey: bodyshop && bodyshop.imexshopid
|
||||
});
|
||||
|
||||
@@ -98,7 +98,6 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
||||
<Form.Item label={t("bodyshop.fields.email")} name="email">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.phone")}
|
||||
name="phone"
|
||||
@@ -356,14 +355,22 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
||||
<Select mode="tags" />
|
||||
</Form.Item>
|
||||
{ClosingPeriod.treatment === "on" && (
|
||||
<>
|
||||
<Form.Item
|
||||
name={["accountingconfig", "ClosingPeriod"]}
|
||||
label={t("bodyshop.fields.closingperiod")} //{t("reportcenter.labels.dates")}
|
||||
>
|
||||
<DatePicker.RangePicker format="MM/DD/YYYY" presets={DatePickerRanges} />
|
||||
</Form.Item>
|
||||
</>
|
||||
<Form.Item
|
||||
name={["accountingconfig", "ClosingPeriod"]}
|
||||
label={t("bodyshop.fields.closingperiod")} //{t("reportcenter.labels.dates")}
|
||||
>
|
||||
<DatePicker.RangePicker format="MM/DD/YYYY" presets={DatePickerRanges} />
|
||||
</Form.Item>
|
||||
)}
|
||||
{ADPPayroll.treatment === "on" && (
|
||||
<Form.Item name={["accountingconfig", "companyCode"]} label={t("bodyshop.fields.companycode")}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)}
|
||||
{ADPPayroll.treatment === "on" && (
|
||||
<Form.Item name={["accountingconfig", "batchID"]} label={t("bodyshop.fields.batchid")}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
</FeatureWrapper>
|
||||
|
||||
@@ -30,219 +30,226 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
|
||||
return (
|
||||
<RbacWrapper action="shop:rbac">
|
||||
<LayoutFormRow>
|
||||
{...HasFeatureAccess({ featureName: "export", bodyshop }) ? [
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.accounting.exportlog")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "accounting:exportlog"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.accounting.payables")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "accounting:payables"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.accounting.payments")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "accounting:payments"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.accounting.receivables")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "accounting:receivables"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
]:[]}
|
||||
{...HasFeatureAccess({ featureName: "bills", bodyshop }) ? [
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.bills.delete")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "bills:delete"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.bills.enter")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "bills:enter"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.bills.list")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "bills:list"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.bills.reexport")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "bills:reexport"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.bills.view")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "bills:view"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
]:[]}
|
||||
|
||||
{...HasFeatureAccess({ featureName: "courtesycars", bodyshop }) ? [
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.contracts.create")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "contracts:create"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.contracts.detail")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "contracts:detail"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.contracts.list")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "contracts:list"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.courtesycar.create")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "courtesycar:create"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.courtesycar.detail")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "courtesycar:detail"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.courtesycar.list")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "courtesycar:list"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
]:[]}
|
||||
{...HasFeatureAccess({ featureName: "csi", bodyshop }) ? [
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.csi.export")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "csi:export"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.csi.page")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "csi:page"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
]:[]}
|
||||
{...HasFeatureAccess({ featureName: "export", bodyshop })
|
||||
? [
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.accounting.exportlog")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "accounting:exportlog"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.accounting.payables")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "accounting:payables"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.accounting.payments")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "accounting:payments"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.accounting.receivables")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "accounting:receivables"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
]
|
||||
: []}
|
||||
{...HasFeatureAccess({ featureName: "bills", bodyshop })
|
||||
? [
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.bills.delete")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "bills:delete"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.bills.enter")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "bills:enter"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.bills.list")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "bills:list"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.bills.reexport")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "bills:reexport"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.bills.view")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "bills:view"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
]
|
||||
: []}
|
||||
{...HasFeatureAccess({ featureName: "courtesycars", bodyshop })
|
||||
? [
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.contracts.create")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "contracts:create"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.contracts.detail")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "contracts:detail"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.contracts.list")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "contracts:list"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.courtesycar.create")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "courtesycar:create"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.courtesycar.detail")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "courtesycar:detail"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.courtesycar.list")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "courtesycar:list"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
]
|
||||
: []}
|
||||
{...HasFeatureAccess({ featureName: "csi", bodyshop })
|
||||
? [
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.csi.export")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "csi:export"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.csi.page")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "csi:page"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
]
|
||||
: []}
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.employees.page")}
|
||||
rules={[
|
||||
@@ -255,6 +262,18 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.employee_teams.page")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "employee_teams:page"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.jobs.admin")}
|
||||
rules={[
|
||||
@@ -435,31 +454,6 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.employees.page")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "employees:page"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.employee_teams.page")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "employee_teams:page"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.payments.enter")}
|
||||
rules={[
|
||||
@@ -522,7 +516,6 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.production.list")}
|
||||
rules={[
|
||||
@@ -561,128 +554,118 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
)}
|
||||
{...HasFeatureAccess({ featureName: "timetickets", bodyshop }) ? [
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.shiftclock.view")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "shiftclock:view"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.shop.config")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "shop:config"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.timetickets.edit")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "timetickets:edit"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.timetickets.shiftedit")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "timetickets:shiftedit"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.timetickets.editcommitted")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "timetickets:editcommitted"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.ttapprovals.view")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "ttapprovals:view"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.ttapprovals.approve")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "ttapprovals:approve"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.timetickets.enter")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "timetickets:enter"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.timetickets.list")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "timetickets:list"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.timetickets.shiftedit")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "timetickets:shiftedit"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
]:[]}
|
||||
{...HasFeatureAccess({ featureName: "timetickets", bodyshop })
|
||||
? [
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.shiftclock.view")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "shiftclock:view"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.shop.config")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "shop:config"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.timetickets.edit")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "timetickets:edit"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.timetickets.shiftedit")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "timetickets:shiftedit"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.timetickets.editcommitted")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "timetickets:editcommitted"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.ttapprovals.view")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "ttapprovals:view"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.ttapprovals.approve")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "ttapprovals:approve"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.timetickets.enter")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "timetickets:enter"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.timetickets.list")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "timetickets:list"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
]
|
||||
: []}
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.shop.vendors")}
|
||||
rules={[
|
||||
@@ -757,7 +740,6 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.users.editaccess")}
|
||||
rules={[
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Alert, Form, InputNumber, Switch } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoIntellipay);
|
||||
|
||||
export function ShopInfoIntellipay({ bodyshop, form }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item dependencies={[["intellipay_config", "enable_cash_discount"]]}>
|
||||
{() => {
|
||||
const { intellipay_config } = form.getFieldsValue();
|
||||
|
||||
if (intellipay_config?.enable_cash_discount)
|
||||
return <Alert message={t("bodyshop.labels.intellipay_cash_discount")} />;
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
<LayoutFormRow noDivider>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.intellipay_config.enable_cash_discount")}
|
||||
valuePropName="checked"
|
||||
name={["intellipay_config", "enable_cash_discount"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.intellipay_config.cash_discount_percentage")}
|
||||
valuePropName="checked"
|
||||
dependencies={[["intellipay_config", "enable_cash_discount"]]}
|
||||
name={["intellipay_config", "cash_discount_percentage"]}
|
||||
rules={[
|
||||
({ getFieldsValue }) => ({ required: form.getFieldValue(["intellipay_config", "enable_cash_discount"]) })
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={1} suffix='%'/>
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { onlyUnique } from "../../utils/arrayHelper";
|
||||
import dayjs from "../../utils/day";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import RbacWrapper, { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import TimeTicketEnterButton from "../time-ticket-enter-button/time-ticket-enter-button.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -52,6 +52,10 @@ export function TimeTicketList({
|
||||
splitKey: bodyshop.imexshopid
|
||||
});
|
||||
|
||||
const canEditCommittedTimeTickets = HasRbacAccess({ bodyshop, authLevel, action: "timetickets:editcommitted" });
|
||||
const canEditTimeTickets = HasRbacAccess({ bodyshop, authLevel, action: "timetickets:edit" });
|
||||
const canEditShiftTickets = HasRbacAccess({ bodyshop, authLevel, action: "timetickets:shiftedit" });
|
||||
|
||||
const totals = useMemo(() => {
|
||||
if (timetickets)
|
||||
return timetickets.reduce(
|
||||
@@ -65,6 +69,18 @@ export function TimeTicketList({
|
||||
return { productivehrs: 0, actualhrs: 0 };
|
||||
}, [timetickets]);
|
||||
|
||||
const isDisabled = (record) => {
|
||||
if (disabled === true || !record.id) return true;
|
||||
|
||||
const isShiftTicket = !record.ciecacode;
|
||||
const isCommitted = record.committed_at;
|
||||
if (isShiftTicket) {
|
||||
return !(canEditShiftTickets && (!isCommitted || canEditCommittedTimeTickets));
|
||||
}
|
||||
|
||||
return !(canEditTimeTickets && (!isCommitted || canEditCommittedTimeTickets));
|
||||
};
|
||||
|
||||
const columns = [
|
||||
...(Enhanced_Payroll.treatment === "on"
|
||||
? [
|
||||
@@ -206,76 +222,55 @@ export function TimeTicketList({
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
]),
|
||||
{
|
||||
title: t("timetickets.fields.created_by"),
|
||||
dataIndex: "created_by",
|
||||
key: "created_by",
|
||||
sorter: (a, b) => alphaSort(a.created_by, b.created_by),
|
||||
sortOrder: state.sortedInfo.columnKey === "created_by" && state.sortedInfo.order,
|
||||
render: (text, record) => record.created_by
|
||||
},
|
||||
// {
|
||||
// title: "Pay",
|
||||
// dataIndex: "pay",
|
||||
// key: "pay",
|
||||
// render: (text, record) =>
|
||||
// Dinero({ amount: Math.round(record.rate * 100) })
|
||||
// .multiply(record.flat_rate ? record.productivehrs : record.actualhrs)
|
||||
// .toFormat("$0.00"),
|
||||
// },
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
render: (text, record) => (
|
||||
<Space wrap>
|
||||
{techConsole && (
|
||||
<TimeTicketEnterButton
|
||||
actions={{ refetch }}
|
||||
context={{ id: record.id, timeticket: record }}
|
||||
disabled={!record.job || disabled}
|
||||
>
|
||||
<EditFilled />
|
||||
</TimeTicketEnterButton>
|
||||
)}
|
||||
{!techConsole && (
|
||||
<RbacWrapper
|
||||
action="timetickets:edit"
|
||||
noauth={() => {
|
||||
return <div />;
|
||||
}}
|
||||
>
|
||||
<TimeTicketEnterButton
|
||||
actions={{ refetch }}
|
||||
context={{
|
||||
id: record.id,
|
||||
timeticket: record
|
||||
}}
|
||||
disabled={
|
||||
HasRbacAccess({
|
||||
bodyshop,
|
||||
authLevel: authLevel,
|
||||
action: "timetickets:editcommitted"
|
||||
}) &&
|
||||
HasRbacAccess({
|
||||
bodyshop,
|
||||
authLevel: authLevel,
|
||||
action: "timetickets:shiftedit"
|
||||
})
|
||||
? disabled
|
||||
: !record.jobid
|
||||
}
|
||||
>
|
||||
<EditFilled />
|
||||
</TimeTicketEnterButton>
|
||||
</RbacWrapper>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
{
|
||||
title: t("timetickets.fields.created_by"),
|
||||
dataIndex: "created_by",
|
||||
key: "created_by",
|
||||
sorter: (a, b) => alphaSort(a.created_by, b.created_by),
|
||||
sortOrder: state.sortedInfo.columnKey === "created_by" && state.sortedInfo.order,
|
||||
render: (text, record) => record.created_by
|
||||
},
|
||||
// {
|
||||
// title: "Pay",
|
||||
// dataIndex: "pay",
|
||||
// key: "pay",
|
||||
// render: (text, record) =>
|
||||
// Dinero({ amount: Math.round(record.rate * 100) })
|
||||
// .multiply(record.flat_rate ? record.productivehrs : record.actualhrs)
|
||||
// .toFormat("$0.00"),
|
||||
// },
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
render: (text, record) => (
|
||||
<Space wrap>
|
||||
{techConsole && (
|
||||
<TimeTicketEnterButton
|
||||
actions={{ refetch }}
|
||||
context={{ id: record.id, timeticket: record }}
|
||||
disabled={!record.job || disabled}
|
||||
>
|
||||
<EditFilled />
|
||||
</TimeTicketEnterButton>
|
||||
)}
|
||||
{!techConsole && (
|
||||
<TimeTicketEnterButton
|
||||
actions={{ refetch }}
|
||||
context={{
|
||||
id: record.id,
|
||||
timeticket: record
|
||||
}}
|
||||
disabled={isDisabled(record)}
|
||||
>
|
||||
<EditFilled />
|
||||
</TimeTicketEnterButton>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
|
||||
@@ -329,7 +329,9 @@ export function LaborAllocationContainer({ jobid, loading, lineTicketData, hideT
|
||||
timetickets={lineTicketData.timetickets}
|
||||
adjustments={lineTicketData.jobs_by_pk.lbr_adjustments}
|
||||
/>
|
||||
{!hideTimeTickets && <TimeTicketList loading={loading} timetickets={lineTicketData.timetickets} techConsole />}
|
||||
{!hideTimeTickets && (
|
||||
<TimeTicketList loading={loading} timetickets={jobid ? lineTicketData.timetickets : []} techConsole />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { AlertOutlined } from "@ant-design/icons";
|
||||
import { Alert, Button, Col, Row, Space } from "antd";
|
||||
import { Alert, Button, Col, notification, Row, Space } from "antd";
|
||||
import i18n from "i18next";
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectUpdateAvailable } from "../../redux/application/application.selectors";
|
||||
import { useRegisterSW } from "virtual:pwa-register/react";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import useCountDown from "../../utils/countdownHook";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
updateAvailable: selectUpdateAvailable
|
||||
@@ -19,6 +20,15 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
export function UpdateAlert({ updateAvailable }) {
|
||||
const { t } = useTranslation();
|
||||
const [timerStarted, setTimerStarted] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [
|
||||
timeLeft,
|
||||
{
|
||||
start //pause, resume, reset
|
||||
}
|
||||
] = useCountDown(180000, 1000);
|
||||
|
||||
const {
|
||||
offlineReady: [offlineReady],
|
||||
needRefresh: [needRefresh],
|
||||
@@ -31,7 +41,7 @@ export function UpdateAlert({ updateAvailable }) {
|
||||
() => {
|
||||
r.update();
|
||||
},
|
||||
10 * 60 * 1000
|
||||
30 * 60 * 1000
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -40,11 +50,43 @@ export function UpdateAlert({ updateAvailable }) {
|
||||
}
|
||||
});
|
||||
|
||||
const ReloadNewVersion = useCallback(() => {
|
||||
setLoading(true);
|
||||
updateServiceWorker(true);
|
||||
setTimeout(() => {
|
||||
window.location.reload(true);
|
||||
}, 5000);
|
||||
}, [updateServiceWorker]);
|
||||
|
||||
useEffect(() => {
|
||||
if (import.meta.env.DEV) {
|
||||
console.log(`SW Status => Refresh? ${needRefresh} - offlineReady? ${offlineReady}`);
|
||||
if (needRefresh) {
|
||||
start();
|
||||
setTimerStarted(true);
|
||||
}
|
||||
}, [needRefresh, offlineReady]);
|
||||
}, [start, needRefresh, offlineReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (needRefresh && timerStarted && timeLeft < 60000) {
|
||||
notification.open({
|
||||
type: "warning",
|
||||
closable: false,
|
||||
duration: 65000,
|
||||
key: "autoupdate",
|
||||
message: t("general.actions.autoupdate", {
|
||||
time: (timeLeft / 1000).toFixed(0),
|
||||
app: InstanceRenderManager({
|
||||
imex: "$t(titles.imexonline)",
|
||||
rome: "$t(titles.romeonline)",
|
||||
promanager: "$t(titles.promanager)"
|
||||
})
|
||||
}),
|
||||
placement: "bottomRight"
|
||||
});
|
||||
}
|
||||
if (needRefresh && timerStarted && timeLeft <= 0) {
|
||||
ReloadNewVersion();
|
||||
}
|
||||
}, [timeLeft, t, needRefresh, ReloadNewVersion, timerStarted]);
|
||||
|
||||
if (!needRefresh) return null;
|
||||
|
||||
@@ -75,9 +117,10 @@ export function UpdateAlert({ updateAvailable }) {
|
||||
<Button onClick={() => window.open("https://imex-online.noticeable.news/", "_blank")}>
|
||||
{i18n.t("general.actions.viewreleasenotes")}
|
||||
</Button>
|
||||
<Button type="primary" onClick={() => updateServiceWorker(true)}>
|
||||
{i18n.t("general.actions.refresh")}
|
||||
<Button loading={loading} type="primary" onClick={() => ReloadNewVersion()}>
|
||||
{i18n.t("general.actions.refresh")} {`(${(timeLeft / 1000).toFixed(0)} s)`}
|
||||
</Button>
|
||||
<Button onClick={() => start(300000)}>{i18n.t("general.actions.delay")}</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -5,7 +5,7 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
//To be used as a form element only.
|
||||
// To be used as a form element only.
|
||||
|
||||
const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, preferredMake, showPhone }, ref) => {
|
||||
const [option, setOption] = useState(value);
|
||||
@@ -33,9 +33,25 @@ const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, pref
|
||||
if (!value || !options) return label;
|
||||
const discount = options?.find((o) => o.id === value)?.discount;
|
||||
return (
|
||||
<div className="imex-flex-row" style={{ width: "100%" }}>
|
||||
<div style={{ flex: 1 }}>{label}</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexWrap: "nowrap",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap"
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
{discount && discount !== 0 ? <Tag color="green">{`${discount * 100}%`}</Tag> : null}
|
||||
</div>
|
||||
);
|
||||
@@ -45,36 +61,67 @@ const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, pref
|
||||
optionFilterProp="name"
|
||||
onSelect={onSelect}
|
||||
disabled={disabled || false}
|
||||
optionLabelProp={"name"}
|
||||
optionLabelProp="name"
|
||||
>
|
||||
{favorites
|
||||
? favorites.map((o) => (
|
||||
<Option key={`favorite-${o.id}`} value={o.id} name={o.name} discount={o.discount}>
|
||||
<div className="imex-flex-row">
|
||||
<div style={{ flex: 1 }}>{o.name}</div>
|
||||
<Space style={{ marginLeft: "1rem" }}>
|
||||
<HeartOutlined style={{ color: "red" }} />
|
||||
{o.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
|
||||
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
|
||||
</Space>
|
||||
{favorites &&
|
||||
favorites.map((o) => (
|
||||
<Option key={`favorite-${o.id}`} value={o.id} name={o.name} discount={o.discount}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexWrap: "nowrap",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap"
|
||||
}}
|
||||
>
|
||||
{o.name}
|
||||
</div>
|
||||
</Option>
|
||||
))
|
||||
: null}
|
||||
{options
|
||||
? options.map((o) => (
|
||||
<Option key={o.id} value={o.id} name={o.name} discount={o.discount}>
|
||||
<div className="imex-flex-row" style={{ width: "100%" }}>
|
||||
<div style={{ flex: 1 }}>{o.name}</div>
|
||||
|
||||
<Space style={{ marginLeft: "1rem" }}>
|
||||
{o.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
|
||||
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
|
||||
</Space>
|
||||
<Space style={{ marginLeft: "1rem" }}>
|
||||
<HeartOutlined style={{ color: "red" }} />
|
||||
{o.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
|
||||
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
|
||||
</Space>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
{options &&
|
||||
options.map((o) => (
|
||||
<Option key={o.id} value={o.id} name={o.name} discount={o.discount}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexWrap: "nowrap",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap"
|
||||
}}
|
||||
>
|
||||
{o.name}
|
||||
</div>
|
||||
</Option>
|
||||
))
|
||||
: null}
|
||||
<Space style={{ marginLeft: "1rem" }}>
|
||||
{o.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
|
||||
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
|
||||
</Space>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { connect } from "react-redux";
|
||||
import { GlobalOutlined } from "@ant-design/icons";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import React from "react";
|
||||
import { selectWssStatus } from "../../redux/application/application.selectors";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
wssStatus: selectWssStatus
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(WssStatusDisplay);
|
||||
|
||||
export function WssStatusDisplay({ wssStatus }) {
|
||||
console.log("🚀 ~ WssStatusDisplay ~ wssStatus:", wssStatus);
|
||||
return <GlobalOutlined style={{ color: wssStatus === "connected" ? "green" : "red", marginRight: ".5rem" }} />;
|
||||
}
|
||||
13
client/src/contexts/SocketIO/socketContext.jsx
Normal file
13
client/src/contexts/SocketIO/socketContext.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
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;
|
||||
88
client/src/contexts/SocketIO/useSocket.js
Normal file
88
client/src/contexts/SocketIO/useSocket.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import SocketIO from "socket.io-client";
|
||||
import { auth } from "../../firebase/firebase.utils";
|
||||
import { store } from "../../redux/store";
|
||||
import { setWssStatus } from "../../redux/application/application.actions";
|
||||
const useSocket = (bodyshop) => {
|
||||
const socketRef = useRef(null);
|
||||
const [clientId, setClientId] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = auth.onIdTokenChanged(async (user) => {
|
||||
if (user) {
|
||||
const newToken = await user.getIdToken();
|
||||
|
||||
if (socketRef.current) {
|
||||
// Send new token to server
|
||||
socketRef.current.emit("update-token", newToken);
|
||||
} else if (bodyshop && bodyshop.id) {
|
||||
// Initialize the socket
|
||||
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: newToken },
|
||||
reconnectionAttempts: Infinity,
|
||||
reconnectionDelay: 2000,
|
||||
reconnectionDelayMax: 10000
|
||||
});
|
||||
|
||||
socketRef.current = socketInstance;
|
||||
|
||||
const handleBodyshopMessage = (message) => {
|
||||
if (!import.meta.env.DEV) return;
|
||||
console.log(`Received message for bodyshop ${bodyshop.id}:`, message);
|
||||
};
|
||||
|
||||
const handleConnect = () => {
|
||||
console.log("Socket connected:", socketInstance.id);
|
||||
socketInstance.emit("join-bodyshop-room", bodyshop.id);
|
||||
setClientId(socketInstance.id);
|
||||
store.dispatch(setWssStatus("connected"))
|
||||
};
|
||||
|
||||
const handleReconnect = (attempt) => {
|
||||
console.log(`Socket reconnected after ${attempt} attempts`);
|
||||
store.dispatch(setWssStatus("connected"))
|
||||
};
|
||||
|
||||
const handleConnectionError = (err) => {
|
||||
console.error("Socket connection error:", err);
|
||||
store.dispatch(setWssStatus("error"))
|
||||
};
|
||||
|
||||
const handleDisconnect = () => {
|
||||
console.log("Socket disconnected");
|
||||
store.dispatch(setWssStatus("disconnected"))
|
||||
};
|
||||
|
||||
socketInstance.on("connect", handleConnect);
|
||||
socketInstance.on("reconnect", handleReconnect);
|
||||
socketInstance.on("connect_error", handleConnectionError);
|
||||
socketInstance.on("disconnect", handleDisconnect);
|
||||
socketInstance.on("bodyshop-message", handleBodyshopMessage);
|
||||
}
|
||||
} else {
|
||||
// User is not authenticated
|
||||
if (socketRef.current) {
|
||||
socketRef.current.disconnect();
|
||||
socketRef.current = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up the listener on unmount
|
||||
return () => {
|
||||
unsubscribe();
|
||||
if (socketRef.current) {
|
||||
socketRef.current.disconnect();
|
||||
socketRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [bodyshop]);
|
||||
|
||||
return { socket: socketRef.current, clientId };
|
||||
};
|
||||
|
||||
export default useSocket;
|
||||
@@ -5,7 +5,6 @@ import { getFirestore } from "firebase/firestore";
|
||||
import { getMessaging, getToken, onMessage } from "firebase/messaging";
|
||||
import { store } from "../redux/store";
|
||||
import axios from "axios";
|
||||
import { checkBeta } from "../utils/handleBeta";
|
||||
|
||||
const config = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
|
||||
initializeApp(config);
|
||||
@@ -88,7 +87,7 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
|
||||
operationName: eventName,
|
||||
variables: additionalParams,
|
||||
dbevent: false,
|
||||
env: checkBeta() ? "beta" : "master"
|
||||
env: `master-AIO|${import.meta.env.VITE_APP_GIT_SHA_DATE}`
|
||||
});
|
||||
// console.log(
|
||||
// "%c[Analytics]",
|
||||
|
||||
@@ -138,7 +138,8 @@ export const QUERY_BODYSHOP = gql`
|
||||
tt_enforce_hours_for_tech_console
|
||||
md_tasks_presets
|
||||
use_paint_scale_data
|
||||
md_ro_guard
|
||||
intellipay_config
|
||||
md_ro_guard
|
||||
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
|
||||
id
|
||||
name
|
||||
@@ -266,7 +267,8 @@ export const UPDATE_SHOP = gql`
|
||||
enforce_conversion_category
|
||||
tt_enforce_hours_for_tech_console
|
||||
md_tasks_presets
|
||||
md_ro_guard
|
||||
intellipay_config
|
||||
md_ro_guard
|
||||
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
|
||||
id
|
||||
name
|
||||
|
||||
@@ -2461,6 +2461,14 @@ export const SUBSCRIPTION_JOBS_IN_PRODUCTION = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW = gql`
|
||||
subscription SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW {
|
||||
jobs: jobs_inproduction {
|
||||
id
|
||||
updated_at
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const QUERY_JOBS_IN_PRODUCTION = gql`
|
||||
query QUERY_JOBS_IN_PRODUCTION {
|
||||
|
||||
@@ -23,17 +23,14 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
|
||||
|
||||
export const socket = SocketIO(
|
||||
import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : window.location.origin,
|
||||
{
|
||||
path: "/ws",
|
||||
withCredentials: true,
|
||||
auth: async (callback) => {
|
||||
const token = auth.currentUser && (await auth.currentUser.getIdToken());
|
||||
callback({ token });
|
||||
}
|
||||
export const socket = SocketIO(import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "", {
|
||||
path: "/ws",
|
||||
withCredentials: true,
|
||||
auth: async (callback) => {
|
||||
const token = auth.currentUser && (await auth.currentUser.getIdToken());
|
||||
callback({ token });
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -34,7 +34,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
|
||||
|
||||
export const socket = SocketIO(
|
||||
import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "http://localhost:4000", // for dev testing,
|
||||
import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "", // for dev testing,
|
||||
{
|
||||
path: "/ws",
|
||||
withCredentials: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FloatButton, Layout, Spin } from "antd";
|
||||
// import preval from "preval.macro";
|
||||
import React, { lazy, Suspense, useEffect, useState } from "react";
|
||||
import React, { lazy, Suspense, useContext, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, Route, Routes } from "react-router-dom";
|
||||
@@ -19,11 +19,13 @@ import PartnerPingComponent from "../../components/partner-ping/partner-ping.com
|
||||
import PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container";
|
||||
import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component";
|
||||
import { requestForToken } from "../../firebase/firebase.utils";
|
||||
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||
import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors";
|
||||
|
||||
import UpdateAlert from "../../components/update-alert/update-alert.component";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
||||
import "./manage.page.styles.scss";
|
||||
import WssStatusDisplayComponent from "../../components/wss-status-display/wss-status-display.component.jsx";
|
||||
|
||||
const JobsPage = lazy(() => import("../jobs/jobs.page"));
|
||||
|
||||
@@ -110,6 +112,7 @@ const mapDispatchToProps = (dispatch) => ({});
|
||||
export function Manage({ conflict, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
const [chatVisible] = useState(false);
|
||||
const { socket, clientId } = useContext(SocketContext);
|
||||
|
||||
useEffect(() => {
|
||||
const widgetId = InstanceRenderManager({
|
||||
@@ -129,6 +132,7 @@ export function Manage({ conflict, bodyshop }) {
|
||||
promanager: t("titles.promanager")
|
||||
});
|
||||
}, [t]);
|
||||
|
||||
const AppRouteTable = (
|
||||
<Suspense
|
||||
fallback={
|
||||
@@ -569,6 +573,13 @@ export function Manage({ conflict, bodyshop }) {
|
||||
else if (bodyshop && bodyshop.sub_status !== "active") PageContent = <ShopSubStatusComponent />;
|
||||
else PageContent = AppRouteTable;
|
||||
|
||||
const broadcastMessage = () => {
|
||||
if (socket && bodyshop && bodyshop.id) {
|
||||
console.log(`Broadcasting message to bodyshop ${bodyshop.id}:`);
|
||||
socket.emit("broadcast-to-bodyshop", bodyshop.id, `Hello from ${clientId}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{import.meta.env.PROD && <ChatAffixContainer bodyshop={bodyshop} chatVisible={chatVisible} />}
|
||||
@@ -594,7 +605,8 @@ export function Manage({ conflict, bodyshop }) {
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex" }}>
|
||||
<div>
|
||||
<WssStatusDisplayComponent />
|
||||
<div onClick={broadcastMessage}>
|
||||
{`${InstanceRenderManager({
|
||||
imex: t("titles.imexonline"),
|
||||
rome: t("titles.romeonline"),
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
import React from "react";
|
||||
import ProductionBoardKanbanContainer from "../../components/production-board-kanban/production-board-kanban.container";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ProductionBoardComponent);
|
||||
|
||||
export default function ProductionBoardComponent() {
|
||||
return <ProductionBoardKanbanContainer />;
|
||||
export function ProductionBoardComponent({ bodyshop }) {
|
||||
const {
|
||||
treatments: { Production_Use_View }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Production_Use_View"],
|
||||
splitKey: bodyshop && bodyshop.imexshopid
|
||||
});
|
||||
|
||||
return <ProductionBoardKanbanContainer subscriptionType={Production_Use_View.treatment} />;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,31 @@ import React from "react";
|
||||
import NoteUpsertModal from "../../components/note-upsert-modal/note-upsert-modal.container";
|
||||
import ProductionListTable from "../../components/production-list-table/production-list-table.container";
|
||||
|
||||
export default function ProductionListComponent() {
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ProductionListComponent);
|
||||
|
||||
export function ProductionListComponent({ bodyshop }) {
|
||||
const {
|
||||
treatments: { Production_Use_View }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Production_Use_View"],
|
||||
splitKey: bodyshop && bodyshop.imexshopid
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<NoteUpsertModal />
|
||||
<ProductionListTable />
|
||||
<ProductionListTable bodyshop={bodyshop} subscriptionType={Production_Use_View.treatment} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { Tabs } from "antd";
|
||||
import React, { useEffect } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import queryString from "query-string";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||
import ShopCsiConfig from "../../components/shop-csi-config/shop-csi-config.component";
|
||||
import ShopEmployeesContainer from "../../components/shop-employees/shop-employees.container";
|
||||
import ShopInfoContainer from "../../components/shop-info/shop-info.container";
|
||||
import ShopCsiConfig from "../../components/shop-csi-config/shop-csi-config.component";
|
||||
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import ShopInfoUsersComponent from "../../components/shop-users/shop-users.component";
|
||||
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import ShopInfoUsersComponent from "../../components/shop-users/shop-users.component";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
|
||||
import ShopTeamsContainer from "../../components/shop-teams/shop-teams.container";
|
||||
import { HasFeatureAccess } from "../../components/feature-wrapper/feature-wrapper.component";
|
||||
import ShopTeamsContainer from "../../components/shop-teams/shop-teams.container";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import RbacWrapperComponent from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||
import TechLookupJobsList from "../../components/tech-lookup-jobs-list/tech-lookup-jobs-list.component";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import TechLookupJobsDrawer from "../../components/tech-lookup-jobs-drawer/tech-lookup-jobs-drawer.component";
|
||||
|
||||
export default function TechLookupContainer() {
|
||||
const { t } = useTranslation();
|
||||
@@ -20,6 +21,7 @@ export default function TechLookupContainer() {
|
||||
return (
|
||||
<div>
|
||||
<RbacWrapperComponent action="jobs:list-active">
|
||||
<TechLookupJobsDrawer />
|
||||
<TechLookupJobsList />
|
||||
</RbacWrapperComponent>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,6 @@ import ErrorBoundary from "../../components/error-boundary/error-boundary.compon
|
||||
import FeatureWrapper from "../../components/feature-wrapper/feature-wrapper.component";
|
||||
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
|
||||
import TechHeader from "../../components/tech-header/tech-header.component";
|
||||
import TechLookupJobsDrawer from "../../components/tech-lookup-jobs-drawer/tech-lookup-jobs-drawer.component";
|
||||
import TechSider from "../../components/tech-sider/tech-sider.component";
|
||||
import UpdateAlert from "../../components/update-alert/update-alert.component";
|
||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
@@ -68,7 +67,7 @@ export function TechPage({ technician }) {
|
||||
<Layout>
|
||||
<UpdateAlert />
|
||||
<TechHeader />
|
||||
<TechLookupJobsDrawer />
|
||||
|
||||
<TaskUpsertModalContainer />
|
||||
<Content className="tech-content-container">
|
||||
<ErrorBoundary>
|
||||
|
||||
@@ -67,3 +67,7 @@ export const setUpdateAvailable = (isUpdateAvailable) => ({
|
||||
type: ApplicationActionTypes.SET_UPDATE_AVAILABLE,
|
||||
payload: isUpdateAvailable
|
||||
});
|
||||
export const setWssStatus = (status) => ({
|
||||
type: ApplicationActionTypes.SET_WSS_STATUS,
|
||||
payload: status
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import ApplicationActionTypes from "./application.types";
|
||||
const INITIAL_STATE = {
|
||||
loading: false,
|
||||
online: true,
|
||||
wssStatus: "disconnected",
|
||||
updateAvailable: false,
|
||||
breadcrumbs: [],
|
||||
recentItems: [],
|
||||
@@ -87,6 +88,9 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
|
||||
case ApplicationActionTypes.SET_PROBLEM_JOBS: {
|
||||
return { ...state, problemJobs: action.payload };
|
||||
}
|
||||
case ApplicationActionTypes.SET_WSS_STATUS: {
|
||||
return { ...state, wssStatus: action.payload };
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -22,3 +22,4 @@ export const selectJobReadOnly = createSelector([selectApplication], (applicatio
|
||||
export const selectOnline = createSelector([selectApplication], (application) => application.online);
|
||||
export const selectProblemJobs = createSelector([selectApplication], (application) => application.problemJobs);
|
||||
export const selectUpdateAvailable = createSelector([selectApplication], (application) => application.updateAvailable);
|
||||
export const selectWssStatus = createSelector([selectApplication], (application) => application.wssStatus);
|
||||
|
||||
@@ -12,6 +12,7 @@ const ApplicationActionTypes = {
|
||||
SET_ONLINE_STATUS: "SET_ONLINE_STATUS",
|
||||
INSERT_AUDIT_TRAIL: "INSERT_AUDIT_TRAIL",
|
||||
SET_PROBLEM_JOBS: "SET_PROBLEM_JOBS",
|
||||
SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE"
|
||||
SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE",
|
||||
SET_WSS_STATUS: "SET_WSS_STATUS"
|
||||
};
|
||||
export default ApplicationActionTypes;
|
||||
|
||||
@@ -36,7 +36,8 @@ export function* openChatByPhone({ payload }) {
|
||||
data: { conversations }
|
||||
} = yield client.query({
|
||||
query: CONVERSATION_ID_BY_PHONE,
|
||||
variables: { phone: p.number }
|
||||
variables: { phone: p.number },
|
||||
fetchPolicy: 'no-cache'
|
||||
});
|
||||
|
||||
if (conversations.length === 0) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
signInWithEmailAndPassword,
|
||||
signOut
|
||||
} from "firebase/auth";
|
||||
import { doc, getDoc, setDoc } from "firebase/firestore";
|
||||
import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "firebase/firestore";
|
||||
import { getToken } from "firebase/messaging";
|
||||
import i18next from "i18next";
|
||||
import LogRocket from "logrocket";
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
validatePasswordResetSuccess
|
||||
} from "./user.actions";
|
||||
import UserActionTypes from "./user.types";
|
||||
import cleanAxios from "../../utils/CleanAxios";
|
||||
|
||||
const fpPromise = FingerprintJS.load();
|
||||
|
||||
@@ -177,10 +178,24 @@ export function* setInstanceIdSaga({ payload: uid }) {
|
||||
// Get the visitor identifier when you need it.
|
||||
const fp = yield fpPromise;
|
||||
const result = yield fp.get();
|
||||
yield setDoc(userInstanceRef, {
|
||||
timestamp: new Date(),
|
||||
fingerprint: result.visitorId
|
||||
});
|
||||
const res = yield cleanAxios.get("https://api.ipify.org/?format=json");
|
||||
const udoc = yield getDoc(userInstanceRef);
|
||||
|
||||
if (!udoc.data()) {
|
||||
yield setDoc(userInstanceRef, {
|
||||
timestamp: new Date(),
|
||||
fingerprint: result.visitorId,
|
||||
//totalFingerprint: result,
|
||||
ip: [res.data.ip]
|
||||
});
|
||||
} else {
|
||||
yield updateDoc(userInstanceRef, {
|
||||
timestamp: new Date(),
|
||||
fingerprint: result.visitorId,
|
||||
//totalFingerprint: result,
|
||||
ip: arrayUnion(res.data.ip)
|
||||
});
|
||||
}
|
||||
|
||||
yield put(setLocalFingerprint(result.visitorId));
|
||||
yield delay(5 * 60 * 1000);
|
||||
@@ -227,6 +242,10 @@ export function* signInSuccessSaga({ payload }) {
|
||||
window.$crisp.push(["set", "user:nickname", [payload.displayName || payload.email]]);
|
||||
window.$crisp.push(["set", "session:segments", [["user"]]]);
|
||||
},
|
||||
rome: () => {
|
||||
window.$zoho.salesiq.visitor.name(payload.displayName || payload.email);
|
||||
window.$zoho.salesiq.visitor.email(payload.email);
|
||||
},
|
||||
promanager: () => {
|
||||
Userpilot.identify(payload.email, {
|
||||
email: payload.email
|
||||
@@ -295,8 +314,8 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
||||
try {
|
||||
const userEmail = yield select((state) => state.user.currentUser.email);
|
||||
try {
|
||||
//console.log("Setting shop timezone.");
|
||||
// dayjs.tz.setDefault(payload.timezone);
|
||||
console.log("Setting shop timezone.");
|
||||
day.tz.setDefault(payload.timezone);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
@@ -356,6 +375,9 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
||||
if (authRecord[0] && authRecord[0].user.validemail) {
|
||||
window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]);
|
||||
}
|
||||
},
|
||||
rome: () => {
|
||||
window.$zoho.salesiq.visitor.info({ "Shop Name": payload.shopname });
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
"jobclosedwithbypass": "Job was invoiced using the master bypass password. ",
|
||||
"jobconverted": "Job converted and assigned number {{ro_number}}.",
|
||||
"jobdelivery": "Job intake completed. Status set to {{status}}. Actual completion is {{actual_completion}}.",
|
||||
"jobexported": "",
|
||||
"jobexported": "Job has been exported",
|
||||
"jobfieldchanged": "Job field $t(jobs.fields.{{field}}) changed to {{value}}.",
|
||||
"jobimported": "Job imported.",
|
||||
"jobinproductionchange": "Job production status set to {{inproduction}}",
|
||||
@@ -230,7 +230,7 @@
|
||||
"markexported": "Mark Exported",
|
||||
"markforreexport": "Mark for Re-export",
|
||||
"new": "New Bill",
|
||||
"nobilllines": "This part has not yet been recieved.",
|
||||
"nobilllines": "",
|
||||
"noneselected": "No bill selected.",
|
||||
"onlycmforinvoiced": "Only credit memos can be entered for any Job that has been invoiced, exported, or voided.",
|
||||
"printlabels": "Print Labels",
|
||||
@@ -270,9 +270,9 @@
|
||||
"testrender": "Test Render"
|
||||
},
|
||||
"errors": {
|
||||
"creatingdefaultview": "Error creating default view.",
|
||||
"loading": "Unable to load shop details. Please call technical support.",
|
||||
"saving": "Error encountered while saving. {{message}}",
|
||||
"creatingdefaultview": "Error creating default view."
|
||||
"saving": "Error encountered while saving. {{message}}"
|
||||
},
|
||||
"fields": {
|
||||
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
|
||||
@@ -285,19 +285,21 @@
|
||||
},
|
||||
"appt_length": "Default Appointment Length",
|
||||
"attach_pdf_to_email": "Attach PDF copy to sent emails?",
|
||||
"batchid": "ADP Batch ID",
|
||||
"bill_allow_post_to_closed": "Allow Bills to be posted to Closed Jobs",
|
||||
"bill_federal_tax_rate": "Bills - Federal Tax Rate %",
|
||||
"bill_local_tax_rate": "Bill - Local Tax Rate %",
|
||||
"bill_state_tax_rate": "Bill - Provincial/State Tax Rate %",
|
||||
"city": "City",
|
||||
"closingperiod": "Closing Period",
|
||||
"companycode": "ADP Company Code",
|
||||
"country": "Country",
|
||||
"dailybodytarget": "Scoreboard - Daily Body Target",
|
||||
"dailypainttarget": "Scoreboard - Daily Paint Target",
|
||||
"default_adjustment_rate": "Default Labor Deduction Adjustment Rate",
|
||||
"deliver": {
|
||||
"templates": "Delivery Templates",
|
||||
"require_actual_delivery_date": "Require Actual Delivery"
|
||||
"require_actual_delivery_date": "Require Actual Delivery",
|
||||
"templates": "Delivery Templates"
|
||||
},
|
||||
"dms": {
|
||||
"apcontrol": "AP Control Number",
|
||||
@@ -330,6 +332,10 @@
|
||||
"next_contact_hours": "Automatic Next Contact Date - Hours from Intake",
|
||||
"templates": "Intake Templates"
|
||||
},
|
||||
"intellipay_config": {
|
||||
"cash_discount_percentage": "Cash Discount %",
|
||||
"enable_cash_discount": "Enable Cash Discounting"
|
||||
},
|
||||
"invoice_federal_tax_rate": "Invoices - Federal Tax Rate",
|
||||
"invoice_local_tax_rate": "Invoices - Local Tax Rate",
|
||||
"invoice_state_tax_rate": "Invoices - State Tax Rate",
|
||||
@@ -661,6 +667,8 @@
|
||||
"filehandlers": "Adjusters",
|
||||
"insurancecos": "Insurance Companies",
|
||||
"intakechecklist": "Intake Checklist",
|
||||
"intellipay": "IntelliPay",
|
||||
"intellipay_cash_discount": "Please ensure that cash discounting has been enabled on your merchant account. Reach out to IntelliPay Support if you need assistance. ",
|
||||
"jobstatuses": "Job Statuses",
|
||||
"laborrates": "Labor Rates",
|
||||
"licensing": "Licensing",
|
||||
@@ -700,10 +708,10 @@
|
||||
"workingdays": "Working Days"
|
||||
},
|
||||
"successes": {
|
||||
"save": "Shop configuration saved successfully. ",
|
||||
"unsavedchanges": "Unsaved changes will be lost. Are you sure you want to continue?",
|
||||
"areyousure": "Are you sure you want to continue?",
|
||||
"defaultviewcreated": "Default view created successfully."
|
||||
"defaultviewcreated": "Default view created successfully.",
|
||||
"save": "Shop configuration saved successfully. ",
|
||||
"unsavedchanges": "Unsaved changes will be lost. Are you sure you want to continue?"
|
||||
},
|
||||
"validation": {
|
||||
"centermustexist": "The chosen responsibility center does not exist.",
|
||||
@@ -1133,8 +1141,8 @@
|
||||
},
|
||||
"general": {
|
||||
"actions": {
|
||||
"defaults": "Defaults",
|
||||
"add": "Add",
|
||||
"autoupdate": "{{app}} will automatically update in {{time}} seconds. Please save all changes.",
|
||||
"calculate": "Calculate",
|
||||
"cancel": "Cancel",
|
||||
"clear": "Clear",
|
||||
@@ -1142,6 +1150,8 @@
|
||||
"copied": "Copied!",
|
||||
"copylink": "Copy Link",
|
||||
"create": "Create",
|
||||
"defaults": "Defaults",
|
||||
"delay": "Delay Update (5 mins)",
|
||||
"delete": "Delete",
|
||||
"deleteall": "Delete All",
|
||||
"deselectall": "Deselect All",
|
||||
@@ -1153,10 +1163,12 @@
|
||||
"print": "Print",
|
||||
"refresh": "Refresh",
|
||||
"remove": "Remove",
|
||||
"remove_alert": "Are you sure you want to dismiss the alert?",
|
||||
"reset": "Reset your changes.",
|
||||
"resetpassword": "Reset Password",
|
||||
"save": "Save",
|
||||
"saveandnew": "Save and New",
|
||||
"saveas": "Save As",
|
||||
"selectall": "Select All",
|
||||
"send": "Send",
|
||||
"sendbysms": "Send by SMS",
|
||||
@@ -1164,9 +1176,7 @@
|
||||
"submit": "Submit",
|
||||
"tryagain": "Try Again",
|
||||
"view": "View",
|
||||
"viewreleasenotes": "See What's Changed",
|
||||
"remove_alert": "Are you sure you want to dismiss the alert?",
|
||||
"saveas": "Save As"
|
||||
"viewreleasenotes": "See What's Changed"
|
||||
},
|
||||
"errors": {
|
||||
"fcm": "You must allow notification permissions to have real time messaging. Click to try again.",
|
||||
@@ -1181,7 +1191,6 @@
|
||||
"vehicle": "Vehicle"
|
||||
},
|
||||
"labels": {
|
||||
"unsavedchanges": "Unsaved changes.",
|
||||
"actions": "Actions",
|
||||
"areyousure": "Are you sure?",
|
||||
"barcode": "Barcode",
|
||||
@@ -1250,6 +1259,7 @@
|
||||
"tuesday": "Tuesday",
|
||||
"tvmode": "TV Mode",
|
||||
"unknown": "Unknown",
|
||||
"unsavedchanges": "Unsaved changes.",
|
||||
"username": "Username",
|
||||
"view": "View",
|
||||
"wednesday": "Wednesday",
|
||||
@@ -1363,6 +1373,7 @@
|
||||
},
|
||||
"job_payments": {
|
||||
"buttons": {
|
||||
"create_short_link": "Generate Short Link",
|
||||
"goback": "Go Back",
|
||||
"proceedtopayment": "Proceed to Payment",
|
||||
"refundpayment": "Refund Payment"
|
||||
@@ -2739,41 +2750,6 @@
|
||||
}
|
||||
},
|
||||
"production": {
|
||||
"constants": {
|
||||
"main_profile": "Default"
|
||||
},
|
||||
"options": {
|
||||
"small": "Small",
|
||||
"medium": "Medium",
|
||||
"large": "Large",
|
||||
"vertical": "Vertical",
|
||||
"horizontal": "Horizontal"
|
||||
},
|
||||
"settings": {
|
||||
"layout": "Layout",
|
||||
"information": "Information",
|
||||
"statistics_title": "Statistics",
|
||||
"board_settings": "Board Settings",
|
||||
"filters_title": "Filters",
|
||||
"filters": {
|
||||
"md_ins_cos": "Insurance Companies",
|
||||
"md_estimators": "Estimators"
|
||||
},
|
||||
"statistics": {
|
||||
"total_hours_in_production": "Hours in Production",
|
||||
"total_lab_in_production": "Body Hours in Production",
|
||||
"total_lar_in_production": "Refinish Hours in Production",
|
||||
"total_amount_in_production": "Dollars in Production",
|
||||
"jobs_in_production": "Jobs in Production",
|
||||
"total_hours_on_board": "Hours on Board",
|
||||
"total_lab_on_board": "Body Hours on Board",
|
||||
"total_lar_on_board": "Refinish Hours on Board",
|
||||
"total_amount_on_board": "Dollars on Board",
|
||||
"total_jobs_on_board": "Jobs on Board",
|
||||
"tasks_in_production": "Tasks in Production",
|
||||
"tasks_on_board": "Tasks on Board"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"addcolumns": "Add Columns",
|
||||
"bodypriority-clear": "Clear Body Priority",
|
||||
@@ -2788,29 +2764,23 @@
|
||||
"suspend": "Suspend",
|
||||
"unsuspend": "Unsuspend"
|
||||
},
|
||||
"constants": {
|
||||
"main_profile": "Default"
|
||||
},
|
||||
"errors": {
|
||||
"boardupdate": "Error encountered updating Job. {{message}}",
|
||||
"removing": "Error removing from production board. {{error}}",
|
||||
"settings": "Error saving board settings: {{error}}",
|
||||
"name_exists": "A Profile with this name already exists. Please choose a different name.",
|
||||
"name_required": "Profile name is required."
|
||||
"name_required": "Profile name is required.",
|
||||
"removing": "Error removing from production board. {{error}}",
|
||||
"settings": "Error saving board settings: {{error}}"
|
||||
},
|
||||
"labels": {
|
||||
"kiosk_mode": "Kiosk Mode",
|
||||
"on": "On",
|
||||
"off": "Off",
|
||||
"wide": "Wide",
|
||||
"tall": "Tall",
|
||||
"vertical": "Vertical",
|
||||
"horizontal": "Horizontal",
|
||||
"orientation": "Board Orientation",
|
||||
"card_size": "Card Size",
|
||||
"model_info": "Vehicle Info",
|
||||
"actual_in": "Actual In",
|
||||
"addnewprofile": "Add New Profile",
|
||||
"alert": "Alert",
|
||||
"tasks": "Tasks",
|
||||
"alertoff": "Remove alert from Job",
|
||||
"alerton": "Add alert to Job",
|
||||
"alerts": "Alerts",
|
||||
"ats": "Alternative Transportation",
|
||||
"bodyhours": "B",
|
||||
"bodypriority": "B/P",
|
||||
@@ -2820,6 +2790,7 @@
|
||||
"qbo_usa": "QBO USA"
|
||||
}
|
||||
},
|
||||
"card_size": "Card Size",
|
||||
"cardcolor": "Colored Cards",
|
||||
"cardsettings": "Card Settings",
|
||||
"clm_no": "Claim Number",
|
||||
@@ -2828,48 +2799,100 @@
|
||||
"detailpriority": "D/P",
|
||||
"employeeassignments": "Employee Assignments",
|
||||
"employeesearch": "Employee Search",
|
||||
"estimator": "Estimator",
|
||||
"horizontal": "Horizontal",
|
||||
"ins_co_nm": "Insurance Company Name",
|
||||
"jobdetail": "Job Details",
|
||||
"kiosk_mode": "Kiosk Mode",
|
||||
"laborhrs": "Labor Hours",
|
||||
"legend": "Legend:",
|
||||
"model_info": "Vehicle Info",
|
||||
"note": "Production Note",
|
||||
"off": "Off",
|
||||
"on": "On",
|
||||
"orientation": "Board Orientation",
|
||||
"ownr_nm": "Customer Name",
|
||||
"paintpriority": "P/P",
|
||||
"partsstatus": "Parts Status",
|
||||
"estimator": "Estimator",
|
||||
"subtotal": "Subtotal",
|
||||
"production_note": "Production Note",
|
||||
"refinishhours": "R",
|
||||
"scheduled_completion": "Scheduled Completion",
|
||||
"selectview": "Select a View",
|
||||
"stickyheader": "Sticky Header (BETA)",
|
||||
"sublets": "Sublets",
|
||||
"subtotal": "Subtotal",
|
||||
"tall": "Tall",
|
||||
"tasks": "Tasks",
|
||||
"totalhours": "Total Hrs ",
|
||||
"touchtime": "T/T",
|
||||
"vertical": "Vertical",
|
||||
"viewname": "View Name",
|
||||
"alerts": "Alerts",
|
||||
"addnewprofile": "Add New Profile"
|
||||
"wide": "Wide"
|
||||
},
|
||||
"options": {
|
||||
"horizontal": "Horizontal",
|
||||
"large": "Large",
|
||||
"medium": "Medium",
|
||||
"small": "Small",
|
||||
"vertical": "Vertical"
|
||||
},
|
||||
"settings": {
|
||||
"board_settings": "Board Settings",
|
||||
"filters": {
|
||||
"md_estimators": "Estimators",
|
||||
"md_ins_cos": "Insurance Companies"
|
||||
},
|
||||
"filters_title": "Filters",
|
||||
"information": "Information",
|
||||
"layout": "Layout",
|
||||
"statistics": {
|
||||
"jobs_in_production": "Jobs in Production",
|
||||
"tasks_in_production": "Tasks in Production",
|
||||
"tasks_on_board": "Tasks on Board",
|
||||
"tasks_in_view": "Tasks in View",
|
||||
"total_amount_in_production": "Dollars in Production",
|
||||
"total_amount_on_board": "Dollars on Board",
|
||||
"total_amount_in_view": "Dollars in View",
|
||||
"total_hours_in_production": "Hours in Production",
|
||||
"total_hours_on_board": "Hours on Board",
|
||||
"total_hours_in_view": "Hours in View",
|
||||
"total_jobs_on_board": "Jobs on Board",
|
||||
"total_jobs_in_view": "Jobs in View",
|
||||
"total_lab_in_production": "Body Hours in Production",
|
||||
"total_lab_on_board": "Body Hours on Board",
|
||||
"total_lab_in_view": "Body Hours in View",
|
||||
"total_lar_in_production": "Refinish Hours in Production",
|
||||
"total_lar_on_board": "Refinish Hours on Board",
|
||||
"total_lar_in_view": "Refinish Hours in View"
|
||||
},
|
||||
"statistics_title": "Statistics"
|
||||
},
|
||||
"statistics": {
|
||||
"currency_symbol": "$",
|
||||
"hours": "Hours",
|
||||
"jobs": "Jobs",
|
||||
"jobs_in_production": "Jobs in Production",
|
||||
"tasks": "Tasks",
|
||||
"tasks_in_production": "Tasks in Production",
|
||||
"tasks_on_board": "Tasks on Board",
|
||||
"tasks_in_view": "Tasks in View",
|
||||
"total_amount_in_production": "Dollars in Production",
|
||||
"total_amount_on_board": "Dollars on Board",
|
||||
"total_amount_in_view": "Dollars in View",
|
||||
"total_hours_in_production": "Hours in Production",
|
||||
"total_hours_on_board": "Hours on Board",
|
||||
"total_hours_in_view": "Hours in View",
|
||||
"total_jobs_on_board": "Jobs on Board",
|
||||
"total_jobs_in_view": "Jobs in View",
|
||||
"total_lab_in_production": "Body Hours in Production",
|
||||
"total_lab_on_board": "Body Hours on Board",
|
||||
"total_lab_in_view": "Body Hours in View",
|
||||
"total_lar_in_production": "Refinish Hours in Production",
|
||||
"total_lar_on_board": "Refinish Hours on Board",
|
||||
"total_lar_in_view": "Refinish Hours in View"
|
||||
},
|
||||
"successes": {
|
||||
"removed": "Job removed from production."
|
||||
},
|
||||
"statistics": {
|
||||
"total_hours_in_production": "Hours in Production",
|
||||
"total_lab_in_production": "Body Hours in Production",
|
||||
"total_lar_in_production": "Refinish Hours in Production",
|
||||
"total_amount_in_production": "Dollars in Production",
|
||||
"jobs_in_production": "Jobs in Production",
|
||||
"total_hours_on_board": "Hours on Board",
|
||||
"total_lab_on_board": "Body Hours on Board",
|
||||
"total_lar_on_board": "Refinish Hours on Board",
|
||||
"total_amount_on_board": "Dollars on Board",
|
||||
"total_jobs_on_board": "Jobs on Board",
|
||||
"tasks_in_production": "Tasks in Production",
|
||||
"tasks_on_board": "Tasks on Board",
|
||||
"tasks": "Tasks",
|
||||
"hours": "Hours",
|
||||
"currency_symbol": "$",
|
||||
"jobs": "Jobs"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
@@ -2927,6 +2950,8 @@
|
||||
"vendor": "Vendor"
|
||||
},
|
||||
"templates": {
|
||||
"adp_payroll_flat": "ADP Payroll - Flat Rate",
|
||||
"adp_payroll_straight": "ADP Payroll - Straight Time",
|
||||
"anticipated_revenue": "Anticipated Revenue",
|
||||
"ar_aging": "AR Aging",
|
||||
"attendance_detail": "Attendance (All Employees)",
|
||||
@@ -3418,6 +3443,18 @@
|
||||
"vehicledetail": "Vehicle Details {{vehicle}} | {{app}}",
|
||||
"vehicles": "All Vehicles | {{app}}"
|
||||
},
|
||||
"trello": {
|
||||
"labels": {
|
||||
"add_card": "Add Card",
|
||||
"add_lane": "Add Lane",
|
||||
"cancel": "Cancel",
|
||||
"delete_lane": "Delete Lane",
|
||||
"description": "Description",
|
||||
"label": "Label",
|
||||
"lane_actions": "Lane Actions",
|
||||
"title": "Title"
|
||||
}
|
||||
},
|
||||
"tt_approvals": {
|
||||
"actions": {
|
||||
"approveselected": "Approve Selected"
|
||||
@@ -3556,18 +3593,6 @@
|
||||
"validation": {
|
||||
"unique_vendor_name": "You must enter a unique vendor name."
|
||||
}
|
||||
},
|
||||
"trello": {
|
||||
"labels": {
|
||||
"add_card": "Add Card",
|
||||
"add_lane": "Add Lane",
|
||||
"delete_lane": "Delete Lane",
|
||||
"lane_actions": "Lane Actions",
|
||||
"title": "Title",
|
||||
"description": "Description",
|
||||
"label": "Label",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,9 +270,9 @@
|
||||
"testrender": ""
|
||||
},
|
||||
"errors": {
|
||||
"creatingdefaultview": "",
|
||||
"loading": "No se pueden cargar los detalles de la tienda. Por favor llame al soporte técnico.",
|
||||
"saving": "",
|
||||
"creatingdefaultview": ""
|
||||
"saving": ""
|
||||
},
|
||||
"fields": {
|
||||
"ReceivableCustomField": "",
|
||||
@@ -285,19 +285,21 @@
|
||||
},
|
||||
"appt_length": "",
|
||||
"attach_pdf_to_email": "",
|
||||
"batchid": "",
|
||||
"bill_allow_post_to_closed": "",
|
||||
"bill_federal_tax_rate": "",
|
||||
"bill_local_tax_rate": "",
|
||||
"bill_state_tax_rate": "",
|
||||
"city": "",
|
||||
"closingperiod": "",
|
||||
"companycode": "",
|
||||
"country": "",
|
||||
"dailybodytarget": "",
|
||||
"dailypainttarget": "",
|
||||
"default_adjustment_rate": "",
|
||||
"deliver": {
|
||||
"templates": "",
|
||||
"require_actual_delivery_date": ""
|
||||
"require_actual_delivery_date": "",
|
||||
"templates": ""
|
||||
},
|
||||
"dms": {
|
||||
"apcontrol": "",
|
||||
@@ -330,6 +332,10 @@
|
||||
"next_contact_hours": "",
|
||||
"templates": ""
|
||||
},
|
||||
"intellipay_config": {
|
||||
"cash_discount_percentage": "",
|
||||
"enable_cash_discount": ""
|
||||
},
|
||||
"invoice_federal_tax_rate": "",
|
||||
"invoice_local_tax_rate": "",
|
||||
"invoice_state_tax_rate": "",
|
||||
@@ -661,6 +667,8 @@
|
||||
"filehandlers": "",
|
||||
"insurancecos": "",
|
||||
"intakechecklist": "",
|
||||
"intellipay": "",
|
||||
"intellipay_cash_discount": "",
|
||||
"jobstatuses": "",
|
||||
"laborrates": "",
|
||||
"licensing": "",
|
||||
@@ -700,10 +708,10 @@
|
||||
"workingdays": ""
|
||||
},
|
||||
"successes": {
|
||||
"save": "",
|
||||
"unsavedchanges": "",
|
||||
"areyousure": "",
|
||||
"defaultviewcreated": ""
|
||||
"defaultviewcreated": "",
|
||||
"save": "",
|
||||
"unsavedchanges": ""
|
||||
},
|
||||
"validation": {
|
||||
"centermustexist": "",
|
||||
@@ -1133,8 +1141,8 @@
|
||||
},
|
||||
"general": {
|
||||
"actions": {
|
||||
"defaults": "defaults",
|
||||
"add": "",
|
||||
"autoupdate": "",
|
||||
"calculate": "",
|
||||
"cancel": "",
|
||||
"clear": "",
|
||||
@@ -1142,6 +1150,8 @@
|
||||
"copied": "",
|
||||
"copylink": "",
|
||||
"create": "",
|
||||
"defaults": "defaults",
|
||||
"delay": "",
|
||||
"delete": "Borrar",
|
||||
"deleteall": "",
|
||||
"deselectall": "",
|
||||
@@ -1153,10 +1163,12 @@
|
||||
"print": "",
|
||||
"refresh": "",
|
||||
"remove": "",
|
||||
"remove_alert": "",
|
||||
"reset": " Restablecer a original.",
|
||||
"resetpassword": "",
|
||||
"save": "Salvar",
|
||||
"saveandnew": "",
|
||||
"saveas": "",
|
||||
"selectall": "",
|
||||
"send": "",
|
||||
"sendbysms": "",
|
||||
@@ -1164,9 +1176,7 @@
|
||||
"submit": "",
|
||||
"tryagain": "",
|
||||
"view": "",
|
||||
"viewreleasenotes": "",
|
||||
"remove_alert": "",
|
||||
"saveas": ""
|
||||
"viewreleasenotes": ""
|
||||
},
|
||||
"errors": {
|
||||
"fcm": "",
|
||||
@@ -1181,7 +1191,6 @@
|
||||
"vehicle": ""
|
||||
},
|
||||
"labels": {
|
||||
"unsavedchanges": "",
|
||||
"actions": "Comportamiento",
|
||||
"areyousure": "",
|
||||
"barcode": "código de barras",
|
||||
@@ -1250,6 +1259,7 @@
|
||||
"tuesday": "",
|
||||
"tvmode": "",
|
||||
"unknown": "Desconocido",
|
||||
"unsavedchanges": "",
|
||||
"username": "",
|
||||
"view": "",
|
||||
"wednesday": "",
|
||||
@@ -1363,6 +1373,7 @@
|
||||
},
|
||||
"job_payments": {
|
||||
"buttons": {
|
||||
"create_short_link": "",
|
||||
"goback": "",
|
||||
"proceedtopayment": "",
|
||||
"refundpayment": ""
|
||||
@@ -2739,41 +2750,6 @@
|
||||
}
|
||||
},
|
||||
"production": {
|
||||
"constants": {
|
||||
"main_profile": ""
|
||||
},
|
||||
"options": {
|
||||
"small": "",
|
||||
"medium": "",
|
||||
"large": "",
|
||||
"vertical": "",
|
||||
"horizontal": ""
|
||||
},
|
||||
"settings": {
|
||||
"layout": "",
|
||||
"information": "",
|
||||
"statistics_title": "",
|
||||
"board_settings": "",
|
||||
"filters_title": "",
|
||||
"filters": {
|
||||
"md_ins_cos": "",
|
||||
"md_estimators": ""
|
||||
},
|
||||
"statistics": {
|
||||
"total_hours_in_production": "",
|
||||
"total_lab_in_production": "",
|
||||
"total_lar_in_production": "",
|
||||
"total_amount_in_production": "",
|
||||
"jobs_in_production": "",
|
||||
"total_hours_on_board": "",
|
||||
"total_lab_on_board": "",
|
||||
"total_lar_on_board": "",
|
||||
"total_amount_on_board": "",
|
||||
"total_jobs_on_board": "",
|
||||
"tasks_in_production": "",
|
||||
"tasks_on_board": ""
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"addcolumns": "",
|
||||
"bodypriority-clear": "",
|
||||
@@ -2788,29 +2764,23 @@
|
||||
"suspend": "",
|
||||
"unsuspend": ""
|
||||
},
|
||||
"constants": {
|
||||
"main_profile": ""
|
||||
},
|
||||
"errors": {
|
||||
"boardupdate": "",
|
||||
"removing": "",
|
||||
"settings": "",
|
||||
"name_exists": "",
|
||||
"name_required": ""
|
||||
"name_required": "",
|
||||
"removing": "",
|
||||
"settings": ""
|
||||
},
|
||||
"labels": {
|
||||
"kiosk_mode": "",
|
||||
"on": "",
|
||||
"off": "",
|
||||
"wide": "",
|
||||
"tall": "",
|
||||
"vertical": "",
|
||||
"horizontal": "",
|
||||
"orientation": "",
|
||||
"card_size": "",
|
||||
"model_info": "",
|
||||
"actual_in": "",
|
||||
"addnewprofile": "",
|
||||
"alert": "",
|
||||
"tasks": "",
|
||||
"alertoff": "",
|
||||
"alerton": "",
|
||||
"alerts": "",
|
||||
"ats": "",
|
||||
"bodyhours": "",
|
||||
"bodypriority": "",
|
||||
@@ -2820,6 +2790,7 @@
|
||||
"qbo_usa": ""
|
||||
}
|
||||
},
|
||||
"card_size": "",
|
||||
"cardcolor": "",
|
||||
"cardsettings": "",
|
||||
"clm_no": "",
|
||||
@@ -2828,48 +2799,100 @@
|
||||
"detailpriority": "",
|
||||
"employeeassignments": "",
|
||||
"employeesearch": "",
|
||||
"estimator": "",
|
||||
"horizontal": "",
|
||||
"ins_co_nm": "",
|
||||
"jobdetail": "",
|
||||
"kiosk_mode": "",
|
||||
"laborhrs": "",
|
||||
"legend": "",
|
||||
"model_info": "",
|
||||
"note": "",
|
||||
"off": "",
|
||||
"on": "",
|
||||
"orientation": "",
|
||||
"ownr_nm": "",
|
||||
"paintpriority": "",
|
||||
"partsstatus": "",
|
||||
"estimator": "",
|
||||
"subtotal": "",
|
||||
"production_note": "",
|
||||
"refinishhours": "",
|
||||
"scheduled_completion": "",
|
||||
"selectview": "",
|
||||
"stickyheader": "",
|
||||
"sublets": "",
|
||||
"subtotal": "",
|
||||
"tall": "",
|
||||
"tasks": "",
|
||||
"totalhours": "",
|
||||
"touchtime": "",
|
||||
"vertical": "",
|
||||
"viewname": "",
|
||||
"alerts": "",
|
||||
"addnewprofile": ""
|
||||
"wide": ""
|
||||
},
|
||||
"options": {
|
||||
"horizontal": "",
|
||||
"large": "",
|
||||
"medium": "",
|
||||
"small": "",
|
||||
"vertical": ""
|
||||
},
|
||||
"settings": {
|
||||
"board_settings": "",
|
||||
"filters": {
|
||||
"md_estimators": "",
|
||||
"md_ins_cos": ""
|
||||
},
|
||||
"filters_title": "",
|
||||
"information": "",
|
||||
"layout": "",
|
||||
"statistics": {
|
||||
"jobs_in_production": "",
|
||||
"tasks_in_production": "",
|
||||
"tasks_on_board": "",
|
||||
"tasks_in_view": "",
|
||||
"total_amount_in_production": "",
|
||||
"total_amount_on_board": "",
|
||||
"total_amount_in_view": "",
|
||||
"total_hours_in_production": "",
|
||||
"total_hours_on_board": "",
|
||||
"total_hours_in_view": "",
|
||||
"total_jobs_on_board": "",
|
||||
"total_jobs_in_view": "",
|
||||
"total_lab_in_production": "",
|
||||
"total_lab_on_board": "",
|
||||
"total_lab_in_view": "",
|
||||
"total_lar_in_production": "",
|
||||
"total_lar_on_board": "",
|
||||
"total_lar_in_view": ""
|
||||
},
|
||||
"statistics_title": ""
|
||||
},
|
||||
"statistics": {
|
||||
"currency_symbol": "",
|
||||
"hours": "",
|
||||
"jobs": "",
|
||||
"jobs_in_production": "",
|
||||
"tasks": "",
|
||||
"tasks_in_production": "",
|
||||
"tasks_on_board": "",
|
||||
"tasks_in_view": "",
|
||||
"total_amount_in_production": "",
|
||||
"total_amount_on_board": "",
|
||||
"total_amount_in_view": "",
|
||||
"total_hours_in_production": "",
|
||||
"total_hours_on_board": "",
|
||||
"total_hours_in_view": "",
|
||||
"total_jobs_on_board": "",
|
||||
"total_jobs_in_view": "",
|
||||
"total_lab_in_production": "",
|
||||
"total_lab_on_board": "",
|
||||
"total_lab_in_view": "",
|
||||
"total_lar_in_production": "",
|
||||
"total_lar_on_board": "",
|
||||
"total_lar_in_view": ""
|
||||
},
|
||||
"successes": {
|
||||
"removed": ""
|
||||
},
|
||||
"statistics": {
|
||||
"total_hours_in_production": "",
|
||||
"total_lab_in_production": "",
|
||||
"total_lar_in_production": "",
|
||||
"total_amount_in_production": "",
|
||||
"jobs_in_production": "",
|
||||
"total_hours_on_board": "",
|
||||
"total_lab_on_board": "",
|
||||
"total_lar_on_board": "",
|
||||
"total_amount_on_board": "",
|
||||
"total_jobs_on_board": "",
|
||||
"tasks_in_production": "",
|
||||
"tasks_on_board": "",
|
||||
"tasks": "",
|
||||
"hours": "",
|
||||
"currency_symbol": "",
|
||||
"jobs": ""
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
@@ -2927,6 +2950,8 @@
|
||||
"vendor": ""
|
||||
},
|
||||
"templates": {
|
||||
"adp_payroll_flat": "",
|
||||
"adp_payroll_straight": "",
|
||||
"anticipated_revenue": "",
|
||||
"ar_aging": "",
|
||||
"attendance_detail": "",
|
||||
@@ -3418,6 +3443,18 @@
|
||||
"vehicledetail": "Detalles del vehículo {{vehicle}} | {{app}}",
|
||||
"vehicles": "Todos los vehiculos | {{app}}"
|
||||
},
|
||||
"trello": {
|
||||
"labels": {
|
||||
"add_card": "",
|
||||
"add_lane": "",
|
||||
"cancel": "",
|
||||
"delete_lane": "",
|
||||
"description": "",
|
||||
"label": "",
|
||||
"lane_actions": "",
|
||||
"title": ""
|
||||
}
|
||||
},
|
||||
"tt_approvals": {
|
||||
"actions": {
|
||||
"approveselected": ""
|
||||
@@ -3556,18 +3593,6 @@
|
||||
"validation": {
|
||||
"unique_vendor_name": ""
|
||||
}
|
||||
},
|
||||
"trello": {
|
||||
"labels": {
|
||||
"add_card": "",
|
||||
"add_lane": "",
|
||||
"delete_lane": "",
|
||||
"lane_actions": "",
|
||||
"title": "",
|
||||
"description": "",
|
||||
"label": "",
|
||||
"cancel": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,9 +2,9 @@ import axios from "axios";
|
||||
import { auth } from "../firebase/firebase.utils";
|
||||
import InstanceRenderManager from "./instanceRenderMgr";
|
||||
|
||||
axios.defaults.baseURL =
|
||||
import.meta.env.VITE_APP_AXIOS_BASE_API_URL ||
|
||||
(import.meta.env.MODE === "production" ? "https://api.imex.online/" : "http://localhost:4000/");
|
||||
axios.defaults.baseURL = import.meta.env.DEV
|
||||
? "/api/"
|
||||
: import.meta.env.VITE_APP_AXIOS_BASE_API_URL || "https://api.imex.online/";
|
||||
|
||||
export const axiosAuthInterceptorId = axios.interceptors.request.use(
|
||||
async (config) => {
|
||||
|
||||
@@ -2158,6 +2158,32 @@ export const TemplateList = (type, context) => {
|
||||
field: i18n.t("tasks.fields.created_at")
|
||||
},
|
||||
group: "jobs"
|
||||
},
|
||||
adp_payroll_flat: {
|
||||
title: i18n.t("reportcenter.templates.adp_payroll_flat"),
|
||||
subject: i18n.t("reportcenter.templates.adp_payroll_flat"),
|
||||
key: "adp_payroll_flat",
|
||||
reporttype: "text",
|
||||
disabled: false,
|
||||
rangeFilter: {
|
||||
object: i18n.t("reportcenter.labels.objects.timetickets"),
|
||||
field: i18n.t("timetickets.fields.committed_at")
|
||||
},
|
||||
group: "payroll",
|
||||
adp_payroll: true
|
||||
},
|
||||
adp_payroll_straight: {
|
||||
title: i18n.t("reportcenter.templates.adp_payroll_straight"),
|
||||
subject: i18n.t("reportcenter.templates.adp_payroll_straight"),
|
||||
key: "adp_payroll_straight",
|
||||
reporttype: "text",
|
||||
disabled: false,
|
||||
rangeFilter: {
|
||||
object: i18n.t("reportcenter.labels.objects.timetickets"),
|
||||
field: i18n.t("timetickets.fields.date")
|
||||
},
|
||||
group: "payroll",
|
||||
adp_payroll: true
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Sometimes referred to as PageSize, this variable controls the amount of records
|
||||
// to show on one page during pagination.
|
||||
export const pageLimit = 50;
|
||||
export const exportPageLimit = 10;
|
||||
|
||||
84
client/src/utils/countdownHook.js
Normal file
84
client/src/utils/countdownHook.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import React from "react";
|
||||
|
||||
const useCountDown = (timeToCount = 60 * 1000, interval = 1000) => {
|
||||
const [timeLeft, setTimeLeft] = React.useState(0);
|
||||
const timer = React.useRef({});
|
||||
|
||||
const run = (ts) => {
|
||||
if (!timer.current.started) {
|
||||
timer.current.started = ts;
|
||||
timer.current.lastInterval = ts;
|
||||
}
|
||||
|
||||
const localInterval = Math.min(interval, timer.current.timeLeft || Infinity);
|
||||
if (ts - timer.current.lastInterval >= localInterval) {
|
||||
timer.current.lastInterval += localInterval;
|
||||
setTimeLeft((timeLeft) => {
|
||||
timer.current.timeLeft = timeLeft - localInterval;
|
||||
return timer.current.timeLeft;
|
||||
});
|
||||
}
|
||||
|
||||
if (ts - timer.current.started < timer.current.timeToCount) {
|
||||
timer.current.requestId = window.requestAnimationFrame(run);
|
||||
} else {
|
||||
timer.current = {};
|
||||
setTimeLeft(0);
|
||||
}
|
||||
};
|
||||
|
||||
const start = React.useCallback(
|
||||
(ttc) => {
|
||||
window.cancelAnimationFrame(timer.current.requestId);
|
||||
|
||||
const newTimeToCount = ttc !== undefined ? ttc : timeToCount;
|
||||
timer.current.started = null;
|
||||
timer.current.lastInterval = null;
|
||||
timer.current.timeToCount = newTimeToCount;
|
||||
timer.current.requestId = window.requestAnimationFrame(run);
|
||||
|
||||
setTimeLeft(newTimeToCount);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
const pause = React.useCallback(() => {
|
||||
window.cancelAnimationFrame(timer.current.requestId);
|
||||
timer.current.started = null;
|
||||
timer.current.lastInterval = null;
|
||||
timer.current.timeToCount = timer.current.timeLeft;
|
||||
}, []);
|
||||
|
||||
const resume = React.useCallback(
|
||||
() => {
|
||||
if (!timer.current.started && timer.current.timeLeft > 0) {
|
||||
window.cancelAnimationFrame(timer.current.requestId);
|
||||
timer.current.requestId = window.requestAnimationFrame(run);
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
const reset = React.useCallback(() => {
|
||||
if (timer.current.timeLeft) {
|
||||
window.cancelAnimationFrame(timer.current.requestId);
|
||||
timer.current = {};
|
||||
setTimeLeft(0);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const actions = React.useMemo(
|
||||
() => ({ start, pause, resume, reset }), // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => window.cancelAnimationFrame(timer.current.requestId);
|
||||
}, []);
|
||||
|
||||
return [timeLeft, actions];
|
||||
};
|
||||
|
||||
export default useCountDown;
|
||||
@@ -1,47 +0,0 @@
|
||||
export const BETA_KEY = "betaSwitchImex";
|
||||
|
||||
export const checkBeta = () => {
|
||||
const cookie = document.cookie.split("; ").find((row) => row.startsWith(BETA_KEY));
|
||||
return cookie ? cookie.split("=")[1] === "true" : false;
|
||||
};
|
||||
|
||||
export const setBeta = (value) => {
|
||||
const domain = window.location.hostname.split(".").slice(-2).join(".");
|
||||
document.cookie = `${BETA_KEY}=${value}; path=/; domain=.${domain}`;
|
||||
};
|
||||
|
||||
export const handleBeta = () => {
|
||||
if (window.location.hostname.startsWith("localhost")) {
|
||||
console.log("Not on beta or test, so no need to handle beta.");
|
||||
return;
|
||||
}
|
||||
|
||||
const isBeta = checkBeta();
|
||||
const currentHostName = window.location.hostname;
|
||||
|
||||
// Determine if the host name starts with "beta" or "www.beta"
|
||||
const isBetaHost = currentHostName.startsWith("beta.");
|
||||
const isBetaHostWithWWW = currentHostName.startsWith("www.beta.");
|
||||
|
||||
if (isBeta) {
|
||||
// If beta is on and we are not on a beta domain, redirect to the beta version
|
||||
if (!isBetaHost && !isBetaHostWithWWW) {
|
||||
const newHostName = currentHostName.startsWith("www.")
|
||||
? `www.beta.${currentHostName.replace(/^www\./, "")}`
|
||||
: `beta.${currentHostName}`;
|
||||
const href = `${window.location.protocol}//${newHostName}${window.location.pathname}${window.location.search}${window.location.hash}`;
|
||||
window.location.replace(href);
|
||||
}
|
||||
// Otherwise, if beta is on and we're already on a beta domain, stay there
|
||||
} else {
|
||||
// If beta is off and we are on a beta domain, redirect to the non-beta version
|
||||
if (isBetaHost || isBetaHostWithWWW) {
|
||||
const newHostName = currentHostName.replace(/^www\.beta\./, "www.").replace(/^beta\./, "");
|
||||
const href = `${window.location.protocol}//${newHostName}${window.location.pathname}${window.location.search}${window.location.hash}`;
|
||||
window.location.replace(href);
|
||||
}
|
||||
// Otherwise, if beta is off and we're not on a beta domain, stay there
|
||||
}
|
||||
};
|
||||
|
||||
export default handleBeta;
|
||||
@@ -2,5 +2,5 @@ import { store } from "../redux/store";
|
||||
|
||||
export function CreateExplorerLinkForJob({ jobid }) {
|
||||
const bodyshop = store.getState().user.bodyshop;
|
||||
return `imexmedia://${bodyshop.localmediaservernetwork}\\Jobs\\${jobid}`;
|
||||
return `imexmedia://`.concat(encodeURIComponent(`${bodyshop.localmediaservernetwork}\\Jobs\\${jobid}`));
|
||||
}
|
||||
|
||||
@@ -3,16 +3,22 @@ import { promises as fsPromises } from "fs";
|
||||
import { createRequire } from "module";
|
||||
import * as path from "path";
|
||||
import * as url from "url";
|
||||
import { defineConfig } from "vite";
|
||||
import { createLogger, defineConfig } from "vite";
|
||||
import { ViteEjsPlugin } from "vite-plugin-ejs";
|
||||
import eslint from "vite-plugin-eslint";
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
import InstanceRenderManager from "./src/utils/instanceRenderMgr";
|
||||
import chalk from "chalk";
|
||||
//import { visualizer } from "rollup-plugin-visualizer";
|
||||
|
||||
process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", {
|
||||
timeZone: "America/Los_Angeles"
|
||||
});
|
||||
|
||||
const getFormattedTimestamp = () =>
|
||||
new Date().toLocaleTimeString("en-US", { hour12: true }).replace("AM", "a.m.").replace("PM", "p.m.");
|
||||
|
||||
/** This is a hack around react-virtualized, should be removed when switching to react-virtuoso */
|
||||
const WRONG_CODE = `import { bpfrpt_proptype_WindowScroller } from "../WindowScroller.js";`;
|
||||
|
||||
function reactVirtualizedFix() {
|
||||
@@ -32,10 +38,16 @@ function reactVirtualizedFix() {
|
||||
}
|
||||
};
|
||||
}
|
||||
/** End of hack */
|
||||
|
||||
export const logger = createLogger("info", {
|
||||
allowClearScreen: false
|
||||
});
|
||||
|
||||
export default defineConfig({
|
||||
base: "/",
|
||||
plugins: [
|
||||
//visualizer(),
|
||||
ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })),
|
||||
VitePWA({
|
||||
injectRegister: "auto",
|
||||
@@ -99,7 +111,6 @@ export default defineConfig({
|
||||
reactVirtualizedFix(),
|
||||
react(),
|
||||
eslint()
|
||||
// CompressionPlugin(), //Cloudfront already compresses assets, so not needed.
|
||||
],
|
||||
define: {
|
||||
APP_VERSION: JSON.stringify(process.env.npm_package_version)
|
||||
@@ -107,7 +118,69 @@ export default defineConfig({
|
||||
server: {
|
||||
host: true,
|
||||
port: 3000,
|
||||
open: true
|
||||
open: true,
|
||||
proxy: {
|
||||
"/ws": {
|
||||
target: "ws://localhost:4000",
|
||||
rewriteWsOrigin: true,
|
||||
secure: false,
|
||||
ws: true
|
||||
},
|
||||
"/wss": {
|
||||
target: "ws://localhost:4000",
|
||||
rewriteWsOrigin: true,
|
||||
secure: false,
|
||||
ws: true
|
||||
},
|
||||
"/api": {
|
||||
target: "http://localhost:4000",
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
ws: false,
|
||||
rewrite: (path) => {
|
||||
const replacedValue = path.replace(/^\/api/, "");
|
||||
logger.info(
|
||||
`${chalk.grey.bold(getFormattedTimestamp())} ${chalk.cyan.bold("[vite]")} ${chalk.green.bold("[API]")} ${chalk.blue(replacedValue)}`
|
||||
);
|
||||
return replacedValue;
|
||||
}
|
||||
}
|
||||
},
|
||||
https: {
|
||||
key: await fsPromises.readFile("../certs/key.pem"),
|
||||
cert: await fsPromises.readFile("../certs/cert.pem"),
|
||||
allowHTTP1: false // Force HTTP/2
|
||||
}
|
||||
},
|
||||
preview: {
|
||||
port: 6000,
|
||||
host: true,
|
||||
open: true,
|
||||
https: {
|
||||
key: await fsPromises.readFile("../certs/key.pem"),
|
||||
cert: await fsPromises.readFile("../certs/cert.pem"),
|
||||
allowHTTP1: false // Force HTTP/2
|
||||
},
|
||||
proxy: {
|
||||
"/ws": {
|
||||
target: "ws://localhost:4000",
|
||||
rewriteWsOrigin: true,
|
||||
secure: false,
|
||||
ws: true
|
||||
},
|
||||
"/wss": {
|
||||
target: "ws://localhost:4000",
|
||||
rewriteWsOrigin: true,
|
||||
secure: false,
|
||||
ws: true
|
||||
},
|
||||
"/api": {
|
||||
target: "http://localhost:4000",
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
ws: false
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
@@ -115,17 +188,63 @@ export default defineConfig({
|
||||
manualChunks: {
|
||||
antd: ["antd"],
|
||||
"react-redux": ["react-redux"],
|
||||
redux: ["redux"]
|
||||
redux: ["redux"],
|
||||
lodash: ["lodash"],
|
||||
"@sentry/react": ["@sentry/react"],
|
||||
"@splitsoftware/splitio-react": ["@splitsoftware/splitio-react"],
|
||||
logrocket: ["logrocket"],
|
||||
"firebase/app": ["firebase/app"],
|
||||
"firebase/firestore": ["firebase/firestore"],
|
||||
"firebase/firestore/lite": ["firebase/firestore/lite"],
|
||||
"firebase/auth": ["firebase/auth"],
|
||||
"firebase/functions": ["firebase/functions"],
|
||||
"firebase/storage": ["firebase/storage"],
|
||||
"firebase/database": ["firebase/database"],
|
||||
"firebase/remote-config": ["firebase/remote-config"],
|
||||
"firebase/performance": ["firebase/performance"],
|
||||
"@firebase/app": ["@firebase/app"],
|
||||
"@firebase/firestore": ["@firebase/firestore"],
|
||||
"@firebase/firestore/lite": ["@firebase/firestore/lite"],
|
||||
"@firebase/auth": ["@firebase/auth"],
|
||||
"@firebase/functions": ["@firebase/functions"],
|
||||
"@firebase/storage": ["@firebase/storage"],
|
||||
"@firebase/database": ["@firebase/database"],
|
||||
"@firebase/remote-config": ["@firebase/remote-config"],
|
||||
"@firebase/performance": ["@firebase/performance"],
|
||||
markerjs2: ["markerjs2"],
|
||||
"@apollo/client": ["@apollo/client"],
|
||||
"libphonenumber-js": ["libphonenumber-js"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ["react", "react-dom", "antd", "@apollo/client", "@reduxjs/toolkit", "axios"],
|
||||
include: [
|
||||
"react",
|
||||
"react-dom",
|
||||
"antd",
|
||||
"lodash",
|
||||
"@sentry/react",
|
||||
"@apollo/client",
|
||||
"@reduxjs/toolkit",
|
||||
"axios",
|
||||
"react-router-dom",
|
||||
"dayjs",
|
||||
"redux",
|
||||
"react-redux"
|
||||
],
|
||||
esbuildOptions: {
|
||||
loader: {
|
||||
".js": "jsx"
|
||||
}
|
||||
}
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
api: "modern-compiler",
|
||||
quietDeps: true // Quite Deprecation Warnings, should be disabled occasionally before major upgrades
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
179
docker-compose.yml
Normal file
179
docker-compose.yml
Normal file
@@ -0,0 +1,179 @@
|
||||
#############################
|
||||
# Ports Exposed
|
||||
# 4000 - Imex Node API
|
||||
# 4556 - LocalStack (Local AWS)
|
||||
# 3333 - SocketIO Admin-UI (Optional)
|
||||
# 3334 - Redis-Insights (Optional)
|
||||
#############################
|
||||
|
||||
services:
|
||||
|
||||
# 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: Used to emulate AWS services locally, currently setup for SES
|
||||
# Notes: Set the ENV Debug to 1 for additional logging
|
||||
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=ses,secretsmanager
|
||||
- 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 - Used in conjunction with LocalStack to set required permission to send emails
|
||||
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/id_rsa
|
||||
"
|
||||
# Node App: The Main IMEX API
|
||||
node-app:
|
||||
build:
|
||||
context: .
|
||||
container_name: node-app
|
||||
hostname: imex-api
|
||||
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:
|
||||
- "4000:4000"
|
||||
- "9229:9229"
|
||||
volumes:
|
||||
- .:/app
|
||||
- node-app-npm-cache:/app/node_modules
|
||||
|
||||
# ## Optional Container to Observe SocketIO data
|
||||
# socketio-admin-ui:
|
||||
# image: maitrungduc1410/socket.io-admin-ui
|
||||
# container_name: socketio-admin-ui
|
||||
# networks:
|
||||
# - redis-cluster-net
|
||||
# ports:
|
||||
# - "3333:80"
|
||||
|
||||
# ##Optional Container to Observe Redis Cluster Data
|
||||
# redis-insight:
|
||||
# image: redislabs/redisinsight:latest
|
||||
# container_name: redis-insight
|
||||
# hostname: redis-insight
|
||||
# restart: unless-stopped
|
||||
# ports:
|
||||
# - "3334:5540"
|
||||
# networks:
|
||||
# - redis-cluster-net
|
||||
# volumes:
|
||||
# - redis-insight-data:/db
|
||||
|
||||
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:
|
||||
redis-insight-data:
|
||||
@@ -1,4 +1,4 @@
|
||||
Must set the environment variables using:
|
||||
|
||||
firebase functions:config:set auth.graphql_endpoint="https://db.dev.bodyshop.app/v1/graphql"
|
||||
firebase functions:config:set auth.graphql_endpoint="https://db.dev.imex.online/v1/graphql"
|
||||
auth.hasura_secret_admin_key="Dev-BodyShopApp!"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user