Compare commits

...

474 Commits

Author SHA1 Message Date
Allan Carr
b667524a04 Merged in release/2024-08-23-LEGACY (pull request #1639)
IO-2887 Null out BillData if returnfrombill is not available
2024-08-20 23:17:11 +00:00
Allan Carr
011cb1b349 Merged in feature/IO-2887-Returnfrombill-Parts-Drawer-LEGACY (pull request #1638)
IO-2887 Null out BillData if returnfrombill is not available
2024-08-20 23:16:45 +00:00
Allan Carr
9b2cdddb88 IO-2887 Null out BillData if returnfrombill is not available
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-20 16:17:04 -07:00
Allan Carr
5a4c0e4f65 Merged in release/2024-08-23-LEGACY (pull request #1637)
IO-2887 Returnfrombill Parts Drawer Legacy
2024-08-20 22:47:06 +00:00
Allan Carr
c71db5133d Merged in feature/IO-2887-Returnfrombill-Parts-Drawer-LEGACY (pull request #1636)
IO-2887 Returnfrombill Parts Drawer Legacy
2024-08-20 22:46:36 +00:00
Allan Carr
ee733434e6 IO-2887 Returnfrombill Parts Drawer Legacy
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-20 15:45:21 -07:00
Dave Richer
c2050e7e26 Merged in release/2024-08-16-LEGACY (pull request #1626)
Release/2024 08 16 LEGACY

Approved-by: Allan Carr
2024-08-16 23:31:21 +00:00
Dave Richer
5f9c756172 Merged in release/2024-08-16-LEGACY (pull request #1624)
IO-2884 Production List Filter LEGACY
2024-08-16 16:17:42 +00:00
Allan Carr
c0096a0dec Merged in feature/IO-2884-Production-List-Filter-LEGACY (pull request #1621)
IO-2884 Production List Filter LEGACY

Approved-by: Dave Richer
2024-08-16 16:16:29 +00:00
Allan Carr
41e40b9165 IO-2884 Production List Filter LEGACY
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-16 09:16:32 -07:00
Dave Richer
6528509288 Merged in release/2024-08-16-LEGACY (pull request #1609)
- Improve handle beta code (AIO Version)
2024-08-15 15:21:48 +00:00
Dave Richer
1e566c82cd Merged in feature/IO-2878-Enhance-Beta-Switch-legacy (pull request #1608)
- Improve handle beta code (AIO Version)
2024-08-15 15:21:28 +00:00
Dave Richer
d45be60668 - Improve handle beta code (AIO Version)
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-15 11:19:26 -04:00
Allan Carr
a18e40a63c Merged in release/2024-08-16-LEGACY (pull request #1605)
IO-2876 OpenSearch Sorters LEGACY
2024-08-14 19:40:24 +00:00
Allan Carr
62d5511337 Merged in feature/IO-2876-OpenSearch-Sorters-LEGACY (pull request #1604)
IO-2876 OpenSearch Sorters LEGACY

Approved-by: Dave Richer
2024-08-14 19:37:26 +00:00
Allan Carr
ac37074666 IO-2876 OpenSearch Sorters LEGACY
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-14 11:47:34 -07:00
Dave Richer
c171847555 Merged in release/2024-08-16-LEGACY (pull request #1602)
- Improve handle beta code (AIO Version)
2024-08-14 18:42:09 +00:00
Dave Richer
9467c57c8f Merged in feature/IO-2878-Enhance-Beta-Switch-legacy (pull request #1600)
- Improve handle beta code (AIO Version)
2024-08-14 17:41:59 +00:00
Dave Richer
87f591ce1b - Improve handle beta code (AIO Version)
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-14 13:33:27 -04:00
Allan Carr
c3a950088e Merged in release/2024-08-16-LEGACY (pull request #1592)
Release/2024 08 16 LEGACY

Approved-by: Dave Richer
2024-08-12 23:54:09 +00:00
Allan Carr
7e60e2c699 Merged in release/2024-08-16-LEGACY (pull request #1591)
Release/2024 08 16 LEGACY
2024-08-12 22:23:44 +00:00
Allan Carr
704e35e0b0 Merged in feature/IO-2564-LEGACY-Row-Expander-Drawers (pull request #1589)
IO-2564 LEGACY Row Expander Drawers
2024-08-12 22:17:22 +00:00
Allan Carr
d616d204d0 Merged in feature/IO-2861-LEGACY-Disable-Vendor-and-Invoice-#-on-Edit-of-InHouse (pull request #1590)
IO-2861 LEGACY Disable Editing of InHouse Invoice for Vendor and Invoice Number
2024-08-12 22:16:24 +00:00
Allan Carr
8eae657bb3 IO-2564 LEGACY Row Expander Drawers
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-12 15:04:42 -07:00
Allan Carr
492cee29e7 IO-2861 LEGACY Disable Editing of InHouse Invoice for Vendor and Invoice Number
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-12 12:36:31 -07:00
Dave Richer
b4cd016008 Merged in release/2024-05-31 (pull request #1455)
IO-2792 Resolve bill receive of credit memo and noline
2024-06-01 17:21:19 +00:00
Dave Richer
e685ca5ad8 Merged in release/2024-05-31 (pull request #1454)
IO-2792 Resolve bill receive of credit memo and noline
2024-06-01 17:20:41 +00:00
Patrick Fic
7595cbf11d IO-2792 Resolve bill receive of credit memo and noline 2024-05-29 14:10:32 -07:00
Dave Richer
786f2f212c Merged in release/2024-05-24 (pull request #1448)
Release/2024 05 24
2024-05-24 20:44:00 +00:00
Dave Richer
0fa110cc51 Merged in release/2024-05-24 (pull request #1446)
Release/2024 05 24
2024-05-24 17:22:07 +00:00
Patrick Fic
bdfbe52244 Remove API from master deployment. 2024-05-24 10:18:58 -07:00
Patrick Fic
0ca75298d8 Merge branch 'release/2024-05-17' into release/2024-05-24 2024-05-24 10:17:42 -07:00
Dave Richer
1ffad4a8b0 Progress Commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-14 12:37:33 -04:00
Dave Richer
cac7df66e1 Fix issue with global search 2024-05-14 12:19:58 -04:00
Patrick Fic
76441e49e1 Merge branch 'test' of bitbucket.org:snaptsoft/bodyshop into test 2024-05-03 14:39:26 -07:00
Patrick Fic
524385af7d Merge branch 'release/2024-04-26' into test 2024-05-03 14:39:06 -07:00
Patrick Fic
cb50ca7897 Merged in release/2024-04-26 (pull request #1439)
Release/2024 04 26
2024-05-03 21:35:26 +00: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
d6d6ced7a4 CC modifications. 2024-05-01 11:47:42 -07:00
Patrick Fic
ded7393aec Merged in release/2024-04-26 (pull request #1437)
Release/2024 04 26
2024-04-26 16:06:44 +00: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
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
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
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
b269d54f2c Merge branch 'release/2024-04-19' into test 2024-04-16 16:11:10 -07:00
Patrick Fic
069d508528 Update cron trigger timing. 2024-04-16 16:09:54 -07:00
Patrick Fic
d213c382c9 Merge branch 'release/2024-04-19' into test 2024-04-16 16:03:42 -07:00
Patrick Fic
878e81dc8f Hasura schema changes for tasks. 2024-04-16 16:02:25 -07:00
Dave Richer
deab90b326 Merged in release/2024-04-19 (pull request #1417)
Release/2024 04 19
2024-04-16 19:55:26 +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
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
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
Allan Carr
8c74276b98 Merged in release/2024-04-12 (pull request #1410)
IO-2609 Fix Spelling Mistake in object name
2024-04-11 21:39:59 +00: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
Allan Carr
ac1ad12ac4 Merged in release/2024-04-12 (pull request #1407)
Release/2024 04 12
2024-04-11 21:23:28 +00: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
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
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
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
Allan Carr
dedc594b58 Merged in release/2024-04-05 (pull request #1400)
Release/2024 04 05
2024-04-08 19:58:02 +00: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
Allan Carr
88a71dd647 IO-2731 Payment Edit
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-04-08 12:23:04 -07: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
Allan Carr
02747f4144 Merged in release/2024-04-05 (pull request #1397)
IO-2568 Button Padding in Print Center Label Modal
2024-04-05 19:03:48 +00: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
Allan Carr
05b1078e27 Merged in release/2024-04-05 (pull request #1394)
IO-2750 Missing Mutation return fields
2024-04-05 15:55:38 +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
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
Allan Carr
cc57eb40cf Merged in release/2024-04-05 (pull request #1391)
Release/2024 04 05
2024-04-04 19:27:21 +00:00
Patrick Fic
37196e65c3 Add schema changes for RO Guard to bodyshop table. 2024-04-03 09:46:52 -07: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
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
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
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
Allan Carr
19ec4cb021 Merged in release/2024-03-28 (pull request #1382)
Release/2024 03 28
2024-03-28 21:15:25 +00:00
Allan Carr
33518e5c25 Merged in release/2024-03-28 (pull request #1381)
Release/2024 03 28
2024-03-28 20:26:17 +00: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
Patrick Fic
af6bc0fb5a Resolve hasura API url again. 2024-03-28 10:23:22 -07:00
Dave Richer
b34909b7f3 Merged in release/2024-03-28 (pull request #1378)
Release/2024 03 28
2024-03-28 17:00:21 +00: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
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
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
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
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
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
Dave Richer
bd599cfea4 Merged in release/2024-03-22 (pull request #1371)
IO-2721 Owners Note in Owners Card
2024-03-22 16:19:15 +00: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
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
e8917f5736 Merged in release/2024-03-22 (pull request #1368)
Release/2024 03 22
2024-03-21 21:43:06 +00: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
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
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
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
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
Dave Richer
2215c8439e - Hasura Migration Changes
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-15 16:12:27 -04: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
Patrick Fic
b7ee95059d Merged in release/2024-03-15 (pull request #1360)
Release/2024 03 15
2024-03-15 17:28:36 +00: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
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
Dave Richer
f59d369529 Merged in release/2024-03-15 (pull request #1352)
- Implement
2024-03-15 14:49:47 +00: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
Dave Richer
eefac38d9c Merged in release/2024-03-15 (pull request #1349)
- Fix bug
2024-03-14 19:00:06 +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
249b74a597 Merged in release/2024-03-15 (pull request #1347)
Release/2024 03 15
2024-03-14 16:39:05 +00: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
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
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
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
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
Allan Carr
4fb491697c Merged in release/2024-03-08 (pull request #1329)
Release/2024 03 08
2024-03-08 19:39:07 +00: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
09da3f349b Merged in release/2024-03-08 (pull request #1326)
- Add new special filters
2024-03-08 17:48:15 +00: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
e8f4f30cc4 Merged in release/2024-03-08 (pull request #1324)
Release/2024 03 08
2024-03-08 17:39:02 +00: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
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
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
Patrick Fic
8c4f395ff9 Merged in release/2024-03-01 (pull request #1316)
Resolve refund payment logging.
2024-03-01 19:59:42 +00:00
Patrick Fic
85a3aeb335 Resolve refund payment logging. 2024-03-01 11:51:01 -08:00
Allan Carr
acbf75ff4d Merged in release/2024-03-01 (pull request #1313)
Release/2024 03 01

Approved-by: Dave Richer
2024-03-01 17:18:20 +00: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
c691d44c44 Merged in release/2024-02-23 (pull request #1307)
Release/2024 02 23
2024-02-23 22:00:15 +00:00
Allan Carr
36e3828e2d Merged in release/2024-02-23 (pull request #1309)
IO-2640 Adjust Filters and Sorters for Table
2024-02-23 21:26:20 +00:00
Allan Carr
ebb3a13ff5 Merged in feature/IO-2640-TV-Mode-for-Scheduled-In-Out (pull request #1308)
IO-2640 Adjust Filters and Sorters for Table

Approved-by: Dave Richer
2024-02-23 21:14:18 +00:00
Allan Carr
3846b7c5fc IO-2640 Adjust Filters and Sorters for Table
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-23 13:01:46 -08:00
Allan Carr
281c9bbc2a Merged in release/2024-02-23 (pull request #1304)
Release/2024 02 23
2024-02-22 21:53:33 +00:00
Allan Carr
93d139f926 Merged in feature/IO-2640-TV-Mode-for-Scheduled-In-Out (pull request #1303)
IO-2640 TV Mode for Schedule In and Out Dashboard Components

Approved-by: Dave Richer
2024-02-22 21:50:18 +00:00
Allan Carr
4d1f40537c IO-2640 Change Variable Names and adjust CSS
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-22 12:57:48 -08:00
Dave Richer
896f1415f7 Merged in feature/IO-2636-Customized-Report-Filtering-Version-2 (pull request #1302)
Feature/IO-2636 Customized Report Filtering Version 2
2024-02-21 21:58:27 +00:00
Dave Richer
8a01cd9cb0 - call changes
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-21 16:57:56 -05:00
Allan Carr
c2013d47e7 IO-2640 TV Mode for Schedule In and Out Dashboard Components
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-21 12:03:35 -08:00
Dave Richer
0f20807690 Merge remote-tracking branch 'origin/master' into feature/IO-2636-Customized-Report-Filtering-Version-2 2024-02-21 14:30:28 -05:00
Dave Richer
2a45be6a45 - fix on change set Field value issues
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-21 14:29:56 -05:00
Dave Richer
b63602143e - fix select box being weird on scroll / resize
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-21 14:10:50 -05:00
Dave Richer
2add712270 - add additional date picker presets in development
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-21 13:23:22 -05:00
Dave Richer
77c8f74bcb - Restore functionality
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-21 13:13:26 -05:00
Allan Carr
1d38102541 Merged in feature/IO-2578-Scoreboard-Entries (pull request #1300)
IO-2578 Scoreboard Entries Modal

Approved-by: Dave Richer
2024-02-21 17:36:28 +00:00
Allan Carr
6117b5ab64 Merged in feature/IO-2562-Job-Info-Block-CC-Info (pull request #1298)
IO-2562 CC Info in Job Block UI Correction

Approved-by: Dave Richer
2024-02-21 17:35:39 +00:00
Allan Carr
578f0a110e Merged in feature/IO-2556-CC-Sort-Order (pull request #1301)
IO-2556 CC Sort Order

Approved-by: Dave Richer
2024-02-21 17:34:56 +00:00
Allan Carr
b5c66274ca Merged in feature/IO-2557-New-CC-Contract-Warnings (pull request #1299)
IO-2557 New CC Contract Warnings

Approved-by: Dave Richer
2024-02-21 17:34:08 +00:00
Dave Richer
be46bdc57f - Default sorts!
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-20 19:11:37 -05:00
Dave Richer
6263e63a1d Merge branch 'feature/IO-2636-Customized-Report-Filtering-Version-2' of bitbucket.org:snaptsoft/bodyshop into feature/IO-2636-Customized-Report-Filtering-Version-2 2024-02-20 19:11:10 -05:00
Dave Richer
83d702f12b - Default sorts!
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-20 19:10:34 -05:00
Dave Richer
37708a0b59 - Default sorts!
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-20 19:07:23 -05:00
Allan Carr
6cfcab8156 IO-2557 / IO-1019 Update tooltip
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-20 14:52:40 -08:00
Allan Carr
33ec18986d IO-2556 CC Sort Order
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-20 13:10:57 -08:00
Dave Richer
6b7b34ae79 - Progress Commit, this fills agreed upon functionality
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-20 16:00:59 -05:00
Allan Carr
06ef2482ba IO-2562 CC Info in Job Block UI Correction
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-20 12:53:12 -08:00
Allan Carr
83bd485597 IO-2557 New CC Contract Warnings
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-20 09:19:30 -08:00
Dave Richer
6921f2fe68 Merge branch 'release/2024-02-16' into feature/IO-2636-Customized-Report-Filtering-Version-2 2024-02-16 20:49:04 -05:00
Allan Carr
a7e199932c IO-2578 Scoreboard Entries Modal
Correct OK button, add sorting to table, adjust date to only be a date, remove closeable on modal

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-16 17:14:11 -08:00
Allan Carr
2427c14b7b Merged in release/2024-02-16 (pull request #1296)
IO-2631 Correct Import Statement for moment

Approved-by: Dave Richer
2024-02-16 21:37:49 +00:00
Allan Carr
455e79a6f7 Merged in release/2024-02-16 (pull request #1295)
IO-2631 Correct Import Statement for moment
2024-02-16 20:34:07 +00:00
Allan Carr
66655d449e Merged in feature/IO-2631-Update-Schedule-Completion-on-Import (pull request #1294)
IO-2631 Correct Import Statement for moment

Approved-by: Dave Richer
2024-02-16 20:33:05 +00:00
Allan Carr
845a84c4c8 IO-2631 Correct Import Statement for moment 2024-02-16 12:30:12 -08:00
Dave Richer
0ff5ea3d59 Merged in release/2024-02-16 (pull request #1291)
Release/2024 02 16

Approved-by: Patrick Fic
2024-02-16 20:21:35 +00:00
Allan Carr
bc73e75217 Merged in release/2024-02-16 (pull request #1293)
IO-2631 Correct for Business Days
2024-02-16 19:25:01 +00:00
Allan Carr
09492e647e Merged in feature/IO-2631-Update-Schedule-Completion-on-Import (pull request #1292)
IO-2631 Correct for Business Days

Approved-by: Dave Richer
2024-02-16 19:19:08 +00:00
Allan Carr
2d6594cc73 IO-2631 Correct for Business Days 2024-02-16 11:08:59 -08:00
Dave Richer
3b8e83d88a - clear stage
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-16 12:32:40 -05:00
Dave Richer
3ec4dbb5b8 - big progress commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-16 12:25:24 -05:00
Allan Carr
7b3f71701e Merged in release/2024-02-16 (pull request #1288)
IO-2637 Correct for Timezone offset
2024-02-16 17:24:44 +00:00
Allan Carr
601fdbba39 Merged in feature/IO-2637-Add-Scoreboard-Date (pull request #1287)
IO-2637 Correct for Timezone offset

Approved-by: Dave Richer
2024-02-16 17:23:05 +00:00
Allan Carr
830adc4ef3 IO-2637 Correct for Timezone offset 2024-02-16 08:52:33 -08:00
Dave Richer
383a383a9f Merged in release/2024-02-16 (pull request #1286)
- Fix issues with labels on sorters
2024-02-16 02:05:56 +00:00
Dave Richer
3f216195ca Merge branch 'feature/IO-2456-Report-Filters-From-Master' into release/2024-02-16 2024-02-15 21:03:30 -05:00
Dave Richer
bcb8de0937 - Fix issues with labels on sorters
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-15 21:03:04 -05:00
Dave Richer
9cc0d6175e - Progress commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-15 21:01:43 -05:00
Dave Richer
7ee853ae15 Merged in release/2024-02-16 (pull request #1285)
Release/2024 02 16
2024-02-15 17:45:53 +00:00
Dave Richer
4c07632ee6 Merged in feature/IO-2456-Report-Filters-From-Master (pull request #1283)
- Report Center Filters Version 1 retargeted to Master

Approved-by: Patrick Fic
2024-02-15 17:32:38 +00:00
Dave Richer
767c219af8 - Remove additional console.log
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-15 11:59:56 -05:00
Dave Richer
cfc301570e - Remove redundant CSS
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-15 11:57:27 -05:00
Allan Carr
fb718f9f37 Merged in feature/IO-2631-Update-Schedule-Completion-on-Import (pull request #1284)
IO-2631 Update Scheduled Completion on Supp

Approved-by: Dave Richer
2024-02-15 16:53:41 +00:00
Dave Richer
cafc0e5628 - Update GUI and provide loading state
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-15 11:49:02 -05:00
Dave Richer
a635725839 - Remove console.log statements
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-15 10:58:19 -05:00
Allan Carr
eb8e9b10ef IO-2631 Update Scheduled Completion on Supp 2024-02-14 16:47:25 -08:00
Dave Richer
2584f7129c - Report Center Filters Version 1 retargeted to Master
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-14 14:31:35 -05:00
Allan Carr
6c996037d6 Merged in release/2024-02-02 (pull request #1274)
Release/2024 02 02

Approved-by: Patrick Fic
2024-02-09 18:47:24 +00:00
Allan Carr
3510d081cd Merged in release/2024-02-02 (pull request #1273)
IO-2630 Adjust Query to match Tags
2024-02-09 17:55:09 +00:00
Allan Carr
f421b45222 Merged in feature/IO-2630-Parts-Queue-Mods (pull request #1272)
IO-2630 Adjust Query to match Tags
2024-02-09 17:54:46 +00:00
Allan Carr
30cf46a158 IO-2630 Adjust Query to match Tags 2024-02-09 09:54:22 -08:00
Allan Carr
471105828c Merged in release/2024-02-02 (pull request #1271)
Release/2024 02 02
2024-02-09 16:20:01 +00:00
Allan Carr
8d37c54f89 Merged in feature/IO-2630-Parts-Queue-Mods (pull request #1269)
Feature/IO-2630 Parts Queue Mods

Approved-by: Dave Richer
2024-02-09 16:14:51 +00:00
Allan Carr
f9b9f39418 IO-2630 Add in Drawer for Parts Queue 2024-02-08 17:18:14 -08:00
Allan Carr
05a5df789b IO-2030 Change & Add Columns, Add Sorters and Filters 2024-02-08 13:22:00 -08:00
Allan Carr
1ec1f5d3a8 Merged in release/2024-02-02 (pull request #1268)
IO-2626 Adjust Image Prop on customer page
2024-02-08 17:58:46 +00:00
Allan Carr
bed87eda97 Merged in feature/IO-2626-CSI-Pages (pull request #1267)
IO-2626 Adjust Image Prop on customer page
2024-02-08 17:58:21 +00:00
Allan Carr
67008c35b8 IO-2626 Adjust Image Prop on customer page 2024-02-08 09:57:39 -08:00
Patrick Fic
355de9efd0 Merge branch 'release/2024-02-02' into test 2024-02-06 10:05:44 -08:00
Patrick Fic
9b95e40a13 Merge branch 'release/2024-02-02' of bitbucket.org:snaptsoft/bodyshop into release/2024-02-02 2024-02-06 10:05:26 -08:00
Patrick Fic
e3a998b6f8 Merge branch 'feature/IO-2626-CSI-Pages' into release/2024-02-02 2024-02-06 10:05:11 -08:00
Patrick Fic
3c7ede0155 IO-2626 Prevent crisp from loading on anon CSI page. 2024-02-06 10:04:59 -08:00
Allan Carr
82c7aa1347 Merged in feature/IO-2626-CSI-Pages (pull request #1265)
IO-2626 Resource Class for Test
2024-02-06 17:37:46 +00:00
Allan Carr
4ccd912363 Merge branch 'feature/IO-2626-CSI-Pages' of bitbucket.org:snaptsoft/bodyshop into feature/IO-2626-CSI-Pages 2024-02-06 09:36:16 -08:00
Patrick Fic
2c14731ebd Merge branch 'release/2024-02-02' into test 2024-02-06 09:30:29 -08:00
Patrick Fic
c0525842fd Merge branch 'feature/IO-2626-CSI-Pages' into release/2024-02-02 2024-02-06 09:29:39 -08:00
Patrick Fic
dd5ca5d233 Add resource class to test build as well. 2024-02-06 09:29:22 -08:00
Allan Carr
616a4b04a0 IO-2626 Resource Class for Test 2024-02-06 09:10:32 -08:00
Allan Carr
065d2d5688 Merged in release/2024-02-02 (pull request #1264)
IO-2626 CICD Resource Size Change
2024-02-06 16:56:32 +00:00
Allan Carr
9a34640c88 Merged in feature/IO-2626-CSI-Pages (pull request #1263)
IO-2626 CICD Resource Size Change
2024-02-06 16:55:55 +00:00
Allan Carr
3110be4703 IO-2626 CICD Resource Size Change 2024-02-06 08:52:51 -08:00
Allan Carr
879dea9fa7 Merged in release/2024-02-02 (pull request #1260)
Release/2024 02 02
2024-02-06 16:18:33 +00:00
Allan Carr
971c8d41ff Merged in feature/IO-2626-CSI-Pages (pull request #1253)
IO-2626 CSI Pages
2024-02-06 16:17:50 +00:00
Allan Carr
7c303a5154 IO-2626 Change Server Variable Name for response 2024-02-05 20:06:28 -08:00
Allan Carr
9383b37a41 IO-2626 Modify seachparm and fix linking 2024-02-05 20:03:17 -08:00
Allan Carr
205d507097 IO-2626 Update Translations 2024-02-02 22:50:40 -08:00
Allan Carr
97a1bd66d1 IO-2626 Correct Sorting, Linking, Pagination and Update Response Container 2024-02-02 22:45:06 -08:00
Allan Carr
0d1ff6390c IO-2626 Correct Error Page footer 2024-02-02 12:35:18 -08:00
Allan Carr
830d2c87d2 IO-2626 CSI Pages
Move to Server side initial commit
2024-02-02 11:55:57 -08:00
Allan Carr
3ceada30a3 Merged in release/2024-02-02 (pull request #1252)
IO-2624 federal_tax_exempt destructure
2024-02-02 17:47:36 +00:00
Allan Carr
da98e7b886 Merged in feature/IO-2624-Federal-Tax-Exempt-Destructure (pull request #1251)
IO-2624 federal_tax_exempt destructure

Approved-by: Dave Richer
2024-02-02 17:18:24 +00:00
Allan Carr
a74a9ba5a1 IO-2624 federal_tax_exempt destructure 2024-01-31 09:59:56 -08:00
Patrick Fic
b8a3081488 Merged in feature/Sentry-Improvements (pull request #1247)
Change tracing targets.
2024-01-31 01:09:03 +00:00
Patrick Fic
c8fc1b0f68 Merged in feature/Sentry-Improvements (pull request #1246)
Change tracing targets.
2024-01-31 01:08:37 +00:00
Patrick Fic
d26eb81339 Merge branch 'feature/Sentry-Improvements' into test 2024-01-30 16:58:35 -08:00
Patrick Fic
da1ddb874f Change tracing targets. 2024-01-30 16:58:11 -08:00
Patrick Fic
771f9ceaa8 Merged in feature/Sentry-Improvements (pull request #1242)
Feature/Sentry Improvements
2024-01-30 21:45:22 +00:00
Patrick Fic
9b61da5c62 Merged in feature/Sentry-Improvements (pull request #1239)
Feature/Sentry Improvements
2024-01-30 21:42:33 +00:00
Patrick Fic
fcdcdbbf7f Merge branch 'feature/Sentry-Improvements' into test 2024-01-30 13:33:13 -08:00
Patrick Fic
7daf7540b3 Resolve Typo in CI. 2024-01-30 13:32:58 -08:00
Patrick Fic
d482447dc9 Merge branch 'feature/Sentry-Improvements' into test 2024-01-30 13:26:32 -08:00
Patrick Fic
ce6940629d Exclude source map upload in CI. 2024-01-30 13:25:48 -08:00
Patrick Fic
647dfbfa06 Merge branch 'feature/Sentry-Improvements' into test 2024-01-30 13:14:43 -08:00
Patrick Fic
b706b96d32 Remove sentry test button. 2024-01-30 13:14:27 -08:00
Patrick Fic
f17899285f Merge branch 'feature/Sentry-Improvements' into test 2024-01-30 12:53:20 -08:00
Patrick Fic
2427bc72f2 Updated CI. 2024-01-30 12:53:07 -08:00
Patrick Fic
47f5641518 Merge branch 'test' of bitbucket.org:snaptsoft/bodyshop into test 2024-01-30 12:48:00 -08:00
Patrick Fic
6e41a282ca Merge branch 'feature/Sentry-Improvements' into test 2024-01-30 12:47:38 -08:00
Patrick Fic
8bc1a9d9ee Initial sentry improvements to deploy and verify against test. 2024-01-30 12:47:12 -08:00
Patrick Fic
ba30225ba1 Merged in release/2024-01-29 (pull request #1230)
IO-1532 resolve update logic issue for status timings.
2024-01-29 17:06:31 +00:00
Patrick Fic
abbd2263ee Merged in release/2024-01-29 (pull request #1229)
IO-1532 resolve update logic issue for status timings.
2024-01-29 17:06:19 +00:00
Patrick Fic
cb4a6e8774 IO-1532 resolve update logic issue for status timings. 2024-01-29 09:05:46 -08:00
Dave Richer
23f640028d Merged in release/2024-01-26 (pull request #1227)
Release/2024 01 26
2024-01-27 02:25:22 +00:00
Dave Richer
4b70c89e54 Merged in release/2024-01-26 (pull request #1224)
- updates from lifecyle component.
2024-01-26 23:07:40 +00:00
Dave Richer
3e5e6263fe Merged in feature/IO-1532-Tracking-Department-Cycle-Times (pull request #1223)
- updates from lifecyle component.
2024-01-26 23:07:02 +00:00
Dave Richer
48cef3e188 - updates from lifecyle component.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-26 18:06:14 -05:00
Dave Richer
5e2c1aae4f Merged in release/2024-01-26 (pull request #1222)
- Fix use hook
2024-01-26 21:34:47 +00:00
Dave Richer
8c0d6b2f6b Merged in feature/IO-1532-Tracking-Department-Cycle-Times (pull request #1221)
- Fix use hook
2024-01-26 21:34:17 +00:00
Dave Richer
22ee8acd0d - Fix use hook
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-26 16:33:40 -05:00
Dave Richer
fda06bb298 Merged in release/2024-01-26 (pull request #1220)
Release/2024 01 26
2024-01-26 21:21:52 +00:00
Dave Richer
afdcfb7bf6 Merge branch 'feature/IO-1532-Tracking-Department-Cycle-Times' into release/2024-01-26 2024-01-26 16:14:01 -05:00
Dave Richer
c1f6d06128 - Finish department cycle times.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-26 16:10:24 -05:00
Dave Richer
120a8a4576 - Finish department cycle times.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-26 16:09:46 -05:00
Dave Richer
89224e871c - Progress Update
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-26 12:57:06 -05:00
Allan Carr
3dfd45ca0f Merged in release/2024-01-26 (pull request #1217)
Release/2024 01 26
2024-01-26 16:53:56 +00:00
Allan Carr
fa0d472fb6 Merged in feature/IO-2543-AR-Aging (pull request #1216)
IO-2543 Add wrap to space components to maintain limits of card

Approved-by: Dave Richer
2024-01-26 16:52:59 +00:00
Dave Richer
b0f4ad7e4f - Progress Update
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-26 11:48:43 -05:00
Allan Carr
7503d86c69 IO-2543 Add wrap to space components to maintain limits of card 2024-01-26 08:42:57 -08:00
Dave Richer
efd1c17033 - Revert Hasura
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-26 08:21:43 -08:00
Dave Richer
c7a0072f2d - Revert Hasura
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-26 11:19:03 -05:00
Allan Carr
908942ec09 IO-2543 Revert Lost Sales for Datedisable 2024-01-26 08:11:29 -08:00
Allan Carr
e4d3b53349 Merged in feature/IO-2543-AR-Aging (pull request #1215)
IO-2543 Revert Lost Sales for Datedisable

Approved-by: Dave Richer
2024-01-26 16:10:08 +00:00
Allan Carr
86e748502c Merged in release/2024-01-26 (pull request #1212)
Release/2024 01 26

Approved-by: Dave Richer
2024-01-25 20:27:32 +00:00
Dave Richer
03ce5458b5 Merge branch 'release/2024-01-26' into feature/IO-1532-Tracking-Department-Cycle-Times 2024-01-25 14:00:26 -05:00
Dave Richer
61c03ee206 Merge branch 'release/2024-01-26' into feature/IO-1532-Tracking-Department-Cycle-Times 2024-01-25 13:56:25 -05:00
Dave Richer
0e4f5b8b2a - progress update.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-25 13:35:20 -05:00
Allan Carr
b5d4944ad8 Merged in feature/IO-2543-AR-Aging (pull request #1210)
IO-2543 AR Aging

Approved-by: Dave Richer
Approved-by: Patrick Fic
2024-01-25 18:33:20 +00:00
Patrick Fic
50f84d40e1 Allow negative balance for AR. 2024-01-25 10:30:03 -08:00
Dave Richer
a394d6b37e - Major Progress Commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-25 12:38:32 -05:00
Dave Richer
f8408908b2 - Major Progress Commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-24 21:40:05 -05:00
Allan Carr
eb8519dc1d IO-2543 Move queries to jobs.queries.js and correct layout 2024-01-24 18:37:41 -08:00
Allan Carr
36dd97394f IO-2543 Adjust for having no dates 2024-01-24 16:15:32 -08:00
Allan Carr
03d4e4dcd1 IO-2543 AR Aging 2024-01-24 16:01:48 -08:00
Dave Richer
6489a8666f - Progress
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-24 18:39:59 -05:00
Dave Richer
5ea64ed805 - Progress
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-24 17:18:43 -05:00
Dave Richer
d740446ccb - Progress
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-24 10:07:07 -05:00
Dave Richer
d0a2bb7da0 - human readable dates
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-23 12:58:57 -05:00
Dave Richer
5de4ef5d83 - human readable dates
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-23 12:54:38 -05:00
Dave Richer
f59bdf9030 - Progress Update
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-23 12:35:58 -05:00
Dave Richer
cfe0727447 - Rough in front end / backend
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-23 10:20:26 -05:00
Dave Richer
09d112350a - Rough in front end / backend
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-23 01:37:11 -05:00
Dave Richer
52f8eabd2b - Finish cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-23 00:02:18 -05:00
Dave Richer
a162b275a3 - Finish cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-22 23:11:10 -05:00
Dave Richer
2e7232bb65 - Finish cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-22 23:00:31 -05:00
Dave Richer
82dc9e1c56 - Finish cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-22 19:07:16 -05:00
Dave Richer
272a3f579a - Minor cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-22 18:53:57 -05:00
Dave Richer
ff1ceb20cb - Minor cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-22 18:39:27 -05:00
Dave Richer
343179d4fe - Minor cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-22 18:05:35 -05:00
Dave Richer
eabbc2211b - Minor cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-22 17:21:46 -05:00
Dave Richer
7f587680ca - Scaffolding.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-22 16:38:26 -05:00
Patrick Fic
d61ed03ef1 Merge in EULA changes & update to AR Tracking schema. 2024-01-22 11:52:42 -08:00
Dave Richer
166efdc877 progressUpdate
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-22 14:25:21 -05:00
Allan Carr
9274742520 Merged in release/2024-01-19 (pull request #1206)
Release/2024 01 19
2024-01-20 01:03:59 +00:00
Patrick Fic
9b4c85c9e3 IO-2543 additional schema correction. 2024-01-19 13:50:14 -08:00
Patrick Fic
0f85250d40 Merged in release/2024-01-19 (pull request #1205)
Release/2024 01 19
2024-01-19 21:19:03 +00:00
Allan Carr
66c64ce9e0 Merged in feature/IO-2599-Techstation-Linking (pull request #1204)
IO-2599 Remove Space and put Div in to space components

Approved-by: Patrick Fic
2024-01-19 21:18:39 +00:00
Patrick Fic
c9cbffdec8 IO-2543 Schema changes for AR calculation. 2024-01-19 13:15:06 -08:00
Allan Carr
ca3145ce0f IO-2599 Remove Space and put Div in to space components 2024-01-19 12:19:07 -08:00
Allan Carr
f04bc59966 Merged in release/2024-01-19 (pull request #1184)
Release/2024 01 19

Approved-by: Dave Richer
2024-01-19 16:59:22 +00:00
Allan Carr
52f9106776 Merged in feature/IO-2608-Employee-Filter-and-Sort (pull request #1197)
IO-2608 Filter and Sort columns in Employee table

Approved-by: Dave Richer
2024-01-19 16:58:45 +00:00
Allan Carr
1a2cc5623d Merged in feature/IO-2598-Tech-Create-IOU (pull request #1201)
IO-2598 Restrict IOU from Tech Console

Approved-by: Dave Richer
2024-01-19 16:58:13 +00:00
Allan Carr
d06037df1f IO-2598 Restrict IOU from Tech Console 2024-01-19 08:47:17 -08:00
Allan Carr
cb8632641e IO-2608 Filter and Sort columns in Employee table 2024-01-18 17:28:46 -08:00
Dave Richer
adbfcddd9d Merged in feature/IO-1828-Beta-Updates-To-Test (pull request #1194)
Feature/IO-1828 Beta Updates To Test
2024-01-18 21:49:33 +00:00
Dave Richer
e3aea55e91 Merged in feature/IO-1828-Beta-Updates-To-Test (pull request #1193)
Feature/IO-1828 Beta Updates To Test
2024-01-18 21:48:51 +00:00
Dave Richer
e6e24dae4b Merged in feature/IO-1828-Beta-Updates-To-Test (pull request #1192)
- update handleBeta
2024-01-18 21:36:14 +00:00
Dave Richer
bf6b1c202f - update handleBeta
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-18 16:35:30 -05:00
Dave Richer
4ad32117c3 Merged in feature/IO-1828-Beta-Updates-To-Test (pull request #1191)
- update handleBeta
2024-01-18 21:23:38 +00:00
Dave Richer
0cab47f984 - update handleBeta
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-18 16:22:58 -05:00
Dave Richer
befb8009e0 Merged in feature/IO-1828-Beta-Updates-To-Test (pull request #1190)
- update handleBeta
2024-01-18 21:16:22 +00:00
Dave Richer
829e611692 - update handleBeta
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-18 16:16:06 -05:00
Patrick Fic
e0e62a52be Cherry pick schema chengs from IO-2477 2024-01-18 13:08:39 -08:00
Dave Richer
41bec72390 Merged in feature/IO-1828-Beta-Updates-To-Test (pull request #1189)
- update handleBeta
2024-01-18 21:07:45 +00:00
Dave Richer
23c0f8e383 - update handleBeta
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-18 16:07:17 -05:00
Allan Carr
764a89e5bf Merged in feature/IO-2599-Techstation-Linking (pull request #1183)
Feature/IO-2599 Techstation Linking

Approved-by: Dave Richer
2024-01-18 19:33:20 +00:00
Allan Carr
f4908ed265 IO-2599 Tech Station Linking 2024-01-18 11:30:58 -08:00
Dave Richer
fcd9c19f0b Merged in feature/IO-1828-Beta-Switch-For-Test (pull request #1180)
Feature/IO-1828 Beta Switch For Test
2024-01-18 18:20:51 +00:00
Dave Richer
430823dde0 - remove source maps from prod
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-18 13:20:04 -05:00
Dave Richer
7e919a7221 Merge branch 'master' into feature/IO-1828-Beta-Switch-For-Test 2024-01-18 13:19:09 -05:00
Allan Carr
7e7f055c34 Merged in feature/IO-2600-Tech-Job-Logout-Console-Log (pull request #1167)
IO-2600 Tech Console Job Logout Console Log

Approved-by: Dave Richer
2024-01-18 01:51:46 +00:00
Allan Carr
6e04ed7c83 Merged in feature/IO-2589-Manual-Job-Ownr-Validation (pull request #1168)
IO-2589 Allow Company Only for Customer Creation

Approved-by: Dave Richer
2024-01-18 01:51:30 +00:00
Allan Carr
5ec07f9620 Merged in feature/IO-2606-Schedule-Appointment-Modal-Close (pull request #1171)
IO-2606 Modal Closeable for Schedule Appointment

Approved-by: Dave Richer
2024-01-18 01:51:06 +00:00
Allan Carr
d3da4a8a1e Merged in feature/IO-2601-Tech-Console-Titles (pull request #1177)
IO-2601 Tech Console Titles

Approved-by: Dave Richer
2024-01-18 01:50:40 +00:00
Allan Carr
9ee10dc5f8 IO-2601 Tech Console Titles 2024-01-17 17:52:14 -08:00
Allan Carr
572963d987 IO-2606 Modal Closeable for Schedule Appointment 2024-01-17 17:09:17 -08:00
Allan Carr
636be8989e IO-2589 Allow Company Only for Customer Creation 2024-01-17 14:22:47 -08:00
Allan Carr
dea7fd71ef IO-2600 Tech Console Job Logout Console Log 2024-01-17 09:41:57 -08:00
Allan Carr
be08ed8551 Merged in feature/IO-2531-Retain-Filter-State-on-Jobs-Page (pull request #1166)
IO-2531 Retain Filtered Statue for Jobs Pages

Approved-by: Dave Richer
2024-01-16 20:22:04 +00:00
Allan Carr
65ce588287 Merged in feature/IO-2603-Open-Orders-Excel (pull request #1164)
IO-2603 Open Orders Excel

Approved-by: Dave Richer
2024-01-16 20:20:25 +00:00
Allan Carr
0cc367b25e IO-2531 Remove Console Log 2024-01-16 12:12:53 -08:00
Allan Carr
2ce8549502 IO-2531 Retain Filtered Statue for Jobs Pages 2024-01-16 12:11:18 -08:00
Allan Carr
7245d4eab2 IO-2603 Open Orders Excel 2024-01-15 18:54:28 -08:00
Allan Carr
260607cb72 Merged in release/2024-01-12 (pull request #1161)
IO-2520 Adjust to imexshopid instead of shopname & prettify
2024-01-13 05:12:28 +00:00
Allan Carr
ebe5c5b113 IO-2520 Adjust to imexshopid instead of shopname & prettify 2024-01-12 21:06:39 -08:00
Allan Carr
810738539b Merged in release/2024-01-12 (pull request #1160)
IO-2520 Adjust to imexshopid instead of shopname & prettify
2024-01-13 05:05:25 +00:00
Allan Carr
80539949fb Merged in feature/IO-2520-Kaizen-Data-Pump (pull request #1159)
IO-2520 Adjust to imexshopid instead of shopname & prettify
2024-01-13 05:04:54 +00:00
Dave Richer
525182c2a7 Merged in feature/IO-1828-Beta-Switch-For-Test (pull request #1158)
- Add in the Beta Switch on test
2024-01-13 02:07:43 +00:00
Dave Richer
3704c0cb12 - Add in the Beta Switch on test
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-12 20:50:40 -05:00
Dave Richer
c8c844cfba Merged in feature/IO-1828-Beta-Switch-For-Test (pull request #1157)
- Add in the Beta Switch on test
2024-01-13 01:48:01 +00:00
Dave Richer
4c4e16b0c9 - Add in the Beta Switch on test
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-12 20:47:06 -05:00
Allan Carr
69f727c4e5 Merged in release/2024-01-12 (pull request #1156)
IO-2520 Add in Server Key format
2024-01-13 00:34:42 +00:00
Allan Carr
04cff4acb1 IO-2520 Add in Server Key format 2024-01-12 16:26:09 -08:00
Allan Carr
4e4fcc3ae4 Merged in release/2024-01-12 (pull request #1154)
IO-2520 Add in Server Key format
2024-01-13 00:25:24 +00:00
Allan Carr
485f9d6025 Merged in feature/IO-2520-Kaizen-Data-Pump (pull request #1153)
IO-2520 Add in Server Key format
2024-01-13 00:24:19 +00:00
Allan Carr
a697ade93a Merged in release/2024-01-12 (pull request #1152)
IO-2602 Beta domain
2024-01-13 00:02:58 +00:00
Allan Carr
7db07b5a94 Merged in release/2024-01-12 (pull request #1151)
IO-2602 Beta domain
2024-01-12 23:41:38 +00:00
Allan Carr
9ec50875a2 Merged in feature/IO-2602-BETA-domain (pull request #1150)
IO-2602 Beta domain
2024-01-12 23:39:22 +00:00
Allan Carr
02b6875eec IO-2602 Beta domain 2024-01-12 15:41:10 -08:00
Allan Carr
e7e4c534bc Merged in release/2024-01-12 (pull request #1149)
Release/2024 01 12
2024-01-12 19:59:15 +00:00
Allan Carr
e438fa1d99 Merged in release/2024-01-12 (pull request #1148)
Release/2024 01 12
2024-01-12 18:50:45 +00:00
Allan Carr
4abbf50a46 Merged in feature/IO-2520-Kaizen-Data-Pump (pull request #1145)
IO-2520 Kaizen Data Pump
2024-01-12 18:16:07 +00:00
Allan Carr
3e9279d89a IO-2520 Change Query Time Bound 2024-01-09 12:00:24 -08:00
Allan Carr
1305277c09 IO-2520 Kaizen Data Pump 2024-01-09 11:08:02 -08:00
Allan Carr
3c47c672d4 Merged in feature/IO-2518-Warning-on-VIN-Length (pull request #1143)
IO-2518 Null coalesce for v_vin for warning
2024-01-08 19:18:24 +00:00
Allan Carr
258d99cd41 Merged in release/2024-01-12 (pull request #1142)
IO-2518 Null coalesce for v_vin for warning
2024-01-08 18:41:15 +00:00
Allan Carr
83356fa4ef Merged in feature/IO-2518-Warning-on-VIN-Length (pull request #1141)
IO-2518 Null coalesce for v_vin for warning
2024-01-08 18:40:43 +00:00
Allan Carr
25429e78f8 IO-2518 Null coalesce for v_vin for warning 2024-01-08 10:37:19 -08:00
Allan Carr
de90bd1bb0 Merged in release/2024-01-05 (pull request #1137)
Release/2024 01 05
2024-01-05 22:43:56 +00:00
Allan Carr
aa6cb4c1d2 Merged in release/2024-01-05 (pull request #1136)
IO-2514 Only Unique items in Menu
2024-01-05 21:08:10 +00:00
Allan Carr
e871ba600f Merged in feature/IO-2514-Production-Board-Estimators (pull request #1135)
IO-2514 Only Unique items in Menu
2024-01-05 21:07:36 +00:00
Allan Carr
fe3698980d IO-2514 Only Unique items in Menu 2024-01-05 13:09:15 -08:00
Allan Carr
89b640f71c Merged in release/2024-01-05 (pull request #1133)
Release/2024 01 05
2024-01-05 20:04:10 +00:00
Allan Carr
d13a9cd04a Merged in feature/IO-2522-Load-Level-Table (pull request #1134)
IO-2522 Load Level Table Change
2024-01-05 20:02:26 +00:00
Allan Carr
c0dab92d0e IO-2522 Load Level Table Change 2024-01-05 12:01:47 -08:00
Patrick Fic
9c897972ad Minor intellipay change. 2024-01-05 08:53:17 -08:00
Allan Carr
307e244475 Merged in feature/IO-2514-Production-Board-Estimators (pull request #1130)
Feature/IO-2514 Production Board Estimators

Approved-by: Dave Richer
2024-01-05 00:57:44 +00:00
Allan Carr
9d3aca646b IO-2514 Production Board Estimator filter by table data 2024-01-03 10:55:45 -08:00
Allan Carr
e6e61466df Merged in feature/IO-2518-Warning-on-VIN-Length (pull request #1129)
IO-2518 Dealership Vin Warning

Approved-by: Dave Richer
2024-01-02 15:01:51 +00:00
Allan Carr
db7f9fe2ab Merged in feature/IO-2517-All-Courtesy-Car-Warning (pull request #1128)
IO-2517 All Courtesy Car Warning Indicator

Approved-by: Dave Richer
2024-01-02 15:01:22 +00:00
Allan Carr
ded798fdf1 IO-2518 Dealership Vin Warning 2023-12-29 16:37:34 -08:00
Allan Carr
bfe94e3068 IO-2517 Add same check within C/C form 2023-12-29 16:17:37 -08:00
Allan Carr
823f07409a IO-2517 All Courtesy Car Warning Indicator 2023-12-29 16:03:40 -08:00
Allan Carr
1a4bc720c2 Merged in release/2023-12-29 (pull request #1127)
Release/2023 12 29
2023-12-29 22:08:52 +00:00
Allan Carr
73cacdec24 Merged in release/2023-12-29 (pull request #1126)
Release/2023 12 29
2023-12-29 21:29:21 +00:00
Allan Carr
b0d077e104 IO-2514 Production Board Estimators 2023-12-28 10:12:59 -08:00
Allan Carr
ea72d44b42 Merged in release/2023-12-15 (pull request #1119)
IO-2501 Correct for missing query variables
2023-12-22 04:32:06 +00:00
Allan Carr
d1b9b5546b Merged in release/2023-12-15 (pull request #1117)
IO-2505 Conversation List Print
2023-12-21 17:39:49 +00:00
Allan Carr
84f0affaed Merged in release/2023-12-15 (pull request #1115)
Release/2023 12 15
2023-12-20 21:04:54 +00:00
Allan Carr
fc4b5c6b1d Merged in release/2023-12-15 (pull request #1112)
IO-2506 Correct for variable immutibility and nested ifs
2023-12-19 17:17:16 +00:00
Allan Carr
2c232a71d5 Merged in release/2023-12-15 (pull request #1110)
Release/2023 12 15
2023-12-19 16:38:10 +00:00
Allan Carr
f8e1758788 Merged in revert-pr-1097 (pull request #1098)
Revert "Revert "Revert "Revert "Fix issues with limits.
2023-12-01 02:37:45 +00:00
Allan Carr
5c95c72f40 Revert "Revert "Revert "Revert "Fix issues with limits. (pull request #1097)" 2023-12-01 02:37:10 +00:00
Allan Carr
98f816b069 Merged in revert-pr-1091 (pull request #1097)
Revert "Revert "Revert "Fix issues with limits.
2023-11-30 20:23:05 +00:00
Allan Carr
3ca6308dd2 Revert "Revert "Revert "Fix issues with limits. (pull request #1091)" 2023-11-30 20:22:42 +00:00
Allan Carr
a2c2aa11ac Merged in release/2023-12-01 (pull request #1096)
Release/2023 12 01
2023-11-30 16:58:33 +00:00
Allan Carr
b5b772d0c2 Merged in release/2023-12-01 (pull request #1093)
IO-2465 Adjust Headerfile override and comment out line
2023-11-30 01:16:39 +00:00
Allan Carr
4d8a2e635c Merged in revert-pr-1090 (pull request #1091)
Revert "Revert "Fix issues with limits.
2023-11-30 00:25:08 +00:00
Allan Carr
0852d55837 Revert "Revert "Fix issues with limits. (pull request #1090)" 2023-11-30 00:22:20 +00:00
Allan Carr
4c38ddf3cd Merged in revert-pr-1089 (pull request #1090)
Revert "Fix issues with limits.
2023-11-29 23:55:28 +00:00
Allan Carr
e15edeadb5 Revert "Fix issues with limits. (pull request #1089)" 2023-11-29 23:54:37 +00:00
Dave Richer
422c7baada Merged in release/2023-12-01 (pull request #1089)
Fix issues with limits.

Approved-by: Allan Carr
2023-11-29 22:30:12 +00:00
Dave Richer
2a2f8e51b3 Merged in release/2023-12-01 (pull request #1087)
Release/2023 12 01

Approved-by: Allan Carr
2023-11-29 21:27:34 +00:00
Dave Richer
85b1875a22 Merged in release/2023-12-01 (pull request #1085)
Release/2023 12 01

Approved-by: Allan Carr
2023-11-29 18:59:37 +00:00
300 changed files with 64084 additions and 5421 deletions

View File

@@ -42,31 +42,22 @@ jobs:
app-build:
docker:
- image: cimg/node:16.15.0
resource_class: large
working_directory: ~/repo/client
steps:
- checkout:
path: ~/repo
- restore_cache:
name: Restore Yarn Package Cache
keys:
- yarn-packages-{{ checksum "yarn.lock" }}
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
- save_cache:
name: Save Yarn Package Cache
key: yarn-packages-{{ checksum "yarn.lock" }}
paths:
- ~/.cache/yarn
command: npm i
- run: yarn run build
- run: npm run build
- aws-s3/sync:
from: build
to: "s3://imex-online-production/"
arguments: "--exclude '*.map'"
- jira/notify
test-hasura-migrate:
@@ -92,31 +83,22 @@ jobs:
test-app-build:
docker:
- image: cimg/node:16.15.0
resource_class: large
working_directory: ~/repo/client
steps:
- checkout:
path: ~/repo
- restore_cache:
name: Restore Yarn Package Cache
keys:
- yarn-packages-{{ checksum "yarn.lock" }}
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
- save_cache:
name: Save Yarn Package Cache
key: yarn-packages-{{ checksum "yarn.lock" }}
paths:
- ~/.cache/yarn
command: npm i
- run: yarn run build:test
- run: npm run build:test
- aws-s3/sync:
from: build
to: "s3://imex-online-test/"
arguments: "--exclude '*.map'"
- jira/notify
admin-app-build:
@@ -152,10 +134,6 @@ jobs:
workflows:
deploy_and_build:
jobs:
- api-deploy:
filters:
branches:
only: master
- app-build:
filters:
branches:
@@ -177,4 +155,4 @@ workflows:
#- admin-app-build:
#filters:
#branches:
#only: master
#only: master

16
.prettierrc.js Normal file
View File

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

View File

@@ -0,0 +1,189 @@
# Filters and Sorters
This documentation details the schema required for `.filters` files on the report server. It is used to dynamically
modify the graphQL query and provide the user more power over their reports.
For filters and sorters, valid types include (`type` key in the schema):
- string (default)
- number
- bool or boolean
- date
## Special Notes
- When passing the data to the template server, the property filters and sorters is added to the data object and will reflect the filters and sorters the user has selected
## High level Schema Overview
```javascript
const schema = {
"filters": [
{
"name": "jobs.joblines.mod_lb_hrs", // Name and path of the field in the graphQL query
"translation": "jobs.joblines.mod_lb_hrs_1", // Translation key for the label used in the GUI
"label": "mod_lb_hrs_1", // Label used in the case the GUI does not contain a translation
"type": "number" // Type of field, can be number or string currently
},
// ... more filters
],
"sorters": [
{
"name": "jobs.joblines.mod_lb_hrs", // Name and path of the field in the graphQL query
"translation": "jobs.joblines.mod_lb_hrs_1", // Translation key for the label used in the GUI
"label": "mod_lb_hrs_1", // Label used in the case the GUI does not contain a translation
"type": "number" // Type of field, can be number or string currently
},
// ... more sorters
],
"dates": {
// This is not yet implemented and will be added in a future release
}
}
```
## Filters
Filters effect the where clause of the graphQL query. They are used to filter the data returned from the server.
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",
"label": "Status",
"type": "string",
"reflector": {
"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`
The following cases are available
- `special.job_statuses` - This will reflect the statuses of the jobs table `bodyshop.md_ro_statuses.statuses'`
- `special.cost_centers` - This will reflect the cost centers `bodyshop.md_responsibility_centers.costs`
- `special.categories` - This will reflect the categories `bodyshop.md_categories`
- `special.insurance_companies` - This will reflect the insurance companies `bodyshop.md_ins_cos`'
- `special.employee_teams` - This will reflect the employee teams `bodyshop.employee_teams`
- `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.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",`
This will produce a where clause at the `joblines` level of the graphQL query,
```graphql
query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!) {
jobs(
where: {date_invoiced: {_is_null: true}, date_open: {_gte: $starttz, _lte: $endtz}, ro_number: {_is_null: false}, voided: {_eq: false}}
) {
joblines(
order_by: {line_no: asc}
where: {removed: {_eq: false}, mod_lb_hrs: {_lt: 3}}
) {
line_no
mod_lbr_ty
mod_lb_hrs
convertedtolbr
convertedtolbr_data
}
ownr_co_nm
ownr_fn
ownr_ln
plate_no
ro_number
status
v_make_desc
v_model_desc
v_model_yr
v_vin
v_color
}
}
```
### Path with brackets,top level
`"name": "[jobs].joblines.mod_lb_hrs",`
This will produce a where clause at the `jobs` level of the graphQL query.
```graphql
query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!) {
jobs(
where: {date_invoiced: {_is_null: true}, date_open: {_gte: $starttz, _lte: $endtz}, ro_number: {_is_null: false}, voided: {_eq: false}, joblines: {mod_lb_hrs: {_gt: 4}}}
) {
joblines(
order_by: {line_no: asc}
where: {removed: {_eq: false}}
) {
line_no
mod_lbr_ty
mod_lb_hrs
convertedtolbr
convertedtolbr_data
}
ownr_co_nm
ownr_fn
ownr_ln
plate_no
ro_number
status
v_make_desc
v_model_desc
v_model_yr
v_vin
v_color
}
}
```
## Known Caveats
- Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs`
is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not.
- The type object must be 'string' or 'number' or 'bool' or 'boolean' or 'date' and is case-sensitive.
- The `translation` key is used to look up the label in the GUI, if it is not found, the `label` key is used.
- 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
- Sorters follow the same schema as filters, however, they do not do square bracket wrapping to indicate level hoisting,
a filter added on `job.md_status` would be added at the top level, and a filter added on `jobs.joblines.mod_lb_hrs`
would be added at the `joblines` level.
- Most of the reports currently do sorting on a template level, this will need to change to actually see the results
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`.
```json
{
"name": "jobs.joblines.mod_lb_hrs",
"translation": "jobs.joblines.mod_lb_hrs_1",
"label": "mod_lb_hrs_1",
"type": "number",
"default": {
"order": 1,
"direction": "asc"
}
}
```

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
GENERATE_SOURCEMAP=false
GENERATE_SOURCEMAP=true
REACT_APP_GRAPHQL_ENDPOINT=https://db.imex.online/v1/graphql
REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.imex.online/v1/graphql
REACT_APP_GA_CODE=231103507

3
client/.gitignore vendored Normal file
View File

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

View File

@@ -1,25 +1,25 @@
// craco.config.js
const TerserPlugin = require("terser-webpack-plugin");
const CracoLessPlugin = require("craco-less");
const SentryWebpackPlugin = require("@sentry/webpack-plugin");
//const SentryWebpackPlugin = require("@sentry/webpack-plugin");
module.exports = {
plugins: [
{
plugin: SentryWebpackPlugin,
options: {
// sentry-cli configuration
authToken:
"6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260",
org: "snapt-software",
project: "imexonline",
release: process.env.REACT_APP_GIT_SHA,
// {
// plugin: SentryWebpackPlugin,
// options: {
// // sentry-cli configuration
// authToken:
// "6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260",
// org: "snapt-software",
// project: "imexonline",
// release: process.env.REACT_APP_GIT_SHA,
// webpack-specific configuration
include: ".",
ignore: ["node_modules", "webpack.config.js"],
},
},
// // webpack-specific configuration
// include: ".",
// ignore: ["node_modules", "webpack.config.js"],
// },
// },
{
plugin: CracoLessPlugin,
options: {

649
client/package-lock.json generated
View File

@@ -13,12 +13,14 @@
"@craco/craco": "^7.0.0",
"@fingerprintjs/fingerprintjs": "^3.4.2",
"@jsreport/browser-client": "^3.1.0",
"@sentry/react": "^7.40.0",
"@sentry/cli": "^2.27.0",
"@sentry/react": "^7.99.0",
"@sentry/tracing": "^7.40.0",
"@splitsoftware/splitio-react": "^1.8.1",
"@tanem/react-nprogress": "^5.0.8",
"antd": "^4.24.8",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0",
"axios": "^1.3.4",
"craco-less": "^2.0.0",
"dinero.js": "^1.9.1",
@@ -4214,40 +4216,195 @@
"integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==",
"license": "MIT"
},
"node_modules/@sentry/browser": {
"version": "7.40.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.40.0.tgz",
"integrity": "sha512-07rZ+cTcpmYB1r84/oZtmSPJJvLCxW8yIh/5s4MdKRyZpqIDKhOz6cCS/4j+l1V+MeLcNLZBjFtNdKA2eocTpg==",
"license": "MIT",
"node_modules/@sentry-internal/feedback": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.99.0.tgz",
"integrity": "sha512-exIO1o+bE0MW4z30FxC0cYzJ4ZHSMlDPMHCBDPzU+MWGQc/fb8s58QUrx5Dnm6HTh9G3H+YlroCxIo9u0GSwGQ==",
"dependencies": {
"@sentry/core": "7.40.0",
"@sentry/replay": "7.40.0",
"@sentry/types": "7.40.0",
"@sentry/utils": "7.40.0",
"tslib": "^1.9.3"
"@sentry/core": "7.99.0",
"@sentry/types": "7.99.0",
"@sentry/utils": "7.99.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@sentry-internal/feedback/node_modules/@sentry/core": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.99.0.tgz",
"integrity": "sha512-vOAtzcAXEUtS/oW7wi3wMkZ3hsb5Ch96gKyrrj/mXdOp2zrcwdNV6N9/pawq2E9P/7Pw8AXw4CeDZztZrjQLuA==",
"dependencies": {
"@sentry/types": "7.99.0",
"@sentry/utils": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/browser/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"license": "0BSD"
"node_modules/@sentry-internal/feedback/node_modules/@sentry/types": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.99.0.tgz",
"integrity": "sha512-94qwOw4w40sAs5mCmzcGyj8ZUu/KhnWnuMZARRq96k+SjRW/tHFAOlIdnFSrt3BLPvSOK7R3bVAskZQ0N4FTmA==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/feedback/node_modules/@sentry/utils": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.99.0.tgz",
"integrity": "sha512-cYZy5WNTkWs5GgggGnjfGqC44CWir0pAv4GVVSx0fsup4D4pMKBJPrtub15f9uC+QkUf3vVkqwpBqeFxtmJQTQ==",
"dependencies": {
"@sentry/types": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/replay-canvas": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.99.0.tgz",
"integrity": "sha512-PoIkfusToDq0snfl2M6HJx/1KJYtXxYhQplrn11kYadO04SdG0XGXf4h7wBTMEQ7LDEAtQyvsOu4nEQtTO3YjQ==",
"dependencies": {
"@sentry/core": "7.99.0",
"@sentry/replay": "7.99.0",
"@sentry/types": "7.99.0",
"@sentry/utils": "7.99.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/core": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.99.0.tgz",
"integrity": "sha512-vOAtzcAXEUtS/oW7wi3wMkZ3hsb5Ch96gKyrrj/mXdOp2zrcwdNV6N9/pawq2E9P/7Pw8AXw4CeDZztZrjQLuA==",
"dependencies": {
"@sentry/types": "7.99.0",
"@sentry/utils": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/types": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.99.0.tgz",
"integrity": "sha512-94qwOw4w40sAs5mCmzcGyj8ZUu/KhnWnuMZARRq96k+SjRW/tHFAOlIdnFSrt3BLPvSOK7R3bVAskZQ0N4FTmA==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/utils": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.99.0.tgz",
"integrity": "sha512-cYZy5WNTkWs5GgggGnjfGqC44CWir0pAv4GVVSx0fsup4D4pMKBJPrtub15f9uC+QkUf3vVkqwpBqeFxtmJQTQ==",
"dependencies": {
"@sentry/types": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/tracing": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.99.0.tgz",
"integrity": "sha512-z3JQhHjoM1KdM20qrHwRClKJrNLr2CcKtCluq7xevLtXHJWNAQQbafnWD+Aoj85EWXBzKt9yJMv2ltcXJ+at+w==",
"dependencies": {
"@sentry/core": "7.99.0",
"@sentry/types": "7.99.0",
"@sentry/utils": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/tracing/node_modules/@sentry/core": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.99.0.tgz",
"integrity": "sha512-vOAtzcAXEUtS/oW7wi3wMkZ3hsb5Ch96gKyrrj/mXdOp2zrcwdNV6N9/pawq2E9P/7Pw8AXw4CeDZztZrjQLuA==",
"dependencies": {
"@sentry/types": "7.99.0",
"@sentry/utils": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/tracing/node_modules/@sentry/types": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.99.0.tgz",
"integrity": "sha512-94qwOw4w40sAs5mCmzcGyj8ZUu/KhnWnuMZARRq96k+SjRW/tHFAOlIdnFSrt3BLPvSOK7R3bVAskZQ0N4FTmA==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/tracing/node_modules/@sentry/utils": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.99.0.tgz",
"integrity": "sha512-cYZy5WNTkWs5GgggGnjfGqC44CWir0pAv4GVVSx0fsup4D4pMKBJPrtub15f9uC+QkUf3vVkqwpBqeFxtmJQTQ==",
"dependencies": {
"@sentry/types": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/browser": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.99.0.tgz",
"integrity": "sha512-bgfoUv3wkwwLgN5YUOe0ibB3y268ZCnamZh6nLFqnY/UBKC1+FXWFdvzVON/XKUm62LF8wlpCybOf08ebNj2yg==",
"dependencies": {
"@sentry-internal/feedback": "7.99.0",
"@sentry-internal/replay-canvas": "7.99.0",
"@sentry-internal/tracing": "7.99.0",
"@sentry/core": "7.99.0",
"@sentry/replay": "7.99.0",
"@sentry/types": "7.99.0",
"@sentry/utils": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/browser/node_modules/@sentry/core": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.99.0.tgz",
"integrity": "sha512-vOAtzcAXEUtS/oW7wi3wMkZ3hsb5Ch96gKyrrj/mXdOp2zrcwdNV6N9/pawq2E9P/7Pw8AXw4CeDZztZrjQLuA==",
"dependencies": {
"@sentry/types": "7.99.0",
"@sentry/utils": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/browser/node_modules/@sentry/types": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.99.0.tgz",
"integrity": "sha512-94qwOw4w40sAs5mCmzcGyj8ZUu/KhnWnuMZARRq96k+SjRW/tHFAOlIdnFSrt3BLPvSOK7R3bVAskZQ0N4FTmA==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/browser/node_modules/@sentry/utils": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.99.0.tgz",
"integrity": "sha512-cYZy5WNTkWs5GgggGnjfGqC44CWir0pAv4GVVSx0fsup4D4pMKBJPrtub15f9uC+QkUf3vVkqwpBqeFxtmJQTQ==",
"dependencies": {
"@sentry/types": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/cli": {
"version": "1.74.6",
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-1.74.6.tgz",
"integrity": "sha512-pJ7JJgozyjKZSTjOGi86chIngZMLUlYt2HOog+OJn+WGvqEkVymu8m462j1DiXAnex9NspB4zLLNuZ/R6rTQHg==",
"dev": true,
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.27.0.tgz",
"integrity": "sha512-pc0opd71W8lGhYvmB1keQtJkarxzCS9f9ErKYv6TfXOOX6drvwkyA6vD/6xEnpzyvqGAuGRU4T4sEeLD3irwUQ==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"https-proxy-agent": "^5.0.0",
"mkdirp": "^0.5.5",
"node-fetch": "^2.6.7",
"npmlog": "^4.1.2",
"progress": "^2.0.3",
"proxy-from-env": "^1.1.0",
"which": "^2.0.2"
@@ -4256,7 +4413,124 @@
"sentry-cli": "bin/sentry-cli"
},
"engines": {
"node": ">= 8"
"node": ">= 10"
},
"optionalDependencies": {
"@sentry/cli-darwin": "2.27.0",
"@sentry/cli-linux-arm": "2.27.0",
"@sentry/cli-linux-arm64": "2.27.0",
"@sentry/cli-linux-i686": "2.27.0",
"@sentry/cli-linux-x64": "2.27.0",
"@sentry/cli-win32-i686": "2.27.0",
"@sentry/cli-win32-x64": "2.27.0"
}
},
"node_modules/@sentry/cli-darwin": {
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.27.0.tgz",
"integrity": "sha512-/DOZlN5rK19g7YP2OaVNauQhUrRfJ88RDr6qURFiqdxYHDc3isPFGHZJmeZBTwOnDDepyZb4XLaOyfwvAOxHig==",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-linux-arm": {
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.27.0.tgz",
"integrity": "sha512-JmMQ9zgFhkZUEN5WIYuJisu4Jif/ThRHDjbsbXBRbUkkgRn88hgUfg299djMvlZZxjpl3K9AEua+1TIUeQd0Sg==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux",
"freebsd"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-linux-arm64": {
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.27.0.tgz",
"integrity": "sha512-f+zuB9XGfB8pNamNgSDhqsavuLuzi6saZxbr3uQf30bA5AESI5hspOd1zPcidOORCVZxiPzQe3+T7avBI1XLuw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux",
"freebsd"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-linux-i686": {
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.27.0.tgz",
"integrity": "sha512-/4eyz7jnYp20mZqNtpvCEBkxFW0nEjEZRo2BiASQ5/7K8CmoJRe1vhpDA0WOfzi1zTFIfpdE1/RZm2CjHS6DHQ==",
"cpu": [
"x86",
"ia32"
],
"optional": true,
"os": [
"linux",
"freebsd"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-linux-x64": {
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.27.0.tgz",
"integrity": "sha512-ptu7wXecnYssihzHlxEOaqbFHWmNEfbepBKGXTdWK2kC+D51+7yHsR9xRdThwVID1bisFgjAveKmBQjmKuXjHQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux",
"freebsd"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-win32-i686": {
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.27.0.tgz",
"integrity": "sha512-Db4/xmdE5qV4Aq7Yc8vRw22Y46JJdGMdsMsl5jIf0GVSQPgO23O/2uTiDGpPOdeq91K9EtvpH1zQfDLIfLMaXw==",
"cpu": [
"x86",
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-win32-x64": {
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.27.0.tgz",
"integrity": "sha512-q7y/BH4iGfs0TD5PXh2Q8oqnTbOIufoT1NWJcKqvZcOiqCLK3PNUiq7xUeX1PMTrFYAh3Bm6EekOnMavqvbGmg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/core": {
@@ -4280,16 +4554,15 @@
"license": "0BSD"
},
"node_modules/@sentry/react": {
"version": "7.40.0",
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.40.0.tgz",
"integrity": "sha512-7yYagpOCdsXnVTtLL8Y7wAf2xXgsk2ncuju3O/G4kEckkLewZWmQeoknOSGFlAgVdGNhTaXc2WGzgOiBMOkhug==",
"license": "MIT",
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.99.0.tgz",
"integrity": "sha512-RtHwgzMHJhzJfSQpVG0SDPQYMTGDX3Q37/YWI59S4ALMbSW4/F6n/eQAvGVYZKbh2UCSqgFuRWaXOYkSZT17wA==",
"dependencies": {
"@sentry/browser": "7.40.0",
"@sentry/types": "7.40.0",
"@sentry/utils": "7.40.0",
"hoist-non-react-statics": "^3.3.2",
"tslib": "^1.9.3"
"@sentry/browser": "7.99.0",
"@sentry/core": "7.99.0",
"@sentry/types": "7.99.0",
"@sentry/utils": "7.99.0",
"hoist-non-react-statics": "^3.3.2"
},
"engines": {
"node": ">=8"
@@ -4298,26 +4571,82 @@
"react": "15.x || 16.x || 17.x || 18.x"
}
},
"node_modules/@sentry/react/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"license": "0BSD"
"node_modules/@sentry/react/node_modules/@sentry/core": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.99.0.tgz",
"integrity": "sha512-vOAtzcAXEUtS/oW7wi3wMkZ3hsb5Ch96gKyrrj/mXdOp2zrcwdNV6N9/pawq2E9P/7Pw8AXw4CeDZztZrjQLuA==",
"dependencies": {
"@sentry/types": "7.99.0",
"@sentry/utils": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/react/node_modules/@sentry/types": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.99.0.tgz",
"integrity": "sha512-94qwOw4w40sAs5mCmzcGyj8ZUu/KhnWnuMZARRq96k+SjRW/tHFAOlIdnFSrt3BLPvSOK7R3bVAskZQ0N4FTmA==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/react/node_modules/@sentry/utils": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.99.0.tgz",
"integrity": "sha512-cYZy5WNTkWs5GgggGnjfGqC44CWir0pAv4GVVSx0fsup4D4pMKBJPrtub15f9uC+QkUf3vVkqwpBqeFxtmJQTQ==",
"dependencies": {
"@sentry/types": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/replay": {
"version": "7.40.0",
"resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.40.0.tgz",
"integrity": "sha512-Y9Kvo9jKouUdrHQhHVv5SmWZClF5o7BFI6oVpLlv4zXORPQlyoZONM/9sxiMvvH73alDSpxzCoxyhlypAOH4ww==",
"license": "MIT",
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.99.0.tgz",
"integrity": "sha512-gyN/I2WpQrLAZDT+rScB/0jnFL2knEVBo8U8/OVt8gNP20Pq8T/rDZKO/TG0cBfvULDUbJj2P4CJryn2p/O2rA==",
"dependencies": {
"@sentry/core": "7.40.0",
"@sentry/types": "7.40.0",
"@sentry/utils": "7.40.0"
"@sentry-internal/tracing": "7.99.0",
"@sentry/core": "7.99.0",
"@sentry/types": "7.99.0",
"@sentry/utils": "7.99.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@sentry/replay/node_modules/@sentry/core": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.99.0.tgz",
"integrity": "sha512-vOAtzcAXEUtS/oW7wi3wMkZ3hsb5Ch96gKyrrj/mXdOp2zrcwdNV6N9/pawq2E9P/7Pw8AXw4CeDZztZrjQLuA==",
"dependencies": {
"@sentry/types": "7.99.0",
"@sentry/utils": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/replay/node_modules/@sentry/types": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.99.0.tgz",
"integrity": "sha512-94qwOw4w40sAs5mCmzcGyj8ZUu/KhnWnuMZARRq96k+SjRW/tHFAOlIdnFSrt3BLPvSOK7R3bVAskZQ0N4FTmA==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/replay/node_modules/@sentry/utils": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.99.0.tgz",
"integrity": "sha512-cYZy5WNTkWs5GgggGnjfGqC44CWir0pAv4GVVSx0fsup4D4pMKBJPrtub15f9uC+QkUf3vVkqwpBqeFxtmJQTQ==",
"dependencies": {
"@sentry/types": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/tracing": {
"version": "7.40.0",
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.40.0.tgz",
@@ -4381,6 +4710,27 @@
"node": ">= 8"
}
},
"node_modules/@sentry/webpack-plugin/node_modules/@sentry/cli": {
"version": "1.77.3",
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-1.77.3.tgz",
"integrity": "sha512-c3eDqcDRmy4TFz2bFU5Y6QatlpoBPPa8cxBooaS4aMQpnIdLYPF1xhyyiW0LQlDUNc3rRjNF7oN5qKoaRoMTQQ==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"https-proxy-agent": "^5.0.0",
"mkdirp": "^0.5.5",
"node-fetch": "^2.6.7",
"progress": "^2.0.3",
"proxy-from-env": "^1.1.0",
"which": "^2.0.2"
},
"bin": {
"sentry-cli": "bin/sentry-cli"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@sinclair/typebox": {
"version": "0.24.51",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz",
@@ -6112,12 +6462,21 @@
"@apollo/client": "^3.0.0"
}
},
"node_modules/aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"dev": true,
"license": "ISC"
"node_modules/apollo-link-sentry": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/apollo-link-sentry/-/apollo-link-sentry-3.3.0.tgz",
"integrity": "sha512-wLffWmo5sRw3rHN1Ck6azM0oxObvtaBBf3AC8cLX4SxhyjmkRIagGDji6CFkyAhxupPz0b9/H1u4Ocx+63lNug==",
"dependencies": {
"deepmerge": "^4.2.2",
"dot-prop": "^6.0.0",
"tslib": "^2.0.3",
"zen-observable-ts": "^1.2.5"
},
"peerDependencies": {
"@apollo/client": "^3.2.3",
"@sentry/browser": "^7.41.0",
"graphql": "15 - 16"
}
},
"node_modules/arch": {
"version": "2.2.0",
@@ -6140,17 +6499,6 @@
],
"license": "MIT"
},
"node_modules/are-we-there-yet": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz",
"integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==",
"dev": true,
"license": "ISC",
"dependencies": {
"delegates": "^1.0.0",
"readable-stream": "^2.0.6"
}
},
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
@@ -7571,16 +7919,6 @@
"node": ">=4"
}
},
"node_modules/code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/collect-v8-coverage": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
@@ -7743,13 +8081,6 @@
"node": ">=0.8"
}
},
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
"dev": true,
"license": "ISC"
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -8911,13 +9242,6 @@
"node": ">=0.4.0"
}
},
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
"dev": true,
"license": "MIT"
},
"node_modules/denque": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz",
@@ -9218,6 +9542,28 @@
"tslib": "^2.0.3"
}
},
"node_modules/dot-prop": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz",
"integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==",
"dependencies": {
"is-obj": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/dot-prop/node_modules/is-obj": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
"engines": {
"node": ">=8"
}
},
"node_modules/dotenv": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz",
@@ -10464,6 +10810,8 @@
},
"node_modules/eventemitter2": {
"version": "6.4.7",
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz",
"integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==",
"dev": true,
"license": "MIT"
},
@@ -11283,74 +11631,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gauge": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
"integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==",
"dev": true,
"license": "ISC",
"dependencies": {
"aproba": "^1.0.3",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.0",
"object-assign": "^4.1.0",
"signal-exit": "^3.0.0",
"string-width": "^1.0.1",
"strip-ansi": "^3.0.1",
"wide-align": "^1.1.0"
}
},
"node_modules/gauge/node_modules/ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/gauge/node_modules/is-fullwidth-code-point": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"number-is-nan": "^1.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/gauge/node_modules/string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==",
"dev": true,
"license": "MIT",
"dependencies": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
"strip-ansi": "^3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/gauge/node_modules/strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^2.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -11753,13 +12033,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
"dev": true,
"license": "ISC"
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
@@ -15744,19 +16017,6 @@
"node": ">=8"
}
},
"node_modules/npmlog": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"dev": true,
"license": "ISC",
"dependencies": {
"are-we-there-yet": "~1.1.2",
"console-control-strings": "~1.1.0",
"gauge": "~2.7.3",
"set-blocking": "~2.0.0"
}
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
@@ -15769,16 +16029,6 @@
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/nwsapi": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.1.tgz",
@@ -17794,7 +18044,6 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
@@ -20526,13 +20775,6 @@
"node": ">= 0.8.0"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"dev": true,
"license": "ISC"
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -22110,19 +22352,6 @@
"is-typedarray": "^1.0.0"
}
},
"node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/typescript-compare": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz",
@@ -23080,16 +23309,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"node_modules/wildcard": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz",

View File

@@ -9,12 +9,14 @@
"@craco/craco": "^7.0.0",
"@fingerprintjs/fingerprintjs": "^3.4.2",
"@jsreport/browser-client": "^3.1.0",
"@sentry/react": "^7.40.0",
"@sentry/cli": "^2.27.0",
"@sentry/react": "^7.99.0",
"@sentry/tracing": "^7.40.0",
"@splitsoftware/splitio-react": "^1.8.1",
"@tanem/react-nprogress": "^5.0.8",
"antd": "^4.24.8",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0",
"axios": "^1.3.4",
"craco-less": "^2.0.0",
"dinero.js": "^1.9.1",
@@ -88,13 +90,14 @@
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "craco start",
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build && npm run sentry:sourcemaps",
"build:test": "env-cmd -f .env.test npm run build",
"build-deploy:test": "npm run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'",
"buildcra": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
"test": "cypress open",
"eject": "react-scripts eject",
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular ."
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .",
"sentry:sourcemaps": "sentry-cli sourcemaps inject --org imex --project imexonline ./build && sentry-cli sourcemaps upload --org imex --project imexonline ./build"
},
"eslintConfig": {
"extends": [

View File

@@ -8,6 +8,7 @@ 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";
moment.locale("en-US");
@@ -18,7 +19,7 @@ export const factory = SplitSdk({
},
});
export default function AppContainer() {
function AppContainer() {
const { t } = useTranslation();
return (
@@ -42,3 +43,5 @@ export default function AppContainer() {
</ApolloProvider>
);
}
export default Sentry.withProfiler(AppContainer);

View File

@@ -22,6 +22,7 @@ import {
} from "../redux/user/user.selectors";
import PrivateRoute from "../utils/private-route";
import "./App.styles.scss";
import handleBeta from "../utils/handleBeta";
const ResetPassword = lazy(() =>
import("../pages/reset-password/reset-password.component")
@@ -57,7 +58,6 @@ export function App({
if (!navigator.onLine) {
setOnline(false);
}
checkUserSession();
}, [checkUserSession, setOnline]);
@@ -73,6 +73,7 @@ export function App({
window.addEventListener("online", function (e) {
setOnline(true);
});
useEffect(() => {
if (currentUser.authorized && bodyshop) {
client.setAttribute("imexshopid", bodyshop.imexshopid);
@@ -107,6 +108,8 @@ export function App({
/>
);
handleBeta();
return (
<Switch>
<Suspense fallback={<LoadingSpinner message="ImEX Online" />}>

View File

@@ -1,21 +1,21 @@
import { Input, Table, Checkbox, Card, Space } from "antd";
import { Card, Checkbox, Input, Space, Table } from "antd";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort, dateSort } from "../../utils/sorters";
import PayableExportButton from "../payable-export-button/payable-export-button.component";
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
import { DateFormatter } from "../../utils/DateFormatter";
import queryString from "query-string";
import { logImEXEvent } from "../../firebase/firebase.utils";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
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,
@@ -138,7 +138,6 @@ export function AccountingPayablesTableComponent({
title: t("exportlogs.labels.attempts"),
dataIndex: "attempts",
key: "attempts",
render: (text, record) => (
<ExportLogsCountDisplay logs={record.exportlogs} />
),
@@ -147,8 +146,6 @@ export function AccountingPayablesTableComponent({
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}

View File

@@ -8,14 +8,16 @@ 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 { 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,
@@ -75,7 +77,11 @@ export function AccountingPayablesTableComponent({
dataIndex: "owner",
key: "owner",
ellipsis: true,
sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
sorter: (a, b) =>
alphaSort(
OwnerNameDisplayFunction(a.job),
OwnerNameDisplayFunction(b.job)
),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
@@ -94,6 +100,9 @@ export function AccountingPayablesTableComponent({
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>
),
@@ -112,18 +121,21 @@ export function AccountingPayablesTableComponent({
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("payments.fields.exportedat"),
// dataIndex: "exportedat",
// key: "exportedat",
// render: (text, record) => (
// <DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
// ),
// },
{
title: t("exportlogs.labels.attempts"),
dataIndex: "attempts",
@@ -137,8 +149,6 @@ export function AccountingPayablesTableComponent({
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
sorter: (a, b) => a.clm_total - b.clm_total,
render: (text, record) => (
<PaymentExportButton
paymentId={record.id}

View File

@@ -4,17 +4,19 @@ 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 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,
@@ -63,7 +65,7 @@ export function AccountingReceivablesTableComponent({
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => a.status - b.status,
sorter: (a, b) => statusSort(a, b, bodyshop.md_ro_statuses.statuses),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
},
@@ -83,7 +85,8 @@ export function AccountingReceivablesTableComponent({
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
@@ -103,6 +106,15 @@ export function AccountingReceivablesTableComponent({
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}>

View File

@@ -3,10 +3,22 @@ import { useMutation } from "@apollo/client";
import { Button, notification, Popconfirm } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { DELETE_BILL } from "../../graphql/bills.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
export default function BillDeleteButton({ bill, callback }) {
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
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);
@@ -36,6 +48,11 @@ export default function BillDeleteButton({ bill, callback }) {
if (!!!result.errors) {
notification["success"]({ message: t("bills.successes.deleted") });
insertAuditTrail({
jobid: jobid,
operation: AuditTrailMapping.billdeleted(bill.invoice_number),
type: "billdeleted",
});
if (callback && typeof callback === "function") callback(bill.id);
} else {

View File

@@ -1,5 +1,5 @@
import { useMutation, useQuery } from "@apollo/client";
import { Button, Form, PageHeader, Popconfirm, Space } from "antd";
import { Button, Divider, Form, PageHeader, Popconfirm, Space } from "antd";
import moment from "moment";
import queryString from "query-string";
import React, { useState } from "react";
@@ -33,8 +33,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export default connect(
@@ -150,6 +150,7 @@ export function BillDetailEditcontainer({
jobid: bill.jobid,
billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number),
type: "billupdated",
});
await refetch();
@@ -163,6 +164,7 @@ export function BillDetailEditcontainer({
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
const isinhouse = data && data.bills_by_pk && data.bills_by_pk.isinhouse;
return (
<>
@@ -206,8 +208,8 @@ export function BillDetailEditcontainer({
initialValues={transformData(data)}
layout="vertical"
>
<BillFormContainer form={form} billEdit disabled={exported} />
<BillFormContainer form={form} billEdit disabled={exported} disableInHouse={isinhouse}/>
<Divider orientation="left">{t("general.labels.media")}</Divider>
{bodyshop.uselocalmediaserver ? (
<JobsDocumentsLocalGallery
job={{ id: data ? data.bills_by_pk.jobid : null }}

View File

@@ -16,8 +16,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export default connect(
@@ -45,6 +45,7 @@ export function BillDetailEditReturn({
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,
@@ -173,7 +174,11 @@ export function BillDetailEditReturn({
</Form>
</Modal>
<Button
disabled={data.bills_by_pk.is_credit_memo || disabled}
disabled={
data.bills_by_pk.is_credit_memo ||
data.bills_by_pk.isinhouse ||
disabled
}
onClick={() => {
setVisible(true);
}}

View File

@@ -37,8 +37,8 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
insertAuditTrail: ({ jobid, billid, operation }) =>
dispatch(insertAuditTrail({ jobid, billid, operation })),
insertAuditTrail: ({ jobid, billid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, billid, operation, type })),
});
const Templates = TemplateList("job_special");
@@ -94,6 +94,7 @@ function BillEnterModalContainer({
location,
outstanding_returns,
inventory,
federal_tax_exempt,
...remainingValues
} = values;
@@ -170,6 +171,7 @@ function BillEnterModalContainer({
mod_lbr_ty: key,
hours: adjustmentsToInsert[key].toFixed(1),
}),
type: "jobmodifylbradj",
});
});
@@ -319,6 +321,7 @@ function BillEnterModalContainer({
operation: AuditTrailMapping.billposted(
r1.data.insert_bills.returning[0].invoice_number
),
type: "billposted",
});
if (enterAgain) {

View File

@@ -47,6 +47,7 @@ export function BillFormComponent({
loadLines,
billEdit,
disableInvNumber,
disableInHouse,
job,
loadOutstandingReturns,
loadInventory,
@@ -198,7 +199,7 @@ export function BillFormComponent({
]}
>
<VendorSearchSelect
disabled={disabled}
disabled={disabled || disableInHouse}
options={vendorAutoCompleteOptions}
preferredMake={preferredMake}
onSelect={handleVendorSelect}
@@ -271,7 +272,7 @@ export function BillFormComponent({
}),
]}
>
<Input disabled={disabled || disableInvNumber} />
<Input disabled={disabled || disableInvNumber || disableInHouse} />
</Form.Item>
<Form.Item
label={t("bills.fields.date")}
@@ -504,10 +505,11 @@ export function BillFormComponent({
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) => {

View File

@@ -22,6 +22,7 @@ export function BillFormContainer({
billEdit,
disabled,
disableInvNumber,
disableInHouse
}) {
const { Simple_Inventory } = useTreatments(
["Simple_Inventory"],
@@ -57,6 +58,7 @@ export function BillFormContainer({
job={lineData ? lineData.jobs_by_pk : null}
responsibilityCenters={bodyshop.md_responsibility_centers || null}
disableInvNumber={disableInvNumber}
disableInHouse={disableInHouse}
loadOutstandingReturns={loadOutstandingReturns}
loadInventory={loadInventory}
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}

View File

@@ -9,8 +9,8 @@ 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 { alphaSort, dateSort } from "../../utils/sorters";
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";
@@ -58,9 +58,9 @@ export function BillsListTableComponent({
<EditFilled />
</Button>
)}
<BillDeleteButton bill={record} />
<BillDeleteButton bill={record} jobid={job.id} />
<BillDetailEditReturnComponent
data={{ bills_by_pk: { ...record, jobid: job.id } }}
data={{ bills_by_pk: { ...record, jobid: job.id, job: job } }}
disabled={
record.is_credit_memo ||
record.vendorid === bodyshop.inhousevendorid ||

View File

@@ -1,8 +1,8 @@
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 { 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);
@@ -26,10 +26,14 @@ export default function CABCpvrtCalculator({ disabled, form }) {
<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>
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<Space>
<Button type="primary" htmlType="submit">
{t("general.actions.calculate")}
</Button>
<Button onClick={() => setVisibility(false)}>Close</Button>
</Space>
</div>
</Form>
</div>
);

View File

@@ -13,7 +13,6 @@ import {
notification,
} from "antd";
import axios from "axios";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -22,7 +21,6 @@ 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";
@@ -37,8 +35,8 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
});
@@ -48,12 +46,12 @@ const CardPaymentModalComponent = ({
toggleModalVisible,
insertAuditTrail,
}) => {
const { context } = cardPaymentModal;
const { context, actions } = cardPaymentModal;
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
// const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
const { t } = useTranslation();
@@ -65,7 +63,6 @@ const CardPaymentModalComponent = ({
}
);
console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data);
//Initialize the intellipay window.
const SetIntellipayCallbackFunctions = () => {
console.log("*** Set IntelliPay callback functions.");
@@ -74,16 +71,20 @@ const CardPaymentModalComponent = ({
});
window.intellipay.runOnApproval(async function (response) {
console.warn("*** Running On Approval Script ***");
form.setFieldValue("paymentResponse", response);
form.submit();
//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) => ({
@@ -102,55 +103,15 @@ const CardPaymentModalComponent = ({
insertAuditTrail({
jobid: payment.jobid,
operation: AuditTrailMapping.failedpayment(),
type: "failedpayment",
})
);
});
};
const handleFinish = async (values) => {
try {
await insertPayment({
variables: {
paymentInput: values.payments.map((payment) => ({
amount: payment.amount,
transactionid: (values.paymentResponse.paymentid || "").toString(),
payer: t("payments.labels.customer"),
type: values.paymentResponse.cardbrand,
jobid: payment.jobid,
date: moment(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();
@@ -163,6 +124,7 @@ const CardPaymentModalComponent = ({
const response = await axios.post("/intellipay/lightbox_credentials", {
bodyshop,
refresh: !!window.intellipay,
paymentSplitMeta: form.getFieldsValue(),
});
if (window.intellipay) {
@@ -191,7 +153,6 @@ const CardPaymentModalComponent = ({
<Card title="Card Payment">
<Spin spinning={loading}>
<Form
onFinish={handleFinish}
form={form}
layout="vertical"
initialValues={{
@@ -272,23 +233,14 @@ const CardPaymentModalComponent = ({
}
>
{() => {
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
@@ -343,6 +295,13 @@ const CardPaymentModalComponent = ({
value={totalAmountToCharge?.toFixed(2)}
hidden
/>
<Input
className="ipayfield"
data-ipayname="comment"
//type="hidden"
value={btoa(JSON.stringify(payments))}
hidden
/>
<Button
type="primary"
// data-ipayname="submit"
@@ -357,11 +316,6 @@ const CardPaymentModalComponent = ({
);
}}
</Form.Item>
{/* Lightbox payment response when it is completed */}
<Form.Item name="paymentResponse" hidden>
<Input type="hidden" />
</Form.Item>
</Form>
</Spin>
</Card>

View File

@@ -68,6 +68,30 @@ export default function ContractFormComponent({
<FormDateTimePicker />
</Form.Item>
)}
{create && (
<Form.Item
shouldUpdate={(p, c) => p.scheduledreturn !== c.scheduledreturn}
>
{() => {
const insuranceOver =
selectedCar &&
selectedCar.insuranceexpires &&
moment(selectedCar.insuranceexpires)
.endOf("day")
.isBefore(moment(form.getFieldValue("scheduledreturn")));
if (insuranceOver)
return (
<Space direction="vertical" style={{ color: "tomato" }}>
<span>
<WarningFilled style={{ marginRight: ".3rem" }} />
{t("contracts.labels.insuranceexpired")}
</span>
</Space>
);
return <></>;
}}
</Form.Item>
)}
</LayoutFormRow>
<LayoutFormRow grow>
<Form.Item
@@ -90,16 +114,17 @@ export default function ContractFormComponent({
>
{() => {
const mileageOver =
selectedCar &&
selectedCar.nextservicekm <= form.getFieldValue("kmstart");
selectedCar && selectedCar.nextservicekm
? selectedCar.nextservicekm <= form.getFieldValue("kmstart")
: false;
const dueForService =
selectedCar &&
selectedCar.nextservicedate &&
moment(selectedCar.nextservicedate).isBefore(
moment(form.getFieldValue("scheduledreturn"))
);
moment(selectedCar.nextservicedate)
.endOf("day")
.isSameOrBefore(
moment(form.getFieldValue("scheduledreturn"))
);
if (mileageOver || dueForService)
return (
<Space direction="vertical" style={{ color: "tomato" }}>
@@ -117,7 +142,6 @@ export default function ContractFormComponent({
</span>
</Space>
);
return <></>;
}}
</Form.Item>

View File

@@ -10,11 +10,15 @@ import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel
import CourtesyCarReadiness from "../courtesy-car-readiness-select/courtesy-car-readiness-select.component";
import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
//import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
export default function CourtesyCarCreateFormComponent({
form,
saveLoading,
newCC,
}) {
const { t } = useTranslation();
const client = useApolloClient();
@@ -33,7 +37,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
}
/>
{/* <FormFieldsChanged form={form} /> */}
{newCC ? null : <FormFieldsChanged form={form} />}
<LayoutFormRow header={t("courtesycars.labels.vehicle")}>
<Form.Item
label={t("courtesycars.fields.year")}
@@ -214,10 +218,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
>
<CourtesyCarStatus />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.readiness")}
name="readiness"
>
<Form.Item label={t("courtesycars.fields.readiness")} name="readiness">
<CourtesyCarReadiness />
</Form.Item>
<div>
@@ -234,8 +235,9 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
>
{() => {
const nextservicekm = form.getFieldValue("nextservicekm");
const mileageOver =
nextservicekm && nextservicekm <= form.getFieldValue("mileage");
const mileageOver = nextservicekm
? nextservicekm <= form.getFieldValue("mileage")
: false;
if (mileageOver)
return (
<Space direction="vertical" style={{ color: "tomato" }}>

View File

@@ -37,6 +37,9 @@ const CourtesyCarStatusComponent = ({ value, onChange }, ref) => {
<Option value="courtesycars.status.leasereturn">
{t("courtesycars.status.leasereturn")}
</Option>
<Option value="courtesycars.status.unavailable">
{t("courtesycars.status.unavailable")}
</Option>
</Select>
);
};

View File

@@ -17,13 +17,18 @@ import { DateTimeFormatter } from "../../utils/DateFormatter";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
});
const [searchText, setSearchText] = useState("");
const [filter, setFilter] = useLocalStorage(
"filter_courtesy_cars_list",
null
);
const { t } = useTranslation();
const columns = [
@@ -50,11 +55,16 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
dataIndex: "status",
key: "status",
sorter: (a, b) => alphaSort(a.status, b.status),
filteredValue: filter?.status || null,
filters: [
{
text: t("courtesycars.status.in"),
value: "courtesycars.status.in",
},
{
text: t("courtesycars.status.inservice"),
value: "courtesycars.status.inservice",
},
{
text: t("courtesycars.status.out"),
value: "courtesycars.status.out",
@@ -67,23 +77,43 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
text: t("courtesycars.status.leasereturn"),
value: "courtesycars.status.leasereturn",
},
{
text: t("courtesycars.status.unavailable"),
value: "courtesycars.status.unavailable",
},
],
onFilter: (value, record) => value.includes(record.status),
onFilter: (value, record) => record.status === value,
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => {
const { nextservicedate, nextservicekm, mileage } = record;
const { nextservicedate, nextservicekm, mileage, insuranceexpires } =
record;
const mileageOver = nextservicekm <= mileage;
const mileageOver = nextservicekm ? nextservicekm <= mileage : false;
const dueForService =
nextservicedate && moment(nextservicedate).isBefore(moment());
nextservicedate &&
moment(nextservicedate).endOf("day").isSameOrBefore(moment());
const insuranceOver =
insuranceexpires &&
moment(insuranceexpires).endOf("day").isBefore(moment());
return (
<Space>
{t(record.status)}
{(mileageOver || dueForService) && (
<Tooltip title={t("contracts.labels.cardueforservice")}>
{(mileageOver || dueForService || insuranceOver) && (
<Tooltip
title={
(mileageOver || dueForService) && insuranceOver
? t("contracts.labels.insuranceexpired") +
" / " +
t("contracts.labels.cardueforservice")
: insuranceOver
? t("contracts.labels.insuranceexpired")
: t("contracts.labels.cardueforservice")
}
>
<WarningFilled style={{ color: "tomato" }} />
</Tooltip>
)}
@@ -96,6 +126,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
dataIndex: "readiness",
key: "readiness",
sorter: (a, b) => alphaSort(a.readiness, b.readiness),
filteredValue: filter?.readiness || null,
filters: [
{
text: t("courtesycars.readiness.ready"),
@@ -155,7 +186,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
title: t("courtesycars.fields.fuel"),
dataIndex: "fuel",
key: "fuel",
sorter: (a, b) => alphaSort(a.fuel, b.fuel),
sorter: (a, b) => a.fuel - b.fuel,
sortOrder:
state.sortedInfo.columnKey === "fuel" && state.sortedInfo.order,
render: (text, record) => {
@@ -164,12 +195,14 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
return t("courtesycars.labels.fuel.full");
case 88:
return t("courtesycars.labels.fuel.78");
case 75:
return t("courtesycars.labels.fuel.34");
case 63:
return t("courtesycars.labels.fuel.58");
case 50:
return t("courtesycars.labels.fuel.12");
case 38:
return t("courtesycars.labels.fuel.34");
return t("courtesycars.labels.fuel.38");
case 25:
return t("courtesycars.labels.fuel.14");
case 13:
@@ -211,7 +244,8 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
setState({ ...state, sortedInfo: sorter });
setFilter(filters);
};
const tableData = searchText

View File

@@ -5,6 +5,7 @@ import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom";
import { QUERY_CSI_RESPONSE_BY_PK } from "../../graphql/csi.queries";
import { DateFormatter } from "../../utils/DateFormatter";
import AlertComponent from "../alert/alert.component";
import ConfigFormComponents from "../config-form-components/config-form-components.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
@@ -44,6 +45,13 @@ export default function CsiResponseFormContainer() {
readOnly
componentList={data.csi_by_pk.csiquestion.config}
/>
{data.csi_by_pk.validuntil ? (
<>
{t("csi.fields.validuntil")}
{": "}
<DateFormatter>{data.csi_by_pk.validuntil}</DateFormatter>
</>
) : null}
</Form>
</Card>
);

View File

@@ -5,9 +5,11 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useHistory, useLocation } from "react-router-dom";
import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import {pageLimit} from "../../utils/config";
import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
export default function CsiResponseListPaginated({
refetch,
@@ -16,23 +18,23 @@ export default function CsiResponseListPaginated({
total,
}) {
const search = queryString.parse(useLocation().search);
const { responseid, page, sortcolumn, sortorder } = search;
const { responseid } = search;
const history = useHistory();
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
page: "",
});
const { t } = useTranslation();
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
width: "8%",
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
sortOrder: sortcolumn === "ro_number" && sortorder,
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
<Link to={"/manage/jobs/" + record.job.id}>
{record.job.ro_number || t("general.labels.na")}
@@ -41,15 +43,18 @@ export default function CsiResponseListPaginated({
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
width: "25%",
sortOrder: sortcolumn === "owner" && sortorder,
dataIndex: "owner_name",
key: "owner_name",
sorter: (a, b) =>
alphaSort(
OwnerNameDisplayFunction(a.job),
OwnerNameDisplayFunction(b.job)
),
sortOrder:
state.sortedInfo.columnKey === "owner_name" && state.sortedInfo.order,
render: (text, record) => {
return record.job.owner ? (
<Link to={"/manage/owners/" + record.job.owner.id}>
return record.job.ownerid ? (
<Link to={"/manage/owners/" + record.job.ownerid}>
<OwnerNameDisplay ownerObject={record.job} />
</Link>
) : (
@@ -64,9 +69,9 @@ export default function CsiResponseListPaginated({
dataIndex: "completedon",
key: "completedon",
ellipsis: true,
sorter: (a, b) => a.completedon - b.completedon,
width: "25%",
sortOrder: sortcolumn === "completedon" && sortorder,
sorter: (a, b) => dateSort(a.completedon, b.completedon),
sortOrder:
state.sortedInfo.columnKey === "completedon" && state.sortedInfo.order,
render: (text, record) => {
return record.completedon ? (
<DateFormatter>{record.completedon}</DateFormatter>
@@ -76,11 +81,12 @@ export default function CsiResponseListPaginated({
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
search.page = pagination.current;
search.sortcolumn = sorter.columnKey;
search.sortorder = sorter.order;
history.push({ search: queryString.stringify(search) });
setState({
...state,
filteredInfo: filters,
sortedInfo: sorter,
page: pagination.current,
});
};
const handleOnRowClick = (record) => {
@@ -108,7 +114,7 @@ export default function CsiResponseListPaginated({
pagination={{
position: "top",
pageSize: pageLimit,
current: parseInt(page || 1),
current: parseInt(state.page || 1),
total: total,
}}
columns={columns}
@@ -122,13 +128,6 @@ export default function CsiResponseListPaginated({
selectedRowKeys: [responseid],
type: "radio",
}}
onRow={(record, rowIndex) => {
return {
onClick: (event) => {
handleOnRowClick(record);
}, // click row
};
}}
/>
</Card>
);

View File

@@ -0,0 +1,169 @@
import {Card, Table, Tag} from "antd";
import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
import {useTranslation} from "react-i18next";
import React, {useEffect, useState} from "react";
import moment from "moment";
import DashboardRefreshRequired from "../refresh-required.component";
import axios from "axios";
const fortyFiveDaysAgo = () => moment().subtract(45, 'days').toLocaleString();
export default function JobLifecycleDashboardComponent({data, bodyshop, ...cardProps}) {
const {t} = useTranslation();
const [loading, setLoading] = useState(false);
const [lifecycleData, setLifecycleData] = useState(null);
useEffect(() => {
async function getLifecycleData() {
if (data && data.job_lifecycle) {
setLoading(true);
const response = await axios.post("/job/lifecycle", {
jobids: data.job_lifecycle.map(x => x.id),
statuses: bodyshop.md_ro_statuses
});
setLifecycleData(response.data.durations);
setLoading(false);
}
}
getLifecycleData().catch(e => {
console.error(`Error in getLifecycleData: ${e}`);
})
}, [data, bodyshop]);
const columns = [
{
title: t('job_lifecycle.columns.status'),
dataIndex: 'status',
bgColor: 'red',
key: 'status',
render: (text, record) => {
return <Tag color={record.color}>{record.status}</Tag>
}
},
{
title: t('job_lifecycle.columns.human_readable'),
dataIndex: 'humanReadable',
key: 'humanReadable',
},
{
title: t('job_lifecycle.columns.status_count'),
key: 'statusCount',
render: (text, record) => {
return lifecycleData.statusCounts[record.status];
}
},
{
title: t('job_lifecycle.columns.percentage'),
dataIndex: 'percentage',
key: 'percentage',
render: (text, record) => {
return record.percentage.toFixed(2) + '%';
}
},
];
if (!data) return null;
if (!data.job_lifecycle || !lifecycleData) return <DashboardRefreshRequired {...cardProps} />;
const extra = `${t('job_lifecycle.content.calculated_based_on')} ${lifecycleData.jobs} ${t('job_lifecycle.content.jobs_in_since')} ${fortyFiveDaysAgo()}`
return (
<Card title={t("job_lifecycle.titles.dashboard")} {...cardProps}>
<LoadingSkeleton loading={loading}>
<div style={{overflow: 'scroll', height: "100%"}}>
<div id="bar-container" style={{
display: 'flex',
width: '100%',
height: '100px',
textAlign: 'center',
borderRadius: '5px',
borderWidth: '5px',
borderStyle: 'solid',
borderColor: '#f0f2f5',
margin: 0,
padding: 0
}}>
{lifecycleData.summations.map((key, index, array) => {
const isFirst = index === 0;
const isLast = index === array.length - 1;
return (
<div key={key.status} style={{
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
margin: 0,
padding: 0,
borderTop: '1px solid #f0f2f5',
borderBottom: '1px solid #f0f2f5',
borderLeft: isFirst ? '1px solid #f0f2f5' : undefined,
borderRight: isLast ? '1px solid #f0f2f5' : undefined,
backgroundColor: key.color,
width: `${key.percentage}%`
}}
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
>
{key.percentage > 15 ?
<>
<div>{key.roundedPercentage}</div>
<div style={{
backgroundColor: '#f0f2f5',
borderRadius: '5px',
paddingRight: '2px',
paddingLeft: '2px',
fontSize: '0.8rem',
}}>
{key.status}
</div>
</>
: null}
</div>
);
})}
</div>
<Card extra={extra} type='inner' title={t('job_lifecycle.content.legend_title')}
style={{marginTop: '10px'}}>
<div>
{lifecycleData.summations.map((key) => (
<Tag color={key.color} style={{width: '13vh', padding: '4px', margin: '4px'}}>
<div
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
style={{
backgroundColor: '#f0f2f5',
color: '#000',
padding: '4px',
textAlign: 'center'
}}>
{key.status} [{lifecycleData.statusCounts[key.status]}] ({key.roundedPercentage})
</div>
</Tag>
))}
</div>
</Card>
<Card style={{marginTop: "5px"}} type='inner' title={t("job_lifecycle.titles.top_durations")}>
<Table size="small" pagination={false} columns={columns}
dataSource={lifecycleData.summations.sort((a, b) => b.value - a.value).slice(0, 3)}/>
</Card>
</div>
</LoadingSkeleton>
</Card>
);
}
export const JobLifecycleDashboardGQL = `
job_lifecycle: jobs(where: {
actual_in: {
_gte: "${moment().subtract(45, 'days').toISOString()}"
}
}) {
id
actual_in
} `;

View File

@@ -3,21 +3,32 @@ import {
ExclamationCircleFilled,
PauseCircleOutlined,
} from "@ant-design/icons";
import { Card, Space, Table, Tooltip } from "antd";
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { TimeFormatter } from "../../../utils/DateFormatter";
import { onlyUnique } from "../../../utils/arrayHelper";
import { alphaSort, dateSort } from "../../../utils/sorters";
import useLocalStorage from "../../../utils/useLocalStorage";
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
import OwnerNameDisplay from "../../owner-name-display/owner-name-display.component";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../../owner-name-display/owner-name-display.component";
import DashboardRefreshRequired from "../refresh-required.component";
import {pageLimit} from "../../../utils/config";
export default function DashboardScheduledInToday({ data, ...cardProps }) {
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: {},
});
const [isTvModeScheduledIn, setIsTvModeScheduledIn] = useLocalStorage(
"isTvModeScheduledIn",
false
);
if (!data) return null;
if (!data.scheduled_in_today)
return <DashboardRefreshRequired {...cardProps} />;
@@ -31,6 +42,12 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
alt_transport: item.job.alt_transport,
clm_no: item.job.clm_no,
jobid: item.job.jobid,
joblines_body: item.job.joblines
.filter((l) => l.mod_lbr_ty !== "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0),
joblines_ref: item.job.joblines
.filter((l) => l.mod_lbr_ty === "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0),
ins_co_nm: item.job.ins_co_nm,
iouparent: item.job.iouparent,
ownerid: item.job.ownerid,
@@ -49,7 +66,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
v_vin: item.job.v_vin,
vehicleid: item.job.vehicleid,
note: item.note,
start: moment(item.start).format("hh:mm a"),
start: item.start,
title: item.title,
};
appt.push(i);
@@ -59,11 +76,192 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
return new moment(a.start) - new moment(b.start);
});
const columns = [
const tvFontSize = 16;
const tvFontWeight = "bold";
const tvColumns = [
{
title: t("appointments.fields.time"),
dataIndex: "start",
key: "start",
ellipsis: true,
sorter: (a, b) => dateSort(a.start, b.start),
sortOrder:
state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<TimeFormatter>{record.start}</TimeFormatter>
</span>
),
},
{
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.jobid}
onClick={(e) => e.stopPropagation()}
>
<Space>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.ro_number || t("general.labels.na")}
{record.production_vars && record.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
{record.iouparent && (
<Tooltip title={t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} />
</Tooltip>
)}
</span>
</Space>
</Link>
),
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.ownerid ? (
<Link
to={"/manage/owners/" + record.ownerid}
onClick={(e) => e.stopPropagation()}
>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<OwnerNameDisplay ownerObject={record} />
</span>
</Link>
) : (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<OwnerNameDisplay ownerObject={record} />
</span>
);
},
},
{
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}
onClick={(e) => e.stopPropagation()}
>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</span>
</Link>
) : (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{`${
record.v_model_yr || ""
} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
);
},
},
{
title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport",
key: "alt_transport",
ellipsis: true,
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
sortOrder:
state.sortedInfo.columnKey === "alt_transport" &&
state.sortedInfo.order,
filters:
(appt &&
appt
.map((j) => j.alt_transport)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Alt. Transport",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.alt_transport),
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.alt_transport}
</span>
),
},
{
title: t("jobs.fields.lab"),
dataIndex: "joblines_body",
key: "joblines_body",
sorter: (a, b) => a.joblines_body - b.joblines_body,
sortOrder:
state.sortedInfo.columnKey === "joblines_body" &&
state.sortedInfo.order,
align: "right",
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.joblines_body.toFixed(1)}
</span>
),
},
{
title: t("jobs.fields.lar"),
dataIndex: "joblines_ref",
key: "joblines_ref",
sorter: (a, b) => a.joblines_ref - b.joblines_ref,
sortOrder:
state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order,
align: "right",
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.joblines_ref.toFixed(1)}
</span>
),
},
];
const columns = [
{
title: t("appointments.fields.time"),
dataIndex: "start",
key: "start",
ellipsis: true,
sorter: (a, b) => dateSort(a.start, b.start),
sortOrder:
state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
render: (text, record) => <TimeFormatter>{record.start}</TimeFormatter>,
},
{
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.jobid}
@@ -91,7 +289,10 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
dataIndex: "owner",
key: "owner",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.ownerid ? (
<Link
@@ -108,23 +309,16 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
},
},
{
title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
title: t("dashboard.labels.phone"),
dataIndex: "ownr_ph",
key: "ownr_ph",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
),
},
{
title: t("jobs.fields.ownr_ph2"),
dataIndex: "ownr_ph2",
key: "ownr_ph2",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
<Space size="small" wrap>
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
</Space>
),
},
{
@@ -134,7 +328,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ea} jobid={record.jobid} />
<a href={`mailto:${record.ownr_ea}`}>{record.ownr_ea}</a>
),
},
{
@@ -142,6 +336,15 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
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
@@ -165,43 +368,80 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
key: "ins_co_nm",
ellipsis: true,
responsive: ["md"],
},
{
title: t("appointments.fields.time"),
dataIndex: "start",
key: "start",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
sortOrder:
state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,
filters:
(appt &&
appt
.map((j) => j.ins_co_nm)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Ins. Co.*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.ins_co_nm),
},
{
title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport",
key: "alt_transport",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
sortOrder:
state.sortedInfo.columnKey === "alt_transport" &&
state.sortedInfo.order,
filters:
(appt &&
appt
.map((j) => j.alt_transport)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Alt. Transport",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.alt_transport),
},
];
const handleTableChange = (sorter) => {
setState({ ...state, sortedInfo: sorter });
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<Card
title={t("dashboard.titles.scheduledintoday", {
title={t("dashboard.titles.scheduledindate", {
date: moment().startOf("day").format("MM/DD/YYYY"),
})}
extra={
<Space>
<Typography.Text>{t("general.labels.tvmode")}</Typography.Text>
<Switch
onClick={() => setIsTvModeScheduledIn(!isTvModeScheduledIn)}
defaultChecked={isTvModeScheduledIn}
/>
</Space>
}
{...cardProps}
>
<div style={{ height: "100%" }}>
<Table
onChange={handleTableChange}
pagination={{ position: "top", defaultPageSize: pageLimit }}
columns={columns}
pagination={false}
columns={isTvModeScheduledIn ? tvColumns : columns}
scroll={{ x: true, y: "calc(100% - 2em)" }}
rowKey="id"
style={{ height: "85%" }}
dataSource={appt}
size={isTvModeScheduledIn ? "small" : "middle"}
/>
</div>
</Card>
@@ -220,6 +460,10 @@ export const DashboardScheduledInTodayGql = `
alt_transport
clm_no
jobid: id
joblines(where: {removed: {_eq: false}}) {
mod_lb_hrs
mod_lbr_ty
}
ins_co_nm
iouparent
ownerid

View File

@@ -3,37 +3,272 @@ import {
ExclamationCircleFilled,
PauseCircleOutlined,
} from "@ant-design/icons";
import { Card, Space, Table, Tooltip } from "antd";
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { TimeFormatter } from "../../../utils/DateFormatter";
import { onlyUnique } from "../../../utils/arrayHelper";
import { alphaSort, dateSort } from "../../../utils/sorters";
import useLocalStorage from "../../../utils/useLocalStorage";
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
import OwnerNameDisplay from "../../owner-name-display/owner-name-display.component";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../../owner-name-display/owner-name-display.component";
import DashboardRefreshRequired from "../refresh-required.component";
import {pageLimit} from "../../../utils/config";
export default function DashboardScheduledOutToday({ data, ...cardProps }) {
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: {},
});
const [isTvModeScheduledOut, setIsTvModeScheduledOut] = useLocalStorage(
"isTvModeScheduledOut",
false
);
if (!data) return null;
if (!data.scheduled_out_today)
return <DashboardRefreshRequired {...cardProps} />;
data.scheduled_out_today.forEach((item) => {
item.scheduled_completion= moment(item.scheduled_completion).format("hh:mm a")
item.joblines_body = item.joblines
? item.joblines
.filter((l) => l.mod_lbr_ty !== "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0)
: 0;
item.joblines_ref = item.joblines
? item.joblines
.filter((l) => l.mod_lbr_ty === "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0)
: 0;
});
data.scheduled_out_today.sort(function (a, b) {
return new Date(a.scheduled_completion) - new Date(b.scheduled_completion);
});
const columns = [
const tvFontSize = 18;
const tvFontWeight = "bold";
const tvColumns = [
{
title: t("jobs.fields.scheduled_completion"),
dataIndex: "scheduled_completion",
key: "scheduled_completion",
ellipsis: true,
sorter: (a, b) =>
dateSort(a.scheduled_completion, b.scheduled_completion),
sortOrder:
state.sortedInfo.columnKey === "scheduled_completion" &&
state.sortedInfo.order,
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<TimeFormatter>{record.scheduled_completion}</TimeFormatter>
</span>
),
},
{
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.jobid}
onClick={(e) => e.stopPropagation()}
>
<Space>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.ro_number || t("general.labels.na")}
{record.production_vars && record.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
{record.iouparent && (
<Tooltip title={t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} />
</Tooltip>
)}
</span>
</Space>
</Link>
),
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.ownerid ? (
<Link
to={"/manage/owners/" + record.ownerid}
onClick={(e) => e.stopPropagation()}
>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<OwnerNameDisplay ownerObject={record} />
</span>
</Link>
) : (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<OwnerNameDisplay ownerObject={record} />
</span>
);
},
},
{
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}
onClick={(e) => e.stopPropagation()}
>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</span>
</Link>
) : (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{`${
record.v_model_yr || ""
} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
);
},
},
{
title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport",
key: "alt_transport",
ellipsis: true,
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
sortOrder:
state.sortedInfo.columnKey === "alt_transport" &&
state.sortedInfo.order,
filters:
(data.scheduled_out_today &&
data.scheduled_out_today
.map((j) => j.alt_transport)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Alt. Transport*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.alt_transport),
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.alt_transport}
</span>
),
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
ellipsis: true,
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filters:
(data.scheduled_out_today &&
data.scheduled_out_today
.map((j) => j.status)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.status),
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.status}
</span>
),
},
{
title: t("jobs.fields.lab"),
dataIndex: "joblines_body",
key: "joblines_body",
sorter: (a, b) => a.joblines_body - b.joblines_body,
sortOrder:
state.sortedInfo.columnKey === "joblines_body" &&
state.sortedInfo.order,
align: "right",
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.joblines_body.toFixed(1)}
</span>
),
},
{
title: t("jobs.fields.lar"),
dataIndex: "joblines_ref",
key: "joblines_ref",
sorter: (a, b) => a.joblines_ref - b.joblines_ref,
sortOrder:
state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order,
align: "right",
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.joblines_ref.toFixed(1)}
</span>
),
},
];
const columns = [
{
title: t("jobs.fields.scheduled_completion"),
dataIndex: "scheduled_completion",
key: "scheduled_completion",
ellipsis: true,
sorter: (a, b) =>
dateSort(a.scheduled_completion, b.scheduled_completion),
sortOrder:
state.sortedInfo.columnKey === "scheduled_completion" &&
state.sortedInfo.order,
render: (text, record) => (
<TimeFormatter>{record.scheduled_completion}</TimeFormatter>
),
},
{
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.jobid}
@@ -61,7 +296,10 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
dataIndex: "owner",
key: "owner",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.ownerid ? (
<Link
@@ -78,23 +316,16 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
},
},
{
title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
title: t("dashboard.labels.phone"),
dataIndex: "ownr_ph",
key: "ownr_ph",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
),
},
{
title: t("jobs.fields.ownr_ph2"),
dataIndex: "ownr_ph2",
key: "ownr_ph2",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
<Space size="small" wrap>
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
</Space>
),
},
{
@@ -104,7 +335,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ea} jobid={record.jobid} />
<a href={`mailto:${record.ownr_ea}`}>{record.ownr_ea}</a>
),
},
{
@@ -112,6 +343,15 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
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
@@ -135,43 +375,80 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
key: "ins_co_nm",
ellipsis: true,
responsive: ["md"],
},
{
title: t("jobs.fields.scheduled_completion"),
dataIndex: "scheduled_completion",
key: "scheduled_completion",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
sortOrder:
state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,
filters:
(data.scheduled_out_today &&
data.scheduled_out_today
.map((j) => j.ins_co_nm)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Ins. Co.*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.ins_co_nm),
},
{
title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport",
key: "alt_transport",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
sortOrder:
state.sortedInfo.columnKey === "alt_transport" &&
state.sortedInfo.order,
filters:
(data.scheduled_out_today &&
data.scheduled_out_today
.map((j) => j.alt_transport)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Alt. Transport*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.alt_transport),
},
];
const handleTableChange = (sorter) => {
setState({ ...state, sortedInfo: sorter });
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<Card
title={t("dashboard.titles.scheduledouttoday", {
title={t("dashboard.titles.scheduledoutdate", {
date: moment().startOf("day").format("MM/DD/YYYY"),
})}
extra={
<Space>
<Typography.Text>{t("general.labels.tvmode")}</Typography.Text>
<Switch
onClick={() => setIsTvModeScheduledOut(!isTvModeScheduledOut)}
defaultChecked={isTvModeScheduledOut}
/>
</Space>
}
{...cardProps}
>
<div style={{ height: "100%" }}>
<Table
onChange={handleTableChange}
pagination={{ position: "top", defaultPageSize: pageLimit }}
columns={columns}
pagination={false}
columns={isTvModeScheduledOut ? tvColumns : columns}
scroll={{ x: true, y: "calc(100% - 2em)" }}
rowKey="id"
style={{ height: "85%" }}
dataSource={data.scheduled_out_today}
size={isTvModeScheduledOut ? "small" : "middle"}
/>
</div>
</Card>
@@ -188,6 +465,10 @@ export const DashboardScheduledOutTodayGql = `
alt_transport
clm_no
jobid: id
joblines(where: {removed: {_eq: false}}) {
mod_lb_hrs
mod_lbr_ty
}
ins_co_nm
iouparent
ownerid
@@ -200,6 +481,7 @@ export const DashboardScheduledOutTodayGql = `
production_vars
ro_number
scheduled_completion
status
suspended
v_make_desc
v_model_desc

View File

@@ -1,386 +1,391 @@
import Icon, { SyncOutlined } from "@ant-design/icons";
import { gql, useMutation, useQuery } from "@apollo/client";
import { Button, Dropdown, Menu, PageHeader, Space, notification } from "antd";
import Icon, {SyncOutlined} from "@ant-design/icons";
import {gql, useMutation, useQuery} from "@apollo/client";
import {Button, Dropdown, Menu, notification, PageHeader, Space} from "antd";
import i18next from "i18next";
import _ from "lodash";
import moment from "moment";
import React, { useState } from "react";
import { Responsive, WidthProvider } from "react-grid-layout";
import { useTranslation } from "react-i18next";
import { MdClose } from "react-icons/md";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import React, {useState} from "react";
import {Responsive, WidthProvider} from "react-grid-layout";
import {useTranslation} from "react-i18next";
import {MdClose} from "react-icons/md";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {logImEXEvent} from "../../firebase/firebase.utils";
import {UPDATE_DASHBOARD_LAYOUT} from "../../graphql/user.queries";
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import DashboardMonthlyEmployeeEfficiency, {
DashboardMonthlyEmployeeEfficiencyGql,
DashboardMonthlyEmployeeEfficiencyGql,
} from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component";
import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component";
import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component";
import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component";
import DashboardMonthlyRevenueGraph, {
DashboardMonthlyRevenueGraphGql,
DashboardMonthlyRevenueGraphGql,
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component";
import DashboardProjectedMonthlySales, {
DashboardProjectedMonthlySalesGql,
DashboardProjectedMonthlySalesGql,
} from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component";
import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component";
import DashboardTotalProductionDollars
from "../dashboard-components/total-production-dollars/total-production-dollars.component";
import DashboardTotalProductionHours, {
DashboardTotalProductionHoursGql,
DashboardTotalProductionHoursGql,
} from "../dashboard-components/total-production-hours/total-production-hours.component";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
//Combination of the following:
// /node_modules/react-grid-layout/css/styles.css
// /node_modules/react-resizable/css/styles.css
import DashboardScheduledInToday, {
DashboardScheduledInTodayGql,
DashboardScheduledInTodayGql,
} from "../dashboard-components/scheduled-in-today/scheduled-in-today.component";
import DashboardScheduledOutToday, {
DashboardScheduledOutTodayGql,
DashboardScheduledOutTodayGql,
} from "../dashboard-components/scheduled-out-today/scheduled-out-today.component";
import JobLifecycleDashboardComponent, {
JobLifecycleDashboardGQL
} from "../dashboard-components/job-lifecycle/job-lifecycle-dashboard.component";
import "./dashboard-grid.styles.scss";
import { GenerateDashboardData } from "./dashboard-grid.utils";
import {GenerateDashboardData} from "./dashboard-grid.utils";
const ResponsiveReactGridLayout = WidthProvider(Responsive);
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function DashboardGridComponent({ currentUser, bodyshop }) {
const { t } = useTranslation();
const [state, setState] = useState({
...(bodyshop.associations[0].user.dashboardlayout
? bodyshop.associations[0].user.dashboardlayout
: { items: [], layout: {}, layouts: [] }),
});
const { loading, error, data, refetch } = useQuery(
createDashboardQuery(state),
{ fetchPolicy: "network-only", nextFetchPolicy: "network-only" }
);
const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
const handleLayoutChange = async (layout, layouts) => {
logImEXEvent("dashboard_change_layout");
setState({ ...state, layout, layouts });
const result = await updateLayout({
variables: {
email: currentUser.email,
layout: { ...state, layout, layouts },
},
export function DashboardGridComponent({currentUser, bodyshop}) {
const {t} = useTranslation();
const [state, setState] = useState({
...(bodyshop.associations[0].user.dashboardlayout
? bodyshop.associations[0].user.dashboardlayout
: {items: [], layout: {}, layouts: []}),
});
if (!!result.errors) {
notification["error"]({
message: t("dashboard.errors.updatinglayout", {
message: JSON.stringify(result.errors),
}),
});
}
};
const handleRemoveComponent = (key) => {
logImEXEvent("dashboard_remove_component", { name: key });
const idxToRemove = state.items.findIndex((i) => i.i === key);
const items = _.cloneDeep(state.items);
const {loading, error, data, refetch} = useQuery(
createDashboardQuery(state),
{fetchPolicy: "network-only", nextFetchPolicy: "network-only"}
);
items.splice(idxToRemove, 1);
setState({ ...state, items });
};
const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
const handleAddComponent = (e) => {
logImEXEvent("dashboard_add_component", { name: e });
setState({
...state,
items: [
...state.items,
{
i: e.key,
x: (state.items.length * 2) % (state.cols || 12),
y: 99, // puts it at the bottom
w: componentList[e.key].w || 2,
h: componentList[e.key].h || 2,
},
],
});
};
const handleLayoutChange = async (layout, layouts) => {
logImEXEvent("dashboard_change_layout");
const dashboarddata = React.useMemo(
() => GenerateDashboardData(data),
[data]
);
const existingLayoutKeys = state.items.map((i) => i.i);
const addComponentOverlay = (
<Menu onClick={handleAddComponent}>
{Object.keys(componentList).map((key) => (
<Menu.Item
key={key}
value={key}
disabled={existingLayoutKeys.includes(key)}
>
{componentList[key].label}
</Menu.Item>
))}
</Menu>
);
setState({...state, layout, layouts});
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<div>
<PageHeader
extra={
<Space>
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<Dropdown overlay={addComponentOverlay} trigger={["click"]}>
<Button>{t("dashboard.actions.addcomponent")}</Button>
</Dropdown>
</Space>
const result = await updateLayout({
variables: {
email: currentUser.email,
layout: {...state, layout, layouts},
},
});
if (!!result.errors) {
notification["error"]({
message: t("dashboard.errors.updatinglayout", {
message: JSON.stringify(result.errors),
}),
});
}
/>
};
const handleRemoveComponent = (key) => {
logImEXEvent("dashboard_remove_component", {name: key});
const idxToRemove = state.items.findIndex((i) => i.i === key);
<ResponsiveReactGridLayout
className="layout"
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
width="100%"
layouts={state.layouts}
onLayoutChange={handleLayoutChange}
// onBreakpointChange={onBreakpointChange}
>
{state.items.map((item, index) => {
const TheComponent = componentList[item.i].component;
return (
<div
key={item.i}
data-grid={{
...item,
minH: componentList[item.i].minH || 1,
minW: componentList[item.i].minW || 1,
}}
const items = _.cloneDeep(state.items);
items.splice(idxToRemove, 1);
setState({...state, items});
};
const handleAddComponent = (e) => {
logImEXEvent("dashboard_add_component", {name: e});
setState({
...state,
items: [
...state.items,
{
i: e.key,
x: (state.items.length * 2) % (state.cols || 12),
y: 99, // puts it at the bottom
w: componentList[e.key].w || 2,
h: componentList[e.key].h || 2,
},
],
});
};
const dashboarddata = React.useMemo(
() => GenerateDashboardData(data),
[data]
);
const existingLayoutKeys = state.items.map((i) => i.i);
const addComponentOverlay = (
<Menu onClick={handleAddComponent}>
{Object.keys(componentList).map((key) => (
<Menu.Item
key={key}
value={key}
disabled={existingLayoutKeys.includes(key)}
>
{componentList[key].label}
</Menu.Item>
))}
</Menu>
);
if (error) return <AlertComponent message={error.message} type="error"/>;
return (
<div>
<PageHeader
extra={
<Space>
<Button onClick={() => refetch()}>
<SyncOutlined/>
</Button>
<Dropdown overlay={addComponentOverlay} trigger={["click"]}>
<Button>{t("dashboard.actions.addcomponent")}</Button>
</Dropdown>
</Space>
}
/>
<ResponsiveReactGridLayout
className="layout"
breakpoints={{lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0}}
cols={{lg: 12, md: 10, sm: 6, xs: 4, xxs: 2}}
width="100%"
layouts={state.layouts}
onLayoutChange={handleLayoutChange}
// onBreakpointChange={onBreakpointChange}
>
<LoadingSkeleton loading={loading}>
<Icon
component={MdClose}
key={item.i}
style={{
position: "absolute",
zIndex: "2",
right: ".25rem",
top: ".25rem",
cursor: "pointer",
}}
onClick={() => handleRemoveComponent(item.i)}
/>
<TheComponent className="dashboard-card" data={dashboarddata} />
</LoadingSkeleton>
</div>
);
})}
</ResponsiveReactGridLayout>
</div>
);
{state.items.map((item, index) => {
const TheComponent = componentList[item.i].component;
return (
<div
key={item.i}
data-grid={{
...item,
minH: componentList[item.i].minH || 1,
minW: componentList[item.i].minW || 1,
}}
>
<LoadingSkeleton loading={loading}>
<Icon
component={MdClose}
key={item.i}
style={{
position: "absolute",
zIndex: "2",
right: ".25rem",
top: ".25rem",
cursor: "pointer",
}}
onClick={() => handleRemoveComponent(item.i)}
/>
<TheComponent className="dashboard-card" bodyshop={bodyshop} data={dashboarddata}/>
</LoadingSkeleton>
</div>
);
})}
</ResponsiveReactGridLayout>
</div>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
mapStateToProps,
mapDispatchToProps
)(DashboardGridComponent);
const componentList = {
ProductionDollars: {
label: i18next.t("dashboard.titles.productiondollars"),
component: DashboardTotalProductionDollars,
gqlFragment: null,
w: 1,
h: 1,
minW: 2,
minH: 1,
},
ProductionHours: {
label: i18next.t("dashboard.titles.productionhours"),
component: DashboardTotalProductionHours,
gqlFragment: DashboardTotalProductionHoursGql,
w: 3,
h: 1,
minW: 3,
minH: 1,
},
ProjectedMonthlySales: {
label: i18next.t("dashboard.titles.projectedmonthlysales"),
component: DashboardProjectedMonthlySales,
gqlFragment: DashboardProjectedMonthlySalesGql,
w: 2,
h: 1,
minW: 2,
minH: 1,
},
MonthlyRevenueGraph: {
label: i18next.t("dashboard.titles.monthlyrevenuegraph"),
component: DashboardMonthlyRevenueGraph,
gqlFragment: DashboardMonthlyRevenueGraphGql,
w: 4,
h: 2,
minW: 4,
minH: 2,
},
MonthlyJobCosting: {
label: i18next.t("dashboard.titles.monthlyjobcosting"),
component: DashboardMonthlyJobCosting,
gqlFragment: null,
minW: 6,
minH: 3,
w: 6,
h: 3,
},
MonthlyPartsSales: {
label: i18next.t("dashboard.titles.monthlypartssales"),
component: DashboardMonthlyPartsSales,
gqlFragment: null,
minW: 2,
minH: 2,
w: 2,
h: 2,
},
MonthlyLaborSales: {
label: i18next.t("dashboard.titles.monthlylaborsales"),
component: DashboardMonthlyLaborSales,
gqlFragment: null,
minW: 2,
minH: 2,
w: 2,
h: 2,
},
MonthlyEmployeeEfficency: {
label: i18next.t("dashboard.titles.monthlyemployeeefficiency"),
component: DashboardMonthlyEmployeeEfficiency,
gqlFragment: DashboardMonthlyEmployeeEfficiencyGql,
minW: 2,
minH: 2,
w: 2,
h: 2,
},
ScheduleInToday: {
label: i18next.t("dashboard.titles.scheduledintoday", {
date: moment().startOf("day").format("MM/DD/YYYY"),
}),
component: DashboardScheduledInToday,
gqlFragment: DashboardScheduledInTodayGql,
minW: 10,
minH: 2,
w: 10,
h: 2,
},
ScheduleOutToday: {
label: i18next.t("dashboard.titles.scheduledouttoday", {
date: moment().startOf("day").format("MM/DD/YYYY"),
}),
component: DashboardScheduledOutToday,
gqlFragment: DashboardScheduledOutTodayGql,
minW: 10,
minH: 2,
w: 10,
h: 2,
},
ProductionDollars: {
label: i18next.t("dashboard.titles.productiondollars"),
component: DashboardTotalProductionDollars,
gqlFragment: null,
w: 1,
h: 1,
minW: 2,
minH: 1,
},
ProductionHours: {
label: i18next.t("dashboard.titles.productionhours"),
component: DashboardTotalProductionHours,
gqlFragment: DashboardTotalProductionHoursGql,
w: 3,
h: 1,
minW: 3,
minH: 1,
},
ProjectedMonthlySales: {
label: i18next.t("dashboard.titles.projectedmonthlysales"),
component: DashboardProjectedMonthlySales,
gqlFragment: DashboardProjectedMonthlySalesGql,
w: 2,
h: 1,
minW: 2,
minH: 1,
},
MonthlyRevenueGraph: {
label: i18next.t("dashboard.titles.monthlyrevenuegraph"),
component: DashboardMonthlyRevenueGraph,
gqlFragment: DashboardMonthlyRevenueGraphGql,
w: 4,
h: 2,
minW: 4,
minH: 2,
},
MonthlyJobCosting: {
label: i18next.t("dashboard.titles.monthlyjobcosting"),
component: DashboardMonthlyJobCosting,
gqlFragment: null,
minW: 6,
minH: 3,
w: 6,
h: 3,
},
MonthlyPartsSales: {
label: i18next.t("dashboard.titles.monthlypartssales"),
component: DashboardMonthlyPartsSales,
gqlFragment: null,
minW: 2,
minH: 2,
w: 2,
h: 2,
},
MonthlyLaborSales: {
label: i18next.t("dashboard.titles.monthlylaborsales"),
component: DashboardMonthlyLaborSales,
gqlFragment: null,
minW: 2,
minH: 2,
w: 2,
h: 2,
},
// Typo in Efficency should be Efficiency, but changing it would reset users dashboard settings
MonthlyEmployeeEfficency: {
label: i18next.t("dashboard.titles.monthlyemployeeefficiency"),
component: DashboardMonthlyEmployeeEfficiency,
gqlFragment: DashboardMonthlyEmployeeEfficiencyGql,
minW: 2,
minH: 2,
w: 2,
h: 2,
},
ScheduleInToday: {
label: i18next.t("dashboard.titles.scheduledintoday"),
component: DashboardScheduledInToday,
gqlFragment: DashboardScheduledInTodayGql,
minW: 6,
minH: 2,
w: 10,
h: 3,
},
ScheduleOutToday: {
label: i18next.t("dashboard.titles.scheduledouttoday"),
component: DashboardScheduledOutToday,
gqlFragment: DashboardScheduledOutTodayGql,
minW: 6,
minH: 2,
w: 10,
h: 3,
},
JobLifecycle: {
label: i18next.t("dashboard.titles.joblifecycle"),
component: JobLifecycleDashboardComponent,
gqlFragment: JobLifecycleDashboardGQL,
minW: 6,
minH: 3,
w: 6,
h: 3,
},
};
const createDashboardQuery = (state) => {
const componentBasedAdditions =
state &&
Array.isArray(state.layout) &&
state.layout
.map((item, index) => componentList[item.i].gqlFragment || "")
.join("");
return gql`
query QUERY_DASHBOARD_DETAILS {
${componentBasedAdditions || ""}
monthly_sales: jobs(where: {_and: [
{ voided: {_eq: false}},
{date_invoiced: {_gte: "${moment()
const componentBasedAdditions =
state &&
Array.isArray(state.layout) &&
state.layout
.map((item, index) => componentList[item.i].gqlFragment || "")
.join("");
return gql`
query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""}
monthly_sales: jobs(where: {_and: [
{ voided: {_eq: false}},
{date_invoiced: {_gte: "${moment()
.startOf("month")
.startOf("day")
.toISOString()}"}}, {date_invoiced: {_lte: "${moment()
.startOf("day")
.toISOString()}"}}, {date_invoiced: {_lte: "${moment()
.endOf("month")
.endOf("day")
.toISOString()}"}}]}) {
id
ro_number
date_invoiced
job_totals
rate_la1
rate_la2
rate_la3
rate_la4
rate_laa
rate_lab
rate_lad
rate_lae
rate_laf
rate_lag
rate_lam
rate_lar
rate_las
rate_lau
rate_ma2s
rate_ma2t
rate_ma3s
rate_mabl
rate_macs
rate_mahw
rate_mapa
rate_mash
rate_matd
joblines(where: { removed: { _eq: false } }) {
id
mod_lbr_ty
mod_lb_hrs
act_price
part_qty
part_type
}
}
production_jobs: jobs(where: { inproduction: { _eq: true } }) {
id
ro_number
ins_co_nm
job_totals
joblines(where: { removed: { _eq: false } }) {
id
mod_lbr_ty
mod_lb_hrs
act_price
part_qty
part_type
}
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
aggregate {
sum {
mod_lb_hrs
}
}
}
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
aggregate {
sum {
mod_lb_hrs
}
}
}
}
}
`;
id
ro_number
date_invoiced
job_totals
rate_la1
rate_la2
rate_la3
rate_la4
rate_laa
rate_lab
rate_lad
rate_lae
rate_laf
rate_lag
rate_lam
rate_lar
rate_las
rate_lau
rate_ma2s
rate_ma2t
rate_ma3s
rate_mabl
rate_macs
rate_mahw
rate_mapa
rate_mash
rate_matd
joblines(where: { removed: { _eq: false } }) {
id
mod_lbr_ty
mod_lb_hrs
act_price
part_qty
part_type
}
}
production_jobs: jobs(where: { inproduction: { _eq: true } }) {
id
ro_number
ins_co_nm
job_totals
joblines(where: { removed: { _eq: false } }) {
id
mod_lbr_ty
mod_lb_hrs
act_price
part_qty
part_type
}
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
aggregate {
sum {
mod_lb_hrs
}
}
}
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
aggregate {
sum {
mod_lb_hrs
}
}
}
}
}`;
};

View File

@@ -128,7 +128,7 @@
height: 100%;
width: 100%;
.ant-card-body {
height: 80%;
height: calc(100% - 2rem);
width: 100%;
// // background-color: red;
// height: 90%;

View File

@@ -2,7 +2,7 @@ import { Button, Col, Collapse, Result, Row, Space } from "antd";
import React from "react";
import { withTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
import * as Sentry from "@sentry/react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
@@ -138,7 +138,6 @@ class ErrorBoundary extends React.Component {
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withTranslation()(ErrorBoundary));
export default Sentry.withErrorBoundary(
connect(mapStateToProps, mapDispatchToProps)(withTranslation()(ErrorBoundary))
);

View File

@@ -3,16 +3,13 @@ import axios from "axios";
import _ from "lodash";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useHistory } from "react-router-dom";
import { Link } from "react-router-dom";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
export default function GlobalSearchOs() {
const { t } = useTranslation();
const history = useHistory();
const [loading, setLoading] = useState(false);
const [data, setData] = useState(false);
@@ -21,7 +18,7 @@ export default function GlobalSearchOs() {
try {
setLoading(true);
const searchData = await axios.post("/search", {
search: v,
search: v
});
const resultsByType = {
@@ -29,7 +26,7 @@ export default function GlobalSearchOs() {
jobs: [],
bills: [],
owners: [],
vehicles: [],
vehicles: []
};
searchData.data.hits.hits.forEach((hit) => {
@@ -50,16 +47,14 @@ export default function GlobalSearchOs() {
<span>
<OwnerNameDisplay ownerObject={job} />
</span>
<span>{`${job.v_model_yr || ""} ${
job.v_make_desc || ""
} ${job.v_model_desc || ""}`}</span>
<span>{`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""}`}</span>
<span>{`${job.clm_no || ""}`}</span>
<span>{`${job.plate_no || ""}`}</span>
</Space>
</Link>
),
)
};
}),
})
},
{
label: renderTitle(t("menus.header.search.owners")),
@@ -69,53 +64,39 @@ export default function GlobalSearchOs() {
value: OwnerNameDisplayFunction(owner),
label: (
<Link to={`/manage/owners/${owner.id}`}>
<Space
size="small"
split={<Divider type="vertical" />}
wrap
>
<Space size="small" split={<Divider type="vertical" />} wrap>
<span>
<OwnerNameDisplay ownerObject={owner} />
</span>
<PhoneNumberFormatter>
{owner.ownr_ph1}
</PhoneNumberFormatter>
<PhoneNumberFormatter>
{owner.ownr_ph2}
</PhoneNumberFormatter>
<PhoneNumberFormatter>{owner.ownr_ph1}</PhoneNumberFormatter>
<PhoneNumberFormatter>{owner.ownr_ph2}</PhoneNumberFormatter>
</Space>
</Link>
),
)
};
}),
})
},
{
label: renderTitle(t("menus.header.search.vehicles")),
options: resultsByType.vehicles.map((vehicle) => {
return {
key: vehicle.id,
value: `${vehicle.v_model_yr || ""} ${
vehicle.v_make_desc || ""
} ${vehicle.v_model_desc || ""}`,
value: `${vehicle.v_model_yr || ""} ${vehicle.v_make_desc || ""} ${vehicle.v_model_desc || ""}`,
label: (
<Link to={`/manage/vehicles/${vehicle.id}`}>
<Space size="small" split={<Divider type="vertical" />}>
<span>
{`${vehicle.v_model_yr || ""} ${
vehicle.v_make_desc || ""
} ${vehicle.v_model_desc || ""}`}
{`${vehicle.v_model_yr || ""} ${vehicle.v_make_desc || ""} ${vehicle.v_model_desc || ""}`}
</span>
<span>{vehicle.plate_no || ""}</span>
<span>
<VehicleVinDisplay>
{vehicle.v_vin || ""}
</VehicleVinDisplay>
<VehicleVinDisplay>{vehicle.v_vin || ""}</VehicleVinDisplay>
</span>
</Space>
</Link>
),
)
};
}),
})
},
{
label: renderTitle(t("menus.header.search.payments")),
@@ -133,9 +114,9 @@ export default function GlobalSearchOs() {
<span>{payment.transactionid || ""}</span>
</Space>
</Link>
),
)
};
}),
})
},
{
label: renderTitle(t("menus.header.search.bills")),
@@ -151,10 +132,10 @@ export default function GlobalSearchOs() {
<span>{bill.date}</span>
</Space>
</Link>
),
)
};
}),
},
})
}
// {
// label: renderTitle(t("menus.header.search.phonebook")),
// options: resultsByType.search_phonebook.map((pb) => {
@@ -196,15 +177,7 @@ export default function GlobalSearchOs() {
};
return (
<AutoComplete
options={data}
onSearch={handleSearch}
defaultActiveFirstOption
onSelect={(val, opt) => {
history.push(opt.label.props.to);
}}
onClear={() => setData([])}
>
<AutoComplete options={data} onSearch={handleSearch} defaultActiveFirstOption onClear={() => setData([])}>
<Input.Search
size="large"
placeholder={t("general.labels.globalsearch")}

View File

@@ -3,28 +3,19 @@ import { AutoComplete, Divider, Input, Space } from "antd";
import _ from "lodash";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link, useHistory } from "react-router-dom";
import { Link } from "react-router-dom";
import { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import AlertComponent from "../alert/alert.component";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
export default function GlobalSearch() {
const { t } = useTranslation();
const history = useHistory();
const [callSearch, { loading, error, data }] =
useLazyQuery(GLOBAL_SEARCH_QUERY);
const [callSearch, { loading, error, data }] = useLazyQuery(GLOBAL_SEARCH_QUERY);
const executeSearch = (v) => {
if (
v &&
v.variables.search &&
v.variables.search !== "" &&
v.variables.search.length >= 3
)
callSearch(v);
if (v && v.variables.search && v.variables.search !== "" && v.variables.search.length >= 3) callSearch(v);
};
const debouncedExecuteSearch = _.debounce(executeSearch, 750);
@@ -53,15 +44,13 @@ export default function GlobalSearch() {
<span>
<OwnerNameDisplay ownerObject={job} />
</span>
<span>{`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${
job.v_model_desc || ""
}`}</span>
<span>{`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""}`}</span>
<span>{`${job.clm_no || ""}`}</span>
</Space>
</Link>
),
)
};
}),
})
},
{
label: renderTitle(t("menus.header.search.owners")),
@@ -75,45 +64,35 @@ export default function GlobalSearch() {
<span>
<OwnerNameDisplay ownerObject={owner} />
</span>
<PhoneNumberFormatter>
{owner.ownr_ph1}
</PhoneNumberFormatter>
<PhoneNumberFormatter>
{owner.ownr_ph2}
</PhoneNumberFormatter>
<PhoneNumberFormatter>{owner.ownr_ph1}</PhoneNumberFormatter>
<PhoneNumberFormatter>{owner.ownr_ph2}</PhoneNumberFormatter>
</Space>
</Link>
),
)
};
}),
})
},
{
label: renderTitle(t("menus.header.search.vehicles")),
options: data.search_vehicles.map((vehicle) => {
return {
key: vehicle.id,
value: `${vehicle.v_model_yr || ""} ${
vehicle.v_make_desc || ""
} ${vehicle.v_model_desc || ""}`,
value: `${vehicle.v_model_yr || ""} ${vehicle.v_make_desc || ""} ${vehicle.v_model_desc || ""}`,
label: (
<Link to={`/manage/vehicles/${vehicle.id}`}>
<Space size="small" split={<Divider type="vertical" />}>
<span>
{`${vehicle.v_model_yr || ""} ${
vehicle.v_make_desc || ""
} ${vehicle.v_model_desc || ""}`}
{`${vehicle.v_model_yr || ""} ${vehicle.v_make_desc || ""} ${vehicle.v_model_desc || ""}`}
</span>
<span>{vehicle.plate_no || ""}</span>
<span>
<VehicleVinDisplay>
{vehicle.v_vin || ""}
</VehicleVinDisplay>
<VehicleVinDisplay>{vehicle.v_vin || ""}</VehicleVinDisplay>
</span>
</Space>
</Link>
),
)
};
}),
})
},
{
label: renderTitle(t("menus.header.search.payments")),
@@ -131,9 +110,9 @@ export default function GlobalSearch() {
<span>{payment.transactionid || ""}</span>
</Space>
</Link>
),
)
};
}),
})
},
{
label: renderTitle(t("menus.header.search.bills")),
@@ -149,46 +128,35 @@ export default function GlobalSearch() {
<span>{bill.date}</span>
</Space>
</Link>
),
)
};
}),
})
},
{
label: renderTitle(t("menus.header.search.phonebook")),
options: data.search_phonebook.map((pb) => {
return {
key: pb.id,
value: `${pb.firstname || ""} ${pb.lastname || ""} ${
pb.company || ""
}`,
value: `${pb.firstname || ""} ${pb.lastname || ""} ${pb.company || ""}`,
label: (
<Link to={`/manage/phonebook?phonebookentry=${pb.id}`}>
<Space size="small" split={<Divider type="vertical" />}>
<span>{`${pb.firstname || ""} ${pb.lastname || ""} ${
pb.company || ""
}`}</span>
<span>{`${pb.firstname || ""} ${pb.lastname || ""} ${pb.company || ""}`}</span>
<PhoneNumberFormatter>{pb.phone1}</PhoneNumberFormatter>
<span>{pb.email}</span>
</Space>
</Link>
),
)
};
}),
},
})
}
]
: [];
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<AutoComplete
options={options}
onSearch={handleSearch}
defaultActiveFirstOption
onSelect={(val, opt) => {
history.push(opt.label.props.to);
}}
>
<AutoComplete options={options} onSearch={handleSearch} defaultActiveFirstOption>
<Input.Search
size="large"
placeholder={t("general.labels.globalsearch")}

View File

@@ -13,7 +13,7 @@ import Icon, {
FileFilled,
//GlobalOutlined,
HomeFilled,
ImportOutlined,
ImportOutlined, InfoCircleOutlined,
LineChartOutlined,
PaperClipOutlined,
PhoneOutlined,
@@ -26,8 +26,8 @@ import Icon, {
UserOutlined,
} from "@ant-design/icons";
import { useTreatments } from "@splitsoftware/splitio-react";
import { Layout, Menu } from "antd";
import React from "react";
import {Layout, Menu, Switch, Tooltip} from "antd";
import React, {useEffect, useState} from "react";
import { useTranslation } from "react-i18next";
import { BsKanban } from "react-icons/bs";
import {
@@ -52,6 +52,7 @@ import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import {handleBeta, setBeta, checkBeta} from "../../utils/handleBeta";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
@@ -102,9 +103,21 @@ function Header({
{},
bodyshop && bodyshop.imexshopid
);
const [betaSwitch, setBetaSwitch] = useState(false);
const { t } = useTranslation();
useEffect(() => {
const isBeta = checkBeta();
setBetaSwitch(isBeta);
}, []);
const betaSwitchChange = (checked) => {
setBeta(checked);
setBetaSwitch(checked);
handleBeta();
}
return (
<Layout.Header>
<Menu
@@ -431,6 +444,17 @@ function Header({
</Menu.Item>
))}
</Menu.SubMenu>
<Menu.Item style={{marginLeft: 'auto'}} key="profile">
<Tooltip title="A more modern ImEX Online is ready for you to try! You can switch back at any time.">
<InfoCircleOutlined/>
<span style={{marginRight: 8}}>Try the new ImEX Online</span>
<Switch
checked={betaSwitch}
onChange={betaSwitchChange}
/>
</Tooltip>
</Menu.Item>
</Menu>
</Layout.Header>
);

View File

@@ -53,6 +53,7 @@ export default function ScheduleEventContainer({ bodyshop, event, refetch }) {
insertAuditTrail({
jobid: event.job.id,
operation: AuditTrailMapping.appointmentcancel(lost_sale_reason),
type: "appointmentcancel",
})
);
}

View File

@@ -28,8 +28,8 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export function JobChecklistForm({
@@ -183,6 +183,7 @@ export function JobChecklistForm({
(type === "intake" && bodyshop.md_ro_statuses.default_arrived) ||
(type === "deliver" && bodyshop.md_ro_statuses.default_delivered)
),
type: "jobchecklist",
});
} else {
notification["error"]({

View File

@@ -7,21 +7,31 @@ import { connect } from "react-redux";
import { useHistory } from "react-router";
import { createStructuredSelector } from "reselect";
import { UPDATE_JOB_LINES_IOU } from "../../graphql/jobs-lines.queries";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { CreateIouForJob } from "../jobs-detail-header-actions/jobs-detail-header-actions.duplicate.util";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
technician: selectTechnician,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(JobCreateIOU);
export function JobCreateIOU({ bodyshop, currentUser, job, selectedJobLines }) {
export function JobCreateIOU({
bodyshop,
currentUser,
job,
selectedJobLines,
technician,
}) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const client = useApolloClient();
@@ -79,13 +89,19 @@ export function JobCreateIOU({ bodyshop, currentUser, job, selectedJobLines }) {
title={t("jobs.labels.createiouwarning")}
onConfirm={handleCreateIou}
disabled={
!selectedJobLines || selectedJobLines.length === 0 || !job.converted
!selectedJobLines ||
selectedJobLines.length === 0 ||
!job.converted ||
technician
}
>
<Button
loading={loading}
disabled={
!selectedJobLines || selectedJobLines.length === 0 || !job.converted
!selectedJobLines ||
selectedJobLines.length === 0 ||
!job.converted ||
technician
}
>
{t("jobs.actions.createiou")}

View File

@@ -1,15 +1,37 @@
import React from "react";
import { Card } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { selectTechnician } from "../../redux/tech/tech.selectors";
export default function JobDetailCardTemplate({
const mapStateToProps = createStructuredSelector({
technician: selectTechnician,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobDetailCardTemplate);
export function JobDetailCardTemplate({
loading,
title,
extraLink,
technician,
...otherProps
}) {
const { t } = useTranslation();
let extra;
if (extraLink) extra = { extra: <Link to={extraLink}>More</Link> };
if (extraLink && !technician)
extra = {
extra: <Link to={extraLink}>{t("jobs.labels.cards.more")}</Link>,
};
return (
<Card
size="small"

View File

@@ -18,10 +18,8 @@ export default function JobDetailCardsTotalsComponent({ loading, data }) {
/>
<Statistic
className="imex-flex-row__margin-large"
title={t("jobs.fields.ded_amt")}
value={Dinero({
amount: Math.round((data.ded_amt || 0) * 100),
}).toFormat()}
title={t("jobs.fields.customerowing")}
value={Dinero(data.job_totals.totals.custPayable.total).toFormat()}
/>
<Statistic
className="imex-flex-row__margin-large"

View File

@@ -1,14 +1,26 @@
import { useQuery } from "@apollo/client";
import { Col, Divider, Row, Skeleton, Space, Timeline, Typography } from "antd";
import { Col, Row, Skeleton, Timeline, Typography } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { GET_JOB_LINE_ORDERS } from "../../graphql/jobs.queries";
import { selectTechnician } from "../../redux/tech/tech.selectors.js";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
import AlertComponent from "../alert/alert.component";
import BillDetailEditcontainer from "../bill-detail-edit/bill-detail-edit.container.jsx";
export default function JobLinesExpander({ jobline, jobid }) {
const mapStateToProps = createStructuredSelector({
technician: selectTechnician,
});
const mapDispatchToProps = (dispatch) => ({});
export default connect(mapStateToProps, mapDispatchToProps)(JobLinesExpander);
export function JobLinesExpander({ jobline, jobid, technician }) {
const { t } = useTranslation();
const { loading, error, data } = useQuery(GET_JOB_LINE_ORDERS, {
fetchPolicy: "network-only",
@@ -31,15 +43,31 @@ export default function JobLinesExpander({ jobline, jobid }) {
{data.parts_order_lines.length > 0 ? (
data.parts_order_lines.map((line) => (
<Timeline.Item key={line.id}>
<Space split={<Divider type="vertical" />} wrap>
<Link
to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`}
>
{line.parts_order.order_number}
</Link>
<DateFormatter>{line.parts_order.order_date}</DateFormatter>
{line.parts_order.vendor.name}
</Space>
<Row wrap>
<Col span={4}>
{!technician ? (
<Link
to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`}
>
{line.parts_order.order_number}
</Link>
) : (
`${line.parts_order.order_number}`
)}
</Col>
<Col span={4}>
<DateFormatter>{line.parts_order.order_date}</DateFormatter>
</Col>
<Col span={4}>{line.parts_order.vendor.name}</Col>
{line.backordered_eta ? (
<Col span={4}>
<span>
{`${t("parts_orders.fields.backordered_eta")}: `}
<DateFormatter>{line.backordered_eta}</DateFormatter>
</span>
</Col>
) : null}
</Row>
</Timeline.Item>
))
) : (
@@ -51,17 +79,22 @@ export default function JobLinesExpander({ jobline, jobid }) {
</Col>
<Col md={24} lg={12}>
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
<BillDetailEditcontainer />
<Timeline>
{data.billlines.length > 0 ? (
data.billlines.map((line) => (
<Timeline.Item key={line.id}>
<Row wrap>
<Col span={4}>
<Link
to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`}
>
{line.bill.invoice_number}
</Link>
{!technician ? (
<Link
to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`}
>
{line.bill.invoice_number}
</Link>
) : (
`${line.bill.invoice_number}`
)}
</Col>
<Col span={4}>
<span>
@@ -71,7 +104,7 @@ export default function JobLinesExpander({ jobline, jobid }) {
</Col>
<Col span={4}>
<span>
{`${t("billlines.fields.actual_cost")}: `}
{`${t("billlines.fields.actual_cost")}: `}
<CurrencyFormatter>{line.actual_cost}</CurrencyFormatter>
</span>
</Col>
@@ -83,9 +116,7 @@ export default function JobLinesExpander({ jobline, jobid }) {
</Timeline.Item>
))
) : (
<Timeline.Item>
{t("parts_orders.labels.notyetordered")}
</Timeline.Item>
<Timeline.Item>{t("bills.labels.nobilllines")}</Timeline.Item>
)}
</Timeline>
</Col>

View File

@@ -1,12 +1,12 @@
import {
DeleteFilled,
EditFilled,
FilterFilled,
HomeOutlined,
MinusCircleTwoTone,
PlusCircleTwoTone,
SyncOutlined,
WarningFilled,
EditFilled,
PlusCircleTwoTone,
MinusCircleTwoTone,
HomeOutlined,
} from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import {
@@ -20,6 +20,8 @@ import {
Tag,
} from "antd";
import axios from "axios";
import _ from "lodash";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -28,23 +30,19 @@ import { DELETE_JOB_LINE_BY_PK } from "../../graphql/jobs-lines.queries";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { onlyUnique } from "../../utils/arrayHelper";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters";
import JobCreateIOU from "../job-create-iou/job-create-iou.component";
import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component";
import JobLineLocationPopup from "../job-line-location-popup/job-line-location-popup.component";
import JobLineNotePopup from "../job-line-note-popup/job-line-note-popup.component";
import JobLineStatusPopup from "../job-line-status-popup/job-line-status-popup.component";
import JobLinesBillRefernece from "../job-lines-bill-reference/job-lines-bill-reference.component";
// import AllocationsAssignmentContainer from "../allocations-assignment/allocations-assignment.container";
// import AllocationsBulkAssignmentContainer from "../allocations-bulk-assignment/allocations-bulk-assignment.container";
// import AllocationsEmployeeLabelContainer from "../allocations-employee-label/allocations-employee-label.container";
import PartsOrderDrawer from "../parts-order-list-table/parts-order-list-table-drawer.component";
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
import _ from "lodash";
import JobCreateIOU from "../job-create-iou/job-create-iou.component";
import JobLinesExpander from "./job-lines-expander.component";
import { selectBodyshop } from "../../redux/user/user.selectors";
import moment from "moment";
import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -57,6 +55,8 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setModalContext({ context: context, modal: "jobLineEdit" })),
setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
setPartsReceiveContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsReceive" })),
setBillEnterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "billEnter" })),
});
@@ -66,6 +66,7 @@ export function JobLinesComponent({
jobRO,
technician,
setPartsOrderContext,
setPartsReceiveContext,
loading,
refetch,
jobLines,
@@ -74,6 +75,8 @@ export function JobLinesComponent({
setJobLineEditContext,
form,
setBillEnterContext,
billsQuery,
handlePartsOrderOnRowClick,
}) {
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
@@ -341,7 +344,7 @@ export function JobLinesComponent({
key: "actions",
render: (text, record) => (
<Space>
{(record.manual_line || jobIsPrivate) && (
{(record.manual_line || jobIsPrivate) && !technician && (
<>
<Button
disabled={jobRO}
@@ -424,6 +427,14 @@ export function JobLinesComponent({
return (
<div>
<PartsOrderModalContainer />
{!technician && (
<PartsOrderDrawer
job={job}
billsQuery={billsQuery}
handleOnRowClick={handlePartsOrderOnRowClick}
setPartsReceiveContext={setPartsReceiveContext}
/>
)}
<PageHeader
title={t("jobs.labels.estimatelines")}
extra={

View File

@@ -1,6 +1,15 @@
import React, { useMemo, useState } from "react";
import JobLinesComponent from "./job-lines.component";
function JobLinesContainer({ job, joblines, refetch, form, ...rest }) {
function JobLinesContainer({
job,
joblines,
billsQuery,
handleBillOnRowClick,
handlePartsOrderOnRowClick,
refetch,
form,
...rest
}) {
const [searchText, setSearchText] = useState("");
const jobLines = useMemo(() => {
@@ -37,6 +46,9 @@ function JobLinesContainer({ job, joblines, refetch, form, ...rest }) {
<JobLinesComponent
refetch={refetch}
jobLines={jobLines}
billsQuery={billsQuery}
handleBillOnRowClick={handleBillOnRowClick}
handlePartsOrderOnRowClick={handlePartsOrderOnRowClick}
setSearchText={setSearchText}
job={job}
form={form}

View File

@@ -23,7 +23,6 @@ export function JobEmployeeAssignments({
jobRO,
body,
refinish,
prep,
csr,
handleAdd,
@@ -78,7 +77,7 @@ export function JobEmployeeAssignments({
setVisibility(false);
}}
>
Assign
{t("allocations.actions.assign")}
</Button>
<Button onClick={() => setVisibility(false)}>Close</Button>
</Space>

View File

@@ -14,8 +14,8 @@ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export default connect(
mapStateToProps,
@@ -43,12 +43,13 @@ export function JobEmployeeAssignmentsContainer({
});
if (refetch) refetch();
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobassignmentchange(operation, name),
});
if (!!result.errors) {
if (!!!result.errors) {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobassignmentchange(operation, name),
type: "jobassignmentchange",
});
} else {
notification["error"]({
message: t("jobs.errors.assigning", {
message: JSON.stringify(result.errors),
@@ -66,17 +67,19 @@ export function JobEmployeeAssignmentsContainer({
variables: { jobId: job.id, job: { [empAssignment]: null } },
});
if (!!result.errors) {
if (!!!result.errors) {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobassignmentremoved(operation),
type: "jobassignmentremoved",
});
} else {
notification["error"]({
message: t("jobs.errors.assigning", {
message: JSON.stringify(result.errors),
}),
});
}
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobassignmentremoved(operation),
});
setLoading(false);
};

View File

@@ -0,0 +1,246 @@
import React, {useCallback, useEffect, useState} from 'react';
import moment from "moment";
import axios from 'axios';
import {Badge, Card, Space, Table, Tag} from 'antd';
import {gql, useQuery} from "@apollo/client";
import {DateTimeFormatterFunction} from "../../utils/DateFormatter";
import {isEmpty} from "lodash";
import {useTranslation} from "react-i18next";
require('./job-lifecycle.styles.scss');
// show text on bar if text can fit
export function JobLifecycleComponent({job, statuses, ...rest}) {
const [loading, setLoading] = useState(true);
const [lifecycleData, setLifecycleData] = useState(null);
const {t} = useTranslation(); // Used for tracking external state changes.
const {data} = useQuery(gql`
query get_job_test($id: uuid!){
jobs_by_pk(id:$id){
id
status
}
}
`, {
variables: {
id: job.id
},
fetchPolicy: 'cache-only'
});
/**
* Gets the lifecycle data for the job.
* @returns {Promise<void>}
*/
const getLifecycleData = useCallback(async () => {
if (job && job.id && statuses && statuses.statuses) {
try {
setLoading(true);
const response = await axios.post("/job/lifecycle", {
jobids: job.id,
statuses: statuses.statuses
});
const data = response.data.transition[job.id];
setLifecycleData(data);
} catch (err) {
console.error(`${t('job_lifecycle.errors.fetch')}: ${err.message}`);
} finally {
setLoading(false);
}
}
}, [job, statuses, t]);
useEffect(() => {
if (!data) return;
setTimeout(() => {
getLifecycleData().catch(err => console.error(`${t('job_lifecycle.errors.fetch')}: ${err.message}`));
}, 500);
}, [data, getLifecycleData, t]);
const columns = [
{
title: t('job_lifecycle.columns.value'),
dataIndex: 'value',
key: 'value',
},
{
title: t('job_lifecycle.columns.start'),
dataIndex: 'start',
key: 'start',
render: (text) => DateTimeFormatterFunction(text),
sorter: (a, b) => moment(a.start).unix() - moment(b.start).unix(),
},
{
title: t('job_lifecycle.columns.relative_start'),
dataIndex: 'start_readable',
key: 'start_readable',
},
{
title: t('job_lifecycle.columns.end'),
dataIndex: 'end',
key: 'end',
sorter: (a, b) => {
if (isEmpty(a.end) || isEmpty(b.end)) {
if (isEmpty(a.end) && isEmpty(b.end)) {
return 0;
}
return isEmpty(a.end) ? 1 : -1;
}
return moment(a.end).unix() - moment(b.end).unix();
},
render: (text) => isEmpty(text) ? t('job_lifecycle.content.not_available') : DateTimeFormatterFunction(text)
},
{
title: t('job_lifecycle.columns.relative_end'),
dataIndex: 'end_readable',
key: 'end_readable',
},
{
title: t('job_lifecycle.columns.duration'),
dataIndex: 'duration_readable',
key: 'duration_readable',
sorter: (a, b) => a.duration - b.duration,
},
];
return (
<Card loading={loading} title={t('job_lifecycle.content.title')}>
{!loading ? (
lifecycleData && lifecycleData.lifecycle && lifecycleData.durations ? (
<Space direction='vertical' style={{width: '100%'}}>
<Card
type='inner'
title={(
<Space direction='horizontal' size='small'>
<Badge status='processing' count={lifecycleData.durations.totalStatuses}/>
{t('job_lifecycle.content.title_durations')}
</Space>
)}
style={{width: '100%'}}
>
<div id="bar-container" style={{
display: 'flex',
width: '100%',
height: '100px',
textAlign: 'center',
borderRadius: '5px',
borderWidth: '5px',
borderStyle: 'solid',
borderColor: '#f0f2f5',
margin: 0,
padding: 0
}}>
{lifecycleData.durations.summations.map((key, index, array) => {
const isFirst = index === 0;
const isLast = index === array.length - 1;
return (
<div key={key.status} style={{
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
margin: 0,
padding: 0,
borderTop: '1px solid #f0f2f5',
borderBottom: '1px solid #f0f2f5',
borderLeft: isFirst ? '1px solid #f0f2f5' : undefined,
borderRight: isLast ? '1px solid #f0f2f5' : undefined,
backgroundColor: key.color,
width: `${key.percentage}%`
}}
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
>
{key.percentage > 15 ?
<>
<div>{key.roundedPercentage}</div>
<div style={{
backgroundColor: '#f0f2f5',
borderRadius: '5px',
paddingRight: '2px',
paddingLeft: '2px',
fontSize: '0.8rem',
}}>
{key.status}
</div>
</>
: null}
</div>
);
})}
</div>
<Card type='inner' title={t('job_lifecycle.content.legend_title')}
style={{marginTop: '10px'}}>
<div>
{lifecycleData.durations.summations.map((key) => (
<Tag color={key.color} style={{width: '13vh', padding: '4px', margin: '4px'}}>
<div
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
style={{
backgroundColor: '#f0f2f5',
color: '#000',
padding: '4px',
textAlign: 'center'
}}>
{key.status} ({key.roundedPercentage})
</div>
</Tag>
))}
</div>
</Card>
{(lifecycleData?.durations?.humanReadableTotal) ||
(lifecycleData.lifecycle[0] && lifecycleData.lifecycle[0].value && lifecycleData?.durations?.totalCurrentStatusDuration?.humanReadable) ?
<Card style={{marginTop: '10px'}}>
<ul>
{lifecycleData.durations && lifecycleData.durations.humanReadableTotal &&
<li>
<span
style={{fontWeight: 'bold'}}>{t('job_lifecycle.content.previous_status_accumulated_time')}:</span> {lifecycleData.durations.humanReadableTotal}
</li>
}
{lifecycleData.lifecycle[0] && lifecycleData.lifecycle[0].value && lifecycleData?.durations?.totalCurrentStatusDuration?.humanReadable &&
<li>
<span
style={{fontWeight: 'bold'}}>{t('job_lifecycle.content.current_status_accumulated_time')} ({lifecycleData.lifecycle[0].value}):</span> {lifecycleData.durations.totalCurrentStatusDuration.humanReadable}
</li>
}
</ul>
</Card>
: null}
</Card>
<Card type='inner' title={(
<>
<Space direction="horizontal" size="small">
<Badge status='processing' count={lifecycleData.lifecycle.length}/>
{t('job_lifecycle.content.title_transitions')}
</Space>
</>
)}>
<Table style={{
overflow: 'auto',
width: '100%',
}} columns={columns} dataSource={lifecycleData.lifecycle}/>
</Card>
</Space>
) : (
<Card type='inner' style={{textAlign: 'center'}}>
{t('job_lifecycle.content.data_unavailable')}
</Card>
)
) : (
<Card type='inner' title={t('job_lifecycle.content.title_loading')}>
{t('job_lifecycle.content.loading')}
</Card>
)}
</Card>
);
}
export default JobLifecycleComponent;

View File

@@ -23,14 +23,18 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
technician: selectTechnician,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
@@ -41,6 +45,7 @@ export function JobLineConvertToLabor({
jobline,
job,
insertAuditTrail,
technician,
...otherBtnProps
}) {
const { t } = useTranslation();
@@ -113,6 +118,7 @@ export function JobLineConvertToLabor({
hours: calculateAdjustment({ mod_lbr_ty, job, jobline }).toFixed(1),
mod_lbr_ty,
}),
type: "jobmodifylbradj",
});
setLoading(false);
setVisibility(false);
@@ -221,7 +227,7 @@ export function JobLineConvertToLabor({
return (
<>
{children}
{jobline.act_price !== 0 && (
{jobline.act_price !== 0 && !technician && (
<Popover
disabled={jobline.convertedtolbr}
content={overlay}

View File

@@ -82,7 +82,7 @@ export default function JobReconciliationBillsTable({
state.sortedInfo.order,
render: (text, record) => (
<Checkbox disabled checked={record.bill.is_credit_memo} />
<Checkbox checked={record.bill.is_credit_memo} />
),
},
];

View File

@@ -1,16 +1,16 @@
import { useMutation, useLazyQuery } from "@apollo/client";
import { CheckCircleOutlined } from "@ant-design/icons";
import { useLazyQuery, useMutation } from "@apollo/client";
import {
Button,
Card,
Form,
InputNumber,
notification,
Popover,
Space,
notification,
} from "antd";
import moment from "moment";
import React, { useState, useEffect } from "react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
import {
@@ -50,6 +50,7 @@ export default function ScoreboardAddButton({
const handleFinish = async (values) => {
logImEXEvent("job_close_add_to_scoreboard");
values.date = moment(values.date).format("YYYY-MM-DD");
setLoading(true);
let result;
@@ -177,7 +178,7 @@ export default function ScoreboardAddButton({
return acc + job.lbr_adjustments[val];
}, 0);
form.setFieldsValue({
date: new moment(),
date: moment(),
bodyhrs: Math.round(v.bodyhrs * 10) / 10,
painthrs: Math.round(v.painthrs * 10) / 10,
});

View File

@@ -14,8 +14,8 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminStatus);
@@ -32,6 +32,7 @@ export function JobsAdminStatus({ insertAuditTrail, bodyshop, job }) {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobstatuschange(status),
type: "admin_jobstatuschange",
});
// refetch();
})
@@ -53,12 +54,14 @@ export function JobsAdminStatus({ insertAuditTrail, bodyshop, job }) {
);
return (
<Dropdown overlay={statusmenu} trigger={["click"]} key="changestatus">
<Button shape="round">
<span>{job.status}</span>
<>
<Dropdown overlay={statusmenu} trigger={["click"]} key="changestatus">
<Button shape="round">
<span>{job.status}</span>
<DownCircleFilled />
</Button>
</Dropdown>
<DownCircleFilled />
</Button>
</Dropdown>
</>
);
}

View File

@@ -20,8 +20,8 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export default connect(
@@ -57,6 +57,7 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
? DateTimeFormat(changedAuditFields[key])
: changedAuditFields[key]
),
type: "admin_jobfieldchange",
});
});

View File

@@ -1,34 +1,18 @@
import { useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import { gql } from "@apollo/client";
import { Button, Space, notification } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import {
DELETE_DELIVERY_CHECKLIST,
DELETE_INTAKE_CHECKLIST,
} from "../../graphql/jobs.queries";
export default function JobAdminDeleteIntake({ job }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [deleteIntake] = useMutation(gql`
mutation DELETE_INTAKE($jobId: uuid!) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { intakechecklist: null }
) {
id
intakechecklist
}
}
`);
const [DELETE_DELIVERY] = useMutation(gql`
mutation DELETE_DELIVERY($jobId: uuid!) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { deliverchecklist: null }
) {
id
deliverchecklist
}
}
`);
const [deleteIntake] = useMutation(DELETE_INTAKE_CHECKLIST);
const [deleteDelivery] = useMutation(DELETE_DELIVERY_CHECKLIST);
const handleDelete = async (values) => {
setLoading(true);
@@ -50,7 +34,7 @@ export default function JobAdminDeleteIntake({ job }) {
const handleDeleteDelivery = async (values) => {
setLoading(true);
const result = await DELETE_DELIVERY({
const result = await deleteDelivery({
variables: { jobId: job.id },
});
@@ -68,12 +52,22 @@ export default function JobAdminDeleteIntake({ job }) {
return (
<>
<Button loading={loading} onClick={handleDelete}>
{t("jobs.labels.deleteintake")}
</Button>
<Button loading={loading} onClick={handleDeleteDelivery}>
{t("jobs.labels.deletedelivery")}
</Button>
<Space wrap>
<Button
loading={loading}
onClick={handleDelete}
disabled={!job.intakechecklist}
>
{t("jobs.labels.deleteintake")}
</Button>
<Button
loading={loading}
onClick={handleDeleteDelivery}
disabled={!job.deliverychecklist}
>
{t("jobs.labels.deletedelivery")}
</Button>
</Space>
</>
);
}

View File

@@ -1,5 +1,5 @@
import { gql, useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import { useMutation } from "@apollo/client";
import { Button, Space, notification } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -7,6 +7,11 @@ import moment from "moment";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import {
MARK_JOB_AS_EXPORTED,
MARK_JOB_AS_UNINVOICED,
MARK_JOB_FOR_REEXPORT,
} from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import {
selectBodyshop,
@@ -18,8 +23,8 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export default connect(
mapStateToProps,
@@ -35,58 +40,18 @@ export function JobAdminMarkReexport({
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
const [markJobForReexport] = useMutation(gql`
mutation MARK_JOB_FOR_REEXPORT($jobId: uuid!) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { date_exported: null
status: "${bodyshop.md_ro_statuses.default_invoiced}"
}
) {
id
date_exported
status
date_invoiced
}
}
`);
const [markJobExported] = useMutation(gql`
mutation MARK_JOB_AS_EXPORTED($jobId: uuid!, $date_exported: timestamptz!) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { date_exported: $date_exported
status: "${bodyshop.md_ro_statuses.default_exported}"
}
) {
id
date_exported
date_invoiced
status
}
}
`);
const [markJobUninvoiced] = useMutation(gql`
mutation MARK_JOB_AS_UNINVOICED($jobId: uuid!, ) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { date_exported: null
date_invoiced: null
status: "${bodyshop.md_ro_statuses.default_delivered}"
}
) {
id
date_exported
date_invoiced
status
}
}
`);
const [markJobForReexport] = useMutation(MARK_JOB_FOR_REEXPORT);
const [markJobExported] = useMutation(MARK_JOB_AS_EXPORTED);
const [markJobUninvoiced] = useMutation(MARK_JOB_AS_UNINVOICED);
const handleMarkForExport = async () => {
setLoading(true);
const result = await markJobForReexport({
variables: { jobId: job.id },
variables: {
jobId: job.id,
default_invoiced: bodyshop.md_ro_statuses.default_invoiced,
},
});
if (!result.errors) {
@@ -94,6 +59,7 @@ export function JobAdminMarkReexport({
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobmarkforreexport(),
type: "admin_jobmarkforreexport",
});
} else {
notification["error"]({
@@ -108,7 +74,11 @@ export function JobAdminMarkReexport({
const handleMarkExported = async () => {
setLoading(true);
const result = await markJobExported({
variables: { jobId: job.id, date_exported: moment() },
variables: {
jobId: job.id,
date_exported: moment(),
default_exported: bodyshop.md_ro_statuses.default_exported,
},
});
await insertExportLog({
@@ -130,6 +100,7 @@ export function JobAdminMarkReexport({
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobmarkexported(),
type: "admin_jobmarkexported",
});
} else {
notification["error"]({
@@ -144,7 +115,10 @@ export function JobAdminMarkReexport({
const handleUninvoice = async () => {
setLoading(true);
const result = await markJobUninvoiced({
variables: { jobId: job.id },
variables: {
jobId: job.id,
default_delivered: bodyshop.md_ro_statuses.default_delivered,
},
});
if (!result.errors) {
@@ -152,6 +126,7 @@ export function JobAdminMarkReexport({
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobuninvoice(),
type: "admin_jobuninvoice",
});
} else {
notification["error"]({
@@ -165,27 +140,29 @@ export function JobAdminMarkReexport({
return (
<>
<Button
loading={loading}
disabled={!job.date_exported}
onClick={handleMarkForExport}
>
{t("jobs.labels.markforreexport")}
</Button>
<Button
loading={loading}
disabled={job.date_exported}
onClick={handleMarkExported}
>
{t("jobs.actions.markasexported")}
</Button>
<Button
loading={loading}
disabled={!job.date_invoiced || job.date_exported}
onClick={handleUninvoice}
>
{t("jobs.actions.uninvoice")}
</Button>
<Space wrap>
<Button
loading={loading}
disabled={!job.date_exported}
onClick={handleMarkForExport}
>
{t("jobs.labels.markforreexport")}
</Button>
<Button
loading={loading}
disabled={job.date_exported}
onClick={handleMarkExported}
>
{t("jobs.actions.markasexported")}
</Button>
<Button
loading={loading}
disabled={!job.date_invoiced || job.date_exported}
onClick={handleUninvoice}
>
{t("jobs.actions.uninvoice")}
</Button>
</Space>
</>
);
}

View File

@@ -0,0 +1,66 @@
import { useMutation } from "@apollo/client";
import { Switch, notification } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UPDATE_REMOVE_FROM_AR } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminRemoveAR);
export function JobsAdminRemoveAR({ insertAuditTrail, job }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [switchValue, setSwitchValue] = useState(job.remove_from_ar);
const [mutationUpdateRemoveFromAR] = useMutation(UPDATE_REMOVE_FROM_AR);
const handleChange = async (value) => {
setLoading(true);
const result = await mutationUpdateRemoveFromAR({
variables: { jobId: job.id, remove_from_ar: value },
});
if (!result.errors) {
notification["success"]({ message: t("jobs.successes.save") });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_job_remove_from_ar(value),
type: "admin_job_remove_from_ar",
});
setSwitchValue(value);
} else {
notification["error"]({
message: t("jobs.errors.saving", {
error: JSON.stringify(result.errors),
}),
});
}
setLoading(false);
};
return (
<>
<div style={{ display: "flex", alignItems: "center" }}>
<div style={{ marginRight: "10px" }}>
{t("jobs.labels.remove_from_ar")}:
</div>
<div>
<Switch
checked={switchValue}
loading={loading}
onChange={handleChange}
/>
</div>
</div>
</>
);
}

View File

@@ -1,9 +1,10 @@
import { gql, useMutation } from "@apollo/client";
import { 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 { UNVOID_JOB } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import {
selectBodyshop,
@@ -16,8 +17,8 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminUnvoid);
@@ -29,66 +30,17 @@ export function JobsAdminUnvoid({
}) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [updateJob] = useMutation(gql`
mutation UNVOID_JOB($jobId: uuid!) {
update_jobs_by_pk(pk_columns: {id: $jobId}, _set: {voided: false, status: "${
bodyshop.md_ro_statuses.default_imported
}", date_void: null}) {
id
date_void
voided
status
}
insert_notes(objects: {jobid: $jobId, audit: true, created_by: "${
currentUser.email
}", text: "${t("jobs.labels.unvoidnote")}"}) {
returning {
id
}
}
}
`);
// const result = await voidJob({
// variables: {
// jobId: job.id,
// job: {
// status: bodyshop.md_ro_statuses.default_void,
// voided: true,
// },
// note: [
// {
// jobid: job.id,
// created_by: currentUser.email,
// audit: true,
// text: t("jobs.labels.voidnote", {
// date: moment().format("MM/DD/yyy"),
// time: moment().format("hh:mm a"),
// }),
// },
// ],
// },
// });
// if (!!!result.errors) {
// notification["success"]({
// message: t("jobs.successes.voided"),
// });
// //go back to jobs list.
// history.push(`/manage/`);
// } else {
// notification["error"]({
// message: t("jobs.errors.voiding", {
// error: JSON.stringify(result.errors),
// }),
// });
// }
const [mutationUnvoidJob] = useMutation(UNVOID_JOB);
const handleUpdate = async (values) => {
setLoading(true);
const result = await updateJob({
variables: { jobId: job.id },
const result = await mutationUnvoidJob({
variables: {
jobId: job.id,
default_imported: bodyshop.md_ro_statuses.default_imported,
currentUserEmail: currentUser.email,
text: t("jobs.labels.unvoidnote"),
},
});
if (!result.errors) {
@@ -97,6 +49,7 @@ mutation UNVOID_JOB($jobId: uuid!) {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobunvoid(),
type: "admin_jobunvoid",
});
} else {
notification["error"]({
@@ -110,8 +63,10 @@ mutation UNVOID_JOB($jobId: uuid!) {
};
return (
<Button loading={loading} disabled={!job.voided} onClick={handleUpdate}>
{t("jobs.actions.unvoid")}
</Button>
<>
<Button loading={loading} disabled={!job.voided} onClick={handleUpdate}>
{t("jobs.actions.unvoid")}
</Button>
</>
);
}

View File

@@ -6,10 +6,10 @@ import {
useQuery,
} from "@apollo/client";
import { useTreatments } from "@splitsoftware/splitio-react";
import { Col, notification, Row } from "antd";
import { Col, Row, notification } from "antd";
import Axios from "axios";
import Dinero from "dinero.js";
import moment from "moment";
import moment from "moment-business-days";
import queryString from "query-string";
import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -30,8 +30,8 @@ import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import confirmDialog from "../../utils/asyncConfirm";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import confirmDialog from "../../utils/asyncConfirm";
import CriticalPartsScan from "../../utils/criticalPartsScan";
import AlertComponent from "../alert/alert.component";
import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component";
@@ -47,8 +47,8 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export function JobsAvailableContainer({
bodyshop,
@@ -73,7 +73,15 @@ export function JobsAvailableContainer({
const [selectedJob, setSelectedJob] = useState(null);
const [selectedOwner, setSelectedOwner] = useState(null);
const [partsQueueToggle, setPartsQueueToggle] = useState(bodyshop.md_functionality_toggles.parts_queue_toggle);
const [partsQueueToggle, setPartsQueueToggle] = useState(
bodyshop.md_functionality_toggles.parts_queue_toggle
);
const [updateSchComp, setSchComp] = useState({
actual_in: moment(),
checked: false,
scheduled_completion: moment(),
automatic: false,
});
const [insertLoading, setInsertLoading] = useState(false);
@@ -182,6 +190,7 @@ export function JobsAvailableContainer({
insertAuditTrail({
jobid: r.data.insert_jobs.returning[0].id,
operation: AuditTrailMapping.jobimported(),
type: "jobimported",
});
deleteJob({
@@ -197,11 +206,16 @@ export function JobsAvailableContainer({
notification["error"]({
message: t("jobs.errors.creating", { error: err.message }),
});
refetch().catch(e => {console.error(`Something went wrong in jobs available table container - ${err.message || ''}`)});
refetch().catch((e) => {
console.error(
`Something went wrong in jobs available table container - ${
err.message || ""
}`
);
});
setInsertLoading(false);
setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle);
}
};
//Supplement scenario
@@ -225,6 +239,22 @@ export function JobsAvailableContainer({
//IO-539 Check for Parts Rate on PAL for SGI use case.
await CheckTaxRates(supp, bodyshop);
if (updateSchComp.checked === true) {
if (updateSchComp.automatic === true) {
const job_hrs = supp.joblines.data.reduce(
(acc, val) => acc + val.mod_lb_hrs,
0
);
const num_days = job_hrs / bodyshop.target_touchtime;
supp.actual_in = updateSchComp.actual_in;
supp.scheduled_completion = moment(
updateSchComp.actual_in
).businessAdd(num_days, "days");
} else {
supp.scheduled_completion = updateSchComp.scheduled_completion;
}
}
delete supp.owner;
delete supp.vehicle;
delete supp.ins_co_nm;
@@ -261,9 +291,9 @@ export function JobsAvailableContainer({
},
});
setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle);
setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle);
if (CriticalPartsScanning.treatment === "on") {
if (CriticalPartsScanning.treatment === "on") {
CriticalPartsScan(updateResult.data.update_jobs.returning[0].id);
}
if (updateResult.errors) {
@@ -321,6 +351,7 @@ export function JobsAvailableContainer({
insertAuditTrail({
jobid: selectedJob,
operation: AuditTrailMapping.jobsupplement(),
type: "jobsupplement",
});
}
};
@@ -367,7 +398,6 @@ export function JobsAvailableContainer({
if (error) return <AlertComponent type="error" message={error.message} />;
return (
<LoadingSpinner
loading={insertLoading}
@@ -384,7 +414,6 @@ export function JobsAvailableContainer({
visible={ownerModalVisible}
onOk={onOwnerFindModalOk}
onCancel={onOwnerModalCancel}
/>
<JobsFindModalContainer
loading={estDataRaw.loading}
@@ -398,6 +427,8 @@ export function JobsAvailableContainer({
modalSearchState={modalSearchState}
partsQueueToggle={partsQueueToggle}
setPartsQueueToggle={setPartsQueueToggle}
updateSchComp={updateSchComp}
setSchComp={setSchComp}
/>
<Row gutter={[16, 16]}>
<Col span={24}>

View File

@@ -16,8 +16,8 @@ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
@@ -35,6 +35,7 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobstatuschange(status),
type: "jobstatuschange",
});
// refetch();
})

View File

@@ -9,10 +9,12 @@ import { createStructuredSelector } from "reselect";
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import client from "../../utils/GraphQLClient";
const mapStateToProps = createStructuredSelector({
@@ -20,6 +22,11 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
function updateJobCache(items) {
client.cache.modify({
id: "ROOT_QUERY",
@@ -40,6 +47,7 @@ export function JobsCloseExportButton({
disabled,
setSelectedJobs,
refetch,
insertAuditTrail,
}) {
const history = useHistory();
const { t } = useTranslation();
@@ -181,6 +189,11 @@ export function JobsCloseExportButton({
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobexported(),
type: "jobexported",
});
updateJobCache(
jobUpdateResponse.data.update_jobs.returning.map((job) => job.id)
);
@@ -192,12 +205,21 @@ export function JobsCloseExportButton({
});
}
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
if (
bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo &&
successfulTransactions.length > 0
) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobexported(),
type: "jobexported",
});
updateJobCache([
...new Set(
successfulTransactions.map(
@@ -227,4 +249,7 @@ export function JobsCloseExportButton({
);
}
export default connect(mapStateToProps, null)(JobsCloseExportButton);
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsCloseExportButton);

View File

@@ -25,8 +25,8 @@ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export function JobsConvertButton({
@@ -78,6 +78,7 @@ export function JobsConvertButton({
operation: AuditTrailMapping.jobconverted(
res.data.update_jobs.returning[0].ro_number
),
type: "jobconverted",
});
setVisible(false);

View File

@@ -1,4 +1,12 @@
import { Collapse, Form, Input, InputNumber, Select, Switch } from "antd";
import {
Collapse,
Form,
Input,
InputNumber,
Select,
Space,
Switch,
} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -10,6 +18,8 @@ import FormItemEmail from "../form-items-formatted/email-form-item.component";
import FormItemPhone, {
PhoneItemFormatterValidation,
} from "../form-items-formatted/phone-form-item.component";
import JobsDetailChangeEstimator from "../jobs-detail-change-estimator/jobs-detail-change-estimator.component";
import JobsDetailChangeFilehandler from "../jobs-detail-change-filehandler/jobs-detail-change-filehandler.component";
import JobsDetailRatesChangeButton from "../jobs-detail-rates-change-button/jobs-detail-rates-change-button.component";
import JobsDetailRatesParts from "../jobs-detail-rates/jobs-detail-rates.parts.component";
import JobsMarkPstExempt from "../jobs-mark-pst-exempt/jobs-mark-pst-exempt.component";
@@ -25,6 +35,15 @@ const mapDispatchToProps = (dispatch) => ({
export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
const { t } = useTranslation();
const { getFieldValue } = form;
const handleInsCoChange = (value) => {
const selectedCompany = bodyshop.md_ins_cos.find((s) => s.name === value);
if (selectedCompany) {
form.setFieldValue("ins_addr1", selectedCompany.street1);
form.setFieldValue("ins_city", selectedCompany.city);
}
};
return (
<div>
<Collapse defaultActiveKey="insurance">
@@ -34,26 +53,20 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
forceRender
>
<LayoutFormRow>
<Form.Item label={t("jobs.fields.ins_co_id")} name="ins_co_id">
<Form.Item label={t("jobs.fields.clm_no")} name="clm_no">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.policy_no")} name="policy_no">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.clm_no")} name="clm_no">
<Input />
</Form.Item>
<Form.Item
label={t("jobs.fields.regie_number")}
name="regie_number"
>
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.loss_date")} name="loss_date">
<FormDatePicker />
</Form.Item>
<Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm">
<Select>
<Select onChange={handleInsCoChange}>
{bodyshop.md_ins_cos.map((s) => (
<Select.Option key={s.name} value={s.name}>
{s.name}
@@ -67,7 +80,15 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
<Form.Item label={t("jobs.fields.ins_city")} name="ins_city">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.ins_ct_ln")} name="ins_ct_ln">
<Form.Item
label={
<Space>
{t("jobs.fields.ins_ct_ln")}
<JobsDetailChangeFilehandler form={form} />
</Space>
}
name="ins_ct_ln"
>
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.ins_ct_fn")} name="ins_ct_fn">
@@ -95,11 +116,24 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
>
<FormItemEmail email={getFieldValue("ins_ea")} />
</Form.Item>
<Form.Item label={t("jobs.fields.loss_date")} name="loss_date">
<FormDatePicker />
</Form.Item>
<Form.Item label={t("jobs.fields.kmin")} name="kmin">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.est_co_nm")} name="est_co_nm">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.est_ct_fn")} name="est_ct_fn">
<Form.Item
label={
<Space>
{t("jobs.fields.est_ct_fn")}
<JobsDetailChangeEstimator form={form} />
</Space>
}
name="est_ct_fn"
>
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.est_ct_ln")} name="est_ct_ln">

View File

@@ -19,10 +19,13 @@ export default function JobsCreateOwnerInfoNewComponent() {
label={t("owners.fields.ownr_ln")}
name={["owner", "data", "ownr_ln"]}
rules={[
{
required: state.owner.new,
({ getFieldValue }) => ({
required:
state.owner.new &&
(!getFieldValue(["owner", "data", "ownr_co_nm"]) ||
getFieldValue(["owner", "data", "ownr_co_nm"]) === ""),
//message: t("general.validation.required"),
},
}),
]}
>
<Input disabled={!state.owner.new} />
@@ -31,10 +34,13 @@ export default function JobsCreateOwnerInfoNewComponent() {
label={t("owners.fields.ownr_fn")}
name={["owner", "data", "ownr_fn"]}
rules={[
{
required: state.owner.new,
({ getFieldValue }) => ({
required:
state.owner.new &&
(!getFieldValue(["owner", "data", "ownr_co_nm"]) ||
getFieldValue(["owner", "data", "ownr_co_nm"]) === ""),
//message: t("general.validation.required"),
},
}),
]}
>
<Input disabled={!state.owner.new} />
@@ -51,6 +57,17 @@ export default function JobsCreateOwnerInfoNewComponent() {
<Form.Item
label={t("owners.fields.ownr_co_nm")}
name={["owner", "data", "ownr_co_nm"]}
rules={[
({ getFieldValue }) => ({
required:
state.owner.new &&
(!getFieldValue(["owner", "data", "ownr_ln"]) ||
!getFieldValue(["owner", "data", "ownr_fn"]) ||
getFieldValue(["owner", "data", "ownr_ln"]) === "" ||
getFieldValue(["owner", "data", "ownr_fn"]) === ""),
//message: t("general.validation.required"),
}),
]}
>
<Input disabled={!state.owner.new} />
</Form.Item>

View File

@@ -37,6 +37,15 @@ const lossColDamage = { sm: { span: 24 }, md: { span: 6 }, lg: { span: 4 } };
export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
const { getFieldValue } = form;
const { t } = useTranslation();
const handleInsCoChange = (value) => {
const selectedCompany = bodyshop.md_ins_cos.find((s) => s.name === value);
if (selectedCompany) {
form.setFieldValue("ins_addr1", selectedCompany.street1);
form.setFieldValue("ins_city", selectedCompany.city);
}
};
return (
<div>
<FormRow header={t("jobs.forms.claiminfo")}>
@@ -71,7 +80,7 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
</Form.Item>
<Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm">
<Select disabled={jobRO}>
<Select disabled={jobRO} onChange={handleInsCoChange}>
{bodyshop.md_ins_cos.map((s) => (
<Select.Option key={s.name} value={s.name}>
{s.name}

View File

@@ -29,6 +29,7 @@ export default function AddToProduction(
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobinproductionchange(!remove),
type: "jobinproductionchange",
})
);
if (completionCallback) completionCallback();

View File

@@ -52,8 +52,8 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setModalContext({ context: context, modal: "timeTicket" })),
setCardPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "cardPayment" })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export function JobsDetailHeaderActions({
@@ -115,6 +115,7 @@ export function JobsDetailHeaderActions({
? !job.production_vars.alert
: true
),
type: "alertToggle",
});
};
@@ -129,6 +130,13 @@ export function JobsDetailHeaderActions({
},
},
});
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobsuspend(
!!job.suspended ? !job.suspended : true
),
type: "jobsuspend",
});
};
const statusmenu = (
@@ -184,6 +192,7 @@ export function JobsDetailHeaderActions({
jobid: job.id,
operation:
AuditTrailMapping.appointmentcancel(lost_sale_reason),
type: "appointmentcancel",
});
return;
}
@@ -295,7 +304,7 @@ export function JobsDetailHeaderActions({
disabled={!job.converted}
onClick={() => {
setCardPaymentContext({
actions: {},
actions: { refetch },
context: { jobid: job.id },
});
}}
@@ -540,6 +549,11 @@ export function JobsDetailHeaderActions({
notification["success"]({
message: t("jobs.successes.voided"),
});
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobvoid(),
type: "jobvoid",
});
//go back to jobs list.
history.push(`/manage/`);
} else {

View File

@@ -4,7 +4,8 @@ import {
PauseCircleOutlined,
WarningFilled,
} from "@ant-design/icons";
import { Card, Col, Row, Space, Tag, Tooltip } from "antd";
import { Card, Col, Divider, Row, Space, Tag, Tooltip } from "antd";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -15,6 +16,7 @@ import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import DataLabel from "../data-label/data-label.component";
import JobAltTransportChange from "../job-at-change/job-at-change.component";
@@ -61,6 +63,13 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
${job.v_make_desc || ""}
${job.v_model_desc || ""}`.trim();
const bodyHrs = job.joblines
.filter((j) => j.mod_lbr_ty !== "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0);
const refinishHrs = job.joblines
.filter((line) => line.mod_lbr_ty === "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0);
const ownerTitle = OwnerNameDisplayFunction(job).trim();
return (
@@ -92,7 +101,13 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
{job.status === bodyshop.md_ro_statuses.default_scheduled &&
job.scheduled_in ? (
<Tag>
<DateTimeFormatter>{job.scheduled_in}</DateTimeFormatter>
<Link
to={`/manage/schedule?date=${moment(
job.scheduled_in
).format("YYYY-MM-DD")}`}
>
<DateTimeFormatter>{job.scheduled_in}</DateTimeFormatter>
</Link>
</Tag>
) : null}
</Space>
@@ -122,20 +137,23 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
</DataLabel>
{job.cccontracts.length > 0 && (
<DataLabel label={t("jobs.labels.contracts")}>
{job.cccontracts.map((c) => (
<Link
key={c.id}
to={`/manage/courtesycars/contracts/${c.id}`}
>{`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`}</Link>
{job.cccontracts.map((c, index) => (
<Space wrap>
<Link
key={c.id}
to={`/manage/courtesycars/contracts/${c.id}`}
>
{`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`}
{index !== job.cccontracts.length - 1 ? "," : null}
</Link>
</Space>
))}
</DataLabel>
)}
<DataLabel label={t("jobs.fields.production_vars.note")}>
<ProductionListColumnProductionNote record={job} />
</DataLabel>
<Space>
<Space wrap>
{job.special_coverage_policy && (
<Tag color="tomato">
<Space>
@@ -160,19 +178,35 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
<Card
style={{ height: "100%" }}
title={
<Link to={disabled ? "#" : `/manage/owners/${job.owner.id}`}>
{ownerTitle.length > 0
? ownerTitle
: t("owner.labels.noownerinfo")}
</Link>
disabled ? (
<>
{ownerTitle.length > 0
? ownerTitle
: t("owner.labels.noownerinfo")}
</>
) : (
<Link to={`/manage/owners/${job.owner.id}`}>
{ownerTitle.length > 0
? ownerTitle
: t("owner.labels.noownerinfo")}
</Link>
)
}
>
<div>
<DataLabel key="2" label={t("jobs.fields.ownr_ph1")}>
<ChatOpenButton phone={job.ownr_ph1} jobid={job.id} />
{disabled ? (
<PhoneNumberFormatter>{job.ownr_ph1}</PhoneNumberFormatter>
) : (
<ChatOpenButton phone={job.ownr_ph1} jobid={job.id} />
)}
</DataLabel>
<DataLabel key="22" label={t("jobs.fields.ownr_ph2")}>
<ChatOpenButton phone={job.ownr_ph2} jobid={job.id} />
{disabled ? (
<PhoneNumberFormatter>{job.ownr_ph2}</PhoneNumberFormatter>
) : (
<ChatOpenButton phone={job.ownr_ph2} jobid={job.id} />
)}
</DataLabel>
<DataLabel key="3" label={t("owners.fields.address")}>
{`${job.ownr_addr1 || ""} ${job.ownr_addr2 || ""} ${
@@ -180,13 +214,23 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
} ${job.ownr_st || ""} ${job.ownr_zip || ""}`}
</DataLabel>
<DataLabel key="4" label={t("owners.fields.ownr_ea")}>
{job.ownr_ea || ""}
{disabled ? (
<>{job.ownr_ea || ""}</>
) : job.ownr_ea ? (
<a href={`mailto:${job.ownr_ea}`}>{job.ownr_ea}</a>
) : null}
</DataLabel>
{job.owner?.tax_number && (
<DataLabel key="5" label={t("owners.fields.tax_number")}>
{job.owner?.tax_number || ""}
</DataLabel>
)}
<DataLabel
label={t("owners.fields.note")}
valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}
>
{job.owner?.note || ""}
</DataLabel>
</div>
</Card>
</Col>
@@ -195,17 +239,19 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
style={{ height: "100%" }}
title={
job.vehicle ? (
<Link
to={
disabled
? "#"
: job.vehicle && `/manage/vehicles/${job.vehicle.id}`
}
>
{vehicleTitle.length > 0
? vehicleTitle
: t("vehicles.labels.novehinfo")}
</Link>
disabled ? (
<>
{vehicleTitle.length > 0
? vehicleTitle
: t("vehicles.labels.novehinfo")}{" "}
</>
) : (
<Link to={job.vehicle && `/manage/vehicles/${job.vehicle.id}`}>
{vehicleTitle.length > 0
? vehicleTitle
: t("vehicles.labels.novehinfo")}
</Link>
)
) : (
<span></span>
)
@@ -221,12 +267,19 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
<VehicleVinDisplay>
{`${job.v_vin || t("general.labels.na")}`}
</VehicleVinDisplay>
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
job.v_vin?.length !== 17 ? (
<WarningFilled
style={{ color: "tomato", marginLeft: ".3rem" }}
/>
) : null
) : null}
</DataLabel>
<DataLabel label={t("jobs.fields.regie_number")}>
{job.regie_number || t("general.labels.na")}
</DataLabel>
<DataLabel label={t("jobs.labels.relatedros")}>
<JobsRelatedRos jobid={job.id} job={job} />
<JobsRelatedRos jobid={job.id} job={job} disabled={disabled} />
</DataLabel>
{job.vehicle && job.vehicle.notes && (
<DataLabel
@@ -266,6 +319,11 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
>
<div>
<JobEmployeeAssignments job={job} />
<Divider style={{ margin: ".5rem" }} />
<DataLabel label={t("jobs.labels.labor_hrs")}>
{bodyHrs.toFixed(1)} / {refinishHrs.toFixed(1)} /{" "}
{(bodyHrs + refinishHrs).toFixed(1)}
</DataLabel>
</div>
</Card>
</Col>

View File

@@ -1,44 +1,12 @@
import { useQuery } from "@apollo/client";
import queryString from "query-string";
import React from "react";
import { useHistory, useLocation } from "react-router-dom";
import { QUERY_BILLS_BY_JOBID } from "../../graphql/bills.queries";
import JobsDetailPliComponent from "./jobs-detail-pli.component";
export default function JobsDetailPliContainer({ job }) {
const billsQuery = useQuery(QUERY_BILLS_BY_JOBID, {
variables: { jobid: job.id },
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const search = queryString.parse(useLocation().search);
const history = useHistory();
const handleBillOnRowClick = (record) => {
if (record) {
if (record.id) {
search.billid = record.id;
history.push({ search: queryString.stringify(search) });
}
} else {
delete search.billid;
history.push({ search: queryString.stringify(search) });
}
};
const handlePartsOrderOnRowClick = (record) => {
if (record) {
if (record.id) {
search.partsorderid = record.id;
history.push({ search: queryString.stringify(search) });
}
} else {
delete search.partsorderid;
history.push({ search: queryString.stringify(search) });
}
};
export default function JobsDetailPliContainer({
job,
billsQuery,
handleBillOnRowClick,
handlePartsOrderOnRowClick,
}) {
return (
<JobsDetailPliComponent
job={job}

View File

@@ -9,10 +9,12 @@ import { createStructuredSelector } from "reselect";
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { UPDATE_JOBS } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import client from "../../utils/GraphQLClient";
const mapStateToProps = createStructuredSelector({
@@ -20,6 +22,11 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
function updateJobCache(items) {
client.cache.modify({
id: "ROOT_QUERY",
@@ -41,6 +48,7 @@ export function JobsExportAllButton({
loadingCallback,
completedCallback,
refetch,
insertAuditTrail,
}) {
const { t } = useTranslation();
const [updateJob] = useMutation(UPDATE_JOBS);
@@ -177,6 +185,13 @@ export function JobsExportAllButton({
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
jobUpdateResponse.data.update_jobs.returning.forEach((job) => {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobexported(),
type: "jobexported",
});
});
updateJobCache(
jobUpdateResponse.data.update_jobs.returning.map(
(job) => job.id
@@ -190,13 +205,17 @@ export function JobsExportAllButton({
});
}
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
if (
bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo &&
successfulTransactions.length > 0
) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
updateJobCache([
const successfulTransactionsSet = [
...new Set(
successfulTransactions.map(
(st) =>
@@ -207,7 +226,15 @@ export function JobsExportAllButton({
]
)
),
]);
];
if (successfulTransactionsSet.length > 0) {
insertAuditTrail({
jobid: successfulTransactionsSet[0],
operation: AuditTrailMapping.jobexported(),
type: "jobexported",
});
}
updateJobCache(successfulTransactionsSet);
}
}
})
@@ -225,4 +252,7 @@ export function JobsExportAllButton({
);
}
export default connect(mapStateToProps, null)(JobsExportAllButton);
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsExportAllButton);

View File

@@ -1,9 +1,11 @@
import { SyncOutlined } from "@ant-design/icons";
import { Checkbox, Divider, Input, Table, Button } from "antd";
import React from "react";
import { Button, Checkbox, Divider, Input, Space, Table } from "antd";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import PhoneFormatter from "../../utils/PhoneFormatter";
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
export default function JobsFindModalComponent({
@@ -16,11 +18,13 @@ export default function JobsFindModalComponent({
jobsListRefetch,
partsQueueToggle,
setPartsQueueToggle,
updateSchComp,
setSchComp,
}) {
const { t } = useTranslation();
const [modalSearch, setModalSearch] = modalSearchState;
const [importOptions, setImportOptions] = importOptionsState;
const [checkUTT, setCheckUTT] = useState(false);
const columns = [
{
title: t("jobs.fields.ro_number"),
@@ -142,6 +146,35 @@ export default function JobsFindModalComponent({
if (record) {
if (record.id) {
setSelectedJob(record.id);
if (record.actual_in && record.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: record.actual_in,
scheduled_completion: record.scheduled_completion,
});
} else {
if (record.actual_in && !record.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: record.actual_in,
scheduled_completion: moment(),
});
}
if (!record.actual_in && record.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: moment(),
scheduled_completion: moment(record.scheduled_completion),
});
}
if (!record.actual_in && !record.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: moment(),
scheduled_completion: moment(),
});
}
}
return;
}
}
@@ -177,6 +210,35 @@ export default function JobsFindModalComponent({
rowSelection={{
onSelect: (props) => {
setSelectedJob(props.id);
if (props.actual_in && props.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: props.actual_in,
scheduled_completion: props.scheduled_completion,
});
} else {
if (props.actual_in && !props.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: props.actual_in,
scheduled_completion: moment(),
});
}
if (!props.actual_in && props.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: moment(),
scheduled_completion: moment(props.scheduled_completion),
});
}
if (!props.actual_in && !props.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: moment(),
scheduled_completion: moment(),
});
}
}
},
type: "radio",
selectedRowKeys: [selectedJob],
@@ -190,23 +252,58 @@ export default function JobsFindModalComponent({
}}
/>
<Divider />
<Checkbox
defaultChecked={importOptions.overrideHeader}
onChange={(e) =>
setImportOptions({
...importOptions,
overrideHeaders: e.target.checked,
})
}
>
{t("jobs.labels.override_header")}
</Checkbox>
<Checkbox
<Space>
<Checkbox
defaultChecked={importOptions.overrideHeader}
onChange={(e) =>
setImportOptions({
...importOptions,
overrideHeaders: e.target.checked,
})
}
>
{t("jobs.labels.override_header")}
</Checkbox>
<Checkbox
checked={partsQueueToggle}
onChange={(e) => setPartsQueueToggle(e.target.checked)}
>
{t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")}
</Checkbox>
>
{t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")}
</Checkbox>
<Checkbox
checked={updateSchComp.checked}
onChange={(e) =>
setSchComp({ ...updateSchComp, checked: e.target.checked })
}
>
{t("jobs.labels.update_scheduled_completion")}
</Checkbox>
{updateSchComp.checked === true ? (
<>
{checkUTT === false ? (
<FormDateTimePickerComponent
value={updateSchComp.scheduled_completion}
onChange={(e) => {
setSchComp({ ...updateSchComp, scheduled_completion: e });
}}
/>
) : null}
<Checkbox
checked={checkUTT}
onChange={(e) => {
setCheckUTT(e.target.checked);
setSchComp({
...updateSchComp,
scheduled_completion: null,
automatic: true,
});
}}
>
{t("jobs.labels.calc_scheuled_completion")}
</Checkbox>
</>
) : null}
</Space>
</div>
);
}

View File

@@ -26,6 +26,8 @@ export default connect(
modalSearchState,
partsQueueToggle,
setPartsQueueToggle,
updateSchComp,
setSchComp,
...modalProps
}) {
const { t } = useTranslation();
@@ -95,6 +97,8 @@ export default connect(
modalSearchState={modalSearchState}
partsQueueToggle={partsQueueToggle}
setPartsQueueToggle={setPartsQueueToggle}
updateSchComp={updateSchComp}
setSchComp={setSchComp}
/>
) : null}
</Modal>

View File

@@ -10,9 +10,11 @@ import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { pageLimit } from "../../utils/config";
import { alphaSort, statusSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
import StartChatButton from "../chat-open-button/chat-open-button.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
@@ -25,6 +27,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
const search = queryString.parse(useLocation().search);
const [openSearchResults, setOpenSearchResults] = useState([]);
const [searchLoading, setSearchLoading] = useState(false);
const [filter, setFilter] = useLocalStorage("filter_jobs_all", null);
const { page, sortcolumn, sortorder } = search;
const history = useHistory();
@@ -34,7 +37,10 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: true, //(a, b) => alphaSort(a.ro_number, b.ro_number),
sorter: search?.search
? (a, b) =>
parseInt((a.ro_number || "0").replace(/\D/g, "")) - parseInt((b.ro_number || "0").replace(/\D/g, ""))
: true,
sortOrder: sortcolumn === "ro_number" && sortorder,
render: (text, record) => (
<Link to={"/manage/jobs/" + record.id}>
@@ -48,7 +54,6 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
key: "ownr_ln",
ellipsis: true,
//sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
//sortOrder: sortcolumn === "ownr_ln" && sortorder,
render: (text, record) => {
return record.ownerid ? (
@@ -66,7 +71,6 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
ellipsis: true,
render: (text, record) => (
<StartChatButton phone={record.ownr_ph1} jobid={record.id} />
@@ -76,7 +80,6 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
title: t("jobs.fields.ownr_ph2"),
dataIndex: "ownr_ph2",
key: "ownr_ph2",
ellipsis: true,
render: (text, record) => (
<StartChatButton phone={record.ownr_ph2} jobid={record.id} />
@@ -86,13 +89,13 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
ellipsis: true,
sorter: true, // (a, b) => alphaSort(a.status, b.status),
sorter: search?.search ? (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.active_statuses) : true,
sortOrder: sortcolumn === "status" && sortorder,
render: (text, record) => {
return record.status || t("general.labels.na");
},
filteredValue: filter?.status || null,
filters: bodyshop.md_ro_statuses.statuses.map((s) => {
return { text: s, value: [s] };
}),
@@ -103,7 +106,6 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
render: (text, record) => {
return record.vehicleid ? (
@@ -124,7 +126,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
dataIndex: "plate_no",
key: "plate_no",
ellipsis: true,
sorter: true, //(a, b) => alphaSort(a.plate_no, b.plate_no),
sorter: search?.search ? (a, b) => alphaSort(a.plate_no, b.plate_no) : true,
sortOrder: sortcolumn === "plate_no" && sortorder,
render: (text, record) => {
return record.plate_no ? record.plate_no : "";
@@ -135,7 +137,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
dataIndex: "clm_no",
key: "clm_no",
ellipsis: true,
sorter: true, //(a, b) => alphaSort(a.clm_no, b.clm_no),
sorter: search?.search ? (a, b) => alphaSort(a.clm_no, b.clm_no) : true,
sortOrder: sortcolumn === "clm_no" && sortorder,
render: (text, record) =>
`${record.clm_no || ""}${
@@ -153,7 +155,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
dataIndex: "clm_total",
key: "clm_total",
sorter: true, //(a, b) => a.clm_total - b.clm_total,
sorter: search?.search ? (a, b) => a.clm_total - b.clm_total : true,
sortOrder: sortcolumn === "clm_total" && sortorder,
render: (text, record) => {
return record.clm_total ? (
@@ -167,7 +169,6 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
title: t("jobs.fields.owner_owing"),
dataIndex: "owner_owing",
key: "owner_owing",
render: (text, record) => (
<CurrencyFormatter>{record.owner_owing}</CurrencyFormatter>
),
@@ -189,6 +190,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
} else {
delete search.statusFilters;
}
setFilter(filters);
history.push({ search: queryString.stringify(search) });
};

View File

@@ -1,8 +1,8 @@
import {
SyncOutlined,
ExclamationCircleFilled,
PauseCircleOutlined,
BranchesOutlined,
BranchesOutlined,
ExclamationCircleFilled,
PauseCircleOutlined,
SyncOutlined,
} from "@ant-design/icons";
import { useQuery } from "@apollo/client";
import { Button, Card, Grid, Input, Space, Table, Tooltip } from "antd";
@@ -14,382 +14,411 @@ import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { onlyUnique } from "../../utils/arrayHelper";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters";
import { onlyUnique } from "../../utils/arrayHelper";
import { alphaSort, statusSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
import AlertComponent from "../alert/alert.component";
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import { OwnerNameDisplayFunction } from "./../owner-name-display/owner-name-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop,
});
export function JobsList({ bodyshop }) {
const searchParams = queryString.parse(useLocation().search);
const { selected } = searchParams;
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
variables: {
statuses: bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"],
},
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const searchParams = queryString.parse(useLocation().search);
const { selected } = searchParams;
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
variables: {
statuses: bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"],
},
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
});
const [state, setState] = useState({ sortedInfo: {} });
const [filter, setFilter] = useLocalStorage("filter_jobs_list", null);
const { t } = useTranslation();
const history = useHistory();
const [searchText, setSearchText] = useState("");
const { t } = useTranslation();
const history = useHistory();
const [searchText, setSearchText] = useState("");
if (error) return <AlertComponent message={error.message} type="error" />;
if (error) return <AlertComponent message={error.message} type="error" />;
const jobs = data
? searchText === ""
? data.jobs
: data.jobs.filter(
(j) =>
(j.ro_number || "")
.toString()
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.ownr_co_nm || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.comments || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.ownr_fn || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.ownr_ln || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.clm_no || "").toLowerCase().includes(searchText.toLowerCase()) ||
(j.plate_no || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.v_model_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.est_ct_fn || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.est_ct_ln || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.v_make_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase())
)
: [];
const jobs = data
? searchText === ""
? data.jobs
: data.jobs.filter(
(j) =>
(j.ro_number || "")
.toString()
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.ownr_co_nm || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.comments || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.ownr_fn || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.ownr_ln || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.clm_no || "").toLowerCase().includes(searchText.toLowerCase()) ||
(j.plate_no || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.v_model_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.est_ct_fn || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.est_ct_ln || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.v_make_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase())
)
: [];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, sortedInfo: sorter });
setFilter(filters);
};
const handleOnRowClick = (record) => {
if (record) {
if (record.id) {
history.push({
search: queryString.stringify({
...searchParams,
selected: record.id,
}),
});
}
}
};
const handleOnRowClick = (record) => {
if (record) {
if (record.id) {
history.push({
search: queryString.stringify({
...searchParams,
selected: record.id,
}),
});
}
}
};
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: (a, b) =>
parseInt((a.ro_number || "0").replace(/\D/g, "")) -
parseInt((b.ro_number || "0").replace(/\D/g, "")),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
<Link
to={"/manage/jobs/" + record.id}
onClick={(e) => e.stopPropagation()}
>
<Space>
{record.ro_number || t("general.labels.na")}
{record.production_vars && record.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
{record.iouparent && (
<Tooltip title={t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} />
</Tooltip>
)}
</Space>
</Link>
),
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.ownerid ? (
<Link
to={"/manage/owners/" + record.ownerid}
onClick={(e) => e.stopPropagation()}
>
<OwnerNameDisplay ownerObject={record} />
</Link>
) : (
<span>
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: (a, b) =>
parseInt((a.ro_number || "0").replace(/\D/g, "")) -
parseInt((b.ro_number || "0").replace(/\D/g, "")),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
<Link
to={"/manage/jobs/" + record.id}
onClick={(e) => e.stopPropagation()}
>
<Space>
{record.ro_number || t("general.labels.na")}
{record.production_vars && record.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
{record.iouparent && (
<Tooltip title={t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} />
</Tooltip>
)}
</Space>
</Link>
),
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.ownerid ? (
<Link
to={"/manage/owners/" + record.ownerid}
onClick={(e) => e.stopPropagation()}
>
<OwnerNameDisplay ownerObject={record} />
</Link>
) : (
<span>
<OwnerNameDisplay ownerObject={record} />
</span>
);
);
},
},
{
title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph1} jobid={record.id} />
),
},
{
title: t("jobs.fields.ownr_ph2"),
dataIndex: "ownr_ph2",
key: "ownr_ph2",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph2} jobid={record.id} />
),
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
ellipsis: true,
sorter: (a, b) =>
statusSort(a.status, b.status, bodyshop.md_ro_statuses.active_statuses),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filteredValue: filter?.status || null,
filters:
(jobs &&
jobs
.map((j) => j.status)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
value: [s],
};
})
.sort((a, b) =>
statusSort(
a.text,
b.text,
bodyshop.md_ro_statuses.active_statuses
)
)) ||
[],
onFilter: (value, record) => value.includes(record.status),
},
{
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}
onClick={(e) => e.stopPropagation()}
>
{`${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("vehicles.fields.plate_no"),
dataIndex: "plate_no",
key: "plate_no",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
sortOrder:
state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
},
{
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder:
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
render: (text, record) =>
`${record.clm_no || ""}${
record.po_number ? ` (PO: ${record.po_number})` : ""
}`,
},
{
title: t("jobs.fields.ins_co_nm"),
dataIndex: "ins_co_nm",
key: "ins_co_nm",
ellipsis: true,
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
sortOrder:
state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,
filteredValue: filter?.ins_co_nm || null,
filters:
(jobs &&
jobs
.map((j) => j.ins_co_nm)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Ins. Co.*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.ins_co_nm),
responsive: ["md"],
},
{
title: t("jobs.fields.clm_total"),
dataIndex: "clm_total",
key: "clm_total",
responsive: ["md"],
ellipsis: true,
sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder:
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
),
},
{
title: t("jobs.labels.estimator"),
dataIndex: "jobs.labels.estimator",
key: "estimator",
ellipsis: true,
responsive: ["xl"],
sorter: (a, b) =>
alphaSort(
`${a.est_ct_fn || ""} ${a.est_ct_ln || ""}`.trim(),
`${b.est_ct_fn || ""} ${b.est_ct_ln || ""}`.trim()
),
sortOrder:
state.sortedInfo.columnKey === "estimator" && state.sortedInfo.order,
filterSearch: true,
filteredValue: filter?.estimator || null,
filters:
(jobs &&
jobs
.map((j) => `${j.est_ct_fn || ""} ${j.est_ct_ln || ""}`.trim())
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Estimator*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) =>
value.includes(
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim()
),
render: (text, record) =>
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim(),
},
{
title: t("jobs.fields.comment"),
dataIndex: "comment",
key: "comment",
ellipsis: true,
responsive: ["md"],
},
// {
// title: t("jobs.fields.owner_owing"),
// dataIndex: "owner_owing",
// key: "owner_owing",
// responsive: ["md"],
// render: (text, record) => (
// <CurrencyFormatter>{record.owner_owing}</CurrencyFormatter>
// ),
// },
];
const scrollMapper = {
xs: true,
sm: true,
md: true,
lg: "100%",
xl: "100%",
xxl: "100%",
};
return (
<Card
title={t("titles.bc.jobs-active")}
extra={
<Space wrap>
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<Input.Search
placeholder={t("general.labels.search")}
onChange={(e) => {
setSearchText(e.target.value);
}}
value={searchText}
enterButton
/>
</Space>
}
>
<Table
loading={loading}
pagination={{ defaultPageSize: 50 }}
columns={columns}
rowKey="id"
dataSource={jobs}
scroll={{
x: selectedBreakpoint ? scrollMapper[selectedBreakpoint[0]] : "100%",
}}
rowSelection={{
onSelect: (record) => {
handleOnRowClick(record);
},
selectedRowKeys: [selected],
type: "radio",
}}
onChange={handleTableChange}
onRow={(record, rowIndex) => {
return {
onClick: (event) => {
handleOnRowClick(record);
},
},
{
title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph1} jobid={record.id} />
),
},
{
title: t("jobs.fields.ownr_ph2"),
dataIndex: "ownr_ph2",
key: "ownr_ph2",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph2} jobid={record.id} />
),
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
ellipsis: true,
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filters:
(jobs &&
jobs
.map((j) => j.status)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
value: [s],
};
})) ||
[],
onFilter: (value, record) => value.includes(record.status),
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
render: (text, record) => {
return record.vehicleid ? (
<Link
to={"/manage/vehicles/" + record.vehicleid}
onClick={(e) => e.stopPropagation()}
>
{`${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("vehicles.fields.plate_no"),
dataIndex: "plate_no",
key: "plate_no",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
sortOrder:
state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
},
{
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder:
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
render: (text, record) =>
`${record.clm_no || ""}${
record.po_number ? ` (PO: ${record.po_number})` : ""
}`,
},
{
title: t("jobs.fields.ins_co_nm"),
dataIndex: "ins_co_nm",
key: "ins_co_nm",
ellipsis: true,
filters:
(jobs &&
jobs
.map((j) => j.ins_co_nm)
.filter(onlyUnique)
.map((s) => {
return {
text: s,
value: [s],
};
})) ||
[],
onFilter: (value, record) => value.includes(record.ins_co_nm),
responsive: ["md"],
},
{
title: t("jobs.fields.clm_total"),
dataIndex: "clm_total",
key: "clm_total",
responsive: ["md"],
ellipsis: true,
sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder:
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
),
},
{
title: t("jobs.labels.estimator"),
dataIndex: "jobs.labels.estimator",
key: "jobs.labels.estimator",
ellipsis: true,
responsive: ["xl"],
filterSearch: true,
filters:
(jobs &&
jobs
.map((j) => `${j.est_ct_fn || ""} ${j.est_ct_ln || ""}`.trim())
.filter(onlyUnique)
.map((s) => {
return {
text: s || "N/A",
value: [s],
};
})) ||
[],
onFilter: (value, record) =>
value.includes(
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim()
),
render: (text, record) =>
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim(),
},
{
title: t("jobs.fields.comment"),
dataIndex: "comment",
key: "comment",
ellipsis: true,
responsive: ["md"],
},
// {
// title: t("jobs.fields.owner_owing"),
// dataIndex: "owner_owing",
// key: "owner_owing",
// responsive: ["md"],
// render: (text, record) => (
// <CurrencyFormatter>{record.owner_owing}</CurrencyFormatter>
// ),
// },
];
const scrollMapper = {
xs: true,
sm: true,
md: true,
lg: "100%",
xl: "100%",
xxl: "100%",
};
return (
<Card
title={t("titles.bc.jobs-active")}
extra={
<Space wrap>
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<Input.Search
placeholder={t("general.labels.search")}
onChange={(e) => {
setSearchText(e.target.value);
}}
value={searchText}
enterButton
/>
</Space>
}
>
<Table
loading={loading}
pagination={{ defaultPageSize: 50 }}
columns={columns}
rowKey="id"
dataSource={jobs}
scroll={{
x: selectedBreakpoint ? scrollMapper[selectedBreakpoint[0]] : "100%",
}}
rowSelection={{
onSelect: (record) => {
handleOnRowClick(record);
},
selectedRowKeys: [selected],
type: "radio",
}}
onChange={handleTableChange}
onRow={(record, rowIndex) => {
return {
onClick: (event) => {
handleOnRowClick(record);
},
};
}}
/>
</Card>
);
};
}}
/>
</Card>
);
}
export default connect(mapStateToProps, null)(JobsList);

View File

@@ -19,8 +19,8 @@ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export default connect(mapStateToProps, mapDispatchToProps)(JobNotesContainer);
@@ -49,6 +49,7 @@ export function JobNotesContainer({ jobId, insertAuditTrail }) {
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobnotedeleted(),
type: "jobnotedeleted",
});
});
setDeleteLoading(false);

View File

@@ -16,11 +16,12 @@ import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { onlyUnique } from "../../utils/arrayHelper";
import { alphaSort } from "../../utils/sorters";
import { pageLimit } from "../../utils/config";
import { alphaSort, statusSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
import AlertComponent from "../alert/alert.component";
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -53,10 +54,8 @@ export function JobsReadyList({ bodyshop }) {
nextFetchPolicy: "network-only",
});
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
});
const [state, setState] = useState({ sortedInfo: {} });
const [filter, setFilter] = useLocalStorage("filter_jobs_ready", null);
const { t } = useTranslation();
const history = useHistory();
@@ -105,7 +104,8 @@ export function JobsReadyList({ bodyshop }) {
: [];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
setState({ ...state, sortedInfo: sorter });
setFilter(filters);
};
const handleOnRowClick = (record) => {
@@ -129,7 +129,6 @@ export function JobsReadyList({ bodyshop }) {
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}
@@ -157,7 +156,6 @@ export function JobsReadyList({ bodyshop }) {
dataIndex: "owner",
key: "owner",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sortOrder:
@@ -197,16 +195,15 @@ export function JobsReadyList({ bodyshop }) {
<ChatOpenButton phone={record.ownr_ph2} jobid={record.id} />
),
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
ellipsis: true,
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filteredValue: filter?.status || null,
filters:
(jobs &&
jobs
@@ -217,11 +214,17 @@ export function JobsReadyList({ bodyshop }) {
text: s || "No Status*",
value: [s],
};
})) ||
})
.sort((a, b) =>
statusSort(
a.text,
b.text,
bodyshop.md_ro_statuses.active_statuses
)
)) ||
[],
onFilter: (value, record) => value.includes(record.status),
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
@@ -274,6 +277,7 @@ export function JobsReadyList({ bodyshop }) {
dataIndex: "ins_co_nm",
key: "ins_co_nm",
ellipsis: true,
filteredValue: filter?.ins_co_nm || null,
filters:
(jobs &&
jobs
@@ -281,10 +285,11 @@ export function JobsReadyList({ bodyshop }) {
.filter(onlyUnique)
.map((s) => {
return {
text: s,
text: s || "No Ins Co.*",
value: [s],
};
})) ||
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.ins_co_nm),
responsive: ["md"],
@@ -295,7 +300,6 @@ export function JobsReadyList({ bodyshop }) {
key: "clm_total",
responsive: ["md"],
ellipsis: true,
sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder:
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
@@ -306,9 +310,10 @@ export function JobsReadyList({ bodyshop }) {
{
title: t("jobs.labels.estimator"),
dataIndex: "jobs.labels.estimator",
key: "jobs.labels.estimator",
key: "estimator",
ellipsis: true,
responsive: ["xl"],
filteredValue: filter?.estimator || null,
filterSearch: true,
filters:
(jobs &&
@@ -317,10 +322,11 @@ export function JobsReadyList({ bodyshop }) {
.filter(onlyUnique)
.map((s) => {
return {
text: s || "N/A",
text: s || "No Estimator*",
value: [s],
};
})) ||
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) =>
value.includes(

View File

@@ -2,7 +2,7 @@ import { Space, Tag } from "antd";
import React from "react";
import { Link } from "react-router-dom";
export default function JobsRelatedRos({ jobid, job }) {
export default function JobsRelatedRos({ jobid, job, disabled }) {
if (!(job && job.vehicle && job.vehicle.jobs)) return null;
return (
<Space wrap>
@@ -10,9 +10,15 @@ export default function JobsRelatedRos({ jobid, job }) {
.filter((j) => j.id !== job.id)
.map((j) => (
<Tag key={j.id}>
<Link to={`/manage/jobs/${j?.id}`}>{`${j.ro_number || "N/A"}${
j.clm_no ? ` | ${j.clm_no}` : ""
}${j.status ? ` | ${j.status}` : ""}`}</Link>
{disabled ? (
<>{`${j.ro_number || "N/A"}${j.clm_no ? ` | ${j.clm_no}` : ""}${
j.status ? ` | ${j.status}` : ""
}`}</>
) : (
<Link to={`/manage/jobs/${j?.id}`}>{`${j.ro_number || "N/A"}${
j.clm_no ? ` | ${j.clm_no}` : ""
}${j.status ? ` | ${j.status}` : ""}`}</Link>
)}
</Tag>
))}
</Space>

View File

@@ -20,8 +20,8 @@ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export default connect(
mapStateToProps,
@@ -76,6 +76,7 @@ export function LaborAllocationsAdjustmentEdit({
values.hours -
((adjustments && adjustments[mod_lbr_ty]) || 0).toFixed(1),
}),
type: "jobmodifylbradj",
});
}
setLoading(false);

View File

@@ -19,8 +19,8 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("noteUpsert")),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export function NoteUpsertModalContainer({
@@ -70,6 +70,7 @@ export function NoteUpsertModalContainer({
insertAuditTrail({
jobid: context.jobId,
operation: AuditTrailMapping.jobnoteupdated(),
type: "jobnoteupdated",
});
});
if (refetch) refetch();
@@ -102,6 +103,7 @@ export function NoteUpsertModalContainer({
insertAuditTrail({
jobid: newJobId,
operation: AuditTrailMapping.jobnoteadded(),
type: "jobnoteadded",
});
});
}
@@ -115,6 +117,7 @@ export function NoteUpsertModalContainer({
insertAuditTrail({
jobid: context.jobId,
operation: AuditTrailMapping.jobnoteadded(),
type: "jobnoteadded",
});
}
};

View File

@@ -6,7 +6,8 @@ import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort, statusSort } from "../../utils/sorters";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
import OwnerDetailUpdateJobsComponent from "../owner-detail-update-jobs/owner-detail-update-jobs.component";
const mapStateToProps = createStructuredSelector({
@@ -24,7 +25,7 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const columns = [
{
title: t("jobs.fields.ro_number"),
@@ -44,6 +45,15 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
title: t("jobs.fields.vehicle"),
dataIndex: "vehicleid",
key: "vehicleid",
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 === "vehicleid" && state.sortedInfo.order,
render: (text, record) =>
record.vehicleid ? (
<Link to={`/manage/vehicles/${record.vehicleid}`}>
@@ -67,11 +77,28 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
sorter: (a, b) =>
statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filters: bodyshop.md_ro_statuses.statuses.map((status) => ({
text: status,
value: status,
})),
onFilter: (value, record) => value.includes(record.status),
},
{
title: t("jobs.fields.actual_completion"),
dataIndex: "actual_completion",
key: "actual_completion",
render: (text, record) => (
<DateTimeFormatter>{record.actual_completion}</DateTimeFormatter>
),
sorter: (a, b) => dateSort(a.actual_completion, b.actual_completion),
sortOrder:
state.sortedInfo.columnKey === "actual_completion" &&
state.sortedInfo.order,
},
{
title: t("jobs.fields.clm_total"),
dataIndex: "clm_total",

View File

@@ -0,0 +1,416 @@
import { DeleteFilled } from "@ant-design/icons";
import { useLazyQuery, useMutation } from "@apollo/client";
import {
Button,
Drawer,
Grid,
PageHeader,
Popconfirm,
Space,
Table,
} 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 } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { QUERY_BILL_BY_PK } from "../../graphql/bills.queries";
import { DELETE_PARTS_ORDER } from "../../graphql/parts-orders.queries";
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 { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort } from "../../utils/sorters";
import DataLabel from "../data-label/data-label.component";
import FeatureWrapperComponent from "../feature-wrapper/feature-wrapper.component";
import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component";
import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component";
import PartsOrderDeleteLine from "../parts-order-delete-line/parts-order-delete-line.component";
import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component";
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
import PrintWrapper from "../print-wrapper/print-wrapper.component";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setBillEnterContext: (context) =>
dispatch(
setModalContext({
context: context,
modal: "billEnter",
})
),
setPartsReceiveContext: (context) =>
dispatch(
setModalContext({
context: context,
modal: "partsReceive",
})
),
});
export function PartsOrderListTableDrawerComponent({
setBillEnterContext,
bodyshop,
jobRO,
job,
billsQuery,
handleOnRowClick,
setPartsReceiveContext,
}) {
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const bpoints = {
xs: "100%",
sm: "100%",
md: "100%",
lg: "75%",
xl: "75%",
xxl: "65%",
};
const drawerPercentage = selectedBreakpoint
? bpoints[selectedBreakpoint[0]]
: "100%";
const responsibilityCenters = bodyshop.md_responsibility_centers;
const Templates = TemplateList("partsorder", { job });
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
});
const [billData, setBillData] = useState(null);
const search = queryString.parse(useLocation().search);
const selectedpartsorder = search.partsorderid;
const [billQuery] = useLazyQuery(QUERY_BILL_BY_PK);
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
const { refetch } = billsQuery;
const selectedPartsOrderRecord = parts_orders.find(
(r) => r.id === selectedpartsorder
);
useEffect(() => {
const fetchData = async () => {
if (selectedPartsOrderRecord?.returnfrombill) {
try {
const { data } = await billQuery({
variables: { billid: selectedPartsOrderRecord.returnfrombill },
});
setBillData(data);
} catch (error) {
console.error("Error fetching bill data:", error);
}
} else setBillData(null);
};
fetchData();
}, [selectedPartsOrderRecord, billQuery]);
const recordActions = (record) => (
<Space direction="horizontal" wrap>
<Button
disabled={
jobRO ||
record.return ||
record.vendor.id === bodyshop.inhousevendorid
}
onClick={() => {
logImEXEvent("parts_order_receive_bill");
setPartsReceiveContext({
actions: { refetch: refetch },
context: {
jobId: job.id,
job: job,
partsorderlines: record.parts_order_lines.map((pol) => ({
joblineid: pol.job_line_id,
id: pol.id,
line_desc: pol.line_desc,
quantity: pol.quantity,
act_price: pol.act_price,
oem_partno: pol.oem_partno,
})),
},
});
}}
>
{t("parts_orders.actions.receive")}
</Button>
<Popconfirm
title={t("parts_orders.labels.confirmdelete")}
disabled={jobRO}
onConfirm={async () => {
//Delete the parts return.!
await deletePartsOrder({
variables: { partsOrderId: record.id },
update(cache) {
cache.modify({
fields: {
parts_orders(existingPartsOrders, { readField }) {
return existingPartsOrders.filter(
(billref) => record.id !== readField("id", billref)
);
},
},
});
},
});
}}
>
<Button disabled={jobRO}>
<DeleteFilled />
</Button>
</Popconfirm>
<FeatureWrapperComponent featureName="bills" noauth={() => null}>
<Button
disabled={
(jobRO ? !record.return : jobRO) ||
record.vendor.id === bodyshop.inhousevendorid
}
onClick={() => {
logImEXEvent("parts_order_receive_bill");
setBillEnterContext({
actions: { refetch: refetch },
context: {
job: job,
bill: {
vendorid: record.vendor.id,
is_credit_memo: record.return,
billlines: record.parts_order_lines.map((pol) => ({
joblineid: pol.job_line_id || "noline",
line_desc: pol.line_desc,
quantity: pol.quantity,
actual_price: pol.act_price,
cost_center: pol.jobline?.part_type
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
? pol.jobline.part_type !== "PAE"
? pol.jobline.part_type
: null
: responsibilityCenters.defaults &&
(responsibilityCenters.defaults.costs[
pol.jobline.part_type
] ||
null)
: null,
})),
},
},
});
}}
>
{t("parts_orders.actions.receivebill")}
</Button>
</FeatureWrapperComponent>
<PrintWrapper
templateObject={{
name: record.return
? Templates.parts_return_slip.key
: Templates.parts_order.key,
variables: { id: record.id },
}}
messageObject={{
subject: record.return
? Templates.parts_return_slip.subject
: Templates.parts_order.subject,
to: record.vendor.email,
}}
id={job.id}
/>
</Space>
);
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const rowExpander = (record) => {
const columns = [
{
title: t("parts_orders.fields.line_desc"),
dataIndex: "line_desc",
key: "line_desc",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
sortOrder:
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
},
{
title: t("parts_orders.fields.quantity"),
dataIndex: "quantity",
key: "quantity",
sorter: (a, b) => a.quantity - b.quantity,
sortOrder:
state.sortedInfo.columnKey === "quantity" && state.sortedInfo.order,
},
{
title: t("parts_orders.fields.act_price"),
dataIndex: "act_price",
key: "act_price",
sorter: (a, b) => a.act_price - b.act_price,
sortOrder:
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.act_price}</CurrencyFormatter>
),
},
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
? [
{
title: t("parts_orders.fields.cost"),
dataIndex: "cost",
key: "cost",
sorter: (a, b) => a.cost - b.cost,
sortOrder:
state.sortedInfo.columnKey === "cost" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.cost}</CurrencyFormatter>
),
},
]
: []),
{
title: t("parts_orders.fields.part_type"),
dataIndex: "part_type",
key: "part_type",
render: (text, record) =>
record.part_type
? t(`joblines.fields.part_types.${record.part_type}`)
: null,
},
{
title: t("parts_orders.fields.oem_partno"),
dataIndex: "oem_partno",
key: "oem_partno",
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
sortOrder:
state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order,
},
{
title: t("parts_orders.fields.line_remarks"),
dataIndex: "line_remarks",
key: "line_remarks",
},
{
title: t("parts_orders.fields.status"),
dataIndex: "status",
key: "status",
},
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
? [
{
title: t("parts_orders.fields.cm_received"),
dataIndex: "cm_received",
key: "cm_received",
render: (text, record) => (
<PartsOrderCmReceived
orderLineId={record.id}
checked={record.cm_received}
partsorderid={selectedPartsOrderRecord.id}
/>
),
},
]
: []),
{
title: t("parts_orders.fields.backordered_on"),
dataIndex: "backordered_on",
key: "backordered_on",
render: (text, record) => <DateFormatter>{text}</DateFormatter>,
},
{
title: t("parts_orders.fields.backordered_eta"),
dataIndex: "backordered_eta",
key: "backordered_eta",
render: (text, record) => (
<PartsOrderBackorderEta
backordered_eta={record.backordered_eta}
disabled={jobRO}
partsOrderStatus={record.status}
partsLineId={record.id}
jobLineId={record.job_line_id}
/>
),
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<Space wrap>
<PartsOrderDeleteLine
disabled={jobRO}
partsOrderStatus={record.status}
partsLineId={record.id}
partsOrderId={selectedpartsorder}
jobLineId={record.job_line_id}
/>
<PartsOrderLineBackorderButton
disabled={jobRO}
partsOrderStatus={record.status}
partsLineId={record.id}
jobLineId={record.job_line_id}
/>
</Space>
),
},
];
return (
<div>
<PageHeader
title={
billData
? `${record.vendor.name} - ${record.order_number} - ${t(
"bills.labels.returnfrombill"
)}: ${billData.bills_by_pk.invoice_number}`
: `${record.vendor.name} - ${record.order_number}`
}
extra={recordActions(record)}
/>
<Table
scroll={{
x: true, //y: "50rem"
}}
columns={columns}
rowKey="id"
dataSource={record.parts_order_lines}
onChange={handleTableChange}
/>
<DataLabel label={t("parts_orders.fields.comments")}>
<div style={{ whiteSpace: "pre" }}>{record.comments}</div>
</DataLabel>
</div>
);
};
return (
<div>
<PartsReceiveModalContainer />
<Drawer
placement="right"
onClose={() => handleOnRowClick(null)}
open={selectedpartsorder}
closable
width={drawerPercentage}
>
{selectedPartsOrderRecord && rowExpander(selectedPartsOrderRecord)}
</Drawer>
</div>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(PartsOrderListTableDrawerComponent);

View File

@@ -1,39 +1,21 @@
import { DeleteFilled, EyeFilled, SyncOutlined } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import {
Button,
Card,
Checkbox,
Drawer,
Grid,
Input,
PageHeader,
Popconfirm,
Space,
Table,
} from "antd";
import queryString from "query-string";
import { Button, Card, Checkbox, Input, Popconfirm, Space, Table } from "antd";
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 { logImEXEvent } from "../../firebase/firebase.utils";
import { DELETE_PARTS_ORDER } from "../../graphql/parts-orders.queries";
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 } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants";
import DataLabel from "../data-label/data-label.component";
import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component";
import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component";
import PartsOrderDeleteLine from "../parts-order-delete-line/parts-order-delete-line.component";
import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component";
import { alphaSort } from "../../utils/sorters";
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
import PrintWrapper from "../print-wrapper/print-wrapper.component";
import PartsOrderDrawer from "./parts-order-list-table-drawer.component";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
@@ -56,21 +38,6 @@ export function PartsOrderListTableComponent({
handleOnRowClick,
setPartsReceiveContext,
}) {
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const bpoints = {
xs: "100%",
sm: "100%",
md: "100%",
lg: "75%",
xl: "75%",
xxl: "65%",
};
const drawerPercentage = selectedBreakpoint
? bpoints[selectedBreakpoint[0]]
: "100%";
const responsibilityCenters = bodyshop.md_responsibility_centers;
const Templates = TemplateList("partsorder", { job });
@@ -78,10 +45,8 @@ export function PartsOrderListTableComponent({
const [state, setState] = useState({
sortedInfo: {},
});
const search = queryString.parse(useLocation().search);
const selectedpartsorder = search.partsorderid;
const [searchText, setSearchText] = useState("");
const [searchText, setSearchText] = useState("");
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
@@ -90,7 +55,11 @@ export function PartsOrderListTableComponent({
const recordActions = (record, showView = false) => (
<Space wrap>
{showView && (
<Button onClick={() => handleOnRowClick(record)}>
<Button
onClick={() => {
handleOnRowClick(record);
}}
>
<EyeFilled />
</Button>
)}
@@ -166,7 +135,7 @@ export function PartsOrderListTableComponent({
is_credit_memo: record.return,
billlines: record.parts_order_lines.map((pol) => {
return {
joblineid: pol.job_line_id,
joblineid: pol.job_line_id || "noline",
line_desc: pol.line_desc,
quantity: pol.quantity,
@@ -277,164 +246,6 @@ export function PartsOrderListTableComponent({
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const selectedPartsOrderRecord = parts_orders.find(
(r) => r.id === selectedpartsorder
);
const rowExpander = (record) => {
const columns = [
{
title: t("parts_orders.fields.line_desc"),
dataIndex: "line_desc",
key: "line_desc",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
sortOrder:
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
},
{
title: t("parts_orders.fields.quantity"),
dataIndex: "quantity",
key: "quantity",
sorter: (a, b) => a.quantity - b.quantity,
sortOrder:
state.sortedInfo.columnKey === "quantity" && state.sortedInfo.order,
},
{
title: t("parts_orders.fields.act_price"),
dataIndex: "act_price",
key: "act_price",
sorter: (a, b) => a.act_price - b.act_price,
sortOrder:
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.act_price}</CurrencyFormatter>
),
},
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
? [
{
title: t("parts_orders.fields.cost"),
dataIndex: "cost",
key: "cost",
sorter: (a, b) => a.cost - b.cost,
sortOrder:
state.sortedInfo.columnKey === "cost" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.cost}</CurrencyFormatter>
),
},
]
: []),
{
title: t("parts_orders.fields.part_type"),
dataIndex: "part_type",
key: "part_type",
render: (text, record) =>
record.part_type
? t(`joblines.fields.part_types.${record.part_type}`)
: null,
},
{
title: t("parts_orders.fields.oem_partno"),
dataIndex: "oem_partno",
key: "oem_partno",
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
sortOrder:
state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order,
},
{
title: t("parts_orders.fields.line_remarks"),
dataIndex: "line_remarks",
key: "line_remarks",
},
{
title: t("parts_orders.fields.status"),
dataIndex: "status",
key: "status",
},
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
? [
{
title: t("parts_orders.fields.cm_received"),
dataIndex: "cm_received",
key: "cm_received",
render: (text, record) => (
<PartsOrderCmReceived
orderLineId={record.id}
checked={record.cm_received}
partsorderid={selectedPartsOrderRecord.id}
/>
),
},
]
: []),
{
title: t("parts_orders.fields.backordered_on"),
dataIndex: "backordered_on",
key: "backordered_on",
render: (text, record) => <DateFormatter>{text}</DateFormatter>,
},
{
title: t("parts_orders.fields.backordered_eta"),
dataIndex: "backordered_eta",
key: "backordered_eta",
render: (text, record) => (
<PartsOrderBackorderEta
backordered_eta={record.backordered_eta}
disabled={jobRO}
partsOrderStatus={record.status}
partsLineId={record.id}
jobLineId={record.job_line_id}
/>
),
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<Space wrap>
<PartsOrderDeleteLine
disabled={jobRO}
partsOrderStatus={record.status}
partsLineId={record.id}
partsOrderId={selectedpartsorder}
jobLineId={record.job_line_id}
/>
<PartsOrderLineBackorderButton
disabled={jobRO}
partsOrderStatus={record.status}
partsLineId={record.id}
jobLineId={record.job_line_id}
/>
</Space>
),
},
];
return (
<div>
<PageHeader
title={record && `${record.vendor.name} - ${record.order_number}`}
extra={recordActions(record)}
/>
<Table
scroll={{
x: true, //y: "50rem"
}}
columns={columns}
rowKey="id"
dataSource={record.parts_order_lines}
/>
<DataLabel label={t("parts_orders.fields.comments")}>
<div style={{ whiteSpace: "pre" }}>{record.comments}</div>
</DataLabel>
</div>
);
};
const filteredPartsOrders = parts_orders
? searchText === ""
? parts_orders
@@ -470,15 +281,12 @@ export function PartsOrderListTableComponent({
}
>
<PartsReceiveModalContainer />
<Drawer
placement="right"
onClose={() => handleOnRowClick(null)}
visible={selectedpartsorder}
closable
width={drawerPercentage}
>
{selectedPartsOrderRecord && rowExpander(selectedPartsOrderRecord)}
</Drawer>
<PartsOrderDrawer
job={job}
billsQuery={billsQuery}
handleOnRowClick={handleOnRowClick}
setPartsReceiveContext={setPartsReceiveContext}
/>
<Table
loading={billsQuery.loading}
scroll={{

View File

@@ -1,17 +1,17 @@
import { DeleteFilled, WarningFilled, DownOutlined } from "@ant-design/icons";
import { DeleteFilled, DownOutlined, WarningFilled } from "@ant-design/icons";
import { useTreatments } from "@splitsoftware/splitio-react";
import {
Checkbox,
Divider,
Dropdown,
Form,
Input,
InputNumber,
Menu,
Radio,
Select,
Space,
Tag,
Select,
Menu,
Dropdown,
Checkbox,
} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -255,7 +255,7 @@ export function PartsOrderModalComponent({
},
]}
>
<InputNumber />
<InputNumber min={1} />
</Form.Item>
<Form.Item
label={t("parts_orders.fields.act_price")}

View File

@@ -1,12 +1,16 @@
import { useMutation, useQuery, useApolloClient } from "@apollo/client";
import { useApolloClient, useMutation, useQuery } from "@apollo/client";
import { useTreatments } from "@splitsoftware/splitio-react";
import { Form, Modal, notification } from "antd";
import axios from "axios";
import _ from "lodash";
import moment from "moment";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent, auth } from "../../firebase/firebase.utils";
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
import { UPDATE_JOB_LINE_STATUS } from "../../graphql/jobs-lines.queries";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import {
INSERT_NEW_PARTS_ORDERS,
QUERY_PARTS_ORDER_OEC,
@@ -29,10 +33,6 @@ import { TemplateList } from "../../utils/TemplateConstants";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import PartsOrderModalComponent from "./parts-order-modal.component";
import axios from "axios";
import { useTreatments } from "@splitsoftware/splitio-react";
import _ from "lodash";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
@@ -45,8 +45,8 @@ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("partsOrder")),
setBillEnterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "billEnter" })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export function PartsOrderModalContainer({
@@ -142,6 +142,7 @@ export function PartsOrderModalContainer({
: AuditTrailMapping.jobspartsorder(
insertResult.data.insert_parts_orders.returning[0].order_number
),
type: isReturn ? "jobspartsreturn" : "jobspartsorder",
});
const jobLinesResult = await updateJobLines({

View File

@@ -0,0 +1,77 @@
import { useQuery } from "@apollo/client";
import { Card, Divider, Drawer, Grid } from "antd";
import queryString from "query-string";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link, useHistory, useLocation } from "react-router-dom";
import { QUERY_PARTS_QUEUE_CARD_DETAILS } from "../../graphql/jobs.queries";
import AlertComponent from "../alert/alert.component";
import JobsDetailHeader from "../jobs-detail-header/jobs-detail-header.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import PartsQueueJobLinesComponent from "./parts-queue-job-lines.component";
export default function PartsQueueDetailCard() {
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const bpoints = {
xs: "100%",
sm: "100%",
md: "100%",
lg: "75%",
xl: "75%",
xxl: "60%",
};
const drawerPercentage = selectedBreakpoint
? bpoints[selectedBreakpoint[0]]
: "100%";
const searchParams = queryString.parse(useLocation().search);
const { selected } = searchParams;
const history = useHistory();
const { loading, error, data } = useQuery(QUERY_PARTS_QUEUE_CARD_DETAILS, {
variables: { id: selected },
skip: !selected,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const { t } = useTranslation();
const handleDrawerClose = () => {
delete searchParams.selected;
history.push({
search: queryString.stringify({
...searchParams,
}),
});
};
return (
<Drawer
visible={!!selected}
destroyOnClose
width={drawerPercentage}
placement="right"
onClose={handleDrawerClose}
>
{loading ? <LoadingSpinner /> : null}
{error ? <AlertComponent message={error.message} type="error" /> : null}
{data ? (
<Card
title={
<Link to={`/manage/jobs/${data.jobs_by_pk.id}`}>
{data.jobs_by_pk.ro_number || t("general.labels.na")}
</Link>
}
>
<JobsDetailHeader job={data ? data.jobs_by_pk : null} />
<Divider type="horizontal" />
<PartsQueueJobLinesComponent
jobLines={data.jobs_by_pk ? data.jobs_by_pk.joblines : null}
/>
</Card>
) : null}
</Drawer>
);
}

View File

@@ -0,0 +1,208 @@
import { Card, 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 { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { onlyUnique } from "../../utils/arrayHelper";
import { alphaSort } from "../../utils/sorters";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
jobRO: selectJobReadOnly,
});
const mapDispatchToProps = (dispatch) => ({});
export function PartsQueueJobLinesComponent({ jobRO, loading, jobLines }) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: {},
});
const { t } = useTranslation();
const columns = [
{
title: "#",
dataIndex: "line_no",
key: "line_no",
sorter: (a, b) => a.line_no - b.line_no,
sortOrder:
state.sortedInfo.columnKey === "line_no" && state.sortedInfo.order,
},
{
title: t("joblines.fields.line_desc"),
dataIndex: "line_desc",
key: "line_desc",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
onCell: (record) => ({
className: record.manual_line && "job-line-manual",
style: {
...(record.critical ? { boxShadow: " -.5em 0 0 #FFC107" } : {}),
},
}),
sortOrder:
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
ellipsis: true,
},
{
title: t("joblines.fields.oem_partno"),
dataIndex: "oem_partno",
key: "oem_partno",
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
sortOrder:
state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order,
ellipsis: true,
render: (text, record) =>
`${record.oem_partno || ""} ${
record.alt_partno ? `(${record.alt_partno})` : ""
}`.trim(),
},
{
title: t("joblines.fields.part_type"),
dataIndex: "part_type",
key: "part_type",
filteredValue: state.filteredInfo.part_type || null,
sorter: (a, b) => alphaSort(a.part_type, b.part_type),
sortOrder:
state.sortedInfo.columnKey === "part_type" && state.sortedInfo.order,
filters: [
{
text: t("jobs.labels.partsfilter"),
value: [
"PAN",
"PAC",
"PAR",
"PAL",
"PAA",
"PAM",
"PAP",
"PAS",
"PASL",
"PAG",
],
},
{
text: t("joblines.fields.part_types.PAN"),
value: ["PAN"],
},
{
text: t("joblines.fields.part_types.PAP"),
value: ["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.PAG"),
value: ["PAG"],
},
{
text: t("joblines.fields.part_types.PAS"),
value: ["PAS"],
},
{
text: t("joblines.fields.part_types.PASL"),
value: ["PASL"],
},
{
text: t("joblines.fields.part_types.PAC"),
value: ["PAC"],
},
{
text: t("joblines.fields.part_types.PAR"),
value: ["PAR"],
},
{
text: t("joblines.fields.part_types.PAM"),
value: ["PAM"],
},
],
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.part_qty"),
dataIndex: "part_qty",
key: "part_qty",
},
{
title: t("joblines.fields.act_price"),
dataIndex: "act_price",
key: "act_price",
sorter: (a, b) => a.act_price - b.act_price,
sortOrder:
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
ellipsis: true,
render: (text, record) => (
<CurrencyFormatter>
{record.db_ref === "900510" || record.db_ref === "900511"
? record.prt_dsmk_m
: record.act_price}
</CurrencyFormatter>
),
},
{
title: t("joblines.fields.location"),
dataIndex: "location",
key: "location",
},
{
title: t("joblines.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filteredValue: state.filteredInfo.status || null,
filters:
(jobLines &&
jobLines
.map((l) => l.status)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
value: [s],
};
})) ||
[],
onFilter: (value, record) => value.includes(record.status),
},
];
const handleTableChange = (pagination, filters, sorter) => {
setState((state) => ({
...state,
filteredInfo: filters,
sortedInfo: sorter,
}));
};
return (
<Card title={t("jobs.labels.parts_lines")}>
<Table
columns={columns}
rowKey="id"
loading={loading}
pagination={false}
dataSource={jobLines}
onChange={handleTableChange}
/>
</Card>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(PartsQueueJobLinesComponent);

View File

@@ -8,30 +8,28 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import AlertComponent from "../../components/alert/alert.component";
import JobPartsQueueCount from "../../components/job-parts-queue-count/job-parts-queue-count.component";
import JobRemoveFromPartsQueue from "../../components/job-remove-from-parst-queue/job-remove-from-parts-queue.component";
import OwnerNameDisplay from "../../components/owner-name-display/owner-name-display.component";
import ProductionListColumnComment from "../../components/production-list-columns/production-list-columns.comment.component";
import { QUERY_PARTS_QUEUE } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateTimeFormatter, TimeAgoFormatter } from "../../utils/DateFormatter";
import { onlyUnique } from "../../utils/arrayHelper";
import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
import {pageLimit} from "../../utils/config";
import AlertComponent from "../alert/alert.component";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
import JobRemoveFromPartsQueue from "../job-remove-from-parst-queue/job-remove-from-parts-queue.component";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
export function PartsQueuePageComponent({ bodyshop }) {
export function PartsQueueListComponent({ bodyshop }) {
const searchParams = queryString.parse(useLocation().search);
const {
//page,
sortcolumn,
sortorder,
statusFilters,
} = searchParams;
const { selected, sortcolumn, sortorder, statusFilters } = searchParams;
const history = useHistory();
const [filter, setFilter] = useLocalStorage("filter_parts_queue", null);
@@ -39,19 +37,8 @@ export function PartsQueuePageComponent({ bodyshop }) {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
variables: {
// offset: page ? (page - 1) * 25 : 0,
// limit: 25,
statuses: (statusFilters && JSON.parse(statusFilters)) ||
bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"],
order: [
{
[sortcolumn || "ro_number"]: sortorder
? sortorder === "descend"
? "desc"
: "asc"
: "desc",
},
],
},
});
@@ -107,6 +94,17 @@ export function PartsQueuePageComponent({ bodyshop }) {
history.push({ search: queryString.stringify(searchParams) });
};
const handleOnRowClick = (record) => {
if (record?.id) {
history.replace({
search: queryString.stringify({
...searchParams,
selected: record.id,
}),
});
}
};
const columns = [
{
title: t("jobs.fields.ro_number"),
@@ -125,7 +123,8 @@ export function PartsQueuePageComponent({ bodyshop }) {
title: t("jobs.fields.owner"),
dataIndex: "ownr_ln",
key: "ownr_ln",
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder: sortcolumn === "ownr_ln" && sortorder,
render: (text, record) => {
return record.ownerid ? (
@@ -139,6 +138,56 @@ export function PartsQueuePageComponent({ bodyshop }) {
);
},
},
{
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: sortcolumn === "vehicle" && sortorder,
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.ins_co_nm_short"),
dataIndex: "ins_co_nm",
key: "ins_co_nm",
ellipsis: true,
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
sortOrder: sortcolumn === "ins_co_nm" && sortorder,
filteredValue: filter?.ins_co_nm || null,
filters:
(jobs &&
jobs
.map((j) => j.ins_co_nm)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Ins. Co.*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.ins_co_nm),
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
@@ -170,23 +219,16 @@ export function PartsQueuePageComponent({ bodyshop }) {
),
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
title: t("jobs.fields.scheduled_completion"),
dataIndex: "scheduled_completion",
key: "scheduled_completion",
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>
);
},
sorter: (a, b) =>
dateSort(a.scheduled_completion, b.scheduled_completion),
sortOrder: sortcolumn === "scheduled_completion" && sortorder,
render: (text, record) => (
<DateTimeFormatter>{record.scheduled_completion}</DateTimeFormatter>
),
},
// {
// title: t("vehicles.fields.plate_no"),
@@ -198,14 +240,6 @@ export function PartsQueuePageComponent({ bodyshop }) {
// return record.plate_no ? record.plate_no : "";
// },
// },
{
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: sortcolumn === "clm_no" && sortorder,
},
// {
// title: t("jobs.fields.clm_total"),
// dataIndex: "clm_total",
@@ -307,9 +341,23 @@ export function PartsQueuePageComponent({ bodyshop }) {
style={{ height: "100%" }}
scroll={{ x: true }}
onChange={handleTableChange}
rowSelection={{
onSelect: (record) => {
handleOnRowClick(record);
},
selectedRowKeys: [selected],
type: "radio",
}}
onRow={(record, rowIndex) => {
return {
onClick: (event) => {
handleOnRowClick(record);
},
};
}}
/>
</Card>
);
}
export default connect(mapStateToProps, null)(PartsQueuePageComponent);
export default connect(mapStateToProps, null)(PartsQueueListComponent);

View File

@@ -59,8 +59,8 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
await insertPayment({
variables: {
paymentInput: {
amount: -refund_response.data.amount,
transactionid: payment_response.response.receiptelements.transid,
amount: -refund_response?.data?.amount,
transactionid: payment_response?.response?.receiptelements?.transid,
payer: record.payer,
type: "Refund",
jobid: payment_response.jobid,
@@ -139,8 +139,8 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
contentStyle={{ fontWeight: "600" }}
column={4}
>
<Descriptions.Item label={t("job_payments.titles.payer")}>
{record.payer}
<Descriptions.Item label={t("job_payments.titles.hint")}>
{payment_response?.response?.methodhint}
</Descriptions.Item>
<Descriptions.Item label={t("job_payments.titles.payername")}>
{payment_response?.response?.nameOnCard ?? ""}
@@ -155,7 +155,7 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
{record.transactionid}
</Descriptions.Item>
<Descriptions.Item label={t("job_payments.titles.paymentid")}>
{payment_response?.response?.paymentreferenceid ?? ""}
{payment_response?.ext_paymentid ?? ""}
</Descriptions.Item>
<Descriptions.Item label={t("job_payments.titles.paymenttype")}>
{record.type}

View File

@@ -1,16 +1,18 @@
import React from "react";
import { Button, notification } from "antd";
import { useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { setModalContext } from "../../redux/modals/modals.actions";
import { connect } from "react-redux";
import { UPDATE_PAYMENT } from "../../graphql/payments.queries";
import { selectCurrentUser } from "../../redux/user/user.selectors";
import { createStructuredSelector } from "reselect";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { UPDATE_PAYMENT } from "../../graphql/payments.queries";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectPayment } from "../../redux/modals/modals.selectors";
import { selectCurrentUser } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
paymentModal: selectPayment,
});
const mapDispatchToProps = (dispatch) => ({
@@ -24,6 +26,7 @@ const PaymentMarkForExportButton = ({
refetch,
setPaymentContext,
currentUser,
paymentModal,
}) => {
const { t } = useTranslation();
const [insertExportLog, { loading: exportLogLoading }] =
@@ -46,7 +49,6 @@ const PaymentMarkForExportButton = ({
],
},
});
const paymentUpdateResponse = await updatePayment({
variables: {
paymentId: payment.id,
@@ -55,25 +57,33 @@ const PaymentMarkForExportButton = ({
},
},
});
if (!!!paymentUpdateResponse.errors) {
notification.open({
type: "success",
key: "paymentsuccessmarkforexport",
message: t("payments.successes.markexported"),
});
if (refetch) refetch();
setPaymentContext({
actions: {
refetch,
},
context: {
...paymentModal.context,
...payment,
exportedat: today,
},
});
if (refetch) {
if (paymentModal.context.refetchRequiresContext) {
refetch(
paymentUpdateResponse &&
paymentUpdateResponse.data.update_payments.returning[0]
);
} else {
refetch();
}
}
} else {
notification["error"]({
message: t("payments.errors.exporting", {

View File

@@ -1,5 +1,4 @@
import { useMutation } from "@apollo/client";
import { Button, Form, Modal, notification, Space } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -19,8 +18,8 @@ import {
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import PaymentForm from "../payment-form/payment-form.component";
import PaymentReexportButton from "../payment-reexport-button/payment-reexport-button.component";
import PaymentMarkForExportButton from "../payment-mark-export-button/payment-mark-export-button-component";
import PaymentReexportButton from "../payment-reexport-button/payment-reexport-button.component";
const mapStateToProps = createStructuredSelector({
paymentModal: selectPayment,
@@ -97,16 +96,21 @@ function PaymentModalContainer({
});
if (!!!updatedPayment.errors) {
notification["success"]({ message: t("payments.successes.payment") });
notification["success"]({ message: t("payments.successes.paymentupdate") });
} else {
notification["error"]({ message: t("payments.errors.payment") });
notification["error"]({ message: t("payments.errors.paymentupdate") });
}
}
if (actions.refetch)
actions.refetch(
updatedPayment && updatedPayment.data.update_payments.returning[0]
);
if (actions.refetch) {
if (context.refetchRequiresContext) {
actions.refetch(
updatedPayment && updatedPayment.data.update_payments.returning[0]
);
} else {
actions.refetch();
}
}
if (enterAgain) {
const prev = form.getFieldsValue(["date"]);
@@ -159,7 +163,7 @@ function PaymentModalContainer({
}}
afterClose={() => form.resetFields()}
footer={
<span>
<Space>
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
<Button loading={loading} onClick={() => form.submit()}>
{t("general.actions.save")}
@@ -175,7 +179,7 @@ function PaymentModalContainer({
{t("general.actions.saveandnew")}
</Button>
)}
</span>
</Space>
}
>
{!context || (context && !context.id) ? null : (
@@ -194,7 +198,6 @@ function PaymentModalContainer({
autoComplete={"off"}
form={form}
layout="vertical"
initialValues={context || {}}
disabled={context?.exportedat}
>
<PaymentForm form={form} />

View File

@@ -1,17 +1,27 @@
import React from "react";
import { Button, notification } from "antd";
import { useTranslation } from "react-i18next";
import { UPDATE_PAYMENT } from "../../graphql/payments.queries";
import { useMutation } from "@apollo/client";
import { setModalContext } from "../../redux/modals/modals.actions";
import { Button, notification } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UPDATE_PAYMENT } from "../../graphql/payments.queries";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectPayment } from "../../redux/modals/modals.selectors";
const mapStateToProps = createStructuredSelector({
paymentModal: selectPayment,
});
const mapDispatchToProps = (dispatch) => ({
setPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "payment" })),
});
const PaymentReexportButton = ({ payment, refetch, setPaymentContext }) => {
const PaymentReexportButton = ({
paymentModal,
payment,
refetch,
setPaymentContext,
}) => {
const { t } = useTranslation();
const [updatePayment, { loading }] = useMutation(UPDATE_PAYMENT);
@@ -24,25 +34,32 @@ const PaymentReexportButton = ({ payment, refetch, setPaymentContext }) => {
},
},
});
if (!!!paymentUpdateResponse.errors) {
notification.open({
type: "success",
key: "paymentsuccessexport",
message: t("payments.successes.markreexported"),
});
if (refetch) refetch();
setPaymentContext({
actions: {
refetch,
},
context: {
...paymentModal.context,
...payment,
exportedat: null,
},
});
if (refetch) {
if (paymentModal.context.refetchRequiresContext) {
refetch(
paymentUpdateResponse &&
paymentUpdateResponse.data.update_payments.returning[0]
);
} else {
refetch();
}
}
} else {
notification["error"]({
message: t("payments.errors.exporting", {
@@ -63,4 +80,7 @@ const PaymentReexportButton = ({ payment, refetch, setPaymentContext }) => {
);
};
export default connect(null, mapDispatchToProps)(PaymentReexportButton);
export default connect(
mapStateToProps,
mapDispatchToProps
)(PaymentReexportButton);

View File

@@ -14,11 +14,11 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants";
import { pageLimit } from "../../utils/config";
import { alphaSort } from "../../utils/sorters";
import CaBcEtfTableModalContainer from "../ca-bc-etf-table-modal/ca-bc-etf-table-modal.container";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
import {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -184,7 +184,10 @@ export function PaymentsListPaginated({
}
: refetch,
},
context: apolloResults ? apolloResults : record,
context: {
...(apolloResults ? apolloResults : record),
refetchRequiresContext: true,
},
});
}}
>

View File

@@ -1,6 +1,12 @@
import { Button, Form, Input, PageHeader, Space } from "antd";
import React 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 FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import PhoneFormItem, {
@@ -8,12 +14,6 @@ import PhoneFormItem, {
} from "../form-items-formatted/phone-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
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";
const mapStateToProps = createStructuredSelector({
authLevel: selectAuthLevel,
bodyshop: selectBodyshop,
@@ -46,13 +46,19 @@ export function PhonebookFormComponent({
return (
<div>
<PageHeader
title={`${form.getFieldValue("firstname") || ""} ${
form.getFieldValue("lastname") || ""
}${
form.getFieldValue("company")
? ` - ${form.getFieldValue("company")}`
: ""
}`}
title={
<Form.Item shouldUpdate>
{() =>
`${form.getFieldValue("firstname") || ""} ${
form.getFieldValue("lastname") || ""
}${
form.getFieldValue("company")
? ` - ${form.getFieldValue("company")}`
: ""
}`
}
</Form.Item>
}
extra={
<Space>
<Button

View File

@@ -6,6 +6,7 @@ import {
notification,
Popover,
Radio,
Space,
} from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -115,10 +116,16 @@ export function PrintCenterJobsLabels({ bodyshop, jobId }) {
>
<InputNumber min={1} precision={0} max={99} />
</Form.Item>
<Button type="primary" loading={loading} onClick={handleOk}>
{t("general.actions.print")}
</Button>
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<Space>
<Button type="primary" loading={loading} onClick={handleOk}>
{t("general.actions.print")}
</Button>
<Button onClick={handleCancel}>
{t("general.actions.cancel")}
</Button>
</Space>
</div>
</Form>
</Card>
);

View File

@@ -29,8 +29,8 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export function ProductionBoardKanbanComponent({
@@ -115,7 +115,7 @@ export function ProductionBoardKanbanComponent({
// console.log("==> New Card is somewhere in the middle");
movedCardNewKanbanParent = newChildCard.kanbanparent;
} else {
throw new Error("==> !!!!!!Couldn't find a parent.!!!! <==");
console.log("==> !!!!!!Couldn't find a parent.!!!! <==");
}
const newChildCardNewParent = newChildCard ? card.id : null;
const update = await client.mutate({
@@ -133,6 +133,7 @@ export function ProductionBoardKanbanComponent({
insertAuditTrail({
jobid: card.id,
operation: AuditTrailMapping.jobstatuschange(destination.toColumnId),
type: "jobstatuschange",
});
if (update.errors) {
@@ -153,7 +154,7 @@ export function ProductionBoardKanbanComponent({
0
)
.toFixed(1);
const totalLAB = data
const totalLAB = data
.reduce(
(acc, val) => acc + (val.labhrs?.aggregate?.sum?.mod_lb_hrs || 0),
0

View File

@@ -18,8 +18,8 @@ const sortByParentId = (arr) => {
//console.log("sortByParentId -> byParentsIdsList", byParentsIdsList);
while (byParentsIdsList[parentId]) {
sortedList.push(byParentsIdsList[parentId][0]);
parentId = byParentsIdsList[parentId][0].id;
sortedList.push(...byParentsIdsList[parentId]); //Spread in the whole list in case several items have the same parents.
parentId = byParentsIdsList[parentId][byParentsIdsList[parentId].length -1].id; //Grab the ID from the last one.
}
if (byParentsIdsList["null"])

View File

@@ -1,7 +1,7 @@
import React from "react";
import { Button, Dropdown, Menu } from "antd";
import dataSource from "./production-list-columns.data";
import React from "react";
import { useTranslation } from "react-i18next";
import dataSource from "./production-list-columns.data";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -24,6 +24,7 @@ export function ProductionColumnsComponent({
columnState,
technician,
bodyshop,
data,
tableState,
}) {
const [columns, setColumns] = columnState;
@@ -36,6 +37,7 @@ export function ProductionColumnsComponent({
bodyshop,
technician,
state: tableState,
data: data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
}).filter((i) => i.key === e.key),
]);
@@ -46,6 +48,7 @@ export function ProductionColumnsComponent({
technician,
state: tableState,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
data: data,
});
const menu = (
<Menu

View File

@@ -13,8 +13,8 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export function ProductionListColumnAlert({ record, insertAuditTrail }) {
@@ -46,6 +46,7 @@ export function ProductionListColumnAlert({ record, insertAuditTrail }) {
? !record.production_vars.alert
: true
),
type: "alertToggle",
}).then(() => {
if (record.refetch) record.refetch();
});

View File

@@ -1,15 +1,15 @@
import { PauseCircleOutlined, BranchesOutlined } from "@ant-design/icons";
import { Space, Tooltip } from "antd";
import { BranchesOutlined, PauseCircleOutlined } from "@ant-design/icons";
import { Checkbox, Space, Tooltip } from "antd";
import i18n from "i18next";
import moment from "moment";
import { Link } from "react-router-dom";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { TimeFormatter } from "../../utils/DateFormatter";
import PhoneFormatter from "../../utils/PhoneFormatter";
import { onlyUnique } from "../../utils/arrayHelper";
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
import JobAltTransportChange from "../job-at-change/job-at-change.component";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
import ProductionListColumnAlert from "./production-list-columns.alert.component";
import ProductionListColumnBodyPriority from "./production-list-columns.bodypriority.component";
@@ -25,18 +25,14 @@ import ProductionListColumnCategory from "./production-list-columns.status.categ
import ProductionListColumnStatus from "./production-list-columns.status.component";
import ProductionlistColumnTouchTime from "./prodution-list-columns.touchtime.component";
const r = ({ technician, state, activeStatuses, bodyshop }) => {
const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
return [
{
title: i18n.t("jobs.actions.viewdetail"),
dataIndex: "viewdetail",
key: "viewdetail",
ellipsis: true,
render: (text, record) => (
<Link to={{ search: `?selected=${record.id}` }}>
{i18n.t("general.labels.view")}
</Link>
),
render: (text, record) => <Link to={{ search: `?selected=${record.id}` }}>{i18n.t("general.labels.view")}</Link>
},
{
title: i18n.t("jobs.fields.ro_number"),
@@ -44,23 +40,18 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
key: "ro_number",
ellipsis: true,
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) =>
technician ? (
<Link to={`/tech/joblookup?selected=${record.id}`}>
{record.ro_number}
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
{record.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
</Link>
) : (
<Link to={`/manage/jobs/${record.id}`}>
<Space>
{record.ro_number}
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
{record.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
{record.iouparent && (
<Tooltip title={i18n.t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} />
@@ -68,17 +59,23 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
)}
</Space>
</Link>
),
)
},
{
title: i18n.t("jobs.fields.owner"),
dataIndex: "ownr",
key: "ownr",
ellipsis: true,
render: (text, record) => <OwnerNameDisplay ownerObject={record} />,
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sortOrder:
state.sortedInfo.columnKey === "ownr" && state.sortedInfo.order,
render: (text, record) =>
technician ? (
<OwnerNameDisplay ownerObject={record} />
) : (
<Link to={`/manage/owners/${record.ownerid}`}>
<OwnerNameDisplay ownerObject={record} />
</Link>
),
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder: state.sortedInfo.columnKey === "ownr" && state.sortedInfo.order
},
{
title: i18n.t("jobs.fields.vehicle"),
@@ -87,18 +84,22 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
ellipsis: true,
sorter: (a, b) =>
alphaSort(
a.v_make_desc + a.v_model_desc,
b.v_make_desc + b.v_model_desc
`${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) => (
<Link to={`/manage/vehicles/${record.vehicleid}`}>{`${
record.v_model_yr || ""
} ${record.v_make_desc || ""} ${record.v_model_desc || ""} ${
record.v_color || ""
} ${record.plate_no || ""}`}</Link>
),
sortOrder: state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) =>
technician ? (
<>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
} ${record.v_color || ""} ${record.plate_no || ""}`}</>
) : (
<Link to={`/manage/vehicles/${record.vehicleid}`}>{`${
record.v_model_yr || ""
} ${record.v_make_desc || ""} ${record.v_model_desc || ""} ${
record.v_color || ""
} ${record.plate_no || ""}`}</Link>
)
},
{
title: i18n.t("jobs.fields.actual_in"),
@@ -106,11 +107,8 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
key: "actual_in",
ellipsis: true,
sorter: (a, b) => dateSort(a.actual_in, b.actual_in),
sortOrder:
state.sortedInfo.columnKey === "actual_in" && state.sortedInfo.order,
render: (text, record) => (
<ProductionListDate record={record} field="actual_in" time />
),
sortOrder: state.sortedInfo.columnKey === "actual_in" && state.sortedInfo.order,
render: (text, record) => <ProductionListDate record={record} field="actual_in" time />
},
{
title: i18n.t("jobs.fields.actual_in") + " (HH:MM)",
@@ -118,28 +116,16 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
key: "actual_in_time",
ellipsis: true,
render: (text, record) => (
<TimeFormatter>{record.actual_in}</TimeFormatter>
),
render: (text, record) => <TimeFormatter>{record.actual_in}</TimeFormatter>
},
{
title: i18n.t("jobs.fields.scheduled_completion"),
dataIndex: "scheduled_completion",
key: "scheduled_completion",
ellipsis: true,
sorter: (a, b) =>
dateSort(a.scheduled_completion, b.scheduled_completion),
sortOrder:
state.sortedInfo.columnKey === "scheduled_completion" &&
state.sortedInfo.order,
render: (text, record) => (
<ProductionListDate
record={record}
field="scheduled_completion"
pastIndicator
time
/>
),
sorter: (a, b) => dateSort(a.scheduled_completion, b.scheduled_completion),
sortOrder: state.sortedInfo.columnKey === "scheduled_completion" && state.sortedInfo.order,
render: (text, record) => <ProductionListDate record={record} field="scheduled_completion" pastIndicator time />
},
{
title: i18n.t("jobs.fields.scheduled_completion") + " (HH:MM)",
@@ -147,9 +133,7 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
key: "scheduled_completion_time",
ellipsis: true,
render: (text, record) => (
<TimeFormatter>{record.scheduled_completion}</TimeFormatter>
),
render: (text, record) => <TimeFormatter>{record.scheduled_completion}</TimeFormatter>
},
{
title: i18n.t("jobs.fields.date_last_contacted"),
@@ -157,10 +141,8 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
key: "date_last_contacted",
ellipsis: true,
sorter: (a, b) => dateSort(a.date_last_contacted, b.date_last_contacted),
sortOrder:
state.sortedInfo.columnKey === "date_last_contacted" &&
state.sortedInfo.order,
render: (text, record) => <ProductionListLastContacted record={record} />,
sortOrder: state.sortedInfo.columnKey === "date_last_contacted" && state.sortedInfo.order,
render: (text, record) => <ProductionListLastContacted record={record} />
},
{
title: i18n.t("jobs.fields.date_next_contact"),
@@ -168,22 +150,8 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
key: "date_next_contact",
ellipsis: true,
sorter: (a, b) => dateSort(a.date_next_contact, b.date_next_contact),
sortOrder:
state.sortedInfo.columnKey === "date_next_contact" &&
state.sortedInfo.order,
render: (text, record) => (
<span
style={{
color:
record.date_next_contact &&
moment(record.date_next_contact).isBefore(moment())
? "red"
: "",
}}
>
<ProductionListDate record={record} field="date_next_contact" time />
</span>
),
sortOrder: state.sortedInfo.columnKey === "date_next_contact" && state.sortedInfo.order,
render: (text, record) => <ProductionListDate record={record} field="date_next_contact" pastIndicator time />
},
{
title: i18n.t("jobs.fields.scheduled_delivery"),
@@ -191,17 +159,8 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
key: "scheduled_delivery",
ellipsis: true,
sorter: (a, b) => dateSort(a.scheduled_delivery, b.scheduled_delivery),
sortOrder:
state.sortedInfo.columnKey === "scheduled_delivery" &&
state.sortedInfo.order,
render: (text, record) => (
<ProductionListDate
record={record}
field="scheduled_delivery"
pastIndicator
time
/>
),
sortOrder: state.sortedInfo.columnKey === "scheduled_delivery" && state.sortedInfo.order,
render: (text, record) => <ProductionListDate record={record} field="scheduled_delivery" pastIndicator time />
},
{
title: i18n.t("jobs.fields.scheduled_delivery") + " (HH:MM)",
@@ -209,9 +168,7 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
key: "scheduled_delivery_time",
ellipsis: true,
render: (text, record) => (
<TimeFormatter>{record.scheduled_delivery}</TimeFormatter>
),
render: (text, record) => <TimeFormatter>{record.scheduled_delivery}</TimeFormatter>
},
{
title: i18n.t("jobs.fields.ins_co_nm"),
@@ -219,8 +176,7 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
key: "ins_co_nm",
ellipsis: true,
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
sortOrder:
state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,
sortOrder: state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order
},
{
title: i18n.t("jobs.fields.clm_no"),
@@ -228,8 +184,7 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
key: "clm_no",
ellipsis: true,
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder:
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
sortOrder: state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order
},
{
title: i18n.t("jobs.fields.clm_total"),
@@ -237,11 +192,8 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
key: "clm_total",
ellipsis: true,
sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder:
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
),
sortOrder: state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
render: (text, record) => <CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
},
{
title: i18n.t("jobs.fields.owner_owing"),
@@ -249,35 +201,36 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
key: "owner_owing",
ellipsis: true,
sorter: (a, b) => a.owner_owing - b.owner_owing,
sortOrder:
state.sortedInfo.columnKey === "owner_owing" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.owner_owing}</CurrencyFormatter>
),
sortOrder: state.sortedInfo.columnKey === "owner_owing" && state.sortedInfo.order,
render: (text, record) => <CurrencyFormatter>{record.owner_owing}</CurrencyFormatter>
},
{
title: i18n.t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
ellipsis: true,
render: (text, record) => (
<PhoneFormatter>{record.ownr_ph1}</PhoneFormatter>
),
render: (text, record) => <PhoneFormatter>{record.ownr_ph1}</PhoneFormatter>
},
{
title: i18n.t("jobs.fields.ownr_ph2"),
dataIndex: "ownr_ph2",
key: "ownr_ph2",
ellipsis: true,
render: (text, record) => (
<PhoneFormatter>{record.ownr_ph2}</PhoneFormatter>
),
render: (text, record) => <PhoneFormatter>{record.ownr_ph2}</PhoneFormatter>
},
{
title: i18n.t("jobs.fields.specialcoveragepolicy"),
dataIndex: "special_coverage_policy",
key: "special_coverage_policy",
ellipsis: true,
sorter: (a, b) => Number(a.special_coverage_policy) - Number(b.special_coverage_policy),
sortOrder: state.sortedInfo.columnKey === "special_coverage_policy" && state.sortedInfo.order,
filters: [
{ text: "True", value: true },
{ text: "False", value: false }
],
onFilter: (value, record) => value === record.special_coverage_policy,
render: (text, record) => <Checkbox checked={record.special_coverage_policy} />
},
{
@@ -286,15 +239,23 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
key: "alt_transport",
ellipsis: true,
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
sortOrder:
state.sortedInfo.columnKey === "alt_transport" &&
state.sortedInfo.order,
sortOrder: state.sortedInfo.columnKey === "alt_transport" && state.sortedInfo.order,
filters:
(bodyshop &&
bodyshop.appt_alt_transport.map((s) => {
return {
text: s,
value: [s]
};
})) ||
[],
onFilter: (value, record) => value.includes(record.alt_transport),
render: (text, record) => (
<div>
{record.alt_transport}
<JobAltTransportChange job={record} />
</div>
),
)
},
{
title: i18n.t("jobs.fields.status"),
@@ -302,9 +263,8 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
key: "status",
ellipsis: true,
sorter: (a, b) => statusSort(a.status, b.status, activeStatuses),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => <ProductionListColumnStatus record={record} />,
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => <ProductionListColumnStatus record={record} />
},
{
title: i18n.t("jobs.fields.category"),
@@ -317,37 +277,30 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
bodyshop.md_categories.map((s) => {
return {
text: s,
value: [s],
value: [s]
};
})) ||
[],
onFilter: (value, record) => value.includes(record.category),
sorter: (a, b) => alphaSort(a.category, b.category),
sortOrder:
state.sortedInfo.columnKey === "category" && state.sortedInfo.order,
render: (text, record) => (
<ProductionListColumnCategory record={record} />
),
sortOrder: state.sortedInfo.columnKey === "category" && state.sortedInfo.order,
render: (text, record) => <ProductionListColumnCategory record={record} />
},
{
title: i18n.t("production.labels.bodyhours"),
dataIndex: "labhrs",
key: "labhrs",
sorter: (a, b) =>
a.labhrs.aggregate.sum.mod_lb_hrs - b.labhrs.aggregate.sum.mod_lb_hrs,
sortOrder:
state.sortedInfo.columnKey === "labhrs" && state.sortedInfo.order,
render: (text, record) => record.labhrs.aggregate.sum.mod_lb_hrs,
sorter: (a, b) => a.labhrs.aggregate.sum.mod_lb_hrs - b.labhrs.aggregate.sum.mod_lb_hrs,
sortOrder: state.sortedInfo.columnKey === "labhrs" && state.sortedInfo.order,
render: (text, record) => record.labhrs.aggregate.sum.mod_lb_hrs
},
{
title: i18n.t("production.labels.refinishhours"),
dataIndex: "larhrs",
key: "larhrs",
sorter: (a, b) =>
a.larhrs.aggregate.sum.mod_lb_hrs - b.larhrs.aggregate.sum.mod_lb_hrs,
sortOrder:
state.sortedInfo.columnKey === "larhrs" && state.sortedInfo.order,
render: (text, record) => record.larhrs.aggregate.sum.mod_lb_hrs,
sorter: (a, b) => a.larhrs.aggregate.sum.mod_lb_hrs - b.larhrs.aggregate.sum.mod_lb_hrs,
sortOrder: state.sortedInfo.columnKey === "larhrs" && state.sortedInfo.order,
render: (text, record) => record.larhrs.aggregate.sum.mod_lb_hrs
},
{
title: i18n.t("production.labels.totalhours"),
@@ -357,34 +310,36 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
a.labhrs.aggregate.sum.mod_lb_hrs +
a.larhrs.aggregate.sum.mod_lb_hrs -
(b.labhrs.aggregate.sum.mod_lb_hrs + b.larhrs.aggregate.sum.mod_lb_hrs),
sortOrder:
state.sortedInfo.columnKey === "totalhours" && state.sortedInfo.order,
sortOrder: state.sortedInfo.columnKey === "totalhours" && state.sortedInfo.order,
render: (text, record) =>
(
record.labhrs.aggregate.sum.mod_lb_hrs +
record.larhrs.aggregate.sum.mod_lb_hrs
).toFixed(1),
(record.labhrs.aggregate.sum.mod_lb_hrs + record.larhrs.aggregate.sum.mod_lb_hrs).toFixed(1)
},
{
title: i18n.t("production.labels.alert"),
dataIndex: "alert",
key: "alert",
render: (text, record) => <ProductionListColumnAlert record={record} />,
sorter: (a, b) => Number(a.production_vars?.alert || false) - Number(b.production_vars?.alert || false),
sortOrder: state.sortedInfo.columnKey === "alert" && state.sortedInfo.order,
filters: [
{ text: "True", value: true },
{ text: "False", value: false }
],
onFilter: (value, record) => value === (record.production_vars?.alert || false),
render: (text, record) => <ProductionListColumnAlert record={record} />
},
{
title: i18n.t("production.labels.note"),
dataIndex: "note",
key: "note",
ellipsis: true,
render: (text, record) => <ProductionListColumnNote record={record} />,
render: (text, record) => <ProductionListColumnNote record={record} />
},
{
title: i18n.t("production.labels.comment"),
dataIndex: "comment",
key: "comment",
ellipsis: true,
render: (text, record) => <ProductionListColumnComment record={record} />,
render: (text, record) => <ProductionListColumnComment record={record} />
},
{
title: i18n.t("production.labels.touchtime"),
@@ -392,7 +347,7 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
key: "tt",
render: (text, record) => {
return <ProductionlistColumnTouchTime job={record} />;
},
}
},
{
title: i18n.t("production.labels.bodypriority"),
@@ -402,11 +357,8 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
sorter: (a, b) =>
((a.production_vars && a.production_vars.bodypriority) || 11) -
((b.production_vars && b.production_vars.bodypriority) || 11),
sortOrder:
state.sortedInfo.columnKey === "bodypriority" && state.sortedInfo.order,
render: (text, record) => (
<ProductionListColumnBodyPriority record={record} />
),
sortOrder: state.sortedInfo.columnKey === "bodypriority" && state.sortedInfo.order,
render: (text, record) => <ProductionListColumnBodyPriority record={record} />
},
{
title: i18n.t("production.labels.paintpriority"),
@@ -416,12 +368,8 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
sorter: (a, b) =>
((a.production_vars && a.production_vars.paintpriority) || 11) -
((b.production_vars && b.production_vars.paintpriority) || 11),
sortOrder:
state.sortedInfo.columnKey === "paintpriority" &&
state.sortedInfo.order,
render: (text, record) => (
<ProductionListColumnPaintPriority record={record} />
),
sortOrder: state.sortedInfo.columnKey === "paintpriority" && state.sortedInfo.order,
render: (text, record) => <ProductionListColumnPaintPriority record={record} />
},
{
title: i18n.t("production.labels.detailpriority"),
@@ -431,110 +379,99 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
sorter: (a, b) =>
((a.production_vars && a.production_vars.detailpriority) || 11) -
((b.production_vars && b.production_vars.detailpriority) || 11),
sortOrder:
state.sortedInfo.columnKey === "detailpriority" &&
state.sortedInfo.order,
render: (text, record) => (
<ProductionListColumnDetailPriority record={record} />
),
sortOrder: state.sortedInfo.columnKey === "detailpriority" && state.sortedInfo.order,
render: (text, record) => <ProductionListColumnDetailPriority record={record} />
},
{
title: i18n.t("production.labels.sublets"),
dataIndex: "sublets",
key: "sublets",
render: (text, record) => (
<ProductionSubletsManageComponent subletJobLines={record.subletLines} />
),
render: (text, record) => <ProductionSubletsManageComponent subletJobLines={record.subletLines} />
},
{
title: i18n.t("jobs.fields.employee_body"),
dataIndex: "employee_body",
key: "employee_body",
sortOrder:
state.sortedInfo.columnKey === "employee_body" &&
state.sortedInfo.order,
sortOrder: state.sortedInfo.columnKey === "employee_body" && state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
bodyshop.employees?.find((e) => e.id === a.employee_body)?.first_name,
bodyshop.employees?.find((e) => e.id === b.employee_body)?.first_name
),
render: (text, record) => (
<ProductionListEmployeeAssignment
record={record}
type="employee_body"
/>
),
render: (text, record) => <ProductionListEmployeeAssignment record={record} type="employee_body" />
},
{
title: i18n.t("jobs.fields.employee_prep"),
dataIndex: "employee_prep",
key: "employee_prep",
sortOrder:
state.sortedInfo.columnKey === "employee_prep" &&
state.sortedInfo.order,
sortOrder: state.sortedInfo.columnKey === "employee_prep" && state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
bodyshop.employees?.find((e) => e.id === a.employee_prep)?.first_name,
bodyshop.employees?.find((e) => e.id === b.employee_prep)?.first_name
),
render: (text, record) => (
<ProductionListEmployeeAssignment
record={record}
type="employee_prep"
/>
),
render: (text, record) => <ProductionListEmployeeAssignment record={record} type="employee_prep" />
},
{
title: i18n.t("jobs.fields.employee_csr"),
dataIndex: "employee_csr",
key: "employee_csr",
sortOrder:
state.sortedInfo.columnKey === "employee_csr" && state.sortedInfo.order,
sortOrder: state.sortedInfo.columnKey === "employee_csr" && state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
bodyshop.employees?.find((e) => e.id === a.employee_csr)?.first_name,
bodyshop.employees?.find((e) => e.id === b.employee_csr)?.first_name
),
render: (text, record) => (
<ProductionListEmployeeAssignment record={record} type="employee_csr" />
),
render: (text, record) => <ProductionListEmployeeAssignment record={record} type="employee_csr" />
},
{
title: i18n.t("jobs.fields.employee_refinish"),
dataIndex: "employee_refinish",
key: "employee_refinish",
sortOrder:
state.sortedInfo.columnKey === "employee_refinish" &&
state.sortedInfo.order,
sortOrder: state.sortedInfo.columnKey === "employee_refinish" && state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
bodyshop.employees?.find((e) => e.id === a.employee_refinish)
?.first_name,
bodyshop.employees?.find((e) => e.id === b.employee_refinish)
?.first_name
bodyshop.employees?.find((e) => e.id === a.employee_refinish)?.first_name,
bodyshop.employees?.find((e) => e.id === b.employee_refinish)?.first_name
),
render: (text, record) => (
<ProductionListEmployeeAssignment
record={record}
type="employee_refinish"
/>
),
render: (text, record) => <ProductionListEmployeeAssignment record={record} type="employee_refinish" />
},
{
title: i18n.t("jobs.labels.parts_received"),
dataIndex: "parts_received",
key: "parts_received",
render: (text, record) => (
<ProductionListColumnPartsReceived record={record} />
),
render: (text, record) => <ProductionListColumnPartsReceived record={record} />
},
{
title: i18n.t("jobs.fields.partsstatus"),
dataIndex: "partsstatus",
key: "partsstatus",
render: (text, record) => (
<JobPartsQueueCount parts={record.joblines_status} record={record} />
),
render: (text, record) => <JobPartsQueueCount parts={record.joblines_status} record={record} />
},
{
title: i18n.t("jobs.labels.estimator"),
dataIndex: "estimator",
key: "estimator",
sorter: (a, b) =>
alphaSort(
`${a.est_ct_fn || ""} ${a.est_ct_ln || ""}`.trim(),
`${b.est_ct_fn || ""} ${b.est_ct_ln || ""}`.trim()
),
sortOrder: state.sortedInfo.columnKey === "estimator" && state.sortedInfo.order,
filters:
(data &&
data
.map((j) => `${j.est_ct_fn || ""} ${j.est_ct_ln || ""}`.trim())
.filter(onlyUnique)
.map((s) => {
return {
text: s || "N/A",
value: [s]
};
})) ||
[],
onFilter: (value, record) => value.includes(`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim()),
render: (text, record) => `${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim()
},
//Added as a place holder for St Claude. Not implemented as it requires another join for a field used by only 1 client.
@@ -564,12 +501,8 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
key: "date_repairstarted",
ellipsis: true,
sorter: (a, b) => dateSort(a.date_repairstarted, b.date_repairstarted),
sortOrder:
state.sortedInfo.columnKey === "date_repairstarted" &&
state.sortedInfo.order,
render: (text, record) => (
<ProductionListDate record={record} field="date_repairstarted" time />
),
sortOrder: state.sortedInfo.columnKey === "date_repairstarted" && state.sortedInfo.order,
render: (text, record) => <ProductionListDate record={record} field="date_repairstarted" time />
},
{
title: i18n.t("jobs.fields.date_repairstarted") + " (HH:MM)",
@@ -577,10 +510,8 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
key: "date_repairstarted_time",
ellipsis: true,
render: (text, record) => (
<TimeFormatter>{record.date_repairstarted}</TimeFormatter>
),
},
render: (text, record) => <TimeFormatter>{record.date_repairstarted}</TimeFormatter>
}
];
};
export default r;

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