Compare commits

...

273 Commits

Author SHA1 Message Date
Patrick Fic
d52426f5f5 IO-3239 Additional logging fixes. 2025-05-28 15:21:42 -07:00
Patrick Fic
2c508cf1a1 IO-3239 QBO Logging and integration log schema changes. 2025-05-22 11:54:17 -07:00
Patrick Fic
16a91c772a Merged in release/2025-06-02 (pull request #2330)
Release/2025 06 02
2025-05-22 16:46:27 +00:00
Dave Richer
5c47088b11 release/2025-06-02 - Lint Updates 2025-05-22 11:37:47 -04:00
Allan Carr
8e5dc4fa71 Merged in feature/IO-3243-Job-Costing-TOW (pull request #2327)
IO-3243 Job Costing TOW

Approved-by: Dave Richer
2025-05-22 15:13:34 +00:00
Allan Carr
39c3729f6d Merged in feature/IO-3230-Customer-List-Excel (pull request #2326)
IO-3230 Customer List Excel

Approved-by: Dave Richer
2025-05-22 15:13:03 +00:00
Allan Carr
e3d854e02b IO-3243 Job Costing TOW
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2025-05-21 17:53:33 -07:00
Allan Carr
618acf2acf IO-3230 Customer List Excel
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-05-21 16:28:09 -07:00
Dave Richer
2cf2b70293 Merged in feature/IO-3182-Phone-Number-Consent (pull request #2325)
Feature/IO-3182 Phone Number Consent
2025-05-21 19:17:52 +00:00
Dave Richer
0541afceb8 feature/IO-3182-Phone-Number-Consent - Checkpoint 2025-05-21 15:17:11 -04:00
Dave Richer
28ed3f9936 Merged in feature/IO-3182-Phone-Number-Consent (pull request #2324)
DO NOT MERGE JUST USING TO UNDO
2025-05-21 19:03:58 +00:00
Dave Richer
6afa50332b feature/IO-3182-Phone-Number-Consent - Checkpoint 2025-05-21 15:03:02 -04:00
Dave Richer
8c8c68867d feature/IO-3182-Phone-Number-Consent - Checkpoint 2025-05-21 14:39:17 -04:00
Dave Richer
8ee52598e8 feature/IO-3182-Phone-Number-Consent - Checkpoint 2025-05-21 14:32:35 -04:00
Allan Carr
c822028174 Merged in feature/IO-3235-FeatureAccess-VisualBoard-Color (pull request #2322)
IO-3235 FeatureAccess on VisualBoard for SmartSchedule Option of Color Cards

Approved-by: Dave Richer
2025-05-21 18:06:04 +00:00
Allan Carr
36b82c6195 Merged in feature/IO-3236-HasFeatureAccess-Date (pull request #2323)
IO-3236 HasFeatureAccess Date

Approved-by: Dave Richer
2025-05-21 18:05:23 +00:00
Allan Carr
079dffce4d IO-3236 HasFeatureAccess Date
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-05-20 17:16:27 -07:00
Allan Carr
831802f5af IO-3235 FeatureAccess on VisualBoard for SmartSchedule Option of Color Cards
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-05-20 15:25:29 -07:00
Dave Richer
7bd5190bf2 feature/IO-3182-Phone-Number-Consent - Checkpoint 2025-05-20 18:19:39 -04:00
Dave Richer
83860152a9 feature/IO-3182-Phone-Number-Consent - Checkpoint 2025-05-20 16:04:36 -04:00
Dave Richer
1e10493615 Merged in feature/IO-3182-Phone-Number-Consent (pull request #2321)
Feature/IO-3182 Phone Number Consent
2025-05-20 17:45:24 +00:00
Dave Richer
9d81c68a4d feature/IO-3182-Phone-Number-Consent - Checkpoint 2025-05-20 13:14:05 -04:00
Dave Richer
985d066978 feature/IO-3182-Phone-Number-Consent - Finish Database changes 2025-05-20 12:49:32 -04:00
Dave Richer
6ad9e27d1d feature/IO-3182-Phone-Number-Consent - Merge master / bump deps 2025-05-20 12:38:31 -04:00
Dave Richer
19ebdda5b3 Merge remote-tracking branch 'origin/master-AIO' into feature/IO-3182-Phone-Number-Consent 2025-05-20 12:30:15 -04:00
Allan Carr
4602dd1183 Merged in master-AIO (pull request #2320)
IO-3217 OTSL Labor Type
2025-05-19 21:03:00 +00:00
Allan Carr
6005eaee6a Merged in feature/IO-3217-OTSL-Labor-Type (pull request #2319)
IO-3217 OTSL Labor Type
2025-05-19 21:02:01 +00:00
Patrick Fic
6d59e3994f Merged in feature/IO-3105-qbo-version-update (pull request #2318)
feature/IO-3105-qbo-version-update

Approved-by: Patrick Fic
2025-05-19 19:30:34 +00:00
Patrick Fic
f770b2f1b1 IO-3105 Add QBO Minor Version. 2025-05-19 12:28:50 -07:00
Patrick Fic
b014744940 IO-3239 Add integration log statements on QBO. 2025-05-19 11:10:02 -07:00
Dave Richer
714c90c25e Merge remote-tracking branch 'origin/master-AIO' into feature/IO-3182-Phone-Number-Consent 2025-05-15 18:55:23 -04:00
Dave Richer
9a3a971da6 Clear Stage 2025-05-15 18:55:20 -04:00
Dave Richer
96cba0aaab Clear Stage 2025-05-15 18:54:55 -04:00
Patrick Fic
c069600cfd Merged in hotfix/2025-05-15 (pull request #2317)
Hotfix/2025 05 15 IO-3217 IO-3066 IO-3210 IO-2328
2025-05-15 22:47:26 +00:00
Patrick Fic
186cbf2c97 Merge branch 'feature/IO-3066-ems-upload' into hotfix/2025-05-15 2025-05-15 15:47:04 -07:00
Patrick Fic
392988ae11 Io-3066 resolve typo. 2025-05-15 15:46:45 -07:00
Allan Carr
2e33b79eb9 Merged in feature/IO-3210-Podium-Datapump (pull request #2315)
IO-3210 Podium Datapump

Approved-by: Patrick Fic
2025-05-15 22:39:39 +00:00
Allan Carr
d4f718c44c Merged in feature/IO-3210-Podium-Datapump (pull request #2314)
IO-3210 Podium Datapump

Approved-by: Patrick Fic
2025-05-15 22:39:28 +00:00
Patrick Fic
fa99ef7b37 Merge branch 'feature/IO-3066-ems-upload' into hotfix/2025-05-15 2025-05-15 15:36:44 -07:00
Patrick Fic
c4aff1b516 Merge branch 'feature/IO-2328-intellipay-querystring-package' into hotfix/2025-05-15 2025-05-15 15:36:31 -07:00
Patrick Fic
61276bb2d1 IO-2328 change querystring versions. 2025-05-15 15:35:56 -07:00
Allan Carr
8b89e2eb9d IO-3210 Podium Datapump
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-05-15 15:27:59 -07:00
Patrick Fic
9ab41308e7 IO-3066 add EMS upload functionality. 2025-05-15 12:53:41 -07:00
Allan Carr
f76052ec9b Merge branch 'master-AIO' into feature/IO-3210-Podium-Datapump
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-05-15 12:41:09 -07:00
Patrick Fic
b8841e3ded Merged in release/2025-05-09 (pull request #2312)
IO-3190 Add nulll coalesce and check for non-intake events.

Approved-by: Patrick Fic
2025-05-15 00:57:44 +00:00
Patrick Fic
a49b3f6496 IO-3190 Add nulll coalesce and check for non-intake events. 2025-05-14 17:56:15 -07:00
Dave Richer
3e17ec3cf8 Merged in release/2025-05-09 (pull request #2310)
[DO NOT MERGE ] Release/2025-05-09 into master-AIO
2025-05-14 20:10:07 +00:00
Dave Richer
76c0c7c41e release/2025-05-09 - Bump Deps 2025-05-13 10:43:15 -04:00
Dave Richer
025b986f60 Merged in feature/IO-3228-Notifications-1.6-and-Deprecations (pull request #2308)
feature/IO-3228-Notifications-1.6-and-Deprecations
2025-05-09 14:39:59 +00:00
Dave Richer
6e6addd62f feature/IO-3228-Notifications-1.6-and-Deprecations
- See Ticket for full details (Notifications restrictions, AntD deprecations)
2025-05-09 10:38:19 -04:00
Dave Richer
266c3acf34 Merged in feature/IO-3214-Job-Status-Card-Extension (pull request #2306)
feature/IO-3214-Job-Status-Card-Extension - PR Notes/Package Updates
2025-05-08 15:27:53 +00:00
Dave Richer
c4631f50e5 feature/IO-3214-Job-Status-Card-Extension - PR Notes/Package Updates 2025-05-08 11:25:44 -04:00
Dave Richer
ca18291425 Merged in feature/IO-3214-Job-Status-Card-Extension (pull request #2304)
feature/IO-3214-Job-Status-Card-Extension - Complete
2025-05-06 21:10:52 +00:00
Dave Richer
110fad2abc feature/IO-3214-Job-Status-Card-Extension - Complete 2025-05-06 17:08:46 -04:00
Dave Richer
b7456cecd4 release/2025-05-09 - Restore GraphQL Dep 2025-05-06 14:55:18 -04:00
Dave Richer
84db1fe81b Merged in feature/IO-3225-Notifications-1.5 (pull request #2300)
Feature/IO-3225 Notifications 1.5 into Release/2025-05-09
2025-05-06 18:20:20 +00:00
Dave Richer
b539111be8 feature/IO-3225-Notifications-1.5: Final Refactoring / Optimization 2025-05-06 14:19:22 -04:00
Dave Richer
8a8bc5a6ed feature/IO-3225-Notifications-1.5: Final Refactoring / Optimization 2025-05-06 13:53:40 -04:00
Dave Richer
020db91105 feature/IO-3225-Notifications-1.5: checkpoint 2025-05-06 13:38:42 -04:00
Dave Richer
1dd28af752 feature/IO-3225-Notifications-1.5: Package Updates / Maintenance 2025-05-06 12:58:21 -04:00
Dave Richer
5ba192eee0 feature/IO-3225-Notifications-1.5: Finish 2025-05-05 17:06:23 -04:00
Dave Richer
8109a12898 feature/IO-3225-Notifications-1.5: DB Changes 2025-05-05 15:02:44 -04:00
Dave Richer
2deb7fd520 feature/IO-3225-Notifications-1.5: DB Changes 2025-05-05 13:57:25 -04:00
Dave Richer
f6cd136679 feature/IO-3225-Notifications-1.5: Packages 2025-05-05 13:16:57 -04:00
Dave Richer
e50cb86296 release/2025-04-25 - Update DD, trying again 2025-05-02 13:45:14 -04:00
Dave Richer
a5a01c44fa release/2025-04-25 - Remove DD 2025-04-30 14:07:01 -04:00
Dave Richer
947e0705e4 release/2025-04-25 - Update/Restore DD for test purposes.4 2025-04-30 11:37:21 -04:00
Dave Richer
aa8a6a837d release/2025-04-25: remove body-parser reference from pay-all.js 2025-04-30 11:30:25 -04:00
Dave Richer
5db440fc9c release/2025-04-25: remove body-parser reference from misc routes 2025-04-30 11:28:15 -04:00
Dave Richer
c299b9376a release/2025-04-25: revert body parser, remove DD 2025-04-30 10:26:38 -04:00
Dave Richer
e5d530ea3e release/2025-04-25: Update Backend Packages 2025-04-28 17:15:13 -04:00
Dave Richer
6da9850946 release/2025-04-25: Update Sentry 2025-04-28 13:03:02 -04:00
Dave Richer
f62609f60c release/2025-04-25: revert body parser to discrete package 2025-04-28 12:58:27 -04:00
Dave Richer
b2d8c66e5b release/2025-04-25: clean 2025-04-28 12:17:40 -04:00
Dave Richer
3c4ed3ba0c release/2025-04-25: Patch Updates to packages 2025-04-26 12:16:46 -04:00
Dave Richer
2e7f827c3f release/2025-04-25: Patch Updates to packages 2025-04-26 11:18:39 -04:00
Patrick Fic
dc82b39dc8 Remove crisp status reporter. 2025-04-25 20:27:07 -07:00
Dave Richer
a9814c1eb1 Merged in release/2025-04-25 (pull request #2287)
Release/2025-04-25 into Master-AIO -  IO-2282, IO-3066, IO-3164, IO-3187, IO-3190, IO-3200, IO-3210, IO-3212, IO-3213, IO-3215, IO-3220, IO-3223
2025-04-26 00:43:22 +00:00
Patrick Fic
bdb741caf8 Merged in feature/IO-3223-canny (pull request #2288)
IO-3223 Add Canny for feature request and change log.

Approved-by: Dave Richer
2025-04-25 21:12:43 +00:00
Dave Richer
f50b198c21 feature/IO-3223-canny - Small syntactic update 2025-04-25 17:12:18 -04:00
Dave Richer
3495326de3 feature/IO-3223-canny - Merge release / fix conflicts 2025-04-25 17:06:44 -04:00
Patrick Fic
b5973085e7 IO-3223 Add Canny for feature request and change log. 2025-04-25 14:02:40 -07:00
Dave Richer
8687214420 release/2025-04-25 - update handleInvoiceBasedPayment.test.js 2025-04-25 11:58:47 -04:00
Dave Richer
d61b89a1e5 release/2025-04-25 - Add logging around handleInvoiceBasePayment paymentResponse, toned logs down. fixed issue in paymentResponseResults 2025-04-25 11:54:36 -04:00
Allan Carr
468b42abd2 Merged in feature/IO-3220-VPB-Popup (pull request #2284)
IO-3220 VPB Board Settings Popup

Approved-by: Dave Richer
2025-04-25 14:59:13 +00:00
Allan Carr
fc03e5f983 IO-3220 VPB Board Settings Popup
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-04-24 13:32:27 -07:00
Allan Carr
c4742e38ea Merged in feature/IO-3213-Hit-and-Run-Toggle (pull request #2280)
IO-3213 Hit and Run Toggle

Approved-by: Dave Richer
2025-04-24 15:41:41 +00:00
Allan Carr
99e1adbe13 Merge branch 'release/2025-04-25' into feature/IO-3213-Hit-and-Run-Toggle
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>

# Conflicts:
#	client/src/components/jobs-detail-general/jobs-detail-general.component.jsx
2025-04-24 08:42:32 -07:00
Allan Carr
eb5c797a43 Merged in feature/IO-3215-Employee-Assignment-Timeticket-Modal (pull request #2279)
IO-3215 Employee Assignment Timeticket Modal

Approved-by: Dave Richer
2025-04-24 15:34:42 +00:00
Allan Carr
0595c5545e Merged in feature/IO-3212-ACV-Amount (pull request #2281)
IO-3212 ACV Amount

Approved-by: Dave Richer
2025-04-24 15:33:41 +00:00
Allan Carr
12c87ed689 IO-3217 OTSL Labor Type
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-04-23 18:48:35 -07:00
Allan Carr
55944257aa IO-3212 ACV Amount
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-04-23 13:16:49 -07:00
Allan Carr
03241778fa IO-3212 ACV Amount
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-04-23 13:10:42 -07:00
Allan Carr
555b81fb14 IO-3213 Hit and Run Toggle
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-04-23 12:04:29 -07:00
Allan Carr
a56b720e09 IO-3215 Employee Assignment Timeticket Modal
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-04-23 09:51:44 -07:00
Allan Carr
b89eede164 Merged in feature/IO-3164-Schedule-Completion-Business-Days (pull request #2277)
IO-3164 Schedule Completion Business Days

Approved-by: Dave Richer
2025-04-22 13:48:43 +00:00
Allan Carr
c21cc8d6b9 IO-3164 Schedule Completion Business Days
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-04-21 12:27:33 -07:00
Dave Richer
d02a6bc197 Merged in feature/IO-2282-VSSTA-Integration (pull request #2270)
[DO NOT MERGE] Feature/IO-2282 VSSTA Integration into release/2025-04-25
2025-04-21 17:00:07 +00:00
Dave Richer
360c1ce82d feature/IO-2282-VSSTA-Integration 2025-04-21 12:57:00 -04:00
Allan Carr
a7ef02976c Merged in feature/IO-3200-Extended-Crisp-Segments (pull request #2276)
IO-3200 Extended Crisp Segments for BASIC/LITE

Approved-by: Dave Richer
2025-04-21 16:54:31 +00:00
Allan Carr
6a9e36ea4d IO-3200 Extended Crisp Segments for BASIC/LITE
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-04-17 16:20:34 -07:00
Allan Carr
37d4c0a40f IO-3210 Podium Datapump CRON Trigger
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-04-17 12:42:19 -07:00
Allan Carr
5ebca3ff06 Merged in feature/IO-3210-Podium-Datapump (pull request #2275)
IO-3210 Podium Datapump CRON Trigger
2025-04-17 19:42:05 +00:00
Allan Carr
1969a92226 Merged in feature/IO-3210-Podium-Datapump (pull request #2273)
IO-3210 Podium Datapump

Approved-by: Dave Richer
2025-04-17 17:40:59 +00:00
Allan Carr
8840ffc9ba IO-3210 Product Fruits Insurance Company Add Button ID
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-04-17 10:03:08 -07:00
Allan Carr
19e42ef397 IO-3210 Podium Datapump
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-04-16 17:52:22 -07:00
Allan Carr
c7eb026986 Merged in feature/IO-3190-Quick-Intake-Schedule-Event (pull request #2271)
IO-3190 Quick Intake Schedule Event

Approved-by: Dave Richer
2025-04-16 20:16:41 +00:00
Allan Carr
b0dcd3618e IO-3190 Quick Intake Schedule Event
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-04-16 13:00:01 -07:00
Allan Carr
5f23f135f2 Merged in feature/IO-3187-Admin-Enhancements (pull request #2269)
IO-3187 Admin Enhancements

Approved-by: Dave Richer
2025-04-16 19:10:03 +00:00
Allan Carr
159ee7364d IO-3187 Admin Enhancements
add BCC

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-04-15 22:08:21 -07:00
Dave Richer
aa6ad109c9 feature/IO-3187-Admin-Enhancements - Minor cleanup 2025-04-15 14:21:28 -04:00
Dave Richer
f2a896d568 feature/IO-3187-Admin-Enhancements - Minor cleanup 2025-04-15 14:02:29 -04:00
Dave Richer
546ebba0bd feature/IO-3187-Admin-Enhancements - Minor cleanup 2025-04-15 13:57:50 -04:00
Dave Richer
0e75f54d6e feature/IO-2282-VSSTA-Integration: - doc blocks / cleanup 2025-04-15 13:39:34 -04:00
Dave Richer
30f34a17ea feature/IO-2282-VSSTA-Integration: - doc blocks / cleanup 2025-04-15 13:20:07 -04:00
Dave Richer
6035d94404 feature/IO-2282-VSSTA-Integration: - doc blocks / cleanup 2025-04-15 13:05:42 -04:00
Dave Richer
0b7a23d555 feature/IO-2282-VSSTA-Integration: - include some tests for media utils 2025-04-15 13:02:54 -04:00
Dave Richer
91fe1f4af9 feature/IO-2282-VSSTA-Integration: - Finish Integration 2025-04-15 12:55:38 -04:00
Dave Richer
f09cb7b247 feature/IO-2282-VSSTA-Integration: - Finish Integration 2025-04-15 12:40:33 -04:00
Dave Richer
35a7222f5e feature/IO-2282-VSSTA-Integration: - checkpoint 2025-04-15 11:29:44 -04:00
Dave Richer
d444821cf7 feature/IO-2282-VSSTA-Integration: - checkpoint 2025-04-15 10:46:49 -04:00
Allan Carr
b5cb520944 IO-3187 Admin Enhancements
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-04-14 17:07:57 -07:00
Allan Carr
6814a3bc33 Merge branch 'master-AIO' into feature/IO-3187-Admin-Enhancements
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-04-14 16:57:20 -07:00
Patrick Fic
19c2b19abc Merged in feature/IO-3066-partner-refresh (pull request #2268)
IO-3066 Call partner refresh on shop change.
2025-04-14 22:32:44 +00:00
Patrick Fic
22b011139d IO-3066 Call partner refresh on shop change. 2025-04-14 15:29:45 -07:00
Dave Richer
5b30daefe5 Merge remote-tracking branch 'origin/master-AIO' into feature/IO-2282-VSSTA-Integration 2025-04-14 11:03:29 -04:00
Dave Richer
e015d3574a Merged in release/2025-04-11 (pull request #2267)
Release/2025-04-11 into master-AIO - IO-2769, IO-2885, IO-3045, IO-3066, IO-3193, IO-3198, IO-3202
2025-04-12 01:27:08 +00:00
Dave Richer
60140902d4 Merged in feature/IO-3045-Product-Fruits-Tours-Modifications (pull request #2265)
feature/IO-3045-Product-Fruits-Tours-Modifications - Add in roles prop on Product fruits, tie the usage of it to a new db bodyshop var tours_enabled.
2025-04-10 16:28:21 +00:00
Dave Richer
84f41b2c11 feature/IO-3045-Product-Fruits-Tours-Modifications - Add in roles prop on Product fruits, tie the usage of it to a new db bodyshop var tours_enabled. 2025-04-10 12:27:54 -04:00
Dave Richer
e8b9fcbc6e feature/IO-2282-VSSTA-Integration:
- Clean up imgproxy-media.js
2025-04-10 09:37:31 -04:00
Dave Richer
5adf591670 feature/IO-2282-VSSTA-Integration:
- Clean up imgproxy-media.js
2025-04-10 09:27:49 -04:00
Dave Richer
f55764e859 feature/IO-2282-VSSTA-Integration:
- Boilerplate in new route
- Fix issues with imgproxy
- Clean up imgproxy
2025-04-09 14:56:49 -04:00
Dave Richer
282fa787a9 Merged in feature/IO-2769-Job-Totals-Testing (pull request #2263)
feature/IO-2769-Job-Totals-Testing - packages, final update
2025-04-09 16:53:04 +00:00
Dave Richer
037efff81c feature/IO-2769-Job-Totals-Testing - packages, final update 2025-04-09 12:52:23 -04:00
Patrick Fic
e26eb17d09 Merged in feature/IO-3066-ems-parts-order-data (pull request #2262)
IO-3066 Add additional CIECA fields for new partner

Approved-by: Patrick Fic
2025-04-09 16:34:42 +00:00
Patrick Fic
fbea9fde27 IO-3066 Add additional CIECA fields for new partner 2025-04-09 09:33:37 -07:00
Dave Richer
ce7cf6bdbe Merged in feature/IO-2769-Job-Totals-Testing (pull request #2260)
feature/IO-2769-Job-Totals-Testing - updated tests
2025-04-09 15:19:09 +00:00
Dave Richer
2c47e5d852 feature/IO-2769-Job-Totals-Testing - updated tests 2025-04-09 11:18:09 -04:00
Dave Richer
a6f809b20a Merged in feature/IO-2769-Job-Totals-Testing (pull request #2258)
feature/IO-2769-Job-Totals-Testing - non-related
2025-04-08 22:14:52 +00:00
Dave Richer
2bcad68351 feature/IO-2769-Job-Totals-Testing - non-related 2025-04-08 18:14:20 -04:00
Dave Richer
6b1b393804 Merged in feature/IO-2769-Job-Totals-Testing (pull request #2256)
Feature/IO-2769 Job Totals Testing
2025-04-08 22:11:26 +00:00
Dave Richer
c5181d1c5d feature/IO-2769-Job-Totals-Testing - non-related 2025-04-08 18:10:28 -04:00
Dave Richer
e33ff2a45d feature/IO-2769-Job-Totals-Testing - cleanup 2025-04-08 17:04:50 -04:00
Allan Carr
9eb77964db Merged in feature/IO-3202-HasFeatureAccess-Boolean (pull request #2250)
IO-3202 HasFeatureAccess Boolean

Approved-by: Dave Richer
2025-04-08 17:17:50 +00:00
Allan Carr
0a68d2791d IO-3202 HasFeatureAccess Boolean
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-04-08 09:51:52 -07:00
Dave Richer
11928d9a7e Merged in feature/IO-2769-Job-Totals-Testing (pull request #2253)
Feature/IO-2769 Job Totals Testing
2025-04-08 16:23:34 +00:00
Dave Richer
c169bb5d5d feature/IO-2769-Job-Totals-Testing - update end point 2025-04-08 12:19:33 -04:00
Dave Richer
3cc4f1c63e feature/IO-2769-Job-Totals-Testing - fix typo 2025-04-08 12:00:43 -04:00
Dave Richer
5237b1d535 Merged in feature/IO-2769-Job-Totals-Testing (pull request #2251)
feature/IO-2769-Job-Totals-Testing
2025-04-08 15:47:09 +00:00
Dave Richer
cd56c50cf9 feature/IO-2769-Job-Totals-Testing 2025-04-08 11:46:16 -04:00
Dave Richer
a18ce18d72 feature/IO-2769-Job-Totals-Testing 2025-04-08 11:42:27 -04:00
Allan Carr
3691d32aaa IO-3202 HasFeatureAccess Boolean
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-04-07 17:36:39 -07:00
Dave Richer
5f66488410 release/2025-04-11 - Remove unused constant from server.js 2025-04-07 13:06:47 -04:00
Dave Richer
d1be7f6e09 Merged in test-AIO (pull request #2249)
Test AIO
2025-04-07 17:04:04 +00:00
Dave Richer
44f02f28a6 Merged in release/2025-04-11 (pull request #2248)
release/2025-04-11 - Add Ivana to Usage Report
2025-04-07 17:03:00 +00:00
Dave Richer
6d33622b4e release/2025-04-11 - Add Ivana to Usage Report 2025-04-07 13:01:23 -04:00
Dave Richer
f8b8e23ef4 Merged in release/2025-04-11 (pull request #2247)
release/2025-04-11 - Fix Packages
2025-04-07 16:12:42 +00:00
Dave Richer
db09d09428 release/2025-04-11 - Fix Packages 2025-04-07 12:11:53 -04:00
Dave Richer
451820a67c Merged in release/2025-04-11 (pull request #2246)
Release/2025 04 11
2025-04-04 18:19:46 +00:00
Dave Richer
ba0ce5027e Merged in feature/IO-2769-Job-Totals-Testing (pull request #2245)
feature/IO-2769-Job-Totals-testing: Setup testing method for job totals

Approved-by: Patrick Fic
2025-04-04 18:18:51 +00:00
Dave Richer
f777d26cc1 feature/IO-2769-Job-Totals-testing: Allow for both american and canadian capture + tests 2025-04-04 14:15:30 -04:00
Dave Richer
1463037878 feature/IO-2769-Job-Totals-testing: Allow for both american and canadian capture + tests 2025-04-04 13:54:53 -04:00
Dave Richer
7ddec0bb0f feature/IO-2769-Job-Totals-testing: Allow for both american and canadian capture + tests 2025-04-04 13:49:21 -04:00
Dave Richer
51c2d3351a feature/IO-2769-Job-Totals-testing: Allow for both american and canadian capture + tests 2025-04-04 13:47:38 -04:00
Dave Richer
8323fa6696 feature/IO-2769-Job-Totals-testing: Allow for both american and canadian capture + tests 2025-04-04 13:45:45 -04:00
Dave Richer
27a3932c08 feature/IO-2769-Job-Totals-testing: Allow for both american and canadian capture + tests 2025-04-04 13:43:19 -04:00
Dave Richer
add88659a4 feature/IO-2769-Job-Totals-testing: Setup testing method for job totals 2025-04-04 13:23:33 -04:00
Dave Richer
320ad065d0 feature/IO-2769-Job-Totals-testing: Setup testing method for job totals 2025-04-04 13:19:44 -04:00
Dave Richer
a9bc51949a Merged in release/2025-04-11 (pull request #2244)
feature/IO-3198-Seperate-Socket-Provider-useSocket
2025-04-03 17:12:19 +00:00
Dave Richer
39d1397221 Merged in feature/IO-3198-Seperate-Socket-Provider-useSocket (pull request #2243)
feature/IO-3198-Seperate-Socket-Provider-useSocket
2025-04-03 17:11:55 +00:00
Dave Richer
b44b71072f feature/IO-3198-Seperate-Socket-Provider-useSocket 2025-04-03 13:10:24 -04:00
Dave Richer
f3e2a83bab Merged in release/2025-04-11 (pull request #2242)
IO-3193 Reconciliation Zero Part Price or Qty
2025-04-03 16:37:40 +00:00
Allan Carr
0ef030bb89 Merged in feature/IO-3193-Reconciliation-Zero-Part (pull request #2241)
IO-3193 Reconciliation Zero Part Price or Qty

Approved-by: Dave Richer
2025-04-03 15:26:21 +00:00
Allan Carr
3e9e6baf32 IO-3193 Reconciliation Zero Part Price or Qty
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-04-02 13:56:02 -07:00
Dave Richer
c03d45b3fc Merged in release/2025-04-11 (pull request #2240)
Release/2025 04 11
2025-04-02 18:15:36 +00:00
Dave Richer
0a9b583c4b Merged in feature/IO-2885-IntelliPay-App-Postback-Support (pull request #2236)
Feature/IO-2885 IntelliPay App Postback Support
2025-04-02 18:15:07 +00:00
Dave Richer
54ac0c84a7 Merged release/2025-04-11 into feature/IO-2885-IntelliPay-App-Postback-Support 2025-04-02 17:44:54 +00:00
Dave Richer
4d59798d8d Merged in release/2025-03-28 (pull request #2239)
release/2025-03-28 - Add Cookies Provider
2025-04-02 17:44:40 +00:00
Dave Richer
f95dab544d Merged release/2025-04-11 into feature/IO-2885-IntelliPay-App-Postback-Support 2025-04-02 16:59:45 +00:00
Dave Richer
41e43dda96 feature/IO-2885-IntelliPay-App-Postback-Support - Cleanup 2025-04-02 11:59:54 -04:00
Dave Richer
cec60db78c Merge branch 'release/2025-03-28' into feature/IO-2885-IntelliPay-App-Postback-Support 2025-04-02 11:50:42 -04:00
Dave Richer
7e741e4af9 Merged in release/2025-03-28 (pull request #2238)
release/2025-03-28 - Add Cookies Provider
2025-04-02 15:47:49 +00:00
Dave Richer
24d47ae1c5 Merged in release/2025-03-28 (pull request #2237)
Release/2025 03 28
2025-04-02 15:39:16 +00:00
Dave Richer
f556d59ad7 release/2025-03-28 - Add Cookies Provider 2025-04-02 11:38:40 -04:00
Dave Richer
09c4662436 feature/IO-2885-IntelliPay-App-Postback
- Add Tests
2025-04-02 11:34:48 -04:00
Dave Richer
9bf6ba9cf0 feature/IO-2885-IntelliPay-App-Postback
- Refactor / Add Tests
2025-04-02 11:09:03 -04:00
Dave Richer
7843ca9b1a Merged in release/2025-03-28 (pull request #2235)
[DO NOT MERGE ]Release/2025-03-28 into master-AIO - IO-2999, IO-3092, IO-3176, IO-3178, IO-3181, IO-3183, IO-3185, IO-3187
2025-04-02 12:51:24 +00:00
Dave Richer
c78b9866a3 feature/IO-2885-IntelliPay-App-Postback
- Finish ticket
2025-04-01 15:15:48 -04:00
Patrick Fic
c8701aba63 Add region capture to Crisp. 2025-04-01 10:03:17 -07:00
Dave Richer
09c1a8ae35 feature/IO-2885-IntelliPay-App-Postback-Support
- Clean intellipay.js
2025-04-01 12:57:32 -04:00
Dave Richer
0ef2814de3 feature/IO-2885-IntelliPay-App-Postback-Support
- Clean intellipay.js, add new route scaffolding
2025-04-01 12:33:41 -04:00
Dave Richer
8e105f0b36 feature/IO-2885-IntelliPay-App-Postback-Support
- Replace/Remove body-parser with Expresses built in body parsing middleware
2025-04-01 12:20:50 -04:00
Dave Richer
ba4da3e35c feature/IO-2885-IntelliPay-App-Postback-Support
- Packages
2025-04-01 12:04:14 -04:00
Dave Richer
1b8be56c15 Merged in release/2025-03-28 (pull request #2234)
Release/2025-03-28 into test-AIO - IO-3181
2025-03-28 16:18:08 +00:00
Dave Richer
f6e65f82e5 Merged in feature/IO-3181-Test-Framework-Selection (pull request #2233)
Feature/IO-3181 Test Framework Selection
2025-03-28 16:17:31 +00:00
Dave Richer
8b7bb099f3 feature/IO-3181-Test-Framework-Selection - Skeletons complete 2025-03-28 12:16:36 -04:00
Dave Richer
2b26db78eb Merged in release/2025-03-28 (pull request #2232)
IO-3187 Admin Enhancements
2025-03-27 14:04:43 +00:00
Allan Carr
663d91b648 Merged in feature/IO-3187-Admin-Enhancements (pull request #2231)
IO-3187 Admin Enhancements

Approved-by: Dave Richer
2025-03-27 14:04:09 +00:00
Allan Carr
2a7686ec75 IO-3187 Admin Enhancements
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-03-26 15:21:42 -07:00
Dave Richer
549cb56cdf feature/IO-3181-Test-Framework-Selection - Skeletons complete 2025-03-26 16:54:05 -04:00
Dave Richer
146bb6c5c0 feature/IO-3181-Test-Framework-Selection - Skeletons complete 2025-03-26 16:52:59 -04:00
Dave Richer
67b6da7c31 feature/IO-3181-Test-Framework-Selection - Skeletons complete 2025-03-26 16:51:53 -04:00
Dave Richer
c2d96922c8 Merged in release/2025-03-28 (pull request #2230)
IO-3176 IntelliPay Payment Mapping
2025-03-26 18:24:26 +00:00
Allan Carr
624894621b Merged in feature/IO-3176-IntelliPay-Payment-Mapping (pull request #2229)
IO-3176 IntelliPay Payment Mapping

Approved-by: Dave Richer
2025-03-26 18:13:51 +00:00
Allan Carr
3fba215266 Merge branch 'release/2025-03-28' into feature/IO-3176-IntelliPay-Payment-Mapping
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-03-25 20:17:50 -07:00
Allan Carr
bbf291e8f3 IO-3176 IntelliPay Payment Mapping
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-03-25 20:16:34 -07:00
Dave Richer
70b4ec7948 Merged in release/2025-03-28 (pull request #2228)
release/2025-03-28 - Modify vite config
2025-03-25 20:53:36 +00:00
Dave Richer
341fc09c22 release/2025-03-28 - Modify vite config 2025-03-25 16:51:33 -04:00
Dave Richer
a3ec364034 Merged in release/2025-03-28 (pull request #2227)
release/2025-03-28 - Modify vite config
2025-03-25 20:48:25 +00:00
Dave Richer
fb30529808 release/2025-03-28 - Modify vite config 2025-03-25 16:48:03 -04:00
Dave Richer
e1728b275b Merged in release/2025-03-28 (pull request #2226)
release/2025-03-28 - Package locks
2025-03-25 20:38:36 +00:00
Dave Richer
46999145fc release/2025-03-28 - Package locks 2025-03-25 16:38:03 -04:00
Allan Carr
9d1f810af2 Merge branch 'release/2025-03-28' into feature/IO-3176-IntelliPay-Payment-Mapping
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-03-25 12:22:36 -07:00
Dave Richer
10d55df461 Merged in release/2025-03-28 (pull request #2224)
Release/2025 03 28
2025-03-25 19:07:28 +00:00
Dave Richer
b9693aae95 Merge remote-tracking branch 'origin/test-AIO' into release/2025-03-28 2025-03-25 15:04:46 -04:00
Dave Richer
02f5f1985c release/2025-03-28 - Up two deps 2025-03-25 15:01:50 -04:00
Dave Richer
37edceee84 Merged in feature/IO-3092-imgproxy (pull request #2225)
Feature/IO-3092 imgproxy
2025-03-25 18:58:34 +00:00
Allan Carr
1fd63012b0 Merged in release/2025-03-28 (pull request #2222)
Release/2025 03 28
2025-03-25 00:03:29 +00:00
Dave Richer
cf084fa168 Merged in release/2025-03-28 (pull request #2218)
IO-3176 IntelliPay Payment Method Mapping
2025-03-19 18:19:45 +00:00
Dave Richer
96af289640 Merged in release/2025-03-28 (pull request #2216)
IO-2999 IO Test Report Server Migration
2025-03-17 18:37:32 +00:00
Dave Richer
f8df351de6 Merged in release/2025-03-14 (pull request #2214)
Release/2025 03 14
2025-03-14 15:29:10 +00:00
Dave Richer
b8c096f4ff Merged in release/2025-03-14 (pull request #2211)
IO-3172 RO Basic Payments V2
2025-03-13 21:03:15 +00:00
Dave Richer
93ad23b615 Merged in release/2025-03-14 (pull request #2209)
Release/2025 03 14
2025-03-13 19:34:49 +00:00
Dave Richer
0a918535bb Merged in release/2025-03-14 (pull request #2208)
IO-3096-GlobalNotifications - Verify status reporter is a function and exists prior to calling it in cleanup task
2025-03-13 19:00:54 +00:00
Dave Richer
4863b16b5f Merged in release/2025-03-14 (pull request #2206)
IO-3096-GlobalNotifications - Add in a function to exclude extra logging from production
2025-03-13 17:58:20 +00:00
Dave Richer
a27f5e2153 Merged in release/2025-03-14 (pull request #2203)
IO-3170-HotfixFoRedis
2025-03-13 15:52:48 +00:00
Dave Richer
3ffea50072 Merged in release/2025-03-14 (pull request #2201)
IO-3166-Global-Notifications-Part-2: Remove unused event handler (hasura),
2025-03-13 15:32:21 +00:00
Dave Richer
34af7d3880 Merged in release/2025-03-14 (pull request #2198)
IO-3166-Global-Notifications-Part-2: add additional key prefixes for dev v prod
2025-03-13 01:14:18 +00:00
Dave Richer
4432721c27 Merged in release/2025-03-14 (pull request #2195)
IO-3166-Global-Notifications-Part-2: Make sure BULLMQ prefixes do not collide
2025-03-13 00:03:35 +00:00
Dave Richer
65ad4d9426 Merged in release/2025-03-14 (pull request #2192)
Release/2025-03-14 into test-AIO - IO-3172 IO-3166
2025-03-12 16:09:26 +00:00
Dave Richer
18924b4f08 Merged in release/2025-03-14 (pull request #2188)
Release/2025 03 14
2025-03-11 19:16:00 +00:00
Dave Richer
c524f5f0e0 Merged in release/2025-03-14 (pull request #2185)
Release/2025 03 14
2025-03-11 17:18:59 +00:00
Dave Richer
2fbac78eec Merged in release/2025-03-14 (pull request #2181)
IO-3166-Global-Notifications-Part-2: getAwsClusterFix
2025-03-07 21:00:29 +00:00
Dave Richer
4734971d48 Merged in release/2025-03-14 (pull request #2178)
IO-3170-Enhanced-GetRedisEndpointsFromAWS - Fix to prevent breaking
2025-03-07 20:30:58 +00:00
Dave Richer
fc1055c644 Merged in release/2024-03-14 (pull request #2174)
IO-3166-Global-Notifications-Part-2 - Improved GetRedisNodesFromAWS
2025-03-07 20:13:18 +00:00
Dave Richer
24798390b5 Merged in release/2024-03-14 (pull request #2170)
IO-3166-Global-Notifications-Part-2 - Small styling change
2025-03-07 18:52:24 +00:00
Dave Richer
a992dead04 Merged in release/2024-03-14 (pull request #2167)
IO-3166-Global-Notifications-Part-2 - Checkpoint
2025-03-07 16:05:48 +00:00
Dave Richer
f039cd8d0d Merged in release/2024-03-14 (pull request #2164)
Release/2024 03 14
2025-03-06 22:44:23 +00:00
Dave Richer
494e691230 Merged in release/2024-03-14 (pull request #2161)
Release/2024 03 14
2025-03-06 21:08:05 +00:00
Dave Richer
4cc7366290 Merged in release/2024-03-14 (pull request #2158)
Release/2024 03 14
2025-03-06 18:40:37 +00:00
Dave Richer
fd9d660a61 Merged in release/2024-03-14 (pull request #2155)
Release/2024 03 14
2025-03-05 22:33:06 +00:00
Dave Richer
0b5bd4f718 Merged in release/2024-03-14 (pull request #2152)
Release/2024 03 14
2025-03-05 18:56:08 +00:00
Dave Richer
7511b42bd4 Merged in release/2024-03-14 (pull request #2149)
Release/2024 03 14
2025-03-05 16:47:40 +00:00
Dave Richer
26f94c4d5b Merged in release/2024-03-14 (pull request #2146)
Release/2024 03 14
2025-03-04 22:56:23 +00:00
Dave Richer
aa55f4840b Merged in release/2024-03-14 (pull request #2142)
Release/2024 03 14
2025-03-04 16:58:41 +00:00
Dave Richer
2810428d19 test-AIO - Merge in GlobalNotifications branch 2025-03-04 11:35:12 -05:00
Patrick Fic
83da64f96b Merged in feature/IO-3162-sentry-improvements (pull request #2137)
IO-3162 Resize test CI boxes.
2025-02-28 23:27:49 +00:00
Patrick Fic
1f8d027f97 Merged in feature/IO-3162-sentry-improvements (pull request #2136)
feature/IO-3162-sentry-improvements

Approved-by: Patrick Fic
2025-02-28 23:19:23 +00:00
Patrick Fic
2f8ba20a5b Merged in feature/IO-3162-sentry-improvements (pull request #2135)
feature/IO-3162-sentry-improvements

Approved-by: Patrick Fic
2025-02-28 23:04:41 +00:00
Patrick Fic
b525f920e0 Merged in feature/IO-3162-sentry-improvements (pull request #2134)
feature/IO-3162-sentry-improvements
2025-02-28 22:49:41 +00:00
Dave Richer
91fe6745fe Merged in release/2025-02-28 (pull request #2133)
IO-2561 Return Items Modal
2025-02-28 17:37:25 +00:00
Patrick Fic
b9073fe3f5 Merged in feature/IO-3092-imgproxy (pull request #2132)
IO-3092 Refactor exports.

Approved-by: Dave Richer
2025-02-28 17:35:07 +00:00
Patrick Fic
2c95b49ae1 Merged in feature/IO-3092-imgproxy (pull request #2130)
Feature/IO-3092 imgproxy
2025-02-27 21:55:00 +00:00
Patrick Fic
9bde06e110 Merged in release/2025-02-28 (pull request #2127)
Add catch error handling.

Approved-by: Dave Richer
2025-02-21 16:50:57 +00:00
Patrick Fic
30449ca113 Merged in release/2025-02-28 (pull request #2126)
Remove email from handler.

Approved-by: Patrick Fic
2025-02-21 00:38:01 +00:00
Patrick Fic
0405d19f98 Merged in release/2025-02-28 (pull request #2125)
release/2025-02-28

Approved-by: Patrick Fic
2025-02-20 23:46:37 +00:00
Patrick Fic
2c5310403b Merged in release/2025-02-28 (pull request #2124)
release/2025-02-28

Approved-by: Patrick Fic
2025-02-20 23:38:06 +00:00
Dave Richer
e2ef4f1caf Merged in release/2025-02-28 (pull request #2119)
feature/IO-3146-Hotfix-For-Email-Translations
2025-02-19 15:40:47 +00:00
Allan Carr
b32a2d4d86 Merged in release/2025-02-14 (pull request #2116)
Release/2025 02 14

Approved-by: Dave Richer
2025-02-13 17:38:26 +00:00
Dave Richer
7c92484ae0 Merged in release/2025-02-14 (pull request #2113)
Release/2025-02-14 into test-AIO - IO-3127 IO-3128 IO-3077 IO-3131
2025-02-12 19:07:08 +00:00
Allan Carr
67cada5d8e Merged in hotfix/2025-02-06 (pull request #2109)
Hotfix/2025 02 06
2025-02-06 16:34:52 +00:00
Dave Richer
4bf68b637f Merged in release/2025-01-31 (pull request #2101)
release/2025-01-31 - fix teams icon
2025-02-04 18:48:35 +00:00
Dave Richer
b40c433865 Merged in release/2025-01-31 (pull request #2100)
Release/2025 01 31 into test-AIO - IO-3096 IO-2825 IO-3123
2025-02-04 18:04:45 +00:00
Dave Richer
55ed499ab5 Merged in release/2025-01-31 (pull request #2095)
Release/2025 01 31
2025-01-31 18:25:32 +00:00
Dave Richer
353bc3bc05 Merged in release/2025-01-31 (pull request #2091)
Release/2025 01 31 into test-AIO - IO-2681
2025-01-30 20:18:08 +00:00
Dave Richer
df5c96345c Merged in release/2025-01-31 (pull request #2089)
Release/2025 01 31
2025-01-30 17:12:44 +00:00
Dave Richer
2c7c187c45 Merged in release/2025-01-31 (pull request #2084)
Release/2025 01 31 into test-AIO - IO-3108 IO-2676
2025-01-27 18:11:07 +00:00
Dave Richer
3a5a78d60a Merged in release/2025-01-31 (pull request #2076)
feature/IO-3103-Ant5-Notifications - Job Icons fixed (spacing)
2025-01-22 18:48:25 +00:00
Dave Richer
6dd2871c07 Merged in release/2025-01-31 (pull request #2074)
Release/2025 01 31 into test-AIO -  IO-2952, IO-3099, IO-3101, IO-3103
2025-01-22 18:11:57 +00:00
Dave Richer
ef36ab9da0 Merged in release/2025-01-17 (pull request #2065)
hotfix/AdditionalProductFruitsIds - Add additional IDs for product fruits
2025-01-17 18:14:49 +00:00
Dave Richer
a917f6bcdf Merged in release/2025-01-17 (pull request #2063)
Release/2025 01 17 into test-AIO - IO-2951 IO-999 IO-3096
2025-01-17 17:54:01 +00:00
Dave Richer
c5d6457146 Merged in release/2025-01-17 (pull request #2060)
IO-3063 LOU on Schedule PopOver
2025-01-16 15:24:29 +00:00
Dave Richer
f3831e934f Merged in release/2025-01-17 (pull request #2058)
Release/2025 01 17
2025-01-15 15:50:58 +00:00
257 changed files with 13165 additions and 8946 deletions

9
.gitignore vendored
View File

@@ -121,3 +121,12 @@ logs/oAuthClient-log.log
/*.env.*
.idea/*
.idea
# Vitest
vitest-report*/
vitest-coverage/
*.vitest.log
test-output.txt
server/job/test/fixtures
.github

View File

@@ -56,4 +56,5 @@ COPY . .
EXPOSE 4000 9229
# Start the application
CMD ["nodemon", "--legacy-watch", "--inspect=0.0.0.0:9229", "server.js"]
RUN echo "Starting the application..."
CMD ["nodemon", "--ignore", "./server/job/test/fixtures", "--legacy-watch", "--inspect=0.0.0.0:9229", "server.js"]

File diff suppressed because it is too large Load Diff

View File

@@ -11,8 +11,8 @@
"license": "ISC",
"description": "",
"dependencies": {
"express": "^4.21.1",
"mailparser": "^3.7.1",
"express": "^5.1.0",
"mailparser": "^3.7.2",
"node-fetch": "^3.3.2"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -12,3 +12,5 @@ VITE_APP_AXIOS_BASE_API_URL=/api/
VITE_APP_REPORTS_SERVER_URL=https://reports.test.imex.online
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_INSTANCE=IMEX
TEST_USERNAME="test@imex.dev"
TEST_PASSWORD="test123"

View File

@@ -14,3 +14,5 @@ VITE_APP_REPORTS_SERVER_URL=https://reports.test.romeonline.io
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_COUNTRY=USA
VITE_APP_INSTANCE=ROME
TEST_USERNAME="test@imex.dev"
TEST_PASSWORD="test123"

11
client/.gitignore vendored
View File

@@ -1,3 +1,14 @@
# Vitest
vitest-report*/
vitest-coverage/
*.vitest.log
test-output.txt
# Playwright
playwright-report/
test-results/
playwright/.cache/
*.playwright.log
# Sentry Config File
.sentryclirc

View File

@@ -74,50 +74,8 @@
})();
</script>
<% } %>
<script>
!(function () {
"use strict";
var e = [
"debug",
"destroy",
"do",
"help",
"identify",
"is",
"off",
"on",
"ready",
"render",
"reset",
"safe",
"set"
];
if (window.noticeable) console.warn("Noticeable SDK code snippet loaded more than once");
else {
var n = (window.noticeable = window.noticeable || []);
<script>!function(w,d,i,s){function l(){if(!d.getElementById(i)){var f=d.getElementsByTagName(s)[0],e=d.createElement(s);e.type="text/javascript",e.async=!0,e.src="https://canny.io/sdk.js",f.parentNode.insertBefore(e,f)}}if("function"!=typeof w.Canny){var c=function(){c.q.push(arguments)};c.q=[],w.Canny=c,"complete"===d.readyState?l():w.attachEvent?w.attachEvent("onload",l):w.addEventListener("load",l,!1)}}(window,document,"canny-jssdk","script");</script>
function t(e) {
return function () {
var t = Array.prototype.slice.call(arguments);
return t.unshift(e), n.push(t), n;
};
}
!(function () {
for (var o = 0; o < e.length; o++) {
var r = e[o];
n[r] = t(r);
}
})(),
(function () {
var e = document.createElement("script");
(e.async = !0), (e.src = "https://sdk.noticeable.io/l.js");
var n = document.head;
n.insertBefore(e, n.firstChild);
})();
}
})();
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

4107
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,26 +8,25 @@
"private": true,
"proxy": "http://localhost:4000",
"dependencies": {
"@ant-design/pro-layout": "^7.22.3",
"@apollo/client": "^3.13.5",
"@ant-design/pro-layout": "^7.22.4",
"@apollo/client": "^3.13.6",
"@emotion/is-prop-valid": "^1.3.1",
"@fingerprintjs/fingerprintjs": "^4.6.1",
"@firebase/analytics": "^0.10.12",
"@firebase/app": "^0.11.3",
"@firebase/auth": "^1.9.1",
"@firebase/firestore": "^4.7.10",
"@firebase/messaging": "^0.12.17",
"@firebase/analytics": "^0.10.16",
"@firebase/app": "^0.13.0",
"@firebase/auth": "^1.10.5",
"@firebase/firestore": "^4.7.15",
"@firebase/messaging": "^0.12.21",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.6.1",
"@sentry/cli": "^2.42.4",
"@sentry/react": "^9.9.0",
"@sentry/vite-plugin": "^3.2.2",
"@splitsoftware/splitio-react": "^2.0.1",
"@reduxjs/toolkit": "^2.8.2",
"@sentry/cli": "^2.45.0",
"@sentry/react": "^9.22.0",
"@sentry/vite-plugin": "^3.5.0",
"@splitsoftware/splitio-react": "^2.1.1",
"@tanem/react-nprogress": "^5.0.53",
"@vitejs/plugin-react": "^4.3.4",
"antd": "^5.24.5",
"antd": "^5.25.2",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^4.2.0",
"apollo-link-sentry": "^4.3.0",
"autosize": "^6.0.1",
"axios": "^1.8.4",
"classnames": "^2.5.1",
@@ -38,18 +37,18 @@
"dotenv": "^16.4.7",
"env-cmd": "^10.1.0",
"exifr": "^7.1.3",
"graphql": "^16.10.0",
"graphql": "^16.11.0",
"i18next": "^24.2.3",
"i18next-browser-languagedetector": "^8.0.4",
"i18next-browser-languagedetector": "^8.1.0",
"immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.12.6",
"libphonenumber-js": "^1.12.8",
"logrocket": "^9.0.2",
"markerjs2": "^2.32.4",
"memoize-one": "^6.0.0",
"normalize-url": "^8.0.1",
"object-hash": "^3.0.0",
"prop-types": "^15.8.1",
"query-string": "^9.1.1",
"query-string": "^9.2.0",
"raf-schd": "^4.0.3",
"react": "^18.3.1",
"react-big-calendar": "^1.18.0",
@@ -70,17 +69,17 @@
"react-resizable": "^3.0.5",
"react-router-dom": "^6.30.0",
"react-sticky": "^6.0.3",
"react-virtuoso": "^4.12.5",
"recharts": "^2.15.0",
"react-virtuoso": "^4.12.7",
"recharts": "^2.15.2",
"redux": "^5.0.1",
"redux-actions": "^3.0.3",
"redux-persist": "^6.0.0",
"redux-saga": "^1.3.0",
"redux-state-sync": "^3.1.4",
"reselect": "^5.1.1",
"sass": "^1.86.0",
"sass": "^1.89.0",
"socket.io-client": "^4.8.1",
"styled-components": "^6.1.16",
"styled-components": "^6.1.18",
"subscriptions-transport-ws": "^0.11.0",
"use-memo-one": "^1.1.3",
"vite-plugin-ejs": "^1.7.0",
@@ -100,7 +99,14 @@
"build:production:imex": "env-cmd -f .env.production.imex npm run build",
"build:production:rome": "env-cmd -f .env.production.rome npm run build",
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .",
"eulaize": "node src/utils/eulaize.js"
"eulaize": "node src/utils/eulaize.js",
"test:unit": "vitest run",
"test:watch": "vitest",
"test:e2e:imex": "playwright test --config playwright.config.js",
"test:e2e:rome": "playwright test --config playwright.rome.config.js",
"test:e2e:imex:headed": "playwright test --config playwright.config.js --headed",
"test:e2e:rome:headed": "playwright test --config playwright.rome.config.js --headed",
"test:e2e:report": "playwright show-report"
},
"browserslist": {
"production": [
@@ -123,30 +129,38 @@
"devDependencies": {
"@ant-design/icons": "^6.0.0",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-react": "^7.26.3",
"@dotenvx/dotenvx": "^1.39.0",
"@babel/preset-react": "^7.27.1",
"@dotenvx/dotenvx": "^1.44.1",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/react": "^11.14.0",
"@eslint/js": "^9.23.0",
"@sentry/webpack-plugin": "^3.2.2",
"browserslist": "^4.24.4",
"@eslint/js": "^9.27.0",
"@playwright/test": "^1.51.1",
"@sentry/webpack-plugin": "^3.5.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@vitejs/plugin-react": "^4.3.4",
"browserslist": "^4.24.5",
"browserslist-to-esbuild": "^2.1.1",
"chalk": "^5.4.1",
"eslint": "^8.57.1",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react": "^7.37.5",
"globals": "^15.15.0",
"memfs": "^4.17.0",
"jsdom": "^26.0.0",
"memfs": "^4.17.2",
"os-browserify": "^0.3.0",
"playwright": "^1.51.1",
"react-error-overlay": "^6.1.0",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.3",
"vite": "^6.2.3",
"vite-plugin-babel": "^1.3.0",
"vite": "^6.3.5",
"vite-plugin-babel": "^1.3.1",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-node-polyfills": "^0.23.0",
"vite-plugin-pwa": "^0.21.2",
"vite-plugin-pwa": "^1.0.0",
"vite-plugin-style-import": "^2.0.0",
"vitest": "^3.1.4",
"workbox-window": "^7.3.0"
}
}

View File

@@ -0,0 +1,25 @@
import { defineConfig } from "@playwright/test";
import dotenv from "dotenv";
dotenv.config({
path: "./.env.development.imex",
prefix: "TEST_"
});
export default defineConfig({
testDir: "./tests/e2e",
testMatch: "*.e2e.js",
timeout: 60 * 1000,
reporter: [["list"], ["html"]],
use: {
baseURL: "https://localhost:3000",
browser: "chromium",
ignoreHTTPSErrors: true
},
webServer: {
command: "npm run start:imex",
ignoreHTTPSErrors: true,
url: "https://localhost:3000/health", // Health check endpoint will tell us when the server is ready
reuseExistingServer: !process.env.CI // Reuse server locally, not in CI
}
});

View File

@@ -0,0 +1,25 @@
import { defineConfig } from "@playwright/test";
import dotenv from "dotenv";
dotenv.config({
path: "./.env.development.rome",
prefix: "TEST_"
});
export default defineConfig({
testDir: "./tests/e2e",
testMatch: "*.e2e.js",
timeout: 60 * 1000,
reporter: [["list"], ["html"]],
use: {
baseURL: "https://localhost:3000",
browser: "chromium",
ignoreHTTPSErrors: true
},
webServer: {
command: "npm run start:rome",
ignoreHTTPSErrors: true,
url: "https://localhost:3000/health", // Health check endpoint will tell us when the server is ready
reuseExistingServer: !process.env.CI // Reuse server locally, not in CI
}
});

View File

@@ -10,6 +10,7 @@ import client from "../utils/GraphQLClient";
import App from "./App";
import * as Sentry from "@sentry/react";
import themeProvider from "./themeProvider";
import { CookiesProvider } from "react-cookie";
// Base Split configuration
const config = {
@@ -38,26 +39,28 @@ function AppContainer() {
const { t } = useTranslation();
return (
<ApolloProvider client={client}>
<ConfigProvider
input={{ autoComplete: "new-password" }}
locale={enLocale}
theme={themeProvider}
form={{
validateMessages: {
// eslint-disable-next-line no-template-curly-in-string
required: t("general.validation.required", { label: "${label}" })
}
}}
>
<GlobalLoadingBar />
<SplitFactoryProvider config={config}>
<SplitClientProvider>
<App />
</SplitClientProvider>
</SplitFactoryProvider>
</ConfigProvider>
</ApolloProvider>
<CookiesProvider>
<ApolloProvider client={client}>
<ConfigProvider
input={{ autoComplete: "new-password" }}
locale={enLocale}
theme={themeProvider}
form={{
validateMessages: {
// eslint-disable-next-line no-template-curly-in-string
required: t("general.validation.required", { label: "${label}" })
}
}}
>
<GlobalLoadingBar />
<SplitFactoryProvider config={config}>
<SplitClientProvider>
<App />
</SplitClientProvider>
</SplitFactoryProvider>
</ConfigProvider>
</ApolloProvider>
</CookiesProvider>
);
}

View File

@@ -21,8 +21,8 @@ import "./App.styles.scss";
import Eula from "../components/eula/eula.component";
import InstanceRenderMgr from "../utils/instanceRenderMgr";
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
import { SocketProvider } from "../contexts/SocketIO/useSocket.jsx";
import { NotificationProvider } from "../contexts/Notifications/notificationContext.jsx";
import SocketProvider from "../contexts/SocketIO/socketProvider.jsx";
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
@@ -142,11 +142,10 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
>
<ProductFruitsWrapper
currentUser={currentUser}
workspaceCode={InstanceRenderMgr({
imex: null,
rome: "9BkbEseqNqxw8jUH"
})}
bodyshop={bodyshop}
workspaceCode={bodyshop?.tours_enabled ? "9BkbEseqNqxw8jUH" : ""}
/>
<NotificationProvider>
<Routes>
<Route

View File

@@ -1,8 +1,16 @@
import React from "react";
import { ProductFruits } from "react-product-fruits";
import PropTypes from "prop-types";
import { ProductFruits } from "react-product-fruits";
import dayjs from "dayjs";
const ProductFruitsWrapper = React.memo(({ currentUser, bodyshop, workspaceCode }) => {
const featureProps = bodyshop?.features
? Object.entries(bodyshop.features).reduce((acc, [key, value]) => {
acc[key] = value === true || (typeof value === "string" && dayjs(value).isAfter(dayjs()));
return acc;
}, {})
: {};
const ProductFruitsWrapper = React.memo(({ currentUser, workspaceCode }) => {
return (
workspaceCode &&
currentUser?.authorized === true &&
@@ -14,7 +22,8 @@ const ProductFruitsWrapper = React.memo(({ currentUser, workspaceCode }) => {
language="en"
user={{
email: currentUser.email,
username: currentUser.email
username: currentUser.email,
props: featureProps
}}
/>
)
@@ -28,5 +37,6 @@ ProductFruitsWrapper.propTypes = {
authorized: PropTypes.bool,
email: PropTypes.string
}),
workspaceCode: PropTypes.string
workspaceCode: PropTypes.string,
bodyshop: PropTypes.object
};

View File

@@ -1,3 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Alert component should render Alert component 1`] = `ShallowWrapper {}`;

View File

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

View File

@@ -0,0 +1,31 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import AlertComponent from "./alert.component";
describe("AlertComponent", () => {
it("renders with default props", () => {
render(<AlertComponent message="Default Alert" />);
expect(screen.getByText("Default Alert")).toBeInTheDocument();
expect(screen.getByRole("alert")).toHaveClass("ant-alert");
});
it("applies type prop correctly", () => {
render(<AlertComponent message="Success Alert" type="success" />);
const alert = screen.getByRole("alert");
expect(screen.getByText("Success Alert")).toBeInTheDocument();
expect(alert).toHaveClass("ant-alert-success");
});
it("displays description when provided", () => {
render(<AlertComponent message="Error Alert" description="Something went wrong" type="error" />);
expect(screen.getByText("Error Alert")).toBeInTheDocument();
expect(screen.getByText("Something went wrong")).toBeInTheDocument();
expect(screen.getByRole("alert")).toHaveClass("ant-alert-error");
});
it("is closable and shows icon when props are set", () => {
render(<AlertComponent message="Warning Alert" type="warning" showIcon closable />);
expect(screen.getByText("Warning Alert")).toBeInTheDocument();
expect(screen.getByRole("button", { name: /close/i })).toBeInTheDocument(); // Close button
});
});

View File

@@ -1,5 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AllocationsAssignmentComponent component should create an allocation on save 1`] = `ReactWrapper {}`;
exports[`AllocationsAssignmentComponent component should render AllocationsAssignmentComponent component 1`] = `ReactWrapper {}`;

View File

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

View File

@@ -14,8 +14,21 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
setPartsOrderContext: (context) =>
dispatch(
setModalContext({
context: context,
modal: "partsOrder"
})
),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(
insertAuditTrail({
jobid,
operation,
type
})
)
});
export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditReturn);
@@ -69,7 +82,7 @@ export function BillDetailEditReturn({ setPartsOrderContext, insertAuditTrail, b
<Modal
open={open}
onCancel={() => setOpen(false)}
destroyOnClose
destroyOnHidden
title={t("bills.actions.return")}
onOk={() => form.submit()}
>

View File

@@ -29,7 +29,7 @@ export default function BillDetailEditcontainer() {
delete search.billid;
history({ search: queryString.stringify(search) });
}}
destroyOnClose
destroyOnHidden
open={search.billid}
>
<BillDetailEditComponent />

View File

@@ -412,7 +412,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
)}
</Space>
}
destroyOnClose
destroyOnHidden
>
<Form
onFinish={handleFinish}

View File

@@ -75,7 +75,7 @@ export function ContractsFindModalContainer({ caBcEtfTableModal, toggleModalVisi
title={t("payments.labels.findermodal")}
onCancel={() => toggleModalVisible()}
onOk={() => toggleModalVisible()}
destroyOnClose
destroyOnHidden
forceRender
>
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>

View File

@@ -40,7 +40,7 @@ function CardPaymentModalContainer({ cardPaymentModal, toggleModalVisible, bodys
</Button>
]}
width="80%"
destroyOnClose
destroyOnHidden
>
<CardPaymentModalComponent />
</Modal>

View File

@@ -3,11 +3,11 @@ import { getToken } from "@firebase/messaging";
import axios from "axios";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
import { messaging, requestForToken } from "../../firebase/firebase.utils";
import ChatPopupComponent from "../chat-popup/chat-popup.component";
import "./chat-affix.styles.scss";
import { registerMessagingHandlers, unregisterMessagingHandlers } from "./registerMessagingSocketHandlers";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
export function ChatAffixContainer({ bodyshop, chatVisible }) {
const { t } = useTranslation();
@@ -34,16 +34,14 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
SubscribeToTopicForFCMNotification();
//Register WS handlers
// Register WebSocket handlers
if (socket && socket.connected) {
registerMessagingHandlers({ socket, client });
}
return () => {
if (socket && socket.connected) {
return () => {
unregisterMessagingHandlers({ socket });
}
};
};
}
}, [bodyshop, socket, t, client]);
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;

View File

@@ -3,10 +3,10 @@ import { Button } from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { TOGGLE_CONVERSATION_ARCHIVE } from "../../graphql/conversations.queries";
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors.js";
import { connect } from "react-redux";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop

View File

@@ -1,5 +1,5 @@
import { Badge, Card, List, Space, Tag } from "antd";
import React, { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { connect } from "react-redux";
import { Virtuoso } from "react-virtuoso";
import { createStructuredSelector } from "reselect";
@@ -10,35 +10,60 @@ import PhoneFormatter from "../../utils/PhoneFormatter";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import _ from "lodash";
import "./chat-conversation-list.styles.scss";
import { useQuery } from "@apollo/client";
import { GET_PHONE_NUMBER_OPT_OUTS } from "../../graphql/phone-number-opt-out.queries.js";
import { phone } from "phone";
import { useTranslation } from "react-i18next";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
selectedConversation: selectSelectedConversation
selectedConversation: selectSelectedConversation,
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
setSelectedConversation: (conversationId) => dispatch(setSelectedConversation(conversationId))
});
function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation }) {
// That comma is there for a reason, do not remove it
function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation, bodyshop }) {
const { t } = useTranslation();
const [, forceUpdate] = useState(false);
// Re-render every minute
const phoneNumbers = conversationList.map((item) => phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, ""));
const { data: optOutData } = useQuery(GET_PHONE_NUMBER_OPT_OUTS, {
variables: {
bodyshopid: bodyshop.id,
phone_numbers: phoneNumbers
},
skip: !conversationList.length,
fetchPolicy: "cache-and-network"
});
const optOutMap = useMemo(() => {
const map = new Map();
optOutData?.phone_number_opt_out?.forEach((optOut) => {
map.set(optOut.phone_number, true);
});
return map;
}, [optOutData?.phone_number_opt_out]);
useEffect(() => {
const interval = setInterval(() => {
forceUpdate((prev) => !prev); // Toggle state to trigger re-render
}, 60000); // 1 minute in milliseconds
return () => clearInterval(interval); // Cleanup on unmount
forceUpdate((prev) => !prev);
}, 60000);
return () => clearInterval(interval);
}, []);
// Memoize the sorted conversation list
const sortedConversationList = React.useMemo(() => {
const sortedConversationList = useMemo(() => {
return _.orderBy(conversationList, ["updated_at"], ["desc"]);
}, [conversationList]);
const renderConversation = (index) => {
const renderConversation = (index, t) => {
const item = sortedConversationList[index];
const normalizedPhone = phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, "");
const hasOptOutEntry = optOutMap.has(normalizedPhone);
const cardContentRight = <TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
const cardContentLeft =
item.job_conversations.length > 0
@@ -60,7 +85,12 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
</>
);
const cardExtra = <Badge count={item.messages_aggregate.aggregate.count} />;
const cardExtra = (
<>
<Badge count={item.messages_aggregate.aggregate.count} />
{hasOptOutEntry && <Tag color="red">{t("messaging.labels.no_consent")}</Tag>}
</>
);
const getCardStyle = () =>
item.id === selectedConversation
@@ -73,9 +103,25 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
onClick={() => setSelectedConversation(item.id)}
className={`chat-list-item ${item.id === selectedConversation ? "chat-list-selected-conversation" : ""}`}
>
<Card style={getCardStyle()} bordered={false} size="small" extra={cardExtra} title={cardTitle}>
<div style={{ display: "inline-block", width: "70%", textAlign: "left" }}>{cardContentLeft}</div>
<div style={{ display: "inline-block", width: "30%", textAlign: "right" }}>{cardContentRight}</div>
<Card style={getCardStyle()} variant={true} size="small" extra={cardExtra} title={cardTitle}>
<div
style={{
display: "inline-block",
width: "70%",
textAlign: "left"
}}
>
{cardContentLeft}
</div>
<div
style={{
display: "inline-block",
width: "30%",
textAlign: "right"
}}
>
{cardContentRight}
</div>
</Card>
</List.Item>
);
@@ -85,7 +131,7 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
<div className="chat-list-container">
<Virtuoso
data={sortedConversationList}
itemContent={(index) => renderConversation(index)}
itemContent={(index) => renderConversation(index, t)}
style={{ height: "100%", width: "100%" }}
/>
</div>

View File

@@ -24,7 +24,7 @@
/* Add spacing and better alignment for items */
.chat-list-item {
padding: 0.5rem 0; /* Add spacing between list items */
padding: 0.2rem 0; /* Add spacing between list items */
.ant-card {
border-radius: 8px; /* Slight rounding for card edges */

View File

@@ -4,10 +4,10 @@ import { Link } from "react-router-dom";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors.js";
import { connect } from "react-redux";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop

View File

@@ -3,11 +3,11 @@ import axios from "axios";
import { useCallback, useEffect, useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
import { CONVERSATION_SUBSCRIPTION_BY_PK, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries";
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ChatConversationComponent from "./chat-conversation.component";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
const mapStateToProps = createStructuredSelector({
selectedConversation: selectSelectedConversation,

View File

@@ -4,11 +4,11 @@ import { Input, Spin, Tag, Tooltip } from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_CONVERSATION_LABEL } from "../../graphql/conversations.queries";
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors.js";
import { connect } from "react-redux";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop

View File

@@ -37,7 +37,7 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
variables: {
jobId: conversation.job_conversations[0] && conversation.job_conversations[0].jobid
jobId: conversation.job_conversations[0] && conversation.job_conversations[0]?.jobid
},
skip: !open || !conversation.job_conversations || conversation.job_conversations.length === 0
@@ -67,14 +67,14 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
<>
{!bodyshop.uselocalmediaserver && (
<JobsDocumentImgproxyGalleryExternal
jobId={conversation.job_conversations[0].jobid}
jobId={conversation.job_conversations[0]?.jobid}
externalMediaState={[selectedMedia, setSelectedMedia]}
/>
)}
{bodyshop.uselocalmediaserver && open && (
<JobDocumentsLocalGalleryExternal
externalMediaState={[selectedMedia, setSelectedMedia]}
jobId={conversation.job_conversations[0] && conversation.job_conversations[0].jobid}
jobId={conversation.job_conversations[0] && conversation.job_conversations[0]?.jobid}
/>
)}
</>
@@ -89,7 +89,7 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
{bodyshop.uselocalmediaserver && open && (
<JobDocumentsLocalGalleryExternal
externalMediaState={[selectedMedia, setSelectedMedia]}
jobId={conversation.job_conversations[0] && conversation.job_conversations[0].jobid}
jobId={conversation.job_conversations[0] && conversation.job_conversations[0]?.jobid}
/>
)}
</>

View File

@@ -5,7 +5,7 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser

View File

@@ -7,7 +7,7 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { searchingForConversation } from "../../redux/messaging/messaging.selectors";
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
const mapStateToProps = createStructuredSelector({

View File

@@ -12,9 +12,9 @@ import ChatConversationListComponent from "../chat-conversation-list/chat-conver
import ChatConversationContainer from "../chat-conversation/chat-conversation.container";
import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
import "./chat-popup.styles.scss";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
const mapStateToProps = createStructuredSelector({
selectedConversation: selectSelectedConversation,

View File

@@ -1,5 +1,5 @@
import { LoadingOutlined, SendOutlined } from "@ant-design/icons";
import { Input, Spin } from "antd";
import { Alert, Input, Spin } from "antd";
import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -10,6 +10,9 @@ import { selectIsSending, selectMessage } from "../../redux/messaging/messaging.
import { selectBodyshop } from "../../redux/user/user.selectors";
import ChatMediaSelector from "../chat-media-selector/chat-media-selector.component";
import ChatPresetsComponent from "../chat-presets/chat-presets.component";
import { useQuery } from "@apollo/client";
import { phone } from "phone";
import { GET_PHONE_NUMBER_OPT_OUT } from "../../graphql/phone-number-opt-out.queries";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -25,16 +28,24 @@ const mapDispatchToProps = (dispatch) => ({
function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSending, message, setMessage }) {
const inputArea = useRef(null);
const [selectedMedia, setSelectedMedia] = useState([]);
const { t } = useTranslation();
const normalizedPhone = phone(conversation.phone_num, "CA").phoneNumber.replace(/^\+1/, "");
const { data: optOutData } = useQuery(GET_PHONE_NUMBER_OPT_OUT, {
variables: { bodyshopid: bodyshop.id, phone_number: normalizedPhone },
fetchPolicy: "cache-and-network"
});
const isOptedOut = !!optOutData?.phone_number_opt_out?.[0];
useEffect(() => {
inputArea.current.focus();
}, [isSending, setMessage]);
const { t } = useTranslation();
const handleEnter = () => {
const selectedImages = selectedMedia.filter((i) => i.isSelected);
if ((message === "" || !message) && selectedImages.length === 0) return;
if (isOptedOut) return; // Prevent sending if phone number is opted out
logImEXEvent("messaging_send_message");
if (selectedImages.length < 11) {
@@ -44,7 +55,8 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
messagingServiceSid: bodyshop.messagingservicesid,
conversationid: conversation.id,
selectedMedia: selectedImages,
imexshopid: bodyshop.imexshopid
imexshopid: bodyshop.imexshopid,
bodyshopid: bodyshop.id
};
sendMessage(newMessage);
setSelectedMedia(
@@ -57,6 +69,7 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
return (
<div className="imex-flex-row" style={{ width: "100%" }}>
{isOptedOut && <Alert message={t("messaging.errors.no_consent")} type="warning" style={{ marginBottom: 8 }} />}
<ChatPresetsComponent className="imex-flex-row__margin" />
<ChatMediaSelector
conversation={conversation}
@@ -71,18 +84,18 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
ref={inputArea}
autoSize={{ minRows: 1, maxRows: 4 }}
value={message}
disabled={isSending}
disabled={isSending || isOptedOut}
placeholder={t("messaging.labels.typeamessage")}
onChange={(e) => setMessage(e.target.value)}
onPressEnter={(event) => {
event.preventDefault();
if (!!!event.shiftKey) handleEnter();
if (!event.shiftKey && !isOptedOut) handleEnter();
}}
/>
</span>
<SendOutlined
className="chat-send-message-button"
// disabled={message === "" || !message}
disabled={isOptedOut || message === "" || !message}
onClick={handleEnter}
/>
<Spin

View File

@@ -8,10 +8,10 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries";
import ChatTagRo from "./chat-tag-ro.component";
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors.js";
import { connect } from "react-redux";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop

View File

@@ -63,7 +63,7 @@ export function ContractsFindModalContainer({
title={t("contracts.labels.findermodal")}
onCancel={() => toggleModalVisible()}
onOk={() => toggleModalVisible()}
destroyOnClose
destroyOnHidden
forceRender
>
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>

View File

@@ -152,7 +152,7 @@ export function EmailOverlayContainer({ emailConfig, modalVisible, toggleEmailOv
}, [modalVisible]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<Modal
destroyOnClose={true}
destroyOnHidden
open={modalVisible}
maskClosable={false}
width={"80%"}

View File

@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
const { Option } = Select;
//To be used as a form element only.
const EmployeeSearchSelect = ({ options, ...props }) => {
const EmployeeSearchSelect = ({ options, showEmail, ...props }) => {
const { t } = useTranslation();
return (
@@ -21,12 +21,16 @@ const EmployeeSearchSelect = ({ options, ...props }) => {
{options
? options.map((o) => (
<Option key={o.id} value={o.id} search={`${o.employee_number} ${o.first_name} ${o.last_name}`}>
<Space>
{`${o.employee_number} ${o.first_name} ${o.last_name}`}
<Tag color="green">
<Space size="small">
{`${o.employee_number ?? ""} ${o.first_name} ${o.last_name}`}
<Tag color="green" style={{ padding: "0.1 0.1rem", marginRight: "1px", marginLeft: "1px" }}>
{o.flat_rate ? t("timetickets.labels.flat_rate") : t("timetickets.labels.straight_time")}
</Tag>
{showEmail && o.user_email ? (
<Tag color="blue" style={{ padding: "0.1 0.1rem", marginRight: "1px", marginLeft: "1px" }}>
{o.user_email}
</Tag>
) : null}
</Space>
</Option>
))

View File

@@ -20,6 +20,7 @@ function FeatureWrapper({
children,
upsellComponent,
bypass,
// eslint-disable-next-line no-unused-vars
...restProps
}) {
const { t } = useTranslation();
@@ -78,7 +79,12 @@ export function HasFeatureAccess({ featureName, bodyshop, bypass, debug = false
}
return true;
}
return bodyshop?.features?.allAccess || dayjs(bodyshop?.features[featureName]).isAfter(dayjs());
return (
bodyshop?.features?.allAccess ||
(typeof bodyshop?.features?.[featureName] === "boolean"
? bodyshop?.features?.[featureName]
: dayjs(bodyshop?.features?.[featureName]).isAfter(dayjs()))
);
}
export default connect(mapStateToProps, null)(FeatureWrapper);

View File

@@ -40,7 +40,6 @@ import { RiSurveyLine } from "react-icons/ri";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js";
import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions";
@@ -51,6 +50,8 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import LockWrapper from "../lock-wrapper/lock-wrapper.component";
import NotificationCenterContainer from "../notification-center/notification-center.container.jsx";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
import { useIsEmployee } from "../../utils/useIsEmployee.js";
// Redux mappings
const mapStateToProps = createStructuredSelector({
@@ -98,6 +99,7 @@ function Header({
const baseTitleRef = useRef(document.title || "");
const lastSetTitleRef = useRef("");
const userAssociationId = bodyshop?.associations?.[0]?.id;
const isEmployee = useIsEmployee(bodyshop, currentUser);
const {
data: unreadData,
@@ -682,7 +684,7 @@ function Header({
icon: unreadLoading ? (
<Spin size="small" />
) : (
<Badge offset={[8, 0]} size="small" count={unreadCount}>
<Badge offset={[8, 0]} size="small" count={isEmployee ? unreadCount : 0}>
<BellFilled />
</Badge>
),

View File

@@ -98,7 +98,7 @@ export function InventoryUpsertModalContainer({ currentUser, bodyshop, inventory
onCancel={() => {
toggleModalVisible();
}}
destroyOnClose
destroyOnHidden
>
<Form form={form} onFinish={handleFinish} layout="vertical">
<InventoryUpsertModal form={form} />

View File

@@ -1,5 +1,5 @@
import { AlertFilled } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { useLazyQuery, useMutation } from "@apollo/client";
import { Button, Divider, Dropdown, Form, Input, Popover, Select, Space } from "antd";
import parsePhoneNumber from "libphonenumber-js";
import queryString from "query-string";
@@ -8,24 +8,30 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
import { GET_JOB_BY_PK_QUICK_INTAKE, JOB_PRODUCTION_TOGGLE } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
import dayjs from "../../utils/day";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import DataLabel from "../data-label/data-label.component";
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component";
import { HasFeatureAccess } from "./../feature-wrapper/feature-wrapper.component";
import ScheduleAtChange from "./job-at-change.component";
import ScheduleEventColor from "./schedule-event.color.component";
import ScheduleEventNote from "./schedule-event.note.component";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -33,7 +39,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({
setScheduleContext: (context) => dispatch(setModalContext({ context: context, modal: "schedule" })),
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
setMessage: (text) => dispatch(setMessage(text))
setMessage: (text) => dispatch(setMessage(text)),
insertAuditTrail: ({ jobid, operation }) => dispatch(insertAuditTrail({ jobid, operation }))
});
export function ScheduleEventComponent({
@@ -43,16 +50,42 @@ export function ScheduleEventComponent({
event,
refetch,
handleCancel,
setScheduleContext
setScheduleContext,
insertAuditTrail
}) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const history = useNavigate();
const searchParams = queryString.parse(useLocation().search);
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
const [mutationUpdateJob] = useMutation(JOB_PRODUCTION_TOGGLE);
const [title, setTitle] = useState(event.title);
const { socket } = useSocket();
const notification = useNotification();
const [form] = Form.useForm();
const [popOverVisible, setPopOverVisible] = useState(false);
const [getJobDetails] = useLazyQuery(GET_JOB_BY_PK_QUICK_INTAKE, {
variables: { id: event.job?.id },
onCompleted: (data) => {
if (data?.jobs_by_pk) {
const totalHours =
(data.jobs_by_pk.labhrs?.aggregate?.sum?.mod_lb_hrs || 0) +
(data.jobs_by_pk.larhrs?.aggregate?.sum?.mod_lb_hrs || 0);
form.setFieldsValue({
actual_in: data.jobs_by_pk.actual_in ? data.jobs_by_pk.actual_in : dayjs(),
scheduled_completion: data.jobs_by_pk.scheduled_completion
? data.jobs_by_pk.scheduled_completion
: totalHours && bodyshop.ss_configuration.nobusinessdays
? dayjs().businessDaysAdd(totalHours / (bodyshop.target_touchtime || 1), "day")
: dayjs().add(totalHours / (bodyshop.target_touchtime || 1), "day"),
scheduled_delivery: data.jobs_by_pk.scheduled_delivery
});
}
},
fetchPolicy: "network-only"
});
const blockContent = (
<Space direction="vertical" wrap>
@@ -89,6 +122,74 @@ export function ScheduleEventComponent({
</Space>
);
const handleConvert = async (values) => {
const res = await mutationUpdateJob({
variables: {
jobId: event.job.id,
job: {
...values,
status: bodyshop.md_ro_statuses.default_arrived,
inproduction: true
}
}
});
if (!res.errors) {
notification["success"]({
message: t("jobs.successes.converted")
});
insertAuditTrail({
jobid: event.job.id,
operation: AuditTrailMapping.jobintake(
res.data.update_jobs.returning[0].status,
DateTimeFormatterFunction(values.scheduled_completion)
)
});
setPopOverVisible(false);
refetch();
}
};
const popMenu = (
<div onClick={(e) => e.stopPropagation()}>
<Form layout="vertical" form={form} onFinish={handleConvert}>
<Form.Item
name={["actual_in"]}
label={t("jobs.fields.actual_in")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<FormDateTimePickerComponent disabled={event.ro_number} />
</Form.Item>
<Form.Item
name={["scheduled_completion"]}
label={t("jobs.fields.scheduled_completion")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<FormDateTimePickerComponent disabled={event.ro_number} />
</Form.Item>
<Form.Item name={["scheduled_delivery"]} label={t("jobs.fields.scheduled_delivery")}>
<FormDateTimePickerComponent disabled={event.ro_number} />
</Form.Item>
<Space wrap>
<Button type="primary" onClick={() => form.submit()}>
{t("general.actions.save")}
</Button>
</Space>
</Form>
</div>
);
const popoverContent = (
<div style={{ maxWidth: "40vw" }}>
{!event.isintake ? (
@@ -294,7 +395,7 @@ export function ScheduleEventComponent({
) : (
<ScheduleManualEvent event={event} />
)}
{event.isintake ? (
{event.isintake && HasFeatureAccess({ featureName: "checklist", bodyshop }) ? (
<Link
to={{
pathname: `/manage/jobs/${event.job && event.job.id}/intake`,
@@ -303,7 +404,23 @@ export function ScheduleEventComponent({
>
<Button disabled={event.arrived}>{t("appointments.actions.intake")}</Button>
</Link>
) : null}
) : (
<Popover //open={open}
content={popMenu}
open={popOverVisible}
onOpenChange={setPopOverVisible}
onClick={(e) => {
if (event.job?.id) {
e.stopPropagation();
getJobDetails();
}
}}
getPopupContainer={(trigger) => trigger.parentNode}
trigger="click"
>
<Button disabled={event.arrived}>{t("jobs.actions.intake_quick")}</Button>
</Popover>
)}
</Space>
</div>
);

View File

@@ -49,7 +49,7 @@ export function JobCostingModalContainer({ jobCostingModal, toggleModalVisible }
}}
cancelButtonProps={{ style: { display: "none" } }}
width="90%"
destroyOnClose
destroyOnHidden
>
{!costingData ? (
<LoadingSpinner loading={true} />

View File

@@ -6,7 +6,8 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { QUERY_JOB_CARD_DETAILS, UPDATE_JOB } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions.js";
@@ -31,7 +32,13 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
setPrintCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "printCenter" })),
setPrintCenterContext: (context) =>
dispatch(
setModalContext({
context: context,
modal: "printCenter"
})
),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(
insertAuditTrail({
@@ -86,7 +93,7 @@ export function JobDetailCards({ bodyshop, setPrintCenterContext, insertAuditTra
};
return (
<Drawer open={!!selected} destroyOnClose width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
<Drawer open={!!selected} destroyOnHidden width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
{loading ? <LoadingSpinner /> : null}
{error ? <AlertComponent message={error.message} type="error" /> : null}
{data ? (

View File

@@ -1,9 +1,9 @@
import { Col, Row } from "antd";
import React, { useState } from "react";
import { useState } from "react";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import JobReconciliationBillsTable from "../job-reconciliation-bills-table/job-reconciliation-bills-table.component";
import JobReconciliationPartsTable from "../job-reconciliation-parts-table/job-reconciliation-parts-table.component";
import JobReconciliationTotals from "../job-reconciliation-totals/job-reconciliation-totals.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
export default function JobReconciliationModalComponent({ job, bills }) {
const jobLineState = useState([]);
@@ -20,7 +20,7 @@ export default function JobReconciliationModalComponent({ job, bills }) {
const filterFunction = InstanceRenderManager({
imex: (j) =>
(j.part_type !== null && j.part_type !== "PAE") ||
(j.part_type !== null && j.part_type !== "PAE" && j.act_price !== 0 && j.part_qty !== 0) ||
(j.line_desc && j.line_desc.toLowerCase().includes("towing") && j.lbr_op === "OP13") ||
j.db_ref === "936004", //ADD SHIPPING LINE.
rome: (j) =>

View File

@@ -44,7 +44,7 @@ function JobReconciliationModalContainer({ reconciliationModal, toggleModalVisib
onOk={handleCancel}
onCancel={handleCancel}
cancelButtonProps={{ display: "none" }}
destroyOnClose
destroyOnHidden
className="imex-reconciliation-modal"
>
{loading && <LoadingSpinner loading={loading} />}

View File

@@ -24,7 +24,8 @@ export default function JobWatcherToggleComponent({
handleToggleSelf,
handleRemoveWatcher,
handleWatcherSelect,
handleTeamSelect
handleTeamSelect,
isEmployee
}) {
const { t } = useTranslation();
@@ -66,22 +67,32 @@ export default function JobWatcherToggleComponent({
<List>
<List.Item
actions={[
<Button
type={isWatching ? "primary" : "default"}
danger={!isWatching}
icon={isWatching ? <EyeOutlined /> : <EyeFilled />}
size="medium"
onClick={handleToggleSelf}
loading={adding || removing}
>
{isWatching ? t("notifications.labels.unwatch") : t("notifications.labels.watch")}
</Button>
<Tooltip title={!isEmployee ? t("notifications.tooltips.not-employee") : ""} placement="top">
<span>
<Button
type={isWatching ? "primary" : "default"}
danger={!isWatching}
icon={isWatching ? <EyeOutlined /> : <EyeFilled />}
size="medium"
onClick={handleToggleSelf}
loading={adding || removing}
disabled={!isEmployee || adding || removing}
>
{isWatching ? t("notifications.labels.unwatch") : t("notifications.labels.watch")}
</Button>
</span>
</Tooltip>
]}
>
<List.Item.Meta>
<Text type="secondary" style={{ marginBottom: 8, display: "block" }}>
{t("notifications.labels.watching-issue")}
</Text>
{!isEmployee && (
<Text type="danger" style={{ marginBottom: 8, display: "block" }}>
{t("notifications.tooltips.not-employee")}
</Text>
)}
</List.Item.Meta>
</List.Item>
</List>
@@ -98,12 +109,16 @@ export default function JobWatcherToggleComponent({
<EmployeeSearchSelectComponent
style={{ minWidth: "100%" }}
options={
bodyshop?.employees?.filter((e) =>
jobWatchers.every((w) => w.user_email !== e.user_email && e.active && e.user_email)
bodyshop?.employees?.filter(
(e) =>
e.user_email && // Ensure user_email is not null or undefined
e.active && // Ensure employee is active
jobWatchers.every((w) => w.user_email !== e.user_email) // Ensure not already a watcher
) || []
}
placeholder={t("notifications.labels.employee-search")}
value={selectedWatcher}
showEmail={true}
onChange={(value) => {
setSelectedWatcher(value);
handleWatcherSelect(value);

View File

@@ -6,6 +6,7 @@ import { createStructuredSelector } from "reselect";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import JobWatcherToggleComponent from "./job-watcher-toggle.component.jsx";
import { useIsEmployee } from "../../utils/useIsEmployee.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -21,13 +22,14 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
splitKey: bodyshop && bodyshop.imexshopid
});
const userEmail = currentUser.email;
const jobid = job.id;
const isEmployee = useIsEmployee(bodyshop, currentUser);
const [open, setOpen] = useState(false);
const [selectedWatcher, setSelectedWatcher] = useState(null);
const [selectedTeam, setSelectedTeam] = useState(null);
const userEmail = currentUser.email;
const jobid = job.id;
// Fetch current watchers with refetch capability
const {
data: watcherData,
@@ -139,13 +141,13 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
});
const handleToggleSelf = useCallback(async () => {
if (adding || removing) return;
if (adding || removing || !isEmployee) return;
if (isWatching) {
await removeWatcher({ variables: { jobid, userEmail } });
} else {
await addWatcher({ variables: { jobid, userEmail } });
}
}, [isWatching, addWatcher, removeWatcher, jobid, userEmail, adding, removing]);
}, [isWatching, addWatcher, removeWatcher, jobid, userEmail, adding, removing, isEmployee]);
const handleRemoveWatcher = useCallback(
async (email) => {
@@ -187,7 +189,16 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
setSelectedTeam(null);
return;
}
await Promise.all(newWatchers.map((email) => addWatcher({ variables: { jobid, userEmail: email } })));
await Promise.all(
newWatchers.map((email) =>
addWatcher({
variables: {
jobid,
userEmail: email
}
})
)
);
},
[jobWatchers, addWatcher, jobid, adding]
);
@@ -212,6 +223,7 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
handleWatcherSelect={handleWatcherSelect}
handleTeamSelect={handleTeamSelect}
currentUser={currentUser}
isEmployee={isEmployee} // Pass isEmployee to the component
/>
);
}

View File

@@ -106,7 +106,12 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
<Form.Item label={t("jobs.fields.date_open")} name="date_open">
<DateTimePicker />
</Form.Item>
<Form.Item label={t("jobs.fields.estimate_sent_approval")} name="estimate_sent_approval">
<DateTimePicker />
</Form.Item>
<Form.Item label={t("jobs.fields.estimate_approved")} name="estimate_approved">
<DateTimePicker />
</Form.Item>
<Form.Item label={t("jobs.fields.date_scheduled")} name="date_scheduled">
<DateTimePicker />
</Form.Item>

View File

@@ -4,11 +4,12 @@ import { Col, Row } from "antd";
import Axios from "axios";
import _ from "lodash";
import queryString from "query-string";
import React, { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { logImEXEvent } from "../../firebase/firebase.utils";
import {
DELETE_AVAILABLE_JOB,
@@ -33,7 +34,6 @@ import OwnerFindModalContainer from "../owner-find-modal/owner-find-modal.contai
import { GetSupplementDelta } from "./jobs-available-supplement.estlines.util";
import HeaderFields from "./jobs-available-supplement.headerfields";
import JobsAvailableTableComponent from "./jobs-available-table.component";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -195,7 +195,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
await deleteJob({
variables: { id: estData.id }
}).then((r) => {
}).then(() => {
refetch();
setInsertLoading(false);
});
@@ -315,7 +315,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
deleteJob({
variables: { id: estData.id }
}).then((r) => {
}).then(() => {
refetch();
setInsertLoading(false);
});
@@ -372,7 +372,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
loadEstData({ variables: { id: record.id } });
modalSearchState[1](record.clm_no);
setJobModalVisible(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line
}, []);
useEffect(() => {
@@ -456,7 +456,7 @@ function replaceEmpty(someObj, replaceValue = null) {
return JSON.parse(temp);
}
async function CheckTaxRatesUSA(estData, bodyshop) {
async function CheckTaxRatesUSA(estData) {
if (!estData.parts_tax_rates?.PAM) {
estData.parts_tax_rates.PAM = estData.parts_tax_rates.PAC;
}
@@ -568,7 +568,7 @@ async function CheckTaxRates(estData, bodyshop) {
});
//}
}
function ResolveCCCLineIssues(estData, bodyshop) {
function ResolveCCCLineIssues(estData) {
//Find all misc amounts, populate them to the act price.
//This needs to be done before cleansing unq_seq since some misc prices could move over.
estData.joblines.data.forEach((line) => {
@@ -585,6 +585,9 @@ function ResolveCCCLineIssues(estData, bodyshop) {
// line.notes += ` | ET/UT Update (prev = ${line.mod_lbr_ty})`;
line.mod_lbr_ty = "LAR";
}
if (line.mod_lbr_ty === "OTSL") {
line.mod_lbr_ty = line.mod_lbr_hrs === 0 ? null : "LAB";
}
}
});
});

View File

@@ -7,6 +7,7 @@ import { selectJobReadOnly } from "../../redux/application/application.selectors
import { selectBodyshop } from "../../redux/user/user.selectors";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import FormRow from "../layout-form-row/layout-form-row.component";
import dayjs from "../../utils/day";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
@@ -40,6 +41,20 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
<Form.Item label={t("jobs.fields.date_rentalresp")} name="date_rentalresp">
<DateTimePicker disabled={jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.estimate_sent_approval")} name="estimate_sent_approval">
<DateTimePicker
disabled={true}
value={job.estimate_sent_approval ? dayjs(job.estimate_sent_approval) : null}
placeholder={t("general.labels.na")}
/>
</Form.Item>
<Form.Item label={t("jobs.fields.estimate_approved")} name="estimate_approved">
<DateTimePicker
disabled={true}
value={job.estimate_approved ? dayjs(job.estimate_approved) : null}
placeholder={t("general.labels.na")}
/>
</Form.Item>
</FormRow>
<FormRow header={t("jobs.forms.scheddates")}>
@@ -76,21 +91,15 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
<DateTimePicker disabled={jobRO} />
</Form.Item>
<Form.Item shouldUpdate>
{() => {
return (
<Form.Item
label={t("jobs.fields.actual_completion")}
name="actual_completion"
rules={[
{
required: jobInPostProduction
}
]}
>
<DateTimePicker disabled={jobRO} />
</Form.Item>
);
}}
{() => (
<Form.Item
label={t("jobs.fields.actual_completion")}
name="actual_completion"
rules={[{ required: jobInPostProduction }]}
>
<DateTimePicker disabled={jobRO} />
</Form.Item>
)}
</Form.Item>
<Form.Item label={t("jobs.fields.scheduled_delivery")} name="scheduled_delivery">
<DateTimePicker disabled={jobRO} />
@@ -103,15 +112,12 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
<Form.Item label={t("jobs.fields.date_invoiced")} name="date_invoiced">
<DateTimePicker disabled={true || jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.date_exported")} name="date_exported">
<DateTimePicker disabled={true || jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.date_void")} name="date_void">
<DateTimePicker disabled={true || jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.date_lost_sale")} name="date_lost_sale">
<DateTimePicker disabled={true || jobRO} />
</Form.Item>

View File

@@ -1,5 +1,4 @@
import { Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -188,6 +187,12 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
<Form.Item label={t("jobs.fields.tlos_ind")} name="tlos_ind" valuePropName="checked">
<Switch disabled={jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.hit_and_run")} name="hit_and_run" valuePropName="checked">
<Switch disabled={jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.acv_amount")} name="acv_amount">
<CurrencyInput disabled={jobRO} min={0} />
</Form.Item>
</FormRow>
</Col>
<Col {...lossColDamage}>

View File

@@ -9,7 +9,7 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
import { GET_CURRENT_QUESTIONSET_ID, INSERT_CSI } from "../../graphql/csi.queries";
@@ -133,6 +133,16 @@ export function JobsDetailHeaderActions({
const { socket } = useSocket();
const notification = useNotification();
const isDevEnv = import.meta.env.DEV;
const isProdEnv = import.meta.env.PROD;
const userEmail = currentUser?.email || "";
const devEmails = ["imex.dev", "rome.dev"];
const prodEmails = ["imex.prod", "rome.prod", "imex.test", "rome.test"];
const hasValidEmail = (emails) => emails.some((email) => userEmail.endsWith(email));
const canSubmitForTesting = (isDevEnv && hasValidEmail(devEmails)) || (isProdEnv && hasValidEmail(prodEmails));
const {
treatments: { ImEXPay }
} = useSplitTreatments({
@@ -171,7 +181,7 @@ export function JobsDetailHeaderActions({
{ defaultOpenStatus: bodyshop.md_ro_statuses.default_imported },
(newJobId) => {
history(`/manage/jobs/${newJobId}`);
notification["success"]({
notification.success({
message: t("jobs.successes.duplicated")
});
},
@@ -181,7 +191,7 @@ export function JobsDetailHeaderActions({
const handleDuplicateConfirm = () =>
DuplicateJob(client, job.id, { defaultOpenStatus: bodyshop.md_ro_statuses.default_imported }, (newJobId) => {
history(`/manage/jobs/${newJobId}`);
notification["success"]({
notification.success({
message: t("jobs.successes.duplicated")
});
});
@@ -217,13 +227,13 @@ export function JobsDetailHeaderActions({
const result = await deleteJob({ variables: { id: job.id } });
if (!result.errors) {
notification["success"]({
notification.success({
message: t("jobs.successes.delete")
});
//go back to jobs list.
history(`/manage/`);
} else {
notification["error"]({
notification.error({
message: t("jobs.errors.deleted", {
error: JSON.stringify(result.errors)
})
@@ -275,9 +285,9 @@ export function JobsDetailHeaderActions({
});
if (!result.errors) {
notification["success"]({ message: t("csi.successes.created") });
notification.success({ message: t("csi.successes.created") });
} else {
notification["error"]({
notification.error({
message: t("csi.errors.creating", {
message: JSON.stringify(result.errors)
})
@@ -316,7 +326,7 @@ export function JobsDetailHeaderActions({
`${window.location.protocol}//${window.location.host}/csi/${result.data.insert_csi.returning[0].id}`
);
} else {
notification["error"]({
notification.error({
message: t("messaging.error.invalidphone")
});
}
@@ -328,7 +338,7 @@ export function JobsDetailHeaderActions({
);
}
} else {
notification["error"]({
notification.error({
message: t("csi.errors.notconfigured")
});
}
@@ -358,7 +368,7 @@ export function JobsDetailHeaderActions({
});
setMessage(`${window.location.protocol}//${window.location.host}/csi/${job.csiinvites[0].id}`);
} else {
notification["error"]({
notification.error({
message: t("messaging.error.invalidphone")
});
}
@@ -398,7 +408,7 @@ export function JobsDetailHeaderActions({
});
if (!result.errors) {
notification["success"]({
notification.success({
message: t("jobs.successes.voided")
});
insertAuditTrail({
@@ -409,7 +419,7 @@ export function JobsDetailHeaderActions({
//go back to jobs list.
history(`/manage/`);
} else {
notification["error"]({
notification.error({
message: t("jobs.errors.voiding", {
error: JSON.stringify(result.errors)
})
@@ -442,7 +452,7 @@ export function JobsDetailHeaderActions({
console.log("handle -> XML", QbXmlResponse);
} catch (error) {
console.log("Error getting QBXML from Server.", error);
notification["error"]({
notification.error({
message: t("jobs.errors.exporting", {
error: "Unable to retrieve QBXML. " + JSON.stringify(error.message)
})
@@ -460,7 +470,7 @@ export function JobsDetailHeaderActions({
});
} catch (error) {
console.log("Error connecting to quickbooks or partner.", error);
notification["error"]({
notification.error({
message: t("jobs.errors.exporting-partner")
});
@@ -556,7 +566,7 @@ export function JobsDetailHeaderActions({
}
});
if (!jobUpdate.errors) {
notification["success"]({
notification.success({
message: t("appointments.successes.canceled")
});
insertAuditTrail({
@@ -931,11 +941,11 @@ export function JobsDetailHeaderActions({
});
if (!result.errors) {
notification["success"]({
notification.success({
message: t("jobs.successes.partsqueue")
});
} else {
notification["error"]({
notification.error({
message: t("jobs.errors.saving", {
error: JSON.stringify(result.errors)
})
@@ -1111,6 +1121,27 @@ export function JobsDetailHeaderActions({
});
}
if (canSubmitForTesting) {
menuItems.push({
key: "submitfortesting",
id: "job-actions-submitfortesting",
label: t("menus.jobsactions.submit-for-testing"),
onClick: async () => {
try {
await axios.post("/job/totals-recorder", { id: job.id });
notification.success({
message: t("general.messages.submit-for-testing")
});
} catch (err) {
console.error(`Error submitting job for testing: ${err?.message}`);
notification.error({
message: t("general.errors.submit-for-testing-error")
});
}
}
});
}
const menu = {
items: menuItems,
key: "popovermenu"

View File

@@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { GET_JOB_BY_PK_QUICK_INTAKE, JOB_PRODUCTION_TOGGLE } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
@@ -12,7 +13,6 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
const mapStateToProps = createStructuredSelector({
@@ -44,9 +44,16 @@ export function JobsDetailHeaderActionsToggleProduction({
variables: { id: job.id },
onCompleted: (data) => {
if (data?.jobs_by_pk) {
const totalHours =
(data.jobs_by_pk.labhrs?.aggregate?.sum?.mod_lb_hrs || 0) +
(data.jobs_by_pk.larhrs?.aggregate?.sum?.mod_lb_hrs || 0);
form.setFieldsValue({
actual_in: data.jobs_by_pk.actual_in ? data.jobs_by_pk.actual_in : dayjs(),
scheduled_completion: data.jobs_by_pk.scheduled_completion,
scheduled_completion: data.jobs_by_pk.scheduled_completion
? data.jobs_by_pk.scheduled_completion
: totalHours && bodyshop.ss_configuration.nobusinessdays
? dayjs().businessDaysAdd(totalHours / (bodyshop.target_touchtime || 1), "day")
: dayjs().add(totalHours / (bodyshop.target_touchtime || 1), "day"),
actual_completion: data.jobs_by_pk.actual_completion,
scheduled_delivery: data.jobs_by_pk.scheduled_delivery,
actual_delivery: data.jobs_by_pk.actual_delivery

View File

@@ -1,15 +1,18 @@
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, WarningFilled } from "@ant-design/icons";
import { Card, Col, Divider, Row, Space, Tag, Tooltip } from "antd";
import React, { useState } from "react";
import { Card, Checkbox, Col, Divider, Row, Space, Tag, Tooltip } from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { useMutation } from "@apollo/client";
import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import dayjs from "../../utils/day";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import DataLabel from "../data-label/data-label.component";
@@ -21,7 +24,7 @@ import ProductionListColumnComment from "../production-list-columns/production-l
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
import "./jobs-detail-header.styles.scss";
import dayjs from "../../utils/day";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
@@ -29,41 +32,55 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
setPrintCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "printCenter" }))
setPrintCenterContext: (context) =>
dispatch(
setModalContext({
context: context,
modal: "printCenter"
})
)
});
const colSpan = {
xs: {
span: 24
},
sm: {
span: 24
},
md: {
span: 12
},
lg: {
span: 6
},
xl: {
span: 6
}
xs: { span: 24 },
sm: { span: 24 },
md: { span: 12 },
lg: { span: 6 },
xl: { span: 6 }
};
export function JobsDetailHeader({ job, bodyshop, disabled }) {
const { t } = useTranslation();
const { notification } = useNotification();
const [notesClamped, setNotesClamped] = useState(true);
const vehicleTitle = `${job.v_model_yr || ""} ${job.v_color || ""}
${job.v_make_desc || ""}
${job.v_model_desc || ""}`.trim();
const [updateJob] = useMutation(UPDATE_JOB);
const vehicleTitle =
`${job.v_model_yr || ""} ${job.v_color || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""}`.trim();
const bodyHrs = job.joblines.filter((j) => j.mod_lbr_ty !== "LAR").reduce((acc, val) => acc + val.mod_lb_hrs, 0);
const refinishHrs = job.joblines
.filter((line) => line.mod_lbr_ty === "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0);
const ownerTitle = OwnerNameDisplayFunction(job).trim();
// Handle checkbox changes
const handleCheckboxChange = async (field, checked) => {
const value = checked ? dayjs().toISOString() : null;
try {
await updateJob({
variables: {
jobId: job.id,
job: { [field]: value }
},
refetchQueries: ["GET_JOB_BY_PK"],
awaitRefetchQueries: true
});
} catch (error) {
notification.error({
message: t("jobs.errors.saving", { error: error.message })
});
}
};
return (
<Row gutter={[16, 16]} style={{ alignItems: "stretch" }}>
<Col {...colSpan}>
@@ -72,11 +89,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
<DataLabel label={t("jobs.fields.status")}>
<Space wrap>
{job.status}
{job.inproduction && (
<Tag color="#f50" key="production">
{t("jobs.labels.inproduction")}
</Tag>
)}
{job.inproduction && <Tag color="#f50">{t("jobs.labels.inproduction")}</Tag>}
{job.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
{job.iouparent && (
<Link to={`/manage/jobs/${job.iouparent}`}>
@@ -110,7 +123,6 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
<span style={{ margin: "0rem .5rem" }}>/</span>
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
</DataLabel>
<DataLabel label={t("jobs.fields.alt_transport")}>
{job.alt_transport}
<JobAltTransportChange job={job} />
@@ -127,11 +139,39 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
))}
</DataLabel>
)}
<DataLabel label={t("jobs.fields.production_vars.note")}>
<ProductionListColumnProductionNote record={job} />
</DataLabel>
<DataLabel label={t("jobs.fields.estimate_sent_approval")}>
<Space>
<Checkbox
checked={!!job.estimate_sent_approval}
onChange={(e) => handleCheckboxChange("estimate_sent_approval", e.target.checked)}
disabled={disabled}
>
{job.estimate_sent_approval && (
<span style={{ color: "#888" }}>
<DateTimeFormatter>{job.estimate_sent_approval}</DateTimeFormatter>
</span>
)}
</Checkbox>
</Space>
</DataLabel>
<DataLabel label={t("jobs.fields.estimate_approved")}>
<Space>
<Checkbox
checked={!!job.estimate_approved}
onChange={(e) => handleCheckboxChange("estimate_approved", e.target.checked)}
disabled={disabled}
>
{job.estimate_approved && (
<span style={{ color: "#888" }}>
<DateTimeFormatter>{job.estimate_approved}</DateTimeFormatter>
</span>
)}
</Checkbox>
</Space>
</DataLabel>
<Space wrap>
{job.special_coverage_policy && (
<Tag color="tomato">
@@ -149,6 +189,14 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
</Space>
</Tag>
)}
{job.hit_and_run && (
<Tag color="green">
<Space>
<WarningFilled />
<span>{t("jobs.fields.hit_and_run")}</span>
</Space>
</Tag>
)}
</Space>
</div>
</Card>

View File

@@ -1,12 +1,10 @@
import { Button, Space } from "antd";
import axios from "axios";
import React, { useState } from "react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
import cleanAxios from "../../utils/CleanAxios";
import formatBytes from "../../utils/formatbytes";
//import yauzl from "yauzl";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
@@ -28,7 +26,7 @@ const mapDispatchToProps = (dispatch) => ({
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyDownloadButton);
export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, identifier }) {
export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, identifier, jobId }) {
const { t } = useTranslation();
const [download, setDownload] = useState(null);
const [loading, setLoading] = useState(false);
@@ -46,6 +44,7 @@ export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, i
};
});
}
function standardMediaDownload(bufferData) {
const a = document.createElement("a");
const url = window.URL.createObjectURL(new Blob([bufferData]));
@@ -53,13 +52,14 @@ export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, i
a.download = `${identifier || "documents"}.zip`;
a.click();
}
const handleDownload = async () => {
logImEXEvent("jobs_documents_download");
setLoading(true);
const zipUrl = await axios({
url: "/media/imgproxy/download",
method: "POST",
data: { documentids: imagesToDownload.map((_) => _.id) }
data: { jobId, documentids: imagesToDownload.map((_) => _.id) }
});
const theDownloadedZip = await cleanAxios({

View File

@@ -75,7 +75,7 @@ function JobsDocumentsImgproxyComponent({
<SyncOutlined />
</Button>
<JobsDocumentsGallerySelectAllComponent galleryImages={galleryImages} setGalleryImages={setGalleryImages} />
<JobsDocumentsDownloadButton galleryImages={galleryImages} identifier={downloadIdentifier} />
<JobsDocumentsDownloadButton galleryImages={galleryImages} identifier={downloadIdentifier} jobId={jobId} />
<JobsDocumentsDeleteButton
galleryImages={galleryImages}
deletionCallback={billsCallback || fetchThumbnails || refetch}

View File

@@ -65,7 +65,7 @@ export default connect(
<Modal
title={t("jobs.labels.existing_jobs")}
width={"80%"}
destroyOnClose
destroyOnHidden
okButtonProps={{ disabled: selectedJob ? false : true }}
{...modalProps}
>

View File

@@ -20,7 +20,14 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("noteUpsert")),
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(
insertAuditTrail({
jobid,
operation,
type
})
)
});
export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleModalVisible, insertAuditTrail }) {
@@ -123,7 +130,7 @@ export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleM
onCancel={() => {
toggleModalVisible();
}}
destroyOnClose
destroyOnHidden
>
<Form form={form} onFinish={handleFinish} layout="vertical">
<NoteUpsertModalComponent form={form} />

View File

@@ -1,11 +1,11 @@
import { Virtuoso } from "react-virtuoso";
import { Badge, Button, Space, Spin, Switch, Tooltip, Typography } from "antd";
import { Alert, Badge, Button, Space, Spin, Switch, Tooltip, Typography } from "antd";
import { CheckCircleFilled, CheckCircleOutlined, EyeFilled, EyeOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import "./notification-center.styles.scss";
import day from "../../utils/day.js";
import { forwardRef, useRef, useEffect } from "react";
import { forwardRef, useEffect, useRef } from "react";
import { DateTimeFormat } from "../../utils/DateFormatter.jsx";
const { Text, Title } = Typography;
@@ -26,7 +26,8 @@ const NotificationCenterComponent = forwardRef(
markAllRead,
loadMore,
onNotificationClick,
unreadCount
unreadCount,
isEmployee
},
ref
) => {
@@ -93,7 +94,12 @@ const NotificationCenterComponent = forwardRef(
) : (
<EyeOutlined className="notification-toggle-icon" />
)}
<Switch checked={showUnreadOnly} onChange={(checked) => toggleUnreadOnly(checked)} size="small" />
<Switch
checked={showUnreadOnly}
onChange={(checked) => toggleUnreadOnly(checked)}
size="small"
disabled={!isEmployee}
/>
</Space>
</Tooltip>
<Tooltip title={t("notifications.labels.mark-all-read")}>
@@ -106,14 +112,20 @@ const NotificationCenterComponent = forwardRef(
</Tooltip>
</div>
</div>
<Virtuoso
ref={virtuosoRef}
style={{ height: "400px", width: "100%" }}
data={notifications}
totalCount={notifications.length}
endReached={loadMore}
itemContent={renderNotification}
/>
{!isEmployee ? (
<div style={{ padding: 10 }}>
<Alert message={t("notifications.labels.employee-notification")} type="warning" />
</div>
) : (
<Virtuoso
ref={virtuosoRef}
style={{ height: "400px", width: "100%" }}
data={notifications}
totalCount={notifications.length}
endReached={loadMore}
itemContent={renderNotification}
/>
)}
</div>
);
}

View File

@@ -3,10 +3,11 @@ import { useQuery } from "@apollo/client";
import { connect } from "react-redux";
import NotificationCenterComponent from "./notification-center.component";
import { GET_NOTIFICATIONS } from "../../graphql/notifications.queries";
import { INITIAL_NOTIFICATIONS, useSocket } from "../../contexts/SocketIO/useSocket.jsx";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors.js";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
import day from "../../utils/day.js";
import { INITIAL_NOTIFICATIONS, useSocket } from "../../contexts/SocketIO/useSocket.js";
import { useIsEmployee } from "../../utils/useIsEmployee.js";
// This will be used to poll for notifications when the socket is disconnected
const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
@@ -17,17 +18,18 @@ const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
* @param onClose
* @param bodyshop
* @param unreadCount
* @param currentUser
* @returns {JSX.Element}
* @constructor
*/
const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }) => {
const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount, currentUser }) => {
const [showUnreadOnly, setShowUnreadOnly] = useState(false);
const [notifications, setNotifications] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const { isConnected, markNotificationRead, markAllNotificationsRead } = useSocket();
const notificationRef = useRef(null);
const userAssociationId = bodyshop?.associations?.[0]?.id;
const isEmployee = useIsEmployee(bodyshop, currentUser);
const baseWhereClause = useMemo(() => {
return { associationid: { _eq: userAssociationId } };
@@ -51,7 +53,7 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
fetchPolicy: "cache-and-network",
notifyOnNetworkStatusChange: true,
pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(),
skip: !userAssociationId,
skip: !userAssociationId || !isEmployee,
onError: (err) => {
console.error(`Error polling Notifications: ${err?.message || ""}`);
setTimeout(() => refetch(), day.duration(2, "seconds").asMilliseconds());
@@ -71,7 +73,7 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
}, [visible, onClose]);
useEffect(() => {
if (data?.notifications) {
if (data?.notifications && isEmployee) {
const processedNotifications = data.notifications
.map((notif) => {
let scenarioText;
@@ -101,11 +103,13 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
})
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
setNotifications(processedNotifications);
} else if (!isEmployee) {
setNotifications([]); // Clear notifications if not an employee
}
}, [data]);
}, [data, isEmployee]);
const loadMore = useCallback(() => {
if (!queryLoading && data?.notifications.length) {
if (!queryLoading && data?.notifications.length && isEmployee) {
setIsLoading(true); // Show spinner during fetchMore
fetchMore({
variables: { offset: data.notifications.length, where: whereClause },
@@ -121,13 +125,14 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
})
.finally(() => setIsLoading(false)); // Hide spinner when done
}
}, [data?.notifications?.length, fetchMore, queryLoading, whereClause]);
}, [data?.notifications?.length, fetchMore, queryLoading, whereClause, isEmployee]);
const handleToggleUnreadOnly = (value) => {
setShowUnreadOnly(value);
};
const handleMarkAllRead = useCallback(() => {
if (!isEmployee) return; // Do nothing if not an employee
setIsLoading(true);
markAllNotificationsRead()
.then(() => {
@@ -147,7 +152,7 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
})
.catch((e) => console.error(`Error marking all notifications read: ${e?.message || ""}`))
.finally(() => setIsLoading(false));
}, [markAllNotificationsRead, userAssociationId, showUnreadOnly]);
}, [markAllNotificationsRead, userAssociationId, showUnreadOnly, isEmployee]);
const handleNotificationClick = useCallback(
(notificationId) => {
@@ -170,17 +175,18 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
);
useEffect(() => {
if (visible && !isConnected) {
if (visible && !isConnected && isEmployee) {
setIsLoading(true);
refetch()
.catch((err) => console.error(`Error re-fetching notifications: ${err?.message || ""}`))
.finally(() => setIsLoading(false));
}
}, [visible, isConnected, refetch]);
}, [visible, isConnected, refetch, isEmployee]);
return (
<NotificationCenterComponent
ref={notificationRef}
isEmployee={isEmployee}
visible={visible}
onClose={onClose}
notifications={notifications}
@@ -196,7 +202,8 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
};
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
bodyshop: selectBodyshop,
currentUser: selectCurrentUser
});
export default connect(mapStateToProps, null)(NotificationCenterContainer);

View File

@@ -1,32 +1,41 @@
import { useMutation, useQuery } from "@apollo/client";
import { useEffect, useState } from "react";
import { Button, Card, Checkbox, Form, Space, Table } from "antd";
import { Alert, Button, Card, Checkbox, Divider, Form, Space, Switch, Table, Typography } from "antd";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectCurrentUser } from "../../redux/user/user.selectors";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import { QUERY_NOTIFICATION_SETTINGS, UPDATE_NOTIFICATION_SETTINGS } from "../../graphql/user.queries.js";
import {
QUERY_NOTIFICATION_SETTINGS,
UPDATE_NOTIFICATION_SETTINGS,
UPDATE_NOTIFICATIONS_AUTOADD
} from "../../graphql/user.queries.js";
import { notificationScenarios } from "../../utils/jobNotificationScenarios.js";
import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
import PropTypes from "prop-types";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import ColumnHeaderCheckbox from "../notification-settings/column-header-checkbox.component.jsx";
import { useIsEmployee } from "../../utils/useIsEmployee.js";
/**
* Notifications Settings Form
* @param currentUser
* @param bodyshop
* @returns {JSX.Element}
* @constructor
*/
const NotificationSettingsForm = ({ currentUser }) => {
const NotificationSettingsForm = ({ currentUser, bodyshop }) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const [initialValues, setInitialValues] = useState({});
const [isDirty, setIsDirty] = useState(false);
const [autoAddEnabled, setAutoAddEnabled] = useState(false);
const [initialAutoAdd, setInitialAutoAdd] = useState(false);
const notification = useNotification();
const isEmployee = useIsEmployee(bodyshop, currentUser);
// Fetch notification settings.
// Fetch notification settings and notifications_autoadd
const { loading, error, data } = useQuery(QUERY_NOTIFICATION_SETTINGS, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
@@ -34,13 +43,16 @@ const NotificationSettingsForm = ({ currentUser }) => {
skip: !currentUser
});
const [updateNotificationSettings, { loading: saving }] = useMutation(UPDATE_NOTIFICATION_SETTINGS);
const [updateNotificationSettings, { loading: savingSettings }] = useMutation(UPDATE_NOTIFICATION_SETTINGS);
const [updateNotificationsAutoAdd, { loading: savingAutoAdd }] = useMutation(UPDATE_NOTIFICATIONS_AUTOADD);
// Populate form with fetched data.
// Populate form with fetched data
useEffect(() => {
if (data?.associations?.length > 0) {
const settings = data.associations[0].notification_settings || {};
// Ensure each scenario has an object with { app, email, fcm }.
const autoAdd = data.associations[0].notifications_autoadd ?? false;
// Ensure each scenario has an object with { app, email, fcm }
const formattedValues = notificationScenarios.reduce((acc, scenario) => {
acc[scenario] = settings[scenario] ?? { app: false, email: false, fcm: false };
return acc;
@@ -48,32 +60,66 @@ const NotificationSettingsForm = ({ currentUser }) => {
setInitialValues(formattedValues);
form.setFieldsValue(formattedValues);
setIsDirty(false); // Reset dirty state when new data loads.
setAutoAddEnabled(autoAdd);
setInitialAutoAdd(autoAdd);
setIsDirty(false); // Reset dirty state when new data loads
}
}, [data, form]);
// Handle toggle of notifications_autoadd
const handleAutoAddToggle = async (checked) => {
if (data?.associations?.length > 0) {
const userId = data.associations[0].id;
try {
const result = await updateNotificationsAutoAdd({
variables: { id: userId, autoadd: checked }
});
if (!result?.errors) {
setAutoAddEnabled(checked);
setInitialAutoAdd(checked);
notification.success({ message: t("notifications.labels.auto-add-success") });
setIsDirty(false); // Reset dirty state if only auto-add was changed
} else {
throw new Error("Failed to update auto-add setting");
}
} catch (err) {
setAutoAddEnabled(!checked); // Revert on error
notification.error({ message: t("notifications.labels.auto-add-failure") });
}
}
};
// Handle save of notification settings
const handleSave = async (values) => {
if (data?.associations?.length > 0) {
const userId = data.associations[0].id;
// Save the updated notification settings.
const result = await updateNotificationSettings({ variables: { id: userId, ns: values } });
if (!result?.errors) {
notification.success({ message: t("notifications.labels.notification-settings-success") });
setInitialValues(values);
setIsDirty(false);
} else {
try {
const result = await updateNotificationSettings({ variables: { id: userId, ns: values } });
if (!result?.errors) {
notification.success({ message: t("notifications.labels.notification-settings-success") });
setInitialValues(values);
setIsDirty(false);
} else {
throw new Error("Failed to update notification settings");
}
} catch (err) {
notification.error({ message: t("notifications.labels.notification-settings-failure") });
}
}
};
// Mark the form as dirty on any manual change.
// Mark the form as dirty on any manual change
const handleFormChange = () => {
setIsDirty(true);
};
// Check if auto-add has changed
const isAutoAddDirty = autoAddEnabled !== initialAutoAdd;
// Handle reset of form and auto-add
const handleReset = () => {
form.setFieldsValue(initialValues);
setAutoAddEnabled(initialAutoAdd);
setIsDirty(false);
};
@@ -139,17 +185,30 @@ const NotificationSettingsForm = ({ currentUser }) => {
title={t("notifications.labels.notificationscenarios")}
extra={
<Space>
<Button type="default" onClick={handleReset} disabled={!isDirty}>
<Typography.Text type="secondary">{t("notifications.labels.auto-add")}</Typography.Text>
<Switch
checked={autoAddEnabled}
onChange={handleAutoAddToggle}
loading={savingAutoAdd}
// checkedChildren={t("notifications.labels.auto-add-on")}
// unCheckedChildren={t("notifications.labels.auto-add-off")}
/>
<Button type="default" onClick={handleReset} disabled={!isDirty && !isAutoAddDirty}>
{t("general.actions.clear")}
</Button>
<Button type="primary" htmlType="submit" disabled={!isDirty} loading={saving}>
<Button type="primary" htmlType="submit" disabled={!isDirty} loading={savingSettings}>
{t("notifications.labels.save")}
</Button>
</Space>
}
>
{!isEmployee && (
<div style={{ width: "100%", marginBottom: "10px" }}>
<Alert message={t("notifications.labels.employee-notification")} type="warning" />
</div>
)}
<Table dataSource={dataSource} columns={columns} pagination={false} bordered rowKey="key" />
<Divider />
</Card>
</Form>
);
@@ -158,11 +217,13 @@ const NotificationSettingsForm = ({ currentUser }) => {
NotificationSettingsForm.propTypes = {
currentUser: PropTypes.shape({
email: PropTypes.string.isRequired
}).isRequired
}).isRequired,
bodyshop: PropTypes.object.isRequired
};
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser
currentUser: selectCurrentUser,
bodyshop: selectBodyshop
});
export default connect(mapStateToProps)(NotificationSettingsForm);

View File

@@ -333,7 +333,7 @@ export function PartsOrderModalContainer({
onOk={() => form.submit()}
okButtonProps={{ loading: saving }}
cancelButtonProps={{ loading: saving }}
destroyOnClose
destroyOnHidden
width="75%"
forceRender
>

View File

@@ -46,7 +46,7 @@ export default function PartsQueueDetailCard() {
};
return (
<Drawer open={!!selected} destroyOnClose width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
<Drawer open={!!selected} destroyOnHidden width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
{loading ? <LoadingSpinner /> : null}
{error ? <AlertComponent message={error.message} type="error" /> : null}
{data ? (

View File

@@ -90,7 +90,7 @@ export function PartsReceiveModalContainer({ partsReceiveModal, toggleModalVisib
onCancel={() => toggleModalVisible()}
onOk={() => form.submit()}
okButtonProps={{ loading: loading }}
destroyOnClose
destroyOnHidden
forceRender
width="50%"
>

View File

@@ -134,7 +134,7 @@ function PaymentModalContainer({ paymentModal, toggleModalVisible, bodyshop }) {
<Modal
title={!context || (context && !context.id) ? t("payments.labels.new") : t("payments.labels.edit")}
open={open}
destroyOnClose
destroyOnHidden
okText={t("general.actions.save")}
onOk={() => form.submit()}
width="50%"

View File

@@ -10,7 +10,7 @@ import { createStructuredSelector } from "reselect";
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,

View File

@@ -0,0 +1,62 @@
import { useQuery } from "@apollo/client";
import { Input, Table } from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import { GET_PHONE_NUMBER_OPT_OUTS } from "../../graphql/phone-number-opt-out.queries";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import { TimeAgoFormatter } from "../../utils/DateFormatter";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser
});
const mapDispatchToProps = () => ({});
function PhoneNumberConsentList({ bodyshop, currentUser }) {
const { t } = useTranslation();
const [search, setSearch] = useState("");
const { loading, data } = useQuery(GET_PHONE_NUMBER_OPT_OUTS, {
variables: { bodyshopid: bodyshop.id, search: search ? `%${search}%` : undefined },
fetchPolicy: "network-only"
});
const columns = [
{
title: t("consent.phone_number"),
dataIndex: "phone_number",
render: (text) => <PhoneNumberFormatter>{text}</PhoneNumberFormatter>,
sorter: (a, b) => a.phone_number.localeCompare(b.phone_number)
},
{
title: t("consent.created_at"),
dataIndex: "created_at",
render: (text) => <TimeAgoFormatter>{text}</TimeAgoFormatter>,
sorter: (a, b) => new Date(a.created_at) - new Date(b.created_at)
}
];
return (
<div>
<Input.Search
placeholder={t("general.labels.search")}
onSearch={(value) => setSearch(value)}
style={{ marginBottom: 16 }}
/>
<Table
columns={columns}
dataSource={data?.phone_number_opt_out}
loading={loading}
rowKey="id"
style={{ marginTop: 16 }}
/>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(PhoneNumberConsentList);

View File

@@ -32,7 +32,7 @@ export function PrintCenterModalContainer({ printCenterModal, toggleModalVisible
okText={t("general.actions.close")}
width="90%"
title={t("printcenter.labels.title")}
destroyOnClose
destroyOnHidden
>
<PrintCenterModalComponent context={context} />
</Modal>

View File

@@ -12,7 +12,7 @@ import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import ProductionBoardKanbanComponent from "./production-board-kanban.component";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,

View File

@@ -1,7 +1,6 @@
import React from "react";
import { Card, Form, Select } from "antd";
import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
const FilterSettings = ({
selectedMdInsCos,

View File

@@ -1,10 +1,9 @@
import { Card, Checkbox, Col, Form, Row } from "antd";
import React from "react";
import PropTypes from "prop-types";
const InformationSettings = ({ t }) => (
<Card title={t("production.settings.information")}>
<Row gutter={[16, 16]}>
<Card title={t("production.settings.information")} style={{ maxWidth: "100%", overflowX: "auto" }}>
<Row gutter={[16, 16]} wrap>
{[
"model_info",
"ownr_nm",
@@ -21,7 +20,7 @@ const InformationSettings = ({ t }) => (
"subtotal",
"tasks"
].map((item) => (
<Col span={4} key={item}>
<Col xs={24} sm={12} md={8} lg={6} key={item}>
<Form.Item name={item} valuePropName="checked">
<Checkbox>{t(`production.labels.${item}`)}</Checkbox>
</Form.Item>

View File

@@ -1,9 +1,16 @@
import { Card, Col, Form, Radio, Row } from "antd";
import React from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../../redux/user/user.selectors";
import { HasFeatureAccess } from "../../feature-wrapper/feature-wrapper.component";
const LayoutSettings = ({ t }) => (
<Card title={t("production.settings.layout")}>
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const LayoutSettings = ({ t, bodyshop }) => (
<Card title={t("production.settings.layout")} style={{ maxWidth: "100%", overflowX: "auto" }}>
<Row gutter={[16, 16]}>
{[
{
@@ -31,14 +38,18 @@ const LayoutSettings = ({ t }) => (
{ value: false, label: t("production.labels.wide") }
]
},
{
name: "cardcolor",
label: t("production.labels.cardcolor"),
options: [
{ value: true, label: t("production.labels.on") },
{ value: false, label: t("production.labels.off") }
]
},
...(HasFeatureAccess({ bodyshop, featureName: "smartscheduling" })
? [
{
name: "cardcolor",
label: t("production.labels.cardcolor"),
options: [
{ value: true, label: t("production.labels.on") },
{ value: false, label: t("production.labels.off") }
]
}
]
: []),
{
name: "kiosk",
label: t("production.labels.kiosk_mode"),
@@ -48,9 +59,9 @@ const LayoutSettings = ({ t }) => (
]
}
].map(({ name, label, options }) => (
<Col span={4} key={name}>
<Col xs={24} sm={16} md={10} lg={8} key={name}>
<Form.Item name={name} label={label}>
<Radio.Group>
<Radio.Group style={{ display: "flex", flexWrap: "nowrap" }}>
{options.map((option) => (
<Radio.Button key={option.value.toString()} value={option.value}>
{option.label}
@@ -68,4 +79,4 @@ LayoutSettings.propTypes = {
t: PropTypes.func.isRequired
};
export default LayoutSettings;
export default connect(mapStateToProps)(LayoutSettings);

View File

@@ -1,8 +1,7 @@
import { Card, Checkbox, Form } from "antd";
import PropTypes from "prop-types";
import { DragDropContext, Draggable, Droppable } from "../trello-board/dnd/lib/index.js";
import { statisticsItems } from "./defaultKanbanSettings.js";
import { Card, Checkbox, Form } from "antd";
import React from "react";
import PropTypes from "prop-types";
const StatisticsSettings = ({ t, statisticsOrder, setStatisticsOrder, setHasChanges }) => {
const onDragEnd = (result) => {

View File

@@ -1,17 +1,17 @@
import { SettingOutlined } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, Card, Col, Form, Popover, Row, Tabs } from "antd";
import { isFunction } from "lodash";
import PropTypes from "prop-types";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNotification } from "../../../contexts/Notifications/notificationContext.jsx";
import { UPDATE_KANBAN_SETTINGS } from "../../../graphql/user.queries.js";
import { defaultKanbanSettings, mergeWithDefaults } from "./defaultKanbanSettings.js";
import LayoutSettings from "./LayoutSettings.jsx";
import InformationSettings from "./InformationSettings.jsx";
import StatisticsSettings from "./StatisticsSettings.jsx";
import FilterSettings from "./FilterSettings.jsx";
import PropTypes from "prop-types";
import { isFunction } from "lodash";
import { useNotification } from "../../../contexts/Notifications/notificationContext.jsx";
import { SettingOutlined } from "@ant-design/icons";
import InformationSettings from "./InformationSettings.jsx";
import LayoutSettings from "./LayoutSettings.jsx";
import StatisticsSettings from "./StatisticsSettings.jsx";
function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bodyshop, data, onSettingsChange }) {
const [form] = Form.useForm();
@@ -87,7 +87,7 @@ function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bod
};
const overlay = (
<Card style={{ minWidth: "80vw" }}>
<Card style={{ maxWidth: "80vw", width: "100%"}}>
<Form form={form} onFinish={handleFinish} layout="vertical" onValuesChange={handleValuesChange}>
<Tabs
defaultActiveKey="1"

View File

@@ -100,26 +100,28 @@ const BoardContainer = ({
const onLaneDrag = useCallback(
async ({ draggableId, type, source, reason, mode, destination, combine }) => {
setIsDragging(false);
setDragTime(source.droppableId);
if (!type || type !== "lane" || !source || !destination || isEqual(source, destination)) return;
setIsProcessing(true);
// Only update drag time if it's a valid drop with a different destination
if (type === "lane" && source && destination && !isEqual(source, destination)) {
setDragTime(source.droppableId);
setIsProcessing(true);
dispatch(
actions.moveCardAcrossLanes({
fromLaneId: source.droppableId,
toLaneId: destination.droppableId,
cardId: draggableId,
index: destination.index
})
);
dispatch(
actions.moveCardAcrossLanes({
fromLaneId: source.droppableId,
toLaneId: destination.droppableId,
cardId: draggableId,
index: destination.index
})
);
try {
await onDragEnd({ draggableId, type, source, reason, mode, destination, combine });
} catch (err) {
console.error("Error in onLaneDrag", err);
} finally {
setIsProcessing(false);
try {
await onDragEnd({ draggableId, type, source, reason, mode, destination, combine });
} catch (err) {
console.error("Error in onLaneDrag", err);
} finally {
setIsProcessing(false);
}
}
},
[dispatch, onDragEnd, setDragTime]

View File

@@ -120,15 +120,14 @@ const Lane = ({
const Component = orientation === "vertical" ? VirtuosoGrid : Virtuoso;
const FinalComponent = collapsed ? "div" : Component;
const commonProps = {
useWindowScroll: true,
data: renderedCards
data: renderedCards,
customScrollParent: laneRef.current
};
const verticalProps = {
...commonProps,
listClassName: "grid-container",
itemClassName: "grid-item",
customScrollParent: laneRef.current,
components: {
List: ListComponent,
Item: ItemComponent
@@ -142,7 +141,6 @@ const Lane = ({
components: { Item: HeightPreservingItem },
overscan: { main: 3, reverse: 3 },
itemContent: (index, item) => renderDraggable(index, item),
scrollerRef: provided.innerRef,
style: {
minWidth: maxCardWidth,
minHeight: maxLaneHeight
@@ -180,13 +178,14 @@ const Lane = ({
override={orientation !== "horizontal" && (collapsed || !renderedCards.length)}
>
<div
{...provided.droppableProps}
ref={provided.innerRef}
ref={laneRef} // Ensure laneRef is set here
style={{ height: "100%", width: "100%" }} // Make it scrollable
className={`react-trello-lane ${collapsed ? "lane-collapsed" : ""}`}
style={{ ...provided.droppableProps.style }}
>
<FinalComponent {...finalComponentProps} />
{shouldRenderPlaceholder && provided.placeholder}
<div {...provided.droppableProps} ref={provided.innerRef} style={{ ...provided.droppableProps.style }}>
<FinalComponent {...finalComponentProps} />
{shouldRenderPlaceholder && provided.placeholder}
</div>
</div>
</HeightMemoryWrapper>
);

View File

@@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
import { logImEXEvent } from "../../firebase/firebase.utils.js";
import { QUERY_JOB_CARD_DETAILS, UPDATE_JOB } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions.js";

View File

@@ -10,7 +10,7 @@ import {
import ProductionListTable from "./production-list-table.component";
import _ from "lodash";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
export default function ProductionListTableContainer({ bodyshop, subscriptionType = "direct" }) {
const client = useApolloClient();

View File

@@ -8,7 +8,7 @@ import { selectCurrentUser } from "../../redux/user/user.selectors";
import { logImEXEvent, updateCurrentPassword } from "../../firebase/firebase.utils";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
import NotificationSettingsForm from "../notification-settings/notification-settings-form.component.jsx";
const mapStateToProps = createStructuredSelector({

View File

@@ -54,6 +54,9 @@ export function ProfileShopsContainer({ bodyshop, currentUser }) {
//Force window refresh.
//Ping the new partner to refresh.
axios.post("http://localhost:1337/refresh");
window.location.reload();
};

View File

@@ -6,6 +6,7 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { QUERY_ACTIVE_EMPLOYEES, QUERY_ACTIVE_EMPLOYEES_WITH_EMAIL } from "../../graphql/employees.queries";
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
import { selectReportCenter } from "../../redux/modals/modals.selectors";
@@ -18,11 +19,10 @@ import EmployeeSearchSelectEmail from "../employee-search-select/employee-search
import EmployeeSearchSelect from "../employee-search-select/employee-search-select.component";
import BlurWrapperComponent from "../feature-wrapper/blur-wrapper.component";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component";
import "./report-center-modal.styles.scss";
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
const mapStateToProps = createStructuredSelector({
reportCenterModal: selectReportCenter,
@@ -389,5 +389,7 @@ const restrictedReports = [
{ key: "job_costing_ro_date_detail", days: 183 },
{ key: "job_costing_ro_estimator", days: 183 },
{ key: "job_lifecycle_date_detail", days: 183 },
{ key: "job_lifecycle_date_summary", days: 183 }
{ key: "job_lifecycle_date_summary", days: 183 },
{ key: "customer_list", days: 183 },
{ key: "customer_list_excel", days: 183 }
];

View File

@@ -28,7 +28,7 @@ export function ReportCenterModalContainer({ reportCenterModal, toggleModalVisib
onOk={() => toggleModalVisible()}
onCancel={() => toggleModalVisible()}
cancelButtonProps={{ style: { display: "none" } }}
destroyOnClose
destroyOnHidden
width="80%"
>
<RbacWrapperComponent action="shop:reportcenter">

View File

@@ -1,6 +1,6 @@
import { Button, Col, Form, Input, Row, Select, Space, Switch, Typography } from "antd";
import axios from "axios";
import React, { useState } from "react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -8,16 +8,16 @@ import { calculateScheduleLoad } from "../../redux/application/application.actio
import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateFormatter } from "../../utils/DateFormatter";
import dayjs from "../../utils/day";
import BlurWrapper from "../feature-wrapper/blur-wrapper.component";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import EmailInput from "../form-items-formatted/email-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
import ScheduleDayViewContainer from "../schedule-day-view/schedule-day-view.container";
import ScheduleExistingAppointmentsList from "../schedule-existing-appointments-list/schedule-existing-appointments-list.component";
import "./schedule-job-modal.scss";
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
import BlurWrapper from "../feature-wrapper/blur-wrapper.component";
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
import "./schedule-job-modal.scss";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -60,10 +60,12 @@ export function ScheduleJobModalComponent({
const totalHours =
lbrHrsData.jobs_by_pk.labhrs.aggregate.sum.mod_lb_hrs + lbrHrsData.jobs_by_pk.larhrs.aggregate.sum.mod_lb_hrs;
if (values.start && !values.scheduled_completion)
form.setFieldsValue({
scheduled_completion: dayjs(values.start).businessDaysAdd(totalHours / bodyshop.target_touchtime, "day")
});
if (values.start && !values.scheduled_completion) {
const addDays = bodyshop.ss_configuration.nobusinessdays
? dayjs(values.start).add(totalHours / (bodyshop.target_touchtime || 1), "day")
: dayjs(values.start).businessDaysAdd(totalHours / (bodyshop.target_touchtime || 1), "day");
form.setFieldsValue({ scheduled_completion: addDays });
}
}
};

View File

@@ -209,7 +209,7 @@ export function ScheduleJobModalContainer({
onOk={() => form.submit()}
width={"90%"}
maskClosable={false}
destroyOnClose
destroyOnHidden
okButtonProps={{
loading: loading
}}

View File

@@ -106,7 +106,7 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
<>
<Modal
open={state.open}
destroyOnClose
destroyOnHidden
width="80%"
closable={false}
cancelButtonProps={{ style: { display: "none" } }}

View File

@@ -1,4 +1,3 @@
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Button, Card, Tabs } from "antd";
import React from "react";
@@ -24,6 +23,8 @@ import ShopInfoRoGuard from "./shop-info.roguard.component";
import ShopInfoIntellipay from "./shop-intellipay-config.component";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
import ShopInfoNotificationsAutoadd from "./shop-info.notifications-autoadd.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -41,6 +42,7 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
names: ["CriticalPartsScanning", "Enhanced_Payroll"],
splitKey: bodyshop.imexshopid
});
const { scenarioNotificationsOn } = useSocket();
const { t } = useTranslation();
const history = useNavigate();
@@ -137,9 +139,21 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
{
key: "intellipay",
label: InstanceRenderManager({ rome: t("bodyshop.labels.romepay"), imex: t("bodyshop.labels.imexpay") }),
label: InstanceRenderManager({
rome: t("bodyshop.labels.romepay"),
imex: t("bodyshop.labels.imexpay")
}),
children: <ShopInfoIntellipay form={form} />
}
},
...(scenarioNotificationsOn
? [
{
key: "notifications_autoadd",
label: t("bodyshop.labels.notifications.followers"),
children: <ShopInfoNotificationsAutoadd form={form} bodyshop={bodyshop} />
}
]
: [])
];
return (
<Card

View File

@@ -0,0 +1,25 @@
import { Typography } from "antd";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import PhoneNumberConsentList from "../phone-number-consent/phone-number-consent.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({});
function ShopInfoConsentComponent({ bodyshop }) {
const { t } = useTranslation();
return (
<div>
<Typography.Title level={4}>{t("settings.title")}</Typography.Title>
{<PhoneNumberConsentList bodyshop={bodyshop} />}
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoConsentComponent);

View File

@@ -906,6 +906,7 @@ export function ShopInfoGeneral({ form, bodyshop }) {
add();
}}
style={{ width: "100%" }}
id="insurancecos-add-button"
>
{t("general.actions.add")}
</Button>

View File

@@ -0,0 +1,57 @@
import { Form, Typography } from "antd";
import { useTranslation } from "react-i18next";
import EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component.jsx";
const { Text, Paragraph } = Typography;
export default function ShopInfoNotificationsAutoadd({ bodyshop }) {
const { t } = useTranslation();
// Filter employee options to ensure active employees with valid IDs
const employeeOptions = bodyshop?.employees?.filter((e) => e.active && e.user_email && e.id) || [];
return (
<div>
<Paragraph>{t("bodyshop.fields.notifications.description")}</Paragraph>
<Text type="secondary">{t("bodyshop.labels.notifications.followers")}</Text>
{employeeOptions.length > 0 ? (
<Form.Item
name="notification_followers"
rules={[
{
type: "array",
message: t("general.validation.array")
},
{
validator: async (_, value) => {
if (!value || value.length === 0) {
return Promise.resolve(); // Allow empty array
}
const hasInvalid = value.some((id) => id == null || typeof id !== "string" || id.trim() === "");
if (hasInvalid) {
return Promise.reject(new Error(t("bodyshop.fields.notifications.invalid_followers")));
}
return Promise.resolve();
}
}
]}
>
<EmployeeSearchSelectComponent
style={{ minWidth: "100%" }}
mode="multiple"
options={employeeOptions}
placeholder={t("bodyshop.fields.notifications.placeholder")}
showEmail={true}
onChange={(value) => {
// Filter out null or invalid values before passing to Form
const cleanedValue = value?.filter((id) => id != null && typeof id === "string" && id.trim() !== "");
return cleanedValue;
}}
/>
</Form.Item>
) : (
<Text type="secondary">{t("bodyshop.fields.no_employees_available")}</Text>
)}
</div>
);
}

View File

@@ -1,16 +1,15 @@
import { DeleteFilled } from "@ant-design/icons";
import { Button, Divider, Form, Input, InputNumber, Select, Space, Switch, TimePicker } 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 { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import ColorpickerFormItemComponent from "../form-items-formatted/colorpicker-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 { ColorPicker } from "./shop-info.rostatus.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
@@ -78,6 +77,13 @@ export function ShopInfoSchedulingComponent({ form, bodyshop }) {
>
<InputNumber min={0} />
</Form.Item>
<Form.Item
name={["ss_configuration", "nobusinessdays"]}
label={t("bodyshop.fields.ss_configuration.nobusinessdays")}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name={["md_lost_sale_reasons"]}
label={t("bodyshop.fields.md_lost_sale_reasons")}

View File

@@ -25,23 +25,6 @@ export function ShopTemplateTestRender({ bodyshop, query, emailEditorRef, style
emailEditorRef.current.exportHtml(async (data) => {
try {
// const inlineHtml = await axios.post("/render/inlinecss", {
// html: data.html,
// url: `${window.location.protocol}://${window.location.host}/`,
// });
// const { data: contextData } = await client.query({
// query: gql(query),
// variables: variables,
//
// });
// const renderResponse = await axios.post("/render", {
// view: inlineHtml.data,
// context: { ...contextData, bodyshop: bodyshop },
// });
// displayTemplateInWindowNoprint(renderResponse.data);
setLoading(false);
} catch (error) {
setLoading(false);

View File

@@ -275,7 +275,7 @@ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, to
toggleModalVisible();
}}
okButtonProps={{ disabled: !isTouched }}
destroyOnClose
destroyOnHidden
>
<Form
form={form}

View File

@@ -70,7 +70,7 @@ export function TechLookupJobsDrawer({ bodyshop, setPrintCenterContext }) {
};
return (
<Drawer open={!!selected} destroyOnClose width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
<Drawer open={!!selected} destroyOnHidden width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
{loading ? <LoadingSpinner /> : null}
{error ? <AlertComponent message={error.message} type="error" /> : null}
{data ? (

View File

@@ -39,7 +39,7 @@ export function TimeTicketListTeamPay({ bodyshop, context, actions }) {
return (
<>
<Modal width={"80%"} open={visible} destroyOnClose onOk={handleOk} onCancel={() => setVisible(false)}>
<Modal width={"80%"} open={visible} destroyOnHidden onOk={handleOk} onCancel={() => setVisible(false)}>
<Form layout="vertical" form={form} initialValues={{ jobid: jobId }}>
<LayoutFormRow grow noDivider>
<Form.Item shouldUpdate>

View File

@@ -1,7 +1,6 @@
import { useLazyQuery } from "@apollo/client";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Form, Input, InputNumber, Select, Switch } from "antd";
import React from "react";
import { Card, Form, Input, InputNumber, Select, Space, Switch } from "antd";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -19,6 +18,7 @@ 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 JobEmployeeAssignmentsContainer from "./../job-employee-assignments/job-employee-assignments.container";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -319,10 +319,15 @@ export function TimeTicketModalComponent({
}
export function LaborAllocationContainer({ jobid, loading, lineTicketData, hideTimeTickets = false }) {
const { t } = useTranslation();
if (loading) return <LoadingSkeleton />;
if (!lineTicketData) return null;
if (!jobid) return null;
return (
<div>
<Space direction="vertical" style={{ width: "100%" }}>
<Card style={{ height: "100%" }} title={t("jobs.labels.employeeassignments")}>
<JobEmployeeAssignmentsContainer job={lineTicketData.jobs_by_pk} />
</Card>
<LaborAllocationsTable
jobId={jobid}
joblines={lineTicketData.joblines}
@@ -332,6 +337,6 @@ export function LaborAllocationContainer({ jobid, loading, lineTicketData, hideT
{!hideTimeTickets && (
<TimeTicketList loading={loading} timetickets={jobid ? lineTicketData.timetickets : []} techConsole />
)}
</div>
</Space>
);
}

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