Compare commits

...

144 Commits

Author SHA1 Message Date
Dave Richer
bbd52091d8 IO-2932-Scheduling-Lag-on-AIO:
profiler

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-17 14:06:22 -04:00
Dave Richer
873eb65e75 IO-2932-Scheduling-Lag-on-AIO:
null collaesnce

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-17 13:36:33 -04:00
Dave Richer
4b6e140e3e IO-2932-Scheduling-Lag-on-AIO:
Bump React-Big-Calendar

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-17 13:29:10 -04:00
Dave Richer
8f118937f3 IO-2932-Scheduling-Lag-on-AIO:
Bump React-Big-Calendar

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-17 12:54:19 -04:00
Dave Richer
cd0a08a7be IO-2932-Scheduling-Lag-on-AIO:
Bump React-Big-Calendar

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-17 12:34:11 -04:00
Dave Richer
b0ea516fd6 IO-2932-Scheduling-Lag-on-AIO:
Bump React-Big-Calendar

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-17 12:00:37 -04:00
Dave Richer
10ba19f0d2 IO-2932-Scheduling-Lag-on-AIO:
Full Optimization of all Schedule related components.

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-16 23:02:20 -04:00
Dave Richer
449330441a Merged in release/2024-09-13 (pull request #1725)
Release/2024 09 13 - IO-2913 - IO-2915 - IO-2733 - IO-2923 - IO-2925 - IO-2928 - IO-2927 - IO-2928 - IO-2913 - IO-2733 - IO-2926
2024-09-16 16:28:40 +00:00
Dave Richer
fcab5e6ef2 Merged in feature/IO-2926-Vendor-Discount-Wrapping-In-Parts-Order (pull request #1723)
feature/IO-2926-Vendor-Discount-Wrapping-In-Parts-Order - Fix Wrapping in Vendor Search Select
2024-09-16 16:18:05 +00:00
Dave Richer
0212b837ea feature/IO-2926-Vendor-Discount-Wrapping-In-Parts-Order - Fix Wrapping in Vendor Search Select
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-16 12:16:29 -04:00
Patrick Fic
e7438a099e Merged in feature/IO-2733-pwa-timer (pull request #1721)
IO-2733 Add loading state and further delay reload.
2024-09-13 18:24:11 +00:00
Patrick Fic
b3303e3c38 IO-2733 Add loading state and further delay reload. 2024-09-13 11:22:38 -07:00
Patrick Fic
c69c86d193 Merged in feature/IO-2733-pwa-timer (pull request #1719)
IO-2733 Resolve notification showing incorrect time.
2024-09-13 17:51:33 +00:00
Patrick Fic
73ec8b8a70 IO-2733 Resolve notification showing incorrect time. 2024-09-13 10:51:04 -07:00
Patrick Fic
af09796df8 Merged in feature/IO-2733-pwa-timer (pull request #1717)
IO-2733 Add Timer Started check to prevent auto refresh early.
2024-09-13 16:59:58 +00:00
Patrick Fic
954504de8d IO-2733 Add Timer Started check to prevent auto refresh early. 2024-09-13 09:58:46 -07:00
Allan Carr
0aba040338 Merged in feature/IO-2928-QBO-CAUSA-Payable-TAX (pull request #1714)
IO-2928 QBO CA US Tax  Accumulator

Approved-by: Patrick Fic
2024-09-13 14:43:58 +00:00
Allan Carr
c3bfe87674 Merge branch 'feature/IO-2913-ADP-Payroll' into release/2024-09-13
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-09-12 19:59:47 -07:00
Allan Carr
9aa1279144 IO-2913 Add in Translations
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-09-12 19:53:50 -07:00
Allan Carr
4e6c45b195 IO-2928 Null coalesce billline amount
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-09-12 17:13:29 -07:00
Allan Carr
4fdb939bd2 Merged in feature/IO-2927-qbo-usa-gst-itc (pull request #1715)
IO-2927 Extend Logging passthru

Approved-by: Patrick Fic
2024-09-12 23:58:24 +00:00
Allan Carr
062a1dcc72 IO-2927 Extend Logging passthru
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-09-12 16:29:37 -07:00
Allan Carr
7b420b1855 IO-2928 QBO CA US Tax Accumulator
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-09-12 16:27:43 -07:00
Allan Carr
40f61bbc8f Merged in feature/IO-2927-qbo-usa-gst-itc (pull request #1713)
IO-2927 Correct accountmeta
2024-09-12 22:23:54 +00:00
Allan Carr
f5d821c394 Merged in feature/IO-2927-qbo-usa-gst-itc (pull request #1712)
IO-2927 Correct accountmeta
2024-09-12 22:12:32 +00:00
Allan Carr
3958ec9189 IO-2927 Correct accountmeta
accounts doesn't exist in recievables, switch to items

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-09-12 15:11:06 -07:00
Allan Carr
1e4f52e541 Merged in feature/IO-2927-qbo-usa-gst-itc (pull request #1710)
Feature/IO-2927 qbo usa gst itc

IO-2927
2024-09-12 20:33:27 +00:00
Patrick Fic
5cc5cb444e Merged in feature/IO-2927-qbo-usa-gst-itc (pull request #1709)
IO-2997 Add better error handling for 400 requests.

Approved-by: Allan Carr
2024-09-12 20:26:16 +00:00
Patrick Fic
4acf0c59ca IO-2997 Remove unnecessary comment. 2024-09-12 13:25:14 -07:00
Patrick Fic
2858a5e871 IO-2997 Add better error handling for 400 requests. 2024-09-12 13:23:53 -07:00
Patrick Fic
24496d3ee1 Merged in feature/IO-2927-qbo-usa-gst-itc (pull request #1707)
IO-2927 Update QBO Payable to use ITC.

Approved-by: Allan Carr
2024-09-12 20:04:33 +00:00
Patrick Fic
0a5df69b12 IO-2927 Update QBO Payable to use ITC. 2024-09-12 13:03:23 -07:00
Patrick Fic
80efea02c6 Merged in feature/IO-2925-ppc-40-points (pull request #1704)
IO-2925 Add 40% as PPC choice.

Approved-by: Allan Carr
2024-09-12 17:15:03 +00:00
Patrick Fic
9f5c282b41 IO-2925 Add 40% as PPC choice. 2024-09-12 09:19:49 -07:00
Allan Carr
b2602c3385 Merged in feature/IO-2923-Edit-Bill-Line-original_actual_price (pull request #1702)
IO-2923 Edit Bill Line original_actual_price

Approved-by: Dave Richer
2024-09-12 15:54:58 +00:00
Allan Carr
0e584af424 IO-2923 Edit Bill Line original_actual_price
IO-2923 Edit Bill Line original_actual_price

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-09-11 16:36:28 -07:00
Patrick Fic
cdc3de2a33 Merge branch 'feature/IO-2733-pwa-timer' into release/2024-09-13 2024-09-10 15:58:59 -07:00
Patrick Fic
3bfa556b02 IO-2733 Added countdown timer to PWA Refresh & cache busting meta. 2024-09-10 15:54:15 -07:00
Allan Carr
44cb7577e2 Merged in feature/IO-2913-ADP-Payroll (pull request #1698)
IO-2913 ADP Payroll Reports

Approved-by: Dave Richer
2024-09-10 20:24:51 +00:00
Allan Carr
46d2b08477 Merged in feature/IO-2915-Customer-Portion-Totals-Federal-Tax (pull request #1699)
IO-2915 Customer Portion Totals - Federal Tax

Approved-by: Dave Richer
2024-09-10 20:24:13 +00:00
Allan Carr
0193ff9e65 IO-2915 Customer Portion Totals - Federal Tax
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-09-10 11:55:42 -07:00
Allan Carr
fd9a51209f IO-2913 ADP Payroll Reports
Uses ADPPayroll Split

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-09-10 11:21:03 -07:00
Dave Richer
d0a7b87e04 Merged in feature/IO-2916-Remove-Beta-Switch-AIO (pull request #1693)
feature/IO-2916-Remove-Beta-Switch-AIO - Remove Beta Switch
2024-09-10 17:29:31 +00:00
Dave Richer
799b24c90e feature/IO-2916-Remove-Beta-Switch-LEGACY - Remove cookie
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-10 13:16:08 -04:00
Dave Richer
3e1a8c87d1 feature/IO-2916-Remove-Beta-Switch-AIO - Remove cookie
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-10 13:10:49 -04:00
Dave Richer
c886d874de feature/IO-2916-Remove-Beta-Switch-AIO - Remove Beta Switch
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-09 23:34:54 -04:00
Allan Carr
4dfb020089 Merged in release/2024-09-06 (pull request #1691)
Release/2024 09 06
2024-09-07 01:08:25 +00:00
Patrick Fic
bc6f05acbc IO-2907 change CI step to deploy instead of build 2024-09-06 13:55:53 -07:00
Patrick Fic
2701bbd501 IO-2907 Resolve Hasura on CI and improve Jira notify. 2024-09-06 13:43:58 -07:00
Patrick Fic
1f2040d97c IO-2907 Updated CI to update Jira #1. 2024-09-06 13:32:48 -07:00
Allan Carr
43963a3e91 Merged in feature/IO-2904-Production-Board-Visual-Subtotal (pull request #1687)
IO-2904 Production Board Visual Subtotal

Approved-by: Dave Richer
2024-09-06 18:10:49 +00:00
Allan Carr
4287311adb Merged in feature/IO-2902-Duplicate-RBAC-Items (pull request #1686)
IO-2902 Duplicate RBAC Items

Approved-by: Dave Richer
2024-09-06 18:09:15 +00:00
Allan Carr
d0e8589a76 Merged in feature/IO-2893-Editing-Shift-Tickets (pull request #1685)
IO-2893 Enhance disable of editing of tickets

Approved-by: Dave Richer
2024-09-06 18:08:26 +00:00
Allan Carr
c4bab72947 DB change to .env
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-09-05 16:36:59 -07:00
Allan Carr
aa4b4998fa IO-2904 Production Board Visual Subtotal
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-09-05 16:18:49 -07:00
Patrick Fic
ed4566e00f Merge branch 'feature/IO-2707-timeticket-rerender' into release/2024-09-06 2024-09-05 13:51:42 -07:00
Patrick Fic
5c2cdfe16c Merge branch 'feature/IO-2724-tech-modal-duplicated' into release/2024-09-06 2024-09-05 13:51:34 -07:00
Patrick Fic
12c75357b5 Revert IP tracking to only single device users. 2024-09-05 11:53:48 -07:00
Patrick Fic
d40f3ee45a Add in IP tracking for SingleDeviceOnly checks. 2024-09-05 11:52:06 -07:00
Allan Carr
96a0def846 IO-2902 Fix prettier formatting
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-09-05 11:44:26 -07:00
Allan Carr
1fd595d0de IO-2902 Duplicate RBAC Items
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-09-05 11:42:11 -07:00
Allan Carr
52cf4f3d1f IO-2893 Enhance disable of editing of tickets
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-09-05 11:27:51 -07:00
Dave Richer
4d9be1d232 Merged in bugfix/dinero-for-production-board-amounts (pull request #1684)
- removed unused function
2024-09-05 17:59:41 +00:00
Dave Richer
fb2bc20b4f - removed unused function
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-05 13:59:03 -04:00
Dave Richer
744593e96a Merged in bugfix/dinero-for-production-board-amounts (pull request #1682)
Bugfix/dinero for production board amounts
2024-09-05 17:40:27 +00:00
Dave Richer
1e9308be9b Merged in bugfix/dinero-for-production-board-amounts (pull request #1681)
- Use Dinero in place of straight math in production board
2024-09-05 17:40:00 +00:00
Dave Richer
411605e121 - Use Dinero in place of straight math in production board
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-05 13:38:50 -04:00
Dave Richer
1da8d6abb3 Merged in bugfix/dinero-for-production-board-amounts (pull request #1680)
- Use Dinero in place of straight math in production board
2024-09-05 16:48:21 +00:00
Dave Richer
cdcef798df - Use Dinero in place of straight math in production board
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-05 12:47:19 -04:00
Patrick Fic
f7207a9f3f Add missing PWA dependency for Vite. 2024-09-05 09:17:42 -07:00
Patrick Fic
7a54b55bd4 IO-2724 Resolve tech console showing 2 drawers on production board. 2024-09-05 08:26:49 -07:00
Patrick Fic
991dfc2ad5 IO-2707 resolve time ticket modal rerender issue. 2024-09-05 08:17:45 -07:00
Allan Carr
718c8291a8 Merged in release/2024-08-30 (pull request #1679)
IO-2894 Null check
2024-09-03 16:01:19 +00:00
Allan Carr
f1e84c348b Merged in feature/IO-2894-Modify-Shift-Memo (pull request #1677)
IO-2894 Null check
2024-09-03 15:54:13 +00:00
Allan Carr
2a2d399a98 IO-2894 Null check
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-09-03 08:54:06 -07:00
Allan Carr
5f513a8bef Merged in release/2024-08-30 (pull request #1676)
IO-2892 Correct Cron Trigger
2024-08-31 18:56:03 +00:00
Allan Carr
4b96d5a707 Merged in feature/IO-2892-Autohouse-Claimscorp-Cron (pull request #1674)
IO-2892 Correct Cron Trigger
2024-08-31 18:53:26 +00:00
Allan Carr
220f3d4410 IO-2892 Correct Cron Trigger
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-31 11:53:02 -07:00
Allan Carr
841f62bd84 Merged in release/2024-08-30 (pull request #1673)
Release/2024 08 30
2024-08-30 20:34:21 +00:00
Allan Carr
f3f16b78d5 IO-2894 Prettier code
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-29 15:00:21 -07:00
Allan Carr
91e2e7931b Merged in feature/IO-2894-Modify-Shift-Memo (pull request #1671)
Feature/IO-2894 Modify Shift Memo
2024-08-29 22:00:14 +00:00
Allan Carr
1e855799f8 IO-2894 Modify Shift Memo
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-29 14:59:14 -07:00
Allan Carr
3c6faf8473 Merged in feature/IO-2893-Editing-Shift-Tickets (pull request #1670)
Feature/IO-2893 Editing Shift Tickets
2024-08-29 20:48:43 +00:00
Allan Carr
c994eaaa8e IO-2893 Correct RBACs for editing tickets
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-29 13:48:39 -07:00
Allan Carr
517d8f4163 IO-2893 Editing Shift Tickets
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-29 13:39:21 -07:00
Allan Carr
9deb2964a5 Merged in feature/IO-2892-Autohouse-Claimscorp-Cron (pull request #1668)
Feature/IO-2892 Autohouse Claimscorp Cron
2024-08-29 20:03:23 +00:00
Allan Carr
9cf9f8b844 Merged in feature/IO-2901-Production-Board-List-empty-config (pull request #1669)
IO-2901 Production Board List config
2024-08-29 20:03:12 +00:00
Allan Carr
ad46ea74c0 IO-2901 Production Board List config
If production_config=[] then board crashes as it can't find production_config[0]

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-29 13:03:30 -07:00
Allan Carr
2a28855e4b IO-2892 Gate for non-production environment
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-29 08:47:59 -07:00
Allan Carr
8d25f60097 Merge branch 'master-AIO' into feature/IO-2892-Autohouse-Claimscorp-Cron
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>

# Conflicts:
#	hasura/metadata/cron_triggers.yaml
2024-08-28 17:29:15 -07:00
Allan Carr
982a51f16e Merged in feature/IO-2890-Kaizen-Datapump-Cron (pull request #1666)
IO-2890 Gate if environment isn't Production
2024-08-28 23:17:17 +00:00
Allan Carr
68d02648d7 IO-2890 Gate if environment isn't Production
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-28 16:15:32 -07:00
Allan Carr
6e8122849a Merged in release/2024-08-23 (pull request #1665)
IO-2890 Update Time
2024-08-24 06:57:41 +00:00
Allan Carr
b04ae84941 IO-2890 Update Time
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-23 23:56:37 -07:00
Allan Carr
932979d5fb Merged in feature/IO-2890-Kaizen-Datapump-Cron (pull request #1663)
IO-2890 Update Time
2024-08-24 06:56:34 +00:00
Dave Richer
f7ef32c58d Merged in release/2024-08-23 (pull request #1662)
Release/2024 08 23
2024-08-24 02:08:03 +00:00
Dave Richer
f7108b4b8c Merged in feature/IO-2834-Enhance-DateTime-Picker (pull request #1660)
- Final DateTimePicker update
2024-08-23 19:59:32 +00:00
Dave Richer
882038a794 - Final DateTimePicker update
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-23 15:55:54 -04:00
Allan Carr
aec23fe46b Merged in feature/IO-2890-Kaizen-Datapump-Cron (pull request #1657)
IO-2890 Kaizen Datapump Cron

Approved-by: Dave Richer
2024-08-23 15:51:58 +00:00
Allan Carr
89d5b1cfe4 IO-2892 Autohouse & Claimscorp Data Pump Cron
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-22 16:42:42 -07:00
Allan Carr
35ac0b0c6a IO-2890 Kaizen Datapump Cron
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-22 16:36:00 -07:00
Allan Carr
2a2a0f8961 Merged in feature/IO-2520-Kaizen-Data-Pump (pull request #1655)
IO-2520 Change Logging back to default and adjust start and end to be default

Approved-by: Dave Richer
2024-08-22 20:28:01 +00:00
Allan Carr
d9902b9744 Merged in feature/IO-2895-Adjustment-to-bottom (pull request #1654)
IO-2895 Adjustment to Bottom Line
2024-08-22 20:12:46 +00:00
Allan Carr
f82478a362 IO-2895 Adjustment to Bottom Line
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-22 13:11:00 -07:00
Allan Carr
bb3d3fbe72 IO-2520 Change Logging back to default and adjust start and end to be default
Datapump will be run daily as per Sofia

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-22 11:35:15 -07:00
Allan Carr
4fa0593bb5 Merge branch 'master-AIO' into feature/IO-2520-Kaizen-Data-Pump
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-22 10:23:57 -07:00
Dave Richer
41517ca7d4 Merged in release/2024-08-23 (pull request #1653)
Release/2024 08 23
2024-08-22 14:44:29 +00:00
Dave Richer
35c9f649ad - Rollback ZOHO
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-22 10:41:27 -04:00
Dave Richer
ad2f2e55a5 Merge branch 'feature/IO-2834-Enhance-DateTime-Picker' into release/2024-08-23 2024-08-21 21:09:17 -04:00
Dave Richer
41c446ddb3 - Checkpoint
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-21 21:04:52 -04:00
Dave Richer
7d6aa8489d - Checkpoint
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-21 20:49:50 -04:00
Allan Carr
63f1e0f07c IO-2834 Placeholder Translations
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-21 15:44:00 -07:00
Dave Richer
98f4423624 - Checkpoint
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-21 17:55:40 -04:00
Dave Richer
1ac4cbb59f Merged in feature/IO-2886-Product-List-Profiles (pull request #1647)
Feature/IO-2886 Product List Profiles
2024-08-21 20:51:47 +00:00
Allan Carr
24ebfbfbf5 Merged in feature/IO-2888-Production-Employee-Sort-Enhancment (pull request #1646)
IO-2888 Production List Employee Sort Enhacement

Approved-by: Dave Richer
2024-08-21 20:50:51 +00:00
Dave Richer
7ff1051d3c - Checkpoint
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-21 16:49:29 -04:00
Dave Richer
8af3364660 - Checkpoint
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-21 16:31:42 -04:00
Dave Richer
02f4677aef - Checkpoint
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-21 16:22:56 -04:00
Dave Richer
11785f3b86 - Checkpoint
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-21 15:39:16 -04:00
Dave Richer
90532427b6 - Checkpoint
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-21 15:32:22 -04:00
Allan Carr
cc9979ff4b IO-2834 Split Date and DateTime formats, remove shorthand and checks
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-21 12:25:38 -07:00
Dave Richer
c89e4f1b41 - Checkpoint
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-21 15:14:33 -04:00
Dave Richer
c3e6d3dc48 - Checkpoint
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-21 15:09:13 -04:00
Allan Carr
ad1ce7b220 IO-2888 Production List Employee Sort Enhacement
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-21 11:00:59 -07:00
Dave Richer
fd4dbdfb3a Merged in feature/IO-2834-Enhance-DateTime-Picker (pull request #1644)
- enhancements / improvements / stuff
2024-08-21 17:54:39 +00:00
Dave Richer
153cf6a840 - enhancements / improvements / stuff
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-21 13:53:43 -04:00
Dave Richer
a567d0d6dd - enhancements / improvements / stuff
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-21 13:46:08 -04:00
Dave Richer
297599a45b Merged in feature/IO-2834-Enhance-DateTime-Picker (pull request #1642)
- enhancements / improvements / stuff
2024-08-21 16:51:50 +00:00
Dave Richer
678ca591c1 - enhancements / improvements / stuff
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-21 12:50:11 -04:00
Allan Carr
c19f8167e8 Merged in feature/IO-2887-Returnfrombill-Parts-Drawer (pull request #1640)
IO-2887 Null out BillData if returnfrombill is not available
2024-08-21 00:12:03 +00:00
Allan Carr
cc2d474fda IO-2887 Null out BillData if returnfrombill is not available
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-20 17:12:11 -07:00
Dave Richer
9058aca16e - only load chataffix in prod, no more annoying messages / alert dismissals in dev
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-20 14:44:28 -04:00
Dave Richer
1c186f7fa5 - fix missed FormDatePicker reference
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-20 14:27:28 -04:00
Dave Richer
46da3285f8 - Revert ZOHO (put back in)
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-20 14:20:58 -04:00
Dave Richer
b419929ad7 Merged in feature/IO-2886-Product-List-Profiles (pull request #1633)
Feature/IO-2886 Product List Profiles
2024-08-20 18:13:13 +00:00
Dave Richer
8018daa2dc - Address changes to profile from call
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-20 14:12:19 -04:00
Dave Richer
1e7c285fef - Address changes to profile from call
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-20 13:46:35 -04:00
Dave Richer
0b072e6089 Merged in feature/IO-2834-Enhance-DateTime-Picker (pull request #1632)
- Enhance Date Time Picker
2024-08-20 17:27:24 +00:00
Allan Carr
4fd6203987 Merged in feature/IO-2887-Returnfrombill-Parts-Drawer (pull request #1631)
IO-2887 Returnfrombill Parts Drawer Info

Approved-by: Dave Richer
2024-08-20 17:25:41 +00:00
Dave Richer
51d264098c - Enhance Date Time Picker
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-20 13:24:26 -04:00
Allan Carr
680a66b156 IO-2887 Returnfrombill Parts Drawer Info
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-08-20 10:00:52 -07:00
Dave Richer
481a14e529 Merged in feature/IO-2886-Product-List-Profiles (pull request #1629)
- Improve profile handling in product list view
2024-08-20 14:45:41 +00:00
Dave Richer
f3e43334c4 - Improve profile handling in product list view
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-20 10:44:38 -04:00
Dave Richer
0054b00d01 - Improve profile handling in product list view
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-19 17:45:36 -04:00
98 changed files with 7520 additions and 4896 deletions

View File

@@ -5,6 +5,7 @@ orbs:
aws-s3: circleci/aws-s3@4.0.0
aws-cli: circleci/aws-cli@4.0
eb: circleci/aws-elastic-beanstalk@2.0.1
jira: circleci/jira@2.1.0
jobs:
imex-api-deploy:
docker:
@@ -18,6 +19,12 @@ jobs:
eb status --verbose
eb deploy
eb status
- jira/notify:
environment: Production (ImEX) - API
environment_type: production
job_type: deployment
pipeline_id: << pipeline.id >>
pipeline_number: << pipeline.number >>
imex-hasura-migrate:
docker:
@@ -33,11 +40,16 @@ jobs:
- run:
name: Execute migration
command: |
npm install hasura-cli -g
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
hasura migrate apply --endpoint https://db.imex.online/ --admin-secret << parameters.secret >>
hasura metadata apply --endpoint https://db.imex.online/ --admin-secret << parameters.secret >>
hasura metadata reload --endpoint https://db.imex.online/ --admin-secret << parameters.secret >>
- jira/notify:
environment: Production (ImEX) - Hasura
environment_type: production
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
imex-app-build:
docker:
- image: cimg/node:18.18.2
@@ -62,6 +74,7 @@ jobs:
to: "s3://imex-online-production/"
arguments: "--exclude '*.map'"
imex-app-beta-build:
docker:
- image: cimg/node:18.18.2
@@ -86,6 +99,12 @@ jobs:
from: dist
to: "s3://imex-online-beta/"
arguments: "--exclude '*.map'"
- jira/notify:
environment: Production (ImEX) - Front End
environment_type: production
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
rome-api-deploy:
docker:
@@ -99,7 +118,12 @@ jobs:
eb status --verbose
eb deploy
eb status
- jira/notify:
environment: Production (Rome) - API
environment_type: production
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
rome-hasura-migrate:
docker:
- image: cimg/node:18.18.2
@@ -114,11 +138,16 @@ jobs:
- run:
name: Execute migration
command: |
npm install hasura-cli -g
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
hasura migrate apply --endpoint https://db.romeonline.io/ --admin-secret << parameters.secret >>
hasura metadata apply --endpoint https://db.romeonline.io/ --admin-secret << parameters.secret >>
hasura metadata reload --endpoint https://db.romeonline.io/ --admin-secret << parameters.secret >>
- jira/notify:
environment: Production (Rome) - Hasura
environment_type: production
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
rome-app-build:
docker:
- image: cimg/node:18.18.2
@@ -143,6 +172,12 @@ jobs:
from: dist
to: "s3://rome-online-production/"
arguments: "--exclude '*.map'"
- jira/notify:
environment: Production (Rome) - Front End
environment_type: production
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
promanager-app-build:
docker:
@@ -168,6 +203,12 @@ jobs:
from: dist
to: "s3://promanager-production/"
arguments: "--exclude '*.map'"
- jira/notify:
environment: Production (ProManager) - Front End
environment_type: production
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
test-rome-hasura-migrate:
docker:
@@ -183,10 +224,18 @@ jobs:
- run:
name: Execute migration
command: |
npm install hasura-cli -g
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
hasura migrate apply --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >>
sleep 5
hasura metadata apply --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >>
sleep 10
hasura metadata reload --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >>
- jira/notify:
environment: Test (Rome) - Hasura
environment_type: testing
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
test-rome-app-build:
docker:
@@ -212,6 +261,12 @@ jobs:
from: dist
to: "s3://rome-online-test/"
arguments: "--exclude '*.map'"
- jira/notify:
environment: Test (Rome) - Front End
environment_type: testing
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
test-promanager-app-build:
docker:
@@ -237,6 +292,12 @@ jobs:
from: dist
to: "s3://promanager-testing/"
arguments: "--exclude '*.map'"
- jira/notify:
environment: Test (ProManager) - Front End
environment_type: testing
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
test-hasura-migrate:
docker:
@@ -252,10 +313,18 @@ jobs:
- run:
name: Execute migration
command: |
npm install hasura-cli -g
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
hasura migrate apply --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
sleep 15
hasura metadata apply --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
sleep 30
hasura metadata reload --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
- jira/notify:
environment: Test (ImEX) - Hasura
environment_type: testing
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
imex-test-app-build:
docker:
@@ -302,7 +371,12 @@ jobs:
from: dist
to: "s3://imex-online-test-beta/"
arguments: "--exclude '*.map'"
- jira/notify:
environment: Test (ImEX) - Front End
environment_type: testing
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
admin-app-build:
docker:
@@ -353,7 +427,7 @@ workflows:
secret: ${HASURA_PROD_SECRET}
filters:
branches:
only: master
only: master-AIO
- rome-api-deploy:
filters:
branches:
@@ -363,7 +437,7 @@ workflows:
branches:
only: master-AIO
- rome-hasura-migrate:
secret: ${HASURA_PROD_SECRET}
secret: ${HASURA_ROME_PROD_SECRET}
filters:
branches:
only: master-AIO

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.imex.online/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.imex.online/v1/graphql
VITE_APP_GA_CODE=231099835
VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test

View File

@@ -1,5 +1,5 @@
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.imex.online/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.imex.online/v1/graphql
VITE_APP_GA_CODE=231099835
VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test

View File

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

View File

@@ -2,6 +2,9 @@
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
<link rel="icon" href="/favicon.png"/>
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>

View File

@@ -47,7 +47,7 @@
"query-string": "^9.1.0",
"raf-schd": "^4.0.3",
"react": "^18.3.1",
"react-big-calendar": "^1.13.2",
"react-big-calendar": "^1.14.1",
"react-color": "^2.19.3",
"react-cookie": "^7.2.0",
"react-dom": "^18.3.1",
@@ -109,7 +109,8 @@
"vite-plugin-legacy": "^2.1.0",
"vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-pwa": "^0.20.1",
"vite-plugin-style-import": "^2.0.0"
"vite-plugin-style-import": "^2.0.0",
"workbox-window": "^7.1.0"
},
"engines": {
"node": ">=18.18.2"
@@ -14670,9 +14671,9 @@
}
},
"node_modules/react-big-calendar": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-1.13.2.tgz",
"integrity": "sha512-yzeVRM1I+JloeJXytrZx2lJWKUfLAi5bsgGuBjh3aFSHZrdFcGnfA7LE6pBacdyOG+NGP+332m2MziszkmQWcw==",
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-1.14.1.tgz",
"integrity": "sha512-6Le0kV/4yiV/mlqv5YYBBS+FaBeYBPNGjcYitLoVdPCiXsc0xzSHyX8+2FRqX9AM16XZYIjjomouK3wcnq6+XQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.7",
@@ -18429,6 +18430,7 @@
"resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.1.0.tgz",
"integrity": "sha512-ZHeROyqR+AS5UPzholQRDttLFqGMwP0Np8MKWAdyxsDETxq3qOAyXvqessc3GniohG6e0mAqSQyKOHmT8zPF7g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/trusted-types": "^2.0.2",
"workbox-core": "7.1.0"

View File

@@ -47,7 +47,7 @@
"query-string": "^9.1.0",
"raf-schd": "^4.0.3",
"react": "^18.3.1",
"react-big-calendar": "^1.13.2",
"react-big-calendar": "^1.14.1",
"react-color": "^2.19.3",
"react-cookie": "^7.2.0",
"react-dom": "^18.3.1",
@@ -153,6 +153,7 @@
"vite-plugin-legacy": "^2.1.0",
"vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-pwa": "^0.20.1",
"vite-plugin-style-import": "^2.0.0"
"vite-plugin-style-import": "^2.0.0",
"workbox-window": "^7.1.0"
}
}

View File

@@ -18,7 +18,6 @@ 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/handleBeta";
import Eula from "../components/eula/eula.component";
import InstanceRenderMgr from "../utils/instanceRenderMgr";
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
@@ -108,8 +107,6 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
return <LoadingSpinner message={t("general.labels.loggingin")} />;
}
handleBeta();
if (!online) {
return (
<Result

View File

@@ -98,7 +98,7 @@ export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail
});
billlines.forEach((billline) => {
const { deductedfromlbr, inventories, jobline, ...il } = billline;
const { deductedfromlbr, inventories, jobline, original_actual_price, create_ppc, ...il } = billline;
delete il.__typename;
if (il.id) {

View File

@@ -14,7 +14,6 @@ import dayjs from "../../utils/day";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import AlertComponent from "../alert/alert.component";
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import JobSearchSelect from "../job-search-select/job-search-select.component";
@@ -22,6 +21,7 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import BillFormLines from "./bill-form.lines.component";
import { CalculateBillTotal } from "./bill-form.totals.utility";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -276,7 +276,7 @@ export function BillFormComponent({
})
]}
>
<FormDatePicker disabled={disabled} />
<DateTimePicker isDateOnly disabled={disabled} />
</Form.Item>
<Form.Item
label={t("bills.fields.is_credit_memo")}

View File

@@ -8,8 +8,8 @@ import { DateFormatter } from "../../utils/DateFormatter";
import ContractStatusSelector from "../contract-status-select/contract-status-select.component";
import ContractsRatesChangeButton from "../contracts-rates-change-button/contracts-rates-change-button.component";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormDateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import InputPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
@@ -196,7 +196,7 @@ export default function ContractFormComponent({ form, create = false, selectedJo
}
]}
>
<FormDatePicker />
<DateTimePicker isDateOnly />
</Form.Item>
{dlExpiresBeforeReturn && (
<Space style={{ color: "tomato" }}>
@@ -274,7 +274,7 @@ export default function ContractFormComponent({ form, create = false, selectedJo
<InputPhone />
</Form.Item>
<Form.Item label={t("contracts.fields.driver_dob")} name="driver_dob">
<FormDatePicker />
<DateTimePicker isDateOnly />
</Form.Item>
</LayoutFormRow>
<ContractsRatesChangeButton form={form} />

View File

@@ -10,16 +10,12 @@ import { DateFormatter } from "../../utils/DateFormatter";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
import CourtesyCarReadiness from "../courtesy-car-readiness-select/courtesy-car-readiness-select.component";
import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
export default function CourtesyCarCreateFormComponent({
form,
saveLoading,
newCC,
}) {
export default function CourtesyCarCreateFormComponent({ form, saveLoading, newCC }) {
const { t } = useTranslation();
const client = useApolloClient();
@@ -161,16 +157,16 @@ export default function CourtesyCarCreateFormComponent({
<Input />
</Form.Item>
<Form.Item label={t("courtesycars.fields.purchasedate")} name="purchasedate">
<FormDatePicker />
<DateTimePicker isDateOnly />
</Form.Item>
<Form.Item label={t("courtesycars.fields.servicestartdate")} name="servicestartdate">
<FormDatePicker />
<DateTimePicker isDateOnly />
</Form.Item>
<Form.Item label={t("courtesycars.fields.serviceenddate")} name="serviceenddate">
<FormDatePicker />
<DateTimePicker isDateOnly />
</Form.Item>
<Form.Item label={t("courtesycars.fields.leaseenddate")} name="leaseenddate">
<FormDatePicker />
<DateTimePicker isDateOnly />
</Form.Item>
</LayoutFormRow>
@@ -228,7 +224,7 @@ export default function CourtesyCarCreateFormComponent({
</div>
<div>
<Form.Item label={t("courtesycars.fields.nextservicedate")} name="nextservicedate">
<FormDatePicker />
<DateTimePicker isDateOnly />
</Form.Item>
<Form.Item shouldUpdate={(p, c) => p.nextservicedate !== c.nextservicedate}>
{() => {
@@ -260,7 +256,7 @@ export default function CourtesyCarCreateFormComponent({
</Form.Item>
<div>
<Form.Item label={t("courtesycars.fields.registrationexpires")} name="registrationexpires">
<FormDatePicker />
<DateTimePicker isDateOnly />
</Form.Item>
<Form.Item shouldUpdate={(p, c) => p.registrationexpires !== c.registrationexpires}>
{() => {
@@ -293,7 +289,7 @@ export default function CourtesyCarCreateFormComponent({
}
]}
>
<FormDatePicker />
<DateTimePicker isDateOnly />
</Form.Item>
<Form.Item shouldUpdate={(p, c) => p.insuranceexpires !== c.insuranceexpires}>
{() => {

View File

@@ -2,7 +2,7 @@ import { Form, InputNumber } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
export default function CourtesyCarReturnModalComponent() {
const { t } = useTranslation();
@@ -19,7 +19,7 @@ export default function CourtesyCarReturnModalComponent() {
}
]}
>
<FormDatePicker />
<DateTimePicker isDateOnly />
</Form.Item>
<Form.Item
label={t("contracts.fields.kmend")}

View File

@@ -24,9 +24,9 @@ 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";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -164,7 +164,7 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
<Input disabled />
</Form.Item>
<Form.Item name="inservicedate" label={t("jobs.fields.dms.inservicedate")}>
<FormDatePicker />
<DateTimePicker isDateOnly />
</Form.Item>
</LayoutFormRow>
<Space>

View File

@@ -4,7 +4,6 @@ import Markdown from "react-markdown";
import { createStructuredSelector } from "reselect";
import { selectCurrentEula, selectCurrentUser } from "../../redux/user/user.selectors";
import { connect } from "react-redux";
import { FormDatePicker } from "../form-date-picker/form-date-picker.component";
import { INSERT_EULA_ACCEPTANCE } from "../../graphql/user.queries";
import { useMutation } from "@apollo/client";
import { acceptEula } from "../../redux/user/user.actions";
@@ -12,6 +11,7 @@ import { useTranslation } from "react-i18next";
import day from "../../utils/day";
import "./eula.styles.scss";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const Eula = ({ currentEula, currentUser, acceptEula }) => {
const [formReady, setFormReady] = useState(false);
@@ -216,7 +216,7 @@ const EulaFormComponent = ({ form, handleChange, onFinish, t }) => (
}
]}
>
<FormDatePicker onChange={handleChange} onlyToday aria-label={t("eula.labels.date_accepted")} />
<DateTimePicker isDateOnly onChange={handleChange} onlyToday aria-label={t("eula.labels.date_accepted")} />
</Form.Item>
</Col>
</Row>

View File

@@ -1,123 +0,0 @@
import { DatePicker } from "antd";
import dayjs from "../../utils/day";
import React, { useRef } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(FormDatePicker);
const dateFormat = "MM/DD/YYYY";
export function FormDatePicker({
bodyshop,
value,
onChange,
onBlur,
onlyFuture,
onlyToday,
isDateOnly = true,
...restProps
}) {
const ref = useRef();
const handleChange = (newDate) => {
if (value !== newDate && onChange) {
onChange(isDateOnly ? newDate && newDate.format("YYYY-MM-DD") : newDate);
}
};
const handleKeyDown = (e) => {
if (e.key.toLowerCase() === "t") {
if (onChange) {
onChange(isDateOnly ? dayjs().format("YYYY-MM-DD") : dayjs());
}
} else if (e.key.toLowerCase() === "enter") {
if (ref.current && ref.current.blur) ref.current.blur();
}
};
const handleBlur = (e) => {
const v = e.target.value;
if (!v) return;
const formats = [
"MMDDYY",
"MMDDYYYY",
"MM/DD/YY",
"MM/DD/YYYY",
"M/DD/YY",
"M/DD/YYYY",
"MM/D/YY",
"MM/D/YYYY",
"M/D/YY",
"M/D/YYYY",
"D/MM/YY",
"D/MM/YYYY",
"DD/M/YY",
"DD/M/YYYY",
"D/M/YY",
"D/M/YYYY"
];
let _a;
// Iterate through formats to find the correct one
for (let format of formats) {
_a = dayjs(v, format);
if (v === _a.format(format)) {
break;
}
}
if (_a.isValid() && value && value.isValid && value.isValid()) {
_a.set({
hours: value.hours(),
minutes: value.minutes(),
seconds: value.seconds(),
milliseconds: value.milliseconds()
});
}
if (_a.isValid() && onChange) {
if (onlyFuture) {
if (dayjs().subtract(1, "day").isBefore(_a)) {
onChange(isDateOnly ? _a.format("YYYY-MM-DD") : _a);
} else {
onChange(isDateOnly ? dayjs().format("YYYY-MM-DD") : dayjs());
}
} else {
onChange(isDateOnly ? _a.format("YYYY-MM-DD") : _a);
}
}
};
return (
<div onKeyDown={handleKeyDown}>
<DatePicker
ref={ref}
value={value ? dayjs(value) : null}
onChange={handleChange}
format={dateFormat}
onBlur={onBlur || handleBlur}
showToday={false}
disabledTime
disabledDate={(d) => {
if (onlyToday) {
return !dayjs().isSame(d, "day");
} else if (onlyFuture) {
return dayjs().subtract(1, "day").isAfter(d);
}
}}
{...restProps}
/>
</div>
);
}

View File

@@ -1,48 +0,0 @@
import { DatePicker } from "antd";
import dayjs from "../../utils/day.js";
import React, { useRef } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(FormDateTimePickerEnhanced);
const dateFormat = "MM/DD/YYYY h:mm a";
export function FormDateTimePickerEnhanced({
bodyshop,
value,
onBlur,
onlyFuture,
onlyToday,
isDateOnly = true,
...restProps
}) {
const ref = useRef();
return (
<div>
<DatePicker
ref={ref}
value={value ? dayjs(value) : null}
format={dateFormat}
onBlur={onBlur}
showToday={false}
disabledDate={(d) => {
if (onlyToday) {
return !dayjs().isSame(d, "day");
} else if (onlyFuture) {
return dayjs().subtract(1, "day").isAfter(d);
}
}}
{...restProps}
/>
</div>
);
}

View File

@@ -1,46 +1,105 @@
import React, { forwardRef } from "react";
//import DatePicker from "react-datepicker";
//import "react-datepicker/src/stylesheets/datepicker.scss";
import { Space, TimePicker } from "antd";
import { DatePicker } from "antd";
import PropTypes from "prop-types";
import React, { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import dayjs from "../../utils/day";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
//To be used as a form element only.
import { fuzzyMatchDate } from "./formats.js";
const DateTimePicker = ({ value, onChange, onBlur, id, onlyFuture, ...restProps }, ref) => {
// const handleChange = (newDate) => {
// if (value !== newDate && onChange) {
// onChange(newDate);
// }
// };
const DateTimePicker = ({ value, onChange, onBlur, id, onlyFuture, onlyToday, isDateOnly = false, ...restProps }) => {
const [isManualInput, setIsManualInput] = useState(false);
const { t } = useTranslation();
const handleChange = useCallback(
(newDate) => {
if (onChange) {
onChange(newDate || null);
}
setIsManualInput(false);
},
[onChange]
);
const handleBlur = useCallback(
(e) => {
// Bail if this is not a manual input
if (!isManualInput) {
return;
}
// Reset manual input flag
setIsManualInput(false);
const v = e?.target?.value;
if (!v) return;
let parsedDate = isDateOnly ? fuzzyMatchDate(v)?.startOf("day") : fuzzyMatchDate(v);
if (parsedDate && onChange) {
onChange(parsedDate);
}
},
[isManualInput, isDateOnly, onChange]
);
const handleKeyDown = useCallback(
(e) => {
setIsManualInput(true);
if (e.key.toLowerCase() === "t" && onChange) {
e.preventDefault();
setIsManualInput(false);
onChange(dayjs());
} else if (e.key.toLowerCase() === "enter") {
handleBlur(e);
}
},
[onChange, handleBlur]
);
const handleDisabledDate = useCallback(
(current) => {
if (onlyToday) {
return !dayjs().isSame(current, "day");
} else if (onlyFuture) {
return dayjs().subtract(1, "day").isAfter(current);
}
return false;
},
[onlyToday, onlyFuture]
);
return (
<Space direction="vertical" style={{ width: "100%" }} id={id}>
<FormDatePicker
{...restProps}
{...(onlyFuture && {
disabledDate: (d) => dayjs().subtract(1, "day").isAfter(d)
})}
value={value}
onBlur={onBlur}
onChange={onChange}
onlyFuture={onlyFuture}
isDateOnly={false}
/>
<TimePicker
<div onKeyDown={handleKeyDown} id={id} style={{ width: "100%" }}>
<DatePicker
showTime={
isDateOnly
? false
: {
format: "hh:mm a",
minuteStep: 15,
defaultValue: dayjs(dayjs(), "HH:mm:ss")
}
}
format={isDateOnly ? "MM/DD/YYYY" : "MM/DD/YYYY hh:mm a"}
value={value ? dayjs(value) : null}
{...(onlyFuture && {
disabledDate: (d) => dayjs().isAfter(d)
})}
onChange={onChange}
disableSeconds={true}
minuteStep={15}
onBlur={onBlur}
format="hh:mm a"
onChange={handleChange}
placeholder={isDateOnly ? t("general.labels.date") : t("general.labels.datetime")}
onBlur={onBlur || handleBlur}
disabledDate={handleDisabledDate}
{...restProps}
/>
</Space>
</div>
);
};
export default forwardRef(DateTimePicker);
DateTimePicker.propTypes = {
value: PropTypes.any,
onChange: PropTypes.func,
onBlur: PropTypes.func,
id: PropTypes.string,
onlyFuture: PropTypes.bool,
onlyToday: PropTypes.bool,
isDateOnly: PropTypes.bool
};
export default React.memo(DateTimePicker);

View File

@@ -0,0 +1,63 @@
import dayjs from "../../utils/day";
const dateFormats = [
"MMDDYYYY",
"MMDDYY",
"M/D/YYYY",
"MM/D/YYYY",
"M/DD/YYYY",
"MM/DD/YYYY",
"M/D/YY",
"MM/D/YY",
"M/DD/YY",
"MM/DD/YY"
];
const timeFormats = ["h:mm A", "h:mmA", "h A", "hA", "hh:mm A", "hh:mm:ss A"];
const dateTimeFormats = [
...["M/D/YYYY", "MM/D/YYYY", "M/DD/YYYY", "MM/DD/YYYY", "M/D/YY", "MM/D/YY", "M/DD/YY", "MM/DD/YY"].flatMap(
(dateFormat) => timeFormats.map((timeFormat) => `${dateFormat} ${timeFormat}`)
),
...["MMDDYYYY", "MMDDYY"].flatMap((dateFormat) => timeFormats.map((timeFormat) => `${dateFormat} ${timeFormat}`)),
"M/D/YYYY",
"MM/D/YYYY",
"M/DD/YYYY",
"MM/DD/YYYY",
"M/D/YY",
"MM/D/YY",
"M/DD/YY",
"MM/DD/YY",
"MMDDYYYY",
"MMDDYY"
];
const sanitizeInput = (input) =>
input
.trim()
.toUpperCase()
.replace(/\s*(am|pm)\s*/i, " $1")
.replaceAll(".", "/")
.replaceAll("-", "/");
export const fuzzyMatchDate = (dateString) => {
const sanitizedInput = sanitizeInput(dateString);
for (const format of dateFormats) {
const parsedDate = dayjs(sanitizedInput, format, true);
if (parsedDate.isValid()) {
return parsedDate;
}
}
for (const format of dateTimeFormats) {
const parsedDateTime = dayjs(sanitizedInput, format, true);
if (parsedDateTime.isValid()) {
return parsedDateTime; // Return the dayjs object
}
}
return null; // If no matching format is found
};

View File

@@ -13,7 +13,6 @@ import Icon, {
FileFilled,
HomeFilled,
ImportOutlined,
InfoCircleOutlined,
LineChartOutlined,
PaperClipOutlined,
PhoneOutlined,
@@ -27,8 +26,8 @@ import Icon, {
UserOutlined
} from "@ant-design/icons";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Layout, Menu, Switch, Tooltip } from "antd";
import React, { useEffect, useState } from "react";
import { Layout, Menu } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { BsKanban } from "react-icons/bs";
import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar, FaTasks } from "react-icons/fa";
@@ -43,7 +42,6 @@ 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 { checkBeta, handleBeta, setBeta } from "../../utils/handleBeta";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
@@ -115,20 +113,22 @@ function Header({
names: ["ImEXPay", "DmsAp", "Simple_Inventory"],
splitKey: bodyshop && bodyshop.imexshopid
});
const [betaSwitch, setBetaSwitch] = useState(false);
const { t } = useTranslation();
useEffect(() => {
const isBeta = checkBeta();
setBetaSwitch(isBeta);
}, []);
const betaSwitchChange = (checked) => {
setBeta(checked);
setBetaSwitch(checked);
handleBeta();
const deleteBetaCookie = () => {
const cookieExists = document.cookie.split("; ").some((row) => row.startsWith(`betaSwitchImex=`));
if (cookieExists) {
const domain = window.location.hostname.split(".").slice(-2).join(".");
document.cookie = `betaSwitchImex=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${domain}`;
console.log(`betaSwitchImex cookie deleted`);
} else {
console.log(`betaSwitchImex cookie does not exist`);
}
};
deleteBetaCookie();
const accountingChildren = [];
if (
@@ -695,31 +695,6 @@ function Header({
}
];
InstanceRenderManager({
executeFunction: true,
args: [],
imex: () => {
menuItems.push({
key: "beta-switch",
id: "header-beta-switch",
style: { marginLeft: "auto" },
label: (
<Tooltip
title={`A more modern ${InstanceRenderManager({
imex: t("titles.imexonline"),
rome: t("titles.romeonline"),
promanager: t("titles.promanager")
})} is ready for you to try! You can switch back at any time.`}
>
<InfoCircleOutlined />
<span style={{ marginRight: 8 }}>Try the new app</span>
<Switch checked={betaSwitch} onChange={betaSwitchChange} />
</Tooltip>
)
});
}
});
return (
<Layout.Header>
<Menu

View File

@@ -1,4 +1,4 @@
import React from "react";
import React, { useCallback, useMemo } from "react";
import { useMutation } from "@apollo/client";
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
import { useTranslation } from "react-i18next";
@@ -11,55 +11,61 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function ScheduleEventColor({ bodyshop, event }) {
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
const { t } = useTranslation();
const onClick = async ({ key }) => {
const result = await updateAppointment({
variables: {
appid: event.id,
app: { color: key === "null" ? null : key }
}
});
if (!!!result.errors) {
notification["success"]({ message: t("appointments.successes.saved") });
} else {
notification["error"]({
message: t("appointments.errors.saving", {
error: JSON.stringify(result.errors)
})
const onClick = useCallback(
async ({ key }) => {
const result = await updateAppointment({
variables: {
appid: event.id,
app: { color: key === "null" ? null : key }
}
});
if (!result.errors) {
notification.success({ message: t("appointments.successes.saved") });
} else {
notification.error({
message: t("appointments.errors.saving", {
error: JSON.stringify(result.errors)
})
});
}
},
[event.id, t, updateAppointment]
);
const selectedColor = useMemo(() => {
if (event.color && bodyshop.appt_colors) {
const colorObj = bodyshop.appt_colors.find((color) => color.color.hex === event.color);
return colorObj?.label;
}
};
return null;
}, [event.color, bodyshop.appt_colors]);
const selectedColor =
event.color &&
bodyshop.appt_colors &&
bodyshop.appt_colors.filter((color) => color.color.hex === event.color)[0]?.label;
const menu = {
defaultSelectedKeys: [event.color],
onClick: onClick,
items: [
...(bodyshop.appt_colors || []).map((color) => ({
key: color.color.hex,
label: color.label,
style: { color: color.color.hex }
})),
{ type: "divider" },
{ key: "null", label: t("general.actions.clear") }
]
};
const menu = useMemo(
() => ({
defaultSelectedKeys: [event.color],
onClick: onClick,
items: [
...(bodyshop.appt_colors || []).map((color) => ({
key: color.color.hex,
label: color.label,
style: { color: color.color.hex }
})),
{ type: "divider" },
{ key: "null", label: t("general.actions.clear") }
]
}),
[bodyshop.appt_colors, event.color, onClick, t]
);
return (
<Dropdown menu={menu}>
<a href=" #" onClick={(e) => e.preventDefault()}>
<a href="#" onClick={(e) => e.preventDefault()}>
{selectedColor}
<DownOutlined />
</a>
@@ -67,4 +73,4 @@ export function ScheduleEventColor({ bodyshop, event }) {
);
}
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleEventColor);
export default connect(mapStateToProps)(React.memo(ScheduleEventColor));

View File

@@ -2,11 +2,10 @@ import { AlertFilled } from "@ant-design/icons";
import { Button, Divider, Dropdown, Form, Input, notification, Popover, Select, Space } from "antd";
import parsePhoneNumber from "libphonenumber-js";
import dayjs from "../../utils/day";
import queryString from "query-string";
import React, { useState } from "react";
import React, { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { Link, useNavigate, useSearchParams } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
import { setModalContext } from "../../redux/modals/modals.actions";
@@ -27,6 +26,7 @@ import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
setScheduleContext: (context) => dispatch(setModalContext({ context: context, modal: "schedule" })),
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
@@ -44,301 +44,319 @@ export function ScheduleEventComponent({
}) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const history = useNavigate();
const searchParams = queryString.parse(useLocation().search);
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
const [title, setTitle] = useState(event.title);
const blockContent = (
<Space direction="vertical" wrap>
<Input
value={title}
onChange={(e) => setTitle(e.currentTarget.value)}
onBlur={async () => {
await updateAppointment({
variables: {
appid: event.id,
app: {
title: title
}
},
optimisticResponse: {
update_appointments: {
__typename: "appointments_mutation_response",
returning: [
{
...event,
title: title,
__typename: "appointments"
}
]
}
}
});
}}
/>
<Button onClick={() => handleCancel({ id: event.id })} disabled={event.arrived}>
{t("appointments.actions.unblock")}
</Button>
</Space>
const handleTitleBlur = useCallback(async () => {
await updateAppointment({
variables: {
appid: event.id,
app: {
title: title
}
},
optimisticResponse: {
update_appointments: {
__typename: "appointments_mutation_response",
returning: [
{
...event,
title: title,
__typename: "appointments"
}
]
}
}
});
}, [updateAppointment, event, title]);
const handleUnblock = useCallback(() => {
handleCancel({ id: event.id });
}, [handleCancel, event.id]);
const handlePreviewClick = useCallback(() => {
const params = new URLSearchParams(searchParams);
params.set("selected", event.job?.id);
navigate({ search: `?${params.toString()}` });
}, [navigate, searchParams, event.job?.id]);
const handleSendEmailReminder = useCallback(() => {
const Template = TemplateList("job").appointment_reminder;
GenerateDocument(
{
name: Template.key,
variables: { id: event.job.id }
},
{
to: event.job && event.job.ownr_ea,
subject: Template.subject
},
"e",
event.job && event.job.id
);
}, [event.job]);
const handleSendSMSReminder = useCallback(() => {
const p = parsePhoneNumber(event.job.ownr_ph1, "CA");
if (p && p.isValid()) {
openChatByPhone({
phone_num: p.formatInternational(),
jobid: event.job.id
});
setMessage(
t("appointments.labels.reminder", {
shopname: bodyshop.shopname,
date: dayjs(event.start).format("MM/DD/YYYY"),
time: dayjs(event.start).format("HH:mm a")
})
);
setOpen(false);
} else {
notification.error({
message: t("messaging.error.invalidphone")
});
}
}, [event.job, openChatByPhone, setMessage, t, bodyshop.shopname, event.start, setOpen]);
const reminderMenuItems = useMemo(
() => [
{
key: "email",
label: t("general.labels.email"),
disabled: event.arrived,
onClick: handleSendEmailReminder
},
{
key: "sms",
label: t("general.labels.sms"),
disabled: event.arrived || !bodyshop.messagingservicesid,
onClick: handleSendSMSReminder
}
],
[t, event.arrived, handleSendEmailReminder, handleSendSMSReminder, bodyshop.messagingservicesid]
);
const popoverContent = (
<div style={{ maxWidth: "40vw" }}>
{!event.isintake ? (
<Space>
<strong>{event.title}</strong>
<ScheduleEventColor event={event} />
</Space>
) : (
<Space>
<strong>
<OwnerNameDisplay ownerObject={event.job} />
</strong>
<span style={{ margin: 4 }}>
{`${(event.job && event.job.v_model_yr) || ""} ${
(event.job && event.job.v_make_desc) || ""
} ${(event.job && event.job.v_model_desc) || ""}`}
</span>
<ScheduleEventColor event={event} />
</Space>
)}
const reminderMenu = useMemo(() => ({ items: reminderMenuItems }), [reminderMenuItems]);
{event.job ? (
<div>
<DataLabel label={t("jobs.fields.ro_number")}>{(event.job && event.job.ro_number) || ""}</DataLabel>
<DataLabel label={t("jobs.fields.clm_total")}>
<CurrencyFormatter>{(event.job && event.job.clm_total) || ""}</CurrencyFormatter>
</DataLabel>
<DataLabel hideIfNull label={t("jobs.fields.ins_co_nm")}>
{(event.job && event.job.ins_co_nm) || ""}
</DataLabel>
<DataLabel hideIfNull label={t("jobs.fields.clm_no")}>
{(event.job && event.job.clm_no) || ""}
</DataLabel>
<DataLabel label={t("jobs.fields.ownr_ea")}>{(event.job && event.job.ownr_ea) || ""}</DataLabel>
<DataLabel label={t("jobs.fields.ownr_ph1")}>
<ChatOpenButton phone={event.job && event.job.ownr_ph1} jobid={event.job.id} />
</DataLabel>
<DataLabel label={t("jobs.fields.ownr_ph2")}>
<ChatOpenButton phone={event.job && event.job.ownr_ph2} jobid={event.job.id} />
</DataLabel>
<DataLabel label={t("jobs.fields.alt_transport")}>
{(event.job && event.job.alt_transport) || ""}
<ScheduleAtChange job={event && event.job} />
</DataLabel>
<ScheduleEventNote event={event} />
</div>
) : (
<div>{event.note || ""}</div>
)}
<Divider />
<Space wrap>
{event.job ? (
<Link to={`/manage/jobs/${event.job && event.job.id}`}>
<Button>{t("appointments.actions.viewjob")}</Button>
</Link>
) : null}
{event.job ? (
<Button
onClick={() => {
history({
search: queryString.stringify({
...searchParams,
selected: event.job.id
})
});
}}
>
{t("appointments.actions.preview")}
</Button>
) : null}
{event.job ? (
<Dropdown
menu={{
items: [
{
key: "email",
label: t("general.labels.email"),
disabled: event.arrived,
onClick: () => {
const Template = TemplateList("job").appointment_reminder;
GenerateDocument(
{
name: Template.key,
variables: { id: event.job.id }
},
{
to: event.job && event.job.ownr_ea,
subject: Template.subject
},
"e",
event.job && event.job.id
);
}
},
{
key: "sms",
label: t("general.labels.sms"),
disabled: event.arrived || !bodyshop.messagingservicesid,
onClick: () => {
const p = parsePhoneNumber(event.job.ownr_ph1, "CA");
if (p && p.isValid()) {
openChatByPhone({
phone_num: p.formatInternational(),
jobid: event.job.id
});
setMessage(
t("appointments.labels.reminder", {
shopname: bodyshop.shopname,
date: dayjs(event.start).format("MM/DD/YYYY"),
time: dayjs(event.start).format("HH:mm a")
})
);
setOpen(false);
} else {
notification["error"]({
message: t("messaging.error.invalidphone")
});
}
}
}
]
}}
>
<Button>{t("appointments.actions.sendreminder")}</Button>
</Dropdown>
) : null}
{event.arrived ? (
<Button
// onClick={() => handleCancel(event.id)}
disabled={event.arrived}
>
{t("appointments.actions.cancel")}
</Button>
) : (
<Popover
trigger="click"
disabled={event.arrived}
content={
<Form
layout="vertical"
onFinish={({ lost_sale_reason }) => {
handleCancel({ id: event.id, lost_sale_reason });
}}
>
<Form.Item
name="lost_sale_reason"
label={t("jobs.fields.lost_sale_reason")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Select
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
label: lsr,
value: lsr
}))}
/>
</Form.Item>
<Button htmlType="submit">{t("appointments.actions.cancel")}</Button>
</Form>
}
>
<Button
// onClick={() => handleCancel(event.id)}
disabled={event.arrived}
>
{t("appointments.actions.cancel")}
</Button>
</Popover>
)}
const handleCancelFormFinish = useCallback(
({ lost_sale_reason }) => {
handleCancel({ id: event.id, lost_sale_reason });
},
[handleCancel, event.id]
);
{event.isintake ? (
<Button
disabled={event.arrived}
onClick={() => {
setOpen(false);
setScheduleContext({
actions: { refetch: refetch },
context: {
jobId: event.job.id,
job: event.job,
previousEvent: event.id,
color: event.color,
alt_transport: event.job && event.job.alt_transport,
note: event.note
}
});
}}
>
{t("appointments.actions.reschedule")}
</Button>
) : (
<ScheduleManualEvent event={event} />
)}
{event.isintake ? (
<Link
to={{
pathname: `/manage/jobs/${event.job && event.job.id}/intake`,
search: `?appointmentId=${event.id}`
}}
>
<Button disabled={event.arrived}>{t("appointments.actions.intake")}</Button>
</Link>
) : null}
const handleRescheduleClick = useCallback(() => {
setOpen(false);
setScheduleContext({
actions: { refetch: refetch },
context: {
jobId: event.job.id,
job: event.job,
previousEvent: event.id,
color: event.color,
alt_transport: event.job && event.job.alt_transport,
note: event.note
}
});
}, [setOpen, setScheduleContext, refetch, event]);
const handleOpenChange = useCallback(
(vis) => {
if (!event.vacation) setOpen(vis);
},
[event.vacation]
);
const blockContent = useMemo(
() => (
<Space direction="vertical" wrap>
<Input value={title} onChange={(e) => setTitle(e.currentTarget.value)} onBlur={handleTitleBlur} />
<Button onClick={handleUnblock} disabled={event.arrived}>
{t("appointments.actions.unblock")}
</Button>
</Space>
</div>
),
[title, handleTitleBlur, handleUnblock, event.arrived, t]
);
const RegularEvent = event.isintake ? (
<Space
wrap
size="small"
style={{
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
}}
>
{event.note && <AlertFilled className="production-alert" />}
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
const popoverContent = useMemo(() => {
console.log("hit");
return (
<div style={{ maxWidth: "40vw" }}>
{!event.isintake ? (
<Space>
<strong>{event.title}</strong>
<ScheduleEventColor event={event} />
</Space>
) : (
<Space>
<strong>
<OwnerNameDisplay ownerObject={event.job} />
</strong>
<span style={{ margin: 4 }}>
{`${(event.job && event.job.v_model_yr) || ""} ${
(event.job && event.job.v_make_desc) || ""
} ${(event.job && event.job.v_model_desc) || ""}`}
</span>
<ScheduleEventColor event={event} />
</Space>
)}
<OwnerNameDisplay ownerObject={event.job} />
{event.job ? (
<div>
<DataLabel label={t("jobs.fields.ro_number")}>{(event.job && event.job.ro_number) || ""}</DataLabel>
<DataLabel label={t("jobs.fields.clm_total")}>
<CurrencyFormatter>{(event.job && event.job.clm_total) || ""}</CurrencyFormatter>
</DataLabel>
<DataLabel hideIfNull label={t("jobs.fields.ins_co_nm")}>
{(event.job && event.job.ins_co_nm) || ""}
</DataLabel>
<DataLabel hideIfNull label={t("jobs.fields.clm_no")}>
{(event.job && event.job.clm_no) || ""}
</DataLabel>
<DataLabel label={t("jobs.fields.ownr_ea")}>{(event.job && event.job.ownr_ea) || ""}</DataLabel>
<DataLabel label={t("jobs.fields.ownr_ph1")}>
<ChatOpenButton phone={event.job && event.job.ownr_ph1} jobid={event.job.id} />
</DataLabel>
<DataLabel label={t("jobs.fields.ownr_ph2")}>
<ChatOpenButton phone={event.job && event.job.ownr_ph2} jobid={event.job.id} />
</DataLabel>
<DataLabel label={t("jobs.fields.alt_transport")}>
{(event.job && event.job.alt_transport) || ""}
<ScheduleAtChange job={event && event.job} />
</DataLabel>
<ScheduleEventNote event={event} />
</div>
) : (
<div>{event.note || ""}</div>
)}
<Divider />
<Space wrap>
{event.job ? (
<Link to={`/manage/jobs/${event.job && event.job.id}`}>
<Button>{t("appointments.actions.viewjob")}</Button>
</Link>
) : null}
{event.job ? <Button onClick={handlePreviewClick}>{t("appointments.actions.preview")}</Button> : null}
{event.job ? (
<Dropdown menu={reminderMenu}>
<Button>{t("appointments.actions.sendreminder")}</Button>
</Dropdown>
) : null}
{event.arrived ? (
<Button disabled={event.arrived}>{t("appointments.actions.cancel")}</Button>
) : (
<Popover
trigger="click"
disabled={event.arrived}
content={
<Form layout="vertical" onFinish={handleCancelFormFinish}>
<Form.Item
name="lost_sale_reason"
label={t("jobs.fields.lost_sale_reason")}
rules={[
{
required: true
}
]}
>
<Select
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
label: lsr,
value: lsr
}))}
/>
</Form.Item>
<Button htmlType="submit">{t("appointments.actions.cancel")}</Button>
</Form>
}
>
<Button disabled={event.arrived}>{t("appointments.actions.cancel")}</Button>
</Popover>
)}
{`${(event.job && event.job.v_model_yr) || ""} ${
(event.job && event.job.v_make_desc) || ""
} ${(event.job && event.job.v_model_desc) || ""}`}
{event.isintake ? (
<Button disabled={event.arrived} onClick={handleRescheduleClick}>
{t("appointments.actions.reschedule")}
</Button>
) : (
<ScheduleManualEvent event={event} />
)}
{event.isintake ? (
<Link
to={{
pathname: `/manage/jobs/${event.job && event.job.id}/intake`,
search: `?appointmentId=${event.id}`
}}
>
<Button disabled={event.arrived}>{t("appointments.actions.intake")}</Button>
</Link>
) : null}
</Space>
</div>
);
}, [
event,
t,
handlePreviewClick,
reminderMenu,
bodyshop.md_lost_sale_reasons,
handleCancelFormFinish,
handleRescheduleClick
]);
{`(${(event.job && event.job.labhrs.aggregate.sum.mod_lb_hrs) || "0"} / ${
(event.job && event.job.larhrs.aggregate.sum.mod_lb_hrs) || "0"
})`}
const RegularEvent = useMemo(
() =>
event.isintake ? (
<Space
wrap
size="small"
style={{
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
}}
>
{event.note && <AlertFilled className="production-alert" />}
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
{event.job && event.job.alt_transport && <div style={{ margin: ".1rem" }}>{event.job.alt_transport}</div>}
</Space>
) : (
<div
style={{
height: "100%",
width: "100%",
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
}}
>
<strong>{`${event.title || ""}`}</strong>
</div>
<OwnerNameDisplay ownerObject={event.job} />
{`${(event.job && event.job.v_model_yr) || ""} ${
(event.job && event.job.v_make_desc) || ""
} ${(event.job && event.job.v_model_desc) || ""}`}
{`(${(event.job && event.job.labhrs.aggregate.sum.mod_lb_hrs) || "0"} / ${
(event.job && event.job.larhrs.aggregate.sum.mod_lb_hrs) || "0"
})`}
{event.job && event.job.alt_transport && <div style={{ margin: ".1rem" }}>{event.job.alt_transport}</div>}
</Space>
) : (
<div
style={{
height: "100%",
width: "100%",
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
}}
>
<strong>{`${event.title || ""}`}</strong>
</div>
),
[event, t]
);
return (
<Popover
open={open}
onOpenChange={(vis) => !event.vacation && setOpen(vis)}
onOpenChange={handleOpenChange}
trigger="click"
content={event.block ? blockContent : popoverContent}
style={{
height: "100%",
width: "100%",
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
}}
>
@@ -347,4 +365,4 @@ export function ScheduleEventComponent({
);
}
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleEventComponent);
export default connect(mapStateToProps, mapDispatchToProps)(React.memo(ScheduleEventComponent));

View File

@@ -1,6 +1,6 @@
import { useMutation } from "@apollo/client";
import { notification } from "antd";
import React from "react";
import React, { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { useDispatch } from "react-redux";
import { logImEXEvent } from "../../firebase/firebase.utils";
@@ -10,64 +10,70 @@ import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import ScheduleEventComponent from "./schedule-event.component";
export default function ScheduleEventContainer({ bodyshop, event, refetch }) {
function ScheduleEventContainer({ bodyshop, event, refetch }) {
const dispatch = useDispatch();
const { t } = useTranslation();
const [cancelAppointment] = useMutation(CANCEL_APPOINTMENT_BY_ID);
const [updateJob] = useMutation(UPDATE_JOB);
const handleCancel = async ({ id, lost_sale_reason }) => {
logImEXEvent("schedule_cancel_appt");
const cancelAppt = await cancelAppointment({
variables: { appid: event.id }
});
notification["success"]({
message: t("appointments.successes.canceled")
});
const handleCancel = useCallback(
async ({ id, lost_sale_reason }) => {
logImEXEvent("schedule_cancel_appt");
if (!!cancelAppt.errors) {
notification["error"]({
message: t("appointments.errors.canceling", {
message: JSON.stringify(cancelAppt.errors)
})
const cancelAppt = await cancelAppointment({
variables: { appid: event.id }
});
return;
}
if (event.job) {
const jobUpdate = await updateJob({
variables: {
jobId: event.job.id,
job: {
date_scheduled: null,
scheduled_in: null,
scheduled_completion: null,
lost_sale_reason,
date_lost_sale: new Date(),
status: bodyshop.md_ro_statuses.default_imported
}
}
});
if (!jobUpdate.errors) {
dispatch(
insertAuditTrail({
jobid: event.job.id,
operation: AuditTrailMapping.appointmentcancel(lost_sale_reason),
type: "appointmentcancel"
})
);
}
if (!!jobUpdate.errors) {
notification["error"]({
message: t("jobs.errors.updating", {
message: JSON.stringify(jobUpdate.errors)
if (!cancelAppt.errors) {
notification.success({
message: t("appointments.successes.canceled")
});
} else {
notification.error({
message: t("appointments.errors.canceling", {
message: JSON.stringify(cancelAppt.errors)
})
});
return;
}
}
if (refetch) refetch();
};
if (event.job) {
const jobUpdate = await updateJob({
variables: {
jobId: event.job.id,
job: {
date_scheduled: null,
scheduled_in: null,
scheduled_completion: null,
lost_sale_reason,
date_lost_sale: new Date(),
status: bodyshop.md_ro_statuses.default_imported
}
}
});
if (!jobUpdate.errors) {
dispatch(
insertAuditTrail({
jobid: event.job.id,
operation: AuditTrailMapping.appointmentcancel(lost_sale_reason),
type: "appointmentcancel"
})
);
} else {
notification.error({
message: t("jobs.errors.updating", {
message: JSON.stringify(jobUpdate.errors)
})
});
return;
}
}
if (refetch) refetch();
},
[cancelAppointment, event.id, event.job, updateJob, bodyshop.md_ro_statuses.default_imported, dispatch, t, refetch]
);
return <ScheduleEventComponent event={event} refetch={refetch} handleCancel={handleCancel} />;
}
export default React.memo(ScheduleEventContainer);

View File

@@ -1,7 +1,7 @@
import { EditFilled, SaveFilled } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, Input, notification, Space } from "antd";
import React, { useState } from "react";
import React, { useState, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -12,9 +12,6 @@ import DataLabel from "../data-label/data-label.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function ScheduleEventNote({ event }) {
const [editing, setEditing] = useState(false);
@@ -23,9 +20,9 @@ export function ScheduleEventNote({ event }) {
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
const { t } = useTranslation();
const toggleEdit = async () => {
const toggleEdit = useCallback(async () => {
if (editing) {
//Await the update
// Await the update
setLoading(true);
const result = await updateAppointment({
variables: {
@@ -34,10 +31,10 @@ export function ScheduleEventNote({ event }) {
}
});
if (!!!result.errors) {
// notification["success"]({ message: t("appointments.successes.saved") });
if (!result.errors) {
// notification.success({ message: t("appointments.successes.saved") });
} else {
notification["error"]({
notification.error({
message: t("jobs.errors.saving", {
error: JSON.stringify(result.errors)
})
@@ -45,11 +42,15 @@ export function ScheduleEventNote({ event }) {
}
setEditing(false);
setLoading(false);
} else {
setEditing(true);
}
setLoading(false);
};
}, [editing, note, updateAppointment, event.id, t]);
const handleNoteChange = useCallback((e) => {
setNote(e.target.value);
}, []);
return (
<DataLabel label={t("appointments.fields.note")}>
@@ -57,7 +58,7 @@ export function ScheduleEventNote({ event }) {
{!editing ? (
event.note || ""
) : (
<Input.TextArea rows={3} value={note} onChange={(e) => setNote(e.target.value)} style={{ maxWidth: "8vw" }} />
<Input.TextArea rows={3} value={note} onChange={handleNoteChange} style={{ maxWidth: "8vw" }} />
)}
<Button onClick={toggleEdit} loading={loading}>
{editing ? <SaveFilled /> : <EditFilled />}
@@ -67,4 +68,4 @@ export function ScheduleEventNote({ event }) {
);
}
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleEventNote);
export default connect(mapStateToProps)(React.memo(ScheduleEventNote));

View File

@@ -10,8 +10,8 @@ import {
QUERY_SCOREBOARD_ENTRY,
UPDATE_SCOREBOARD_ENTRY
} from "../../graphql/scoreboard.queries";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
export default function ScoreboardAddButton({ job, disabled, ...otherBtnProps }) {
const { t } = useTranslation();
@@ -86,7 +86,7 @@ export default function ScoreboardAddButton({ job, disabled, ...otherBtnProps })
}
]}
>
<FormDatePicker />
<DateTimePicker isDateOnly />
</Form.Item>
<Form.Item
label={t("scoreboard.fields.bodyhrs")}

View File

@@ -141,10 +141,14 @@ export function JobTotalsTableTotals({ bodyshop, job }) {
key: t("jobs.fields.ded_amt"),
total: job.job_totals.totals.custPayable.deductible
},
// {
// key: t("jobs.fields.federal_tax_payable"),
// total: job.job_totals.totals.custPayable.federal_tax,
// },
...(InstanceRenderManager({
imex: [{
key: t("jobs.fields.federal_tax_payable"),
total: job.job_totals.totals.custPayable.federal_tax
}],
rome: [],
promanager: "USE_ROME"
})),
{
key: t("jobs.fields.other_amount_payable"),
total: job.job_totals.totals.custPayable.other_customer_amount

View File

@@ -5,7 +5,6 @@ import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
@@ -20,7 +19,14 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(
insertAuditTrail({
jobid,
operation,
type
})
)
});
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminDatesChange);
@@ -87,7 +93,7 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
<FormFieldsChanged form={form} />
<LayoutFormRow header={t("jobs.forms.estdates")}>
<Form.Item label={t("jobs.fields.date_estimated")} name="date_estimated">
<FormDatePicker format="MM/DD/YYYY" />
<DateTimePicker format="MM/DD/YYYY" isDateOnly />
</Form.Item>
<Form.Item label={t("jobs.fields.date_towin")} name="date_towin">
<DateTimePicker />

View File

@@ -1,18 +1,9 @@
import {
Collapse,
Form,
Input,
InputNumber,
Select,
Space,
Switch,
} from "antd";
import { Collapse, Form, Input, InputNumber, Select, Space, Switch } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
@@ -29,6 +20,7 @@ import JobsDetailRatesTaxes from "../jobs-detail-rates/jobs-detail-rates.taxes.c
import JobsMarkPstExempt from "../jobs-mark-pst-exempt/jobs-mark-pst-exempt.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -61,10 +53,7 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
<Form.Item label={t("jobs.fields.policy_no")} name="policy_no">
<Input />
</Form.Item>
<Form.Item
label={t("jobs.fields.regie_number")}
name="regie_number"
>
<Form.Item label={t("jobs.fields.regie_number")} name="regie_number">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm">
@@ -116,7 +105,7 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
<FormItemEmail email={getFieldValue("ins_ea")} />
</Form.Item>
<Form.Item label={t("jobs.fields.loss_date")} name="loss_date">
<FormDatePicker />
<DateTimePicker isDateOnly />
</Form.Item>
<Form.Item label={t("jobs.fields.kmin")} name="kmin">
<Input />

View File

@@ -2,9 +2,9 @@ import { Form, Input } from "antd";
import React, { useContext } from "react";
import { useTranslation } from "react-i18next";
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import JobsCreateVehicleInfoPredefined from "./jobs-create-vehicle-info.predefined.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
export default function JobsCreateVehicleInfoNewComponent({ form }) {
const [state] = useContext(JobCreateContext);
@@ -113,7 +113,7 @@ export default function JobsCreateVehicleInfoNewComponent({ form }) {
<Input disabled={!state.vehicle.new} />
</Form.Item>
<Form.Item label={t("vehicles.fields.v_prod_dt")} name={["vehicle", "data", "v_prod_dt"]}>
<FormDatePicker disabled={!state.vehicle.new} />
<DateTimePicker isDateOnly disabled={!state.vehicle.new} />
</Form.Item>
</LayoutFormRow>
<LayoutFormRow grow>

View File

@@ -5,7 +5,6 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import FormRow from "../layout-form-row/layout-form-row.component";
@@ -30,7 +29,7 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
<div>
<FormRow header={t("jobs.forms.estdates")}>
<Form.Item label={t("jobs.fields.date_estimated")} name="date_estimated">
<FormDatePicker disabled={jobRO} />
<DateTimePicker disabled={jobRO} isDateOnly />
</Form.Item>
<Form.Item label={t("jobs.fields.date_open")} name="date_open">
<DateTimePicker disabled={jobRO} />
@@ -45,7 +44,7 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
<FormRow header={t("jobs.forms.scheddates")}>
<Form.Item label={t("jobs.fields.date_scheduled")} name="date_scheduled">
<FormDatePicker disabled={jobRO} />
<DateTimePicker disabled={jobRO} isDateOnly />
</Form.Item>
<Tooltip title={t("jobs.labels.scheduledinchange")}>
<Form.Item label={t("jobs.fields.scheduled_in")} name="scheduled_in">
@@ -85,7 +84,6 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
rules={[
{
required: jobInPostProduction
//message: t("general.validation.required"),
}
]}
>

View File

@@ -5,7 +5,6 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
@@ -13,6 +12,7 @@ import Car from "../job-damage-visual/job-damage-visual.component";
import JobsDetailChangeEstimator from "../jobs-detail-change-estimator/jobs-detail-change-estimator.component";
import JobsDetailChangeFileHandler from "../jobs-detail-change-filehandler/jobs-detail-change-filehandler.component";
import FormRow from "../layout-form-row/layout-form-row.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
@@ -152,7 +152,7 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
<Input disabled={jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.loss_date")} name="loss_date">
<FormDatePicker disabled={jobRO} />
<DateTimePicker isDateOnly disabled={jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.loss_of_use")} name="loss_of_use">
<Input disabled={jobRO} />

View File

@@ -23,7 +23,7 @@ export function PartnerPingComponent({ bodyshop }) {
// Execute the created function directly
checkPartnerStatus(bodyshop);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [bodyshop]);
}, [bodyshop?.id]);
return <></>;
}

View File

@@ -8,8 +8,8 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import { MUTATION_UPDATE_BO_ETA } from "../../graphql/parts-orders.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateFormatter } from "../../utils/DateFormatter";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import { CalendarFilled } from "@ant-design/icons";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -62,7 +62,7 @@ export function PartsOrderBackorderEta({
<div>
<Form form={form} onFinish={handleFinish}>
<Form.Item name="eta">
<FormDatePicker />
<DateTimePicker isDateOnly />
</Form.Item>
<Button type="primary" onClick={() => form.submit()}>
{t("general.actions.save")}

View File

@@ -7,7 +7,7 @@ import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { MUTATION_BACKORDER_PART_LINE } from "../../graphql/parts-orders.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -71,7 +71,7 @@ export function PartsOrderLineBackorderButton({ partsOrderStatus, partsLineId, j
<div>
<Form form={form} onFinish={handleFinish}>
<Form.Item name="eta">
<FormDatePicker />
<DateTimePicker isDateOnly />
</Form.Item>
<Button type="primary" onClick={() => form.submit()}>
{t("parts_orders.actions.backordered")}

View File

@@ -1,8 +1,7 @@
import { DeleteFilled, EyeFilled } from "@ant-design/icons";
import { DeleteFilled } 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";
@@ -83,47 +82,34 @@ export function PartsOrderListTableDrawerComponent({
sortedInfo: {}
});
const [returnfrombill, setReturnFromBill] = useState();
const [billData, setBillData] = useState();
const [billData, setBillData] = useState(null);
const search = queryString.parse(useLocation().search);
const selectedpartsorder = search.partsorderid;
const [billQuery] = useLazyQuery(QUERY_BILL_BY_PK);
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
const { refetch } = billsQuery;
const [billQuery] = useLazyQuery(QUERY_BILL_BY_PK);
const selectedPartsOrderRecord = parts_orders.find((r) => r.id === selectedpartsorder);
useEffect(() => {
if (returnfrombill === null) {
setBillData(null);
} else {
const fetchData = async () => {
const result = await billQuery({
variables: { billid: returnfrombill }
});
setBillData(result.data);
};
fetchData();
}
}, [returnfrombill, billQuery]);
const fetchData = async () => {
if (selectedPartsOrderRecord?.returnfrombill) {
try {
const { data } = await billQuery({
variables: { billid: selectedPartsOrderRecord.returnfrombill }
});
setBillData(data);
} catch (error) {
console.error("Error fetching bill data:", error);
}
} else setBillData(null);
};
fetchData();
}, [selectedPartsOrderRecord, billQuery]);
const recordActions = (record, showView = false) => (
const recordActions = (record) => (
<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={() => {
@@ -133,16 +119,14 @@ export function PartsOrderListTableDrawerComponent({
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
};
})
partsorderlines: record.parts_order_lines.map((pol) => ({
joblineid: pol.job_line_id,
id: pol.id,
line_desc: pol.line_desc,
quantity: pol.quantity,
act_price: pol.act_price,
oem_partno: pol.oem_partno
}))
}
});
}}
@@ -167,7 +151,6 @@ export function PartsOrderListTableDrawerComponent({
disabled={jobRO}
onConfirm={async () => {
//Delete the parts return.!
await deletePartsOrder({
variables: { partsOrderId: record.id },
update(cache) {
@@ -191,7 +174,6 @@ export function PartsOrderListTableDrawerComponent({
disabled={(jobRO ? !record.return : jobRO) || record.vendor.id === bodyshop.inhousevendorid}
onClick={() => {
logImEXEvent("parts_order_receive_bill");
setBillEnterContext({
actions: { refetch: refetch },
context: {
@@ -199,24 +181,20 @@ export function PartsOrderListTableDrawerComponent({
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
};
})
billlines: record.parts_order_lines.map((pol) => ({
joblineid: pol.job_line_id || "noline",
line_desc: pol.line_desc,
quantity: pol.quantity,
actual_price: pol.act_price,
cost_center: pol.jobline?.part_type
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
? pol.jobline.part_type !== "PAE"
? pol.jobline.part_type
: null
: responsibilityCenters.defaults &&
(responsibilityCenters.defaults.costs[pol.jobline.part_type] || null)
: null
}))
}
}
});
@@ -243,8 +221,6 @@ export function PartsOrderListTableDrawerComponent({
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const selectedPartsOrderRecord = parts_orders.find((r) => r.id === selectedpartsorder);
const rowExpander = (record) => {
const columns = [
{

View File

@@ -27,6 +27,10 @@ export default function PartsOrderModalPriceChange({ form, field }) {
key: "25",
label: t("parts_orders.labels.discount", { percent: "25%" })
},
{
key: "40",
label: t("parts_orders.labels.discount", { percent: "40%" })
},
{
key: "custom",
label: (

View File

@@ -6,12 +6,12 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import PartsOrderModalPriceChange from "./parts-order-modal-price-change.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -74,7 +74,7 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState,
]}
label={t("parts_orders.fields.deliver_by")}
>
<FormDatePicker onlyFuture />
<DateTimePicker isDateOnly onlyFuture />
</Form.Item>
{job && job.special_coverage_policy && (
<Tag color="tomato">

View File

@@ -5,11 +5,11 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import DatePickerFormItem from "../form-date-picker/form-date-picker.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import JobSearchSelect from "../job-search-select/job-search-select.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import PaymentFormTotalPayments from "./payment-form.totalpayments.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -77,7 +77,7 @@ export function PaymentFormComponent({ form, bodyshop, disabled }) {
}
]}
>
<DatePickerFormItem disabled={disabled} />
<DateTimePicker isDateOnly disabled={disabled} />
</Form.Item>
</LayoutFormRow>

View File

@@ -6,11 +6,11 @@ import {
PauseCircleOutlined
} from "@ant-design/icons";
import { Card, Col, Row, Space, Tooltip } from "antd";
import Dinero from "dinero.js";
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";
@@ -18,8 +18,8 @@ import ProductionSubletsManageComponent from "../production-sublets-manage/produ
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";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
const cardColor = (ssbuckets, totalHrs) => {
const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > totalHrs));
@@ -213,21 +213,13 @@ const EstimatorToolTip = ({ metadata, cardSettings }) => {
};
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;
const dineroAmount = Dinero(metadata?.job_totals?.totals?.subtotal ?? Dinero()).toFormat();
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 title={`${dineroAmount}`} kiosk={cardSettings.kiosk}>
{dineroAmount}
</EllipsesToolTip>
</Col>
)

View File

@@ -3,6 +3,7 @@ import { Card, Statistic } from "antd";
import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";
import { defaultKanbanSettings, statisticsItems } from "./settings/defaultKanbanSettings.js";
import Dinero from "dinero.js";
export const StatisticType = {
HOURS: "hours",
@@ -32,7 +33,21 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
};
const calculateTotalAmount = (items, key) => {
return items.reduce((acc, item) => acc + (item[key]?.totals?.subtotal?.amount || 0), 0);
return items.reduce((acc, item) => acc.add(Dinero(item[key]?.totals?.subtotal ?? Dinero())), Dinero({ amount: 0 }));
};
const calculateReducerTotalAmount = (lanes, key) => {
return lanes.reduce(
(acc, lane) => {
return acc.add(
lane.cards.reduce(
(laneAcc, card) => laneAcc.add(Dinero(card.metadata[key]?.totals?.subtotal ?? Dinero())),
Dinero({ amount: 0 })
)
);
},
Dinero({ amount: 0 })
);
};
const calculateReducerTotal = (lanes, key, subKey) => {
@@ -43,14 +58,6 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
}, 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);
@@ -87,9 +94,15 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
const totalAmountInProduction = useMemo(() => {
if (!cardSettings.totalAmountInProduction) return null;
const total = calculateTotalAmount(data, "job_totals");
return parseFloat(total.toFixed(2));
return total.toFormat("$0,0.00");
}, [data, cardSettings.totalAmountInProduction]);
const totalAmountOnBoard = useMemo(() => {
if (!reducerData || !cardSettings.totalAmountOnBoard) return null;
const total = calculateReducerTotalAmount(reducerData.lanes, "job_totals");
return total.toFormat("$0,0.00");
}, [reducerData, cardSettings.totalAmountOnBoard]);
const totalHrsOnBoard = useMemo(() => {
if (!reducerData || !cardSettings.totalHrsOnBoard) return null;
const total =
@@ -118,12 +131,6 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
[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);
@@ -191,7 +198,6 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
<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")

View File

@@ -28,6 +28,11 @@ import ProductionListColumnCategory from "./production-list-columns.status.categ
import ProductionListColumnStatus from "./production-list-columns.status.component";
import ProductionListColumnTouchTime from "./prodution-list-columns.touchtime.component";
const getEmployeeName = (employeeId, employees) => {
const employee = employees.find((e) => e.id === employeeId);
return employee ? `${employee.first_name} ${employee.last_name}` : "";
};
const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatments }) => {
const { Enhanced_Payroll } = treatments;
return [
@@ -426,8 +431,8 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
sortOrder: state.sortedInfo.columnKey === "employee_body" && state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
bodyshop.employees?.find((e) => e.id === a.employee_body)?.first_name,
bodyshop.employees?.find((e) => e.id === b.employee_body)?.first_name
getEmployeeName(a.employee_body, bodyshop.employees),
getEmployeeName(b.employee_body, bodyshop.employees)
),
render: (text, record) => (
<ProductionListEmployeeAssignment refetch={refetch} record={record} type="employee_body" />
@@ -440,8 +445,8 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
sortOrder: state.sortedInfo.columnKey === "employee_prep" && state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
bodyshop.employees?.find((e) => e.id === a.employee_prep)?.first_name,
bodyshop.employees?.find((e) => e.id === b.employee_prep)?.first_name
getEmployeeName(a.employee_prep, bodyshop.employees),
getEmployeeName(b.employee_prep, bodyshop.employees)
),
render: (text, record) => (
<ProductionListEmployeeAssignment record={record} refetch={refetch} type="employee_prep" />
@@ -460,8 +465,8 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
sortOrder: state.sortedInfo.columnKey === "employee_csr" && state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
bodyshop.employees?.find((e) => e.id === a.employee_csr)?.first_name,
bodyshop.employees?.find((e) => e.id === b.employee_csr)?.first_name
getEmployeeName(a.employee_csr, bodyshop.employees),
getEmployeeName(b.employee_csr, bodyshop.employees)
),
render: (text, record) => (
<ProductionListEmployeeAssignment refetch={refetch} record={record} type="employee_csr" />
@@ -474,8 +479,8 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
sortOrder: state.sortedInfo.columnKey === "employee_refinish" && state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
bodyshop.employees?.find((e) => e.id === a.employee_refinish)?.first_name,
bodyshop.employees?.find((e) => e.id === b.employee_refinish)?.first_name
getEmployeeName(a.employee_refinish, bodyshop.employees),
getEmployeeName(b.employee_refinish, bodyshop.employees)
),
render: (text, record) => (
<ProductionListEmployeeAssignment record={record} refetch={refetch} type="employee_refinish" />

View File

@@ -1,12 +1,12 @@
import { useMutation } from "@apollo/client";
import { Button, Card, Dropdown, Space, TimePicker } from "antd";
import { Button, Card, Dropdown, Space } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { DateFormatter } from "../../utils/DateFormatter";
import dayjs from "../../utils/day";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
export default function ProductionListDate({ record, field, time, pastIndicator }) {
const [updateAlert] = useMutation(UPDATE_JOB);
@@ -57,22 +57,14 @@ export default function ProductionListDate({ record, field, time, pastIndicator
label: (
<Card style={{ padding: "1rem" }} onClick={(e) => e.stopPropagation()}>
<Space direction={"vertical"}>
<FormDatePicker
<DateTimePicker
onClick={(e) => e.stopPropagation()}
value={(record[field] && dayjs(record[field])) || null}
onChange={handleChange}
format="MM/DD/YYYY"
format={time ? "MM/DD/YYYY hh:mm a" : "MM/DD/YYYY"}
isDateOnly={!time}
showTime={time ? { format: "hh:mm a", minuteStep: 15 } : false}
/>
{time && (
<TimePicker
onClick={(e) => e.stopPropagation()}
value={(record[field] && dayjs(record[field])) || null}
onChange={handleChange}
minuteStep={15}
format="hh:mm a"
/>
)}
<Button onClick={() => setOpen(false)}>{t("general.actions.close")}</Button>
</Space>
</Card>

View File

@@ -1,93 +0,0 @@
import { useMutation } from "@apollo/client";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { Button, Form, Input, notification, Popover, Space } from "antd";
import { useTranslation } from "react-i18next";
import { UPDATE_SHOP } from "../../graphql/bodyshop.queries";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { isFunction } from "lodash";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function ProductionListSaveConfigButton({ columns, bodyshop, tableState, onSave }) {
const [updateShop] = useMutation(UPDATE_SHOP);
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const [form] = Form.useForm();
const { t } = useTranslation();
const handleSaveConfig = async (values) => {
logImEXEvent("production_save_config");
setLoading(true);
const result = await updateShop({
variables: {
id: bodyshop.id,
shop: {
production_config: [
...bodyshop.production_config.filter((b) => b.name !== values.name),
//Assign it to the name
{
name: values.name,
columns: {
columnKeys: columns.map((i) => {
return { key: i.key, width: i.width };
}),
tableState
}
}
]
}
}
});
if (!!!result.errors) {
notification["success"]({ message: t("bodyshop.successes.save") });
if (onSave && isFunction(onSave)) {
onSave();
}
} else {
notification["error"]({
message: t("bodyshop.errors.saving", {
error: JSON.stringify(result.errors)
})
});
}
form.resetFields();
setOpen(false);
setLoading(false);
};
const popMenu = (
<div>
<Form layout="vertical" form={form} onFinish={handleSaveConfig}>
<Form.Item label={t("production.labels.viewname")} name="name" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Space wrap>
<Button type="primary" danger onClick={() => form.submit()} loading={loading}>
{t("general.actions.save")}
</Button>
<Button onClick={() => setOpen(false)}>{t("general.actions.close")}</Button>
</Space>
</Form>
</div>
);
return (
<Popover open={open} content={popMenu}>
<Button loading={loading} onClick={() => setOpen(true)}>
{t("production.actions.saveconfig")}
</Button>
</Popover>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(ProductionListSaveConfigButton);

View File

@@ -0,0 +1,505 @@
import { DeleteOutlined, ExclamationCircleOutlined, PlusOutlined } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, Form, Input, Modal, notification, Popconfirm, Popover, Select, Space } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_ACTIVE_PROD_LIST_VIEW } from "../../graphql/associations.queries";
import { UPDATE_SHOP } from "../../graphql/bodyshop.queries";
import ProductionListColumns from "../production-list-columns/production-list-columns.data";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { isFunction } from "lodash";
const { confirm } = Modal;
export function ProductionListConfigManager({
refetch,
bodyshop,
technician,
currentUser,
state,
data,
columns,
setColumns,
setState,
onSave,
hasUnsavedChanges,
setHasUnsavedChanges
}) {
const { t } = useTranslation();
const [updateDefaultProdView] = useMutation(UPDATE_ACTIVE_PROD_LIST_VIEW);
const [updateShop] = useMutation(UPDATE_SHOP);
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const [isAddingNewProfile, setIsAddingNewProfile] = useState(false);
const [form] = Form.useForm();
const [activeView, setActiveView] = useState(() => {
const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email);
return assoc && assoc.default_prod_list_view;
});
const defaultState = {
sortedInfo: {
columnKey: "ro_number",
order: null
},
filteredInfo: {}
};
const ensureDefaultState = (state) => {
return {
sortedInfo: state?.sortedInfo || defaultState.sortedInfo,
filteredInfo: state?.filteredInfo || defaultState.filteredInfo,
...state
};
};
const createDefaultView = async () => {
const defaultConfig = {
name: t("production.constants.main_profile"),
columns: {
columnKeys: [
{ key: "ro_number", width: 100 },
{ key: "ownr", width: 100 },
{ key: "vehicle", width: 100 },
{ key: "ins_co_nm", width: 100 },
{ key: "actual_in", width: 100 },
{ key: "scheduled_completion", width: 100 },
{ key: "labhrs", width: 100 },
{ key: "employee_body", width: 100 },
{ key: "larhrs", width: 100 },
{ key: "employee_refinish", width: 100 },
{ key: "tt", width: 100 },
{ key: "status", width: 100 },
{ key: "sublets", width: 100 },
{ key: "viewdetail", width: 100 }
],
tableState: ensureDefaultState(state)
}
};
const result = await updateShop({
variables: {
id: bodyshop.id,
shop: {
production_config: [defaultConfig]
}
}
});
if (!result.errors) {
await updateActiveProdView(t("production.constants.main_profile"));
window.location.reload(); // Reload the page
} else {
notification.error({
message: t("bodyshop.errors.creatingdefaultview", {
error: JSON.stringify(result.errors)
})
});
}
};
const {
treatments: { Enhanced_Payroll }
} = useSplitTreatments({
attributes: {},
names: ["Enhanced_Payroll"],
splitKey: bodyshop.imexshopid
});
const updateActiveProdView = async (viewName) => {
const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email);
if (assoc) {
await updateDefaultProdView({
variables: { assocId: assoc.id, view: viewName },
update(cache) {
cache.modify({
id: cache.identify(bodyshop),
fields: {
associations(existingAssociations) {
return existingAssociations.map((a) => {
if (a.useremail !== currentUser.email) return a;
return { ...a, default_prod_list_view: viewName };
});
}
}
});
}
});
setActiveView(viewName);
setHasUnsavedChanges(false);
}
};
const handleSelect = async (value) => {
if (hasUnsavedChanges) {
confirm({
title: t("general.labels.unsavedchanges"),
icon: <ExclamationCircleOutlined />,
content: t("general.messages.unsavedchangespopup"),
onOk: () => proceedWithSelect(value),
onCancel() {
// Do nothing if canceled
}
});
} else {
await proceedWithSelect(value);
}
};
const proceedWithSelect = async (value) => {
if (value === "add_new") {
setIsAddingNewProfile(true);
setOpen(true);
return;
}
const selectedConfig = bodyshop.production_config.find((pc) => pc.name === value);
// If the selected profile doesn't exist, revert to the main profile
if (!selectedConfig) {
const mainProfileConfig = bodyshop.production_config.find(
(pc) => pc.name === t("production.constants.main_profile")
);
if (mainProfileConfig) {
await updateActiveProdView(t("production.constants.main_profile"));
setColumns(
mainProfileConfig.columns.columnKeys.map((k) => {
return {
...ProductionListColumns({
bodyshop,
refetch,
technician,
state: ensureDefaultState(state),
data: data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
treatments: { Enhanced_Payroll }
}).find((e) => e.key === k.key),
width: k.width
};
})
);
const newState = ensureDefaultState(mainProfileConfig.columns.tableState);
setState(newState);
if (onSave && isFunction(onSave)) {
onSave();
}
return;
}
}
// If the selected profile exists, proceed as normal
if (selectedConfig) {
const newColumns = selectedConfig.columns.columnKeys.map((k) => {
return {
...ProductionListColumns({
bodyshop,
refetch,
technician,
state: ensureDefaultState(state),
data: data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
treatments: { Enhanced_Payroll }
}).find((e) => e.key === k.key),
width: k.width
};
});
setColumns(newColumns);
const newState = ensureDefaultState(selectedConfig.columns.tableState);
setState(newState);
await updateActiveProdView(value);
if (onSave && isFunction(onSave)) {
onSave();
}
}
};
const handleTrash = async (name) => {
if (name === t("production.constants.main_profile")) return;
const remainingConfigs = bodyshop.production_config.filter((b) => b.name !== name);
await updateShop({
variables: {
id: bodyshop.id,
shop: {
production_config: remainingConfigs
}
},
awaitRefetchQueries: true
});
if (name === activeView) {
// Only switch profiles if the deleted profile was the active profile
if (remainingConfigs.length > 0) {
const nextConfig = remainingConfigs[0];
await updateActiveProdView(nextConfig.name);
setColumns(
nextConfig.columns.columnKeys.map((k) => {
return {
...ProductionListColumns({
technician,
state: ensureDefaultState(state),
refetch,
data: data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
treatments: { Enhanced_Payroll }
}).find((e) => e.key === k.key),
width: k.width
};
})
);
setState(ensureDefaultState(nextConfig.columns.tableState));
} else {
await updateActiveProdView(null);
setColumns([]);
setState(defaultState);
}
} else {
// Revert back to the active view and load its columns and state
const activeConfig = bodyshop.production_config.find((pc) => pc.name === activeView);
if (activeConfig) {
await updateActiveProdView(activeView);
setColumns(
activeConfig.columns.columnKeys.map((k) => {
return {
...ProductionListColumns({
technician,
state: ensureDefaultState(state),
refetch,
data: data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
treatments: { Enhanced_Payroll }
}).find((e) => e.key === k.key),
width: k.width
};
})
);
setState(ensureDefaultState(activeConfig.columns.tableState));
}
}
};
const handleSaveConfig = async (values) => {
logImEXEvent("production_save_config");
setLoading(true);
const profileName = isAddingNewProfile ? values.name : activeView;
const result = await updateShop({
variables: {
id: bodyshop.id,
shop: {
production_config: [
...bodyshop.production_config.filter((b) => b.name !== profileName),
{
name: profileName,
columns: {
columnKeys: columns.map((i) => ({ key: i.key, width: i.width })),
tableState: ensureDefaultState(state)
}
}
]
}
}
});
if (!result.errors) {
notification.success({ message: t("bodyshop.successes.save") });
if (isAddingNewProfile) {
await updateActiveProdView(profileName);
}
if (onSave && isFunction(onSave)) {
onSave();
}
setHasUnsavedChanges(false);
} else {
notification.error({
message: t("bodyshop.errors.saving", {
error: JSON.stringify(result.errors)
})
});
}
form.resetFields();
setOpen(false);
setLoading(false);
setIsAddingNewProfile(false);
};
useEffect(() => {
const validateAndSetDefaultView = () => {
const configExists = bodyshop.production_config.some((pc) => pc.name === activeView);
if (!configExists) {
// If the default view doesn't exist, revert to the main profile
const mainProfileConfig = bodyshop.production_config.find(
(pc) => pc.name === t("production.constants.main_profile")
);
if (mainProfileConfig) {
setActiveView(t("production.constants.main_profile"));
setColumns(
mainProfileConfig.columns.columnKeys.map((k) => {
return {
...ProductionListColumns({
bodyshop,
refetch,
technician,
state: ensureDefaultState(state),
data: data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
treatments: { Enhanced_Payroll }
}).find((e) => e.key === k.key),
width: k.width
};
})
);
setState(ensureDefaultState(mainProfileConfig.columns.tableState));
updateActiveProdView(t("production.constants.main_profile"));
}
} else {
// If the default view exists, set it as active
setActiveView(activeView);
}
};
if (!bodyshop.production_config || bodyshop.production_config.length === 0) {
createDefaultView().catch((e) => {
console.error("Something went wrong saving the production list view Config.");
});
} else {
validateAndSetDefaultView();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeView, bodyshop.production_config]);
const popMenu = (
<div>
<Form layout="vertical" form={form} onFinish={handleSaveConfig}>
{isAddingNewProfile && (
<Form.Item
label={t("production.labels.viewname")}
name="name"
rules={[
{ required: true, message: t("production.errors.name_required") },
{
validator: (_, value) => {
if (!value) {
return Promise.resolve();
}
const nameExists = bodyshop.production_config.some((pc) => pc.name === value);
if (nameExists) {
return Promise.reject(new Error(t("production.errors.name_exists")));
}
return Promise.resolve();
}
}
]}
>
<Input />
</Form.Item>
)}
<Space wrap>
<Button
type="primary"
danger
onClick={() => form.submit()}
loading={loading}
disabled={form.getFieldsError().some(({ errors }) => errors.length)}
>
{t("general.actions.save")}
</Button>
{!isAddingNewProfile && (
<Button
type="default"
onClick={() => {
setIsAddingNewProfile(true);
setOpen(true);
}}
>
{t("general.actions.saveas")}
</Button>
)}
<Button
onClick={() => {
setIsAddingNewProfile(false);
setOpen(false);
}}
>
{t("general.actions.cancel")}
</Button>
</Space>
</Form>
</div>
);
return (
<Space>
<Button loading={loading} onClick={() => setOpen(true)} disabled={isAddingNewProfile || !hasUnsavedChanges}>
{t("production.actions.saveconfig")}
</Button>
<Popover open={open} content={popMenu} placement="bottom">
<Select
style={{
minWidth: "150px"
}}
onSelect={handleSelect}
placeholder={t("production.labels.selectview")}
optionLabelProp="label"
popupMatchSelectWidth={false}
value={activeView}
disabled={open || isAddingNewProfile} // Disable the Select box when the popover is open or adding a new profile
>
{bodyshop.production_config
.slice()
.sort((a, b) =>
a.name === t("production.constants.main_profile")
? -1
: b.name === t("production.constants.main_profile")
? 1
: 0
) //
.map((config) => (
<Select.Option key={config.name} label={config.name}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span
style={{
flex: 1,
maxWidth: "80%",
marginRight: "1rem",
textOverflow: "ellipsis"
}}
>
{config.name}
</span>
{config.name !== t("production.constants.main_profile") && (
<Popconfirm
placement="right"
title={t("general.labels.areyousure")}
onConfirm={() => handleTrash(config.name)}
onCancel={(e) => e.stopPropagation()}
>
<DeleteOutlined onClick={(e) => e.stopPropagation()} />
</Popconfirm>
)}
</div>
</Select.Option>
))}
<Select.Option key="add_new" label={t("production.labels.addnewprofile")}>
<div style={{ display: "flex", alignItems: "center" }}>
<PlusOutlined style={{ marginRight: "0.5rem" }} />
{t("production.labels.addnewprofile")}
</div>
</Select.Option>
</Select>
</Popover>
</Space>
);
}

View File

@@ -1,172 +0,0 @@
import { DeleteOutlined } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Popconfirm, Select } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UPDATE_ACTIVE_PROD_LIST_VIEW } from "../../graphql/associations.queries";
import { UPDATE_SHOP } from "../../graphql/bodyshop.queries";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import ProductionListColumns from "../production-list-columns/production-list-columns.data";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { isFunction } from "lodash";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
technician: selectTechnician,
currentUser: selectCurrentUser
});
export function ProductionListTable({
refetch,
bodyshop,
technician,
currentUser,
state,
data,
setColumns,
setState,
onProfileChange
}) {
const { t } = useTranslation();
const [updateDefaultProdView] = useMutation(UPDATE_ACTIVE_PROD_LIST_VIEW);
const [updateShop] = useMutation(UPDATE_SHOP);
const {
treatments: { Enhanced_Payroll }
} = useSplitTreatments({
attributes: {},
names: ["Enhanced_Payroll"],
splitKey: bodyshop.imexshopid
});
const handleSelect = async (value, option) => {
const newColumns = bodyshop.production_config
.filter((pc) => pc.name === value)[0]
.columns.columnKeys.map((k) => {
return {
...ProductionListColumns({
bodyshop,
refetch,
technician,
state,
data: data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
treatments: { Enhanced_Payroll }
}).find((e) => e.key === k.key),
width: k.width
};
});
setColumns(newColumns);
const newState = bodyshop.production_config.filter((pc) => pc.name === value)[0].columns.tableState;
setState(newState);
const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email);
if (assoc) {
await updateDefaultProdView({
variables: { assocId: assoc.id, view: value },
update(cache) {
cache.modify({
id: cache.identify(bodyshop),
fields: {
associations(existingAssociations, { readField }) {
return existingAssociations.map((a) => {
if (a.useremail !== currentUser.email) return a;
return { ...a, default_prod_list_view: value };
});
}
}
});
}
});
}
if (onProfileChange && isFunction(onProfileChange)) {
onProfileChange({ value, option, newColumns, newState, assoc });
}
};
const handleTrash = async (name) => {
await updateShop({
variables: {
id: bodyshop.id,
shop: {
production_config: bodyshop.production_config.filter((b) => b.name !== name)
}
},
awaitRefetchQueries: true
});
setColumns(
bodyshop.production_config[0].columns.columnKeys.map((k) => {
return {
...ProductionListColumns({
technician,
state,
refetch,
data: data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
treatments: { Enhanced_Payroll }
}).find((e) => e.key === k.key),
width: k.width
};
})
);
setState(bodyshop.production_config[0].columns.tableState);
};
const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email);
const defaultView = assoc && assoc.default_prod_list_view;
return (
<div style={{ width: "10rem" }}>
<Select
onSelect={handleSelect}
placeholder={t("production.labels.selectview")}
optionLabelProp="label"
popupMatchSelectWidth={false}
defaultValue={defaultView}
>
{bodyshop.production_config.map((config) => (
<Select.Option key={config.name} label={config.name}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center"
}}
>
<span
style={{
flex: 1,
maxWidth: "80%",
marginRight: "1rem",
textOverflow: "ellipsis"
}}
>
{config.name}
</span>
<Popconfirm
placement="right"
title={t("general.labels.areyousure")}
onConfirm={() => handleTrash(config.name)}
>
<DeleteOutlined
onClick={(e) => {
e.stopPropagation();
}}
/>
</Popconfirm>
</div>
</Select.Option>
))}
</Select>
</div>
);
}
export default connect(mapStateToProps, null)(ProductionListTable);

View File

@@ -1,24 +1,23 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Button, Dropdown, Input, Space, Statistic, Table } from "antd";
import { SyncOutlined } from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-layout";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Button, Dropdown, Input, Space, Statistic, Table } from "antd";
import _ from "lodash";
import React, { useEffect, useMemo, useRef, useState } from "react";
import ReactDragListView from "react-drag-listview";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import Prompt from "../../utils/prompt.js";
import AlertComponent from "../alert/alert.component.jsx";
import ProductionListColumnsAdd from "../production-list-columns/production-list-columns.add.component";
import ProductionListColumns from "../production-list-columns/production-list-columns.data";
import ProductionListDetail from "../production-list-detail/production-list-detail.component";
import ProductionListSaveConfigButton from "../production-list-save-config-button/production-list-save-config-button.component";
import { ProductionListConfigManager } from "./production-list-config-manager.component.jsx";
import ProductionListPrint from "./production-list-print.component";
import ProductionListTableViewSelect from "./production-list-table-view-select.component";
import ResizeableTitle from "./production-list-table.resizeable.component";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { SyncOutlined } from "@ant-design/icons";
import Prompt from "../../utils/prompt.js";
import _ from "lodash";
import AlertComponent from "../alert/alert.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -44,7 +43,7 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
const initialStateRef = useRef(
(bodyshop.production_config &&
bodyshop.production_config.find((p) => p.name === defaultView)?.columns.tableState) ||
bodyshop.production_config[0]?.columns.tableState || {
(bodyshop.production_config && bodyshop.production_config[0]?.columns.tableState) || {
sortedInfo: {},
filteredInfo: { text: "" }
}
@@ -270,23 +269,23 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
data={data}
onColumnAdd={addColumn}
/>
<ProductionListSaveConfigButton
<ProductionListConfigManager
columns={columns}
tableState={state}
onSave={() => {
setHasUnsavedChanges(false);
}}
/>
<ProductionListTableViewSelect
setColumns={setColumns}
state={state}
setState={setState}
setColumns={setColumns}
onProfileChange={() => {
initialStateRef.current = state;
setHasUnsavedChanges(false);
}}
refetch={refetch}
data={data}
bodyshop={bodyshop}
technician={technician}
currentUser={currentUser}
setHasUnsavedChanges={setHasUnsavedChanges}
hasUnsavedChanges={hasUnsavedChanges}
onSave={() => {
setHasUnsavedChanges(false);
initialStateRef.current = state;
}}
/>
<Input
onChange={(e) => setSearchText(e.target.value)}

View File

@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
import { getOrderOperatorsByType, getWhereOperatorsByType } from "../../utils/graphQLmodifier";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import { generateInternalReflections } from "./report-center-modal-utils";
import { FormDatePicker } from "../form-date-picker/form-date-picker.component.jsx";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
export default function ReportCenterModalFiltersSortersComponent({ form, bodyshop }) {
return (
@@ -196,7 +196,8 @@ function FiltersSection({ filters, form, bodyshop }) {
// We have a type of date, so we will use a date picker
if (type === "date") {
return (
<FormDatePicker
<DateTimePicker
isDateOnly
disabled={!operator}
onChange={(date) => form.setFieldValue(fieldPath, date)}
/>

View File

@@ -34,28 +34,34 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
const [form] = Form.useForm();
const [search, setSearch] = useState("");
const {
treatments: { Enhanced_Payroll }
treatments: { Enhanced_Payroll, ADPPayroll }
} = useSplitTreatments({
attributes: {},
names: ["Enhanced_Payroll"],
names: ["Enhanced_Payroll", "ADPPayroll"],
splitKey: bodyshop.imexshopid
});
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
const Templates = TemplateList("report_center");
const ReportsList =
Enhanced_Payroll.treatment === "on"
? Object.keys(Templates)
.map((key) => {
return Templates[key];
})
.filter((temp) => temp.enhanced_payroll === undefined || temp.enhanced_payroll === true)
: Object.keys(Templates)
.map((key) => {
return Templates[key];
})
.filter((temp) => temp.enhanced_payroll === undefined || temp.enhanced_payroll === false);
const ReportsList = Object.keys(Templates)
.map((key) => Templates[key])
.filter((temp) => {
const enhancedPayrollOn = Enhanced_Payroll.treatment === "on";
const adpPayrollOn = ADPPayroll.treatment === "on";
if (enhancedPayrollOn && adpPayrollOn) {
return temp.enhanced_payroll !== false || temp.adp_payroll !== false;
}
if (enhancedPayrollOn) {
return temp.enhanced_payroll !== false && temp.adp_payroll !== true;
}
if (adpPayrollOn) {
return temp.adp_payroll !== false && temp.enhanced_payroll !== true;
}
return temp.enhanced_payroll !== true && temp.adp_payroll !== true;
});
const { open } = reportCenterModal;
@@ -104,7 +110,7 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
to: values.to,
subject: Templates[values.key]?.subject
},
values.sendbyexcel === "excel" ? "x" : values.sendby === "email" ? "e" : "p",
values.sendbytext === "text" ? values.sendbytext : values.sendbyexcel === "excel" ? "x" : values.sendby === "email" ? "e" : "p",
id
);
setLoading(false);
@@ -291,7 +297,15 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
</Radio.Group>
</Form.Item>
);
if (reporttype !== "excel")
if (reporttype === "text")
return (
<Form.Item label={t("general.labels.sendby")} name="sendbytext" initialValue="text">
<Radio.Group>
<Radio value="text">{t("general.labels.text")}</Radio>
</Radio.Group>
</Form.Item>
);
if (reporttype !== "excel" || reporttype !== "text")
return (
<Form.Item label={t("general.labels.sendby")} name="sendby" initialValue="print">
<Radio.Group>

View File

@@ -1,50 +1,36 @@
import { Space } from "antd";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectScheduleLoad } from "../../redux/application/application.selectors";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
scheduleLoad: selectScheduleLoad
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function ScheduleAtsSummary({ scheduleLoad, appointments }) {
const ScheduleAtsSummary = React.memo(function ScheduleAtsSummary({ appointments }) {
const { t } = useTranslation();
const atsSummary = useMemo(() => {
let atsSummary = {};
if (!appointments || appointments.length === 0) {
return {};
}
const summary = {};
appointments
.filter((a) => a.isintake)
.filter((a) => a.isintake && a.job?.alt_transport)
.forEach((a) => {
if (!a.job.alt_transport) return;
if (!atsSummary[a.job.alt_transport]) {
atsSummary[a.job.alt_transport] = 1;
} else {
atsSummary[a.job.alt_transport] = atsSummary[a.job.alt_transport] + 1;
}
const key = a.job.alt_transport;
summary[key] = (summary[key] || 0) + 1;
});
return atsSummary;
return summary;
}, [appointments]);
if (Object.keys(atsSummary).length > 0)
if (Object.keys(atsSummary).length > 0) {
return (
<Space wrap>
{t("schedule.labels.atssummary")}
{Object.keys(atsSummary).map((key) => (
<span key={key}>{`${key}: ${atsSummary[key]}`}</span>
{Object.entries(atsSummary).map(([key, value]) => (
<span key={key}>{`${key}: ${value}`}</span>
))}
</Space>
);
}
return null;
}
});
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleAtsSummary);
export default ScheduleAtsSummary;

View File

@@ -1,7 +1,7 @@
import { useMutation } from "@apollo/client";
import { Dropdown, notification } from "antd";
import dayjs from "../../utils/day";
import React from "react";
import React, { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -13,57 +13,61 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({});
export function ScheduleBlockDay({ date, children, refetch, bodyshop, alreadyBlocked }) {
const ScheduleBlockDay = React.memo(function ScheduleBlockDay({ date, children, refetch, bodyshop, alreadyBlocked }) {
const { t } = useTranslation();
const [insertBlock] = useMutation(INSERT_APPOINTMENT_BLOCK);
const handleMenu = async (e) => {
e.domEvent.stopPropagation();
const handleMenu = useCallback(
async (e) => {
e.domEvent.stopPropagation();
if (e.key === "block") {
const blockAppt = {
title: t("appointments.labels.blocked"),
block: true,
isintake: false,
bodyshopid: bodyshop.id,
start: dayjs(date).startOf("day"),
end: dayjs(date).endOf("day")
};
logImEXEvent("dashboard_change_layout");
if (e.key === "block") {
const blockAppt = {
title: t("appointments.labels.blocked"),
block: true,
isintake: false,
bodyshopid: bodyshop.id,
start: dayjs(date).startOf("day"),
end: dayjs(date).endOf("day")
};
logImEXEvent("dashboard_change_layout");
const result = await insertBlock({
variables: { app: [blockAppt] }
});
if (!!result.errors) {
notification["error"]({
message: t("appointments.errors.blocking", {
message: JSON.stringify(result.errors)
})
const result = await insertBlock({
variables: { app: [blockAppt] }
});
}
if (!!refetch) refetch();
}
};
if (result.errors) {
notification.error({
message: t("appointments.errors.blocking", {
message: JSON.stringify(result.errors)
})
});
}
const menu = {
items: [
{
key: "block",
label: t("appointments.actions.block")
if (refetch) refetch();
}
],
onClick: handleMenu
};
},
[t, bodyshop.id, date, insertBlock, refetch]
);
const menu = useMemo(
() => ({
items: [
{
key: "block",
label: t("appointments.actions.block")
}
],
onClick: handleMenu
}),
[t, handleMenu]
);
return (
<Dropdown menu={menu} disabled={alreadyBlocked} trigger={["contextMenu"]}>
{children}
</Dropdown>
);
}
});
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleBlockDay);
export default connect(mapStateToProps)(ScheduleBlockDay);

View File

@@ -10,55 +10,48 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function ScheduleCalendarHeaderGraph({ bodyshop, loadData }) {
const mapDispatchToProps = () => ({});
const ScheduleCalendarHeaderGraph = React.memo(function ScheduleCalendarHeaderGraph({ bodyshop, loadData }) {
const { ssbuckets } = bodyshop;
const { t } = useTranslation();
const data = useMemo(() => {
return (
(loadData &&
loadData.expectedLoad &&
Object.keys(loadData.expectedLoad).map((key) => {
const metadataBucket = ssbuckets.filter((b) => b.id === key)[0];
return {
bucket: loadData.expectedLoad[key].label,
current: loadData.expectedLoad[key].count,
target: metadataBucket && metadataBucket.target
};
})) ||
[]
);
const data = useMemo(() => {
if (!loadData || !loadData.expectedLoad || !ssbuckets) return [];
return Object.keys(loadData.expectedLoad).map((key) => {
const metadataBucket = ssbuckets.find((b) => b.id === key);
return {
bucket: loadData.expectedLoad[key].label,
current: loadData.expectedLoad[key].count,
target: metadataBucket?.target || 0
};
});
}, [loadData, ssbuckets]);
const popContent = (
<div>
<Space>
{t("appointments.labels.expectedprodhrs")}
<strong>{loadData?.expectedHours?.toFixed(1)}</strong>
{t("appointments.labels.expectedjobs")}
<strong>{loadData?.expectedJobCount}</strong>
</Space>
<RadarChart
// cx={300}
// cy={250}
// outerRadius={150}
width={800}
height={600}
data={data}
>
<PolarGrid />
<PolarAngleAxis dataKey="bucket" />
<PolarRadiusAxis angle={90} />
<Radar name="Ideal Load" dataKey="target" stroke="darkgreen" fill="white" fillOpacity={0} />
<Radar name="EOD Load" dataKey="current" stroke="dodgerblue" fill="dodgerblue" fillOpacity={0.6} />
<Tooltip />
<Legend />
</RadarChart>
</div>
const popContent = useMemo(
() => (
<div>
<Space>
{t("appointments.labels.expectedprodhrs")}
<strong>{loadData?.expectedHours?.toFixed(1) || 0}</strong>
{t("appointments.labels.expectedjobs")}
<strong>{loadData?.expectedJobCount || 0}</strong>
</Space>
<RadarChart width={300} height={250} data={data}>
<PolarGrid />
<PolarAngleAxis dataKey="bucket" />
<PolarRadiusAxis angle={90} />
<Radar name="Ideal Load" dataKey="target" stroke="darkgreen" fill="white" fillOpacity={0} />
<Radar name="EOD Load" dataKey="current" stroke="dodgerblue" fill="dodgerblue" fillOpacity={0.6} />
<Tooltip />
<Legend />
</RadarChart>
</div>
),
[t, loadData, data]
);
return (
@@ -66,6 +59,6 @@ export function ScheduleCalendarHeaderGraph({ bodyshop, loadData }) {
<RadarChartOutlined />
</Popover>
);
}
});
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleCalendarHeaderGraph);

View File

@@ -1,9 +1,8 @@
import React, { useCallback, useMemo } from "react";
import Icon from "@ant-design/icons";
import { Popover, Space } from "antd";
import _ from "lodash";
import dayjs from "../../utils/day";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { MdFileDownload, MdFileUpload } from "react-icons/md";
import { connect } from "react-redux";
@@ -24,115 +23,114 @@ const mapStateToProps = createStructuredSelector({
calculating: selectScheduleLoadCalculating
});
const mapDispatchToProps = (dispatch) => ({});
const mapDispatchToProps = () => ({});
export function ScheduleCalendarHeaderComponent({
export const ScheduleCalendarHeaderComponent = React.memo(function ScheduleCalendarHeaderComponent({
bodyshop,
label,
refetch,
date,
load,
calculating,
events,
...otherProps
events
}) {
const dayjsDate = useMemo(() => dayjs(date), [date]);
const { t } = useTranslation();
const ATSToday = useMemo(() => {
if (!events) return [];
return _.groupBy(
events.filter((e) => !e.vacation && e.isintake && dayjs(date).isSame(dayjs(e.start), "day")),
"job.alt_transport"
);
}, [events, date]);
const filteredEvents = events.filter((e) => !e.vacation && e.isintake && dayjsDate.isSame(dayjs(e.start), "day"));
return _.groupBy(filteredEvents, "job.alt_transport");
}, [events, dayjsDate]);
const isDayBlocked = useMemo(() => {
if (!events) return [];
return events && events.filter((e) => dayjs(date).isSame(dayjs(e.start), "day") && e.block);
}, [events, date]);
return events.filter((e) => dayjsDate.isSame(dayjs(e.start), "day") && e.block);
}, [events, dayjsDate]);
const { t } = useTranslation();
const loadData = load[date.toISOString().substr(0, 10)];
const dateString = dayjsDate.format("YYYY-MM-DD");
const loadData = load[dateString];
const jobsOutPopup = () => (
<div onClick={(e) => e.stopPropagation()}>
<table>
<tbody>
{loadData && loadData.allJobsOut ? (
loadData.allJobsOut.map((j) => (
<tr key={j.id}>
<td style={{ padding: "2.5px" }}>
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link> (
{j.status})
</td>
<td style={{ padding: "2.5px" }}>
<OwnerNameDisplay ownerObject={j} />
</td>
<td style={{ padding: "2.5px" }}>
{`(${j.labhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0}/${
j.larhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0
}/${(
j.labhrs.aggregate?.sum?.mod_lb_hrs +
j.larhrs.aggregate?.sum?.mod_lb_hrs
).toFixed(1)} ${t("general.labels.hours")})`}
</td>
<td style={{ padding: "2.5px" }}>
<DateTimeFormatter>
{j.scheduled_completion}
</DateTimeFormatter>
</td>
const jobsOutPopup = useCallback(
() => (
<div onClick={(e) => e.stopPropagation()}>
<table>
<tbody>
{loadData && loadData.allJobsOut && loadData.allJobsOut.length > 0 ? (
loadData.allJobsOut.map((j) => (
<tr key={j.id}>
<td style={{ padding: "2.5px" }}>
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link> ({j.status})
</td>
<td style={{ padding: "2.5px" }}>
<OwnerNameDisplay ownerObject={j} />
</td>
<td style={{ padding: "2.5px" }}>
{`(${j.labhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0}/${
j.larhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0
}/${(
(j.labhrs?.aggregate?.sum?.mod_lb_hrs || 0) + (j.larhrs?.aggregate?.sum?.mod_lb_hrs || 0)
).toFixed(1)} ${t("general.labels.hours")})`}
</td>
<td style={{ padding: "2.5px" }}>
<DateTimeFormatter>{j.scheduled_completion}</DateTimeFormatter>
</td>
</tr>
))
) : (
<tr>
<td style={{ padding: "2.5px" }}>{t("appointments.labels.nocompletingjobs")}</td>
</tr>
))
) : (
<tr>
<td style={{ padding: "2.5px" }}>
{t("appointments.labels.nocompletingjobs")}
</td>
</tr>
)}
</tbody>
</table>
</div>
)}
</tbody>
</table>
</div>
),
[loadData, t]
);
const jobsInPopup = () => (
<div onClick={(e) => e.stopPropagation()}>
<table>
<tbody>
{loadData && loadData.allJobsIn ? (
loadData.allJobsIn.map((j) => (
<tr key={j.id}>
<td style={{ padding: "2.5px" }}>
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
</td>
<td style={{ padding: "2.5px" }}>
<OwnerNameDisplay ownerObject={j} />
</td>
<td style={{ padding: "2.5px" }}>
{`(${j.labhrs?.aggregate?.sum.mod_lb_hrs?.toFixed(1) || 0}/${
j.larhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0
}/${(
j.labhrs?.aggregate?.sum?.mod_lb_hrs +
j.larhrs?.aggregate?.sum?.mod_lb_hrs
).toFixed(1)} ${t("general.labels.hours")})`}
</td>
<td style={{ padding: "2.5px" }}>
<DateTimeFormatter>{j.scheduled_in}</DateTimeFormatter>
</td>
const jobsInPopup = useCallback(
() => (
<div onClick={(e) => e.stopPropagation()}>
<table>
<tbody>
{loadData && loadData.allJobsIn && loadData.allJobsIn.length > 0 ? (
loadData.allJobsIn.map((j) => (
<tr key={j.id}>
<td style={{ padding: "2.5px" }}>
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
</td>
<td style={{ padding: "2.5px" }}>
<OwnerNameDisplay ownerObject={j} />
</td>
<td style={{ padding: "2.5px" }}>
{`(${j.labhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0}/${
j.larhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0
}/${(
(j.labhrs?.aggregate?.sum?.mod_lb_hrs || 0) + (j.larhrs?.aggregate?.sum?.mod_lb_hrs || 0)
).toFixed(1)} ${t("general.labels.hours")})`}
</td>
<td style={{ padding: "2.5px" }}>
<DateTimeFormatter>{j.scheduled_in}</DateTimeFormatter>
</td>
</tr>
))
) : (
<tr>
<td style={{ padding: "2.5px" }}>{t("appointments.labels.noarrivingjobs")}</td>
</tr>
))
) : (
<tr>
<td style={{ padding: "2.5px" }}>
{t("appointments.labels.noarrivingjobs")}
</td>
</tr>
)}
</tbody>
</table>
</div>
)}
</tbody>
</table>
</div>
),
[loadData, t]
);
const LoadComponent = loadData ? (
<div>
const LoadComponent = useMemo(() => {
if (!loadData) return null;
return (
<div>
<Space align="center">
<Popover
placement={"bottom"}
@@ -141,12 +139,8 @@ export function ScheduleCalendarHeaderComponent({
title={t("appointments.labels.arrivingjobs")}
>
<Icon component={MdFileDownload} style={{ color: "green" }} />
{(loadData.allHoursInBody || 0) &&
loadData.allHoursInBody.toFixed(1)}
/
{(loadData.allHoursInRefinish || 0) &&
loadData.allHoursInRefinish.toFixed(1)}
/{(loadData.allHoursIn || 0) && loadData.allHoursIn.toFixed(1)}
{(loadData.allHoursInBody || 0).toFixed(1)}/{(loadData.allHoursInRefinish || 0).toFixed(1)}/
{(loadData.allHoursIn || 0).toFixed(1)}
</Popover>
<Popover
placement={"bottom"}
@@ -155,57 +149,31 @@ export function ScheduleCalendarHeaderComponent({
title={t("appointments.labels.completingjobs")}
>
<Icon component={MdFileUpload} style={{ color: "red" }} />
{(loadData.allHoursOut || 0) && loadData.allHoursOut.toFixed(1)}
{(loadData.allHoursOut || 0).toFixed(1)}
</Popover>
<ScheduleCalendarHeaderGraph loadData={loadData} />
</Space>
<div>
<ul style={{ listStyleType: "none", columns: "2 auto", padding: 0 }}>
{Object.keys(ATSToday).map((key, idx) => (
<li key={idx}>{`${key === "null" || key === "undefined" ? "N/A" : key}: ${ATSToday[key].length}`}</li>
))}
</ul>
<div>
<ul style={{ listStyleType: "none", columns: "2 auto", padding: 0 }}>
{Object.keys(ATSToday).map((key, idx) => (
<li key={idx}>{`${key === "null" || key === "undefined" ? "N/A" : key}: ${ATSToday[key].length}`}</li>
))}
</ul>
</div>
</div>
</div>
) : null;
const isShopOpen = (date) => {
let day;
switch (dayjs(date).day()) {
case 0:
day = "sunday";
break;
case 1:
day = "monday";
break;
case 2:
day = "tuesday";
break;
case 3:
day = "wednesday";
break;
case 4:
day = "thursday";
break;
case 5:
day = "friday";
break;
case 6:
day = "saturday";
break;
default:
day = "sunday";
break;
}
);
}, [loadData, jobsInPopup, jobsOutPopup, t, ATSToday]);
const isShopOpen = useCallback(() => {
const days = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
const day = days[dayjsDate.day()];
return bodyshop.workingdays[day];
};
}, [bodyshop, dayjsDate]);
return (
<div className="imex-calendar-load">
<ScheduleBlockDay alreadyBlocked={isDayBlocked.length > 0} date={date} refetch={refetch}>
<div style={{ color: isShopOpen(date) ? "" : "tomato" }}>
<div style={{ color: isShopOpen() ? "" : "tomato" }}>
{label}
{InstanceRenderMgr({
imex: calculating ? <LoadingSkeleton /> : LoadComponent,
@@ -216,6 +184,6 @@ export function ScheduleCalendarHeaderComponent({
</ScheduleBlockDay>
</div>
);
}
});
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleCalendarHeaderComponent);

View File

@@ -1,29 +1,28 @@
import dayjs from "../../utils/day";
export function getRange(dateParam, viewParam) {
let start, end;
let date = dateParam || new Date();
let view = viewParam || "week";
// if view is day: from dayjs(date).startOf('day') to dayjs(date).endOf('day');
if (view === "day") {
start = dayjs(date).startOf("day");
end = dayjs(date).endOf("day");
}
// if view is week: from dayjs(date).startOf('isoWeek') to dayjs(date).endOf('isoWeek');
else if (view === "week") {
start = dayjs(date).startOf("week");
end = dayjs(date).endOf("week");
}
//if view is month: from dayjs(date).startOf('month').subtract(7, 'day') to dayjs(date).endOf('month').add(7, 'day'); i do additional 7 days math because you can see adjacent weeks on month view (that is the way how i generate my recurrent events for the Big Calendar, but if you need only start-end of month - just remove that math);
else if (view === "month") {
start = dayjs(date).startOf("month").subtract(7, "day");
end = dayjs(date).endOf("month").add(7, "day");
}
// if view is agenda: from dayjs(date).startOf('day') to dayjs(date).endOf('day').add(1, 'month');
else if (view === "agenda") {
start = dayjs(date).startOf("day");
end = dayjs(date).endOf("day").add(1, "month");
}
// Predefine range calculation functions for each view
const viewRanges = {
day: (date) => ({
start: date.startOf("day"),
end: date.endOf("day")
}),
week: (date) => ({
start: date.startOf("week"),
end: date.endOf("week")
}),
month: (date) => ({
// Adjusting for adjacent weeks
start: date.startOf("month").subtract(7, "day"),
end: date.endOf("month").add(7, "day")
}),
agenda: (date) => ({
start: date.startOf("day"),
end: date.endOf("day").add(1, "month")
})
};
return { start, end };
export function getRange(dateParam = new Date(), viewParam = "week") {
const date = dayjs(dateParam);
const view = viewRanges[viewParam] ? viewParam : "week";
return viewRanges[view](date);
}

View File

@@ -1,6 +1,6 @@
import dayjs from "../../utils/day";
import queryString from "query-string";
import React from "react";
import React, { useCallback, useMemo } from "react";
import { Calendar, dayjsLocalizer } from "react-big-calendar";
import { connect } from "react-redux";
import { Link, useLocation, useNavigate } from "react-router-dom";
@@ -19,9 +19,10 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
problemJobs: selectProblemJobs
});
const localizer = dayjsLocalizer(dayjs);
export function ScheduleCalendarWrapperComponent({
export const ScheduleCalendarWrapperComponent = React.memo(function ScheduleCalendarWrapperComponent({
bodyshop,
problemJobs,
data,
@@ -31,23 +32,79 @@ export function ScheduleCalendarWrapperComponent({
date,
...otherProps
}) {
const search = queryString.parse(useLocation().search);
const history = useNavigate();
const location = useLocation();
const search = useMemo(() => queryString.parse(location.search), [location.search]);
const navigate = useNavigate();
const { t } = useTranslation();
const handleEventPropStyles = (event, start, end, isSelected) => {
return {
...(event.color && !((search.view || defaultView) === "agenda")
? {
style: {
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
}
}
: {}),
className: `${event.arrived ? "imex-event-arrived" : ""} ${event.block ? "imex-event-block" : ""}`
};
};
const selectedDate = new Date(date || dayjs(search.date) || Date.now());
const selectedDate = useMemo(() => {
return new Date(date || dayjs(search.date).toDate() || Date.now());
}, [date, search.date]);
const minTime = useMemo(() => {
return bodyshop.schedule_start_time ? new Date(bodyshop.schedule_start_time) : new Date("2020-01-01T06:00:00");
}, [bodyshop.schedule_start_time]);
const maxTime = useMemo(() => {
return bodyshop.schedule_end_time ? new Date(bodyshop.schedule_end_time) : new Date("2020-01-01T20:00:00");
}, [bodyshop.schedule_end_time]);
const handleEventPropStyles = useCallback(
(event, start, end, isSelected) => {
return {
...(event.color && !((search.view || defaultView) === "agenda")
? {
style: {
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
}
}
: {}),
className: `${event.arrived ? "imex-event-arrived" : ""} ${event.block ? "imex-event-block" : ""}`
};
},
[search.view, defaultView]
);
const eventComponent = useCallback(
(e) => <Event bodyshop={bodyshop} event={e.event} refetch={refetch} />,
[bodyshop, refetch]
);
const headerComponent = useCallback(
(p) => <HeaderComponent {...p} events={data} refetch={refetch} />,
[data, refetch]
);
const calendarComponents = useMemo(
() => ({
event: eventComponent,
header: headerComponent
}),
[eventComponent, headerComponent]
);
const onNavigate = useCallback(
(date, view, action) => {
const newSearch = { ...search, date: date.toISOString().substr(0, 10) };
navigate({ search: queryString.stringify(newSearch) });
},
[search, navigate]
);
const onView = useCallback(
(view) => {
const newSearch = { ...search, view };
navigate({ search: queryString.stringify(newSearch) });
},
[search, navigate]
);
const onRangeChange = useCallback(
(range) => {
if (setDateRangeCallback) setDateRangeCallback(range);
},
[setDateRangeCallback]
);
return (
<>
@@ -109,32 +166,20 @@ export function ScheduleCalendarWrapperComponent({
events={data}
defaultView={search.view || defaultView || "week"}
date={selectedDate}
onNavigate={(date, view, action) => {
search.date = date.toISOString().substr(0, 10);
history({ search: queryString.stringify(search) });
}}
onRangeChange={(start, end) => {
if (setDateRangeCallback) setDateRangeCallback({ start, end });
}}
onView={(view) => {
search.view = view;
history({ search: queryString.stringify(search) });
}}
onNavigate={onNavigate}
onRangeChange={onRangeChange}
onView={onView}
step={15}
// timeslots={1}
showMultiDayTimes
localizer={localizer}
min={bodyshop.schedule_start_time ? new Date(bodyshop.schedule_start_time) : new Date("2020-01-01T06:00:00")}
max={bodyshop.schedule_end_time ? new Date(bodyshop.schedule_end_time) : new Date("2020-01-01T20:00:00")}
min={minTime}
max={maxTime}
eventPropGetter={handleEventPropStyles}
components={{
event: (e) => Event({ bodyshop: bodyshop, event: e.event, refetch: refetch }),
header: (p) => <HeaderComponent {...p} events={data} refetch={refetch} />
}}
components={calendarComponents}
{...otherProps}
/>
</>
);
}
});
export default connect(mapStateToProps, null)(ScheduleCalendarWrapperComponent);

View File

@@ -1,8 +1,8 @@
import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Checkbox, Col, Row, Select, Space } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import { t } from "i18next";
import React, { useMemo } from "react";
import React, { Profiler, useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import useLocalStorage from "../../utils/useLocalStorage";
import ScheduleAtsSummary from "../schedule-ats-summary/schedule-ats-summary.component";
import ScheduleCalendarWrapperComponent from "../schedule-calendar-wrapper/scheduler-calendar-wrapper.component";
@@ -18,19 +18,17 @@ import _ from "lodash";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleCalendarComponent);
export function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
const ScheduleCalendarComponent = React.memo(function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
const { t } = useTranslation();
const [filter, setFilter] = useLocalStorage("filter_events", {
intake: true,
manual: true,
employeevacation: true,
ins_co_nm: null
});
const [estimatorsFilter, setEstimatiorsFilter] = useLocalStorage("estimators", []);
const [estimatorsFilter, setEstimatorsFilter] = useLocalStorage("estimators", []);
const estimators = useMemo(() => {
return _.uniq([
@@ -48,7 +46,7 @@ export function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
d.__typename === "appointments"
? estimatorsFilter.length === 0
? true
: !!estimatorsFilter.find((e) => e === `${d.job?.est_ct_fn || ""} ${d.job?.est_ct_ln || ""}`.trim())
: estimatorsFilter.includes(`${d.job?.est_ct_fn || ""} ${d.job?.est_ct_ln || ""}`.trim())
: true;
return (
@@ -62,7 +60,85 @@ export function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
});
}, [data, filter, estimatorsFilter]);
const estimatorsOptions = useMemo(() => {
return estimators.map((e) => ({
label: e,
value: e
}));
}, [estimators]);
const insCoNmOptions = useMemo(() => {
return bodyshop.md_ins_cos.map((i) => ({
label: i.name,
value: i.name
}));
}, [bodyshop.md_ins_cos]);
const handleEstimatorsFilterChange = useCallback(
(e) => {
setEstimatorsFilter(e);
},
[setEstimatorsFilter]
);
const handleEstimatorsFilterClear = useCallback(() => {
setEstimatorsFilter([]);
}, [setEstimatorsFilter]);
const handleInsCoNmFilterChange = useCallback(
(e) => {
setFilter((prevFilter) => ({ ...prevFilter, ins_co_nm: e }));
},
[setFilter]
);
const handleInsCoNmFilterClear = useCallback(() => {
setFilter((prevFilter) => ({ ...prevFilter, ins_co_nm: [] }));
}, [setFilter]);
const handleIntakeFilterChange = useCallback(
(e) => {
const checked = e.target.checked;
setFilter((prevFilter) => ({ ...prevFilter, intake: checked }));
},
[setFilter]
);
const handleManualFilterChange = useCallback(
(e) => {
const checked = e.target.checked;
setFilter((prevFilter) => ({ ...prevFilter, manual: checked }));
},
[setFilter]
);
const handleEmployeeVacationFilterChange = useCallback(
(e) => {
const checked = e.target.checked;
setFilter((prevFilter) => ({ ...prevFilter, employeevacation: checked }));
},
[setFilter]
);
const handleRefetch = useCallback(() => {
refetch();
}, [refetch]);
return (
// TODO Remove when done
// <Profiler
// id="cal"
// onRender={(id, phase, actualDuration, baseDuration, startTime, commitTime) => {
// console.dir({
// id,
// phase,
// actualDuration,
// baseDuration,
// startTime,
// commitTime
// });
// }}
// >
<Row gutter={[16, 16]}>
<ScheduleModal />
@@ -76,65 +152,35 @@ export function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
mode="multiple"
placeholder={t("schedule.labels.estimators")}
allowClear
onClear={() => setEstimatiorsFilter([])}
value={[...estimatorsFilter]}
onChange={(e) => {
setEstimatiorsFilter(e);
}}
options={estimators.map((e) => ({
label: e,
value: e
}))}
onClear={handleEstimatorsFilterClear}
value={estimatorsFilter}
onChange={handleEstimatorsFilterChange}
options={estimatorsOptions}
/>
<Select
style={{ minWidth: "15rem" }}
mode="multiple"
placeholder={t("schedule.labels.ins_co_nm_filter")}
allowClear
onClear={() => setFilter({ ...filter, ins_co_nm: [] })}
value={filter?.ins_co_nm ? filter.ins_co_nm : []}
onChange={(e) => {
setFilter({ ...filter, ins_co_nm: e });
}}
options={bodyshop.md_ins_cos.map((i) => ({
label: i.name,
value: i.name
}))}
onClear={handleInsCoNmFilterClear}
value={filter.ins_co_nm || []}
onChange={handleInsCoNmFilterChange}
options={insCoNmOptions}
/>
<Checkbox
checked={filter?.intake}
onChange={(e) => {
setFilter({ ...filter, intake: e.target.checked });
}}
>
<Checkbox checked={filter.intake} onChange={handleIntakeFilterChange}>
{t("schedule.labels.intake")}
</Checkbox>
<Checkbox
checked={filter?.manual}
onChange={(e) => {
setFilter({ ...filter, manual: e.target.checked });
}}
>
<Checkbox checked={filter.manual} onChange={handleManualFilterChange}>
{t("schedule.labels.manual")}
</Checkbox>
<Checkbox
checked={filter?.employeevacation}
onChange={(e) => {
setFilter({ ...filter, employeevacation: e.target.checked });
}}
>
<Checkbox checked={filter.employeevacation} onChange={handleEmployeeVacationFilterChange}>
{t("schedule.labels.employeevacation")}
</Checkbox>
<ScheduleVerifyIntegrity />
<Button
onClick={() => {
refetch();
}}
>
<Button onClick={handleRefetch}>
<SyncOutlined />
</Button>
<ScheduleProductionList />
<ScheduleManualEvent />
</Space>
}
@@ -147,5 +193,9 @@ export function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
</Card>
</Col>
</Row>
// TODO Remove when done
// </Profiler>
);
}
});
export default connect(mapStateToProps)(ScheduleCalendarComponent);

View File

@@ -15,56 +15,65 @@ import dayjs from "../../utils/day";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
calculateScheduleLoad: (endDate) => dispatch(calculateScheduleLoad(endDate))
});
export function ScheduleCalendarContainer({ calculateScheduleLoad }) {
const search = queryString.parse(useLocation().search);
const ScheduleCalendarContainer = React.memo(function ScheduleCalendarContainer({ calculateScheduleLoad }) {
const location = useLocation();
const search = useMemo(() => queryString.parse(location.search), [location.search]);
const { date, view } = search;
const range = useMemo(() => getRange(date, view), [date, view]);
const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_APPOINTMENTS, {
variables: {
const queryVariables = useMemo(
() => ({
start: range.start.toDate(),
end: range.end.toDate(),
startd: range.start,
endd: range.end
},
skip: !!!range.start || !!!range.end,
}),
[range]
);
const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_APPOINTMENTS, {
variables: queryVariables,
skip: !range.start || !range.end,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only"
});
useEffect(() => {
if (data && range.end) calculateScheduleLoad(range.end);
}, [data, range, calculateScheduleLoad]);
if (data && range.end) {
calculateScheduleLoad(range.end);
}
}, [data, range.end, calculateScheduleLoad]);
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type="error" />;
let normalizedData = [
...data.appointments.map((e) => {
//Required because Hasura returns a string instead of a date object.
return Object.assign({}, e, { start: new Date(e.start) }, { end: new Date(e.end) });
}),
...data.employee_vacation.map((e) => {
//Required because Hasura returns a string instead of a date object.
return {
const normalizedData = useMemo(() => {
if (!data) return [];
return [
...data.appointments.map((e) => ({
...e,
title: `${
(e.employee.first_name && e.employee.first_name.substr(0, 1)) || ""
} ${e.employee.last_name || ""} OUT`,
start: new Date(e.start),
end: new Date(e.end)
})),
...data.employee_vacation.map((e) => ({
...e,
title: `${e.employee.first_name?.[0] || ""} ${e.employee.last_name || ""} OUT`,
color: "red",
start: dayjs(e.start).startOf("day").toDate(),
end: dayjs(e.end).startOf("day").toDate(),
allDay: true,
vacation: true
};
})
];
}))
];
}, [data]);
return <ScheduleCalendarComponent refetch={refetch} data={data ? normalizedData : []} />;
}
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type="error" />;
return <ScheduleCalendarComponent refetch={refetch} data={normalizedData} />;
});
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleCalendarContainer);

View File

@@ -2,9 +2,14 @@ import React from "react";
import { useTranslation } from "react-i18next";
import ScheduleCalendarWrapperComponent from "../schedule-calendar-wrapper/scheduler-calendar-wrapper.component";
export default function ScheduleDayViewComponent({ data, day }) {
const ScheduleDayViewComponent = React.memo(function ScheduleDayViewComponent({ data, day }) {
const { t } = useTranslation();
if (data)
return <ScheduleCalendarWrapperComponent events={data} defaultView="day" view={"day"} views={["day"]} date={day} />;
else return <div>{t("appointments.labels.nodateselected")}</div>;
}
if (data) {
return <ScheduleCalendarWrapperComponent events={data} defaultView="day" view="day" views={["day"]} date={day} />;
} else {
return <div>{t("appointments.labels.nodateselected")}</div>;
}
});
export default ScheduleDayViewComponent;

View File

@@ -1,4 +1,4 @@
import React from "react";
import React, { useMemo } from "react";
import ScheduleDayViewComponent from "./schedule-day-view.component";
import { useQuery } from "@apollo/client";
import { QUERY_APPOINTMENT_BY_DATE } from "../../graphql/appointments.queries";
@@ -6,45 +6,59 @@ import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import dayjs from "../../utils/day";
import { useTranslation } from "react-i18next";
export default function ScheduleDayViewContainer({ day }) {
const ScheduleDayViewContainer = React.memo(function ScheduleDayViewContainer({ day }) {
const { t } = useTranslation();
// Memoize dayjs computations
const dayjsDay = useMemo(() => dayjs(day), [day]);
// Memoize query variables
const queryVariables = useMemo(
() => ({
start: dayjsDay.startOf("day").toISOString(),
end: dayjsDay.endOf("day").toISOString(),
startd: dayjsDay.startOf("day").format("YYYY-MM-DD"),
endd: dayjsDay.add(1, "day").format("YYYY-MM-DD")
}),
[dayjsDay]
);
// Use the useQuery hook
const { loading, error, data } = useQuery(QUERY_APPOINTMENT_BY_DATE, {
variables: {
start: dayjs(day).startOf("day"),
end: dayjs(day).endOf("day"),
startd: dayjs(day).startOf("day").format("YYYY-MM-DD"),
endd: dayjs(day).add(1, "day").format("YYYY-MM-DD")
},
skip: !dayjs(day).isValid(),
variables: queryVariables,
skip: !dayjsDay.isValid(),
fetchPolicy: "network-only",
nextFetchPolicy: "network-only"
});
const { t } = useTranslation();
// Memoize normalizedData
const normalizedData = useMemo(() => {
if (!data) return [];
const appointments = data.appointments.map((e) => ({
...e,
start: new Date(e.start),
end: new Date(e.end)
}));
const vacations = data.employee_vacation.map((e) => ({
...e,
title: `${e.employee.first_name?.[0] || ""} ${e.employee.last_name || ""} OUT`,
color: "red",
start: dayjs(e.start).startOf("day").toDate(),
end: dayjs(e.end).startOf("day").toDate(),
vacation: true
}));
return [...appointments, ...vacations];
}, [data]);
// Handle conditional rendering
if (!day) return <div>{t("appointments.labels.nodateselected")}</div>;
if (loading) return <LoadingSkeleton paragraph={{ rows: 4 }} />;
if (error) return <div>{error.message}</div>;
let normalizedData;
if (data) {
normalizedData = [
...data.appointments.map((e) => {
//Required becuase Hasura returns a string instead of a date object.
return Object.assign({}, e, { start: new Date(e.start) }, { end: new Date(e.end) });
}),
...data.employee_vacation.map((e) => {
//Required becuase Hasura returns a string instead of a date object.
return {
...e,
title: `${
(e.employee.first_name && e.employee.first_name.substr(0, 1)) || ""
} ${e.employee.last_name || ""} OUT`,
color: "red",
start: dayjs(e.start).startOf("day").toDate(),
end: dayjs(e.end).startOf("day").toDate(),
vacation: true
};
})
];
}
return <ScheduleDayViewComponent data={normalizedData} day={day} />;
});
return <ScheduleDayViewComponent data={data ? normalizedData : []} day={day} />;
}
export default ScheduleDayViewContainer;

View File

@@ -1,38 +1,43 @@
import React from "react";
import React, { useMemo } from "react";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import AlertComponent from "../alert/alert.component";
import { Timeline } from "antd";
import { useTranslation } from "react-i18next";
import { DateTimeFormatter } from "../../utils/DateFormatter";
export default function ScheduleExistingAppointmentsList({ existingAppointments }) {
const ScheduleExistingAppointmentsList = React.memo(function ScheduleExistingAppointmentsList({
existingAppointments
}) {
const { t } = useTranslation();
if (existingAppointments.loading) return <LoadingSpinner />;
if (existingAppointments.error) return <AlertComponent message={existingAppointments.error.message} type="error" />;
const { loading, error, data } = existingAppointments;
const items = useMemo(() => {
if (!data) return [];
return data.appointments.map((item) => ({
key: item.id,
color: item.canceled ? "red" : item.arrived ? "green" : "blue",
children: (
<>
{item.canceled
? t("appointments.labels.cancelledappointment")
: item.arrived
? t("appointments.labels.arrivedon")
: t("appointments.labels.scheduledfor")}
<DateTimeFormatter>{item.start}</DateTimeFormatter>
</>
)
}));
}, [data, t]);
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<div>
{t("appointments.labels.priorappointments")}
<Timeline
items={
existingAppointments.data
? existingAppointments.data.appointments.map((item) => ({
key: item.id,
color: item.canceled ? "red" : item.arrived ? "green" : "blue",
children: (
<>
{item.canceled
? t("appointments.labels.cancelledappointment")
: item.arrived
? t("appointments.labels.arrivedon")
: t("appointments.labels.scheduledfor")}
<DateTimeFormatter>{item.start}</DateTimeFormatter>
</>
)
}))
: []
}
/>
<Timeline items={items} />
</div>
);
}
});
export default ScheduleExistingAppointmentsList;

View File

@@ -1,7 +1,7 @@
import { Button, Col, Form, Input, Row, Select, Space, Switch, Typography } from "antd";
import axios from "axios";
import dayjs from "../../utils/day";
import React, { useState } from "react";
import React, { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -19,12 +19,12 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
calculateScheduleLoad: (endDate) => dispatch(calculateScheduleLoad(endDate))
});
export function ScheduleJobModalComponent({
const ScheduleJobModalComponent = React.memo(function ScheduleJobModalComponent({
bodyshop,
form,
existingAppointments,
@@ -36,7 +36,7 @@ export function ScheduleJobModalComponent({
const [loading, setLoading] = useState(false);
const [smartOptions, setSmartOptions] = useState([]);
const handleSmartScheduling = async () => {
const handleSmartScheduling = useCallback(async () => {
setLoading(true);
try {
const response = await axios.post("/scheduling/job", {
@@ -48,21 +48,66 @@ export function ScheduleJobModalComponent({
} finally {
setLoading(false);
}
};
}, [jobId]);
const handleDateBlur = () => {
const handleDateBlur = useCallback(() => {
const values = form.getFieldsValue();
if (lbrHrsData) {
const totalHours =
lbrHrsData.jobs_by_pk.labhrs.aggregate.sum.mod_lb_hrs + lbrHrsData.jobs_by_pk.larhrs.aggregate.sum.mod_lb_hrs;
(lbrHrsData.jobs_by_pk.labhrs.aggregate.sum.mod_lb_hrs || 0) +
(lbrHrsData.jobs_by_pk.larhrs.aggregate.sum.mod_lb_hrs || 0);
if (values.start && !values.scheduled_completion)
form.setFieldsValue({
scheduled_completion: dayjs(values.start).businessDaysAdd(totalHours / bodyshop.target_touchtime, "day")
});
}
};
}, [form, lbrHrsData, bodyshop.target_touchtime]);
const colorOptions = useMemo(() => {
return (
bodyshop.appt_colors &&
bodyshop.appt_colors.map((color) => (
<Select.Option style={{ color: color.color.hex }} key={color.color.hex} value={color.color.hex}>
{color.label}
</Select.Option>
))
);
}, [bodyshop.appt_colors]);
const altTransportOptions = useMemo(() => {
return (
bodyshop.appt_alt_transport &&
bodyshop.appt_alt_transport.map((alt) => (
<Select.Option key={alt} value={alt}>
{alt}
</Select.Option>
))
);
}, [bodyshop.appt_alt_transport]);
const smartOptionsButtons = useMemo(() => {
return smartOptions.map((d, idx) => (
<Button
className="imex-flex-row__margin"
key={idx}
onClick={() => {
const ssDate = dayjs(d);
if (ssDate.isBefore(dayjs())) {
form.setFieldsValue({ start: dayjs() });
} else {
form.setFieldsValue({
start: dayjs(d).add(8, "hour")
});
}
handleDateBlur();
}}
>
<DateFormatter includeDay>{d}</DateFormatter>
</Button>
));
}, [smartOptions, form, handleDateBlur]);
return (
<Row gutter={[16, 16]}>
@@ -80,7 +125,6 @@ export function ScheduleJobModalComponent({
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
@@ -92,7 +136,6 @@ export function ScheduleJobModalComponent({
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
@@ -107,25 +150,7 @@ export function ScheduleJobModalComponent({
<Button onClick={handleSmartScheduling} loading={loading}>
{t("appointments.actions.calculate")}
</Button>
{smartOptions.map((d, idx) => (
<Button
className="imex-flex-row__margin"
key={idx}
onClick={() => {
const ssDate = dayjs(d);
if (ssDate.isBefore(dayjs())) {
form.setFieldsValue({ start: dayjs() });
} else {
form.setFieldsValue({
start: dayjs(d).add(8, "hour")
});
}
handleDateBlur();
}}
>
<DateFormatter includeDay>{d}</DateFormatter>
</Button>
))}
{smartOptionsButtons}
</Space>
</>
),
@@ -144,20 +169,10 @@ export function ScheduleJobModalComponent({
</LayoutFormRow>
<LayoutFormRow grow>
<Form.Item name="color" label={t("appointments.fields.color")}>
<Select allowClear>
{bodyshop.appt_colors &&
bodyshop.appt_colors.map((color) => (
<Select.Option style={{ color: color.color.hex }} key={color.color.hex} value={color.color.hex}>
{color.label}
</Select.Option>
))}
</Select>
<Select allowClear>{colorOptions}</Select>
</Form.Item>
<Form.Item name={"alt_transport"} label={t("jobs.fields.alt_transport")}>
<Select allowClear>
{bodyshop.appt_alt_transport &&
bodyshop.appt_alt_transport.map((alt) => <Select.Option key={alt}>{alt}</Select.Option>)}
</Select>
<Select allowClear>{altTransportOptions}</Select>
</Form.Item>
<Form.Item name={"note"} label={t("appointments.fields.note")}>
<Input />
@@ -183,6 +198,6 @@ export function ScheduleJobModalComponent({
</Col>
</Row>
);
}
});
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleJobModalComponent);

View File

@@ -1,7 +1,7 @@
import { useMutation, useQuery } from "@apollo/client";
import { Form, Modal, notification } from "antd";
import dayjs from "../../utils/day";
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -27,13 +27,21 @@ const mapStateToProps = createStructuredSelector({
scheduleModal: selectSchedule,
currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("schedule")),
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(
insertAuditTrail({
jobid,
operation,
type
})
)
});
export function ScheduleJobModalContainer({
const ScheduleJobModalContainer = React.memo(function ScheduleJobModalContainer({
scheduleModal,
bodyshop,
toggleModalVisible,
@@ -43,168 +51,186 @@ export function ScheduleJobModalContainer({
}) {
const { open, context, actions } = scheduleModal;
const { jobId, job, previousEvent } = context;
const { refetch } = actions;
const [form] = Form.useForm();
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const { data: lbrHrsData } = useQuery(QUERY_LBR_HRS_BY_PK, {
variables: { id: job && job.id },
skip: !job || !job.id,
variables: { id: job?.id },
skip: !job?.id,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only"
});
const [loading, setLoading] = useState(false);
const [cancelAppointment] = useMutation(CANCEL_APPOINTMENT_BY_ID);
const [insertAppointment] = useMutation(INSERT_APPOINTMENT);
const [updateJobStatus] = useMutation(UPDATE_JOBS);
useEffect(() => {
if (job) form.resetFields();
}, [job, form]);
const { t } = useTranslation();
const existingAppointments = useQuery(QUERY_APPOINTMENTS_BY_JOBID, {
variables: { jobid: jobId },
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
skip: !open || !!!jobId
skip: !open || !jobId
});
useEffect(() => {
if (
existingAppointments.data &&
existingAppointments.data.appointments.length > 0 &&
!existingAppointments.data.appointments[0].canceled
) {
form.setFieldsValue({
color: existingAppointments.data.appointments[0].color,
if (job) form.resetFields();
}, [job, form]);
note: existingAppointments.data.appointments[0].note
useEffect(() => {
const appointments = existingAppointments.data?.appointments;
if (appointments?.length && !appointments[0].canceled) {
form.setFieldsValue({
color: appointments[0].color,
note: appointments[0].note
});
}
}, [existingAppointments.data, form]);
const handleFinish = async (values) => {
logImEXEvent("schedule_new_appointment");
const handleFinish = useCallback(
async (values) => {
logImEXEvent("schedule_new_appointment");
setLoading(true);
setLoading(true);
if (!!previousEvent) {
const cancelAppt = await cancelAppointment({
variables: { appid: previousEvent }
});
if (!!cancelAppt.errors) {
notification["error"]({
message: t("appointments.errors.canceling", {
message: JSON.stringify(cancelAppt.errors)
})
if (previousEvent) {
const cancelAppt = await cancelAppointment({
variables: { appid: previousEvent }
});
return;
}
notification["success"]({
message: t("appointments.successes.canceled")
});
}
if (existingAppointments.data.appointments.length > 0) {
await Promise.all(
existingAppointments.data.appointments.map((app) => {
return cancelAppointment({
variables: { appid: app.id }
if (cancelAppt.errors) {
notification.error({
message: t("appointments.errors.canceling", {
message: JSON.stringify(cancelAppt.errors)
})
});
})
);
}
return;
}
const appt = await insertAppointment({
variables: {
app: {
jobid: jobId,
bodyshopid: bodyshop.id,
start: dayjs(values.start),
end: dayjs(values.start).add(bodyshop.appt_length || 60, "minute"),
color: values.color,
note: values.note,
created_by: currentUser.email
},
jobId: jobId,
altTransport: values.alt_transport
notification.success({
message: t("appointments.successes.canceled")
});
}
});
if (!appt.errors) {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.appointmentinsert(DateTimeFormat(values.start)),
type: "appointmentinsert"
});
}
const existingApps = existingAppointments.data?.appointments || [];
if (existingApps.length > 0) {
await Promise.all(
existingApps.map((app) =>
cancelAppointment({
variables: { appid: app.id }
})
)
);
}
if (!!appt.errors) {
notification["error"]({
message: t("appointments.errors.saving", {
message: JSON.stringify(appt.errors)
})
});
return;
}
notification["success"]({
message: t("appointments.successes.created")
});
if (jobId) {
const jobUpdate = await updateJobStatus({
const appt = await insertAppointment({
variables: {
jobIds: [jobId],
fields: {
status: bodyshop.md_ro_statuses.default_scheduled,
date_scheduled: new Date(),
scheduled_in: values.start,
scheduled_completion: values.scheduled_completion,
lost_sale_reason: null,
date_lost_sale: null
}
app: {
jobid: jobId,
bodyshopid: bodyshop.id,
start: dayjs(values.start),
end: dayjs(values.start).add(bodyshop.appt_length || 60, "minute"),
color: values.color,
note: values.note,
created_by: currentUser.email
},
jobId: jobId,
altTransport: values.alt_transport
}
});
if (!!jobUpdate.errors) {
notification["error"]({
if (!appt.errors) {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.appointmentinsert(DateTimeFormat(values.start)),
type: "appointmentinsert"
});
} else {
notification.error({
message: t("appointments.errors.saving", {
message: JSON.stringify(jobUpdate.errors)
message: JSON.stringify(appt.errors)
})
});
return;
}
}
setLoading(false);
toggleModalVisible();
if (values.notifyCustomer) {
setEmailOptions({
jobid: jobId,
messageOptions: {
to: [values.email],
replyTo: bodyshop.email,
subject: TemplateList("appointment").appointment_confirmation.subject
},
template: {
name: TemplateList("appointment").appointment_confirmation.key,
variables: {
id: appt.data.insert_appointments.returning[0].id
}
}
notification.success({
message: t("appointments.successes.created")
});
}
if (refetch) refetch();
};
if (jobId) {
const jobUpdate = await updateJobStatus({
variables: {
jobIds: [jobId],
fields: {
status: bodyshop.md_ro_statuses.default_scheduled,
date_scheduled: new Date(),
scheduled_in: values.start,
scheduled_completion: values.scheduled_completion,
lost_sale_reason: null,
date_lost_sale: null
}
}
});
if (jobUpdate.errors) {
notification.error({
message: t("appointments.errors.saving", {
message: JSON.stringify(jobUpdate.errors)
})
});
return;
}
}
if (values.notifyCustomer) {
setEmailOptions({
jobid: jobId,
messageOptions: {
to: [values.email],
replyTo: bodyshop.email,
subject: TemplateList("appointment").appointment_confirmation.subject
},
template: {
name: TemplateList("appointment").appointment_confirmation.key,
variables: {
id: appt.data.insert_appointments.returning[0].id
}
}
});
}
if (refetch) refetch();
toggleModalVisible();
setLoading(false);
},
[
t,
previousEvent,
cancelAppointment,
existingAppointments.data,
insertAppointment,
jobId,
bodyshop.id,
bodyshop.appt_length,
currentUser.email,
insertAuditTrail,
job,
updateJobStatus,
bodyshop.md_ro_statuses.default_scheduled,
setEmailOptions,
refetch,
toggleModalVisible,
bodyshop.email
]
);
return (
<Modal
open={open}
onCancel={() => toggleModalVisible()}
onCancel={toggleModalVisible}
onOk={() => form.submit()}
width={"90%"}
width="90%"
maskClosable={false}
destroyOnClose
okButtonProps={{
@@ -217,10 +243,9 @@ export function ScheduleJobModalContainer({
layout="vertical"
onFinish={handleFinish}
initialValues={{
notifyCustomer: !!(job && job.ownr_ea),
email: (job && job.ownr_ea) || "",
notifyCustomer: !!job?.ownr_ea,
email: job?.ownr_ea || "",
start: null,
// smartDates: [],
scheduled_completion: null,
color: context.color,
alt_transport: context.alt_transport,
@@ -236,6 +261,6 @@ export function ScheduleJobModalContainer({
</Form>
</Modal>
);
}
});
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleJobModalContainer);

View File

@@ -1,7 +1,7 @@
import { useMutation } from "@apollo/client";
import { Button, Card, Form, Input, Popover, Select, Space } from "antd";
import dayjs from "../../utils/day";
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 { createStructuredSelector } from "reselect";
@@ -13,142 +13,143 @@ import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleManualEvent);
export function ScheduleManualEvent({ bodyshop, event }) {
const ScheduleManualEvent = React.memo(function ScheduleManualEvent({ bodyshop, event }) {
const { t } = useTranslation();
const [insertAppointment] = useMutation(INSERT_MANUAL_APPT);
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
const [insertAppointment] = useMutation(INSERT_MANUAL_APPT, {
refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"]
});
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT, {
refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"]
});
const [loading, setLoading] = useState(false);
const [form] = Form.useForm();
const [visibility, setVisibility] = useState(false);
// const [callQuery, { loading: entryLoading, data: entryData }] = useLazyQuery(
// QUERY_SCOREBOARD_ENTRY
// );
const handleFinish = useCallback(
async (values) => {
logImEXEvent("schedule_manual_event");
setLoading(true);
try {
if (event && event.id) {
await updateAppointment({
variables: { appid: event.id, app: values }
});
} else {
await insertAppointment({
variables: {
apt: {
...values,
isintake: false,
bodyshopid: bodyshop.id
}
}
});
}
form.resetFields();
setVisibility(false);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
},
[event, updateAppointment, insertAppointment, bodyshop.id, form]
);
const handleClick = useCallback(() => {
setVisibility(true);
}, []);
useEffect(() => {
if (visibility && event) {
form.setFieldsValue(event);
} else if (!visibility) {
form.resetFields();
}
}, [visibility, form, event]);
const handleFinish = async (values) => {
logImEXEvent("schedule_manual_event");
setLoading(true);
try {
if (event && event.id) {
updateAppointment({
variables: { appid: event.id, app: values },
refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"]
});
} else {
insertAppointment({
variables: {
apt: { ...values, isintake: false, bodyshopid: bodyshop.id }
},
refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"]
});
}
form.resetFields();
setVisibility(false);
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
};
const colorOptions = useMemo(() => {
return bodyshop.appt_colors.map((col, idx) => (
<Select.Option key={idx} value={col.color.hex}>
{col.label}
</Select.Option>
));
}, [bodyshop.appt_colors]);
const overlay = (
<Card>
<div>
<Form form={form} layout="vertical" onFinish={handleFinish}>
<Form.Item
label={t("appointments.fields.title")}
name="title"
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Input />
</Form.Item>
<Form.Item label={t("appointments.fields.note")} name="note">
<Input />
</Form.Item>
<Form.Item
label={t("appointments.fields.start")}
name="start"
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<FormDateTimePickerComponent />
</Form.Item>
<Form.Item
label={t("appointments.fields.end")}
name="end"
rules={[
{
required: true
//message: t("general.validation.required"),
},
({ getFieldValue }) => ({
async validator(rule, value) {
if (value) {
const { start } = form.getFieldsValue();
if (dayjs(start).isAfter(dayjs(value))) {
return Promise.reject(t("employees.labels.endmustbeafterstart"));
} else {
return Promise.resolve();
}
<Form form={form} layout="vertical" onFinish={handleFinish}>
<Form.Item
label={t("appointments.fields.title")}
name="title"
rules={[
{
required: true
}
]}
>
<Input />
</Form.Item>
<Form.Item label={t("appointments.fields.note")} name="note">
<Input />
</Form.Item>
<Form.Item
label={t("appointments.fields.start")}
name="start"
rules={[
{
required: true
}
]}
>
<FormDateTimePickerComponent />
</Form.Item>
<Form.Item
label={t("appointments.fields.end")}
name="end"
rules={[
{
required: true
},
({ getFieldValue }) => ({
validator(rule, value) {
if (value) {
const start = form.getFieldValue("start");
if (dayjs(start).isAfter(dayjs(value))) {
return Promise.reject(t("employees.labels.endmustbeafterstart"));
} else {
return Promise.resolve();
}
} else {
return Promise.resolve();
}
})
]}
>
<FormDateTimePickerComponent />
</Form.Item>
<Form.Item label={t("appointments.fields.color")} name="color">
<Select>
{bodyshop.appt_colors.map((col, idx) => (
<Select.Option key={idx} value={col.color.hex}>
{col.label}
</Select.Option>
))}
</Select>
</Form.Item>
<Space wrap>
<Button type="primary" htmlType="submit">
{t("general.actions.save")}
</Button>
<Button onClick={() => setVisibility(false)}>{t("general.actions.cancel")}</Button>
</Space>
</Form>
</div>
}
})
]}
>
<FormDateTimePickerComponent />
</Form.Item>
<Form.Item label={t("appointments.fields.color")} name="color">
<Select>{colorOptions}</Select>
</Form.Item>
<Space wrap>
<Button type="primary" htmlType="submit" loading={loading}>
{t("general.actions.save")}
</Button>
<Button onClick={() => setVisibility(false)}>{t("general.actions.cancel")}</Button>
</Space>
</Form>
</Card>
);
const handleClick = (e) => {
setVisibility(true);
};
return (
<Popover content={overlay} open={visibility}>
<Button loading={loading} onClick={handleClick}>
<Button onClick={handleClick}>
{event ? t("appointments.actions.reschedule") : t("appointments.labels.manualevent")}
</Button>
</Popover>
);
}
});
export default connect(mapStateToProps)(ScheduleManualEvent);

View File

@@ -1,6 +1,6 @@
import { DownOutlined } from "@ant-design/icons";
import { Button, Card, Popover } from "antd";
import React from "react";
import React, { useCallback } from "react";
import { useLazyQuery } from "@apollo/client";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
@@ -11,52 +11,52 @@ import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import "./schedule-production-list.styles.scss";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
export default function ScheduleProductionList() {
const ScheduleProductionList = React.memo(function ScheduleProductionList() {
const { t } = useTranslation();
const [callQuery, { loading, error, data }] = useLazyQuery(QUERY_JOBS_IN_PRODUCTION);
const content = () => {
const content = useCallback(() => {
return (
<Card>
<div onClick={(e) => e.stopPropagation()} className="jobs-in-production-table">
{loading ? <LoadingSkeleton /> : null}
{error ? <AlertComponent message={error.message} type="error" /> : null}
{data ? (
{loading && <LoadingSkeleton />}
{error && <AlertComponent message={error.message} type="error" />}
{data && data.jobs && (
<table>
<tbody>
{data && data.jobs
? data.jobs.map((j) => (
<tr key={j.id}>
<td>
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
</td>
<td>
<OwnerNameDisplay ownerObject={j} />
</td>
<td>{`${j.v_model_yr || ""} ${j.v_make_desc || ""} ${j.v_model_desc || ""}`}</td>
<td>{`${j.labhrs.aggregate.sum.mod_lb_hrs || "0"} / ${
j.larhrs.aggregate.sum.mod_lb_hrs || "0"
}`}</td>
<td>
<DateTimeFormatter>{j.scheduled_completion}</DateTimeFormatter>
</td>
</tr>
))
: null}
{data.jobs.map((j) => (
<tr key={j.id}>
<td>
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
</td>
<td>
<OwnerNameDisplay ownerObject={j} />
</td>
<td>{`${j.v_model_yr || ""} ${j.v_make_desc || ""} ${j.v_model_desc || ""}`}</td>
<td>{`${j.labhrs.aggregate.sum.mod_lb_hrs || "0"} / ${
j.larhrs.aggregate.sum.mod_lb_hrs || "0"
}`}</td>
<td>
<DateTimeFormatter>{j.scheduled_completion}</DateTimeFormatter>
</td>
</tr>
))}
</tbody>
</table>
) : null}
)}
</div>
</Card>
);
};
}, [loading, error, data]);
return (
<Popover content={content} trigger="click" placement="bottomRight">
<Button onClick={() => callQuery()}>
<Button onClick={callQuery}>
{t("appointments.labels.inproduction")}
<DownOutlined />
</Button>
</Popover>
);
}
});
export default ScheduleProductionList;

View File

@@ -1,7 +1,7 @@
import { useApolloClient } from "@apollo/client";
import { Button } from "antd";
import dayjs from "../../utils/day";
import React, { useState } from "react";
import React, { useCallback, useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_SCHEDULE_LOAD_DATA } from "../../graphql/appointments.queries";
@@ -10,49 +10,46 @@ import { selectCurrentUser } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleVerifyIntegrity);
export function ScheduleVerifyIntegrity({ currentUser }) {
const ScheduleVerifyIntegrity = React.memo(function ScheduleVerifyIntegrity({ currentUser }) {
const [loading, setLoading] = useState(false);
const client = useApolloClient();
const handleVerify = async () => {
const handleVerify = useCallback(async () => {
setLoading(true);
const {
data: { arrJobs, compJobs, prodJobs }
} = await client.query({
query: QUERY_SCHEDULE_LOAD_DATA,
variables: { start: dayjs(), end: dayjs().add(180, "day") }
});
try {
const {
data: { arrJobs, compJobs, prodJobs }
} = await client.query({
query: QUERY_SCHEDULE_LOAD_DATA,
variables: { start: dayjs(), end: dayjs().add(180, "day") }
});
//check that the leaving jobs are either in the arriving list, or in production.
const issues = [];
// Check that the completing jobs are either in production or arriving within the next 180 days.
const issues = compJobs.filter((j) => {
const inProdJobs = prodJobs.some((p) => p.id === j.id);
const inArrJobs = arrJobs.some((p) => p.id === j.id);
return !(inProdJobs || inArrJobs);
});
compJobs.forEach((j) => {
const inProdJobs = prodJobs.find((p) => p.id === j.id);
const inArrJobs = arrJobs.find((p) => p.id === j.id);
console.log("The following completing jobs are not in production or arriving within the next 180 days:", issues);
} catch (error) {
console.error("Error verifying schedule integrity:", error);
} finally {
setLoading(false);
}
}, [client]);
if (!(inProdJobs || inArrJobs)) {
// NOT FOUND!
issues.push(j);
}
});
console.log(
"The following completing jobs are not in production, or are arriving within the next 180 days. ",
issues
);
// TODO: A Global helper with developer emails
if (currentUser.email !== "patrick@imex.prod") {
return null;
}
setLoading(false);
};
return (
<Button loading={loading} onClick={handleVerify}>
Developer Use Only - Verify Schedule Integrity
</Button>
);
});
if (currentUser.email === "patrick@imex.prod")
return (
<Button loading={loading} onClick={handleVerify}>
Developer Use Only - Verify Schedule Integrity
</Button>
);
else return null;
}
export default connect(mapStateToProps)(ScheduleVerifyIntegrity);

View File

@@ -4,7 +4,7 @@ import dayjs from "../../utils/day";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_SCOREBOARD_ENTRY } from "../../graphql/scoreboard.queries";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
export default function ScoreboardEntryEdit({ entry }) {
const [open, setOpen] = useState(false);
@@ -52,7 +52,7 @@ export default function ScoreboardEntryEdit({ entry }) {
}
]}
>
<FormDatePicker />
<DateTimePicker isDateOnly />
</Form.Item>
<Form.Item
label={t("scoreboard.fields.bodyhrs")}

View File

@@ -5,7 +5,7 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_VACATION } from "../../graphql/employees.queries";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
export default function ShopEmployeeAddVacation({ employee }) {
const { t } = useTranslation();
@@ -64,7 +64,7 @@ export default function ShopEmployeeAddVacation({ employee }) {
}
]}
>
<FormDatePicker />
<DateTimePicker isDateOnly />
</Form.Item>
<Form.Item
label={t("employees.fields.vacation.end")}
@@ -90,7 +90,7 @@ export default function ShopEmployeeAddVacation({ employee }) {
})
]}
>
<FormDatePicker />
<DateTimePicker isDateOnly />
</Form.Item>
<Space wrap>

View File

@@ -21,12 +21,12 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import CiecaSelect from "../../utils/Ciecaselect";
import { DateFormatter } from "../../utils/DateFormatter";
import AlertComponent from "../alert/alert.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import ShopEmployeeAddVacation from "./shop-employees-add-vacation.component";
import queryString from "query-string";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -266,10 +266,10 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
}
]}
>
<FormDatePicker />
<DateTimePicker isDateOnly />
</Form.Item>
<Form.Item label={t("employees.fields.termination_date")} name="termination_date">
<FormDatePicker />
<DateTimePicker isDateOnly />
</Form.Item>
<Form.Item
label={t("employees.fields.user_email")}

View File

@@ -7,13 +7,13 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import DatePickerRanges from "../../utils/DatePickerRanges";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import FeatureWrapper, { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import FeatureWrapper, { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
// TODO: Client Update, this might break
const timeZonesList = Intl.supportedValuesOf("timeZone");
const mapStateToProps = createStructuredSelector({
@@ -28,10 +28,10 @@ export function ShopInfoGeneral({ form, bodyshop }) {
const { t } = useTranslation();
const {
treatments: { ClosingPeriod }
treatments: { ClosingPeriod, ADPPayroll }
} = useSplitTreatments({
attributes: {},
names: ["ClosingPeriod"],
names: ["ClosingPeriod", "ADPPayroll"],
splitKey: bodyshop && bodyshop.imexshopid
});
@@ -98,7 +98,6 @@ export function ShopInfoGeneral({ form, bodyshop }) {
<Form.Item label={t("bodyshop.fields.email")} name="email">
<Input />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.phone")}
name="phone"
@@ -356,14 +355,22 @@ export function ShopInfoGeneral({ form, bodyshop }) {
<Select mode="tags" />
</Form.Item>
{ClosingPeriod.treatment === "on" && (
<>
<Form.Item
name={["accountingconfig", "ClosingPeriod"]}
label={t("bodyshop.fields.closingperiod")} //{t("reportcenter.labels.dates")}
>
<DatePicker.RangePicker format="MM/DD/YYYY" presets={DatePickerRanges} />
</Form.Item>
</>
<Form.Item
name={["accountingconfig", "ClosingPeriod"]}
label={t("bodyshop.fields.closingperiod")} //{t("reportcenter.labels.dates")}
>
<DatePicker.RangePicker format="MM/DD/YYYY" presets={DatePickerRanges} />
</Form.Item>
)}
{ADPPayroll.treatment === "on" && (
<Form.Item name={["accountingconfig", "companyCode"]} label={t("bodyshop.fields.companycode")}>
<Input />
</Form.Item>
)}
{ADPPayroll.treatment === "on" && (
<Form.Item name={["accountingconfig", "batchID"]} label={t("bodyshop.fields.batchid")}>
<Input />
</Form.Item>
)}
</LayoutFormRow>
</FeatureWrapper>

View File

@@ -30,219 +30,226 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
return (
<RbacWrapper action="shop:rbac">
<LayoutFormRow>
{...HasFeatureAccess({ featureName: "export", bodyshop }) ? [
<Form.Item
label={t("bodyshop.fields.rbac.accounting.exportlog")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "accounting:exportlog"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.accounting.payables")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "accounting:payables"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.accounting.payments")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "accounting:payments"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.accounting.receivables")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "accounting:receivables"]}
>
<InputNumber />
</Form.Item>
]:[]}
{...HasFeatureAccess({ featureName: "bills", bodyshop }) ? [
<Form.Item
label={t("bodyshop.fields.rbac.bills.delete")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:delete"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.bills.enter")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:enter"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.bills.list")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:list"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.bills.reexport")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:reexport"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.bills.view")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:view"]}
>
<InputNumber />
</Form.Item>
]:[]}
{...HasFeatureAccess({ featureName: "courtesycars", bodyshop }) ? [
<Form.Item
label={t("bodyshop.fields.rbac.contracts.create")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "contracts:create"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.contracts.detail")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "contracts:detail"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.contracts.list")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "contracts:list"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.courtesycar.create")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "courtesycar:create"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.courtesycar.detail")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "courtesycar:detail"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.courtesycar.list")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "courtesycar:list"]}
>
<InputNumber />
</Form.Item>
]:[]}
{...HasFeatureAccess({ featureName: "csi", bodyshop }) ? [
<Form.Item
label={t("bodyshop.fields.rbac.csi.export")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "csi:export"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.csi.page")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "csi:page"]}
>
<InputNumber />
</Form.Item>
]:[]}
{...HasFeatureAccess({ featureName: "export", bodyshop })
? [
<Form.Item
label={t("bodyshop.fields.rbac.accounting.exportlog")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "accounting:exportlog"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.accounting.payables")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "accounting:payables"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.accounting.payments")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "accounting:payments"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.accounting.receivables")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "accounting:receivables"]}
>
<InputNumber />
</Form.Item>
]
: []}
{...HasFeatureAccess({ featureName: "bills", bodyshop })
? [
<Form.Item
label={t("bodyshop.fields.rbac.bills.delete")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:delete"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.bills.enter")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:enter"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.bills.list")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:list"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.bills.reexport")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:reexport"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.bills.view")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:view"]}
>
<InputNumber />
</Form.Item>
]
: []}
{...HasFeatureAccess({ featureName: "courtesycars", bodyshop })
? [
<Form.Item
label={t("bodyshop.fields.rbac.contracts.create")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "contracts:create"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.contracts.detail")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "contracts:detail"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.contracts.list")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "contracts:list"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.courtesycar.create")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "courtesycar:create"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.courtesycar.detail")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "courtesycar:detail"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.courtesycar.list")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "courtesycar:list"]}
>
<InputNumber />
</Form.Item>
]
: []}
{...HasFeatureAccess({ featureName: "csi", bodyshop })
? [
<Form.Item
label={t("bodyshop.fields.rbac.csi.export")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "csi:export"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.csi.page")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "csi:page"]}
>
<InputNumber />
</Form.Item>
]
: []}
<Form.Item
label={t("bodyshop.fields.rbac.employees.page")}
rules={[
@@ -255,6 +262,18 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.employee_teams.page")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "employee_teams:page"]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.jobs.admin")}
rules={[
@@ -435,31 +454,6 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.employees.page")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "employees:page"]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.employee_teams.page")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "employee_teams:page"]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.payments.enter")}
rules={[
@@ -522,7 +516,6 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
<InputNumber />
</Form.Item>
)}
<Form.Item
label={t("bodyshop.fields.rbac.production.list")}
rules={[
@@ -561,128 +554,118 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
<InputNumber />
</Form.Item>
)}
{...HasFeatureAccess({ featureName: "timetickets", bodyshop }) ? [
<Form.Item
label={t("bodyshop.fields.rbac.shiftclock.view")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "shiftclock:view"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.shop.config")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "shop:config"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.edit")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:edit"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.shiftedit")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:shiftedit"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.editcommitted")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:editcommitted"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.ttapprovals.view")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "ttapprovals:view"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.ttapprovals.approve")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "ttapprovals:approve"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.enter")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:enter"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.list")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:list"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.shiftedit")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:shiftedit"]}
>
<InputNumber />
</Form.Item>
]:[]}
{...HasFeatureAccess({ featureName: "timetickets", bodyshop })
? [
<Form.Item
label={t("bodyshop.fields.rbac.shiftclock.view")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "shiftclock:view"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.shop.config")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "shop:config"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.edit")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:edit"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.shiftedit")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:shiftedit"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.editcommitted")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:editcommitted"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.ttapprovals.view")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "ttapprovals:view"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.ttapprovals.approve")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "ttapprovals:approve"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.enter")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:enter"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.list")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:list"]}
>
<InputNumber />
</Form.Item>
]
: []}
<Form.Item
label={t("bodyshop.fields.rbac.shop.vendors")}
rules={[
@@ -757,7 +740,6 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
<InputNumber />
</Form.Item>
)}
<Form.Item
label={t("bodyshop.fields.rbac.users.editaccess")}
rules={[

View File

@@ -1,14 +1,13 @@
import { Col, Form, Input, Row, Select, Switch } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { FormDatePicker } from "../form-date-picker/form-date-picker.component.jsx";
import { createStructuredSelector } from "reselect";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
import dayjs from "../../utils/day";
import { connect } from "react-redux";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx";
import JobSearchSelectComponent from "../job-search-select/job-search-select.component.jsx";
import { FormDateTimePickerEnhanced } from "../form-date-time-picker-enhanced/form-date-time-picker-enhanced.component.jsx";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -246,7 +245,8 @@ export function TaskUpsertModalComponent({
</Col>
<Col span={8}>
<Form.Item label={t("tasks.fields.due_date")} name="due_date">
<FormDatePicker
<DateTimePicker
isDateOnly
onlyFuture
format="MM/DD/YYYY"
presets={generatePresets(selectedJobDetails)}
@@ -278,12 +278,7 @@ export function TaskUpsertModalComponent({
}
]}
>
<FormDateTimePickerEnhanced
onlyFuture
showTime
minuteStep={15}
presets={generatePresets(selectedJobDetails)}
/>
<DateTimePicker onlyFuture presets={generatePresets(selectedJobDetails)} />
</Form.Item>
</Col>
</Row>

View File

@@ -1,5 +1,4 @@
import { Button, Card, DatePicker, Form, Popover, Radio, Space } from "antd";
import dayjs from "../../utils/day";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -7,10 +6,12 @@ import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import DatePIckerRanges from "../../utils/DatePickerRanges";
import dayjs from "../../utils/day";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
const mapStateToProps = createStructuredSelector({
bodyshop: selectTechnician,
technician: selectTechnician
});
const mapDispatchToProps = (dispatch) => ({
@@ -18,7 +19,7 @@ const mapDispatchToProps = (dispatch) => ({
});
export default connect(mapStateToProps, mapDispatchToProps)(TechJobPrintTickets);
export function TechJobPrintTickets({ technician, event, attendacePrint }) {
export function TechJobPrintTickets({ bodyshop, technician, event, attendacePrint }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
@@ -57,7 +58,8 @@ export function TechJobPrintTickets({ technician, event, attendacePrint }) {
subject:
attendacePrint === true ? Templates.attendance_employee.subject : Templates.timetickets_employee.subject
},
values.sendby // === "email" ? "e" : "p"
values.sendby,
bodyshop
);
} catch (error) {
console.log(error);

View File

@@ -7,9 +7,9 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { GET_JOB_INFO_DRAW_CALCULATIONS } from "../../graphql/jobs-lines.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -71,7 +71,7 @@ export function TimeTicketListTeamPay({ bodyshop, context, actions }) {
}
]}
>
<FormDatePicker />
<DateTimePicker isDateOnly />
</Form.Item>
</LayoutFormRow>

View File

@@ -1,6 +1,6 @@
import { EditFilled, SyncOutlined } from "@ant-design/icons";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Button, Card, Checkbox, Space, Table } from "antd";
import dayjs from "../../utils/day";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -10,10 +10,10 @@ import { setModalContext } from "../../redux/modals/modals.actions";
import { selectAuthLevel, selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import { onlyUnique } from "../../utils/arrayHelper";
import dayjs from "../../utils/day";
import { alphaSort, dateSort } from "../../utils/sorters";
import RbacWrapper, { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
import TimeTicketEnterButton from "../time-ticket-enter-button/time-ticket-enter-button.component";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -52,6 +52,10 @@ export function TimeTicketList({
splitKey: bodyshop.imexshopid
});
const canEditCommittedTimeTickets = HasRbacAccess({ bodyshop, authLevel, action: "timetickets:editcommitted" });
const canEditTimeTickets = HasRbacAccess({ bodyshop, authLevel, action: "timetickets:edit" });
const canEditShiftTickets = HasRbacAccess({ bodyshop, authLevel, action: "timetickets:shiftedit" });
const totals = useMemo(() => {
if (timetickets)
return timetickets.reduce(
@@ -65,6 +69,18 @@ export function TimeTicketList({
return { productivehrs: 0, actualhrs: 0 };
}, [timetickets]);
const isDisabled = (record) => {
if (disabled === true || !record.id) return true;
const isShiftTicket = !record.ciecacode;
const isCommitted = record.committed_at;
if (isShiftTicket) {
return !(canEditShiftTickets && (!isCommitted || canEditCommittedTimeTickets));
}
return !(canEditTimeTickets && (!isCommitted || canEditCommittedTimeTickets));
};
const columns = [
...(Enhanced_Payroll.treatment === "on"
? [
@@ -165,7 +181,7 @@ export function TimeTicketList({
key: "memo",
sorter: (a, b) => alphaSort(a.memo, b.memo),
sortOrder: state.sortedInfo.columnKey === "memo" && state.sortedInfo.order,
render: (text, record) => (record.clockon || record.clockoff ? t(record.memo) : record.memo)
render: (text, record) => (record.memo?.startsWith("timetickets.labels") ? t(record.memo) : record.memo)
},
...(Enhanced_Payroll.treatment === "on"
? [
@@ -206,76 +222,55 @@ export function TimeTicketList({
return null;
}
}
},
}
]),
{
title: t("timetickets.fields.created_by"),
dataIndex: "created_by",
key: "created_by",
sorter: (a, b) => alphaSort(a.created_by, b.created_by),
sortOrder: state.sortedInfo.columnKey === "created_by" && state.sortedInfo.order,
render: (text, record) => record.created_by
},
// {
// title: "Pay",
// dataIndex: "pay",
// key: "pay",
// render: (text, record) =>
// Dinero({ amount: Math.round(record.rate * 100) })
// .multiply(record.flat_rate ? record.productivehrs : record.actualhrs)
// .toFormat("$0.00"),
// },
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<Space wrap>
{techConsole && (
<TimeTicketEnterButton
actions={{ refetch }}
context={{ id: record.id, timeticket: record }}
disabled={!record.job || disabled}
>
<EditFilled />
</TimeTicketEnterButton>
)}
{!techConsole && (
<RbacWrapper
action="timetickets:edit"
noauth={() => {
return <div />;
}}
>
<TimeTicketEnterButton
actions={{ refetch }}
context={{
id: record.id,
timeticket: record
}}
disabled={
HasRbacAccess({
bodyshop,
authLevel: authLevel,
action: "timetickets:editcommitted"
}) &&
HasRbacAccess({
bodyshop,
authLevel: authLevel,
action: "timetickets:shiftedit"
})
? disabled
: !record.jobid
}
>
<EditFilled />
</TimeTicketEnterButton>
</RbacWrapper>
)}
</Space>
)
}
{
title: t("timetickets.fields.created_by"),
dataIndex: "created_by",
key: "created_by",
sorter: (a, b) => alphaSort(a.created_by, b.created_by),
sortOrder: state.sortedInfo.columnKey === "created_by" && state.sortedInfo.order,
render: (text, record) => record.created_by
},
// {
// title: "Pay",
// dataIndex: "pay",
// key: "pay",
// render: (text, record) =>
// Dinero({ amount: Math.round(record.rate * 100) })
// .multiply(record.flat_rate ? record.productivehrs : record.actualhrs)
// .toFormat("$0.00"),
// },
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<Space wrap>
{techConsole && (
<TimeTicketEnterButton
actions={{ refetch }}
context={{ id: record.id, timeticket: record }}
disabled={!record.job || disabled}
>
<EditFilled />
</TimeTicketEnterButton>
)}
{!techConsole && (
<TimeTicketEnterButton
actions={{ refetch }}
context={{
id: record.id,
timeticket: record
}}
disabled={isDisabled(record)}
>
<EditFilled />
</TimeTicketEnterButton>
)}
</Space>
)
}
];
const handleTableChange = (pagination, filters, sorter) => {

View File

@@ -1,4 +1,5 @@
import { useLazyQuery } from "@apollo/client";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Form, Input, InputNumber, Select, Switch } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -7,8 +8,10 @@ import { createStructuredSelector } from "reselect";
import { GET_LINE_TICKET_BY_PK } from "../../graphql/jobs-lines.queries";
import { selectAuthLevel, selectBodyshop } from "../../redux/user/user.selectors";
import EmployeeSearchSelect from "../employee-search-select/employee-search-select.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormDateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import {
default as DateTimePicker,
default as FormDateTimePicker
} from "../form-date-time-picker/form-date-time-picker.component";
import JobSearchSelect from "../job-search-select/job-search-select.component";
import LaborAllocationsTable from "../labor-allocations-table/labor-allocations-table.component";
import { CalculateAllocationsTotals } from "../labor-allocations-table/labor-allocations-table.utility";
@@ -16,7 +19,6 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
import TimeTicketList from "../time-ticket-list/time-ticket-list.component";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -60,8 +62,8 @@ export function TimeTicketModalComponent({
{item.cost_center === "timetickets.labels.shift"
? t(item.cost_center)
: bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber || Enhanced_Payroll.treatment === "on"
? t(`joblines.fields.lbr_types.${item.cost_center.toUpperCase()}`)
: item.cost_center}
? t(`joblines.fields.lbr_types.${item.cost_center.toUpperCase()}`)
: item.cost_center}
</Select.Option>
))}
</Select>
@@ -69,13 +71,7 @@ export function TimeTicketModalComponent({
};
const MemoInput = ({ value, ...props }) => {
return (
<Input
value={value?.startsWith("timetickets.") ? t(value) : value}
{...props}
disabled={value?.startsWith("timetickets.") || disabled}
/>
);
return <Input value={value?.startsWith("timetickets.labels") ? t(value) : value} {...props} />;
};
return (
@@ -111,7 +107,7 @@ export function TimeTicketModalComponent({
}
]}
>
<FormDatePicker />
<DateTimePicker isDateOnly />
</Form.Item>
<Form.Item
name="employeeid"
@@ -333,7 +329,9 @@ export function LaborAllocationContainer({ jobid, loading, lineTicketData, hideT
timetickets={lineTicketData.timetickets}
adjustments={lineTicketData.jobs_by_pk.lbr_adjustments}
/>
{!hideTimeTickets && <TimeTicketList loading={loading} timetickets={lineTicketData.timetickets} techConsole />}
{!hideTimeTickets && (
<TimeTicketList loading={loading} timetickets={jobid ? lineTicketData.timetickets : []} techConsole />
)}
</div>
);
}

View File

@@ -39,7 +39,7 @@ export default function TimeTicketShiftActive({ timetickets, refetch, isTechCons
renderItem={(ticket) => (
<List.Item>
<Card
title={t(ticket.memo)}
title={ticket.memo?.startsWith("timetickets.labels") ? t(ticket.memo) : ticket.memo}
actions={[
<TechClockOffButton
jobId={ticket.jobid}

View File

@@ -1,13 +1,14 @@
import { AlertOutlined } from "@ant-design/icons";
import { Alert, Button, Col, Row, Space } from "antd";
import { Alert, Button, Col, notification, Row, Space } from "antd";
import i18n from "i18next";
import React, { useEffect } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectUpdateAvailable } from "../../redux/application/application.selectors";
import { useRegisterSW } from "virtual:pwa-register/react";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import useCountDown from "../../utils/countdownHook";
const mapStateToProps = createStructuredSelector({
updateAvailable: selectUpdateAvailable
@@ -19,6 +20,15 @@ const mapDispatchToProps = (dispatch) => ({
export function UpdateAlert({ updateAvailable }) {
const { t } = useTranslation();
const [timerStarted, setTimerStarted] = useState(false);
const [loading, setLoading] = useState(false);
const [
timeLeft,
{
start //pause, resume, reset
}
] = useCountDown(180000, 1000);
const {
offlineReady: [offlineReady],
needRefresh: [needRefresh],
@@ -31,7 +41,7 @@ export function UpdateAlert({ updateAvailable }) {
() => {
r.update();
},
10 * 60 * 1000
30 * 60 * 1000
);
}
},
@@ -40,11 +50,43 @@ export function UpdateAlert({ updateAvailable }) {
}
});
const ReloadNewVersion = useCallback(() => {
setLoading(true);
updateServiceWorker(true);
setTimeout(() => {
window.location.reload(true);
}, 5000);
}, [updateServiceWorker]);
useEffect(() => {
if (import.meta.env.DEV) {
console.log(`SW Status => Refresh? ${needRefresh} - offlineReady? ${offlineReady}`);
if (needRefresh) {
start();
setTimerStarted(true);
}
}, [needRefresh, offlineReady]);
}, [start, needRefresh, offlineReady]);
useEffect(() => {
if (needRefresh && timerStarted && timeLeft < 60000) {
notification.open({
type: "warning",
closable: false,
duration: 65000,
key: "autoupdate",
message: t("general.actions.autoupdate", {
time: (timeLeft / 1000).toFixed(0),
app: InstanceRenderManager({
imex: "$t(titles.imexonline)",
rome: "$t(titles.romeonline)",
promanager: "$t(titles.promanager)"
})
}),
placement: "bottomRight"
});
}
if (needRefresh && timerStarted && timeLeft <= 0) {
ReloadNewVersion();
}
}, [timeLeft, t, needRefresh, ReloadNewVersion, timerStarted]);
if (!needRefresh) return null;
@@ -75,9 +117,10 @@ export function UpdateAlert({ updateAvailable }) {
<Button onClick={() => window.open("https://imex-online.noticeable.news/", "_blank")}>
{i18n.t("general.actions.viewreleasenotes")}
</Button>
<Button type="primary" onClick={() => updateServiceWorker(true)}>
{i18n.t("general.actions.refresh")}
<Button loading={loading} type="primary" onClick={() => ReloadNewVersion()}>
{i18n.t("general.actions.refresh")} {`(${(timeLeft / 1000).toFixed(0)} s)`}
</Button>
<Button onClick={() => start(300000)}>{i18n.t("general.actions.delay")}</Button>
</Space>
</Col>
</Row>

View File

@@ -1,9 +1,9 @@
import { Form, Input } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
export default function VehicleDetailFormComponent({ form, loading }) {
const { t } = useTranslation();
@@ -102,7 +102,7 @@ export default function VehicleDetailFormComponent({ form, loading }) {
<Input />
</Form.Item>
<Form.Item label={t("vehicles.fields.v_prod_dt")} name="v_prod_dt">
<FormDatePicker />
<DateTimePicker isDateOnly />
</Form.Item>
<Form.Item label={t("vehicles.fields.v_paint_codes", { number: 1 })} name={["v_paint_codes", "paint_cd1"]}>

View File

@@ -5,7 +5,7 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter";
const { Option } = Select;
//To be used as a form element only.
// To be used as a form element only.
const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, preferredMake, showPhone }, ref) => {
const [option, setOption] = useState(value);
@@ -33,9 +33,25 @@ const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, pref
if (!value || !options) return label;
const discount = options?.find((o) => o.id === value)?.discount;
return (
<div className="imex-flex-row" style={{ width: "100%" }}>
<div style={{ flex: 1 }}>{label}</div>
<div
style={{
display: "flex",
alignItems: "center",
flexWrap: "nowrap",
width: "100%"
}}
>
<div
style={{
flex: 1,
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap"
}}
>
{label}
</div>
{discount && discount !== 0 ? <Tag color="green">{`${discount * 100}%`}</Tag> : null}
</div>
);
@@ -45,36 +61,67 @@ const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, pref
optionFilterProp="name"
onSelect={onSelect}
disabled={disabled || false}
optionLabelProp={"name"}
optionLabelProp="name"
>
{favorites
? favorites.map((o) => (
<Option key={`favorite-${o.id}`} value={o.id} name={o.name} discount={o.discount}>
<div className="imex-flex-row">
<div style={{ flex: 1 }}>{o.name}</div>
<Space style={{ marginLeft: "1rem" }}>
<HeartOutlined style={{ color: "red" }} />
{o.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
</Space>
{favorites &&
favorites.map((o) => (
<Option key={`favorite-${o.id}`} value={o.id} name={o.name} discount={o.discount}>
<div
style={{
display: "flex",
alignItems: "center",
flexWrap: "nowrap",
width: "100%"
}}
>
<div
style={{
flex: 1,
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap"
}}
>
{o.name}
</div>
</Option>
))
: null}
{options
? options.map((o) => (
<Option key={o.id} value={o.id} name={o.name} discount={o.discount}>
<div className="imex-flex-row" style={{ width: "100%" }}>
<div style={{ flex: 1 }}>{o.name}</div>
<Space style={{ marginLeft: "1rem" }}>
{o.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
</Space>
<Space style={{ marginLeft: "1rem" }}>
<HeartOutlined style={{ color: "red" }} />
{o.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
</Space>
</div>
</Option>
))}
{options &&
options.map((o) => (
<Option key={o.id} value={o.id} name={o.name} discount={o.discount}>
<div
style={{
display: "flex",
alignItems: "center",
flexWrap: "nowrap",
width: "100%"
}}
>
<div
style={{
flex: 1,
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap"
}}
>
{o.name}
</div>
</Option>
))
: null}
<Space style={{ marginLeft: "1rem" }}>
{o.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
</Space>
</div>
</Option>
))}
</Select>
);
};

View File

@@ -5,7 +5,6 @@ import { getFirestore } from "firebase/firestore";
import { getMessaging, getToken, onMessage } from "firebase/messaging";
import { store } from "../redux/store";
import axios from "axios";
import { checkBeta } from "../utils/handleBeta";
const config = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
initializeApp(config);
@@ -88,7 +87,7 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
operationName: eventName,
variables: additionalParams,
dbevent: false,
env: checkBeta() ? "beta" : "master"
env: "master"
});
// console.log(
// "%c[Analytics]",

View File

@@ -571,7 +571,7 @@ export function Manage({ conflict, bodyshop }) {
return (
<>
<ChatAffixContainer bodyshop={bodyshop} chatVisible={chatVisible} />
{import.meta.env.PROD && <ChatAffixContainer bodyshop={bodyshop} chatVisible={chatVisible} />}
<Layout style={{ minHeight: "100vh" }} className="layout-container">
<UpdateAlert />
<HeaderContainer />

View File

@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
import RbacWrapperComponent from "../../components/rbac-wrapper/rbac-wrapper.component";
import TechLookupJobsList from "../../components/tech-lookup-jobs-list/tech-lookup-jobs-list.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import TechLookupJobsDrawer from "../../components/tech-lookup-jobs-drawer/tech-lookup-jobs-drawer.component";
export default function TechLookupContainer() {
const { t } = useTranslation();
@@ -20,6 +21,7 @@ export default function TechLookupContainer() {
return (
<div>
<RbacWrapperComponent action="jobs:list-active">
<TechLookupJobsDrawer />
<TechLookupJobsList />
</RbacWrapperComponent>
</div>

View File

@@ -9,7 +9,6 @@ import ErrorBoundary from "../../components/error-boundary/error-boundary.compon
import FeatureWrapper from "../../components/feature-wrapper/feature-wrapper.component";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import TechHeader from "../../components/tech-header/tech-header.component";
import TechLookupJobsDrawer from "../../components/tech-lookup-jobs-drawer/tech-lookup-jobs-drawer.component";
import TechSider from "../../components/tech-sider/tech-sider.component";
import UpdateAlert from "../../components/update-alert/update-alert.component";
import { selectTechnician } from "../../redux/tech/tech.selectors";
@@ -68,7 +67,7 @@ export function TechPage({ technician }) {
<Layout>
<UpdateAlert />
<TechHeader />
<TechLookupJobsDrawer />
<TaskUpsertModalContainer />
<Content className="tech-content-container">
<ErrorBoundary>

View File

@@ -10,7 +10,7 @@ import {
signInWithEmailAndPassword,
signOut
} from "firebase/auth";
import { doc, getDoc, setDoc } from "firebase/firestore";
import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "firebase/firestore";
import { getToken } from "firebase/messaging";
import i18next from "i18next";
import LogRocket from "logrocket";
@@ -48,6 +48,7 @@ import {
validatePasswordResetSuccess
} from "./user.actions";
import UserActionTypes from "./user.types";
import cleanAxios from "../../utils/CleanAxios";
const fpPromise = FingerprintJS.load();
@@ -177,10 +178,24 @@ export function* setInstanceIdSaga({ payload: uid }) {
// Get the visitor identifier when you need it.
const fp = yield fpPromise;
const result = yield fp.get();
yield setDoc(userInstanceRef, {
timestamp: new Date(),
fingerprint: result.visitorId
});
const res = yield cleanAxios.get("https://api.ipify.org/?format=json");
const udoc = yield getDoc(userInstanceRef);
if (!udoc.data()) {
yield setDoc(userInstanceRef, {
timestamp: new Date(),
fingerprint: result.visitorId,
//totalFingerprint: result,
ip: [res.data.ip]
});
} else {
yield updateDoc(userInstanceRef, {
timestamp: new Date(),
fingerprint: result.visitorId,
//totalFingerprint: result,
ip: arrayUnion(res.data.ip)
});
}
yield put(setLocalFingerprint(result.visitorId));
yield delay(5 * 60 * 1000);

View File

@@ -230,7 +230,7 @@
"markexported": "Mark Exported",
"markforreexport": "Mark for Re-export",
"new": "New Bill",
"nobilllines": "This part has not yet been recieved.",
"nobilllines": "",
"noneselected": "No bill selected.",
"onlycmforinvoiced": "Only credit memos can be entered for any Job that has been invoiced, exported, or voided.",
"printlabels": "Print Labels",
@@ -270,6 +270,7 @@
"testrender": "Test Render"
},
"errors": {
"creatingdefaultview": "Error creating default view.",
"loading": "Unable to load shop details. Please call technical support.",
"saving": "Error encountered while saving. {{message}}"
},
@@ -284,19 +285,21 @@
},
"appt_length": "Default Appointment Length",
"attach_pdf_to_email": "Attach PDF copy to sent emails?",
"batchid": "ADP Batch ID",
"bill_allow_post_to_closed": "Allow Bills to be posted to Closed Jobs",
"bill_federal_tax_rate": "Bills - Federal Tax Rate %",
"bill_local_tax_rate": "Bill - Local Tax Rate %",
"bill_state_tax_rate": "Bill - Provincial/State Tax Rate %",
"city": "City",
"closingperiod": "Closing Period",
"companycode": "ADP Company Code",
"country": "Country",
"dailybodytarget": "Scoreboard - Daily Body Target",
"dailypainttarget": "Scoreboard - Daily Paint Target",
"default_adjustment_rate": "Default Labor Deduction Adjustment Rate",
"deliver": {
"templates": "Delivery Templates",
"require_actual_delivery_date": "Require Actual Delivery"
"require_actual_delivery_date": "Require Actual Delivery",
"templates": "Delivery Templates"
},
"dms": {
"apcontrol": "AP Control Number",
@@ -699,7 +702,10 @@
"workingdays": "Working Days"
},
"successes": {
"save": "Shop configuration saved successfully. "
"areyousure": "Are you sure you want to continue?",
"defaultviewcreated": "Default view created successfully.",
"save": "Shop configuration saved successfully. ",
"unsavedchanges": "Unsaved changes will be lost. Are you sure you want to continue?"
},
"validation": {
"centermustexist": "The chosen responsibility center does not exist.",
@@ -1129,8 +1135,8 @@
},
"general": {
"actions": {
"defaults": "Defaults",
"add": "Add",
"autoupdate": "{{app}} will automatically update in {{time}} seconds. Please save all changes.",
"calculate": "Calculate",
"cancel": "Cancel",
"clear": "Clear",
@@ -1138,6 +1144,8 @@
"copied": "Copied!",
"copylink": "Copy Link",
"create": "Create",
"defaults": "Defaults",
"delay": "Delay Update (5 mins)",
"delete": "Delete",
"deleteall": "Delete All",
"deselectall": "Deselect All",
@@ -1149,10 +1157,12 @@
"print": "Print",
"refresh": "Refresh",
"remove": "Remove",
"remove_alert": "Are you sure you want to dismiss the alert?",
"reset": "Reset your changes.",
"resetpassword": "Reset Password",
"save": "Save",
"saveandnew": "Save and New",
"saveas": "Save As",
"selectall": "Select All",
"send": "Send",
"sendbysms": "Send by SMS",
@@ -1160,8 +1170,7 @@
"submit": "Submit",
"tryagain": "Try Again",
"view": "View",
"viewreleasenotes": "See What's Changed",
"remove_alert": "Are you sure you want to dismiss the alert?"
"viewreleasenotes": "See What's Changed"
},
"errors": {
"fcm": "You must allow notification permissions to have real time messaging. Click to try again.",
@@ -1183,6 +1192,8 @@
"clear": "Clear",
"confirmpassword": "Confirm Password",
"created_at": "Created At",
"date": "Select Date",
"datetime": "Select Date & Time",
"email": "Email",
"errors": "Errors",
"excel": "Excel",
@@ -1242,6 +1253,7 @@
"tuesday": "Tuesday",
"tvmode": "TV Mode",
"unknown": "Unknown",
"unsavedchanges": "Unsaved changes.",
"username": "Username",
"view": "View",
"wednesday": "Wednesday",
@@ -2731,38 +2743,6 @@
}
},
"production": {
"options": {
"small": "Small",
"medium": "Medium",
"large": "Large",
"vertical": "Vertical",
"horizontal": "Horizontal"
},
"settings": {
"layout": "Layout",
"information": "Information",
"statistics_title": "Statistics",
"board_settings": "Board Settings",
"filters_title": "Filters",
"filters": {
"md_ins_cos": "Insurance Companies",
"md_estimators": "Estimators"
},
"statistics": {
"total_hours_in_production": "Hours in Production",
"total_lab_in_production": "Body Hours in Production",
"total_lar_in_production": "Refinish Hours in Production",
"total_amount_in_production": "Dollars in Production",
"jobs_in_production": "Jobs in Production",
"total_hours_on_board": "Hours on Board",
"total_lab_on_board": "Body Hours on Board",
"total_lar_on_board": "Refinish Hours on Board",
"total_amount_on_board": "Dollars on Board",
"total_jobs_on_board": "Jobs on Board",
"tasks_in_production": "Tasks in Production",
"tasks_on_board": "Tasks on Board"
}
},
"actions": {
"addcolumns": "Add Columns",
"bodypriority-clear": "Clear Body Priority",
@@ -2777,27 +2757,23 @@
"suspend": "Suspend",
"unsuspend": "Unsuspend"
},
"constants": {
"main_profile": "Default"
},
"errors": {
"boardupdate": "Error encountered updating Job. {{message}}",
"name_exists": "A Profile with this name already exists. Please choose a different name.",
"name_required": "Profile name is required.",
"removing": "Error removing from production board. {{error}}",
"settings": "Error saving board settings: {{error}}"
},
"labels": {
"kiosk_mode": "Kiosk Mode",
"on": "On",
"off": "Off",
"wide": "Wide",
"tall": "Tall",
"vertical": "Vertical",
"horizontal": "Horizontal",
"orientation": "Board Orientation",
"card_size": "Card Size",
"model_info": "Vehicle Info",
"actual_in": "Actual In",
"addnewprofile": "Add New Profile",
"alert": "Alert",
"tasks": "Tasks",
"alertoff": "Remove alert from Job",
"alerton": "Add alert to Job",
"alerts": "Alerts",
"ats": "Alternative Transportation",
"bodyhours": "B",
"bodypriority": "B/P",
@@ -2807,6 +2783,7 @@
"qbo_usa": "QBO USA"
}
},
"card_size": "Card Size",
"cardcolor": "Colored Cards",
"cardsettings": "Card Settings",
"clm_no": "Claim Number",
@@ -2815,47 +2792,88 @@
"detailpriority": "D/P",
"employeeassignments": "Employee Assignments",
"employeesearch": "Employee Search",
"estimator": "Estimator",
"horizontal": "Horizontal",
"ins_co_nm": "Insurance Company Name",
"jobdetail": "Job Details",
"kiosk_mode": "Kiosk Mode",
"laborhrs": "Labor Hours",
"legend": "Legend:",
"model_info": "Vehicle Info",
"note": "Production Note",
"off": "Off",
"on": "On",
"orientation": "Board Orientation",
"ownr_nm": "Customer Name",
"paintpriority": "P/P",
"partsstatus": "Parts Status",
"estimator": "Estimator",
"subtotal": "Subtotal",
"production_note": "Production Note",
"refinishhours": "R",
"scheduled_completion": "Scheduled Completion",
"selectview": "Select a View",
"stickyheader": "Sticky Header (BETA)",
"sublets": "Sublets",
"subtotal": "Subtotal",
"tall": "Tall",
"tasks": "Tasks",
"totalhours": "Total Hrs ",
"touchtime": "T/T",
"vertical": "Vertical",
"viewname": "View Name",
"alerts": "Alerts"
"wide": "Wide"
},
"options": {
"horizontal": "Horizontal",
"large": "Large",
"medium": "Medium",
"small": "Small",
"vertical": "Vertical"
},
"settings": {
"board_settings": "Board Settings",
"filters": {
"md_estimators": "Estimators",
"md_ins_cos": "Insurance Companies"
},
"filters_title": "Filters",
"information": "Information",
"layout": "Layout",
"statistics": {
"jobs_in_production": "Jobs in Production",
"tasks_in_production": "Tasks in Production",
"tasks_on_board": "Tasks on Board",
"total_amount_in_production": "Dollars in Production",
"total_amount_on_board": "Dollars on Board",
"total_hours_in_production": "Hours in Production",
"total_hours_on_board": "Hours on Board",
"total_jobs_on_board": "Jobs on Board",
"total_lab_in_production": "Body Hours in Production",
"total_lab_on_board": "Body Hours on Board",
"total_lar_in_production": "Refinish Hours in Production",
"total_lar_on_board": "Refinish Hours on Board"
},
"statistics_title": "Statistics"
},
"statistics": {
"currency_symbol": "$",
"hours": "Hours",
"jobs": "Jobs",
"jobs_in_production": "Jobs in Production",
"tasks": "Tasks",
"tasks_in_production": "Tasks in Production",
"tasks_on_board": "Tasks on Board",
"total_amount_in_production": "Dollars in Production",
"total_amount_on_board": "Dollars on Board",
"total_hours_in_production": "Hours in Production",
"total_hours_on_board": "Hours on Board",
"total_jobs_on_board": "Jobs on Board",
"total_lab_in_production": "Body Hours in Production",
"total_lab_on_board": "Body Hours on Board",
"total_lar_in_production": "Refinish Hours in Production",
"total_lar_on_board": "Refinish Hours on Board"
},
"successes": {
"removed": "Job removed from production."
},
"statistics": {
"total_hours_in_production": "Hours in Production",
"total_lab_in_production": "Body Hours in Production",
"total_lar_in_production": "Refinish Hours in Production",
"total_amount_in_production": "Dollars in Production",
"jobs_in_production": "Jobs in Production",
"total_hours_on_board": "Hours on Board",
"total_lab_on_board": "Body Hours on Board",
"total_lar_on_board": "Refinish Hours on Board",
"total_amount_on_board": "Dollars on Board",
"total_jobs_on_board": "Jobs on Board",
"tasks_in_production": "Tasks in Production",
"tasks_on_board": "Tasks on Board",
"tasks": "Tasks",
"hours": "Hours",
"currency_symbol": "$",
"jobs": "Jobs"
}
},
"profile": {
@@ -2913,6 +2931,8 @@
"vendor": "Vendor"
},
"templates": {
"adp_payroll_flat": "ADP Payroll - Flat Rate",
"adp_payroll_straight": "ADP Payroll - Straight Time",
"anticipated_revenue": "Anticipated Revenue",
"ar_aging": "AR Aging",
"attendance_detail": "Attendance (All Employees)",
@@ -3404,6 +3424,18 @@
"vehicledetail": "Vehicle Details {{vehicle}} | {{app}}",
"vehicles": "All Vehicles | {{app}}"
},
"trello": {
"labels": {
"add_card": "Add Card",
"add_lane": "Add Lane",
"cancel": "Cancel",
"delete_lane": "Delete Lane",
"description": "Description",
"label": "Label",
"lane_actions": "Lane Actions",
"title": "Title"
}
},
"tt_approvals": {
"actions": {
"approveselected": "Approve Selected"
@@ -3542,18 +3574,6 @@
"validation": {
"unique_vendor_name": "You must enter a unique vendor name."
}
},
"trello": {
"labels": {
"add_card": "Add Card",
"add_lane": "Add Lane",
"delete_lane": "Delete Lane",
"lane_actions": "Lane Actions",
"title": "Title",
"description": "Description",
"label": "Label",
"cancel": "Cancel"
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -270,6 +270,7 @@
"testrender": ""
},
"errors": {
"creatingdefaultview": "",
"loading": "Impossible de charger les détails de la boutique. Veuillez appeler le support technique.",
"saving": ""
},
@@ -284,19 +285,21 @@
},
"appt_length": "",
"attach_pdf_to_email": "",
"batchid": "",
"bill_allow_post_to_closed": "",
"bill_federal_tax_rate": "",
"bill_local_tax_rate": "",
"bill_state_tax_rate": "",
"city": "",
"closingperiod": "",
"companycode": "",
"country": "",
"dailybodytarget": "",
"dailypainttarget": "",
"default_adjustment_rate": "",
"deliver": {
"templates": "",
"require_actual_delivery_date": ""
"require_actual_delivery_date": "",
"templates": ""
},
"dms": {
"apcontrol": "",
@@ -699,7 +702,10 @@
"workingdays": ""
},
"successes": {
"save": ""
"areyousure": "",
"defaultviewcreated": "",
"save": "",
"unsavedchanges": ""
},
"validation": {
"centermustexist": "",
@@ -1129,8 +1135,8 @@
},
"general": {
"actions": {
"defaults": "",
"add": "",
"autoupdate": "",
"calculate": "",
"cancel": "",
"clear": "",
@@ -1138,6 +1144,8 @@
"copied": "",
"copylink": "",
"create": "",
"defaults": "",
"delay": "",
"delete": "Effacer",
"deleteall": "",
"deselectall": "",
@@ -1149,10 +1157,12 @@
"print": "",
"refresh": "",
"remove": "",
"remove_alert": "",
"reset": " Rétablir l'original.",
"resetpassword": "",
"save": "sauvegarder",
"saveandnew": "",
"saveas": "",
"selectall": "",
"send": "",
"sendbysms": "",
@@ -1160,8 +1170,7 @@
"submit": "",
"tryagain": "",
"view": "",
"viewreleasenotes": "",
"remove_alert": ""
"viewreleasenotes": ""
},
"errors": {
"fcm": "",
@@ -1183,6 +1192,8 @@
"clear": "",
"confirmpassword": "",
"created_at": "",
"date": "",
"datetime": "",
"email": "",
"errors": "",
"excel": "",
@@ -1242,6 +1253,7 @@
"tuesday": "",
"tvmode": "",
"unknown": "Inconnu",
"unsavedchanges": "",
"username": "",
"view": "",
"wednesday": "",
@@ -2731,38 +2743,6 @@
}
},
"production": {
"options": {
"small": "",
"medium": "",
"large": "",
"vertical": "",
"horizontal": ""
},
"settings": {
"layout": "",
"information": "",
"statistics_title": "",
"board_settings": "",
"filters_title": "",
"filters": {
"md_ins_cos": "",
"md_estimators": ""
},
"statistics": {
"total_hours_in_production": "",
"total_lab_in_production": "",
"total_lar_in_production": "",
"total_amount_in_production": "",
"jobs_in_production": "",
"total_hours_on_board": "",
"total_lab_on_board": "",
"total_lar_on_board": "",
"total_amount_on_board": "",
"total_jobs_on_board": "",
"tasks_in_production": "",
"tasks_on_board": ""
}
},
"actions": {
"addcolumns": "",
"bodypriority-clear": "",
@@ -2777,27 +2757,23 @@
"suspend": "",
"unsuspend": ""
},
"constants": {
"main_profile": ""
},
"errors": {
"boardupdate": "",
"name_exists": "",
"name_required": "",
"removing": "",
"settings": ""
},
"labels": {
"kiosk_mode": "",
"on": "",
"off": "",
"wide": "",
"tall": "",
"vertical": "",
"horizontal": "",
"orientation": "",
"card_size": "",
"model_info": "",
"actual_in": "",
"addnewprofile": "",
"alert": "",
"tasks": "",
"alertoff": "",
"alerton": "",
"alerts": "",
"ats": "",
"bodyhours": "",
"bodypriority": "",
@@ -2807,6 +2783,7 @@
"qbo_usa": ""
}
},
"card_size": "",
"cardcolor": "",
"cardsettings": "",
"clm_no": "",
@@ -2815,47 +2792,88 @@
"detailpriority": "",
"employeeassignments": "",
"employeesearch": "",
"estimator": "",
"horizontal": "",
"ins_co_nm": "",
"jobdetail": "",
"kiosk_mode": "",
"laborhrs": "",
"legend": "",
"model_info": "",
"note": "",
"off": "",
"on": "",
"orientation": "",
"ownr_nm": "",
"paintpriority": "",
"partsstatus": "",
"estimator": "",
"subtotal": "",
"production_note": "",
"refinishhours": "",
"scheduled_completion": "",
"selectview": "",
"stickyheader": "",
"sublets": "",
"subtotal": "",
"tall": "",
"tasks": "",
"totalhours": "",
"touchtime": "",
"vertical": "",
"viewname": "",
"alerts": ""
"wide": ""
},
"options": {
"horizontal": "",
"large": "",
"medium": "",
"small": "",
"vertical": ""
},
"settings": {
"board_settings": "",
"filters": {
"md_estimators": "",
"md_ins_cos": ""
},
"filters_title": "",
"information": "",
"layout": "",
"statistics": {
"jobs_in_production": "",
"tasks_in_production": "",
"tasks_on_board": "",
"total_amount_in_production": "",
"total_amount_on_board": "",
"total_hours_in_production": "",
"total_hours_on_board": "",
"total_jobs_on_board": "",
"total_lab_in_production": "",
"total_lab_on_board": "",
"total_lar_in_production": "",
"total_lar_on_board": ""
},
"statistics_title": ""
},
"statistics": {
"currency_symbol": "",
"hours": "",
"jobs": "",
"jobs_in_production": "",
"tasks": "",
"tasks_in_production": "",
"tasks_on_board": "",
"total_amount_in_production": "",
"total_amount_on_board": "",
"total_hours_in_production": "",
"total_hours_on_board": "",
"total_jobs_on_board": "",
"total_lab_in_production": "",
"total_lab_on_board": "",
"total_lar_in_production": "",
"total_lar_on_board": ""
},
"successes": {
"removed": ""
},
"statistics": {
"total_hours_in_production": "",
"total_lab_in_production": "",
"total_lar_in_production": "",
"total_amount_in_production": "",
"jobs_in_production": "",
"total_hours_on_board": "",
"total_lab_on_board": "",
"total_lar_on_board": "",
"total_amount_on_board": "",
"total_jobs_on_board": "",
"tasks_in_production": "",
"tasks_on_board": "",
"tasks": "",
"hours": "",
"currency_symbol": "",
"jobs": ""
}
},
"profile": {
@@ -2913,6 +2931,8 @@
"vendor": ""
},
"templates": {
"adp_payroll_flat": "",
"adp_payroll_straight": "",
"anticipated_revenue": "",
"ar_aging": "",
"attendance_detail": "",
@@ -3404,6 +3424,18 @@
"vehicledetail": "Détails du véhicule {{vehicle} | {{app}}",
"vehicles": "Tous les véhicules | {{app}}"
},
"trello": {
"labels": {
"add_card": "",
"add_lane": "",
"cancel": "",
"delete_lane": "",
"description": "",
"label": "",
"lane_actions": "",
"title": ""
}
},
"tt_approvals": {
"actions": {
"approveselected": ""
@@ -3542,18 +3574,6 @@
"validation": {
"unique_vendor_name": ""
}
},
"trello": {
"labels": {
"add_card": "",
"add_lane": "",
"delete_lane": "",
"lane_actions": "",
"title": "",
"description": "",
"label": "",
"cancel": ""
}
}
}
}

View File

@@ -2158,6 +2158,32 @@ export const TemplateList = (type, context) => {
field: i18n.t("tasks.fields.created_at")
},
group: "jobs"
},
adp_payroll_flat: {
title: i18n.t("reportcenter.templates.adp_payroll_flat"),
subject: i18n.t("reportcenter.templates.adp_payroll_flat"),
key: "adp_payroll_flat",
reporttype: "text",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.timetickets"),
field: i18n.t("timetickets.fields.committed_at")
},
group: "payroll",
adp_payroll: true
},
adp_payroll_straight: {
title: i18n.t("reportcenter.templates.adp_payroll_straight"),
subject: i18n.t("reportcenter.templates.adp_payroll_straight"),
key: "adp_payroll_straight",
reporttype: "text",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.timetickets"),
field: i18n.t("timetickets.fields.date")
},
group: "payroll",
adp_payroll: true
}
}
: {}),

View File

@@ -0,0 +1,84 @@
import React from "react";
const useCountDown = (timeToCount = 60 * 1000, interval = 1000) => {
const [timeLeft, setTimeLeft] = React.useState(0);
const timer = React.useRef({});
const run = (ts) => {
if (!timer.current.started) {
timer.current.started = ts;
timer.current.lastInterval = ts;
}
const localInterval = Math.min(interval, timer.current.timeLeft || Infinity);
if (ts - timer.current.lastInterval >= localInterval) {
timer.current.lastInterval += localInterval;
setTimeLeft((timeLeft) => {
timer.current.timeLeft = timeLeft - localInterval;
return timer.current.timeLeft;
});
}
if (ts - timer.current.started < timer.current.timeToCount) {
timer.current.requestId = window.requestAnimationFrame(run);
} else {
timer.current = {};
setTimeLeft(0);
}
};
const start = React.useCallback(
(ttc) => {
window.cancelAnimationFrame(timer.current.requestId);
const newTimeToCount = ttc !== undefined ? ttc : timeToCount;
timer.current.started = null;
timer.current.lastInterval = null;
timer.current.timeToCount = newTimeToCount;
timer.current.requestId = window.requestAnimationFrame(run);
setTimeLeft(newTimeToCount);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const pause = React.useCallback(() => {
window.cancelAnimationFrame(timer.current.requestId);
timer.current.started = null;
timer.current.lastInterval = null;
timer.current.timeToCount = timer.current.timeLeft;
}, []);
const resume = React.useCallback(
() => {
if (!timer.current.started && timer.current.timeLeft > 0) {
window.cancelAnimationFrame(timer.current.requestId);
timer.current.requestId = window.requestAnimationFrame(run);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const reset = React.useCallback(() => {
if (timer.current.timeLeft) {
window.cancelAnimationFrame(timer.current.requestId);
timer.current = {};
setTimeLeft(0);
}
}, []);
const actions = React.useMemo(
() => ({ start, pause, resume, reset }), // eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
React.useEffect(() => {
return () => window.cancelAnimationFrame(timer.current.requestId);
}, []);
return [timeLeft, actions];
};
export default useCountDown;

View File

@@ -1,47 +0,0 @@
export const BETA_KEY = "betaSwitchImex";
export const checkBeta = () => {
const cookie = document.cookie.split("; ").find((row) => row.startsWith(BETA_KEY));
return cookie ? cookie.split("=")[1] === "true" : false;
};
export const setBeta = (value) => {
const domain = window.location.hostname.split(".").slice(-2).join(".");
document.cookie = `${BETA_KEY}=${value}; path=/; domain=.${domain}`;
};
export const handleBeta = () => {
if (window.location.hostname.startsWith("localhost")) {
console.log("Not on beta or test, so no need to handle beta.");
return;
}
const isBeta = checkBeta();
const currentHostName = window.location.hostname;
// Determine if the host name starts with "beta" or "www.beta"
const isBetaHost = currentHostName.startsWith("beta.");
const isBetaHostWithWWW = currentHostName.startsWith("www.beta.");
if (isBeta) {
// If beta is on and we are not on a beta domain, redirect to the beta version
if (!isBetaHost && !isBetaHostWithWWW) {
const newHostName = currentHostName.startsWith("www.")
? `www.beta.${currentHostName.replace(/^www\./, "")}`
: `beta.${currentHostName}`;
const href = `${window.location.protocol}//${newHostName}${window.location.pathname}${window.location.search}${window.location.hash}`;
window.location.replace(href);
}
// Otherwise, if beta is on and we're already on a beta domain, stay there
} else {
// If beta is off and we are on a beta domain, redirect to the non-beta version
if (isBetaHost || isBetaHostWithWWW) {
const newHostName = currentHostName.replace(/^www\.beta\./, "www.").replace(/^beta\./, "");
const href = `${window.location.protocol}//${newHostName}${window.location.pathname}${window.location.search}${window.location.hash}`;
window.location.replace(href);
}
// Otherwise, if beta is off and we're not on a beta domain, stay there
}
};
export default handleBeta;

View File

@@ -1,5 +1,5 @@
version: 2
endpoint: https://db.dev.bodyshop.app
endpoint: https://db.dev.imex.online
admin_secret: Dev-BodyShopApp!
metadata_directory: metadata
actions:

View File

@@ -1,3 +1,27 @@
- name: AutoHouse Data Pump
webhook: '{{HASURA_API_URL}}/data/ah'
schedule: 0 6 * * *
include_in_metadata: true
payload: {}
headers:
- name: x-imex-auth
value_from_env: DATAPUMP_AUTH
- name: Claimscorp Data Pump
webhook: '{{HASURA_API_URL}}/data/cc'
schedule: 30 6 * * *
include_in_metadata: true
payload: {}
headers:
- name: x-imex-auth
value_from_env: DATAPUMP_AUTH
- name: Kaizen Data Pump
webhook: '{{HASURA_API_URL}}/data/kaizen'
schedule: 30 5 * * *
include_in_metadata: true
payload: {}
headers:
- name: x-imex-auth
value_from_env: DATAPUMP_AUTH
- name: Task Reminders
webhook: '{{HASURA_API_URL}}/tasks-remind-handler'
schedule: '*/15 * * * *'

View File

@@ -94,7 +94,10 @@ exports.default = async (req, res) => {
ret.push({
billid: bill.id,
success: false,
errorMessage: (error && error.authResponse && error.authResponse.body) || (error && error.message)
errorMessage:
(error && error.authResponse && error.authResponse.body) ||
error.response?.data?.Fault?.Error.map((e) => e.Detail).join(", ") ||
(error && error.message)
});
//Add the export log error.
@@ -209,14 +212,14 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop)
AccountBasedExpenseLineDetail: {
...(bill.job.class ? { ClassRef: { value: classes[bill.job.class] } } : {}),
AccountRef: {
value: accounts[bodyshop.md_responsibility_centers.taxes.federal.accountdesc]
value: accounts[bodyshop.md_responsibility_centers.taxes.federal_itc.accountdesc]
}
},
Amount: Dinero({
amount: Math.round(
bill.billlines.reduce((acc, val) => {
return acc + val.actual_cost * val.quantity;
return acc + val.applicable_taxes?.federal ? (val.actual_cost * val.quantity ?? 0) : 0;
}, 0) * 100
)
})
@@ -274,6 +277,8 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop)
} catch (error) {
logger.log("qbo-payables-error", "DEBUG", req.user.email, bill.id, {
error: error, //(error && error.authResponse && error.authResponse.body) || (error && error.message),
validationError: JSON.stringify(error?.response?.data),
accountmeta: JSON.stringify({ accounts, taxCodes, classes }),
method: "InsertBill"
});
throw error;

View File

@@ -179,7 +179,11 @@ exports.default = async (req, res) => {
ret.push({
jobid: job.id,
success: false,
errorMessage: (error && error.authResponse && error.authResponse.body) || (error && error.message)
errorMessage:
error?.authResponse?.body ||
error?.response?.data?.Fault?.Error.map((e) => e.Detail).join(", ") ||
error?.response?.data ||
error?.message
});
console.log(error);
logger.log("qbo-receivable-create-error", "ERROR", req.user.email, {
@@ -254,7 +258,6 @@ async function InsertInsuranceCo(oauthClient, qbo_realmId, req, job, bodyshop) {
throw new Error(
`Insurance Company '${job.ins_co_nm}' not found in shop configuration. Please make sure it exists or change the insurance company name on the job to one that exists.`
);
return;
}
const Customer = {
DisplayName: job.ins_co_nm.trim(),
@@ -575,7 +578,9 @@ async function InsertInvoice(oauthClient, qbo_realmId, req, job, bodyshop, paren
} catch (error) {
logger.log("qbo-receivables-error", "DEBUG", req.user.email, job.id, {
error,
method: "InsertOwner"
method: "InsertInvoice",
validationError: JSON.stringify(error?.response?.data),
accountmeta: JSON.stringify({ items, taxCodes, classes })
});
throw error;
}

View File

@@ -31,6 +31,12 @@ const ftpSetup = {
};
exports.default = async (req, res) => {
// Only process if in production environment.
if (process.env.NODE_ENV !== "production") {
res.sendStatus(403);
return;
}
//Query for the List of Bodyshop Clients.
logger.log("autohouse-start", "DEBUG", "api", null, null);
const { bodyshops } = await client.request(queries.GET_AUTOHOUSE_SHOPS);

View File

@@ -31,6 +31,12 @@ const ftpSetup = {
};
exports.default = async (req, res) => {
// Only process if in production environment.
if (process.env.NODE_ENV !== "production") {
res.sendStatus(403);
return;
}
//Query for the List of Bodyshop Clients.
logger.log("claimscorp-start", "DEBUG", "api", null, null);
const { bodyshops } = await client.request(queries.GET_CLAIMSCORP_SHOPS);

View File

@@ -31,6 +31,12 @@ const ftpSetup = {
};
exports.default = async (req, res) => {
// Only process if in production environment.
if (process.env.NODE_ENV !== "production") {
res.sendStatus(403);
return;
}
//Query for the List of Bodyshop Clients.
logger.log("kaizen-start", "DEBUG", "api", null, null);
const kaizenShopsIDs = ["SUMMIT", "STRATHMORE", "SUNRIDGE", "SHAW"];
@@ -56,8 +62,8 @@ exports.default = async (req, res) => {
try {
const { jobs, bodyshops_by_pk } = await client.request(queries.KAIZEN_QUERY, {
bodyshopid: bodyshop.id,
start: start ? moment(start).startOf("hours") : moment().subtract(2, "hours").startOf("hour"),
...(end && { end: moment(end).endOf("hours") })
start: start ? moment(start).startOf("day") : moment().subtract(5, "days").startOf("day"),
...(end && { end: moment(end).endOf("day") })
});
const kaizenObject = {
@@ -176,24 +182,19 @@ exports.default = async (req, res) => {
} finally {
sftp.end();
}
// sendServerEmail({
// subject: `Kaizen Report ${moment().format("MM-DD-YY")}`,
// text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
// Uploaded: ${JSON.stringify(
// allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
// null,
// 2
// )}
// `,
// });
sendServerEmail({
subject: `Kaizen Report ${moment().format("MM-DD-YY")}`,
text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
Uploaded: ${JSON.stringify(
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
null,
2
)}
`
});
res.sendStatus(200);
} catch (error) {
res.status(200).json(error);
sendServerEmail({
subject: `Kaizen Report ${moment().format("MM-DD-YY @ HH:mm:ss")}`,
text: `Errors: JSON.stringify(error)}
All Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}`
});
}
};

View File

@@ -965,22 +965,17 @@ function CalculateTaxesTotals(job, otherTotals) {
}
});
if (job.adjustment_bottom_line) {
const subtotal_before_adjustment = subtotal.add(Dinero({ amount: Math.round(job.adjustment_bottom_line * -100) }));
const percent_of_adjustment =
Math.round(
subtotal_before_adjustment.toUnit() /
(job.adjustment_bottom_line > 0 ? job.adjustment_bottom_line : job.adjustment_bottom_line * -1)
) / 100;
Object.keys(taxableAmountsByTier).forEach((taxTierKey) => {
taxable_adjustment = taxableAmountsByTier[taxTierKey].multiply(percent_of_adjustment);
if (job.adjustment_bottom_line > 0) {
taxableAmountsByTier[taxTierKey] = taxableAmountsByTier[taxTierKey].add(taxable_adjustment);
} else {
taxableAmountsByTier[taxTierKey] = taxableAmountsByTier[taxTierKey].subtract(taxable_adjustment);
if (job.adjustment_bottom_line && job.adjustment_bottom_line !== 0) {
for (let tyCounter = 1; tyCounter <= 5; tyCounter++) {
if (IsTrueOrYes(pfp["PAN"][`prt_tx_in${tyCounter}`])) {
//This amount is taxable for this type.
taxableAmountsByTier[`ty${tyCounter}Tax`] = taxableAmountsByTier[`ty${tyCounter}Tax`].add(
Dinero({
amount: Math.round(job.adjustment_bottom_line * 100)
})
);
}
});
}
}
const remainingTaxableAmounts = taxableAmountsByTier;