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 116850 additions and 118450 deletions

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ For filters and sorters, valid types include (`type` key in the schema):
- bool or boolean
- date
# Special Notes
## 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
## High level Schema Overview
@@ -46,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.
## Reflection
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",
"translation": "jobs.fields.status",
@@ -58,7 +59,7 @@ Filters can make use of reflection to pre-fill select boxes, the following is an
"type": "internal",
"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`
@@ -73,9 +74,13 @@ The following cases are available
- `special.employees` - This will reflect the employees `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.referral_sources` - This will reflect the referral sources `bodyshop.md_referral_sources
- `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
`"name": "jobs.joblines.mod_lb_hrs",`
@@ -155,6 +160,7 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!
- 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.
- 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
@@ -165,6 +171,7 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!
using the 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.
- 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`.

View File

@@ -1,20 +1,20 @@
module.exports = {
apps: [
{
name: "IO Test API",
cwd: "./io",
script: "./server.js",
env: {
NODE_ENV: "test",
},
},
apps: [
{
name: "IO Test API",
cwd: "./io",
script: "./server.js",
env: {
NODE_ENV: "test"
}
},
{
name: "Bitbucket Webhook",
script: "./webhook/index.js",
env: {
NODE_ENV: "production",
},
},
],
{
name: "Bitbucket Webhook",
script: "./webhook/index.js",
env: {
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_WS=wss://db.dev.bodyshop.app/v1/graphql
VITE_APP_GA_CODE=231099835
VITE_APP_FIREBASE_CONFIG={ "apiKey": "AIzaSyAuLQR9SV5LsVxjU8wh9hvFLdhcAHU6cxE", "authDomain": "rome-prod-1.firebaseapp.com", "projectId": "rome-prod-1", "storageBucket": "rome-prod-1.appspot.com", "messagingSenderId": "147786367145", "appId": "1:147786367145:web:9d4cba68071c3f29a8a9b8", "measurementId": "G-G8Z9DRHTZS"}
# VITE_APP_FIREBASE_CONFIG={ "apiKey": "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=https://res.cloudinary.com/io-test
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_WS=wss://db.dev.bodyshop.app/v1/graphql
VITE_APP_GA_CODE=231099835
VITE_APP_FIREBASE_CONFIG={ "apiKey": "AIzaSyAuLQR9SV5LsVxjU8wh9hvFLdhcAHU6cxE", "authDomain": "rome-prod-1.firebaseapp.com", "projectId": "rome-prod-1", "storageBucket": "rome-prod-1.appspot.com", "messagingSenderId": "147786367145", "appId": "1:147786367145:web:9d4cba68071c3f29a8a9b8", "measurementId": "G-G8Z9DRHTZS"}
# VITE_APP_FIREBASE_CONFIG={ "apiKey": "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=https://res.cloudinary.com/io-test
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
const TerserPlugin = require("terser-webpack-plugin");
const CracoLessPlugin = require("craco-less");
const {convertLegacyToken} = require('@ant-design/compatible/lib');
const {theme} = require('antd/lib');
const { convertLegacyToken } = require("@ant-design/compatible/lib");
const { theme } = require("antd/lib");
const {defaultAlgorithm, defaultSeed} = theme;
const { defaultAlgorithm, defaultSeed } = theme;
const mapToken = defaultAlgorithm(defaultSeed);
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.
module.exports = {
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: {...v4Token},
javascriptEnabled: true,
},
},
},
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: { ...v4Token },
javascriptEnabled: true
}
}
}
}
],
webpack: {
configure: (webpackConfig) => {
return {
...webpackConfig,
// Required for Dev Server
devServer: {
...webpackConfig.devServer,
allowedHosts: "all"
},
],
webpack: {
configure: (webpackConfig) => {
return {
...webpackConfig,
// Required for Dev Server
devServer: {
...webpackConfig.devServer,
allowedHosts: 'all',
},
optimization: {
...webpackConfig.optimization,
// Workaround for CircleCI bug caused by the number of CPUs shown
// https://github.com/facebook/create-react-app/issues/8320
minimizer: webpackConfig.optimization.minimizer.map((item) => {
if (item instanceof TerserPlugin) {
item.options.parallel = 2;
}
optimization: {
...webpackConfig.optimization,
// Workaround for CircleCI bug caused by the number of CPUs shown
// https://github.com/facebook/create-react-app/issues/8320
minimizer: webpackConfig.optimization.minimizer.map((item) => {
if (item instanceof TerserPlugin) {
item.options.parallel = 2;
}
return item;
}),
},
};
},
},
devtool: "source-map",
return item;
})
}
};
}
},
devtool: "source-map"
};

View File

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

View File

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

View File

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

View File

@@ -1,177 +1,173 @@
/// <reference types="cypress" />
context('Assertions', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/assertions')
})
context("Assertions", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/commands/assertions");
});
describe('Implicit Assertions', () => {
it('.should() - make an assertion about the current subject', () => {
// https://on.cypress.io/should
cy.get('.assertion-table')
.find('tbody tr:last')
.should('have.class', 'success')
.find('td')
.first()
// checking the text of the <td> element in various ways
.should('have.text', 'Column content')
.should('contain', 'Column content')
.should('have.html', 'Column content')
// chai-jquery uses "is()" to check if element matches selector
.should('match', 'td')
// to match text content against a regular expression
// first need to invoke jQuery method text()
// and then match using regular expression
.invoke('text')
.should('match', /column content/i)
describe("Implicit Assertions", () => {
it(".should() - make an assertion about the current subject", () => {
// https://on.cypress.io/should
cy.get(".assertion-table")
.find("tbody tr:last")
.should("have.class", "success")
.find("td")
.first()
// checking the text of the <td> element in various ways
.should("have.text", "Column content")
.should("contain", "Column content")
.should("have.html", "Column content")
// chai-jquery uses "is()" to check if element matches selector
.should("match", "td")
// to match text content against a regular expression
// first need to invoke jQuery method text()
// and then match using regular expression
.invoke("text")
.should("match", /column content/i);
// a better way to check element's text content against a regular expression
// is to use "cy.contains"
// https://on.cypress.io/contains
cy.get('.assertion-table')
.find('tbody tr:last')
// finds first <td> element with text content matching regular expression
.contains('td', /column content/i)
.should('be.visible')
// a better way to check element's text content against a regular expression
// is to use "cy.contains"
// https://on.cypress.io/contains
cy.get(".assertion-table")
.find("tbody tr:last")
// finds first <td> element with text content matching regular expression
.contains("td", /column content/i)
.should("be.visible");
// for more information about asserting element's text
// see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-elements-text-contents
// for more information about asserting element's text
// 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', () => {
// https://on.cypress.io/and
cy.get('.assertions-link')
.should('have.class', 'active')
.and('have.attr', 'href')
.and('include', 'cypress.io')
})
})
it("can throw any error", () => {
cy.get(".docs-header")
.find("div")
.should(($div) => {
if ($div.length !== 1) {
// you can throw your own errors
throw new Error("Did not find 1 element");
}
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'}
const className = $div[0].className;
expect(o).to.equal(o)
expect(o).to.deep.equal({foo: 'bar'})
// matching text using regular expression
expect('FooBar').to.match(/bar$/i)
})
if (!className.match(/heading-/)) {
throw new Error(`Could not find class "heading-" in ${className}`);
}
});
});
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())
it("matches unknown text between two elements", () => {
/**
* Text from the first element.
* @type {string}
*/
let text;
// jquery map returns jquery object
// and .get() convert this to simple array
const paragraphs = texts.get()
/**
* 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();
// array should have length of 3
expect(paragraphs, 'has 3 paragraphs').to.have.length(3)
cy.get(".two-elements")
.find(".first")
.then(($first) => {
// save text from the first element
text = normalizeText($first.text());
});
// 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',
])
})
})
cy.get(".two-elements")
.find(".second")
.should(($div) => {
// we can massage text before comparing
const secondText = normalizeText($div.text());
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)
expect(secondText, "second text").to.equal(text);
});
});
const className = $div[0].className
it("assert - assert shape of an object", () => {
const person = {
name: "Joe",
age: 20
};
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')
})
})
assert.isObject(person, "value is object");
});
it('can throw any error', () => {
cy.get('.docs-header')
.find('div')
.should(($div) => {
if ($div.length !== 1) {
// you can throw your own errors
throw new Error('Did not find 1 element')
}
it("retries the should callback until assertions pass", () => {
cy.get("#random-number").should(($div) => {
const n = parseFloat($div.text());
const className = $div[0].className
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)
})
})
})
})
expect(n).to.be.gte(1).and.be.lte(10);
});
});
});
});

View File

@@ -1,97 +1,96 @@
/// <reference types="cypress" />
context('Connectors', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/connectors')
})
context("Connectors", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/commands/connectors");
});
it('.each() - iterate over an array of elements', () => {
// https://on.cypress.io/each
cy.get('.connectors-each-ul>li')
.each(($el, index, $list) => {
console.log($el, index, $list)
})
})
it(".each() - iterate over an array of elements", () => {
// https://on.cypress.io/each
cy.get(".connectors-each-ul>li").each(($el, index, $list) => {
console.log($el, index, $list);
});
});
it('.its() - get properties on the current subject', () => {
// https://on.cypress.io/its
cy.get('.connectors-its-ul>li')
// calls the 'length' property yielding that value
.its('length')
.should('be.gt', 2)
})
it(".its() - get properties on the current subject", () => {
// https://on.cypress.io/its
cy.get(".connectors-its-ul>li")
// calls the 'length' property yielding that value
.its("length")
.should("be.gt", 2);
});
it('.invoke() - invoke a function on the current subject', () => {
// our div is hidden in our script.js
// $('.connectors-div').hide()
it(".invoke() - invoke a function on the current subject", () => {
// our div is hidden in our script.js
// $('.connectors-div').hide()
// https://on.cypress.io/invoke
cy.get('.connectors-div').should('be.hidden')
// call the jquery method 'show' on the 'div.container'
.invoke('show')
.should('be.visible')
})
// https://on.cypress.io/invoke
cy.get(".connectors-div")
.should("be.hidden")
// call the jquery method 'show' on the 'div.container'
.invoke("show")
.should("be.visible");
});
it('.spread() - spread an array as individual args to callback function', () => {
// https://on.cypress.io/spread
const arr = ['foo', 'bar', 'baz']
it(".spread() - spread an array as individual args to callback function", () => {
// https://on.cypress.io/spread
const arr = ["foo", "bar", "baz"];
cy.wrap(arr).spread((foo, bar, baz) => {
expect(foo).to.eq('foo')
expect(bar).to.eq('bar')
expect(baz).to.eq('baz')
cy.wrap(arr).spread((foo, bar, baz) => {
expect(foo).to.eq("foo");
expect(bar).to.eq("bar");
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('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 original subject without return", () => {
cy.wrap(1)
.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 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)
})
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);
})
it('yields the original subject without return', () => {
cy.wrap(1)
.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)
})
})
})
})
.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" />
context('Cookies', () => {
beforeEach(() => {
Cypress.Cookies.debug(true)
context("Cookies", () => {
beforeEach(() => {
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
// any 3rd party cookies picked up such as cloudflare
cy.clearCookies()
})
// clear cookies again after visiting to remove
// any 3rd party cookies picked up such as cloudflare
cy.clearCookies();
});
it('cy.getCookie() - get a browser cookie', () => {
// https://on.cypress.io/getcookie
cy.get('#getCookie .set-a-cookie').click()
it("cy.getCookie() - get a browser cookie", () => {
// https://on.cypress.io/getcookie
cy.get("#getCookie .set-a-cookie").click();
// cy.getCookie() yields a cookie object
cy.getCookie('token').should('have.property', 'value', '123ABC')
})
// cy.getCookie() yields a cookie object
cy.getCookie("token").should("have.property", "value", "123ABC");
});
it('cy.getCookies() - get browser cookies', () => {
// https://on.cypress.io/getcookies
cy.getCookies().should('be.empty')
it("cy.getCookies() - get browser cookies", () => {
// https://on.cypress.io/getcookies
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().should('have.length', 1).should((cookies) => {
// each cookie has these properties
expect(cookies[0]).to.have.property('name', 'token')
expect(cookies[0]).to.have.property('value', '123ABC')
expect(cookies[0]).to.have.property('httpOnly', false)
expect(cookies[0]).to.have.property('secure', false)
expect(cookies[0]).to.have.property('domain')
expect(cookies[0]).to.have.property('path')
})
})
// cy.getCookies() yields an array of cookies
cy.getCookies()
.should("have.length", 1)
.should((cookies) => {
// each cookie has these properties
expect(cookies[0]).to.have.property("name", "token");
expect(cookies[0]).to.have.property("value", "123ABC");
expect(cookies[0]).to.have.property("httpOnly", false);
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', () => {
// https://on.cypress.io/setcookie
cy.getCookies().should('be.empty')
it("cy.setCookie() - set a browser cookie", () => {
// https://on.cypress.io/setcookie
cy.getCookies().should("be.empty");
cy.setCookie('foo', 'bar')
cy.setCookie("foo", "bar");
// cy.getCookie() yields a cookie object
cy.getCookie('foo').should('have.property', 'value', 'bar')
})
// cy.getCookie() yields a cookie object
cy.getCookie("foo").should("have.property", "value", "bar");
});
it('cy.clearCookie() - clear a browser cookie', () => {
// https://on.cypress.io/clearcookie
cy.getCookie('token').should('be.null')
it("cy.clearCookie() - clear a browser cookie", () => {
// https://on.cypress.io/clearcookie
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.clearCookie('token').should('be.null')
// cy.clearCookies() yields 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', () => {
// https://on.cypress.io/clearcookies
cy.getCookies().should('be.empty')
it("cy.clearCookies() - clear browser cookies", () => {
// https://on.cypress.io/clearcookies
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()
// cy.clearCookies() yields null
cy.clearCookies();
cy.getCookies().should('be.empty')
})
})
cy.getCookies().should("be.empty");
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,163 +1,165 @@
/// <reference types="cypress" />
context('Network Requests', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/network-requests')
context("Network Requests", () => {
beforeEach(() => {
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', () => {
// 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')
})
})
// we don't know the exact post id - only that it will be > 100
// since JSONPlaceholder has built-in 100 posts
expect(response.body).property("id").to.be.a("number").and.to.be.gt(100);
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')
})
})
// we don't know the user id here - since it was in above closure
// so in this test just confirm that the property is there
expect(response.body).property("userId").to.be.a("number");
});
});
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,
},
it("cy.request() - save response in the shared test context", () => {
// https://on.cypress.io/variables-and-aliases
cy.request("https://jsonplaceholder.cypress.io/users?_limit=1")
.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')
.should('be.an', 'array')
.and('have.length', 1)
.its('0') // yields first element of the array
.should('contain', {
postId: 1,
id: 3,
})
})
.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.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.intercept() - route responses to matching requests", () => {
// https://on.cypress.io/intercept
// we don't know the exact post id - only that it will be > 100
// since JSONPlaceholder has built-in 100 posts
expect(response.body).property('id').to.be.a('number')
.and.to.be.gt(100)
let message = "whoa, this comment does not exist";
// we don't know the user id here - since it was in above closure
// so in this test just confirm that the property is there
expect(response.body).property('userId').to.be.a('number')
})
})
// Listen to GET to comments/1
cy.intercept("GET", "**/comments/*").as("getComment");
it('cy.request() - save response in the shared test context', () => {
// https://on.cypress.io/variables-and-aliases
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1')
.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)
})
})
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get(".network-btn").click();
it('cy.intercept() - route responses to matching requests', () => {
// https://on.cypress.io/intercept
// https://on.cypress.io/wait
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
cy.intercept('GET', '**/comments/*').as('getComment')
// we have code that posts a comment when
// 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
// the button is clicked in scripts.js
cy.get('.network-btn').click()
// 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");
// https://on.cypress.io/wait
cy.wait('@getComment').its('response.statusCode').should('be.oneOf', [200, 304])
// we have code that puts a comment when
// the button is clicked in scripts.js
cy.get(".network-put").click();
// Listen to POST to comments
cy.intercept('POST', '**/comments').as('postComment')
cy.wait("@putComment");
// we have code that posts a comment when
// 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()')
})
// 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)
})
})
// 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" />
context('Querying', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/querying')
})
context("Querying", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/commands/querying");
});
// The most commonly used query is 'cy.get()', you can
// think of this like the '$' in jQuery
// The most commonly used query is 'cy.get()', you can
// think of this like the '$' in jQuery
it('cy.get() - query DOM elements', () => {
// https://on.cypress.io/get
it("cy.get() - query DOM elements", () => {
// 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')
// ↲
// Use CSS selectors just like jQuery
cy.get("#querying .well>button:first").should("contain", "Button");
// ↲
// 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
// by invoking `.attr()` method
cy.get('[data-test-id="test-example"]')
.invoke('attr', 'data-test-id')
.should('equal', 'test-example')
// 'cy.get()' yields jQuery object, you can get its attribute
// by invoking `.attr()` method
cy.get('[data-test-id="test-example"]').invoke("attr", "data-test-id").should("equal", "test-example");
// or you can get element's CSS property
cy.get('[data-test-id="test-example"]')
.invoke('css', 'position')
.should('equal', 'static')
// or you can get element's CSS property
cy.get('[data-test-id="test-example"]').invoke("css", "position").should("equal", "static");
// or use assertions directly during 'cy.get()'
// https://on.cypress.io/assertions
cy.get('[data-test-id="test-example"]')
.should('have.attr', 'data-test-id', 'test-example')
.and('have.css', 'position', 'static')
})
// or use assertions directly during 'cy.get()'
// https://on.cypress.io/assertions
cy.get('[data-test-id="test-example"]')
.should("have.attr", "data-test-id", "test-example")
.and("have.css", "position", "static");
});
it('cy.contains() - query DOM elements with matching content', () => {
// https://on.cypress.io/contains
cy.get('.query-list')
.contains('bananas')
.should('have.class', 'third')
it("cy.contains() - query DOM elements with matching content", () => {
// https://on.cypress.io/contains
cy.get(".query-list").contains("bananas").should("have.class", "third");
// we can pass a regexp to `.contains()`
cy.get('.query-list')
.contains(/^b\w+/)
.should('have.class', 'third')
// we can pass a regexp to `.contains()`
cy.get(".query-list").contains(/^b\w+/).should("have.class", "third");
cy.get('.query-list')
.contains('apples')
.should('have.class', 'first')
cy.get(".query-list").contains("apples").should("have.class", "first");
// passing a selector to contains will
// yield the selector containing the text
cy.get('#querying')
.contains('ul', 'oranges')
.should('have.class', 'query-list')
// passing a selector to contains will
// yield the selector containing the text
cy.get("#querying").contains("ul", "oranges").should("have.class", "query-list");
cy.get('.query-button')
.contains('Save Form')
.should('have.class', 'btn')
})
cy.get(".query-button").contains("Save Form").should("have.class", "btn");
});
it('.within() - query DOM elements within a specific element', () => {
// https://on.cypress.io/within
cy.get('.query-form').within(() => {
cy.get('input:first').should('have.attr', 'placeholder', 'Email')
cy.get('input:last').should('have.attr', 'placeholder', 'Password')
})
})
it(".within() - query DOM elements within a specific element", () => {
// https://on.cypress.io/within
cy.get(".query-form").within(() => {
cy.get("input:first").should("have.attr", "placeholder", "Email");
cy.get("input:last").should("have.attr", "placeholder", "Password");
});
});
it('cy.root() - query the root DOM element', () => {
// https://on.cypress.io/root
it("cy.root() - query the root DOM element", () => {
// https://on.cypress.io/root
// By default, root is the document
cy.root().should('match', 'html')
// By default, root is the document
cy.root().should("match", "html");
cy.get('.query-ul').within(() => {
// In this within, the root is now the ul DOM element
cy.root().should('have.class', 'query-ul')
})
})
cy.get(".query-ul").within(() => {
// In this within, the root is now the ul DOM element
cy.root().should("have.class", "query-ul");
});
});
it('best practices - selecting elements', () => {
// https://on.cypress.io/best-practices#Selecting-Elements
cy.get('[data-cy=best-practices-selecting-elements]').within(() => {
// Worst - too generic, no context
cy.get('button').click()
it("best practices - selecting elements", () => {
// https://on.cypress.io/best-practices#Selecting-Elements
cy.get("[data-cy=best-practices-selecting-elements]").within(() => {
// Worst - too generic, no context
cy.get("button").click();
// Bad. Coupled to styling. Highly subject to change.
cy.get('.btn.btn-large').click()
// Bad. Coupled to styling. Highly subject to change.
cy.get(".btn.btn-large").click();
// Average. Coupled to the `name` attribute which has HTML semantics.
cy.get('[name=submission]').click()
// Average. Coupled to the `name` attribute which has HTML semantics.
cy.get("[name=submission]").click();
// Better. But still coupled to styling or JS event listeners.
cy.get('#main').click()
// Better. But still coupled to styling or JS event listeners.
cy.get("#main").click();
// Slightly better. Uses an ID but also ensures the element
// has an ARIA role attribute
cy.get('#main[role=button]').click()
// Slightly better. Uses an ID but also ensures the element
// has an ARIA role attribute
cy.get("#main[role=button]").click();
// Much better. But still coupled to text content that may change.
cy.contains('Submit').click()
// Much better. But still coupled to text content that may change.
cy.contains("Submit").click();
// Best. Insulated from all changes.
cy.get('[data-cy=submit]').click()
})
})
})
// Best. Insulated from all changes.
cy.get("[data-cy=submit]").click();
});
});
});

View File

@@ -2,205 +2,202 @@
// remove no check once Cypress.sinon is typed
// https://github.com/cypress-io/cypress/issues/6720
context('Spies, Stubs, and Clock', () => {
it('cy.spy() - wrap a method in a spy', () => {
// https://on.cypress.io/spy
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
context("Spies, Stubs, and Clock", () => {
it("cy.spy() - wrap a method in a spy", () => {
// https://on.cypress.io/spy
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
const obj = {
foo() {
},
}
const obj = {
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', () => {
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
it("cy.spy() retries until assertions pass", () => {
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
const obj = {
/**
* Prints the argument passed
* @param x {any}
*/
foo(x) {
console.log('obj.foo called with', x)
},
}
const obj = {
/**
* Prints the argument passed
* @param x {any}
*/
foo(x) {
console.log("obj.foo called with", x);
}
};
cy.spy(obj, 'foo').as('foo')
cy.spy(obj, "foo").as("foo");
setTimeout(() => {
obj.foo('first')
}, 500)
setTimeout(() => {
obj.foo("first");
}, 500);
setTimeout(() => {
obj.foo('second')
}, 2500)
setTimeout(() => {
obj.foo("second");
}, 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', () => {
// https://on.cypress.io/stub
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
it("cy.stub() - create a stub and/or replace a function with stub", () => {
// https://on.cypress.io/stub
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
const obj = {
/**
* prints both arguments to the console
* @param a {string}
* @param b {string}
*/
foo(a, b) {
console.log('a', a, 'b', b)
},
}
const obj = {
/**
* prints both arguments to the console
* @param a {string}
* @param b {string}
*/
foo(a, 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', () => {
// https://on.cypress.io/clock
it("cy.clock() - control time in the browser", () => {
// https://on.cypress.io/clock
// create the date in UTC so its always the same
// no matter what local timezone the browser is running in
const now = new Date(Date.UTC(2017, 2, 14)).getTime()
// create the date in UTC so its always the same
// no matter what local timezone the browser is running in
const now = new Date(Date.UTC(2017, 2, 14)).getTime();
cy.clock(now)
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
cy.get('#clock-div').click()
.should('have.text', '1489449600')
})
cy.clock(now);
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
cy.get("#clock-div").click().should("have.text", "1489449600");
});
it('cy.tick() - move time in the browser', () => {
// https://on.cypress.io/tick
it("cy.tick() - move time in the browser", () => {
// https://on.cypress.io/tick
// create the date in UTC so its always the same
// no matter what local timezone the browser is running in
const now = new Date(Date.UTC(2017, 2, 14)).getTime()
// create the date in UTC so its always the same
// no matter what local timezone the browser is running in
const now = new Date(Date.UTC(2017, 2, 14)).getTime();
cy.clock(now)
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
cy.get('#tick-div').click()
.should('have.text', '1489449600')
cy.clock(now);
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
cy.get("#tick-div").click().should("have.text", "1489449600");
cy.tick(10000) // 10 seconds passed
cy.get('#tick-div').click()
.should('have.text', '1489449610')
})
cy.tick(10000); // 10 seconds passed
cy.get("#tick-div").click().should("have.text", "1489449610");
});
it('cy.stub() matches depending on arguments', () => {
// see all possible matchers at
// https://sinonjs.org/releases/latest/matchers/
const greeter = {
/**
* Greets a person
* @param {string} name
*/
greet(name) {
return `Hello, ${name}!`
},
}
it("cy.stub() matches depending on arguments", () => {
// see all possible matchers at
// https://sinonjs.org/releases/latest/matchers/
const greeter = {
/**
* Greets a person
* @param {string} name
*/
greet(name) {
return `Hello, ${name}!`;
}
};
cy.stub(greeter, 'greet')
.callThrough() // if you want non-matched calls to call the real method
.withArgs(Cypress.sinon.match.string).returns('Hi')
.withArgs(Cypress.sinon.match.number).throws(new Error('Invalid name'))
cy.stub(greeter, "greet")
.callThrough() // if you want non-matched calls to call the real method
.withArgs(Cypress.sinon.match.string)
.returns("Hi")
.withArgs(Cypress.sinon.match.number)
.throws(new Error("Invalid name"));
expect(greeter.greet('World')).to.equal('Hi')
// @ts-ignore
expect(() => greeter.greet(42)).to.throw('Invalid name')
expect(greeter.greet).to.have.been.calledTwice
expect(greeter.greet("World")).to.equal("Hi");
// @ts-ignore
expect(() => greeter.greet(42)).to.throw("Invalid name");
expect(greeter.greet).to.have.been.calledTwice;
// non-matched calls goes the actual method
// @ts-ignore
expect(greeter.greet()).to.equal('Hello, undefined!')
})
// non-matched calls goes the actual method
// @ts-ignore
expect(greeter.greet()).to.equal("Hello, undefined!");
});
it('matches call arguments using Sinon matchers', () => {
// see all possible matchers at
// https://sinonjs.org/releases/latest/matchers/
const calculator = {
/**
* returns the sum of two arguments
* @param a {number}
* @param b {number}
*/
add(a, b) {
return a + b
},
}
it("matches call arguments using Sinon matchers", () => {
// see all possible matchers at
// https://sinonjs.org/releases/latest/matchers/
const calculator = {
/**
* returns the sum of two arguments
* @param a {number}
* @param b {number}
*/
add(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
expect(spy).to.be.calledWith(2, 3)
// if we want to assert the exact values used during the call
expect(spy).to.be.calledWith(2, 3);
// let's confirm "add" method was called with two numbers
expect(spy).to.be.calledWith(Cypress.sinon.match.number, Cypress.sinon.match.number)
// let's confirm "add" method was called with two numbers
expect(spy).to.be.calledWith(Cypress.sinon.match.number, Cypress.sinon.match.number);
// alternatively, provide the value to match
expect(spy).to.be.calledWith(Cypress.sinon.match(2), Cypress.sinon.match(3))
// alternatively, provide the value to match
expect(spy).to.be.calledWith(Cypress.sinon.match(2), Cypress.sinon.match(3));
// match any value
expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3)
// match any value
expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3);
// match any value from a list
expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3)
// match any value from a list
expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3);
/**
* Returns true if the given number is event
* @param {number} x
*/
const isEven = (x) => x % 2 === 0
/**
* Returns true if the given number is event
* @param {number} x
*/
const isEven = (x) => x % 2 === 0;
// expect the value to pass a custom predicate function
// the second argument to "sinon.match(predicate, message)" is
// shown if the predicate does not pass and assertion fails
expect(spy).to.be.calledWith(Cypress.sinon.match(isEven, 'isEven'), 3)
// expect the value to pass a custom predicate function
// the second argument to "sinon.match(predicate, message)" is
// shown if the predicate does not pass and assertion fails
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
* @param {number} limit
* @returns {(x: number) => boolean}
*/
const isGreaterThan = (limit) => (x) => x > limit
/**
* Returns a function that checks if a given number is larger than the limit
* @param {number} limit
* @returns {(x: number) => boolean}
*/
const isGreaterThan = (limit) => (x) => x > limit;
/**
* Returns a function that checks if a given number is less than the limit
* @param {number} limit
* @returns {(x: number) => boolean}
*/
const isLessThan = (limit) => (x) => x < limit
/**
* Returns a function that checks if a given number is less than the limit
* @param {number} limit
* @returns {(x: number) => boolean}
*/
const isLessThan = (limit) => (x) => x < limit;
// you can combine several matchers using "and", "or"
expect(spy).to.be.calledWith(
Cypress.sinon.match.number,
Cypress.sinon.match(isGreaterThan(2), '> 2').and(Cypress.sinon.match(isLessThan(4), '< 4')),
)
// you can combine several matchers using "and", "or"
expect(spy).to.be.calledWith(
Cypress.sinon.match.number,
Cypress.sinon.match(isGreaterThan(2), "> 2").and(Cypress.sinon.match(isLessThan(4), "< 4"))
);
expect(spy).to.be.calledWith(
Cypress.sinon.match.number,
Cypress.sinon.match(isGreaterThan(200), '> 200').or(Cypress.sinon.match(3)),
)
expect(spy).to.be.calledWith(
Cypress.sinon.match.number,
Cypress.sinon.match(isGreaterThan(200), "> 200").or(Cypress.sinon.match(3))
);
// matchers can be used from BDD assertions
cy.get('@add').should('have.been.calledWith',
Cypress.sinon.match.number, Cypress.sinon.match(3))
// matchers can be used from BDD assertions
cy.get("@add").should("have.been.calledWith", Cypress.sinon.match.number, Cypress.sinon.match(3));
// you can alias matchers for shorter test code
const {match: M} = Cypress.sinon
// you can alias matchers for shorter test code
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" />
context('Traversal', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/traversal')
})
context("Traversal", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/commands/traversal");
});
it('.children() - get child DOM elements', () => {
// https://on.cypress.io/children
cy.get('.traversal-breadcrumb')
.children('.active')
.should('contain', 'Data')
})
it(".children() - get child DOM elements", () => {
// https://on.cypress.io/children
cy.get(".traversal-breadcrumb").children(".active").should("contain", "Data");
});
it('.closest() - get closest ancestor DOM element', () => {
// https://on.cypress.io/closest
cy.get('.traversal-badge')
.closest('ul')
.should('have.class', 'list-group')
})
it(".closest() - get closest ancestor DOM element", () => {
// https://on.cypress.io/closest
cy.get(".traversal-badge").closest("ul").should("have.class", "list-group");
});
it('.eq() - get a DOM element at a specific index', () => {
// https://on.cypress.io/eq
cy.get('.traversal-list>li')
.eq(1).should('contain', 'siamese')
})
it(".eq() - get a DOM element at a specific index", () => {
// https://on.cypress.io/eq
cy.get(".traversal-list>li").eq(1).should("contain", "siamese");
});
it('.filter() - get DOM elements that match the selector', () => {
// https://on.cypress.io/filter
cy.get('.traversal-nav>li')
.filter('.active').should('contain', 'About')
})
it(".filter() - get DOM elements that match the selector", () => {
// https://on.cypress.io/filter
cy.get(".traversal-nav>li").filter(".active").should("contain", "About");
});
it('.find() - get descendant DOM elements of the selector', () => {
// https://on.cypress.io/find
cy.get('.traversal-pagination')
.find('li').find('a')
.should('have.length', 7)
})
it(".find() - get descendant DOM elements of the selector", () => {
// https://on.cypress.io/find
cy.get(".traversal-pagination").find("li").find("a").should("have.length", 7);
});
it('.first() - get first DOM element', () => {
// https://on.cypress.io/first
cy.get('.traversal-table td')
.first().should('contain', '1')
})
it(".first() - get first DOM element", () => {
// https://on.cypress.io/first
cy.get(".traversal-table td").first().should("contain", "1");
});
it('.last() - get last DOM element', () => {
// https://on.cypress.io/last
cy.get('.traversal-buttons .btn')
.last().should('contain', 'Submit')
})
it(".last() - get last DOM element", () => {
// https://on.cypress.io/last
cy.get(".traversal-buttons .btn").last().should("contain", "Submit");
});
it('.next() - get next sibling DOM element', () => {
// https://on.cypress.io/next
cy.get('.traversal-ul')
.contains('apples').next().should('contain', 'oranges')
})
it(".next() - get next sibling DOM element", () => {
// https://on.cypress.io/next
cy.get(".traversal-ul").contains("apples").next().should("contain", "oranges");
});
it('.nextAll() - get all next sibling DOM elements', () => {
// https://on.cypress.io/nextall
cy.get('.traversal-next-all')
.contains('oranges')
.nextAll().should('have.length', 3)
})
it(".nextAll() - get all next sibling DOM elements", () => {
// https://on.cypress.io/nextall
cy.get(".traversal-next-all").contains("oranges").nextAll().should("have.length", 3);
});
it('.nextUntil() - get next sibling DOM elements until next el', () => {
// https://on.cypress.io/nextuntil
cy.get('#veggies')
.nextUntil('#nuts').should('have.length', 3)
})
it(".nextUntil() - get next sibling DOM elements until next el", () => {
// https://on.cypress.io/nextuntil
cy.get("#veggies").nextUntil("#nuts").should("have.length", 3);
});
it('.not() - remove DOM elements from set of DOM elements', () => {
// https://on.cypress.io/not
cy.get('.traversal-disabled .btn')
.not('[disabled]').should('not.contain', 'Disabled')
})
it(".not() - remove DOM elements from set of DOM elements", () => {
// https://on.cypress.io/not
cy.get(".traversal-disabled .btn").not("[disabled]").should("not.contain", "Disabled");
});
it('.parent() - get parent DOM element from DOM elements', () => {
// https://on.cypress.io/parent
cy.get('.traversal-mark')
.parent().should('contain', 'Morbi leo risus')
})
it(".parent() - get parent DOM element from DOM elements", () => {
// https://on.cypress.io/parent
cy.get(".traversal-mark").parent().should("contain", "Morbi leo risus");
});
it('.parents() - get parent DOM elements from DOM elements', () => {
// https://on.cypress.io/parents
cy.get('.traversal-cite')
.parents().should('match', 'blockquote')
})
it(".parents() - get parent DOM elements from DOM elements", () => {
// https://on.cypress.io/parents
cy.get(".traversal-cite").parents().should("match", "blockquote");
});
it('.parentsUntil() - get parent DOM elements from DOM elements until el', () => {
// https://on.cypress.io/parentsuntil
cy.get('.clothes-nav')
.find('.active')
.parentsUntil('.clothes-nav')
.should('have.length', 2)
})
it(".parentsUntil() - get parent DOM elements from DOM elements until el", () => {
// https://on.cypress.io/parentsuntil
cy.get(".clothes-nav").find(".active").parentsUntil(".clothes-nav").should("have.length", 2);
});
it('.prev() - get previous sibling DOM element', () => {
// https://on.cypress.io/prev
cy.get('.birds').find('.active')
.prev().should('contain', 'Lorikeets')
})
it(".prev() - get previous sibling DOM element", () => {
// https://on.cypress.io/prev
cy.get(".birds").find(".active").prev().should("contain", "Lorikeets");
});
it('.prevAll() - get all previous sibling DOM elements', () => {
// https://on.cypress.io/prevall
cy.get('.fruits-list').find('.third')
.prevAll().should('have.length', 2)
})
it(".prevAll() - get all previous sibling DOM elements", () => {
// https://on.cypress.io/prevall
cy.get(".fruits-list").find(".third").prevAll().should("have.length", 2);
});
it('.prevUntil() - get all previous sibling DOM elements until el', () => {
// https://on.cypress.io/prevuntil
cy.get('.foods-list').find('#nuts')
.prevUntil('#veggies').should('have.length', 3)
})
it(".prevUntil() - get all previous sibling DOM elements until el", () => {
// https://on.cypress.io/prevuntil
cy.get(".foods-list").find("#nuts").prevUntil("#veggies").should("have.length", 3);
});
it('.siblings() - get all sibling DOM elements', () => {
// https://on.cypress.io/siblings
cy.get('.traversal-pills .active')
.siblings().should('have.length', 2)
})
})
it(".siblings() - get all sibling DOM elements", () => {
// https://on.cypress.io/siblings
cy.get(".traversal-pills .active").siblings().should("have.length", 2);
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,11 +2,7 @@
"compilerOptions": {
"allowJs": true,
"baseUrl": "../node_modules",
"types": [
"cypress"
]
"types": ["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) => {
uri = new URL(uri + ".js", parentUri).href;
return registry[uri] || (
new Promise(resolve => {
if ("document" in self) {
const script = document.createElement("script");
script.src = uri;
script.onload = resolve;
document.head.appendChild(script);
} else {
nextDefineUri = uri;
importScripts(uri);
resolve();
}
})
.then(() => {
return (
registry[uri] ||
new Promise((resolve) => {
if ("document" in self) {
const script = document.createElement("script");
script.src = uri;
script.onload = resolve;
document.head.appendChild(script);
} else {
nextDefineUri = uri;
importScripts(uri);
resolve();
}
}).then(() => {
let promise = registry[uri];
if (!promise) {
throw new Error(`Module ${uri} didnt register its module`);
@@ -53,21 +51,20 @@ if (!self.define) {
return;
}
let exports = {};
const require = depUri => singleRequire(depUri, uri);
const require = (depUri) => singleRequire(depUri, uri);
const specialDeps = {
module: { uri },
exports,
require
};
registry[uri] = Promise.all(depsNames.map(
depName => specialDeps[depName] || require(depName)
)).then(deps => {
registry[uri] = Promise.all(depsNames.map((depName) => specialDeps[depName] || require(depName))).then((deps) => {
factory(...deps);
return exports;
});
};
}
define(['./workbox-b5f7729d'], (function (workbox) { 'use strict';
define(["./workbox-b5f7729d"], function (workbox) {
"use strict";
self.skipWaiting();
workbox.clientsClaim();
@@ -77,16 +74,23 @@ define(['./workbox-b5f7729d'], (function (workbox) { 'use strict';
* requests for URLs in the manifest.
* See https://goo.gl/S9QRab
*/
workbox.precacheAndRoute([{
"url": "registerSW.js",
"revision": "3ca0b8505b4bec776b69afdba2768812"
}, {
"url": "index.html",
"revision": "0.sa702m4aq68"
}], {});
workbox.precacheAndRoute(
[
{
url: "registerSW.js",
revision: "3ca0b8505b4bec776b69afdba2768812"
},
{
url: "index.html",
revision: "0.sa702m4aq68"
}
],
{}
);
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
allowlist: [/^\/$/]
}));
}));
workbox.registerRoute(
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",
"@apollo/client": "^3.8.10",
"@asseinfo/react-kanban": "^2.2.0",
"@craco/craco": "^7.1.0",
"@fingerprintjs/fingerprintjs": "^4.2.2",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.2.1",
"@sentry/cli": "^2.28.6",
"@sentry/react": "^7.102.1",
"@sentry/tracing": "^7.102.1",
"@sentry/react": "^7.104.0",
"@splitsoftware/splitio-react": "^1.11.0",
"@tanem/react-nprogress": "^5.0.51",
"@vitejs/plugin-legacy": "^5.3.0",
"@vitejs/plugin-react": "^4.2.1",
"@vitejs/plugin-react-refresh": "^1.3.6",
"@vitejs/plugin-react-swc": "^3.6.0",
"antd": "^5.14.2",
"antd": "^5.15.3",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0",
"axios": "^1.6.7",
"consola": "^3.2.3",
"dayjs": "^1.11.10",
"dayjs-business-days2": "^1.2.2",
"dinero.js": "^1.9.1",
"dotenv": "^16.4.5",
"enquire-js": "^0.2.1",
"env-cmd": "^10.1.0",
"esbuild": "^0.20.0",
"exifr": "^7.1.3",
"firebase": "^10.8.0",
"firebase": "^10.8.1",
"graphql": "^16.6.0",
"i18next": "^23.10.0",
"i18next-browser-languagedetector": "^7.0.2",
"jsoneditor": "^10.0.1",
"jsreport-browser-client-dist": "^1.3.0",
"libphonenumber-js": "^1.10.57",
"logrocket": "^8.0.1",
"markerjs2": "^2.32.0",
"normalize-url": "^8.0.0",
"phone": "^3.1.42",
"preval.macro": "^5.0.0",
"prop-types": "^15.8.1",
"query-string": "^8.2.0",
"rc-queue-anim": "^2.0.0",
"rc-scroll-anim": "^2.7.6",
"query-string": "^9.0.0",
"react": "^18.2.0",
"react-big-calendar": "^1.10.3",
"react-big-calendar": "^1.11.0",
"react-color": "^2.19.3",
"react-cookie": "^7.1.0",
"react-dom": "^18.2.0",
@@ -65,17 +51,17 @@
"react-i18next": "^14.0.5",
"react-icons": "^5.0.1",
"react-image-lightbox": "^5.1.4",
"react-intersection-observer": "^9.8.1",
"react-joyride": "^2.7.4",
"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-resizable": "^3.0.5",
"react-router-dom": "^6.22.1",
"react-router-dom": "^6.22.2",
"react-scripts": "^5.0.1",
"react-sticky": "^6.0.3",
"react-sublime-video": "^0.2.5",
"react-virtualized": "^9.22.5",
"recharts": "^2.12.1",
"recharts": "^2.12.2",
"redux": "^5.0.1",
"redux-persist": "^6.0.0",
"redux-saga": "^1.3.0",
@@ -86,17 +72,15 @@
"styled-components": "^6.1.8",
"subscriptions-transport-ws": "^0.11.0",
"terser-webpack-plugin": "^5.3.10",
"vite-plugin-compression": "^0.5.1",
"userpilot": "^1.3.1",
"vite-plugin-ejs": "^1.7.0",
"vite-plugin-svgr": "^4.2.0",
"web-vitals": "^3.5.2",
"workbox-core": "^7.0.0",
"workbox-expiration": "^7.0.0",
"workbox-navigation-preload": "^7.0.0",
"workbox-precaching": "^7.0.0",
"workbox-routing": "^7.0.0",
"workbox-strategies": "^7.0.0",
"yauzl": "^3.1.0"
"workbox-strategies": "^7.0.0"
},
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
@@ -139,10 +123,13 @@
"resolutions": {
"react-error-overlay": "6.0.9"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "4.6.1"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-react": "^7.23.3",
"@dotenvx/dotenvx": "^0.15.0",
"@dotenvx/dotenvx": "^0.15.4",
"@emotion/babel-plugin": "^11.11.0",
"@emotion/react": "^11.11.3",
"@sentry/webpack-plugin": "^2.14.2",
@@ -151,9 +138,10 @@
"@testing-library/cypress": "^10.0.1",
"browserslist": "^4.22.3",
"browserslist-to-esbuild": "^2.1.1",
"craco-less": "^3.0.1",
"cross-env": "^7.0.3",
"cypress": "^13.6.6",
"eslint": "^8.57.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-cypress": "^2.15.1",
"memfs": "^4.6.0",
"os-browserify": "^0.3.0",
@@ -162,6 +150,7 @@
"source-map-explorer": "^2.5.3",
"vite": "^5.0.11",
"vite-plugin-babel": "^1.2.0",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-legacy": "^2.1.0",
"vite-plugin-node-polyfills": "^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 {SplitFactoryProvider, SplitSdk,} from '@splitsoftware/splitio-react';
import {ConfigProvider} from "antd";
import { ApolloProvider } from "@apollo/client";
import { SplitFactoryProvider, SplitSdk } from "@splitsoftware/splitio-react";
import { ConfigProvider } from "antd";
import enLocale from "antd/es/locale/en_US";
import dayjs from "../utils/day";
import 'dayjs/locale/en';
import "dayjs/locale/en";
import React from "react";
import {useTranslation} from "react-i18next";
import { useTranslation } from "react-i18next";
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
import client from "../utils/GraphQLClient";
import App from "./App";
import * as Sentry from "@sentry/react";
import themeProvider from "./themeProvider";
import { Userpilot } from "userpilot";
// Initialize Userpilot
if (import.meta.env.DEV) {
Userpilot.initialize("NX-69145f08");
}
dayjs.locale("en");
const config = {
core: {
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
key: "anon",
},
core: {
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
key: "anon"
}
};
export const factory = SplitSdk(config);
function AppContainer() {
const {t} = useTranslation();
const { t } = useTranslation();
return (
<ApolloProvider client={client}>
<ConfigProvider
//componentSize="small"
input={{autoComplete: "new-password"}}
locale={enLocale}
theme={themeProvider}
form={{
validateMessages: {
// eslint-disable-next-line no-template-curly-in-string
required: t("general.validation.required", {label: "${label}"}),
},
}}
>
<GlobalLoadingBar/>
<SplitFactoryProvider factory={factory}>
<App/>
</SplitFactoryProvider>
</ConfigProvider>
</ApolloProvider>
);
return (
<ApolloProvider client={client}>
<ConfigProvider
//componentSize="small"
input={{ autoComplete: "new-password" }}
locale={enLocale}
theme={themeProvider}
form={{
validateMessages: {
// eslint-disable-next-line no-template-curly-in-string
required: t("general.validation.required", { label: "${label}" })
}
}}
>
<GlobalLoadingBar />
<SplitFactoryProvider factory={factory}>
<App />
</SplitFactoryProvider>
</ConfigProvider>
</ApolloProvider>
);
}
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 { selectOnline } from "../redux/application/application.selectors";
import { checkUserSession } from "../redux/user/user.actions";
import {
selectBodyshop,
selectCurrentEula,
selectCurrentUser,
} from "../redux/user/user.selectors";
import { selectBodyshop, selectCurrentEula, selectCurrentUser } from "../redux/user/user.selectors";
import PrivateRoute from "../components/PrivateRoute";
import "./App.styles.scss";
import handleBeta from "../utils/betaHandler";
import Eula from "../components/eula/eula.component";
import InstanceRenderMgr from "../utils/instanceRenderMgr";
const ResetPassword = lazy(() =>
import("../pages/reset-password/reset-password.component")
);
import { ProductFruits } from "react-product-fruits";
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
const MobilePaymentContainer = lazy(() =>
import("../pages/mobile-payment/mobile-payment.container")
);
const MobilePaymentContainer = lazy(() => import("../pages/mobile-payment/mobile-payment.container"));
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
online: selectOnline,
bodyshop: selectBodyshop,
currentEula: selectCurrentEula,
currentUser: selectCurrentUser,
online: selectOnline,
bodyshop: selectBodyshop,
currentEula: selectCurrentEula
});
const mapDispatchToProps = (dispatch) => ({
checkUserSession: () => dispatch(checkUserSession()),
setOnline: (isOnline) => dispatch(setOnline(isOnline)),
checkUserSession: () => dispatch(checkUserSession()),
setOnline: (isOnline) => dispatch(setOnline(isOnline))
});
export function App({
bodyshop,
checkUserSession,
currentUser,
online,
setOnline,
currentEula,
}) {
const client = useSplitClient().client;
const [listenersAdded, setListenersAdded] = useState(false);
const { t } = useTranslation();
export function App({ bodyshop, checkUserSession, currentUser, online, setOnline, currentEula }) {
const client = useSplitClient().client;
const [listenersAdded, setListenersAdded] = useState(false);
const { t } = useTranslation();
useEffect(() => {
if (!navigator.onLine) {
setOnline(false);
}
checkUserSession();
}, [checkUserSession, setOnline]);
//const b = Grid.useBreakpoint();
// console.log("Breakpoints:", b);
// Associate event listeners, memoize to prevent multiple listeners being added
useEffect(() => {
const offlineListener = (e) => {
setOnline(false);
};
const onlineListener = (e) => {
setOnline(true);
};
if (!listenersAdded) {
console.log("Added events for offline and online");
window.addEventListener("offline", offlineListener);
window.addEventListener("online", onlineListener);
setListenersAdded(true);
}
return () => {
window.removeEventListener("offline", offlineListener);
window.removeEventListener("online", onlineListener);
};
}, [setOnline, listenersAdded]);
useEffect(() => {
if (currentUser.authorized && bodyshop) {
client.setAttribute("imexshopid", bodyshop.imexshopid);
if (
client.getTreatment("LogRocket_Tracking") === "on" ||
window.location.hostname ===
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")} />;
useEffect(() => {
if (!navigator.onLine) {
setOnline(false);
}
handleBeta();
checkUserSession();
}, [checkUserSession, setOnline]);
if (!online)
return (
<Result
status="warning"
title={t("general.labels.nointernet")}
subTitle={t("general.labels.nointernet_sub")}
extra={
<Button
type="primary"
onClick={() => {
window.location.reload();
}}
>
{t("general.actions.refresh")}
</Button>
}
/>
//const b = Grid.useBreakpoint();
// console.log("Breakpoints:", b);
// 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.
})
);
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 (
<Suspense
fallback={
<LoadingSpinner
message={InstanceRenderMgr({
imex: t("titles.imexonline"),
rome: t("titles.romeonline"),
promanager: t("titles.promanager")
})}
/>
}
>
<Routes>
<Route
path="*"
element={
<ErrorBoundary>
<LandingPage />
</ErrorBoundary>
}
/>
<Route
path="/signin"
element={
<ErrorBoundary>
<SignInPage />
</ErrorBoundary>
}
/>
<Route
path="/resetpassword"
element={
<ErrorBoundary>
<ResetPassword />
</ErrorBoundary>
}
/>
<Route
path="/csi/:surveyId"
element={
<ErrorBoundary>
<CsiPage />
</ErrorBoundary>
}
/>
<Route
path="/disclaimer"
element={
<ErrorBoundary>
<DisclaimerPage />
</ErrorBoundary>
}
/>
<Route
path="/mp/:paymentIs"
element={
<ErrorBoundary>
<MobilePaymentContainer />
</ErrorBoundary>
}
/>
<Route
path="/manage/*"
element={
<ErrorBoundary>
<PrivateRoute
isAuthorized={currentUser.authorized}
/>
</ErrorBoundary>
}
>
<Route path="*" element={<ManagePage />} />
</Route>
<Route
path="/tech/*"
element={
<ErrorBoundary>
<PrivateRoute
isAuthorized={currentUser.authorized}
/>
</ErrorBoundary>
}
>
<Route path="*" element={<TechPageContainer />} />
</Route>
<Route
path="/edit/*"
element={
<PrivateRoute isAuthorized={currentUser.authorized} />
}
>
<Route path="*" element={<DocumentEditorContainer />} />
</Route>
</Routes>
</Suspense>
<Result
status="warning"
title={t("general.labels.nointernet")}
subTitle={t("general.labels.nointernet_sub")}
extra={
<Button
type="primary"
onClick={() => {
window.location.reload();
}}
>
{t("general.actions.refresh")}
</Button>
}
/>
);
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);

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

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

View File

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

View File

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

View File

@@ -1,233 +1,196 @@
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 { Card, Checkbox, Input, Space, Table } from "antd";
import queryString from "query-string";
import {logImEXEvent} from "../../firebase/firebase.utils";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {selectBodyshop} from "../../redux/user/user.selectors";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectBodyshop } from "../../redux/user/user.selectors";
import 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 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 {pageLimit} from "../../utils/config";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(AccountingPayablesTableComponent);
export default connect(mapStateToProps, mapDispatchToProps)(AccountingPayablesTableComponent);
export function AccountingPayablesTableComponent({
bodyshop,
loading,
bills,
refetch,
}) {
const {t} = useTranslation();
const [selectedBills, setSelectedBills] = useState([]);
const [transInProgress, setTransInProgress] = useState(false);
const [state, setState] = useState({
sortedInfo: {},
search: "",
});
export function AccountingPayablesTableComponent({ bodyshop, loading, bills, refetch }) {
const { t } = useTranslation();
const [selectedBills, setSelectedBills] = useState([]);
const [transInProgress, setTransInProgress] = useState(false);
const [state, setState] = useState({
sortedInfo: {},
search: ""
});
const handleTableChange = (pagination, filters, sorter) => {
setState({...state, filteredInfo: filters, sortedInfo: sorter});
};
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
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) => (
<Link
to={{
pathname: `/manage/shop/vendors`,
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>
}
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) => (
<Link
to={{
pathname: `/manage/shop/vendors`,
search: queryString.stringify({ selectedvendor: record.vendor.id })
}}
>
<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>
);
{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",
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 React, {useState} from "react";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {Link} from "react-router-dom";
import {createStructuredSelector} from "reselect";
import {logImEXEvent} from "../../firebase/firebase.utils";
import {selectBodyshop} from "../../redux/user/user.selectors";
import { Card, Input, Space, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import {DateFormatter, DateTimeFormatter} from "../../utils/DateFormatter";
import {alphaSort, dateSort} from "../../utils/sorters";
import { DateFormatter, DateTimeFormatter } 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 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 PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component";
import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(AccountingPayablesTableComponent);
export default connect(mapStateToProps, mapDispatchToProps)(AccountingPayablesTableComponent);
export function AccountingPayablesTableComponent({
bodyshop,
loading,
payments,
refetch,
}) {
const {t} = useTranslation();
const [selectedPayments, setSelectedPayments] = useState([]);
const [transInProgress, setTransInProgress] = useState(false);
const [state, setState] = useState({
sortedInfo: {},
search: "",
});
export function AccountingPayablesTableComponent({ bodyshop, loading, payments, refetch }) {
const { t } = useTranslation();
const [selectedPayments, setSelectedPayments] = useState([]);
const [transInProgress, setTransInProgress] = useState(false);
const [state, setState] = useState({
sortedInfo: {},
search: ""
});
const handleTableChange = (pagination, filters, sorter) => {
setState({...state, filteredInfo: filters, sortedInfo: sorter});
};
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const columns = [
{
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("payments.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>,
},
const columns = [
{
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("payments.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("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.job.owner ? (
<Link to={"/manage/owners/" + record.job.owner.id}>
<OwnerNameDisplay ownerObject={record.job}/>
</Link>
) : (
<span>
<OwnerNameDisplay ownerObject={record.job}/>
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a.job), OwnerNameDisplayFunction(b.job)),
sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.job.owner ? (
<Link to={"/manage/owners/" + record.job.owner.id}>
<OwnerNameDisplay ownerObject={record.job} />
</Link>
) : (
<span>
<OwnerNameDisplay ownerObject={record.job} />
</span>
);
},
},
{
title: t("payments.fields.amount"),
dataIndex: "amount",
key: "amount",
render: (text, record) => (
<CurrencyFormatter>{record.amount}</CurrencyFormatter>
),
},
{
title: t("payments.fields.memo"),
dataIndex: "memo",
key: "memo",
},
{
title: t("payments.fields.transactionid"),
dataIndex: "transactionid",
key: "transactionid",
},
{
title: t("payments.fields.created_at"),
dataIndex: "created_at",
key: "created_at",
render: (text, record) => (
<DateTimeFormatter>{record.created_at}</DateTimeFormatter>
),
},
{
title: t("payments.fields.exportedat"),
dataIndex: "exportedat",
key: "exportedat",
render: (text, record) => (
<DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
),
},
{
title: t("exportlogs.labels.attempts"),
dataIndex: "attempts",
key: "attempts",
);
}
},
{
title: t("payments.fields.amount"),
dataIndex: "amount",
key: "amount",
sorter: (a, b) => a.amount - b.amount,
sortOrder: state.sortedInfo.columnKey === "amount" && state.sortedInfo.order,
render: (text, record) => <CurrencyFormatter>{record.amount}</CurrencyFormatter>
},
{
title: t("payments.fields.memo"),
dataIndex: "memo",
key: "memo"
},
{
title: t("payments.fields.transactionid"),
dataIndex: "transactionid",
key: "transactionid"
},
{
title: t("payments.fields.created_at"),
dataIndex: "created_at",
key: "created_at",
sorter: (a, b) => dateSort(a.created_at, b.created_at),
sortOrder: state.sortedInfo.columnKey === "created_at" && state.sortedInfo.order,
render: (text, record) => <DateTimeFormatter>{record.created_at}</DateTimeFormatter>
},
//{
// title: t("payments.fields.exportedat"),
// dataIndex: "exportedat",
// key: "exportedat",
// render: (text, record) => (
// <DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
// ),
//},
{
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) => <ExportLogsCountDisplay logs={record.exportlogs} />
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<PaymentExportButton
paymentId={record.id}
disabled={transInProgress || !!record.exportedat}
loadingCallback={setTransInProgress}
setSelectedPayments={setSelectedPayments}
refetch={refetch}
/>
),
},
];
render: (text, record) => (
<PaymentExportButton
paymentId={record.id}
disabled={transInProgress || !!record.exportedat}
loadingCallback={setTransInProgress}
setSelectedPayments={setSelectedPayments}
refetch={refetch}
/>
)
}
];
const handleSearch = (e) => {
setState({...state, search: e.target.value});
logImEXEvent("account_payments_table_search");
};
const handleSearch = (e) => {
setState({ ...state, search: e.target.value });
logImEXEvent("account_payments_table_search");
};
const dataSource = state.search
? payments.filter(
(v) =>
(v.paymentnum || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
((v.job && v.job.ro_number) || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
((v.job && v.job.ownr_fn) || "")
.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;
const dataSource = state.search
? payments.filter(
(v) =>
(v.paymentnum || "").toLowerCase().includes(state.search.toLowerCase()) ||
((v.job && v.job.ro_number) || "").toLowerCase().includes(state.search.toLowerCase()) ||
((v.job && v.job.ownr_fn) || "").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 (
<Card
extra={
<Space wrap>
<PaymentMarkSelectedExported
paymentIds={selectedPayments}
disabled={transInProgress || selectedPayments.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedPayments}
refetch={refetch}
/>
<PaymentsExportAllButton
paymentIds={selectedPayments}
disabled={transInProgress || selectedPayments.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedPayments}
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) =>
setSelectedPayments(selectedRows.map((i) => i.id)),
onSelect: (record, selected, selectedRows, nativeEvent) => {
setSelectedPayments(selectedRows.map((i) => i.id));
},
getCheckboxProps: (record) => ({
disabled: record.exported,
}),
selectedRowKeys: selectedPayments,
type: "checkbox",
}}
/>
</Card>
);
return (
<Card
extra={
<Space wrap>
<PaymentMarkSelectedExported
paymentIds={selectedPayments}
disabled={transInProgress || selectedPayments.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedPayments}
refetch={refetch}
/>
<PaymentsExportAllButton
paymentIds={selectedPayments}
disabled={transInProgress || selectedPayments.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedPayments}
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) => setSelectedPayments(selectedRows.map((i) => i.id)),
onSelect: (record, selected, selectedRows, nativeEvent) => {
setSelectedPayments(selectedRows.map((i) => i.id));
},
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 React, {useState} from "react";
import {useTranslation} from "react-i18next";
import {Link} from "react-router-dom";
import {logImEXEvent} from "../../firebase/firebase.utils";
import { Button, Card, Input, Space, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { logImEXEvent } from "../../firebase/firebase.utils";
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 JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {selectBodyshop} from "../../redux/user/user.selectors";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import {DateFormatter} from "../../utils/DateFormatter";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateFormatter } from "../../utils/DateFormatter";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(AccountingReceivablesTableComponent);
export default connect(mapStateToProps, mapDispatchToProps)(AccountingReceivablesTableComponent);
export function AccountingReceivablesTableComponent({
bodyshop,
loading,
jobs,
refetch,
}) {
const {t} = useTranslation();
const [selectedJobs, setSelectedJobs] = useState([]);
const [transInProgress, setTransInProgress] = useState(false);
export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, refetch }) {
const { t } = useTranslation();
const [selectedJobs, setSelectedJobs] = useState([]);
const [transInProgress, setTransInProgress] = useState(false);
const [state, setState] = useState({
sortedInfo: {},
search: "",
});
const [state, setState] = useState({
sortedInfo: {},
search: ""
});
const handleTableChange = (pagination, filters, sorter) => {
setState({...state, filteredInfo: filters, sortedInfo: sorter});
};
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
<Link to={"/manage/jobs/" + record.id}>{record.ro_number}</Link>
),
},
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => <Link to={"/manage/jobs/" + record.id}>{record.ro_number}</Link>
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => a.status - b.status,
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
},
{
title: t("jobs.fields.date_invoiced"),
dataIndex: "date_invoiced",
key: "date_invoiced",
sorter: (a, b) => dateSort(a.date_invoiced, b.date_invoiced),
sortOrder:
state.sortedInfo.columnKey === "date_invoiced" &&
state.sortedInfo.order,
render: (text, record) => (
<DateFormatter>{record.date_invoiced}</DateFormatter>
),
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.owner ? (
<Link to={"/manage/owners/" + record.owner.id}>
<OwnerNameDisplay ownerObject={record}/>
</Link>
) : (
<span>
<OwnerNameDisplay ownerObject={record}/>
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => statusSort(a, b, bodyshop.md_ro_statuses.statuses),
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order
},
{
title: t("jobs.fields.date_invoiced"),
dataIndex: "date_invoiced",
key: "date_invoiced",
sorter: (a, b) => dateSort(a.date_invoiced, b.date_invoiced),
sortOrder: state.sortedInfo.columnKey === "date_invoiced" && state.sortedInfo.order,
render: (text, record) => <DateFormatter>{record.date_invoiced}</DateFormatter>
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.owner ? (
<Link to={"/manage/owners/" + record.owner.id}>
<OwnerNameDisplay ownerObject={record} />
</Link>
) : (
<span>
<OwnerNameDisplay ownerObject={record} />
</span>
);
},
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
render: (text, record) => {
return record.vehicleid ? (
<Link to={"/manage/vehicles/" + record.vehicleid}>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</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",
ellipsis: true,
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",
key: "clm_total",
sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder:
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
render: (text, record) => {
return <CurrencyFormatter>{record.clm_total}</CurrencyFormatter>;
},
},
{
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",
);
}
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${a.v_model_desc || ""}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder: state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => {
return record.vehicleid ? (
<Link to={"/manage/vehicles/" + record.vehicleid}>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}
</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",
ellipsis: true,
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",
key: "clm_total",
sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder: state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
render: (text, record) => {
return <CurrencyFormatter>{record.clm_total}</CurrencyFormatter>;
}
},
{
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) => (
<Space wrap>
<JobExportButton
jobId={record.id}
disabled={!!record.date_exported}
setSelectedJobs={setSelectedJobs}
refetch={refetch}
/>
<Link to={`/manage/jobs/${record.id}/close`}>
<Button>{t("jobs.labels.viewallocations")}</Button>
</Link>
</Space>
),
},
];
render: (text, record) => (
<Space wrap>
<JobExportButton
jobId={record.id}
disabled={!!record.date_exported}
setSelectedJobs={setSelectedJobs}
refetch={refetch}
/>
<Link to={`/manage/jobs/${record.id}/close`}>
<Button>{t("jobs.labels.viewallocations")}</Button>
</Link>
</Space>
)
}
];
const handleSearch = (e) => {
setState({...state, search: e.target.value});
logImEXEvent("accounting_receivables_search");
};
const handleSearch = (e) => {
setState({ ...state, search: e.target.value });
logImEXEvent("accounting_receivables_search");
};
const dataSource = state.search
? jobs.filter(
(v) =>
(v.ro_number || "")
.toString()
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(v.ownr_fn || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(v.ownr_ln || "")
.toLowerCase()
.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;
const dataSource = state.search
? jobs.filter(
(v) =>
(v.ro_number || "").toString().toLowerCase().includes(state.search.toLowerCase()) ||
(v.ownr_fn || "").toLowerCase().includes(state.search.toLowerCase()) ||
(v.ownr_ln || "").toLowerCase().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 (
<Card
extra={
<Space wrap>
{!bodyshop.cdk_dealerid && !bodyshop.pbs_serialnumber && (
<JobsExportAllButton
jobIds={selectedJobs}
disabled={transInProgress || selectedJobs.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedJobs}
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",
}}
return (
<Card
extra={
<Space wrap>
{!bodyshop.cdk_dealerid && !bodyshop.pbs_serialnumber && (
<JobsExportAllButton
jobIds={selectedJobs}
disabled={transInProgress || selectedJobs.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedJobs}
refetch={refetch}
/>
</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";
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 Alert from "./alert.component";
describe("Alert component", () => {
let wrapper;
beforeEach(() => {
const mockProps = {
type: "error",
message: "Test error message.",
};
let wrapper;
beforeEach(() => {
const mockProps = {
type: "error",
message: "Test error message."
};
wrapper = shallow(<Alert {...mockProps} />);
});
wrapper = shallow(<Alert {...mockProps} />);
});
it("should render Alert component", () => {
expect(wrapper).toMatchSnapshot();
});
it("should render Alert component", () => {
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 {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {selectBodyshop} from "../../redux/user/user.selectors";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
bodyshop: selectBodyshop
});
export function AllocationsAssignmentComponent({
bodyshop,
handleAssignment,
assignment,
setAssignment,
visibilityState,
maxHours
}) {
const {t} = useTranslation();
bodyshop,
handleAssignment,
assignment,
setAssignment,
visibilityState,
maxHours
}) {
const { t } = useTranslation();
const onChange = e => {
setAssignment({...assignment, employeeid: e});
};
const onChange = (e) => {
setAssignment({ ...assignment, employeeid: e });
};
const [visibility, setVisibility] = visibilityState;
const [visibility, setVisibility] = visibilityState;
const popContent = (
<div>
<Select id="employeeSelector"
showSearch
style={{width: 200}}
placeholder='Select a person'
optionFilterProp='children'
onChange={onChange}
filterOption={(input, option) =>
option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}>
{bodyshop.employees.map(emp => (
<Select.Option value={emp.id} key={emp.id}>
{`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}
</Select>
<InputNumber
defaultValue={assignment.hours}
placeholder={t("joblines.fields.mod_lb_hrs")}
max={parseFloat(maxHours)}
min={0}
onChange={e => setAssignment({...assignment, hours: e})}
/>
const popContent = (
<div>
<Select
id="employeeSelector"
showSearch
style={{ width: 200 }}
placeholder="Select a person"
optionFilterProp="children"
onChange={onChange}
filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
>
{bodyshop.employees.map((emp) => (
<Select.Option value={emp.id} key={emp.id}>
{`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}
</Select>
<InputNumber
defaultValue={assignment.hours}
placeholder={t("joblines.fields.mod_lb_hrs")}
max={parseFloat(maxHours)}
min={0}
onChange={(e) => setAssignment({ ...assignment, hours: e })}
/>
<Button
type='primary'
disabled={!assignment.employeeid}
onClick={handleAssignment}>
Assign
</Button>
<Button onClick={() => setVisibility(false)}>Close</Button>
</div>
);
<Button type="primary" disabled={!assignment.employeeid} onClick={handleAssignment}>
Assign
</Button>
<Button onClick={() => setVisibility(false)}>Close</Button>
</div>
);
return (
<Popover content={popContent} open={visibility}>
<Button onClick={() => setVisibility(true)}>
{t("allocations.actions.assign")}
</Button>
</Popover>
);
return (
<Popover content={popContent} open={visibility}>
<Button onClick={() => setVisibility(true)}>{t("allocations.actions.assign")}</Button>
</Popover>
);
}
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 {MockBodyshop} from "../../utils/TestingHelpers";
import {AllocationsAssignmentComponent} from "./allocations-assignment.component";
import { MockBodyshop } from "../../utils/TestingHelpers";
import { AllocationsAssignmentComponent } from "./allocations-assignment.component";
describe("AllocationsAssignmentComponent component", () => {
let wrapper;
let wrapper;
beforeEach(() => {
const mockProps = {
bodyshop: MockBodyshop,
handleAssignment: jest.fn(),
assignment: {},
setAssignment: jest.fn(),
visibilityState: [false, jest.fn()],
maxHours: 4,
};
beforeEach(() => {
const mockProps = {
bodyshop: MockBodyshop,
handleAssignment: jest.fn(),
assignment: {},
setAssignment: jest.fn(),
visibilityState: [false, jest.fn()],
maxHours: 4
};
wrapper = mount(<AllocationsAssignmentComponent {...mockProps} />);
});
wrapper = mount(<AllocationsAssignmentComponent {...mockProps} />);
});
it("should render AllocationsAssignmentComponent component", () => {
expect(wrapper).toMatchSnapshot();
});
it("should render AllocationsAssignmentComponent component", () => {
expect(wrapper).toMatchSnapshot();
});
it("should render a list of employees", () => {
const empList = wrapper.find("#employeeSelector");
expect(empList.children()).to.have.lengthOf(2);
});
it("should render a list of employees", () => {
const empList = wrapper.find("#employeeSelector");
expect(empList.children()).to.have.lengthOf(2);
});
it("should create an allocation on save", () => {
wrapper.find("Button").simulate("click");
expect(wrapper).toMatchSnapshot();
});
it("should create an allocation on save", () => {
wrapper.find("Button").simulate("click");
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 {useMutation} from "@apollo/client";
import {INSERT_ALLOCATION} from "../../graphql/allocations.queries";
import {useTranslation} from "react-i18next";
import {notification} from "antd";
import { useMutation } from "@apollo/client";
import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
import { useTranslation } from "react-i18next";
import { notification } from "antd";
export default function AllocationsAssignmentContainer({
jobLineId,
hours,
refetch,
}) {
const visibilityState = useState(false);
const {t} = useTranslation();
const [assignment, setAssignment] = useState({
joblineid: jobLineId,
hours: parseFloat(hours),
employeeid: null,
});
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
export default function AllocationsAssignmentContainer({ jobLineId, hours, refetch }) {
const visibilityState = useState(false);
const { t } = useTranslation();
const [assignment, setAssignment] = useState({
joblineid: jobLineId,
hours: parseFloat(hours),
employeeid: null
});
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
const handleAssignment = () => {
insertAllocation({variables: {alloc: {...assignment}}})
.then((r) => {
notification["success"]({
message: t("allocations.successes.save"),
});
visibilityState[1](false);
if (refetch) refetch();
})
.catch((error) => {
notification["error"]({
message: t("employees.errors.saving", {message: error.message}),
});
});
};
const handleAssignment = () => {
insertAllocation({ variables: { alloc: { ...assignment } } })
.then((r) => {
notification["success"]({
message: t("allocations.successes.save")
});
visibilityState[1](false);
if (refetch) refetch();
})
.catch((error) => {
notification["error"]({
message: t("employees.errors.saving", { message: error.message })
});
});
};
return (
<AllocationsAssignmentComponent
handleAssignment={handleAssignment}
maxHours={hours}
assignment={assignment}
setAssignment={setAssignment}
visibilityState={visibilityState}
/>
);
return (
<AllocationsAssignmentComponent
handleAssignment={handleAssignment}
maxHours={hours}
assignment={assignment}
setAssignment={setAssignment}
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 {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {selectBodyshop} from "../../redux/user/user.selectors";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop
});
export default connect(
mapStateToProps,
null
mapStateToProps,
null
)(function AllocationsBulkAssignmentComponent({
disabled,
bodyshop,
handleAssignment,
assignment,
setAssignment,
visibilityState,
}) {
const {t} = useTranslation();
disabled,
bodyshop,
handleAssignment,
assignment,
setAssignment,
visibilityState
}) {
const { t } = useTranslation();
const onChange = (e) => {
setAssignment({...assignment, employeeid: e});
};
const onChange = (e) => {
setAssignment({ ...assignment, employeeid: e });
};
const [visibility, setVisibility] = visibilityState;
const [visibility, setVisibility] = visibilityState;
const popContent = (
<div>
<Select
showSearch
style={{width: 200}}
placeholder="Select a person"
optionFilterProp="children"
onChange={onChange}
filterOption={(input, option) =>
option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
>
{bodyshop.employees.map((emp) => (
<Select.Option value={emp.id} key={emp.id}>
{`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}
</Select>
const popContent = (
<div>
<Select
showSearch
style={{ width: 200 }}
placeholder="Select a person"
optionFilterProp="children"
onChange={onChange}
filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
>
{bodyshop.employees.map((emp) => (
<Select.Option value={emp.id} key={emp.id}>
{`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}
</Select>
<Button
type="primary"
disabled={!assignment.employeeid}
onClick={handleAssignment}
>
Assign
</Button>
<Button onClick={() => setVisibility(false)}>Close</Button>
</div>
);
<Button type="primary" disabled={!assignment.employeeid} onClick={handleAssignment}>
Assign
</Button>
<Button onClick={() => setVisibility(false)}>Close</Button>
</div>
);
return (
<Popover content={popContent} open={visibility}>
<Button disabled={disabled} onClick={() => setVisibility(true)}>
{t("allocations.actions.assign")}
</Button>
</Popover>
);
return (
<Popover content={popContent} open={visibility}>
<Button disabled={disabled} onClick={() => setVisibility(true)}>
{t("allocations.actions.assign")}
</Button>
</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 {useMutation} from "@apollo/client";
import {INSERT_ALLOCATION} from "../../graphql/allocations.queries";
import {useTranslation} from "react-i18next";
import {notification} from "antd";
import { useMutation } from "@apollo/client";
import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
import { useTranslation } from "react-i18next";
import { notification } from "antd";
export default function AllocationsBulkAssignmentContainer({
jobLines,
refetch,
}) {
const visibilityState = useState(false);
const {t} = useTranslation();
const [assignment, setAssignment] = useState({
employeeid: null,
export default function AllocationsBulkAssignmentContainer({ jobLines, refetch }) {
const visibilityState = useState(false);
const { t } = useTranslation();
const [assignment, setAssignment] = useState({
employeeid: null
});
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
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 = () => {
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();
});
};
return (
<AllocationsBulkAssignment
disabled={jobLines.length > 0 ? false : true}
handleAssignment={handleAssignment}
assignment={assignment}
setAssignment={setAssignment}
visibilityState={visibilityState}
/>
);
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 React from "react";
import {MdRemoveCircleOutline} from "react-icons/md";
import { MdRemoveCircleOutline } from "react-icons/md";
export default function AllocationsLabelComponent({allocation, handleClick}) {
return (
<div style={{display: "flex", alignItems: "center"}}>
export default function AllocationsLabelComponent({ allocation, handleClick }) {
return (
<div style={{ display: "flex", alignItems: "center" }}>
<span>
{`${allocation.employee.first_name || ""} ${
allocation.employee.last_name || ""
} (${allocation.hours || ""})`}
{`${allocation.employee.first_name || ""} ${allocation.employee.last_name || ""} (${allocation.hours || ""})`}
</span>
<Icon
style={{color: "red", padding: "0px 4px"}}
component={MdRemoveCircleOutline}
onClick={handleClick}
/>
</div>
);
<Icon style={{ color: "red", padding: "0px 4px" }} component={MdRemoveCircleOutline} onClick={handleClick} />
</div>
);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,30 +1,30 @@
import React from "react";
import {List} from "antd";
import { List } from "antd";
import Icon from "@ant-design/icons";
import {FaArrowRight} from "react-icons/fa";
import { FaArrowRight } from "react-icons/fa";
export default function AuditTrailValuesComponent({oldV, newV}) {
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>
);
export default function AuditTrailValuesComponent({ oldV, newV }) {
if (!oldV && !newV) return <div></div>;
if (!oldV && newV)
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>
<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>
);
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 Barcode from "react-barcode";
import {useTranslation} from "react-i18next";
import { useTranslation } from "react-i18next";
export default function BarcodePopupComponent({value, children}) {
const {t} = useTranslation();
return (
<div>
<Popover
content={
<Barcode
value={value || ""}
background="transparent"
displayValue={false}
/>
}
>
{children ? children : <Tag>{t("general.labels.barcode")}</Tag>}
</Popover>
</div>
);
export default function BarcodePopupComponent({ value, children }) {
const { t } = useTranslation();
return (
<div>
<Popover content={<Barcode value={value || ""} 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 React, {useEffect} from "react";
import {useTranslation} from "react-i18next";
import { Checkbox, Form, Skeleton, Typography } from "antd";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
import "./bill-cm-returns-table.styles.scss";
export default function BillCmdReturnsTableComponent({
form,
returnLoading,
returnData,
}) {
const {t} = useTranslation();
export default function BillCmdReturnsTableComponent({ form, returnLoading, returnData }) {
const { t } = useTranslation();
useEffect(() => {
if (returnData) {
form.setFieldsValue({
outstanding_returns: returnData.parts_order_lines,
});
useEffect(() => {
if (returnData) {
form.setFieldsValue({
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 (
<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 (returnLoading) return <Skeleton />;
if (!isReturn) {
return null;
}
return (
<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 (
<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>
<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>
);
<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.Item>
);
</Form.List>
);
}}
</Form.Item>
);
}

View File

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

View File

@@ -1,80 +1,88 @@
import {DeleteFilled} from "@ant-design/icons";
import {useMutation} from "@apollo/client";
import {Button, notification, Popconfirm} from "antd";
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import {DELETE_BILL} from "../../graphql/bills.queries";
import { DeleteFilled } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, notification, Popconfirm } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { DELETE_BILL } from "../../graphql/bills.queries";
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 [loading, setLoading] = useState(false);
const {t} = useTranslation();
const [deleteBill] = useMutation(DELETE_BILL);
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
});
const handleDelete = async () => {
setLoading(true);
const result = await deleteBill({
variables: {billId: bill.id},
update(cache, {errors}) {
if (errors) return;
cache.modify({
fields: {
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)
);
},
},
});
export default connect(mapStateToProps, mapDispatchToProps)(BillDeleteButton);
export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
const [deleteBill] = useMutation(DELETE_BILL);
const handleDelete = async () => {
setLoading(true);
const result = await deleteBill({
variables: { billId: bill.id },
update(cache, { errors }) {
if (errors) return;
cache.modify({
fields: {
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),
}),
});
search_bills(existingBills, { readField }) {
return existingBills.filter((billref) => bill.id !== readField("id", billref));
}
}
}
});
}
});
setLoading(false);
};
if (!!!result.errors) {
notification["success"]({ message: t("bills.successes.deleted") });
insertAuditTrail({
jobid: jobid,
operation: AuditTrailMapping.billdeleted(bill.invoice_number),
type: "billdeleted"
});
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>
);
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);
};
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 {Button, Form, Popconfirm, Space} from "antd";
import { useMutation, useQuery } from "@apollo/client";
import { Button, Divider, Form, Popconfirm, Space } from "antd";
import dayjs from "../../utils/day";
import queryString from "query-string";
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {useLocation} from "react-router-dom";
import {createStructuredSelector} from "reselect";
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 {insertAuditTrail} from "../../redux/application/application.actions";
import {setModalContext} from "../../redux/modals/modals.actions";
import {selectBodyshop} from "../../redux/user/user.selectors";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
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 { insertAuditTrail } from "../../redux/application/application.actions";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import AlertComponent from "../alert/alert.component";
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 LoadingSkeleton from "../loading-skeleton/loading-skeleton.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({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({context: context, modal: "partsOrder"})),
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillDetailEditcontainer);
export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditcontainer);
export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail, bodyshop,}) {
const search = queryString.parse(useLocation().search);
export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail, bodyshop }) {
const search = queryString.parse(useLocation().search);
const {t} = useTranslation();
const [form] = Form.useForm();
const [open, setOpen] = useState(false);
const [updateLoading, setUpdateLoading] = useState(false);
const [update_bill] = useMutation(UPDATE_BILL);
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
const { t } = useTranslation();
const [form] = Form.useForm();
const [open, setOpen] = useState(false);
const [updateLoading, setUpdateLoading] = useState(false);
const [update_bill] = useMutation(UPDATE_BILL);
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
const {loading, error, data, refetch} = useQuery(QUERY_BILL_BY_PK, {
variables: {billid: search.billid},
skip: !!!search.billid,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
const { loading, error, data, refetch } = useQuery(QUERY_BILL_BY_PK, {
variables: { billid: search.billid },
skip: !!!search.billid,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only"
});
// ... 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 = () => {
//It's got a previously deducted bill line!
if (
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length >
0
)
setOpen(true);
else {
form.submit();
}
};
data.bills_by_pk.billlines.forEach((a) => {
const matchingRecord = billlines.find((b) => b.id === a.id);
if (!matchingRecord) {
deletedJobLines.push(a);
}
});
const handleFinish = async (values) => {
setUpdateLoading(true);
//let adjustmentsToInsert = {};
deletedJobLines.forEach((d) => {
updates.push(deleteBillLine({ variables: { id: d.id } }));
});
const {billlines, upload, ...bill} = values;
const updates = [];
billlines.forEach((billline) => {
const { deductedfromlbr, inventories, jobline, ...il } = billline;
delete il.__typename;
if (il.id) {
updates.push(
update_bill({
variables: {billId: search.billid, bill: bill},
})
updateBillLine({
variables: {
billLineId: il.id,
billLine: {
...il,
deductedfromlbr: deductedfromlbr,
joblineid: il.joblineid === "noline" ? null : il.joblineid
}
}
})
);
billlines.forEach((l) => {
delete l.selected;
});
//Find bill lines that were deleted.
const deletedJobLines = [];
data.bills_by_pk.billlines.forEach((a) => {
const matchingRecord = billlines.find((b) => b.id === a.id);
if (!matchingRecord) {
deletedJobLines.push(a);
} 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
}
]
}
});
})
);
}
});
deletedJobLines.forEach((d) => {
updates.push(deleteBillLine({variables: {id: d.id}}));
});
await Promise.all(updates);
billlines.forEach((billline) => {
const {deductedfromlbr, inventories, jobline, ...il} = billline;
delete il.__typename;
insertAuditTrail({
jobid: bill.jobid,
billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number),
type: "billupdated"
});
if (il.id) {
updates.push(
updateBillLine({
variables: {
billLineId: il.id,
billLine: {
...il,
deductedfromlbr: deductedfromlbr,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
},
})
);
} else {
//It's a new line, have to insert it.
updates.push(
insertBillLine({
variables: {
billLines: [
{
...il,
deductedfromlbr: deductedfromlbr,
billid: search.billid,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
],
},
})
);
}
});
await refetch();
form.setFieldsValue(transformData(data));
form.resetFields();
setOpen(false);
setUpdateLoading(false);
};
await Promise.all(updates);
if (error) return <AlertComponent message={error.message} type="error" />;
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
insertAuditTrail({
jobid: bill.jobid,
billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number),
});
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
await refetch();
form.setFieldsValue(transformData(data));
form.resetFields();
setOpen(false);
setUpdateLoading(false);
};
if (error) return <AlertComponent message={error.message} type="error"/>;
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
return (
return (
<>
{loading && <LoadingSkeleton />}
{data && (
<>
{loading && <LoadingSkeleton/>}
{data && (
<>
<PageHeader
title={
data &&
`${data.bills_by_pk.invoice_number} - ${data.bills_by_pk.vendor.name}`
}
extra={
<Space>
<BillDetailEditReturn data={data}/>
<BillPrintButton billid={search.billid}/>
<Popconfirm
open={open}
onConfirm={() => form.submit()}
onCancel={() => setOpen(false)}
okButtonProps={{loading: updateLoading}}
title={t("bills.labels.editadjwarning")}
>
<Button
htmlType="submit"
disabled={exported}
onClick={handleSave}
loading={updateLoading}
type="primary"
>
{t("general.actions.save")}
</Button>
</Popconfirm>
<BillReeportButtonComponent bill={data && data.bills_by_pk}/>
<BillMarkExportedButton bill={data && data.bills_by_pk}/>
</Space>
}
/>
<Form
form={form}
onFinish={handleFinish}
initialValues={transformData(data)}
layout="vertical"
>
<BillFormContainer form={form} billEdit disabled={exported}/>
{bodyshop.uselocalmediaserver ? (
<JobsDocumentsLocalGallery
job={{id: data ? data.bills_by_pk.jobid : null}}
invoice_number={data ? data.bills_by_pk.invoice_number : null}
vendorid={data ? data.bills_by_pk.vendorid : null}
/>
) : (
<JobDocumentsGallery
jobId={data ? data.bills_by_pk.jobid : null}
billId={search.billid}
documentsList={data ? data.bills_by_pk.documents : []}
billsCallback={refetch}
/>
)}
</Form>
</>
<PageHeader
title={data && `${data.bills_by_pk.invoice_number} - ${data.bills_by_pk.vendor.name}`}
extra={
<Space>
<BillDetailEditReturn data={data} />
<BillPrintButton billid={search.billid} />
<Popconfirm
open={open}
onConfirm={() => form.submit()}
onCancel={() => setOpen(false)}
okButtonProps={{ loading: updateLoading }}
title={t("bills.labels.editadjwarning")}
>
<Button
htmlType="submit"
disabled={exported}
onClick={handleSave}
loading={updateLoading}
type="primary"
>
{t("general.actions.save")}
</Button>
</Popconfirm>
<BillReeportButtonComponent bill={data && data.bills_by_pk} />
<BillMarkExportedButton bill={data && data.bills_by_pk} />
</Space>
}
/>
<Form form={form} onFinish={handleFinish} initialValues={transformData(data)} layout="vertical">
<BillFormContainer form={form} billEdit disabled={exported} />
<Divider orientation="left">{t("general.labels.media")}</Divider>
{bodyshop.uselocalmediaserver ? (
<JobsDocumentsLocalGallery
job={{ id: data ? data.bills_by_pk.jobid : null }}
invoice_number={data ? data.bills_by_pk.invoice_number : null}
vendorid={data ? data.bills_by_pk.vendorid : null}
/>
) : (
<JobDocumentsGallery
jobId={data ? data.bills_by_pk.jobid : null}
billId={search.billid}
documentsList={data ? data.bills_by_pk.documents : []}
billsCallback={refetch}
/>
)}
</Form>
</>
);
)}
</>
);
}
const transformData = (data) => {
return data
? {
...data.bills_by_pk,
return data
? {
...data.bills_by_pk,
billlines: data.bills_by_pk.billlines.map((i) => {
return {
...i,
joblineid: !!i.joblineid ? i.joblineid : "noline",
applicable_taxes: {
federal:
(i.applicable_taxes && i.applicable_taxes.federal) || false,
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
local: (i.applicable_taxes && i.applicable_taxes.local) || false,
},
};
}),
date: data.bills_by_pk ? dayjs(data.bills_by_pk.date) : null,
}
: {};
billlines: data.bills_by_pk.billlines.map((i) => {
return {
...i,
joblineid: !!i.joblineid ? i.joblineid : "noline",
applicable_taxes: {
federal: (i.applicable_taxes && i.applicable_taxes.federal) || false,
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
local: (i.applicable_taxes && i.applicable_taxes.local) || false
}
};
}),
date: data.bills_by_pk ? 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 React, {useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {useLocation, useNavigate} from "react-router-dom";
import {createStructuredSelector} from "reselect";
import {insertAuditTrail} from "../../redux/application/application.actions";
import {setModalContext} from "../../redux/modals/modals.actions";
import {selectBodyshop} from "../../redux/user/user.selectors";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({context: context, modal: "partsOrder"})),
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillDetailEditReturn);
export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditReturn);
export function BillDetailEditReturn({
setPartsOrderContext,
insertAuditTrail,
bodyshop,
data,
disabled,
}) {
const search = queryString.parse(useLocation().search);
const history = useNavigate();
const {t} = useTranslation();
const [form] = Form.useForm();
const [open, setOpen] = useState(false);
export function BillDetailEditReturn({ setPartsOrderContext, insertAuditTrail, bodyshop, data, disabled }) {
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 selectedLines = billlines.filter((l) => l.selected).map((l) => l.id);
const handleFinish = ({ billlines }) => {
const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id);
setPartsOrderContext({
actions: {},
context: {
jobId: data.bills_by_pk.jobid,
vendorId: data.bills_by_pk.vendorid,
returnFromBill: data.bills_by_pk.id,
invoiceNumber: data.bills_by_pk.invoice_number,
linesToOrder: data.bills_by_pk.billlines
.filter((l) => selectedLines.includes(l.id))
.map((i) => {
return {
line_desc: i.line_desc,
// db_price: i.actual_price,
act_price: i.actual_price,
cost: i.actual_cost,
quantity: i.quantity,
joblineid: i.joblineid,
oem_partno: i.jobline && i.jobline.oem_partno,
part_type: i.jobline && i.jobline.part_type,
};
}),
isReturn: true,
},
});
delete search.billid;
setPartsOrderContext({
actions: {},
context: {
jobId: data.bills_by_pk.jobid,
job: data.bills_by_pk.job,
vendorId: data.bills_by_pk.vendorid,
returnFromBill: data.bills_by_pk.id,
invoiceNumber: data.bills_by_pk.invoice_number,
linesToOrder: data.bills_by_pk.billlines
.filter((l) => selectedLines.includes(l.id))
.map((i) => {
return {
line_desc: i.line_desc,
// db_price: i.actual_price,
act_price: i.actual_price,
cost: i.actual_cost,
quantity: i.quantity,
joblineid: i.joblineid,
oem_partno: i.jobline && i.jobline.oem_partno,
part_type: i.jobline && i.jobline.part_type
};
}),
isReturn: true
}
});
delete search.billid;
history({search: queryString.stringify(search)});
setOpen(false);
};
useEffect(() => {
if (open === false) form.resetFields();
}, [open, form]);
history({ search: queryString.stringify(search) });
setOpen(false);
};
useEffect(() => {
if (open === false) form.resetFields();
}, [open, form]);
return (
<>
<Modal
open={open}
onCancel={() => setOpen(false)}
destroyOnClose
title={t("bills.actions.return")}
onOk={() => form.submit()}
>
<Form
initialValues={data && data.bills_by_pk}
onFinish={handleFinish}
form={form}
>
<Form.List name={["billlines"]}>
{(fields, {add, remove, move}) => {
return (
<table style={{tableLayout: "auto", width: "100%"}}>
<thead>
<tr>
<td>
<Checkbox
onChange={(e) => {
form.setFieldsValue({
billlines: form
.getFieldsValue()
.billlines.map((b) => ({
...b,
selected: e.target.checked,
})),
});
}}
/>
</td>
<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>
</thead>
<tbody>
{fields.map((field, index) => (
<tr key={field.key}>
<td>
<Form.Item
// label={t("joblines.fields.selected")}
key={`${index}selected`}
name={[field.name, "selected"]}
valuePropName="checked"
>
<Checkbox/>
</Form.Item>
</td>
<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
// label={t("joblines.fields.quantity")}
key={`${index}quantity`}
name={[field.name, "quantity"]}
>
<ReadOnlyFormItemComponent/>
</Form.Item>
</td>
<td>
<Form.Item
// label={t("joblines.fields.actual_price")}
key={`${index}actual_price`}
name={[field.name, "actual_price"]}
>
<ReadOnlyFormItemComponent type="currency"/>
</Form.Item>
</td>
<td>
<Form.Item
// label={t("joblines.fields.actual_cost")}
key={`${index}actual_cost`}
name={[field.name, "actual_cost"]}
>
<ReadOnlyFormItemComponent type="currency"/>
</Form.Item>
</td>
</tr>
))}
</tbody>
</table>
);
}}
</Form.List>
</Form>
</Modal>
<Button
disabled={data.bills_by_pk.is_credit_memo || disabled}
onClick={() => {
setOpen(true);
}}
>
{t("bills.actions.return")}
</Button>
</>
);
return (
<>
<Modal
open={open}
onCancel={() => setOpen(false)}
destroyOnClose
title={t("bills.actions.return")}
onOk={() => form.submit()}
>
<Form initialValues={data && data.bills_by_pk} onFinish={handleFinish} form={form}>
<Form.List name={["billlines"]}>
{(fields, { add, remove, move }) => {
return (
<table style={{ tableLayout: "auto", width: "100%" }}>
<thead>
<tr>
<td>
<Checkbox
onChange={(e) => {
form.setFieldsValue({
billlines: form.getFieldsValue().billlines.map((b) => ({
...b,
selected: e.target.checked
}))
});
}}
/>
</td>
<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>
</thead>
<tbody>
{fields.map((field, index) => (
<tr key={field.key}>
<td>
<Form.Item
// label={t("joblines.fields.selected")}
key={`${index}selected`}
name={[field.name, "selected"]}
valuePropName="checked"
>
<Checkbox />
</Form.Item>
</td>
<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
// label={t("joblines.fields.quantity")}
key={`${index}quantity`}
name={[field.name, "quantity"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
// label={t("joblines.fields.actual_price")}
key={`${index}actual_price`}
name={[field.name, "actual_price"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
<td>
<Form.Item
// label={t("joblines.fields.actual_cost")}
key={`${index}actual_cost`}
name={[field.name, "actual_cost"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
</tr>
))}
</tbody>
</table>
);
}}
</Form.List>
</Form>
</Modal>
<Button
disabled={data.bills_by_pk.is_credit_memo || data.bills_by_pk.isinhouse || disabled}
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 React from "react";
import {useLocation, useNavigate} from "react-router-dom";
import { useLocation, useNavigate } from "react-router-dom";
import BillDetailEditComponent from "./bill-detail-edit-component";
export default function BillDetailEditcontainer() {
const search = queryString.parse(useLocation().search);
const history = useNavigate();
const search = queryString.parse(useLocation().search);
const history = useNavigate();
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const bpoints = {
xs: "100%",
sm: "100%",
md: "100%",
lg: "100%",
xl: "90%",
xxl: "90%",
};
const drawerPercentage = selectedBreakpoint
? bpoints[selectedBreakpoint[0]]
: "100%";
const bpoints = {
xs: "100%",
sm: "100%",
md: "100%",
lg: "100%",
xl: "90%",
xxl: "90%"
};
const drawerPercentage = selectedBreakpoint ? bpoints[selectedBreakpoint[0]] : "100%";
return (
<Drawer
width={drawerPercentage}
onClose={() => {
delete search.billid;
history({search: queryString.stringify(search)});
}}
destroyOnClose
open={search.billid}
>
<BillDetailEditComponent/>
</Drawer>
);
return (
<Drawer
width={drawerPercentage}
onClose={() => {
delete search.billid;
history({ search: queryString.stringify(search) });
}}
destroyOnClose
open={search.billid}
>
<BillDetailEditComponent />
</Drawer>
);
}

View File

@@ -1,467 +1,426 @@
import {useApolloClient, useMutation} from "@apollo/client";
import {useSplitTreatments} from "@splitsoftware/splitio-react";
import {Button, Checkbox, Form, Modal, notification, Space} from "antd";
import { useApolloClient, useMutation } from "@apollo/client";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Button, Checkbox, Form, Modal, notification, Space } from "antd";
import _ from "lodash";
import React, {useEffect, useMemo, useState} from "react";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {INSERT_NEW_BILL} from "../../graphql/bills.queries";
import {UPDATE_INVENTORY_LINES} from "../../graphql/inventory.queries";
import {UPDATE_JOB_LINE} from "../../graphql/jobs-lines.queries";
import {QUERY_JOB_LBR_ADJUSTMENTS, UPDATE_JOB,} from "../../graphql/jobs.queries";
import {MUTATION_MARK_RETURN_RECEIVED} from "../../graphql/parts-orders.queries";
import {insertAuditTrail} from "../../redux/application/application.actions";
import {toggleModalVisible} from "../../redux/modals/modals.actions";
import {selectBillEnterModal} from "../../redux/modals/modals.selectors";
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
import React, { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_NEW_BILL } from "../../graphql/bills.queries";
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
import { QUERY_JOB_LBR_ADJUSTMENTS, UPDATE_JOB } from "../../graphql/jobs.queries";
import { MUTATION_MARK_RETURN_RECEIVED } from "../../graphql/parts-orders.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import {GenerateDocument} from "../../utils/RenderTemplate";
import {TemplateList} from "../../utils/TemplateConstants";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import confirmDialog from "../../utils/asyncConfirm";
import useLocalStorage from "../../utils/useLocalStorage";
import BillFormContainer from "../bill-form/bill-form.container";
import {CalculateBillTotal} from "../bill-form/bill-form.totals.utility";
import {handleUpload as handleLocalUpload} from "../documents-local-upload/documents-local-upload.utility";
import {handleUpload} from "../documents-upload/documents-upload.utility";
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
import { handleUpload } from "../documents-upload/documents-upload.utility";
const mapStateToProps = createStructuredSelector({
billEnterModal: selectBillEnterModal,
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
billEnterModal: selectBillEnterModal,
bodyshop: selectBodyshop,
currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
insertAuditTrail: ({jobid, billid, operation}) =>
dispatch(insertAuditTrail({jobid, billid, operation})),
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
insertAuditTrail: ({ jobid, billid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, billid, operation, type }))
});
const Templates = TemplateList("job_special");
function BillEnterModalContainer({
billEnterModal,
toggleModalVisible,
bodyshop,
currentUser,
insertAuditTrail,
}) {
const [form] = Form.useForm();
const {t} = useTranslation();
const [enterAgain, setEnterAgain] = useState(false);
const [insertBill] = useMutation(INSERT_NEW_BILL);
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
);
function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop, currentUser, insertAuditTrail }) {
const [form] = Form.useForm();
const { t } = useTranslation();
const [enterAgain, setEnterAgain] = useState(false);
const [insertBill] = useMutation(INSERT_NEW_BILL);
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({
attributes: {},
names: ["Enhanced_Payroll"],
splitKey: bodyshop.imexshopid,
const {
treatments: { Enhanced_Payroll }
} = useSplitTreatments({
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(() => {
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;
await Promise.all(
payrollAdjustmentsToInsert.map((li) => {
return updateJobLines({
variables: {
lineId: li.id,
line: {
convertedtolbr: li.convertedtolbr,
convertedtolbr_data: li.convertedtolbr_data
}
}
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(
payrollAdjustmentsToInsert.map((li) => {
return updateJobLines({
variables: {
lineId: li.id,
line: {
convertedtolbr: li.convertedtolbr,
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 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 markPolReceived =
outstanding_returns &&
outstanding_returns.filter((o) => o.cm_received === true);
const newAdjustments = _.cloneDeep(existingAdjustments.data.jobs_by_pk.lbr_adjustments);
if (markPolReceived && markPolReceived.length > 0) {
const r2 = await updatePartsOrderLines({
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();
adjKeys.forEach((key) => {
newAdjustments[key] = (newAdjustments[key] || 0) + adjustmentsToInsert[key];
insertAuditTrail({
jobid: values.jobid,
billid: billId,
operation: AuditTrailMapping.billposted(
r1.data.insert_bills.returning[0].invoice_number
),
jobid: values.jobid,
operation: AuditTrailMapping.jobmodifylbradj({
mod_lbr_ty: key,
hours: adjustmentsToInsert[key].toFixed(1)
}),
type: "jobmodifylbradj"
});
});
if (enterAgain) {
// form.resetFields();
form.setFieldsValue({
...formValues,
billlines: [],
});
form.resetFields();
} else {
toggleModalVisible();
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 = 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);
};
notification["error"]({
message: t("parts_orders.errors.updating", {
message: JSON.stringify(r2.errors)
})
});
}
}
const handleCancel = () => {
const r = window.confirm(t("general.labels.cancel"));
if (r === true) {
toggleModalVisible();
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.
useEffect(() => {
if (enterAgain) form.submit();
}, [enterAgain, form]);
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
}
: {})
}
}
});
})
);
}
useEffect(() => {
if (billEnterModal.open) {
form.setFieldsValue(formValues);
} else {
form.resetFields();
}
}, [billEnterModal.open, form, formValues]);
/////////////////////////
if (upload && upload.length > 0) {
//insert Each of the documents?
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);
}}
>
{t("general.actions.saveandnew")}
</Button>
)}
</Space>
if (bodyshop.uselocalmediaserver) {
upload.forEach((u) => {
handleLocalUpload({
ev: { file: u.originFileObj },
context: {
jobid: values.jobid,
invoice_number: remainingValues.invoice_number,
vendorid: remainingValues.vendorid
}
destroyOnClose
>
<Form
onFinish={handleFinish}
autoComplete={"off"}
layout="vertical"
form={form}
onFinishFailed={() => {
setEnterAgain(false);
}}
});
});
} 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({
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
form={form}
disableInvNumber={billEnterModal.context.disableInvNumber}
/>
</Form>
</Modal>
);
{t("general.actions.saveandnew")}
</Button>
)}
</Space>
}
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(
mapStateToProps,
mapDispatchToProps
)(BillEnterModalContainer);
export default connect(mapStateToProps, mapDispatchToProps)(BillEnterModalContainer);

View File

@@ -1,136 +1,114 @@
import {Form, Input, Table} from "antd";
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import { Form, Input, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import {alphaSort} from "../../utils/sorters";
import { alphaSort } from "../../utils/sorters";
import BillFormItemsExtendedFormItem from "./bill-form-lines.extended.formitem.component";
export default function BillFormLinesExtended({
lineData,
discount,
form,
responsibilityCenters,
disabled,
}) {
const [search, setSearch] = useState("");
const {t} = useTranslation();
const columns = [
export default function BillFormLinesExtended({ lineData, discount, form, responsibilityCenters, disabled }) {
const [search, setSearch] = useState("");
const { t } = useTranslation();
const columns = [
{
title: t("joblines.fields.line_desc"),
dataIndex: "line_desc",
key: "line_desc",
width: "10%",
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"),
dataIndex: "line_desc",
key: "line_desc",
width: "10%",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
text: t("jobs.labels.partsfilter"),
value: ["PAN", "PAP", "PAL", "PAA", "PAS", "PASL"]
},
{
title: t("joblines.fields.oem_partno"),
dataIndex: "oem_partno",
key: "oem_partno",
width: "10%",
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
text: t("joblines.fields.part_types.PAN"),
value: ["PAN", "PAP"]
},
{
title: t("joblines.fields.part_type"),
dataIndex: "part_type",
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.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)
},
{
title: t("joblines.fields.act_price"),
dataIndex: "act_price",
key: "act_price",
width: "10%",
sorter: (a, b) => a.act_price - b.act_price,
shouldCellUpdate: false,
render: (text, record) => (
<>
<CurrencyFormatter>
{record.db_ref === "900510" || record.db_ref === "900511"
? record.prt_dsmk_m
: record.act_price}
</CurrencyFormatter>
{record.part_qty ? `(x ${record.part_qty})` : null}
{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("joblines.fields.act_price"),
dataIndex: "act_price",
key: "act_price",
width: "10%",
sorter: (a, b) => a.act_price - b.act_price,
shouldCellUpdate: false,
render: (text, record) => (
<>
<CurrencyFormatter>
{record.db_ref === "900510" || record.db_ref === "900511" ? record.prt_dsmk_m : record.act_price}
</CurrencyFormatter>
{record.part_qty ? `(x ${record.part_qty})` : null}
{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",
render: (text, record, index) => (
<Form.Item noStyle name={["billlineskeys", record.id]}>
<BillFormItemsExtendedFormItem
form={form}
record={record}
index={index}
responsibilityCenters={responsibilityCenters}
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}
/>
render: (text, record, index) => (
<Form.Item noStyle name={["billlineskeys", record.id]}>
<BillFormItemsExtendedFormItem
form={form}
record={record}
index={index}
responsibilityCenters={responsibilityCenters}
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>
);
}

View File

@@ -1,284 +1,216 @@
import React from "react";
import {MinusCircleFilled, PlusCircleFilled, WarningOutlined,} from "@ant-design/icons";
import {Button, Form, Input, InputNumber, Select, Space, Switch} from "antd";
import {useTranslation} from "react-i18next";
import { MinusCircleFilled, PlusCircleFilled, WarningOutlined } from "@ant-design/icons";
import { Button, Form, Input, InputNumber, Select, Space, Switch } from "antd";
import { useTranslation } from "react-i18next";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {selectBodyshop} from "../../redux/user/user.selectors";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import CiecaSelect from "../../utils/Ciecaselect";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillFormItemsExtendedFormItem);
export default connect(mapStateToProps, mapDispatchToProps)(BillFormItemsExtendedFormItem);
export function BillFormItemsExtendedFormItem({
value,
bodyshop,
form,
record,
index,
disabled,
responsibilityCenters,
discount,
}) {
// 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>
);
value,
bodyshop,
form,
record,
index,
disabled,
responsibilityCenters,
discount
}) {
// const { billlineskeys } = form.getFieldsValue("billlineskeys");
const { t } = useTranslation();
if (!value)
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);
<Button
onClick={() => {
const values = form.getFieldsValue("billlineskeys");
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>
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>
);
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 {useApolloClient} from "@apollo/client";
import {useSplitTreatments} from "@splitsoftware/splitio-react";
import {Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload,} from "antd";
import Icon, { UploadOutlined } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload } from "antd";
import 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 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 InstanceRenderManager from "../../utils/instanceRenderMgr";
import AlertComponent from "../alert/alert.component";
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
@@ -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 VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import BillFormLines from "./bill-form.lines.component";
import {CalculateBillTotal} from "./bill-form.totals.utility";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import { CalculateBillTotal } from "./bill-form.totals.utility";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({});
export function BillFormComponent({
bodyshop,
disabled,
form,
vendorAutoCompleteOptions,
lineData,
responsibilityCenters,
loadLines,
billEdit,
disableInvNumber,
job,
loadOutstandingReturns,
loadInventory,
preferredMake
}) {
bodyshop,
disabled,
form,
vendorAutoCompleteOptions,
lineData,
responsibilityCenters,
loadLines,
billEdit,
disableInvNumber,
job,
loadOutstandingReturns,
loadInventory,
preferredMake
}) {
const { t } = useTranslation();
const client = useApolloClient();
const [discount, setDiscount] = useState(0);
const {t} = useTranslation();
const client = useApolloClient();
const [discount, setDiscount] = useState(0);
const {
treatments: { Extended_Bill_Posting, ClosingPeriod }
} = useSplitTreatments({
attributes: {},
names: ["Extended_Bill_Posting", "ClosingPeriod"],
splitKey: bodyshop.imexshopid
});
const {treatments: {Extended_Bill_Posting, ClosingPeriod}} = useSplitTreatments({
attributes: {},
names: ["Extended_Bill_Posting", "ClosingPeriod"],
splitKey: bodyshop.imexshopid,
});
const handleVendorSelect = (props, opt) => {
setDiscount(opt.discount);
const handleVendorSelect = (props, opt) => {
setDiscount(opt.discount);
opt &&
!billEdit &&
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: opt.value,
},
});
};
const handleFederalTaxExemptSwitchToggle = (checked) => {
// Early gate
if (!checked) return;
const values = form.getFieldsValue("billlines");
// Gate bill lines
if (!values?.billlines?.length) return;
const billlines = values.billlines.map((b) => {
b.applicable_taxes.federal = false;
return b;
opt &&
!billEdit &&
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: opt.value
}
});
form.setFieldsValue({ billlines });
};
};
useEffect(() => {
if (job) form.validateFields(["is_credit_memo"]);
}, [job, form]);
const handleFederalTaxExemptSwitchToggle = (checked) => {
// Early gate
if (!checked) return;
const values = form.getFieldsValue("billlines");
// Gate bill lines
if (!values?.billlines?.length) return;
useEffect(() => {
const vendorId = form.getFieldValue("vendorid");
if (vendorId && vendorAutoCompleteOptions) {
const matchingVendors = vendorAutoCompleteOptions.filter(
(v) => v.id === vendorId
);
if (matchingVendors.length === 1) {
setDiscount(matchingVendors[0].discount);
}
}
const jobId = form.getFieldValue("jobid");
if (jobId) {
loadLines({variables: {id: jobId}});
if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) {
loadOutstandingReturns({
variables: {
jobId: jobId,
vendorId: vendorId,
},
});
}
}
const billlines = values.billlines.map((b) => {
b.applicable_taxes.federal = false;
return b;
});
form.setFieldsValue({ billlines });
};
if (vendorId === bodyshop.inhousevendorid && !billEdit) {
loadInventory();
}
}, [
form,
billEdit,
loadOutstandingReturns,
loadInventory,
setDiscount,
vendorAutoCompleteOptions,
loadLines,
bodyshop.inhousevendorid,
]);
useEffect(() => {
if (job) form.validateFields(["is_credit_memo"]);
}, [job, form]);
return (
<div>
<FormFieldsChanged form={form}/>
<Form.Item
style={{display: "none"}}
name="isinhouse"
valuePropName="checked"
>
<Switch/>
</Form.Item>
<LayoutFormRow grow>
<Form.Item
name="jobid"
label={t("bills.fields.ro_number")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<JobSearchSelect
disabled={billEdit || disabled}
convertedOnly
notExported={false}
onBlur={() => {
if (form.getFieldValue("jobid") !== null) {
loadLines({variables: {id: form.getFieldValue("jobid")}});
if (form.getFieldValue("vendorid") !== null) {
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: form.getFieldValue("vendorid"),
},
});
}
}
}}
/>
</Form.Item>
<Form.Item
label={t("bills.fields.vendor")}
name="vendorid"
// style={{ display: billEdit ? "none" : null }}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({getFieldValue}) => ({
validator(rule, value) {
if (
value &&
!getFieldValue(["isinhouse"]) &&
value === bodyshop.inhousevendorid
) {
return Promise.reject(t("bills.validation.manualinhouse"));
}
return Promise.resolve();
},
}),
]}
>
<VendorSearchSelect
disabled={disabled}
options={vendorAutoCompleteOptions}
preferredMake={preferredMake}
onSelect={handleVendorSelect}
/>
</Form.Item>
</LayoutFormRow>
{job &&
job.ious &&
job.ious.length > 0 &&
job.ious.map((iou) => (
<Alert
key={iou.id}
type="warning"
message={
<Space>
{t("bills.labels.iouexists")}
<Link
target="_blank"
rel="noopener noreferrer"
to={`/manage/jobs/${iou.id}?tab=repairdata`}
>
<Space>
{iou.ro_number}
<Icon component={MdOpenInNew}/>
</Space>
</Link>
</Space>
}
/>
))}
<LayoutFormRow>
<Form.Item
label={t("bills.fields.invoice_number")}
name="invoice_number"
validateTrigger="onBlur"
hasFeedback
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({getFieldValue}) => ({
async validator(rule, value) {
const vendorid = getFieldValue("vendorid");
if (vendorid && value) {
const response = await client.query({
query: CHECK_BILL_INVOICE_NUMBER,
variables: {
invoice_number: value,
vendorid: vendorid,
},
});
useEffect(() => {
const vendorId = form.getFieldValue("vendorid");
if (vendorId && vendorAutoCompleteOptions) {
const matchingVendors = vendorAutoCompleteOptions.filter((v) => v.id === vendorId);
if (matchingVendors.length === 1) {
setDiscount(matchingVendors[0].discount);
}
}
const jobId = form.getFieldValue("jobid");
if (jobId) {
loadLines({ variables: { id: jobId } });
if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) {
loadOutstandingReturns({
variables: {
jobId: jobId,
vendorId: vendorId
}
});
}
}
if (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 (vendorId === bodyshop.inhousevendorid && !billEdit) {
loadInventory();
}
}, [
form,
billEdit,
loadOutstandingReturns,
loadInventory,
setDiscount,
vendorAutoCompleteOptions,
loadLines,
bodyshop.inhousevendorid
]);
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
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} />
return (
<div>
<FormFieldsChanged form={form} />
<Form.Item style={{ display: "none" }} name="isinhouse" valuePropName="checked">
<Switch />
</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 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;
<LayoutFormRow grow>
<Form.Item
name="jobid"
label={t("bills.fields.ro_number")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<JobSearchSelect
disabled={billEdit || disabled}
convertedOnly
notExported={false}
onBlur={() => {
if (form.getFieldValue("jobid") !== null && form.getFieldValue("jobid") !== undefined) {
loadLines({ variables: { id: form.getFieldValue("jobid") } });
if (form.getFieldValue("vendorid") !== null && form.getFieldValue("vendorid") !== undefined) {
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: form.getFieldValue("vendorid")
}
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>
<Form.Item
label={t("bills.fields.vendor")}
name="vendorid"
// style={{ display: billEdit ? "none" : null }}
rules={[
{
required: true
//message: t("general.validation.required"),
},
({ getFieldValue }) => ({
validator(rule, value) {
if (value && !getFieldValue(["isinhouse"]) && value === bodyshop.inhousevendorid) {
return Promise.reject(t("bills.validation.manualinhouse"));
}
return Promise.resolve();
}
})
]}
>
<VendorSearchSelect
disabled={disabled}
options={vendorAutoCompleteOptions}
preferredMake={preferredMake}
onSelect={handleVendorSelect}
/>
</Form.Item>
</LayoutFormRow>
{job &&
job.ious &&
job.ious.length > 0 &&
job.ious.map((iou) => (
<Alert
key={iou.id}
type="warning"
message={
<Space>
{t("bills.labels.iouexists")}
<Link target="_blank" rel="noopener noreferrer" to={`/manage/jobs/${iou.id}?tab=repairdata`}>
<Space>
{iou.ro_number}
<Icon component={MdOpenInNew} />
</Space>
</Link>
</Space>
}
/>
))}
<LayoutFormRow>
<Form.Item
label={t("bills.fields.invoice_number")}
name="invoice_number"
validateTrigger="onBlur"
hasFeedback
rules={[
{
required: true
//message: t("general.validation.required"),
},
({ getFieldValue }) => ({
async validator(rule, value) {
const vendorid = getFieldValue("vendorid");
if (vendorid && value) {
const response = await client.query({
query: CHECK_BILL_INVOICE_NUMBER,
variables: {
invoice_number: value,
vendorid: vendorid
}
});
if (response.data.bills_aggregate.aggregate.count === 0) {
return Promise.resolve();
} else if (
response.data.bills_aggregate.nodes.length === 1 &&
response.data.bills_aggregate.nodes[0].id === form.getFieldValue("id")
) {
return Promise.resolve();
}
return Promise.reject(t("bills.validation.unique_invoice_number"));
} else {
return Promise.resolve();
}
}
})
]}
>
<Input disabled={disabled || disableInvNumber} />
</Form.Item>
<Form.Item
label={t("bills.fields.date")}
name="date"
rules={[
{
required: true
//message: t("general.validation.required"),
},
({ getFieldValue }) => ({
validator(rule, value) {
if (ClosingPeriod.treatment === "on" && bodyshop.accountingconfig.ClosingPeriod) {
if (
dayjs(value)
.startOf("day")
.isSameOrAfter(dayjs(bodyshop.accountingconfig.ClosingPeriod[0]).startOf("day")) &&
dayjs(value)
.startOf("day")
.isSameOrBefore(dayjs(bodyshop.accountingconfig.ClosingPeriod[1]).endOf("day"))
) {
return Promise.resolve();
} else {
return Promise.reject(t("bills.validation.closingperiod"));
}
} else {
return Promise.resolve();
}
}
})
]}
>
<FormDatePicker disabled={disabled} />
</Form.Item>
<Form.Item
label={t("bills.fields.is_credit_memo")}
name="is_credit_memo"
valuePropName="checked"
rules={[
({ getFieldValue }) => ({
validator(rule, value) {
if (value === true && getFieldValue("jobid") && getFieldValue("vendorid")) {
//Removed as this would cause an additional reload when validating the form on submit and clear the values.
// loadOutstandingReturns({
// variables: {
// jobId: form.getFieldValue("jobid"),
// vendorId: form.getFieldValue("vendorid"),
// },
// });
}
if (
!bodyshop.bill_allow_post_to_closed &&
job &&
(job.status === bodyshop.md_ro_statuses.default_invoiced ||
job.status === bodyshop.md_ro_statuses.default_exported ||
job.status === bodyshop.md_ro_statuses.default_void) &&
(value === false || !value)
) {
return Promise.reject(t("bills.labels.onlycmforinvoiced"));
}
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>
</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);

View File

@@ -1,83 +1,67 @@
import {useLazyQuery, useQuery} from "@apollo/client";
import {useSplitTreatments} from "@splitsoftware/splitio-react";
import { useLazyQuery, useQuery } from "@apollo/client";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import React from "react";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {QUERY_OUTSTANDING_INVENTORY} from "../../graphql/inventory.queries";
import {GET_JOB_LINES_TO_ENTER_BILL} from "../../graphql/jobs-lines.queries";
import {QUERY_UNRECEIVED_LINES} from "../../graphql/parts-orders.queries";
import {SEARCH_VENDOR_AUTOCOMPLETE} from "../../graphql/vendors.queries";
import {selectBodyshop} from "../../redux/user/user.selectors";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries";
import { GET_JOB_LINES_TO_ENTER_BILL } from "../../graphql/jobs-lines.queries";
import { QUERY_UNRECEIVED_LINES } from "../../graphql/parts-orders.queries";
import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import BillCmdReturnsTableComponent from "../bill-cm-returns-table/bill-cm-returns-table.component";
import BillInventoryTable from "../bill-inventory-table/bill-inventory-table.component";
import BillFormComponent from "./bill-form.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop
});
export function BillFormContainer({
bodyshop,
form,
billEdit,
disabled,
disableInvNumber,
}) {
const {treatments: {Simple_Inventory}} = useSplitTreatments({
attributes: {},
names: ["Simple_Inventory"],
splitKey: bodyshop && bodyshop.imexshopid,
});
export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber }) {
const {
treatments: { Simple_Inventory }
} = useSplitTreatments({
attributes: {},
names: ["Simple_Inventory"],
splitKey: bodyshop && bodyshop.imexshopid
});
const {data: VendorAutoCompleteData} = useQuery(
SEARCH_VENDOR_AUTOCOMPLETE,
{fetchPolicy: "network-only", nextFetchPolicy: "network-only"}
);
const { data: VendorAutoCompleteData } = useQuery(SEARCH_VENDOR_AUTOCOMPLETE, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only"
});
const [loadLines, {data: lineData}] = useLazyQuery(
GET_JOB_LINES_TO_ENTER_BILL
);
const [loadLines, { data: lineData }] = useLazyQuery(GET_JOB_LINES_TO_ENTER_BILL);
const [loadOutstandingReturns, {loading: returnLoading, data: returnData}] =
useLazyQuery(QUERY_UNRECEIVED_LINES);
const [loadInventory, {loading: inventoryLoading, data: inventoryData}] =
useLazyQuery(QUERY_OUTSTANDING_INVENTORY);
const [loadOutstandingReturns, { loading: returnLoading, data: returnData }] = useLazyQuery(QUERY_UNRECEIVED_LINES);
const [loadInventory, { loading: inventoryLoading, data: inventoryData }] = useLazyQuery(QUERY_OUTSTANDING_INVENTORY);
return (
<>
<BillFormComponent
disabled={disabled}
form={form}
billEdit={billEdit}
vendorAutoCompleteOptions={
VendorAutoCompleteData && VendorAutoCompleteData.vendors
}
loadLines={loadLines}
lineData={lineData ? lineData.joblines : []}
job={lineData ? lineData.jobs_by_pk : null}
responsibilityCenters={bodyshop.md_responsibility_centers || null}
disableInvNumber={disableInvNumber}
loadOutstandingReturns={loadOutstandingReturns}
loadInventory={loadInventory}
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}
/>
{!billEdit && (
<BillCmdReturnsTableComponent
form={form}
returnLoading={returnLoading}
returnData={returnData}
/>
)}
{Simple_Inventory.treatment === "on" && (
<BillInventoryTable
form={form}
inventoryLoading={inventoryLoading}
inventoryData={billEdit ? [] : inventoryData}
billEdit={billEdit}
/>
)}
</>
);
return (
<>
<BillFormComponent
disabled={disabled}
form={form}
billEdit={billEdit}
vendorAutoCompleteOptions={VendorAutoCompleteData && VendorAutoCompleteData.vendors}
loadLines={loadLines}
lineData={lineData ? lineData.joblines : []}
job={lineData ? lineData.jobs_by_pk : null}
responsibilityCenters={bodyshop.md_responsibility_centers || null}
disableInvNumber={disableInvNumber}
loadOutstandingReturns={loadOutstandingReturns}
loadInventory={loadInventory}
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}
/>
{!billEdit && <BillCmdReturnsTableComponent form={form} returnLoading={returnLoading} returnData={returnData} />}
{Simple_Inventory.treatment === "on" && (
<BillInventoryTable
form={form}
inventoryLoading={inventoryLoading}
inventoryData={billEdit ? [] : inventoryData}
billEdit={billEdit}
/>
)}
</>
);
}
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";
export const CalculateBillTotal = (invoice) => {
const {total, billlines, federal_tax_rate, local_tax_rate, state_tax_rate} =
invoice;
const { total, billlines, federal_tax_rate, local_tax_rate, state_tax_rate } = invoice;
//TODO Determine why this recalculates so many times.
let subtotal = Dinero({amount: 0});
let federalTax = Dinero({amount: 0});
let stateTax = Dinero({amount: 0});
let localTax = Dinero({amount: 0});
//TODO Determine why this recalculates so many times.
let subtotal = Dinero({ amount: 0 });
let federalTax = Dinero({ amount: 0 });
let stateTax = Dinero({ amount: 0 });
let localTax = Dinero({ amount: 0 });
if (!!!billlines) return null;
if (!!!billlines) return null;
billlines.forEach((i) => {
if (!!i) {
const itemTotal = Dinero({
amount: Math.round((i.actual_cost || 0) * 100),
}).multiply(i.quantity || 1);
billlines.forEach((i) => {
if (!!i) {
const itemTotal = Dinero({
amount: Math.round((i.actual_cost || 0) * 100)
}).multiply(i.quantity || 1);
subtotal = subtotal.add(itemTotal);
if (i.applicable_taxes?.federal) {
federalTax = federalTax.add(
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));
}
});
subtotal = subtotal.add(itemTotal);
if (i.applicable_taxes?.federal) {
federalTax = federalTax.add(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));
}
});
const invoiceTotal = Dinero({amount: Math.round((total || 0) * 100)});
const enteredTotal = subtotal.add(federalTax).add(stateTax).add(localTax);
const discrepancy = enteredTotal.subtract(invoiceTotal);
const invoiceTotal = Dinero({ amount: Math.round((total || 0) * 100) });
const enteredTotal = subtotal.add(federalTax).add(stateTax).add(localTax);
const discrepancy = enteredTotal.subtract(invoiceTotal);
return {
subtotal,
federalTax,
stateTax,
localTax,
enteredTotal,
invoiceTotal,
discrepancy,
};
return {
subtotal,
federalTax,
stateTax,
localTax,
enteredTotal,
invoiceTotal,
discrepancy
};
};

View File

@@ -1,173 +1,153 @@
import {Checkbox, Form, Skeleton, Typography} from "antd";
import React, {useEffect} from "react";
import {useTranslation} from "react-i18next";
import { Checkbox, Form, Skeleton, Typography } from "antd";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
import "./bill-inventory-table.styles.scss";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {selectBodyshop} from "../../redux/user/user.selectors";
import {selectBillEnterModal} from "../../redux/modals/modals.selectors";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
billEnterModal: selectBillEnterModal,
bodyshop: selectBodyshop,
billEnterModal: selectBillEnterModal
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(BillInventoryTable);
export function BillInventoryTable({
billEnterModal,
bodyshop,
form,
billEdit,
inventoryLoading,
inventoryData,
}) {
const {t} = useTranslation();
export function BillInventoryTable({ billEnterModal, bodyshop, form, billEdit, inventoryLoading, inventoryData }) {
const { t } = useTranslation();
useEffect(() => {
if (inventoryData && inventoryData.inventory) {
form.setFieldsValue({
inventory: billEnterModal.context.consumeinventoryid
? inventoryData.inventory.map((i) => {
if (i.id === billEnterModal.context.consumeinventoryid)
i.consumefrominventory = true;
return i;
})
: inventoryData.inventory,
});
useEffect(() => {
if (inventoryData && inventoryData.inventory) {
form.setFieldsValue({
inventory: billEnterModal.context.consumeinventoryid
? inventoryData.inventory.map((i) => {
if (i.id === billEnterModal.context.consumeinventoryid) i.consumefrominventory = true;
return i;
})
: 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 (
<Form.Item
shouldUpdate={(prev, cur) => prev.vendorid !== cur.vendorid}
noStyle
>
{() => {
const is_inhouse =
form.getFieldValue("vendorid") === bodyshop.inhousevendorid;
if (inventoryLoading) return <Skeleton />;
if (!is_inhouse || billEdit) {
return null;
}
return (
<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 (
<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>
<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>
);
<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.Item>
);
</Form.List>
);
}}
</Form.Item>
);
}

View File

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

View File

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

View File

@@ -1,97 +1,89 @@
import {gql, useMutation} from "@apollo/client";
import {Button, notification} from "antd";
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import { gql, useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {selectAuthLevel, selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
import {HasRbacAccess} from "../rbac-wrapper/rbac-wrapper.component";
import {INSERT_EXPORT_LOG} from "../../graphql/accounting.queries";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectAuthLevel, selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
authLevel: selectAuthLevel,
currentUser: selectCurrentUser,
bodyshop: selectBodyshop,
authLevel: selectAuthLevel,
currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillMarkExportedButton);
export default connect(mapStateToProps, mapDispatchToProps)(BillMarkExportedButton);
export function BillMarkExportedButton({
currentUser,
bodyshop,
authLevel,
bill,
}) {
const {t} = useTranslation();
const [loading, setLoading] = useState(false);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
export function BillMarkExportedButton({ currentUser, bodyshop, authLevel, bill }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
const [updateBill] = useMutation(gql`
mutation UPDATE_BILL($billId: uuid!) {
update_bills(where: { id: { _eq: $billId } }, _set: { exported: true }) {
returning {
id
exported
exported_at
}
}
const [updateBill] = useMutation(gql`
mutation UPDATE_BILL($billId: uuid!) {
update_bills(where: { id: { _eq: $billId } }, _set: { exported: true }) {
returning {
id
exported
exported_at
}
`);
}
}
`);
const handleUpdate = async () => {
setLoading(true);
const result = await updateBill({
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",
const handleUpdate = async () => {
setLoading(true);
const result = await updateBill({
variables: { billId: bill.id }
});
if (hasAccess)
return (
<Button loading={loading} disabled={bill.exported} onClick={handleUpdate}>
{t("bills.labels.markexported")}
</Button>
);
await insertExportLog({
variables: {
logs: [
{
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 React, {useState} from "react";
import {useTranslation} from "react-i18next";
import {GenerateDocument} from "../../utils/RenderTemplate";
import {TemplateList} from "../../utils/TemplateConstants";
import { Button, Space } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
export default function BillPrintButton({billid}) {
const {t} = useTranslation();
const [loading, setLoading] = useState(false);
const Templates = TemplateList("job_special");
export default function BillPrintButton({ billid }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const Templates = TemplateList("job_special");
const submitHandler = async () => {
setLoading(true);
try {
await GenerateDocument(
{
name: Templates.parts_invoice_label_single.key,
variables: {
id: billid,
},
},
{},
"p"
);
} catch (e) {
console.warn("Warning: Error generating a document.");
}
setLoading(false);
};
const submitHandler = async () => {
setLoading(true);
try {
await GenerateDocument(
{
name: Templates.parts_invoice_label_single.key,
variables: {
id: billid
}
},
{},
"p"
);
} catch (e) {
console.warn("Warning: Error generating a document.");
}
setLoading(false);
};
return (
<Space wrap>
<Button loading={loading} onClick={submitHandler}>
{t("bills.labels.printlabels")}
</Button>
</Space>
);
return (
<Space wrap>
<Button loading={loading} onClick={submitHandler}>
{t("bills.labels.printlabels")}
</Button>
</Space>
);
}

View File

@@ -1,79 +1,72 @@
import {gql, useMutation} from "@apollo/client";
import {Button, notification} from "antd";
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import { gql, useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {selectAuthLevel, selectBodyshop,} from "../../redux/user/user.selectors";
import {HasRbacAccess} from "../rbac-wrapper/rbac-wrapper.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectAuthLevel, selectBodyshop } from "../../redux/user/user.selectors";
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
authLevel: selectAuthLevel,
bodyshop: selectBodyshop,
authLevel: selectAuthLevel
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillMarkForReexportButton);
export default connect(mapStateToProps, mapDispatchToProps)(BillMarkForReexportButton);
export function BillMarkForReexportButton({bodyshop, authLevel, bill}) {
const {t} = useTranslation();
const [loading, setLoading] = useState(false);
export function BillMarkForReexportButton({ bodyshop, authLevel, bill }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [updateBill] = useMutation(gql`
mutation UPDATE_BILL($billId: uuid!) {
update_bills(where: { id: { _eq: $billId } }, _set: { exported: false }) {
returning {
id
exported
exported_at
}
}
const [updateBill] = useMutation(gql`
mutation UPDATE_BILL($billId: uuid!) {
update_bills(where: { id: { _eq: $billId } }, _set: { exported: false }) {
returning {
id
exported
exported_at
}
`);
}
}
`);
const handleUpdate = async () => {
setLoading(true);
const result = await updateBill({
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",
const handleUpdate = async () => {
setLoading(true);
const result = await updateBill({
variables: { billId: bill.id }
});
if (hasAccess)
return (
<Button
loading={loading}
disabled={!bill.exported}
onClick={handleUpdate}
>
{t("bills.labels.markforreexport")}
</Button>
);
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.
};
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 {useMutation} from "@apollo/client";
import {Button, notification, Tooltip} from "antd";
import {t} from "i18next";
import { FileAddFilled } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, notification, Tooltip } from "antd";
import { t } from "i18next";
import dayjs from "./../../utils/day";
import React, {useState} from "react";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {INSERT_INVENTORY_AND_CREDIT} from "../../graphql/inventory.queries";
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
import {CalculateBillTotal} from "../bill-form/bill-form.totals.utility";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_INVENTORY_AND_CREDIT } from "../../graphql/inventory.queries";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
import queryString from "query-string";
import {useLocation} from "react-router-dom";
import { useLocation } from "react-router-dom";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
bodyshop: selectBodyshop,
currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BilllineAddInventory);
export default connect(mapStateToProps, mapDispatchToProps)(BilllineAddInventory);
export function BilllineAddInventory({
currentUser,
bodyshop,
billline,
disabled,
jobid,
}) {
const [loading, setLoading] = useState(false);
const {billid} = queryString.parse(useLocation().search);
const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT);
export function BilllineAddInventory({ currentUser, bodyshop, billline, disabled, jobid }) {
const [loading, setLoading] = useState(false);
const { billid } = queryString.parse(useLocation().search);
const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT);
const addToInventory = async () => {
setLoading(true);
const addToInventory = async () => {
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 = {
vendorid: bodyshop.inhousevendorid,
invoice_number: "ih",
jobid: jobid,
isinhouse: true,
is_credit_memo: true,
date: dayjs().format("YYYY-MM-DD"),
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate,
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate,
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate,
total: 0,
billlines: [
{
actual_price: billline.actual_price,
actual_cost: billline.actual_cost,
quantity: billline.quantity,
line_desc: billline.line_desc,
cost_center: billline.cost_center,
deductedfromlbr: billline.deductedfromlbr,
applicable_taxes: {
local: billline.applicable_taxes.local,
state: billline.applicable_taxes.state,
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),
}),
});
const cm = {
vendorid: bodyshop.inhousevendorid,
invoice_number: "ih",
jobid: jobid,
isinhouse: true,
is_credit_memo: true,
date: dayjs().format("YYYY-MM-DD"),
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate,
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate,
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate,
total: 0,
billlines: [
{
actual_price: billline.actual_price,
actual_cost: billline.actual_cost,
quantity: billline.quantity,
line_desc: billline.line_desc,
cost_center: billline.cost_center,
deductedfromlbr: billline.deductedfromlbr,
applicable_taxes: {
local: billline.applicable_taxes.local,
state: billline.applicable_taxes.state,
federal: billline.applicable_taxes.federal
}
}
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>
);
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 (
<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 {Button, Card, Checkbox, Input, Space, Table} from "antd";
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {selectJobReadOnly} from "../../redux/application/application.selectors";
import {setModalContext} from "../../redux/modals/modals.actions";
import {selectBodyshop} from "../../redux/user/user.selectors";
import { EditFilled, SyncOutlined } from "@ant-design/icons";
import { Button, Card, Checkbox, Input, Space, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import {DateFormatter} from "../../utils/DateFormatter";
import {alphaSort, dateSort} from "../../utils/sorters";
import {TemplateList} from "../../utils/TemplateConstants";
import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort, dateSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants";
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
import { FaTasks } from "react-icons/fa";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
bodyshop: selectBodyshop,
jobRO: selectJobReadOnly,
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({context: context, modal: "partsOrder"})),
setBillEnterContext: (context) =>
dispatch(setModalContext({context: context, modal: "billEnter"})),
setReconciliationContext: (context) =>
dispatch(setModalContext({context: context, modal: "reconciliation"})),
setBillEnterContext: (context) =>
dispatch(
setModalContext({
context: context,
modal: "billEnter"
})
),
setReconciliationContext: (context) =>
dispatch(
setModalContext({
context: context,
modal: "reconciliation"
})
),
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
});
export function BillsListTableComponent({
bodyshop,
jobRO,
job,
billsQuery,
handleOnRowClick,
setPartsOrderContext,
setBillEnterContext,
setReconciliationContext,
}) {
const {t} = useTranslation();
bodyshop,
jobRO,
job,
billsQuery,
handleOnRowClick,
setBillEnterContext,
setReconciliationContext,
setTaskUpsertContext
}) {
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
});
// const search = queryString.parse(useLocation().search);
// const selectedBill = search.billid;
const [searchText, setSearchText] = useState("");
const [state, setState] = useState({
sortedInfo: {}
});
// const search = queryString.parse(useLocation().search);
// const selectedBill = search.billid;
const [searchText, setSearchText] = useState("");
const Templates = TemplateList("bill");
const bills = billsQuery.data ? billsQuery.data.bills : [];
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
}
/>
const Templates = TemplateList("bill");
const bills = billsQuery.data ? billsQuery.data.bills : [];
const { refetch } = billsQuery;
{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,
},
});
}}
>
{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>
const recordActions = (record, showView = false) => (
<Space wrap>
{showView && (
<Button onClick={() => handleOnRowClick(record)}>
<EditFilled />
</Button>
)}
<Button
title={t("tasks.buttons.create")}
onClick={() => {
setTaskUpsertContext({
context: {
jobid: job.id,
billid: record.id
}
>
<Table
loading={billsQuery.loading}
scroll={{
x: true, // y: "50rem"
});
}}
>
<FaTasks />
</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"
dataSource={filteredBills}
onChange={handleTableChange}
/>
</Card>
);
>
{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={{
x: true // y: "50rem"
}}
columns={columns}
rowKey="id"
dataSource={filteredBills}
onChange={handleTableChange}
/>
</Card>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillsListTableComponent);
export default connect(mapStateToProps, mapDispatchToProps)(BillsListTableComponent);

View File

@@ -1,121 +1,112 @@
import React, {useState} from "react";
import {QUERY_ALL_VENDORS} from "../../graphql/vendors.queries";
import {useQuery} from "@apollo/client";
import React, { useState } from "react";
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
import { useQuery } from "@apollo/client";
import queryString from "query-string";
import {useLocation, useNavigate} from "react-router-dom";
import {Input, Table} from "antd";
import {useTranslation} from "react-i18next";
import {alphaSort} from "../../utils/sorters";
import { useLocation, useNavigate } from "react-router-dom";
import { Input, Table } from "antd";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
import AlertComponent from "../alert/alert.component";
export default function BillsVendorsList() {
const search = queryString.parse(useLocation().search);
const history = useNavigate();
const search = queryString.parse(useLocation().search);
const history = useNavigate();
const {loading, error, data} = useQuery(QUERY_ALL_VENDORS, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const { loading, error, data } = useQuery(QUERY_ALL_VENDORS, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only"
});
const {t} = useTranslation();
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
search: "",
});
const [state, setState] = useState({
sortedInfo: {},
search: ""
});
const handleTableChange = (pagination, filters, sorter) => {
setState({...state, filteredInfo: filters, sortedInfo: sorter});
};
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const columns = [
{
title: t("vendors.fields.name"),
dataIndex: "name",
key: "name",
sorter: (a, b) => alphaSort(a.name, b.name),
sortOrder:
state.sortedInfo.columnKey === "name" && state.sortedInfo.order,
const columns = [
{
title: t("vendors.fields.name"),
dataIndex: "name",
key: "name",
sorter: (a, b) => alphaSort(a.name, b.name),
sortOrder: 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);
},
{
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],
type: "radio",
}}
onRow={(record, rowIndex) => {
return {
onClick: (event) => {
handleOnRowClick(record);
}, // click row
};
}}
/>
);
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 {Breadcrumb, Col, Row} from "antd";
import { HomeFilled } from "@ant-design/icons";
import { Breadcrumb, Col, Row } from "antd";
import React from "react";
import {connect} from "react-redux";
import {Link} from "react-router-dom";
import {createStructuredSelector} from "reselect";
import {selectBreadcrumbs} from "../../redux/application/application.selectors";
import {selectBodyshop} from "../../redux/user/user.selectors";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { selectBreadcrumbs } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import GlobalSearch from "../global-search/global-search.component";
import GlobalSearchOs from "../global-search/global-search-os.component";
import "./breadcrumbs.styles.scss";
import {useSplitTreatments} from "@splitsoftware/splitio-react";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({
breadcrumbs: selectBreadcrumbs,
bodyshop: selectBodyshop,
breadcrumbs: selectBreadcrumbs,
bodyshop: selectBodyshop
});
export function BreadCrumbs({breadcrumbs, bodyshop}) {
const {treatments: {OpenSearch}} = useSplitTreatments({
attributes: {},
names: ["OpenSearch"],
splitKey: bodyshop && bodyshop.imexshopid,
});
// TODO - Client Update - Technically key is not doing anything here
return (
<Row className="breadcrumb-container">
<Col xs={24} sm={24} md={16}>
<Breadcrumb
separator=">"
items={[
{
key: "home",
title: (
<Link to={`/manage/`}>
<HomeFilled/>{" "}
{(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) ||
""}
</Link>
),
},
...breadcrumbs.map((item) =>
item.link
? {
key: item.label,
title: <Link to={item.link}>{item.label}</Link>,
}
: {
key: item.label,
title: item.label,
}
),
]}
/>
</Col>
<Col xs={24} sm={24} md={8}>
{OpenSearch.treatment === "on" ? <GlobalSearchOs/> : <GlobalSearch/>}
</Col>
</Row>
);
export function BreadCrumbs({ breadcrumbs, bodyshop }) {
const {
treatments: { OpenSearch }
} = useSplitTreatments({
attributes: {},
names: ["OpenSearch"],
splitKey: bodyshop && bodyshop.imexshopid
});
// TODO - Client Update - Technically key is not doing anything here
return (
<Row className="breadcrumb-container">
<Col xs={24} sm={24} md={16}>
<Breadcrumb
separator=">"
items={[
{
key: "home",
title: (
<Link to={`/manage/`}>
<HomeFilled /> {(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) || ""}
</Link>
)
},
...breadcrumbs.map((item) =>
item.link
? {
key: item.label,
title: <Link to={item.link}>{item.label}</Link>
}
: {
key: item.label,
title: item.label
}
)
]}
/>
</Col>
<Col xs={24} sm={24} md={8}>
{OpenSearch.treatment === "on" ? <GlobalSearchOs /> : <GlobalSearch />}
</Col>
</Row>
);
}
export default connect(mapStateToProps, null)(BreadCrumbs);

View File

@@ -1,99 +1,87 @@
import {Button, Form, Modal} from "antd";
import React, {useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {logImEXEvent} from "../../firebase/firebase.utils";
import {toggleModalVisible} from "../../redux/modals/modals.actions";
import {selectCaBcEtfTableConvert} from "../../redux/modals/modals.selectors";
import {GenerateDocument} from "../../utils/RenderTemplate";
import {TemplateList} from "../../utils/TemplateConstants";
import { Button, Form, Modal } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectCaBcEtfTableConvert } from "../../redux/modals/modals.selectors";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import CaBcEtfTableModalComponent from "./ca-bc-etf-table.modal.component";
const mapStateToProps = createStructuredSelector({
caBcEtfTableModal: selectCaBcEtfTableConvert,
caBcEtfTableModal: selectCaBcEtfTableConvert
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () =>
dispatch(toggleModalVisible("ca_bc_eftTableConvert")),
toggleModalVisible: () => dispatch(toggleModalVisible("ca_bc_eftTableConvert"))
});
export function ContractsFindModalContainer({
caBcEtfTableModal,
toggleModalVisible,
}) {
const {t} = useTranslation();
export function ContractsFindModalContainer({ caBcEtfTableModal, toggleModalVisible }) {
const { t } = useTranslation();
const {open} = caBcEtfTableModal;
const [loading, setLoading] = useState(false);
const [form] = Form.useForm();
const EtfTemplate = TemplateList("special").ca_bc_etf_table;
const { open } = caBcEtfTableModal;
const [loading, setLoading] = useState(false);
const [form] = Form.useForm();
const EtfTemplate = TemplateList("special").ca_bc_etf_table;
const handleFinish = async (values) => {
logImEXEvent("ca_bc_etf_table_parse");
setLoading(true);
const claimNumbers = [];
values.table.split("\n").forEach((row, idx, arr) => {
const {1: claim, 2: shortclaim, 4: amount} = row.split("\t");
if (!claim || !shortclaim) return;
const trimmedShortClaim = shortclaim.trim();
// const trimmedClaim = claim.trim();
if (amount.slice(-1) === "-") {
}
const handleFinish = async (values) => {
logImEXEvent("ca_bc_etf_table_parse");
setLoading(true);
const claimNumbers = [];
values.table.split("\n").forEach((row, idx, arr) => {
const { 1: claim, 2: shortclaim, 4: amount } = row.split("\t");
if (!claim || !shortclaim) return;
const trimmedShortClaim = shortclaim.trim();
// const trimmedClaim = claim.trim();
if (amount.slice(-1) === "-") {
}
claimNumbers.push({
claim: trimmedShortClaim,
amount: amount.slice(-1) === "-" ? parseFloat(amount) * -1 : amount,
});
});
claimNumbers.push({
claim: trimmedShortClaim,
amount: amount.slice(-1) === "-" ? parseFloat(amount) * -1 : amount
});
});
await GenerateDocument(
{
name: EtfTemplate.key,
variables: {
claimNumbers: `%(${claimNumbers.map((c) => c.claim).join("|")})%`,
claimdata: claimNumbers,
},
},
{},
values.sendby === "email" ? "e" : "p"
);
setLoading(false);
};
useEffect(() => {
if (open) {
form.resetFields();
await GenerateDocument(
{
name: EtfTemplate.key,
variables: {
claimNumbers: `%(${claimNumbers.map((c) => c.claim).join("|")})%`,
claimdata: claimNumbers
}
}, [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>
},
{},
values.sendby === "email" ? "e" : "p"
);
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(
mapStateToProps,
mapDispatchToProps
)(ContractsFindModalContainer);
export default connect(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 {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {selectBodyshop} from "../../redux/user/user.selectors";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop
});
export default connect(mapStateToProps, null)(PartsReceiveModalComponent);
export function PartsReceiveModalComponent({bodyshop, form}) {
const {t} = useTranslation();
export function PartsReceiveModalComponent({ bodyshop, form }) {
const { t } = useTranslation();
return (
<div>
<Form.Item
name="table"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input.TextArea rows={8}/>
</Form.Item>
<Form.Item
label={t("general.labels.sendby")}
name="sendby"
initialValue="print"
>
<Radio.Group>
<Radio value="email">{t("general.labels.email")}</Radio>
<Radio value="print">{t("general.labels.print")}</Radio>
</Radio.Group>
</Form.Item>
</div>
);
return (
<div>
<Form.Item
name="table"
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Input.TextArea rows={8} />
</Form.Item>
<Form.Item label={t("general.labels.sendby")} name="sendby" initialValue="print">
<Radio.Group>
<Radio value="email">{t("general.labels.email")}</Radio>
<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 {Button, Form, InputNumber, Popover} from "antd";
import {logImEXEvent} from "../../firebase/firebase.utils";
import {useTranslation} from "react-i18next";
import {CalculatorFilled} from "@ant-design/icons";
import { CalculatorFilled } from "@ant-design/icons";
import { Button, Form, InputNumber, Popover, Space } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
export default function CABCpvrtCalculator({ disabled, form }) {
const [visibility, setVisibility] = useState(false);
export default function CABCpvrtCalculator({disabled, form}) {
const [visibility, setVisibility] = useState(false);
const { t } = useTranslation();
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) => {
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 popContent = (
<div>
<Form onFinish={handleFinish} initialValues={{rate: 1.5}}>
<Form.Item name="rate" label={t("jobs.labels.ca_bc_pvrt.rate")}>
<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/>
const popContent = (
<div>
<Form onFinish={handleFinish} initialValues={{ rate: 1.5 }}>
<Form.Item name="rate" label={t("jobs.labels.ca_bc_pvrt.rate")}>
<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>
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<Space>
<Button type="primary" htmlType="submit">
{t("general.actions.calculate")}
</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 {useLazyQuery, useMutation} from "@apollo/client";
import {Button, Card, Col, Form, Input, notification, Row, Space, Spin, Statistic,} from "antd";
import { DeleteFilled } from "@ant-design/icons";
import { useLazyQuery, useMutation } from "@apollo/client";
import { Button, Card, Col, Form, Input, Row, Space, Spin, Statistic, notification } from "antd";
import axios from "axios";
import dayjs from "../../utils/day";
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {INSERT_PAYMENT_RESPONSE, QUERY_RO_AND_OWNER_BY_JOB_PKS,} from "../../graphql/payment_response.queries";
import {INSERT_NEW_PAYMENT} from "../../graphql/payments.queries";
import {insertAuditTrail} from "../../redux/application/application.actions";
import {toggleModalVisible} from "../../redux/modals/modals.actions";
import {selectCardPayment} from "../../redux/modals/modals.selectors";
import {selectBodyshop} from "../../redux/user/user.selectors";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_PAYMENT_RESPONSE, QUERY_RO_AND_OWNER_BY_JOB_PKS } from "../../graphql/payment_response.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectCardPayment } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
const mapStateToProps = createStructuredSelector({
cardPaymentModal: selectCardPayment,
bodyshop: selectBodyshop,
cardPaymentModal: selectCardPayment,
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })),
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment"))
});
const CardPaymentModalComponent = ({
bodyshop,
cardPaymentModal,
toggleModalVisible,
insertAuditTrail,
}) => {
const {context} = cardPaymentModal;
const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisible, insertAuditTrail }) => {
const { context, actions } = cardPaymentModal;
const [form] = Form.useForm();
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
const {t} = useTranslation();
const [loading, setLoading] = useState(false);
// const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
const { t } = useTranslation();
const [, {data, refetch, queryLoading}] = useLazyQuery(
QUERY_RO_AND_OWNER_BY_JOB_PKS,
{
variables: {jobids: [context.jobid]},
skip: true,
const [, { data, refetch, queryLoading }] = useLazyQuery(QUERY_RO_AND_OWNER_BY_JOB_PKS, {
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);
//Initialize the intellipay window.
const SetIntellipayCallbackFunctions = () => {
console.log("*** Set IntelliPay callback functions.");
window.intellipay.runOnClose(() => {
//window.intellipay.initialize();
});
payments.forEach((payment) =>
insertAuditTrail({
jobid: payment.jobid,
operation: AuditTrailMapping.failedpayment(),
type: "failedpayment"
})
);
});
};
window.intellipay.runOnApproval(async function (response) {
console.warn("*** Running On Approval Script ***");
form.setFieldValue("paymentResponse", response);
form.submit();
});
window.intellipay.runOnNonApproval(async function (response) {
// Mutate unsuccessful payment
const handleIntelliPayCharge = async () => {
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({
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,
})),
},
});
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",
message: t("job_payments.notifications.error.openingip")
});
setLoading(false);
}
};
payments.forEach((payment) =>
insertAuditTrail({
jobid: payment.jobid,
operation: AuditTrailMapping.failedpayment(),
})
);
});
};
return (
<Card title="Card Payment">
<Spin spinning={loading}>
<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) => {
try {
await insertPayment({
variables: {
paymentInput: values.payments.map((payment) => ({
amount: payment.amount,
transactionid: (values.paymentResponse.paymentid || "").toString(),
payer: t("payments.labels.customer"),
type: values.paymentResponse.cardbrand,
jobid: payment.jobid,
date: dayjs(Date.now()),
payment_responses: {
data: [
{
amount: payment.amount,
bodyshopid: bodyshop.id,
jobid: payment.jobid,
declinereason: values.paymentResponse.declinereason,
ext_paymentid: values.paymentResponse.paymentid.toString(),
successful: true,
response: values.paymentResponse,
},
],
},
})),
},
refetchQueries: ["GET_JOB_BY_PK"],
});
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();
<Form.Item
shouldUpdate={(prevValues, curValues) =>
prevValues.payments?.map((p) => p?.jobid).join() !== curValues.payments?.map((p) => p?.jobid).join()
}
} catch (error) {
notification.open({
type: "error",
message: t("job_payments.notifications.error.openingip"),
});
setLoading(false);
}
};
>
{() => {
//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
) {
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 (
<Card title="Card Payment">
<Spin spinning={loading}>
<Form
onFinish={handleFinish}
form={form}
layout="vertical"
initialValues={{
payments: context.jobid ? [{jobid: context.jobid}] : [],
}}
>
<Form.List name={["payments"]}>
{(fields, {add, remove, move}) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Row gutter={[16, 16]}>
<Col span={16}>
<Form.Item
key={`${index}jobid`}
label={t("jobs.fields.ro_number")}
name={[field.name, "jobid"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<JobSearchSelectComponent
notExported={false}
clm_no
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
key={`${index}amount`}
label={t("payments.fields.amount")}
name={[field.name, "amount"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<CurrencyFormItemComponent/>
</Form.Item>
</Col>
<Col span={2}>
<DeleteFilled
style={{margin: "1rem"}}
onClick={() => {
remove(field.name);
}}
/>
</Col>
</Row>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{width: "100%"}}
>
{t("general.actions.add")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
<Form.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>
);
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)}
/>
<Input
className="ipayfield"
data-ipayname="comment"
type="hidden"
value={btoa(JSON.stringify(payments))}
hidden
/>
<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>
</Form>
</Spin>
</Card>
);
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(CardPaymentModalComponent);
export default connect(mapStateToProps, mapDispatchToProps)(CardPaymentModalComponent);

View File

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

View File

@@ -1,98 +1,97 @@
import {useApolloClient} from "@apollo/client";
import {getToken, onMessage} from "@firebase/messaging";
import {Button, notification, Space} from "antd";
import { useApolloClient } from "@apollo/client";
import { getToken, onMessage } from "@firebase/messaging";
import { Button, notification, Space } from "antd";
import axios from "axios";
import React, {useEffect} from "react";
import {useTranslation} from "react-i18next";
import {messaging, requestForToken} from "../../firebase/firebase.utils";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { messaging, requestForToken } from "../../firebase/firebase.utils";
import FcmHandler from "../../utils/fcm-handler";
import ChatPopupComponent from "../chat-popup/chat-popup.component";
import "./chat-affix.styles.scss";
export function ChatAffixContainer({bodyshop, chatVisible}) {
const {t} = useTranslation();
const client = useApolloClient();
useEffect(() => {
if (!bodyshop || !bodyshop.messagingservicesid) return;
export function ChatAffixContainer({ bodyshop, chatVisible }) {
const { t } = useTranslation();
const client = useApolloClient();
async function SubscribeToTopic() {
try {
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>
),
});
}
}
useEffect(() => {
if (!bodyshop || !bodyshop.messagingservicesid) return;
SubscribeToTopic();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [bodyshop]);
async function SubscribeToTopic() {
try {
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(() => {
function handleMessage(payload) {
FcmHandler({
client,
payload: (payload && payload.data && payload.data.data) || payload.data,
});
}
SubscribeToTopic();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [bodyshop]);
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]);
useEffect(() => {
function handleMessage(payload) {
FcmHandler({
client,
payload: (payload && payload.data && payload.data.data) || payload.data
});
}
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 (
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
{bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent/> : null}
</div>
);
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
return (
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
{bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent /> : null}
</div>
);
}
export default ChatAffixContainer;

View File

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

View File

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

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