Compare commits
107 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 | ||
|
|
9803841617 | ||
|
|
4dffbfe6fa | ||
|
|
1b5cddd371 | ||
|
|
6a7005299a |
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",
|
"request": "launch",
|
||||||
"url": "http://localhost:3000",
|
"url": "http://localhost:3000",
|
||||||
"webRoot": "${workspaceRoot}/client/src"
|
"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
|
||||||
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-----
|
||||||
@@ -8,7 +8,7 @@ VITE_APP_CLOUDINARY_API_KEY=957865933348715
|
|||||||
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
||||||
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
|
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
|
||||||
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
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_REPORTS_SERVER_URL=https://reports3.test.imex.online
|
||||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||||
VITE_APP_INSTANCE=IMEX
|
VITE_APP_INSTANCE=IMEX
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ VITE_APP_CLOUDINARY_API_KEY=957865933348715
|
|||||||
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
||||||
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
|
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
|
||||||
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
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_REPORTS_SERVER_URL=https://reports3.test.imex.online
|
||||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||||
VITE_APP_INSTANCE=PROMANAGER
|
VITE_APP_INSTANCE=PROMANAGER
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ VITE_APP_CLOUDINARY_API_KEY=957865933348715
|
|||||||
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
||||||
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BP1B7ZTYpn-KMt6nOxlld6aS8Skt3Q7ZLEqP0hAvGHxG4UojPYiXZ6kPlzZkUC5jH-EcWXomTLtmadAIxurfcHo'
|
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BP1B7ZTYpn-KMt6nOxlld6aS8Skt3Q7ZLEqP0hAvGHxG4UojPYiXZ6kPlzZkUC5jH-EcWXomTLtmadAIxurfcHo'
|
||||||
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
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_REPORTS_SERVER_URL=https://reports3.test.imex.online
|
||||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||||
VITE_APP_COUNTRY=USA
|
VITE_APP_COUNTRY=USA
|
||||||
|
|||||||
1
client/.gitignore
vendored
1
client/.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
# Sentry Config File
|
# Sentry Config File
|
||||||
.sentryclirc
|
.sentryclirc
|
||||||
|
/dev-dist
|
||||||
|
|||||||
@@ -12,6 +12,6 @@ module.exports = defineConfig({
|
|||||||
setupNodeEvents(on, config) {
|
setupNodeEvents(on, config) {
|
||||||
return require("./cypress/plugins/index.js")(on, config);
|
return require("./cypress/plugins/index.js")(on, config);
|
||||||
},
|
},
|
||||||
baseUrl: "http://localhost:3000"
|
baseUrl: "https://localhost:3000"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<meta name="theme-color" content="#1690ff"/>
|
<meta name="theme-color" content="#1690ff"/>
|
||||||
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
|
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
|
||||||
<!-- TODO:AIo Update the individual logos for each.-->
|
<!-- 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">
|
<link rel="mask-icon" href="/mask-icon.svg" color="#FFFFFF">
|
||||||
<!--
|
<!--
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
|||||||
2123
client/package-lock.json
generated
2123
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,37 +9,37 @@
|
|||||||
"proxy": "http://localhost:4000",
|
"proxy": "http://localhost:4000",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/pro-layout": "^7.19.12",
|
"@ant-design/pro-layout": "^7.19.12",
|
||||||
"@apollo/client": "^3.11.4",
|
"@apollo/client": "^3.11.8",
|
||||||
"@emotion/is-prop-valid": "^1.3.0",
|
"@emotion/is-prop-valid": "^1.3.1",
|
||||||
"@fingerprintjs/fingerprintjs": "^4.4.3",
|
"@fingerprintjs/fingerprintjs": "^4.5.0",
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
"@reduxjs/toolkit": "^2.2.7",
|
"@reduxjs/toolkit": "^2.2.7",
|
||||||
"@sentry/cli": "^2.33.1",
|
"@sentry/cli": "^2.36.2",
|
||||||
"@sentry/react": "^7.114.0",
|
"@sentry/react": "^7.114.0",
|
||||||
"@splitsoftware/splitio-react": "^1.12.1",
|
"@splitsoftware/splitio-react": "^1.13.0",
|
||||||
"@tanem/react-nprogress": "^5.0.51",
|
"@tanem/react-nprogress": "^5.0.51",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"antd": "^5.20.1",
|
"antd": "^5.20.1",
|
||||||
"apollo-link-logger": "^2.0.1",
|
"apollo-link-logger": "^2.0.1",
|
||||||
"apollo-link-sentry": "^3.3.0",
|
"apollo-link-sentry": "^3.3.0",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"axios": "^1.7.4",
|
"axios": "^1.7.7",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"css-box-model": "^1.2.1",
|
"css-box-model": "^1.2.1",
|
||||||
"dayjs": "^1.11.12",
|
"dayjs": "^1.11.13",
|
||||||
"dayjs-business-days2": "^1.2.2",
|
"dayjs-business-days2": "^1.2.2",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"firebase": "^10.12.5",
|
"firebase": "^10.13.2",
|
||||||
"graphql": "^16.9.0",
|
"graphql": "^16.9.0",
|
||||||
"i18next": "^23.12.3",
|
"i18next": "^23.15.1",
|
||||||
"i18next-browser-languagedetector": "^8.0.0",
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
"immutability-helper": "^3.1.1",
|
"immutability-helper": "^3.1.1",
|
||||||
"libphonenumber-js": "^1.11.5",
|
"libphonenumber-js": "^1.11.9",
|
||||||
"logrocket": "^8.1.2",
|
"logrocket": "^8.1.2",
|
||||||
"markerjs2": "^2.32.1",
|
"markerjs2": "^2.32.2",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
"normalize-url": "^8.0.1",
|
"normalize-url": "^8.0.1",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
"query-string": "^9.1.0",
|
"query-string": "^9.1.0",
|
||||||
"raf-schd": "^4.0.3",
|
"raf-schd": "^4.0.3",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-big-calendar": "^1.13.2",
|
"react-big-calendar": "^1.14.1",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-cookie": "^7.2.0",
|
"react-cookie": "^7.2.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
@@ -58,15 +58,15 @@
|
|||||||
"react-icons": "^5.3.0",
|
"react-icons": "^5.3.0",
|
||||||
"react-image-lightbox": "^5.1.4",
|
"react-image-lightbox": "^5.1.4",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"react-number-format": "^5.4.0",
|
"react-number-format": "^5.4.2",
|
||||||
"react-popopo": "^2.1.9",
|
"react-popopo": "^2.1.9",
|
||||||
"react-product-fruits": "^2.2.6",
|
"react-product-fruits": "^2.2.61",
|
||||||
"react-redux": "^9.1.2",
|
"react-redux": "^9.1.2",
|
||||||
"react-resizable": "^3.0.5",
|
"react-resizable": "^3.0.5",
|
||||||
"react-router-dom": "^6.26.0",
|
"react-router-dom": "^6.26.2",
|
||||||
"react-sticky": "^6.0.3",
|
"react-sticky": "^6.0.3",
|
||||||
"react-virtualized": "^9.22.5",
|
"react-virtualized": "^9.22.5",
|
||||||
"react-virtuoso": "^4.10.1",
|
"react-virtuoso": "^4.10.4",
|
||||||
"recharts": "^2.12.7",
|
"recharts": "^2.12.7",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"redux-actions": "^3.0.3",
|
"redux-actions": "^3.0.3",
|
||||||
@@ -74,12 +74,12 @@
|
|||||||
"redux-saga": "^1.3.0",
|
"redux-saga": "^1.3.0",
|
||||||
"redux-state-sync": "^3.1.4",
|
"redux-state-sync": "^3.1.4",
|
||||||
"reselect": "^5.1.1",
|
"reselect": "^5.1.1",
|
||||||
"sass": "^1.77.8",
|
"sass": "^1.79.3",
|
||||||
"socket.io-client": "^4.7.5",
|
"socket.io-client": "^4.8.0",
|
||||||
"styled-components": "^6.1.12",
|
"styled-components": "^6.1.13",
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
"subscriptions-transport-ws": "^0.11.0",
|
||||||
"use-memo-one": "^1.1.3",
|
"use-memo-one": "^1.1.3",
|
||||||
"userpilot": "^1.3.5",
|
"userpilot": "^1.3.6",
|
||||||
"vite-plugin-ejs": "^1.7.0",
|
"vite-plugin-ejs": "^1.7.0",
|
||||||
"web-vitals": "^3.5.2"
|
"web-vitals": "^3.5.2"
|
||||||
},
|
},
|
||||||
@@ -91,6 +91,9 @@
|
|||||||
"start:imex": "dotenvx run --env-file=.env.development.imex -- vite",
|
"start:imex": "dotenvx run --env-file=.env.development.imex -- vite",
|
||||||
"start:rome": "dotenvx run --env-file=.env.development.rome -- vite",
|
"start:rome": "dotenvx run --env-file=.env.development.rome -- vite",
|
||||||
"start:promanager": "dotenvx run --env-file=.env.development.promanager -- 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:imex": "env-cmd -f .env.test.imex npm run build",
|
||||||
"build:test:rome": "env-cmd -f .env.test.rome 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",
|
"build:test:promanager": "env-cmd -f .env.test.promanager npm run build",
|
||||||
@@ -131,29 +134,29 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-react": "^7.24.7",
|
"@babel/preset-react": "^7.24.7",
|
||||||
"@dotenvx/dotenvx": "^1.7.0",
|
"@dotenvx/dotenvx": "^1.14.1",
|
||||||
"@emotion/babel-plugin": "^11.12.0",
|
"@emotion/babel-plugin": "^11.12.0",
|
||||||
"@emotion/react": "^11.13.0",
|
"@emotion/react": "^11.13.3",
|
||||||
"@sentry/webpack-plugin": "^2.22.2",
|
"@sentry/webpack-plugin": "^2.22.4",
|
||||||
"@testing-library/cypress": "^10.0.2",
|
"@testing-library/cypress": "^10.0.2",
|
||||||
"browserslist": "^4.23.3",
|
"browserslist": "^4.23.3",
|
||||||
"browserslist-to-esbuild": "^2.1.1",
|
"browserslist-to-esbuild": "^2.1.1",
|
||||||
|
"chalk": "^5.3.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cypress": "^13.13.3",
|
"cypress": "^13.14.2",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"eslint-plugin-cypress": "^2.15.1",
|
"eslint-plugin-cypress": "^2.15.1",
|
||||||
"memfs": "^4.11.1",
|
"memfs": "^4.12.0",
|
||||||
"os-browserify": "^0.3.0",
|
"os-browserify": "^0.3.0",
|
||||||
"react-error-overlay": "6.0.11",
|
"react-error-overlay": "6.0.11",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"source-map-explorer": "^2.5.3",
|
"source-map-explorer": "^2.5.3",
|
||||||
"vite": "^5.4.0",
|
"vite": "^5.4.7",
|
||||||
"vite-plugin-babel": "^1.2.0",
|
"vite-plugin-babel": "^1.2.0",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-legacy": "^2.1.0",
|
|
||||||
"vite-plugin-node-polyfills": "^0.22.0",
|
"vite-plugin-node-polyfills": "^0.22.0",
|
||||||
"vite-plugin-pwa": "^0.20.1",
|
"vite-plugin-pwa": "^0.20.5",
|
||||||
"vite-plugin-style-import": "^2.0.0",
|
"vite-plugin-style-import": "^2.0.0",
|
||||||
"workbox-window": "^7.1.0"
|
"workbox-window": "^7.1.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import "./App.styles.scss";
|
|||||||
import Eula from "../components/eula/eula.component";
|
import Eula from "../components/eula/eula.component";
|
||||||
import InstanceRenderMgr from "../utils/instanceRenderMgr";
|
import InstanceRenderMgr from "../utils/instanceRenderMgr";
|
||||||
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
|
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
|
||||||
|
import { SocketProvider } from "../contexts/SocketIO/socketContext.jsx";
|
||||||
|
|
||||||
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
|
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
|
||||||
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
|
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
|
||||||
@@ -201,7 +202,9 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
|||||||
path="/manage/*"
|
path="/manage/*"
|
||||||
element={
|
element={
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
<SocketProvider bodyshop={bodyshop}>
|
||||||
|
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||||
|
</SocketProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -211,7 +214,9 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
|||||||
path="/tech/*"
|
path="/tech/*"
|
||||||
element={
|
element={
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
<SocketProvider bodyshop={bodyshop}>
|
||||||
|
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||||
|
</SocketProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import { pageLimit } from "../../utils/config";
|
import { exportPageLimit } from "../../utils/config";
|
||||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||||
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.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
|
<Table
|
||||||
loading={loading}
|
loading={loading}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
pagination={{ position: "top", pageSize: pageLimit }}
|
pagination={{ position: "top", pageSize: exportPageLimit }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
import { pageLimit } from "../../utils/config";
|
import { exportPageLimit } from "../../utils/config";
|
||||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||||
@@ -177,7 +177,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, payments,
|
|||||||
<Table
|
<Table
|
||||||
loading={loading}
|
loading={loading}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
pagination={{ position: "top", pageSize: pageLimit }}
|
pagination={{ position: "top", pageSize: exportPageLimit }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { Button, Card, Input, Space, Table } from "antd";
|
import { Button, Card, Input, Space, Table } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import { exportPageLimit } from "../../utils/config";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
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 JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
||||||
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-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 JobMarkSelectedExported from "../jobs-mark-selected-exported/jobs-mark-selected-exported";
|
||||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||||
@@ -201,7 +201,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
|
|||||||
<Table
|
<Table
|
||||||
loading={loading}
|
loading={loading}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
pagination={{ position: "top" }}
|
pagination={{ position: "top", pageSize: exportPageLimit }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
|
|||||||
@@ -3,13 +3,15 @@ import axios from "axios";
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||||
|
|
||||||
export default function GlobalSearchOs() {
|
export default function GlobalSearchOs() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [data, setData] = useState(false);
|
const [data, setData] = useState(false);
|
||||||
|
|
||||||
@@ -177,7 +179,18 @@ export default function GlobalSearchOs() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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
|
<Input.Search
|
||||||
size="large"
|
size="large"
|
||||||
placeholder={t("general.labels.globalsearch")}
|
placeholder={t("general.labels.globalsearch")}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { AutoComplete, Divider, Input, Space } from "antd";
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries";
|
||||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
@@ -13,6 +13,7 @@ import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.compon
|
|||||||
export default function GlobalSearch() {
|
export default function GlobalSearch() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [callSearch, { loading, error, data }] = useLazyQuery(GLOBAL_SEARCH_QUERY);
|
const [callSearch, { loading, error, data }] = useLazyQuery(GLOBAL_SEARCH_QUERY);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const executeSearch = (v) => {
|
const executeSearch = (v) => {
|
||||||
if (v && v.variables.search && v.variables.search !== "" && v.variables.search.length >= 3) callSearch(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 debouncedExecuteSearch = _.debounce(executeSearch, 750);
|
||||||
|
|
||||||
const handleSearch = (value) => {
|
const handleSearch = (value) => {
|
||||||
console.log("Handle Search");
|
|
||||||
debouncedExecuteSearch({ variables: { search: value } });
|
debouncedExecuteSearch({ variables: { search: value } });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -156,7 +156,17 @@ export default function GlobalSearch() {
|
|||||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
|
|
||||||
return (
|
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
|
<Input.Search
|
||||||
size="large"
|
size="large"
|
||||||
placeholder={t("general.labels.globalsearch")}
|
placeholder={t("general.labels.globalsearch")}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { notification } from "antd";
|
import { notification } from "antd";
|
||||||
|
import Axios from "axios";
|
||||||
|
import Dinero from "dinero.js";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -7,13 +10,10 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import { INSERT_NEW_JOB_LINE, UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
import { INSERT_NEW_JOB_LINE, UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
||||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
import { selectJobLineEditModal } from "../../redux/modals/modals.selectors";
|
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 UndefinedToNull from "../../utils/undefinedtonull";
|
||||||
import JobLinesUpdsertModal from "./job-lines-upsert-modal.component";
|
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({
|
const mapStateToProps = createStructuredSelector({
|
||||||
jobLineEditModal: selectJobLineEditModal,
|
jobLineEditModal: selectJobLineEditModal,
|
||||||
@@ -82,13 +82,15 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo
|
|||||||
variables: {
|
variables: {
|
||||||
lineId: jobLineEditModal.context.id,
|
lineId: jobLineEditModal.context.id,
|
||||||
line: {
|
line: {
|
||||||
...values,
|
...UndefinedToNull({
|
||||||
prt_dsmk_m: Dinero({
|
...values,
|
||||||
amount: Math.round(values.act_price * 100)
|
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"]
|
refetchQueries: ["GET_LINE_TICKET_BY_PK"]
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ export function JobsExportAllButton({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
|
<Button onClick={handleQbxml} loading={loading} disabled={disabled || jobIds?.length > 10}>
|
||||||
{t("jobs.actions.exportselected")}
|
{t("jobs.actions.exportselected")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -242,7 +242,8 @@ export function PartsOrderListTableComponent({
|
|||||||
title: t("general.labels.actions"),
|
title: t("general.labels.actions"),
|
||||||
dataIndex: "actions",
|
dataIndex: "actions",
|
||||||
key: "actions",
|
key: "actions",
|
||||||
render: (text, record) => recordActions(record, true)
|
render: (text, record) => recordActions(record, true),
|
||||||
|
id: "parts-order-list-table-actions"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ export function PayableExportAll({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
|
<Button onClick={handleQbxml} loading={loading} disabled={disabled || billids?.length > 10}>
|
||||||
{t("jobs.actions.exportselected")}
|
{t("jobs.actions.exportselected")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ export function PaymentsExportAllButton({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
|
<Button onClick={handleQbxml} loading={loading} disabled={disabled || paymentIds?.length > 10}>
|
||||||
{t("jobs.actions.exportselected")}
|
{t("jobs.actions.exportselected")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
|
|||||||
const cardSettings = useMemo(() => {
|
const cardSettings = useMemo(() => {
|
||||||
const kanbanSettings = associationSettings?.kanban_settings;
|
const kanbanSettings = associationSettings?.kanban_settings;
|
||||||
return mergeWithDefaults(kanbanSettings);
|
return mergeWithDefaults(kanbanSettings);
|
||||||
}, [associationSettings]);
|
}, [associationSettings?.kanban_settings]);
|
||||||
|
|
||||||
const handleSettingsChange = () => {
|
const handleSettingsChange = () => {
|
||||||
setFilter(defaultFilters);
|
setFilter(defaultFilters);
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useEffect, useMemo, useRef } from "react";
|
import React, { useContext, useEffect, useMemo, useRef } from "react";
|
||||||
import { useQuery, useSubscription } from "@apollo/client";
|
import { useApolloClient, useQuery, useSubscription } from "@apollo/client";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {
|
import {
|
||||||
|
QUERY_EXACT_JOB_IN_PRODUCTION,
|
||||||
QUERY_JOBS_IN_PRODUCTION,
|
QUERY_JOBS_IN_PRODUCTION,
|
||||||
SUBSCRIPTION_JOBS_IN_PRODUCTION,
|
SUBSCRIPTION_JOBS_IN_PRODUCTION,
|
||||||
SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW
|
SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW
|
||||||
@@ -10,6 +11,8 @@ import {
|
|||||||
import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
|
import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import ProductionBoardKanbanComponent from "./production-board-kanban.component";
|
import ProductionBoardKanbanComponent from "./production-board-kanban.component";
|
||||||
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -17,7 +20,20 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionType = "direct" }) {
|
function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionType = "direct" }) {
|
||||||
const fired = useRef(false); // useRef to keep track of whether the subscription fired
|
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(
|
const combinedStatuses = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -34,9 +50,12 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionTyp
|
|||||||
onError: (error) => console.error(`Error fetching jobs in production: ${error.message}`)
|
onError: (error) => console.error(`Error fetching jobs in production: ${error.message}`)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const subscriptionEnabled = Websocket_Production?.treatment === "off";
|
||||||
|
|
||||||
const { data: updatedJobs } = useSubscription(
|
const { data: updatedJobs } = useSubscription(
|
||||||
subscriptionType === "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION,
|
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}`)
|
onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -46,22 +65,112 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionTyp
|
|||||||
onError: (error) => console.error(`Error fetching Kanban settings: ${error.message}`)
|
onError: (error) => console.error(`Error fetching Kanban settings: ${error.message}`)
|
||||||
});
|
});
|
||||||
|
|
||||||
// const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!updatedJobs) {
|
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, refetch, subscriptionEnabled]);
|
||||||
|
|
||||||
|
// Socket.IO implementation for users with Split treatment "off"
|
||||||
|
useEffect(() => {
|
||||||
|
if (subscriptionEnabled || !socket || !bodyshop || !bodyshop.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!fired.current) {
|
|
||||||
fired.current = true;
|
const handleJobUpdates = async (jobChangedData) => {
|
||||||
return;
|
const jobId = jobChangedData.id;
|
||||||
}
|
|
||||||
refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`));
|
// Access the existing cache for QUERY_JOBS_IN_PRODUCTION
|
||||||
}, [updatedJobs, refetch]);
|
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(() => {
|
const filteredAssociationSettings = useMemo(() => {
|
||||||
return associationSettings?.associations[0] || null;
|
return associationSettings?.associations[0] || null;
|
||||||
}, [associationSettings]);
|
}, [associationSettings?.associations]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProductionBoardKanbanComponent
|
<ProductionBoardKanbanComponent
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
border-radius: 5px 5px 0 0;
|
border-radius: 5px 5px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.production-alert {
|
.production-alert {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
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 = [
|
const statisticsItems = [
|
||||||
{ id: 0, name: "totalHrs", label: "total_hours_in_production" },
|
{ id: 0, name: "totalHrs", label: "total_hours_in_production" },
|
||||||
{ id: 1, name: "totalAmountInProduction", label: "total_amount_in_production" },
|
{ id: 1, name: "totalAmountInProduction", label: "total_amount_in_production" },
|
||||||
{ id: 2, name: "totalLAB", label: "total_lab_in_production" },
|
{ id: 2, name: "totalLAB", label: "total_lab_in_production" },
|
||||||
{ id: 3, name: "totalLAR", label: "total_lar_in_production" },
|
{ id: 3, name: "totalLAR", label: "total_lar_in_production" },
|
||||||
{ id: 4, name: "jobsInProduction", label: "jobs_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: 5,
|
||||||
{ id: 8, name: "totalLAROnBoard", label: "total_lar_on_board" },
|
name: "totalHrsOnBoard",
|
||||||
{ id: 9, name: "jobsOnBoard", label: "total_jobs_on_board" },
|
label: InstanceRenderManager({
|
||||||
{ id: 10, name: "tasksOnBoard", label: "tasks_on_board" },
|
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" }
|
{ id: 11, name: "tasksInProduction", label: "tasks_in_production" }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ function getFurthestAway({ pageBorderBox, draggable, candidates }) {
|
|||||||
const axis = candidate.axis;
|
const axis = candidate.axis;
|
||||||
const target = patch(
|
const target = patch(
|
||||||
candidate.axis.line,
|
candidate.axis.line,
|
||||||
// use the current center of the dragging item on the main axis
|
// use the center of the list on the main axis
|
||||||
pageBorderBox.center[axis.line],
|
candidate.page.borderBox.center[axis.line],
|
||||||
// use the center of the list on the cross axis
|
// use the center of the list on the cross axis
|
||||||
candidate.page.borderBox.center[axis.crossAxisLine]
|
candidate.page.borderBox.center[axis.crossAxisLine]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import getBodyElement from "../get-body-element";
|
|||||||
const isEqual = (base) => (value) => base === value;
|
const isEqual = (base) => (value) => base === value;
|
||||||
const isScroll = isEqual("scroll");
|
const isScroll = isEqual("scroll");
|
||||||
const isAuto = isEqual("auto");
|
const isAuto = isEqual("auto");
|
||||||
|
const isOverlay = isEqual("overlay");
|
||||||
const isVisible = isEqual("visible");
|
const isVisible = isEqual("visible");
|
||||||
const isEither = (overflow, fn) => fn(overflow.overflowX) || fn(overflow.overflowY);
|
const isEither = (overflow, fn) => fn(overflow.overflowX) || fn(overflow.overflowY);
|
||||||
const isBoth = (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,
|
overflowX: style.overflowX,
|
||||||
overflowY: style.overflowY
|
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
|
// Special case for a body element
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ function getSelector(contextId) {
|
|||||||
return `[${attributes.dragHandle.contextId}="${contextId}"]`;
|
return `[${attributes.dragHandle.contextId}="${contextId}"]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findClosestDragHandleFromEvent(contextId, event) {
|
export function findClosestDragHandleFromEvent(contextId, event) {
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
if (!isElement(target)) {
|
if (!isElement(target)) {
|
||||||
warning("event.target must be a Element");
|
warning("event.target must be a Element");
|
||||||
|
|||||||
@@ -240,11 +240,14 @@ export default function useTouchSensor(api) {
|
|||||||
y: clientY
|
y: clientY
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handle = api.findClosestDragHandle(event);
|
||||||
|
invariant(handle, "Touch sensor unable to find drag handle");
|
||||||
|
|
||||||
// unbind this event handler
|
// unbind this event handler
|
||||||
unbindEventsRef.current();
|
unbindEventsRef.current();
|
||||||
|
|
||||||
// eslint-disable-next-line no-use-before-define
|
// 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
|
// not including stop or startPendingDrag as it is not defined initially
|
||||||
@@ -288,7 +291,7 @@ export default function useTouchSensor(api) {
|
|||||||
}
|
}
|
||||||
}, [stop]);
|
}, [stop]);
|
||||||
const bindCapturingEvents = useCallback(
|
const bindCapturingEvents = useCallback(
|
||||||
function bindCapturingEvents() {
|
function bindCapturingEvents(target) {
|
||||||
const options = {
|
const options = {
|
||||||
capture: true,
|
capture: true,
|
||||||
passive: false
|
passive: false
|
||||||
@@ -307,7 +310,7 @@ export default function useTouchSensor(api) {
|
|||||||
// Old behaviour:
|
// Old behaviour:
|
||||||
// https://gist.github.com/parris/dda613e3ae78f14eb2dc9fa0f4bfce3d
|
// https://gist.github.com/parris/dda613e3ae78f14eb2dc9fa0f4bfce3d
|
||||||
// https://stackoverflow.com/questions/33298828/touch-move-event-dont-fire-after-touch-start-target-is-removed
|
// 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);
|
const unbindWindow = bindEvents(window, getWindowBindings(args), options);
|
||||||
unbindEventsRef.current = function unbindAll() {
|
unbindEventsRef.current = function unbindAll() {
|
||||||
unbindTarget();
|
unbindTarget();
|
||||||
@@ -330,7 +333,7 @@ export default function useTouchSensor(api) {
|
|||||||
[getPhase, setPhase]
|
[getPhase, setPhase]
|
||||||
);
|
);
|
||||||
const startPendingDrag = useCallback(
|
const startPendingDrag = useCallback(
|
||||||
function startPendingDrag(actions, point) {
|
function startPendingDrag(actions, point, target) {
|
||||||
invariant(getPhase().type === "IDLE", "Expected to move from IDLE to PENDING drag");
|
invariant(getPhase().type === "IDLE", "Expected to move from IDLE to PENDING drag");
|
||||||
const longPressTimerId = setTimeout(startDragging, timeForLongPress);
|
const longPressTimerId = setTimeout(startDragging, timeForLongPress);
|
||||||
setPhase({
|
setPhase({
|
||||||
@@ -339,7 +342,7 @@ export default function useTouchSensor(api) {
|
|||||||
actions,
|
actions,
|
||||||
longPressTimerId
|
longPressTimerId
|
||||||
});
|
});
|
||||||
bindCapturingEvents();
|
bindCapturingEvents(target);
|
||||||
},
|
},
|
||||||
[bindCapturingEvents, getPhase, setPhase, startDragging]
|
[bindCapturingEvents, getPhase, setPhase, startDragging]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ import getBorderBoxCenterPosition from "../get-border-box-center-position";
|
|||||||
import { warning } from "../../dev-warning";
|
import { warning } from "../../dev-warning";
|
||||||
import useLayoutEffect from "../use-isomorphic-layout-effect";
|
import useLayoutEffect from "../use-isomorphic-layout-effect";
|
||||||
import { noop } from "../../empty";
|
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 findDraggable from "../get-elements/find-draggable";
|
||||||
import bindEvents from "../event-bindings/bind-events";
|
import bindEvents from "../event-bindings/bind-events";
|
||||||
|
|
||||||
@@ -339,6 +341,9 @@ export default function useSensorMarshal({ contextId, store, registry, customSen
|
|||||||
}),
|
}),
|
||||||
[contextId, lockAPI, registry, store]
|
[contextId, lockAPI, registry, store]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const findClosestDragHandle = useCallback((event) => findClosestDragHandleFromEvent(contextId, event), [contextId]);
|
||||||
|
|
||||||
const findClosestDraggableId = useCallback((event) => findClosestDraggableIdFromEvent(contextId, event), [contextId]);
|
const findClosestDraggableId = useCallback((event) => findClosestDraggableIdFromEvent(contextId, event), [contextId]);
|
||||||
const findOptionsForDraggable = useCallback(
|
const findOptionsForDraggable = useCallback(
|
||||||
(id) => {
|
(id) => {
|
||||||
@@ -370,9 +375,18 @@ export default function useSensorMarshal({ contextId, store, registry, customSen
|
|||||||
findClosestDraggableId,
|
findClosestDraggableId,
|
||||||
findOptionsForDraggable,
|
findOptionsForDraggable,
|
||||||
tryReleaseLock,
|
tryReleaseLock,
|
||||||
isLockClaimed
|
isLockClaimed,
|
||||||
|
findClosestDragHandle
|
||||||
}),
|
}),
|
||||||
[canGetLock, tryGetLock, findClosestDraggableId, findOptionsForDraggable, tryReleaseLock, isLockClaimed]
|
[
|
||||||
|
canGetLock,
|
||||||
|
tryGetLock,
|
||||||
|
findClosestDraggableId,
|
||||||
|
findOptionsForDraggable,
|
||||||
|
tryReleaseLock,
|
||||||
|
isLockClaimed,
|
||||||
|
findClosestDragHandle
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Bad ass
|
// Bad ass
|
||||||
|
|||||||
@@ -83,7 +83,13 @@ const getFinalStyles = (contextId) => {
|
|||||||
return {
|
return {
|
||||||
selector: getSelector(attributes.draggable.contextId),
|
selector: getSelector(attributes.draggable.contextId),
|
||||||
styles: {
|
styles: {
|
||||||
dragging: transition,
|
dragging: `
|
||||||
|
${transition}
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
`,
|
||||||
dropAnimating: transition,
|
dropAnimating: transition,
|
||||||
userCancel: transition
|
userCancel: transition
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,9 @@ export default function useStyleMarshal(contextId, nonce) {
|
|||||||
const remove = (ref) => {
|
const remove = (ref) => {
|
||||||
const current = ref.current;
|
const current = ref.current;
|
||||||
invariant(current, "Cannot unmount ref as it is not set");
|
invariant(current, "Cannot unmount ref as it is not set");
|
||||||
getHead().removeChild(current);
|
if (getHead().contains(current)) {
|
||||||
|
getHead().removeChild(current);
|
||||||
|
}
|
||||||
ref.current = null;
|
ref.current = null;
|
||||||
};
|
};
|
||||||
remove(alwaysRef);
|
remove(alwaysRef);
|
||||||
|
|||||||
@@ -21,25 +21,26 @@ export function ProductionListColumnStatus({ record, bodyshop, insertAuditTrail
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const handleSetStatus = async (e) => {
|
const handleSetStatus = async (e) => {
|
||||||
logImEXEvent("production_change_status");
|
if (bodyshop.md_ro_statuses.production_statuses.includes(record.status) && !bodyshop.md_ro_statuses.post_production_statuses.includes(record.status)) {
|
||||||
// e.stopPropagation();
|
logImEXEvent("production_change_status");
|
||||||
setLoading(true);
|
// e.stopPropagation();
|
||||||
const { key } = e;
|
setLoading(true);
|
||||||
await updateJob({
|
const { key } = e;
|
||||||
variables: {
|
await updateJob({
|
||||||
jobId: record.id,
|
variables: {
|
||||||
job: {
|
jobId: record.id,
|
||||||
status: key
|
job: {
|
||||||
|
status: key
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
insertAuditTrail({
|
||||||
insertAuditTrail({
|
jobid: record.id,
|
||||||
jobid: record.id,
|
operation: AuditTrailMapping.jobstatuschange(key),
|
||||||
operation: AuditTrailMapping.jobstatuschange(key),
|
type: "jobstatuschange"
|
||||||
type: "jobstatuschange"
|
});
|
||||||
});
|
setLoading(false);
|
||||||
|
}
|
||||||
setLoading(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const menu = {
|
const menu = {
|
||||||
|
|||||||
@@ -457,41 +457,42 @@ export function ProductionListConfigManager({
|
|||||||
value={activeView}
|
value={activeView}
|
||||||
disabled={open || isAddingNewProfile} // Disable the Select box when the popover is open or adding a new profile
|
disabled={open || isAddingNewProfile} // Disable the Select box when the popover is open or adding a new profile
|
||||||
>
|
>
|
||||||
{bodyshop.production_config
|
{bodyshop?.production_config &&
|
||||||
.slice()
|
bodyshop.production_config
|
||||||
.sort((a, b) =>
|
.slice()
|
||||||
a.name === t("production.constants.main_profile")
|
.sort((a, b) =>
|
||||||
? -1
|
a.name === t("production.constants.main_profile")
|
||||||
: b.name === t("production.constants.main_profile")
|
? -1
|
||||||
? 1
|
: b.name === t("production.constants.main_profile")
|
||||||
: 0
|
? 1
|
||||||
) //
|
: 0
|
||||||
.map((config) => (
|
) //
|
||||||
<Select.Option key={config.name} label={config.name}>
|
.map((config) => (
|
||||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
<Select.Option key={config.name} label={config.name}>
|
||||||
<span
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||||
style={{
|
<span
|
||||||
flex: 1,
|
style={{
|
||||||
maxWidth: "80%",
|
flex: 1,
|
||||||
marginRight: "1rem",
|
maxWidth: "80%",
|
||||||
textOverflow: "ellipsis"
|
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()}
|
|
||||||
>
|
>
|
||||||
<DeleteOutlined onClick={(e) => e.stopPropagation()} />
|
{config.name}
|
||||||
</Popconfirm>
|
</span>
|
||||||
)}
|
{config.name !== t("production.constants.main_profile") && (
|
||||||
</div>
|
<Popconfirm
|
||||||
</Select.Option>
|
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")}>
|
<Select.Option key="add_new" label={t("production.labels.addnewprofile")}>
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
<PlusOutlined style={{ marginRight: "0.5rem" }} />
|
<PlusOutlined style={{ marginRight: "0.5rem" }} />
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
|||||||
|
|
||||||
const initialColumnsRef = useRef(
|
const initialColumnsRef = useRef(
|
||||||
(initialStateRef.current &&
|
(initialStateRef.current &&
|
||||||
bodyshop.production_config
|
bodyshop?.production_config
|
||||||
.find((p) => p.name === defaultView)
|
?.find((p) => p.name === defaultView)
|
||||||
?.columns.columnKeys.map((k) => {
|
?.columns.columnKeys.map((k) => {
|
||||||
return {
|
return {
|
||||||
...ProductionListColumns({
|
...ProductionListColumns({
|
||||||
@@ -76,8 +76,8 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const matchingColumnConfig = useMemo(() => {
|
const matchingColumnConfig = useMemo(() => {
|
||||||
return bodyshop.production_config.find((p) => p.name === defaultView);
|
return bodyshop?.production_config?.find((p) => p.name === defaultView);
|
||||||
}, [bodyshop.production_config, defaultView]);
|
}, [bodyshop.production_config]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newColumns =
|
const newColumns =
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useApolloClient, useQuery, useSubscription } from "@apollo/client";
|
import { useApolloClient, useQuery, useSubscription } from "@apollo/client";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useContext, useEffect, useState, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
QUERY_EXACT_JOB_IN_PRODUCTION,
|
QUERY_EXACT_JOB_IN_PRODUCTION,
|
||||||
QUERY_EXACT_JOBS_IN_PRODUCTION,
|
QUERY_EXACT_JOBS_IN_PRODUCTION,
|
||||||
@@ -9,19 +9,46 @@ import {
|
|||||||
} from "../../graphql/jobs.queries";
|
} from "../../graphql/jobs.queries";
|
||||||
import ProductionListTable from "./production-list-table.component";
|
import ProductionListTable from "./production-list-table.component";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||||
|
|
||||||
export default function ProductionListTableContainer({ subscriptionType = "direct" }) {
|
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, {
|
const { refetch, loading, data } = useQuery(QUERY_JOBS_IN_PRODUCTION, {
|
||||||
pollInterval: 3600000,
|
pollInterval: 3600000,
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only"
|
nextFetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
const client = useApolloClient();
|
|
||||||
const [joblist, setJoblist] = useState([]);
|
// Use GraphQL subscription when subscription is enabled
|
||||||
const { data: updatedJobs } = useSubscription(
|
const { data: updatedJobs } = useSubscription(
|
||||||
subscriptionType === "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION
|
subscriptionType === "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION,
|
||||||
|
{
|
||||||
|
skip: !subscriptionEnabled
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Update joblist when data changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!(data && data.jobs)) return;
|
if (!(data && data.jobs)) return;
|
||||||
setJoblist(
|
setJoblist(
|
||||||
@@ -31,34 +58,134 @@ export default function ProductionListTableContainer({ subscriptionType = "direc
|
|||||||
);
|
);
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
|
// Handle updates from GraphQL subscription
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!updatedJobs || joblist.length === 0) return;
|
if (subscriptionEnabled) {
|
||||||
|
if (!updatedJobs || joblist.length === 0) return;
|
||||||
|
|
||||||
const jobDiff = _.differenceWith(
|
const jobDiff = _.differenceWith(
|
||||||
joblist,
|
joblist,
|
||||||
updatedJobs.jobs,
|
updatedJobs.jobs,
|
||||||
(a, b) => a.id === b.id && a.updated_at === b.updated_at
|
(a, b) => a.id === b.id && a.updated_at === b.updated_at
|
||||||
);
|
);
|
||||||
|
|
||||||
if (jobDiff.length > 1) {
|
if (jobDiff.length > 1) {
|
||||||
getUpdatedJobsData(jobDiff.map((j) => j.id));
|
getUpdatedJobsData(jobDiff.map((j) => j.id));
|
||||||
} else if (jobDiff.length === 1) {
|
} else if (jobDiff.length === 1) {
|
||||||
jobDiff.forEach((job) => {
|
jobDiff.forEach((job) => {
|
||||||
getUpdatedJobData(job.id);
|
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);
|
const handleJobUpdates = async (jobChangedData) => {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
const jobId = jobChangedData.id;
|
||||||
}, [updatedJobs]);
|
|
||||||
|
|
||||||
|
// 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) => {
|
const getUpdatedJobData = async (jobId) => {
|
||||||
client.query({
|
await client.query({
|
||||||
query: QUERY_EXACT_JOB_IN_PRODUCTION,
|
query: QUERY_EXACT_JOB_IN_PRODUCTION,
|
||||||
variables: { id: jobId }
|
variables: { id: jobId },
|
||||||
|
fetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const getUpdatedJobsData = async (jobIds) => {
|
|
||||||
|
const getUpdatedJobsData = (jobIds) => {
|
||||||
client.query({
|
client.query({
|
||||||
query: QUERY_EXACT_JOBS_IN_PRODUCTION,
|
query: QUERY_EXACT_JOBS_IN_PRODUCTION,
|
||||||
variables: { ids: jobIds }
|
variables: { ids: jobIds }
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -23,17 +23,14 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
|
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
|
||||||
|
|
||||||
export const socket = SocketIO(
|
export const socket = SocketIO(import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "", {
|
||||||
import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : window.location.origin,
|
path: "/ws",
|
||||||
{
|
withCredentials: true,
|
||||||
path: "/ws",
|
auth: async (callback) => {
|
||||||
withCredentials: true,
|
const token = auth.currentUser && (await auth.currentUser.getIdToken());
|
||||||
auth: async (callback) => {
|
callback({ token });
|
||||||
const token = auth.currentUser && (await auth.currentUser.getIdToken());
|
|
||||||
callback({ token });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
|
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
|
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
|
||||||
|
|
||||||
export const socket = SocketIO(
|
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",
|
path: "/ws",
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FloatButton, Layout, Spin } from "antd";
|
import { FloatButton, Layout, Spin } from "antd";
|
||||||
// import preval from "preval.macro";
|
// import preval from "preval.macro";
|
||||||
import React, { lazy, Suspense, useEffect, useState } from "react";
|
import React, { lazy, Suspense, useContext, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, Route, Routes } from "react-router-dom";
|
import { Link, Route, Routes } from "react-router-dom";
|
||||||
@@ -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 PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container";
|
||||||
import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component";
|
import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component";
|
||||||
import { requestForToken } from "../../firebase/firebase.utils";
|
import { requestForToken } from "../../firebase/firebase.utils";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||||
import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
import UpdateAlert from "../../components/update-alert/update-alert.component";
|
import UpdateAlert from "../../components/update-alert/update-alert.component";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
||||||
import "./manage.page.styles.scss";
|
import "./manage.page.styles.scss";
|
||||||
|
import WssStatusDisplayComponent from "../../components/wss-status-display/wss-status-display.component.jsx";
|
||||||
|
|
||||||
const JobsPage = lazy(() => import("../jobs/jobs.page"));
|
const JobsPage = lazy(() => import("../jobs/jobs.page"));
|
||||||
|
|
||||||
@@ -110,6 +112,7 @@ const mapDispatchToProps = (dispatch) => ({});
|
|||||||
export function Manage({ conflict, bodyshop }) {
|
export function Manage({ conflict, bodyshop }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [chatVisible] = useState(false);
|
const [chatVisible] = useState(false);
|
||||||
|
const { socket, clientId } = useContext(SocketContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const widgetId = InstanceRenderManager({
|
const widgetId = InstanceRenderManager({
|
||||||
@@ -129,6 +132,7 @@ export function Manage({ conflict, bodyshop }) {
|
|||||||
promanager: t("titles.promanager")
|
promanager: t("titles.promanager")
|
||||||
});
|
});
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
const AppRouteTable = (
|
const AppRouteTable = (
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={
|
||||||
@@ -569,6 +573,13 @@ export function Manage({ conflict, bodyshop }) {
|
|||||||
else if (bodyshop && bodyshop.sub_status !== "active") PageContent = <ShopSubStatusComponent />;
|
else if (bodyshop && bodyshop.sub_status !== "active") PageContent = <ShopSubStatusComponent />;
|
||||||
else PageContent = AppRouteTable;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{import.meta.env.PROD && <ChatAffixContainer bodyshop={bodyshop} chatVisible={chatVisible} />}
|
{import.meta.env.PROD && <ChatAffixContainer bodyshop={bodyshop} chatVisible={chatVisible} />}
|
||||||
@@ -594,7 +605,8 @@ export function Manage({ conflict, bodyshop }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex" }}>
|
<div style={{ display: "flex" }}>
|
||||||
<div>
|
<WssStatusDisplayComponent />
|
||||||
|
<div onClick={broadcastMessage}>
|
||||||
{`${InstanceRenderManager({
|
{`${InstanceRenderManager({
|
||||||
imex: t("titles.imexonline"),
|
imex: t("titles.imexonline"),
|
||||||
rome: t("titles.romeonline"),
|
rome: t("titles.romeonline"),
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function ProductionListComponent({ bodyshop }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NoteUpsertModal />
|
<NoteUpsertModal />
|
||||||
<ProductionListTable subscriptionType={Production_Use_View.treatment} />
|
<ProductionListTable bodyshop={bodyshop} subscriptionType={Production_Use_View.treatment} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,3 +67,7 @@ export const setUpdateAvailable = (isUpdateAvailable) => ({
|
|||||||
type: ApplicationActionTypes.SET_UPDATE_AVAILABLE,
|
type: ApplicationActionTypes.SET_UPDATE_AVAILABLE,
|
||||||
payload: isUpdateAvailable
|
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 = {
|
const INITIAL_STATE = {
|
||||||
loading: false,
|
loading: false,
|
||||||
online: true,
|
online: true,
|
||||||
|
wssStatus: "disconnected",
|
||||||
updateAvailable: false,
|
updateAvailable: false,
|
||||||
breadcrumbs: [],
|
breadcrumbs: [],
|
||||||
recentItems: [],
|
recentItems: [],
|
||||||
@@ -87,6 +88,9 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
|
|||||||
case ApplicationActionTypes.SET_PROBLEM_JOBS: {
|
case ApplicationActionTypes.SET_PROBLEM_JOBS: {
|
||||||
return { ...state, problemJobs: action.payload };
|
return { ...state, problemJobs: action.payload };
|
||||||
}
|
}
|
||||||
|
case ApplicationActionTypes.SET_WSS_STATUS: {
|
||||||
|
return { ...state, wssStatus: action.payload };
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,3 +22,4 @@ export const selectJobReadOnly = createSelector([selectApplication], (applicatio
|
|||||||
export const selectOnline = createSelector([selectApplication], (application) => application.online);
|
export const selectOnline = createSelector([selectApplication], (application) => application.online);
|
||||||
export const selectProblemJobs = createSelector([selectApplication], (application) => application.problemJobs);
|
export const selectProblemJobs = createSelector([selectApplication], (application) => application.problemJobs);
|
||||||
export const selectUpdateAvailable = createSelector([selectApplication], (application) => application.updateAvailable);
|
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",
|
SET_ONLINE_STATUS: "SET_ONLINE_STATUS",
|
||||||
INSERT_AUDIT_TRAIL: "INSERT_AUDIT_TRAIL",
|
INSERT_AUDIT_TRAIL: "INSERT_AUDIT_TRAIL",
|
||||||
SET_PROBLEM_JOBS: "SET_PROBLEM_JOBS",
|
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;
|
export default ApplicationActionTypes;
|
||||||
|
|||||||
@@ -242,6 +242,10 @@ export function* signInSuccessSaga({ payload }) {
|
|||||||
window.$crisp.push(["set", "user:nickname", [payload.displayName || payload.email]]);
|
window.$crisp.push(["set", "user:nickname", [payload.displayName || payload.email]]);
|
||||||
window.$crisp.push(["set", "session:segments", [["user"]]]);
|
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: () => {
|
promanager: () => {
|
||||||
Userpilot.identify(payload.email, {
|
Userpilot.identify(payload.email, {
|
||||||
email: payload.email
|
email: payload.email
|
||||||
@@ -310,8 +314,8 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
|||||||
try {
|
try {
|
||||||
const userEmail = yield select((state) => state.user.currentUser.email);
|
const userEmail = yield select((state) => state.user.currentUser.email);
|
||||||
try {
|
try {
|
||||||
//console.log("Setting shop timezone.");
|
console.log("Setting shop timezone.");
|
||||||
// dayjs.tz.setDefault(payload.timezone);
|
day.tz.setDefault(payload.timezone);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
@@ -371,6 +375,9 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
|||||||
if (authRecord[0] && authRecord[0].user.validemail) {
|
if (authRecord[0] && authRecord[0].user.validemail) {
|
||||||
window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]);
|
window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
rome: () => {
|
||||||
|
window.$zoho.salesiq.visitor.info({ "Shop Name": payload.shopname });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -119,7 +119,7 @@
|
|||||||
"jobclosedwithbypass": "Job was invoiced using the master bypass password. ",
|
"jobclosedwithbypass": "Job was invoiced using the master bypass password. ",
|
||||||
"jobconverted": "Job converted and assigned number {{ro_number}}.",
|
"jobconverted": "Job converted and assigned number {{ro_number}}.",
|
||||||
"jobdelivery": "Job intake completed. Status set to {{status}}. Actual completion is {{actual_completion}}.",
|
"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}}.",
|
"jobfieldchanged": "Job field $t(jobs.fields.{{field}}) changed to {{value}}.",
|
||||||
"jobimported": "Job imported.",
|
"jobimported": "Job imported.",
|
||||||
"jobinproductionchange": "Job production status set to {{inproduction}}",
|
"jobinproductionchange": "Job production status set to {{inproduction}}",
|
||||||
@@ -2849,15 +2849,21 @@
|
|||||||
"jobs_in_production": "Jobs in Production",
|
"jobs_in_production": "Jobs in Production",
|
||||||
"tasks_in_production": "Tasks in Production",
|
"tasks_in_production": "Tasks in Production",
|
||||||
"tasks_on_board": "Tasks on Board",
|
"tasks_on_board": "Tasks on Board",
|
||||||
|
"tasks_in_view": "Tasks in View",
|
||||||
"total_amount_in_production": "Dollars in Production",
|
"total_amount_in_production": "Dollars in Production",
|
||||||
"total_amount_on_board": "Dollars on Board",
|
"total_amount_on_board": "Dollars on Board",
|
||||||
|
"total_amount_in_view": "Dollars in View",
|
||||||
"total_hours_in_production": "Hours in Production",
|
"total_hours_in_production": "Hours in Production",
|
||||||
"total_hours_on_board": "Hours on Board",
|
"total_hours_on_board": "Hours on Board",
|
||||||
|
"total_hours_in_view": "Hours in View",
|
||||||
"total_jobs_on_board": "Jobs on Board",
|
"total_jobs_on_board": "Jobs on Board",
|
||||||
|
"total_jobs_in_view": "Jobs in View",
|
||||||
"total_lab_in_production": "Body Hours in Production",
|
"total_lab_in_production": "Body Hours in Production",
|
||||||
"total_lab_on_board": "Body Hours on Board",
|
"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_in_production": "Refinish Hours in Production",
|
||||||
"total_lar_on_board": "Refinish Hours on Board"
|
"total_lar_on_board": "Refinish Hours on Board",
|
||||||
|
"total_lar_in_view": "Refinish Hours in View"
|
||||||
},
|
},
|
||||||
"statistics_title": "Statistics"
|
"statistics_title": "Statistics"
|
||||||
},
|
},
|
||||||
@@ -2869,15 +2875,21 @@
|
|||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"tasks_in_production": "Tasks in Production",
|
"tasks_in_production": "Tasks in Production",
|
||||||
"tasks_on_board": "Tasks on Board",
|
"tasks_on_board": "Tasks on Board",
|
||||||
|
"tasks_in_view": "Tasks in View",
|
||||||
"total_amount_in_production": "Dollars in Production",
|
"total_amount_in_production": "Dollars in Production",
|
||||||
"total_amount_on_board": "Dollars on Board",
|
"total_amount_on_board": "Dollars on Board",
|
||||||
|
"total_amount_in_view": "Dollars in View",
|
||||||
"total_hours_in_production": "Hours in Production",
|
"total_hours_in_production": "Hours in Production",
|
||||||
"total_hours_on_board": "Hours on Board",
|
"total_hours_on_board": "Hours on Board",
|
||||||
|
"total_hours_in_view": "Hours in View",
|
||||||
"total_jobs_on_board": "Jobs on Board",
|
"total_jobs_on_board": "Jobs on Board",
|
||||||
|
"total_jobs_in_view": "Jobs in View",
|
||||||
"total_lab_in_production": "Body Hours in Production",
|
"total_lab_in_production": "Body Hours in Production",
|
||||||
"total_lab_on_board": "Body Hours on Board",
|
"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_in_production": "Refinish Hours in Production",
|
||||||
"total_lar_on_board": "Refinish Hours on Board"
|
"total_lar_on_board": "Refinish Hours on Board",
|
||||||
|
"total_lar_in_view": "Refinish Hours in View"
|
||||||
},
|
},
|
||||||
"successes": {
|
"successes": {
|
||||||
"removed": "Job removed from production."
|
"removed": "Job removed from production."
|
||||||
|
|||||||
@@ -2849,15 +2849,21 @@
|
|||||||
"jobs_in_production": "",
|
"jobs_in_production": "",
|
||||||
"tasks_in_production": "",
|
"tasks_in_production": "",
|
||||||
"tasks_on_board": "",
|
"tasks_on_board": "",
|
||||||
|
"tasks_in_view": "",
|
||||||
"total_amount_in_production": "",
|
"total_amount_in_production": "",
|
||||||
"total_amount_on_board": "",
|
"total_amount_on_board": "",
|
||||||
|
"total_amount_in_view": "",
|
||||||
"total_hours_in_production": "",
|
"total_hours_in_production": "",
|
||||||
"total_hours_on_board": "",
|
"total_hours_on_board": "",
|
||||||
|
"total_hours_in_view": "",
|
||||||
"total_jobs_on_board": "",
|
"total_jobs_on_board": "",
|
||||||
|
"total_jobs_in_view": "",
|
||||||
"total_lab_in_production": "",
|
"total_lab_in_production": "",
|
||||||
"total_lab_on_board": "",
|
"total_lab_on_board": "",
|
||||||
|
"total_lab_in_view": "",
|
||||||
"total_lar_in_production": "",
|
"total_lar_in_production": "",
|
||||||
"total_lar_on_board": ""
|
"total_lar_on_board": "",
|
||||||
|
"total_lar_in_view": ""
|
||||||
},
|
},
|
||||||
"statistics_title": ""
|
"statistics_title": ""
|
||||||
},
|
},
|
||||||
@@ -2869,15 +2875,21 @@
|
|||||||
"tasks": "",
|
"tasks": "",
|
||||||
"tasks_in_production": "",
|
"tasks_in_production": "",
|
||||||
"tasks_on_board": "",
|
"tasks_on_board": "",
|
||||||
|
"tasks_in_view": "",
|
||||||
"total_amount_in_production": "",
|
"total_amount_in_production": "",
|
||||||
"total_amount_on_board": "",
|
"total_amount_on_board": "",
|
||||||
|
"total_amount_in_view": "",
|
||||||
"total_hours_in_production": "",
|
"total_hours_in_production": "",
|
||||||
"total_hours_on_board": "",
|
"total_hours_on_board": "",
|
||||||
|
"total_hours_in_view": "",
|
||||||
"total_jobs_on_board": "",
|
"total_jobs_on_board": "",
|
||||||
|
"total_jobs_in_view": "",
|
||||||
"total_lab_in_production": "",
|
"total_lab_in_production": "",
|
||||||
"total_lab_on_board": "",
|
"total_lab_on_board": "",
|
||||||
|
"total_lab_in_view": "",
|
||||||
"total_lar_in_production": "",
|
"total_lar_in_production": "",
|
||||||
"total_lar_on_board": ""
|
"total_lar_on_board": "",
|
||||||
|
"total_lar_in_view": ""
|
||||||
},
|
},
|
||||||
"successes": {
|
"successes": {
|
||||||
"removed": ""
|
"removed": ""
|
||||||
|
|||||||
@@ -2845,40 +2845,52 @@
|
|||||||
"filters_title": "",
|
"filters_title": "",
|
||||||
"information": "",
|
"information": "",
|
||||||
"layout": "",
|
"layout": "",
|
||||||
"statistics": {
|
"statistics": {
|
||||||
"jobs_in_production": "",
|
"jobs_in_production": "",
|
||||||
"tasks_in_production": "",
|
"tasks_in_production": "",
|
||||||
"tasks_on_board": "",
|
"tasks_on_board": "",
|
||||||
"total_amount_in_production": "",
|
"tasks_in_view": "",
|
||||||
"total_amount_on_board": "",
|
"total_amount_in_production": "",
|
||||||
"total_hours_in_production": "",
|
"total_amount_on_board": "",
|
||||||
"total_hours_on_board": "",
|
"total_amount_in_view": "",
|
||||||
"total_jobs_on_board": "",
|
"total_hours_in_production": "",
|
||||||
"total_lab_in_production": "",
|
"total_hours_on_board": "",
|
||||||
"total_lab_on_board": "",
|
"total_hours_in_view": "",
|
||||||
"total_lar_in_production": "",
|
"total_jobs_on_board": "",
|
||||||
"total_lar_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_title": ""
|
||||||
},
|
},
|
||||||
"statistics": {
|
"statistics": {
|
||||||
"currency_symbol": "",
|
"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": "",
|
||||||
"total_amount_in_production": "",
|
"tasks_in_view": "",
|
||||||
"total_amount_on_board": "",
|
"total_amount_in_production": "",
|
||||||
"total_hours_in_production": "",
|
"total_amount_on_board": "",
|
||||||
"total_hours_on_board": "",
|
"total_amount_in_view": "",
|
||||||
"total_jobs_on_board": "",
|
"total_hours_in_production": "",
|
||||||
"total_lab_in_production": "",
|
"total_hours_on_board": "",
|
||||||
"total_lab_on_board": "",
|
"total_hours_in_view": "",
|
||||||
"total_lar_in_production": "",
|
"total_jobs_on_board": "",
|
||||||
"total_lar_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": {
|
"successes": {
|
||||||
"removed": ""
|
"removed": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import axios from "axios";
|
|||||||
import { auth } from "../firebase/firebase.utils";
|
import { auth } from "../firebase/firebase.utils";
|
||||||
import InstanceRenderManager from "./instanceRenderMgr";
|
import InstanceRenderManager from "./instanceRenderMgr";
|
||||||
|
|
||||||
axios.defaults.baseURL =
|
axios.defaults.baseURL = import.meta.env.DEV
|
||||||
import.meta.env.VITE_APP_AXIOS_BASE_API_URL ||
|
? "/api/"
|
||||||
(import.meta.env.MODE === "production" ? "https://api.imex.online/" : "http://localhost:4000/");
|
: import.meta.env.VITE_APP_AXIOS_BASE_API_URL || "https://api.imex.online/";
|
||||||
|
|
||||||
export const axiosAuthInterceptorId = axios.interceptors.request.use(
|
export const axiosAuthInterceptorId = axios.interceptors.request.use(
|
||||||
async (config) => {
|
async (config) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
// Sometimes referred to as PageSize, this variable controls the amount of records
|
// Sometimes referred to as PageSize, this variable controls the amount of records
|
||||||
// to show on one page during pagination.
|
// to show on one page during pagination.
|
||||||
export const pageLimit = 50;
|
export const pageLimit = 50;
|
||||||
|
export const exportPageLimit = 10;
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ import { store } from "../redux/store";
|
|||||||
|
|
||||||
export function CreateExplorerLinkForJob({ jobid }) {
|
export function CreateExplorerLinkForJob({ jobid }) {
|
||||||
const bodyshop = store.getState().user.bodyshop;
|
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 { createRequire } from "module";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import * as url from "url";
|
import * as url from "url";
|
||||||
import { defineConfig } from "vite";
|
import { createLogger, defineConfig } from "vite";
|
||||||
import { ViteEjsPlugin } from "vite-plugin-ejs";
|
import { ViteEjsPlugin } from "vite-plugin-ejs";
|
||||||
import eslint from "vite-plugin-eslint";
|
import eslint from "vite-plugin-eslint";
|
||||||
import { VitePWA } from "vite-plugin-pwa";
|
import { VitePWA } from "vite-plugin-pwa";
|
||||||
import InstanceRenderManager from "./src/utils/instanceRenderMgr";
|
import InstanceRenderManager from "./src/utils/instanceRenderMgr";
|
||||||
|
import chalk from "chalk";
|
||||||
|
//import { visualizer } from "rollup-plugin-visualizer";
|
||||||
|
|
||||||
process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", {
|
process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", {
|
||||||
timeZone: "America/Los_Angeles"
|
timeZone: "America/Los_Angeles"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const 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";`;
|
const WRONG_CODE = `import { bpfrpt_proptype_WindowScroller } from "../WindowScroller.js";`;
|
||||||
|
|
||||||
function reactVirtualizedFix() {
|
function reactVirtualizedFix() {
|
||||||
@@ -32,10 +38,16 @@ function reactVirtualizedFix() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
/** End of hack */
|
||||||
|
|
||||||
|
export const logger = createLogger("info", {
|
||||||
|
allowClearScreen: false
|
||||||
|
});
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: "/",
|
base: "/",
|
||||||
plugins: [
|
plugins: [
|
||||||
|
//visualizer(),
|
||||||
ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })),
|
ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })),
|
||||||
VitePWA({
|
VitePWA({
|
||||||
injectRegister: "auto",
|
injectRegister: "auto",
|
||||||
@@ -99,7 +111,6 @@ export default defineConfig({
|
|||||||
reactVirtualizedFix(),
|
reactVirtualizedFix(),
|
||||||
react(),
|
react(),
|
||||||
eslint()
|
eslint()
|
||||||
// CompressionPlugin(), //Cloudfront already compresses assets, so not needed.
|
|
||||||
],
|
],
|
||||||
define: {
|
define: {
|
||||||
APP_VERSION: JSON.stringify(process.env.npm_package_version)
|
APP_VERSION: JSON.stringify(process.env.npm_package_version)
|
||||||
@@ -107,7 +118,69 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
host: true,
|
host: true,
|
||||||
port: 3000,
|
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: {
|
build: {
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
@@ -115,17 +188,63 @@ export default defineConfig({
|
|||||||
manualChunks: {
|
manualChunks: {
|
||||||
antd: ["antd"],
|
antd: ["antd"],
|
||||||
"react-redux": ["react-redux"],
|
"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: {
|
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: {
|
esbuildOptions: {
|
||||||
loader: {
|
loader: {
|
||||||
".js": "jsx"
|
".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:
|
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!"
|
auth.hasura_secret_admin_key="Dev-BodyShopApp!"
|
||||||
|
|||||||
35
nodemon.json
Normal file
35
nodemon.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"watch": [
|
||||||
|
"server.js",
|
||||||
|
"server"
|
||||||
|
],
|
||||||
|
"ignore": [
|
||||||
|
"client",
|
||||||
|
".circleci",
|
||||||
|
".platform",
|
||||||
|
".idea",
|
||||||
|
".vscode",
|
||||||
|
"_reference",
|
||||||
|
"firebase",
|
||||||
|
"hasura",
|
||||||
|
"logs",
|
||||||
|
"redis",
|
||||||
|
".dockerignore",
|
||||||
|
".ebignore",
|
||||||
|
".editorconfig",
|
||||||
|
".env.development",
|
||||||
|
".env.development.rome",
|
||||||
|
".eslintrc.json",
|
||||||
|
".gitignore",
|
||||||
|
".npmmrc",
|
||||||
|
".prettierrc.js",
|
||||||
|
"bodyshop_translations.babel",
|
||||||
|
"Dockerfile",
|
||||||
|
"ecosystem.config.js",
|
||||||
|
"job-totals-testing-util.js",
|
||||||
|
"nodemon.json",
|
||||||
|
"package.json",
|
||||||
|
"package-lock.json",
|
||||||
|
"setadmin.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
1795
package-lock.json
generated
1795
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
44
package.json
44
package.json
@@ -19,44 +19,50 @@
|
|||||||
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\""
|
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-secrets-manager": "^3.629.0",
|
"@aws-sdk/client-elasticache": "^3.675.0",
|
||||||
"@aws-sdk/client-ses": "^3.629.0",
|
"@aws-sdk/client-secrets-manager": "^3.675.0",
|
||||||
"@aws-sdk/credential-provider-node": "^3.629.0",
|
"@aws-sdk/client-ses": "^3.675.0",
|
||||||
"@opensearch-project/opensearch": "^2.11.0",
|
"@aws-sdk/credential-provider-node": "^3.675.0",
|
||||||
"aws4": "^1.13.1",
|
"@opensearch-project/opensearch": "^2.12.0",
|
||||||
"axios": "^1.7.4",
|
"@socket.io/admin-ui": "^0.5.1",
|
||||||
|
"@socket.io/redis-adapter": "^8.3.0",
|
||||||
|
"aws4": "^1.13.2",
|
||||||
|
"axios": "^1.7.7",
|
||||||
"better-queue": "^3.8.12",
|
"better-queue": "^3.8.12",
|
||||||
"bluebird": "^3.7.2",
|
"bluebird": "^3.7.2",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.3",
|
||||||
"canvas": "^2.11.2",
|
"canvas": "^2.11.2",
|
||||||
"chart.js": "^4.4.3",
|
"chart.js": "^4.4.5",
|
||||||
"cloudinary": "^2.4.0",
|
"cloudinary": "^2.5.1",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"csrf": "^3.1.0",
|
"csrf": "^3.1.0",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.21.1",
|
||||||
"firebase-admin": "^12.3.1",
|
"firebase-admin": "^12.6.0",
|
||||||
"graphql": "^16.9.0",
|
"graphql": "^16.9.0",
|
||||||
"graphql-request": "^6.1.0",
|
"graphql-request": "^6.1.0",
|
||||||
"graylog2": "^0.2.1",
|
"graylog2": "^0.2.1",
|
||||||
"inline-css": "^4.0.2",
|
"inline-css": "^4.0.2",
|
||||||
"intuit-oauth": "^4.1.2",
|
"intuit-oauth": "^4.1.2",
|
||||||
"json-2-csv": "^5.5.5",
|
"ioredis": "^5.4.1",
|
||||||
|
"json-2-csv": "^5.5.6",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"moment-timezone": "^0.5.45",
|
"moment-timezone": "^0.5.46",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"node-mailjet": "^6.0.5",
|
"node-mailjet": "^6.0.6",
|
||||||
"node-persist": "^4.0.3",
|
"node-persist": "^4.0.3",
|
||||||
"nodemailer": "^6.9.14",
|
"nodemailer": "^6.9.15",
|
||||||
"phone": "^3.1.49",
|
"phone": "^3.1.51",
|
||||||
"recursive-diff": "^1.0.9",
|
"recursive-diff": "^1.0.9",
|
||||||
|
"redis": "^4.7.0",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"soap": "^1.1.1",
|
"soap": "^1.1.5",
|
||||||
"socket.io": "^4.7.5",
|
"socket.io": "^4.8.0",
|
||||||
|
"socket.io-adapter": "^2.5.5",
|
||||||
"ssh2-sftp-client": "^10.0.3",
|
"ssh2-sftp-client": "^10.0.3",
|
||||||
"twilio": "^4.23.0",
|
"twilio": "^4.23.0",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^10.0.0",
|
||||||
|
|||||||
20
redis/Dockerfile
Normal file
20
redis/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Use the official Redis image
|
||||||
|
FROM redis:7.0-alpine
|
||||||
|
|
||||||
|
# Copy the Redis configuration file
|
||||||
|
COPY redis.conf /usr/local/etc/redis/redis.conf
|
||||||
|
|
||||||
|
# Copy the entrypoint script
|
||||||
|
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||||
|
|
||||||
|
# Make the entrypoint script executable
|
||||||
|
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||||
|
|
||||||
|
# Debugging step: List contents of /usr/local/bin
|
||||||
|
RUN ls -l /usr/local/bin
|
||||||
|
|
||||||
|
# Expose Redis ports
|
||||||
|
EXPOSE 6379 16379
|
||||||
|
|
||||||
|
# Set the entrypoint
|
||||||
|
ENTRYPOINT ["entrypoint.sh"]
|
||||||
36
redis/entrypoint.sh
Normal file
36
redis/entrypoint.sh
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
LOCKFILE="/redis-lock/redis-cluster-init.lock"
|
||||||
|
|
||||||
|
# Start Redis server in the background
|
||||||
|
redis-server /usr/local/etc/redis/redis.conf &
|
||||||
|
|
||||||
|
# Wait for Redis server to start
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Only initialize the cluster if the lock file doesn't exist
|
||||||
|
if [ ! -f "$LOCKFILE" ]; then
|
||||||
|
echo "Initializing Redis Cluster..."
|
||||||
|
|
||||||
|
# Create lock file to prevent further initialization attempts
|
||||||
|
touch "$LOCKFILE"
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "Lock file created successfully at $LOCKFILE."
|
||||||
|
else
|
||||||
|
echo "Failed to create lock file."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Initialize the Redis cluster
|
||||||
|
yes yes | redis-cli --cluster create \
|
||||||
|
redis-node-1:6379 \
|
||||||
|
redis-node-2:6379 \
|
||||||
|
redis-node-3:6379 \
|
||||||
|
--cluster-replicas 0
|
||||||
|
|
||||||
|
echo "Redis Cluster initialized."
|
||||||
|
else
|
||||||
|
echo "Cluster already initialized, skipping initialization."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Keep the container running
|
||||||
|
tail -f /dev/null
|
||||||
6
redis/redis.conf
Normal file
6
redis/redis.conf
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
bind 0.0.0.0
|
||||||
|
port 6379
|
||||||
|
cluster-enabled yes
|
||||||
|
cluster-config-file nodes.conf
|
||||||
|
cluster-node-timeout 5000
|
||||||
|
appendonly yes
|
||||||
391
server.js
391
server.js
@@ -1,110 +1,329 @@
|
|||||||
// Import core modules
|
|
||||||
const express = require("express");
|
|
||||||
const cors = require("cors");
|
const cors = require("cors");
|
||||||
const bodyParser = require("body-parser");
|
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const http = require("http");
|
||||||
|
const Redis = require("ioredis");
|
||||||
|
const express = require("express");
|
||||||
|
const bodyParser = require("body-parser");
|
||||||
const compression = require("compression");
|
const compression = require("compression");
|
||||||
const cookieParser = require("cookie-parser");
|
const cookieParser = require("cookie-parser");
|
||||||
const http = require("http");
|
|
||||||
const { Server } = require("socket.io");
|
const { Server } = require("socket.io");
|
||||||
|
const { createAdapter } = require("@socket.io/redis-adapter");
|
||||||
|
const { instrument } = require("@socket.io/admin-ui");
|
||||||
|
const { isString, isEmpty } = require("lodash");
|
||||||
|
|
||||||
|
const logger = require("./server/utils/logger");
|
||||||
|
const { applyRedisHelpers } = require("./server/utils/redisHelpers");
|
||||||
|
const { applyIOHelpers } = require("./server/utils/ioHelpers");
|
||||||
|
const { redisSocketEvents } = require("./server/web-sockets/redisSocketEvents");
|
||||||
|
const { ElastiCacheClient, DescribeCacheClustersCommand } = require("@aws-sdk/client-elasticache");
|
||||||
|
const { default: InstanceManager } = require("./server/utils/instanceMgr");
|
||||||
|
|
||||||
// Load environment variables
|
// Load environment variables
|
||||||
require("dotenv").config({
|
require("dotenv").config({
|
||||||
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Import custom utilities and handlers
|
const CLUSTER_RETRY_BASE_DELAY = 100;
|
||||||
const logger = require("./server/utils/logger");
|
const CLUSTER_RETRY_MAX_DELAY = 5000;
|
||||||
|
const CLUSTER_RETRY_JITTER = 100;
|
||||||
|
|
||||||
// Express app and server setup
|
/**
|
||||||
const app = express();
|
* CORS Origin for Socket.IO
|
||||||
const port = process.env.PORT || 5000;
|
* @type {string[][]}
|
||||||
const server = http.createServer(app);
|
*/
|
||||||
const io = new Server(server, {
|
const SOCKETIO_CORS_ORIGIN = [
|
||||||
path: "/ws",
|
"https://test.imex.online",
|
||||||
cors: {
|
"https://www.test.imex.online",
|
||||||
origin: [
|
"http://localhost:3000",
|
||||||
"https://test.imex.online",
|
"https://localhost:3000",
|
||||||
"https://www.test.imex.online",
|
"https://imex.online",
|
||||||
"http://localhost:3000",
|
"https://www.imex.online",
|
||||||
"https://imex.online",
|
"https://romeonline.io",
|
||||||
"https://www.imex.online",
|
"https://www.romeonline.io",
|
||||||
"https://romeonline.io", //Added in all RO and PM routes to simplyify setup.
|
"https://test.romeonline.io",
|
||||||
"https://www.romeonline.io",
|
"https://www.test.romeonline.io",
|
||||||
"https://beta.test.romeonline.io",
|
"https://beta.romeonline.io",
|
||||||
"https://www.beta.test.romeonline.io",
|
"https://www.beta.romeonline.io",
|
||||||
"https://beta.romeonline.io",
|
"https://beta.test.imex.online",
|
||||||
"https://www.beta.romeonline.io",
|
"https://www.beta.test.imex.online",
|
||||||
"https://beta.test.imex.online",
|
"https://beta.imex.online",
|
||||||
"https://www.beta.test.imex.online",
|
"https://www.beta.imex.online",
|
||||||
"https://beta.imex.online",
|
"https://www.test.promanager.web-est.com",
|
||||||
"https://www.beta.imex.online",
|
"https://test.promanager.web-est.com",
|
||||||
"https://www.test.promanager.web-est.com",
|
"https://www.promanager.web-est.com",
|
||||||
"https://test.promanager.web-est.com",
|
"https://promanager.web-est.com",
|
||||||
"https://www.promanager.web-est.com",
|
"https://old.imex.online",
|
||||||
"https://www.promanager.web-est.com"
|
"https://www.old.imex.online",
|
||||||
],
|
"https://wsadmin.imex.online",
|
||||||
methods: ["GET", "POST"],
|
"https://www.wsadmin.imex.online"
|
||||||
credentials: true,
|
];
|
||||||
exposedHeaders: ["set-cookie"]
|
|
||||||
|
const SOCKETIO_CORS_ORIGIN_DEV = ["http://localhost:3333", "https://localhost:3333"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware for Express app
|
||||||
|
* @param app
|
||||||
|
*/
|
||||||
|
const applyMiddleware = ({ app }) => {
|
||||||
|
app.use(compression());
|
||||||
|
app.use(cookieParser());
|
||||||
|
app.use(bodyParser.json({ limit: "50mb" }));
|
||||||
|
app.use(bodyParser.urlencoded({ limit: "50mb", extended: true }));
|
||||||
|
app.use(cors({ credentials: true, exposedHeaders: ["set-cookie"] }));
|
||||||
|
|
||||||
|
// Helper middleware
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
req.logger = logger;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route groupings for Express app
|
||||||
|
* @param app
|
||||||
|
*/
|
||||||
|
const applyRoutes = ({ app }) => {
|
||||||
|
app.use("/", require("./server/routes/miscellaneousRoutes"));
|
||||||
|
app.use("/notifications", require("./server/routes/notificationsRoutes"));
|
||||||
|
app.use("/render", require("./server/routes/renderRoutes"));
|
||||||
|
app.use("/mixdata", require("./server/routes/mixDataRoutes"));
|
||||||
|
app.use("/accounting", require("./server/routes/accountingRoutes"));
|
||||||
|
app.use("/qbo", require("./server/routes/qboRoutes"));
|
||||||
|
app.use("/media", require("./server/routes/mediaRoutes"));
|
||||||
|
app.use("/sms", require("./server/routes/smsRoutes"));
|
||||||
|
app.use("/job", require("./server/routes/jobRoutes"));
|
||||||
|
app.use("/scheduling", require("./server/routes/schedulingRoutes"));
|
||||||
|
app.use("/utils", require("./server/routes/utilRoutes"));
|
||||||
|
app.use("/data", require("./server/routes/dataRoutes"));
|
||||||
|
app.use("/adm", require("./server/routes/adminRoutes"));
|
||||||
|
app.use("/tech", require("./server/routes/techRoutes"));
|
||||||
|
app.use("/intellipay", require("./server/routes/intellipayRoutes"));
|
||||||
|
app.use("/cdk", require("./server/routes/cdkRoutes"));
|
||||||
|
app.use("/csi", require("./server/routes/csiRoutes"));
|
||||||
|
app.use("/payroll", require("./server/routes/payrollRoutes"));
|
||||||
|
|
||||||
|
// Default route for forbidden access
|
||||||
|
app.get("/", (req, res) => {
|
||||||
|
res.status(200).send("Access Forbidden.");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Redis nodes from AWS ElastiCache
|
||||||
|
* @returns {Promise<string[]>}
|
||||||
|
*/
|
||||||
|
const getRedisNodesFromAWS = async () => {
|
||||||
|
const client = new ElastiCacheClient({
|
||||||
|
region: InstanceManager({
|
||||||
|
imex: "ca-central-1",
|
||||||
|
rome: "us-east-2"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
ReplicationGroupId: process.env.REDIS_CLUSTER_ID,
|
||||||
|
ShowCacheNodeInfo: true
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch the cache clusters associated with the replication group
|
||||||
|
const command = new DescribeCacheClustersCommand(params);
|
||||||
|
const response = await client.send(command);
|
||||||
|
const cacheClusters = response.CacheClusters;
|
||||||
|
|
||||||
|
return cacheClusters.flatMap((cluster) =>
|
||||||
|
cluster.CacheNodes.map((node) => `${node.Endpoint.Address}:${node.Endpoint.Port}`)
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.log(`Error fetching Redis nodes from AWS: ${err.message}`, "ERROR", "redis", "api");
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
exports.io = io;
|
|
||||||
|
|
||||||
require("./server/web-sockets/web-socket");
|
/**
|
||||||
|
* Connect to Redis Cluster
|
||||||
|
* @returns {Promise<unknown>}
|
||||||
|
*/
|
||||||
|
const connectToRedisCluster = async () => {
|
||||||
|
let redisServers;
|
||||||
|
|
||||||
// Middleware
|
if (isString(process.env?.REDIS_CLUSTER_ID) && !isEmpty(process.env?.REDIS_CLUSTER_ID)) {
|
||||||
app.use(compression());
|
// Fetch Redis nodes from AWS if AWS environment variables are present
|
||||||
app.use(cookieParser());
|
redisServers = await getRedisNodesFromAWS();
|
||||||
app.use(bodyParser.json({ limit: "50mb" }));
|
} else {
|
||||||
app.use(bodyParser.urlencoded({ limit: "50mb", extended: true }));
|
// Use the Dockerized Redis cluster in development
|
||||||
app.use(cors({ credentials: true, exposedHeaders: ["set-cookie"] }));
|
if (isEmpty(process.env?.REDIS_URL) || !isString(process.env?.REDIS_URL)) {
|
||||||
|
logger.log(`[${process.env.NODE_ENV}] No or Malformed REDIS_URL present.`, "ERROR", "redis", "api");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
redisServers = JSON.parse(process.env.REDIS_URL);
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(
|
||||||
|
`[${process.env.NODE_ENV}] Failed to parse REDIS_URL: ${error.message}. Exiting...`,
|
||||||
|
"ERROR",
|
||||||
|
"redis",
|
||||||
|
"api"
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helper middleware
|
const clusterRetryStrategy = (times) => {
|
||||||
app.use((req, res, next) => {
|
const delay =
|
||||||
req.logger = logger;
|
Math.min(CLUSTER_RETRY_BASE_DELAY + times * 50, CLUSTER_RETRY_MAX_DELAY) + Math.random() * CLUSTER_RETRY_JITTER;
|
||||||
next();
|
logger.log(
|
||||||
});
|
`[${process.env.NODE_ENV}] Redis cluster not yet ready. Retrying in ${delay.toFixed(2)}ms`,
|
||||||
|
"ERROR",
|
||||||
|
"redis",
|
||||||
|
"api"
|
||||||
|
);
|
||||||
|
return delay;
|
||||||
|
};
|
||||||
|
|
||||||
// Route groupings
|
const redisCluster = new Redis.Cluster(redisServers, {
|
||||||
app.use("/", require("./server/routes/miscellaneousRoutes"));
|
clusterRetryStrategy,
|
||||||
app.use("/notifications", require("./server/routes/notificationsRoutes"));
|
enableAutoPipelining: true,
|
||||||
app.use("/render", require("./server/routes/renderRoutes"));
|
enableReadyCheck: true,
|
||||||
app.use("/mixdata", require("./server/routes/mixDataRoutes"));
|
redisOptions: {
|
||||||
app.use("/accounting", require("./server/routes/accountingRoutes"));
|
// connectTimeout: 10000, // Timeout for connecting in ms
|
||||||
app.use("/qbo", require("./server/routes/qboRoutes"));
|
// idleTimeoutMillis: 30000, // Close idle connections after 30s
|
||||||
app.use("/media", require("./server/routes/mediaRoutes"));
|
// maxRetriesPerRequest: 5 // Retry a maximum of 5 times per request
|
||||||
app.use("/sms", require("./server/routes/smsRoutes"));
|
}
|
||||||
app.use("/job", require("./server/routes/jobRoutes"));
|
});
|
||||||
app.use("/scheduling", require("./server/routes/schedulingRoutes"));
|
|
||||||
app.use("/utils", require("./server/routes/utilRoutes"));
|
|
||||||
app.use("/data", require("./server/routes/dataRoutes"));
|
|
||||||
app.use("/adm", require("./server/routes/adminRoutes"));
|
|
||||||
app.use("/tech", require("./server/routes/techRoutes"));
|
|
||||||
app.use("/intellipay", require("./server/routes/intellipayRoutes"));
|
|
||||||
app.use("/cdk", require("./server/routes/cdkRoutes"));
|
|
||||||
app.use("/csi", require("./server/routes/csiRoutes"));
|
|
||||||
app.use("/payroll", require("./server/routes/payrollRoutes"));
|
|
||||||
|
|
||||||
// Default route for forbidden access
|
return new Promise((resolve, reject) => {
|
||||||
app.get("/", (req, res) => {
|
redisCluster.on("ready", () => {
|
||||||
res.status(200).send("Access Forbidden.");
|
logger.log(`[${process.env.NODE_ENV}] Redis cluster connection established.`, "INFO", "redis", "api");
|
||||||
});
|
resolve(redisCluster);
|
||||||
|
});
|
||||||
|
|
||||||
|
redisCluster.on("error", (err) => {
|
||||||
|
logger.log(`[${process.env.NODE_ENV}] Redis cluster connection failed: ${err.message}`, "ERROR", "redis", "api");
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply Redis to the server
|
||||||
|
* @param server
|
||||||
|
* @param app
|
||||||
|
*/
|
||||||
|
const applySocketIO = async ({ server, app }) => {
|
||||||
|
const redisCluster = await connectToRedisCluster();
|
||||||
|
|
||||||
|
// Handle errors
|
||||||
|
redisCluster.on("error", (err) => {
|
||||||
|
logger.log(`[${process.env.NODE_ENV}] Redis ERROR`, "ERROR", "redis", "api");
|
||||||
|
});
|
||||||
|
|
||||||
|
const pubClient = redisCluster;
|
||||||
|
const subClient = pubClient.duplicate();
|
||||||
|
|
||||||
|
pubClient.on("error", (err) => logger.log(`Redis pubClient error: ${err}`, "ERROR", "redis"));
|
||||||
|
subClient.on("error", (err) => logger.log(`Redis subClient error: ${err}`, "ERROR", "redis"));
|
||||||
|
|
||||||
|
process.on("SIGINT", async () => {
|
||||||
|
logger.log("Closing Redis connections...", "INFO", "redis", "api");
|
||||||
|
try {
|
||||||
|
await Promise.all([pubClient.disconnect(), subClient.disconnect()]);
|
||||||
|
logger.log("Redis connections closed. Process will exit.", "INFO", "redis", "api");
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(`Error closing Redis connections: ${error.message}`, "ERROR", "redis", "api");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const ioRedis = new Server(server, {
|
||||||
|
path: "/wss",
|
||||||
|
adapter: createAdapter(pubClient, subClient),
|
||||||
|
cors: {
|
||||||
|
origin:
|
||||||
|
process.env?.NODE_ENV === "development"
|
||||||
|
? [...SOCKETIO_CORS_ORIGIN, ...SOCKETIO_CORS_ORIGIN_DEV]
|
||||||
|
: SOCKETIO_CORS_ORIGIN,
|
||||||
|
methods: ["GET", "POST"],
|
||||||
|
credentials: true,
|
||||||
|
exposedHeaders: ["set-cookie"]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isString(process.env.REDIS_ADMIN_PASS) && !isEmpty(process.env.REDIS_ADMIN_PASS)) {
|
||||||
|
logger.log(`[${process.env.NODE_ENV}] Initializing Redis Admin UI....`, "INFO", "redis", "api");
|
||||||
|
instrument(ioRedis, {
|
||||||
|
auth: {
|
||||||
|
type: "basic",
|
||||||
|
username: "admin",
|
||||||
|
password: process.env.REDIS_ADMIN_PASS
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: process.env.REDIS_ADMIN_MODE || "development"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const io = new Server(server, {
|
||||||
|
path: "/ws",
|
||||||
|
cors: {
|
||||||
|
origin: SOCKETIO_CORS_ORIGIN,
|
||||||
|
methods: ["GET", "POST"],
|
||||||
|
credentials: true,
|
||||||
|
exposedHeaders: ["set-cookie"]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const api = {
|
||||||
|
pubClient,
|
||||||
|
io,
|
||||||
|
ioRedis,
|
||||||
|
redisCluster
|
||||||
|
};
|
||||||
|
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
Object.assign(req, api);
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.assign(module.exports, api);
|
||||||
|
|
||||||
|
return api;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main function to start the server
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
await server.listen(port);
|
const app = express();
|
||||||
|
const port = process.env.PORT || 5000;
|
||||||
|
|
||||||
|
const server = http.createServer(app);
|
||||||
|
|
||||||
|
const { pubClient, ioRedis } = await applySocketIO({ server, app });
|
||||||
|
const redisHelpers = applyRedisHelpers({ pubClient, app, logger });
|
||||||
|
const ioHelpers = applyIOHelpers({ app, redisHelpers, ioRedis, logger });
|
||||||
|
|
||||||
|
// Legacy Socket Events
|
||||||
|
require("./server/web-sockets/web-socket");
|
||||||
|
|
||||||
|
applyMiddleware({ app });
|
||||||
|
applyRoutes({ app });
|
||||||
|
redisSocketEvents({ io: ioRedis, redisHelpers, ioHelpers, logger });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.listen(port);
|
||||||
|
logger.log(`[${process.env.NODE_ENV}] Server started on port ${port}`, "INFO", "api");
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(`[${process.env.NODE_ENV}] Server failed to start on port ${port}`, "ERROR", "api", error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
main()
|
main().catch((error) => {
|
||||||
.then(() => {
|
logger.log(`Main-API-Error: Something was not caught in the application.`, "error", "api", null, {
|
||||||
logger.log(`[${process.env.NODE_ENV || "DEVELOPMENT"}] Server started on port ${port}`, "INFO", "api");
|
error: error.message,
|
||||||
})
|
errorjson: JSON.stringify(error)
|
||||||
.catch((error) => {
|
|
||||||
logger.log(
|
|
||||||
`[${process.env.NODE_ENV || "DEVELOPMENT"}] Server failed to start on port ${port}`,
|
|
||||||
"ERROR",
|
|
||||||
"api",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
// Note: If we want the app to crash on all uncaught async operations, we would
|
||||||
|
// need to put a `process.exit(1);` here
|
||||||
|
});
|
||||||
|
|||||||
@@ -602,18 +602,22 @@ async function MarkJobExported(socket, jobid) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function InsertFailedExportLog(socket, error) {
|
async function InsertFailedExportLog(socket, error) {
|
||||||
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
|
try {
|
||||||
const result = await client
|
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
|
||||||
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
|
const result = await client
|
||||||
.request(queries.INSERT_EXPORT_LOG, {
|
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
|
||||||
log: {
|
.request(queries.INSERT_EXPORT_LOG, {
|
||||||
bodyshopid: socket.JobData.bodyshop.id,
|
log: {
|
||||||
jobid: socket.JobData.id,
|
bodyshopid: socket.JobData.bodyshop.id,
|
||||||
successful: false,
|
jobid: socket.JobData.id,
|
||||||
message: [error],
|
successful: false,
|
||||||
useremail: socket.user.email
|
message: [error],
|
||||||
}
|
useremail: socket.user.email
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
} catch (error2) {
|
||||||
|
CdkBase.createLogEvent(socket, "ERROR", `Error in InsertFailedExportLog - ${error} - ${JSON.stringify(error2)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -986,18 +986,22 @@ async function MarkJobExported(socket, jobid) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function InsertFailedExportLog(socket, error) {
|
async function InsertFailedExportLog(socket, error) {
|
||||||
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
|
try {
|
||||||
const result = await client
|
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
|
||||||
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
|
const result = await client
|
||||||
.request(queries.INSERT_EXPORT_LOG, {
|
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
|
||||||
log: {
|
.request(queries.INSERT_EXPORT_LOG, {
|
||||||
bodyshopid: socket.JobData.bodyshop.id,
|
log: {
|
||||||
jobid: socket.JobData.id,
|
bodyshopid: socket.JobData.bodyshop.id,
|
||||||
successful: false,
|
jobid: socket.JobData.id,
|
||||||
message: [error],
|
successful: false,
|
||||||
useremail: socket.user.email
|
message: [error],
|
||||||
}
|
useremail: socket.user.email
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
} catch (error2) {
|
||||||
|
CdkBase.createLogEvent(socket, "ERROR", `Error in InsertFailedExportLog - ${error} - ${JSON.stringify(error2)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
31
server/email/mailer.js
Normal file
31
server/email/mailer.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
const { isString, isEmpty } = require("lodash");
|
||||||
|
const { defaultProvider } = require("@aws-sdk/credential-provider-node");
|
||||||
|
const { default: InstanceManager } = require("../utils/instanceMgr");
|
||||||
|
const aws = require("@aws-sdk/client-ses");
|
||||||
|
const nodemailer = require("nodemailer");
|
||||||
|
|
||||||
|
const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME);
|
||||||
|
|
||||||
|
const sesConfig = {
|
||||||
|
apiVersion: "latest",
|
||||||
|
credentials: defaultProvider(),
|
||||||
|
region: isLocal
|
||||||
|
? "ca-central-1"
|
||||||
|
: InstanceManager({
|
||||||
|
imex: "ca-central-1",
|
||||||
|
rome: "us-east-2"
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLocal) {
|
||||||
|
sesConfig.endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:4566`;
|
||||||
|
console.log(`SES Mailer set to LocalStack end point: ${sesConfig.endpoint}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ses = new aws.SES(sesConfig);
|
||||||
|
|
||||||
|
let transporter = nodemailer.createTransport({
|
||||||
|
SES: { ses, aws }
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = transporter;
|
||||||
@@ -3,30 +3,13 @@ require("dotenv").config({
|
|||||||
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
||||||
});
|
});
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
let nodemailer = require("nodemailer");
|
|
||||||
let aws = require("@aws-sdk/client-ses");
|
|
||||||
let { defaultProvider } = require("@aws-sdk/credential-provider-node");
|
|
||||||
const InstanceManager = require("../utils/instanceMgr").default;
|
const InstanceManager = require("../utils/instanceMgr").default;
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
const client = require("../graphql-client/graphql-client").client;
|
const client = require("../graphql-client/graphql-client").client;
|
||||||
const queries = require("../graphql-client/queries");
|
const queries = require("../graphql-client/queries");
|
||||||
const { isObject } = require("lodash");
|
const { isObject } = require("lodash");
|
||||||
const generateEmailTemplate = require("./generateTemplate");
|
const generateEmailTemplate = require("./generateTemplate");
|
||||||
const moment = require("moment");
|
const mailer = require("./mailer");
|
||||||
|
|
||||||
const ses = new aws.SES({
|
|
||||||
// The key apiVersion is no longer supported in v3, and can be removed.
|
|
||||||
// @deprecated The client uses the "latest" apiVersion.
|
|
||||||
apiVersion: "latest",
|
|
||||||
defaultProvider,
|
|
||||||
region: InstanceManager({
|
|
||||||
imex: "ca-central-1",
|
|
||||||
rome: "us-east-2"
|
|
||||||
})
|
|
||||||
});
|
|
||||||
let transporter = nodemailer.createTransport({
|
|
||||||
SES: { ses, aws }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get the image from the URL and return it as a base64 string
|
// Get the image from the URL and return it as a base64 string
|
||||||
const getImage = async (imageUrl) => {
|
const getImage = async (imageUrl) => {
|
||||||
@@ -66,7 +49,7 @@ const logEmail = async (req, email) => {
|
|||||||
const sendServerEmail = async ({ subject, text }) => {
|
const sendServerEmail = async ({ subject, text }) => {
|
||||||
if (process.env.NODE_ENV === undefined) return;
|
if (process.env.NODE_ENV === undefined) return;
|
||||||
try {
|
try {
|
||||||
transporter.sendMail(
|
mailer.sendMail(
|
||||||
{
|
{
|
||||||
from: InstanceManager({
|
from: InstanceManager({
|
||||||
imex: `ImEX Online API - ${process.env.NODE_ENV} <noreply@imex.online>`,
|
imex: `ImEX Online API - ${process.env.NODE_ENV} <noreply@imex.online>`,
|
||||||
@@ -96,9 +79,9 @@ const sendServerEmail = async ({ subject, text }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendProManagerWelcomeEmail = async ({to, subject, html}) => {
|
const sendProManagerWelcomeEmail = async ({ to, subject, html }) => {
|
||||||
try {
|
try {
|
||||||
await transporter.sendMail({
|
await mailer.sendMail({
|
||||||
from: `ProManager <noreply@promanager.web-est.com>`,
|
from: `ProManager <noreply@promanager.web-est.com>`,
|
||||||
to,
|
to,
|
||||||
subject,
|
subject,
|
||||||
@@ -112,7 +95,7 @@ const sendProManagerWelcomeEmail = async ({to, subject, html}) => {
|
|||||||
|
|
||||||
const sendTaskEmail = async ({ to, subject, type = "text", html, text, attachments }) => {
|
const sendTaskEmail = async ({ to, subject, type = "text", html, text, attachments }) => {
|
||||||
try {
|
try {
|
||||||
transporter.sendMail(
|
mailer.sendMail(
|
||||||
{
|
{
|
||||||
from: InstanceManager({
|
from: InstanceManager({
|
||||||
imex: `ImEX Online <noreply@imex.online>`,
|
imex: `ImEX Online <noreply@imex.online>`,
|
||||||
@@ -166,7 +149,7 @@ const sendEmail = async (req, res) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
transporter.sendMail(
|
mailer.sendMail(
|
||||||
{
|
{
|
||||||
from: `${req.body.from.name} <${req.body.from.address}>`,
|
from: `${req.body.from.name} <${req.body.from.address}>`,
|
||||||
replyTo: req.body.ReplyTo.Email,
|
replyTo: req.body.ReplyTo.Email,
|
||||||
@@ -280,7 +263,7 @@ const emailBounce = async (req, res) => {
|
|||||||
status: "Bounced",
|
status: "Bounced",
|
||||||
context: message.bounce?.bouncedRecipients
|
context: message.bounce?.bouncedRecipients
|
||||||
});
|
});
|
||||||
transporter.sendMail(
|
mailer.sendMail(
|
||||||
{
|
{
|
||||||
from: InstanceMgr({
|
from: InstanceMgr({
|
||||||
imex: `ImEX Online <noreply@imex.online>`,
|
imex: `ImEX Online <noreply@imex.online>`,
|
||||||
|
|||||||
@@ -2,30 +2,14 @@ const path = require("path");
|
|||||||
require("dotenv").config({
|
require("dotenv").config({
|
||||||
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
||||||
});
|
});
|
||||||
let nodemailer = require("nodemailer");
|
|
||||||
let aws = require("@aws-sdk/client-ses");
|
|
||||||
let { defaultProvider } = require("@aws-sdk/credential-provider-node");
|
|
||||||
const InstanceManager = require("../utils/instanceMgr").default;
|
const InstanceManager = require("../utils/instanceMgr").default;
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
const client = require("../graphql-client/graphql-client").client;
|
const client = require("../graphql-client/graphql-client").client;
|
||||||
const queries = require("../graphql-client/queries");
|
const queries = require("../graphql-client/queries");
|
||||||
const generateEmailTemplate = require("./generateTemplate");
|
const generateEmailTemplate = require("./generateTemplate");
|
||||||
const moment = require("moment");
|
const moment = require("moment-timezone");
|
||||||
const { taskEmailQueue } = require("./tasksEmailsQueue");
|
const { taskEmailQueue } = require("./tasksEmailsQueue");
|
||||||
|
const mailer = require("./mailer");
|
||||||
const ses = new aws.SES({
|
|
||||||
apiVersion: "latest",
|
|
||||||
defaultProvider,
|
|
||||||
region: InstanceManager({
|
|
||||||
imex: "ca-central-1",
|
|
||||||
rome: "us-east-2"
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const transporter = nodemailer.createTransport({
|
|
||||||
SES: { ses, aws },
|
|
||||||
sendingRate: InstanceManager({ imex: 40, rome: 10 })
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize the Tasks Email Queue
|
// Initialize the Tasks Email Queue
|
||||||
const tasksEmailQueue = taskEmailQueue();
|
const tasksEmailQueue = taskEmailQueue();
|
||||||
@@ -41,28 +25,32 @@ const tasksEmailQueueCleanup = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== "development") {
|
// if (process.env.NODE_ENV !== "development") {
|
||||||
// Handling SIGINT (e.g., Ctrl+C)
|
// // Handling SIGINT (e.g., Ctrl+C)
|
||||||
process.on("SIGINT", async () => {
|
// process.on("SIGINT", async () => {
|
||||||
await tasksEmailQueueCleanup();
|
// console.log("Handling SIGNIT For Tasks Cleanup");
|
||||||
process.exit(0);
|
// await tasksEmailQueueCleanup();
|
||||||
});
|
// process.exit(0);
|
||||||
// Handling SIGTERM (e.g., sent by system shutdown)
|
// });
|
||||||
process.on("SIGTERM", async () => {
|
// // Handling SIGTERM (e.g., sent by system shutdown)
|
||||||
await tasksEmailQueueCleanup();
|
// process.on("SIGTERM", async () => {
|
||||||
process.exit(0);
|
// console.log("Handling SIGTERM For Tasks Cleanup");
|
||||||
});
|
// await tasksEmailQueueCleanup();
|
||||||
// Handling uncaught exceptions
|
// process.exit(0);
|
||||||
process.on("uncaughtException", async (err) => {
|
// });
|
||||||
await tasksEmailQueueCleanup();
|
// // Handling uncaught exceptions
|
||||||
process.exit(1); // Exit with an 'error' code
|
// process.on("uncaughtException", async (err) => {
|
||||||
});
|
// console.log("Handling uncaughtException For Tasks Cleanup");
|
||||||
// Handling unhandled promise rejections
|
// await tasksEmailQueueCleanup();
|
||||||
process.on("unhandledRejection", async (reason, promise) => {
|
// process.exit(1);
|
||||||
await tasksEmailQueueCleanup();
|
// });
|
||||||
process.exit(1); // Exit with an 'error' code
|
// // Handling unhandled promise rejections
|
||||||
});
|
// process.on("unhandledRejection", async (reason, promise) => {
|
||||||
}
|
// console.log("Handling unhandledRejection For Tasks Cleanup");
|
||||||
|
// await tasksEmailQueueCleanup();
|
||||||
|
// process.exit(1);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format the date for the email.
|
* Format the date for the email.
|
||||||
@@ -108,12 +96,13 @@ const getEndpoints = (bodyshop) =>
|
|||||||
: "https://romeonline.io"
|
: "https://romeonline.io"
|
||||||
});
|
});
|
||||||
|
|
||||||
const generateTemplateArgs = (title, priority, description, dueDate, bodyshop, job, taskId) => {
|
const generateTemplateArgs = (title, priority, description, dueDate, bodyshop, job, taskId, dateLine) => {
|
||||||
const endPoints = getEndpoints(bodyshop);
|
const endPoints = getEndpoints(bodyshop);
|
||||||
return {
|
return {
|
||||||
header: title,
|
header: title,
|
||||||
subHeader: `Body Shop: ${bodyshop.shopname} | Priority: ${formatPriority(priority)} ${formatDate(dueDate)}`,
|
subHeader: `Body Shop: ${bodyshop.shopname} | Priority: ${formatPriority(priority)} ${formatDate(dueDate)}`,
|
||||||
body: `Reference: ${job.ro_number || "N/A"} | ${job.ownr_co_nm ? job.ownr_co_nm : `${job.ownr_fn || ""} ${job.ownr_ln || ""}`.trim()} | ${`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""}`.trim()}<br>${description ? description.concat("<br>") : ""}<a href="${endPoints}/manage/tasks/alltasks?taskid=${taskId}">View this task.</a>`
|
body: `Reference: ${job.ro_number || "N/A"} | ${job.ownr_co_nm ? job.ownr_co_nm : `${job.ownr_fn || ""} ${job.ownr_ln || ""}`.trim()} | ${`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""}`.trim()}<br>${description ? description.concat("<br>") : ""}<a href="${endPoints}/manage/tasks/alltasks?taskid=${taskId}">View this task.</a>`,
|
||||||
|
dateLine
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -125,6 +114,7 @@ const generateTemplateArgs = (title, priority, description, dueDate, bodyshop, j
|
|||||||
* @param html
|
* @param html
|
||||||
* @param taskIds
|
* @param taskIds
|
||||||
* @param successCallback
|
* @param successCallback
|
||||||
|
* @param requestInstance
|
||||||
*/
|
*/
|
||||||
const sendMail = (type, to, subject, html, taskIds, successCallback, requestInstance) => {
|
const sendMail = (type, to, subject, html, taskIds, successCallback, requestInstance) => {
|
||||||
const fromEmails = InstanceManager({
|
const fromEmails = InstanceManager({
|
||||||
@@ -135,7 +125,7 @@ const sendMail = (type, to, subject, html, taskIds, successCallback, requestInst
|
|||||||
: "Rome Online <noreply@romeonline.io>"
|
: "Rome Online <noreply@romeonline.io>"
|
||||||
});
|
});
|
||||||
|
|
||||||
transporter.sendMail(
|
mailer.sendMail(
|
||||||
{
|
{
|
||||||
from: fromEmails,
|
from: fromEmails,
|
||||||
to,
|
to,
|
||||||
@@ -152,8 +142,6 @@ const sendMail = (type, to, subject, html, taskIds, successCallback, requestInst
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
// }
|
|
||||||
// });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -178,6 +166,8 @@ const taskAssignedEmail = async (req, res) => {
|
|||||||
id: newTask.id
|
id: newTask.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const dateLine = moment().tz(tasks_by_pk.bodyshop.timezone).format("M/DD/YYYY @ hh:mm a");
|
||||||
|
|
||||||
sendMail(
|
sendMail(
|
||||||
"assigned",
|
"assigned",
|
||||||
tasks_by_pk.assigned_to_employee.user_email,
|
tasks_by_pk.assigned_to_employee.user_email,
|
||||||
@@ -190,7 +180,8 @@ const taskAssignedEmail = async (req, res) => {
|
|||||||
newTask.due_date,
|
newTask.due_date,
|
||||||
tasks_by_pk.bodyshop,
|
tasks_by_pk.bodyshop,
|
||||||
tasks_by_pk.job,
|
tasks_by_pk.job,
|
||||||
newTask.id
|
newTask.id,
|
||||||
|
dateLine
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
null,
|
null,
|
||||||
@@ -247,7 +238,7 @@ const tasksRemindEmail = async (req, res) => {
|
|||||||
const fromEmails = InstanceManager({
|
const fromEmails = InstanceManager({
|
||||||
imex: "ImEX Online <noreply@imex.online>",
|
imex: "ImEX Online <noreply@imex.online>",
|
||||||
rome:
|
rome:
|
||||||
tasksRequest?.tasks[0].bodyshop.convenient_company === "promanager"
|
tasksRequest?.tasks[0].bodyshop.convenient_company === "promanager"
|
||||||
? "ProManager <noreply@promanager.web-est.com>"
|
? "ProManager <noreply@promanager.web-est.com>"
|
||||||
: "Rome Online <noreply@romeonline.io>"
|
: "Rome Online <noreply@romeonline.io>"
|
||||||
});
|
});
|
||||||
@@ -259,6 +250,8 @@ const tasksRemindEmail = async (req, res) => {
|
|||||||
|
|
||||||
const taskIds = groupedTasks[recipient.email].map((task) => task.id);
|
const taskIds = groupedTasks[recipient.email].map((task) => task.id);
|
||||||
|
|
||||||
|
const dateLine = moment().tz(tasksRequest?.tasks[0].bodyshop.timezone).format("M/DD/YYYY @ hh:mm a");
|
||||||
|
|
||||||
// There is only the one email to send to this author.
|
// There is only the one email to send to this author.
|
||||||
if (recipient.count === 1) {
|
if (recipient.count === 1) {
|
||||||
const onlyTask = groupedTasks[recipient.email][0];
|
const onlyTask = groupedTasks[recipient.email][0];
|
||||||
@@ -274,7 +267,8 @@ const tasksRemindEmail = async (req, res) => {
|
|||||||
onlyTask.due_date,
|
onlyTask.due_date,
|
||||||
onlyTask.bodyshop,
|
onlyTask.bodyshop,
|
||||||
onlyTask.job,
|
onlyTask.job,
|
||||||
onlyTask.id
|
onlyTask.id,
|
||||||
|
dateLine
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -283,7 +277,7 @@ const tasksRemindEmail = async (req, res) => {
|
|||||||
const endPoints = InstanceManager({
|
const endPoints = InstanceManager({
|
||||||
imex: process.env?.NODE_ENV === "test" ? "https://test.imex.online" : "https://imex.online",
|
imex: process.env?.NODE_ENV === "test" ? "https://test.imex.online" : "https://imex.online",
|
||||||
rome:
|
rome:
|
||||||
tasksRequest?.tasks[0].bodyshop.convenient_company === "promanager"
|
tasksRequest?.tasks[0].bodyshop.convenient_company === "promanager"
|
||||||
? process.env?.NODE_ENV === "test"
|
? process.env?.NODE_ENV === "test"
|
||||||
? "https//test.promanager.web-est.com"
|
? "https//test.promanager.web-est.com"
|
||||||
: "https://promanager.web-est.com"
|
: "https://promanager.web-est.com"
|
||||||
@@ -297,6 +291,7 @@ const tasksRemindEmail = async (req, res) => {
|
|||||||
emailData.html = generateEmailTemplate({
|
emailData.html = generateEmailTemplate({
|
||||||
header: `${allTasks.length} Tasks require your attention`,
|
header: `${allTasks.length} Tasks require your attention`,
|
||||||
subHeader: `Please click on the Tasks below to view the Task.`,
|
subHeader: `Please click on the Tasks below to view the Task.`,
|
||||||
|
dateLine,
|
||||||
body: `<ul>
|
body: `<ul>
|
||||||
${allTasks
|
${allTasks
|
||||||
.map((task) =>
|
.map((task) =>
|
||||||
@@ -335,6 +330,29 @@ const tasksRemindEmail = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Note: Uncomment this to test locally, it will call the remind_at email check every 20 seconds
|
||||||
|
// const callTaskRemindEmailInternally = () => {
|
||||||
|
// const req = {
|
||||||
|
// body: {
|
||||||
|
// // You can mock any request data here if needed
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const res = {
|
||||||
|
// status: (code) => {
|
||||||
|
// return {
|
||||||
|
// json: (data) => {
|
||||||
|
// console.log(`Response Status: ${code}`, data);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// // Call the taskRemindEmail function with mock req and res
|
||||||
|
// tasksRemindEmail(req, res);
|
||||||
|
// };
|
||||||
|
// setInterval(callTaskRemindEmailInternally, 20000);
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
taskAssignedEmail,
|
taskAssignedEmail,
|
||||||
tasksRemindEmail,
|
tasksRemindEmail,
|
||||||
|
|||||||
@@ -2489,6 +2489,7 @@ exports.QUERY_REMIND_TASKS = `
|
|||||||
bodyshop {
|
bodyshop {
|
||||||
shopname
|
shopname
|
||||||
convenient_company
|
convenient_company
|
||||||
|
timezone
|
||||||
}
|
}
|
||||||
bodyshopid
|
bodyshopid
|
||||||
}
|
}
|
||||||
@@ -2512,6 +2513,7 @@ query QUERY_TASK_BY_ID($id: uuid!) {
|
|||||||
bodyshop{
|
bodyshop{
|
||||||
shopname
|
shopname
|
||||||
convenient_company
|
convenient_company
|
||||||
|
timezone
|
||||||
}
|
}
|
||||||
job{
|
job{
|
||||||
ro_number
|
ro_number
|
||||||
|
|||||||
@@ -7,21 +7,32 @@ require("dotenv").config({
|
|||||||
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Emit this to bodyshop room
|
||||||
|
|
||||||
exports.default = async (req, res) => {
|
exports.default = async (req, res) => {
|
||||||
const { useremail, bodyshopid, operationName, variables, env, time, dbevent, user } = req.body;
|
const { useremail, bodyshopid, operationName, variables, env, time, dbevent, user } = req.body;
|
||||||
|
const {
|
||||||
|
ioRedis,
|
||||||
|
ioHelpers: { getBodyshopRoom }
|
||||||
|
} = req;
|
||||||
try {
|
try {
|
||||||
await client.request(queries.INSERT_IOEVENT, {
|
// await client.request(queries.INSERT_IOEVENT, {
|
||||||
event: {
|
// event: {
|
||||||
operationname: operationName,
|
// operationname: operationName,
|
||||||
time,
|
// time,
|
||||||
dbevent,
|
// dbevent,
|
||||||
env,
|
// env,
|
||||||
variables,
|
// variables,
|
||||||
bodyshopid,
|
// bodyshopid,
|
||||||
useremail
|
// useremail
|
||||||
}
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
ioRedis.to(getBodyshopRoom(bodyshopid)).emit("bodyshop-message", {
|
||||||
|
operationName,
|
||||||
|
useremail
|
||||||
});
|
});
|
||||||
|
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("ioevent-error", "trace", user, null, {
|
logger.log("ioevent-error", "trace", user, null, {
|
||||||
|
|||||||
32
server/job/job-updated.js
Normal file
32
server/job/job-updated.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const { isObject } = require("lodash");
|
||||||
|
|
||||||
|
const jobUpdated = async (req, res) => {
|
||||||
|
const { ioRedis, logger, ioHelpers } = req;
|
||||||
|
|
||||||
|
if (!req?.body?.event?.data?.new || !isObject(req?.body?.event?.data?.new)) {
|
||||||
|
logger.log("job-update-error", "ERROR", req.user?.email, null, {
|
||||||
|
message: `Malformed Job Update request sent from Hasura`,
|
||||||
|
body: req?.body
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
status: "error",
|
||||||
|
message: `Malformed Job Update request sent from Hasura`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log("job-update", "INFO", req.user?.email, null, {
|
||||||
|
message: `Job updated event received from Hasura`,
|
||||||
|
jobid: req?.body?.event?.data?.new?.id
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedJob = req.body.event.data.new;
|
||||||
|
const bodyshopID = updatedJob.shopid;
|
||||||
|
|
||||||
|
// Emit the job-updated event only to the room corresponding to the bodyshop
|
||||||
|
ioRedis.to(ioHelpers.getBodyshopRoom(bodyshopID)).emit("production-job-updated", updatedJob);
|
||||||
|
|
||||||
|
return res.json({ message: "Job updated and event emitted" });
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = jobUpdated;
|
||||||
@@ -14,3 +14,4 @@ exports.costing = require("./job-costing").JobCosting;
|
|||||||
exports.costingmulti = require("./job-costing").JobCostingMulti;
|
exports.costingmulti = require("./job-costing").JobCostingMulti;
|
||||||
exports.statustransition = require("./job-status-transition").statustransition;
|
exports.statustransition = require("./job-status-transition").statustransition;
|
||||||
exports.lifecycle = require("./job-lifecycle");
|
exports.lifecycle = require("./job-lifecycle");
|
||||||
|
exports.jobUpdated = require("./job-updated");
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const ppc = require("../ccc/partspricechange");
|
|||||||
const { partsScan } = require("../parts-scan/parts-scan");
|
const { partsScan } = require("../parts-scan/parts-scan");
|
||||||
const eventAuthorizationMiddleware = require("../middleware/eventAuthorizationMIddleware");
|
const eventAuthorizationMiddleware = require("../middleware/eventAuthorizationMIddleware");
|
||||||
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
||||||
const { totals, statustransition, totalsSsu, costing, lifecycle, costingmulti } = require("../job/job");
|
const { totals, statustransition, totalsSsu, costing, lifecycle, costingmulti, jobUpdated } = require("../job/job");
|
||||||
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
|
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
|
||||||
|
|
||||||
router.post("/totals", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, totals);
|
router.post("/totals", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, totals);
|
||||||
@@ -16,5 +16,6 @@ router.post("/lifecycle", validateFirebaseIdTokenMiddleware, withUserGraphQLClie
|
|||||||
router.post("/costingmulti", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, costingmulti);
|
router.post("/costingmulti", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, costingmulti);
|
||||||
router.post("/partsscan", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, partsScan);
|
router.post("/partsscan", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, partsScan);
|
||||||
router.post("/ppc", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, ppc.generatePpc);
|
router.post("/ppc", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, ppc.generatePpc);
|
||||||
|
router.post("/job-updated", eventAuthorizationMiddleware, jobUpdated);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
17
server/utils/ioHelpers.js
Normal file
17
server/utils/ioHelpers.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const applyIOHelpers = ({ app, api, io, logger }) => {
|
||||||
|
const getBodyshopRoom = (bodyshopID) => `bodyshop-broadcast-room:${bodyshopID}`;
|
||||||
|
|
||||||
|
const ioHelpersAPI = {
|
||||||
|
getBodyshopRoom
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper middleware
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
req.ioHelpers = ioHelpersAPI;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
return ioHelpersAPI;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { applyIOHelpers };
|
||||||
229
server/utils/redisHelpers.js
Normal file
229
server/utils/redisHelpers.js
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
/**
|
||||||
|
* Apply Redis helper functions
|
||||||
|
* @param pubClient
|
||||||
|
* @param app
|
||||||
|
* @param logger
|
||||||
|
*/
|
||||||
|
const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
||||||
|
// Store session data in Redis
|
||||||
|
const setSessionData = async (socketId, key, value) => {
|
||||||
|
try {
|
||||||
|
await pubClient.hset(`socket:${socketId}`, key, JSON.stringify(value)); // Use Redis pubClient
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(`Error Setting Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Retrieve session data from Redis
|
||||||
|
const getSessionData = async (socketId, key) => {
|
||||||
|
try {
|
||||||
|
const data = await pubClient.hget(`socket:${socketId}`, key);
|
||||||
|
return data ? JSON.parse(data) : null;
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(`Error Getting Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clear session data from Redis
|
||||||
|
const clearSessionData = async (socketId) => {
|
||||||
|
try {
|
||||||
|
await pubClient.del(`socket:${socketId}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(`Error Clearing Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store multiple session data in Redis
|
||||||
|
const setMultipleSessionData = async (socketId, keyValues) => {
|
||||||
|
try {
|
||||||
|
// keyValues is expected to be an object { key1: value1, key2: value2, ... }
|
||||||
|
const entries = Object.entries(keyValues).map(([key, value]) => [key, JSON.stringify(value)]);
|
||||||
|
await pubClient.hset(`socket:${socketId}`, ...entries.flat());
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(`Error Setting Multiple Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Retrieve multiple session data from Redis
|
||||||
|
const getMultipleSessionData = async (socketId, keys) => {
|
||||||
|
try {
|
||||||
|
const data = await pubClient.hmget(`socket:${socketId}`, keys);
|
||||||
|
// Redis returns an object with null values for missing keys, so we parse the non-null ones
|
||||||
|
return Object.fromEntries(keys.map((key, index) => [key, data[index] ? JSON.parse(data[index]) : null]));
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(`Error Getting Multiple Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setMultipleFromArraySessionData = async (socketId, keyValueArray) => {
|
||||||
|
try {
|
||||||
|
// Use Redis multi/pipeline to batch the commands
|
||||||
|
const multi = pubClient.multi();
|
||||||
|
keyValueArray.forEach(([key, value]) => {
|
||||||
|
multi.hset(`socket:${socketId}`, key, JSON.stringify(value));
|
||||||
|
});
|
||||||
|
await multi.exec(); // Execute all queued commands
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(`Error Setting Multiple Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to add an item to the end of the Redis list
|
||||||
|
const addItemToEndOfList = async (socketId, key, newItem) => {
|
||||||
|
try {
|
||||||
|
await pubClient.rpush(`socket:${socketId}:${key}`, JSON.stringify(newItem));
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(`Error adding item to the end of the list for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to add an item to the beginning of the Redis list
|
||||||
|
const addItemToBeginningOfList = async (socketId, key, newItem) => {
|
||||||
|
try {
|
||||||
|
await pubClient.lpush(`socket:${socketId}:${key}`, JSON.stringify(newItem));
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(`Error adding item to the beginning of the list for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to clear a list in Redis
|
||||||
|
const clearList = async (socketId, key) => {
|
||||||
|
try {
|
||||||
|
await pubClient.del(`socket:${socketId}:${key}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(`Error clearing list for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add methods to manage room users
|
||||||
|
const addUserToRoom = async (room, user) => {
|
||||||
|
try {
|
||||||
|
await pubClient.sadd(room, JSON.stringify(user));
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(`Error adding user to room ${room}: ${error}`, "ERROR", "redis");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeUserFromRoom = async (room, user) => {
|
||||||
|
try {
|
||||||
|
await pubClient.srem(room, JSON.stringify(user));
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(`Error removing user to room ${room}: ${error}`, "ERROR", "redis");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUsersInRoom = async (room) => {
|
||||||
|
try {
|
||||||
|
const users = await pubClient.smembers(room);
|
||||||
|
return users.map((user) => JSON.parse(user));
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(`Error getting users in room ${room}: ${error}`, "ERROR", "redis");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const api = {
|
||||||
|
setSessionData,
|
||||||
|
getSessionData,
|
||||||
|
clearSessionData,
|
||||||
|
setMultipleSessionData,
|
||||||
|
getMultipleSessionData,
|
||||||
|
setMultipleFromArraySessionData,
|
||||||
|
addItemToEndOfList,
|
||||||
|
addItemToBeginningOfList,
|
||||||
|
clearList,
|
||||||
|
addUserToRoom,
|
||||||
|
removeUserFromRoom,
|
||||||
|
getUsersInRoom
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(module.exports, api);
|
||||||
|
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
req.sessionUtils = api;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Demo to show how all the helper functions work
|
||||||
|
// const demoSessionData = async () => {
|
||||||
|
// const socketId = "testSocketId";
|
||||||
|
//
|
||||||
|
// // 1. Test setSessionData and getSessionData
|
||||||
|
// await setSessionData(socketId, "field1", "Hello, Redis!");
|
||||||
|
// const field1Value = await getSessionData(socketId, "field1");
|
||||||
|
// console.log("Retrieved single field value:", field1Value);
|
||||||
|
//
|
||||||
|
// // 2. Test setMultipleSessionData and getMultipleSessionData
|
||||||
|
// await setMultipleSessionData(socketId, { field2: "Second Value", field3: "Third Value" });
|
||||||
|
// const multipleFields = await getMultipleSessionData(socketId, ["field2", "field3"]);
|
||||||
|
// console.log("Retrieved multiple field values:", multipleFields);
|
||||||
|
//
|
||||||
|
// // 3. Test setMultipleFromArraySessionData
|
||||||
|
// await setMultipleFromArraySessionData(socketId, [
|
||||||
|
// ["field4", "Fourth Value"],
|
||||||
|
// ["field5", "Fifth Value"]
|
||||||
|
// ]);
|
||||||
|
//
|
||||||
|
// // Retrieve and log all fields
|
||||||
|
// const allFields = await getMultipleSessionData(socketId, ["field1", "field2", "field3", "field4", "field5"]);
|
||||||
|
// console.log("Retrieved all field values:", allFields);
|
||||||
|
//
|
||||||
|
// // 4. Test list functions
|
||||||
|
// // Add item to the end of a Redis list
|
||||||
|
// await addItemToEndOfList(socketId, "logEvents", { event: "Log Event 1", timestamp: new Date() });
|
||||||
|
// await addItemToEndOfList(socketId, "logEvents", { event: "Log Event 2", timestamp: new Date() });
|
||||||
|
//
|
||||||
|
// // Add item to the beginning of a Redis list
|
||||||
|
// await addItemToBeginningOfList(socketId, "logEvents", { event: "First Log Event", timestamp: new Date() });
|
||||||
|
//
|
||||||
|
// // Retrieve the entire list
|
||||||
|
// const logEventsData = await pubClient.lrange(`socket:${socketId}:logEvents`, 0, -1);
|
||||||
|
// const logEvents = logEventsData.map((item) => JSON.parse(item));
|
||||||
|
// console.log("Log Events List:", logEvents);
|
||||||
|
//
|
||||||
|
// // 5. Test clearList
|
||||||
|
// await clearList(socketId, "logEvents");
|
||||||
|
// console.log("Log Events List cleared.");
|
||||||
|
//
|
||||||
|
// // Retrieve the list after clearing to confirm it's empty
|
||||||
|
// const logEventsAfterClear = await pubClient.lrange(`socket:${socketId}:logEvents`, 0, -1);
|
||||||
|
// console.log("Log Events List after clearing:", logEventsAfterClear); // Should be an empty array
|
||||||
|
//
|
||||||
|
// // 6. Test clearSessionData
|
||||||
|
// await clearSessionData(socketId);
|
||||||
|
// console.log("Session data cleared.");
|
||||||
|
//
|
||||||
|
// // 7. Test room functions
|
||||||
|
// const roomName = "testRoom";
|
||||||
|
// const user1 = { id: 1, name: "Alice" };
|
||||||
|
// const user2 = { id: 2, name: "Bob" };
|
||||||
|
//
|
||||||
|
// // Add users to room
|
||||||
|
// await addUserToRoom(roomName, user1);
|
||||||
|
// await addUserToRoom(roomName, user2);
|
||||||
|
//
|
||||||
|
// // Get users in room
|
||||||
|
// const usersInRoom = await getUsersInRoom(roomName);
|
||||||
|
// console.log(`Users in room ${roomName}:`, usersInRoom);
|
||||||
|
//
|
||||||
|
// // Remove a user from room
|
||||||
|
// await removeUserFromRoom(roomName, user1);
|
||||||
|
//
|
||||||
|
// // Get users in room after removal
|
||||||
|
// const usersInRoomAfterRemoval = await getUsersInRoom(roomName);
|
||||||
|
// console.log(`Users in room ${roomName} after removal:`, usersInRoomAfterRemoval);
|
||||||
|
//
|
||||||
|
// // Clean up: remove remaining users from room
|
||||||
|
// await removeUserFromRoom(roomName, user2);
|
||||||
|
//
|
||||||
|
// // Verify room is empty
|
||||||
|
// const usersInRoomAfterCleanup = await getUsersInRoom(roomName);
|
||||||
|
// console.log(`Users in room ${roomName} after cleanup:`, usersInRoomAfterCleanup); // Should be empty
|
||||||
|
// };
|
||||||
|
// if (process.env.NODE_ENV === "development") {
|
||||||
|
// demoSessionData();
|
||||||
|
// }
|
||||||
|
|
||||||
|
return api;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { applyRedisHelpers };
|
||||||
143
server/web-sockets/redisSocketEvents.js
Normal file
143
server/web-sockets/redisSocketEvents.js
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
const { admin } = require("../firebase/firebase-handler");
|
||||||
|
|
||||||
|
const redisSocketEvents = ({
|
||||||
|
io,
|
||||||
|
redisHelpers: { setSessionData, clearSessionData }, // Note: Used if we persist user to Redis
|
||||||
|
ioHelpers: { getBodyshopRoom },
|
||||||
|
logger
|
||||||
|
}) => {
|
||||||
|
// Logging helper functions
|
||||||
|
const createLogEvent = (socket, level, message) => {
|
||||||
|
console.log(`[IOREDIS LOG EVENT] - ${socket?.user?.email} - ${socket.id} - ${message}`);
|
||||||
|
logger.log("ioredis-log-event", level, socket?.user?.email, null, { wsmessage: message });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Socket Auth Middleware
|
||||||
|
const authMiddleware = (socket, next) => {
|
||||||
|
try {
|
||||||
|
if (socket.handshake.auth.token) {
|
||||||
|
admin
|
||||||
|
.auth()
|
||||||
|
.verifyIdToken(socket.handshake.auth.token)
|
||||||
|
.then((user) => {
|
||||||
|
socket.user = user;
|
||||||
|
// Note: if we ever want to capture user data across sockets
|
||||||
|
// Uncomment the following line and then remove the next() to a second then()
|
||||||
|
// return setSessionData(socket.id, "user", user);
|
||||||
|
next();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
next(new Error(`Authentication error: ${error.message}`));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
next(new Error("Authentication error - no authorization token."));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Uncaught connection error:::", error);
|
||||||
|
logger.log("websocket-connection-error", "error", null, null, {
|
||||||
|
...error
|
||||||
|
});
|
||||||
|
next(new Error(`Authentication error ${error}`));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register Socket Events
|
||||||
|
const registerSocketEvents = (socket) => {
|
||||||
|
createLogEvent(socket, "DEBUG", `Registering RedisIO Socket Events.`);
|
||||||
|
|
||||||
|
// Token Update Events
|
||||||
|
const registerUpdateEvents = (socket) => {
|
||||||
|
const updateToken = async (newToken) => {
|
||||||
|
try {
|
||||||
|
// noinspection UnnecessaryLocalVariableJS
|
||||||
|
const user = await admin.auth().verifyIdToken(newToken, true);
|
||||||
|
socket.user = user;
|
||||||
|
|
||||||
|
// If We ever want to persist user Data across workers
|
||||||
|
// await setSessionData(socket.id, "user", user);
|
||||||
|
|
||||||
|
createLogEvent(socket, "INFO", "Token updated successfully");
|
||||||
|
|
||||||
|
socket.emit("token-updated", { success: true });
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === "auth/id-token-expired") {
|
||||||
|
createLogEvent(socket, "WARNING", "Stale token received, waiting for new token");
|
||||||
|
socket.emit("token-updated", {
|
||||||
|
success: false,
|
||||||
|
error: "Stale token."
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
createLogEvent(socket, "ERROR", `Token update failed: ${error.message}`);
|
||||||
|
socket.emit("token-updated", { success: false, error: error.message });
|
||||||
|
// For any other errors, optionally disconnect the socket
|
||||||
|
socket.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
socket.on("update-token", updateToken);
|
||||||
|
};
|
||||||
|
// Room Broadcast Events
|
||||||
|
const registerRoomAndBroadcastEvents = (socket) => {
|
||||||
|
const joinBodyshopRoom = (bodyshopUUID) => {
|
||||||
|
try {
|
||||||
|
const room = getBodyshopRoom(bodyshopUUID);
|
||||||
|
socket.join(room);
|
||||||
|
createLogEvent(socket, "DEBUG", `Client joined bodyshop room: ${room}`);
|
||||||
|
} catch (error) {
|
||||||
|
createLogEvent(socket, "ERROR", `Error joining room: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const leaveBodyshopRoom = (bodyshopUUID) => {
|
||||||
|
try {
|
||||||
|
const room = getBodyshopRoom(bodyshopUUID);
|
||||||
|
socket.leave(room);
|
||||||
|
createLogEvent(socket, "DEBUG", `Client left bodyshop room: ${room}`);
|
||||||
|
} catch (error) {
|
||||||
|
createLogEvent(socket, "ERROR", `Error joining room: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const broadcastToBodyshopRoom = (bodyshopUUID, message) => {
|
||||||
|
try {
|
||||||
|
const room = getBodyshopRoom(bodyshopUUID);
|
||||||
|
io.to(room).emit("bodyshop-message", message);
|
||||||
|
createLogEvent(socket, "DEBUG", `Broadcast message to bodyshop ${room}`);
|
||||||
|
} catch (error) {
|
||||||
|
createLogEvent(socket, "ERROR", `Error getting room: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on("join-bodyshop-room", joinBodyshopRoom);
|
||||||
|
socket.on("leave-bodyshop-room", leaveBodyshopRoom);
|
||||||
|
socket.on("broadcast-to-bodyshop", broadcastToBodyshopRoom);
|
||||||
|
};
|
||||||
|
// Disconnect Events
|
||||||
|
const registerDisconnectEvents = (socket) => {
|
||||||
|
const disconnect = () => {
|
||||||
|
createLogEvent(socket, "DEBUG", `User disconnected.`);
|
||||||
|
const rooms = Array.from(socket.rooms).filter((room) => room !== socket.id);
|
||||||
|
for (const room of rooms) {
|
||||||
|
socket.leave(room);
|
||||||
|
}
|
||||||
|
// If we ever want to persist the user across workers
|
||||||
|
// clearSessionData(socket.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on("disconnect", disconnect);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call Handlers
|
||||||
|
registerRoomAndBroadcastEvents(socket);
|
||||||
|
registerUpdateEvents(socket);
|
||||||
|
registerDisconnectEvents(socket);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Associate Middleware and Handlers
|
||||||
|
io.use(authMiddleware);
|
||||||
|
io.on("connection", registerSocketEvents);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
redisSocketEvents
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user