Compare commits

..

17 Commits

Author SHA1 Message Date
Dave Richer
02a1d39ca4 - Bump packages (minus major)
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-04 12:50:50 -04:00
Dave Richer
d50efe9890 Merge branch 'master-AIO' into feature/IO-2825-Node-20-Update 2024-09-04 12:45:28 -04:00
Dave Richer
789af9fb30 - Merge master-aio, bump packages.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-19 12:35:07 -04:00
Dave Richer
18da11f593 - Merge master-aio, bump packages.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-19 12:27:30 -04:00
Dave Richer
0ca1937140 - Bump deps
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-07 11:26:46 -04:00
Dave Richer
7898d5a595 - merge master fix conflicts
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-07 11:17:43 -04:00
Dave Richer
e8aa4f2dac - Merge Master, update packages.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-22 14:53:56 -04:00
Dave Richer
271d15d969 Merge remote-tracking branch 'origin/master-AIO' into feature/IO-2825-Node-20-Update 2024-07-22 14:49:13 -04:00
Dave Richer
136d52ec0b - Merge Master, update packages.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-22 14:47:44 -04:00
Dave Richer
90b9ab906a - Merge master-aio
- Package updates

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-16 09:09:13 -04:00
Dave Richer
02fd5e1701 - Update packages (continued)
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-09 13:07:18 -04:00
Dave Richer
2dcd05c593 - Update packages / Merge master
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-09 13:01:16 -04:00
Dave Richer
0e9414cbc8 Merge remote-tracking branch 'origin/master-AIO' into feature/IO-2825-Node-20-Update 2024-07-09 12:59:56 -04:00
Dave Richer
ddc235637a - Refresh packages
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-06-27 15:13:22 -04:00
Dave Richer
60549597cb - Merge Master-AIO
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-06-27 15:09:29 -04:00
Dave Richer
9fe94481e7 Merged in master-AIO (pull request #1503)
Master AIO
2024-06-26 00:54:55 +00:00
Dave Richer
b78d0253de - Node 20 / Package Updates (including new packages for updated production board that are not yet used) / ESLint error fix / react-grid-layout added to optimized deps.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-06-20 14:57:45 -04:00
189 changed files with 6508 additions and 12771 deletions

View File

@@ -5,7 +5,6 @@ orbs:
aws-s3: circleci/aws-s3@4.0.0
aws-cli: circleci/aws-cli@4.0
eb: circleci/aws-elastic-beanstalk@2.0.1
jira: circleci/jira@2.1.0
jobs:
imex-api-deploy:
docker:
@@ -19,12 +18,6 @@ jobs:
eb status --verbose
eb deploy
eb status
- jira/notify:
environment: Production (ImEX) - API
environment_type: production
job_type: deployment
pipeline_id: << pipeline.id >>
pipeline_number: << pipeline.number >>
imex-hasura-migrate:
docker:
@@ -40,16 +33,11 @@ jobs:
- run:
name: Execute migration
command: |
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
npm install hasura-cli -g
hasura migrate apply --endpoint https://db.imex.online/ --admin-secret << parameters.secret >>
hasura metadata apply --endpoint https://db.imex.online/ --admin-secret << parameters.secret >>
hasura metadata reload --endpoint https://db.imex.online/ --admin-secret << parameters.secret >>
- jira/notify:
environment: Production (ImEX) - Hasura
environment_type: production
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
imex-app-build:
docker:
- image: cimg/node:18.18.2
@@ -74,7 +62,6 @@ jobs:
to: "s3://imex-online-production/"
arguments: "--exclude '*.map'"
imex-app-beta-build:
docker:
- image: cimg/node:18.18.2
@@ -99,12 +86,6 @@ jobs:
from: dist
to: "s3://imex-online-beta/"
arguments: "--exclude '*.map'"
- jira/notify:
environment: Production (ImEX) - Front End
environment_type: production
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
rome-api-deploy:
docker:
@@ -118,12 +99,7 @@ jobs:
eb status --verbose
eb deploy
eb status
- jira/notify:
environment: Production (Rome) - API
environment_type: production
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
rome-hasura-migrate:
docker:
- image: cimg/node:18.18.2
@@ -138,16 +114,11 @@ jobs:
- run:
name: Execute migration
command: |
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
npm install hasura-cli -g
hasura migrate apply --endpoint https://db.romeonline.io/ --admin-secret << parameters.secret >>
hasura metadata apply --endpoint https://db.romeonline.io/ --admin-secret << parameters.secret >>
hasura metadata reload --endpoint https://db.romeonline.io/ --admin-secret << parameters.secret >>
- jira/notify:
environment: Production (Rome) - Hasura
environment_type: production
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
rome-app-build:
docker:
- image: cimg/node:18.18.2
@@ -172,12 +143,6 @@ jobs:
from: dist
to: "s3://rome-online-production/"
arguments: "--exclude '*.map'"
- jira/notify:
environment: Production (Rome) - Front End
environment_type: production
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
promanager-app-build:
docker:
@@ -203,12 +168,6 @@ jobs:
from: dist
to: "s3://promanager-production/"
arguments: "--exclude '*.map'"
- jira/notify:
environment: Production (ProManager) - Front End
environment_type: production
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
test-rome-hasura-migrate:
docker:
@@ -224,18 +183,10 @@ jobs:
- run:
name: Execute migration
command: |
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
npm install hasura-cli -g
hasura migrate apply --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >>
sleep 5
hasura metadata apply --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >>
sleep 10
hasura metadata reload --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >>
- jira/notify:
environment: Test (Rome) - Hasura
environment_type: testing
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
test-rome-app-build:
docker:
@@ -261,12 +212,6 @@ jobs:
from: dist
to: "s3://rome-online-test/"
arguments: "--exclude '*.map'"
- jira/notify:
environment: Test (Rome) - Front End
environment_type: testing
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
test-promanager-app-build:
docker:
@@ -292,12 +237,6 @@ jobs:
from: dist
to: "s3://promanager-testing/"
arguments: "--exclude '*.map'"
- jira/notify:
environment: Test (ProManager) - Front End
environment_type: testing
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
test-hasura-migrate:
docker:
@@ -313,18 +252,10 @@ jobs:
- run:
name: Execute migration
command: |
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
npm install hasura-cli -g
hasura migrate apply --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
sleep 15
hasura metadata apply --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
sleep 30
hasura metadata reload --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
- jira/notify:
environment: Test (ImEX) - Hasura
environment_type: testing
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
imex-test-app-build:
docker:
@@ -371,12 +302,7 @@ jobs:
from: dist
to: "s3://imex-online-test-beta/"
arguments: "--exclude '*.map'"
- jira/notify:
environment: Test (ImEX) - Front End
environment_type: testing
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
admin-app-build:
docker:
@@ -427,7 +353,7 @@ workflows:
secret: ${HASURA_PROD_SECRET}
filters:
branches:
only: master-AIO
only: master
- rome-api-deploy:
filters:
branches:
@@ -437,7 +363,7 @@ workflows:
branches:
only: master-AIO
- rome-hasura-migrate:
secret: ${HASURA_ROME_PROD_SECRET}
secret: ${HASURA_PROD_SECRET}
filters:
branches:
only: master-AIO

View File

@@ -1,24 +0,0 @@
# 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

View File

15
.vscode/launch.json vendored
View File

@@ -14,21 +14,6 @@
"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>/**"
]
}
]
}

View File

@@ -1,47 +0,0 @@
# 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"]

View File

@@ -1,64 +0,0 @@
# 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

View File

@@ -1,59 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IMEX IO Extractor</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
}
textarea {
width: 100%;
height: 200px;
}
.output-box {
margin-top: 20px;
padding: 10px;
border: 1px solid #ccc;
background-color: #f9f9f9;
min-height: 40px;
}
.copy-button {
margin-top: 10px;
}
</style>
</head>
<body>
<h1>IMEX IO Extractor</h1>
<textarea id="inputText" placeholder="Paste your text here..."></textarea>
<br>
<button onclick="extractIO()">Extract</button>
<div class="output-box" id="outputBox" contenteditable="true"></div>
<button class="copy-button" onclick="copyToClipboard()">Copy to Clipboard</button>
<script>
function extractIO() {
const inputText = document.getElementById('inputText').value;
const ioNumbers = [...new Set(inputText.match(/IO-\d{4}/g))] // Extract unique IO-#### matches
.map(io => ({ io, num: parseInt(io.split('-')[1]) })) // Extract number part for sorting
.sort((a, b) => a.num - b.num) // Sort by the number
.map(item => item.io); // Extract sorted IO-####
document.getElementById('outputBox').innerText = ioNumbers.join(', '); // Display horizontally
}
function copyToClipboard() {
const outputBox = document.getElementById('outputBox');
const range = document.createRange();
range.selectNodeContents(outputBox);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
document.execCommand('copy');
}
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +0,0 @@
-----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-----

View File

@@ -1,27 +0,0 @@
-----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-----

View File

@@ -1 +0,0 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC82Xl+5WYs2/vYFk3GdOpfkId/leLxTGNhpjEchNILHtuWeGlVgt2pe9w5ZfNux+NyH916fvdCJbifYVjPvmgYyFQaU5jN1YoD0j1KPT1DLVHTpGHAOJkZ3mpoyzAR5emQ2aLa7hB3aQl4N9RBSKCMLT1XzU7++ueIa1AAlJx+Ib+K12b5R+fT1ngxiTQdaOqIOiQ0SySgQ+yJtgitv5Dq9224+Rk77BT+yZeE+2DDlJPqhB2ab+BU2xGDlc3PGuKt77PUZZq2edUveE+ed+md/xbnz13KazsYYnCE96tIZM3VufRPj70Lr0J3ssEm3GK4V1GZ2UYsss9FHjxG6vvX dave@DaveRicher-IMEX

View File

@@ -1,28 +0,0 @@
-----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-----

View File

@@ -1,5 +1,5 @@
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.imex.online/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.imex.online/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
VITE_APP_GA_CODE=231099835
VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
@@ -8,7 +8,7 @@ VITE_APP_CLOUDINARY_API_KEY=957865933348715
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
VITE_APP_AXIOS_BASE_API_URL=/api/
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_INSTANCE=IMEX

View File

@@ -1,5 +1,5 @@
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.imex.online/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.imex.online/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
VITE_APP_GA_CODE=231099835
VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
@@ -8,7 +8,7 @@ VITE_APP_CLOUDINARY_API_KEY=957865933348715
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
VITE_APP_AXIOS_BASE_API_URL=/api/
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_INSTANCE=PROMANAGER

View File

@@ -1,15 +1,14 @@
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.imex.online/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.imex.online/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
VITE_APP_GA_CODE=231099835
# VITE_APP_FIREBASE_CONFIG={ "apiKey": "AIzaSyAuLQR9SV5LsVxjU8wh9hvFLdhcAHU6cxE", "authDomain": "rome-prod-1.firebaseapp.com", "projectId": "rome-prod-1", "storageBucket": "rome-prod-1.appspot.com", "messagingSenderId": "147786367145", "appId": "1:147786367145:web:9d4cba68071c3f29a8a9b8", "measurementId": "G-G8Z9DRHTZS"}
VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
VITE_APP_FIREBASE_CONFIG={ "apiKey": "AIzaSyAuLQR9SV5LsVxjU8wh9hvFLdhcAHU6cxE", "authDomain": "rome-prod-1.firebaseapp.com", "projectId": "rome-prod-1", "storageBucket": "rome-prod-1.appspot.com", "messagingSenderId": "147786367145", "appId": "1:147786367145:web:9d4cba68071c3f29a8a9b8", "measurementId": "G-G8Z9DRHTZS"}
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
VITE_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test
VITE_APP_CLOUDINARY_API_KEY=957865933348715
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BP1B7ZTYpn-KMt6nOxlld6aS8Skt3Q7ZLEqP0hAvGHxG4UojPYiXZ6kPlzZkUC5jH-EcWXomTLtmadAIxurfcHo'
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
VITE_APP_AXIOS_BASE_API_URL=/api/
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_COUNTRY=USA

1
client/.gitignore vendored
View File

@@ -1,4 +1,3 @@
# Sentry Config File
.sentryclirc
/dev-dist

View File

@@ -12,6 +12,6 @@ module.exports = defineConfig({
setupNodeEvents(on, config) {
return require("./cypress/plugins/index.js")(on, config);
},
baseUrl: "https://localhost:3000"
baseUrl: "http://localhost:3000"
}
});

View File

@@ -2,9 +2,6 @@
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
<link rel="icon" href="/favicon.png"/>
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
@@ -17,7 +14,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="/logo192.png"/>
<link rel="apple-touch-icon" href="public/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
@@ -49,23 +46,77 @@
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
<meta name="description" content="Rome Online"/>
<title>Rome Online</title>
<script type="text/javascript" id="zsiqchat">
var $zoho = $zoho || {};
$zoho.salesiq = $zoho.salesiq || {
widgetcode: "siq01bb8ac617280bdacddfeb528f07734dadc64ef3f05efef9f769c1ec171af666",
values: {},
ready: function () {
}
};
var d = document;
s = d.createElement("script");
s.type = "text/javascript";
s.id = "zsiqscript";
s.defer = true;
s.src = "https://salesiq.zohopublic.com/widget";
t = d.getElementsByTagName("script")[0];
t.parentNode.insertBefore(s, t);
</script>
<!--Use the below code snippet to provide real time updates to the live chat plugin without the need of copying and paste each time to your website when changes are made via PBX-->
<call-us-selector phonesystem-url=https://rometech.east.3cx.us:5001
party="LiveChat528346"></call-us-selector>
<!--Incase you don't want real time updates to the live chat plugin when options are changed, use the below code snippet. Please note that each time you change the settings you will need to copy and paste the snippet code to your website-->
<!--<call-us
phonesystem-url=https://rometech.east.3cx.us:5001
style="position:fixed;font-size:16px;line-height:17px;z-index: 99999;right: 20px; bottom: 20px;"
id="wp-live-chat-by-3CX"
minimized="true"
animation-style="noanimation"
party="LiveChat528346"
minimized-style="bubbleright"
allow-call="true"
allow-video="false"
allow-soundnotifications="true"
enable-mute="true"
enable-onmobile="true"
offline-enabled="true"
enable="true"
ignore-queueownership="false"
authentication="both"
show-operator-actual-name="true"
aknowledge-received="true"
gdpr-enabled="false"
message-userinfo-format="name"
message-dateformat="both"
lang="browser"
button-icon-type="default"
greeting-visibility="none"
greeting-offline-visibility="none"
chat-delay="2000"
enable-direct-call="true"
enable-ga="false"
></call-us>-->
<script defer src=https://downloads-global.3cx.com/downloads/livechatandtalk/v1/callus.js
id="tcx-callus-js" charset="utf-8"></script>
<% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %>
<title>ProManager</title>

1692
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,17 +9,17 @@
"proxy": "http://localhost:4000",
"dependencies": {
"@ant-design/pro-layout": "^7.19.12",
"@apollo/client": "^3.11.8",
"@emotion/is-prop-valid": "^1.3.1",
"@fingerprintjs/fingerprintjs": "^4.5.0",
"@apollo/client": "^3.11.6",
"@emotion/is-prop-valid": "^1.3.0",
"@fingerprintjs/fingerprintjs": "^4.4.3",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.2.7",
"@sentry/cli": "^2.36.2",
"@sentry/cli": "^2.34.1",
"@sentry/react": "^7.114.0",
"@splitsoftware/splitio-react": "^1.13.0",
"@splitsoftware/splitio-react": "^1.12.1",
"@tanem/react-nprogress": "^5.0.51",
"@vitejs/plugin-react": "^4.3.1",
"antd": "^5.20.1",
"antd": "^5.20.5",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0",
"autosize": "^6.0.1",
@@ -32,14 +32,14 @@
"dotenv": "^16.4.5",
"env-cmd": "^10.1.0",
"exifr": "^7.1.3",
"firebase": "^10.13.2",
"firebase": "^10.13.1",
"graphql": "^16.9.0",
"i18next": "^23.15.1",
"i18next": "^23.14.0",
"i18next-browser-languagedetector": "^8.0.0",
"immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.11.9",
"libphonenumber-js": "^1.11.7",
"logrocket": "^8.1.2",
"markerjs2": "^2.32.2",
"markerjs2": "^2.32.1",
"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.14.1",
"react-big-calendar": "^1.13.4",
"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.2",
"react-number-format": "^5.4.1",
"react-popopo": "^2.1.9",
"react-product-fruits": "^2.2.61",
"react-redux": "^9.1.2",
"react-resizable": "^3.0.5",
"react-router-dom": "^6.26.2",
"react-router-dom": "^6.26.1",
"react-sticky": "^6.0.3",
"react-virtualized": "^9.22.5",
"react-virtuoso": "^4.10.4",
"react-virtuoso": "^4.10.2",
"recharts": "^2.12.7",
"redux": "^5.0.1",
"redux-actions": "^3.0.3",
@@ -74,26 +74,22 @@
"redux-saga": "^1.3.0",
"redux-state-sync": "^3.1.4",
"reselect": "^5.1.1",
"sass": "^1.79.3",
"socket.io-client": "^4.8.0",
"sass": "^1.78.0",
"socket.io-client": "^4.7.5",
"styled-components": "^6.1.13",
"subscriptions-transport-ws": "^0.11.0",
"use-memo-one": "^1.1.3",
"userpilot": "^1.3.6",
"userpilot": "^1.3.5",
"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",
@@ -134,30 +130,29 @@
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-react": "^7.24.7",
"@dotenvx/dotenvx": "^1.14.1",
"@dotenvx/dotenvx": "^1.13.3",
"@emotion/babel-plugin": "^11.12.0",
"@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.14.2",
"cypress": "^13.14.1",
"eslint": "^8.57.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-cypress": "^2.15.1",
"memfs": "^4.12.0",
"memfs": "^4.11.1",
"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.7",
"vite": "^5.4.3",
"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.5",
"vite-plugin-style-import": "^2.0.0",
"workbox-window": "^7.1.0"
"vite-plugin-pwa": "^0.20.4",
"vite-plugin-style-import": "^2.0.0"
}
}

View File

@@ -18,10 +18,10 @@ import { checkUserSession } from "../redux/user/user.actions";
import { selectBodyshop, selectCurrentEula, selectCurrentUser } from "../redux/user/user.selectors";
import PrivateRoute from "../components/PrivateRoute";
import "./App.styles.scss";
import handleBeta from "../utils/handleBeta";
import Eula from "../components/eula/eula.component";
import InstanceRenderMgr from "../utils/instanceRenderMgr";
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
import { SocketProvider } from "../contexts/SocketIO/socketContext.jsx";
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
@@ -108,6 +108,8 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
return <LoadingSpinner message={t("general.labels.loggingin")} />;
}
handleBeta();
if (!online) {
return (
<Result
@@ -202,9 +204,7 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
path="/manage/*"
element={
<ErrorBoundary>
<SocketProvider bodyshop={bodyshop}>
<PrivateRoute isAuthorized={currentUser.authorized} />
</SocketProvider>
<PrivateRoute isAuthorized={currentUser.authorized} />
</ErrorBoundary>
}
>
@@ -214,9 +214,7 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
path="/tech/*"
element={
<ErrorBoundary>
<SocketProvider bodyshop={bodyshop}>
<PrivateRoute isAuthorized={currentUser.authorized} />
</SocketProvider>
<PrivateRoute isAuthorized={currentUser.authorized} />
</ErrorBoundary>
}
>

View File

@@ -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 { exportPageLimit } from "../../utils/config";
import { pageLimit } 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: exportPageLimit }}
pagination={{ position: "top", pageSize: pageLimit }}
columns={columns}
rowKey="id"
onChange={handleTableChange}

View File

@@ -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 { exportPageLimit } from "../../utils/config";
import { pageLimit } 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: exportPageLimit }}
pagination={{ position: "top", pageSize: pageLimit }}
columns={columns}
rowKey="id"
onChange={handleTableChange}

View File

@@ -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", pageSize: exportPageLimit }}
pagination={{ position: "top" }}
columns={columns}
rowKey="id"
onChange={handleTableChange}

View File

@@ -98,7 +98,7 @@ export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail
});
billlines.forEach((billline) => {
const { deductedfromlbr, inventories, jobline, original_actual_price, create_ppc, ...il } = billline;
const { deductedfromlbr, inventories, jobline, ...il } = billline;
delete il.__typename;
if (il.id) {

View File

@@ -1,6 +1,6 @@
import { DeleteFilled, CopyFilled } from "@ant-design/icons";
import { DeleteFilled } from "@ant-design/icons";
import { useLazyQuery, useMutation } from "@apollo/client";
import { Button, Card, Col, Form, Input, Row, Space, Spin, Statistic, message, notification } from "antd";
import { Button, Card, Col, Form, Input, Row, Space, Spin, Statistic, notification } from "antd";
import axios from "axios";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -14,12 +14,10 @@ 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,
currentUser: getCurrentUser
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
@@ -27,17 +25,11 @@ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment"))
});
const CardPaymentModalComponent = ({
bodyshop,
currentUser,
cardPaymentModal,
toggleModalVisible,
insertAuditTrail
}) => {
const CardPaymentModalComponent = ({ bodyshop, 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);
@@ -45,7 +37,7 @@ const CardPaymentModalComponent = ({
const [, { data, refetch, queryLoading }] = useLazyQuery(QUERY_RO_AND_OWNER_BY_JOB_PKS, {
variables: { jobids: [context.jobid] },
skip: !context?.jobid
skip: true
});
//Initialize the intellipay window.
@@ -59,7 +51,8 @@ const CardPaymentModalComponent = ({
//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);
@@ -93,6 +86,7 @@ const CardPaymentModalComponent = ({
});
};
const handleIntelliPayCharge = async () => {
setLoading(true);
//Validate
@@ -107,7 +101,7 @@ const CardPaymentModalComponent = ({
const response = await axios.post("/intellipay/lightbox_credentials", {
bodyshop,
refresh: !!window.intellipay,
paymentSplitMeta: form.getFieldsValue()
paymentSplitMeta: form.getFieldsValue(),
});
if (window.intellipay) {
@@ -132,42 +126,6 @@ const CardPaymentModalComponent = ({
}
};
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}>
@@ -244,14 +202,16 @@ const CardPaymentModalComponent = ({
<Form.Item
shouldUpdate={(prevValues, curValues) =>
prevValues.payments?.map((p) => p?.jobid + p?.amount).join() !==
curValues.payments?.map((p) => p?.jobid + p?.amount).join()
prevValues.payments?.map((p) => p?.jobid).join() !== curValues.payments?.map((p) => p?.jobid).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 (
@@ -286,6 +246,7 @@ const CardPaymentModalComponent = ({
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} />
@@ -312,36 +273,11 @@ const CardPaymentModalComponent = ({
>
{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>
);

View File

@@ -3,15 +3,13 @@ import axios from "axios";
import _ from "lodash";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom";
import { Link } 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);
@@ -179,18 +177,7 @@ export default function GlobalSearchOs() {
};
return (
<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([])}
>
<AutoComplete options={data} onSearch={handleSearch} defaultActiveFirstOption onClear={() => setData([])}>
<Input.Search
size="large"
placeholder={t("general.labels.globalsearch")}

View File

@@ -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, useNavigate } from "react-router-dom";
import { Link } from "react-router-dom";
import { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import AlertComponent from "../alert/alert.component";
@@ -13,7 +13,6 @@ 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);
@@ -21,6 +20,7 @@ export default function GlobalSearch() {
const debouncedExecuteSearch = _.debounce(executeSearch, 750);
const handleSearch = (value) => {
console.log("Handle Search");
debouncedExecuteSearch({ variables: { search: value } });
};
@@ -156,17 +156,7 @@ export default function GlobalSearch() {
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<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);
}}
>
<AutoComplete options={options} onSearch={handleSearch} defaultActiveFirstOption>
<Input.Search
size="large"
placeholder={t("general.labels.globalsearch")}

View File

@@ -13,6 +13,7 @@ import Icon, {
FileFilled,
HomeFilled,
ImportOutlined,
InfoCircleOutlined,
LineChartOutlined,
PaperClipOutlined,
PhoneOutlined,
@@ -26,8 +27,8 @@ import Icon, {
UserOutlined
} from "@ant-design/icons";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Layout, Menu } from "antd";
import React from "react";
import { Layout, Menu, Switch, Tooltip } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { BsKanban } from "react-icons/bs";
import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar, FaTasks } from "react-icons/fa";
@@ -42,6 +43,7 @@ import { selectRecentItems, selectSelectedHeader } from "../../redux/application
import { setModalContext } from "../../redux/modals/modals.actions";
import { signOutStart } from "../../redux/user/user.actions";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import { checkBeta, handleBeta, setBeta } from "../../utils/handleBeta";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
@@ -113,21 +115,19 @@ function Header({
names: ["ImEXPay", "DmsAp", "Simple_Inventory"],
splitKey: bodyshop && bodyshop.imexshopid
});
const [betaSwitch, setBetaSwitch] = useState(false);
const { t } = useTranslation();
const deleteBetaCookie = () => {
const cookieExists = document.cookie.split("; ").some((row) => row.startsWith(`betaSwitchImex=`));
if (cookieExists) {
const domain = window.location.hostname.split(".").slice(-2).join(".");
document.cookie = `betaSwitchImex=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${domain}`;
console.log(`betaSwitchImex cookie deleted`);
} else {
console.log(`betaSwitchImex cookie does not exist`);
}
};
useEffect(() => {
const isBeta = checkBeta();
setBetaSwitch(isBeta);
}, []);
deleteBetaCookie();
const betaSwitchChange = (checked) => {
setBeta(checked);
setBetaSwitch(checked);
handleBeta();
};
const accountingChildren = [];
@@ -695,6 +695,31 @@ function Header({
}
];
InstanceRenderManager({
executeFunction: true,
args: [],
imex: () => {
menuItems.push({
key: "beta-switch",
id: "header-beta-switch",
style: { marginLeft: "auto" },
label: (
<Tooltip
title={`A more modern ${InstanceRenderManager({
imex: t("titles.imexonline"),
rome: t("titles.romeonline"),
promanager: t("titles.promanager")
})} is ready for you to try! You can switch back at any time.`}
>
<InfoCircleOutlined />
<span style={{ marginRight: 8 }}>Try the new app</span>
<Switch checked={betaSwitch} onChange={betaSwitchChange} />
</Tooltip>
)
});
}
});
return (
<Layout.Header>
<Menu

View File

@@ -1,8 +1,5 @@
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";
@@ -10,10 +7,13 @@ 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,15 +82,13 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo
variables: {
lineId: jobLineEditModal.context.id,
line: {
...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)
...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)
}
},
refetchQueries: ["GET_LINE_TICKET_BY_PK"]

View File

@@ -141,14 +141,10 @@ export function JobTotalsTableTotals({ bodyshop, job }) {
key: t("jobs.fields.ded_amt"),
total: job.job_totals.totals.custPayable.deductible
},
...(InstanceRenderManager({
imex: [{
key: t("jobs.fields.federal_tax_payable"),
total: job.job_totals.totals.custPayable.federal_tax
}],
rome: [],
promanager: "USE_ROME"
})),
// {
// key: t("jobs.fields.federal_tax_payable"),
// total: job.job_totals.totals.custPayable.federal_tax,
// },
{
key: t("jobs.fields.other_amount_payable"),
total: job.job_totals.totals.custPayable.other_customer_amount

View File

@@ -219,7 +219,7 @@ export function JobsExportAllButton({
};
return (
<Button onClick={handleQbxml} loading={loading} disabled={disabled || jobIds?.length > 10}>
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
{t("jobs.actions.exportselected")}
</Button>
);

View File

@@ -250,8 +250,8 @@ export function JobsList({ bodyshop }) {
},
{
title: t("jobs.labels.estimator"),
dataIndex: "estimator",
key: "estimator",
dataIndex: "jobs.labels.estimator",
key: "jobs.labels.estimator",
ellipsis: true,
responsive: ["xl"],
sorter: (a, b) =>

View File

@@ -242,8 +242,7 @@ export function PartsOrderListTableComponent({
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
render: (text, record) => recordActions(record, true),
id: "parts-order-list-table-actions"
render: (text, record) => recordActions(record, true)
}
];

View File

@@ -27,10 +27,6 @@ export default function PartsOrderModalPriceChange({ form, field }) {
key: "25",
label: t("parts_orders.labels.discount", { percent: "25%" })
},
{
key: "40",
label: t("parts_orders.labels.discount", { percent: "40%" })
},
{
key: "custom",
label: (

View File

@@ -200,7 +200,7 @@ export function PayableExportAll({
);
return (
<Button onClick={handleQbxml} loading={loading} disabled={disabled || billids?.length > 10}>
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
{t("jobs.actions.exportselected")}
</Button>
);

View File

@@ -180,7 +180,7 @@ export function PaymentsExportAllButton({
};
return (
<Button onClick={handleQbxml} loading={loading} disabled={disabled || paymentIds?.length > 10}>
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
{t("jobs.actions.exportselected")}
</Button>
);

View File

@@ -8,12 +8,11 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
@@ -21,7 +20,7 @@ const mapDispatchToProps = (dispatch) => ({
});
export default connect(mapStateToProps, mapDispatchToProps)(PaymentsGenerateLink);
export function PaymentsGenerateLink({ bodyshop, currentUser, callback, job, openChatByPhone, setMessage }) {
export function PaymentsGenerateLink({ bodyshop, callback, job, openChatByPhone, setMessage }) {
const { t } = useTranslation();
const [form] = Form.useForm();
@@ -31,35 +30,29 @@ export function PaymentsGenerateLink({ bodyshop, currentUser, callback, job, ope
const handleFinish = async ({ amount }) => {
setLoading(true);
let p;
try {
p = parsePhoneNumber(job.ownr_ph1 || "", "CA");
} catch (error) {
console.log("Unable to parse phone number");
}
const p = parsePhoneNumber(job.ownr_ph1, "CA");
setLoading(true);
const response = await axios.post("/intellipay/generate_payment_url", {
bodyshop,
amount: amount,
account: job.ro_number,
comment: btoa(JSON.stringify({ payments: [{ jobid: job.id, amount }], userEmail: currentUser.email }))
invoice: job.id
});
setLoading(false);
setPaymentLink(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
})
);
}
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();

View File

@@ -6,11 +6,11 @@ import {
PauseCircleOutlined
} from "@ant-design/icons";
import { Card, Col, Row, Space, Tooltip } from "antd";
import Dinero from "dinero.js";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import Dinero from "dinero.js";
import ProductionAlert from "../production-list-columns/production-list-columns.alert.component";
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
@@ -18,8 +18,8 @@ import ProductionSubletsManageComponent from "../production-sublets-manage/produ
import dayjs from "../../utils/day";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
const cardColor = (ssbuckets, totalHrs) => {
const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > totalHrs));
@@ -213,13 +213,21 @@ const EstimatorToolTip = ({ metadata, cardSettings }) => {
};
const SubtotalTooltip = ({ metadata, cardSettings, t }) => {
const dineroAmount = Dinero(metadata?.job_totals?.totals?.subtotal ?? Dinero()).toFormat();
const amount = metadata?.job_totals?.totals?.subtotal?.amount;
const dineroAmount = amount ? Dinero({ amount: parseInt(amount * 100) }).toFormat("0,0.00") : null;
return (
cardSettings?.subtotal && (
<Col span={cardSettings.compact ? 24 : 12}>
<EllipsesToolTip title={`${dineroAmount}`} kiosk={cardSettings.kiosk}>
{dineroAmount}
<EllipsesToolTip
title={!!amount ? `${t("production.statistics.currency_symbol")}${dineroAmount}` : null}
kiosk={cardSettings.kiosk}
>
{!!amount ? (
<span>{`${t("production.statistics.currency_symbol")}${dineroAmount}`}</span>
) : (
<span>&nbsp;</span>
)}
</EllipsesToolTip>
</Col>
)

View File

@@ -185,7 +185,7 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
const cardSettings = useMemo(() => {
const kanbanSettings = associationSettings?.kanban_settings;
return mergeWithDefaults(kanbanSettings);
}, [associationSettings?.kanban_settings]);
}, [associationSettings]);
const handleSettingsChange = () => {
setFilter(defaultFilters);

View File

@@ -1,40 +1,18 @@
import React, { useContext, useEffect, useMemo, useRef } from "react";
import { useApolloClient, useQuery, useSubscription } from "@apollo/client";
import React, { useEffect, useMemo } from "react";
import { useQuery, useSubscription } from "@apollo/client";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
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_JOBS_IN_PRODUCTION, SUBSCRIPTION_JOBS_IN_PRODUCTION } 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, 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
});
function ProductionBoardKanbanContainer({ bodyshop, currentUser }) {
const combinedStatuses = useMemo(
() => [
...bodyshop.md_ro_statuses.production_statuses,
@@ -50,127 +28,26 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionTyp
onError: (error) => console.error(`Error fetching 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 { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION, {
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 (subscriptionEnabled) {
if (!updatedJobs) {
return;
}
if (!fired.current) {
fired.current = true;
return;
}
if (updatedJobs && data) {
refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`));
}
}, [updatedJobs, refetch, subscriptionEnabled]);
// Socket.IO implementation for users with Split treatment "off"
useEffect(() => {
if (subscriptionEnabled || !socket || !bodyshop || !bodyshop.id) {
return;
}
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]);
}, [updatedJobs, data, refetch]);
const filteredAssociationSettings = useMemo(() => {
return associationSettings?.associations[0] || null;
}, [associationSettings?.associations]);
}, [associationSettings]);
return (
<ProductionBoardKanbanComponent

View File

@@ -3,7 +3,6 @@ import { Card, Statistic } from "antd";
import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";
import { defaultKanbanSettings, statisticsItems } from "./settings/defaultKanbanSettings.js";
import Dinero from "dinero.js";
export const StatisticType = {
HOURS: "hours",
@@ -33,21 +32,7 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
};
const calculateTotalAmount = (items, key) => {
return items.reduce((acc, item) => acc.add(Dinero(item[key]?.totals?.subtotal ?? Dinero())), Dinero({ amount: 0 }));
};
const calculateReducerTotalAmount = (lanes, key) => {
return lanes.reduce(
(acc, lane) => {
return acc.add(
lane.cards.reduce(
(laneAcc, card) => laneAcc.add(Dinero(card.metadata[key]?.totals?.subtotal ?? Dinero())),
Dinero({ amount: 0 })
)
);
},
Dinero({ amount: 0 })
);
return items.reduce((acc, item) => acc + (item[key]?.totals?.subtotal?.amount || 0), 0);
};
const calculateReducerTotal = (lanes, key, subKey) => {
@@ -58,6 +43,14 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
}, 0);
};
const calculateReducerTotalAmount = (lanes, key) => {
return lanes.reduce((acc, lane) => {
return (
acc + lane.cards.reduce((laneAcc, card) => laneAcc + (card.metadata[key]?.totals?.subtotal?.amount || 0), 0)
);
}, 0);
};
const formatValue = (value, type) => {
if (type === StatisticType.JOBS) {
return value.toFixed(0);
@@ -94,15 +87,9 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
const totalAmountInProduction = useMemo(() => {
if (!cardSettings.totalAmountInProduction) return null;
const total = calculateTotalAmount(data, "job_totals");
return total.toFormat("$0,0.00");
return parseFloat(total.toFixed(2));
}, [data, cardSettings.totalAmountInProduction]);
const totalAmountOnBoard = useMemo(() => {
if (!reducerData || !cardSettings.totalAmountOnBoard) return null;
const total = calculateReducerTotalAmount(reducerData.lanes, "job_totals");
return total.toFormat("$0,0.00");
}, [reducerData, cardSettings.totalAmountOnBoard]);
const totalHrsOnBoard = useMemo(() => {
if (!reducerData || !cardSettings.totalHrsOnBoard) return null;
const total =
@@ -131,6 +118,12 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
[reducerData, cardSettings.jobsOnBoard]
);
const totalAmountOnBoard = useMemo(() => {
if (!reducerData || !cardSettings.totalAmountOnBoard) return null;
const total = calculateReducerTotalAmount(reducerData.lanes, "job_totals");
return parseFloat(total.toFixed(2));
}, [reducerData, cardSettings.totalAmountOnBoard]);
const tasksInProduction = useMemo(() => {
if (!data || !cardSettings.tasksInProduction) return null;
return data.reduce((acc, item) => acc + (item.tasks_aggregate?.aggregate?.count || 0), 0);
@@ -198,6 +191,7 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
<Statistic
title={t(`production.statistics.${stat.label}`)}
value={formatValue(stat.value, stat.type)}
prefix={stat.type === StatisticType.AMOUNT ? t("production.statistics.currency_symbol") : undefined}
suffix={
stat.type === StatisticType.HOURS
? t("production.statistics.hours")

View File

@@ -17,6 +17,7 @@
border-radius: 5px 5px 0 0;
}
.production-alert {
background: transparent;
border: none;
@@ -69,8 +70,3 @@
}
}
}
.clone.is-dragging .ant-card {
border: #1890ff 2px solid !important;
border-radius: 12px;
}

View File

@@ -1,66 +1,15 @@
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: 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: 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: 11, name: "tasksInProduction", label: "tasks_in_production" }
];

View File

@@ -25,8 +25,8 @@ function getFurthestAway({ pageBorderBox, draggable, candidates }) {
const axis = candidate.axis;
const target = patch(
candidate.axis.line,
// use the center of the list on the main axis
candidate.page.borderBox.center[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 cross axis
candidate.page.borderBox.center[axis.crossAxisLine]
);

View File

@@ -5,7 +5,6 @@ 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);
@@ -15,7 +14,7 @@ const isElementScrollable = (el) => {
overflowX: style.overflowX,
overflowY: style.overflowY
};
return isEither(overflow, isScroll) || isEither(overflow, isAuto) || isEither(overflow, isOverlay);
return isEither(overflow, isScroll) || isEither(overflow, isAuto);
};
// Special case for a body element

View File

@@ -8,7 +8,7 @@ function getSelector(contextId) {
return `[${attributes.dragHandle.contextId}="${contextId}"]`;
}
export function findClosestDragHandleFromEvent(contextId, event) {
function findClosestDragHandleFromEvent(contextId, event) {
const target = event.target;
if (!isElement(target)) {
warning("event.target must be a Element");

View File

@@ -240,14 +240,11 @@ 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, handle);
startPendingDrag(actions, point);
}
}),
// not including stop or startPendingDrag as it is not defined initially
@@ -291,7 +288,7 @@ export default function useTouchSensor(api) {
}
}, [stop]);
const bindCapturingEvents = useCallback(
function bindCapturingEvents(target) {
function bindCapturingEvents() {
const options = {
capture: true,
passive: false
@@ -310,7 +307,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(target, getHandleBindings(args), options);
const unbindTarget = bindEvents(window, getHandleBindings(args), options);
const unbindWindow = bindEvents(window, getWindowBindings(args), options);
unbindEventsRef.current = function unbindAll() {
unbindTarget();
@@ -333,7 +330,7 @@ export default function useTouchSensor(api) {
[getPhase, setPhase]
);
const startPendingDrag = useCallback(
function startPendingDrag(actions, point, target) {
function startPendingDrag(actions, point) {
invariant(getPhase().type === "IDLE", "Expected to move from IDLE to PENDING drag");
const longPressTimerId = setTimeout(startDragging, timeForLongPress);
setPhase({
@@ -342,7 +339,7 @@ export default function useTouchSensor(api) {
actions,
longPressTimerId
});
bindCapturingEvents(target);
bindCapturingEvents();
},
[bindCapturingEvents, getPhase, setPhase, startDragging]
);

View File

@@ -23,9 +23,7 @@ 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, {
findClosestDragHandleFromEvent
} from "./find-closest-draggable-id-from-event";
import findClosestDraggableIdFromEvent from "./find-closest-draggable-id-from-event";
import findDraggable from "../get-elements/find-draggable";
import bindEvents from "../event-bindings/bind-events";
@@ -341,9 +339,6 @@ 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) => {
@@ -375,18 +370,9 @@ export default function useSensorMarshal({ contextId, store, registry, customSen
findClosestDraggableId,
findOptionsForDraggable,
tryReleaseLock,
isLockClaimed,
findClosestDragHandle
isLockClaimed
}),
[
canGetLock,
tryGetLock,
findClosestDraggableId,
findOptionsForDraggable,
tryReleaseLock,
isLockClaimed,
findClosestDragHandle
]
[canGetLock, tryGetLock, findClosestDraggableId, findOptionsForDraggable, tryReleaseLock, isLockClaimed]
);
// Bad ass

View File

@@ -83,13 +83,7 @@ const getFinalStyles = (contextId) => {
return {
selector: getSelector(attributes.draggable.contextId),
styles: {
dragging: `
${transition}
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
`,
dragging: transition,
dropAnimating: transition,
userCancel: transition
}

View File

@@ -67,9 +67,7 @@ export default function useStyleMarshal(contextId, nonce) {
const remove = (ref) => {
const current = ref.current;
invariant(current, "Cannot unmount ref as it is not set");
if (getHead().contains(current)) {
getHead().removeChild(current);
}
getHead().removeChild(current);
ref.current = null;
};
remove(alwaysRef);

View File

@@ -298,16 +298,6 @@ 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} />
},
{

View File

@@ -21,26 +21,25 @@ export function ProductionListColumnStatus({ record, bodyshop, insertAuditTrail
const [loading, setLoading] = useState(false);
const handleSetStatus = async (e) => {
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
}
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 = {

View File

@@ -457,42 +457,41 @@ 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 &&
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"
}}
{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()}
>
{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>
))}
<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" }} />

View File

@@ -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]);
return bodyshop.production_config.find((p) => p.name === defaultView);
}, [bodyshop.production_config, defaultView]);
useEffect(() => {
const newColumns =

View File

@@ -1,54 +1,24 @@
import { useApolloClient, useQuery, useSubscription } from "@apollo/client";
import React, { useContext, useEffect, useState, useRef } from "react";
import React, { useEffect, useState } 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_VIEW
SUBSCRIPTION_JOBS_IN_PRODUCTION
} 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({ 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
export default function ProductionListTableContainer() {
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(
@@ -58,134 +28,34 @@ export default function ProductionListTableContainer({ bodyshop, subscriptionTyp
);
}, [data]);
// Handle updates from GraphQL subscription
useEffect(() => {
if (subscriptionEnabled) {
if (!updatedJobs || joblist.length === 0) return;
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);
});
}
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;
}
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
if (jobDiff.length > 1) {
getUpdatedJobsData(jobDiff.map((j) => j.id));
} else if (jobDiff.length === 1) {
jobDiff.forEach((job) => {
getUpdatedJobData(job.id);
});
}
const existingJobs = existingJobsCache?.jobs || [];
setJoblist(updatedJobs.jobs);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [updatedJobs]);
// 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) => {
await client.query({
client.query({
query: QUERY_EXACT_JOB_IN_PRODUCTION,
variables: { id: jobId },
fetchPolicy: "network-only"
variables: { id: jobId }
});
};
const getUpdatedJobsData = (jobIds) => {
const getUpdatedJobsData = async (jobIds) => {
client.query({
query: QUERY_EXACT_JOBS_IN_PRODUCTION,
variables: { ids: jobIds }

View File

@@ -34,34 +34,28 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
const [form] = Form.useForm();
const [search, setSearch] = useState("");
const {
treatments: { Enhanced_Payroll, ADPPayroll }
treatments: { Enhanced_Payroll }
} = useSplitTreatments({
attributes: {},
names: ["Enhanced_Payroll", "ADPPayroll"],
names: ["Enhanced_Payroll"],
splitKey: bodyshop.imexshopid
});
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
const Templates = TemplateList("report_center");
const ReportsList = Object.keys(Templates)
.map((key) => Templates[key])
.filter((temp) => {
const enhancedPayrollOn = Enhanced_Payroll.treatment === "on";
const adpPayrollOn = ADPPayroll.treatment === "on";
if (enhancedPayrollOn && adpPayrollOn) {
return temp.enhanced_payroll !== false || temp.adp_payroll !== false;
}
if (enhancedPayrollOn) {
return temp.enhanced_payroll !== false && temp.adp_payroll !== true;
}
if (adpPayrollOn) {
return temp.adp_payroll !== false && temp.enhanced_payroll !== true;
}
return temp.enhanced_payroll !== true && temp.adp_payroll !== true;
});
const ReportsList =
Enhanced_Payroll.treatment === "on"
? Object.keys(Templates)
.map((key) => {
return Templates[key];
})
.filter((temp) => temp.enhanced_payroll === undefined || temp.enhanced_payroll === true)
: Object.keys(Templates)
.map((key) => {
return Templates[key];
})
.filter((temp) => temp.enhanced_payroll === undefined || temp.enhanced_payroll === false);
const { open } = reportCenterModal;
@@ -110,7 +104,7 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
to: values.to,
subject: Templates[values.key]?.subject
},
values.sendbytext === "text" ? values.sendbytext : values.sendbyexcel === "excel" ? "x" : values.sendby === "email" ? "e" : "p",
values.sendbyexcel === "excel" ? "x" : values.sendby === "email" ? "e" : "p",
id
);
setLoading(false);
@@ -297,15 +291,7 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
</Radio.Group>
</Form.Item>
);
if (reporttype === "text")
return (
<Form.Item label={t("general.labels.sendby")} name="sendbytext" initialValue="text">
<Radio.Group>
<Radio value="text">{t("general.labels.text")}</Radio>
</Radio.Group>
</Form.Item>
);
if (reporttype !== "excel" || reporttype !== "text")
if (reporttype !== "excel")
return (
<Form.Item label={t("general.labels.sendby")} name="sendby" initialValue="print">
<Radio.Group>

View File

@@ -1,505 +0,0 @@
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;

View File

@@ -1,7 +1,7 @@
import dayjs from "../../utils/day";
import queryString from "query-string";
import React from "react";
import { Calendar } from "react-big-calendar";
import { Calendar, dayjsLocalizer } from "react-big-calendar";
import { connect } from "react-redux";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect";
@@ -14,13 +14,12 @@ 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 = local(dayjs);
const localizer = dayjsLocalizer(dayjs);
export function ScheduleCalendarWrapperComponent({
bodyshop,

View File

@@ -4,7 +4,7 @@ export const CalculateWorkingDaysThisMonth = () => dayjs().endOf("month").busine
export const CalculateWorkingDaysInPeriod = (start, end) => dayjs(end).businessDiff(dayjs(start));
export const CalculateWorkingDaysAsOfToday = () => dayjs().endOf("day").businessDiff(dayjs().startOf("month"));
export const CalculateWorkingDaysAsOfToday = () => dayjs().businessDaysInMonth().length;
export const CalculateWorkingDaysLastMonth = () =>
dayjs().subtract(1, "month").endOf("month").businessDaysInMonth().length;

View File

@@ -20,7 +20,6 @@ import ShopInfoTaskPresets from "./shop-info.task-presets.component";
import queryString from "query-string";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import ShopInfoRoGuard from "./shop-info.roguard.component";
import ShopInfoIntellipay from "./shop-intellipay-config.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -136,17 +135,6 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
],
rome: "USE_IMEX",
promanager: []
}),
...InstanceRenderManager({
imex: [],
rome: [
{
key: "intellipay",
label: t("bodyshop.labels.intellipay"),
children: <ShopInfoIntellipay form={form} />
}
],
promanager: []
})
];
return (

View File

@@ -7,13 +7,13 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import DatePickerRanges from "../../utils/DatePickerRanges";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import FeatureWrapper, { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import FeatureWrapper, { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
// TODO: Client Update, this might break
const timeZonesList = Intl.supportedValuesOf("timeZone");
const mapStateToProps = createStructuredSelector({
@@ -28,10 +28,10 @@ export function ShopInfoGeneral({ form, bodyshop }) {
const { t } = useTranslation();
const {
treatments: { ClosingPeriod, ADPPayroll }
treatments: { ClosingPeriod }
} = useSplitTreatments({
attributes: {},
names: ["ClosingPeriod", "ADPPayroll"],
names: ["ClosingPeriod"],
splitKey: bodyshop && bodyshop.imexshopid
});
@@ -98,6 +98,7 @@ export function ShopInfoGeneral({ form, bodyshop }) {
<Form.Item label={t("bodyshop.fields.email")} name="email">
<Input />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.phone")}
name="phone"
@@ -355,22 +356,14 @@ export function ShopInfoGeneral({ form, bodyshop }) {
<Select mode="tags" />
</Form.Item>
{ClosingPeriod.treatment === "on" && (
<Form.Item
name={["accountingconfig", "ClosingPeriod"]}
label={t("bodyshop.fields.closingperiod")} //{t("reportcenter.labels.dates")}
>
<DatePicker.RangePicker format="MM/DD/YYYY" presets={DatePickerRanges} />
</Form.Item>
)}
{ADPPayroll.treatment === "on" && (
<Form.Item name={["accountingconfig", "companyCode"]} label={t("bodyshop.fields.companycode")}>
<Input />
</Form.Item>
)}
{ADPPayroll.treatment === "on" && (
<Form.Item name={["accountingconfig", "batchID"]} label={t("bodyshop.fields.batchid")}>
<Input />
</Form.Item>
<>
<Form.Item
name={["accountingconfig", "ClosingPeriod"]}
label={t("bodyshop.fields.closingperiod")} //{t("reportcenter.labels.dates")}
>
<DatePicker.RangePicker format="MM/DD/YYYY" presets={DatePickerRanges} />
</Form.Item>
</>
)}
</LayoutFormRow>
</FeatureWrapper>

View File

@@ -30,226 +30,219 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
return (
<RbacWrapper action="shop:rbac">
<LayoutFormRow>
{...HasFeatureAccess({ featureName: "export", bodyshop })
? [
<Form.Item
label={t("bodyshop.fields.rbac.accounting.exportlog")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "accounting:exportlog"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.accounting.payables")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "accounting:payables"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.accounting.payments")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "accounting:payments"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.accounting.receivables")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "accounting:receivables"]}
>
<InputNumber />
</Form.Item>
]
: []}
{...HasFeatureAccess({ featureName: "bills", bodyshop })
? [
<Form.Item
label={t("bodyshop.fields.rbac.bills.delete")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:delete"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.bills.enter")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:enter"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.bills.list")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:list"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.bills.reexport")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:reexport"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.bills.view")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:view"]}
>
<InputNumber />
</Form.Item>
]
: []}
{...HasFeatureAccess({ featureName: "courtesycars", bodyshop })
? [
<Form.Item
label={t("bodyshop.fields.rbac.contracts.create")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "contracts:create"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.contracts.detail")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "contracts:detail"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.contracts.list")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "contracts:list"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.courtesycar.create")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "courtesycar:create"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.courtesycar.detail")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "courtesycar:detail"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.courtesycar.list")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "courtesycar:list"]}
>
<InputNumber />
</Form.Item>
]
: []}
{...HasFeatureAccess({ featureName: "csi", bodyshop })
? [
<Form.Item
label={t("bodyshop.fields.rbac.csi.export")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "csi:export"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.csi.page")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "csi:page"]}
>
<InputNumber />
</Form.Item>
]
: []}
{...HasFeatureAccess({ featureName: "export", bodyshop }) ? [
<Form.Item
label={t("bodyshop.fields.rbac.accounting.exportlog")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "accounting:exportlog"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.accounting.payables")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "accounting:payables"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.accounting.payments")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "accounting:payments"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.accounting.receivables")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "accounting:receivables"]}
>
<InputNumber />
</Form.Item>
]:[]}
{...HasFeatureAccess({ featureName: "bills", bodyshop }) ? [
<Form.Item
label={t("bodyshop.fields.rbac.bills.delete")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:delete"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.bills.enter")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:enter"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.bills.list")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:list"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.bills.reexport")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:reexport"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.bills.view")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:view"]}
>
<InputNumber />
</Form.Item>
]:[]}
{...HasFeatureAccess({ featureName: "courtesycars", bodyshop }) ? [
<Form.Item
label={t("bodyshop.fields.rbac.contracts.create")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "contracts:create"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.contracts.detail")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "contracts:detail"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.contracts.list")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "contracts:list"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.courtesycar.create")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "courtesycar:create"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.courtesycar.detail")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "courtesycar:detail"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.courtesycar.list")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "courtesycar:list"]}
>
<InputNumber />
</Form.Item>
]:[]}
{...HasFeatureAccess({ featureName: "csi", bodyshop }) ? [
<Form.Item
label={t("bodyshop.fields.rbac.csi.export")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "csi:export"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.csi.page")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "csi:page"]}
>
<InputNumber />
</Form.Item>
]:[]}
<Form.Item
label={t("bodyshop.fields.rbac.employees.page")}
rules={[
@@ -262,18 +255,6 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.employee_teams.page")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "employee_teams:page"]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.jobs.admin")}
rules={[
@@ -454,6 +435,31 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.employees.page")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "employees:page"]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.employee_teams.page")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "employee_teams:page"]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.payments.enter")}
rules={[
@@ -516,6 +522,7 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
<InputNumber />
</Form.Item>
)}
<Form.Item
label={t("bodyshop.fields.rbac.production.list")}
rules={[
@@ -554,118 +561,128 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
<InputNumber />
</Form.Item>
)}
{...HasFeatureAccess({ featureName: "timetickets", bodyshop })
? [
<Form.Item
label={t("bodyshop.fields.rbac.shiftclock.view")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "shiftclock:view"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.shop.config")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "shop:config"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.edit")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:edit"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.shiftedit")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:shiftedit"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.editcommitted")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:editcommitted"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.ttapprovals.view")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "ttapprovals:view"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.ttapprovals.approve")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "ttapprovals:approve"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.enter")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:enter"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.list")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:list"]}
>
<InputNumber />
</Form.Item>
]
: []}
{...HasFeatureAccess({ featureName: "timetickets", bodyshop }) ? [
<Form.Item
label={t("bodyshop.fields.rbac.shiftclock.view")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "shiftclock:view"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.shop.config")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "shop:config"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.edit")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:edit"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.shiftedit")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:shiftedit"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.editcommitted")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:editcommitted"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.ttapprovals.view")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "ttapprovals:view"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.ttapprovals.approve")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "ttapprovals:approve"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.enter")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:enter"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.list")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:list"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.shiftedit")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:shiftedit"]}
>
<InputNumber />
</Form.Item>
]:[]}
<Form.Item
label={t("bodyshop.fields.rbac.shop.vendors")}
rules={[
@@ -740,6 +757,7 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
<InputNumber />
</Form.Item>
)}
<Form.Item
label={t("bodyshop.fields.rbac.users.editaccess")}
rules={[

View File

@@ -1,54 +0,0 @@
import { Alert, Form, InputNumber, Switch } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoIntellipay);
export function ShopInfoIntellipay({ bodyshop, form }) {
const { t } = useTranslation();
return (
<>
<Form.Item dependencies={[["intellipay_config", "enable_cash_discount"]]}>
{() => {
const { intellipay_config } = form.getFieldsValue();
if (intellipay_config?.enable_cash_discount)
return <Alert message={t("bodyshop.labels.intellipay_cash_discount")} />;
}}
</Form.Item>
<LayoutFormRow noDivider>
<Form.Item
label={t("bodyshop.fields.intellipay_config.enable_cash_discount")}
valuePropName="checked"
name={["intellipay_config", "enable_cash_discount"]}
>
<Switch />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.intellipay_config.cash_discount_percentage")}
valuePropName="checked"
dependencies={[["intellipay_config", "enable_cash_discount"]]}
name={["intellipay_config", "cash_discount_percentage"]}
rules={[
({ getFieldsValue }) => ({ required: form.getFieldValue(["intellipay_config", "enable_cash_discount"]) })
]}
>
<InputNumber min={0} max={100} precision={1} suffix='%'/>
</Form.Item>
</LayoutFormRow>
</>
);
}

View File

@@ -12,7 +12,7 @@ import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import { onlyUnique } from "../../utils/arrayHelper";
import dayjs from "../../utils/day";
import { alphaSort, dateSort } from "../../utils/sorters";
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
import RbacWrapper, { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
import TimeTicketEnterButton from "../time-ticket-enter-button/time-ticket-enter-button.component";
const mapStateToProps = createStructuredSelector({
@@ -52,10 +52,6 @@ export function TimeTicketList({
splitKey: bodyshop.imexshopid
});
const canEditCommittedTimeTickets = HasRbacAccess({ bodyshop, authLevel, action: "timetickets:editcommitted" });
const canEditTimeTickets = HasRbacAccess({ bodyshop, authLevel, action: "timetickets:edit" });
const canEditShiftTickets = HasRbacAccess({ bodyshop, authLevel, action: "timetickets:shiftedit" });
const totals = useMemo(() => {
if (timetickets)
return timetickets.reduce(
@@ -69,18 +65,6 @@ export function TimeTicketList({
return { productivehrs: 0, actualhrs: 0 };
}, [timetickets]);
const isDisabled = (record) => {
if (disabled === true || !record.id) return true;
const isShiftTicket = !record.ciecacode;
const isCommitted = record.committed_at;
if (isShiftTicket) {
return !(canEditShiftTickets && (!isCommitted || canEditCommittedTimeTickets));
}
return !(canEditTimeTickets && (!isCommitted || canEditCommittedTimeTickets));
};
const columns = [
...(Enhanced_Payroll.treatment === "on"
? [
@@ -257,16 +241,59 @@ export function TimeTicketList({
</TimeTicketEnterButton>
)}
{!techConsole && (
<TimeTicketEnterButton
actions={{ refetch }}
context={{
id: record.id,
timeticket: record
<RbacWrapper
action="timetickets:edit"
noauth={() => {
return <div />;
}}
disabled={isDisabled(record)}
>
<EditFilled />
</TimeTicketEnterButton>
<TimeTicketEnterButton
actions={{ refetch }}
context={{
id: record.id,
timeticket: record
}}
disabled={
record.ciecacode
? record.committed_at
? HasRbacAccess({
bodyshop,
authLevel: authLevel,
action: "timetickets:editcommitted"
}) &&
HasRbacAccess({
bodyshop,
authLevel: authLevel,
action: "timetickets:edit"
})
: HasRbacAccess({
bodyshop,
authLevel: authLevel,
action: "timetickets:edit"
})
: record.committed_at
? HasRbacAccess({
bodyshop,
authLevel: authLevel,
action: "timetickets:editcommitted"
}) &&
HasRbacAccess({
bodyshop,
authLevel: authLevel,
action: "timetickets:shiftedit"
})
: HasRbacAccess({
bodyshop,
authLevel: authLevel,
action: "timetickets:shiftedit"
})
? disabled
: !record.jobid
}
>
<EditFilled />
</TimeTicketEnterButton>
</RbacWrapper>
)}
</Space>
)

View File

@@ -329,9 +329,7 @@ export function LaborAllocationContainer({ jobid, loading, lineTicketData, hideT
timetickets={lineTicketData.timetickets}
adjustments={lineTicketData.jobs_by_pk.lbr_adjustments}
/>
{!hideTimeTickets && (
<TimeTicketList loading={loading} timetickets={jobid ? lineTicketData.timetickets : []} techConsole />
)}
{!hideTimeTickets && <TimeTicketList loading={loading} timetickets={lineTicketData.timetickets} techConsole />}
</div>
);
}

View File

@@ -1,14 +1,13 @@
import { AlertOutlined } from "@ant-design/icons";
import { Alert, Button, Col, notification, Row, Space } from "antd";
import { Alert, Button, Col, Row, Space } from "antd";
import i18n from "i18next";
import React, { useCallback, useEffect, useState } from "react";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectUpdateAvailable } from "../../redux/application/application.selectors";
import { useRegisterSW } from "virtual:pwa-register/react";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import useCountDown from "../../utils/countdownHook";
const mapStateToProps = createStructuredSelector({
updateAvailable: selectUpdateAvailable
@@ -20,15 +19,6 @@ const mapDispatchToProps = (dispatch) => ({
export function UpdateAlert({ updateAvailable }) {
const { t } = useTranslation();
const [timerStarted, setTimerStarted] = useState(false);
const [loading, setLoading] = useState(false);
const [
timeLeft,
{
start //pause, resume, reset
}
] = useCountDown(180000, 1000);
const {
offlineReady: [offlineReady],
needRefresh: [needRefresh],
@@ -41,7 +31,7 @@ export function UpdateAlert({ updateAvailable }) {
() => {
r.update();
},
30 * 60 * 1000
10 * 60 * 1000
);
}
},
@@ -50,43 +40,11 @@ export function UpdateAlert({ updateAvailable }) {
}
});
const ReloadNewVersion = useCallback(() => {
setLoading(true);
updateServiceWorker(true);
setTimeout(() => {
window.location.reload(true);
}, 5000);
}, [updateServiceWorker]);
useEffect(() => {
if (needRefresh) {
start();
setTimerStarted(true);
if (import.meta.env.DEV) {
console.log(`SW Status => Refresh? ${needRefresh} - offlineReady? ${offlineReady}`);
}
}, [start, needRefresh, offlineReady]);
useEffect(() => {
if (needRefresh && timerStarted && timeLeft < 60000) {
notification.open({
type: "warning",
closable: false,
duration: 65000,
key: "autoupdate",
message: t("general.actions.autoupdate", {
time: (timeLeft / 1000).toFixed(0),
app: InstanceRenderManager({
imex: "$t(titles.imexonline)",
rome: "$t(titles.romeonline)",
promanager: "$t(titles.promanager)"
})
}),
placement: "bottomRight"
});
}
if (needRefresh && timerStarted && timeLeft <= 0) {
ReloadNewVersion();
}
}, [timeLeft, t, needRefresh, ReloadNewVersion, timerStarted]);
}, [needRefresh, offlineReady]);
if (!needRefresh) return null;
@@ -117,10 +75,9 @@ export function UpdateAlert({ updateAvailable }) {
<Button onClick={() => window.open("https://imex-online.noticeable.news/", "_blank")}>
{i18n.t("general.actions.viewreleasenotes")}
</Button>
<Button loading={loading} type="primary" onClick={() => ReloadNewVersion()}>
{i18n.t("general.actions.refresh")} {`(${(timeLeft / 1000).toFixed(0)} s)`}
<Button type="primary" onClick={() => updateServiceWorker(true)}>
{i18n.t("general.actions.refresh")}
</Button>
<Button onClick={() => start(300000)}>{i18n.t("general.actions.delay")}</Button>
</Space>
</Col>
</Row>

View File

@@ -5,7 +5,7 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter";
const { Option } = Select;
// To be used as a form element only.
//To be used as a form element only.
const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, preferredMake, showPhone }, ref) => {
const [option, setOption] = useState(value);
@@ -33,25 +33,9 @@ const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, pref
if (!value || !options) return label;
const discount = options?.find((o) => o.id === value)?.discount;
return (
<div
style={{
display: "flex",
alignItems: "center",
flexWrap: "nowrap",
width: "100%"
}}
>
<div
style={{
flex: 1,
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap"
}}
>
{label}
</div>
<div className="imex-flex-row" style={{ width: "100%" }}>
<div style={{ flex: 1 }}>{label}</div>
{discount && discount !== 0 ? <Tag color="green">{`${discount * 100}%`}</Tag> : null}
</div>
);
@@ -61,67 +45,36 @@ const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, pref
optionFilterProp="name"
onSelect={onSelect}
disabled={disabled || false}
optionLabelProp="name"
optionLabelProp={"name"}
>
{favorites &&
favorites.map((o) => (
<Option key={`favorite-${o.id}`} value={o.id} name={o.name} discount={o.discount}>
<div
style={{
display: "flex",
alignItems: "center",
flexWrap: "nowrap",
width: "100%"
}}
>
<div
style={{
flex: 1,
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap"
}}
>
{o.name}
{favorites
? favorites.map((o) => (
<Option key={`favorite-${o.id}`} value={o.id} name={o.name} discount={o.discount}>
<div className="imex-flex-row">
<div style={{ flex: 1 }}>{o.name}</div>
<Space style={{ marginLeft: "1rem" }}>
<HeartOutlined style={{ color: "red" }} />
{o.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
</Space>
</div>
<Space style={{ marginLeft: "1rem" }}>
<HeartOutlined style={{ color: "red" }} />
{o.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
</Space>
</div>
</Option>
))}
{options &&
options.map((o) => (
<Option key={o.id} value={o.id} name={o.name} discount={o.discount}>
<div
style={{
display: "flex",
alignItems: "center",
flexWrap: "nowrap",
width: "100%"
}}
>
<div
style={{
flex: 1,
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap"
}}
>
{o.name}
</Option>
))
: null}
{options
? options.map((o) => (
<Option key={o.id} value={o.id} name={o.name} discount={o.discount}>
<div className="imex-flex-row" style={{ width: "100%" }}>
<div style={{ flex: 1 }}>{o.name}</div>
<Space style={{ marginLeft: "1rem" }}>
{o.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
</Space>
</div>
<Space style={{ marginLeft: "1rem" }}>
{o.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
</Space>
</div>
</Option>
))}
</Option>
))
: null}
</Select>
);
};

View File

@@ -1,18 +0,0 @@
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" }} />;
}

View File

@@ -1,13 +0,0 @@
import React, { createContext } from "react";
import useSocket from "./useSocket"; // Import the custom hook
// Create the SocketContext
const SocketContext = createContext(null);
export const SocketProvider = ({ children, bodyshop }) => {
const { socket, clientId } = useSocket(bodyshop);
return <SocketContext.Provider value={{ socket, clientId }}> {children}</SocketContext.Provider>;
};
export default SocketContext;

View File

@@ -1,88 +0,0 @@
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;

View File

@@ -5,6 +5,7 @@ import { getFirestore } from "firebase/firestore";
import { getMessaging, getToken, onMessage } from "firebase/messaging";
import { store } from "../redux/store";
import axios from "axios";
import { checkBeta } from "../utils/handleBeta";
const config = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
initializeApp(config);
@@ -87,7 +88,7 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
operationName: eventName,
variables: additionalParams,
dbevent: false,
env: `master-AIO|${import.meta.env.VITE_APP_GIT_SHA_DATE}`
env: checkBeta() ? "beta" : "master"
});
// console.log(
// "%c[Analytics]",

View File

@@ -138,8 +138,7 @@ export const QUERY_BODYSHOP = gql`
tt_enforce_hours_for_tech_console
md_tasks_presets
use_paint_scale_data
intellipay_config
md_ro_guard
md_ro_guard
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
id
name
@@ -267,8 +266,7 @@ export const UPDATE_SHOP = gql`
enforce_conversion_category
tt_enforce_hours_for_tech_console
md_tasks_presets
intellipay_config
md_ro_guard
md_ro_guard
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
id
name

View File

@@ -2461,14 +2461,6 @@ 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 {

View File

@@ -23,14 +23,17 @@ 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 : "", {
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 : window.location.origin,
{
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();

View File

@@ -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 : "", // for dev testing,
import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "http://localhost:4000", // for dev testing,
{
path: "/ws",
withCredentials: true,

View File

@@ -1,6 +1,6 @@
import { FloatButton, Layout, Spin } from "antd";
// import preval from "preval.macro";
import React, { lazy, Suspense, useContext, useEffect, useState } from "react";
import React, { lazy, Suspense, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, Route, Routes } from "react-router-dom";
@@ -19,13 +19,11 @@ 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"));
@@ -112,7 +110,6 @@ const mapDispatchToProps = (dispatch) => ({});
export function Manage({ conflict, bodyshop }) {
const { t } = useTranslation();
const [chatVisible] = useState(false);
const { socket, clientId } = useContext(SocketContext);
useEffect(() => {
const widgetId = InstanceRenderManager({
@@ -132,7 +129,6 @@ export function Manage({ conflict, bodyshop }) {
promanager: t("titles.promanager")
});
}, [t]);
const AppRouteTable = (
<Suspense
fallback={
@@ -573,13 +569,6 @@ 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} />}
@@ -605,8 +594,7 @@ export function Manage({ conflict, bodyshop }) {
}}
>
<div style={{ display: "flex" }}>
<WssStatusDisplayComponent />
<div onClick={broadcastMessage}>
<div>
{`${InstanceRenderManager({
imex: t("titles.imexonline"),
rome: t("titles.romeonline"),

View File

@@ -1,26 +1,6 @@
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 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} />;
export default function ProductionBoardComponent() {
return <ProductionBoardKanbanContainer />;
}

View File

@@ -2,31 +2,11 @@ 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";
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
});
export default function ProductionListComponent() {
return (
<>
<NoteUpsertModal />
<ProductionListTable bodyshop={bodyshop} subscriptionType={Production_Use_View.treatment} />
<ProductionListTable />
</>
);
}

View File

@@ -1,21 +1,21 @@
import { Tabs } from "antd";
import queryString from "query-string";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import ShopCsiConfig from "../../components/shop-csi-config/shop-csi-config.component";
import queryString from "query-string";
import { useTranslation } from "react-i18next";
import ShopEmployeesContainer from "../../components/shop-employees/shop-employees.container";
import ShopInfoContainer from "../../components/shop-info/shop-info.container";
import ShopInfoUsersComponent from "../../components/shop-users/shop-users.component";
import ShopCsiConfig from "../../components/shop-csi-config/shop-csi-config.component";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import ShopInfoUsersComponent from "../../components/shop-users/shop-users.component";
import { HasFeatureAccess } from "../../components/feature-wrapper/feature-wrapper.component";
import ShopTeamsContainer from "../../components/shop-teams/shop-teams.container";
import { HasFeatureAccess } from "../../components/feature-wrapper/feature-wrapper.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop

View File

@@ -3,7 +3,6 @@ import { useTranslation } from "react-i18next";
import RbacWrapperComponent from "../../components/rbac-wrapper/rbac-wrapper.component";
import TechLookupJobsList from "../../components/tech-lookup-jobs-list/tech-lookup-jobs-list.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import TechLookupJobsDrawer from "../../components/tech-lookup-jobs-drawer/tech-lookup-jobs-drawer.component";
export default function TechLookupContainer() {
const { t } = useTranslation();
@@ -21,7 +20,6 @@ export default function TechLookupContainer() {
return (
<div>
<RbacWrapperComponent action="jobs:list-active">
<TechLookupJobsDrawer />
<TechLookupJobsList />
</RbacWrapperComponent>
</div>

View File

@@ -9,6 +9,7 @@ import ErrorBoundary from "../../components/error-boundary/error-boundary.compon
import FeatureWrapper from "../../components/feature-wrapper/feature-wrapper.component";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import TechHeader from "../../components/tech-header/tech-header.component";
import TechLookupJobsDrawer from "../../components/tech-lookup-jobs-drawer/tech-lookup-jobs-drawer.component";
import TechSider from "../../components/tech-sider/tech-sider.component";
import UpdateAlert from "../../components/update-alert/update-alert.component";
import { selectTechnician } from "../../redux/tech/tech.selectors";
@@ -67,7 +68,7 @@ export function TechPage({ technician }) {
<Layout>
<UpdateAlert />
<TechHeader />
<TechLookupJobsDrawer />
<TaskUpsertModalContainer />
<Content className="tech-content-container">
<ErrorBoundary>

View File

@@ -67,7 +67,3 @@ export const setUpdateAvailable = (isUpdateAvailable) => ({
type: ApplicationActionTypes.SET_UPDATE_AVAILABLE,
payload: isUpdateAvailable
});
export const setWssStatus = (status) => ({
type: ApplicationActionTypes.SET_WSS_STATUS,
payload: status
});

View File

@@ -3,7 +3,6 @@ import ApplicationActionTypes from "./application.types";
const INITIAL_STATE = {
loading: false,
online: true,
wssStatus: "disconnected",
updateAvailable: false,
breadcrumbs: [],
recentItems: [],
@@ -88,9 +87,6 @@ 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;
}

View File

@@ -22,4 +22,3 @@ 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);

View File

@@ -12,7 +12,6 @@ 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_WSS_STATUS: "SET_WSS_STATUS"
SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE"
};
export default ApplicationActionTypes;

View File

@@ -36,8 +36,7 @@ export function* openChatByPhone({ payload }) {
data: { conversations }
} = yield client.query({
query: CONVERSATION_ID_BY_PHONE,
variables: { phone: p.number },
fetchPolicy: 'no-cache'
variables: { phone: p.number }
});
if (conversations.length === 0) {

View File

@@ -10,7 +10,7 @@ import {
signInWithEmailAndPassword,
signOut
} from "firebase/auth";
import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "firebase/firestore";
import { doc, getDoc, setDoc } from "firebase/firestore";
import { getToken } from "firebase/messaging";
import i18next from "i18next";
import LogRocket from "logrocket";
@@ -48,7 +48,6 @@ import {
validatePasswordResetSuccess
} from "./user.actions";
import UserActionTypes from "./user.types";
import cleanAxios from "../../utils/CleanAxios";
const fpPromise = FingerprintJS.load();
@@ -178,24 +177,10 @@ export function* setInstanceIdSaga({ payload: uid }) {
// Get the visitor identifier when you need it.
const fp = yield fpPromise;
const result = yield fp.get();
const res = yield cleanAxios.get("https://api.ipify.org/?format=json");
const udoc = yield getDoc(userInstanceRef);
if (!udoc.data()) {
yield setDoc(userInstanceRef, {
timestamp: new Date(),
fingerprint: result.visitorId,
//totalFingerprint: result,
ip: [res.data.ip]
});
} else {
yield updateDoc(userInstanceRef, {
timestamp: new Date(),
fingerprint: result.visitorId,
//totalFingerprint: result,
ip: arrayUnion(res.data.ip)
});
}
yield setDoc(userInstanceRef, {
timestamp: new Date(),
fingerprint: result.visitorId
});
yield put(setLocalFingerprint(result.visitorId));
yield delay(5 * 60 * 1000);
@@ -242,10 +227,6 @@ 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
@@ -314,8 +295,8 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
try {
const userEmail = yield select((state) => state.user.currentUser.email);
try {
console.log("Setting shop timezone.");
day.tz.setDefault(payload.timezone);
//console.log("Setting shop timezone.");
// dayjs.tz.setDefault(payload.timezone);
} catch (error) {
console.log(error);
}
@@ -375,9 +356,6 @@ 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) {

View File

@@ -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": "Job has been exported",
"jobexported": "",
"jobfieldchanged": "Job field $t(jobs.fields.{{field}}) changed to {{value}}.",
"jobimported": "Job imported.",
"jobinproductionchange": "Job production status set to {{inproduction}}",
@@ -230,7 +230,7 @@
"markexported": "Mark Exported",
"markforreexport": "Mark for Re-export",
"new": "New Bill",
"nobilllines": "",
"nobilllines": "This part has not yet been recieved.",
"noneselected": "No bill selected.",
"onlycmforinvoiced": "Only credit memos can be entered for any Job that has been invoiced, exported, or voided.",
"printlabels": "Print Labels",
@@ -270,9 +270,9 @@
"testrender": "Test Render"
},
"errors": {
"creatingdefaultview": "Error creating default view.",
"loading": "Unable to load shop details. Please call technical support.",
"saving": "Error encountered while saving. {{message}}"
"saving": "Error encountered while saving. {{message}}",
"creatingdefaultview": "Error creating default view."
},
"fields": {
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
@@ -285,21 +285,19 @@
},
"appt_length": "Default Appointment Length",
"attach_pdf_to_email": "Attach PDF copy to sent emails?",
"batchid": "ADP Batch ID",
"bill_allow_post_to_closed": "Allow Bills to be posted to Closed Jobs",
"bill_federal_tax_rate": "Bills - Federal Tax Rate %",
"bill_local_tax_rate": "Bill - Local Tax Rate %",
"bill_state_tax_rate": "Bill - Provincial/State Tax Rate %",
"city": "City",
"closingperiod": "Closing Period",
"companycode": "ADP Company Code",
"country": "Country",
"dailybodytarget": "Scoreboard - Daily Body Target",
"dailypainttarget": "Scoreboard - Daily Paint Target",
"default_adjustment_rate": "Default Labor Deduction Adjustment Rate",
"deliver": {
"require_actual_delivery_date": "Require Actual Delivery",
"templates": "Delivery Templates"
"templates": "Delivery Templates",
"require_actual_delivery_date": "Require Actual Delivery"
},
"dms": {
"apcontrol": "AP Control Number",
@@ -332,10 +330,6 @@
"next_contact_hours": "Automatic Next Contact Date - Hours from Intake",
"templates": "Intake Templates"
},
"intellipay_config": {
"cash_discount_percentage": "Cash Discount %",
"enable_cash_discount": "Enable Cash Discounting"
},
"invoice_federal_tax_rate": "Invoices - Federal Tax Rate",
"invoice_local_tax_rate": "Invoices - Local Tax Rate",
"invoice_state_tax_rate": "Invoices - State Tax Rate",
@@ -667,8 +661,6 @@
"filehandlers": "Adjusters",
"insurancecos": "Insurance Companies",
"intakechecklist": "Intake Checklist",
"intellipay": "IntelliPay",
"intellipay_cash_discount": "Please ensure that cash discounting has been enabled on your merchant account. Reach out to IntelliPay Support if you need assistance. ",
"jobstatuses": "Job Statuses",
"laborrates": "Labor Rates",
"licensing": "Licensing",
@@ -708,10 +700,10 @@
"workingdays": "Working Days"
},
"successes": {
"areyousure": "Are you sure you want to continue?",
"defaultviewcreated": "Default view created successfully.",
"save": "Shop configuration saved successfully. ",
"unsavedchanges": "Unsaved changes will be lost. Are you sure you want to continue?"
"unsavedchanges": "Unsaved changes will be lost. Are you sure you want to continue?",
"areyousure": "Are you sure you want to continue?",
"defaultviewcreated": "Default view created successfully."
},
"validation": {
"centermustexist": "The chosen responsibility center does not exist.",
@@ -1141,8 +1133,8 @@
},
"general": {
"actions": {
"defaults": "Defaults",
"add": "Add",
"autoupdate": "{{app}} will automatically update in {{time}} seconds. Please save all changes.",
"calculate": "Calculate",
"cancel": "Cancel",
"clear": "Clear",
@@ -1150,8 +1142,6 @@
"copied": "Copied!",
"copylink": "Copy Link",
"create": "Create",
"defaults": "Defaults",
"delay": "Delay Update (5 mins)",
"delete": "Delete",
"deleteall": "Delete All",
"deselectall": "Deselect All",
@@ -1163,12 +1153,10 @@
"print": "Print",
"refresh": "Refresh",
"remove": "Remove",
"remove_alert": "Are you sure you want to dismiss the alert?",
"reset": "Reset your changes.",
"resetpassword": "Reset Password",
"save": "Save",
"saveandnew": "Save and New",
"saveas": "Save As",
"selectall": "Select All",
"send": "Send",
"sendbysms": "Send by SMS",
@@ -1176,7 +1164,9 @@
"submit": "Submit",
"tryagain": "Try Again",
"view": "View",
"viewreleasenotes": "See What's Changed"
"viewreleasenotes": "See What's Changed",
"remove_alert": "Are you sure you want to dismiss the alert?",
"saveas": "Save As"
},
"errors": {
"fcm": "You must allow notification permissions to have real time messaging. Click to try again.",
@@ -1191,6 +1181,7 @@
"vehicle": "Vehicle"
},
"labels": {
"unsavedchanges": "Unsaved changes.",
"actions": "Actions",
"areyousure": "Are you sure?",
"barcode": "Barcode",
@@ -1259,7 +1250,6 @@
"tuesday": "Tuesday",
"tvmode": "TV Mode",
"unknown": "Unknown",
"unsavedchanges": "Unsaved changes.",
"username": "Username",
"view": "View",
"wednesday": "Wednesday",
@@ -1373,7 +1363,6 @@
},
"job_payments": {
"buttons": {
"create_short_link": "Generate Short Link",
"goback": "Go Back",
"proceedtopayment": "Proceed to Payment",
"refundpayment": "Refund Payment"
@@ -2750,6 +2739,41 @@
}
},
"production": {
"constants": {
"main_profile": "Default"
},
"options": {
"small": "Small",
"medium": "Medium",
"large": "Large",
"vertical": "Vertical",
"horizontal": "Horizontal"
},
"settings": {
"layout": "Layout",
"information": "Information",
"statistics_title": "Statistics",
"board_settings": "Board Settings",
"filters_title": "Filters",
"filters": {
"md_ins_cos": "Insurance Companies",
"md_estimators": "Estimators"
},
"statistics": {
"total_hours_in_production": "Hours in Production",
"total_lab_in_production": "Body Hours in Production",
"total_lar_in_production": "Refinish Hours in Production",
"total_amount_in_production": "Dollars in Production",
"jobs_in_production": "Jobs in Production",
"total_hours_on_board": "Hours on Board",
"total_lab_on_board": "Body Hours on Board",
"total_lar_on_board": "Refinish Hours on Board",
"total_amount_on_board": "Dollars on Board",
"total_jobs_on_board": "Jobs on Board",
"tasks_in_production": "Tasks in Production",
"tasks_on_board": "Tasks on Board"
}
},
"actions": {
"addcolumns": "Add Columns",
"bodypriority-clear": "Clear Body Priority",
@@ -2764,23 +2788,29 @@
"suspend": "Suspend",
"unsuspend": "Unsuspend"
},
"constants": {
"main_profile": "Default"
},
"errors": {
"boardupdate": "Error encountered updating Job. {{message}}",
"name_exists": "A Profile with this name already exists. Please choose a different name.",
"name_required": "Profile name is required.",
"removing": "Error removing from production board. {{error}}",
"settings": "Error saving board settings: {{error}}"
"settings": "Error saving board settings: {{error}}",
"name_exists": "A Profile with this name already exists. Please choose a different name.",
"name_required": "Profile name is required."
},
"labels": {
"kiosk_mode": "Kiosk Mode",
"on": "On",
"off": "Off",
"wide": "Wide",
"tall": "Tall",
"vertical": "Vertical",
"horizontal": "Horizontal",
"orientation": "Board Orientation",
"card_size": "Card Size",
"model_info": "Vehicle Info",
"actual_in": "Actual In",
"addnewprofile": "Add New Profile",
"alert": "Alert",
"tasks": "Tasks",
"alertoff": "Remove alert from Job",
"alerton": "Add alert to Job",
"alerts": "Alerts",
"ats": "Alternative Transportation",
"bodyhours": "B",
"bodypriority": "B/P",
@@ -2790,7 +2820,6 @@
"qbo_usa": "QBO USA"
}
},
"card_size": "Card Size",
"cardcolor": "Colored Cards",
"cardsettings": "Card Settings",
"clm_no": "Claim Number",
@@ -2799,100 +2828,48 @@
"detailpriority": "D/P",
"employeeassignments": "Employee Assignments",
"employeesearch": "Employee Search",
"estimator": "Estimator",
"horizontal": "Horizontal",
"ins_co_nm": "Insurance Company Name",
"jobdetail": "Job Details",
"kiosk_mode": "Kiosk Mode",
"laborhrs": "Labor Hours",
"legend": "Legend:",
"model_info": "Vehicle Info",
"note": "Production Note",
"off": "Off",
"on": "On",
"orientation": "Board Orientation",
"ownr_nm": "Customer Name",
"paintpriority": "P/P",
"partsstatus": "Parts Status",
"estimator": "Estimator",
"subtotal": "Subtotal",
"production_note": "Production Note",
"refinishhours": "R",
"scheduled_completion": "Scheduled Completion",
"selectview": "Select a View",
"stickyheader": "Sticky Header (BETA)",
"sublets": "Sublets",
"subtotal": "Subtotal",
"tall": "Tall",
"tasks": "Tasks",
"totalhours": "Total Hrs ",
"touchtime": "T/T",
"vertical": "Vertical",
"viewname": "View Name",
"wide": "Wide"
},
"options": {
"horizontal": "Horizontal",
"large": "Large",
"medium": "Medium",
"small": "Small",
"vertical": "Vertical"
},
"settings": {
"board_settings": "Board Settings",
"filters": {
"md_estimators": "Estimators",
"md_ins_cos": "Insurance Companies"
},
"filters_title": "Filters",
"information": "Information",
"layout": "Layout",
"statistics": {
"jobs_in_production": "Jobs in Production",
"tasks_in_production": "Tasks in Production",
"tasks_on_board": "Tasks on Board",
"tasks_in_view": "Tasks in View",
"total_amount_in_production": "Dollars in Production",
"total_amount_on_board": "Dollars on Board",
"total_amount_in_view": "Dollars in View",
"total_hours_in_production": "Hours in Production",
"total_hours_on_board": "Hours on Board",
"total_hours_in_view": "Hours in View",
"total_jobs_on_board": "Jobs on Board",
"total_jobs_in_view": "Jobs in View",
"total_lab_in_production": "Body Hours in Production",
"total_lab_on_board": "Body Hours on Board",
"total_lab_in_view": "Body Hours in View",
"total_lar_in_production": "Refinish Hours in Production",
"total_lar_on_board": "Refinish Hours on Board",
"total_lar_in_view": "Refinish Hours in View"
},
"statistics_title": "Statistics"
},
"statistics": {
"currency_symbol": "$",
"hours": "Hours",
"jobs": "Jobs",
"jobs_in_production": "Jobs in Production",
"tasks": "Tasks",
"tasks_in_production": "Tasks in Production",
"tasks_on_board": "Tasks on Board",
"tasks_in_view": "Tasks in View",
"total_amount_in_production": "Dollars in Production",
"total_amount_on_board": "Dollars on Board",
"total_amount_in_view": "Dollars in View",
"total_hours_in_production": "Hours in Production",
"total_hours_on_board": "Hours on Board",
"total_hours_in_view": "Hours in View",
"total_jobs_on_board": "Jobs on Board",
"total_jobs_in_view": "Jobs in View",
"total_lab_in_production": "Body Hours in Production",
"total_lab_on_board": "Body Hours on Board",
"total_lab_in_view": "Body Hours in View",
"total_lar_in_production": "Refinish Hours in Production",
"total_lar_on_board": "Refinish Hours on Board",
"total_lar_in_view": "Refinish Hours in View"
"alerts": "Alerts",
"addnewprofile": "Add New Profile"
},
"successes": {
"removed": "Job removed from production."
},
"statistics": {
"total_hours_in_production": "Hours in Production",
"total_lab_in_production": "Body Hours in Production",
"total_lar_in_production": "Refinish Hours in Production",
"total_amount_in_production": "Dollars in Production",
"jobs_in_production": "Jobs in Production",
"total_hours_on_board": "Hours on Board",
"total_lab_on_board": "Body Hours on Board",
"total_lar_on_board": "Refinish Hours on Board",
"total_amount_on_board": "Dollars on Board",
"total_jobs_on_board": "Jobs on Board",
"tasks_in_production": "Tasks in Production",
"tasks_on_board": "Tasks on Board",
"tasks": "Tasks",
"hours": "Hours",
"currency_symbol": "$",
"jobs": "Jobs"
}
},
"profile": {
@@ -2950,8 +2927,6 @@
"vendor": "Vendor"
},
"templates": {
"adp_payroll_flat": "ADP Payroll - Flat Rate",
"adp_payroll_straight": "ADP Payroll - Straight Time",
"anticipated_revenue": "Anticipated Revenue",
"ar_aging": "AR Aging",
"attendance_detail": "Attendance (All Employees)",
@@ -3443,18 +3418,6 @@
"vehicledetail": "Vehicle Details {{vehicle}} | {{app}}",
"vehicles": "All Vehicles | {{app}}"
},
"trello": {
"labels": {
"add_card": "Add Card",
"add_lane": "Add Lane",
"cancel": "Cancel",
"delete_lane": "Delete Lane",
"description": "Description",
"label": "Label",
"lane_actions": "Lane Actions",
"title": "Title"
}
},
"tt_approvals": {
"actions": {
"approveselected": "Approve Selected"
@@ -3593,6 +3556,18 @@
"validation": {
"unique_vendor_name": "You must enter a unique vendor name."
}
},
"trello": {
"labels": {
"add_card": "Add Card",
"add_lane": "Add Lane",
"delete_lane": "Delete Lane",
"lane_actions": "Lane Actions",
"title": "Title",
"description": "Description",
"label": "Label",
"cancel": "Cancel"
}
}
}
}

View File

@@ -270,9 +270,9 @@
"testrender": ""
},
"errors": {
"creatingdefaultview": "",
"loading": "No se pueden cargar los detalles de la tienda. Por favor llame al soporte técnico.",
"saving": ""
"saving": "",
"creatingdefaultview": ""
},
"fields": {
"ReceivableCustomField": "",
@@ -285,21 +285,19 @@
},
"appt_length": "",
"attach_pdf_to_email": "",
"batchid": "",
"bill_allow_post_to_closed": "",
"bill_federal_tax_rate": "",
"bill_local_tax_rate": "",
"bill_state_tax_rate": "",
"city": "",
"closingperiod": "",
"companycode": "",
"country": "",
"dailybodytarget": "",
"dailypainttarget": "",
"default_adjustment_rate": "",
"deliver": {
"require_actual_delivery_date": "",
"templates": ""
"templates": "",
"require_actual_delivery_date": ""
},
"dms": {
"apcontrol": "",
@@ -332,10 +330,6 @@
"next_contact_hours": "",
"templates": ""
},
"intellipay_config": {
"cash_discount_percentage": "",
"enable_cash_discount": ""
},
"invoice_federal_tax_rate": "",
"invoice_local_tax_rate": "",
"invoice_state_tax_rate": "",
@@ -667,8 +661,6 @@
"filehandlers": "",
"insurancecos": "",
"intakechecklist": "",
"intellipay": "",
"intellipay_cash_discount": "",
"jobstatuses": "",
"laborrates": "",
"licensing": "",
@@ -708,10 +700,10 @@
"workingdays": ""
},
"successes": {
"areyousure": "",
"defaultviewcreated": "",
"save": "",
"unsavedchanges": ""
"unsavedchanges": "",
"areyousure": "",
"defaultviewcreated": ""
},
"validation": {
"centermustexist": "",
@@ -1141,8 +1133,8 @@
},
"general": {
"actions": {
"defaults": "defaults",
"add": "",
"autoupdate": "",
"calculate": "",
"cancel": "",
"clear": "",
@@ -1150,8 +1142,6 @@
"copied": "",
"copylink": "",
"create": "",
"defaults": "defaults",
"delay": "",
"delete": "Borrar",
"deleteall": "",
"deselectall": "",
@@ -1163,12 +1153,10 @@
"print": "",
"refresh": "",
"remove": "",
"remove_alert": "",
"reset": " Restablecer a original.",
"resetpassword": "",
"save": "Salvar",
"saveandnew": "",
"saveas": "",
"selectall": "",
"send": "",
"sendbysms": "",
@@ -1176,7 +1164,9 @@
"submit": "",
"tryagain": "",
"view": "",
"viewreleasenotes": ""
"viewreleasenotes": "",
"remove_alert": "",
"saveas": ""
},
"errors": {
"fcm": "",
@@ -1191,6 +1181,7 @@
"vehicle": ""
},
"labels": {
"unsavedchanges": "",
"actions": "Comportamiento",
"areyousure": "",
"barcode": "código de barras",
@@ -1259,7 +1250,6 @@
"tuesday": "",
"tvmode": "",
"unknown": "Desconocido",
"unsavedchanges": "",
"username": "",
"view": "",
"wednesday": "",
@@ -1373,7 +1363,6 @@
},
"job_payments": {
"buttons": {
"create_short_link": "",
"goback": "",
"proceedtopayment": "",
"refundpayment": ""
@@ -2750,6 +2739,41 @@
}
},
"production": {
"constants": {
"main_profile": ""
},
"options": {
"small": "",
"medium": "",
"large": "",
"vertical": "",
"horizontal": ""
},
"settings": {
"layout": "",
"information": "",
"statistics_title": "",
"board_settings": "",
"filters_title": "",
"filters": {
"md_ins_cos": "",
"md_estimators": ""
},
"statistics": {
"total_hours_in_production": "",
"total_lab_in_production": "",
"total_lar_in_production": "",
"total_amount_in_production": "",
"jobs_in_production": "",
"total_hours_on_board": "",
"total_lab_on_board": "",
"total_lar_on_board": "",
"total_amount_on_board": "",
"total_jobs_on_board": "",
"tasks_in_production": "",
"tasks_on_board": ""
}
},
"actions": {
"addcolumns": "",
"bodypriority-clear": "",
@@ -2764,23 +2788,29 @@
"suspend": "",
"unsuspend": ""
},
"constants": {
"main_profile": ""
},
"errors": {
"boardupdate": "",
"name_exists": "",
"name_required": "",
"removing": "",
"settings": ""
"settings": "",
"name_exists": "",
"name_required": ""
},
"labels": {
"kiosk_mode": "",
"on": "",
"off": "",
"wide": "",
"tall": "",
"vertical": "",
"horizontal": "",
"orientation": "",
"card_size": "",
"model_info": "",
"actual_in": "",
"addnewprofile": "",
"alert": "",
"tasks": "",
"alertoff": "",
"alerton": "",
"alerts": "",
"ats": "",
"bodyhours": "",
"bodypriority": "",
@@ -2790,7 +2820,6 @@
"qbo_usa": ""
}
},
"card_size": "",
"cardcolor": "",
"cardsettings": "",
"clm_no": "",
@@ -2799,100 +2828,48 @@
"detailpriority": "",
"employeeassignments": "",
"employeesearch": "",
"estimator": "",
"horizontal": "",
"ins_co_nm": "",
"jobdetail": "",
"kiosk_mode": "",
"laborhrs": "",
"legend": "",
"model_info": "",
"note": "",
"off": "",
"on": "",
"orientation": "",
"ownr_nm": "",
"paintpriority": "",
"partsstatus": "",
"estimator": "",
"subtotal": "",
"production_note": "",
"refinishhours": "",
"scheduled_completion": "",
"selectview": "",
"stickyheader": "",
"sublets": "",
"subtotal": "",
"tall": "",
"tasks": "",
"totalhours": "",
"touchtime": "",
"vertical": "",
"viewname": "",
"wide": ""
},
"options": {
"horizontal": "",
"large": "",
"medium": "",
"small": "",
"vertical": ""
},
"settings": {
"board_settings": "",
"filters": {
"md_estimators": "",
"md_ins_cos": ""
},
"filters_title": "",
"information": "",
"layout": "",
"statistics": {
"jobs_in_production": "",
"tasks_in_production": "",
"tasks_on_board": "",
"tasks_in_view": "",
"total_amount_in_production": "",
"total_amount_on_board": "",
"total_amount_in_view": "",
"total_hours_in_production": "",
"total_hours_on_board": "",
"total_hours_in_view": "",
"total_jobs_on_board": "",
"total_jobs_in_view": "",
"total_lab_in_production": "",
"total_lab_on_board": "",
"total_lab_in_view": "",
"total_lar_in_production": "",
"total_lar_on_board": "",
"total_lar_in_view": ""
},
"statistics_title": ""
},
"statistics": {
"currency_symbol": "",
"hours": "",
"jobs": "",
"jobs_in_production": "",
"tasks": "",
"tasks_in_production": "",
"tasks_on_board": "",
"tasks_in_view": "",
"total_amount_in_production": "",
"total_amount_on_board": "",
"total_amount_in_view": "",
"total_hours_in_production": "",
"total_hours_on_board": "",
"total_hours_in_view": "",
"total_jobs_on_board": "",
"total_jobs_in_view": "",
"total_lab_in_production": "",
"total_lab_on_board": "",
"total_lab_in_view": "",
"total_lar_in_production": "",
"total_lar_on_board": "",
"total_lar_in_view": ""
"alerts": "",
"addnewprofile": ""
},
"successes": {
"removed": ""
},
"statistics": {
"total_hours_in_production": "",
"total_lab_in_production": "",
"total_lar_in_production": "",
"total_amount_in_production": "",
"jobs_in_production": "",
"total_hours_on_board": "",
"total_lab_on_board": "",
"total_lar_on_board": "",
"total_amount_on_board": "",
"total_jobs_on_board": "",
"tasks_in_production": "",
"tasks_on_board": "",
"tasks": "",
"hours": "",
"currency_symbol": "",
"jobs": ""
}
},
"profile": {
@@ -2950,8 +2927,6 @@
"vendor": ""
},
"templates": {
"adp_payroll_flat": "",
"adp_payroll_straight": "",
"anticipated_revenue": "",
"ar_aging": "",
"attendance_detail": "",
@@ -3443,18 +3418,6 @@
"vehicledetail": "Detalles del vehículo {{vehicle}} | {{app}}",
"vehicles": "Todos los vehiculos | {{app}}"
},
"trello": {
"labels": {
"add_card": "",
"add_lane": "",
"cancel": "",
"delete_lane": "",
"description": "",
"label": "",
"lane_actions": "",
"title": ""
}
},
"tt_approvals": {
"actions": {
"approveselected": ""
@@ -3593,6 +3556,18 @@
"validation": {
"unique_vendor_name": ""
}
},
"trello": {
"labels": {
"add_card": "",
"add_lane": "",
"delete_lane": "",
"lane_actions": "",
"title": "",
"description": "",
"label": "",
"cancel": ""
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,9 +2,9 @@ import axios from "axios";
import { auth } from "../firebase/firebase.utils";
import InstanceRenderManager from "./instanceRenderMgr";
axios.defaults.baseURL = import.meta.env.DEV
? "/api/"
: import.meta.env.VITE_APP_AXIOS_BASE_API_URL || "https://api.imex.online/";
axios.defaults.baseURL =
import.meta.env.VITE_APP_AXIOS_BASE_API_URL ||
(import.meta.env.MODE === "production" ? "https://api.imex.online/" : "http://localhost:4000/");
export const axiosAuthInterceptorId = axios.interceptors.request.use(
async (config) => {

View File

@@ -2158,32 +2158,6 @@ export const TemplateList = (type, context) => {
field: i18n.t("tasks.fields.created_at")
},
group: "jobs"
},
adp_payroll_flat: {
title: i18n.t("reportcenter.templates.adp_payroll_flat"),
subject: i18n.t("reportcenter.templates.adp_payroll_flat"),
key: "adp_payroll_flat",
reporttype: "text",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.timetickets"),
field: i18n.t("timetickets.fields.committed_at")
},
group: "payroll",
adp_payroll: true
},
adp_payroll_straight: {
title: i18n.t("reportcenter.templates.adp_payroll_straight"),
subject: i18n.t("reportcenter.templates.adp_payroll_straight"),
key: "adp_payroll_straight",
reporttype: "text",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.timetickets"),
field: i18n.t("timetickets.fields.date")
},
group: "payroll",
adp_payroll: true
}
}
: {}),

View File

@@ -1,4 +1,3 @@
// 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;

View File

@@ -1,84 +0,0 @@
import React from "react";
const useCountDown = (timeToCount = 60 * 1000, interval = 1000) => {
const [timeLeft, setTimeLeft] = React.useState(0);
const timer = React.useRef({});
const run = (ts) => {
if (!timer.current.started) {
timer.current.started = ts;
timer.current.lastInterval = ts;
}
const localInterval = Math.min(interval, timer.current.timeLeft || Infinity);
if (ts - timer.current.lastInterval >= localInterval) {
timer.current.lastInterval += localInterval;
setTimeLeft((timeLeft) => {
timer.current.timeLeft = timeLeft - localInterval;
return timer.current.timeLeft;
});
}
if (ts - timer.current.started < timer.current.timeToCount) {
timer.current.requestId = window.requestAnimationFrame(run);
} else {
timer.current = {};
setTimeLeft(0);
}
};
const start = React.useCallback(
(ttc) => {
window.cancelAnimationFrame(timer.current.requestId);
const newTimeToCount = ttc !== undefined ? ttc : timeToCount;
timer.current.started = null;
timer.current.lastInterval = null;
timer.current.timeToCount = newTimeToCount;
timer.current.requestId = window.requestAnimationFrame(run);
setTimeLeft(newTimeToCount);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const pause = React.useCallback(() => {
window.cancelAnimationFrame(timer.current.requestId);
timer.current.started = null;
timer.current.lastInterval = null;
timer.current.timeToCount = timer.current.timeLeft;
}, []);
const resume = React.useCallback(
() => {
if (!timer.current.started && timer.current.timeLeft > 0) {
window.cancelAnimationFrame(timer.current.requestId);
timer.current.requestId = window.requestAnimationFrame(run);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const reset = React.useCallback(() => {
if (timer.current.timeLeft) {
window.cancelAnimationFrame(timer.current.requestId);
timer.current = {};
setTimeLeft(0);
}
}, []);
const actions = React.useMemo(
() => ({ start, pause, resume, reset }), // eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
React.useEffect(() => {
return () => window.cancelAnimationFrame(timer.current.requestId);
}, []);
return [timeLeft, actions];
};
export default useCountDown;

View File

@@ -0,0 +1,47 @@
export const BETA_KEY = "betaSwitchImex";
export const checkBeta = () => {
const cookie = document.cookie.split("; ").find((row) => row.startsWith(BETA_KEY));
return cookie ? cookie.split("=")[1] === "true" : false;
};
export const setBeta = (value) => {
const domain = window.location.hostname.split(".").slice(-2).join(".");
document.cookie = `${BETA_KEY}=${value}; path=/; domain=.${domain}`;
};
export const handleBeta = () => {
if (window.location.hostname.startsWith("localhost")) {
console.log("Not on beta or test, so no need to handle beta.");
return;
}
const isBeta = checkBeta();
const currentHostName = window.location.hostname;
// Determine if the host name starts with "beta" or "www.beta"
const isBetaHost = currentHostName.startsWith("beta.");
const isBetaHostWithWWW = currentHostName.startsWith("www.beta.");
if (isBeta) {
// If beta is on and we are not on a beta domain, redirect to the beta version
if (!isBetaHost && !isBetaHostWithWWW) {
const newHostName = currentHostName.startsWith("www.")
? `www.beta.${currentHostName.replace(/^www\./, "")}`
: `beta.${currentHostName}`;
const href = `${window.location.protocol}//${newHostName}${window.location.pathname}${window.location.search}${window.location.hash}`;
window.location.replace(href);
}
// Otherwise, if beta is on and we're already on a beta domain, stay there
} else {
// If beta is off and we are on a beta domain, redirect to the non-beta version
if (isBetaHost || isBetaHostWithWWW) {
const newHostName = currentHostName.replace(/^www\.beta\./, "www.").replace(/^beta\./, "");
const href = `${window.location.protocol}//${newHostName}${window.location.pathname}${window.location.search}${window.location.hash}`;
window.location.replace(href);
}
// Otherwise, if beta is off and we're not on a beta domain, stay there
}
};
export default handleBeta;

View File

@@ -2,5 +2,5 @@ import { store } from "../redux/store";
export function CreateExplorerLinkForJob({ jobid }) {
const bodyshop = store.getState().user.bodyshop;
return `imexmedia://`.concat(encodeURIComponent(`${bodyshop.localmediaservernetwork}\\Jobs\\${jobid}`));
return `imexmedia://${bodyshop.localmediaservernetwork}\\Jobs\\${jobid}`;
}

View File

@@ -3,22 +3,16 @@ import { promises as fsPromises } from "fs";
import { createRequire } from "module";
import * as path from "path";
import * as url from "url";
import { createLogger, defineConfig } from "vite";
import { 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() {
@@ -38,16 +32,10 @@ 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",
@@ -111,6 +99,7 @@ export default defineConfig({
reactVirtualizedFix(),
react(),
eslint()
// CompressionPlugin(), //Cloudfront already compresses assets, so not needed.
],
define: {
APP_VERSION: JSON.stringify(process.env.npm_package_version)
@@ -118,69 +107,7 @@ export default defineConfig({
server: {
host: true,
port: 3000,
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
}
}
open: true
},
build: {
rollupOptions: {
@@ -188,63 +115,17 @@ export default defineConfig({
manualChunks: {
antd: ["antd"],
"react-redux": ["react-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"]
redux: ["redux"]
}
}
}
},
optimizeDeps: {
include: [
"react",
"react-dom",
"antd",
"lodash",
"@sentry/react",
"@apollo/client",
"@reduxjs/toolkit",
"axios",
"react-router-dom",
"dayjs",
"redux",
"react-redux"
],
include: ["react", "react-dom", "antd", "@apollo/client", "@reduxjs/toolkit", "axios", "react-grid-layout"],
esbuildOptions: {
loader: {
".js": "jsx"
}
}
},
css: {
preprocessorOptions: {
scss: {
api: "modern-compiler",
quietDeps: true // Quite Deprecation Warnings, should be disabled occasionally before major upgrades
}
}
}
});

View File

@@ -1,179 +0,0 @@
#############################
# 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:

View File

@@ -1,4 +1,4 @@
Must set the environment variables using:
firebase functions:config:set auth.graphql_endpoint="https://db.dev.imex.online/v1/graphql"
firebase functions:config:set auth.graphql_endpoint="https://db.dev.bodyshop.app/v1/graphql"
auth.hasura_secret_admin_key="Dev-BodyShopApp!"

Some files were not shown because too many files have changed in this diff Show More