Compare commits

...

428 Commits

Author SHA1 Message Date
Dave Richer
18da11f593 - Merge master-aio, bump packages.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-19 12:27:30 -04:00
Dave Richer
82ecb5533f Merged in release/2024-08-16 (pull request #1625)
Release/2024 08 16

Approved-by: Allan Carr
2024-08-16 23:31:33 +00:00
Allan Carr
d3289d85f1 Merged in feature/IO-2879-936001-TOWING-to-QB (pull request #1627)
IO-2879 Adjust placement of variable
2024-08-16 19:25:14 +00:00
Allan Carr
e628b1364c IO-2879 Adjust placement of variable
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-16 12:27:26 -07:00
Allan Carr
6c421c1447 Merged in feature/IO-2884-Filter-for-Production-Board-Alert (pull request #1620)
IO-2884 Production List Board Filter

Approved-by: Dave Richer
2024-08-16 16:16:53 +00:00
Dave Richer
99369e7040 Merged in feature/IO-2884-Add-Alert-Filter-To-Production-Board (pull request #1622)
- Add Alert Filter to visual production board
2024-08-16 16:15:54 +00:00
Dave Richer
01cbdf14a9 - Add Alert Filter to visual production board
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-16 12:14:52 -04:00
Allan Carr
f691aca241 IO-2884 Production List Board Filter
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-16 09:11:29 -07:00
Dave Richer
85495a11e3 Merged in feature/IO-2884-Add-Alert-Filter-To-Production-Board (pull request #1618)
Feature/IO-2884 Add Alert Filter To Production Board

Approved-by: Allan Carr
2024-08-16 15:38:29 +00:00
Dave Richer
134ce05d27 - Add Alert Filter to visual production board
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-16 11:16:20 -04:00
Dave Richer
3498fbc8f1 - Add Alert Filter to visual production board
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-16 11:14:32 -04:00
Dave Richer
f49f72ce7f - Revert ZOHO Change
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-16 10:49:59 -04:00
Dave Richer
a5e3b6ce33 Merged in feature/IO-2882-Unsaved-Listview-Changes (pull request #1612)
- Production Board List View Unsaved Changes Prompt

Approved-by: Allan Carr
2024-08-16 00:45:48 +00:00
Dave Richer
0fd945b859 - fix
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-15 20:42:18 -04:00
Dave Richer
879eba0247 - Add Alert
- Fix 2 bugs

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-15 20:16:03 -04:00
Allan Carr
bb49dd77a1 Merged in feature/IO-2879-936001-TOWING-to-QB (pull request #1613)
IO-2879 936001 TOWING to QB

Approved-by: Dave Richer
2024-08-15 21:16:27 +00:00
Allan Carr
ae705322f8 IO-2879 936001 TOWING to QB
Prevent double sending 936001 Towing lines to QB

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-15 13:41:33 -07:00
Dave Richer
36d92d4060 - Production Board List View Unsaved Changes Prompt
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-15 14:59:47 -04:00
Dave Richer
3ce2b1ab19 - revert (put back in) zoho change
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-15 13:35:18 -04:00
Dave Richer
52e756a78a Merged in feature/IO-2878-Enhance-Beta-Switch (pull request #1607)
- Improve handle beta code (AIO Version)
2024-08-15 15:21:09 +00:00
Dave Richer
5a36cb7cf1 - Improve handle beta code (AIO Version)
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-15 11:19:59 -04:00
Allan Carr
9138f4be16 Merged in feature/IO-2856-Mark-Exported-Buttons (pull request #1601)
IO-2856 Mark Exported Button

Approved-by: Dave Richer
2024-08-14 18:40:51 +00:00
Allan Carr
df93357cec IO-2856 Mark Exported Button
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-14 11:38:35 -07:00
Dave Richer
8ab23c4ca6 Merged in feature/IO-2878-Enhance-Beta-Switch (pull request #1599)
- Improve handle beta code (AIO Version)
2024-08-14 17:40:50 +00:00
Allan Carr
f179d69420 Merged in feature/IO-2876-Opensearch-Sorters (pull request #1598)
IO-2876 Filtered Search Sorters correction

Approved-by: Dave Richer
2024-08-14 17:39:06 +00:00
Dave Richer
730a7a233d - Improve handle beta code (AIO Version)
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-14 13:29:35 -04:00
Dave Richer
84ad10fa9c Merged in feature/IO-2877-Package-Updates-Alert-Update (pull request #1597)
- Remove Joyride and its cause - Package updates (front + back)
2024-08-14 15:56:43 +00:00
Patrick Fic
b1cda41f56 - Remove Joyride and its cause
- Package updates (front + back)

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-14 11:54:52 -04:00
Allan Carr
0bce921f69 IO-2876 Filtered Search Sorters correction
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-13 16:00:17 -07:00
Dave Richer
97282740f5 Merged in release/2024-08-09-no-zoho (pull request #1596)
Release/2024 08 09 no zoho
2024-08-13 21:10:30 +00:00
Patrick Fic
150ae02978 - revert change to zoho
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-13 17:06:40 -04:00
Allan Carr
abeffb2a19 Merged in feature/IO-2520-Kaizen-Data-Pump (pull request #1594)
IO-2520 Add SHAW to Shop list
2024-08-13 19:29:26 +00:00
Allan Carr
70058b6bd4 IO-2520 Add SHAW to Shop list
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-13 12:32:14 -07:00
Allan Carr
67c63f81d9 Merge branch 'master-AIO' into feature/IO-2520-Kaizen-Data-Pump
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-13 12:30:42 -07:00
Allan Carr
fb8452b2bb Merged in feature/IO-2874-InsertBill-full-Error-Logging (pull request #1593)
IO-2874 InsertBill Full Error Log
2024-08-13 18:00:39 +00:00
Allan Carr
1f281e8439 IO-2874 InsertBill Full Error Log
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-13 11:03:15 -07:00
Dave Richer
223c705e8f Merged in feature/IO-2869-Production-Board-List-Alert-2 (pull request #1587)
- mute gray more, mute more buttons
2024-08-09 17:28:44 +00:00
Dave Richer
00d8b533f4 - mute gray more, mute more buttons
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-09 13:28:01 -04:00
Dave Richer
3b25a8fe07 - Hotfix for rome ZOHO
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-09 12:40:21 -04:00
Dave Richer
a57b9cddb5 Merged in feature/IO-2869-Production-Board-List-Alert-2 (pull request #1583)
- Fix alert in table view - (modify some docs)

Approved-by: Allan Carr
2024-08-08 17:02:45 +00:00
Dave Richer
064ed1bb8b - Enhance Production Alert
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-08 12:16:56 -04:00
Dave Richer
efda254981 - Fix alert in table view
- (modify some docs)

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-08 11:46:42 -04:00
Dave Richer
3fa6b8b6ac Merged in feature/IO-2868-Add-Tasks-To-Production-Board (pull request #1581)
- Add Tasks to production board (and required refactors)
2024-08-08 14:15:41 +00:00
Dave Richer
4603643240 Merged release/2024-08-09 into feature/IO-2868-Add-Tasks-To-Production-Board 2024-08-07 14:38:29 +00:00
Allan Carr
f82b02958f Merged in feature/IO-2866-Add-OP20-to-masterdata (pull request #1580)
IO-2866 Add OP20 to masterdata

Approved-by: Dave Richer
2024-08-07 14:36:59 +00:00
Dave Richer
5d1f61753b - Add Tasks to production board (and required refactors)
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-07 10:36:00 -04:00
Allan Carr
883043cde3 IO-2866 Add OP20 to masterdata
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-06 17:39:55 -07:00
Allan Carr
85ce7c638d Merged in feature/IO-2861-Disable-Edit-Vendor-&-Invoice-Number-on-In-House (pull request #1574)
IO-2861 Disable Editing of Vendor and Invoice Number when In House

Approved-by: Dave Richer
2024-08-06 20:26:00 +00:00
Allan Carr
547e279693 Merged in hotfix/2024-08-06 (pull request #1579)
IO-2865 Job Total Labor InstanceManger
2024-08-06 19:32:32 +00:00
Allan Carr
b9bdefe898 Merged in feature/IO-2865-Job-Total-Labor-InstanceManager (pull request #1578)
IO-2865 Job Total Labor InstanceManger
2024-08-06 19:31:53 +00:00
Allan Carr
c963cb6fcc IO-2865 Job Total Labor InstanceManger
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-06 12:22:55 -07:00
Allan Carr
fa6d4cce2a Merged in feature/IO-2864-LAET-&-LAUT-Labor-Type (pull request #1575)
IO-2864 Adjust to LAET and LAUT adjustment to target both Rome and ProManager
2024-08-06 19:22:48 +00:00
Allan Carr
c7188d5a71 Merged in feature/IO-2865-Job-Total-Labor-InstanceManager (pull request #1576)
IO-2865 Job Total Labor InstanceManger
2024-08-06 19:22:15 +00:00
Allan Carr
c333d72743 IO-2864 Adjust to LAET and LAUT adjustment to target both Rome and ProManager
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-06 11:54:22 -07:00
Allan Carr
2a1ec4eff3 IO-2861 Disable Editing of Vendor and Invoice Number when In House
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-06 09:15:37 -07:00
Dave Richer
190b2e5b5c Merged in release/2024-08-02 (pull request #1573)
- Show default stats instead of no stats if statisticsOrder object is not yet valid.
2024-08-02 23:55:03 +00:00
Dave Richer
3e2a517272 Merged in feature/IO-2743-Production-Board-GridDND (pull request #1572)
- Show default stats instead of no stats if statisticsOrder object is not yet valid.
2024-08-02 23:54:16 +00:00
Dave Richer
a6cd35bd06 - Show default stats instead of no stats if statisticsOrder object is not yet valid.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-02 19:52:49 -04:00
Dave Richer
ba3032e553 Merged in release/2024-08-02 (pull request #1571)
- Fix bug
2024-08-02 23:39:35 +00:00
Dave Richer
20fd452335 Merged in feature/IO-2743-Production-Board-GridDND (pull request #1569)
- Fix bug
2024-08-02 23:35:26 +00:00
Dave Richer
8f83a5f5e6 - Fix bug
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-02 19:34:15 -04:00
Dave Richer
4e417d1b10 Merged in release/2024-08-02 (pull request #1568)
Release/2024 08 02
2024-08-02 22:28:13 +00:00
Allan Carr
3ee63a503a Merged in feature/IO-2854-ProManager-Profile-Discounts (pull request #1567)
Dinero Object
2024-08-02 22:10:16 +00:00
Allan Carr
8dd0e12398 Dinero Object
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-02 15:12:51 -07:00
Allan Carr
253b4d0ddc Merged in feature/IO-2854-ProManager-Profile-Discounts (pull request #1566)
Correct for Dinero type
2024-08-02 21:54:24 +00:00
Allan Carr
a8b0931659 Correct for Dinero type
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-02 14:49:20 -07:00
Allan Carr
11a182c68a Correct query and object call
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-02 14:19:58 -07:00
Allan Carr
2583d21cf1 Merged in feature/IO-2854-ProManager-Profile-Discounts (pull request #1564)
Correct query and object call
2024-08-02 21:17:49 +00:00
Dave Richer
aa6032d74f Merged in feature/IO-2743-Production-Board-GridDND (pull request #1563)
Feature/IO-2743 Production Board GridDND
2024-08-02 20:37:59 +00:00
Dave Richer
e1bac5fe9f - V1 Finished
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-02 16:37:00 -04:00
Dave Richer
0c74848c54 Merged in feature/IO-2743-Production-Board-GridDND (pull request #1561)
Feature/IO-2743 Production Board GridDND
2024-08-02 20:34:33 +00:00
Dave Richer
1c4f2d2de0 - V1 Finished
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-02 16:33:50 -04:00
Dave Richer
d67d2aa064 Merge remote-tracking branch 'origin/master-AIO' into feature/IO-2743-Production-Board-GridDND 2024-08-02 16:32:17 -04:00
Dave Richer
208f3281a9 - V1 Finished
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-02 16:28:50 -04:00
Dave Richer
fb7e3bc812 - Check Point
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-02 15:49:09 -04:00
Allan Carr
884adb963c Merged in release/2024-08-02 (pull request #1560)
IO-2854 Handle Exporting to Accounting
2024-08-02 19:18:38 +00:00
Allan Carr
29c4df9f76 Merged in feature/IO-2854-ProManager-Profile-Discounts (pull request #1559)
IO-2854 Handle Exporting to Accounting

Approved-by: Dave Richer
2024-08-02 19:13:03 +00:00
Allan Carr
f1940a320c IO-2854 Handle Exporting to Accounting
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-02 11:30:08 -07:00
Dave Richer
a297bba193 - Check Point
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-02 11:29:31 -04:00
Allan Carr
375a3fe386 Merged in hotfix/AIO/2024-08-01 (pull request #1558)
Hotfix/AIO/2024 08 01
2024-08-01 21:59:59 +00:00
Allan Carr
d837754ef6 Merged in feature/IO-2857-Tech-Job-Drawer-PartsOrderDrawer-Component (pull request #1557)
Feature/IO-2857 Tech Job Drawer PartsOrderDrawer Component
2024-08-01 21:58:49 +00:00
Allan Carr
49a1f0c42c Correct for Labor Adjustment being null
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-01 14:42:07 -07:00
Allan Carr
a5033e10cd Merged in release/2024-08-02 (pull request #1556)
Correct for Labor Adjustment being null
2024-08-01 21:40:13 +00:00
Allan Carr
00f0952ab2 Merged in feature/IO-2854-ProManager-Profile-Discounts (pull request #1555)
Correct for Labor Adjustment being null
2024-08-01 21:39:34 +00:00
Allan Carr
ec5b636c26 Merge branch 'release/2024-08-02' into test-AIO
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>

# Conflicts:
#	client/src/translations/en_us/common.json
#	client/src/translations/es/common.json
#	client/src/translations/fr/common.json
2024-08-01 13:57:45 -07:00
Allan Carr
7126cc0d56 Correction for upsteam change
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-01 13:31:44 -07:00
Allan Carr
568d8887ce Merged in feature/IO-2854-ProManager-Profile-Discounts (pull request #1554)
Correction for upsteam change
2024-08-01 20:29:37 +00:00
Allan Carr
4927e84a88 Adjustment to only have PartsDrawer show if not a technician
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-01 12:47:41 -07:00
Allan Carr
d2d8039cb9 Merged in feature/IO-2857-Tech-Job-Drawer-PartsOrderDrawer-Component (pull request #1553)
IO-2857 Tech console fixes

Approved-by: Dave Richer
2024-08-01 19:46:10 +00:00
Allan Carr
5da242d785 Remove unneeded spaces
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-01 12:29:04 -07:00
Allan Carr
87f25b1ed4 IO-2857 Tech console fixes
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-01 12:16:56 -07:00
Dave Richer
4f9afc8578 Merged in feature/IO-2743-Production-Board-GridDND (pull request #1552)
- fix regression in settings - add defaults
2024-08-01 15:52:27 +00:00
Dave Richer
defadf70e3 - fix regression in settings
- add defaults

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-01 11:51:13 -04:00
Dave Richer
82be3853f0 Merged in feature/IO-2743-Production-Board-GridDND (pull request #1551)
Feature/IO-2743 Production Board GridDND
2024-08-01 15:31:32 +00:00
Dave Richer
ff184926fc - add noteupsert modal to PB
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-01 11:31:06 -04:00
Dave Richer
0e9768c0f6 - fix regression in card sizes
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-01 11:27:46 -04:00
Allan Carr
805c924a15 Merged in feature/IO-2852-Production-Add-Job-Note (pull request #1550)
IO-2852 Production Add Job Note
2024-07-31 22:26:56 +00:00
Allan Carr
10ff8fc432 Reverting in favor of correcting in new Production Board
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-31 15:22:22 -07:00
Allan Carr
e2db0bab9f Merged in feature/IO-2854-ProManager-Profile-Discounts (pull request #1549)
IO-2854 Profile Adjustments for LA and MA

Approved-by: Dave Richer
2024-07-31 22:14:59 +00:00
Allan Carr
92ee5169ab IO-2852 Production Add Job Note
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-31 14:49:01 -07:00
Allan Carr
5cf2345dca IO-2854 Profile Adjustments for LA and MA
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-31 14:15:31 -07:00
Dave Richer
0accf6a18f Merged in feature/IO-2743-Production-Board-GridDND (pull request #1548)
Feature/IO-2743 Production Board GridDND

Approved-by: Allan Carr
2024-07-31 20:47:28 +00:00
Dave Richer
52e46aad86 - fix bug with body empty
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-31 14:18:01 -04:00
Dave Richer
43f49e5e51 - minor refactor to defaults
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-31 14:07:36 -04:00
Dave Richer
60908b123d - Clean up Card Settings
- Code refactor for maintainability
- Allow users to adjust the order of the statistics via drag and drop

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-31 14:04:50 -04:00
Dave Richer
5169fd178a Merged in feature/IO-2743-Production-Board-GridDND (pull request #1547)
Statistics and Tool Tip Bug fix
2024-07-30 21:31:20 +00:00
Dave Richer
bbc446ef01 - Finish up with Statistics
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-30 17:24:55 -04:00
Dave Richer
65bb8c6f26 Merge remote-tracking branch 'origin/master-AIO' into feature/IO-2743-Production-Board-GridDND 2024-07-30 13:01:46 -04:00
Allan Carr
171f40819c Merged in release/2024-07-26 (pull request #1546)
Release/2024 07 26

Approved-by: Dave Richer
2024-07-29 21:27:54 +00:00
Allan Carr
4baa26cb25 Merged in release/2024-07-26 (pull request #1545)
Release/2024 07 26
2024-07-29 19:52:48 +00:00
Allan Carr
01da7fde31 Merged in feature/IO-2564-Row-Expander-Links (pull request #1543)
IO-2564 Row Expander Links

Approved-by: Dave Richer
2024-07-29 18:57:50 +00:00
Allan Carr
3c8234d715 Merged in feature/IO-2853-Production-Board-Date-Modal (pull request #1544)
IO-2853 Production Board Date Modal

Approved-by: Dave Richer
2024-07-29 18:53:48 +00:00
Allan Carr
866f242465 IO-2853 Production Board Date Modal
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-29 11:30:52 -07:00
Allan Carr
f690596825 IO-2564 Row Expander Links
Move Drawer from Parts Order into seperate componenet and pass props down from main page and then link the BillDetailEdit Container into joblines

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-29 11:21:40 -07:00
Allan Carr
5f494e4b78 IO-2564 Revert location of query
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-26 13:28:29 -07:00
Allan Carr
7b814329da Merged in release/2024-07-26 (pull request #1542)
IO-2564 Revert location of query
2024-07-26 20:27:00 +00:00
Allan Carr
2fc0e1cf50 Merged in feature/IO-2564-Row-Expander-Links (pull request #1541)
IO-2564 Revert location of query
2024-07-26 20:26:20 +00:00
Allan Carr
ba3f98aeb4 Merged in release/2024-07-26 (pull request #1540)
Release/2024 07 26
2024-07-26 16:15:53 +00:00
Dave Richer
f49818ade3 - Alans tool tip issue
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-25 14:07:15 -04:00
Dave Richer
b37c70494b Merged in feature/IO-2743-Production-Board-GridDND (pull request #1539)
Feature/IO-2743 Production Board GridDND
2024-07-25 17:18:28 +00:00
Dave Richer
ff1db26f41 - fix minor issue with production notes / add some notes on
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-25 13:16:30 -04:00
Allan Carr
3550c97966 Merged in feature/IO-2849-Missing-Translation-from-Shop-Config (pull request #1538)
IO-2849 Missing Translation from Shop Config

Approved-by: Dave Richer
2024-07-25 16:57:22 +00:00
Allan Carr
956c686360 Merged in feature/IO-2850-Missing-Fields-from-Shop-Config (pull request #1537)
IO-2850 Missing fields from save on Shop Config Taxes
2024-07-25 16:56:26 +00:00
Allan Carr
508b0d7711 Merged in feature/IO-2851-Rome-Manual-Job-Creation-Taxes (pull request #1536)
IO-2851 Manual Job Creation Taxes

Approved-by: Dave Richer
2024-07-25 16:55:41 +00:00
Allan Carr
abb1464e30 IO-2849 Missing Translation from Shop Config
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-25 08:39:42 -07:00
Allan Carr
704d5415d6 IO-2850 Missing fields from save on Shop Config Taxes
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-25 08:38:01 -07:00
Allan Carr
6cc1cfd1b0 IO-2851 Manual Job Creation Taxes
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-25 08:36:08 -07:00
Dave Richer
e6455a7fd3 - Fix a zooming issue (zoom far in, then out, lanes don't adjust back) I noticed during the call.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-23 16:08:48 -04:00
Dave Richer
a9aad2a751 Merged in feature/IO-2743-Production-Board-GridDND (pull request #1535)
Production Board Update 1

Approved-by: Allan Carr
2024-07-23 16:50:24 +00:00
Allan Carr
1bf745345d Merged in feature/IO-2564-Row-Expander-Links (pull request #1533)
IO-2564 Row Expander Links

Approved-by: Dave Richer
2024-07-23 03:36:53 +00:00
Allan Carr
63c71ed923 Merged in feature/IO-2848-Update_Job-Query (pull request #1534)
IO-2848 UPDATE_JOB query

Approved-by: Dave Richer
2024-07-23 03:35:53 +00:00
Dave Richer
3a844aefa0 - Merge Master, update packages.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-22 23:34:46 -04:00
Allan Carr
4a16df36dd IO-2848 UPDATE_JOB query
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-22 17:35:05 -07:00
Allan Carr
93c92e8976 IO-2564 Row Expander Links
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-22 13:30:08 -07:00
Dave Richer
136d52ec0b - Merge Master, update packages.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-22 14:47:44 -04:00
Dave Richer
9523e5534d Merged in feature/IO-2743-Production-Board-GridDND (pull request #1532)
Feature/IO-2743 Production Board GridDND

Approved-by: Allan Carr
2024-07-19 21:56:52 +00:00
Dave Richer
d20048026c Merge remote-tracking branch 'origin/master-AIO' into feature/IO-2743-Production-Board-GridDND 2024-07-19 16:17:22 -04:00
Dave Richer
7ba3ed2b89 Merged in release/2024-07-19 (pull request #1531)
Release/2024 07 19
2024-07-19 20:02:39 +00:00
Dave Richer
2cc0b7d741 Merged in bugfix/ProductFruitsWrapper (pull request #1530)
Bugfix/ProductFruitsWrapper
2024-07-19 20:02:11 +00:00
Dave Richer
97693fbcff - Add Prop Types to ProductFruitsWrapper.jsx
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-19 16:01:40 -04:00
Dave Richer
5e94b1a71e - Improve product fruits wrapper for extra checks
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-19 15:50:09 -04:00
Allan Carr
970275e62a Merged in release/2024-07-19 (pull request #1529)
Release/2024 07 19
2024-07-19 19:02:15 +00:00
Allan Carr
15c7b6f1c8 Merged in release/2024-07-19 (pull request #1528)
Release/2024 07 19
2024-07-19 17:26:48 +00:00
Dave Richer
aa076da255 Merged in bugfix/ProductFruitsWrapper (pull request #1527)
- Improve product fruits wrapper for extra checks

Approved-by: Allan Carr
2024-07-19 17:24:22 +00:00
Dave Richer
3dd154de79 - Improve product fruits wrapper for extra checks
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-19 13:23:43 -04:00
Dave Richer
72a2366abe - Improve product fruits wrapper for extra checks
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-19 13:18:10 -04:00
Allan Carr
36f517e8e1 Merged in feature/IO-2845-Payment-by-Date-Grouped-by-Payment-Type (pull request #1525)
IO-2845 Payments Grouped by Payment Type

Approved-by: Dave Richer
2024-07-19 17:16:26 +00:00
Allan Carr
981fb57d36 Merged in feature/IO-2847-Employee-Rate-Filter (pull request #1526)
IO-2847 Employee Rate Filter

Approved-by: Dave Richer
2024-07-19 17:15:04 +00:00
Dave Richer
3149c3cfc6 - Patricks request for card settings to be buttons and not switches
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-19 11:54:50 -04:00
Allan Carr
a059c2b5a8 IO-2847 Employee Rate Filter
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-19 08:41:12 -07:00
Dave Richer
f91e0ec39f - Package updates
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-19 10:56:51 -04:00
Dave Richer
c31d4096c6 - Make sure subscription and initial query only grab jobs that have statuses that are on the board
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-18 16:59:17 -04:00
Dave Richer
118f14ed4c - Touch point for working kanbanparents
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-18 15:45:11 -04:00
Allan Carr
207bb39672 IO-2845 Payments Grouped by Payment Type
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-17 14:15:38 -07:00
Dave Richer
0d0791cc41 Merge remote-tracking branch 'origin/master-AIO' into feature/IO-2743-Production-Board-GridDND 2024-07-17 12:41:21 -04:00
Dave Richer
16e34e4ed9 Merged in bugfix/productfruits (pull request #1523)
- misc updates / clear stage
2024-07-17 16:30:53 +00:00
Dave Richer
cd04f2b2b2 Merged in bugfix/productfruits (pull request #1524)
- misc updates / clear stage
2024-07-17 16:30:40 +00:00
Dave Richer
26e164b4d1 - misc updates / clear stage
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-17 12:22:31 -04:00
Dave Richer
f45aa69917 - misc updates / clear stage
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-16 15:53:56 -04:00
Dave Richer
4b44ff29ee - Merge master-aio
- Package updates
- Fix subscription

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-16 09:43:57 -04:00
Allan Carr
f15f5d7309 Merged in release/2024-07-19 (pull request #1522)
IO-2843 State Tax for QBO_USA and Region CA_
2024-07-15 23:56:22 +00:00
Allan Carr
ae6b2fe4f5 Merged in feature/IO-2843-QBO_USA-Switch-for-IO (pull request #1521)
IO-2843 State Tax for QBO_USA and Region CA_
2024-07-15 23:53:59 +00:00
Allan Carr
4ca686126a IO-2843 State Tax for QBO_USA and Region CA_
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-15 16:53:48 -07:00
Dave Richer
ea55b1a797 - Remove Fake data as it is no longer required for testing.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-15 13:47:05 -04:00
Dave Richer
668366e886 - Fixed the bug where dragging a item in a 1 item lane in vertical mode and not releasing it in a valid position or the same position, would cause the card the vanish until the lane was refreshed. (One of the three big ones)
- Adjusted Small / Medium / Large as per allan

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-15 13:44:26 -04:00
Dave Richer
eb1b2aef55 - Don't do anything if the destination and source are the same
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-15 12:47:15 -04:00
Dave Richer
906430add5 - Finish adjusting the Board Settings component.
- Extract some components out of Lane.jsx into their own files
- Fix misc bugs around preserving lane height in Vertical mode
- Add missing non english translation strings

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-15 12:06:12 -04:00
Dave Richer
f8fbbd2323 - Finish adjusting the Board Settings component.
- Extract some components out of Lane.jsx into their own files
- Fix misc bugs around preserving lane height in Vertical mode
- Add missing non english translation strings

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-15 11:51:34 -04:00
Dave Richer
adab6ef0c6 - Fix bug where in vertical Mode, the lanes would be as large as the largest lane.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-15 10:11:46 -04:00
Dave Richer
ddfd91617f - Optimize isBodyEmpty
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-14 21:35:58 -04:00
Dave Richer
8418b4cb91 - Stage Clear
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-14 21:32:46 -04:00
Dave Richer
e593943d99 - Working Configurable card sizes for both Horizontal and Vertical!
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-14 21:24:24 -04:00
Dave Richer
b58b5e65dc - clear stage.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-14 19:04:50 -04:00
Dave Richer
6d393987e8 - clear stage.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-14 19:01:35 -04:00
Allan Carr
c254a8abfe Merged in release/2024-07-12 (pull request #1520)
Release/2024 07 12
2024-07-12 20:15:53 +00:00
Allan Carr
852231b503 Merged in release/2024-07-12 (pull request #1519)
Release/2024 07 12
2024-07-11 23:10:42 +00:00
Allan Carr
f864e40a90 Merged in feature/IO-2836-Charts-Route (pull request #1518)
IO-2836 Charts Route

Approved-by: Dave Richer
2024-07-11 22:17:01 +00:00
Allan Carr
802f70dde8 IO-2836 Charts Route
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-11 11:25:27 -07:00
Allan Carr
6cb4fd6b93 Merged in feature/IO-2839-DMS-Allocation-Labels (pull request #1514)
IO-2839 DMS Allocation Labels

Approved-by: Dave Richer
2024-07-11 17:10:49 +00:00
Allan Carr
70ca6edcb2 Merged in feature/IO-2840-Area-of-Damage-Correction (pull request #1515)
IO-2840 Correct for Area of Damage that is only 1 Charater intead of 2

Approved-by: Dave Richer
2024-07-11 17:09:41 +00:00
Allan Carr
9dcc861740 Merged in IO-2841-Non-Parts-in-jobline_status (pull request #1516)
IO-2841 Non-Parts listed in Jobline_Status

Approved-by: Dave Richer
2024-07-11 17:09:04 +00:00
Allan Carr
fecb7fb24b Merged in feature/IO-2837-Job-Card-Add-to-Scoreboard (pull request #1517)
IO-2837 Add to Scoreboard from Job Drawer

Approved-by: Dave Richer
2024-07-11 17:06:37 +00:00
Allan Carr
c5b94db8af Merged in feature/IO-2520-Kaizen-Data-Pump (pull request #1513)
IO-2520 Kaizen Data Pump

Approved-by: Dave Richer
2024-07-11 17:06:23 +00:00
Allan Carr
6690c9c692 IO-2841 Non-Parts listed in Jobline_Status
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-11 09:15:18 -07:00
Allan Carr
506edcb3c6 IO-2837 Add to Scoreboard from Job Drawer
Query restricted necessary lines for proper labour calculations

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-10 16:45:35 -07:00
Allan Carr
c45e53e38b IO-2840 Correct for Area of Damage that is only 1 Charater intead of 2
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-10 16:00:47 -07:00
Allan Carr
d8d8a4701e IO-2839 DMS Allocation Labels
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-10 14:54:04 -07:00
Allan Carr
970c4ee9e1 IO-2520 Kaizen Data Pump
Additional fields requested

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-10 10:33:53 -07:00
Allan Carr
c2386f43ef Merge branch 'master-AIO' into feature/IO-2520-Kaizen-Data-Pump
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-10 10:32:21 -07:00
Dave Richer
870878d151 - Center RO / Justify Alerts
- Prevent Height redraw in Vertical mode
- add default setting for model_info
- cleanup renderDroppable

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-09 16:52:23 -04:00
Dave Richer
cdf7bcf839 - clear stage.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-09 15:57:51 -04:00
Allan Carr
0fd3e75862 Merged in feature/IO-2835-CDK-Calculate-Allocations-Route (pull request #1511)
IO-2835 CDK Calculate Allocation Route

Approved-by: Dave Richer
2024-07-09 15:54:21 +00:00
Allan Carr
fcd3234fd9 Merged in feature/IO-2838-JobCloseRoGuardSublet (pull request #1512)
IO-2838 JobCloseRoGuardSublet

Approved-by: Dave Richer
2024-07-09 15:52:45 +00:00
Allan Carr
2afa810e6c IO-2838 JobCloseRoGuardSublet
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-08 18:15:47 -07:00
Allan Carr
ff33b924b2 IO-2835 CDK Calculate Allocation Route
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-07-08 18:07:33 -07:00
Dave Richer
8a4fee7aea - Daily checkpoint, speed (grid), and presentation
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-08 17:57:26 -04:00
Dave Richer
fa578efee4 - Vite config was using a new on BrowserTracing when not required, causing a failed built product.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-08 12:32:54 -04:00
Dave Richer
4b6b4c0c63 - Fix bug where changing the card settings would default the orientation back to horizontal despite showing vertical mode on the toggle.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-08 10:46:31 -04:00
Dave Richer
8199ab83ef - Consolidate the react-trello dir into the production-board-kanban.component.jsx dir
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-05 21:29:44 -04:00
Dave Richer
8e50d0ba53 - So so many things
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-05 21:23:28 -04:00
Dave Richer
7d72d66a97 - Lane Checkpoint: In Horizontal mode, all lanes will be the height of the largest lane, this way if you are dragging cards from the bottom of one lane, you do not need to drag to the top of another.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-05 15:15:28 -04:00
Dave Richer
3454694887 - Packages updated / Confirmed
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-05 12:55:57 -04:00
Dave Richer
2a51c8a2bf - Checkpoint, so so much
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-04 18:00:02 -04:00
Dave Richer
524f623fd4 - Performance / bug fixes / massive deleting of react-trello files we are not using.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-03 15:30:15 -04:00
Dave Richer
7ab7e18d96 - Performance / bug fixes / massive deleting of react-trello files we are not using.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-03 15:10:01 -04:00
Dave Richer
6f2c8dba5a - Optimization and Edgecases
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-03 01:10:11 -04:00
Dave Richer
162d8bfffe - Optimization and Edgecases
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-02 20:50:34 -04:00
Dave Richer
61569d97cb - Checkpoint
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-02 18:04:08 -04:00
Dave Richer
975622a31c - Checkpoint (Major Bug Fixed)
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-02 15:34:03 -04:00
Dave Richer
7524d06126 - Checkpoint
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-02 14:16:25 -04:00
Dave Richer
7f75eeadb9 - LaneDrag / DragEnd, fixed to support Virtual Lists.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-06-30 21:58:08 -04:00
Dave Richer
4db2b73397 - Checkpoint, this thing is fast has heck.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-06-30 21:19:04 -04:00
Dave Richer
84bb25985b Merged in release/2024-06-28 (pull request #1510)
Release - 2024/06/28

Approved-by: Allan Carr
2024-06-28 23:02:23 +00:00
Dave Richer
2f493c63f8 - Clear stage prior to implementing replacement for collapsed lanes (with virtual lists)
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-06-28 15:08:26 -04:00
Allan Carr
2761356f02 Merged in release/2024-06-28 (pull request #1509)
IO-2832 Purchases by RO - Invoice Date bound
2024-06-28 17:04:50 +00:00
Allan Carr
e38a58550f Merged in feature/IO-2832-Purchases-by-RO-Date (pull request #1508)
IO-2832 Purchases by RO - Invoice Date bound

Approved-by: Dave Richer
2024-06-28 15:33:02 +00:00
Allan Carr
25ef4c6228 IO-2832 Purchases by RO - Invoice Date bound
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-27 18:57:43 -07:00
Dave Richer
8207a52b6b - Fix Console deprecation around default props for DND lib
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-06-27 16:51:03 -04:00
Dave Richer
8bbe7a1f0f - Cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-06-27 16:28:49 -04:00
Dave Richer
2b052b3d43 - Update feature base packages
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-06-27 15:06:32 -04:00
Dave Richer
94e7bf3f92 - Merge master into feature parent.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-06-27 14:56:29 -04:00
Allan Carr
277fbeebc8 Merged in release/2024-06-28 (pull request #1506)
Release/2024 06 28

Approved-by: Dave Richer
2024-06-26 18:09:45 +00:00
Allan Carr
64e3f5f2ac Merged in release/2024-06-28 (pull request #1505)
Release/2024 06 28
2024-06-26 16:33:28 +00:00
Allan Carr
399df78957 Merged in feature/IO-2793-QBO-State-Tax-Null (pull request #1504)
IO-2793 Insure Part Tax Type Exists
2024-06-26 16:31:40 +00:00
Allan Carr
294325343b IO-2793 Insure Part Tax Type Exists
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-26 09:32:04 -07:00
Allan Carr
ed17eec948 Merged in feature/IO-2829-Multiple-Towing-Lines (pull request #1500)
IO-2829 Multiple Towing Lines

Approved-by: Dave Richer
2024-06-26 00:51:52 +00:00
Allan Carr
f87c95079c Merged in feature/IO-2830-Bill-Line-Select-Search-Component (pull request #1501)
IO-2830 Bill Line Select Search Component

Approved-by: Dave Richer
2024-06-26 00:50:58 +00:00
Allan Carr
327149ffc9 IO-2830 Bill Line Select Search Component
remove unused variable

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-25 13:32:01 -07:00
Allan Carr
f81b21b933 IO-2829 Multiple Towing Lines
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-25 13:26:56 -07:00
Allan Carr
a559b56983 Merged in feature/IO-2520-Kaizen-Data-Pump (pull request #1499)
IO-2520 Kaizen Data Pump

Approved-by: Dave Richer
2024-06-25 19:22:55 +00:00
Allan Carr
6a9030b653 IO-2520 Kaizen Data Pump
Add in Repair Line Details, Time Ticket Details, Void Date

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-25 11:57:38 -07:00
Dave Richer
bb7f7deb50 - Package updates / Base update to clear stage prior to merging in changes
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-06-24 14:22:09 -04:00
Dave Richer
55c10890bd Merge remote-tracking branch 'origin/master-AIO' into feature/IO-2743-Production-Board-GridDND 2024-06-24 14:10:24 -04:00
Dave Richer
fc7da187f4 Merged in release/2024-06-21 (pull request #1498)
Release/2024 06 21
2024-06-21 22:47:20 +00:00
Allan Carr
edef0b194d Merged in release/2024-06-21 (pull request #1497)
IO-2820 Adjustment to Bottom Line
2024-06-21 22:33:09 +00:00
Allan Carr
f593f83ec1 Merged in feature/IO-2820-Adjust-to-Bottom-Line-State-Tax (pull request #1496)
IO-2820 Adjustment to Bottom Line
2024-06-21 22:32:03 +00:00
Allan Carr
5752f123ac IO-2820 Adjustment to Bottom Line
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-21 15:32:55 -07:00
Allan Carr
37f0c98e75 Merged in release/2024-06-21 (pull request #1495)
IO-2828 Add InstanceManager and correct delete button for delivery checklist

Approved-by: Dave Richer
2024-06-21 18:39:43 +00:00
Allan Carr
6396d68584 Merged in feature/IO-2828-Deliver-Checklist-Delete (pull request #1494)
IO-2828 Add InstanceManager and correct delete button for delivery checklist
2024-06-21 18:14:38 +00:00
Allan Carr
bfd29f25dd IO-2828 Add InstanceManager and correct delete button for delivery checklist
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-21 11:12:10 -07:00
Allan Carr
e75e35e4ee Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1493)
Feature/IO-2793 State Tax Null QBO
2024-06-20 21:39:24 +00:00
Allan Carr
48f18514af Merged in release/2024-06-21 (pull request #1492)
IO-2793 Change Additional Costs tax item
2024-06-20 21:15:15 +00:00
Allan Carr
d7ddbf7e8d IO-2793 Change Additional Costs tax item
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-20 14:15:10 -07:00
Allan Carr
1b0198af63 Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1491)
IO-2793 Change Additional Costs tax item
2024-06-20 21:14:35 +00:00
Allan Carr
ace16ba873 IO-2793 Adjustment for parts
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-20 13:46:54 -07:00
Allan Carr
76c13700be Merged in release/2024-06-21 (pull request #1490)
IO-2793 Adjustment for parts
2024-06-20 20:46:19 +00:00
Allan Carr
f57a4bd948 Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1489)
IO-2793 Adjustment for parts
2024-06-20 20:45:44 +00:00
Allan Carr
8ae8737fe2 Merged in release/2024-06-21 (pull request #1488)
Release/2024 06 21
2024-06-20 19:04:05 +00:00
Allan Carr
fef4393f9c Merged in feature/IO-2795-Delivery-Checklist-Option (pull request #1487)
IO-2795 Delivery Date required on Delivery Checklist

Approved-by: Dave Richer
2024-06-20 18:43:20 +00:00
Allan Carr
47adb6d40a IO-2795 Delivery Date required on Delivery Checklist
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-20 11:26:05 -07:00
Allan Carr
4940b10910 IO-2793 Adjustmnet for OP14
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-20 08:30:58 -07:00
Allan Carr
9c8e241ef7 Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1486)
IO-2793 Adjustmnet for OP14
2024-06-20 15:30:14 +00:00
Dave Richer
36f7b7a1a1 - Clear stage (progress update), moving branches to verify some outputs.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-06-19 14:06:39 -04:00
Allan Carr
c3d6b98c89 Merged in feature/IO-2820-Adjust-to-Bottom-Line-State-Tax (pull request #1483)
IO-2820 State Tax & Adjustment to Bottom Line

Approved-by: Dave Richer
2024-06-18 20:39:40 +00:00
Allan Carr
4b49654083 Merged in feature/IO-2814-Job-Costing-Correction (pull request #1484)
IO-2814 Job Costing Correction for OP14 Duplication

Approved-by: Dave Richer
2024-06-18 20:39:16 +00:00
Allan Carr
78223078f4 Merged in feature/IO-2816-Unsaved-Changes-Manual-Creation-of-Job (pull request #1485)
IO-2816 Unsaved Changes on Manual Job Creation

Approved-by: Dave Richer
2024-06-18 20:39:03 +00:00
Allan Carr
ca4b78d44c IO-2816 Unsaved Changes on Manual Job Creation
Correct Hardcoded button labels

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-18 13:16:58 -07:00
Dave Richer
b0a5f2d998 - Fix Horizontal mode lanes wrapping
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-06-18 15:31:56 -04:00
Dave Richer
19c980bc3b - CSS Fine tuning :(
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-06-18 15:22:06 -04:00
Dave Richer
78d6b9699b - Dependency Update Checkpoint
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-06-18 14:01:40 -04:00
Dave Richer
82d8cb22df Merge remote-tracking branch 'origin/master-AIO' into feature/IO-2743-Production-Board-GridDND 2024-06-18 13:48:21 -04:00
Allan Carr
d7bfc789e2 IO-2814 Job Costing Correction for OP14 Duplication
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-18 08:44:35 -07:00
Allan Carr
b54d0ed62a Merge branch 'master-AIO' into feature/IO-2814-Job-Costing-Correction
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-18 08:43:51 -07:00
Allan Carr
8386443cb0 IO-2820 State Tax & Adjustment to Bottom Line
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-17 10:13:36 -07:00
Dave Richer
9c41af82d7 - Progress Check
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-06-14 23:50:10 -04:00
Dave Richer
2997dd4e4d - Update the DND createStore due to it being marked for deprecation.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-06-14 13:39:51 -04:00
Dave Richer
4602648fe5 - Begin to integrate DND with trello
- Resolve all ESLint issues in DND

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-06-13 17:52:03 -04:00
Dave Richer
a8f2ca5643 - MAJOR DND PROGRESS UPDATE
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-06-12 17:27:38 -04:00
Dave Richer
7959dc67ce Merged in release/AIO/2024-06-07 (pull request #1482)
Release/AIO/2024 06 07
2024-06-11 00:38:31 +00:00
Allan Carr
0d2cdec75c IO-2793 Correction for Sublet Part Tax
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-10 14:42:21 -07:00
Allan Carr
cd055f7344 Merged in release/AIO/2024-06-07 (pull request #1481)
IO-2793 Correction for Sublet Part Tax
2024-06-10 21:42:16 +00:00
Allan Carr
a4116b6c28 Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1480)
IO-2793 Correction for Sublet Part Tax
2024-06-10 21:41:17 +00:00
Allan Carr
269ef25ece IO-2793 Better tax handling
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-10 14:14:15 -07:00
Allan Carr
c43709395f Merged in release/AIO/2024-06-07 (pull request #1479)
IO-2793 Better tax handling
2024-06-10 21:13:55 +00:00
Allan Carr
575fbd5357 Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1478)
IO-2793 Better tax handling
2024-06-10 21:13:20 +00:00
Dave Richer
10f60752c8 - Add in new DND Library (react-beautiful-dnd-grid), resides in client/src/components/trello-board/dnd/lib/index.js
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-06-10 15:54:29 -04:00
Allan Carr
870f8463db Merged in release/AIO/2024-06-07 (pull request #1477)
IO-2793 Correct passed Variable
2024-06-10 18:27:02 +00:00
Allan Carr
2ad887fb82 Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1476)
IO-2793 Correct passed Variable
2024-06-10 18:25:27 +00:00
Allan Carr
40a1a86f72 IO-2793 Correct passed Variable
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-10 11:25:06 -07:00
Allan Carr
5567a69142 Merged in release/AIO/2024-06-07 (pull request #1475)
IO-2793 Correct variables
2024-06-10 18:04:15 +00:00
Allan Carr
95d43d936c Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1474)
IO-2793 Correct variables
2024-06-10 18:03:12 +00:00
Allan Carr
9f56568680 IO-2793 Correct variables
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-10 10:58:01 -07:00
Allan Carr
762fec1d9a Merged in release/AIO/2024-06-07 (pull request #1473)
IO-2793 Change to function for better clarity
2024-06-10 17:49:59 +00:00
Allan Carr
3428940c72 Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1472)
IO-2793 Change to function for better clarity
2024-06-10 17:49:06 +00:00
Allan Carr
ea604a5e64 IO-2793 Change to function for better clarity
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-10 09:59:45 -07:00
Allan Carr
0946b0d3a3 Merged in release/AIO/2024-06-07 (pull request #1471)
IO-2793 Correct Parts Side for Taxes
2024-06-07 23:02:20 +00:00
Allan Carr
5b76473cbc IO-2793 Correct Parts Side for Taxes
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-07 15:59:51 -07:00
Allan Carr
db5359e086 Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1470)
Feature/IO-2793 State Tax Null QBO
2024-06-07 22:58:58 +00:00
Allan Carr
976dd2cbfb Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1469)
IO-2793 Add Comment to see output for testing
2024-06-07 21:31:50 +00:00
Allan Carr
35046f11c2 IO-2793 Add Comment to see output for testing
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-07 14:31:33 -07:00
Allan Carr
f4c4005a2a IO-2814 Remove Comment as api.test.romeonline.io doesn't update w/ CICD
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-07 12:16:59 -07:00
Allan Carr
b6956b3649 Merged in release/AIO/2024-06-07 (pull request #1468)
IO-2814 Remove Comment as api.test.romeonline.io doesn't update w/ CICD
2024-06-07 19:16:31 +00:00
Allan Carr
69d8d27ad3 Merged in feature/IO-2814-Job-Costing-Correction (pull request #1467)
IO-2814 Remove Comment as api.test.romeonline.io doesn't update w/ CICD
2024-06-07 19:16:01 +00:00
Dave Richer
6cf4a50a83 - Clear stage before moving to a sub-sub branch.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-06-07 15:04:36 -04:00
Allan Carr
755acd24f0 IO-2814 Correct Parts Price and add Console Log
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-07 11:40:39 -07:00
Allan Carr
b2b4902d23 Merged in release/AIO/2024-06-07 (pull request #1466)
IO-2814 Correct Parts Price and add Console Log
2024-06-07 18:40:32 +00:00
Allan Carr
079d6cfee6 Merged in feature/IO-2814-Job-Costing-Correction (pull request #1465)
IO-2814 Correct Parts Price and add Console Log
2024-06-07 18:39:43 +00:00
Allan Carr
6f69129829 Merged in release/AIO/2024-06-07 (pull request #1464)
Release/AIO/2024 06 07
2024-06-07 15:26:04 +00:00
Allan Carr
3e3b3c269a Merged in feature/IO-2814-Job-Costing-Correction (pull request #1462)
IO-2814 Job Cost Correction
2024-06-07 03:18:49 +00:00
Allan Carr
39f1af7d4b Merged in feature/IO-2793-State-Tax-Null-QBO (pull request #1463)
IO-2793 State Tax Null QBO
2024-06-07 03:18:40 +00:00
Allan Carr
1ea4d616d7 IO-2793 State Tax Null QBO
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-06 15:00:58 -07:00
Allan Carr
fdf0ecf6f6 IO-2814 Job Cost Correction
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-06-06 14:51:44 -07:00
Dave Richer
d846894d22 Merge remote-tracking branch 'origin/master-AIO' into feature/IO-2743-Production-Board 2024-06-05 12:59:19 -04:00
Allan Carr
7d3101c877 Merged in release/AIO/2024-06-07 (pull request #1461)
IO-2793 State Tax to QBO refactor

Approved-by: Dave Richer
2024-06-04 15:20:12 +00:00
Allan Carr
e5f7285253 Merge branch 'feature/IO-2793-State-Tax-Null-QBO' into release/AIO/2024-06-07
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>

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

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

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

Approved-by: Dave Richer
2024-06-01 17:16:55 +00:00
Allan Carr
885477fa68 IO-2793 State Tax Rate Null send to QBO
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-05-31 12:31:49 -07:00
Dave Richer
8f031a78c1 Merge branch 'refs/heads/master-AIO' into feature/IO-2743-Production-Board 2024-05-31 13:25:19 -04:00
Dave Richer
c7db792793 Merged in feature/IO-2796-Header-IDS (pull request #1450)
- Add Additional IDS to headers
2024-05-31 17:23:55 +00:00
Dave Richer
31be51ef79 Merged release/AIO/2024-05-31 into feature/IO-2796-Header-IDS 2024-05-31 17:23:45 +00:00
Dave Richer
e4066c1570 - Add Additional IDS to headers
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-31 13:20:54 -04:00
Dave Richer
69b36a4c34 - quick stage clear
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-31 12:17:42 -04:00
Dave Richer
c7b8df5655 - Remove unused packages
- Consume the kafika-smooth-dnd lib as a sub dir under trello
- update above code to fix any linting errors

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-31 11:31:55 -04:00
Dave Richer
d85768b2ac - Major Performance boost reducing uncessasry board renders
- Move Orientation Toggle to Board Settings -> Display
- Delete Card Settings, replaced with Board Settings

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-30 16:36:55 -04:00
Patrick Fic
ec480f2cb1 Resolve Bill Posting errors on Beta. 2024-05-29 14:08:49 -07:00
Dave Richer
a569c1f4f9 - Stability Check with test data included.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-29 16:37:55 -04:00
Patrick Fic
f5b30c9376 Resolve API Route. 2024-05-28 12:23:42 -07:00
Patrick Fic
412508fbcf Merge branch 'hotfix/AIO/2024-05-28' into master-AIO 2024-05-28 12:19:28 -07:00
Patrick Fic
dbfd9bce54 Resolve region for US intellipay. 2024-05-28 12:19:18 -07:00
Dave Richer
07a8e5b216 - Missing css class
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-28 12:05:52 -04:00
Dave Richer
38bf58c613 - Missing css class
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-28 10:20:29 -04:00
Dave Richer
ba90d72d55 - Minor front end package updates
- Fixed missing key issues in JobLifecycleComponent

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-27 13:57:48 -04:00
Dave Richer
9889bee924 - Remove unused server dependencies.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-27 13:26:15 -04:00
Dave Richer
a19e4e8f16 - Server side Patch and Minor package checkpoint
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-27 12:32:07 -04:00
Dave Richer
5121852fbc - Merge in Master-AIO
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-27 12:22:52 -04:00
Dave Richer
52e0558c79 Merged in release/AIO/2024-05-24 (pull request #1449)
Release/AIO/2024 05 24
2024-05-24 20:45:23 +00:00
Dave Richer
1d1ff5d20b Merged in release/AIO/2024-05-24 (pull request #1447)
Release/AIO/2024 05 24
2024-05-24 17:22:41 +00:00
Dave Richer
ec00697d31 - Fixed bug where Lane draggable no longer worked.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-23 16:23:47 -04:00
Dave Richer
c25714b68e - Documentation and Vertical Lane Padding
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-23 16:07:04 -04:00
Allan Carr
562d0b8641 Merged in feature/IO-2785-IO-Rescue-Header (pull request #1444)
IO-2785 AIO IO Header Rescue Link

Approved-by: Dave Richer
2024-05-23 19:47:27 +00:00
Dave Richer
dc0147c5f9 - Fix Legacy bug of 'Card Settings' button, only opening, and not toggling, the card Settings
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-23 15:20:04 -04:00
Dave Richer
296afdbeee - Optimize Production Board Card Component,
- Fix issue with production note
- Refactor shared Global styles into their own global style.

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

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

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

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

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

Approved-by: Patrick Fic
2024-04-19 18:35:50 +00:00
Patrick Fic
059b854db9 Merged in feature/IO-2677-Tasks (pull request #1430)
Update reminder interval and resolve server side.
2024-04-19 17:54:29 +00:00
Dave Richer
6fd70b165b Merged in feature/IO-2760-IDS-for-headers (pull request #1429)
- Add Additional tags (prettier also fixed some double spaced imports)
2024-04-19 17:32:04 +00:00
Dave Richer
11d94cf286 Merged in feature/IO-2760-IDS-for-headers (pull request #1428)
- Add Additional tags (prettier also fixed some double spaced imports)

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

Approved-by: Patrick Fic
2024-04-19 16:29:32 +00:00
Patrick Fic
6513432993 Merge branch 'feature/IO-2677-Tasks' into test-AIO 2024-04-18 16:28:20 -07:00
Allan Carr
132ecffc40 Merged in feature/IO-2677-Tasks (pull request #1426)
IO-2667 Employee Tasks report

Approved-by: Dave Richer
2024-04-18 20:52:31 +00:00
Patrick Fic
cdf02a8eac Merged in release/AIO/2024-04-19 (pull request #1420)
Resolve CCC supplement with UNQ_SEQ.
2024-04-18 20:49:45 +00:00
Allan Carr
45c943a78f Merged in feature/IO-2677-Tasks (pull request #1425)
IO-2667 Tasks Reports

Approved-by: Dave Richer
2024-04-18 18:25:27 +00:00
Dave Richer
25d145d864 Merged in feature/IO-2760-IDS-for-headers (pull request #1423)
Add IDs in Header

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

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

Approved-by: Patrick Fic
2024-04-18 14:47:20 +00:00
406 changed files with 32489 additions and 25645 deletions

View File

@@ -2,7 +2,7 @@ NGROK TEsting:
./ngrok.exe http http://localhost:4000 -host-header="localhost:4000"
Finding deadfiles - run from client directory
npx deadfile ./src/index.js --exclude build templates
npx deadfile ./src/index.jsx --exclude build templates
#Crushing all hasura migrations by creating a new initialization from the server.
hasura migrate create "Init" --from-server --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
@@ -11,4 +11,4 @@ Production-ImEXOnline!@#'
hasura migrate status --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
Generate the license file:
$ generate-license-file --input package.json --output third-party-licenses.txt --overwrite
$ generate-license-file --input package.json --output third-party-licenses.txt --overwrite

View File

@@ -0,0 +1,41 @@
# Production Board Notes:
## General Notes
- You can single click the lane footer to collapse/un-collapse the lane
- You can double click the lane header to collapse/un-collapse the lane
- If you need to scroll horizontally, you can hold shift and use the mouse scroll wheel, or press the mouse scroll wheel while scrolling
## Board Settings
#### Layout
- Board Orientation (Vertical or Horizontal)
- This determines the orientation of the card layout on the board.
- Horizontal is the default setting, and how the prior board was set up.
- Vertical is the new setting and allows lanes to be displayed vertically, with a grid of cards
- Card Size (Small, Medium, Large)
- This determines the size of the cards on the board.
- Small is the default setting, and how the prior board was set up.
- Medium and Large are new settings and allow for larger cards to be displayed on the board.
- Compact Cards (Tall or Wide)
- Formally called 'Compact'
- When on, data is displayed on the card vertically
- when turned off, some fields may share horizontal space, tightening the card layout
- Colored Cards (On or Off)
- When on, cards are colored based on the Status color
- Kiosk Mode (On or Off)
- This should be turned on if the shop is using it on a tablet (Ipad)
#### Information
These allow users to turn fields on or off, turning them all off will show the card in the most minimal form
### Statistics
- The statistics section allows users to see accumulations of both jobs on the board, and jobs in production.
- you can click a statistic to turn it on and off, and drag and drop the statistics to rearrange them
### Filters
- Allows you to set, and persist filters for estimators and insurance companies

View File

@@ -4,7 +4,7 @@ Clone Repository for:
{
"name": "node-webhook-scripts",
"version": "1.0.0",
"main": "index.js",
"main": "index.jsx",
"dependencies": {
"express": "^4.16.4"
},

View File

@@ -11,7 +11,7 @@ module.exports = {
{
name: "Bitbucket Webhook",
script: "./webhook/index.js",
script: "./webhook/index.jsx",
env: {
NODE_ENV: "production"
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
// This example plugins/index.jsx can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.

View File

@@ -1,5 +1,5 @@
// ***********************************************************
// This example support/index.js is processed and
// This example support/index.jsx is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and

View File

@@ -1,174 +1,177 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
<link rel="icon" href="/favicon.png" />
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
<link rel="icon" href="/ro-favicon.png" />
<% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %>
<link rel="icon" href="/pm/pm-favicon.ico" />
<% } %>
<head>
<meta charset="utf-8"/>
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
<link rel="icon" href="/favicon.png"/>
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
<link rel="icon" href="/ro-favicon.png"/>
<% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %>
<link rel="icon" href="/pm/pm-favicon.ico"/>
<% } %>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#1690ff" />
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
<!-- TODO:AIo Update the individual logos for each.-->
<link rel="apple-touch-icon" href="public/logo192.png" />
<link rel="mask-icon" href="/mask-icon.svg" color="#FFFFFF">
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="theme-color" content="#1690ff"/>
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
<!-- TODO:AIo Update the individual logos for each.-->
<link rel="apple-touch-icon" href="public/logo192.png"/>
<link rel="mask-icon" href="/mask-icon.svg" color="#FFFFFF">
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
<meta name="description" content="ImEX Online" />
<title>ImEX Online</title>
<script type="text/javascript">
window.$crisp = [];
window.CRISP_WEBSITE_ID = '36724f62-2eb0-4b29-9cdd-9905fb99913e';
(function () {
d = document;
s = d.createElement('script');
s.src = 'https://client.crisp.chat/l.js';
s.async = 1;
d.getElementsByTagName('head')[0].appendChild(s);
})();
</script>
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
<meta name="description" content="Rome Online" />
<title>Rome Online</title>
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
<meta name="description" content="ImEX Online"/>
<title>ImEX Online</title>
<script type="text/javascript">
window.$crisp = [];
window.CRISP_WEBSITE_ID = '36724f62-2eb0-4b29-9cdd-9905fb99913e';
(function () {
d = document;
s = d.createElement('script');
s.src = 'https://client.crisp.chat/l.js';
s.async = 1;
d.getElementsByTagName('head')[0].appendChild(s);
})();
</script>
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
<meta name="description" content="Rome Online"/>
<title>Rome Online</title>
<!--Use the below code snippet to provide real time updates to the live chat plugin without the need of copying and paste each time to your website when changes are made via PBX-->
<!--Use the below code snippet to provide real time updates to the live chat plugin without the need of copying and paste each time to your website when changes are made via PBX-->
<call-us-selector phonesystem-url=https://rometech.east.3cx.us:5001 party="LiveChat528346"></call-us-selector>
<call-us-selector phonesystem-url=https://rometech.east.3cx.us:5001
party="LiveChat528346"></call-us-selector>
<!--Incase you don't want real time updates to the live chat plugin when options are changed, use the below code snippet. Please note that each time you change the settings you will need to copy and paste the snippet code to your website-->
<!--<call-us
phonesystem-url=https://rometech.east.3cx.us:5001
style="position:fixed;font-size:16px;line-height:17px;z-index: 99999;right: 20px; bottom: 20px;"
id="wp-live-chat-by-3CX"
minimized="true"
animation-style="noanimation"
party="LiveChat528346"
minimized-style="bubbleright"
allow-call="true"
allow-video="false"
allow-soundnotifications="true"
enable-mute="true"
enable-onmobile="true"
offline-enabled="true"
enable="true"
ignore-queueownership="false"
authentication="both"
show-operator-actual-name="true"
aknowledge-received="true"
gdpr-enabled="false"
message-userinfo-format="name"
message-dateformat="both"
lang="browser"
button-icon-type="default"
greeting-visibility="none"
greeting-offline-visibility="none"
chat-delay="2000"
enable-direct-call="true"
enable-ga="false"
></call-us>-->
<script defer src=https://downloads-global.3cx.com/downloads/livechatandtalk/v1/callus.js id="tcx-callus-js" charset="utf-8"></script>
<!--Incase you don't want real time updates to the live chat plugin when options are changed, use the below code snippet. Please note that each time you change the settings you will need to copy and paste the snippet code to your website-->
<!--<call-us
phonesystem-url=https://rometech.east.3cx.us:5001
style="position:fixed;font-size:16px;line-height:17px;z-index: 99999;right: 20px; bottom: 20px;"
id="wp-live-chat-by-3CX"
minimized="true"
animation-style="noanimation"
party="LiveChat528346"
minimized-style="bubbleright"
allow-call="true"
allow-video="false"
allow-soundnotifications="true"
enable-mute="true"
enable-onmobile="true"
offline-enabled="true"
enable="true"
ignore-queueownership="false"
authentication="both"
show-operator-actual-name="true"
aknowledge-received="true"
gdpr-enabled="false"
message-userinfo-format="name"
message-dateformat="both"
lang="browser"
button-icon-type="default"
greeting-visibility="none"
greeting-offline-visibility="none"
chat-delay="2000"
enable-direct-call="true"
enable-ga="false"
></call-us>-->
<script defer src=https://downloads-global.3cx.com/downloads/livechatandtalk/v1/callus.js
id="tcx-callus-js" charset="utf-8"></script>
<% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %>
<title>ProManager</title>
<meta name="description" content="ProManager"/>
<% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %>
<title>ProManager</title>
<meta name="description" content="ProManager" />
<% } %>
<script>
!(function () {
'use strict';
var e = [
'debug',
'destroy',
'do',
'help',
'identify',
'is',
'off',
'on',
'ready',
'render',
'reset',
'safe',
'set',
];
if (window.noticeable) console.warn('Noticeable SDK code snippet loaded more than once');
else {
var n = (window.noticeable = window.noticeable || []);
<% } %>
<script>
!(function () {
'use strict';
var e = [
'debug',
'destroy',
'do',
'help',
'identify',
'is',
'off',
'on',
'ready',
'render',
'reset',
'safe',
'set',
];
if (window.noticeable) console.warn('Noticeable SDK code snippet loaded more than once');
else {
var n = (window.noticeable = window.noticeable || []);
function t(e) {
return function () {
var t = Array.prototype.slice.call(arguments);
return t.unshift(e), n.push(t), n;
};
}
!(function () {
for (var o = 0; o < e.length; o++) {
var r = e[o];
n[r] = t(r);
}
})(),
(function () {
var e = document.createElement('script');
(e.async = !0), (e.src = 'https://sdk.noticeable.io/l.js');
var n = document.head;
n.insertBefore(e, n.firstChild);
})();
function t(e) {
return function () {
var t = Array.prototype.slice.call(arguments);
return t.unshift(e), n.push(t), n;
};
}
})();
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="src/index.jsx"></script>
</body>
!(function () {
for (var o = 0; o < e.length; o++) {
var r = e[o];
n[r] = t(r);
}
})(),
(function () {
var e = document.createElement('script');
(e.async = !0), (e.src = 'https://sdk.noticeable.io/l.js');
var n = document.head;
n.insertBefore(e, n.firstChild);
})();
}
})();
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="src/index.jsx"></script>
</body>
</html>

22629
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,90 +2,91 @@
"name": "bodyshop",
"version": "0.2.1",
"engines": {
"node": "18.18.2"
"node": ">=18.18.2"
},
"type": "module",
"private": true,
"proxy": "http://localhost:4000",
"dependencies": {
"@ant-design/compatible": "^5.1.2",
"@ant-design/pro-layout": "^7.17.16",
"@apollo/client": "^3.8.10",
"@asseinfo/react-kanban": "^2.2.0",
"@fingerprintjs/fingerprintjs": "^4.2.2",
"@ant-design/pro-layout": "^7.19.12",
"@apollo/client": "^3.11.4",
"@emotion/is-prop-valid": "^1.3.0",
"@fingerprintjs/fingerprintjs": "^4.4.3",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.2.1",
"@sentry/cli": "^2.28.6",
"@sentry/react": "^7.104.0",
"@splitsoftware/splitio-react": "^1.11.0",
"@reduxjs/toolkit": "^2.2.7",
"@sentry/cli": "^2.33.1",
"@sentry/react": "^7.114.0",
"@splitsoftware/splitio-react": "^1.12.1",
"@tanem/react-nprogress": "^5.0.51",
"@vitejs/plugin-react": "^4.2.1",
"antd": "^5.15.3",
"@vitejs/plugin-react": "^4.3.1",
"antd": "^5.20.2",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0",
"axios": "^1.6.7",
"dayjs": "^1.11.10",
"autosize": "^6.0.1",
"axios": "^1.7.4",
"classnames": "^2.5.1",
"css-box-model": "^1.2.1",
"dayjs": "^1.11.12",
"dayjs-business-days2": "^1.2.2",
"dinero.js": "^1.9.1",
"dotenv": "^16.4.5",
"env-cmd": "^10.1.0",
"exifr": "^7.1.3",
"firebase": "^10.8.1",
"graphql": "^16.6.0",
"i18next": "^23.10.0",
"i18next-browser-languagedetector": "^7.0.2",
"libphonenumber-js": "^1.10.57",
"logrocket": "^8.0.1",
"markerjs2": "^2.32.0",
"normalize-url": "^8.0.0",
"firebase": "^10.13.0",
"graphql": "^16.9.0",
"i18next": "^23.14.0",
"i18next-browser-languagedetector": "^8.0.0",
"immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.11.7",
"logrocket": "^8.1.2",
"markerjs2": "^2.32.1",
"memoize-one": "^6.0.0",
"normalize-url": "^8.0.1",
"object-hash": "^3.0.0",
"prop-types": "^15.8.1",
"query-string": "^9.0.0",
"react": "^18.2.0",
"react-big-calendar": "^1.11.0",
"query-string": "^9.1.0",
"raf-schd": "^4.0.3",
"react": "^18.3.1",
"react-big-calendar": "^1.13.3",
"react-color": "^2.19.3",
"react-cookie": "^7.1.0",
"react-dom": "^18.2.0",
"react-cookie": "^7.2.0",
"react-dom": "^18.3.1",
"react-drag-listview": "^2.0.0",
"react-grid-gallery": "^1.0.0",
"react-grid-gallery": "^1.0.1",
"react-grid-layout": "1.3.4",
"react-i18next": "^14.0.5",
"react-icons": "^5.0.1",
"react-i18next": "^14.1.3",
"react-icons": "^5.3.0",
"react-image-lightbox": "^5.1.4",
"react-joyride": "^2.7.4",
"react-markdown": "^9.0.1",
"react-number-format": "^5.3.3",
"react-product-fruits": "^2.2.6",
"react-redux": "^9.1.0",
"react-number-format": "^5.4.0",
"react-popopo": "^2.1.9",
"react-product-fruits": "^2.2.61",
"react-redux": "^9.1.2",
"react-resizable": "^3.0.5",
"react-router-dom": "^6.22.2",
"react-scripts": "^5.0.1",
"react-router-dom": "^6.26.1",
"react-sticky": "^6.0.3",
"react-virtualized": "^9.22.5",
"recharts": "^2.12.2",
"react-virtuoso": "^4.10.1",
"recharts": "^2.12.7",
"redux": "^5.0.1",
"redux-actions": "^3.0.3",
"redux-persist": "^6.0.0",
"redux-saga": "^1.3.0",
"redux-state-sync": "^3.1.4",
"reselect": "^5.1.0",
"sass": "^1.71.1",
"socket.io-client": "^4.7.4",
"styled-components": "^6.1.8",
"reselect": "^5.1.1",
"sass": "^1.77.8",
"socket.io-client": "^4.7.5",
"styled-components": "^6.1.12",
"subscriptions-transport-ws": "^0.11.0",
"terser-webpack-plugin": "^5.3.10",
"userpilot": "^1.3.1",
"use-memo-one": "^1.1.3",
"userpilot": "^1.3.5",
"vite-plugin-ejs": "^1.7.0",
"web-vitals": "^3.5.2",
"workbox-core": "^7.0.0",
"workbox-expiration": "^7.0.0",
"workbox-navigation-preload": "^7.0.0",
"workbox-precaching": "^7.0.0",
"workbox-routing": "^7.0.0",
"workbox-strategies": "^7.0.0"
"web-vitals": "^3.5.2"
},
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "vite",
"build": "vite build",
"build": "dotenvx run --env-file=.env.development.imex -- vite build",
"start:imex": "dotenvx run --env-file=.env.development.imex -- vite",
"start:rome": "dotenvx run --env-file=.env.development.rome -- vite",
"start:promanager": "dotenvx run --env-file=.env.development.promanager -- vite",
@@ -128,32 +129,30 @@
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-react": "^7.23.3",
"@dotenvx/dotenvx": "^0.15.4",
"@emotion/babel-plugin": "^11.11.0",
"@emotion/react": "^11.11.3",
"@sentry/webpack-plugin": "^2.14.2",
"@swc/core": "^1.3.107",
"@swc/plugin-styled-components": "^1.5.108",
"@testing-library/cypress": "^10.0.1",
"browserslist": "^4.22.3",
"@babel/preset-react": "^7.24.7",
"@dotenvx/dotenvx": "^1.7.0",
"@emotion/babel-plugin": "^11.12.0",
"@emotion/react": "^11.13.0",
"@sentry/webpack-plugin": "^2.22.2",
"@testing-library/cypress": "^10.0.2",
"browserslist": "^4.23.3",
"browserslist-to-esbuild": "^2.1.1",
"cross-env": "^7.0.3",
"cypress": "^13.6.6",
"cypress": "^13.13.3",
"eslint": "^8.57.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-cypress": "^2.15.1",
"memfs": "^4.6.0",
"memfs": "^4.11.1",
"os-browserify": "^0.3.0",
"react-error-overlay": "6.0.11",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.3",
"vite": "^5.0.11",
"vite": "^5.4.1",
"vite-plugin-babel": "^1.2.0",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-legacy": "^2.1.0",
"vite-plugin-node-polyfills": "^0.19.0",
"vite-plugin-pwa": "^0.19.0",
"vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-pwa": "^0.20.1",
"vite-plugin-style-import": "^2.0.0"
}
}

View File

@@ -16422,7 +16422,7 @@ For when you don't want to write the same thing over and over to cache a method
$ npm install --save-dev stubs
```
```js
var mylib = require('./lib/index.js')
var mylib = require('./lib/index.jsx')
var stubs = require('stubs')
// make it a noop

View File

@@ -16567,7 +16567,7 @@ even more slower.
## Benchmarks
```bash
$ node benchmarks/index.js
$ node benchmarks/index.jsx
Benchmarking: sign
elliptic#sign x 262 ops/sec ±0.51% (177 runs sampled)
eccjs#sign x 55.91 ops/sec ±0.90% (144 runs sampled)

View File

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

View File

@@ -7,9 +7,7 @@ import { connect } from "react-redux";
import { Route, Routes } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import DocumentEditorContainer from "../components/document-editor/document-editor.container";
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
//Component Imports
import ErrorBoundary from "../components/error-boundary/error-boundary.component"; // Component Imports
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
import DisclaimerPage from "../pages/disclaimer/disclaimer.page";
import LandingPage from "../pages/landing/landing.page";
@@ -20,23 +18,24 @@ import { checkUserSession } from "../redux/user/user.actions";
import { selectBodyshop, selectCurrentEula, selectCurrentUser } from "../redux/user/user.selectors";
import PrivateRoute from "../components/PrivateRoute";
import "./App.styles.scss";
import handleBeta from "../utils/betaHandler";
import handleBeta from "../utils/handleBeta";
import Eula from "../components/eula/eula.component";
import InstanceRenderMgr from "../utils/instanceRenderMgr";
import { ProductFruits } from "react-product-fruits";
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
const MobilePaymentContainer = lazy(() => import("../pages/mobile-payment/mobile-payment.container"));
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
online: selectOnline,
bodyshop: selectBodyshop,
currentEula: selectCurrentEula
});
const mapDispatchToProps = (dispatch) => ({
checkUserSession: () => dispatch(checkUserSession()),
setOnline: (isOnline) => dispatch(setOnline(isOnline))
@@ -60,11 +59,11 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
// Associate event listeners, memoize to prevent multiple listeners being added
useEffect(() => {
const offlineListener = (e) => {
const offlineListener = () => {
setOnline(false);
};
const onlineListener = (e) => {
const onlineListener = () => {
setOnline(true);
};
@@ -98,7 +97,7 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
InstanceRenderMgr({
imex: "gvfvfw/bodyshopapp",
rome: "rome-online/rome-online",
promanager: "" //TODO:AIO Add in log rocket for promanager instances.
promanager: "" // TODO: AIO Add in log rocket for promanager instances.
})
);
}
@@ -111,24 +110,20 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
handleBeta();
if (!online)
if (!online) {
return (
<Result
status="warning"
title={t("general.labels.nointernet")}
subTitle={t("general.labels.nointernet_sub")}
extra={
<Button
type="primary"
onClick={() => {
window.location.reload();
}}
>
<Button type="primary" onClick={() => window.location.reload()}>
{t("general.actions.refresh")}
</Button>
}
/>
);
}
if (currentEula && !currentUser.eulaIsAccepted) {
return <Eula />;
@@ -147,18 +142,13 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
/>
}
>
<ProductFruits
<ProductFruitsWrapper
currentUser={currentUser}
workspaceCode={InstanceRenderMgr({
imex: null,
rome: null,
rome: "9BkbEseqNqxw8jUH",
promanager: "aoJoEifvezYI0Z0P"
})}
debug
language="en"
user={{
email: currentUser.email,
username: currentUser.email
}}
/>
<Routes>

View File

@@ -161,3 +161,15 @@
.rowWithColor > td {
background-color: var(--bgColor) !important;
}
.muted-button {
color: lightgray;
border: none;
background: none;
cursor: pointer;
font-size: 16px; /* Adjust as needed */
}
.muted-button:hover {
color: darkgrey;
}

View File

@@ -0,0 +1,32 @@
import React from "react";
import { ProductFruits } from "react-product-fruits";
import PropTypes from "prop-types";
const ProductFruitsWrapper = React.memo(({ currentUser, workspaceCode }) => {
return (
workspaceCode &&
currentUser?.authorized === true &&
currentUser?.email && (
<ProductFruits
lifeCycle="unmount"
workspaceCode={workspaceCode}
debug
language="en"
user={{
email: currentUser.email,
username: currentUser.email
}}
/>
)
);
});
export default ProductFruitsWrapper;
ProductFruitsWrapper.propTypes = {
currentUser: PropTypes.shape({
authorized: PropTypes.bool,
email: PropTypes.string
}),
workspaceCode: PropTypes.string
};

View File

@@ -13,6 +13,7 @@ import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateFormatter } from "../../utils/DateFormatter";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import JobMarkSelectedExported from "../jobs-mark-selected-exported/jobs-mark-selected-exported";
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
@@ -170,13 +171,22 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
extra={
<Space wrap>
{!bodyshop.cdk_dealerid && !bodyshop.pbs_serialnumber && (
<JobsExportAllButton
jobIds={selectedJobs}
disabled={transInProgress || selectedJobs.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedJobs}
refetch={refetch}
/>
<>
<JobMarkSelectedExported
jobIds={selectedJobs}
disabled={transInProgress || selectedJobs.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedJobs}
refetch={refetch}
/>
<JobsExportAllButton
jobIds={selectedJobs}
disabled={transInProgress || selectedJobs.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedJobs}
refetch={refetch}
/>
</>
)}
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
<Input.Search

View File

@@ -1,6 +1,6 @@
import { PageHeader } from "@ant-design/pro-layout";
import { useMutation, useQuery } from "@apollo/client";
import { Button, Divider, Form, Popconfirm, Space } from "antd";
import dayjs from "../../utils/day";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -13,6 +13,7 @@ import { insertAuditTrail } from "../../redux/application/application.actions";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import dayjs from "../../utils/day";
import AlertComponent from "../alert/alert.component";
import BillFormContainer from "../bill-form/bill-form.container";
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
@@ -22,7 +23,6 @@ import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-galler
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import BillDetailEditReturn from "./bill-detail-edit-return.component";
import { PageHeader } from "@ant-design/pro-layout";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -153,6 +153,7 @@ export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail
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 (
<>
@@ -188,7 +189,7 @@ export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail
}
/>
<Form form={form} onFinish={handleFinish} 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

View File

@@ -41,7 +41,8 @@ export function BillFormComponent({
job,
loadOutstandingReturns,
loadInventory,
preferredMake
preferredMake,
disableInHouse
}) {
const { t } = useTranslation();
const client = useApolloClient();
@@ -177,7 +178,7 @@ export function BillFormComponent({
]}
>
<VendorSearchSelect
disabled={disabled}
disabled={disabled || disableInHouse}
options={vendorAutoCompleteOptions}
preferredMake={preferredMake}
onSelect={handleVendorSelect}
@@ -243,7 +244,7 @@ export function BillFormComponent({
})
]}
>
<Input disabled={disabled || disableInvNumber} />
<Input disabled={disabled || disableInvNumber || disableInHouse} />
</Form.Item>
<Form.Item
label={t("bills.fields.date")}

View File

@@ -16,7 +16,7 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber }) {
export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber, disableInHouse }) {
const {
treatments: { Simple_Inventory }
} = useSplitTreatments({
@@ -47,6 +47,7 @@ export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableI
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

@@ -3,8 +3,6 @@ import React, { forwardRef } from "react";
import { useTranslation } from "react-i18next";
import InstanceRenderMgr from "../../utils/instanceRenderMgr";
//To be used as a form element only.
const { Option } = Select;
const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps }, ref) => {
const { t } = useTranslation();
@@ -25,31 +23,27 @@ const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps },
);
}}
notFoundContent={"Removed."}
{...restProps}
>
<Select.Option key={null} value={"noline"} cost={0} line_desc={""}>
{t("billlines.labels.other")}
</Select.Option>
{options
? options.map((item) => (
<Option
disabled={allowRemoved ? false : item.removed}
key={item.id}
value={item.id}
cost={item.act_price ? item.act_price : 0}
part_type={item.part_type}
line_desc={item.line_desc}
part_qty={item.part_qty}
oem_partno={item.oem_partno}
alt_partno={item.alt_partno}
act_price={item.act_price}
style={{
...(item.removed ? { textDecoration: "line-through" } : {})
}}
name={`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
item.oem_partno ? ` - ${item.oem_partno}` : ""
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()}
>
options={[
{ value: "noline", label: t("billlines.labels.other"), name: t("billlines.labels.other") },
...options.map((item) => ({
disabled: allowRemoved ? false : item.removed,
key: item.id,
value: item.id,
cost: item.act_price ? item.act_price : 0,
part_type: item.part_type,
line_desc: item.line_desc,
part_qty: item.part_qty,
oem_partno: item.oem_partno,
alt_partno: item.alt_partno,
act_price: item.act_price,
style: {
...(item.removed ? { textDecoration: "line-through" } : {})
},
name: `${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
item.oem_partno ? ` - ${item.oem_partno}` : ""
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim(),
label: (
<>
<span>
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
item.oem_partno ? ` - ${item.oem_partno}` : ""
@@ -60,14 +54,15 @@ const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps },
<span style={{ float: "right", paddingleft: "1rem" }}>{`${item.mod_lb_hrs} units`}</span>
)
})}
<span style={{ float: "right", paddingleft: "1rem" }}>
{item.act_price ? `$${item.act_price && item.act_price.toFixed(2)}` : ``}
</span>
</Option>
))
: null}
</Select>
</>
)
}))
]}
{...restProps}
></Select>
);
};
export default forwardRef(BillLineSearchSelect);

View File

@@ -1,14 +1,12 @@
import { DeleteFilled } from "@ant-design/icons";
import { useLazyQuery, useMutation } from "@apollo/client";
import { Button, Card, Col, Form, Input, notification, Row, Space, Spin, Statistic } from "antd";
import { Button, Card, Col, Form, Input, Row, Space, Spin, Statistic, notification } from "antd";
import axios from "axios";
import dayjs from "../../utils/day";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_PAYMENT_RESPONSE, QUERY_RO_AND_OWNER_BY_JOB_PKS } from "../../graphql/payment_response.queries";
import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectCardPayment } from "../../redux/modals/modals.selectors";
@@ -28,12 +26,12 @@ const mapDispatchToProps = (dispatch) => ({
});
const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, 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();
@@ -42,7 +40,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
skip: true
});
console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data);
//Initialize the intellipay window.
const SetIntellipayCallbackFunctions = () => {
console.log("*** Set IntelliPay callback functions.");
@@ -51,16 +48,20 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
});
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) => ({
@@ -85,50 +86,9 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
});
};
const handleFinish = async (values) => {
try {
await insertPayment({
variables: {
paymentInput: values.payments.map((payment) => ({
amount: payment.amount,
transactionid: (values.paymentResponse.paymentid || "").toString(),
payer: t("payments.labels.customer"),
type: values.paymentResponse.cardbrand,
jobid: payment.jobid,
date: dayjs(Date.now()),
payment_responses: {
data: [
{
amount: payment.amount,
bodyshopid: bodyshop.id,
jobid: payment.jobid,
declinereason: values.paymentResponse.declinereason,
ext_paymentid: values.paymentResponse.paymentid.toString(),
successful: true,
response: values.paymentResponse
}
]
}
}))
},
refetchQueries: ["GET_JOB_BY_PK"]
});
toggleModalVisible();
} catch (error) {
console.error(error);
notification.open({
type: "error",
message: t("payments.errors.inserting", { error: error.message })
});
} finally {
setLoading(false);
}
};
const handleIntelliPayCharge = async () => {
setLoading(true);
//Validate
try {
await form.validateFields();
@@ -140,7 +100,8 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
try {
const response = await axios.post("/intellipay/lightbox_credentials", {
bodyshop,
refresh: !!window.intellipay
refresh: !!window.intellipay,
paymentSplitMeta: form.getFieldsValue(),
});
if (window.intellipay) {
@@ -169,7 +130,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
<Card title="Card Payment">
<Spin spinning={loading}>
<Form
onFinish={handleFinish}
form={form}
layout="vertical"
initialValues={{
@@ -246,18 +206,14 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
}
>
{() => {
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.");
if (
payments?.length > 0 &&
payments?.filter((p) => p?.jobid).length === payments?.length
) {
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
@@ -300,6 +256,13 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
type="hidden"
value={totalAmountToCharge?.toFixed(2)}
/>
<Input
className="ipayfield"
data-ipayname="comment"
type="hidden"
value={btoa(JSON.stringify(payments))}
hidden
/>
<Button
type="primary"
// data-ipayname="submit"
@@ -314,11 +277,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
);
}}
</Form.Item>
{/* Lightbox payment response when it is completed */}
<Form.Item name="paymentResponse" hidden>
<Input type="hidden" />
</Form.Item>
</Form>
</Spin>
</Card>

View File

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

View File

@@ -61,7 +61,11 @@ 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) => record.status === value,
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,

View File

@@ -9,7 +9,6 @@ import axios from "axios";
const fortyFiveDaysAgo = () => dayjs().subtract(45, "day").toLocaleString();
export default function JobLifecycleDashboardComponent({ data, bodyshop, ...cardProps }) {
console.log("🚀 ~ JobLifecycleDashboardComponent ~ bodyshop:", bodyshop);
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [lifecycleData, setLifecycleData] = useState(null);
@@ -143,7 +142,7 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
>
<div>
{lifecycleData.summations.map((key) => (
<Tag color={key.color} style={{ width: "13vh", padding: "4px", margin: "4px" }}>
<Tag key={key.status} 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}`}
@@ -165,6 +164,7 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
size="small"
pagination={false}
columns={columns}
rowKey={(record) => record.status}
dataSource={lifecycleData.summations.sort((a, b) => b.value - a.value).slice(0, 3)}
/>
</Card>

View File

@@ -36,9 +36,6 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
};
});
console.log("Scheduled Out Today");
console.dir(scheduledOutToday);
const tvFontSize = 18;
const tvFontWeight = "bold";
@@ -89,8 +86,6 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
console.log("Render record out today");
console.dir(record);
return record.ownerid ? (
<Link to={"/manage/owners/" + record.ownerid} onClick={(e) => e.stopPropagation()}>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>

View File

@@ -14,7 +14,6 @@ import {
Typography
} from "antd";
import Dinero from "dinero.js";
import dayjs from "../../utils/day";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -22,6 +21,7 @@ import { createStructuredSelector } from "reselect";
import { determineDmsType } from "../../pages/dms/dms.container";
import { selectBodyshop } from "../../redux/user/user.selectors";
import i18n from "../../translations/i18n";
import dayjs from "../../utils/day";
import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component";
import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
@@ -89,7 +89,7 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
job.area_of_damage && job.area_of_damage.impact1
? " " +
t("jobs.labels.dms.damageto", {
area_of_damage: (job.area_of_damage && job.area_of_damage.impact1) || "UNKNOWN"
area_of_damage: (job.area_of_damage && job.area_of_damage.impact1.padStart(2, "0")) || "UNKNOWN"
})
: ""
}`.slice(0, 239),

View File

@@ -3,14 +3,13 @@ import axios from "axios";
import _ from "lodash";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useNavigate } 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 VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
export default function GlobalSearchOs() {
const { t } = useTranslation();
const history = useNavigate();
const [loading, setLoading] = useState(false);
const [data, setData] = useState(false);
@@ -178,15 +177,7 @@ export default function GlobalSearchOs() {
};
return (
<AutoComplete
options={data}
onSearch={handleSearch}
defaultActiveFirstOption
onSelect={(val, opt) => {
history(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,7 +3,7 @@ import { AutoComplete, Divider, Input, Space } from "antd";
import _ from "lodash";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom";
import { Link } from "react-router-dom";
import { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import AlertComponent from "../alert/alert.component";
@@ -12,7 +12,6 @@ import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.compon
export default function GlobalSearch() {
const { t } = useTranslation();
const history = useNavigate();
const [callSearch, { loading, error, data }] = useLazyQuery(GLOBAL_SEARCH_QUERY);
const executeSearch = (v) => {
@@ -157,14 +156,7 @@ export default function GlobalSearch() {
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<AutoComplete
options={options}
onSearch={handleSearch}
defaultActiveFirstOption
onSelect={(val, opt) => {
history(opt.label.props.to);
}}
>
<AutoComplete options={options} onSearch={handleSearch} defaultActiveFirstOption>
<Input.Search
size="large"
placeholder={t("general.labels.globalsearch")}

View File

@@ -32,6 +32,7 @@ import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { BsKanban } from "react-icons/bs";
import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar, FaTasks } from "react-icons/fa";
import { FiLogOut } from "react-icons/fi";
import { GiPayMoney, GiPlayerTime, GiSettingsKnobs } from "react-icons/gi";
import { IoBusinessOutline } from "react-icons/io5";
import { RiSurveyLine } from "react-icons/ri";
@@ -42,8 +43,7 @@ import { selectRecentItems, selectSelectedHeader } from "../../redux/application
import { setModalContext } from "../../redux/modals/modals.actions";
import { signOutStart } from "../../redux/user/user.actions";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import { FiLogOut } from "react-icons/fi";
import { checkBeta, handleBeta, setBeta } from "../../utils/betaHandler";
import { checkBeta, handleBeta, setBeta } from "../../utils/handleBeta";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
@@ -141,11 +141,13 @@ function Header({
accountingChildren.push(
{
key: "bills",
id: "header-accounting-bills",
icon: <Icon component={FaFileInvoiceDollar} />,
label: <Link to="/manage/bills">{t("menus.header.bills")}</Link>
},
{
key: "enterbills",
id: "header-accounting-enterbills",
icon: <Icon component={GiPayMoney} />,
label: t("menus.header.enterbills"),
onClick: () => {
@@ -165,6 +167,7 @@ function Header({
},
{
key: "inventory",
id: "header-accounting-inventory",
icon: <Icon component={FaFileInvoiceDollar} />,
label: <Link to="/manage/inventory">{t("menus.header.inventory")}</Link>
}
@@ -183,11 +186,13 @@ function Header({
},
{
key: "allpayments",
id: "header-accounting-allpayments",
icon: <BankFilled />,
label: <Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
},
{
key: "enterpayments",
id: "header-accounting-enterpayments",
icon: <Icon component={FaCreditCard} />,
label: t("menus.header.enterpayment"),
onClick: () => {
@@ -203,6 +208,7 @@ function Header({
if (ImEXPay.treatment === "on") {
accountingChildren.push({
key: "entercardpayments",
id: "header-accounting-entercardpayments",
icon: <Icon component={FaCreditCard} />,
label: t("menus.header.entercardpayment"),
onClick: () => {
@@ -227,6 +233,7 @@ function Header({
},
{
key: "timetickets",
id: "header-accounting-timetickets",
icon: <FieldTimeOutlined />,
label: <Link to="/manage/timetickets">{t("menus.header.timetickets")}</Link>
}
@@ -235,6 +242,7 @@ function Header({
if (bodyshop?.md_tasks_presets?.use_approvals) {
accountingChildren.push({
key: "ttapprovals",
id: "header-accounting-ttapprovals",
icon: <FieldTimeOutlined />,
label: <Link to="/manage/ttapprovals">{t("menus.header.ttapprovals")}</Link>
});
@@ -244,6 +252,7 @@ function Header({
key: "entertimetickets",
icon: <Icon component={GiPlayerTime} />,
label: t("menus.header.entertimeticket"),
id: "header-accounting-entertimetickets",
onClick: () => {
setTimeTicketContext({
actions: {},
@@ -264,6 +273,7 @@ function Header({
const accountingExportChildren = [
{
key: "receivables",
id: "header-accounting-receivables",
label: <Link to="/manage/accounting/receivables">{t("menus.header.accounting-receivables")}</Link>
}
];
@@ -271,6 +281,7 @@ function Header({
if (!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) || DmsAp.treatment === "on") {
accountingExportChildren.push({
key: "payables",
id: "header-accounting-payables",
label: <Link to="/manage/accounting/payables">{t("menus.header.accounting-payables")}</Link>
});
}
@@ -278,6 +289,7 @@ function Header({
if (!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber))) {
accountingExportChildren.push({
key: "payments",
id: "header-accounting-payments",
label: <Link to="/manage/accounting/payments">{t("menus.header.accounting-payments")}</Link>
});
}
@@ -288,6 +300,7 @@ function Header({
},
{
key: "exportlogs",
id: "header-accounting-exportlogs",
label: <Link to="/manage/accounting/exportlogs">{t("menus.header.export-logs")}</Link>
}
);
@@ -301,6 +314,7 @@ function Header({
) {
accountingChildren.push({
key: "accountingexport",
id: "header-accounting-export",
icon: <ExportOutlined />,
label: t("menus.header.export"),
children: accountingExportChildren
@@ -311,10 +325,12 @@ function Header({
{
key: "home",
icon: <HomeFilled />,
id: "header-home",
label: <Link to="/manage/">{t("menus.header.home")}</Link>
},
{
key: "schedule",
id: "header-schedule",
icon: <Icon component={FaCalendarAlt} />,
label: <Link to="/manage/schedule">{t("menus.header.schedule")}</Link>
},
@@ -326,16 +342,19 @@ function Header({
children: [
{
key: "activejobs",
id: "header-active-jobs",
icon: <FileFilled />,
label: <Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
},
{
key: "readyjobs",
id: "header-ready-jobs",
icon: <CheckCircleOutlined />,
label: <Link to="/manage/jobs/ready">{t("menus.header.readyjobs")}</Link>
},
{
key: "parts-queue",
id: "header-parts-queue",
icon: <ToolFilled />,
label: <Link to="/manage/partsqueue">{t("menus.header.parts-queue")}</Link>
},
@@ -347,6 +366,7 @@ function Header({
},
{
key: "newjob",
id: "header-new-job",
icon: <FileAddOutlined />,
label: <Link to="/manage/jobs/new">{t("menus.header.newjob")}</Link>
},
@@ -355,6 +375,7 @@ function Header({
},
{
key: "alljobs",
id: "header-all-jobs",
icon: <UnorderedListOutlined />,
label: <Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link>
},
@@ -363,6 +384,7 @@ function Header({
},
{
key: "productionlist",
id: "header-production-list",
icon: <ScheduleOutlined />,
label: <Link to="/manage/production/list">{t("menus.header.productionlist")}</Link>
},
@@ -374,6 +396,7 @@ function Header({
? [
{
key: "productionboard",
id: "header-production-board",
icon: <Icon component={BsKanban} />,
label: <Link to="/manage/production/board">{t("menus.header.productionboard")}</Link>
}
@@ -391,6 +414,7 @@ function Header({
},
{
key: "scoreboard",
id: "header-scoreboard",
icon: <LineChartOutlined />,
label: <Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link>
}
@@ -401,15 +425,18 @@ function Header({
{
key: "customers",
icon: <UserOutlined />,
id: "header-customers",
label: t("menus.header.customers"),
children: [
{
key: "owners",
id: "header-owners",
icon: <TeamOutlined />,
label: <Link to="/manage/owners">{t("menus.header.owners")}</Link>
},
{
key: "vehicles",
id: "header-vehicles",
icon: <CarFilled />,
label: <Link to="/manage/vehicles">{t("menus.header.vehicles")}</Link>
}
@@ -423,21 +450,25 @@ function Header({
? [
{
key: "ccs",
id: "header-css",
icon: <CarFilled />,
label: t("menus.header.courtesycars"),
children: [
{
key: "courtesycarsall",
id: "header-courtesycars-all",
icon: <CarFilled />,
label: <Link to="/manage/courtesycars">{t("menus.header.courtesycars-all")}</Link>
},
{
key: "contracts",
id: "header-contracts",
icon: <FileFilled />,
label: <Link to="/manage/courtesycars/contracts">{t("menus.header.courtesycars-contracts")}</Link>
},
{
key: "newcontract",
id: "header-newcontract",
icon: <FileAddFilled />,
label: <Link to="/manage/courtesycars/contracts/new">{t("menus.header.courtesycars-newcontract")}</Link>
}
@@ -450,6 +481,7 @@ function Header({
? [
{
key: "accounting",
id: "header-accounting",
icon: <DollarCircleFilled />,
label: t("menus.header.accounting"),
children: accountingChildren
@@ -458,6 +490,7 @@ function Header({
: []),
{
key: "phonebook",
id: "header-phonebook",
icon: <PhoneOutlined />,
label: <Link to="/manage/phonebook">{t("menus.header.phonebook")}</Link>
},
@@ -469,6 +502,7 @@ function Header({
? [
{
key: "temporarydocs",
id: "header-temporarydocs",
icon: <PaperClipOutlined />,
label: <Link to="/manage/temporarydocs">{t("menus.header.temporarydocs")}</Link>
}
@@ -505,21 +539,25 @@ function Header({
},
{
key: "shopsubmenu",
id: "header-shopsubmenu",
icon: <SettingOutlined />,
label: t("menus.header.shop"),
children: [
{
key: "shop",
id: "header-shop",
icon: <Icon component={GiSettingsKnobs} />,
label: <Link to="/manage/shop?tab=info">{t("menus.header.shop_config")}</Link>
},
{
key: "dashboard",
id: "header-dashboard",
icon: <DashboardFilled />,
label: <Link to="/manage/dashboard">{t("menus.header.dashboard")}</Link>
},
{
key: "reportcenter",
id: "header-reportcenter",
icon: <BarChartOutlined />,
label: t("menus.header.reportcenter"),
onClick: () => {
@@ -531,6 +569,7 @@ function Header({
},
{
key: "shop-vendors",
id: "header-shop-vendors",
icon: <Icon component={IoBusinessOutline} />,
label: <Link to="/manage/shop/vendors">{t("menus.header.shop_vendors")}</Link>
},
@@ -542,6 +581,7 @@ function Header({
? [
{
key: "shop-csi",
id: "header-shop-csi",
icon: <Icon component={RiSurveyLine} />,
label: <Link to="/manage/shop/csi">{t("menus.header.shop_csi")}</Link>
}
@@ -555,6 +595,7 @@ function Header({
children: [
{
key: "signout",
id: "header-signout",
icon: <Icon component={FiLogOut} />,
danger: true,
label: t("user.actions.signout"),
@@ -562,6 +603,7 @@ function Header({
},
{
key: "help",
id: "header-help",
icon: <Icon component={QuestionCircleFilled} />,
label: t("menus.header.help"),
onClick: () => {
@@ -576,14 +618,22 @@ function Header({
);
}
},
// {
// key: 'rescue',
// icon: <Icon component={CarFilled}/>,
// label: t("menus.header.rescueme"),
// onClick: () => {
// window.open("https://imexrescue.com/", "_blank");
// }
// },
...(InstanceRenderManager({
imex: true,
rome: false,
promanager: false
})
? [
{
key: "rescue",
icon: <Icon component={CarFilled} />,
label: t("menus.header.rescueme"),
onClick: () => {
window.open("https://imexrescue.com/", "_blank");
}
}
]
: []),
...(InstanceRenderManager({
imex: true,
@@ -593,6 +643,7 @@ function Header({
? [
{
key: "shiftclock",
id: "header-shiftclock",
icon: <Icon component={GiPlayerTime} />,
label: <Link to="/manage/shiftclock">{t("menus.header.shiftclock")}</Link>
}
@@ -600,6 +651,7 @@ function Header({
: []),
{
key: "profile",
id: "header-profile",
icon: <UserOutlined />,
label: <Link to="/manage/profile">{t("menus.currentuser.profile")}</Link>
}
@@ -635,6 +687,7 @@ function Header({
{
key: "recent",
icon: <ClockCircleFilled />,
id: "header-recent",
children: recentItems.map((i, idx) => ({
key: idx,
label: <Link to={i.url}>{i.label}</Link>
@@ -648,6 +701,7 @@ function Header({
imex: () => {
menuItems.push({
key: "beta-switch",
id: "header-beta-switch",
style: { marginLeft: "auto" },
label: (
<Tooltip

View File

@@ -1,6 +1,5 @@
import { useMutation } from "@apollo/client";
import { Button, Card, Form, Input, notification, Switch } from "antd";
import dayjs from "../../../../utils/day";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -14,6 +13,7 @@ import { UPDATE_OWNER } from "../../../../graphql/owners.queries";
import { insertAuditTrail } from "../../../../redux/application/application.actions";
import { selectBodyshop, selectCurrentUser } from "../../../../redux/user/user.selectors";
import AuditTrailMapping from "../../../../utils/AuditTrailMappings";
import dayjs from "../../../../utils/day";
import ConfigFormComponents from "../../../config-form-components/config-form-components.component";
import DateTimePicker from "../../../form-date-time-picker/form-date-time-picker.component";
@@ -275,7 +275,19 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
>
<DateTimePicker disabled={readOnly} />
</Form.Item>
<Form.Item name="actual_delivery" label={t("jobs.fields.actual_delivery")} disabled={readOnly}>
<Form.Item
name="actual_delivery"
label={t("jobs.fields.actual_delivery")}
rules={[
{
required: bodyshop.deliverchecklist.actual_delivery
? bodyshop.deliverchecklist.actual_delivery
: false
//message: t("general.validation.required"),
}
]}
disabled={readOnly}
>
<DateTimePicker disabled={readOnly} />
</Form.Item>
<Form.Item

View File

@@ -3,7 +3,7 @@ import React from "react";
import { useQuery } from "@apollo/client";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_BILLS_BY_JOBID } from "../../graphql/bills.queries";
import { QUERY_PARTS_BILLS_BY_JOBID } from "../../graphql/bills.queries";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
@@ -19,7 +19,7 @@ const mapDispatchToProps = (dispatch) => ({
export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRoGuardBills);
export function JobCloseRoGuardBills({ job, jobRO, bodyshop, form, warningCallback }) {
const { loading, error, data } = useQuery(QUERY_BILLS_BY_JOBID, {
const { loading, error, data } = useQuery(QUERY_PARTS_BILLS_BY_JOBID, {
variables: { jobid: job.id },
fetchPolicy: "network-only",
nextFetchPolicy: "network-only"

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useState } from "react";
import { LockOutlined } from "@ant-design/icons";
import { Badge, Card, Col, Collapse, Form, Input, Row, Space, Tooltip } from "antd";
import React, { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -12,9 +12,8 @@ import JobCloseRoGuardBills from "./job-close-ro-guard.bills";
import JobCloseRoGuardPpd from "./job-close-ro-guard.ppd";
import JobCloseRoGuardProfit from "./job-close-ro-guard.profit";
import "./job-close-ro-guard.styles.scss";
import JobCloseRoGuardSublet from "./job-close-ro-guard.sublet";
import JobCloseRoGuardTtLifecycle from "./job-close-ro-guard.tt-lifecycle";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import JobCloseRoGuardTtLifecycle from "./job-close-ro-guard.tt-lifecycle";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser

View File

@@ -1,14 +1,21 @@
import React from "react";
import { useTranslation } from "react-i18next";
import CardTemplate from "./job-detail-cards.template.component";
import Car from "../job-damage-visual/job-damage-visual.component";
import CardTemplate from "./job-detail-cards.template.component";
export default function JobDetailCardsDamageComponent({ loading, data }) {
const { t } = useTranslation();
const { area_of_damage } = data;
return (
<CardTemplate loading={loading} title={t("jobs.labels.cards.damage")}>
{area_of_damage ? <Car dmg1={area_of_damage.impact1} dmg2={area_of_damage.impact2} /> : t("jobs.errors.nodamage")}
{area_of_damage ? (
<Car
dmg1={area_of_damage.impact1 && area_of_damage.impact1.padStart(2, "0")}
dmg2={area_of_damage.impact2 && area_of_damage.impact2.padStart(2, "0")}
/>
) : (
t("jobs.errors.nodamage")
)}
</CardTemplate>
);
}

View File

@@ -26,6 +26,16 @@ export function JobDetailCardsPartsComponent({ loading, data, jobRO }) {
const { t } = useTranslation();
const { joblines_status } = data;
const filteredJobLines = data.joblines.filter(
(j) =>
j.part_type !== null &&
j.part_type !== "PAE" &&
j.part_type !== "PAS" &&
j.part_type !== "PASL" &&
j.part_qty !== 0 &&
j.act_price !== 0
);
//TODO: Correct jobline_statuses view by including the part_qty !== 0 and act_price !== 0
const columns = [
{
title: t("joblines.fields.line_desc"),
@@ -95,7 +105,7 @@ export function JobDetailCardsPartsComponent({ loading, data, jobRO }) {
<div>
<CardTemplate loading={loading} title={t("jobs.labels.cards.parts")}>
<PartsStatusPie joblines_status={joblines_status} />
<Table key="id" columns={columns} dataSource={data ? data.joblines : []} />
<Table key="id" columns={columns} dataSource={filteredJobLines ? filteredJobLines : []} />
</CardTemplate>
</div>
);

View File

@@ -2,26 +2,30 @@ import { useQuery } from "@apollo/client";
import { Col, Row, Skeleton, Space, 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 { QUERY_JOBLINE_TASKS_PAGINATED } from "../../graphql/tasks.queries.js";
import { selectTechnician } from "../../redux/tech/tech.selectors.js";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
import AlertComponent from "../alert/alert.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { QUERY_JOBLINE_TASKS_PAGINATED } from "../../graphql/tasks.queries.js";
import BillDetailEditcontainer from "../bill-detail-edit/bill-detail-edit.container.jsx";
import FeatureWrapper from "../feature-wrapper/feature-wrapper.component.jsx";
import TaskListContainer from "../task-list/task-list.container.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
bodyshop: selectBodyshop,
technician: selectTechnician
});
const mapDispatchToProps = (dispatch) => ({});
export default connect(mapStateToProps, mapDispatchToProps)(JobLinesExpander);
export function JobLinesExpander({ jobline, jobid, bodyshop }) {
export function JobLinesExpander({ jobline, jobid, bodyshop, technician }) {
const { t } = useTranslation();
const { loading, error, data } = useQuery(GET_JOB_LINE_ORDERS, {
fetchPolicy: "network-only",
@@ -46,9 +50,15 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) {
children: (
<Row wrap>
<Col span={4}>
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`}>
{line.parts_order.order_number}
</Link>
{!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>
@@ -83,70 +93,81 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) {
key: line.id,
children: (
<Row>
<Col span={8}>
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.id}`}>{line.parts_dispatch.number}</Link>
</Col>
<Col span={8}>{line.parts_dispatch.number}</Col>
<Col span={8}>
{bodyshop.employees.find((e) => e.id === line.parts_dispatch.employeeid)?.first_name}
</Col>
<Col span={8}>
<Space>
{t("parts_dispatch_lines.fields.accepted_at")}
<DateFormatter>{line.accepted_at}</DateFormatter>
</Space>
{line.accepted_at ? (
<Space>
{t("parts_dispatch_lines.fields.accepted_at")}
<DateFormatter>{line.accepted_at}</DateFormatter>
</Space>
) : null}
</Col>
</Row>
)
}))
: {
key: "dispatch-lines",
children: t("parts_orders.labels.notyetordered")
}
}
/>
</Col>
<Col md={24} lg={8}>
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
<Timeline
items={
data.billlines.length > 0
? data.billlines.map((line) => ({
key: line.id,
children: (
<Row wrap>
<Col span={4}>
<Link to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`}>
{line.bill.invoice_number}
</Link>
</Col>
<Col span={4}>
<span>
{`${t("billlines.fields.actual_price")}: `}
<CurrencyFormatter>{line.actual_price}</CurrencyFormatter>
</span>
</Col>
<Col span={4}>
<span>
{`${t("billlines.fields.actual_cost")}: `}
<CurrencyFormatter>{line.actual_cost}</CurrencyFormatter>
</span>
</Col>
<Col span={4}>
<DateFormatter>{line.bill.date}</DateFormatter>
</Col>
<Col span={4}> {line.bill.vendor.name}</Col>
</Row>
)
}))
: [
{
key: "no-orders",
children: t("bills.labels.nobilllines")
key: "dispatch-lines",
children: t("parts_dispatch.labels.notyetdispatched")
}
]
}
/>
</Col>
<FeatureWrapper featureName="bills" noauth={() => null}>
<Col md={24} lg={8}>
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
<BillDetailEditcontainer />
<Timeline
items={
data.billlines.length > 0
? data.billlines.map((line) => ({
key: line.id,
children: (
<Row wrap>
<Col span={4}>
{!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>
{`${t("billlines.fields.actual_price")}: `}
<CurrencyFormatter>{line.actual_price}</CurrencyFormatter>
</span>
</Col>
<Col span={4}>
<span>
{`${t("billlines.fields.actual_cost")}: `}
<CurrencyFormatter>{line.actual_cost}</CurrencyFormatter>
</span>
</Col>
<Col span={4}>
<DateFormatter>{line.bill.date}</DateFormatter>
</Col>
<Col span={4}> {line.bill.vendor.name}</Col>
</Row>
)
}))
: [
{
key: "no-orders",
children: t("bills.labels.nobilllines")
}
]
}
/>
</Col>
</FeatureWrapper>
<Col md={24} lg={24}>
<TaskListContainer
parentJobId={jobid}

View File

@@ -3,13 +3,21 @@ import { Button, Form, notification, Popover, Tooltip } from "antd";
import axios from "axios";
import { t } from "i18next";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UPDATE_LINE_PPC } from "../../graphql/jobs-lines.queries";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component";
export default function JobLinesPartPriceChange({ job, line, refetch }) {
const mapStateToProps = createStructuredSelector({
technician: selectTechnician
});
const mapDispatchToProps = (dispatch) => ({});
export function JobLinesPartPriceChange({ job, line, refetch, technician }) {
const [loading, setLoading] = useState(false);
const [updatePartPrice] = useMutation(UPDATE_LINE_PPC);
@@ -52,7 +60,7 @@ export default function JobLinesPartPriceChange({ job, line, refetch }) {
}
};
const popcontent = InstanceRenderManager({
const popcontent = !technician && InstanceRenderManager({
imex: null,
rome: (
<Form layout="vertical" onFinish={handleFinish} initialValues={{ act_price: line.act_price }}>
@@ -95,3 +103,4 @@ export default function JobLinesPartPriceChange({ job, line, refetch }) {
</JobLineConvertToLabor>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(JobLinesPartPriceChange);

View File

@@ -31,18 +31,20 @@ import JobLinesBillRefernece from "../job-lines-bill-reference/job-lines-bill-re
// import AllocationsEmployeeLabelContainer from "../allocations-employee-label/allocations-employee-label.container";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import _ from "lodash";
import { FaTasks } from "react-icons/fa";
import { selectBodyshop } from "../../redux/user/user.selectors";
import dayjs from "../../utils/day";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import JobCreateIOU from "../job-create-iou/job-create-iou.component";
import JobLineBulkAssignComponent from "../job-line-bulk-assign/job-line-bulk-assign.component";
import JobLineDispatchButton from "../job-line-dispatch-button/job-line-dispatch-button.component";
import JoblineTeamAssignment from "../job-line-team-assignment/job-line-team-assignmnent.component";
import JobSendPartPriceChangeComponent from "../job-send-parts-price-change/job-send-parts-price-change.component";
import PartsOrderDrawer from "../parts-order-list-table/parts-order-list-table-drawer.component";
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
import JobLinesExpander from "./job-lines-expander.component";
import JobLinesPartPriceChange from "./job-lines-part-price-change.component";
import { FaTasks } from "react-icons/fa";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -53,6 +55,7 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({
setJobLineEditContext: (context) => 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" })),
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
});
@@ -62,6 +65,7 @@ export function JobLinesComponent({
jobRO,
technician,
setPartsOrderContext,
setPartsReceiveContext,
loading,
refetch,
jobLines,
@@ -70,7 +74,11 @@ export function JobLinesComponent({
setJobLineEditContext,
form,
setBillEnterContext,
setTaskUpsertContext
setTaskUpsertContext,
billsQuery,
handleBillOnRowClick,
handlePartsOrderOnRowClick,
handlePartsDispatchOnRowClick
}) {
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
const {
@@ -197,7 +205,6 @@ export function JobLinesComponent({
onFilter: (value, record) => value.includes(record.part_type),
render: (text, record) => (record.part_type ? t(`joblines.fields.part_types.${record.part_type}`) : null)
},
{
title: t("joblines.fields.act_price"),
dataIndex: "act_price",
@@ -212,7 +219,6 @@ export function JobLinesComponent({
dataIndex: "part_qty",
key: "part_qty"
},
// {
// title: t('joblines.fields.tax_part'),
// dataIndex: 'tax_part',
@@ -321,7 +327,7 @@ export function JobLinesComponent({
key: "actions",
render: (text, record) => (
<Space>
{(record.manual_line || jobIsPrivate) && (
{(record.manual_line || jobIsPrivate) && !technician && (
<>
<Button
disabled={jobRO}
@@ -336,7 +342,6 @@ export function JobLinesComponent({
</Button>
</>
)}
<Button
title={t("tasks.buttons.create")}
onClick={() => {
@@ -350,7 +355,7 @@ export function JobLinesComponent({
>
<FaTasks />
</Button>
{(record.manual_line || jobIsPrivate) && (
{(record.manual_line || jobIsPrivate) && !technician && (
<>
<Button
disabled={jobRO}
@@ -436,6 +441,15 @@ export function JobLinesComponent({
return (
<div>
<PartsOrderModalContainer />
{!technician && (
<PartsOrderDrawer
job={job}
billsQuery={billsQuery}
handleOnRowClick={handlePartsOrderOnRowClick}
setPartsReceiveContext={setPartsReceiveContext}
setTaskUpsertContext={setTaskUpsertContext}
/>
)}
<PageHeader
title={t("jobs.labels.estimatelines")}
extra={
@@ -552,7 +566,7 @@ export function JobLinesComponent({
>
{t("joblines.actions.new")}
</Button>
{bodyshop.region_config.toLowerCase().startsWith("us") && <JobSendPartPriceChangeComponent job={job} />}
{InstanceRenderManager({ rome: <JobSendPartPriceChangeComponent job={job} disabled={technician} /> })}
<JobCreateIOU job={job} selectedJobLines={selectedLines} />
<Input.Search
placeholder={t("general.labels.search")}

View File

@@ -1,7 +1,17 @@
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,
handlePartsDispatchOnRowClick,
refetch,
form,
...rest
}) {
const [searchText, setSearchText] = useState("");
const jobLines = useMemo(() => {
@@ -22,7 +32,19 @@ function JobLinesContainer({ job, joblines, refetch, form, ...rest }) {
}, [joblines, searchText]);
return (
<JobLinesComponent refetch={refetch} jobLines={jobLines} setSearchText={setSearchText} job={job} form={form} />
<div>
<JobLinesComponent
refetch={refetch}
jobLines={jobLines}
billsQuery={billsQuery}
handleBillOnRowClick={handleBillOnRowClick}
handlePartsOrderOnRowClick={handlePartsOrderOnRowClick}
handlePartsDispatchOnRowClick={handlePartsDispatchOnRowClick}
setSearchText={setSearchText}
job={job}
form={form}
/>
</div>
);
}

View File

@@ -11,17 +11,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, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
});
export default connect(mapStateToProps, mapDispatchToProps)(JobLineConvertToLabor);
export function JobLineConvertToLabor({ children, jobline, job, insertAuditTrail, ...otherBtnProps }) {
export function JobLineConvertToLabor({ children, jobline, job, insertAuditTrail, technician, ...otherBtnProps }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
@@ -165,7 +166,7 @@ export function JobLineConvertToLabor({ children, jobline, job, insertAuditTrail
return (
<>
{children}
{jobline.act_price !== 0 && (
{jobline.act_price !== 0 && !technician && (
<Popover disabled={jobline.convertedtolbr} content={overlay} open={visibility} placement="bottom">
<Tooltip title={t("joblines.actions.converttolabor")}>
<Button

View File

@@ -3,7 +3,7 @@ import axios from "axios";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
export default function JobSendPartPriceChangeComponent({ job }) {
export default function JobSendPartPriceChangeComponent({ job, disabled }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const handleClick = async () => {
@@ -24,7 +24,7 @@ export default function JobSendPartPriceChangeComponent({ job }) {
};
return (
<Button onClick={handleClick} loading={loading}>
<Button onClick={handleClick} loading={loading} disabled={disabled}>
{t("jobs.actions.sendpartspricechange")}
</Button>
);

View File

@@ -3,8 +3,8 @@ import Dinero from "dinero.js";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import { alphaSort } from "../../utils/sorters";
export default function JobTotalsTableLabor({ job }) {
const { t } = useTranslation();
@@ -56,16 +56,49 @@ export default function JobTotalsTableLabor({ job }) {
sortOrder: state.sortedInfo.columnKey === "mod_lb_hrs" && state.sortedInfo.order,
render: (text, record) => record.hours.toFixed(1)
},
{
title: t("joblines.fields.total"),
dataIndex: "total",
key: "total",
align: "right",
sorter: (a, b) => a.total.amount - b.total.amount,
sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
render: (text, record) => Dinero(record.total).toFormat()
}
...InstanceRenderManager({
imex: [
{
title: t("joblines.fields.total"),
dataIndex: "total",
key: "total",
align: "right",
sorter: (a, b) => a.total.amount - b.total.amount,
sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
render: (text, record) => Dinero(record.total).toFormat()
}
],
rome: [
{
title: t("joblines.fields.amount"),
dataIndex: "base",
key: "base",
align: "right",
sorter: (a, b) => a.base.amount - b.base.amount,
sortOrder: state.sortedInfo.columnKey === "base" && state.sortedInfo.order,
render: (text, record) => Dinero(record.base).toFormat()
},
{
title: t("joblines.fields.adjustment"),
dataIndex: "adjustment",
key: "adjustment",
align: "right",
sorter: (a, b) => a.adjustment.amount - b.adjustment.amount,
sortOrder: state.sortedInfo.columnKey === "adjustment" && state.sortedInfo.order,
render: (text, record) => Dinero(record.adjustment).toFormat()
},
{
title: t("joblines.fields.total"),
dataIndex: "total",
key: "total",
align: "right",
sorter: (a, b) => a.total.amount - b.total.amount,
sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
render: (text, record) => Dinero(record.total).toFormat()
}
],
promanager: "USE_ROME"
})
];
const handleTableChange = (pagination, filters, sorter) => {
@@ -91,6 +124,16 @@ export default function JobTotalsTableLabor({ job }) {
<Table.Summary.Cell>
{(job.job_totals.rates.mapa.hours + job.job_totals.rates.mash.hours).toFixed(1)}
</Table.Summary.Cell>
{InstanceRenderManager({
imex: null,
rome: (
<>
<Table.Summary.Cell />
<Table.Summary.Cell />
</>
),
promanager: "USE_ROME"
})}
<Table.Summary.Cell align="right">
<strong>{Dinero(job.job_totals.rates.rates_subtotal).toFormat()}</strong>
</Table.Summary.Cell>
@@ -122,7 +165,29 @@ export default function JobTotalsTableLabor({ job }) {
<CurrencyFormatter>{job.job_totals.rates.mapa.rate}</CurrencyFormatter>
</Table.Summary.Cell>
<Table.Summary.Cell>{job.job_totals.rates.mapa.hours.toFixed(1)}</Table.Summary.Cell>
<Table.Summary.Cell align="right">{Dinero(job.job_totals.rates.mapa.total).toFormat()}</Table.Summary.Cell>
{InstanceRenderManager({
imex: (
<>
<Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mapa.total).toFormat()}
</Table.Summary.Cell>
</>
),
rome: (
<>
<Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mapa.base).toFormat()}
</Table.Summary.Cell>
<Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mapa.adjustment).toFormat()}
</Table.Summary.Cell>
<Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mapa.total).toFormat()}
</Table.Summary.Cell>
</>
),
promanager: "USE_ROME"
})}
</Table.Summary.Row>
<Table.Summary.Row>
<Table.Summary.Cell>
@@ -151,7 +216,29 @@ export default function JobTotalsTableLabor({ job }) {
<CurrencyFormatter>{job.job_totals.rates.mash.rate}</CurrencyFormatter>
</Table.Summary.Cell>
<Table.Summary.Cell>{job.job_totals.rates.mash.hours.toFixed(1)}</Table.Summary.Cell>
<Table.Summary.Cell align="right">{Dinero(job.job_totals.rates.mash.total).toFormat()}</Table.Summary.Cell>
{InstanceRenderManager({
imex: (
<>
<Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mash.total).toFormat()}
</Table.Summary.Cell>
</>
),
rome: (
<>
<Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mash.base).toFormat()}
</Table.Summary.Cell>
<Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mash.adjustment).toFormat()}
</Table.Summary.Cell>
<Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mash.total).toFormat()}
</Table.Summary.Cell>
</>
),
promanager: "USE_ROME"
})}
</Table.Summary.Row>
<Table.Summary.Row>
<Table.Summary.Cell>
@@ -159,6 +246,16 @@ export default function JobTotalsTableLabor({ job }) {
</Table.Summary.Cell>
<Table.Summary.Cell />
<Table.Summary.Cell />
{InstanceRenderManager({
imex: null,
rome: (
<>
<Table.Summary.Cell />
<Table.Summary.Cell />
</>
),
promanager: "USE_ROME"
})}
<Table.Summary.Cell align="right">
<strong>{Dinero(job.job_totals.rates.subtotal).toFormat()}</strong>
</Table.Summary.Cell>

View File

@@ -3,6 +3,7 @@ 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";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
export default function JobAdminDeleteIntake({ job }) {
const { t } = useTranslation();
@@ -47,16 +48,22 @@ export default function JobAdminDeleteIntake({ job }) {
setLoading(false);
};
return (
const InstanceRender = InstanceRenderManager({
imex: true,
rome: "USE_IMEX",
promanager: false
});
return InstanceRender ? (
<>
<Space wrap>
<Button loading={loading} onClick={handleDelete} disabled={!job.intakechecklist}>
{t("jobs.labels.deleteintake")}
</Button>
<Button loading={loading} onClick={handleDeleteDelivery} disabled={!job.deliverychecklist}>
<Button loading={loading} onClick={handleDeleteDelivery} disabled={!job.deliverchecklist}>
{t("jobs.labels.deletedelivery")}
</Button>
</Space>
</>
);
) : null;
}

View File

@@ -3,7 +3,6 @@ import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Col, Row, notification } from "antd";
import Axios from "axios";
import _ from "lodash";
import dayjs from "../../utils/day";
import queryString from "query-string";
import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -24,6 +23,8 @@ import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selecto
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import confirmDialog from "../../utils/asyncConfirm";
import CriticalPartsScan from "../../utils/criticalPartsScan";
import dayjs from "../../utils/day";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import AlertComponent from "../alert/alert.component";
import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component";
import JobsFindModalContainer from "../jobs-find-modal/jobs-find-modal.container";
@@ -32,7 +33,6 @@ import OwnerFindModalContainer from "../owner-find-modal/owner-find-modal.contai
import { GetSupplementDelta } from "./jobs-available-supplement.estlines.util";
import HeaderFields from "./jobs-available-supplement.headerfields";
import JobsAvailableTableComponent from "./jobs-available-table.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -580,12 +580,13 @@ function ResolveCCCLineIssues(estData, bodyshop) {
InstanceRenderManager({
executeFunction: true,
args: [],
promanager: () => {
rome: () => {
if (line.mod_lbr_ty === "LAET" || line.mod_lbr_ty === "LAUT") {
// line.notes += ` | ET/UT Update (prev = ${line.mod_lbr_ty})`;
line.mod_lbr_ty = "LAR";
}
}
},
promanager: "USE_ROME"
});
});

View File

@@ -189,7 +189,10 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
</Col>
<Col {...lossColDamage}>
{job.area_of_damage ? (
<Car dmg1={job.area_of_damage.impact1} dmg2={job.area_of_damage.impact2} />
<Car
dmg1={job.area_of_damage.impact1 && job.area_of_damage.impact1.padStart(2, "0")}
dmg2={job.area_of_damage.impact2 && job.area_of_damage.impact2.padStart(2, "0")}
/>
) : (
t("jobs.errors.nodamage")
)}

View File

@@ -1,6 +1,9 @@
import { DownCircleFilled } from "@ant-design/icons";
import { useApolloClient, useMutation } from "@apollo/client";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Button, Card, Dropdown, Form, Input, Modal, notification, Popconfirm, Popover, Select, Space } from "antd";
import axios from "axios";
import parsePhoneNumber from "libphonenumber-js";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -8,27 +11,24 @@ import { Link, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
import { GET_CURRENT_QUESTIONSET_ID, INSERT_CSI } from "../../graphql/csi.queries";
import { DELETE_JOB, UPDATE_JOB, VOID_JOB } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { setEmailOptions } from "../../redux/email/email.actions";
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants";
import dayjs from "../../utils/day";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
import axios from "axios";
import { setEmailOptions } from "../../redux/email/email.actions";
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
import { GET_CURRENT_QUESTIONSET_ID, INSERT_CSI } from "../../graphql/csi.queries";
import { TemplateList } from "../../utils/TemplateConstants";
import parsePhoneNumber from "libphonenumber-js";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
import dayjs from "../../utils/day";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
const mapStateToProps = createStructuredSelector({
@@ -83,6 +83,13 @@ const mapDispatchToProps = (dispatch) => ({
modal: "timeTicketTask"
})
),
setTaskUpsertContext: (context) =>
dispatch(
setModalContext({
context: context,
modal: "taskUpsert"
})
),
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
setMessage: (text) => dispatch(setMessage(text))
@@ -104,7 +111,8 @@ export function JobsDetailHeaderActions({
setEmailOptions,
openChatByPhone,
setMessage,
setTimeTicketTaskContext
setTimeTicketTaskContext,
setTaskUpsertContext
}) {
const { t } = useTranslation();
const client = useApolloClient();
@@ -633,6 +641,7 @@ export function JobsDetailHeaderActions({
const menuItems = [
{
key: "schedule",
id: "job-actions-schedule",
disabled: !jobInPreProduction || !job.converted || jobRO,
label: t("jobs.actions.schedule"),
onClick: () => {
@@ -649,6 +658,7 @@ export function JobsDetailHeaderActions({
},
{
key: "cancelallappointments",
id: "job-actions-cancelallappointments",
onClick: () => {
if (job.status !== bodyshop.md_ro_statuses.default_scheduled) {
return;
@@ -662,6 +672,7 @@ export function JobsDetailHeaderActions({
imex: [
{
key: "intake",
id: "job-actions-intake",
disabled: !!job.intakechecklist || !jobInPreProduction || !job.converted || jobRO,
label:
!!job.intakechecklist || !jobInPreProduction || !job.converted || jobRO ? (
@@ -672,6 +683,7 @@ export function JobsDetailHeaderActions({
},
{
key: "deliver",
id: "job-actions-deliver",
disabled: !jobInProduction || jobRO,
label: !jobInProduction ? (
t("jobs.actions.deliver")
@@ -681,6 +693,7 @@ export function JobsDetailHeaderActions({
},
{
key: "checklist",
id: "job-actions-checklist",
disabled: !job.converted,
label: <Link to={`/manage/jobs/${job.id}/checklist`}>{t("jobs.actions.viewchecklist")}</Link>
}
@@ -689,6 +702,7 @@ export function JobsDetailHeaderActions({
promanager: [
{
key: "toggleproduction",
id: "job-actions-toggleproduction",
disabled: !job.converted || jobRO,
label: <JobsDetailHeaderActionsToggleProduction job={job} refetch={refetch} />
}
@@ -702,6 +716,7 @@ export function JobsDetailHeaderActions({
? [
{
key: "entertimetickets",
id: "job-actions-entertimetickets",
disabled: !job.converted || (!bodyshop.tt_allow_post_to_invoiced && job.date_invoiced),
label: t("timetickets.actions.enter"),
onClick: () => {
@@ -725,6 +740,7 @@ export function JobsDetailHeaderActions({
if (bodyshop.md_tasks_presets.enable_tasks) {
menuItems.push({
key: "claimtimetickettasks",
id: "job-actions-claimtimetickettasks",
disabled: !job.converted || (!bodyshop.tt_allow_post_to_invoiced && job.date_invoiced),
onClick: () => {
setTimeTicketTaskContext({
@@ -738,6 +754,7 @@ export function JobsDetailHeaderActions({
menuItems.push({
key: "enterpayments",
id: "job-actions-enterpayments",
disabled: !job.converted,
label: t("menus.header.enterpayment"),
onClick: () => {
@@ -753,22 +770,24 @@ export function JobsDetailHeaderActions({
if (ImEXPay.treatment === "on") {
menuItems.push({
key: "entercardpayments",
id: "job-actions-entercardpayments",
disabled: !job.converted,
label: t("menus.header.entercardpayment"),
onClick: () => {
logImEXEvent("job_header_enter_card_payment");
setCardPaymentContext({
actions: {},
actions: { refetch },
context: { jobid: job.id }
});
}
});
}
if (HasFeatureAccess({ featureName: "courtesycars" })) {
if (HasFeatureAccess({ featureName: "courtesycars", bodyshop })) {
menuItems.push({
key: "cccontract",
id: "job-actions-cccontract",
disabled: jobRO || !job.converted,
label: (
<Link state={{ jobId: job.id }} to="/manage/courtesycars/contracts/new">
@@ -778,16 +797,29 @@ export function JobsDetailHeaderActions({
});
}
menuItems.push({
key: "createtask",
id: "job-actions-createtask",
label: t("menus.header.create_task"),
onClick: () =>
setTaskUpsertContext({
actions: {},
context: { jobid: job.id }
})
});
menuItems.push(
job.inproduction
? {
key: "removefromproduction",
id: "job-actions-removefromproduction",
disabled: !job.converted,
label: t("jobs.actions.removefromproduction"),
onClick: () => AddToProduction(client, job.id, refetch, true)
}
: {
key: "addtoproduction",
id: "job-actions-addtoproduction",
disabled: !job.converted,
label: t("jobs.actions.addtoproduction"),
onClick: () => AddToProduction(client, job.id, refetch)
@@ -797,12 +829,14 @@ export function JobsDetailHeaderActions({
menuItems.push(
{
key: "togglesuspend",
id: "job-actions-togglesuspend",
onClick: handleSuspend,
label: job.suspended ? t("production.actions.unsuspend") : t("production.actions.suspend")
},
{
key: "toggleAlert",
onClick: handleAlertToggle,
id: "job-actions-togglealert",
label:
job.production_vars && job.production_vars.alert
? t("production.labels.alertoff")
@@ -814,6 +848,7 @@ export function JobsDetailHeaderActions({
children: [
{
key: "duplicate",
id: "job-actions-duplicate",
label: (
<Popconfirm
title={t("jobs.labels.duplicateconfirm")}
@@ -829,6 +864,7 @@ export function JobsDetailHeaderActions({
},
{
key: "duplicatenolines",
id: "job-actions-duplicatenolines",
label: (
<Popconfirm
title={t("jobs.labels.duplicateconfirm")}
@@ -852,6 +888,7 @@ export function JobsDetailHeaderActions({
? [
{
key: "postbills",
id: "job-actions-postbills",
disabled: !job.converted,
label: t("jobs.actions.postbills"),
onClick: () => {
@@ -870,6 +907,7 @@ export function JobsDetailHeaderActions({
{
key: "addtopartsqueue",
id: "job-actions-addtopartsqueue",
disabled: !job.converted || !jobInProduction || jobRO,
label: t("jobs.actions.addtopartsqueue"),
onClick: async () => {
@@ -895,6 +933,7 @@ export function JobsDetailHeaderActions({
},
{
key: "closejob",
id: "job-actions-closejob",
disabled: !jobInPostProduction,
label: !jobInPostProduction ? (
t("menus.jobsactions.closejob")
@@ -910,6 +949,7 @@ export function JobsDetailHeaderActions({
},
{
key: "admin",
id: "job-actions-admin",
label: (
<Link
to={{
@@ -931,6 +971,7 @@ export function JobsDetailHeaderActions({
) {
menuItems.push({
key: "exportcustdata",
id: "job-actions-exportcustdata",
disabled: !job.converted,
label: t("jobs.actions.exportcustdata"),
onClick: handleExportCustData
@@ -941,18 +982,21 @@ export function JobsDetailHeaderActions({
const children = [
{
key: "email",
id: "job-actions-email",
disabled: !!!job.ownr_ea,
label: t("general.labels.email"),
onClick: handleCreateCsi
},
{
key: "text",
id: "job-actions-text",
disabled: !!!job.ownr_ph1,
label: t("general.labels.text"),
onClick: handleCreateCsi
},
{
key: "generate",
id: "job-actions-generate",
disabled: job.csiinvites && job.csiinvites.length > 0,
label: t("jobs.actions.generatecsi"),
onClick: handleCreateCsi
@@ -986,6 +1030,7 @@ export function JobsDetailHeaderActions({
}
menuItems.push({
key: "sendcsi",
id: "job-actions-sendcsi",
label: t("jobs.actions.sendcsi"),
disabled: !job.converted,
children
@@ -994,6 +1039,7 @@ export function JobsDetailHeaderActions({
menuItems.push({
key: "jobcosting",
id: "job-actions-jobcosting",
disabled: !job.converted,
label: t("jobs.labels.jobcosting"),
onClick: () => {
@@ -1011,6 +1057,7 @@ export function JobsDetailHeaderActions({
if (job && !job.converted) {
menuItems.push({
key: "deletejob",
id: "job-actions-deletejob",
label: (
<Popconfirm
title={t("jobs.labels.deleteconfirm")}
@@ -1027,6 +1074,7 @@ export function JobsDetailHeaderActions({
menuItems.push({
key: "manualevent",
id: "job-actions-manualevent",
onClick: (e) => {
setVisibility(true);
},
@@ -1036,6 +1084,7 @@ export function JobsDetailHeaderActions({
if (!jobRO && job.converted) {
menuItems.push({
key: "voidjob",
id: "job-actions-voidjob",
label: (
<RbacWrapper action="jobs:void" noauth>
<Popconfirm

View File

@@ -1,55 +1,13 @@
import { useQuery } from "@apollo/client";
import queryString from "query-string";
import React from "react";
import { useLocation, useNavigate } 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 = useNavigate();
const handleBillOnRowClick = (record) => {
if (record) {
if (record.id) {
search.billid = record.id;
history({ search: queryString.stringify(search) });
}
} else {
delete search.billid;
history({ search: queryString.stringify(search) });
}
};
const handlePartsOrderOnRowClick = (record) => {
if (record) {
if (record.id) {
search.partsorderid = record.id;
history({ search: queryString.stringify(search) });
}
} else {
delete search.partsorderid;
history({ search: queryString.stringify(search) });
}
};
const handlePartsDispatchOnRowClick = (record) => {
if (record) {
if (record.id) {
search.partsdispatchid = record.id;
history.push({ search: queryString.stringify(search) });
}
} else {
delete search.partsdispatchid;
history.push({ search: queryString.stringify(search) });
}
};
export default function JobsDetailPliContainer({
job,
billsQuery,
handleBillOnRowClick,
handlePartsOrderOnRowClick,
handlePartsDispatchOnRowClick
}) {
return (
<JobsDetailPliComponent
job={job}

View File

@@ -1,4 +1,4 @@
import { Collapse, Form, Switch } from "antd";
import { Collapse, Form, InputNumber, Switch } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -17,6 +17,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
<Collapse defaultActiveKey={expanded && "rates"}>
<Collapse.Panel forceRender header={t("jobs.labels.cieca_pfl")} key="cieca_pfl">
<LayoutFormRow header={t("joblines.fields.lbr_types.LAB")}>
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAB", "lbr_adjp"]}>
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
</Form.Item>
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
name={["cieca_pfl", "LAB", "lbr_tax_in"]}
@@ -24,6 +27,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
>
<Switch disabled={jobRO} />
</Form.Item>
<Form.Item shouldUpdate>
{() => {
return (
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
name={["cieca_pfl", "LAB", "lbr_taxp"]}
rules={[
{
required: form.getFieldValue(["cieca_pfl", "LAB", "lbr_tax_in"])
//message: t("general.validation.required"),
}
]}
>
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
</Form.Item>
);
}}
</Form.Item>
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
name={["cieca_pfl", "LAB", "lbr_tx_in1"]}
@@ -61,6 +82,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
</Form.Item>
</LayoutFormRow>
<LayoutFormRow header={t("joblines.fields.lbr_types.LAD")}>
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAD", "lbr_adjp"]}>
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
</Form.Item>
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
name={["cieca_pfl", "LAD", "lbr_tax_in"]}
@@ -68,6 +92,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
>
<Switch disabled={jobRO} />
</Form.Item>
<Form.Item shouldUpdate>
{() => {
return (
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
name={["cieca_pfl", "LAD", "lbr_taxp"]}
rules={[
{
required: form.getFieldValue(["cieca_pfl", "LAD", "lbr_tax_in"])
//message: t("general.validation.required"),
}
]}
>
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
</Form.Item>
);
}}
</Form.Item>
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
name={["cieca_pfl", "LAD", "lbr_tx_in1"]}
@@ -105,6 +147,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
</Form.Item>
</LayoutFormRow>
<LayoutFormRow header={t("joblines.fields.lbr_types.LAE")}>
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAE", "lbr_adjp"]}>
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
</Form.Item>
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
name={["cieca_pfl", "LAE", "lbr_tax_in"]}
@@ -112,6 +157,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
>
<Switch disabled={jobRO} />
</Form.Item>
<Form.Item shouldUpdate>
{() => {
return (
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
name={["cieca_pfl", "LAE", "lbr_taxp"]}
rules={[
{
required: form.getFieldValue(["cieca_pfl", "LAE", "lbr_tax_in"])
//message: t("general.validation.required"),
}
]}
>
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
</Form.Item>
);
}}
</Form.Item>
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
name={["cieca_pfl", "LAE", "lbr_tx_in1"]}
@@ -149,6 +212,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
</Form.Item>
</LayoutFormRow>
<LayoutFormRow header={t("joblines.fields.lbr_types.LAF")}>
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAF", "lbr_adjp"]}>
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
</Form.Item>
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
name={["cieca_pfl", "LAF", "lbr_tax_in"]}
@@ -156,6 +222,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
>
<Switch disabled={jobRO} />
</Form.Item>
<Form.Item shouldUpdate>
{() => {
return (
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
name={["cieca_pfl", "LAF", "lbr_taxp"]}
rules={[
{
required: form.getFieldValue(["cieca_pfl", "LAF", "lbr_tax_in"])
//message: t("general.validation.required"),
}
]}
>
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
</Form.Item>
);
}}
</Form.Item>
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
name={["cieca_pfl", "LAF", "lbr_tx_in1"]}
@@ -193,6 +277,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
</Form.Item>
</LayoutFormRow>
<LayoutFormRow header={t("joblines.fields.lbr_types.LAG")}>
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAG", "lbr_adjp"]}>
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
</Form.Item>
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
name={["cieca_pfl", "LAG", "lbr_tax_in"]}
@@ -200,6 +287,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
>
<Switch disabled={jobRO} />
</Form.Item>
<Form.Item shouldUpdate>
{() => {
return (
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
name={["cieca_pfl", "LAG", "lbr_taxp"]}
rules={[
{
required: form.getFieldValue(["cieca_pfl", "LAG", "lbr_tax_in"])
//message: t("general.validation.required"),
}
]}
>
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
</Form.Item>
);
}}
</Form.Item>
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
name={["cieca_pfl", "LAG", "lbr_tx_in1"]}
@@ -237,6 +342,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
</Form.Item>
</LayoutFormRow>
<LayoutFormRow header={t("joblines.fields.lbr_types.LAM")}>
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAM", "lbr_adjp"]}>
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
</Form.Item>
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
name={["cieca_pfl", "LAM", "lbr_tax_in"]}
@@ -244,6 +352,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
>
<Switch disabled={jobRO} />
</Form.Item>
<Form.Item shouldUpdate>
{() => {
return (
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
name={["cieca_pfl", "LAM", "lbr_taxp"]}
rules={[
{
required: form.getFieldValue(["cieca_pfl", "LAM", "lbr_tax_in"])
//message: t("general.validation.required"),
}
]}
>
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
</Form.Item>
);
}}
</Form.Item>
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
name={["cieca_pfl", "LAM", "lbr_tx_in1"]}
@@ -281,6 +407,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
</Form.Item>
</LayoutFormRow>
<LayoutFormRow header={t("joblines.fields.lbr_types.LAR")}>
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAR", "lbr_adjp"]}>
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
</Form.Item>
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
name={["cieca_pfl", "LAR", "lbr_tax_in"]}
@@ -288,6 +417,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
>
<Switch disabled={jobRO} />
</Form.Item>
<Form.Item shouldUpdate>
{() => {
return (
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
name={["cieca_pfl", "LAR", "lbr_taxp"]}
rules={[
{
required: form.getFieldValue(["cieca_pfl", "LAR", "lbr_tax_in"])
//message: t("general.validation.required"),
}
]}
>
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
</Form.Item>
);
}}
</Form.Item>
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
name={["cieca_pfl", "LAR", "lbr_tx_in1"]}
@@ -325,6 +472,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
</Form.Item>
</LayoutFormRow>
<LayoutFormRow header={t("joblines.fields.lbr_types.LAS")}>
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAS", "lbr_adjp"]}>
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
</Form.Item>
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
name={["cieca_pfl", "LAS", "lbr_tax_in"]}
@@ -332,6 +482,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
>
<Switch disabled={jobRO} />
</Form.Item>
<Form.Item shouldUpdate>
{() => {
return (
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
name={["cieca_pfl", "LAS", "lbr_taxp"]}
rules={[
{
required: form.getFieldValue(["cieca_pfl", "LAS", "lbr_tax_in"])
//message: t("general.validation.required"),
}
]}
>
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
</Form.Item>
);
}}
</Form.Item>
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
name={["cieca_pfl", "LAS", "lbr_tx_in1"]}
@@ -369,6 +537,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
</Form.Item>
</LayoutFormRow>
<LayoutFormRow header={t("joblines.fields.lbr_types.LAU")}>
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAU", "lbr_adjp"]}>
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
</Form.Item>
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
name={["cieca_pfl", "LAU", "lbr_tax_in"]}
@@ -376,6 +547,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
>
<Switch disabled={jobRO} />
</Form.Item>
<Form.Item shouldUpdate>
{() => {
return (
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
name={["cieca_pfl", "LAU", "lbr_taxp"]}
rules={[
{
required: form.getFieldValue(["cieca_pfl", "LAU", "lbr_tax_in"])
//message: t("general.validation.required"),
}
]}
>
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
</Form.Item>
);
}}
</Form.Item>
<Form.Item
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
name={["cieca_pfl", "LAU", "lbr_tx_in1"]}

View File

@@ -23,7 +23,9 @@ export function JobsDetailRatesMaterials({ jobRO, expanded, required = true, for
<Form.Item label={t("jobs.fields.materials.cal_opcode")} name={["materials", "MAPA", "cal_opcode"]}>
<Input disabled={jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.materials.mat_adjp")} name={["materials", "MAPA", "mat_adjp"]}>
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
</Form.Item>
<Form.Item
label={t("jobs.fields.materials.tax_ind")}
name={["materials", "MAPA", "tax_ind"]}
@@ -31,6 +33,24 @@ export function JobsDetailRatesMaterials({ jobRO, expanded, required = true, for
>
<Switch />
</Form.Item>
<Form.Item shouldUpdate>
{() => {
return (
<Form.Item
label={t("jobs.fields.materials.mat_taxp")}
name={["materials", "MAPA", "mat_taxp"]}
rules={[
{
required: form.getFieldValue(["materials", "MAPA", "tax_ind"])
//message: t("general.validation.required"),
}
]}
>
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
</Form.Item>
);
}}
</Form.Item>
<Form.Item
label={t("jobs.fields.materials.mat_tx_in1")}
name={["materials", "MAPA", "mat_tx_in1"]}
@@ -74,7 +94,9 @@ export function JobsDetailRatesMaterials({ jobRO, expanded, required = true, for
<Form.Item label={t("jobs.fields.materials.cal_opcode")} name={["materials", "MASH", "cal_opcode"]}>
<Input disabled={jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.materials.mat_adjp")} name={["materials", "MAPA", "mat_adjp"]}>
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
</Form.Item>
<Form.Item
label={t("jobs.fields.materials.tax_ind")}
name={["materials", "MASH", "tax_ind"]}
@@ -82,6 +104,24 @@ export function JobsDetailRatesMaterials({ jobRO, expanded, required = true, for
>
<Switch />
</Form.Item>
<Form.Item shouldUpdate>
{() => {
return (
<Form.Item
label={t("jobs.fields.materials.mat_taxp")}
name={["materials", "MASH", "mat_taxp"]}
rules={[
{
required: form.getFieldValue(["materials", "MASH", "tax_ind"])
//message: t("general.validation.required"),
}
]}
>
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
</Form.Item>
);
}}
</Form.Item>
<Form.Item
label={t("jobs.fields.materials.mat_tx_in1")}
name={["materials", "MASH", "mat_tx_in1"]}

View File

@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
const mapStateToProps = createStructuredSelector({
@@ -988,18 +989,24 @@ export function JobsDetailRatesParts({ jobRO, expanded, required = true, form })
<Form.Item label={t("jobs.fields.tax_str_rt")} name="tax_str_rt">
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.tax_paint_mat_rt")} name="tax_paint_mat_rt">
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.tax_shop_mat_rt")} name="tax_shop_mat_rt">
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
</Form.Item>
{InstanceRenderManager({ imex: true, rome: false, promanager: "USE_ROME" }) ? (
<>
<Form.Item label={t("jobs.fields.tax_paint_mat_rt")} name="tax_paint_mat_rt">
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.tax_shop_mat_rt")} name="tax_shop_mat_rt">
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
</Form.Item>{" "}
</>
) : null}
<Form.Item label={t("jobs.fields.tax_sub_rt")} name="tax_sub_rt">
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.tax_lbr_rt")} name="tax_lbr_rt">
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
</Form.Item>
{InstanceRenderManager({ imex: true, rome: false, promanager: "USE_ROME" }) ? (
<Form.Item label={t("jobs.fields.tax_lbr_rt")} name="tax_lbr_rt">
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
</Form.Item>
) : null}
<Form.Item label={t("jobs.fields.tax_levies_rt")} name="tax_levies_rt">
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
</Form.Item>

View File

@@ -11,6 +11,7 @@ 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";
@@ -37,7 +38,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}>{record.ro_number || t("general.labels.na")}</Link>
@@ -49,7 +53,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 ? (
@@ -67,7 +70,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} />
},
@@ -75,7 +77,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} />
},
@@ -85,7 +86,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
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");
@@ -100,7 +101,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 ? (
@@ -117,7 +117,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 : "";
@@ -128,7 +128,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 || ""}${record.po_number ? ` (PO: ${record.po_number})` : ""}`
},
@@ -142,8 +142,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
title: t("jobs.fields.clm_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 ? (
@@ -157,7 +156,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>
},
{

View File

@@ -16,19 +16,15 @@ 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 { setJoyRideSteps } from "../../redux/application/application.actions";
import { OwnerNameDisplayFunction } from "./../owner-name-display/owner-name-display.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
setJoyRideSteps: (steps) => dispatch(setJoyRideSteps(steps))
});
const mapDispatchToProps = (dispatch) => ({});
export function JobsList({ bodyshop, setJoyRideSteps }) {
export function JobsList({ bodyshop }) {
const searchParams = queryString.parse(useLocation().search);
const { selected } = searchParams;
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
@@ -315,34 +311,6 @@ export function JobsList({ bodyshop, setJoyRideSteps }) {
title={t("titles.bc.jobs-active")}
extra={
<Space wrap>
{InstanceRenderManager({
promanager: (
<Button
onClick={() =>
setJoyRideSteps([
{
target: "#active-jobs-list",
content: "This is where you will see all work coming in and currently here."
},
{
target: "#header-jobs",
spotlightClicks: true,
disableOverlayClose: true,
content:
"The jobs menu lets you access additional pages to see more information. You can import new jobs, search all jobs, or manage your current production."
},
{
target: "#header-jobs-available",
content: "You can find jobs to import here."
}
])
}
>
Start Walk Through
</Button>
)
})}
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>

View File

@@ -0,0 +1,105 @@
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 { 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";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
});
export default connect(mapStateToProps, mapDispatchToProps)(JobMarkSelectedExported);
export function JobMarkSelectedExported({
bodyshop,
currentUser,
jobIds,
disabled,
loadingCallback,
completedCallback,
refetch,
insertAuditTrail
}) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
const [updateJob] = useMutation(UPDATE_JOBS);
const handleUpdate = async () => {
setLoading(true);
loadingCallback(true);
const result = await updateJob({
variables: {
jobIds: jobIds,
fields: {
status: bodyshop.md_ro_statuses.default_exported || "Exported*",
date_exported: new Date()
}
},
update(cache) {}
});
await insertExportLog({
variables: {
logs: jobIds.map((id) => {
return {
bodyshopid: bodyshop.id,
jobid: id,
successful: true,
message: JSON.stringify([t("general.labels.markedexported")]),
useremail: currentUser.email
};
})
}
});
if (!result.errors) {
notification["success"]({ message: t("jobs.successes.save") });
result.data.update_jobs.returning.forEach((job) => {
console.log("results job", job.id, "audit: ", AuditTrailMapping.admin_jobmarkexported());
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobmarkexported(),
type: "admin_jobmarkexported"
});
});
} else {
notification["error"]({
message: t("jobs.errors.saving", {
error: JSON.stringify(result.errors)
})
});
}
loadingCallback(false);
completedCallback && completedCallback([]);
setLoading(false);
refetch && refetch();
setOpen(false);
};
return (
<Popconfirm
open={open}
title={t("general.labels.areyousure")}
onCancel={() => setOpen(false)}
onConfirm={handleUpdate}
disabled={disabled}
>
<Button loading={loading} disabled={disabled} onClick={() => setOpen(true)} type="primary" danger>
{t("jobs.actions.markasexported")}
</Button>
</Popconfirm>
);
}

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({
@@ -75,7 +76,18 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
})),
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,411 @@
import { DeleteFilled, EyeFilled } from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-layout";
import { useLazyQuery, useMutation } from "@apollo/client";
import { Button, Drawer, Grid, Popconfirm, Space, Table } from "antd";
import queryString from "query-string";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { FaTasks } from "react-icons/fa";
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"
})
),
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
});
export function PartsOrderListTableDrawerComponent({
setBillEnterContext,
bodyshop,
jobRO,
job,
billsQuery,
handleOnRowClick,
setPartsReceiveContext,
setTaskUpsertContext
}) {
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 [returnfrombill, setReturnFromBill] = useState();
const [billData, setBillData] = useState();
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;
useEffect(() => {
if (returnfrombill === null) {
setBillData(null);
} else {
const fetchData = async () => {
const result = await billQuery({
variables: { billid: returnfrombill }
});
setBillData(result.data);
};
fetchData();
}
}, [returnfrombill, billQuery]);
const recordActions = (record, showView = false) => (
<Space direction="horizontal" wrap>
{showView && (
<Button
onClick={() => {
if (record.returnfrombill) {
setReturnFromBill(record.returnfrombill);
} else {
setReturnFromBill(null);
}
handleOnRowClick(record);
}}
>
<EyeFilled />
</Button>
)}
<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) => {
return {
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>
<Button
title={t("tasks.buttons.create")}
onClick={() => {
setTaskUpsertContext({
context: {
jobid: job.id,
partsorderid: record.id
}
});
}}
>
<FaTasks />
</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) => {
return {
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 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={
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,32 +1,23 @@
import { DeleteFilled, EyeFilled, SyncOutlined } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, Card, Checkbox, Drawer, Grid, Input, Popconfirm, Space, Table } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
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 { FaTasks } from "react-icons/fa";
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 FeatureWrapperComponent from "../feature-wrapper/feature-wrapper.component";
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
import PrintWrapper from "../print-wrapper/print-wrapper.component";
import FeatureWrapperComponent from "../feature-wrapper/feature-wrapper.component";
import { FaTasks } from "react-icons/fa";
import PartsOrderDrawer from "./parts-order-list-table-drawer.component";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
@@ -61,19 +52,6 @@ export function PartsOrderListTableComponent({
setPartsReceiveContext,
setTaskUpsertContext
}) {
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 });
@@ -81,10 +59,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 : [];
@@ -93,7 +69,11 @@ export function PartsOrderListTableComponent({
const recordActions = (record, showView = false) => (
<Space direction="horizontal" wrap>
{showView && (
<Button onClick={() => handleOnRowClick(record)}>
<Button
onClick={() => {
handleOnRowClick(record);
}}
>
<EyeFilled />
</Button>
)}
@@ -175,7 +155,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,
@@ -270,147 +250,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
@@ -441,15 +280,13 @@ export function PartsOrderListTableComponent({
}
>
<PartsReceiveModalContainer />
<Drawer
placement="right"
onClose={() => handleOnRowClick(null)}
open={selectedpartsorder}
closable
width={drawerPercentage}
>
{selectedPartsOrderRecord && rowExpander(selectedPartsOrderRecord)}
</Drawer>
<PartsOrderDrawer
job={job}
billsQuery={billsQuery}
handleOnRowClick={handleOnRowClick}
setPartsReceiveContext={setPartsReceiveContext}
setTaskUpsertContext={setTaskUpsertContext}
/>
<Table
loading={billsQuery.loading}
scroll={{

View File

@@ -90,7 +90,7 @@ export function BillMarkSelectedExported({
onConfirm={handleUpdate}
disabled={disabled}
>
<Button loading={loading} disabled={disabled} onClick={() => setOpen(true)}>
<Button loading={loading} disabled={disabled} onClick={() => setOpen(true)} type="primary" danger>
{t("bills.labels.markexported")}
</Button>
</Popconfirm>

View File

@@ -119,8 +119,14 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
return (
<div>
<Descriptions title={t("job_payments.titles.descriptions")} contentStyle={{ fontWeight: "600" }} column={4}>
<Descriptions.Item label={t("job_payments.titles.payer")}>{record.payer}</Descriptions.Item>
<Descriptions
title={t("job_payments.titles.descriptions")}
contentStyle={{ fontWeight: "600" }}
column={4}
>
<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 ?? ""}
</Descriptions.Item>
@@ -132,7 +138,7 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
</Descriptions.Item>
<Descriptions.Item label={t("job_payments.titles.transactionid")}>{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}</Descriptions.Item>
<Descriptions.Item label={t("job_payments.titles.paymentnum")}>{record.paymentnum}</Descriptions.Item>

View File

@@ -90,7 +90,7 @@ export function PaymentMarkSelectedExported({
onConfirm={handleUpdate}
disabled={disabled}
>
<Button loading={loading} disabled={disabled} onClick={() => setOpen(true)}>
<Button loading={loading} disabled={disabled} onClick={() => setOpen(true)} type="primary" danger>
{t("bills.labels.markexported")}
</Button>
</Popconfirm>

View File

@@ -1,22 +1,31 @@
import { Input, Space, Spin } from "antd";
import React from "react";
import { Button, Input, Space, Spin } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { ExclamationCircleFilled, ExclamationCircleOutlined } from "@ant-design/icons";
import { selectBodyshop } from "../../redux/user/user.selectors";
import EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(ProductionBoardFilters);
export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading }) {
const { t } = useTranslation();
const [alertFilter, setAlertFilter] = useState(false);
const toggleAlertFilter = () => {
const newAlertFilter = !alertFilter;
setAlertFilter(newAlertFilter);
setFilter({ ...filter, alert: newAlertFilter });
};
return (
<Space wrap>
@@ -36,6 +45,13 @@ export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading })
onChange={(emp) => setFilter({ ...filter, employeeId: emp })}
allowClear
/>
<Button
type={alertFilter ? "primary" : "default"}
onClick={toggleAlertFilter}
icon={alertFilter ? <ExclamationCircleFilled /> : <ExclamationCircleOutlined />}
>
{t("production.labels.alerts")}
</Button>
</Space>
);
}

View File

@@ -1,11 +0,0 @@
.imex-kanban-card {
padding: 0px !important;
.ant-card-body {
padding: 0.8rem;
}
.ant-card-head {
padding: 0rem 0.8rem;
}
}

View File

@@ -1,201 +0,0 @@
import {
BranchesOutlined,
CalendarOutlined,
DownloadOutlined,
EyeFilled,
PauseCircleOutlined
} from "@ant-design/icons";
import { Card, Col, Row, Space, Tooltip } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import ProductionAlert from "../production-list-columns/production-list-columns.alert.component";
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
import "./production-board-card.styles.scss";
import dayjs from "../../utils/day";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
const cardColor = (ssbuckets, totalHrs) => {
const bucket = ssbuckets.filter((bucket) => bucket.gte <= totalHrs && (!!bucket.lt ? bucket.lt > totalHrs : true))[0];
let color = { r: 255, g: 255, b: 255 };
if (bucket && bucket.color) {
color = bucket.color;
if (bucket.color.rgb) {
color = bucket.color.rgb;
}
}
return color;
};
function getContrastYIQ(bgColor) {
const yiq = (bgColor.r * 299 + bgColor.g * 587 + bgColor.b * 114) / 1000;
return yiq >= 128 ? "black" : "white";
}
export default function ProductionBoardCard(technician, card, bodyshop, cardSettings) {
const { t } = useTranslation();
let employee_body, employee_prep, employee_refinish, employee_csr;
if (card.employee_body) {
employee_body = bodyshop.employees.find((e) => e.id === card.employee_body);
}
if (card.employee_prep) {
employee_prep = bodyshop.employees.find((e) => e.id === card.employee_prep);
}
if (card.employee_refinish) {
employee_refinish = bodyshop.employees.find((e) => e.id === card.employee_refinish);
}
if (card.employee_csr) {
employee_csr = bodyshop.employees.find((e) => e.id === card.employee_csr);
}
// if (card.employee_csr) {
// employee_csr = bodyshop.employees.find((e) => e.id === card.employee_csr);
// }
const pastDueAlert =
!!card.scheduled_completion &&
((dayjs().isSameOrAfter(dayjs(card.scheduled_completion), "day") && "production-completion-past") ||
(dayjs().add(1, "day").isSame(dayjs(card.scheduled_completion), "day") && "production-completion-soon"));
const totalHrs = card.labhrs.aggregate.sum.mod_lb_hrs + card.larhrs.aggregate.sum.mod_lb_hrs;
const bgColor = cardColor(bodyshop.ssbuckets, totalHrs);
return (
<Card
className="react-kanban-card imex-kanban-card"
size="small"
style={{
backgroundColor:
cardSettings && cardSettings.cardcolor && `rgba(${bgColor.r},${bgColor.g},${bgColor.b},${bgColor.a})`,
color: cardSettings && cardSettings.cardcolor && getContrastYIQ(bgColor)
}}
title={
<Space>
<ProductionAlert record={card} key="alert" />
{card.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
{card.iouparent && (
<Tooltip title={t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} />
</Tooltip>
)}
<span style={{ fontWeight: "bolder" }}>
<Link to={technician ? `/tech/joblookup?selected=${card.id}` : `/manage/jobs/${card.id}`}>
{card.ro_number || t("general.labels.na")}
</Link>
</span>
</Space>
}
extra={
<Link to={{ search: `?selected=${card.id}` }}>
<EyeFilled />
</Link>
}
>
<Row>
{cardSettings && cardSettings.ownr_nm && (
<Col span={24}>
{cardSettings && cardSettings.compact ? (
<div className="ellipses">{`${card.ownr_ln || ""} ${card.ownr_co_nm || ""}`}</div>
) : (
<div className="ellipses">
<OwnerNameDisplay ownerObject={card} />
</div>
)}
</Col>
)}
<Col span={24}>
<div className="ellipses">{`${card.v_model_yr || ""} ${
card.v_make_desc || ""
} ${card.v_model_desc || ""}`}</div>
</Col>
{cardSettings && cardSettings.ins_co_nm && card.ins_co_nm && (
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
<div className="ellipses">{card.ins_co_nm || ""}</div>
</Col>
)}
{cardSettings && cardSettings.clm_no && card.clm_no && (
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
<div className="ellipses">{card.clm_no || ""}</div>
</Col>
)}
{cardSettings && cardSettings.employeeassignments && (
<Col span={24}>
<Row>
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`B: ${
employee_body ? `${employee_body.first_name.substr(0, 3)} ${employee_body.last_name.charAt(0)}` : ""
} ${card.labhrs.aggregate.sum.mod_lb_hrs || "?"}h`}</Col>
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`P: ${
employee_prep ? `${employee_prep.first_name.substr(0, 3)} ${employee_prep.last_name.charAt(0)}` : ""
}`}</Col>
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`R: ${
employee_refinish
? `${employee_refinish.first_name.substr(0, 3)} ${employee_refinish.last_name.charAt(0)}`
: ""
} ${card.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`}</Col>
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`C: ${
employee_csr ? `${employee_csr.first_name} ${employee_csr.last_name}` : ""
}`}</Col>
</Row>
</Col>
)}
{/* {cardSettings && cardSettings.laborhrs && (
<Col span={24}>
<Row>
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`B: ${
card.labhrs.aggregate.sum.mod_lb_hrs || "?"
} hrs`}</Col>
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`R: ${
card.larhrs.aggregate.sum.mod_lb_hrs || "?"
} hrs`}</Col>
</Row>
</Col>
)} */}
{cardSettings && cardSettings.actual_in && card.actual_in && (
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
<Space>
<DownloadOutlined />
<DateTimeFormatter format="MM/DD">{card.actual_in}</DateTimeFormatter>
</Space>
</Col>
)}
{cardSettings && cardSettings.scheduled_completion && card.scheduled_completion && (
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
<Space className={pastDueAlert}>
<CalendarOutlined />
<DateTimeFormatter format="MM/DD">{card.scheduled_completion}</DateTimeFormatter>
</Space>
</Col>
)}
{cardSettings && cardSettings.ats && card.alt_transport && (
<Col span={12}>
<div>{card.alt_transport || ""}</div>
</Col>
)}
{cardSettings && cardSettings.sublets && (
<Col span={12}>
<ProductionSubletsManageComponent subletJobLines={card.subletLines} />
</Col>
)}
{cardSettings && cardSettings.production_note && (
<Col span={24}>
{cardSettings && cardSettings.production_note && <ProductionListColumnProductionNote record={card} />}
</Col>
)}
{cardSettings && cardSettings.partsstatus && (
<Col span={24}>
<JobPartsQueueCount parts={card.joblines_status} />
</Col>
)}
</Row>
</Card>
);
}

View File

@@ -0,0 +1,437 @@
import {
BranchesOutlined,
CalendarOutlined,
DownloadOutlined,
EyeFilled,
PauseCircleOutlined
} from "@ant-design/icons";
import { Card, Col, Row, Space, Tooltip } from "antd";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import Dinero from "dinero.js";
import ProductionAlert from "../production-list-columns/production-list-columns.alert.component";
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
import dayjs from "../../utils/day";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
const cardColor = (ssbuckets, totalHrs) => {
const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > totalHrs));
return bucket && bucket.color ? bucket.color.rgb || bucket.color : { r: 255, g: 255, b: 255 };
};
const getContrastYIQ = (bgColor) =>
(bgColor.r * 299 + bgColor.g * 587 + bgColor.b * 114) / 1000 >= 128 ? "black" : "white";
const findEmployeeById = (employees, id) => employees.find((e) => e.id === id);
const EllipsesToolTip = React.memo(({ title, children, kiosk }) => {
if (kiosk || !title) {
return <div className="ellipses no-select">{children}</div>;
}
return (
<Tooltip title={title}>
<div className="ellipses">{children}</div>
</Tooltip>
);
});
const OwnerNameToolTip = ({ metadata, cardSettings }) =>
cardSettings?.ownr_nm && (
<Col span={24}>
<EllipsesToolTip
title={metadata.ownr_ln || metadata.ownr_co_nm ? <OwnerNameDisplay ownerObject={metadata} /> : null}
kiosk={cardSettings.kiosk}
>
{metadata.ownr_ln || metadata.ownr_co_nm ? (
cardSettings.compact ? (
`${metadata.ownr_ln || ""} ${metadata.ownr_co_nm || ""}`
) : (
<OwnerNameDisplay ownerObject={metadata} />
)
) : (
<span>&nbsp;</span>
)}
</EllipsesToolTip>
</Col>
);
const ModelInfoToolTip = ({ metadata, cardSettings }) =>
cardSettings?.model_info && (
<Col span={24}>
<EllipsesToolTip
title={
metadata.v_model_yr || metadata.v_make_desc || metadata.v_model_desc
? `${metadata.v_model_yr || ""} ${metadata.v_make_desc || ""} ${metadata.v_model_desc || ""}`
: null
}
kiosk={cardSettings.kiosk}
>
{metadata.v_model_yr || metadata.v_make_desc || metadata.v_model_desc ? (
`${metadata.v_model_yr || ""} ${metadata.v_make_desc || ""} ${metadata.v_model_desc || ""}`
) : (
<span>&nbsp;</span>
)}
</EllipsesToolTip>
</Col>
);
const InsuranceCompanyToolTip = ({ metadata, cardSettings }) =>
cardSettings?.ins_co_nm && (
<Col span={cardSettings.compact ? 24 : 12}>
<EllipsesToolTip title={metadata.ins_co_nm || null} kiosk={cardSettings.kiosk}>
{metadata.ins_co_nm ? metadata.ins_co_nm : <span>&nbsp;</span>}
</EllipsesToolTip>
</Col>
);
const ClaimNumberToolTip = ({ metadata, cardSettings }) =>
cardSettings?.clm_no && (
<Col span={cardSettings.compact ? 24 : 12}>
<EllipsesToolTip title={metadata.clm_no || null} kiosk={cardSettings.kiosk}>
{metadata.clm_no ? metadata.clm_no : <span>&nbsp;</span>}
</EllipsesToolTip>
</Col>
);
const EmployeeAssignmentsToolTip = ({
metadata,
cardSettings,
employee_body,
employee_prep,
employee_refinish,
employee_csr
}) =>
cardSettings?.employeeassignments && (
<Col span={24}>
<Row>
<Col span={cardSettings.compact ? 24 : 12}>
<EllipsesToolTip
title={
employee_body || metadata.labhrs.aggregate.sum.mod_lb_hrs
? `B: ${employee_body ? `${employee_body.first_name.substring(0, 3)} ${employee_body.last_name.charAt(0)}` : ""} ${metadata.labhrs.aggregate.sum.mod_lb_hrs || "?"}h`
: null
}
kiosk={cardSettings.kiosk}
>
{employee_body || metadata.labhrs.aggregate.sum.mod_lb_hrs ? (
`B: ${employee_body ? `${employee_body.first_name.substring(0, 3)} ${employee_body.last_name.charAt(0)}` : ""} ${metadata.labhrs.aggregate.sum.mod_lb_hrs || "?"}h`
) : (
<span>&nbsp;</span>
)}
</EllipsesToolTip>
</Col>
<Col span={cardSettings.compact ? 24 : 12}>
<EllipsesToolTip
title={
employee_prep
? `P: ${employee_prep ? `${employee_prep.first_name.substring(0, 3)} ${employee_prep.last_name.charAt(0)}` : ""}`
: null
}
kiosk={cardSettings.kiosk}
>
{employee_prep ? (
`P: ${employee_prep ? `${employee_prep.first_name.substring(0, 3)} ${employee_prep.last_name.charAt(0)}` : ""}`
) : (
<span>&nbsp;</span>
)}
</EllipsesToolTip>
</Col>
<Col span={cardSettings.compact ? 24 : 12}>
<EllipsesToolTip
title={
employee_refinish || metadata.larhrs.aggregate.sum.mod_lb_hrs
? `R: ${employee_refinish ? `${employee_refinish.first_name.substring(0, 3)} ${employee_refinish.last_name.charAt(0)}` : ""} ${metadata.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`
: null
}
kiosk={cardSettings.kiosk}
>
{employee_refinish || metadata.larhrs.aggregate.sum.mod_lb_hrs ? (
`R: ${employee_refinish ? `${employee_refinish.first_name.substring(0, 3)} ${employee_refinish.last_name.charAt(0)}` : ""} ${metadata.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`
) : (
<span>&nbsp;</span>
)}
</EllipsesToolTip>
</Col>
<Col span={cardSettings.compact ? 24 : 12}>
<EllipsesToolTip
title={
employee_csr ? `C: ${employee_csr ? `${employee_csr.first_name} ${employee_csr.last_name}` : ""}` : null
}
kiosk={cardSettings.kiosk}
>
{employee_csr ? (
`C: ${employee_csr ? `${employee_csr.first_name} ${employee_csr.last_name}` : ""}`
) : (
<span>&nbsp;</span>
)}
</EllipsesToolTip>
</Col>
</Row>
</Col>
);
const ActualInToolTip = ({ metadata, cardSettings }) =>
cardSettings?.actual_in && (
<Col span={cardSettings.compact ? 24 : 12}>
<EllipsesToolTip title={metadata.actual_in || null} kiosk={cardSettings.kiosk}>
{metadata.actual_in ? (
<Space>
<DownloadOutlined />
<DateTimeFormatter format="MM/DD">{metadata.actual_in}</DateTimeFormatter>
</Space>
) : (
<span>&nbsp;</span>
)}
</EllipsesToolTip>
</Col>
);
const EstimatorToolTip = ({ metadata, cardSettings }) => {
return (
cardSettings?.estimator && (
<Col span={cardSettings.compact ? 24 : 12}>
<EllipsesToolTip
title={metadata.est_ct_fn && metadata.est_ct_ln ? `${metadata.est_ct_fn} ${metadata.est_ct_ln}` : null}
kiosk={cardSettings.kiosk}
>
{metadata.est_ct_fn && metadata.est_ct_ln ? (
<span>E: {`${metadata.est_ct_fn} ${metadata.est_ct_ln}`}</span>
) : (
<span>&nbsp;</span>
)}
</EllipsesToolTip>
</Col>
)
);
};
const SubtotalTooltip = ({ metadata, cardSettings, t }) => {
const amount = metadata?.job_totals?.totals?.subtotal?.amount;
const dineroAmount = amount ? Dinero({ amount: parseInt(amount * 100) }).toFormat("0,0.00") : null;
return (
cardSettings?.subtotal && (
<Col span={cardSettings.compact ? 24 : 12}>
<EllipsesToolTip
title={!!amount ? `${t("production.statistics.currency_symbol")}${dineroAmount}` : null}
kiosk={cardSettings.kiosk}
>
{!!amount ? (
<span>{`${t("production.statistics.currency_symbol")}${dineroAmount}`}</span>
) : (
<span>&nbsp;</span>
)}
</EllipsesToolTip>
</Col>
)
);
};
const ScheduledCompletionToolTip = ({ metadata, cardSettings, pastDueAlert }) =>
cardSettings?.scheduled_completion && (
<Col span={cardSettings.compact ? 24 : 12}>
<EllipsesToolTip title={metadata.scheduled_completion || null} kiosk={cardSettings.kiosk}>
{metadata.scheduled_completion ? (
<Space className={pastDueAlert}>
<CalendarOutlined />
<DateTimeFormatter format="MM/DD">{metadata.scheduled_completion}</DateTimeFormatter>
</Space>
) : (
<span>&nbsp;</span>
)}
</EllipsesToolTip>
</Col>
);
const AltTransportToolTip = ({ metadata, cardSettings }) =>
cardSettings?.ats && (
<Col span={12}>
<EllipsesToolTip title={metadata.alt_transport || null} kiosk={cardSettings.kiosk}>
{metadata.alt_transport ? metadata.alt_transport : <span>&nbsp;</span>}
</EllipsesToolTip>
</Col>
);
const SubletsComponent = ({ metadata, cardSettings }) =>
cardSettings?.sublets && (
<Col span={12}>
{metadata.subletLines ? (
<ProductionSubletsManageComponent subletJobLines={metadata.subletLines} />
) : (
<span>&nbsp;</span>
)}
</Col>
);
const ProductionNoteComponent = ({ metadata, cardSettings, card }) =>
cardSettings?.production_note && (
<Col span={24} style={{ margin: "2px 0" }}>
<ProductionListColumnProductionNote
record={{
production_vars: metadata?.production_vars,
id: card?.id,
refetch: card?.refetch
}}
/>
</Col>
);
const PartsStatusComponent = ({ metadata, cardSettings }) =>
cardSettings?.partsstatus && (
<Col span={24} style={{ textAlign: "center" }}>
{metadata.joblines_status ? <JobPartsQueueCount parts={metadata.joblines_status} /> : <span>&nbsp;</span>}
</Col>
);
const TasksToolTip = ({ metadata, cardSettings, t }) =>
cardSettings?.tasks && (
<Col span={12}>
<EllipsesToolTip
title={`${t("production.labels.tasks")}: ${metadata.tasks_aggregate?.aggregate?.count || 0}`}
kiosk={cardSettings.kiosk}
>
{metadata.tasks_aggregate?.aggregate?.count ? (
`T: ${metadata.tasks_aggregate.aggregate.count}`
) : (
<span>T: 0</span>
)}
</EllipsesToolTip>
</Col>
);
export default function ProductionBoardCard({ technician, card, bodyshop, cardSettings, clone }) {
const { t } = useTranslation();
const { metadata } = card;
const employees = useMemo(() => bodyshop.employees, [bodyshop.employees]);
const { employee_body, employee_prep, employee_refinish, employee_csr } = useMemo(() => {
return {
employee_body: metadata?.employee_body && findEmployeeById(employees, metadata.employee_body),
employee_prep: metadata?.employee_prep && findEmployeeById(employees, metadata.employee_prep),
employee_refinish: metadata?.employee_refinish && findEmployeeById(employees, metadata.employee_refinish),
employee_csr: metadata?.employee_csr && findEmployeeById(employees, metadata.employee_csr)
};
}, [metadata, employees]);
const pastDueAlert = useMemo(() => {
if (!metadata?.scheduled_completion) return null;
const completionDate = dayjs(metadata.scheduled_completion);
if (dayjs().isSameOrAfter(completionDate, "day")) return "production-completion-past";
if (dayjs().add(1, "day").isSame(completionDate, "day")) return "production-completion-soon";
return null;
}, [metadata?.scheduled_completion]);
const totalHrs = useMemo(() => {
return metadata?.labhrs && metadata?.larhrs
? metadata.labhrs.aggregate.sum.mod_lb_hrs + metadata.larhrs.aggregate.sum.mod_lb_hrs
: 0;
}, [metadata?.labhrs, metadata?.larhrs]);
const bgColor = useMemo(() => cardColor(bodyshop.ssbuckets, totalHrs), [bodyshop.ssbuckets, totalHrs]);
const contrastYIQ = useMemo(() => getContrastYIQ(bgColor), [bgColor]);
const isBodyEmpty = useMemo(() => {
return !(
cardSettings?.ownr_nm ||
cardSettings?.model_info ||
cardSettings?.ins_co_nm ||
cardSettings?.clm_no ||
cardSettings?.employeeassignments ||
cardSettings?.actual_in ||
cardSettings?.scheduled_completion ||
cardSettings?.ats ||
cardSettings?.sublets ||
cardSettings?.production_note ||
cardSettings?.partsstatus ||
cardSettings?.estimator ||
cardSettings?.subtotal ||
cardSettings?.tasks
);
}, [cardSettings]);
const headerContent = (
<div className="header-content-container">
<div className="inner-container">
<ProductionAlert id={card.id} productionVars={metadata?.production_vars} refetch={card?.refetch} key="alert" />
{metadata?.suspended && <PauseCircleOutlined className="circle-outline" key="suspended" />}
{metadata?.iouparent && (
<EllipsesToolTip
title={t("jobs.labels.iou")}
key="iouparent"
className="iouparent"
kiosk={cardSettings.kiosk}
>
<BranchesOutlined className="branches-outlined" />
</EllipsesToolTip>
)}
</div>
<span className="tech-container">
<Link to={technician ? `/tech/joblookup?selected=${card.id}` : `/manage/jobs/${card.id}`}>
{metadata?.ro_number || t("general.labels.na")}
</Link>
</span>
{isBodyEmpty && (
<div className="body-empty-container">
<Link to={{ search: `?selected=${card.id}` }}>
<EyeFilled />
</Link>
</div>
)}
</div>
);
const bodyContent = (
<Row>
<OwnerNameToolTip metadata={metadata} cardSettings={cardSettings} />
<ModelInfoToolTip metadata={metadata} cardSettings={cardSettings} />
<InsuranceCompanyToolTip metadata={metadata} cardSettings={cardSettings} />
<ClaimNumberToolTip metadata={metadata} cardSettings={cardSettings} />
<EmployeeAssignmentsToolTip
metadata={metadata}
cardSettings={cardSettings}
employee_body={employee_body}
employee_prep={employee_prep}
employee_refinish={employee_refinish}
employee_csr={employee_csr}
/>
<EstimatorToolTip metadata={metadata} cardSettings={cardSettings} />
<TasksToolTip metadata={metadata} cardSettings={cardSettings} t={t} />
<SubtotalTooltip metadata={metadata} cardSettings={cardSettings} t={t} />
<ActualInToolTip metadata={metadata} cardSettings={cardSettings} />
<ScheduledCompletionToolTip metadata={metadata} cardSettings={cardSettings} pastDueAlert={pastDueAlert} />
<AltTransportToolTip metadata={metadata} cardSettings={cardSettings} />
<SubletsComponent metadata={metadata} cardSettings={cardSettings} />
<ProductionNoteComponent metadata={metadata} cardSettings={cardSettings} card={card} />
<PartsStatusComponent metadata={metadata} cardSettings={cardSettings} />
</Row>
);
return (
<Card
className={`react-trello-card ${cardSettings.kiosk ? "kiosk-mode" : ""}`}
size="small"
style={{
backgroundColor: cardSettings?.cardcolor && `rgba(${bgColor.r},${bgColor.g},${bgColor.b},${bgColor.a})`,
color: cardSettings?.cardcolor && contrastYIQ
}}
title={!isBodyEmpty ? headerContent : null}
extra={
!isBodyEmpty && (
<Link to={{ search: `?selected=${card.id}` }}>
<EyeFilled />
</Link>
)
}
>
{isBodyEmpty ? headerContent : bodyContent}
</Card>
);
}

View File

@@ -1,125 +0,0 @@
import { useMutation } from "@apollo/client";
import { Button, Card, Col, Form, notification, Popover, Row, Switch } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_KANBAN_SETTINGS } from "../../graphql/user.queries";
export default function ProductionBoardKanbanCardSettings({ associationSettings }) {
const [form] = Form.useForm();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [updateKbSettings] = useMutation(UPDATE_KANBAN_SETTINGS);
useEffect(() => {
form.setFieldsValue(associationSettings && associationSettings.kanban_settings);
}, [form, associationSettings, open]);
const { t } = useTranslation();
const handleFinish = async (values) => {
setLoading(true);
const result = await updateKbSettings({
variables: {
id: associationSettings && associationSettings.id,
ks: values
}
});
if (result.errors) {
notification.open({
type: "error",
message: t("production.errors.settings", {
error: JSON.stringify(result.errors)
})
});
}
setOpen(false);
setLoading(false);
};
const overlay = (
<div>
<Card>
<Form form={form} onFinish={handleFinish} layout="vertical">
<Row gutter={[16, 16]}>
<Col span={12}>
<Form.Item label={t("production.labels.compact")} name="compact" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item valuePropName="checked" label={t("production.labels.ownr_nm")} name="ownr_nm">
<Switch />
</Form.Item>
<Form.Item valuePropName="checked" label={t("production.labels.clm_no")} name="clm_no">
<Switch />
</Form.Item>
<Form.Item valuePropName="checked" label={t("production.labels.ins_co_nm")} name="ins_co_nm">
<Switch />
</Form.Item>
{/* <Form.Item
valuePropName="checked"
label={t("production.labels.laborhrs")}
name="laborhrs"
>
<Switch />
</Form.Item> */}
<Form.Item
valuePropName="checked"
label={t("production.labels.employeeassignments")}
name="employeeassignments"
>
<Switch />
</Form.Item>
<Form.Item valuePropName="checked" label={t("production.labels.actual_in")} name="actual_in">
<Switch />
</Form.Item>
<Form.Item valuePropName="checked" label={t("production.labels.cardcolor")} name="cardcolor">
<Switch />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
valuePropName="checked"
label={t("production.labels.scheduled_completion")}
name="scheduled_completion"
>
<Switch />
</Form.Item>
<Form.Item valuePropName="checked" label={t("production.labels.ats")} name="ats">
<Switch />
</Form.Item>
<Form.Item valuePropName="checked" label={t("production.labels.production_note")} name="production_note">
<Switch />
</Form.Item>
{/* <Form.Item
valuePropName='checked' label={t("production.labels.alert")} name="alert">
<Switch/>
</Form.Item> */}
<Form.Item valuePropName="checked" label={t("production.labels.sublets")} name="sublets">
<Switch />
</Form.Item>
<Form.Item valuePropName="checked" label={t("production.labels.partsstatus")} name="partsstatus">
<Switch />
</Form.Item>
<Form.Item valuePropName="checked" label={t("production.labels.stickyheader")} name="stickyheader">
<Switch />
</Form.Item>
</Col>
</Row>
</Form>
<Button
onClick={() => {
form.submit();
}}
>
{t("general.actions.save")}
</Button>
</Card>
</div>
);
return (
<Popover content={overlay} open={open} placement="topRight">
<Button loading={loading} onClick={() => setOpen(true)}>
{t("production.labels.cardsettings")}
</Button>
</Popover>
);
}

View File

@@ -1,265 +1,235 @@
import { SyncOutlined } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client";
import Board, { moveCard } from "@asseinfo/react-kanban";
import { Button, Grid, notification, Space, Statistic } from "antd";
import Board from "./trello-board/index";
import { Button, notification, Skeleton, Space } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Sticky, StickyContainer } from "react-sticky";
import { createStructuredSelector } from "reselect";
import styled from "styled-components";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { generate_UPDATE_JOB_KANBAN } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import IndefiniteLoading from "../indefinite-loading/indefinite-loading.component";
import ProductionBoardFilters from "../production-board-filters/production-board-filters.component";
import ProductionBoardCard from "../production-board-kanban-card/production-board-kanban-card.component";
import ProductionListDetailComponent from "../production-list-detail/production-list-detail.component";
import ProductionBoardKanbanCardSettings from "./production-board-kanban.card-settings.component";
//import "@asseinfo/react-kanban/dist/styles.css";
import CardColorLegend from "../production-board-kanban-card/production-board-kanban-card-color-legend.component";
import CardColorLegend from "./production-board-kanban-card-color-legend.component.jsx";
import "./production-board-kanban.styles.scss";
import { createBoardData } from "./production-board-kanban.utils.js";
import ProductionBoardKanbanSettings from "./settings/production-board-kanban.settings.component.jsx";
import cloneDeep from "lodash/cloneDeep";
import isEqual from "lodash/isEqual";
import { defaultFilters, mergeWithDefaults } from "./settings/defaultKanbanSettings.js";
import NoteUpsertModal from "../../components/note-upsert-modal/note-upsert-modal.container";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
technician: selectTechnician
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(
insertAuditTrail({
jobid,
operation,
type
})
)
});
export function ProductionBoardKanbanComponent({
data,
bodyshop,
refetch,
technician,
insertAuditTrail,
associationSettings
}) {
const [boardLanes, setBoardLanes] = useState({
columns: [{ id: "Loading...", title: "Loading...", cards: [] }]
});
const [filter, setFilter] = useState({ search: "", employeeId: null });
function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTrail, associationSettings, statuses }) {
const [boardLanes, setBoardLanes] = useState({ lanes: [] });
const [filter, setFilter] = useState(defaultFilters);
const [loading, setLoading] = useState(true);
const [isMoving, setIsMoving] = useState(false);
const [orientation, setOrientation] = useState("vertical");
const { t } = useTranslation();
useEffect(() => {
const boardData = createBoardData(
[...bodyshop.md_ro_statuses.production_statuses, ...(bodyshop.md_ro_statuses.additional_board_statuses || [])],
data,
filter
);
boardData.columns = boardData.columns.map((d) => {
return { ...d, title: `${d.title} (${d.cards.length})` };
});
setBoardLanes(boardData);
setIsMoving(false);
}, [data, setBoardLanes, setIsMoving, bodyshop.md_ro_statuses, filter]);
const client = useApolloClient();
const handleDragEnd = async (card, source, destination) => {
logImEXEvent("kanban_drag_end");
useEffect(() => {
if (associationSettings) {
setLoading(true);
setOrientation(associationSettings?.kanban_settings?.orientation ? "vertical" : "horizontal");
setLoading(false);
}
}, [associationSettings]);
useEffect(() => {
setIsMoving(true);
setBoardLanes(moveCard(boardLanes, source, destination));
const sameColumnTransfer = source.fromColumnId === destination.toColumnId;
const sourceColumn = boardLanes.columns.find((x) => x.id === source.fromColumnId);
const destinationColumn = boardLanes.columns.find((x) => x.id === destination.toColumnId);
const movedCardWillBeFirst = destination.toPosition === 0;
const movedCardWillBeLast = destinationColumn.cards.length - destination.toPosition < 1;
const lastCardInDestinationColumn = destinationColumn.cards[destinationColumn.cards.length - 1];
const oldChildCard = sourceColumn.cards[source.fromPosition + 1];
const newChildCard = movedCardWillBeLast
? null
: destinationColumn.cards[
sameColumnTransfer
? source.fromPosition - destination.toPosition > 0
? destination.toPosition
: destination.toPosition + 1
: destination.toPosition
];
const oldChildCardNewParent = oldChildCard ? card.kanbanparent : null;
let movedCardNewKanbanParent;
if (movedCardWillBeFirst) {
//console.log("==> New Card is first.");
movedCardNewKanbanParent = "-1";
} else if (movedCardWillBeLast) {
// console.log("==> New Card is last.");
movedCardNewKanbanParent = lastCardInDestinationColumn.id;
} else if (!!newChildCard) {
// console.log("==> New Card is somewhere in the middle");
movedCardNewKanbanParent = newChildCard.kanbanparent;
} else {
console.log("==> !!!!!!Couldn't find a parent.!!!! <==");
}
const newChildCardNewParent = newChildCard ? card.id : null;
const update = await client.mutate({
mutation: generate_UPDATE_JOB_KANBAN(
oldChildCard ? oldChildCard.id : null,
oldChildCardNewParent,
card.id,
movedCardNewKanbanParent,
destination.toColumnId,
newChildCard ? newChildCard.id : null,
newChildCardNewParent
)
});
insertAuditTrail({
jobid: card.id,
operation: AuditTrailMapping.jobstatuschange(destination.toColumnId),
type: "jobstatuschange"
const newBoardData = createBoardData({
statuses,
data,
filter,
cardSettings: associationSettings?.kanban_settings
});
if (update.errors) {
notification["error"]({
message: t("production.errors.boardupdate", {
message: JSON.stringify(update.errors)
})
});
newBoardData.lanes = newBoardData.lanes.map((lane) => ({
...lane,
title: `${lane.title} (${lane.cards.length})`
}));
setBoardLanes((prevBoardLanes) => {
const deepClonedData = cloneDeep(newBoardData);
if (!isEqual(prevBoardLanes, deepClonedData)) {
return deepClonedData;
}
return prevBoardLanes;
});
setIsMoving(false);
}, [data, bodyshop.md_ro_statuses, filter, statuses, associationSettings?.kanban_settings]);
const getCardByID = useCallback((data, cardId) => {
for (const lane of data.lanes) {
for (const card of lane.cards) {
if (card.id === cardId) {
return card;
}
}
}
return null;
}, []);
const onDragEnd = useCallback(
async ({ type, source, destination, draggableId }) => {
logImEXEvent("kanban_drag_end");
if (!type || type !== "lane" || !source || !destination || isMoving) return;
setIsMoving(true);
const targetLane = boardLanes.lanes.find((lane) => lane.id === destination.droppableId);
const sourceLane = boardLanes.lanes.find((lane) => lane.id === source.droppableId);
if (!targetLane || !sourceLane) {
setIsMoving(false);
console.error("Invalid source or destination lane");
return;
}
const sameColumnTransfer = source.droppableId === destination.droppableId;
const sourceCard = getCardByID(boardLanes, draggableId);
const movedCardWillBeFirst = destination.index === 0;
const movedCardWillBeLast = destination.index >= targetLane.cards.length - 1;
const lastCardInTargetLane = targetLane.cards[targetLane.cards.length - 1];
const oldChildCard = sourceLane.cards[source.index + 1];
const newChildCard = movedCardWillBeLast
? null
: targetLane.cards[
sameColumnTransfer
? source.index < destination.index
? destination.index + 1
: destination.index
: destination.index
];
const oldChildCardNewParent = oldChildCard ? sourceCard.metadata.kanbanparent : null;
let movedCardNewKanbanParent;
if (movedCardWillBeFirst) {
movedCardNewKanbanParent = "-1";
} else if (movedCardWillBeLast) {
movedCardNewKanbanParent = lastCardInTargetLane.id;
} else if (newChildCard) {
movedCardNewKanbanParent = newChildCard.metadata.kanbanparent;
} else {
console.error("==> !!!!!!Couldn't find a parent.!!!! <==");
}
const newChildCardNewParent = newChildCard ? draggableId : null;
try {
const update = await client.mutate({
mutation: generate_UPDATE_JOB_KANBAN(
oldChildCard ? oldChildCard.id : null,
oldChildCardNewParent,
draggableId,
movedCardNewKanbanParent,
targetLane.id,
newChildCard ? newChildCard.id : null,
newChildCardNewParent
)
});
insertAuditTrail({
jobid: draggableId,
operation: AuditTrailMapping.jobstatuschange(targetLane.id),
type: "jobstatuschange"
});
if (update.errors) {
notification["error"]({
message: t("production.errors.boardupdate", {
message: JSON.stringify(update.errors)
})
});
}
} catch (error) {
notification["error"]({
message: t("production.errors.boardupdate", {
message: error.message
})
});
} finally {
setIsMoving(false);
}
},
[boardLanes, client, getCardByID, isMoving, t, insertAuditTrail]
);
const cardSettings = useMemo(() => {
const kanbanSettings = associationSettings?.kanban_settings;
return mergeWithDefaults(kanbanSettings);
}, [associationSettings]);
const handleSettingsChange = () => {
setFilter(defaultFilters);
};
const totalHrs = data
.reduce(
(acc, val) => acc + (val.labhrs?.aggregate?.sum?.mod_lb_hrs || 0) + (val.larhrs?.aggregate?.sum?.mod_lb_hrs || 0),
0
)
.toFixed(1);
const totalLAB = data.reduce((acc, val) => acc + (val.labhrs?.aggregate?.sum?.mod_lb_hrs || 0), 0).toFixed(1);
const totalLAR = data.reduce((acc, val) => acc + (val.larhrs?.aggregate?.sum?.mod_lb_hrs || 0), 0).toFixed(1);
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const standardSizes = {
xs: "250",
sm: "250",
md: "250",
lg: "250",
xl: "250",
xxl: "250"
};
const compactSizes = {
xs: "150",
sm: "150",
md: "150",
lg: "150",
xl: "155",
xxl: "155"
};
const width = selectedBreakpoint
? associationSettings && associationSettings.kanban_settings && associationSettings.kanban_settings.compact
? compactSizes[selectedBreakpoint[0]]
: standardSizes[selectedBreakpoint[0]]
: "250";
const stickyHeader = {
renderColumnHeader: ({ title }) => (
<Sticky>
{({
style,
// the following are also available but unused in this example
isSticky,
wasSticky,
distanceFromTop,
distanceFromBottom,
calculatedHeight
}) => (
<div className="react-kanban-column-header" style={{ ...style, zIndex: "99", backgroundColor: "#ddd" }}>
{title}
</div>
)}
</Sticky>
)
};
const cardSettings =
associationSettings &&
associationSettings.kanban_settings &&
Object.keys(associationSettings.kanban_settings).length > 0
? associationSettings.kanban_settings
: {
ats: true,
clm_no: true,
compact: false,
ownr_nm: true,
sublets: true,
ins_co_nm: true,
production_note: true,
employeeassignments: true,
scheduled_completion: true,
stickyheader: false,
cardcolor: false
};
if (loading) {
return <Skeleton active />;
}
return (
<Container width={width}>
<div>
<IndefiniteLoading loading={isMoving} />
<PageHeader
title={
<Space>
<Statistic title={t("dashboard.titles.productionhours")} value={totalHrs} />
<Statistic title={t("dashboard.titles.labhours")} value={totalLAB} />
<Statistic title={t("dashboard.titles.larhours")} value={totalLAR} />
<Statistic title={t("appointments.labels.inproduction")} value={data && data.length} />
</Space>
}
title={cardSettings.cardcolor && <CardColorLegend cardSettings={cardSettings} bodyshop={bodyshop} />}
style={{ paddingInline: 0, paddingBlock: 0 }}
extra={
<Space wrap>
<Button onClick={() => refetch && refetch()}>
<SyncOutlined />
</Button>
<ProductionBoardFilters filter={filter} setFilter={setFilter} loading={isMoving} />
<ProductionBoardKanbanCardSettings associationSettings={associationSettings} />
<ProductionBoardKanbanSettings
parentLoading={setLoading}
associationSettings={associationSettings}
onSettingsChange={handleSettingsChange}
bodyshop={bodyshop}
data={data}
/>
</Space>
}
/>
{cardSettings.cardcolor && <CardColorLegend cardSettings={cardSettings} bodyshop={bodyshop} />}
<NoteUpsertModal />
<ProductionListDetailComponent jobs={data} />
<StickyContainer>
<Board
style={{ height: "100%" }}
children={boardLanes}
disableCardDrag={isMoving}
{...(cardSettings.stickyheader && stickyHeader)}
renderCard={(card) => ProductionBoardCard(technician, card, bodyshop, cardSettings)}
onCardDragEnd={handleDragEnd}
/>
</StickyContainer>
</Container>
<Board
queryData={data}
data={boardLanes}
onDragEnd={onDragEnd}
orientation={orientation}
cardSettings={cardSettings}
/>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(ProductionBoardKanbanComponent);
const Container = styled.div`
.react-kanban-card-skeleton,
.react-kanban-card,
.react-kanban-card-adder-form {
box-sizing: border-box;
max-width: ${(props) => props.width}px;
min-width: ${(props) => props.width}px;
}
`;

View File

@@ -1,14 +1,8 @@
import { useApolloClient, useQuery, useSubscription } from "@apollo/client";
import _ from "lodash";
import React, { useEffect, useState } from "react";
import React, { useEffect, useMemo } from "react";
import { useQuery, useSubscription } from "@apollo/client";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
QUERY_EXACT_JOB_IN_PRODUCTION,
QUERY_EXACT_JOBS_IN_PRODUCTION,
QUERY_JOBS_IN_PRODUCTION,
SUBSCRIPTION_JOBS_IN_PRODUCTION
} from "../../graphql/jobs.queries";
import { QUERY_JOBS_IN_PRODUCTION, SUBSCRIPTION_JOBS_IN_PRODUCTION } from "../../graphql/jobs.queries";
import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import ProductionBoardKanbanComponent from "./production-board-kanban.component";
@@ -18,76 +12,53 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser
});
export function ProductionBoardKanbanContainer({ bodyshop, currentUser }) {
function ProductionBoardKanbanContainer({ bodyshop, currentUser }) {
const combinedStatuses = useMemo(
() => [
...bodyshop.md_ro_statuses.production_statuses,
...(bodyshop.md_ro_statuses.additional_board_statuses || [])
],
[bodyshop.md_ro_statuses.production_statuses, bodyshop.md_ro_statuses.additional_board_statuses]
);
const { refetch, loading, data } = useQuery(QUERY_JOBS_IN_PRODUCTION, {
pollInterval: 3600000,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only"
nextFetchPolicy: "network-only",
onError: (error) => console.error(`Error fetching jobs in production: ${error.message}`)
});
const client = useApolloClient();
const [joblist, setJoblist] = useState([]);
const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION);
useEffect(() => {
if (!(data && data.jobs)) return;
setJoblist(
data.jobs.map((j) => {
return { id: j.id, updated_at: j.updated_at };
})
);
}, [data]);
useEffect(() => {
if (!updatedJobs || joblist.length === 0) return;
const jobDiff = _.differenceWith(
joblist,
updatedJobs.jobs,
(a, b) => a.id === b.id && a.updated_at === b.updated_at
);
jobDiff.forEach((job) => {
getUpdatedJobData(job.id);
});
if (jobDiff.length > 1) {
getUpdatedJobsData(jobDiff.map((j) => j.id));
} else if (jobDiff.length === 1) {
jobDiff.forEach((job) => {
getUpdatedJobData(job.id);
});
}
setJoblist(updatedJobs.jobs);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [updatedJobs]);
const getUpdatedJobData = async (jobId) => {
client.query({
query: QUERY_EXACT_JOB_IN_PRODUCTION,
variables: { id: jobId }
});
};
const getUpdatedJobsData = async (jobIds) => {
client.query({
query: QUERY_EXACT_JOBS_IN_PRODUCTION,
variables: { ids: jobIds }
});
};
const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION, {
onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
});
const { loading: associationSettingsLoading, data: associationSettings } = useQuery(QUERY_KANBAN_SETTINGS, {
variables: { email: currentUser.email }
variables: { email: currentUser.email },
onError: (error) => console.error(`Error fetching Kanban settings: ${error.message}`)
});
// const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
useEffect(() => {
if (updatedJobs && data) {
refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`));
}
}, [updatedJobs, data, refetch]);
const filteredAssociationSettings = useMemo(() => {
return associationSettings?.associations[0] || null;
}, [associationSettings]);
return (
<ProductionBoardKanbanComponent
loading={loading || associationSettingsLoading}
data={data ? data.jobs : []}
refetch={refetch}
associationSettings={
associationSettings && associationSettings.associations[0] ? associationSettings.associations[0] : null
}
associationSettings={filteredAssociationSettings}
bodyshop={bodyshop}
statuses={combinedStatuses}
/>
);
}
export default connect(mapStateToProps, null)(ProductionBoardKanbanContainer);
export default connect(mapStateToProps)(ProductionBoardKanbanContainer);

View File

@@ -0,0 +1,215 @@
import React, { useMemo } from "react";
import { Card, Statistic } from "antd";
import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";
import { defaultKanbanSettings, statisticsItems } from "./settings/defaultKanbanSettings.js";
export const StatisticType = {
HOURS: "hours",
AMOUNT: "amount",
JOBS: "jobs",
TASKS: "tasks"
};
const mergeStatistics = (items, values) => {
const valuesMap = values.reduce((acc, value) => {
acc[value.id] = value;
return acc;
}, {});
return items.map((item) => ({
...item,
value: valuesMap[item.id]?.value,
type: valuesMap[item.id]?.type
}));
};
const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
const { t } = useTranslation();
const calculateTotal = (items, key, subKey) => {
return items.reduce((acc, item) => acc + (item[key]?.aggregate?.sum?.[subKey] || 0), 0);
};
const calculateTotalAmount = (items, key) => {
return items.reduce((acc, item) => acc + (item[key]?.totals?.subtotal?.amount || 0), 0);
};
const calculateReducerTotal = (lanes, key, subKey) => {
return lanes.reduce((acc, lane) => {
return (
acc + lane.cards.reduce((laneAcc, card) => laneAcc + (card.metadata[key]?.aggregate?.sum?.[subKey] || 0), 0)
);
}, 0);
};
const calculateReducerTotalAmount = (lanes, key) => {
return lanes.reduce((acc, lane) => {
return (
acc + lane.cards.reduce((laneAcc, card) => laneAcc + (card.metadata[key]?.totals?.subtotal?.amount || 0), 0)
);
}, 0);
};
const formatValue = (value, type) => {
if (type === StatisticType.JOBS) {
return value.toFixed(0);
}
if (type === StatisticType.HOURS) {
return value.toFixed(2);
}
return value;
};
const totalHrs = useMemo(() => {
if (!cardSettings.totalHrs) return null;
const total = calculateTotal(data, "labhrs", "mod_lb_hrs") + calculateTotal(data, "larhrs", "mod_lb_hrs");
return parseFloat(total.toFixed(2));
}, [data, cardSettings.totalHrs]);
const totalLAB = useMemo(() => {
if (!cardSettings.totalLAB) return null;
const total = calculateTotal(data, "labhrs", "mod_lb_hrs");
return parseFloat(total.toFixed(2));
}, [data, cardSettings.totalLAB]);
const totalLAR = useMemo(() => {
if (!cardSettings.totalLAR) return null;
const total = calculateTotal(data, "larhrs", "mod_lb_hrs");
return parseFloat(total.toFixed(2));
}, [data, cardSettings.totalLAR]);
const jobsInProduction = useMemo(
() => (cardSettings.jobsInProduction ? data.length : null),
[data, cardSettings.jobsInProduction]
);
const totalAmountInProduction = useMemo(() => {
if (!cardSettings.totalAmountInProduction) return null;
const total = calculateTotalAmount(data, "job_totals");
return parseFloat(total.toFixed(2));
}, [data, cardSettings.totalAmountInProduction]);
const totalHrsOnBoard = useMemo(() => {
if (!reducerData || !cardSettings.totalHrsOnBoard) return null;
const total =
calculateReducerTotal(reducerData.lanes, "labhrs", "mod_lb_hrs") +
calculateReducerTotal(reducerData.lanes, "larhrs", "mod_lb_hrs");
return parseFloat(total.toFixed(2));
}, [reducerData, cardSettings.totalHrsOnBoard]);
const totalLABOnBoard = useMemo(() => {
if (!reducerData || !cardSettings.totalLABOnBoard) return null;
const total = calculateReducerTotal(reducerData.lanes, "labhrs", "mod_lb_hrs");
return parseFloat(total.toFixed(2));
}, [reducerData, cardSettings.totalLABOnBoard]);
const totalLAROnBoard = useMemo(() => {
if (!reducerData || !cardSettings.totalLAROnBoard) return null;
const total = calculateReducerTotal(reducerData.lanes, "larhrs", "mod_lb_hrs");
return parseFloat(total.toFixed(2));
}, [reducerData, cardSettings.totalLAROnBoard]);
const jobsOnBoard = useMemo(
() =>
reducerData && cardSettings.jobsOnBoard
? reducerData.lanes.reduce((acc, lane) => acc + lane.cards.length, 0)
: null,
[reducerData, cardSettings.jobsOnBoard]
);
const totalAmountOnBoard = useMemo(() => {
if (!reducerData || !cardSettings.totalAmountOnBoard) return null;
const total = calculateReducerTotalAmount(reducerData.lanes, "job_totals");
return parseFloat(total.toFixed(2));
}, [reducerData, cardSettings.totalAmountOnBoard]);
const tasksInProduction = useMemo(() => {
if (!data || !cardSettings.tasksInProduction) return null;
return data.reduce((acc, item) => acc + (item.tasks_aggregate?.aggregate?.count || 0), 0);
}, [data, cardSettings.tasksInProduction]);
const tasksOnBoard = useMemo(() => {
if (!reducerData || !cardSettings.tasksOnBoard) return null;
return reducerData.lanes.reduce((acc, lane) => {
return (
acc + lane.cards.reduce((laneAcc, card) => laneAcc + (card.metadata.tasks_aggregate?.aggregate?.count || 0), 0)
);
}, 0);
}, [reducerData, cardSettings.tasksOnBoard]);
const statistics = useMemo(
() =>
mergeStatistics(statisticsItems, [
{ id: 0, value: totalHrs, type: StatisticType.HOURS },
{ id: 1, value: totalAmountInProduction, type: StatisticType.AMOUNT },
{ id: 2, value: totalLAB, type: StatisticType.HOURS },
{ id: 3, value: totalLAR, type: StatisticType.HOURS },
{ id: 4, value: jobsInProduction, type: StatisticType.JOBS },
{ id: 5, value: totalHrsOnBoard, type: StatisticType.HOURS },
{ id: 6, value: totalAmountOnBoard, type: StatisticType.AMOUNT },
{ id: 7, value: totalLABOnBoard, type: StatisticType.HOURS },
{ id: 8, value: totalLAROnBoard, type: StatisticType.HOURS },
{ id: 9, value: jobsOnBoard, type: StatisticType.JOBS },
{ id: 10, value: tasksOnBoard, type: StatisticType.TASKS },
{ id: 11, value: tasksInProduction, type: StatisticType.TASKS }
]),
[
totalHrs,
totalAmountInProduction,
totalLAB,
totalLAR,
jobsInProduction,
totalHrsOnBoard,
totalAmountOnBoard,
totalLABOnBoard,
totalLAROnBoard,
jobsOnBoard,
tasksOnBoard,
tasksInProduction
]
);
const sortedStatistics = useMemo(() => {
const statisticsMap = new Map(statistics.map((stat) => [stat.id, stat]));
return (
cardSettings?.statisticsOrder ? cardSettings.statisticsOrder : defaultKanbanSettings.statisticsOrder
).reduce((sorted, orderId) => {
const value = statisticsMap.get(orderId);
if (value && value.value !== null) {
sorted.push(value);
}
return sorted;
}, []);
}, [statistics, cardSettings.statisticsOrder]);
return (
<div style={{ display: "flex", gap: "5px", flexWrap: "wrap", marginBottom: "5px" }}>
{sortedStatistics.map((stat) => (
<Card styles={{ body: { padding: "8px" } }} key={stat.id}>
<Statistic
title={t(`production.statistics.${stat.label}`)}
value={formatValue(stat.value, stat.type)}
prefix={stat.type === StatisticType.AMOUNT ? t("production.statistics.currency_symbol") : undefined}
suffix={
stat.type === StatisticType.HOURS
? t("production.statistics.hours")
: stat.type === StatisticType.JOBS
? t("production.statistics.jobs")
: undefined
}
/>
</Card>
))}
</div>
);
};
ProductionStatistics.propTypes = {
data: PropTypes.array.isRequired,
cardSettings: PropTypes.object.isRequired,
reducerData: PropTypes.object
};
export default ProductionStatistics;

View File

@@ -1,145 +1,72 @@
.react-kanban-board {
.react-trello-board {
padding: 5px;
}
.react-kanban-card {
border-radius: 3px;
background-color: #fff;
padding: 4px;
margin-bottom: 7px;
.height-preserving-container:empty {
min-height: calc(var(--child-height));
box-sizing: border-box;
}
// .react-kanban-card-skeleton,
// .react-kanban-card,
// .react-kanban-card-adder-form {
// box-sizing: border-box;
// max-width: 145px;
// min-width: 145px;
// }
.react-kanban-card--dragging {
box-shadow: 2px 2px grey;
.height-preserving-container {
}
.react-kanban-card__description {
padding-top: 10px;
}
.react-kanban-card__title {
border-bottom: 1px solid #eee;
padding-bottom: 5px;
.react-trello-column-header {
font-weight: bold;
display: flex;
justify-content: space-between;
}
.react-kanban-column {
padding: 10px;
border-radius: 2px;
background-color: #eee;
margin: 5px;
}
.react-kanban-column input:focus {
outline: none;
}
.react-kanban-card-adder-form {
border-radius: 3px;
background-color: #fff;
padding: 10px;
margin-bottom: 7px;
}
.react-kanban-card-adder-form input {
border: 0px;
font-family: inherit;
font-size: inherit;
}
.react-kanban-card-adder-button {
width: 100%;
margin-top: 5px;
background-color: transparent;
cursor: pointer;
border: 1px solid #ccc;
transition: 0.3s;
border-radius: 3px;
font-size: 20px;
margin-bottom: 10px;
font-weight: bold;
background-color: #d0d0d0;
border-radius: 5px 5px 0 0;
}
.react-kanban-card-adder-button:hover {
background-color: #ccc;
}
.react-kanban-card-adder-form__title {
font-weight: bold;
border-bottom: 1px solid #eee;
padding-bottom: 5px;
font-weight: bold;
display: flex;
justify-content: space-between;
width: 100%;
padding: 0px;
}
.react-kanban-card-adder-form__title:focus {
outline: none;
}
.react-kanban-card-adder-form__description {
width: 100%;
margin-top: 10px;
}
.react-kanban-card-adder-form__description:focus {
outline: none;
}
.react-kanban-card-adder-form__button {
background-color: #eee;
.production-alert {
background: transparent;
border: none;
padding: 5px;
width: 45%;
margin-top: 5px;
border-radius: 3px;
}
.react-trello-footer {
background-color: #d0d0d0;
border-radius: 0 0 5px 5px;
}
.react-kanban-card-adder-form__button:hover {
transition: 0.3s;
cursor: pointer;
background-color: #ccc;
.grid-item {
margin: 1px; // TODO: (Note) THis is where we set the margin for vertical
}
.react-kanban-column-header {
padding-bottom: 10px;
font-weight: bold;
.lane-title {
vertical-align: middle;
.icon {
margin-right: 8px; /* Adjust the spacing as needed */
}
}
.react-kanban-column-header input:focus {
outline: none;
}
.react-kanban-column-header__button {
color: #333333;
background-color: #ffffff;
border-color: #cccccc;
}
.react-kanban-column-header__button:hover,
.react-kanban-column-header__button:focus,
.react-kanban-column-header__button:active {
background-color: #e6e6e6;
}
.react-kanban-column-adder-button {
border: 2px dashed #eee;
height: 132px;
margin: 5px;
}
.react-kanban-column-adder-button:hover {
cursor: pointer;
.header-content-container {
display: flex;
justify-content: center;
align-items: center;
position: relative;
.body-empty-container {
position: absolute;
right: 0;
}
.tech-container {
font-weight: bolder;
text-align: center;
flex: 1;
.branches-outlined {
color: orangered;
}
}
.inner-container {
display: flex;
align-items: center;
position: absolute;
left: 0;
.circle-outline {
color: orangered;
margin-left: 8px;
}
.iou-parent {
margin-left: 8px;
}
}
}

View File

@@ -1,121 +1,107 @@
// Function to sort an array of objects by parentId
import { groupBy } from "lodash";
const sortByParentId = (arr) => {
// return arr.reduce((accumulator, currentValue) => {
// //Find the parent item.
// let item = accumulator.find((x) => x.id === currentValue.kanbanparent);
// //Get index of parent item
// let index = accumulator.indexOf(item);
// index = index !== -1 ? index + 1 : 0;
// accumulator.splice(index, 0, currentValue);
// return accumulator;
// }, []);
let parentId = "-1";
const sortedList = [];
const byParentsIdsList = groupBy(arr, "kanbanparent"); // Create a new array with objects indexed by parentId
//console.log("sortByParentId -> byParentsIdsList", byParentsIdsList);
const byParentsIdsList = groupBy(arr, "kanbanparent");
while (byParentsIdsList[parentId]) {
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.
sortedList.push(...byParentsIdsList[parentId]);
parentId = byParentsIdsList[parentId][byParentsIdsList[parentId].length - 1].id;
}
if (byParentsIdsList["null"]) byParentsIdsList["null"].map((i) => sortedList.push(i));
if (byParentsIdsList["null"]) {
sortedList.push(...byParentsIdsList["null"]);
}
//Validate that the 2 arrays are of the same length and no children are missing.
// Ensure all items are included in the sorted list
if (arr.length !== sortedList.length) {
arr.map((origItem) => {
if (!!!sortedList.find((s) => s.id === origItem.id)) {
arr.forEach((origItem) => {
if (!sortedList.some((s) => s.id === origItem.id)) {
sortedList.push(origItem);
console.log("DATA CONSISTENCY ERROR: ", origItem.ro_number);
}
return 1;
});
}
return sortedList;
};
export const createBoardData = (AllStatuses, Jobs, filter) => {
const { search, employeeId } = filter;
const boardLanes = {
columns: AllStatuses.map((s) => {
return {
id: s,
title: s,
cards: []
};
})
};
// Function to create board data based on statuses and jobs, with optional filtering
export const createBoardData = ({ statuses, data, filter, cardSettings }) => {
const { search, employeeId, alert } = filter;
const filteredJobs =
(search === "" || !search) && !employeeId
? Jobs
: Jobs.filter((j) => {
let include = false;
if (search && search !== "") {
include = CheckSearch(search, j);
}
const lanes = statuses.map((status) => ({
id: status,
title: status,
cards: []
}));
if (!!employeeId) {
include =
include ||
j.employee_body === employeeId ||
j.employee_prep === employeeId ||
j.employee_csr === employeeId ||
j.employee_refinish === employeeId;
}
let filteredJobs =
(search === "" || !search) && !employeeId ? data : data.filter((job) => checkFilter(search, employeeId, job));
return include;
});
// Filter jobs by selectedMdInsCos if it has values
if (cardSettings?.selectedMdInsCos?.length > 0) {
filteredJobs = filteredJobs.filter((job) => cardSettings.selectedMdInsCos.includes(job.ins_co_nm));
}
const DataGroupedByStatus = groupBy(filteredJobs, (d) => d.status);
// Filter jobs by selectedEstimators if it has values
if (cardSettings?.selectedEstimators?.length > 0) {
filteredJobs = filteredJobs.filter((job) =>
cardSettings.selectedEstimators.includes(`${job.est_ct_fn} ${job.est_ct_ln}`)
);
}
Object.keys(DataGroupedByStatus).map((statusGroupKey) => {
// Filter jobs by alert if alert filter is true
if (alert) {
filteredJobs = filteredJobs.filter((job) => job.production_vars?.alert);
}
const DataGroupedByStatus = groupBy(filteredJobs, "status");
Object.keys(DataGroupedByStatus).forEach((statusGroupKey) => {
try {
const needle = boardLanes.columns.find((l) => l.id === statusGroupKey);
if (!needle?.cards) return null;
needle.cards = sortByParentId(DataGroupedByStatus[statusGroupKey]);
const lane = lanes.find((l) => l.id === statusGroupKey);
if (!lane) return;
lane.cards = sortByParentId(DataGroupedByStatus[statusGroupKey]).map((job) => {
const { id, title, description, due_date, ...metadata } = job;
return {
id,
title,
description,
label: due_date || "",
metadata
};
});
} catch (error) {
console.log("Error while creating board card", error);
console.error("Error while creating board card", error);
}
return null;
});
return boardLanes;
return { lanes };
};
const CheckSearch = (search, job) => {
return (
(job.ro_number || "").toLowerCase().includes(search.toLowerCase()) ||
(job.ownr_fn || "").toLowerCase().includes(search.toLowerCase()) ||
(job.ownr_co_nm || "").toLowerCase().includes(search.toLowerCase()) ||
(job.ownr_ln || "").toLowerCase().includes(search.toLowerCase()) ||
(job.status || "").toLowerCase().includes(search.toLowerCase()) ||
(job.v_make_desc || "").toLowerCase().includes(search.toLowerCase()) ||
(job.v_model_desc || "").toLowerCase().includes(search.toLowerCase()) ||
(job.clm_no || "").toLowerCase().includes(search.toLowerCase()) ||
(job.plate_no || "").toLowerCase().includes(search.toLowerCase())
);
// Function to check if a job matches the search and/or employeeId filter
const checkFilter = (search, employeeId, job) => {
const lowerSearch = search?.toLowerCase() ?? "";
const matchesSearch =
lowerSearch &&
[
job.ro_number,
job.ownr_fn,
job.ownr_co_nm,
job.ownr_ln,
job.status,
job.v_make_desc,
job.v_model_desc,
job.clm_no,
job.plate_no
].some((field) => field?.toLowerCase().includes(lowerSearch));
const matchesEmployeeId =
employeeId && [job.employee_body, job.employee_prep, job.employee_csr, job.employee_refinish].includes(employeeId);
return matchesSearch || matchesEmployeeId;
};
// export const updateBoardOnMove = (board, card, source, destination) => {
// //Slice from source
// const sourceCardList = board.columns.find((x) => x.id === source.fromColumnId)
// .cards;
// sourceCardList.slice(source.fromPosition, 0);
// //Splice into destination.
// const destCardList = board.columns.find(
// (x) => x.id === destination.toColumnId
// ).cards;
// console.log("updateBoardOnMove -> destCardList", destCardList);
// destCardList.splice(destination.toPosition, 0, card);
// console.log("updateBoardOnMove -> destCardList", destCardList);
// console.log("board", board);
// return board;
// };

View File

@@ -0,0 +1,81 @@
import React from "react";
import { Card, Form, Select } from "antd";
import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";
const FilterSettings = ({
selectedMdInsCos,
setSelectedMdInsCos,
selectedEstimators,
setSelectedEstimators,
setHasChanges,
bodyshop,
data
}) => {
const { t } = useTranslation();
const extractNames = (source, firstNameKey, lastNameKey) =>
source.map((item) => ({
firstName: item[firstNameKey],
lastName: item[lastNameKey]
}));
const bodyshopNames = extractNames(bodyshop.md_estimators, "est_ct_fn", "est_ct_ln");
const dataNames = extractNames(data, "est_ct_fn", "est_ct_ln");
const combinedNames = [...bodyshopNames, ...dataNames];
const uniqueNames = Array.from(
new Map(combinedNames.map((item) => [`${item.firstName} ${item.lastName}`, item])).values()
);
return (
<Card title={t("production.settings.filters_title")}>
<Form.Item label={t("production.settings.filters.md_ins_cos")}>
<Select
mode="multiple"
placeholder={t("production.settings.filters.md_ins_cos")}
value={selectedMdInsCos}
onChange={(value) => {
setSelectedMdInsCos(value);
setHasChanges(true);
}}
options={bodyshop.md_ins_cos.map((item) => ({
value: item.name,
label: item.name
}))}
/>
</Form.Item>
<Form.Item label={t("production.settings.filters.md_estimators")}>
<Select
mode="multiple"
placeholder={t("production.settings.filters.md_estimators")}
value={selectedEstimators}
onChange={(value) => {
setSelectedEstimators(value);
setHasChanges(true);
}}
options={uniqueNames.map((item) => {
const name = `${item.firstName} ${item.lastName}`.trim();
return {
value: name,
label: name
};
})}
/>
</Form.Item>
</Card>
);
};
FilterSettings.propTypes = {
selectedMdInsCos: PropTypes.array.isRequired,
setSelectedMdInsCos: PropTypes.func.isRequired,
setHasChanges: PropTypes.func.isRequired,
selectedEstimators: PropTypes.array.isRequired,
setSelectedEstimators: PropTypes.func,
bodyshop: PropTypes.object.isRequired,
data: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default FilterSettings;

View File

@@ -0,0 +1,38 @@
import { Card, Checkbox, Col, Form, Row } from "antd";
import React from "react";
import PropTypes from "prop-types";
const InformationSettings = ({ t }) => (
<Card title={t("production.settings.information")}>
<Row gutter={[16, 16]}>
{[
"model_info",
"ownr_nm",
"clm_no",
"ins_co_nm",
"employeeassignments",
"actual_in",
"scheduled_completion",
"ats",
"production_note",
"sublets",
"partsstatus",
"estimator",
"subtotal",
"tasks"
].map((item) => (
<Col span={4} key={item}>
<Form.Item name={item} valuePropName="checked">
<Checkbox>{t(`production.labels.${item}`)}</Checkbox>
</Form.Item>
</Col>
))}
</Row>
</Card>
);
InformationSettings.propTypes = {
t: PropTypes.func.isRequired
};
export default InformationSettings;

View File

@@ -0,0 +1,71 @@
import { Card, Col, Form, Radio, Row } from "antd";
import React from "react";
import PropTypes from "prop-types";
const LayoutSettings = ({ t }) => (
<Card title={t("production.settings.layout")}>
<Row gutter={[16, 16]}>
{[
{
name: "orientation",
label: t("production.labels.orientation"),
options: [
{ value: true, label: t("production.labels.vertical") },
{ value: false, label: t("production.labels.horizontal") }
]
},
{
name: "cardSize",
label: t("production.labels.card_size"),
options: [
{ value: "small", label: t("production.options.small") },
{ value: "medium", label: t("production.options.medium") },
{ value: "large", label: t("production.options.large") }
]
},
{
name: "compact",
label: t("production.labels.compact"),
options: [
{ value: true, label: t("production.labels.tall") },
{ value: false, label: t("production.labels.wide") }
]
},
{
name: "cardcolor",
label: t("production.labels.cardcolor"),
options: [
{ value: true, label: t("production.labels.on") },
{ value: false, label: t("production.labels.off") }
]
},
{
name: "kiosk",
label: t("production.labels.kiosk_mode"),
options: [
{ value: true, label: t("production.labels.on") },
{ value: false, label: t("production.labels.off") }
]
}
].map(({ name, label, options }) => (
<Col span={4} key={name}>
<Form.Item name={name} label={label}>
<Radio.Group>
{options.map((option) => (
<Radio.Button key={option.value.toString()} value={option.value}>
{option.label}
</Radio.Button>
))}
</Radio.Group>
</Form.Item>
</Col>
))}
</Row>
</Card>
);
LayoutSettings.propTypes = {
t: PropTypes.func.isRequired
};
export default LayoutSettings;

View File

@@ -0,0 +1,59 @@
import { DragDropContext, Draggable, Droppable } from "../trello-board/dnd/lib/index.js";
import { statisticsItems } from "./defaultKanbanSettings.js";
import { Card, Checkbox, Form } from "antd";
import React from "react";
import PropTypes from "prop-types";
const StatisticsSettings = ({ t, statisticsOrder, setStatisticsOrder, setHasChanges }) => {
const onDragEnd = (result) => {
if (!result.destination) return;
const newOrder = Array.from(statisticsOrder);
const [movedItem] = newOrder.splice(result.source.index, 1);
newOrder.splice(result.destination.index, 0, movedItem);
setStatisticsOrder(newOrder);
setHasChanges(true);
};
return (
<Card title={t("production.settings.statistics_title")}>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable direction="grid" droppableId="statistics">
{(provided) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
style={{ display: "flex", flexWrap: "wrap", gap: "8px" }}
>
{statisticsOrder.map((itemId, index) => {
const item = statisticsItems.find((stat) => stat.id === itemId);
return (
<Draggable key={itemId} draggableId={itemId.toString()} index={index}>
{(provided) => (
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<Card styles={{ body: { padding: "5px" } }} style={{ marginBottom: 8, flex: "0 1 auto" }}>
<Form.Item style={{ marginBottom: 0 }} name={item.name} valuePropName="checked">
<Checkbox>{t(`production.settings.statistics.${item.label}`)}</Checkbox>
</Form.Item>
</Card>
</div>
)}
</Draggable>
);
})}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
</Card>
);
};
StatisticsSettings.propTypes = {
t: PropTypes.func.isRequired,
statisticsOrder: PropTypes.arrayOf(PropTypes.number).isRequired,
setStatisticsOrder: PropTypes.func.isRequired,
setHasChanges: PropTypes.func.isRequired
};
export default StatisticsSettings;

View File

@@ -0,0 +1,69 @@
const statisticsItems = [
{ id: 0, name: "totalHrs", label: "total_hours_in_production" },
{ id: 1, name: "totalAmountInProduction", label: "total_amount_in_production" },
{ id: 2, name: "totalLAB", label: "total_lab_in_production" },
{ id: 3, name: "totalLAR", label: "total_lar_in_production" },
{ id: 4, name: "jobsInProduction", label: "jobs_in_production" },
{ id: 5, name: "totalHrsOnBoard", label: "total_hours_on_board" },
{ id: 6, name: "totalAmountOnBoard", label: "total_amount_on_board" },
{ id: 7, name: "totalLABOnBoard", label: "total_lab_on_board" },
{ id: 8, name: "totalLAROnBoard", label: "total_lar_on_board" },
{ id: 9, name: "jobsOnBoard", label: "total_jobs_on_board" },
{ id: 10, name: "tasksOnBoard", label: "tasks_on_board" },
{ id: 11, name: "tasksInProduction", label: "tasks_in_production" }
];
const defaultKanbanSettings = {
ats: true,
clm_no: true,
compact: false,
ownr_nm: true,
sublets: true,
ins_co_nm: true,
production_note: true,
employeeassignments: true,
scheduled_completion: true,
cardcolor: false,
orientation: false,
tasks: false,
cardSize: "small",
model_info: true,
kiosk: false,
totalHrs: true,
totalAmountInProduction: false,
totalLAB: true,
totalLAR: true,
jobsInProduction: true,
totalHrsOnBoard: false,
totalLABOnBoard: false,
totalLAROnBoard: false,
jobsOnBoard: false,
tasksOnBoard: false,
tasksInProduction: false,
totalAmountOnBoard: true,
estimator: false,
subtotal: false,
statisticsOrder: statisticsItems.map((item) => item.id),
selectedMdInsCos: [],
selectedEstimators: []
};
const defaultFilters = { search: "", employeeId: null, alert: false };
const mergeWithDefaults = (settings) => {
// Create a new object that starts with the default settings
const mergedSettings = { ...defaultKanbanSettings };
// Override with the provided settings, if any
if (settings) {
for (const key in settings) {
if (settings.hasOwnProperty(key)) {
mergedSettings[key] = settings[key];
}
}
}
return mergedSettings;
};
export { defaultKanbanSettings, statisticsItems, mergeWithDefaults, defaultFilters };

View File

@@ -0,0 +1,169 @@
import { useMutation } from "@apollo/client";
import { Button, Card, Col, Form, notification, Popover, Row, Tabs } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_KANBAN_SETTINGS } from "../../../graphql/user.queries.js";
import { defaultKanbanSettings, mergeWithDefaults } from "./defaultKanbanSettings.js";
import LayoutSettings from "./LayoutSettings.jsx";
import InformationSettings from "./InformationSettings.jsx";
import StatisticsSettings from "./StatisticsSettings.jsx";
import FilterSettings from "./FilterSettings.jsx";
import PropTypes from "prop-types";
import { isFunction } from "lodash";
function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bodyshop, data, onSettingsChange }) {
const [form] = Form.useForm();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
const [statisticsOrder, setStatisticsOrder] = useState(defaultKanbanSettings.statisticsOrder);
const [selectedMdInsCos, setSelectedMdInsCos] = useState(defaultKanbanSettings.selectedMdInsCos);
const [selectedEstimators, setSelectedEstimators] = useState(defaultKanbanSettings.selectedEstimators);
const [updateKbSettings] = useMutation(UPDATE_KANBAN_SETTINGS);
const { t } = useTranslation();
useEffect(() => {
if (associationSettings?.kanban_settings) {
const finalSettings = mergeWithDefaults(associationSettings.kanban_settings);
form.setFieldsValue(finalSettings);
setStatisticsOrder(finalSettings.statisticsOrder);
setSelectedMdInsCos(finalSettings.selectedMdInsCos);
setSelectedEstimators(finalSettings.selectedEstimators);
}
}, [form, associationSettings]);
const handleFinish = async (values) => {
setLoading(true);
parentLoading(true);
const result = await updateKbSettings({
variables: {
id: associationSettings?.id,
ks: {
...associationSettings.kanban_settings,
...values,
statisticsOrder,
selectedMdInsCos,
selectedEstimators
}
}
});
if (result.errors) {
notification.open({
type: "error",
message: t("production.errors.settings", {
error: JSON.stringify(result.errors)
})
});
}
setOpen(false);
setLoading(false);
parentLoading(false);
if (onSettingsChange && isFunction(onSettingsChange)) {
onSettingsChange(values);
}
setHasChanges(false);
};
const handleValuesChange = () => setHasChanges(true);
const handleRestoreDefaults = () => {
form.setFieldsValue({
...defaultKanbanSettings,
statisticsOrder: defaultKanbanSettings.statisticsOrder
});
setStatisticsOrder(defaultKanbanSettings.statisticsOrder);
setSelectedMdInsCos(defaultKanbanSettings.selectedMdInsCos);
setSelectedEstimators(defaultKanbanSettings.selectedEstimators);
setHasChanges(true);
};
const overlay = (
<Card style={{ minWidth: "80vw" }}>
<Form form={form} onFinish={handleFinish} layout="vertical" onValuesChange={handleValuesChange}>
<Tabs
defaultActiveKey="1"
items={[
{
key: "1",
label: t("production.settings.layout"),
children: <LayoutSettings t={t} />
},
{
key: "2",
label: t("production.settings.information"),
children: <InformationSettings t={t} />
},
{
key: "3",
label: t("production.settings.statistics_title"),
children: (
<StatisticsSettings
t={t}
statisticsOrder={statisticsOrder}
setStatisticsOrder={setStatisticsOrder}
setHasChanges={setHasChanges}
/>
)
},
{
key: "4",
label: t("production.settings.filters_title"),
children: (
<FilterSettings
selectedMdInsCos={selectedMdInsCos}
setSelectedMdInsCos={setSelectedMdInsCos}
selectedEstimators={selectedEstimators}
setSelectedEstimators={setSelectedEstimators}
setHasChanges={setHasChanges}
bodyshop={bodyshop}
data={data}
/>
)
}
]}
/>
<Row justify="center" style={{ marginTop: 15 }} gutter={16}>
<Col span={8}>
<Button block onClick={() => setOpen(false)}>
{t("general.actions.cancel")}
</Button>
</Col>
<Col span={8}>
<Button block onClick={handleRestoreDefaults}>
{t("general.actions.defaults")}
</Button>
</Col>
<Col span={8}>
<Button block onClick={form.submit} loading={loading} type="primary" disabled={!hasChanges}>
{t("general.actions.save")}
</Button>
</Col>
</Row>
</Form>
</Card>
);
return (
<Popover content={overlay} open={open} placement="topRight">
<Button loading={loading} onClick={() => setOpen(!open)}>
{t("production.settings.board_settings")}
</Button>
</Popover>
);
}
ProductionBoardKanbanSettings.propTypes = {
associationSettings: PropTypes.object,
parentLoading: PropTypes.func.isRequired,
bodyshop: PropTypes.object.isRequired,
onSettingsChange: PropTypes.func,
data: PropTypes.array
};
export default ProductionBoardKanbanSettings;

View File

@@ -0,0 +1,83 @@
import React, { useEffect, useRef, useState } from "react";
import PropTypes from "prop-types";
/**
* Height Memory Wrapper
* @param children
* @param maxHeight
* @param setMaxHeight
* @param override - Override the minHeight style from being set
* @param itemKey - Unique key to preserve height for items with the same key
* @returns {JSX.Element}
*/
const HeightMemoryWrapper = ({ children, maxHeight, setMaxHeight, override, itemKey }) => {
const ref = useRef(null);
const heightMapRef = useRef(new Map());
const [localMaxHeight, setLocalMaxHeight] = useState(maxHeight);
const [devicePixelRatio, setDevicePixelRatio] = useState(window.devicePixelRatio);
useEffect(() => {
const currentRef = ref.current;
const updateHeight = () => {
const currentHeight = currentRef?.firstChild?.clientHeight || 0;
if (itemKey) {
const keyHeight = heightMapRef.current.get(itemKey) || 0;
const newHeight = Math.max(keyHeight, currentHeight);
heightMapRef.current.set(itemKey, newHeight);
setLocalMaxHeight(newHeight);
} else {
setLocalMaxHeight((prevHeight) => Math.max(prevHeight, currentHeight));
}
setMaxHeight((prevHeight) => Math.max(prevHeight, currentHeight));
};
const resizeObserver = new ResizeObserver(updateHeight);
if (currentRef?.firstChild) {
resizeObserver.observe(currentRef.firstChild);
}
const resizeHandler = () => {
if (Math.abs(window.devicePixelRatio - devicePixelRatio) > 0.1) {
// Threshold to detect significant zoom level change
heightMapRef.current.clear(); // Clearing the height memory as zoom level has changed significantly
setLocalMaxHeight(0); // Reset local max height
setDevicePixelRatio(window.devicePixelRatio); // Update the recorded device pixel ratio
}
updateHeight();
};
window.addEventListener("resize", resizeHandler);
return () => {
if (currentRef?.firstChild) {
resizeObserver.unobserve(currentRef.firstChild);
}
window.removeEventListener("resize", resizeHandler);
};
}, [itemKey, setMaxHeight, devicePixelRatio]);
useEffect(() => {
if (itemKey && heightMapRef.current.has(itemKey)) {
setLocalMaxHeight(heightMapRef.current.get(itemKey));
}
}, [itemKey]);
const style = override ? {} : { minHeight: localMaxHeight };
return (
<div ref={ref} style={style}>
{children}
</div>
);
};
HeightMemoryWrapper.propTypes = {
children: PropTypes.node.isRequired,
maxHeight: PropTypes.number.isRequired,
setMaxHeight: PropTypes.func.isRequired,
override: PropTypes.bool,
itemKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
};
export default HeightMemoryWrapper;

View File

@@ -0,0 +1,24 @@
import React, { useEffect, useState } from "react";
const HeightPreservingItem = ({ children, ...props }) => {
const [size, setSize] = useState(0);
const knownSize = props["data-known-size"];
useEffect(() => {
setSize((prevSize) => {
return knownSize === 0 ? prevSize : knownSize;
});
}, [setSize, knownSize]);
return (
<div
{...props}
className="height-preserving-container"
style={{
"--child-height": `${size}px`
}}
>
{children}
</div>
);
};
export default HeightPreservingItem;

View File

@@ -0,0 +1,9 @@
import React from "react";
const ItemComponent = ({ children, maxCardHeight, maxCardWidth, ...props }) => (
<div style={{ minWidth: maxCardWidth, minHeight: maxCardHeight }} {...props}>
{children}
</div>
);
export default ItemComponent;

View File

@@ -0,0 +1,9 @@
import React from "react";
const ItemWrapper = React.memo(({ children, ...props }) => (
<div {...props} className="item-wrapper">
{children}
</div>
));
export default ItemWrapper;

View File

@@ -0,0 +1,11 @@
import React from "react";
import { LaneFooter } from "../styles/Base.js";
import { CollapseBtn, ExpandBtn } from "../styles/Elements.js";
const LaneFooterComponent = ({ onClick, collapsed }) => (
<LaneFooter className="react-trello-footer" onClick={onClick}>
{collapsed ? <ExpandBtn /> : <CollapseBtn />}
</LaneFooter>
);
export default LaneFooterComponent;

View File

@@ -0,0 +1,9 @@
import React, { forwardRef } from "react";
const ListComponent = forwardRef(({ style, children, ...props }, ref) => (
<div ref={ref} {...props} style={{ ...style }}>
{children}
</div>
));
export default ListComponent;

View File

@@ -0,0 +1,55 @@
import React, { useEffect, useRef } from "react";
import PropTypes from "prop-types";
const SizeMemoryWrapper = ({ children, maxHeight, setMaxHeight, maxWidth, setMaxWidth }) => {
const ref = useRef(null);
useEffect(() => {
const currentRef = ref.current;
const updateSize = () => {
const currentHeight = currentRef?.firstChild?.clientHeight || 0;
const currentWidth = currentRef?.firstChild?.clientWidth || 0;
setMaxHeight((prevHeight) => Math.max(prevHeight, currentHeight));
setMaxWidth((prevWidth) => Math.max(prevWidth, currentWidth));
};
const resizeObserver = new ResizeObserver(updateSize);
if (currentRef?.firstChild) {
resizeObserver.observe(currentRef.firstChild);
}
const handleLoad = () => {
if (window.devicePixelRatio < 1) {
return; // Do not update width and height
}
updateSize();
};
window.addEventListener("load", handleLoad);
return () => {
if (currentRef?.firstChild) {
resizeObserver.unobserve(currentRef.firstChild);
}
window.removeEventListener("load", handleLoad);
};
}, [setMaxHeight, setMaxWidth]);
return (
<div ref={ref} className="size-memory-wrapper" style={{ minHeight: maxHeight, minWidth: maxWidth }}>
{children}
</div>
);
};
SizeMemoryWrapper.propTypes = {
children: PropTypes.node.isRequired,
maxHeight: PropTypes.number.isRequired,
setMaxHeight: PropTypes.func.isRequired,
maxWidth: PropTypes.number.isRequired,
setMaxWidth: PropTypes.func.isRequired
};
export default SizeMemoryWrapper;

View File

@@ -0,0 +1,39 @@
import { BoardContainer } from "../index";
import { useMemo } from "react";
import { StyleHorizontal, StyleVertical } from "../styles/Base.js";
import { cardSizesVertical } from "../styles/Globals.js";
const Board = ({ id, className, orientation, cardSettings, ...additionalProps }) => {
const OrientationStyle = useMemo(
() => (orientation === "horizontal" ? StyleHorizontal : StyleVertical),
[orientation]
);
const gridItemWidth = useMemo(() => {
switch (cardSettings?.cardSize) {
case "small":
return cardSizesVertical.small;
case "large":
return cardSizesVertical.large;
case "medium":
return cardSizesVertical.medium;
default:
return cardSizesVertical.small;
}
}, [cardSettings]);
return (
<>
<OrientationStyle {...{ gridItemWidth }}>
<BoardContainer
orientation={orientation}
cardSettings={cardSettings}
{...additionalProps}
className="react-trello-board"
/>
</OrientationStyle>
</>
);
};
export default Board;

View File

@@ -0,0 +1,173 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { DragDropContext } from "../dnd/lib";
import PropTypes from "prop-types";
import isEqual from "lodash/isEqual";
import Lane from "./Lane";
import { PopoverWrapper } from "react-popopo";
import * as actions from "../../../../redux/trello/trello.actions.js";
import { BoardWrapper } from "../styles/Base.js";
import ProductionStatistics from "../../production-board-kanban.statistics.jsx";
const useDragMap = () => {
const dragMapRef = useRef(new Map());
const setDragTime = (laneId) => {
dragMapRef.current.set(laneId, Date.now());
};
const getLastDragTime = (laneId) => {
return dragMapRef.current.get(laneId);
};
return { setDragTime, getLastDragTime };
};
const BoardContainer = ({
data,
onDataChange = () => {},
onDragEnd = () => {},
laneSortFunction = () => {},
orientation = "horizontal",
cardSettings = {},
eventBusHandle,
reducerData,
queryData
}) => {
const [isDragging, setIsDragging] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const [maxLaneHeight, setMaxLaneHeight] = useState(0);
const [maxCardHeight, setMaxCardHeight] = useState(0);
const [maxCardWidth, setMaxCardWidth] = useState(0);
const dispatch = useDispatch();
const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
const { setDragTime, getLastDragTime } = useDragMap();
const wireEventBus = useCallback(() => {
const eventBus = {
publish: (event) => {
switch (event.type) {
// case "ADD_CARD":
// return dispatch(actions.addCard({ laneId: event.laneId, card: event.card }));
// case "REMOVE_CARD":
// return dispatch(actions.removeCard({ laneId: event.laneId, cardId: event.cardId }));
// case "REFRESH_BOARD":
// return dispatch(actions.loadBoard(event.data));
// case "UPDATE_CARDS":
// return dispatch(actions.updateCards({ laneId: event.laneId, cards: event.cards }));
// case "UPDATE_CARD":
// return dispatch(actions.updateCard({ laneId: event.laneId, updatedCard: event.card }));
// case "UPDATE_LANES":
// return dispatch(actions.updateLanes(event.lanes));
// case "UPDATE_LANE":
// return dispatch(actions.updateLane(event.lane));
case "MOVE_CARD":
return dispatch(
actions.moveCardAcrossLanes({
fromLaneId: event.fromLaneId,
toLaneId: event.toLaneId,
cardId: event.cardId,
index: event.index,
event
})
);
default:
return;
}
}
};
eventBusHandle(eventBus);
}, [dispatch, eventBusHandle]);
useEffect(() => {
dispatch(actions.loadBoard(data));
if (eventBusHandle) {
wireEventBus();
}
}, [data, eventBusHandle, dispatch, wireEventBus]);
useEffect(() => {
if (!isEqual(currentReducerData, reducerData)) {
onDataChange(currentReducerData);
}
}, [currentReducerData, reducerData, onDataChange]);
const onDragStart = useCallback(() => {
setIsDragging(true);
}, []);
const onLaneDrag = useCallback(
async ({ draggableId, type, source, reason, mode, destination, combine }) => {
setIsDragging(false);
setDragTime(source.droppableId);
if (!type || type !== "lane" || !source || !destination || isEqual(source, destination)) return;
setIsProcessing(true);
dispatch(
actions.moveCardAcrossLanes({
fromLaneId: source.droppableId,
toLaneId: destination.droppableId,
cardId: draggableId,
index: destination.index
})
);
try {
await onDragEnd({ draggableId, type, source, reason, mode, destination, combine });
} catch (err) {
console.error("Error in onLaneDrag", err);
} finally {
setIsProcessing(false);
}
},
[dispatch, onDragEnd, setDragTime]
);
return (
<div>
<ProductionStatistics data={queryData} reducerData={currentReducerData} cardSettings={cardSettings} />
<PopoverWrapper>
<BoardWrapper orientation={orientation}>
<DragDropContext onDragEnd={onLaneDrag} onDragStart={onDragStart} contextId="production-board">
{currentReducerData.lanes.map((lane, index) => (
<Lane
key={lane.id}
id={lane.id}
title={lane.title}
index={index}
laneSortFunction={laneSortFunction}
orientation={orientation}
cards={lane.cards}
isDragging={isDragging}
isProcessing={isProcessing}
cardSettings={cardSettings}
maxLaneHeight={maxLaneHeight}
setMaxLaneHeight={setMaxLaneHeight}
maxCardHeight={maxCardHeight}
setMaxCardHeight={setMaxCardHeight}
maxCardWidth={maxCardWidth}
setMaxCardWidth={setMaxCardWidth}
lastDrag={getLastDragTime(lane.id)}
/>
))}
</DragDropContext>
</BoardWrapper>
</PopoverWrapper>
</div>
);
};
BoardContainer.propTypes = {
id: PropTypes.string,
data: PropTypes.object.isRequired,
reducerData: PropTypes.object,
onDataChange: PropTypes.func,
eventBusHandle: PropTypes.func,
laneSortFunction: PropTypes.func,
handleDragEnd: PropTypes.func,
orientation: PropTypes.string
};
export default BoardContainer;

View File

@@ -0,0 +1,291 @@
import React, { useCallback, useMemo, useRef, useState } from "react";
import PropTypes from "prop-types";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import * as actions from "../../../../redux/trello/trello.actions.js";
import { Draggable, Droppable } from "../dnd/lib";
import { Virtuoso, VirtuosoGrid } from "react-virtuoso";
import HeightPreservingItem from "../components/HeightPreservingItem.jsx";
import { Section } from "../styles/Base.js";
import LaneFooter from "../components/LaneFooter.jsx";
import { EyeInvisibleOutlined, EyeOutlined } from "@ant-design/icons";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../../../redux/user/user.selectors.js";
import { selectTechnician } from "../../../../redux/tech/tech.selectors.js";
import ProductionBoardCard from "../../production-board-kanban-card.component.jsx";
import HeightMemoryWrapper from "../components/HeightMemoryWrapper.jsx";
import SizeMemoryWrapper from "../components/SizeMemoryWrapper.jsx";
import ListComponent from "../components/ListComponent.jsx";
import ItemComponent from "../components/ItemComponent.jsx";
import ItemWrapper from "../components/ItemWrapper.jsx";
import objectHash from "object-hash";
/**
* Lane is a React component that represents a lane in a Trello-like board.
* @param id
* @param title
* @param index
* @param isProcessing
* @param laneSortFunction
* @param cards
* @param cardSettings
* @param orientation
* @param maxLaneHeight
* @param setMaxLaneHeight
* @param maxCardHeight
* @param setMaxCardHeight
* @param maxCardWidth
* @param setMaxCardWidth
* @param lastDrag
* @param technician -- connected to redux
* @param bodyshop -- connected to redux
* @returns {Element}
* @constructor
*/
const Lane = ({
id,
title,
index,
isProcessing,
laneSortFunction,
cards,
cardSettings = {},
orientation = "vertical",
maxLaneHeight,
setMaxLaneHeight,
maxCardHeight,
setMaxCardHeight,
maxCardWidth,
setMaxCardWidth,
lastDrag,
technician,
bodyshop
}) => {
const [collapsed, setCollapsed] = useState(false);
const laneRef = useRef(null);
const sortedCards = useMemo(() => {
if (!cards) return [];
if (!laneSortFunction) return cards;
return [...cards].sort(laneSortFunction);
}, [cards, laneSortFunction]);
const toggleLaneCollapsed = useCallback(() => {
setCollapsed((prevCollapsed) => !prevCollapsed);
}, []);
const renderDraggable = useCallback(
(index, card) => {
if (!card) {
console.log("null card");
return null;
}
return (
<Draggable draggableId={card.id} index={index} key={card.id} isDragDisabled={isProcessing}>
{(provided, snapshot) => (
<div
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
style={provided.draggableProps.style}
className={`item ${snapshot.isDragging ? "is-dragging" : ""}`}
key={card.id}
>
<SizeMemoryWrapper
maxHeight={maxCardHeight}
setMaxHeight={setMaxCardHeight}
maxWidth={maxCardWidth}
setMaxWidth={setMaxCardWidth}
>
<ProductionBoardCard
technician={technician}
bodyshop={bodyshop}
cardSettings={cardSettings}
key={card.id}
card={card}
style={{ minHeight: maxCardHeight, minWidth: maxCardWidth }}
className="react-trello-card"
/>
</SizeMemoryWrapper>
</div>
)}
</Draggable>
);
},
[isProcessing, technician, bodyshop, cardSettings, maxCardHeight, setMaxCardHeight, maxCardWidth, setMaxCardWidth]
);
const renderDroppable = useCallback(
(provided, renderedCards) => {
const Component = orientation === "vertical" ? VirtuosoGrid : Virtuoso;
const FinalComponent = collapsed ? "div" : Component;
const commonProps = {
useWindowScroll: true,
data: renderedCards
};
const verticalProps = {
...commonProps,
listClassName: "grid-container",
itemClassName: "grid-item",
customScrollParent: laneRef.current,
components: {
List: ListComponent,
Item: ItemComponent
},
itemContent: (index, item) => <ItemWrapper>{renderDraggable(index, item)}</ItemWrapper>,
overscan: { main: 10, reverse: 10 }
};
const horizontalProps = {
...commonProps,
components: { Item: HeightPreservingItem },
overscan: { main: 3, reverse: 3 },
itemContent: (index, item) => renderDraggable(index, item),
scrollerRef: provided.innerRef,
style: {
minWidth: maxCardWidth,
minHeight: maxLaneHeight
}
};
const componentProps = orientation === "vertical" ? verticalProps : horizontalProps;
// If the lane is collapsed, we want to render a div instead of the virtualized list, and we want to set the height to the max height of the lane so that
// the lane doesn't shrink when collapsed (in horizontal mode)
const finalComponentProps = collapsed
? orientation === "horizontal"
? {
style: {
height: maxLaneHeight
}
}
: {}
: componentProps;
// If the lane is horizontal and collapsed, we want to render a placeholder so that the lane doesn't shrink to 0 height and grows when
// a card is dragged over it
const shouldRenderPlaceholder = orientation !== "horizontal" && (collapsed || renderedCards.length === 0);
return (
<HeightMemoryWrapper
itemKey={objectHash({
id,
orientation,
cardSettings,
cardLength: renderedCards?.length
})}
maxHeight={maxLaneHeight}
setMaxHeight={setMaxLaneHeight}
override={orientation !== "horizontal" && (collapsed || !renderedCards.length)}
>
<div
{...provided.droppableProps}
ref={provided.innerRef}
className={`react-trello-lane ${collapsed ? "lane-collapsed" : ""}`}
style={{ ...provided.droppableProps.style }}
>
<FinalComponent {...finalComponentProps} />
{shouldRenderPlaceholder && provided.placeholder}
</div>
</HeightMemoryWrapper>
);
},
[orientation, collapsed, renderDraggable, maxLaneHeight, setMaxLaneHeight, maxCardWidth, id, cardSettings]
);
const renderDragContainer = useCallback(
() => (
<Droppable
droppableId={id}
index={index}
type="lane"
direction={orientation === "horizontal" ? "vertical" : "grid"}
mode="virtual"
renderClone={(provided, snapshot, rubric) => {
const card = sortedCards[rubric.source.index];
return (
<div
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
style={{
...provided.draggableProps.style,
minHeight: maxCardHeight,
minWidth: maxCardWidth
}}
className={`clone ${snapshot.isDragging ? "is-dragging" : ""}`}
key={card.id}
>
<ProductionBoardCard
technician={technician}
bodyshop={bodyshop}
cardSettings={cardSettings}
key={card.id}
className="react-trello-card"
card={card}
clone={false}
/>
</div>
);
}}
>
{(provided) => renderDroppable(provided, sortedCards)}
</Droppable>
),
[
id,
index,
orientation,
renderDroppable,
sortedCards,
technician,
bodyshop,
cardSettings,
maxCardHeight,
maxCardWidth
]
);
return (
<Section key={`lane-${id}-${lastDrag}`} orientation={orientation} cardSettings={cardSettings}>
<div onDoubleClick={toggleLaneCollapsed} className="react-trello-column-header">
<span className="lane-title">
{collapsed ? <EyeInvisibleOutlined className="icon" /> : <EyeOutlined className="icon" />}
{title}
</span>
</div>
{renderDragContainer()}
<LaneFooter onClick={toggleLaneCollapsed} collapsed={collapsed} />
</Section>
);
};
Lane.propTypes = {
id: PropTypes.string.isRequired,
title: PropTypes.node.isRequired,
index: PropTypes.number.isRequired,
laneSortFunction: PropTypes.func,
cards: PropTypes.array.isRequired,
orientation: PropTypes.string.isRequired,
isProcessing: PropTypes.bool.isRequired,
cardSettings: PropTypes.object.isRequired,
maxLaneHeight: PropTypes.number.isRequired,
setMaxLaneHeight: PropTypes.func.isRequired,
maxCardHeight: PropTypes.number.isRequired,
setMaxCardHeight: PropTypes.func.isRequired,
maxCardWidth: PropTypes.number.isRequired,
setMaxCardWidth: PropTypes.func.isRequired,
lastDrag: PropTypes.number
};
const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actions, dispatch)
});
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
technician: selectTechnician
});
export default connect(mapStateToProps, mapDispatchToProps)(Lane);

View File

@@ -0,0 +1,67 @@
import { isEqual, origin } from "./state/position";
export const curves = {
outOfTheWay: "cubic-bezier(0.2, 0, 0, 1)",
drop: "cubic-bezier(.2,1,.1,1)"
};
export const combine = {
opacity: {
// while dropping: fade out totally
drop: 0,
// while dragging: fade out partially
combining: 0.7
},
scale: {
drop: 0.75
}
};
export const timings = {
outOfTheWay: 0.2,
// greater than the out of the way time
// so that when the drop ends everything will
// have to be out of the way
minDropTime: 0.33,
maxDropTime: 0.55
};
// slow timings
// uncomment to use
// export const timings = {
// outOfTheWay: 2,
// // greater than the out of the way time
// // so that when the drop ends everything will
// // have to be out of the way
// minDropTime: 3,
// maxDropTime: 4,
// };
const outOfTheWayTiming = `${timings.outOfTheWay}s ${curves.outOfTheWay}`;
export const placeholderTransitionDelayTime = 0.1;
export const transitions = {
fluid: `opacity ${outOfTheWayTiming}`,
snap: `transform ${outOfTheWayTiming}, opacity ${outOfTheWayTiming}`,
drop: (duration) => {
const timing = `${duration}s ${curves.drop}`;
return `transform ${timing}, opacity ${timing}`;
},
outOfTheWay: `transform ${outOfTheWayTiming}`,
placeholder: `height ${outOfTheWayTiming}, width ${outOfTheWayTiming}, margin ${outOfTheWayTiming}`
};
const moveTo = (offset) => (isEqual(offset, origin) ? null : `translate(${offset.x}px, ${offset.y}px)`);
export const transforms = {
moveTo,
drop: (offset, isCombining) => {
const translate = moveTo(offset);
if (!translate) {
return null;
}
// only transforming the translate
if (!isCombining) {
return translate;
}
// when dropping while combining we also update the scale
return `${translate} scale(${combine.scale.drop})`;
}
};

View File

@@ -0,0 +1,28 @@
const average = (values) => {
const sum = values.reduce((previous, current) => previous + current, 0);
return sum / values.length;
};
export default (groupSize) => {
console.log("Starting average action timer middleware");
console.log(`Will take an average every ${groupSize} actions`);
const bucket = {};
return () => (next) => (action) => {
const start = performance.now();
const result = next(action);
const end = performance.now();
const duration = end - start;
if (!bucket[action.type]) {
bucket[action.type] = [duration];
return result;
}
bucket[action.type].push(duration);
if (bucket[action.type].length < groupSize) {
return result;
}
console.warn(`Average time for ${action.type}`, average(bucket[action.type]));
// reset
bucket[action.type] = [];
return result;
};
};

View File

@@ -0,0 +1,10 @@
import * as timings from "../timings";
export default () => (next) => (action) => {
timings.forceEnable();
const key = `redux action: ${action.type}`;
timings.start(key);
const result = next(action);
timings.finish(key);
return result;
};

View File

@@ -0,0 +1,16 @@
export default (mode = "verbose") =>
(store) =>
(next) =>
(action) => {
if (mode === "light") {
console.log("🏃‍ Action:", action.type);
return next(action);
}
console.group(`action: ${action.type}`);
console.log("action payload", action.payload);
console.log("state before", store.getState());
const result = next(action);
console.log("state after", store.getState());
console.groupEnd();
return result;
};

View File

@@ -0,0 +1,10 @@
export default () => (next) => (action) => {
const title = `👾 redux (action): ${action.type}`;
const startMark = `${action.type}:start`;
const endMark = `${action.type}:end`;
performance.mark(startMark);
const result = next(action);
performance.mark(endMark);
performance.measure(title, startMark, endMark);
return result;
};

View File

@@ -0,0 +1,68 @@
const records = {};
let isEnabled = false;
const isTimingsEnabled = () => isEnabled;
export const forceEnable = () => {
isEnabled = true;
};
// Debug: uncomment to enable
// forceEnable();
export const start = (key) => {
// we want to strip all the code out for production builds
// draw back: can only do timings in dev env (which seems to be fine for now)
if (import.meta.env.DEV) {
if (!isTimingsEnabled()) {
return;
}
const now = performance.now();
records[key] = now;
}
};
export const finish = (key) => {
if (import.meta.env.DEV) {
if (!isTimingsEnabled()) {
return;
}
const now = performance.now();
const previous = records[key];
if (!previous) {
// eslint-disable-next-line no-console
console.warn("cannot finish timing as no previous time found", key);
return;
}
const result = now - previous;
const rounded = result.toFixed(2);
const style = (() => {
if (result < 12) {
return {
textColor: "green",
symbol: "✅"
};
}
if (result < 40) {
return {
textColor: "orange",
symbol: "⚠️"
};
}
return {
textColor: "red",
symbol: "❌"
};
})();
// eslint-disable-next-line no-console
console.log(
`${style.symbol} %cTiming %c${rounded} %cms %c${key}`,
// title
"color: blue; font-weight: bold;",
// result
`color: ${style.textColor}; font-size: 1.1em;`,
// ms
"color: grey;",
// key
"color: purple; font-weight: bold;"
);
}
};

View File

@@ -0,0 +1,44 @@
const isProduction = import.meta.env.PROD;
// not replacing newlines (which \s does)
const spacesAndTabs = /[ \t]{2,}/g;
const lineStartWithSpaces = /^[ \t]*/gm;
// using .trim() to clear the any newlines before the first text and after last text
const clean = (value) => value.replace(spacesAndTabs, " ").replace(lineStartWithSpaces, "").trim();
const getDevMessage = (message) =>
clean(`
%creact-beautiful-dnd
%c${clean(message)}
%c👷 This is a development only message. It will be removed in production builds.
`);
export const getFormattedMessage = (message) => [
getDevMessage(message),
// title (green400)
"color: #00C584; font-size: 1.2em; font-weight: bold;",
// message
"line-height: 1.5",
// footer (purple300)
"color: #723874;"
];
const isDisabledFlag = "__react-beautiful-dnd-disable-dev-warnings";
export function log(type, message) {
// no warnings in production
if (isProduction) {
return;
}
// manual opt out of warnings
if (typeof window !== "undefined" && window[isDisabledFlag]) {
return;
}
// eslint-disable-next-line no-console
console[type](...getFormattedMessage(message));
}
export const warning = log.bind(null, "warn");
export const error = log.bind(null, "error");

View File

@@ -0,0 +1,5 @@
export function noop() {}
export function identity(value) {
return value;
}

View File

@@ -0,0 +1,18 @@
// Components
export { default as DragDropContext } from "./view/drag-drop-context";
export { default as Droppable } from "./view/droppable";
export { default as Draggable } from "./view/draggable";
// Default sensors
export { useMouseSensor, useTouchSensor, useKeyboardSensor } from "./view/use-sensor-marshal";
// Utils
export { resetServerContext } from "./view/drag-drop-context";
// Public flow types
// Droppable types
// Draggable types

View File

@@ -0,0 +1,32 @@
/* eslint-disable no-restricted-syntax */
const isProduction = import.meta.env.PROD;
const prefix = "Invariant failed";
// Want to use this:
// export class RbdInvariant extends Error { }
// But it causes babel to bring in a lot of code
export function RbdInvariant(message) {
this.message = message;
}
// $FlowFixMe
RbdInvariant.prototype.toString = function toString() {
return this.message;
};
// A copy-paste of tiny-invariant but with a custom error type
// Throw an error if the condition fails
export function invariant(condition, message) {
if (condition) {
return;
}
if (isProduction) {
// In production we strip the message but still throw
throw new RbdInvariant(prefix);
} else {
// When not in production we allow the message to pass through
// *This block will be removed in production builds*
throw new RbdInvariant(`${prefix}: ${message || ""}`);
}
}

View File

@@ -0,0 +1,54 @@
/* eslint-disable no-restricted-globals */
export function isInteger(value) {
if (Number.isInteger) {
return Number.isInteger(value);
}
return typeof value === "number" && isFinite(value) && Math.floor(value) === value;
}
// Using this helper to ensure there are correct flow types
// https://github.com/facebook/flow/issues/2221
export function values(map) {
if (Object.values) {
// $FlowFixMe - Object.values currently does not have good flow support
return Object.values(map);
}
return Object.keys(map).map((key) => map[key]);
}
// Could also extend to pass index and list
// TODO: swap order
export function findIndex(list, predicate) {
if (list.findIndex) {
return list.findIndex(predicate);
}
// Using a for loop so that we can exit early
for (let i = 0; i < list.length; i++) {
if (predicate(list[i])) {
return i;
}
}
// Array.prototype.find returns -1 when nothing is found
return -1;
}
export function find(list, predicate) {
if (list.find) {
return list.find(predicate);
}
const index = findIndex(list, predicate);
if (index !== -1) {
return list[index];
}
// Array.prototype.find returns undefined when nothing is found
return undefined;
}
// Using this rather than Array.from as Array.from adds 2kb to the gzip
// document.querySelector actually returns Element[], but flow thinks it is HTMLElement[]
// So we downcast the result to Element[]
export function toArray(list) {
return Array.prototype.slice.call(list);
}

View File

@@ -0,0 +1,91 @@
const dragHandleUsageInstructions = `
Press space bar to start a drag.
When dragging you can use the arrow keys to move the item around and escape to cancel.
Some screen readers may require you to be in focus mode or to use your pass through key
`;
const position = (index) => index + 1;
// We cannot list what index the Droppable is in automatically as we are not sure how
// the Droppable's have been configured
const onDragStart = (start) => `
You have lifted an item in position ${position(start.source.index)}
`;
const withLocation = (source, destination) => {
const isInHomeList = source.droppableId === destination.droppableId;
const startPosition = position(source.index);
const endPosition = position(destination.index);
if (isInHomeList) {
return `
You have moved the item from position ${startPosition}
to position ${endPosition}
`;
}
return `
You have moved the item from position ${startPosition}
in list ${source.droppableId}
to list ${destination.droppableId}
in position ${endPosition}
`;
};
const withCombine = (id, source, combine) => {
const inHomeList = source.droppableId === combine.droppableId;
if (inHomeList) {
return `
The item ${id}
has been combined with ${combine.draggableId}`;
}
return `
The item ${id}
in list ${source.droppableId}
has been combined with ${combine.draggableId}
in list ${combine.droppableId}
`;
};
const onDragUpdate = (update) => {
const location = update.destination;
if (location) {
return withLocation(update.source, location);
}
const combine = update.combine;
if (combine) {
return withCombine(update.draggableId, update.source, combine);
}
return "You are over an area that cannot be dropped on";
};
const returnedToStart = (source) => `
The item has returned to its starting position
of ${position(source.index)}
`;
const onDragEnd = (result) => {
if (result.reason === "CANCEL") {
return `
Movement cancelled.
${returnedToStart(result.source)}
`;
}
const location = result.destination;
const combine = result.combine;
if (location) {
return `
You have dropped the item.
${withLocation(result.source, location)}
`;
}
if (combine) {
return `
You have dropped the item.
${withCombine(result.draggableId, result.source, combine)}
`;
}
return `
The item has been dropped while not over a drop area.
${returnedToStart(result.source)}
`;
};
const preset = {
dragHandleUsageInstructions,
onDragStart,
onDragUpdate,
onDragEnd
};
export default preset;

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