Compare commits
157 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b479684fe4 | ||
|
|
5b81912bd3 | ||
|
|
3c98a94c38 | ||
|
|
1d98de6d4d | ||
|
|
0ce5d9063a | ||
|
|
3b84e1d6ec | ||
|
|
d62f6e2116 | ||
|
|
71a26cc4ac | ||
|
|
32441e9406 | ||
|
|
e6dade1206 | ||
|
|
43d34cae07 | ||
|
|
a72a7948fe | ||
|
|
a24f6639a1 | ||
|
|
b2a0af32e9 | ||
|
|
cc58d14d32 | ||
|
|
9ce419b949 | ||
|
|
5053816be7 | ||
|
|
30ca34ea93 | ||
|
|
68d1a404b3 | ||
|
|
85e82b85ea | ||
|
|
23467280b4 | ||
|
|
aedad1c48f | ||
|
|
05cc4dd188 | ||
|
|
ea6351ea06 | ||
|
|
87d3ceb408 | ||
|
|
d08dd2b506 | ||
|
|
8a047d14a1 | ||
|
|
e103772aa4 | ||
|
|
c332699dc8 | ||
|
|
25e6e61d10 | ||
|
|
cdcd6b636a | ||
|
|
7879591bcf | ||
|
|
7fc6556866 | ||
|
|
3f5489ce7e | ||
|
|
5a90854861 | ||
|
|
8347a8c098 | ||
|
|
2bf074d85a | ||
|
|
50d47cd679 | ||
|
|
3a4e06eaa2 | ||
|
|
4be71726d4 | ||
|
|
c78db7eb08 | ||
|
|
e4dc711481 | ||
|
|
5114138c67 | ||
|
|
68b8743002 | ||
|
|
8f312bfffb | ||
|
|
7e7e109cfe | ||
|
|
05e5545466 | ||
|
|
ddb0990645 | ||
|
|
04dec6d91c | ||
|
|
a883b817b0 | ||
|
|
b7423aebf6 | ||
|
|
ee70aeb952 | ||
|
|
74d95e7cbb | ||
|
|
f6f6fab5ba | ||
|
|
699ffc822a | ||
|
|
4e35f5402c | ||
|
|
9b997d0924 | ||
|
|
d705f8211e | ||
|
|
03761bbb2a | ||
|
|
4d0794e90e | ||
|
|
e615c4a55b | ||
|
|
51eb3423f3 | ||
|
|
f6318666d9 | ||
|
|
544d4b8136 | ||
|
|
edf4846d55 | ||
|
|
f3754de843 | ||
|
|
3d920ad151 | ||
|
|
575f056360 | ||
|
|
716d9affb5 | ||
|
|
b01dd52da2 | ||
|
|
c75fddc2c0 | ||
|
|
db0c16f31d | ||
|
|
b286ab2439 | ||
|
|
fa57828ebd | ||
|
|
8052767002 | ||
|
|
932f572fb5 | ||
|
|
328a64eb90 | ||
|
|
c661fce8f1 | ||
|
|
60d1396011 | ||
|
|
3b647dfd37 | ||
|
|
50fe588949 | ||
|
|
0ced053d21 | ||
|
|
b8cf4a4d75 | ||
|
|
ff72657a82 | ||
|
|
92a96fdae6 | ||
|
|
b1a96d55ad | ||
|
|
49657816c6 | ||
|
|
7094b6ffbf | ||
|
|
ed7c2574eb | ||
|
|
45a9e37342 | ||
|
|
9e6a458203 | ||
|
|
55a279a700 | ||
|
|
82e2e332cf | ||
|
|
103d7c2bb2 | ||
|
|
f5f0b75617 | ||
|
|
c163554c3f | ||
|
|
bd75f593c2 | ||
|
|
fbc1866363 | ||
|
|
6480f7f2aa | ||
|
|
4cb3a79429 | ||
|
|
ca521eaeba | ||
|
|
f287ba2dac | ||
|
|
ff46bbbb3f | ||
|
|
cafca35500 | ||
|
|
9803841617 | ||
|
|
4dffbfe6fa | ||
|
|
6e61159608 | ||
|
|
3c85de3e34 | ||
|
|
1b5cddd371 | ||
|
|
6a7005299a | ||
|
|
1a4c9faab1 | ||
|
|
bfbf34e11d | ||
|
|
646754732d | ||
|
|
439d9e7b74 | ||
|
|
464f7044f0 | ||
|
|
7cde2f64af | ||
|
|
f674fff930 | ||
|
|
efc1157653 | ||
|
|
0677712d6e | ||
|
|
2e106a5d07 | ||
|
|
da7b97042e | ||
|
|
f018a2b2a6 | ||
|
|
c3f7d7bad2 | ||
|
|
70d857bfec | ||
|
|
f3265901b6 | ||
|
|
7c8ac50426 | ||
|
|
8ad39fe855 | ||
|
|
13b6218c43 | ||
|
|
bece3278f4 | ||
|
|
4c0a1960ad | ||
|
|
47324422a6 | ||
|
|
3b1da6901d | ||
|
|
fc6ec54233 | ||
|
|
64928d0849 | ||
|
|
56a580b1e7 | ||
|
|
f7af3b407b | ||
|
|
9a0674f5d7 | ||
|
|
cc30ea658e | ||
|
|
59869def31 | ||
|
|
a5d3f2caf1 | ||
|
|
4ad87a522c | ||
|
|
453812222b | ||
|
|
145cf7cc93 | ||
|
|
c09e22ed96 | ||
|
|
cdb2d4d2d6 | ||
|
|
29f0031c1e | ||
|
|
e8099e130a | ||
|
|
1cbca1ddf0 | ||
|
|
eeed004fe2 | ||
|
|
5a180b86fb | ||
|
|
e3059b41ae | ||
|
|
1a5c71048c | ||
|
|
fc4e97c9b5 | ||
|
|
1bb2212e4a | ||
|
|
0e9ad1258d | ||
|
|
2a33f462a3 | ||
|
|
cbc164dbeb |
24
.dockerignore
Normal file
24
.dockerignore
Normal file
@@ -0,0 +1,24 @@
|
||||
# Directories to exclude
|
||||
.circleci
|
||||
.idea
|
||||
.platform
|
||||
.vscode
|
||||
_reference
|
||||
client
|
||||
redis/dockerdata
|
||||
hasura
|
||||
node_modules
|
||||
# Files to exclude
|
||||
.ebignore
|
||||
.editorconfig
|
||||
.eslintrc.json
|
||||
.gitignore
|
||||
.prettierrc.js
|
||||
Dockerfile
|
||||
README.MD
|
||||
bodyshop_translations.babel
|
||||
docker-compose.yml
|
||||
ecosystem.config.js
|
||||
|
||||
# Optional: Exclude logs and temporary files
|
||||
*.log
|
||||
0
.localstack/.gitkeep
Normal file
0
.localstack/.gitkeep
Normal file
15
.vscode/launch.json
vendored
15
.vscode/launch.json
vendored
@@ -14,6 +14,21 @@
|
||||
"request": "launch",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceRoot}/client/src"
|
||||
},
|
||||
{
|
||||
"name": "Attach to Node.js in Docker",
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"address": "localhost",
|
||||
"port": 9229,
|
||||
"localRoot": "${workspaceFolder}",
|
||||
"remoteRoot": "/app",
|
||||
"protocol": "inspector",
|
||||
"restart": true,
|
||||
"sourceMaps": true,
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
47
Dockerfile
Normal file
47
Dockerfile
Normal file
@@ -0,0 +1,47 @@
|
||||
# Use Amazon Linux 2023 as the base image
|
||||
FROM amazonlinux:2023
|
||||
|
||||
# Install Git and Node.js (Amazon Linux 2023 uses the DNF package manager)
|
||||
RUN dnf install -y git \
|
||||
&& curl -sL https://rpm.nodesource.com/setup_20.x | bash - \
|
||||
&& dnf install -y nodejs \
|
||||
&& dnf clean all
|
||||
|
||||
|
||||
# Install dependencies required by node-canvas
|
||||
RUN dnf install -y \
|
||||
gcc \
|
||||
gcc-c++ \
|
||||
cairo-devel \
|
||||
pango-devel \
|
||||
libjpeg-turbo-devel \
|
||||
giflib-devel \
|
||||
libpng-devel \
|
||||
make \
|
||||
python3 \
|
||||
python3-pip \
|
||||
&& dnf clean all
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# This is because our test route uses a git commit hash
|
||||
RUN git config --global --add safe.directory /app
|
||||
|
||||
# Copy package.json and package-lock.json
|
||||
COPY package.json ./
|
||||
|
||||
# Install Nodemon
|
||||
RUN npm install -g nodemon
|
||||
|
||||
# Install dependencies
|
||||
RUN npm i --no-package-lock
|
||||
|
||||
# Copy the rest of your application code
|
||||
COPY . .
|
||||
|
||||
# Expose the port your app runs on (adjust if necessary)
|
||||
EXPOSE 4000 9229
|
||||
|
||||
# Start the application
|
||||
CMD ["nodemon", "--legacy-watch", "--inspect=0.0.0.0:9229", "server.js"]
|
||||
64
_reference/dockerreadme.md
Normal file
64
_reference/dockerreadme.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Setting up External Networking and Static IP for WSL2 using Hyper-V
|
||||
|
||||
This guide will walk you through the steps to configure your WSL2 (Windows Subsystem for Linux) instance to use an external Hyper-V virtual switch, enabling it to connect directly to your local network. Additionally, you'll learn how to assign a static IP address to your WSL2 instance.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Windows 11**
|
||||
2. **Docker Desktop For Windows (Latest Version)
|
||||
|
||||
# Docker Setup
|
||||
Inside the root of the project exists the `docker-compose.yaml` file, you can simply run
|
||||
`docker-compose up` to launch the backend.
|
||||
|
||||
Things to note:
|
||||
- When installing NPM packages, you will need to rebuild the `node-app` container
|
||||
- Making changes to the server files will restart the `node-app`
|
||||
|
||||
# Local Stack
|
||||
- LocalStack Front end (Optional) - https://apps.microsoft.com/detail/9ntrnft9zws2?hl=en-us&gl=US
|
||||
- http://localhost:4566/_aws/ses will allow you to see emails sent
|
||||
|
||||
# Docker Commands
|
||||
|
||||
## General `docker-compose` Commands:
|
||||
1. Bring up the services, force a rebuild of all services, and do not use the cache: `docker-compose up --build --no-cache`
|
||||
2. Start Containers in Detached Mode: This will run the containers in the background (detached mode): `docker-compose up -d`
|
||||
3. Stop and Remove Containers: Stops and removes the containers gracefully: `docker-compose down`
|
||||
4. Stop containers without removing them: `docker-compose stop`
|
||||
5. Remove Containers, Volumes, and Networks: `docker-compose down --volumes`
|
||||
6. Force rebuild of containers: `docker-compose build --no-cache`
|
||||
7. View running Containers: `docker-compose ps`
|
||||
8. View a specific containers logs: `docker-compose logs <container-name>`
|
||||
9. Scale services (multiple instances of a service): `docker-compose up --scale <container-name>=<instances number> -d`
|
||||
10. Watch a specific containers logs in realtime with timestamps: `docker-compose logs -f --timestamps <container-name>`
|
||||
|
||||
## Volume Management Commands
|
||||
1. List Docker volumes: `docker volume ls`
|
||||
2. Remove Unused volumes `docker volume prune`
|
||||
3. Remove specific volumes `docker volume rm <volume-name>`
|
||||
4. Inspect a volume: `docker volume inspect <volume-name>`
|
||||
|
||||
## Container Image Management Commands:
|
||||
1. List running containers: `docker ps`
|
||||
2. List all containers: `docker os -a`
|
||||
3. Remove Stopped containers: `docker container prune`
|
||||
4. Remove a specific container: `docker container rm <container-name>`
|
||||
5. Remove a specific image: `docker rmi <image-name>:<version>`
|
||||
6. Remove all unused images: `docker image prune -a`
|
||||
|
||||
## Network Management Commands:
|
||||
1. List networks: `docker network ls`
|
||||
2. Inspect a specific network: `docker network inspect <network-name>`
|
||||
3. Remove a specific network: `docker network rm <network-name>`
|
||||
4. Remove unused networks: `docker network prune`
|
||||
|
||||
## Debugging and maintenance:
|
||||
1. Enter a Running container: `docker exec -it <container name> /bin/bash` (could also be `/bin/sh` or for example `redis-cli` on a redis node)
|
||||
2. View container resource usage: `docker stats`
|
||||
3. Check Disk space used by Docker: `docker system df`
|
||||
4. Remove all unused Data (Nuclear option): `docker system prune`
|
||||
|
||||
## Specific examples
|
||||
1. To simulate a Clean state, one should run `docker system prune` followed by `docker volume prune -a`
|
||||
2. You can run `docker-compose up` without the `-d` option, and you will get what is identical to the experience you were used to, this includes being able to control-c and bring the entire stack down
|
||||
59
_reference/prHelper.html
Normal file
59
_reference/prHelper.html
Normal file
@@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>IMEX IO Extractor</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
.output-box {
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
background-color: #f9f9f9;
|
||||
min-height: 40px;
|
||||
}
|
||||
.copy-button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>IMEX IO Extractor</h1>
|
||||
<textarea id="inputText" placeholder="Paste your text here..."></textarea>
|
||||
<br>
|
||||
<button onclick="extractIO()">Extract</button>
|
||||
|
||||
<div class="output-box" id="outputBox" contenteditable="true"></div>
|
||||
<button class="copy-button" onclick="copyToClipboard()">Copy to Clipboard</button>
|
||||
|
||||
<script>
|
||||
function extractIO() {
|
||||
const inputText = document.getElementById('inputText').value;
|
||||
const ioNumbers = [...new Set(inputText.match(/IO-\d{4}/g))] // Extract unique IO-#### matches
|
||||
.map(io => ({ io, num: parseInt(io.split('-')[1]) })) // Extract number part for sorting
|
||||
.sort((a, b) => a.num - b.num) // Sort by the number
|
||||
.map(item => item.io); // Extract sorted IO-####
|
||||
|
||||
document.getElementById('outputBox').innerText = ioNumbers.join(', '); // Display horizontally
|
||||
}
|
||||
|
||||
function copyToClipboard() {
|
||||
const outputBox = document.getElementById('outputBox');
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(outputBox);
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
document.execCommand('copy');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,4 +1,4 @@
|
||||
<babeledit_project be_version="2.7.1" version="1.2">
|
||||
<babeledit_project version="1.2" be_version="2.7.1">
|
||||
<!--
|
||||
|
||||
BabelEdit project file
|
||||
@@ -22361,6 +22361,27 @@
|
||||
<folder_node>
|
||||
<name>buttons</name>
|
||||
<children>
|
||||
<concept_node>
|
||||
<name>create_short_link</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>goback</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
|
||||
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_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
|
||||
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
||||
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000
|
||||
VITE_APP_AXIOS_BASE_API_URL=/api/
|
||||
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
|
||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||
VITE_APP_INSTANCE=IMEX
|
||||
|
||||
@@ -8,7 +8,7 @@ VITE_APP_CLOUDINARY_API_KEY=957865933348715
|
||||
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
||||
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
|
||||
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
||||
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000
|
||||
VITE_APP_AXIOS_BASE_API_URL=/api/
|
||||
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
|
||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||
VITE_APP_INSTANCE=PROMANAGER
|
||||
|
||||
@@ -9,7 +9,7 @@ VITE_APP_CLOUDINARY_API_KEY=957865933348715
|
||||
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
||||
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BP1B7ZTYpn-KMt6nOxlld6aS8Skt3Q7ZLEqP0hAvGHxG4UojPYiXZ6kPlzZkUC5jH-EcWXomTLtmadAIxurfcHo'
|
||||
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
||||
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000
|
||||
VITE_APP_AXIOS_BASE_API_URL=/api/
|
||||
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
|
||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||
VITE_APP_COUNTRY=USA
|
||||
|
||||
1
client/.gitignore
vendored
1
client/.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
|
||||
# Sentry Config File
|
||||
.sentryclirc
|
||||
/dev-dist
|
||||
|
||||
@@ -12,6 +12,6 @@ module.exports = defineConfig({
|
||||
setupNodeEvents(on, config) {
|
||||
return require("./cypress/plugins/index.js")(on, config);
|
||||
},
|
||||
baseUrl: "http://localhost:3000"
|
||||
baseUrl: "https://localhost:3000"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="theme-color" content="#1690ff"/>
|
||||
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
|
||||
<!-- TODO:AIo Update the individual logos for each.-->
|
||||
<link rel="apple-touch-icon" href="public/logo192.png"/>
|
||||
<link rel="apple-touch-icon" href="/logo192.png"/>
|
||||
<link rel="mask-icon" href="/mask-icon.svg" color="#FFFFFF">
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
|
||||
2124
client/package-lock.json
generated
2124
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,37 +9,37 @@
|
||||
"proxy": "http://localhost:4000",
|
||||
"dependencies": {
|
||||
"@ant-design/pro-layout": "^7.19.12",
|
||||
"@apollo/client": "^3.11.4",
|
||||
"@emotion/is-prop-valid": "^1.3.0",
|
||||
"@fingerprintjs/fingerprintjs": "^4.4.3",
|
||||
"@apollo/client": "^3.11.8",
|
||||
"@emotion/is-prop-valid": "^1.3.1",
|
||||
"@fingerprintjs/fingerprintjs": "^4.5.0",
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.2.7",
|
||||
"@sentry/cli": "^2.33.1",
|
||||
"@sentry/cli": "^2.36.2",
|
||||
"@sentry/react": "^7.114.0",
|
||||
"@splitsoftware/splitio-react": "^1.12.1",
|
||||
"@splitsoftware/splitio-react": "^1.13.0",
|
||||
"@tanem/react-nprogress": "^5.0.51",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"antd": "^5.20.1",
|
||||
"apollo-link-logger": "^2.0.1",
|
||||
"apollo-link-sentry": "^3.3.0",
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.7.4",
|
||||
"axios": "^1.7.7",
|
||||
"classnames": "^2.5.1",
|
||||
"css-box-model": "^1.2.1",
|
||||
"dayjs": "^1.11.12",
|
||||
"dayjs": "^1.11.13",
|
||||
"dayjs-business-days2": "^1.2.2",
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"env-cmd": "^10.1.0",
|
||||
"exifr": "^7.1.3",
|
||||
"firebase": "^10.12.5",
|
||||
"firebase": "^10.13.2",
|
||||
"graphql": "^16.9.0",
|
||||
"i18next": "^23.12.3",
|
||||
"i18next": "^23.15.1",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"libphonenumber-js": "^1.11.5",
|
||||
"libphonenumber-js": "^1.11.9",
|
||||
"logrocket": "^8.1.2",
|
||||
"markerjs2": "^2.32.1",
|
||||
"markerjs2": "^2.32.2",
|
||||
"memoize-one": "^6.0.0",
|
||||
"normalize-url": "^8.0.1",
|
||||
"object-hash": "^3.0.0",
|
||||
@@ -47,7 +47,7 @@
|
||||
"query-string": "^9.1.0",
|
||||
"raf-schd": "^4.0.3",
|
||||
"react": "^18.3.1",
|
||||
"react-big-calendar": "^1.13.2",
|
||||
"react-big-calendar": "^1.14.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-cookie": "^7.2.0",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -58,15 +58,15 @@
|
||||
"react-icons": "^5.3.0",
|
||||
"react-image-lightbox": "^5.1.4",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-number-format": "^5.4.0",
|
||||
"react-number-format": "^5.4.2",
|
||||
"react-popopo": "^2.1.9",
|
||||
"react-product-fruits": "^2.2.6",
|
||||
"react-product-fruits": "^2.2.61",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-resizable": "^3.0.5",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"react-router-dom": "^6.26.2",
|
||||
"react-sticky": "^6.0.3",
|
||||
"react-virtualized": "^9.22.5",
|
||||
"react-virtuoso": "^4.10.1",
|
||||
"react-virtuoso": "^4.10.4",
|
||||
"recharts": "^2.12.7",
|
||||
"redux": "^5.0.1",
|
||||
"redux-actions": "^3.0.3",
|
||||
@@ -74,22 +74,26 @@
|
||||
"redux-saga": "^1.3.0",
|
||||
"redux-state-sync": "^3.1.4",
|
||||
"reselect": "^5.1.1",
|
||||
"sass": "^1.77.8",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"styled-components": "^6.1.12",
|
||||
"sass": "^1.79.3",
|
||||
"socket.io-client": "^4.8.0",
|
||||
"styled-components": "^6.1.13",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
"use-memo-one": "^1.1.3",
|
||||
"userpilot": "^1.3.5",
|
||||
"userpilot": "^1.3.6",
|
||||
"vite-plugin-ejs": "^1.7.0",
|
||||
"web-vitals": "^3.5.2"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "echo 'when updating react-big-calendar, remember to check to localizer in the calendar wrapper'",
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||
"start": "vite",
|
||||
"build": "dotenvx run --env-file=.env.development.imex -- vite build",
|
||||
"start:imex": "dotenvx run --env-file=.env.development.imex -- vite",
|
||||
"start:rome": "dotenvx run --env-file=.env.development.rome -- vite",
|
||||
"start:promanager": "dotenvx run --env-file=.env.development.promanager -- vite",
|
||||
"preview:imex": "dotenvx run --env-file=.env.development.imex -- vite preview",
|
||||
"preview:rome": "dotenvx run --env-file=.env.development.rome -- vite preview",
|
||||
"preview:promanager": "dotenvx run --env-file=.env.development.promanager -- vite preview",
|
||||
"build:test:imex": "env-cmd -f .env.test.imex npm run build",
|
||||
"build:test:rome": "env-cmd -f .env.test.rome npm run build",
|
||||
"build:test:promanager": "env-cmd -f .env.test.promanager npm run build",
|
||||
@@ -130,29 +134,29 @@
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-react": "^7.24.7",
|
||||
"@dotenvx/dotenvx": "^1.7.0",
|
||||
"@dotenvx/dotenvx": "^1.14.1",
|
||||
"@emotion/babel-plugin": "^11.12.0",
|
||||
"@emotion/react": "^11.13.0",
|
||||
"@sentry/webpack-plugin": "^2.22.2",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@sentry/webpack-plugin": "^2.22.4",
|
||||
"@testing-library/cypress": "^10.0.2",
|
||||
"browserslist": "^4.23.3",
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
"chalk": "^5.3.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "^13.13.3",
|
||||
"cypress": "^13.14.2",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-cypress": "^2.15.1",
|
||||
"memfs": "^4.11.1",
|
||||
"memfs": "^4.12.0",
|
||||
"os-browserify": "^0.3.0",
|
||||
"react-error-overlay": "6.0.11",
|
||||
"redux-logger": "^3.0.6",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"vite": "^5.4.0",
|
||||
"vite": "^5.4.7",
|
||||
"vite-plugin-babel": "^1.2.0",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-legacy": "^2.1.0",
|
||||
"vite-plugin-node-polyfills": "^0.22.0",
|
||||
"vite-plugin-pwa": "^0.20.1",
|
||||
"vite-plugin-pwa": "^0.20.5",
|
||||
"vite-plugin-style-import": "^2.0.0",
|
||||
"workbox-window": "^7.1.0"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import "./App.styles.scss";
|
||||
import Eula from "../components/eula/eula.component";
|
||||
import InstanceRenderMgr from "../utils/instanceRenderMgr";
|
||||
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
|
||||
import { SocketProvider } from "../contexts/SocketIO/socketContext.jsx";
|
||||
|
||||
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
|
||||
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
|
||||
@@ -201,7 +202,9 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
||||
path="/manage/*"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||
<SocketProvider bodyshop={bodyshop}>
|
||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||
</SocketProvider>
|
||||
</ErrorBoundary>
|
||||
}
|
||||
>
|
||||
@@ -211,7 +214,9 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
||||
path="/tech/*"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||
<SocketProvider bodyshop={bodyshop}>
|
||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||
</SocketProvider>
|
||||
</ErrorBoundary>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { exportPageLimit } from "../../utils/config";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
|
||||
@@ -175,7 +175,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
|
||||
<Table
|
||||
loading={loading}
|
||||
dataSource={dataSource}
|
||||
pagination={{ position: "top", pageSize: pageLimit }}
|
||||
pagination={{ position: "top", pageSize: exportPageLimit }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
onChange={handleTableChange}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { exportPageLimit } from "../../utils/config";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
@@ -177,7 +177,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, payments,
|
||||
<Table
|
||||
loading={loading}
|
||||
dataSource={dataSource}
|
||||
pagination={{ position: "top", pageSize: pageLimit }}
|
||||
pagination={{ position: "top", pageSize: exportPageLimit }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
onChange={handleTableChange}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Button, Card, Input, Space, Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { exportPageLimit } from "../../utils/config";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
||||
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import JobMarkSelectedExported from "../jobs-mark-selected-exported/jobs-mark-selected-exported";
|
||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
@@ -201,7 +201,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
|
||||
<Table
|
||||
loading={loading}
|
||||
dataSource={dataSource}
|
||||
pagination={{ position: "top" }}
|
||||
pagination={{ position: "top", pageSize: exportPageLimit }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
onChange={handleTableChange}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { DeleteFilled, CopyFilled } from "@ant-design/icons";
|
||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||
import { Button, Card, Col, Form, Input, Row, Space, Spin, Statistic, notification } from "antd";
|
||||
import { Button, Card, Col, Form, Input, Row, Space, Spin, Statistic, message, notification } from "antd";
|
||||
import axios from "axios";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -14,10 +14,12 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
||||
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
|
||||
import { getCurrentUser } from "../../firebase/firebase.utils";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
cardPaymentModal: selectCardPayment,
|
||||
bodyshop: selectBodyshop
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: getCurrentUser
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
@@ -25,11 +27,17 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment"))
|
||||
});
|
||||
|
||||
const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisible, insertAuditTrail }) => {
|
||||
const CardPaymentModalComponent = ({
|
||||
bodyshop,
|
||||
currentUser,
|
||||
cardPaymentModal,
|
||||
toggleModalVisible,
|
||||
insertAuditTrail
|
||||
}) => {
|
||||
const { context, actions } = cardPaymentModal;
|
||||
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [paymentLink, setPaymentLink] = useState();
|
||||
const [loading, setLoading] = useState(false);
|
||||
// const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
||||
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
|
||||
@@ -37,7 +45,7 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
|
||||
const [, { data, refetch, queryLoading }] = useLazyQuery(QUERY_RO_AND_OWNER_BY_JOB_PKS, {
|
||||
variables: { jobids: [context.jobid] },
|
||||
skip: true
|
||||
skip: !context?.jobid
|
||||
});
|
||||
|
||||
//Initialize the intellipay window.
|
||||
@@ -51,8 +59,7 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
//2024-04-25: Nothing is going to happen here anymore. We'll completely rely on the callback.
|
||||
//Add a slight delay to allow the refetch to properly get the data.
|
||||
setTimeout(() => {
|
||||
if (actions && actions.refetch && typeof actions.refetch === "function")
|
||||
actions.refetch();
|
||||
if (actions && actions.refetch && typeof actions.refetch === "function") actions.refetch();
|
||||
setLoading(false);
|
||||
toggleModalVisible();
|
||||
}, 750);
|
||||
@@ -86,7 +93,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const handleIntelliPayCharge = async () => {
|
||||
setLoading(true);
|
||||
//Validate
|
||||
@@ -101,7 +107,7 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
const response = await axios.post("/intellipay/lightbox_credentials", {
|
||||
bodyshop,
|
||||
refresh: !!window.intellipay,
|
||||
paymentSplitMeta: form.getFieldsValue(),
|
||||
paymentSplitMeta: form.getFieldsValue()
|
||||
});
|
||||
|
||||
if (window.intellipay) {
|
||||
@@ -126,6 +132,42 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
}
|
||||
};
|
||||
|
||||
const handleIntelliPayChargeShortLink = async () => {
|
||||
setLoading(true);
|
||||
//Validate
|
||||
try {
|
||||
await form.validateFields();
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { payments } = form.getFieldsValue();
|
||||
const response = await axios.post("/intellipay/generate_payment_url", {
|
||||
bodyshop,
|
||||
amount: payments?.reduce((acc, val) => {
|
||||
return acc + (val?.amount || 0);
|
||||
}, 0),
|
||||
account: payments && data && data.jobs.length > 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null,
|
||||
comment: btoa(JSON.stringify({ payments, userEmail: currentUser.email })),
|
||||
paymentSplitMeta: form.getFieldsValue()
|
||||
});
|
||||
if (response.data) {
|
||||
setPaymentLink(response.data?.shorUrl);
|
||||
navigator.clipboard.writeText(response.data?.shorUrl);
|
||||
message.success(t("general.actions.copied"));
|
||||
}
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
notification.open({
|
||||
type: "error",
|
||||
message: t("job_payments.notifications.error.openingip")
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title="Card Payment">
|
||||
<Spin spinning={loading}>
|
||||
@@ -202,16 +244,14 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
|
||||
<Form.Item
|
||||
shouldUpdate={(prevValues, curValues) =>
|
||||
prevValues.payments?.map((p) => p?.jobid).join() !== curValues.payments?.map((p) => p?.jobid).join()
|
||||
prevValues.payments?.map((p) => p?.jobid + p?.amount).join() !==
|
||||
curValues.payments?.map((p) => p?.jobid + p?.amount).join()
|
||||
}
|
||||
>
|
||||
{() => {
|
||||
//If all of the job ids have been fileld in, then query and update the IP field.
|
||||
const { payments } = form.getFieldsValue();
|
||||
if (
|
||||
payments?.length > 0 &&
|
||||
payments?.filter((p) => p?.jobid).length === payments?.length
|
||||
) {
|
||||
if (payments?.length > 0 && payments?.filter((p) => p?.jobid).length === payments?.length) {
|
||||
refetch({ jobids: payments.map((p) => p.jobid) });
|
||||
}
|
||||
return (
|
||||
@@ -246,7 +286,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
const totalAmountToCharge = payments?.reduce((acc, val) => {
|
||||
return acc + (val?.amount || 0);
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<Space style={{ float: "right" }}>
|
||||
<Statistic title="Amount To Charge" value={totalAmountToCharge} precision={2} />
|
||||
@@ -273,11 +312,36 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
>
|
||||
{t("job_payments.buttons.proceedtopayment")}
|
||||
</Button>
|
||||
<Space direction="vertical" align="center">
|
||||
<Button
|
||||
type="primary"
|
||||
// data-ipayname="submit"
|
||||
className="ipayfield"
|
||||
loading={queryLoading || loading}
|
||||
disabled={!(totalAmountToCharge > 0)}
|
||||
onClick={handleIntelliPayChargeShortLink}
|
||||
>
|
||||
{t("job_payments.buttons.create_short_link")}
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
{paymentLink && (
|
||||
<Space
|
||||
style={{ cursor: "pointer", float: "right" }}
|
||||
align="end"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(paymentLink);
|
||||
message.success(t("general.actions.copied"));
|
||||
}}
|
||||
>
|
||||
<div>{paymentLink}</div>
|
||||
<CopyFilled />
|
||||
</Space>
|
||||
)}
|
||||
</Spin>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -3,13 +3,15 @@ import axios from "axios";
|
||||
import _ from "lodash";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||
|
||||
export default function GlobalSearchOs() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState(false);
|
||||
|
||||
@@ -177,7 +179,18 @@ export default function GlobalSearchOs() {
|
||||
};
|
||||
|
||||
return (
|
||||
<AutoComplete options={data} onSearch={handleSearch} defaultActiveFirstOption onClear={() => setData([])}>
|
||||
<AutoComplete
|
||||
options={data}
|
||||
onSearch={handleSearch}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key !== "Enter") return;
|
||||
const firstUrlForSearch = data?.[0]?.options?.[0]?.label?.props?.to;
|
||||
if (!firstUrlForSearch) return;
|
||||
navigate(firstUrlForSearch);
|
||||
}}
|
||||
defaultActiveFirstOption
|
||||
onClear={() => setData([])}
|
||||
>
|
||||
<Input.Search
|
||||
size="large"
|
||||
placeholder={t("general.labels.globalsearch")}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AutoComplete, Divider, Input, Space } from "antd";
|
||||
import _ from "lodash";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries";
|
||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
@@ -13,6 +13,7 @@ import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.compon
|
||||
export default function GlobalSearch() {
|
||||
const { t } = useTranslation();
|
||||
const [callSearch, { loading, error, data }] = useLazyQuery(GLOBAL_SEARCH_QUERY);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const executeSearch = (v) => {
|
||||
if (v && v.variables.search && v.variables.search !== "" && v.variables.search.length >= 3) callSearch(v);
|
||||
@@ -20,7 +21,6 @@ export default function GlobalSearch() {
|
||||
const debouncedExecuteSearch = _.debounce(executeSearch, 750);
|
||||
|
||||
const handleSearch = (value) => {
|
||||
console.log("Handle Search");
|
||||
debouncedExecuteSearch({ variables: { search: value } });
|
||||
};
|
||||
|
||||
@@ -156,7 +156,17 @@ export default function GlobalSearch() {
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
return (
|
||||
<AutoComplete options={options} onSearch={handleSearch} defaultActiveFirstOption>
|
||||
<AutoComplete
|
||||
options={options}
|
||||
onSearch={handleSearch}
|
||||
defaultActiveFirstOption
|
||||
onKeyDown={(e) => {
|
||||
if (e.key !== "Enter") return;
|
||||
const firstUrlForSearch = options?.[0]?.options?.[0]?.label?.props?.to;
|
||||
if (!firstUrlForSearch) return;
|
||||
navigate(firstUrlForSearch);
|
||||
}}
|
||||
>
|
||||
<Input.Search
|
||||
size="large"
|
||||
placeholder={t("general.labels.globalsearch")}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { notification } from "antd";
|
||||
import Axios from "axios";
|
||||
import Dinero from "dinero.js";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -7,13 +10,10 @@ import { createStructuredSelector } from "reselect";
|
||||
import { INSERT_NEW_JOB_LINE, UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectJobLineEditModal } from "../../redux/modals/modals.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CriticalPartsScan from "../../utils/criticalPartsScan";
|
||||
import UndefinedToNull from "../../utils/undefinedtonull";
|
||||
import JobLinesUpdsertModal from "./job-lines-upsert-modal.component";
|
||||
import Axios from "axios";
|
||||
import Dinero from "dinero.js";
|
||||
import CriticalPartsScan from "../../utils/criticalPartsScan";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobLineEditModal: selectJobLineEditModal,
|
||||
@@ -82,13 +82,15 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo
|
||||
variables: {
|
||||
lineId: jobLineEditModal.context.id,
|
||||
line: {
|
||||
...values,
|
||||
prt_dsmk_m: Dinero({
|
||||
amount: Math.round(values.act_price * 100)
|
||||
...UndefinedToNull({
|
||||
...values,
|
||||
prt_dsmk_m: Dinero({
|
||||
amount: Math.round(values.act_price * 100)
|
||||
})
|
||||
.percentage(Math.abs(values.prt_dsmk_p || 0))
|
||||
.multiply(values.prt_dsmk_p >= 0 ? 1 : -1)
|
||||
.toFormat(0.0)
|
||||
})
|
||||
.percentage(Math.abs(values.prt_dsmk_p || 0))
|
||||
.multiply(values.prt_dsmk_p >= 0 ? 1 : -1)
|
||||
.toFormat(0.0)
|
||||
}
|
||||
},
|
||||
refetchQueries: ["GET_LINE_TICKET_BY_PK"]
|
||||
|
||||
@@ -219,7 +219,7 @@ export function JobsExportAllButton({
|
||||
};
|
||||
|
||||
return (
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled || jobIds?.length > 10}>
|
||||
{t("jobs.actions.exportselected")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -242,7 +242,8 @@ export function PartsOrderListTableComponent({
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
render: (text, record) => recordActions(record, true)
|
||||
render: (text, record) => recordActions(record, true),
|
||||
id: "parts-order-list-table-actions"
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ export function PayableExportAll({
|
||||
);
|
||||
|
||||
return (
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled || billids?.length > 10}>
|
||||
{t("jobs.actions.exportselected")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -180,7 +180,7 @@ export function PaymentsExportAllButton({
|
||||
};
|
||||
|
||||
return (
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled || paymentIds?.length > 10}>
|
||||
{t("jobs.actions.exportselected")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -8,11 +8,12 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
||||
@@ -20,7 +21,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PaymentsGenerateLink);
|
||||
|
||||
export function PaymentsGenerateLink({ bodyshop, callback, job, openChatByPhone, setMessage }) {
|
||||
export function PaymentsGenerateLink({ bodyshop, currentUser, callback, job, openChatByPhone, setMessage }) {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
@@ -30,29 +31,35 @@ export function PaymentsGenerateLink({ bodyshop, callback, job, openChatByPhone,
|
||||
|
||||
const handleFinish = async ({ amount }) => {
|
||||
setLoading(true);
|
||||
|
||||
const p = parsePhoneNumber(job.ownr_ph1, "CA");
|
||||
let p;
|
||||
try {
|
||||
p = parsePhoneNumber(job.ownr_ph1 || "", "CA");
|
||||
} catch (error) {
|
||||
console.log("Unable to parse phone number");
|
||||
}
|
||||
setLoading(true);
|
||||
const response = await axios.post("/intellipay/generate_payment_url", {
|
||||
bodyshop,
|
||||
amount: amount,
|
||||
account: job.ro_number,
|
||||
invoice: job.id
|
||||
comment: btoa(JSON.stringify({ payments: [{ jobid: job.id, amount }], userEmail: currentUser.email }))
|
||||
});
|
||||
setLoading(false);
|
||||
setPaymentLink(response.data.shorUrl);
|
||||
|
||||
openChatByPhone({
|
||||
phone_num: p.formatInternational(),
|
||||
jobid: job.id
|
||||
});
|
||||
setMessage(
|
||||
t("payments.labels.smspaymentreminder", {
|
||||
shopname: bodyshop.shopname,
|
||||
amount: amount,
|
||||
payment_link: response.data.shorUrl
|
||||
})
|
||||
);
|
||||
if (p) {
|
||||
openChatByPhone({
|
||||
phone_num: p.formatInternational(),
|
||||
jobid: job.id
|
||||
});
|
||||
setMessage(
|
||||
t("payments.labels.smspaymentreminder", {
|
||||
shopname: bodyshop.shopname,
|
||||
amount: amount,
|
||||
payment_link: response.data.shorUrl
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
//Add in confirmation & errors.
|
||||
if (callback) callback();
|
||||
|
||||
@@ -185,7 +185,7 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
|
||||
const cardSettings = useMemo(() => {
|
||||
const kanbanSettings = associationSettings?.kanban_settings;
|
||||
return mergeWithDefaults(kanbanSettings);
|
||||
}, [associationSettings]);
|
||||
}, [associationSettings?.kanban_settings]);
|
||||
|
||||
const handleSettingsChange = () => {
|
||||
setFilter(defaultFilters);
|
||||
|
||||
@@ -1,18 +1,40 @@
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import { useQuery, useSubscription } from "@apollo/client";
|
||||
import React, { useContext, useEffect, useMemo, useRef } from "react";
|
||||
import { useApolloClient, useQuery, useSubscription } from "@apollo/client";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { QUERY_JOBS_IN_PRODUCTION, SUBSCRIPTION_JOBS_IN_PRODUCTION } from "../../graphql/jobs.queries";
|
||||
import {
|
||||
QUERY_EXACT_JOB_IN_PRODUCTION,
|
||||
QUERY_JOBS_IN_PRODUCTION,
|
||||
SUBSCRIPTION_JOBS_IN_PRODUCTION,
|
||||
SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW
|
||||
} from "../../graphql/jobs.queries";
|
||||
import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import ProductionBoardKanbanComponent from "./production-board-kanban.component";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
|
||||
function ProductionBoardKanbanContainer({ bodyshop, currentUser }) {
|
||||
function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionType = "direct" }) {
|
||||
const fired = useRef(false);
|
||||
const client = useApolloClient();
|
||||
const { socket } = useContext(SocketContext); // Get the socket from context
|
||||
const reconnectTimeout = useRef(null); // To store the reconnect timeout
|
||||
const disconnectTime = useRef(null); // To track disconnection time
|
||||
const acceptableReconnectTime = 2000; // 2 seconds threshold
|
||||
|
||||
const {
|
||||
treatments: { Websocket_Production }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Websocket_Production"],
|
||||
splitKey: bodyshop && bodyshop.imexshopid
|
||||
});
|
||||
|
||||
const combinedStatuses = useMemo(
|
||||
() => [
|
||||
...bodyshop.md_ro_statuses.production_statuses,
|
||||
@@ -28,26 +50,127 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser }) {
|
||||
onError: (error) => console.error(`Error fetching jobs in production: ${error.message}`)
|
||||
});
|
||||
|
||||
const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION, {
|
||||
onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
|
||||
});
|
||||
const subscriptionEnabled = Websocket_Production?.treatment === "off";
|
||||
|
||||
const { data: updatedJobs } = useSubscription(
|
||||
subscriptionType === "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION,
|
||||
{
|
||||
skip: !subscriptionEnabled,
|
||||
onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
|
||||
}
|
||||
);
|
||||
|
||||
const { loading: associationSettingsLoading, data: associationSettings } = useQuery(QUERY_KANBAN_SETTINGS, {
|
||||
variables: { email: currentUser.email },
|
||||
onError: (error) => console.error(`Error fetching Kanban settings: ${error.message}`)
|
||||
});
|
||||
|
||||
// const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
|
||||
|
||||
useEffect(() => {
|
||||
if (updatedJobs && data) {
|
||||
if (subscriptionEnabled) {
|
||||
if (!updatedJobs) {
|
||||
return;
|
||||
}
|
||||
if (!fired.current) {
|
||||
fired.current = true;
|
||||
return;
|
||||
}
|
||||
refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`));
|
||||
}
|
||||
}, [updatedJobs, data, refetch]);
|
||||
}, [updatedJobs, refetch, subscriptionEnabled]);
|
||||
|
||||
// Socket.IO implementation for users with Split treatment "off"
|
||||
useEffect(() => {
|
||||
if (subscriptionEnabled || !socket || !bodyshop || !bodyshop.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleJobUpdates = async (jobChangedData) => {
|
||||
const jobId = jobChangedData.id;
|
||||
|
||||
// Access the existing cache for QUERY_JOBS_IN_PRODUCTION
|
||||
const existingJobsCache = client.readQuery({
|
||||
query: QUERY_JOBS_IN_PRODUCTION
|
||||
});
|
||||
|
||||
const existingJobs = existingJobsCache?.jobs || [];
|
||||
|
||||
// Check if the job already exists in the cached jobs
|
||||
const existingJob = existingJobs.find((job) => job.id === jobId);
|
||||
|
||||
if (existingJob) {
|
||||
// If the job exists, we update the cache without making any additional queries
|
||||
client.writeQuery({
|
||||
query: QUERY_JOBS_IN_PRODUCTION,
|
||||
data: {
|
||||
jobs: existingJobs.map((job) =>
|
||||
job.id === jobId ? { ...existingJob, ...jobChangedData, __typename: "jobs" } : job
|
||||
)
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// If the job doesn't exist, fetch it from the server and then add it to the cache
|
||||
try {
|
||||
const { data: jobData } = await client.query({
|
||||
query: QUERY_EXACT_JOB_IN_PRODUCTION,
|
||||
variables: { id: jobId },
|
||||
fetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
// Add the job to the existing cached jobs
|
||||
client.writeQuery({
|
||||
query: QUERY_JOBS_IN_PRODUCTION,
|
||||
data: {
|
||||
jobs: [...existingJobs, { ...jobData.job, __typename: "jobs" }]
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error fetching job ${jobId}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisconnect = () => {
|
||||
// Capture the disconnection time
|
||||
disconnectTime.current = Date.now();
|
||||
};
|
||||
|
||||
const handleReconnect = () => {
|
||||
const reconnectTime = Date.now();
|
||||
const disconnectionDuration = reconnectTime - disconnectTime.current;
|
||||
|
||||
// Only refetch if disconnection was longer than the acceptable reconnect time
|
||||
if (disconnectionDuration >= acceptableReconnectTime) {
|
||||
if (!reconnectTimeout.current) {
|
||||
reconnectTimeout.current = setTimeout(() => {
|
||||
const randomDelay = Math.floor(Math.random() * (30000 - 10000 + 1)) + 10000; // Random delay between 10 and 30 seconds
|
||||
setTimeout(() => {
|
||||
if (refetch) refetch().catch((err) => console.error(`Issue `));
|
||||
reconnectTimeout.current = null; // Clear the timeout reference after refetch
|
||||
}, randomDelay);
|
||||
}, acceptableReconnectTime);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for 'job-changed', 'disconnect', and 'connect' events
|
||||
socket.on("production-job-updated", handleJobUpdates);
|
||||
socket.on("disconnect", handleDisconnect);
|
||||
socket.on("connect", handleReconnect);
|
||||
|
||||
// Clean up on unmount or when dependencies change
|
||||
return () => {
|
||||
socket.off("production-job-updated", handleJobUpdates);
|
||||
socket.off("disconnect", handleDisconnect);
|
||||
socket.off("connect", handleReconnect);
|
||||
if (reconnectTimeout.current) {
|
||||
clearTimeout(reconnectTimeout.current);
|
||||
}
|
||||
};
|
||||
}, [subscriptionEnabled, socket, bodyshop, client, refetch]);
|
||||
|
||||
const filteredAssociationSettings = useMemo(() => {
|
||||
return associationSettings?.associations[0] || null;
|
||||
}, [associationSettings]);
|
||||
}, [associationSettings?.associations]);
|
||||
|
||||
return (
|
||||
<ProductionBoardKanbanComponent
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
|
||||
.production-alert {
|
||||
background: transparent;
|
||||
border: none;
|
||||
@@ -70,3 +69,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.clone.is-dragging .ant-card {
|
||||
border: #1890ff 2px solid !important;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,66 @@
|
||||
import InstanceRenderManager from "../../../utils/instanceRenderMgr.js";
|
||||
|
||||
const statisticsItems = [
|
||||
{ id: 0, name: "totalHrs", label: "total_hours_in_production" },
|
||||
{ id: 1, name: "totalAmountInProduction", label: "total_amount_in_production" },
|
||||
{ id: 2, name: "totalLAB", label: "total_lab_in_production" },
|
||||
{ id: 3, name: "totalLAR", label: "total_lar_in_production" },
|
||||
{ id: 4, name: "jobsInProduction", label: "jobs_in_production" },
|
||||
{ id: 5, name: "totalHrsOnBoard", label: "total_hours_on_board" },
|
||||
{ id: 6, name: "totalAmountOnBoard", label: "total_amount_on_board" },
|
||||
{ id: 7, name: "totalLABOnBoard", label: "total_lab_on_board" },
|
||||
{ id: 8, name: "totalLAROnBoard", label: "total_lar_on_board" },
|
||||
{ id: 9, name: "jobsOnBoard", label: "total_jobs_on_board" },
|
||||
{ id: 10, name: "tasksOnBoard", label: "tasks_on_board" },
|
||||
|
||||
{
|
||||
id: 5,
|
||||
name: "totalHrsOnBoard",
|
||||
label: InstanceRenderManager({
|
||||
imex: "total_hours_in_view",
|
||||
rome: "total_hours_on_board",
|
||||
promanager: "total_hours_on_board"
|
||||
})
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "totalAmountOnBoard",
|
||||
label: InstanceRenderManager({
|
||||
imex: "total_amount_in_view",
|
||||
rome: "total_amount_on_board",
|
||||
promanager: "total_amount_on_board"
|
||||
})
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "totalLABOnBoard",
|
||||
label: InstanceRenderManager({
|
||||
imex: "total_lab_in_view",
|
||||
rome: "total_lab_on_board",
|
||||
promanager: "total_lab_on_board"
|
||||
})
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "totalLAROnBoard",
|
||||
label: InstanceRenderManager({
|
||||
imex: "total_lar_in_view",
|
||||
rome: "total_lar_on_board",
|
||||
promanager: "total_lar_on_board"
|
||||
})
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "jobsOnBoard",
|
||||
label: InstanceRenderManager({
|
||||
imex: "total_jobs_in_view",
|
||||
rome: "total_jobs_on_board",
|
||||
promanager: "total_jobs_on_board"
|
||||
})
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "tasksOnBoard",
|
||||
label: InstanceRenderManager({
|
||||
imex: "tasks_in_view",
|
||||
rome: "tasks_on_board",
|
||||
promanager: "tasks_on_board"
|
||||
})
|
||||
},
|
||||
{ id: 11, name: "tasksInProduction", label: "tasks_in_production" }
|
||||
];
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ function getFurthestAway({ pageBorderBox, draggable, candidates }) {
|
||||
const axis = candidate.axis;
|
||||
const target = patch(
|
||||
candidate.axis.line,
|
||||
// use the current center of the dragging item on the main axis
|
||||
pageBorderBox.center[axis.line],
|
||||
// use the center of the list on the main axis
|
||||
candidate.page.borderBox.center[axis.line],
|
||||
// use the center of the list on the cross axis
|
||||
candidate.page.borderBox.center[axis.crossAxisLine]
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import getBodyElement from "../get-body-element";
|
||||
const isEqual = (base) => (value) => base === value;
|
||||
const isScroll = isEqual("scroll");
|
||||
const isAuto = isEqual("auto");
|
||||
const isOverlay = isEqual("overlay");
|
||||
const isVisible = isEqual("visible");
|
||||
const isEither = (overflow, fn) => fn(overflow.overflowX) || fn(overflow.overflowY);
|
||||
const isBoth = (overflow, fn) => fn(overflow.overflowX) && fn(overflow.overflowY);
|
||||
@@ -14,7 +15,7 @@ const isElementScrollable = (el) => {
|
||||
overflowX: style.overflowX,
|
||||
overflowY: style.overflowY
|
||||
};
|
||||
return isEither(overflow, isScroll) || isEither(overflow, isAuto);
|
||||
return isEither(overflow, isScroll) || isEither(overflow, isAuto) || isEither(overflow, isOverlay);
|
||||
};
|
||||
|
||||
// Special case for a body element
|
||||
|
||||
@@ -8,7 +8,7 @@ function getSelector(contextId) {
|
||||
return `[${attributes.dragHandle.contextId}="${contextId}"]`;
|
||||
}
|
||||
|
||||
function findClosestDragHandleFromEvent(contextId, event) {
|
||||
export function findClosestDragHandleFromEvent(contextId, event) {
|
||||
const target = event.target;
|
||||
if (!isElement(target)) {
|
||||
warning("event.target must be a Element");
|
||||
|
||||
@@ -240,11 +240,14 @@ export default function useTouchSensor(api) {
|
||||
y: clientY
|
||||
};
|
||||
|
||||
const handle = api.findClosestDragHandle(event);
|
||||
invariant(handle, "Touch sensor unable to find drag handle");
|
||||
|
||||
// unbind this event handler
|
||||
unbindEventsRef.current();
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
startPendingDrag(actions, point);
|
||||
startPendingDrag(actions, point, handle);
|
||||
}
|
||||
}),
|
||||
// not including stop or startPendingDrag as it is not defined initially
|
||||
@@ -288,7 +291,7 @@ export default function useTouchSensor(api) {
|
||||
}
|
||||
}, [stop]);
|
||||
const bindCapturingEvents = useCallback(
|
||||
function bindCapturingEvents() {
|
||||
function bindCapturingEvents(target) {
|
||||
const options = {
|
||||
capture: true,
|
||||
passive: false
|
||||
@@ -307,7 +310,7 @@ export default function useTouchSensor(api) {
|
||||
// Old behaviour:
|
||||
// https://gist.github.com/parris/dda613e3ae78f14eb2dc9fa0f4bfce3d
|
||||
// https://stackoverflow.com/questions/33298828/touch-move-event-dont-fire-after-touch-start-target-is-removed
|
||||
const unbindTarget = bindEvents(window, getHandleBindings(args), options);
|
||||
const unbindTarget = bindEvents(target, getHandleBindings(args), options);
|
||||
const unbindWindow = bindEvents(window, getWindowBindings(args), options);
|
||||
unbindEventsRef.current = function unbindAll() {
|
||||
unbindTarget();
|
||||
@@ -330,7 +333,7 @@ export default function useTouchSensor(api) {
|
||||
[getPhase, setPhase]
|
||||
);
|
||||
const startPendingDrag = useCallback(
|
||||
function startPendingDrag(actions, point) {
|
||||
function startPendingDrag(actions, point, target) {
|
||||
invariant(getPhase().type === "IDLE", "Expected to move from IDLE to PENDING drag");
|
||||
const longPressTimerId = setTimeout(startDragging, timeForLongPress);
|
||||
setPhase({
|
||||
@@ -339,7 +342,7 @@ export default function useTouchSensor(api) {
|
||||
actions,
|
||||
longPressTimerId
|
||||
});
|
||||
bindCapturingEvents();
|
||||
bindCapturingEvents(target);
|
||||
},
|
||||
[bindCapturingEvents, getPhase, setPhase, startDragging]
|
||||
);
|
||||
|
||||
@@ -23,7 +23,9 @@ import getBorderBoxCenterPosition from "../get-border-box-center-position";
|
||||
import { warning } from "../../dev-warning";
|
||||
import useLayoutEffect from "../use-isomorphic-layout-effect";
|
||||
import { noop } from "../../empty";
|
||||
import findClosestDraggableIdFromEvent from "./find-closest-draggable-id-from-event";
|
||||
import findClosestDraggableIdFromEvent, {
|
||||
findClosestDragHandleFromEvent
|
||||
} from "./find-closest-draggable-id-from-event";
|
||||
import findDraggable from "../get-elements/find-draggable";
|
||||
import bindEvents from "../event-bindings/bind-events";
|
||||
|
||||
@@ -339,6 +341,9 @@ export default function useSensorMarshal({ contextId, store, registry, customSen
|
||||
}),
|
||||
[contextId, lockAPI, registry, store]
|
||||
);
|
||||
|
||||
const findClosestDragHandle = useCallback((event) => findClosestDragHandleFromEvent(contextId, event), [contextId]);
|
||||
|
||||
const findClosestDraggableId = useCallback((event) => findClosestDraggableIdFromEvent(contextId, event), [contextId]);
|
||||
const findOptionsForDraggable = useCallback(
|
||||
(id) => {
|
||||
@@ -370,9 +375,18 @@ export default function useSensorMarshal({ contextId, store, registry, customSen
|
||||
findClosestDraggableId,
|
||||
findOptionsForDraggable,
|
||||
tryReleaseLock,
|
||||
isLockClaimed
|
||||
isLockClaimed,
|
||||
findClosestDragHandle
|
||||
}),
|
||||
[canGetLock, tryGetLock, findClosestDraggableId, findOptionsForDraggable, tryReleaseLock, isLockClaimed]
|
||||
[
|
||||
canGetLock,
|
||||
tryGetLock,
|
||||
findClosestDraggableId,
|
||||
findOptionsForDraggable,
|
||||
tryReleaseLock,
|
||||
isLockClaimed,
|
||||
findClosestDragHandle
|
||||
]
|
||||
);
|
||||
|
||||
// Bad ass
|
||||
|
||||
@@ -83,7 +83,13 @@ const getFinalStyles = (contextId) => {
|
||||
return {
|
||||
selector: getSelector(attributes.draggable.contextId),
|
||||
styles: {
|
||||
dragging: transition,
|
||||
dragging: `
|
||||
${transition}
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
`,
|
||||
dropAnimating: transition,
|
||||
userCancel: transition
|
||||
}
|
||||
|
||||
@@ -67,7 +67,9 @@ export default function useStyleMarshal(contextId, nonce) {
|
||||
const remove = (ref) => {
|
||||
const current = ref.current;
|
||||
invariant(current, "Cannot unmount ref as it is not set");
|
||||
getHead().removeChild(current);
|
||||
if (getHead().contains(current)) {
|
||||
getHead().removeChild(current);
|
||||
}
|
||||
ref.current = null;
|
||||
};
|
||||
remove(alwaysRef);
|
||||
|
||||
@@ -298,6 +298,16 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => statusSort(a.status, b.status, activeStatuses),
|
||||
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
filters:
|
||||
activeStatuses
|
||||
?.map((s) => {
|
||||
return {
|
||||
text: s || "No Status*",
|
||||
value: [s]
|
||||
};
|
||||
})
|
||||
.sort((a, b) => statusSort(a.text, b.text, activeStatuses)) || [],
|
||||
onFilter: (value, record) => value.includes(record.status),
|
||||
render: (text, record) => <ProductionListColumnStatus record={record} />
|
||||
},
|
||||
{
|
||||
|
||||
@@ -21,25 +21,26 @@ export function ProductionListColumnStatus({ record, bodyshop, insertAuditTrail
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSetStatus = async (e) => {
|
||||
logImEXEvent("production_change_status");
|
||||
// e.stopPropagation();
|
||||
setLoading(true);
|
||||
const { key } = e;
|
||||
await updateJob({
|
||||
variables: {
|
||||
jobId: record.id,
|
||||
job: {
|
||||
status: key
|
||||
if (bodyshop.md_ro_statuses.production_statuses.includes(record.status) && !bodyshop.md_ro_statuses.post_production_statuses.includes(record.status)) {
|
||||
logImEXEvent("production_change_status");
|
||||
// e.stopPropagation();
|
||||
setLoading(true);
|
||||
const { key } = e;
|
||||
await updateJob({
|
||||
variables: {
|
||||
jobId: record.id,
|
||||
job: {
|
||||
status: key
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: record.id,
|
||||
operation: AuditTrailMapping.jobstatuschange(key),
|
||||
type: "jobstatuschange"
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: record.id,
|
||||
operation: AuditTrailMapping.jobstatuschange(key),
|
||||
type: "jobstatuschange"
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const menu = {
|
||||
|
||||
@@ -457,41 +457,42 @@ export function ProductionListConfigManager({
|
||||
value={activeView}
|
||||
disabled={open || isAddingNewProfile} // Disable the Select box when the popover is open or adding a new profile
|
||||
>
|
||||
{bodyshop.production_config
|
||||
.slice()
|
||||
.sort((a, b) =>
|
||||
a.name === t("production.constants.main_profile")
|
||||
? -1
|
||||
: b.name === t("production.constants.main_profile")
|
||||
? 1
|
||||
: 0
|
||||
) //
|
||||
.map((config) => (
|
||||
<Select.Option key={config.name} label={config.name}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<span
|
||||
style={{
|
||||
flex: 1,
|
||||
maxWidth: "80%",
|
||||
marginRight: "1rem",
|
||||
textOverflow: "ellipsis"
|
||||
}}
|
||||
>
|
||||
{config.name}
|
||||
</span>
|
||||
{config.name !== t("production.constants.main_profile") && (
|
||||
<Popconfirm
|
||||
placement="right"
|
||||
title={t("general.labels.areyousure")}
|
||||
onConfirm={() => handleTrash(config.name)}
|
||||
onCancel={(e) => e.stopPropagation()}
|
||||
{bodyshop?.production_config &&
|
||||
bodyshop.production_config
|
||||
.slice()
|
||||
.sort((a, b) =>
|
||||
a.name === t("production.constants.main_profile")
|
||||
? -1
|
||||
: b.name === t("production.constants.main_profile")
|
||||
? 1
|
||||
: 0
|
||||
) //
|
||||
.map((config) => (
|
||||
<Select.Option key={config.name} label={config.name}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<span
|
||||
style={{
|
||||
flex: 1,
|
||||
maxWidth: "80%",
|
||||
marginRight: "1rem",
|
||||
textOverflow: "ellipsis"
|
||||
}}
|
||||
>
|
||||
<DeleteOutlined onClick={(e) => e.stopPropagation()} />
|
||||
</Popconfirm>
|
||||
)}
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
{config.name}
|
||||
</span>
|
||||
{config.name !== t("production.constants.main_profile") && (
|
||||
<Popconfirm
|
||||
placement="right"
|
||||
title={t("general.labels.areyousure")}
|
||||
onConfirm={() => handleTrash(config.name)}
|
||||
onCancel={(e) => e.stopPropagation()}
|
||||
>
|
||||
<DeleteOutlined onClick={(e) => e.stopPropagation()} />
|
||||
</Popconfirm>
|
||||
)}
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
<Select.Option key="add_new" label={t("production.labels.addnewprofile")}>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<PlusOutlined style={{ marginRight: "0.5rem" }} />
|
||||
|
||||
@@ -51,8 +51,8 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
||||
|
||||
const initialColumnsRef = useRef(
|
||||
(initialStateRef.current &&
|
||||
bodyshop.production_config
|
||||
.find((p) => p.name === defaultView)
|
||||
bodyshop?.production_config
|
||||
?.find((p) => p.name === defaultView)
|
||||
?.columns.columnKeys.map((k) => {
|
||||
return {
|
||||
...ProductionListColumns({
|
||||
@@ -76,8 +76,8 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
||||
const { t } = useTranslation();
|
||||
|
||||
const matchingColumnConfig = useMemo(() => {
|
||||
return bodyshop.production_config.find((p) => p.name === defaultView);
|
||||
}, [bodyshop.production_config, defaultView]);
|
||||
return bodyshop?.production_config?.find((p) => p.name === defaultView);
|
||||
}, [bodyshop.production_config]);
|
||||
|
||||
useEffect(() => {
|
||||
const newColumns =
|
||||
|
||||
@@ -1,24 +1,54 @@
|
||||
import { useApolloClient, useQuery, useSubscription } from "@apollo/client";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useContext, useEffect, useState, useRef } from "react";
|
||||
import {
|
||||
QUERY_EXACT_JOB_IN_PRODUCTION,
|
||||
QUERY_EXACT_JOBS_IN_PRODUCTION,
|
||||
QUERY_JOBS_IN_PRODUCTION,
|
||||
SUBSCRIPTION_JOBS_IN_PRODUCTION
|
||||
SUBSCRIPTION_JOBS_IN_PRODUCTION,
|
||||
SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW
|
||||
} from "../../graphql/jobs.queries";
|
||||
import ProductionListTable from "./production-list-table.component";
|
||||
import _ from "lodash";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||
|
||||
export default function ProductionListTableContainer() {
|
||||
export default function ProductionListTableContainer({ bodyshop, subscriptionType = "direct" }) {
|
||||
const client = useApolloClient();
|
||||
const { socket } = useContext(SocketContext);
|
||||
const [joblist, setJoblist] = useState([]);
|
||||
const reconnectTimeout = useRef(null); // To store the reconnect timeout
|
||||
const disconnectTime = useRef(null); // To store the time of disconnection
|
||||
|
||||
const acceptableReconnectTime = 2000; // 2 seconds threshold
|
||||
|
||||
// Get Split treatment
|
||||
const {
|
||||
treatments: { Websocket_Production }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Websocket_Production"],
|
||||
splitKey: bodyshop && bodyshop.imexshopid
|
||||
});
|
||||
|
||||
// Determine if subscription is enabled
|
||||
const subscriptionEnabled = Websocket_Production?.treatment === "off";
|
||||
|
||||
// Use GraphQL query
|
||||
const { refetch, loading, data } = useQuery(QUERY_JOBS_IN_PRODUCTION, {
|
||||
pollInterval: 3600000,
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
const client = useApolloClient();
|
||||
const [joblist, setJoblist] = useState([]);
|
||||
const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION);
|
||||
|
||||
// Use GraphQL subscription when subscription is enabled
|
||||
const { data: updatedJobs } = useSubscription(
|
||||
subscriptionType === "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION,
|
||||
{
|
||||
skip: !subscriptionEnabled
|
||||
}
|
||||
);
|
||||
|
||||
// Update joblist when data changes
|
||||
useEffect(() => {
|
||||
if (!(data && data.jobs)) return;
|
||||
setJoblist(
|
||||
@@ -28,34 +58,134 @@ export default function ProductionListTableContainer() {
|
||||
);
|
||||
}, [data]);
|
||||
|
||||
// Handle updates from GraphQL subscription
|
||||
useEffect(() => {
|
||||
if (!updatedJobs || joblist.length === 0) return;
|
||||
if (subscriptionEnabled) {
|
||||
if (!updatedJobs || joblist.length === 0) return;
|
||||
|
||||
const jobDiff = _.differenceWith(
|
||||
joblist,
|
||||
updatedJobs.jobs,
|
||||
(a, b) => a.id === b.id && a.updated_at === b.updated_at
|
||||
);
|
||||
const jobDiff = _.differenceWith(
|
||||
joblist,
|
||||
updatedJobs.jobs,
|
||||
(a, b) => a.id === b.id && a.updated_at === b.updated_at
|
||||
);
|
||||
|
||||
if (jobDiff.length > 1) {
|
||||
getUpdatedJobsData(jobDiff.map((j) => j.id));
|
||||
} else if (jobDiff.length === 1) {
|
||||
jobDiff.forEach((job) => {
|
||||
getUpdatedJobData(job.id);
|
||||
});
|
||||
if (jobDiff.length > 1) {
|
||||
getUpdatedJobsData(jobDiff.map((j) => j.id));
|
||||
} else if (jobDiff.length === 1) {
|
||||
jobDiff.forEach((job) => {
|
||||
getUpdatedJobData(job.id);
|
||||
});
|
||||
}
|
||||
|
||||
setJoblist(updatedJobs.jobs);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [updatedJobs, subscriptionEnabled]);
|
||||
|
||||
// Handle updates from Socket.IO when subscription is disabled
|
||||
useEffect(() => {
|
||||
if (subscriptionEnabled || !socket || !bodyshop || !bodyshop.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
setJoblist(updatedJobs.jobs);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [updatedJobs]);
|
||||
const handleJobUpdates = async (jobChangedData) => {
|
||||
const jobId = jobChangedData.id;
|
||||
|
||||
// Access the existing cache for QUERY_JOBS_IN_PRODUCTION
|
||||
const existingJobsCache = client.readQuery({
|
||||
query: QUERY_JOBS_IN_PRODUCTION
|
||||
});
|
||||
|
||||
const existingJobs = existingJobsCache?.jobs || [];
|
||||
|
||||
// Check if the job already exists in the cached jobs
|
||||
const existingJob = existingJobs.find((job) => job.id === jobId);
|
||||
|
||||
if (existingJob) {
|
||||
// If the job exists, we update the cache without making any additional queries
|
||||
client.writeQuery({
|
||||
query: QUERY_JOBS_IN_PRODUCTION,
|
||||
data: {
|
||||
jobs: existingJobs.map((job) =>
|
||||
job.id === jobId ? { ...existingJob, ...jobChangedData, __typename: "jobs" } : job
|
||||
)
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// If the job doesn't exist, fetch it from the server and then add it to the cache
|
||||
try {
|
||||
const { data: jobData } = await client.query({
|
||||
query: QUERY_EXACT_JOB_IN_PRODUCTION,
|
||||
variables: { id: jobId },
|
||||
fetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
// Add the job to the existing cached jobs
|
||||
client.writeQuery({
|
||||
query: QUERY_JOBS_IN_PRODUCTION,
|
||||
data: {
|
||||
jobs: [...existingJobs, { ...jobData.job, __typename: "jobs" }]
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error fetching job ${jobId}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisconnect = () => {
|
||||
// Capture the time when the disconnection happens
|
||||
disconnectTime.current = Date.now();
|
||||
};
|
||||
|
||||
const handleReconnect = () => {
|
||||
// Calculate how long the disconnection lasted
|
||||
const reconnectTime = Date.now();
|
||||
const disconnectionDuration = reconnectTime - disconnectTime.current;
|
||||
|
||||
// If disconnection lasted less than acceptable reconnect time, do nothing
|
||||
if (disconnectionDuration < acceptableReconnectTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule a refetch with a random delay between 10 and 30 seconds
|
||||
if (!reconnectTimeout.current) {
|
||||
reconnectTimeout.current = setTimeout(() => {
|
||||
const randomDelay = Math.floor(Math.random() * (30000 - 10000 + 1)) + 10000; // Random delay between 10 and 30 seconds
|
||||
setTimeout(() => {
|
||||
if (refetch) refetch();
|
||||
reconnectTimeout.current = null; // Clear the timeout reference after refetch
|
||||
}, randomDelay);
|
||||
}, acceptableReconnectTime);
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for 'production-job-updated', 'disconnect', and 'connect' events
|
||||
socket.on("production-job-updated", handleJobUpdates);
|
||||
socket.on("disconnect", handleDisconnect);
|
||||
socket.on("connect", handleReconnect);
|
||||
|
||||
// Clean up on unmount or when dependencies change
|
||||
return () => {
|
||||
socket.off("production-job-updated", handleJobUpdates);
|
||||
socket.off("disconnect", handleDisconnect);
|
||||
socket.off("connect", handleReconnect);
|
||||
if (reconnectTimeout.current) {
|
||||
clearTimeout(reconnectTimeout.current);
|
||||
}
|
||||
};
|
||||
}, [subscriptionEnabled, socket, bodyshop, client, refetch]);
|
||||
|
||||
// Functions to fetch updated job data
|
||||
const getUpdatedJobData = async (jobId) => {
|
||||
client.query({
|
||||
await client.query({
|
||||
query: QUERY_EXACT_JOB_IN_PRODUCTION,
|
||||
variables: { id: jobId }
|
||||
variables: { id: jobId },
|
||||
fetchPolicy: "network-only"
|
||||
});
|
||||
};
|
||||
const getUpdatedJobsData = async (jobIds) => {
|
||||
|
||||
const getUpdatedJobsData = (jobIds) => {
|
||||
client.query({
|
||||
query: QUERY_EXACT_JOBS_IN_PRODUCTION,
|
||||
variables: { ids: jobIds }
|
||||
|
||||
505
client/src/components/schedule-calendar-wrapper/localizer.js
Normal file
505
client/src/components/schedule-calendar-wrapper/localizer.js
Normal file
@@ -0,0 +1,505 @@
|
||||
import isBetween from "dayjs/plugin/isBetween";
|
||||
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
|
||||
import isSameOrBefore from "dayjs/plugin/isSameOrBefore";
|
||||
import localeData from "dayjs/plugin/localeData";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import minMax from "dayjs/plugin/minMax";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import { DateLocalizer } from "react-big-calendar";
|
||||
|
||||
function arrayWithHoles(arr) {
|
||||
if (Array.isArray(arr)) return arr;
|
||||
}
|
||||
|
||||
function iterableToArrayLimit(arr, i) {
|
||||
if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return;
|
||||
var _arr = [];
|
||||
var _n = true;
|
||||
var _d = false;
|
||||
var _e = undefined;
|
||||
try {
|
||||
for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
|
||||
_arr.push(_s.value);
|
||||
if (i && _arr.length === i) break;
|
||||
}
|
||||
} catch (err) {
|
||||
_d = true;
|
||||
_e = err;
|
||||
} finally {
|
||||
try {
|
||||
if (!_n && _i["return"] != null) _i["return"]();
|
||||
} finally {
|
||||
if (_d) throw _e;
|
||||
}
|
||||
}
|
||||
return _arr;
|
||||
}
|
||||
|
||||
function unsupportedIterableToArray(o, minLen) {
|
||||
if (!o) return;
|
||||
if (typeof o === "string") return arrayLikeToArray(o, minLen);
|
||||
var n = Object.prototype.toString.call(o).slice(8, -1);
|
||||
if (n === "Object" && o.constructor) n = o.constructor.name;
|
||||
if (n === "Map" || n === "Set") return Array.from(o);
|
||||
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return arrayLikeToArray(o, minLen);
|
||||
}
|
||||
|
||||
function arrayLikeToArray(arr, len) {
|
||||
if (len == null || len > arr.length) len = arr.length;
|
||||
for (var i = 0, arr2 = new Array(len); i < len; i++) {
|
||||
arr2[i] = arr[i];
|
||||
}
|
||||
return arr2;
|
||||
}
|
||||
|
||||
function nonIterableRest() {
|
||||
throw new TypeError(
|
||||
"Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."
|
||||
);
|
||||
}
|
||||
|
||||
function _slicedToArray(arr, i) {
|
||||
return arrayWithHoles(arr) || iterableToArrayLimit(arr, i) || unsupportedIterableToArray(arr, i) || nonIterableRest();
|
||||
}
|
||||
|
||||
function fixUnit(unit) {
|
||||
var datePart = unit ? unit.toLowerCase() : unit;
|
||||
if (datePart === "FullYear") {
|
||||
datePart = "year";
|
||||
} else if (!datePart) {
|
||||
datePart = undefined;
|
||||
}
|
||||
return datePart;
|
||||
}
|
||||
|
||||
var timeRangeFormat = function timeRangeFormat(_ref3, culture, local) {
|
||||
var start = _ref3.start,
|
||||
end = _ref3.end;
|
||||
return local.format(start, "LT", culture) + " – " + local.format(end, "LT", culture);
|
||||
};
|
||||
var timeRangeStartFormat = function timeRangeStartFormat(_ref4, culture, local) {
|
||||
var start = _ref4.start;
|
||||
return local.format(start, "LT", culture) + " – ";
|
||||
};
|
||||
var timeRangeEndFormat = function timeRangeEndFormat(_ref5, culture, local) {
|
||||
var end = _ref5.end;
|
||||
return " – " + local.format(end, "LT", culture);
|
||||
};
|
||||
var weekRangeFormat = function weekRangeFormat(_ref, culture, local) {
|
||||
var start = _ref.start,
|
||||
end = _ref.end;
|
||||
return (
|
||||
local.format(start, "MMMM DD", culture) +
|
||||
" – " +
|
||||
// updated to use this localizer 'eq()' method
|
||||
local.format(end, local.eq(start, end, "month") ? "DD" : "MMMM DD", culture)
|
||||
);
|
||||
};
|
||||
var dateRangeFormat = function dateRangeFormat(_ref2, culture, local) {
|
||||
var start = _ref2.start,
|
||||
end = _ref2.end;
|
||||
return local.format(start, "L", culture) + " – " + local.format(end, "L", culture);
|
||||
};
|
||||
|
||||
var formats = {
|
||||
dateFormat: "DD",
|
||||
dayFormat: "DD ddd",
|
||||
weekdayFormat: "ddd",
|
||||
selectRangeFormat: timeRangeFormat,
|
||||
eventTimeRangeFormat: timeRangeFormat,
|
||||
eventTimeRangeStartFormat: timeRangeStartFormat,
|
||||
eventTimeRangeEndFormat: timeRangeEndFormat,
|
||||
timeGutterFormat: "LT",
|
||||
monthHeaderFormat: "MMMM YYYY",
|
||||
dayHeaderFormat: "dddd MMM DD",
|
||||
dayRangeHeaderFormat: weekRangeFormat,
|
||||
agendaHeaderFormat: dateRangeFormat,
|
||||
agendaDateFormat: "ddd MMM DD",
|
||||
agendaTimeFormat: "LT",
|
||||
agendaTimeRangeFormat: timeRangeFormat
|
||||
};
|
||||
|
||||
const localizer = (dayjsLib) => {
|
||||
// load dayjs plugins
|
||||
dayjsLib.extend(isBetween);
|
||||
dayjsLib.extend(isSameOrAfter);
|
||||
dayjsLib.extend(isSameOrBefore);
|
||||
dayjsLib.extend(localeData);
|
||||
dayjsLib.extend(localizedFormat);
|
||||
dayjsLib.extend(minMax);
|
||||
dayjsLib.extend(utc);
|
||||
var locale = function locale(dj, c) {
|
||||
return c ? dj.locale(c) : dj;
|
||||
};
|
||||
|
||||
// if the timezone plugin is loaded,
|
||||
// then use the timezone aware version
|
||||
|
||||
//TODO This was the issue entirely...
|
||||
// var dayjs = dayjsLib.tz ? dayjsLib.tz : dayjsLib;
|
||||
var dayjs = dayjsLib;
|
||||
|
||||
function getTimezoneOffset(date) {
|
||||
// ensures this gets cast to timezone
|
||||
return dayjs(date).toDate().getTimezoneOffset();
|
||||
}
|
||||
|
||||
function getDstOffset(start, end) {
|
||||
var _st$tz$$x$$timezone;
|
||||
// convert to dayjs, in case
|
||||
var st = dayjs(start);
|
||||
var ed = dayjs(end);
|
||||
// if not using the dayjs timezone plugin
|
||||
if (!dayjs.tz) {
|
||||
return st.toDate().getTimezoneOffset() - ed.toDate().getTimezoneOffset();
|
||||
}
|
||||
/**
|
||||
* If a default timezone has been applied, then
|
||||
* use this to get the proper timezone offset, otherwise default
|
||||
* the timezone to the browser local
|
||||
*/
|
||||
var tzName =
|
||||
(_st$tz$$x$$timezone = st.tz().$x.$timezone) !== null && _st$tz$$x$$timezone !== void 0
|
||||
? _st$tz$$x$$timezone
|
||||
: dayjsLib.tz.guess();
|
||||
// invert offsets to be inline with moment.js
|
||||
var startOffset = -dayjs.tz(+st, tzName).utcOffset();
|
||||
var endOffset = -dayjs.tz(+ed, tzName).utcOffset();
|
||||
return startOffset - endOffset;
|
||||
}
|
||||
|
||||
function getDayStartDstOffset(start) {
|
||||
var dayStart = dayjs(start).startOf("day");
|
||||
return getDstOffset(dayStart, start);
|
||||
}
|
||||
|
||||
/*** BEGIN localized date arithmetic methods with dayjs ***/
|
||||
function defineComparators(a, b, unit) {
|
||||
var datePart = fixUnit(unit);
|
||||
var dtA = datePart ? dayjs(a).startOf(datePart) : dayjs(a);
|
||||
var dtB = datePart ? dayjs(b).startOf(datePart) : dayjs(b);
|
||||
return [dtA, dtB, datePart];
|
||||
}
|
||||
|
||||
function startOf() {
|
||||
var date = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
|
||||
var unit = arguments.length > 1 ? arguments[1] : undefined;
|
||||
var datePart = fixUnit(unit);
|
||||
if (datePart) {
|
||||
return dayjs(date).startOf(datePart).toDate();
|
||||
}
|
||||
return dayjs(date).toDate();
|
||||
}
|
||||
|
||||
function endOf() {
|
||||
var date = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
|
||||
var unit = arguments.length > 1 ? arguments[1] : undefined;
|
||||
var datePart = fixUnit(unit);
|
||||
if (datePart) {
|
||||
return dayjs(date).endOf(datePart).toDate();
|
||||
}
|
||||
return dayjs(date).toDate();
|
||||
}
|
||||
|
||||
// dayjs comparison operations *always* convert both sides to dayjs objects
|
||||
// prior to running the comparisons
|
||||
function eq(a, b, unit) {
|
||||
var _defineComparators = defineComparators(a, b, unit),
|
||||
_defineComparators2 = _slicedToArray(_defineComparators, 3),
|
||||
dtA = _defineComparators2[0],
|
||||
dtB = _defineComparators2[1],
|
||||
datePart = _defineComparators2[2];
|
||||
return dtA.isSame(dtB, datePart);
|
||||
}
|
||||
|
||||
function neq(a, b, unit) {
|
||||
return !eq(a, b, unit);
|
||||
}
|
||||
|
||||
function gt(a, b, unit) {
|
||||
var _defineComparators3 = defineComparators(a, b, unit),
|
||||
_defineComparators4 = _slicedToArray(_defineComparators3, 3),
|
||||
dtA = _defineComparators4[0],
|
||||
dtB = _defineComparators4[1],
|
||||
datePart = _defineComparators4[2];
|
||||
return dtA.isAfter(dtB, datePart);
|
||||
}
|
||||
|
||||
function lt(a, b, unit) {
|
||||
var _defineComparators5 = defineComparators(a, b, unit),
|
||||
_defineComparators6 = _slicedToArray(_defineComparators5, 3),
|
||||
dtA = _defineComparators6[0],
|
||||
dtB = _defineComparators6[1],
|
||||
datePart = _defineComparators6[2];
|
||||
return dtA.isBefore(dtB, datePart);
|
||||
}
|
||||
|
||||
function gte(a, b, unit) {
|
||||
var _defineComparators7 = defineComparators(a, b, unit),
|
||||
_defineComparators8 = _slicedToArray(_defineComparators7, 3),
|
||||
dtA = _defineComparators8[0],
|
||||
dtB = _defineComparators8[1],
|
||||
datePart = _defineComparators8[2];
|
||||
return dtA.isSameOrBefore(dtB, datePart);
|
||||
}
|
||||
|
||||
function lte(a, b, unit) {
|
||||
var _defineComparators9 = defineComparators(a, b, unit),
|
||||
_defineComparators10 = _slicedToArray(_defineComparators9, 3),
|
||||
dtA = _defineComparators10[0],
|
||||
dtB = _defineComparators10[1],
|
||||
datePart = _defineComparators10[2];
|
||||
return dtA.isSameOrBefore(dtB, datePart);
|
||||
}
|
||||
|
||||
function inRange(day, min, max) {
|
||||
var unit = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : "day";
|
||||
var datePart = fixUnit(unit);
|
||||
var djDay = dayjs(day);
|
||||
var djMin = dayjs(min);
|
||||
var djMax = dayjs(max);
|
||||
return djDay.isBetween(djMin, djMax, datePart, "[]");
|
||||
}
|
||||
|
||||
function min(dateA, dateB) {
|
||||
var dtA = dayjs(dateA);
|
||||
var dtB = dayjs(dateB);
|
||||
var minDt = dayjsLib.min(dtA, dtB);
|
||||
return minDt.toDate();
|
||||
}
|
||||
|
||||
function max(dateA, dateB) {
|
||||
var dtA = dayjs(dateA);
|
||||
var dtB = dayjs(dateB);
|
||||
var maxDt = dayjsLib.max(dtA, dtB);
|
||||
return maxDt.toDate();
|
||||
}
|
||||
|
||||
function merge(date, time) {
|
||||
if (!date && !time) return null;
|
||||
var tm = dayjs(time).format("HH:mm:ss");
|
||||
var dt = dayjs(date).startOf("day").format("MM/DD/YYYY");
|
||||
// We do it this way to avoid issues when timezone switching
|
||||
return dayjsLib("".concat(dt, " ").concat(tm), "MM/DD/YYYY HH:mm:ss").toDate();
|
||||
}
|
||||
|
||||
function add(date, adder, unit) {
|
||||
var datePart = fixUnit(unit);
|
||||
return dayjs(date).add(adder, datePart).toDate();
|
||||
}
|
||||
|
||||
function range(start, end) {
|
||||
var unit = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "day";
|
||||
var datePart = fixUnit(unit);
|
||||
// because the add method will put these in tz, we have to start that way
|
||||
var current = dayjs(start).toDate();
|
||||
var days = [];
|
||||
while (lte(current, end)) {
|
||||
days.push(current);
|
||||
current = add(current, 1, datePart);
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
function ceil(date, unit) {
|
||||
var datePart = fixUnit(unit);
|
||||
var floor = startOf(date, datePart);
|
||||
return eq(floor, date) ? floor : add(floor, 1, datePart);
|
||||
}
|
||||
|
||||
function diff(a, b) {
|
||||
var unit = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "day";
|
||||
var datePart = fixUnit(unit);
|
||||
// don't use 'defineComparators' here, as we don't want to mutate the values
|
||||
var dtA = dayjs(a);
|
||||
var dtB = dayjs(b);
|
||||
return dtB.diff(dtA, datePart);
|
||||
}
|
||||
|
||||
function minutes(date) {
|
||||
var dt = dayjs(date);
|
||||
return dt.minutes();
|
||||
}
|
||||
|
||||
function firstOfWeek(culture) {
|
||||
var data = culture ? dayjsLib.localeData(culture) : dayjsLib.localeData();
|
||||
return data ? data.firstDayOfWeek() : 0;
|
||||
}
|
||||
|
||||
function firstVisibleDay(date) {
|
||||
return dayjs(date).startOf("month").startOf("week").toDate();
|
||||
}
|
||||
|
||||
function lastVisibleDay(date) {
|
||||
return dayjs(date).endOf("month").endOf("week").toDate();
|
||||
}
|
||||
|
||||
function visibleDays(date) {
|
||||
var current = firstVisibleDay(date);
|
||||
var last = lastVisibleDay(date);
|
||||
var days = [];
|
||||
while (lte(current, last)) {
|
||||
days.push(current);
|
||||
current = add(current, 1, "d");
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
/*** END localized date arithmetic methods with dayjs ***/
|
||||
|
||||
/**
|
||||
* Moved from TimeSlots.js, this method overrides the method of the same name
|
||||
* in the localizer.js, using dayjs to construct the js Date
|
||||
* @param {Date} dt - date to start with
|
||||
* @param {Number} minutesFromMidnight
|
||||
* @param {Number} offset
|
||||
* @returns {Date}
|
||||
*/
|
||||
function getSlotDate(dt, minutesFromMidnight, offset) {
|
||||
return dayjs(dt)
|
||||
.startOf("day")
|
||||
.minute(minutesFromMidnight + offset)
|
||||
.toDate();
|
||||
}
|
||||
|
||||
// dayjs will automatically handle DST differences in it's calculations
|
||||
function getTotalMin(start, end) {
|
||||
return diff(start, end, "minutes");
|
||||
}
|
||||
|
||||
function getMinutesFromMidnight(start) {
|
||||
var dayStart = dayjs(start).startOf("day");
|
||||
var day = dayjs(start);
|
||||
return day.diff(dayStart, "minutes") + getDayStartDstOffset(start);
|
||||
}
|
||||
|
||||
// These two are used by DateSlotMetrics
|
||||
function continuesPrior(start, first) {
|
||||
var djStart = dayjs(start);
|
||||
var djFirst = dayjs(first);
|
||||
return djStart.isBefore(djFirst, "day");
|
||||
}
|
||||
|
||||
function continuesAfter(start, end, last) {
|
||||
var djEnd = dayjs(end);
|
||||
var djLast = dayjs(last);
|
||||
return djEnd.isSameOrAfter(djLast, "minutes");
|
||||
}
|
||||
|
||||
function daySpan(start, end) {
|
||||
var startDay = dayjs(start);
|
||||
var endDay = dayjs(end);
|
||||
return endDay.diff(startDay, "day");
|
||||
}
|
||||
|
||||
// These two are used by eventLevels
|
||||
function sortEvents(_ref6) {
|
||||
var _ref6$evtA = _ref6.evtA,
|
||||
aStart = _ref6$evtA.start,
|
||||
aEnd = _ref6$evtA.end,
|
||||
aAllDay = _ref6$evtA.allDay,
|
||||
_ref6$evtB = _ref6.evtB,
|
||||
bStart = _ref6$evtB.start,
|
||||
bEnd = _ref6$evtB.end,
|
||||
bAllDay = _ref6$evtB.allDay;
|
||||
var startSort = +startOf(aStart, "day") - +startOf(bStart, "day");
|
||||
var durA = daySpan(aStart, aEnd);
|
||||
var durB = daySpan(bStart, bEnd);
|
||||
return (
|
||||
startSort ||
|
||||
// sort by start Day first
|
||||
durB - durA ||
|
||||
// events spanning multiple days go first
|
||||
!!bAllDay - !!aAllDay ||
|
||||
// then allDay single day events
|
||||
+aStart - +bStart ||
|
||||
// then sort by start time *don't need dayjs conversion here
|
||||
+aEnd - +bEnd // then sort by end time *don't need dayjs conversion here either
|
||||
);
|
||||
}
|
||||
|
||||
function inEventRange(_ref7) {
|
||||
var _ref7$event = _ref7.event,
|
||||
start = _ref7$event.start,
|
||||
end = _ref7$event.end,
|
||||
_ref7$range = _ref7.range,
|
||||
rangeStart = _ref7$range.start,
|
||||
rangeEnd = _ref7$range.end;
|
||||
var startOfDay = dayjs(start).startOf("day");
|
||||
var eEnd = dayjs(end);
|
||||
var rStart = dayjs(rangeStart);
|
||||
var rEnd = dayjs(rangeEnd);
|
||||
var startsBeforeEnd = startOfDay.isSameOrBefore(rEnd, "day");
|
||||
// when the event is zero duration we need to handle a bit differently
|
||||
var sameMin = !startOfDay.isSame(eEnd, "minutes");
|
||||
var endsAfterStart = sameMin ? eEnd.isAfter(rStart, "minutes") : eEnd.isSameOrAfter(rStart, "minutes");
|
||||
return startsBeforeEnd && endsAfterStart;
|
||||
}
|
||||
|
||||
function isSameDate(date1, date2) {
|
||||
var dt = dayjs(date1);
|
||||
var dt2 = dayjs(date2);
|
||||
return dt.isSame(dt2, "day");
|
||||
}
|
||||
|
||||
/**
|
||||
* This method, called once in the localizer constructor, is used by eventLevels
|
||||
* 'eventSegments()' to assist in determining the 'span' of the event in the display,
|
||||
* specifically when using a timezone that is greater than the browser native timezone.
|
||||
* @returns number
|
||||
*/
|
||||
function browserTZOffset() {
|
||||
/**
|
||||
* Date.prototype.getTimezoneOffset horrifically flips the positive/negative from
|
||||
* what you see in it's string, so we have to jump through some hoops to get a value
|
||||
* we can actually compare.
|
||||
*/
|
||||
var dt = new Date();
|
||||
var neg = /-/.test(dt.toString()) ? "-" : "";
|
||||
var dtOffset = dt.getTimezoneOffset();
|
||||
var comparator = Number("".concat(neg).concat(Math.abs(dtOffset)));
|
||||
// dayjs correctly provides positive/negative offset, as expected
|
||||
var mtOffset = dayjs().utcOffset();
|
||||
return mtOffset > comparator ? 1 : 0;
|
||||
}
|
||||
|
||||
return new DateLocalizer({
|
||||
formats: formats,
|
||||
firstOfWeek: firstOfWeek,
|
||||
firstVisibleDay: firstVisibleDay,
|
||||
lastVisibleDay: lastVisibleDay,
|
||||
visibleDays: visibleDays,
|
||||
format: function format(value, _format, culture) {
|
||||
return locale(dayjs(value), culture).format(_format);
|
||||
},
|
||||
lt: lt,
|
||||
lte: lte,
|
||||
gt: gt,
|
||||
gte: gte,
|
||||
eq: eq,
|
||||
neq: neq,
|
||||
merge: merge,
|
||||
inRange: inRange,
|
||||
startOf: startOf,
|
||||
endOf: endOf,
|
||||
range: range,
|
||||
add: add,
|
||||
diff: diff,
|
||||
ceil: ceil,
|
||||
min: min,
|
||||
max: max,
|
||||
minutes: minutes,
|
||||
getSlotDate: getSlotDate,
|
||||
getTimezoneOffset: getTimezoneOffset,
|
||||
getDstOffset: getDstOffset,
|
||||
getTotalMin: getTotalMin,
|
||||
getMinutesFromMidnight: getMinutesFromMidnight,
|
||||
continuesPrior: continuesPrior,
|
||||
continuesAfter: continuesAfter,
|
||||
sortEvents: sortEvents,
|
||||
inEventRange: inEventRange,
|
||||
isSameDate: isSameDate,
|
||||
browserTZOffset: browserTZOffset
|
||||
});
|
||||
};
|
||||
export default localizer;
|
||||
@@ -1,7 +1,7 @@
|
||||
import dayjs from "../../utils/day";
|
||||
import queryString from "query-string";
|
||||
import React from "react";
|
||||
import { Calendar, dayjsLocalizer } from "react-big-calendar";
|
||||
import { Calendar } from "react-big-calendar";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -14,12 +14,13 @@ import { selectProblemJobs } from "../../redux/application/application.selectors
|
||||
import { Alert, Collapse, Space } from "antd";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import local from "./localizer";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
problemJobs: selectProblemJobs
|
||||
});
|
||||
const localizer = dayjsLocalizer(dayjs);
|
||||
const localizer = local(dayjs);
|
||||
|
||||
export function ScheduleCalendarWrapperComponent({
|
||||
bodyshop,
|
||||
|
||||
@@ -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;
|
||||
@@ -87,7 +87,7 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
|
||||
operationName: eventName,
|
||||
variables: additionalParams,
|
||||
dbevent: false,
|
||||
env: "master"
|
||||
env: `master-AIO|${import.meta.env.VITE_APP_GIT_SHA_DATE}`
|
||||
});
|
||||
// console.log(
|
||||
// "%c[Analytics]",
|
||||
|
||||
@@ -2461,6 +2461,14 @@ export const SUBSCRIPTION_JOBS_IN_PRODUCTION = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW = gql`
|
||||
subscription SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW {
|
||||
jobs: jobs_inproduction {
|
||||
id
|
||||
updated_at
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const QUERY_JOBS_IN_PRODUCTION = gql`
|
||||
query QUERY_JOBS_IN_PRODUCTION {
|
||||
|
||||
@@ -23,17 +23,14 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
|
||||
|
||||
export const socket = SocketIO(
|
||||
import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : window.location.origin,
|
||||
{
|
||||
path: "/ws",
|
||||
withCredentials: true,
|
||||
auth: async (callback) => {
|
||||
const token = auth.currentUser && (await auth.currentUser.getIdToken());
|
||||
callback({ token });
|
||||
}
|
||||
export const socket = SocketIO(import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "", {
|
||||
path: "/ws",
|
||||
withCredentials: true,
|
||||
auth: async (callback) => {
|
||||
const token = auth.currentUser && (await auth.currentUser.getIdToken());
|
||||
callback({ token });
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -34,7 +34,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
|
||||
|
||||
export const socket = SocketIO(
|
||||
import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "http://localhost:4000", // for dev testing,
|
||||
import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "", // for dev testing,
|
||||
{
|
||||
path: "/ws",
|
||||
withCredentials: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FloatButton, Layout, Spin } from "antd";
|
||||
// import preval from "preval.macro";
|
||||
import React, { lazy, Suspense, useEffect, useState } from "react";
|
||||
import React, { lazy, Suspense, useContext, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, Route, Routes } from "react-router-dom";
|
||||
@@ -19,11 +19,13 @@ import PartnerPingComponent from "../../components/partner-ping/partner-ping.com
|
||||
import PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container";
|
||||
import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component";
|
||||
import { requestForToken } from "../../firebase/firebase.utils";
|
||||
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||
import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors";
|
||||
|
||||
import UpdateAlert from "../../components/update-alert/update-alert.component";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
||||
import "./manage.page.styles.scss";
|
||||
import WssStatusDisplayComponent from "../../components/wss-status-display/wss-status-display.component.jsx";
|
||||
|
||||
const JobsPage = lazy(() => import("../jobs/jobs.page"));
|
||||
|
||||
@@ -110,6 +112,7 @@ const mapDispatchToProps = (dispatch) => ({});
|
||||
export function Manage({ conflict, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
const [chatVisible] = useState(false);
|
||||
const { socket, clientId } = useContext(SocketContext);
|
||||
|
||||
useEffect(() => {
|
||||
const widgetId = InstanceRenderManager({
|
||||
@@ -129,6 +132,7 @@ export function Manage({ conflict, bodyshop }) {
|
||||
promanager: t("titles.promanager")
|
||||
});
|
||||
}, [t]);
|
||||
|
||||
const AppRouteTable = (
|
||||
<Suspense
|
||||
fallback={
|
||||
@@ -569,6 +573,13 @@ export function Manage({ conflict, bodyshop }) {
|
||||
else if (bodyshop && bodyshop.sub_status !== "active") PageContent = <ShopSubStatusComponent />;
|
||||
else PageContent = AppRouteTable;
|
||||
|
||||
const broadcastMessage = () => {
|
||||
if (socket && bodyshop && bodyshop.id) {
|
||||
console.log(`Broadcasting message to bodyshop ${bodyshop.id}:`);
|
||||
socket.emit("broadcast-to-bodyshop", bodyshop.id, `Hello from ${clientId}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{import.meta.env.PROD && <ChatAffixContainer bodyshop={bodyshop} chatVisible={chatVisible} />}
|
||||
@@ -594,7 +605,8 @@ export function Manage({ conflict, bodyshop }) {
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex" }}>
|
||||
<div>
|
||||
<WssStatusDisplayComponent />
|
||||
<div onClick={broadcastMessage}>
|
||||
{`${InstanceRenderManager({
|
||||
imex: t("titles.imexonline"),
|
||||
rome: t("titles.romeonline"),
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
import React from "react";
|
||||
import ProductionBoardKanbanContainer from "../../components/production-board-kanban/production-board-kanban.container";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ProductionBoardComponent);
|
||||
|
||||
export default function ProductionBoardComponent() {
|
||||
return <ProductionBoardKanbanContainer />;
|
||||
export function ProductionBoardComponent({ bodyshop }) {
|
||||
const {
|
||||
treatments: { Production_Use_View }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Production_Use_View"],
|
||||
splitKey: bodyshop && bodyshop.imexshopid
|
||||
});
|
||||
|
||||
return <ProductionBoardKanbanContainer subscriptionType={Production_Use_View.treatment} />;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,31 @@ import React from "react";
|
||||
import NoteUpsertModal from "../../components/note-upsert-modal/note-upsert-modal.container";
|
||||
import ProductionListTable from "../../components/production-list-table/production-list-table.container";
|
||||
|
||||
export default function ProductionListComponent() {
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ProductionListComponent);
|
||||
|
||||
export function ProductionListComponent({ bodyshop }) {
|
||||
const {
|
||||
treatments: { Production_Use_View }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Production_Use_View"],
|
||||
splitKey: bodyshop && bodyshop.imexshopid
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<NoteUpsertModal />
|
||||
<ProductionListTable />
|
||||
<ProductionListTable bodyshop={bodyshop} subscriptionType={Production_Use_View.treatment} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,3 +67,7 @@ export const setUpdateAvailable = (isUpdateAvailable) => ({
|
||||
type: ApplicationActionTypes.SET_UPDATE_AVAILABLE,
|
||||
payload: isUpdateAvailable
|
||||
});
|
||||
export const setWssStatus = (status) => ({
|
||||
type: ApplicationActionTypes.SET_WSS_STATUS,
|
||||
payload: status
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import ApplicationActionTypes from "./application.types";
|
||||
const INITIAL_STATE = {
|
||||
loading: false,
|
||||
online: true,
|
||||
wssStatus: "disconnected",
|
||||
updateAvailable: false,
|
||||
breadcrumbs: [],
|
||||
recentItems: [],
|
||||
@@ -87,6 +88,9 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
|
||||
case ApplicationActionTypes.SET_PROBLEM_JOBS: {
|
||||
return { ...state, problemJobs: action.payload };
|
||||
}
|
||||
case ApplicationActionTypes.SET_WSS_STATUS: {
|
||||
return { ...state, wssStatus: action.payload };
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -22,3 +22,4 @@ export const selectJobReadOnly = createSelector([selectApplication], (applicatio
|
||||
export const selectOnline = createSelector([selectApplication], (application) => application.online);
|
||||
export const selectProblemJobs = createSelector([selectApplication], (application) => application.problemJobs);
|
||||
export const selectUpdateAvailable = createSelector([selectApplication], (application) => application.updateAvailable);
|
||||
export const selectWssStatus = createSelector([selectApplication], (application) => application.wssStatus);
|
||||
|
||||
@@ -12,6 +12,7 @@ const ApplicationActionTypes = {
|
||||
SET_ONLINE_STATUS: "SET_ONLINE_STATUS",
|
||||
INSERT_AUDIT_TRAIL: "INSERT_AUDIT_TRAIL",
|
||||
SET_PROBLEM_JOBS: "SET_PROBLEM_JOBS",
|
||||
SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE"
|
||||
SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE",
|
||||
SET_WSS_STATUS: "SET_WSS_STATUS"
|
||||
};
|
||||
export default ApplicationActionTypes;
|
||||
|
||||
@@ -36,7 +36,8 @@ export function* openChatByPhone({ payload }) {
|
||||
data: { conversations }
|
||||
} = yield client.query({
|
||||
query: CONVERSATION_ID_BY_PHONE,
|
||||
variables: { phone: p.number }
|
||||
variables: { phone: p.number },
|
||||
fetchPolicy: 'no-cache'
|
||||
});
|
||||
|
||||
if (conversations.length === 0) {
|
||||
|
||||
@@ -242,6 +242,10 @@ export function* signInSuccessSaga({ payload }) {
|
||||
window.$crisp.push(["set", "user:nickname", [payload.displayName || payload.email]]);
|
||||
window.$crisp.push(["set", "session:segments", [["user"]]]);
|
||||
},
|
||||
rome: () => {
|
||||
window.$zoho.salesiq.visitor.name(payload.displayName || payload.email);
|
||||
window.$zoho.salesiq.visitor.email(payload.email);
|
||||
},
|
||||
promanager: () => {
|
||||
Userpilot.identify(payload.email, {
|
||||
email: payload.email
|
||||
@@ -310,8 +314,8 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
||||
try {
|
||||
const userEmail = yield select((state) => state.user.currentUser.email);
|
||||
try {
|
||||
//console.log("Setting shop timezone.");
|
||||
// dayjs.tz.setDefault(payload.timezone);
|
||||
console.log("Setting shop timezone.");
|
||||
day.tz.setDefault(payload.timezone);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
@@ -371,6 +375,9 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
||||
if (authRecord[0] && authRecord[0].user.validemail) {
|
||||
window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]);
|
||||
}
|
||||
},
|
||||
rome: () => {
|
||||
window.$zoho.salesiq.visitor.info({ "Shop Name": payload.shopname });
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
"jobclosedwithbypass": "Job was invoiced using the master bypass password. ",
|
||||
"jobconverted": "Job converted and assigned number {{ro_number}}.",
|
||||
"jobdelivery": "Job intake completed. Status set to {{status}}. Actual completion is {{actual_completion}}.",
|
||||
"jobexported": "",
|
||||
"jobexported": "Job has been exported",
|
||||
"jobfieldchanged": "Job field $t(jobs.fields.{{field}}) changed to {{value}}.",
|
||||
"jobimported": "Job imported.",
|
||||
"jobinproductionchange": "Job production status set to {{inproduction}}",
|
||||
@@ -1373,6 +1373,7 @@
|
||||
},
|
||||
"job_payments": {
|
||||
"buttons": {
|
||||
"create_short_link": "Generate Short Link",
|
||||
"goback": "Go Back",
|
||||
"proceedtopayment": "Proceed to Payment",
|
||||
"refundpayment": "Refund Payment"
|
||||
@@ -2848,15 +2849,21 @@
|
||||
"jobs_in_production": "Jobs in Production",
|
||||
"tasks_in_production": "Tasks in Production",
|
||||
"tasks_on_board": "Tasks on Board",
|
||||
"tasks_in_view": "Tasks in View",
|
||||
"total_amount_in_production": "Dollars in Production",
|
||||
"total_amount_on_board": "Dollars on Board",
|
||||
"total_amount_in_view": "Dollars in View",
|
||||
"total_hours_in_production": "Hours in Production",
|
||||
"total_hours_on_board": "Hours on Board",
|
||||
"total_hours_in_view": "Hours in View",
|
||||
"total_jobs_on_board": "Jobs on Board",
|
||||
"total_jobs_in_view": "Jobs in View",
|
||||
"total_lab_in_production": "Body Hours in Production",
|
||||
"total_lab_on_board": "Body Hours on Board",
|
||||
"total_lab_in_view": "Body Hours in View",
|
||||
"total_lar_in_production": "Refinish Hours in Production",
|
||||
"total_lar_on_board": "Refinish Hours on Board"
|
||||
"total_lar_on_board": "Refinish Hours on Board",
|
||||
"total_lar_in_view": "Refinish Hours in View"
|
||||
},
|
||||
"statistics_title": "Statistics"
|
||||
},
|
||||
@@ -2868,15 +2875,21 @@
|
||||
"tasks": "Tasks",
|
||||
"tasks_in_production": "Tasks in Production",
|
||||
"tasks_on_board": "Tasks on Board",
|
||||
"tasks_in_view": "Tasks in View",
|
||||
"total_amount_in_production": "Dollars in Production",
|
||||
"total_amount_on_board": "Dollars on Board",
|
||||
"total_amount_in_view": "Dollars in View",
|
||||
"total_hours_in_production": "Hours in Production",
|
||||
"total_hours_on_board": "Hours on Board",
|
||||
"total_hours_in_view": "Hours in View",
|
||||
"total_jobs_on_board": "Jobs on Board",
|
||||
"total_jobs_in_view": "Jobs in View",
|
||||
"total_lab_in_production": "Body Hours in Production",
|
||||
"total_lab_on_board": "Body Hours on Board",
|
||||
"total_lab_in_view": "Body Hours in View",
|
||||
"total_lar_in_production": "Refinish Hours in Production",
|
||||
"total_lar_on_board": "Refinish Hours on Board"
|
||||
"total_lar_on_board": "Refinish Hours on Board",
|
||||
"total_lar_in_view": "Refinish Hours in View"
|
||||
},
|
||||
"successes": {
|
||||
"removed": "Job removed from production."
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1373,6 +1373,7 @@
|
||||
},
|
||||
"job_payments": {
|
||||
"buttons": {
|
||||
"create_short_link": "",
|
||||
"goback": "",
|
||||
"proceedtopayment": "",
|
||||
"refundpayment": ""
|
||||
@@ -2844,40 +2845,52 @@
|
||||
"filters_title": "",
|
||||
"information": "",
|
||||
"layout": "",
|
||||
"statistics": {
|
||||
"jobs_in_production": "",
|
||||
"tasks_in_production": "",
|
||||
"tasks_on_board": "",
|
||||
"total_amount_in_production": "",
|
||||
"total_amount_on_board": "",
|
||||
"total_hours_in_production": "",
|
||||
"total_hours_on_board": "",
|
||||
"total_jobs_on_board": "",
|
||||
"total_lab_in_production": "",
|
||||
"total_lab_on_board": "",
|
||||
"total_lar_in_production": "",
|
||||
"total_lar_on_board": ""
|
||||
},
|
||||
"statistics": {
|
||||
"jobs_in_production": "",
|
||||
"tasks_in_production": "",
|
||||
"tasks_on_board": "",
|
||||
"tasks_in_view": "",
|
||||
"total_amount_in_production": "",
|
||||
"total_amount_on_board": "",
|
||||
"total_amount_in_view": "",
|
||||
"total_hours_in_production": "",
|
||||
"total_hours_on_board": "",
|
||||
"total_hours_in_view": "",
|
||||
"total_jobs_on_board": "",
|
||||
"total_jobs_in_view": "",
|
||||
"total_lab_in_production": "",
|
||||
"total_lab_on_board": "",
|
||||
"total_lab_in_view": "",
|
||||
"total_lar_in_production": "",
|
||||
"total_lar_on_board": "",
|
||||
"total_lar_in_view": ""
|
||||
},
|
||||
"statistics_title": ""
|
||||
},
|
||||
"statistics": {
|
||||
"currency_symbol": "",
|
||||
"hours": "",
|
||||
"jobs": "",
|
||||
"jobs_in_production": "",
|
||||
"tasks": "",
|
||||
"tasks_in_production": "",
|
||||
"tasks_on_board": "",
|
||||
"total_amount_in_production": "",
|
||||
"total_amount_on_board": "",
|
||||
"total_hours_in_production": "",
|
||||
"total_hours_on_board": "",
|
||||
"total_jobs_on_board": "",
|
||||
"total_lab_in_production": "",
|
||||
"total_lab_on_board": "",
|
||||
"total_lar_in_production": "",
|
||||
"total_lar_on_board": ""
|
||||
},
|
||||
"statistics": {
|
||||
"currency_symbol": "",
|
||||
"hours": "",
|
||||
"jobs": "",
|
||||
"jobs_in_production": "",
|
||||
"tasks": "",
|
||||
"tasks_in_production": "",
|
||||
"tasks_on_board": "",
|
||||
"tasks_in_view": "",
|
||||
"total_amount_in_production": "",
|
||||
"total_amount_on_board": "",
|
||||
"total_amount_in_view": "",
|
||||
"total_hours_in_production": "",
|
||||
"total_hours_on_board": "",
|
||||
"total_hours_in_view": "",
|
||||
"total_jobs_on_board": "",
|
||||
"total_jobs_in_view": "",
|
||||
"total_lab_in_production": "",
|
||||
"total_lab_on_board": "",
|
||||
"total_lab_in_view": "",
|
||||
"total_lar_in_production": "",
|
||||
"total_lar_on_board": "",
|
||||
"total_lar_in_view": ""
|
||||
},
|
||||
"successes": {
|
||||
"removed": ""
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import axios from "axios";
|
||||
import { auth } from "../firebase/firebase.utils";
|
||||
import InstanceRenderManager from "./instanceRenderMgr";
|
||||
|
||||
axios.defaults.baseURL =
|
||||
import.meta.env.VITE_APP_AXIOS_BASE_API_URL ||
|
||||
(import.meta.env.MODE === "production" ? "https://api.imex.online/" : "http://localhost:4000/");
|
||||
axios.defaults.baseURL = import.meta.env.DEV
|
||||
? "/api/"
|
||||
: import.meta.env.VITE_APP_AXIOS_BASE_API_URL || "https://api.imex.online/";
|
||||
|
||||
export const axiosAuthInterceptorId = axios.interceptors.request.use(
|
||||
async (config) => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Sometimes referred to as PageSize, this variable controls the amount of records
|
||||
// to show on one page during pagination.
|
||||
export const pageLimit = 50;
|
||||
export const exportPageLimit = 10;
|
||||
|
||||
@@ -2,5 +2,5 @@ import { store } from "../redux/store";
|
||||
|
||||
export function CreateExplorerLinkForJob({ jobid }) {
|
||||
const bodyshop = store.getState().user.bodyshop;
|
||||
return `imexmedia://${bodyshop.localmediaservernetwork}\\Jobs\\${jobid}`;
|
||||
return `imexmedia://`.concat(encodeURIComponent(`${bodyshop.localmediaservernetwork}\\Jobs\\${jobid}`));
|
||||
}
|
||||
|
||||
@@ -3,16 +3,22 @@ import { promises as fsPromises } from "fs";
|
||||
import { createRequire } from "module";
|
||||
import * as path from "path";
|
||||
import * as url from "url";
|
||||
import { defineConfig } from "vite";
|
||||
import { createLogger, defineConfig } from "vite";
|
||||
import { ViteEjsPlugin } from "vite-plugin-ejs";
|
||||
import eslint from "vite-plugin-eslint";
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
import InstanceRenderManager from "./src/utils/instanceRenderMgr";
|
||||
import chalk from "chalk";
|
||||
//import { visualizer } from "rollup-plugin-visualizer";
|
||||
|
||||
process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", {
|
||||
timeZone: "America/Los_Angeles"
|
||||
});
|
||||
|
||||
const getFormattedTimestamp = () =>
|
||||
new Date().toLocaleTimeString("en-US", { hour12: true }).replace("AM", "a.m.").replace("PM", "p.m.");
|
||||
|
||||
/** This is a hack around react-virtualized, should be removed when switching to react-virtuoso */
|
||||
const WRONG_CODE = `import { bpfrpt_proptype_WindowScroller } from "../WindowScroller.js";`;
|
||||
|
||||
function reactVirtualizedFix() {
|
||||
@@ -32,10 +38,16 @@ function reactVirtualizedFix() {
|
||||
}
|
||||
};
|
||||
}
|
||||
/** End of hack */
|
||||
|
||||
export const logger = createLogger("info", {
|
||||
allowClearScreen: false
|
||||
});
|
||||
|
||||
export default defineConfig({
|
||||
base: "/",
|
||||
plugins: [
|
||||
//visualizer(),
|
||||
ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })),
|
||||
VitePWA({
|
||||
injectRegister: "auto",
|
||||
@@ -99,7 +111,6 @@ export default defineConfig({
|
||||
reactVirtualizedFix(),
|
||||
react(),
|
||||
eslint()
|
||||
// CompressionPlugin(), //Cloudfront already compresses assets, so not needed.
|
||||
],
|
||||
define: {
|
||||
APP_VERSION: JSON.stringify(process.env.npm_package_version)
|
||||
@@ -107,7 +118,69 @@ export default defineConfig({
|
||||
server: {
|
||||
host: true,
|
||||
port: 3000,
|
||||
open: true
|
||||
open: true,
|
||||
proxy: {
|
||||
"/ws": {
|
||||
target: "ws://localhost:4000",
|
||||
rewriteWsOrigin: true,
|
||||
secure: false,
|
||||
ws: true
|
||||
},
|
||||
"/wss": {
|
||||
target: "ws://localhost:4000",
|
||||
rewriteWsOrigin: true,
|
||||
secure: false,
|
||||
ws: true
|
||||
},
|
||||
"/api": {
|
||||
target: "http://localhost:4000",
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
ws: false,
|
||||
rewrite: (path) => {
|
||||
const replacedValue = path.replace(/^\/api/, "");
|
||||
logger.info(
|
||||
`${chalk.grey.bold(getFormattedTimestamp())} ${chalk.cyan.bold("[vite]")} ${chalk.green.bold("[API]")} ${chalk.blue(replacedValue)}`
|
||||
);
|
||||
return replacedValue;
|
||||
}
|
||||
}
|
||||
},
|
||||
https: {
|
||||
key: await fsPromises.readFile("../certs/key.pem"),
|
||||
cert: await fsPromises.readFile("../certs/cert.pem"),
|
||||
allowHTTP1: false // Force HTTP/2
|
||||
}
|
||||
},
|
||||
preview: {
|
||||
port: 6000,
|
||||
host: true,
|
||||
open: true,
|
||||
https: {
|
||||
key: await fsPromises.readFile("../certs/key.pem"),
|
||||
cert: await fsPromises.readFile("../certs/cert.pem"),
|
||||
allowHTTP1: false // Force HTTP/2
|
||||
},
|
||||
proxy: {
|
||||
"/ws": {
|
||||
target: "ws://localhost:4000",
|
||||
rewriteWsOrigin: true,
|
||||
secure: false,
|
||||
ws: true
|
||||
},
|
||||
"/wss": {
|
||||
target: "ws://localhost:4000",
|
||||
rewriteWsOrigin: true,
|
||||
secure: false,
|
||||
ws: true
|
||||
},
|
||||
"/api": {
|
||||
target: "http://localhost:4000",
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
ws: false
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
@@ -115,17 +188,63 @@ export default defineConfig({
|
||||
manualChunks: {
|
||||
antd: ["antd"],
|
||||
"react-redux": ["react-redux"],
|
||||
redux: ["redux"]
|
||||
redux: ["redux"],
|
||||
lodash: ["lodash"],
|
||||
"@sentry/react": ["@sentry/react"],
|
||||
"@splitsoftware/splitio-react": ["@splitsoftware/splitio-react"],
|
||||
logrocket: ["logrocket"],
|
||||
"firebase/app": ["firebase/app"],
|
||||
"firebase/firestore": ["firebase/firestore"],
|
||||
"firebase/firestore/lite": ["firebase/firestore/lite"],
|
||||
"firebase/auth": ["firebase/auth"],
|
||||
"firebase/functions": ["firebase/functions"],
|
||||
"firebase/storage": ["firebase/storage"],
|
||||
"firebase/database": ["firebase/database"],
|
||||
"firebase/remote-config": ["firebase/remote-config"],
|
||||
"firebase/performance": ["firebase/performance"],
|
||||
"@firebase/app": ["@firebase/app"],
|
||||
"@firebase/firestore": ["@firebase/firestore"],
|
||||
"@firebase/firestore/lite": ["@firebase/firestore/lite"],
|
||||
"@firebase/auth": ["@firebase/auth"],
|
||||
"@firebase/functions": ["@firebase/functions"],
|
||||
"@firebase/storage": ["@firebase/storage"],
|
||||
"@firebase/database": ["@firebase/database"],
|
||||
"@firebase/remote-config": ["@firebase/remote-config"],
|
||||
"@firebase/performance": ["@firebase/performance"],
|
||||
markerjs2: ["markerjs2"],
|
||||
"@apollo/client": ["@apollo/client"],
|
||||
"libphonenumber-js": ["libphonenumber-js"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ["react", "react-dom", "antd", "@apollo/client", "@reduxjs/toolkit", "axios"],
|
||||
include: [
|
||||
"react",
|
||||
"react-dom",
|
||||
"antd",
|
||||
"lodash",
|
||||
"@sentry/react",
|
||||
"@apollo/client",
|
||||
"@reduxjs/toolkit",
|
||||
"axios",
|
||||
"react-router-dom",
|
||||
"dayjs",
|
||||
"redux",
|
||||
"react-redux"
|
||||
],
|
||||
esbuildOptions: {
|
||||
loader: {
|
||||
".js": "jsx"
|
||||
}
|
||||
}
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
api: "modern-compiler",
|
||||
quietDeps: true // Quite Deprecation Warnings, should be disabled occasionally before major upgrades
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
179
docker-compose.yml
Normal file
179
docker-compose.yml
Normal file
@@ -0,0 +1,179 @@
|
||||
#############################
|
||||
# Ports Exposed
|
||||
# 4000 - Imex Node API
|
||||
# 4556 - LocalStack (Local AWS)
|
||||
# 3333 - SocketIO Admin-UI (Optional)
|
||||
# 3334 - Redis-Insights (Optional)
|
||||
#############################
|
||||
|
||||
services:
|
||||
|
||||
# Redis Node 1
|
||||
redis-node-1:
|
||||
build:
|
||||
context: ./redis
|
||||
container_name: redis-node-1
|
||||
hostname: redis-node-1
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- redis-cluster-net
|
||||
volumes:
|
||||
- redis-node-1-data:/data
|
||||
- redis-lock:/redis-lock
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
# Redis Node 2
|
||||
redis-node-2:
|
||||
build:
|
||||
context: ./redis
|
||||
container_name: redis-node-2
|
||||
hostname: redis-node-2
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- redis-cluster-net
|
||||
volumes:
|
||||
- redis-node-2-data:/data
|
||||
- redis-lock:/redis-lock
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
# Redis Node 3
|
||||
redis-node-3:
|
||||
build:
|
||||
context: ./redis
|
||||
container_name: redis-node-3
|
||||
hostname: redis-node-3
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- redis-cluster-net
|
||||
volumes:
|
||||
- redis-node-3-data:/data
|
||||
- redis-lock:/redis-lock
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
# LocalStack: Used to emulate AWS services locally, currently setup for SES
|
||||
# Notes: Set the ENV Debug to 1 for additional logging
|
||||
localstack:
|
||||
image: localstack/localstack
|
||||
container_name: localstack
|
||||
hostname: localstack
|
||||
networks:
|
||||
- redis-cluster-net
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
- SERVICES=ses,secretsmanager
|
||||
- DEBUG=0
|
||||
- AWS_ACCESS_KEY_ID=test
|
||||
- AWS_SECRET_ACCESS_KEY=test
|
||||
- AWS_DEFAULT_REGION=ca-central-1
|
||||
- EXTRA_CORS_ALLOWED_HEADERS=Authorization,Content-Type
|
||||
- EXTRA_CORS_ALLOWED_ORIGINS=*
|
||||
- EXTRA_CORS_EXPOSE_HEADERS=Authorization,Content-Type
|
||||
ports:
|
||||
- "4566:4566"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
|
||||
# AWS-CLI - Used in conjunction with LocalStack to set required permission to send emails
|
||||
aws-cli:
|
||||
image: amazon/aws-cli
|
||||
container_name: aws-cli
|
||||
hostname: aws-cli
|
||||
networks:
|
||||
- redis-cluster-net
|
||||
depends_on:
|
||||
localstack:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- './localstack:/tmp/localstack'
|
||||
- './certs:/tmp/certs'
|
||||
|
||||
environment:
|
||||
- AWS_ACCESS_KEY_ID=test
|
||||
- AWS_SECRET_ACCESS_KEY=test
|
||||
- AWS_DEFAULT_REGION=ca-central-1
|
||||
entrypoint: /bin/sh -c
|
||||
command: >
|
||||
"
|
||||
aws --endpoint-url=http://localstack:4566 ses verify-domain-identity --domain imex.online --region ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 ses verify-email-identity --email-address noreply@imex.online --region ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 secretsmanager create-secret --name CHATTER_PRIVATE_KEY --secret-string file:///tmp/certs/id_rsa
|
||||
"
|
||||
# Node App: The Main IMEX API
|
||||
node-app:
|
||||
build:
|
||||
context: .
|
||||
container_name: node-app
|
||||
hostname: imex-api
|
||||
networks:
|
||||
- redis-cluster-net
|
||||
env_file:
|
||||
- .env.development
|
||||
depends_on:
|
||||
redis-node-1:
|
||||
condition: service_healthy
|
||||
redis-node-2:
|
||||
condition: service_healthy
|
||||
redis-node-3:
|
||||
condition: service_healthy
|
||||
localstack:
|
||||
condition: service_healthy
|
||||
aws-cli:
|
||||
condition: service_completed_successfully
|
||||
ports:
|
||||
- "4000:4000"
|
||||
- "9229:9229"
|
||||
volumes:
|
||||
- .:/app
|
||||
- node-app-npm-cache:/app/node_modules
|
||||
|
||||
# ## Optional Container to Observe SocketIO data
|
||||
# socketio-admin-ui:
|
||||
# image: maitrungduc1410/socket.io-admin-ui
|
||||
# container_name: socketio-admin-ui
|
||||
# networks:
|
||||
# - redis-cluster-net
|
||||
# ports:
|
||||
# - "3333:80"
|
||||
|
||||
# ##Optional Container to Observe Redis Cluster Data
|
||||
# redis-insight:
|
||||
# image: redislabs/redisinsight:latest
|
||||
# container_name: redis-insight
|
||||
# hostname: redis-insight
|
||||
# restart: unless-stopped
|
||||
# ports:
|
||||
# - "3334:5540"
|
||||
# networks:
|
||||
# - redis-cluster-net
|
||||
# volumes:
|
||||
# - redis-insight-data:/db
|
||||
|
||||
networks:
|
||||
redis-cluster-net:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
node-app-npm-cache:
|
||||
redis-node-1-data:
|
||||
redis-node-2-data:
|
||||
redis-node-3-data:
|
||||
redis-lock:
|
||||
redis-insight-data:
|
||||
@@ -1,4 +1,4 @@
|
||||
Must set the environment variables using:
|
||||
|
||||
firebase functions:config:set auth.graphql_endpoint="https://db.dev.bodyshop.app/v1/graphql"
|
||||
firebase functions:config:set auth.graphql_endpoint="https://db.dev.imex.online/v1/graphql"
|
||||
auth.hasura_secret_admin_key="Dev-BodyShopApp!"
|
||||
|
||||
@@ -918,6 +918,7 @@
|
||||
- bill_tax_rates
|
||||
- cdk_configuration
|
||||
- cdk_dealerid
|
||||
- chatterid
|
||||
- city
|
||||
- claimscorpid
|
||||
- convenient_company
|
||||
@@ -4358,6 +4359,35 @@
|
||||
template_engine: Kriti
|
||||
url: '{{$base_url}}/opensearch'
|
||||
version: 2
|
||||
- table:
|
||||
name: jobs_inproduction
|
||||
schema: public
|
||||
object_relationships:
|
||||
- name: bodyshop
|
||||
using:
|
||||
manual_configuration:
|
||||
column_mapping:
|
||||
shopid: id
|
||||
insertion_order: null
|
||||
remote_table:
|
||||
name: bodyshops
|
||||
schema: public
|
||||
select_permissions:
|
||||
- role: user
|
||||
permission:
|
||||
columns:
|
||||
- id
|
||||
- shopid
|
||||
- updated_at
|
||||
filter:
|
||||
bodyshop:
|
||||
associations:
|
||||
_and:
|
||||
- user:
|
||||
authid:
|
||||
_eq: X-Hasura-User-Id
|
||||
- active:
|
||||
_eq: true
|
||||
- table:
|
||||
name: masterdata
|
||||
schema: public
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."bodyshops" add column "chatterid" text
|
||||
-- null;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."bodyshops" add column "chatterid" text
|
||||
null;
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE INDEX "courtesycars_idx_fleet" on
|
||||
"public"."courtesycars" using btree ("fleetnumber");
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS "public"."courtesycars_idx_fleet";
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE INDEX "idx_jobs_ownrfn" on
|
||||
"public"."jobs" using gin ("ownr_fn");
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS "public"."idx_jobs_ownrfn";
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE INDEX "idx_jobs_ownrln" on
|
||||
"public"."jobs" using gin ("ownr_ln");
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS "public"."idx_jobs_ownrln";
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE INDEX "jobs_idx_iouparent" on
|
||||
"public"."jobs" using btree ("iouparent");
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS "public"."jobs_idx_iouparent";
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE INDEX "idx_jobs_ronumber" on
|
||||
"public"."jobs" using gin ("ro_number");
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS "public"."idx_jobs_ronumber";
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE INDEX "idx_jobs_clmno" on
|
||||
"public"."jobs" using gin ("clm_no");
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS "public"."idx_jobs_clmno";
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE INDEX "idx_jobs_vmodeldesc" on
|
||||
"public"."jobs" using gin ("v_model_desc");
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS "public"."idx_jobs_vmodeldesc";
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE INDEX "idx_jobs_vmakedesc" on
|
||||
"public"."jobs" using gin ("v_make_desc");
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS "public"."idx_jobs_vmakedesc";
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE INDEX "idx_jobs_plateno" on
|
||||
"public"."jobs" using gin ("plate_no");
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS "public"."idx_jobs_plateno";
|
||||
11
hasura/migrations/1726773072213_run_sql_migration/down.sql
Normal file
11
hasura/migrations/1726773072213_run_sql_migration/down.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- CREATE
|
||||
-- OR REPLACE VIEW "public"."jobs_inproduction" AS
|
||||
-- SELECT
|
||||
-- j.id,
|
||||
-- j.updated_at
|
||||
-- FROM
|
||||
-- jobs j
|
||||
-- WHERE
|
||||
-- j.inproduction=true;
|
||||
9
hasura/migrations/1726773072213_run_sql_migration/up.sql
Normal file
9
hasura/migrations/1726773072213_run_sql_migration/up.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
CREATE
|
||||
OR REPLACE VIEW "public"."jobs_inproduction" AS
|
||||
SELECT
|
||||
j.id,
|
||||
j.updated_at
|
||||
FROM
|
||||
jobs j
|
||||
WHERE
|
||||
j.inproduction=true;
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- CREATE OR REPLACE VIEW "public"."jobs_inproduction" AS
|
||||
-- SELECT j.id,
|
||||
-- j.updated_at,
|
||||
-- j.shopid
|
||||
-- FROM jobs j
|
||||
-- WHERE (j.inproduction = true);
|
||||
6
hasura/migrations/1726773316245_run_sql_migration/up.sql
Normal file
6
hasura/migrations/1726773316245_run_sql_migration/up.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
CREATE OR REPLACE VIEW "public"."jobs_inproduction" AS
|
||||
SELECT j.id,
|
||||
j.updated_at,
|
||||
j.shopid
|
||||
FROM jobs j
|
||||
WHERE (j.inproduction = true);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user