Compare commits

..

123 Commits

Author SHA1 Message Date
Allan Carr
7da3ef4e0c IO-2302 California Consumer Law Update 1
Change label for FIPPA switch
2023-06-12 15:00:17 -07:00
Patrick Fic
4c32171fbb Merged in feature/america (pull request #859)
Feature/america
2023-06-12 19:42:56 +00:00
Patrick Fic
f9633505a2 Merge branch 'master' into feature/america 2023-06-12 10:05:03 -07:00
Patrick Fic
9d503b12e6 Merged in feature/america (pull request #847)
Feature/america
2023-06-06 23:04:34 +00:00
Patrick Fic
bccfc127f3 Resolve null line desc issue. 2023-06-06 12:26:26 -07:00
Patrick Fic
2332d8756d Resolve password reset issue. 2023-06-06 08:11:28 -07:00
Patrick Fic
a4abaed30a Merged in feature/america (pull request #841)
IO-2294 add parts summary list table.
2023-06-02 21:09:25 +00:00
Patrick Fic
d47e18df1f Merge branch 'feature/IO-2294-parts-summary-list' into feature/america 2023-06-02 14:08:24 -07:00
Patrick Fic
c2d094da35 IO-2294 add parts summary list table. 2023-06-02 14:08:01 -07:00
Patrick Fic
6e54b10239 Merged in feature/america (pull request #839)
Feature/america
2023-06-02 19:21:58 +00:00
Patrick Fic
e6c44ec52a Resolve CI issues. 2023-06-02 09:43:46 -07:00
Patrick Fic
39b8ecf785 IO-2313 Remove part type for secondary CCC lines. 2023-06-02 09:38:54 -07:00
Patrick Fic
d87f871334 Merged in feature/america (pull request #838)
Feature/america
2023-06-02 16:36:40 +00:00
Patrick Fic
07cea56e2b Merged in release/2023-06-02 (pull request #837)
Release/2023 06 02
2023-06-02 16:35:52 +00:00
Patrick Fic
dbf3226f97 Merge branch 'feature/payroll' into feature/america 2023-06-02 09:32:00 -07:00
Patrick Fic
4b926edf24 Merged in feature/america (pull request #831)
Feature/america
2023-06-02 02:04:14 +00:00
Patrick Fic
ce0fd42995 Merge branch 'feature/IO-2310-active-jobs-ro-sort' into feature/america 2023-06-01 18:30:00 -07:00
Patrick Fic
bdf91443e0 IO-2276 Change file handler to adjuster. 2023-06-01 18:13:36 -07:00
Patrick Fic
81fc747c03 Update noticeable link to Rome Online. 2023-05-30 11:31:35 -07:00
Patrick Fic
3a25ed13b9 Merged in feature/america (pull request #825)
Feature/america
2023-05-30 18:19:30 +00:00
Patrick Fic
21c5eb6786 Increase Zoho waiting time. 2023-05-30 11:18:29 -07:00
Patrick Fic
e85203e429 Merge branch 'master' into feature/america 2023-05-29 14:49:13 -07:00
Patrick Fic
6b2d10d589 Zoho changes. 2023-05-26 11:15:50 -07:00
Patrick Fic
3d03de193e Add commit and uncommit functionality. 2023-05-25 15:23:14 -07:00
Patrick Fic
a56de72a6b Time Ticket Task Improvmenets. 2023-05-25 11:44:01 -07:00
Patrick Fic
0849bbbba6 Update translation laberl. 2023-05-25 10:14:17 -07:00
Patrick Fic
0cd41d2036 Merged in feature/america (pull request #792)
Feature/america
2023-05-18 22:00:12 +00:00
Patrick Fic
a93bcd6ab6 Resolve calculation issue for private jobs. 2023-05-17 15:19:11 -07:00
Patrick Fic
77b72e160b Update noticeable widget to Rome Online. 2023-05-17 14:47:06 -07:00
Patrick Fic
b32c37f4af Merged in feature/america (pull request #791)
feature/america

Approved-by: Patrick Fic
2023-05-17 20:49:37 +00:00
Patrick Fic
d0beea5cb3 Merge branch 'release/2023-05-19' into feature/america 2023-05-17 13:49:05 -07:00
Patrick Fic
30df6887c5 Resolve missing intellipay translations. 2023-05-17 12:27:27 -07:00
Patrick Fic
bc7823dda1 Merged in feature/america (pull request #790)
Feature/america
2023-05-17 14:52:41 +00:00
Patrick Fic
cbb195a313 Merge branch 'feature/payroll' into feature/america 2023-05-17 07:51:57 -07:00
Patrick Fic
097dc06c65 Merge branch 'release/2023-05-19' into feature/payroll 2023-05-17 07:51:23 -07:00
Patrick Fic
b06fbd25a0 Add shop config section. 2023-05-17 07:49:25 -07:00
Patrick Fic
89bbf274e5 Merged in release/2023-05-12 (pull request #787)
Release/2023 05 12
2023-05-12 18:18:28 +00:00
Patrick Fic
d854177e5a Merged in feature/america (pull request #774)
Resolve null coalescing when entering bill and add Zoho.
2023-05-10 17:55:26 +00:00
Patrick Fic
aa9f82f2d4 Resolve null coalescing when entering bill and add Zoho. 2023-05-10 10:54:35 -07:00
Patrick Fic
2b45264628 Merged in feature/america (pull request #772)
Feature/america
2023-05-08 15:54:39 +00:00
Patrick Fic
4da299f431 Merged in master (pull request #771)
Master
2023-05-08 15:53:29 +00:00
Patrick Fic
9f9aa447a6 Merged in feature/america (pull request #770)
Update to part price change for CCC.
2023-05-08 15:52:11 +00:00
Patrick Fic
6a9f1597cb Update to part price change for CCC. 2023-05-08 08:51:29 -07:00
Patrick Fic
cddc48b851 Merged in feature/america (pull request #769)
Remove filter on PPC button.
2023-05-08 15:17:35 +00:00
Patrick Fic
84ab6d300c Remove filter on PPC button. 2023-05-08 08:16:50 -07:00
Patrick Fic
3c5026c2fb Merged in feature/america (pull request #755)
Feature/america
2023-05-04 20:54:29 +00:00
Patrick Fic
c3c8959d2e Merge branch 'release/2023-05-05' into feature/america 2023-05-04 13:52:32 -07:00
Patrick Fic
2c459d2636 Merged in feature/america (pull request #747)
Default log rocket on in Rome Production.
2023-05-03 21:52:10 +00:00
Patrick Fic
a33c481dd5 Default log rocket on in Rome Production. 2023-05-03 14:51:51 -07:00
Patrick Fic
d6b2f93e54 Merged in feature/america (pull request #740)
Fresh Chat ID.
2023-05-01 22:40:33 +00:00
Patrick Fic
802e945829 Fresh Chat ID. 2023-05-01 15:39:51 -07:00
Patrick Fic
a4a885f33a Merged in feature/america (pull request #739)
Replace Crisp with FreshChat.
2023-05-01 22:30:35 +00:00
Patrick Fic
3380cebb28 Replace Crisp with FreshChat. 2023-05-01 15:29:37 -07:00
Patrick Fic
21ed415f4f Merged in feature/america (pull request #734)
Feature/america
2023-04-27 22:58:39 +00:00
Patrick Fic
51dd89d36a Merge branch 'feature/payroll' into feature/america 2023-04-27 15:52:37 -07:00
Patrick Fic
bad96f231c Code clean up prior to merge. 2023-04-27 15:51:39 -07:00
Patrick Fic
184d72c444 Merge branch 'release/2023-04-28' into feature/america 2023-04-27 15:21:42 -07:00
Patrick Fic
6ca773050f WIP Payroll Commit process. 2023-04-25 10:24:26 -07:00
Patrick Fic
648e8aaae0 QBO USA Changes for tax and non tax items. 2023-04-24 13:13:32 -07:00
Patrick Fic
ad9868b575 IO-2206 WIP Time Ticket Approval Queue. 2023-04-21 11:23:48 -07:00
Patrick Fic
d4d10998f8 Merge branch 'release/2023-04-21' into feature/payroll 2023-04-20 15:05:48 -07:00
Patrick Fic
ba22c31deb Merged in feature/america (pull request #709)
Feature/america
2023-04-18 20:28:08 +00:00
Patrick Fic
f6be133a78 Merge branch 'feature/payroll' into feature/america 2023-04-18 13:27:02 -07:00
Patrick Fic
3768164f1f IO-2206 Refine claim tasks modal to add validations and insertions. 2023-04-18 13:25:42 -07:00
Patrick Fic
8465e7539f Merge remote-tracking branch 'origin/master' into feature/payroll 2023-04-17 13:12:47 -07:00
Patrick Fic
542eca5867 Claim Tasks WIP. 2023-04-12 12:44:19 -07:00
Patrick Fic
27a67c5f4a Added team pay calculator. 2023-04-06 10:36:29 -07:00
Patrick Fic
e00c40f2d5 Merge branch 'release/2023-04-07' into feature/payroll 2023-04-06 08:10:57 -07:00
Patrick Fic
b664e231dc Merge branch 'release/2023-04-07' into feature/america 2023-04-05 17:23:15 -07:00
Patrick Fic
92d6977f9e Merge branch 'rome/master' of https://bitbucket.org/snaptsoft/bodyshop into rome/master 2023-04-05 15:30:10 -07:00
Patrick Fic
4059700468 Merge branch 'release/2023-04-07' into feature/america 2023-04-05 15:29:36 -07:00
Patrick Fic
196bdd83ba IO-2206 Payroll UI updates for discussions with Rome Clients. 2023-04-05 09:13:37 -07:00
Patrick Fic
fbd52bc14e QB USA Changes 2023-04-04 14:03:08 -07:00
Patrick Fic
8013c988d0 Merged in feature/america (pull request #699)
added the logic for aws secrets manager
2023-03-28 17:17:50 +00:00
Patrick Fic
460d6fcf12 Merge branch 'feature/intellipay' into feature/america 2023-03-28 10:16:51 -07:00
Patrick Fic
92353c4853 Merged in feature/america (pull request #698)
Feature/america
2023-03-28 16:55:36 +00:00
Patrick Fic
8ca2a89f0f Improved totals calculations 2023-03-28 09:54:47 -07:00
Patrick Fic
f54c2367f3 Merge branch 'release/2023-03-24' into feature/america 2023-03-24 08:39:36 -07:00
Patrick Fic
27452085e9 Merge remote-tracking branch 'origin/release/2023-03-24' into feature/america 2023-03-21 09:13:08 -07:00
Patrick Fic
2bff7d9567 Merge branch 'release/2023-03-21' into release/2023-03-24 2023-03-21 09:10:15 -07:00
Patrick Fic
c3749f62fe Updated supplementing mechanism. 2023-03-20 14:56:02 -07:00
Patrick Fic
b82c04a16a Merge branch 'feature/intellipay' into feature/america 2023-03-20 14:11:39 -07:00
Patrick Fic
0ec099cdf5 IO-2217 Part Price Changes for CCC. 2023-03-20 14:10:06 -07:00
Patrick Fic
d3f49094d8 Add ppc field to job lines. 2023-03-20 13:20:43 -07:00
Patrick Fic
4e2ad3bc62 IO-2222 Service Side Generation of part price changes. 2023-03-16 13:33:20 -07:00
Patrick Fic
05c287fcf7 Merged in feature/america (pull request #683)
Additional whitelabel changes.

Approved-by: Patrick Fic
2023-02-27 23:23:52 +00:00
Patrick Fic
c11cef4119 Additional whitelabel changes. 2023-02-27 15:23:14 -08:00
Patrick Fic
d6126cc5ce Merged in feature/america (pull request #681)
feature/america

Approved-by: Patrick Fic
2023-02-25 00:04:44 +00:00
Patrick Fic
d88c925a68 Environment setup and email white labeling. 2023-02-24 16:04:04 -08:00
Patrick Fic
372a572400 Merge branch 'master' into feature/america 2023-02-22 08:06:29 -08:00
Patrick Fic
623ee8fbb1 WIP TotalsUpdates. 2023-02-22 08:02:10 -08:00
Patrick Fic
729e0fc5ca Resolve CI issues. 2023-02-14 16:21:17 -08:00
Patrick Fic
d052cde6ca Add circle ci for Rome. 2023-02-14 16:08:31 -08:00
Patrick Fic
b52bbab903 Updates to landing pages. 2023-02-14 16:03:41 -08:00
Patrick Fic
9d42135ebf Merged in master (pull request #677)
Integrate master into USA branch.
2023-02-14 23:12:48 +00:00
Patrick Fic
9ce5aa2189 Rome Firebase Changes. 2023-02-13 10:09:43 -08:00
Patrick Fic
e277292194 Remove unnecesary firebase deployment items. 2023-02-10 14:29:22 -08:00
Patrick Fic
649961c831 Update API email. 2023-02-10 09:56:35 -08:00
Patrick Fic
e00cde2fbd Merge branch 'release/2023-01-27' into feature/america 2023-01-27 15:33:59 -08:00
Patrick Fic
25b89d191b Futher calculations refinments and QBD testing. 2023-01-26 17:01:12 -08:00
Patrick Fic
0a60f77bfc WIP America 2023-01-26 10:58:35 -08:00
Patrick Fic
66a80e439f Revert all package upgrades to unblock testing. 2023-01-25 11:20:51 -08:00
Patrick Fic
d8cc25a54f Rome whitelabelling. 2023-01-24 14:11:53 -08:00
Patrick Fic
39b9cfb348 Merge branch 'feature/major-package-upgrades' into feature/america 2023-01-24 13:22:10 -08:00
Patrick Fic
c6087574be Resolve master merge conflicts. 2023-01-24 10:56:28 -08:00
Patrick Fic
41c63cbfe9 Improve backwards compatibility for job totals calculations. 2023-01-20 07:59:47 -08:00
Patrick Fic
f1afc81e7d Resolve UI issue. 2023-01-20 07:59:18 -08:00
Patrick Fic
018f8a19bb Merge remote-tracking branch 'origin/feature/major-package-upgrades' into feature/america 2023-01-19 13:23:57 -08:00
Patrick Fic
cccd007ba6 Additional changes for CCC calculations. 2023-01-19 08:58:23 -08:00
Patrick Fic
6d5b8baadf Automatic code mods. 2023-01-17 20:22:03 -08:00
Patrick Fic
07e36415e4 Automatic code mods. 2023-01-17 20:21:49 -08:00
Patrick Fic
8dc480826b Antd package update. 2023-01-17 20:10:59 -08:00
Patrick Fic
533a5b87ec Changing of deprecated antd props. 2023-01-17 20:00:29 -08:00
Patrick Fic
7bd83557c1 Remainder of menu refactoring updates. 2023-01-17 19:48:37 -08:00
Patrick Fic
ef290e79b1 Antd 4.24.x compatibility updates => Menu fixes. 2023-01-17 19:14:31 -08:00
Patrick Fic
713b3f4f6d Documents Gallery Package Fixes 2023-01-17 13:59:44 -08:00
Patrick Fic
8fd9614f69 Package cleanup and upgrades. 2023-01-17 11:41:45 -08:00
Patrick Fic
6682a98770 Merge branch 'master' into feature/major-package-upgrades 2023-01-17 09:43:54 -08:00
Patrick Fic
392b405978 Add massaging of data to CCC import. 2023-01-16 15:54:23 -08:00
Patrick Fic
be61850d18 Merge in latest release. 2023-01-04 11:33:53 -08:00
Patrick Fic
f4208a2212 Client side package updates. 2022-12-22 15:06:10 -08:00
Patrick Fic
0b7278a2a8 Server side upgrades 2022-12-22 12:31:16 -08:00
Patrick Fic
bee078fe18 Add user definted rates to labor misc for autohouse. 2022-12-20 12:38:41 -08:00
650 changed files with 49610 additions and 96586 deletions

View File

@@ -2,21 +2,23 @@ version: 2.1
orbs:
#snyk: snyk/snyk@0.0.8
#cypress: cypress-io/cypress@1.23.0
aws-s3: circleci/aws-s3@4.0.0
eb: circleci/aws-elastic-beanstalk@2.0.1
aws-s3: circleci/aws-s3@2.0.0
eb: circleci/aws-elastic-beanstalk@1.0.2
jira: circleci/jira@1.3.1
jobs:
api-deploy:
docker:
- image: cimg/node:18.18.2
- image: "cimg/base:stable"
steps:
- checkout
- eb/setup
- run:
command: |
eb init imex-online-production-api -r ca-central-1 -p "Node.js 18 running on 64bit Amazon Linux 2"
eb init imex-online-production-api -r ca-central-1 -p "Node.js 16 running on 64bit Amazon Linux 2"
eb status --verbose
eb deploy
eb status
- jira/notify
hasura-migrate:
docker:
@@ -46,36 +48,89 @@ jobs:
steps:
- checkout:
path: ~/repo
- restore_cache:
name: Restore Yarn Package Cache
keys:
- yarn-packages-{{ checksum "yarn.lock" }}
- run:
name: Install Dependencies
command: npm i
command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
- save_cache:
name: Save Yarn Package Cache
key: yarn-packages-{{ checksum "yarn.lock" }}
paths:
- ~/.cache/yarn
- run: npm run build
- run: yarn run build
- aws-s3/sync:
from: build
to: "s3://imex-online-production/"
arguments: "--exclude '*.map'"
app-beta-build:
- jira/notify
rome-api-deploy:
docker:
- image: cimg/node:18.18.2
resource_class: xlarge
- image: "cimg/base:stable"
steps:
- checkout
- eb/setup
- run:
command: |
eb init romeonline-productionapi -r us-east-2 -p "Node.js 16 running on 64bit Amazon Linux 2"
eb status --verbose
eb deploy
eb status
- jira/notify
rome-hasura-migrate:
docker:
- image: cimg/node:16.15.0
parameters:
secret:
type: string
default: $HASURA_PROD_SECRET
working_directory: ~/repo/hasura
steps:
- checkout:
path: ~/repo
- run:
name: Execute migration
command: |
npm install hasura-cli -g
hasura migrate apply --endpoint https://db.romeonline.io/ --admin-secret << parameters.secret >>
hasura metadata apply --endpoint https://db.romeonline.io/ --admin-secret << parameters.secret >>
hasura metadata reload --endpoint https://db.romeonline.io/ --admin-secret << parameters.secret >>
rome-app-build:
docker:
- image: cimg/node:16.15.0
working_directory: ~/repo/client
steps:
- checkout:
path: ~/repo
- restore_cache:
name: Restore Yarn Package Cache
keys:
- yarn-packages-{{ checksum "yarn.lock" }}
- run:
name: Install Dependencies
command: npm i
command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
- save_cache:
name: Save Yarn Package Cache
key: yarn-packages-{{ checksum "yarn.lock" }}
paths:
- ~/.cache/yarn
- run: npm run build
- run: yarn run build
- aws-s3/sync:
from: build
to: "s3://imex-online-beta/"
arguments: "--exclude '*.map'"
to: "s3://rome-online-production/"
- jira/notify
test-hasura-migrate:
docker:
@@ -106,37 +161,26 @@ jobs:
steps:
- checkout:
path: ~/repo
- restore_cache:
name: Restore Yarn Package Cache
keys:
- yarn-packages-{{ checksum "yarn.lock" }}
- run:
name: Install Dependencies
command: npm i
command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
- save_cache:
name: Save Yarn Package Cache
key: yarn-packages-{{ checksum "yarn.lock" }}
paths:
- ~/.cache/yarn
- run: npm run build:test
- run: yarn run build:test
- aws-s3/sync:
from: build
to: "s3://imex-online-test/"
arguments: "--exclude '*.map'"
test-app-beta-build:
docker:
- image: cimg/node:18.18.2
resource_class: snaptsoft/pfic
working_directory: ~/repo/client
steps:
- checkout:
path: ~/repo
- run:
name: Install Dependencies
command: npm i
- run: npm run build:test
- aws-s3/sync:
from: build
to: "s3://imex-online-test-beta/"
arguments: "--exclude '*.map'"
- jira/notify
admin-app-build:
docker:
@@ -179,23 +223,28 @@ workflows:
filters:
branches:
only: master
- app-beta-build:
filters:
branches:
only: master-beta
- hasura-migrate:
secret: ${HASURA_PROD_SECRET}
filters:
branches:
only: master
- rome-api-deploy:
filters:
branches:
only: rome/master
- rome-app-build:
filters:
branches:
only: rome/master
- rome-hasura-migrate:
secret: ${HASURA_PROD_SECRET}
filters:
branches:
only: rome/master
- test-app-build:
filters:
branches:
only: test
- test-app-beta-build:
filters:
branches:
only: test-beta
- test-hasura-migrate:
secret: ${HASURA_TEST_SECRET}
filters:

4
.gitignore vendored
View File

@@ -117,6 +117,4 @@ logs/oAuthClient-log.log
.node-persist/**
/*.env.*
.idea/*
.idea
/*.env.*

1
.npmrc
View File

@@ -1 +0,0 @@
legacy-peer-deps=true

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,14 @@
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
VITE_APP_GA_CODE=231099835
VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
VITE_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test
VITE_APP_CLOUDINARY_API_KEY=957865933348715
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
REACT_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
REACT_APP_GA_CODE=231099835
REACT_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"}
REACT_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
REACT_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test
REACT_APP_CLOUDINARY_API_KEY=957865933348715
REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
REACT_APP_FIREBASE_PUBLIC_VAPID_KEY='BP1B7ZTYpn-KMt6nOxlld6aS8Skt3Q7ZLEqP0hAvGHxG4UojPYiXZ6kPlzZkUC5jH-EcWXomTLtmadAIxurfcHo'
REACT_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
REACT_APP_AXIOS_BASE_API_URL=http://localhost:4000
REACT_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
REACT_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
REACT_APP_COUNTRY=USA

View File

@@ -1,14 +1,13 @@
GENERATE_SOURCEMAP=true
REACT_APP_GRAPHQL_ENDPOINT=https://db.imex.online/v1/graphql
REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.imex.online/v1/graphql
REACT_APP_GRAPHQL_ENDPOINT=https://db.romeonline.io/v1/graphql
REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.romeonline.io/v1/graphql
REACT_APP_GA_CODE=231103507
REACT_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDSezy-jGJreo7ulgpLdlpOwAOrgcaEkhU","authDomain":"imex-prod.firebaseapp.com","databaseURL":"https://imex-prod.firebaseio.com","projectId":"imex-prod","storageBucket":"imex-prod.appspot.com","messagingSenderId":"253497221485","appId":"1:253497221485:web:3c81c483b94db84b227a64","measurementId":"G-NTWBKG2L0M"}
REACT_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"}
REACT_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/bodyshop
REACT_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/bodyshop
REACT_APP_CLOUDINARY_API_KEY=473322739956866
REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
REACT_APP_FIREBASE_PUBLIC_VAPID_KEY='BMgZT1NZztW2DsJl8Mg2L04hgY9FzAg6b8fbzgNAfww2VDzH3VE63Ot9EaP_U7KWS2JT-7HPHaw0T_Tw_5vkZc8'
REACT_APP_FIREBASE_PUBLIC_VAPID_KEY='BP1B7ZTYpn-KMt6nOxlld6aS8Skt3Q7ZLEqP0hAvGHxG4UojPYiXZ6kPlzZkUC5jH-EcWXomTLtmadAIxurfcHo'
REACT_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
REACT_APP_AXIOS_BASE_API_URL=https://api.imex.online/
REACT_APP_REPORTS_SERVER_URL=https://reports.imex.online
REACT_APP_AXIOS_BASE_API_URL=https://api.romeonline.io/
REACT_APP_REPORTS_SERVER_URL=https://reports.romeonline.io
REACT_APP_SPLIT_API=et9pjkik6bn67he5evpmpr1agoo7gactphgk

View File

@@ -8,7 +8,7 @@ REACT_APP_CLOUDINARY_API_KEY=473322739956866
REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
REACT_APP_FIREBASE_PUBLIC_VAPID_KEY='BN2GcDPjipR5MTEosO5dT4CfQ3cmrdBIsI4juoOQrRijn_5aRiHlwj1mlq0W145mOusx6xynEKl_tvYJhpCc9lo'
REACT_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
REACT_APP_AXIOS_BASE_API_URL=https://api.test.imex.online/
REACT_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
REACT_APP_AXIOS_BASE_API_URL=https://api.romeonline.io/
REACT_APP_REPORTS_SERVER_URL=https://reports.romeonline.io
REACT_APP_IS_TEST=true
REACT_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc

3
client/.gitignore vendored
View File

@@ -1,3 +0,0 @@
# Sentry Config File
.sentryclirc

View File

@@ -1 +0,0 @@
legacy-peer-deps=true

View File

@@ -1,68 +1,73 @@
// craco.config.js
const TerserPlugin = require("terser-webpack-plugin");
const CracoLessPlugin = require("craco-less");
const {convertLegacyToken} = require('@ant-design/compatible/lib');
const {theme} = require('antd/lib');
const {defaultAlgorithm, defaultSeed} = theme;
const mapToken = defaultAlgorithm(defaultSeed);
const v4Token = convertLegacyToken(mapToken);
// TODO, At the moment we are using less in the Dashboard. Once we remove this we can remove the less processor entirely.
const SentryWebpackPlugin = require("@sentry/webpack-plugin");
module.exports = {
plugins: [
// {
// plugin: SentryWebpackPlugin,
// options: {
// // sentry-cli configuration
// authToken:
// "6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260",
// org: "snapt-software",
// project: "imexonline",
// release: process.env.REACT_APP_GIT_SHA,
//
// // webpack-specific configuration
// include: ".",
// ignore: ["node_modules", "webpack.config.js"],
// },
// },
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: {...v4Token},
javascriptEnabled: true,
},
},
},
},
],
webpack: {
configure: (webpackConfig) => {
return {
...webpackConfig,
// Required for Dev Server
devServer: {
...webpackConfig.devServer,
allowedHosts: 'all',
},
optimization: {
...webpackConfig.optimization,
// Workaround for CircleCI bug caused by the number of CPUs shown
// https://github.com/facebook/create-react-app/issues/8320
minimizer: webpackConfig.optimization.minimizer.map((item) => {
if (item instanceof TerserPlugin) {
item.options.parallel = 2;
}
plugins: [
{
plugin: SentryWebpackPlugin,
options: {
// sentry-cli configuration
authToken:
"6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260",
org: "snapt-software",
project: "rome-online",
release: process.env.REACT_APP_GIT_SHA,
return item;
}),
},
};
},
// webpack-specific configuration
include: ".",
ignore: ["node_modules", "webpack.config.js"],
},
},
devtool: "source-map",
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: {
...(process.env.NODE_ENV === "development"
? { "@primary-color": "#B22234" }
: {
//"@primary-color": "#1DA57A"
}),
// "@primary-color": " #1890ff", // primary color for all components
// "@link-color": "#1890ff", // link color
// "@success-color": "#52c41a", // success state color
// "@warning-color": "#faad14", // warning state color
// "@error-color": "#f5222d", // error state color
// "@font-size-base": "14px", // major text font size
// " @heading-color": "rgba(0, 0, 0, 0.85)", // heading text color
// "@text-color": "rgba(0, 0, 0, 0.65)", // major text color
// "@text-color-secondary": "rgba(0, 0, 0, 0.45)", // secondary text color
// "@disabled-color": "rgba(0, 0, 0, 0.25)", // disable state color
// "@border-radius-base": "2px", // major border radius
// "@border-color-base": "#d9d9d9", // major border color
// "@box-shadow-base":
// "0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08),0 9px 28px 8px rgba(0, 0, 0, 0.05); // major shadow for layers }",
},
javascriptEnabled: true,
},
},
},
},
],
webpack: {
configure: (webpackConfig) => ({
...webpackConfig,
optimization: {
...webpackConfig.optimization,
// Workaround for CircleCI bug caused by the number of CPUs shown
// https://github.com/facebook/create-react-app/issues/8320
minimizer: webpackConfig.optimization.minimizer.map((item) => {
if (item instanceof TerserPlugin) {
item.options.parallel = 2;
}
return item;
}),
},
}),
},
devtool: "source-map",
};

View File

@@ -1,17 +0,0 @@
const { defineConfig } = require('cypress')
module.exports = defineConfig({
experimentalStudio: true,
env: {
FIREBASE_USERNAME: 'cypress@imex.test',
FIREBASE_PASSWORD: 'cypress',
},
e2e: {
// We've imported your old cypress plugins here.
// You may want to clean this up later by importing these.
setupNodeEvents(on, config) {
return require('./cypress/plugins/index.js')(on, config)
},
baseUrl: 'http://localhost:3000',
},
})

8
client/cypress.json Normal file
View File

@@ -0,0 +1,8 @@
{
"baseUrl": "http://localhost:3000",
"experimentalStudio": true,
"env": {
"FIREBASE_USERNAME": "cypress@imex.test",
"FIREBASE_PASSWORD": "cypress"
}
}

View File

@@ -8872,13 +8872,13 @@
│ ├─ email: luis@luisrudge.net
│ ├─ path: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-flexbugs-fixes
│ └─ licenseFile: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-flexbugs-fixes/LICENSE
├─ postcss-focus-open@4.0.0
├─ postcss-focus-visible@4.0.0
│ ├─ licenses: CC0-1.0
│ ├─ repository: https://github.com/jonathantneal/postcss-focus-open
│ ├─ repository: https://github.com/jonathantneal/postcss-focus-visible
│ ├─ publisher: Jonathan Neal
│ ├─ email: jonathantneal@hotmail.com
│ ├─ path: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-open
│ └─ licenseFile: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-open/LICENSE.md
│ ├─ path: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-visible
│ └─ licenseFile: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-visible/LICENSE.md
├─ postcss-focus-within@3.0.0
│ ├─ licenses: CC0-1.0
│ ├─ repository: https://github.com/jonathantneal/postcss-focus-within

16334
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,112 +1,100 @@
{
"name": "bodyshop",
"version": "0.2.1",
"engines": {
"node": "18.18.2"
},
"type": "commonjs",
"private": true,
"proxy": "http://localhost:4000",
"dependencies": {
"@ant-design/compatible": "^5.1.2",
"@ant-design/pro-layout": "^7.17.16",
"@apollo/client": "^3.9.0",
"@apollo/client": "^3.7.9",
"@asseinfo/react-kanban": "^2.2.0",
"@craco/craco": "^7.1.0",
"@fingerprintjs/fingerprintjs": "^4.2.2",
"@craco/craco": "^7.0.0",
"@fingerprintjs/fingerprintjs": "^3.3.3",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.1.0",
"@sentry/cli": "^2.27.0",
"@sentry/react": "^7.99.0",
"@sentry/tracing": "^7.99.0",
"@splitsoftware/splitio-react": "^1.11.0",
"@tanem/react-nprogress": "^5.0.51",
"@vitejs/plugin-legacy": "^5.3.0",
"@vitejs/plugin-react": "^4.2.1",
"@vitejs/plugin-react-refresh": "^1.3.6",
"@vitejs/plugin-react-swc": "^3.6.0",
"antd": "^5.13.3",
"@sentry/react": "^7.40.0",
"@sentry/tracing": "^7.40.0",
"@splitsoftware/splitio-react": "^1.8.1",
"@tanem/react-nprogress": "^5.0.8",
"antd": "^4.24.8",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0",
"axios": "^1.6.7",
"consola": "^3.2.3",
"dayjs": "^1.11.10",
"dayjs-business-days2": "^1.2.2",
"axios": "^1.3.4",
"craco-less": "^2.0.0",
"dinero.js": "^1.9.1",
"dotenv": "^16.4.1",
"dotenv": "^16.0.1",
"enquire-js": "^0.2.1",
"env-cmd": "^10.1.0",
"esbuild": "^0.20.0",
"exifr": "^7.1.3",
"firebase": "^10.7.2",
"firebase": "^9.17.1",
"graphql": "^16.6.0",
"i18next": "^23.8.1",
"i18next-browser-languagedetector": "^7.0.2",
"jsoneditor": "^10.0.0",
"i18next": "^22.4.10",
"i18next-browser-languagedetector": "^7.0.1",
"jsoneditor": "^9.9.0",
"jsreport-browser-client-dist": "^1.3.0",
"libphonenumber-js": "^1.10.54",
"logrocket": "^7.0.0",
"markerjs2": "^2.32.0",
"libphonenumber-js": "^1.10.21",
"logrocket": "^3.0.1",
"markerjs2": "^2.28.1",
"moment-business-days": "^1.2.0",
"moment-timezone": "^0.5.41",
"normalize-url": "^8.0.0",
"phone": "^3.1.42",
"phone": "^3.1.35",
"preval.macro": "^5.0.0",
"prop-types": "^15.8.1",
"query-string": "^8.1.0",
"query-string": "^7.1.3",
"rc-queue-anim": "^2.0.0",
"rc-scroll-anim": "^2.7.6",
"react": "^18.2.0",
"react-big-calendar": "^1.8.7",
"react": "^17.0.2",
"react-big-calendar": "^1.6.8",
"react-color": "^2.19.3",
"react-cookie": "^7.0.2",
"react-dom": "^18.2.0",
"react-drag-listview": "^2.0.0",
"react-cookie": "^4.1.1",
"react-dom": "^17.0.2",
"react-drag-listview": "^0.2.1",
"react-grid-gallery": "^1.0.0",
"react-grid-layout": "1.3.4",
"react-i18next": "^14.0.1",
"react-icons": "^5.0.1",
"react-grid-layout": "^1.3.4",
"react-i18next": "^12.2.0",
"react-icons": "^4.7.1",
"react-image-lightbox": "^5.1.4",
"react-intersection-observer": "^9.5.3",
"react-markdown": "^9.0.1",
"react-number-format": "^5.1.4",
"react-redux": "^9.1.0",
"react-resizable": "^3.0.5",
"react-router-dom": "^6.21.2",
"react-intersection-observer": "^9.4.3",
"react-number-format": "^5.1.3",
"react-redux": "^8.0.5",
"react-resizable": "^3.0.4",
"react-router-dom": "^5.3.0",
"react-scripts": "^5.0.1",
"react-sticky": "^6.0.3",
"react-sublime-video": "^0.2.5",
"react-virtualized": "^9.22.5",
"recharts": "^2.11.0",
"redux": "^5.0.1",
"react-virtualized": "^9.22.3",
"recharts": "^2.4.3",
"redux": "^4.2.1",
"redux-persist": "^6.0.0",
"redux-saga": "^1.3.0",
"redux-saga": "^1.2.2",
"redux-state-sync": "^3.1.4",
"reselect": "^5.1.0",
"sass": "^1.70.0",
"socket.io-client": "^4.7.4",
"styled-components": "^6.1.8",
"reselect": "^4.1.7",
"sass": "^1.58.3",
"socket.io-client": "^4.6.1",
"styled-components": "^5.3.6",
"subscriptions-transport-ws": "^0.11.0",
"terser-webpack-plugin": "^5.3.10",
"vite-plugin-svgr": "^4.2.0",
"web-vitals": "^3.5.2",
"workbox-core": "^7.0.0",
"workbox-expiration": "^7.0.0",
"workbox-navigation-preload": "^7.0.0",
"workbox-precaching": "^7.0.0",
"workbox-routing": "^7.0.0",
"workbox-strategies": "^7.0.0",
"web-vitals": "^2.1.4",
"workbox-background-sync": "^6.5.3",
"workbox-broadcast-update": "^6.5.3",
"workbox-cacheable-response": "^6.5.3",
"workbox-core": "^6.5.3",
"workbox-expiration": "^6.5.3",
"workbox-google-analytics": "^6.5.3",
"workbox-navigation-preload": "^6.5.3",
"workbox-precaching": "^6.5.3",
"workbox-range-requests": "^6.5.3",
"workbox-routing": "^6.5.3",
"workbox-strategies": "^6.5.3",
"workbox-streams": "^6.5.3",
"yauzl": "^2.10.0"
},
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "vite",
"build": "cross-env-shell VITE_APP_GIT_SHA=\\\"`git rev-parse --short HEAD`\\\" vite build && npm run sentry:sourcemaps",
"build:test": "env-cmd -f .env.test npm run build",
"build-deploy:test": "npm run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'",
"buildcra": "cross-env-shell VITE_APP_GIT_SHA=\\\"`git rev-parse --short HEAD`\\\" vite build",
"start": "craco start",
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
"build:test": "env-cmd -f .env.test yarn run build",
"build-deploy:test": "yarn run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'",
"buildcra": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
"test": "cypress open",
"eject": "react-scripts eject",
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .",
"eulaize": "node src/utils/eulaize.js",
"sentry:sourcemaps": "sentry-cli sourcemaps inject --org imex --project imexonline ./build && sentry-cli sourcemaps upload --org imex --project imexonline ./build"
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular ."
},
"eslintConfig": {
"extends": [
@@ -131,29 +119,12 @@
"react-error-overlay": "6.0.9"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-react": "^7.23.3",
"@emotion/babel-plugin": "^11.11.0",
"@emotion/react": "^11.11.3",
"@sentry/webpack-plugin": "^2.10.3",
"@swc/core": "^1.3.107",
"@swc/plugin-styled-components": "^1.5.108",
"@testing-library/cypress": "^10.0.1",
"browserslist": "^4.22.3",
"browserslist-to-esbuild": "^2.1.1",
"craco-less": "^3.0.1",
"cross-env": "^7.0.3",
"cypress": "^13.6.4",
"eslint-plugin-cypress": "^2.15.1",
"memfs": "^4.6.0",
"os-browserify": "^0.3.0",
"@sentry/webpack-plugin": "^1.20.0",
"@testing-library/cypress": "^8.0.3",
"cypress": "^10.3.1",
"eslint-plugin-cypress": "^2.12.1",
"react-error-overlay": "6.0.11",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.3",
"vite": "^5.0.11",
"vite-plugin-babel": "^1.2.0",
"vite-plugin-legacy": "^2.1.0",
"vite-plugin-node-polyfills": "^0.19.0",
"vite-plugin-style-import": "^2.0.0"
"source-map-explorer": "^2.5.2"
}
}

View File

@@ -190,7 +190,7 @@ This package contains the following license and notice below:
# @firebase/logger
This package serves as the base of all logging in the JS SDK. Any logging that
is intended to be open to Firebase end developers should go through this
is intended to be visible to Firebase end developers should go through this
module.
## Basic Usage
@@ -9375,7 +9375,7 @@ parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently open
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the

View File

@@ -1029,7 +1029,7 @@ The following NPM packages may be included in this product:
- postcss-dir-pseudo-class@5.0.0
- postcss-double-position-gradients@1.0.0
- postcss-env-function@2.0.2
- postcss-focus-open@4.0.0
- postcss-focus-visible@4.0.0
- postcss-focus-within@3.0.0
- postcss-gap-properties@2.0.0
- postcss-image-set-function@3.0.1
@@ -1699,7 +1699,7 @@ This package contains the following license and notice below:
# @firebase/logger
This package serves as the base of all logging in the JS SDK. Any logging that
is intended to be open to Firebase end developers should go through this
is intended to be visible to Firebase end developers should go through this
module.
## Basic Usage
@@ -24029,7 +24029,7 @@ parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently open
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the

View File

@@ -28,6 +28,17 @@ switch (this.location.hostname) {
// measurementId: "${config.measurementId}",
};
break;
case "romeonline.io":
firebaseConfig = {
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",
};
break;
case "imex.online":
default:
firebaseConfig = {

View File

@@ -2,23 +2,31 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.png" />
<link rel="icon" href="%PUBLIC_URL%/ro-favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#002366" />
<meta name="description" content="ImEX Online" />
<meta name="description" content="Rome Online" />
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
<link rel="apple-touch-icon" href="public/logo192.png" />
<link rel="apple-touch-icon" href="logo192.png" />
<script type="text/javascript">
window.$crisp = [];
window.CRISP_WEBSITE_ID = "36724f62-2eb0-4b29-9cdd-9905fb99913e";
(function () {
d = document;
s = d.createElement("script");
s.src = "https://client.crisp.chat/l.js";
s.async = 1;
d.getElementsByTagName("head")[0].appendChild(s);
})();
var $zoho = $zoho || {};
$zoho.salesiq = $zoho.salesiq || {
widgetcode:
"2ee4b2212fbdb380fb1e5e612f1e2dd7fe52032bee013140e27458e960add8e65b3cc65a44e7ecddabee40ced28dcfbd",
values: {},
ready: function () {},
};
var d = document;
s = d.createElement("script");
s.type = "text/javascript";
s.id = "zsiqscript";
s.defer = true;
s.src = "https://salesiq.zoho.com/widget";
t = d.getElementsByTagName("script")[0];
t.parentNode.insertBefore(s, t);
d.write("<div id='zsiqwidget'></div>");
</script>
<script>
!(function () {
"use strict";
@@ -67,7 +75,7 @@
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="/manifest.json" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
@@ -77,12 +85,10 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>ImEX Online</title>
<title>Rome Online</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="src/index.js"></script>
</body>
</html>

View File

@@ -1,6 +1,6 @@
{
"short_name": "ImEX Online",
"name": "ImEX Online",
"short_name": "Rome Online",
"name": "Rome Online",
"description": "The ultimate bodyshop management system.",
"icons": [
{

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,53 +1,50 @@
import {ApolloProvider} from "@apollo/client";
import {SplitFactoryProvider, SplitSdk,} from '@splitsoftware/splitio-react';
import {ConfigProvider} from "antd";
import { ApolloProvider } from "@apollo/client";
import { SplitFactory, SplitSdk } from "@splitsoftware/splitio-react";
import { ConfigProvider } from "antd";
import enLocale from "antd/es/locale/en_US";
import dayjs from "../utils/day";
import 'dayjs/locale/en';
import moment from "moment";
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 client from "../utils/GraphQLClient";
import App from "./App";
import * as Sentry from "@sentry/react";
import themeProvider from "./themeProvider";
moment.locale("en-US");
dayjs.locale("en");
export const factory = SplitSdk({
core: {
authorizationKey: process.env.REACT_APP_SPLIT_API,
key: "anon",
},
});
const config = {
core: {
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
key: "anon",
},
};
export const factory = SplitSdk(config);
export default function AppContainer() {
const { t } = useTranslation();
function AppContainer() {
const {t} = useTranslation();
return (
<ApolloProvider client={client}>
<ConfigProvider
//componentSize="small"
input={{autoComplete: "new-password"}}
locale={enLocale}
theme={themeProvider}
form={{
validateMessages: {
// eslint-disable-next-line no-template-curly-in-string
required: t("general.validation.required", {label: "${label}"}),
},
}}
>
<GlobalLoadingBar/>
<SplitFactoryProvider factory={factory}>
<App/>
</SplitFactoryProvider>
</ConfigProvider>
</ApolloProvider>
);
return (
<ApolloProvider client={client}>
<ConfigProvider
//componentSize="small"
input={{ autoComplete: "new-password" }}
locale={enLocale}
theme={{
token: {
colorPrimary: "#326ade",
colorInfo: "#326ade"
},
}}
form={{
validateMessages: {
// eslint-disable-next-line no-template-curly-in-string
required: t("general.validation.required", { label: "${label}" }),
},
}}
>
<GlobalLoadingBar />
<SplitFactory factory={factory}>
<App />
</SplitFactory>
</ConfigProvider>
</ApolloProvider>
);
}
export default Sentry.withProfiler(AppContainer);

View File

@@ -1,156 +1,161 @@
import {useSplitClient} from "@splitsoftware/splitio-react";
import {Button, Result} from "antd";
import { useClient } from "@splitsoftware/splitio-react";
import { Button, Result } from "antd";
import LogRocket from "logrocket";
import React, {lazy, Suspense, useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {Route, Routes} from "react-router-dom";
import {createStructuredSelector} from "reselect";
import React, { lazy, Suspense, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Route, Switch } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import DocumentEditorContainer from "../components/document-editor/document-editor.container";
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
//Component Imports
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
import DisclaimerPage from "../pages/disclaimer/disclaimer.page";
import LandingPage from "../pages/landing/landing.page";
import TechPageContainer from "../pages/tech/tech.page.container";
import {setOnline} from "../redux/application/application.actions";
import {selectOnline} from "../redux/application/application.selectors";
import {checkUserSession} from "../redux/user/user.actions";
import {selectBodyshop, selectCurrentEula, selectCurrentUser,} from "../redux/user/user.selectors";
import PrivateRoute from "../components/PrivateRoute";
import { setOnline } from "../redux/application/application.actions";
import { selectOnline } from "../redux/application/application.selectors";
import { checkUserSession } from "../redux/user/user.actions";
import {
selectBodyshop,
selectCurrentUser,
} from "../redux/user/user.selectors";
import PrivateRoute from "../utils/private-route";
import "./App.styles.scss";
import handleBeta from "../utils/betaHandler";
import Eula from "../components/eula/eula.component";
const ResetPassword = lazy(() =>
import("../pages/reset-password/reset-password.component")
import("../pages/reset-password/reset-password.component")
);
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
const MobilePaymentContainer = lazy(() =>
import("../pages/mobile-payment/mobile-payment.container")
import("../pages/mobile-payment/mobile-payment.container")
);
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
online: selectOnline,
bodyshop: selectBodyshop,
currentEula: selectCurrentEula
currentUser: selectCurrentUser,
online: selectOnline,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
checkUserSession: () => dispatch(checkUserSession()),
setOnline: (isOnline) => dispatch(setOnline(isOnline)),
checkUserSession: () => dispatch(checkUserSession()),
setOnline: (isOnline) => dispatch(setOnline(isOnline)),
});
export function App({bodyshop, checkUserSession, currentUser, online, setOnline, currentEula}) {
const client = useSplitClient().client;
const [listenersAdded, setListenersAdded] = useState(false)
const {t} = useTranslation();
export function App({
bodyshop,
checkUserSession,
currentUser,
online,
setOnline,
}) {
const client = useClient();
useEffect(() => {
if (!navigator.onLine) {
setOnline(false);
}
checkUserSession();
}, [checkUserSession, setOnline]);
//const b = Grid.useBreakpoint();
// console.log("Breakpoints:", b);
// Associate event listeners, memoize to prevent multiple listeners being added
useEffect(() => {
const offlineListener = (e) => {
setOnline(false);
}
const onlineListener = (e) => {
setOnline(true);
}
if (!listenersAdded) {
console.log('Added events for offline and online');
window.addEventListener("offline", offlineListener);
window.addEventListener("online", onlineListener);
setListenersAdded(true);
}
return () => {
window.removeEventListener("offline", offlineListener);
window.removeEventListener("online", onlineListener);
}
}, [setOnline, listenersAdded]);
useEffect(() => {
if (currentUser.authorized && bodyshop) {
client.setAttribute("imexshopid", bodyshop.imexshopid);
if (
client.getTreatment("LogRocket_Tracking") === "on" ||
window.location.hostname === 'beta.imex.online'
) {
console.log("LR Start");
LogRocket.init("gvfvfw/bodyshopapp");
}
}
}, [bodyshop, client, currentUser.authorized]);
if (currentUser.authorized === null) {
return <LoadingSpinner message={t("general.labels.loggingin")}/>;
useEffect(() => {
if (!navigator.onLine) {
setOnline(false);
}
handleBeta();
checkUserSession();
}, [checkUserSession, setOnline]);
if (!online)
return (
<Result
status="warning"
title={t("general.labels.nointernet")}
subTitle={t("general.labels.nointernet_sub")}
extra={
<Button
type="primary"
onClick={() => {
window.location.reload();
}}
>
{t("general.actions.refresh")}
</Button>
}
/>
);
//const b = Grid.useBreakpoint();
// console.log("Breakpoints:", b);
if (currentEula && !currentUser.eulaIsAccepted) {
return <Eula/>
const { t } = useTranslation();
window.addEventListener("offline", function (e) {
setOnline(false);
});
window.addEventListener("online", function (e) {
setOnline(true);
});
useEffect(() => {
if (currentUser.authorized && bodyshop) {
client.setAttribute("imexshopid", bodyshop.imexshopid);
LogRocket.init("rome-online/rome-online");
if (client.getTreatment("LogRocket_Tracking") === "on") {
LogRocket.init("rome-online/rome-online");
}
}
}, [bodyshop, client, currentUser.authorized]);
// Any route that is not assigned and matched will default to the Landing Page component
if (currentUser.authorized === null) {
return <LoadingSpinner message={t("general.labels.loggingin")} />;
}
if (!online)
return (
<Suspense fallback={<LoadingSpinner message="ImEX Online"/>}>
<Routes>
<Route path="*" element={<ErrorBoundary><LandingPage/></ErrorBoundary>}/>
<Route path="/signin" element={<ErrorBoundary><SignInPage/></ErrorBoundary>}/>
<Route path="/resetpassword" element={<ErrorBoundary><ResetPassword/></ErrorBoundary>}/>
<Route path="/csi/:surveyId" element={<ErrorBoundary><CsiPage/></ErrorBoundary>}/>
<Route path="/disclaimer" element={<ErrorBoundary><DisclaimerPage/></ErrorBoundary>}/>
<Route path="/mp/:paymentIs" element={<ErrorBoundary><MobilePaymentContainer/></ErrorBoundary>}/>
<Route path="/manage/*"
element={<ErrorBoundary><PrivateRoute isAuthorized={currentUser.authorized}/></ErrorBoundary>}>
<Route path="*" element={<ManagePage/>}/>
</Route>
<Route path="/tech/*"
element={<ErrorBoundary><PrivateRoute isAuthorized={currentUser.authorized}/></ErrorBoundary>}>
<Route path="*" element={<TechPageContainer/>}/>
</Route>
<Route path="/edit/*" element={<PrivateRoute isAuthorized={currentUser.authorized}/>}>
<Route path="*" element={<DocumentEditorContainer/>}/>
</Route>
</Routes>
</Suspense>
<Result
status="warning"
title={t("general.labels.nointernet")}
subTitle={t("general.labels.nointernet_sub")}
extra={
<Button
type="primary"
onClick={() => {
window.location.reload();
}}
>
{t("general.actions.refresh")}
</Button>
}
/>
);
return (
<Switch>
<Suspense fallback={<LoadingSpinner />}>
<ErrorBoundary>
<Route exact path="/" component={LandingPage} />
</ErrorBoundary>
<ErrorBoundary>
<Route exact path="/signin" component={SignInPage} />
</ErrorBoundary>
<ErrorBoundary>
<Route exact path="/resetpassword" component={ResetPassword} />
</ErrorBoundary>
<ErrorBoundary>
<Route exact path="/csi/:surveyId" component={CsiPage} />
</ErrorBoundary>
<ErrorBoundary>
<Route exact path="/disclaimer" component={DisclaimerPage} />
</ErrorBoundary>
<ErrorBoundary>
<Route
exact
path="/mp/:paymentIs"
component={MobilePaymentContainer}
/>
</ErrorBoundary>
<ErrorBoundary>
<PrivateRoute
isAuthorized={currentUser.authorized}
path="/manage"
component={ManagePage}
/>
</ErrorBoundary>
<ErrorBoundary>
<PrivateRoute
isAuthorized={currentUser.authorized}
path="/tech"
component={TechPageContainer}
/>
</ErrorBoundary>
<ErrorBoundary>
<PrivateRoute
isAuthorized={currentUser.authorized}
path="/edit"
component={DocumentEditorContainer}
/>
</ErrorBoundary>
</Suspense>
</Switch>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(App);

View File

@@ -1,10 +1,6 @@
//Global Styles.
@import "react-big-calendar/lib/sass/styles";
.ant-menu-item-divider {
border-bottom: 1px solid #74695c !important;
}
.imex-table-header {
display: flex;
flex-wrap: wrap;
@@ -147,11 +143,19 @@
}
}
//Update row highlighting on production board.
.ant-table-tbody > tr.ant-table-row:hover > td {
background: #e7f3ff !important;
}
.job-line-manual {
color: tomato;
font-style: italic;
}
td.ant-table-column-sort {
background-color: transparent;
}
.ant-table-tbody > tr.ant-table-row:nth-child(2n) > td {
background-color: #f4f4f4;

View File

@@ -1,60 +0,0 @@
import {defaultsDeep} from "lodash";
import {theme} from "antd";
const {defaultAlgorithm, darkAlgorithm} = theme;
let isDarkMode = false;
/**
* Default theme
* @type {{components: {Menu: {itemDividerBorderColor: string}}}}
*/
const defaultTheme = {
components: {
Table: {
rowHoverBg: '#e7f3ff',
rowSelectedBg: '#e6f7ff',
headerSortHoverBg: 'transparent',
},
Menu: {
darkItemHoverBg: '#1677ff',
itemHoverBg: '#1677ff',
horizontalItemHoverBg: '#1677ff',
}
},
token: {
colorPrimary: '#1677ff'
}
};
/**
* Development theme
* @type {{components: {Menu: {itemHoverBg: string, darkItemHoverBg: string, horizontalItemHoverBg: string}}, token: {colorPrimary: string}}}
*/
const devTheme = {
components: {
Menu: {
darkItemHoverBg: '#a51d1d',
itemHoverBg: '#a51d1d',
horizontalItemHoverBg: '#a51d1d',
}
},
token: {
colorPrimary: '#a51d1d'
}
};
/**
* Production theme
* @type {{components: {Menu: {itemHoverBg: string, darkItemHoverBg: string, horizontalItemHoverBg: string}}, token: {colorPrimary: string}}}
*/
const prodTheme = {};
const currentTheme = import.meta.env.DEV ? devTheme
: prodTheme;
const finaltheme = {
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
...defaultsDeep(currentTheme, defaultTheme)
}
export default finaltheme;

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@@ -1,17 +0,0 @@
import React, {useEffect} from "react";
import {Outlet, useLocation, useNavigate} from "react-router-dom";
function PrivateRoute({component: Component, isAuthorized, ...rest}) {
const location = useLocation();
const navigate = useNavigate();
useEffect(() => {
if (!isAuthorized) {
navigate(`/signin?redirect=${location.pathname}`);
}
}, [isAuthorized, navigate,location]);
return <Outlet/>;
}
export default PrivateRoute;

View File

@@ -15,7 +15,6 @@ import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
import {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -211,7 +210,7 @@ export function AccountingPayablesTableComponent({
<Table
loading={loading}
dataSource={dataSource}
pagination={{ position: "top", pageSize: pageLimit }}
pagination={{ position: "top", pageSize: 50 }}
columns={columns}
rowKey="id"
onChange={handleTableChange}

View File

@@ -15,7 +15,6 @@ import PaymentExportButton from "../payment-export-button/payment-export-button.
import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component";
import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -210,7 +209,7 @@ export function AccountingPayablesTableComponent({
<Table
loading={loading}
dataSource={dataSource}
pagination={{ position: "top", pageSize: pageLimit }}
pagination={{ position: "top", pageSize: 50 }}
columns={columns}
rowKey="id"
onChange={handleTableChange}

View File

@@ -2,5 +2,5 @@ import { Alert } from "antd";
import React from "react";
export default function AlertComponent(props) {
return <Alert {...props} />;
return <Alert {...props} />;
}

View File

@@ -61,7 +61,7 @@ export function AllocationsAssignmentComponent({
);
return (
<Popover content={popContent} open={visibility}>
<Popover content={popContent} visible={visibility}>
<Button onClick={() => setVisibility(true)}>
{t("allocations.actions.assign")}
</Button>

View File

@@ -59,7 +59,7 @@ export default connect(
);
return (
<Popover content={popContent} open={visibility}>
<Popover content={popContent} visible={visibility}>
<Button disabled={disabled} onClick={() => setVisibility(true)}>
{t("allocations.actions.assign")}
</Button>

View File

@@ -4,7 +4,6 @@ import { alphaSort } from "../../utils/sorters";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { useTranslation } from "react-i18next";
import AuditTrailValuesComponent from "../audit-trail-values/audit-trail-values.component";
import {pageLimit} from "../../utils/config";
export default function AuditTrailListComponent({ loading, data }) {
const [state, setState] = useState({
@@ -75,7 +74,7 @@ export default function AuditTrailListComponent({ loading, data }) {
<Table
{...formItemLayout}
loading={loading}
pagination={{ position: "top", defaultPageSize: pageLimit }}
pagination={{ position: "top", defaultPageSize: 25 }}
columns={columns}
rowKey="id"
dataSource={data}

View File

@@ -3,7 +3,6 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import {pageLimit} from "../../utils/config";
export default function EmailAuditTrailListComponent({ loading, data }) {
const [state, setState] = useState({
@@ -54,7 +53,7 @@ export default function EmailAuditTrailListComponent({ loading, data }) {
<Table
{...formItemLayout}
loading={loading}
pagination={{ position: "top", defaultPageSize: pageLimit }}
pagination={{ position: "top", defaultPageSize: 25 }}
columns={columns}
rowKey="id"
dataSource={data}

View File

@@ -1,6 +1,6 @@
import {useMutation, useQuery} from "@apollo/client";
import {Button, Form, Popconfirm, Space} from "antd";
import dayjs from "../../utils/day";
import { useMutation, useQuery } from "@apollo/client";
import { Button, Form, PageHeader, Popconfirm, Space } from "antd";
import moment from "moment";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -20,148 +20,148 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
import AlertComponent from "../alert/alert.component";
import BillFormContainer from "../bill-form/bill-form.container";
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
import BillPrintButton from "../bill-print-button/bill-print-button.component";
import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component";
import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import BillDetailEditReturn from "./bill-detail-edit-return.component";
import {PageHeader} from "@ant-design/pro-layout";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({context: context, modal: "partsOrder"})),
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
mapStateToProps,
mapDispatchToProps
)(BillDetailEditcontainer);
export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail, bodyshop,}) {
const search = queryString.parse(useLocation().search);
export function BillDetailEditcontainer({
setPartsOrderContext,
insertAuditTrail,
bodyshop,
}) {
const search = queryString.parse(useLocation().search);
const {t} = useTranslation();
const [form] = Form.useForm();
const [open, setOpen] = useState(false);
const [updateLoading, setUpdateLoading] = useState(false);
const [update_bill] = useMutation(UPDATE_BILL);
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
const { t } = useTranslation();
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
const [updateLoading, setUpdateLoading] = useState(false);
const [update_bill] = useMutation(UPDATE_BILL);
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
const {loading, error, data, refetch} = useQuery(QUERY_BILL_BY_PK, {
variables: {billid: search.billid},
skip: !!!search.billid,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
const { loading, error, data, refetch } = useQuery(QUERY_BILL_BY_PK, {
variables: { billid: search.billid },
skip: !!!search.billid,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const handleSave = () => {
//It's got a previously deducted bill line!
if (
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length >
0
)
setVisible(true);
else {
form.submit();
}
};
const handleFinish = async (values) => {
setUpdateLoading(true);
//let adjustmentsToInsert = {};
const { billlines, upload, ...bill } = values;
const updates = [];
updates.push(
update_bill({
variables: { billId: search.billid, bill: bill },
})
);
billlines.forEach((l) => {
delete l.selected;
});
// ... rest of the code remains the same
//Find bill lines that were deleted.
const deletedJobLines = [];
const handleSave = () => {
//It's got a previously deducted bill line!
if (
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length >
0
)
setOpen(true);
else {
form.submit();
}
};
data.bills_by_pk.billlines.forEach((a) => {
const matchingRecord = billlines.find((b) => b.id === a.id);
if (!matchingRecord) {
deletedJobLines.push(a);
}
});
const handleFinish = async (values) => {
setUpdateLoading(true);
//let adjustmentsToInsert = {};
deletedJobLines.forEach((d) => {
updates.push(deleteBillLine({ variables: { id: d.id } }));
});
const {billlines, upload, ...bill} = values;
const updates = [];
billlines.forEach((billline) => {
const { deductedfromlbr, inventories, jobline, ...il } = billline;
delete il.__typename;
if (il.id) {
updates.push(
update_bill({
variables: {billId: search.billid, bill: bill},
})
updateBillLine({
variables: {
billLineId: il.id,
billLine: {
...il,
deductedfromlbr: deductedfromlbr,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
},
})
);
} else {
//It's a new line, have to insert it.
updates.push(
insertBillLine({
variables: {
billLines: [
{
...il,
deductedfromlbr: deductedfromlbr,
billid: search.billid,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
],
},
})
);
}
});
billlines.forEach((l) => {
delete l.selected;
});
await Promise.all(updates);
//Find bill lines that were deleted.
const deletedJobLines = [];
insertAuditTrail({
jobid: bill.jobid,
billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number),
});
data.bills_by_pk.billlines.forEach((a) => {
const matchingRecord = billlines.find((b) => b.id === a.id);
if (!matchingRecord) {
deletedJobLines.push(a);
}
});
await refetch();
form.setFieldsValue(transformData(data));
form.resetFields();
setVisible(false);
setUpdateLoading(false);
};
deletedJobLines.forEach((d) => {
updates.push(deleteBillLine({variables: {id: d.id}}));
});
if (error) return <AlertComponent message={error.message} type="error" />;
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
billlines.forEach((billline) => {
const {deductedfromlbr, inventories, jobline, ...il} = billline;
delete il.__typename;
if (il.id) {
updates.push(
updateBillLine({
variables: {
billLineId: il.id,
billLine: {
...il,
deductedfromlbr: deductedfromlbr,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
},
})
);
} else {
//It's a new line, have to insert it.
updates.push(
insertBillLine({
variables: {
billLines: [
{
...il,
deductedfromlbr: deductedfromlbr,
billid: search.billid,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
],
},
})
);
}
});
await Promise.all(updates);
insertAuditTrail({
jobid: bill.jobid,
billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number),
});
await refetch();
form.setFieldsValue(transformData(data));
form.resetFields();
setOpen(false);
setUpdateLoading(false);
};
if (error) return <AlertComponent message={error.message} type="error"/>;
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
return (
<>
@@ -176,11 +176,11 @@ export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail,
extra={
<Space>
<BillDetailEditReturn data={data} />
<BillPrintButton billid={search.billid} />
<Popconfirm
open={open}
visible={visible}
onConfirm={() => form.submit()}
onCancel={() => setOpen(false)}
onCancel={() => setVisible(false)}
okButtonProps={{ loading: updateLoading }}
title={t("bills.labels.editadjwarning")}
>
@@ -207,45 +207,45 @@ export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail,
>
<BillFormContainer form={form} billEdit disabled={exported} />
{bodyshop.uselocalmediaserver ? (
<JobsDocumentsLocalGallery
job={{id: data ? data.bills_by_pk.jobid : null}}
invoice_number={data ? data.bills_by_pk.invoice_number : null}
vendorid={data ? data.bills_by_pk.vendorid : null}
/>
) : (
<JobDocumentsGallery
jobId={data ? data.bills_by_pk.jobid : null}
billId={search.billid}
documentsList={data ? data.bills_by_pk.documents : []}
billsCallback={refetch}
/>
)}
</Form>
</>
{bodyshop.uselocalmediaserver ? (
<JobsDocumentsLocalGallery
job={{ id: data ? data.bills_by_pk.jobid : null }}
invoice_number={data ? data.bills_by_pk.invoice_number : null}
vendorid={data ? data.bills_by_pk.vendorid : null}
/>
) : (
<JobDocumentsGallery
jobId={data ? data.bills_by_pk.jobid : null}
billId={search.billid}
documentsList={data ? data.bills_by_pk.documents : []}
billsCallback={refetch}
/>
)}
</Form>
</>
);
)}
</>
);
}
const transformData = (data) => {
return data
? {
...data.bills_by_pk,
return data
? {
...data.bills_by_pk,
billlines: data.bills_by_pk.billlines.map((i) => {
return {
...i,
joblineid: !!i.joblineid ? i.joblineid : "noline",
applicable_taxes: {
federal:
(i.applicable_taxes && i.applicable_taxes.federal) || false,
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
local: (i.applicable_taxes && i.applicable_taxes.local) || false,
},
};
}),
date: data.bills_by_pk ? dayjs(data.bills_by_pk.date) : null,
}
: {};
billlines: data.bills_by_pk.billlines.map((i) => {
return {
...i,
joblineid: !!i.joblineid ? i.joblineid : "noline",
applicable_taxes: {
federal:
(i.applicable_taxes && i.applicable_taxes.federal) || false,
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
local: (i.applicable_taxes && i.applicable_taxes.local) || false,
},
};
}),
date: data.bills_by_pk ? moment(data.bills_by_pk.date) : null,
}
: {};
};

View File

@@ -3,7 +3,7 @@ import queryString from "query-string";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation, useNavigate } from "react-router-dom";
import { useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { setModalContext } from "../../redux/modals/modals.actions";
@@ -33,10 +33,10 @@ export function BillDetailEditReturn({
disabled,
}) {
const search = queryString.parse(useLocation().search);
const history = useNavigate();
const history = useHistory();
const { t } = useTranslation();
const [form] = Form.useForm();
const [open, setOpen] = useState(false);
const [visible, setVisible] = useState(false);
const handleFinish = ({ billlines }) => {
const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id);
@@ -67,18 +67,18 @@ export function BillDetailEditReturn({
});
delete search.billid;
history({ search: queryString.stringify(search) });
setOpen(false);
history.push({ search: queryString.stringify(search) });
setVisible(false);
};
useEffect(() => {
if (open === false) form.resetFields();
}, [open, form]);
if (visible === false) form.resetFields();
}, [visible, form]);
return (
<>
<Modal
open={open}
onCancel={() => setOpen(false)}
visible={visible}
onCancel={() => setVisible(false)}
destroyOnClose
title={t("bills.actions.return")}
onOk={() => form.submit()}
@@ -175,7 +175,7 @@ export function BillDetailEditReturn({
<Button
disabled={data.bills_by_pk.is_credit_memo || disabled}
onClick={() => {
setOpen(true);
setVisible(true);
}}
>
{t("bills.actions.return")}

View File

@@ -1,12 +1,12 @@
import { Drawer, Grid } from "antd";
import queryString from "query-string";
import React from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { useHistory, useLocation } from "react-router-dom";
import BillDetailEditComponent from "./bill-detail-edit-component";
export default function BillDetailEditcontainer() {
const search = queryString.parse(useLocation().search);
const history = useNavigate();
const history = useHistory();
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
@@ -29,10 +29,10 @@ export default function BillDetailEditcontainer() {
width={drawerPercentage}
onClose={() => {
delete search.billid;
history({ search: queryString.stringify(search) });
history.push({ search: queryString.stringify(search) });
}}
destroyOnClose
open={search.billid}
visible={search.billid}
>
<BillDetailEditComponent />
</Drawer>

View File

@@ -1,18 +1,18 @@
import { useApolloClient, useMutation } from "@apollo/client";
import { Button, Checkbox, Form, Modal, Space, notification } from "antd";
import { Button, Checkbox, Form, Modal, notification, Space } from "antd";
import _ from "lodash";
import React, { useEffect, useMemo, useState } from "react";
import React, { useEffect, useState, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_NEW_BILL } from "../../graphql/bills.queries";
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
import {
QUERY_JOB_LBR_ADJUSTMENTS,
UPDATE_JOB,
} from "../../graphql/jobs.queries";
import { MUTATION_MARK_RETURN_RECEIVED } from "../../graphql/parts-orders.queries";
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
@@ -20,15 +20,15 @@ import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import confirmDialog from "../../utils/asyncConfirm";
import useLocalStorage from "../../utils/useLocalStorage";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import BillFormContainer from "../bill-form/bill-form.container";
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
import { handleUpload } from "../documents-upload/documents-upload.utility";
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
import useLocalStorage from "../../utils/useLocalStorage";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
const mapStateToProps = createStructuredSelector({
billEnterModal: selectBillEnterModal,
@@ -37,8 +37,8 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
insertAuditTrail: ({ jobid, billid, operation }) =>
dispatch(insertAuditTrail({ jobid, billid, operation })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
const Templates = TemplateList("job_special");
@@ -126,24 +126,13 @@ function BillEnterModalContainer({
deductedfromlbr: deductedfromlbr,
lbr_adjustment,
joblineid: i.joblineid === "noline" ? null : i.joblineid,
applicable_taxes: {
federal:
(i.applicable_taxes && i.applicable_taxes.federal) ||
false,
state:
(i.applicable_taxes && i.applicable_taxes.state) ||
false,
local:
(i.applicable_taxes && i.applicable_taxes.local) ||
false,
},
};
}),
},
},
],
},
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID", "GET_JOB_BY_PK"],
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"],
});
const adjKeys = Object.keys(adjustmentsToInsert);
@@ -316,9 +305,7 @@ function BillEnterModalContainer({
insertAuditTrail({
jobid: values.jobid,
billid: billId,
operation: AuditTrailMapping.billposted(
r1.data.insert_bills.returning[0].invoice_number
),
operation: AuditTrailMapping.billposted(remainingValues.invoice_number),
});
if (enterAgain) {
@@ -346,18 +333,18 @@ function BillEnterModalContainer({
}, [enterAgain, form]);
useEffect(() => {
if (billEnterModal.open) {
if (billEnterModal.visible) {
form.setFieldsValue(formValues);
} else {
form.resetFields();
}
}, [billEnterModal.open, form, formValues]);
}, [billEnterModal.visible, form, formValues]);
return (
<Modal
title={t("bills.labels.new")}
width={"98%"}
open={billEnterModal.open}
visible={billEnterModal.visible}
okText={t("general.actions.save")}
keyboard="false"
onOk={() => form.submit()}

View File

@@ -1,18 +1,25 @@
import Icon, {UploadOutlined} from "@ant-design/icons";
import {useApolloClient} from "@apollo/client";
import {useSplitTreatments} from "@splitsoftware/splitio-react";
import {Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload,} from "antd";
import dayjs from "../../utils/day";
import React, {useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
import {MdOpenInNew} from "react-icons/md";
import {connect} from "react-redux";
import {Link} from "react-router-dom";
import {createStructuredSelector} from "reselect";
import {CHECK_BILL_INVOICE_NUMBER} from "../../graphql/bills.queries";
import {selectBodyshop} from "../../redux/user/user.selectors";
import Icon, { UploadOutlined } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client";
import { MdOpenInNew } from "react-icons/md";
import {
Alert,
Divider,
Form,
Input,
Select,
Space,
Statistic,
Switch,
Upload,
} from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { CHECK_BILL_INVOICE_NUMBER } from "../../graphql/bills.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.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 CurrencyInput from "../form-items-formatted/currency-form-item.component";
@@ -20,307 +27,273 @@ import JobSearchSelect from "../job-search-select/job-search-select.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import BillFormLines from "./bill-form.lines.component";
import {CalculateBillTotal} from "./bill-form.totals.utility";
import { CalculateBillTotal } from "./bill-form.totals.utility";
import { useTreatments } from "@splitsoftware/splitio-react";
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({});
export function BillFormComponent({bodyshop, disabled, form, vendorAutoCompleteOptions, lineData, responsibilityCenters, loadLines, billEdit, disableInvNumber, job, loadOutstandingReturns, loadInventory, preferredMake}) {
export function BillFormComponent({
bodyshop,
disabled,
form,
vendorAutoCompleteOptions,
lineData,
responsibilityCenters,
loadLines,
billEdit,
disableInvNumber,
job,
loadOutstandingReturns,
loadInventory,
}) {
const { t } = useTranslation();
const client = useApolloClient();
const [discount, setDiscount] = useState(0);
const { Extended_Bill_Posting } = useTreatments(
["Extended_Bill_Posting"],
{},
bodyshop.imexshopid
);
const {t} = useTranslation();
const client = useApolloClient();
const [discount, setDiscount] = useState(0);
const handleVendorSelect = (props, opt) => {
setDiscount(opt.discount);
const { treatments: {Extended_Bill_Posting, ClosingPeriod} } = useSplitTreatments({
attributes: {},
names: ["Extended_Bill_Posting", "ClosingPeriod"],
splitKey: bodyshop.imexshopid,
});
const handleVendorSelect = (props, opt) => {
setDiscount(opt.discount);
opt &&
!billEdit &&
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: opt.value,
},
});
};
const handleFederalTaxExemptSwitchToggle = (checked) => {
// Early gate
if (!checked) return;
const values = form.getFieldsValue("billlines");
// Gate bill lines
if (!values?.billlines?.length) return;
const billlines = values.billlines.map((b) => {
b.applicable_taxes.federal = false;
return b;
});
form.setFieldsValue({ billlines });
opt &&
!billEdit &&
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: opt.value,
},
});
};
useEffect(() => {
if (job) form.validateFields(["is_credit_memo"]);
}, [job, form]);
useEffect(() => {
const vendorId = form.getFieldValue("vendorid");
if (vendorId && vendorAutoCompleteOptions) {
const matchingVendors = vendorAutoCompleteOptions.filter(
(v) => v.id === vendorId
);
if (matchingVendors.length === 1) {
setDiscount(matchingVendors[0].discount);
}
}
const jobId = form.getFieldValue("jobid");
if (jobId) {
loadLines({variables: {id: jobId}});
if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) {
loadOutstandingReturns({
useEffect(() => {
const vendorId = form.getFieldValue("vendorid");
if (vendorId && vendorAutoCompleteOptions) {
const matchingVendors = vendorAutoCompleteOptions.filter(
(v) => v.id === vendorId
);
if (matchingVendors.length === 1) {
setDiscount(matchingVendors[0].discount);
}
}
const jobId = form.getFieldValue("jobid");
if (jobId) {
loadLines({ variables: { id: jobId } });
if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) {
loadOutstandingReturns({
variables: {
jobId: jobId,
vendorId: vendorId,
},
});
}
}
if (vendorId === bodyshop.inhousevendorid && !billEdit) {
loadInventory();
}
}, [
form,
billEdit,
loadOutstandingReturns,
loadInventory,
setDiscount,
vendorAutoCompleteOptions,
loadLines,
bodyshop.inhousevendorid,
]);
return (
<div>
<FormFieldsChanged form={form} />
<Form.Item
style={{ display: "none" }}
name="isinhouse"
valuePropName="checked"
>
<Switch />
</Form.Item>
<LayoutFormRow grow>
<Form.Item
name="jobid"
label={t("bills.fields.ro_number")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<JobSearchSelect
disabled={billEdit || disabled}
convertedOnly
notExported={false}
onBlur={() => {
if (form.getFieldValue("jobid") !== null) {
loadLines({ variables: { id: form.getFieldValue("jobid") } });
if (form.getFieldValue("vendorid") !== null) {
loadOutstandingReturns({
variables: {
jobId: jobId,
vendorId: vendorId,
jobId: form.getFieldValue("jobid"),
vendorId: form.getFieldValue("vendorid"),
},
});
});
}
}
}}
/>
</Form.Item>
<Form.Item
label={t("bills.fields.vendor")}
name="vendorid"
// style={{ display: billEdit ? "none" : null }}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({ getFieldValue }) => ({
validator(rule, value) {
if (
value &&
!getFieldValue(["isinhouse"]) &&
value === bodyshop.inhousevendorid
) {
return Promise.reject(t("bills.validation.manualinhouse"));
}
return Promise.resolve();
},
}),
]}
>
<VendorSearchSelect
disabled={disabled}
options={vendorAutoCompleteOptions}
onSelect={handleVendorSelect}
/>
</Form.Item>
</LayoutFormRow>
{job &&
job.ious &&
job.ious.length > 0 &&
job.ious.map((iou) => (
<Alert
key={iou.id}
type="warning"
message={
<Space>
{t("bills.labels.iouexists")}
<Link
target="_blank"
rel="noopener noreferrer"
to={`/manage/jobs/${iou.id}?tab=repairdata`}
>
<Space>
{iou.ro_number}
<Icon component={MdOpenInNew} />
</Space>
</Link>
</Space>
}
}
/>
))}
<LayoutFormRow>
<Form.Item
label={t("bills.fields.invoice_number")}
name="invoice_number"
validateTrigger="onBlur"
hasFeedback
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({ getFieldValue }) => ({
async validator(rule, value) {
const vendorid = getFieldValue("vendorid");
if (vendorid && value) {
const response = await client.query({
query: CHECK_BILL_INVOICE_NUMBER,
variables: {
invoice_number: value,
vendorid: vendorid,
},
});
if (vendorId === bodyshop.inhousevendorid && !billEdit) {
loadInventory();
}
}, [
form,
billEdit,
loadOutstandingReturns,
loadInventory,
setDiscount,
vendorAutoCompleteOptions,
loadLines,
bodyshop.inhousevendorid,
]);
if (response.data.bills_aggregate.aggregate.count === 0) {
return Promise.resolve();
} else if (
response.data.bills_aggregate.nodes.length === 1 &&
response.data.bills_aggregate.nodes[0].id ===
form.getFieldValue("id")
) {
return Promise.resolve();
}
return Promise.reject(
t("bills.validation.unique_invoice_number")
);
} else {
return Promise.resolve();
}
},
}),
]}
>
<Input disabled={disabled || disableInvNumber} />
</Form.Item>
<Form.Item
label={t("bills.fields.date")}
name="date"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<FormDatePicker disabled={disabled} />
</Form.Item>
<Form.Item
label={t("bills.fields.is_credit_memo")}
name="is_credit_memo"
valuePropName="checked"
rules={[
({ getFieldValue }) => ({
validator(rule, value) {
if (
value === true &&
getFieldValue("jobid") &&
getFieldValue("vendorid")
) {
//Removed as this would cause an additional reload when validating the form on submit and clear the values.
// loadOutstandingReturns({
// variables: {
// jobId: form.getFieldValue("jobid"),
// vendorId: form.getFieldValue("vendorid"),
// },
// });
}
return (
<div>
<FormFieldsChanged form={form}/>
<Form.Item
style={{display: "none"}}
name="isinhouse"
valuePropName="checked"
>
<Switch/>
</Form.Item>
<LayoutFormRow grow>
<Form.Item
name="jobid"
label={t("bills.fields.ro_number")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<JobSearchSelect
disabled={billEdit || disabled}
convertedOnly
notExported={false}
onBlur={() => {
if (form.getFieldValue("jobid") !== null) {
loadLines({variables: {id: form.getFieldValue("jobid")}});
if (form.getFieldValue("vendorid") !== null) {
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: form.getFieldValue("vendorid"),
},
});
}
}
}}
/>
</Form.Item>
<Form.Item
label={t("bills.fields.vendor")}
name="vendorid"
// style={{ display: billEdit ? "none" : null }}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({getFieldValue}) => ({
validator(rule, value) {
if (
value &&
!getFieldValue(["isinhouse"]) &&
value === bodyshop.inhousevendorid
) {
return Promise.reject(t("bills.validation.manualinhouse"));
}
return Promise.resolve();
},
}),
]}
>
<VendorSearchSelect
disabled={disabled}
options={vendorAutoCompleteOptions}
preferredMake={preferredMake}
onSelect={handleVendorSelect}
/>
</Form.Item>
</LayoutFormRow>
{job &&
job.ious &&
job.ious.length > 0 &&
job.ious.map((iou) => (
<Alert
key={iou.id}
type="warning"
message={
<Space>
{t("bills.labels.iouexists")}
<Link
target="_blank"
rel="noopener noreferrer"
to={`/manage/jobs/${iou.id}?tab=repairdata`}
>
<Space>
{iou.ro_number}
<Icon component={MdOpenInNew}/>
</Space>
</Link>
</Space>
}
/>
))}
<LayoutFormRow>
<Form.Item
label={t("bills.fields.invoice_number")}
name="invoice_number"
validateTrigger="onBlur"
hasFeedback
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({getFieldValue}) => ({
async validator(rule, value) {
const vendorid = getFieldValue("vendorid");
if (vendorid && value) {
const response = await client.query({
query: CHECK_BILL_INVOICE_NUMBER,
variables: {
invoice_number: value,
vendorid: vendorid,
},
});
if (response.data.bills_aggregate.aggregate.count === 0) {
return Promise.resolve();
} else if (
response.data.bills_aggregate.nodes.length === 1 &&
response.data.bills_aggregate.nodes[0].id ===
form.getFieldValue("id")
) {
return Promise.resolve();
}
return Promise.reject(
t("bills.validation.unique_invoice_number")
);
} else {
return Promise.resolve();
}
},
}),
]}
>
<Input disabled={disabled || disableInvNumber}/>
</Form.Item>
<Form.Item
label={t("bills.fields.date")}
name="date"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({getFieldValue}) => ({
validator(rule, value) {
if (
ClosingPeriod.treatment === "on" &&
bodyshop.accountingconfig.ClosingPeriod
) {
if (
dayjs(value)
.startOf("day")
.isSameOrAfter(
dayjs(
bodyshop.accountingconfig.ClosingPeriod[0]
).startOf("day")
) &&
dayjs(value)
.startOf("day")
.isSameOrBefore(
dayjs(
bodyshop.accountingconfig.ClosingPeriod[1]
).endOf("day")
)
) {
return Promise.resolve();
} else {
return Promise.reject(t("bills.validation.closingperiod"));
}
} else {
return Promise.resolve();
}
},
}),
]}
>
<FormDatePicker disabled={disabled}/>
</Form.Item>
<Form.Item
label={t("bills.fields.is_credit_memo")}
name="is_credit_memo"
valuePropName="checked"
rules={[
({getFieldValue}) => ({
validator(rule, value) {
if (
value === true &&
getFieldValue("jobid") &&
getFieldValue("vendorid")
) {
//Removed as this would cause an additional reload when validating the form on submit and clear the values.
// loadOutstandingReturns({
// variables: {
// jobId: form.getFieldValue("jobid"),
// vendorId: form.getFieldValue("vendorid"),
// },
// });
}
if (
!bodyshop.bill_allow_post_to_closed &&
job &&
(job.status === bodyshop.md_ro_statuses.default_invoiced ||
job.status === bodyshop.md_ro_statuses.default_exported ||
job.status === bodyshop.md_ro_statuses.default_void) &&
(value === false || !value)
) {
return Promise.reject(t("bills.labels.onlycmforinvoiced"));
}
if (
!bodyshop.bill_allow_post_to_closed &&
job &&
(job.status === bodyshop.md_ro_statuses.default_invoiced ||
job.status === bodyshop.md_ro_statuses.default_exported ||
job.status === bodyshop.md_ro_statuses.default_void) &&
(value === false || !value)
) {
return Promise.reject(t("bills.labels.onlycmforinvoiced"));
}
return Promise.resolve();
},
@@ -375,16 +348,7 @@ export function BillFormComponent({bodyshop, disabled, form, vendorAutoCompleteO
>
<CurrencyInput min={0} />
</Form.Item>
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
<Form.Item
span={2}
label={t("bills.labels.federal_tax_exempt")}
name="federal_tax_exempt"
>
<Switch onChange={handleFederalTaxExemptSwitchToggle} />
</Form.Item>
) : null}
<Form.Item shouldUpdate span={13}>
<Form.Item shouldUpdate span={15}>
{() => {
const values = form.getFieldsValue([
"billlines",
@@ -402,7 +366,7 @@ export function BillFormComponent({bodyshop, disabled, form, vendorAutoCompleteO
totals = CalculateBillTotal(values);
if (!!totals)
return (
<div align="right">
<div>
<Space wrap>
<Statistic
title={t("bills.labels.subtotal")}
@@ -460,55 +424,55 @@ export function BillFormComponent({bodyshop, disabled, form, vendorAutoCompleteO
</LayoutFormRow>
<Divider orientation="left">{t("bills.labels.bill_lines")}</Divider>
{Extended_Bill_Posting.treatment === "on" ? (
<BillFormLinesExtended
lineData={lineData}
discount={discount}
form={form}
responsibilityCenters={responsibilityCenters}
disabled={disabled}
/>
) : (
<BillFormLines
lineData={lineData}
discount={discount}
form={form}
responsibilityCenters={responsibilityCenters}
disabled={disabled}
billEdit={billEdit}
/>
)}
{Extended_Bill_Posting.treatment === "on" ? (
<BillFormLinesExtended
lineData={lineData}
discount={discount}
form={form}
responsibilityCenters={responsibilityCenters}
disabled={disabled}
/>
) : (
<BillFormLines
lineData={lineData}
discount={discount}
form={form}
responsibilityCenters={responsibilityCenters}
disabled={disabled}
billEdit={billEdit}
/>
)}
<Form.Item
name="upload"
label="Upload"
style={{display: billEdit ? "none" : null}}
valuePropName="fileList"
getValueFromEvent={(e) => {
if (Array.isArray(e)) {
return e;
}
return e && e.fileList;
}}
>
<Upload.Dragger
multiple={true}
name="logo"
beforeUpload={() => false}
listType="picture"
>
<>
<p className="ant-upload-drag-icon">
<UploadOutlined/>
</p>
<p className="ant-upload-text">
Click or drag files to this area to upload.
</p>
</>
</Upload.Dragger>
</Form.Item>
</div>
);
<Form.Item
name="upload"
label="Upload"
style={{ display: billEdit ? "none" : null }}
valuePropName="fileList"
getValueFromEvent={(e) => {
if (Array.isArray(e)) {
return e;
}
return e && e.fileList;
}}
>
<Upload.Dragger
multiple={true}
name="logo"
beforeUpload={() => false}
listType="picture"
>
<>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text">
Click or drag files to this area to upload.
</p>
</>
</Upload.Dragger>
</Form.Item>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(BillFormComponent);

View File

@@ -1,16 +1,16 @@
import { useLazyQuery, useQuery } from "@apollo/client";
import {useSplitTreatments} from "@splitsoftware/splitio-react";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries";
import { GET_JOB_LINES_TO_ENTER_BILL } from "../../graphql/jobs-lines.queries";
import { QUERY_UNRECEIVED_LINES } from "../../graphql/parts-orders.queries";
import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import BillCmdReturnsTableComponent from "../bill-cm-returns-table/bill-cm-returns-table.component";
import BillInventoryTable from "../bill-inventory-table/bill-inventory-table.component";
import BillFormComponent from "./bill-form.component";
import BillCmdReturnsTableComponent from "../bill-cm-returns-table/bill-cm-returns-table.component";
import { QUERY_UNRECEIVED_LINES } from "../../graphql/parts-orders.queries";
import BillInventoryTable from "../bill-inventory-table/bill-inventory-table.component";
import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries";
import { useTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -23,11 +23,11 @@ export function BillFormContainer({
disabled,
disableInvNumber,
}) {
const { treatments: {Simple_Inventory} } = useSplitTreatments({
attributes: {},
names: ["Simple_Inventory"],
splitKey: bodyshop && bodyshop.imexshopid,
});
const { Simple_Inventory } = useTreatments(
["Simple_Inventory"],
{},
bodyshop && bodyshop.imexshopid
);
const { data: VendorAutoCompleteData } = useQuery(
SEARCH_VENDOR_AUTOCOMPLETE,
@@ -59,7 +59,6 @@ export function BillFormContainer({
disableInvNumber={disableInvNumber}
loadOutstandingReturns={loadOutstandingReturns}
loadInventory={loadInventory}
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}
/>
{!billEdit && (
<BillCmdReturnsTableComponent

View File

@@ -1,5 +1,5 @@
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
import {useSplitTreatments} from "@splitsoftware/splitio-react";
import { useTreatments } from "@splitsoftware/splitio-react";
import {
Button,
Form,
@@ -41,14 +41,11 @@ export function BillEnterModalLinesComponent({
}) {
const { t } = useTranslation();
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
const { treatments: {Simple_Inventory} } = useSplitTreatments({
attributes: {},
names: ["Simple_Inventory"],
splitKey: bodyshop && bodyshop.imexshopid,
});
const { Simple_Inventory } = useTreatments(
["Simple_Inventory"],
{},
bodyshop && bodyshop.imexshopid
);
const columns = (remove) => {
return [
{
@@ -470,8 +467,7 @@ export function BillEnterModalLinesComponent({
return {
key: `${field.index}fedtax`,
valuePropName: "checked",
initialValue:
form.getFieldValue("federal_tax_exempt") === true ? false : true,
// initialValue: true,
name: [field.name, "applicable_taxes", "federal"],
};
},

View File

@@ -19,14 +19,14 @@ export const CalculateBillTotal = (invoice) => {
}).multiply(i.quantity || 1);
subtotal = subtotal.add(itemTotal);
if (i.applicable_taxes.federal) {
if (i.applicable_taxes?.federal) {
federalTax = federalTax.add(
itemTotal.percentage(federal_tax_rate || 0)
);
}
if (i.applicable_taxes.state)
if (i.applicable_taxes?.state)
stateTax = stateTax.add(itemTotal.percentage(state_tax_rate || 0));
if (i.applicable_taxes.local)
if (i.applicable_taxes?.local)
localTax = localTax.add(itemTotal.percentage(local_tax_rate || 0));
}
});

View File

@@ -15,8 +15,7 @@ const BillLineSearchSelect = (
disabled={disabled}
ref={ref}
showSearch
popupMatchSelectWidth={false}
optionLabelProp={"name"}
dropdownMatchSelectWidth={false}
// optionFilterProp="line_desc"
filterOption={(inputValue, option) => {
return (
@@ -58,9 +57,6 @@ const BillLineSearchSelect = (
style={{
...(item.removed ? { textDecoration: "line-through" } : {}),
}}
name={`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
item.oem_partno ? ` - ${item.oem_partno}` : ""
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()}
>
<span>
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${

View File

@@ -1,38 +0,0 @@
import { Button, Space } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
export default function BillPrintButton({ billid }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const Templates = TemplateList("job_special");
const submitHandler = async () => {
setLoading(true);
try {
await GenerateDocument(
{
name: Templates.parts_invoice_label_single.key,
variables: {
id: billid,
},
},
{},
"p"
);
} catch (e) {
console.warn("Warning: Error generating a document.");
}
setLoading(false);
};
return (
<Space wrap>
<Button loading={loading} onClick={submitHandler}>
{t("bills.labels.printlabels")}
</Button>
</Space>
);
}

View File

@@ -2,7 +2,7 @@ import { FileAddFilled } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, notification, Tooltip } from "antd";
import { t } from "i18next";
import dayjs from "./../../utils/day";
import moment from "moment";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -36,6 +36,7 @@ export function BilllineAddInventory({
}) {
const [loading, setLoading] = useState(false);
const { billid } = queryString.parse(useLocation().search);
const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT);
const addToInventory = async () => {
@@ -49,7 +50,7 @@ export function BilllineAddInventory({
jobid: jobid,
isinhouse: true,
is_credit_memo: true,
date: dayjs().format("YYYY-MM-DD"),
date: moment().format("YYYY-MM-DD"),
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate,
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate,
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate,
@@ -91,7 +92,7 @@ export function BilllineAddInventory({
pol: {
returnfrombill: billid,
vendorid: bodyshop.inhousevendorid,
deliver_by: dayjs().format("YYYY-MM-DD"),
deliver_by: moment().format("YYYY-MM-DD"),
parts_order_lines: {
data: [
{

View File

@@ -2,7 +2,7 @@ import React, { useState } from "react";
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
import { useQuery } from "@apollo/client";
import queryString from "query-string";
import { useLocation, useNavigate } from "react-router-dom";
import { useHistory, useLocation } from "react-router-dom";
import { Table, Input } from "antd";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
@@ -10,7 +10,7 @@ import AlertComponent from "../alert/alert.component";
export default function BillsVendorsList() {
const search = queryString.parse(useLocation().search);
const history = useNavigate();
const history = useHistory();
const { loading, error, data } = useQuery(QUERY_ALL_VENDORS, {
fetchPolicy: "network-only",

View File

@@ -1,64 +1,54 @@
import {HomeFilled} from "@ant-design/icons";
import {Breadcrumb, Col, Row} from "antd";
import { HomeFilled } from "@ant-design/icons";
import { Breadcrumb, Row, Col } from "antd";
import React from "react";
import {connect} from "react-redux";
import {Link} from "react-router-dom";
import {createStructuredSelector} from "reselect";
import {selectBreadcrumbs} from "../../redux/application/application.selectors";
import {selectBodyshop} from "../../redux/user/user.selectors";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { selectBreadcrumbs } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import GlobalSearch from "../global-search/global-search.component";
import GlobalSearchOs from "../global-search/global-search-os.component";
import "./breadcrumbs.styles.scss";
import {useSplitTreatments} from "@splitsoftware/splitio-react";
import { useTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({
breadcrumbs: selectBreadcrumbs,
bodyshop: selectBodyshop,
breadcrumbs: selectBreadcrumbs,
bodyshop: selectBodyshop,
});
export function BreadCrumbs({breadcrumbs, bodyshop}) {
export function BreadCrumbs({ breadcrumbs, bodyshop }) {
const { OpenSearch } = useTreatments(
["OpenSearch"],
{},
bodyshop && bodyshop.imexshopid
);
const {treatments: {OpenSearch}} = useSplitTreatments({
attributes: {},
names: ["OpenSearch"],
splitKey: bodyshop && bodyshop.imexshopid,
});
// TODO - Client Update - Technically key is not doing anything here
return (
<Row className="breadcrumb-container">
<Col xs={24} sm={24} md={16}>
<Breadcrumb
separator=">"
items={[
{
key: "home",
title: (
<Link to={`/manage/`}>
<HomeFilled/>{" "}
{(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) ||
""}
</Link>
),
},
...breadcrumbs.map((item) =>
item.link
? {
key: item.label,
title: <Link to={item.link}>{item.label}</Link>,
}
: {
key: item.label,
title: item.label,
}
),
]}
/>
</Col>
<Col xs={24} sm={24} md={8}>
{OpenSearch.treatment === "on" ? <GlobalSearchOs/> : <GlobalSearch/>}
</Col>
</Row>
);
return (
<Row className="breadcrumb-container">
<Col xs={24} sm={24} md={16}>
<Breadcrumb separator=">">
<Breadcrumb.Item>
<Link to={`/manage`}>
<HomeFilled />{" "}
{(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) ||
""}
</Link>
</Breadcrumb.Item>
{breadcrumbs.map((item) =>
item.link ? (
<Breadcrumb.Item key={item.label}>
<Link to={item.link}>{item.label} </Link>
</Breadcrumb.Item>
) : (
<Breadcrumb.Item key={item.label}>{item.label}</Breadcrumb.Item>
)
)}
</Breadcrumb>
</Col>
<Col xs={24} sm={24} md={8}>
{OpenSearch.treatment === "on" ? <GlobalSearchOs /> : <GlobalSearch />}
</Col>
</Row>
);
}
export default connect(mapStateToProps, null)(BreadCrumbs);

View File

@@ -25,7 +25,7 @@ export function ContractsFindModalContainer({
}) {
const { t } = useTranslation();
const { open } = caBcEtfTableModal;
const { visible } = caBcEtfTableModal;
const [loading, setLoading] = useState(false);
const [form] = Form.useForm();
const EtfTemplate = TemplateList("special").ca_bc_etf_table;
@@ -63,14 +63,14 @@ export function ContractsFindModalContainer({
};
useEffect(() => {
if (open) {
if (visible) {
form.resetFields();
}
}, [open, form]);
}, [visible, form]);
return (
<Modal
open={open}
visible={visible}
width="70%"
title={t("payments.labels.findermodal")}
onCancel={() => toggleModalVisible()}

View File

@@ -38,7 +38,7 @@ export default function CABCpvrtCalculator({ disabled, form }) {
<Popover
destroyTooltipOnHide
content={popContent}
open={visibility}
visible={visibility}
disabled={disabled}
>
<Button disabled={disabled} onClick={() => setVisibility(true)}>

View File

@@ -1,40 +1,21 @@
import { DeleteFilled } from "@ant-design/icons";
import { useLazyQuery, useMutation } from "@apollo/client";
import {
Button,
Card,
Col,
Form,
Input,
Row,
Space,
Spin,
Statistic,
notification,
} from "antd";
import React, { useCallback, useEffect } from "react";
import axios from "axios";
import dayjs from "../../utils/day";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { Button, Card, Form, Input, InputNumber, Row, Select } from "antd";
import moment from "moment";
import { useMutation, useQuery } from "@apollo/client";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
import {
INSERT_PAYMENT_RESPONSE,
QUERY_RO_AND_OWNER_BY_JOB_PKS,
QUERY_RO_AND_OWNER_BY_JOB_PK,
} from "../../graphql/payment_response.queries";
import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
import DataLabel from "../data-label/data-label.component";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectCardPayment } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
const mapStateToProps = createStructuredSelector({
cardPaymentModal: selectCardPayment,
bodyshop: selectBodyshop,
});
import { connect } from "react-redux";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
@@ -44,331 +25,253 @@ const mapDispatchToProps = (dispatch) => ({
const CardPaymentModalComponent = ({
bodyshop,
cardPaymentModal,
context,
toggleModalVisible,
insertAuditTrail,
}) => {
const { context } = cardPaymentModal;
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const amount = Form.useWatch("amount", form);
const payer = Form.useWatch("payer", form);
const jobid = Form.useWatch("jobid", form);
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
const { t } = useTranslation();
const [, { data, refetch, queryLoading }] = useLazyQuery(
QUERY_RO_AND_OWNER_BY_JOB_PKS,
{
variables: { jobids: [context.jobid] },
skip: true,
}
);
const { data, refetch } = useQuery(QUERY_RO_AND_OWNER_BY_JOB_PK, {
variables: { jobid: context?.jobid ?? "" },
});
console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data);
//Initialize the intellipay window.
const SetIntellipayCallbackFunctions = () => {
console.log("*** Set IntelliPay callback functions.");
window.intellipay.runOnClose(() => {
//window.intellipay.initialize();
});
window.intellipay.runOnApproval(async function (response) {
console.warn("*** Running On Approval Script ***");
form.setFieldValue("paymentResponse", response);
form.submit();
});
window.intellipay.runOnNonApproval(async function (response) {
const nonApproval = useCallback(
async (response) => {
// Mutate unsuccessful payment
const { payments } = form.getFieldsValue();
await insertPaymentResponse({
variables: {
paymentResponse: payments.map((payment) => ({
amount: payment.amount,
paymentResponse: {
amount: response.amount,
bodyshopid: bodyshop.id,
jobid: payment.jobid,
jobid: jobid || context.jobid,
declinereason: response.declinereason,
ext_paymentid: response.paymentid.toString(),
successful: false,
response,
})),
},
},
});
payments.forEach((payment) =>
insertAuditTrail({
jobid: payment.jobid,
operation: AuditTrailMapping.failedpayment(),
})
);
});
};
const handleFinish = async (values) => {
try {
await insertPayment({
variables: {
paymentInput: values.payments.map((payment) => ({
amount: payment.amount,
transactionid: (values.paymentResponse.paymentid || "").toString(),
payer: t("payments.labels.customer"),
type: values.paymentResponse.cardbrand,
jobid: payment.jobid,
date: dayjs(Date.now()),
payment_responses: {
data: [
{
amount: payment.amount,
bodyshopid: bodyshop.id,
jobid: payment.jobid,
declinereason: values.paymentResponse.declinereason,
ext_paymentid: values.paymentResponse.paymentid.toString(),
successful: true,
response: values.paymentResponse,
},
],
},
})),
},
refetchQueries: ["GET_JOB_BY_PK"],
// Insert failed payment to audit trail
insertAuditTrail({
jobid: jobid || context?.jobid,
operation: AuditTrailMapping.failedpayment(),
});
toggleModalVisible();
} catch (error) {
console.error(error);
notification.open({
type: "error",
message: t("payments.errors.inserting", { error: error.message }),
},
[bodyshop, context, insertAuditTrail, insertPaymentResponse, jobid]
);
const initIntellipayFunctions = useCallback(() => {
if (window.intellipay !== undefined && typeof jobid !== "undefined") {
console.log("intellipay init functions");
window.intellipay.runOnClose(() => {
window.intellipay.initialize();
});
} finally {
setLoading(false);
window.intellipay.runOnApproval(async function (response) {
form.setFieldValue("paymentResponse", response);
form.submit();
toggleModalVisible();
});
window.intellipay.runOnNonApproval(nonApproval);
}
};
}, [form, jobid, nonApproval, toggleModalVisible]);
const handleIntelliPayCharge = async () => {
setLoading(true);
//Validate
try {
await form.validateFields();
} catch (error) {
setLoading(false);
return;
const initJobId = useCallback(() => {
if (context?.jobid) {
form.setFieldValue("jobid", context.jobid);
}
try {
const response = await axios.post("/intellipay/lightbox_credentials", {
bodyshop,
refresh: !!window.intellipay,
});
form.setFieldValue("payer", t("payments.labels.customer"));
}, [context?.jobid, form, t]);
if (window.intellipay) {
// eslint-disable-next-line no-eval
eval(response.data);
SetIntellipayCallbackFunctions();
window.intellipay.autoOpen();
} else {
useEffect(() => {
initJobId();
axios
.post("/intellipay/lightbox_credentials", { bodyshop })
.then((response) => {
var rg = document.createRange();
let node = rg.createContextualFragment(response.data);
document.documentElement.appendChild(node);
SetIntellipayCallbackFunctions();
window.intellipay.isAutoOpen = true;
window.intellipay.initialize();
}
} catch (error) {
notification.open({
type: "error",
message: t("job_payments.notifications.error.openingip"),
initIntellipayFunctions();
});
setLoading(false);
function handleEvents(...props) {
const operation = props[0].data.operation;
if (operation === "updateform") {
props[0].stopImmediatePropagation();
}
}
window.addEventListener("message", handleEvents, false);
return () => window.removeEventListener("message", handleEvents, false);
}, [bodyshop, initJobId, initIntellipayFunctions]);
const handleFinish = async (values) => {
const paymentResult = await insertPayment({
variables: {
paymentInput: {
amount: values.amount,
transactionid: values.paymentResponse.receiptelements.transid,
payer: values.payer,
type: values.paymentResponse.cardType,
jobid: values.jobid,
date: moment(Date.now()),
},
},
update(cache, { data }) {
cache.modify({
id: cache.identify({ id: jobid, __typename: "jobs" }),
fields: {
payments(payments) {
return [...data.insert_payments.returning, ...payments];
},
},
});
},
});
await insertPaymentResponse({
variables: {
paymentResponse: {
amount: values.amount,
bodyshopid: bodyshop.id,
paymentid: paymentResult.data.insert_payments.returning[0].id,
jobid: values.jobid,
declinereason: values.paymentResponse.declinereason,
ext_paymentid: values.paymentResponse.paymentid.toString(),
successful: true,
response: values.paymentResponse,
},
},
});
};
return (
<Card title="Card Payment">
<Spin spinning={loading}>
<Form
onFinish={handleFinish}
form={form}
layout="vertical"
initialValues={{
payments: context.jobid ? [{ jobid: context.jobid }] : [],
}}
>
<Form.List name={["payments"]}>
{(fields, { add, remove, move }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Row gutter={[16, 16]}>
<Col span={16}>
<Form.Item
key={`${index}jobid`}
label={t("jobs.fields.ro_number")}
name={[field.name, "jobid"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<JobSearchSelectComponent
notExported={false}
clm_no
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
key={`${index}amount`}
label={t("payments.fields.amount")}
name={[field.name, "amount"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<CurrencyFormItemComponent />
</Form.Item>
</Col>
<Col span={2}>
<DeleteFilled
style={{ margin: "1rem" }}
onClick={() => {
remove(field.name);
}}
/>
</Col>
</Row>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{ width: "100%" }}
>
{t("general.actions.add")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
<Form onFinish={handleFinish} form={form}>
<LayoutFormRow grow>
<Form.Item
name="jobid"
label={t("bills.fields.ro_number")}
rules={[
{
required: true,
// message: t("general.validation.required"),
},
]}
>
<JobSearchSelectComponent
disabled={context?.jobid}
notExported={false}
clm_no
onChange={(e) => {
refetch({ jobid: e });
}}
/>
</Form.Item>
</LayoutFormRow>
{/* Lighbox Input amount needs to be hidden */}
<Input
className="ipayfield"
data-ipayname="amount"
type="hidden"
value={amount}
hidden
/>
<Input
className="ipayfield"
data-ipayname="account"
type="hidden"
value={data?.jobs_by_pk.ro_number}
hidden
/>
<Input
className="ipayfield"
data-ipayname="email"
type="hidden"
value={data?.jobs_by_pk.owner.ownr_ea}
hidden
/>
{/* Lightbox payment response when it is completed */}
<Form.Item name="paymentResponse" hidden>
<Input type="hidden" value={amount} />
</Form.Item>
<LayoutFormRow grow>
<Form.Item
label={t("payments.fields.payer")}
name="payer"
rules={[
{
required: true,
// message: t("general.validation.required"),
},
]}
>
<Select>
<Select.Option value={t("payments.labels.customer")}>
{t("payments.labels.customer")}
</Select.Option>
<Select.Option value={t("payments.labels.insurance")}>
{t("payments.labels.insurance")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item
shouldUpdate={(prevValues, curValues) =>
prevValues.payments?.map((p) => p?.jobid).join() !==
curValues.payments?.map((p) => p?.jobid).join()
}
label="Amount"
name="amount"
rules={[
{
required: true,
// message: t("general.validation.required"),
},
]}
>
{() => {
console.log("Updating the owner info section.");
//If all of the job ids have been fileld in, then query and update the IP field.
const { payments } = form.getFieldsValue();
if (
payments?.length > 0 &&
payments?.filter((p) => p?.jobid).length === payments?.length
) {
console.log("**Calling refetch.");
refetch({ jobids: payments.map((p) => p.jobid) });
}
console.log(
"Acc info",
data,
payments && data && data.jobs.length > 0
? data.jobs.map((j) => j.ro_number).join(", ")
: null
);
return (
<>
<Input
className="ipayfield"
data-ipayname="account"
//type="hidden"
value={
payments && data && data.jobs.length > 0
? data.jobs.map((j) => j.ro_number).join(", ")
: null
}
hidden
/>
<Input
className="ipayfield"
data-ipayname="email"
// type="hidden"
value={
payments && data && data.jobs.length > 0
? data.jobs.filter((j) => j.ownr_ea)[0]?.ownr_ea
: null
}
hidden
/>
</>
);
}}
</Form.Item>
<Form.Item
shouldUpdate={(prevValues, curValues) =>
prevValues.payments?.map((p) => p?.amount).join() !==
curValues.payments?.map((p) => p?.amount).join()
}
>
{() => {
const { payments } = form.getFieldsValue();
const totalAmountToCharge = payments?.reduce((acc, val) => {
return acc + (val?.amount || 0);
}, 0);
return (
<Space style={{ float: "right" }}>
<Statistic
title="Amount To Charge"
value={totalAmountToCharge}
precision={2}
/>
<Input
className="ipayfield"
data-ipayname="amount"
//type="hidden"
value={totalAmountToCharge?.toFixed(2)}
hidden
/>
<Button
type="primary"
// data-ipayname="submit"
className="ipayfield"
loading={queryLoading || loading}
disabled={!(totalAmountToCharge > 0)}
onClick={handleIntelliPayCharge}
>
{t("job_payments.buttons.proceedtopayment")}
</Button>
</Space>
);
}}
<InputNumber />
</Form.Item>
{/* Lightbox payment response when it is completed */}
<Form.Item name="paymentResponse" hidden>
<Input type="hidden" />
</Form.Item>
</Form>
</Spin>
<Row justify="space-around">
<Button
type="primary"
data-ipayname="submit"
className="ipayfield"
disabled={!amount || !payer || !jobid}
>
{t("job_payments.buttons.proceedtopayment")}
</Button>
{context?.balance && (
<DataLabel
valueStyle={{
color: context?.balance.getAmount() !== 0 ? "red" : "green",
}}
label={t("payments.labels.balance")}
>
{context?.balance.toFormat()}
</DataLabel>
)}
</Row>
</LayoutFormRow>
</Form>
</Card>
);
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(CardPaymentModalComponent);
export default connect(null, mapDispatchToProps)(CardPaymentModalComponent);

View File

@@ -22,7 +22,7 @@ function CardPaymentModalContainer({
toggleModalVisible,
bodyshop,
}) {
const { open } = cardPaymentModal;
const { context, visible } = cardPaymentModal;
const { t } = useTranslation();
const handleCancel = () => {
@@ -35,7 +35,7 @@ function CardPaymentModalContainer({
return (
<Modal
open={open}
visible={visible}
onOk={handleOK}
onCancel={handleCancel}
footer={[
@@ -43,10 +43,10 @@ function CardPaymentModalContainer({
{t("job_payments.buttons.goback")}
</Button>,
]}
width="80%"
width="60%"
destroyOnClose
>
<CardPaymentModalComponent />
<CardPaymentModalComponent bodyshop={bodyshop} context={context} />
</Modal>
);
}

View File

@@ -4,11 +4,20 @@ import { Button, notification, Space } from "antd";
import axios from "axios";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { messaging, requestForToken } from "../../firebase/firebase.utils";
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import FcmHandler from "../../utils/fcm-handler";
import ChatPopupComponent from "../chat-popup/chat-popup.component";
import "./chat-affix.styles.scss";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
chatVisible: selectChatVisible,
});
export function ChatAffixContainer({ bodyshop, chatVisible }) {
const { t } = useTranslation();
const client = useApolloClient();
@@ -19,7 +28,7 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
try {
const r = await axios.post("/notifications/subscribe", {
fcm_tokens: await getToken(messaging, {
vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY,
vapidKey: process.env.REACT_APP_FIREBASE_PUBLIC_VAPID_KEY,
}),
type: "messaging",
imexshopid: bodyshop.imexshopid,
@@ -27,34 +36,35 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
console.log("FCM Topic Subscription", r.data);
} catch (error) {
console.log(
"Error attempting to subscribe to messaging topic: ",
error
"Error attempting to subscribe to messaging topic: ",
error
);
notification.open({
type: "warning",
message: t("general.errors.fcm"),
btn: (
<Space>
<Button
onClick={async () => {
await requestForToken();
SubscribeToTopic();
}}
>
{t("general.actions.tryagain")}
</Button>
<Button
onClick={() => {
const win = window.open(
"https://help.imex.online/en/article/enabling-notifications-o978xi/",
"_blank"
);
win.focus();
}}
>
{t("general.labels.help")}
</Button>
</Space>
<Space>
<Button
onClick={async () => {
await requestForToken();
SubscribeToTopic();
}}
>
{t("general.actions.tryagain")}
</Button>
<Button
onClick={() => {
const win = window.open(
"https://help.imex.online/en/article/enabling-notifications-o978xi/",
"_blank"
);
win.focus();
}}
>
{t("general.labels.help")}
</Button>
</Space>
),
});
}
@@ -71,16 +81,16 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
payload: (payload && payload.data && payload.data.data) || payload.data,
});
}
let stopMessageListener, channel;
let stopMessageListenr, channel;
try {
stopMessageListener = onMessage(messaging, handleMessage);
stopMessageListenr = onMessage(messaging, handleMessage);
channel = new BroadcastChannel("imex-sw-messages");
channel.addEventListener("message", handleMessage);
} catch (error) {
console.log("Unable to set event listeners.");
}
return () => {
stopMessageListener && stopMessageListener();
stopMessageListenr && stopMessageListenr();
channel && channel.removeEventListener("message", handleMessage);
};
}, [client]);
@@ -88,10 +98,9 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
return (
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
{bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent /> : null}
</div>
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
{bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent /> : null}
</div>
);
}
export default ChatAffixContainer;
export default connect(mapStateToProps, null)(ChatAffixContainer);

View File

@@ -1,122 +1,120 @@
import {Badge, Card, List, Space, Tag} from "antd";
import { Badge, List, Tag } from "antd";
import React from "react";
import {connect} from "react-redux";
import {AutoSizer, CellMeasurer, CellMeasurerCache, List as VirtualizedList,} from "react-virtualized";
import {createStructuredSelector} from "reselect";
import {setSelectedConversation} from "../../redux/messaging/messaging.actions";
import {selectSelectedConversation} from "../../redux/messaging/messaging.selectors";
import {TimeAgoFormatter} from "../../utils/DateFormatter";
import { connect } from "react-redux";
import {
AutoSizer,
CellMeasurer,
CellMeasurerCache,
List as VirtualizedList,
} from "react-virtualized";
import { createStructuredSelector } from "reselect";
import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
import { TimeAgoFormatter } from "../../utils/DateFormatter";
import PhoneFormatter from "../../utils/PhoneFormatter";
import {OwnerNameDisplayFunction} from "../owner-name-display/owner-name-display.component";
import _ from "lodash";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import "./chat-conversation-list.styles.scss";
const mapStateToProps = createStructuredSelector({
selectedConversation: selectSelectedConversation,
selectedConversation: selectSelectedConversation,
});
const mapDispatchToProps = (dispatch) => ({
setSelectedConversation: (conversationId) =>
dispatch(setSelectedConversation(conversationId)),
setSelectedConversation: (conversationId) =>
dispatch(setSelectedConversation(conversationId)),
});
function ChatConversationListComponent({
conversationList,
selectedConversation,
setSelectedConversation,
loadMoreConversations,
}) {
const cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 60,
});
conversationList,
selectedConversation,
setSelectedConversation,
loadMoreConversations,
}) {
const cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 60,
});
const rowRenderer = ({index, key, style, parent}) => {
const item = conversationList[index];
const cardContentRight =
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
const cardContentLeft = item.job_conversations.length > 0
? item.job_conversations.map((j, idx) => (
<Tag key={idx}>{j.job.ro_number}</Tag>
))
: null;
const names = <>{_.uniq(item.job_conversations.map((j, idx) =>
OwnerNameDisplayFunction(j.job)
))}</>
const cardTitle = <>
{item.label && <Tag color="blue">{item.label}</Tag>}
{item.job_conversations.length > 0 ? (
<Space direction="vertical">
{names}
</Space>
) : (
<Space>
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
</Space>
)}
</>
const cardExtra = <Badge count={item.messages_aggregate.aggregate.count || 0}/>
const getCardStyle = () =>
item.id === selectedConversation
? { backgroundColor: 'rgba(128, 128, 128, 0.2)' }
: { backgroundColor: index % 2 === 0 ? '#f0f2f5' : '#ffffff' };
return (
<CellMeasurer
key={key}
cache={cache}
parent={parent}
columnIndex={0}
rowIndex={index}
>
<List.Item
onClick={() => setSelectedConversation(item.id)}
style={style}
className={`chat-list-item
${
item.id === selectedConversation
? "chat-list-selected-conversation"
: null
}`}
>
<Card style={getCardStyle()} bordered={false} size="small" extra={cardExtra} title={cardTitle}>
<div style={{display: 'inline-block', width: '70%', textAlign: 'left'}}>
{cardContentLeft}
</div>
<div
style={{display: 'inline-block', width: '30%', textAlign: 'right'}}>{cardContentRight}</div>
</Card>
</List.Item>
</CellMeasurer>
);
};
const rowRenderer = ({ index, key, style, parent }) => {
const item = conversationList[index];
return (
<div className="chat-list-container">
<AutoSizer>
{({height, width}) => (
<VirtualizedList
height={height}
width={width}
rowCount={conversationList.length}
rowHeight={cache.rowHeight}
rowRenderer={rowRenderer}
onScroll={({scrollTop, scrollHeight, clientHeight}) => {
if (scrollTop + clientHeight === scrollHeight) {
loadMoreConversations();
}
}}
/>
)}
</AutoSizer>
</div>
<CellMeasurer
key={key}
cache={cache}
parent={parent}
columnIndex={0}
rowIndex={index}
>
<List.Item
onClick={() => setSelectedConversation(item.id)}
className={`chat-list-item ${
item.id === selectedConversation
? "chat-list-selected-conversation"
: null
}`}
style={style}
>
<div
style={{
display: "inline-block",
}}
>
{item.label && <div className="chat-name">{item.label}</div>}
{item.job_conversations.length > 0 ? (
<div className="chat-name">
{item.job_conversations.map((j, idx) => (
<div key={idx}>
<OwnerNameDisplay ownerObject={j.job} />
</div>
))}
</div>
) : (
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
)}
</div>
<div style={{ display: "inline-block" }}>
<div>
{item.job_conversations.length > 0
? item.job_conversations.map((j, idx) => (
<Tag key={idx} className="ro-number-tag">
{j.job.ro_number}
</Tag>
))
: null}
</div>
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>
</div>
<Badge count={item.messages_aggregate.aggregate.count || 0} />
</List.Item>
</CellMeasurer>
);
};
return (
<div className="chat-list-container">
<AutoSizer>
{({ height, width }) => (
<VirtualizedList
height={height}
width={width}
rowCount={conversationList.length}
rowHeight={cache.rowHeight}
rowRenderer={rowRenderer}
onScroll={({ scrollTop, scrollHeight, clientHeight }) => {
if (scrollTop + clientHeight === scrollHeight) {
loadMoreConversations();
}
}}
/>
)}
</AutoSizer>
</div>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
mapStateToProps,
mapDispatchToProps
)(ChatConversationListComponent);

View File

@@ -1,14 +1,27 @@
.chat-list-selected-conversation {
background-color: rgba(128, 128, 128, 0.2);
}
.chat-list-container {
flex: 1;
overflow: hidden;
height: 100%;
border: 1px solid gainsboro;
}
.chat-list-item {
.ant-card-head {
border: none;
}
display: flex;
flex-direction: row;
&:hover {
cursor: pointer;
color: #ff7a00;
}
.chat-name {
flex: 1;
display: inline;
}
.ro-number-tag {
align-self: baseline;
}
padding: 12px 24px;
border-bottom: 1px solid gainsboro;
}

View File

@@ -4,7 +4,6 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import ChatArchiveButton from "../chat-archive-button/chat-archive-button.component";
import ChatConversationTitleTags from "../chat-conversation-title-tags/chat-conversation-title-tags.component";
import ChatLabelComponent from "../chat-label/chat-label.component";
import ChatPrintButton from "../chat-print-button/chat-print-button.component";
import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container";
export default function ChatConversationTitle({ conversation }) {
@@ -14,7 +13,6 @@ export default function ChatConversationTitle({ conversation }) {
{conversation && conversation.phone_num}
</PhoneNumberFormatter>
<ChatLabelComponent conversation={conversation} />
<ChatPrintButton conversation={conversation} />
<ChatConversationTitleTags
jobConversations={
(conversation && conversation.job_conversations) || []

View File

@@ -27,7 +27,7 @@ export function ChatMediaSelector({
conversation,
}) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [visible, setVisible] = useState(false);
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
fetchPolicy: "network-only",
@@ -39,13 +39,13 @@ export function ChatMediaSelector({
},
skip:
!open ||
!visible ||
!conversation.job_conversations ||
conversation.job_conversations.length === 0,
});
const handleVisibleChange = (change) => {
setOpen(change);
const handleVisibleChange = (visible) => {
setVisible(visible);
};
useEffect(() => {
@@ -65,7 +65,7 @@ export function ChatMediaSelector({
externalMediaState={[selectedMedia, setSelectedMedia]}
/>
)}
{bodyshop.uselocalmediaserver && open && (
{bodyshop.uselocalmediaserver && visible && (
<JobDocumentsLocalGalleryExternal
externalMediaState={[selectedMedia, setSelectedMedia]}
jobId={
@@ -88,8 +88,8 @@ export function ChatMediaSelector({
}
title={t("messaging.labels.selectmedia")}
trigger="click"
open={open}
onOpenChange={handleVisibleChange}
visible={visible}
onVisibleChange={handleVisibleChange}
>
<Badge count={selectedMedia.filter((s) => s.isSelected).length}>
<PictureFilled style={{ margin: "0 .5rem" }} />

View File

@@ -1,7 +1,7 @@
import Icon from "@ant-design/icons";
import { Tooltip } from "antd";
import i18n from "i18next";
import dayjs from "../../utils/day";
import moment from "moment";
import React, { useEffect, useRef } from "react";
import { MdDone, MdDoneAll } from "react-icons/md";
import {
@@ -52,7 +52,7 @@ export default function ChatMessageListComponent({ messages }) {
<div style={{ fontSize: 10 }}>
{i18n.t("messaging.labels.sentby", {
by: messages[index].userid,
time: dayjs(messages[index].created_at).format(
time: moment(messages[index].created_at).format(
"MM/DD/YYYY @ hh:mm a"
),
})}

View File

@@ -8,23 +8,15 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { searchingForConversation } from "../../redux/messaging/messaging.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
searchingForConversation: searchingForConversation,
});
const mapDispatchToProps = (dispatch) => ({
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
});
export function ChatOpenButton({
bodyshop,
searchingForConversation,
phone,
jobid,
openChatByPhone,
}) {
export function ChatOpenButton({ bodyshop, phone, jobid, openChatByPhone }) {
const { t } = useTranslation();
if (!phone) return <></>;
@@ -37,7 +29,7 @@ export function ChatOpenButton({
onClick={(e) => {
e.stopPropagation();
const p = parsePhoneNumber(phone, "CA");
if (searchingForConversation) return; //This is to prevent finding the same thing twice.
if (p && p.isValid()) {
openChatByPhone({ phone_num: p.formatInternational(), jobid: jobid });
} else {

View File

@@ -132,7 +132,7 @@ export function ChatPopupComponent({
onClick={() => toggleChatVisible()}
style={{ cursor: "pointer" }}
>
<MessageOutlined className="chat-popup-info-icon" />
<MessageOutlined />
<strong>{t("messaging.labels.messaging")}</strong>
</div>
)}

View File

@@ -13,9 +13,6 @@
height: 100%;
}
}
.chat-popup-info-icon {
margin-right: 5px;
}
@media only screen and (min-width: 992px) {
.chat-popup {

View File

@@ -1,5 +1,5 @@
import { PlusCircleOutlined } from "@ant-design/icons";
import { Dropdown } from "antd";
import { Dropdown, Menu } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -16,16 +16,19 @@ const mapDispatchToProps = (dispatch) => ({
});
export function ChatPresetsComponent({ bodyshop, setMessage, className }) {
const items = bodyshop.md_messaging_presets.map((i, idx) => ({
key: idx,
label: (i.label),
onClick: () => setMessage(i.text),
}));
const menu = (
<Menu>
{bodyshop.md_messaging_presets.map((i, idx) => (
<Menu.Item onClick={() => setMessage(i.text)} key={idx}>
{i.label}
</Menu.Item>
))}
</Menu>
);
return (
<div className={className}>
<Dropdown trigger={["click"]} menu={{items}}>
<Dropdown trigger={["click"]} overlay={menu}>
<PlusCircleOutlined />
</Dropdown>
</div>

View File

@@ -1,45 +0,0 @@
import { MailOutlined, PrinterOutlined } from "@ant-design/icons";
import { Space, Spin } from "antd";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setEmailOptions } from "../../redux/email/email.actions";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
});
export function ChatPrintButton({ conversation }) {
const [loading, setLoading] = useState(false);
const generateDocument = (type) => {
setLoading(true);
GenerateDocument(
{
name: TemplateList("messaging").conversation_list.key,
variables: { id: conversation.id },
},
{
subject: TemplateList("messaging").conversation_list.subject,
},
type,
conversation.id
).catch(e => {
console.warn('Something went wrong generating a document.');
});
setLoading(false);
}
return (
<Space wrap>
<PrinterOutlined onClick={() => generateDocument('p')}/>
<MailOutlined onClick={() => generateDocument('e')}/>
{loading && <Spin />}
</Space>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatPrintButton);

View File

@@ -9,17 +9,17 @@ export default function ChatTagRoComponent({
loading,
handleSearch,
handleInsertTag,
setOpen,
setVisible,
}) {
const { t } = useTranslation();
return (
<Space>
<Space flex>
<div style={{ width: "15rem" }}>
<Select
showSearch
autoFocus
popupMatchSelectWidth
dropdownMatchSelectWidth
placeholder={t("general.labels.search")}
filterOption={false}
onSearch={handleSearch}
@@ -38,7 +38,7 @@ export default function ChatTagRoComponent({
{loading ? (
<LoadingOutlined />
) : (
<CloseCircleOutlined onClick={() => setOpen(false)} />
<CloseCircleOutlined onClick={() => setVisible(false)} />
)}
</Space>
);

View File

@@ -11,7 +11,7 @@ import ChatTagRo from "./chat-tag-ro.component";
export default function ChatTagRoContainer({ conversation }) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [visible, setVisible] = useState(false);
const [loadRo, { loading, data }] = useLazyQuery(SEARCH_FOR_JOBS);
@@ -33,7 +33,7 @@ export default function ChatTagRoContainer({ conversation }) {
const handleInsertTag = (value, option) => {
logImEXEvent("messaging_add_job_tag");
insertTag({ variables: { jobId: option.key } });
setOpen(false);
setVisible(false);
};
const existingJobTags =
@@ -47,16 +47,16 @@ export default function ChatTagRoContainer({ conversation }) {
return (
<div>
{open ? (
{visible ? (
<ChatTagRo
loading={loading}
roOptions={roOptions}
handleSearch={handleSearch}
handleInsertTag={handleInsertTag}
setOpen={setOpen}
setVisible={setVisible}
/>
) : (
<Tag onClick={() => setOpen(true)}>
<Tag onClick={() => setVisible(true)}>
<PlusOutlined />
{t("messaging.actions.link")}
</Tag>

View File

@@ -35,15 +35,6 @@ export default function ContractsCarsComponent({
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => <div>{t(record.status)}</div>,
},
{
title: t("courtesycars.fields.readiness"),
dataIndex: "readiness",
key: "readiness",
sorter: (a, b) => alphaSort(a.readiness, b.readiness),
sortOrder:
state.sortedInfo.columnKey === "readiness" && state.sortedInfo.order,
render: (text, record) => t(record.readiness),
},
{
title: t("courtesycars.fields.year"),
dataIndex: "year",
@@ -68,14 +59,6 @@ export default function ContractsCarsComponent({
sortOrder:
state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
},
{
title: t("courtesycars.fields.color"),
dataIndex: "color",
key: "color",
sorter: (a, b) => alphaSort(a.color, b.color),
sortOrder:
state.sortedInfo.columnKey === "color" && state.sortedInfo.order,
},
{
title: t("courtesycars.fields.plate"),
dataIndex: "plate",
@@ -110,9 +93,6 @@ export default function ContractsCarsComponent({
(cc.model || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(cc.color || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(cc.plate || "").toLowerCase().includes(state.search.toLowerCase())
);

View File

@@ -1,5 +1,5 @@
import { useQuery } from "@apollo/client";
import dayjs from "../../utils/day";
import moment from "moment";
import React from "react";
import { QUERY_AVAILABLE_CC } from "../../graphql/courtesy-car.queries";
import AlertComponent from "../alert/alert.component";
@@ -7,7 +7,7 @@ import ContractCarsComponent from "./contract-cars.component";
export default function ContractCarsContainer({ selectedCarState, form }) {
const { loading, error, data } = useQuery(QUERY_AVAILABLE_CC, {
variables: { today: dayjs().format("YYYY-MM-DD") },
variables: { today: moment().format("YYYY-MM-DD") },
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});

View File

@@ -10,11 +10,11 @@ import {
Space,
} from "antd";
import axios from "axios";
import dayjs from "../../utils/day";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useNavigate } from "react-router-dom";
import { useHistory } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { INSERT_NEW_JOB } from "../../graphql/jobs.queries";
import {
@@ -38,17 +38,17 @@ export function ContractConvertToRo({
disabled,
}) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const [insertJob] = useMutation(INSERT_NEW_JOB);
const history = useNavigate();
const history = useHistory();
const handleFinish = async (values) => {
setLoading(true);
const contractLength = dayjs(contract.actualreturn).diff(
dayjs(contract.start),
"day"
const contractLength = moment(contract.actualreturn).diff(
moment(contract.start),
"days"
);
const billingLines = [];
if (contractLength > 0)
@@ -306,7 +306,7 @@ export function ContractConvertToRo({
});
}
setOpen(false);
setVisible(false);
setLoading(false);
};
@@ -380,7 +380,7 @@ export function ContractConvertToRo({
<Button type="primary" htmlType="submit" loading={loading}>
{t("contracts.actions.convertoro")}
</Button>
<Button onClick={() => setOpen(false)}>
<Button onClick={() => setVisible(false)}>
{t("general.actions.close")}
</Button>
</Space>
@@ -390,9 +390,9 @@ export function ContractConvertToRo({
return (
<div>
<Popover content={popContent} open={open}>
<Popover content={popContent} visible={visible}>
<Button
onClick={() => setOpen(true)}
onClick={() => setVisible(true)}
loading={loading}
disabled={!contract.dailyrate || !contract.actualreturn || disabled}
>

View File

@@ -1,6 +1,6 @@
import { WarningFilled } from "@ant-design/icons";
import { Form, Input, InputNumber, Space } from "antd";
import dayjs from "../../utils/day";
import moment from "moment";
import React from "react";
import { useTranslation } from "react-i18next";
import { DateFormatter } from "../../utils/DateFormatter";
@@ -96,8 +96,8 @@ export default function ContractFormComponent({
const dueForService =
selectedCar &&
selectedCar.nextservicedate &&
dayjs(selectedCar.nextservicedate).isBefore(
dayjs(form.getFieldValue("scheduledreturn"))
moment(selectedCar.nextservicedate).isBefore(
moment(form.getFieldValue("scheduledreturn"))
);
if (mileageOver || dueForService)
@@ -190,9 +190,9 @@ export default function ContractFormComponent({
}
>
{() => {
const dlExpiresBeforeReturn = dayjs(
const dlExpiresBeforeReturn = moment(
form.getFieldValue("driver_dlexpiry")
).isBefore(dayjs(form.getFieldValue("scheduledreturn")));
).isBefore(moment(form.getFieldValue("scheduledreturn")));
return (
<div>

View File

@@ -4,7 +4,6 @@ import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import {pageLimit} from "../../utils/config";
export default function ContractsJobsComponent({
loading,
@@ -176,7 +175,7 @@ export default function ContractsJobsComponent({
loading={loading}
pagination={{
position: "top",
defaultPageSize: pageLimit,
defaultPageSize: 10,
defaultCurrent: defaultCurrent,
}}
columns={columns}

View File

@@ -2,10 +2,10 @@ import { useQuery } from "@apollo/client";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import ContractJobsComponent from "./contract-jobs.component";
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -15,7 +15,6 @@ export function ContractJobsContainer({ selectedJobState, bodyshop }) {
const { loading, error, data } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
variables: {
statuses: bodyshop.md_ro_statuses.active_statuses || ["Open"],
isConverted: true,
},
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",

View File

@@ -1,5 +1,5 @@
import { Button, Input, Modal, Typography } from "antd";
import dayjs from "../../utils/day";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import aamva from "../../utils/aamva";
@@ -26,8 +26,8 @@ export default function ContractLicenseDecodeButton({ form }) {
const values = {
driver_dlnumber: decodedBarcode.dl,
driver_dlexpiry: dayjs(
`20${decodedBarcode.expiration_date}${dayjs(
driver_dlexpiry: moment(
`20${decodedBarcode.expiration_date}${moment(
decodedBarcode.birthday
).format("DD")}`
),
@@ -38,7 +38,7 @@ export default function ContractLicenseDecodeButton({ form }) {
driver_city: decodedBarcode.city,
driver_state: decodedBarcode.state,
driver_zip: decodedBarcode.postal_code,
driver_dob: dayjs(decodedBarcode.birthday),
driver_dob: moment(decodedBarcode.birthday),
};
form.setFieldsValue(values);
@@ -55,7 +55,7 @@ export default function ContractLicenseDecodeButton({ form }) {
return (
<div>
<Modal
open={modalVisible}
visible={modalVisible}
okText={t("contracts.actions.senddltoform")}
onOk={handleInsertForm}
okButtonProps={{ disabled: !!!decodedBarcode }}
@@ -94,14 +94,14 @@ export default function ContractLicenseDecodeButton({ form }) {
{decodedBarcode.address}
</DataLabel>
<DataLabel label={t("contracts.fields.driver_dlexpiry")}>
{dayjs(
`20${decodedBarcode.expiration_date}${dayjs(
{moment(
`20${decodedBarcode.expiration_date}${moment(
decodedBarcode.birthday
).format("DD")}`
).format("MM/DD/YYYY")}
</DataLabel>
<DataLabel label={t("contracts.fields.driver_dob")}>
{dayjs(decodedBarcode.birthday).format("MM/DD/YYYY")}
{moment(decodedBarcode.birthday).format("MM/DD/YYYY")}
</DataLabel>
<div>
<Typography.Title level={4}>

View File

@@ -31,7 +31,7 @@ export function ContractsFindModalContainer({
}) {
const { t } = useTranslation();
const { open } = contractFinderModal;
const { visible } = contractFinderModal;
const [form] = Form.useForm();
@@ -52,14 +52,14 @@ export function ContractsFindModalContainer({
};
useEffect(() => {
if (open) {
if (visible) {
form.resetFields();
}
}, [open, form]);
}, [visible, form]);
return (
<Modal
open={open}
visible={visible}
width="70%"
title={t("contracts.labels.findermodal")}
onCancel={() => toggleModalVisible()}

View File

@@ -3,17 +3,16 @@ import { Button, Card, Input, Space, Table, Typography } from "antd";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useNavigate, useLocation } from "react-router-dom";
import { setModalContext } from "../../redux/modals/modals.actions";
import { Link, useHistory, useLocation } from "react-router-dom";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import ContractsFindModalContainer from "../contracts-find-modal/contracts-find-modal.container";
import { setModalContext } from "../../redux/modals/modals.actions";
import dayjs from "../../utils/day";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import moment from "moment";
import { selectBodyshop } from "../../redux/user/user.selectors";
import {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -39,7 +38,7 @@ export function ContractsList({
sortedInfo: {},
filteredInfo: { text: "" },
});
const history = useNavigate();
const history = useHistory();
const search = queryString.parse(useLocation().search);
const { page } = search;
@@ -152,8 +151,8 @@ export function ContractsList({
render: (text, record) =>
(record.actualreturn &&
record.start &&
`${dayjs(record.actualreturn)
.diff(dayjs(record.start), "day", true)
`${moment(record.actualreturn)
.diff(moment(record.start), "days", true)
.toFixed(1)} days`) ||
"",
},
@@ -164,7 +163,7 @@ export function ContractsList({
search.page = pagination.current;
search.sortcolumn = sorter.columnKey;
search.sortorder = sorter.order;
history({ search: queryString.stringify(search) });
history.push({ search: queryString.stringify(search) });
};
return (
@@ -179,7 +178,7 @@ export function ContractsList({
<Button
onClick={() => {
delete search.search;
history({ search: queryString.stringify(search) });
history.push({ search: queryString.stringify(search) });
}}
>
{t("general.actions.clear")}
@@ -196,7 +195,7 @@ export function ContractsList({
placeholder={search.searh || t("general.labels.search")}
onSearch={(value) => {
search.search = value;
history({ search: queryString.stringify(search) });
history.push({ search: queryString.stringify(search) });
}}
/>
</Space>
@@ -210,7 +209,7 @@ export function ContractsList({
}}
pagination={{
position: "top",
pageSize: pageLimit,
pageSize: 25,
current: parseInt(page || 1),
total: total,
}}

View File

@@ -1,5 +1,5 @@
import { DownOutlined } from "@ant-design/icons";
import { Dropdown } from "antd";
import { Dropdown, Menu } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -18,16 +18,20 @@ export function ContractsRatesChangeButton({ disabled, form, bodyshop }) {
form.setFieldsValue(rate);
};
const menuItems = bodyshop.md_ccc_rates.map((i, idx) => ({
key: idx,
label: i.label,
value: i,
}));
const menu = {items: menuItems, onClick: handleClick};
const menu = (
<div>
<Menu onClick={handleClick}>
{bodyshop.md_ccc_rates.map((rate, idx) => (
<Menu.Item value={rate} key={idx}>
{rate.label}
</Menu.Item>
))}
</Menu>
</div>
);
return (
<Dropdown menu={menu} disabled={disabled}>
<Dropdown overlay={menu} disabled={disabled}>
<a
className="ant-dropdown-link"
href=" #"

View File

@@ -2,18 +2,17 @@ import { Card, Table } from "antd";
import queryString from "query-string";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { Link, useHistory, useLocation } from "react-router-dom";
import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import {pageLimit} from "../../utils/config";
export default function CourtesyCarContractListComponent({
contracts,
totalContracts,
}) {
const search =queryString.parse(useLocation().search);
const search = queryString.parse(useLocation().search);
const { page, sortcolumn, sortorder } = search;
const history = useNavigate();
const history = useHistory();
const { t } = useTranslation();
@@ -81,7 +80,7 @@ export default function CourtesyCarContractListComponent({
search.page = pagination.current;
search.sortcolumn = sorter.columnKey;
search.sortorder = sorter.order;
history({ search: queryString.stringify(search) });
history.push({ search: queryString.stringify(search) });
};
return (
@@ -90,7 +89,7 @@ export default function CourtesyCarContractListComponent({
scroll={{ x: true }}
pagination={{
position: "top",
pageSize: pageLimit,
pageSize: 25,
current: parseInt(page || 1),
total: totalContracts,
}}

View File

@@ -1,14 +1,12 @@
import { WarningFilled } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client";
import { Button, Form, Input, InputNumber, Space } from "antd";
import {PageHeader} from "@ant-design/pro-layout";
import dayjs from "../../utils/day";
import { Button, Form, Input, InputNumber, PageHeader, Space } from "antd";
import moment from "moment";
import React from "react";
import { useTranslation } from "react-i18next";
import { CHECK_CC_FLEET_NUMBER } from "../../graphql/courtesy-car.queries";
import { DateFormatter } from "../../utils/DateFormatter";
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 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";
@@ -36,18 +34,6 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
{/* <FormFieldsChanged form={form} /> */}
<LayoutFormRow header={t("courtesycars.labels.vehicle")}>
<Form.Item
label={t("courtesycars.fields.year")}
name="year"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.make")}
name="make"
@@ -72,6 +58,18 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
>
<Input />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.year")}
name="year"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.plate")}
name="plate"
@@ -120,7 +118,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
},
]}
>
<InputNumber min={0} precision={0} />
<InputNumber precision={0} />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.fleetnumber")}
@@ -215,27 +213,49 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
>
<CourtesyCarStatus />
</Form.Item>
<Form.Item label={t("courtesycars.fields.readiness")} name="readiness">
<CourtesyCarReadiness />
<Form.Item
label={t("courtesycars.fields.nextservicekm")}
name="nextservicekm"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber />
</Form.Item>
<div>
<Form.Item
label={t("courtesycars.fields.nextservicekm")}
name="nextservicekm"
label={t("courtesycars.fields.nextservicedate")}
name="nextservicedate"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber min={0} precision={0} />
<FormDatePicker />
</Form.Item>
<Form.Item
shouldUpdate={(p, c) =>
p.mileage !== c.mileage || p.nextservicekm !== c.nextservicekm
p.mileage !== c.mileage ||
p.nextservicedate !== c.nextservicedate ||
p.nextservicekm !== c.nextservicekm
}
>
{() => {
const nextservicedate = form.getFieldValue("nextservicedate");
const nextservicekm = form.getFieldValue("nextservicekm");
const mileageOver = nextservicekm
? nextservicekm <= form.getFieldValue("mileage")
: false;
if (mileageOver)
const mileageOver =
nextservicekm <= form.getFieldValue("mileage");
const dueForService =
nextservicedate && moment(nextservicedate).isBefore(moment());
if (mileageOver || dueForService)
return (
<Space direction="vertical" style={{ color: "tomato" }}>
<span>
@@ -243,36 +263,6 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
{t("contracts.labels.cardueforservice")}
</span>
<span>{`${nextservicekm} km`}</span>
</Space>
);
return <></>;
}}
</Form.Item>
</div>
<div>
<Form.Item
label={t("courtesycars.fields.nextservicedate")}
name="nextservicedate"
>
<FormDatePicker />
</Form.Item>
<Form.Item
shouldUpdate={(p, c) => p.nextservicedate !== c.nextservicedate}
>
{() => {
const nextservicedate = form.getFieldValue("nextservicedate");
const dueForService =
nextservicedate &&
dayjs(nextservicedate).endOf("day").isSameOrBefore(dayjs());
if (dueForService)
return (
<Space direction="vertical" style={{ color: "tomato" }}>
<span>
<WarningFilled style={{ marginRight: ".3rem" }} />
{t("contracts.labels.cardueforservice")}
</span>
<span>
<DateFormatter>{nextservicedate}</DateFormatter>
</span>
@@ -293,6 +283,12 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
<Form.Item
label={t("courtesycars.fields.registrationexpires")}
name="registrationexpires"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<FormDatePicker />
</Form.Item>
@@ -304,8 +300,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
{() => {
const expires = form.getFieldValue("registrationexpires");
const dateover =
expires && dayjs(expires).endOf("day").isBefore(dayjs());
const dateover = expires && moment(expires).isBefore(moment());
if (dateover)
return (
@@ -335,13 +330,14 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
<FormDatePicker />
</Form.Item>
<Form.Item
shouldUpdate={(p, c) => p.insuranceexpires !== c.insuranceexpires}
shouldUpdate={(p, c) =>
p.insuranceexpires !== c.insuranceexpires
}
>
{() => {
const expires = form.getFieldValue("insuranceexpires");
const dateover =
expires && dayjs(expires).endOf("day").isBefore(dayjs());
const dateover = expires && moment(expires).isBefore(moment());
if (dateover)
return (

View File

@@ -34,32 +34,6 @@ const CourtesyCarFuelComponent = (props, ref) => {
step={null}
style={{ marginLeft: "2rem", marginRight: "2rem" }}
{...props}
tooltip={{
formatter: (value) => {
switch (value) {
case 0:
return t("courtesycars.labels.fuel.empty");
case 13:
return t("courtesycars.labels.fuel.18");
case 25:
return t("courtesycars.labels.fuel.14");
case 38:
return t("courtesycars.labels.fuel.38");
case 50:
return t("courtesycars.labels.fuel.12");
case 63:
return t("courtesycars.labels.fuel.58");
case 75:
return t("courtesycars.labels.fuel.34");
case 88:
return t("courtesycars.labels.fuel.78");
case 100:
return t("courtesycars.labels.fuel.full");
default:
return value;
}
},
}}
/>
);
};

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