Compare commits

..

2 Commits

Author SHA1 Message Date
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
136d52ec0b - Merge Master, update packages.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-22 14:47:44 -04:00
287 changed files with 6498 additions and 15157 deletions

View File

@@ -5,7 +5,6 @@ orbs:
aws-s3: circleci/aws-s3@4.0.0 aws-s3: circleci/aws-s3@4.0.0
aws-cli: circleci/aws-cli@4.0 aws-cli: circleci/aws-cli@4.0
eb: circleci/aws-elastic-beanstalk@2.0.1 eb: circleci/aws-elastic-beanstalk@2.0.1
jira: circleci/jira@2.1.0
jobs: jobs:
imex-api-deploy: imex-api-deploy:
docker: docker:
@@ -19,12 +18,6 @@ jobs:
eb status --verbose eb status --verbose
eb deploy eb deploy
eb status 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: imex-hasura-migrate:
docker: docker:
@@ -40,16 +33,11 @@ jobs:
- run: - run:
name: Execute migration name: Execute migration
command: | 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 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 apply --endpoint https://db.imex.online/ --admin-secret << parameters.secret >>
hasura metadata reload --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: imex-app-build:
docker: docker:
- image: cimg/node:18.18.2 - image: cimg/node:18.18.2
@@ -74,7 +62,6 @@ jobs:
to: "s3://imex-online-production/" to: "s3://imex-online-production/"
arguments: "--exclude '*.map'" arguments: "--exclude '*.map'"
imex-app-beta-build: imex-app-beta-build:
docker: docker:
- image: cimg/node:18.18.2 - image: cimg/node:18.18.2
@@ -99,12 +86,6 @@ jobs:
from: dist from: dist
to: "s3://imex-online-beta/" to: "s3://imex-online-beta/"
arguments: "--exclude '*.map'" 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: rome-api-deploy:
docker: docker:
@@ -118,12 +99,7 @@ jobs:
eb status --verbose eb status --verbose
eb deploy eb deploy
eb status 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: rome-hasura-migrate:
docker: docker:
- image: cimg/node:18.18.2 - image: cimg/node:18.18.2
@@ -138,16 +114,11 @@ jobs:
- run: - run:
name: Execute migration name: Execute migration
command: | 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 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 apply --endpoint https://db.romeonline.io/ --admin-secret << parameters.secret >>
hasura metadata reload --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: rome-app-build:
docker: docker:
- image: cimg/node:18.18.2 - image: cimg/node:18.18.2
@@ -172,12 +143,6 @@ jobs:
from: dist from: dist
to: "s3://rome-online-production/" to: "s3://rome-online-production/"
arguments: "--exclude '*.map'" 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: promanager-app-build:
docker: docker:
@@ -203,12 +168,6 @@ jobs:
from: dist from: dist
to: "s3://promanager-production/" to: "s3://promanager-production/"
arguments: "--exclude '*.map'" 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: test-rome-hasura-migrate:
docker: docker:
@@ -224,18 +183,10 @@ jobs:
- run: - run:
name: Execute migration name: Execute migration
command: | 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 >> 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 >> 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 >> 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: test-rome-app-build:
docker: docker:
@@ -261,12 +212,6 @@ jobs:
from: dist from: dist
to: "s3://rome-online-test/" to: "s3://rome-online-test/"
arguments: "--exclude '*.map'" 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: test-promanager-app-build:
docker: docker:
@@ -292,12 +237,6 @@ jobs:
from: dist from: dist
to: "s3://promanager-testing/" to: "s3://promanager-testing/"
arguments: "--exclude '*.map'" 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: test-hasura-migrate:
docker: docker:
@@ -313,18 +252,10 @@ jobs:
- run: - run:
name: Execute migration name: Execute migration
command: | 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 >> 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 >> 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 >> 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: imex-test-app-build:
docker: docker:
@@ -371,12 +302,7 @@ jobs:
from: dist from: dist
to: "s3://imex-online-test-beta/" to: "s3://imex-online-test-beta/"
arguments: "--exclude '*.map'" 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: admin-app-build:
docker: docker:
@@ -427,7 +353,7 @@ workflows:
secret: ${HASURA_PROD_SECRET} secret: ${HASURA_PROD_SECRET}
filters: filters:
branches: branches:
only: master-AIO only: master
- rome-api-deploy: - rome-api-deploy:
filters: filters:
branches: branches:
@@ -437,7 +363,7 @@ workflows:
branches: branches:
only: master-AIO only: master-AIO
- rome-hasura-migrate: - rome-hasura-migrate:
secret: ${HASURA_ROME_PROD_SECRET} secret: ${HASURA_PROD_SECRET}
filters: filters:
branches: branches:
only: master-AIO 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

View File

@@ -1,2 +1 @@
client_max_body_size 50M; client_max_body_size 50M;
client_body_buffer_size 5M;

15
.vscode/launch.json vendored
View File

@@ -14,21 +14,6 @@
"request": "launch", "request": "launch",
"url": "http://localhost:3000", "url": "http://localhost:3000",
"webRoot": "${workspaceRoot}/client/src" "webRoot": "${workspaceRoot}/client/src"
},
{
"name": "Attach to Node.js in Docker",
"type": "node",
"request": "attach",
"address": "localhost",
"port": 9229,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/app",
"protocol": "inspector",
"restart": true,
"sourceMaps": true,
"skipFiles": [
"<node_internals>/**"
]
} }
] ]
} }

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

View File

@@ -1,7 +0,0 @@
This will connect to your dockers local stack session and render the email in HTML.
```shell
node index.js
```
http://localhost:3334

View File

@@ -1,116 +0,0 @@
// index.js
import express from 'express';
import fetch from 'node-fetch';
import {simpleParser} from 'mailparser';
const app = express();
const PORT = 3334;
app.get('/', async (req, res) => {
try {
const response = await fetch('http://localhost:4566/_aws/ses');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
const messagesHtml = await parseMessages(data.messages);
res.send(renderHtml(messagesHtml));
} catch (error) {
console.error('Error fetching messages:', error);
res.status(500).send('Error fetching messages');
}
});
async function parseMessages(messages) {
const parsedMessages = await Promise.all(
messages.map(async (message, index) => {
try {
const parsed = await simpleParser(message.RawData);
return `
<div class="shadow-md rounded-lg p-4 mb-6" style="background-color: lightgray">
<div class="shadow-md rounded-lg p-4 mb-6" style="background-color: white">
<div class="mb-2">
<span class="font-bold text-lg">Message ${index + 1}</span>
</div>
<div class="mb-2">
<span class="font-semibold">From:</span> ${message.Source}
</div>
<div class="mb-2">
<span class="font-semibold">Region:</span> ${message.Region}
</div>
<div class="mb-2">
<span class="font-semibold">Timestamp:</span> ${message.Timestamp}
</div>
</div>
<div class="prose">
${parsed.html || parsed.textAsHtml || 'No HTML content available'}
</div>
</div>
`;
} catch (error) {
console.error('Error parsing email:', error);
return `
<div class="bg-white shadow-md rounded-lg p-4 mb-6">
<div class="mb-2">
<span class="font-bold text-lg">Message ${index + 1}</span>
</div>
<div class="mb-2">
<span class="font-semibold">From:</span> ${message.Source}
</div>
<div class="mb-2">
<span class="font-semibold">Region:</span> ${message.Region}
</div>
<div class="mb-2">
<span class="font-semibold">Timestamp:</span> ${message.Timestamp}
</div>
<div class="text-red-500">
Error parsing email content
</div>
</div>
`;
}
})
);
return parsedMessages.join('');
}
function renderHtml(messagesHtml) {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email Messages Viewer</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
background-color: #f3f4f6;
font-family: Arial, sans-serif;
}
.container {
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
.prose {
line-height: 1.6;
}
</style>
</head>
<body>
<div class="container bg-white shadow-lg rounded-lg p-6">
<h1 class="text-2xl font-bold text-center mb-6">Email Messages Viewer</h1>
<div id="messages-container">
${messagesHtml}
</div>
</div>
</body>
</html>
`;
}
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +0,0 @@
{
"name": "localemailviewer",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"express": "^4.21.1",
"mailparser": "^3.7.1",
"node-fetch": "^3.3.2"
}
}

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=https://db.dev.bodyshop.app/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.imex.online/v1/graphql VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
VITE_APP_GA_CODE=231099835 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_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test VITE_APP_CLOUDINARY_ENDPOINT_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_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4' VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
VITE_APP_AXIOS_BASE_API_URL=/api/ VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_INSTANCE=IMEX VITE_APP_INSTANCE=IMEX

View File

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

View File

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

1
client/.gitignore vendored
View File

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

View File

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

View File

@@ -2,9 +2,6 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"/> <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') { %> <% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
<link rel="icon" href="/favicon.png"/> <link rel="icon" href="/favicon.png"/>
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %> <% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
@@ -17,7 +14,7 @@
<meta name="theme-color" content="#1690ff"/> <meta name="theme-color" content="#1690ff"/>
<!-- <link rel="apple-touch-icon" href="logo192.png" /> --> <!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
<!-- TODO:AIo Update the individual logos for each.--> <!-- TODO:AIo Update the individual logos for each.-->
<link rel="apple-touch-icon" href="/logo192.png"/> <link rel="apple-touch-icon" href="public/logo192.png"/>
<link rel="mask-icon" href="/mask-icon.svg" color="#FFFFFF"> <link rel="mask-icon" href="/mask-icon.svg" color="#FFFFFF">
<!-- <!--
manifest.json provides metadata used when your web app is installed on a manifest.json provides metadata used when your web app is installed on a
@@ -49,23 +46,77 @@
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %> <% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
<meta name="description" content="Rome Online"/> <meta name="description" content="Rome Online"/>
<title>Rome Online</title> <title>Rome Online</title>
<script type="text/javascript" id="zsiqchat">
var $zoho = $zoho || {}; <!--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-->
$zoho.salesiq = $zoho.salesiq || {
widgetcode: "siq01bb8ac617280bdacddfeb528f07734dadc64ef3f05efef9f769c1ec171af666", <call-us-selector phonesystem-url=https://rometech.east.3cx.us:5001
values: {}, party="LiveChat528346"></call-us-selector>
ready: function () {
} <!--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-->
};
var d = document; <!--<call-us
s = d.createElement("script");
s.type = "text/javascript"; phonesystem-url=https://rometech.east.3cx.us:5001
s.id = "zsiqscript";
s.defer = true; style="position:fixed;font-size:16px;line-height:17px;z-index: 99999;right: 20px; bottom: 20px;"
s.src = "https://salesiq.zohopublic.com/widget";
t = d.getElementsByTagName("script")[0]; id="wp-live-chat-by-3CX"
t.parentNode.insertBefore(s, t);
</script> 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') { %> <% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %>
<title>ProManager</title> <title>ProManager</title>

2109
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,37 +9,37 @@
"proxy": "http://localhost:4000", "proxy": "http://localhost:4000",
"dependencies": { "dependencies": {
"@ant-design/pro-layout": "^7.19.12", "@ant-design/pro-layout": "^7.19.12",
"@apollo/client": "^3.11.8", "@apollo/client": "^3.11.4",
"@emotion/is-prop-valid": "^1.3.1", "@emotion/is-prop-valid": "^1.3.0",
"@fingerprintjs/fingerprintjs": "^4.5.0", "@fingerprintjs/fingerprintjs": "^4.4.3",
"@jsreport/browser-client": "^3.1.0", "@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.2.7", "@reduxjs/toolkit": "^2.2.7",
"@sentry/cli": "^2.36.2", "@sentry/cli": "^2.33.1",
"@sentry/react": "^7.114.0", "@sentry/react": "^7.114.0",
"@splitsoftware/splitio-react": "^1.13.0", "@splitsoftware/splitio-react": "^1.12.1",
"@tanem/react-nprogress": "^5.0.51", "@tanem/react-nprogress": "^5.0.51",
"@vitejs/plugin-react": "^4.3.1", "@vitejs/plugin-react": "^4.3.1",
"antd": "^5.20.1", "antd": "^5.20.2",
"apollo-link-logger": "^2.0.1", "apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0", "apollo-link-sentry": "^3.3.0",
"autosize": "^6.0.1", "autosize": "^6.0.1",
"axios": "^1.7.7", "axios": "^1.7.4",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"css-box-model": "^1.2.1", "css-box-model": "^1.2.1",
"dayjs": "^1.11.13", "dayjs": "^1.11.12",
"dayjs-business-days2": "^1.2.2", "dayjs-business-days2": "^1.2.2",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"env-cmd": "^10.1.0", "env-cmd": "^10.1.0",
"exifr": "^7.1.3", "exifr": "^7.1.3",
"firebase": "^10.13.2", "firebase": "^10.13.0",
"graphql": "^16.9.0", "graphql": "^16.9.0",
"i18next": "^23.15.1", "i18next": "^23.14.0",
"i18next-browser-languagedetector": "^8.0.0", "i18next-browser-languagedetector": "^8.0.0",
"immutability-helper": "^3.1.1", "immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.11.9", "libphonenumber-js": "^1.11.7",
"logrocket": "^8.1.2", "logrocket": "^8.1.2",
"markerjs2": "^2.32.2", "markerjs2": "^2.32.1",
"memoize-one": "^6.0.0", "memoize-one": "^6.0.0",
"normalize-url": "^8.0.1", "normalize-url": "^8.0.1",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
@@ -47,7 +47,7 @@
"query-string": "^9.1.0", "query-string": "^9.1.0",
"raf-schd": "^4.0.3", "raf-schd": "^4.0.3",
"react": "^18.3.1", "react": "^18.3.1",
"react-big-calendar": "^1.14.1", "react-big-calendar": "^1.13.3",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-cookie": "^7.2.0", "react-cookie": "^7.2.0",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@@ -58,15 +58,15 @@
"react-icons": "^5.3.0", "react-icons": "^5.3.0",
"react-image-lightbox": "^5.1.4", "react-image-lightbox": "^5.1.4",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-number-format": "^5.4.2", "react-number-format": "^5.4.0",
"react-popopo": "^2.1.9", "react-popopo": "^2.1.9",
"react-product-fruits": "^2.2.61", "react-product-fruits": "^2.2.61",
"react-redux": "^9.1.2", "react-redux": "^9.1.2",
"react-resizable": "^3.0.5", "react-resizable": "^3.0.5",
"react-router-dom": "^6.26.2", "react-router-dom": "^6.26.1",
"react-sticky": "^6.0.3", "react-sticky": "^6.0.3",
"react-virtualized": "^9.22.5", "react-virtualized": "^9.22.5",
"react-virtuoso": "^4.10.4", "react-virtuoso": "^4.10.1",
"recharts": "^2.12.7", "recharts": "^2.12.7",
"redux": "^5.0.1", "redux": "^5.0.1",
"redux-actions": "^3.0.3", "redux-actions": "^3.0.3",
@@ -74,26 +74,22 @@
"redux-saga": "^1.3.0", "redux-saga": "^1.3.0",
"redux-state-sync": "^3.1.4", "redux-state-sync": "^3.1.4",
"reselect": "^5.1.1", "reselect": "^5.1.1",
"sass": "^1.79.3", "sass": "^1.77.8",
"socket.io-client": "^4.8.0", "socket.io-client": "^4.7.5",
"styled-components": "^6.1.13", "styled-components": "^6.1.12",
"subscriptions-transport-ws": "^0.11.0", "subscriptions-transport-ws": "^0.11.0",
"use-memo-one": "^1.1.3", "use-memo-one": "^1.1.3",
"userpilot": "^1.3.6", "userpilot": "^1.3.5",
"vite-plugin-ejs": "^1.7.0", "vite-plugin-ejs": "^1.7.0",
"web-vitals": "^3.5.2" "web-vitals": "^3.5.2"
}, },
"scripts": { "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'", "analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "vite", "start": "vite",
"build": "dotenvx run --env-file=.env.development.imex -- vite build", "build": "dotenvx run --env-file=.env.development.imex -- vite build",
"start:imex": "dotenvx run --env-file=.env.development.imex -- vite", "start:imex": "dotenvx run --env-file=.env.development.imex -- vite",
"start:rome": "dotenvx run --env-file=.env.development.rome -- vite", "start:rome": "dotenvx run --env-file=.env.development.rome -- vite",
"start:promanager": "dotenvx run --env-file=.env.development.promanager -- vite", "start:promanager": "dotenvx run --env-file=.env.development.promanager -- vite",
"preview:imex": "dotenvx run --env-file=.env.development.imex -- vite preview",
"preview:rome": "dotenvx run --env-file=.env.development.rome -- vite preview",
"preview:promanager": "dotenvx run --env-file=.env.development.promanager -- vite preview",
"build:test:imex": "env-cmd -f .env.test.imex npm run build", "build:test:imex": "env-cmd -f .env.test.imex npm run build",
"build:test:rome": "env-cmd -f .env.test.rome npm run build", "build:test:rome": "env-cmd -f .env.test.rome npm run build",
"build:test:promanager": "env-cmd -f .env.test.promanager npm run build", "build:test:promanager": "env-cmd -f .env.test.promanager npm run build",
@@ -134,30 +130,29 @@
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-react": "^7.24.7", "@babel/preset-react": "^7.24.7",
"@dotenvx/dotenvx": "^1.14.1", "@dotenvx/dotenvx": "^1.7.0",
"@emotion/babel-plugin": "^11.12.0", "@emotion/babel-plugin": "^11.12.0",
"@emotion/react": "^11.13.3", "@emotion/react": "^11.13.0",
"@sentry/webpack-plugin": "^2.22.4", "@sentry/webpack-plugin": "^2.22.2",
"@testing-library/cypress": "^10.0.2", "@testing-library/cypress": "^10.0.2",
"browserslist": "^4.23.3", "browserslist": "^4.23.3",
"browserslist-to-esbuild": "^2.1.1", "browserslist-to-esbuild": "^2.1.1",
"chalk": "^5.3.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"cypress": "^13.14.2", "cypress": "^13.13.3",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"eslint-plugin-cypress": "^2.15.1", "eslint-plugin-cypress": "^2.15.1",
"memfs": "^4.12.0", "memfs": "^4.11.1",
"os-browserify": "^0.3.0", "os-browserify": "^0.3.0",
"react-error-overlay": "6.0.11", "react-error-overlay": "6.0.11",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.3", "source-map-explorer": "^2.5.3",
"vite": "^5.4.7", "vite": "^5.4.1",
"vite-plugin-babel": "^1.2.0", "vite-plugin-babel": "^1.2.0",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-plugin-legacy": "^2.1.0",
"vite-plugin-node-polyfills": "^0.22.0", "vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-pwa": "^0.20.5", "vite-plugin-pwa": "^0.20.1",
"vite-plugin-style-import": "^2.0.0", "vite-plugin-style-import": "^2.0.0"
"workbox-window": "^7.1.0"
} }
} }

View File

@@ -2,6 +2,8 @@ import { ApolloProvider } from "@apollo/client";
import { SplitFactoryProvider, SplitSdk } from "@splitsoftware/splitio-react"; import { SplitFactoryProvider, SplitSdk } from "@splitsoftware/splitio-react";
import { ConfigProvider } from "antd"; import { ConfigProvider } from "antd";
import enLocale from "antd/es/locale/en_US"; import enLocale from "antd/es/locale/en_US";
import dayjs from "../utils/day";
import "dayjs/locale/en";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component"; import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
@@ -17,6 +19,8 @@ if (import.meta.env.DEV) {
Userpilot.initialize("NX-69145f08"); Userpilot.initialize("NX-69145f08");
} }
dayjs.locale("en");
const config = { const config = {
core: { core: {
authorizationKey: import.meta.env.VITE_APP_SPLIT_API, authorizationKey: import.meta.env.VITE_APP_SPLIT_API,

View File

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

View File

@@ -9,7 +9,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import { exportPageLimit } from "../../utils/config"; import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters"; import { alphaSort, dateSort } from "../../utils/sorters";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component"; import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component"; import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
@@ -175,7 +175,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
<Table <Table
loading={loading} loading={loading}
dataSource={dataSource} dataSource={dataSource}
pagination={{ position: "top", pageSize: exportPageLimit }} pagination={{ position: "top", pageSize: pageLimit }}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
onChange={handleTableChange} onChange={handleTableChange}

View File

@@ -8,7 +8,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter"; import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import { exportPageLimit } from "../../utils/config"; import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters"; import { alphaSort, dateSort } from "../../utils/sorters";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component"; import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
@@ -177,7 +177,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, payments,
<Table <Table
loading={loading} loading={loading}
dataSource={dataSource} dataSource={dataSource}
pagination={{ position: "top", pageSize: exportPageLimit }} pagination={{ position: "top", pageSize: pageLimit }}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
onChange={handleTableChange} onChange={handleTableChange}

View File

@@ -1,18 +1,18 @@
import { Button, Card, Input, Space, Table } from "antd"; import { Button, Card, Input, Space, Table } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { exportPageLimit } from "../../utils/config";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort, dateSort, statusSort } from "../../utils/sorters"; import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component"; import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component"; import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateFormatter } from "../../utils/DateFormatter";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import JobMarkSelectedExported from "../jobs-mark-selected-exported/jobs-mark-selected-exported"; import JobMarkSelectedExported from "../jobs-mark-selected-exported/jobs-mark-selected-exported";
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
@@ -201,7 +201,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
<Table <Table
loading={loading} loading={loading}
dataSource={dataSource} dataSource={dataSource}
pagination={{ position: "top", pageSize: exportPageLimit }} pagination={{ position: "top" }}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
onChange={handleTableChange} onChange={handleTableChange}

View File

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

View File

@@ -14,6 +14,7 @@ import dayjs from "../../utils/day";
import InstanceRenderManager from "../../utils/instanceRenderMgr"; import InstanceRenderManager from "../../utils/instanceRenderMgr";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component"; import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import JobSearchSelect from "../job-search-select/job-search-select.component"; import JobSearchSelect from "../job-search-select/job-search-select.component";
@@ -21,7 +22,6 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component"; import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import BillFormLines from "./bill-form.lines.component"; import BillFormLines from "./bill-form.lines.component";
import { CalculateBillTotal } from "./bill-form.totals.utility"; import { CalculateBillTotal } from "./bill-form.totals.utility";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
@@ -276,7 +276,7 @@ export function BillFormComponent({
}) })
]} ]}
> >
<DateTimePicker isDateOnly disabled={disabled} /> <FormDatePicker disabled={disabled} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("bills.fields.is_credit_memo")} label={t("bills.fields.is_credit_memo")}

View File

@@ -7,10 +7,10 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CiecaSelect from "../../utils/Ciecaselect"; import CiecaSelect from "../../utils/Ciecaselect";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import BillLineSearchSelect from "../bill-line-search-select/bill-line-search-select.component"; import BillLineSearchSelect from "../bill-line-search-select/bill-line-search-select.component";
import BilllineAddInventory from "../billline-add-inventory/billline-add-inventory.component"; import BilllineAddInventory from "../billline-add-inventory/billline-add-inventory.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
@@ -72,14 +72,7 @@ export function BillEnterModalLinesComponent({
<BillLineSearchSelect <BillLineSearchSelect
disabled={disabled} disabled={disabled}
options={lineData} options={lineData}
style={{ style={{ width: "100%", minWidth: "10rem" }}
width: "20rem",
maxWidth: "20rem",
minWidth: "10rem",
whiteSpace: "normal",
height: "auto",
minHeight: "32px" // default height of Ant Design inputs
}}
allowRemoved={form.getFieldValue("is_credit_memo") || false} allowRemoved={form.getFieldValue("is_credit_memo") || false}
onSelect={(value, opt) => { onSelect={(value, opt) => {
setFieldsValue({ setFieldsValue({
@@ -112,7 +105,7 @@ export function BillEnterModalLinesComponent({
title: t("billlines.fields.line_desc"), title: t("billlines.fields.line_desc"),
dataIndex: "line_desc", dataIndex: "line_desc",
editable: true, editable: true,
width: "20rem",
formItemProps: (field) => { formItemProps: (field) => {
return { return {
key: `${field.index}line_desc`, key: `${field.index}line_desc`,
@@ -126,7 +119,7 @@ export function BillEnterModalLinesComponent({
] ]
}; };
}, },
formInput: (record, index) => <Input.TextArea disabled={disabled} autoSize /> formInput: (record, index) => <Input disabled={disabled} />
}, },
{ {
title: t("billlines.fields.quantity"), title: t("billlines.fields.quantity"),

View File

@@ -11,7 +11,7 @@ const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps },
disabled={disabled} disabled={disabled}
ref={ref} ref={ref}
showSearch showSearch
popupMatchSelectWidth={true} popupMatchSelectWidth={false}
optionLabelProp={"name"} optionLabelProp={"name"}
// optionFilterProp="line_desc" // optionFilterProp="line_desc"
filterOption={(inputValue, option) => { filterOption={(inputValue, option) => {
@@ -43,7 +43,7 @@ const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps },
item.oem_partno ? ` - ${item.oem_partno}` : "" item.oem_partno ? ` - ${item.oem_partno}` : ""
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim(), }${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim(),
label: ( label: (
<div style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}> <>
<span> <span>
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${ {`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
item.oem_partno ? ` - ${item.oem_partno}` : "" item.oem_partno ? ` - ${item.oem_partno}` : ""
@@ -57,7 +57,7 @@ const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps },
<span style={{ float: "right", paddingleft: "1rem" }}> <span style={{ float: "right", paddingleft: "1rem" }}>
{item.act_price ? `$${item.act_price && item.act_price.toFixed(2)}` : ``} {item.act_price ? `$${item.act_price && item.act_price.toFixed(2)}` : ``}
</span> </span>
</div> </>
) )
})) }))
]} ]}

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 { 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 axios from "axios";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -14,12 +14,10 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component"; import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
import JobSearchSelectComponent from "../job-search-select/job-search-select.component"; import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
import { getCurrentUser } from "../../firebase/firebase.utils";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
cardPaymentModal: selectCardPayment, cardPaymentModal: selectCardPayment,
bodyshop: selectBodyshop, bodyshop: selectBodyshop
currentUser: getCurrentUser
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
@@ -27,17 +25,11 @@ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")) toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment"))
}); });
const CardPaymentModalComponent = ({ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisible, insertAuditTrail }) => {
bodyshop,
currentUser,
cardPaymentModal,
toggleModalVisible,
insertAuditTrail
}) => {
const { context, actions } = cardPaymentModal; const { context, actions } = cardPaymentModal;
const [form] = Form.useForm(); const [form] = Form.useForm();
const [paymentLink, setPaymentLink] = useState();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
// const [insertPayment] = useMutation(INSERT_NEW_PAYMENT); // const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE); const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
@@ -45,7 +37,7 @@ const CardPaymentModalComponent = ({
const [, { data, refetch, queryLoading }] = useLazyQuery(QUERY_RO_AND_OWNER_BY_JOB_PKS, { const [, { data, refetch, queryLoading }] = useLazyQuery(QUERY_RO_AND_OWNER_BY_JOB_PKS, {
variables: { jobids: [context.jobid] }, variables: { jobids: [context.jobid] },
skip: !context?.jobid skip: true
}); });
//Initialize the intellipay window. //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. //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. //Add a slight delay to allow the refetch to properly get the data.
setTimeout(() => { setTimeout(() => {
if (actions && actions.refetch && typeof actions.refetch === "function") actions.refetch(); if (actions && actions.refetch && typeof actions.refetch === "function")
actions.refetch();
setLoading(false); setLoading(false);
toggleModalVisible(); toggleModalVisible();
}, 750); }, 750);
@@ -93,6 +86,7 @@ const CardPaymentModalComponent = ({
}); });
}; };
const handleIntelliPayCharge = async () => { const handleIntelliPayCharge = async () => {
setLoading(true); setLoading(true);
//Validate //Validate
@@ -107,7 +101,7 @@ const CardPaymentModalComponent = ({
const response = await axios.post("/intellipay/lightbox_credentials", { const response = await axios.post("/intellipay/lightbox_credentials", {
bodyshop, bodyshop,
refresh: !!window.intellipay, refresh: !!window.intellipay,
paymentSplitMeta: form.getFieldsValue() paymentSplitMeta: form.getFieldsValue(),
}); });
if (window.intellipay) { 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 ( return (
<Card title="Card Payment"> <Card title="Card Payment">
<Spin spinning={loading}> <Spin spinning={loading}>
@@ -244,14 +202,16 @@ const CardPaymentModalComponent = ({
<Form.Item <Form.Item
shouldUpdate={(prevValues, curValues) => shouldUpdate={(prevValues, curValues) =>
prevValues.payments?.map((p) => p?.jobid + p?.amount).join() !== prevValues.payments?.map((p) => p?.jobid).join() !== curValues.payments?.map((p) => p?.jobid).join()
curValues.payments?.map((p) => p?.jobid + p?.amount).join()
} }
> >
{() => { {() => {
//If all of the job ids have been fileld in, then query and update the IP field. //If all of the job ids have been fileld in, then query and update the IP field.
const { payments } = form.getFieldsValue(); 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) }); refetch({ jobids: payments.map((p) => p.jobid) });
} }
return ( return (
@@ -286,6 +246,7 @@ const CardPaymentModalComponent = ({
const totalAmountToCharge = payments?.reduce((acc, val) => { const totalAmountToCharge = payments?.reduce((acc, val) => {
return acc + (val?.amount || 0); return acc + (val?.amount || 0);
}, 0); }, 0);
return ( return (
<Space style={{ float: "right" }}> <Space style={{ float: "right" }}>
<Statistic title="Amount To Charge" value={totalAmountToCharge} precision={2} /> <Statistic title="Amount To Charge" value={totalAmountToCharge} precision={2} />
@@ -312,36 +273,11 @@ const CardPaymentModalComponent = ({
> >
{t("job_payments.buttons.proceedtopayment")} {t("job_payments.buttons.proceedtopayment")}
</Button> </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> </Space>
); );
}} }}
</Form.Item> </Form.Item>
</Form> </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> </Spin>
</Card> </Card>
); );

View File

@@ -1,17 +1,15 @@
import { WarningFilled } from "@ant-design/icons"; import { WarningFilled } from "@ant-design/icons";
import { Form, Input, InputNumber, Space } from "antd"; import { Form, Input, InputNumber, Space } from "antd";
import dayjs from "../../utils/day";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import dayjs from "../../utils/day";
//import ContractLicenseDecodeButton from "../contract-license-decode-button/contract-license-decode-button.component"; //import ContractLicenseDecodeButton from "../contract-license-decode-button/contract-license-decode-button.component";
import ContractStatusSelector from "../contract-status-select/contract-status-select.component"; import ContractStatusSelector from "../contract-status-select/contract-status-select.component";
import ContractsRatesChangeButton from "../contracts-rates-change-button/contracts-rates-change-button.component"; import ContractsRatesChangeButton from "../contracts-rates-change-button/contracts-rates-change-button.component";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component"; import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
import { import FormDatePicker from "../form-date-picker/form-date-picker.component";
default as DateTimePicker, import FormDateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
default as FormDateTimePicker
} from "../form-date-time-picker/form-date-time-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import InputPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component"; import InputPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
@@ -20,10 +18,10 @@ import ContractFormJobPrefill from "./contract-form-job-prefill.component";
export default function ContractFormComponent({ form, create = false, selectedJobState, selectedCar }) { export default function ContractFormComponent({ form, create = false, selectedJobState, selectedCar }) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<> <div>
{!create && <FormFieldsChanged form={form} />} <FormFieldsChanged form={form} />
<LayoutFormRow> <LayoutFormRow>
{!create && ( {create ? null : (
<Form.Item <Form.Item
label={t("contracts.fields.status")} label={t("contracts.fields.status")}
name="status" name="status"
@@ -52,7 +50,7 @@ export default function ContractFormComponent({ form, create = false, selectedJo
<Form.Item label={t("contracts.fields.scheduledreturn")} name="scheduledreturn"> <Form.Item label={t("contracts.fields.scheduledreturn")} name="scheduledreturn">
<FormDateTimePicker /> <FormDateTimePicker />
</Form.Item> </Form.Item>
{!create && ( {create ? null : (
<Form.Item label={t("contracts.fields.actualreturn")} name="actualreturn"> <Form.Item label={t("contracts.fields.actualreturn")} name="actualreturn">
<FormDateTimePicker /> <FormDateTimePicker />
</Form.Item> </Form.Item>
@@ -124,7 +122,7 @@ export default function ContractFormComponent({ form, create = false, selectedJo
}} }}
</Form.Item> </Form.Item>
)} )}
{!create && ( {create ? null : (
<Form.Item label={t("contracts.fields.kmend")} name="kmend"> <Form.Item label={t("contracts.fields.kmend")} name="kmend">
<InputNumber /> <InputNumber />
</Form.Item> </Form.Item>
@@ -147,21 +145,25 @@ export default function ContractFormComponent({ form, create = false, selectedJo
> >
<CourtesyCarFuelSlider /> <CourtesyCarFuelSlider />
</Form.Item> </Form.Item>
{!create && ( {create ? null : (
<Form.Item label={t("contracts.fields.fuelin")} name="fuelin" span={8}> <Form.Item label={t("contracts.fields.fuelin")} name="fuelin" span={8}>
<CourtesyCarFuelSlider /> <CourtesyCarFuelSlider />
</Form.Item> </Form.Item>
)} )}
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow header={t("contracts.labels.driverinformation")}> <div>
<Space wrap> <Space wrap>
{create && selectedJobState && ( {selectedJobState && (
<ContractFormJobPrefill jobId={selectedJobState && selectedJobState[0]} form={form} /> <div>
<ContractFormJobPrefill jobId={selectedJobState && selectedJobState[0]} form={form} />
</div>
)} )}
{/* {<ContractLicenseDecodeButton form={form} />} */} {
//<ContractLicenseDecodeButton form={form} />
}
</Space> </Space>
</LayoutFormRow> </div>
<LayoutFormRow noDivider={true}> <LayoutFormRow header={t("contracts.labels.driverinformation")}>
<Form.Item <Form.Item
label={t("contracts.fields.driver_dlnumber")} label={t("contracts.fields.driver_dlnumber")}
name="driver_dlnumber" name="driver_dlnumber"
@@ -181,8 +183,9 @@ export default function ContractFormComponent({ form, create = false, selectedJo
const dlExpiresBeforeReturn = dayjs(form.getFieldValue("driver_dlexpiry")).isBefore( const dlExpiresBeforeReturn = dayjs(form.getFieldValue("driver_dlexpiry")).isBefore(
dayjs(form.getFieldValue("scheduledreturn")) dayjs(form.getFieldValue("scheduledreturn"))
); );
return ( return (
<> <div>
<Form.Item <Form.Item
label={t("contracts.fields.driver_dlexpiry")} label={t("contracts.fields.driver_dlexpiry")}
name="driver_dlexpiry" name="driver_dlexpiry"
@@ -193,7 +196,7 @@ export default function ContractFormComponent({ form, create = false, selectedJo
} }
]} ]}
> >
<DateTimePicker isDateOnly /> <FormDatePicker />
</Form.Item> </Form.Item>
{dlExpiresBeforeReturn && ( {dlExpiresBeforeReturn && (
<Space style={{ color: "tomato" }}> <Space style={{ color: "tomato" }}>
@@ -201,10 +204,11 @@ export default function ContractFormComponent({ form, create = false, selectedJo
<span>{t("contracts.labels.dlexpirebeforereturn")}</span> <span>{t("contracts.labels.dlexpirebeforereturn")}</span>
</Space> </Space>
)} )}
</> </div>
); );
}} }}
</Form.Item> </Form.Item>
<Form.Item label={t("contracts.fields.driver_dlst")} name="driver_dlst"> <Form.Item label={t("contracts.fields.driver_dlst")} name="driver_dlst">
<Input /> <Input />
</Form.Item> </Form.Item>
@@ -270,7 +274,7 @@ export default function ContractFormComponent({ form, create = false, selectedJo
<InputPhone /> <InputPhone />
</Form.Item> </Form.Item>
<Form.Item label={t("contracts.fields.driver_dob")} name="driver_dob"> <Form.Item label={t("contracts.fields.driver_dob")} name="driver_dob">
<DateTimePicker isDateOnly /> <FormDatePicker />
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
<ContractsRatesChangeButton form={form} /> <ContractsRatesChangeButton form={form} />
@@ -311,6 +315,6 @@ export default function ContractFormComponent({ form, create = false, selectedJo
<InputNumber precision={2} /> <InputNumber precision={2} />
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
</> </div>
); );
} }

View File

@@ -10,12 +10,16 @@ import { DateFormatter } from "../../utils/DateFormatter";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component"; import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
import CourtesyCarReadiness from "../courtesy-car-readiness-select/courtesy-car-readiness-select.component"; import CourtesyCarReadiness from "../courtesy-car-readiness-select/courtesy-car-readiness-select.component";
import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component"; import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
export default function CourtesyCarCreateFormComponent({ form, saveLoading, newCC }) { export default function CourtesyCarCreateFormComponent({
form,
saveLoading,
newCC,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const client = useApolloClient(); const client = useApolloClient();
@@ -157,16 +161,16 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading, newC
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item label={t("courtesycars.fields.purchasedate")} name="purchasedate"> <Form.Item label={t("courtesycars.fields.purchasedate")} name="purchasedate">
<DateTimePicker isDateOnly /> <FormDatePicker />
</Form.Item> </Form.Item>
<Form.Item label={t("courtesycars.fields.servicestartdate")} name="servicestartdate"> <Form.Item label={t("courtesycars.fields.servicestartdate")} name="servicestartdate">
<DateTimePicker isDateOnly /> <FormDatePicker />
</Form.Item> </Form.Item>
<Form.Item label={t("courtesycars.fields.serviceenddate")} name="serviceenddate"> <Form.Item label={t("courtesycars.fields.serviceenddate")} name="serviceenddate">
<DateTimePicker isDateOnly /> <FormDatePicker />
</Form.Item> </Form.Item>
<Form.Item label={t("courtesycars.fields.leaseenddate")} name="leaseenddate"> <Form.Item label={t("courtesycars.fields.leaseenddate")} name="leaseenddate">
<DateTimePicker isDateOnly /> <FormDatePicker />
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
@@ -224,7 +228,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading, newC
</div> </div>
<div> <div>
<Form.Item label={t("courtesycars.fields.nextservicedate")} name="nextservicedate"> <Form.Item label={t("courtesycars.fields.nextservicedate")} name="nextservicedate">
<DateTimePicker isDateOnly /> <FormDatePicker />
</Form.Item> </Form.Item>
<Form.Item shouldUpdate={(p, c) => p.nextservicedate !== c.nextservicedate}> <Form.Item shouldUpdate={(p, c) => p.nextservicedate !== c.nextservicedate}>
{() => { {() => {
@@ -256,7 +260,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading, newC
</Form.Item> </Form.Item>
<div> <div>
<Form.Item label={t("courtesycars.fields.registrationexpires")} name="registrationexpires"> <Form.Item label={t("courtesycars.fields.registrationexpires")} name="registrationexpires">
<DateTimePicker isDateOnly /> <FormDatePicker />
</Form.Item> </Form.Item>
<Form.Item shouldUpdate={(p, c) => p.registrationexpires !== c.registrationexpires}> <Form.Item shouldUpdate={(p, c) => p.registrationexpires !== c.registrationexpires}>
{() => { {() => {
@@ -289,7 +293,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading, newC
} }
]} ]}
> >
<DateTimePicker isDateOnly /> <FormDatePicker />
</Form.Item> </Form.Item>
<Form.Item shouldUpdate={(p, c) => p.insuranceexpires !== c.insuranceexpires}> <Form.Item shouldUpdate={(p, c) => p.insuranceexpires !== c.insuranceexpires}>
{() => { {() => {

View File

@@ -2,7 +2,7 @@ import { Form, InputNumber } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component"; import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx"; import FormDatePicker from "../form-date-picker/form-date-picker.component";
export default function CourtesyCarReturnModalComponent() { export default function CourtesyCarReturnModalComponent() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -19,7 +19,7 @@ export default function CourtesyCarReturnModalComponent() {
} }
]} ]}
> >
<DateTimePicker isDateOnly /> <FormDatePicker />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("contracts.fields.kmend")} label={t("contracts.fields.kmend")}

View File

@@ -40,6 +40,8 @@ export function DmsLogEvents({ socket, logs, bodyshop }) {
function LogLevelHierarchy(level) { function LogLevelHierarchy(level) {
switch (level) { switch (level) {
case "TRACE":
return "pink";
case "DEBUG": case "DEBUG":
return "orange"; return "orange";
case "INFO": case "INFO":

View File

@@ -24,9 +24,9 @@ import i18n from "../../translations/i18n";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component"; import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component";
import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component"; import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
@@ -164,7 +164,7 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
<Input disabled /> <Input disabled />
</Form.Item> </Form.Item>
<Form.Item name="inservicedate" label={t("jobs.fields.dms.inservicedate")}> <Form.Item name="inservicedate" label={t("jobs.fields.dms.inservicedate")}>
<DateTimePicker isDateOnly /> <FormDatePicker />
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
<Space> <Space>

View File

@@ -4,14 +4,14 @@ import Markdown from "react-markdown";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectCurrentEula, selectCurrentUser } from "../../redux/user/user.selectors"; import { selectCurrentEula, selectCurrentUser } from "../../redux/user/user.selectors";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { FormDatePicker } from "../form-date-picker/form-date-picker.component";
import { INSERT_EULA_ACCEPTANCE } from "../../graphql/user.queries"; import { INSERT_EULA_ACCEPTANCE } from "../../graphql/user.queries";
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { acceptEula } from "../../redux/user/user.actions"; import { acceptEula } from "../../redux/user/user.actions";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import dayjs from "../../utils/day"; import day from "../../utils/day";
import "./eula.styles.scss"; import "./eula.styles.scss";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const Eula = ({ currentEula, currentUser, acceptEula }) => { const Eula = ({ currentEula, currentUser, acceptEula }) => {
const [formReady, setFormReady] = useState(false); const [formReady, setFormReady] = useState(false);
@@ -208,7 +208,7 @@ const EulaFormComponent = ({ form, handleChange, onFinish, t }) => (
{ {
required: true, required: true,
validator: (_, value) => { validator: (_, value) => {
if (dayjs(value).isSame(dayjs(), "day")) { if (day(value).isSame(day(), "day")) {
return Promise.resolve(); return Promise.resolve();
} }
return Promise.reject(new Error(t("eula.messages.date_accepted"))); return Promise.reject(new Error(t("eula.messages.date_accepted")));
@@ -216,7 +216,7 @@ const EulaFormComponent = ({ form, handleChange, onFinish, t }) => (
} }
]} ]}
> >
<DateTimePicker isDateOnly onChange={handleChange} onlyToday aria-label={t("eula.labels.date_accepted")} /> <FormDatePicker onChange={handleChange} onlyToday aria-label={t("eula.labels.date_accepted")} />
</Form.Item> </Form.Item>
</Col> </Col>
</Row> </Row>

View File

@@ -0,0 +1,123 @@
import { DatePicker } from "antd";
import dayjs from "../../utils/day";
import React, { useRef } from "react";
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)(FormDatePicker);
const dateFormat = "MM/DD/YYYY";
export function FormDatePicker({
bodyshop,
value,
onChange,
onBlur,
onlyFuture,
onlyToday,
isDateOnly = true,
...restProps
}) {
const ref = useRef();
const handleChange = (newDate) => {
if (value !== newDate && onChange) {
onChange(isDateOnly ? newDate && newDate.format("YYYY-MM-DD") : newDate);
}
};
const handleKeyDown = (e) => {
if (e.key.toLowerCase() === "t") {
if (onChange) {
onChange(isDateOnly ? dayjs().format("YYYY-MM-DD") : dayjs());
}
} else if (e.key.toLowerCase() === "enter") {
if (ref.current && ref.current.blur) ref.current.blur();
}
};
const handleBlur = (e) => {
const v = e.target.value;
if (!v) return;
const formats = [
"MMDDYY",
"MMDDYYYY",
"MM/DD/YY",
"MM/DD/YYYY",
"M/DD/YY",
"M/DD/YYYY",
"MM/D/YY",
"MM/D/YYYY",
"M/D/YY",
"M/D/YYYY",
"D/MM/YY",
"D/MM/YYYY",
"DD/M/YY",
"DD/M/YYYY",
"D/M/YY",
"D/M/YYYY"
];
let _a;
// Iterate through formats to find the correct one
for (let format of formats) {
_a = dayjs(v, format);
if (v === _a.format(format)) {
break;
}
}
if (_a.isValid() && value && value.isValid && value.isValid()) {
_a.set({
hours: value.hours(),
minutes: value.minutes(),
seconds: value.seconds(),
milliseconds: value.milliseconds()
});
}
if (_a.isValid() && onChange) {
if (onlyFuture) {
if (dayjs().subtract(1, "day").isBefore(_a)) {
onChange(isDateOnly ? _a.format("YYYY-MM-DD") : _a);
} else {
onChange(isDateOnly ? dayjs().format("YYYY-MM-DD") : dayjs());
}
} else {
onChange(isDateOnly ? _a.format("YYYY-MM-DD") : _a);
}
}
};
return (
<div onKeyDown={handleKeyDown}>
<DatePicker
ref={ref}
value={value ? dayjs(value) : null}
onChange={handleChange}
format={dateFormat}
onBlur={onBlur || handleBlur}
showToday={false}
disabledTime
disabledDate={(d) => {
if (onlyToday) {
return !dayjs().isSame(d, "day");
} else if (onlyFuture) {
return dayjs().subtract(1, "day").isAfter(d);
}
}}
{...restProps}
/>
</div>
);
}

View File

@@ -0,0 +1,48 @@
import { DatePicker } from "antd";
import dayjs from "../../utils/day.js";
import React, { useRef } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(FormDateTimePickerEnhanced);
const dateFormat = "MM/DD/YYYY h:mm a";
export function FormDateTimePickerEnhanced({
bodyshop,
value,
onBlur,
onlyFuture,
onlyToday,
isDateOnly = true,
...restProps
}) {
const ref = useRef();
return (
<div>
<DatePicker
ref={ref}
value={value ? dayjs(value) : null}
format={dateFormat}
onBlur={onBlur}
showToday={false}
disabledDate={(d) => {
if (onlyToday) {
return !dayjs().isSame(d, "day");
} else if (onlyFuture) {
return dayjs().subtract(1, "day").isAfter(d);
}
}}
{...restProps}
/>
</div>
);
}

View File

@@ -1,123 +1,46 @@
import { DatePicker } from "antd"; import React, { forwardRef } from "react";
import PropTypes from "prop-types"; //import DatePicker from "react-datepicker";
import React, { useCallback, useState } from "react"; //import "react-datepicker/src/stylesheets/datepicker.scss";
import { useTranslation } from "react-i18next"; import { Space, TimePicker } from "antd";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import { fuzzyMatchDate } from "./formats.js"; import FormDatePicker from "../form-date-picker/form-date-picker.component";
import { createStructuredSelector } from "reselect"; //To be used as a form element only.
import { selectBodyshop } from "../../redux/user/user.selectors.js";
import { connect } from "react-redux";
const mapStateToProps = createStructuredSelector({ const DateTimePicker = ({ value, onChange, onBlur, id, onlyFuture, ...restProps }, ref) => {
bodyshop: selectBodyshop // const handleChange = (newDate) => {
}); // if (value !== newDate && onChange) {
// onChange(newDate);
const DateTimePicker = ({ // }
value, // };
onChange,
onBlur,
id,
onlyFuture,
onlyToday,
isDateOnly = false,
bodyshop,
...restProps
}) => {
const [isManualInput, setIsManualInput] = useState(false);
const { t } = useTranslation();
const handleChange = useCallback(
(newDate) => {
if (!newDate) return;
if (onChange) {
onChange(bodyshop?.timezone ? dayjs(newDate).tz(bodyshop.timezone, true) : newDate);
}
setIsManualInput(false);
},
[onChange, bodyshop?.timezone]
);
const handleBlur = useCallback(
(e) => {
// Bail if this is not a manual input
if (!isManualInput) {
return;
}
// Reset manual input flag
setIsManualInput(false);
const v = e?.target?.value;
if (!v) return;
let parsedDate = isDateOnly ? fuzzyMatchDate(v)?.startOf("day") : fuzzyMatchDate(v);
if (parsedDate && onChange) {
onChange(parsedDate);
}
},
[isManualInput, isDateOnly, onChange]
);
const handleKeyDown = useCallback(
(e) => {
setIsManualInput(true);
if (e.key.toLowerCase() === "t" && onChange) {
e.preventDefault();
setIsManualInput(false);
onChange(dayjs());
} else if (e.key.toLowerCase() === "enter") {
handleBlur(e);
}
},
[onChange, handleBlur]
);
const handleDisabledDate = useCallback(
(current) => {
if (onlyToday) {
return !dayjs().isSame(current, "day");
} else if (onlyFuture) {
return dayjs().subtract(1, "day").isAfter(current);
}
return false;
},
[onlyToday, onlyFuture]
);
return ( return (
<div onKeyDown={handleKeyDown} id={id} style={{ width: "100%" }}> <Space direction="vertical" style={{ width: "100%" }} id={id}>
<DatePicker <FormDatePicker
showTime={ {...restProps}
isDateOnly {...(onlyFuture && {
? false disabledDate: (d) => dayjs().subtract(1, "day").isAfter(d)
: { })}
format: "hh:mm a", value={value}
minuteStep: 15, onBlur={onBlur}
defaultValue: dayjs(dayjs(), "HH:mm:ss") onChange={onChange}
} onlyFuture={onlyFuture}
} isDateOnly={false}
format={isDateOnly ? "MM/DD/YYYY" : "MM/DD/YYYY hh:mm a"} />
<TimePicker
value={value ? dayjs(value) : null} value={value ? dayjs(value) : null}
onChange={handleChange} {...(onlyFuture && {
placeholder={isDateOnly ? t("general.labels.date") : t("general.labels.datetime")} disabledDate: (d) => dayjs().isAfter(d)
onBlur={onBlur || handleBlur} })}
disabledDate={handleDisabledDate} onChange={onChange}
disableSeconds={true}
minuteStep={15}
onBlur={onBlur}
format="hh:mm a"
{...restProps} {...restProps}
/> />
</div> </Space>
); );
}; };
DateTimePicker.propTypes = { export default forwardRef(DateTimePicker);
value: PropTypes.any,
onChange: PropTypes.func,
onBlur: PropTypes.func,
id: PropTypes.string,
onlyFuture: PropTypes.bool,
onlyToday: PropTypes.bool,
isDateOnly: PropTypes.bool
};
export default connect(mapStateToProps, null)(DateTimePicker);

View File

@@ -1,63 +0,0 @@
import dayjs from "../../utils/day";
const dateFormats = [
"MMDDYYYY",
"MMDDYY",
"M/D/YYYY",
"MM/D/YYYY",
"M/DD/YYYY",
"MM/DD/YYYY",
"M/D/YY",
"MM/D/YY",
"M/DD/YY",
"MM/DD/YY"
];
const timeFormats = ["h:mm A", "h:mmA", "h A", "hA", "hh:mm A", "hh:mm:ss A"];
const dateTimeFormats = [
...["M/D/YYYY", "MM/D/YYYY", "M/DD/YYYY", "MM/DD/YYYY", "M/D/YY", "MM/D/YY", "M/DD/YY", "MM/DD/YY"].flatMap(
(dateFormat) => timeFormats.map((timeFormat) => `${dateFormat} ${timeFormat}`)
),
...["MMDDYYYY", "MMDDYY"].flatMap((dateFormat) => timeFormats.map((timeFormat) => `${dateFormat} ${timeFormat}`)),
"M/D/YYYY",
"MM/D/YYYY",
"M/DD/YYYY",
"MM/DD/YYYY",
"M/D/YY",
"MM/D/YY",
"M/DD/YY",
"MM/DD/YY",
"MMDDYYYY",
"MMDDYY"
];
const sanitizeInput = (input) =>
input
.trim()
.toUpperCase()
.replace(/\s*(am|pm)\s*/i, " $1")
.replaceAll(".", "/")
.replaceAll("-", "/");
export const fuzzyMatchDate = (dateString) => {
const sanitizedInput = sanitizeInput(dateString);
for (const format of dateFormats) {
const parsedDate = dayjs(sanitizedInput, format, true);
if (parsedDate.isValid()) {
return parsedDate;
}
}
for (const format of dateTimeFormats) {
const parsedDateTime = dayjs(sanitizedInput, format, true);
if (parsedDateTime.isValid()) {
return parsedDateTime; // Return the dayjs object
}
}
return null; // If no matching format is found
};

View File

@@ -3,15 +3,13 @@ import axios from "axios";
import _ from "lodash"; import _ from "lodash";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom"; import { Link } from "react-router-dom";
import PhoneNumberFormatter from "../../utils/PhoneFormatter"; import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component"; import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
export default function GlobalSearchOs() { export default function GlobalSearchOs() {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [data, setData] = useState(false); const [data, setData] = useState(false);
@@ -179,18 +177,7 @@ export default function GlobalSearchOs() {
}; };
return ( return (
<AutoComplete <AutoComplete options={data} onSearch={handleSearch} defaultActiveFirstOption onClear={() => setData([])}>
options={data}
onSearch={handleSearch}
onKeyDown={(e) => {
if (e.key !== "Enter") return;
const firstUrlForSearch = data?.[0]?.options?.[0]?.label?.props?.to;
if (!firstUrlForSearch) return;
navigate(firstUrlForSearch);
}}
defaultActiveFirstOption
onClear={() => setData([])}
>
<Input.Search <Input.Search
size="large" size="large"
placeholder={t("general.labels.globalsearch")} placeholder={t("general.labels.globalsearch")}

View File

@@ -3,7 +3,7 @@ import { AutoComplete, Divider, Input, Space } from "antd";
import _ from "lodash"; import _ from "lodash";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom"; import { Link } from "react-router-dom";
import { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries"; import { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries";
import PhoneNumberFormatter from "../../utils/PhoneFormatter"; import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
@@ -13,7 +13,6 @@ import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.compon
export default function GlobalSearch() { export default function GlobalSearch() {
const { t } = useTranslation(); const { t } = useTranslation();
const [callSearch, { loading, error, data }] = useLazyQuery(GLOBAL_SEARCH_QUERY); const [callSearch, { loading, error, data }] = useLazyQuery(GLOBAL_SEARCH_QUERY);
const navigate = useNavigate();
const executeSearch = (v) => { const executeSearch = (v) => {
if (v && v.variables.search && v.variables.search !== "" && v.variables.search.length >= 3) callSearch(v); if (v && v.variables.search && v.variables.search !== "" && v.variables.search.length >= 3) callSearch(v);
@@ -21,6 +20,7 @@ export default function GlobalSearch() {
const debouncedExecuteSearch = _.debounce(executeSearch, 750); const debouncedExecuteSearch = _.debounce(executeSearch, 750);
const handleSearch = (value) => { const handleSearch = (value) => {
console.log("Handle Search");
debouncedExecuteSearch({ variables: { search: value } }); debouncedExecuteSearch({ variables: { search: value } });
}; };
@@ -156,17 +156,7 @@ export default function GlobalSearch() {
if (error) return <AlertComponent message={error.message} type="error" />; if (error) return <AlertComponent message={error.message} type="error" />;
return ( return (
<AutoComplete <AutoComplete options={options} onSearch={handleSearch} defaultActiveFirstOption>
options={options}
onSearch={handleSearch}
defaultActiveFirstOption
onKeyDown={(e) => {
if (e.key !== "Enter") return;
const firstUrlForSearch = options?.[0]?.options?.[0]?.label?.props?.to;
if (!firstUrlForSearch) return;
navigate(firstUrlForSearch);
}}
>
<Input.Search <Input.Search
size="large" size="large"
placeholder={t("general.labels.globalsearch")} placeholder={t("general.labels.globalsearch")}

View File

@@ -13,6 +13,7 @@ import Icon, {
FileFilled, FileFilled,
HomeFilled, HomeFilled,
ImportOutlined, ImportOutlined,
InfoCircleOutlined,
LineChartOutlined, LineChartOutlined,
PaperClipOutlined, PaperClipOutlined,
PhoneOutlined, PhoneOutlined,
@@ -26,8 +27,8 @@ import Icon, {
UserOutlined UserOutlined
} from "@ant-design/icons"; } from "@ant-design/icons";
import { useSplitTreatments } from "@splitsoftware/splitio-react"; import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Layout, Menu } from "antd"; import { Layout, Menu, Switch, Tooltip } from "antd";
import React from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { BsKanban } from "react-icons/bs"; import { BsKanban } from "react-icons/bs";
import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar, FaTasks } from "react-icons/fa"; import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar, FaTasks } from "react-icons/fa";
@@ -42,6 +43,7 @@ import { selectRecentItems, selectSelectedHeader } from "../../redux/application
import { setModalContext } from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
import { signOutStart } from "../../redux/user/user.actions"; import { signOutStart } from "../../redux/user/user.actions";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import { checkBeta, handleBeta, setBeta } from "../../utils/handleBeta";
import InstanceRenderManager from "../../utils/instanceRenderMgr"; import InstanceRenderManager from "../../utils/instanceRenderMgr";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component"; import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
@@ -113,18 +115,19 @@ function Header({
names: ["ImEXPay", "DmsAp", "Simple_Inventory"], names: ["ImEXPay", "DmsAp", "Simple_Inventory"],
splitKey: bodyshop && bodyshop.imexshopid splitKey: bodyshop && bodyshop.imexshopid
}); });
const [betaSwitch, setBetaSwitch] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
// const deleteBetaCookie = () => { useEffect(() => {
// const cookieExists = document.cookie.split("; ").some((row) => row.startsWith(`betaSwitchImex=`)); const isBeta = checkBeta();
// if (cookieExists) { setBetaSwitch(isBeta);
// const domain = window.location.hostname.split(".").slice(-2).join("."); }, []);
// document.cookie = `betaSwitchImex=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${domain}`;
// } const betaSwitchChange = (checked) => {
// }; setBeta(checked);
// setBetaSwitch(checked);
// deleteBetaCookie(); handleBeta();
};
const accountingChildren = []; const accountingChildren = [];
@@ -692,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 ( return (
<Layout.Header> <Layout.Header>
<Menu <Menu

View File

@@ -23,7 +23,6 @@ import ScheduleEventColor from "./schedule-event.color.component";
import ScheduleEventNote from "./schedule-event.note.component"; import ScheduleEventNote from "./schedule-event.note.component";
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries"; import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
@@ -128,9 +127,6 @@ export function ScheduleEventComponent({
{(event.job && event.job.alt_transport) || ""} {(event.job && event.job.alt_transport) || ""}
<ScheduleAtChange job={event && event.job} /> <ScheduleAtChange job={event && event.job} />
</DataLabel> </DataLabel>
<DataLabel label={t("jobs.fields.comment")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}>
<ProductionListColumnComment record={event && event.job} />
</DataLabel>
<ScheduleEventNote event={event} /> <ScheduleEventNote event={event} />
</div> </div>
) : ( ) : (
@@ -320,7 +316,6 @@ export function ScheduleEventComponent({
})`} })`}
{event.job && event.job.alt_transport && <div style={{ margin: ".1rem" }}>{event.job.alt_transport}</div>} {event.job && event.job.alt_transport && <div style={{ margin: ".1rem" }}>{event.job.alt_transport}</div>}
{event?.job?.comment && `C: ${event.job.comment}`}
</Space> </Space>
) : ( ) : (
<div <div

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import dayjs from "../../utils/day"; import day from "../../utils/day";
import axios from "axios"; import axios from "axios";
import { Badge, Card, Space, Table, Tag } from "antd"; import { Badge, Card, Space, Table, Tag } from "antd";
import { gql, useQuery } from "@apollo/client"; import { gql, useQuery } from "@apollo/client";
@@ -72,7 +72,7 @@ export function JobLifecycleComponent({ job, statuses, ...rest }) {
dataIndex: "start", dataIndex: "start",
key: "start", key: "start",
render: (text) => DateTimeFormatterFunction(text), render: (text) => DateTimeFormatterFunction(text),
sorter: (a, b) => dayjs(a.start).unix() - dayjs(b.start).unix() sorter: (a, b) => day(a.start).unix() - day(b.start).unix()
}, },
{ {
title: t("job_lifecycle.columns.relative_start"), title: t("job_lifecycle.columns.relative_start"),
@@ -90,7 +90,7 @@ export function JobLifecycleComponent({ job, statuses, ...rest }) {
} }
return isEmpty(a.end) ? 1 : -1; return isEmpty(a.end) ? 1 : -1;
} }
return dayjs(a.end).unix() - dayjs(b.end).unix(); return day(a.end).unix() - day(b.end).unix();
}, },
render: (text) => (isEmpty(text) ? t("job_lifecycle.content.not_available") : DateTimeFormatterFunction(text)) render: (text) => (isEmpty(text) ? t("job_lifecycle.content.not_available") : DateTimeFormatterFunction(text))
}, },

View File

@@ -2,7 +2,7 @@ import React, { useState } from "react";
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { Button, Form, notification, Popover, Select, Space } from "antd"; import { Button, Form, notification, Popover, Select, Space } from "antd";
import dayjs from "../../utils/day"; import day from "../../utils/day";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
@@ -48,7 +48,7 @@ export function JobLineDispatchButton({
const result = await dispatchLines({ const result = await dispatchLines({
variables: { variables: {
partsDispatch: { partsDispatch: {
dispatched_at: dayjs(), dispatched_at: day(),
employeeid: values.employeeid, employeeid: values.employeeid,
jobid: job.id, jobid: job.id,
dispatched_by: currentUser.email, dispatched_by: currentUser.email,
@@ -138,11 +138,7 @@ export function JobLineDispatchButton({
return ( return (
<Popover open={visible} content={popMenu}> <Popover open={visible} content={popMenu}>
<Button <Button disabled={selectedLines.length === 0 || jobRO || disabled} loading={loading} onClick={() => setVisible(true)}>
disabled={selectedLines.length === 0 || jobRO || disabled}
loading={loading}
onClick={() => setVisible(true)}
>
{t("joblines.actions.dispatchparts", { count: selectedLines.length })} {t("joblines.actions.dispatchparts", { count: selectedLines.length })}
</Button> </Button>
</Popover> </Popover>

View File

@@ -1,8 +1,5 @@
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { notification } from "antd"; import { notification } from "antd";
import Axios from "axios";
import Dinero from "dinero.js";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -10,10 +7,13 @@ import { createStructuredSelector } from "reselect";
import { INSERT_NEW_JOB_LINE, UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries"; import { INSERT_NEW_JOB_LINE, UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
import { toggleModalVisible } from "../../redux/modals/modals.actions"; import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectJobLineEditModal } from "../../redux/modals/modals.selectors"; import { selectJobLineEditModal } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CriticalPartsScan from "../../utils/criticalPartsScan";
import UndefinedToNull from "../../utils/undefinedtonull"; import UndefinedToNull from "../../utils/undefinedtonull";
import JobLinesUpdsertModal from "./job-lines-upsert-modal.component"; import JobLinesUpdsertModal from "./job-lines-upsert-modal.component";
import Axios from "axios";
import Dinero from "dinero.js";
import CriticalPartsScan from "../../utils/criticalPartsScan";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
jobLineEditModal: selectJobLineEditModal, jobLineEditModal: selectJobLineEditModal,
@@ -82,15 +82,13 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo
variables: { variables: {
lineId: jobLineEditModal.context.id, lineId: jobLineEditModal.context.id,
line: { line: {
...UndefinedToNull({ ...values,
...values, prt_dsmk_m: Dinero({
prt_dsmk_m: Dinero({ amount: Math.round(values.act_price * 100)
amount: Math.round(values.act_price * 100)
})
.percentage(Math.abs(values.prt_dsmk_p || 0))
.multiply(values.prt_dsmk_p >= 0 ? 1 : -1)
.toFormat(0.0)
}) })
.percentage(Math.abs(values.prt_dsmk_p || 0))
.multiply(values.prt_dsmk_p >= 0 ? 1 : -1)
.toFormat(0.0)
} }
}, },
refetchQueries: ["GET_LINE_TICKET_BY_PK"] refetchQueries: ["GET_LINE_TICKET_BY_PK"]

View File

@@ -10,8 +10,8 @@ import {
QUERY_SCOREBOARD_ENTRY, QUERY_SCOREBOARD_ENTRY,
UPDATE_SCOREBOARD_ENTRY UPDATE_SCOREBOARD_ENTRY
} from "../../graphql/scoreboard.queries"; } from "../../graphql/scoreboard.queries";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
export default function ScoreboardAddButton({ job, disabled, ...otherBtnProps }) { export default function ScoreboardAddButton({ job, disabled, ...otherBtnProps }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -86,7 +86,7 @@ export default function ScoreboardAddButton({ job, disabled, ...otherBtnProps })
} }
]} ]}
> >
<DateTimePicker isDateOnly /> <FormDatePicker />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("scoreboard.fields.bodyhrs")} label={t("scoreboard.fields.bodyhrs")}

View File

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

View File

@@ -5,6 +5,7 @@ import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { UPDATE_JOB } from "../../graphql/jobs.queries"; import { UPDATE_JOB } from "../../graphql/jobs.queries";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component"; import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
@@ -19,14 +20,7 @@ const mapStateToProps = createStructuredSelector({
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
dispatch(
insertAuditTrail({
jobid,
operation,
type
})
)
}); });
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminDatesChange); export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminDatesChange);
@@ -93,7 +87,7 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
<FormFieldsChanged form={form} /> <FormFieldsChanged form={form} />
<LayoutFormRow header={t("jobs.forms.estdates")}> <LayoutFormRow header={t("jobs.forms.estdates")}>
<Form.Item label={t("jobs.fields.date_estimated")} name="date_estimated"> <Form.Item label={t("jobs.fields.date_estimated")} name="date_estimated">
<DateTimePicker format="MM/DD/YYYY" isDateOnly /> <FormDatePicker format="MM/DD/YYYY" />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.date_towin")} name="date_towin"> <Form.Item label={t("jobs.fields.date_towin")} name="date_towin">
<DateTimePicker /> <DateTimePicker />

View File

@@ -1,9 +1,18 @@
import { Collapse, Form, Input, InputNumber, Select, Space, Switch } from "antd"; import {
Collapse,
Form,
Input,
InputNumber,
Select,
Space,
Switch,
} from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component"; import FormItemEmail from "../form-items-formatted/email-form-item.component";
import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component"; import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
@@ -20,7 +29,6 @@ import JobsDetailRatesTaxes from "../jobs-detail-rates/jobs-detail-rates.taxes.c
import JobsMarkPstExempt from "../jobs-mark-pst-exempt/jobs-mark-pst-exempt.component"; import JobsMarkPstExempt from "../jobs-mark-pst-exempt/jobs-mark-pst-exempt.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr"; import InstanceRenderManager from "../../utils/instanceRenderMgr";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
@@ -53,7 +61,10 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
<Form.Item label={t("jobs.fields.policy_no")} name="policy_no"> <Form.Item label={t("jobs.fields.policy_no")} name="policy_no">
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.regie_number")} name="regie_number"> <Form.Item
label={t("jobs.fields.regie_number")}
name="regie_number"
>
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm"> <Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm">
@@ -105,7 +116,7 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
<FormItemEmail email={getFieldValue("ins_ea")} /> <FormItemEmail email={getFieldValue("ins_ea")} />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.loss_date")} name="loss_date"> <Form.Item label={t("jobs.fields.loss_date")} name="loss_date">
<DateTimePicker isDateOnly /> <FormDatePicker />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.kmin")} name="kmin"> <Form.Item label={t("jobs.fields.kmin")} name="kmin">
<Input /> <Input />

View File

@@ -2,9 +2,9 @@ import { Form, Input } from "antd";
import React, { useContext } from "react"; import React, { useContext } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import JobCreateContext from "../../pages/jobs-create/jobs-create.context"; import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import JobsCreateVehicleInfoPredefined from "./jobs-create-vehicle-info.predefined.component"; import JobsCreateVehicleInfoPredefined from "./jobs-create-vehicle-info.predefined.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
export default function JobsCreateVehicleInfoNewComponent({ form }) { export default function JobsCreateVehicleInfoNewComponent({ form }) {
const [state] = useContext(JobCreateContext); const [state] = useContext(JobCreateContext);
@@ -113,7 +113,7 @@ export default function JobsCreateVehicleInfoNewComponent({ form }) {
<Input disabled={!state.vehicle.new} /> <Input disabled={!state.vehicle.new} />
</Form.Item> </Form.Item>
<Form.Item label={t("vehicles.fields.v_prod_dt")} name={["vehicle", "data", "v_prod_dt"]}> <Form.Item label={t("vehicles.fields.v_prod_dt")} name={["vehicle", "data", "v_prod_dt"]}>
<DateTimePicker isDateOnly disabled={!state.vehicle.new} /> <FormDatePicker disabled={!state.vehicle.new} />
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow grow> <LayoutFormRow grow>

View File

@@ -5,6 +5,7 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors"; import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component"; import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import FormRow from "../layout-form-row/layout-form-row.component"; import FormRow from "../layout-form-row/layout-form-row.component";
@@ -29,7 +30,7 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
<div> <div>
<FormRow header={t("jobs.forms.estdates")}> <FormRow header={t("jobs.forms.estdates")}>
<Form.Item label={t("jobs.fields.date_estimated")} name="date_estimated"> <Form.Item label={t("jobs.fields.date_estimated")} name="date_estimated">
<DateTimePicker disabled={jobRO} isDateOnly /> <FormDatePicker disabled={jobRO} />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.date_open")} name="date_open"> <Form.Item label={t("jobs.fields.date_open")} name="date_open">
<DateTimePicker disabled={jobRO} /> <DateTimePicker disabled={jobRO} />
@@ -44,7 +45,7 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
<FormRow header={t("jobs.forms.scheddates")}> <FormRow header={t("jobs.forms.scheddates")}>
<Form.Item label={t("jobs.fields.date_scheduled")} name="date_scheduled"> <Form.Item label={t("jobs.fields.date_scheduled")} name="date_scheduled">
<DateTimePicker disabled={jobRO} isDateOnly /> <FormDatePicker disabled={jobRO} />
</Form.Item> </Form.Item>
<Tooltip title={t("jobs.labels.scheduledinchange")}> <Tooltip title={t("jobs.labels.scheduledinchange")}>
<Form.Item label={t("jobs.fields.scheduled_in")} name="scheduled_in"> <Form.Item label={t("jobs.fields.scheduled_in")} name="scheduled_in">
@@ -84,6 +85,7 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
rules={[ rules={[
{ {
required: jobInPostProduction required: jobInPostProduction
//message: t("general.validation.required"),
} }
]} ]}
> >

View File

@@ -5,6 +5,7 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors"; import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component"; import FormItemEmail from "../form-items-formatted/email-form-item.component";
import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component"; import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
@@ -12,7 +13,6 @@ import Car from "../job-damage-visual/job-damage-visual.component";
import JobsDetailChangeEstimator from "../jobs-detail-change-estimator/jobs-detail-change-estimator.component"; import JobsDetailChangeEstimator from "../jobs-detail-change-estimator/jobs-detail-change-estimator.component";
import JobsDetailChangeFileHandler from "../jobs-detail-change-filehandler/jobs-detail-change-filehandler.component"; import JobsDetailChangeFileHandler from "../jobs-detail-change-filehandler/jobs-detail-change-filehandler.component";
import FormRow from "../layout-form-row/layout-form-row.component"; import FormRow from "../layout-form-row/layout-form-row.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly, jobRO: selectJobReadOnly,
@@ -152,7 +152,7 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
<Input disabled={jobRO} /> <Input disabled={jobRO} />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.loss_date")} name="loss_date"> <Form.Item label={t("jobs.fields.loss_date")} name="loss_date">
<DateTimePicker isDateOnly disabled={jobRO} /> <FormDatePicker disabled={jobRO} />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.loss_of_use")} name="loss_of_use"> <Form.Item label={t("jobs.fields.loss_of_use")} name="loss_of_use">
<Input disabled={jobRO} /> <Input disabled={jobRO} />

View File

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

View File

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

View File

@@ -23,7 +23,7 @@ export function PartnerPingComponent({ bodyshop }) {
// Execute the created function directly // Execute the created function directly
checkPartnerStatus(bodyshop); checkPartnerStatus(bodyshop);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [bodyshop?.id]); }, [bodyshop]);
return <></>; return <></>;
} }

View File

@@ -1,6 +1,6 @@
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { Button, Card, Col, notification, Row, Table } from "antd"; import { Button, Card, Col, notification, Row, Table } from "antd";
import dayjs from "../../utils/day"; import day from "../../utils/day";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { UPDATE_PARTS_DISPATCH_LINE } from "../../graphql/parts-dispatch.queries"; import { UPDATE_PARTS_DISPATCH_LINE } from "../../graphql/parts-dispatch.queries";
@@ -11,7 +11,7 @@ export default function PartsDispatchExpander({ dispatch, job }) {
const [updateDispatchLine] = useMutation(UPDATE_PARTS_DISPATCH_LINE); const [updateDispatchLine] = useMutation(UPDATE_PARTS_DISPATCH_LINE);
const handleAccept = async ({ partsDispatchLineId }) => { const handleAccept = async ({ partsDispatchLineId }) => {
const accepted_at = dayjs(); const accepted_at = day();
const result = await updateDispatchLine({ const result = await updateDispatchLine({
variables: { id: partsDispatchLineId, line: { accepted_at } }, variables: { id: partsDispatchLineId, line: { accepted_at } },
optimisticResponse: { optimisticResponse: {

View File

@@ -8,8 +8,8 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import { MUTATION_UPDATE_BO_ETA } from "../../graphql/parts-orders.queries"; import { MUTATION_UPDATE_BO_ETA } from "../../graphql/parts-orders.queries";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import { CalendarFilled } from "@ant-design/icons"; import { CalendarFilled } from "@ant-design/icons";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
@@ -62,7 +62,7 @@ export function PartsOrderBackorderEta({
<div> <div>
<Form form={form} onFinish={handleFinish}> <Form form={form} onFinish={handleFinish}>
<Form.Item name="eta"> <Form.Item name="eta">
<DateTimePicker isDateOnly /> <FormDatePicker />
</Form.Item> </Form.Item>
<Button type="primary" onClick={() => form.submit()}> <Button type="primary" onClick={() => form.submit()}>
{t("general.actions.save")} {t("general.actions.save")}

View File

@@ -7,7 +7,7 @@ import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import { MUTATION_BACKORDER_PART_LINE } from "../../graphql/parts-orders.queries"; import { MUTATION_BACKORDER_PART_LINE } from "../../graphql/parts-orders.queries";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx"; import FormDatePicker from "../form-date-picker/form-date-picker.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
@@ -71,7 +71,7 @@ export function PartsOrderLineBackorderButton({ partsOrderStatus, partsLineId, j
<div> <div>
<Form form={form} onFinish={handleFinish}> <Form form={form} onFinish={handleFinish}>
<Form.Item name="eta"> <Form.Item name="eta">
<DateTimePicker isDateOnly /> <FormDatePicker />
</Form.Item> </Form.Item>
<Button type="primary" onClick={() => form.submit()}> <Button type="primary" onClick={() => form.submit()}>
{t("parts_orders.actions.backordered")} {t("parts_orders.actions.backordered")}

View File

@@ -1,7 +1,8 @@
import { DeleteFilled } from "@ant-design/icons"; import { DeleteFilled, EyeFilled } from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-layout"; import { PageHeader } from "@ant-design/pro-layout";
import { useLazyQuery, useMutation } from "@apollo/client"; import { useLazyQuery, useMutation } from "@apollo/client";
import { Button, Drawer, Grid, Popconfirm, Space, Table } from "antd"; import { Button, Drawer, Grid, Popconfirm, Space, Table } from "antd";
import queryString from "query-string"; import queryString from "query-string";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -82,34 +83,47 @@ export function PartsOrderListTableDrawerComponent({
sortedInfo: {} sortedInfo: {}
}); });
const [billData, setBillData] = useState(null); const [returnfrombill, setReturnFromBill] = useState();
const [billData, setBillData] = useState();
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const selectedpartsorder = search.partsorderid; const selectedpartsorder = search.partsorderid;
const [billQuery] = useLazyQuery(QUERY_BILL_BY_PK);
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER); const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : []; const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
const { refetch } = billsQuery; const { refetch } = billsQuery;
const [billQuery] = useLazyQuery(QUERY_BILL_BY_PK);
const selectedPartsOrderRecord = parts_orders.find((r) => r.id === selectedpartsorder);
useEffect(() => { useEffect(() => {
const fetchData = async () => { if (returnfrombill === null) {
if (selectedPartsOrderRecord?.returnfrombill) { setBillData(null);
try { } else {
const { data } = await billQuery({ const fetchData = async () => {
variables: { billid: selectedPartsOrderRecord.returnfrombill } const result = await billQuery({
}); variables: { billid: returnfrombill }
setBillData(data); });
} catch (error) { setBillData(result.data);
console.error("Error fetching bill data:", error); };
} fetchData();
} else setBillData(null); }
}; }, [returnfrombill, billQuery]);
fetchData();
}, [selectedPartsOrderRecord, billQuery]);
const recordActions = (record) => ( const recordActions = (record, showView = false) => (
<Space direction="horizontal" wrap> <Space direction="horizontal" wrap>
{showView && (
<Button
onClick={() => {
if (record.returnfrombill) {
setReturnFromBill(record.returnfrombill);
} else {
setReturnFromBill(null);
}
handleOnRowClick(record);
}}
>
<EyeFilled />
</Button>
)}
<Button <Button
disabled={jobRO || record.return || record.vendor.id === bodyshop.inhousevendorid} disabled={jobRO || record.return || record.vendor.id === bodyshop.inhousevendorid}
onClick={() => { onClick={() => {
@@ -119,14 +133,16 @@ export function PartsOrderListTableDrawerComponent({
context: { context: {
jobId: job.id, jobId: job.id,
job: job, job: job,
partsorderlines: record.parts_order_lines.map((pol) => ({ partsorderlines: record.parts_order_lines.map((pol) => {
joblineid: pol.job_line_id, return {
id: pol.id, joblineid: pol.job_line_id,
line_desc: pol.line_desc, id: pol.id,
quantity: pol.quantity, line_desc: pol.line_desc,
act_price: pol.act_price, quantity: pol.quantity,
oem_partno: pol.oem_partno act_price: pol.act_price,
})) oem_partno: pol.oem_partno
};
})
} }
}); });
}} }}
@@ -151,6 +167,7 @@ export function PartsOrderListTableDrawerComponent({
disabled={jobRO} disabled={jobRO}
onConfirm={async () => { onConfirm={async () => {
//Delete the parts return.! //Delete the parts return.!
await deletePartsOrder({ await deletePartsOrder({
variables: { partsOrderId: record.id }, variables: { partsOrderId: record.id },
update(cache) { update(cache) {
@@ -174,6 +191,7 @@ export function PartsOrderListTableDrawerComponent({
disabled={(jobRO ? !record.return : jobRO) || record.vendor.id === bodyshop.inhousevendorid} disabled={(jobRO ? !record.return : jobRO) || record.vendor.id === bodyshop.inhousevendorid}
onClick={() => { onClick={() => {
logImEXEvent("parts_order_receive_bill"); logImEXEvent("parts_order_receive_bill");
setBillEnterContext({ setBillEnterContext({
actions: { refetch: refetch }, actions: { refetch: refetch },
context: { context: {
@@ -181,20 +199,24 @@ export function PartsOrderListTableDrawerComponent({
bill: { bill: {
vendorid: record.vendor.id, vendorid: record.vendor.id,
is_credit_memo: record.return, is_credit_memo: record.return,
billlines: record.parts_order_lines.map((pol) => ({ billlines: record.parts_order_lines.map((pol) => {
joblineid: pol.job_line_id || "noline", return {
line_desc: pol.line_desc, joblineid: pol.job_line_id || "noline",
quantity: pol.quantity, line_desc: pol.line_desc,
actual_price: pol.act_price, quantity: pol.quantity,
cost_center: pol.jobline?.part_type
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid actual_price: pol.act_price,
? pol.jobline.part_type !== "PAE"
? pol.jobline.part_type cost_center: pol.jobline?.part_type
: null ? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
: responsibilityCenters.defaults && ? pol.jobline.part_type !== "PAE"
(responsibilityCenters.defaults.costs[pol.jobline.part_type] || null) ? pol.jobline.part_type
: null : null
})) : responsibilityCenters.defaults &&
(responsibilityCenters.defaults.costs[pol.jobline.part_type] || null)
: null
};
})
} }
} }
}); });
@@ -221,6 +243,8 @@ export function PartsOrderListTableDrawerComponent({
setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
}; };
const selectedPartsOrderRecord = parts_orders.find((r) => r.id === selectedpartsorder);
const rowExpander = (record) => { const rowExpander = (record) => {
const columns = [ const columns = [
{ {

View File

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

View File

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

View File

@@ -6,12 +6,12 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component"; import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import PartsOrderModalPriceChange from "./parts-order-modal-price-change.component"; import PartsOrderModalPriceChange from "./parts-order-modal-price-change.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
@@ -74,7 +74,7 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState,
]} ]}
label={t("parts_orders.fields.deliver_by")} label={t("parts_orders.fields.deliver_by")}
> >
<DateTimePicker isDateOnly onlyFuture /> <FormDatePicker onlyFuture />
</Form.Item> </Form.Item>
{job && job.special_coverage_policy && ( {job && job.special_coverage_policy && (
<Tag color="tomato"> <Tag color="tomato">

View File

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

View File

@@ -5,11 +5,11 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import DatePickerFormItem from "../form-date-picker/form-date-picker.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import JobSearchSelect from "../job-search-select/job-search-select.component"; import JobSearchSelect from "../job-search-select/job-search-select.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import PaymentFormTotalPayments from "./payment-form.totalpayments.component"; import PaymentFormTotalPayments from "./payment-form.totalpayments.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
@@ -77,7 +77,7 @@ export function PaymentFormComponent({ form, bodyshop, disabled }) {
} }
]} ]}
> >
<DateTimePicker isDateOnly disabled={disabled} /> <DatePickerFormItem disabled={disabled} />
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>

View File

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

View File

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

View File

@@ -6,11 +6,11 @@ import {
PauseCircleOutlined PauseCircleOutlined
} from "@ant-design/icons"; } from "@ant-design/icons";
import { Card, Col, Row, Space, Tooltip } from "antd"; import { Card, Col, Row, Space, Tooltip } from "antd";
import Dinero from "dinero.js";
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { DateTimeFormatter } from "../../utils/DateFormatter"; import { DateTimeFormatter } from "../../utils/DateFormatter";
import Dinero from "dinero.js";
import ProductionAlert from "../production-list-columns/production-list-columns.alert.component"; import ProductionAlert from "../production-list-columns/production-list-columns.alert.component";
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.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 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 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 cardColor = (ssbuckets, totalHrs) => {
const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > totalHrs)); const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > totalHrs));
@@ -213,13 +213,21 @@ const EstimatorToolTip = ({ metadata, cardSettings }) => {
}; };
const SubtotalTooltip = ({ metadata, cardSettings, t }) => { 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 ( return (
cardSettings?.subtotal && ( cardSettings?.subtotal && (
<Col span={cardSettings.compact ? 24 : 12}> <Col span={cardSettings.compact ? 24 : 12}>
<EllipsesToolTip title={`${dineroAmount}`} kiosk={cardSettings.kiosk}> <EllipsesToolTip
{dineroAmount} 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> </EllipsesToolTip>
</Col> </Col>
) )

View File

@@ -1,14 +1,12 @@
import { SyncOutlined } from "@ant-design/icons"; import { SyncOutlined } from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-layout";
import { useApolloClient } from "@apollo/client"; import { useApolloClient } from "@apollo/client";
import Board from "./trello-board/index";
import { Button, notification, Skeleton, Space } from "antd"; import { Button, notification, Skeleton, Space } from "antd";
import cloneDeep from "lodash/cloneDeep"; import { PageHeader } from "@ant-design/pro-layout";
import isEqual from "lodash/isEqual";
import React, { useCallback, useEffect, useMemo, useState } from "react"; import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import NoteUpsertModal from "../../components/note-upsert-modal/note-upsert-modal.container";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import { generate_UPDATE_JOB_KANBAN } from "../../graphql/jobs.queries"; import { generate_UPDATE_JOB_KANBAN } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
@@ -17,13 +15,14 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
import IndefiniteLoading from "../indefinite-loading/indefinite-loading.component"; import IndefiniteLoading from "../indefinite-loading/indefinite-loading.component";
import ProductionBoardFilters from "../production-board-filters/production-board-filters.component"; import ProductionBoardFilters from "../production-board-filters/production-board-filters.component";
import ProductionListDetailComponent from "../production-list-detail/production-list-detail.component"; import ProductionListDetailComponent from "../production-list-detail/production-list-detail.component";
import ProductionListPrint from "../production-list-table/production-list-print.component.jsx";
import CardColorLegend from "./production-board-kanban-card-color-legend.component.jsx"; import CardColorLegend from "./production-board-kanban-card-color-legend.component.jsx";
import "./production-board-kanban.styles.scss"; import "./production-board-kanban.styles.scss";
import { createBoardData } from "./production-board-kanban.utils.js"; import { createBoardData } from "./production-board-kanban.utils.js";
import { defaultFilters, mergeWithDefaults } from "./settings/defaultKanbanSettings.js";
import ProductionBoardKanbanSettings from "./settings/production-board-kanban.settings.component.jsx"; import ProductionBoardKanbanSettings from "./settings/production-board-kanban.settings.component.jsx";
import Board from "./trello-board/index"; import cloneDeep from "lodash/cloneDeep";
import isEqual from "lodash/isEqual";
import { defaultFilters, mergeWithDefaults } from "./settings/defaultKanbanSettings.js";
import NoteUpsertModal from "../../components/note-upsert-modal/note-upsert-modal.container";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
@@ -186,7 +185,7 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
const cardSettings = useMemo(() => { const cardSettings = useMemo(() => {
const kanbanSettings = associationSettings?.kanban_settings; const kanbanSettings = associationSettings?.kanban_settings;
return mergeWithDefaults(kanbanSettings); return mergeWithDefaults(kanbanSettings);
}, [associationSettings?.kanban_settings]); }, [associationSettings]);
const handleSettingsChange = () => { const handleSettingsChange = () => {
setFilter(defaultFilters); setFilter(defaultFilters);
@@ -215,7 +214,6 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
bodyshop={bodyshop} bodyshop={bodyshop}
data={data} data={data}
/> />
<ProductionListPrint />
</Space> </Space>
} }
/> />

View File

@@ -1,40 +1,18 @@
import React, { useContext, useEffect, useMemo, useRef } from "react"; import React, { useEffect, useMemo } from "react";
import { useApolloClient, useQuery, useSubscription } from "@apollo/client"; import { useQuery, useSubscription } from "@apollo/client";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { import { QUERY_JOBS_IN_PRODUCTION, SUBSCRIPTION_JOBS_IN_PRODUCTION } from "../../graphql/jobs.queries";
QUERY_EXACT_JOB_IN_PRODUCTION,
QUERY_JOBS_IN_PRODUCTION,
SUBSCRIPTION_JOBS_IN_PRODUCTION,
SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW
} from "../../graphql/jobs.queries";
import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries"; import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import ProductionBoardKanbanComponent from "./production-board-kanban.component"; import ProductionBoardKanbanComponent from "./production-board-kanban.component";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser currentUser: selectCurrentUser
}); });
function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionType = "direct" }) { function ProductionBoardKanbanContainer({ bodyshop, currentUser }) {
const fired = useRef(false);
const client = useApolloClient();
const { socket } = useContext(SocketContext); // Get the socket from context
const reconnectTimeout = useRef(null); // To store the reconnect timeout
const disconnectTime = useRef(null); // To track disconnection time
const acceptableReconnectTime = 2000; // 2 seconds threshold
const {
treatments: { Websocket_Production }
} = useSplitTreatments({
attributes: {},
names: ["Websocket_Production"],
splitKey: bodyshop && bodyshop.imexshopid
});
const combinedStatuses = useMemo( const combinedStatuses = useMemo(
() => [ () => [
...bodyshop.md_ro_statuses.production_statuses, ...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}`) onError: (error) => console.error(`Error fetching jobs in production: ${error.message}`)
}); });
const subscriptionEnabled = Websocket_Production?.treatment === "off"; const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION, {
onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
const { data: updatedJobs } = useSubscription( });
subscriptionType === "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION,
{
skip: !subscriptionEnabled,
onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
}
);
const { loading: associationSettingsLoading, data: associationSettings } = useQuery(QUERY_KANBAN_SETTINGS, { const { loading: associationSettingsLoading, data: associationSettings } = useQuery(QUERY_KANBAN_SETTINGS, {
variables: { email: currentUser.email }, variables: { email: currentUser.email },
onError: (error) => console.error(`Error fetching Kanban settings: ${error.message}`) onError: (error) => console.error(`Error fetching Kanban settings: ${error.message}`)
}); });
// const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
useEffect(() => { useEffect(() => {
if (subscriptionEnabled) { if (updatedJobs && data) {
if (!updatedJobs) {
return;
}
if (!fired.current) {
fired.current = true;
return;
}
refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`)); refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`));
} }
}, [updatedJobs, refetch, subscriptionEnabled]); }, [updatedJobs, data, refetch]);
// Socket.IO implementation for users with Split treatment "off"
useEffect(() => {
if (subscriptionEnabled || !socket || !bodyshop || !bodyshop.id) {
return;
}
const handleJobUpdates = async (jobChangedData) => {
const jobId = jobChangedData.id;
// Access the existing cache for QUERY_JOBS_IN_PRODUCTION
const existingJobsCache = client.readQuery({
query: QUERY_JOBS_IN_PRODUCTION
});
const existingJobs = existingJobsCache?.jobs || [];
// Check if the job already exists in the cached jobs
const existingJob = existingJobs.find((job) => job.id === jobId);
if (existingJob) {
// If the job exists, we update the cache without making any additional queries
client.writeQuery({
query: QUERY_JOBS_IN_PRODUCTION,
data: {
jobs: existingJobs.map((job) =>
job.id === jobId ? { ...existingJob, ...jobChangedData, __typename: "jobs" } : job
)
}
});
} else {
// If the job doesn't exist, fetch it from the server and then add it to the cache
try {
const { data: jobData } = await client.query({
query: QUERY_EXACT_JOB_IN_PRODUCTION,
variables: { id: jobId },
fetchPolicy: "network-only"
});
// Add the job to the existing cached jobs
client.writeQuery({
query: QUERY_JOBS_IN_PRODUCTION,
data: {
jobs: [...existingJobs, { ...jobData.job, __typename: "jobs" }]
}
});
} catch (error) {
console.error(`Error fetching job ${jobId}: ${error.message}`);
}
}
};
const handleDisconnect = () => {
// Capture the disconnection time
disconnectTime.current = Date.now();
};
const handleReconnect = () => {
const reconnectTime = Date.now();
const disconnectionDuration = reconnectTime - disconnectTime.current;
// Only refetch if disconnection was longer than the acceptable reconnect time
if (disconnectionDuration >= acceptableReconnectTime) {
if (!reconnectTimeout.current) {
reconnectTimeout.current = setTimeout(() => {
const randomDelay = Math.floor(Math.random() * (30000 - 10000 + 1)) + 10000; // Random delay between 10 and 30 seconds
setTimeout(() => {
if (refetch) refetch().catch((err) => console.error(`Issue `));
reconnectTimeout.current = null; // Clear the timeout reference after refetch
}, randomDelay);
}, acceptableReconnectTime);
}
}
};
// Listen for 'job-changed', 'disconnect', and 'connect' events
socket.on("production-job-updated", handleJobUpdates);
socket.on("disconnect", handleDisconnect);
socket.on("connect", handleReconnect);
// Clean up on unmount or when dependencies change
return () => {
socket.off("production-job-updated", handleJobUpdates);
socket.off("disconnect", handleDisconnect);
socket.off("connect", handleReconnect);
if (reconnectTimeout.current) {
clearTimeout(reconnectTimeout.current);
}
};
}, [subscriptionEnabled, socket, bodyshop, client, refetch]);
const filteredAssociationSettings = useMemo(() => { const filteredAssociationSettings = useMemo(() => {
return associationSettings?.associations[0] || null; return associationSettings?.associations[0] || null;
}, [associationSettings?.associations]); }, [associationSettings]);
return ( return (
<ProductionBoardKanbanComponent <ProductionBoardKanbanComponent

View File

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

View File

@@ -17,6 +17,7 @@
border-radius: 5px 5px 0 0; border-radius: 5px 5px 0 0;
} }
.production-alert { .production-alert {
background: transparent; background: transparent;
border: none; 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 = [ const statisticsItems = [
{ id: 0, name: "totalHrs", label: "total_hours_in_production" }, { id: 0, name: "totalHrs", label: "total_hours_in_production" },
{ id: 1, name: "totalAmountInProduction", label: "total_amount_in_production" }, { id: 1, name: "totalAmountInProduction", label: "total_amount_in_production" },
{ id: 2, name: "totalLAB", label: "total_lab_in_production" }, { id: 2, name: "totalLAB", label: "total_lab_in_production" },
{ id: 3, name: "totalLAR", label: "total_lar_in_production" }, { id: 3, name: "totalLAR", label: "total_lar_in_production" },
{ id: 4, name: "jobsInProduction", label: "jobs_in_production" }, { id: 4, name: "jobsInProduction", label: "jobs_in_production" },
{ id: 5, name: "totalHrsOnBoard", label: "total_hours_on_board" },
{ { id: 6, name: "totalAmountOnBoard", label: "total_amount_on_board" },
id: 5, { id: 7, name: "totalLABOnBoard", label: "total_lab_on_board" },
name: "totalHrsOnBoard", { id: 8, name: "totalLAROnBoard", label: "total_lar_on_board" },
label: InstanceRenderManager({ { id: 9, name: "jobsOnBoard", label: "total_jobs_on_board" },
imex: "total_hours_in_view", { id: 10, name: "tasksOnBoard", label: "tasks_on_board" },
rome: "total_hours_on_board",
promanager: "total_hours_on_board"
})
},
{
id: 6,
name: "totalAmountOnBoard",
label: InstanceRenderManager({
imex: "total_amount_in_view",
rome: "total_amount_on_board",
promanager: "total_amount_on_board"
})
},
{
id: 7,
name: "totalLABOnBoard",
label: InstanceRenderManager({
imex: "total_lab_in_view",
rome: "total_lab_on_board",
promanager: "total_lab_on_board"
})
},
{
id: 8,
name: "totalLAROnBoard",
label: InstanceRenderManager({
imex: "total_lar_in_view",
rome: "total_lar_on_board",
promanager: "total_lar_on_board"
})
},
{
id: 9,
name: "jobsOnBoard",
label: InstanceRenderManager({
imex: "total_jobs_in_view",
rome: "total_jobs_on_board",
promanager: "total_jobs_on_board"
})
},
{
id: 10,
name: "tasksOnBoard",
label: InstanceRenderManager({
imex: "tasks_in_view",
rome: "tasks_on_board",
promanager: "tasks_on_board"
})
},
{ id: 11, name: "tasksInProduction", label: "tasks_in_production" } { id: 11, name: "tasksInProduction", label: "tasks_in_production" }
]; ];

View File

@@ -25,8 +25,8 @@ function getFurthestAway({ pageBorderBox, draggable, candidates }) {
const axis = candidate.axis; const axis = candidate.axis;
const target = patch( const target = patch(
candidate.axis.line, candidate.axis.line,
// use the center of the list on the main axis // use the current center of the dragging item on the main axis
candidate.page.borderBox.center[axis.line], pageBorderBox.center[axis.line],
// use the center of the list on the cross axis // use the center of the list on the cross axis
candidate.page.borderBox.center[axis.crossAxisLine] candidate.page.borderBox.center[axis.crossAxisLine]
); );

View File

@@ -5,7 +5,6 @@ import getBodyElement from "../get-body-element";
const isEqual = (base) => (value) => base === value; const isEqual = (base) => (value) => base === value;
const isScroll = isEqual("scroll"); const isScroll = isEqual("scroll");
const isAuto = isEqual("auto"); const isAuto = isEqual("auto");
const isOverlay = isEqual("overlay");
const isVisible = isEqual("visible"); const isVisible = isEqual("visible");
const isEither = (overflow, fn) => fn(overflow.overflowX) || fn(overflow.overflowY); const isEither = (overflow, fn) => fn(overflow.overflowX) || fn(overflow.overflowY);
const isBoth = (overflow, fn) => fn(overflow.overflowX) && fn(overflow.overflowY); const isBoth = (overflow, fn) => fn(overflow.overflowX) && fn(overflow.overflowY);
@@ -15,7 +14,7 @@ const isElementScrollable = (el) => {
overflowX: style.overflowX, overflowX: style.overflowX,
overflowY: style.overflowY 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 // Special case for a body element

View File

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

View File

@@ -240,14 +240,11 @@ export default function useTouchSensor(api) {
y: clientY y: clientY
}; };
const handle = api.findClosestDragHandle(event);
invariant(handle, "Touch sensor unable to find drag handle");
// unbind this event handler // unbind this event handler
unbindEventsRef.current(); unbindEventsRef.current();
// eslint-disable-next-line no-use-before-define // eslint-disable-next-line no-use-before-define
startPendingDrag(actions, point, handle); startPendingDrag(actions, point);
} }
}), }),
// not including stop or startPendingDrag as it is not defined initially // not including stop or startPendingDrag as it is not defined initially
@@ -291,7 +288,7 @@ export default function useTouchSensor(api) {
} }
}, [stop]); }, [stop]);
const bindCapturingEvents = useCallback( const bindCapturingEvents = useCallback(
function bindCapturingEvents(target) { function bindCapturingEvents() {
const options = { const options = {
capture: true, capture: true,
passive: false passive: false
@@ -310,7 +307,7 @@ export default function useTouchSensor(api) {
// Old behaviour: // Old behaviour:
// https://gist.github.com/parris/dda613e3ae78f14eb2dc9fa0f4bfce3d // https://gist.github.com/parris/dda613e3ae78f14eb2dc9fa0f4bfce3d
// https://stackoverflow.com/questions/33298828/touch-move-event-dont-fire-after-touch-start-target-is-removed // https://stackoverflow.com/questions/33298828/touch-move-event-dont-fire-after-touch-start-target-is-removed
const unbindTarget = bindEvents(target, getHandleBindings(args), options); const unbindTarget = bindEvents(window, getHandleBindings(args), options);
const unbindWindow = bindEvents(window, getWindowBindings(args), options); const unbindWindow = bindEvents(window, getWindowBindings(args), options);
unbindEventsRef.current = function unbindAll() { unbindEventsRef.current = function unbindAll() {
unbindTarget(); unbindTarget();
@@ -333,7 +330,7 @@ export default function useTouchSensor(api) {
[getPhase, setPhase] [getPhase, setPhase]
); );
const startPendingDrag = useCallback( const startPendingDrag = useCallback(
function startPendingDrag(actions, point, target) { function startPendingDrag(actions, point) {
invariant(getPhase().type === "IDLE", "Expected to move from IDLE to PENDING drag"); invariant(getPhase().type === "IDLE", "Expected to move from IDLE to PENDING drag");
const longPressTimerId = setTimeout(startDragging, timeForLongPress); const longPressTimerId = setTimeout(startDragging, timeForLongPress);
setPhase({ setPhase({
@@ -342,7 +339,7 @@ export default function useTouchSensor(api) {
actions, actions,
longPressTimerId longPressTimerId
}); });
bindCapturingEvents(target); bindCapturingEvents();
}, },
[bindCapturingEvents, getPhase, setPhase, startDragging] [bindCapturingEvents, getPhase, setPhase, startDragging]
); );

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