Compare commits

...

524 Commits

Author SHA1 Message Date
Allan Carr
98bff6d8f6 IO-2824 Dev Server Instance Switch
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-20 10:56:07 -07:00
Dave Richer
7959dc67ce Merged in release/AIO/2024-06-07 (pull request #1482)
Release/AIO/2024 06 07
2024-06-11 00:38:31 +00:00
Allan Carr
a4116b6c28 Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1480)
IO-2793 Correction for Sublet Part Tax
2024-06-10 21:41:17 +00:00
Allan Carr
0d2cdec75c IO-2793 Correction for Sublet Part Tax
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-10 14:42:21 -07:00
Allan Carr
269ef25ece IO-2793 Better tax handling
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-10 14:14:15 -07:00
Allan Carr
575fbd5357 Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1478)
IO-2793 Better tax handling
2024-06-10 21:13:20 +00:00
Allan Carr
2ad887fb82 Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1476)
IO-2793 Correct passed Variable
2024-06-10 18:25:27 +00:00
Allan Carr
40a1a86f72 IO-2793 Correct passed Variable
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-10 11:25:06 -07:00
Allan Carr
95d43d936c Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1474)
IO-2793 Correct variables
2024-06-10 18:03:12 +00:00
Allan Carr
9f56568680 IO-2793 Correct variables
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-10 10:58:01 -07:00
Allan Carr
3428940c72 Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1472)
IO-2793 Change to function for better clarity
2024-06-10 17:49:06 +00:00
Allan Carr
ea604a5e64 IO-2793 Change to function for better clarity
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-10 09:59:45 -07:00
Allan Carr
5b76473cbc IO-2793 Correct Parts Side for Taxes
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-07 15:59:51 -07:00
Allan Carr
db5359e086 Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1470)
Feature/IO-2793 State Tax Null QBO
2024-06-07 22:58:58 +00:00
Allan Carr
35046f11c2 IO-2793 Add Comment to see output for testing
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-07 14:31:33 -07:00
Allan Carr
69d8d27ad3 Merged in feature/IO-2814-Job-Costing-Correction (pull request #1467)
IO-2814 Remove Comment as api.test.romeonline.io doesn't update w/ CICD
2024-06-07 19:16:01 +00:00
Allan Carr
f4c4005a2a IO-2814 Remove Comment as api.test.romeonline.io doesn't update w/ CICD
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-07 12:16:59 -07:00
Allan Carr
755acd24f0 IO-2814 Correct Parts Price and add Console Log
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-07 11:40:39 -07:00
Allan Carr
079d6cfee6 Merged in feature/IO-2814-Job-Costing-Correction (pull request #1465)
IO-2814 Correct Parts Price and add Console Log
2024-06-07 18:39:43 +00:00
Allan Carr
3e3b3c269a Merged in feature/IO-2814-Job-Costing-Correction (pull request #1462)
IO-2814 Job Cost Correction
2024-06-07 03:18:49 +00:00
Allan Carr
39f1af7d4b Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1463)
IO-2793 State Tax Null QBO
2024-06-07 03:18:40 +00:00
Allan Carr
1ea4d616d7 IO-2793 State Tax Null QBO
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-06 15:00:58 -07:00
Allan Carr
fdf0ecf6f6 IO-2814 Job Cost Correction
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-06 14:51:44 -07:00
Allan Carr
e5f7285253 Merge branch 'feature/IO-2793-State-Tax-Null-QBO' into release/AIO/2024-06-07
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>

# Conflicts:
#	server/accounting/qb-receivables-lines.js
2024-06-03 17:28:18 -07:00
Allan Carr
e46c304f7c IO-2793 State Tax to QBO refactor
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-03 17:23:33 -07:00
Dave Richer
1aa570db90 Merged in hotfix/revert-tax-import-code (pull request #1459)
revert previous change due to problems with tax import.
2024-06-03 22:04:31 +00:00
Dave Richer
1c2be3c890 Merged in hotfix/revert-tax-import-code (pull request #1458)
revert previous change due to problems with tax import.
2024-06-03 22:04:05 +00:00
Dave Richer
05e4eacf34 revert previous change due to problems with tax import.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-06-03 18:03:22 -04:00
Allan Carr
e1417b03f9 Merged in feature/IO-2801-os-loader-bills-data (pull request #1456)
IO-2801 os-loader bills data

Approved-by: Dave Richer
2024-06-03 18:59:08 +00:00
Allan Carr
7b99de8046 IO-2801 os-loader bills data
add in job object to bills in opensearch for ro_number to be searchable again

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-03 11:50:44 -07:00
Dave Richer
6dc03d7f67 Merged in release/AIO/2024-05-31 (pull request #1452)
Release/AIO/2024 05 31
2024-06-01 17:19:20 +00:00
Allan Carr
2a5e6a51aa Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1451)
IO-2793 State Tax Rate Null send to QBO

Approved-by: Dave Richer
2024-06-01 17:16:55 +00:00
Allan Carr
885477fa68 IO-2793 State Tax Rate Null send to QBO
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-05-31 12:31:49 -07:00
Dave Richer
c7db792793 Merged in feature/IO-2796-Header-IDS (pull request #1450)
- Add Additional IDS to headers
2024-05-31 17:23:55 +00:00
Dave Richer
31be51ef79 Merged release/AIO/2024-05-31 into feature/IO-2796-Header-IDS 2024-05-31 17:23:45 +00:00
Dave Richer
e4066c1570 - Add Additional IDS to headers
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-31 13:20:54 -04:00
Patrick Fic
ec480f2cb1 Resolve Bill Posting errors on Beta. 2024-05-29 14:08:49 -07:00
Patrick Fic
f5b30c9376 Resolve API Route. 2024-05-28 12:23:42 -07:00
Patrick Fic
412508fbcf Merge branch 'hotfix/AIO/2024-05-28' into master-AIO 2024-05-28 12:19:28 -07:00
Patrick Fic
dbfd9bce54 Resolve region for US intellipay. 2024-05-28 12:19:18 -07:00
Dave Richer
52e0558c79 Merged in release/AIO/2024-05-24 (pull request #1449)
Release/AIO/2024 05 24
2024-05-24 20:45:23 +00:00
Allan Carr
562d0b8641 Merged in feature/IO-2785-IO-Rescue-Header (pull request #1444)
IO-2785 AIO IO Header Rescue Link

Approved-by: Dave Richer
2024-05-23 19:47:27 +00:00
Patrick Fic
d48d6cdd91 Merge branch 'hotfix/AIO/2024-05-23' into release/AIO/2024-05-24 2024-05-23 11:40:35 -07:00
Patrick Fic
9363790541 Merge branch 'hotfix/AIO/2024-05-23' into master-AIO 2024-05-23 10:58:13 -07:00
Patrick Fic
2bceba948d Resolve postback logging. 2024-05-23 10:57:39 -07:00
Allan Carr
168d4246af IO-2785 AIO IO Header Rescue Link
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-05-22 11:43:19 -07:00
Patrick Fic
cdf6050ec2 Merge branch 'release/AIO/2024-05-17' into release/AIO/2024-05-24 2024-05-22 08:43:41 -07:00
Patrick Fic
f451155689 Add Firebase Service Worker to public. 2024-05-15 10:27:42 -07:00
Dave Richer
9f06e19346 Fix eslint regression
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-14 12:38:56 -04:00
Dave Richer
f4be3e9668 Fix issue with global search 2024-05-14 12:20:49 -04:00
Patrick Fic
ee613db0cb Merged in test-AIO (pull request #1443)
Resolve instance conflict for promanager.
2024-05-13 15:26:07 +00:00
Patrick Fic
55b892a74e Resolve instance conflict for promanager. 2024-05-13 08:25:45 -07:00
Patrick Fic
16754d9657 Merged in test-AIO (pull request #1442)
Test AIO
2024-05-10 22:36:17 +00:00
Patrick Fic
33db67122c Fix RBAC spread. 2024-05-10 15:35:26 -07:00
Patrick Fic
b9b88b0e23 Merged in release/AIO/2024-05-10 (pull request #1441)
Release/AIO/2024 05 10
2024-05-10 16:11:42 +00:00
Patrick Fic
992ad71910 Merge branch 'release/AIO/2024-05-10' into test-AIO 2024-05-10 08:33:31 -07:00
Patrick Fic
51d1f926c2 Improve spread for feature wrapped RBAC items. 2024-05-10 08:29:52 -07:00
Patrick Fic
c70447f337 Merge branch 'release/AIO/2024-05-10' into test-AIO 2024-05-10 08:08:32 -07:00
Patrick Fic
dc367e1a30 Add PAE Part Tax type for USA. 2024-05-10 08:08:15 -07:00
Patrick Fic
ee7997ffbc Merge branch 'release/AIO/2024-05-10' into test-AIO 2024-05-08 15:16:55 -07:00
Patrick Fic
2dbb5adbbb Merge branch 'feature/AIO/promanager' into release/AIO/2024-05-10 2024-05-08 15:16:47 -07:00
Patrick Fic
cc9f342575 Update expired page, remove RBAC, and prevent sign in. 2024-05-08 15:16:03 -07:00
Patrick Fic
252262f4a7 Merge branch 'master-AIO' into feature/AIO/promanager 2024-05-08 14:21:55 -07:00
Patrick Fic
f77a16648f Merged in release/AIO/2024-04-26 (pull request #1440)
Release/AIO/2024 04 26
2024-05-03 21:42:14 +00:00
Patrick Fic
d6e3c54b68 Merge branch 'release/AIO/2024-04-26' into test-AIO 2024-05-03 14:38:38 -07:00
Patrick Fic
7294e96a77 Merge branch 'release/2024-04-26' into release/AIO/2024-04-26 2024-05-03 14:38:04 -07:00
Allan Carr
a4f4f45251 Merged in feature/IO-2717-Paint-Material-from-Scale-for-CDK (pull request #1438)
IO-2717 CDK Use Scale Data for Paint Materials Cost
2024-05-02 05:53:35 +00:00
Allan Carr
acc91abc0c IO-2717 CDK Use Scale Data for Paint Materials Cost
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-05-01 13:26:24 -07:00
Patrick Fic
18d5176cb9 Merge branch 'release/2024-04-26' into release/AIO/2024-04-26 2024-05-01 11:49:09 -07:00
Patrick Fic
d6d6ced7a4 CC modifications. 2024-05-01 11:47:42 -07:00
Patrick Fic
52809cc849 Merge branch 'release/AIO/2024-04-26' into test-AIO 2024-04-29 12:10:03 -07:00
Patrick Fic
c525b7ea3f Place featurewrapped on bills in job line expander. 2024-04-29 12:06:42 -07:00
Patrick Fic
e799417aaf Remove header for partner interaction to comply with CORS. 2024-04-29 10:50:18 -07:00
Patrick Fic
ef077c2d48 Add render manager for part price changes. 2024-04-29 10:33:34 -07:00
Patrick Fic
e78b114544 Merge branch 'release/AIO/2024-04-26' into test-AIO 2024-04-26 08:18:25 -07:00
Patrick Fic
df170ddd27 Merge branch 'release/2024-04-26' into release/AIO/2024-04-26 2024-04-25 17:24:59 -07:00
Patrick Fic
17928741e3 Merge branch 'feature/IO-2766-intellipay-postback-refactor' into release/2024-04-26 2024-04-25 16:44:55 -07:00
Patrick Fic
a6b825ffdf Move intellipay to server side processing. 2024-04-25 16:43:49 -07:00
Patrick Fic
0d3bc0e95f Merge branch 'hotfix/AIO/2024-04-24' into release/AIO/2024-04-26 2024-04-25 08:19:00 -07:00
Patrick Fic
0f62a2d73d Merge branch 'hotfix/AIO/2024-04-24' into master-AIO 2024-04-24 11:13:22 -07:00
Patrick Fic
bcdd32f92f Merge branch 'hotfix/AIO/2024-04-24' into test-AIO 2024-04-24 10:52:18 -07:00
Patrick Fic
6865fcbffc Missing import instance 2024-04-24 10:51:09 -07:00
Patrick Fic
8e623c71a9 Merge branch 'hotfix/AIO/2024-04-24' into test-AIO 2024-04-24 10:17:40 -07:00
Patrick Fic
1e06502464 Correct instance manager for job totals in other files. 2024-04-24 10:17:28 -07:00
Allan Carr
f2914868e2 Merged in feature/IO-2761-Actual-Complete-in-Vehicle-Owner-Details (pull request #1434)
IO-2761 Actual Completion in Vehicle and Owners Job Details lists

Approved-by: Dave Richer
2024-04-23 17:40:10 +00:00
Allan Carr
561f0313f9 Merged in feature/IO-2762-Return-From-Bill-Reference (pull request #1435)
IO-2762 Return from Bill Reference in Parts Return Drawer

Approved-by: Dave Richer
2024-04-23 17:39:27 +00:00
Allan Carr
1e27c4e1ae Merged in feature/IO-2763-Job-Action-Button (pull request #1436)
IO-2763 Job Action Button

Approved-by: Dave Richer
2024-04-23 17:38:28 +00:00
Allan Carr
46676ba8eb IO-2763 Job Action Button
Create Courtesy Car and Create Task items added

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-22 15:18:13 -07:00
Allan Carr
02a49efbea IO-2762 Return from Bill Reference in Parts Return Drawer
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-22 14:14:54 -07:00
Patrick Fic
5f2a5e1025 Merge branch 'release/AIO/2024-04-26' into test-AIO 2024-04-22 14:03:50 -07:00
Patrick Fic
42552e4f4f Promanager resolution & remove product fruit dev items. 2024-04-22 14:03:29 -07:00
Patrick Fic
f2af78f056 Merge branch 'release/AIO/2024-04-26' into test-AIO 2024-04-22 13:15:11 -07:00
Patrick Fic
269d55bde3 Add missing translations. 2024-04-22 13:14:58 -07:00
Patrick Fic
71f3dbbeb4 Merge branch 'release/AIO/2024-04-26' into test-AIO 2024-04-22 12:39:53 -07:00
Patrick Fic
73dfb74ed7 ProManager instance server updates. 2024-04-22 12:39:30 -07:00
Patrick Fic
7dd6baef33 Hardcore generic template for ProManager. 2024-04-22 11:28:49 -07:00
Patrick Fic
519b532091 Add download messages for partner. 2024-04-22 11:28:36 -07:00
Allan Carr
4fb9c37c0d IO-2761 Actual Completion in Vehicle and Owners Job Details lists
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-22 09:15:59 -07:00
Patrick Fic
c9b63be29f Hardcore generic template for ProManager. 2024-04-22 08:43:41 -07:00
Patrick Fic
84bc735dce Merged in test-AIO (pull request #1432)
Test AIO
2024-04-19 19:43:56 +00:00
Patrick Fic
a44b9417a1 Merged in feature/IO-2677-Tasks (pull request #1433)
Resolve missing query paramters.
2024-04-19 19:41:48 +00:00
Patrick Fic
6b157ed43c Resolve missing query paramters. 2024-04-19 12:41:26 -07:00
Allan Carr
9050276ea7 Merged in feature/IO-2677-Tasks (pull request #1431)
Feature/IO-2677 Tasks

Approved-by: Patrick Fic
2024-04-19 18:35:50 +00:00
Allan Carr
683293d042 IO-2667 Tasks adjust for no RO
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-19 11:34:18 -07:00
Allan Carr
70d009ab49 IO-2667 Tasks Change multi subject line
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-19 11:27:46 -07:00
Allan Carr
dbc2d10d6d IO-2667 Tasks Email Generation
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-19 11:25:02 -07:00
Patrick Fic
059b854db9 Merged in feature/IO-2677-Tasks (pull request #1430)
Update reminder interval and resolve server side.
2024-04-19 17:54:29 +00:00
Patrick Fic
13569a1785 Update reminder interval and resolve server side. 2024-04-19 10:53:25 -07:00
Dave Richer
6fd70b165b Merged in feature/IO-2760-IDS-for-headers (pull request #1429)
- Add Additional tags (prettier also fixed some double spaced imports)
2024-04-19 17:32:04 +00:00
Dave Richer
11d94cf286 Merged in feature/IO-2760-IDS-for-headers (pull request #1428)
- Add Additional tags (prettier also fixed some double spaced imports)

Approved-by: Patrick Fic
2024-04-19 17:21:40 +00:00
Dave Richer
c98a48ea14 - Add Additional tags (prettier also fixed some double spaced imports)
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-19 12:57:45 -04:00
Allan Carr
b8fa80419b Merged in feature/IO-2677-Tasks (pull request #1427)
Feature/IO-2677 Tasks

Approved-by: Patrick Fic
2024-04-19 16:29:32 +00:00
Patrick Fic
2a8846297f Update assigned_to handling on front end & debug task assignment. 2024-04-19 09:22:20 -07:00
Patrick Fic
fb322f760f Hasura migration changes to change assigned_to to uuid not email. 2024-04-19 08:10:45 -07:00
Allan Carr
0b9f718106 IO-2667 Adjust Email messages
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-18 18:25:52 -07:00
Patrick Fic
57b3b3a9cd Remove hasura constraint. 2024-04-18 16:42:10 -07:00
Patrick Fic
6513432993 Merge branch 'feature/IO-2677-Tasks' into test-AIO 2024-04-18 16:28:20 -07:00
Patrick Fic
80a564d4b6 Add back hasura metadata for tasks. 2024-04-18 16:27:51 -07:00
Allan Carr
132ecffc40 Merged in feature/IO-2677-Tasks (pull request #1426)
IO-2667 Employee Tasks report

Approved-by: Dave Richer
2024-04-18 20:52:31 +00:00
Patrick Fic
cdf02a8eac Merged in release/AIO/2024-04-19 (pull request #1420)
Resolve CCC supplement with UNQ_SEQ.
2024-04-18 20:49:45 +00:00
Allan Carr
7d22dcab7c IO-2667 Employee Tasks report
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-18 13:42:42 -07:00
Allan Carr
45c943a78f Merged in feature/IO-2677-Tasks (pull request #1425)
IO-2667 Tasks Reports

Approved-by: Dave Richer
2024-04-18 18:25:27 +00:00
Allan Carr
cf82a8013f IO-2667 Tasks Reports
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-18 10:46:43 -07:00
Dave Richer
25d145d864 Merged in feature/IO-2760-IDS-for-headers (pull request #1423)
Add IDs in Header

Approved-by: Patrick Fic
2024-04-18 17:31:13 +00:00
Dave Richer
7822e3f90e Merged in feature/IO-2760-IDS-for-headers (pull request #1424)
Add Header IDS

Approved-by: Patrick Fic
2024-04-18 17:25:15 +00:00
Dave Richer
a4a612fbe4 - adjust moment import
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-18 13:17:13 -04:00
Dave Richer
07da472e82 Merged in feature/IO-2677-Tasks (pull request #1422)
- adjust moment import
2024-04-18 16:22:24 +00:00
Dave Richer
e9096632a4 - adjust moment import
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-18 12:15:01 -04:00
Dave Richer
6475a8bdce Merged in feature/IO-2677-Tasks (pull request #1421)
- Tasks Email Queue

Approved-by: Patrick Fic
2024-04-18 14:47:20 +00:00
Patrick Fic
535f92b7d2 Additional tasks changes. 2024-04-18 07:46:05 -07:00
Dave Richer
00b91616f5 - Tasks Email Queue
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-17 20:14:21 -04:00
Dave Richer
b8c34762ed - Tasks Email Queue
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-17 19:49:49 -04:00
Dave Richer
9f92347936 Merged in feature/IO-2667 -Tasks (pull request #1389)
Feature/IO-2667 Tasks

Approved-by: Patrick Fic
2024-04-17 20:29:28 +00:00
Dave Richer
b657a893ad - adjust col widths for remind_at and due_date (cosmetic)
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-17 13:40:39 -04:00
Dave Richer
387670212a - Fix regression in last refactor.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-17 13:16:42 -04:00
Dave Richer
b8e42544ae - Fix regression in last refactor.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-17 13:09:50 -04:00
Dave Richer
bcea47c2c6 - Fix regression in last refactor.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-17 13:06:48 -04:00
Dave Richer
b38aaba56c - Merge in Test-AIO
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-17 12:45:19 -04:00
Patrick Fic
8099607d90 Merge branch 'release/AIO/2024-04-19' into test-AIO 2024-04-16 16:10:51 -07:00
Patrick Fic
2d9412e4e8 Merge branch 'release/2024-04-19' into release/AIO/2024-04-19 2024-04-16 16:10:33 -07:00
Patrick Fic
069d508528 Update cron trigger timing. 2024-04-16 16:09:54 -07:00
Patrick Fic
d68ce67e4f Merge branch 'release/AIO/2024-04-19' into test-AIO 2024-04-16 16:04:06 -07:00
Patrick Fic
a9d38e743f Merge branch 'release/2024-04-19' into release/AIO/2024-04-19 2024-04-16 16:03:06 -07:00
Patrick Fic
878e81dc8f Hasura schema changes for tasks. 2024-04-16 16:02:25 -07:00
Dave Richer
5ccf74f99c Merge remote-tracking branch 'origin/test-AIO' into feature/IO-2677-Tasks 2024-04-16 18:56:14 -04:00
Patrick Fic
6c823f4914 Merge branch 'release/AIO/2024-04-19' into test-AIO 2024-04-16 15:33:27 -07:00
Patrick Fic
de35155ffe Resolve CCC supplement with UNQ_SEQ. 2024-04-16 15:15:25 -07:00
Dave Richer
470eb19a2f - Final Push Prior to Live Testing
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-16 16:11:41 -04:00
Dave Richer
947bc33946 Merge remote-tracking branch 'origin/test-AIO' into feature/IO-2677-Tasks 2024-04-16 15:56:41 -04:00
Dave Richer
8bcaabfb57 Merged in release/2024-04-19 (pull request #1418)
Release/2024 04 19
2024-04-16 19:56:29 +00:00
Dave Richer
a1f7e7b755 Merged in feature/IO-2667-Migrations-For-Remind-At-Sent (pull request #1416)
- Migrations for remind_at_sent
2024-04-16 19:52:13 +00:00
Dave Richer
c8f8a86a98 - Migrations for remind_at_sent
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-16 15:45:56 -04:00
Dave Richer
bb205af019 - Progress
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-16 15:36:27 -04:00
Patrick Fic
443c6046f9 Merged in release/2024-04-19 (pull request #1415)
Resolve schedule header display.
2024-04-15 21:02:39 +00:00
Patrick Fic
1620b94a7b Merged in release/2024-04-19 (pull request #1414)
Resolve schedule header display.
2024-04-15 20:47:48 +00:00
Patrick Fic
81f94eac6c Resolve schedule header display. 2024-04-15 13:47:19 -07:00
Patrick Fic
ffada75d9e Merged in test-AIO (pull request #1412)
Test AIO

Approved-by: Dave Richer
2024-04-12 18:05:35 +00:00
Dave Richer
34d773bcd8 - Cosmetic requests
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-12 13:51:57 -04:00
Allan Carr
ec7509670d Merged in release/2024-04-12 (pull request #1413)
Release/2024 04 12

Approved-by: Dave Richer
2024-04-12 17:08:21 +00:00
Dave Richer
eceac11af2 - Cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-12 12:58:24 -04:00
Dave Richer
dd64598850 - Dynamic Date Time Presets for Tasks
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-12 12:29:04 -04:00
Dave Richer
650ace6be6 - cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-12 11:53:03 -04:00
Dave Richer
1c71a5c5e0 - small bug fix on modifying task by key via url
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-12 11:43:21 -04:00
Dave Richer
9c699a634b - optimization
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-12 11:35:53 -04:00
Dave Richer
a47d17bbf5 - priority
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-12 11:21:08 -04:00
Dave Richer
164f67d6ce - Move refresh button at Allans behest
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-11 23:48:48 -04:00
Dave Richer
465b9e7177 - Simplified solution in previous commit
- Additional regression testing fixes

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-11 23:47:02 -04:00
Dave Richer
f5a914c318 - Merge in test-AIO
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-11 22:21:53 -04:00
Dave Richer
de486d2e73 - Fix console warn, add two missing try catch blocks
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-11 22:15:48 -04:00
Dave Richer
d7f946ec2a - Fix an issue with not having an upper context to store intermediate values. Thus allowing me to fix the top level 'Add Task'
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-11 21:01:38 -04:00
Dave Richer
4d1480bb61 - allan found bug
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-11 19:12:06 -04:00
Patrick Fic
25a49473f9 Merge branch 'release/AIO/2024-04-12' into test-AIO 2024-04-11 15:22:26 -07:00
Patrick Fic
8e86e7fba5 Resolve linting errors from merge. 2024-04-11 15:22:05 -07:00
Patrick Fic
f9b380a0d4 Merged in release/AIO/2024-04-12 (pull request #1411)
Release/AIO/2024 04 12
2024-04-11 22:16:32 +00:00
Patrick Fic
f664a56b16 Merge branch 'release/2024-04-12' into release/AIO/2024-04-12 2024-04-11 15:15:54 -07:00
Allan Carr
0acfd3c4b1 Merged in feature/IO-2609-Calendar-BPT-HRS (pull request #1409)
IO-2609 Fix Spelling Mistake in object name
2024-04-11 21:39:06 +00:00
Allan Carr
bfc4cb1ad9 IO-2609 Fix Spelling Mistake in object name
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-11 14:38:08 -07:00
Patrick Fic
66dd1a9a2b Merge branch 'feature/AIO/promanager' into test-AIO 2024-04-11 14:29:09 -07:00
Allan Carr
1f42be2e54 Merged in feature/IO-2753-Qty-Parts-Order-Modal (pull request #1405)
IO-2753 Parts Order/Return Quantity restrict to above 0
2024-04-11 21:22:37 +00:00
Allan Carr
30d344af6b Merged in feature/IO-2752-BO-ETA-Jobline-Expander (pull request #1404)
IO-2752 BO ETA Jobline Expander
2024-04-11 21:22:19 +00:00
Allan Carr
3ca989fd8c Merged in release/2024-04-05 (pull request #1401)
Release/2024 04 05

Approved-by: Dave Richer
2024-04-11 21:22:03 +00:00
Patrick Fic
73c38b3ae4 Merged in feature/IO-2458-RO-Closer (pull request #1406)
Feature/IO-2458 RO Closer
2024-04-11 20:25:25 +00:00
Patrick Fic
08749b0c92 Add audit log to job close with bypass. 2024-04-11 13:24:45 -07:00
Patrick Fic
ea73af371e Minor bug fixes. 2024-04-11 13:07:43 -07:00
Allan Carr
ce2086a480 IO-2753 Parts Order/Return Quantity restrict to above 0
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-11 12:43:14 -07:00
Allan Carr
63f7106d2b IO-2752 BO ETA Jobline Expander
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-11 11:55:55 -07:00
Dave Richer
8cce6ea6e3 - additional cleanup and validation / fixes
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-11 14:44:21 -04:00
Dave Richer
77c486b4c9 - additional cleanup and validation / fixes
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-11 14:25:16 -04:00
Dave Richer
7c8f276bb0 - small regression fix
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-11 12:56:41 -04:00
Patrick Fic
e991586254 Merged in feature/IO-2458-RO-Closer (pull request #1403)
Bug fixes and formatting for RO guard.
2024-04-11 16:54:35 +00:00
Dave Richer
d61ab796a7 - small regression fix
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-11 12:52:53 -04:00
Dave Richer
e634369975 - small regression fix
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-11 12:50:24 -04:00
Patrick Fic
453236cf3a Bug fixes and formatting for RO guard. 2024-04-11 09:49:00 -07:00
Dave Richer
3530476b07 -
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-11 12:27:56 -04:00
Allan Carr
e75d8d1874 Merged in feature/IO-2609-Calendar-BPT-HRS (pull request #1402)
IO-2609 Body & Refinish Times included in Calendar View

Approved-by: Dave Richer
2024-04-11 16:27:02 +00:00
Patrick Fic
719fa6a67d ProMan totals changes. 2024-04-11 09:23:20 -07:00
Dave Richer
148cd43c5d - Fix a bug where a relationship deeplink would not pop if the user was already on the jobdetails page
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-10 16:57:37 -04:00
Dave Richer
3d753a2d19 - PR Change Requests (Progress)
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-10 16:48:06 -04:00
Dave Richer
693d02de87 - PR Change Requests
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-09 16:53:37 -04:00
Dave Richer
80b4ef3ae8 - PR Change Requests
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-09 16:49:03 -04:00
Dave Richer
6b9269eb2d - Progress
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-09 16:32:26 -04:00
Dave Richer
15c9529885 Merge remote-tracking branch 'origin/test-AIO' into feature/IO-2677-Tasks 2024-04-09 15:24:04 -04:00
Patrick Fic
512cd70d13 Merge branch 'feature/IO-2458-RO-Closer' into test-AIO 2024-04-09 12:23:49 -07:00
Patrick Fic
e5599ff4c4 Update prettier config and add as dev dependency. 2024-04-09 12:23:10 -07:00
Dave Richer
32041ee3fc - Progress
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-09 15:03:02 -04:00
Allan Carr
07bf84ed69 IO-2609 Body & Refinish Times included in Calendar View
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-09 11:55:20 -07:00
Dave Richer
0c0449aa17 - Hasura and PrettierRC
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-09 12:54:58 -04:00
Dave Richer
26cb527d37 - Add additional field to tasks table
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-09 11:18:43 -04:00
Dave Richer
33c282051b Fix Formatting issues
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-08 22:25:07 -04:00
Dave Richer
df0f8ef9dc Progress
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-08 22:07:14 -04:00
Dave Richer
e23f13a1b3 - Merge Test-AIO
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-08 20:25:08 -04:00
Patrick Fic
f5b8bf1d74 Resolve unnecessary import. 2024-04-08 14:32:02 -07:00
Patrick Fic
61766017ea Resolve supplement import for CCC. 2024-04-08 14:28:58 -07:00
Patrick Fic
706984a53b Resolve CCC supplement import. 2024-04-08 14:27:33 -07:00
Patrick Fic
e137feca20 Resolve ESLint Warnings 2024-04-08 14:20:34 -07:00
Patrick Fic
9e66d7c929 Merge branch 'release/AIO/2024-04-05' into test-AIO 2024-04-08 13:56:10 -07:00
Patrick Fic
9605bf5c21 Merge branch 'release/2024-04-05' into release/AIO/2024-04-05 2024-04-08 13:55:43 -07:00
Allan Carr
c2cc7b1e9e Merged in feature/IO-2731-Payment-Edit (pull request #1399)
IO-2731 Payment Edit
2024-04-08 19:56:43 +00:00
Allan Carr
fe55eccbf9 Merged in feature/IO-2749-Parts-Return-Pass-Jobs-Data (pull request #1398)
IO-2749 Pass Jobs data from Parts Return to Parts Order Modal
2024-04-08 19:55:49 +00:00
Patrick Fic
47e17dc78a Merge branch 'feature/IO-2727-resolve-payment-refetch' into release/AIO/2024-04-05 2024-04-08 12:43:15 -07:00
Allan Carr
88a71dd647 IO-2731 Payment Edit
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-08 12:23:04 -07:00
Patrick Fic
ce0b3a8635 Merge branch 'feature/IO-2727-resolve-payment-refetch' into test-AIO 2024-04-08 11:08:57 -07:00
Patrick Fic
b8e4520366 Resolve ES Lint error. 2024-04-08 11:05:12 -07:00
Dave Richer
bd3d86a6dd - Tasks Audit Trail Additions
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-08 13:26:37 -04:00
Allan Carr
c3b395c99e IO-2749 Pass Jobs data from Parts Return to Parts Order Modal
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-08 09:53:20 -07:00
Patrick Fic
a2a7c1c58c Add contextRefect to payment export buttons. 2024-04-08 09:48:39 -07:00
Dave Richer
eb4e5d9576 Merge remote-tracking branch 'origin/test-AIO' into feature/IO-2677-Tasks 2024-04-08 12:31:00 -04:00
Patrick Fic
1469960643 Resolve payment refetch. 2024-04-05 12:52:26 -07:00
Allan Carr
19a03ec080 Merge branch 'release/2024-04-05' into test-AIO
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>

# Conflicts:
#	client/src/components/print-center-jobs-labels/print-center-jobs-labels.component.jsx
2024-04-05 12:21:35 -07:00
Allan Carr
33d5d9b462 Merged in feature/IO-2568-Payment-Modal-Button-Spacing (pull request #1396)
IO-2568 Button Padding in Print Center Label Modal
2024-04-05 19:03:09 +00:00
Allan Carr
b5a371d0cf IO-2568 Button Padding in Print Center Label Modal
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-05 12:01:26 -07:00
Dave Richer
b61fd17879 - add smart refetch to mark-export and reexport
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-05 14:54:16 -04:00
Dave Richer
6722f8b1e5 - reversion
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-05 13:41:37 -04:00
Dave Richer
69ff75157d - reversion
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-05 13:35:44 -04:00
Dave Richer
7fad968ad2 - Cleanups
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-05 13:24:38 -04:00
Allan Carr
7c619f5439 Merged in release/2024-04-05 (pull request #1395)
IO-2750 Missing Mutation return fields
2024-04-05 16:00:52 +00:00
Allan Carr
3b35f38ad5 Merged in feature/IO-2750-Missing-Jobline-Fields (pull request #1393)
IO-2750 Missing Mutation return fields
2024-04-05 15:55:13 +00:00
Dave Richer
096017c3d6 Merge remote-tracking branch 'origin/test-AIO' into feature/IO-2677-Tasks 2024-04-05 11:22:13 -04:00
Dave Richer
4ff2ab1bc8 - Remove actions params in Payment modal, which was causing issues.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-05 11:20:16 -04:00
Dave Richer
ef698529d7 - Changes required by ESLint.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-05 10:21:31 -04:00
Dave Richer
167d5bd89a - Changes required by ESLint.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-05 10:08:56 -04:00
Dave Richer
c328a55453 Merge remote-tracking branch 'origin/test-AIO' into feature/IO-2677-Tasks 2024-04-05 09:59:57 -04:00
Dave Richer
83a976e98f - Add ESLint back to Vite
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-05 09:59:30 -04:00
Dave Richer
d004133ad6 - Merge AIO
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-05 09:35:35 -04:00
Patrick Fic
d7caaecaf7 Merge branch 'feature/IO-2458-RO-Closer' into test-AIO 2024-04-04 20:42:17 -07:00
Patrick Fic
96242a555a RO Closer bug fixes. 2024-04-04 20:41:39 -07:00
Patrick Fic
7ece1256f2 Uncomment ProManager calculations. 2024-04-04 20:01:47 -07:00
Allan Carr
1f5c1b9658 IO-2750 Missing Mutation return fields
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-04 17:00:30 -07:00
Patrick Fic
a43134511b Merge branch 'release/2024-04-05' into test-AIO 2024-04-04 12:56:09 -07:00
Patrick Fic
b188cce3ea Merge branch 'test-AIO' of bitbucket.org:snaptsoft/bodyshop into test-AIO 2024-04-04 12:52:02 -07:00
Patrick Fic
56dfd174dd Merge branch 'hotfix/AIO/2024-04-04' into master-AIO 2024-04-04 11:32:09 -07:00
Patrick Fic
39d4f56812 Merge branch 'hotfix/AIO/2024-04-04' into test-AIO 2024-04-04 11:32:01 -07:00
Patrick Fic
f85e3d8d60 Resolve job costing. 2024-04-04 11:31:37 -07:00
Patrick Fic
c33eaa6c68 Merged in feature/IO-2458-RO-Closer (pull request #1390)
Feature/IO-2458 RO Closer
2024-04-04 18:14:11 +00:00
Patrick Fic
c4d39cf3d3 Merge branch 'test-AIO' into feature/IO-2458-RO-Closer
# Conflicts:
#	client/src/components/job-bills-total/job-bills-total.component.jsx
#	client/src/components/job-costing-statistics/job-costing-statistics.component.jsx
#	client/src/components/job-lifecycle/job-lifecycle.component.jsx
#	client/src/components/job-payments/job-payments.component.jsx
#	client/src/components/labor-allocations-table/labor-allocations-table.component.jsx
#	client/src/components/labor-allocations-table/labor-allocations-table.payroll.component.jsx
#	client/src/components/shop-info/shop-info.component.jsx
#	client/src/graphql/bodyshop.queries.js
#	client/src/graphql/jobs.queries.js
#	client/src/pages/jobs-close/jobs-close.component.jsx
#	client/src/utils/instanceRenderMgr.js
2024-04-04 11:12:49 -07:00
Patrick Fic
532cd4937b Merge branch 'hotfix/AIO/2024-04-04' into master-AIO 2024-04-04 10:51:57 -07:00
Patrick Fic
926c13569d Merge branch 'hotfix/AIO/2024-04-04' into test-AIO 2024-04-04 10:51:25 -07:00
Patrick Fic
cbf5c5729f Resolve instancemgr arg for supplement import. 2024-04-04 10:51:10 -07:00
Patrick Fic
5e63ce7271 Final changes for RO closer. 2024-04-03 14:18:45 -07:00
Dave Richer
91bc73baf2 Merge remote-tracking branch 'origin/test-AIO' into feature/IO-2677-Tasks 2024-04-03 14:18:22 -04:00
Dave Richer
ab031c01de - reapply proper prettier formatting.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-03 14:09:09 -04:00
Dave Richer
e51f72ff98 - its sign, not sing :D
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-03 13:47:40 -04:00
Dave Richer
3eb010285d - Update date picker presets
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-03 13:01:56 -04:00
Patrick Fic
37196e65c3 Add schema changes for RO Guard to bodyshop table. 2024-04-03 09:46:52 -07:00
Patrick Fic
5c0f228876 Remove debug statement. 2024-04-03 09:27:29 -07:00
Dave Richer
2b172f9999 - fix
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-02 21:54:07 -04:00
Dave Richer
0803f5af35 - Progress Commit (Emailzzz)
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-02 21:53:07 -04:00
Dave Richer
69ac2f0a6c - Progress Commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-02 17:33:54 -04:00
Dave Richer
0c842e0e15 - Progress Commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-02 17:20:00 -04:00
Dave Richer
d94678d4f4 - Progress Commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-02 15:20:23 -04:00
Allan Carr
b20c605c85 Merged in feature/IO-2568-Payment-Modal-Button-Spacing (pull request #1384)
IO-2568 Payment Modal Button Spacing

Approved-by: Dave Richer
2024-04-02 19:03:49 +00:00
Allan Carr
d8ac708536 Merged in feature/IO-2552-PVRT-Button-Spacing (pull request #1386)
IO-2552 PVRT Button Spacing and Alignment

Approved-by: Dave Richer
2024-04-02 19:03:36 +00:00
Allan Carr
8a32fe50f3 Merged in feature/IO-2730-Bill-Search-Result-Align (pull request #1383)
IO-2730 Bill Search Result Align

Approved-by: Dave Richer
2024-04-02 19:03:06 +00:00
Allan Carr
7897a490bd Merged in feature/IO-2563-Repair-Line-Expander-Bills-Transaltion (pull request #1385)
IO-2563 Repair LIne Expander Bills Translation

Approved-by: Dave Richer
2024-04-02 19:02:50 +00:00
Allan Carr
17d73fc6d7 Merged in feature/IO-2553-Edit-CC-Unsaved-Changes (pull request #1388)
IO-2553 Unsaved Changes on Edit CC

Approved-by: Dave Richer
2024-04-02 19:02:28 +00:00
Dave Richer
90814f41a2 - Progress Commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-02 15:01:51 -04:00
Allan Carr
7d1910086e IO-2553 Unsaved Changes on Edit CC
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-02 10:39:41 -07:00
Dave Richer
282dbd0913 - Progress Commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-01 20:20:01 -04:00
Patrick Fic
46def33bc8 Merge branch 'test-AIO' into master-AIO 2024-04-01 15:02:56 -07:00
Patrick Fic
fc41226404 CI Fixes. 2024-04-01 15:02:13 -07:00
Patrick Fic
a51e088f69 Merged in test-AIO (pull request #1387)
Rome Deployment & minor bug fixes for IO.
2024-04-01 21:55:37 +00:00
Patrick Fic
3683bc161d Minor bug fixes & Rome CI 2024-04-01 14:42:05 -07:00
Allan Carr
817c41afb9 IO-2552 PVRT Button Spacing and Alignment
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-01 14:27:25 -07:00
Allan Carr
04315a9045 IO-2563 Repair LIne Expander Bills Translation
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-01 14:07:58 -07:00
Allan Carr
d0871ffe21 IO-2568 Payment Modal Button Spacing
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-01 13:52:25 -07:00
Dave Richer
1343b68cc6 - Progress Commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-01 16:34:31 -04:00
Allan Carr
dca587d6e0 IO-2730 Bill Search Result Align
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-01 13:20:30 -07:00
Patrick Fic
72aece7f14 Minor Changes. 2024-04-01 12:34:44 -07:00
Dave Richer
ae07f71e76 - Progress Commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-01 14:45:55 -04:00
Patrick Fic
a3d6d44089 IO-2723 Change tech sider order. 2024-04-01 10:46:22 -07:00
Patrick Fic
07a18bcd8c IO-2661 Rename CSR labels to Writer for Rome & PM 2024-04-01 10:44:57 -07:00
Patrick Fic
9b9ac505e9 Refactor task modal opening. 2024-03-28 14:41:54 -07:00
Patrick Fic
8f7e2bd9d3 Merged in test-AIO (pull request #1379)
Test AIO
2024-03-28 21:21:32 +00:00
Allan Carr
19ec4cb021 Merged in release/2024-03-28 (pull request #1382)
Release/2024 03 28
2024-03-28 21:15:25 +00:00
Patrick Fic
a7aa2a31a6 Merge branch 'release/2024-03-28' into test-AIO 2024-03-28 13:29:58 -07:00
Allan Carr
346e82bdbc Merged in feature/IO-2702-Limit-Production-Colors-to-Production-Statuses (pull request #1380)
IO-2702 Adjust concat
2024-03-28 20:24:19 +00:00
Allan Carr
fd9575e5a5 IO-2702 Adjust concat
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-28 13:25:13 -07:00
Dave Richer
54dc9c8587 - merge AIO
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-28 15:03:43 -04:00
Dave Richer
9f9fa3b952 - Progress commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-28 15:02:06 -04:00
Patrick Fic
3acf271dcf Merge branch 'release/2024-03-28' into test-AIO 2024-03-28 10:30:06 -07:00
Patrick Fic
af6bc0fb5a Resolve hasura API url again. 2024-03-28 10:23:22 -07:00
Dave Richer
cc7c98336f - Progress commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-28 13:03:09 -04:00
Allan Carr
d0e289757f Merged in feature/IO-2705-Insurance-Co-ID (pull request #1375)
IO-2705 Insurance Co Id and correct Select Insurance and Layout for concistency

Approved-by: Dave Richer
2024-03-28 16:59:52 +00:00
Allan Carr
becc54f7b3 Merged in feature/IO-2714-New-Payment-Cache (pull request #1376)
IO-2714 New Payment Cache

Approved-by: Dave Richer
2024-03-28 16:51:59 +00:00
Allan Carr
e3fabdac91 Merged in feature/IO-2727-Payment-Export-Re-Export-Button (pull request #1377)
IO-2727 Payment Re-export/Export button

Approved-by: Dave Richer
2024-03-28 16:51:20 +00:00
Allan Carr
40621db556 Merged in feature/IO-2702-Limit-Production-Colors-to-Production-Statuses (pull request #1374)
IO-2702 Limit Production Colors to Production Statuses

Approved-by: Dave Richer
2024-03-28 16:50:51 +00:00
Dave Richer
dc22b96bed - Progress commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-28 12:30:14 -04:00
Patrick Fic
c3fa4ef8af Minor fixes. 2024-03-28 09:26:24 -07:00
Patrick Fic
0a784c6873 Resolve gallery issues. 2024-03-27 15:47:00 -07:00
Patrick Fic
e1df64d592 Reformat all project files to use the prettier config file. 2024-03-27 15:35:07 -07:00
Patrick Fic
b161530381 Minor Test Changes. 2024-03-27 15:13:47 -07:00
Dave Richer
ae9e9f4b72 - Progress commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-27 17:00:07 -04:00
Patrick Fic
136ef63c14 Add PPC. 2024-03-27 13:26:28 -07:00
Patrick Fic
c2e64a124d Cherry picked from Rome changes to apply to IO. 2024-03-27 12:21:28 -07:00
Allan Carr
1fa83d124d IO-2727 Payment Re-export/Export button
Send data back to page

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-27 12:14:47 -07:00
Patrick Fic
b1dfd23fd4 minor RO fixes. 2024-03-27 09:54:40 -07:00
Patrick Fic
3eaa23beac CI credentials for ROme. 2024-03-27 09:05:37 -07:00
Patrick Fic
090c55fb3d CI changes. 2024-03-27 08:59:46 -07:00
Patrick Fic
bf5d1b69d4 Replace CI for Rome Test env. 2024-03-27 08:57:08 -07:00
Patrick Fic
7f930fcd5b Product fruit updates. 2024-03-27 08:15:50 -07:00
Dave Richer
301c680bff - Progress commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-26 21:36:27 -04:00
Dave Richer
595159f24d - Progress commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-26 19:59:30 -04:00
Allan Carr
cc734d3981 IO-2714 New Payment Cache
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-26 16:04:05 -07:00
Patrick Fic
2ace0189bc Web-est bug fixes. 2024-03-26 15:28:12 -07:00
Allan Carr
b41d69593d IO-2705 Insurance Co Id and correct Select Insurance and Layout for concistency
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-26 14:53:52 -07:00
Patrick Fic
248aa65195 Reinstate product fruits. 2024-03-26 13:56:10 -07:00
Allan Carr
b7055aac84 IO-2702 Limit Production Colors to Production Statuses
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-26 13:45:04 -07:00
Dave Richer
3bf1ec25c1 merge aio 2024-03-26 10:09:29 -04:00
Patrick Fic
815ada0516 WIP RO Closer. 2024-03-25 15:38:38 -07:00
Patrick Fic
98b14f2a5b resolve pbs. 2024-03-22 15:18:01 -07:00
Patrick Fic
bfa5cdcede FIx PBS export. 2024-03-22 14:58:26 -07:00
Patrick Fic
37b251a514 Resolve CI 2024-03-22 11:43:32 -07:00
Patrick Fic
9c6e514db0 Add optional dependency for CI. 2024-03-22 11:40:33 -07:00
Patrick Fic
b83a39dc4a Fix CI. 2024-03-22 11:37:03 -07:00
Patrick Fic
0ea422ab7c CI updates to replace beta environments. 2024-03-22 11:35:46 -07:00
Dave Richer
09e505399c Merged in release/2024-03-22 (pull request #1372)
Release/2024 03 22

Approved-by: Patrick Fic
2024-03-22 18:27:16 +00:00
Patrick Fic
80b9b5fbf6 Merge branch 'release/2024-03-22' into master-AIO 2024-03-22 11:16:20 -07:00
Dave Richer
40c801592d - Progress Commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-22 12:20:56 -04:00
Allan Carr
42f9f275c7 Merged in feature/IO-2721-Owners-Note-in-Owners-Card (pull request #1370)
IO-2721 Owners Note in Owners Card

Approved-by: Dave Richer
2024-03-22 16:18:23 +00:00
Patrick Fic
15a9988894 Merge branch 'master-AIO' into feature/IO-2458-RO-Closer 2024-03-22 08:41:12 -07:00
Allan Carr
39b119b2e8 IO-2721 Owners Note in Owners Card
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-21 17:49:46 -07:00
Patrick Fic
21a724fbf1 Missing in merge. 2024-03-21 15:16:17 -07:00
Patrick Fic
d6e27c7ced Merge branch 'release/2024-03-22' into master-AIO 2024-03-21 15:16:08 -07:00
Allan Carr
5bc224206c Merged in feature/IO-2711-Check-Box-Visibility (pull request #1362)
IO-2711 Check Box Visibility

Approved-by: Patrick Fic
2024-03-21 21:42:43 +00:00
Allan Carr
190da863c2 Merged in feature/IO-2698-Fuel-Level-&-Sorter (pull request #1363)
IO-2698 Fuel Level & Sorter

Approved-by: Patrick Fic
2024-03-21 21:41:12 +00:00
Allan Carr
2711b5fce5 Merged in feature/IO-2686-Disable-Return-on-isinhouse (pull request #1364)
IO-2686 Disable Return Item button on Bill Drawer if InHouse

Approved-by: Patrick Fic
2024-03-21 21:40:39 +00:00
Allan Carr
8e9358cd6f Merged in feature/IO-2713-Bills-&-Media-Visual-Seperation (pull request #1365)
IO-2713 Visually Seperate Bill Entry & Media Areas

Approved-by: Patrick Fic
2024-03-21 21:40:12 +00:00
Allan Carr
0e31bbb789 Merged in feature/IO-2710-Job-Assignment (pull request #1366)
IO-2710 Job Assignment

Approved-by: Patrick Fic
2024-03-21 21:39:36 +00:00
Allan Carr
3b635aeed3 Merged in feature/IO-2719-Missing-CC-Filter (pull request #1367)
IO-2719 Missing CC Filter

Approved-by: Patrick Fic
2024-03-21 21:39:05 +00:00
Patrick Fic
32a98ed438 Disable product fruits in production. 2024-03-21 14:38:05 -07:00
Patrick Fic
9b060b272c IO-2692 Await refetch for returns posting. 2024-03-21 12:23:45 -07:00
Patrick Fic
cd9e67c9df Resolve search select. 2024-03-21 11:58:00 -07:00
Allan Carr
1f896b1ede IO-2719 Missing CC Filter
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-21 11:09:13 -07:00
Patrick Fic
23f9f8fa20 Resolve additional typo. 2024-03-21 11:05:51 -07:00
Patrick Fic
5bb8286301 Add back notification for FCM. 2024-03-21 11:05:38 -07:00
Dave Richer
9012e4deec - Progress Commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-21 13:42:08 -04:00
Patrick Fic
568e74b21f Resolve display issue for Multi payer on job close. 2024-03-21 09:49:19 -07:00
Patrick Fic
8d729f1e83 IO-2716 Resolve payers missing icon. 2024-03-21 09:17:07 -07:00
Patrick Fic
6463e13eeb IO-2715 Resolve vendor search select. 2024-03-21 09:12:49 -07:00
Dave Richer
ab2323e5c1 - Progress Commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-20 22:19:52 -04:00
Dave Richer
f31ae9ac6d - Progress Commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-20 18:18:24 -04:00
Patrick Fic
6831a79407 Minor proman changes. 2024-03-20 14:01:17 -07:00
Dave Richer
27c24619c3 Merge remote-tracking branch 'origin/master-AIO' into feature/IO-2677-Tasks 2024-03-20 15:11:26 -04:00
Dave Richer
38aef71269 Progress 2024-03-20 15:11:08 -04:00
Patrick Fic
e162460b9c Pro man logo, CDK updates for Rome, Update auto allocate. 2024-03-20 11:14:15 -07:00
Patrick Fic
c55374edc8 IO-2692 Refetch POL on CM received. 2024-03-20 08:48:32 -07:00
Allan Carr
b1ca09bd4f IO-2710 Prettierr
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-19 17:55:59 -07:00
Allan Carr
f9ca36ec89 IO-2710 Job Assignment
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-19 17:52:47 -07:00
Allan Carr
9d479d4b4d IO-2713 Visually Seperate Bill Entry & Media Areas
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-19 17:19:22 -07:00
Allan Carr
23b5b740cb IO-2686 Disable Return Item button on Bill Drawer if InHouse
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-19 17:17:15 -07:00
Allan Carr
8f9b05b974 IO-2698 Fuel Level & Sorter
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-19 14:41:34 -07:00
Allan Carr
d1132e7d45 IO-2711 Check Box Visibility
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-19 14:30:31 -07:00
Patrick Fic
922ba4939e Strip unneeded dependencies. 2024-03-19 14:04:27 -07:00
Patrick Fic
c2a4e75005 IO-2709 Remove additional committed buttons for non enhanced payroll. 2024-03-19 13:39:44 -07:00
Patrick Fic
926ff3cb6f IO-2704 Resolve CDK Allocation Calculation. 2024-03-19 13:26:25 -07:00
Dave Richer
960b0b4d09 Merged in feature/IO-2650-Bugfix-On-Statuses (pull request #1361)
- Hasura Migration Changes
2024-03-19 20:26:09 +00:00
Dave Richer
f35ea026b8 - Hasura Migration Changes
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-19 16:25:21 -04:00
Patrick Fic
c28d5b664d Resolve back end Instance Manager imports. 2024-03-19 13:12:19 -07:00
Patrick Fic
5fcf9712be IO-2691 Resolve additional cost inclusion. 2024-03-19 13:05:59 -07:00
Patrick Fic
f05d909a5c IO-2708 Lifecycle dashboard component. 2024-03-19 12:59:23 -07:00
Patrick Fic
a429756823 IO-2706 Missing translation 2024-03-19 11:33:50 -07:00
Patrick Fic
8460a99085 IO-2704 QBO/D Export resolution 2024-03-19 10:54:25 -07:00
Patrick Fic
8cb18d6d1d IO-2701 resolve missing dashboard translation. 2024-03-19 10:42:07 -07:00
Patrick Fic
cdd5b26443 IO-2703 Resolve incorrect Feature wrapper use. 2024-03-19 10:16:42 -07:00
Patrick Fic
01a9e7dc34 IO-2700 Remove committed button for non-enhanced payroll on time ticket list. 2024-03-19 09:48:30 -07:00
Patrick Fic
2ea3a10c8b IO-2696 Incorrect job totals showing per instance. 2024-03-19 09:42:07 -07:00
Patrick Fic
214e29d292 IO-2695 Respect payroll treatment for prod table column generation. 2024-03-19 09:20:09 -07:00
Patrick Fic
f618719419 IO-2693 Remove committed column in table for non-enhanced payroll. 2024-03-19 08:56:23 -07:00
Patrick Fic
1b16d75dc7 IO-2687 Delay template load to wait for translations. 2024-03-19 08:52:30 -07:00
Patrick Fic
a6497bb14e IO-2689 Preserve vendor on save and new. 2024-03-19 08:20:08 -07:00
Patrick Fic
8ea7db6488 WIP RO Closer. 2024-03-19 08:10:45 -07:00
Patrick Fic
b1fc2828c8 Merge branch 'feature/IO-2682-Hasura-Migrations-For-Tasks' into release/2024-03-22 2024-03-18 15:51:14 -07:00
Patrick Fic
bc25c23982 Resolve event env var. 2024-03-18 15:41:32 -07:00
Patrick Fic
2604507403 IO-2689 Resolve save and new for bill posting. 2024-03-18 13:52:02 -07:00
Patrick Fic
737c4164c4 IO-2688 Add default GST for bill posting on ImEX. 2024-03-18 13:43:23 -07:00
Patrick Fic
85f973f5e6 IO-2685 Resolve missing parts rates for ImEX. 2024-03-18 13:40:06 -07:00
Patrick Fic
f855e6423e Missing translations. 2024-03-18 08:19:29 -07:00
Patrick Fic
0ae9bb7e64 Merge branch 'master-beta' into master-AIO 2024-03-15 13:35:45 -07:00
Dave Richer
2215c8439e - Hasura Migration Changes
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-15 16:12:27 -04:00
Patrick Fic
ec517a0fc3 Remove walkthrough for non PM. 2024-03-15 11:58:48 -07:00
Dave Richer
3d8f16bb71 Merged in test-beta (pull request #1355)
[DO NOT MERGE] - 3/15/2024 - Beta Release

Approved-by: Allan Carr
2024-03-15 18:51:22 +00:00
Dave Richer
f98c9e6f71 Merged in release/2024-03-15 (pull request #1354)
Release - 2024 03 15

Approved-by: Allan Carr
2024-03-15 18:50:55 +00:00
Dave Richer
4a87be6adb Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1359)
Feature/IO-1828 Front End Package Updates
2024-03-15 17:28:09 +00:00
Dave Richer
da3087da75 Merge remote-tracking branch 'origin/release/2024-03-15' into feature/IO-1828-Front-End-Package-Updates 2024-03-15 13:27:39 -04:00
Patrick Fic
28f2e8ad30 Merged in feature/IO-2679-interactivity-tracking (pull request #1358)
Feature/IO-2679 interactivity tracking
2024-03-15 17:25:59 +00:00
Patrick Fic
c27e206687 Add index to audit trail. 2024-03-15 10:24:00 -07:00
Patrick Fic
01fd253f1d Manual modification to hasura migration. 2024-03-15 10:23:13 -07:00
Dave Richer
0a56b75aec Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1357)
Add ioevent logging for events.
2024-03-15 17:08:36 +00:00
Dave Richer
00b5036083 - merge release and adjust
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-15 13:07:47 -04:00
Patrick Fic
e67bc0d953 Merged in feature/IO-2679-interactivity-tracking (pull request #1356)
Add ioevent logging for events.
2024-03-15 16:56:37 +00:00
Patrick Fic
3eab3e2fb6 Add ioevent logging for events. 2024-03-15 09:55:14 -07:00
Patrick Fic
f279fc1e81 ImEX and App Improvements. 2024-03-15 09:29:49 -07:00
Dave Richer
8e8b038fe5 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1353)
Feature/IO-1828 Front End Package Updates
2024-03-15 14:55:40 +00:00
Dave Richer
17b412c12d - merge release and adjust
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-15 10:55:00 -04:00
Dave Richer
3adf6b649b Merged in feature/IO-2678-Linkable-schedule (pull request #1351)
- Implement
2024-03-15 14:49:06 +00:00
Dave Richer
f8243aa2b3 - Implement
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-15 10:47:46 -04:00
Patrick Fic
7e01f667b3 Resolve translations issues. 2024-03-14 13:58:06 -07:00
Dave Richer
2ad715f8a5 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1350)
Feature/IO-1828 Front End Package Updates
2024-03-14 19:06:47 +00:00
Dave Richer
3c3f50d138 Merged in feature/IO-2650-Lifecycle-V2 (pull request #1348)
- Fix bug
2024-03-14 18:59:37 +00:00
Dave Richer
4f7e1b81ac - Fix bug
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-14 14:59:06 -04:00
Dave Richer
0cb0942119 - Merge release
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-14 14:57:59 -04:00
Dave Richer
806bdc4c70 Merged in feature/IO-2650-Lifecycle-V2 (pull request #1346)
- Missing translation
2024-03-14 16:38:12 +00:00
Dave Richer
a0572a0cec - Missing translation
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-14 12:37:38 -04:00
Dave Richer
04cdf13e86 Merged in feature/IO-2650-Lifecycle-V2 (pull request #1344)
Feature/IO-2650 Lifecycle V2
2024-03-14 16:33:14 +00:00
Dave Richer
50349e91dc - remove duplicated code
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-14 12:32:40 -04:00
Dave Richer
9998a8f154 - Fix bug on humanReadable field we have not consumed until now.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-14 12:28:07 -04:00
Allan Carr
9dcbcb2a43 Merged in feature/IO-2671-Vehicle-Detail-Table (pull request #1343)
IO-2671 Add in appropriate sorters at same time

Approved-by: Dave Richer
2024-03-14 15:58:46 +00:00
Allan Carr
5773f7a0f3 Merged in feature/IO-2650-Lifecycle-Report (pull request #1345)
IO-2650 Job Lifecycle Report Center Reports

Approved-by: Dave Richer
2024-03-14 15:58:17 +00:00
Allan Carr
8614d88e71 Merged in feature/IO-2630-Parts-Queue-Mods (pull request #1342)
IO-2630 Adjust for onRow selection

Approved-by: Dave Richer
2024-03-14 15:57:45 +00:00
Allan Carr
fa5e26c52a IO-2650 Job Lifecycle Report Center Reports
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-13 17:22:42 -07:00
Dave Richer
90e1cbd390 - Job Lifecycle Dashboard Component.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-13 19:20:10 -04:00
Allan Carr
947a3c6a88 IO-2671 Add in appropriate sorters at same time
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-13 12:31:51 -07:00
Allan Carr
a33662e6f0 IO-2630 Adjust for oRow selection
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-13 12:04:45 -07:00
Patrick Fic
6d1f04369e Merged in feature/IO-2674-reorder-resp-centers (pull request #1340)
IO-2674 Add reordering arrows for responsibility centers in IO.

Approved-by: Allan Carr
2024-03-13 17:07:09 +00:00
Patrick Fic
4a27726ef3 Adjusted label for payers & added reorder to payers. 2024-03-13 12:54:31 -04:00
Patrick Fic
f61b89f213 Updated translations. 2024-03-13 12:52:20 -04:00
Allan Carr
eedba97237 Merged in feature/IO-2625-BPT-Hrs-in-Employee-Assignment (pull request #1339)
IO-2625 B/P/T Hrs Display in Employee Assignment Block

Approved-by: Dave Richer
2024-03-13 16:50:47 +00:00
Allan Carr
bcf095ed4f Merged in feature/IO-2669-Next-Contact-Date-formating (pull request #1337)
IO-2669 Next Contact Formating

Approved-by: Dave Richer
2024-03-13 16:50:00 +00:00
Allan Carr
c3f9e268c7 Merged in feature/IO-2671-Vehicle-Detail-Table (pull request #1338)
IO-2671 Missing field in Vehicle query

Approved-by: Dave Richer
2024-03-13 16:49:36 +00:00
Allan Carr
c8442f0750 Merged in feature/IO-2570-Totals-Card-on-Draw-have-Customer-Owing (pull request #1341)
IO-2570 Change Ded to Customer Owing amount

Approved-by: Dave Richer
2024-03-13 16:49:13 +00:00
Allan Carr
da5b446c30 Merge branch 'master' into feature/IO-2630-Parts-Queue-Mods
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-13 09:49:02 -07:00
Allan Carr
e13b2bb969 Merged in feature/IO-2520-Kaizen-Data-Pump (pull request #1336)
IO-2520 Change where email notification occurs

Approved-by: Dave Richer
2024-03-13 16:48:01 +00:00
Allan Carr
e8969c4698 IO-2570 Change Ded to Customer Owing amount
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-13 09:47:33 -07:00
Patrick Fic
940f77bf86 Merge branch 'master-beta' into master-AIO 2024-03-13 11:38:19 -04:00
Patrick Fic
346d32a2bb IO-2674 Add reordering arrows for responsibility centers in IO. 2024-03-13 08:02:20 -04:00
Dave Richer
6ba00a90be Merge branch 'master' into feature/IO-2650-Lifecycle-V2 2024-03-12 18:11:08 -04:00
Dave Richer
4293d20313 - Backend Changes for Lifecycle Data
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-12 18:10:09 -04:00
Allan Carr
706c70c509 IO-2625 B/P/T Hrs Display in Employee Assignment Block
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-12 14:49:12 -07:00
Allan Carr
e872b1bf0a IO-2671 Missing field in Vehicle query
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-12 11:55:07 -07:00
Allan Carr
379fa060d8 IO-2669 Next Contact Formating
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-12 11:24:57 -07:00
Allan Carr
a6258c6456 Merged in feature/IO-2650-Lifecycle-Report (pull request #1335)
IO-2650 Lifecycle Report for Print Center

Approved-by: Dave Richer
2024-03-12 17:01:19 +00:00
Patrick Fic
5623497e32 Add sample JoyRide walkthrough. 2024-03-12 10:48:27 -04:00
Patrick Fic
d9d30b59f0 Report center filtering and shop info instance management. 2024-03-12 09:40:39 -04:00
Patrick Fic
53a55dd1ef Added job intake and delivery bypass. 2024-03-12 08:44:35 -04:00
Allan Carr
d6bf0a225b IO-2520 Change where email notification occurs
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-11 17:46:19 -07:00
Allan Carr
ec2b914e5e Merge branch 'master' into feature/IO-2520-Kaizen-Data-Pump
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-11 17:45:26 -07:00
Allan Carr
309a20148a IO-2650 Lifecycle Report for Print Center
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-11 17:41:37 -07:00
Dave Richer
386de19703 Merged in test-beta (pull request #1330)
Test Beta into Master Beta for 03-08 Release

Approved-by: Allan Carr
2024-03-11 17:48:21 +00:00
Dave Richer
013b56778b Merged in release/2024-03-08 (pull request #1331)
Release into Master for 03 08 Release

Approved-by: Allan Carr
2024-03-11 17:47:51 +00:00
Dave Richer
34f48f6c9e Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1334)
- Regression
2024-03-11 17:04:02 +00:00
Dave Richer
31dd526907 - Regression
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-11 13:02:37 -04:00
Dave Richer
59c4a7ffb2 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1333)
- Regression
2024-03-11 16:36:02 +00:00
Dave Richer
8fdee890dc - Regression
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-11 12:35:19 -04:00
Dave Richer
e429249e18 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1332)
- Regression
2024-03-11 16:20:48 +00:00
Dave Richer
2a6518d4f1 - Regression
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-11 12:19:20 -04:00
Dave Richer
ad0ccf0cff Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-03-11 11:26:23 -04:00
Dave Richer
8f8215111d - Regression
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-11 11:26:06 -04:00
Dave Richer
d81b8c65b8 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-03-11 09:50:45 -04:00
Dave Richer
2b36fd9037 Merge release / update 2024-03-11 09:50:20 -04:00
Allan Carr
9dec4a3a61 Merged in feature/IO-2663-Export-Pages-Sorters-and-Filters (pull request #1327)
Correct Sorters
2024-03-08 19:36:40 +00:00
Allan Carr
189b4db90f Merged in feature/IO-2575-Production-Special-Coverage (pull request #1328)
Bring in line with other components
2024-03-08 19:36:27 +00:00
Allan Carr
c49fa1c527 Bring in line with other components
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-08 11:32:48 -08:00
Allan Carr
53ef048f6f Correct Sorters
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-08 11:24:41 -08:00
Dave Richer
1cc7eed983 Merged in feature/IO-2662-New-Report-Reflectors (pull request #1325)
- Add new special filters
2024-03-08 17:47:43 +00:00
Dave Richer
1c8f377212 - Add new special filters
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-08 12:47:11 -05:00
Dave Richer
c050947276 Merged in feature/IO-2662-New-Report-Reflectors (pull request #1323)
- Add new special filters
2024-03-08 17:37:10 +00:00
Dave Richer
9bde1f820d - Add new special filters
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-08 12:36:29 -05:00
Allan Carr
393765640c Merged in feature/IO-2663-Export-Pages-Sorters-and-Filters (pull request #1321)
IO-2663 Export Pages Sorters and Filters

Approved-by: Dave Richer
2024-03-08 01:14:40 +00:00
Allan Carr
69c2836425 Merged in feature/IO-2665-Jobs-sorters-and-query (pull request #1322)
IO-2665 Jobs page sorters and query adjustment

Approved-by: Dave Richer
2024-03-08 01:14:07 +00:00
Allan Carr
ec9edf30eb Merged in feature/IO-2651-Audit-Log-Extension (pull request #1320)
IO-2651 Audit Log Extension

Approved-by: Dave Richer
2024-03-08 01:13:42 +00:00
Allan Carr
6a812f9ea7 IO-2665 Jobs page sorters and query adjustment
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-07 11:53:34 -08:00
Allan Carr
fa86254bfd Prettierr
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-06 15:30:56 -08:00
Allan Carr
58ab7afbb3 IO-2663 Export Pages Sorters and Filters
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-06 15:29:24 -08:00
Allan Carr
fa7d90d2a9 IO-2651 Audit Log Extension
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-05 15:28:22 -08:00
Allan Carr
fbf9047974 Merged in feature/IO-2660-Phonebook-Draw-Title-Bug (pull request #1317)
IO-2660 Phonebook Drawer Title

Approved-by: Dave Richer
2024-03-05 20:32:29 +00:00
Allan Carr
3e9b795052 Merged in feature/IO-2575-Production-Special-Coverage (pull request #1318)
IO-2575 Special Coverage and Sorters & Filters

Approved-by: Dave Richer
2024-03-05 20:31:53 +00:00
Dave Richer
c3a78e4f71 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1319)
- Fix for IO-2540
2024-03-05 20:30:50 +00:00
Dave Richer
bf9fc128cd - Fix for IO-2540
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-05 15:13:40 -05:00
Allan Carr
d29ffc21e5 IO-2575 Special Coverage and Sorters & Filters
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-05 11:49:57 -08:00
Patrick Fic
1ae2133d23 ProManager WIP. 2024-03-05 08:41:52 -05:00
Allan Carr
959f7780e8 IO-2660 Phonebook Drawer Title
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-04 12:13:41 -08:00
Dave Richer
e47731702a Merged in release/2024-03-01 (pull request #1315)
Release/2024 03 01
2024-03-01 23:12:11 +00:00
Dave Richer
7bc9d18f5c Merged in test-beta (pull request #1314)
Test beta
2024-03-01 23:11:42 +00:00
Dave Richer
d68f1765d1 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-03-01 15:01:37 -05:00
Dave Richer
d557de7eec - Merge release
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-01 15:01:17 -05:00
Patrick Fic
85a3aeb335 Resolve refund payment logging. 2024-03-01 11:51:01 -08:00
Dave Richer
74d23010b8 Merge remote-tracking branch 'origin/feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-03-01 13:23:31 -05:00
Dave Richer
68fcd47315 - Merge release
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-01 13:21:33 -05:00
Dave Richer
cca7215f10 Merge remote-tracking branch 'origin/release/2024-03-01' into feature/IO-1828-Front-End-Package-Updates
# Conflicts:
#	_reference/reportFiltersAndSorters.md
#	client/src/components/bill-delete-button/bill-delete-button.component.jsx
#	client/src/components/bills-list-table/bills-list-table.component.jsx
#	client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx
#	client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx
#	client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx
#	client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx
#	client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx
#	client/src/components/scoreboard-day-stats/scoreboard-day-stats.component.jsx
#	client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx
#	client/src/pages/dms/dms.container.jsx
#	client/src/redux/application/application.sagas.js
#	client/src/translations/en_us/common.json
#	client/src/translations/es/common.json
#	client/src/translations/fr/common.json
#	client/src/utils/AuditTrailMappings.js
#	client/src/utils/graphQLmodifier.js
#	package-lock.json
#	package.json
2024-03-01 13:03:37 -05:00
Dave Richer
80b7ae0e54 Merge branch 'feautre/IO-2647-Reporting-V3-From-Master' into release/2024-03-01
# Conflicts:
#	_reference/reportFiltersAndSorters.md
2024-02-29 22:25:52 -05:00
Dave Richer
0529ac4478 - Reports V3 Targeted at Master
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-29 22:22:55 -05:00
Allan Carr
3cafbebbee Merged in feature/IO-1366-Audit-Logging (pull request #1311)
Feature/IO-1366 Audit Logging

Approved-by: Dave Richer
2024-03-01 02:44:05 +00:00
Allan Carr
6f248d864e Merged in feature/IO-2656-Job-Count-on-Scoreboard (pull request #1312)
IO-2656 Job Count on Scoreboard Jobs

Approved-by: Dave Richer
2024-03-01 02:43:23 +00:00
Allan Carr
a4a84572b7 IO-2656 Job Count on Scoreboard Jobs
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-29 15:36:58 -08:00
Allan Carr
a45d0bb9f4 IO-1366 Job Exported Audit Trail
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-29 14:13:45 -08:00
Allan Carr
a2e0f9fbe7 IO-1366 Audit Log for Bill Delete, Job Suspend, Job Void, Correct Saga
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-28 14:01:35 -08:00
Allan Carr
e37dc0a18f Merge branch 'master' into feature/IO-1366-Audit-Logging
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-28 13:56:25 -08:00
Allan Carr
0bc00d46cf Merged in feature/IO-2654-Courtesy-Car-List-Filter (pull request #1310)
IO-2654 Local Storage Filter State for Courtesy Car List

Approved-by: Dave Richer
2024-02-28 18:23:49 +00:00
Allan Carr
e80e40bb76 IO-2654 Local Storage Filter State for Courtesy Car List
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-27 16:37:07 -08:00
Allan Carr
a88c102b27 Prettier and Package Update
Azura Storage Blob and Trivago Prettier Sort Imports

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-27 16:04:41 -08:00
Dave Richer
a97197ccc7 Merged in test-beta (pull request #1306)
Test beta
2024-02-23 22:02:01 +00:00
Dave Richer
c691d44c44 Merged in release/2024-02-23 (pull request #1307)
Release/2024 02 23
2024-02-23 22:00:15 +00:00
Dave Richer
6eb48f92b3 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-02-23 16:59:43 -05:00
Dave Richer
dc09e47bf5 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-02-23 16:36:55 -05:00
Dave Richer
7fba6cb5e6 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-02-23 11:47:11 -05:00
Dave Richer
07fa92f6d6 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1305)
Feature/IO-1828 Front End Package Updates
2024-02-22 23:45:38 +00:00
Allan Carr
747cd52f54 Merged in test-beta (pull request #1297)
Test beta

Approved-by: Dave Richer
2024-02-16 21:36:57 +00:00
Dave Richer
c9fb40a9ff Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-02-16 15:36:53 -05:00
Dave Richer
3d70aa8b6c Merged in test-beta (pull request #1290)
Test beta

Approved-by: Patrick Fic
2024-02-16 20:21:30 +00:00
Dave Richer
b2ef0fb7d0 Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-02-16 14:21:01 -05:00
Dave Richer
0a27c38b56 Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1289)
Feature/IO-1828 Front End Package Updates
2024-02-16 17:53:11 +00:00
Dave Richer
5d68da846a Merge branch 'feature/IO-1828-Front-End-Package-Updates' into test-beta 2024-02-14 13:45:14 -05:00
Dave Richer
17aaaf38d1 Merged in test-beta (pull request #1281)
- fix time input boxes with showSeconds deprecated prop
2024-02-12 21:29:01 +00:00
Dave Richer
e09081e9ef Merged in feature/IO-1828-Front-End-Package-Updates (pull request #1280)
- fix time input boxes with showSeconds deprecated prop
2024-02-12 21:28:25 +00:00
973 changed files with 117073 additions and 118493 deletions

View File

@@ -41,9 +41,8 @@ jobs:
imex-app-build: imex-app-build:
docker: docker:
- image: cimg/node:18.18.2 - image: cimg/node:18.18.2
resource_class: large
working_directory: ~/repo/client working_directory: ~/repo/client
steps: steps:
- checkout: - checkout:
path: ~/repo path: ~/repo
@@ -63,6 +62,31 @@ jobs:
to: "s3://imex-online-production/" to: "s3://imex-online-production/"
arguments: "--exclude '*.map'" arguments: "--exclude '*.map'"
imex-app-beta-build:
docker:
- image: cimg/node:18.18.2
resource_class: large
working_directory: ~/repo/client
steps:
- checkout:
path: ~/repo
- run:
name: Install Dependencies
command: npm i
- run: npm run build:production:imex
- aws-cli/setup:
aws_access_key_id: AWS_ACCESS_KEY_ID
aws_secret_access_key: AWS_SECRET_ACCESS_KEY
region: AWS_REGION
- aws-s3/sync:
from: dist
to: "s3://imex-online-beta/"
arguments: "--exclude '*.map'"
rome-api-deploy: rome-api-deploy:
docker: docker:
- image: "cimg/base:stable" - image: "cimg/base:stable"
@@ -108,7 +132,7 @@ jobs:
name: Install Dependencies name: Install Dependencies
command: npm i command: npm i
- run: npm run build:rome - run: npm run build:production:rome
- aws-cli/setup: - aws-cli/setup:
aws_access_key_id: AWS_ACCESS_KEY_ID aws_access_key_id: AWS_ACCESS_KEY_ID
@@ -116,13 +140,38 @@ jobs:
region: AWS_REGION region: AWS_REGION
- aws-s3/sync: - aws-s3/sync:
from: build from: dist
to: "s3://rome-online-production/" to: "s3://rome-online-production/"
arguments: "--exclude '*.map'" arguments: "--exclude '*.map'"
promanager-app-build:
docker:
- image: cimg/node:18.18.2
working_directory: ~/repo/client
steps:
- checkout:
path: ~/repo
- run:
name: Install Dependencies
command: npm i
- run: npm run build:production:promanager
- aws-cli/setup:
aws_access_key_id: AWS_ACCESS_KEY_ID
aws_secret_access_key: AWS_SECRET_ACCESS_KEY
region: AWS_REGION
- aws-s3/sync:
from: dist
to: "s3://promanager-production/"
arguments: "--exclude '*.map'"
test-rome-hasura-migrate: test-rome-hasura-migrate:
docker: docker:
- image: cimg/node:16.15.0 - image: cimg/node:18.18.2
parameters: parameters:
secret: secret:
type: string type: string
@@ -154,15 +203,44 @@ jobs:
- run: npm run build:test:rome - run: npm run build:test:rome
- aws-cli/setup:
aws_access_key_id: AWS_ACCESS_KEY_ID
aws_secret_access_key: AWS_SECRET_ACCESS_KEY
region: AWS_REGION
- aws-s3/sync: - aws-s3/sync:
from: build from: dist
to: "s3://rome-online-test/" to: "s3://rome-online-test/"
arguments: "--exclude '*.map'" arguments: "--exclude '*.map'"
test-promanager-app-build:
docker:
- image: cimg/node:18.18.2
working_directory: ~/repo/client
steps:
- checkout:
path: ~/repo
- run:
name: Install Dependencies
command: npm i
- run: npm run build:test:promanager
- aws-cli/setup:
aws_access_key_id: AWS_ACCESS_KEY_ID
aws_secret_access_key: AWS_SECRET_ACCESS_KEY
region: AWS_REGION
- aws-s3/sync:
from: dist
to: "s3://promanager-testing/"
arguments: "--exclude '*.map'"
test-hasura-migrate: test-hasura-migrate:
docker: docker:
- image: cimg/node:16.15.0 - image: cimg/node:18.18.2
parameters: parameters:
secret: secret:
type: string type: string
@@ -181,7 +259,7 @@ jobs:
imex-test-app-build: imex-test-app-build:
docker: docker:
- image: cimg/node:16.15.0 - image: cimg/node:18.18.2
resource_class: large resource_class: large
working_directory: ~/repo/client working_directory: ~/repo/client
@@ -199,6 +277,32 @@ jobs:
to: "s3://imex-online-test/" to: "s3://imex-online-test/"
arguments: "--exclude '*.map'" arguments: "--exclude '*.map'"
imex-test-app-beta-build:
docker:
- image: cimg/node:18.18.2
resource_class: large
working_directory: ~/repo/client
steps:
- checkout:
path: ~/repo
- run:
name: Install Dependencies
command: npm i
- run: npm run build:test:imex
- aws-cli/setup:
aws_access_key_id: AWS_ACCESS_KEY_ID
aws_secret_access_key: AWS_SECRET_ACCESS_KEY
region: AWS_REGION
- aws-s3/sync:
from: dist
to: "s3://imex-online-test-beta/"
arguments: "--exclude '*.map'"
admin-app-build: admin-app-build:
docker: docker:
@@ -236,12 +340,16 @@ workflows:
- imex-api-deploy: - imex-api-deploy:
filters: filters:
branches: branches:
only: master only: master-AIO
- imex-app-build: - imex-app-build:
filters: filters:
branches: branches:
only: master only: master
- hasura-migrate: - imex-app-beta-build:
filters:
branches:
only: master-AIO
- imex-hasura-migrate:
secret: ${HASURA_PROD_SECRET} secret: ${HASURA_PROD_SECRET}
filters: filters:
branches: branches:
@@ -249,35 +357,46 @@ workflows:
- rome-api-deploy: - rome-api-deploy:
filters: filters:
branches: branches:
only: rome/master only: master-AIO
- rome-app-build: - rome-app-build:
filters: filters:
branches: branches:
only: rome/master only: master-AIO
- rome-hasura-migrate: - rome-hasura-migrate:
secret: ${HASURA_PROD_SECRET} secret: ${HASURA_PROD_SECRET}
filters: filters:
branches: branches:
only: rome/master only: master-AIO
- imex-test-app-build: - imex-test-app-build:
filters: filters:
branches: branches:
only: test only: test
- imex-test-app-beta-build:
filters:
branches:
only: test-AIO
- test-hasura-migrate: - test-hasura-migrate:
secret: ${HASURA_TEST_SECRET} secret: ${HASURA_TEST_SECRET}
filters: filters:
branches: branches:
only: test only: test-AIO
- test-rome-app-build: - test-rome-app-build:
filters: filters:
branches: branches:
only: rome/test only: test-AIO
- test-promanager-app-build:
filters:
branches:
only: test-AIO
- promanager-app-build:
filters:
branches:
only: master-AIO
- test-rome-hasura-migrate: - test-rome-hasura-migrate:
secret: ${HASURA_ROME_TEST_SECRET} secret: ${HASURA_ROME_TEST_SECRET}
filters: filters:
branches: branches:
only: rome/test only: test-AIO
#- admin-app-build: #- admin-app-build:
#filters: #filters:
#branches: #branches:

View File

@@ -1,16 +1,18 @@
exports.default = { const config = {
printWidth: 120, printWidth: 120,
useTabs: false, useTabs: false,
tabWidth: 2, tabWidth: 2,
trailingComma: 'es5', trailingComma: "none",
semi: true, semi: true,
singleQuote: false, singleQuote: false,
bracketSpacing: true, bracketSpacing: true,
arrowParens: 'always', arrowParens: "always",
jsxSingleQuote: false, jsxSingleQuote: false,
bracketSameLine: false, bracketSameLine: false,
endOfLine: 'lf', endOfLine: "lf"
importOrder: ['^@core/(.*)$', '^@server/(.*)$', '^@ui/(.*)$', '^[./]'], // importOrder: ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
importOrderSeparation: true, // importOrderSeparation: true,
importOrderSortSpecifiers: true, // importOrderSortSpecifiers: true
}; };
module.exports = config;

View File

@@ -0,0 +1,316 @@
## Microsoft Teams
Integrating Microsoft Teams into your Node.js backend and React frontend application can enhance communication and workflow efficiency,
especially in a body shop management context for the automotive industry. Microsoft Teams provides several ways to integrate its functionalities into your application,
including bots, tabs, messaging extensions, webhooks, and connectors. Here's an overview of the options and how to implement them:
### 0.5 - Share to Teams (TM)
- https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/build-and-test/share-to-teams-from-web-apps
- https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/build-and-test/share-to-teams-from-personal-app-or-tab
- https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/device-capabilities/people-picker-capability
- (Example of some Teams code Integrations) https://github.com/microsoft/teams-powerapps-app-templates/
### 1. Microsoft Graph API
The Microsoft Graph API is the gateway to data and intelligence in Microsoft 365, including Teams. It allows you to work with Teams data like channels, messages, and more. You can use it to post status changes back to Teams or read data from Teams to affect your site.
Backend (Node.js): Use the @microsoft/microsoft-graph-client package to make API calls to Teams. You will need to handle authentication with Azure AD, which can be done using the @azure/identity package to get tokens for Graph API requests.
*Implementation Steps*:
Register your application in Azure AD to get the client_id and client_secret.
(Is client_id and client_secret going to be something required for every customer or is it going to be a singleton)
Implement OAuth 2.0 authorization flow to obtain access tokens.
(This will need to be tracked by association to a bodyshop)
bodyshop->Channels | People ->Messages
Use the access token to make requests to the Microsoft Graph API to interact with Teams.
### 2. Webhooks and Connectors
Webhooks allow your application to send notifications to a Teams channel, which is useful for posting status changes. Connectors are a set of predefined webhooks that offer a more integrated experience.
Setup:
In Microsoft Teams, configure an incoming webhook for the channel you want to post messages to.
Use the webhook URL to send JSON payloads from your Node.js backend, which can then be displayed in Teams.
### 3. Bots
Bots in Teams can interact with users to take commands or post information. You can use the Bot Framework along with the Teams activity handler to create bots that can communicate with your React frontend and Node.js backend.
Backend (Node.js): Use the botbuilder package to create and manage your bot's interactions with Teams.
*Implementation Steps*:
Create a bot registration in Azure Bot Services.
Implement the bot logic in your Node.js application using the Bot Framework SDK.
Use the Microsoft Bot Framework's TeamsActivityHandler to respond to Teams-specific activities.
### 4. Tabs
Tabs in Teams allow you to integrate web-based content as part of Teams, which can be your React application or specific parts of it. This is particularly useful for creating a seamless experience within Teams.
*Implementation Steps*:
- Create a Teams app manifest that defines your tab and its configuration.
- Host your React application or the specific parts you want to embed as a tab.
- Use the Teams SDK in your React application to interact with Teams context and APIs.
*Getting Started*:
Microsoft Teams Toolkit for Visual Studio Code: This toolkit simplifies the process of setting up your Teams application, including authentication, configuration, and deployment.
Documentation and Samples: Microsoft provides extensive documentation and sample code for developing Teams applications, which can be invaluable for getting started and solving specific challenges.
Implementing these features requires a good understanding of both the Microsoft Teams platform and your application's architecture.
Start small, perhaps by implementing notifications via webhooks, and gradually add more complex integrations like bots or tabs based on your needs and user feedback.
### Examples:
##### Posting Messages to a Teams Channel Using Incoming Webhooks
1 - Set up an Incoming Webhook in Microsoft Teams:
- Go to the Teams channel where you want to post messages.
- Click on the three dots (...) next to the channel name and select Connectors.
- Search for Incoming Webhook, click Add, and then Configure.
- Name your webhook, upload an image if desired, and click Create.
- Copy the webhook URL provided.
(Patrick Note: The clients might have issues with setting up webhooks, it requires a lot of user configuration and our users are not technical).
2 - Node.js Code to Post a Message:
Ensure you have axios or any HTTP client library installed in your Node.js project. If not, you can install axios via npm: npm install axios.
Use the following code snippet to post a message to the Teams channel:
```javascript
const axios = require('axios');
// Replace 'YOUR_WEBHOOK_URL' with your actual webhook URL
const webhookUrl = 'YOUR_WEBHOOK_URL';
const message = {
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
"summary": "Issue 176715375",
"themeColor": "0078D7",
"title": "Issue opened: \"Push notifications not working\"",
"sections": [{
"activityTitle": "A new issue was created",
"activitySubtitle": "Today, 2:36 PM",
"activityImage": "https://example.com/issues/image.png",
"facts": [{
"name": "Repository:",
"value": "Repo name"
},
{
"name": "Issue #:",
"value": "176715375"
}
],
"markdown": true
}]
};
axios.post(webhookUrl, message)
.then(response => console.log('Successfully sent message to Teams channel'))
.catch(error => console.error('Error sending message to Teams channel', error));
```
#### Creating a Simple Bot for Teams
1 Prerequisites:
- Register your bot with the Microsoft Bot Framework and get your bot's App ID and App Password.
- Use the Bot Framework SDK for JavaScript.
2 Install Dependencies:
- You need to install the botbuilder package. Run npm install botbuilder.
The following example demonstrates a simple bot that echoes back received messages.
```javascript
const { BotFrameworkAdapter, MemoryStorage, ConversationState, TurnContext } = require('botbuilder');
// Create adapter.
// See https://aka.ms/about-bot-adapter to learn more about adapters.
const adapter = new BotFrameworkAdapter({
appId: process.env.MicrosoftAppId,
appPassword: process.env.MicrosoftAppPassword
});
// Create conversation state with in-memory storage provider.
const conversationState = new ConversationState(new MemoryStorage());
adapter.use(conversationState);
// Listen for incoming requests.
server.post('/api/messages', (req, res) => {
adapter.processActivity(req, res, async (context) => {
// Echo back what the user said
if (context.activity.type === 'message') {
await context.sendActivity(`You said '${context.activity.text}'`);
}
});
});
```
Running Your Bot:
- Make sure to expose your bot endpoint (/api/messages in this case) to the internet using a service like ngrok.
- Update your bot's messaging endpoint in the Microsoft Bot Framework portal to point to the ngrok URL.
### Slash Commands (Bot)
1 - Create a Microsoft Teams App: You need to develop a Teams app that can interact with users through commands. This involves using the Microsoft Teams Developer Platform and possibly the Bot Framework.
2 - Use Bots for Custom Commands: The primary way to introduce custom slash commands in Teams is through bots. Bots can respond to specific commands (or messages) that are input by users. When you create a bot for Teams, you can define custom commands that the bot will recognize and respond to.
3 - Developing the Bot: You can develop a bot using the Microsoft Bot Framework. This allows your bot to receive and send messages to a Teams channel or chat. Within your bot's code, you can define what actions to take when it receives specific commands.
4 - Register Your Bot with Microsoft Bot Framework: Register your bot with the Microsoft Bot Framework and configure it to work with Microsoft Teams. This step involves getting a Microsoft App ID and password that are necessary for your bot to communicate with the Teams platform.
5 - Add Your Bot to Microsoft Teams: Once your bot is developed and registered, you can package your Teams app (which includes the bot) and upload it to Microsoft Teams. This will make your bot available to users within Teams, where they can interact with it using the custom commands you've defined.
6 - Handling Slash Commands: In the context of your bot's code, you will need to interpret messages that start with a slash (/) as commands. You can then parse the command text and perform the appropriate actions or respond accordingly.
7 - Publish Your App: For broader distribution, you can publish your Teams app to the Teams app store or distribute it within your organization through the Teams admin center.
/ <command> <arb> ....
(Unrelated to teams but then used in teams for functionality)
- A Job has a todo list
- a todo item may have an employee
- a todo item may have a due date
- a todo item may have a priority
--- Call notes ----
- Tasks (TODO List)
- EMail reminders
## Slack
Integrating Slack into your Node.js backend and React frontend application can significantly streamline communication and operations for your automotive industry body shop management software. Slack offers various integration points including bots, apps, webhooks, and its rich API to facilitate interactions between your application and Slack workspace. Here's how you can leverage these integration points:
### 1. **Slack Web API**
The Slack Web API allows you to interact with Slack, enabling functionalities like sending messages, managing channels, and more directly from your application.
- **Backend (Node.js)**: Utilize the `@slack/web-api` package to make API calls from your Node.js backend. This will be the backbone for actions such as posting status updates to Slack channels or handling commands from Slack that can affect your site.
- **Implementation Steps**:
1. Create a Slack app in your Slack workspace and obtain the API tokens.
2. Use the `@slack/web-api` package to authenticate and interact with Slack API endpoints.
3. Implement features such as sending messages or processing events from Slack.
### 2. **Incoming Webhooks**
For simpler integrations focused on sending notifications to Slack channels, incoming webhooks are straightforward and effective. They allow you to send messages to a specific channel without a full-blown app.
- **Setup**:
1. Create an incoming webhook from the Slack app configuration page.
2. Use the webhook URL to send messages from your Node.js backend by making simple HTTP POST requests with your message payload.
### 3. **Bots**
Slack bots can facilitate interactive experiences within your Slack workspace, responding to commands, posting notifications, or even pulling data from your site on demand.
- **Backend (Node.js)**: Leverage the `@slack/bolt` framework, which simplifies creating Slack bots with event handling, messaging, and built-in OAuth support.
- **Implementation Steps**:
1. Create a Slack app and enable bot features.
2. Use the `@slack/bolt` package to develop your bot, handling events like messages or commands.
3. Deploy your bot and set it to listen to incoming events from Slack.
### 4. **Slack Block Kit**
For more engaging and interactive messages, Slack's Block Kit provides a UI framework that allows you to create richly formatted messages, modals, and more.
- **Frontend (React)** and **Backend (Node.js)**: Utilize Slack's Block Kit to design complex messages with buttons, sections, and interactive components. You can send these payloads through the Web API or from bots to enhance your app's interaction with users.
### Getting Started
- **Slack API Documentation and Tools**: Slack's API documentation is comprehensive and includes tutorials, tooling (like Block Kit Builder), and SDK documentation to help you get started.
- **Testing and Development**: Slack provides a sandbox environment for you to test your app's integrations and interactions without affecting your live workspace.
To integrate Slack into your application effectively, start by planning out the interactions you need (e.g., notifications, commands, information retrieval) and map these to the appropriate Slack features. Then, incrementally build and test each integration, utilizing Slack's development tools and your existing Node.js and React knowledge to create a seamless experience for your users.
#### Examples
1. Incoming Webhooks
Incoming Webhooks are a simple way to post messages from your Node.js application into Slack channels. They're perfect for notifying team members about status changes or updates.
Setup:
1 - Create a new Slack app in your workspace and enable incoming webhooks.
2 - Add a new webhook to your app and choose the channel it will post to.
3 - Use the webhook URL to send messages from your backend.
Node.js Example:
```javascript
const axios = require('axios');
const webhookUrl = 'YOUR_SLACK_WEBHOOK_URL';
async function postMessageToSlack(message) {
await axios.post(webhookUrl, {
text: message, // Your message here
});
}
postMessageToSlack('New status update on the automotive project!').catch(console.error);
```
2. Bots
Slack bots can interact with users via messages, respond to commands, and even post updates. They're great for building interactive features.
Setup:
1 - Create a Slack app and add the Bot Token Scopes (like chat:write).
2 - Install the app to your workspace to get your bot token.
3 - Use the @slack/bolt framework for easy bot development in Node.js.
Node.js Example:
```javascript
const { App } = require('@slack/bolt');
const app = new App({
token: process.env.SLACK_BOT_TOKEN, // Set your bot's access token here
signingSecret: process.env.SLACK_SIGNING_SECRET // Set your app's signing secret here
});
app.message('hello', async ({ message, say }) => {
await say(`Hey there <@${message.user}>!`);
});
(async () => {
await app.start(process.env.PORT || 3000);
console.log('Slack bot is running!');
})();
```
3. Slash Commands
Slash Commands allow users to interact with your application directly from Slack by typing commands that start with /.
Setup:
1 - Create a Slack app and configure a new Slash Command (e.g., /statusupdate).
2 - Point the command request URL to your Node.js backend endpoint.
3 - Implement the endpoint to handle the command and respond accordingly.
Node.js Example:
```javascript
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
app.post('/slack/commands/statusupdate', (req, res) => {
const { text, user_name } = req.body; // Command input and user info
// Logic to handle the command goes here. For example, update a status or fetch information.
res.send(`Status update received: ${text} - from ${user_name}`);
});
app.listen(process.env.PORT || 3000, () => console.log('Server is running'));
```
4. Interactive Components
Interactive components like buttons or menus can make your Slack messages more engaging and interactive.
Setup:
1 - Enable Interactivity in your Slack app settings.
2 - Implement an endpoint in your Node.js backend to handle interactions.
3 - Send messages with interactive components from your backend.
Implementing these features into your Node.js and React application will enable a more dynamic and integrated experience for users of your body shop management software. Start with the simpler integrations like webhooks and progressively incorporate bots and interactive components as needed.
ACTION ITEMS:
- Slot in tasks, this will be a dependency for things we want to do in the future.
- This would involve GUI components
- This would involve a backend component

View File

@@ -567,4 +567,4 @@
"description": "Exempt" "description": "Exempt"
} }
] ]
} }

View File

@@ -3,7 +3,13 @@
This documentation details the schema required for `.filters` files on the report server. It is used to dynamically This documentation details the schema required for `.filters` files on the report server. It is used to dynamically
modify the graphQL query and provide the user more power over their reports. modify the graphQL query and provide the user more power over their reports.
# Special Notes For filters and sorters, valid types include (`type` key in the schema):
- string (default)
- number
- bool or boolean
- date
## Special Notes
- When passing the data to the template server, the property filters and sorters is added to the data object and will reflect the filters and sorters the user has selected - When passing the data to the template server, the property filters and sorters is added to the data object and will reflect the filters and sorters the user has selected
## High level Schema Overview ## High level Schema Overview
@@ -40,9 +46,10 @@ Filters effect the where clause of the graphQL query. They are used to filter th
A note on special notation used in the `name` field. A note on special notation used in the `name` field.
## Reflection ## Reflection
Filters can make use of reflection to pre-fill select boxes, the following is an example of that in the filters file. Filters can make use of reflection to pre-fill select boxes, the following is an example of that in the filters file.
``` ```json
{ {
"name": "jobs.status", "name": "jobs.status",
"translation": "jobs.fields.status", "translation": "jobs.fields.status",
@@ -52,7 +59,7 @@ Filters can make use of reflection to pre-fill select boxes, the following is an
"type": "internal", "type": "internal",
"name": "special.job_statuses" "name": "special.job_statuses"
} }
}, }
``` ```
in this example, a reflector with the type 'internal' (all types at the moment require this, and it is used for future functionality), with a name of `special.job_statuses` in this example, a reflector with the type 'internal' (all types at the moment require this, and it is used for future functionality), with a name of `special.job_statuses`
@@ -67,7 +74,13 @@ The following cases are available
- `special.employees` - This will reflect the employees `bodyshop.employees` - `special.employees` - This will reflect the employees `bodyshop.employees`
- `special.first_names` - This will reflect the first names `bodyshop.employees` - `special.first_names` - This will reflect the first names `bodyshop.employees`
- `special.last_names` - This will reflect the last names `bodyshop.employees` - `special.last_names` - This will reflect the last names `bodyshop.employees`
- - `special.referral_sources` - This will reflect the referral sources `bodyshop.md_referral_sources`
- `special.class`- This will reflect the class `bodyshop.md_classes`
- `special.lost_sale_reasons` - This will reflect the lost sale reasons `bodyshop.md_lost_sale_reasons`
- `special.alt_transports` - This will reflect the alternative transports `bodyshop.appt_alt_transport`
- `special.payment_types` - This will reflect the payment types `bodyshop.md_payment_types`
- `special.payment_payers` - This is a special case with a key value set of [Customer, Insurance]
### Path without brackets, multi level ### Path without brackets, multi level
`"name": "jobs.joblines.mod_lb_hrs",` `"name": "jobs.joblines.mod_lb_hrs",`
@@ -142,12 +155,12 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!
- Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs` - Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs`
is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not. is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not.
- The `dates` object is not yet implemented and will be added in a future release. - The type object must be 'string' or 'number' or 'bool' or 'boolean' or 'date' and is case-sensitive.
- The type object must be 'string' or 'number' and is case-sensitive.
- The `translation` key is used to look up the label in the GUI, if it is not found, the `label` key is used. - The `translation` key is used to look up the label in the GUI, if it is not found, the `label` key is used.
- Do not add the ability to filter things that are already filtered as part of the original query, this would be - Do not add the ability to filter things that are already filtered as part of the original query, this would be
redundant and could cause issues. redundant and could cause issues.
- Do not add the ability to filter on things like FK constraints, must like the above example. - Do not add the ability to filter on things like FK constraints, must like the above example.
- *INHERITANCE CAVEAT* If you have a filters file on a parent report that has a child that you do not want the filters inherited from, you must place a blank filters file (valid json so {}) on the child report level. This will than fetch the child filters, which are empty and move along, versus inheriting the parent filters.
## Sorters ## Sorters
@@ -158,6 +171,7 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!
using the sorters. using the sorters.
### Default Sorters ### Default Sorters
- A sorter can be given a default object containing a `order` and `direction` key value. This will be used to sort the report if the user does not select any of the sorters themselves. - A sorter can be given a default object containing a `order` and `direction` key value. This will be used to sort the report if the user does not select any of the sorters themselves.
- The `order` key is the order in which the sorters are applied, and the `direction` key is the direction of the sort, either `asc` or `desc`. - The `order` key is the order in which the sorters are applied, and the `direction` key is the direction of the sort, either `asc` or `desc`.
@@ -172,4 +186,4 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!
"direction": "asc" "direction": "asc"
} }
} }
``` ```

View File

@@ -1,20 +1,20 @@
module.exports = { module.exports = {
apps: [ apps: [
{ {
name: "IO Test API", name: "IO Test API",
cwd: "./io", cwd: "./io",
script: "./server.js", script: "./server.js",
env: { env: {
NODE_ENV: "test", NODE_ENV: "test"
}, }
}, },
{ {
name: "Bitbucket Webhook", name: "Bitbucket Webhook",
script: "./webhook/index.js", script: "./webhook/index.js",
env: { env: {
NODE_ENV: "production", NODE_ENV: "production"
}, }
}, }
], ]
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,8 @@
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
VITE_APP_GA_CODE=231099835 VITE_APP_GA_CODE=231099835
VITE_APP_FIREBASE_CONFIG={ "apiKey": "AIzaSyAuLQR9SV5LsVxjU8wh9hvFLdhcAHU6cxE", "authDomain": "rome-prod-1.firebaseapp.com", "projectId": "rome-prod-1", "storageBucket": "rome-prod-1.appspot.com", "messagingSenderId": "147786367145", "appId": "1:147786367145:web:9d4cba68071c3f29a8a9b8", "measurementId": "G-G8Z9DRHTZS"} # VITE_APP_FIREBASE_CONFIG={ "apiKey": "AIzaSyAuLQR9SV5LsVxjU8wh9hvFLdhcAHU6cxE", "authDomain": "rome-prod-1.firebaseapp.com", "projectId": "rome-prod-1", "storageBucket": "rome-prod-1.appspot.com", "messagingSenderId": "147786367145", "appId": "1:147786367145:web:9d4cba68071c3f29a8a9b8", "measurementId": "G-G8Z9DRHTZS"}
VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
VITE_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test VITE_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test
VITE_APP_CLOUDINARY_API_KEY=957865933348715 VITE_APP_CLOUDINARY_API_KEY=957865933348715

View File

@@ -1,7 +1,8 @@
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
VITE_APP_GA_CODE=231099835 VITE_APP_GA_CODE=231099835
VITE_APP_FIREBASE_CONFIG={ "apiKey": "AIzaSyAuLQR9SV5LsVxjU8wh9hvFLdhcAHU6cxE", "authDomain": "rome-prod-1.firebaseapp.com", "projectId": "rome-prod-1", "storageBucket": "rome-prod-1.appspot.com", "messagingSenderId": "147786367145", "appId": "1:147786367145:web:9d4cba68071c3f29a8a9b8", "measurementId": "G-G8Z9DRHTZS"} # VITE_APP_FIREBASE_CONFIG={ "apiKey": "AIzaSyAuLQR9SV5LsVxjU8wh9hvFLdhcAHU6cxE", "authDomain": "rome-prod-1.firebaseapp.com", "projectId": "rome-prod-1", "storageBucket": "rome-prod-1.appspot.com", "messagingSenderId": "147786367145", "appId": "1:147786367145:web:9d4cba68071c3f29a8a9b8", "measurementId": "G-G8Z9DRHTZS"}
VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
VITE_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test VITE_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test
VITE_APP_CLOUDINARY_API_KEY=957865933348715 VITE_APP_CLOUDINARY_API_KEY=957865933348715

8
client/.eslintrc Normal file
View File

@@ -0,0 +1,8 @@
{
"extends": [
"react-app"
],
"rules": {
"no-useless-rename": "off"
}
}

View File

@@ -1,10 +1,10 @@
// craco.config.js // craco.config.js
const TerserPlugin = require("terser-webpack-plugin"); const TerserPlugin = require("terser-webpack-plugin");
const CracoLessPlugin = require("craco-less"); const CracoLessPlugin = require("craco-less");
const {convertLegacyToken} = require('@ant-design/compatible/lib'); const { convertLegacyToken } = require("@ant-design/compatible/lib");
const {theme} = require('antd/lib'); const { theme } = require("antd/lib");
const {defaultAlgorithm, defaultSeed} = theme; const { defaultAlgorithm, defaultSeed } = theme;
const mapToken = defaultAlgorithm(defaultSeed); const mapToken = defaultAlgorithm(defaultSeed);
const v4Token = convertLegacyToken(mapToken); const v4Token = convertLegacyToken(mapToken);
@@ -12,43 +12,42 @@ 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. // TODO, At the moment we are using less in the Dashboard. Once we remove this we can remove the less processor entirely.
module.exports = { module.exports = {
plugins: [ plugins: [
{
{ plugin: CracoLessPlugin,
plugin: CracoLessPlugin, options: {
options: { lessLoaderOptions: {
lessLoaderOptions: { lessOptions: {
lessOptions: { modifyVars: { ...v4Token },
modifyVars: {...v4Token}, javascriptEnabled: true
javascriptEnabled: true, }
}, }
}, }
}, }
],
webpack: {
configure: (webpackConfig) => {
return {
...webpackConfig,
// Required for Dev Server
devServer: {
...webpackConfig.devServer,
allowedHosts: "all"
}, },
], optimization: {
webpack: { ...webpackConfig.optimization,
configure: (webpackConfig) => { // Workaround for CircleCI bug caused by the number of CPUs shown
return { // https://github.com/facebook/create-react-app/issues/8320
...webpackConfig, minimizer: webpackConfig.optimization.minimizer.map((item) => {
// Required for Dev Server if (item instanceof TerserPlugin) {
devServer: { item.options.parallel = 2;
...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;
}
return item; return item;
}), })
}, }
}; };
}, }
}, },
devtool: "source-map", devtool: "source-map"
}; };

View File

@@ -1,17 +1,17 @@
const {defineConfig} = require('cypress') const { defineConfig } = require("cypress");
module.exports = defineConfig({ module.exports = defineConfig({
experimentalStudio: true, experimentalStudio: true,
env: { env: {
FIREBASE_USERNAME: 'cypress@imex.test', FIREBASE_USERNAME: "cypress@imex.test",
FIREBASE_PASSWORD: 'cypress', 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);
}, },
e2e: { baseUrl: "http://localhost:3000"
// 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',
},
})

View File

@@ -1,24 +1,19 @@
/// <reference types="Cypress" /> /// <reference types="Cypress" />
const {FIREBASE_USERNAME, FIREBASE_PASSWORcD} = Cypress.env(); const { FIREBASE_USERNAME, FIREBASE_PASSWORcD } = Cypress.env();
describe("Renders the General Page", () => { describe("Renders the General Page", () => {
beforeEach(() => { beforeEach(() => {
cy.visit("/"); cy.visit("/");
}); });
it("Renders Correctly", () => { it("Renders Correctly", () => {});
}); it("Has the Slogan", () => {
it("Has the Slogan", () => { cy.findByText("A whole x22new kind of shop management system.").should("exist");
cy.findByText("A whole x22new kind of shop management system.").should( /* ==== Generated with Cypress Studio ==== */
"exist" cy.get(".ant-menu-item-active > .ant-menu-title-content > .header0-item-block").click();
); cy.get("#email").clear();
/* ==== Generated with Cypress Studio ==== */ cy.get("#email").type("patrick@imex.dev");
cy.get( cy.get("#password").clear();
".ant-menu-item-active > .ant-menu-title-content > .header0-item-block" cy.get("#password").type("patrick123{enter}");
).click(); cy.get(".ant-form > .ant-btn").click();
cy.get("#email").clear(); /* ==== End Cypress Studio ==== */
cy.get("#email").type("patrick@imex.dev"); });
cy.get("#password").clear();
cy.get("#password").type("patrick123{enter}");
cy.get(".ant-form > .ant-btn").click();
/* ==== End Cypress Studio ==== */
});
}); });

View File

@@ -11,133 +11,114 @@
// please read our getting started guide: // please read our getting started guide:
// https://on.cypress.io/introduction-to-cypress // https://on.cypress.io/introduction-to-cypress
describe('example to-do app', () => { describe("example to-do app", () => {
beforeEach(() => {
// Cypress starts out with a blank slate for each test
// so we must tell it to visit our website with the `cy.visit()` command.
// Since we want to visit the same URL at the start of all our tests,
// we include it in our beforeEach function so that it runs before each test
cy.visit("https://example.cypress.io/todo");
});
it("displays two todo items by default", () => {
// We use the `cy.get()` command to get all elements that match the selector.
// Then, we use `should` to assert that there are two matched items,
// which are the two default items.
cy.get(".todo-list li").should("have.length", 2);
// We can go even further and check that the default todos each contain
// the correct text. We use the `first` and `last` functions
// to get just the first and last matched elements individually,
// and then perform an assertion with `should`.
cy.get(".todo-list li").first().should("have.text", "Pay electric bill");
cy.get(".todo-list li").last().should("have.text", "Walk the dog");
});
it("can add new todo items", () => {
// We'll store our item text in a variable so we can reuse it
const newItem = "Feed the cat";
// Let's get the input element and use the `type` command to
// input our new list item. After typing the content of our item,
// we need to type the enter key as well in order to submit the input.
// This input has a data-test attribute so we'll use that to select the
// element in accordance with best practices:
// https://on.cypress.io/selecting-elements
cy.get("[data-test=new-todo]").type(`${newItem}{enter}`);
// Now that we've typed our new item, let's check that it actually was added to the list.
// Since it's the newest item, it should exist as the last element in the list.
// In addition, with the two default items, we should have a total of 3 elements in the list.
// Since assertions yield the element that was asserted on,
// we can chain both of these assertions together into a single statement.
cy.get(".todo-list li").should("have.length", 3).last().should("have.text", newItem);
});
it("can check off an item as completed", () => {
// In addition to using the `get` command to get an element by selector,
// we can also use the `contains` command to get an element by its contents.
// However, this will yield the <label>, which is lowest-level element that contains the text.
// In order to check the item, we'll find the <input> element for this <label>
// by traversing up the dom to the parent element. From there, we can `find`
// the child checkbox <input> element and use the `check` command to check it.
cy.contains("Pay electric bill").parent().find("input[type=checkbox]").check();
// Now that we've checked the button, we can go ahead and make sure
// that the list element is now marked as completed.
// Again we'll use `contains` to find the <label> element and then use the `parents` command
// to traverse multiple levels up the dom until we find the corresponding <li> element.
// Once we get that element, we can assert that it has the completed class.
cy.contains("Pay electric bill").parents("li").should("have.class", "completed");
});
context("with a checked task", () => {
beforeEach(() => { beforeEach(() => {
// Cypress starts out with a blank slate for each test // We'll take the command we used above to check off an element
// so we must tell it to visit our website with the `cy.visit()` command. // Since we want to perform multiple tests that start with checking
// Since we want to visit the same URL at the start of all our tests, // one element, we put it in the beforeEach hook
// we include it in our beforeEach function so that it runs before each test // so that it runs at the start of every test.
cy.visit('https://example.cypress.io/todo') cy.contains("Pay electric bill").parent().find("input[type=checkbox]").check();
}) });
it('displays two todo items by default', () => { it("can filter for uncompleted tasks", () => {
// We use the `cy.get()` command to get all elements that match the selector. // We'll click on the "active" button in order to
// Then, we use `should` to assert that there are two matched items, // display only incomplete items
// which are the two default items. cy.contains("Active").click();
cy.get('.todo-list li').should('have.length', 2)
// We can go even further and check that the default todos each contain // After filtering, we can assert that there is only the one
// the correct text. We use the `first` and `last` functions // incomplete item in the list.
// to get just the first and last matched elements individually, cy.get(".todo-list li").should("have.length", 1).first().should("have.text", "Walk the dog");
// and then perform an assertion with `should`.
cy.get('.todo-list li').first().should('have.text', 'Pay electric bill')
cy.get('.todo-list li').last().should('have.text', 'Walk the dog')
})
it('can add new todo items', () => { // For good measure, let's also assert that the task we checked off
// We'll store our item text in a variable so we can reuse it // does not exist on the page.
const newItem = 'Feed the cat' cy.contains("Pay electric bill").should("not.exist");
});
// Let's get the input element and use the `type` command to it("can filter for completed tasks", () => {
// input our new list item. After typing the content of our item, // We can perform similar steps as the test above to ensure
// we need to type the enter key as well in order to submit the input. // that only completed tasks are shown
// This input has a data-test attribute so we'll use that to select the cy.contains("Completed").click();
// element in accordance with best practices:
// https://on.cypress.io/selecting-elements
cy.get('[data-test=new-todo]').type(`${newItem}{enter}`)
// Now that we've typed our new item, let's check that it actually was added to the list. cy.get(".todo-list li").should("have.length", 1).first().should("have.text", "Pay electric bill");
// Since it's the newest item, it should exist as the last element in the list.
// In addition, with the two default items, we should have a total of 3 elements in the list.
// Since assertions yield the element that was asserted on,
// we can chain both of these assertions together into a single statement.
cy.get('.todo-list li')
.should('have.length', 3)
.last()
.should('have.text', newItem)
})
it('can check off an item as completed', () => { cy.contains("Walk the dog").should("not.exist");
// In addition to using the `get` command to get an element by selector, });
// we can also use the `contains` command to get an element by its contents.
// However, this will yield the <label>, which is lowest-level element that contains the text.
// In order to check the item, we'll find the <input> element for this <label>
// by traversing up the dom to the parent element. From there, we can `find`
// the child checkbox <input> element and use the `check` command to check it.
cy.contains('Pay electric bill')
.parent()
.find('input[type=checkbox]')
.check()
// Now that we've checked the button, we can go ahead and make sure it("can delete all completed tasks", () => {
// that the list element is now marked as completed. // First, let's click the "Clear completed" button
// Again we'll use `contains` to find the <label> element and then use the `parents` command // `contains` is actually serving two purposes here.
// to traverse multiple levels up the dom until we find the corresponding <li> element. // First, it's ensuring that the button exists within the dom.
// Once we get that element, we can assert that it has the completed class. // This button only appears when at least one task is checked
cy.contains('Pay electric bill') // so this command is implicitly verifying that it does exist.
.parents('li') // Second, it selects the button so we can click it.
.should('have.class', 'completed') cy.contains("Clear completed").click();
})
context('with a checked task', () => { // Then we can make sure that there is only one element
beforeEach(() => { // in the list and our element does not exist
// We'll take the command we used above to check off an element cy.get(".todo-list li").should("have.length", 1).should("not.have.text", "Pay electric bill");
// Since we want to perform multiple tests that start with checking
// one element, we put it in the beforeEach hook
// so that it runs at the start of every test.
cy.contains('Pay electric bill')
.parent()
.find('input[type=checkbox]')
.check()
})
it('can filter for uncompleted tasks', () => { // Finally, make sure that the clear button no longer exists.
// We'll click on the "active" button in order to cy.contains("Clear completed").should("not.exist");
// display only incomplete items });
cy.contains('Active').click() });
});
// After filtering, we can assert that there is only the one
// incomplete item in the list.
cy.get('.todo-list li')
.should('have.length', 1)
.first()
.should('have.text', 'Walk the dog')
// For good measure, let's also assert that the task we checked off
// does not exist on the page.
cy.contains('Pay electric bill').should('not.exist')
})
it('can filter for completed tasks', () => {
// We can perform similar steps as the test above to ensure
// that only completed tasks are shown
cy.contains('Completed').click()
cy.get('.todo-list li')
.should('have.length', 1)
.first()
.should('have.text', 'Pay electric bill')
cy.contains('Walk the dog').should('not.exist')
})
it('can delete all completed tasks', () => {
// First, let's click the "Clear completed" button
// `contains` is actually serving two purposes here.
// First, it's ensuring that the button exists within the dom.
// This button only appears when at least one task is checked
// so this command is implicitly verifying that it does exist.
// Second, it selects the button so we can click it.
cy.contains('Clear completed').click()
// Then we can make sure that there is only one element
// in the list and our element does not exist
cy.get('.todo-list li')
.should('have.length', 1)
.should('not.have.text', 'Pay electric bill')
// Finally, make sure that the clear button no longer exists.
cy.contains('Clear completed').should('not.exist')
})
})
})

View File

@@ -1,299 +1,284 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Actions', () => { context("Actions", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/actions') cy.visit("https://example.cypress.io/commands/actions");
}) });
// https://on.cypress.io/interacting-with-elements // https://on.cypress.io/interacting-with-elements
it('.type() - type into a DOM element', () => { it(".type() - type into a DOM element", () => {
// https://on.cypress.io/type // https://on.cypress.io/type
cy.get('.action-email') cy.get(".action-email")
.type('fake@email.com').should('have.value', 'fake@email.com') .type("fake@email.com")
.should("have.value", "fake@email.com")
// .type() with special character sequences // .type() with special character sequences
.type('{leftarrow}{rightarrow}{uparrow}{downarrow}') .type("{leftarrow}{rightarrow}{uparrow}{downarrow}")
.type('{del}{selectall}{backspace}') .type("{del}{selectall}{backspace}")
// .type() with key modifiers // .type() with key modifiers
.type('{alt}{option}') //these are equivalent .type("{alt}{option}") //these are equivalent
.type('{ctrl}{control}') //these are equivalent .type("{ctrl}{control}") //these are equivalent
.type('{meta}{command}{cmd}') //these are equivalent .type("{meta}{command}{cmd}") //these are equivalent
.type('{shift}') .type("{shift}")
// Delay each keypress by 0.1 sec // Delay each keypress by 0.1 sec
.type('slow.typing@email.com', {delay: 100}) .type("slow.typing@email.com", { delay: 100 })
.should('have.value', 'slow.typing@email.com') .should("have.value", "slow.typing@email.com");
cy.get('.action-disabled') cy.get(".action-disabled")
// Ignore error checking prior to type // Ignore error checking prior to type
// like whether the input is visible or disabled // like whether the input is visible or disabled
.type('disabled error checking', {force: true}) .type("disabled error checking", { force: true })
.should('have.value', 'disabled error checking') .should("have.value", "disabled error checking");
}) });
it('.focus() - focus on a DOM element', () => { it(".focus() - focus on a DOM element", () => {
// https://on.cypress.io/focus // https://on.cypress.io/focus
cy.get('.action-focus').focus() cy.get(".action-focus").focus().should("have.class", "focus").prev().should("have.attr", "style", "color: orange;");
.should('have.class', 'focus') });
.prev().should('have.attr', 'style', 'color: orange;')
})
it('.blur() - blur off a DOM element', () => { it(".blur() - blur off a DOM element", () => {
// https://on.cypress.io/blur // https://on.cypress.io/blur
cy.get('.action-blur').type('About to blur').blur() cy.get(".action-blur")
.should('have.class', 'error') .type("About to blur")
.prev().should('have.attr', 'style', 'color: red;') .blur()
}) .should("have.class", "error")
.prev()
.should("have.attr", "style", "color: red;");
});
it('.clear() - clears an input or textarea element', () => { it(".clear() - clears an input or textarea element", () => {
// https://on.cypress.io/clear // https://on.cypress.io/clear
cy.get('.action-clear').type('Clear this text') cy.get(".action-clear")
.should('have.value', 'Clear this text') .type("Clear this text")
.clear() .should("have.value", "Clear this text")
.should('have.value', '') .clear()
}) .should("have.value", "");
});
it('.submit() - submit a form', () => { it(".submit() - submit a form", () => {
// https://on.cypress.io/submit // https://on.cypress.io/submit
cy.get('.action-form') cy.get(".action-form").find('[type="text"]').type("HALFOFF");
.find('[type="text"]').type('HALFOFF')
cy.get('.action-form').submit() cy.get(".action-form").submit().next().should("contain", "Your form has been submitted!");
.next().should('contain', 'Your form has been submitted!') });
})
it('.click() - click on a DOM element', () => { it(".click() - click on a DOM element", () => {
// https://on.cypress.io/click // https://on.cypress.io/click
cy.get('.action-btn').click() cy.get(".action-btn").click();
// You can click on 9 specific positions of an element: // You can click on 9 specific positions of an element:
// ----------------------------------- // -----------------------------------
// | topLeft top topRight | // | topLeft top topRight |
// | | // | |
// | | // | |
// | | // | |
// | left center right | // | left center right |
// | | // | |
// | | // | |
// | | // | |
// | bottomLeft bottom bottomRight | // | bottomLeft bottom bottomRight |
// ----------------------------------- // -----------------------------------
// clicking in the center of the element is the default // clicking in the center of the element is the default
cy.get('#action-canvas').click() cy.get("#action-canvas").click();
cy.get('#action-canvas').click('topLeft') cy.get("#action-canvas").click("topLeft");
cy.get('#action-canvas').click('top') cy.get("#action-canvas").click("top");
cy.get('#action-canvas').click('topRight') cy.get("#action-canvas").click("topRight");
cy.get('#action-canvas').click('left') cy.get("#action-canvas").click("left");
cy.get('#action-canvas').click('right') cy.get("#action-canvas").click("right");
cy.get('#action-canvas').click('bottomLeft') cy.get("#action-canvas").click("bottomLeft");
cy.get('#action-canvas').click('bottom') cy.get("#action-canvas").click("bottom");
cy.get('#action-canvas').click('bottomRight') cy.get("#action-canvas").click("bottomRight");
// .click() accepts an x and y coordinate // .click() accepts an x and y coordinate
// that controls where the click occurs :) // that controls where the click occurs :)
cy.get('#action-canvas') cy.get("#action-canvas")
.click(80, 75) // click 80px on x coord and 75px on y coord .click(80, 75) // click 80px on x coord and 75px on y coord
.click(170, 75) .click(170, 75)
.click(80, 165) .click(80, 165)
.click(100, 185) .click(100, 185)
.click(125, 190) .click(125, 190)
.click(150, 185) .click(150, 185)
.click(170, 165) .click(170, 165);
// click multiple elements by passing multiple: true // click multiple elements by passing multiple: true
cy.get('.action-labels>.label').click({multiple: true}) cy.get(".action-labels>.label").click({ multiple: true });
// Ignore error checking prior to clicking // Ignore error checking prior to clicking
cy.get('.action-opacity>.btn').click({force: true}) cy.get(".action-opacity>.btn").click({ force: true });
}) });
it('.dblclick() - double click on a DOM element', () => { it(".dblclick() - double click on a DOM element", () => {
// https://on.cypress.io/dblclick // https://on.cypress.io/dblclick
// Our app has a listener on 'dblclick' event in our 'scripts.js' // Our app has a listener on 'dblclick' event in our 'scripts.js'
// that hides the div and shows an input on double click // that hides the div and shows an input on double click
cy.get('.action-div').dblclick().should('not.be.visible') cy.get(".action-div").dblclick().should("not.be.visible");
cy.get('.action-input-hidden').should('be.visible') cy.get(".action-input-hidden").should("be.visible");
}) });
it('.rightclick() - right click on a DOM element', () => { it(".rightclick() - right click on a DOM element", () => {
// https://on.cypress.io/rightclick // https://on.cypress.io/rightclick
// Our app has a listener on 'contextmenu' event in our 'scripts.js' // Our app has a listener on 'contextmenu' event in our 'scripts.js'
// that hides the div and shows an input on right click // that hides the div and shows an input on right click
cy.get('.rightclick-action-div').rightclick().should('not.be.visible') cy.get(".rightclick-action-div").rightclick().should("not.be.visible");
cy.get('.rightclick-action-input-hidden').should('be.visible') cy.get(".rightclick-action-input-hidden").should("be.visible");
}) });
it('.check() - check a checkbox or radio element', () => { it(".check() - check a checkbox or radio element", () => {
// https://on.cypress.io/check // https://on.cypress.io/check
// By default, .check() will check all // By default, .check() will check all
// matching checkbox or radio elements in succession, one after another // matching checkbox or radio elements in succession, one after another
cy.get('.action-checkboxes [type="checkbox"]').not('[disabled]') cy.get('.action-checkboxes [type="checkbox"]').not("[disabled]").check().should("be.checked");
.check().should('be.checked')
cy.get('.action-radios [type="radio"]').not('[disabled]') cy.get('.action-radios [type="radio"]').not("[disabled]").check().should("be.checked");
.check().should('be.checked')
// .check() accepts a value argument // .check() accepts a value argument
cy.get('.action-radios [type="radio"]') cy.get('.action-radios [type="radio"]').check("radio1").should("be.checked");
.check('radio1').should('be.checked')
// .check() accepts an array of values // .check() accepts an array of values
cy.get('.action-multiple-checkboxes [type="checkbox"]') cy.get('.action-multiple-checkboxes [type="checkbox"]').check(["checkbox1", "checkbox2"]).should("be.checked");
.check(['checkbox1', 'checkbox2']).should('be.checked')
// Ignore error checking prior to checking // Ignore error checking prior to checking
cy.get('.action-checkboxes [disabled]') cy.get(".action-checkboxes [disabled]").check({ force: true }).should("be.checked");
.check({force: true}).should('be.checked')
cy.get('.action-radios [type="radio"]') cy.get('.action-radios [type="radio"]').check("radio3", { force: true }).should("be.checked");
.check('radio3', {force: true}).should('be.checked') });
})
it('.uncheck() - uncheck a checkbox element', () => { it(".uncheck() - uncheck a checkbox element", () => {
// https://on.cypress.io/uncheck // https://on.cypress.io/uncheck
// By default, .uncheck() will uncheck all matching // By default, .uncheck() will uncheck all matching
// checkbox elements in succession, one after another // checkbox elements in succession, one after another
cy.get('.action-check [type="checkbox"]') cy.get('.action-check [type="checkbox"]').not("[disabled]").uncheck().should("not.be.checked");
.not('[disabled]')
.uncheck().should('not.be.checked')
// .uncheck() accepts a value argument // .uncheck() accepts a value argument
cy.get('.action-check [type="checkbox"]') cy.get('.action-check [type="checkbox"]').check("checkbox1").uncheck("checkbox1").should("not.be.checked");
.check('checkbox1')
.uncheck('checkbox1').should('not.be.checked')
// .uncheck() accepts an array of values // .uncheck() accepts an array of values
cy.get('.action-check [type="checkbox"]') cy.get('.action-check [type="checkbox"]')
.check(['checkbox1', 'checkbox3']) .check(["checkbox1", "checkbox3"])
.uncheck(['checkbox1', 'checkbox3']).should('not.be.checked') .uncheck(["checkbox1", "checkbox3"])
.should("not.be.checked");
// Ignore error checking prior to unchecking // Ignore error checking prior to unchecking
cy.get('.action-check [disabled]') cy.get(".action-check [disabled]").uncheck({ force: true }).should("not.be.checked");
.uncheck({force: true}).should('not.be.checked') });
})
it('.select() - select an option in a <select> element', () => { it(".select() - select an option in a <select> element", () => {
// https://on.cypress.io/select // https://on.cypress.io/select
// at first, no option should be selected // at first, no option should be selected
cy.get('.action-select') cy.get(".action-select").should("have.value", "--Select a fruit--");
.should('have.value', '--Select a fruit--')
// Select option(s) with matching text content // Select option(s) with matching text content
cy.get('.action-select').select('apples') cy.get(".action-select").select("apples");
// confirm the apples were selected // confirm the apples were selected
// note that each value starts with "fr-" in our HTML // note that each value starts with "fr-" in our HTML
cy.get('.action-select').should('have.value', 'fr-apples') cy.get(".action-select").should("have.value", "fr-apples");
cy.get('.action-select-multiple') cy.get(".action-select-multiple")
.select(['apples', 'oranges', 'bananas']) .select(["apples", "oranges", "bananas"])
// when getting multiple values, invoke "val" method first // when getting multiple values, invoke "val" method first
.invoke('val') .invoke("val")
.should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas']) .should("deep.equal", ["fr-apples", "fr-oranges", "fr-bananas"]);
// Select option(s) with matching value // Select option(s) with matching value
cy.get('.action-select').select('fr-bananas') cy.get(".action-select")
// can attach an assertion right away to the element .select("fr-bananas")
.should('have.value', 'fr-bananas') // can attach an assertion right away to the element
.should("have.value", "fr-bananas");
cy.get('.action-select-multiple') cy.get(".action-select-multiple")
.select(['fr-apples', 'fr-oranges', 'fr-bananas']) .select(["fr-apples", "fr-oranges", "fr-bananas"])
.invoke('val') .invoke("val")
.should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas']) .should("deep.equal", ["fr-apples", "fr-oranges", "fr-bananas"]);
// assert the selected values include oranges // assert the selected values include oranges
cy.get('.action-select-multiple') cy.get(".action-select-multiple").invoke("val").should("include", "fr-oranges");
.invoke('val').should('include', 'fr-oranges') });
})
it('.scrollIntoView() - scroll an element into view', () => { it(".scrollIntoView() - scroll an element into view", () => {
// https://on.cypress.io/scrollintoview // https://on.cypress.io/scrollintoview
// normally all of these buttons are hidden, // normally all of these buttons are hidden,
// because they're not within // because they're not within
// the viewable area of their parent // the viewable area of their parent
// (we need to scroll to see them) // (we need to scroll to see them)
cy.get('#scroll-horizontal button') cy.get("#scroll-horizontal button").should("not.be.visible");
.should('not.be.visible')
// scroll the button into view, as if the user had scrolled // scroll the button into view, as if the user had scrolled
cy.get('#scroll-horizontal button').scrollIntoView() cy.get("#scroll-horizontal button").scrollIntoView().should("be.visible");
.should('be.visible')
cy.get('#scroll-vertical button') cy.get("#scroll-vertical button").should("not.be.visible");
.should('not.be.visible')
// Cypress handles the scroll direction needed // Cypress handles the scroll direction needed
cy.get('#scroll-vertical button').scrollIntoView() cy.get("#scroll-vertical button").scrollIntoView().should("be.visible");
.should('be.visible')
cy.get('#scroll-both button') cy.get("#scroll-both button").should("not.be.visible");
.should('not.be.visible')
// Cypress knows to scroll to the right and down // Cypress knows to scroll to the right and down
cy.get('#scroll-both button').scrollIntoView() cy.get("#scroll-both button").scrollIntoView().should("be.visible");
.should('be.visible') });
})
it('.trigger() - trigger an event on a DOM element', () => { it(".trigger() - trigger an event on a DOM element", () => {
// https://on.cypress.io/trigger // https://on.cypress.io/trigger
// To interact with a range input (slider) // To interact with a range input (slider)
// we need to set its value & trigger the // we need to set its value & trigger the
// event to signal it changed // event to signal it changed
// Here, we invoke jQuery's val() method to set // Here, we invoke jQuery's val() method to set
// the value and trigger the 'change' event // the value and trigger the 'change' event
cy.get('.trigger-input-range') cy.get(".trigger-input-range")
.invoke('val', 25) .invoke("val", 25)
.trigger('change') .trigger("change")
.get('input[type=range]').siblings('p') .get("input[type=range]")
.should('have.text', '25') .siblings("p")
}) .should("have.text", "25");
});
it('cy.scrollTo() - scroll the window or element to a position', () => { it("cy.scrollTo() - scroll the window or element to a position", () => {
// https://on.cypress.io/scrollto // https://on.cypress.io/scrollto
// You can scroll to 9 specific positions of an element: // You can scroll to 9 specific positions of an element:
// ----------------------------------- // -----------------------------------
// | topLeft top topRight | // | topLeft top topRight |
// | | // | |
// | | // | |
// | | // | |
// | left center right | // | left center right |
// | | // | |
// | | // | |
// | | // | |
// | bottomLeft bottom bottomRight | // | bottomLeft bottom bottomRight |
// ----------------------------------- // -----------------------------------
// if you chain .scrollTo() off of cy, we will // if you chain .scrollTo() off of cy, we will
// scroll the entire window // scroll the entire window
cy.scrollTo('bottom') cy.scrollTo("bottom");
cy.get('#scrollable-horizontal').scrollTo('right') cy.get("#scrollable-horizontal").scrollTo("right");
// or you can scroll to a specific coordinate: // or you can scroll to a specific coordinate:
// (x axis, y axis) in pixels // (x axis, y axis) in pixels
cy.get('#scrollable-vertical').scrollTo(250, 250) cy.get("#scrollable-vertical").scrollTo(250, 250);
// or you can scroll to a specific percentage // or you can scroll to a specific percentage
// of the (width, height) of the element // of the (width, height) of the element
cy.get('#scrollable-both').scrollTo('75%', '25%') cy.get("#scrollable-both").scrollTo("75%", "25%");
// control the easing of the scroll (default is 'swing') // control the easing of the scroll (default is 'swing')
cy.get('#scrollable-vertical').scrollTo('center', {easing: 'linear'}) cy.get("#scrollable-vertical").scrollTo("center", { easing: "linear" });
// control the duration of the scroll (in ms) // control the duration of the scroll (in ms)
cy.get('#scrollable-both').scrollTo('center', {duration: 2000}) cy.get("#scrollable-both").scrollTo("center", { duration: 2000 });
}) });
}) });

View File

@@ -1,39 +1,35 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Aliasing', () => { context("Aliasing", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/aliasing') cy.visit("https://example.cypress.io/commands/aliasing");
}) });
it('.as() - alias a DOM element for later use', () => { it(".as() - alias a DOM element for later use", () => {
// https://on.cypress.io/as // https://on.cypress.io/as
// Alias a DOM element for use later // Alias a DOM element for use later
// We don't have to traverse to the element // We don't have to traverse to the element
// later in our code, we reference it with @ // later in our code, we reference it with @
cy.get('.as-table').find('tbody>tr') cy.get(".as-table").find("tbody>tr").first().find("td").first().find("button").as("firstBtn");
.first().find('td').first()
.find('button').as('firstBtn')
// when we reference the alias, we place an // when we reference the alias, we place an
// @ in front of its name // @ in front of its name
cy.get('@firstBtn').click() cy.get("@firstBtn").click();
cy.get('@firstBtn') cy.get("@firstBtn").should("have.class", "btn-success").and("contain", "Changed");
.should('have.class', 'btn-success') });
.and('contain', 'Changed')
})
it('.as() - alias a route for later use', () => { it(".as() - alias a route for later use", () => {
// Alias the route to wait for its response // Alias the route to wait for its response
cy.intercept('GET', '**/comments/*').as('getComment') cy.intercept("GET", "**/comments/*").as("getComment");
// we have code that gets a comment when // we have code that gets a comment when
// the button is clicked in scripts.js // the button is clicked in scripts.js
cy.get('.network-btn').click() cy.get(".network-btn").click();
// https://on.cypress.io/wait // https://on.cypress.io/wait
cy.wait('@getComment').its('response.statusCode').should('eq', 200) cy.wait("@getComment").its("response.statusCode").should("eq", 200);
}) });
}) });

View File

@@ -1,177 +1,173 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Assertions', () => { context("Assertions", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/assertions') cy.visit("https://example.cypress.io/commands/assertions");
}) });
describe('Implicit Assertions', () => { describe("Implicit Assertions", () => {
it('.should() - make an assertion about the current subject', () => { it(".should() - make an assertion about the current subject", () => {
// https://on.cypress.io/should // https://on.cypress.io/should
cy.get('.assertion-table') cy.get(".assertion-table")
.find('tbody tr:last') .find("tbody tr:last")
.should('have.class', 'success') .should("have.class", "success")
.find('td') .find("td")
.first() .first()
// checking the text of the <td> element in various ways // checking the text of the <td> element in various ways
.should('have.text', 'Column content') .should("have.text", "Column content")
.should('contain', 'Column content') .should("contain", "Column content")
.should('have.html', 'Column content') .should("have.html", "Column content")
// chai-jquery uses "is()" to check if element matches selector // chai-jquery uses "is()" to check if element matches selector
.should('match', 'td') .should("match", "td")
// to match text content against a regular expression // to match text content against a regular expression
// first need to invoke jQuery method text() // first need to invoke jQuery method text()
// and then match using regular expression // and then match using regular expression
.invoke('text') .invoke("text")
.should('match', /column content/i) .should("match", /column content/i);
// a better way to check element's text content against a regular expression // a better way to check element's text content against a regular expression
// is to use "cy.contains" // is to use "cy.contains"
// https://on.cypress.io/contains // https://on.cypress.io/contains
cy.get('.assertion-table') cy.get(".assertion-table")
.find('tbody tr:last') .find("tbody tr:last")
// finds first <td> element with text content matching regular expression // finds first <td> element with text content matching regular expression
.contains('td', /column content/i) .contains("td", /column content/i)
.should('be.visible') .should("be.visible");
// for more information about asserting element's text // for more information about asserting element's text
// see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-elements-text-contents // see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-elements-text-contents
});
it(".and() - chain multiple assertions together", () => {
// https://on.cypress.io/and
cy.get(".assertions-link").should("have.class", "active").and("have.attr", "href").and("include", "cypress.io");
});
});
describe("Explicit Assertions", () => {
// https://on.cypress.io/assertions
it("expect - make an assertion about a specified subject", () => {
// We can use Chai's BDD style assertions
expect(true).to.be.true;
const o = { foo: "bar" };
expect(o).to.equal(o);
expect(o).to.deep.equal({ foo: "bar" });
// matching text using regular expression
expect("FooBar").to.match(/bar$/i);
});
it("pass your own callback function to should()", () => {
// Pass a function to should that can have any number
// of explicit assertions within it.
// The ".should(cb)" function will be retried
// automatically until it passes all your explicit assertions or times out.
cy.get(".assertions-p")
.find("p")
.should(($p) => {
// https://on.cypress.io/$
// return an array of texts from all of the p's
// @ts-ignore TS6133 unused variable
const texts = $p.map((i, el) => Cypress.$(el).text());
// jquery map returns jquery object
// and .get() convert this to simple array
const paragraphs = texts.get();
// array should have length of 3
expect(paragraphs, "has 3 paragraphs").to.have.length(3);
// use second argument to expect(...) to provide clear
// message with each assertion
expect(paragraphs, "has expected text in each paragraph").to.deep.eq([
"Some text from first p",
"More text from second p",
"And even more text from third p"
]);
});
});
it("finds element by class name regex", () => {
cy.get(".docs-header")
.find("div")
// .should(cb) callback function will be retried
.should(($div) => {
expect($div).to.have.length(1);
const className = $div[0].className;
expect(className).to.match(/heading-/);
}) })
// .then(cb) callback is not retried,
// it either passes or fails
.then(($div) => {
expect($div, "text content").to.have.text("Introduction");
});
});
it('.and() - chain multiple assertions together', () => { it("can throw any error", () => {
// https://on.cypress.io/and cy.get(".docs-header")
cy.get('.assertions-link') .find("div")
.should('have.class', 'active') .should(($div) => {
.and('have.attr', 'href') if ($div.length !== 1) {
.and('include', 'cypress.io') // you can throw your own errors
}) throw new Error("Did not find 1 element");
}) }
describe('Explicit Assertions', () => { const className = $div[0].className;
// https://on.cypress.io/assertions
it('expect - make an assertion about a specified subject', () => {
// We can use Chai's BDD style assertions
expect(true).to.be.true
const o = {foo: 'bar'}
expect(o).to.equal(o) if (!className.match(/heading-/)) {
expect(o).to.deep.equal({foo: 'bar'}) throw new Error(`Could not find class "heading-" in ${className}`);
// matching text using regular expression }
expect('FooBar').to.match(/bar$/i) });
}) });
it('pass your own callback function to should()', () => { it("matches unknown text between two elements", () => {
// Pass a function to should that can have any number /**
// of explicit assertions within it. * Text from the first element.
// The ".should(cb)" function will be retried * @type {string}
// automatically until it passes all your explicit assertions or times out. */
cy.get('.assertions-p') let text;
.find('p')
.should(($p) => {
// https://on.cypress.io/$
// return an array of texts from all of the p's
// @ts-ignore TS6133 unused variable
const texts = $p.map((i, el) => Cypress.$(el).text())
// jquery map returns jquery object /**
// and .get() convert this to simple array * Normalizes passed text,
const paragraphs = texts.get() * useful before comparing text with spaces and different capitalization.
* @param {string} s Text to normalize
*/
const normalizeText = (s) => s.replace(/\s/g, "").toLowerCase();
// array should have length of 3 cy.get(".two-elements")
expect(paragraphs, 'has 3 paragraphs').to.have.length(3) .find(".first")
.then(($first) => {
// save text from the first element
text = normalizeText($first.text());
});
// use second argument to expect(...) to provide clear cy.get(".two-elements")
// message with each assertion .find(".second")
expect(paragraphs, 'has expected text in each paragraph').to.deep.eq([ .should(($div) => {
'Some text from first p', // we can massage text before comparing
'More text from second p', const secondText = normalizeText($div.text());
'And even more text from third p',
])
})
})
it('finds element by class name regex', () => { expect(secondText, "second text").to.equal(text);
cy.get('.docs-header') });
.find('div') });
// .should(cb) callback function will be retried
.should(($div) => {
expect($div).to.have.length(1)
const className = $div[0].className it("assert - assert shape of an object", () => {
const person = {
name: "Joe",
age: 20
};
expect(className).to.match(/heading-/) assert.isObject(person, "value is object");
}) });
// .then(cb) callback is not retried,
// it either passes or fails
.then(($div) => {
expect($div, 'text content').to.have.text('Introduction')
})
})
it('can throw any error', () => { it("retries the should callback until assertions pass", () => {
cy.get('.docs-header') cy.get("#random-number").should(($div) => {
.find('div') const n = parseFloat($div.text());
.should(($div) => {
if ($div.length !== 1) {
// you can throw your own errors
throw new Error('Did not find 1 element')
}
const className = $div[0].className expect(n).to.be.gte(1).and.be.lte(10);
});
if (!className.match(/heading-/)) { });
throw new Error(`Could not find class "heading-" in ${className}`) });
} });
})
})
it('matches unknown text between two elements', () => {
/**
* Text from the first element.
* @type {string}
*/
let text
/**
* Normalizes passed text,
* useful before comparing text with spaces and different capitalization.
* @param {string} s Text to normalize
*/
const normalizeText = (s) => s.replace(/\s/g, '').toLowerCase()
cy.get('.two-elements')
.find('.first')
.then(($first) => {
// save text from the first element
text = normalizeText($first.text())
})
cy.get('.two-elements')
.find('.second')
.should(($div) => {
// we can massage text before comparing
const secondText = normalizeText($div.text())
expect(secondText, 'second text').to.equal(text)
})
})
it('assert - assert shape of an object', () => {
const person = {
name: 'Joe',
age: 20,
}
assert.isObject(person, 'value is object')
})
it('retries the should callback until assertions pass', () => {
cy.get('#random-number')
.should(($div) => {
const n = parseFloat($div.text())
expect(n).to.be.gte(1).and.be.lte(10)
})
})
})
})

View File

@@ -1,97 +1,96 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Connectors', () => { context("Connectors", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/connectors') cy.visit("https://example.cypress.io/commands/connectors");
}) });
it('.each() - iterate over an array of elements', () => { it(".each() - iterate over an array of elements", () => {
// https://on.cypress.io/each // https://on.cypress.io/each
cy.get('.connectors-each-ul>li') cy.get(".connectors-each-ul>li").each(($el, index, $list) => {
.each(($el, index, $list) => { console.log($el, index, $list);
console.log($el, index, $list) });
}) });
})
it('.its() - get properties on the current subject', () => { it(".its() - get properties on the current subject", () => {
// https://on.cypress.io/its // https://on.cypress.io/its
cy.get('.connectors-its-ul>li') cy.get(".connectors-its-ul>li")
// calls the 'length' property yielding that value // calls the 'length' property yielding that value
.its('length') .its("length")
.should('be.gt', 2) .should("be.gt", 2);
}) });
it('.invoke() - invoke a function on the current subject', () => { it(".invoke() - invoke a function on the current subject", () => {
// our div is hidden in our script.js // our div is hidden in our script.js
// $('.connectors-div').hide() // $('.connectors-div').hide()
// https://on.cypress.io/invoke // https://on.cypress.io/invoke
cy.get('.connectors-div').should('be.hidden') cy.get(".connectors-div")
// call the jquery method 'show' on the 'div.container' .should("be.hidden")
.invoke('show') // call the jquery method 'show' on the 'div.container'
.should('be.visible') .invoke("show")
}) .should("be.visible");
});
it('.spread() - spread an array as individual args to callback function', () => { it(".spread() - spread an array as individual args to callback function", () => {
// https://on.cypress.io/spread // https://on.cypress.io/spread
const arr = ['foo', 'bar', 'baz'] const arr = ["foo", "bar", "baz"];
cy.wrap(arr).spread((foo, bar, baz) => { cy.wrap(arr).spread((foo, bar, baz) => {
expect(foo).to.eq('foo') expect(foo).to.eq("foo");
expect(bar).to.eq('bar') expect(bar).to.eq("bar");
expect(baz).to.eq('baz') expect(baz).to.eq("baz");
});
});
describe(".then()", () => {
it("invokes a callback function with the current subject", () => {
// https://on.cypress.io/then
cy.get(".connectors-list > li").then(($lis) => {
expect($lis, "3 items").to.have.length(3);
expect($lis.eq(0), "first item").to.contain("Walk the dog");
expect($lis.eq(1), "second item").to.contain("Feed the cat");
expect($lis.eq(2), "third item").to.contain("Write JavaScript");
});
});
it("yields the returned value to the next command", () => {
cy.wrap(1)
.then((num) => {
expect(num).to.equal(1);
return 2;
}) })
}) .then((num) => {
expect(num).to.equal(2);
});
});
describe('.then()', () => { it("yields the original subject without return", () => {
it('invokes a callback function with the current subject', () => { cy.wrap(1)
// https://on.cypress.io/then .then((num) => {
cy.get('.connectors-list > li') expect(num).to.equal(1);
.then(($lis) => { // note that nothing is returned from this callback
expect($lis, '3 items').to.have.length(3)
expect($lis.eq(0), 'first item').to.contain('Walk the dog')
expect($lis.eq(1), 'second item').to.contain('Feed the cat')
expect($lis.eq(2), 'third item').to.contain('Write JavaScript')
})
}) })
.then((num) => {
// this callback receives the original unchanged value 1
expect(num).to.equal(1);
});
});
it('yields the returned value to the next command', () => { it("yields the value yielded by the last Cypress command inside", () => {
cy.wrap(1) cy.wrap(1)
.then((num) => { .then((num) => {
expect(num).to.equal(1) expect(num).to.equal(1);
// note how we run a Cypress command
return 2 // the result yielded by this Cypress command
}) // will be passed to the second ".then"
.then((num) => { cy.wrap(2);
expect(num).to.equal(2)
})
}) })
.then((num) => {
it('yields the original subject without return', () => { // this callback receives the value yielded by "cy.wrap(2)"
cy.wrap(1) expect(num).to.equal(2);
.then((num) => { });
expect(num).to.equal(1) });
// note that nothing is returned from this callback });
}) });
.then((num) => {
// this callback receives the original unchanged value 1
expect(num).to.equal(1)
})
})
it('yields the value yielded by the last Cypress command inside', () => {
cy.wrap(1)
.then((num) => {
expect(num).to.equal(1)
// note how we run a Cypress command
// the result yielded by this Cypress command
// will be passed to the second ".then"
cy.wrap(2)
})
.then((num) => {
// this callback receives the value yielded by "cy.wrap(2)"
expect(num).to.equal(2)
})
})
})
})

View File

@@ -1,77 +1,79 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Cookies', () => { context("Cookies", () => {
beforeEach(() => { beforeEach(() => {
Cypress.Cookies.debug(true) Cypress.Cookies.debug(true);
cy.visit('https://example.cypress.io/commands/cookies') cy.visit("https://example.cypress.io/commands/cookies");
// clear cookies again after visiting to remove // clear cookies again after visiting to remove
// any 3rd party cookies picked up such as cloudflare // any 3rd party cookies picked up such as cloudflare
cy.clearCookies() cy.clearCookies();
}) });
it('cy.getCookie() - get a browser cookie', () => { it("cy.getCookie() - get a browser cookie", () => {
// https://on.cypress.io/getcookie // https://on.cypress.io/getcookie
cy.get('#getCookie .set-a-cookie').click() cy.get("#getCookie .set-a-cookie").click();
// cy.getCookie() yields a cookie object // cy.getCookie() yields a cookie object
cy.getCookie('token').should('have.property', 'value', '123ABC') cy.getCookie("token").should("have.property", "value", "123ABC");
}) });
it('cy.getCookies() - get browser cookies', () => { it("cy.getCookies() - get browser cookies", () => {
// https://on.cypress.io/getcookies // https://on.cypress.io/getcookies
cy.getCookies().should('be.empty') cy.getCookies().should("be.empty");
cy.get('#getCookies .set-a-cookie').click() cy.get("#getCookies .set-a-cookie").click();
// cy.getCookies() yields an array of cookies // cy.getCookies() yields an array of cookies
cy.getCookies().should('have.length', 1).should((cookies) => { cy.getCookies()
// each cookie has these properties .should("have.length", 1)
expect(cookies[0]).to.have.property('name', 'token') .should((cookies) => {
expect(cookies[0]).to.have.property('value', '123ABC') // each cookie has these properties
expect(cookies[0]).to.have.property('httpOnly', false) expect(cookies[0]).to.have.property("name", "token");
expect(cookies[0]).to.have.property('secure', false) expect(cookies[0]).to.have.property("value", "123ABC");
expect(cookies[0]).to.have.property('domain') expect(cookies[0]).to.have.property("httpOnly", false);
expect(cookies[0]).to.have.property('path') expect(cookies[0]).to.have.property("secure", false);
}) expect(cookies[0]).to.have.property("domain");
}) expect(cookies[0]).to.have.property("path");
});
});
it('cy.setCookie() - set a browser cookie', () => { it("cy.setCookie() - set a browser cookie", () => {
// https://on.cypress.io/setcookie // https://on.cypress.io/setcookie
cy.getCookies().should('be.empty') cy.getCookies().should("be.empty");
cy.setCookie('foo', 'bar') cy.setCookie("foo", "bar");
// cy.getCookie() yields a cookie object // cy.getCookie() yields a cookie object
cy.getCookie('foo').should('have.property', 'value', 'bar') cy.getCookie("foo").should("have.property", "value", "bar");
}) });
it('cy.clearCookie() - clear a browser cookie', () => { it("cy.clearCookie() - clear a browser cookie", () => {
// https://on.cypress.io/clearcookie // https://on.cypress.io/clearcookie
cy.getCookie('token').should('be.null') cy.getCookie("token").should("be.null");
cy.get('#clearCookie .set-a-cookie').click() cy.get("#clearCookie .set-a-cookie").click();
cy.getCookie('token').should('have.property', 'value', '123ABC') cy.getCookie("token").should("have.property", "value", "123ABC");
// cy.clearCookies() yields null // cy.clearCookies() yields null
cy.clearCookie('token').should('be.null') cy.clearCookie("token").should("be.null");
cy.getCookie('token').should('be.null') cy.getCookie("token").should("be.null");
}) });
it('cy.clearCookies() - clear browser cookies', () => { it("cy.clearCookies() - clear browser cookies", () => {
// https://on.cypress.io/clearcookies // https://on.cypress.io/clearcookies
cy.getCookies().should('be.empty') cy.getCookies().should("be.empty");
cy.get('#clearCookies .set-a-cookie').click() cy.get("#clearCookies .set-a-cookie").click();
cy.getCookies().should('have.length', 1) cy.getCookies().should("have.length", 1);
// cy.clearCookies() yields null // cy.clearCookies() yields null
cy.clearCookies() cy.clearCookies();
cy.getCookies().should('be.empty') cy.getCookies().should("be.empty");
}) });
}) });

View File

@@ -1,202 +1,208 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Cypress.Commands', () => { context("Cypress.Commands", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api') cy.visit("https://example.cypress.io/cypress-api");
}) });
// https://on.cypress.io/custom-commands // https://on.cypress.io/custom-commands
it('.add() - create a custom command', () => { it(".add() - create a custom command", () => {
Cypress.Commands.add('console', { Cypress.Commands.add(
prevSubject: true, "console",
}, (subject, method) => { {
// the previous subject is automatically received prevSubject: true
// and the commands arguments are shifted },
(subject, method) => {
// the previous subject is automatically received
// and the commands arguments are shifted
// allow us to change the console method used // allow us to change the console method used
method = method || 'log' method = method || "log";
// log the subject to the console // log the subject to the console
// @ts-ignore TS7017 // @ts-ignore TS7017
console[method]('The subject is', subject) console[method]("The subject is", subject);
// whatever we return becomes the new subject // whatever we return becomes the new subject
// we don't want to change the subject so // we don't want to change the subject so
// we return whatever was passed in // we return whatever was passed in
return subject return subject;
}) }
);
// @ts-ignore TS2339 // @ts-ignore TS2339
cy.get('button').console('info').then(($button) => { cy.get("button")
// subject is still $button .console("info")
}) .then(($button) => {
}) // subject is still $button
}) });
});
});
context('Cypress.Cookies', () => { context("Cypress.Cookies", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api') cy.visit("https://example.cypress.io/cypress-api");
}) });
// https://on.cypress.io/cookies // https://on.cypress.io/cookies
it('.debug() - enable or disable debugging', () => { it(".debug() - enable or disable debugging", () => {
Cypress.Cookies.debug(true) Cypress.Cookies.debug(true);
// Cypress will now log in the console when // Cypress will now log in the console when
// cookies are set or cleared // cookies are set or cleared
cy.setCookie('fakeCookie', '123ABC') cy.setCookie("fakeCookie", "123ABC");
cy.clearCookie('fakeCookie') cy.clearCookie("fakeCookie");
cy.setCookie('fakeCookie', '123ABC') cy.setCookie("fakeCookie", "123ABC");
cy.clearCookie('fakeCookie') cy.clearCookie("fakeCookie");
cy.setCookie('fakeCookie', '123ABC') cy.setCookie("fakeCookie", "123ABC");
}) });
it('.preserveOnce() - preserve cookies by key', () => { it(".preserveOnce() - preserve cookies by key", () => {
// normally cookies are reset after each test // normally cookies are reset after each test
cy.getCookie('fakeCookie').should('not.be.ok') cy.getCookie("fakeCookie").should("not.be.ok");
// preserving a cookie will not clear it when // preserving a cookie will not clear it when
// the next test starts // the next test starts
cy.setCookie('lastCookie', '789XYZ') cy.setCookie("lastCookie", "789XYZ");
Cypress.Cookies.preserveOnce('lastCookie') Cypress.Cookies.preserveOnce("lastCookie");
}) });
it('.defaults() - set defaults for all cookies', () => { it(".defaults() - set defaults for all cookies", () => {
// now any cookie with the name 'session_id' will // now any cookie with the name 'session_id' will
// not be cleared before each new test runs // not be cleared before each new test runs
Cypress.Cookies.defaults({ Cypress.Cookies.defaults({
preserve: 'session_id', preserve: "session_id"
}) });
}) });
}) });
context('Cypress.arch', () => { context("Cypress.arch", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api') cy.visit("https://example.cypress.io/cypress-api");
}) });
it('Get CPU architecture name of underlying OS', () => { it("Get CPU architecture name of underlying OS", () => {
// https://on.cypress.io/arch // https://on.cypress.io/arch
expect(Cypress.arch).to.exist expect(Cypress.arch).to.exist;
}) });
}) });
context('Cypress.config()', () => { context("Cypress.config()", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api') cy.visit("https://example.cypress.io/cypress-api");
}) });
it('Get and set configuration options', () => { it("Get and set configuration options", () => {
// https://on.cypress.io/config // https://on.cypress.io/config
let myConfig = Cypress.config() let myConfig = Cypress.config();
expect(myConfig).to.have.property('animationDistanceThreshold', 5) expect(myConfig).to.have.property("animationDistanceThreshold", 5);
expect(myConfig).to.have.property('baseUrl', null) expect(myConfig).to.have.property("baseUrl", null);
expect(myConfig).to.have.property('defaultCommandTimeout', 4000) expect(myConfig).to.have.property("defaultCommandTimeout", 4000);
expect(myConfig).to.have.property('requestTimeout', 5000) expect(myConfig).to.have.property("requestTimeout", 5000);
expect(myConfig).to.have.property('responseTimeout', 30000) expect(myConfig).to.have.property("responseTimeout", 30000);
expect(myConfig).to.have.property('viewportHeight', 660) expect(myConfig).to.have.property("viewportHeight", 660);
expect(myConfig).to.have.property('viewportWidth', 1000) expect(myConfig).to.have.property("viewportWidth", 1000);
expect(myConfig).to.have.property('pageLoadTimeout', 60000) expect(myConfig).to.have.property("pageLoadTimeout", 60000);
expect(myConfig).to.have.property('waitForAnimations', true) expect(myConfig).to.have.property("waitForAnimations", true);
expect(Cypress.config('pageLoadTimeout')).to.eq(60000) expect(Cypress.config("pageLoadTimeout")).to.eq(60000);
// this will change the config for the rest of your tests! // this will change the config for the rest of your tests!
Cypress.config('pageLoadTimeout', 20000) Cypress.config("pageLoadTimeout", 20000);
expect(Cypress.config('pageLoadTimeout')).to.eq(20000) expect(Cypress.config("pageLoadTimeout")).to.eq(20000);
Cypress.config('pageLoadTimeout', 60000) Cypress.config("pageLoadTimeout", 60000);
}) });
}) });
context('Cypress.dom', () => { context("Cypress.dom", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api') cy.visit("https://example.cypress.io/cypress-api");
}) });
// https://on.cypress.io/dom // https://on.cypress.io/dom
it('.isHidden() - determine if a DOM element is hidden', () => { it(".isHidden() - determine if a DOM element is hidden", () => {
let hiddenP = Cypress.$('.dom-p p.hidden').get(0) let hiddenP = Cypress.$(".dom-p p.hidden").get(0);
let visibleP = Cypress.$('.dom-p p.visible').get(0) let visibleP = Cypress.$(".dom-p p.visible").get(0);
// our first paragraph has css class 'hidden' // our first paragraph has css class 'hidden'
expect(Cypress.dom.isHidden(hiddenP)).to.be.true expect(Cypress.dom.isHidden(hiddenP)).to.be.true;
expect(Cypress.dom.isHidden(visibleP)).to.be.false expect(Cypress.dom.isHidden(visibleP)).to.be.false;
}) });
}) });
context('Cypress.env()', () => { context("Cypress.env()", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api') cy.visit("https://example.cypress.io/cypress-api");
}) });
// We can set environment variables for highly dynamic values // We can set environment variables for highly dynamic values
// https://on.cypress.io/environment-variables // https://on.cypress.io/environment-variables
it('Get environment variables', () => { it("Get environment variables", () => {
// https://on.cypress.io/env // https://on.cypress.io/env
// set multiple environment variables // set multiple environment variables
Cypress.env({ Cypress.env({
host: 'veronica.dev.local', host: "veronica.dev.local",
api_server: 'http://localhost:8888/v1/', api_server: "http://localhost:8888/v1/"
}) });
// get environment variable // get environment variable
expect(Cypress.env('host')).to.eq('veronica.dev.local') expect(Cypress.env("host")).to.eq("veronica.dev.local");
// set environment variable // set environment variable
Cypress.env('api_server', 'http://localhost:8888/v2/') Cypress.env("api_server", "http://localhost:8888/v2/");
expect(Cypress.env('api_server')).to.eq('http://localhost:8888/v2/') expect(Cypress.env("api_server")).to.eq("http://localhost:8888/v2/");
// get all environment variable // get all environment variable
expect(Cypress.env()).to.have.property('host', 'veronica.dev.local') expect(Cypress.env()).to.have.property("host", "veronica.dev.local");
expect(Cypress.env()).to.have.property('api_server', 'http://localhost:8888/v2/') expect(Cypress.env()).to.have.property("api_server", "http://localhost:8888/v2/");
}) });
}) });
context('Cypress.log', () => { context("Cypress.log", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api') cy.visit("https://example.cypress.io/cypress-api");
}) });
it('Control what is printed to the Command Log', () => { it("Control what is printed to the Command Log", () => {
// https://on.cypress.io/cypress-log // https://on.cypress.io/cypress-log
}) });
}) });
context('Cypress.platform', () => { context("Cypress.platform", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api') cy.visit("https://example.cypress.io/cypress-api");
}) });
it('Get underlying OS name', () => { it("Get underlying OS name", () => {
// https://on.cypress.io/platform // https://on.cypress.io/platform
expect(Cypress.platform).to.be.exist expect(Cypress.platform).to.be.exist;
}) });
}) });
context('Cypress.version', () => { context("Cypress.version", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api') cy.visit("https://example.cypress.io/cypress-api");
}) });
it('Get current version of Cypress being run', () => { it("Get current version of Cypress being run", () => {
// https://on.cypress.io/version // https://on.cypress.io/version
expect(Cypress.version).to.be.exist expect(Cypress.version).to.be.exist;
}) });
}) });
context('Cypress.spec', () => { context("Cypress.spec", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api') cy.visit("https://example.cypress.io/cypress-api");
}) });
it('Get current spec information', () => { it("Get current spec information", () => {
// https://on.cypress.io/spec // https://on.cypress.io/spec
// wrap the object so we can inspect it easily by clicking in the command log // wrap the object so we can inspect it easily by clicking in the command log
cy.wrap(Cypress.spec).should('include.keys', ['name', 'relative', 'absolute']) cy.wrap(Cypress.spec).should("include.keys", ["name", "relative", "absolute"]);
}) });
}) });

View File

@@ -3,86 +3,84 @@
/// JSON fixture file can be loaded directly using /// JSON fixture file can be loaded directly using
// the built-in JavaScript bundler // the built-in JavaScript bundler
// @ts-ignore // @ts-ignore
const requiredExample = require('../../fixtures/example') const requiredExample = require("../../fixtures/example");
context('Files', () => { context("Files", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/files') cy.visit("https://example.cypress.io/commands/files");
}) });
beforeEach(() => { beforeEach(() => {
// load example.json fixture file and store // load example.json fixture file and store
// in the test context object // in the test context object
cy.fixture('example.json').as('example') cy.fixture("example.json").as("example");
}) });
it('cy.fixture() - load a fixture', () => { it("cy.fixture() - load a fixture", () => {
// https://on.cypress.io/fixture // https://on.cypress.io/fixture
// Instead of writing a response inline you can // Instead of writing a response inline you can
// use a fixture file's content. // use a fixture file's content.
// when application makes an Ajax request matching "GET **/comments/*" // when application makes an Ajax request matching "GET **/comments/*"
// Cypress will intercept it and reply with the object in `example.json` fixture // Cypress will intercept it and reply with the object in `example.json` fixture
cy.intercept('GET', '**/comments/*', {fixture: 'example.json'}).as('getComment') cy.intercept("GET", "**/comments/*", { fixture: "example.json" }).as("getComment");
// we have code that gets a comment when // we have code that gets a comment when
// the button is clicked in scripts.js // the button is clicked in scripts.js
cy.get('.fixture-btn').click() cy.get(".fixture-btn").click();
cy.wait('@getComment').its('response.body') cy.wait("@getComment")
.should('have.property', 'name') .its("response.body")
.and('include', 'Using fixtures to represent data') .should("have.property", "name")
}) .and("include", "Using fixtures to represent data");
});
it('cy.fixture() or require - load a fixture', function () { it("cy.fixture() or require - load a fixture", function () {
// we are inside the "function () { ... }" // we are inside the "function () { ... }"
// callback and can use test context object "this" // callback and can use test context object "this"
// "this.example" was loaded in "beforeEach" function callback // "this.example" was loaded in "beforeEach" function callback
expect(this.example, 'fixture in the test context') expect(this.example, "fixture in the test context").to.deep.equal(requiredExample);
.to.deep.equal(requiredExample)
// or use "cy.wrap" and "should('deep.equal', ...)" assertion // or use "cy.wrap" and "should('deep.equal', ...)" assertion
cy.wrap(this.example) cy.wrap(this.example).should("deep.equal", requiredExample);
.should('deep.equal', requiredExample) });
})
it('cy.readFile() - read file contents', () => { it("cy.readFile() - read file contents", () => {
// https://on.cypress.io/readfile // https://on.cypress.io/readfile
// You can read a file and yield its contents // You can read a file and yield its contents
// The filePath is relative to your project's root. // The filePath is relative to your project's root.
cy.readFile('cypress.json').then((json) => { cy.readFile("cypress.json").then((json) => {
expect(json).to.be.an('object') expect(json).to.be.an("object");
}) });
}) });
it('cy.writeFile() - write to a file', () => { it("cy.writeFile() - write to a file", () => {
// https://on.cypress.io/writefile // https://on.cypress.io/writefile
// You can write to a file // You can write to a file
// Use a response from a request to automatically // Use a response from a request to automatically
// generate a fixture file for use later // generate a fixture file for use later
cy.request('https://jsonplaceholder.cypress.io/users') cy.request("https://jsonplaceholder.cypress.io/users").then((response) => {
.then((response) => { cy.writeFile("cypress/fixtures/users.json", response.body);
cy.writeFile('cypress/fixtures/users.json', response.body) });
})
cy.fixture('users').should((users) => { cy.fixture("users").should((users) => {
expect(users[0].name).to.exist expect(users[0].name).to.exist;
}) });
// JavaScript arrays and objects are stringified // JavaScript arrays and objects are stringified
// and formatted into text. // and formatted into text.
cy.writeFile('cypress/fixtures/profile.json', { cy.writeFile("cypress/fixtures/profile.json", {
id: 8739, id: 8739,
name: 'Jane', name: "Jane",
email: 'jane@example.com', email: "jane@example.com"
}) });
cy.fixture('profile').should((profile) => { cy.fixture("profile").should((profile) => {
expect(profile.name).to.eq('Jane') expect(profile.name).to.eq("Jane");
}) });
}) });
}) });

View File

@@ -1,52 +1,58 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Local Storage', () => { context("Local Storage", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/local-storage') cy.visit("https://example.cypress.io/commands/local-storage");
}) });
// Although local storage is automatically cleared // Although local storage is automatically cleared
// in between tests to maintain a clean state // in between tests to maintain a clean state
// sometimes we need to clear the local storage manually // sometimes we need to clear the local storage manually
it('cy.clearLocalStorage() - clear all data in local storage', () => { it("cy.clearLocalStorage() - clear all data in local storage", () => {
// https://on.cypress.io/clearlocalstorage // https://on.cypress.io/clearlocalstorage
cy.get('.ls-btn').click().should(() => { cy.get(".ls-btn")
expect(localStorage.getItem('prop1')).to.eq('red') .click()
expect(localStorage.getItem('prop2')).to.eq('blue') .should(() => {
expect(localStorage.getItem('prop3')).to.eq('magenta') expect(localStorage.getItem("prop1")).to.eq("red");
}) expect(localStorage.getItem("prop2")).to.eq("blue");
expect(localStorage.getItem("prop3")).to.eq("magenta");
});
// clearLocalStorage() yields the localStorage object // clearLocalStorage() yields the localStorage object
cy.clearLocalStorage().should((ls) => { cy.clearLocalStorage().should((ls) => {
expect(ls.getItem('prop1')).to.be.null expect(ls.getItem("prop1")).to.be.null;
expect(ls.getItem('prop2')).to.be.null expect(ls.getItem("prop2")).to.be.null;
expect(ls.getItem('prop3')).to.be.null expect(ls.getItem("prop3")).to.be.null;
}) });
cy.get('.ls-btn').click().should(() => { cy.get(".ls-btn")
expect(localStorage.getItem('prop1')).to.eq('red') .click()
expect(localStorage.getItem('prop2')).to.eq('blue') .should(() => {
expect(localStorage.getItem('prop3')).to.eq('magenta') expect(localStorage.getItem("prop1")).to.eq("red");
}) expect(localStorage.getItem("prop2")).to.eq("blue");
expect(localStorage.getItem("prop3")).to.eq("magenta");
});
// Clear key matching string in Local Storage // Clear key matching string in Local Storage
cy.clearLocalStorage('prop1').should((ls) => { cy.clearLocalStorage("prop1").should((ls) => {
expect(ls.getItem('prop1')).to.be.null expect(ls.getItem("prop1")).to.be.null;
expect(ls.getItem('prop2')).to.eq('blue') expect(ls.getItem("prop2")).to.eq("blue");
expect(ls.getItem('prop3')).to.eq('magenta') expect(ls.getItem("prop3")).to.eq("magenta");
}) });
cy.get('.ls-btn').click().should(() => { cy.get(".ls-btn")
expect(localStorage.getItem('prop1')).to.eq('red') .click()
expect(localStorage.getItem('prop2')).to.eq('blue') .should(() => {
expect(localStorage.getItem('prop3')).to.eq('magenta') expect(localStorage.getItem("prop1")).to.eq("red");
}) expect(localStorage.getItem("prop2")).to.eq("blue");
expect(localStorage.getItem("prop3")).to.eq("magenta");
});
// Clear keys matching regex in Local Storage // Clear keys matching regex in Local Storage
cy.clearLocalStorage(/prop1|2/).should((ls) => { cy.clearLocalStorage(/prop1|2/).should((ls) => {
expect(ls.getItem('prop1')).to.be.null expect(ls.getItem("prop1")).to.be.null;
expect(ls.getItem('prop2')).to.be.null expect(ls.getItem("prop2")).to.be.null;
expect(ls.getItem('prop3')).to.eq('magenta') expect(ls.getItem("prop3")).to.eq("magenta");
}) });
}) });
}) });

View File

@@ -1,32 +1,32 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Location', () => { context("Location", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/location') cy.visit("https://example.cypress.io/commands/location");
}) });
it('cy.hash() - get the current URL hash', () => { it("cy.hash() - get the current URL hash", () => {
// https://on.cypress.io/hash // https://on.cypress.io/hash
cy.hash().should('be.empty') cy.hash().should("be.empty");
}) });
it('cy.location() - get window.location', () => { it("cy.location() - get window.location", () => {
// https://on.cypress.io/location // https://on.cypress.io/location
cy.location().should((location) => { cy.location().should((location) => {
expect(location.hash).to.be.empty expect(location.hash).to.be.empty;
expect(location.href).to.eq('https://example.cypress.io/commands/location') expect(location.href).to.eq("https://example.cypress.io/commands/location");
expect(location.host).to.eq('example.cypress.io') expect(location.host).to.eq("example.cypress.io");
expect(location.hostname).to.eq('example.cypress.io') expect(location.hostname).to.eq("example.cypress.io");
expect(location.origin).to.eq('https://example.cypress.io') expect(location.origin).to.eq("https://example.cypress.io");
expect(location.pathname).to.eq('/commands/location') expect(location.pathname).to.eq("/commands/location");
expect(location.port).to.eq('') expect(location.port).to.eq("");
expect(location.protocol).to.eq('https:') expect(location.protocol).to.eq("https:");
expect(location.search).to.be.empty expect(location.search).to.be.empty;
}) });
}) });
it('cy.url() - get the current URL', () => { it("cy.url() - get the current URL", () => {
// https://on.cypress.io/url // https://on.cypress.io/url
cy.url().should('eq', 'https://example.cypress.io/commands/location') cy.url().should("eq", "https://example.cypress.io/commands/location");
}) });
}) });

View File

@@ -1,106 +1,98 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Misc', () => { context("Misc", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/misc') cy.visit("https://example.cypress.io/commands/misc");
}) });
it('.end() - end the command chain', () => { it(".end() - end the command chain", () => {
// https://on.cypress.io/end // https://on.cypress.io/end
// cy.end is useful when you want to end a chain of commands // cy.end is useful when you want to end a chain of commands
// and force Cypress to re-query from the root element // and force Cypress to re-query from the root element
cy.get('.misc-table').within(() => { cy.get(".misc-table").within(() => {
// ends the current chain and yields null // ends the current chain and yields null
cy.contains('Cheryl').click().end() cy.contains("Cheryl").click().end();
// queries the entire table again // queries the entire table again
cy.contains('Charles').click() cy.contains("Charles").click();
}) });
}) });
it('cy.exec() - execute a system command', () => { it("cy.exec() - execute a system command", () => {
// execute a system command. // execute a system command.
// so you can take actions necessary for // so you can take actions necessary for
// your test outside the scope of Cypress. // your test outside the scope of Cypress.
// https://on.cypress.io/exec // https://on.cypress.io/exec
// we can use Cypress.platform string to // we can use Cypress.platform string to
// select appropriate command // select appropriate command
// https://on.cypress/io/platform // https://on.cypress/io/platform
cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`) cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`);
// on CircleCI Windows build machines we have a failure to run bash shell // on CircleCI Windows build machines we have a failure to run bash shell
// https://github.com/cypress-io/cypress/issues/5169 // https://github.com/cypress-io/cypress/issues/5169
// so skip some of the tests by passing flag "--env circle=true" // so skip some of the tests by passing flag "--env circle=true"
const isCircleOnWindows = Cypress.platform === 'win32' && Cypress.env('circle') const isCircleOnWindows = Cypress.platform === "win32" && Cypress.env("circle");
if (isCircleOnWindows) { if (isCircleOnWindows) {
cy.log('Skipping test on CircleCI') cy.log("Skipping test on CircleCI");
return return;
} }
// cy.exec problem on Shippable CI // cy.exec problem on Shippable CI
// https://github.com/cypress-io/cypress/issues/6718 // https://github.com/cypress-io/cypress/issues/6718
const isShippable = Cypress.platform === 'linux' && Cypress.env('shippable') const isShippable = Cypress.platform === "linux" && Cypress.env("shippable");
if (isShippable) { if (isShippable) {
cy.log('Skipping test on ShippableCI') cy.log("Skipping test on ShippableCI");
return return;
} }
cy.exec('echo Jane Lane') cy.exec("echo Jane Lane").its("stdout").should("contain", "Jane Lane");
.its('stdout').should('contain', 'Jane Lane')
if (Cypress.platform === 'win32') { if (Cypress.platform === "win32") {
cy.exec('print cypress.json') cy.exec("print cypress.json").its("stderr").should("be.empty");
.its('stderr').should('be.empty') } else {
} else { cy.exec("cat cypress.json").its("stderr").should("be.empty");
cy.exec('cat cypress.json')
.its('stderr').should('be.empty')
cy.exec('pwd') cy.exec("pwd").its("code").should("eq", 0);
.its('code').should('eq', 0) }
} });
})
it('cy.focused() - get the DOM element that has focus', () => { it("cy.focused() - get the DOM element that has focus", () => {
// https://on.cypress.io/focused // https://on.cypress.io/focused
cy.get('.misc-form').find('#name').click() cy.get(".misc-form").find("#name").click();
cy.focused().should('have.id', 'name') cy.focused().should("have.id", "name");
cy.get('.misc-form').find('#description').click() cy.get(".misc-form").find("#description").click();
cy.focused().should('have.id', 'description') cy.focused().should("have.id", "description");
}) });
context('Cypress.Screenshot', function () { context("Cypress.Screenshot", function () {
it('cy.screenshot() - take a screenshot', () => { it("cy.screenshot() - take a screenshot", () => {
// https://on.cypress.io/screenshot // https://on.cypress.io/screenshot
cy.screenshot('my-image') cy.screenshot("my-image");
}) });
it('Cypress.Screenshot.defaults() - change default config of screenshots', function () { it("Cypress.Screenshot.defaults() - change default config of screenshots", function () {
Cypress.Screenshot.defaults({ Cypress.Screenshot.defaults({
blackout: ['.foo'], blackout: [".foo"],
capture: 'viewport', capture: "viewport",
clip: {x: 0, y: 0, width: 200, height: 200}, clip: { x: 0, y: 0, width: 200, height: 200 },
scale: false, scale: false,
disableTimersAndAnimations: true, disableTimersAndAnimations: true,
screenshotOnRunFailure: true, screenshotOnRunFailure: true,
onBeforeScreenshot() { onBeforeScreenshot() {},
}, onAfterScreenshot() {}
onAfterScreenshot() { });
}, });
}) });
})
})
it('cy.wrap() - wrap an object', () => { it("cy.wrap() - wrap an object", () => {
// https://on.cypress.io/wrap // https://on.cypress.io/wrap
cy.wrap({foo: 'bar'}) cy.wrap({ foo: "bar" }).should("have.property", "foo").and("include", "bar");
.should('have.property', 'foo') });
.and('include', 'bar') });
})
})

View File

@@ -1,56 +1,56 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Navigation', () => { context("Navigation", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io') cy.visit("https://example.cypress.io");
cy.get('.navbar-nav').contains('Commands').click() cy.get(".navbar-nav").contains("Commands").click();
cy.get('.dropdown-menu').contains('Navigation').click() cy.get(".dropdown-menu").contains("Navigation").click();
}) });
it('cy.go() - go back or forward in the browser\'s history', () => { it("cy.go() - go back or forward in the browser's history", () => {
// https://on.cypress.io/go // https://on.cypress.io/go
cy.location('pathname').should('include', 'navigation') cy.location("pathname").should("include", "navigation");
cy.go('back') cy.go("back");
cy.location('pathname').should('not.include', 'navigation') cy.location("pathname").should("not.include", "navigation");
cy.go('forward') cy.go("forward");
cy.location('pathname').should('include', 'navigation') cy.location("pathname").should("include", "navigation");
// clicking back // clicking back
cy.go(-1) cy.go(-1);
cy.location('pathname').should('not.include', 'navigation') cy.location("pathname").should("not.include", "navigation");
// clicking forward // clicking forward
cy.go(1) cy.go(1);
cy.location('pathname').should('include', 'navigation') cy.location("pathname").should("include", "navigation");
}) });
it('cy.reload() - reload the page', () => { it("cy.reload() - reload the page", () => {
// https://on.cypress.io/reload // https://on.cypress.io/reload
cy.reload() cy.reload();
// reload the page without using the cache // reload the page without using the cache
cy.reload(true) cy.reload(true);
}) });
it('cy.visit() - visit a remote url', () => { it("cy.visit() - visit a remote url", () => {
// https://on.cypress.io/visit // https://on.cypress.io/visit
// Visit any sub-domain of your current domain // Visit any sub-domain of your current domain
// Pass options to the visit // Pass options to the visit
cy.visit('https://example.cypress.io/commands/navigation', { cy.visit("https://example.cypress.io/commands/navigation", {
timeout: 50000, // increase total time for the visit to resolve timeout: 50000, // increase total time for the visit to resolve
onBeforeLoad(contentWindow) { onBeforeLoad(contentWindow) {
// contentWindow is the remote page's window object // contentWindow is the remote page's window object
expect(typeof contentWindow === 'object').to.be.true expect(typeof contentWindow === "object").to.be.true;
}, },
onLoad(contentWindow) { onLoad(contentWindow) {
// contentWindow is the remote page's window object // contentWindow is the remote page's window object
expect(typeof contentWindow === 'object').to.be.true expect(typeof contentWindow === "object").to.be.true;
}, }
}) });
}) });
}) });

View File

@@ -1,163 +1,165 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Network Requests', () => { context("Network Requests", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/network-requests') cy.visit("https://example.cypress.io/commands/network-requests");
});
// Manage HTTP requests in your app
it("cy.request() - make an XHR request", () => {
// https://on.cypress.io/request
cy.request("https://jsonplaceholder.cypress.io/comments").should((response) => {
expect(response.status).to.eq(200);
// the server sometimes gets an extra comment posted from another machine
// which gets returned as 1 extra object
expect(response.body).to.have.property("length").and.be.oneOf([500, 501]);
expect(response).to.have.property("headers");
expect(response).to.have.property("duration");
});
});
it("cy.request() - verify response using BDD syntax", () => {
cy.request("https://jsonplaceholder.cypress.io/comments").then((response) => {
// https://on.cypress.io/assertions
expect(response).property("status").to.equal(200);
expect(response).property("body").to.have.property("length").and.be.oneOf([500, 501]);
expect(response).to.include.keys("headers", "duration");
});
});
it("cy.request() with query parameters", () => {
// will execute request
// https://jsonplaceholder.cypress.io/comments?postId=1&id=3
cy.request({
url: "https://jsonplaceholder.cypress.io/comments",
qs: {
postId: 1,
id: 3
}
}) })
.its("body")
.should("be.an", "array")
.and("have.length", 1)
.its("0") // yields first element of the array
.should("contain", {
postId: 1,
id: 3
});
});
// Manage HTTP requests in your app it("cy.request() - pass result to the second request", () => {
// first, let's find out the userId of the first user we have
cy.request("https://jsonplaceholder.cypress.io/users?_limit=1")
.its("body") // yields the response object
.its("0") // yields the first element of the returned list
// the above two commands its('body').its('0')
// can be written as its('body.0')
// if you do not care about TypeScript checks
.then((user) => {
expect(user).property("id").to.be.a("number");
// make a new post on behalf of the user
cy.request("POST", "https://jsonplaceholder.cypress.io/posts", {
userId: user.id,
title: "Cypress Test Runner",
body: "Fast, easy and reliable testing for anything that runs in a browser."
});
})
// note that the value here is the returned value of the 2nd request
// which is the new post object
.then((response) => {
expect(response).property("status").to.equal(201); // new entity created
expect(response).property("body").to.contain({
title: "Cypress Test Runner"
});
it('cy.request() - make an XHR request', () => { // we don't know the exact post id - only that it will be > 100
// https://on.cypress.io/request // since JSONPlaceholder has built-in 100 posts
cy.request('https://jsonplaceholder.cypress.io/comments') expect(response.body).property("id").to.be.a("number").and.to.be.gt(100);
.should((response) => {
expect(response.status).to.eq(200)
// the server sometimes gets an extra comment posted from another machine
// which gets returned as 1 extra object
expect(response.body).to.have.property('length').and.be.oneOf([500, 501])
expect(response).to.have.property('headers')
expect(response).to.have.property('duration')
})
})
it('cy.request() - verify response using BDD syntax', () => { // we don't know the user id here - since it was in above closure
cy.request('https://jsonplaceholder.cypress.io/comments') // so in this test just confirm that the property is there
.then((response) => { expect(response.body).property("userId").to.be.a("number");
// https://on.cypress.io/assertions });
expect(response).property('status').to.equal(200) });
expect(response).property('body').to.have.property('length').and.be.oneOf([500, 501])
expect(response).to.include.keys('headers', 'duration')
})
})
it('cy.request() with query parameters', () => { it("cy.request() - save response in the shared test context", () => {
// will execute request // https://on.cypress.io/variables-and-aliases
// https://jsonplaceholder.cypress.io/comments?postId=1&id=3 cy.request("https://jsonplaceholder.cypress.io/users?_limit=1")
cy.request({ .its("body")
url: 'https://jsonplaceholder.cypress.io/comments', .its("0") // yields the first element of the returned list
qs: { .as("user") // saves the object in the test context
postId: 1, .then(function () {
id: 3, // NOTE 👀
}, // By the time this callback runs the "as('user')" command
// has saved the user object in the test context.
// To access the test context we need to use
// the "function () { ... }" callback form,
// otherwise "this" points at a wrong or undefined object!
cy.request("POST", "https://jsonplaceholder.cypress.io/posts", {
userId: this.user.id,
title: "Cypress Test Runner",
body: "Fast, easy and reliable testing for anything that runs in a browser."
}) })
.its('body') .its("body")
.should('be.an', 'array') .as("post"); // save the new post from the response
.and('have.length', 1) })
.its('0') // yields first element of the array .then(function () {
.should('contain', { // When this callback runs, both "cy.request" API commands have finished
postId: 1, // and the test context has "user" and "post" objects set.
id: 3, // Let's verify them.
}) expect(this.post, "post has the right user id").property("userId").to.equal(this.user.id);
}) });
});
it('cy.request() - pass result to the second request', () => { it("cy.intercept() - route responses to matching requests", () => {
// first, let's find out the userId of the first user we have // https://on.cypress.io/intercept
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1')
.its('body') // yields the response object
.its('0') // yields the first element of the returned list
// the above two commands its('body').its('0')
// can be written as its('body.0')
// if you do not care about TypeScript checks
.then((user) => {
expect(user).property('id').to.be.a('number')
// make a new post on behalf of the user
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', {
userId: user.id,
title: 'Cypress Test Runner',
body: 'Fast, easy and reliable testing for anything that runs in a browser.',
})
})
// note that the value here is the returned value of the 2nd request
// which is the new post object
.then((response) => {
expect(response).property('status').to.equal(201) // new entity created
expect(response).property('body').to.contain({
title: 'Cypress Test Runner',
})
// we don't know the exact post id - only that it will be > 100 let message = "whoa, this comment does not exist";
// since JSONPlaceholder has built-in 100 posts
expect(response.body).property('id').to.be.a('number')
.and.to.be.gt(100)
// we don't know the user id here - since it was in above closure // Listen to GET to comments/1
// so in this test just confirm that the property is there cy.intercept("GET", "**/comments/*").as("getComment");
expect(response.body).property('userId').to.be.a('number')
})
})
it('cy.request() - save response in the shared test context', () => { // we have code that gets a comment when
// https://on.cypress.io/variables-and-aliases // the button is clicked in scripts.js
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1') cy.get(".network-btn").click();
.its('body').its('0') // yields the first element of the returned list
.as('user') // saves the object in the test context
.then(function () {
// NOTE 👀
// By the time this callback runs the "as('user')" command
// has saved the user object in the test context.
// To access the test context we need to use
// the "function () { ... }" callback form,
// otherwise "this" points at a wrong or undefined object!
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', {
userId: this.user.id,
title: 'Cypress Test Runner',
body: 'Fast, easy and reliable testing for anything that runs in a browser.',
})
.its('body').as('post') // save the new post from the response
})
.then(function () {
// When this callback runs, both "cy.request" API commands have finished
// and the test context has "user" and "post" objects set.
// Let's verify them.
expect(this.post, 'post has the right user id').property('userId').to.equal(this.user.id)
})
})
it('cy.intercept() - route responses to matching requests', () => { // https://on.cypress.io/wait
// https://on.cypress.io/intercept cy.wait("@getComment").its("response.statusCode").should("be.oneOf", [200, 304]);
let message = 'whoa, this comment does not exist' // Listen to POST to comments
cy.intercept("POST", "**/comments").as("postComment");
// Listen to GET to comments/1 // we have code that posts a comment when
cy.intercept('GET', '**/comments/*').as('getComment') // the button is clicked in scripts.js
cy.get(".network-post").click();
cy.wait("@postComment").should(({ request, response }) => {
expect(request.body).to.include("email");
expect(request.headers).to.have.property("content-type");
expect(response && response.body).to.have.property("name", "Using POST in cy.intercept()");
});
// we have code that gets a comment when // Stub a response to PUT comments/ ****
// the button is clicked in scripts.js cy.intercept(
cy.get('.network-btn').click() {
method: "PUT",
url: "**/comments/*"
},
{
statusCode: 404,
body: { error: message },
headers: { "access-control-allow-origin": "*" },
delayMs: 500
}
).as("putComment");
// https://on.cypress.io/wait // we have code that puts a comment when
cy.wait('@getComment').its('response.statusCode').should('be.oneOf', [200, 304]) // the button is clicked in scripts.js
cy.get(".network-put").click();
// Listen to POST to comments cy.wait("@putComment");
cy.intercept('POST', '**/comments').as('postComment')
// we have code that posts a comment when // our 404 statusCode logic in scripts.js executed
// the button is clicked in scripts.js cy.get(".network-put-comment").should("contain", message);
cy.get('.network-post').click() });
cy.wait('@postComment').should(({request, response}) => { });
expect(request.body).to.include('email')
expect(request.headers).to.have.property('content-type')
expect(response && response.body).to.have.property('name', 'Using POST in cy.intercept()')
})
// Stub a response to PUT comments/ ****
cy.intercept({
method: 'PUT',
url: '**/comments/*',
}, {
statusCode: 404,
body: {error: message},
headers: {'access-control-allow-origin': '*'},
delayMs: 500,
}).as('putComment')
// we have code that puts a comment when
// the button is clicked in scripts.js
cy.get('.network-put').click()
cy.wait('@putComment')
// our 404 statusCode logic in scripts.js executed
cy.get('.network-put-comment').should('contain', message)
})
})

View File

@@ -1,114 +1,100 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Querying', () => { context("Querying", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/querying') cy.visit("https://example.cypress.io/commands/querying");
}) });
// The most commonly used query is 'cy.get()', you can // The most commonly used query is 'cy.get()', you can
// think of this like the '$' in jQuery // think of this like the '$' in jQuery
it('cy.get() - query DOM elements', () => { it("cy.get() - query DOM elements", () => {
// https://on.cypress.io/get // https://on.cypress.io/get
cy.get('#query-btn').should('contain', 'Button') cy.get("#query-btn").should("contain", "Button");
cy.get('.query-btn').should('contain', 'Button') cy.get(".query-btn").should("contain", "Button");
cy.get('#querying .well>button:first').should('contain', 'Button') cy.get("#querying .well>button:first").should("contain", "Button");
// ↲ // ↲
// Use CSS selectors just like jQuery // Use CSS selectors just like jQuery
cy.get('[data-test-id="test-example"]').should('have.class', 'example') cy.get('[data-test-id="test-example"]').should("have.class", "example");
// 'cy.get()' yields jQuery object, you can get its attribute // 'cy.get()' yields jQuery object, you can get its attribute
// by invoking `.attr()` method // by invoking `.attr()` method
cy.get('[data-test-id="test-example"]') cy.get('[data-test-id="test-example"]').invoke("attr", "data-test-id").should("equal", "test-example");
.invoke('attr', 'data-test-id')
.should('equal', 'test-example')
// or you can get element's CSS property // or you can get element's CSS property
cy.get('[data-test-id="test-example"]') cy.get('[data-test-id="test-example"]').invoke("css", "position").should("equal", "static");
.invoke('css', 'position')
.should('equal', 'static')
// or use assertions directly during 'cy.get()' // or use assertions directly during 'cy.get()'
// https://on.cypress.io/assertions // https://on.cypress.io/assertions
cy.get('[data-test-id="test-example"]') cy.get('[data-test-id="test-example"]')
.should('have.attr', 'data-test-id', 'test-example') .should("have.attr", "data-test-id", "test-example")
.and('have.css', 'position', 'static') .and("have.css", "position", "static");
}) });
it('cy.contains() - query DOM elements with matching content', () => { it("cy.contains() - query DOM elements with matching content", () => {
// https://on.cypress.io/contains // https://on.cypress.io/contains
cy.get('.query-list') cy.get(".query-list").contains("bananas").should("have.class", "third");
.contains('bananas')
.should('have.class', 'third')
// we can pass a regexp to `.contains()` // we can pass a regexp to `.contains()`
cy.get('.query-list') cy.get(".query-list").contains(/^b\w+/).should("have.class", "third");
.contains(/^b\w+/)
.should('have.class', 'third')
cy.get('.query-list') cy.get(".query-list").contains("apples").should("have.class", "first");
.contains('apples')
.should('have.class', 'first')
// passing a selector to contains will // passing a selector to contains will
// yield the selector containing the text // yield the selector containing the text
cy.get('#querying') cy.get("#querying").contains("ul", "oranges").should("have.class", "query-list");
.contains('ul', 'oranges')
.should('have.class', 'query-list')
cy.get('.query-button') cy.get(".query-button").contains("Save Form").should("have.class", "btn");
.contains('Save Form') });
.should('have.class', 'btn')
})
it('.within() - query DOM elements within a specific element', () => { it(".within() - query DOM elements within a specific element", () => {
// https://on.cypress.io/within // https://on.cypress.io/within
cy.get('.query-form').within(() => { cy.get(".query-form").within(() => {
cy.get('input:first').should('have.attr', 'placeholder', 'Email') cy.get("input:first").should("have.attr", "placeholder", "Email");
cy.get('input:last').should('have.attr', 'placeholder', 'Password') cy.get("input:last").should("have.attr", "placeholder", "Password");
}) });
}) });
it('cy.root() - query the root DOM element', () => { it("cy.root() - query the root DOM element", () => {
// https://on.cypress.io/root // https://on.cypress.io/root
// By default, root is the document // By default, root is the document
cy.root().should('match', 'html') cy.root().should("match", "html");
cy.get('.query-ul').within(() => { cy.get(".query-ul").within(() => {
// In this within, the root is now the ul DOM element // In this within, the root is now the ul DOM element
cy.root().should('have.class', 'query-ul') cy.root().should("have.class", "query-ul");
}) });
}) });
it('best practices - selecting elements', () => { it("best practices - selecting elements", () => {
// https://on.cypress.io/best-practices#Selecting-Elements // https://on.cypress.io/best-practices#Selecting-Elements
cy.get('[data-cy=best-practices-selecting-elements]').within(() => { cy.get("[data-cy=best-practices-selecting-elements]").within(() => {
// Worst - too generic, no context // Worst - too generic, no context
cy.get('button').click() cy.get("button").click();
// Bad. Coupled to styling. Highly subject to change. // Bad. Coupled to styling. Highly subject to change.
cy.get('.btn.btn-large').click() cy.get(".btn.btn-large").click();
// Average. Coupled to the `name` attribute which has HTML semantics. // Average. Coupled to the `name` attribute which has HTML semantics.
cy.get('[name=submission]').click() cy.get("[name=submission]").click();
// Better. But still coupled to styling or JS event listeners. // Better. But still coupled to styling or JS event listeners.
cy.get('#main').click() cy.get("#main").click();
// Slightly better. Uses an ID but also ensures the element // Slightly better. Uses an ID but also ensures the element
// has an ARIA role attribute // has an ARIA role attribute
cy.get('#main[role=button]').click() cy.get("#main[role=button]").click();
// Much better. But still coupled to text content that may change. // Much better. But still coupled to text content that may change.
cy.contains('Submit').click() cy.contains("Submit").click();
// Best. Insulated from all changes. // Best. Insulated from all changes.
cy.get('[data-cy=submit]').click() cy.get("[data-cy=submit]").click();
}) });
}) });
}) });

View File

@@ -2,205 +2,202 @@
// remove no check once Cypress.sinon is typed // remove no check once Cypress.sinon is typed
// https://github.com/cypress-io/cypress/issues/6720 // https://github.com/cypress-io/cypress/issues/6720
context('Spies, Stubs, and Clock', () => { context("Spies, Stubs, and Clock", () => {
it('cy.spy() - wrap a method in a spy', () => { it("cy.spy() - wrap a method in a spy", () => {
// https://on.cypress.io/spy // https://on.cypress.io/spy
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
const obj = { const obj = {
foo() { foo() {}
}, };
}
const spy = cy.spy(obj, 'foo').as('anyArgs') const spy = cy.spy(obj, "foo").as("anyArgs");
obj.foo() obj.foo();
expect(spy).to.be.called expect(spy).to.be.called;
}) });
it('cy.spy() retries until assertions pass', () => { it("cy.spy() retries until assertions pass", () => {
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
const obj = { const obj = {
/** /**
* Prints the argument passed * Prints the argument passed
* @param x {any} * @param x {any}
*/ */
foo(x) { foo(x) {
console.log('obj.foo called with', x) console.log("obj.foo called with", x);
}, }
} };
cy.spy(obj, 'foo').as('foo') cy.spy(obj, "foo").as("foo");
setTimeout(() => { setTimeout(() => {
obj.foo('first') obj.foo("first");
}, 500) }, 500);
setTimeout(() => { setTimeout(() => {
obj.foo('second') obj.foo("second");
}, 2500) }, 2500);
cy.get('@foo').should('have.been.calledTwice') cy.get("@foo").should("have.been.calledTwice");
}) });
it('cy.stub() - create a stub and/or replace a function with stub', () => { it("cy.stub() - create a stub and/or replace a function with stub", () => {
// https://on.cypress.io/stub // https://on.cypress.io/stub
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
const obj = { const obj = {
/** /**
* prints both arguments to the console * prints both arguments to the console
* @param a {string} * @param a {string}
* @param b {string} * @param b {string}
*/ */
foo(a, b) { foo(a, b) {
console.log('a', a, 'b', b) console.log("a", a, "b", b);
}, }
} };
const stub = cy.stub(obj, 'foo').as('foo') const stub = cy.stub(obj, "foo").as("foo");
obj.foo('foo', 'bar') obj.foo("foo", "bar");
expect(stub).to.be.called expect(stub).to.be.called;
}) });
it('cy.clock() - control time in the browser', () => { it("cy.clock() - control time in the browser", () => {
// https://on.cypress.io/clock // https://on.cypress.io/clock
// create the date in UTC so its always the same // create the date in UTC so its always the same
// no matter what local timezone the browser is running in // no matter what local timezone the browser is running in
const now = new Date(Date.UTC(2017, 2, 14)).getTime() const now = new Date(Date.UTC(2017, 2, 14)).getTime();
cy.clock(now) cy.clock(now);
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
cy.get('#clock-div').click() cy.get("#clock-div").click().should("have.text", "1489449600");
.should('have.text', '1489449600') });
})
it('cy.tick() - move time in the browser', () => { it("cy.tick() - move time in the browser", () => {
// https://on.cypress.io/tick // https://on.cypress.io/tick
// create the date in UTC so its always the same // create the date in UTC so its always the same
// no matter what local timezone the browser is running in // no matter what local timezone the browser is running in
const now = new Date(Date.UTC(2017, 2, 14)).getTime() const now = new Date(Date.UTC(2017, 2, 14)).getTime();
cy.clock(now) cy.clock(now);
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
cy.get('#tick-div').click() cy.get("#tick-div").click().should("have.text", "1489449600");
.should('have.text', '1489449600')
cy.tick(10000) // 10 seconds passed cy.tick(10000); // 10 seconds passed
cy.get('#tick-div').click() cy.get("#tick-div").click().should("have.text", "1489449610");
.should('have.text', '1489449610') });
})
it('cy.stub() matches depending on arguments', () => { it("cy.stub() matches depending on arguments", () => {
// see all possible matchers at // see all possible matchers at
// https://sinonjs.org/releases/latest/matchers/ // https://sinonjs.org/releases/latest/matchers/
const greeter = { const greeter = {
/** /**
* Greets a person * Greets a person
* @param {string} name * @param {string} name
*/ */
greet(name) { greet(name) {
return `Hello, ${name}!` return `Hello, ${name}!`;
}, }
} };
cy.stub(greeter, 'greet') cy.stub(greeter, "greet")
.callThrough() // if you want non-matched calls to call the real method .callThrough() // if you want non-matched calls to call the real method
.withArgs(Cypress.sinon.match.string).returns('Hi') .withArgs(Cypress.sinon.match.string)
.withArgs(Cypress.sinon.match.number).throws(new Error('Invalid name')) .returns("Hi")
.withArgs(Cypress.sinon.match.number)
.throws(new Error("Invalid name"));
expect(greeter.greet('World')).to.equal('Hi') expect(greeter.greet("World")).to.equal("Hi");
// @ts-ignore // @ts-ignore
expect(() => greeter.greet(42)).to.throw('Invalid name') expect(() => greeter.greet(42)).to.throw("Invalid name");
expect(greeter.greet).to.have.been.calledTwice expect(greeter.greet).to.have.been.calledTwice;
// non-matched calls goes the actual method // non-matched calls goes the actual method
// @ts-ignore // @ts-ignore
expect(greeter.greet()).to.equal('Hello, undefined!') expect(greeter.greet()).to.equal("Hello, undefined!");
}) });
it('matches call arguments using Sinon matchers', () => { it("matches call arguments using Sinon matchers", () => {
// see all possible matchers at // see all possible matchers at
// https://sinonjs.org/releases/latest/matchers/ // https://sinonjs.org/releases/latest/matchers/
const calculator = { const calculator = {
/** /**
* returns the sum of two arguments * returns the sum of two arguments
* @param a {number} * @param a {number}
* @param b {number} * @param b {number}
*/ */
add(a, b) { add(a, b) {
return a + b return a + b;
}, }
} };
const spy = cy.spy(calculator, 'add').as('add') const spy = cy.spy(calculator, "add").as("add");
expect(calculator.add(2, 3)).to.equal(5) expect(calculator.add(2, 3)).to.equal(5);
// if we want to assert the exact values used during the call // if we want to assert the exact values used during the call
expect(spy).to.be.calledWith(2, 3) expect(spy).to.be.calledWith(2, 3);
// let's confirm "add" method was called with two numbers // let's confirm "add" method was called with two numbers
expect(spy).to.be.calledWith(Cypress.sinon.match.number, Cypress.sinon.match.number) expect(spy).to.be.calledWith(Cypress.sinon.match.number, Cypress.sinon.match.number);
// alternatively, provide the value to match // alternatively, provide the value to match
expect(spy).to.be.calledWith(Cypress.sinon.match(2), Cypress.sinon.match(3)) expect(spy).to.be.calledWith(Cypress.sinon.match(2), Cypress.sinon.match(3));
// match any value // match any value
expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3) expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3);
// match any value from a list // match any value from a list
expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3) expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3);
/** /**
* Returns true if the given number is event * Returns true if the given number is event
* @param {number} x * @param {number} x
*/ */
const isEven = (x) => x % 2 === 0 const isEven = (x) => x % 2 === 0;
// expect the value to pass a custom predicate function // expect the value to pass a custom predicate function
// the second argument to "sinon.match(predicate, message)" is // the second argument to "sinon.match(predicate, message)" is
// shown if the predicate does not pass and assertion fails // shown if the predicate does not pass and assertion fails
expect(spy).to.be.calledWith(Cypress.sinon.match(isEven, 'isEven'), 3) expect(spy).to.be.calledWith(Cypress.sinon.match(isEven, "isEven"), 3);
/** /**
* Returns a function that checks if a given number is larger than the limit * Returns a function that checks if a given number is larger than the limit
* @param {number} limit * @param {number} limit
* @returns {(x: number) => boolean} * @returns {(x: number) => boolean}
*/ */
const isGreaterThan = (limit) => (x) => x > limit const isGreaterThan = (limit) => (x) => x > limit;
/** /**
* Returns a function that checks if a given number is less than the limit * Returns a function that checks if a given number is less than the limit
* @param {number} limit * @param {number} limit
* @returns {(x: number) => boolean} * @returns {(x: number) => boolean}
*/ */
const isLessThan = (limit) => (x) => x < limit const isLessThan = (limit) => (x) => x < limit;
// you can combine several matchers using "and", "or" // you can combine several matchers using "and", "or"
expect(spy).to.be.calledWith( expect(spy).to.be.calledWith(
Cypress.sinon.match.number, Cypress.sinon.match.number,
Cypress.sinon.match(isGreaterThan(2), '> 2').and(Cypress.sinon.match(isLessThan(4), '< 4')), Cypress.sinon.match(isGreaterThan(2), "> 2").and(Cypress.sinon.match(isLessThan(4), "< 4"))
) );
expect(spy).to.be.calledWith( expect(spy).to.be.calledWith(
Cypress.sinon.match.number, Cypress.sinon.match.number,
Cypress.sinon.match(isGreaterThan(200), '> 200').or(Cypress.sinon.match(3)), Cypress.sinon.match(isGreaterThan(200), "> 200").or(Cypress.sinon.match(3))
) );
// matchers can be used from BDD assertions // matchers can be used from BDD assertions
cy.get('@add').should('have.been.calledWith', cy.get("@add").should("have.been.calledWith", Cypress.sinon.match.number, Cypress.sinon.match(3));
Cypress.sinon.match.number, Cypress.sinon.match(3))
// you can alias matchers for shorter test code // you can alias matchers for shorter test code
const {match: M} = Cypress.sinon const { match: M } = Cypress.sinon;
cy.get('@add').should('have.been.calledWith', M.number, M(3)) cy.get("@add").should("have.been.calledWith", M.number, M(3));
}) });
}) });

View File

@@ -1,121 +1,97 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Traversal', () => { context("Traversal", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/traversal') cy.visit("https://example.cypress.io/commands/traversal");
}) });
it('.children() - get child DOM elements', () => { it(".children() - get child DOM elements", () => {
// https://on.cypress.io/children // https://on.cypress.io/children
cy.get('.traversal-breadcrumb') cy.get(".traversal-breadcrumb").children(".active").should("contain", "Data");
.children('.active') });
.should('contain', 'Data')
})
it('.closest() - get closest ancestor DOM element', () => { it(".closest() - get closest ancestor DOM element", () => {
// https://on.cypress.io/closest // https://on.cypress.io/closest
cy.get('.traversal-badge') cy.get(".traversal-badge").closest("ul").should("have.class", "list-group");
.closest('ul') });
.should('have.class', 'list-group')
})
it('.eq() - get a DOM element at a specific index', () => { it(".eq() - get a DOM element at a specific index", () => {
// https://on.cypress.io/eq // https://on.cypress.io/eq
cy.get('.traversal-list>li') cy.get(".traversal-list>li").eq(1).should("contain", "siamese");
.eq(1).should('contain', 'siamese') });
})
it('.filter() - get DOM elements that match the selector', () => { it(".filter() - get DOM elements that match the selector", () => {
// https://on.cypress.io/filter // https://on.cypress.io/filter
cy.get('.traversal-nav>li') cy.get(".traversal-nav>li").filter(".active").should("contain", "About");
.filter('.active').should('contain', 'About') });
})
it('.find() - get descendant DOM elements of the selector', () => { it(".find() - get descendant DOM elements of the selector", () => {
// https://on.cypress.io/find // https://on.cypress.io/find
cy.get('.traversal-pagination') cy.get(".traversal-pagination").find("li").find("a").should("have.length", 7);
.find('li').find('a') });
.should('have.length', 7)
})
it('.first() - get first DOM element', () => { it(".first() - get first DOM element", () => {
// https://on.cypress.io/first // https://on.cypress.io/first
cy.get('.traversal-table td') cy.get(".traversal-table td").first().should("contain", "1");
.first().should('contain', '1') });
})
it('.last() - get last DOM element', () => { it(".last() - get last DOM element", () => {
// https://on.cypress.io/last // https://on.cypress.io/last
cy.get('.traversal-buttons .btn') cy.get(".traversal-buttons .btn").last().should("contain", "Submit");
.last().should('contain', 'Submit') });
})
it('.next() - get next sibling DOM element', () => { it(".next() - get next sibling DOM element", () => {
// https://on.cypress.io/next // https://on.cypress.io/next
cy.get('.traversal-ul') cy.get(".traversal-ul").contains("apples").next().should("contain", "oranges");
.contains('apples').next().should('contain', 'oranges') });
})
it('.nextAll() - get all next sibling DOM elements', () => { it(".nextAll() - get all next sibling DOM elements", () => {
// https://on.cypress.io/nextall // https://on.cypress.io/nextall
cy.get('.traversal-next-all') cy.get(".traversal-next-all").contains("oranges").nextAll().should("have.length", 3);
.contains('oranges') });
.nextAll().should('have.length', 3)
})
it('.nextUntil() - get next sibling DOM elements until next el', () => { it(".nextUntil() - get next sibling DOM elements until next el", () => {
// https://on.cypress.io/nextuntil // https://on.cypress.io/nextuntil
cy.get('#veggies') cy.get("#veggies").nextUntil("#nuts").should("have.length", 3);
.nextUntil('#nuts').should('have.length', 3) });
})
it('.not() - remove DOM elements from set of DOM elements', () => { it(".not() - remove DOM elements from set of DOM elements", () => {
// https://on.cypress.io/not // https://on.cypress.io/not
cy.get('.traversal-disabled .btn') cy.get(".traversal-disabled .btn").not("[disabled]").should("not.contain", "Disabled");
.not('[disabled]').should('not.contain', 'Disabled') });
})
it('.parent() - get parent DOM element from DOM elements', () => { it(".parent() - get parent DOM element from DOM elements", () => {
// https://on.cypress.io/parent // https://on.cypress.io/parent
cy.get('.traversal-mark') cy.get(".traversal-mark").parent().should("contain", "Morbi leo risus");
.parent().should('contain', 'Morbi leo risus') });
})
it('.parents() - get parent DOM elements from DOM elements', () => { it(".parents() - get parent DOM elements from DOM elements", () => {
// https://on.cypress.io/parents // https://on.cypress.io/parents
cy.get('.traversal-cite') cy.get(".traversal-cite").parents().should("match", "blockquote");
.parents().should('match', 'blockquote') });
})
it('.parentsUntil() - get parent DOM elements from DOM elements until el', () => { it(".parentsUntil() - get parent DOM elements from DOM elements until el", () => {
// https://on.cypress.io/parentsuntil // https://on.cypress.io/parentsuntil
cy.get('.clothes-nav') cy.get(".clothes-nav").find(".active").parentsUntil(".clothes-nav").should("have.length", 2);
.find('.active') });
.parentsUntil('.clothes-nav')
.should('have.length', 2)
})
it('.prev() - get previous sibling DOM element', () => { it(".prev() - get previous sibling DOM element", () => {
// https://on.cypress.io/prev // https://on.cypress.io/prev
cy.get('.birds').find('.active') cy.get(".birds").find(".active").prev().should("contain", "Lorikeets");
.prev().should('contain', 'Lorikeets') });
})
it('.prevAll() - get all previous sibling DOM elements', () => { it(".prevAll() - get all previous sibling DOM elements", () => {
// https://on.cypress.io/prevall // https://on.cypress.io/prevall
cy.get('.fruits-list').find('.third') cy.get(".fruits-list").find(".third").prevAll().should("have.length", 2);
.prevAll().should('have.length', 2) });
})
it('.prevUntil() - get all previous sibling DOM elements until el', () => { it(".prevUntil() - get all previous sibling DOM elements until el", () => {
// https://on.cypress.io/prevuntil // https://on.cypress.io/prevuntil
cy.get('.foods-list').find('#nuts') cy.get(".foods-list").find("#nuts").prevUntil("#veggies").should("have.length", 3);
.prevUntil('#veggies').should('have.length', 3) });
})
it('.siblings() - get all sibling DOM elements', () => { it(".siblings() - get all sibling DOM elements", () => {
// https://on.cypress.io/siblings // https://on.cypress.io/siblings
cy.get('.traversal-pills .active') cy.get(".traversal-pills .active").siblings().should("have.length", 2);
.siblings().should('have.length', 2) });
}) });
})

View File

@@ -1,110 +1,108 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Utilities', () => { context("Utilities", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/utilities') cy.visit("https://example.cypress.io/utilities");
}) });
it('Cypress._ - call a lodash method', () => { it("Cypress._ - call a lodash method", () => {
// https://on.cypress.io/_ // https://on.cypress.io/_
cy.request('https://jsonplaceholder.cypress.io/users') cy.request("https://jsonplaceholder.cypress.io/users").then((response) => {
.then((response) => { let ids = Cypress._.chain(response.body).map("id").take(3).value();
let ids = Cypress._.chain(response.body).map('id').take(3).value()
expect(ids).to.deep.eq([1, 2, 3]) expect(ids).to.deep.eq([1, 2, 3]);
}) });
}) });
it('Cypress.$ - call a jQuery method', () => { it("Cypress.$ - call a jQuery method", () => {
// https://on.cypress.io/$ // https://on.cypress.io/$
let $li = Cypress.$('.utility-jquery li:first') let $li = Cypress.$(".utility-jquery li:first");
cy.wrap($li) cy.wrap($li).should("not.have.class", "active").click().should("have.class", "active");
.should('not.have.class', 'active') });
.click()
.should('have.class', 'active')
})
it('Cypress.Blob - blob utilities and base64 string conversion', () => { it("Cypress.Blob - blob utilities and base64 string conversion", () => {
// https://on.cypress.io/blob // https://on.cypress.io/blob
cy.get('.utility-blob').then(($div) => { cy.get(".utility-blob").then(($div) => {
// https://github.com/nolanlawson/blob-util#imgSrcToDataURL // https://github.com/nolanlawson/blob-util#imgSrcToDataURL
// get the dataUrl string for the javascript-logo // get the dataUrl string for the javascript-logo
return Cypress.Blob.imgSrcToDataURL('https://example.cypress.io/assets/img/javascript-logo.png', undefined, 'anonymous') return Cypress.Blob.imgSrcToDataURL(
.then((dataUrl) => { "https://example.cypress.io/assets/img/javascript-logo.png",
// create an <img> element and set its src to the dataUrl undefined,
let img = Cypress.$('<img />', {src: dataUrl}) "anonymous"
).then((dataUrl) => {
// create an <img> element and set its src to the dataUrl
let img = Cypress.$("<img />", { src: dataUrl });
// need to explicitly return cy here since we are initially returning // need to explicitly return cy here since we are initially returning
// the Cypress.Blob.imgSrcToDataURL promise to our test // the Cypress.Blob.imgSrcToDataURL promise to our test
// append the image // append the image
$div.append(img) $div.append(img);
cy.get('.utility-blob img').click() cy.get(".utility-blob img").click().should("have.attr", "src", dataUrl);
.should('have.attr', 'src', dataUrl) });
}) });
}) });
})
it('Cypress.minimatch - test out glob patterns against strings', () => { it("Cypress.minimatch - test out glob patterns against strings", () => {
// https://on.cypress.io/minimatch // https://on.cypress.io/minimatch
let matching = Cypress.minimatch('/users/1/comments', '/users/*/comments', { let matching = Cypress.minimatch("/users/1/comments", "/users/*/comments", {
matchBase: true, matchBase: true
}) });
expect(matching, 'matching wildcard').to.be.true expect(matching, "matching wildcard").to.be.true;
matching = Cypress.minimatch('/users/1/comments/2', '/users/*/comments', { matching = Cypress.minimatch("/users/1/comments/2", "/users/*/comments", {
matchBase: true, matchBase: true
}) });
expect(matching, 'comments').to.be.false expect(matching, "comments").to.be.false;
// ** matches against all downstream path segments // ** matches against all downstream path segments
matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/**', { matching = Cypress.minimatch("/foo/bar/baz/123/quux?a=b&c=2", "/foo/**", {
matchBase: true, matchBase: true
}) });
expect(matching, 'comments').to.be.true expect(matching, "comments").to.be.true;
// whereas * matches only the next path segment // whereas * matches only the next path segment
matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/*', { matching = Cypress.minimatch("/foo/bar/baz/123/quux?a=b&c=2", "/foo/*", {
matchBase: false, matchBase: false
}) });
expect(matching, 'comments').to.be.false expect(matching, "comments").to.be.false;
}) });
it('Cypress.Promise - instantiate a bluebird promise', () => { it("Cypress.Promise - instantiate a bluebird promise", () => {
// https://on.cypress.io/promise // https://on.cypress.io/promise
let waited = false let waited = false;
/** /**
* @return Bluebird<string> * @return Bluebird<string>
*/ */
function waitOneSecond() { function waitOneSecond() {
// return a promise that resolves after 1 second // return a promise that resolves after 1 second
// @ts-ignore TS2351 (new Cypress.Promise) // @ts-ignore TS2351 (new Cypress.Promise)
return new Cypress.Promise((resolve, reject) => { return new Cypress.Promise((resolve, reject) => {
setTimeout(() => { setTimeout(() => {
// set waited to true // set waited to true
waited = true waited = true;
// resolve with 'foo' string // resolve with 'foo' string
resolve('foo') resolve("foo");
}, 1000) }, 1000);
}) });
} }
cy.then(() => { cy.then(() => {
// return a promise to cy.then() that // return a promise to cy.then() that
// is awaited until it resolves // is awaited until it resolves
// @ts-ignore TS7006 // @ts-ignore TS7006
return waitOneSecond().then((str) => { return waitOneSecond().then((str) => {
expect(str).to.eq('foo') expect(str).to.eq("foo");
expect(waited).to.be.true expect(waited).to.be.true;
}) });
}) });
}) });
}) });

View File

@@ -1,59 +1,59 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Viewport', () => { context("Viewport", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/viewport') cy.visit("https://example.cypress.io/commands/viewport");
}) });
it('cy.viewport() - set the viewport size and dimension', () => { it("cy.viewport() - set the viewport size and dimension", () => {
// https://on.cypress.io/viewport // https://on.cypress.io/viewport
cy.get('#navbar').should('be.visible') cy.get("#navbar").should("be.visible");
cy.viewport(320, 480) cy.viewport(320, 480);
// the navbar should have collapse since our screen is smaller // the navbar should have collapse since our screen is smaller
cy.get('#navbar').should('not.be.visible') cy.get("#navbar").should("not.be.visible");
cy.get('.navbar-toggle').should('be.visible').click() cy.get(".navbar-toggle").should("be.visible").click();
cy.get('.nav').find('a').should('be.visible') cy.get(".nav").find("a").should("be.visible");
// lets see what our app looks like on a super large screen // lets see what our app looks like on a super large screen
cy.viewport(2999, 2999) cy.viewport(2999, 2999);
// cy.viewport() accepts a set of preset sizes // cy.viewport() accepts a set of preset sizes
// to easily set the screen to a device's width and height // to easily set the screen to a device's width and height
// We added a cy.wait() between each viewport change so you can see // We added a cy.wait() between each viewport change so you can see
// the change otherwise it is a little too fast to see :) // the change otherwise it is a little too fast to see :)
cy.viewport('macbook-15') cy.viewport("macbook-15");
cy.wait(200) cy.wait(200);
cy.viewport('macbook-13') cy.viewport("macbook-13");
cy.wait(200) cy.wait(200);
cy.viewport('macbook-11') cy.viewport("macbook-11");
cy.wait(200) cy.wait(200);
cy.viewport('ipad-2') cy.viewport("ipad-2");
cy.wait(200) cy.wait(200);
cy.viewport('ipad-mini') cy.viewport("ipad-mini");
cy.wait(200) cy.wait(200);
cy.viewport('iphone-6+') cy.viewport("iphone-6+");
cy.wait(200) cy.wait(200);
cy.viewport('iphone-6') cy.viewport("iphone-6");
cy.wait(200) cy.wait(200);
cy.viewport('iphone-5') cy.viewport("iphone-5");
cy.wait(200) cy.wait(200);
cy.viewport('iphone-4') cy.viewport("iphone-4");
cy.wait(200) cy.wait(200);
cy.viewport('iphone-3') cy.viewport("iphone-3");
cy.wait(200) cy.wait(200);
// cy.viewport() accepts an orientation for all presets // cy.viewport() accepts an orientation for all presets
// the default orientation is 'portrait' // the default orientation is 'portrait'
cy.viewport('ipad-2', 'portrait') cy.viewport("ipad-2", "portrait");
cy.wait(200) cy.wait(200);
cy.viewport('iphone-4', 'landscape') cy.viewport("iphone-4", "landscape");
cy.wait(200) cy.wait(200);
// The viewport will be reset back to the default dimensions // The viewport will be reset back to the default dimensions
// in between tests (the default can be set in cypress.json) // in between tests (the default can be set in cypress.json)
}) });
}) });

View File

@@ -1,31 +1,31 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Waiting', () => { context("Waiting", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/waiting') cy.visit("https://example.cypress.io/commands/waiting");
}) });
// BE CAREFUL of adding unnecessary wait times. // BE CAREFUL of adding unnecessary wait times.
// https://on.cypress.io/best-practices#Unnecessary-Waiting // https://on.cypress.io/best-practices#Unnecessary-Waiting
// https://on.cypress.io/wait // https://on.cypress.io/wait
it('cy.wait() - wait for a specific amount of time', () => { it("cy.wait() - wait for a specific amount of time", () => {
cy.get('.wait-input1').type('Wait 1000ms after typing') cy.get(".wait-input1").type("Wait 1000ms after typing");
cy.wait(1000) cy.wait(1000);
cy.get('.wait-input2').type('Wait 1000ms after typing') cy.get(".wait-input2").type("Wait 1000ms after typing");
cy.wait(1000) cy.wait(1000);
cy.get('.wait-input3').type('Wait 1000ms after typing') cy.get(".wait-input3").type("Wait 1000ms after typing");
cy.wait(1000) cy.wait(1000);
}) });
it('cy.wait() - wait for a specific route', () => { it("cy.wait() - wait for a specific route", () => {
// Listen to GET to comments/1 // Listen to GET to comments/1
cy.intercept('GET', '**/comments/*').as('getComment') cy.intercept("GET", "**/comments/*").as("getComment");
// we have code that gets a comment when // we have code that gets a comment when
// the button is clicked in scripts.js // the button is clicked in scripts.js
cy.get('.network-btn').click() cy.get(".network-btn").click();
// wait for GET comments/1 // wait for GET comments/1
cy.wait('@getComment').its('response.statusCode').should('be.oneOf', [200, 304]) cy.wait("@getComment").its("response.statusCode").should("be.oneOf", [200, 304]);
}) });
}) });

View File

@@ -1,22 +1,22 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Window', () => { context("Window", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/window') cy.visit("https://example.cypress.io/commands/window");
}) });
it('cy.window() - get the global window object', () => { it("cy.window() - get the global window object", () => {
// https://on.cypress.io/window // https://on.cypress.io/window
cy.window().should('have.property', 'top') cy.window().should("have.property", "top");
}) });
it('cy.document() - get the document object', () => { it("cy.document() - get the document object", () => {
// https://on.cypress.io/document // https://on.cypress.io/document
cy.document().should('have.property', 'charset').and('eq', 'UTF-8') cy.document().should("have.property", "charset").and("eq", "UTF-8");
}) });
it('cy.title() - get the title', () => { it("cy.title() - get the title", () => {
// https://on.cypress.io/title // https://on.cypress.io/title
cy.title().should('include', 'Kitchen Sink') cy.title().should("include", "Kitchen Sink");
}) });
}) });

View File

@@ -2,4 +2,4 @@
"id": 8739, "id": 8739,
"name": "Jane", "name": "Jane",
"email": "jane@example.com" "email": "jane@example.com"
} }

View File

@@ -17,6 +17,6 @@
*/ */
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
module.exports = (on, config) => { module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits // `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config // `config` is the resolved Cypress config
} };

View File

@@ -14,7 +14,7 @@
// *********************************************************** // ***********************************************************
// Import commands.js using ES2015 syntax: // Import commands.js using ES2015 syntax:
import './commands' import "./commands";
// Alternatively you can use CommonJS syntax: // Alternatively you can use CommonJS syntax:
// require('./commands') // require('./commands')

View File

@@ -2,11 +2,7 @@
"compilerOptions": { "compilerOptions": {
"allowJs": true, "allowJs": true,
"baseUrl": "../node_modules", "baseUrl": "../node_modules",
"types": [ "types": ["cypress"]
"cypress"
]
}, },
"include": [ "include": ["**/*.*"]
"**/*.*"
]
} }

View File

@@ -1 +1,2 @@
if('serviceWorker' in navigator) navigator.serviceWorker.register('/dev-sw.js?dev-sw', { scope: '/', type: 'classic' }) if ("serviceWorker" in navigator)
navigator.serviceWorker.register("/dev-sw.js?dev-sw", { scope: "/", type: "classic" });

View File

@@ -21,22 +21,20 @@ if (!self.define) {
const singleRequire = (uri, parentUri) => { const singleRequire = (uri, parentUri) => {
uri = new URL(uri + ".js", parentUri).href; uri = new URL(uri + ".js", parentUri).href;
return registry[uri] || ( return (
registry[uri] ||
new Promise(resolve => { new Promise((resolve) => {
if ("document" in self) { if ("document" in self) {
const script = document.createElement("script"); const script = document.createElement("script");
script.src = uri; script.src = uri;
script.onload = resolve; script.onload = resolve;
document.head.appendChild(script); document.head.appendChild(script);
} else { } else {
nextDefineUri = uri; nextDefineUri = uri;
importScripts(uri); importScripts(uri);
resolve(); resolve();
} }
}) }).then(() => {
.then(() => {
let promise = registry[uri]; let promise = registry[uri];
if (!promise) { if (!promise) {
throw new Error(`Module ${uri} didnt register its module`); throw new Error(`Module ${uri} didnt register its module`);
@@ -53,21 +51,20 @@ if (!self.define) {
return; return;
} }
let exports = {}; let exports = {};
const require = depUri => singleRequire(depUri, uri); const require = (depUri) => singleRequire(depUri, uri);
const specialDeps = { const specialDeps = {
module: { uri }, module: { uri },
exports, exports,
require require
}; };
registry[uri] = Promise.all(depsNames.map( registry[uri] = Promise.all(depsNames.map((depName) => specialDeps[depName] || require(depName))).then((deps) => {
depName => specialDeps[depName] || require(depName)
)).then(deps => {
factory(...deps); factory(...deps);
return exports; return exports;
}); });
}; };
} }
define(['./workbox-b5f7729d'], (function (workbox) { 'use strict'; define(["./workbox-b5f7729d"], function (workbox) {
"use strict";
self.skipWaiting(); self.skipWaiting();
workbox.clientsClaim(); workbox.clientsClaim();
@@ -77,16 +74,23 @@ define(['./workbox-b5f7729d'], (function (workbox) { 'use strict';
* requests for URLs in the manifest. * requests for URLs in the manifest.
* See https://goo.gl/S9QRab * See https://goo.gl/S9QRab
*/ */
workbox.precacheAndRoute([{ workbox.precacheAndRoute(
"url": "registerSW.js", [
"revision": "3ca0b8505b4bec776b69afdba2768812" {
}, { url: "registerSW.js",
"url": "index.html", revision: "3ca0b8505b4bec776b69afdba2768812"
"revision": "0.sa702m4aq68" },
}], {}); {
url: "index.html",
revision: "0.sa702m4aq68"
}
],
{}
);
workbox.cleanupOutdatedCaches(); workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { workbox.registerRoute(
allowlist: [/^\/$/] new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
})); allowlist: [/^\/$/]
})
})); );
});

File diff suppressed because it is too large Load Diff

2401
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,50 +12,36 @@
"@ant-design/pro-layout": "^7.17.16", "@ant-design/pro-layout": "^7.17.16",
"@apollo/client": "^3.8.10", "@apollo/client": "^3.8.10",
"@asseinfo/react-kanban": "^2.2.0", "@asseinfo/react-kanban": "^2.2.0",
"@craco/craco": "^7.1.0",
"@fingerprintjs/fingerprintjs": "^4.2.2", "@fingerprintjs/fingerprintjs": "^4.2.2",
"@jsreport/browser-client": "^3.1.0", "@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.2.1", "@reduxjs/toolkit": "^2.2.1",
"@sentry/cli": "^2.28.6", "@sentry/cli": "^2.28.6",
"@sentry/react": "^7.102.1", "@sentry/react": "^7.104.0",
"@sentry/tracing": "^7.102.1",
"@splitsoftware/splitio-react": "^1.11.0", "@splitsoftware/splitio-react": "^1.11.0",
"@tanem/react-nprogress": "^5.0.51", "@tanem/react-nprogress": "^5.0.51",
"@vitejs/plugin-legacy": "^5.3.0",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"@vitejs/plugin-react-refresh": "^1.3.6", "antd": "^5.15.3",
"@vitejs/plugin-react-swc": "^3.6.0",
"antd": "^5.14.2",
"apollo-link-logger": "^2.0.1", "apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0", "apollo-link-sentry": "^3.3.0",
"axios": "^1.6.7", "axios": "^1.6.7",
"consola": "^3.2.3",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"dayjs-business-days2": "^1.2.2", "dayjs-business-days2": "^1.2.2",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"enquire-js": "^0.2.1",
"env-cmd": "^10.1.0", "env-cmd": "^10.1.0",
"esbuild": "^0.20.0",
"exifr": "^7.1.3", "exifr": "^7.1.3",
"firebase": "^10.8.0", "firebase": "^10.8.1",
"graphql": "^16.6.0", "graphql": "^16.6.0",
"i18next": "^23.10.0", "i18next": "^23.10.0",
"i18next-browser-languagedetector": "^7.0.2", "i18next-browser-languagedetector": "^7.0.2",
"jsoneditor": "^10.0.1",
"jsreport-browser-client-dist": "^1.3.0",
"libphonenumber-js": "^1.10.57", "libphonenumber-js": "^1.10.57",
"logrocket": "^8.0.1", "logrocket": "^8.0.1",
"markerjs2": "^2.32.0", "markerjs2": "^2.32.0",
"normalize-url": "^8.0.0", "normalize-url": "^8.0.0",
"phone": "^3.1.42",
"preval.macro": "^5.0.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"query-string": "^8.2.0", "query-string": "^9.0.0",
"rc-queue-anim": "^2.0.0",
"rc-scroll-anim": "^2.7.6",
"react": "^18.2.0", "react": "^18.2.0",
"react-big-calendar": "^1.10.3", "react-big-calendar": "^1.11.0",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-cookie": "^7.1.0", "react-cookie": "^7.1.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@@ -65,17 +51,17 @@
"react-i18next": "^14.0.5", "react-i18next": "^14.0.5",
"react-icons": "^5.0.1", "react-icons": "^5.0.1",
"react-image-lightbox": "^5.1.4", "react-image-lightbox": "^5.1.4",
"react-intersection-observer": "^9.8.1", "react-joyride": "^2.7.4",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-number-format": "^5.1.4", "react-number-format": "^5.3.3",
"react-product-fruits": "^2.2.6",
"react-redux": "^9.1.0", "react-redux": "^9.1.0",
"react-resizable": "^3.0.5", "react-resizable": "^3.0.5",
"react-router-dom": "^6.22.1", "react-router-dom": "^6.22.2",
"react-scripts": "^5.0.1", "react-scripts": "^5.0.1",
"react-sticky": "^6.0.3", "react-sticky": "^6.0.3",
"react-sublime-video": "^0.2.5",
"react-virtualized": "^9.22.5", "react-virtualized": "^9.22.5",
"recharts": "^2.12.1", "recharts": "^2.12.2",
"redux": "^5.0.1", "redux": "^5.0.1",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"redux-saga": "^1.3.0", "redux-saga": "^1.3.0",
@@ -86,17 +72,15 @@
"styled-components": "^6.1.8", "styled-components": "^6.1.8",
"subscriptions-transport-ws": "^0.11.0", "subscriptions-transport-ws": "^0.11.0",
"terser-webpack-plugin": "^5.3.10", "terser-webpack-plugin": "^5.3.10",
"vite-plugin-compression": "^0.5.1", "userpilot": "^1.3.1",
"vite-plugin-ejs": "^1.7.0", "vite-plugin-ejs": "^1.7.0",
"vite-plugin-svgr": "^4.2.0",
"web-vitals": "^3.5.2", "web-vitals": "^3.5.2",
"workbox-core": "^7.0.0", "workbox-core": "^7.0.0",
"workbox-expiration": "^7.0.0", "workbox-expiration": "^7.0.0",
"workbox-navigation-preload": "^7.0.0", "workbox-navigation-preload": "^7.0.0",
"workbox-precaching": "^7.0.0", "workbox-precaching": "^7.0.0",
"workbox-routing": "^7.0.0", "workbox-routing": "^7.0.0",
"workbox-strategies": "^7.0.0", "workbox-strategies": "^7.0.0"
"yauzl": "^3.1.0"
}, },
"scripts": { "scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'", "analyze": "source-map-explorer 'build/static/js/*.js'",
@@ -139,10 +123,13 @@
"resolutions": { "resolutions": {
"react-error-overlay": "6.0.9" "react-error-overlay": "6.0.9"
}, },
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "4.6.1"
},
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-react": "^7.23.3", "@babel/preset-react": "^7.23.3",
"@dotenvx/dotenvx": "^0.15.0", "@dotenvx/dotenvx": "^0.15.4",
"@emotion/babel-plugin": "^11.11.0", "@emotion/babel-plugin": "^11.11.0",
"@emotion/react": "^11.11.3", "@emotion/react": "^11.11.3",
"@sentry/webpack-plugin": "^2.14.2", "@sentry/webpack-plugin": "^2.14.2",
@@ -151,9 +138,10 @@
"@testing-library/cypress": "^10.0.1", "@testing-library/cypress": "^10.0.1",
"browserslist": "^4.22.3", "browserslist": "^4.22.3",
"browserslist-to-esbuild": "^2.1.1", "browserslist-to-esbuild": "^2.1.1",
"craco-less": "^3.0.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"cypress": "^13.6.6", "cypress": "^13.6.6",
"eslint": "^8.57.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-cypress": "^2.15.1", "eslint-plugin-cypress": "^2.15.1",
"memfs": "^4.6.0", "memfs": "^4.6.0",
"os-browserify": "^0.3.0", "os-browserify": "^0.3.0",
@@ -162,6 +150,7 @@
"source-map-explorer": "^2.5.3", "source-map-explorer": "^2.5.3",
"vite": "^5.0.11", "vite": "^5.0.11",
"vite-plugin-babel": "^1.2.0", "vite-plugin-babel": "^1.2.0",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-legacy": "^2.1.0", "vite-plugin-legacy": "^2.1.0",
"vite-plugin-node-polyfills": "^0.19.0", "vite-plugin-node-polyfills": "^0.19.0",
"vite-plugin-pwa": "^0.19.0", "vite-plugin-pwa": "^0.19.0",

View File

@@ -0,0 +1,56 @@
// Scripts for firebase and firebase messaging
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-messaging.js");
// Initialize the Firebase app in the service worker by passing the generated config
let firebaseConfig;
switch (this.location.hostname) {
case "localhost":
firebaseConfig = {
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",
};
break;
case "test.imex.online":
firebaseConfig = {
apiKey: "AIzaSyBw7_GTy7GtQyfkIRPVrWHEGKfcqeyXw0c",
authDomain: "imex-test.firebaseapp.com",
projectId: "imex-test",
storageBucket: "imex-test.appspot.com",
messagingSenderId: "991923618608",
appId: "1:991923618608:web:633437569cdad78299bef5",
// measurementId: "${config.measurementId}",
};
break;
case "imex.online":
default:
firebaseConfig = {
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",
};
}
firebase.initializeApp(firebaseConfig);
// Retrieve firebase messaging
const messaging = firebase.messaging();
messaging.onBackgroundMessage(function (payload) {
// Customize notification here
const channel = new BroadcastChannel("imex-sw-messages");
channel.postMessage(payload);
//self.registration.showNotification(notificationTitle, notificationOptions);
});

View File

@@ -1,53 +1,58 @@
import {ApolloProvider} from "@apollo/client"; import { ApolloProvider } from "@apollo/client";
import {SplitFactoryProvider, SplitSdk,} from '@splitsoftware/splitio-react'; import { SplitFactoryProvider, SplitSdk } from "@splitsoftware/splitio-react";
import {ConfigProvider} from "antd"; import { ConfigProvider } from "antd";
import enLocale from "antd/es/locale/en_US"; import enLocale from "antd/es/locale/en_US";
import dayjs from "../utils/day"; import dayjs from "../utils/day";
import 'dayjs/locale/en'; import "dayjs/locale/en";
import React from "react"; import React from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component"; import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
import client from "../utils/GraphQLClient"; import client from "../utils/GraphQLClient";
import App from "./App"; import App from "./App";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import themeProvider from "./themeProvider"; import themeProvider from "./themeProvider";
import { Userpilot } from "userpilot";
// Initialize Userpilot
if (import.meta.env.DEV) {
Userpilot.initialize("NX-69145f08");
}
dayjs.locale("en"); dayjs.locale("en");
const config = { const config = {
core: { core: {
authorizationKey: import.meta.env.VITE_APP_SPLIT_API, authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
key: "anon", key: "anon"
}, }
}; };
export const factory = SplitSdk(config); export const factory = SplitSdk(config);
function AppContainer() { function AppContainer() {
const {t} = useTranslation(); const { t } = useTranslation();
return ( return (
<ApolloProvider client={client}> <ApolloProvider client={client}>
<ConfigProvider <ConfigProvider
//componentSize="small" //componentSize="small"
input={{autoComplete: "new-password"}} input={{ autoComplete: "new-password" }}
locale={enLocale} locale={enLocale}
theme={themeProvider} theme={themeProvider}
form={{ form={{
validateMessages: { validateMessages: {
// eslint-disable-next-line no-template-curly-in-string // eslint-disable-next-line no-template-curly-in-string
required: t("general.validation.required", {label: "${label}"}), required: t("general.validation.required", { label: "${label}" })
}, }
}} }}
> >
<GlobalLoadingBar/> <GlobalLoadingBar />
<SplitFactoryProvider factory={factory}> <SplitFactoryProvider factory={factory}>
<App/> <App />
</SplitFactoryProvider> </SplitFactoryProvider>
</ConfigProvider> </ConfigProvider>
</ApolloProvider> </ApolloProvider>
); );
} }
export default Sentry.withProfiler(AppContainer); export default Sentry.withProfiler(AppContainer);

View File

@@ -17,233 +17,225 @@ import TechPageContainer from "../pages/tech/tech.page.container";
import { setOnline } from "../redux/application/application.actions"; import { setOnline } from "../redux/application/application.actions";
import { selectOnline } from "../redux/application/application.selectors"; import { selectOnline } from "../redux/application/application.selectors";
import { checkUserSession } from "../redux/user/user.actions"; import { checkUserSession } from "../redux/user/user.actions";
import { import { selectBodyshop, selectCurrentEula, selectCurrentUser } from "../redux/user/user.selectors";
selectBodyshop,
selectCurrentEula,
selectCurrentUser,
} from "../redux/user/user.selectors";
import PrivateRoute from "../components/PrivateRoute"; import PrivateRoute from "../components/PrivateRoute";
import "./App.styles.scss"; import "./App.styles.scss";
import handleBeta from "../utils/betaHandler"; import handleBeta from "../utils/betaHandler";
import Eula from "../components/eula/eula.component"; import Eula from "../components/eula/eula.component";
import InstanceRenderMgr from "../utils/instanceRenderMgr"; import InstanceRenderMgr from "../utils/instanceRenderMgr";
const ResetPassword = lazy(() => import { ProductFruits } from "react-product-fruits";
import("../pages/reset-password/reset-password.component")
); const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
const ManagePage = lazy(() => import("../pages/manage/manage.page.container")); const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page")); const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
const CsiPage = lazy(() => import("../pages/csi/csi.container.page")); const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
const MobilePaymentContainer = lazy(() => const MobilePaymentContainer = lazy(() => import("../pages/mobile-payment/mobile-payment.container"));
import("../pages/mobile-payment/mobile-payment.container")
);
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
online: selectOnline, online: selectOnline,
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentEula: selectCurrentEula, currentEula: selectCurrentEula
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
checkUserSession: () => dispatch(checkUserSession()), checkUserSession: () => dispatch(checkUserSession()),
setOnline: (isOnline) => dispatch(setOnline(isOnline)), setOnline: (isOnline) => dispatch(setOnline(isOnline))
}); });
export function App({ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline, currentEula }) {
bodyshop, const client = useSplitClient().client;
checkUserSession, const [listenersAdded, setListenersAdded] = useState(false);
currentUser, const { t } = useTranslation();
online,
setOnline,
currentEula,
}) {
const client = useSplitClient().client;
const [listenersAdded, setListenersAdded] = useState(false);
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
if (!navigator.onLine) { if (!navigator.onLine) {
setOnline(false); 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 ===
InstanceRenderMgr({
imex: "beta.imex.online",
rome: "beta.romeonline.io",
})
) {
console.log("LR Start");
LogRocket.init(
InstanceRenderMgr({
imex: "gvfvfw/bodyshopapp",
rome: "rome-online/rome-online",
promanager: "", //TODO:AIO Add in log rocket for promanager instances.
})
);
}
}
}, [bodyshop, client, currentUser.authorized]);
if (currentUser.authorized === null) {
return <LoadingSpinner message={t("general.labels.loggingin")} />;
} }
handleBeta(); checkUserSession();
}, [checkUserSession, setOnline]);
if (!online) //const b = Grid.useBreakpoint();
return ( // console.log("Breakpoints:", b);
<Result
status="warning" // Associate event listeners, memoize to prevent multiple listeners being added
title={t("general.labels.nointernet")} useEffect(() => {
subTitle={t("general.labels.nointernet_sub")} const offlineListener = (e) => {
extra={ setOnline(false);
<Button };
type="primary"
onClick={() => { const onlineListener = (e) => {
window.location.reload(); setOnline(true);
}} };
>
{t("general.actions.refresh")} if (!listenersAdded) {
</Button> 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 ===
InstanceRenderMgr({
imex: "beta.imex.online",
rome: "beta.romeonline.io"
})
) {
console.log("LR Start");
LogRocket.init(
InstanceRenderMgr({
imex: "gvfvfw/bodyshopapp",
rome: "rome-online/rome-online",
promanager: "" //TODO:AIO Add in log rocket for promanager instances.
})
); );
}
if (currentEula && !currentUser.eulaIsAccepted) {
return <Eula />;
} }
}, [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")} />;
}
handleBeta();
if (!online)
return ( return (
<Suspense <Result
fallback={ status="warning"
<LoadingSpinner title={t("general.labels.nointernet")}
message={InstanceRenderMgr({ subTitle={t("general.labels.nointernet_sub")}
imex: t("titles.imexonline"), extra={
rome: t("titles.romeonline"), <Button
promanager: t("titles.promanager") type="primary"
})} onClick={() => {
/> window.location.reload();
} }}
> >
<Routes> {t("general.actions.refresh")}
<Route </Button>
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>
); );
if (currentEula && !currentUser.eulaIsAccepted) {
return <Eula />;
}
// Any route that is not assigned and matched will default to the Landing Page component
return (
<Suspense
fallback={
<LoadingSpinner
message={InstanceRenderMgr({
imex: t("titles.imexonline"),
rome: t("titles.romeonline"),
promanager: t("titles.promanager")
})}
/>
}
>
<ProductFruits
workspaceCode={InstanceRenderMgr({
imex: null,
rome: "9BkbEseqNqxw8jUH",
promanager: "aoJoEifvezYI0Z0P"
})}
debug
language="en"
user={{
email: currentUser.email,
username: currentUser.email
}}
/>
<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>
);
} }
export default connect(mapStateToProps, mapDispatchToProps)(App); export default connect(mapStateToProps, mapDispatchToProps)(App);

View File

@@ -154,7 +154,6 @@
font-style: italic; font-style: italic;
} }
.ant-table-tbody > tr.ant-table-row:nth-child(2n) > td { .ant-table-tbody > tr.ant-table-row:nth-child(2n) > td {
background-color: #f4f4f4; background-color: #f4f4f4;
} }

View File

@@ -1,8 +1,8 @@
import {defaultsDeep} from "lodash"; import { defaultsDeep } from "lodash";
import {theme} from "antd"; import { theme } from "antd";
import InstanceRenderMgr from '../utils/instanceRenderMgr' import InstanceRenderMgr from "../utils/instanceRenderMgr";
const {defaultAlgorithm, darkAlgorithm} = theme; const { defaultAlgorithm, darkAlgorithm } = theme;
let isDarkMode = false; let isDarkMode = false;
@@ -13,28 +13,28 @@ let isDarkMode = false;
const defaultTheme = { const defaultTheme = {
components: { components: {
Table: { Table: {
rowHoverBg: '#e7f3ff', rowHoverBg: "#e7f3ff",
rowSelectedBg: '#e6f7ff', rowSelectedBg: "#e6f7ff",
headerSortHoverBg: 'transparent', headerSortHoverBg: "transparent"
}, },
Menu: { Menu: {
darkItemHoverBg: '#1890ff', darkItemHoverBg: "#1890ff",
itemHoverBg: '#1890ff', itemHoverBg: "#1890ff",
horizontalItemHoverBg: '#1890ff', horizontalItemHoverBg: "#1890ff"
}, }
}, },
token: { token: {
colorPrimary: InstanceRenderMgr({ colorPrimary: InstanceRenderMgr({
imex: '#1890ff', imex: "#1890ff",
rome: '#326ade', rome: "#326ade",
promanager:"#1d69a6" promanager: "#1d69a6"
}), }),
colorInfo: InstanceRenderMgr({ colorInfo: InstanceRenderMgr({
imex: '#1890ff', imex: "#1890ff",
rome: '#326ade', rome: "#326ade",
promanager:"#1d69a6" promanager: "#1d69a6"
}), })
}, }
}; };
/** /**
@@ -42,16 +42,16 @@ const defaultTheme = {
* @type {{components: {Menu: {itemHoverBg: string, darkItemHoverBg: string, horizontalItemHoverBg: string}}, token: {colorPrimary: string}}} * @type {{components: {Menu: {itemHoverBg: string, darkItemHoverBg: string, horizontalItemHoverBg: string}}, token: {colorPrimary: string}}}
*/ */
const devTheme = { const devTheme = {
components: { components: {
Menu: { Menu: {
darkItemHoverBg: '#a51d1d', darkItemHoverBg: "#a51d1d",
itemHoverBg: '#a51d1d', itemHoverBg: "#a51d1d",
horizontalItemHoverBg: '#a51d1d', horizontalItemHoverBg: "#a51d1d"
}
},
token: {
colorPrimary: '#a51d1d'
} }
},
token: {
colorPrimary: "#a51d1d"
}
}; };
/** /**
@@ -60,11 +60,10 @@ const devTheme = {
*/ */
const prodTheme = {}; const prodTheme = {};
const currentTheme = import.meta.env.DEV ? devTheme const currentTheme = import.meta.env.DEV ? devTheme : prodTheme;
: prodTheme;
const finaltheme = { const finaltheme = {
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm, algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
...defaultsDeep(currentTheme, defaultTheme) ...defaultsDeep(currentTheme, defaultTheme)
} };
export default finaltheme; export default finaltheme;

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -131,4 +131,4 @@
"author": "iconkitchen", "author": "iconkitchen",
"version": 1 "version": 1
} }
} }

View File

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

View File

@@ -1,31 +1,30 @@
import {Button} from "antd"; import { Button } from "antd";
import React from "react"; import React from "react";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {setModalContext} from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
const mapStateToProps = createStructuredSelector({}); const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setRefundPaymentContext: (context) => setRefundPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "refund_payment" }))
dispatch(setModalContext({context: context, modal: "refund_payment"})),
}); });
function Test({setRefundPaymentContext, refundPaymentModal}) { function Test({ setRefundPaymentContext, refundPaymentModal }) {
console.log("refundPaymentModal", refundPaymentModal); console.log("refundPaymentModal", refundPaymentModal);
return ( return (
<div> <div>
<Button <Button
onClick={() => onClick={() =>
setRefundPaymentContext({ setRefundPaymentContext({
context: {}, context: {}
}) })
} }
> >
Open Modal Open Modal
</Button> </Button>
</div> </div>
); );
} }
export default connect(mapStateToProps, mapDispatchToProps)(Test); export default connect(mapStateToProps, mapDispatchToProps)(Test);

View File

@@ -1,233 +1,196 @@
import {Card, Checkbox, Input, Space, Table} from "antd"; import { Card, Checkbox, Input, Space, Table } from "antd";
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import {Link} from "react-router-dom";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import {alphaSort, dateSort} from "../../utils/sorters";
import PayableExportButton from "../payable-export-button/payable-export-button.component";
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
import {DateFormatter} from "../../utils/DateFormatter";
import queryString from "query-string"; import queryString from "query-string";
import {logImEXEvent} from "../../firebase/firebase.utils"; import React, { useState } from "react";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; import { useTranslation } from "react-i18next";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {createStructuredSelector} from "reselect"; import { Link } from "react-router-dom";
import {selectBodyshop} from "../../redux/user/user.selectors"; import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component"; import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
import PayableExportButton from "../payable-export-button/payable-export-button.component";
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component"; import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
import {pageLimit} from "../../utils/config"; import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(AccountingPayablesTableComponent);
mapStateToProps,
mapDispatchToProps
)(AccountingPayablesTableComponent);
export function AccountingPayablesTableComponent({ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, refetch }) {
bodyshop, const { t } = useTranslation();
loading, const [selectedBills, setSelectedBills] = useState([]);
bills, const [transInProgress, setTransInProgress] = useState(false);
refetch, const [state, setState] = useState({
}) { sortedInfo: {},
const {t} = useTranslation(); search: ""
const [selectedBills, setSelectedBills] = useState([]); });
const [transInProgress, setTransInProgress] = useState(false);
const [state, setState] = useState({
sortedInfo: {},
search: "",
});
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({...state, filteredInfo: filters, sortedInfo: sorter}); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
}; };
const columns = [ const columns = [
{ {
title: t("bills.fields.vendorname"), title: t("bills.fields.vendorname"),
dataIndex: "vendorname", dataIndex: "vendorname",
key: "vendorname", key: "vendorname",
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name), sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
sortOrder: sortOrder: state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order, render: (text, record) => (
render: (text, record) => ( <Link
<Link to={{
to={{ pathname: `/manage/shop/vendors`,
pathname: `/manage/shop/vendors`, search: queryString.stringify({ selectedvendor: record.vendor.id })
search: queryString.stringify({selectedvendor: record.vendor.id}), }}
}}
>
{record.vendor.name}
</Link>
),
},
{
title: t("bills.fields.invoice_number"),
dataIndex: "invoice_number",
key: "invoice_number",
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
sortOrder:
state.sortedInfo.columnKey === "invoice_number" &&
state.sortedInfo.order,
render: (text, record) => (
<Link
to={{
pathname: `/manage/bills`,
search: queryString.stringify({
billid: record.id,
vendorid: record.vendor.id,
}),
}}
>
{record.invoice_number}
</Link>
),
},
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
<Link to={`/manage/jobs/${record.job.id}`}>{record.job.ro_number}</Link>
),
},
{
title: t("bills.fields.date"),
dataIndex: "date",
key: "date",
sorter: (a, b) => dateSort(a.date, b.date),
sortOrder:
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
},
{
title: t("bills.fields.total"),
dataIndex: "total",
key: "total",
sorter: (a, b) => a.total - b.total,
sortOrder:
state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.total}</CurrencyFormatter>
),
},
{
title: t("bills.fields.is_credit_memo"),
dataIndex: "is_credit_memo",
key: "is_credit_memo",
sorter: (a, b) => a.is_credit_memo - b.is_credit_memo,
sortOrder:
state.sortedInfo.columnKey === "is_credit_memo" &&
state.sortedInfo.order,
render: (text, record) => (
<Checkbox disabled checked={record.is_credit_memo}/>
),
},
{
title: t("exportlogs.labels.attempts"),
dataIndex: "attempts",
key: "attempts",
render: (text, record) => (
<ExportLogsCountDisplay logs={record.exportlogs}/>
),
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
sorter: (a, b) => a.clm_total - b.clm_total,
render: (text, record) => (
<PayableExportButton
billId={record.id}
disabled={transInProgress || !!record.exported}
loadingCallback={setTransInProgress}
setSelectedBills={setSelectedBills}
refetch={refetch}
/>
),
},
];
const handleSearch = (e) => {
setState({...state, search: e.target.value});
logImEXEvent("accounting_payables_table_search");
};
const dataSource = state.search
? bills.filter(
(v) =>
(v.vendor.name || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(v.invoice_number || "")
.toLowerCase()
.includes(state.search.toLowerCase())
)
: bills;
return (
<Card
extra={
<Space wrap>
<BillMarkSelectedExported
billids={selectedBills}
disabled={transInProgress || selectedBills.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedBills}
refetch={refetch}
/>
<PayableExportAll
billids={selectedBills}
disabled={transInProgress || selectedBills.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedBills}
refetch={refetch}
/>
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
<QboAuthorizeComponent/>
)}
<Input
value={state.search}
onChange={handleSearch}
placeholder={t("general.labels.search")}
allowClear
/>
</Space>
}
> >
<Table {record.vendor.name}
loading={loading} </Link>
dataSource={dataSource} )
pagination={{position: "top", pageSize: pageLimit}} },
columns={columns} {
rowKey="id" title: t("bills.fields.invoice_number"),
onChange={handleTableChange} dataIndex: "invoice_number",
rowSelection={{ key: "invoice_number",
onSelectAll: (selected, selectedRows) => sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
setSelectedBills(selectedRows.map((i) => i.id)), sortOrder: state.sortedInfo.columnKey === "invoice_number" && state.sortedInfo.order,
onSelect: (record, selected, selectedRows, nativeEvent) => { render: (text, record) => (
setSelectedBills(selectedRows.map((i) => i.id)); <Link
}, to={{
getCheckboxProps: (record) => ({ pathname: `/manage/bills`,
disabled: record.exported, search: queryString.stringify({
}), billid: record.id,
selectedRowKeys: selectedBills, vendorid: record.vendor.id
type: "checkbox", })
}} }}
/> >
</Card> {record.invoice_number}
); </Link>
)
},
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => <Link to={`/manage/jobs/${record.job.id}`}>{record.job.ro_number}</Link>
},
{
title: t("bills.fields.date"),
dataIndex: "date",
key: "date",
sorter: (a, b) => dateSort(a.date, b.date),
sortOrder: state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>
},
{
title: t("bills.fields.total"),
dataIndex: "total",
key: "total",
sorter: (a, b) => a.total - b.total,
sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
render: (text, record) => <CurrencyFormatter>{record.total}</CurrencyFormatter>
},
{
title: t("bills.fields.is_credit_memo"),
dataIndex: "is_credit_memo",
key: "is_credit_memo",
sorter: (a, b) => a.is_credit_memo - b.is_credit_memo,
sortOrder: state.sortedInfo.columnKey === "is_credit_memo" && state.sortedInfo.order,
render: (text, record) => <Checkbox disabled checked={record.is_credit_memo} />
},
{
title: t("exportlogs.labels.attempts"),
dataIndex: "attempts",
key: "attempts",
render: (text, record) => <ExportLogsCountDisplay logs={record.exportlogs} />
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<PayableExportButton
billId={record.id}
disabled={transInProgress || !!record.exported}
loadingCallback={setTransInProgress}
setSelectedBills={setSelectedBills}
refetch={refetch}
/>
)
}
];
const handleSearch = (e) => {
setState({ ...state, search: e.target.value });
logImEXEvent("accounting_payables_table_search");
};
const dataSource = state.search
? bills.filter(
(v) =>
(v.vendor.name || "").toLowerCase().includes(state.search.toLowerCase()) ||
(v.invoice_number || "").toLowerCase().includes(state.search.toLowerCase())
)
: bills;
return (
<Card
extra={
<Space wrap>
<BillMarkSelectedExported
billids={selectedBills}
disabled={transInProgress || selectedBills.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedBills}
refetch={refetch}
/>
<PayableExportAll
billids={selectedBills}
disabled={transInProgress || selectedBills.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedBills}
refetch={refetch}
/>
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
<Input value={state.search} onChange={handleSearch} placeholder={t("general.labels.search")} allowClear />
</Space>
}
>
<Table
loading={loading}
dataSource={dataSource}
pagination={{ position: "top", pageSize: pageLimit }}
columns={columns}
rowKey="id"
onChange={handleTableChange}
rowSelection={{
onSelectAll: (selected, selectedRows) => setSelectedBills(selectedRows.map((i) => i.id)),
onSelect: (record, selected, selectedRows, nativeEvent) => {
setSelectedBills(selectedRows.map((i) => i.id));
},
getCheckboxProps: (record) => ({
disabled: record.exported
}),
selectedRowKeys: selectedBills,
type: "checkbox"
}}
/>
</Card>
);
} }

View File

@@ -1,232 +1,198 @@
import {Card, Input, Space, Table} from "antd"; import { Card, Input, Space, Table } from "antd";
import React, {useState} from "react"; import React, { useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {Link} from "react-router-dom"; import { Link } from "react-router-dom";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {logImEXEvent} from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import {selectBodyshop} from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import {DateFormatter, DateTimeFormatter} from "../../utils/DateFormatter"; import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import {alphaSort, dateSort} from "../../utils/sorters"; import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component"; import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import PaymentExportButton from "../payment-export-button/payment-export-button.component"; import PaymentExportButton from "../payment-export-button/payment-export-button.component";
import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component"; 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 PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(AccountingPayablesTableComponent);
mapStateToProps,
mapDispatchToProps
)(AccountingPayablesTableComponent);
export function AccountingPayablesTableComponent({ export function AccountingPayablesTableComponent({ bodyshop, loading, payments, refetch }) {
bodyshop, const { t } = useTranslation();
loading, const [selectedPayments, setSelectedPayments] = useState([]);
payments, const [transInProgress, setTransInProgress] = useState(false);
refetch, const [state, setState] = useState({
}) { sortedInfo: {},
const {t} = useTranslation(); search: ""
const [selectedPayments, setSelectedPayments] = useState([]); });
const [transInProgress, setTransInProgress] = useState(false);
const [state, setState] = useState({
sortedInfo: {},
search: "",
});
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({...state, filteredInfo: filters, sortedInfo: sorter}); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
}; };
const columns = [ const columns = [
{ {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number), sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
sortOrder: sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, render: (text, record) => <Link to={"/manage/jobs/" + record.job.id}>{record.job.ro_number}</Link>
render: (text, record) => ( },
<Link to={"/manage/jobs/" + record.job.id}>{record.job.ro_number}</Link> {
), title: t("payments.fields.date"),
}, dataIndex: "date",
{ key: "date",
title: t("payments.fields.date"), sorter: (a, b) => dateSort(a.date, b.date),
dataIndex: "date", sortOrder: state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
key: "date", render: (text, record) => <DateFormatter>{record.date}</DateFormatter>
sorter: (a, b) => dateSort(a.date, b.date), },
sortOrder:
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
},
{ {
title: t("jobs.fields.owner"), title: t("jobs.fields.owner"),
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
ellipsis: true, ellipsis: true,
sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln), sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a.job), OwnerNameDisplayFunction(b.job)),
sortOrder: sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, render: (text, record) => {
render: (text, record) => { return record.job.owner ? (
return record.job.owner ? ( <Link to={"/manage/owners/" + record.job.owner.id}>
<Link to={"/manage/owners/" + record.job.owner.id}> <OwnerNameDisplay ownerObject={record.job} />
<OwnerNameDisplay ownerObject={record.job}/> </Link>
</Link> ) : (
) : ( <span>
<span> <OwnerNameDisplay ownerObject={record.job} />
<OwnerNameDisplay ownerObject={record.job}/>
</span> </span>
); );
}, }
}, },
{ {
title: t("payments.fields.amount"), title: t("payments.fields.amount"),
dataIndex: "amount", dataIndex: "amount",
key: "amount", key: "amount",
render: (text, record) => ( sorter: (a, b) => a.amount - b.amount,
<CurrencyFormatter>{record.amount}</CurrencyFormatter> sortOrder: state.sortedInfo.columnKey === "amount" && state.sortedInfo.order,
), render: (text, record) => <CurrencyFormatter>{record.amount}</CurrencyFormatter>
}, },
{ {
title: t("payments.fields.memo"), title: t("payments.fields.memo"),
dataIndex: "memo", dataIndex: "memo",
key: "memo", key: "memo"
}, },
{ {
title: t("payments.fields.transactionid"), title: t("payments.fields.transactionid"),
dataIndex: "transactionid", dataIndex: "transactionid",
key: "transactionid", key: "transactionid"
}, },
{ {
title: t("payments.fields.created_at"), title: t("payments.fields.created_at"),
dataIndex: "created_at", dataIndex: "created_at",
key: "created_at", key: "created_at",
render: (text, record) => ( sorter: (a, b) => dateSort(a.created_at, b.created_at),
<DateTimeFormatter>{record.created_at}</DateTimeFormatter> sortOrder: state.sortedInfo.columnKey === "created_at" && state.sortedInfo.order,
), render: (text, record) => <DateTimeFormatter>{record.created_at}</DateTimeFormatter>
}, },
{ //{
title: t("payments.fields.exportedat"), // title: t("payments.fields.exportedat"),
dataIndex: "exportedat", // dataIndex: "exportedat",
key: "exportedat", // key: "exportedat",
render: (text, record) => ( // render: (text, record) => (
<DateTimeFormatter>{record.exportedat}</DateTimeFormatter> // <DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
), // ),
}, //},
{ {
title: t("exportlogs.labels.attempts"), title: t("exportlogs.labels.attempts"),
dataIndex: "attempts", dataIndex: "attempts",
key: "attempts", key: "attempts",
render: (text, record) => ( render: (text, record) => <ExportLogsCountDisplay logs={record.exportlogs} />
<ExportLogsCountDisplay logs={record.exportlogs}/> },
), {
}, title: t("general.labels.actions"),
{ dataIndex: "actions",
title: t("general.labels.actions"), key: "actions",
dataIndex: "actions",
key: "actions",
sorter: (a, b) => a.clm_total - b.clm_total,
render: (text, record) => ( render: (text, record) => (
<PaymentExportButton <PaymentExportButton
paymentId={record.id} paymentId={record.id}
disabled={transInProgress || !!record.exportedat} disabled={transInProgress || !!record.exportedat}
loadingCallback={setTransInProgress} loadingCallback={setTransInProgress}
setSelectedPayments={setSelectedPayments} setSelectedPayments={setSelectedPayments}
refetch={refetch} refetch={refetch}
/> />
), )
}, }
]; ];
const handleSearch = (e) => { const handleSearch = (e) => {
setState({...state, search: e.target.value}); setState({ ...state, search: e.target.value });
logImEXEvent("account_payments_table_search"); logImEXEvent("account_payments_table_search");
}; };
const dataSource = state.search const dataSource = state.search
? payments.filter( ? payments.filter(
(v) => (v) =>
(v.paymentnum || "") (v.paymentnum || "").toLowerCase().includes(state.search.toLowerCase()) ||
.toLowerCase() ((v.job && v.job.ro_number) || "").toLowerCase().includes(state.search.toLowerCase()) ||
.includes(state.search.toLowerCase()) || ((v.job && v.job.ownr_fn) || "").toLowerCase().includes(state.search.toLowerCase()) ||
((v.job && v.job.ro_number) || "") ((v.job && v.job.ownr_ln) || "").toLowerCase().includes(state.search.toLowerCase()) ||
.toLowerCase() ((v.job && v.job.ownr_co_nm) || "").toLowerCase().includes(state.search.toLowerCase())
.includes(state.search.toLowerCase()) || )
((v.job && v.job.ownr_fn) || "") : payments;
.toLowerCase()
.includes(state.search.toLowerCase()) ||
((v.job && v.job.ownr_ln) || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
((v.job && v.job.ownr_co_nm) || "")
.toLowerCase()
.includes(state.search.toLowerCase())
)
: payments;
return ( return (
<Card <Card
extra={ extra={
<Space wrap> <Space wrap>
<PaymentMarkSelectedExported <PaymentMarkSelectedExported
paymentIds={selectedPayments} paymentIds={selectedPayments}
disabled={transInProgress || selectedPayments.length === 0} disabled={transInProgress || selectedPayments.length === 0}
loadingCallback={setTransInProgress} loadingCallback={setTransInProgress}
completedCallback={setSelectedPayments} completedCallback={setSelectedPayments}
refetch={refetch} refetch={refetch}
/> />
<PaymentsExportAllButton <PaymentsExportAllButton
paymentIds={selectedPayments} paymentIds={selectedPayments}
disabled={transInProgress || selectedPayments.length === 0} disabled={transInProgress || selectedPayments.length === 0}
loadingCallback={setTransInProgress} loadingCallback={setTransInProgress}
completedCallback={setSelectedPayments} completedCallback={setSelectedPayments}
refetch={refetch} refetch={refetch}
/> />
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && ( {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
<QboAuthorizeComponent/> <Input value={state.search} onChange={handleSearch} placeholder={t("general.labels.search")} allowClear />
)} </Space>
<Input }
value={state.search} >
onChange={handleSearch} <Table
placeholder={t("general.labels.search")} loading={loading}
allowClear dataSource={dataSource}
/> pagination={{ position: "top", pageSize: pageLimit }}
</Space> columns={columns}
} rowKey="id"
> onChange={handleTableChange}
<Table rowSelection={{
loading={loading} onSelectAll: (selected, selectedRows) => setSelectedPayments(selectedRows.map((i) => i.id)),
dataSource={dataSource} onSelect: (record, selected, selectedRows, nativeEvent) => {
pagination={{position: "top", pageSize: pageLimit}} setSelectedPayments(selectedRows.map((i) => i.id));
columns={columns} },
rowKey="id" getCheckboxProps: (record) => ({
onChange={handleTableChange} disabled: record.exported
rowSelection={{ }),
onSelectAll: (selected, selectedRows) => selectedRowKeys: selectedPayments,
setSelectedPayments(selectedRows.map((i) => i.id)), type: "checkbox"
onSelect: (record, selected, selectedRows, nativeEvent) => { }}
setSelectedPayments(selectedRows.map((i) => i.id)); />
}, </Card>
getCheckboxProps: (record) => ({ );
disabled: record.exported,
}),
selectedRowKeys: selectedPayments,
type: "checkbox",
}}
/>
</Card>
);
} }

View File

@@ -1,247 +1,212 @@
import {Button, Card, Input, Space, Table} from "antd"; import { Button, Card, Input, Space, Table } from "antd";
import React, {useState} from "react"; import React, { useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {Link} from "react-router-dom"; import { Link } from "react-router-dom";
import {logImEXEvent} from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import {alphaSort, dateSort} from "../../utils/sorters"; import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component"; import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component"; import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {selectBodyshop} from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; import { DateFormatter } from "../../utils/DateFormatter";
import {DateFormatter} from "../../utils/DateFormatter";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component"; import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(AccountingReceivablesTableComponent);
mapStateToProps,
mapDispatchToProps
)(AccountingReceivablesTableComponent);
export function AccountingReceivablesTableComponent({ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, refetch }) {
bodyshop, const { t } = useTranslation();
loading, const [selectedJobs, setSelectedJobs] = useState([]);
jobs, const [transInProgress, setTransInProgress] = useState(false);
refetch,
}) {
const {t} = useTranslation();
const [selectedJobs, setSelectedJobs] = useState([]);
const [transInProgress, setTransInProgress] = useState(false);
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
search: "", search: ""
}); });
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({...state, filteredInfo: filters, sortedInfo: sorter}); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
}; };
const columns = [ const columns = [
{ {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder: sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, render: (text, record) => <Link to={"/manage/jobs/" + record.id}>{record.ro_number}</Link>
render: (text, record) => ( },
<Link to={"/manage/jobs/" + record.id}>{record.ro_number}</Link>
),
},
{ {
title: t("jobs.fields.status"), title: t("jobs.fields.status"),
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
sorter: (a, b) => a.status - b.status, sorter: (a, b) => statusSort(a, b, bodyshop.md_ro_statuses.statuses),
sortOrder: sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order
state.sortedInfo.columnKey === "status" && state.sortedInfo.order, },
}, {
{ title: t("jobs.fields.date_invoiced"),
title: t("jobs.fields.date_invoiced"), dataIndex: "date_invoiced",
dataIndex: "date_invoiced", key: "date_invoiced",
key: "date_invoiced", sorter: (a, b) => dateSort(a.date_invoiced, b.date_invoiced),
sorter: (a, b) => dateSort(a.date_invoiced, b.date_invoiced), sortOrder: state.sortedInfo.columnKey === "date_invoiced" && state.sortedInfo.order,
sortOrder: render: (text, record) => <DateFormatter>{record.date_invoiced}</DateFormatter>
state.sortedInfo.columnKey === "date_invoiced" && },
state.sortedInfo.order, {
render: (text, record) => ( title: t("jobs.fields.owner"),
<DateFormatter>{record.date_invoiced}</DateFormatter> dataIndex: "owner",
), key: "owner",
}, sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
{ sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
title: t("jobs.fields.owner"), render: (text, record) => {
dataIndex: "owner", return record.owner ? (
key: "owner", <Link to={"/manage/owners/" + record.owner.id}>
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), <OwnerNameDisplay ownerObject={record} />
sortOrder: </Link>
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, ) : (
render: (text, record) => { <span>
return record.owner ? ( <OwnerNameDisplay ownerObject={record} />
<Link to={"/manage/owners/" + record.owner.id}>
<OwnerNameDisplay ownerObject={record}/>
</Link>
) : (
<span>
<OwnerNameDisplay ownerObject={record}/>
</span> </span>
); );
}, }
}, },
{ {
title: t("jobs.fields.vehicle"), title: t("jobs.fields.vehicle"),
dataIndex: "vehicle", dataIndex: "vehicle",
key: "vehicle", key: "vehicle",
ellipsis: true, ellipsis: true,
render: (text, record) => { sorter: (a, b) =>
return record.vehicleid ? ( alphaSort(
<Link to={"/manage/vehicles/" + record.vehicleid}> `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${a.v_model_desc || ""}`,
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ `${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
record.v_model_desc || "" ),
}`} sortOrder: state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
</Link> render: (text, record) => {
) : ( return record.vehicleid ? (
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ <Link to={"/manage/vehicles/" + record.vehicleid}>
record.v_model_desc || "" {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}
}`}</span> </Link>
); ) : (
}, <span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
}, );
{ }
title: t("jobs.fields.clm_no"), },
dataIndex: "clm_no", {
key: "clm_no", title: t("jobs.fields.clm_no"),
ellipsis: true, dataIndex: "clm_no",
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no), key: "clm_no",
sortOrder: ellipsis: true,
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order, sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
}, sortOrder: state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order
{ },
title: t("jobs.fields.clm_total"), {
dataIndex: "clm_total", title: t("jobs.fields.clm_total"),
key: "clm_total", dataIndex: "clm_total",
sorter: (a, b) => a.clm_total - b.clm_total, key: "clm_total",
sortOrder: sorter: (a, b) => a.clm_total - b.clm_total,
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order, sortOrder: state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return <CurrencyFormatter>{record.clm_total}</CurrencyFormatter>; return <CurrencyFormatter>{record.clm_total}</CurrencyFormatter>;
}, }
}, },
{ {
title: t("exportlogs.labels.attempts"), title: t("exportlogs.labels.attempts"),
dataIndex: "attempts", dataIndex: "attempts",
key: "attempts", key: "attempts",
render: (text, record) => ( render: (text, record) => <ExportLogsCountDisplay logs={record.exportlogs} />
<ExportLogsCountDisplay logs={record.exportlogs}/> },
), {
}, title: t("general.labels.actions"),
{ dataIndex: "actions",
title: t("general.labels.actions"), key: "actions",
dataIndex: "actions",
key: "actions",
render: (text, record) => ( render: (text, record) => (
<Space wrap> <Space wrap>
<JobExportButton <JobExportButton
jobId={record.id} jobId={record.id}
disabled={!!record.date_exported} disabled={!!record.date_exported}
setSelectedJobs={setSelectedJobs} setSelectedJobs={setSelectedJobs}
refetch={refetch} refetch={refetch}
/> />
<Link to={`/manage/jobs/${record.id}/close`}> <Link to={`/manage/jobs/${record.id}/close`}>
<Button>{t("jobs.labels.viewallocations")}</Button> <Button>{t("jobs.labels.viewallocations")}</Button>
</Link> </Link>
</Space> </Space>
), )
}, }
]; ];
const handleSearch = (e) => { const handleSearch = (e) => {
setState({...state, search: e.target.value}); setState({ ...state, search: e.target.value });
logImEXEvent("accounting_receivables_search"); logImEXEvent("accounting_receivables_search");
}; };
const dataSource = state.search const dataSource = state.search
? jobs.filter( ? jobs.filter(
(v) => (v) =>
(v.ro_number || "") (v.ro_number || "").toString().toLowerCase().includes(state.search.toLowerCase()) ||
.toString() (v.ownr_fn || "").toLowerCase().includes(state.search.toLowerCase()) ||
.toLowerCase() (v.ownr_ln || "").toLowerCase().includes(state.search.toLowerCase()) ||
.includes(state.search.toLowerCase()) || (v.ownr_co_nm || "").toLowerCase().includes(state.search.toLowerCase()) ||
(v.ownr_fn || "") (v.v_model_desc || "").toLowerCase().includes(state.search.toLowerCase()) ||
.toLowerCase() (v.v_make_desc || "").toLowerCase().includes(state.search.toLowerCase()) ||
.includes(state.search.toLowerCase()) || (v.clm_no || "").toLowerCase().includes(state.search.toLowerCase())
(v.ownr_ln || "") )
.toLowerCase() : jobs;
.includes(state.search.toLowerCase()) ||
(v.ownr_co_nm || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(v.v_model_desc || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(v.v_make_desc || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(v.clm_no || "").toLowerCase().includes(state.search.toLowerCase())
)
: jobs;
return ( return (
<Card <Card
extra={ extra={
<Space wrap> <Space wrap>
{!bodyshop.cdk_dealerid && !bodyshop.pbs_serialnumber && ( {!bodyshop.cdk_dealerid && !bodyshop.pbs_serialnumber && (
<JobsExportAllButton <JobsExportAllButton
jobIds={selectedJobs} jobIds={selectedJobs}
disabled={transInProgress || selectedJobs.length === 0} disabled={transInProgress || selectedJobs.length === 0}
loadingCallback={setTransInProgress} loadingCallback={setTransInProgress}
completedCallback={setSelectedJobs} completedCallback={setSelectedJobs}
refetch={refetch} refetch={refetch}
/>
)}
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
<QboAuthorizeComponent/>
)}
<Input.Search
value={state.search}
onChange={handleSearch}
placeholder={t("general.labels.search")}
allowClear
/>
</Space>
}
>
<Table
loading={loading}
dataSource={dataSource}
pagination={{position: "top"}}
columns={columns}
rowKey="id"
onChange={handleTableChange}
rowSelection={{
onSelectAll: (selected, selectedRows) =>
setSelectedJobs(selectedRows.map((i) => i.id)),
onSelect: (record, selected, selectedRows, nativeEvent) => {
setSelectedJobs(selectedRows.map((i) => i.id));
},
getCheckboxProps: (record) => ({
disabled: record.exported,
}),
selectedRowKeys: selectedJobs,
type: "checkbox",
}}
/> />
</Card> )}
); {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
<Input.Search
value={state.search}
onChange={handleSearch}
placeholder={t("general.labels.search")}
allowClear
/>
</Space>
}
>
<Table
loading={loading}
dataSource={dataSource}
pagination={{ position: "top" }}
columns={columns}
rowKey="id"
onChange={handleTableChange}
rowSelection={{
onSelectAll: (selected, selectedRows) => setSelectedJobs(selectedRows.map((i) => i.id)),
onSelect: (record, selected, selectedRows, nativeEvent) => {
setSelectedJobs(selectedRows.map((i) => i.id));
},
getCheckboxProps: (record) => ({
disabled: record.exported
}),
selectedRowKeys: selectedJobs,
type: "checkbox"
}}
/>
</Card>
);
} }

View File

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

View File

@@ -1,19 +1,19 @@
import {shallow} from "enzyme"; import { shallow } from "enzyme";
import React from "react"; import React from "react";
import Alert from "./alert.component"; import Alert from "./alert.component";
describe("Alert component", () => { describe("Alert component", () => {
let wrapper; let wrapper;
beforeEach(() => { beforeEach(() => {
const mockProps = { const mockProps = {
type: "error", type: "error",
message: "Test error message.", message: "Test error message."
}; };
wrapper = shallow(<Alert {...mockProps} />); wrapper = shallow(<Alert {...mockProps} />);
}); });
it("should render Alert component", () => { it("should render Alert component", () => {
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
}); });

View File

@@ -1,72 +1,67 @@
import {Button, InputNumber, Popover, Select} from "antd"; import { Button, InputNumber, Popover, Select } from "antd";
import React from "react"; import React from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {selectBodyshop} from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
}); });
export function AllocationsAssignmentComponent({ export function AllocationsAssignmentComponent({
bodyshop, bodyshop,
handleAssignment, handleAssignment,
assignment, assignment,
setAssignment, setAssignment,
visibilityState, visibilityState,
maxHours maxHours
}) { }) {
const {t} = useTranslation(); const { t } = useTranslation();
const onChange = e => { const onChange = (e) => {
setAssignment({...assignment, employeeid: e}); setAssignment({ ...assignment, employeeid: e });
}; };
const [visibility, setVisibility] = visibilityState; const [visibility, setVisibility] = visibilityState;
const popContent = ( const popContent = (
<div> <div>
<Select id="employeeSelector" <Select
showSearch id="employeeSelector"
style={{width: 200}} showSearch
placeholder='Select a person' style={{ width: 200 }}
optionFilterProp='children' placeholder="Select a person"
onChange={onChange} optionFilterProp="children"
filterOption={(input, option) => onChange={onChange}
option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0 filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
}> >
{bodyshop.employees.map(emp => ( {bodyshop.employees.map((emp) => (
<Select.Option value={emp.id} key={emp.id}> <Select.Option value={emp.id} key={emp.id}>
{`${emp.first_name} ${emp.last_name}`} {`${emp.first_name} ${emp.last_name}`}
</Select.Option> </Select.Option>
))} ))}
</Select> </Select>
<InputNumber <InputNumber
defaultValue={assignment.hours} defaultValue={assignment.hours}
placeholder={t("joblines.fields.mod_lb_hrs")} placeholder={t("joblines.fields.mod_lb_hrs")}
max={parseFloat(maxHours)} max={parseFloat(maxHours)}
min={0} min={0}
onChange={e => setAssignment({...assignment, hours: e})} onChange={(e) => setAssignment({ ...assignment, hours: e })}
/> />
<Button <Button type="primary" disabled={!assignment.employeeid} onClick={handleAssignment}>
type='primary' Assign
disabled={!assignment.employeeid} </Button>
onClick={handleAssignment}> <Button onClick={() => setVisibility(false)}>Close</Button>
Assign </div>
</Button> );
<Button onClick={() => setVisibility(false)}>Close</Button>
</div>
);
return ( return (
<Popover content={popContent} open={visibility}> <Popover content={popContent} open={visibility}>
<Button onClick={() => setVisibility(true)}> <Button onClick={() => setVisibility(true)}>{t("allocations.actions.assign")}</Button>
{t("allocations.actions.assign")} </Popover>
</Button> );
</Popover>
);
} }
export default connect(mapStateToProps, null)(AllocationsAssignmentComponent); export default connect(mapStateToProps, null)(AllocationsAssignmentComponent);

View File

@@ -1,35 +1,35 @@
import {mount} from "enzyme"; import { mount } from "enzyme";
import React from "react"; import React from "react";
import {MockBodyshop} from "../../utils/TestingHelpers"; import { MockBodyshop } from "../../utils/TestingHelpers";
import {AllocationsAssignmentComponent} from "./allocations-assignment.component"; import { AllocationsAssignmentComponent } from "./allocations-assignment.component";
describe("AllocationsAssignmentComponent component", () => { describe("AllocationsAssignmentComponent component", () => {
let wrapper; let wrapper;
beforeEach(() => { beforeEach(() => {
const mockProps = { const mockProps = {
bodyshop: MockBodyshop, bodyshop: MockBodyshop,
handleAssignment: jest.fn(), handleAssignment: jest.fn(),
assignment: {}, assignment: {},
setAssignment: jest.fn(), setAssignment: jest.fn(),
visibilityState: [false, jest.fn()], visibilityState: [false, jest.fn()],
maxHours: 4, maxHours: 4
}; };
wrapper = mount(<AllocationsAssignmentComponent {...mockProps} />); wrapper = mount(<AllocationsAssignmentComponent {...mockProps} />);
}); });
it("should render AllocationsAssignmentComponent component", () => { it("should render AllocationsAssignmentComponent component", () => {
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it("should render a list of employees", () => { it("should render a list of employees", () => {
const empList = wrapper.find("#employeeSelector"); const empList = wrapper.find("#employeeSelector");
expect(empList.children()).to.have.lengthOf(2); expect(empList.children()).to.have.lengthOf(2);
}); });
it("should create an allocation on save", () => { it("should create an allocation on save", () => {
wrapper.find("Button").simulate("click"); wrapper.find("Button").simulate("click");
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
}); });

View File

@@ -1,47 +1,43 @@
import React, {useState} from "react"; import React, { useState } from "react";
import AllocationsAssignmentComponent from "./allocations-assignment.component"; import AllocationsAssignmentComponent from "./allocations-assignment.component";
import {useMutation} from "@apollo/client"; import { useMutation } from "@apollo/client";
import {INSERT_ALLOCATION} from "../../graphql/allocations.queries"; import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {notification} from "antd"; import { notification } from "antd";
export default function AllocationsAssignmentContainer({ export default function AllocationsAssignmentContainer({ jobLineId, hours, refetch }) {
jobLineId, const visibilityState = useState(false);
hours, const { t } = useTranslation();
refetch, const [assignment, setAssignment] = useState({
}) { joblineid: jobLineId,
const visibilityState = useState(false); hours: parseFloat(hours),
const {t} = useTranslation(); employeeid: null
const [assignment, setAssignment] = useState({ });
joblineid: jobLineId, const [insertAllocation] = useMutation(INSERT_ALLOCATION);
hours: parseFloat(hours),
employeeid: null,
});
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
const handleAssignment = () => { const handleAssignment = () => {
insertAllocation({variables: {alloc: {...assignment}}}) insertAllocation({ variables: { alloc: { ...assignment } } })
.then((r) => { .then((r) => {
notification["success"]({ notification["success"]({
message: t("allocations.successes.save"), message: t("allocations.successes.save")
}); });
visibilityState[1](false); visibilityState[1](false);
if (refetch) refetch(); if (refetch) refetch();
}) })
.catch((error) => { .catch((error) => {
notification["error"]({ notification["error"]({
message: t("employees.errors.saving", {message: error.message}), message: t("employees.errors.saving", { message: error.message })
}); });
}); });
}; };
return ( return (
<AllocationsAssignmentComponent <AllocationsAssignmentComponent
handleAssignment={handleAssignment} handleAssignment={handleAssignment}
maxHours={hours} maxHours={hours}
assignment={assignment} assignment={assignment}
setAssignment={setAssignment} setAssignment={setAssignment}
visibilityState={visibilityState} visibilityState={visibilityState}
/> />
); );
} }

View File

@@ -1,68 +1,62 @@
import {Button, Popover, Select} from "antd"; import { Button, Popover, Select } from "antd";
import React from "react"; import React from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {selectBodyshop} from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
export default connect( export default connect(
mapStateToProps, mapStateToProps,
null null
)(function AllocationsBulkAssignmentComponent({ )(function AllocationsBulkAssignmentComponent({
disabled, disabled,
bodyshop, bodyshop,
handleAssignment, handleAssignment,
assignment, assignment,
setAssignment, setAssignment,
visibilityState, visibilityState
}) { }) {
const {t} = useTranslation(); const { t } = useTranslation();
const onChange = (e) => { const onChange = (e) => {
setAssignment({...assignment, employeeid: e}); setAssignment({ ...assignment, employeeid: e });
}; };
const [visibility, setVisibility] = visibilityState; const [visibility, setVisibility] = visibilityState;
const popContent = ( const popContent = (
<div> <div>
<Select <Select
showSearch showSearch
style={{width: 200}} style={{ width: 200 }}
placeholder="Select a person" placeholder="Select a person"
optionFilterProp="children" optionFilterProp="children"
onChange={onChange} onChange={onChange}
filterOption={(input, option) => filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0 >
} {bodyshop.employees.map((emp) => (
> <Select.Option value={emp.id} key={emp.id}>
{bodyshop.employees.map((emp) => ( {`${emp.first_name} ${emp.last_name}`}
<Select.Option value={emp.id} key={emp.id}> </Select.Option>
{`${emp.first_name} ${emp.last_name}`} ))}
</Select.Option> </Select>
))}
</Select>
<Button <Button type="primary" disabled={!assignment.employeeid} onClick={handleAssignment}>
type="primary" Assign
disabled={!assignment.employeeid} </Button>
onClick={handleAssignment} <Button onClick={() => setVisibility(false)}>Close</Button>
> </div>
Assign );
</Button>
<Button onClick={() => setVisibility(false)}>Close</Button>
</div>
);
return ( return (
<Popover content={popContent} open={visibility}> <Popover content={popContent} open={visibility}>
<Button disabled={disabled} onClick={() => setVisibility(true)}> <Button disabled={disabled} onClick={() => setVisibility(true)}>
{t("allocations.actions.assign")} {t("allocations.actions.assign")}
</Button> </Button>
</Popover> </Popover>
); );
}); });

View File

@@ -1,47 +1,44 @@
import React, {useState} from "react"; import React, { useState } from "react";
import AllocationsBulkAssignment from "./allocations-bulk-assignment.component"; import AllocationsBulkAssignment from "./allocations-bulk-assignment.component";
import {useMutation} from "@apollo/client"; import { useMutation } from "@apollo/client";
import {INSERT_ALLOCATION} from "../../graphql/allocations.queries"; import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {notification} from "antd"; import { notification } from "antd";
export default function AllocationsBulkAssignmentContainer({ export default function AllocationsBulkAssignmentContainer({ jobLines, refetch }) {
jobLines, const visibilityState = useState(false);
refetch, const { t } = useTranslation();
}) { const [assignment, setAssignment] = useState({
const visibilityState = useState(false); employeeid: null
const {t} = useTranslation(); });
const [assignment, setAssignment] = useState({ const [insertAllocation] = useMutation(INSERT_ALLOCATION);
employeeid: null,
const handleAssignment = () => {
const allocs = jobLines.reduce((acc, value) => {
acc.push({
joblineid: value.id,
hours: parseFloat(value.mod_lb_hrs) || 0,
employeeid: assignment.employeeid
});
return acc;
}, []);
insertAllocation({ variables: { alloc: allocs } }).then((r) => {
notification["success"]({
message: t("employees.successes.save")
});
visibilityState[1](false);
if (refetch) refetch();
}); });
const [insertAllocation] = useMutation(INSERT_ALLOCATION); };
const handleAssignment = () => { return (
const allocs = jobLines.reduce((acc, value) => { <AllocationsBulkAssignment
acc.push({ disabled={jobLines.length > 0 ? false : true}
joblineid: value.id, handleAssignment={handleAssignment}
hours: parseFloat(value.mod_lb_hrs) || 0, assignment={assignment}
employeeid: assignment.employeeid, setAssignment={setAssignment}
}); visibilityState={visibilityState}
return acc; />
}, []); );
insertAllocation({variables: {alloc: allocs}}).then((r) => {
notification["success"]({
message: t("employees.successes.save"),
});
visibilityState[1](false);
if (refetch) refetch();
});
};
return (
<AllocationsBulkAssignment
disabled={jobLines.length > 0 ? false : true}
handleAssignment={handleAssignment}
assignment={assignment}
setAssignment={setAssignment}
visibilityState={visibilityState}
/>
);
} }

View File

@@ -1,20 +1,14 @@
import Icon from "@ant-design/icons"; import Icon from "@ant-design/icons";
import React from "react"; import React from "react";
import {MdRemoveCircleOutline} from "react-icons/md"; import { MdRemoveCircleOutline } from "react-icons/md";
export default function AllocationsLabelComponent({allocation, handleClick}) { export default function AllocationsLabelComponent({ allocation, handleClick }) {
return ( return (
<div style={{display: "flex", alignItems: "center"}}> <div style={{ display: "flex", alignItems: "center" }}>
<span> <span>
{`${allocation.employee.first_name || ""} ${ {`${allocation.employee.first_name || ""} ${allocation.employee.last_name || ""} (${allocation.hours || ""})`}
allocation.employee.last_name || ""
} (${allocation.hours || ""})`}
</span> </span>
<Icon <Icon style={{ color: "red", padding: "0px 4px" }} component={MdRemoveCircleOutline} onClick={handleClick} />
style={{color: "red", padding: "0px 4px"}} </div>
component={MdRemoveCircleOutline} );
onClick={handleClick}
/>
</div>
);
} }

View File

@@ -1,32 +1,27 @@
import React from "react"; import React from "react";
import {useMutation} from "@apollo/client"; import { useMutation } from "@apollo/client";
import {DELETE_ALLOCATION} from "../../graphql/allocations.queries"; import { DELETE_ALLOCATION } from "../../graphql/allocations.queries";
import AllocationsLabelComponent from "./allocations-employee-label.component"; import AllocationsLabelComponent from "./allocations-employee-label.component";
import {notification} from "antd"; import { notification } from "antd";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
export default function AllocationsLabelContainer({allocation, refetch}) { export default function AllocationsLabelContainer({ allocation, refetch }) {
const [deleteAllocation] = useMutation(DELETE_ALLOCATION); const [deleteAllocation] = useMutation(DELETE_ALLOCATION);
const {t} = useTranslation(); const { t } = useTranslation();
const handleClick = (e) => { const handleClick = (e) => {
e.preventDefault(); e.preventDefault();
deleteAllocation({variables: {id: allocation.id}}) deleteAllocation({ variables: { id: allocation.id } })
.then((r) => { .then((r) => {
notification["success"]({ notification["success"]({
message: t("allocations.successes.deleted"), message: t("allocations.successes.deleted")
}); });
if (refetch) refetch(); if (refetch) refetch();
}) })
.catch((error) => { .catch((error) => {
notification["error"]({message: t("allocations.errors.deleting")}); notification["error"]({ message: t("allocations.errors.deleting") });
}); });
}; };
return ( return <AllocationsLabelComponent allocation={allocation} handleClick={handleClick} />;
<AllocationsLabelComponent
allocation={allocation}
handleClick={handleClick}
/>
);
} }

View File

@@ -1,85 +1,75 @@
import React, {useState} from "react"; import React, { useState } from "react";
import {Table} from "antd"; import { Table } from "antd";
import {alphaSort} from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import {DateTimeFormatter} from "../../utils/DateFormatter"; import { DateTimeFormatter } from "../../utils/DateFormatter";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import AuditTrailValuesComponent from "../audit-trail-values/audit-trail-values.component"; import AuditTrailValuesComponent from "../audit-trail-values/audit-trail-values.component";
import {pageLimit} from "../../utils/config"; import { pageLimit } from "../../utils/config";
export default function AuditTrailListComponent({loading, data}) { export default function AuditTrailListComponent({ loading, data }) {
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
filteredInfo: {}, filteredInfo: {}
}); });
const {t} = useTranslation(); const { t } = useTranslation();
const columns = [ const columns = [
{ {
title: t("audit.fields.created"), title: t("audit.fields.created"),
dataIndex: " created", dataIndex: " created",
key: " created", key: " created",
width: "10%", width: "10%",
render: (text, record) => ( render: (text, record) => <DateTimeFormatter>{record.created}</DateTimeFormatter>,
<DateTimeFormatter>{record.created}</DateTimeFormatter> sorter: (a, b) => a.created - b.created,
), sortOrder: state.sortedInfo.columnKey === "created" && state.sortedInfo.order
sorter: (a, b) => a.created - b.created, },
sortOrder: {
state.sortedInfo.columnKey === "created" && state.sortedInfo.order, title: t("audit.fields.operation"),
}, dataIndex: "operation",
{ key: "operation",
title: t("audit.fields.operation"), width: "10%",
dataIndex: "operation", sorter: (a, b) => alphaSort(a.operation, b.operation),
key: "operation", sortOrder: state.sortedInfo.columnKey === "operation" && state.sortedInfo.order
width: "10%", },
sorter: (a, b) => alphaSort(a.operation, b.operation), {
sortOrder: title: t("audit.fields.values"),
state.sortedInfo.columnKey === "operation" && state.sortedInfo.order, dataIndex: " old_val",
}, key: " old_val",
{ width: "10%",
title: t("audit.fields.values"), render: (text, record) => <AuditTrailValuesComponent oldV={record.old_val} newV={record.new_val} />
dataIndex: " old_val", },
key: " old_val", {
width: "10%", title: t("audit.fields.useremail"),
render: (text, record) => ( dataIndex: "useremail",
<AuditTrailValuesComponent key: "useremail",
oldV={record.old_val} width: "10%",
newV={record.new_val} sorter: (a, b) => alphaSort(a.useremail, b.useremail),
/> sortOrder: state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order
), }
}, ];
{
title: t("audit.fields.useremail"),
dataIndex: "useremail",
key: "useremail",
width: "10%",
sorter: (a, b) => alphaSort(a.useremail, b.useremail),
sortOrder:
state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order,
},
];
const formItemLayout = { const formItemLayout = {
labelCol: { labelCol: {
xs: {span: 12}, xs: { span: 12 },
sm: {span: 5}, sm: { span: 5 }
}, },
wrapperCol: { wrapperCol: {
xs: {span: 24}, xs: { span: 24 },
sm: {span: 12}, sm: { span: 12 }
}, }
}; };
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({...state, filteredInfo: filters, sortedInfo: sorter}); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
}; };
return ( return (
<Table <Table
{...formItemLayout} {...formItemLayout}
loading={loading} loading={loading}
pagination={{position: "top", defaultPageSize: pageLimit}} pagination={{ position: "top", defaultPageSize: pageLimit }}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
dataSource={data} dataSource={data}
onChange={handleTableChange} onChange={handleTableChange}
/> />
); );
} }

View File

@@ -1,40 +1,34 @@
import React from "react"; import React from "react";
import AuditTrailListComponent from "./audit-trail-list.component"; import AuditTrailListComponent from "./audit-trail-list.component";
import {useQuery} from "@apollo/client"; import { useQuery } from "@apollo/client";
import {QUERY_AUDIT_TRAIL} from "../../graphql/audit_trail.queries"; import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import {logImEXEvent} from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import EmailAuditTrailListComponent from "./email-audit-trail-list.component"; import EmailAuditTrailListComponent from "./email-audit-trail-list.component";
import {Card, Row} from "antd"; import { Card, Row } from "antd";
export default function AuditTrailListContainer({recordId}) { export default function AuditTrailListContainer({ recordId }) {
const {loading, error, data} = useQuery(QUERY_AUDIT_TRAIL, { const { loading, error, data } = useQuery(QUERY_AUDIT_TRAIL, {
variables: {id: recordId}, variables: { id: recordId },
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only"
}); });
logImEXEvent("audittrail_view", {recordId}); logImEXEvent("audittrail_view", { recordId });
return ( return (
<div> <div>
{error ? ( {error ? (
<AlertComponent type="error" message={error.message}/> <AlertComponent type="error" message={error.message} />
) : ( ) : (
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Card> <Card>
<AuditTrailListComponent <AuditTrailListComponent loading={loading} data={data ? data.audit_trail : []} />
loading={loading} </Card>
data={data ? data.audit_trail : []} <Card>
/> <EmailAuditTrailListComponent loading={loading} data={data ? data.audit_trail : []} />
</Card> </Card>
<Card> </Row>
<EmailAuditTrailListComponent )}
loading={loading} </div>
data={data ? data.audit_trail : []} );
/>
</Card>
</Row>
)}
</div>
);
} }

View File

@@ -1,64 +1,60 @@
import {Table} from "antd"; import { Table } from "antd";
import React, {useState} from "react"; import React, { useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {DateTimeFormatter} from "../../utils/DateFormatter"; import { DateTimeFormatter } from "../../utils/DateFormatter";
import {alphaSort} from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import {pageLimit} from "../../utils/config"; import { pageLimit } from "../../utils/config";
export default function EmailAuditTrailListComponent({loading, data}) { export default function EmailAuditTrailListComponent({ loading, data }) {
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
filteredInfo: {}, filteredInfo: {}
}); });
const {t} = useTranslation(); const { t } = useTranslation();
const columns = [ const columns = [
{ {
title: t("audit.fields.created"), title: t("audit.fields.created"),
dataIndex: " created", dataIndex: " created",
key: " created", key: " created",
width: "10%", width: "10%",
render: (text, record) => ( render: (text, record) => <DateTimeFormatter>{record.created}</DateTimeFormatter>,
<DateTimeFormatter>{record.created}</DateTimeFormatter> sorter: (a, b) => a.created - b.created,
), sortOrder: state.sortedInfo.columnKey === "created" && state.sortedInfo.order
sorter: (a, b) => a.created - b.created, },
sortOrder:
state.sortedInfo.columnKey === "created" && state.sortedInfo.order,
},
{ {
title: t("audit.fields.useremail"), title: t("audit.fields.useremail"),
dataIndex: "useremail", dataIndex: "useremail",
key: "useremail", key: "useremail",
width: "10%", width: "10%",
sorter: (a, b) => alphaSort(a.useremail, b.useremail), sorter: (a, b) => alphaSort(a.useremail, b.useremail),
sortOrder: sortOrder: state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order
state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order, }
}, ];
];
const formItemLayout = { const formItemLayout = {
labelCol: { labelCol: {
xs: {span: 12}, xs: { span: 12 },
sm: {span: 5}, sm: { span: 5 }
}, },
wrapperCol: { wrapperCol: {
xs: {span: 24}, xs: { span: 24 },
sm: {span: 12}, sm: { span: 12 }
}, }
}; };
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({...state, filteredInfo: filters, sortedInfo: sorter}); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
}; };
return ( return (
<Table <Table
{...formItemLayout} {...formItemLayout}
loading={loading} loading={loading}
pagination={{position: "top", defaultPageSize: pageLimit}} pagination={{ position: "top", defaultPageSize: pageLimit }}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
dataSource={data} dataSource={data}
onChange={handleTableChange} onChange={handleTableChange}
/> />
); );
} }

View File

@@ -1,30 +1,30 @@
import React from "react"; import React from "react";
import {List} from "antd"; import { List } from "antd";
import Icon from "@ant-design/icons"; import Icon from "@ant-design/icons";
import {FaArrowRight} from "react-icons/fa"; import { FaArrowRight } from "react-icons/fa";
export default function AuditTrailValuesComponent({oldV, newV}) { export default function AuditTrailValuesComponent({ oldV, newV }) {
if (!oldV && !newV) return <div></div>; if (!oldV && !newV) return <div></div>;
if (!oldV && newV)
return (
<List style={{width: "800px"}} bordered size='small'>
{Object.keys(newV).map((key, idx) => (
<List.Item key={idx} value={key}>
{key}: {JSON.stringify(newV[key])}
</List.Item>
))}
</List>
);
if (!oldV && newV)
return ( return (
<List style={{width: "800px"}} bordered size='small'> <List style={{ width: "800px" }} bordered size="small">
{Object.keys(oldV).map((key, idx) => ( {Object.keys(newV).map((key, idx) => (
<List.Item key={idx}> <List.Item key={idx} value={key}>
{key}: {oldV[key]} <Icon component={FaArrowRight}/> {key}: {JSON.stringify(newV[key])}
{JSON.stringify(newV[key])} </List.Item>
</List.Item> ))}
))} </List>
</List>
); );
return (
<List style={{ width: "800px" }} bordered size="small">
{Object.keys(oldV).map((key, idx) => (
<List.Item key={idx}>
{key}: {oldV[key]} <Icon component={FaArrowRight} />
{JSON.stringify(newV[key])}
</List.Item>
))}
</List>
);
} }

View File

@@ -1,23 +1,15 @@
import {Popover, Tag} from "antd"; import { Popover, Tag } from "antd";
import React from "react"; import React from "react";
import Barcode from "react-barcode"; import Barcode from "react-barcode";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
export default function BarcodePopupComponent({value, children}) { export default function BarcodePopupComponent({ value, children }) {
const {t} = useTranslation(); const { t } = useTranslation();
return ( return (
<div> <div>
<Popover <Popover content={<Barcode value={value || ""} background="transparent" displayValue={false} />}>
content={ {children ? children : <Tag>{t("general.labels.barcode")}</Tag>}
<Barcode </Popover>
value={value || ""} </div>
background="transparent" );
displayValue={false}
/>
}
>
{children ? children : <Tag>{t("general.labels.barcode")}</Tag>}
</Popover>
</div>
);
} }

View File

@@ -1,136 +1,128 @@
import {Checkbox, Form, Skeleton, Typography} from "antd"; import { Checkbox, Form, Skeleton, Typography } from "antd";
import React, {useEffect} from "react"; import React, { useEffect } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component"; import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
import "./bill-cm-returns-table.styles.scss"; import "./bill-cm-returns-table.styles.scss";
export default function BillCmdReturnsTableComponent({ export default function BillCmdReturnsTableComponent({ form, returnLoading, returnData }) {
form, const { t } = useTranslation();
returnLoading,
returnData,
}) {
const {t} = useTranslation();
useEffect(() => { useEffect(() => {
if (returnData) { if (returnData) {
form.setFieldsValue({ form.setFieldsValue({
outstanding_returns: returnData.parts_order_lines, outstanding_returns: returnData.parts_order_lines
}); });
}
}, [returnData, form]);
return (
<Form.Item
shouldUpdate={(prev, cur) =>
prev.jobid !== cur.jobid || prev.is_credit_memo !== cur.is_credit_memo || prev.vendorid !== cur.vendorid
}
noStyle
>
{() => {
const isReturn = form.getFieldValue("is_credit_memo");
if (!isReturn) {
return null;
} }
}, [returnData, form]);
return ( if (returnLoading) return <Skeleton />;
<Form.Item
shouldUpdate={(prev, cur) =>
prev.jobid !== cur.jobid ||
prev.is_credit_memo !== cur.is_credit_memo ||
prev.vendorid !== cur.vendorid
}
noStyle
>
{() => {
const isReturn = form.getFieldValue("is_credit_memo");
if (!isReturn) { return (
return null; <Form.List name="outstanding_returns">
} {(fields, { add, remove, move }) => {
return (
<>
<Typography.Title level={4}>{t("bills.labels.creditsnotreceived")}</Typography.Title>
<table className="bill-cm-returns-table">
<thead>
<tr>
<th>{t("parts_orders.fields.line_desc")}</th>
<th>{t("parts_orders.fields.part_type")}</th>
<th>{t("parts_orders.fields.quantity")}</th>
<th>{t("parts_orders.fields.act_price")}</th>
<th>{t("parts_orders.fields.cost")}</th>
<th>{t("parts_orders.labels.mark_as_received")}</th>
</tr>
</thead>
<tbody>
{fields.map((field, index) => (
<tr key={field.key}>
<td>
<Form.Item
// label={t("joblines.fields.line_desc")}
key={`${index}line_desc`}
name={[field.name, "line_desc"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
if (returnLoading) return <Skeleton/>; <td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}part_type`}
name={[field.name, "part_type"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}quantity`}
name={[field.name, "quantity"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}act_price`}
name={[field.name, "act_price"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}cost`}
name={[field.name, "cost"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
return ( <td>
<Form.List name="outstanding_returns"> <Form.Item
{(fields, {add, remove, move}) => { span={2}
return ( //label={t("joblines.fields.mod_lb_hrs")}
<> key={`${index}cm_received`}
<Typography.Title level={4}> name={[field.name, "cm_received"]}
{t("bills.labels.creditsnotreceived")} valuePropName="checked"
</Typography.Title> >
<table className="bill-cm-returns-table"> <Checkbox />
<thead> </Form.Item>
<tr> </td>
<th>{t("parts_orders.fields.line_desc")}</th> </tr>
<th>{t("parts_orders.fields.part_type")}</th> ))}
<th>{t("parts_orders.fields.quantity")}</th> </tbody>
<th>{t("parts_orders.fields.act_price")}</th> </table>
<th>{t("parts_orders.fields.cost")}</th> </>
<th>{t("parts_orders.labels.mark_as_received")}</th> );
</tr>
</thead>
<tbody>
{fields.map((field, index) => (
<tr key={field.key}>
<td>
<Form.Item
// label={t("joblines.fields.line_desc")}
key={`${index}line_desc`}
name={[field.name, "line_desc"]}
>
<ReadOnlyFormItemComponent/>
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}part_type`}
name={[field.name, "part_type"]}
>
<ReadOnlyFormItemComponent/>
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}quantity`}
name={[field.name, "quantity"]}
>
<ReadOnlyFormItemComponent/>
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}act_price`}
name={[field.name, "act_price"]}
>
<ReadOnlyFormItemComponent type="currency"/>
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}cost`}
name={[field.name, "cost"]}
>
<ReadOnlyFormItemComponent type="currency"/>
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}cm_received`}
name={[field.name, "cm_received"]}
valuePropName="checked"
>
<Checkbox/>
</Form.Item>
</td>
</tr>
))}
</tbody>
</table>
</>
);
}}
</Form.List>
);
}} }}
</Form.Item> </Form.List>
); );
}}
</Form.Item>
);
} }

View File

@@ -16,4 +16,4 @@
tr:hover { tr:hover {
background-color: #f5f5f5; background-color: #f5f5f5;
} }
} }

View File

@@ -1,80 +1,88 @@
import {DeleteFilled} from "@ant-design/icons"; import { DeleteFilled } from "@ant-design/icons";
import {useMutation} from "@apollo/client"; import { useMutation } from "@apollo/client";
import {Button, notification, Popconfirm} from "antd"; import { Button, notification, Popconfirm } from "antd";
import React, {useState} from "react"; import React, { useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {DELETE_BILL} from "../../graphql/bills.queries"; import { DELETE_BILL } from "../../graphql/bills.queries";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component"; import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
export default function BillDeleteButton({bill, callback}) { const mapStateToProps = createStructuredSelector({});
const [loading, setLoading] = useState(false); const mapDispatchToProps = (dispatch) => ({
const {t} = useTranslation(); insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
const [deleteBill] = useMutation(DELETE_BILL); });
const handleDelete = async () => { export default connect(mapStateToProps, mapDispatchToProps)(BillDeleteButton);
setLoading(true);
const result = await deleteBill({ export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
variables: {billId: bill.id}, const [loading, setLoading] = useState(false);
update(cache, {errors}) { const { t } = useTranslation();
if (errors) return; const [deleteBill] = useMutation(DELETE_BILL);
cache.modify({
fields: { const handleDelete = async () => {
bills(existingBills, {readField}) { setLoading(true);
return existingBills.filter( const result = await deleteBill({
(billref) => bill.id !== readField("id", billref) variables: { billId: bill.id },
); update(cache, { errors }) {
}, if (errors) return;
search_bills(existingBills, {readField}) { cache.modify({
return existingBills.filter( fields: {
(billref) => bill.id !== readField("id", billref) bills(existingBills, { readField }) {
); return existingBills.filter((billref) => bill.id !== readField("id", billref));
},
},
});
}, },
}); search_bills(existingBills, { readField }) {
return existingBills.filter((billref) => bill.id !== readField("id", billref));
if (!!!result.errors) {
notification["success"]({message: t("bills.successes.deleted")});
if (callback && typeof callback === "function") callback(bill.id);
} else {
//Check if it's an fkey violation.
const error = JSON.stringify(result.errors);
if (error.toLowerCase().includes("inventory_billid_fkey")) {
notification["error"]({
message: t("bills.errors.deleting", {
error: t("bills.errors.existinginventoryline"),
}),
});
} else {
notification["error"]({
message: t("bills.errors.deleting", {
error: JSON.stringify(result.errors),
}),
});
} }
} }
});
}
});
setLoading(false); if (!!!result.errors) {
}; notification["success"]({ message: t("bills.successes.deleted") });
insertAuditTrail({
jobid: jobid,
operation: AuditTrailMapping.billdeleted(bill.invoice_number),
type: "billdeleted"
});
return ( if (callback && typeof callback === "function") callback(bill.id);
<RbacWrapper action="bills:delete" noauth={<></>}> } else {
<Popconfirm //Check if it's an fkey violation.
disabled={bill.exported} const error = JSON.stringify(result.errors);
onConfirm={handleDelete}
title={t("bills.labels.deleteconfirm")} if (error.toLowerCase().includes("inventory_billid_fkey")) {
> notification["error"]({
<Button message: t("bills.errors.deleting", {
disabled={bill.exported} error: t("bills.errors.existinginventoryline")
// onClick={handleDelete} })
loading={loading} });
> } else {
<DeleteFilled/> notification["error"]({
</Button> message: t("bills.errors.deleting", {
</Popconfirm> error: JSON.stringify(result.errors)
</RbacWrapper> })
); });
}
}
setLoading(false);
};
return (
<RbacWrapper action="bills:delete" noauth={<></>}>
<Popconfirm disabled={bill.exported} onConfirm={handleDelete} title={t("bills.labels.deleteconfirm")}>
<Button
disabled={bill.exported}
// onClick={handleDelete}
loading={loading}
>
<DeleteFilled />
</Button>
</Popconfirm>
</RbacWrapper>
);
} }

View File

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

View File

@@ -1,185 +1,169 @@
import {Button, Checkbox, Form, Modal} from "antd"; import { Button, Checkbox, Form, Modal } from "antd";
import queryString from "query-string"; import queryString from "query-string";
import React, {useEffect, useState} from "react"; import React, { useEffect, useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {useLocation, useNavigate} from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {insertAuditTrail} from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
import {setModalContext} from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
import {selectBodyshop} from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component"; import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) => setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
dispatch(setModalContext({context: context, modal: "partsOrder"})), insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
}); });
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditReturn);
mapStateToProps,
mapDispatchToProps
)(BillDetailEditReturn);
export function BillDetailEditReturn({ export function BillDetailEditReturn({ setPartsOrderContext, insertAuditTrail, bodyshop, data, disabled }) {
setPartsOrderContext, const search = queryString.parse(useLocation().search);
insertAuditTrail, const history = useNavigate();
bodyshop, const { t } = useTranslation();
data, const [form] = Form.useForm();
disabled, const [open, setOpen] = useState(false);
}) {
const search = queryString.parse(useLocation().search);
const history = useNavigate();
const {t} = useTranslation();
const [form] = Form.useForm();
const [open, setOpen] = useState(false);
const handleFinish = ({billlines}) => { const handleFinish = ({ billlines }) => {
const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id); const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id);
setPartsOrderContext({ setPartsOrderContext({
actions: {}, actions: {},
context: { context: {
jobId: data.bills_by_pk.jobid, jobId: data.bills_by_pk.jobid,
vendorId: data.bills_by_pk.vendorid, job: data.bills_by_pk.job,
returnFromBill: data.bills_by_pk.id, vendorId: data.bills_by_pk.vendorid,
invoiceNumber: data.bills_by_pk.invoice_number, returnFromBill: data.bills_by_pk.id,
linesToOrder: data.bills_by_pk.billlines invoiceNumber: data.bills_by_pk.invoice_number,
.filter((l) => selectedLines.includes(l.id)) linesToOrder: data.bills_by_pk.billlines
.map((i) => { .filter((l) => selectedLines.includes(l.id))
return { .map((i) => {
line_desc: i.line_desc, return {
// db_price: i.actual_price, line_desc: i.line_desc,
act_price: i.actual_price, // db_price: i.actual_price,
cost: i.actual_cost, act_price: i.actual_price,
quantity: i.quantity, cost: i.actual_cost,
joblineid: i.joblineid, quantity: i.quantity,
oem_partno: i.jobline && i.jobline.oem_partno, joblineid: i.joblineid,
part_type: i.jobline && i.jobline.part_type, oem_partno: i.jobline && i.jobline.oem_partno,
}; part_type: i.jobline && i.jobline.part_type
}), };
isReturn: true, }),
}, isReturn: true
}); }
delete search.billid; });
delete search.billid;
history({search: queryString.stringify(search)}); history({ search: queryString.stringify(search) });
setOpen(false); setOpen(false);
}; };
useEffect(() => { useEffect(() => {
if (open === false) form.resetFields(); if (open === false) form.resetFields();
}, [open, form]); }, [open, form]);
return ( return (
<> <>
<Modal <Modal
open={open} open={open}
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
destroyOnClose destroyOnClose
title={t("bills.actions.return")} title={t("bills.actions.return")}
onOk={() => form.submit()} onOk={() => form.submit()}
> >
<Form <Form initialValues={data && data.bills_by_pk} onFinish={handleFinish} form={form}>
initialValues={data && data.bills_by_pk} <Form.List name={["billlines"]}>
onFinish={handleFinish} {(fields, { add, remove, move }) => {
form={form} return (
> <table style={{ tableLayout: "auto", width: "100%" }}>
<Form.List name={["billlines"]}> <thead>
{(fields, {add, remove, move}) => { <tr>
return ( <td>
<table style={{tableLayout: "auto", width: "100%"}}> <Checkbox
<thead> onChange={(e) => {
<tr> form.setFieldsValue({
<td> billlines: form.getFieldsValue().billlines.map((b) => ({
<Checkbox ...b,
onChange={(e) => { selected: e.target.checked
form.setFieldsValue({ }))
billlines: form });
.getFieldsValue() }}
.billlines.map((b) => ({ />
...b, </td>
selected: e.target.checked, <td>{t("billlines.fields.line_desc")}</td>
})), <td>{t("billlines.fields.quantity")}</td>
}); <td>{t("billlines.fields.actual_price")}</td>
}} <td>{t("billlines.fields.actual_cost")}</td>
/> </tr>
</td> </thead>
<td>{t("billlines.fields.line_desc")}</td> <tbody>
<td>{t("billlines.fields.quantity")}</td> {fields.map((field, index) => (
<td>{t("billlines.fields.actual_price")}</td> <tr key={field.key}>
<td>{t("billlines.fields.actual_cost")}</td> <td>
</tr> <Form.Item
</thead> // label={t("joblines.fields.selected")}
<tbody> key={`${index}selected`}
{fields.map((field, index) => ( name={[field.name, "selected"]}
<tr key={field.key}> valuePropName="checked"
<td> >
<Form.Item <Checkbox />
// label={t("joblines.fields.selected")} </Form.Item>
key={`${index}selected`} </td>
name={[field.name, "selected"]} <td>
valuePropName="checked" <Form.Item
> // label={t("joblines.fields.line_desc")}
<Checkbox/> key={`${index}line_desc`}
</Form.Item> name={[field.name, "line_desc"]}
</td> >
<td> <ReadOnlyFormItemComponent />
<Form.Item </Form.Item>
// label={t("joblines.fields.line_desc")} </td>
key={`${index}line_desc`} <td>
name={[field.name, "line_desc"]} <Form.Item
> // label={t("joblines.fields.quantity")}
<ReadOnlyFormItemComponent/> key={`${index}quantity`}
</Form.Item> name={[field.name, "quantity"]}
</td> >
<td> <ReadOnlyFormItemComponent />
<Form.Item </Form.Item>
// label={t("joblines.fields.quantity")} </td>
key={`${index}quantity`} <td>
name={[field.name, "quantity"]} <Form.Item
> // label={t("joblines.fields.actual_price")}
<ReadOnlyFormItemComponent/> key={`${index}actual_price`}
</Form.Item> name={[field.name, "actual_price"]}
</td> >
<td> <ReadOnlyFormItemComponent type="currency" />
<Form.Item </Form.Item>
// label={t("joblines.fields.actual_price")} </td>
key={`${index}actual_price`} <td>
name={[field.name, "actual_price"]} <Form.Item
> // label={t("joblines.fields.actual_cost")}
<ReadOnlyFormItemComponent type="currency"/> key={`${index}actual_cost`}
</Form.Item> name={[field.name, "actual_cost"]}
</td> >
<td> <ReadOnlyFormItemComponent type="currency" />
<Form.Item </Form.Item>
// label={t("joblines.fields.actual_cost")} </td>
key={`${index}actual_cost`} </tr>
name={[field.name, "actual_cost"]} ))}
> </tbody>
<ReadOnlyFormItemComponent type="currency"/> </table>
</Form.Item> );
</td> }}
</tr> </Form.List>
))} </Form>
</tbody> </Modal>
</table> <Button
); disabled={data.bills_by_pk.is_credit_memo || data.bills_by_pk.isinhouse || disabled}
}} onClick={() => {
</Form.List> setOpen(true);
</Form> }}
</Modal> >
<Button {t("bills.actions.return")}
disabled={data.bills_by_pk.is_credit_memo || disabled} </Button>
onClick={() => { </>
setOpen(true); );
}}
>
{t("bills.actions.return")}
</Button>
</>
);
} }

View File

@@ -1,40 +1,38 @@
import {Drawer, Grid} from "antd"; import { Drawer, Grid } from "antd";
import queryString from "query-string"; import queryString from "query-string";
import React from "react"; import React from "react";
import {useLocation, useNavigate} from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import BillDetailEditComponent from "./bill-detail-edit-component"; import BillDetailEditComponent from "./bill-detail-edit-component";
export default function BillDetailEditcontainer() { export default function BillDetailEditcontainer() {
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const history = useNavigate(); const history = useNavigate();
const selectedBreakpoint = Object.entries(Grid.useBreakpoint()) const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1]) .filter((screen) => !!screen[1])
.slice(-1)[0]; .slice(-1)[0];
const bpoints = { const bpoints = {
xs: "100%", xs: "100%",
sm: "100%", sm: "100%",
md: "100%", md: "100%",
lg: "100%", lg: "100%",
xl: "90%", xl: "90%",
xxl: "90%", xxl: "90%"
}; };
const drawerPercentage = selectedBreakpoint const drawerPercentage = selectedBreakpoint ? bpoints[selectedBreakpoint[0]] : "100%";
? bpoints[selectedBreakpoint[0]]
: "100%";
return ( return (
<Drawer <Drawer
width={drawerPercentage} width={drawerPercentage}
onClose={() => { onClose={() => {
delete search.billid; delete search.billid;
history({search: queryString.stringify(search)}); history({ search: queryString.stringify(search) });
}} }}
destroyOnClose destroyOnClose
open={search.billid} open={search.billid}
> >
<BillDetailEditComponent/> <BillDetailEditComponent />
</Drawer> </Drawer>
); );
} }

View File

@@ -1,467 +1,426 @@
import {useApolloClient, useMutation} from "@apollo/client"; import { useApolloClient, useMutation } from "@apollo/client";
import {useSplitTreatments} from "@splitsoftware/splitio-react"; import { useSplitTreatments } from "@splitsoftware/splitio-react";
import {Button, Checkbox, Form, Modal, notification, Space} from "antd"; import { Button, Checkbox, Form, Modal, notification, Space } from "antd";
import _ from "lodash"; import _ from "lodash";
import React, {useEffect, useMemo, useState} from "react"; import React, { useEffect, useMemo, useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {INSERT_NEW_BILL} from "../../graphql/bills.queries"; import { INSERT_NEW_BILL } from "../../graphql/bills.queries";
import {UPDATE_INVENTORY_LINES} from "../../graphql/inventory.queries"; import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
import {UPDATE_JOB_LINE} from "../../graphql/jobs-lines.queries"; import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
import {QUERY_JOB_LBR_ADJUSTMENTS, UPDATE_JOB,} from "../../graphql/jobs.queries"; import { QUERY_JOB_LBR_ADJUSTMENTS, UPDATE_JOB } from "../../graphql/jobs.queries";
import {MUTATION_MARK_RETURN_RECEIVED} from "../../graphql/parts-orders.queries"; import { MUTATION_MARK_RETURN_RECEIVED } from "../../graphql/parts-orders.queries";
import {insertAuditTrail} from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
import {toggleModalVisible} from "../../redux/modals/modals.actions"; import { toggleModalVisible } from "../../redux/modals/modals.actions";
import {selectBillEnterModal} from "../../redux/modals/modals.selectors"; import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
import {GenerateDocument} from "../../utils/RenderTemplate"; import { GenerateDocument } from "../../utils/RenderTemplate";
import {TemplateList} from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import confirmDialog from "../../utils/asyncConfirm"; import confirmDialog from "../../utils/asyncConfirm";
import useLocalStorage from "../../utils/useLocalStorage"; import useLocalStorage from "../../utils/useLocalStorage";
import BillFormContainer from "../bill-form/bill-form.container"; import BillFormContainer from "../bill-form/bill-form.container";
import {CalculateBillTotal} from "../bill-form/bill-form.totals.utility"; import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
import {handleUpload as handleLocalUpload} from "../documents-local-upload/documents-local-upload.utility"; import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
import {handleUpload} from "../documents-upload/documents-upload.utility"; import { handleUpload } from "../documents-upload/documents-upload.utility";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
billEnterModal: selectBillEnterModal, billEnterModal: selectBillEnterModal,
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser, currentUser: selectCurrentUser
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")), toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
insertAuditTrail: ({jobid, billid, operation}) => insertAuditTrail: ({ jobid, billid, operation, type }) =>
dispatch(insertAuditTrail({jobid, billid, operation})), dispatch(insertAuditTrail({ jobid, billid, operation, type }))
}); });
const Templates = TemplateList("job_special"); const Templates = TemplateList("job_special");
function BillEnterModalContainer({ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop, currentUser, insertAuditTrail }) {
billEnterModal, const [form] = Form.useForm();
toggleModalVisible, const { t } = useTranslation();
bodyshop, const [enterAgain, setEnterAgain] = useState(false);
currentUser, const [insertBill] = useMutation(INSERT_NEW_BILL);
insertAuditTrail, const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
}) { const [updatePartsOrderLines] = useMutation(MUTATION_MARK_RETURN_RECEIVED);
const [form] = Form.useForm(); const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES);
const {t} = useTranslation(); const [loading, setLoading] = useState(false);
const [enterAgain, setEnterAgain] = useState(false); const client = useApolloClient();
const [insertBill] = useMutation(INSERT_NEW_BILL); const [generateLabel, setGenerateLabel] = useLocalStorage("enter_bill_generate_label", false);
const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
const [updatePartsOrderLines] = useMutation(MUTATION_MARK_RETURN_RECEIVED);
const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES);
const [loading, setLoading] = useState(false);
const client = useApolloClient();
const [generateLabel, setGenerateLabel] = useLocalStorage(
"enter_bill_generate_label",
false
);
const {treatments: {Enhanced_Payroll}} = useSplitTreatments({ const {
attributes: {}, treatments: { Enhanced_Payroll }
names: ["Enhanced_Payroll"], } = useSplitTreatments({
splitKey: bodyshop.imexshopid, attributes: {},
names: ["Enhanced_Payroll"],
splitKey: bodyshop.imexshopid
});
const formValues = useMemo(() => {
return {
...billEnterModal.context.bill,
//Added as a part of IO-2436 for capturing parts price changes.
billlines: billEnterModal.context?.bill?.billlines?.map((line) => ({
...line,
original_actual_price: line.actual_price
})),
jobid: (billEnterModal.context.job && billEnterModal.context.job.id) || null,
federal_tax_rate: (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.federal_tax_rate) || 0,
state_tax_rate: (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.state_tax_rate) || 0,
local_tax_rate: (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.local_tax_rate) || 0
};
}, [billEnterModal, bodyshop]);
const handleFinish = async (values) => {
let totals = CalculateBillTotal(values);
if (totals.discrepancy.getAmount() !== 0) {
if (!(await confirmDialog(t("bills.labels.savewithdiscrepancy")))) {
return;
}
}
setLoading(true);
const { upload, location, outstanding_returns, inventory, federal_tax_exempt, ...remainingValues } = values;
let adjustmentsToInsert = {};
let payrollAdjustmentsToInsert = [];
const r1 = await insertBill({
variables: {
bill: [
{
...remainingValues,
billlines: {
data:
remainingValues.billlines &&
remainingValues.billlines.map((i) => {
const {
deductedfromlbr,
lbr_adjustment,
location: lineLocation,
part_type,
create_ppc,
original_actual_price,
...restI
} = i;
if (Enhanced_Payroll.treatment === "on") {
if (
deductedfromlbr &&
true //payroll is on
) {
payrollAdjustmentsToInsert.push({
id: i.joblineid,
convertedtolbr: true,
convertedtolbr_data: {
mod_lb_hrs: lbr_adjustment.mod_lb_hrs * -1,
mod_lbr_ty: lbr_adjustment.mod_lbr_ty
}
});
}
} else {
if (deductedfromlbr) {
adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] =
(adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] || 0) -
restI.actual_price / lbr_adjustment.rate;
}
}
return {
...restI,
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"],
awaitRefetchQueries: true
}); });
const formValues = useMemo(() => { await Promise.all(
return { payrollAdjustmentsToInsert.map((li) => {
...billEnterModal.context.bill, return updateJobLines({
//Added as a part of IO-2436 for capturing parts price changes. variables: {
billlines: billEnterModal.context?.bill?.billlines?.map((line) => ({ lineId: li.id,
...line, line: {
original_actual_price: line.actual_price, convertedtolbr: li.convertedtolbr,
})), convertedtolbr_data: li.convertedtolbr_data
jobid:
(billEnterModal.context.job && billEnterModal.context.job.id) || null,
federal_tax_rate:
(bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.federal_tax_rate) ||
0,
state_tax_rate:
(bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.state_tax_rate) ||
0,
local_tax_rate:
(bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.local_tax_rate) ||
0,
};
}, [billEnterModal, bodyshop]);
const handleFinish = async (values) => {
let totals = CalculateBillTotal(values);
if (totals.discrepancy.getAmount() !== 0) {
if (!(await confirmDialog(t("bills.labels.savewithdiscrepancy")))) {
return;
} }
} }
setLoading(true);
const {
upload,
location,
outstanding_returns,
inventory,
federal_tax_exempt,
...remainingValues
} = values;
let adjustmentsToInsert = {};
let payrollAdjustmentsToInsert = [];
const r1 = await insertBill({
variables: {
bill: [
{
...remainingValues,
billlines: {
data:
remainingValues.billlines &&
remainingValues.billlines.map((i) => {
const {
deductedfromlbr,
lbr_adjustment,
location: lineLocation,
part_type,
create_ppc,
original_actual_price,
...restI
} = i;
if (Enhanced_Payroll.treatment === "on") {
if (
deductedfromlbr &&
true //payroll is on
) {
payrollAdjustmentsToInsert.push({
id: i.joblineid,
convertedtolbr: true,
convertedtolbr_data: {
mod_lb_hrs: lbr_adjustment.mod_lb_hrs * -1,
mod_lbr_ty: lbr_adjustment.mod_lbr_ty,
},
});
}
} else {
if (deductedfromlbr) {
adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] =
(adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] || 0) -
restI.actual_price / lbr_adjustment.rate;
}
}
return {
...restI,
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"],
}); });
})
);
await Promise.all( const adjKeys = Object.keys(adjustmentsToInsert);
payrollAdjustmentsToInsert.map((li) => { if (adjKeys.length > 0) {
return updateJobLines({ //Query the adjustments, merge, and update them.
variables: { const existingAdjustments = await client.query({
lineId: li.id, query: QUERY_JOB_LBR_ADJUSTMENTS,
line: { variables: {
convertedtolbr: li.convertedtolbr, id: values.jobid
convertedtolbr_data: li.convertedtolbr_data,
},
},
});
})
);
const adjKeys = Object.keys(adjustmentsToInsert);
if (adjKeys.length > 0) {
//Query the adjustments, merge, and update them.
const existingAdjustments = await client.query({
query: QUERY_JOB_LBR_ADJUSTMENTS,
variables: {
id: values.jobid,
},
});
const newAdjustments = _.cloneDeep(
existingAdjustments.data.jobs_by_pk.lbr_adjustments
);
adjKeys.forEach((key) => {
newAdjustments[key] =
(newAdjustments[key] || 0) + adjustmentsToInsert[key];
insertAuditTrail({
jobid: values.jobid,
operation: AuditTrailMapping.jobmodifylbradj({
mod_lbr_ty: key,
hours: adjustmentsToInsert[key].toFixed(1),
}),
});
});
const jobUpdate = client.mutate({
mutation: UPDATE_JOB,
variables: {
jobId: values.jobid,
job: {lbr_adjustments: newAdjustments},
},
});
if (!!jobUpdate.errors) {
notification["error"]({
message: t("jobs.errors.saving", {
message: JSON.stringify(jobUpdate.errors),
}),
});
return;
}
} }
});
const markPolReceived = const newAdjustments = _.cloneDeep(existingAdjustments.data.jobs_by_pk.lbr_adjustments);
outstanding_returns &&
outstanding_returns.filter((o) => o.cm_received === true);
if (markPolReceived && markPolReceived.length > 0) { adjKeys.forEach((key) => {
const r2 = await updatePartsOrderLines({ newAdjustments[key] = (newAdjustments[key] || 0) + adjustmentsToInsert[key];
variables: {partsLineIds: markPolReceived.map((p) => p.id)},
});
if (!!r2.errors) {
setLoading(false);
setEnterAgain(false);
notification["error"]({
message: t("parts_orders.errors.updating", {
message: JSON.stringify(r2.errors),
}),
});
}
}
if (!!r1.errors) {
setLoading(false);
setEnterAgain(false);
notification["error"]({
message: t("bills.errors.creating", {
message: JSON.stringify(r1.errors),
}),
});
}
const billId = r1.data.insert_bills.returning[0].id;
const markInventoryConsumed =
inventory && inventory.filter((i) => i.consumefrominventory);
if (markInventoryConsumed && markInventoryConsumed.length > 0) {
const r2 = await updateInventoryLines({
variables: {
InventoryIds: markInventoryConsumed.map((p) => p.id),
consumedbybillid: billId,
},
});
if (!!r2.errors) {
setLoading(false);
setEnterAgain(false);
notification["error"]({
message: t("inventory.errors.updating", {
message: JSON.stringify(r2.errors),
}),
});
}
}
//If it's not a credit memo, update the statuses.
if (!values.is_credit_memo) {
await Promise.all(
remainingValues.billlines
.filter((il) => il.joblineid !== "noline")
.map((li) => {
return updateJobLines({
variables: {
lineId: li.joblineid,
line: {
location: li.location || location,
status:
bodyshop.md_order_statuses.default_received || "Received*",
//Added parts price changes.
...(li.create_ppc &&
li.original_actual_price !== li.actual_price
? {
act_price_before_ppc: li.original_actual_price,
act_price: li.actual_price,
}
: {}),
},
},
});
})
);
}
/////////////////////////
if (upload && upload.length > 0) {
//insert Each of the documents?
if (bodyshop.uselocalmediaserver) {
upload.forEach((u) => {
handleLocalUpload({
ev: {file: u.originFileObj},
context: {
jobid: values.jobid,
invoice_number: remainingValues.invoice_number,
vendorid: remainingValues.vendorid,
},
});
});
} else {
upload.forEach((u) => {
handleUpload(
{file: u.originFileObj},
{
bodyshop: bodyshop,
uploaded_by: currentUser.email,
jobId: values.jobid,
billId: billId,
tagsArray: null,
callback: null,
}
);
});
}
}
///////////////////////////
setLoading(false);
notification["success"]({
message: t("bills.successes.created"),
});
if (generateLabel) {
GenerateDocument(
{
name: Templates.parts_invoice_label_single.key,
variables: {
id: billId,
},
},
{},
"p"
);
}
if (billEnterModal.actions.refetch) billEnterModal.actions.refetch();
insertAuditTrail({ insertAuditTrail({
jobid: values.jobid, jobid: values.jobid,
billid: billId, operation: AuditTrailMapping.jobmodifylbradj({
operation: AuditTrailMapping.billposted( mod_lbr_ty: key,
r1.data.insert_bills.returning[0].invoice_number hours: adjustmentsToInsert[key].toFixed(1)
), }),
type: "jobmodifylbradj"
}); });
});
if (enterAgain) { const jobUpdate = client.mutate({
// form.resetFields(); mutation: UPDATE_JOB,
form.setFieldsValue({ variables: {
...formValues, jobId: values.jobid,
billlines: [], job: { lbr_adjustments: newAdjustments }
});
form.resetFields();
} else {
toggleModalVisible();
} }
});
if (!!jobUpdate.errors) {
notification["error"]({
message: t("jobs.errors.saving", {
message: JSON.stringify(jobUpdate.errors)
})
});
return;
}
}
const markPolReceived = outstanding_returns && outstanding_returns.filter((o) => o.cm_received === true);
if (markPolReceived && markPolReceived.length > 0) {
const r2 = await updatePartsOrderLines({
variables: { partsLineIds: markPolReceived.map((p) => p.id) },
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"]
});
if (!!r2.errors) {
setLoading(false);
setEnterAgain(false); setEnterAgain(false);
}; notification["error"]({
message: t("parts_orders.errors.updating", {
message: JSON.stringify(r2.errors)
})
});
}
}
const handleCancel = () => { if (!!r1.errors) {
const r = window.confirm(t("general.labels.cancel")); setLoading(false);
if (r === true) { setEnterAgain(false);
toggleModalVisible(); notification["error"]({
message: t("bills.errors.creating", {
message: JSON.stringify(r1.errors)
})
});
}
const billId = r1.data.insert_bills.returning[0].id;
const markInventoryConsumed = inventory && inventory.filter((i) => i.consumefrominventory);
if (markInventoryConsumed && markInventoryConsumed.length > 0) {
const r2 = await updateInventoryLines({
variables: {
InventoryIds: markInventoryConsumed.map((p) => p.id),
consumedbybillid: billId
} }
}; });
if (!!r2.errors) {
setLoading(false);
setEnterAgain(false);
notification["error"]({
message: t("inventory.errors.updating", {
message: JSON.stringify(r2.errors)
})
});
}
}
//If it's not a credit memo, update the statuses.
useEffect(() => { if (!values.is_credit_memo) {
if (enterAgain) form.submit(); await Promise.all(
}, [enterAgain, form]); remainingValues.billlines
.filter((il) => il.joblineid !== "noline")
.map((li) => {
return updateJobLines({
variables: {
lineId: li.joblineid,
line: {
location: li.location || location,
status: bodyshop.md_order_statuses.default_received || "Received*",
//Added parts price changes.
...(li.create_ppc && li.original_actual_price !== li.actual_price
? {
act_price_before_ppc: li.original_actual_price,
act_price: li.actual_price
}
: {})
}
}
});
})
);
}
useEffect(() => { /////////////////////////
if (billEnterModal.open) { if (upload && upload.length > 0) {
form.setFieldsValue(formValues); //insert Each of the documents?
} else {
form.resetFields();
}
}, [billEnterModal.open, form, formValues]);
return ( if (bodyshop.uselocalmediaserver) {
<Modal upload.forEach((u) => {
title={t("bills.labels.new")} handleLocalUpload({
width={"98%"} ev: { file: u.originFileObj },
open={billEnterModal.open} context: {
okText={t("general.actions.save")} jobid: values.jobid,
keyboard="false" invoice_number: remainingValues.invoice_number,
onOk={() => form.submit()} vendorid: remainingValues.vendorid
onCancel={handleCancel}
afterClose={() => {
form.resetFields();
setLoading(false);
}}
footer={
<Space>
<Checkbox
checked={generateLabel}
onChange={(e) => setGenerateLabel(e.target.checked)}
>
{t("bills.labels.generatepartslabel")}
</Checkbox>
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
<Button loading={loading} onClick={() => form.submit()}>
{t("general.actions.save")}
</Button>
{billEnterModal.context && billEnterModal.context.id ? null : (
<Button
type="primary"
loading={loading}
onClick={() => {
setEnterAgain(true);
}}
>
{t("general.actions.saveandnew")}
</Button>
)}
</Space>
} }
destroyOnClose });
> });
<Form } else {
onFinish={handleFinish} upload.forEach((u) => {
autoComplete={"off"} handleUpload(
layout="vertical" { file: u.originFileObj },
form={form} {
onFinishFailed={() => { bodyshop: bodyshop,
setEnterAgain(false); uploaded_by: currentUser.email,
}} jobId: values.jobid,
billId: billId,
tagsArray: null,
callback: null
}
);
});
}
}
///////////////////////////
setLoading(false);
notification["success"]({
message: t("bills.successes.created")
});
if (generateLabel) {
GenerateDocument(
{
name: Templates.parts_invoice_label_single.key,
variables: {
id: billId
}
},
{},
"p"
);
}
if (billEnterModal.actions.refetch) billEnterModal.actions.refetch();
insertAuditTrail({
jobid: values.jobid,
billid: billId,
operation: AuditTrailMapping.billposted(r1.data.insert_bills.returning[0].invoice_number),
type: "billposted"
});
if (enterAgain) {
form.resetFields();
form.setFieldsValue({
...formValues,
vendorid: values.vendorid,
billlines: []
});
// form.resetFields();
} else {
toggleModalVisible();
}
setEnterAgain(false);
};
const handleCancel = () => {
const r = window.confirm(t("general.labels.cancel"));
if (r === true) {
toggleModalVisible();
}
};
useEffect(() => {
if (enterAgain) form.submit();
}, [enterAgain, form]);
useEffect(() => {
if (billEnterModal.open) {
form.setFieldsValue(formValues);
} else {
form.resetFields();
}
}, [billEnterModal.open, form, formValues]);
return (
<Modal
title={t("bills.labels.new")}
width={"98%"}
open={billEnterModal.open}
okText={t("general.actions.save")}
keyboard="false"
onOk={() => form.submit()}
onCancel={handleCancel}
afterClose={() => {
form.resetFields();
setLoading(false);
}}
footer={
<Space>
<Checkbox checked={generateLabel} onChange={(e) => setGenerateLabel(e.target.checked)}>
{t("bills.labels.generatepartslabel")}
</Checkbox>
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
<Button loading={loading} onClick={() => form.submit()}>
{t("general.actions.save")}
</Button>
{billEnterModal.context && billEnterModal.context.id ? null : (
<Button
type="primary"
loading={loading}
onClick={() => {
setEnterAgain(true);
}}
> >
<BillFormContainer {t("general.actions.saveandnew")}
form={form} </Button>
disableInvNumber={billEnterModal.context.disableInvNumber} )}
/> </Space>
</Form> }
</Modal> destroyOnClose
); >
<Form
onFinish={handleFinish}
autoComplete={"off"}
layout="vertical"
form={form}
onFinishFailed={() => {
setEnterAgain(false);
}}
>
<BillFormContainer form={form} disableInvNumber={billEnterModal.context.disableInvNumber} />
</Form>
</Modal>
);
} }
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(BillEnterModalContainer);
mapStateToProps,
mapDispatchToProps
)(BillEnterModalContainer);

View File

@@ -1,136 +1,114 @@
import {Form, Input, Table} from "antd"; import { Form, Input, Table } from "antd";
import React, {useState} from "react"; import React, { useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import {alphaSort} from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import BillFormItemsExtendedFormItem from "./bill-form-lines.extended.formitem.component"; import BillFormItemsExtendedFormItem from "./bill-form-lines.extended.formitem.component";
export default function BillFormLinesExtended({ export default function BillFormLinesExtended({ lineData, discount, form, responsibilityCenters, disabled }) {
lineData, const [search, setSearch] = useState("");
discount, const { t } = useTranslation();
form, const columns = [
responsibilityCenters, {
disabled, title: t("joblines.fields.line_desc"),
}) { dataIndex: "line_desc",
const [search, setSearch] = useState(""); key: "line_desc",
const {t} = useTranslation(); width: "10%",
const columns = [ sorter: (a, b) => alphaSort(a.line_desc, b.line_desc)
},
{
title: t("joblines.fields.oem_partno"),
dataIndex: "oem_partno",
key: "oem_partno",
width: "10%",
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno)
},
{
title: t("joblines.fields.part_type"),
dataIndex: "part_type",
key: "part_type",
width: "10%",
filters: [
{ {
title: t("joblines.fields.line_desc"), text: t("jobs.labels.partsfilter"),
dataIndex: "line_desc", value: ["PAN", "PAP", "PAL", "PAA", "PAS", "PASL"]
key: "line_desc",
width: "10%",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
}, },
{ {
title: t("joblines.fields.oem_partno"), text: t("joblines.fields.part_types.PAN"),
dataIndex: "oem_partno", value: ["PAN", "PAP"]
key: "oem_partno",
width: "10%",
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
}, },
{ {
title: t("joblines.fields.part_type"), text: t("joblines.fields.part_types.PAL"),
dataIndex: "part_type", value: ["PAL"]
key: "part_type",
width: "10%",
filters: [
{
text: t("jobs.labels.partsfilter"),
value: ["PAN", "PAP", "PAL", "PAA", "PAS", "PASL"],
},
{
text: t("joblines.fields.part_types.PAN"),
value: ["PAN", "PAP"],
},
{
text: t("joblines.fields.part_types.PAL"),
value: ["PAL"],
},
{
text: t("joblines.fields.part_types.PAA"),
value: ["PAA"],
},
{
text: t("joblines.fields.part_types.PAS"),
value: ["PAS", "PASL"],
},
],
onFilter: (value, record) => value.includes(record.part_type),
render: (text, record) =>
record.part_type
? t(`joblines.fields.part_types.${record.part_type}`)
: null,
}, },
{
text: t("joblines.fields.part_types.PAA"),
value: ["PAA"]
},
{
text: t("joblines.fields.part_types.PAS"),
value: ["PAS", "PASL"]
}
],
onFilter: (value, record) => value.includes(record.part_type),
render: (text, record) => (record.part_type ? t(`joblines.fields.part_types.${record.part_type}`) : null)
},
{ {
title: t("joblines.fields.act_price"), title: t("joblines.fields.act_price"),
dataIndex: "act_price", dataIndex: "act_price",
key: "act_price", key: "act_price",
width: "10%", width: "10%",
sorter: (a, b) => a.act_price - b.act_price, sorter: (a, b) => a.act_price - b.act_price,
shouldCellUpdate: false, shouldCellUpdate: false,
render: (text, record) => ( render: (text, record) => (
<> <>
<CurrencyFormatter> <CurrencyFormatter>
{record.db_ref === "900510" || record.db_ref === "900511" {record.db_ref === "900510" || record.db_ref === "900511" ? record.prt_dsmk_m : record.act_price}
? record.prt_dsmk_m </CurrencyFormatter>
: record.act_price} {record.part_qty ? `(x ${record.part_qty})` : null}
</CurrencyFormatter> {record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? (
{record.part_qty ? `(x ${record.part_qty})` : null} <span style={{ marginLeft: ".2rem" }}>{`(${record.prt_dsmk_p}%)`}</span>
{record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? ( ) : (
<span <></>
style={{marginLeft: ".2rem"}} )}
>{`(${record.prt_dsmk_p}%)`}</span> </>
) : ( )
<></> },
)} {
</> title: t("billlines.fields.posting"),
), dataIndex: "posting",
}, key: "posting",
{
title: t("billlines.fields.posting"),
dataIndex: "posting",
key: "posting",
render: (text, record, index) => ( render: (text, record, index) => (
<Form.Item noStyle name={["billlineskeys", record.id]}> <Form.Item noStyle name={["billlineskeys", record.id]}>
<BillFormItemsExtendedFormItem <BillFormItemsExtendedFormItem
form={form} form={form}
record={record} record={record}
index={index} index={index}
responsibilityCenters={responsibilityCenters} responsibilityCenters={responsibilityCenters}
discount={discount} discount={discount}
/> />
</Form.Item>
),
},
];
const data =
search === ""
? lineData
: lineData.filter(
(l) =>
(l.line_desc &&
l.line_desc.toLowerCase().includes(search.toLowerCase())) ||
(l.oem_partno &&
l.oem_partno.toLowerCase().includes(search.toLowerCase())) ||
(l.act_price &&
l.act_price.toString().startsWith(search.toString()))
);
return (
<Form.Item noStyle name="billlineskeys">
<button onClick={() => console.log(form.getFieldsValue())}>form</button>
<Input onChange={(e) => setSearch(e.target.value)} allowClear/>
<Table
pagination={false}
size="small"
columns={columns}
rowKey="id"
dataSource={data}
/>
</Form.Item> </Form.Item>
); )
}
];
const data =
search === ""
? lineData
: lineData.filter(
(l) =>
(l.line_desc && l.line_desc.toLowerCase().includes(search.toLowerCase())) ||
(l.oem_partno && l.oem_partno.toLowerCase().includes(search.toLowerCase())) ||
(l.act_price && l.act_price.toString().startsWith(search.toString()))
);
return (
<Form.Item noStyle name="billlineskeys">
<button onClick={() => console.log(form.getFieldsValue())}>form</button>
<Input onChange={(e) => setSearch(e.target.value)} allowClear />
<Table pagination={false} size="small" columns={columns} rowKey="id" dataSource={data} />
</Form.Item>
);
} }

View File

@@ -1,284 +1,216 @@
import React from "react"; import React from "react";
import {MinusCircleFilled, PlusCircleFilled, WarningOutlined,} from "@ant-design/icons"; import { MinusCircleFilled, PlusCircleFilled, WarningOutlined } from "@ant-design/icons";
import {Button, Form, Input, InputNumber, Select, Space, Switch} from "antd"; import { Button, Form, Input, InputNumber, Select, Space, Switch } from "antd";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {selectBodyshop} from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import CiecaSelect from "../../utils/Ciecaselect"; import CiecaSelect from "../../utils/Ciecaselect";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(BillFormItemsExtendedFormItem);
mapStateToProps,
mapDispatchToProps
)(BillFormItemsExtendedFormItem);
export function BillFormItemsExtendedFormItem({ export function BillFormItemsExtendedFormItem({
value, value,
bodyshop, bodyshop,
form, form,
record, record,
index, index,
disabled, disabled,
responsibilityCenters, responsibilityCenters,
discount, discount
}) { }) {
// const { billlineskeys } = form.getFieldsValue("billlineskeys"); // const { billlineskeys } = form.getFieldsValue("billlineskeys");
const {t} = useTranslation();
if (!value)
return (
<Button
onClick={() => {
const values = form.getFieldsValue("billlineskeys");
form.setFieldsValue({
...values,
billlineskeys: {
...(values.billlineskeys || {}),
[record.id]: {
joblineid: record.id,
line_desc: record.line_desc,
quantity: record.part_qty || 1,
actual_price: record.act_price,
cost_center: record.part_type
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
? record.part_type
: responsibilityCenters.defaults &&
(responsibilityCenters.defaults.costs[record.part_type] ||
null)
: null,
},
},
});
}}
>
<PlusCircleFilled/>
</Button>
);
const { t } = useTranslation();
if (!value)
return ( return (
<Space wrap> <Button
<Form.Item onClick={() => {
label={t("billlines.fields.line_desc")} const values = form.getFieldsValue("billlineskeys");
name={["billlineskeys", record.id, "line_desc"]}
>
<Input disabled={disabled}/>
</Form.Item>
<Form.Item
label={t("billlines.fields.quantity")}
name={["billlineskeys", record.id, "quantity"]}
>
<InputNumber precision={0} min={0} disabled={disabled}/>
</Form.Item>
<Form.Item
label={t("billlines.fields.actual_price")}
name={["billlineskeys", record.id, "actual_price"]}
>
<CurrencyInput
min={0}
disabled={disabled}
onBlur={(e) => {
const {billlineskeys} = form.getFieldsValue("billlineskeys");
form.setFieldsValue({
billlineskeys: {
...billlineskeys,
[record.id]: {
...billlineskeys[billlineskeys],
actual_cost: !!billlineskeys[billlineskeys].actual_cost
? billlineskeys[billlineskeys].actual_cost
: Math.round(
(parseFloat(e.target.value) * (1 - discount) +
Number.EPSILON) *
100
) / 100,
},
},
});
}}
/>
</Form.Item>
<Form.Item
label={t("billlines.fields.actual_cost")}
name={["billlineskeys", record.id, "actual_cost"]}
>
<CurrencyInput min={0} disabled={disabled}/>
</Form.Item>
<Form.Item shouldUpdate>
{() => {
const line = value;
if (!!!line) return null;
const lineDiscount = (
1 -
Math.round((line.actual_cost / line.actual_price) * 100) / 100
).toPrecision(2);
if (lineDiscount - discount === 0) return <div/>; form.setFieldsValue({
return <WarningOutlined style={{color: "red"}}/>; ...values,
}} billlineskeys: {
</Form.Item> ...(values.billlineskeys || {}),
<Form.Item [record.id]: {
label={t("billlines.fields.cost_center")} joblineid: record.id,
name={["billlineskeys", record.id, "cost_center"]} line_desc: record.line_desc,
> quantity: record.part_qty || 1,
<Select showSearch style={{minWidth: "3rem"}} disabled={disabled}> actual_price: record.act_price,
{bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber cost_center: record.part_type
? CiecaSelect(true, false) ? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
: responsibilityCenters.costs.map((item) => ( ? record.part_type
<Select.Option key={item.name}>{item.name}</Select.Option> : responsibilityCenters.defaults && (responsibilityCenters.defaults.costs[record.part_type] || null)
))} : null
</Select> }
</Form.Item> }
<Form.Item });
label={t("billlines.fields.location")} }}
name={["billlineskeys", record.id, "location"]} >
> <PlusCircleFilled />
<Select disabled={disabled}> </Button>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("billlines.fields.deductedfromlbr")}
name={["billlineskeys", record.id, "deductedfromlbr"]}
valuePropName="checked"
>
<Switch disabled={disabled}/>
</Form.Item>
<Form.Item shouldUpdate style={{display: "inline-block"}}>
{() => {
if (
form.getFieldsValue("billlineskeys").billlineskeys[record.id]
.deductedfromlbr
)
return (
<div>
<Form.Item
label={t("joblines.fields.mod_lbr_ty")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={[
"billlineskeys",
record.id,
"lbr_adjustment",
"mod_lbr_ty",
]}
>
<Select allowClear>
<Select.Option value="LAA">
{t("joblines.fields.lbr_types.LAA")}
</Select.Option>
<Select.Option value="LAB">
{t("joblines.fields.lbr_types.LAB")}
</Select.Option>
<Select.Option value="LAD">
{t("joblines.fields.lbr_types.LAD")}
</Select.Option>
<Select.Option value="LAE">
{t("joblines.fields.lbr_types.LAE")}
</Select.Option>
<Select.Option value="LAF">
{t("joblines.fields.lbr_types.LAF")}
</Select.Option>
<Select.Option value="LAG">
{t("joblines.fields.lbr_types.LAG")}
</Select.Option>
<Select.Option value="LAM">
{t("joblines.fields.lbr_types.LAM")}
</Select.Option>
<Select.Option value="LAR">
{t("joblines.fields.lbr_types.LAR")}
</Select.Option>
<Select.Option value="LAS">
{t("joblines.fields.lbr_types.LAS")}
</Select.Option>
<Select.Option value="LAU">
{t("joblines.fields.lbr_types.LAU")}
</Select.Option>
<Select.Option value="LA1">
{t("joblines.fields.lbr_types.LA1")}
</Select.Option>
<Select.Option value="LA2">
{t("joblines.fields.lbr_types.LA2")}
</Select.Option>
<Select.Option value="LA3">
{t("joblines.fields.lbr_types.LA3")}
</Select.Option>
<Select.Option value="LA4">
{t("joblines.fields.lbr_types.LA4")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item
label={t("jobs.labels.adjustmentrate")}
name={["billlineskeys", record.id, "lbr_adjustment", "rate"]}
initialValue={bodyshop.default_adjustment_rate}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber precision={2} min={0.01}/>
</Form.Item>
</div>
);
return <></>;
}}
</Form.Item>
<Form.Item
label={t("billlines.fields.federal_tax_applicable")}
name={["billlineskeys", record.id, "applicable_taxes", "federal"]}
valuePropName="checked"
>
<Switch disabled={disabled}/>
</Form.Item>
<Form.Item
label={t("billlines.fields.state_tax_applicable")}
name={["billlineskeys", record.id, "applicable_taxes", "state"]}
valuePropName="checked"
>
<Switch disabled={disabled}/>
</Form.Item>
<Form.Item
label={t("billlines.fields.local_tax_applicable")}
name={["billlineskeys", record.id, "applicable_taxes", "local"]}
valuePropName="checked"
>
<Switch disabled={disabled}/>
</Form.Item>
<Button
onClick={() => {
const values = form.getFieldsValue("billlineskeys");
form.setFieldsValue({
...values,
billlineskeys: {
...(values.billlineskeys || {}),
[record.id]: null,
},
});
}}
>
<MinusCircleFilled/>
</Button>
</Space>
); );
return (
<Space wrap>
<Form.Item label={t("billlines.fields.line_desc")} name={["billlineskeys", record.id, "line_desc"]}>
<Input disabled={disabled} />
</Form.Item>
<Form.Item label={t("billlines.fields.quantity")} name={["billlineskeys", record.id, "quantity"]}>
<InputNumber precision={0} min={0} disabled={disabled} />
</Form.Item>
<Form.Item label={t("billlines.fields.actual_price")} name={["billlineskeys", record.id, "actual_price"]}>
<CurrencyInput
min={0}
disabled={disabled}
onBlur={(e) => {
const { billlineskeys } = form.getFieldsValue("billlineskeys");
form.setFieldsValue({
billlineskeys: {
...billlineskeys,
[record.id]: {
...billlineskeys[billlineskeys],
actual_cost: !!billlineskeys[billlineskeys].actual_cost
? billlineskeys[billlineskeys].actual_cost
: Math.round((parseFloat(e.target.value) * (1 - discount) + Number.EPSILON) * 100) / 100
}
}
});
}}
/>
</Form.Item>
<Form.Item label={t("billlines.fields.actual_cost")} name={["billlineskeys", record.id, "actual_cost"]}>
<CurrencyInput min={0} disabled={disabled} />
</Form.Item>
<Form.Item shouldUpdate>
{() => {
const line = value;
if (!!!line) return null;
const lineDiscount = (1 - Math.round((line.actual_cost / line.actual_price) * 100) / 100).toPrecision(2);
if (lineDiscount - discount === 0) return <div />;
return <WarningOutlined style={{ color: "red" }} />;
}}
</Form.Item>
<Form.Item label={t("billlines.fields.cost_center")} name={["billlineskeys", record.id, "cost_center"]}>
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}>
{bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
? CiecaSelect(true, false)
: responsibilityCenters.costs.map((item) => <Select.Option key={item.name}>{item.name}</Select.Option>)}
</Select>
</Form.Item>
<Form.Item label={t("billlines.fields.location")} name={["billlineskeys", record.id, "location"]}>
<Select disabled={disabled}>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("billlines.fields.deductedfromlbr")}
name={["billlineskeys", record.id, "deductedfromlbr"]}
valuePropName="checked"
>
<Switch disabled={disabled} />
</Form.Item>
<Form.Item shouldUpdate style={{ display: "inline-block" }}>
{() => {
if (form.getFieldsValue("billlineskeys").billlineskeys[record.id].deductedfromlbr)
return (
<div>
<Form.Item
label={t("joblines.fields.mod_lbr_ty")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["billlineskeys", record.id, "lbr_adjustment", "mod_lbr_ty"]}
>
<Select allowClear>
<Select.Option value="LAA">{t("joblines.fields.lbr_types.LAA")}</Select.Option>
<Select.Option value="LAB">{t("joblines.fields.lbr_types.LAB")}</Select.Option>
<Select.Option value="LAD">{t("joblines.fields.lbr_types.LAD")}</Select.Option>
<Select.Option value="LAE">{t("joblines.fields.lbr_types.LAE")}</Select.Option>
<Select.Option value="LAF">{t("joblines.fields.lbr_types.LAF")}</Select.Option>
<Select.Option value="LAG">{t("joblines.fields.lbr_types.LAG")}</Select.Option>
<Select.Option value="LAM">{t("joblines.fields.lbr_types.LAM")}</Select.Option>
<Select.Option value="LAR">{t("joblines.fields.lbr_types.LAR")}</Select.Option>
<Select.Option value="LAS">{t("joblines.fields.lbr_types.LAS")}</Select.Option>
<Select.Option value="LAU">{t("joblines.fields.lbr_types.LAU")}</Select.Option>
<Select.Option value="LA1">{t("joblines.fields.lbr_types.LA1")}</Select.Option>
<Select.Option value="LA2">{t("joblines.fields.lbr_types.LA2")}</Select.Option>
<Select.Option value="LA3">{t("joblines.fields.lbr_types.LA3")}</Select.Option>
<Select.Option value="LA4">{t("joblines.fields.lbr_types.LA4")}</Select.Option>
</Select>
</Form.Item>
<Form.Item
label={t("jobs.labels.adjustmentrate")}
name={["billlineskeys", record.id, "lbr_adjustment", "rate"]}
initialValue={bodyshop.default_adjustment_rate}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<InputNumber precision={2} min={0.01} />
</Form.Item>
</div>
);
return <></>;
}}
</Form.Item>
<Form.Item
label={t("billlines.fields.federal_tax_applicable")}
name={["billlineskeys", record.id, "applicable_taxes", "federal"]}
valuePropName="checked"
>
<Switch disabled={disabled} />
</Form.Item>
<Form.Item
label={t("billlines.fields.state_tax_applicable")}
name={["billlineskeys", record.id, "applicable_taxes", "state"]}
valuePropName="checked"
>
<Switch disabled={disabled} />
</Form.Item>
<Form.Item
label={t("billlines.fields.local_tax_applicable")}
name={["billlineskeys", record.id, "applicable_taxes", "local"]}
valuePropName="checked"
>
<Switch disabled={disabled} />
</Form.Item>
<Button
onClick={() => {
const values = form.getFieldsValue("billlineskeys");
form.setFieldsValue({
...values,
billlineskeys: {
...(values.billlineskeys || {}),
[record.id]: null
}
});
}}
>
<MinusCircleFilled />
</Button>
</Space>
);
} }

View File

@@ -1,16 +1,17 @@
import Icon, {UploadOutlined} from "@ant-design/icons"; import Icon, { UploadOutlined } from "@ant-design/icons";
import {useApolloClient} from "@apollo/client"; import { useApolloClient } from "@apollo/client";
import {useSplitTreatments} from "@splitsoftware/splitio-react"; import { useSplitTreatments } from "@splitsoftware/splitio-react";
import {Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload,} from "antd"; import { Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload } from "antd";
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 dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import React, {useEffect, useState} from "react"; import InstanceRenderManager from "../../utils/instanceRenderMgr";
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 AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component"; import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component"; import FormDatePicker from "../form-date-picker/form-date-picker.component";
@@ -20,522 +21,456 @@ import JobSearchSelect from "../job-search-select/job-search-select.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component"; import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import BillFormLines from "./bill-form.lines.component"; import BillFormLines from "./bill-form.lines.component";
import {CalculateBillTotal} from "./bill-form.totals.utility"; import { CalculateBillTotal } from "./bill-form.totals.utility";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({}); const mapDispatchToProps = (dispatch) => ({});
export function BillFormComponent({ export function BillFormComponent({
bodyshop, bodyshop,
disabled, disabled,
form, form,
vendorAutoCompleteOptions, vendorAutoCompleteOptions,
lineData, lineData,
responsibilityCenters, responsibilityCenters,
loadLines, loadLines,
billEdit, billEdit,
disableInvNumber, disableInvNumber,
job, job,
loadOutstandingReturns, loadOutstandingReturns,
loadInventory, loadInventory,
preferredMake preferredMake
}) { }) {
const { t } = useTranslation();
const client = useApolloClient();
const [discount, setDiscount] = useState(0);
const {t} = useTranslation(); const {
const client = useApolloClient(); treatments: { Extended_Bill_Posting, ClosingPeriod }
const [discount, setDiscount] = useState(0); } = useSplitTreatments({
attributes: {},
names: ["Extended_Bill_Posting", "ClosingPeriod"],
splitKey: bodyshop.imexshopid
});
const {treatments: {Extended_Bill_Posting, ClosingPeriod}} = useSplitTreatments({ const handleVendorSelect = (props, opt) => {
attributes: {}, setDiscount(opt.discount);
names: ["Extended_Bill_Posting", "ClosingPeriod"],
splitKey: bodyshop.imexshopid,
});
opt &&
const handleVendorSelect = (props, opt) => { !billEdit &&
setDiscount(opt.discount); loadOutstandingReturns({
variables: {
opt && jobId: form.getFieldValue("jobid"),
!billEdit && vendorId: opt.value
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 }); };
};
useEffect(() => { const handleFederalTaxExemptSwitchToggle = (checked) => {
if (job) form.validateFields(["is_credit_memo"]); // Early gate
}, [job, form]); if (!checked) return;
const values = form.getFieldsValue("billlines");
// Gate bill lines
if (!values?.billlines?.length) return;
useEffect(() => { const billlines = values.billlines.map((b) => {
const vendorId = form.getFieldValue("vendorid"); b.applicable_taxes.federal = false;
if (vendorId && vendorAutoCompleteOptions) { return b;
const matchingVendors = vendorAutoCompleteOptions.filter( });
(v) => v.id === vendorId form.setFieldsValue({ billlines });
); };
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) { useEffect(() => {
loadInventory(); if (job) form.validateFields(["is_credit_memo"]);
} }, [job, form]);
}, [
form,
billEdit,
loadOutstandingReturns,
loadInventory,
setDiscount,
vendorAutoCompleteOptions,
loadLines,
bodyshop.inhousevendorid,
]);
return ( useEffect(() => {
<div> const vendorId = form.getFieldValue("vendorid");
<FormFieldsChanged form={form}/> if (vendorId && vendorAutoCompleteOptions) {
<Form.Item const matchingVendors = vendorAutoCompleteOptions.filter((v) => v.id === vendorId);
style={{display: "none"}} if (matchingVendors.length === 1) {
name="isinhouse" setDiscount(matchingVendors[0].discount);
valuePropName="checked" }
> }
<Switch/> const jobId = form.getFieldValue("jobid");
</Form.Item> if (jobId) {
<LayoutFormRow grow> loadLines({ variables: { id: jobId } });
<Form.Item if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) {
name="jobid" loadOutstandingReturns({
label={t("bills.fields.ro_number")} variables: {
rules={[ jobId: jobId,
{ vendorId: vendorId
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) { if (vendorId === bodyshop.inhousevendorid && !billEdit) {
return Promise.resolve(); loadInventory();
} else if ( }
response.data.bills_aggregate.nodes.length === 1 && }, [
response.data.bills_aggregate.nodes[0].id === form,
form.getFieldValue("id") billEdit,
) { loadOutstandingReturns,
return Promise.resolve(); loadInventory,
} setDiscount,
return Promise.reject( vendorAutoCompleteOptions,
t("bills.validation.unique_invoice_number") loadLines,
); bodyshop.inhousevendorid
} 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 ( return (
!bodyshop.bill_allow_post_to_closed && <div>
job && <FormFieldsChanged form={form} />
(job.status === bodyshop.md_ro_statuses.default_invoiced || <Form.Item style={{ display: "none" }} name="isinhouse" valuePropName="checked">
job.status === bodyshop.md_ro_statuses.default_exported || <Switch />
job.status === bodyshop.md_ro_statuses.default_void) &&
(value === false || !value)
) {
return Promise.reject(t("bills.labels.onlycmforinvoiced"));
}
return Promise.resolve();
},
}),
]}
>
<Switch/>
</Form.Item>
<Form.Item
label={t("bills.fields.total")}
name="total"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<CurrencyInput min={0} disabled={disabled}/>
</Form.Item>
{!billEdit && (
<Form.Item label={t("bills.fields.allpartslocation")} name="location">
<Select style={{width: "10rem"}} disabled={disabled} allowClear>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
</Form.Item>
)}
</LayoutFormRow>
<LayoutFormRow>
{
InstanceRenderManager({imex:
<Form.Item
span={3}
label={t("bills.fields.federal_tax_rate")}
name="federal_tax_rate"
>
<CurrencyInput min={0} disabled={disabled} />
</Form.Item> })
}
<Form.Item
span={3}
label={t("bills.fields.state_tax_rate")}
name="state_tax_rate"
>
<CurrencyInput min={0} disabled={disabled}/>
</Form.Item>
{
InstanceRenderManager({imex: <>
<Form.Item
span={3}
label={t("bills.fields.local_tax_rate")}
name="local_tax_rate"
>
<CurrencyInput min={0} />
</Form.Item> </Form.Item>
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? ( <LayoutFormRow grow>
<Form.Item <Form.Item
span={2} name="jobid"
label={t("bills.labels.federal_tax_exempt")} label={t("bills.fields.ro_number")}
name="federal_tax_exempt" rules={[
> {
<Switch onChange={handleFederalTaxExemptSwitchToggle} /> required: true
</Form.Item> //message: t("general.validation.required"),
) : null} }
</>}) ]}
} >
<Form.Item shouldUpdate span={13}> <JobSearchSelect
{() => { disabled={billEdit || disabled}
const values = form.getFieldsValue([ convertedOnly
"billlines", notExported={false}
"total", onBlur={() => {
"federal_tax_rate", if (form.getFieldValue("jobid") !== null && form.getFieldValue("jobid") !== undefined) {
"state_tax_rate", loadLines({ variables: { id: form.getFieldValue("jobid") } });
"local_tax_rate", if (form.getFieldValue("vendorid") !== null && form.getFieldValue("vendorid") !== undefined) {
]); loadOutstandingReturns({
let totals; variables: {
if ( jobId: form.getFieldValue("jobid"),
!!values.total && vendorId: form.getFieldValue("vendorid")
!!values.billlines &&
values.billlines.length > 0
)
totals = CalculateBillTotal(values);
if (!!totals)
return (
<div align="right">
<Space wrap>
<Statistic
title={t("bills.labels.subtotal")}
value={totals.subtotal.toFormat()}
precision={2}
/>
{
InstanceRenderManager({imex: <Statistic
title={t("bills.labels.federal_tax")}
value={totals.federalTax.toFormat()}
precision={2}
/> })
}
<Statistic
title={t("bills.labels.state_tax")}
value={totals.stateTax.toFormat()}
precision={2}
/>
{
InstanceRenderManager({imex: <Statistic
title={t("bills.labels.local_tax")}
value={totals.localTax.toFormat()}
precision={2}
/>})
}
<Statistic
title={t("bills.labels.entered_total")}
value={totals.enteredTotal.toFormat()}
precision={2}
/>
<Statistic
title={t("bills.labels.bill_total")}
value={totals.invoiceTotal.toFormat()}
precision={2}
/>
<Statistic
title={t("bills.labels.discrepancy")}
valueStyle={{
color:
totals.discrepancy.getAmount() === 0
? "green"
: "red",
}}
value={totals.discrepancy.toFormat()}
precision={2}
/>
</Space>
{form.getFieldValue("is_credit_memo") ? (
<AlertComponent
type="warning"
message={t("bills.labels.enteringcreditmemo")}
/>
) : null}
</div>
);
return null;
}}
</Form.Item>
</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}
/>
)}
<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" </Form.Item>
beforeUpload={() => false} <Form.Item
listType="picture" label={t("bills.fields.vendor")}
> name="vendorid"
<> // style={{ display: billEdit ? "none" : null }}
<p className="ant-upload-drag-icon"> rules={[
<UploadOutlined/> {
</p> required: true
<p className="ant-upload-text"> //message: t("general.validation.required"),
Click or drag files to this area to upload. },
</p> ({ getFieldValue }) => ({
</> validator(rule, value) {
</Upload.Dragger> 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"));
}
return Promise.resolve();
}
})
]}
>
<Switch />
</Form.Item>
<Form.Item
label={t("bills.fields.total")}
name="total"
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<CurrencyInput min={0} disabled={disabled} />
</Form.Item>
{!billEdit && (
<Form.Item label={t("bills.fields.allpartslocation")} name="location">
<Select style={{ width: "10rem" }} disabled={disabled} allowClear>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
</Form.Item>
)}
</LayoutFormRow>
<LayoutFormRow>
{InstanceRenderManager({
imex: (
<Form.Item span={3} label={t("bills.fields.federal_tax_rate")} name="federal_tax_rate">
<CurrencyInput min={0} disabled={disabled} />
</Form.Item> </Form.Item>
</div> )
); })}
<Form.Item span={3} label={t("bills.fields.state_tax_rate")} name="state_tax_rate">
<CurrencyInput min={0} disabled={disabled} />
</Form.Item>
{InstanceRenderManager({
imex: (
<>
<Form.Item span={3} label={t("bills.fields.local_tax_rate")} name="local_tax_rate">
<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}>
{() => {
const values = form.getFieldsValue([
"billlines",
"total",
"federal_tax_rate",
"state_tax_rate",
"local_tax_rate"
]);
let totals;
if (!!values.total && !!values.billlines && values.billlines.length > 0)
totals = CalculateBillTotal(values);
if (!!totals)
return (
<div align="right">
<Space size="large" wrap>
<Statistic title={t("bills.labels.subtotal")} value={totals.subtotal.toFormat()} precision={2} />
{InstanceRenderManager({
imex: (
<Statistic
title={t("bills.labels.federal_tax")}
value={totals.federalTax.toFormat()}
precision={2}
/>
)
})}
<Statistic title={t("bills.labels.state_tax")} value={totals.stateTax.toFormat()} precision={2} />
{InstanceRenderManager({
imex: (
<Statistic
title={t("bills.labels.local_tax")}
value={totals.localTax.toFormat()}
precision={2}
/>
)
})}
<Statistic
title={t("bills.labels.entered_total")}
value={totals.enteredTotal.toFormat()}
precision={2}
/>
<Statistic
title={t("bills.labels.bill_total")}
value={totals.invoiceTotal.toFormat()}
precision={2}
/>
<Statistic
title={t("bills.labels.discrepancy")}
valueStyle={{
color: totals.discrepancy.getAmount() === 0 ? "green" : "red"
}}
value={totals.discrepancy.toFormat()}
precision={2}
/>
</Space>
{form.getFieldValue("is_credit_memo") ? (
<AlertComponent type="warning" message={t("bills.labels.enteringcreditmemo")} />
) : null}
</div>
);
return null;
}}
</Form.Item>
</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}
/>
)}
<Divider orientation="left" style={{ display: billEdit ? "none" : null }}>
{t("documents.labels.upload")}
</Divider>
<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); export default connect(mapStateToProps, mapDispatchToProps)(BillFormComponent);

View File

@@ -1,83 +1,67 @@
import {useLazyQuery, useQuery} from "@apollo/client"; import { useLazyQuery, useQuery } from "@apollo/client";
import {useSplitTreatments} from "@splitsoftware/splitio-react"; import { useSplitTreatments } from "@splitsoftware/splitio-react";
import React from "react"; import React from "react";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {QUERY_OUTSTANDING_INVENTORY} from "../../graphql/inventory.queries"; import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries";
import {GET_JOB_LINES_TO_ENTER_BILL} from "../../graphql/jobs-lines.queries"; import { GET_JOB_LINES_TO_ENTER_BILL } from "../../graphql/jobs-lines.queries";
import {QUERY_UNRECEIVED_LINES} from "../../graphql/parts-orders.queries"; import { QUERY_UNRECEIVED_LINES } from "../../graphql/parts-orders.queries";
import {SEARCH_VENDOR_AUTOCOMPLETE} from "../../graphql/vendors.queries"; import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries";
import {selectBodyshop} from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import BillCmdReturnsTableComponent from "../bill-cm-returns-table/bill-cm-returns-table.component"; import BillCmdReturnsTableComponent from "../bill-cm-returns-table/bill-cm-returns-table.component";
import BillInventoryTable from "../bill-inventory-table/bill-inventory-table.component"; import BillInventoryTable from "../bill-inventory-table/bill-inventory-table.component";
import BillFormComponent from "./bill-form.component"; import BillFormComponent from "./bill-form.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
export function BillFormContainer({ export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber }) {
bodyshop, const {
form, treatments: { Simple_Inventory }
billEdit, } = useSplitTreatments({
disabled, attributes: {},
disableInvNumber, names: ["Simple_Inventory"],
}) { splitKey: bodyshop && bodyshop.imexshopid
const {treatments: {Simple_Inventory}} = useSplitTreatments({ });
attributes: {},
names: ["Simple_Inventory"],
splitKey: bodyshop && bodyshop.imexshopid,
});
const {data: VendorAutoCompleteData} = useQuery( const { data: VendorAutoCompleteData } = useQuery(SEARCH_VENDOR_AUTOCOMPLETE, {
SEARCH_VENDOR_AUTOCOMPLETE, fetchPolicy: "network-only",
{fetchPolicy: "network-only", nextFetchPolicy: "network-only"} nextFetchPolicy: "network-only"
); });
const [loadLines, {data: lineData}] = useLazyQuery( const [loadLines, { data: lineData }] = useLazyQuery(GET_JOB_LINES_TO_ENTER_BILL);
GET_JOB_LINES_TO_ENTER_BILL
);
const [loadOutstandingReturns, {loading: returnLoading, data: returnData}] = const [loadOutstandingReturns, { loading: returnLoading, data: returnData }] = useLazyQuery(QUERY_UNRECEIVED_LINES);
useLazyQuery(QUERY_UNRECEIVED_LINES); const [loadInventory, { loading: inventoryLoading, data: inventoryData }] = useLazyQuery(QUERY_OUTSTANDING_INVENTORY);
const [loadInventory, {loading: inventoryLoading, data: inventoryData}] =
useLazyQuery(QUERY_OUTSTANDING_INVENTORY);
return ( return (
<> <>
<BillFormComponent <BillFormComponent
disabled={disabled} disabled={disabled}
form={form} form={form}
billEdit={billEdit} billEdit={billEdit}
vendorAutoCompleteOptions={ vendorAutoCompleteOptions={VendorAutoCompleteData && VendorAutoCompleteData.vendors}
VendorAutoCompleteData && VendorAutoCompleteData.vendors loadLines={loadLines}
} lineData={lineData ? lineData.joblines : []}
loadLines={loadLines} job={lineData ? lineData.jobs_by_pk : null}
lineData={lineData ? lineData.joblines : []} responsibilityCenters={bodyshop.md_responsibility_centers || null}
job={lineData ? lineData.jobs_by_pk : null} disableInvNumber={disableInvNumber}
responsibilityCenters={bodyshop.md_responsibility_centers || null} loadOutstandingReturns={loadOutstandingReturns}
disableInvNumber={disableInvNumber} loadInventory={loadInventory}
loadOutstandingReturns={loadOutstandingReturns} preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}
loadInventory={loadInventory} />
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null} {!billEdit && <BillCmdReturnsTableComponent form={form} returnLoading={returnLoading} returnData={returnData} />}
/> {Simple_Inventory.treatment === "on" && (
{!billEdit && ( <BillInventoryTable
<BillCmdReturnsTableComponent form={form}
form={form} inventoryLoading={inventoryLoading}
returnLoading={returnLoading} inventoryData={billEdit ? [] : inventoryData}
returnData={returnData} billEdit={billEdit}
/> />
)} )}
{Simple_Inventory.treatment === "on" && ( </>
<BillInventoryTable );
form={form}
inventoryLoading={inventoryLoading}
inventoryData={billEdit ? [] : inventoryData}
billEdit={billEdit}
/>
)}
</>
);
} }
export default connect(mapStateToProps, null)(BillFormContainer); export default connect(mapStateToProps, null)(BillFormContainer);

File diff suppressed because it is too large Load Diff

View File

@@ -1,47 +1,42 @@
import Dinero from "dinero.js"; import Dinero from "dinero.js";
export const CalculateBillTotal = (invoice) => { export const CalculateBillTotal = (invoice) => {
const {total, billlines, federal_tax_rate, local_tax_rate, state_tax_rate} = const { total, billlines, federal_tax_rate, local_tax_rate, state_tax_rate } = invoice;
invoice;
//TODO Determine why this recalculates so many times. //TODO Determine why this recalculates so many times.
let subtotal = Dinero({amount: 0}); let subtotal = Dinero({ amount: 0 });
let federalTax = Dinero({amount: 0}); let federalTax = Dinero({ amount: 0 });
let stateTax = Dinero({amount: 0}); let stateTax = Dinero({ amount: 0 });
let localTax = Dinero({amount: 0}); let localTax = Dinero({ amount: 0 });
if (!!!billlines) return null; if (!!!billlines) return null;
billlines.forEach((i) => { billlines.forEach((i) => {
if (!!i) { if (!!i) {
const itemTotal = Dinero({ const itemTotal = Dinero({
amount: Math.round((i.actual_cost || 0) * 100), amount: Math.round((i.actual_cost || 0) * 100)
}).multiply(i.quantity || 1); }).multiply(i.quantity || 1);
subtotal = subtotal.add(itemTotal); subtotal = subtotal.add(itemTotal);
if (i.applicable_taxes?.federal) { if (i.applicable_taxes?.federal) {
federalTax = federalTax.add( federalTax = federalTax.add(itemTotal.percentage(federal_tax_rate || 0));
itemTotal.percentage(federal_tax_rate || 0) }
); if (i.applicable_taxes?.state) stateTax = stateTax.add(itemTotal.percentage(state_tax_rate || 0));
} if (i.applicable_taxes?.local) localTax = localTax.add(itemTotal.percentage(local_tax_rate || 0));
if (i.applicable_taxes?.state) }
stateTax = stateTax.add(itemTotal.percentage(state_tax_rate || 0)); });
if (i.applicable_taxes?.local)
localTax = localTax.add(itemTotal.percentage(local_tax_rate || 0));
}
});
const invoiceTotal = Dinero({amount: Math.round((total || 0) * 100)}); const invoiceTotal = Dinero({ amount: Math.round((total || 0) * 100) });
const enteredTotal = subtotal.add(federalTax).add(stateTax).add(localTax); const enteredTotal = subtotal.add(federalTax).add(stateTax).add(localTax);
const discrepancy = enteredTotal.subtract(invoiceTotal); const discrepancy = enteredTotal.subtract(invoiceTotal);
return { return {
subtotal, subtotal,
federalTax, federalTax,
stateTax, stateTax,
localTax, localTax,
enteredTotal, enteredTotal,
invoiceTotal, invoiceTotal,
discrepancy, discrepancy
}; };
}; };

View File

@@ -1,173 +1,153 @@
import {Checkbox, Form, Skeleton, Typography} from "antd"; import { Checkbox, Form, Skeleton, Typography } from "antd";
import React, {useEffect} from "react"; import React, { useEffect } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component"; import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
import "./bill-inventory-table.styles.scss"; import "./bill-inventory-table.styles.scss";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {selectBodyshop} from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import {selectBillEnterModal} from "../../redux/modals/modals.selectors"; import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
billEnterModal: selectBillEnterModal, billEnterModal: selectBillEnterModal
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(BillInventoryTable); export default connect(mapStateToProps, mapDispatchToProps)(BillInventoryTable);
export function BillInventoryTable({ export function BillInventoryTable({ billEnterModal, bodyshop, form, billEdit, inventoryLoading, inventoryData }) {
billEnterModal, const { t } = useTranslation();
bodyshop,
form,
billEdit,
inventoryLoading,
inventoryData,
}) {
const {t} = useTranslation();
useEffect(() => { useEffect(() => {
if (inventoryData && inventoryData.inventory) { if (inventoryData && inventoryData.inventory) {
form.setFieldsValue({ form.setFieldsValue({
inventory: billEnterModal.context.consumeinventoryid inventory: billEnterModal.context.consumeinventoryid
? inventoryData.inventory.map((i) => { ? inventoryData.inventory.map((i) => {
if (i.id === billEnterModal.context.consumeinventoryid) if (i.id === billEnterModal.context.consumeinventoryid) i.consumefrominventory = true;
i.consumefrominventory = true; return i;
return i; })
}) : inventoryData.inventory
: inventoryData.inventory, });
}); }
}, [inventoryData, form, billEnterModal.context.consumeinventoryid]);
return (
<Form.Item shouldUpdate={(prev, cur) => prev.vendorid !== cur.vendorid} noStyle>
{() => {
const is_inhouse = form.getFieldValue("vendorid") === bodyshop.inhousevendorid;
if (!is_inhouse || billEdit) {
return null;
} }
}, [inventoryData, form, billEnterModal.context.consumeinventoryid]);
return ( if (inventoryLoading) return <Skeleton />;
<Form.Item
shouldUpdate={(prev, cur) => prev.vendorid !== cur.vendorid}
noStyle
>
{() => {
const is_inhouse =
form.getFieldValue("vendorid") === bodyshop.inhousevendorid;
if (!is_inhouse || billEdit) { return (
return null; <Form.List name="inventory">
} {(fields, { add, remove, move }) => {
return (
<>
<Typography.Title level={4}>{t("inventory.labels.inventory")}</Typography.Title>
<table className="bill-inventory-table">
<thead>
<tr>
<th>{t("billlines.fields.line_desc")}</th>
<th>{t("vendors.fields.name")}</th>
<th>{t("billlines.fields.quantity")}</th>
<th>{t("billlines.fields.actual_price")}</th>
<th>{t("billlines.fields.actual_cost")}</th>
<th>{t("inventory.fields.comment")}</th>
<th>{t("inventory.actions.consumefrominventory")}</th>
</tr>
</thead>
<tbody>
{fields.map((field, index) => (
<tr key={field.key}>
<td>
<Form.Item
// label={t("joblines.fields.line_desc")}
key={`${index}line_desc`}
name={[field.name, "line_desc"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
if (inventoryLoading) return <Skeleton/>; <td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}part_type`}
name={[field.name, "billline", "bill", "vendor", "name"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}quantity`}
name={[field.name, "quantity"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}act_price`}
name={[field.name, "actual_price"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}cost`}
name={[field.name, "actual_cost"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}comment`}
name={[field.name, "comment"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
return ( <td>
<Form.List name="inventory"> <Form.Item
{(fields, {add, remove, move}) => { span={2}
return ( //label={t("joblines.fields.mod_lb_hrs")}
<> key={`${index}consumefrominventory`}
<Typography.Title level={4}> name={[field.name, "consumefrominventory"]}
{t("inventory.labels.inventory")} valuePropName="checked"
</Typography.Title> >
<table className="bill-inventory-table"> <Checkbox />
<thead> </Form.Item>
<tr> </td>
<th>{t("billlines.fields.line_desc")}</th> </tr>
<th>{t("vendors.fields.name")}</th> ))}
<th>{t("billlines.fields.quantity")}</th> </tbody>
<th>{t("billlines.fields.actual_price")}</th> </table>
<th>{t("billlines.fields.actual_cost")}</th> </>
<th>{t("inventory.fields.comment")}</th> );
<th>{t("inventory.actions.consumefrominventory")}</th>
</tr>
</thead>
<tbody>
{fields.map((field, index) => (
<tr key={field.key}>
<td>
<Form.Item
// label={t("joblines.fields.line_desc")}
key={`${index}line_desc`}
name={[field.name, "line_desc"]}
>
<ReadOnlyFormItemComponent/>
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}part_type`}
name={[
field.name,
"billline",
"bill",
"vendor",
"name",
]}
>
<ReadOnlyFormItemComponent/>
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}quantity`}
name={[field.name, "quantity"]}
>
<ReadOnlyFormItemComponent/>
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}act_price`}
name={[field.name, "actual_price"]}
>
<ReadOnlyFormItemComponent type="currency"/>
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}cost`}
name={[field.name, "actual_cost"]}
>
<ReadOnlyFormItemComponent type="currency"/>
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}comment`}
name={[field.name, "comment"]}
>
<ReadOnlyFormItemComponent/>
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}consumefrominventory`}
name={[field.name, "consumefrominventory"]}
valuePropName="checked"
>
<Checkbox/>
</Form.Item>
</td>
</tr>
))}
</tbody>
</table>
</>
);
}}
</Form.List>
);
}} }}
</Form.Item> </Form.List>
); );
}}
</Form.Item>
);
} }

View File

@@ -16,4 +16,4 @@
tr:hover { tr:hover {
background-color: #f5f5f5; background-color: #f5f5f5;
} }
} }

View File

@@ -1,98 +1,70 @@
import {Select} from "antd"; import { Select } from "antd";
import React, {forwardRef} from "react"; import React, { forwardRef } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import InstanceRenderMgr from '../../utils/instanceRenderMgr'; import InstanceRenderMgr from "../../utils/instanceRenderMgr";
//To be used as a form element only. //To be used as a form element only.
const {Option} = Select; const { Option } = Select;
const BillLineSearchSelect = ( const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps }, ref) => {
{options, disabled, allowRemoved, ...restProps}, const { t } = useTranslation();
ref
) => {
const {t} = useTranslation();
return ( return (
<Select <Select
disabled={disabled} disabled={disabled}
ref={ref} ref={ref}
showSearch showSearch
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
optionLabelProp={"name"} optionLabelProp={"name"}
// optionFilterProp="line_desc" // optionFilterProp="line_desc"
filterOption={(inputValue, option) => { filterOption={(inputValue, option) => {
return ( return (
(option.line_desc && (option.line_desc && option.line_desc.toLowerCase().includes(inputValue.toLowerCase())) ||
option.line_desc (option.oem_partno && option.oem_partno.toLowerCase().includes(inputValue.toLowerCase())) ||
.toLowerCase() (option.alt_partno && option.alt_partno.toLowerCase().includes(inputValue.toLowerCase())) ||
.includes(inputValue.toLowerCase())) || (option.act_price && option.act_price.toString().startsWith(inputValue.toString()))
(option.oem_partno && );
option.oem_partno }}
.toLowerCase() notFoundContent={"Removed."}
.includes(inputValue.toLowerCase())) || options={[
(option.alt_partno && { value: "noline", label: t("billlines.labels.other"), name: t("billlines.labels.other") },
option.alt_partno ...options.map((item) => ({
.toLowerCase() disabled: allowRemoved ? false : item.removed,
.includes(inputValue.toLowerCase())) || key: item.id,
(option.act_price && value: item.id,
option.act_price.toString().startsWith(inputValue.toString())) cost: item.act_price ? item.act_price : 0,
); part_type: item.part_type,
}} line_desc: item.line_desc,
notFoundContent={"Removed."} part_qty: item.part_qty,
{...restProps} oem_partno: item.oem_partno,
> alt_partno: item.alt_partno,
<Select.Option key={null} value={"noline"} cost={0} line_desc={""}> act_price: item.act_price,
{t("billlines.labels.other")} style: {
</Select.Option> ...(item.removed ? { textDecoration: "line-through" } : {})
{options },
? options.map((item) => ( name: `${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
<Option item.oem_partno ? ` - ${item.oem_partno}` : ""
disabled={allowRemoved ? false : item.removed} }${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim(),
key={item.id} label: (
value={item.id} <>
cost={item.act_price ? item.act_price : 0}
part_type={item.part_type}
line_desc={item.line_desc}
part_qty={item.part_qty}
oem_partno={item.oem_partno}
alt_partno={item.alt_partno}
act_price={item.act_price}
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> <span>
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${ {`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
item.oem_partno ? ` - ${item.oem_partno}` : "" item.oem_partno ? ` - ${item.oem_partno}` : ""
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()} }${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()}
</span> </span>
{ {InstanceRenderMgr({
InstanceRenderMgr rome: item.act_price === 0 && item.mod_lb_hrs > 0 && (
( <span style={{ float: "right", paddingleft: "1rem" }}>{`${item.mod_lb_hrs} units`}</span>
{ )
rome: item.act_price === 0 && item.mod_lb_hrs > 0 && ( })}
<span style={{float: "right", paddingleft: "1rem"}}> <span style={{ float: "right", paddingleft: "1rem" }}>
{`${item.mod_lb_hrs} units`} {item.act_price ? `$${item.act_price && item.act_price.toFixed(2)}` : ``}
</span>
)
}
)
}
<span style={{float: "right", paddingleft: "1rem"}}>
{item.act_price
? `$${item.act_price && item.act_price.toFixed(2)}`
: ``}
</span> </span>
</Option> </>
)) )
: null} }))
</Select> ]}
); {...restProps}
></Select>
);
}; };
export default forwardRef(BillLineSearchSelect); export default forwardRef(BillLineSearchSelect);

View File

@@ -1,97 +1,89 @@
import {gql, useMutation} from "@apollo/client"; import { gql, useMutation } from "@apollo/client";
import {Button, notification} from "antd"; import { Button, notification } from "antd";
import React, {useState} from "react"; import React, { useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {selectAuthLevel, selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; import { selectAuthLevel, selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import {HasRbacAccess} from "../rbac-wrapper/rbac-wrapper.component"; import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
import {INSERT_EXPORT_LOG} from "../../graphql/accounting.queries"; import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
authLevel: selectAuthLevel, authLevel: selectAuthLevel,
currentUser: selectCurrentUser, currentUser: selectCurrentUser
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(BillMarkExportedButton);
mapStateToProps,
mapDispatchToProps
)(BillMarkExportedButton);
export function BillMarkExportedButton({ export function BillMarkExportedButton({ currentUser, bodyshop, authLevel, bill }) {
currentUser, const { t } = useTranslation();
bodyshop, const [loading, setLoading] = useState(false);
authLevel, const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
bill,
}) {
const {t} = useTranslation();
const [loading, setLoading] = useState(false);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
const [updateBill] = useMutation(gql` const [updateBill] = useMutation(gql`
mutation UPDATE_BILL($billId: uuid!) { mutation UPDATE_BILL($billId: uuid!) {
update_bills(where: { id: { _eq: $billId } }, _set: { exported: true }) { update_bills(where: { id: { _eq: $billId } }, _set: { exported: true }) {
returning { returning {
id id
exported exported
exported_at exported_at
}
}
} }
`); }
}
`);
const handleUpdate = async () => { const handleUpdate = async () => {
setLoading(true); setLoading(true);
const result = await updateBill({ const result = await updateBill({
variables: {billId: bill.id}, variables: { billId: bill.id }
});
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
billid: bill.id,
successful: true,
message: JSON.stringify([t("general.labels.markedexported")]),
useremail: currentUser.email,
},
],
},
});
if (!result.errors) {
notification["success"]({
message: t("bills.successes.markexported"),
});
} else {
notification["error"]({
message: t("bills.errors.saving", {
error: JSON.stringify(result.errors),
}),
});
}
setLoading(false);
//Get the owner details, populate it all back into the job.
};
const hasAccess = HasRbacAccess({
bodyshop,
authLevel,
action: "bills:reexport",
}); });
if (hasAccess) await insertExportLog({
return ( variables: {
<Button loading={loading} disabled={bill.exported} onClick={handleUpdate}> logs: [
{t("bills.labels.markexported")} {
</Button> bodyshopid: bodyshop.id,
); billid: bill.id,
successful: true,
message: JSON.stringify([t("general.labels.markedexported")]),
useremail: currentUser.email
}
]
}
});
return <></>; if (!result.errors) {
notification["success"]({
message: t("bills.successes.markexported")
});
} else {
notification["error"]({
message: t("bills.errors.saving", {
error: JSON.stringify(result.errors)
})
});
}
setLoading(false);
//Get the owner details, populate it all back into the job.
};
const hasAccess = HasRbacAccess({
bodyshop,
authLevel,
action: "bills:reexport"
});
if (hasAccess)
return (
<Button loading={loading} disabled={bill.exported} onClick={handleUpdate}>
{t("bills.labels.markexported")}
</Button>
);
return <></>;
} }

View File

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

View File

@@ -1,79 +1,72 @@
import {gql, useMutation} from "@apollo/client"; import { gql, useMutation } from "@apollo/client";
import {Button, notification} from "antd"; import { Button, notification } from "antd";
import React, {useState} from "react"; import React, { useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {selectAuthLevel, selectBodyshop,} from "../../redux/user/user.selectors"; import { selectAuthLevel, selectBodyshop } from "../../redux/user/user.selectors";
import {HasRbacAccess} from "../rbac-wrapper/rbac-wrapper.component"; import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
authLevel: selectAuthLevel, authLevel: selectAuthLevel
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(BillMarkForReexportButton);
mapStateToProps,
mapDispatchToProps
)(BillMarkForReexportButton);
export function BillMarkForReexportButton({bodyshop, authLevel, bill}) { export function BillMarkForReexportButton({ bodyshop, authLevel, bill }) {
const {t} = useTranslation(); const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [updateBill] = useMutation(gql` const [updateBill] = useMutation(gql`
mutation UPDATE_BILL($billId: uuid!) { mutation UPDATE_BILL($billId: uuid!) {
update_bills(where: { id: { _eq: $billId } }, _set: { exported: false }) { update_bills(where: { id: { _eq: $billId } }, _set: { exported: false }) {
returning { returning {
id id
exported exported
exported_at exported_at
}
}
} }
`); }
}
`);
const handleUpdate = async () => { const handleUpdate = async () => {
setLoading(true); setLoading(true);
const result = await updateBill({ const result = await updateBill({
variables: {billId: bill.id}, variables: { billId: bill.id }
});
if (!result.errors) {
notification["success"]({
message: t("bills.successes.reexport"),
});
} else {
notification["error"]({
message: t("bills.errors.saving", {
error: JSON.stringify(result.errors),
}),
});
}
setLoading(false);
//Get the owner details, populate it all back into the job.
};
const hasAccess = HasRbacAccess({
bodyshop,
authLevel,
action: "bills:reexport",
}); });
if (hasAccess) if (!result.errors) {
return ( notification["success"]({
<Button message: t("bills.successes.reexport")
loading={loading} });
disabled={!bill.exported} } else {
onClick={handleUpdate} notification["error"]({
> message: t("bills.errors.saving", {
{t("bills.labels.markforreexport")} error: JSON.stringify(result.errors)
</Button> })
); });
}
setLoading(false);
//Get the owner details, populate it all back into the job.
};
return <></>; const hasAccess = HasRbacAccess({
bodyshop,
authLevel,
action: "bills:reexport"
});
if (hasAccess)
return (
<Button loading={loading} disabled={!bill.exported} onClick={handleUpdate}>
{t("bills.labels.markforreexport")}
</Button>
);
return <></>;
} }

View File

@@ -1,151 +1,136 @@
import {FileAddFilled} from "@ant-design/icons"; import { FileAddFilled } from "@ant-design/icons";
import {useMutation} from "@apollo/client"; import { useMutation } from "@apollo/client";
import {Button, notification, Tooltip} from "antd"; import { Button, notification, Tooltip } from "antd";
import {t} from "i18next"; import { t } from "i18next";
import dayjs from "./../../utils/day"; import dayjs from "./../../utils/day";
import React, {useState} from "react"; import React, { useState } from "react";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {INSERT_INVENTORY_AND_CREDIT} from "../../graphql/inventory.queries"; import { INSERT_INVENTORY_AND_CREDIT } from "../../graphql/inventory.queries";
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import {CalculateBillTotal} from "../bill-form/bill-form.totals.utility"; import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
import queryString from "query-string"; import queryString from "query-string";
import {useLocation} from "react-router-dom"; import { useLocation } from "react-router-dom";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser, currentUser: selectCurrentUser
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(BilllineAddInventory);
mapStateToProps,
mapDispatchToProps
)(BilllineAddInventory);
export function BilllineAddInventory({ export function BilllineAddInventory({ currentUser, bodyshop, billline, disabled, jobid }) {
currentUser, const [loading, setLoading] = useState(false);
bodyshop, const { billid } = queryString.parse(useLocation().search);
billline, const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT);
disabled,
jobid,
}) {
const [loading, setLoading] = useState(false);
const {billid} = queryString.parse(useLocation().search);
const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT);
const addToInventory = async () => { const addToInventory = async () => {
setLoading(true); setLoading(true);
//Check to make sure there are no existing items already in the inventory. //Check to make sure there are no existing items already in the inventory.
const cm = { const cm = {
vendorid: bodyshop.inhousevendorid, vendorid: bodyshop.inhousevendorid,
invoice_number: "ih", invoice_number: "ih",
jobid: jobid, jobid: jobid,
isinhouse: true, isinhouse: true,
is_credit_memo: true, is_credit_memo: true,
date: dayjs().format("YYYY-MM-DD"), date: dayjs().format("YYYY-MM-DD"),
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate, federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate,
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate, state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate,
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate, local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate,
total: 0, total: 0,
billlines: [ billlines: [
{ {
actual_price: billline.actual_price, actual_price: billline.actual_price,
actual_cost: billline.actual_cost, actual_cost: billline.actual_cost,
quantity: billline.quantity, quantity: billline.quantity,
line_desc: billline.line_desc, line_desc: billline.line_desc,
cost_center: billline.cost_center, cost_center: billline.cost_center,
deductedfromlbr: billline.deductedfromlbr, deductedfromlbr: billline.deductedfromlbr,
applicable_taxes: { applicable_taxes: {
local: billline.applicable_taxes.local, local: billline.applicable_taxes.local,
state: billline.applicable_taxes.state, state: billline.applicable_taxes.state,
federal: billline.applicable_taxes.federal, federal: billline.applicable_taxes.federal
}, }
},
],
};
cm.total = CalculateBillTotal(cm).enteredTotal.getAmount() / 100;
const insertResult = await insertInventoryLine({
variables: {
joblineId:
billline.joblineid === "noline" ? billline.id : billline.joblineid, //This will return null as there will be no jobline that has the id of the bill line.
//Unfortunately, we can't send null as the GQL syntax validation fails.
joblineStatus: bodyshop.md_order_statuses.default_returned,
inv: {
shopid: bodyshop.id,
billlineid: billline.id,
actual_price: billline.actual_price,
actual_cost: billline.actual_cost,
quantity: billline.quantity,
line_desc: billline.line_desc,
},
cm: {...cm, billlines: {data: cm.billlines}}, //Fix structure for apollo insert.
pol: {
returnfrombill: billid,
vendorid: bodyshop.inhousevendorid,
deliver_by: dayjs().format("YYYY-MM-DD"),
parts_order_lines: {
data: [
{
line_desc: billline.line_desc,
act_price: billline.actual_price,
cost: billline.actual_cost,
quantity: billline.quantity,
job_line_id:
billline.joblineid === "noline" ? null : billline.joblineid,
part_type: billline.jobline && billline.jobline.part_type,
cm_received: true,
},
],
},
order_date: "2022-06-01",
orderedby: currentUser.email,
jobid: jobid,
user_email: currentUser.email,
return: true,
status: "Ordered",
},
},
refetchQueries: ["QUERY_BILL_BY_PK"],
});
if (!insertResult.errors) {
notification.open({
type: "success",
message: t("inventory.successes.inserted"),
});
} else {
notification.open({
type: "error",
message: t("inventory.errors.inserting", {
error: JSON.stringify(insertResult.errors),
}),
});
} }
]
setLoading(false);
}; };
return ( cm.total = CalculateBillTotal(cm).enteredTotal.getAmount() / 100;
<Tooltip title={t("inventory.actions.addtoinventory")}>
<Button const insertResult = await insertInventoryLine({
loading={loading} variables: {
disabled={ joblineId: billline.joblineid === "noline" ? billline.id : billline.joblineid, //This will return null as there will be no jobline that has the id of the bill line.
disabled || billline?.inventories?.length >= billline.quantity //Unfortunately, we can't send null as the GQL syntax validation fails.
} joblineStatus: bodyshop.md_order_statuses.default_returned,
onClick={addToInventory} inv: {
> shopid: bodyshop.id,
<FileAddFilled/> billlineid: billline.id,
{billline?.inventories?.length > 0 && ( actual_price: billline.actual_price,
<div>({billline?.inventories?.length} in inv)</div> actual_cost: billline.actual_cost,
)} quantity: billline.quantity,
</Button> line_desc: billline.line_desc
</Tooltip> },
); cm: { ...cm, billlines: { data: cm.billlines } }, //Fix structure for apollo insert.
pol: {
returnfrombill: billid,
vendorid: bodyshop.inhousevendorid,
deliver_by: dayjs().format("YYYY-MM-DD"),
parts_order_lines: {
data: [
{
line_desc: billline.line_desc,
act_price: billline.actual_price,
cost: billline.actual_cost,
quantity: billline.quantity,
job_line_id: billline.joblineid === "noline" ? null : billline.joblineid,
part_type: billline.jobline && billline.jobline.part_type,
cm_received: true
}
]
},
order_date: "2022-06-01",
orderedby: currentUser.email,
jobid: jobid,
user_email: currentUser.email,
return: true,
status: "Ordered"
}
},
refetchQueries: ["QUERY_BILL_BY_PK"]
});
if (!insertResult.errors) {
notification.open({
type: "success",
message: t("inventory.successes.inserted")
});
} else {
notification.open({
type: "error",
message: t("inventory.errors.inserting", {
error: JSON.stringify(insertResult.errors)
})
});
}
setLoading(false);
};
return (
<Tooltip title={t("inventory.actions.addtoinventory")}>
<Button
loading={loading}
disabled={disabled || billline?.inventories?.length >= billline.quantity}
onClick={addToInventory}
>
<FileAddFilled />
{billline?.inventories?.length > 0 && <div>({billline?.inventories?.length} in inv)</div>}
</Button>
</Tooltip>
);
} }

View File

@@ -1,236 +1,236 @@
import {EditFilled, SyncOutlined} from "@ant-design/icons"; import { EditFilled, SyncOutlined } from "@ant-design/icons";
import {Button, Card, Checkbox, Input, Space, Table} from "antd"; import { Button, Card, Checkbox, Input, Space, Table } from "antd";
import React, {useState} from "react"; import React, { useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {selectJobReadOnly} from "../../redux/application/application.selectors"; import { selectJobReadOnly } from "../../redux/application/application.selectors";
import {setModalContext} from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
import {selectBodyshop} from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import {DateFormatter} from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import {alphaSort, dateSort} from "../../utils/sorters"; import { alphaSort, dateSort } from "../../utils/sorters";
import {TemplateList} from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component"; import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component"; import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component"; import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
import { FaTasks } from "react-icons/fa";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly, jobRO: selectJobReadOnly,
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) => setBillEnterContext: (context) =>
dispatch(setModalContext({context: context, modal: "partsOrder"})), dispatch(
setBillEnterContext: (context) => setModalContext({
dispatch(setModalContext({context: context, modal: "billEnter"})), context: context,
setReconciliationContext: (context) => modal: "billEnter"
dispatch(setModalContext({context: context, modal: "reconciliation"})), })
),
setReconciliationContext: (context) =>
dispatch(
setModalContext({
context: context,
modal: "reconciliation"
})
),
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
}); });
export function BillsListTableComponent({ export function BillsListTableComponent({
bodyshop, bodyshop,
jobRO, jobRO,
job, job,
billsQuery, billsQuery,
handleOnRowClick, handleOnRowClick,
setPartsOrderContext, setBillEnterContext,
setBillEnterContext, setReconciliationContext,
setReconciliationContext, setTaskUpsertContext
}) { }) {
const {t} = useTranslation(); const { t } = useTranslation();
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {}
}); });
// const search = queryString.parse(useLocation().search); // const search = queryString.parse(useLocation().search);
// const selectedBill = search.billid; // const selectedBill = search.billid;
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const Templates = TemplateList("bill"); const Templates = TemplateList("bill");
const bills = billsQuery.data ? billsQuery.data.bills : []; const bills = billsQuery.data ? billsQuery.data.bills : [];
const {refetch} = billsQuery; const { refetch } = billsQuery;
const recordActions = (record, showView = false) => (
<Space wrap>
{showView && (
<Button onClick={() => handleOnRowClick(record)}>
<EditFilled/>
</Button>
)}
<BillDeleteButton bill={record}/>
<BillDetailEditReturnComponent
data={{bills_by_pk: {...record, jobid: job.id}}}
disabled={
record.is_credit_memo ||
record.vendorid === bodyshop.inhousevendorid ||
jobRO
}
/>
{record.isinhouse && ( const recordActions = (record, showView = false) => (
<PrintWrapperComponent <Space wrap>
templateObject={{ {showView && (
name: Templates.inhouse_invoice.key, <Button onClick={() => handleOnRowClick(record)}>
variables: {id: record.id}, <EditFilled />
}} </Button>
messageObject={{subject: Templates.inhouse_invoice.subject}} )}
/> <Button
)} title={t("tasks.buttons.create")}
</Space> onClick={() => {
); setTaskUpsertContext({
const columns = [ context: {
{ jobid: job.id,
title: t("bills.fields.vendorname"), billid: record.id
dataIndex: "vendorname",
key: "vendorname",
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
sortOrder:
state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
render: (text, record) => <span>{record.vendor.name}</span>,
},
{
title: t("bills.fields.invoice_number"),
dataIndex: "invoice_number",
key: "invoice_number",
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
sortOrder:
state.sortedInfo.columnKey === "invoice_number" &&
state.sortedInfo.order,
},
{
title: t("bills.fields.date"),
dataIndex: "date",
key: "date",
sorter: (a, b) => dateSort(a.date, b.date),
sortOrder:
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
},
{
title: t("bills.fields.total"),
dataIndex: "total",
key: "total",
sorter: (a, b) => a.total - b.total,
sortOrder:
state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.total}</CurrencyFormatter>
),
},
{
title: t("bills.fields.is_credit_memo"),
dataIndex: "is_credit_memo",
key: "is_credit_memo",
sorter: (a, b) => a.is_credit_memo - b.is_credit_memo,
sortOrder:
state.sortedInfo.columnKey === "is_credit_memo" &&
state.sortedInfo.order,
render: (text, record) => <Checkbox checked={record.is_credit_memo}/>,
},
{
title: t("bills.fields.exported"),
dataIndex: "exported",
key: "exported",
sorter: (a, b) => a.exported - b.exported,
sortOrder:
state.sortedInfo.columnKey === "exported" && state.sortedInfo.order,
render: (text, record) => <Checkbox checked={record.exported}/>,
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
render: (text, record) => recordActions(record, true),
},
];
const handleTableChange = (pagination, filters, sorter) => {
setState({...state, filteredInfo: filters, sortedInfo: sorter});
};
const filteredBills = bills
? searchText === ""
? bills
: bills.filter(
(b) =>
(b.invoice_number || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(b.vendor.name || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(b.total || "")
.toString()
.toLowerCase()
.includes(searchText.toLowerCase())
)
: [];
return (
<Card
title={t("bills.labels.bills")}
extra={
<Space wrap>
<Button onClick={() => refetch()}>
<SyncOutlined/>
</Button>
{job && job.converted ? (
<>
<Button
onClick={() => {
setBillEnterContext({
actions: {refetch: billsQuery.refetch},
context: {
job,
},
});
}}
>
{t("jobs.actions.postbills")}
</Button>
<Button
onClick={() => {
setReconciliationContext({
actions: {refetch: billsQuery.refetch},
context: {
job,
bills: (billsQuery.data && billsQuery.data.bills) || [],
},
});
}}
>
{t("jobs.actions.reconcile")}
</Button>
</>
) : null}
<Input.Search
placeholder={t("general.labels.search")}
value={searchText}
onChange={(e) => {
e.preventDefault();
setSearchText(e.target.value);
}}
/>
</Space>
} }
> });
<Table }}
loading={billsQuery.loading} >
scroll={{ <FaTasks />
x: true, // y: "50rem" </Button>
<BillDeleteButton bill={record} jobid={job.id} />
<BillDetailEditReturnComponent
data={{ bills_by_pk: { ...record, jobid: job.id, job: job } }}
disabled={record.is_credit_memo || record.vendorid === bodyshop.inhousevendorid || jobRO}
/>
{record.isinhouse && (
<PrintWrapperComponent
templateObject={{
name: Templates.inhouse_invoice.key,
variables: { id: record.id }
}}
messageObject={{ subject: Templates.inhouse_invoice.subject }}
/>
)}
</Space>
);
const columns = [
{
title: t("bills.fields.vendorname"),
dataIndex: "vendorname",
key: "vendorname",
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
sortOrder: state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
render: (text, record) => <span>{record.vendor.name}</span>
},
{
title: t("bills.fields.invoice_number"),
dataIndex: "invoice_number",
key: "invoice_number",
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
sortOrder: state.sortedInfo.columnKey === "invoice_number" && state.sortedInfo.order
},
{
title: t("bills.fields.date"),
dataIndex: "date",
key: "date",
sorter: (a, b) => dateSort(a.date, b.date),
sortOrder: state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>
},
{
title: t("bills.fields.total"),
dataIndex: "total",
key: "total",
sorter: (a, b) => a.total - b.total,
sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
render: (text, record) => <CurrencyFormatter>{record.total}</CurrencyFormatter>
},
{
title: t("bills.fields.is_credit_memo"),
dataIndex: "is_credit_memo",
key: "is_credit_memo",
sorter: (a, b) => a.is_credit_memo - b.is_credit_memo,
sortOrder: state.sortedInfo.columnKey === "is_credit_memo" && state.sortedInfo.order,
render: (text, record) => <Checkbox checked={record.is_credit_memo} />
},
{
title: t("bills.fields.exported"),
dataIndex: "exported",
key: "exported",
sorter: (a, b) => a.exported - b.exported,
sortOrder: state.sortedInfo.columnKey === "exported" && state.sortedInfo.order,
render: (text, record) => <Checkbox checked={record.exported} />
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
render: (text, record) => recordActions(record, true)
}
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const filteredBills = bills
? searchText === ""
? bills
: bills.filter(
(b) =>
(b.invoice_number || "").toLowerCase().includes(searchText.toLowerCase()) ||
(b.vendor.name || "").toLowerCase().includes(searchText.toLowerCase()) ||
(b.total || "").toString().toLowerCase().includes(searchText.toLowerCase())
)
: [];
return (
<Card
title={t("bills.labels.bills")}
extra={
<Space wrap>
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
{job && job.converted ? (
<>
<Button
onClick={() => {
setBillEnterContext({
actions: { refetch: billsQuery.refetch },
context: {
job
}
});
}} }}
columns={columns} >
rowKey="id" {t("jobs.actions.postbills")}
dataSource={filteredBills} </Button>
onChange={handleTableChange} <Button
/> onClick={() => {
</Card> setReconciliationContext({
); actions: { refetch: billsQuery.refetch },
context: {
job,
bills: (billsQuery.data && billsQuery.data.bills) || []
}
});
}}
>
{t("jobs.actions.reconcile")}
</Button>
</>
) : null}
<Input.Search
placeholder={t("general.labels.search")}
value={searchText}
onChange={(e) => {
e.preventDefault();
setSearchText(e.target.value);
}}
/>
</Space>
}
>
<Table
loading={billsQuery.loading}
scroll={{
x: true // y: "50rem"
}}
columns={columns}
rowKey="id"
dataSource={filteredBills}
onChange={handleTableChange}
/>
</Card>
);
} }
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(BillsListTableComponent);
mapStateToProps,
mapDispatchToProps
)(BillsListTableComponent);

View File

@@ -1,121 +1,112 @@
import React, {useState} from "react"; import React, { useState } from "react";
import {QUERY_ALL_VENDORS} from "../../graphql/vendors.queries"; import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
import {useQuery} from "@apollo/client"; import { useQuery } from "@apollo/client";
import queryString from "query-string"; import queryString from "query-string";
import {useLocation, useNavigate} from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import {Input, Table} from "antd"; import { Input, Table } from "antd";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {alphaSort} from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
export default function BillsVendorsList() { export default function BillsVendorsList() {
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const history = useNavigate(); const history = useNavigate();
const {loading, error, data} = useQuery(QUERY_ALL_VENDORS, { const { loading, error, data } = useQuery(QUERY_ALL_VENDORS, {
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only"
}); });
const {t} = useTranslation(); const { t } = useTranslation();
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
search: "", search: ""
}); });
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({...state, filteredInfo: filters, sortedInfo: sorter}); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
}; };
const columns = [ const columns = [
{ {
title: t("vendors.fields.name"), title: t("vendors.fields.name"),
dataIndex: "name", dataIndex: "name",
key: "name", key: "name",
sorter: (a, b) => alphaSort(a.name, b.name), sorter: (a, b) => alphaSort(a.name, b.name),
sortOrder: sortOrder: state.sortedInfo.columnKey === "name" && state.sortedInfo.order
state.sortedInfo.columnKey === "name" && state.sortedInfo.order, },
{
title: t("vendors.fields.cost_center"),
dataIndex: "cost_center",
key: "cost_center",
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
sortOrder: state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order
},
{
title: t("vendors.fields.city"),
dataIndex: "city",
key: "city"
}
];
const handleOnRowClick = (record) => {
if (record) {
delete search.billid;
if (record.id) {
search.vendorid = record.id;
history.push({ search: queryString.stringify(search) });
}
} else {
delete search.vendorid;
history.push({ search: queryString.stringify(search) });
}
};
const handleSearch = (e) => {
setState({ ...state, search: e.target.value });
};
if (error) return <AlertComponent message={error.message} type="error" />;
const dataSource = state.search
? data.vendors.filter(
(v) =>
(v.name || "").toLowerCase().includes(state.search.toLowerCase()) ||
(v.cost_center || "").toLowerCase().includes(state.search.toLowerCase()) ||
(v.city || "").toLowerCase().includes(state.search.toLowerCase())
)
: (data && data.vendors) || [];
return (
<Table
loading={loading}
title={() => {
return (
<div>
<Input value={state.search} onChange={handleSearch} placeholder={t("general.labels.search")} allowClear />
</div>
);
}}
dataSource={dataSource}
pagination={{ position: "top" }}
columns={columns}
rowKey="id"
onChange={handleTableChange}
rowSelection={{
onSelect: (record) => {
handleOnRowClick(record);
}, },
{ selectedRowKeys: [search.vendorid],
title: t("vendors.fields.cost_center"), type: "radio"
dataIndex: "cost_center", }}
key: "cost_center", onRow={(record, rowIndex) => {
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center), return {
sortOrder: onClick: (event) => {
state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order, handleOnRowClick(record);
}, } // click row
{ };
title: t("vendors.fields.city"), }}
dataIndex: "city", />
key: "city", );
},
];
const handleOnRowClick = (record) => {
if (record) {
delete search.billid;
if (record.id) {
search.vendorid = record.id;
history.push({search: queryString.stringify(search)});
}
} else {
delete search.vendorid;
history.push({search: queryString.stringify(search)});
}
};
const handleSearch = (e) => {
setState({...state, search: e.target.value});
};
if (error) return <AlertComponent message={error.message} type="error"/>;
const dataSource = state.search
? data.vendors.filter(
(v) =>
(v.name || "").toLowerCase().includes(state.search.toLowerCase()) ||
(v.cost_center || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(v.city || "").toLowerCase().includes(state.search.toLowerCase())
)
: (data && data.vendors) || [];
return (
<Table
loading={loading}
title={() => {
return (
<div>
<Input
value={state.search}
onChange={handleSearch}
placeholder={t("general.labels.search")}
allowClear
/>
</div>
);
}}
dataSource={dataSource}
pagination={{position: "top"}}
columns={columns}
rowKey="id"
onChange={handleTableChange}
rowSelection={{
onSelect: (record) => {
handleOnRowClick(record);
},
selectedRowKeys: [search.vendorid],
type: "radio",
}}
onRow={(record, rowIndex) => {
return {
onClick: (event) => {
handleOnRowClick(record);
}, // click row
};
}}
/>
);
} }

View File

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

View File

@@ -1,99 +1,87 @@
import {Button, Form, Modal} from "antd"; import { Button, Form, Modal } from "antd";
import React, {useEffect, useState} from "react"; import React, { useEffect, useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {logImEXEvent} from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import {toggleModalVisible} from "../../redux/modals/modals.actions"; import { toggleModalVisible } from "../../redux/modals/modals.actions";
import {selectCaBcEtfTableConvert} from "../../redux/modals/modals.selectors"; import { selectCaBcEtfTableConvert } from "../../redux/modals/modals.selectors";
import {GenerateDocument} from "../../utils/RenderTemplate"; import { GenerateDocument } from "../../utils/RenderTemplate";
import {TemplateList} from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import CaBcEtfTableModalComponent from "./ca-bc-etf-table.modal.component"; import CaBcEtfTableModalComponent from "./ca-bc-etf-table.modal.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
caBcEtfTableModal: selectCaBcEtfTableConvert, caBcEtfTableModal: selectCaBcEtfTableConvert
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => toggleModalVisible: () => dispatch(toggleModalVisible("ca_bc_eftTableConvert"))
dispatch(toggleModalVisible("ca_bc_eftTableConvert")),
}); });
export function ContractsFindModalContainer({ export function ContractsFindModalContainer({ caBcEtfTableModal, toggleModalVisible }) {
caBcEtfTableModal, const { t } = useTranslation();
toggleModalVisible,
}) {
const {t} = useTranslation();
const {open} = caBcEtfTableModal; const { open } = caBcEtfTableModal;
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [form] = Form.useForm(); const [form] = Form.useForm();
const EtfTemplate = TemplateList("special").ca_bc_etf_table; const EtfTemplate = TemplateList("special").ca_bc_etf_table;
const handleFinish = async (values) => { const handleFinish = async (values) => {
logImEXEvent("ca_bc_etf_table_parse"); logImEXEvent("ca_bc_etf_table_parse");
setLoading(true); setLoading(true);
const claimNumbers = []; const claimNumbers = [];
values.table.split("\n").forEach((row, idx, arr) => { values.table.split("\n").forEach((row, idx, arr) => {
const {1: claim, 2: shortclaim, 4: amount} = row.split("\t"); const { 1: claim, 2: shortclaim, 4: amount } = row.split("\t");
if (!claim || !shortclaim) return; if (!claim || !shortclaim) return;
const trimmedShortClaim = shortclaim.trim(); const trimmedShortClaim = shortclaim.trim();
// const trimmedClaim = claim.trim(); // const trimmedClaim = claim.trim();
if (amount.slice(-1) === "-") { if (amount.slice(-1) === "-") {
} }
claimNumbers.push({ claimNumbers.push({
claim: trimmedShortClaim, claim: trimmedShortClaim,
amount: amount.slice(-1) === "-" ? parseFloat(amount) * -1 : amount, amount: amount.slice(-1) === "-" ? parseFloat(amount) * -1 : amount
}); });
}); });
await GenerateDocument( await GenerateDocument(
{ {
name: EtfTemplate.key, name: EtfTemplate.key,
variables: { variables: {
claimNumbers: `%(${claimNumbers.map((c) => c.claim).join("|")})%`, claimNumbers: `%(${claimNumbers.map((c) => c.claim).join("|")})%`,
claimdata: claimNumbers, claimdata: claimNumbers
},
},
{},
values.sendby === "email" ? "e" : "p"
);
setLoading(false);
};
useEffect(() => {
if (open) {
form.resetFields();
} }
}, [open, form]); },
{},
return ( values.sendby === "email" ? "e" : "p"
<Modal
open={open}
width="70%"
title={t("payments.labels.findermodal")}
onCancel={() => toggleModalVisible()}
onOk={() => toggleModalVisible()}
destroyOnClose
forceRender
>
<Form
form={form}
layout="vertical"
autoComplete="no"
onFinish={handleFinish}
>
<CaBcEtfTableModalComponent form={form}/>
<Button onClick={() => form.submit()} type="primary" loading={loading}>
{t("general.labels.search")}
</Button>
</Form>
</Modal>
); );
setLoading(false);
};
useEffect(() => {
if (open) {
form.resetFields();
}
}, [open, form]);
return (
<Modal
open={open}
width="70%"
title={t("payments.labels.findermodal")}
onCancel={() => toggleModalVisible()}
onOk={() => toggleModalVisible()}
destroyOnClose
forceRender
>
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>
<CaBcEtfTableModalComponent form={form} />
<Button onClick={() => form.submit()} type="primary" loading={loading}>
{t("general.labels.search")}
</Button>
</Form>
</Modal>
);
} }
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(ContractsFindModalContainer);
mapStateToProps,
mapDispatchToProps
)(ContractsFindModalContainer);

View File

@@ -1,42 +1,38 @@
import {Form, Input, Radio} from "antd"; import { Form, Input, Radio } from "antd";
import React from "react"; import React from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {selectBodyshop} from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
export default connect(mapStateToProps, null)(PartsReceiveModalComponent); export default connect(mapStateToProps, null)(PartsReceiveModalComponent);
export function PartsReceiveModalComponent({bodyshop, form}) { export function PartsReceiveModalComponent({ bodyshop, form }) {
const {t} = useTranslation(); const { t } = useTranslation();
return ( return (
<div> <div>
<Form.Item <Form.Item
name="table" name="table"
rules={[ rules={[
{ {
required: true, required: true
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, }
]} ]}
> >
<Input.TextArea rows={8}/> <Input.TextArea rows={8} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item label={t("general.labels.sendby")} name="sendby" initialValue="print">
label={t("general.labels.sendby")} <Radio.Group>
name="sendby" <Radio value="email">{t("general.labels.email")}</Radio>
initialValue="print" <Radio value="print">{t("general.labels.print")}</Radio>
> </Radio.Group>
<Radio.Group> </Form.Item>
<Radio value="email">{t("general.labels.email")}</Radio> </div>
<Radio value="print">{t("general.labels.print")}</Radio> );
</Radio.Group>
</Form.Item>
</div>
);
} }

View File

@@ -1,50 +1,48 @@
import React, {useState} from "react"; import { CalculatorFilled } from "@ant-design/icons";
import {Button, Form, InputNumber, Popover} from "antd"; import { Button, Form, InputNumber, Popover, Space } from "antd";
import {logImEXEvent} from "../../firebase/firebase.utils"; import React, { useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {CalculatorFilled} from "@ant-design/icons"; import { logImEXEvent } from "../../firebase/firebase.utils";
export default function CABCpvrtCalculator({ disabled, form }) {
const [visibility, setVisibility] = useState(false);
export default function CABCpvrtCalculator({disabled, form}) { const { t } = useTranslation();
const [visibility, setVisibility] = useState(false);
const {t} = useTranslation(); const handleFinish = async (values) => {
logImEXEvent("job_ca_bc_pvrt_calculate");
form.setFieldsValue({
ca_bc_pvrt: ((values.rate || 0) * (values.days || 0)).toFixed(2)
});
form.setFields([{ name: "ca_bc_pvrt", touched: true }]);
setVisibility(false);
};
const handleFinish = async (values) => { const popContent = (
logImEXEvent("job_ca_bc_pvrt_calculate"); <div>
form.setFieldsValue({ <Form onFinish={handleFinish} initialValues={{ rate: 1.5 }}>
ca_bc_pvrt: ((values.rate || 0) * (values.days || 0)).toFixed(2), <Form.Item name="rate" label={t("jobs.labels.ca_bc_pvrt.rate")}>
}); <InputNumber precision={2} min={0} />
form.setFields([{name: "ca_bc_pvrt", touched: true}]); </Form.Item>
setVisibility(false); <Form.Item name="days" label={t("jobs.labels.ca_bc_pvrt.days")}>
}; <InputNumber precision={0} min={0} />
</Form.Item>
const popContent = ( <div style={{ display: "flex", justifyContent: "flex-end" }}>
<div> <Space>
<Form onFinish={handleFinish} initialValues={{rate: 1.5}}> <Button type="primary" htmlType="submit">
<Form.Item name="rate" label={t("jobs.labels.ca_bc_pvrt.rate")}> {t("general.actions.calculate")}
<InputNumber precision={2} min={0}/>
</Form.Item>
<Form.Item name="days" label={t("jobs.labels.ca_bc_pvrt.days")}>
<InputNumber precision={0} min={0}/>
</Form.Item>
<Button type="primary" htmlType="submit">
{t("general.actions.calculate")}
</Button>
<Button onClick={() => setVisibility(false)}>Close</Button>
</Form>
</div>
);
return (
<Popover
destroyTooltipOnHide
content={popContent}
open={visibility}
disabled={disabled}
>
<Button disabled={disabled} onClick={() => setVisibility(true)}>
<CalculatorFilled/>
</Button> </Button>
</Popover> <Button onClick={() => setVisibility(false)}>Close</Button>
); </Space>
</div>
</Form>
</div>
);
return (
<Popover destroyTooltipOnHide content={popContent} open={visibility} disabled={disabled}>
<Button disabled={disabled} onClick={() => setVisibility(true)}>
<CalculatorFilled />
</Button>
</Popover>
);
} }

View File

@@ -1,357 +1,286 @@
import {DeleteFilled} from "@ant-design/icons"; import { DeleteFilled } from "@ant-design/icons";
import {useLazyQuery, useMutation} from "@apollo/client"; import { useLazyQuery, useMutation } from "@apollo/client";
import {Button, Card, Col, Form, Input, notification, Row, Space, Spin, Statistic,} from "antd"; import { Button, Card, Col, Form, Input, Row, Space, Spin, Statistic, notification } from "antd";
import axios from "axios"; import axios from "axios";
import dayjs from "../../utils/day"; import React, { useState } from "react";
import React, {useState} from "react"; import { useTranslation } from "react-i18next";
import {useTranslation} from "react-i18next"; import { connect } from "react-redux";
import {connect} from "react-redux"; import { createStructuredSelector } from "reselect";
import {createStructuredSelector} from "reselect"; import { INSERT_PAYMENT_RESPONSE, QUERY_RO_AND_OWNER_BY_JOB_PKS } from "../../graphql/payment_response.queries";
import {INSERT_PAYMENT_RESPONSE, QUERY_RO_AND_OWNER_BY_JOB_PKS,} from "../../graphql/payment_response.queries"; import { insertAuditTrail } from "../../redux/application/application.actions";
import {INSERT_NEW_PAYMENT} from "../../graphql/payments.queries"; import { toggleModalVisible } from "../../redux/modals/modals.actions";
import {insertAuditTrail} from "../../redux/application/application.actions"; import { selectCardPayment } from "../../redux/modals/modals.selectors";
import {toggleModalVisible} from "../../redux/modals/modals.actions"; import { selectBodyshop } from "../../redux/user/user.selectors";
import {selectCardPayment} from "../../redux/modals/modals.selectors";
import {selectBodyshop} from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component"; import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
import JobSearchSelectComponent from "../job-search-select/job-search-select.component"; import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
cardPaymentModal: selectCardPayment, cardPaymentModal: selectCardPayment,
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) => insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })),
dispatch(insertAuditTrail({jobid, operation})), toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment"))
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
}); });
const CardPaymentModalComponent = ({ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisible, insertAuditTrail }) => {
bodyshop, const { context, actions } = cardPaymentModal;
cardPaymentModal,
toggleModalVisible,
insertAuditTrail,
}) => {
const {context} = cardPaymentModal;
const [form] = Form.useForm(); const [form] = Form.useForm();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT); // const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE); const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
const {t} = useTranslation(); const { t } = useTranslation();
const [, {data, refetch, queryLoading}] = useLazyQuery( const [, { data, refetch, queryLoading }] = useLazyQuery(QUERY_RO_AND_OWNER_BY_JOB_PKS, {
QUERY_RO_AND_OWNER_BY_JOB_PKS, variables: { jobids: [context.jobid] },
{ skip: true
variables: {jobids: [context.jobid]}, });
skip: true,
//Initialize the intellipay window.
const SetIntellipayCallbackFunctions = () => {
console.log("*** Set IntelliPay callback functions.");
window.intellipay.runOnClose(() => {
//window.intellipay.initialize();
});
window.intellipay.runOnApproval(async function (response) {
//2024-04-25: Nothing is going to happen here anymore. We'll completely rely on the callback.
//Add a slight delay to allow the refetch to properly get the data.
setTimeout(() => {
if (actions && actions.refetch && typeof actions.refetch === "function")
actions.refetch();
setLoading(false);
toggleModalVisible();
}, 750);
});
window.intellipay.runOnNonApproval(async function (response) {
// Mutate unsuccessful payment
const { payments } = form.getFieldsValue();
await insertPaymentResponse({
variables: {
paymentResponse: payments.map((payment) => ({
amount: payment.amount,
bodyshopid: bodyshop.id,
jobid: payment.jobid,
declinereason: response.declinereason,
ext_paymentid: response.paymentid.toString(),
successful: false,
response
}))
} }
); });
console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data); payments.forEach((payment) =>
//Initialize the intellipay window. insertAuditTrail({
const SetIntellipayCallbackFunctions = () => { jobid: payment.jobid,
console.log("*** Set IntelliPay callback functions."); operation: AuditTrailMapping.failedpayment(),
window.intellipay.runOnClose(() => { type: "failedpayment"
//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 handleIntelliPayCharge = async () => {
// Mutate unsuccessful payment setLoading(true);
//Validate
try {
await form.validateFields();
} catch (error) {
setLoading(false);
return;
}
const {payments} = form.getFieldsValue(); try {
const response = await axios.post("/intellipay/lightbox_credentials", {
bodyshop,
refresh: !!window.intellipay,
paymentSplitMeta: form.getFieldsValue(),
});
await insertPaymentResponse({ if (window.intellipay) {
variables: { // eslint-disable-next-line no-eval
paymentResponse: payments.map((payment) => ({ eval(response.data);
amount: payment.amount, SetIntellipayCallbackFunctions();
bodyshopid: bodyshop.id, window.intellipay.autoOpen();
jobid: payment.jobid, } else {
declinereason: response.declinereason, var rg = document.createRange();
ext_paymentid: response.paymentid.toString(), let node = rg.createContextualFragment(response.data);
successful: false, document.documentElement.appendChild(node);
response, SetIntellipayCallbackFunctions();
})), window.intellipay.isAutoOpen = true;
}, window.intellipay.initialize();
}); }
} catch (error) {
notification.open({
type: "error",
message: t("job_payments.notifications.error.openingip")
});
setLoading(false);
}
};
payments.forEach((payment) => return (
insertAuditTrail({ <Card title="Card Payment">
jobid: payment.jobid, <Spin spinning={loading}>
operation: AuditTrailMapping.failedpayment(), <Form
}) 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>
const handleFinish = async (values) => { <Form.Item
try { shouldUpdate={(prevValues, curValues) =>
await insertPayment({ prevValues.payments?.map((p) => p?.jobid).join() !== curValues.payments?.map((p) => p?.jobid).join()
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"],
});
toggleModalVisible();
} catch (error) {
console.error(error);
notification.open({
type: "error",
message: t("payments.errors.inserting", {error: error.message}),
});
} finally {
setLoading(false);
}
};
const handleIntelliPayCharge = async () => {
setLoading(true);
//Validate
try {
await form.validateFields();
} catch (error) {
setLoading(false);
return;
}
try {
const response = await axios.post("/intellipay/lightbox_credentials", {
bodyshop,
refresh: !!window.intellipay,
});
if (window.intellipay) {
// eslint-disable-next-line no-eval
eval(response.data);
SetIntellipayCallbackFunctions();
window.intellipay.autoOpen();
} else {
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", //If all of the job ids have been fileld in, then query and update the IP field.
message: t("job_payments.notifications.error.openingip"), const { payments } = form.getFieldsValue();
}); if (
setLoading(false); payments?.length > 0 &&
} payments?.filter((p) => p?.jobid).length === payments?.length
}; ) {
refetch({ jobids: payments.map((p) => p.jobid) });
}
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
}
/>
<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
}
/>
</>
);
}}
</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 ( return (
<Card title="Card Payment"> <Space style={{ float: "right" }}>
<Spin spinning={loading}> <Statistic title="Amount To Charge" value={totalAmountToCharge} precision={2} />
<Form <Input
onFinish={handleFinish} className="ipayfield"
form={form} data-ipayname="amount"
layout="vertical" type="hidden"
initialValues={{ value={totalAmountToCharge?.toFixed(2)}
payments: context.jobid ? [{jobid: context.jobid}] : [], />
}} <Input
> className="ipayfield"
<Form.List name={["payments"]}> data-ipayname="comment"
{(fields, {add, remove, move}) => { type="hidden"
return ( value={btoa(JSON.stringify(payments))}
<div> hidden
{fields.map((field, index) => ( />
<Form.Item key={field.key}> <Button
<Row gutter={[16, 16]}> type="primary"
<Col span={16}> // data-ipayname="submit"
<Form.Item className="ipayfield"
key={`${index}jobid`} loading={queryLoading || loading}
label={t("jobs.fields.ro_number")} disabled={!(totalAmountToCharge > 0)}
name={[field.name, "jobid"]} onClick={handleIntelliPayCharge}
rules={[ >
{ {t("job_payments.buttons.proceedtopayment")}
required: true, </Button>
//message: t("general.validation.required"), </Space>
}, );
]} }}
> </Form.Item>
<JobSearchSelectComponent </Form>
notExported={false} </Spin>
clm_no </Card>
/> );
</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.Item
shouldUpdate={(prevValues, curValues) =>
prevValues.payments?.map((p) => p?.jobid).join() !==
curValues.payments?.map((p) => p?.jobid).join()
}
>
{() => {
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
}
/>
<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
}
/>
</>
);
}}
</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)}
/>
<Button
type="primary"
// data-ipayname="submit"
className="ipayfield"
loading={queryLoading || loading}
disabled={!(totalAmountToCharge > 0)}
onClick={handleIntelliPayCharge}
>
{t("job_payments.buttons.proceedtopayment")}
</Button>
</Space>
);
}}
</Form.Item>
{/* Lightbox payment response when it is completed */}
<Form.Item name="paymentResponse" hidden>
<Input type="hidden"/>
</Form.Item>
</Form>
</Spin>
</Card>
);
}; };
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(CardPaymentModalComponent);
mapStateToProps,
mapDispatchToProps
)(CardPaymentModalComponent);

View File

@@ -1,57 +1,50 @@
import {Button, Modal} from "antd"; import { Button, Modal } from "antd";
import React from "react"; import React from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {toggleModalVisible} from "../../redux/modals/modals.actions"; import { toggleModalVisible } from "../../redux/modals/modals.actions";
import {selectCardPayment} from "../../redux/modals/modals.selectors"; import { selectCardPayment } from "../../redux/modals/modals.selectors";
import {selectBodyshop} from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CardPaymentModalComponent from "./card-payment-modal.component."; import CardPaymentModalComponent from "./card-payment-modal.component.";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
cardPaymentModal: selectCardPayment, cardPaymentModal: selectCardPayment,
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")), toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment"))
}); });
function CardPaymentModalContainer({ function CardPaymentModalContainer({ cardPaymentModal, toggleModalVisible, bodyshop }) {
cardPaymentModal, const { open } = cardPaymentModal;
toggleModalVisible, const { t } = useTranslation();
bodyshop,
}) {
const {open} = cardPaymentModal;
const {t} = useTranslation();
const handleCancel = () => { const handleCancel = () => {
toggleModalVisible(); toggleModalVisible();
}; };
const handleOK = () => { const handleOK = () => {
toggleModalVisible(); toggleModalVisible();
}; };
return ( return (
<Modal <Modal
open={open} open={open}
onOk={handleOK} onOk={handleOK}
onCancel={handleCancel} onCancel={handleCancel}
footer={[ footer={[
<Button key="back" onClick={handleCancel}> <Button key="back" onClick={handleCancel}>
{t("job_payments.buttons.goback")} {t("job_payments.buttons.goback")}
</Button>, </Button>
]} ]}
width="80%" width="80%"
destroyOnClose destroyOnClose
> >
<CardPaymentModalComponent/> <CardPaymentModalComponent />
</Modal> </Modal>
); );
} }
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(CardPaymentModalContainer);
mapStateToProps,
mapDispatchToProps
)(CardPaymentModalContainer);

View File

@@ -1,98 +1,97 @@
import {useApolloClient} from "@apollo/client"; import { useApolloClient } from "@apollo/client";
import {getToken, onMessage} from "@firebase/messaging"; import { getToken, onMessage } from "@firebase/messaging";
import {Button, notification, Space} from "antd"; import { Button, notification, Space } from "antd";
import axios from "axios"; import axios from "axios";
import React, {useEffect} from "react"; import React, { useEffect } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {messaging, requestForToken} from "../../firebase/firebase.utils"; import { messaging, requestForToken } from "../../firebase/firebase.utils";
import FcmHandler from "../../utils/fcm-handler"; import FcmHandler from "../../utils/fcm-handler";
import ChatPopupComponent from "../chat-popup/chat-popup.component"; import ChatPopupComponent from "../chat-popup/chat-popup.component";
import "./chat-affix.styles.scss"; import "./chat-affix.styles.scss";
export function ChatAffixContainer({bodyshop, chatVisible}) { export function ChatAffixContainer({ bodyshop, chatVisible }) {
const {t} = useTranslation(); const { t } = useTranslation();
const client = useApolloClient(); const client = useApolloClient();
useEffect(() => {
if (!bodyshop || !bodyshop.messagingservicesid) return;
async function SubscribeToTopic() { useEffect(() => {
try { if (!bodyshop || !bodyshop.messagingservicesid) return;
const r = await axios.post("/notifications/subscribe", {
fcm_tokens: await getToken(messaging, {
vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY,
}),
type: "messaging",
imexshopid: bodyshop.imexshopid,
});
console.log("FCM Topic Subscription", r.data);
} catch (error) {
console.log(
"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>
),
});
}
}
SubscribeToTopic(); async function SubscribeToTopic() {
// eslint-disable-next-line react-hooks/exhaustive-deps try {
}, [bodyshop]); const r = await axios.post("/notifications/subscribe", {
fcm_tokens: await getToken(messaging, {
vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY
}),
type: "messaging",
imexshopid: bodyshop.imexshopid
});
console.log("FCM Topic Subscription", r.data);
} catch (error) {
console.log("Error attempting to subscribe to messaging topic: ", error);
notification.open({
key: "fcm",
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>
)
});
}
}
useEffect(() => { SubscribeToTopic();
function handleMessage(payload) { // eslint-disable-next-line react-hooks/exhaustive-deps
FcmHandler({ }, [bodyshop]);
client,
payload: (payload && payload.data && payload.data.data) || payload.data,
});
}
let stopMessageListener, channel; useEffect(() => {
try { function handleMessage(payload) {
stopMessageListener = onMessage(messaging, handleMessage); FcmHandler({
channel = new BroadcastChannel("imex-sw-messages"); client,
channel.addEventListener("message", handleMessage); payload: (payload && payload.data && payload.data.data) || payload.data
} catch (error) { });
console.log("Unable to set event listeners."); }
}
return () => {
stopMessageListener && stopMessageListener();
channel && channel.removeEventListener("message", handleMessage);
};
}, [client]);
if (!bodyshop || !bodyshop.messagingservicesid) return <></>; let stopMessageListener, channel;
try {
stopMessageListener = 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();
channel && channel.removeEventListener("message", handleMessage);
};
}, [client]);
return ( if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
{bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent/> : null} return (
</div> <div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
); {bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent /> : null}
</div>
);
} }
export default ChatAffixContainer; export default ChatAffixContainer;

View File

@@ -1,29 +1,27 @@
import {useMutation} from "@apollo/client"; import { useMutation } from "@apollo/client";
import {Button} from "antd"; import { Button } from "antd";
import React, {useState} from "react"; import React, { useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {TOGGLE_CONVERSATION_ARCHIVE} from "../../graphql/conversations.queries"; import { TOGGLE_CONVERSATION_ARCHIVE } from "../../graphql/conversations.queries";
export default function ChatArchiveButton({conversation}) { export default function ChatArchiveButton({ conversation }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const {t} = useTranslation(); const { t } = useTranslation();
const [updateConversation] = useMutation(TOGGLE_CONVERSATION_ARCHIVE); const [updateConversation] = useMutation(TOGGLE_CONVERSATION_ARCHIVE);
const handleToggleArchive = async () => { const handleToggleArchive = async () => {
setLoading(true); setLoading(true);
await updateConversation({ await updateConversation({
variables: {id: conversation.id, archived: !conversation.archived}, variables: { id: conversation.id, archived: !conversation.archived },
refetchQueries: ["CONVERSATION_LIST_QUERY"], refetchQueries: ["CONVERSATION_LIST_QUERY"]
}); });
setLoading(false); setLoading(false);
}; };
return ( return (
<Button onClick={handleToggleArchive} loading={loading} type="primary"> <Button onClick={handleToggleArchive} loading={loading} type="primary">
{conversation.archived {conversation.archived ? t("messaging.labels.unarchive") : t("messaging.labels.archive")}
? t("messaging.labels.unarchive") </Button>
: t("messaging.labels.archive")} );
</Button>
);
} }

View File

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

View File

@@ -1,56 +1,56 @@
import {useMutation} from "@apollo/client"; import { useMutation } from "@apollo/client";
import {Tag} from "antd"; import { Tag } from "antd";
import React from "react"; import React from "react";
import {Link} from "react-router-dom"; import { Link } from "react-router-dom";
import {logImEXEvent} from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import {REMOVE_CONVERSATION_TAG} from "../../graphql/job-conversations.queries"; import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
export default function ChatConversationTitleTags({jobConversations}) { export default function ChatConversationTitleTags({ jobConversations }) {
const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG); const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG);
const handleRemoveTag = (jobId) => { const handleRemoveTag = (jobId) => {
const convId = jobConversations[0].conversationid; const convId = jobConversations[0].conversationid;
if (!!convId) { if (!!convId) {
removeJobConversation({ removeJobConversation({
variables: { variables: {
conversationId: convId, conversationId: convId,
jobId: jobId, jobId: jobId
}, },
update(cache) { update(cache) {
cache.modify({ cache.modify({
id: cache.identify({id: convId, __typename: "conversations"}), id: cache.identify({ id: convId, __typename: "conversations" }),
fields: { fields: {
job_conversations(ex) { job_conversations(ex) {
return ex.filter((e) => e.jobid !== jobId); return ex.filter((e) => e.jobid !== jobId);
}, }
}, }
}); });
},
});
logImEXEvent("messaging_remove_job_tag", {
conversationId: convId,
jobId: jobId,
});
} }
}; });
logImEXEvent("messaging_remove_job_tag", {
conversationId: convId,
jobId: jobId
});
}
};
return ( return (
<div> <div>
{jobConversations.map((item) => ( {jobConversations.map((item) => (
<Tag <Tag
key={item.job.id} key={item.job.id}
closable closable
color="blue" color="blue"
style={{cursor: "pointer"}} style={{ cursor: "pointer" }}
onClose={() => handleRemoveTag(item.job.id)} onClose={() => handleRemoveTag(item.job.id)}
> >
<Link to={`/manage/jobs/${item.job.id}`}> <Link to={`/manage/jobs/${item.job.id}`}>
{`${item.job.ro_number || "?"} | `} {`${item.job.ro_number || "?"} | `}
<OwnerNameDisplay ownerObject={item.job}/> <OwnerNameDisplay ownerObject={item.job} />
</Link> </Link>
</Tag> </Tag>
))} ))}
</div> </div>
); );
} }

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